diff --git a/Makefile b/Makefile index f94c8ddc..c586ad12 100644 --- a/Makefile +++ b/Makefile @@ -537,6 +537,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \ $(BIN)/test_dyn_ww $(BIN)/test_selfcheck $(BIN)/test_at_test_ww \ $(BIN)/test_attest_record $(BIN)/test_attest_drop \ + $(BIN)/test_declns_sep \ $(BIN)/test_fmt_run $(BIN)/test_log_run $(BIN)/test_fnmatch_run \ $(BIN)/test_shlex_run $(BIN)/test_getenv_run $(BIN)/test_dirs_run \ $(BIN)/test_dirs_toolong_run \ @@ -2965,6 +2966,15 @@ $(BIN)/test_attest_drop: test/wcc/911_attest_drop.c $(BIN)/w6c $(BIN)/w6c_ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# C2c — #23/#30 decl-namespace (dup family + builtin carve-out via direct +# w6c/w6c_ww) + module-fn coexistence (dir-package `ww build --sep` + run, +# both driver stages + cross-order byte-id) → all six tools + drivers + +# libwwrt for the builtin_redecl link. +$(BIN)/test_declns_sep: test/wcc/989_declns_sep.c $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6a_ww \ + $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_fmt_run: test/wcc/970_fmt_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/test/wcc/989_declns_sep.c b/test/wcc/989_declns_sep.c new file mode 100644 index 00000000..00c12245 --- /dev/null +++ b/test/wcc/989_declns_sep.c @@ -0,0 +1,392 @@ +/* + * 989_declns_sep — #23/#30 declaration-namespace + module-fn coexistence + * under the directory-package model (task #83, M4 E2-C2c). + * + * Re-hosts every decl-namespace assertion that today lives ONLY in + * 910/997 onto a gate that survives the M4 flip (910/997 are deleted at + * the flip — they feed combined.ww / amalgamation inputs). Three legs: + * + * dup family — a duplicate top-level fn/type/def/let in one (flat) + * module must build-FAIL on BOTH stages. The fixtures + * are flat single-package (the surviving shape) and feed + * `w6c` / `w6c_ww` DIRECTLY — no driver resolution; the + * dup is an install-pass property, not a build-model one. + * 712 covered the cstage let arm; 997 covered the wwstage + * arm — this gate asserts BOTH stages reject every kind, + * closing the ww arm that dies with 997. + * builtin_redecl— the carve-out: a user redecl of a pre-seeded builtin + * name (`nomem`) is NOT a duplicate. cstage keeps no + * builtins in scope (lookup_builtin wins first), wwstage + * seeds them into c.top so installtop must DROP the redecl + * rather than error. Both stages accept + run to 7. + * modfn coexist — a top-level value-namespace `fn aa` and an imported + * MODULE `aa` coexist: bare `aa()` binds the fn (a module + * is not callable), `aa.helper()` binds through the module. + * RESHAPED from the dying multi-`package`-single-file + * 910/997 fixtures to dir-packages (696 precedent), built + * via `ww build --sep` + run. The _vbu twin flips the decl + * order (`fn aa` BEFORE `import aa`): cstage installs every + * SK_USE in an order-independent first pass, wwstage in + * source order, so vbu exercises a DISTINCT promote path — + * its asm must be byte-identical to the use-before-value + * order, the teeth against silent order-dependence. + * + * dup_xpkg (same leaf in distinct packages = legal) RIDES on 696/697's + * dir-package coverage — no port (spec C2c). + * + * Light wwstage-driver test (CLAUDE.md rule 14): every modfn intermediate + * is `-o`-redirected to /tmp, so it is phase-1 parallel-safe; the dup + + * builtin legs feed w6c directly with `-o /tmp`. Models 989_sepbuild_run + + * 911_attest_drop conventions; 989 prefix per the sep-gate precedent (the + * 7xx cgen/check range is exhausted). + */ +#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 +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; +} + +/* 0 if `needle` occurs in the file at `path`, else 1 (or -1 on read err). */ +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; +} + +/* + * dup family — direct-w6c/w6c_ww. Each fixture is a flat single-package + * with one duplicate top-level decl; both stages must build-FAIL, and the + * diagnostic must name the duplicate (non-vacuity: a fixture that failed + * for an unrelated reason would pass an exit-only check). cstage prefixes + * line:col, wwstage does not, so the asserted substring is the shared tail. + */ +static const struct { + const char *fixture; + const char *diag; /* shared "duplicate " substring */ +} dup_rows[] = { + { "test/wcc/data/dup_fn.ww", "duplicate fn foo" }, + { "test/wcc/data/dup_type.ww", "duplicate type t" }, + { "test/wcc/data/dup_def.ww", "duplicate def D" }, + { "test/wcc/data/dup_let.ww", "duplicate let g" }, +}; + +static int +dup_family(const char *bin) +{ + int pid = getpid(); + char err[256], cmd[4096]; + snprintf(err, sizeof err, "/tmp/declns_dup_%d.err", pid); + + int rc = 0; + const char *comps[] = { "w6c", "w6c_ww" }; + for (size_t r = 0; r < sizeof dup_rows / sizeof dup_rows[0]; r++) { + for (size_t c = 0; c < sizeof comps / sizeof comps[0]; c++) { + snprintf(cmd, sizeof cmd, "%s/%s %s -o /dev/null 2>%s", + bin, comps[c], dup_rows[r].fixture, err); + if (runwait(cmd) == 0) { + fprintf(stderr, "declns FAIL: %s accepted %s (expected " + "build-fail)\n", comps[c], dup_rows[r].fixture); + rc = 1; + } else if (file_contains(err, dup_rows[r].diag) != 0) { + fprintf(stderr, "declns FAIL: %s rejected %s but not on " + "`%s` (wrong reason)\n", comps[c], dup_rows[r].fixture, + dup_rows[r].diag); + rc = 1; + } + } + } + unlink(err); + return rc; +} + +/* + * builtin_redecl — the carve-out. Import-free + flat, so it feeds the + * direct w6c→w6a→w6l pipeline (mirrors 911's linkfail staging). The + * builtin `nomem` (= !void) wins, so g()'s i32 arm returns 7; both stages + * must accept AND run to that exact code. + */ +static const struct { + const char *comp; + const char *asmt; + const char *linkt; +} stages[] = { + { "w6c", "w6a", "w6l" }, + { "w6c_ww", "w6a_ww", "w6l_ww" }, +}; + +static int +builtin_redecl(const char *bin) +{ + int pid = getpid(); + char rt[1024], asmf[256], obj[256], exe[256], cmd[4096]; + snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin); + + int rc = 0; + for (size_t i = 0; i < sizeof stages / sizeof stages[0]; i++) { + snprintf(asmf, sizeof asmf, "/tmp/declns_br_%s_%d.s", stages[i].comp, pid); + snprintf(obj, sizeof obj, "/tmp/declns_br_%s_%d.o", stages[i].comp, pid); + snprintf(exe, sizeof exe, "/tmp/declns_br_%s_%d.exe", stages[i].comp, pid); + + int ok = 1; + snprintf(cmd, sizeof cmd, + "%s/%s test/wcc/data/builtin_redecl_ok.ww -o %s 2>/dev/null", + bin, stages[i].comp, asmf); + if (ok && runwait(cmd) != 0) { + fprintf(stderr, "declns FAIL: %s rejected builtin_redecl " + "(carve-out must accept)\n", stages[i].comp); + ok = 0; rc = 1; + } + snprintf(cmd, sizeof cmd, "%s/%s -o %s %s 2>/dev/null", + bin, stages[i].asmt, obj, asmf); + if (ok && runwait(cmd) != 0) { + fprintf(stderr, "declns FAIL: %s asm of builtin_redecl\n", + stages[i].asmt); + ok = 0; rc = 1; + } + snprintf(cmd, sizeof cmd, "%s/%s -o %s %s %s 2>/dev/null", + bin, stages[i].linkt, exe, obj, rt); + if (ok && runwait(cmd) != 0) { + fprintf(stderr, "declns FAIL: %s link of builtin_redecl\n", + stages[i].linkt); + ok = 0; rc = 1; + } + if (ok) { + int got = runwait(exe); + if (got != 7) { + fprintf(stderr, "declns FAIL: builtin_redecl (%s) " + "exit=%d want=7\n", stages[i].comp, got); + rc = 1; + } + } + unlink(asmf); unlink(obj); unlink(exe); + } + return rc; +} + +/* + * modfn coexist — the two dir-package layouts. Build each via `ww build + * --sep` + run, both driver stages, expect exit 6 (aa()=1 + aa.helper()=5). + * `ww build` (NOT `ww run`) per spec: run is not sep-wired until the driver + * flip. Returns 0 on success; leaves .sepwork in `td` for the byte-id + * compares below. + */ +struct modfn { const char *lay; const char *fixture; }; +static const struct modfn modfn_lays[] = { + { "coexist", "test/wcc/data/modfn_coexist/main.ww" }, + { "coexist_vbu", "test/wcc/data/modfn_coexist_vbu/main.ww" }, +}; + +/* per-package output the sep driver materializes; the root carries no .wwi + * (post-#69 it is compiled without -I), so its suffix set omits .wwi. */ +static const struct { + const char *pkg; + const char *suf; +} parts[] = { + { "aa", ".s" }, + { "aa", ".wwi" }, + { "aa", ".unit.ww" }, + { "__root", ".s" }, + { "__root", ".unit.ww" }, +}; + +static int +modfn_build(const char *bin, const char *td) +{ + char cmd[4096], prog[1024]; + int rc = 0; + for (size_t l = 0; l < sizeof modfn_lays / sizeof modfn_lays[0]; l++) { + for (size_t s = 0; s < sizeof stages / sizeof stages[0]; s++) { + const char *drv = (s == 0) ? "ww" : "ww_ww"; + snprintf(prog, sizeof prog, "%s/%s.%s", td, modfn_lays[l].lay, + (s == 0) ? "cs" : "ww"); + snprintf(cmd, sizeof cmd, "%s/%s build --sep -o %s %s 2>/dev/null", + bin, drv, prog, modfn_lays[l].fixture); + if (runwait(cmd) != 0) { + fprintf(stderr, "declns FAIL: %s build --sep %s\n", drv, + modfn_lays[l].lay); + rc = 1; + continue; + } + int got = runwait(prog); + if (got != 6) { + fprintf(stderr, "declns FAIL: modfn %s (%s) exit=%d want=6 " + "(coexistence mis-resolved)\n", modfn_lays[l].lay, drv, got); + rc = 1; + } + } + } + return rc; +} + +/* cs==ww (rule 10): for one layout, every per-package .s/.wwi/.unit.ww the + * cstage and wwstage sep drivers emitted must be byte-identical. */ +static int +modfn_cs_eq_ww(const char *td, const char *lay) +{ + int rc = 0; + for (size_t p = 0; p < sizeof parts / sizeof parts[0]; p++) { + char a[1024], b[1024]; + snprintf(a, sizeof a, "%s/%s.cs.sepwork/%s%s", td, lay, parts[p].pkg, parts[p].suf); + snprintf(b, sizeof b, "%s/%s.ww.sepwork/%s%s", td, lay, parts[p].pkg, parts[p].suf); + if (files_eq(a, b) != 0) { + fprintf(stderr, "declns FAIL: %s cs!=ww for %s%s (rule 10)\n", + lay, parts[p].pkg, parts[p].suf); + rc = 1; + } + } + return rc; +} + +/* cross-order byte-id: the value-before-use layout must emit asm/interface + * byte-identical to the use-before-value layout (same stage). Silent + * order-dependence in the SK_USE/value-sym promote is exactly what regresses; + * a divergence here is the teeth. */ +static int +modfn_order_id(const char *td) +{ + int rc = 0; + const char *tags[] = { "cs", "ww" }; + for (size_t t = 0; t < sizeof tags / sizeof tags[0]; t++) { + for (size_t p = 0; p < sizeof parts / sizeof parts[0]; p++) { + /* `.unit.ww` is the driver's verbatim source-assembly + * intermediate — it embeds the two layouts' differently + * ORDERED source text, so it differs by construction. The + * order-independence claim is about CODEGEN, so it is tested + * on the compiled `.s`/`.wwi` only. */ + if (strcmp(parts[p].suf, ".unit.ww") == 0) continue; + char a[1024], b[1024]; + snprintf(a, sizeof a, "%s/coexist.%s.sepwork/%s%s", + td, tags[t], parts[p].pkg, parts[p].suf); + snprintf(b, sizeof b, "%s/coexist_vbu.%s.sepwork/%s%s", + td, tags[t], parts[p].pkg, parts[p].suf); + if (files_eq(a, b) != 0) { + fprintf(stderr, "declns FAIL: vbu!=non-vbu for %s%s (%s) — " + "silent decl-order dependence\n", parts[p].pkg, + parts[p].suf, tags[t]); + rc = 1; + } + } + } + return rc; +} + +/* non-vacuity: __root.s must carry BOTH calls distinctly — through the + * module (`aa.helper`) and to the local value fn (`main.aa`) — proving the + * two namespaces resolved to different symbols rather than collapsing. */ +static int +modfn_coexist_present(const char *td) +{ + int rc = 0; + const char *tags[] = { "cs", "ww" }; + for (size_t t = 0; t < sizeof tags / sizeof tags[0]; t++) { + char rs[1024]; + snprintf(rs, sizeof rs, "%s/coexist.%s.sepwork/__root.s", td, tags[t]); + if (file_contains(rs, "CALL\taa.helper(SB)") != 0) { + fprintf(stderr, "declns FAIL: __root.s (%s) lacks module-qualified " + "`CALL aa.helper(SB)`\n", tags[t]); + rc = 1; + } + if (file_contains(rs, "CALL\tmain.aa(SB)") != 0) { + fprintf(stderr, "declns FAIL: __root.s (%s) lacks local-fn " + "`CALL main.aa(SB)`\n", tags[t]); + rc = 1; + } + } + return rc; +} + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) { fprintf(stderr, "declns FAIL: getcwd\n"); return 1; } + + int fail = 0; + fail += dup_family(bin); + fail += builtin_redecl(bin); + + char td[64], cmd[256]; + snprintf(td, sizeof td, "/tmp/wwdeclns_%d", getpid()); + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + mkdir(td, 0755); + + fail += modfn_build(bin, td); + fail += modfn_cs_eq_ww(td, "coexist"); + fail += modfn_cs_eq_ww(td, "coexist_vbu"); + fail += modfn_order_id(td); + fail += modfn_coexist_present(td); + + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + + if (fail) { + fprintf(stderr, "declns: %d check(s) failed\n", fail); + return 1; + } + printf("declns: dup fn/type/def/let build-fail (both stages) + " + "builtin_redecl accept→7 + modfn dir-pkg coexist→6 (cs==ww, " + "vbu byte-id to use-before-value), both stages (#23/#30)\n"); + return 0; +} diff --git a/test/wcc/data/modfn_coexist/aa/aa.ww b/test/wcc/data/modfn_coexist/aa/aa.ww new file mode 100644 index 00000000..fedf3da5 --- /dev/null +++ b/test/wcc/data/modfn_coexist/aa/aa.ww @@ -0,0 +1,5 @@ +// #30 dir-package — the imported module `aa`. Paired with ../main.ww, +// whose primary package also declares a value-namespace `fn aa`. Driver: +// the modfn_coexist leg of the decl-namespace gate (NNN_declns_sep.c). +package aa; +export fn helper() i32 = { return 5; }; diff --git a/test/wcc/data/modfn_coexist/main.ww b/test/wcc/data/modfn_coexist/main.ww new file mode 100644 index 00000000..22e939f5 --- /dev/null +++ b/test/wcc/data/modfn_coexist/main.ww @@ -0,0 +1,11 @@ +// #30 dir-package coexistence — the value-namespace `fn aa` and the +// imported MODULE `aa` (SK_USE) coexist. `aa()` binds the local fn (a +// module is not callable); `aa.helper()` binds through the module (the +// fn has no field `helper`). Both must resolve, so any mis-resolution +// fails to COMPILE; running to 1+5=6 proves both bind. The dir-package +// reshape of the dying single-file modfn_coexist_ok.ww (M4 amalgamation +// shape retired at the flip). +package main; +import aa; +fn aa() i32 = { return 1; }; +export fn main() i32 = { return aa() + aa.helper(); }; diff --git a/test/wcc/data/modfn_coexist_vbu/aa/aa.ww b/test/wcc/data/modfn_coexist_vbu/aa/aa.ww new file mode 100644 index 00000000..61486cbe --- /dev/null +++ b/test/wcc/data/modfn_coexist_vbu/aa/aa.ww @@ -0,0 +1,6 @@ +// #30 dir-package — the imported module `aa` for the VALUE-BEFORE-USE +// twin. Identical to ../../modfn_coexist/aa/aa.ww; kept as its own dir so +// the two layouts are independently buildable. Driver: the +// modfn_coexist_vbu leg of the decl-namespace gate (NNN_declns_sep.c). +package aa; +export fn helper() i32 = { return 5; }; diff --git a/test/wcc/data/modfn_coexist_vbu/main.ww b/test/wcc/data/modfn_coexist_vbu/main.ww new file mode 100644 index 00000000..f62990d6 --- /dev/null +++ b/test/wcc/data/modfn_coexist_vbu/main.ww @@ -0,0 +1,12 @@ +// #30 VALUE-BEFORE-USE — the twin of ../modfn_coexist/main.ww with the +// decl order flipped: `fn aa` is declared BEFORE `import aa`. cstage is +// order-independent (installs every SK_USE in a first pass); wwstage +// installs in source order, so this direction exercises a DISTINCT path +// (installdecl's N_USE arm must set use_alias on the pre-installed value +// sym). The gate asserts this builds + runs to 6 AND emits asm +// byte-identical to the use-before-value order — silent order-dependence +// is exactly what regresses. +package main; +fn aa() i32 = { return 1; }; +import aa; +export fn main() i32 = { return aa() + aa.helper(); };