cstage+selfhost+test: refuse let/param shadow of imported module (#19)
When `use fmt;` is in scope and a local/param named `fmt` shadows it, `fmt.X` in the body silently resolved to the str-typed value sym and emitted `CALL AX` through str.ptr → runtime crash. Surfaced during #15 (lib/log's printfln family); worked around by renaming the param `fmt`→`format`. Per rob + user, option (C): "value names and module names are disjoint." Refuse the shadow at the decl site. Single rule, no non-local reasoning, no silent footgun if a future lib/X exports a new leaf. cstage: src_imports walks file->list for N_USE entries (skipping self-imports where u->module == u->str — same-module fixtures like lib/fmt/fmttest.ww carry these); check_module_shadow runs before each SK_PARAM / SK_VAR scope_define (param, clet, mlet, forrange single + tuple, mcase). Wwstage mirror in check.ww; wwdump-only diagnostic today, full enforcement waits on #11 checkfile pass. Bootstrap byte-id holds — no codegen change. One source patch in selfhost/cmd/w6a/main.ww renames an outer `let asm: asm_;` to `s` to sidestep task #27 (cstage localoff scope-blind dedup); unrelated to #19 but the new rule's first run flagged it as a self-shadow. Test 708 (param_shadow_mod): 4 rows — neg_param (param shadow errs at fn decl line), neg_let (let shadow errs at let decl), pos_rename (rename compiles + runs), pos_selfimp (in-module use is skipped). 4 wired sites without dedicated rows deferred to task #28. Follow-up: lib/log can revert format→fmt now that the silent crash is impossible.
This commit is contained in:
163
test/wcc/708_param_shadow_mod.c
Normal file
163
test/wcc/708_param_shadow_mod.c
Normal file
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 706_param_shadow_mod — "value names and module names are disjoint."
|
||||
*
|
||||
* Pre-fix (task #19): a fn param / local-let named `shadowmod` while
|
||||
* the same source carried `use shadowmod;` would compile cleanly and
|
||||
* silently miscompile any `shadowmod.X` body lookup — cstage's cexpr
|
||||
* N_DOT path resolved the inner ident through the shadow's value
|
||||
* bits, then emitted CALL through the str's .ptr field. Symptom in
|
||||
* the field was a SIGSEGV inside lib/log's lprintfln (worked around
|
||||
* by renaming `fmt: str` → `format: str` at commit 6b6d7dd).
|
||||
*
|
||||
* Post-fix: cstage check.c (and wwstage check.ww, run by wwdump_ww)
|
||||
* refuse the bind at the decl site with a `<kind> '<name>' shadows
|
||||
* imported module '<name>'` diagnostic. Same-leaf top-level decls
|
||||
* (e.g. `use fnmatch; fn fnmatch(...)`) are exempt — the rule fires
|
||||
* only for nested-scope binds whose declaring source file imports
|
||||
* the module.
|
||||
*
|
||||
* row | shape | gate
|
||||
* -----------+--------------------------------------+--------------
|
||||
* neg_param | `use shadowmod; fn p(shadowmod: str)`| must fail
|
||||
* neg_let | `use shadowmod; ... let shadowmod` | must fail
|
||||
* pos_rename | rename param away from `shadowmod` | must succeed,
|
||||
* exit = 42
|
||||
*
|
||||
* 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).
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.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;
|
||||
}
|
||||
|
||||
static int
|
||||
run_neg(const char *driver, const char *fixdir, const char *tag)
|
||||
{
|
||||
char src[64];
|
||||
snprintf(src, sizeof src, "neg_%s.ww", tag);
|
||||
char cmd[2048];
|
||||
/* cd into the fixture dir so the driver's source-dir-first
|
||||
* import search resolves `use shadowmod;`. */
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"cd %s && %s build %s >/dev/null 2>&1", fixdir, driver, src);
|
||||
int rc = runwait(cmd);
|
||||
if (rc == 0) {
|
||||
fprintf(stderr,
|
||||
"param_shadow_mod[neg_%s]: build unexpectedly succeeded "
|
||||
"— shadow rule did not fire\n", tag);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
run_pos(const char *driver, const char *fixdir)
|
||||
{
|
||||
char cmd[2048];
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"cd %s && %s build pos_rename.ww >/dev/null 2>&1",
|
||||
fixdir, driver);
|
||||
if (runwait(cmd) != 0) {
|
||||
fprintf(stderr,
|
||||
"param_shadow_mod[pos_rename]: build failed — rule "
|
||||
"over-triggered on the rename\n");
|
||||
return 1;
|
||||
}
|
||||
char bin[2048];
|
||||
snprintf(bin, sizeof bin, "%s/pos_rename", fixdir);
|
||||
int got = runwait(bin);
|
||||
unlink(bin);
|
||||
if (got != 42) {
|
||||
fprintf(stderr,
|
||||
"param_shadow_mod[pos_rename]: exit=%d want=42\n", got);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
static int
|
||||
run_pos_selfimp(const char *driver, const char *fixdir)
|
||||
{
|
||||
char cmd[2048];
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"cd %s && %s build selfimp/selfimptest.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");
|
||||
return 1;
|
||||
}
|
||||
char bin[2048];
|
||||
snprintf(bin, sizeof bin, "%s/selfimptest", fixdir);
|
||||
int got = runwait(bin);
|
||||
unlink(bin);
|
||||
if (got != 7) {
|
||||
fprintf(stderr,
|
||||
"param_shadow_mod[pos_selfimp]: exit=%d want=7\n", got);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
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];
|
||||
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
|
||||
|
||||
char fixdir[1024];
|
||||
if (getcwd(fixdir, sizeof fixdir) == NULL) return 1;
|
||||
size_t cwd_n = strlen(fixdir);
|
||||
const char *rel = "/test/wcc/data/paramshadowmod";
|
||||
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_pos(cdrv, fixdir);
|
||||
fail += run_pos_selfimp(cdrv, fixdir);
|
||||
|
||||
if (fail) {
|
||||
fprintf(stderr,
|
||||
"param_shadow_mod: %d row(s) failed\n", fail);
|
||||
return 1;
|
||||
}
|
||||
printf("param_shadow_mod: 4/4 ok\n");
|
||||
return 0;
|
||||
}
|
||||
10
test/wcc/data/paramshadowmod/neg_let.ww
Normal file
10
test/wcc/data/paramshadowmod/neg_let.ww
Normal file
@@ -0,0 +1,10 @@
|
||||
// neg_let — local `let shadowmod: i32 = ...` shadows the imported
|
||||
// module from inside a fn body. Same rule fires for nested-scope
|
||||
// let binds, not just params.
|
||||
|
||||
use shadowmod;
|
||||
|
||||
export fn main() i32 = {
|
||||
let shadowmod: i32 = 0i32;
|
||||
return shadowmod;
|
||||
};
|
||||
13
test/wcc/data/paramshadowmod/neg_param.ww
Normal file
13
test/wcc/data/paramshadowmod/neg_param.ww
Normal file
@@ -0,0 +1,13 @@
|
||||
// neg_param — fn param `shadowmod: str` shadows the imported module.
|
||||
// Under the "value names and module names are disjoint" rule the
|
||||
// build must fail with a clear diagnostic at the param decl site.
|
||||
|
||||
use shadowmod;
|
||||
|
||||
fn probe(shadowmod: str) i32 = {
|
||||
return shadowmod.len;
|
||||
};
|
||||
|
||||
export fn main() i32 = {
|
||||
return probe("hi");
|
||||
};
|
||||
14
test/wcc/data/paramshadowmod/pos_rename.ww
Normal file
14
test/wcc/data/paramshadowmod/pos_rename.ww
Normal file
@@ -0,0 +1,14 @@
|
||||
// pos_rename — positive case. The fn param is renamed away from the
|
||||
// imported module's bareword, so the rule doesn't fire and the body
|
||||
// can call `shadowmod.say()` cleanly. Built + run; exit code = 42.
|
||||
|
||||
use shadowmod;
|
||||
|
||||
fn probe(s: str) i32 = {
|
||||
let _ = s;
|
||||
return shadowmod.say();
|
||||
};
|
||||
|
||||
export fn main() i32 = {
|
||||
return probe("hi");
|
||||
};
|
||||
7
test/wcc/data/paramshadowmod/selfimp/selfimp.ww
Normal file
7
test/wcc/data/paramshadowmod/selfimp/selfimp.ww
Normal file
@@ -0,0 +1,7 @@
|
||||
// pos_selfimp/selfimp.ww — minimal "module" body. The interesting
|
||||
// scenario lives in the sibling selfimptest.ww file, which carries
|
||||
// `use selfimp;` from inside the same module.
|
||||
|
||||
export fn touch() i32 = {
|
||||
return 0i32;
|
||||
};
|
||||
19
test/wcc/data/paramshadowmod/selfimp/selfimptest.ww
Normal file
19
test/wcc/data/paramshadowmod/selfimp/selfimptest.ww
Normal file
@@ -0,0 +1,19 @@
|
||||
// pos_selfimp/selfimptest.ww — same-module self-import case. This
|
||||
// file's MODULE tag is "selfimp" (parent dir basename), and it
|
||||
// carries `use selfimp;` — exactly the lib/fmt/fmttest.ww shape that
|
||||
// originally surfaced check_module_shadow's over-trigger on
|
||||
// `fn bsprintf(... fmt: str, ...)`.
|
||||
//
|
||||
// src_imports' self-import skip (u->module == u->str) drops these
|
||||
// entries from the import scan, so the param `selfimp: str` here
|
||||
// must NOT be flagged as shadowing — build + run, exit = 7.
|
||||
|
||||
use selfimp;
|
||||
|
||||
fn probe(selfimp: str) i32 = {
|
||||
return selfimp.len;
|
||||
};
|
||||
|
||||
export fn main() i32 = {
|
||||
return probe("regress");
|
||||
};
|
||||
7
test/wcc/data/paramshadowmod/shadowmod/shadowmod.ww
Normal file
7
test/wcc/data/paramshadowmod/shadowmod/shadowmod.ww
Normal file
@@ -0,0 +1,7 @@
|
||||
// paramshadowmod/shadowmod — a tiny module the negative/positive
|
||||
// fixtures import as `use shadowmod;`. Carries one fn so the leaf
|
||||
// resolves through the module dot path when name resolution succeeds.
|
||||
|
||||
export fn say() i32 = {
|
||||
return 42i32;
|
||||
};
|
||||
Reference in New Issue
Block a user