From 322667b820cee87352bf50b7cb1c68016f7045cb Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 5 Jun 2026 07:13:00 +0900 Subject: [PATCH] =?UTF-8?q?cgen:=20#35/#46=20Family=20C=20=E2=80=94=20tagg?= =?UTF-8?q?ed=20transport=20from=20deref/cast/unwrap=20sources=20goes=20me?= =?UTF-8?q?m-based,=20both=20stages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A tagged value reaching a transport consumer from a DEREF, CAST, or UNWRAP source materialized as ONE scalar word — the N_UN(STAR) arm's localloadop pulled word0 (the tag) and every cursor consumer then transported garbage payload (cs pushed stale DX, ww stored 0/garbage; divergent asm AND silent-wrong on both stages — ken f35 asm proof + ken37v D3a/D3b). Consumers × sources × sizes all wrong: arg push, let-init, assign, return, match scrutinee, as, widen — from *p at 16/24/32B, from identity/widening tagged casts, and from `?`/`!` whose success variant is itself tagged (nested box: payload words dropped). `is` and ww-match-16B passed only by stale-register luck. Fix extends the #37 mem-based machinery (26d3754) below the 32B cap instead of re-deriving: - cg_tagged_memread/taggedmemread: an N_UN(STAR) deref of a non-nullable tagged box is mem-based at ANY size — the pointer value IS the box address. The N_UN emitter skips the scalar load (joins the TY_FN/TY_ARRAY value-is-address skip); the existing size-generic memread arms in widen-store/match/as pick the source up unchanged, `is` loads the tag through the address. - arg push: the memread fatal becomes the mem-based push (words high→low from (AX)) — closes #35's word0-only push and wires the 33-48B INDEX/DOT loud as a side effect. Pop side drains via pushargsrev's returned word count, unchanged. - cg_tagged_castpeel/taggedcastpeel: tagged→tagged casts are transport-transparent; peel at the call-arg widen decision and the widen-store entry so the ident/deref arms see the carrier and the remap keys on the operand type. The identity-only subset (cg_tagged_idcastpeel) applies at is/as/match, which key variant indices on the scrutinee's own type; surviving non-identity casts die LOUD there and at the widen cursor arms (rule 7) instead of word0 garbage. The peel also wires 929's >48B memarg same-type cast row (place resolves post-peel; loud pin flips to a run row). - tryprop/tryunw: IDENT sources load the cursor from their slot, memread sources from the box address (≤32B); >32B non-call and global tagged idents die loud (rule 7). A TAGGED success variant shifts the nested box past the outer tag (twin of the #241 tuple shift) — closes the unwrap-source half (ken unw16). - wwstage alignment UP to the cstage type-keyed routes: rhstaggedabicall admits N_UN deref + N_TRYPROP/N_TRYUNW (stamped type), matchscrutt carries the N_UN stamped type (spill size + variant indices; was nil → tag-0 clamp + mis-sized spill), cgreturn routes memread sources through the widener (the fall-through wrapped the un-deref'd POINTER as payload), and pushargsrev's aistagged gate admits the deref kind. Emitters and consumers ship as ONE commit: they share the memread contract, and splitting opens a transient window where a wired emitter hands an address to an unwired consumer — the #61/#37 route-sharing fuse. The test flips ride along because they pin the flip itself: 941's two #37 deref loud-symmetry pins become run rows (the loud is now wired), 929's fail_rvalue_cast becomes memarg_idcast_peeled. No-drift bar held: ≤32B IDENT/INDEX/DOT sources emit byte-identical asm vs master 4c46d3a (probe corpus nd1: ident let/match/arg, struct-field, indexed element, call, nullable, ident-widen — both stages IDENTICAL); is-on-deref is incidentally byte-identical too (the tag load moved from the emitter to the consumer). Tests: 941 grows 252→272 checks — ken's exact f35/D3a/D3b shapes, each consumer × source × size cell (16/24/32/56B, str + struct payloads), neighbor-guard row, identity-cast arg, widening-cast let (payload checked — the old cs pass was is-only luck), success-first unwrap-to-tagged + ident/deref unwrap, the 56B slice-deref let+match flips (payload-pinned), and 2 rule-7 loud pins (global tagged `?`, cast-to-third-union). Reviewer-C rows commit ken's remaining adversarial shapes (gC1 deref-wrapped cast arg, gC2 void-variant deref, gC3 slice-element-pointer deref, gC6 56B memarg-leg deref arg — that one a regression pin, already place-resolved at base), a 40B deref ARG (the 33-48B mem-push leg, silent word0 at base), and a multi-arg pop-balance row (tagged-deref arg mid-list, called twice — the original #35 1-push-2-pops symptom). At base 4c46d3a the impl rows fail 53/254 (silent-wrong exits, cs≠ww asm, missing louds); the reviewer rows kill at base too (flip rows LOUD, others wrong-exit) except the gC6 pin. At HEAD 272/272 + 929 22 rows + test-unit 284. Residuals (filed separately, pre-existing): #216 success-tag divergence — error-FIRST unions emit CMPQ $1 (cs s_tag) vs CMPQ $0 (ww first-param) and ww's first-param success-type read misses the tagged shift; match-expr tagged yield is cstage-checker-rejected while ww runs it (the #34/#43 acceptance family); `as` binding a STRUCT payload at let-init stays loud ("aggregate init from unhandled rhs shape"); wwstage tagged-source arg-widen-into-WIDER-slot (the pushargsrev #21-comment out-of-scope boundary, task #55) — the deref leg of that family is now cstage-correct via the widen-store memread arm but stays wwstage-silent-wrong (joins the pre-existing INDEX leg; ident leg runs by prefix-luck under-push); truncating scalar cast as a box payload drops the conversion (task #56, both stages, untouched by the peel — scalar→scalar casts are never peeled). --- cmd/w6c/cgen.c | 301 +++++++++++++++++--- selfhost/cmd/w6c/main.combined.ww | 339 +++++++++++++++++++++-- selfhost/cmd/wcc/cgenexpr.ww | 182 ++++++++++-- selfhost/cmd/wcc/cgenstmt.ww | 10 + selfhost/cmd/wcc/cgenutil.ww | 147 +++++++++- selfhost/cmd/wwdump/main.combined.ww | 339 +++++++++++++++++++++-- test/wcc/929_tagged_memarg_run.c | 16 +- test/wcc/941_tuple_slot_layout_run.c | 397 +++++++++++++++++++++++++-- 8 files changed, 1592 insertions(+), 139 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 1a85d24f..7281fecc 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -653,17 +653,69 @@ tagged_memarg_size(Type *t) * 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. */ + * consumer must branch on this before reading AX as the tag. + * Family C (#35/#46): a DEREF source is mem-based at ANY size — the + * pointer value IS the box address, so the N_UN(STAR) emitter skips + * the scalar load (which carried only the tag word) and the + * consumers copy from memory. ≤32B INDEX/DOT keep the cursor + * byte-for-byte (the #37 no-drift bar); the nullable one-word fold + * stays a scalar deref. */ static int cg_tagged_memread(Node *e) { Type *u; - if (e == NULL || (e->kind != N_INDEX && e->kind != N_DOT)) return 0; + if (e == NULL) return 0; + if (e->kind == N_UN && e->op == TK_STAR) { + u = type_chase_named(e->type); + return u && u->kind == TY_TAGGED && !u->nullable + && u->size > 8; + } + if (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; } +/* cg_tagged_castpeel — Family C (#35): a tagged→tagged cast is + * transport-transparent — the operand's box IS the value; transport + * consumers (widen-store, arg push) derive the remap from the + * operand's type. Peeling exposes the ident/deref carrier their + * source arms key on; cgexpr on the cast node itself collapses to + * one word. Concrete-variant casts (`7: size`) keep their node so + * variant-tag lookup sees the cast's type. The nullable one-word + * fold never spills a cursor — excluded. */ +static Node * +cg_tagged_castpeel(Node *e) +{ + while (e && e->kind == N_CAST && e->lhs) { + Type *cu = type_chase_named(e->type); + Type *iu = type_chase_named(e->lhs->type); + if (cu == NULL || cu->kind != TY_TAGGED || cu->nullable) + break; + if (iu == NULL || iu->kind != TY_TAGGED || iu->nullable) + break; + e = e->lhs; + } + return e; +} + +/* cg_tagged_idcastpeel — the IDENTITY-only subset of the peel for + * consumers that key variant indices on the scrutinee's own type + * (is/as/match): same-type casts are no-ops there, but a WIDENING + * cast changes the tag numbering and must NOT be peeled — those die + * loud at the consumer's cast catch-all instead. */ +static Node * +cg_tagged_idcastpeel(Node *e) +{ + while (e && e->kind == N_CAST && e->lhs + && type_eq(e->type, e->lhs->type)) { + Type *cu = type_chase_named(e->type); + if (cu == NULL || cu->kind != TY_TAGGED) break; + e = e->lhs; + } + return e; +} + /* type_isnullable — TY_TAGGED with the (*T | void) one-word fold. */ static int type_isnullable(Type *t) @@ -2380,6 +2432,12 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, if (via_outer) goto copy_out; return; } + /* Family C (#35): a tagged→tagged cast is transport-transparent + * — peel it so the ident/deref/memread source arms below see the + * carrier and the remap keys on the operand's type. Pre-#35 the + * cast node fell to the cursor arm, whose cgexpr collapsed to + * word0 (`let w: un3 = (v: un3)` stored garbage payload). */ + src = cg_tagged_castpeel(src); /* `expr: TaggedAlias` where the cast's destination IS the union * itself is a widening, not a re-interpret. cgexpr on the cast * leaves the inner expression's register shape (str: AX=ptr, @@ -2475,6 +2533,14 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, fatal("#37: >32B tagged payload from " "a non-mem-based source (kind %d) " "unwired (rule 7)", src->kind); + /* Family C catch-all (rule 7): a tagged + * cast surviving cg_tagged_castpeel (cast + * to a THIRD union) has no cursor — loud, + * not word0 garbage. */ + if (src->kind == N_CAST) + fatal("#35: tagged cast source shape " + "unwired at the widen nested arm " + "(rule 7)"); cgexpr(c, src, *locals_p); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8)); @@ -2532,6 +2598,12 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, fatal("#37: >32B tagged source of a non-mem-" "based kind (%d) unwired (rule 7)", src->kind); + /* Family C catch-all (rule 7): a tagged cast + * surviving cg_tagged_castpeel (cast to a THIRD + * union) has no cursor — loud, not word0 garbage. */ + if (src->kind == N_CAST) + fatal("#35: tagged cast source shape unwired " + "at the widen subset arm (rule 7)"); cgexpr(c, src, *locals_p); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 0)); @@ -4153,6 +4225,18 @@ cgexpr(Cg *c, Node *n, Local *locals) if (ru && (ru->kind == TY_FN || ru->kind == TY_ARRAY)) break; + /* Family C (#35/#46): a tagged box behind + * *p joins the mem-based class at ANY size + * (cg_tagged_memread) — AX = p's value IS + * the box address. The scalar load below + * pulled word0 (the tag) and every cursor + * consumer then transported garbage payload + * words — silent-wrong on both stages (the + * ken f35/D3a/D3b family). The nullable + * one-word fold stays a scalar deref. */ + if (ru && ru->kind == TY_TAGGED + && !ru->nullable && ru->size > 8) + break; } /* f64/f32 result rides X0 (SSE), not AX — an integer * MOVQ strands the value off the float ABI and the @@ -8258,6 +8342,15 @@ cgexpr(Cg *c, Node *n, Local *locals) } } } + /* Family C (#35): peel tagged→tagged casts FIRST so the + * widen decision below keys on the operand's type — an + * identity cast (`take((v: un))`) reduces to the ident + * fast path, a widening cast (`take((v: wider))`) trips + * widen[i] and re-boxes with the remap. cgexpr on the + * cast node itself collapses to one word (silent word0 + * push pre-#35). */ + for (int i = 0; i < argcount; i++) + args[i] = cg_tagged_castpeel(args[i]); /* widen[i]: param is tagged and arg needs re-layout. * - arg is a concrete variant (str/struct/scalar) — wrap * in the param's slot shape. @@ -8696,23 +8789,31 @@ 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)); - if (sz > 16) - ins1(c, A_PUSHQ, areg(D_CX)); - if (sz > 8) - ins1(c, A_PUSHQ, areg(D_DX)); - ins1(c, A_PUSHQ, areg(D_AX)); + /* #35 (Family C): a mem-based read left the + * box ADDRESS in AX — push the words from + * memory high→low, the mem twin of the + * cursor push below. Covers the any-size + * deref source and the 33-48B INDEX/DOT + * reads that loud-stopped here pre-#35. */ + if (cg_tagged_memread(args[i])) { + int msz = tagged_arg_size( + args[i]->type); + for (int k = msz - 8; k >= 0; k -= 8) { + ins2(c, A_MOVQ, amem(D_AX, k), + areg(D_DX)); + ins1(c, A_PUSHQ, areg(D_DX)); + } + } else { + int sz = tagged_arg_size( + args[i]->type); + if (sz > 24) + ins1(c, A_PUSHQ, areg(D_R8)); + if (sz > 16) + ins1(c, A_PUSHQ, areg(D_CX)); + if (sz > 8) + ins1(c, A_PUSHQ, areg(D_DX)); + ins1(c, A_PUSHQ, areg(D_AX)); + } } else if (tuparg_push) { /* #163: tuple ARG (param twin of #164's return). * cgexpr above left the tuple in the return-ABI @@ -9127,7 +9228,8 @@ cgexpr(Cg *c, Node *n, Local *locals) * Nullable folded `(*T | void)`: slot is one 8B word holding * the pointer; null IS the void variant. Discriminator = * value, not a separate tag. */ - Node *s = n->lhs; + /* Family C (#35): identity-cast peel — see the `is` twin. */ + Node *s = cg_tagged_idcastpeel(n->lhs); Type *st = s ? s->type : NULL; Type *su = (st && st->kind == TY_NAMED) ? st->under : st; int is_nullable = type_isnullable(st); @@ -9228,6 +9330,12 @@ cgexpr(Cg *c, Node *n, Local *locals) fatal("#37: >32B tagged match scrutinee from " "a non-mem-based source (kind %d) unwired " "(rule 7)", s->kind); + /* Family C catch-all (rule 7): a widening tagged + * cast scrutinee has no cursor — loud. */ + if (s->kind == N_CAST && !is_nullable + && su && su->kind == TY_TAGGED) + fatal("#35: tagged cast source shape unwired " + "at match (rule 7)"); cgexpr(c, s, locals); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0)); if (!is_nullable) { @@ -9370,18 +9478,54 @@ 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 " "#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; + /* Family C (#35/#46): non-call sources don't fill the + * AX/DX/CX/R8 cursor the unwrap below reads — an IDENT + * loads it from its slot, a mem-based read (deref at any + * size, >32B INDEX/DOT) from the box address cgexpr left + * in AX. Both were silent word0 unwraps pre-#35. >32B + * stays loud (the cursor can't carry it; #40 family). */ + if (u && u->kind == TY_TAGGED && !u->nullable + && (int)u->size > TUPLE_GPCAP * 8 + && n->lhs->kind != N_CALL) + fatal("#37: `?` on a >32B mem-based tagged read " + "unwired (#40-family follow-up)"); + if (n->lhs && n->lhs->kind == N_IDENT + && u && u->kind == TY_TAGGED && !u->nullable) { + int boff = localfind(locals, n->lhs->str); + int bsz = (int)u->size; + /* rule 7: a module-level tagged `g?` has no frame + * slot; the global cursor load is unwired. */ + if (boff == 0) + fatal("#35: `?` on a global tagged ident " + "unwired (rule 7)"); + if (bsz > 24) + ins2(c, A_MOVQ, amem(D_BP, boff + 24), + areg(D_R8)); + if (bsz > 16) + ins2(c, A_MOVQ, amem(D_BP, boff + 16), + areg(D_CX)); + if (bsz > 8) + ins2(c, A_MOVQ, amem(D_BP, boff + 8), + areg(D_DX)); + ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX)); + } else if (cg_tagged_memread(n->lhs)) { + int bsz = (int)type_chase_named(n->lhs->type)->size; + cgexpr(c, n->lhs, locals); + if (bsz > 24) + ins2(c, A_MOVQ, amem(D_AX, 24), areg(D_R8)); + if (bsz > 16) + ins2(c, A_MOVQ, amem(D_AX, 16), areg(D_CX)); + if (bsz > 8) + ins2(c, A_MOVQ, amem(D_AX, 8), areg(D_DX)); + ins2(c, A_MOVQ, amem(D_AX, 0), areg(D_AX)); + } else + cgexpr(c, n->lhs, locals); Type *r = cg_ret_type; if (r && r->kind == TY_NAMED) r = r->under; if (u && u->kind == TY_TAGGED && u->nullable) { @@ -9444,6 +9588,24 @@ cgexpr(Cg *c, Node *n, Local *locals) cg_tagged_tuple_payload_shift(c, stu); break; } + /* Family C (#35, unwrap source): a TAGGED success + * variant is a NESTED box (ww keeps nested unions + * un-flattened) riding the payload words intact — + * shift past the outer tag so consumers see the + * standard AX=tag cursor. The scalar MOVQ DX,AX + * below carried only the inner tag and dropped the + * payload (ken unw16). Nullable folds to one word + * and stays on the scalar move. */ + if (stu && stu->kind == TY_TAGGED && !stu->nullable) { + ins2(c, A_MOVQ, areg(D_DX), areg(D_AX)); + if (stu->size > 8) + ins2(c, A_MOVQ, areg(D_CX), + areg(D_DX)); + if (stu->size > 16) + ins2(c, A_MOVQ, areg(D_R8), + areg(D_CX)); + break; + } } if (success_is_str) { /* str IS []u8: success value arrives in the tagged @@ -9465,13 +9627,45 @@ 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; + /* Family C (#35/#46): see the N_TRYPROP twin — ident loads + * the cursor from its slot, a mem-based read from the box + * address; >32B non-call stays loud (#40 family). */ + if (u && u->kind == TY_TAGGED && !u->nullable + && (int)u->size > TUPLE_GPCAP * 8 + && n->lhs->kind != N_CALL) + fatal("#37: `!` on a >32B mem-based tagged read " + "unwired (#40-family follow-up)"); + if (n->lhs && n->lhs->kind == N_IDENT + && u && u->kind == TY_TAGGED && !u->nullable) { + int boff = localfind(locals, n->lhs->str); + int bsz = (int)u->size; + if (boff == 0) + fatal("#35: `!` on a global tagged ident " + "unwired (rule 7)"); + if (bsz > 24) + ins2(c, A_MOVQ, amem(D_BP, boff + 24), + areg(D_R8)); + if (bsz > 16) + ins2(c, A_MOVQ, amem(D_BP, boff + 16), + areg(D_CX)); + if (bsz > 8) + ins2(c, A_MOVQ, amem(D_BP, boff + 8), + areg(D_DX)); + ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX)); + } else if (cg_tagged_memread(n->lhs)) { + int bsz = (int)type_chase_named(n->lhs->type)->size; + cgexpr(c, n->lhs, locals); + if (bsz > 24) + ins2(c, A_MOVQ, amem(D_AX, 24), areg(D_R8)); + if (bsz > 16) + ins2(c, A_MOVQ, amem(D_AX, 16), areg(D_CX)); + if (bsz > 8) + ins2(c, A_MOVQ, amem(D_AX, 8), areg(D_DX)); + ins2(c, A_MOVQ, amem(D_AX, 0), areg(D_AX)); + } else + cgexpr(c, n->lhs, locals); if (u && u->kind == TY_TAGGED && u->nullable) { char *cont = mklabel(c, "tryunw_ok"); ins2(c, A_CMPQ, aimm(0), areg(D_AX)); @@ -9505,6 +9699,19 @@ cgexpr(Cg *c, Node *n, Local *locals) cg_tagged_tuple_payload_shift(c, stu); break; } + /* Family C (#35): TAGGED success = nested box on the + * payload words — shift past the outer tag (see the + * N_TRYPROP twin). */ + if (stu && stu->kind == TY_TAGGED && !stu->nullable) { + ins2(c, A_MOVQ, areg(D_DX), areg(D_AX)); + if (stu->size > 8) + ins2(c, A_MOVQ, areg(D_CX), + areg(D_DX)); + if (stu->size > 16) + ins2(c, A_MOVQ, areg(D_R8), + areg(D_CX)); + break; + } } if (success_is_str) { /* str IS []u8: success arrives DX=ptr, CX=len, R8=cap @@ -9527,12 +9734,25 @@ cgexpr(Cg *c, Node *n, Local *locals) && cg_sret_retsize(n->lhs->type) > 0) 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)) + /* Family C (#35): identity casts are transport no-ops — + * peel so the ident emission carries; a WIDENING tagged + * cast renumbers the tag the compare below keys on and + * has no wired source arm — loud, not a mis-keyed test. */ + Node *tl = cg_tagged_idcastpeel(n->lhs); + { + Type *tcu = tl ? type_chase_named(tl->type) : NULL; + if (tl && tl->kind == N_CAST && tcu + && tcu->kind == TY_TAGGED && !tcu->nullable) + fatal("#35: tagged cast source shape unwired " + "at `is` (rule 7)"); + } + cgexpr(c, tl, locals); + /* #37: a mem-based box read (>32B INDEX/DOT, any-size + * deref) leaves its ADDRESS in AX — load the tag word + * from memory before the compare. */ + if (cg_tagged_memread(tl)) ins2(c, A_MOVQ, amem(D_AX, 0), areg(D_AX)); - Type *u = n->lhs ? n->lhs->type : NULL; + Type *u = tl ? tl->type : NULL; if (u && u->kind == TY_NAMED) u = u->under; Type *vt = n->rhs ? n->rhs->type : NULL; char *ne = mklabel(c, "is_ne"); @@ -9572,7 +9792,8 @@ cgexpr(Cg *c, Node *n, Local *locals) * Nullable: the slot's word IS the pointer. *T variant * asserts non-null; void variant asserts null. The value * left in AX after the check is the pointer itself. */ - Node *s = n->lhs; + /* Family C (#35): identity-cast peel — see the `is` twin. */ + Node *s = cg_tagged_idcastpeel(n->lhs); Type *st = s ? s->type : NULL; Type *u = (st && st->kind == TY_NAMED) ? st->under : st; Type *vt = n->type; @@ -9617,6 +9838,12 @@ cgexpr(Cg *c, Node *n, Local *locals) fatal("#37: `as` on a >32B tagged value from " "a non-mem-based source (kind %d) unwired " "(rule 7)", s->kind); + /* Family C catch-all (rule 7): a widening tagged + * cast source has no cursor — loud. */ + if (s->kind == N_CAST && u && u->kind == TY_TAGGED + && !u->nullable) + fatal("#35: tagged cast source shape unwired " + "at `as` (rule 7)"); 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)) { diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index b7bdb488..1f4ecc2f 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -16121,6 +16121,15 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { let nextparam: *node = nil; if (param != nil) { nextparam = param.next; }; let rest: i32 = pushargsrev(c, arg.next, nextparam, memphase); + // Family C (#35): peel tagged→tagged casts FIRST so every gate + // below keys on the operand — an identity cast reduces to the + // ident fast path, a widening cast trips the widen branch with + // the operand as source. cgexpr on the cast node collapses to + // one word (silent word0 push pre-#35). Mirrors cstage's + // args[i] = cg_tagged_castpeel(args[i]) pre-pass; the cgcall + // pop side counts via pushargsrev's return, so the drain stays + // balanced. + arg = taggedcastpeel(c, arg); // #38b MEMORY-class detection: keyed off the declared param's // type (so widening into a >48B slot is caught), else the arg's // own stamped type (fn-ptr callee carries no param nodes). @@ -16314,6 +16323,19 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { }; }; }; + // Family C (#35): a DEREF source is a + // tagged box too (mem-based, any size) + // — without this gate the widening + // scalar branch boxed the box. Same + // slotsize key as the N_INDEX/N_DOT + // stamped-carrier arms above. + if (arg.kind == nkind.N_UN && arg.op == tkind.TK_STAR) { + if (istaggedtype(c, arg)) { + if (slotsize(c, arg) == slotsize(c, ptype)) { + aistagged = true; + }; + }; + }; if (!aistagged) { widensz = slotsize(c, ptype); let tagged: *node = resolvetagged(c, ptype); @@ -16907,17 +16929,26 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { // keeps the stamped-carrier kind gate (#67 pattern) — the // remaining kinds (deref/cast/unwrap) are word0-only reads today, // filed residual. - if (arg.kind == nkind.N_INDEX || arg.kind == nkind.N_DOT) { + if (arg.kind == nkind.N_INDEX || arg.kind == nkind.N_DOT + || (arg.kind == nkind.N_UN && arg.op == tkind.TK_STAR)) { 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. + // #35 (Family C): a mem-based read left the box + // ADDRESS in AX — push the words from memory + // high→low, the mem twin of the cursor push below. + // Covers the any-size deref source and the 33-48B + // INDEX/DOT reads that loud-stopped here pre-#35. + // 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); + let mk35: i32 = isz - 8; + for (mk35 >= 0) { + emitline("\tMOVQ\t"); + emitdispreg(mk35: i64, "AX"); + emitline(", DX\n"); + emitline("\tPUSHQ\tDX\n"); + mk35 -= 8; + }; + return rest + isz / 8; }; if (isz > 24) { emitline("\tPUSHQ\tR8\n"); }; if (isz > 16) { emitline("\tPUSHQ\tCX\n"); }; @@ -18390,6 +18421,14 @@ fn matchscrutt(c: *cgen, scrut: *node) *node = { if (!istaggedtype(c, scrut)) { return nil; }; return scrut; }; + // Family C (#46): a DEREF scrutinee — stamped-carrier like the + // non-ident N_INDEX/N_DOT arms (`match (*p)` spill size + variant + // indices key off scrut.type_; pre-#46 nil here clamped the + // variant to 0 and mis-sized @match_spill). + if (k == nkind.N_UN && scrut.op == tkind.TK_STAR) { + if (!istaggedtype(c, scrut)) { return nil; }; + return scrut; + }; return nil; }; @@ -19053,6 +19092,17 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { if (src.kind == nkind.N_DOT) { if (typeistagged(src.type_: *tinfo)) { return true; }; }; + // Family C (#35/#46): a DEREF source is mem-based (taggedmemread, + // any size) — the widen-store's memread arm copies the box from + // the address cgexpr leaves in AX. An unwrap (`?`/`!`) source + // fills the cursor after the tagged-success payload shift (the + // cgtryprop/cgtryunw twin of cstage's kind-blind su-tagged arm). + if (src.kind == nkind.N_UN && src.op == tkind.TK_STAR) { + if (typeistagged(src.type_: *tinfo)) { return true; }; + }; + if (src.kind == nkind.N_TRYPROP || src.kind == nkind.N_TRYUNW) { + if (typeistagged(src.type_: *tinfo)) { return true; }; + }; return false; }; @@ -19063,8 +19113,21 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { // gates key separately on callsretsize). Every cursor-spill consumer // must branch on this before reading AX as the tag. Mirrors cstage // cg_tagged_memread. +// Family C (#35/#46): a DEREF source is mem-based at ANY size — the +// pointer value IS the box address, so cgun skips the scalar load +// (which carried only the tag) and consumers copy from memory. ≤32B +// INDEX/DOT keep the cursor byte-for-byte (the #37 no-drift bar); +// the nullable one-word fold stays a scalar deref. fn taggedmemread(c: *cgen, e: *node) bool = { if (e == nil) { return false; }; + if (e.kind == nkind.N_UN && e.op == tkind.TK_STAR) { + let du: *tinfo = e.type_: *tinfo; + for (du != nil && du.kind == tykind.TY_NAMED) { du = du.under; }; + if (du == nil) { return false; }; + if (du.kind != tykind.TY_TAGGED) { return false; }; + if (du.nullable != 0) { return false; }; + return du.size: i32 > 8; + }; 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; }; @@ -19073,6 +19136,49 @@ fn taggedmemread(c: *cgen, e: *node) bool = { return u.size: i32 > TUPLE_GPCAP * 8; }; +// taggedcastpeel — Family C (#35): a tagged→tagged cast is transport- +// transparent — the operand's box IS the value; transport consumers +// (widen-store, arg push) derive the remap from the operand's type. +// Peeling exposes the ident/deref carrier their source arms key on; +// cgexpr on the cast node itself collapses to one word. Concrete- +// variant casts (`7: size`) keep their node for variant-tag lookup; +// the nullable one-word fold never spills a cursor — excluded. +// Mirrors cstage cg_tagged_castpeel. +fn taggedcastpeel(c: *cgen, e: *node) *node = { + for (e != nil && e.kind == nkind.N_CAST && e.lhs != nil) { + let cu: *tinfo = e.type_: *tinfo; + for (cu != nil && cu.kind == tykind.TY_NAMED) { cu = cu.under; }; + if (cu == nil) { return e; }; + if (cu.kind != tykind.TY_TAGGED) { return e; }; + if (cu.nullable != 0) { return e; }; + let iu: *tinfo = e.lhs.type_: *tinfo; + for (iu != nil && iu.kind == tykind.TY_NAMED) { iu = iu.under; }; + if (iu == nil) { return e; }; + if (iu.kind != tykind.TY_TAGGED) { return e; }; + if (iu.nullable != 0) { return e; }; + e = e.lhs; + }; + return e; +}; + +// taggedidcastpeel — the IDENTITY-only subset of the peel for +// consumers that key variant indices on the scrutinee's own type +// (is/as/match): same-type casts are no-ops there, but a WIDENING +// cast changes the tag numbering and must NOT be peeled — those die +// loud at the consumer's cast catch-all instead. Mirrors cstage +// cg_tagged_idcastpeel. +fn taggedidcastpeel(c: *cgen, e: *node) *node = { + for (e != nil && e.kind == nkind.N_CAST && e.lhs != nil) { + if (!typeeq(e.type_: *tinfo, e.lhs.type_: *tinfo)) { return e; }; + let cu: *tinfo = e.type_: *tinfo; + for (cu != nil && cu.kind == tykind.TY_NAMED) { cu = cu.under; }; + if (cu == nil) { return e; }; + if (cu.kind != tykind.TY_TAGGED) { return e; }; + e = e.lhs; + }; + return e; +}; + // 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 @@ -19211,6 +19317,12 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s emitline("(BP)\n"); return; }; + // Family C (#35): a tagged→tagged cast is transport-transparent + // — peel it so the ident/deref/memread source arms below see the + // carrier and the remap keys on the operand's type. Pre-#35 the + // cast node fell to the scalar arm (`let w: un3 = (v: un3)` + // stored tag 0 + word0). Mirrors cstage cg_widen_tagged_store. + src = taggedcastpeel(c, src); // `expr: TaggedAlias` where the cast's destination IS the union // itself is a widening, not a re-interpret. cgexpr on a CAST // produces the inner's register shape (str: AX=ptr, BX=len), not @@ -19364,6 +19476,15 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s os.write(2, m37a.ptr, m37a.len: u64); os.exit(1); }; + // Family C catch-all (rule 7): a tagged cast + // surviving taggedcastpeel (cast to a THIRD + // union) has no cursor — loud, not word0 + // garbage. Mirrors cstage. + if (src.kind == nkind.N_CAST) { + let m35a: str = "#35: tagged cast source shape unwired at the widen nested arm (rule 7)\n"; + os.write(2, m35a.ptr, m35a.len: u64); + os.exit(1); + }; cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8): i64); @@ -19509,6 +19630,16 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s os.write(2, m37f.ptr, m37f.len: u64); os.exit(1); }; + // Family C catch-all (rule 7): a tagged cast surviving + // taggedcastpeel (cast to a THIRD union) would fall to the + // scalar arm below and silently truncate — loud. Mirrors + // cstage's widen subset-arm cast bound. + if (src.kind == nkind.N_CAST && sf37 != nil + && sf37.kind == tykind.TY_TAGGED && sf37.nullable == 0) { + let m35b: str = "#35: tagged cast source shape unwired at the widen subset arm (rule 7)\n"; + os.write(2, m35b.ptr, m35b.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 @@ -20828,6 +20959,103 @@ fn cgtrytupleshift(c: *cgen, n: *node) bool = { return true; }; +// cgtrytaggedshift — Family C (#35, unwrap source): if the `?`/`!` +// operand's success variant (tag 0) is itself a TAGGED union, the +// unwrapped value is a NESTED box (ww keeps nested unions +// un-flattened) riding the payload words intact — shift past the +// outer tag so consumers see the standard AX=tag cursor. The scalar +// MOVQ DX,AX tail carried only the inner tag and dropped the payload +// (ken unw16). Nullable folds to one word and stays on the scalar +// move. Twin of cgtrytupleshift; mirrors cstage N_TRYPROP/N_TRYUNW. +fn cgtrytaggedshift(c: *cgen, n: *node) bool = { + if (n.lhs == nil) { return false; }; + let ou: *tinfo = n.lhs.type_: *tinfo; + for (ou != nil && ou.kind == tykind.TY_NAMED) { ou = ou.under; }; + if (ou == nil) { return false; }; + if (ou.kind != tykind.TY_TAGGED) { return false; }; + if (ou.params == nil) { return false; }; + let sv: *tinfo = ou.params.type_; + for (sv != nil && sv.kind == tykind.TY_NAMED) { sv = sv.under; }; + if (sv == nil) { return false; }; + if (sv.kind != tykind.TY_TAGGED) { return false; }; + if (sv.nullable != 0) { return false; }; + emitline("\tMOVQ\tDX, AX\n"); + if (sv.size: i32 > 8) { emitline("\tMOVQ\tCX, DX\n"); }; + if (sv.size: i32 > 16) { emitline("\tMOVQ\tR8, CX\n"); }; + return true; +}; + +// cgtryunwcursor — Family C (#35/#46): land the `?`/`!` operand's +// tagged box in the AX/DX/CX/R8 cursor for the unwrap tail. Non-call +// sources don't fill the cursor on their own: an IDENT loads it from +// its frame slot, a mem-based read (deref at any size, >32B +// INDEX/DOT) from the box address cgexpr leaves in AX. Both were +// silent word0 unwraps pre-#35. >32B non-call stays loud (the cursor +// cannot carry it; #40 family). Mirrors cstage N_TRYPROP/N_TRYUNW. +fn cgtryunwcursor(c: *cgen, n: *node, opname: str) void = { + let u: *tinfo = nil; + if (n.lhs != nil) { u = n.lhs.type_: *tinfo; }; + for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; }; + let utag: bool = false; + if (u != nil) { + if (u.kind == tykind.TY_TAGGED && u.nullable == 0) { + utag = true; + }; + }; + if (utag && u.size: i32 > TUPLE_GPCAP * 8 + && n.lhs.kind != nkind.N_CALL) { + let p37: str = "#37: `"; + os.write(2, p37.ptr, p37.len: u64); + os.write(2, opname.ptr, opname.len: u64); + let m37t: str = "` on a >32B mem-based tagged read unwired (#40-family follow-up)\n"; + os.write(2, m37t.ptr, m37t.len: u64); + os.exit(1); + }; + if (utag && n.lhs.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, n.lhs.str); + if (lc == nil) { + let p35: str = "#35: `"; + os.write(2, p35.ptr, p35.len: u64); + os.write(2, opname.ptr, opname.len: u64); + let m35g: str = "` on a global tagged ident unwired (rule 7)\n"; + os.write(2, m35g.ptr, m35g.len: u64); + os.exit(1); + }; + let boff: i32 = lc.off; + let bsz: i32 = u.size: i32; + if (bsz > 24) { + emitline("\tMOVQ\t"); + emitoff((boff + 24): i64); + emitline("(BP), R8\n"); + }; + if (bsz > 16) { + emitline("\tMOVQ\t"); + emitoff((boff + 16): i64); + emitline("(BP), CX\n"); + }; + if (bsz > 8) { + emitline("\tMOVQ\t"); + emitoff((boff + 8): i64); + emitline("(BP), DX\n"); + }; + emitline("\tMOVQ\t"); + emitoff(boff: i64); + emitline("(BP), AX\n"); + return; + }; + if (taggedmemread(c, n.lhs)) { + let bsz2: i32 = 0; + if (u != nil) { bsz2 = u.size: i32; }; + cgexpr(c, n.lhs); + if (bsz2 > 24) { emitline("\tMOVQ\t24(AX), R8\n"); }; + if (bsz2 > 16) { emitline("\tMOVQ\t16(AX), CX\n"); }; + if (bsz2 > 8) { emitline("\tMOVQ\t8(AX), DX\n"); }; + emitline("\tMOVQ\t(AX), AX\n"); + return; + }; + cgexpr(c, n.lhs); +}; + // cgtryprop — `e?` propagates the error variant up the stack. // Success tag = 0 (#216 tracks the legacy/flag-aware success-tag // divergence — out of scope here, success check stays `CMPQ $0`). @@ -20846,19 +21074,15 @@ 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); os.exit(1); }; - cgexpr(c, n.lhs); + // Family C (#35/#46): ident/deref sources land the box in the + // cursor here (was a silent word0 unwrap); call sources keep + // the plain cgexpr emission byte-for-byte. + cgtryunwcursor(c, n, "?"); // AX = tag. If non-zero, this is an error; pop frame and RET. let cl: str = mklabel(c, "tryprop_ok"); emitline("\tCMPQ\t$0, AX\n"); @@ -20921,6 +21145,7 @@ fn cgtryprop(c: *cgen, n: *node) void = { // (shift past the tag) so the destructure / let consumer reads every // element, not just word0. Success variant = tag 0 (first param). if (cgtrytupleshift(c, n)) { return; }; + if (cgtrytaggedshift(c, n)) { return; }; // Success: unwrap value. Tag-only result was AX; the rest of // the codegen expects the success value in AX (and BX for str). // AX=tag, DX=val0, CX=val1 from the call ABI. For str success, @@ -20986,13 +21211,8 @@ 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); + // Family C (#35/#46): see the cgtryprop twin. + cgtryunwcursor(c, n, "!"); let cl: str = mklabel(c, "tryunw_ok"); emitline("\tCMPQ\t$0, AX\n"); emitline("\tJE\t"); @@ -21003,6 +21223,7 @@ fn cgtryunw(c: *cgen, n: *node) void = { // #241: tuple success payload fills the cursor (shift past the tag) — // same rvalue-tuple-into-cursor story as cgtryprop. if (cgtrytupleshift(c, n)) { return; }; + if (cgtrytaggedshift(c, n)) { return; }; // Unwrap success value. (Same shuffle pattern as cgtryprop.) let succisstr: bool = false; if (n.lhs != nil) { @@ -21060,7 +21281,11 @@ fn cgtypetest(c: *cgen, n: *node) void = { // with output parameters): wwstage cgen has a trap with i32 // stored via *i32 in this context — direct assignment of the // local works, indirection through &scrutoff drops sign bits. - let lhs: *node = n.lhs; + // Family C (#35): identity casts are transport no-ops — peel so + // the ident emission carries; a WIDENING tagged cast renumbers + // the tag the compare keys on and has no wired source arm — + // loud below, not a mis-keyed test. Mirrors cstage N_TYPETEST. + let lhs: *node = taggedidcastpeel(c, n.lhs); // #38b residual (rule 7): an sret-class call result leaves AX = // dest pointer, not the tag — mem-based test is a #40-family // follow-up. Mirrors cstage cgen.c N_TYPETEST gate. @@ -21095,6 +21320,21 @@ fn cgtypetest(c: *cgen, n: *node) void = { // Pre-#45 this fell through to scrutoff=0 and the // tag read landed on (BP) — the saved-BP word. nonident = true; + // Family C catch-all (rule 7): a widening tagged + // cast source — loud. Mirrors cstage. + { + let icu: *tinfo = lhs.type_: *tinfo; + for (icu != nil && icu.kind == tykind.TY_NAMED) { + icu = icu.under; + }; + if (lhs.kind == nkind.N_CAST && icu != nil + && icu.kind == tykind.TY_TAGGED + && icu.nullable == 0) { + let m35i: str = "#35: tagged cast source shape unwired at `is` (rule 7)\n"; + os.write(2, m35i.ptr, m35i.len: u64); + os.exit(1); + }; + }; cgexpr(c, lhs); // #37: a >32B box read leaves its ADDRESS in AX — // load the tag word from memory before the compare. @@ -21214,7 +21454,8 @@ fn cgtypeassert(c: *cgen, n: *node) void = { // index, otherwise unwrap to T's ABI: scalar/ptr → AX, 16B // str → (AX, BX). Mirrors cgmatch's slot-based value load. // Slot resolution inlined; see cgtypetest comment. - let lhs: *node = n.lhs; + // Family C (#35): identity-cast peel — see the cgtypetest twin. + let lhs: *node = taggedidcastpeel(c, n.lhs); // #38b residual (rule 7): the spill below reads the cursor, which // an sret-class call result never fills. Mirrors cstage cgen.c // N_TYPEASSERT gate. @@ -21272,6 +21513,21 @@ fn cgtypeassert(c: *cgen, n: *node) void = { os.write(2, m37s.ptr, m37s.len: u64); os.exit(1); }; + // Family C catch-all (rule 7): a widening tagged + // cast source — loud. Mirrors cstage. + { + let acu: *tinfo = lhs.type_: *tinfo; + for (acu != nil && acu.kind == tykind.TY_NAMED) { + acu = acu.under; + }; + if (lhs.kind == nkind.N_CAST && acu != nil + && acu.kind == tykind.TY_TAGGED + && acu.nullable == 0) { + let m35s: str = "#35: tagged cast source shape unwired at `as` (rule 7)\n"; + os.write(2, m35s.ptr, m35s.len: u64); + os.exit(1); + }; + }; cgexpr(c, lhs); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); @@ -22902,7 +23158,8 @@ fn cgmatch(c: *cgen, n: *node) void = { // layout: [+0]=tag, [+8]=value0, [+16]=value1. Bindings // (`case let v: T =>`) get a fresh local slot loaded from // slot+8 (and slot+16 for str-typed payload). - let scrut: *node = n.lhs; + // Family C (#35): identity-cast peel — see the cgtypetest twin. + let scrut: *node = taggedidcastpeel(c, n.lhs); let scrutoff: i32 = 0; let scrutt: *node = nil; if (scrut != nil) { @@ -22980,6 +23237,16 @@ fn cgmatch(c: *cgen, n: *node) void = { os.write(2, m37n.ptr, m37n.len: u64); os.exit(1); }; + // Family C catch-all (rule 7): a widening tagged + // cast scrutinee has no cursor — loud. Mirrors + // cstage cgmatch. + if (scrut.kind == nkind.N_CAST && ms37 != nil + && ms37.kind == tykind.TY_TAGGED + && ms37.nullable == 0) { + let m35m: str = "#35: tagged cast source shape unwired at match (rule 7)\n"; + os.write(2, m35m.ptr, m35m.len: u64); + os.exit(1); + }; cgexpr(c, scrut); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); @@ -25076,6 +25343,16 @@ fn cgun(c: *cgen, n: *node) void = { for (rti != nil && rti.kind == tykind.TY_NAMED) { rti = rti.under; }; if (rti != nil && rti.kind == tykind.TY_FN) { return; }; if (rti != nil && rti.kind == tykind.TY_ARRAY) { return; }; + // Family C (#35/#46): a tagged box behind *p joins the + // mem-based class at ANY size (taggedmemread) — AX = p's + // value IS the box address. The scalar load below pulled + // word0 (the tag) and every cursor consumer transported + // garbage payload words — silent-wrong both stages (ken + // f35/D3a/D3b). The nullable one-word fold stays a scalar + // deref. Mirrors cstage N_UN TK_STAR. + if (rti != nil && rti.kind == tykind.TY_TAGGED) { + if (rti.nullable == 0 && rti.size: i32 > 8) { return; }; + }; // f64/f32 result rides X0 (SSE), not AX — an integer MOVQ // strands the value off the float ABI and the caller's // MOVSD X0 reads stale bits (#96). Mirrors the float @@ -32341,6 +32618,16 @@ fn cgreturn(c: *cgen, n: *node) void = { if (rhs.kind == nkind.N_TUPLE) { needswiden = true; }; + // Family C (#35/#46): a mem-based tagged + // read (`return *p`, any size) routes + // through the widener's memread arm — + // the cgexpr fall-through below wrapped + // the un-deref'd POINTER as a scalar + // payload (silent wrong). Mirrors + // cstage cgreturn's widen-store route. + if (taggedmemread(c, rhs)) { + needswiden = true; + }; }; }; if (needswiden) { diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index ff90ca1f..e2a42eb2 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -183,6 +183,103 @@ fn cgtrytupleshift(c: *cgen, n: *node) bool = { return true; }; +// cgtrytaggedshift — Family C (#35, unwrap source): if the `?`/`!` +// operand's success variant (tag 0) is itself a TAGGED union, the +// unwrapped value is a NESTED box (ww keeps nested unions +// un-flattened) riding the payload words intact — shift past the +// outer tag so consumers see the standard AX=tag cursor. The scalar +// MOVQ DX,AX tail carried only the inner tag and dropped the payload +// (ken unw16). Nullable folds to one word and stays on the scalar +// move. Twin of cgtrytupleshift; mirrors cstage N_TRYPROP/N_TRYUNW. +fn cgtrytaggedshift(c: *cgen, n: *node) bool = { + if (n.lhs == nil) { return false; }; + let ou: *tinfo = n.lhs.type_: *tinfo; + for (ou != nil && ou.kind == tykind.TY_NAMED) { ou = ou.under; }; + if (ou == nil) { return false; }; + if (ou.kind != tykind.TY_TAGGED) { return false; }; + if (ou.params == nil) { return false; }; + let sv: *tinfo = ou.params.type_; + for (sv != nil && sv.kind == tykind.TY_NAMED) { sv = sv.under; }; + if (sv == nil) { return false; }; + if (sv.kind != tykind.TY_TAGGED) { return false; }; + if (sv.nullable != 0) { return false; }; + emitline("\tMOVQ\tDX, AX\n"); + if (sv.size: i32 > 8) { emitline("\tMOVQ\tCX, DX\n"); }; + if (sv.size: i32 > 16) { emitline("\tMOVQ\tR8, CX\n"); }; + return true; +}; + +// cgtryunwcursor — Family C (#35/#46): land the `?`/`!` operand's +// tagged box in the AX/DX/CX/R8 cursor for the unwrap tail. Non-call +// sources don't fill the cursor on their own: an IDENT loads it from +// its frame slot, a mem-based read (deref at any size, >32B +// INDEX/DOT) from the box address cgexpr leaves in AX. Both were +// silent word0 unwraps pre-#35. >32B non-call stays loud (the cursor +// cannot carry it; #40 family). Mirrors cstage N_TRYPROP/N_TRYUNW. +fn cgtryunwcursor(c: *cgen, n: *node, opname: str) void = { + let u: *tinfo = nil; + if (n.lhs != nil) { u = n.lhs.type_: *tinfo; }; + for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; }; + let utag: bool = false; + if (u != nil) { + if (u.kind == tykind.TY_TAGGED && u.nullable == 0) { + utag = true; + }; + }; + if (utag && u.size: i32 > TUPLE_GPCAP * 8 + && n.lhs.kind != nkind.N_CALL) { + let p37: str = "#37: `"; + os.write(2, p37.ptr, p37.len: u64); + os.write(2, opname.ptr, opname.len: u64); + let m37t: str = "` on a >32B mem-based tagged read unwired (#40-family follow-up)\n"; + os.write(2, m37t.ptr, m37t.len: u64); + os.exit(1); + }; + if (utag && n.lhs.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, n.lhs.str); + if (lc == nil) { + let p35: str = "#35: `"; + os.write(2, p35.ptr, p35.len: u64); + os.write(2, opname.ptr, opname.len: u64); + let m35g: str = "` on a global tagged ident unwired (rule 7)\n"; + os.write(2, m35g.ptr, m35g.len: u64); + os.exit(1); + }; + let boff: i32 = lc.off; + let bsz: i32 = u.size: i32; + if (bsz > 24) { + emitline("\tMOVQ\t"); + emitoff((boff + 24): i64); + emitline("(BP), R8\n"); + }; + if (bsz > 16) { + emitline("\tMOVQ\t"); + emitoff((boff + 16): i64); + emitline("(BP), CX\n"); + }; + if (bsz > 8) { + emitline("\tMOVQ\t"); + emitoff((boff + 8): i64); + emitline("(BP), DX\n"); + }; + emitline("\tMOVQ\t"); + emitoff(boff: i64); + emitline("(BP), AX\n"); + return; + }; + if (taggedmemread(c, n.lhs)) { + let bsz2: i32 = 0; + if (u != nil) { bsz2 = u.size: i32; }; + cgexpr(c, n.lhs); + if (bsz2 > 24) { emitline("\tMOVQ\t24(AX), R8\n"); }; + if (bsz2 > 16) { emitline("\tMOVQ\t16(AX), CX\n"); }; + if (bsz2 > 8) { emitline("\tMOVQ\t8(AX), DX\n"); }; + emitline("\tMOVQ\t(AX), AX\n"); + return; + }; + cgexpr(c, n.lhs); +}; + // cgtryprop — `e?` propagates the error variant up the stack. // Success tag = 0 (#216 tracks the legacy/flag-aware success-tag // divergence — out of scope here, success check stays `CMPQ $0`). @@ -201,19 +298,15 @@ 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); os.exit(1); }; - cgexpr(c, n.lhs); + // Family C (#35/#46): ident/deref sources land the box in the + // cursor here (was a silent word0 unwrap); call sources keep + // the plain cgexpr emission byte-for-byte. + cgtryunwcursor(c, n, "?"); // AX = tag. If non-zero, this is an error; pop frame and RET. let cl: str = mklabel(c, "tryprop_ok"); emitline("\tCMPQ\t$0, AX\n"); @@ -276,6 +369,7 @@ fn cgtryprop(c: *cgen, n: *node) void = { // (shift past the tag) so the destructure / let consumer reads every // element, not just word0. Success variant = tag 0 (first param). if (cgtrytupleshift(c, n)) { return; }; + if (cgtrytaggedshift(c, n)) { return; }; // Success: unwrap value. Tag-only result was AX; the rest of // the codegen expects the success value in AX (and BX for str). // AX=tag, DX=val0, CX=val1 from the call ABI. For str success, @@ -341,13 +435,8 @@ 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); + // Family C (#35/#46): see the cgtryprop twin. + cgtryunwcursor(c, n, "!"); let cl: str = mklabel(c, "tryunw_ok"); emitline("\tCMPQ\t$0, AX\n"); emitline("\tJE\t"); @@ -358,6 +447,7 @@ fn cgtryunw(c: *cgen, n: *node) void = { // #241: tuple success payload fills the cursor (shift past the tag) — // same rvalue-tuple-into-cursor story as cgtryprop. if (cgtrytupleshift(c, n)) { return; }; + if (cgtrytaggedshift(c, n)) { return; }; // Unwrap success value. (Same shuffle pattern as cgtryprop.) let succisstr: bool = false; if (n.lhs != nil) { @@ -415,7 +505,11 @@ fn cgtypetest(c: *cgen, n: *node) void = { // with output parameters): wwstage cgen has a trap with i32 // stored via *i32 in this context — direct assignment of the // local works, indirection through &scrutoff drops sign bits. - let lhs: *node = n.lhs; + // Family C (#35): identity casts are transport no-ops — peel so + // the ident emission carries; a WIDENING tagged cast renumbers + // the tag the compare keys on and has no wired source arm — + // loud below, not a mis-keyed test. Mirrors cstage N_TYPETEST. + let lhs: *node = taggedidcastpeel(c, n.lhs); // #38b residual (rule 7): an sret-class call result leaves AX = // dest pointer, not the tag — mem-based test is a #40-family // follow-up. Mirrors cstage cgen.c N_TYPETEST gate. @@ -450,6 +544,21 @@ fn cgtypetest(c: *cgen, n: *node) void = { // Pre-#45 this fell through to scrutoff=0 and the // tag read landed on (BP) — the saved-BP word. nonident = true; + // Family C catch-all (rule 7): a widening tagged + // cast source — loud. Mirrors cstage. + { + let icu: *tinfo = lhs.type_: *tinfo; + for (icu != nil && icu.kind == tykind.TY_NAMED) { + icu = icu.under; + }; + if (lhs.kind == nkind.N_CAST && icu != nil + && icu.kind == tykind.TY_TAGGED + && icu.nullable == 0) { + let m35i: str = "#35: tagged cast source shape unwired at `is` (rule 7)\n"; + os.write(2, m35i.ptr, m35i.len: u64); + os.exit(1); + }; + }; cgexpr(c, lhs); // #37: a >32B box read leaves its ADDRESS in AX — // load the tag word from memory before the compare. @@ -569,7 +678,8 @@ fn cgtypeassert(c: *cgen, n: *node) void = { // index, otherwise unwrap to T's ABI: scalar/ptr → AX, 16B // str → (AX, BX). Mirrors cgmatch's slot-based value load. // Slot resolution inlined; see cgtypetest comment. - let lhs: *node = n.lhs; + // Family C (#35): identity-cast peel — see the cgtypetest twin. + let lhs: *node = taggedidcastpeel(c, n.lhs); // #38b residual (rule 7): the spill below reads the cursor, which // an sret-class call result never fills. Mirrors cstage cgen.c // N_TYPEASSERT gate. @@ -627,6 +737,21 @@ fn cgtypeassert(c: *cgen, n: *node) void = { os.write(2, m37s.ptr, m37s.len: u64); os.exit(1); }; + // Family C catch-all (rule 7): a widening tagged + // cast source — loud. Mirrors cstage. + { + let acu: *tinfo = lhs.type_: *tinfo; + for (acu != nil && acu.kind == tykind.TY_NAMED) { + acu = acu.under; + }; + if (lhs.kind == nkind.N_CAST && acu != nil + && acu.kind == tykind.TY_TAGGED + && acu.nullable == 0) { + let m35s: str = "#35: tagged cast source shape unwired at `as` (rule 7)\n"; + os.write(2, m35s.ptr, m35s.len: u64); + os.exit(1); + }; + }; cgexpr(c, lhs); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); @@ -2257,7 +2382,8 @@ fn cgmatch(c: *cgen, n: *node) void = { // layout: [+0]=tag, [+8]=value0, [+16]=value1. Bindings // (`case let v: T =>`) get a fresh local slot loaded from // slot+8 (and slot+16 for str-typed payload). - let scrut: *node = n.lhs; + // Family C (#35): identity-cast peel — see the cgtypetest twin. + let scrut: *node = taggedidcastpeel(c, n.lhs); let scrutoff: i32 = 0; let scrutt: *node = nil; if (scrut != nil) { @@ -2335,6 +2461,16 @@ fn cgmatch(c: *cgen, n: *node) void = { os.write(2, m37n.ptr, m37n.len: u64); os.exit(1); }; + // Family C catch-all (rule 7): a widening tagged + // cast scrutinee has no cursor — loud. Mirrors + // cstage cgmatch. + if (scrut.kind == nkind.N_CAST && ms37 != nil + && ms37.kind == tykind.TY_TAGGED + && ms37.nullable == 0) { + let m35m: str = "#35: tagged cast source shape unwired at match (rule 7)\n"; + os.write(2, m35m.ptr, m35m.len: u64); + os.exit(1); + }; cgexpr(c, scrut); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); @@ -4431,6 +4567,16 @@ fn cgun(c: *cgen, n: *node) void = { for (rti != nil && rti.kind == tykind.TY_NAMED) { rti = rti.under; }; if (rti != nil && rti.kind == tykind.TY_FN) { return; }; if (rti != nil && rti.kind == tykind.TY_ARRAY) { return; }; + // Family C (#35/#46): a tagged box behind *p joins the + // mem-based class at ANY size (taggedmemread) — AX = p's + // value IS the box address. The scalar load below pulled + // word0 (the tag) and every cursor consumer transported + // garbage payload words — silent-wrong both stages (ken + // f35/D3a/D3b). The nullable one-word fold stays a scalar + // deref. Mirrors cstage N_UN TK_STAR. + if (rti != nil && rti.kind == tykind.TY_TAGGED) { + if (rti.nullable == 0 && rti.size: i32 > 8) { return; }; + }; // f64/f32 result rides X0 (SSE), not AX — an integer MOVQ // strands the value off the float ABI and the caller's // MOVSD X0 reads stale bits (#96). Mirrors the float diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 37d7d311..72b76dbe 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -915,6 +915,16 @@ fn cgreturn(c: *cgen, n: *node) void = { if (rhs.kind == nkind.N_TUPLE) { needswiden = true; }; + // Family C (#35/#46): a mem-based tagged + // read (`return *p`, any size) routes + // through the widener's memread arm — + // the cgexpr fall-through below wrapped + // the un-deref'd POINTER as a scalar + // payload (silent wrong). Mirrors + // cstage cgreturn's widen-store route. + if (taggedmemread(c, rhs)) { + needswiden = true; + }; }; }; if (needswiden) { diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 7a588be6..b73e9926 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -111,6 +111,15 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { let nextparam: *node = nil; if (param != nil) { nextparam = param.next; }; let rest: i32 = pushargsrev(c, arg.next, nextparam, memphase); + // Family C (#35): peel tagged→tagged casts FIRST so every gate + // below keys on the operand — an identity cast reduces to the + // ident fast path, a widening cast trips the widen branch with + // the operand as source. cgexpr on the cast node collapses to + // one word (silent word0 push pre-#35). Mirrors cstage's + // args[i] = cg_tagged_castpeel(args[i]) pre-pass; the cgcall + // pop side counts via pushargsrev's return, so the drain stays + // balanced. + arg = taggedcastpeel(c, arg); // #38b MEMORY-class detection: keyed off the declared param's // type (so widening into a >48B slot is caught), else the arg's // own stamped type (fn-ptr callee carries no param nodes). @@ -304,6 +313,19 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { }; }; }; + // Family C (#35): a DEREF source is a + // tagged box too (mem-based, any size) + // — without this gate the widening + // scalar branch boxed the box. Same + // slotsize key as the N_INDEX/N_DOT + // stamped-carrier arms above. + if (arg.kind == nkind.N_UN && arg.op == tkind.TK_STAR) { + if (istaggedtype(c, arg)) { + if (slotsize(c, arg) == slotsize(c, ptype)) { + aistagged = true; + }; + }; + }; if (!aistagged) { widensz = slotsize(c, ptype); let tagged: *node = resolvetagged(c, ptype); @@ -897,17 +919,26 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { // keeps the stamped-carrier kind gate (#67 pattern) — the // remaining kinds (deref/cast/unwrap) are word0-only reads today, // filed residual. - if (arg.kind == nkind.N_INDEX || arg.kind == nkind.N_DOT) { + if (arg.kind == nkind.N_INDEX || arg.kind == nkind.N_DOT + || (arg.kind == nkind.N_UN && arg.op == tkind.TK_STAR)) { 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. + // #35 (Family C): a mem-based read left the box + // ADDRESS in AX — push the words from memory + // high→low, the mem twin of the cursor push below. + // Covers the any-size deref source and the 33-48B + // INDEX/DOT reads that loud-stopped here pre-#35. + // 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); + let mk35: i32 = isz - 8; + for (mk35 >= 0) { + emitline("\tMOVQ\t"); + emitdispreg(mk35: i64, "AX"); + emitline(", DX\n"); + emitline("\tPUSHQ\tDX\n"); + mk35 -= 8; + }; + return rest + isz / 8; }; if (isz > 24) { emitline("\tPUSHQ\tR8\n"); }; if (isz > 16) { emitline("\tPUSHQ\tCX\n"); }; @@ -2380,6 +2411,14 @@ fn matchscrutt(c: *cgen, scrut: *node) *node = { if (!istaggedtype(c, scrut)) { return nil; }; return scrut; }; + // Family C (#46): a DEREF scrutinee — stamped-carrier like the + // non-ident N_INDEX/N_DOT arms (`match (*p)` spill size + variant + // indices key off scrut.type_; pre-#46 nil here clamped the + // variant to 0 and mis-sized @match_spill). + if (k == nkind.N_UN && scrut.op == tkind.TK_STAR) { + if (!istaggedtype(c, scrut)) { return nil; }; + return scrut; + }; return nil; }; @@ -3043,6 +3082,17 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { if (src.kind == nkind.N_DOT) { if (typeistagged(src.type_: *tinfo)) { return true; }; }; + // Family C (#35/#46): a DEREF source is mem-based (taggedmemread, + // any size) — the widen-store's memread arm copies the box from + // the address cgexpr leaves in AX. An unwrap (`?`/`!`) source + // fills the cursor after the tagged-success payload shift (the + // cgtryprop/cgtryunw twin of cstage's kind-blind su-tagged arm). + if (src.kind == nkind.N_UN && src.op == tkind.TK_STAR) { + if (typeistagged(src.type_: *tinfo)) { return true; }; + }; + if (src.kind == nkind.N_TRYPROP || src.kind == nkind.N_TRYUNW) { + if (typeistagged(src.type_: *tinfo)) { return true; }; + }; return false; }; @@ -3053,8 +3103,21 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { // gates key separately on callsretsize). Every cursor-spill consumer // must branch on this before reading AX as the tag. Mirrors cstage // cg_tagged_memread. +// Family C (#35/#46): a DEREF source is mem-based at ANY size — the +// pointer value IS the box address, so cgun skips the scalar load +// (which carried only the tag) and consumers copy from memory. ≤32B +// INDEX/DOT keep the cursor byte-for-byte (the #37 no-drift bar); +// the nullable one-word fold stays a scalar deref. fn taggedmemread(c: *cgen, e: *node) bool = { if (e == nil) { return false; }; + if (e.kind == nkind.N_UN && e.op == tkind.TK_STAR) { + let du: *tinfo = e.type_: *tinfo; + for (du != nil && du.kind == tykind.TY_NAMED) { du = du.under; }; + if (du == nil) { return false; }; + if (du.kind != tykind.TY_TAGGED) { return false; }; + if (du.nullable != 0) { return false; }; + return du.size: i32 > 8; + }; 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; }; @@ -3063,6 +3126,49 @@ fn taggedmemread(c: *cgen, e: *node) bool = { return u.size: i32 > TUPLE_GPCAP * 8; }; +// taggedcastpeel — Family C (#35): a tagged→tagged cast is transport- +// transparent — the operand's box IS the value; transport consumers +// (widen-store, arg push) derive the remap from the operand's type. +// Peeling exposes the ident/deref carrier their source arms key on; +// cgexpr on the cast node itself collapses to one word. Concrete- +// variant casts (`7: size`) keep their node for variant-tag lookup; +// the nullable one-word fold never spills a cursor — excluded. +// Mirrors cstage cg_tagged_castpeel. +fn taggedcastpeel(c: *cgen, e: *node) *node = { + for (e != nil && e.kind == nkind.N_CAST && e.lhs != nil) { + let cu: *tinfo = e.type_: *tinfo; + for (cu != nil && cu.kind == tykind.TY_NAMED) { cu = cu.under; }; + if (cu == nil) { return e; }; + if (cu.kind != tykind.TY_TAGGED) { return e; }; + if (cu.nullable != 0) { return e; }; + let iu: *tinfo = e.lhs.type_: *tinfo; + for (iu != nil && iu.kind == tykind.TY_NAMED) { iu = iu.under; }; + if (iu == nil) { return e; }; + if (iu.kind != tykind.TY_TAGGED) { return e; }; + if (iu.nullable != 0) { return e; }; + e = e.lhs; + }; + return e; +}; + +// taggedidcastpeel — the IDENTITY-only subset of the peel for +// consumers that key variant indices on the scrutinee's own type +// (is/as/match): same-type casts are no-ops there, but a WIDENING +// cast changes the tag numbering and must NOT be peeled — those die +// loud at the consumer's cast catch-all instead. Mirrors cstage +// cg_tagged_idcastpeel. +fn taggedidcastpeel(c: *cgen, e: *node) *node = { + for (e != nil && e.kind == nkind.N_CAST && e.lhs != nil) { + if (!typeeq(e.type_: *tinfo, e.lhs.type_: *tinfo)) { return e; }; + let cu: *tinfo = e.type_: *tinfo; + for (cu != nil && cu.kind == tykind.TY_NAMED) { cu = cu.under; }; + if (cu == nil) { return e; }; + if (cu.kind != tykind.TY_TAGGED) { return e; }; + e = e.lhs; + }; + return e; +}; + // 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 @@ -3201,6 +3307,12 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s emitline("(BP)\n"); return; }; + // Family C (#35): a tagged→tagged cast is transport-transparent + // — peel it so the ident/deref/memread source arms below see the + // carrier and the remap keys on the operand's type. Pre-#35 the + // cast node fell to the scalar arm (`let w: un3 = (v: un3)` + // stored tag 0 + word0). Mirrors cstage cg_widen_tagged_store. + src = taggedcastpeel(c, src); // `expr: TaggedAlias` where the cast's destination IS the union // itself is a widening, not a re-interpret. cgexpr on a CAST // produces the inner's register shape (str: AX=ptr, BX=len), not @@ -3354,6 +3466,15 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s os.write(2, m37a.ptr, m37a.len: u64); os.exit(1); }; + // Family C catch-all (rule 7): a tagged cast + // surviving taggedcastpeel (cast to a THIRD + // union) has no cursor — loud, not word0 + // garbage. Mirrors cstage. + if (src.kind == nkind.N_CAST) { + let m35a: str = "#35: tagged cast source shape unwired at the widen nested arm (rule 7)\n"; + os.write(2, m35a.ptr, m35a.len: u64); + os.exit(1); + }; cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8): i64); @@ -3499,6 +3620,16 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s os.write(2, m37f.ptr, m37f.len: u64); os.exit(1); }; + // Family C catch-all (rule 7): a tagged cast surviving + // taggedcastpeel (cast to a THIRD union) would fall to the + // scalar arm below and silently truncate — loud. Mirrors + // cstage's widen subset-arm cast bound. + if (src.kind == nkind.N_CAST && sf37 != nil + && sf37.kind == tykind.TY_TAGGED && sf37.nullable == 0) { + let m35b: str = "#35: tagged cast source shape unwired at the widen subset arm (rule 7)\n"; + os.write(2, m35b.ptr, m35b.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 c09f7be9..7308b4e4 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -16121,6 +16121,15 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { let nextparam: *node = nil; if (param != nil) { nextparam = param.next; }; let rest: i32 = pushargsrev(c, arg.next, nextparam, memphase); + // Family C (#35): peel tagged→tagged casts FIRST so every gate + // below keys on the operand — an identity cast reduces to the + // ident fast path, a widening cast trips the widen branch with + // the operand as source. cgexpr on the cast node collapses to + // one word (silent word0 push pre-#35). Mirrors cstage's + // args[i] = cg_tagged_castpeel(args[i]) pre-pass; the cgcall + // pop side counts via pushargsrev's return, so the drain stays + // balanced. + arg = taggedcastpeel(c, arg); // #38b MEMORY-class detection: keyed off the declared param's // type (so widening into a >48B slot is caught), else the arg's // own stamped type (fn-ptr callee carries no param nodes). @@ -16314,6 +16323,19 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { }; }; }; + // Family C (#35): a DEREF source is a + // tagged box too (mem-based, any size) + // — without this gate the widening + // scalar branch boxed the box. Same + // slotsize key as the N_INDEX/N_DOT + // stamped-carrier arms above. + if (arg.kind == nkind.N_UN && arg.op == tkind.TK_STAR) { + if (istaggedtype(c, arg)) { + if (slotsize(c, arg) == slotsize(c, ptype)) { + aistagged = true; + }; + }; + }; if (!aistagged) { widensz = slotsize(c, ptype); let tagged: *node = resolvetagged(c, ptype); @@ -16907,17 +16929,26 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { // keeps the stamped-carrier kind gate (#67 pattern) — the // remaining kinds (deref/cast/unwrap) are word0-only reads today, // filed residual. - if (arg.kind == nkind.N_INDEX || arg.kind == nkind.N_DOT) { + if (arg.kind == nkind.N_INDEX || arg.kind == nkind.N_DOT + || (arg.kind == nkind.N_UN && arg.op == tkind.TK_STAR)) { 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. + // #35 (Family C): a mem-based read left the box + // ADDRESS in AX — push the words from memory + // high→low, the mem twin of the cursor push below. + // Covers the any-size deref source and the 33-48B + // INDEX/DOT reads that loud-stopped here pre-#35. + // 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); + let mk35: i32 = isz - 8; + for (mk35 >= 0) { + emitline("\tMOVQ\t"); + emitdispreg(mk35: i64, "AX"); + emitline(", DX\n"); + emitline("\tPUSHQ\tDX\n"); + mk35 -= 8; + }; + return rest + isz / 8; }; if (isz > 24) { emitline("\tPUSHQ\tR8\n"); }; if (isz > 16) { emitline("\tPUSHQ\tCX\n"); }; @@ -18390,6 +18421,14 @@ fn matchscrutt(c: *cgen, scrut: *node) *node = { if (!istaggedtype(c, scrut)) { return nil; }; return scrut; }; + // Family C (#46): a DEREF scrutinee — stamped-carrier like the + // non-ident N_INDEX/N_DOT arms (`match (*p)` spill size + variant + // indices key off scrut.type_; pre-#46 nil here clamped the + // variant to 0 and mis-sized @match_spill). + if (k == nkind.N_UN && scrut.op == tkind.TK_STAR) { + if (!istaggedtype(c, scrut)) { return nil; }; + return scrut; + }; return nil; }; @@ -19053,6 +19092,17 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { if (src.kind == nkind.N_DOT) { if (typeistagged(src.type_: *tinfo)) { return true; }; }; + // Family C (#35/#46): a DEREF source is mem-based (taggedmemread, + // any size) — the widen-store's memread arm copies the box from + // the address cgexpr leaves in AX. An unwrap (`?`/`!`) source + // fills the cursor after the tagged-success payload shift (the + // cgtryprop/cgtryunw twin of cstage's kind-blind su-tagged arm). + if (src.kind == nkind.N_UN && src.op == tkind.TK_STAR) { + if (typeistagged(src.type_: *tinfo)) { return true; }; + }; + if (src.kind == nkind.N_TRYPROP || src.kind == nkind.N_TRYUNW) { + if (typeistagged(src.type_: *tinfo)) { return true; }; + }; return false; }; @@ -19063,8 +19113,21 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { // gates key separately on callsretsize). Every cursor-spill consumer // must branch on this before reading AX as the tag. Mirrors cstage // cg_tagged_memread. +// Family C (#35/#46): a DEREF source is mem-based at ANY size — the +// pointer value IS the box address, so cgun skips the scalar load +// (which carried only the tag) and consumers copy from memory. ≤32B +// INDEX/DOT keep the cursor byte-for-byte (the #37 no-drift bar); +// the nullable one-word fold stays a scalar deref. fn taggedmemread(c: *cgen, e: *node) bool = { if (e == nil) { return false; }; + if (e.kind == nkind.N_UN && e.op == tkind.TK_STAR) { + let du: *tinfo = e.type_: *tinfo; + for (du != nil && du.kind == tykind.TY_NAMED) { du = du.under; }; + if (du == nil) { return false; }; + if (du.kind != tykind.TY_TAGGED) { return false; }; + if (du.nullable != 0) { return false; }; + return du.size: i32 > 8; + }; 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; }; @@ -19073,6 +19136,49 @@ fn taggedmemread(c: *cgen, e: *node) bool = { return u.size: i32 > TUPLE_GPCAP * 8; }; +// taggedcastpeel — Family C (#35): a tagged→tagged cast is transport- +// transparent — the operand's box IS the value; transport consumers +// (widen-store, arg push) derive the remap from the operand's type. +// Peeling exposes the ident/deref carrier their source arms key on; +// cgexpr on the cast node itself collapses to one word. Concrete- +// variant casts (`7: size`) keep their node for variant-tag lookup; +// the nullable one-word fold never spills a cursor — excluded. +// Mirrors cstage cg_tagged_castpeel. +fn taggedcastpeel(c: *cgen, e: *node) *node = { + for (e != nil && e.kind == nkind.N_CAST && e.lhs != nil) { + let cu: *tinfo = e.type_: *tinfo; + for (cu != nil && cu.kind == tykind.TY_NAMED) { cu = cu.under; }; + if (cu == nil) { return e; }; + if (cu.kind != tykind.TY_TAGGED) { return e; }; + if (cu.nullable != 0) { return e; }; + let iu: *tinfo = e.lhs.type_: *tinfo; + for (iu != nil && iu.kind == tykind.TY_NAMED) { iu = iu.under; }; + if (iu == nil) { return e; }; + if (iu.kind != tykind.TY_TAGGED) { return e; }; + if (iu.nullable != 0) { return e; }; + e = e.lhs; + }; + return e; +}; + +// taggedidcastpeel — the IDENTITY-only subset of the peel for +// consumers that key variant indices on the scrutinee's own type +// (is/as/match): same-type casts are no-ops there, but a WIDENING +// cast changes the tag numbering and must NOT be peeled — those die +// loud at the consumer's cast catch-all instead. Mirrors cstage +// cg_tagged_idcastpeel. +fn taggedidcastpeel(c: *cgen, e: *node) *node = { + for (e != nil && e.kind == nkind.N_CAST && e.lhs != nil) { + if (!typeeq(e.type_: *tinfo, e.lhs.type_: *tinfo)) { return e; }; + let cu: *tinfo = e.type_: *tinfo; + for (cu != nil && cu.kind == tykind.TY_NAMED) { cu = cu.under; }; + if (cu == nil) { return e; }; + if (cu.kind != tykind.TY_TAGGED) { return e; }; + e = e.lhs; + }; + return e; +}; + // 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 @@ -19211,6 +19317,12 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s emitline("(BP)\n"); return; }; + // Family C (#35): a tagged→tagged cast is transport-transparent + // — peel it so the ident/deref/memread source arms below see the + // carrier and the remap keys on the operand's type. Pre-#35 the + // cast node fell to the scalar arm (`let w: un3 = (v: un3)` + // stored tag 0 + word0). Mirrors cstage cg_widen_tagged_store. + src = taggedcastpeel(c, src); // `expr: TaggedAlias` where the cast's destination IS the union // itself is a widening, not a re-interpret. cgexpr on a CAST // produces the inner's register shape (str: AX=ptr, BX=len), not @@ -19364,6 +19476,15 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s os.write(2, m37a.ptr, m37a.len: u64); os.exit(1); }; + // Family C catch-all (rule 7): a tagged cast + // surviving taggedcastpeel (cast to a THIRD + // union) has no cursor — loud, not word0 + // garbage. Mirrors cstage. + if (src.kind == nkind.N_CAST) { + let m35a: str = "#35: tagged cast source shape unwired at the widen nested arm (rule 7)\n"; + os.write(2, m35a.ptr, m35a.len: u64); + os.exit(1); + }; cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8): i64); @@ -19509,6 +19630,16 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s os.write(2, m37f.ptr, m37f.len: u64); os.exit(1); }; + // Family C catch-all (rule 7): a tagged cast surviving + // taggedcastpeel (cast to a THIRD union) would fall to the + // scalar arm below and silently truncate — loud. Mirrors + // cstage's widen subset-arm cast bound. + if (src.kind == nkind.N_CAST && sf37 != nil + && sf37.kind == tykind.TY_TAGGED && sf37.nullable == 0) { + let m35b: str = "#35: tagged cast source shape unwired at the widen subset arm (rule 7)\n"; + os.write(2, m35b.ptr, m35b.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 @@ -20828,6 +20959,103 @@ fn cgtrytupleshift(c: *cgen, n: *node) bool = { return true; }; +// cgtrytaggedshift — Family C (#35, unwrap source): if the `?`/`!` +// operand's success variant (tag 0) is itself a TAGGED union, the +// unwrapped value is a NESTED box (ww keeps nested unions +// un-flattened) riding the payload words intact — shift past the +// outer tag so consumers see the standard AX=tag cursor. The scalar +// MOVQ DX,AX tail carried only the inner tag and dropped the payload +// (ken unw16). Nullable folds to one word and stays on the scalar +// move. Twin of cgtrytupleshift; mirrors cstage N_TRYPROP/N_TRYUNW. +fn cgtrytaggedshift(c: *cgen, n: *node) bool = { + if (n.lhs == nil) { return false; }; + let ou: *tinfo = n.lhs.type_: *tinfo; + for (ou != nil && ou.kind == tykind.TY_NAMED) { ou = ou.under; }; + if (ou == nil) { return false; }; + if (ou.kind != tykind.TY_TAGGED) { return false; }; + if (ou.params == nil) { return false; }; + let sv: *tinfo = ou.params.type_; + for (sv != nil && sv.kind == tykind.TY_NAMED) { sv = sv.under; }; + if (sv == nil) { return false; }; + if (sv.kind != tykind.TY_TAGGED) { return false; }; + if (sv.nullable != 0) { return false; }; + emitline("\tMOVQ\tDX, AX\n"); + if (sv.size: i32 > 8) { emitline("\tMOVQ\tCX, DX\n"); }; + if (sv.size: i32 > 16) { emitline("\tMOVQ\tR8, CX\n"); }; + return true; +}; + +// cgtryunwcursor — Family C (#35/#46): land the `?`/`!` operand's +// tagged box in the AX/DX/CX/R8 cursor for the unwrap tail. Non-call +// sources don't fill the cursor on their own: an IDENT loads it from +// its frame slot, a mem-based read (deref at any size, >32B +// INDEX/DOT) from the box address cgexpr leaves in AX. Both were +// silent word0 unwraps pre-#35. >32B non-call stays loud (the cursor +// cannot carry it; #40 family). Mirrors cstage N_TRYPROP/N_TRYUNW. +fn cgtryunwcursor(c: *cgen, n: *node, opname: str) void = { + let u: *tinfo = nil; + if (n.lhs != nil) { u = n.lhs.type_: *tinfo; }; + for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; }; + let utag: bool = false; + if (u != nil) { + if (u.kind == tykind.TY_TAGGED && u.nullable == 0) { + utag = true; + }; + }; + if (utag && u.size: i32 > TUPLE_GPCAP * 8 + && n.lhs.kind != nkind.N_CALL) { + let p37: str = "#37: `"; + os.write(2, p37.ptr, p37.len: u64); + os.write(2, opname.ptr, opname.len: u64); + let m37t: str = "` on a >32B mem-based tagged read unwired (#40-family follow-up)\n"; + os.write(2, m37t.ptr, m37t.len: u64); + os.exit(1); + }; + if (utag && n.lhs.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, n.lhs.str); + if (lc == nil) { + let p35: str = "#35: `"; + os.write(2, p35.ptr, p35.len: u64); + os.write(2, opname.ptr, opname.len: u64); + let m35g: str = "` on a global tagged ident unwired (rule 7)\n"; + os.write(2, m35g.ptr, m35g.len: u64); + os.exit(1); + }; + let boff: i32 = lc.off; + let bsz: i32 = u.size: i32; + if (bsz > 24) { + emitline("\tMOVQ\t"); + emitoff((boff + 24): i64); + emitline("(BP), R8\n"); + }; + if (bsz > 16) { + emitline("\tMOVQ\t"); + emitoff((boff + 16): i64); + emitline("(BP), CX\n"); + }; + if (bsz > 8) { + emitline("\tMOVQ\t"); + emitoff((boff + 8): i64); + emitline("(BP), DX\n"); + }; + emitline("\tMOVQ\t"); + emitoff(boff: i64); + emitline("(BP), AX\n"); + return; + }; + if (taggedmemread(c, n.lhs)) { + let bsz2: i32 = 0; + if (u != nil) { bsz2 = u.size: i32; }; + cgexpr(c, n.lhs); + if (bsz2 > 24) { emitline("\tMOVQ\t24(AX), R8\n"); }; + if (bsz2 > 16) { emitline("\tMOVQ\t16(AX), CX\n"); }; + if (bsz2 > 8) { emitline("\tMOVQ\t8(AX), DX\n"); }; + emitline("\tMOVQ\t(AX), AX\n"); + return; + }; + cgexpr(c, n.lhs); +}; + // cgtryprop — `e?` propagates the error variant up the stack. // Success tag = 0 (#216 tracks the legacy/flag-aware success-tag // divergence — out of scope here, success check stays `CMPQ $0`). @@ -20846,19 +21074,15 @@ 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); os.exit(1); }; - cgexpr(c, n.lhs); + // Family C (#35/#46): ident/deref sources land the box in the + // cursor here (was a silent word0 unwrap); call sources keep + // the plain cgexpr emission byte-for-byte. + cgtryunwcursor(c, n, "?"); // AX = tag. If non-zero, this is an error; pop frame and RET. let cl: str = mklabel(c, "tryprop_ok"); emitline("\tCMPQ\t$0, AX\n"); @@ -20921,6 +21145,7 @@ fn cgtryprop(c: *cgen, n: *node) void = { // (shift past the tag) so the destructure / let consumer reads every // element, not just word0. Success variant = tag 0 (first param). if (cgtrytupleshift(c, n)) { return; }; + if (cgtrytaggedshift(c, n)) { return; }; // Success: unwrap value. Tag-only result was AX; the rest of // the codegen expects the success value in AX (and BX for str). // AX=tag, DX=val0, CX=val1 from the call ABI. For str success, @@ -20986,13 +21211,8 @@ 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); + // Family C (#35/#46): see the cgtryprop twin. + cgtryunwcursor(c, n, "!"); let cl: str = mklabel(c, "tryunw_ok"); emitline("\tCMPQ\t$0, AX\n"); emitline("\tJE\t"); @@ -21003,6 +21223,7 @@ fn cgtryunw(c: *cgen, n: *node) void = { // #241: tuple success payload fills the cursor (shift past the tag) — // same rvalue-tuple-into-cursor story as cgtryprop. if (cgtrytupleshift(c, n)) { return; }; + if (cgtrytaggedshift(c, n)) { return; }; // Unwrap success value. (Same shuffle pattern as cgtryprop.) let succisstr: bool = false; if (n.lhs != nil) { @@ -21060,7 +21281,11 @@ fn cgtypetest(c: *cgen, n: *node) void = { // with output parameters): wwstage cgen has a trap with i32 // stored via *i32 in this context — direct assignment of the // local works, indirection through &scrutoff drops sign bits. - let lhs: *node = n.lhs; + // Family C (#35): identity casts are transport no-ops — peel so + // the ident emission carries; a WIDENING tagged cast renumbers + // the tag the compare keys on and has no wired source arm — + // loud below, not a mis-keyed test. Mirrors cstage N_TYPETEST. + let lhs: *node = taggedidcastpeel(c, n.lhs); // #38b residual (rule 7): an sret-class call result leaves AX = // dest pointer, not the tag — mem-based test is a #40-family // follow-up. Mirrors cstage cgen.c N_TYPETEST gate. @@ -21095,6 +21320,21 @@ fn cgtypetest(c: *cgen, n: *node) void = { // Pre-#45 this fell through to scrutoff=0 and the // tag read landed on (BP) — the saved-BP word. nonident = true; + // Family C catch-all (rule 7): a widening tagged + // cast source — loud. Mirrors cstage. + { + let icu: *tinfo = lhs.type_: *tinfo; + for (icu != nil && icu.kind == tykind.TY_NAMED) { + icu = icu.under; + }; + if (lhs.kind == nkind.N_CAST && icu != nil + && icu.kind == tykind.TY_TAGGED + && icu.nullable == 0) { + let m35i: str = "#35: tagged cast source shape unwired at `is` (rule 7)\n"; + os.write(2, m35i.ptr, m35i.len: u64); + os.exit(1); + }; + }; cgexpr(c, lhs); // #37: a >32B box read leaves its ADDRESS in AX — // load the tag word from memory before the compare. @@ -21214,7 +21454,8 @@ fn cgtypeassert(c: *cgen, n: *node) void = { // index, otherwise unwrap to T's ABI: scalar/ptr → AX, 16B // str → (AX, BX). Mirrors cgmatch's slot-based value load. // Slot resolution inlined; see cgtypetest comment. - let lhs: *node = n.lhs; + // Family C (#35): identity-cast peel — see the cgtypetest twin. + let lhs: *node = taggedidcastpeel(c, n.lhs); // #38b residual (rule 7): the spill below reads the cursor, which // an sret-class call result never fills. Mirrors cstage cgen.c // N_TYPEASSERT gate. @@ -21272,6 +21513,21 @@ fn cgtypeassert(c: *cgen, n: *node) void = { os.write(2, m37s.ptr, m37s.len: u64); os.exit(1); }; + // Family C catch-all (rule 7): a widening tagged + // cast source — loud. Mirrors cstage. + { + let acu: *tinfo = lhs.type_: *tinfo; + for (acu != nil && acu.kind == tykind.TY_NAMED) { + acu = acu.under; + }; + if (lhs.kind == nkind.N_CAST && acu != nil + && acu.kind == tykind.TY_TAGGED + && acu.nullable == 0) { + let m35s: str = "#35: tagged cast source shape unwired at `as` (rule 7)\n"; + os.write(2, m35s.ptr, m35s.len: u64); + os.exit(1); + }; + }; cgexpr(c, lhs); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); @@ -22902,7 +23158,8 @@ fn cgmatch(c: *cgen, n: *node) void = { // layout: [+0]=tag, [+8]=value0, [+16]=value1. Bindings // (`case let v: T =>`) get a fresh local slot loaded from // slot+8 (and slot+16 for str-typed payload). - let scrut: *node = n.lhs; + // Family C (#35): identity-cast peel — see the cgtypetest twin. + let scrut: *node = taggedidcastpeel(c, n.lhs); let scrutoff: i32 = 0; let scrutt: *node = nil; if (scrut != nil) { @@ -22980,6 +23237,16 @@ fn cgmatch(c: *cgen, n: *node) void = { os.write(2, m37n.ptr, m37n.len: u64); os.exit(1); }; + // Family C catch-all (rule 7): a widening tagged + // cast scrutinee has no cursor — loud. Mirrors + // cstage cgmatch. + if (scrut.kind == nkind.N_CAST && ms37 != nil + && ms37.kind == tykind.TY_TAGGED + && ms37.nullable == 0) { + let m35m: str = "#35: tagged cast source shape unwired at match (rule 7)\n"; + os.write(2, m35m.ptr, m35m.len: u64); + os.exit(1); + }; cgexpr(c, scrut); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); @@ -25076,6 +25343,16 @@ fn cgun(c: *cgen, n: *node) void = { for (rti != nil && rti.kind == tykind.TY_NAMED) { rti = rti.under; }; if (rti != nil && rti.kind == tykind.TY_FN) { return; }; if (rti != nil && rti.kind == tykind.TY_ARRAY) { return; }; + // Family C (#35/#46): a tagged box behind *p joins the + // mem-based class at ANY size (taggedmemread) — AX = p's + // value IS the box address. The scalar load below pulled + // word0 (the tag) and every cursor consumer transported + // garbage payload words — silent-wrong both stages (ken + // f35/D3a/D3b). The nullable one-word fold stays a scalar + // deref. Mirrors cstage N_UN TK_STAR. + if (rti != nil && rti.kind == tykind.TY_TAGGED) { + if (rti.nullable == 0 && rti.size: i32 > 8) { return; }; + }; // f64/f32 result rides X0 (SSE), not AX — an integer MOVQ // strands the value off the float ABI and the caller's // MOVSD X0 reads stale bits (#96). Mirrors the float @@ -32341,6 +32618,16 @@ fn cgreturn(c: *cgen, n: *node) void = { if (rhs.kind == nkind.N_TUPLE) { needswiden = true; }; + // Family C (#35/#46): a mem-based tagged + // read (`return *p`, any size) routes + // through the widener's memread arm — + // the cgexpr fall-through below wrapped + // the un-deref'd POINTER as a scalar + // payload (silent wrong). Mirrors + // cstage cgreturn's widen-store route. + if (taggedmemread(c, rhs)) { + needswiden = true; + }; }; }; if (needswiden) { diff --git a/test/wcc/929_tagged_memarg_run.c b/test/wcc/929_tagged_memarg_run.c index d744ff6c..41f3fd23 100644 --- a/test/wcc/929_tagged_memarg_run.c +++ b/test/wcc/929_tagged_memarg_run.c @@ -571,12 +571,13 @@ static const struct row rows[] = { 0, NULL, NULL, 1, "#38b: >48B tagged arg mixed with register-overflow stack " "args unwired" }, - /* LOUD-STOP: exact-type RVALUE source (same-type cast) — no - * place to resolve; stays loud post-#40. Marker is the - * parenthetical tail shared verbatim by both stages (cstage - * interposes the numeric node kind), complementing - * fail_global_src's prefix pin of the same guard. */ - { "fail_rvalue_cast", + /* #35 (Family C) FLIP: the exact-type same-type cast is peeled + * (cg_tagged_castpeel / taggedcastpeel) before the memarg place + * resolution, so `probe((a: t_u))` reduces to the wired IDENT + * place and runs — the old loud pin ("rvalue and + * unresolvable-place sources unwired") covers only the shapes + * with no peeled place left. */ + { "memarg_idcast_peeled", MEM56_TYPES MEM56_PROBE "export fn main() i32 = {\n" @@ -584,8 +585,7 @@ static const struct row rows[] = { " if (probe((a: t_u)) != 1) { return 1; };\n" " return 0;\n" "};\n", - 0, NULL, NULL, 1, - "(rvalue and unresolvable-place sources unwired)" }, + 0, NULL, NULL, 0, NULL }, }; static const char *g_bin; diff --git a/test/wcc/941_tuple_slot_layout_run.c b/test/wcc/941_tuple_slot_layout_run.c index 406458bb..ad21c147 100644 --- a/test/wcc/941_tuple_slot_layout_run.c +++ b/test/wcc/941_tuple_slot_layout_run.c @@ -889,11 +889,11 @@ static const struct row rows[] = { " };\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", + /* #37→#35 FLIP (Family C): the >32B deref let-source loud is now + * WIRED — taggedmemread covers N_UN(STAR) at any size, so the + * widener's mem-read arm copies the box. The loud-symmetry pin + * (reviewer-37) becomes a runtime row. */ + { "fc_deref_56_slice_let", "package main;\n" "type inst_lit = rune;\n" "type inst_repeat = struct {\n" @@ -910,14 +910,20 @@ static const struct row rows[] = { " let p = &insts[0];\n" " let w = *p;\n" " if (!(w is inst_repeat)) { return 1; };\n" + " match (w) {\n" + " case let ir: inst_repeat => {\n" + " if (ir.id != 7 || ir.origin != 3) { return 2; };\n" + " if ((ir.min as size) != 11) { return 3; };\n" + " if ((ir.max as size) != 22) { return 4; };\n" + " };\n" + " case let l: inst_lit => { return 5; };\n" + " };\n" " return 0;\n" - "};\n", 0, - 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", + "};\n", 0, K_RUN, NULL }, + /* #37→#35 FLIP (Family C): match on a >32B deref scrutinee is + * now wired through the mem-read spill (matchscrutt carries the + * N_UN stamped type, sizing @match_spill off the box). */ + { "fc_deref_56_slice_match", "package main;\n" "type inst_lit = rune;\n" "type inst_repeat = struct {\n" @@ -933,12 +939,15 @@ static const struct row rows[] = { " 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" + " case let ir: inst_repeat => {\n" + " if (ir.id != 7) { return 1; };\n" + " if (!(ir.min is size)) { return 2; };\n" + " if ((ir.max as size) != 22) { return 3; };\n" + " };\n" + " case let l: inst_lit => { return 4; };\n" " };\n" " return 0;\n" - "};\n", 0, - K_BUILDERR, "#37: >32B tagged match scrutinee from a non-mem-based source" }, + "};\n", 0, K_RUN, NULL }, /* ...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 @@ -1484,6 +1493,362 @@ static const struct row rows[] = { " return 0;\n" "};\n", 0, K_BUILDERR, "unsupported assign target shape" }, + /* ---- Family C (#35/#46): tagged transport from DEREF / CAST / + * UNWRAP sources. Pre-fix every cell was silent-wrong (the source + * materialized as ONE scalar word — tag only; cs pushed stale + * regs, ww stored 0/garbage; ken f35 + ken37v D3a/D3b). The fix + * extends the #37 mem-based predicate to ANY-size deref (the + * pointer value IS the box address), peels tagged→tagged casts at + * the transport consumers, and shifts the nested-box payload on + * `?`/`!` unwrap. Every K_RUN row also pins cs==ww byte-id. */ + { "fc_arg_deref_16", /* ken f35 exact shape */ + "package main;\n" + "fn take(e: (void | size)) size = {\n" + " if (!(e is size)) { return 99; };\n" + " return e as size;\n" + "};\n" + "export fn main() i32 = {\n" + " let v: (void | size) = 7: size;\n" + " let p: *(void | size) = &v;\n" + " if (take(*p) != 7) { return 1; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_deref_let_match_16", /* ken D3a exact shape */ + "package main;\n" + "type un16 = (void | size);\n" + "export fn main() i32 = {\n" + " let v: un16 = 5: size;\n" + " let p: *un16 = &v;\n" + " let w = *p;\n" + " if (!(w is size)) { return 1; };\n" + " match (*p) {\n" + " case let s: size => { if (s != 5) { return 2; }; };\n" + " case void => { return 3; };\n" + " };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_deref_let_match_32str", /* ken D3b exact shape (at-cap, str payload) */ + "package main;\n" + "type un32 = (void | str);\n" + "export fn main() i32 = {\n" + " let v: un32 = \"atcap\";\n" + " let p: *un32 = &v;\n" + " let w = *p;\n" + " if (!(w is str)) { return 1; };\n" + " match (*p) {\n" + " case let s: str => { if (s.len != 5) { return 2; }; };\n" + " case void => { return 3; };\n" + " };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_deref_asg_16", /* ASSIGN (not let-init) consumer */ + "package main;\n" + "type un16 = (void | size);\n" + "export fn main() i32 = {\n" + " let v: un16 = 5: size;\n" + " let p: *un16 = &v;\n" + " let w: un16 = void;\n" + " w = *p;\n" + " if (!(w is size)) { return 1; };\n" + " if ((w as size) != 5) { return 2; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_deref_ret_16", /* return *p — the ww cgreturn fall-through wrapped the POINTER */ + "package main;\n" + "type un16 = (void | size);\n" + "fn get(p: *un16) un16 = {\n" + " return *p;\n" + "};\n" + "export fn main() i32 = {\n" + " let v: un16 = 5: size;\n" + " let w = get(&v);\n" + " if (!(w is size)) { return 1; };\n" + " if ((w as size) != 5) { return 2; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_deref_is_as_16", /* direct is/as consumers */ + "package main;\n" + "type un16 = (void | size);\n" + "export fn main() i32 = {\n" + " let v: un16 = 5: size;\n" + " let p: *un16 = &v;\n" + " if (!((*p) is size)) { return 1; };\n" + " if (((*p) as size) != 5) { return 2; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_deref_as_32str", /* as on at-cap str payload */ + "package main;\n" + "type un32 = (void | str);\n" + "export fn main() i32 = {\n" + " let v: un32 = \"atcap\";\n" + " let p: *un32 = &v;\n" + " let s = (*p) as str;\n" + " if (s.len != 5) { return 1; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_deref_arg_32str", /* arg push, at-cap str payload */ + "package main;\n" + "type un32 = (void | str);\n" + "fn take(e: un32) i32 = {\n" + " if (!(e is str)) { return 99; };\n" + " let s = e as str;\n" + " if (s.len != 5) { return 98; };\n" + " return 0;\n" + "};\n" + "export fn main() i32 = {\n" + " let v: un32 = \"atcap\";\n" + " let p: *un32 = &v;\n" + " return take(*p);\n" + "};\n", 0, K_RUN, NULL }, + { "fc_deref_widen_16to24", /* deref into a WIDER union (remap rides) */ + "package main;\n" + "type un16 = (void | size);\n" + "type un3 = (void | size | str);\n" + "export fn main() i32 = {\n" + " let v: un16 = 5: size;\n" + " let p: *un16 = &v;\n" + " let w: un3 = *p;\n" + " if (!(w is size)) { return 1; };\n" + " if ((w as size) != 5) { return 2; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_deref_struct24_neighbor", /* struct payload + neighbor-guard locals */ + "package main;\n" + "type s2 = struct { a: size, b: size };\n" + "type un24 = (void | s2);\n" + "export fn main() i32 = {\n" + " let lo: size = 111;\n" + " let v: un24 = s2 { a = 5, b = 6 };\n" + " let hi: size = 222;\n" + " let p: *un24 = &v;\n" + " let w = *p;\n" + " if (!(w is s2)) { return 1; };\n" + " match (w) {\n" + " case let s: s2 => { if (s.a != 5 || s.b != 6) { return 2; }; };\n" + " case void => { return 3; };\n" + " };\n" + " if (lo != 111) { return 4; };\n" + " if (hi != 222) { return 5; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_deref_56_memread", /* >32B deref — the #37 loud flips to working */ + "package main;\n" + "type big = struct { a: u64, b: u64, c: u64, d: u64, e: u64, f: u64 };\n" + "type un56 = (void | big);\n" + "export fn main() i32 = {\n" + " let v: un56 = big { a = 1u64, b = 2u64, c = 3u64, d = 4u64, e = 5u64, f = 6u64 };\n" + " let p: *un56 = &v;\n" + " let w = *p;\n" + " if (!(w is big)) { return 1; };\n" + " match (*p) {\n" + " case let s: big => { if (s.a != 1u64 || s.f != 6u64) { return 2; }; };\n" + " case void => { return 3; };\n" + " };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_cast_ident_arg", /* reviewer-22's cast shape: identity cast at ARG */ + "package main;\n" + "type un16 = (void | size);\n" + "fn take(e: un16) size = {\n" + " if (!(e is size)) { return 99; };\n" + " return e as size;\n" + "};\n" + "export fn main() i32 = {\n" + " let v: un16 = 7: size;\n" + " if (take((v: un16)) != 7) { return 1; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_cast_widen_let", /* widening cast at let — payload checked (was garbage-payload) */ + "package main;\n" + "type un16 = (void | size);\n" + "type un3 = (void | size | str);\n" + "export fn main() i32 = {\n" + " let v: un16 = 5: size;\n" + " let w: un3 = (v: un3);\n" + " if (!(w is size)) { return 1; };\n" + " if ((w as size) != 5) { return 2; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_unwrap_tagged_let", /* ken unw16 (success-first): `?` success variant is itself tagged */ + "package main;\n" + "type un16 = (void | size);\n" + "type err = !void;\n" + "fn get() (un16 | err) = {\n" + " let v: un16 = 7: size;\n" + " return v;\n" + "};\n" + "fn use() (size | err) = {\n" + " let w = get()?;\n" + " if (!(w is size)) { return 99; };\n" + " return w as size;\n" + "};\n" + "export fn main() i32 = {\n" + " match (use()) {\n" + " case let s: size => { if (s != 7) { return 1; }; };\n" + " case err => { return 2; };\n" + " };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_unwrap_ident", /* `v?` on an IDENT — was a silent word0 unwrap */ + "package main;\n" + "type err = !void;\n" + "fn use(v: (size | err)) (size | err) = {\n" + " let w = v?;\n" + " return w: size;\n" + "};\n" + "export fn main() i32 = {\n" + " let v: (size | err) = 7: size;\n" + " match (use(v)) {\n" + " case let s: size => { if (s != 7) { return 1; }; };\n" + " case err => { return 2; };\n" + " };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_unwrap_deref", /* `(*p)?` — mem-based unwrap, was silent garbage */ + "package main;\n" + "type err = !void;\n" + "fn use(p: *(size | err)) (size | err) = {\n" + " let x = (*p)?;\n" + " return x: size;\n" + "};\n" + "export fn main() i32 = {\n" + " let v: (size | err) = 7: size;\n" + " match (use(&v)) {\n" + " case let s: size => { if (s != 7) { return 1; }; };\n" + " case err => { return 2; };\n" + " };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_cast_deref_arg", /* ken gC1 second half: identity cast WRAPPING a deref at ARG */ + "package main;\n" + "type un16 = (void | size);\n" + "fn take(e: un16) size = {\n" + " if (!(e is size)) { return 99; };\n" + " return e as size;\n" + "};\n" + "export fn main() i32 = {\n" + " let v: un16 = 7: size;\n" + " let p: *un16 = &v;\n" + " if (take(((*p): un16)) != 7) { return 1; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_deref_void_16", /* ken gC2: VOID variant through deref — tag-0 path at match/is/let */ + "package main;\n" + "type un16 = (void | size);\n" + "export fn main() i32 = {\n" + " let v: un16 = void;\n" + " let p: *un16 = &v;\n" + " match (*p) {\n" + " case let s: size => { return 1; };\n" + " case void => { };\n" + " };\n" + " if ((*p) is size) { return 2; };\n" + " let w = *p;\n" + " if (!(w is void)) { return 3; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_deref_slice_elem_16", /* ken gC3: pointer INTO a slice element, size + void variants */ + "package main;\n" + "type un16 = (void | size);\n" + "export fn main() i32 = {\n" + " let xs: []un16 = [];\n" + " append(xs, 11: size);\n" + " append(xs, void);\n" + " let p: *un16 = &xs[0];\n" + " match (*p) {\n" + " case let s: size => { if (s != 11) { return 1; }; };\n" + " case void => { return 2; };\n" + " };\n" + " let q: *un16 = &xs[1];\n" + " if (!((*q) is void)) { return 3; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_deref_arg_40", /* 33-48B deref ARG — the mem-push leg the #37-era loud guarded + * (silent word0 at base for deref; payload words checked late-first) */ + "package main;\n" + "type big4 = struct { a: u64, b: u64, c: u64, d: u64 };\n" + "type un40 = (void | big4);\n" + "fn take(e: un40) u64 = {\n" + " match (e) {\n" + " case let g: big4 => {\n" + " if (g.b != 2u64 || g.c != 3u64) { return 90u64; };\n" + " return g.a + g.d;\n" + " };\n" + " case void => { return 0u64; };\n" + " };\n" + "};\n" + "export fn main() i32 = {\n" + " let v: un40 = big4 { a = 30u64, b = 2u64, c = 3u64, d = 12u64 };\n" + " let p: *un40 = &v;\n" + " if (take(*p) != 42u64) { return 1; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_deref_arg_56", /* ken gC6: >48B deref ARG — the MEMORY-class memarg place leg + * (already place-resolved at base; regression pin, not a flip) */ + "package main;\n" + "type big = struct { a: u64, b: u64, c: u64, d: u64, e: u64, f: u64 };\n" + "type un56 = (void | big);\n" + "fn take(e: un56) u64 = {\n" + " match (e) {\n" + " case let g: big => {\n" + " if (g.b != 2u64 || g.e != 5u64) { return 90u64; };\n" + " return g.a + g.f;\n" + " };\n" + " case void => { return 0u64; };\n" + " };\n" + "};\n" + "export fn main() i32 = {\n" + " let v: un56 = big { a = 30u64, b = 2u64, c = 3u64, d = 4u64, e = 5u64, f = 12u64 };\n" + " let p: *un56 = &v;\n" + " if (take(*p) != 42u64) { return 1; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "fc_arg_balance_multi", /* POP-side balance: tagged-deref arg is NEITHER first nor last + * (the original #35 symptom was 1-push-N-pops); called TWICE so a + * persistent SP imbalance breaks the second call or the epilogue */ + "package main;\n" + "type un16 = (void | size);\n" + "fn take(a: size, e: un16, b: size) size = {\n" + " if (a != 100) { return 90; };\n" + " if (!(e is size)) { return 91; };\n" + " if ((e as size) != 7) { return 92; };\n" + " if (b != 200) { return 93; };\n" + " return a + (e as size) + b;\n" + "};\n" + "export fn main() i32 = {\n" + " let v: un16 = 7: size;\n" + " let p: *un16 = &v;\n" + " if (take(100, *p, 200) != 307) { return 1; };\n" + " if (take(100, *p, 200) != 307) { return 2; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* Family C rule-7 louds: unwired source shapes die loud, not word0. */ + { "fc_loud_global_unwrap", + "package main;\n" + "type err = !void;\n" + "let g: (size | err) = 7: size;\n" + "fn use() (size | err) = {\n" + " let x = g?;\n" + " return x: size;\n" + "};\n" + "export fn main() i32 = {\n" + " match (use()) {\n" + " case let s: size => { if (s != 7) { return 1; }; };\n" + " case err => { return 2; };\n" + " };\n" + " return 0;\n" + "};\n", 0, + K_BUILDERR, "on a global tagged ident unwired" }, + { "fc_loud_cast_third_union", + "package main;\n" + "type un3 = (void | size | str);\n" + "type un4 = (void | size | str | *u8);\n" + "export fn main() i32 = {\n" + " let w: un4 = (5: size): un3;\n" + " if (!(w is size)) { return 1; };\n" + " return 0;\n" + "};\n", 0, + K_BUILDERR, "tagged cast source shape unwired" }, }; /* build+run via a driver (ww / ww_ww); returns 0 pass, nonzero fail. */