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:
22
examples/0184-types-union-member-struct-literal-assign.sx
Normal file
22
examples/0184-types-union-member-struct-literal-assign.sx
Normal file
@@ -0,0 +1,22 @@
|
||||
// Assigning a struct LITERAL to a named-struct member of a plain `union`.
|
||||
// `u.b = .{ code = 9 }` types the literal as the union member's struct type
|
||||
// `S` and stores it — the target type propagates to a union-member lvalue
|
||||
// exactly as it does to a struct field.
|
||||
//
|
||||
// Regression (issue 0133): the literal used to lower as `.unresolved` (the
|
||||
// target-type path only inspected struct fields, not union members) and trip
|
||||
// the LLVM-emission tripwire in emitStructInit.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
S :: struct { code: i64; }
|
||||
U :: union { a: i64; b: S; }
|
||||
|
||||
main :: () {
|
||||
u : U = ---;
|
||||
u.b = .{ code = 9 }; // union member <- struct literal
|
||||
print("code={}\n", u.b.code); // 9
|
||||
|
||||
u.a = 5; // scalar member still works
|
||||
print("a={}\n", u.a); // 5
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
code=9
|
||||
a=5
|
||||
@@ -218,3 +218,14 @@ inferred and annotated `Event` are two DIFFERENT registered types (a
|
||||
same-name shadow), and the divergence is purely that protocol signature
|
||||
registration uses a flat, visibility-unaware lookup. The payload-field
|
||||
machinery is fine once the correct `Event` reaches the binding.
|
||||
|
||||
## Follow-up (2026-06-13)
|
||||
|
||||
The broader-latent union case this fix enabled — a colliding-name union
|
||||
member now resolving to the correct type — was further blocked at codegen by
|
||||
a separate bug: assigning a struct literal to a union member lowered as
|
||||
`.unresolved` ([issue 0133](0133-union-member-struct-literal-assign-unresolved-panic.md),
|
||||
now RESOLVED, which in turn required
|
||||
[issue 0135](0135-xx-pack-index-protocol-erasure-lowers-pack-as-value.md)).
|
||||
With both fixed, the union-member-via-struct-literal path is demonstrable
|
||||
end-to-end (`examples/0184-types-union-member-struct-literal-assign.sx`).
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# 0133 — assigning a struct LITERAL to a union member panics ("unresolved type reached LLVM emission")
|
||||
|
||||
> **RESOLVED (2026-06-13).** Root cause: `lowerAssignment`'s `.field_access`
|
||||
> target-type path used `getStructFields`, which returns nothing for a
|
||||
> `union`, so a union-member LHS never set `target_type` and the RHS struct
|
||||
> literal lowered as `.unresolved` → LLVM-emission tripwire. Fix: a single
|
||||
> pure field-matching resolver `fieldLvalueResolve` (in `src/ir/lower/stmt.zig`)
|
||||
> that both `fieldLvaluePtr` (builds GEPs) and the target-type path
|
||||
> (`res.valueType()`) consume — covering union direct + promoted members,
|
||||
> tuple/vector lanes, and structs, so the lvalue-pointer path and the
|
||||
> target-type path can't diverge. Landing this required first fixing the
|
||||
> latent [issue 0135](0135-xx-pack-index-protocol-erasure-lowers-pack-as-value.md)
|
||||
> (the unified resolver newly types tuple-element LHSs, which routed
|
||||
> `examples/0540`'s `c.sources.0 = xx sources[0]` through pack-index protocol
|
||||
> erasure). Regression test:
|
||||
> [examples/0184-types-union-member-struct-literal-assign.sx](../examples/0184-types-union-member-struct-literal-assign.sx).
|
||||
> The § Confirmed fix block below records the exact patch that landed.
|
||||
|
||||
## Symptom
|
||||
|
||||
One-line: `u.b = .{ ... }` where `b` is a NAMED-struct member of a plain
|
||||
@@ -101,6 +117,177 @@ union-member-lvalue + literal-RHS combination drops it.
|
||||
> resolved, also note in issue 0132 that the broader-latent union case is
|
||||
> now demonstrable end-to-end.
|
||||
|
||||
## Confirmed fix (landed)
|
||||
|
||||
Root cause confirmed exactly as the investigation prompt hypothesized: the
|
||||
target-type path in `lowerAssignment`'s `.field_access` case used
|
||||
`getStructFields`, which returns `&.{}` for a `union` (only `.@"struct"` is
|
||||
handled). So a union-member LHS never set `self.target_type`, and the RHS
|
||||
struct literal lowered with no target → `struct_init.ty == .unresolved` →
|
||||
LLVM-emission tripwire.
|
||||
|
||||
The fix unifies the resolver (per this issue's prompt — "factor a
|
||||
`fieldLvalueType` helper … so the lvalue-pointer path and the target-type
|
||||
path cannot diverge"): a pure `fieldLvalueResolve(obj_ty, field)
|
||||
-> ?FieldResolution` matcher that both `fieldLvaluePtr` (builds GEPs) and the
|
||||
target-type path (`res.valueType()`) consume. With it, the 0133 union repro
|
||||
prints `code=9`, exit 0.
|
||||
|
||||
**Why it was blocked on 0135:** the unified matcher also resolves *tuple
|
||||
element* LHS types (not just structs, as the old `getStructFields` path did).
|
||||
That makes `c.sources.0 = xx sources[0]` in
|
||||
`examples/0540-packs-pack-type-arg-spread.sx` set `target_type = VL(i64)`,
|
||||
routing `xx sources[0]` through `buildProtocolErasure` →
|
||||
`lowerExprAsPtr(sources[0])` → the pre-existing pack-index-address-of bug
|
||||
(issue 0135). Narrowing the fix to dodge tuples would reintroduce the
|
||||
two-resolver divergence this issue explicitly set out to remove — i.e. a
|
||||
workaround — so 0135 was fixed first, then this landed unchanged.
|
||||
|
||||
The patch that landed (`src/ir/lower.zig` + `src/ir/lower/stmt.zig`):
|
||||
|
||||
```diff
|
||||
diff --git a/src/ir/lower.zig b/src/ir/lower.zig
|
||||
--- a/src/ir/lower.zig
|
||||
+++ b/src/ir/lower.zig
|
||||
@@ pub const Lowering = struct {
|
||||
pub const lowerAssignment = lower_stmt.lowerAssignment;
|
||||
+ pub const fieldLvalueResolve = lower_stmt.fieldLvalueResolve;
|
||||
pub const fieldLvaluePtr = lower_stmt.fieldLvaluePtr;
|
||||
```
|
||||
|
||||
In `src/ir/lower/stmt.zig`, replace the struct-only target-type loop in
|
||||
`lowerAssignment`'s `.field_access` branch:
|
||||
|
||||
```diff
|
||||
- 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();
|
||||
+ }
|
||||
```
|
||||
|
||||
Then refactor `fieldLvaluePtr` into a pure `FieldResolution` matcher
|
||||
(`fieldLvalueResolve`) + a thin GEP-builder. Full hunk:
|
||||
|
||||
```zig
|
||||
const FieldResolution = union(enum) {
|
||||
union_direct: struct { index: u32, ty: TypeId },
|
||||
union_promoted: struct { variant_index: u32, variant_ty: TypeId, member_index: u32, ty: TypeId },
|
||||
indexed: struct { index: u32, ty: TypeId },
|
||||
|
||||
fn valueType(self: FieldResolution) TypeId {
|
||||
return switch (self) {
|
||||
.union_direct => |u| u.ty,
|
||||
.union_promoted => |u| u.ty,
|
||||
.indexed => |s| s.ty,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
const union_fields: ?[]const types.TypeInfo.StructInfo.Field = switch (type_info) {
|
||||
.@"union" => |u| u.fields,
|
||||
.tagged_union => |u| u.fields,
|
||||
else => null,
|
||||
};
|
||||
if (union_fields) |fields| {
|
||||
for (fields, 0..) |f, i| {
|
||||
if (f.name == field_name_id) {
|
||||
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) {
|
||||
return .{ .union_promoted = .{ .variant_index = @intCast(i), .variant_ty = f.ty, .member_index = @intCast(si), .ty = sf.ty } };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type_info == .tuple) {
|
||||
const tup = type_info.tuple;
|
||||
var elem_idx: ?usize = null;
|
||||
if (std.fmt.parseInt(usize, field, 10)) |n| {
|
||||
if (n < tup.fields.len) elem_idx = n;
|
||||
} else |_| {
|
||||
if (tup.names) |names| {
|
||||
for (names, 0..) |nm, i| {
|
||||
if (nm == field_name_id and i < tup.fields.len) {
|
||||
elem_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (elem_idx) |idx| {
|
||||
return .{ .indexed = .{ .index = @intCast(idx), .ty = tup.fields[idx] } };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type_info == .vector) {
|
||||
const vidx = Lowering.vectorLaneIndex(field) orelse return null;
|
||||
return .{ .indexed = .{ .index = vidx, .ty = type_info.vector.element } };
|
||||
}
|
||||
|
||||
const struct_fields = self.getStructFields(obj_ty);
|
||||
for (struct_fields, 0..) |f, i| {
|
||||
if (f.name == field_name_id) {
|
||||
return .{ .indexed = .{ .index = @intCast(i), .ty = f.ty } };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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 };
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
(`fieldLvalueResolve` is also registered on `Lowering` in `lower.zig` — the
|
||||
first diff hunk above.) Landed after 0135; the repro moved to
|
||||
`examples/0184-types-union-member-struct-literal-assign.sx` and
|
||||
`examples/0540` stays green.
|
||||
|
||||
## Notes
|
||||
|
||||
- Tripwire site (symptom): `src/backend/llvm/types.zig:176`
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// issue 0133 — assigning a struct LITERAL to a union member panics
|
||||
// ("unresolved type reached LLVM emission").
|
||||
//
|
||||
// `u.b = .{ code = 9 }` where `b` is a named-struct member of a plain
|
||||
// `union`: the RHS struct literal never receives its target type (the
|
||||
// member's type `S`), so it lowers as `.unresolved` and trips the LLVM
|
||||
// tripwire in emitStructInit. A STRUCT-field LHS propagates the target
|
||||
// type fine; a pre-made value needs none — only the union-member-lvalue
|
||||
// + struct-literal-RHS combination drops it. PRE-EXISTING, orthogonal to
|
||||
// name resolution (reproduces with this unique, non-colliding name).
|
||||
//
|
||||
// EXPECT (today): panic. EXPECT (after fix): prints `code=9`, exit 0.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
S :: struct { code: i64; }
|
||||
U :: union { a: i64; b: S; }
|
||||
|
||||
main :: () {
|
||||
u : U = ---;
|
||||
u.b = .{ code = 9 };
|
||||
print("code={}\n", u.b.code);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user