fix: allow lexical import shadowing

This commit is contained in:
2026-08-22 17:53:31 +09:00
parent 89f519d5cb
commit 6279d46652
19 changed files with 1420 additions and 270 deletions

View File

@@ -1,28 +1,24 @@
package rejects_test;
// Cstage-only compile-reject observers on the `ww` driver. Ports of
// the retired native carriers test/wcc/708_param_shadow_mod.c,
// 712_redecl.c and 961_opaque_guards.c; every assertion preserved.
// Compile-reject observers plus the lexical import-shadowing acceptance
// matrix. Ports of the retired native carriers
// test/wcc/708_param_shadow_mod.c, 712_redecl.c and
// 961_opaque_guards.c.
//
// ASYMMETRIC polarity, all three families: the reject lives only in
// cstage check.c. Wwstage's check.ww is a single-pass resolve walk
// with no per-block scoping (#11) and no #108(b) require_sized
// construction/binding guards, so it ACCEPTS these programs — the
// both-stage //ww:error fixture contract cannot carry them
// (residual-carrier-audit.json, 708/712/961 entries). Each row
// asserts the CSTAGE reject only; when wwstage gains the rule (#11
// per-block scoping; the #108(b) guards), the rows graduate to
// //ww:error fixtures and this observer shrinks. The paramshadow neg
// rows additionally require the sibling-module fixture tree, so they
// stay here until a single-file multi-package repro is validated.
// The redeclaration and opaque-guard families below retain their historical
// Cstage-only assertions. Import-name shadowing has the opposite polarity:
// both stages accept a closer lexical value binding, while the package-name
// object remains visible before that binding and after its scope ends. The
// fixture tree is retained because the raw-source route needs a real sibling
// package to exercise selector use and file-local import ownership.
//
// paramshadow (#19/#16) — value names and module names are disjoint:
// a param/let/mlet/for-range/match-case binding named `shadowmod` in
// a file importing module shadowmod is rejected at the decl site; the
// renamed binding in a real `package main` builds+runs 42 (no over-trigger); a param named
// like a module a SIBLING module imports does not trip (src_imports
// filters by the binding's own module — build+run exit 2). The
// carrier's neg_selfimp leg is DROPPED here:
// paramshadow (#19/#16) — an import qualifier is a file-scope binding.
// A param/let/mlet/for-range/match-case binding may shadow it in a closer
// lexical scope. Each legacy neg_* fixture now genuinely uses
// `shadowmod.say()` before or in the binding declaration and then returns a
// value computed from the closer binding. The renamed control still runs 42;
// a parameter named like a package imported only by a sibling package still
// runs 2. The carrier's neg_selfimp leg remains dropped here:
// test/wcc/data/r948_selfimport/case.ww owns the identical claim
// (`//ww:error "self-import"`, both frontends).
//
@@ -67,51 +63,47 @@ fn fixdir() str = {
return strings.concat(testenv.repo(), "/test/wcc/data/paramshadowmod");
};
@test fn paramshadow_neg() void = {
let tags: []str = ["param", "let", "mlet", "forrange_single",
"forrange_tuple", "mcase"];
fn paramshadow_case(src: str, label: str, want: i32) void = {
let stages: []str = ["ww", "ww_ww"];
let tags: []str = ["c", "ww"];
let i: i32 = 0;
for (i < tags.len) {
for (i < stages.len) {
let td: str = testenv.fresh();
// cwd is the fixture dir so the driver's source-dir-first import
// search resolves `import shadowmod;`; -o keeps the binary and
// its sepwork out of the tracked tree.
let av: []str = [testenv.driver("ww"), "build", "-o",
strings.concat(td, "/out"),
strings.concat("neg_", tags[i], ".ww")];
if (runcode(fixdir(), td, strings.concat("neg_", tags[i]), av)
== 0) {
fail(strings.concat("neg_", tags[i]),
"build unexpectedly succeeded -- shadow rule did not fire");
let out: str = strings.concat(td, "/out");
// cwd is the fixture dir so source-dir-first import search resolves
// `import shadowmod;`; -o keeps every product outside the tree.
let av: []str = [testenv.driver(stages[i]), "build", "-o", out, src];
let stem: str = strings.concat(label, "_", tags[i]);
if (runcode(fixdir(), td, strings.concat("build_", stem), av) != 0) {
fail(stem, "build failed -- lexical import shadowing was rejected");
};
let rav: []str = [out];
if (runcode(td, td, strings.concat("run_", stem), rav) != want) {
fail(stem, "built binary exit != exact expected value");
};
testenv.clean(td);
i += 1;
};
};
fn paramshadow_pos(src: str, label: str, want: i32, why: str) void = {
let td: str = testenv.fresh();
let out: str = strings.concat(td, "/out");
let av: []str = [testenv.driver("ww"), "build", "-o", out, src];
if (runcode(fixdir(), td, strings.concat("build_", label), av) != 0) {
fail(label, why);
@test fn paramshadow_lexical_bindings() void = {
let tags: []str = ["param", "let", "mlet", "forrange_single",
"forrange_tuple", "mcase"];
let wants: []i32 = [44, 42, 45, 107, 45, 42];
let i: i32 = 0;
for (i < tags.len) {
paramshadow_case(strings.concat("neg_", tags[i], ".ww"), tags[i],
wants[i]);
i += 1;
};
let rav: []str = [out];
if (runcode(td, td, strings.concat("run_", label), rav) != want) {
fail(label, "built binary exit != expected");
};
testenv.clean(td);
};
@test fn paramshadow_pos_rename() void = {
paramshadow_pos("pos_rename.ww", "pos_rename", 42,
"build failed -- rule over-triggered after the rename");
paramshadow_case("pos_rename.ww", "pos_rename", 42);
};
@test fn paramshadow_pos_crossmod() void = {
paramshadow_pos("pos_crossmod.ww", "pos_crossmod", 2, strings.concat(
"build failed -- shadow rule over-triggered on a ",
"cross-module param"));
paramshadow_case("pos_crossmod.ww", "pos_crossmod", 2);
};
fn rejectrows(family: str, labels: []str, srcs: []str) void = {