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}).
This commit is contained in:
2
Makefile
2
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 \
|
||||
|
||||
@@ -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 `<comp> -T <combined>` 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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
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 `<comp> -T <combined>` 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: `<drv> 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;
|
||||
}
|
||||
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
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;
|
||||
}
|
||||
253
test/xmod/typecheck_test.ww
Normal file
253
test/xmod/typecheck_test.ww
Normal file
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user