selfhost+test: size match-spill slot by scrutinee, not 24B (#9)

wwstage cgmatch hardcoded `spillsz = 24` + unconditional CX write
where cstage emits `slot_size = (su->kind == TY_TAGGED) ? su->size
: 16` with `if (slot_size > 16)` gating. For 1-word-payload variants
like `(*u8 | oserror)` the slot is 16B; wwstage over-allocated and
over-wrote past the receiver's read window.

Factor cgmatch's non-ident scrutinee-type resolution + spill sizing
into matchscrutt + matchspillsz in cgenutil.ww. cgmatch gates CX
write on `spillsz > 16`; R8 gate `> 24` already correct. scanlocals
N_MATCH branch uses the same helpers — scan+emit lockstep.

Test 716: 4 rows × {cstage runtime, wwstage runtime, asm-byte-id}.
Aliased (*u8 | oserror) ok/err arms, raw (*u8 | i64) for hypothesis
breadth, (str | i64) 24B regression guard.
This commit is contained in:
2026-05-17 00:00:55 +09:00
parent deaa777eb8
commit f4176b8749
7 changed files with 561 additions and 210 deletions

View File

@@ -1899,6 +1899,68 @@ export fn resolvetagged(c: *cgen, t: *node) *node = {
return nil;
};
// matchscrutt — resolve a non-ident match scrutinee node to its tagged
// type (or nil if unresolvable). Mirrors cgmatch's inline scrutinee
// type resolution; factored so cgmatch (emit) and scanlocals (count)
// agree on the spill slot's size per the scan+emit lockstep invariant.
// IDENT scrutinees use a different lookup path (read off the local
// directly, no spill) so this returns nil for them too.
fn matchscrutt(c: *cgen, scrut: *node) *node = {
if (scrut == nil) { return nil; };
let k: nkind = scrut.kind;
if (k == nkind.N_IDENT) { return nil; };
if (k == nkind.N_CALL) {
let callee: *node = scrut.lhs;
if (callee != nil) {
let cnm: str;
cnm.ptr = nil; cnm.len = 0;
if (callee.kind == nkind.N_IDENT) { cnm = callee.str; };
if (callee.kind == nkind.N_DOT) { cnm = callee.str; };
if (cnm.len > 0) {
let rt: *node = fnretlookup(c, cnm);
if (rt != nil) { return resolvetagged(c, rt); };
};
};
return nil;
};
if (k == nkind.N_INDEX) {
let ibase: *node = scrut.lhs;
if (ibase == nil) { return nil; };
if (ibase.kind != nkind.N_IDENT) { return nil; };
let bl: *local = localfindnode(c, ibase.str);
let btn: *node = nil;
if (bl != nil) { btn = bl.tnode; }
else { btn = letvartnode(c, ibase.str); };
if (btn == nil) { return nil; };
let bk: nkind = btn.kind;
let etn: *node = nil;
if (bk == nkind.N_TARRAY) { etn = btn.lhs; };
if (bk == nkind.N_TSLICE) { etn = btn.lhs; };
if (bk == nkind.N_TPTR) { etn = btn.lhs; };
if (etn == nil) { return nil; };
return resolvetagged(c, etn);
};
if (k == nkind.N_DOT) {
let ft: *node = dotfieldtnode(c, scrut);
if (ft == nil) { return nil; };
return resolvetagged(c, ft);
};
return nil;
};
// matchspillsz — slot size for the @match_spill scratch a non-ident
// scrutinee lands in. Mirrors cstage's `slot_size = (su->kind ==
// TY_TAGGED) ? su->size : 16` (cmd/w6c/cgen.c cgmatch). 16 default
// when the scrutinee type can't be resolved keeps the historical
// alloc for non-tagged / unresolved cases. Used by both scanlocals
// (counting) and cgmatch (emitting) per rule-10 align-to-cstage.
fn matchspillsz(c: *cgen, scrutt: *node) i32 = {
if (scrutt == nil) { return 16; };
let sz: i32 = slotsize(c, scrutt);
if (sz <= 0) { return 16; };
return sz;
};
// istaggedtype — alias-aware. Mirrors isstrtype: follow N_TNAME to its
// underlying decl, then unwrap a leading N_TBANG so `type error =
// !(invalid | overflow);` is still recognised as tagged. Without the