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:
20
examples/0185-types-tagged-union-member-assign-rejected.sx
Normal file
20
examples/0185-types-tagged-union-member-assign-rejected.sx
Normal file
@@ -0,0 +1,20 @@
|
||||
// A direct write to a tagged-union (enum-with-payload) variant member is
|
||||
// rejected: a tagged union is laid out `{ tag, payload }`, and a member write
|
||||
// would set the payload but leave the tag stale. The variant is set via
|
||||
// construction (`s = .rect(...)`), which writes both tag and payload.
|
||||
//
|
||||
// Regression (issue 0136): `s.rect = .{...}` used to silently store the payload
|
||||
// only, desyncing the tag so a later `match` took the wrong arm. It now errors.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
Shape :: enum {
|
||||
circle: f32;
|
||||
rect: struct { w, h: f32; };
|
||||
}
|
||||
|
||||
main :: () {
|
||||
s : Shape = .circle(1.0);
|
||||
s.rect = .{ w = 4.0, h = 2.0 }; // rejected — use `s = .rect(.{...})` instead
|
||||
print("unreachable: {}\n", s.rect.w);
|
||||
}
|
||||
22
examples/0186-types-tagged-union-nested-field-write.sx
Normal file
22
examples/0186-types-tagged-union-nested-field-write.sx
Normal file
@@ -0,0 +1,22 @@
|
||||
// A write to a sub-field of a tagged-union variant's payload (`s.rect.w = ...`)
|
||||
// is NOT rejected: the immediate object is the payload struct, so it mutates a
|
||||
// field of the already-active variant in place and leaves the tag alone. This
|
||||
// pins the scope of issue 0136's guard — only a WHOLE-variant member write
|
||||
// (`s.rect = ...`) is rejected; nested sub-field writes keep working.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
Shape :: enum {
|
||||
circle: f32;
|
||||
rect: struct { w, h: f32; };
|
||||
}
|
||||
|
||||
main :: () {
|
||||
s : Shape = .rect(.{ w = 1.0, h = 2.0 });
|
||||
s.rect.w = 9.0; // nested sub-field write — allowed
|
||||
r := s.rect;
|
||||
print("w={} h={}\n", r.w, r.h); // 9 2
|
||||
|
||||
s = .circle(3.5); // construction reassign — allowed
|
||||
print("c={}\n", s.circle); // 3.5
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,5 @@
|
||||
error: cannot assign to tagged-union variant 'rect' directly — a member write sets the payload but leaves the tag stale; construct the variant instead (e.g. `x = .rect(...)`)
|
||||
--> examples/0185-types-tagged-union-member-assign-rejected.sx:18:5
|
||||
|
|
||||
18 | s.rect = .{ w = 4.0, h = 2.0 }; // rejected — use `s = .rect(.{...})` instead
|
||||
| ^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
w=9.000000 h=2.000000
|
||||
c=3.500000
|
||||
Reference in New Issue
Block a user