lang: xx <lvalue> borrows the operand's storage instead of heap-copying

`xx <struct-typed local>` used to heap-copy the value through context.allocator.
The protocol value's `ctx` pointed at the heap copy; the original local was
left behind, untouched. Mutations through the protocol never reached the
original, and direct reads of the original never saw protocol mutations.
Two-fork bug, silent, easy to write by mistake.

New rule (Option 3 in the discussion):

- `xx <lvalue>` — identifier, field access, index expression, deref —
  borrows the operand's storage. No heap copy, no `free` needed.
- `xx <rvalue>` — struct literal, function-call result, arithmetic, etc. —
  heap-copies through context.allocator. Unchanged from today.
- `xx @ptr` and `xx <pointer-typed value>` — borrows the pointee. Unchanged.

Single switch in `buildProtocolErasure` ([lower.zig:10334](src/ir/lower.zig#L10334))
gated by a new `isLvalueExpr` helper ([lower.zig:10322](src/ir/lower.zig#L10322)).
Struct-typed operand: if the AST shape is identifier/field/index/deref,
emit `lowerExprAsPtr(operand_node)` and skip the heap-copy; otherwise
keep the alloca-store-heap_copy path.

specs.md §3 ownership table extended to three rows (rvalue, lvalue,
pointer) with examples and rationale per row.

Regressions:

- `examples/130-xx-value-routes-through-context-allocator.sx` — the
  Phase 1.1 witness for heap-copy-via-context-allocator. Previous shape
  (`xx <local-value>`) is now a borrow under Option 3 and no longer
  exercises the heap-copy path. Rewritten to use a struct literal
  (`xx ByValue.{...}`) which still heap-copies through context.allocator
  — Tracer.count = 1 as before.
- `examples/135-xx-lvalue-borrows.sx` — new test. Dereferences a
  TrackingAllocator into a stack value, does `xx tracker` inside a
  push Context, and asserts alloc_count/dealloc_count on the LOCAL go
  up. Under old semantics this would have stayed at 0 (heap copy got
  the increments, local stayed stale).

157/157 example tests pass; chess clean on macOS / iOS sim / Android
(`tools/verify-step.sh` ran green immediately before this work).
This commit is contained in:
agra
2026-05-25 15:23:13 +03:00
parent 82e7b04cca
commit b710a0a42a
7 changed files with 135 additions and 23 deletions

View File

@@ -5,6 +5,28 @@ Tracking checkpoint for the mem.sx Zig-aligned implementation
## Last completed step
- **`xx <lvalue>` borrows the operand's storage** (Option 3 in the
protocol-erasure design discussion). Today's behavior — `xx
<struct-typed local>` heap-copies the value — was a silent footgun:
the protocol value pointed at the heap copy, the original local
stayed stale, mutations through the protocol weren't visible to the
original (and vice versa). Under the new rule, when the operand
names existing storage (identifier, field access, index expression,
dereferenced pointer), `xx` takes its address and the protocol
borrows. Heap-copy is reserved for `xx <rvalue>` — struct literals,
function-call results, arithmetic expressions, anything without its
own storage.
Single point of change at `buildProtocolErasure` in `lower.zig:10334`,
via a new `isLvalueExpr` helper at `lower.zig:10322`. specs.md §3
ownership table updated. The `examples/130-...` regression that
previously tested heap-copy on `xx <local>` now tests `xx
<struct-literal>` (still the heap-copy path); new regression
`examples/135-xx-lvalue-borrows.sx` witnesses the borrow path via
TrackingAllocator. 157/157 example tests + chess clean across all
three platforms (`tools/verify-step.sh` gate ran green right
before this work landed).
- **Phase 1.4 — `valueToLLVMConst` upgraded to handle every interp
`Value` variant.** The serializer at `emit_llvm.zig:734` used to
collapse anything past int/float/boolean into `LLVMConstNull(ty)`
@@ -220,7 +242,19 @@ Allocator value naturally.
## Log
- **2026-05-25 (latest)** — Phase 1.4 shipped. `valueToLLVMConst`
- **2026-05-25 (latest)** — `xx <lvalue>` semantics changed to borrow.
Single change at `lower.zig:10334` (`buildProtocolErasure`) gated by
new `isLvalueExpr` helper at `lower.zig:10322`. specs.md §3
ownership table extended (three modes: rvalue / lvalue / pointer).
`examples/130-xx-value-routes-through-context-allocator.sx` updated
to use a struct literal (rvalue) as the operand — the heap-copy
routing through `context.allocator` is what Phase 1.1 actually
proves, and that path is still active for rvalues. New regression
at `examples/135-xx-lvalue-borrows.sx` witnesses the borrow path
via TrackingAllocator counts on the local. 157/157 + chess green
on all three platforms (`tools/verify-step.sh` ran green
immediately before this).
- **2026-05-25 (penultimate)** — Phase 1.4 shipped. `valueToLLVMConst`
(`emit_llvm.zig:734`) replaced the primitive-only switch with a
full serializer covering null_val, void_val, undef, func_ref,
string, and aggregate (struct + array via