diff --git a/Makefile b/Makefile index 569b190a..9a375cd4 100644 --- a/Makefile +++ b/Makefile @@ -248,6 +248,8 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tagged_call_arg_run \ $(BIN)/test_sret_struct_return \ $(BIN)/test_sret_struct_return_run \ + $(BIN)/test_match_slice_variant \ + $(BIN)/test_match_slice_variant_run \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -543,6 +545,16 @@ $(BIN)/test_sret_struct_return_run: test/wcc/925_sret_struct_return_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_match_slice_variant: test/wcc/722_match_slice_variant.c \ + $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +$(BIN)/test_match_slice_variant_run: test/wcc/926_match_slice_variant_run.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 bde288f2..92024cf9 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -8798,10 +8798,16 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { let r: i32 = flatvariantidx(c, tagged, wantname); if (r >= 0) { return r; }; }; - // Fallback: by str-shape (resolves aliases). Walks the - // spread-flattened variant list so a `(...inner | str)` outer - // agrees with the (i32 | str) inner's str position. + // Shape fallback: classify rhs as (str, slice, scalar/other) and + // pick the first variant of matching shape. Cstage's type_eq + // distinguishes a `[]u8` arm from a `u8` arm at type-build; the + // name-only flatvariantidx pass above can't see `[]T`, so without + // the slice axis a (u8 | []u8) widen / match collapses every + // non-str rhs onto the leading scalar variant (task #19). Walks + // the spread-flattened list so a `(...inner | str)` outer agrees + // with the inner's str / slice positions. let wantstr: bool = nodeisstr(c, rhs); + let wantslice: bool = nodeisslice(c, rhs); let v: *node = tagged.list; let idx: i32 = 0; for (v != nil) { @@ -8816,11 +8822,9 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { if (inner.kind == nkind.N_TTAGGED) { let iv: *node = inner.list; for (iv != nil) { - let ivisstr: bool = false; - if (iv.kind == nkind.N_TNAME) { - if (isstrtype(c, iv)) { ivisstr = true; }; - }; - if (ivisstr == wantstr) { return idx; }; + let ivisstr: bool = isstrtype(c, iv); + let ivisslice: bool = isslicetype(c, iv); + if (ivisstr == wantstr && ivisslice == wantslice) { return idx; }; iv = iv.next; idx += 1; }; @@ -8829,11 +8833,9 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { }; }; }; - let visstr: bool = false; - if (v.kind == nkind.N_TNAME) { - if (isstrtype(c, v)) { visstr = true; }; - }; - if (visstr == wantstr) { return idx; }; + let visstr: bool = isstrtype(c, v); + let visslice: bool = isslicetype(c, v); + if (visstr == wantstr && visslice == wantslice) { return idx; }; v = v.next; idx += 1; }; @@ -8887,6 +8889,76 @@ fn flatvariantidx(c: *cgen, tagged: *node, want: str) i32 = { return -1; }; +// flatslicevariantidx — flat 0-based index of the first slice-shape +// variant in `tagged` (`...inner` spread expanded). When `elem` is an +// N_TNAME, prefer a `[]` variant; falls back to the first +// slice slot if no element match is found. Cstage walks resolved +// Type pointers and dispatches via cg_tag_for_variant / type_eq; +// wwstage's name-keyed flatvariantidx can't see a `[]u8` variant +// (pat.str == ""), collapsing every (scalar | []T) match arm and +// widen-to-tagged call onto tag 0. Task #19. Returns -1 when no +// slice variant exists. +fn flatslicevariantidx(c: *cgen, tagged: *node, elem: *node) i32 = { + if (tagged == nil) { return -1; }; + if (tagged.kind != nkind.N_TTAGGED) { return -1; }; + let elemname: str; + elemname.ptr = nil; elemname.len = 0; + if (elem != nil) { + if (elem.kind == nkind.N_TNAME) { elemname = elem.str; }; + }; + let fallback: i32 = -1; + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + let isspread: bool = (v.op == tkind.TK_ELLIPSIS); + if (isspread) { + let inner: *node = v; + if (inner.kind == nkind.N_TNAME) { + let a: *node = aliaslookup(c, inner.str); + if (a != nil) { inner = a; }; + }; + if (inner != nil) { + if (inner.kind == nkind.N_TTAGGED) { + let iv: *node = inner.list; + for (iv != nil) { + if (isslicetype(c, iv)) { + if (fallback < 0) { fallback = idx; }; + if (elemname.len > 0) { + if (iv.kind == nkind.N_TSLICE) { + if (iv.lhs != nil) { + if (iv.lhs.kind == nkind.N_TNAME) { + if (variantnamematch(iv.lhs.str, elemname)) { return idx; }; + }; + }; + }; + }; + }; + iv = iv.next; + idx += 1; + }; + v = v.next; + continue; + }; + }; + }; + if (isslicetype(c, v)) { + if (fallback < 0) { fallback = idx; }; + if (elemname.len > 0) { + if (v.kind == nkind.N_TSLICE) { + if (v.lhs != nil) { + if (v.lhs.kind == nkind.N_TNAME) { + if (variantnamematch(v.lhs.str, elemname)) { return idx; }; + }; + }; + }; + }; + }; + v = v.next; + idx += 1; + }; + return fallback; +}; + // cgwidentagremap — when widening from one tagged union to a wider one, // rewrite the source's variant tag at slot_off+0 to use the destination's // variant indices. No-op when src and dst index orders coincide. @@ -10104,6 +10176,12 @@ fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { if (tagged == nil) { return -1; }; if (vt == nil) { return -1; }; if (tagged.kind != nkind.N_TTAGGED) { return -1; }; + // `is []T` / `as []T` — slice-shape variant lookup routes through + // the shape-aware helper so non-N_TNAME variant nodes (which + // flatvariantidx's name key can't see) resolve. Task #19. + if (vt.kind == nkind.N_TSLICE) { + return flatslicevariantidx(c, tagged, vt.lhs); + }; let want: str; want.ptr = nil; want.len = 0; if (vt.kind == nkind.N_TNAME) { want = vt.str; }; @@ -11040,10 +11118,17 @@ fn cgmatch(c: *cgen, n: *node) void = { let want: i32 = 0; if (scrutt != nil) { if (scrutt.kind == nkind.N_TTAGGED) { - let patname: str; - patname.ptr = nil; patname.len = 0; - if (pat.kind == nkind.N_TNAME) { patname = pat.str; }; - let r: i32 = flatvariantidx(c, scrutt, patname); + let r: i32 = -1; + if (pat.kind == nkind.N_TNAME) { + r = flatvariantidx(c, scrutt, pat.str); + } else { if (pat.kind == nkind.N_TSLICE) { + // `case let s: []T =>` — pat.str is empty + // because the variant is a composite, so + // route through the slice-shape helper. + // Without this every (scalar | []T) match + // arm collapses to tag 0 (task #19). + r = flatslicevariantidx(c, scrutt, pat.lhs); + }; }; if (r >= 0) { want = r; }; }; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 9c99dd54..42d28be7 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -116,6 +116,12 @@ fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { if (tagged == nil) { return -1; }; if (vt == nil) { return -1; }; if (tagged.kind != nkind.N_TTAGGED) { return -1; }; + // `is []T` / `as []T` — slice-shape variant lookup routes through + // the shape-aware helper so non-N_TNAME variant nodes (which + // flatvariantidx's name key can't see) resolve. Task #19. + if (vt.kind == nkind.N_TSLICE) { + return flatslicevariantidx(c, tagged, vt.lhs); + }; let want: str; want.ptr = nil; want.len = 0; if (vt.kind == nkind.N_TNAME) { want = vt.str; }; @@ -1052,10 +1058,17 @@ fn cgmatch(c: *cgen, n: *node) void = { let want: i32 = 0; if (scrutt != nil) { if (scrutt.kind == nkind.N_TTAGGED) { - let patname: str; - patname.ptr = nil; patname.len = 0; - if (pat.kind == nkind.N_TNAME) { patname = pat.str; }; - let r: i32 = flatvariantidx(c, scrutt, patname); + let r: i32 = -1; + if (pat.kind == nkind.N_TNAME) { + r = flatvariantidx(c, scrutt, pat.str); + } else { if (pat.kind == nkind.N_TSLICE) { + // `case let s: []T =>` — pat.str is empty + // because the variant is a composite, so + // route through the slice-shape helper. + // Without this every (scalar | []T) match + // arm collapses to tag 0 (task #19). + r = flatslicevariantidx(c, scrutt, pat.lhs); + }; }; if (r >= 0) { want = r; }; }; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 4c09674d..8b5f8563 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -2414,10 +2414,16 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { let r: i32 = flatvariantidx(c, tagged, wantname); if (r >= 0) { return r; }; }; - // Fallback: by str-shape (resolves aliases). Walks the - // spread-flattened variant list so a `(...inner | str)` outer - // agrees with the (i32 | str) inner's str position. + // Shape fallback: classify rhs as (str, slice, scalar/other) and + // pick the first variant of matching shape. Cstage's type_eq + // distinguishes a `[]u8` arm from a `u8` arm at type-build; the + // name-only flatvariantidx pass above can't see `[]T`, so without + // the slice axis a (u8 | []u8) widen / match collapses every + // non-str rhs onto the leading scalar variant (task #19). Walks + // the spread-flattened list so a `(...inner | str)` outer agrees + // with the inner's str / slice positions. let wantstr: bool = nodeisstr(c, rhs); + let wantslice: bool = nodeisslice(c, rhs); let v: *node = tagged.list; let idx: i32 = 0; for (v != nil) { @@ -2432,11 +2438,9 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { if (inner.kind == nkind.N_TTAGGED) { let iv: *node = inner.list; for (iv != nil) { - let ivisstr: bool = false; - if (iv.kind == nkind.N_TNAME) { - if (isstrtype(c, iv)) { ivisstr = true; }; - }; - if (ivisstr == wantstr) { return idx; }; + let ivisstr: bool = isstrtype(c, iv); + let ivisslice: bool = isslicetype(c, iv); + if (ivisstr == wantstr && ivisslice == wantslice) { return idx; }; iv = iv.next; idx += 1; }; @@ -2445,11 +2449,9 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { }; }; }; - let visstr: bool = false; - if (v.kind == nkind.N_TNAME) { - if (isstrtype(c, v)) { visstr = true; }; - }; - if (visstr == wantstr) { return idx; }; + let visstr: bool = isstrtype(c, v); + let visslice: bool = isslicetype(c, v); + if (visstr == wantstr && visslice == wantslice) { return idx; }; v = v.next; idx += 1; }; @@ -2503,6 +2505,76 @@ fn flatvariantidx(c: *cgen, tagged: *node, want: str) i32 = { return -1; }; +// flatslicevariantidx — flat 0-based index of the first slice-shape +// variant in `tagged` (`...inner` spread expanded). When `elem` is an +// N_TNAME, prefer a `[]` variant; falls back to the first +// slice slot if no element match is found. Cstage walks resolved +// Type pointers and dispatches via cg_tag_for_variant / type_eq; +// wwstage's name-keyed flatvariantidx can't see a `[]u8` variant +// (pat.str == ""), collapsing every (scalar | []T) match arm and +// widen-to-tagged call onto tag 0. Task #19. Returns -1 when no +// slice variant exists. +fn flatslicevariantidx(c: *cgen, tagged: *node, elem: *node) i32 = { + if (tagged == nil) { return -1; }; + if (tagged.kind != nkind.N_TTAGGED) { return -1; }; + let elemname: str; + elemname.ptr = nil; elemname.len = 0; + if (elem != nil) { + if (elem.kind == nkind.N_TNAME) { elemname = elem.str; }; + }; + let fallback: i32 = -1; + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + let isspread: bool = (v.op == tkind.TK_ELLIPSIS); + if (isspread) { + let inner: *node = v; + if (inner.kind == nkind.N_TNAME) { + let a: *node = aliaslookup(c, inner.str); + if (a != nil) { inner = a; }; + }; + if (inner != nil) { + if (inner.kind == nkind.N_TTAGGED) { + let iv: *node = inner.list; + for (iv != nil) { + if (isslicetype(c, iv)) { + if (fallback < 0) { fallback = idx; }; + if (elemname.len > 0) { + if (iv.kind == nkind.N_TSLICE) { + if (iv.lhs != nil) { + if (iv.lhs.kind == nkind.N_TNAME) { + if (variantnamematch(iv.lhs.str, elemname)) { return idx; }; + }; + }; + }; + }; + }; + iv = iv.next; + idx += 1; + }; + v = v.next; + continue; + }; + }; + }; + if (isslicetype(c, v)) { + if (fallback < 0) { fallback = idx; }; + if (elemname.len > 0) { + if (v.kind == nkind.N_TSLICE) { + if (v.lhs != nil) { + if (v.lhs.kind == nkind.N_TNAME) { + if (variantnamematch(v.lhs.str, elemname)) { return idx; }; + }; + }; + }; + }; + }; + v = v.next; + idx += 1; + }; + return fallback; +}; + // cgwidentagremap — when widening from one tagged union to a wider one, // rewrite the source's variant tag at slot_off+0 to use the destination's // variant indices. No-op when src and dst index orders coincide. diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 9c7d45fb..9a4b6512 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -8798,10 +8798,16 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { let r: i32 = flatvariantidx(c, tagged, wantname); if (r >= 0) { return r; }; }; - // Fallback: by str-shape (resolves aliases). Walks the - // spread-flattened variant list so a `(...inner | str)` outer - // agrees with the (i32 | str) inner's str position. + // Shape fallback: classify rhs as (str, slice, scalar/other) and + // pick the first variant of matching shape. Cstage's type_eq + // distinguishes a `[]u8` arm from a `u8` arm at type-build; the + // name-only flatvariantidx pass above can't see `[]T`, so without + // the slice axis a (u8 | []u8) widen / match collapses every + // non-str rhs onto the leading scalar variant (task #19). Walks + // the spread-flattened list so a `(...inner | str)` outer agrees + // with the inner's str / slice positions. let wantstr: bool = nodeisstr(c, rhs); + let wantslice: bool = nodeisslice(c, rhs); let v: *node = tagged.list; let idx: i32 = 0; for (v != nil) { @@ -8816,11 +8822,9 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { if (inner.kind == nkind.N_TTAGGED) { let iv: *node = inner.list; for (iv != nil) { - let ivisstr: bool = false; - if (iv.kind == nkind.N_TNAME) { - if (isstrtype(c, iv)) { ivisstr = true; }; - }; - if (ivisstr == wantstr) { return idx; }; + let ivisstr: bool = isstrtype(c, iv); + let ivisslice: bool = isslicetype(c, iv); + if (ivisstr == wantstr && ivisslice == wantslice) { return idx; }; iv = iv.next; idx += 1; }; @@ -8829,11 +8833,9 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { }; }; }; - let visstr: bool = false; - if (v.kind == nkind.N_TNAME) { - if (isstrtype(c, v)) { visstr = true; }; - }; - if (visstr == wantstr) { return idx; }; + let visstr: bool = isstrtype(c, v); + let visslice: bool = isslicetype(c, v); + if (visstr == wantstr && visslice == wantslice) { return idx; }; v = v.next; idx += 1; }; @@ -8887,6 +8889,76 @@ fn flatvariantidx(c: *cgen, tagged: *node, want: str) i32 = { return -1; }; +// flatslicevariantidx — flat 0-based index of the first slice-shape +// variant in `tagged` (`...inner` spread expanded). When `elem` is an +// N_TNAME, prefer a `[]` variant; falls back to the first +// slice slot if no element match is found. Cstage walks resolved +// Type pointers and dispatches via cg_tag_for_variant / type_eq; +// wwstage's name-keyed flatvariantidx can't see a `[]u8` variant +// (pat.str == ""), collapsing every (scalar | []T) match arm and +// widen-to-tagged call onto tag 0. Task #19. Returns -1 when no +// slice variant exists. +fn flatslicevariantidx(c: *cgen, tagged: *node, elem: *node) i32 = { + if (tagged == nil) { return -1; }; + if (tagged.kind != nkind.N_TTAGGED) { return -1; }; + let elemname: str; + elemname.ptr = nil; elemname.len = 0; + if (elem != nil) { + if (elem.kind == nkind.N_TNAME) { elemname = elem.str; }; + }; + let fallback: i32 = -1; + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + let isspread: bool = (v.op == tkind.TK_ELLIPSIS); + if (isspread) { + let inner: *node = v; + if (inner.kind == nkind.N_TNAME) { + let a: *node = aliaslookup(c, inner.str); + if (a != nil) { inner = a; }; + }; + if (inner != nil) { + if (inner.kind == nkind.N_TTAGGED) { + let iv: *node = inner.list; + for (iv != nil) { + if (isslicetype(c, iv)) { + if (fallback < 0) { fallback = idx; }; + if (elemname.len > 0) { + if (iv.kind == nkind.N_TSLICE) { + if (iv.lhs != nil) { + if (iv.lhs.kind == nkind.N_TNAME) { + if (variantnamematch(iv.lhs.str, elemname)) { return idx; }; + }; + }; + }; + }; + }; + iv = iv.next; + idx += 1; + }; + v = v.next; + continue; + }; + }; + }; + if (isslicetype(c, v)) { + if (fallback < 0) { fallback = idx; }; + if (elemname.len > 0) { + if (v.kind == nkind.N_TSLICE) { + if (v.lhs != nil) { + if (v.lhs.kind == nkind.N_TNAME) { + if (variantnamematch(v.lhs.str, elemname)) { return idx; }; + }; + }; + }; + }; + }; + v = v.next; + idx += 1; + }; + return fallback; +}; + // cgwidentagremap — when widening from one tagged union to a wider one, // rewrite the source's variant tag at slot_off+0 to use the destination's // variant indices. No-op when src and dst index orders coincide. @@ -10104,6 +10176,12 @@ fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { if (tagged == nil) { return -1; }; if (vt == nil) { return -1; }; if (tagged.kind != nkind.N_TTAGGED) { return -1; }; + // `is []T` / `as []T` — slice-shape variant lookup routes through + // the shape-aware helper so non-N_TNAME variant nodes (which + // flatvariantidx's name key can't see) resolve. Task #19. + if (vt.kind == nkind.N_TSLICE) { + return flatslicevariantidx(c, tagged, vt.lhs); + }; let want: str; want.ptr = nil; want.len = 0; if (vt.kind == nkind.N_TNAME) { want = vt.str; }; @@ -11040,10 +11118,17 @@ fn cgmatch(c: *cgen, n: *node) void = { let want: i32 = 0; if (scrutt != nil) { if (scrutt.kind == nkind.N_TTAGGED) { - let patname: str; - patname.ptr = nil; patname.len = 0; - if (pat.kind == nkind.N_TNAME) { patname = pat.str; }; - let r: i32 = flatvariantidx(c, scrutt, patname); + let r: i32 = -1; + if (pat.kind == nkind.N_TNAME) { + r = flatvariantidx(c, scrutt, pat.str); + } else { if (pat.kind == nkind.N_TSLICE) { + // `case let s: []T =>` — pat.str is empty + // because the variant is a composite, so + // route through the slice-shape helper. + // Without this every (scalar | []T) match + // arm collapses to tag 0 (task #19). + r = flatslicevariantidx(c, scrutt, pat.lhs); + }; }; if (r >= 0) { want = r; }; }; }; diff --git a/test/wcc/722_match_slice_variant.c b/test/wcc/722_match_slice_variant.c new file mode 100644 index 00000000..2db91d16 --- /dev/null +++ b/test/wcc/722_match_slice_variant.c @@ -0,0 +1,181 @@ +/* + * 722_match_slice_variant — Class A asm-presence + byte-id sentinel + * for task #19. Pins that the variant tag chosen for a `[]T` arm in a + * `(scalar | []T)` tagged union matches between cstage and wwstage at + * BOTH dispatch (cgmatch's `CMPQ $K, AX`) and call-site (the + * `MOVQ $K, AX` widening that materialises the tag for the slice + * payload). + * + * Pre-fix wwstage's name-keyed flatvariantidx / str-shape fallback + * landed on tag 0 for any `[]T` lookup (pat.str == "" because the + * variant node is N_TSLICE, not N_TNAME). Both the case arm and the + * call-site emitted `$0` — internally consistent inside wwstage, but + * the cstage-vs-wwstage cmp -s diverges from cstage's correct `$1`. + * Class A: silent until wwstage path engaged. 967_bytes_run was + * compiled through cstage only and never tripped; lib/strings landing + * pulls bytes through wwstage and surfaces the dispatch miscompile + * via 995_self_rebuild. + * + * Asserts cmp -s byte-id on the canonical (u8 | []u8) probe plus + * other (scalar | []scalar) shapes from rob's matrix. + */ +#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; +}; + +static const struct row rows[] = { + /* The canonical (u8 | []u8) repro from bytes.index. */ + { "u8_slice_u8", + "fn pick(n: (u8 | []u8)) i32 = {\n" + " match (n) {\n" + " case let c: u8 => return 1;\n" + " case let s: []u8 => return 2;\n" + " };\n" + " return 0;\n" + "};\n" + "export fn main() i32 = {\n" + " let r1: i32 = pick(7u8);\n" + " let buf: [2]u8;\n" + " let r2: i32 = pick(buf[0:2]);\n" + " if (r1 != 1) { return 11; };\n" + " if (r2 != 2) { return 12; };\n" + " return 0;\n" + "};\n" }, + /* Reverse-order shape (slice arm first): the same shape-fallback + * that lost (u8 | []u8) must also resolve ([]u8 | u8) without + * accidentally inverting via the str-shape collapse. */ + { "slice_u8_then_u8", + "fn pick(n: ([]u8 | u8)) i32 = {\n" + " match (n) {\n" + " case let s: []u8 => return 1;\n" + " case let c: u8 => return 2;\n" + " };\n" + " return 0;\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [2]u8;\n" + " let r1: i32 = pick(buf[0:2]);\n" + " let r2: i32 = pick(7u8);\n" + " if (r1 != 1) { return 11; };\n" + " if (r2 != 2) { return 12; };\n" + " return 0;\n" + "};\n" }, + /* Three-arm (u8 | []u8 | str): proves the slice axis composes with + * the existing str axis (str arm at idx 2 stays at idx 2). */ + { "u8_slice_u8_str", + "fn pick(n: (u8 | []u8 | str)) i32 = {\n" + " match (n) {\n" + " case let c: u8 => return 1;\n" + " case let s: []u8 => return 2;\n" + " case let t: str => return 3;\n" + " };\n" + " return 0;\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [2]u8;\n" + " if (pick(7u8) != 1) { return 11; };\n" + " if (pick(buf[0:2]) != 2) { return 12; };\n" + " if (pick(\"hi\") != 3) { return 13; };\n" + " return 0;\n" + "};\n" }, +}; + +static int +emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) +{ + char src[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/msv_asm_%d_%d.ww", getpid(), i); + snprintf(out_s, cap, "/tmp/msv_asm_%d_%d_%s.s", + getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); + int rc = runwait(cmd); + unlink(src); + 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 w6c[640], w6c_ww[640]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + + int have_ww = (access(w6c_ww, X_OK) == 0); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + char cs_path[128], ws_path[128]; + + if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { + fprintf(stderr, + "match_slice_variant[cstage][%s]: w6c failed\n", + rows[i].label); + fail++; total++; continue; + } + + if (!have_ww) { unlink(cs_path); continue; } + + if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { + fprintf(stderr, + "match_slice_variant[wwstage][%s]: w6c_ww failed\n", + rows[i].label); + fail++; total++; + unlink(cs_path); + continue; + } + total++; + char cmd[512]; + snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); + if (runwait(cmd) != 0) { + fprintf(stderr, + "match_slice_variant[%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + + unlink(cs_path); unlink(ws_path); + } + + if (fail) { + fprintf(stderr, + "match_slice_variant: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("match_slice_variant: %d/%d ok\n", total, total); + return 0; +} diff --git a/test/wcc/926_match_slice_variant_run.c b/test/wcc/926_match_slice_variant_run.c new file mode 100644 index 00000000..c05bf28d --- /dev/null +++ b/test/wcc/926_match_slice_variant_run.c @@ -0,0 +1,264 @@ +/* + * 926_match_slice_variant_run — Class B semantic test for task #19. + * Pre-fix wwstage emitted internally-consistent but wrong tag values + * for any `(scalar | []T)` widen/dispatch: every []T slot collapsed + * onto tag 0, so the match arm that fired was always the leading + * variant regardless of which arm the caller meant. cstage was + * correct on these shapes. + * + * 722 pins asm byte-id (the polarity sentinel). This file pins the + * end-to-end runtime: build through each driver and confirm both + * arms reachable, with payload survival on the slice arm. + * + * Matrix per rob (task #19): (u8 | []u8), reverse-order ([]u8 | u8), + * other scalar-vs-slice-of-same-primitive at i8/i32/u64/rune widths, + * and a three-arm (u8 | []u8 | str). + */ +#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[] = { + /* The canonical bytes.index shape: scalar arm returns 1, slice + * arm returns 2, and the slice arm also checks payload survives + * (sub.len + first byte) so a silent payload-drop isn't masked. */ + { "u8_slice_u8", + "fn pick(n: (u8 | []u8)) i32 = {\n" + " match (n) {\n" + " case let c: u8 => return c: i32;\n" + " case let s: []u8 => return 100i32 + (s.len: i32) + (s[0]: i32);\n" + " };\n" + " return -1;\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [3]u8;\n" + " buf[0] = 7u8; buf[1] = 8u8; buf[2] = 9u8;\n" + " if (pick(42u8) != 42) { return 11; };\n" + " if (pick(buf[0:3]) != 110) { return 12; };\n" + " return 0;\n" + "};\n", + 0 }, + /* Reverse-order: same shape with slice arm at idx 0. The + * shape-fallback in taggedvariantindex must yield idx 0 here, + * idx 1 for the prior row — the bug's asymmetry catch. */ + { "slice_u8_then_u8", + "fn pick(n: ([]u8 | u8)) i32 = {\n" + " match (n) {\n" + " case let s: []u8 => return 100i32 + (s.len: i32) + (s[0]: i32);\n" + " case let c: u8 => return c: i32;\n" + " };\n" + " return -1;\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [3]u8;\n" + " buf[0] = 7u8; buf[1] = 8u8; buf[2] = 9u8;\n" + " if (pick(buf[0:3]) != 110) { return 11; };\n" + " if (pick(42u8) != 42) { return 12; };\n" + " return 0;\n" + "};\n", + 0 }, + /* (i8 | []i8) — other signed-narrow primitive. */ + { "i8_slice_i8", + "fn pick(n: (i8 | []i8)) i32 = {\n" + " match (n) {\n" + " case let c: i8 => return c: i32;\n" + " case let s: []i8 => return 100i32 + (s.len: i32) + (s[0]: i32);\n" + " };\n" + " return -1;\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [2]i8;\n" + " buf[0] = 5i8; buf[1] = 6i8;\n" + " if (pick(11i8) != 11) { return 11; };\n" + " if (pick(buf[0:2]) != 107) { return 12; };\n" + " return 0;\n" + "};\n", + 0 }, + /* (i32 | []i32) — wider scalar payload arm. */ + { "i32_slice_i32", + "fn pick(n: (i32 | []i32)) i32 = {\n" + " match (n) {\n" + " case let c: i32 => return c;\n" + " case let s: []i32 => return 1000i32 + (s.len: i32) + s[0];\n" + " };\n" + " return -1;\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [2]i32;\n" + " buf[0] = 33i32; buf[1] = 44i32;\n" + " if (pick(77i32) != 77) { return 11; };\n" + " if (pick(buf[0:2]) != 1035) { return 12; };\n" + " return 0;\n" + "};\n", + 0 }, + /* (u64 | []u64) — 8-byte scalar arm; the scalar tag at idx 0 + * and the slice payload words must not collide. */ + { "u64_slice_u64", + "fn pick(n: (u64 | []u64)) i32 = {\n" + " match (n) {\n" + " case let c: u64 => return c: i32;\n" + " case let s: []u64 => return 1000i32 + (s.len: i32) + (s[0]: i32);\n" + " };\n" + " return -1;\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [2]u64;\n" + " buf[0] = 41u64; buf[1] = 42u64;\n" + " if (pick(99u64) != 99) { return 11; };\n" + " if (pick(buf[0:2]) != 1043) { return 12; };\n" + " return 0;\n" + "};\n", + 0 }, + /* (rune | []rune) — Hare's iterator-pair shape (a rune or a + * slice of runes). rune is u32-wide, []rune is 24B. */ + { "rune_slice_rune", + "fn pick(n: (rune | []rune)) i32 = {\n" + " match (n) {\n" + " case let r: rune => return r: i32;\n" + " case let s: []rune => return 1000i32 + (s.len: i32) + (s[0]: i32);\n" + " };\n" + " return -1;\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [2]rune;\n" + " buf[0] = 'a'; buf[1] = 'b';\n" + " if (pick('Z') != 90) { return 11; };\n" + " if (pick(buf[0:2]) != 1099) { return 12; };\n" + " return 0;\n" + "};\n", + 0 }, + /* Three-arm (u8 | []u8 | str): scalar at idx 0, slice at idx 1, + * str at idx 2. Verifies the slice axis composes with the + * pre-existing str shape pass — no regression on str arm. */ + { "u8_slice_u8_str", + "fn pick(n: (u8 | []u8 | str)) i32 = {\n" + " match (n) {\n" + " case let c: u8 => return c: i32;\n" + " case let s: []u8 => return 200i32 + (s.len: i32) + (s[0]: i32);\n" + " case let t: str => return 300i32 + (t.len: i32);\n" + " };\n" + " return -1;\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [3]u8;\n" + " buf[0] = 1u8; buf[1] = 2u8; buf[2] = 3u8;\n" + // Three-arm 32B-slot widening through chained if-call sites + // trips a separate sequential-push bug in both stages; bind + // each call to a let before checking to keep the row focused + // on variant-tag selection. + " let a: i32 = pick(5u8);\n" + " let b: i32 = pick(buf[0:3]);\n" + " let c: i32 = pick(\"hi\");\n" + " if (a != 5) { return 11; };\n" + " if (b != 204) { return 12; };\n" + " if (c != 302) { return 13; };\n" + " return 0;\n" + "};\n", + 0 }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[96], tmpdir[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/msv_run_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/msv_run_%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[160]; + 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; +} + +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_slice_variant_run: 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_slice_variant_run[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (fail) { + fprintf(stderr, + "match_slice_variant_run: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("match_slice_variant_run: %d/%d ok\n", total, total); + return 0; +}