fix: union struct-literal init (issue 0158)
A plain union initialized with a struct literal (b : Overlay = .{ f = 3.14 })
silently miscompiled — it fell through the generic struct-literal path
(getStructFields returns empty for a union), building a malformed structInit
whose overlapping zero-fill clobbered the named member, so it read back 0.0
(and a type-pun read segfaulted).
lowerStructLiteral now detects a plain-union target and dispatches to a new
lowerUnionLiteral, which writes each named member into a union-sized slot via
the same lvalue resolver the u.member = v assignment path uses, then loads the
union value back. Validity: the named members must share one arm — a single
direct member, or several promoted members of the same anonymous-struct variant.
Overlapping members, members from different arms, and positional union literals
are rejected with a diagnostic (no silent last-wins); an empty .{} yields an
undefined union (matching the --- form).
specs.md updated. Regressions: examples/types/0194 (valid forms) +
examples/diagnostics/1191 (overlap rejection).
This commit is contained in:
@@ -1661,6 +1661,7 @@ pub const Lowering = struct {
|
||||
pub const lowerAssignment = lower_stmt.lowerAssignment;
|
||||
pub const fieldLvalueResolve = lower_stmt.fieldLvalueResolve;
|
||||
pub const fieldLvaluePtr = lower_stmt.fieldLvaluePtr;
|
||||
pub const lowerUnionLiteral = lower_stmt.lowerUnionLiteral;
|
||||
pub const diagTaggedUnionVariantWrite = lower_stmt.diagTaggedUnionVariantWrite;
|
||||
pub const lowerExprAsPtr = lower_stmt.lowerExprAsPtr;
|
||||
pub const storeOrCompound = lower_stmt.storeOrCompound;
|
||||
|
||||
@@ -86,6 +86,15 @@ pub fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, span: a
|
||||
self.resolveTypeWithBindings(te)
|
||||
else self.target_type orelse .unresolved;
|
||||
|
||||
// Plain (untagged) union target: build by writing each named member into a
|
||||
// union-sized slot. `getStructFields` returns empty for a union, so the
|
||||
// generic struct path below would emit a malformed `structInit` whose
|
||||
// overlapping zero-fill clobbers the named member (issue 0158). Tagged
|
||||
// unions were already handled above.
|
||||
if (!ty.isBuiltin() and self.module.types.get(ty) == .@"union") {
|
||||
return self.lowerUnionLiteral(sl, ty, span);
|
||||
}
|
||||
|
||||
// Get struct field types for coercion and ordering
|
||||
const struct_fields = self.getStructFields(ty);
|
||||
|
||||
|
||||
@@ -1004,6 +1004,77 @@ pub fn fieldLvaluePtr(self: *Lowering, obj_ptr: Ref, obj_ty: TypeId, field: []co
|
||||
}
|
||||
}
|
||||
|
||||
/// Lower a plain (untagged) `union` struct-literal `.{ member = value, ... }`.
|
||||
/// The generic struct-literal path can't build a union — `getStructFields`
|
||||
/// returns empty for a union, so a union literal would fall through to a
|
||||
/// malformed `structInit` whose overlapping zero-fill clobbers the named member
|
||||
/// (issue 0158). Instead, mirror the spec's `--- `+per-field form: write each
|
||||
/// named member into an (otherwise-undefined) union-sized slot via the SAME
|
||||
/// lvalue resolver the assignment path uses, then load the union value back.
|
||||
///
|
||||
/// Validity: union members overlay one storage slot, so the named members must
|
||||
/// all belong to ONE arm — either a single direct member (`.{ f = 3.14 }`) or
|
||||
/// several promoted members of the SAME anonymous-struct variant
|
||||
/// (`.{ x = 1.0, y = 2.0 }`). Naming two direct members, or members from
|
||||
/// different arms, would silently let a later store clobber an earlier one —
|
||||
/// reject it loudly (no silent last-wins). `tagged_union`s never reach here
|
||||
/// (handled earlier in `lowerStructLiteral`).
|
||||
pub fn lowerUnionLiteral(self: *Lowering, sl: *const ast.StructLiteral, ty: TypeId, span: ast.Span) Ref {
|
||||
// Empty `.{}` → an undefined union value (matches the spec's `--- ` form;
|
||||
// `zeroValue` of a union is `constUndef`).
|
||||
if (sl.field_inits.len == 0) return self.zeroValue(ty);
|
||||
|
||||
// Validate every member is named and all share one arm.
|
||||
const Arm = struct { promoted: bool, index: u32 };
|
||||
var arm: ?Arm = null;
|
||||
for (sl.field_inits) |fi| {
|
||||
const fname = fi.name orelse {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, span, "a union literal must name its member(s): `.{{ member = value }}` (positional union init is ambiguous)", .{});
|
||||
return self.zeroValue(ty);
|
||||
};
|
||||
const res = self.fieldLvalueResolve(ty, fname) orelse {
|
||||
_ = self.emitFieldError(ty, fname, span);
|
||||
return self.zeroValue(ty);
|
||||
};
|
||||
const cur: Arm = switch (res) {
|
||||
.union_direct => |u| .{ .promoted = false, .index = u.index },
|
||||
.union_promoted => |u| .{ .promoted = true, .index = u.variant_index },
|
||||
// A union name never resolves to `.indexed`, but be safe rather
|
||||
// than silently mis-store.
|
||||
.indexed => {
|
||||
_ = self.emitFieldError(ty, fname, span);
|
||||
return self.zeroValue(ty);
|
||||
},
|
||||
};
|
||||
if (arm) |a| {
|
||||
// Allowed only when BOTH are promoted members of the SAME variant.
|
||||
if (!a.promoted or !cur.promoted or a.index != cur.index) {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, span, "a union literal may set only one member, or several members of the same anonymous-struct arm — '{s}'s members overlay the same storage", .{self.formatTypeName(ty)});
|
||||
return self.zeroValue(ty);
|
||||
}
|
||||
} else {
|
||||
arm = cur;
|
||||
}
|
||||
}
|
||||
|
||||
// Construct: write each member at its lvalue into an undefined union slot.
|
||||
const slot = self.builder.alloca(ty);
|
||||
for (sl.field_inits) |fi| {
|
||||
const fname = fi.name.?; // validated above
|
||||
const member_ty = (self.fieldLvalueResolve(ty, fname) orelse unreachable).valueType();
|
||||
const saved_tt = self.target_type;
|
||||
self.target_type = member_ty;
|
||||
const val = self.lowerExpr(fi.value);
|
||||
self.target_type = saved_tt;
|
||||
const fl = self.fieldLvaluePtr(slot, ty, fname) orelse unreachable;
|
||||
const coerced = self.coerceToType(val, self.builder.getRefType(val), fl.ty);
|
||||
self.builder.store(fl.ptr, coerced);
|
||||
}
|
||||
return self.builder.load(slot, ty);
|
||||
}
|
||||
|
||||
/// True (and emits the diagnostic) when `obj.field` names a DIRECT variant of a
|
||||
/// tagged union — a store target that would set the payload but NOT the tag
|
||||
/// (issue 0136): a tagged union is laid out `{ tag, payload }`, the write path
|
||||
|
||||
Reference in New Issue
Block a user