fix: reject direct assignment to a tagged-union variant member

A tagged union (enum-with-payload) is laid out { tag, payload }, but a
direct member write `s.rect = payload` lowered to a payload-only store
(union_gep into field 1) with no tag store — the discriminant went stale,
so a later match/== took the wrong arm with no diagnostic (issue 0136).
The read path already distinguishes tagged unions (enum_payload/enum_tag);
the write path treated them like plain unions.

A variant is set via construction (`s = .variant(payload)`, which writes
both tag and payload). A direct member write can't safely set the tag (the
active variant isn't known at the write site), so it is now rejected with a
diagnostic pointing to construction. A new diagTaggedUnionVariantWrite guard
— reusing the shared fieldLvalueResolve matcher, applied at both store sites
(lowerAssignment, lowerMultiAssign) — fires only for a whole-variant write
on a tagged union. Plain `union` writes and nested sub-field writes
(`s.rect.w = ...`) are unaffected.

Resolves issue 0136. Tests: examples/0185 (rejected), 0186 (nested write +
construction still work). specs.md / readme.md updated.
This commit is contained in:
agra
2026-06-13 21:18:40 +03:00
parent 4d32a4d4fb
commit e386a0d0b4
13 changed files with 239 additions and 0 deletions

View File

@@ -1609,6 +1609,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 diagTaggedUnionVariantWrite = lower_stmt.diagTaggedUnionVariantWrite;
pub const lowerExprAsPtr = lower_stmt.lowerExprAsPtr;
pub const storeOrCompound = lower_stmt.storeOrCompound;
pub const emitCompoundOp = lower_stmt.emitCompoundOp;