From f4176b874936bf2f2cc880e6c680a7fe59bd5494 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sun, 17 May 2026 00:00:55 +0900 Subject: [PATCH] selfhost+test: size match-spill slot by scrutinee, not 24B (#9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 7 + selfhost/cmd/w6c/main.combined.ww | 159 +++++++----- selfhost/cmd/wcc/cgendecl.ww | 13 +- selfhost/cmd/wcc/cgenexpr.ww | 84 ++---- selfhost/cmd/wcc/cgenutil.ww | 62 +++++ selfhost/cmd/wwdump/main.combined.ww | 159 +++++++----- test/wcc/716_match_spill_pointer_payload.c | 287 +++++++++++++++++++++ 7 files changed, 561 insertions(+), 210 deletions(-) create mode 100644 test/wcc/716_match_spill_pointer_payload.c diff --git a/Makefile b/Makefile index 87e6d1ad..a5512cd8 100644 --- a/Makefile +++ b/Makefile @@ -239,6 +239,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_struct_field_index \ $(BIN)/test_tagged_return_scratch \ $(BIN)/test_tagged_widen_f64 \ + $(BIN)/test_match_spill_pointer_payload \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -484,6 +485,12 @@ $(BIN)/test_tagged_widen_f64: test/wcc/715_tagged_widen_f64.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_match_spill_pointer_payload: test/wcc/716_match_spill_pointer_payload.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_use_promote_alias: test/wcc/699_use_promote_alias.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 3dacad07..da30c990 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -8197,6 +8197,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 @@ -10676,65 +10738,15 @@ fn cgmatch(c: *cgen, n: *node) void = { // Non-ident scrutinee (call result, arr[i], p.field, // ?, etc.). Spill into an `@match_spill` scratch slot // and dispatch off it. Tagged returns (N_CALL) follow - // the AX:DX:CX convention; tagged-element loads + // the AX:DX:CX[:R8] convention; tagged-element loads // (N_INDEX) and tagged-field loads (N_DOT, fixed by // #28) produce the same triple. Nullable returns are - // single-word (AX = ptr); only +0 is read, so the - // extra stores are harmless. We recover the scrutinee - // type from fnretlookup (N_CALL), the base local's - // array element type (N_INDEX), or the struct field - // type (N_DOT) so dispatch can compute variant - // indices. Slot size is then derived from the - // scrutinee type so slice-variant tagged-unions - // (32B slot) don't overflow a hardcoded 24B scratch. - if (scrut.kind == 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) { scrutt = resolvetagged(c, rt); }; - }; - }; - }; - if (scrut.kind == nkind.N_INDEX) { - let ibase: *node = scrut.lhs; - if (ibase != nil) { - if (ibase.kind == nkind.N_IDENT) { - 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) { - 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) { - scrutt = resolvetagged(c, etn); - }; - }; - }; - }; - }; - if (scrut.kind == nkind.N_DOT) { - let ft: *node = dotfieldtnode(c, scrut); - if (ft != nil) { - scrutt = resolvetagged(c, ft); - }; - }; - // Size the spill to the scrutinee slot. Default 24B - // preserves the historical alloc for non-tagged or - // unresolved cases (nullable, str-returning, etc.). - let spillsz: i32 = 24; - if (scrutt != nil) { - let resolved: i32 = slotsize(c, scrutt); - if (resolved > spillsz) { spillsz = resolved; }; - }; + // single-word (AX = ptr); only +0 is read. + // Scrutinee type + spill size factored into matchscrutt + // / matchspillsz so scanlocals stays lockstep — see + // cgenutil.ww (task #9 align-down to cstage). + scrutt = matchscrutt(c, scrut); + let spillsz: i32 = matchspillsz(c, scrutt); scrutoff = localalloc(c, "@match_spill", spillsz, nil); cgexpr(c, scrut); emitline("\tMOVQ\tAX, "); @@ -10744,15 +10756,17 @@ fn cgmatch(c: *cgen, n: *node) void = { emitline("\tMOVQ\tDX, "); emitoff((scrutoff + 8): i64); emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((scrutoff + 16): i64); - emitline("(BP)\n"); - // R8 carries the 4th return word when the - // scrutinee's tagged union has a slice-payload - // variant (slot 32B). Harmless for narrower - // returns — R8 is callee-clobbered either way. - let ssz: i32 = slotsize(c, scrutt); - if (ssz > 24) { + // CX/R8 writes gated on spill size so 1-word- + // payload variants (slot 16B) don't bump the + // frame past the tag+word0 the receiver reads. + // Mirrors cmd/w6c/cgen.c cgmatch's + // `if (slot_size > 16)` / `> 24` guards. + if (spillsz > 16) { + emitline("\tMOVQ\tCX, "); + emitoff((scrutoff + 16): i64); + emitline("(BP)\n"); + }; + if (spillsz > 24) { emitline("\tMOVQ\tR8, "); emitoff((scrutoff + 24): i64); emitline("(BP)\n"); @@ -16634,14 +16648,19 @@ fn scanlocals(c: *cgen, n: *node) i32 = { }; }; }; - // `match (non-ident)` needs a 24B `@match_spill` scratch slot for - // cgmatch to land the AX:DX:CX return triple. Mirrors C cgen's - // localoff("@match_spill", ...). N_IDENT scrutinees read the slot + // `match (non-ident)` needs an `@match_spill` scratch slot sized + // to the scrutinee's tagged-union slot (16/24/32 for 1/2/3-word + // payload). Mirrors cstage's `slot_size = su->size` default 16 + // in cmd/w6c/cgen.c cgmatch (task #9 align-down to cstage). + // matchspillsz must agree with cgmatch's emit-time computation + // for scan+emit lockstep. N_IDENT scrutinees read the slot // directly off the local — no spill needed. if (n.kind == nkind.N_MATCH) { let sc: *node = n.lhs; if (sc != nil) { - if (sc.kind != nkind.N_IDENT) { total += 24; }; + if (sc.kind != nkind.N_IDENT) { + total += matchspillsz(c, matchscrutt(c, sc)); + }; }; }; // Match-arm binding (`case let v: T => ...`) gets a slot too. diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 6d98b38a..bff24e72 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -168,14 +168,19 @@ fn scanlocals(c: *cgen, n: *node) i32 = { }; }; }; - // `match (non-ident)` needs a 24B `@match_spill` scratch slot for - // cgmatch to land the AX:DX:CX return triple. Mirrors C cgen's - // localoff("@match_spill", ...). N_IDENT scrutinees read the slot + // `match (non-ident)` needs an `@match_spill` scratch slot sized + // to the scrutinee's tagged-union slot (16/24/32 for 1/2/3-word + // payload). Mirrors cstage's `slot_size = su->size` default 16 + // in cmd/w6c/cgen.c cgmatch (task #9 align-down to cstage). + // matchspillsz must agree with cgmatch's emit-time computation + // for scan+emit lockstep. N_IDENT scrutinees read the slot // directly off the local — no spill needed. if (n.kind == nkind.N_MATCH) { let sc: *node = n.lhs; if (sc != nil) { - if (sc.kind != nkind.N_IDENT) { total += 24; }; + if (sc.kind != nkind.N_IDENT) { + total += matchspillsz(c, matchscrutt(c, sc)); + }; }; }; // Match-arm binding (`case let v: T => ...`) gets a slot too. diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index ca17c133..d9012c2e 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -971,65 +971,15 @@ fn cgmatch(c: *cgen, n: *node) void = { // Non-ident scrutinee (call result, arr[i], p.field, // ?, etc.). Spill into an `@match_spill` scratch slot // and dispatch off it. Tagged returns (N_CALL) follow - // the AX:DX:CX convention; tagged-element loads + // the AX:DX:CX[:R8] convention; tagged-element loads // (N_INDEX) and tagged-field loads (N_DOT, fixed by // #28) produce the same triple. Nullable returns are - // single-word (AX = ptr); only +0 is read, so the - // extra stores are harmless. We recover the scrutinee - // type from fnretlookup (N_CALL), the base local's - // array element type (N_INDEX), or the struct field - // type (N_DOT) so dispatch can compute variant - // indices. Slot size is then derived from the - // scrutinee type so slice-variant tagged-unions - // (32B slot) don't overflow a hardcoded 24B scratch. - if (scrut.kind == 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) { scrutt = resolvetagged(c, rt); }; - }; - }; - }; - if (scrut.kind == nkind.N_INDEX) { - let ibase: *node = scrut.lhs; - if (ibase != nil) { - if (ibase.kind == nkind.N_IDENT) { - 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) { - 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) { - scrutt = resolvetagged(c, etn); - }; - }; - }; - }; - }; - if (scrut.kind == nkind.N_DOT) { - let ft: *node = dotfieldtnode(c, scrut); - if (ft != nil) { - scrutt = resolvetagged(c, ft); - }; - }; - // Size the spill to the scrutinee slot. Default 24B - // preserves the historical alloc for non-tagged or - // unresolved cases (nullable, str-returning, etc.). - let spillsz: i32 = 24; - if (scrutt != nil) { - let resolved: i32 = slotsize(c, scrutt); - if (resolved > spillsz) { spillsz = resolved; }; - }; + // single-word (AX = ptr); only +0 is read. + // Scrutinee type + spill size factored into matchscrutt + // / matchspillsz so scanlocals stays lockstep — see + // cgenutil.ww (task #9 align-down to cstage). + scrutt = matchscrutt(c, scrut); + let spillsz: i32 = matchspillsz(c, scrutt); scrutoff = localalloc(c, "@match_spill", spillsz, nil); cgexpr(c, scrut); emitline("\tMOVQ\tAX, "); @@ -1039,15 +989,17 @@ fn cgmatch(c: *cgen, n: *node) void = { emitline("\tMOVQ\tDX, "); emitoff((scrutoff + 8): i64); emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((scrutoff + 16): i64); - emitline("(BP)\n"); - // R8 carries the 4th return word when the - // scrutinee's tagged union has a slice-payload - // variant (slot 32B). Harmless for narrower - // returns — R8 is callee-clobbered either way. - let ssz: i32 = slotsize(c, scrutt); - if (ssz > 24) { + // CX/R8 writes gated on spill size so 1-word- + // payload variants (slot 16B) don't bump the + // frame past the tag+word0 the receiver reads. + // Mirrors cmd/w6c/cgen.c cgmatch's + // `if (slot_size > 16)` / `> 24` guards. + if (spillsz > 16) { + emitline("\tMOVQ\tCX, "); + emitoff((scrutoff + 16): i64); + emitline("(BP)\n"); + }; + if (spillsz > 24) { emitline("\tMOVQ\tR8, "); emitoff((scrutoff + 24): i64); emitline("(BP)\n"); diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 5d509640..f668e46a 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -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 diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 3bf48df5..ce5d316a 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -8197,6 +8197,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 @@ -10676,65 +10738,15 @@ fn cgmatch(c: *cgen, n: *node) void = { // Non-ident scrutinee (call result, arr[i], p.field, // ?, etc.). Spill into an `@match_spill` scratch slot // and dispatch off it. Tagged returns (N_CALL) follow - // the AX:DX:CX convention; tagged-element loads + // the AX:DX:CX[:R8] convention; tagged-element loads // (N_INDEX) and tagged-field loads (N_DOT, fixed by // #28) produce the same triple. Nullable returns are - // single-word (AX = ptr); only +0 is read, so the - // extra stores are harmless. We recover the scrutinee - // type from fnretlookup (N_CALL), the base local's - // array element type (N_INDEX), or the struct field - // type (N_DOT) so dispatch can compute variant - // indices. Slot size is then derived from the - // scrutinee type so slice-variant tagged-unions - // (32B slot) don't overflow a hardcoded 24B scratch. - if (scrut.kind == 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) { scrutt = resolvetagged(c, rt); }; - }; - }; - }; - if (scrut.kind == nkind.N_INDEX) { - let ibase: *node = scrut.lhs; - if (ibase != nil) { - if (ibase.kind == nkind.N_IDENT) { - 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) { - 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) { - scrutt = resolvetagged(c, etn); - }; - }; - }; - }; - }; - if (scrut.kind == nkind.N_DOT) { - let ft: *node = dotfieldtnode(c, scrut); - if (ft != nil) { - scrutt = resolvetagged(c, ft); - }; - }; - // Size the spill to the scrutinee slot. Default 24B - // preserves the historical alloc for non-tagged or - // unresolved cases (nullable, str-returning, etc.). - let spillsz: i32 = 24; - if (scrutt != nil) { - let resolved: i32 = slotsize(c, scrutt); - if (resolved > spillsz) { spillsz = resolved; }; - }; + // single-word (AX = ptr); only +0 is read. + // Scrutinee type + spill size factored into matchscrutt + // / matchspillsz so scanlocals stays lockstep — see + // cgenutil.ww (task #9 align-down to cstage). + scrutt = matchscrutt(c, scrut); + let spillsz: i32 = matchspillsz(c, scrutt); scrutoff = localalloc(c, "@match_spill", spillsz, nil); cgexpr(c, scrut); emitline("\tMOVQ\tAX, "); @@ -10744,15 +10756,17 @@ fn cgmatch(c: *cgen, n: *node) void = { emitline("\tMOVQ\tDX, "); emitoff((scrutoff + 8): i64); emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((scrutoff + 16): i64); - emitline("(BP)\n"); - // R8 carries the 4th return word when the - // scrutinee's tagged union has a slice-payload - // variant (slot 32B). Harmless for narrower - // returns — R8 is callee-clobbered either way. - let ssz: i32 = slotsize(c, scrutt); - if (ssz > 24) { + // CX/R8 writes gated on spill size so 1-word- + // payload variants (slot 16B) don't bump the + // frame past the tag+word0 the receiver reads. + // Mirrors cmd/w6c/cgen.c cgmatch's + // `if (slot_size > 16)` / `> 24` guards. + if (spillsz > 16) { + emitline("\tMOVQ\tCX, "); + emitoff((scrutoff + 16): i64); + emitline("(BP)\n"); + }; + if (spillsz > 24) { emitline("\tMOVQ\tR8, "); emitoff((scrutoff + 24): i64); emitline("(BP)\n"); @@ -16634,14 +16648,19 @@ fn scanlocals(c: *cgen, n: *node) i32 = { }; }; }; - // `match (non-ident)` needs a 24B `@match_spill` scratch slot for - // cgmatch to land the AX:DX:CX return triple. Mirrors C cgen's - // localoff("@match_spill", ...). N_IDENT scrutinees read the slot + // `match (non-ident)` needs an `@match_spill` scratch slot sized + // to the scrutinee's tagged-union slot (16/24/32 for 1/2/3-word + // payload). Mirrors cstage's `slot_size = su->size` default 16 + // in cmd/w6c/cgen.c cgmatch (task #9 align-down to cstage). + // matchspillsz must agree with cgmatch's emit-time computation + // for scan+emit lockstep. N_IDENT scrutinees read the slot // directly off the local — no spill needed. if (n.kind == nkind.N_MATCH) { let sc: *node = n.lhs; if (sc != nil) { - if (sc.kind != nkind.N_IDENT) { total += 24; }; + if (sc.kind != nkind.N_IDENT) { + total += matchspillsz(c, matchscrutt(c, sc)); + }; }; }; // Match-arm binding (`case let v: T => ...`) gets a slot too. diff --git a/test/wcc/716_match_spill_pointer_payload.c b/test/wcc/716_match_spill_pointer_payload.c new file mode 100644 index 00000000..e2b5cc8b --- /dev/null +++ b/test/wcc/716_match_spill_pointer_payload.c @@ -0,0 +1,287 @@ +/* + * 716_match_spill_pointer_payload — wwstage @match_spill scratch slot + * sized to the scrutinee's tagged-union slot, not a hardcoded 24B + * default. Surfaced during task #23 (lib/os path *u8 → str migration) + * where `kpath` originally returned `(*u8 | oserror)`. + * + * Pre-fix (#9): cgmatch's non-ident scrutinee spill defaulted spillsz + * to 24 and only grew it; cgendecl scanlocals mirrored with a hardcoded + * `total += 24`. For a 1-word-payload tagged union (`(*u8 | oserror)` + * with `type oserror = !i64`) the actual slot is 16B (tag + one 8B + * word) — cstage allocated $48 / 16B slot / 2-word ABI (AX=tag, DX), + * wwstage allocated $64 / 24B slot / 3-word ABI (AX=tag, DX, CX). + * Frame-size drift broke bootstrap byte-identity the moment any caller + * matched on such a return. + * + * Fix (#9, wwstage-only per rule 10): factor scrutinee-type resolution + * into matchscrutt; factor slot-size computation into matchspillsz + * (default 16, mirroring cmd/w6c/cgen.c cgmatch's `slot_size = (su-> + * kind == TY_TAGGED) ? su->size : 16`). cgmatch gates CX write on + * `spillsz > 16` (R8 gate `> 24` was already correct). scanlocals + * uses the same helpers so scan + emit stay lockstep. + * + * What this test pins: + * - Asm byte-identity between cstage and wwstage for the canonical + * `(*u8 | oserror)` shape (aliased !i64) and the raw `(*u8 | i64)` + * shape — both must produce $48 frame, 16B spill slot, 2-word + * return ABI. + * - Runtime: both arms (pointer / error) round-trip the payload + * correctly through the narrower spill. + * - 24B-slot regression guard: `(str | i64)` (max payload 16B, slot + * 24B) still byte-identical post-fix — the gate must keep the CX + * write for slot_size > 16. + * + * NOT covered: raw `(*u8 | !i64)` (no alias) — wwstage's variant-index + * resolution for the raw N_TBANG-in-union shape has a separate pre- + * existing tag-emit divergence from cstage (tag=0 vs tag=1) that is + * orthogonal to the spill-slot sizing fixed by #9. Documented at the + * probe .ai/probe_tagged_return_pointer_payload.ww. + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +struct row { const char *label; const char *src; int want; }; + +static const struct row rows[] = { + /* 1. Canonical `(*u8 | oserror)` (aliased !i64). Pointer arm + * returns the first byte through a str-literal `.ptr`. Avoiding + * a top-level `let buf: [16]u8;` sidesteps an orthogonal wwstage + * DATAW-emit divergence for module-scope uninit `[N]u8`. */ + { "ptr_payload_aliased_bang_ok", + "type oserror = !i64;\n" + "fn kpath(p: str) (*u8 | oserror) = {\n" + " if (p.len < 0) { return -36i64: oserror; };\n" + " let s: str = \"A\";\n" + " return s.ptr;\n" + "};\n" + "fn main() i32 = {\n" + " let q: str = \"x\";\n" + " match (kpath(q)) {\n" + " case let e: oserror => return (e: i64): i32;\n" + " case let p: *u8 => return p[0]: i32;\n" + " };\n" + " return 0;\n" + "};\n", + 65 }, + /* 2. Same shape, error arm: pre-fix the 24B spill clobbered CX + * (irrelevant tag-only path) and bumped the frame; this row + * pins the error arm round-trips -36 + 100 = 64. */ + { "ptr_payload_aliased_bang_err", + "type oserror = !i64;\n" + "fn kpath(p: str) (*u8 | oserror) = {\n" + " if (p.len >= 0) { return -36i64: oserror; };\n" + " let s: str = \"A\";\n" + " return s.ptr;\n" + "};\n" + "fn main() i32 = {\n" + " let q: str = \"x\";\n" + " match (kpath(q)) {\n" + " case let e: oserror => return ((e: i64): i32) + 100;\n" + " case let p: *u8 => return p[0]: i32;\n" + " };\n" + " return 0;\n" + "};\n", + 64 }, + /* 3. Raw `(*u8 | i64)` — no alias, no `!`. Hypothesis (b) probe: + * the spill-slot fix holds regardless of whether the payload is + * wrapped in a type alias. */ + { "ptr_payload_raw_i64_ok", + "fn kpath(p: str) (*u8 | i64) = {\n" + " if (p.len < 0) { return -36i64; };\n" + " let s: str = \"Z\";\n" + " return s.ptr;\n" + "};\n" + "fn main() i32 = {\n" + " let q: str = \"x\";\n" + " match (kpath(q)) {\n" + " case let e: i64 => return e: i32;\n" + " case let p: *u8 => return p[0]: i32;\n" + " };\n" + " return 0;\n" + "};\n", + 90 }, + /* 4. 24B-slot regression guard. `(str | i64)` slot = 8 tag + + * 16 str = 24. The CX write at slot+16 is required (carries + * .len for the str arm). Pre-fix this row passed; post-fix the + * `spillsz > 16` gate must keep CX. Returns s.len for "hello" + * = 5. */ + { "str_payload_24B_keeps_cx", + "fn pick(b: bool) (str | i64) = {\n" + " if (b) { return \"hello\"; };\n" + " return 7i64;\n" + "};\n" + "fn main() i32 = {\n" + " match (pick(true)) {\n" + " case let e: i64 => return (e: i32) + 200;\n" + " case let s: str => return s.len: i32;\n" + " };\n" + " return 0;\n" + "};\n", + 5 }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/wcmsp_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/wcmsp_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s", + tmpdir, driver, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + + unlink(src); unlink(outbin); rmdir(tmpdir); + return got; +} + +/* asm_byte_identical — pin frame size + spill layout by diffing the + * w6c vs w6c_ww text output. The whole point of #9 is that wwstage's + * frame stops bloating for 1-word-payload variants, so the bytes + * must match (modulo orthogonal divergences — none on these rows). */ +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[64], cs[64], ws[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/wcmsp_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/wcmsp_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/wcmsp_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c errored\n", r->label); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); + unlink(src); unlink(cs); + return -1; + } + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + int rc = 0; + if (!fc || !fw) { + rc = -1; + } else { + for (;;) { + int a = fgetc(fc); + int b = fgetc(fw); + if (a != b) { rc = -1; break; } + if (a == EOF) break; + } + } + if (fc) fclose(fc); + if (fw) fclose(fw); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + unlink(src); unlink(cs); unlink(ws); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[640]; + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated_on_existence; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "match_spill_pointer_payload: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != rows[i].want) { + fprintf(stderr, + "match_spill_pointer_payload[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, + "match_spill_pointer_payload: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("match_spill_pointer_payload: %d/%d ok\n", total, total); + return 0; +}