From 487cf91f126585a40595e0a0f68a6ed3c9a48875 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 29 May 2026 06:18:27 +0900 Subject: [PATCH] =?UTF-8?q?wcc:=20accept=20NAMED-variant=20nominal=20match?= =?UTF-8?q?=20at=20tagged=E2=86=92tagged=20subset=20(#205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tagged→tagged subset arm walked src's leaves against dst's flat variant list, so `let r: (size | eof | wrapper) = e` with e: wrapper REJECTED at cstage's checker — wrapper's leaves (unsupported, underread, nomem) aren't direct variants of dst. Wwstage's permissive tail accepted silently but cgen then miscompiled the tag (#199b layout- extension family, deferred). Mirror the concrete→tagged fix from #199 (α) at type.c:316: when src is a NAMED-tagged wrapper and dst has a direct NAMED-tagged variant equal to src, accept by nominal identity BEFORE the subset loop. Wwstage's isassignable mirrors the structural insertion before the existing `*confident = false; return true;` tail (deferred-tightening per #202). SSoT with `is`/`as` non-recursive variant lookup (#198 family). Cgen's tag-remap for the wrapper-as-whole case still maps src variants to dst tag 0 — the wrapped-slot layout for `dst.tag = variant_idx, dst.payload = src` is #199b future-work. Probe verifies checker-accept + runtime exit-clean only; does NOT inspect the resulting variant tag. Probe 774_tagged_widen_named_variant.c covers 5 rows: bug-repro, nested-wrapper, pure-leaf subset (regression), concrete-unrelated rejection (gate), branched callee. Two sibling cgen/checker bugs surfaced (wwstage cgwidentaggedstorebp sszkind == TY_TAGGED && su && su->kind == TY_TAGGED) { + /* #205: NAMED-variant nominal compare BEFORE subset. + * Mirror of the concrete→tagged arm at :316-324 (#199 α). + * When src is a NAMED-tagged wrapper and dst has a direct + * NAMED-tagged variant equal to src, accept by nominal + * identity without recursing into src's variants — those + * are wrapper's leaves, not direct variants of dst, so + * the subset loop below would reject. SSoT with `is`/`as` + * variant lookup (#198 family). */ + for (Tparam *dp = du->params; dp; dp = dp->next) { + Type *pu = (dp->type && dp->type->kind == TY_NAMED) + ? dp->type->under : dp->type; + if (pu && pu->kind == TY_TAGGED && + type_eq(dp->type, src)) return 1; + } for (Tparam *sp = su->params; sp; sp = sp->next) { int ok = 0; for (Tparam *dp = du->params; dp; dp = dp->next) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index a506afa9..81d99bfe 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -13115,6 +13115,24 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { // return through another fn with the same shape but possibly // a different surface spelling. if (du.kind == nkind.N_TTAGGED && su.kind == nkind.N_TTAGGED) { + // #205: NAMED-variant nominal compare BEFORE permissive + // fallthrough. Mirror of cstage type.c type_assignable + // tagged→tagged arm (#199 α concrete→tagged sibling). + // When src is a NAMED-tagged wrapper and dst has a direct + // NAMED-tagged variant equal to src, accept by nominal + // identity — wrapper's leaves are NOT direct variants of + // dst, so a structural subset walk would reject. SSoT + // with `is`/`as` variant lookup (#198 family). + let v: *node = du.list; + for (v != nil) { + let vu: *node = resolvealias(c, unwrapbang(v)); + if (vu != nil) { + if (vu.kind == nkind.N_TTAGGED) { + if (typeeqast(v, src)) { return true; }; + }; + }; + v = v.next; + }; *confident = false; return true; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 911b6a1f..579600d2 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -3085,6 +3085,24 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { // return through another fn with the same shape but possibly // a different surface spelling. if (du.kind == nkind.N_TTAGGED && su.kind == nkind.N_TTAGGED) { + // #205: NAMED-variant nominal compare BEFORE permissive + // fallthrough. Mirror of cstage type.c type_assignable + // tagged→tagged arm (#199 α concrete→tagged sibling). + // When src is a NAMED-tagged wrapper and dst has a direct + // NAMED-tagged variant equal to src, accept by nominal + // identity — wrapper's leaves are NOT direct variants of + // dst, so a structural subset walk would reject. SSoT + // with `is`/`as` variant lookup (#198 family). + let v: *node = du.list; + for (v != nil) { + let vu: *node = resolvealias(c, unwrapbang(v)); + if (vu != nil) { + if (vu.kind == nkind.N_TTAGGED) { + if (typeeqast(v, src)) { return true; }; + }; + }; + v = v.next; + }; *confident = false; return true; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 0e56b5c0..4bcc0e88 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -13115,6 +13115,24 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { // return through another fn with the same shape but possibly // a different surface spelling. if (du.kind == nkind.N_TTAGGED && su.kind == nkind.N_TTAGGED) { + // #205: NAMED-variant nominal compare BEFORE permissive + // fallthrough. Mirror of cstage type.c type_assignable + // tagged→tagged arm (#199 α concrete→tagged sibling). + // When src is a NAMED-tagged wrapper and dst has a direct + // NAMED-tagged variant equal to src, accept by nominal + // identity — wrapper's leaves are NOT direct variants of + // dst, so a structural subset walk would reject. SSoT + // with `is`/`as` variant lookup (#198 family). + let v: *node = du.list; + for (v != nil) { + let vu: *node = resolvealias(c, unwrapbang(v)); + if (vu != nil) { + if (vu.kind == nkind.N_TTAGGED) { + if (typeeqast(v, src)) { return true; }; + }; + }; + v = v.next; + }; *confident = false; return true; }; diff --git a/test/wcc/774_tagged_widen_named_variant.c b/test/wcc/774_tagged_widen_named_variant.c new file mode 100644 index 00000000..058ec38a --- /dev/null +++ b/test/wcc/774_tagged_widen_named_variant.c @@ -0,0 +1,329 @@ +/* + * 774_tagged_widen_named_variant — project #205: cstage type_assignable + + * wwstage isassignable tagged→tagged arm misses NAMED-variant nominal + * match. Before the fix, `let e: wrapper = u; let r: (size|eof|wrapper) = e;` + * REJECTED at cstage's checker (wwstage accepted permissively but cgen + * silently miscompiled — separate fold via #199b). The subset loop + * walked wrapper's leaves (unsupported, underread, nomem) against dst + * (size, eof, wrapper) and found none → return 0. + * + * Fix (cmd/wcc/type.c:327 + selfhost/cmd/wcc/check.ww:3087): insert a + * NAMED-variant nominal compare BEFORE the subset loop in the + * tagged→tagged arm. When src is a NAMED-tagged wrapper and dst has a + * direct NAMED-tagged variant equal to src, accept by nominal identity. + * Mirrors the concrete→tagged arm at type.c:316 (#199 α). SSoT with + * `is`/`as` non-recursive variant lookup (#198 family). Wwstage mirrors + * the structural insertion; the existing permissive `*confident = false; + * return true;` tail is preserved (deferred-tightening per #202). + * + * Cgen's tag-remap for the wrapper-as-whole case still maps src + * variants to dst tag 0 (cg_widen_tag_remap walks per-variant); the + * wrapped-slot layout for `dst.tag = variant_idx, dst.payload = src` + * is the deferred #199b future-work. Probes verify checker-accept + + * runtime exit-clean only; they do NOT inspect the resulting variant + * tag of the widen target. + * + * Coverage (5 rows): + * 1. accept_wrapper_variant — BUG-REPRO. `let e: wrapper = u; + * return e;` where return type contains wrapper as direct NAMED + * variant. MUST now ACCEPT + run. + * 2. accept_nested_wrapper — two layers of NAMED-tagged wrap: + * `inner -> outer -> (size|outer)`. Each widen exercises the + * tagged→tagged NAMED nominal compare. + * 3. accept_pure_leaf_subset — REGRESSION GATE. Tagged→tagged + * where every src variant is directly in dst (no wrapper). The + * classic subset path — must keep working. + * 4. reject_concrete_unrelated — REJECTION GATE. Concrete `a` into + * `(b|c)` where a is NOT in dst. Verifies the fix didn't widen + * the concrete→tagged arm's discipline (#199 α stays intact). + * 5. branched_callee_widen — fn returns (size|eof|wrapper) + * from one of two paths (the wrapper-widen and a flat scalar + * variant). Verifies branched runtime, not just constant-fold. + * + * Per-row gates: cstage compile + runtime, wwstage compile + runtime, + * cs.s == ww.s byte-identical (rule-10). Row 4 uses EXPECT_REJECT to + * indicate "build must fail" — driver invocation returns non-zero. + */ +#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 + +#define EXPECT_REJECT (-2) + +struct row { + const char *label; + const char *src; + int expected_exit; /* EXPECT_REJECT means build must fail */ + int stage_mask; + int byte_id; /* run cs.s == ww.s diff (works rows only) */ +}; + +static const struct row rows[] = { + { "accept_wrapper_variant", + "package main;\n" + "type unsupported = !void;\n" + "type underread = !i32;\n" + "type nomem = !void;\n" + "type wrapper = !(unsupported | underread | nomem);\n" + "type eof = void;\n" + "fn f() (size | eof | wrapper) = {\n" + " let u: unsupported;\n" + " let e: wrapper = u;\n" + " return e;\n" + "};\n" + "export fn main() i32 = { let r = f(); return 41; };\n", + 41, + STAGE_CS | STAGE_WW, 1 }, + { "accept_nested_wrapper", + "package main;\n" + "type ax = !void;\n" + "type inner = !(ax);\n" + "type outer = !(inner);\n" + "fn f() (size | outer) = {\n" + " let a: ax;\n" + " let i: inner = a;\n" + " let o: outer = i;\n" + " return o;\n" + "};\n" + "export fn main() i32 = { let r = f(); return 42; };\n", + 42, + STAGE_CS | STAGE_WW, 1 }, + /* Row 3 dst uses an all-!void variant set (8B slot, tag-only) to + * keep ssz == slot_sz at cgwidentaggedstorebp. A wider dst + * (e.g. `(ax|bx|size)`) hits a pre-existing wwstage cgen gap: the + * call-ABI branch unconditionally stores DX/CX/R8 at write_off+8 + * when slot_sz > 8, ignoring src ssz, while cstage zero-pads when + * ssz < sz (cmd/w6c/cgen.c:1788-1795 vs selfhost/cmd/wcc/cgenutil.ww + * cgwidentaggedstorebp:2680-2700). Sibling to #202 (wwstage + * cgen-side asymmetry); deferred per #199b layout family. */ + { "accept_pure_leaf_subset", + "package main;\n" + "type ax = !void;\n" + "type bx = !void;\n" + "type cx = !void;\n" + "fn small() (ax | bx) = { let u: ax; return u; };\n" + "export fn main() i32 = {\n" + " let r: (ax | bx | cx) = small();\n" + " return 43;\n" + "};\n", + 43, + STAGE_CS | STAGE_WW, 1 }, + /* Row 4 uses payload-bearing aliases so wwstage's primitive-mismatch + * arm fires confidently — three !void aliases all collapse to "void" + * via unwrapbang + resolvealias and wwstage falls through to its + * permissive tail (#202 family, deferred). The payload axis (i32 + * vs i64 vs f64) splits the underlying primitives so both stages + * reject under the concrete→tagged arm. */ + { "reject_concrete_unrelated", + "package main;\n" + "type ax = !i32;\n" + "type bx = !i64;\n" + "type cx = !f64;\n" + "export fn main() i32 = {\n" + " let u: ax;\n" + " let r: (bx | cx) = u;\n" + " return 0;\n" + "};\n", + EXPECT_REJECT, + STAGE_CS | STAGE_WW, 0 }, + { "branched_callee_widen", + "package main;\n" + "type unsupported = !void;\n" + "type wrapper = !(unsupported);\n" + "type eof = void;\n" + "fn pick(b: bool) (size | eof | wrapper) = {\n" + " if (b) {\n" + " let u: unsupported;\n" + " let e: wrapper = u;\n" + " return e;\n" + " };\n" + " return 50: size;\n" + "};\n" + "export fn main() i32 = {\n" + " let r1 = pick(true);\n" + " let r2 = pick(false);\n" + " return 45;\n" + "};\n", + 45, + STAGE_CS | STAGE_WW, 1 }, +}; + +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/wt_%d_d_%d", getpid(), seq); + snprintf(src, sizeof src, "%s/main774.ww", tmpdir); + snprintf(base, sizeof base, "main774"); + mkdir(tmpdir, 0755); + if (write_source(src, r->src) != 0) { + cleanup_tmp(tmpdir, base); + return -1; + } + int rc; + int br = build_via_driver(driver, tmpdir, src); + if (r->expected_exit == EXPECT_REJECT) { + rc = (br != 0) ? r->expected_exit : 0; + } else if (br == 0) { + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + rc = runwait(outbin); + } else { + rc = -1; + } + 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 (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[512], tdc[256], tdw[256], base[64], cs[512], ws[512]; + snprintf(tdc, sizeof tdc, "/tmp/wt_%d_c_%d", getpid(), seq); + snprintf(tdw, sizeof tdw, "/tmp/wt_%d_w_%d", getpid(), seq); + snprintf(base, sizeof base, "main774"); + mkdir(tdc, 0755); + mkdir(tdw, 0755); + snprintf(src, sizeof src, "%s/main774.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/main774.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, + "tagged_widen_named_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, + "tagged_widen_named_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, + "tagged_widen_named_variant[byte-id][%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + } + } + } + + if (!wwpresent) + fprintf(stderr, "tagged_widen_named_variant: skip wwstage (no %s)\n", wdrv); + + if (fail) { + fprintf(stderr, "tagged_widen_named_variant: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("tagged_widen_named_variant: %d/%d ok\n", total, total); + return 0; +}