diff --git a/examples/255-autoref-compound-lvalue.sx b/examples/255-autoref-compound-lvalue.sx new file mode 100644 index 0000000..ee546a2 --- /dev/null +++ b/examples/255-autoref-compound-lvalue.sx @@ -0,0 +1,20 @@ +// Auto-ref of a COMPOUND lvalue (field access / index / deref) passed to a +// `*T` parameter must reference the REAL lvalue, not a copy. Regression: a +// field-access argument (e.g. `make_move(self.board, m)`) silently passed the +// address of a temporary copy, so mutations through the pointer were lost with +// no diagnostic. A plain local (`bump(x)`) already auto-ref'd; now compound +// lvalues do too. Expected: w.s.v == 42 (the real field was mutated). + +#import "modules/std.sx"; + +S :: struct { v: s32; } +W :: struct { s: S; } + +bump :: (p: *S) { p.v = p.v + 41; } + +main :: () -> s32 { + w : W = .{ s = .{ v = 1 } }; + bump(w.s); // field access, no `@` — auto-refs &w.s + print("w.s.v = {}\n", w.s.v); // 42, not 1 + return 0; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index d83da96..ff10617 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -6784,6 +6784,37 @@ pub const Lowering = struct { } } } + // Implicit address-of for compound lvalues (field access / index / + // deref): when the param expects `*T` and the arg is an addressable + // lvalue of type `T`, pass the lvalue's real address (GEP) — same + // reference semantics as the identifier case above. Without this the + // arg would be loaded into a temporary and the callee would mutate a + // throwaway copy (silent data loss — e.g. `make_move(self.board, m)`). + if (ai < param_types.len and (arg.data == .field_access or arg.data == .index_expr or arg.data == .deref_expr)) { + const pt = param_types[ai]; + if (!pt.isBuiltin()) { + const pti = self.module.types.get(pt); + if (pti == .pointer and self.inferExprType(arg) == pti.pointer.pointee) { + // `lowerExprAsPtr` yields the lvalue's address, typed + // either as `*T` already (index/deref) or as the pointee + // `T` (a field "place" ref); normalize to `*T` — exactly + // what `@field_access` does. + const place = self.lowerExprAsPtr(arg); + const place_ty = self.builder.getRefType(place); + const ref: ?Ref = if (place_ty == pt) + place + else if (place_ty == pti.pointer.pointee) + self.builder.emit(.{ .addr_of = .{ .operand = place } }, pt) + else + null; + if (ref) |r| { + args.append(self.alloc, r) catch unreachable; + self.target_type = saved_target; + continue; + } + } + } + } const val = self.lowerExpr(arg); self.target_type = saved_target; // Passing a `*T` where a `T` value is expected — a by-reference loop diff --git a/tests/expected/255-autoref-compound-lvalue.exit b/tests/expected/255-autoref-compound-lvalue.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/tests/expected/255-autoref-compound-lvalue.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/tests/expected/255-autoref-compound-lvalue.txt b/tests/expected/255-autoref-compound-lvalue.txt new file mode 100644 index 0000000..01f9c06 --- /dev/null +++ b/tests/expected/255-autoref-compound-lvalue.txt @@ -0,0 +1 @@ +w.s.v = 42