w6c: tagged-union fields on struct globals — LEAQ-based read+write

Field write extends the existing TY_TAGGED branch with an is_global
arm: LEAQ name(SB),CX after cgexpr (no AX/BX clobber), then MOVQ
into slot+foff+0 (tag) and slot+foff+8 (value, plus +16 for str-
typed variants).

Field read now treats tagged fields specially — load AX=tag,
DX=val0, CX=val1 (when union >16B), mirroring the tagged-return
ABI that let-init and match dispatch already expect. Previously
the scalar-load path read 8B into AX and left DX/CX with junk,
which silently broke local tagged-field reads too.
This commit is contained in:
2026-05-12 13:29:25 +09:00
parent 6f04713601
commit 328a53de5b
2 changed files with 105 additions and 4 deletions

View File

@@ -1160,29 +1160,49 @@ cgexpr(Cg *c, Node *n, Local *locals)
if (f == NULL) break;
/* Tagged-union field: synthesise tag and store
* value bytes. Compound ops on tagged fields are
* not meaningful, so only plain `=` is wired. */
* not meaningful, so only plain `=` is wired.
* Three base shapes:
* - via_ptr: base is *struct local; address
* pre-loaded into BX. Buggy with a str
* variant since cgexpr will overwrite BX,
* but matches the existing pre-global
* behaviour.
* - is_global: struct global. LEAQ after
* cgexpr drops the slot address into CX
* without touching AX/BX, so str variants
* work cleanly.
* - else: struct local, BP-relative. */
Type *ft = f->type;
Type *fu = (ft && ft->kind == TY_NAMED)
? ft->under : ft;
if (fu && fu->kind == TY_TAGGED
&& n->op == TK_ASSIGN) {
int boff = localfind(locals, base->str);
int is_global = (boff == 0 && !via_ptr
&& let_islet(base->str));
int foff = (int)f->offset;
Type *vt = n->rhs ? n->rhs->type : NULL;
int tag = cg_tag_for_variant(fu, vt);
/* Resolve the slot's base address
* (either struct local or *struct). */
int addr_in_bx = via_ptr;
if (via_ptr)
ins2(c, A_MOVQ, amem(D_BP, boff),
areg(D_BX));
cgexpr(c, n->rhs, locals);
if (is_global)
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_CX));
if (type_isstr(vt)) {
if (addr_in_bx) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BX, foff + 8));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BX, foff + 16));
} else if (is_global) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_CX, foff + 8));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_CX, foff + 16));
} else {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, boff + foff + 8));
@@ -1193,6 +1213,9 @@ cgexpr(Cg *c, Node *n, Local *locals)
if (addr_in_bx)
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BX, foff + 8));
else if (is_global)
ins2(c, A_MOVQ, areg(D_AX),
amem(D_CX, foff + 8));
else
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, boff + foff + 8));
@@ -1201,6 +1224,10 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_MOVQ,
aimm(tag < 0 ? 0 : tag),
amem(D_BX, foff + 0));
else if (is_global)
ins2(c, A_MOVQ,
aimm(tag < 0 ? 0 : tag),
amem(D_CX, foff + 0));
else
ins2(c, A_MOVQ,
aimm(tag < 0 ? 0 : tag),
@@ -2636,9 +2663,28 @@ cgexpr(Cg *c, Node *n, Local *locals)
base_reg = D_CX;
base_disp = 0;
}
(void)is_global;
for (Tfield *f = u->fields; f; f = f->next) {
if (strcmp(f->name, n->str) != 0) continue;
/* tagged-union field: load AX=tag, DX=val0,
* CX=val1 (last, since for globals CX is also
* the base addr). Mirrors the tagged-return
* ABI so the let-init / match dispatch shapes
* just work. */
Type *tag_fu = (f->type && f->type->kind == TY_NAMED)
? f->type->under : f->type;
if (tag_fu && tag_fu->kind == TY_TAGGED) {
int fo = base_disp + (int)f->offset;
ins2(c, A_MOVQ,
amem(base_reg, fo + 0), areg(D_AX));
ins2(c, A_MOVQ,
amem(base_reg, fo + 8), areg(D_DX));
if (tag_fu->size > 16)
ins2(c, A_MOVQ,
amem(base_reg, fo + 16),
areg(D_CX));
(void)is_global;
break;
}
/* str field: load (ptr, len) into (AX, BX) so the
* value flows through the str-rhs convention. */
Type *str_fu = (f->type && f->type->kind == TY_NAMED)

View File

@@ -232,6 +232,61 @@ static const struct fixture fixtures[] = {
"};\n",
11,
},
{
"struct-tagged-field-i64",
/* Tagged-union field on a struct global. Write goes
* LEAQ name(SB),CX + MOVQ to slot+foff+8 (value) and
* slot+foff+0 (tag). Read pulls AX=tag, DX=val0,
* CX=val1 from those same slots and match dispatches. */
"type tok = (i64 | str | void);\n"
"type ent = struct { id: i32, t: tok };\n"
"let e: ent;\n"
"fn main() i32 = {\n"
"\te.t = 42i64: tok;\n"
"\tlet v: tok = e.t;\n"
"\tmatch (v) {\n"
"\tcase let n: i64 => return n: i32;\n"
"\tcase let s: str => return 1;\n"
"\tcase void => return 2;\n"
"\t};\n"
"\treturn 0;\n"
"};\n",
42,
},
{
"struct-tagged-field-str",
"type tok = (i64 | str | void);\n"
"type ent = struct { id: i32, t: tok };\n"
"let e: ent;\n"
"fn main() i32 = {\n"
"\te.t = \"hello\";\n"
"\tlet v: tok = e.t;\n"
"\tmatch (v) {\n"
"\tcase let n: i64 => return 1;\n"
"\tcase let s: str => return s.len: i32;\n"
"\tcase void => return 2;\n"
"\t};\n"
"\treturn 0;\n"
"};\n",
5,
},
{
"struct-tagged-field-void",
"type tok = (i64 | str | void);\n"
"type ent = struct { id: i32, t: tok };\n"
"let e: ent;\n"
"fn main() i32 = {\n"
"\te.t = void;\n"
"\tlet v: tok = e.t;\n"
"\tmatch (v) {\n"
"\tcase let n: i64 => return 1;\n"
"\tcase let s: str => return 2;\n"
"\tcase void => return 7;\n"
"\t};\n"
"\treturn 0;\n"
"};\n",
7,
},
{
"struct-narrow-field",
/* u8 field on a struct global. Read uses MOVZBQ, store