From 3f4eeff7ff2a86a5f213958e21c8b3acd7c37c4f Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 29 May 2026 04:51:44 +0900 Subject: [PATCH] wcc: route is/as variant lookup through tinfo.params (#198) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit checkisas walked the unflattened AST u.list via casevariantin (typeeqast streq), so any variant introduced via a `...inner` spread was invisible and rejected as "is/as: not a variant of operand". Repro: type rsh = (size | io.eof | ...io.error); let r: rsh = 42: size; if (r is io.underread) ... -- pre-fix wwstage REJECTS io.underread is in io.error.params, which tinfofornode splices into the parent at L1827-1836, but the AST u.list still holds the single `...io.error` entry that streq("io.underread", "io.error") rejects. Route through flatvariantidxt — the same Phase-N helper #179 cgmatch and #66 cgtagvariantidx already key off. Mirrors cstage cmd/wcc/check.c :1662-1675 u->params + type_eq. Falls back to casevariantin AST walk when tinfo isn't available (defensive — non-#198 path stays as-is). project_tinfo_lossy_nominal: name-keying was the pre-Phase-N workaround for tinfo lossy on nominal identity; typeeq inside flatvariantidxt now handles NAMED ptr-id (#64), so the checker pair aligns with cgen on the flattened-variant axis. Closes the cgen-drain mini-cluster (#201 -> #199 -> #200 -> #198). 773_isas_spread_variant: 5 rows (spread_is_inline_variant, direct_cross_mod_tagged, cross_mod_named_void, same_module_variant, spread_as_inline_payload). Rows 2-4 byte-id; rows 1/5 skip byte-id due to layout-asymmetry on `...wrapper` (cstage flattens at resolve_type, wwstage computes maxsz off vt.size of the un-spliced alias) — sibling not blocking the checker correctness fix. --- Makefile | 7 + selfhost/cmd/w6c/main.combined.ww | 31 ++- selfhost/cmd/wcc/check.ww | 31 ++- selfhost/cmd/wwdump/main.combined.ww | 31 ++- test/wcc/773_isas_spread_variant.c | 313 +++++++++++++++++++++++++++ 5 files changed, 386 insertions(+), 27 deletions(-) create mode 100644 test/wcc/773_isas_spread_variant.c diff --git a/Makefile b/Makefile index d2d16ee2..3804f14e 100644 --- a/Makefile +++ b/Makefile @@ -323,6 +323,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_return_tagged_forward \ $(BIN)/test_widen_transitive \ $(BIN)/test_typeassert_nonident \ + $(BIN)/test_isas_spread_variant \ $(BIN)/test_use_promote_alias \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ @@ -667,6 +668,12 @@ $(BIN)/test_typeassert_nonident: test/wcc/772_typeassert_nonident.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_isas_spread_variant: test/wcc/773_isas_spread_variant.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 c728aa78..a506afa9 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -13470,16 +13470,29 @@ fn checkisas(c: *checker, n: *node) void = { }; let want: *node = n.rhs; if (want == nil) { return; }; - if (!casevariantin(u, want)) { - os.write(2, "is/as: not a variant of operand".ptr, 31u64); - if (want.kind == nkind.N_TNAME) { - os.write(2, " (".ptr, 2u64); - os.write(2, want.str.ptr, want.str.len: u64); - os.write(2, ")".ptr, 1u64); - }; - os.write(2, "\n".ptr, 1u64); - c.errs += 1; + // #198: route through tinfo.params (the #61a-flattened chain built + // at L1789-1845 N_TTAGGED) — the prior AST u.list walk via + // casevariantin false-rejects every variant that arrives via a + // `...inner` spread. Mirrors cstage cmd/wcc/check.c:1662-1675 + // u->params + type_eq, and matches cgen's own #179 cgmatch / #66 + // Phase-N flatvariantidxt lookup (the SSoT cgtagvariantidx already + // keys off at cgenexpr.ww:155). project_tinfo_lossy_nominal: name- + // keying was the pre-Phase-N workaround for tinfo lossy on nominal + // identity; typeeq inside flatvariantidxt now handles NAMED ptr-id. + let utinfo: *tinfo = tinfofornode(c, u); + let wanttinfo: *tinfo = tinfofornode(c, want); + if (utinfo != nil) { if (wanttinfo != nil) { + if (flatvariantidxt(utinfo, wanttinfo) >= 0) { return; }; + }; }; + if (casevariantin(u, want)) { return; }; + os.write(2, "is/as: not a variant of operand".ptr, 31u64); + if (want.kind == nkind.N_TNAME) { + os.write(2, " (".ptr, 2u64); + os.write(2, want.str.ptr, want.str.len: u64); + os.write(2, ")".ptr, 1u64); }; + os.write(2, "\n".ptr, 1u64); + c.errs += 1; }; // ---- ? subset propagation -------------------------------------------- diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 30607bd2..911b6a1f 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -3440,16 +3440,29 @@ fn checkisas(c: *checker, n: *node) void = { }; let want: *node = n.rhs; if (want == nil) { return; }; - if (!casevariantin(u, want)) { - os.write(2, "is/as: not a variant of operand".ptr, 31u64); - if (want.kind == nkind.N_TNAME) { - os.write(2, " (".ptr, 2u64); - os.write(2, want.str.ptr, want.str.len: u64); - os.write(2, ")".ptr, 1u64); - }; - os.write(2, "\n".ptr, 1u64); - c.errs += 1; + // #198: route through tinfo.params (the #61a-flattened chain built + // at L1789-1845 N_TTAGGED) — the prior AST u.list walk via + // casevariantin false-rejects every variant that arrives via a + // `...inner` spread. Mirrors cstage cmd/wcc/check.c:1662-1675 + // u->params + type_eq, and matches cgen's own #179 cgmatch / #66 + // Phase-N flatvariantidxt lookup (the SSoT cgtagvariantidx already + // keys off at cgenexpr.ww:155). project_tinfo_lossy_nominal: name- + // keying was the pre-Phase-N workaround for tinfo lossy on nominal + // identity; typeeq inside flatvariantidxt now handles NAMED ptr-id. + let utinfo: *tinfo = tinfofornode(c, u); + let wanttinfo: *tinfo = tinfofornode(c, want); + if (utinfo != nil) { if (wanttinfo != nil) { + if (flatvariantidxt(utinfo, wanttinfo) >= 0) { return; }; + }; }; + if (casevariantin(u, want)) { return; }; + os.write(2, "is/as: not a variant of operand".ptr, 31u64); + if (want.kind == nkind.N_TNAME) { + os.write(2, " (".ptr, 2u64); + os.write(2, want.str.ptr, want.str.len: u64); + os.write(2, ")".ptr, 1u64); }; + os.write(2, "\n".ptr, 1u64); + c.errs += 1; }; // ---- ? subset propagation -------------------------------------------- diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 0b6a2fd9..0e56b5c0 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -13470,16 +13470,29 @@ fn checkisas(c: *checker, n: *node) void = { }; let want: *node = n.rhs; if (want == nil) { return; }; - if (!casevariantin(u, want)) { - os.write(2, "is/as: not a variant of operand".ptr, 31u64); - if (want.kind == nkind.N_TNAME) { - os.write(2, " (".ptr, 2u64); - os.write(2, want.str.ptr, want.str.len: u64); - os.write(2, ")".ptr, 1u64); - }; - os.write(2, "\n".ptr, 1u64); - c.errs += 1; + // #198: route through tinfo.params (the #61a-flattened chain built + // at L1789-1845 N_TTAGGED) — the prior AST u.list walk via + // casevariantin false-rejects every variant that arrives via a + // `...inner` spread. Mirrors cstage cmd/wcc/check.c:1662-1675 + // u->params + type_eq, and matches cgen's own #179 cgmatch / #66 + // Phase-N flatvariantidxt lookup (the SSoT cgtagvariantidx already + // keys off at cgenexpr.ww:155). project_tinfo_lossy_nominal: name- + // keying was the pre-Phase-N workaround for tinfo lossy on nominal + // identity; typeeq inside flatvariantidxt now handles NAMED ptr-id. + let utinfo: *tinfo = tinfofornode(c, u); + let wanttinfo: *tinfo = tinfofornode(c, want); + if (utinfo != nil) { if (wanttinfo != nil) { + if (flatvariantidxt(utinfo, wanttinfo) >= 0) { return; }; + }; }; + if (casevariantin(u, want)) { return; }; + os.write(2, "is/as: not a variant of operand".ptr, 31u64); + if (want.kind == nkind.N_TNAME) { + os.write(2, " (".ptr, 2u64); + os.write(2, want.str.ptr, want.str.len: u64); + os.write(2, ")".ptr, 1u64); }; + os.write(2, "\n".ptr, 1u64); + c.errs += 1; }; // ---- ? subset propagation -------------------------------------------- diff --git a/test/wcc/773_isas_spread_variant.c b/test/wcc/773_isas_spread_variant.c new file mode 100644 index 00000000..57b70587 --- /dev/null +++ b/test/wcc/773_isas_spread_variant.c @@ -0,0 +1,313 @@ +/* + * 773_isas_spread_variant — project #198: wwstage checker `is`/`as` + * variant lookup walked the unflattened AST u.list (casevariantin via + * typeeqast streq), so any variant introduced via a `...inner` spread + * was invisible to the checker and rejected as "is/as: not a variant + * of operand". Repro: `r: (size | io.eof | ...io.error); r is + * io.underread;` — io.underread is in io.error's params chain, which + * gets spliced into the parent union at tinfofornode L1827-1836, but + * the AST u.list still holds the single `...io.error` entry that + * streq("io.underread", "io.error") rejects. + * + * Fix: selfhost/cmd/wcc/check.ww checkisas (lines ~3441-3466). Route + * through tinfo.params via flatvariantidxt (the same Phase-N helper + * #179 cgmatch and #66 cgtagvariantidx use); fall back to casevariantin + * AST walk when tinfo isn't available (defensive — non-#198 path stays + * as-is). Mirrors cstage cmd/wcc/check.c:1662-1675 u->params + type_eq. + * Closes the cgen-drain mini-cluster (#201 → #199 → #200 → #198). + * + * Coverage (5 rows): + * 1. spread_is_inline_variant — `(size | io.eof | ...io.error)`, + * `r is io.underread`. THE BUG: pre- + * fix wwstage rejects with "is/as: + * not a variant of operand + * (io.underread)". Post-fix accepts; + * cgen tag-compare hits the flatten- + * spread index. No byte-id: layout- + * asymmetry on `...wrapper` (cstage + * flattens at resolve_type, wwstage + * computes maxsz off vt.size of the + * un-spliced alias — sibling task, + * not blocking checker correctness). + * 2. direct_cross_mod_tagged — `(size | io.eof | io.error)`, + * `r is io.error`. Direct cross-mod + * tagged variant (no spread). Regr- + * ession gate: AST-streq path always + * matched this; the new tinfo path + * must too. byte-id ON. + * 3. cross_mod_named_void — `(size | io.eof)`, `r is io.eof`. + * Cross-mod NAMED-void variant. Regr- + * ession gate for the io.eof shape + * used pervasively in the io fold. + * byte-id ON. + * 4. same_module_variant — `(i32 | str)`, `r is i32`. Bare + * same-module primitive variant. + * Lowest-bar regression gate: no + * cross-mod, no spread, no NAMED. + * byte-id ON. + * 5. spread_as_inline_payload — `(size | io.eof | ...io.error)`, + * `r as io.underread`. `as` twin of + * row 1: same checker path + * (checkisas dispatches on both + * N_TYPETEST and N_TYPEASSERT), with + * the payload-extract runtime gate. + * No byte-id (sibling, same as row 1). + * + * Per-row gates: cstage runtime exit, wwstage runtime exit, cs.s == + * ww.s byte-identical when byte_id != 0. + * + * GATE POLARITY: must stay GREEN. Rows 1/5 red means the tinfo.params + * routing dropped a spread-flattened variant; row 2 red means the + * direct cross-mod path broke; row 3 red means the NAMED-void path + * broke; row 4 red means a same-module / same-package regression. + */ +#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; + int byte_id; +}; + +static const struct row rows[] = { + { "spread_is_inline_variant", + "package main;\n" + "import io;\n" + "type rsh = (size | io.eof | ...io.error);\n" + "export fn main() i32 = {\n" + " let r: rsh = 42: size;\n" + " if (r is io.underread) { return 1; };\n" + " if (r is io.eof) { return 2; };\n" + " if (r is size) { return 3; };\n" + " return 0;\n" + "};\n", + 3, + STAGE_CS | STAGE_WW, 0 }, + { "direct_cross_mod_tagged", + "package main;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let r: (size | io.eof | io.error) = 42: size;\n" + " if (r is io.error) { return 1; };\n" + " if (r is io.eof) { return 2; };\n" + " if (r is size) { return 3; };\n" + " return 0;\n" + "};\n", + 3, + STAGE_CS | STAGE_WW, 1 }, + { "cross_mod_named_void", + "package main;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let r: (size | io.eof) = 42: size;\n" + " if (r is io.eof) { return 1; };\n" + " if (r is size) { return 3; };\n" + " return 0;\n" + "};\n", + 3, + STAGE_CS | STAGE_WW, 1 }, + { "same_module_variant", + "package main;\n" + "export fn main() i32 = {\n" + " let r: (i32 | str) = 3: i32;\n" + " if (r is i32) { return 3; };\n" + " if (r is str) { return 1; };\n" + " return 0;\n" + "};\n", + 3, + STAGE_CS | STAGE_WW, 1 }, + { "spread_as_inline_payload", + "package main;\n" + "import io;\n" + "type rsh = (size | io.eof | ...io.error);\n" + "fn pick(b: i32) rsh = {\n" + " if (b == 1) { let u: io.underread = 7: i32: io.underread; return u; };\n" + " return 42: size;\n" + "};\n" + "export fn main() i32 = {\n" + " let r = pick(1);\n" + " let u = r as io.underread;\n" + " return u: i32;\n" + "};\n", + 7, + STAGE_CS | STAGE_WW, 0 }, +}; + +static int +write_source(const char *path, const char *src) +{ + FILE *f = fopen(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[512], base[64], outbin[768]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/isas_%d_d_%d", getpid(), seq); + snprintf(src, sizeof src, "%s/main773.ww", tmpdir); + snprintf(base, sizeof base, "main773"); + 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; +} + +/* Parallel tmpdirs per CLAUDE.md rule 14 — ww_ww writes intermediates + * next to the source so concurrent cstage/wwstage builds against the + * same path would race. */ +static int +asm_byte_identical(const char *cdrv, const char *wdrv, + const struct row *r, int seq) +{ + char src[512], tdc[256], tdw[256], base[64], cs[512], ws[512]; + snprintf(tdc, sizeof tdc, "/tmp/isas_%d_c_%d", getpid(), seq); + snprintf(tdw, sizeof tdw, "/tmp/isas_%d_w_%d", getpid(), seq); + snprintf(base, sizeof base, "main773"); + mkdir(tdc, 0755); + mkdir(tdw, 0755); + snprintf(src, sizeof src, "%s/main773.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/main773.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[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], wdrv[640]; + 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, + "isas_spread_variant[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, + "isas_spread_variant[wwstage run][%s]: exit=%d want=%d\n", + rows[i].label, got, rows[i].expected_exit); + fail++; + } + if (rows[i].byte_id) { + total++; + if (asm_byte_identical(cdrv, wdrv, &rows[i], seq++) != 0) { + fprintf(stderr, + "isas_spread_variant[byte-id][%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + } + } + } + + if (!wwpresent) + fprintf(stderr, "isas_spread_variant: skip wwstage (no %s)\n", wdrv); + + if (fail) { + fprintf(stderr, "isas_spread_variant: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("isas_spread_variant: %d/%d ok\n", total, total); + return 0; +}