fix(ir): single-assign field store delegates to fieldLvaluePtr, completing the lvalue consolidation [F0.10]

Migrate lowerAssignment's `.field_access` target onto the shared
`fieldLvaluePtr` resolver, deleting its duplicated union / promoted /
tuple / vector / struct walk. All three lvalue field-store sites —
single-assign, address-of (lowerExprAsPtr), and multi-assign
(lowerMultiAssign) — now resolve through the one resolver, removing the
issue-0083 two-resolver divergence.

Fold vector-lane resolution into `fieldLvaluePtr` (reusing
vectorLaneIndex) so the single resolver covers struct fields, union
direct fields, promoted anonymous-struct union members, tuple elements,
and vector lanes — null only on a genuine miss, which every caller turns
into the read path's `emitFieldError` diagnostic.

`fieldLvaluePtr` now types every field GEP `*field_ty` (the convention
the single-assign path always used), not the bare field value type:
emitStore unwraps one pointer level to find the stored value's type.
The earlier lowerExprAsPtr / lowerMultiAssign walks typed the GEP with
the bare field type, so a field whose own type is a pointer-to-aggregate
(`*Pair`, a two-pointer struct) made emitStore unwrap to the aggregate
and coerceArg's closure auto-promotion store a 16-byte `{ptr,null}`
struct over the 8-byte slot, clobbering the neighbouring field.
Consolidating onto the one `*field_ty` resolver preserves single-assign
and fixes that pre-existing multi-assign / address-of clobber.

The types.zig `.unresolved` tripwire is untouched; no `.s64` / `.void` /
`.unresolved` default remains.

Regression: examples/0167-types-ptr-to-aggregate-field-store.sx (a
`*Pair` field stored via all three lvalue sites leaves the neighbour
intact) + a lowering unit test asserting the `*field_ty` GEP convention.
This commit is contained in:
agra
2026-06-05 14:40:06 +03:00
parent c98bebc4e3
commit ed7665f8ae
7 changed files with 188 additions and 145 deletions

View File

@@ -1174,6 +1174,74 @@ test "lower: multi-assign to a missing struct field emits field-not-found, no co
try std.testing.expect(found);
}
test "lower: shared resolver types a pointer-typed field GEP as *field_ty, not field_ty (issue 0094 clobber)" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = ir_mod.Module.init(alloc);
defer module.deinit();
const span = ast.Span{ .start = 0, .end = 0 };
// Register `S :: struct { p: *s64; }` — the field's own type is a pointer.
const ptr_s64 = module.types.ptrTo(.s64);
const fields = [_]ir_mod.types.TypeInfo.StructInfo.Field{
.{ .name = module.types.internString("p"), .ty = ptr_s64 },
};
_ = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("S"), .fields = &fields } });
// mutate :: (s: *S, q: *s64) { d := 0; s.p, d = q, 1; }
// The multi-assign target routes `s.p` through the shared fieldLvaluePtr
// resolver. Pre-fix that resolver typed the field GEP with the bare field
// value type (`*s64`), so emitStore unwrapped one level to `s64` and
// coerceArg's closure auto-promotion stored a 16-byte struct over the
// 8-byte field, clobbering the neighbour. The resolver now types the GEP
// `*(*s64)` so emitStore stops at the field's own pointer type.
var s_pointee = Node{ .span = span, .data = .{ .type_expr = .{ .name = "S", .is_generic = false } } };
var s_ty = Node{ .span = span, .data = .{ .pointer_type_expr = .{ .pointee_type = &s_pointee } } };
var q_pointee = Node{ .span = span, .data = .{ .type_expr = .{ .name = "s64", .is_generic = false } } };
var q_ty = Node{ .span = span, .data = .{ .pointer_type_expr = .{ .pointee_type = &q_pointee } } };
var d_init = Node{ .span = span, .data = .{ .int_literal = .{ .value = 0 } } };
var d_decl = Node{ .span = span, .data = .{ .var_decl = .{ .name = "d", .name_span = span, .type_annotation = null, .value = &d_init } } };
var s_ident = Node{ .span = span, .data = .{ .identifier = .{ .name = "s" } } };
var target0 = Node{ .span = span, .data = .{ .field_access = .{ .object = &s_ident, .field = "p" } } };
var target1 = Node{ .span = span, .data = .{ .identifier = .{ .name = "d" } } };
var q_rhs = Node{ .span = span, .data = .{ .identifier = .{ .name = "q" } } };
var v1 = Node{ .span = span, .data = .{ .int_literal = .{ .value = 1 } } };
const targets = [_]*Node{ &target0, &target1 };
const values = [_]*Node{ &q_rhs, &v1 };
var massign = Node{ .span = span, .data = .{ .multi_assign = .{ .targets = &targets, .values = &values } } };
const stmts = [_]*Node{ &d_decl, &massign };
var body = Node{ .span = span, .data = .{ .block = .{ .stmts = &stmts } } };
const params = [_]ast.Param{
.{ .name = "s", .name_span = span, .type_expr = &s_ty },
.{ .name = "q", .name_span = span, .type_expr = &q_ty },
};
const fd = ast.FnDecl{ .name = "mutate", .params = &params, .return_type = null, .body = &body };
var lowering = Lowering.init(&module);
lowering.lowerFunction(&fd, "mutate", false);
// The field-store GEP must be typed `*(*s64)`: its pointee is the field's
// own type (`*s64`), not the field's pointee (`s64`).
const func = module.getFunction(FuncId.fromIndex(0));
var found = false;
for (func.blocks.items) |blk| {
for (blk.insts.items) |inst| {
if (inst.op == .struct_gep) {
const info = module.types.get(inst.ty);
try std.testing.expect(info == .pointer);
try std.testing.expectEqual(ptr_s64, info.pointer.pointee);
found = true;
}
}
}
try std.testing.expect(found);
}
test "lower: reflectionArgIsType accepts spelled types, rejects plain values (issue 0090)" {
const alloc = std.testing.allocator;
var module = ir_mod.Module.init(alloc);