diff --git a/Makefile b/Makefile index 500b3726..5a4cf87f 100644 --- a/Makefile +++ b/Makefile @@ -354,15 +354,13 @@ BYTEID_WRAPPER_SOURCES = test/wcc/753_convwrap_audit.c \ test/wcc/930_free_noop_run.c \ test/wcc/946_structparam_run.c \ test/wcc/946_structret_run.c \ - test/wcc/989_m1mangle_sym.c \ - test/wcc/989_m2wwi_run.c \ - test/wcc/989_wwileaf_run.c + test/wcc/989_m1mangle_sym.c # Ww-native byte/artifact observers: single-file ww tests under -# test/byteid/ on the test/testenv helper package. Each drives both -# driver stages itself, so the build needs only cstage plus the two -# stage frontends it launches. -BYTEID_WW_TESTS = test/byteid/libbyteid_test.ww +# test/byteid/ on the test/testenv helper package. Each drives the +# stage tools it observes itself, so the build needs only cstage plus +# the wwstage binaries it launches. +BYTEID_WW_TESTS = test/byteid/libbyteid_test.ww test/byteid/wwi_test.ww BYTEID_WW_TARGETS = $(BYTEID_WW_TESTS:%=wwtest/%) BOOTSTRAP_WRAPPER_SOURCES = test/wcc/950_selfcheck.c \ test/wcc/991_w6a_ww.c \ @@ -463,7 +461,8 @@ $(LIBRARY_TEST_TARGETS): wwtest/%: $(BIN)/ww \ -I $(CURDIR)/lib/ww $* $(BYTEID_WW_TARGETS): wwtest/%: $(BIN)/ww $(BIN)/w6c $(BIN)/w6a \ - $(BIN)/w6l $(LIB)/libwwrt.a $(BIN)/ww_ww $(BIN)/w6c_ww + $(BIN)/w6l $(BIN)/wwdump $(LIB)/libwwrt.a \ + $(BIN)/ww_ww $(BIN)/w6c_ww @echo "ww test $*" @mkdir -p $(WWBUILD)/wwtest/$(subst /,_,$*) @WW_TEST_REPO=$(CURDIR) $(BIN)/ww test \ diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index c0879619..b38bdd95 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -36,10 +36,12 @@ The live declarative compiler corpus has 1,495 fixtures and 2,990 C/WW cells: 338 expected rejections (314 shared and 24 stage-specific), 17 compile-only successes, 198 exit-zero programs, and 942 explicit-exit programs. -133 native C carriers remain. They are partitioned exactly once as five -in-process units, 10 byte/artifact gates, six bootstrap gates, one platform +131 native C carriers remain. They are partitioned exactly once as five +in-process units, 8 byte/artifact gates, six bootstrap gates, one platform gate, and 111 residual compiler, package-layout, ABI, diagnostic-observer, -driver, linker, or FFI gates. Rows migrated to fixtures or native `@test` +driver, linker, or FFI gates. The `.wwi` round-trip observers are +ww-native (`test/byteid/wwi_test.ww`, replacing `989_m2wwi_run` and +`989_wwileaf_run`). Rows migrated to fixtures or native `@test` owners were removed from those carriers; there is no compatibility execution path for retired rows. 74 former byte/artifact carriers whose only assertion was a cstage-vs-wwstage `.s` byte-compare of sources now living in the diff --git a/test/byteid/wwi_test.ww b/test/byteid/wwi_test.ww new file mode 100644 index 00000000..aa062b99 --- /dev/null +++ b/test/byteid/wwi_test.ww @@ -0,0 +1,304 @@ +package wwi_test; + +// .wwi export-data observers. Ports of the retired native carriers +// test/wcc/989_m2wwi_run.c and test/wcc/989_wwileaf_run.c; every +// assertion preserved. +// +// m2* — the M2 `.wwi` producer gate (task #22 arc). Both stages (rule +// 10): the `w6c -I ` producer IS the live import path, and +// `.wwi` is a cross-stage byte-id substrate pinned directly. POSITIVE: +// for ascii/strings/getopt (drew2-audited leak-free), drive the target +// as the PRIMARY module of a driver-combined unit, then (1) w6c -I and +// w6c_ww -I both succeed, (2) cs.wwi == ww.wwi byte-for-byte, (3) the +// emitted .wwi re-parses (wwdump -a exit 0). getopt is the recursion +// stressor. A synth fixture covers the decl-kinds + type-nodes no lib +// package reaches (def const-expr fold, let global, [N]T, fn-ptr, !T, +// tuple, storage-less enum, and the #47 @symbol round-trip); the types +// gate proves the exported limit defs reach the interface. NEGATIVE: an +// exported fn naming a private nominal must be LOUD-REJECTED by +// check_exported_type identically on both stages (nonzero exit + +// byte-identical diagnostic) — without it a vacuous no-op check would +// pass the positive gate silently. +// +// wwileaf — BUG-C (#11) regression pin: a decl-less / export-less +// primary module's `.wwi` `package` line must carry the module's real +// leaf, NOT the literal default "main". Table-driven over the three +// decl-less shapes routing the wwi_emit fallback (empty, comment-only, +// nested dotted path a.b.c -> leaf c). Each row drives the REAL +// producer->importer flow on BOTH stages: (a) the dep `.wwi` package +// line equals the real leaf, (b) the dep `.wwi` is byte-identical +// across stages, (c) a root importing the dep RESOLVES on both stages +// and the two importer `.s` are byte-identical. The composed units are +// fed straight to w6c / w6c_ww (not `ww build`): the producer-unit +// shape `//ww:module-reset ` + body is exactly what the driver's +// sep_emit_body emits. + +import os; +import os.exec; +import strings; +import testenv; +import time; + +fn fail(label: str, why: str) void = { + let m: str = strings.concat("wwi FAIL: ", label, " -- ", why, "\n"); + os.write(2, m.ptr, m.len: u64); + assert(false); +}; + +fn lifetime() time.duration = { + return (180i64 * (time.second: i64)): time.duration; +}; + +// Run argv in `dir`, true iff normal exit 0. +fn runok(dir: str, name: str, argv: []str) bool = { + let co: testenv.commandout; + testenv.runcommand(dir, dir, name, argv, lifetime(), &co); + return co.termination == exec.termination.EXIT && co.code == 0; +}; + +fn firstline(path: str) str = { + let body: str = testenv.readfile(path); + let nl: i32 = testenv.pos(body, "\n"); + if (nl < 0) { return body; }; + return strings.sub(body, 0, nl); +}; + +// one positive package: build the target as primary, produce the `.wwi` +// on both stages, require byte-id + re-parse. +fn m2positive(pkg: str) void = { + let td: str = testenv.fresh(); + let srcbase: str = strings.concat(pkg, ".ww"); + testenv.writefile(strings.concat(td, "/", srcbase), + testenv.readfile(strings.concat(testenv.repo(), "/lib/", pkg, + "/", pkg, ".ww"))); + + // #94 sep layout: drive the package file as the build target so its + // decls are the PRIMARY module of the resolved ROOT unit. -S stops + // after the compiler outputs; the unit is the only artifact consumed + // here. The build's own exit code is deliberately not asserted — the + // resolved-unit check below is the gate (carrier-faithful). + let av: []str = []; + append(av, testenv.driver("ww")); + append(av, "build"); + append(av, "-S"); + append(av, "-I"); + append(av, strings.concat(testenv.repo(), "/lib")); + append(av, "-o"); + append(av, strings.concat(td, "/", pkg)); + append(av, srcbase); + let co: testenv.commandout; + testenv.runcommand(td, td, "drv", av, lifetime(), &co); + + let comb: str = strings.concat(td, "/", pkg, + ".sepwork/__root.unit.ww"); + if (!testenv.exists(comb)) { fail(pkg, "no resolved unit"); }; + + let cs: str = strings.concat(td, "/cs.wwi"); + let ws: str = strings.concat(td, "/ww.wwi"); + let cav: []str = [testenv.driver("w6c"), "-I", cs, comb]; + if (!runok(td, "w6c", cav)) { fail(pkg, "w6c -I rejected"); }; + let wav: []str = [testenv.driver("w6c_ww"), "-I", ws, comb]; + if (!runok(td, "w6c_ww", wav)) { fail(pkg, "w6c_ww -I rejected"); }; + + if (!testenv.same(testenv.readfile(cs), testenv.readfile(ws))) { + fail(pkg, "cs.wwi != ww.wwi (byte-id broken on the .wwi substrate)"); + }; + + // re-parse: the emitted `.wwi` must be valid ww prototype source. + let dav: []str = [testenv.driver("wwdump"), "-a", cs]; + if (!runok(td, "wwdump", dav)) { + fail(pkg, "emitted .wwi does not re-parse (wwdump -a)"); + }; + testenv.clean(td); +}; + +@test fn m2ascii() void = { m2positive("ascii"); }; +@test fn m2strings() void = { m2positive("strings"); }; +@test fn m2getopt() void = { m2positive("getopt"); }; + +// synth: decl-kinds + type-AST nodes the lib packages do not cover — +// `export def` (const-expr unparser, folded 10+2*3, unary -7), `export +// let` global, `[N]T` array (decl + array-dim ident-ref), fn-ptr type, +// `!T`, tuple return, storage-less enum, and the #47 @symbol +// round-trip. `chan T` omitted: the wwstage parser does not yet accept +// it in a return position (pre-existing, unrelated to M2). +@test fn m2synth() void = { + let td: str = testenv.fresh(); + let src: str = strings.concat( + "package synth;\n", + "export type color = enum { RED, GREEN = 5, BLUE };\n", + "export def LIMIT: i32 = 10 + 2 * 3;\n", + "export def NAME: str = \"hi\\n\";\n", + "export def FLAG: bool = true;\n", + "export def NEG: i32 = -7;\n", + "export let counter: i32;\n", + "export let grid: [4]i32;\n", + "export fn apply(f: fn(x: i32) i32, n: i32) i32;\n", + "export fn risky() !i32;\n", + "export fn matrix() [LIMIT]u8;\n", + "export fn pair() (i32, i32);\n", + "export fn opt(p: *color, b: []u8) (i32 | void);\n", + "@symbol(\"rt_ext\") export fn ext(n: i32) i32;\n"); + let p: str = strings.concat(td, "/synth.ww"); + testenv.writefile(p, src); + let cs: str = strings.concat(td, "/cs.wwi"); + let ws: str = strings.concat(td, "/ww.wwi"); + // self-contained package (no imports) -> its own primary unit, so + // w6c -I runs directly on the source (no driver-combined step). + let cav: []str = [testenv.driver("w6c"), "-I", cs, p]; + if (!runok(td, "w6c", cav)) { fail("synth", "w6c -I rejected"); }; + let wav: []str = [testenv.driver("w6c_ww"), "-I", ws, p]; + if (!runok(td, "w6c_ww", wav)) { fail("synth", "w6c_ww -I rejected"); }; + if (!testenv.same(testenv.readfile(cs), testenv.readfile(ws))) { + fail("synth", + "cs.wwi != ww.wwi (const-expr / decl-kind unparse diverges)"); + }; + // #47: the @symbol attribute must survive the round-trip verbatim. + if (!testenv.has(testenv.readfile(cs), + "@symbol(\"rt_ext\") export fn ext")) { + fail("synth", ".wwi dropped @symbol (wrong link symbol under sep)"); + }; + let dav: []str = [testenv.driver("wwdump"), "-a", cs]; + if (!runok(td, "wwdump", dav)) { + fail("synth", "emitted .wwi does not re-parse (wwdump -a)"); + }; + testenv.clean(td); +}; + +// #48 gate: the lib/types limit constants are `export def`s. Produce +// types.wwi on both stages directly from the real lib source, assert +// byte-id + re-parse + that the exported limits actually appear (a +// non-exported def is omitted, so this proves the export reached the +// interface a cross-package sep-compile consumer reads). +@test fn m2types() void = { + let td: str = testenv.fresh(); + let src: str = strings.concat(testenv.repo(), "/lib/types/types.ww"); + let cs: str = strings.concat(td, "/cs.wwi"); + let ws: str = strings.concat(td, "/ww.wwi"); + let cav: []str = [testenv.driver("w6c"), "-I", cs, src]; + if (!runok(td, "w6c", cav)) { fail("types", "w6c -I rejected"); }; + let wav: []str = [testenv.driver("w6c_ww"), "-I", ws, src]; + if (!runok(td, "w6c_ww", wav)) { fail("types", "w6c_ww -I rejected"); }; + if (!testenv.same(testenv.readfile(cs), testenv.readfile(ws))) { + fail("types", "cs.wwi != ww.wwi"); + }; + let body: str = testenv.readfile(cs); + if (!testenv.has(body, "export def I32_MAX") + || !testenv.has(body, "export def RUNE_MAX")) { + fail("types", "limit consts not exported into the .wwi"); + }; + let dav: []str = [testenv.driver("wwdump"), "-a", cs]; + if (!runok(td, "wwdump", dav)) { + fail("types", "emitted .wwi does not re-parse (wwdump -a)"); + }; + testenv.clean(td); +}; + +// negative: an exported fn naming a private nominal must be rejected +// identically (exit + diagnostic) by BOTH stages. +@test fn m2negative() void = { + let td: str = testenv.fresh(); + let src: str = strings.concat( + "package leaktest;\n", + "type secret = struct { x: i32 };\n", + "export fn leaks(s: secret) i32 = { return s.x; };\n", + "export fn clean(a: i32) i32 = { return a; };\n"); + testenv.writefile(strings.concat(td, "/leak.ww"), src); + + // relative leak.ww under td so both stages report the bare + // `leak.ww:L:C:` prefix. + let cav: []str = [testenv.driver("w6c"), "-I", "out.wwi", "leak.ww"]; + let cco: testenv.commandout; + testenv.runcommand(td, td, "cs", cav, lifetime(), &cco); + let wav: []str = [testenv.driver("w6c_ww"), "-I", "out.wwi", "leak.ww"]; + let wco: testenv.commandout; + testenv.runcommand(td, td, "ws", wav, lifetime(), &wco); + + let csok: bool = cco.termination == exec.termination.EXIT + && cco.code == 0; + let wsok: bool = wco.termination == exec.termination.EXIT + && wco.code == 0; + if (csok || wsok) { + fail("negative", "check_exported_type is vacuous (a stage accepted the private-type leak)"); + }; + if (!testenv.same(cco.stderr, wco.stderr)) { + fail("negative", "reject diagnostics differ across stages (rule-10)"); + }; + testenv.clean(td); +}; + +// ---- wwileaf (#11) ----------------------------------------------------- + +fn leafrow(tag: str, path: str, leaf: str, body: str, want: str) void = { + let td: str = testenv.fresh(); + let prod: str = strings.concat(td, "/", tag, ".prod.ww"); + let cswwi: str = strings.concat(td, "/", tag, ".cs.wwi"); + let wwwwi: str = strings.concat(td, "/", tag, ".ww.wwi"); + let css: str = strings.concat(td, "/", tag, ".cs.s"); + let wws: str = strings.concat(td, "/", tag, ".ww.s"); + let root: str = strings.concat(td, "/", tag, ".root.ww"); + let rcss: str = strings.concat(td, "/", tag, ".rcs.s"); + let rwws: str = strings.concat(td, "/", tag, ".rww.s"); + + // the producer unit — exactly the driver's sep_emit_body shape. + testenv.writefile(prod, strings.concat( + "//ww:module-reset ", path, "\n", + "package ", leaf, ";\n", + body)); + + // Leg a — both stages emit the dep `.wwi`; the `package` leaf is + // the module's real leaf, not the default "main". + let cav: []str = [testenv.driver("w6c"), "-c", "-I", cswwi, + "-o", css, prod]; + if (!runok(td, "csprod", cav)) { fail(tag, "w6c producer errored"); }; + let wav: []str = [testenv.driver("w6c_ww"), "-c", "-I", wwwwi, + "-o", wws, prod]; + if (!runok(td, "wsprod", wav)) { fail(tag, "w6c_ww producer errored"); }; + let wantline: str = strings.concat("package ", want, ";"); + if (!testenv.same(firstline(cswwi), wantline)) { + fail(tag, ".wwi package line is not the real leaf (BUG-C)"); + }; + + // Leg b — the dep `.wwi` is byte-identical across stages (rule 10). + if (!testenv.same(testenv.readfile(cswwi), testenv.readfile(wwwwi))) { + fail(tag, "w6c vs w6c_ww .wwi differ (rule 10)"); + }; + + // Leg c — a root importing the dep RESOLVES on both stages + // (pre-fix: "package main does not match import path" REJECT). The + // dep `.wwi` is prepended under //ww:module , exactly as the + // driver composes an importer unit. + testenv.writefile(root, strings.concat( + "//ww:module ", path, "\n", + testenv.readfile(cswwi), "\n", + "//ww:module-reset\n", + "package main;\n", + "import ", path, ";\n", + "export fn main() i32 = { return 42; };\n")); + let rcav: []str = [testenv.driver("w6c"), "-c", "-o", rcss, root]; + if (!runok(td, "csroot", rcav)) { + fail(tag, "w6c rejected the import (BUG-C)"); + }; + let rwav: []str = [testenv.driver("w6c_ww"), "-c", "-o", rwws, root]; + if (!runok(td, "wsroot", rwav)) { + fail(tag, "w6c_ww rejected the import (BUG-C)"); + }; + if (!testenv.same(testenv.readfile(rcss), testenv.readfile(rwws))) { + fail(tag, "importer .s differ cs vs ww (rule 10)"); + }; + testenv.clean(td); +}; + +// Each shape lacks any module-tagged decl, so wwi_emit's decl-scan +// misses and the leaf must come from the parse-stamped N_FILE identity. +@test fn wwileaf_empty() void = { + leafrow("empty", "emptymod", "emptymod", "", "emptymod"); +}; + +@test fn wwileaf_comment() void = { + leafrow("comment", "cmod", "cmod", "// only a comment\n", "cmod"); +}; + +@test fn wwileaf_nested() void = { + leafrow("nested", "a.b.c", "c", "", "c"); +}; diff --git a/test/wcc/989_m2wwi_run.c b/test/wcc/989_m2wwi_run.c deleted file mode 100644 index 36c295df..00000000 --- a/test/wcc/989_m2wwi_run.c +++ /dev/null @@ -1,429 +0,0 @@ -/* - * 989_m2wwi_run — M2 `.wwi` export-data producer gate (task #22 arc). - * Both stages (rule-10): since the sep flip the `w6c -I ` - * producer IS the live import path, and `.wwi` is a cross-stage - * byte-id substrate this gate pins directly. - * - * POSITIVE gate — for each package (ascii/strings/getopt, drew2-audited - * leak-free), drive the target as the PRIMARY module of a driver-combined - * unit, then: - * 1. w6c -I cs.wwi and w6c_ww -I ww.wwi both succeed, - * 2. cs.wwi == ww.wwi byte-for-byte (the new substrate's determinism), - * 3. cs.wwi RE-PARSES under the existing parser (wwdump -a exits 0) — - * proves the producer emits genuinely re-parseable ww prototype - * source, de-risking the M3 consumer. - * getopt is the recursion stressor (enum→struct→struct, ptr-to-nominal, - * tagged-union return, qualified pkg.Name). A synth fixture then covers the - * decl-kinds + type-nodes no lib package reaches: `def` (const-expr unparser - * + fold parity), `let` global, `[N]T` array, `fn(..)R` fn-ptr, `!T`, tuple, - * storage-less enum. - * - * NEGATIVE gate — a fixture whose exported fn names a PRIVATE (non- - * exported) nominal must be LOUD-REJECTED by check_exported_type, - * identically on BOTH stages (same non-zero exit, byte-identical - * diagnostic). Without it a vacuous no-op check would pass the positive - * gate silently. - * - * Compile/produce/cmp only — no driver run, everything under a getpid-keyed - * /tmp directory with no source-tree writes. 9xx is full; shares the 989 prefix per the 989_lib_byteid - * precedent (the `short` name keys the binary). - */ -#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 const char * -absbin(void) -{ - const char *b = getenv("BIN"); - if (!b) b = "out/bin"; - if (b[0] == '/') return b; - static char buf[2048]; - char cwd[1024]; - if (getcwd(cwd, sizeof cwd) == NULL) return NULL; - snprintf(buf, sizeof buf, "%s/%s", cwd, b); - return buf; -} - -static int -slurp(const char *path, char **outbuf, size_t *outlen) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - fseek(f, 0, SEEK_END); - long n = ftell(f); - fseek(f, 0, SEEK_SET); - if (n < 0) { fclose(f); return -1; } - char *b = malloc((size_t)n + 1); - if (!b) { fclose(f); return -1; } - if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } - b[n] = '\0'; - fclose(f); - *outbuf = b; - *outlen = (size_t)n; - return 0; -} - -static int -file_contains(const char *path, const char *needle) -{ - char *b = NULL; - size_t n = 0; - if (slurp(path, &b, &n) < 0) return -1; - int found = (strstr(b, needle) != NULL); - free(b); - return found ? 0 : 1; -} - -static int -files_eq(const char *a, const char *b) -{ - char *ba = NULL, *bb = NULL; - size_t na = 0, nb = 0; - if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) { - free(ba); free(bb); - return -1; - } - int eq = (na == nb && memcmp(ba, bb, na) == 0); - free(ba); free(bb); - return eq ? 0 : 1; -} - -/* one positive package: build the target as primary, produce the `.wwi` - * on both stages, require byte-id + re-parse. Returns 0 on pass. */ -static int -positive(const char *bin, const char *cwd, const char *pkg, int idx) -{ - char td[64], cmd[4096], src[1024]; - int rc = -1; - - snprintf(td, sizeof td, "/tmp/wwm2_%d_%d", getpid(), idx); - if (mkdir(td, 0755) != 0) { - perror(td); - return -1; - } - - snprintf(src, sizeof src, "%s/lib/%s/%s.ww", cwd, pkg, pkg); - snprintf(cmd, sizeof cmd, "cp %s %s/%s.ww", src, td, pkg); - if (runwait(cmd) != 0) { - fprintf(stderr, "m2wwi FAIL: %s — cp\n", pkg); - goto out; - } - - /* #94 sep layout: drive the package file as the build target → - * its decls are the PRIMARY module of the resolved ROOT unit, which the - * producer's primary filter narrows to exactly this package's - * interface. The root unit's amalgamated source is composed at - * .sepwork/__root.unit.ww before codegen. `-S` stops after the - * compiler outputs; the unit is the only artifact consumed here. All - * generated files stay under td. */ - snprintf(cmd, sizeof cmd, - "cd %s && timeout 180 %s/ww build -S " - "-I %s/lib -o %s/%s %s.ww >/dev/null 2>&1", - td, bin, cwd, td, pkg, pkg); - runwait(cmd); - - char comb[1100], cs[1100], ws[1100]; - snprintf(comb, sizeof comb, "%s/%s.sepwork/__root.unit.ww", td, pkg); - snprintf(cs, sizeof cs, "%s/cs.wwi", td); - snprintf(ws, sizeof ws, "%s/ww.wwi", td); - if (access(comb, 0) != 0) { - fprintf(stderr, "m2wwi FAIL: %s — no resolved unit\n", pkg); - goto out; - } - - snprintf(cmd, sizeof cmd, - "timeout 180 %s/w6c -I %s %s >/dev/null 2>&1", bin, cs, comb); - if (runwait(cmd) != 0) { - fprintf(stderr, "m2wwi FAIL: %s — w6c -I rejected\n", pkg); - goto out; - } - snprintf(cmd, sizeof cmd, - "timeout 180 %s/w6c_ww -I %s %s >/dev/null 2>&1", bin, ws, comb); - if (runwait(cmd) != 0) { - fprintf(stderr, "m2wwi FAIL: %s — w6c_ww -I rejected\n", pkg); - goto out; - } - - if (files_eq(cs, ws) != 0) { - fprintf(stderr, "m2wwi FAIL: %s — cs.wwi != ww.wwi (byte-id " - "broken on the .wwi substrate)\n", pkg); - goto out; - } - - /* re-parse: the emitted `.wwi` must be valid ww prototype source. */ - snprintf(cmd, sizeof cmd, - "timeout 180 %s/wwdump -a %s >/dev/null 2>&1", bin, cs); - if (runwait(cmd) != 0) { - fprintf(stderr, "m2wwi FAIL: %s — emitted .wwi does not " - "re-parse (wwdump -a)\n", pkg); - goto out; - } - rc = 0; -out: - snprintf(cmd, sizeof cmd, "rm -rf %s", td); - if (runwait(cmd) != 0) { - fprintf(stderr, "m2wwi: cleanup rm -rf %s failed\n", td); - if (rc == 0) rc = -1; - } - return rc; -} - -/* negative gate: an exported fn naming a private nominal must be rejected - * identically (exit + diagnostic) by BOTH stages. Returns 0 on pass. */ -static const char *leak_src = - "package leaktest;\n" - "type secret = struct { x: i32 };\n" - "export fn leaks(s: secret) i32 = { return s.x; };\n" - "export fn clean(a: i32) i32 = { return a; };\n"; - -static int -negative(const char *bin) -{ - char td[64], cmd[4096], p[512]; - int rc = -1; - - snprintf(td, sizeof td, "/tmp/wwm2neg_%d", getpid()); - if (mkdir(td, 0755) != 0) { - perror(td); - return -1; - } - - snprintf(p, sizeof p, "%s/leak.ww", td); - FILE *f = fopen(p, "wb"); - if (!f) goto out; - fputs(leak_src, f); - fclose(f); - - char cse[1100], wwe[1100]; - snprintf(cse, sizeof cse, "%s/cs.err", td); - snprintf(wwe, sizeof wwe, "%s/ww.err", td); - - /* cd into td so both stages report the bare `leak.ww:L:C:` prefix. */ - snprintf(cmd, sizeof cmd, - "cd %s && timeout 180 %s/w6c -I out.wwi leak.ww >/dev/null 2>cs.err", - td, bin); - int crc = runwait(cmd); - snprintf(cmd, sizeof cmd, - "cd %s && timeout 180 %s/w6c_ww -I out.wwi leak.ww >/dev/null 2>ww.err", - td, bin); - int wrc = runwait(cmd); - - if (crc == 0 || wrc == 0) { - fprintf(stderr, "m2wwi FAIL: negative — check_exported_type is " - "vacuous (cstage exit=%d wwstage exit=%d; both must reject " - "the private-type leak)\n", crc, wrc); - goto out; - } - if (files_eq(cse, wwe) != 0) { - fprintf(stderr, "m2wwi FAIL: negative — reject diagnostics " - "differ across stages (rule-10)\n"); - goto out; - } - rc = 0; -out: - snprintf(cmd, sizeof cmd, "rm -rf %s", td); - if (runwait(cmd) != 0) { - fprintf(stderr, "m2wwi: cleanup rm -rf %s failed\n", td); - if (rc == 0) rc = -1; - } - return rc; -} - -/* synth gate: a single self-contained package exercising the decl-kinds + - * type-AST nodes the lib packages above do NOT cover — `export def` (the - * const-expr unparser, incl a folded `10+2*3` and a unary `-7`), `export - * let` (global), `[N]T` array (decl + array-dim ident-ref), an `fn(...) R` - * fn-pointer type, an `!T` error type, a `(a, b)` tuple return, and a - * storage-less `enum {...}`. Asserts both stages produce a byte-identical, - * re-parseable `.wwi` (the const-expr fold + tokname spelling must agree - * cross-stage — paths the lib gate never reaches). `chan T` is omitted: the - * wwstage parser does not yet accept it in a return position (pre-existing, - * unrelated to M2), so the producer's N_TCHAN arm is unreachable there. - * Returns 0 on pass. */ -static const char *synth_src = - "package synth;\n" - "export type color = enum { RED, GREEN = 5, BLUE };\n" - "export def LIMIT: i32 = 10 + 2 * 3;\n" - "export def NAME: str = \"hi\\n\";\n" - "export def FLAG: bool = true;\n" - "export def NEG: i32 = -7;\n" - "export let counter: i32;\n" - "export let grid: [4]i32;\n" - "export fn apply(f: fn(x: i32) i32, n: i32) i32;\n" - "export fn risky() !i32;\n" - "export fn matrix() [LIMIT]u8;\n" - "export fn pair() (i32, i32);\n" - "export fn opt(p: *color, b: []u8) (i32 | void);\n" - /* #47: a codegen/link-relevant attribute (@symbol — the FFI link- - * symbol override) MUST round-trip into the `.wwi`; dropping it makes - * sep-compile emit `CALL ext` for a `@symbol("rt_ext")` fn. */ - "@symbol(\"rt_ext\") export fn ext(n: i32) i32;\n"; - -static int -synth(const char *bin) -{ - char td[64], cmd[4096], p[512]; - int rc = -1; - - snprintf(td, sizeof td, "/tmp/wwm2syn_%d", getpid()); - if (mkdir(td, 0755) != 0) { - perror(td); - return -1; - } - - snprintf(p, sizeof p, "%s/synth.ww", td); - FILE *f = fopen(p, "wb"); - if (!f) goto out; - fputs(synth_src, f); - fclose(f); - - char cs[1100], ws[1100]; - snprintf(cs, sizeof cs, "%s/cs.wwi", td); - snprintf(ws, sizeof ws, "%s/ww.wwi", td); - - /* self-contained package (no imports) → it is its own primary unit, - * so w6c -I runs directly on the source (no driver-combined step). */ - snprintf(cmd, sizeof cmd, - "timeout 180 %s/w6c -I %s %s/synth.ww >/dev/null 2>&1", bin, cs, td); - if (runwait(cmd) != 0) { - fprintf(stderr, "m2wwi FAIL: synth — w6c -I rejected\n"); - goto out; - } - snprintf(cmd, sizeof cmd, - "timeout 180 %s/w6c_ww -I %s %s/synth.ww >/dev/null 2>&1", bin, ws, td); - if (runwait(cmd) != 0) { - fprintf(stderr, "m2wwi FAIL: synth — w6c_ww -I rejected\n"); - goto out; - } - if (files_eq(cs, ws) != 0) { - fprintf(stderr, "m2wwi FAIL: synth — cs.wwi != ww.wwi (const-expr " - "/ decl-kind unparse diverges across stages)\n"); - goto out; - } - /* #47: the @symbol attribute must survive the round-trip verbatim. */ - if (file_contains(cs, "@symbol(\"rt_ext\") export fn ext") != 0) { - fprintf(stderr, "m2wwi FAIL: synth — .wwi dropped @symbol " - "(sep-compile would emit the wrong link symbol)\n"); - goto out; - } - snprintf(cmd, sizeof cmd, - "timeout 180 %s/wwdump -a %s >/dev/null 2>&1", bin, cs); - if (runwait(cmd) != 0) { - fprintf(stderr, "m2wwi FAIL: synth — emitted .wwi does not " - "re-parse (wwdump -a)\n"); - goto out; - } - rc = 0; -out: - snprintf(cmd, sizeof cmd, "rm -rf %s", td); - if (runwait(cmd) != 0) { - fprintf(stderr, "m2wwi: cleanup rm -rf %s failed\n", td); - if (rc == 0) rc = -1; - } - return rc; -} - -/* #48 gate: the lib/types limit constants are `export def`s (Hare exports - * types::I32_MAX &c). Produce types.wwi on both stages directly from the - * real lib source (no imports → its own primary unit), assert byte-id + - * re-parse + that the exported limits actually appear in the .wwi (a non- - * exported def is omitted, so this proves the export reached the interface - * a cross-package sep-compile consumer reads). Returns 0 on pass. */ -static int -typesexport(const char *bin, const char *cwd) -{ - char td[64], cmd[4096], src[1024]; - int rc = -1; - - snprintf(td, sizeof td, "/tmp/wwm2typ_%d", getpid()); - if (mkdir(td, 0755) != 0) { - perror(td); - return -1; - } - - snprintf(src, sizeof src, "%s/lib/types/types.ww", cwd); - char cs[1100], ws[1100]; - snprintf(cs, sizeof cs, "%s/cs.wwi", td); - snprintf(ws, sizeof ws, "%s/ww.wwi", td); - - snprintf(cmd, sizeof cmd, - "timeout 180 %s/w6c -I %s %s >/dev/null 2>&1", bin, cs, src); - if (runwait(cmd) != 0) { - fprintf(stderr, "m2wwi FAIL: types — w6c -I rejected\n"); - goto out; - } - snprintf(cmd, sizeof cmd, - "timeout 180 %s/w6c_ww -I %s %s >/dev/null 2>&1", bin, ws, src); - if (runwait(cmd) != 0) { - fprintf(stderr, "m2wwi FAIL: types — w6c_ww -I rejected\n"); - goto out; - } - if (files_eq(cs, ws) != 0) { - fprintf(stderr, "m2wwi FAIL: types — cs.wwi != ww.wwi\n"); - goto out; - } - if (file_contains(cs, "export def I32_MAX") != 0 - || file_contains(cs, "export def RUNE_MAX") != 0) { - fprintf(stderr, "m2wwi FAIL: types — limit consts not exported " - "into the .wwi (cross-package sep-compile would not see " - "types.I32_MAX)\n"); - goto out; - } - snprintf(cmd, sizeof cmd, - "timeout 180 %s/wwdump -a %s >/dev/null 2>&1", bin, cs); - if (runwait(cmd) != 0) { - fprintf(stderr, "m2wwi FAIL: types — emitted .wwi does not " - "re-parse (wwdump -a)\n"); - goto out; - } - rc = 0; -out: - snprintf(cmd, sizeof cmd, "rm -rf %s", td); - if (runwait(cmd) != 0) { - fprintf(stderr, "m2wwi: cleanup rm -rf %s failed\n", td); - if (rc == 0) rc = -1; - } - return rc; -} - -int -main(void) -{ - const char *bin = absbin(); - if (!bin) return 1; - char cwd[1024]; - if (getcwd(cwd, sizeof cwd) == NULL) return 1; - - const char *pkgs[] = { "ascii", "strings", "getopt", NULL }; - int fail = 0, npos = 0; - for (int i = 0; pkgs[i]; i++) { - if (positive(bin, cwd, pkgs[i], i) != 0) fail++; - else npos++; - } - if (synth(bin) != 0) fail++; - else npos++; - if (typesexport(bin, cwd) != 0) fail++; - else npos++; - if (negative(bin) != 0) fail++; - - if (fail) { - fprintf(stderr, "m2wwi: %d check(s) failed\n", fail); - return 1; - } - printf("m2wwi: %d package(s) byte-identical + re-parse; negative " - "gate rejects (both stages)\n", npos); - return 0; -} diff --git a/test/wcc/989_wwileaf_run.c b/test/wcc/989_wwileaf_run.c deleted file mode 100644 index 20c0fb2c..00000000 --- a/test/wcc/989_wwileaf_run.c +++ /dev/null @@ -1,276 +0,0 @@ -/* - * 989_wwileaf_run — BUG-C (#11) regression pin: a decl-less / export-less - * primary module's `.wwi` `package` line must carry the module's real leaf, - * NOT the literal default "main". - * - * THE BUG: wwi_emit derived the `package` leaf by scanning for the first - * PRIMARY decl bearing a module tag. A fully empty (or comment-only, or - * export-less) body has NO such decl, so the scan fell through and the leaf - * stayed the seed default "main". The dep's `.wwi` then read `package main;` - * → when its importer composed the dep under `//ww:module `, the - * in-`.wwi` `package main` clause was validated against the import path and - * REJECTED: "package main does not match import path " - * (cmd/wcc/parse.c, both stages). A decl-less module was simply unimportable. - * - * THE FIX (cmd/w6c/wwi.c + selfhost/cmd/wcc/wwi.ww + the two parse sites): - * parse stamps the primary module identity on the N_FILE node (module/nmod, - * "only if empty", never on the imported TK_MODPATH boundary nor the bare - * root reset); wwi_emit falls back to that identity's leaf when the decl-scan - * finds nothing (`!found` — NOT pkg=="main", so a body whose first tagged - * decl legitimately leafs to "main" is unaffected and both stages decide - * identically). A real root `package main` arrives via a bare reset, leaves - * the stamp empty, and stays "main". - * - * Table-driven over the three decl-less shapes that all route the fallback: - * an empty body, a comment-only body, and a NESTED dotted path (a.b.c → leaf - * c, exercising the leaf-slice). Each row drives the REAL producer→importer - * flow on BOTH stages: - * (a) the dep `.wwi` `package` line == the real leaf (pre-fix: "main"); - * (b) the dep `.wwi` is byte-identical w6c vs w6c_ww (rule 10); - * (c) a root that `import`s the dep RESOLVES on both stages (pre-fix: - * hard REJECT) and the two importer `.s` are byte-identical (rule 10). - * - * The composed units are fed straight to w6c / w6c_ww (not `ww build`): the - * producer-unit shape `//ww:module-reset ` + body is exactly what the - * driver's sep_emit_body emits, so this pins the same path without the - * driver's filesystem layout. Non-vacuity: neutering the wwi_emit fallback - * reddens row (a) ("package main;") and row (c) (the import REJECT). - * - * Light wwstage-driver test (CLAUDE.md rule 14): every intermediate lands in - * a private mkdtemp dir, rm -rf'd at exit — no /tmp scratch leak (task #8). - * 989 prefix per the sep-gate precedent; twin of 989_modresetadj_run (#9). - */ -#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 const char * -absbin(void) -{ - const char *b = getenv("BIN"); - if (!b) b = "out/bin"; - if (b[0] == '/') return b; - static char buf[2048]; - char cwd[1024]; - if (getcwd(cwd, sizeof cwd) == NULL) return NULL; - snprintf(buf, sizeof buf, "%s/%s", cwd, b); - return buf; -} - -static int -slurp(const char *path, char **outbuf, size_t *outlen) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - fseek(f, 0, SEEK_END); - long n = ftell(f); - fseek(f, 0, SEEK_SET); - if (n < 0) { fclose(f); return -1; } - char *b = malloc((size_t)n + 1); - if (!b) { fclose(f); return -1; } - if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } - b[n] = '\0'; - fclose(f); - *outbuf = b; - *outlen = (size_t)n; - return 0; -} - -static int -files_eq(const char *a, const char *b) -{ - char *ba = NULL, *bb = NULL; - size_t na = 0, nb = 0; - if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) { - free(ba); free(bb); - return -1; - } - int eq = (na == nb && memcmp(ba, bb, na) == 0); - free(ba); free(bb); - return eq ? 0 : 1; -} - -static int -write_file(const char *path, const char *content) -{ - FILE *f = fopen(path, "wb"); - if (!f) return -1; - fputs(content, f); - fclose(f); - return 0; -} - -/* First physical line of `path` (newline stripped) into `out`. */ -static int -first_line(const char *path, char *out, size_t outsz) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - if (fgets(out, (int)outsz, f) == NULL) { fclose(f); out[0] = '\0'; } - fclose(f); - size_t n = strlen(out); - if (n && out[n - 1] == '\n') out[n - 1] = '\0'; - return 0; -} - -struct row { - const char *tag; /* scratch-file stem */ - const char *path; /* dotted module path on the //ww:module-reset */ - const char *leaf; /* the in-file `package ;` clause */ - const char *body; /* body after the clause (zero module-tagged decls) */ - const char *want; /* expected emitted `package ;` leaf */ -}; - -/* Each shape lacks any module-tagged decl, so wwi_emit's decl-scan misses - * and the leaf must come from the parse-stamped N_FILE identity. */ -static const struct row rows[] = { - { "empty", "emptymod", "emptymod", "", "emptymod" }, - { "comment", "cmod", "cmod", "// only a comment\n", "cmod" }, - { "nested", "a.b.c", "c", "", "c" }, -}; -#define NROW ((int)(sizeof rows / sizeof rows[0])) -#define RET 42 - -static int -run_row(const char *bin, const char *dir, const struct row *r) -{ - char prod[1024], cswwi[1024], wwwwi[1024], css[1024], wws[1024]; - char root[1024], rcss[1024], rwws[1024], unit[8192], cmd[16384]; - - snprintf(prod, sizeof prod, "%s/%s.prod.ww", dir, r->tag); - snprintf(cswwi, sizeof cswwi, "%s/%s.cs.wwi", dir, r->tag); - snprintf(wwwwi, sizeof wwwwi, "%s/%s.ww.wwi", dir, r->tag); - snprintf(css, sizeof css, "%s/%s.cs.s", dir, r->tag); - snprintf(wws, sizeof wws, "%s/%s.ww.s", dir, r->tag); - snprintf(root, sizeof root, "%s/%s.root.ww", dir, r->tag); - snprintf(rcss, sizeof rcss, "%s/%s.rcs.s", dir, r->tag); - snprintf(rwws, sizeof rwws, "%s/%s.rww.s", dir, r->tag); - - /* the producer unit — exactly the driver's sep_emit_body shape. */ - snprintf(unit, sizeof unit, - "//ww:module-reset %s\n" - "package %s;\n" - "%s", - r->path, r->leaf, r->body); - if (write_file(prod, unit) != 0) { - fprintf(stderr, "wwileaf[%s]: cannot write producer unit\n", r->tag); - return 1; - } - - /* Leg a — both stages emit the dep `.wwi`; the `package` leaf is the - * module's real leaf, not the default "main". */ - snprintf(cmd, sizeof cmd, - "%s/w6c -c -I %s -o %s %s 2>/dev/null", bin, cswwi, css, prod); - if (runwait(cmd) != 0) { - fprintf(stderr, "wwileaf[%s] FAIL: w6c producer errored\n", r->tag); - return 1; - } - snprintf(cmd, sizeof cmd, - "%s/w6c_ww -c -I %s -o %s %s 2>/dev/null", bin, wwwwi, wws, prod); - if (runwait(cmd) != 0) { - fprintf(stderr, "wwileaf[%s] FAIL: w6c_ww producer errored\n", r->tag); - return 1; - } - char want[256], line[512]; - snprintf(want, sizeof want, "package %s;", r->want); - if (first_line(cswwi, line, sizeof line) != 0 || strcmp(line, want) != 0) { - fprintf(stderr, - "wwileaf[%s] FAIL: .wwi package line '%s' want '%s' (BUG-C)\n", - r->tag, line, want); - return 1; - } - - /* Leg b — the dep `.wwi` is byte-identical across stages (rule 10). */ - if (files_eq(cswwi, wwwwi) != 0) { - fprintf(stderr, - "wwileaf[%s] FAIL: w6c vs w6c_ww .wwi differ (rule 10)\n", r->tag); - return 1; - } - - /* Leg c — a root importing the dep RESOLVES on both stages (pre-fix: - * "package main does not match import path %s" REJECT). The dep `.wwi` - * is prepended under //ww:module , exactly as the driver composes - * an importer unit. */ - { - char *wwi = NULL; - size_t wlen = 0; - if (slurp(cswwi, &wwi, &wlen) < 0) { - fprintf(stderr, "wwileaf[%s]: cannot read dep .wwi\n", r->tag); - return 1; - } - snprintf(unit, sizeof unit, - "//ww:module %s\n" - "%s\n" - "//ww:module-reset\n" - "package main;\n" - "import %s;\n" - "export fn main() i32 = { return %d; };\n", - r->path, wwi, r->path, RET); - free(wwi); - } - if (write_file(root, unit) != 0) { - fprintf(stderr, "wwileaf[%s]: cannot write importer unit\n", r->tag); - return 1; - } - snprintf(cmd, sizeof cmd, - "%s/w6c -c -o %s %s 2>/dev/null", bin, rcss, root); - if (runwait(cmd) != 0) { - fprintf(stderr, - "wwileaf[%s] FAIL: w6c rejected the import (BUG-C)\n", r->tag); - return 1; - } - snprintf(cmd, sizeof cmd, - "%s/w6c_ww -c -o %s %s 2>/dev/null", bin, rwws, root); - if (runwait(cmd) != 0) { - fprintf(stderr, - "wwileaf[%s] FAIL: w6c_ww rejected the import (BUG-C)\n", r->tag); - return 1; - } - if (files_eq(rcss, rwws) != 0) { - fprintf(stderr, - "wwileaf[%s] FAIL: importer .s differ cs vs ww (rule 10)\n", - r->tag); - return 1; - } - return 0; -} - -int -main(void) -{ - const char *bin = absbin(); - if (!bin) return 1; - - char dir[] = "/tmp/wwileaf_XXXXXX"; - if (mkdtemp(dir) == NULL) { - fprintf(stderr, "wwileaf: mkdtemp failed\n"); - return 1; - } - - int fail = 0; - for (int i = 0; i < NROW; i++) - fail |= run_row(bin, dir, &rows[i]); - - char rm[2048]; - snprintf(rm, sizeof rm, "rm -rf %s", dir); - if (runwait(rm) != 0) { - fprintf(stderr, "wwileaf: cleanup rm -rf %s failed\n", dir); - fail = 1; - } - - if (fail) return 1; - printf("wwileaf: %d decl-less shapes — .wwi leaf derived, byte-id, " - "import resolves cs==ww (#11)\n", NROW); - return 0; -}