Files
sx/examples/130-xx-value-routes-through-context-allocator.sx
agra b710a0a42a 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).
2026-05-25 15:23:13 +03:00

45 lines
1.3 KiB
Plaintext

// Phase 1.1 — the compiler-internal heap-copy that backs `xx <rvalue>`
// protocol erasure must dispatch through `context.allocator`, not call
// libc malloc directly. So when a `push Context.{ allocator = tracer }`
// block is active, a `xx StructLiteral.{}` inside it MUST be allocated
// by the tracker.
//
// Note: `xx` only heap-copies for RVALUES (struct literals, call results).
// `xx <lvalue>` (an identifier, field access, index, or deref) borrows
// the operand's storage, so it never allocates and never reaches this
// path. See specs.md §3 — Protocol value ownership and lifetime.
#import "modules/std.sx";
Tracer :: struct {
count: s64;
init :: () -> *Tracer {
t : *Tracer = xx malloc(size_of(Tracer));
t.count = 0;
t;
}
}
impl Allocator for Tracer {
alloc :: (self: *Tracer, size: s64) -> *void {
self.count += 1;
return malloc(size);
}
dealloc :: (self: *Tracer, ptr: *void) {
free(ptr);
}
}
ByValue :: struct { x: s64; y: s64; }
main :: () -> s32 {
tracer := Tracer.init();
push Context.{ allocator = xx tracer, data = null } {
// Struct-literal operand: rvalue → heap-copy through context.allocator.
ignore : Allocator = xx ByValue.{ x = 1, y = 2 };
_ = ignore;
}
print("Tracer.count = {}\n", tracer.count);
0;
}