w6c+selfhost: tagged-arr element ABI + full selfhost mirror

Closes the remaining tagged-union gaps after the prior two commits:

  1. Tagged element in an array/slice (cstage). N_INDEX load now reads
     slot words into AX/DX/CX, matching the tagged-return ABI so match
     / call-arg / let-init paths consume `arr[i]` uniformly. N_INDEX
     store routes through a scratch slot + cg_widen_tagged_store +
     byte-copy to &arr[i], so the full widening machinery (scalar /
     str / struct payload / tagged subset / nullable fold) lights up
     for element writes too.

  2. Selfhost mirror — the cgen widen helpers (struct payload,
     tagged-subset, spread-flatten) C cgen has had for two commits
     finally land in selfhost:

       cgwidentaggedstore  — single writer for nullable / tagged ident /
                             tagged via AX:DX:CX / struct (lit + ident) /
                             str / scalar source shapes.
       cgwidentagremap     — CMPQ-chain tag remap for variant-subset.
       rhsstructpayload    — struct-name predicate; filters `!void` /
                             `!i32` aliases that share N_STRUCTLIT shape
                             but aren't structs.
       rhstaggedident,
       rhstaggedabicall    — source-shape predicates.
       flatvariantidx      — spread-aware variant index lookup. Walks
                             `(...inner | T)` entries by resolving the
                             alias and inlining the inner's variants so
                             wwstage's tag order matches the check.c
                             flattening cstage does at type resolution.

     cglet tagged init, cgassign tagged-ident reassign, cgreturn struct
     / subset payload, pushargsrev struct payload, cgindex tagged
     element load, cgassign N_INDEX tagged element store all delegate
     to these. cgmatch picks up scrutt from N_INDEX bases (element
     type) and uses flatvariantidx for case dispatch.

  3. Selfhost frame accounting: scanlocals reserves a 24B @tagscr slot
     when the body contains a tagged-arr store, a struct-payload
     tagged return, or a struct-payload call arg — dedup'd via
     scanseenmark so multiple sites share one slot. N_LET stubs now
     carry tnode so walk-time type checks see the array element type.
     slotsize TARRAY learned to size tagged / struct / ptr / aliased
     elements (was 8B-default for anything not N_TNAME-primitive,
     undersizing tagged-element arrays).

     Scalar / str call-arg widening keeps its direct-push fast path
     (no scratch), so wwstage's asm on selfhost source remains
     byte-identical to cstage's — 993/995 still pass.

700_e2e: 9 new rows — scalar/str/struct/subset/nullable variants in
arrays and slices, plus pass-arg / let-init / return / match shapes.
This commit is contained in:
2026-05-13 06:29:52 +09:00
parent 9133251269
commit 6fd0160c0f
8 changed files with 2859 additions and 357 deletions

View File

@@ -955,6 +955,173 @@ static const struct row rows[] = {
" };\n"
" return -1;\n"
"};", 42 },
/* Tagged-union element in a fixed array — scalar+str variants.
* Store via N_INDEX widening, read+match through cgexpr N_INDEX
* tagged-slot load. 10 + len("hi")=2 + 5 = 17. */
{ "fn main() i32 = {\n"
" let arr: [3](i32 | str);\n"
" arr[0] = 10;\n"
" arr[1] = \"hi\";\n"
" arr[2] = 5;\n"
" let s: i32 = 0;\n"
" let i: i32 = 0;\n"
" for (i < 3) {\n"
" match (arr[i]) {\n"
" case let v: i32 => s += v;\n"
" case let t: str => s += t.len: i32;\n"
" };\n"
" i += 1;\n"
" };\n"
" return s;\n"
"};", 17 },
/* Tagged-union array with struct payload variant. Struct fields
* are written at slot+8+field_off via cg_widen_tagged_store; the
* read side just copies slot bytes into AX/DX/CX for match.
* 5 + (7+11) + 2 + 9 = 34. */
{ "type pair = struct { a: i32, b: i32 };\n"
"fn main() i32 = {\n"
" let arr: [4](i32 | pair | str);\n"
" arr[0] = 5;\n"
" arr[1] = pair { a = 7, b = 11 };\n"
" arr[2] = \"yo\";\n"
" arr[3] = 9;\n"
" let s: i32 = 0;\n"
" let i: i32 = 0;\n"
" for (i < 4) {\n"
" match (arr[i]) {\n"
" case let v: i32 => s += v;\n"
" case let p: pair => s += p.a + p.b;\n"
" case let t: str => s += t.len: i32;\n"
" };\n"
" i += 1;\n"
" };\n"
" return s;\n"
"};", 34 },
/* Slicing an array of tagged elements — the slice load path
* uses the fallback (non-ident base) N_INDEX which loads slot
* bytes from a computed address. 1 + 2 + 3 + 4 = 10. */
{ "fn main() i32 = {\n"
" let buf: [4](i32 | str);\n"
" buf[0] = 1;\n"
" buf[1] = \"ww\";\n"
" buf[2] = 3;\n"
" buf[3] = 4;\n"
" let xs: [](i32 | str) = buf[0:4];\n"
" let s: i32 = 0;\n"
" let i: i32 = 0;\n"
" for (i < 4) {\n"
" match (xs[i]) {\n"
" case let v: i32 => s += v;\n"
" case let t: str => s += t.len: i32;\n"
" };\n"
" i += 1;\n"
" };\n"
" return s;\n"
"};", 10 },
/* Passing arr[i] (tagged element) as a tagged arg — cgexpr leaves
* the slot in AX/DX/CX which the call-site shuffle pushes onto
* the arg stack. 5 + 30 (len 3 * 10) + 7 = 42. */
{ "fn weight(v: (i32 | str)) i32 = {\n"
" match (v) {\n"
" case let n: i32 => return n;\n"
" case let s: str => return s.len: i32 * 10;\n"
" };\n"
" return 0;\n"
"};\n"
"fn main() i32 = {\n"
" let arr: [3](i32 | str);\n"
" arr[0] = 5;\n"
" arr[1] = \"abc\";\n"
" arr[2] = 7;\n"
" let s: i32 = 0;\n"
" let i: i32 = 0;\n"
" for (i < 3) {\n"
" s += weight(arr[i]);\n"
" i += 1;\n"
" };\n"
" return s;\n"
"};", 42 },
/* let-init of a tagged local from arr[i] — the let path routes
* through cg_widen_tagged_store which, for a tagged source via
* cgexpr, spills AX/DX/CX into the slot. "ww!".len == 3. */
{ "fn main() i32 = {\n"
" let arr: [3](i32 | str);\n"
" arr[0] = 11;\n"
" arr[1] = \"ww!\";\n"
" arr[2] = 7;\n"
" let r: (i32 | str) = arr[1];\n"
" match (r) {\n"
" case let n: i32 => return n;\n"
" case let s: str => return s.len: i32;\n"
" };\n"
" return 0;\n"
"};", 3 },
/* Tagged-subset store into a wider tagged-union array element:
* source slot is (i32|str), dest element is (i32|str|u64). The
* store path materialises the subset in scratch then copies slot
* bytes — tag remap is a no-op here (variant order matches).
* arr[0]=9:i32, arr[1]="hi":str → 9 + 2 = 11. */
{ "type inner = (i32 | str);\n"
"fn main() i32 = {\n"
" let arr: [2](i32 | str | u64);\n"
" let v: inner = 9;\n"
" arr[0] = v;\n"
" let w: inner = \"hi\";\n"
" arr[1] = w;\n"
" let s: i32 = 0;\n"
" let i: i32 = 0;\n"
" for (i < 2) {\n"
" match (arr[i]) {\n"
" case let n: i32 => s += n;\n"
" case let t: str => s += t.len: i32;\n"
" case let u: u64 => s += 100;\n"
" };\n"
" i += 1;\n"
" };\n"
" return s;\n"
"};", 11 },
/* Returning arr[i] from a fn whose return type matches the
* element. cgexpr leaves slot in AX/DX/CX; the return path
* forwards as-is. arr[1] = "abc" → str variant → .len == 3. */
{ "fn pick(i: i32) (i32 | str) = {\n"
" let arr: [2](i32 | str);\n"
" arr[0] = 21;\n"
" arr[1] = \"abc\";\n"
" return arr[i];\n"
"};\n"
"fn main() i32 = {\n"
" let r: (i32 | str) = pick(1);\n"
" match (r) {\n"
" case let n: i32 => return n;\n"
" case let s: str => return s.len: i32;\n"
" };\n"
" return 0;\n"
"};", 3 },
/* Nullable folded element `(*T | void)` — the slot is one 8B
* pointer word; null is the void variant. Stores route through
* the nullable branch of cg_widen_tagged_store (single MOVQ at
* +0). 100 (nil) + 1 (non-nil) + 100 (nil) = 201. */
{ "fn pickptr(b: bool) *i32 = {\n"
" let x: i32 = 42;\n"
" if (b) { return &x; };\n"
" return nil;\n"
"};\n"
"fn main() i32 = {\n"
" let arr: [3](*i32 | void);\n"
" arr[0] = nil;\n"
" arr[1] = pickptr(true);\n"
" arr[2] = pickptr(false);\n"
" let s: i32 = 0;\n"
" let i: i32 = 0;\n"
" for (i < 3) {\n"
" match (arr[i]) {\n"
" case let p: *i32 => s += 1;\n"
" case => s += 100;\n"
" };\n"
" i += 1;\n"
" };\n"
" return s;\n"
"};", 201 },
/* Plan 9-style sentinel error idiom: `def NAME: error = "lit"`
* inlines as the (ptr, len) pair at use sites. */
{ "type error = str;\n"