fix: propagate union-member type to a struct-literal RHS

Assigning a struct literal to a named-struct member of a plain union
(`u.b = .{ ... }`) lowered the RHS as .unresolved and tripped the
LLVM-emission tripwire: lowerAssignment's .field_access target-type
path used getStructFields, which returns nothing for a union, so the
literal never received its target type.

Unify the lvalue field matcher into a pure fieldLvalueResolve consumed
by both fieldLvaluePtr (GEP builder) and the target-type path, so the
store slot and the RHS target type can't diverge (covers union direct +
promoted members, tuple/vector lanes, and structs).

Resolves issue 0133 (depended on 0135). Regression test: examples/0184.
Notes the now end-to-end union path in issue 0132.
This commit is contained in:
agra
2026-06-13 18:55:41 +03:00
parent 8c47268539
commit 4d32a4d4fb
9 changed files with 325 additions and 67 deletions

View File

@@ -1607,6 +1607,7 @@ pub const Lowering = struct {
pub const lowerConstDecl = lower_stmt.lowerConstDecl;
pub const lowerReturn = lower_stmt.lowerReturn;
pub const lowerAssignment = lower_stmt.lowerAssignment;
pub const fieldLvalueResolve = lower_stmt.fieldLvalueResolve;
pub const fieldLvaluePtr = lower_stmt.fieldLvaluePtr;
pub const lowerExprAsPtr = lower_stmt.lowerExprAsPtr;
pub const storeOrCompound = lower_stmt.storeOrCompound;

View File

@@ -640,15 +640,15 @@ pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void {
const pinfo = self.module.types.get(obj_ty_raw);
break :blk if (pinfo == .pointer) pinfo.pointer.pointee else obj_ty_raw;
} else obj_ty_raw;
if (!obj_ty.isBuiltin()) {
const field_name_id = self.module.types.internString(fa.field);
const struct_fields = self.getStructFields(obj_ty);
for (struct_fields) |f| {
if (f.name == field_name_id) {
self.target_type = f.ty;
break;
}
}
// Resolve the LHS member's type via the SAME resolver the lvalue-
// pointer path uses (fieldLvalueResolve), so the RHS target type
// and the store slot can't diverge. Covers union/tagged-union
// direct + promoted members, tuple/vector lanes, and structs —
// not just structs (a plain getStructFields loop returned nothing
// for a union member, leaving a struct-literal RHS untyped →
// struct_init.ty == .unresolved → LLVM-emission panic; issue 0133).
if (self.fieldLvalueResolve(obj_ty, fa.field)) |res| {
self.target_type = res.valueType();
}
}
}
@@ -837,34 +837,59 @@ pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void {
const FieldLvalue = struct { ptr: Ref, ty: TypeId };
/// Resolve `obj.field` — where `obj_ptr` already points at the aggregate —
/// to a typed pointer into the field's storage plus the field's value type.
/// Pure description of which slot `obj.field` resolves to — the GEP path plus
/// the field's value type — computed WITHOUT emitting any IR. The single
/// field-matching resolver for the LVALUE/WRITE paths: `fieldLvaluePtr` builds
/// GEPs from it, and the assignment target-type path reads `.valueType()` from
/// it, so the lvalue-pointer path and the RHS target-type path can never
/// disagree on which field (or what type) a name resolves to — the two-resolver
/// defect class this codebase keeps burning on. To handle a new aggregate
/// shape, add an arm here and a matching GEP arm in `fieldLvaluePtr`; both fail
/// to compile until the union is exhaustive, forcing the two to stay in lockstep.
///
/// NOTE: the READ path (`lowerFieldAccess`, expr.zig) and the TYPE-INFER path
/// (`ExprTyper.inferType`, expr_typer.zig) still carry their OWN parallel field
/// matchers (emitting `union_get`/`enum_payload`/`struct_get` value reads, and
/// returning a bare `TypeId`, respectively). They are not yet routed through
/// here, so a new aggregate shape must currently be taught to all three. Folding
/// read + infer onto this resolver (switching the descriptor to value-read ops /
/// `.valueType()`) would make it the genuine compiler-wide single matcher.
const FieldResolution = union(enum) {
/// Direct union/tagged-union member: union_gep(index) into the aggregate.
union_direct: struct { index: u32, ty: TypeId },
/// Promoted member of an anonymous-struct union variant: union_gep into
/// the variant struct `variant_ty`, then struct_gep into the member.
union_promoted: struct { variant_index: u32, variant_ty: TypeId, member_index: u32, ty: TypeId },
/// Tuple element / vector lane / plain struct field: a single
/// struct_gep(index) into the aggregate.
indexed: struct { index: u32, ty: TypeId },
/// The field's value type — what the caller coerces the rhs to / sets as
/// the RHS target type. Identical regardless of the GEP path taken.
fn valueType(self: FieldResolution) TypeId {
return switch (self) {
.union_direct => |u| u.ty,
.union_promoted => |u| u.ty,
.indexed => |s| s.ty,
};
}
};
/// Match `obj.field` against the aggregate `obj_ty` and return the resolution
/// descriptor, or null when no field matches (the caller emits the
/// field-not-found diagnostic). Emits NO IR — see `FieldResolution`.
///
/// Handles union direct fields, promoted anonymous-struct union members,
/// tuple elements (numeric or named), vector lanes (`.x`/`.y`/`.z`/`.w` and
/// the colour aliases), and plain struct fields. Returns null when no field
/// matches; the caller emits the field-not-found diagnostic.
///
/// `ptr`'s IR type is `*field_ty` (a pointer to the field), NOT the field
/// value type: `emitStore` reads the store-target pointer's IR type and
/// unwraps one `.pointer` level to find the stored value's type. Labelling
/// the GEP with the bare field type instead would make a field whose own
/// type is a pointer-to-aggregate (`*Pair`) coerce the stored pointer into
/// the aggregate (closure auto-promotion in `coerceArg`), storing an
/// oversized struct that clobbers the neighbouring field. `.ty` carries the
/// field's value type for the caller's coercion.
///
/// Single source of lvalue field resolution shared by all three store/
/// address-of sites — lowerAssignment (single-target store), lowerExprAsPtr
/// (address-of), and lowerMultiAssign (multi-target store) — so they never
/// resolve a field to a different slot or default field 0.
pub fn fieldLvaluePtr(self: *Lowering, obj_ptr: Ref, obj_ty: TypeId, field: []const u8) ?FieldLvalue {
/// the colour aliases), and plain struct fields.
pub fn fieldLvalueResolve(self: *Lowering, obj_ty: TypeId, field: []const u8) ?FieldResolution {
if (obj_ty.isBuiltin()) return null;
const field_name_id = self.module.types.internString(field);
const type_info = self.module.types.get(obj_ty);
// Union / tagged-union: variants overlay at offset 0. A direct field is
// a union_gep; a promoted anonymous-struct member is a union_gep into
// the variant followed by a struct_gep into the member.
// Union / tagged-union: variants overlay at offset 0. A direct field is a
// union_gep; a promoted anonymous-struct member is a union_gep into the
// variant followed by a struct_gep into the member.
const union_fields: ?[]const types.TypeInfo.StructInfo.Field = switch (type_info) {
.@"union" => |u| u.fields,
.tagged_union => |u| u.fields,
@@ -873,17 +898,14 @@ pub fn fieldLvaluePtr(self: *Lowering, obj_ptr: Ref, obj_ty: TypeId, field: []co
if (union_fields) |fields| {
for (fields, 0..) |f, i| {
if (f.name == field_name_id) {
const ptr = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty));
return .{ .ptr = ptr, .ty = f.ty };
return .{ .union_direct = .{ .index = @intCast(i), .ty = f.ty } };
}
if (!f.ty.isBuiltin()) {
const fi = self.module.types.get(f.ty);
if (fi == .@"struct") {
for (fi.@"struct".fields, 0..) |sf, si| {
if (sf.name == field_name_id) {
const ug = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty));
const ptr = self.builder.structGepTyped(ug, @intCast(si), self.module.types.ptrTo(sf.ty), f.ty);
return .{ .ptr = ptr, .ty = sf.ty };
return .{ .union_promoted = .{ .variant_index = @intCast(i), .variant_ty = f.ty, .member_index = @intCast(si), .ty = sf.ty } };
}
}
}
@@ -909,9 +931,7 @@ pub fn fieldLvaluePtr(self: *Lowering, obj_ptr: Ref, obj_ty: TypeId, field: []co
}
}
if (elem_idx) |idx| {
const elem_ty = tup.fields[idx];
const ptr = self.builder.structGepTyped(obj_ptr, @intCast(idx), self.module.types.ptrTo(elem_ty), obj_ty);
return .{ .ptr = ptr, .ty = elem_ty };
return .{ .indexed = .{ .index = @intCast(idx), .ty = tup.fields[idx] } };
}
return null;
}
@@ -921,22 +941,58 @@ pub fn fieldLvaluePtr(self: *Lowering, obj_ptr: Ref, obj_ty: TypeId, field: []co
// non-lane field on a vector is a genuine miss (caller diagnoses).
if (type_info == .vector) {
const vidx = Lowering.vectorLaneIndex(field) orelse return null;
const elem_ty = type_info.vector.element;
const ptr = self.builder.structGepTyped(obj_ptr, vidx, self.module.types.ptrTo(elem_ty), obj_ty);
return .{ .ptr = ptr, .ty = elem_ty };
return .{ .indexed = .{ .index = vidx, .ty = type_info.vector.element } };
}
// Plain struct field.
const struct_fields = self.getStructFields(obj_ty);
for (struct_fields, 0..) |f, i| {
if (f.name == field_name_id) {
const ptr = self.builder.structGepTyped(obj_ptr, @intCast(i), self.module.types.ptrTo(f.ty), obj_ty);
return .{ .ptr = ptr, .ty = f.ty };
return .{ .indexed = .{ .index = @intCast(i), .ty = f.ty } };
}
}
return null;
}
/// Resolve `obj.field` — where `obj_ptr` already points at the aggregate —
/// to a typed pointer into the field's storage plus the field's value type.
/// Delegates the field MATCH to `fieldLvalueResolve` (shared with the RHS
/// target-type path) and only builds the GEP(s) here. Returns null when no
/// field matches; the caller emits the field-not-found diagnostic.
///
/// `ptr`'s IR type is `*field_ty` (a pointer to the field), NOT the field
/// value type: `emitStore` reads the store-target pointer's IR type and
/// unwraps one `.pointer` level to find the stored value's type. Labelling
/// the GEP with the bare field type instead would make a field whose own
/// type is a pointer-to-aggregate (`*Pair`) coerce the stored pointer into
/// the aggregate (closure auto-promotion in `coerceArg`), storing an
/// oversized struct that clobbers the neighbouring field. `.ty` carries the
/// field's value type for the caller's coercion.
///
/// Single source of lvalue field GEP-building shared by all three store/
/// address-of sites — lowerAssignment (single-target store), lowerExprAsPtr
/// (address-of), and lowerMultiAssign (multi-target store); the field MATCH
/// itself is delegated to `fieldLvalueResolve` (above), so they never resolve
/// a field to a different slot or default field 0.
pub fn fieldLvaluePtr(self: *Lowering, obj_ptr: Ref, obj_ty: TypeId, field: []const u8) ?FieldLvalue {
const res = self.fieldLvalueResolve(obj_ty, field) orelse return null;
switch (res) {
.union_direct => |u| {
const ptr = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = u.index, .base_type = obj_ty } }, self.module.types.ptrTo(u.ty));
return .{ .ptr = ptr, .ty = u.ty };
},
.union_promoted => |u| {
const ug = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = u.variant_index, .base_type = obj_ty } }, self.module.types.ptrTo(u.variant_ty));
const ptr = self.builder.structGepTyped(ug, u.member_index, self.module.types.ptrTo(u.ty), u.variant_ty);
return .{ .ptr = ptr, .ty = u.ty };
},
.indexed => |s| {
const ptr = self.builder.structGepTyped(obj_ptr, s.index, self.module.types.ptrTo(s.ty), obj_ty);
return .{ .ptr = ptr, .ty = s.ty };
},
}
}
/// Get the pointer (alloca ref) for an lvalue expression, without loading.
pub fn lowerExprAsPtr(self: *Lowering, node: *const Node) Ref {
switch (node.data) {