w6c+w6c_ww: >32B tagged INDEX/DOT reads go mem-based — box address in AX (#37)

A tagged box wider than the AX/DX/CX/R8 cursor (size > TUPLE_GPCAP*8)
read via N_INDEX or N_DOT now leaves its ADDRESS in AX — joining the
sret-call mem-based class the #38b gates already speak — and every
cursor consumer branches on one shared predicate (cg_tagged_memread /
taggedmemread) before reading AX as the tag. <=32B keeps the cursor
byte-for-byte (32B-at-cap asm proven identical to base on both the
t.N and INDEX routes).

Emitters: N_INDEX ident+fallback arms, N_DOT tuple-element (flips the
#22b loud bound), N_DOT struct-field + ptr-chained-field (were silent
clamps at 32B); wwstage twins gate cgloadtaggedfield at the helper
choke-point. Consumers: match spill x2 and the widen-store subset +
nested arms (the let/assign/return-widen/arg-widen/vararg choke-point)
copy the box from memory, then share the existing zero-pad + tag-remap
tail; `is` loads the tag through the address; `as` spills mem-based.
Rule-7 loud bounds replace silent cursor garbage for the non-mem-based
>32B kinds, `?`/`!`, and the 33-48B in-reg tagged arg push (mem-based
push stays the #35 family); the exact-type >32B return passthrough
from INDEX/DOT flips from its #38b loud into the widener route. The
pre-existing >48B memarg stack blit (cgplaceaddr) never used the
cursor and is pinned unchanged.

Reviewer-37 amendment: the non-mem-based >32B loud was ONE-SIDED on
two wwstage routes — cgwidentaggedstorebp had no fall-through guard
at all (`let w = *p` on a 56B box: cstage loud, wwstage silent word0
truncation), and cgmatch's guard keyed on matchscrutt-resolved
spillsz, which defaults under cap for kinds matchscrutt can't resolve
(N_UN deref et al), so `match (*p)` slipped it the same way. Both now
loud off the stamped src/scrut type_ (the kind-blind key cstage
already uses), restoring the rule-10 symmetry the body claims.

Emitters and consumers ship as ONE commit: they share the memread
contract, and splitting would open a transient window where a wired
emitter hands an address to an unwired consumer (silent garbage) —
the #61-precedent route-sharing fuse. The CX-global-tuple-base LEAQ
arm is TRIPWIRE wiring: a >32B tagged global-tuple element is
unreachable today (module-level tuple inits are int/str-literal-only;
tagged elements loud at the DATA emit), and the LEAQ keeps the same
base_reg generality as the cursor walk it replaces (ken note, task
record).

This was the last 5b compiler gate: `match insts[pc]` on the regex
inst union (inst_lit|inst_repeat, 56B) was silent-wrong gate-blind
byte-id on both stages (payload words 3+ dropped past the R8 clamp).
test 941 grows 165->200 checks: the #22b BUILDERR pin flips to a
runtime row, plus the 56B driver match, str+nested-tagged payload,
let/is/assign, indexed return, widening (identity and reversed-order
remap), 56B memarg, ken's X1 composition row, 32B-at-cap INDEX
boundary, the ptr-chained p.f match (BX-arm) and (*p)[i] fallback-arm
rows, and the two deref loud-symmetry BUILDERR pins. At base f272068
the 11 bug rows fail (2 BUILDERR flips + 7 silent-wrong + 2 missing-
loud pins, both drivers); the anchors pass. Oracle: ken PASS at
bf21964b pre-amendment; re-bind on the amended tree pending (source
bytes changed: cgenutil.ww/cgenexpr.ww louds + combined.ww regen).
This commit is contained in:
2026-06-05 03:41:34 +09:00
parent f272068940
commit 26d375410e
7 changed files with 1271 additions and 107 deletions

View File

@@ -648,6 +648,22 @@ tagged_memarg_size(Type *t)
return (int)t->size;
}
/* cg_tagged_memread — #37: does cgexpr leave this tagged expr's box in
* MEMORY (AX = box address) instead of the AX/DX/CX/R8 cursor? True
* for an N_INDEX/N_DOT read whose box exceeds the 4-reg cursor — the
* same mem-based class as an sret-classified call (which the #38b
* gates key separately on cg_sret_retsize). Every cursor-spill
* consumer must branch on this before reading AX as the tag. */
static int
cg_tagged_memread(Node *e)
{
Type *u;
if (e == NULL || (e->kind != N_INDEX && e->kind != N_DOT)) return 0;
u = type_chase_named(e->type);
if (u == NULL || u->kind != TY_TAGGED) return 0;
return (int)u->size > TUPLE_GPCAP * 8;
}
/* type_isnullable — TY_TAGGED with the (*T | void) one-word fold. */
static int
type_isnullable(Type *t)
@@ -2408,6 +2424,26 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
"cannot be cursor-widened into a "
"tagged slot (mem-to-mem widen "
"unwired)");
if (cg_tagged_memread(src)) {
/* #37: >32B box read — ADDRESS in
* AX; copy the inner box from memory
* into the payload area. */
cgexpr(c, src, *locals_p);
for (int k = 0; k < ssz; k += 8) {
ins2(c, A_MOVQ,
amem(D_AX, k),
areg(D_DX));
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP,
write_off + 8 + k));
}
} else {
/* #37 (rule 7): >32B from a non-mem-based
* kind would spill an unfilled cursor. */
if (ssz > TUPLE_GPCAP * 8)
fatal("#37: >32B tagged payload from "
"a non-mem-based source (kind %d) "
"unwired (rule 7)", src->kind);
cgexpr(c, src, *locals_p);
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + 8));
@@ -2420,6 +2456,7 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
if (ssz > 24)
ins2(c, A_MOVQ, areg(D_R8),
amem(D_BP, write_off + 32));
}
}
ins2(c, A_MOVQ, aimm(nested),
amem(D_BP, write_off + 0));
@@ -2434,6 +2471,17 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + k));
}
} else if (cg_tagged_memread(src)) {
/* #37: >32B box read (insts[pc], t.N, s.f) —
* cgexpr left its ADDRESS in AX; copy the whole
* box from memory. Pad + remap below are shared
* with the ident path (both mem-based). */
cgexpr(c, src, *locals_p);
for (int k = 0; k < ssz; k += 8) {
ins2(c, A_MOVQ, amem(D_AX, k), areg(D_DX));
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP, write_off + k));
}
} else {
/* Tagged source returned via the tagged-return ABI
* (AX=tag, DX=word0, CX=word1, R8=word2). The unused
@@ -2447,6 +2495,12 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
fatal("#40: sret-class call result cannot be "
"cursor-widened into a tagged slot "
"(mem-to-mem widen unwired)");
/* #37 (rule 7): >32B from a non-mem-based kind
* would spill an unfilled cursor. */
if (ssz > TUPLE_GPCAP * 8)
fatal("#37: >32B tagged source of a non-mem-"
"based kind (%d) unwired (rule 7)",
src->kind);
cgexpr(c, src, *locals_p);
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + 0));
@@ -8550,6 +8604,15 @@ cgexpr(Cg *c, Node *n, Local *locals)
fatal("#38b: >32B tagged call result "
"as a call argument unwired "
"(#40-family follow-up)");
/* #37 (rule 7): a 33-48B box from an
* INDEX/DOT read is mem-based (AX = addr,
* no cursor to push) — was silent cursor
* truncation pre-#37; the mem-based push
* is the #35 family. */
if (cg_tagged_memread(args[i]))
fatal("#37: >32B tagged arg from a "
"mem-based read unwired (#35/"
"#40-family follow-up)");
int sz = tagged_arg_size(args[i]->type);
if (sz > 24)
ins1(c, A_PUSHQ, areg(D_R8));
@@ -9008,6 +9071,19 @@ cgexpr(Cg *c, Node *n, Local *locals)
* dispatch fired on a stale slot. */
sl_off = localoff(c, &locals, "@match_spill",
slot_size, cg_frame);
if (cg_tagged_memread(s)) {
/* #37: >32B box — cgexpr left its
* ADDRESS in AX; copy the whole box
* from memory (the cursor can't
* carry it). */
cgexpr(c, s, locals);
for (int k = 0; k < slot_size; k += 8) {
ins2(c, A_MOVQ, amem(D_AX, k),
areg(D_DX));
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP, sl_off + k));
}
} else {
cgexpr(c, s, locals);
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, sl_off + 0));
@@ -9021,6 +9097,7 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_MOVQ, areg(D_R8),
amem(D_BP, sl_off + 24));
}
}
}
} else {
/* Spill non-ident scrutinees (e.g. `match (foo()?)`) into
@@ -9040,7 +9117,25 @@ cgexpr(Cg *c, Node *n, Local *locals)
cg_sret_dest_off = sl_off;
cgexpr(c, s, locals);
cg_sret_dest_off = 0;
} else if (cg_tagged_memread(s)) {
/* #37: >32B box read (insts[pc], t.N) —
* cgexpr left its ADDRESS in AX; copy the
* whole box from memory. */
cgexpr(c, s, locals);
for (int k = 0; k < slot_size; k += 8) {
ins2(c, A_MOVQ, amem(D_AX, k),
areg(D_DX));
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP, sl_off + k));
}
} else {
/* #37 (rule 7): a >32B box from a kind with no
* mem-read convention (cast, ...) would spill the
* cursor it never filled — loud, not garbage. */
if (!is_nullable && slot_size > TUPLE_GPCAP * 8)
fatal("#37: >32B tagged match scrutinee from "
"a non-mem-based source (kind %d) unwired "
"(rule 7)", s->kind);
cgexpr(c, s, locals);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0));
if (!is_nullable) {
@@ -9183,6 +9278,11 @@ cgexpr(Cg *c, Node *n, Local *locals)
fatal("#38b: `?` on an sret-class call result "
"unwired (mem-based unwrap is a #40-family "
"follow-up)");
/* #37 (rule 7): a >32B box read leaves AX = address, not
* the tag the unwrap below compares. */
if (cg_tagged_memread(n->lhs))
fatal("#37: `?` on a >32B mem-based tagged read "
"unwired (#40-family follow-up)");
if (cg_sret_retsize(cg_ret_type) > 0)
fatal("#38b: `?` propagation into a >32B tagged "
"return unwired (sret error-propagate is a "
@@ -9273,6 +9373,10 @@ cgexpr(Cg *c, Node *n, Local *locals)
fatal("#38b: `!` on an sret-class call result "
"unwired (mem-based unwrap is a #40-family "
"follow-up)");
/* #37 (rule 7): see the N_TRYPROP twin. */
if (cg_tagged_memread(n->lhs))
fatal("#37: `!` on a >32B mem-based tagged read "
"unwired (#40-family follow-up)");
cgexpr(c, n->lhs, locals);
Type *u = n->lhs ? n->lhs->type : NULL;
if (u && u->kind == TY_NAMED) u = u->under;
@@ -9332,6 +9436,10 @@ cgexpr(Cg *c, Node *n, Local *locals)
fatal("#38b: `is` on an sret-class call result "
"unwired (#40-family follow-up)");
cgexpr(c, n->lhs, locals);
/* #37: a >32B box read leaves its ADDRESS in AX — load
* the tag word from memory before the compare. */
if (cg_tagged_memread(n->lhs))
ins2(c, A_MOVQ, amem(D_AX, 0), areg(D_AX));
Type *u = n->lhs ? n->lhs->type : NULL;
if (u && u->kind == TY_NAMED) u = u->under;
Type *vt = n->rhs ? n->rhs->type : NULL;
@@ -9399,6 +9507,24 @@ cgexpr(Cg *c, Node *n, Local *locals)
if (sl_off == 0) {
sl_off = localoff(c, &locals, "@asrt_spill",
slot_size, cg_frame);
if (cg_tagged_memread(s)) {
/* #37: >32B box read — ADDRESS in AX; copy
* the whole box from memory. */
cgexpr(c, s, locals);
for (int k = 0; k < slot_size; k += 8) {
ins2(c, A_MOVQ, amem(D_AX, k),
areg(D_DX));
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP, sl_off + k));
}
} else {
/* #37 (rule 7): >32B from a non-mem-based kind
* would spill an unfilled cursor. */
if (!(u && u->kind == TY_TAGGED && u->nullable)
&& slot_size > TUPLE_GPCAP * 8)
fatal("#37: `as` on a >32B tagged value from "
"a non-mem-based source (kind %d) unwired "
"(rule 7)", s->kind);
cgexpr(c, s, locals);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0));
if (!(u && u->kind == TY_TAGGED && u->nullable)) {
@@ -9408,6 +9534,7 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, sl_off + 16));
}
}
}
char *ok = mklabel(c, "asrt_ok");
if (u && u->kind == TY_TAGGED && u->nullable) {
@@ -10014,18 +10141,22 @@ cgexpr(Cg *c, Node *n, Local *locals)
* the ascending walk can't clobber the base. */
if (fu && fu->kind == TY_TAGGED) {
int eslot = tuple_eslot(tp->type);
/* #22b (rule 7): a >32B box overruns the
* 4-reg cursor — pre-bound the walk
* indexed past tuple_rseq (invalid asm)
* while wwstage clamped to R8 (silent
* payload drop). Reachable only since
* the over-cap sret send unwired; the
* mem-based box read is the #37 family. */
if (eslot > TUPLE_GPCAP * 8)
fatal("tagged tuple element read "
"exceeds the AX/DX/CX/R8 box "
"cursor (mem-based read is the "
"#37 family; rule 7)");
/* #37: a >32B box overruns the 4-reg
* cursor — leave its ADDRESS in AX
* (cg_tagged_memread, the sret-receive
* convention); consumers copy from
* memory. Replaces the #22b loud
* bound (pre-bound: cstage indexed
* past tuple_rseq = invalid asm,
* wwstage clamped to R8 = silent
* payload drop). */
if (eslot > TUPLE_GPCAP * 8) {
ins2(c, A_LEAQ,
amem(base_reg,
base_disp + foff),
areg(D_AX));
break;
}
for (int k = 0; k < eslot / 8; k++)
ins2(c, A_MOVQ,
amem(base_reg,
@@ -10074,6 +10205,17 @@ cgexpr(Cg *c, Node *n, Local *locals)
? f->type->under : f->type;
if (tag_fu && tag_fu->kind == TY_TAGGED) {
int fo = base_disp + (int)f->offset;
/* #37: >32B box — ADDRESS in AX (the
* cg_tagged_memread convention), not
* the cursor. */
if ((int)tag_fu->size
> TUPLE_GPCAP * 8) {
ins2(c, A_LEAQ,
amem(base_reg, fo),
areg(D_AX));
(void)is_global;
break;
}
ins2(c, A_MOVQ,
amem(base_reg, fo + 0), areg(D_AX));
ins2(c, A_MOVQ,
@@ -10182,6 +10324,15 @@ cgexpr(Cg *c, Node *n, Local *locals)
? f->type->under : f->type;
if (ptag_fu && ptag_fu->kind == TY_TAGGED) {
int fo = (int)f->offset;
/* #37: >32B box — ADDRESS in
* AX, not the cursor. */
if ((int)ptag_fu->size
> TUPLE_GPCAP * 8) {
ins2(c, A_LEAQ,
amem(D_BX, fo),
areg(D_AX));
break;
}
ins2(c, A_MOVQ,
amem(D_BX, fo + 0),
areg(D_AX));
@@ -10613,6 +10764,15 @@ cgexpr(Cg *c, Node *n, Local *locals)
* pointer). */
if (elem_tagged) {
int ssz = (int)esubu->size;
/* #37: >32B box — ADDRESS in AX (the
* cg_tagged_memread convention); the
* 4-reg cursor walk below would
* truncate past payload word 2. */
if (ssz > TUPLE_GPCAP * 8) {
ins2(c, A_MOVQ, areg(D_BX),
areg(D_AX));
break;
}
if (ssz > 24)
ins2(c, A_MOVQ, amem(D_BX, 24),
areg(D_R8));
@@ -10681,6 +10841,10 @@ cgexpr(Cg *c, Node *n, Local *locals)
* load slot words. */
if (elem_tagged) {
int ssz = (int)esubu->size;
/* #37: >32B box — AX already holds the element
* address; leave it (cg_tagged_memread). */
if (ssz > TUPLE_GPCAP * 8)
break;
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
if (ssz > 24)
ins2(c, A_MOVQ, amem(D_BX, 24), areg(D_R8));
@@ -11574,17 +11738,8 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
* then return the dest pointer. */
if (cg_sret_retsize(rt) > 0) {
int sz = (int)rt->size;
if (passthrough) {
/* exact type but a cursor source
* (N_INDEX/N_DOT) can't carry
* >32B — loud-stop (rule 7,
* #38b residual). */
if (n->lhs->kind != N_CALL)
fatal("#38b: >32B tagged "
"return from a cursor "
"source (kind %d) "
"unsupported",
n->lhs->kind);
if (passthrough
&& n->lhs->kind == N_CALL) {
cg_sret_forward = 1;
cgexpr(c, n->lhs, *locals);
ins2(c, A_MOVQ,
@@ -11592,7 +11747,12 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
areg(D_AX));
} else if (istagged
&& n->lhs->kind != N_IDENT
&& (int)vu->size > TUPLE_GPCAP * 8) {
&& (int)vu->size > TUPLE_GPCAP * 8
&& !cg_tagged_memread(n->lhs)) {
/* #37 wired the N_INDEX/N_DOT
* mem-read into the widener;
* the remaining >32B kinds
* stay loud. */
fatal("#40: widening tagged "
"return-forward of a >32B "
"source needs mem-to-mem "