cgen: #35/#46 Family C — tagged transport from deref/cast/unwrap sources goes mem-based, both stages

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).
This commit is contained in:
2026-06-05 07:13:00 +09:00
parent 4c46d3afde
commit 322667b820
8 changed files with 1592 additions and 139 deletions

View File

@@ -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)) {