diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 77f7a951..8be8bdb5 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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 " diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 77936ac5..e4538534 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -16910,6 +16910,15 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { if (arg.kind == nkind.N_INDEX || arg.kind == nkind.N_DOT) { if (istaggedtype(c, arg)) { let isz: i32 = slotsize(c, arg); + // #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. Mirrors cstage. + if (taggedmemread(c, arg)) { + let m37g: str = "#37: >32B tagged arg from a mem-based read unwired (#35/#40-family follow-up)\n"; + os.write(2, m37g.ptr, m37g.len: u64); + os.exit(1); + }; if (isz > 24) { emitline("\tPUSHQ\tR8\n"); }; if (isz > 16) { emitline("\tPUSHQ\tCX\n"); }; if (isz > 8) { emitline("\tPUSHQ\tDX\n"); }; @@ -19033,6 +19042,23 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { return false; }; +// taggedmemread — #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 callsretsize). Every cursor-spill consumer +// must branch on this before reading AX as the tag. Mirrors cstage +// cg_tagged_memread. +fn taggedmemread(c: *cgen, e: *node) bool = { + if (e == nil) { return false; }; + if (e.kind != nkind.N_INDEX && e.kind != nkind.N_DOT) { return false; }; + let u: *tinfo = e.type_: *tinfo; + for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; }; + if (u == nil) { return false; }; + if (u.kind != tykind.TY_TAGGED) { return false; }; + return u.size: i32 > TUPLE_GPCAP * 8; +}; + // cgloadtaggedfield — load a tagged-union slot at `basereg`+foff // into the tagged-return ABI registers (AX=tag, DX=word0, CX=word1, // R8=word2). Slot sizes: 16B = (tag, word0), 24B = + word1, 32B @@ -19048,6 +19074,16 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { // only register loaded into that is NOT a target is BX, so AX- // or DX-rooted callers must spill first. fn cgloadtaggedfield(c: *cgen, basereg: str, foff: i32, slot_sz: i32) void = { + // #37: >32B box — leave its ADDRESS in AX (taggedmemread, the + // sret-receive convention); the 4-reg cursor walk below would + // truncate past payload word 2. Mirrors cstage's N_DOT + // TY_STRUCT/TY_PTR tagged arms. + if (slot_sz > TUPLE_GPCAP * 8) { + emitline("\tLEAQ\t"); + emitdispreg(foff: i64, basereg); + emitline(", AX\n"); + return; + }; // tag → AX emitline("\tMOVQ\t"); emitdispreg(foff: i64, basereg); @@ -19291,6 +19327,29 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s os.exit(1); }; }; + if (taggedmemread(c, src)) { + // #37: >32B box read — ADDRESS in AX; + // copy the inner box from memory into + // the payload area. Mirrors cstage. + cgexpr(c, src); + let mk: i32 = 0; + for (mk < ssz) { + emitline("\tMOVQ\t"); + emitdispreg(mk: i64, "AX"); + emitline(", DX\n"); + emitline("\tMOVQ\tDX, "); + emitoff((slot_off + 8 + mk): i64); + emitline("(BP)\n"); + mk += 8; + }; + } else { + // #37 (rule 7): >32B from a non-mem-based + // kind would spill an unfilled cursor. + if (ssz > TUPLE_GPCAP * 8) { + let m37a: str = "#37: >32B tagged payload from a non-mem-based source unwired (rule 7)\n"; + os.write(2, m37a.ptr, m37a.len: u64); + os.exit(1); + }; cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8): i64); @@ -19310,6 +19369,7 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s emitoff((slot_off + 32): i64); emitline("(BP)\n"); }; + }; }; emitline("\tMOVQ\t$"); emitint(nested: i64); @@ -19363,6 +19423,40 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s os.exit(1); }; }; + // #37: >32B box read (insts[pc], t.N, s.f) — cgexpr left + // its ADDRESS in AX; copy the whole box from memory, pad, + // tag-remap — the mem-based twin of the ident arm above. + // Mirrors cstage cg_widen_tagged_store's memread arm. + if (taggedmemread(c, src)) { + let su37: *tinfo = src.type_: *tinfo; + for (su37 != nil && su37.kind == tykind.TY_NAMED) { + su37 = su37.under; + }; + let ssz37: i32 = su37.size: i32; + cgexpr(c, src); + let mk37: i32 = 0; + for (mk37 < ssz37) { + emitline("\tMOVQ\t"); + emitdispreg(mk37: i64, "AX"); + emitline(", DX\n"); + emitline("\tMOVQ\tDX, "); + emitoff((slot_off + mk37): i64); + emitline("(BP)\n"); + mk37 += 8; + }; + if (ssz37 < slot_sz) { + emitline("\tXORQ\tAX, AX\n"); + let pp37: i32 = ssz37; + for (pp37 < slot_sz) { + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + pp37): i64); + emitline("(BP)\n"); + pp37 += 8; + }; + }; + cgwidentagremap(c, dt, src.type_: *tinfo, slot_off); + return; + }; cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff(slot_off: i64); @@ -19384,6 +19478,23 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s }; return; }; + // #37 (rule 7): a >32B TAGGED source of a kind the resolver arms + // above don't carry (deref/cast/unwrap/...) would fall to the + // scalar word0 arm below and silently truncate — keyed on the + // stamped src.type_ (kind-blind), the twin of cstage + // cg_widen_tagged_store's generic-else bound. Surfaced by + // reviewer-37's `let w = *p` probe on a 56B box: cstage loud, + // wwstage silent (rule-10 break). + let sf37: *tinfo = src.type_: *tinfo; + for (sf37 != nil && sf37.kind == tykind.TY_NAMED) { + sf37 = sf37.under; + }; + if (sf37 != nil && sf37.kind == tykind.TY_TAGGED + && sf37.size: i32 > TUPLE_GPCAP * 8) { + let m37f: str = "#37: >32B tagged source of a non-mem-based kind unwired (rule 7)\n"; + os.write(2, m37f.ptr, m37f.len: u64); + os.exit(1); + }; // #242: tuple payload. Each element rides ONE register-ABI // eightbyte — scalar/float a single 8B word, a slice/str its 3-word // {ptr,len,cap} header (24B) — matching the tagged-return load @@ -20668,6 +20779,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); @@ -20801,6 +20919,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"); @@ -20905,6 +21029,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; @@ -21053,6 +21183,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); @@ -21074,6 +21226,7 @@ fn cgtypeassert(c: *cgen, n: *node) void = { emitline("(BP)\n"); }; }; + }; }; }; let want: i32 = cgtagvariantidx(c, scrutt, n.rhs); @@ -22150,6 +22303,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"); }; @@ -22209,6 +22369,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"); }; @@ -22264,6 +22431,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"); @@ -22653,7 +22825,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); @@ -22678,7 +22890,7 @@ fn cgmatch(c: *cgen, n: *node) void = { emitline("(BP)\n"); }; }; - }; + }; }; }; }; let endl: str = mklabel(c, "match_end"); @@ -23170,19 +23382,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) { @@ -31860,16 +32071,11 @@ fn cgreturn(c: *cgen, n: *node) void = { // cgwidentaggedstore's non-BP base. if (sretretsize(c, c.fnret) > 0) { let sa38v: i32 = localfind(c, "@sretarg"); - if (forwardtagged) { - // exact type, but only an N_CALL source - // sret's into outer's dest; a cursor - // source (N_INDEX/N_DOT) can't carry - // >32B (rule 7, #38b residual). - if (rhs.kind != nkind.N_CALL) { - let m38d: str = "#38b: >32B tagged return from a cursor source (N_INDEX/N_DOT) unsupported\n"; - os.write(2, m38d.ptr, m38d.len: u64); - os.exit(1); - }; + if (forwardtagged && rhs.kind == nkind.N_CALL) { + // exact-type N_CALL forward: inner sret's + // into outer's dest; an N_INDEX/N_DOT + // source routes through the widener's + // #37 mem-read arm below instead. c.sretforward = 1; cgexpr(c, rhs); emitline("\tMOVQ\t"); @@ -31886,9 +32092,13 @@ fn cgreturn(c: *cgen, n: *node) void = { ru38 = ru38.under; }; if (ru38 != nil) { + // #37 wired the N_INDEX/N_DOT mem-read into + // the widener; the remaining >32B kinds stay + // loud. if (ru38.kind == tykind.TY_TAGGED && rhs.kind != nkind.N_IDENT - && ru38.size: i32 > TUPLE_GPCAP * 8) { + && ru38.size: i32 > TUPLE_GPCAP * 8 + && !taggedmemread(c, rhs)) { let m38e: str = "#40: widening tagged return-forward of a >32B source needs mem-to-mem tag-remap (unwired)\n"; os.write(2, m38e.ptr, m38e.len: u64); os.exit(1); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 41d75b4e..6eb9d7be 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -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) { diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 71fdda80..37d7d311 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -851,16 +851,11 @@ fn cgreturn(c: *cgen, n: *node) void = { // cgwidentaggedstore's non-BP base. if (sretretsize(c, c.fnret) > 0) { let sa38v: i32 = localfind(c, "@sretarg"); - if (forwardtagged) { - // exact type, but only an N_CALL source - // sret's into outer's dest; a cursor - // source (N_INDEX/N_DOT) can't carry - // >32B (rule 7, #38b residual). - if (rhs.kind != nkind.N_CALL) { - let m38d: str = "#38b: >32B tagged return from a cursor source (N_INDEX/N_DOT) unsupported\n"; - os.write(2, m38d.ptr, m38d.len: u64); - os.exit(1); - }; + if (forwardtagged && rhs.kind == nkind.N_CALL) { + // exact-type N_CALL forward: inner sret's + // into outer's dest; an N_INDEX/N_DOT + // source routes through the widener's + // #37 mem-read arm below instead. c.sretforward = 1; cgexpr(c, rhs); emitline("\tMOVQ\t"); @@ -877,9 +872,13 @@ fn cgreturn(c: *cgen, n: *node) void = { ru38 = ru38.under; }; if (ru38 != nil) { + // #37 wired the N_INDEX/N_DOT mem-read into + // the widener; the remaining >32B kinds stay + // loud. if (ru38.kind == tykind.TY_TAGGED && rhs.kind != nkind.N_IDENT - && ru38.size: i32 > TUPLE_GPCAP * 8) { + && ru38.size: i32 > TUPLE_GPCAP * 8 + && !taggedmemread(c, rhs)) { let m38e: str = "#40: widening tagged return-forward of a >32B source needs mem-to-mem tag-remap (unwired)\n"; os.write(2, m38e.ptr, m38e.len: u64); os.exit(1); diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 04c1aba7..d1236356 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -900,6 +900,15 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { if (arg.kind == nkind.N_INDEX || arg.kind == nkind.N_DOT) { if (istaggedtype(c, arg)) { let isz: i32 = slotsize(c, arg); + // #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. Mirrors cstage. + if (taggedmemread(c, arg)) { + let m37g: str = "#37: >32B tagged arg from a mem-based read unwired (#35/#40-family follow-up)\n"; + os.write(2, m37g.ptr, m37g.len: u64); + os.exit(1); + }; if (isz > 24) { emitline("\tPUSHQ\tR8\n"); }; if (isz > 16) { emitline("\tPUSHQ\tCX\n"); }; if (isz > 8) { emitline("\tPUSHQ\tDX\n"); }; @@ -3023,6 +3032,23 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { return false; }; +// taggedmemread — #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 callsretsize). Every cursor-spill consumer +// must branch on this before reading AX as the tag. Mirrors cstage +// cg_tagged_memread. +fn taggedmemread(c: *cgen, e: *node) bool = { + if (e == nil) { return false; }; + if (e.kind != nkind.N_INDEX && e.kind != nkind.N_DOT) { return false; }; + let u: *tinfo = e.type_: *tinfo; + for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; }; + if (u == nil) { return false; }; + if (u.kind != tykind.TY_TAGGED) { return false; }; + return u.size: i32 > TUPLE_GPCAP * 8; +}; + // cgloadtaggedfield — load a tagged-union slot at `basereg`+foff // into the tagged-return ABI registers (AX=tag, DX=word0, CX=word1, // R8=word2). Slot sizes: 16B = (tag, word0), 24B = + word1, 32B @@ -3038,6 +3064,16 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { // only register loaded into that is NOT a target is BX, so AX- // or DX-rooted callers must spill first. fn cgloadtaggedfield(c: *cgen, basereg: str, foff: i32, slot_sz: i32) void = { + // #37: >32B box — leave its ADDRESS in AX (taggedmemread, the + // sret-receive convention); the 4-reg cursor walk below would + // truncate past payload word 2. Mirrors cstage's N_DOT + // TY_STRUCT/TY_PTR tagged arms. + if (slot_sz > TUPLE_GPCAP * 8) { + emitline("\tLEAQ\t"); + emitdispreg(foff: i64, basereg); + emitline(", AX\n"); + return; + }; // tag → AX emitline("\tMOVQ\t"); emitdispreg(foff: i64, basereg); @@ -3281,6 +3317,29 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s os.exit(1); }; }; + if (taggedmemread(c, src)) { + // #37: >32B box read — ADDRESS in AX; + // copy the inner box from memory into + // the payload area. Mirrors cstage. + cgexpr(c, src); + let mk: i32 = 0; + for (mk < ssz) { + emitline("\tMOVQ\t"); + emitdispreg(mk: i64, "AX"); + emitline(", DX\n"); + emitline("\tMOVQ\tDX, "); + emitoff((slot_off + 8 + mk): i64); + emitline("(BP)\n"); + mk += 8; + }; + } else { + // #37 (rule 7): >32B from a non-mem-based + // kind would spill an unfilled cursor. + if (ssz > TUPLE_GPCAP * 8) { + let m37a: str = "#37: >32B tagged payload from a non-mem-based source unwired (rule 7)\n"; + os.write(2, m37a.ptr, m37a.len: u64); + os.exit(1); + }; cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8): i64); @@ -3300,6 +3359,7 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s emitoff((slot_off + 32): i64); emitline("(BP)\n"); }; + }; }; emitline("\tMOVQ\t$"); emitint(nested: i64); @@ -3353,6 +3413,40 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s os.exit(1); }; }; + // #37: >32B box read (insts[pc], t.N, s.f) — cgexpr left + // its ADDRESS in AX; copy the whole box from memory, pad, + // tag-remap — the mem-based twin of the ident arm above. + // Mirrors cstage cg_widen_tagged_store's memread arm. + if (taggedmemread(c, src)) { + let su37: *tinfo = src.type_: *tinfo; + for (su37 != nil && su37.kind == tykind.TY_NAMED) { + su37 = su37.under; + }; + let ssz37: i32 = su37.size: i32; + cgexpr(c, src); + let mk37: i32 = 0; + for (mk37 < ssz37) { + emitline("\tMOVQ\t"); + emitdispreg(mk37: i64, "AX"); + emitline(", DX\n"); + emitline("\tMOVQ\tDX, "); + emitoff((slot_off + mk37): i64); + emitline("(BP)\n"); + mk37 += 8; + }; + if (ssz37 < slot_sz) { + emitline("\tXORQ\tAX, AX\n"); + let pp37: i32 = ssz37; + for (pp37 < slot_sz) { + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + pp37): i64); + emitline("(BP)\n"); + pp37 += 8; + }; + }; + cgwidentagremap(c, dt, src.type_: *tinfo, slot_off); + return; + }; cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff(slot_off: i64); @@ -3374,6 +3468,23 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s }; return; }; + // #37 (rule 7): a >32B TAGGED source of a kind the resolver arms + // above don't carry (deref/cast/unwrap/...) would fall to the + // scalar word0 arm below and silently truncate — keyed on the + // stamped src.type_ (kind-blind), the twin of cstage + // cg_widen_tagged_store's generic-else bound. Surfaced by + // reviewer-37's `let w = *p` probe on a 56B box: cstage loud, + // wwstage silent (rule-10 break). + let sf37: *tinfo = src.type_: *tinfo; + for (sf37 != nil && sf37.kind == tykind.TY_NAMED) { + sf37 = sf37.under; + }; + if (sf37 != nil && sf37.kind == tykind.TY_TAGGED + && sf37.size: i32 > TUPLE_GPCAP * 8) { + let m37f: str = "#37: >32B tagged source of a non-mem-based kind unwired (rule 7)\n"; + os.write(2, m37f.ptr, m37f.len: u64); + os.exit(1); + }; // #242: tuple payload. Each element rides ONE register-ABI // eightbyte — scalar/float a single 8B word, a slice/str its 3-word // {ptr,len,cap} header (24B) — matching the tagged-return load diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 1dd79d90..37971012 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -16910,6 +16910,15 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { if (arg.kind == nkind.N_INDEX || arg.kind == nkind.N_DOT) { if (istaggedtype(c, arg)) { let isz: i32 = slotsize(c, arg); + // #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. Mirrors cstage. + if (taggedmemread(c, arg)) { + let m37g: str = "#37: >32B tagged arg from a mem-based read unwired (#35/#40-family follow-up)\n"; + os.write(2, m37g.ptr, m37g.len: u64); + os.exit(1); + }; if (isz > 24) { emitline("\tPUSHQ\tR8\n"); }; if (isz > 16) { emitline("\tPUSHQ\tCX\n"); }; if (isz > 8) { emitline("\tPUSHQ\tDX\n"); }; @@ -19033,6 +19042,23 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { return false; }; +// taggedmemread — #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 callsretsize). Every cursor-spill consumer +// must branch on this before reading AX as the tag. Mirrors cstage +// cg_tagged_memread. +fn taggedmemread(c: *cgen, e: *node) bool = { + if (e == nil) { return false; }; + if (e.kind != nkind.N_INDEX && e.kind != nkind.N_DOT) { return false; }; + let u: *tinfo = e.type_: *tinfo; + for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; }; + if (u == nil) { return false; }; + if (u.kind != tykind.TY_TAGGED) { return false; }; + return u.size: i32 > TUPLE_GPCAP * 8; +}; + // cgloadtaggedfield — load a tagged-union slot at `basereg`+foff // into the tagged-return ABI registers (AX=tag, DX=word0, CX=word1, // R8=word2). Slot sizes: 16B = (tag, word0), 24B = + word1, 32B @@ -19048,6 +19074,16 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { // only register loaded into that is NOT a target is BX, so AX- // or DX-rooted callers must spill first. fn cgloadtaggedfield(c: *cgen, basereg: str, foff: i32, slot_sz: i32) void = { + // #37: >32B box — leave its ADDRESS in AX (taggedmemread, the + // sret-receive convention); the 4-reg cursor walk below would + // truncate past payload word 2. Mirrors cstage's N_DOT + // TY_STRUCT/TY_PTR tagged arms. + if (slot_sz > TUPLE_GPCAP * 8) { + emitline("\tLEAQ\t"); + emitdispreg(foff: i64, basereg); + emitline(", AX\n"); + return; + }; // tag → AX emitline("\tMOVQ\t"); emitdispreg(foff: i64, basereg); @@ -19291,6 +19327,29 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s os.exit(1); }; }; + if (taggedmemread(c, src)) { + // #37: >32B box read — ADDRESS in AX; + // copy the inner box from memory into + // the payload area. Mirrors cstage. + cgexpr(c, src); + let mk: i32 = 0; + for (mk < ssz) { + emitline("\tMOVQ\t"); + emitdispreg(mk: i64, "AX"); + emitline(", DX\n"); + emitline("\tMOVQ\tDX, "); + emitoff((slot_off + 8 + mk): i64); + emitline("(BP)\n"); + mk += 8; + }; + } else { + // #37 (rule 7): >32B from a non-mem-based + // kind would spill an unfilled cursor. + if (ssz > TUPLE_GPCAP * 8) { + let m37a: str = "#37: >32B tagged payload from a non-mem-based source unwired (rule 7)\n"; + os.write(2, m37a.ptr, m37a.len: u64); + os.exit(1); + }; cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8): i64); @@ -19310,6 +19369,7 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s emitoff((slot_off + 32): i64); emitline("(BP)\n"); }; + }; }; emitline("\tMOVQ\t$"); emitint(nested: i64); @@ -19363,6 +19423,40 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s os.exit(1); }; }; + // #37: >32B box read (insts[pc], t.N, s.f) — cgexpr left + // its ADDRESS in AX; copy the whole box from memory, pad, + // tag-remap — the mem-based twin of the ident arm above. + // Mirrors cstage cg_widen_tagged_store's memread arm. + if (taggedmemread(c, src)) { + let su37: *tinfo = src.type_: *tinfo; + for (su37 != nil && su37.kind == tykind.TY_NAMED) { + su37 = su37.under; + }; + let ssz37: i32 = su37.size: i32; + cgexpr(c, src); + let mk37: i32 = 0; + for (mk37 < ssz37) { + emitline("\tMOVQ\t"); + emitdispreg(mk37: i64, "AX"); + emitline(", DX\n"); + emitline("\tMOVQ\tDX, "); + emitoff((slot_off + mk37): i64); + emitline("(BP)\n"); + mk37 += 8; + }; + if (ssz37 < slot_sz) { + emitline("\tXORQ\tAX, AX\n"); + let pp37: i32 = ssz37; + for (pp37 < slot_sz) { + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + pp37): i64); + emitline("(BP)\n"); + pp37 += 8; + }; + }; + cgwidentagremap(c, dt, src.type_: *tinfo, slot_off); + return; + }; cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff(slot_off: i64); @@ -19384,6 +19478,23 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s }; return; }; + // #37 (rule 7): a >32B TAGGED source of a kind the resolver arms + // above don't carry (deref/cast/unwrap/...) would fall to the + // scalar word0 arm below and silently truncate — keyed on the + // stamped src.type_ (kind-blind), the twin of cstage + // cg_widen_tagged_store's generic-else bound. Surfaced by + // reviewer-37's `let w = *p` probe on a 56B box: cstage loud, + // wwstage silent (rule-10 break). + let sf37: *tinfo = src.type_: *tinfo; + for (sf37 != nil && sf37.kind == tykind.TY_NAMED) { + sf37 = sf37.under; + }; + if (sf37 != nil && sf37.kind == tykind.TY_TAGGED + && sf37.size: i32 > TUPLE_GPCAP * 8) { + let m37f: str = "#37: >32B tagged source of a non-mem-based kind unwired (rule 7)\n"; + os.write(2, m37f.ptr, m37f.len: u64); + os.exit(1); + }; // #242: tuple payload. Each element rides ONE register-ABI // eightbyte — scalar/float a single 8B word, a slice/str its 3-word // {ptr,len,cap} header (24B) — matching the tagged-return load @@ -20668,6 +20779,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); @@ -20801,6 +20919,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"); @@ -20905,6 +21029,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; @@ -21053,6 +21183,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); @@ -21074,6 +21226,7 @@ fn cgtypeassert(c: *cgen, n: *node) void = { emitline("(BP)\n"); }; }; + }; }; }; let want: i32 = cgtagvariantidx(c, scrutt, n.rhs); @@ -22150,6 +22303,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"); }; @@ -22209,6 +22369,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"); }; @@ -22264,6 +22431,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"); @@ -22653,7 +22825,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); @@ -22678,7 +22890,7 @@ fn cgmatch(c: *cgen, n: *node) void = { emitline("(BP)\n"); }; }; - }; + }; }; }; }; let endl: str = mklabel(c, "match_end"); @@ -23170,19 +23382,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) { @@ -31860,16 +32071,11 @@ fn cgreturn(c: *cgen, n: *node) void = { // cgwidentaggedstore's non-BP base. if (sretretsize(c, c.fnret) > 0) { let sa38v: i32 = localfind(c, "@sretarg"); - if (forwardtagged) { - // exact type, but only an N_CALL source - // sret's into outer's dest; a cursor - // source (N_INDEX/N_DOT) can't carry - // >32B (rule 7, #38b residual). - if (rhs.kind != nkind.N_CALL) { - let m38d: str = "#38b: >32B tagged return from a cursor source (N_INDEX/N_DOT) unsupported\n"; - os.write(2, m38d.ptr, m38d.len: u64); - os.exit(1); - }; + if (forwardtagged && rhs.kind == nkind.N_CALL) { + // exact-type N_CALL forward: inner sret's + // into outer's dest; an N_INDEX/N_DOT + // source routes through the widener's + // #37 mem-read arm below instead. c.sretforward = 1; cgexpr(c, rhs); emitline("\tMOVQ\t"); @@ -31886,9 +32092,13 @@ fn cgreturn(c: *cgen, n: *node) void = { ru38 = ru38.under; }; if (ru38 != nil) { + // #37 wired the N_INDEX/N_DOT mem-read into + // the widener; the remaining >32B kinds stay + // loud. if (ru38.kind == tykind.TY_TAGGED && rhs.kind != nkind.N_IDENT - && ru38.size: i32 > TUPLE_GPCAP * 8) { + && ru38.size: i32 > TUPLE_GPCAP * 8 + && !taggedmemread(c, rhs)) { let m38e: str = "#40: widening tagged return-forward of a >32B source needs mem-to-mem tag-remap (unwired)\n"; os.write(2, m38e.ptr, m38e.len: u64); os.exit(1); diff --git a/test/wcc/941_tuple_slot_layout_run.c b/test/wcc/941_tuple_slot_layout_run.c index 5ef7283d..fdf63e06 100644 --- a/test/wcc/941_tuple_slot_layout_run.c +++ b/test/wcc/941_tuple_slot_layout_run.c @@ -540,14 +540,16 @@ static const struct row rows[] = { "};\n", 0, K_BUILDERR, "#22b: tagged element in an over-cap (sret) tuple " "return from a non-ident or widening source unwired" }, - /* #37-class bound this arc newly exposed: a >32B box (40B = - * (void|4x-u64-struct)) became transportable through the sret - * routes, but the t.N READ rides the 4-reg AX/DX/CX/R8 cursor — - * unbounded, cstage emitted INVALID ASM (tuple_rseq[4] OOB → w6a - * "unsupported MOVQ shape") while wwstage clamped k>=3 to R8 and - * silently dropped payload word 3. Now loud both stages; the - * mem-based box read is #37's fold. */ - { "b22_reject_big_box_read", + /* #37 FLIP (was the #22b loud bound; BUILDERR at master a72e815 + * with "tagged tuple element read exceeds the AX/DX/CX/R8 box + * cursor"): a >32B box t.N READ now leaves the box ADDRESS in AX + * (cg_tagged_memread, the sret-receive convention) and every + * cursor consumer copies from memory. The pre-#22b history: cstage + * emitted INVALID ASM (tuple_rseq[4] OOB), wwstage clamped k>=3 to + * R8 and silently dropped payload word 3. Readback covers the + * is-test (N_TYPETEST mem tag load) AND the full payload through a + * direct match on t.N (match-spill mem copy). */ + { "c37_big_box_t0_read", "package main;\n" "type four = struct { a: u64, b: u64, c: u64, d: u64, };\n" "fn g() (void | four) = {\n" @@ -560,11 +562,383 @@ static const struct row rows[] = { "};\n" "export fn main() i32 = {\n" " let t = pr();\n" - " if (!(t.0 is four)) { return 1; };\n" + " if (t.1 != 7) { return 1; };\n" + " if (!(t.0 is four)) { return 2; };\n" + " match (t.0) {\n" + " case let f: four => {\n" + " if (f.a != 1) { return 3; };\n" + " if (f.b != 2) { return 4; };\n" + " if (f.c != 3) { return 5; };\n" + " if (f.d != 4) { return 6; };\n" + " };\n" + " case void => { return 7; };\n" + " };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* #37 THE 5b DRIVER SHAPE: `match insts[pc]` where inst = + * (inst_lit | inst_repeat) = 56B (8 tag + 48 payload: 2x size + + * 2x 16B nested tagged). SILENT-WRONG at master a72e815 (gate- + * blind, byte-id both stages): the N_INDEX cursor load clamped at + * R8, dropping payload words 3+ — min's payload and max's whole + * box read stack garbage. Matches BOTH variants, reads every + * nested-tagged field back. */ + { "c37_idx_match_56", + "package main;\n" + "type inst_lit = rune;\n" + "type inst_repeat = struct {\n" + " id: size,\n" + " origin: size,\n" + " min: (void | size),\n" + " max: (void | size),\n" + "};\n" + "type inst = (inst_lit | inst_repeat);\n" + "export fn main() i32 = {\n" + " let insts: []inst = [];\n" + " let r = inst_repeat{id=7: size, origin=3: size, min=(11: size), max=(22: size)};\n" + " append(insts, r);\n" + " append(insts, ('A': inst_lit));\n" + " let pc: size = 0;\n" + " match (insts[pc]) {\n" + " case let ir: inst_repeat => {\n" + " if (ir.id != 7) { return 1; };\n" + " if (ir.origin != 3) { return 2; };\n" + " if (!(ir.min is size)) { return 3; };\n" + " if (ir.min as size != 11) { return 4; };\n" + " if (!(ir.max is size)) { return 5; };\n" + " if (ir.max as size != 22) { return 6; };\n" + " };\n" + " case let l: inst_lit => { return 7; };\n" + " };\n" + " match (insts[1]) {\n" + " case let l: inst_lit => {\n" + " if (l != 'A') { return 8; };\n" + " };\n" + " case let ir: inst_repeat => { return 9; };\n" + " };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* #37: str + nested-tagged payload variant — the big variant + * carries a 24B str header AND a nested 16B box (64B union); + * readback checks the str CONTENT survives the mem copy, not just + * the tag. SILENT-WRONG at master (words 3+ dropped). */ + { "c37_idx_match_str_nested", + "package main;\n" + "type lit2 = rune;\n" + "type rep2 = struct {\n" + " name: str,\n" + " min: (void | size),\n" + " max: (void | size),\n" + "};\n" + "type in2 = (lit2 | rep2);\n" + "export fn main() i32 = {\n" + " let xs: []in2 = [];\n" + " let r = rep2{name=\"deadbeef\", min=(2: size), max=(5: size)};\n" + " append(xs, r);\n" + " match (xs[0]) {\n" + " case let v: rep2 => {\n" + " if (v.name.len != 8) { return 1; };\n" + " if (v.name[0] != 'd') { return 2; };\n" + " if (v.name[7] != 'f') { return 3; };\n" + " if (!(v.min is size)) { return 4; };\n" + " if (v.min as size != 2) { return 5; };\n" + " if (v.max as size != 5) { return 6; };\n" + " };\n" + " case let l: lit2 => { return 7; };\n" + " };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* #37: let-bind from a >32B indexed read (widener subset arm + * mem-copy), then is-test directly on the indexed read (N_TYPETEST + * mem tag load), then reassign (cgassign route). SILENT-WRONG at + * master. */ + { "c37_idx_let_is_assign", + "package main;\n" + "type inst_lit = rune;\n" + "type inst_repeat = struct {\n" + " id: size,\n" + " origin: size,\n" + " min: (void | size),\n" + " max: (void | size),\n" + "};\n" + "type inst = (inst_lit | inst_repeat);\n" + "export fn main() i32 = {\n" + " let insts: []inst = [];\n" + " let r = inst_repeat{id=7: size, origin=3: size, min=(11: size), max=(22: size)};\n" + " append(insts, r);\n" + " append(insts, ('A': inst_lit));\n" + " if (!(insts[0] is inst_repeat)) { return 1; };\n" + " if (insts[0] is inst_lit) { return 2; };\n" + " let w = insts[0];\n" + " match (w) {\n" + " case let ir: inst_repeat => {\n" + " if (ir.max as size != 22) { return 3; };\n" + " };\n" + " case let l: inst_lit => { return 4; };\n" + " };\n" + " w = insts[1];\n" + " if (!(w is inst_lit)) { return 5; };\n" + " w = insts[0];\n" + " match (w) {\n" + " case let ir: inst_repeat => {\n" + " if (ir.min as size != 11) { return 6; };\n" + " };\n" + " case let l: inst_lit => { return 7; };\n" + " };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* #37: exact-type >32B tagged RETURN from an indexed source — was + * the "#38b: >32B tagged return from a cursor source" LOUD bound; + * now routes through the widener's mem-read arm into the sret + * dest. BUILDERR at master a72e815. */ + { "c37_idx_return", + "package main;\n" + "type inst_lit = rune;\n" + "type inst_repeat = struct {\n" + " id: size,\n" + " origin: size,\n" + " min: (void | size),\n" + " max: (void | size),\n" + "};\n" + "type inst = (inst_lit | inst_repeat);\n" + "fn pick(insts: []inst, pc: size) inst = {\n" + " return insts[pc];\n" + "};\n" + "export fn main() i32 = {\n" + " let insts: []inst = [];\n" + " let r = inst_repeat{id=7: size, origin=3: size, min=(11: size), max=(22: size)};\n" + " append(insts, r);\n" + " let w = pick(insts, 0);\n" + " match (w) {\n" + " case let ir: inst_repeat => {\n" + " if (ir.id != 7) { return 1; };\n" + " if (ir.min as size != 11) { return 2; };\n" + " };\n" + " case let l: inst_lit => { return 3; };\n" + " };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* #37: WIDENING let from a >32B indexed read — subset mem-copy + + * zero-pad + (identity) tag remap; and the 56B (>48B MEMORY-class) + * exact-type ARG from an indexed source (the pre-existing #38b + * memarg stack-blit path, kept working alongside the new emitters: + * cgplaceaddr never sees the cursor). */ + { "c37_widen_and_memarg", + "package main;\n" + "type inst_lit = rune;\n" + "type inst_repeat = struct {\n" + " id: size,\n" + " origin: size,\n" + " min: (void | size),\n" + " max: (void | size),\n" + "};\n" + "type inst = (inst_lit | inst_repeat);\n" + "type wide = (inst_lit | inst_repeat | str);\n" + "fn idof(i: inst) size = {\n" + " match (i) {\n" + " case let ir: inst_repeat => { return ir.id; };\n" + " case let l: inst_lit => { return 999; };\n" + " };\n" + " return 998;\n" + "};\n" + "export fn main() i32 = {\n" + " let insts: []inst = [];\n" + " let r = inst_repeat{id=7: size, origin=3: size, min=(11: size), max=(22: size)};\n" + " append(insts, r);\n" + " let w: wide = insts[0];\n" + " match (w) {\n" + " case let ir: inst_repeat => {\n" + " if (ir.max as size != 22) { return 1; };\n" + " };\n" + " case let l: inst_lit => { return 2; };\n" + " case let s2: str => { return 3; };\n" + " };\n" + " if (idof(insts[0]) != 7) { return 4; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* #37: NON-identity tag remap on a >32B mem-read widen — source + * union declares its variants in REVERSED order vs dst, so the + * widener's remap chain must fire (tag 0 -> 1) after the mem copy + * + zero-pad. SILENT-WRONG at master a72e815. */ + { "c37_widen_remap", + "package main;\n" + "type inst_lit = rune;\n" + "type inst_repeat = struct {\n" + " id: size,\n" + " origin: size,\n" + " min: (void | size),\n" + " max: (void | size),\n" + "};\n" + "type rev = (inst_repeat | inst_lit);\n" + "type wide = (inst_lit | inst_repeat | str);\n" + "export fn main() i32 = {\n" + " let xs: []rev = [];\n" + " let r = inst_repeat{id=7: size, origin=3: size, min=(11: size), max=(22: size)};\n" + " append(xs, r);\n" + " let w: wide = xs[0];\n" + " match (w) {\n" + " case let ir: inst_repeat => {\n" + " if (ir.id != 7) { return 1; };\n" + " if (!(ir.max is size)) { return 2; };\n" + " if (ir.max as size != 22) { return 3; };\n" + " };\n" + " case let l: inst_lit => { return 4; };\n" + " case let s2: str => { return 5; };\n" + " };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* ken X1 (/tmp/ken_22b_validation.md, impl-22b flag: ROW-WORTHY): + * the #22a/#23/#22b/#37 composition — a #23-constructed box + * ((void|st), st from an inline struct-lit with a NESTED TAGGED + * FIELD, box exactly 32B = cursor AT cap) transported through the + * #22b over-cap sret return, every field read back. */ + { "c37_x1_compose", + "package main;\n" + "type st = struct { a: size, m: (void | size), };\n" + "fn pr() ((void | st), size, size) = {\n" + " let e: (void | st) = st{a=4: size, m=(2: size)};\n" + " return (e, 5, 6);\n" + "};\n" + "export fn main() i32 = {\n" + " let t = pr();\n" + " if (!(t.0 is st)) { return 1; };\n" + " match (t.0) {\n" + " case let v: st => {\n" + " if (v.a != 4) { return 2; };\n" + " if (!(v.m is size)) { return 3; };\n" + " if (v.m as size != 2) { return 4; };\n" + " };\n" + " case void => { return 5; };\n" + " };\n" + " if (t.1 != 5) { return 6; };\n" + " if (t.2 != 6) { return 7; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* #37 boundary neighbor: a 32B-EXACT (cursor AT cap) tagged + * element INDEX read keeps the AX/DX/CX/R8 cursor — asm byte- + * identical to master (probed: p6 asm unchanged vs a72e815). + * Sister of b22_k5_box_at_cap (the t.N boundary). */ + { "c37_idx_at_cap", + "package main;\n" + "type box = (void | str);\n" + "export fn main() i32 = {\n" + " let bs: []box = [];\n" + " let s: str = \"abcdefgh\";\n" + " append(bs, s);\n" + " match (bs[0]) {\n" + " case let v: str => {\n" + " if (v.len != 8) { return 1; };\n" + " };\n" + " case void => { return 2; };\n" + " };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* #37 ptr-chained route: match on p.f through a *struct base — + * the cstage D_BX LEAQ arm / wwstage cgloadtaggedfield gate. The + * only #37 emitter pair otherwise covered by probes alone (ken + * M4); unlike the local-field shape (#42 cs!=ww divergence, + * disclosed), the ptr-base spelling is byte-id. SILENT-WRONG at + * master f272068 (exit 1, truncated payload). */ + { "c37_ptr_field_match", + "package main;\n" + "type inst_lit = rune;\n" + "type inst_repeat = struct {\n" + " id: size,\n" + " origin: size,\n" + " min: (void | size),\n" + " max: (void | size),\n" + "};\n" + "type inst = (inst_lit | inst_repeat);\n" + "type holder = struct { pre: size, f: inst, };\n" + "export fn main() i32 = {\n" + " let g = holder{pre=9: size, f=inst_repeat{id=7: size, origin=3: size, min=(11: size), max=(55: size)}};\n" + " let p = &g;\n" + " if (!(p.f is inst_repeat)) { return 1; };\n" + " match (p.f) {\n" + " case let ir: inst_repeat => {\n" + " if (ir.id != 7) { return 2; };\n" + " if (ir.max as size != 55) { return 3; };\n" + " };\n" + " case let l: inst_lit => { return 4; };\n" + " };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* #37 non-ident INDEX base: (*p)[i] rides the cgindex fallback + * arm (AX already the element address — the emitter that LEAVES + * AX rather than LEAQing). SILENT-WRONG at master f272068. */ + { "c37_deref_idx_match", + "package main;\n" + "type inst_lit = rune;\n" + "type inst_repeat = struct {\n" + " id: size,\n" + " origin: size,\n" + " min: (void | size),\n" + " max: (void | size),\n" + "};\n" + "type inst = (inst_lit | inst_repeat);\n" + "export fn main() i32 = {\n" + " let insts: []inst = [];\n" + " let r = inst_repeat{id=7: size, origin=3: size, min=(11: size), max=(22: size)};\n" + " append(insts, r);\n" + " let p = &insts;\n" + " match ((*p)[0]) {\n" + " case let ir: inst_repeat => {\n" + " if (ir.id != 7) { return 1; };\n" + " if (ir.max as size != 22) { return 2; };\n" + " };\n" + " case let l: inst_lit => { return 3; };\n" + " };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* #37 loud-symmetry pin (reviewer-37): a >32B tagged WIDEN/let + * source of a non-mem-based kind (deref) louds on BOTH stages. + * Pre-amendment wwstage fell to the scalar word0 arm and ran + * silent-wrong while cstage loud'ed (rule-10 break). */ + { "c37_reject_deref_widen", + "package main;\n" + "type inst_lit = rune;\n" + "type inst_repeat = struct {\n" + " id: size,\n" + " origin: size,\n" + " min: (void | size),\n" + " max: (void | size),\n" + "};\n" + "type inst = (inst_lit | inst_repeat);\n" + "export fn main() i32 = {\n" + " let insts: []inst = [];\n" + " let r = inst_repeat{id=7: size, origin=3: size, min=(11: size), max=(22: size)};\n" + " append(insts, r);\n" + " let p = &insts[0];\n" + " let w = *p;\n" + " if (!(w is inst_repeat)) { return 1; };\n" " return 0;\n" "};\n", 0, - K_BUILDERR, "tagged tuple element read exceeds the AX/DX/CX/R8 " - "box cursor (mem-based read is the #37 family" }, + K_BUILDERR, "#37: >32B tagged source of a non-mem-based kind" }, + /* #37 loud-symmetry pin (reviewer-37): match on a >32B deref + * scrutinee louds on BOTH stages. Pre-amendment wwstage's + * matchscrutt returned nil for N_UN, spillsz defaulted under + * cap, and the guard was blind — silent-wrong vs cstage loud. */ + { "c37_reject_deref_match", + "package main;\n" + "type inst_lit = rune;\n" + "type inst_repeat = struct {\n" + " id: size,\n" + " origin: size,\n" + " min: (void | size),\n" + " max: (void | size),\n" + "};\n" + "type inst = (inst_lit | inst_repeat);\n" + "export fn main() i32 = {\n" + " let insts: []inst = [];\n" + " let r = inst_repeat{id=7: size, origin=3: size, min=(11: size), max=(22: size)};\n" + " append(insts, r);\n" + " let p = &insts[0];\n" + " match (*p) {\n" + " case let ir: inst_repeat => { return 1; };\n" + " case let l: inst_lit => { return 2; };\n" + " };\n" + " return 0;\n" + "};\n", 0, + K_BUILDERR, "#37: >32B tagged match scrutinee from a non-mem-based source" }, /* ...but the >32B TRANSPORT itself (sret send mem-to-mem + MLET * copy-out) is size-generic and correct — only the cursor read is * bounded. is-checks on the destructured local read the tag from