fix(ir): missing-field multi-assign + promoted-union-member lvalue [F0.10]
Completes the issue-0094 fix. attempt-1 made single-assign and address-of diagnose a missing struct field; the stress-review found two remaining defects in that change: 1. lowerMultiAssign's `.field_access` target kept the pre-fix shape — a struct-only loop that defaulted `field_idx 0` / `field_ty .unresolved` on a miss, then built the GEP and stored unconditionally. A missing field (`p.q, y = 2, 3`) silently wrote field 0 (printed `x=2 y=3`, no diagnostic), and a valid promoted-union / tuple member at a non-zero offset corrupted field 0 instead of its own slot. 2. attempt-1's new union branch in lowerExprAsPtr resolved only DIRECT union field names, so `@v.x` on a promoted anonymous-struct member reported "field 'x' not found on type 'Vec2'" even though `v.x = 41` worked. Both lvalue-pointer sites and the multi-assign store now route through one shared resolver, `fieldLvaluePtr`, that handles struct fields, union direct fields, promoted anonymous-struct union members, and tuple elements, and returns null (no field-0 / `.unresolved` default) on a genuine miss. Each caller emits the read path's `emitFieldError` on null. This collapses the three previously-divergent field-lvalue walks into one, fixing the multi-assign missing-field corruption, the promoted-member over-rejection, and (as a side effect of correct resolution) non-zero-offset promoted-union and tuple multi-assign stores. The types.zig tripwire is untouched. Regression tests: - examples/1145 extended: multi-assign missing field (`p.r, y`) errors, exit 1. - examples/0166 (new): promoted union member written and address-of'd, including a non-zero-offset member (`@v.y`), compiles and runs. - src/ir/lower.test.zig: multi-assign missing-field field-not-found unit test.
This commit is contained in:
@@ -1121,6 +1121,59 @@ test "lower: assigning to a missing struct field emits field-not-found, no panic
|
||||
try std.testing.expect(found);
|
||||
}
|
||||
|
||||
test "lower: multi-assign to a missing struct field emits field-not-found, no corruption (issue 0094)" {
|
||||
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();
|
||||
var diags = errors.DiagnosticList.init(alloc, "", "test.sx");
|
||||
defer diags.deinit();
|
||||
|
||||
// Register `Point :: struct { x: s64; }` so the struct literal resolves.
|
||||
const fields = [_]ir_mod.types.TypeInfo.StructInfo.Field{
|
||||
.{ .name = module.types.internString("x"), .ty = .s64 },
|
||||
};
|
||||
_ = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Point"), .fields = &fields } });
|
||||
|
||||
const span = ast.Span{ .start = 0, .end = 0 };
|
||||
|
||||
// main :: () { p := Point.{ x = 1 }; y := 0; p.r, y = 3, 4; } — `r` is not a field of Point.
|
||||
var x_val = Node{ .span = span, .data = .{ .int_literal = .{ .value = 1 } } };
|
||||
const field_inits = [_]ast.StructFieldInit{.{ .name = "x", .value = &x_val }};
|
||||
var lit = Node{ .span = span, .data = .{ .struct_literal = .{ .struct_name = "Point", .field_inits = &field_inits } } };
|
||||
var decl = Node{ .span = span, .data = .{ .var_decl = .{ .name = "p", .name_span = span, .type_annotation = null, .value = &lit } } };
|
||||
|
||||
var y_init = Node{ .span = span, .data = .{ .int_literal = .{ .value = 0 } } };
|
||||
var y_decl = Node{ .span = span, .data = .{ .var_decl = .{ .name = "y", .name_span = span, .type_annotation = null, .value = &y_init } } };
|
||||
|
||||
var p_ident = Node{ .span = span, .data = .{ .identifier = .{ .name = "p" } } };
|
||||
var target0 = Node{ .span = span, .data = .{ .field_access = .{ .object = &p_ident, .field = "r" } } };
|
||||
var target1 = Node{ .span = span, .data = .{ .identifier = .{ .name = "y" } } };
|
||||
var v0 = Node{ .span = span, .data = .{ .int_literal = .{ .value = 3 } } };
|
||||
var v1 = Node{ .span = span, .data = .{ .int_literal = .{ .value = 4 } } };
|
||||
const targets = [_]*Node{ &target0, &target1 };
|
||||
const values = [_]*Node{ &v0, &v1 };
|
||||
var massign = Node{ .span = span, .data = .{ .multi_assign = .{ .targets = &targets, .values = &values } } };
|
||||
|
||||
const stmts = [_]*Node{ &decl, &y_decl, &massign };
|
||||
var body = Node{ .span = span, .data = .{ .block = .{ .stmts = &stmts } } };
|
||||
const fd = ast.FnDecl{ .name = "main", .params = &.{}, .return_type = null, .body = &body };
|
||||
|
||||
var lowering = Lowering.init(&module);
|
||||
lowering.diagnostics = &diags;
|
||||
// Pre-fix the struct-only loop defaulted field_idx 0 / field_ty .unresolved on
|
||||
// a miss, silently storing into field 0 (no diagnostic); the fix resolves the
|
||||
// target via the shared fieldLvaluePtr and bails with field-not-found.
|
||||
lowering.lowerFunction(&fd, "main", false);
|
||||
|
||||
var found = false;
|
||||
for (diags.items.items) |d| {
|
||||
if (d.level == .err and std.mem.indexOf(u8, d.message, "field 'r' not found on type 'Point'") != null) 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);
|
||||
|
||||
Reference in New Issue
Block a user