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

@@ -14,8 +14,6 @@
static void cstmt(Checker*, Node*);
static Type *cexpr(Checker*, Node*);
static Type *resolve_type(Checker*, Node*);
static void check_module_shadow(Checker*, const char *name, Pos,
const char *kindstr);
static Type *
err(Checker *c, Pos p, const char *fmt, ...)
@@ -69,6 +67,7 @@ static int src_imports(Node *file, const char *modtag, int source,
static Sym *lookup_visible(Checker *c, const char *name);
static Sym *lookup_visible_type(Checker *c, const char *name);
static Sym *lookup_bare_import_binding(Checker *c, const char *name);
static int local_shadows_import(Checker *c, const char *name);
static int reject_bare_import_values(Checker *c, Node *n);
static int reject_bare_import_types(Checker *c, Node *n);
static void resolve_typedecl(Checker *c, Node *d);
@@ -660,6 +659,9 @@ eval_def_const(Checker *c, Node *n, u64 *out, int depth)
}
case N_DOT: {
if (n->lhs == NULL || n->lhs->kind != N_IDENT) return 0;
/* Preserve source-0 `.wwi` self-qualification while refusing to
* fold through a closer lexical value binding. */
if (local_shadows_import(c, n->lhs->str)) return 0;
/* M1 #22: map the qualifier alias to its dotted import path. */
const char *mk = use_path(c->file, c->cur_mod, c->cur_source,
n->lhs->str);
@@ -1618,6 +1620,10 @@ cexpr(Checker *c, Node *n)
return n->type = base;
}
}
if (n->lhs && n->lhs->kind == N_IDENT
&& local_shadows_import(c, n->lhs->str))
return n->type = err(c, n->pos,
"selector '%s' undefined", n->str);
return n->type = err(c, n->pos,
"no enum member '%s' in %s",
n->str ? n->str : "?",
@@ -1650,6 +1656,10 @@ cexpr(Checker *c, Node *n)
for (Tfield *f = u->fields; f; f = f->next)
if (strcmp(f->name, n->str) == 0)
return n->type = f->type;
if (n->lhs && n->lhs->kind == N_IDENT
&& local_shadows_import(c, n->lhs->str))
return n->type = err(c, n->pos,
"selector '%s' undefined", n->str);
return n->type = err(c, n->pos, "no field '%s' in %s",
n->str, type_name(c->a, base));
}
@@ -1660,17 +1670,32 @@ cexpr(Checker *c, Node *n)
if (*q < '0' || *q > '9') { idx = -1; break; }
idx = idx * 10 + (*q - '0');
}
if (idx < 0 && n->lhs && n->lhs->kind == N_IDENT
&& local_shadows_import(c, n->lhs->str))
return n->type = err(c, n->pos,
"selector '%s' undefined", n->str);
if (idx < 0)
return n->type = err(c, n->pos,
"tuple field must be numeric");
Tparam *tp = u->params;
while (idx > 0 && tp) { tp = tp->next; idx--; }
if (tp == NULL && n->lhs && n->lhs->kind == N_IDENT
&& local_shadows_import(c, n->lhs->str))
return n->type = err(c, n->pos,
"selector '%s' undefined", n->str);
if (tp == NULL)
return n->type = err(c, n->pos,
"tuple index out of range");
return n->type = tp->type;
}
/* module-qualified: lhs is IDENT bound as SK_USE */
/* The lexical-shadow slice makes a same-spelled local receiver
* reachable here. Diagnose that invalid local selector, but retain
* the existing lenient fallback for non-identifier extensions such
* as the codegen-supported untyped string-literal `.ptr` form. */
if (n->lhs && n->lhs->kind == N_IDENT
&& local_shadows_import(c, n->lhs->str))
return n->type = err(c, n->pos,
"selector '%s' undefined", n->str);
return n->type = ty_err;
}
case N_INDEX: {
@@ -2308,8 +2333,6 @@ cexpr(Checker *c, Node *n)
type_name(c->a, st));
}
if (cs->str && cs->str[0]) {
check_module_shadow(c, cs->str,
cs->pos, "binding");
scope_define(c->cur, cs->str, SK_VAR, vt, cs);
}
}
@@ -2672,7 +2695,6 @@ clet(Checker *c, Node *n)
desugar_arrayslice(c, declared, n->rhs);
n->type = t;
if (n->str && n->str[0]) {
check_module_shadow(c, n->str, n->pos, "let");
Sym *s = scope_define(c->cur, n->str, SK_VAR, t, n);
if (s == NULL)
err(c, n->pos, "let '%s' redeclared in same scope",
@@ -2753,8 +2775,6 @@ cstmt(Checker *c, Node *n)
for (Node *nm = n->list; nm; nm = nm->next) {
Type *ft = tp ? tp->type : ty_err;
if (nm->str && nm->str[0]) {
check_module_shadow(c, nm->str,
nm->pos, "binding");
if (scope_define(c->cur, nm->str,
SK_VAR, ft, nm) == NULL)
err(c, nm->pos,
@@ -2764,7 +2784,6 @@ cstmt(Checker *c, Node *n)
if (tp) tp = tp->next;
}
} else if (n->str && n->str[0]) {
check_module_shadow(c, n->str, n->pos, "binding");
scope_define(c->cur, n->str, SK_VAR,
elem ? elem : ty_err, n);
}
@@ -2809,6 +2828,9 @@ cstmt(Checker *c, Node *n)
type_name(c->a, rt));
}
Tparam *tp = u ? u->params : NULL;
/* Like a Go VarSpec/ShortVarDecl, every declared type belongs to
* the declaration header: resolve and check all of them before any
* name enters the enclosing lexical scope. */
for (Node *l = n->list; l; l = l->next) {
Type *declared = l->lhs ? resolve_type(c, l->lhs) : NULL;
Type *elem = tp ? tp->type : NULL;
@@ -2819,16 +2841,18 @@ cstmt(Checker *c, Node *n)
l->str, type_name(c->a, declared),
type_name(c->a, elem));
l->type = t;
if (tp) tp = tp->next;
}
for (Node *l = n->list; l; l = l->next) {
if (l->str && l->str[0]) {
check_module_shadow(c, l->str, l->pos, "let");
Sym *s = scope_define(c->cur, l->str, SK_VAR, t, l);
Sym *s = scope_define(c->cur, l->str, SK_VAR,
l->type, l);
if (s == NULL)
err(c, l->pos,
"let '%s' redeclared in same scope",
l->str);
else if (n->op == TK_CONST) s->is_const = 1;
}
if (tp) tp = tp->next;
}
if (u && tp != NULL)
err(c, n->pos, "tuple has extra elements");
@@ -3161,6 +3185,23 @@ lookup_bare_import_binding(Checker *c, const char *name)
return NULL;
}
/* A package-name object remains in the file scope when a closer lexical
* value binding wins lookup. This predicate is intentionally narrower than
* ordinary invalid-selector checking: before lexical import shadowing became
* legal, only this newly reachable path was hidden by the shadow prohibition. */
static int
local_shadows_import(Checker *c, const char *name)
{
if (c == NULL || name == NULL || name[0] == '\0') return 0;
if (!src_imports(c->file, c->cur_mod, c->cur_source, name)) return 0;
for (Scope *s = c->cur; s && s != c->top; s = s->parent) {
Sym *r = scope_lookup_local(s, name);
if (r != NULL)
return r->kind == SK_VAR || r->kind == SK_PARAM;
}
return 0;
}
/* Walk a type subtree only far enough to classify package-name objects. This
* is deliberately not general type checking: the enum/array constant folders
* need the package diagnostic before they reduce an unsupported outer shape
@@ -3301,45 +3342,6 @@ lookup_visible_type(Checker *c, const char *name)
return NULL;
}
/*
* check_module_shadow — refuse value bindings that shadow an
* in-scope imported module bareword. "Value names and module names
* are disjoint": a fn param / let / mcase binding named `fmt` while
* the declaring source carries `use fmt;` would silently miscompile
* any `fmt.X` body lookup through the shadow's value bits (the
* cstage cexpr N_DOT path resolves the inner ident as the shadow
* and emits CALL through its bytes — task #19).
*
* Scope:
* - Fires only for nested-scope binds (c->cur != c->top). Same-leaf
* top-level decls (`use foo; fn foo(...)`) are intentional and
* handled by the SK_USE→SK_X promotion path with use_alias=1.
* - Filters by the declaring source's own use directives. lib/fmt's
* `fn fprintf(fmt: str, ...)` is fine because lib/fmt doesn't
* import itself.
* - Walks every scope (not just innermost) so a deeper shadow that
* happens to mask the SK_USE entry can't suppress the check.
*/
static void
check_module_shadow(Checker *c, const char *name, Pos pos,
const char *kindstr)
{
if (name == NULL || name[0] == '\0') return;
if (c == NULL || c->cur == c->top) return;
int seen_use = 0;
for (Scope *s = c->cur; s; s = s->parent) {
Sym *r = scope_lookup_local(s, name);
if (r && (r->kind == SK_USE || r->use_alias)) {
seen_use = 1;
break;
}
}
if (!seen_use) return;
if (!src_imports(c->file, c->cur_mod, c->cur_source, name)) return;
err(c, pos, "%s '%s' shadows imported module '%s'",
kindstr, name, name);
}
/* Self-contained `.wwi` files can carry the same origin-owned type/const
* fact through two direct dependencies (a diamond), or alongside a direct
* import of that origin. In strict package mode those compiler-generated
@@ -4148,23 +4150,116 @@ reject_nonfunction_main_decls(Checker *c, Node *file)
}
}
/* Import usage is a property of the file-local qualifier occurrence. Record
* qualified syntax before resolving declaration bodies so import diagnostics
* retain production Go's source order without making a failed bare lookup a
* use. WW already rejects lexical bindings that shadow an import qualifier. */
/* Import diagnostics precede body diagnostics, so usage must be known before
* pass 1. The temporary scopes mirror the real declaration points without
* contaminating the compilation scope that pass 1 owns. */
static void
mark_import_uses_node(Checker *c, Node *n, const char *owner, int source)
{
if (n == NULL) return;
if (n->kind == N_TNAME && n->str != NULL) {
switch (n->kind) {
case N_TNAME: {
if (n->str == NULL) return;
const char *dot = strrchr(n->str, '.');
if (dot != NULL) {
char *head = astrndup(c->a, n->str, (size_t)(dot - n->str));
(void)find_use_path(c->file, owner, source, head, 1);
if (scope_lookup(c->cur, head) == NULL)
(void)find_use_path(c->file, owner, source, head, 1);
}
} else if (n->kind == N_DOT && n->lhs != NULL
&& n->lhs->kind == N_IDENT && n->lhs->str != NULL) {
(void)find_use_path(c->file, owner, source, n->lhs->str, 1);
return;
}
case N_FNDECL: {
for (Node *p = n->attr; p; p = p->next)
mark_import_uses_node(c, p, owner, source);
mark_import_uses_node(c, n->lhs, owner, source);
for (Node *p = n->list; p; p = p->next)
mark_import_uses_node(c, p->lhs, owner, source);
Scope *saved = c->cur;
c->cur = newscope(c->a, saved);
for (Node *p = n->list; p; p = p->next)
if (p->str && p->str[0])
(void)scope_define(c->cur, p->str, SK_PARAM, NULL, p);
mark_import_uses_node(c, n->body, owner, source);
c->cur = saved;
return;
}
case N_BLOCK: {
Scope *saved = c->cur;
c->cur = newscope(c->a, saved);
for (Node *p = n->list; p; p = p->next)
mark_import_uses_node(c, p, owner, source);
c->cur = saved;
return;
}
case N_LET:
for (Node *p = n->attr; p; p = p->next)
mark_import_uses_node(c, p, owner, source);
mark_import_uses_node(c, n->lhs, owner, source);
mark_import_uses_node(c, n->rhs, owner, source);
if (n->str && n->str[0])
(void)scope_define(c->cur, n->str, SK_VAR, NULL, n);
return;
case N_MLET:
for (Node *p = n->attr; p; p = p->next)
mark_import_uses_node(c, p, owner, source);
mark_import_uses_node(c, n->rhs, owner, source);
for (Node *p = n->list; p; p = p->next)
mark_import_uses_node(c, p->lhs, owner, source);
for (Node *p = n->list; p; p = p->next)
if (p->str && p->str[0])
(void)scope_define(c->cur, p->str, SK_VAR, NULL, p);
return;
case N_FOR: {
Scope *saved = c->cur;
c->cur = newscope(c->a, saved);
mark_import_uses_node(c, n->lhs, owner, source);
mark_import_uses_node(c, n->cond, owner, source);
mark_import_uses_node(c, n->rhs, owner, source);
mark_import_uses_node(c, n->body, owner, source);
mark_import_uses_node(c, n->els, owner, source);
c->cur = saved;
return;
}
case N_FORRANGE: {
Scope *saved = c->cur;
c->cur = newscope(c->a, saved);
mark_import_uses_node(c, n->lhs, owner, source);
if (n->list != NULL) {
for (Node *p = n->list; p; p = p->next)
if (p->str && p->str[0])
(void)scope_define(c->cur, p->str,
SK_VAR, NULL, p);
} else if (n->str && n->str[0]) {
(void)scope_define(c->cur, n->str, SK_VAR, NULL, n);
}
mark_import_uses_node(c, n->body, owner, source);
mark_import_uses_node(c, n->els, owner, source);
c->cur = saved;
return;
}
case N_MATCH:
mark_import_uses_node(c, n->lhs, owner, source);
for (Node *cs = n->list; cs; cs = cs->next) {
Scope *saved = c->cur;
c->cur = newscope(c->a, saved);
mark_import_uses_node(c, cs->lhs, owner, source);
for (Node *p = cs->list; p; p = p->next)
mark_import_uses_node(c, p, owner, source);
if (cs->str && cs->str[0])
(void)scope_define(c->cur, cs->str, SK_VAR, NULL, cs);
mark_import_uses_node(c, cs->body, owner, source);
c->cur = saved;
}
return;
case N_DOT:
if (n->lhs != NULL && n->lhs->kind == N_IDENT
&& n->lhs->str != NULL
&& scope_lookup(c->cur, n->lhs->str) == NULL)
(void)find_use_path(c->file, owner, source,
n->lhs->str, 1);
break;
default:
break;
}
for (Node *p = n->attr; p; p = p->next)
mark_import_uses_node(c, p, owner, source);
@@ -4180,10 +4275,13 @@ mark_import_uses_node(Checker *c, Node *n, const char *owner, int source)
static void
mark_import_uses(Checker *c, Node *file)
{
Scope *saved = c->cur;
for (Node *d = file->list; d; d = d->next) {
if (d->kind == N_USE) continue;
c->cur = newscope(c->a, NULL);
mark_import_uses_node(c, d, decl_mod(file, d), d->sourceid);
}
c->cur = saved;
}
static void
@@ -4882,8 +4980,6 @@ check_file(Checker *c, Node *file)
Type *fnt = d->type;
for (Tparam *p = fnt->params; p; p = p->next) {
if (p->name && p->name[0]) {
check_module_shadow(c, p->name,
d->pos, "param");
if (scope_define(c->cur, p->name,
SK_PARAM, p->type, d) == NULL)
err(c, d->pos,

View File

@@ -606,10 +606,8 @@ struct Checker {
* rather than colliding io.read. */
int cur_source; /* lexical source-file scope of the declaration
* currently being checked. */
Node *file; /* current N_FILE root; used by check_module_shadow
* to consult the declaring source file's own `use`
* directives when refusing param/let names that
* would shadow an imported module bareword. */
Node *file; /* current N_FILE root; owns source-local import
* qualifier lookup and usage accounting. */
int loops; /* nesting count for break/continue */
int matcharms; /* nesting count for yield */
int errs;

View File

@@ -9760,6 +9760,201 @@ No format bump. Build workdir format remains `18`, test workdir format remains
`19`, semantic storage format remains `3`, and no test-result cache is
introduced.
### 11.49 Implemented lexical shadowing of import bindings
An effective nonblank import qualifier is a file-local package-name object, not
a reserved spelling. An ordinary closer lexical binding may shadow it. Each
occurrence resolves to the nearest visible object: a selector before the local
declaration denotes the import and satisfies that import's use accounting; the
same spelling after a parameter, local, tuple-local, loop/range binder, or
match-arm binder denotes that closer binding. Leaving the nested scope restores
the import binding. A selector consumes an import only when its receiver
actually resolves to that import's package-name object. A selector whose
receiver is a local value neither consumes nor resurrects the same-spelled
import. This includes explicit aliases and qualifiers spelled like builtins.
This is lexical binding recovery, not a new import form or an identity rule.
Blank imports and rejected effective-`init` imports still install no binding;
an unresolved target still fails during resolution before checker binding
semantics. WW's existing `for ... else` behavior has no Go counterpart and is
unchanged.
#### Pinned Go evidence and fact classification
The sole semantic authority is official Go 1.26.5 at commit
`c19862e5f8415b4f24b189d065ed739517c548ba`:
- the language specification defines imports as package objects with file-block
scope, and defines parameter/body/local declaration points, nested scopes,
and inner-declaration shadowing ([`doc/go_spec.html`, lines
21602174 and 21902233](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/doc/go_spec.html#L2160-L2233));
- `types2` constructs a file scope and `PkgName`, then resolves objects from
the innermost scope outwards
([`cmd/compile/internal/types2/resolver.go`, lines
223335](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L223-L335),
[`check.go`, lines 7394](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/check.go#L73-L94));
- parameter and local declaration timing is implemented in
[`signature.go`, lines 143180](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/signature.go#L143-L180)
and [`assignments.go`, lines 525602](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/assignments.go#L525-L602);
- selector checking marks an import used only after its receiver resolves to a
`PkgName`; bodies are processed before unused imports are diagnosed
([`call.go`, lines 672692](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/call.go#L672-L692),
[`check.go`, lines 496523](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/check.go#L496-L523),
[`resolver.go`, lines 706740](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L706-L740)); and
- official fixtures compile a selector before a later local, retain that
declaration-point distinction, and show parameter shadowing
([`test/fixedbugs/bug129.go`, lines 813](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/fixedbugs/bug129.go#L8-L13),
[`issues0.go`, lines 1623](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/issues0.go#L16-L23),
[`bug107.go`, lines 815](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/fixedbugs/bug107.go#L8-L15)).
Those specification, resolver, selector-use, declaration-point, and fixture
facts are **behavior directly implemented or asserted by pinned Go**. That a
WW occurrence before a local consumes the import, an occurrence after it sees
the local, a nested scope restores the import on exit, aliases and
builtin-spelled qualifiers follow the same rule, and a local selector does not
consume the import is **behavior derived from the pinned implementation**.
The behavior honestly applies to WW's existing file-local dotted bindings and
representable lexical scopes without introducing Go's modules, manifests,
quoted/grouped/dot imports, registry, cache, network resolution, or source
build expressions.
#### Fresh four-axis audit and direct pre-fix measurements
The fresh simultaneous audit classified multiple named source operands as a
different, applicable build/package slice; explicit `*_test.ww` build operands
as a different, applicable build/source-selection slice; shared top-level test
process state and fatal-abort topology as different, applicable test slices; and
Go-compatible regular-expression `-run` matching as a different, applicable
test slice. They remain open and unselected: each needs a wider true-owner
change. Grouped, quoted, and dot imports are inapplicable to WW's deliberately
narrow import grammar. No fresh pinned evidence reopened a completed section
through §11.48.
Before this slice, all following results were **directly measured WW behavior**:
- a directory build containing a real package selector followed by a legal
`let shadowmod` failed in both stages before publication; Cstage emitted
positioned 155-byte stderr (SHA-256
`13cfac9494a1f6c0ee958d3b74ecfe8b0efe2a685f31a8a1771fda3fa0af1b02`),
while WWstage emitted unpositioned 76-byte stderr (SHA-256
`298e4991ca9900ef32499762f8ae9abb8ace07c3e672dc66db1e911a8de62b75`).
Both had empty stdout, no output, and empty precreated workdirs;
- same-package and external test shadows failed before descriptor execution;
both emitted exactly `FAIL\n`, published/retained nothing, and left no work
entry. Cstage stderr was 277 bytes (SHA-256
`ac850802e5911f53c3e64be2ab55e1f3442438eadab35a40f63ddac869337e4e`),
WWstage was 186 bytes (SHA-256
`a1229bd19628e5dc909a0bcb848c30cc1dbdf9d370f21ed51e79489c06abcad0`);
- after a genuine selector, `let shadowmod: i32 = 1; return shadowmod.say()`
produced only the shadow prohibition in Cstage, but WWstage additionally
cascaded through `calling non-function` and `asserttyped: dot`; and
- an otherwise-unused import with only local `shadowmod.n` produced the shadow
prohibition rather than unused-import in both stages, proving the former
syntax-only use pass falsely consumed the import. Legal no-shadow controls
built, published byte-identical executables (SHA-256
`7b710cf0973821cec430878d1f90de64485438510512561263c1a16b367c3c78`),
and exited 42.
The direct probes also covered parameter, local, nested, explicit-alias,
selector-before/after, imported-dependency, same-package, external-test, and
honest test-only paths; both stages rejected every legal shadow. Thus the
pre-fix difference was legal-source rejection, false unused-import accounting,
and stage-divergent downstream recovery.
#### Ownership and complete four-axis behavior
The true owners are the Cstage semantic checker in `cmd/wcc/check.c`, its
self-hosted twin in `selfhost/cmd/wcc/check.ww`, and the latter's
`selfhost/cmd/wcc/cgenexpr.ww` local-versus-imported-enum fast path. They
remove the import-shadow prohibition; resolve use accounting through isolated
temporary lexical scopes rather than merely selector spelling; preserve
declaration timing; and gate dotted package/enum shortcuts on the visible
binding. The Cstage code generator already follows checker/local stamps and is
proved rather than redefined. Parsers, loader/source selection, drivers,
coordinator, canonical resolution, graph/action construction, assembler,
archiver, linker, runtime, publisher, and persistence records are not owners.
- **Go-like build:** raw, directory, and imported programs with legal shadowing
now pass checking, build through unchanged actions, publish normally, and run
the local value/field/function-pointer behavior. A genuinely invalid local
selector fails during checking before code generation or downstream tools.
- **Go-like test:** production called by test, same-package, external-test,
honest test-only, filtered, retained, and directly retained products use the
same rule before execution. Discovery, filters, descriptors, process state,
fatal/skip behavior, timeout, retention, and cleanup are unchanged.
- **Go-like package:** import binding remains file scoped; ordinary local scopes
nest within it, and sibling source files remain independent. Declared names,
source roles, package/variant identities, exported declarations,
initialization, symbols, and selected membership do not change.
- **Go-like import:** the nearest visible object wins. Only a selector whose
receiver is the visible package-name object satisfies unused-import accounting;
local field selectors do not. Default/explicit aliases, blank imports,
effective-`init`, missing-target precedence, canonical dotted identity,
contextual local/vendor mapping, visibility, cycles, and direct graph edges
retain their existing semantics.
#### Lifecycle, parity, proof, and formats
Filename/platform/test-role eligibility, byte-sorted source selection, package
clauses, source IDs, loading, and resolution remain earlier owners. Shadowing
does not add/remove an already-resolved direct import edge, rekey actions, or
change action order, variants, initialization dispatch, linker symbols,
`.wwi` ownership, artifacts, publication, or persistence identity. Physical
directories remain loader/runtime/presentation metadata, never canonical
package, import, graph, action, artifact, symbol, publication, or storage
identity.
The lexical-aware prepass keeps source-position diagnostic ordering. It removes
every `shadows imported module` diagnostic while preserving missing target,
invalid effective-`init`, blank/no-binding, collision/redeclaration, and
selector-only package-name diagnostics. An otherwise-unused import is reported
before a later invalid local selector according to source position; local
invalid dots reject with the same positioned recovery stamp in both stages and
cannot cascade into C/WW code generation, assembler, or linker diagnostics.
Valid shadowing reaches ordinary compiler, assembler, archiver, linker, and
runtime paths. The representative package matrix proves byte-identical
Cstage/WWstage unit, interface, assembly, object, archive, initializer, and
published executable artifacts; every test source role proves public binary
parity, and the retained case proves retained-binary parity. A cold invalid
action creates no public/retained/interface/archive/object/executable artifact
and no `.new`, `.install`, `.wwtxn.*`, adjacent `.sepwork`, capture, result, or
request scratch. A warm edit that
makes an import unused while also introducing an invalid local selector
preserves the prior committed generation and public product byte-for-byte;
exact restoration uses ordinary invalidation/reuse and cannot leave poisoned
state. Existing producer/runtime failure, late publication failure, rollback,
concurrency, interruption, process-group ownership, and cleanup remain their
existing owners because this checker slice adds no process, lock, transaction,
or shared runtime state.
The WW-native `lexical_import_bindings_shadow_normally` package observer and
the tool-suite `paramshadow_lexical_bindings` fixture matrix jointly prove
selector before local, self-shadowing initializer, parameter/let/tuple-let/
ordinary-for/range/match-arm declaration timing, all annotated tuple types
before any tuple binder, nested restoration, aliases and builtin-spelled
qualifiers, local struct/pseudo/function-pointer fields, imported-enum name
collisions, local dotted type/value rejection, unused accounting, and
sibling-file isolation. The package observer also covers raw and directory
builds, imported dependencies, every applicable test role, filtered and
retained/direct-retained execution, normalized diagnostic parity, valid
artifact/runtime parity, cold cleanup, warm preservation/restoration, and
residue absence. Blank/effective-`init`/missing-target behavior is unchanged
and remains proved by the immediately preceding focused observers.
Go has no range-loop `else` clause. That WW-only extension is therefore
inapplicable to this pinned-Go slice and was not redefined: the pre-existing
Cstage behavior keeps a range binder visible in `else`, whereas WWstage restores
the outer scope before `else`. Each stage's import-use prepass deliberately
matches its own live checker there; the cross-stage extension difference remains
open and is not presented as lexical-shadow parity proved by this slice.
No serialized format changes. This alters lexical resolution of source bytes
already present in the existing action vouchers and adds no action-key, graph,
artifact-layout, harness-protocol, cache, database, or publication field.
Build workdir format remains `18`, test workdir format remains `19`, semantic
storage format remains `3`, and no test-result cache is introduced.
## 12. Candidate architectures and hard-gate decision
Five candidates were developed as coherent systems, not as feature bins.

View File

@@ -336,6 +336,21 @@ ImportPath = ident { "." ident } .
lookup rather than package-name diagnostics. Builtin spelling does not alter
the object: an import bound as `len`, `size`, `align`, or another builtin name
remains a selector-only package-name object.
An ordinary lexical binding may shadow that file-local package-name object.
Lookup at each occurrence chooses the nearest enclosing binding: a selector
before a later local declaration can use and count the import, while the
same spelling after that declaration denotes the local. Parameters bind for
their whole function body; local `let` bindings begin after their declared
type and initializer have been checked; tuple bindings begin after their
right-hand side and declared types; and loop, range, and match-arm bindings
begin only after their respective initializer/iterable or pattern/type has
been checked. Nested blocks and loop scopes restore the imported package-name
object on exit. These declaration-point rules apply equally to default,
explicit, and builtin-spelled qualifiers. A selector counts as an import use
only when its receiver resolves to that package-name object; a selector on a
closer local, including a field or pseudo-field selector, does not.
Go has no range-loop `else` clause; that WW-only extension is not assigned a
Go-derived scope rule by this paragraph.
Neither form exposes an imported declaration as a bare `Name`; ordinary
unqualified lookup remains limited to lexical, builtin, and same-package
declarations. A blank import creates no package-name object, an effective

View File

@@ -598,7 +598,53 @@ precedence. Type-shaped arguments parsed for `size` and `align` must retain type
checking when either spelling is instead an import binding; neither stage may
fall into an internal expression-kind diagnostic.
The same observer must exercise ordinary root and imported builds plus
The adjacent focused dual-stage
`lexical_import_bindings_shadow_normally` package observer, together with the
tool-suite `paramshadow_lexical_bindings` fixture matrix, is the acceptance
owner for ordinary lexical shadowing of an effective file-local package-name
object. Together they require a selector before a later local to resolve to and
consume the import, while the later spelling resolves to the closer parameter,
`let`, tuple-let, ordinary-`for`/range, or match-arm binding. Nested blocks and
loop exit restore the import; a parameter wins throughout its body;
self-shadowing initializers retain the outer import until the local declaration
point; and every annotated tuple type is checked before any tuple binder enters
scope. Default, explicit, and builtin-spelled qualifiers follow the same rule.
A local value, struct or pseudo-field, function-pointer field, or local
enum/member collision must use the local receiver rather than recover the
same-spelled import. Conversely, a shadowed dotted type or invalid scalar
selector must reject as a local use without a checker/backend cascade. A
selector-shaped local-only occurrence does not consume the import, so its
unused diagnostic retains source-position precedence. Sibling source files
retain independent import bindings and a separately selected legal import use
remains effective. Blank, effective-`init`, and missing-target controls remain
owned by their immediately adjacent focused observers and are explicit
non-effects of this acceptance pair.
This observer exercises raw-source and directory builds, imported dependencies,
production reached by tests, same-package, external-test, and honest
test-only sources, filtered execution, retained test execution, and later
direct retained-binary execution. Cstage and WWstage must agree on status,
stdout, normalized full stderr (including positioned diagnostic order) for
build and test rejection, representative comparable semantic artifacts, every
test role's public binary bytes, retained binary bytes, and valid runtime
results. Its lifecycle matrix requires cold invalid requests to leave no public
or retained product and an empty workdir; a warm valid-to-invalid unused-import
transition whose source also contains an invalid local selector to preserve the
prior public product and complete committed generation byte-for-byte; and
restoration to recover ordinary exact-byte reuse with no `.new`, `.install`,
`.wwtxn.*`, adjacent `.sepwork`, capture/result, or request-scratch residue. The
rule changes neither source eligibility,
package/import identity, direct graph edges, action/variant identity, init
ordering, test process/state/fatal-abort topology, runtime/process ownership,
publication transaction shape, persistence format, concurrency isolation,
interruption handling, nor descendant cleanup; those boundaries retain their
existing owners. Go has no range-loop `else` clause; that WW-only extension's
pre-existing Cstage/WWstage scope difference is explicitly outside this
pinned-Go observer, and each import-use prepass continues to mirror its own
stage's live checker there.
The `bare_import_bindings_require_selectors` observer must exercise ordinary
root and imported builds plus
production, same-package test, external-test, and honest test-only source
roles. Cstage and WWstage must agree on status and normalized stdout/stderr,
including diagnostic order and source position, and valid controls must produce

View File

@@ -3642,13 +3642,16 @@ fn cgdot(c: *cgen, n: *syntax.node) void = {
// → inline the pre-computed constant. `pkg.Enum.MEMBER` keeps
// `pkg` so enumlookupmod can prefer the explicit module on a
// leaf collision; bare `Enum.MEMBER` falls back to c.curmod via
// enumlookup's same-module-first walk.
// enumlookup's same-module-first walk. A live local with the same
// spelling wins before this name-only registry fallback; valid local
// enum selectors were already folded by the checker.
if (lhs != nil) {
let etname: str;
let etmod: str;
etname.ptr = nil; etname.len = 0;
etmod.ptr = nil; etmod.len = 0;
if (lhs.kind == syntax.nkind.N_IDENT) {
if (lhs.kind == syntax.nkind.N_IDENT
&& localfindnode(c, lhs.str) == nil) {
etname = lhs.str;
};
if (lhs.kind == syntax.nkind.N_DOT) {

View File

@@ -37,9 +37,8 @@ type checker = struct {
// preference in bare-leaf lookups.
cursource: i32, // lexical source-file scope of the current decl;
// selects only that file's import bindings.
file: *syntax.node, // N_FILE root; used by checkmoduleshadow
// to consult the declaring source's own
// `use` directives.
file: *syntax.node, // N_FILE root; owns source-local `use`
// bindings and their usage state.
allococtx: *syntax.node, // #3/B': the one empty alloc([], n) call node
// with let-declared slice context this walk;
// any other empty alloc has no element hint
@@ -224,6 +223,24 @@ fn usepathfor(file: *syntax.node, modtag: str, source: i32, alias: str) str = {
// modkeyfor — the module key for a directly imported alias. Empty means
// the referencing package did not itself declare that import.
fn modkeyfor(c: *checker, alias: str) str = {
let found: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, alias);
if (found != nil && found.scope != c.top
&& (found.skind == syntax.skind.SK_VAR
|| found.skind == syntax.skind.SK_PARAM)) {
let empty: str;
return empty;
};
// Imported interface declarations may qualify their own package from the
// source-0 section without a source import node. Preserve that bridge, but
// only after a closer lexical binding has been excluded above.
if (c.cursource == 0 && c.curmod.len != 0) {
let (prefix, suffix) = strings.rcut(c.curmod, ".");
let leaf: str = suffix;
if (leaf.len == 0) { leaf = c.curmod; };
if (syntax.streq(alias, leaf)) {
return usepathfor(c.file, c.curmod, c.cursource, alias);
};
};
return usepathfor(c.file, c.curmod, c.cursource, alias);
};
@@ -264,6 +281,23 @@ fn srcimports(file: *syntax.node, modtag: str, source: i32, name: str) bool = {
return false;
};
// A package-name object remains in the file scope when a closer lexical value
// wins lookup. Keep this predicate restricted to that newly reachable path so
// unrelated WW selector extensions retain their established diagnostics.
fn localshadowsimport(c: *checker, name: str) bool = {
if (!srcimports(c.file, c.curmod, c.cursource, name)) { return false; };
let s: *syntax.scope = c.cur;
for (s != nil && s != c.top) {
let found: *syntax.sym = syntax.scopelookuplocal(s, name);
if (found != nil) {
return found.skind == syntax.skind.SK_VAR
|| found.skind == syntax.skind.SK_PARAM;
};
s = s.parent;
};
return false;
};
fn lookupvisible(c: *checker, name: str) *syntax.sym = {
let found: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, name);
let builtin: *syntax.sym = nil;
@@ -398,39 +432,6 @@ fn synthesizedtestrun(c: *checker, e: *syntax.node) bool = {
return c.synthtestrun != nil && c.synthtestrun == e;
};
// checkmoduleshadow — enforce "value names and module names are
// disjoint" at nested-scope binds. Mirrors cstage check_module_shadow
// (cmd/wcc/check.c). Fires for fn params / lets / forrange iters /
// mcase bindings whose name matches an in-scope `use foo;` import
// declared in the same source file. Top-level decls are exempt
// (their same-leaf-as-module pattern is the intentional coexistence
// shape — `use fnmatch; fn fnmatch(...)` etc.).
fn checkmoduleshadow(c: *checker, name: str, kindstr: str) void = {
if (name.len == 0) { return; };
if (c.cur == c.top) { return; };
let seen: bool = false;
let s: *syntax.scope = c.cur;
for (s != nil) {
let r: *syntax.sym = syntax.scopelookuplocal(s, name);
if (r != nil) {
if (r.skind == syntax.skind.SK_USE) {
seen = true;
s = nil;
};
};
if (s != nil) { s = s.parent; };
};
if (!seen) { return; };
if (!srcimports(c.file, c.curmod, c.cursource, name)) { return; };
cerr(kindstr);
cerr(" '");
cerr(name);
cerr("' shadows imported module '");
cerr(name);
cerr("'\n");
c.errs += 1;
};
// installdecl — install the top-level decl's name into the top scope.
// We don't compute its type yet (that's the resolve pass) — just bind
// the name so forward references resolve.
@@ -593,9 +594,11 @@ fn installtop(c: *checker, d: *syntax.node, nm: str, mod: str, k: syntax.skind,
// `let (a,b) = f()`, `for (let (a,b) .. s)`, and the ww-extension
// multi-assign `a, _ = f()`.
//
// `define` (the binding contexts: let-unpack + for-range) installs each
// named binder as a fresh SK_VAR and back-fills its declared type onto
// .lhs so use sites resolve through the N_IDENT exprtype path. Multi-
// `define` (the binding contexts: let-unpack + for-range) back-fills and
// resolves every declared type before installing any named binder as a fresh
// SK_VAR. This gives the whole declaration header the outer lexical scope,
// matching the Cstage twin and Go's VarSpec/ShortVarDecl declaration point.
// Multi-
// assign targets are pre-declared lvalues, so it passes false: .lhs is
// left untouched (an N_INDEX/N_DOT target carries a live operand there)
// and only the type_ stamp fires on the still-untyped slots.
@@ -614,23 +617,6 @@ fn stamptuplebinds(c: *checker, binds: *syntax.node, elems: *syntax.node,
if (pt != nil) { et = pt.lhs; };
if (define) {
if (b.lhs == nil) { b.lhs = et; };
let bnm: str = b.str;
if (bnm.len > 0) {
checkmoduleshadow(c, bnm, what);
// first registration wins; a nil return is a
// same-scope duplicate — `let (a, a) = ..` /
// `for (let (a, a) .. xs)`. Mirrors cstage
// scope_define == NULL → "redeclared".
let ds: *syntax.sym = syntax.scopedefine(c.cur, bnm, syntax.skind.SK_VAR, nil, b);
if (ds == nil) {
cerr("error: ");
cerr(what);
cerr(" '");
cerr(bnm);
cerr("' redeclared in same scope\n");
c.errs += 1;
};
};
};
if (b.type_ == nil) {
let src: *syntax.node = b.lhs;
@@ -643,6 +629,27 @@ fn stamptuplebinds(c: *checker, binds: *syntax.node, elems: *syntax.node,
b = b.next;
if (pt != nil) { pt = pt.next; };
};
if (define) {
b = binds;
for (b != nil) {
let bnm: str = b.str;
if (bnm.len > 0) {
// First registration wins; a nil return is a same-scope
// duplicate within this declaration.
let ds: *syntax.sym = syntax.scopedefine(c.cur, bnm,
syntax.skind.SK_VAR, nil, b);
if (ds == nil) {
cerr("error: ");
cerr(what);
cerr(" '");
cerr(bnm);
cerr("' redeclared in same scope\n");
c.errs += 1;
};
};
b = b.next;
};
};
};
// resolvewalk — recursive AST walk that, for every nkind.N_IDENT and
@@ -818,7 +825,6 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
} else {
let bnm: str = n.str;
if (bnm.len > 0) {
checkmoduleshadow(c, bnm, "binding");
// C4 (task #7): bind the ELEMENT type so field
// reads off a by-value aggregate binding
// (`for (let t .. threads) { t.pc }`) resolve —
@@ -876,11 +882,13 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
};
// `for (init; cond; post) body` / `for (cond) body` — the body is the
// break/continue target. Walk init/cond/post outside the loop count
// (they hold no statements), bump only around the body, and keep the
// `else` outside (it targets an enclosing loop, like N_FORRANGE above).
// break/continue target. Its initializer scope covers cond/post/body/else;
// only the body is inside the loop count, so `else` still targets an
// enclosing loop.
// Mirrors cstage cmd/wcc/check.c:2529 (N_FOR, c->loops++).
if (k == syntax.nkind.N_FOR) {
let forouter: *syntax.scope = c.cur;
c.cur = syntax.newscope(forouter);
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
if (n.cond != nil) { resolvewalk(c, n.cond); };
if (n.rhs != nil) { resolvewalk(c, n.rhs); };
@@ -888,6 +896,7 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
if (n.body != nil) { resolvewalk(c, n.body); };
c.loops -= 1;
if (n.els != nil) { resolvewalk(c, n.els); };
c.cur = forouter;
return;
};
@@ -930,7 +939,6 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
c.cur = syntax.newscope(outer);
let nm: str = n.str;
if (nm.len > 0) {
checkmoduleshadow(c, nm, "binding");
syntax.scopedefine(c.cur, nm, syntax.skind.SK_VAR, nil, n);
};
if (n.body != nil) {
@@ -997,8 +1005,6 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
// the un-annotated binder stays untyped and asserttyped aborts.
// Mirrors cstage check.c:2017 (cexpr(rhs), unconditional).
if (n.rhs != nil) {
// `rt` would shadow the imported lib/rt module
// (checkmoduleshadow errors); `rty` avoids it.
// #99 alias transparency: a NAMED tuple alias rhs
// (`type pair = (i64,i64)`) destructures like its
// base — resolvealias mirrors cstage's
@@ -1158,7 +1164,6 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
if (k == syntax.nkind.N_LET) {
let nm: str = n.str;
if (nm.len > 0) {
checkmoduleshadow(c, nm, "let");
let s: *syntax.sym = syntax.scopedefine(c.cur, nm, syntax.skind.SK_VAR, nil, n);
// catB-22: flag a `const` binding so checkassign can
// reject a later reassignment. The parser stamps
@@ -4868,8 +4873,7 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
};
}; };
// A.6.1.5b stamp cases — mirror cstage check.c:833-866. Pure
// type-AST stamps; never rewrite e.kind. Lenient on misses
// (cstage errors); falls through to nil under scruttype L656.
// type-AST stamps; never rewrite e.kind.
//
// Pseudo-fields .len/.cap/.ptr on slice/str/array. Cstage
// L833-842. `str` lives as N_TNAME("str") in wwstage — no
@@ -4938,6 +4942,23 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
};
};
}; };
// A same-spelled lexical receiver that fails every valid local
// selector shape is the newly reachable shadowing error path. Stamp
// recovery so asserttyped and cgen cannot replace the causal,
// positioned diagnostic. Missing struct/tuple/enum members use this
// same shadow-specific spelling in both compiler stages.
if (lhsn != nil && lhsn.kind == syntax.nkind.N_IDENT
&& localshadowsimport(c, lhsn.str)) {
// Cstage's N_DOT position is the receiver start; use the
// identifier position rather than WW parser's dot token.
cerr(lhsn.file); cerr(":");
cerr(strconv.i32tos(lhsn.line, strconv.base.DEC)); cerr(":");
cerr(strconv.i32tos(lhsn.col, strconv.base.DEC));
cerr(": error: selector '"); cerr(e.str);
cerr("' undefined\n");
c.errs += 1;
e.type_ = c.tc.tyerr: *void;
};
};
return nil;
};
@@ -7631,7 +7652,6 @@ fn installparams(c: *checker, params: *syntax.node) void = {
normalizevariadicparam(p);
let nm: str = p.str;
if (nm.len > 0) {
checkmoduleshadow(c, nm, "param");
syntax.scopedefine(c.cur, nm, syntax.skind.SK_PARAM, nil, p);
};
};
@@ -7846,39 +7866,195 @@ fn topdeclkind(d: *syntax.node) bool = {
|| d.kind == syntax.nkind.N_LET);
};
// Record only qualified syntax in each owning source before name resolution.
// This preserves source-position import diagnostics and never lets a failed
// bare lookup consume an ordinary import.
fn markimportusesnode(c: *checker, n: *syntax.node, owner: str,
source: i32) void = {
// Mark only a qualifier whose spelling still denotes the source-local package
// binding. The scratch scope contains lexical binders only; package and import
// symbols remain owned by the real checker scope.
fn markimportqualifier(c: *checker, scope: *syntax.scope, owner: str,
source: i32, alias: str) void = {
if (alias.len == 0) { return; };
if (syntax.scopelookup(scope, alias) != nil) { return; };
if (!srcimports(c.file, owner, source, alias)) { return; };
let marked: str = usepathfor(c.file, owner, source, alias);
};
fn markimportusesnode(c: *checker, n: *syntax.node, scope: *syntax.scope,
owner: str, source: i32, top: bool) void = {
if (n == nil) { return; };
if (n.kind == syntax.nkind.N_TNAME) {
// strings.rcut returns the whole input as its head on a miss. Require
// a real qualifier separator so a bare package TNAME never counts as use.
let k: syntax.nkind = n.kind;
if (k == syntax.nkind.N_USE) { return; };
if (k == syntax.nkind.N_TNAME) {
if (strings.contains(n.str, ".")) {
let (head, leaf) = strings.rcut(n.str, ".");
findusepath(c.file, owner, source, head, true);
markimportqualifier(c, scope, owner, source, head);
};
} else { if (n.kind == syntax.nkind.N_DOT && n.lhs != nil
&& n.lhs.kind == syntax.nkind.N_IDENT) {
findusepath(c.file, owner, source, n.lhs.str, true);
}; };
return;
};
if (k == syntax.nkind.N_DOT) {
if (n.lhs != nil && n.lhs.kind == syntax.nkind.N_IDENT) {
markimportqualifier(c, scope, owner, source, n.lhs.str);
} else {
markimportusesnode(c, n.lhs, scope, owner, source, false);
};
return;
};
if (k == syntax.nkind.N_FNDECL) {
let p: *syntax.node = n.attr;
for (p != nil) {
markimportusesnode(c, p, scope, owner, source, false);
p = p.next;
};
markimportusesnode(c, n.lhs, scope, owner, source, false);
p = n.list;
for (p != nil) {
let a: *syntax.node = p.attr;
for (a != nil) {
markimportusesnode(c, a, scope, owner, source, false);
a = a.next;
};
markimportusesnode(c, p.lhs, scope, owner, source, false);
markimportusesnode(c, p.rhs, scope, owner, source, false);
p = p.next;
};
let fnscope: *syntax.scope = syntax.newscope(scope);
p = n.list;
for (p != nil) {
if (p.kind == syntax.nkind.N_PARAM && p.str.len > 0) {
syntax.scopedefine(fnscope, p.str,
syntax.skind.SK_PARAM, nil, p);
};
p = p.next;
};
markimportusesnode(c, n.body, fnscope, owner, source, false);
return;
};
if (k == syntax.nkind.N_BLOCK) {
let inner: *syntax.scope = syntax.newscope(scope);
let p: *syntax.node = n.list;
for (p != nil) {
markimportusesnode(c, p, inner, owner, source, false);
p = p.next;
};
return;
};
if (k == syntax.nkind.N_LET) {
let p: *syntax.node = n.attr;
for (p != nil) {
markimportusesnode(c, p, scope, owner, source, false);
p = p.next;
};
markimportusesnode(c, n.lhs, scope, owner, source, false);
markimportusesnode(c, n.rhs, scope, owner, source, false);
if (!top && n.str.len > 0) {
syntax.scopedefine(scope, n.str, syntax.skind.SK_VAR, nil, n);
};
return;
};
if (k == syntax.nkind.N_MLET) {
let a: *syntax.node = n.attr;
for (a != nil) {
markimportusesnode(c, a, scope, owner, source, false);
a = a.next;
};
markimportusesnode(c, n.rhs, scope, owner, source, false);
let p: *syntax.node = n.list;
for (p != nil) {
a = p.attr;
for (a != nil) {
markimportusesnode(c, a, scope, owner, source, false);
a = a.next;
};
markimportusesnode(c, p.lhs, scope, owner, source, false);
p = p.next;
};
p = n.list;
for (p != nil) {
if (p.str.len > 0) {
syntax.scopedefine(scope, p.str, syntax.skind.SK_VAR, nil, p);
};
p = p.next;
};
return;
};
if (k == syntax.nkind.N_FOR) {
let inner: *syntax.scope = syntax.newscope(scope);
markimportusesnode(c, n.lhs, inner, owner, source, false);
markimportusesnode(c, n.cond, inner, owner, source, false);
markimportusesnode(c, n.rhs, inner, owner, source, false);
markimportusesnode(c, n.body, inner, owner, source, false);
markimportusesnode(c, n.els, inner, owner, source, false);
return;
};
if (k == syntax.nkind.N_FORRANGE) {
markimportusesnode(c, n.lhs, scope, owner, source, false);
let inner: *syntax.scope = syntax.newscope(scope);
let p: *syntax.node = n.list;
for (p != nil) {
let a: *syntax.node = p.attr;
for (a != nil) {
markimportusesnode(c, a, scope, owner, source, false);
a = a.next;
};
markimportusesnode(c, p.lhs, scope, owner, source, false);
if (p.str.len > 0) {
syntax.scopedefine(inner, p.str, syntax.skind.SK_VAR, nil, p);
};
p = p.next;
};
if (n.list == nil && n.str.len > 0) {
syntax.scopedefine(inner, n.str, syntax.skind.SK_VAR, nil, n);
};
markimportusesnode(c, n.body, inner, owner, source, false);
markimportusesnode(c, n.els, scope, owner, source, false);
return;
};
if (k == syntax.nkind.N_MCASE) {
markimportusesnode(c, n.lhs, scope, owner, source, false);
let p: *syntax.node = n.list;
for (p != nil) {
markimportusesnode(c, p, scope, owner, source, false);
p = p.next;
};
let inner: *syntax.scope = syntax.newscope(scope);
if (n.str.len > 0) {
syntax.scopedefine(inner, n.str, syntax.skind.SK_VAR, nil, n);
};
markimportusesnode(c, n.body, inner, owner, source, false);
return;
};
let p: *syntax.node = n.attr;
for (p != nil) { markimportusesnode(c, p, owner, source); p = p.next; };
markimportusesnode(c, n.lhs, owner, source);
markimportusesnode(c, n.rhs, owner, source);
markimportusesnode(c, n.cond, owner, source);
markimportusesnode(c, n.body, owner, source);
markimportusesnode(c, n.els, owner, source);
for (p != nil) {
markimportusesnode(c, p, scope, owner, source, false);
p = p.next;
};
markimportusesnode(c, n.lhs, scope, owner, source, false);
markimportusesnode(c, n.rhs, scope, owner, source, false);
markimportusesnode(c, n.cond, scope, owner, source, false);
markimportusesnode(c, n.body, scope, owner, source, false);
markimportusesnode(c, n.els, scope, owner, source, false);
p = n.list;
for (p != nil) { markimportusesnode(c, p, owner, source); p = p.next; };
for (p != nil) {
markimportusesnode(c, p, scope, owner, source, false);
p = p.next;
};
};
fn markimportuses(c: *checker, file: *syntax.node) void = {
let d: *syntax.node = file.list;
for (d != nil) {
if (d.kind != syntax.nkind.N_USE) {
markimportusesnode(c, d, declmod(file, d), d.sourceid);
let scope: *syntax.scope = syntax.newscope(nil);
markimportusesnode(c, d, scope, declmod(file, d),
d.sourceid, true);
};
d = d.next;
};

View File

@@ -19743,3 +19743,642 @@ fn runtimepath(relative: str) str = {
&& !directoryhasfragment(root, ".install"));
clean(root);
};
// A file-local import package-name object participates in ordinary lexical
// lookup: a closer local wins from its declaration point through its scope,
// and the package qualifier is visible again afterward. Exercise that rule
// through raw and package builds, imported packages, every test source role,
// filtered retained execution, and transactional rejection/restoration.
@test fn lexical_import_bindings_shadow_normally() void = {
let root: str = fresh();
let source: str = strings.concat(root, "/source");
let wire: str = strings.concat(source, "/dep/wire");
let aliasdep: str = strings.concat(source, "/dep/aliasdep");
let builtindep: str = strings.concat(source, "/dep/builtindep");
let palette: str = strings.concat(source, "/dep/palette");
let shadowlib: str = strings.concat(source, "/dep/shadowlib");
let matrix: str = strings.concat(source, "/matrix");
let client: str = strings.concat(source, "/client");
let sibling: str = strings.concat(source, "/sibling");
let invalid: str = strings.concat(source, "/invalid");
let invalidscalar: str = strings.concat(source, "/invalidscalar");
let invalidtype: str = strings.concat(source, "/invalidtype");
let warm: str = strings.concat(source, "/warm");
let roleprod: str = strings.concat(source, "/roleprod");
let rolesame: str = strings.concat(source, "/rolesame");
let roleexternal: str = strings.concat(source, "/roleexternal");
let roleonly: str = strings.concat(source, "/roleonly");
let retainedcase: str = strings.concat(source, "/retainedcase");
let dirs: []str = [wire, aliasdep, builtindep, palette, shadowlib,
matrix, client, sibling, invalid, invalidscalar, invalidtype, warm,
roleprod, rolesame, roleexternal, roleonly, retainedcase];
let di: i32 = 0;
for (di < dirs.len) { mkdirall(dirs[di]); di += 1; };
writefile(strings.concat(wire, "/wire.ww"), strings.concat(
"package wire;\n",
"export type record = struct { n: i32 };\n",
"export type count = i32;\n",
"export fn value() i32 = { return 11; };\n"));
writefile(strings.concat(aliasdep, "/aliasdep.ww"),
"package aliasdep;\nexport fn value() i32 = { return 13; };\n");
writefile(strings.concat(builtindep, "/builtindep.ww"),
"package builtindep;\nexport fn value() i32 = { return 17; };\n");
writefile(strings.concat(palette, "/palette.ww"), strings.concat(
"package palette;\n",
"export type shade = enum i32 { RED = 100 };\n",
"export fn value() i32 = { return shade.RED: i32; };\n"));
// The initializer still sees the import, the declared local wins
// afterward, a nested binding is restored on exit, aliases behave like
// default names, and local dot/call fields beat package and enum shortcuts.
writefile(strings.concat(matrix, "/main.ww"), strings.concat(
"package main;\n",
"import dep.wire;\n",
"import stable dep.aliasdep;\n",
"import len dep.builtindep;\n",
"import shade dep.palette;\n",
"type box = struct { n: i32, RED: i32, call: *fn() i32 };\n",
"fn seven() i32 = { return 7; };\n",
"fn typedpair() (i32, wire.count) = { return 5, 6; };\n",
"fn localafter() bool = { let before: i32 = wire.value(); ",
"let wire: i32 = wire.value() + 1; ",
"return before == 11 && wire == 12; };\n",
"fn nestedrestore() bool = { let before: i32 = wire.value(); ",
"if (true) { let wire: i32 = 3; ",
"if (wire != 3) { return false; }; }; ",
"return before == 11 && wire.value() == 11; };\n",
"fn aliases() bool = { let a: i32 = stable.value(); ",
"let stable: i32 = stable.value() + 1; ",
"let b: i32 = len.value(); let len: i32 = len.value() + 1; ",
"return a == 13 && stable == 14 && b == 17 && len == 18; };\n",
"fn localfields() bool = { let wire: box = box { n = 5, ",
"RED = 0, call = &seven }; let imported: i32 = shade.value(); ",
"let shade: box = box { n = 0, RED = 19, call = &seven }; ",
"return wire.n == 5 && (*wire.call)() == 7 ",
"&& imported == 100 && shade.RED == 19; };\n",
"fn tupledecl() bool = { ",
"let (wire: i32, second: wire.count) = typedpair(); ",
"return wire == 5 && (second: i32) == 6; };\n",
"fn ordinaryfor() bool = { let total: i32 = wire.value(); ",
"for (let wire: i32 = 1; wire < 3; wire += 1) ",
"{ total += wire; }; ",
"return total == 14 && wire.value() == 11; };\n",
"fn main() i32 = { if (!localafter()) { return 1; }; ",
"if (!nestedrestore()) { return 2; }; ",
"if (!aliases()) { return 3; }; ",
"if (!localfields()) { return 4; }; ",
"if (!tupledecl()) { return 5; }; ",
"if (!ordinaryfor()) { return 6; }; return 42; };\n"));
let stages: []str = ["ww", "ww_ww"];
let tags: []str = ["c", "ww"];
let artifactpaths: []str = [
"/dep.wire.unit.ww", "/dep.wire.wwi", "/dep.wire.s",
"/dep.wire.o", "/dep.wire.a",
"/dep.aliasdep.unit.ww", "/dep.aliasdep.wwi", "/dep.aliasdep.s",
"/dep.aliasdep.o", "/dep.aliasdep.a",
"/dep.builtindep.unit.ww", "/dep.builtindep.wwi",
"/dep.builtindep.s", "/dep.builtindep.o", "/dep.builtindep.a",
"/dep.palette.unit.ww", "/dep.palette.wwi", "/dep.palette.s",
"/dep.palette.o", "/dep.palette.a",
"/matrix.unit.ww", "/matrix.wwi", "/matrix.s", "/matrix.o",
"/matrix.a", "/matrix.init.unit.ww", "/matrix.init.s",
"/matrix.init.o"];
let artifactrefs: []str = alloc([], artifactpaths.len: u64)!;
let matrixbin: str = "";
let matrixoutrefs: []str = ["", ""];
let matrixerrrefs: []str = ["", ""];
let si: i32 = 0;
for (si < stages.len) {
let work: str = strings.concat(root, "/matrix-work-", tags[si]);
let output: str = strings.concat(root, "/matrix-output-", tags[si]);
mkdirall(work);
let av: []str = [driver(stages[si]), "build", "-w", work,
"-I", source, "-o", output, "matrix"];
let out: commandout;
runcommand(root, strings.concat("import-shadow-matrix-", tags[si]), av,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0 && os.exists(output)
&& !os.exists(strings.concat(output, ".new"))
&& !os.exists(strings.concat(output, ".sepwork"))
&& !directoryhasnew(work)
&& !directoryhasfragment(work, ".wwtxn.")
&& !directoryhasfragment(work, ".install")
&& !directoryhasfragment(work, ".sepwork"));
matrixoutrefs[si] = strings.dup(out.stdout);
matrixerrrefs[si] = strings.dup(out.stderr);
let runav: []str = [output];
runcommand(root, strings.concat("import-shadow-matrix-run-", tags[si]),
runav, time.second, &out);
expectexit(&out, 42);
assert(out.stdout.len == 0 && out.stderr.len == 0);
let ai: i32 = 0;
for (ai < artifactpaths.len) {
let bytes: str = readfile(strings.concat(work, artifactpaths[ai]));
if (si == 0) { append(artifactrefs, strings.dup(bytes)); }
else { assert(same(artifactrefs[ai], bytes)); };
ai += 1;
};
if (si == 0) { matrixbin = strings.dup(readfile(output)); }
else { assert(same(matrixbin, readfile(output))); };
si += 1;
};
assert(same(matrixoutrefs[0], matrixoutrefs[1])
&& same(matrixerrrefs[0], matrixerrrefs[1]));
// A raw source operand and a package reached through an import graph use
// the same checker and declaration-point rule.
let raw: str = strings.concat(root, "/raw-shadow.ww");
writefile(raw, strings.concat(
"package main;\nimport dep.wire;\n",
"fn main() i32 = { let wire: i32 = wire.value() + 31; ",
"return wire; };\n"));
writefile(strings.concat(shadowlib, "/shadowlib.ww"), strings.concat(
"package shadowlib;\nimport dep.wire;\n",
"export fn value() i32 = { let before: i32 = wire.value(); ",
"let wire: i32 = wire.value() + 31; ",
"if (before != 11) { return 1; }; return wire; };\n"));
writefile(strings.concat(client, "/main.ww"), strings.concat(
"package main;\nimport dep.shadowlib;\n",
"fn main() i32 = { return shadowlib.value(); };\n"));
writefile(strings.concat(sibling, "/imported.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"fn imported() i32 = { return wire.value(); };\n"));
writefile(strings.concat(sibling, "/main.ww"), strings.concat(
"package main;\ntype box = struct { n: i32 };\n",
"fn main() i32 = { let wire: box = box { n = 31 }; ",
"return imported() + wire.n; };\n"));
let rawbin: str = "";
let clientbin: str = "";
let siblingbin: str = "";
si = 0;
for (si < stages.len) {
let rawwork: str = strings.concat(root, "/raw-work-", tags[si]);
let rawout: str = strings.concat(root, "/raw-output-", tags[si]);
mkdirall(rawwork);
let rawav: []str = [driver(stages[si]), "build", "-w", rawwork,
"-I", source, "-o", rawout, raw];
let out: commandout;
runcommand(root, strings.concat("import-shadow-raw-", tags[si]), rawav,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0 && os.exists(rawout)
&& !os.exists(strings.concat(rawout, ".sepwork"))
&& !directoryhasnew(rawwork)
&& !directoryhasfragment(rawwork, ".wwtxn.")
&& !directoryhasfragment(rawwork, ".sepwork"));
let runav: []str = [rawout];
runcommand(root, strings.concat("import-shadow-raw-run-", tags[si]),
runav, time.second, &out);
expectexit(&out, 42);
assert(out.stdout.len == 0 && out.stderr.len == 0);
if (si == 0) { rawbin = strings.dup(readfile(rawout)); }
else { assert(same(rawbin, readfile(rawout))); };
let clientwork: str = strings.concat(root, "/client-work-", tags[si]);
let clientout: str = strings.concat(root, "/client-output-", tags[si]);
mkdirall(clientwork);
let clientav: []str = [driver(stages[si]), "build", "-w", clientwork,
"-I", source, "-o", clientout, "client"];
runcommand(root, strings.concat("import-shadow-client-", tags[si]),
clientav, (120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0
&& os.exists(clientout)
&& !os.exists(strings.concat(clientout, ".sepwork"))
&& !directoryhasnew(clientwork)
&& !directoryhasfragment(clientwork, ".wwtxn.")
&& !directoryhasfragment(clientwork, ".sepwork"));
let clientrun: []str = [clientout];
runcommand(root, strings.concat("import-shadow-client-run-", tags[si]),
clientrun, time.second, &out);
expectexit(&out, 42);
assert(out.stdout.len == 0 && out.stderr.len == 0);
if (si == 0) { clientbin = strings.dup(readfile(clientout)); }
else { assert(same(clientbin, readfile(clientout))); };
let siblingwork: str = strings.concat(root, "/sibling-work-", tags[si]);
let siblingout: str = strings.concat(root, "/sibling-output-", tags[si]);
mkdirall(siblingwork);
let siblingav: []str = [driver(stages[si]), "build", "-w", siblingwork,
"-I", source, "-o", siblingout, "sibling"];
runcommand(root, strings.concat("import-shadow-sibling-", tags[si]),
siblingav, (120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0
&& os.exists(siblingout)
&& !os.exists(strings.concat(siblingout, ".sepwork"))
&& !directoryhasnew(siblingwork)
&& !directoryhasfragment(siblingwork, ".wwtxn.")
&& !directoryhasfragment(siblingwork, ".sepwork"));
let siblingrun: []str = [siblingout];
runcommand(root, strings.concat("import-shadow-sibling-run-", tags[si]),
siblingrun, time.second, &out);
expectexit(&out, 42);
assert(out.stdout.len == 0 && out.stderr.len == 0);
if (si == 0) { siblingbin = strings.dup(readfile(siblingout)); }
else { assert(same(siblingbin, readfile(siblingout))); };
si += 1;
};
// A selector on a closer local is not package use. The otherwise-unused
// import is diagnosed first by source position, ahead of the later invalid
// local selector, and the request commits no cold work or publication.
writefile(strings.concat(invalid, "/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"fn main() i32 = { let wire: i32 = 2; ",
"return wire.n; };\n"));
writefile(strings.concat(invalidscalar, "/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"fn main() i32 = { let before: i32 = wire.value(); ",
"let wire: i32 = 2; if (before == 0) { return 0; }; ",
"return wire.n; };\n"));
writefile(strings.concat(invalidscalar, "/main_test.ww"),
"package main;\n@test fn never() void = { assert(main() == 0); };\n");
writefile(strings.concat(invalidtype, "/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"fn main() i32 = { let before: i32 = wire.value(); ",
"let wire: i32 = 2; let later: wire.record; ",
"return before + wire; };\n"));
let invaliddiag: str = "";
si = 0;
for (si < stages.len) {
let work: str = strings.concat(root, "/invalid-work-", tags[si]);
let output: str = strings.concat(root, "/invalid-output-", tags[si]);
mkdirall(work);
let av: []str = [driver(stages[si]), "build", "-w", work,
"-I", source, "-o", output, "invalid"];
let out: commandout;
runcommand(root, strings.concat("import-shadow-invalid-", tags[si]), av,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(out.stdout.len == 0
&& same(primarydiagnostic(out.stderr),
"\"dep.wire\" imported and not used")
&& occurrences(out.stderr, "imported and not used") == 1
&& occurrences(out.stderr, "selector 'n' undefined") == 1
&& !has(out.stderr, "shadows imported module")
&& !has(out.stderr, "use of package wire not in selector")
&& !has(out.stderr, "asserttyped:")
&& !has(out.stderr, "undefined reference")
&& !os.exists(output)
&& !os.exists(strings.concat(output, ".new"))
&& !os.exists(strings.concat(output, ".sepwork"))
&& directoryisempty(work));
let normalized: str = normalizedtrace(out.stderr,
strings.concat(work, "/"), output);
if (si == 0) { invaliddiag = strings.dup(normalized); }
else { assert(same(invaliddiag, normalized)); };
si += 1;
};
// Once the closer binding exists, value and type selectors must stay on
// that local object. Invalid local value selection is one positioned
// checker error, and a dotted type cannot recover the hidden package name.
let invalidtargets: []str = ["invalidscalar", "invalidtype"];
let invalidcores: []str = ["selector 'n' undefined",
"unknown type 'wire.record'"];
let invalidlocations: []str = [":4:109: error: selector 'n' undefined",
":4:81: error: unknown type 'wire.record'"];
let invalidshaperefs: []str = ["", ""];
let invalidretainedrefs: []str = ["", ""];
si = 0;
for (si < stages.len) {
let ii: i32 = 0;
for (ii < invalidtargets.len) {
let work: str = strings.concat(root, "/invalid-shape-work-",
tags[si], "-", boundarypkgname(ii));
let output: str = strings.concat(root, "/invalid-shape-output-",
tags[si], "-", boundarypkgname(ii));
mkdirall(work);
let av: []str = [driver(stages[si]), "build", "-w", work,
"-I", source, "-o", output, invalidtargets[ii]];
let out: commandout;
runcommand(root, strings.concat("import-shadow-invalid-shape-",
tags[si], "-", boundarypkgname(ii)), av,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(out.stdout.len == 0
&& same(primarydiagnostic(out.stderr), invalidcores[ii])
&& has(out.stderr, invalidlocations[ii])
&& occurrences(out.stderr, invalidcores[ii]) == 1
&& !has(out.stderr, "shadows imported module")
&& !has(out.stderr, "asserttyped:")
&& !has(out.stderr, "calling non-function")
&& !has(out.stderr, "undefined reference")
&& !os.exists(output)
&& !os.exists(strings.concat(output, ".new"))
&& !os.exists(strings.concat(output, ".sepwork"))
&& directoryisempty(work));
let normalized: str = normalizedtrace(out.stderr,
strings.concat(work, "/"), output);
if (si == 0) {
invalidshaperefs[ii] = strings.dup(normalized);
} else { assert(same(invalidshaperefs[ii], normalized)); };
ii += 1;
};
// A cold retained test request must reject before producing a runnable
// or any committed/staged work artifact.
let testwork: str = strings.concat(root,
"/invalid-retained-work-", tags[si]);
let retained: str = strings.concat(root,
"/invalid-retained-", tags[si], ".test");
mkdirall(testwork);
let testav: []str = [driver(stages[si]), "test", "-w", testwork,
"-I", source, "-run", "never", "-o", retained,
"invalidscalar"];
let tout: commandout;
runcommand(root, strings.concat("import-shadow-invalid-retained-",
tags[si]), testav,
(120i64 * (time.second: i64)): time.duration, &tout);
expectexit(&tout, 1);
assert(same(tout.stdout, "FAIL\n")
&& same(primarydiagnostic(tout.stderr), invalidcores[0])
&& has(tout.stderr, invalidlocations[0])
&& occurrences(tout.stderr, invalidcores[0]) == 1
&& !has(tout.stderr, "asserttyped:")
&& !os.exists(retained)
&& !os.exists(strings.concat(retained, ".new"))
&& !os.exists(strings.concat(retained, ".sepwork"))
&& directoryisempty(testwork));
let testnormalized: str = normalizedtrace(tout.stderr,
strings.concat(testwork, "/"), retained);
if (si == 0) {
invalidretainedrefs[si] = strings.dup(testnormalized);
} else {
invalidretainedrefs[si] = strings.dup(testnormalized);
assert(same(invalidretainedrefs[0], invalidretainedrefs[1]));
};
si += 1;
};
// Production called by a test, same-package test source, external-test
// source, and an honest test-only package all accept the same lexical rule.
writefile(strings.concat(roleprod, "/prod.ww"), strings.concat(
"package roleprod;\nimport dep.wire;\n",
"fn value() i32 = { let wire: i32 = wire.value() + 31; ",
"return wire; };\n"));
writefile(strings.concat(roleprod, "/prod_test.ww"),
"package roleprod;\n@test fn shadow_ok() void = { assert(value() == 42); };\n");
writefile(strings.concat(rolesame, "/prod.ww"),
"package rolesame;\nfn anchor() i32 = { return 1; };\n");
writefile(strings.concat(rolesame, "/same_test.ww"), strings.concat(
"package rolesame;\nimport dep.wire;\n",
"@test fn shadow_ok() void = { let before: i32 = wire.value(); ",
"let wire: i32 = wire.value() + 31; ",
"assert(anchor() == 1 && before == 11 && wire == 42); };\n"));
writefile(strings.concat(roleexternal, "/prod.ww"),
"package roleexternal;\nexport fn anchor() i32 = { return 1; };\n");
writefile(strings.concat(roleexternal, "/external_test.ww"), strings.concat(
"package roleexternal_test;\nimport dep.wire;\n",
"@test fn shadow_ok() void = { let wire: i32 = wire.value() + 31; ",
"assert(wire == 42); };\n"));
writefile(strings.concat(roleonly, "/only_test.ww"), strings.concat(
"package roleonly;\nimport dep.wire;\n",
"@test fn shadow_ok() void = { let wire: i32 = wire.value() + 31; ",
"assert(wire == 42); };\n"));
let roletargets: []str = ["roleprod", "rolesame", "roleexternal",
"roleonly"];
let roleoutrefs: []str = ["", "", "", ""];
let roleerrrefs: []str = ["", "", "", ""];
let rolebinrefs: []str = ["", "", "", ""];
si = 0;
for (si < stages.len) {
let ri: i32 = 0;
for (ri < roletargets.len) {
let work: str = strings.concat(root, "/role-work-", tags[si], "-",
boundarypkgname(ri));
let output: str = strings.concat(root, "/role-output-", tags[si], "-",
boundarypkgname(ri));
mkdirall(work);
let av: []str = [driver(stages[si]), "test", "-w", work,
"-I", source, "-run", "shadow_ok", "-o", output,
roletargets[ri]];
let out: commandout;
runcommand(root, strings.concat("import-shadow-role-", tags[si], "-",
boundarypkgname(ri)), av,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stderr.len == 0 && os.exists(output)
&& !os.exists(strings.concat(output, ".sepwork"))
&& occurrences(out.stdout, ".shadow_ok ... ok\n") == 1
&& occurrences(out.stdout,
"1 passed, 0 failed, 0 skipped, 0 harness errors\n") == 1
&& !directoryhasnew(work)
&& !directoryhasfragment(work, ".wwtxn.")
&& !directoryhasfragment(work, ".install")
&& !directoryhasfragment(work, ".sepwork"));
if (si == 0) {
roleoutrefs[ri] = strings.dup(out.stdout);
roleerrrefs[ri] = strings.dup(out.stderr);
rolebinrefs[ri] = strings.dup(readfile(output));
} else {
assert(same(roleoutrefs[ri], out.stdout)
&& same(roleerrrefs[ri], out.stderr)
&& same(rolebinrefs[ri], readfile(output)));
};
ri += 1;
};
si += 1;
};
// Warm semantic rejection preserves every committed action byte and public
// binary. Restoring exact input reuses the same generation byte-for-byte.
let warmfile: str = strings.concat(warm, "/main.ww");
let warmvalid: str = strings.concat(
"package main;\nimport dep.wire;\n",
"fn main() i32 = { let wire: i32 = wire.value() + 31; ",
"return wire; };\n");
let warminvalid: str = strings.concat(
"package main;\nimport dep.wire;\n",
"fn main() i32 = { let wire: i32 = 42; ",
"return wire.n; };\n");
writefile(warmfile, warmvalid);
let warmpaths: []str = ["/.wwtool.ww", "/.wwtool.w6c", "/.wwtool.w6a",
"/.wwtool.stamp", "/dep.wire.unit.ww", "/dep.wire.wwi",
"/dep.wire.s", "/dep.wire.o", "/dep.wire.a", "/warm.unit.ww",
"/warm.wwi", "/warm.s", "/warm.o", "/warm.a",
"/warm.init.unit.ww", "/warm.init.s", "/warm.init.o"];
let warmcross: []str = alloc([], warmpaths.len: u64)!;
let warmbincross: str = "";
let warmdiag: str = "";
// Retention uses a production function called by the selected test. The
// unselected aborting row proves filtering survives retained/direct routes.
let retainedfile: str = strings.concat(retainedcase, "/prod.ww");
let retainedvalid: str = strings.concat(
"package retainedcase;\nimport dep.wire;\n",
"fn value() i32 = { let wire: i32 = wire.value() + 31; ",
"return wire; };\n");
let retainedinvalid: str = strings.concat(
"package retainedcase;\nimport dep.wire;\n",
"fn value() i32 = { let wire: i32 = 42; ",
"return wire.n; };\n");
writefile(retainedfile, retainedvalid);
writefile(strings.concat(retainedcase, "/retained_test.ww"), strings.concat(
"package retainedcase;\n",
"@test fn kept() void = { assert(value() == 42); };\n",
"@test fn not_selected() void = { abort(); };\n"));
let retainedcross: str = "";
let retainedoutrefs: []str = ["", ""];
let retainederrrefs: []str = ["", ""];
let retainedinvaliddiagrefs: []str = ["", ""];
let directoutrefs: []str = ["", ""];
let directerrrefs: []str = ["", ""];
si = 0;
for (si < stages.len) {
let work: str = strings.concat(root, "/warm-work-", tags[si]);
let output: str = strings.concat(root, "/warm-output-", tags[si]);
mkdirall(work);
let av: []str = [driver(stages[si]), "build", "-w", work,
"-I", source, "-o", output, "warm"];
let out: commandout;
runcommand(root, strings.concat("import-shadow-warm-cold-", tags[si]), av,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0 && os.exists(output));
let snapshots: []str = alloc([], warmpaths.len: u64)!;
let wi: i32 = 0;
for (wi < warmpaths.len) {
let bytes: str = strings.dup(readfile(strings.concat(work,
warmpaths[wi])));
append(snapshots, bytes);
if (si == 0) { append(warmcross, strings.dup(bytes)); }
else if (wi >= 4) { assert(same(warmcross[wi], bytes)); };
wi += 1;
};
let binbytes: str = strings.dup(readfile(output));
if (si == 0) { warmbincross = strings.dup(binbytes); }
else { assert(same(warmbincross, binbytes)); };
rewritefile(warmfile, warminvalid);
runcommand(root, strings.concat("import-shadow-warm-invalid-", tags[si]),
av, (120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(out.stdout.len == 0
&& same(primarydiagnostic(out.stderr),
"\"dep.wire\" imported and not used")
&& occurrences(out.stderr, "selector 'n' undefined") == 1
&& same(binbytes, readfile(output))
&& !os.exists(strings.concat(output, ".new"))
&& !os.exists(strings.concat(output, ".sepwork")));
let normalized: str = normalizedtrace(out.stderr,
strings.concat(work, "/"), output);
if (si == 0) { warmdiag = strings.dup(normalized); }
else { assert(same(warmdiag, normalized)); };
wi = 0;
for (wi < warmpaths.len) {
assert(same(snapshots[wi], readfile(strings.concat(work,
warmpaths[wi]))));
wi += 1;
};
assert(!directoryhasnew(work)
&& !directoryhasfragment(work, ".wwtxn.")
&& !directoryhasfragment(work, ".install")
&& !directoryhasfragment(work, ".sepwork"));
rewritefile(warmfile, warmvalid);
runcommand(root, strings.concat("import-shadow-warm-restored-", tags[si]),
av, (120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0
&& same(binbytes, readfile(output))
&& !os.exists(strings.concat(output, ".sepwork"))
&& !directoryhasnew(work)
&& !directoryhasfragment(work, ".wwtxn.")
&& !directoryhasfragment(work, ".install")
&& !directoryhasfragment(work, ".sepwork"));
wi = 0;
for (wi < warmpaths.len) {
assert(same(snapshots[wi], readfile(strings.concat(work,
warmpaths[wi]))));
wi += 1;
};
let testwork: str = strings.concat(root, "/retained-work-", tags[si]);
let retained: str = strings.concat(root, "/retained-", tags[si], ".test");
mkdirall(testwork);
let testav: []str = [driver(stages[si]), "test", "-w", testwork,
"-I", source, "-run", "kept", "-o", retained,
"retainedcase"];
runcommand(root, strings.concat("import-shadow-retained-cold-", tags[si]),
testav, (120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stderr.len == 0 && os.exists(retained)
&& !os.exists(strings.concat(retained, ".sepwork"))
&& occurrences(out.stdout, "retainedcase.kept ... ok\n") == 1
&& occurrences(out.stdout,
"1 passed, 0 failed, 0 skipped, 0 harness errors\n") == 1
&& !has(out.stdout, "not_selected")
&& !directoryhasnew(testwork)
&& !directoryhasfragment(testwork, ".wwtxn.")
&& !directoryhasfragment(testwork, ".sepwork"));
retainedoutrefs[si] = strings.dup(out.stdout);
retainederrrefs[si] = strings.dup(out.stderr);
let retainedbytes: str = strings.dup(readfile(retained));
if (si == 0) { retainedcross = strings.dup(retainedbytes); }
else { assert(same(retainedcross, retainedbytes)); };
rewritefile(retainedfile, retainedinvalid);
runcommand(root, strings.concat("import-shadow-retained-invalid-",
tags[si]), testav,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(out.stdout, "FAIL\n")
&& same(primarydiagnostic(out.stderr),
"\"dep.wire\" imported and not used")
&& occurrences(out.stderr, "selector 'n' undefined") == 1
&& same(retainedbytes, readfile(retained))
&& !os.exists(strings.concat(retained, ".new"))
&& !os.exists(strings.concat(retained, ".sepwork"))
&& !directoryhasnew(testwork)
&& !directoryhasfragment(testwork, ".wwtxn.")
&& !directoryhasfragment(testwork, ".install")
&& !directoryhasfragment(testwork, ".sepwork"));
let retainedinvalidnormalized: str = normalizedtrace(out.stderr,
strings.concat(testwork, "/"), retained);
retainedinvaliddiagrefs[si] = strings.dup(retainedinvalidnormalized);
if (si == 1) {
assert(same(retainedinvaliddiagrefs[0],
retainedinvaliddiagrefs[1]));
};
let directav: []str = [retained, "-package=retainedcase", "kept"];
runcommand(root, strings.concat("import-shadow-retained-direct-",
tags[si]), directav,
(30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stderr.len == 0
&& occurrences(out.stdout, "retainedcase.kept ... ok\n") == 1
&& occurrences(out.stdout,
"1 passed, 0 failed, 0 skipped, 0 harness errors\n") == 1
&& !has(out.stdout, "not_selected"));
directoutrefs[si] = strings.dup(out.stdout);
directerrrefs[si] = strings.dup(out.stderr);
rewritefile(retainedfile, retainedvalid);
runcommand(root, strings.concat("import-shadow-retained-restored-",
tags[si]), testav,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(same(out.stdout, retainedoutrefs[si])
&& same(out.stderr, retainederrrefs[si])
&& same(retainedbytes, readfile(retained))
&& !os.exists(strings.concat(retained, ".sepwork"))
&& !directoryhasnew(testwork)
&& !directoryhasfragment(testwork, ".wwtxn.")
&& !directoryhasfragment(testwork, ".install")
&& !directoryhasfragment(testwork, ".sepwork"));
si += 1;
};
assert(same(retainedoutrefs[0], retainedoutrefs[1])
&& same(retainederrrefs[0], retainederrrefs[1])
&& same(directoutrefs[0], directoutrefs[1])
&& same(directerrrefs[0], directerrrefs[1]));
assert(!directoryhasnew(root)
&& !directoryhasfragment(root, ".wwtxn.")
&& !directoryhasfragment(root, ".install")
&& !directoryhasfragment(root, ".sepwork")
&& !directoryhasfragment(root, ".capture")
&& !directoryhasfragment(root, ".result")
&& !directoryhasfragment(root, ".request"));
clean(root);
};

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 = {

View File

@@ -1,8 +1,6 @@
// crossmod — directory package 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 package imports that leaf.
// crossmod — directory package whose probe() takes a parameter named like the
// module `shadowmod` that a sibling package imports. Since crossmod itself has
// no such file-local package-name object, this is an ordinary parameter.
package crossmod;

View File

@@ -1,15 +1,14 @@
// neg_forrange_single — `for (let shadowmod .. s)` single-binding
// range loop where the loop variable shadows the imported module.
// N_FORRANGE wires check_module_shadow on the single-name branch
// (n->str), so the rule fires at the for header.
// Legacy neg_forrange_single, now a positive range-binding case. The import
// initializes total; the one-byte "A" iteration binds the closer shadowmod
// value in the loop body. Builds and runs 42 + 65 = 107.
package paramshadowmod;
package main;
import shadowmod;
export fn main() i32 = {
let s: str = "abc";
let total: i32 = 0i32;
fn main() i32 = {
let s: str = "A";
let total: i32 = shadowmod.say();
for (let shadowmod .. s) {
total += shadowmod: i32;
};

View File

@@ -1,14 +1,12 @@
// neg_forrange_tuple — `for (let (shadowmod, x) .. s)` tuple-
// destructure range loop where the first binder shadows the
// imported module. N_FORRANGE wires check_module_shadow per-name
// on the tuple branch (n->list), so the rule fires at the for
// header even though `x` is innocuous.
// Legacy neg_forrange_tuple, now a positive tuple-range case. The import
// initializes total; the loop's first tuple element is the closer shadowmod
// binding. Builds and runs 42 + 1 + 2 = 45.
package paramshadowmod;
package main;
import shadowmod;
export fn main() i32 = {
fn main() i32 = {
let buf: [2]i64;
buf[0] = 1i64;
buf[1] = 2i64;
@@ -16,7 +14,7 @@ export fn main() i32 = {
s.ptr = buf.ptr: *(i64, i64);
s.len = 1;
s.cap = 1;
let total: i64 = 0i64;
let total: i64 = shadowmod.say(): i64;
for (let (shadowmod, x) .. s) {
total += shadowmod + x;
};

View File

@@ -1,12 +1,12 @@
// 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.
// Legacy neg_let, now a positive declaration-point case. The initializer
// resolves shadowmod.say through the file-scope import; after the declaration
// shadowmod denotes the closer i32 binding. Builds and runs 42.
package paramshadowmod;
package main;
import shadowmod;
export fn main() i32 = {
let shadowmod: i32 = 0i32;
fn main() i32 = {
let shadowmod: i32 = shadowmod.say();
return shadowmod;
};

View File

@@ -1,9 +1,8 @@
// neg_mcase — `match (r) { case let shadowmod: i64 => ... }` where
// the per-arm binder shadows the imported module. N_MCASE wires
// check_module_shadow before scope_define on cs->str, so the rule
// fires at the case line.
// Legacy neg_mcase, now a positive match-arm case. The import supplies the
// tagged value before the arm binder exists; inside that arm shadowmod is the
// closer i64 binding. Builds and runs 42.
package paramshadowmod;
package main;
import shadowmod;
@@ -14,8 +13,8 @@ fn parse(n: i64) (i64 | i32) = {
return n;
};
export fn main() i32 = {
let r: (i64 | i32) = parse(42i64);
fn main() i32 = {
let r: (i64 | i32) = parse(shadowmod.say(): i64);
let out: i64 = 0i64;
match (r) {
case let shadowmod: i64 => out = shadowmod;

View File

@@ -1,9 +1,8 @@
// neg_mlet — Hare tuple destructure `let (shadowmod, x) = pair();`
// where the first binder shadows the imported module. N_MLET wires
// check_module_shadow per-binder, so the rule fires at the first
// name; the second binder `x` is innocuous.
// Legacy neg_mlet, now a positive tuple-binding case. The selector use before
// the destructure belongs to the import; afterward shadowmod is the first
// tuple element. Builds and runs 42 + 1 + 2 = 45.
package paramshadowmod;
package main;
import shadowmod;
@@ -11,7 +10,8 @@ fn pair() (i64, i64) = {
return 1i64, 2i64;
};
export fn main() i32 = {
fn main() i32 = {
let anchor: i32 = shadowmod.say();
let (shadowmod, x) = pair();
return (shadowmod + x): i32;
return anchor + (shadowmod + x): i32;
};

View File

@@ -1,8 +1,8 @@
// 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.
// Legacy neg_param, now a positive lexical-scope case. The package selector
// in main uses the file-scope import; the parameter named shadowmod is a
// closer binding only inside probe. Builds and runs 42 + len("hi") = 44.
package paramshadowmod;
package main;
import shadowmod;
@@ -10,6 +10,6 @@ fn probe(shadowmod: str) i32 = {
return shadowmod.len;
};
export fn main() i32 = {
return probe("hi");
fn main() i32 = {
return shadowmod.say() + probe("hi");
};

View File

@@ -1,17 +1,14 @@
// 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.
// pos_crossmod — file-local import ownership control. This source imports
// shadowmod, while crossmod's separate source has a parameter of that name
// and no such import. The caller's qualifier cannot leak into the sibling
// package's lexical scope. Both stages build and run 2.
package main;
import shadowmod;
import crossmod;
export fn main() i32 = {
fn main() i32 = {
let _ = shadowmod.say();
return crossmod.probe("hi");
};

View File

@@ -1,6 +1,5 @@
// 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.
// pos_rename — control with an unrelated parameter name. The file-scope
// import remains visible in probe. Both stages build and run 42.
package main;
@@ -11,6 +10,6 @@ fn probe(s: str) i32 = {
return shadowmod.say();
};
export fn main() i32 = {
fn main() i32 = {
return probe("hi");
};

View File

@@ -1,6 +1,6 @@
// 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.
// paramshadowmod/shadowmod — the sibling package used by the lexical
// import-shadowing fixtures. The exported function makes each pre-binding
// qualifier occurrence observable at runtime.
package shadowmod;