From e0afe48c21f14c5cf56843cdea55e1bdc1f014f4 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 8 Aug 2026 14:31:53 +0900 Subject: [PATCH] test: port the xmod typecheck reject observers to ww test/xmod/typecheck_test.ww replaces 989_callarg_typecheck.c and 989_tagged_subset_reject.c. Preserved: both -T __wwtests reservation faces (both stages, diagnostic required), the shape-mismatch collision (both stages), the cstage-only same-shape #37 pin, and both tagged leaf-bridge xmod rows. Not ported, owned elsewhere per the audit: the carriers' twelve ordinary call-arg rows (r989_callarg_* fixtures) and the three w6c-vs-w6c_ww byte-id rows (test-data-byteid over r989_tagsubset_{flatten_subset,slice_subset,nested_wrap}). --- Makefile | 2 +- test/wcc/989_callarg_typecheck.c | 385 --------------------------- test/wcc/989_tagged_subset_reject.c | 391 ---------------------------- test/xmod/typecheck_test.ww | 253 ++++++++++++++++++ 4 files changed, 254 insertions(+), 777 deletions(-) delete mode 100644 test/wcc/989_callarg_typecheck.c delete mode 100644 test/wcc/989_tagged_subset_reject.c create mode 100644 test/xmod/typecheck_test.ww diff --git a/Makefile b/Makefile index a90420c4..ffc79413 100644 --- a/Makefile +++ b/Makefile @@ -373,7 +373,7 @@ SEP_WW_TARGETS = $(SEP_WW_TESTS:%=wwtest/%) # capacity, directive adjacency). Compiler/driver gates like test/sep: # they run under test-compiler. XMOD_WW_TESTS = test/xmod/collide_test.ww test/xmod/m1_test.ww \ - test/xmod/label_test.ww + test/xmod/label_test.ww test/xmod/typecheck_test.ww XMOD_WW_TARGETS = $(XMOD_WW_TESTS:%=wwtest/%) BOOTSTRAP_WRAPPER_SOURCES = test/wcc/950_selfcheck.c \ test/wcc/991_w6a_ww.c \ diff --git a/test/wcc/989_callarg_typecheck.c b/test/wcc/989_callarg_typecheck.c deleted file mode 100644 index 7af72787..00000000 --- a/test/wcc/989_callarg_typecheck.c +++ /dev/null @@ -1,385 +0,0 @@ -/* - * 989_callarg_typecheck — #24: wwstage align UP to cstage on GENERAL - * call-argument assignability (CHECKER-ONLY, cat-A silent-miscompile). - * - * THE BUG: wwstage ran NO general call-arg typecheck. desugarcallargs - * (selfhost/cmd/wcc/check.ww) only had the narrow #258 array→slice arm, - * so ANY mistyped non-array scalar call-arg was SILENTLY accepted: an - * `int` passed where a `[]T` param expects a 24B slice header builds - * rc=0 and runs garbage (the int's 8 bytes read as the slice .len/.ptr). - * cstage rejects every non-variadic arg at cmd/wcc/check.c:1867-1870 - * (`argument type %s not assignable to %s`). - * - * The -T face is the headline silent miscompile: a user - * `const __wwtests: int` + ≥1 @test fn shadows the synth test table, so - * the synth `run(__wwtests)` passes the int 99 where run() wants the - * `[](str, *fn() void)` table — cstage rejected it loud, wwstage - * silently built a broken binary that iterated 99 as a slice .len and - * printed 63 garbage FAIL rows / crashed. - * - * THE FIX is two stacked concerns, both align-UP-to-cstage: - * (a) general per-arg isassignable at the desugarcallargs choke-point, - * mirroring check.c:1867 (conf-gated like the let/return sibling - * sites; subsumes the old narrow #258 reject arm). - * (b) reserve the synth `__wwtests` name under -T in BOTH stages, - * mirroring the `main` reservation (check.c:2996 / check.ww). A - * user `__wwtests` whose type HAPPENS to match run()'s param - * ([](str,*fn()void)) slips (a) but still silently shadows the - * synth table → reservation rejects it loud regardless of type. - * - * The twelve ordinary source rows now live in r989_callarg_* fixtures. - * This wrapper retains only -T package synthesis and multi-package nominal - * collision evidence that the single-case fixture grammar cannot encode. - * - * -T faces (bundle via `ww test -c`, then ` -T ` must - * loud-reject; the synth's run() callee is resolved from the bundled - * lib/test): - * wwtests_int | const __wwtests: int | REJECT [(a)] - * wwtests_shadow | const __wwtests: [](str,*fn()void) | REJECT [(b)] - * - * wwtests_shadow is the (b) teeth: its type matches run()'s param so (a) - * stays silent (both unpatched stages built it rc=0, silently running - * the user's 1-entry table instead of the collected @test set) — only - * the name reservation rejects it. - */ -#include -#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; -} - -static int -filehas(const char *path, const char *needle) -{ - char buf[8192]; - FILE *f = fopen(path, "rb"); - if (!f) return 0; - size_t n = fread(buf, 1, sizeof buf - 1, f); - fclose(f); - buf[n] = '\0'; - return strstr(buf, needle) != NULL; -} - -/* -T reject faces — a user fixture is bundled with lib/test via - * `ww test -c` (compiler-neutral; the synth `run()` callee resolves from - * the bundle), then ` -T ` must loud-reject. */ -struct trow { - const char *label; - const char *src; -}; - -static const struct trow trows[] = { - /* (a) the headline cat-A: user __wwtests:int shadows the synth table - * → run(int) where run wants [](str,*fn()void). Pre-fix wwstage built - * a broken binary (int read as a 24B slice header). */ - { "wwtests_int", - "package main;\n" - "const __wwtests: int = 99;\n" - "@test fn checkfoo() void = { return; };\n" }, - - /* (b) reservation teeth: a user __wwtests whose type MATCHES run()'s - * param typechecks fine — (a)'s isassignable stays silent — but it - * silently shadows the synth table (both unpatched stages built it - * rc=0, running the user's 1-entry table, not the collected @tests). - * Only the `__wwtests` name reservation rejects it. */ - { "wwtests_shadow", - "package main;\n" - "fn dummy() void = { return; };\n" - "const __wwtests: [](str, *fn() void) = [(\"x\", &dummy)];\n" - "@test fn checkfoo() void = { return; };\n" }, -}; - -/* tbundle_reject — #94 sep layout: ` test` carries -T into the - * root unit's w6c pass, where the __wwtests reservation / call-arg - * typecheck must loud-reject, so the driver build must exit nonzero. - * `stage` is the cs/ww label; `drv` is the matching driver (ww / ww_ww). - * Returns 0 on the expected reject. */ -static int -tbundle_reject(const char *bin, const char *stage, const char *drv, - const struct trow *t, int i) -{ - char td[128], src[160], stem[160], errf[176], cmd[4096]; - snprintf(td, sizeof td, "/tmp/catt_%s_%d_XXXXXX", stage, i); - if (mkdtemp(td) == NULL) return -1; - snprintf(src, sizeof src, "%s/input.ww", td); - snprintf(stem, sizeof stem, "%s/prog", td); - snprintf(errf, sizeof errf, "%s/err.txt", td); - int result = -1; - int src_created = 0, command_started = 0; - - FILE *f = fopen(src, "wb"); - if (!f) goto out; - src_created = 1; - fputs(t->src, f); - if (fclose(f) != 0) goto out; - - snprintf(cmd, sizeof cmd, - "%s/%s test -o %s %s >/dev/null 2>%s", - bin, drv, stem, src, errf); - command_started = 1; - int rc = runwait(cmd); - /* Both stages emit the reservation diagnostic; a crash would not (#20). */ - int hasmsg = filehas(errf, "__wwtests is reserved by -T"); - - if (rc == 0) { - fprintf(stderr, "callarg_typecheck[%s][%s]: %s test " - "accepted (expected a loud reject)\n", stage, t->label, drv); - } else if (!hasmsg) { - fprintf(stderr, "callarg_typecheck[%s][%s]: %s test " - "nonzero exit but missing reservation diagnostic (a crash, " - "not a clean reject)\n", stage, t->label, drv); - } else { - result = 0; - } - -out: - { - int cleanfail = 0; - char path[192]; - if (command_started) { - snprintf(path, sizeof path, "%s.sepwork", stem); - snprintf(cmd, sizeof cmd, "rm -rf %s", path); - if (runwait(cmd) != 0) cleanfail = 1; - if (unlink(stem) != 0 && errno != ENOENT) cleanfail = 1; - if (unlink(errf) != 0 && errno != ENOENT) cleanfail = 1; - } - if (src_created && unlink(src) != 0 && errno != ENOENT) cleanfail = 1; - if (rmdir(td) != 0) cleanfail = 1; - if (cleanfail) { - fprintf(stderr, "callarg_typecheck[%s][%s]: temporary cleanup " - "failed\n", stage, t->label); - result = -1; - } - } - return result; -} - -/* multimod_build_fail — write io2.ww (always; defines handle=(file|stream)), - * mod1.ww (when withmod1), and a main.ww with `mainbody`, then `drv build -I` - * the tree. Returns 0 when the build correctly FAILS (the arg is rejected), - * non-zero when it wrongly built. Drives the #24 cross-module collision rows - * that single-file fixtures cannot express. */ -static int -multimod_build_fail(const char *drv, const char *mainbody, int withmod1, - int tag) -{ - char dir[128], io2d[160], mod1d[160], p[224], cmd[2048]; - snprintf(dir, sizeof dir, "/tmp/catcoll_%d_XXXXXX", tag); - if (mkdtemp(dir) == NULL) return -1; - snprintf(io2d, sizeof io2d, "%s/io2", dir); - snprintf(mod1d, sizeof mod1d, "%s/mod1", dir); - int result = -1; - int io2owned = 0, mod1owned = 0; - int io2file = 0, mod1file = 0, mainfile = 0, command_started = 0; - if (mkdir(io2d, 0755) != 0) goto out; - io2owned = 1; - if (withmod1 && mkdir(mod1d, 0755) != 0) goto out; - if (withmod1) mod1owned = 1; - - snprintf(p, sizeof p, "%s/io2.ww", io2d); - FILE *f = fopen(p, "wb"); - if (!f) goto out; - io2file = 1; - fputs("package io2;\n" - "export type vtable = struct { x: i32 };\n" - "export type stream = *vtable;\n" - "export type file = i32;\n" - "export type handle = (file | stream);\n" - "export fn take(h: handle) int = { return 7; };\n", f); - if (fclose(f) != 0) goto out; - - if (withmod1) { - snprintf(p, sizeof p, "%s/mod1.ww", mod1d); - f = fopen(p, "wb"); - if (!f) goto out; - mod1file = 1; - fputs("package mod1;\n" - "export type wbox = struct { y: i64 };\n" - "export type stream = *wbox;\n" - "export fn mk() stream = { return nil; };\n", f); - if (fclose(f) != 0) goto out; - } - - snprintf(p, sizeof p, "%s/main.ww", dir); - f = fopen(p, "wb"); - if (!f) goto out; - mainfile = 1; - fputs(mainbody, f); - if (fclose(f) != 0) goto out; - - char errf[128]; - snprintf(errf, sizeof errf, "%s/err.txt", dir); - if (withmod1) - snprintf(cmd, sizeof cmd, - "cd %s && %s build -I %s -I %s main.ww >/dev/null 2>%s", - dir, drv, io2d, mod1d, errf); - else - snprintf(cmd, sizeof cmd, - "cd %s && %s build -I %s main.ww >/dev/null 2>%s", - dir, drv, io2d, errf); - command_started = 1; - int rc = runwait(cmd); - /* Both collision rows reject on "not assignable"; a crash would not (#20). */ - int hasmsg = filehas(errf, "not assignable"); - - /* build must NOT succeed AND must emit its diagnostic. */ - result = (rc != 0 && hasmsg) ? 0 : -1; - -out: - { - int cleanfail = 0; - char path[256]; - if (command_started) { - snprintf(path, sizeof path, "%s/main.sepwork", dir); - snprintf(cmd, sizeof cmd, "rm -rf %s", path); - if (runwait(cmd) != 0) cleanfail = 1; - snprintf(path, sizeof path, "%s/main", dir); - if (unlink(path) != 0 && errno != ENOENT) cleanfail = 1; - snprintf(path, sizeof path, "%s/err.txt", dir); - if (unlink(path) != 0 && errno != ENOENT) cleanfail = 1; - } - if (mainfile) { - snprintf(path, sizeof path, "%s/main.ww", dir); - if (unlink(path) != 0 && errno != ENOENT) cleanfail = 1; - } - if (io2file) { - snprintf(path, sizeof path, "%s/io2/io2.ww", dir); - if (unlink(path) != 0 && errno != ENOENT) cleanfail = 1; - } - if (mod1file) { - snprintf(path, sizeof path, "%s/mod1/mod1.ww", dir); - if (unlink(path) != 0 && errno != ENOENT) cleanfail = 1; - } - if (mod1owned && rmdir(mod1d) != 0) cleanfail = 1; - if (io2owned && rmdir(io2d) != 0) cleanfail = 1; - if (rmdir(dir) != 0) cleanfail = 1; - if (cleanfail) { - fprintf(stderr, "callarg_typecheck[collide-%d]: temporary " - "cleanup failed\n", tag); - result = -2; - } - } - return result; -} - -/* a []int SLICE passed where io2.handle = (file | stream) is wanted — NO - * slice variant. SHAPE-MISMATCH: cstage rejects nominally, and c3's (B) - * shape-matched-lenient leg ALSO rejects (slice src, no slice-shape variant) - * — the #24-B reachable win, asserted DUAL-STAGE (both drivers must fail). */ -static const char COLLIDE_SHAPE_MISMATCH[] = - "package main;\n" - "import io2;\n" - "export fn main() int = {\n" - " let xs: []int = [1, 2, 3];\n" - " return io2.take(xs);\n" - "};\n"; - -/* mod1.stream (a *mod1.wbox) passed where io2.handle is wanted — a cross- - * module SAME-LEAF, SAME-COARSE-SHAPE (both ptr → scalar/other) collision. - * cstage REJECTS on NOMINAL identity; wwstage's AST-keyed isassignable - * OVER-ACCEPTS it — the tracked #10/#66/#37 residual that (B) shape-narrowing - * cannot reach (the tagged variant node is a bare `stream` with no module, - * byte-identical to the genuine io2.stream; the distinguishing identity lives - * only in tinfo/#66, reachable only by the #37 nominal-tinfo conversion). The - * cs-side pin below asserts ONLY cstage's reject (catching a cs regression + - * recording the eventual ww target); the ww over-accept is documented, NOT - * asserted (a dual-stage row would be dark until #37 lands). */ -static const char COLLIDE_SAME_SHAPE[] = - "package main;\n" - "import io2;\n" - "import mod1;\n" - "export fn main() int = {\n" - " let s: mod1.stream = mod1.mk();\n" - " return io2.take(s);\n" - "};\n"; - -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 tn = (int)(sizeof trows / sizeof trows[0]); - int total = 0, fail = 0; - - /* -T faces: the reject fires inside each driver's w6c/-T pass - * (cstage via ww, wwstage via ww_ww). wwstage gated on ww_ww. */ - for (int i = 0; i < tn; i++) { - total++; - if (tbundle_reject(bin, "cstage", "ww", &trows[i], i) != 0) - fail++; - } - if (access(wdrv, X_OK) == 0) { - for (int i = 0; i < tn; i++) { - total++; - if (tbundle_reject(bin, "wwstage", "ww_ww", &trows[i], - 10 + i) != 0) - fail++; - } - } - - /* #24-B reachable win: the SHAPE-MISMATCH collision ([]int slice into a - * (file|stream) tagged with no slice variant) REJECTS on BOTH stages — - * dual-stage row (cstage nominal + c3's shape-matched-lenient leg). */ - total++; - int rowrc = multimod_build_fail(cdrv, COLLIDE_SHAPE_MISMATCH, 0, 1); - if (rowrc != 0) { - if (rowrc != -2) - fprintf(stderr, "callarg_typecheck[cstage]" - "[collide_shape_mismatch]: built ok, expected reject\n"); - fail++; - } - if (access(wdrv, X_OK) == 0) { /* wwstage gated (ww_ww present) */ - total++; - rowrc = multimod_build_fail(wdrv, COLLIDE_SHAPE_MISMATCH, 0, 2); - if (rowrc != 0) { - if (rowrc != -2) - fprintf(stderr, "callarg_typecheck[wwstage]" - "[collide_shape_mismatch]: built ok, expected " - "reject (#24-B shape-matched-lenient)\n"); - fail++; - } - } - /* #37 cs-side pin: the SAME-SHAPE cross-module collision rejects on - * cstage; the wwstage over-accept is the documented #10/#66/#37 nominal - * residual (NOT asserted — would be dark until #37 lands). */ - total++; - rowrc = multimod_build_fail(cdrv, COLLIDE_SAME_SHAPE, 1, 3); - if (rowrc != 0) { - if (rowrc != -2) - fprintf(stderr, "callarg_typecheck[cstage]" - "[collide_same_shape]: built ok, expected cstage to " - "reject the same-leaf collision (#37 cs-side pin)\n"); - fail++; - } - - if (fail) { - fprintf(stderr, "callarg_typecheck: %d/%d fixtures failed\n", - fail, total); - return 1; - } - printf("callarg_typecheck: %d/%d ok\n", total, total); - return 0; -} diff --git a/test/wcc/989_tagged_subset_reject.c b/test/wcc/989_tagged_subset_reject.c deleted file mode 100644 index 03172dd8..00000000 --- a/test/wcc/989_tagged_subset_reject.c +++ /dev/null @@ -1,391 +0,0 @@ -/* - * 989_tagged_subset_reject — A7: wwstage align UP to cstage on the - * tagged-union → tagged-union assignability subset check (CHECKER-ONLY). - * - * THE BUG: an implicit (no-cast) WIDE → NARROW tagged assignment - * (`(int|bool|str)` returned where `(int|bool)` is wanted) was SILENTLY - * ACCEPTED by wwstage (its tagged→tagged arm fell through a lenient - * `*confident=false; return true` escape) while cstage correctly REJECTS - * it (structural subset loop, cmd/wcc/type.c:360-367). The accepted - * payload (`str`) was then read through the `int` arm by cgen → garbage - * → exit 1. Pure wwstage-align-UP: cstage gets ZERO touch. - * - * THE FIX (selfhost/cmd/wcc/check.ww, tagged→tagged arm): after the #205 - * NAMED-variant nominal loop, replace the lenient escape with cstage's - * structural subset loop — every src variant must appear in dst (nominal - * identity via Layer-1 typeeqast); a missing variant rejects loud. GUARD: - * the subset loop runs ONLY when neither side carries a `...spread` - * (TK_ELLIPSIS) member; a spread-bearing union keeps the lenient escape - * (cstage flattens spreads in resolve, wwstage stays AST-keyed — routing - * a spread through the strict loop would over-reject a valid spread-widen - * cstage accepts; spread decl-form layout is #199b, deferred). - * - * The fix's predicate is a COMPOSITION (drew ruling A, the honest floor): - * per src variant, typeeqast FIRST (exact / structural — primitives, []u8 - * slices, nested unions, Layer-1 nominal aliases), THEN a leaf-only name - * bridge (qualleaf, module ignored) for the bare-vs-qualified spelling mix - * an inline-union callee return forces (variants spelled BARE in the - * callee, QUALIFIED at the consumer; the module is unrecoverable there). - * The two single-file rows below cannot exercise either half distinctly - * (one module → every name is bare → typeeqast's streq matches), so the - * teeth for the COMPOSITION are the dedicated rows: - * - slice_struct_subset: a []u8 (N_TSLICE, not N_TNAME) src variant is - * matched ONLY by typeeqast — a pure-leaf-bridge regression would - * wrongly reject it. - * - xmod_inline_forward: a bare inline-union variant vs its qualified - * consumer spelling is matched ONLY by the leaf bridge (typeeqast / - * aliassym cannot resolve a bare inline-return variant in the - * consumer's scope) — dropping the bridge regresses cs=0/ww=1. - * - * row | src → dst | verdict - * -------------------------+---------------------------------+----------- - * wide_narrow_nocast | (int|bool|str) → (int|bool) | REJECT [bug] - * flatten_subset | (bool|str) → (int|bool|str) | ok 0 +byteid - * slice_struct_subset | (int|[]u8) → (int|bool|[]u8) | ok 0 +byteid - * nested_wrap | inner → (int|inner) | ok 42 +byteid - * concrete_spread_widen | 42,"hi" → (...formattable|bool) | ok 0 - * spread_named_casematch | case e1 of (str|...e1) | REJECT [#115] - * spread_wider_subset | (str|...e1) → (str|bool|int|f64)| ok 0 [lenient] - * - * Cross-module rows (839-style -I build; wwstage-gated; byte-id NOT - * asserted — receiving + re-passing a cross-module tagged return crosses a - * pre-existing cgen frame-layout divergence, cf. 839): - * xmod_inline_forward | e.next (i64|done) → (i64|e.done)| ok 0 - * xmod_inline_narrow | (i64|done|more) → (i64|e.done) | REJECT - * - * Rows flatten_subset / slice_struct_subset / nested_wrap also assert w6c - * vs w6c_ww asm is BYTE-IDENTICAL (rule-10): the fix only changes the - * REJECT decision and a confident-vs-lenient accept on inputs cstage - * already accepted, so the emitted code is unchanged. The two spread-accept - * rows (concrete_spread_widen / spread_wider_subset) assert accept-on-both- - * stages only — they ride the deferred #199b spread path where the stages - * intentionally diverge (wwstage AST-keyed vs cstage pre-flattened), so - * byte-id is NOT asserted (cf. 839's cross-module-return note). - * spread_named_casematch is a #115 case-match reject (not the subset arm), - * pinned here as a symmetric-both-stage regression guard. - * Symmetric runtime/reject ownership is now r989_tagsubset_*; this wrapper - * retains only three assembly byte-ID sources and two xmod package rows. - * - * Both stages must agree (rule-10): REJECT rows build-FAIL on both; accept - * rows build+run to the same exit on cstage (`ww`) and wwstage (`ww_ww`). - */ -#include -#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 expect_build; /* 1 = build+run to want_exit; 0 = must FAIL */ - int want_exit; /* meaningful only when expect_build */ - int byteid; /* 1 = also assert w6c vs w6c_ww .s byte-id */ - const char *diag; /* expected reject diagnostic (NULL for build rows) */ -}; - -static const struct row rows[] = { - /* Runtime ownership moved to r989_*; retain assembly byte identity. */ - { "flatten_subset", - "package main;\n" - "type inner = (bool | str);\n" - "type flat = (int | bool | str);\n" - "fn f(x: inner) flat = { return x; };\n" - "export fn main() int = { return 0; };\n", 1, 0, 1 }, - { "slice_struct_subset", - "package main;\n" - "type a = (int | []u8);\n" - "type b = (int | bool | []u8);\n" - "fn f(x: a) b = { return x; };\n" - "export fn main() int = { return 0; };\n", 1, 0, 1 }, - { "nested_wrap", - "package main;\n" - "type inner = (bool | str);\n" - "type outer = (int | inner);\n" - "fn wrap(x: inner) outer = { return x; };\n" - "export fn main() int = {\n" - "\tlet v: inner = \"hi\";\n" - "\tlet o: outer = wrap(v);\n" - "\tmatch (o) {\n" - "\tcase let i: int => { return 1; };\n" - "\tcase let n: inner => match (n) {\n" - "\tcase let b: bool => { return 2; };\n" - "\tcase let s: str => { return 42; };\n" - "\t};\n" - "\t};\n" - "};\n", 1, 42, 1 }, -}; - -static int -filehas(const char *path, const char *needle) -{ - char buf[8192]; - FILE *f = fopen(path, "rb"); - if (!f) return 0; - size_t n = fread(buf, 1, sizeof buf - 1, f); - fclose(f); - buf[n] = '\0'; - return strstr(buf, needle) != NULL; -} - -static int -asm_byte_identical(const char *bin, const struct row *r) -{ - char dir[] = "/tmp/tsr_asm_XXXXXX"; - char src[128], cs[128], ws[128], cmd[1024]; - int rc = -1, cleanup_fail = 0; - if (mkdtemp(dir) == NULL) return -1; - snprintf(src, sizeof src, "%s/input.ww", dir); - snprintf(cs, sizeof cs, "%s/c.s", dir); - snprintf(ws, sizeof ws, "%s/ww.s", dir); - - FILE *f = fopen(src, "wb"); - if (!f) goto out; - if (fputs(r->src, f) == EOF) { fclose(f); goto out; } - if (fclose(f) != 0) goto out; - - snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); - if (runwait(cmd) != 0) { - fprintf(stderr, "row[%s]: w6c errored\n", r->label); - goto out; - } - snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", - bin, ws, src); - if (runwait(cmd) != 0) { - fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); - goto out; - } - - FILE *fc = fopen(cs, "rb"); - FILE *fw = fopen(ws, "rb"); - rc = 0; - if (!fc || !fw) { - rc = -1; - } else { - 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); - if (rc != 0) - fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", - r->label); -out: - if (unlink(src) != 0 && errno != ENOENT) cleanup_fail = 1; - if (unlink(cs) != 0 && errno != ENOENT) cleanup_fail = 1; - if (unlink(ws) != 0 && errno != ENOENT) cleanup_fail = 1; - if (rmdir(dir) != 0) cleanup_fail = 1; - return cleanup_fail ? -1 : rc; -} - -/* Cross-module rows — the leaf-bridge teeth. A bare inline-union variant - * (callee) vs its qualified consumer spelling is matched ONLY by the leaf - * bridge, and that mix is intrinsically multi-module (one file → all names - * bare → typeeqast's streq matches). 839-style `-I` build; byte-id NOT - * asserted (cross-module tagged return + re-pass crosses a pre-existing - * cgen frame-layout divergence, cf. 839). */ -struct xrow { - const char *label; - const char *esrc; /* package e */ - const char *msrc; /* package main; import e */ - int expect_build; /* 1 = build+run to want_exit; 0 = must FAIL */ - int want_exit; - const char *rejdiag; /* expected reject diagnostic (NULL for build rows) */ -}; - -static const struct xrow xrows[] = { - /* leaf-bridge ACCEPT: e.next returns the inline union (i64|done) - * spelled BARE; forward into qualified (i64|e.done). typeeqast/ - * aliassym cannot resolve the bare `done` in main's scope, so only - * the leaf bridge equates them. Dropping the bridge → cs=0/ww=1. */ - { "xmod_inline_forward", - "package e;\n" - "export type done = !i64;\n" - "export fn next() (i64 | done) = { return 0; };\n", - "package main;\n" - "import e;\n" - "type myres = (i64 | e.done);\n" - "fn forward() myres = { return e.next(); };\n" - "export fn main() i32 = { return 0; };\n", - 1, 0 }, - - /* named genuine-absence REJECT: src carries `more`, absent from dst - * (i64|e.done) → the subset loop rejects a NAMED variant, both - * stages. Guards the over-accept the lenient escape used to allow. */ - { "xmod_inline_narrow", - "package e;\n" - "export type done = !i64;\n" - "export type more = !i64;\n" - "export fn next() (i64 | done | more) = { return 0; };\n", - "package main;\n" - "import e;\n" - "type myres = (i64 | e.done);\n" - "fn forward() myres = { return e.next(); };\n" - "export fn main() i32 = { return 0; };\n", - 0, 0, "not assignable" }, -}; - -/* run_xmod — write e.ww + main.ww into a fresh dir, `ww build -I dir - * main.ww`, run dir/main. Returns the binary's exit, -1 on build - * failure, -2 on setup/cleanup failure, or -3 for a missing diagnostic. */ -static int -run_xmod(const char *driver, const struct xrow *x) -{ - char dir[] = "/tmp/tsrx_XXXXXX"; - if (mkdtemp(dir) == NULL) return -2; - - char path[1024], cmd[4096], errf[1056]; - int got = -2, cleanup_fail = 0; - int wrote = 0; - snprintf(path, sizeof path, "%s/e.ww", dir); - FILE *fe = fopen(path, "wb"); - if (fe) { - int ok = fputs(x->esrc, fe) != EOF; - if (fclose(fe) != 0) ok = 0; - if (ok) wrote++; - } - snprintf(path, sizeof path, "%s/main.ww", dir); - FILE *fm = fopen(path, "wb"); - if (fm) { - int ok = fputs(x->msrc, fm) != EOF; - if (fclose(fm) != 0) ok = 0; - if (ok) wrote++; - } - if (wrote != 2) { - goto out; - } - - snprintf(errf, sizeof errf, "%s/err.txt", dir); - snprintf(cmd, sizeof cmd, "cd %s && %s build -I %s %s/main.ww " - ">/dev/null 2>%s", dir, driver, dir, dir, errf); - int brc = runwait(cmd); - - got = -1; - if (brc == 0) { - snprintf(path, sizeof path, "%s/main", dir); - got = runwait(path); - } else if (x->rejdiag && !filehas(errf, x->rejdiag)) { - /* A REJECT row whose build failed WITHOUT its diagnostic is a - * crash, not a clean reject — return a non-(-1) sentinel so the - * caller (which expects -1 on a clean reject) flags it (#20). */ - got = -3; - } - -out: - snprintf(cmd, sizeof cmd, "rm -rf -- '%s/main.sepwork'", dir); - if (runwait(cmd) != 0) cleanup_fail = 1; - snprintf(path, sizeof path, "%s/main", dir); - if (unlink(path) != 0 && errno != ENOENT) cleanup_fail = 1; - snprintf(path, sizeof path, "%s/err.txt", dir); - if (unlink(path) != 0 && errno != ENOENT) cleanup_fail = 1; - snprintf(path, sizeof path, "%s/main.ww", dir); - if (unlink(path) != 0 && errno != ENOENT) cleanup_fail = 1; - snprintf(path, sizeof path, "%s/e.ww", dir); - if (unlink(path) != 0 && errno != ENOENT) cleanup_fail = 1; - if (rmdir(dir) != 0) cleanup_fail = 1; - return cleanup_fail ? -2 : got; -} - -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); - - struct { const char *name; const char *path; int gated; } - 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 - && access(drivers[d].path, X_OK) != 0) { - fprintf(stderr, "tagged_subset_reject: skip %s (no %s)\n", - drivers[d].name, drivers[d].path); - continue; - } - int xn = (int)(sizeof xrows / sizeof xrows[0]); - for (int i = 0; i < xn; i++) { - total++; - int got = run_xmod(drivers[d].path, &xrows[i]); - if (xrows[i].expect_build) { - if (got != xrows[i].want_exit) { - fprintf(stderr, - "tagged_subset_reject[%s][%s]: " - "exit=%d want=%d\n", - drivers[d].name, xrows[i].label, - got, xrows[i].want_exit); - fail++; - } - } else if (got != -1) { - if (got == -2) - fprintf(stderr, - "tagged_subset_reject[%s][%s]: " - "setup/cleanup failure\n", - drivers[d].name, xrows[i].label); - else if (got == -3) - fprintf(stderr, - "tagged_subset_reject[%s][%s]: " - "reject lacked `%s` diagnostic\n", - drivers[d].name, xrows[i].label, - xrows[i].rejdiag); - else - fprintf(stderr, - "tagged_subset_reject[%s][%s]: " - "built ok, expected a loud reject\n", - drivers[d].name, xrows[i].label); - fail++; - } - } - } - - /* byte-id rows: w6c vs w6c_ww .s must match (rule-10). */ - if (access(wdrv, X_OK) == 0) { - for (int i = 0; i < n; i++) { - if (!rows[i].byteid) continue; - total++; - if (asm_byte_identical(bin, &rows[i]) != 0) - fail++; - } - } - - if (fail) { - fprintf(stderr, - "tagged_subset_reject: %d/%d fixtures failed\n", - fail, total); - return 1; - } - printf("tagged_subset_reject: %d/%d ok\n", total, total); - return 0; -} diff --git a/test/xmod/typecheck_test.ww b/test/xmod/typecheck_test.ww new file mode 100644 index 00000000..3e2b608e --- /dev/null +++ b/test/xmod/typecheck_test.ww @@ -0,0 +1,253 @@ +package typecheck_test; + +// Cross-module assignability/reservation reject observers. Ports of +// the retired native carriers test/wcc/989_callarg_typecheck.c and +// test/wcc/989_tagged_subset_reject.c. +// +// callarg (#24) — the twelve ordinary call-arg rows are owned by the +// r989_callarg_* corpus fixtures (the carrier header records the +// split); the unowned remainder ported here: +// wwtests_int | user `const __wwtests: int` + @test fn under +// | `ww test` -> loud reject, BOTH stages; stderr +// | must carry "__wwtests is reserved by -T" (a +// | crash without the diagnostic FAILS, #20) +// wwtests_shadow | user __wwtests whose type MATCHES run()'s param +// | ([](str,*fn() void)) slips the (a) isassignable +// | teeth; only the (b) name reservation rejects +// shape_mismatch | []int passed where io2.handle=(file|stream) is +// | wanted -> "not assignable", BOTH stages (#24-B +// | shape-matched-lenient leg) +// same_shape | mod1.stream (*wbox) into io2.take -> cstage-ONLY +// | nominal reject; the wwstage over-accept is the +// | documented #10/#66/#37 nominal residual, +// | deliberately NOT asserted (dark until #37) +// +// tagged_subset (A7) — the three asm byte-id rows are owned by +// test-data-byteid over their corpus twins +// r989_tagsubset_{flatten_subset,slice_subset,nested_wrap}; the two +// cross-module leaf-bridge rows ported here (intrinsically two-module: +// one file spells every name bare, so typeeqast streq matches and the +// leaf bridge is never exercised): +// xmod_inline_forward | e.next's inline (i64|done) spelled BARE +// | forwards into qualified (i64|e.done): +// | build+run exit 0 both stages (dropping the +// | leaf bridge regresses cs=0/ww=1) +// xmod_inline_narrow | src carries `more`, absent from dst -> build +// | fails with "not assignable" on both stages +// | (missing diagnostic = crash, fails) +// +// Dropped C machinery, not assertions: the ww_ww-absent skip gate +// (the Make target declares both drivers) and the unlink/rmdir +// accounting (testenv.clean asserts the removal). + +import os; +import os.exec; +import strings; +import testenv; +import time; + +fn fail(label: str, why: str) void = { + let m: str = strings.concat("typecheck FAIL: ", label, " -- ", why, + "\n"); + os.write(2, m.ptr, m.len: u64); + assert(false); +}; + +fn tmo() time.duration = { + return (240i64 * (time.second: i64)): time.duration; +}; + +// ---- -T __wwtests reservation faces ------------------------------------ + +fn tface(label: str, src: str) void = { + let drvs: []str = ["ww", "ww_ww"]; + let i: i32 = 0; + for (i < 2) { + let td: str = testenv.fresh(); + let input: str = strings.concat(td, "/input.ww"); + testenv.writefile(input, src); + let av: []str = [testenv.driver(drvs[i]), "test", "-o", + strings.concat(td, "/prog"), input]; + let co: testenv.commandout; + testenv.runcommand(td, td, strings.concat("t_", drvs[i]), av, + tmo(), &co); + if (co.termination == exec.termination.EXIT && co.code == 0) { + fail(label, strings.concat(drvs[i], " test accepted ", + "(expected a loud reject)")); + }; + if (!testenv.has(co.stderr, "__wwtests is reserved by -T")) { + fail(label, strings.concat(drvs[i], " nonzero exit but ", + "missing reservation diagnostic (a crash, not a clean ", + "reject -- #20)")); + }; + testenv.clean(td); + i += 1; + }; +}; + +@test fn wwtests_int() void = { + tface("wwtests_int", strings.concat( + "package main;\n", + "const __wwtests: int = 99;\n", + "@test fn checkfoo() void = { return; };\n")); +}; + +@test fn wwtests_shadow() void = { + tface("wwtests_shadow", strings.concat( + "package main;\n", + "fn dummy() void = { return; };\n", + "const __wwtests: [](str, *fn() void) = [(\"x\", &dummy)];\n", + "@test fn checkfoo() void = { return; };\n")); +}; + +// ---- cross-module call-arg collision rows ------------------------------ + +fn io2src() str = { + return strings.concat( + "package io2;\n", + "export type vtable = struct { x: i32 };\n", + "export type stream = *vtable;\n", + "export type file = i32;\n", + "export type handle = (file | stream);\n", + "export fn take(h: handle) int = { return 7; };\n"); +}; + +// The build must FAIL with the "not assignable" diagnostic (a crash +// or a diag-less reject also fails, #20). +fn collidereject(label: str, drv: str, withmod1: bool, + mainbody: str) void = { + let td: str = testenv.fresh(); + assert(os.mkdir(strings.concat(td, "/io2"), 493) == 0); + testenv.writefile(strings.concat(td, "/io2/io2.ww"), io2src()); + let av: []str = []; + append(av, testenv.driver(drv)); + append(av, "build"); + append(av, "-I"); + append(av, strings.concat(td, "/io2")); + if (withmod1) { + assert(os.mkdir(strings.concat(td, "/mod1"), 493) == 0); + testenv.writefile(strings.concat(td, "/mod1/mod1.ww"), + strings.concat( + "package mod1;\n", + "export type wbox = struct { y: i64 };\n", + "export type stream = *wbox;\n", + "export fn mk() stream = { return nil; };\n")); + append(av, "-I"); + append(av, strings.concat(td, "/mod1")); + }; + testenv.writefile(strings.concat(td, "/main.ww"), mainbody); + append(av, "main.ww"); + let co: testenv.commandout; + testenv.runcommand(td, td, strings.concat("b_", drv), av, tmo(), &co); + if (co.termination == exec.termination.EXIT && co.code == 0) { + fail(label, strings.concat(drv, + " built ok, expected the collision arg to be rejected")); + }; + if (!testenv.has(co.stderr, "not assignable")) { + fail(label, strings.concat(drv, " reject lacked the `not ", + "assignable` diagnostic (a crash, not a clean reject)")); + }; + testenv.clean(td); +}; + +fn shapemismatchmain() str = { + return strings.concat( + "package main;\n", + "import io2;\n", + "export fn main() int = {\n", + " let xs: []int = [1, 2, 3];\n", + " return io2.take(xs);\n", + "};\n"); +}; + +@test fn collide_shape_mismatch() void = { + collidereject("collide_shape_mismatch", "ww", false, + shapemismatchmain()); + collidereject("collide_shape_mismatch", "ww_ww", false, + shapemismatchmain()); +}; + +// cstage-only: wwstage's AST-keyed isassignable over-accepts the +// same-leaf same-coarse-shape collision -- the tracked #10/#66/#37 +// nominal residual; a dual-stage row would be dark until #37 lands. +@test fn collide_same_shape() void = { + collidereject("collide_same_shape", "ww", true, strings.concat( + "package main;\n", + "import io2;\n", + "import mod1;\n", + "export fn main() int = {\n", + " let s: mod1.stream = mod1.mk();\n", + " return io2.take(s);\n", + "};\n")); +}; + +// ---- tagged-subset cross-module leaf-bridge rows ----------------------- + +fn xrow(label: str, esrc: str, msrc: str, expectbuild: bool) void = { + let drvs: []str = ["ww", "ww_ww"]; + let i: i32 = 0; + for (i < 2) { + let td: str = testenv.fresh(); + testenv.writefile(strings.concat(td, "/e.ww"), esrc); + testenv.writefile(strings.concat(td, "/main.ww"), msrc); + let av: []str = [testenv.driver(drvs[i]), "build", "-I", td, + strings.concat(td, "/main.ww")]; + let co: testenv.commandout; + testenv.runcommand(td, td, strings.concat("b_", drvs[i]), av, + tmo(), &co); + let built: bool = co.termination == exec.termination.EXIT + && co.code == 0; + if (expectbuild) { + if (!built) { + fail(label, strings.concat(drvs[i], " build failed ", + "(leaf bridge dropped? -- bare inline variant vs ", + "qualified consumer spelling)")); + }; + let rav: []str = [strings.concat(td, "/main")]; + let rc: testenv.commandout; + testenv.runcommand(td, td, strings.concat("r_", drvs[i]), + rav, tmo(), &rc); + if (rc.termination != exec.termination.EXIT || rc.code != 0) { + fail(label, strings.concat(drvs[i], " run-exit != 0")); + }; + } else { + if (built) { + fail(label, strings.concat(drvs[i], " built ok, ", + "expected a loud reject (lenient escape over-accept)")); + }; + if (!testenv.has(co.stderr, "not assignable")) { + fail(label, strings.concat(drvs[i], " reject lacked the ", + "`not assignable` diagnostic (a crash, not a clean ", + "reject)")); + }; + }; + testenv.clean(td); + i += 1; + }; +}; + +@test fn xmod_inline_forward() void = { + xrow("xmod_inline_forward", strings.concat( + "package e;\n", + "export type done = !i64;\n", + "export fn next() (i64 | done) = { return 0; };\n"), strings.concat( + "package main;\n", + "import e;\n", + "type myres = (i64 | e.done);\n", + "fn forward() myres = { return e.next(); };\n", + "export fn main() i32 = { return 0; };\n"), true); +}; + +@test fn xmod_inline_narrow() void = { + xrow("xmod_inline_narrow", strings.concat( + "package e;\n", + "export type done = !i64;\n", + "export type more = !i64;\n", + "export fn next() (i64 | done | more) = { return 0; };\n"), + strings.concat( + "package main;\n", + "import e;\n", + "type myres = (i64 | e.done);\n", + "fn forward() myres = { return e.next(); };\n", + "export fn main() i32 = { return 0; };\n"), false); +};