wcc/cgen: #58 indexed tagged-field read+assign cursor arm (both-stage)

Reading or writing a tagged field of an indexed array element
(xs[i].field) was broken on BOTH stages, byte-identically and
silently (#263 gate-blind): the arr[i].field branches had arms for
array/str/slice/float but no TY_TAGGED arm, so the tagged field fell
to the single-word scalar path. READ loaded only the tag word (stale
payload -> `xs[i].min as T` read garbage); ASSIGN stored the raw
unboxed scalar into the tag slot, corrupting the box.

Insert a TY_TAGGED cursor arm before each scalar fallback, both
sites both stages (cgen.c read + assign; cgenexpr.ww cgdot N_INDEX-lhs
read + cgassign indexed-field). READ mirrors cg_tagged_memread
(payload -> DX/CX/R8, tag -> AX last). ASSIGN synthesizes the tag for
the concrete variant (taggedvariantindext) and stores tag+payload via
the str/slice 3-word store spine -- not the source-remap widener
(concrete rhs has no source tag to remap).

>32B / multi-word / float payloads are loud-stopped at all four arms
(emission not yet wired; see #114). That shape is reachable today via
a narrow-variant ctor, so it louds rather than silently miscompiling.
Both stages get the same arm -> byte-id preserved (990-997 green; the
runtime is the net for this #263 class). Pin 944_idx_tagged_field_run
(read/assign runtime rows + >32B expect-loud rows).
This commit is contained in:
2026-06-06 16:21:45 +09:00
parent cc896bd078
commit 351abb0ab3
6 changed files with 905 additions and 0 deletions

View File

@@ -5234,6 +5234,95 @@ cgexpr(Cg *c, Node *n, Local *locals)
amem(D_DX, foff + 16));
break;
}
/* #58: a TAGGED field of an indexed array
* element (`xs[i].f = v`). The scalar store
* below would write the raw unboxed rhs into
* the TAG slot — never boxing, never writing
* the payload (box-corruption, the #38a
* write-twin). BOX (mirror the #24 tagged-
* field-assign tag lookup, cg_tag_for_variant)
* + STORE spine (mirror the co-located str/
* slice 3-word arm above): cgexpr the payload,
* spill it across the index/address
* computation, compute &xs[i]->BX, then store
* the variant tag (constant) at foff+0 and the
* scalar payload at foff+8. Only a SCALAR-
* payload variant (box <=16B) store is wired
* here. A >16B / multi-word / float-payload
* union field IS constructible (a wide box,
* built via a NARROW variant — not unbuildable
* as earlier triage assumed; the #54/#23
* construction hole fires only on STRUCT-
* LITERAL payloads), but its box+memcpy store
* arm is not yet wired, so it LOUD-STOPS rather
* than silently corrupting the box (rule 7, the
* #41 untested-arm trap), byte-id-neutral.
* Reachable + pinned expect-loud (test/wcc/944
* cfail rows). When #114 wires them, that commit
* replaces these stops with the real str/slice/
* struct/float/>32B box+memcpy emission + value
* pin rows. */
if (n->op == TK_ASSIGN && fu
&& fu->kind == TY_TAGGED) {
int bsz = (int)fu->size;
Type *st = n->rhs
? n->rhs->type : NULL;
int h2_isf32 = 0;
if (bsz > TUPLE_GPCAP * 8)
fatal("#58: >32B tagged-"
"field indexed store "
"unreachable until #114");
if (bsz > 16)
fatal("#58: multi-word "
"tagged-field indexed "
"store unreachable "
"until #114");
if (fld_isfloat(st,
&h2_isf32))
fatal("#58: float-payload "
"tagged-field indexed "
"store unreachable "
"until #114");
cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ,
areg(D_AX));
cgexpr(c, idx, locals);
if (esz > 1) {
ins2(c, A_MOVQ,
aimm(esz),
areg(D_CX));
ins2(c, A_IMULQ,
areg(D_CX),
areg(D_AX));
}
if (is_arr)
ins2(c, A_LEAQ,
amem(D_BP, off),
areg(D_BX));
else
ins2(c, A_MOVQ,
amem(D_BP, off),
areg(D_BX));
ins2(c, A_ADDQ,
areg(D_AX),
areg(D_BX));
if (viaptr)
ins2(c, A_MOVQ,
amem(D_BX, 0),
areg(D_BX));
ins1(c, A_POPQ,
areg(D_AX));
int v58tag =
cg_tag_for_variant(fu, st);
ins2(c, A_MOVQ,
aimm(v58tag < 0
? 0 : v58tag),
amem(D_BX, foff + 0));
ins2(c, A_MOVQ,
areg(D_AX),
amem(D_BX, foff + 8));
break;
}
if (n->op == TK_ASSIGN) {
cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ,
@@ -11216,6 +11305,51 @@ cgexpr(Cg *c, Node *n, Local *locals)
areg(D_AX));
goto dot_done;
}
/* #58: a TAGGED field of an indexed array
* element (`xs[i].f`). AX holds &xs[i]; load
* the box cursor (AX=tag, DX/CX/R8=payload)
* mirroring cg_tagged_memread's ≤32B
* convention, tag LAST (it clobbers the base
* AX). Without this arm the field fell to the
* scalar load below, reading only the tag word
* and leaving the payload cursor (DX) stale
* (`xs[i].f as T` read garbage; #38a INDEX-
* spine residual). >32B box: a wide-box union
* (largest variant >32B) IS constructible via a
* NARROW variant (not unbuildable as earlier
* triage assumed; #54/#23 fires only on STRUCT-
* LITERAL payloads), but the mem-based read (LEAQ
* foff(AX),AX, cg_tagged_memread:639) is not yet
* wired here — so this arm LOUD-STOPS rather than
* silently reading a truncated box (rule 7, the
* #41 untested-arm trap), byte-id-neutral.
* Reachable + pinned expect-loud (test/wcc/944
* cfail rows). When #114 wires it, that commit
* replaces this with the LEAQ box-address
* emission + a >32B value pin row. */
if (fu && fu->kind == TY_TAGGED) {
int bsz = (int)fu->size;
if (bsz > TUPLE_GPCAP * 8)
fatal("#58: >32B tagged-field "
"indexed read unreachable "
"until #114");
if (bsz > 24)
ins2(c, A_MOVQ,
amem(D_AX, foff + 24),
areg(D_R8));
if (bsz > 16)
ins2(c, A_MOVQ,
amem(D_AX, foff + 16),
areg(D_CX));
if (bsz > 8)
ins2(c, A_MOVQ,
amem(D_AX, foff + 8),
areg(D_DX));
ins2(c, A_MOVQ,
amem(D_AX, foff + 0),
areg(D_AX));
goto dot_done;
}
int g_isf32 = 0;
if (fld_isfloat(ft, &g_isf32)) {
int mov = g_isf32