diff --git a/Makefile b/Makefile index 54b84364..386816fd 100644 --- a/Makefile +++ b/Makefile @@ -234,7 +234,8 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_w6l $(BIN)/test_data_link \ $(BIN)/test_arch \ $(BIN)/test_e2e $(BIN)/test_ffi $(BIN)/test_dyn $(BIN)/test_stdlib \ - $(BIN)/test_at_test $(BIN)/test_let_global $(BIN)/test_def_neg_global \ + $(BIN)/test_at_test $(BIN)/test_selfimport \ + $(BIN)/test_let_global $(BIN)/test_def_neg_global \ $(BIN)/test_def_const_fold \ $(BIN)/test_int_cast_signed $(BIN)/test_dot_chain \ $(BIN)/test_amp_dot $(BIN)/test_arr_elem_field \ @@ -594,6 +595,10 @@ $(BIN)/test_at_test: test/wcc/910_at_test.c $(BIN)/ww $(BIN)/w6c $(BIN)/w6a \ $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_selfimport: test/wcc/948_selfimport.c $(BIN)/w6c $(BIN)/w6c_ww \ + | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_let_global: test/wcc/630_let_global.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< @@ -928,7 +933,7 @@ $(BIN)/test_cgreturn_variant_zero: test/wcc/707_cgreturn_variant_zero.c \ $(CC) $(CFLAGS) -o $@ $< $(BIN)/test_param_shadow_mod: test/wcc/708_param_shadow_mod.c \ - $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 1fc9681c..6097841d 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -2810,6 +2810,16 @@ check_file(Checker *c, Node *file) * walked in the next pass. */ for (Node *d = file->list; d; d = d->next) { if (d->kind == N_USE) { + /* check-(c) self-import: a package may not import + * itself. Pure owner==leaf string compare, package- + * model-independent — sound under ww's filename-keyed + * file-inclusion imports. check-(a) unused and + * (b)/(d) membership DEFERRED to task #8 (filename- + * keyed pulls lack import->file->symbol provenance). */ + const char *owner = decl_mod(file, d); + if (owner && owner[0] && strcmp(d->str, owner) == 0) + err(c, d->pos, "self-import: package " + "'%s' cannot import itself", owner); Sym *prev = scope_lookup_local(c->cur, d->str); if (prev != NULL) { /* Self-import: the driver concatenates the diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 719b952e..c76d62ba 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -10607,7 +10607,18 @@ fn installdecl(c: *checker, file: *node, d: *node) void = { let k: nkind = d.kind; let nm: str = d.str; let mod: str = declmod(file, d); - if (k == nkind.N_USE) { scopedefine(c.top, nm, skind.SK_USE, nil, d); return; }; + // check-(c) self-import: a package may not import itself. Pure + // owner==leaf string compare, package-model-independent. check-(a) + // unused + (b)/(d) membership DEFERRED to task #8 (filename-keyed + // pulls lack import->file->symbol provenance). Message byte-identical + // to cstage check.c. + if (k == nkind.N_USE) { + if (mod.len != 0 && streq(nm, mod)) { + cerr("self-import: package '"); cerr(mod); + cerr("' cannot import itself\n"); c.errs += 1i32; + }; + scopedefine(c.top, nm, skind.SK_USE, nil, d); return; + }; if (k == nkind.N_DEF) { scopedefineinmodule(c.top, nm, mod, skind.SK_DEF, nil, d); return; }; if (k == nkind.N_TYPEDECL) { scopedefineinmodule(c.top, nm, mod, skind.SK_TYPE, nil, d); return; }; if (k == nkind.N_FNDECL) { scopedefineinmodule(c.top, nm, mod, skind.SK_FN, nil, d); return; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 4ce4c3d7..acb4f276 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -275,7 +275,18 @@ fn installdecl(c: *checker, file: *node, d: *node) void = { let k: nkind = d.kind; let nm: str = d.str; let mod: str = declmod(file, d); - if (k == nkind.N_USE) { scopedefine(c.top, nm, skind.SK_USE, nil, d); return; }; + // check-(c) self-import: a package may not import itself. Pure + // owner==leaf string compare, package-model-independent. check-(a) + // unused + (b)/(d) membership DEFERRED to task #8 (filename-keyed + // pulls lack import->file->symbol provenance). Message byte-identical + // to cstage check.c. + if (k == nkind.N_USE) { + if (mod.len != 0 && streq(nm, mod)) { + cerr("self-import: package '"); cerr(mod); + cerr("' cannot import itself\n"); c.errs += 1i32; + }; + scopedefine(c.top, nm, skind.SK_USE, nil, d); return; + }; if (k == nkind.N_DEF) { scopedefineinmodule(c.top, nm, mod, skind.SK_DEF, nil, d); return; }; if (k == nkind.N_TYPEDECL) { scopedefineinmodule(c.top, nm, mod, skind.SK_TYPE, nil, d); return; }; if (k == nkind.N_FNDECL) { scopedefineinmodule(c.top, nm, mod, skind.SK_FN, nil, d); return; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index a43e77c4..5de3d27c 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -10607,7 +10607,18 @@ fn installdecl(c: *checker, file: *node, d: *node) void = { let k: nkind = d.kind; let nm: str = d.str; let mod: str = declmod(file, d); - if (k == nkind.N_USE) { scopedefine(c.top, nm, skind.SK_USE, nil, d); return; }; + // check-(c) self-import: a package may not import itself. Pure + // owner==leaf string compare, package-model-independent. check-(a) + // unused + (b)/(d) membership DEFERRED to task #8 (filename-keyed + // pulls lack import->file->symbol provenance). Message byte-identical + // to cstage check.c. + if (k == nkind.N_USE) { + if (mod.len != 0 && streq(nm, mod)) { + cerr("self-import: package '"); cerr(mod); + cerr("' cannot import itself\n"); c.errs += 1i32; + }; + scopedefine(c.top, nm, skind.SK_USE, nil, d); return; + }; if (k == nkind.N_DEF) { scopedefineinmodule(c.top, nm, mod, skind.SK_DEF, nil, d); return; }; if (k == nkind.N_TYPEDECL) { scopedefineinmodule(c.top, nm, mod, skind.SK_TYPE, nil, d); return; }; if (k == nkind.N_FNDECL) { scopedefineinmodule(c.top, nm, mod, skind.SK_FN, nil, d); return; }; diff --git a/test/wcc/708_param_shadow_mod.c b/test/wcc/708_param_shadow_mod.c index f778a496..2f967e90 100644 --- a/test/wcc/708_param_shadow_mod.c +++ b/test/wcc/708_param_shadow_mod.c @@ -25,13 +25,26 @@ * neg_forrange_tuple | `for (let (shadowmod, x) .. s)` | fail * neg_mcase | `match (r) { case let shadowmod ... }` | fail * pos_rename | rename param away from `shadowmod` | exit=42 + * neg_selfimp | `package selfimp; import selfimp;` | fail (both stages) + * pos_crossmod | param named like a CROSS-imported mod | exit=2 * - * Fixtures live in test/wcc/data/paramshadowmod/. Cstage-only: - * wwstage's check.ww runs only inside wwdump_ww (diagnostic), and - * the actual selfhost compile pipeline (994_w6c_ww) doesn't trip - * because wwstage's cgen takes the module-qualified emit path for - * any N_DOT-callee bare ident — see STATUS.md's `Wwstage no- - * checkfile-pass smell` note + #11 (deferred wwstage checkfile pass). + * Fixtures live in test/wcc/data/paramshadowmod/. The shadow-rule rows + * (the neg_ rows + pos_rename) are cstage-only: wwstage's check.ww runs + * inside wwdump_ww (diagnostic), and the actual selfhost compile + * pipeline (994_w6c_ww) doesn't trip because wwstage's cgen takes the + * module-qualified emit path for any N_DOT-callee bare ident — see + * STATUS.md's `Wwstage no-checkfile-pass smell` note. + * + * neg_selfimp / pos_crossmod (#16): pos_selfimp's old positive scenario + * (a self-import skipped from the shadow scan so a same-named param + * doesn't trip) is ABOLISHED — #16 check-(c) hard-rejects self-imports. + * neg_selfimp converts it: `package selfimp; import selfimp;` must be + * REJECTED by BOTH w6c and w6c_ww (compile-only, -o /dev/null — rule-14- + * safe, no wwstage driver) with the "self-import" diagnostic. pos_crossmod + * preserves the surviving shadow-TOLERANCE in legit form: src_imports + * filters by the binding's own module, so a param named like a module a + * SIBLING module imports (not this one) does NOT trip. The now-unreachable + * self-import-skip arm in check_module_shadow is task #13 (not removed here). */ #include #include @@ -95,33 +108,74 @@ run_pos(const char *driver, const char *fixdir) } /* - * pos_selfimp — same-module self-import. selfimp/selfimptest.ww - * carries `use selfimp;` from inside the module whose tag is also - * "selfimp" (matches lib/fmt/fmttest.ww's shape that surfaced the - * over-trigger originally). check_module_shadow's u->module == - * u->str skip must drop the directive from the import scan, so the - * `selfimp: str` param does NOT trip the rule. + * neg_selfimp — the bare self-import `package selfimp; import selfimp;` + * (selfimp/selfimptest.ww). #16 check-(c) hard-rejects it; both w6c and + * w6c_ww must fail with the "self-import" diagnostic. Compile-only + * (-o /dev/null): w6c/w6c_ww do not expand imports, but check-(c) fires + * at the N_USE install seam before resolution, so a lone file rejects + * directly — NOT a wwstage driver invocation, so rule-14-safe in 7xx. */ static int -run_pos_selfimp(const char *driver, const char *fixdir) +run_neg_selfimp(const char *comp, const char *fixdir, const char *tag) +{ + char errp[96], cmd[2048]; + snprintf(errp, sizeof errp, "/tmp/psm_selfimp_%s_%d.err", tag, getpid()); + snprintf(cmd, sizeof cmd, + "cd %s && %s selfimp/selfimptest.ww -o /dev/null 2>%s", + fixdir, comp, errp); + int rc = runwait(cmd); + if (rc == 0) { + fprintf(stderr, "param_shadow_mod[neg_selfimp-%s]: accepted " + "self-import (expected reject)\n", tag); + unlink(errp); + return 1; + } + FILE *f = fopen(errp, "rb"); + int found = 0; + if (f) { + char buf[4096]; + size_t n = fread(buf, 1, sizeof buf - 1, f); + fclose(f); + buf[n] = '\0'; + found = strstr(buf, "self-import") != NULL; + } + unlink(errp); + if (!found) { + fprintf(stderr, "param_shadow_mod[neg_selfimp-%s]: rejected " + "but no 'self-import' on stderr\n", tag); + return 1; + } + return 0; +} + +/* + * pos_crossmod — surviving shadow-TOLERANCE in legit form. The bundle + * imports `shadowmod` from module paramshadowmod and pulls module + * `crossmod`, whose probe() has a param named `shadowmod`. src_imports + * filters by the binding's own module, so crossmod's param does NOT trip + * (crossmod carries no `import shadowmod`) even though a sibling module + * imports that leaf. Build + run; exit = 2 (len "hi"). + */ +static int +run_pos_crossmod(const char *driver, const char *fixdir) { char cmd[2048]; snprintf(cmd, sizeof cmd, - "cd %s && %s build selfimp/selfimptest.ww >/dev/null 2>&1", + "cd %s && %s build pos_crossmod.ww >/dev/null 2>&1", fixdir, driver); if (runwait(cmd) != 0) { fprintf(stderr, - "param_shadow_mod[pos_selfimp]: build failed — " - "self-import skip regressed\n"); + "param_shadow_mod[pos_crossmod]: build failed — shadow " + "rule over-triggered on a cross-module param\n"); return 1; } char bin[2048]; - snprintf(bin, sizeof bin, "%s/selfimptest", fixdir); + snprintf(bin, sizeof bin, "%s/pos_crossmod", fixdir); int got = runwait(bin); unlink(bin); - if (got != 7) { + if (got != 2) { fprintf(stderr, - "param_shadow_mod[pos_selfimp]: exit=%d want=7\n", got); + "param_shadow_mod[pos_crossmod]: exit=%d want=2\n", got); return 1; } return 0; @@ -140,8 +194,10 @@ main(void) bin = absbin; } - char cdrv[1024]; + char cdrv[1024], w6c[1024], w6c_ww[1024]; snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); char fixdir[1024]; if (getcwd(fixdir, sizeof fixdir) == NULL) return 1; @@ -150,21 +206,27 @@ main(void) if (cwd_n + strlen(rel) + 1 >= sizeof fixdir) return 1; memcpy(fixdir + cwd_n, rel, strlen(rel) + 1); - int fail = 0; - fail += run_neg(cdrv, fixdir, "param"); - fail += run_neg(cdrv, fixdir, "let"); - fail += run_neg(cdrv, fixdir, "mlet"); - fail += run_neg(cdrv, fixdir, "forrange_single"); - fail += run_neg(cdrv, fixdir, "forrange_tuple"); - fail += run_neg(cdrv, fixdir, "mcase"); - fail += run_pos(cdrv, fixdir); - fail += run_pos_selfimp(cdrv, fixdir); + int fail = 0, total = 0; + total++; fail += run_neg(cdrv, fixdir, "param"); + total++; fail += run_neg(cdrv, fixdir, "let"); + total++; fail += run_neg(cdrv, fixdir, "mlet"); + total++; fail += run_neg(cdrv, fixdir, "forrange_single"); + total++; fail += run_neg(cdrv, fixdir, "forrange_tuple"); + total++; fail += run_neg(cdrv, fixdir, "mcase"); + total++; fail += run_pos(cdrv, fixdir); + /* neg_selfimp: both compilers reject the self-import (compile-only, + * rule-14-safe). w6c always; w6c_ww when built. */ + total++; fail += run_neg_selfimp(w6c, fixdir, "cstage"); + if (access(w6c_ww, X_OK) == 0) { + total++; fail += run_neg_selfimp(w6c_ww, fixdir, "wwstage"); + } + total++; fail += run_pos_crossmod(cdrv, fixdir); if (fail) { fprintf(stderr, "param_shadow_mod: %d row(s) failed\n", fail); return 1; } - printf("param_shadow_mod: 8/8 ok\n"); + printf("param_shadow_mod: %d/%d ok\n", total, total); return 0; } diff --git a/test/wcc/948_selfimport.c b/test/wcc/948_selfimport.c new file mode 100644 index 00000000..f3e25079 --- /dev/null +++ b/test/wcc/948_selfimport.c @@ -0,0 +1,89 @@ +/* + * 948_selfimport — #16 check-(c): a package may not import itself. + * + * Both compiler stages (w6c, w6c_ww) must loud-reject `package foo; + * import foo;` — nonzero exit AND a "self-import" diagnostic on stderr. + * Compile-only (`-o /dev/null`): w6c/w6c_ww do not expand imports (the + * driver does), so a lone fixture triggers check-(c) directly. This is + * NOT a ww_ww DRIVER test — it never invokes the driver — so it is + * Phase-1-safe and does not race the 990-997 byte-id gates (rule 14). + * + * check-(a) unused + (b)/(d) membership are DEFERRED to task #8 (ww's + * filename-keyed file-inclusion imports lack import->file->symbol + * provenance); only self-import is package-model-independent. + */ +#include +#include +#include +#include + +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; +} + +/* reject — ` -o /dev/null` must exit nonzero and print + * on stderr. */ +static int +reject(const char *bin, const char *comp, const char *fixture, + const char *substr) +{ + int pid = getpid(); + char errf[256], cmd[4096], line[4096]; + snprintf(errf, sizeof errf, "/tmp/si948_%s_%d.err", comp, pid); + snprintf(cmd, sizeof cmd, "%s/%s %s -o /dev/null 2>%s", + bin, comp, fixture, errf); + + int rc = system(cmd); + if (rc == 0) { + fprintf(stderr, "948 FAIL: %s accepted %s (expected reject)\n", + comp, fixture); + return 1; + } + + int found = 0; + FILE *f = fopen(errf, "r"); + if (f) { + while (fgets(line, sizeof line, f)) + if (strstr(line, substr)) { found = 1; break; } + fclose(f); + } + unlink(errf); + if (!found) { + fprintf(stderr, "948 FAIL: %s rejected %s but no '%s' on stderr\n", + comp, fixture, substr); + return 1; + } + return 0; +} + +static const struct { + const char *comp; + const char *fixture; + const char *substr; +} rows[] = { + { "w6c", "test/wcc/data/selfimport.ww", "self-import" }, + { "w6c_ww", "test/wcc/data/selfimport.ww", "self-import" }, +}; + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) { fprintf(stderr, "948 FAIL: getcwd\n"); return 1; } + + for (size_t i = 0; i < sizeof rows / sizeof rows[0]; i++) + if (reject(bin, rows[i].comp, rows[i].fixture, rows[i].substr) != 0) + return 1; + + printf("self-import rejected by w6c and w6c_ww\n"); + return 0; +} diff --git a/test/wcc/data/paramshadowmod/crossmod.ww b/test/wcc/data/paramshadowmod/crossmod.ww new file mode 100644 index 00000000..3174278e --- /dev/null +++ b/test/wcc/data/paramshadowmod/crossmod.ww @@ -0,0 +1,12 @@ +// crossmod — sibling module whose probe() takes a param named like the +// module `shadowmod` that paramshadowmod (NOT crossmod) imports. The +// shadow rule is filtered by the BINDING's own module (src_imports +// cur_mod filter): crossmod carries no `import shadowmod`, so this param +// must NOT trip even though a sibling module in the same bundle imports +// that leaf. Legit-form survivor of the abolished self-import tolerance. + +package crossmod; + +export fn probe(shadowmod: str) i32 = { + return shadowmod.len; +}; diff --git a/test/wcc/data/paramshadowmod/pos_crossmod.ww b/test/wcc/data/paramshadowmod/pos_crossmod.ww new file mode 100644 index 00000000..46454422 --- /dev/null +++ b/test/wcc/data/paramshadowmod/pos_crossmod.ww @@ -0,0 +1,17 @@ +// pos_crossmod — cur_mod-filtering positive. Replaces the abolished +// pos_selfimp (self-import is hard-rejected post-#16). This module +// (paramshadowmod) imports `shadowmod`, and the bundle also pulls module +// `crossmod`, whose probe() has a param named `shadowmod`. crossmod does +// NOT import shadowmod, so the shadow rule — filtered by the binding's +// own module — must NOT trip crossmod's param, even though a sibling +// module in the same bundle imports that leaf. Build + run; exit = 2. + +package paramshadowmod; + +import shadowmod; +import crossmod; + +export fn main() i32 = { + let _ = shadowmod.say(); + return crossmod.probe("hi"); +}; diff --git a/test/wcc/data/selfimport.ww b/test/wcc/data/selfimport.ww new file mode 100644 index 00000000..f341fa9d --- /dev/null +++ b/test/wcc/data/selfimport.ww @@ -0,0 +1,3 @@ +package foo; +import foo; +export fn main() int = { return 0; };