wcc/check: reject self-import, both stages (#16 ENFORCE-checker)

check-(c): a package importing itself (any spelling) is a hard error,
mirroring Go. Predicate is leaf==owner at the N_USE/installdecl seam —
sound only after the PREP commits (dotted-test renames, package-less
boundary directive). Identical wording both stages; diagnostics-only,
byte-id-neutral. 948 pins the reject in both compilers; 708's
pos_selfimp (which pinned the abolished self-import skip) converts to
neg_selfimp + new pos_crossmod preserving the param-shadow tolerance
the case existed for. Checks (a) unused and (b)/(d) name-membership
stay deferred to the multi-package arc: imports are filename-keyed
pulls, so those need import->file provenance this compiler lacks.
This commit is contained in:
2026-06-10 15:18:58 +09:00
parent 49a5173f3f
commit 9f8df525c2
10 changed files with 266 additions and 35 deletions

View File

@@ -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 $@ $<

View File

@@ -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

View File

@@ -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; };

View File

@@ -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; };

View File

@@ -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; };

View File

@@ -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 <stdio.h>
#include <stdlib.h>
@@ -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;
}

89
test/wcc/948_selfimport.c Normal file
View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
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 — `<comp> <fixture> -o /dev/null` must exit nonzero and print
* <substr> 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;
}

View File

@@ -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;
};

View File

@@ -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");
};

View File

@@ -0,0 +1,3 @@
package foo;
import foo;
export fn main() int = { return 0; };