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 void cstmt(Checker*, Node*);
static Type *cexpr(Checker*, Node*); static Type *cexpr(Checker*, Node*);
static Type *resolve_type(Checker*, Node*); static Type *resolve_type(Checker*, Node*);
static void check_module_shadow(Checker*, const char *name, Pos,
const char *kindstr);
static Type * static Type *
err(Checker *c, Pos p, const char *fmt, ...) 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(Checker *c, const char *name);
static Sym *lookup_visible_type(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 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_values(Checker *c, Node *n);
static int reject_bare_import_types(Checker *c, Node *n); static int reject_bare_import_types(Checker *c, Node *n);
static void resolve_typedecl(Checker *c, Node *d); 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: { case N_DOT: {
if (n->lhs == NULL || n->lhs->kind != N_IDENT) return 0; 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. */ /* M1 #22: map the qualifier alias to its dotted import path. */
const char *mk = use_path(c->file, c->cur_mod, c->cur_source, const char *mk = use_path(c->file, c->cur_mod, c->cur_source,
n->lhs->str); n->lhs->str);
@@ -1618,6 +1620,10 @@ cexpr(Checker *c, Node *n)
return n->type = base; 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, return n->type = err(c, n->pos,
"no enum member '%s' in %s", "no enum member '%s' in %s",
n->str ? n->str : "?", n->str ? n->str : "?",
@@ -1650,6 +1656,10 @@ cexpr(Checker *c, Node *n)
for (Tfield *f = u->fields; f; f = f->next) for (Tfield *f = u->fields; f; f = f->next)
if (strcmp(f->name, n->str) == 0) if (strcmp(f->name, n->str) == 0)
return n->type = f->type; 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", return n->type = err(c, n->pos, "no field '%s' in %s",
n->str, type_name(c->a, base)); n->str, type_name(c->a, base));
} }
@@ -1660,17 +1670,32 @@ cexpr(Checker *c, Node *n)
if (*q < '0' || *q > '9') { idx = -1; break; } if (*q < '0' || *q > '9') { idx = -1; break; }
idx = idx * 10 + (*q - '0'); 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) if (idx < 0)
return n->type = err(c, n->pos, return n->type = err(c, n->pos,
"tuple field must be numeric"); "tuple field must be numeric");
Tparam *tp = u->params; Tparam *tp = u->params;
while (idx > 0 && tp) { tp = tp->next; idx--; } 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) if (tp == NULL)
return n->type = err(c, n->pos, return n->type = err(c, n->pos,
"tuple index out of range"); "tuple index out of range");
return n->type = tp->type; 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; return n->type = ty_err;
} }
case N_INDEX: { case N_INDEX: {
@@ -2308,8 +2333,6 @@ cexpr(Checker *c, Node *n)
type_name(c->a, st)); type_name(c->a, st));
} }
if (cs->str && cs->str[0]) { 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); 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); desugar_arrayslice(c, declared, n->rhs);
n->type = t; n->type = t;
if (n->str && n->str[0]) { 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); Sym *s = scope_define(c->cur, n->str, SK_VAR, t, n);
if (s == NULL) if (s == NULL)
err(c, n->pos, "let '%s' redeclared in same scope", 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) { for (Node *nm = n->list; nm; nm = nm->next) {
Type *ft = tp ? tp->type : ty_err; Type *ft = tp ? tp->type : ty_err;
if (nm->str && nm->str[0]) { if (nm->str && nm->str[0]) {
check_module_shadow(c, nm->str,
nm->pos, "binding");
if (scope_define(c->cur, nm->str, if (scope_define(c->cur, nm->str,
SK_VAR, ft, nm) == NULL) SK_VAR, ft, nm) == NULL)
err(c, nm->pos, err(c, nm->pos,
@@ -2764,7 +2784,6 @@ cstmt(Checker *c, Node *n)
if (tp) tp = tp->next; if (tp) tp = tp->next;
} }
} else if (n->str && n->str[0]) { } else if (n->str && n->str[0]) {
check_module_shadow(c, n->str, n->pos, "binding");
scope_define(c->cur, n->str, SK_VAR, scope_define(c->cur, n->str, SK_VAR,
elem ? elem : ty_err, n); elem ? elem : ty_err, n);
} }
@@ -2809,6 +2828,9 @@ cstmt(Checker *c, Node *n)
type_name(c->a, rt)); type_name(c->a, rt));
} }
Tparam *tp = u ? u->params : NULL; 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) { for (Node *l = n->list; l; l = l->next) {
Type *declared = l->lhs ? resolve_type(c, l->lhs) : NULL; Type *declared = l->lhs ? resolve_type(c, l->lhs) : NULL;
Type *elem = tp ? tp->type : NULL; Type *elem = tp ? tp->type : NULL;
@@ -2819,16 +2841,18 @@ cstmt(Checker *c, Node *n)
l->str, type_name(c->a, declared), l->str, type_name(c->a, declared),
type_name(c->a, elem)); type_name(c->a, elem));
l->type = t; l->type = t;
if (tp) tp = tp->next;
}
for (Node *l = n->list; l; l = l->next) {
if (l->str && l->str[0]) { 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,
Sym *s = scope_define(c->cur, l->str, SK_VAR, t, l); l->type, l);
if (s == NULL) if (s == NULL)
err(c, l->pos, err(c, l->pos,
"let '%s' redeclared in same scope", "let '%s' redeclared in same scope",
l->str); l->str);
else if (n->op == TK_CONST) s->is_const = 1; else if (n->op == TK_CONST) s->is_const = 1;
} }
if (tp) tp = tp->next;
} }
if (u && tp != NULL) if (u && tp != NULL)
err(c, n->pos, "tuple has extra elements"); err(c, n->pos, "tuple has extra elements");
@@ -3161,6 +3185,23 @@ lookup_bare_import_binding(Checker *c, const char *name)
return NULL; 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 /* Walk a type subtree only far enough to classify package-name objects. This
* is deliberately not general type checking: the enum/array constant folders * is deliberately not general type checking: the enum/array constant folders
* need the package diagnostic before they reduce an unsupported outer shape * 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; 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 /* Self-contained `.wwi` files can carry the same origin-owned type/const
* fact through two direct dependencies (a diamond), or alongside a direct * fact through two direct dependencies (a diamond), or alongside a direct
* import of that origin. In strict package mode those compiler-generated * 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 /* Import diagnostics precede body diagnostics, so usage must be known before
* qualified syntax before resolving declaration bodies so import diagnostics * pass 1. The temporary scopes mirror the real declaration points without
* retain production Go's source order without making a failed bare lookup a * contaminating the compilation scope that pass 1 owns. */
* use. WW already rejects lexical bindings that shadow an import qualifier. */
static void static void
mark_import_uses_node(Checker *c, Node *n, const char *owner, int source) mark_import_uses_node(Checker *c, Node *n, const char *owner, int source)
{ {
if (n == NULL) return; 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, '.'); const char *dot = strrchr(n->str, '.');
if (dot != NULL) { if (dot != NULL) {
char *head = astrndup(c->a, n->str, (size_t)(dot - n->str)); char *head = astrndup(c->a, n->str, (size_t)(dot - n->str));
if (scope_lookup(c->cur, head) == NULL)
(void)find_use_path(c->file, owner, source, head, 1); (void)find_use_path(c->file, owner, source, head, 1);
} }
} else if (n->kind == N_DOT && n->lhs != NULL return;
&& n->lhs->kind == N_IDENT && n->lhs->str != NULL) { }
(void)find_use_path(c->file, owner, source, n->lhs->str, 1); 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) for (Node *p = n->attr; p; p = p->next)
mark_import_uses_node(c, p, owner, source); 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 static void
mark_import_uses(Checker *c, Node *file) mark_import_uses(Checker *c, Node *file)
{ {
Scope *saved = c->cur;
for (Node *d = file->list; d; d = d->next) { for (Node *d = file->list; d; d = d->next) {
if (d->kind == N_USE) continue; if (d->kind == N_USE) continue;
c->cur = newscope(c->a, NULL);
mark_import_uses_node(c, d, decl_mod(file, d), d->sourceid); mark_import_uses_node(c, d, decl_mod(file, d), d->sourceid);
} }
c->cur = saved;
} }
static void static void
@@ -4882,8 +4980,6 @@ check_file(Checker *c, Node *file)
Type *fnt = d->type; Type *fnt = d->type;
for (Tparam *p = fnt->params; p; p = p->next) { for (Tparam *p = fnt->params; p; p = p->next) {
if (p->name && p->name[0]) { if (p->name && p->name[0]) {
check_module_shadow(c, p->name,
d->pos, "param");
if (scope_define(c->cur, p->name, if (scope_define(c->cur, p->name,
SK_PARAM, p->type, d) == NULL) SK_PARAM, p->type, d) == NULL)
err(c, d->pos, err(c, d->pos,

View File

@@ -606,10 +606,8 @@ struct Checker {
* rather than colliding io.read. */ * rather than colliding io.read. */
int cur_source; /* lexical source-file scope of the declaration int cur_source; /* lexical source-file scope of the declaration
* currently being checked. */ * currently being checked. */
Node *file; /* current N_FILE root; used by check_module_shadow Node *file; /* current N_FILE root; owns source-local import
* to consult the declaring source file's own `use` * qualifier lookup and usage accounting. */
* directives when refusing param/let names that
* would shadow an imported module bareword. */
int loops; /* nesting count for break/continue */ int loops; /* nesting count for break/continue */
int matcharms; /* nesting count for yield */ int matcharms; /* nesting count for yield */
int errs; 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 `19`, semantic storage format remains `3`, and no test-result cache is
introduced. 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 ## 12. Candidate architectures and hard-gate decision
Five candidates were developed as coherent systems, not as feature bins. 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 lookup rather than package-name diagnostics. Builtin spelling does not alter
the object: an import bound as `len`, `size`, `align`, or another builtin name the object: an import bound as `len`, `size`, `align`, or another builtin name
remains a selector-only package-name object. 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 Neither form exposes an imported declaration as a bare `Name`; ordinary
unqualified lookup remains limited to lexical, builtin, and same-package unqualified lookup remains limited to lexical, builtin, and same-package
declarations. A blank import creates no package-name object, an effective 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 checking when either spelling is instead an import binding; neither stage may
fall into an internal expression-kind diagnostic. 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 production, same-package test, external-test, and honest test-only source
roles. Cstage and WWstage must agree on status and normalized stdout/stderr, roles. Cstage and WWstage must agree on status and normalized stdout/stderr,
including diagnostic order and source position, and valid controls must produce 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 // → inline the pre-computed constant. `pkg.Enum.MEMBER` keeps
// `pkg` so enumlookupmod can prefer the explicit module on a // `pkg` so enumlookupmod can prefer the explicit module on a
// leaf collision; bare `Enum.MEMBER` falls back to c.curmod via // 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) { if (lhs != nil) {
let etname: str; let etname: str;
let etmod: str; let etmod: str;
etname.ptr = nil; etname.len = 0; etname.ptr = nil; etname.len = 0;
etmod.ptr = nil; etmod.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; etname = lhs.str;
}; };
if (lhs.kind == syntax.nkind.N_DOT) { if (lhs.kind == syntax.nkind.N_DOT) {

View File

@@ -37,9 +37,8 @@ type checker = struct {
// preference in bare-leaf lookups. // preference in bare-leaf lookups.
cursource: i32, // lexical source-file scope of the current decl; cursource: i32, // lexical source-file scope of the current decl;
// selects only that file's import bindings. // selects only that file's import bindings.
file: *syntax.node, // N_FILE root; used by checkmoduleshadow file: *syntax.node, // N_FILE root; owns source-local `use`
// to consult the declaring source's own // bindings and their usage state.
// `use` directives.
allococtx: *syntax.node, // #3/B': the one empty alloc([], n) call node allococtx: *syntax.node, // #3/B': the one empty alloc([], n) call node
// with let-declared slice context this walk; // with let-declared slice context this walk;
// any other empty alloc has no element hint // 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 // modkeyfor — the module key for a directly imported alias. Empty means
// the referencing package did not itself declare that import. // the referencing package did not itself declare that import.
fn modkeyfor(c: *checker, alias: str) str = { 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); 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; 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 = { fn lookupvisible(c: *checker, name: str) *syntax.sym = {
let found: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, name); let found: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, name);
let builtin: *syntax.sym = nil; let builtin: *syntax.sym = nil;
@@ -398,39 +432,6 @@ fn synthesizedtestrun(c: *checker, e: *syntax.node) bool = {
return c.synthtestrun != nil && c.synthtestrun == e; 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. // 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 // We don't compute its type yet (that's the resolve pass) — just bind
// the name so forward references resolve. // 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 // `let (a,b) = f()`, `for (let (a,b) .. s)`, and the ww-extension
// multi-assign `a, _ = f()`. // multi-assign `a, _ = f()`.
// //
// `define` (the binding contexts: let-unpack + for-range) installs each // `define` (the binding contexts: let-unpack + for-range) back-fills and
// named binder as a fresh SK_VAR and back-fills its declared type onto // resolves every declared type before installing any named binder as a fresh
// .lhs so use sites resolve through the N_IDENT exprtype path. Multi- // 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 // 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) // left untouched (an N_INDEX/N_DOT target carries a live operand there)
// and only the type_ stamp fires on the still-untyped slots. // 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 (pt != nil) { et = pt.lhs; };
if (define) { if (define) {
if (b.lhs == nil) { b.lhs = et; }; 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) { if (b.type_ == nil) {
let src: *syntax.node = b.lhs; let src: *syntax.node = b.lhs;
@@ -643,6 +629,27 @@ fn stamptuplebinds(c: *checker, binds: *syntax.node, elems: *syntax.node,
b = b.next; b = b.next;
if (pt != nil) { pt = pt.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 // resolvewalk — recursive AST walk that, for every nkind.N_IDENT and
@@ -818,7 +825,6 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
} else { } else {
let bnm: str = n.str; let bnm: str = n.str;
if (bnm.len > 0) { if (bnm.len > 0) {
checkmoduleshadow(c, bnm, "binding");
// C4 (task #7): bind the ELEMENT type so field // C4 (task #7): bind the ELEMENT type so field
// reads off a by-value aggregate binding // reads off a by-value aggregate binding
// (`for (let t .. threads) { t.pc }`) resolve — // (`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 // `for (init; cond; post) body` / `for (cond) body` — the body is the
// break/continue target. Walk init/cond/post outside the loop count // break/continue target. Its initializer scope covers cond/post/body/else;
// (they hold no statements), bump only around the body, and keep the // only the body is inside the loop count, so `else` still targets an
// `else` outside (it targets an enclosing loop, like N_FORRANGE above). // enclosing loop.
// Mirrors cstage cmd/wcc/check.c:2529 (N_FOR, c->loops++). // Mirrors cstage cmd/wcc/check.c:2529 (N_FOR, c->loops++).
if (k == syntax.nkind.N_FOR) { 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.lhs != nil) { resolvewalk(c, n.lhs); };
if (n.cond != nil) { resolvewalk(c, n.cond); }; if (n.cond != nil) { resolvewalk(c, n.cond); };
if (n.rhs != nil) { resolvewalk(c, n.rhs); }; 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); }; if (n.body != nil) { resolvewalk(c, n.body); };
c.loops -= 1; c.loops -= 1;
if (n.els != nil) { resolvewalk(c, n.els); }; if (n.els != nil) { resolvewalk(c, n.els); };
c.cur = forouter;
return; return;
}; };
@@ -930,7 +939,6 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
c.cur = syntax.newscope(outer); c.cur = syntax.newscope(outer);
let nm: str = n.str; let nm: str = n.str;
if (nm.len > 0) { if (nm.len > 0) {
checkmoduleshadow(c, nm, "binding");
syntax.scopedefine(c.cur, nm, syntax.skind.SK_VAR, nil, n); syntax.scopedefine(c.cur, nm, syntax.skind.SK_VAR, nil, n);
}; };
if (n.body != nil) { 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. // the un-annotated binder stays untyped and asserttyped aborts.
// Mirrors cstage check.c:2017 (cexpr(rhs), unconditional). // Mirrors cstage check.c:2017 (cexpr(rhs), unconditional).
if (n.rhs != nil) { 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 // #99 alias transparency: a NAMED tuple alias rhs
// (`type pair = (i64,i64)`) destructures like its // (`type pair = (i64,i64)`) destructures like its
// base — resolvealias mirrors cstage's // base — resolvealias mirrors cstage's
@@ -1158,7 +1164,6 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
if (k == syntax.nkind.N_LET) { if (k == syntax.nkind.N_LET) {
let nm: str = n.str; let nm: str = n.str;
if (nm.len > 0) { if (nm.len > 0) {
checkmoduleshadow(c, nm, "let");
let s: *syntax.sym = syntax.scopedefine(c.cur, nm, syntax.skind.SK_VAR, nil, n); let s: *syntax.sym = syntax.scopedefine(c.cur, nm, syntax.skind.SK_VAR, nil, n);
// catB-22: flag a `const` binding so checkassign can // catB-22: flag a `const` binding so checkassign can
// reject a later reassignment. The parser stamps // 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 // A.6.1.5b stamp cases — mirror cstage check.c:833-866. Pure
// type-AST stamps; never rewrite e.kind. Lenient on misses // type-AST stamps; never rewrite e.kind.
// (cstage errors); falls through to nil under scruttype L656.
// //
// Pseudo-fields .len/.cap/.ptr on slice/str/array. Cstage // Pseudo-fields .len/.cap/.ptr on slice/str/array. Cstage
// L833-842. `str` lives as N_TNAME("str") in wwstage — no // 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; return nil;
}; };
@@ -7631,7 +7652,6 @@ fn installparams(c: *checker, params: *syntax.node) void = {
normalizevariadicparam(p); normalizevariadicparam(p);
let nm: str = p.str; let nm: str = p.str;
if (nm.len > 0) { if (nm.len > 0) {
checkmoduleshadow(c, nm, "param");
syntax.scopedefine(c.cur, nm, syntax.skind.SK_PARAM, nil, p); 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); || d.kind == syntax.nkind.N_LET);
}; };
// Record only qualified syntax in each owning source before name resolution. // Mark only a qualifier whose spelling still denotes the source-local package
// This preserves source-position import diagnostics and never lets a failed // binding. The scratch scope contains lexical binders only; package and import
// bare lookup consume an ordinary import. // symbols remain owned by the real checker scope.
fn markimportusesnode(c: *checker, n: *syntax.node, owner: str, fn markimportqualifier(c: *checker, scope: *syntax.scope, owner: str,
source: i32) void = { 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 == nil) { return; };
if (n.kind == syntax.nkind.N_TNAME) { let k: syntax.nkind = n.kind;
// strings.rcut returns the whole input as its head on a miss. Require if (k == syntax.nkind.N_USE) { return; };
// a real qualifier separator so a bare package TNAME never counts as use.
if (k == syntax.nkind.N_TNAME) {
if (strings.contains(n.str, ".")) { if (strings.contains(n.str, ".")) {
let (head, leaf) = strings.rcut(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 return;
&& n.lhs.kind == syntax.nkind.N_IDENT) { };
findusepath(c.file, owner, source, n.lhs.str, true);
}; }; 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; let p: *syntax.node = n.attr;
for (p != nil) { markimportusesnode(c, p, owner, source); p = p.next; }; for (p != nil) {
markimportusesnode(c, n.lhs, owner, source); markimportusesnode(c, p, scope, owner, source, false);
markimportusesnode(c, n.rhs, owner, source); p = p.next;
markimportusesnode(c, n.cond, owner, source); };
markimportusesnode(c, n.body, owner, source); markimportusesnode(c, n.lhs, scope, owner, source, false);
markimportusesnode(c, n.els, owner, source);
p = n.list; p = n.list;
for (p != nil) { markimportusesnode(c, p, owner, source); p = p.next; }; 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, 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, scope, owner, source, false);
p = p.next;
};
}; };
fn markimportuses(c: *checker, file: *syntax.node) void = { fn markimportuses(c: *checker, file: *syntax.node) void = {
let d: *syntax.node = file.list; let d: *syntax.node = file.list;
for (d != nil) { for (d != nil) {
if (d.kind != syntax.nkind.N_USE) { 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; d = d.next;
}; };

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
// neg_let — local `let shadowmod: i32 = ...` shadows the imported // Legacy neg_let, now a positive declaration-point case. The initializer
// module from inside a fn body. Same rule fires for nested-scope // resolves shadowmod.say through the file-scope import; after the declaration
// let binds, not just params. // shadowmod denotes the closer i32 binding. Builds and runs 42.
package paramshadowmod; package main;
import shadowmod; import shadowmod;
export fn main() i32 = { fn main() i32 = {
let shadowmod: i32 = 0i32; let shadowmod: i32 = shadowmod.say();
return shadowmod; return shadowmod;
}; };

View File

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

View File

@@ -1,9 +1,8 @@
// neg_mlet — Hare tuple destructure `let (shadowmod, x) = pair();` // Legacy neg_mlet, now a positive tuple-binding case. The selector use before
// where the first binder shadows the imported module. N_MLET wires // the destructure belongs to the import; afterward shadowmod is the first
// check_module_shadow per-binder, so the rule fires at the first // tuple element. Builds and runs 42 + 1 + 2 = 45.
// name; the second binder `x` is innocuous.
package paramshadowmod; package main;
import shadowmod; import shadowmod;
@@ -11,7 +10,8 @@ fn pair() (i64, i64) = {
return 1i64, 2i64; return 1i64, 2i64;
}; };
export fn main() i32 = { fn main() i32 = {
let anchor: i32 = shadowmod.say();
let (shadowmod, x) = pair(); 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. // Legacy neg_param, now a positive lexical-scope case. The package selector
// Under the "value names and module names are disjoint" rule the // in main uses the file-scope import; the parameter named shadowmod is a
// build must fail with a clear diagnostic at the param decl site. // closer binding only inside probe. Builds and runs 42 + len("hi") = 44.
package paramshadowmod; package main;
import shadowmod; import shadowmod;
@@ -10,6 +10,6 @@ fn probe(shadowmod: str) i32 = {
return shadowmod.len; return shadowmod.len;
}; };
export fn main() i32 = { fn main() i32 = {
return probe("hi"); return shadowmod.say() + probe("hi");
}; };

View File

@@ -1,17 +1,14 @@
// pos_crossmod — cur_mod-filtering positive. Replaces the abolished // pos_crossmod — file-local import ownership control. This source imports
// pos_selfimp (self-import is hard-rejected post-#16). This module // shadowmod, while crossmod's separate source has a parameter of that name
// (paramshadowmod) imports `shadowmod`, and the bundle also pulls module // and no such import. The caller's qualifier cannot leak into the sibling
// `crossmod`, whose probe() has a param named `shadowmod`. crossmod does // package's lexical scope. Both stages build and run 2.
// NOT import shadowmod, so the shadow rule — filtered by the binding's
// own module — must NOT trip crossmod's param, even though a sibling
// module in the same bundle imports that leaf. Build + run; exit = 2.
package main; package main;
import shadowmod; import shadowmod;
import crossmod; import crossmod;
export fn main() i32 = { fn main() i32 = {
let _ = shadowmod.say(); let _ = shadowmod.say();
return crossmod.probe("hi"); return crossmod.probe("hi");
}; };

View File

@@ -1,6 +1,5 @@
// pos_rename — positive case. The fn param is renamed away from the // pos_rename — control with an unrelated parameter name. The file-scope
// imported module's bareword, so the rule doesn't fire and the body // import remains visible in probe. Both stages build and run 42.
// can call `shadowmod.say()` cleanly. Built + run; exit code = 42.
package main; package main;
@@ -11,6 +10,6 @@ fn probe(s: str) i32 = {
return shadowmod.say(); return shadowmod.say();
}; };
export fn main() i32 = { fn main() i32 = {
return probe("hi"); return probe("hi");
}; };

View File

@@ -1,6 +1,6 @@
// paramshadowmod/shadowmod — a tiny module the negative/positive // paramshadowmod/shadowmod — the sibling package used by the lexical
// fixtures import as `use shadowmod;`. Carries one fn so the leaf // import-shadowing fixtures. The exported function makes each pre-binding
// resolves through the module dot path when name resolution succeeds. // qualifier occurrence observable at runtime.
package shadowmod; package shadowmod;