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:
@@ -10318,6 +10318,18 @@ pub const Lowering = struct {
|
||||
return self.builder.call(fid, final_args, ret_ty);
|
||||
}
|
||||
|
||||
/// True for expression shapes that name an addressable storage location
|
||||
/// (variables, fields, array elements, dereferenced pointers). Used by
|
||||
/// `xx <struct-typed expr>` to decide between borrow (lvalue → take the
|
||||
/// address) and heap-copy (rvalue → allocate a fresh copy).
|
||||
fn isLvalueExpr(self: *Lowering, node: *const Node) bool {
|
||||
_ = self;
|
||||
return switch (node.data) {
|
||||
.identifier, .field_access, .index_expr, .deref_expr => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Build a protocol value from a concrete value via xx conversion.
|
||||
fn buildProtocolErasure(self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId) Ref {
|
||||
const dst_info = self.module.types.get(dst_ty);
|
||||
@@ -10339,14 +10351,26 @@ pub const Lowering = struct {
|
||||
concrete_ty = pointee;
|
||||
heap_copy = false;
|
||||
} else if (src_info == .@"struct") {
|
||||
// xx acc — operand is a value, need to take address + heap-copy
|
||||
// Struct-typed operand. Split on lvalue-ness:
|
||||
// - lvalue (identifier, field, index, deref): borrow the
|
||||
// storage the operand already names. No heap copy; the
|
||||
// protocol value's ctx points at the caller's slot, and
|
||||
// mutations through the protocol are visible to the
|
||||
// original. Lifetime is the caller's responsibility.
|
||||
// - rvalue (struct literal, call result, etc.): heap-copy
|
||||
// into a fresh allocation so the protocol value is
|
||||
// self-contained and outlives this expression.
|
||||
concrete_type_name = self.module.types.getString(src_info.@"struct".name);
|
||||
concrete_ty = src_ty;
|
||||
heap_copy = true;
|
||||
// Alloca + store to get a pointer (will be heap-copied in buildProtocolValue)
|
||||
const slot = self.builder.alloca(src_ty);
|
||||
self.builder.store(slot, operand);
|
||||
concrete_ptr = slot;
|
||||
if (self.isLvalueExpr(operand_node)) {
|
||||
concrete_ptr = self.lowerExprAsPtr(operand_node);
|
||||
heap_copy = false;
|
||||
} else {
|
||||
heap_copy = true;
|
||||
const slot = self.builder.alloca(src_ty);
|
||||
self.builder.store(slot, operand);
|
||||
concrete_ptr = slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user