w6c+wcc: widen struct/tagged-subset, parse ... spread

Three tagged-union gaps:

  1. Struct-payload widening was broken at every site (call, let,
     assign, return, struct-field init). cg_widen_tagged_store now
     materialises str / scalar / struct-lit / struct-ident / tagged
     payloads at slot+8+field_off and writes the tag last. Call sites
     route through cg_widen_tagged_push (scratch slot + push high→low).

  2. Tagged → wider tagged widening forwarded the source tag verbatim.
     cg_widen_tag_remap emits a CMPQ-chain switch that translates each
     source variant index to the destination's, then zero-pads to the
     wider slot. type_eq grew a TY_TAGGED arm (was returning 1 for any
     two unions); type_assignable now accepts variant-subset and
     rejects the rest.

  3. `(...inner | T)` spread parses (cmd/wcc/parse.c, lib/ww/parse).
     Marks Node.op = TK_ELLIPSIS; resolve_type unwraps NAMED + flattens
     when the spread bit is set so aliases inline like Hare's
     tagged_type unwrap flag.

Selfhost mirror: spread parser ported. Cgen widen helpers not yet
mirrored — wwstage stays byte-identical to cstage on the existing
test corpus, but will emit wrong asm if user code uses the new
patterns (probe sp2 shows the divergence).

700_e2e: 9 new rows covering call/let/assign/return × struct +
tagged subset, plus the spread-flatten case.
This commit is contained in:
2026-05-13 05:30:20 +09:00
parent 47d75d9b59
commit 9133251269
8 changed files with 562 additions and 161 deletions

View File

@@ -828,6 +828,133 @@ static const struct row rows[] = {
" };\n"
" return 0;\n"
"};", 3 },
/* Struct variant of a tagged union at the call site. The arg is
* an N_STRUCTLIT, the param is (str|point). Widening at the call
* site must zero-fill the scratch slot, store each field at
* slot+8+field_off, then push the slot words high→low. */
{ "type point = struct { x: i32, y: i32 };\n"
"fn classify(r: (str | point)) i32 = {\n"
" match (r) {\n"
" case let s: str => return 0 - s.len: i32;\n"
" case let p: point => return p.x + p.y;\n"
" };\n"
" return -1;\n"
"};\n"
"fn main() i32 = {\n"
" return classify(point { x = 10, y = 20 });\n"
"};", 30 },
/* Struct variant passed as a typed local. Widening copies the
* struct words from the local into the scratch slot at +8. */
{ "type point = struct { x: i32, y: i32 };\n"
"fn classify(r: (str | point)) i32 = {\n"
" match (r) {\n"
" case let s: str => return 0 - s.len: i32;\n"
" case let p: point => return p.x + p.y;\n"
" };\n"
" return -1;\n"
"};\n"
"fn main() i32 = {\n"
" let p: point = point { x = 11, y = 22 };\n"
" return classify(p);\n"
"};", 33 },
/* let-init of a tagged-union local from a struct literal: the
* field stores go into slot+8+field_off in-place; tag patched
* last. */
{ "type point = struct { x: i32, y: i32 };\n"
"fn main() i32 = {\n"
" let r: (str | point) = point { x = 7, y = 35 };\n"
" match (r) {\n"
" case let s: str => return 0 - s.len: i32;\n"
" case let p: point => return p.x + p.y;\n"
" };\n"
" return -1;\n"
"};", 42 },
/* Reassign a tagged-union local to a struct literal. Same path
* as let-init but writing into an already-allocated slot. */
{ "type point = struct { x: i32, y: i32 };\n"
"fn main() i32 = {\n"
" let r: (str | point) = \"init\";\n"
" r = point { x = 100, y = 23 };\n"
" match (r) {\n"
" case let s: str => return 0 - s.len: i32;\n"
" case let p: point => return p.x + p.y;\n"
" };\n"
" return -1;\n"
"};", 123 },
/* Return a struct variant of the fn's tagged return type. The
* scratch-slot path materialises the struct payload then loads
* AX/DX/CX from it. */
{ "type point = struct { x: i32, y: i32 };\n"
"fn make() (str | point) = {\n"
" return point { x = 12, y = 30 };\n"
"};\n"
"fn main() i32 = {\n"
" let r: (str | point) = make();\n"
" match (r) {\n"
" case let s: str => return 0 - s.len: i32;\n"
" case let p: point => return p.x + p.y;\n"
" };\n"
" return -1;\n"
"};", 42 },
/* Widen a smaller tagged union to a wider one across slot sizes
* AND remapped variant indices. (i32 | rune) is 16B with i32 at
* tag 0; (str | i32 | rune) is 24B with i32 at tag 1. The widen
* path copies the slot words, zero-pads to 24B, then runs a
* CMPQ-chain switch to remap src tag 0 → dst tag 1. */
{ "fn classify(r: (str | i32 | rune)) i32 = {\n"
" match (r) {\n"
" case let s: str => return 1;\n"
" case let n: i32 => return n;\n"
" case let c: rune => return c: i32 + 100;\n"
" };\n"
" return 0;\n"
"};\n"
"fn main() i32 = {\n"
" let inner: (i32 | rune) = 42: i32;\n"
" return classify(inner);\n"
"};", 42 },
/* Same shape but the rune variant of inner exercises the tag
* remap from src tag 1 → dst tag 2. The rune literal needs an
* explicit `: rune` cast — `'A'` is an untyped rune and the
* variant search picks the first variant that accepts it (i32,
* which also accepts untyped runes). 'A' = 65 + 100 = 165. */
{ "fn classify(r: (str | i32 | rune)) i32 = {\n"
" match (r) {\n"
" case let s: str => return 1;\n"
" case let n: i32 => return n;\n"
" case let c: rune => return c: i32 + 100;\n"
" };\n"
" return 0;\n"
"};\n"
"fn main() i32 = {\n"
" let inner: (i32 | rune) = 'A': rune;\n"
" return classify(inner);\n"
"};", 165 },
/* let-init of a wider tagged union from a smaller-tagged local. */
{ "fn main() i32 = {\n"
" let inner: (i32 | rune) = 42: i32;\n"
" let r: (str | i32 | rune) = inner;\n"
" match (r) {\n"
" case let s: str => return 1;\n"
" case let n: i32 => return n;\n"
" case let c: rune => return c: i32 + 100;\n"
" };\n"
" return 0;\n"
"};", 42 },
/* Spread variant in tagged-union type — `(...inner | T)`. The
* checker flattens the spread's variants into the enclosing
* union so `outer` has variants {i32, rune, str}. */
{ "type inner = (i32 | rune);\n"
"type outer = (...inner | str);\n"
"fn main() i32 = {\n"
" let r: outer = 42: i32;\n"
" match (r) {\n"
" case let n: i32 => return n;\n"
" case let c: rune => return c: i32;\n"
" case let s: str => return 0;\n"
" };\n"
" return -1;\n"
"};", 42 },
/* Plan 9-style sentinel error idiom: `def NAME: error = "lit"`
* inlines as the (ptr, len) pair at use sites. */
{ "type error = str;\n"