diff --git a/Makefile b/Makefile index 2b4f2ffa..6a0bc086 100644 --- a/Makefile +++ b/Makefile @@ -317,6 +317,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_amp_fn_ident \ $(BIN)/test_star_fn_deref \ $(BIN)/test_star_fn_deref_call \ + $(BIN)/test_match_variant_dispatch \ $(BIN)/test_use_promote_alias \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ @@ -625,6 +626,12 @@ $(BIN)/test_star_fn_deref_call: test/wcc/766_star_fn_deref_call.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_match_variant_dispatch: test/wcc/767_match_variant_dispatch.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_arrlit_str_full: test/wcc/711_arrlit_str_full.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 7441f933..c68fb7d8 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -19069,16 +19069,22 @@ fn cgmatch(c: *cgen, n: *node) void = { // rather than the resolved N_TTAGGED node. if (istaggedtype(c, scrutt)) { let r: i32 = -1; - if (pat.kind == nkind.N_TNAME) { - r = flatvariantidx(c, scrutt, pat); - } 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); - }; }; + let pattype: *tinfo = pat.type_: *tinfo; + if (pattype != nil) { + // #179: kind-agnostic dispatch on the resolved + // tinfo. Cstage cg_tag_for_variant works on + // Type, so N_TPTR / N_TFN / N_TPTR(N_TFN) case- + // patterns all reach typeeq; the prior pat.kind + // gate dropped them to r=-1 → tag 0 collapse. + // Slice arm still goes through flatslicevariantidx + // (task #19 untyped-elem fallback when typeeq + // can't match a (scalar | []T) shape). + if (typeisslice(pattype)) { + r = flatslicevariantidx(c, scrutt, pat.lhs); + } else { + r = flatvariantidxt(scrutt.type_: *tinfo, pattype); + }; + }; if (r >= 0) { want = r; }; }; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 9fdc650d..d80a83c4 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1509,16 +1509,22 @@ fn cgmatch(c: *cgen, n: *node) void = { // rather than the resolved N_TTAGGED node. if (istaggedtype(c, scrutt)) { let r: i32 = -1; - if (pat.kind == nkind.N_TNAME) { - r = flatvariantidx(c, scrutt, pat); - } 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); - }; }; + let pattype: *tinfo = pat.type_: *tinfo; + if (pattype != nil) { + // #179: kind-agnostic dispatch on the resolved + // tinfo. Cstage cg_tag_for_variant works on + // Type, so N_TPTR / N_TFN / N_TPTR(N_TFN) case- + // patterns all reach typeeq; the prior pat.kind + // gate dropped them to r=-1 → tag 0 collapse. + // Slice arm still goes through flatslicevariantidx + // (task #19 untyped-elem fallback when typeeq + // can't match a (scalar | []T) shape). + if (typeisslice(pattype)) { + r = flatslicevariantidx(c, scrutt, pat.lhs); + } else { + r = flatvariantidxt(scrutt.type_: *tinfo, pattype); + }; + }; if (r >= 0) { want = r; }; }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 6dc73499..b7ed8548 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -19069,16 +19069,22 @@ fn cgmatch(c: *cgen, n: *node) void = { // rather than the resolved N_TTAGGED node. if (istaggedtype(c, scrutt)) { let r: i32 = -1; - if (pat.kind == nkind.N_TNAME) { - r = flatvariantidx(c, scrutt, pat); - } 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); - }; }; + let pattype: *tinfo = pat.type_: *tinfo; + if (pattype != nil) { + // #179: kind-agnostic dispatch on the resolved + // tinfo. Cstage cg_tag_for_variant works on + // Type, so N_TPTR / N_TFN / N_TPTR(N_TFN) case- + // patterns all reach typeeq; the prior pat.kind + // gate dropped them to r=-1 → tag 0 collapse. + // Slice arm still goes through flatslicevariantidx + // (task #19 untyped-elem fallback when typeeq + // can't match a (scalar | []T) shape). + if (typeisslice(pattype)) { + r = flatslicevariantidx(c, scrutt, pat.lhs); + } else { + r = flatvariantidxt(scrutt.type_: *tinfo, pattype); + }; + }; if (r >= 0) { want = r; }; }; }; diff --git a/test/wcc/767_match_variant_dispatch.c b/test/wcc/767_match_variant_dispatch.c new file mode 100644 index 00000000..93a3974f --- /dev/null +++ b/test/wcc/767_match_variant_dispatch.c @@ -0,0 +1,337 @@ +/* + * 767_match_variant_dispatch — project #179: wwstage cgmatch's + * non-nullable arm gated its flat-tag dispatch on `pat.kind == + * N_TNAME` (with an `N_TSLICE` else-branch for task #19's untyped + * elem fallback). N_TPTR / N_TFN / N_TPTR(N_TFN) case-patterns fell + * through both arms, leaving the local `r` at -1 → `want = 0`, so + * every variant past 0 silently collapsed to tag 0 — the kind of + * miscompile that runtime-passes only when the value happens to be + * variant 0 (zero-coincidence). + * + * Cstage cg_tag_for_variant (cmd/w6c/cgen.c:624) walks Type directly + * and is kind-agnostic; it has never had this gap. Drew's settled + * principle: match dispatch is resolved-type-only (harec + * check.c:2527 stores `_case->type = ctype`). Project #66 Phase-N + * already flipped the typeeq compare; #179 was the one site that + * still keyed on AST kind. Memory: project_tinfo_lossy_nominal. + * + * Fix: selfhost/cmd/wcc/cgenexpr.ww cgmatch non-nullable arm — + * route through `flatvariantidxt(scrutt.type_, pat.type_)` directly, + * guarded only by `istaggedtype(scrutt)` + `typeisslice(pattype)` + * for the slice axis. No new helper; existing flatvariantidxt / + * flatslicevariantidx wire up unchanged. + * + * Coverage (5 rows): + * 1. nullable_ptr_fn — `(*fn(i32) i32 | void)`, branched + * store (#105 lesson: two distinct + * fns picked at runtime), match via + * indirect call. KEN'S VERIFY GATE: + * row 1 byte-id proves wwstage's + * nullable-fold ordering matches + * cstage cg_tag_for_variant — fail + * here flags a separate-signoff fold. + * Goes through the nullable disc path + * (line 1484-1502, untouched by #179). + * 2. ptr_variants_past_zero — `(*i32 | *i64)` storing &i64. + * Pre-fix wwstage emits CMPQ $0 for + * the *i64 arm; post-fix CMPQ $1. + * 3. fn_ptr_variants_via_local — `(*fn(i32) i32 | *fn(i64) i64)` + * built from a local. Direct N_TPTR + * over N_TFN case-pattern is the new- + * shape exercise; store path goes via + * ident (not `&fn` inline) to dodge + * the fn-variant store-side sibling + * bug (see below). + * 4. stored_variant1_roundtrip — `(*i32 | *i64)` with branched + * runtime variant choice (pick = 1 + * forces variant 1; defeats const- + * fold to variant 0). Mirrors #105. + * 5. aliased_ptr_variants — `type pa = *i32; type pb = *i64; + * (pa | pb)`. typeeq through TY_NAMED + * wrappers — the alias-aware path that + * the surface-name compare couldn't + * key on. OR-pattern was the original + * row 5, dropped because the wwstage + * parser doesn't accept `case T1 | T2`. + * + * Per-row gates: cstage runtime exit, wwstage runtime exit, + * cs.s == ww.s byte-identical (rule-10 stage symmetry). + * + * Sibling bug surfaced (not fixed here, filed inline): widening + * `&fn` INLINE into a `(*fn(...) | *fn(...))` slot computes the + * wrong tag in wwstage — taggedvariantindext / typeeq on the + * synthetic fn-ptr tinfo built from N_UN TK_AMP IDENT-of-FN doesn't + * match the variant tinfo, so tag collapses to 0. Storing via an + * intermediate ident-typed local (row 3 shape) routes through a + * different store path and tags correctly. cstage cg_widen_tagged_ + * store hits the same node with the same Type and tags correctly, + * so the divergence is wwstage-only on the inline-amp shape. Out + * of scope for #179 (which is the match-DISPATCH side, not the + * widen-STORE side). + * + * GATE POLARITY: must stay GREEN. A red here means the cgmatch + * non-nullable dispatch regressed, the nullable arm got perturbed, + * or stage symmetry drifted on the new typeeq route. + */ +#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; +} + +#define STAGE_CS 1 +#define STAGE_WW 2 + +struct row { + const char *label; + const char *src; + int expected_exit; + int stage_mask; +}; + +static const struct row rows[] = { + /* KEN'S VERIFY GATE: nullable disc path must byte-id even + * though #179 didn't touch it. A divergence here flags a + * separate flatvariantidxt nullable-fold ordering issue. */ + { "nullable_ptr_fn", + "fn ai(x: i32) i32 = { return x + 1; };\n" + "fn bi(x: i32) i32 = { return x + 100; };\n" + "fn indi(f: *fn(x: i32) i32, x: i32) i32 = { return (*f)(x); };\n" + "export fn main() i32 = {\n" + " let pick: i32 = 1;\n" + " let v: (*fn(x: i32) i32 | void) = &ai;\n" + " if (pick != 0) { v = &bi; };\n" + " let r: i32 = match (v) {\n" + " case void => yield 99: i32;\n" + " case let f: *fn(x: i32) i32 => yield indi(f, 7);\n" + " };\n" + " return r;\n" + "};\n", + 107, + STAGE_CS | STAGE_WW }, + { "ptr_variants_past_zero", + "export fn main() i32 = {\n" + " let a: i64 = 42;\n" + " let v: (*i32 | *i64) = &a;\n" + " let r: i32 = match (v) {\n" + " case *i32 => yield 11: i32;\n" + " case *i64 => yield 22: i32;\n" + " };\n" + " return r;\n" + "};\n", + 22, + STAGE_CS | STAGE_WW }, + { "fn_ptr_variants_via_local", + "fn ai(x: i32) i32 = { return x + 1; };\n" + "fn bi(x: i64) i64 = { return x + 1; };\n" + "fn indl(f: *fn(x: i64) i64, x: i64) i64 = { return (*f)(x); };\n" + "export fn main() i32 = {\n" + " let g: *fn(x: i64) i64 = &bi;\n" + " let v: (*fn(x: i32) i32 | *fn(x: i64) i64) = g;\n" + " let r: i32 = match (v) {\n" + " case let p: *fn(x: i32) i32 => yield 1: i32;\n" + " case let q: *fn(x: i64) i64 => yield indl(q, 9): i32;\n" + " };\n" + " return r;\n" + "};\n", + 10, + STAGE_CS | STAGE_WW }, + { "stored_variant1_roundtrip", + "export fn main() i32 = {\n" + " let a: i32 = 5;\n" + " let b: i64 = 9;\n" + " let pick: i32 = 1;\n" + " let v: (*i32 | *i64) = &a;\n" + " if (pick != 0) { v = &b; };\n" + " let r: i32 = match (v) {\n" + " case *i32 => yield 11: i32;\n" + " case *i64 => yield 22: i32;\n" + " };\n" + " return r;\n" + "};\n", + 22, + STAGE_CS | STAGE_WW }, + { "aliased_ptr_variants", + "type pa = *i32;\n" + "type pb = *i64;\n" + "export fn main() i32 = {\n" + " let a: i64 = 100;\n" + " let g: pb = &a;\n" + " let v: (pa | pb) = g;\n" + " let r: i32 = match (v) {\n" + " case pa => yield 11: i32;\n" + " case pb => yield 22: i32;\n" + " };\n" + " return r;\n" + "};\n", + 22, + STAGE_CS | STAGE_WW }, +}; + +static int +write_source(const char *src_path, const char *src) +{ + FILE *f = fopen(src_path, "wb"); + if (!f) return -1; + fputs(src, f); + fclose(f); + return 0; +} + +static void +cleanup_tmp(const char *tmpdir, const char *base) +{ + char p[512]; + snprintf(p, sizeof p, "%s/%s.ww", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s.s", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s.o", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s.combined.ww", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s", tmpdir, base); unlink(p); + rmdir(tmpdir); +} + +static int +build_via_driver(const char *driver, const char *tmpdir, const char *src) +{ + char cmd[1024]; + snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %s build %s 2>/dev/null", + tmpdir, driver, src); + return runwait(cmd); +} + +static int +run_row(const char *driver, const struct row *r, int seq) +{ + char tmpdir[256], src[256], base[64], outbin[512]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/mvd_%d_d_%d", getpid(), seq); + snprintf(src, sizeof src, "%s/main767.ww", tmpdir); + snprintf(base, sizeof base, "main767"); + mkdir(tmpdir, 0755); + if (write_source(src, r->src) != 0) { + cleanup_tmp(tmpdir, base); + return -1; + } + int rc = -1; + if (build_via_driver(driver, tmpdir, src) == 0) { + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + rc = runwait(outbin); + } + cleanup_tmp(tmpdir, base); + return rc; +} + +/* asm_byte_identical — diff cstage vs wwstage .s. Parallel trees so + * ww_ww writing intermediates next to the source doesn't clobber the + * cstage .s (filed bug per CLAUDE.md rule 14 phase split). */ +static int +asm_byte_identical(const char *cdrv, const char *wdrv, + const struct row *r, int seq) +{ + char src[256], tdc[256], tdw[256], base[64], cs[512], ws[512]; + snprintf(tdc, sizeof tdc, "/tmp/mvd_%d_c_%d", getpid(), seq); + snprintf(tdw, sizeof tdw, "/tmp/mvd_%d_w_%d", getpid(), seq); + snprintf(base, sizeof base, "main767"); + mkdir(tdc, 0755); + mkdir(tdw, 0755); + snprintf(src, sizeof src, "%s/main767.ww", tdc); + if (write_source(src, r->src) != 0) { cleanup_tmp(tdc, base); cleanup_tmp(tdw, base); return -1; } + int rc = -1; + if (build_via_driver(cdrv, tdc, src) != 0) goto out; + snprintf(cs, sizeof cs, "%s/%s.s", tdc, base); + + snprintf(src, sizeof src, "%s/main767.ww", tdw); + if (write_source(src, r->src) != 0) goto out; + if (build_via_driver(wdrv, tdw, src) != 0) goto out; + snprintf(ws, sizeof ws, "%s/%s.s", tdw, base); + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + if (fc && fw) { + rc = 0; + 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); +out: + cleanup_tmp(tdc, base); + cleanup_tmp(tdw, base); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[1024], wdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + int wwpresent = (access(wdrv, X_OK) == 0); + int seq = 0; + + for (int i = 0; i < n; i++) { + if (rows[i].stage_mask & STAGE_CS) { + total++; + int got = run_row(cdrv, &rows[i], seq++); + if (got != rows[i].expected_exit) { + fprintf(stderr, + "match_variant_dispatch[cstage run][%s]: exit=%d want=%d\n", + rows[i].label, got, rows[i].expected_exit); + fail++; + } + } + if (wwpresent && (rows[i].stage_mask & STAGE_WW)) { + total++; + int got = run_row(wdrv, &rows[i], seq++); + if (got != rows[i].expected_exit) { + fprintf(stderr, + "match_variant_dispatch[wwstage run][%s]: exit=%d want=%d\n", + rows[i].label, got, rows[i].expected_exit); + fail++; + } + total++; + if (asm_byte_identical(cdrv, wdrv, &rows[i], seq++) != 0) { + fprintf(stderr, + "match_variant_dispatch[byte-id][%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + } + } + + if (!wwpresent) + fprintf(stderr, "match_variant_dispatch: skip wwstage (no %s)\n", wdrv); + + if (fail) { + fprintf(stderr, "match_variant_dispatch: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("match_variant_dispatch: %d/%d ok\n", total, total); + return 0; +}