diff --git a/Makefile b/Makefile index 73948e66..af136c94 100644 --- a/Makefile +++ b/Makefile @@ -321,6 +321,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_io_types_run \ $(BIN)/test_dot_aliased_ptr \ $(BIN)/test_return_tagged_forward \ + $(BIN)/test_widen_transitive \ $(BIN)/test_use_promote_alias \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ @@ -653,6 +654,12 @@ $(BIN)/test_return_tagged_forward: test/wcc/770_return_tagged_forward.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_widen_transitive: test/wcc/771_widen_transitive.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/cmd/wcc/type.c b/cmd/wcc/type.c index 8ed4cf0e..f7878370 100644 --- a/cmd/wcc/type.c +++ b/cmd/wcc/type.c @@ -304,8 +304,24 @@ type_assignable(Type *dst, Type *src) Type *su = (src->kind == TY_NAMED) ? src->under : src; if (du && du->kind == TY_TAGGED && !(su && su->kind == TY_TAGGED)) { - for (Tparam *p = du->params; p; p = p->next) + /* #199(α): direct variant only — no transitive drill into + * a NAMED-tagged wrapper variant. Cgen has no wrapped-slot + * layout (single-level [tag][payload]), so admitting a + * nested widen silently miscompiled to tag=0 (#199 repro + * io.underread → (size|io.eof|io.error)). ww-stricter than + * Hare; harec types.c:702-739 keeps the drill (#199b is + * the deferred wrapped-slot port). Restores SSoT with + * `is`/`as`'s non-recursive variant lookup. Callers compose + * `let inner: Wrapper = sub; let r: parent = inner;`. */ + for (Tparam *p = du->params; p; p = p->next) { + Type *pu = (p->type && p->type->kind == TY_NAMED) + ? p->type->under : p->type; + if (pu && pu->kind == TY_TAGGED) { + if (type_eq(p->type, src)) return 1; + continue; + } if (type_assignable(p->type, src)) return 1; + } return 0; } if (du && du->kind == TY_TAGGED && diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 5d6fa3ae..95165821 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -13080,21 +13080,32 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { return true; }; // Tagged-union variant inclusion: src is one of dst's variants. - // Recursive isassignable mirrors cstage type_assignable - // (cmd/wcc/type.c:298-299) and harec tagged_select_subtype's - // recursive type_is_assignable call (ref/harec/src/types.c:702-739, - // :718; invoked from the TAGGED arm at :1110-1112). #39 cascade: - // the prior typeeqast-only walk rejected widenings that aren't - // strict surface-eq (NAMED-aliased variants, nested tagged inside - // a variant, concrete → variant after the wrap-induced exprtype - // reshape). #55 surface-nominal fast path is preserved by the - // recursive call's leading typeeqast (line 2257). #57 bare-vs- - // qualified TNAME residual unchanged. + // #199(α): direct variant only — no transitive drill into a + // NAMED-tagged wrapper variant. Cgen has no wrapped-slot layout + // (taggedvariantindext returns -1 → tag=0 silent miscompile on + // io.underread → (size|io.eof|io.error)). ww-stricter than Hare; + // harec keeps the drill at types.c:702-739 (#199b deferred port). + // Restores SSoT with `is`/`as` non-recursive lookup (#198 sibling). + // Callers compose `let inner: Wrapper = sub; let r: parent = inner;`. if (du.kind == nkind.N_TTAGGED && su.kind != nkind.N_TTAGGED) { let v: *node = du.list; for (v != nil) { - let innerconf: bool = false; - if (isassignable(c, v, src, &innerconf)) { return true; }; + // Spread `...wrapper` keeps the recursive drill: the + // wrapper's flat variants are intentionally inlined into + // the parent set, and AST-level params haven't been + // expanded yet (cstage flattens at resolve_type; wwstage + // stays AST-keyed). Plain wrapper variant gets the + // direct-only gate. + let vspread: bool = (v.op == tkind.TK_ELLIPSIS); + let vu: *node = resolvealias(c, unwrapbang(v)); + let vtagged: bool = false; + if (vu != nil) { if (vu.kind == nkind.N_TTAGGED) { vtagged = true; }; }; + if (vtagged && !vspread) { + if (typeeqast(v, src)) { return true; }; + } else { + let innerconf: bool = false; + if (isassignable(c, v, src, &innerconf)) { return true; }; + }; v = v.next; }; return false; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 6d65c35b..30607bd2 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -3050,21 +3050,32 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { return true; }; // Tagged-union variant inclusion: src is one of dst's variants. - // Recursive isassignable mirrors cstage type_assignable - // (cmd/wcc/type.c:298-299) and harec tagged_select_subtype's - // recursive type_is_assignable call (ref/harec/src/types.c:702-739, - // :718; invoked from the TAGGED arm at :1110-1112). #39 cascade: - // the prior typeeqast-only walk rejected widenings that aren't - // strict surface-eq (NAMED-aliased variants, nested tagged inside - // a variant, concrete → variant after the wrap-induced exprtype - // reshape). #55 surface-nominal fast path is preserved by the - // recursive call's leading typeeqast (line 2257). #57 bare-vs- - // qualified TNAME residual unchanged. + // #199(α): direct variant only — no transitive drill into a + // NAMED-tagged wrapper variant. Cgen has no wrapped-slot layout + // (taggedvariantindext returns -1 → tag=0 silent miscompile on + // io.underread → (size|io.eof|io.error)). ww-stricter than Hare; + // harec keeps the drill at types.c:702-739 (#199b deferred port). + // Restores SSoT with `is`/`as` non-recursive lookup (#198 sibling). + // Callers compose `let inner: Wrapper = sub; let r: parent = inner;`. if (du.kind == nkind.N_TTAGGED && su.kind != nkind.N_TTAGGED) { let v: *node = du.list; for (v != nil) { - let innerconf: bool = false; - if (isassignable(c, v, src, &innerconf)) { return true; }; + // Spread `...wrapper` keeps the recursive drill: the + // wrapper's flat variants are intentionally inlined into + // the parent set, and AST-level params haven't been + // expanded yet (cstage flattens at resolve_type; wwstage + // stays AST-keyed). Plain wrapper variant gets the + // direct-only gate. + let vspread: bool = (v.op == tkind.TK_ELLIPSIS); + let vu: *node = resolvealias(c, unwrapbang(v)); + let vtagged: bool = false; + if (vu != nil) { if (vu.kind == nkind.N_TTAGGED) { vtagged = true; }; }; + if (vtagged && !vspread) { + if (typeeqast(v, src)) { return true; }; + } else { + let innerconf: bool = false; + if (isassignable(c, v, src, &innerconf)) { return true; }; + }; v = v.next; }; return false; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 13eb66f9..785fb0a4 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -13080,21 +13080,32 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { return true; }; // Tagged-union variant inclusion: src is one of dst's variants. - // Recursive isassignable mirrors cstage type_assignable - // (cmd/wcc/type.c:298-299) and harec tagged_select_subtype's - // recursive type_is_assignable call (ref/harec/src/types.c:702-739, - // :718; invoked from the TAGGED arm at :1110-1112). #39 cascade: - // the prior typeeqast-only walk rejected widenings that aren't - // strict surface-eq (NAMED-aliased variants, nested tagged inside - // a variant, concrete → variant after the wrap-induced exprtype - // reshape). #55 surface-nominal fast path is preserved by the - // recursive call's leading typeeqast (line 2257). #57 bare-vs- - // qualified TNAME residual unchanged. + // #199(α): direct variant only — no transitive drill into a + // NAMED-tagged wrapper variant. Cgen has no wrapped-slot layout + // (taggedvariantindext returns -1 → tag=0 silent miscompile on + // io.underread → (size|io.eof|io.error)). ww-stricter than Hare; + // harec keeps the drill at types.c:702-739 (#199b deferred port). + // Restores SSoT with `is`/`as` non-recursive lookup (#198 sibling). + // Callers compose `let inner: Wrapper = sub; let r: parent = inner;`. if (du.kind == nkind.N_TTAGGED && su.kind != nkind.N_TTAGGED) { let v: *node = du.list; for (v != nil) { - let innerconf: bool = false; - if (isassignable(c, v, src, &innerconf)) { return true; }; + // Spread `...wrapper` keeps the recursive drill: the + // wrapper's flat variants are intentionally inlined into + // the parent set, and AST-level params haven't been + // expanded yet (cstage flattens at resolve_type; wwstage + // stays AST-keyed). Plain wrapper variant gets the + // direct-only gate. + let vspread: bool = (v.op == tkind.TK_ELLIPSIS); + let vu: *node = resolvealias(c, unwrapbang(v)); + let vtagged: bool = false; + if (vu != nil) { if (vu.kind == nkind.N_TTAGGED) { vtagged = true; }; }; + if (vtagged && !vspread) { + if (typeeqast(v, src)) { return true; }; + } else { + let innerconf: bool = false; + if (isassignable(c, v, src, &innerconf)) { return true; }; + }; v = v.next; }; return false; diff --git a/test/wcc/771_widen_transitive.c b/test/wcc/771_widen_transitive.c new file mode 100644 index 00000000..5645bac3 --- /dev/null +++ b/test/wcc/771_widen_transitive.c @@ -0,0 +1,314 @@ +/* + * 771_widen_transitive — project #199 (α): cstage type_assignable + + * wwstage isassignable must REJECT transitive nested-tagged widen. + * Cgen has no wrapped-slot layout (#199b deferred future-work); the + * pre-α recursive `type_assignable(p->type, src)` walk admitted + * `let r: (size|io.eof|io.error) = u` for u: io.underread, then cgen + * silently miscompiled (cg_tag_for_variant returned -1 → tag=0 → r + * reads as variant 0 = size at runtime). + * + * Fix (cmd/wcc/type.c:308 + selfhost/cmd/wcc/check.ww:3063): the + * tagged-non-tagged arm now matches direct variants ONLY. NAMED-tagged + * wrapper variants (vu.kind == TY_TAGGED) compare nominally (type_eq / + * typeeqast); other variants keep the leaf recursion for primitive + * compat / untyped resolution. wwstage additionally preserves the + * spread-recursion path: `...wrapper` variants (op == TK_ELLIPSIS) drill + * through because the AST list hasn't pre-flattened (cstage flattens at + * resolve_type, wwstage stays AST-keyed). + * + * Restores SSoT inside the checker pair: `is` / `as` / `match` arm + * lookup is already non-recursive (#198 sibling), and the LET-init / + * return / assign arms now agree. Aligns DOWN to the leaner side + * (rule-10 stage symmetry). ww-stricter than Hare; harec keeps the + * drill at ref/harec/src/types.c:702-739 (#199b — wrapped-slot layout + * port deferred per drew). + * + * Coverage (5 rows): + * 1. reject_transitive_widen — `let r: (size|wrapper) = u` with + * u: underread, wrapper = !(underread | nomem). BOTH stages + * MUST reject at the checker. The repro. + * 2. spread_alt_widen — `let r: (size|...wrapper) = u;`. The + * `...` spread inlines wrapper's variants into the parent flat + * set, so underread becomes a DIRECT variant; widen succeeds. + * CS-only runtime: wwstage's `is`/`match` cross-spread variant + * lookup is open-bug #190/#198 (wwstage walks original AST list, + * doesn't see expanded variants); byte-id still gates the asm. + * 3. direct_flat_variant — `let r: (size|underread|nomem) = u` + * with the underlying variants flat in the parent (no wrapper). + * Regression gate: direct widen unchanged by the fix. + * 4. branched_callee_widen — fn returns (size|underread|nomem), + * runtime picks between two flat-variant returns. Verifies the + * cg_widen_tagged_store scalar branch's tag synthesis is + * unchanged for direct widens. + * 5. wrapper_typed_widen — `let r: wrapper = u` with u: + * underread (DIRECT variant of wrapper). Regression gate: the + * single-level wrapper widen is the supported escape hatch and + * MUST continue working. + * + * Per-row gates: cstage compile + runtime, wwstage compile + runtime, + * cs.s == ww.s byte-identical (rule-10). Row 1 uses + * expected_exit = -2 to indicate "build must fail" — the driver + * invocation is expected to return non-zero from the checker. + */ +#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[] = { + { "reject_transitive_widen", + "package main;\n" + "type underread = !i32;\n" + "type nomem = !void;\n" + "type wrapper = !(underread | nomem);\n" + "export fn main() i32 = {\n" + " let u: underread;\n" + " let r: (size | wrapper) = u;\n" + " return 0;\n" + "};\n", + EXPECT_REJECT, + STAGE_CS | STAGE_WW, 0 }, + { "spread_alt_widen", + "package main;\n" + "type underread = !i32;\n" + "type nomem = !void;\n" + "type wrapper = !(underread | nomem);\n" + "export fn main() i32 = {\n" + " let u: underread;\n" + " let r: (size | ...wrapper) = u;\n" + " if (r is underread) { return 70; };\n" + " return 99;\n" + "};\n", + 70, + STAGE_CS, 0 }, + { "direct_flat_variant", + "package main;\n" + "type underread = !i32;\n" + "type nomem = !void;\n" + "export fn main() i32 = {\n" + " let u: underread;\n" + " let r: (size | underread | nomem) = u;\n" + " if (r is underread) { return 71; };\n" + " return 99;\n" + "};\n", + 71, + STAGE_CS | STAGE_WW, 1 }, + { "branched_callee_widen", + "package main;\n" + "type underread = !i32;\n" + "type nomem = !void;\n" + "fn pick(b: bool) (size | underread | nomem) = {\n" + " if (b) { let u: underread; return u; };\n" + " return 42: size;\n" + "};\n" + "export fn main() i32 = {\n" + " let r: (size | underread | nomem) = pick(true);\n" + " if (r is underread) { return 72; };\n" + " return 99;\n" + "};\n", + 72, + STAGE_CS | STAGE_WW, 1 }, + { "wrapper_typed_widen", + "package main;\n" + "type underread = !i32;\n" + "type nomem = !void;\n" + "type wrapper = !(underread | nomem);\n" + "export fn main() i32 = {\n" + " let u: underread;\n" + " let r: wrapper = u;\n" + " if (r is underread) { return 73; };\n" + " return 99;\n" + "};\n", + 73, + 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/main771.ww", tmpdir); + snprintf(base, sizeof base, "main771"); + 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) { + /* Build must fail with a non-zero exit (checker error). */ + 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, "main771"); + mkdir(tdc, 0755); + mkdir(tdw, 0755); + snprintf(src, sizeof src, "%s/main771.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/main771.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, + "widen_transitive[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, + "widen_transitive[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, + "widen_transitive[byte-id][%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + } + } + } + + if (!wwpresent) + fprintf(stderr, "widen_transitive: skip wwstage (no %s)\n", wdrv); + + if (fail) { + fprintf(stderr, "widen_transitive: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("widen_transitive: %d/%d ok\n", total, total); + return 0; +}