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

@@ -201,6 +201,13 @@ fn cgtryprop(c: *cgen, n: *node) void = {
};
};
};
// #37 (rule 7): a >32B box read leaves AX = address, not the
// tag the unwrap below compares. Mirrors cstage.
if (taggedmemread(c, n.lhs)) {
let m37p: str = "#37: `?` on a >32B mem-based tagged read unwired (#40-family follow-up)\n";
os.write(2, m37p.ptr, m37p.len: u64);
os.exit(1);
};
if (sretretsize(c, c.fnret) > 0) {
let m38q: str = "#38b: `?` propagation into a >32B tagged return unwired (sret error-propagate is a #40-family follow-up)\n";
os.write(2, m38q.ptr, m38q.len: u64);
@@ -334,6 +341,12 @@ fn cgtryunw(c: *cgen, n: *node) void = {
};
};
};
// #37 (rule 7): see the cgtryprop twin.
if (taggedmemread(c, n.lhs)) {
let m37u: str = "#37: `!` on a >32B mem-based tagged read unwired (#40-family follow-up)\n";
os.write(2, m37u.ptr, m37u.len: u64);
os.exit(1);
};
cgexpr(c, n.lhs);
let cl: str = mklabel(c, "tryunw_ok");
emitline("\tCMPQ\t$0, AX\n");
@@ -438,6 +451,12 @@ fn cgtypetest(c: *cgen, n: *node) void = {
// tag read landed on (BP) — the saved-BP word.
nonident = true;
cgexpr(c, lhs);
// #37: a >32B box read leaves its ADDRESS in AX —
// load the tag word from memory before the compare.
// Mirrors cstage N_TYPETEST.
if (taggedmemread(c, lhs)) {
emitline("\tMOVQ\t(AX), AX\n");
};
};
};
let want: i32 = 0;
@@ -586,6 +605,28 @@ fn cgtypeassert(c: *cgen, n: *node) void = {
scrutt = matchscrutt(c, lhs);
let spillsz: i32 = matchspillsz(c, scrutt);
scrutoff = localalloc(c, "@asrt_spill", spillsz, nil);
if (taggedmemread(c, lhs)) {
// #37: >32B box read — ADDRESS in AX; copy the
// whole box from memory. Mirrors cstage.
cgexpr(c, lhs);
let ak37: i32 = 0;
for (ak37 < spillsz) {
emitline("\tMOVQ\t");
emitdispreg(ak37: i64, "AX");
emitline(", DX\n");
emitline("\tMOVQ\tDX, ");
emitoff((scrutoff + ak37): i64);
emitline("(BP)\n");
ak37 += 8;
};
} else {
// #37 (rule 7): >32B from a non-mem-based kind would
// spill an unfilled cursor. Mirrors cstage.
if (!isnullabletype(scrutt) && spillsz > TUPLE_GPCAP * 8) {
let m37s: str = "#37: `as` on a >32B tagged value from a non-mem-based source unwired (rule 7)\n";
os.write(2, m37s.ptr, m37s.len: u64);
os.exit(1);
};
cgexpr(c, lhs);
emitline("\tMOVQ\tAX, ");
emitoff(scrutoff: i64);
@@ -607,6 +648,7 @@ fn cgtypeassert(c: *cgen, n: *node) void = {
emitline("(BP)\n");
};
};
};
};
};
let want: i32 = cgtagvariantidx(c, scrutt, n.rhs);
@@ -1683,6 +1725,13 @@ fn cgindex(c: *cgen, n: *node) void = {
return;
};
if (elem_tagged) {
// #37: >32B box — ADDRESS in AX (the taggedmemread
// convention); the 4-reg cursor walk below would
// truncate past payload word 2. Mirrors cstage.
if (elem_slot_sz > TUPLE_GPCAP * 8) {
emitline("\tMOVQ\tBX, AX\n");
return;
};
if (elem_slot_sz > 24) {
emitline("\tMOVQ\t24(BX), R8\n");
};
@@ -1742,6 +1791,13 @@ fn cgindex(c: *cgen, n: *node) void = {
return;
};
if (elem_tagged) {
// #37: >32B box — ADDRESS in AX (the taggedmemread
// convention); the 4-reg cursor walk below would
// truncate past payload word 2. Mirrors cstage.
if (elem_slot_sz > TUPLE_GPCAP * 8) {
emitline("\tMOVQ\tBX, AX\n");
return;
};
if (elem_slot_sz > 24) {
emitline("\tMOVQ\t24(BX), R8\n");
};
@@ -1797,6 +1853,11 @@ fn cgindex(c: *cgen, n: *node) void = {
// arms have it) — a >24B-slot element via a non-ident base
// under-read the cursor and the match spill stored stale R8.
// Mirrors cstage cgen.c:9106-9117.
// #37: >32B box — AX already holds the element address;
// leave it (taggedmemread). Mirrors cstage.
if (elem_slot_sz > TUPLE_GPCAP * 8) {
return;
};
emitline("\tMOVQ\tAX, BX\n");
if (elem_slot_sz > 24) {
emitline("\tMOVQ\t24(BX), R8\n");
@@ -2186,7 +2247,47 @@ fn cgmatch(c: *cgen, n: *node) void = {
c.sretdestoff = scrutoff;
cgexpr(c, scrut);
c.sretdestoff = 0;
} else { if (taggedmemread(c, scrut)) {
// #37: >32B box read (insts[pc], t.N) — cgexpr left
// its ADDRESS in AX; copy the whole box from memory.
// Mirrors cstage cgmatch.
cgexpr(c, scrut);
let mk37: i32 = 0;
for (mk37 < spillsz) {
emitline("\tMOVQ\t");
emitdispreg(mk37: i64, "AX");
emitline(", DX\n");
emitline("\tMOVQ\tDX, ");
emitoff((scrutoff + mk37): i64);
emitline("(BP)\n");
mk37 += 8;
};
} else {
// #37 (rule 7): a >32B box from a kind with no mem-read
// convention would spill the cursor it never filled —
// loud, not garbage. Mirrors cstage.
if (!isnullabletype(scrutt) && spillsz > TUPLE_GPCAP * 8) {
let m37m: str = "#37: >32B tagged match scrutinee from a non-mem-based source unwired (rule 7)\n";
os.write(2, m37m.ptr, m37m.len: u64);
os.exit(1);
};
// #37 (rule 7) stamped twin: matchscrutt returns nil
// for kinds it can't resolve (deref/cast/...), so
// spillsz defaults under cap and the guard above is
// blind there. cstage sizes the spill from the
// stamped s->type, so it louds — key on scrut.type_
// to match. Surfaced by reviewer-37's `match (*p)`
// probe on a 56B box.
let ms37: *tinfo = scrut.type_: *tinfo;
for (ms37 != nil && ms37.kind == tykind.TY_NAMED) {
ms37 = ms37.under;
};
if (ms37 != nil && ms37.kind == tykind.TY_TAGGED
&& ms37.size: i32 > TUPLE_GPCAP * 8) {
let m37n: str = "#37: >32B tagged match scrutinee from a non-mem-based source unwired (rule 7)\n";
os.write(2, m37n.ptr, m37n.len: u64);
os.exit(1);
};
cgexpr(c, scrut);
emitline("\tMOVQ\tAX, ");
emitoff(scrutoff: i64);
@@ -2211,7 +2312,7 @@ fn cgmatch(c: *cgen, n: *node) void = {
emitline("(BP)\n");
};
};
};
}; };
};
};
let endl: str = mklabel(c, "match_end");
@@ -2703,19 +2804,18 @@ fn cgdot(c: *cgen, n: *node) void = {
// tagged arm.
if (istaggedtype(c, tpt)) {
let eslot: i32 = tupeslotn(tpt);
// #22b (rule 7): a >32B box
// overruns the 4-reg cursor —
// pre-bound tupreg clamped k>=3
// to R8 (silent payload drop)
// while cstage emitted invalid
// asm. Reachable only since the
// over-cap sret send unwired;
// the mem-based box read is the
// #37 family. Mirrors cstage.
// #37: a >32B box overruns the
// 4-reg cursor — leave its
// ADDRESS in AX (taggedmemread,
// the sret-receive convention);
// consumers copy from memory.
// Replaces the #22b loud bound.
// Mirrors cstage.
if (eslot > TUPLE_GPCAP * 8) {
let m37: str = "tagged tuple element read exceeds the AX/DX/CX/R8 box cursor (mem-based read is the #37 family; rule 7)\n";
os.write(2, m37.ptr, m37.len: u64);
os.exit(1);
emitline("\tLEAQ\t");
emitoff((lc.off + foff): i64);
emitline("(BP), AX\n");
return;
};
let k: i32 = 0;
for (k < eslot / 8) {