From 6279d46652ebb250454932aa2b000d9aa39a868a Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 22 Aug 2026 17:53:31 +0900 Subject: [PATCH] fix: allow lexical import shadowing --- cmd/wcc/check.c | 220 ++++-- cmd/wcc/ww.h | 6 +- docs/build-system.md | 195 ++++++ docs/spec.md | 15 + docs/test-system-v2.md | 48 +- selfhost/cmd/wcc/cgenexpr.ww | 7 +- selfhost/cmd/wcc/check.ww | 352 +++++++--- test/package/package_test.ww | 639 ++++++++++++++++++ test/tool/rejects_test.ww | 94 ++- .../data/paramshadowmod/crossmod/crossmod.ww | 8 +- .../paramshadowmod/neg_forrange_single.ww | 15 +- .../data/paramshadowmod/neg_forrange_tuple.ww | 14 +- test/wcc/data/paramshadowmod/neg_let.ww | 12 +- test/wcc/data/paramshadowmod/neg_mcase.ww | 13 +- test/wcc/data/paramshadowmod/neg_mlet.ww | 14 +- test/wcc/data/paramshadowmod/neg_param.ww | 12 +- test/wcc/data/paramshadowmod/pos_crossmod.ww | 13 +- test/wcc/data/paramshadowmod/pos_rename.ww | 7 +- .../paramshadowmod/shadowmod/shadowmod.ww | 6 +- 19 files changed, 1420 insertions(+), 270 deletions(-) diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 03cce54b..6aeb0394 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -14,8 +14,6 @@ static void cstmt(Checker*, Node*); static Type *cexpr(Checker*, Node*); static Type *resolve_type(Checker*, Node*); -static void check_module_shadow(Checker*, const char *name, Pos, - const char *kindstr); static Type * err(Checker *c, Pos p, const char *fmt, ...) @@ -69,6 +67,7 @@ static int src_imports(Node *file, const char *modtag, int source, static Sym *lookup_visible(Checker *c, const char *name); static Sym *lookup_visible_type(Checker *c, const char *name); static Sym *lookup_bare_import_binding(Checker *c, const char *name); +static int local_shadows_import(Checker *c, const char *name); static int reject_bare_import_values(Checker *c, Node *n); static int reject_bare_import_types(Checker *c, Node *n); static void resolve_typedecl(Checker *c, Node *d); @@ -660,6 +659,9 @@ eval_def_const(Checker *c, Node *n, u64 *out, int depth) } case N_DOT: { if (n->lhs == NULL || n->lhs->kind != N_IDENT) return 0; + /* Preserve source-0 `.wwi` self-qualification while refusing to + * fold through a closer lexical value binding. */ + if (local_shadows_import(c, n->lhs->str)) return 0; /* M1 #22: map the qualifier alias to its dotted import path. */ const char *mk = use_path(c->file, c->cur_mod, c->cur_source, n->lhs->str); @@ -1618,6 +1620,10 @@ cexpr(Checker *c, Node *n) return n->type = base; } } + if (n->lhs && n->lhs->kind == N_IDENT + && local_shadows_import(c, n->lhs->str)) + return n->type = err(c, n->pos, + "selector '%s' undefined", n->str); return n->type = err(c, n->pos, "no enum member '%s' in %s", n->str ? n->str : "?", @@ -1650,6 +1656,10 @@ cexpr(Checker *c, Node *n) for (Tfield *f = u->fields; f; f = f->next) if (strcmp(f->name, n->str) == 0) return n->type = f->type; + if (n->lhs && n->lhs->kind == N_IDENT + && local_shadows_import(c, n->lhs->str)) + return n->type = err(c, n->pos, + "selector '%s' undefined", n->str); return n->type = err(c, n->pos, "no field '%s' in %s", n->str, type_name(c->a, base)); } @@ -1660,17 +1670,32 @@ cexpr(Checker *c, Node *n) if (*q < '0' || *q > '9') { idx = -1; break; } idx = idx * 10 + (*q - '0'); } + if (idx < 0 && n->lhs && n->lhs->kind == N_IDENT + && local_shadows_import(c, n->lhs->str)) + return n->type = err(c, n->pos, + "selector '%s' undefined", n->str); if (idx < 0) return n->type = err(c, n->pos, "tuple field must be numeric"); Tparam *tp = u->params; while (idx > 0 && tp) { tp = tp->next; idx--; } + if (tp == NULL && n->lhs && n->lhs->kind == N_IDENT + && local_shadows_import(c, n->lhs->str)) + return n->type = err(c, n->pos, + "selector '%s' undefined", n->str); if (tp == NULL) return n->type = err(c, n->pos, "tuple index out of range"); return n->type = tp->type; } - /* module-qualified: lhs is IDENT bound as SK_USE */ + /* The lexical-shadow slice makes a same-spelled local receiver + * reachable here. Diagnose that invalid local selector, but retain + * the existing lenient fallback for non-identifier extensions such + * as the codegen-supported untyped string-literal `.ptr` form. */ + if (n->lhs && n->lhs->kind == N_IDENT + && local_shadows_import(c, n->lhs->str)) + return n->type = err(c, n->pos, + "selector '%s' undefined", n->str); return n->type = ty_err; } case N_INDEX: { @@ -2308,8 +2333,6 @@ cexpr(Checker *c, Node *n) type_name(c->a, st)); } if (cs->str && cs->str[0]) { - check_module_shadow(c, cs->str, - cs->pos, "binding"); scope_define(c->cur, cs->str, SK_VAR, vt, cs); } } @@ -2672,7 +2695,6 @@ clet(Checker *c, Node *n) desugar_arrayslice(c, declared, n->rhs); n->type = t; if (n->str && n->str[0]) { - check_module_shadow(c, n->str, n->pos, "let"); Sym *s = scope_define(c->cur, n->str, SK_VAR, t, n); if (s == NULL) err(c, n->pos, "let '%s' redeclared in same scope", @@ -2753,8 +2775,6 @@ cstmt(Checker *c, Node *n) for (Node *nm = n->list; nm; nm = nm->next) { Type *ft = tp ? tp->type : ty_err; if (nm->str && nm->str[0]) { - check_module_shadow(c, nm->str, - nm->pos, "binding"); if (scope_define(c->cur, nm->str, SK_VAR, ft, nm) == NULL) err(c, nm->pos, @@ -2764,7 +2784,6 @@ cstmt(Checker *c, Node *n) if (tp) tp = tp->next; } } else if (n->str && n->str[0]) { - check_module_shadow(c, n->str, n->pos, "binding"); scope_define(c->cur, n->str, SK_VAR, elem ? elem : ty_err, n); } @@ -2809,6 +2828,9 @@ cstmt(Checker *c, Node *n) type_name(c->a, rt)); } Tparam *tp = u ? u->params : NULL; + /* Like a Go VarSpec/ShortVarDecl, every declared type belongs to + * the declaration header: resolve and check all of them before any + * name enters the enclosing lexical scope. */ for (Node *l = n->list; l; l = l->next) { Type *declared = l->lhs ? resolve_type(c, l->lhs) : NULL; Type *elem = tp ? tp->type : NULL; @@ -2819,16 +2841,18 @@ cstmt(Checker *c, Node *n) l->str, type_name(c->a, declared), type_name(c->a, elem)); l->type = t; + if (tp) tp = tp->next; + } + for (Node *l = n->list; l; l = l->next) { if (l->str && l->str[0]) { - check_module_shadow(c, l->str, l->pos, "let"); - Sym *s = scope_define(c->cur, l->str, SK_VAR, t, l); + Sym *s = scope_define(c->cur, l->str, SK_VAR, + l->type, l); if (s == NULL) err(c, l->pos, "let '%s' redeclared in same scope", l->str); else if (n->op == TK_CONST) s->is_const = 1; } - if (tp) tp = tp->next; } if (u && tp != NULL) err(c, n->pos, "tuple has extra elements"); @@ -3161,6 +3185,23 @@ lookup_bare_import_binding(Checker *c, const char *name) return NULL; } +/* A package-name object remains in the file scope when a closer lexical + * value binding wins lookup. This predicate is intentionally narrower than + * ordinary invalid-selector checking: before lexical import shadowing became + * legal, only this newly reachable path was hidden by the shadow prohibition. */ +static int +local_shadows_import(Checker *c, const char *name) +{ + if (c == NULL || name == NULL || name[0] == '\0') return 0; + if (!src_imports(c->file, c->cur_mod, c->cur_source, name)) return 0; + for (Scope *s = c->cur; s && s != c->top; s = s->parent) { + Sym *r = scope_lookup_local(s, name); + if (r != NULL) + return r->kind == SK_VAR || r->kind == SK_PARAM; + } + return 0; +} + /* Walk a type subtree only far enough to classify package-name objects. This * is deliberately not general type checking: the enum/array constant folders * need the package diagnostic before they reduce an unsupported outer shape @@ -3301,45 +3342,6 @@ lookup_visible_type(Checker *c, const char *name) return NULL; } -/* - * check_module_shadow — refuse value bindings that shadow an - * in-scope imported module bareword. "Value names and module names - * are disjoint": a fn param / let / mcase binding named `fmt` while - * the declaring source carries `use fmt;` would silently miscompile - * any `fmt.X` body lookup through the shadow's value bits (the - * cstage cexpr N_DOT path resolves the inner ident as the shadow - * and emits CALL through its bytes — task #19). - * - * Scope: - * - Fires only for nested-scope binds (c->cur != c->top). Same-leaf - * top-level decls (`use foo; fn foo(...)`) are intentional and - * handled by the SK_USE→SK_X promotion path with use_alias=1. - * - Filters by the declaring source's own use directives. lib/fmt's - * `fn fprintf(fmt: str, ...)` is fine because lib/fmt doesn't - * import itself. - * - Walks every scope (not just innermost) so a deeper shadow that - * happens to mask the SK_USE entry can't suppress the check. - */ -static void -check_module_shadow(Checker *c, const char *name, Pos pos, - const char *kindstr) -{ - if (name == NULL || name[0] == '\0') return; - if (c == NULL || c->cur == c->top) return; - int seen_use = 0; - for (Scope *s = c->cur; s; s = s->parent) { - Sym *r = scope_lookup_local(s, name); - if (r && (r->kind == SK_USE || r->use_alias)) { - seen_use = 1; - break; - } - } - if (!seen_use) return; - if (!src_imports(c->file, c->cur_mod, c->cur_source, name)) return; - err(c, pos, "%s '%s' shadows imported module '%s'", - kindstr, name, name); -} - /* Self-contained `.wwi` files can carry the same origin-owned type/const * fact through two direct dependencies (a diamond), or alongside a direct * import of that origin. In strict package mode those compiler-generated @@ -4148,23 +4150,116 @@ reject_nonfunction_main_decls(Checker *c, Node *file) } } -/* Import usage is a property of the file-local qualifier occurrence. Record - * qualified syntax before resolving declaration bodies so import diagnostics - * retain production Go's source order without making a failed bare lookup a - * use. WW already rejects lexical bindings that shadow an import qualifier. */ +/* Import diagnostics precede body diagnostics, so usage must be known before + * pass 1. The temporary scopes mirror the real declaration points without + * contaminating the compilation scope that pass 1 owns. */ static void mark_import_uses_node(Checker *c, Node *n, const char *owner, int source) { if (n == NULL) return; - if (n->kind == N_TNAME && n->str != NULL) { + switch (n->kind) { + case N_TNAME: { + if (n->str == NULL) return; const char *dot = strrchr(n->str, '.'); if (dot != NULL) { char *head = astrndup(c->a, n->str, (size_t)(dot - n->str)); - (void)find_use_path(c->file, owner, source, head, 1); + if (scope_lookup(c->cur, head) == NULL) + (void)find_use_path(c->file, owner, source, head, 1); } - } else if (n->kind == N_DOT && n->lhs != NULL - && n->lhs->kind == N_IDENT && n->lhs->str != NULL) { - (void)find_use_path(c->file, owner, source, n->lhs->str, 1); + return; + } + case N_FNDECL: { + for (Node *p = n->attr; p; p = p->next) + mark_import_uses_node(c, p, owner, source); + mark_import_uses_node(c, n->lhs, owner, source); + for (Node *p = n->list; p; p = p->next) + mark_import_uses_node(c, p->lhs, owner, source); + Scope *saved = c->cur; + c->cur = newscope(c->a, saved); + for (Node *p = n->list; p; p = p->next) + if (p->str && p->str[0]) + (void)scope_define(c->cur, p->str, SK_PARAM, NULL, p); + mark_import_uses_node(c, n->body, owner, source); + c->cur = saved; + return; + } + case N_BLOCK: { + Scope *saved = c->cur; + c->cur = newscope(c->a, saved); + for (Node *p = n->list; p; p = p->next) + mark_import_uses_node(c, p, owner, source); + c->cur = saved; + return; + } + case N_LET: + for (Node *p = n->attr; p; p = p->next) + mark_import_uses_node(c, p, owner, source); + mark_import_uses_node(c, n->lhs, owner, source); + mark_import_uses_node(c, n->rhs, owner, source); + if (n->str && n->str[0]) + (void)scope_define(c->cur, n->str, SK_VAR, NULL, n); + return; + case N_MLET: + for (Node *p = n->attr; p; p = p->next) + mark_import_uses_node(c, p, owner, source); + mark_import_uses_node(c, n->rhs, owner, source); + for (Node *p = n->list; p; p = p->next) + mark_import_uses_node(c, p->lhs, owner, source); + for (Node *p = n->list; p; p = p->next) + if (p->str && p->str[0]) + (void)scope_define(c->cur, p->str, SK_VAR, NULL, p); + return; + case N_FOR: { + Scope *saved = c->cur; + c->cur = newscope(c->a, saved); + mark_import_uses_node(c, n->lhs, owner, source); + mark_import_uses_node(c, n->cond, owner, source); + mark_import_uses_node(c, n->rhs, owner, source); + mark_import_uses_node(c, n->body, owner, source); + mark_import_uses_node(c, n->els, owner, source); + c->cur = saved; + return; + } + case N_FORRANGE: { + Scope *saved = c->cur; + c->cur = newscope(c->a, saved); + mark_import_uses_node(c, n->lhs, owner, source); + if (n->list != NULL) { + for (Node *p = n->list; p; p = p->next) + if (p->str && p->str[0]) + (void)scope_define(c->cur, p->str, + SK_VAR, NULL, p); + } else if (n->str && n->str[0]) { + (void)scope_define(c->cur, n->str, SK_VAR, NULL, n); + } + mark_import_uses_node(c, n->body, owner, source); + mark_import_uses_node(c, n->els, owner, source); + c->cur = saved; + return; + } + case N_MATCH: + mark_import_uses_node(c, n->lhs, owner, source); + for (Node *cs = n->list; cs; cs = cs->next) { + Scope *saved = c->cur; + c->cur = newscope(c->a, saved); + mark_import_uses_node(c, cs->lhs, owner, source); + for (Node *p = cs->list; p; p = p->next) + mark_import_uses_node(c, p, owner, source); + if (cs->str && cs->str[0]) + (void)scope_define(c->cur, cs->str, SK_VAR, NULL, cs); + mark_import_uses_node(c, cs->body, owner, source); + c->cur = saved; + } + return; + case N_DOT: + if (n->lhs != NULL && n->lhs->kind == N_IDENT + && n->lhs->str != NULL + && scope_lookup(c->cur, n->lhs->str) == NULL) + (void)find_use_path(c->file, owner, source, + n->lhs->str, 1); + break; + default: + break; } for (Node *p = n->attr; p; p = p->next) mark_import_uses_node(c, p, owner, source); @@ -4180,10 +4275,13 @@ mark_import_uses_node(Checker *c, Node *n, const char *owner, int source) static void mark_import_uses(Checker *c, Node *file) { + Scope *saved = c->cur; for (Node *d = file->list; d; d = d->next) { if (d->kind == N_USE) continue; + c->cur = newscope(c->a, NULL); mark_import_uses_node(c, d, decl_mod(file, d), d->sourceid); } + c->cur = saved; } static void @@ -4882,8 +4980,6 @@ check_file(Checker *c, Node *file) Type *fnt = d->type; for (Tparam *p = fnt->params; p; p = p->next) { if (p->name && p->name[0]) { - check_module_shadow(c, p->name, - d->pos, "param"); if (scope_define(c->cur, p->name, SK_PARAM, p->type, d) == NULL) err(c, d->pos, diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index 14efbf4f..aede6e54 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -606,10 +606,8 @@ struct Checker { * rather than colliding io.read. */ int cur_source; /* lexical source-file scope of the declaration * currently being checked. */ - Node *file; /* current N_FILE root; used by check_module_shadow - * to consult the declaring source file's own `use` - * directives when refusing param/let names that - * would shadow an imported module bareword. */ + Node *file; /* current N_FILE root; owns source-local import + * qualifier lookup and usage accounting. */ int loops; /* nesting count for break/continue */ int matcharms; /* nesting count for yield */ int errs; diff --git a/docs/build-system.md b/docs/build-system.md index dd7e01f3..cd58d49b 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -9760,6 +9760,201 @@ No format bump. Build workdir format remains `18`, test workdir format remains `19`, semantic storage format remains `3`, and no test-result cache is introduced. +### 11.49 Implemented lexical shadowing of import bindings + +An effective nonblank import qualifier is a file-local package-name object, not +a reserved spelling. An ordinary closer lexical binding may shadow it. Each +occurrence resolves to the nearest visible object: a selector before the local +declaration denotes the import and satisfies that import's use accounting; the +same spelling after a parameter, local, tuple-local, loop/range binder, or +match-arm binder denotes that closer binding. Leaving the nested scope restores +the import binding. A selector consumes an import only when its receiver +actually resolves to that import's package-name object. A selector whose +receiver is a local value neither consumes nor resurrects the same-spelled +import. This includes explicit aliases and qualifiers spelled like builtins. + +This is lexical binding recovery, not a new import form or an identity rule. +Blank imports and rejected effective-`init` imports still install no binding; +an unresolved target still fails during resolution before checker binding +semantics. WW's existing `for ... else` behavior has no Go counterpart and is +unchanged. + +#### Pinned Go evidence and fact classification + +The sole semantic authority is official Go 1.26.5 at commit +`c19862e5f8415b4f24b189d065ed739517c548ba`: + +- the language specification defines imports as package objects with file-block + scope, and defines parameter/body/local declaration points, nested scopes, + and inner-declaration shadowing ([`doc/go_spec.html`, lines + 2160–2174 and 2190–2233](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 + 223–335](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L223-L335), + [`check.go`, lines 73–94](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 143–180](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/signature.go#L143-L180) + and [`assignments.go`, lines 525–602](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 672–692](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/call.go#L672-L692), + [`check.go`, lines 496–523](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/check.go#L496-L523), + [`resolver.go`, lines 706–740](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 8–13](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/fixedbugs/bug129.go#L8-L13), + [`issues0.go`, lines 16–23](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/issues0.go#L16-L23), + [`bug107.go`, lines 8–15](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/fixedbugs/bug107.go#L8-L15)). + +Those specification, resolver, selector-use, declaration-point, and fixture +facts are **behavior directly implemented or asserted by pinned Go**. That a +WW occurrence before a local consumes the import, an occurrence after it sees +the local, a nested scope restores the import on exit, aliases and +builtin-spelled qualifiers follow the same rule, and a local selector does not +consume the import is **behavior derived from the pinned implementation**. +The behavior honestly applies to WW's existing file-local dotted bindings and +representable lexical scopes without introducing Go's modules, manifests, +quoted/grouped/dot imports, registry, cache, network resolution, or source +build expressions. + +#### Fresh four-axis audit and direct pre-fix measurements + +The fresh simultaneous audit classified multiple named source operands as a +different, applicable build/package slice; explicit `*_test.ww` build operands +as a different, applicable build/source-selection slice; shared top-level test +process state and fatal-abort topology as different, applicable test slices; and +Go-compatible regular-expression `-run` matching as a different, applicable +test slice. They remain open and unselected: each needs a wider true-owner +change. Grouped, quoted, and dot imports are inapplicable to WW's deliberately +narrow import grammar. No fresh pinned evidence reopened a completed section +through §11.48. + +Before this slice, all following results were **directly measured WW behavior**: + +- a directory build containing a real package selector followed by a legal + `let shadowmod` failed in both stages before publication; Cstage emitted + positioned 155-byte stderr (SHA-256 + `13cfac9494a1f6c0ee958d3b74ecfe8b0efe2a685f31a8a1771fda3fa0af1b02`), + while WWstage emitted unpositioned 76-byte stderr (SHA-256 + `298e4991ca9900ef32499762f8ae9abb8ace07c3e672dc66db1e911a8de62b75`). + Both had empty stdout, no output, and empty precreated workdirs; +- same-package and external test shadows failed before descriptor execution; + both emitted exactly `FAIL\n`, published/retained nothing, and left no work + entry. Cstage stderr was 277 bytes (SHA-256 + `ac850802e5911f53c3e64be2ab55e1f3442438eadab35a40f63ddac869337e4e`), + WWstage was 186 bytes (SHA-256 + `a1229bd19628e5dc909a0bcb848c30cc1dbdf9d370f21ed51e79489c06abcad0`); +- after a genuine selector, `let shadowmod: i32 = 1; return shadowmod.say()` + produced only the shadow prohibition in Cstage, but WWstage additionally + cascaded through `calling non-function` and `asserttyped: dot`; and +- an otherwise-unused import with only local `shadowmod.n` produced the shadow + prohibition rather than unused-import in both stages, proving the former + syntax-only use pass falsely consumed the import. Legal no-shadow controls + built, published byte-identical executables (SHA-256 + `7b710cf0973821cec430878d1f90de64485438510512561263c1a16b367c3c78`), + and exited 42. + +The direct probes also covered parameter, local, nested, explicit-alias, +selector-before/after, imported-dependency, same-package, external-test, and +honest test-only paths; both stages rejected every legal shadow. Thus the +pre-fix difference was legal-source rejection, false unused-import accounting, +and stage-divergent downstream recovery. + +#### Ownership and complete four-axis behavior + +The true owners are the Cstage semantic checker in `cmd/wcc/check.c`, its +self-hosted twin in `selfhost/cmd/wcc/check.ww`, and the latter's +`selfhost/cmd/wcc/cgenexpr.ww` local-versus-imported-enum fast path. They +remove the import-shadow prohibition; resolve use accounting through isolated +temporary lexical scopes rather than merely selector spelling; preserve +declaration timing; and gate dotted package/enum shortcuts on the visible +binding. The Cstage code generator already follows checker/local stamps and is +proved rather than redefined. Parsers, loader/source selection, drivers, +coordinator, canonical resolution, graph/action construction, assembler, +archiver, linker, runtime, publisher, and persistence records are not owners. + +- **Go-like build:** raw, directory, and imported programs with legal shadowing + now pass checking, build through unchanged actions, publish normally, and run + the local value/field/function-pointer behavior. A genuinely invalid local + selector fails during checking before code generation or downstream tools. +- **Go-like test:** production called by test, same-package, external-test, + honest test-only, filtered, retained, and directly retained products use the + same rule before execution. Discovery, filters, descriptors, process state, + fatal/skip behavior, timeout, retention, and cleanup are unchanged. +- **Go-like package:** import binding remains file scoped; ordinary local scopes + nest within it, and sibling source files remain independent. Declared names, + source roles, package/variant identities, exported declarations, + initialization, symbols, and selected membership do not change. +- **Go-like import:** the nearest visible object wins. Only a selector whose + receiver is the visible package-name object satisfies unused-import accounting; + local field selectors do not. Default/explicit aliases, blank imports, + effective-`init`, missing-target precedence, canonical dotted identity, + contextual local/vendor mapping, visibility, cycles, and direct graph edges + retain their existing semantics. + +#### Lifecycle, parity, proof, and formats + +Filename/platform/test-role eligibility, byte-sorted source selection, package +clauses, source IDs, loading, and resolution remain earlier owners. Shadowing +does not add/remove an already-resolved direct import edge, rekey actions, or +change action order, variants, initialization dispatch, linker symbols, +`.wwi` ownership, artifacts, publication, or persistence identity. Physical +directories remain loader/runtime/presentation metadata, never canonical +package, import, graph, action, artifact, symbol, publication, or storage +identity. + +The lexical-aware prepass keeps source-position diagnostic ordering. It removes +every `shadows imported module` diagnostic while preserving missing target, +invalid effective-`init`, blank/no-binding, collision/redeclaration, and +selector-only package-name diagnostics. An otherwise-unused import is reported +before a later invalid local selector according to source position; local +invalid dots reject with the same positioned recovery stamp in both stages and +cannot cascade into C/WW code generation, assembler, or linker diagnostics. + +Valid shadowing reaches ordinary compiler, assembler, archiver, linker, and +runtime paths. The representative package matrix proves byte-identical +Cstage/WWstage unit, interface, assembly, object, archive, initializer, and +published executable artifacts; every test source role proves public binary +parity, and the retained case proves retained-binary parity. A cold invalid +action creates no public/retained/interface/archive/object/executable artifact +and no `.new`, `.install`, `.wwtxn.*`, adjacent `.sepwork`, capture, result, or +request scratch. A warm edit that +makes an import unused while also introducing an invalid local selector +preserves the prior committed generation and public product byte-for-byte; +exact restoration uses ordinary invalidation/reuse and cannot leave poisoned +state. Existing producer/runtime failure, late publication failure, rollback, +concurrency, interruption, process-group ownership, and cleanup remain their +existing owners because this checker slice adds no process, lock, transaction, +or shared runtime state. + +The WW-native `lexical_import_bindings_shadow_normally` package observer and +the tool-suite `paramshadow_lexical_bindings` fixture matrix jointly prove +selector before local, self-shadowing initializer, parameter/let/tuple-let/ +ordinary-for/range/match-arm declaration timing, all annotated tuple types +before any tuple binder, nested restoration, aliases and builtin-spelled +qualifiers, local struct/pseudo/function-pointer fields, imported-enum name +collisions, local dotted type/value rejection, unused accounting, and +sibling-file isolation. The package observer also covers raw and directory +builds, imported dependencies, every applicable test role, filtered and +retained/direct-retained execution, normalized diagnostic parity, valid +artifact/runtime parity, cold cleanup, warm preservation/restoration, and +residue absence. Blank/effective-`init`/missing-target behavior is unchanged +and remains proved by the immediately preceding focused observers. + +Go has no range-loop `else` clause. That WW-only extension is therefore +inapplicable to this pinned-Go slice and was not redefined: the pre-existing +Cstage behavior keeps a range binder visible in `else`, whereas WWstage restores +the outer scope before `else`. Each stage's import-use prepass deliberately +matches its own live checker there; the cross-stage extension difference remains +open and is not presented as lexical-shadow parity proved by this slice. + +No serialized format changes. This alters lexical resolution of source bytes +already present in the existing action vouchers and adds no action-key, graph, +artifact-layout, harness-protocol, cache, database, or publication field. +Build workdir format remains `18`, test workdir format remains `19`, semantic +storage format remains `3`, and no test-result cache is introduced. + ## 12. Candidate architectures and hard-gate decision Five candidates were developed as coherent systems, not as feature bins. diff --git a/docs/spec.md b/docs/spec.md index 8809c721..cdd5de7b 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -336,6 +336,21 @@ ImportPath = ident { "." ident } . lookup rather than package-name diagnostics. Builtin spelling does not alter the object: an import bound as `len`, `size`, `align`, or another builtin name remains a selector-only package-name object. + An ordinary lexical binding may shadow that file-local package-name object. + Lookup at each occurrence chooses the nearest enclosing binding: a selector + before a later local declaration can use and count the import, while the + same spelling after that declaration denotes the local. Parameters bind for + their whole function body; local `let` bindings begin after their declared + type and initializer have been checked; tuple bindings begin after their + right-hand side and declared types; and loop, range, and match-arm bindings + begin only after their respective initializer/iterable or pattern/type has + been checked. Nested blocks and loop scopes restore the imported package-name + object on exit. These declaration-point rules apply equally to default, + explicit, and builtin-spelled qualifiers. A selector counts as an import use + only when its receiver resolves to that package-name object; a selector on a + closer local, including a field or pseudo-field selector, does not. + Go has no range-loop `else` clause; that WW-only extension is not assigned a + Go-derived scope rule by this paragraph. Neither form exposes an imported declaration as a bare `Name`; ordinary unqualified lookup remains limited to lexical, builtin, and same-package declarations. A blank import creates no package-name object, an effective diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 31c36387..91051c6d 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -598,7 +598,53 @@ precedence. Type-shaped arguments parsed for `size` and `align` must retain type checking when either spelling is instead an import binding; neither stage may fall into an internal expression-kind diagnostic. -The same observer must exercise ordinary root and imported builds plus +The adjacent focused dual-stage +`lexical_import_bindings_shadow_normally` package observer, together with the +tool-suite `paramshadow_lexical_bindings` fixture matrix, is the acceptance +owner for ordinary lexical shadowing of an effective file-local package-name +object. Together they require a selector before a later local to resolve to and +consume the import, while the later spelling resolves to the closer parameter, +`let`, tuple-let, ordinary-`for`/range, or match-arm binding. Nested blocks and +loop exit restore the import; a parameter wins throughout its body; +self-shadowing initializers retain the outer import until the local declaration +point; and every annotated tuple type is checked before any tuple binder enters +scope. Default, explicit, and builtin-spelled qualifiers follow the same rule. +A local value, struct or pseudo-field, function-pointer field, or local +enum/member collision must use the local receiver rather than recover the +same-spelled import. Conversely, a shadowed dotted type or invalid scalar +selector must reject as a local use without a checker/backend cascade. A +selector-shaped local-only occurrence does not consume the import, so its +unused diagnostic retains source-position precedence. Sibling source files +retain independent import bindings and a separately selected legal import use +remains effective. Blank, effective-`init`, and missing-target controls remain +owned by their immediately adjacent focused observers and are explicit +non-effects of this acceptance pair. + +This observer exercises raw-source and directory builds, imported dependencies, +production reached by tests, same-package, external-test, and honest +test-only sources, filtered execution, retained test execution, and later +direct retained-binary execution. Cstage and WWstage must agree on status, +stdout, normalized full stderr (including positioned diagnostic order) for +build and test rejection, representative comparable semantic artifacts, every +test role's public binary bytes, retained binary bytes, and valid runtime +results. Its lifecycle matrix requires cold invalid requests to leave no public +or retained product and an empty workdir; a warm valid-to-invalid unused-import +transition whose source also contains an invalid local selector to preserve the +prior public product and complete committed generation byte-for-byte; and +restoration to recover ordinary exact-byte reuse with no `.new`, `.install`, +`.wwtxn.*`, adjacent `.sepwork`, capture/result, or request-scratch residue. The +rule changes neither source eligibility, +package/import identity, direct graph edges, action/variant identity, init +ordering, test process/state/fatal-abort topology, runtime/process ownership, +publication transaction shape, persistence format, concurrency isolation, +interruption handling, nor descendant cleanup; those boundaries retain their +existing owners. Go has no range-loop `else` clause; that WW-only extension's +pre-existing Cstage/WWstage scope difference is explicitly outside this +pinned-Go observer, and each import-use prepass continues to mirror its own +stage's live checker there. + +The `bare_import_bindings_require_selectors` observer must exercise ordinary +root and imported builds plus production, same-package test, external-test, and honest test-only source roles. Cstage and WWstage must agree on status and normalized stdout/stderr, including diagnostic order and source position, and valid controls must produce diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 9e6900ce..d2b5b416 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -3642,13 +3642,16 @@ fn cgdot(c: *cgen, n: *syntax.node) void = { // → inline the pre-computed constant. `pkg.Enum.MEMBER` keeps // `pkg` so enumlookupmod can prefer the explicit module on a // leaf collision; bare `Enum.MEMBER` falls back to c.curmod via - // enumlookup's same-module-first walk. + // enumlookup's same-module-first walk. A live local with the same + // spelling wins before this name-only registry fallback; valid local + // enum selectors were already folded by the checker. if (lhs != nil) { let etname: str; let etmod: str; etname.ptr = nil; etname.len = 0; etmod.ptr = nil; etmod.len = 0; - if (lhs.kind == syntax.nkind.N_IDENT) { + if (lhs.kind == syntax.nkind.N_IDENT + && localfindnode(c, lhs.str) == nil) { etname = lhs.str; }; if (lhs.kind == syntax.nkind.N_DOT) { diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index f7410d2c..5a2da2be 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -37,9 +37,8 @@ type checker = struct { // preference in bare-leaf lookups. cursource: i32, // lexical source-file scope of the current decl; // selects only that file's import bindings. - file: *syntax.node, // N_FILE root; used by checkmoduleshadow - // to consult the declaring source's own - // `use` directives. + file: *syntax.node, // N_FILE root; owns source-local `use` + // bindings and their usage state. allococtx: *syntax.node, // #3/B': the one empty alloc([], n) call node // with let-declared slice context this walk; // any other empty alloc has no element hint @@ -224,6 +223,24 @@ fn usepathfor(file: *syntax.node, modtag: str, source: i32, alias: str) str = { // modkeyfor — the module key for a directly imported alias. Empty means // the referencing package did not itself declare that import. fn modkeyfor(c: *checker, alias: str) str = { + let found: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, alias); + if (found != nil && found.scope != c.top + && (found.skind == syntax.skind.SK_VAR + || found.skind == syntax.skind.SK_PARAM)) { + let empty: str; + return empty; + }; + // Imported interface declarations may qualify their own package from the + // source-0 section without a source import node. Preserve that bridge, but + // only after a closer lexical binding has been excluded above. + if (c.cursource == 0 && c.curmod.len != 0) { + let (prefix, suffix) = strings.rcut(c.curmod, "."); + let leaf: str = suffix; + if (leaf.len == 0) { leaf = c.curmod; }; + if (syntax.streq(alias, leaf)) { + return usepathfor(c.file, c.curmod, c.cursource, alias); + }; + }; return usepathfor(c.file, c.curmod, c.cursource, alias); }; @@ -264,6 +281,23 @@ fn srcimports(file: *syntax.node, modtag: str, source: i32, name: str) bool = { return false; }; +// A package-name object remains in the file scope when a closer lexical value +// wins lookup. Keep this predicate restricted to that newly reachable path so +// unrelated WW selector extensions retain their established diagnostics. +fn localshadowsimport(c: *checker, name: str) bool = { + if (!srcimports(c.file, c.curmod, c.cursource, name)) { return false; }; + let s: *syntax.scope = c.cur; + for (s != nil && s != c.top) { + let found: *syntax.sym = syntax.scopelookuplocal(s, name); + if (found != nil) { + return found.skind == syntax.skind.SK_VAR + || found.skind == syntax.skind.SK_PARAM; + }; + s = s.parent; + }; + return false; +}; + fn lookupvisible(c: *checker, name: str) *syntax.sym = { let found: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, name); let builtin: *syntax.sym = nil; @@ -398,39 +432,6 @@ fn synthesizedtestrun(c: *checker, e: *syntax.node) bool = { return c.synthtestrun != nil && c.synthtestrun == e; }; -// checkmoduleshadow — enforce "value names and module names are -// disjoint" at nested-scope binds. Mirrors cstage check_module_shadow -// (cmd/wcc/check.c). Fires for fn params / lets / forrange iters / -// mcase bindings whose name matches an in-scope `use foo;` import -// declared in the same source file. Top-level decls are exempt -// (their same-leaf-as-module pattern is the intentional coexistence -// shape — `use fnmatch; fn fnmatch(...)` etc.). -fn checkmoduleshadow(c: *checker, name: str, kindstr: str) void = { - if (name.len == 0) { return; }; - if (c.cur == c.top) { return; }; - let seen: bool = false; - let s: *syntax.scope = c.cur; - for (s != nil) { - let r: *syntax.sym = syntax.scopelookuplocal(s, name); - if (r != nil) { - if (r.skind == syntax.skind.SK_USE) { - seen = true; - s = nil; - }; - }; - if (s != nil) { s = s.parent; }; - }; - if (!seen) { return; }; - if (!srcimports(c.file, c.curmod, c.cursource, name)) { return; }; - cerr(kindstr); - cerr(" '"); - cerr(name); - cerr("' shadows imported module '"); - cerr(name); - cerr("'\n"); - c.errs += 1; -}; - // installdecl — install the top-level decl's name into the top scope. // We don't compute its type yet (that's the resolve pass) — just bind // the name so forward references resolve. @@ -593,9 +594,11 @@ fn installtop(c: *checker, d: *syntax.node, nm: str, mod: str, k: syntax.skind, // `let (a,b) = f()`, `for (let (a,b) .. s)`, and the ww-extension // multi-assign `a, _ = f()`. // -// `define` (the binding contexts: let-unpack + for-range) installs each -// named binder as a fresh SK_VAR and back-fills its declared type onto -// .lhs so use sites resolve through the N_IDENT exprtype path. Multi- +// `define` (the binding contexts: let-unpack + for-range) back-fills and +// resolves every declared type before installing any named binder as a fresh +// SK_VAR. This gives the whole declaration header the outer lexical scope, +// matching the Cstage twin and Go's VarSpec/ShortVarDecl declaration point. +// Multi- // assign targets are pre-declared lvalues, so it passes false: .lhs is // left untouched (an N_INDEX/N_DOT target carries a live operand there) // and only the type_ stamp fires on the still-untyped slots. @@ -614,23 +617,6 @@ fn stamptuplebinds(c: *checker, binds: *syntax.node, elems: *syntax.node, if (pt != nil) { et = pt.lhs; }; if (define) { if (b.lhs == nil) { b.lhs = et; }; - let bnm: str = b.str; - if (bnm.len > 0) { - checkmoduleshadow(c, bnm, what); - // first registration wins; a nil return is a - // same-scope duplicate — `let (a, a) = ..` / - // `for (let (a, a) .. xs)`. Mirrors cstage - // scope_define == NULL → "redeclared". - let ds: *syntax.sym = syntax.scopedefine(c.cur, bnm, syntax.skind.SK_VAR, nil, b); - if (ds == nil) { - cerr("error: "); - cerr(what); - cerr(" '"); - cerr(bnm); - cerr("' redeclared in same scope\n"); - c.errs += 1; - }; - }; }; if (b.type_ == nil) { let src: *syntax.node = b.lhs; @@ -643,6 +629,27 @@ fn stamptuplebinds(c: *checker, binds: *syntax.node, elems: *syntax.node, b = b.next; if (pt != nil) { pt = pt.next; }; }; + if (define) { + b = binds; + for (b != nil) { + let bnm: str = b.str; + if (bnm.len > 0) { + // First registration wins; a nil return is a same-scope + // duplicate within this declaration. + let ds: *syntax.sym = syntax.scopedefine(c.cur, bnm, + syntax.skind.SK_VAR, nil, b); + if (ds == nil) { + cerr("error: "); + cerr(what); + cerr(" '"); + cerr(bnm); + cerr("' redeclared in same scope\n"); + c.errs += 1; + }; + }; + b = b.next; + }; + }; }; // resolvewalk — recursive AST walk that, for every nkind.N_IDENT and @@ -818,7 +825,6 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { } else { let bnm: str = n.str; if (bnm.len > 0) { - checkmoduleshadow(c, bnm, "binding"); // C4 (task #7): bind the ELEMENT type so field // reads off a by-value aggregate binding // (`for (let t .. threads) { t.pc }`) resolve — @@ -876,11 +882,13 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { }; // `for (init; cond; post) body` / `for (cond) body` — the body is the - // break/continue target. Walk init/cond/post outside the loop count - // (they hold no statements), bump only around the body, and keep the - // `else` outside (it targets an enclosing loop, like N_FORRANGE above). + // break/continue target. Its initializer scope covers cond/post/body/else; + // only the body is inside the loop count, so `else` still targets an + // enclosing loop. // Mirrors cstage cmd/wcc/check.c:2529 (N_FOR, c->loops++). if (k == syntax.nkind.N_FOR) { + let forouter: *syntax.scope = c.cur; + c.cur = syntax.newscope(forouter); if (n.lhs != nil) { resolvewalk(c, n.lhs); }; if (n.cond != nil) { resolvewalk(c, n.cond); }; if (n.rhs != nil) { resolvewalk(c, n.rhs); }; @@ -888,6 +896,7 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { if (n.body != nil) { resolvewalk(c, n.body); }; c.loops -= 1; if (n.els != nil) { resolvewalk(c, n.els); }; + c.cur = forouter; return; }; @@ -930,7 +939,6 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { c.cur = syntax.newscope(outer); let nm: str = n.str; if (nm.len > 0) { - checkmoduleshadow(c, nm, "binding"); syntax.scopedefine(c.cur, nm, syntax.skind.SK_VAR, nil, n); }; if (n.body != nil) { @@ -997,8 +1005,6 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { // the un-annotated binder stays untyped and asserttyped aborts. // Mirrors cstage check.c:2017 (cexpr(rhs), unconditional). if (n.rhs != nil) { - // `rt` would shadow the imported lib/rt module - // (checkmoduleshadow errors); `rty` avoids it. // #99 alias transparency: a NAMED tuple alias rhs // (`type pair = (i64,i64)`) destructures like its // base — resolvealias mirrors cstage's @@ -1158,7 +1164,6 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { if (k == syntax.nkind.N_LET) { let nm: str = n.str; if (nm.len > 0) { - checkmoduleshadow(c, nm, "let"); let s: *syntax.sym = syntax.scopedefine(c.cur, nm, syntax.skind.SK_VAR, nil, n); // catB-22: flag a `const` binding so checkassign can // reject a later reassignment. The parser stamps @@ -4868,8 +4873,7 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { }; }; }; // A.6.1.5b stamp cases — mirror cstage check.c:833-866. Pure - // type-AST stamps; never rewrite e.kind. Lenient on misses - // (cstage errors); falls through to nil under scruttype L656. + // type-AST stamps; never rewrite e.kind. // // Pseudo-fields .len/.cap/.ptr on slice/str/array. Cstage // L833-842. `str` lives as N_TNAME("str") in wwstage — no @@ -4938,6 +4942,23 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { }; }; }; }; + // A same-spelled lexical receiver that fails every valid local + // selector shape is the newly reachable shadowing error path. Stamp + // recovery so asserttyped and cgen cannot replace the causal, + // positioned diagnostic. Missing struct/tuple/enum members use this + // same shadow-specific spelling in both compiler stages. + if (lhsn != nil && lhsn.kind == syntax.nkind.N_IDENT + && localshadowsimport(c, lhsn.str)) { + // Cstage's N_DOT position is the receiver start; use the + // identifier position rather than WW parser's dot token. + cerr(lhsn.file); cerr(":"); + cerr(strconv.i32tos(lhsn.line, strconv.base.DEC)); cerr(":"); + cerr(strconv.i32tos(lhsn.col, strconv.base.DEC)); + cerr(": error: selector '"); cerr(e.str); + cerr("' undefined\n"); + c.errs += 1; + e.type_ = c.tc.tyerr: *void; + }; }; return nil; }; @@ -7631,7 +7652,6 @@ fn installparams(c: *checker, params: *syntax.node) void = { normalizevariadicparam(p); let nm: str = p.str; if (nm.len > 0) { - checkmoduleshadow(c, nm, "param"); syntax.scopedefine(c.cur, nm, syntax.skind.SK_PARAM, nil, p); }; }; @@ -7846,39 +7866,195 @@ fn topdeclkind(d: *syntax.node) bool = { || d.kind == syntax.nkind.N_LET); }; -// Record only qualified syntax in each owning source before name resolution. -// This preserves source-position import diagnostics and never lets a failed -// bare lookup consume an ordinary import. -fn markimportusesnode(c: *checker, n: *syntax.node, owner: str, - source: i32) void = { +// Mark only a qualifier whose spelling still denotes the source-local package +// binding. The scratch scope contains lexical binders only; package and import +// symbols remain owned by the real checker scope. +fn markimportqualifier(c: *checker, scope: *syntax.scope, owner: str, + source: i32, alias: str) void = { + if (alias.len == 0) { return; }; + if (syntax.scopelookup(scope, alias) != nil) { return; }; + if (!srcimports(c.file, owner, source, alias)) { return; }; + let marked: str = usepathfor(c.file, owner, source, alias); +}; + +fn markimportusesnode(c: *checker, n: *syntax.node, scope: *syntax.scope, + owner: str, source: i32, top: bool) void = { if (n == nil) { return; }; - if (n.kind == syntax.nkind.N_TNAME) { - // strings.rcut returns the whole input as its head on a miss. Require - // a real qualifier separator so a bare package TNAME never counts as use. + let k: syntax.nkind = n.kind; + if (k == syntax.nkind.N_USE) { return; }; + + if (k == syntax.nkind.N_TNAME) { if (strings.contains(n.str, ".")) { let (head, leaf) = strings.rcut(n.str, "."); - findusepath(c.file, owner, source, head, true); + markimportqualifier(c, scope, owner, source, head); }; - } else { if (n.kind == syntax.nkind.N_DOT && n.lhs != nil - && n.lhs.kind == syntax.nkind.N_IDENT) { - findusepath(c.file, owner, source, n.lhs.str, true); - }; }; + return; + }; + + if (k == syntax.nkind.N_DOT) { + if (n.lhs != nil && n.lhs.kind == syntax.nkind.N_IDENT) { + markimportqualifier(c, scope, owner, source, n.lhs.str); + } else { + markimportusesnode(c, n.lhs, scope, owner, source, false); + }; + return; + }; + + if (k == syntax.nkind.N_FNDECL) { + let p: *syntax.node = n.attr; + for (p != nil) { + markimportusesnode(c, p, scope, owner, source, false); + p = p.next; + }; + markimportusesnode(c, n.lhs, scope, owner, source, false); + p = n.list; + for (p != nil) { + let a: *syntax.node = p.attr; + for (a != nil) { + markimportusesnode(c, a, scope, owner, source, false); + a = a.next; + }; + markimportusesnode(c, p.lhs, scope, owner, source, false); + markimportusesnode(c, p.rhs, scope, owner, source, false); + p = p.next; + }; + let fnscope: *syntax.scope = syntax.newscope(scope); + p = n.list; + for (p != nil) { + if (p.kind == syntax.nkind.N_PARAM && p.str.len > 0) { + syntax.scopedefine(fnscope, p.str, + syntax.skind.SK_PARAM, nil, p); + }; + p = p.next; + }; + markimportusesnode(c, n.body, fnscope, owner, source, false); + return; + }; + + if (k == syntax.nkind.N_BLOCK) { + let inner: *syntax.scope = syntax.newscope(scope); + let p: *syntax.node = n.list; + for (p != nil) { + markimportusesnode(c, p, inner, owner, source, false); + p = p.next; + }; + return; + }; + + if (k == syntax.nkind.N_LET) { + let p: *syntax.node = n.attr; + for (p != nil) { + markimportusesnode(c, p, scope, owner, source, false); + p = p.next; + }; + markimportusesnode(c, n.lhs, scope, owner, source, false); + markimportusesnode(c, n.rhs, scope, owner, source, false); + if (!top && n.str.len > 0) { + syntax.scopedefine(scope, n.str, syntax.skind.SK_VAR, nil, n); + }; + return; + }; + + if (k == syntax.nkind.N_MLET) { + let a: *syntax.node = n.attr; + for (a != nil) { + markimportusesnode(c, a, scope, owner, source, false); + a = a.next; + }; + markimportusesnode(c, n.rhs, scope, owner, source, false); + let p: *syntax.node = n.list; + for (p != nil) { + a = p.attr; + for (a != nil) { + markimportusesnode(c, a, scope, owner, source, false); + a = a.next; + }; + markimportusesnode(c, p.lhs, scope, owner, source, false); + p = p.next; + }; + p = n.list; + for (p != nil) { + if (p.str.len > 0) { + syntax.scopedefine(scope, p.str, syntax.skind.SK_VAR, nil, p); + }; + p = p.next; + }; + return; + }; + + if (k == syntax.nkind.N_FOR) { + let inner: *syntax.scope = syntax.newscope(scope); + markimportusesnode(c, n.lhs, inner, owner, source, false); + markimportusesnode(c, n.cond, inner, owner, source, false); + markimportusesnode(c, n.rhs, inner, owner, source, false); + markimportusesnode(c, n.body, inner, owner, source, false); + markimportusesnode(c, n.els, inner, owner, source, false); + return; + }; + + if (k == syntax.nkind.N_FORRANGE) { + markimportusesnode(c, n.lhs, scope, owner, source, false); + let inner: *syntax.scope = syntax.newscope(scope); + let p: *syntax.node = n.list; + for (p != nil) { + let a: *syntax.node = p.attr; + for (a != nil) { + markimportusesnode(c, a, scope, owner, source, false); + a = a.next; + }; + markimportusesnode(c, p.lhs, scope, owner, source, false); + if (p.str.len > 0) { + syntax.scopedefine(inner, p.str, syntax.skind.SK_VAR, nil, p); + }; + p = p.next; + }; + if (n.list == nil && n.str.len > 0) { + syntax.scopedefine(inner, n.str, syntax.skind.SK_VAR, nil, n); + }; + markimportusesnode(c, n.body, inner, owner, source, false); + markimportusesnode(c, n.els, scope, owner, source, false); + return; + }; + + if (k == syntax.nkind.N_MCASE) { + markimportusesnode(c, n.lhs, scope, owner, source, false); + let p: *syntax.node = n.list; + for (p != nil) { + markimportusesnode(c, p, scope, owner, source, false); + p = p.next; + }; + let inner: *syntax.scope = syntax.newscope(scope); + if (n.str.len > 0) { + syntax.scopedefine(inner, n.str, syntax.skind.SK_VAR, nil, n); + }; + markimportusesnode(c, n.body, inner, owner, source, false); + return; + }; + let p: *syntax.node = n.attr; - for (p != nil) { markimportusesnode(c, p, owner, source); p = p.next; }; - markimportusesnode(c, n.lhs, owner, source); - markimportusesnode(c, n.rhs, owner, source); - markimportusesnode(c, n.cond, owner, source); - markimportusesnode(c, n.body, owner, source); - markimportusesnode(c, n.els, owner, source); + for (p != nil) { + markimportusesnode(c, p, scope, owner, source, false); + p = p.next; + }; + markimportusesnode(c, n.lhs, scope, owner, source, false); + markimportusesnode(c, n.rhs, scope, owner, source, false); + markimportusesnode(c, n.cond, scope, owner, source, false); + markimportusesnode(c, n.body, scope, owner, source, false); + markimportusesnode(c, n.els, scope, owner, source, false); p = n.list; - for (p != nil) { markimportusesnode(c, p, owner, source); p = p.next; }; + for (p != nil) { + markimportusesnode(c, p, scope, owner, source, false); + p = p.next; + }; }; fn markimportuses(c: *checker, file: *syntax.node) void = { let d: *syntax.node = file.list; for (d != nil) { if (d.kind != syntax.nkind.N_USE) { - markimportusesnode(c, d, declmod(file, d), d.sourceid); + let scope: *syntax.scope = syntax.newscope(nil); + markimportusesnode(c, d, scope, declmod(file, d), + d.sourceid, true); }; d = d.next; }; diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 2c8d40ad..7c43d7e3 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -19743,3 +19743,642 @@ fn runtimepath(relative: str) str = { && !directoryhasfragment(root, ".install")); clean(root); }; + +// A file-local import package-name object participates in ordinary lexical +// lookup: a closer local wins from its declaration point through its scope, +// and the package qualifier is visible again afterward. Exercise that rule +// through raw and package builds, imported packages, every test source role, +// filtered retained execution, and transactional rejection/restoration. +@test fn lexical_import_bindings_shadow_normally() void = { + let root: str = fresh(); + let source: str = strings.concat(root, "/source"); + let wire: str = strings.concat(source, "/dep/wire"); + let aliasdep: str = strings.concat(source, "/dep/aliasdep"); + let builtindep: str = strings.concat(source, "/dep/builtindep"); + let palette: str = strings.concat(source, "/dep/palette"); + let shadowlib: str = strings.concat(source, "/dep/shadowlib"); + let matrix: str = strings.concat(source, "/matrix"); + let client: str = strings.concat(source, "/client"); + let sibling: str = strings.concat(source, "/sibling"); + let invalid: str = strings.concat(source, "/invalid"); + let invalidscalar: str = strings.concat(source, "/invalidscalar"); + let invalidtype: str = strings.concat(source, "/invalidtype"); + let warm: str = strings.concat(source, "/warm"); + let roleprod: str = strings.concat(source, "/roleprod"); + let rolesame: str = strings.concat(source, "/rolesame"); + let roleexternal: str = strings.concat(source, "/roleexternal"); + let roleonly: str = strings.concat(source, "/roleonly"); + let retainedcase: str = strings.concat(source, "/retainedcase"); + let dirs: []str = [wire, aliasdep, builtindep, palette, shadowlib, + matrix, client, sibling, invalid, invalidscalar, invalidtype, warm, + roleprod, rolesame, roleexternal, roleonly, retainedcase]; + let di: i32 = 0; + for (di < dirs.len) { mkdirall(dirs[di]); di += 1; }; + + writefile(strings.concat(wire, "/wire.ww"), strings.concat( + "package wire;\n", + "export type record = struct { n: i32 };\n", + "export type count = i32;\n", + "export fn value() i32 = { return 11; };\n")); + writefile(strings.concat(aliasdep, "/aliasdep.ww"), + "package aliasdep;\nexport fn value() i32 = { return 13; };\n"); + writefile(strings.concat(builtindep, "/builtindep.ww"), + "package builtindep;\nexport fn value() i32 = { return 17; };\n"); + writefile(strings.concat(palette, "/palette.ww"), strings.concat( + "package palette;\n", + "export type shade = enum i32 { RED = 100 };\n", + "export fn value() i32 = { return shade.RED: i32; };\n")); + + // The initializer still sees the import, the declared local wins + // afterward, a nested binding is restored on exit, aliases behave like + // default names, and local dot/call fields beat package and enum shortcuts. + writefile(strings.concat(matrix, "/main.ww"), strings.concat( + "package main;\n", + "import dep.wire;\n", + "import stable dep.aliasdep;\n", + "import len dep.builtindep;\n", + "import shade dep.palette;\n", + "type box = struct { n: i32, RED: i32, call: *fn() i32 };\n", + "fn seven() i32 = { return 7; };\n", + "fn typedpair() (i32, wire.count) = { return 5, 6; };\n", + "fn localafter() bool = { let before: i32 = wire.value(); ", + "let wire: i32 = wire.value() + 1; ", + "return before == 11 && wire == 12; };\n", + "fn nestedrestore() bool = { let before: i32 = wire.value(); ", + "if (true) { let wire: i32 = 3; ", + "if (wire != 3) { return false; }; }; ", + "return before == 11 && wire.value() == 11; };\n", + "fn aliases() bool = { let a: i32 = stable.value(); ", + "let stable: i32 = stable.value() + 1; ", + "let b: i32 = len.value(); let len: i32 = len.value() + 1; ", + "return a == 13 && stable == 14 && b == 17 && len == 18; };\n", + "fn localfields() bool = { let wire: box = box { n = 5, ", + "RED = 0, call = &seven }; let imported: i32 = shade.value(); ", + "let shade: box = box { n = 0, RED = 19, call = &seven }; ", + "return wire.n == 5 && (*wire.call)() == 7 ", + "&& imported == 100 && shade.RED == 19; };\n", + "fn tupledecl() bool = { ", + "let (wire: i32, second: wire.count) = typedpair(); ", + "return wire == 5 && (second: i32) == 6; };\n", + "fn ordinaryfor() bool = { let total: i32 = wire.value(); ", + "for (let wire: i32 = 1; wire < 3; wire += 1) ", + "{ total += wire; }; ", + "return total == 14 && wire.value() == 11; };\n", + "fn main() i32 = { if (!localafter()) { return 1; }; ", + "if (!nestedrestore()) { return 2; }; ", + "if (!aliases()) { return 3; }; ", + "if (!localfields()) { return 4; }; ", + "if (!tupledecl()) { return 5; }; ", + "if (!ordinaryfor()) { return 6; }; return 42; };\n")); + + let stages: []str = ["ww", "ww_ww"]; + let tags: []str = ["c", "ww"]; + let artifactpaths: []str = [ + "/dep.wire.unit.ww", "/dep.wire.wwi", "/dep.wire.s", + "/dep.wire.o", "/dep.wire.a", + "/dep.aliasdep.unit.ww", "/dep.aliasdep.wwi", "/dep.aliasdep.s", + "/dep.aliasdep.o", "/dep.aliasdep.a", + "/dep.builtindep.unit.ww", "/dep.builtindep.wwi", + "/dep.builtindep.s", "/dep.builtindep.o", "/dep.builtindep.a", + "/dep.palette.unit.ww", "/dep.palette.wwi", "/dep.palette.s", + "/dep.palette.o", "/dep.palette.a", + "/matrix.unit.ww", "/matrix.wwi", "/matrix.s", "/matrix.o", + "/matrix.a", "/matrix.init.unit.ww", "/matrix.init.s", + "/matrix.init.o"]; + let artifactrefs: []str = alloc([], artifactpaths.len: u64)!; + let matrixbin: str = ""; + let matrixoutrefs: []str = ["", ""]; + let matrixerrrefs: []str = ["", ""]; + let si: i32 = 0; + for (si < stages.len) { + let work: str = strings.concat(root, "/matrix-work-", tags[si]); + let output: str = strings.concat(root, "/matrix-output-", tags[si]); + mkdirall(work); + let av: []str = [driver(stages[si]), "build", "-w", work, + "-I", source, "-o", output, "matrix"]; + let out: commandout; + runcommand(root, strings.concat("import-shadow-matrix-", tags[si]), av, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 && os.exists(output) + && !os.exists(strings.concat(output, ".new")) + && !os.exists(strings.concat(output, ".sepwork")) + && !directoryhasnew(work) + && !directoryhasfragment(work, ".wwtxn.") + && !directoryhasfragment(work, ".install") + && !directoryhasfragment(work, ".sepwork")); + matrixoutrefs[si] = strings.dup(out.stdout); + matrixerrrefs[si] = strings.dup(out.stderr); + let runav: []str = [output]; + runcommand(root, strings.concat("import-shadow-matrix-run-", tags[si]), + runav, time.second, &out); + expectexit(&out, 42); + assert(out.stdout.len == 0 && out.stderr.len == 0); + let ai: i32 = 0; + for (ai < artifactpaths.len) { + let bytes: str = readfile(strings.concat(work, artifactpaths[ai])); + if (si == 0) { append(artifactrefs, strings.dup(bytes)); } + else { assert(same(artifactrefs[ai], bytes)); }; + ai += 1; + }; + if (si == 0) { matrixbin = strings.dup(readfile(output)); } + else { assert(same(matrixbin, readfile(output))); }; + si += 1; + }; + assert(same(matrixoutrefs[0], matrixoutrefs[1]) + && same(matrixerrrefs[0], matrixerrrefs[1])); + + // A raw source operand and a package reached through an import graph use + // the same checker and declaration-point rule. + let raw: str = strings.concat(root, "/raw-shadow.ww"); + writefile(raw, strings.concat( + "package main;\nimport dep.wire;\n", + "fn main() i32 = { let wire: i32 = wire.value() + 31; ", + "return wire; };\n")); + writefile(strings.concat(shadowlib, "/shadowlib.ww"), strings.concat( + "package shadowlib;\nimport dep.wire;\n", + "export fn value() i32 = { let before: i32 = wire.value(); ", + "let wire: i32 = wire.value() + 31; ", + "if (before != 11) { return 1; }; return wire; };\n")); + writefile(strings.concat(client, "/main.ww"), strings.concat( + "package main;\nimport dep.shadowlib;\n", + "fn main() i32 = { return shadowlib.value(); };\n")); + writefile(strings.concat(sibling, "/imported.ww"), strings.concat( + "package main;\nimport dep.wire;\n", + "fn imported() i32 = { return wire.value(); };\n")); + writefile(strings.concat(sibling, "/main.ww"), strings.concat( + "package main;\ntype box = struct { n: i32 };\n", + "fn main() i32 = { let wire: box = box { n = 31 }; ", + "return imported() + wire.n; };\n")); + let rawbin: str = ""; + let clientbin: str = ""; + let siblingbin: str = ""; + si = 0; + for (si < stages.len) { + let rawwork: str = strings.concat(root, "/raw-work-", tags[si]); + let rawout: str = strings.concat(root, "/raw-output-", tags[si]); + mkdirall(rawwork); + let rawav: []str = [driver(stages[si]), "build", "-w", rawwork, + "-I", source, "-o", rawout, raw]; + let out: commandout; + runcommand(root, strings.concat("import-shadow-raw-", tags[si]), rawav, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 && os.exists(rawout) + && !os.exists(strings.concat(rawout, ".sepwork")) + && !directoryhasnew(rawwork) + && !directoryhasfragment(rawwork, ".wwtxn.") + && !directoryhasfragment(rawwork, ".sepwork")); + let runav: []str = [rawout]; + runcommand(root, strings.concat("import-shadow-raw-run-", tags[si]), + runav, time.second, &out); + expectexit(&out, 42); + assert(out.stdout.len == 0 && out.stderr.len == 0); + if (si == 0) { rawbin = strings.dup(readfile(rawout)); } + else { assert(same(rawbin, readfile(rawout))); }; + + let clientwork: str = strings.concat(root, "/client-work-", tags[si]); + let clientout: str = strings.concat(root, "/client-output-", tags[si]); + mkdirall(clientwork); + let clientav: []str = [driver(stages[si]), "build", "-w", clientwork, + "-I", source, "-o", clientout, "client"]; + runcommand(root, strings.concat("import-shadow-client-", tags[si]), + clientav, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && os.exists(clientout) + && !os.exists(strings.concat(clientout, ".sepwork")) + && !directoryhasnew(clientwork) + && !directoryhasfragment(clientwork, ".wwtxn.") + && !directoryhasfragment(clientwork, ".sepwork")); + let clientrun: []str = [clientout]; + runcommand(root, strings.concat("import-shadow-client-run-", tags[si]), + clientrun, time.second, &out); + expectexit(&out, 42); + assert(out.stdout.len == 0 && out.stderr.len == 0); + if (si == 0) { clientbin = strings.dup(readfile(clientout)); } + else { assert(same(clientbin, readfile(clientout))); }; + + let siblingwork: str = strings.concat(root, "/sibling-work-", tags[si]); + let siblingout: str = strings.concat(root, "/sibling-output-", tags[si]); + mkdirall(siblingwork); + let siblingav: []str = [driver(stages[si]), "build", "-w", siblingwork, + "-I", source, "-o", siblingout, "sibling"]; + runcommand(root, strings.concat("import-shadow-sibling-", tags[si]), + siblingav, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && os.exists(siblingout) + && !os.exists(strings.concat(siblingout, ".sepwork")) + && !directoryhasnew(siblingwork) + && !directoryhasfragment(siblingwork, ".wwtxn.") + && !directoryhasfragment(siblingwork, ".sepwork")); + let siblingrun: []str = [siblingout]; + runcommand(root, strings.concat("import-shadow-sibling-run-", tags[si]), + siblingrun, time.second, &out); + expectexit(&out, 42); + assert(out.stdout.len == 0 && out.stderr.len == 0); + if (si == 0) { siblingbin = strings.dup(readfile(siblingout)); } + else { assert(same(siblingbin, readfile(siblingout))); }; + si += 1; + }; + + // A selector on a closer local is not package use. The otherwise-unused + // import is diagnosed first by source position, ahead of the later invalid + // local selector, and the request commits no cold work or publication. + writefile(strings.concat(invalid, "/main.ww"), strings.concat( + "package main;\nimport dep.wire;\n", + "fn main() i32 = { let wire: i32 = 2; ", + "return wire.n; };\n")); + writefile(strings.concat(invalidscalar, "/main.ww"), strings.concat( + "package main;\nimport dep.wire;\n", + "fn main() i32 = { let before: i32 = wire.value(); ", + "let wire: i32 = 2; if (before == 0) { return 0; }; ", + "return wire.n; };\n")); + writefile(strings.concat(invalidscalar, "/main_test.ww"), + "package main;\n@test fn never() void = { assert(main() == 0); };\n"); + writefile(strings.concat(invalidtype, "/main.ww"), strings.concat( + "package main;\nimport dep.wire;\n", + "fn main() i32 = { let before: i32 = wire.value(); ", + "let wire: i32 = 2; let later: wire.record; ", + "return before + wire; };\n")); + let invaliddiag: str = ""; + si = 0; + for (si < stages.len) { + let work: str = strings.concat(root, "/invalid-work-", tags[si]); + let output: str = strings.concat(root, "/invalid-output-", tags[si]); + mkdirall(work); + let av: []str = [driver(stages[si]), "build", "-w", work, + "-I", source, "-o", output, "invalid"]; + let out: commandout; + runcommand(root, strings.concat("import-shadow-invalid-", tags[si]), av, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 + && same(primarydiagnostic(out.stderr), + "\"dep.wire\" imported and not used") + && occurrences(out.stderr, "imported and not used") == 1 + && occurrences(out.stderr, "selector 'n' undefined") == 1 + && !has(out.stderr, "shadows imported module") + && !has(out.stderr, "use of package wire not in selector") + && !has(out.stderr, "asserttyped:") + && !has(out.stderr, "undefined reference") + && !os.exists(output) + && !os.exists(strings.concat(output, ".new")) + && !os.exists(strings.concat(output, ".sepwork")) + && directoryisempty(work)); + let normalized: str = normalizedtrace(out.stderr, + strings.concat(work, "/"), output); + if (si == 0) { invaliddiag = strings.dup(normalized); } + else { assert(same(invaliddiag, normalized)); }; + si += 1; + }; + + // Once the closer binding exists, value and type selectors must stay on + // that local object. Invalid local value selection is one positioned + // checker error, and a dotted type cannot recover the hidden package name. + let invalidtargets: []str = ["invalidscalar", "invalidtype"]; + let invalidcores: []str = ["selector 'n' undefined", + "unknown type 'wire.record'"]; + let invalidlocations: []str = [":4:109: error: selector 'n' undefined", + ":4:81: error: unknown type 'wire.record'"]; + let invalidshaperefs: []str = ["", ""]; + let invalidretainedrefs: []str = ["", ""]; + si = 0; + for (si < stages.len) { + let ii: i32 = 0; + for (ii < invalidtargets.len) { + let work: str = strings.concat(root, "/invalid-shape-work-", + tags[si], "-", boundarypkgname(ii)); + let output: str = strings.concat(root, "/invalid-shape-output-", + tags[si], "-", boundarypkgname(ii)); + mkdirall(work); + let av: []str = [driver(stages[si]), "build", "-w", work, + "-I", source, "-o", output, invalidtargets[ii]]; + let out: commandout; + runcommand(root, strings.concat("import-shadow-invalid-shape-", + tags[si], "-", boundarypkgname(ii)), av, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 + && same(primarydiagnostic(out.stderr), invalidcores[ii]) + && has(out.stderr, invalidlocations[ii]) + && occurrences(out.stderr, invalidcores[ii]) == 1 + && !has(out.stderr, "shadows imported module") + && !has(out.stderr, "asserttyped:") + && !has(out.stderr, "calling non-function") + && !has(out.stderr, "undefined reference") + && !os.exists(output) + && !os.exists(strings.concat(output, ".new")) + && !os.exists(strings.concat(output, ".sepwork")) + && directoryisempty(work)); + let normalized: str = normalizedtrace(out.stderr, + strings.concat(work, "/"), output); + if (si == 0) { + invalidshaperefs[ii] = strings.dup(normalized); + } else { assert(same(invalidshaperefs[ii], normalized)); }; + ii += 1; + }; + + // A cold retained test request must reject before producing a runnable + // or any committed/staged work artifact. + let testwork: str = strings.concat(root, + "/invalid-retained-work-", tags[si]); + let retained: str = strings.concat(root, + "/invalid-retained-", tags[si], ".test"); + mkdirall(testwork); + let testav: []str = [driver(stages[si]), "test", "-w", testwork, + "-I", source, "-run", "never", "-o", retained, + "invalidscalar"]; + let tout: commandout; + runcommand(root, strings.concat("import-shadow-invalid-retained-", + tags[si]), testav, + (120i64 * (time.second: i64)): time.duration, &tout); + expectexit(&tout, 1); + assert(same(tout.stdout, "FAIL\n") + && same(primarydiagnostic(tout.stderr), invalidcores[0]) + && has(tout.stderr, invalidlocations[0]) + && occurrences(tout.stderr, invalidcores[0]) == 1 + && !has(tout.stderr, "asserttyped:") + && !os.exists(retained) + && !os.exists(strings.concat(retained, ".new")) + && !os.exists(strings.concat(retained, ".sepwork")) + && directoryisempty(testwork)); + let testnormalized: str = normalizedtrace(tout.stderr, + strings.concat(testwork, "/"), retained); + if (si == 0) { + invalidretainedrefs[si] = strings.dup(testnormalized); + } else { + invalidretainedrefs[si] = strings.dup(testnormalized); + assert(same(invalidretainedrefs[0], invalidretainedrefs[1])); + }; + si += 1; + }; + + // Production called by a test, same-package test source, external-test + // source, and an honest test-only package all accept the same lexical rule. + writefile(strings.concat(roleprod, "/prod.ww"), strings.concat( + "package roleprod;\nimport dep.wire;\n", + "fn value() i32 = { let wire: i32 = wire.value() + 31; ", + "return wire; };\n")); + writefile(strings.concat(roleprod, "/prod_test.ww"), + "package roleprod;\n@test fn shadow_ok() void = { assert(value() == 42); };\n"); + writefile(strings.concat(rolesame, "/prod.ww"), + "package rolesame;\nfn anchor() i32 = { return 1; };\n"); + writefile(strings.concat(rolesame, "/same_test.ww"), strings.concat( + "package rolesame;\nimport dep.wire;\n", + "@test fn shadow_ok() void = { let before: i32 = wire.value(); ", + "let wire: i32 = wire.value() + 31; ", + "assert(anchor() == 1 && before == 11 && wire == 42); };\n")); + writefile(strings.concat(roleexternal, "/prod.ww"), + "package roleexternal;\nexport fn anchor() i32 = { return 1; };\n"); + writefile(strings.concat(roleexternal, "/external_test.ww"), strings.concat( + "package roleexternal_test;\nimport dep.wire;\n", + "@test fn shadow_ok() void = { let wire: i32 = wire.value() + 31; ", + "assert(wire == 42); };\n")); + writefile(strings.concat(roleonly, "/only_test.ww"), strings.concat( + "package roleonly;\nimport dep.wire;\n", + "@test fn shadow_ok() void = { let wire: i32 = wire.value() + 31; ", + "assert(wire == 42); };\n")); + let roletargets: []str = ["roleprod", "rolesame", "roleexternal", + "roleonly"]; + let roleoutrefs: []str = ["", "", "", ""]; + let roleerrrefs: []str = ["", "", "", ""]; + let rolebinrefs: []str = ["", "", "", ""]; + si = 0; + for (si < stages.len) { + let ri: i32 = 0; + for (ri < roletargets.len) { + let work: str = strings.concat(root, "/role-work-", tags[si], "-", + boundarypkgname(ri)); + let output: str = strings.concat(root, "/role-output-", tags[si], "-", + boundarypkgname(ri)); + mkdirall(work); + let av: []str = [driver(stages[si]), "test", "-w", work, + "-I", source, "-run", "shadow_ok", "-o", output, + roletargets[ri]]; + let out: commandout; + runcommand(root, strings.concat("import-shadow-role-", tags[si], "-", + boundarypkgname(ri)), av, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0 && os.exists(output) + && !os.exists(strings.concat(output, ".sepwork")) + && occurrences(out.stdout, ".shadow_ok ... ok\n") == 1 + && occurrences(out.stdout, + "1 passed, 0 failed, 0 skipped, 0 harness errors\n") == 1 + && !directoryhasnew(work) + && !directoryhasfragment(work, ".wwtxn.") + && !directoryhasfragment(work, ".install") + && !directoryhasfragment(work, ".sepwork")); + if (si == 0) { + roleoutrefs[ri] = strings.dup(out.stdout); + roleerrrefs[ri] = strings.dup(out.stderr); + rolebinrefs[ri] = strings.dup(readfile(output)); + } else { + assert(same(roleoutrefs[ri], out.stdout) + && same(roleerrrefs[ri], out.stderr) + && same(rolebinrefs[ri], readfile(output))); + }; + ri += 1; + }; + si += 1; + }; + + // Warm semantic rejection preserves every committed action byte and public + // binary. Restoring exact input reuses the same generation byte-for-byte. + let warmfile: str = strings.concat(warm, "/main.ww"); + let warmvalid: str = strings.concat( + "package main;\nimport dep.wire;\n", + "fn main() i32 = { let wire: i32 = wire.value() + 31; ", + "return wire; };\n"); + let warminvalid: str = strings.concat( + "package main;\nimport dep.wire;\n", + "fn main() i32 = { let wire: i32 = 42; ", + "return wire.n; };\n"); + writefile(warmfile, warmvalid); + let warmpaths: []str = ["/.wwtool.ww", "/.wwtool.w6c", "/.wwtool.w6a", + "/.wwtool.stamp", "/dep.wire.unit.ww", "/dep.wire.wwi", + "/dep.wire.s", "/dep.wire.o", "/dep.wire.a", "/warm.unit.ww", + "/warm.wwi", "/warm.s", "/warm.o", "/warm.a", + "/warm.init.unit.ww", "/warm.init.s", "/warm.init.o"]; + let warmcross: []str = alloc([], warmpaths.len: u64)!; + let warmbincross: str = ""; + let warmdiag: str = ""; + + // Retention uses a production function called by the selected test. The + // unselected aborting row proves filtering survives retained/direct routes. + let retainedfile: str = strings.concat(retainedcase, "/prod.ww"); + let retainedvalid: str = strings.concat( + "package retainedcase;\nimport dep.wire;\n", + "fn value() i32 = { let wire: i32 = wire.value() + 31; ", + "return wire; };\n"); + let retainedinvalid: str = strings.concat( + "package retainedcase;\nimport dep.wire;\n", + "fn value() i32 = { let wire: i32 = 42; ", + "return wire.n; };\n"); + writefile(retainedfile, retainedvalid); + writefile(strings.concat(retainedcase, "/retained_test.ww"), strings.concat( + "package retainedcase;\n", + "@test fn kept() void = { assert(value() == 42); };\n", + "@test fn not_selected() void = { abort(); };\n")); + let retainedcross: str = ""; + let retainedoutrefs: []str = ["", ""]; + let retainederrrefs: []str = ["", ""]; + let retainedinvaliddiagrefs: []str = ["", ""]; + let directoutrefs: []str = ["", ""]; + let directerrrefs: []str = ["", ""]; + si = 0; + for (si < stages.len) { + let work: str = strings.concat(root, "/warm-work-", tags[si]); + let output: str = strings.concat(root, "/warm-output-", tags[si]); + mkdirall(work); + let av: []str = [driver(stages[si]), "build", "-w", work, + "-I", source, "-o", output, "warm"]; + let out: commandout; + runcommand(root, strings.concat("import-shadow-warm-cold-", tags[si]), av, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 && os.exists(output)); + let snapshots: []str = alloc([], warmpaths.len: u64)!; + let wi: i32 = 0; + for (wi < warmpaths.len) { + let bytes: str = strings.dup(readfile(strings.concat(work, + warmpaths[wi]))); + append(snapshots, bytes); + if (si == 0) { append(warmcross, strings.dup(bytes)); } + else if (wi >= 4) { assert(same(warmcross[wi], bytes)); }; + wi += 1; + }; + let binbytes: str = strings.dup(readfile(output)); + if (si == 0) { warmbincross = strings.dup(binbytes); } + else { assert(same(warmbincross, binbytes)); }; + rewritefile(warmfile, warminvalid); + runcommand(root, strings.concat("import-shadow-warm-invalid-", tags[si]), + av, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 + && same(primarydiagnostic(out.stderr), + "\"dep.wire\" imported and not used") + && occurrences(out.stderr, "selector 'n' undefined") == 1 + && same(binbytes, readfile(output)) + && !os.exists(strings.concat(output, ".new")) + && !os.exists(strings.concat(output, ".sepwork"))); + let normalized: str = normalizedtrace(out.stderr, + strings.concat(work, "/"), output); + if (si == 0) { warmdiag = strings.dup(normalized); } + else { assert(same(warmdiag, normalized)); }; + wi = 0; + for (wi < warmpaths.len) { + assert(same(snapshots[wi], readfile(strings.concat(work, + warmpaths[wi])))); + wi += 1; + }; + assert(!directoryhasnew(work) + && !directoryhasfragment(work, ".wwtxn.") + && !directoryhasfragment(work, ".install") + && !directoryhasfragment(work, ".sepwork")); + rewritefile(warmfile, warmvalid); + runcommand(root, strings.concat("import-shadow-warm-restored-", tags[si]), + av, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && same(binbytes, readfile(output)) + && !os.exists(strings.concat(output, ".sepwork")) + && !directoryhasnew(work) + && !directoryhasfragment(work, ".wwtxn.") + && !directoryhasfragment(work, ".install") + && !directoryhasfragment(work, ".sepwork")); + wi = 0; + for (wi < warmpaths.len) { + assert(same(snapshots[wi], readfile(strings.concat(work, + warmpaths[wi])))); + wi += 1; + }; + + let testwork: str = strings.concat(root, "/retained-work-", tags[si]); + let retained: str = strings.concat(root, "/retained-", tags[si], ".test"); + mkdirall(testwork); + let testav: []str = [driver(stages[si]), "test", "-w", testwork, + "-I", source, "-run", "kept", "-o", retained, + "retainedcase"]; + runcommand(root, strings.concat("import-shadow-retained-cold-", tags[si]), + testav, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0 && os.exists(retained) + && !os.exists(strings.concat(retained, ".sepwork")) + && occurrences(out.stdout, "retainedcase.kept ... ok\n") == 1 + && occurrences(out.stdout, + "1 passed, 0 failed, 0 skipped, 0 harness errors\n") == 1 + && !has(out.stdout, "not_selected") + && !directoryhasnew(testwork) + && !directoryhasfragment(testwork, ".wwtxn.") + && !directoryhasfragment(testwork, ".sepwork")); + retainedoutrefs[si] = strings.dup(out.stdout); + retainederrrefs[si] = strings.dup(out.stderr); + let retainedbytes: str = strings.dup(readfile(retained)); + if (si == 0) { retainedcross = strings.dup(retainedbytes); } + else { assert(same(retainedcross, retainedbytes)); }; + rewritefile(retainedfile, retainedinvalid); + runcommand(root, strings.concat("import-shadow-retained-invalid-", + tags[si]), testav, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(out.stdout, "FAIL\n") + && same(primarydiagnostic(out.stderr), + "\"dep.wire\" imported and not used") + && occurrences(out.stderr, "selector 'n' undefined") == 1 + && same(retainedbytes, readfile(retained)) + && !os.exists(strings.concat(retained, ".new")) + && !os.exists(strings.concat(retained, ".sepwork")) + && !directoryhasnew(testwork) + && !directoryhasfragment(testwork, ".wwtxn.") + && !directoryhasfragment(testwork, ".install") + && !directoryhasfragment(testwork, ".sepwork")); + let retainedinvalidnormalized: str = normalizedtrace(out.stderr, + strings.concat(testwork, "/"), retained); + retainedinvaliddiagrefs[si] = strings.dup(retainedinvalidnormalized); + if (si == 1) { + assert(same(retainedinvaliddiagrefs[0], + retainedinvaliddiagrefs[1])); + }; + let directav: []str = [retained, "-package=retainedcase", "kept"]; + runcommand(root, strings.concat("import-shadow-retained-direct-", + tags[si]), directav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0 + && occurrences(out.stdout, "retainedcase.kept ... ok\n") == 1 + && occurrences(out.stdout, + "1 passed, 0 failed, 0 skipped, 0 harness errors\n") == 1 + && !has(out.stdout, "not_selected")); + directoutrefs[si] = strings.dup(out.stdout); + directerrrefs[si] = strings.dup(out.stderr); + rewritefile(retainedfile, retainedvalid); + runcommand(root, strings.concat("import-shadow-retained-restored-", + tags[si]), testav, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(same(out.stdout, retainedoutrefs[si]) + && same(out.stderr, retainederrrefs[si]) + && same(retainedbytes, readfile(retained)) + && !os.exists(strings.concat(retained, ".sepwork")) + && !directoryhasnew(testwork) + && !directoryhasfragment(testwork, ".wwtxn.") + && !directoryhasfragment(testwork, ".install") + && !directoryhasfragment(testwork, ".sepwork")); + si += 1; + }; + assert(same(retainedoutrefs[0], retainedoutrefs[1]) + && same(retainederrrefs[0], retainederrrefs[1]) + && same(directoutrefs[0], directoutrefs[1]) + && same(directerrrefs[0], directerrrefs[1])); + assert(!directoryhasnew(root) + && !directoryhasfragment(root, ".wwtxn.") + && !directoryhasfragment(root, ".install") + && !directoryhasfragment(root, ".sepwork") + && !directoryhasfragment(root, ".capture") + && !directoryhasfragment(root, ".result") + && !directoryhasfragment(root, ".request")); + clean(root); +}; diff --git a/test/tool/rejects_test.ww b/test/tool/rejects_test.ww index eb3dd89e..43bad562 100644 --- a/test/tool/rejects_test.ww +++ b/test/tool/rejects_test.ww @@ -1,28 +1,24 @@ package rejects_test; -// Cstage-only compile-reject observers on the `ww` driver. Ports of -// the retired native carriers test/wcc/708_param_shadow_mod.c, -// 712_redecl.c and 961_opaque_guards.c; every assertion preserved. +// Compile-reject observers plus the lexical import-shadowing acceptance +// matrix. Ports of the retired native carriers +// test/wcc/708_param_shadow_mod.c, 712_redecl.c and +// 961_opaque_guards.c. // -// ASYMMETRIC polarity, all three families: the reject lives only in -// cstage check.c. Wwstage's check.ww is a single-pass resolve walk -// with no per-block scoping (#11) and no #108(b) require_sized -// construction/binding guards, so it ACCEPTS these programs — the -// both-stage //ww:error fixture contract cannot carry them -// (residual-carrier-audit.json, 708/712/961 entries). Each row -// asserts the CSTAGE reject only; when wwstage gains the rule (#11 -// per-block scoping; the #108(b) guards), the rows graduate to -// //ww:error fixtures and this observer shrinks. The paramshadow neg -// rows additionally require the sibling-module fixture tree, so they -// stay here until a single-file multi-package repro is validated. +// The redeclaration and opaque-guard families below retain their historical +// Cstage-only assertions. Import-name shadowing has the opposite polarity: +// both stages accept a closer lexical value binding, while the package-name +// object remains visible before that binding and after its scope ends. The +// fixture tree is retained because the raw-source route needs a real sibling +// package to exercise selector use and file-local import ownership. // -// paramshadow (#19/#16) — value names and module names are disjoint: -// a param/let/mlet/for-range/match-case binding named `shadowmod` in -// a file importing module shadowmod is rejected at the decl site; the -// renamed binding in a real `package main` builds+runs 42 (no over-trigger); a param named -// like a module a SIBLING module imports does not trip (src_imports -// filters by the binding's own module — build+run exit 2). The -// carrier's neg_selfimp leg is DROPPED here: +// paramshadow (#19/#16) — an import qualifier is a file-scope binding. +// A param/let/mlet/for-range/match-case binding may shadow it in a closer +// lexical scope. Each legacy neg_* fixture now genuinely uses +// `shadowmod.say()` before or in the binding declaration and then returns a +// value computed from the closer binding. The renamed control still runs 42; +// a parameter named like a package imported only by a sibling package still +// runs 2. The carrier's neg_selfimp leg remains dropped here: // test/wcc/data/r948_selfimport/case.ww owns the identical claim // (`//ww:error "self-import"`, both frontends). // @@ -67,51 +63,47 @@ fn fixdir() str = { return strings.concat(testenv.repo(), "/test/wcc/data/paramshadowmod"); }; -@test fn paramshadow_neg() void = { - let tags: []str = ["param", "let", "mlet", "forrange_single", - "forrange_tuple", "mcase"]; +fn paramshadow_case(src: str, label: str, want: i32) void = { + let stages: []str = ["ww", "ww_ww"]; + let tags: []str = ["c", "ww"]; let i: i32 = 0; - for (i < tags.len) { + for (i < stages.len) { let td: str = testenv.fresh(); - // cwd is the fixture dir so the driver's source-dir-first import - // search resolves `import shadowmod;`; -o keeps the binary and - // its sepwork out of the tracked tree. - let av: []str = [testenv.driver("ww"), "build", "-o", - strings.concat(td, "/out"), - strings.concat("neg_", tags[i], ".ww")]; - if (runcode(fixdir(), td, strings.concat("neg_", tags[i]), av) - == 0) { - fail(strings.concat("neg_", tags[i]), - "build unexpectedly succeeded -- shadow rule did not fire"); + let out: str = strings.concat(td, "/out"); + // cwd is the fixture dir so source-dir-first import search resolves + // `import shadowmod;`; -o keeps every product outside the tree. + let av: []str = [testenv.driver(stages[i]), "build", "-o", out, src]; + let stem: str = strings.concat(label, "_", tags[i]); + if (runcode(fixdir(), td, strings.concat("build_", stem), av) != 0) { + fail(stem, "build failed -- lexical import shadowing was rejected"); + }; + let rav: []str = [out]; + if (runcode(td, td, strings.concat("run_", stem), rav) != want) { + fail(stem, "built binary exit != exact expected value"); }; testenv.clean(td); i += 1; }; }; -fn paramshadow_pos(src: str, label: str, want: i32, why: str) void = { - let td: str = testenv.fresh(); - let out: str = strings.concat(td, "/out"); - let av: []str = [testenv.driver("ww"), "build", "-o", out, src]; - if (runcode(fixdir(), td, strings.concat("build_", label), av) != 0) { - fail(label, why); +@test fn paramshadow_lexical_bindings() void = { + let tags: []str = ["param", "let", "mlet", "forrange_single", + "forrange_tuple", "mcase"]; + let wants: []i32 = [44, 42, 45, 107, 45, 42]; + let i: i32 = 0; + for (i < tags.len) { + paramshadow_case(strings.concat("neg_", tags[i], ".ww"), tags[i], + wants[i]); + i += 1; }; - let rav: []str = [out]; - if (runcode(td, td, strings.concat("run_", label), rav) != want) { - fail(label, "built binary exit != expected"); - }; - testenv.clean(td); }; @test fn paramshadow_pos_rename() void = { - paramshadow_pos("pos_rename.ww", "pos_rename", 42, - "build failed -- rule over-triggered after the rename"); + paramshadow_case("pos_rename.ww", "pos_rename", 42); }; @test fn paramshadow_pos_crossmod() void = { - paramshadow_pos("pos_crossmod.ww", "pos_crossmod", 2, strings.concat( - "build failed -- shadow rule over-triggered on a ", - "cross-module param")); + paramshadow_case("pos_crossmod.ww", "pos_crossmod", 2); }; fn rejectrows(family: str, labels: []str, srcs: []str) void = { diff --git a/test/wcc/data/paramshadowmod/crossmod/crossmod.ww b/test/wcc/data/paramshadowmod/crossmod/crossmod.ww index a8dd2cf6..45c9bd9b 100644 --- a/test/wcc/data/paramshadowmod/crossmod/crossmod.ww +++ b/test/wcc/data/paramshadowmod/crossmod/crossmod.ww @@ -1,8 +1,6 @@ -// crossmod — directory package whose probe() takes a param named like the -// module `shadowmod` that paramshadowmod (NOT crossmod) imports. The -// shadow rule is filtered by the binding's own module (src_imports -// cur_mod filter): crossmod carries no `import shadowmod`, so this param -// must NOT trip even though a sibling package imports that leaf. +// crossmod — directory package whose probe() takes a parameter named like the +// module `shadowmod` that a sibling package imports. Since crossmod itself has +// no such file-local package-name object, this is an ordinary parameter. package crossmod; diff --git a/test/wcc/data/paramshadowmod/neg_forrange_single.ww b/test/wcc/data/paramshadowmod/neg_forrange_single.ww index ad8ff43d..979d0264 100644 --- a/test/wcc/data/paramshadowmod/neg_forrange_single.ww +++ b/test/wcc/data/paramshadowmod/neg_forrange_single.ww @@ -1,15 +1,14 @@ -// neg_forrange_single — `for (let shadowmod .. s)` single-binding -// range loop where the loop variable shadows the imported module. -// N_FORRANGE wires check_module_shadow on the single-name branch -// (n->str), so the rule fires at the for header. +// Legacy neg_forrange_single, now a positive range-binding case. The import +// initializes total; the one-byte "A" iteration binds the closer shadowmod +// value in the loop body. Builds and runs 42 + 65 = 107. -package paramshadowmod; +package main; import shadowmod; -export fn main() i32 = { - let s: str = "abc"; - let total: i32 = 0i32; +fn main() i32 = { + let s: str = "A"; + let total: i32 = shadowmod.say(); for (let shadowmod .. s) { total += shadowmod: i32; }; diff --git a/test/wcc/data/paramshadowmod/neg_forrange_tuple.ww b/test/wcc/data/paramshadowmod/neg_forrange_tuple.ww index aa250f4b..dad4738e 100644 --- a/test/wcc/data/paramshadowmod/neg_forrange_tuple.ww +++ b/test/wcc/data/paramshadowmod/neg_forrange_tuple.ww @@ -1,14 +1,12 @@ -// neg_forrange_tuple — `for (let (shadowmod, x) .. s)` tuple- -// destructure range loop where the first binder shadows the -// imported module. N_FORRANGE wires check_module_shadow per-name -// on the tuple branch (n->list), so the rule fires at the for -// header even though `x` is innocuous. +// Legacy neg_forrange_tuple, now a positive tuple-range case. The import +// initializes total; the loop's first tuple element is the closer shadowmod +// binding. Builds and runs 42 + 1 + 2 = 45. -package paramshadowmod; +package main; import shadowmod; -export fn main() i32 = { +fn main() i32 = { let buf: [2]i64; buf[0] = 1i64; buf[1] = 2i64; @@ -16,7 +14,7 @@ export fn main() i32 = { s.ptr = buf.ptr: *(i64, i64); s.len = 1; s.cap = 1; - let total: i64 = 0i64; + let total: i64 = shadowmod.say(): i64; for (let (shadowmod, x) .. s) { total += shadowmod + x; }; diff --git a/test/wcc/data/paramshadowmod/neg_let.ww b/test/wcc/data/paramshadowmod/neg_let.ww index 44d3ace1..7d062df2 100644 --- a/test/wcc/data/paramshadowmod/neg_let.ww +++ b/test/wcc/data/paramshadowmod/neg_let.ww @@ -1,12 +1,12 @@ -// neg_let — local `let shadowmod: i32 = ...` shadows the imported -// module from inside a fn body. Same rule fires for nested-scope -// let binds, not just params. +// Legacy neg_let, now a positive declaration-point case. The initializer +// resolves shadowmod.say through the file-scope import; after the declaration +// shadowmod denotes the closer i32 binding. Builds and runs 42. -package paramshadowmod; +package main; import shadowmod; -export fn main() i32 = { - let shadowmod: i32 = 0i32; +fn main() i32 = { + let shadowmod: i32 = shadowmod.say(); return shadowmod; }; diff --git a/test/wcc/data/paramshadowmod/neg_mcase.ww b/test/wcc/data/paramshadowmod/neg_mcase.ww index 7e6d169b..61f88cf0 100644 --- a/test/wcc/data/paramshadowmod/neg_mcase.ww +++ b/test/wcc/data/paramshadowmod/neg_mcase.ww @@ -1,9 +1,8 @@ -// neg_mcase — `match (r) { case let shadowmod: i64 => ... }` where -// the per-arm binder shadows the imported module. N_MCASE wires -// check_module_shadow before scope_define on cs->str, so the rule -// fires at the case line. +// Legacy neg_mcase, now a positive match-arm case. The import supplies the +// tagged value before the arm binder exists; inside that arm shadowmod is the +// closer i64 binding. Builds and runs 42. -package paramshadowmod; +package main; import shadowmod; @@ -14,8 +13,8 @@ fn parse(n: i64) (i64 | i32) = { return n; }; -export fn main() i32 = { - let r: (i64 | i32) = parse(42i64); +fn main() i32 = { + let r: (i64 | i32) = parse(shadowmod.say(): i64); let out: i64 = 0i64; match (r) { case let shadowmod: i64 => out = shadowmod; diff --git a/test/wcc/data/paramshadowmod/neg_mlet.ww b/test/wcc/data/paramshadowmod/neg_mlet.ww index b2e2574e..a8d89023 100644 --- a/test/wcc/data/paramshadowmod/neg_mlet.ww +++ b/test/wcc/data/paramshadowmod/neg_mlet.ww @@ -1,9 +1,8 @@ -// neg_mlet — Hare tuple destructure `let (shadowmod, x) = pair();` -// where the first binder shadows the imported module. N_MLET wires -// check_module_shadow per-binder, so the rule fires at the first -// name; the second binder `x` is innocuous. +// Legacy neg_mlet, now a positive tuple-binding case. The selector use before +// the destructure belongs to the import; afterward shadowmod is the first +// tuple element. Builds and runs 42 + 1 + 2 = 45. -package paramshadowmod; +package main; import shadowmod; @@ -11,7 +10,8 @@ fn pair() (i64, i64) = { return 1i64, 2i64; }; -export fn main() i32 = { +fn main() i32 = { + let anchor: i32 = shadowmod.say(); let (shadowmod, x) = pair(); - return (shadowmod + x): i32; + return anchor + (shadowmod + x): i32; }; diff --git a/test/wcc/data/paramshadowmod/neg_param.ww b/test/wcc/data/paramshadowmod/neg_param.ww index 9ea0f008..1706b4bf 100644 --- a/test/wcc/data/paramshadowmod/neg_param.ww +++ b/test/wcc/data/paramshadowmod/neg_param.ww @@ -1,8 +1,8 @@ -// neg_param — fn param `shadowmod: str` shadows the imported module. -// Under the "value names and module names are disjoint" rule the -// build must fail with a clear diagnostic at the param decl site. +// Legacy neg_param, now a positive lexical-scope case. The package selector +// in main uses the file-scope import; the parameter named shadowmod is a +// closer binding only inside probe. Builds and runs 42 + len("hi") = 44. -package paramshadowmod; +package main; import shadowmod; @@ -10,6 +10,6 @@ fn probe(shadowmod: str) i32 = { return shadowmod.len; }; -export fn main() i32 = { - return probe("hi"); +fn main() i32 = { + return shadowmod.say() + probe("hi"); }; diff --git a/test/wcc/data/paramshadowmod/pos_crossmod.ww b/test/wcc/data/paramshadowmod/pos_crossmod.ww index d60e0bce..b7394844 100644 --- a/test/wcc/data/paramshadowmod/pos_crossmod.ww +++ b/test/wcc/data/paramshadowmod/pos_crossmod.ww @@ -1,17 +1,14 @@ -// pos_crossmod — cur_mod-filtering positive. Replaces the abolished -// pos_selfimp (self-import is hard-rejected post-#16). This module -// (paramshadowmod) imports `shadowmod`, and the bundle also pulls module -// `crossmod`, whose probe() has a param named `shadowmod`. crossmod does -// NOT import shadowmod, so the shadow rule — filtered by the binding's -// own module — must NOT trip crossmod's param, even though a sibling -// module in the same bundle imports that leaf. Build + run; exit = 2. +// pos_crossmod — file-local import ownership control. This source imports +// shadowmod, while crossmod's separate source has a parameter of that name +// and no such import. The caller's qualifier cannot leak into the sibling +// package's lexical scope. Both stages build and run 2. package main; import shadowmod; import crossmod; -export fn main() i32 = { +fn main() i32 = { let _ = shadowmod.say(); return crossmod.probe("hi"); }; diff --git a/test/wcc/data/paramshadowmod/pos_rename.ww b/test/wcc/data/paramshadowmod/pos_rename.ww index b96844ee..bec40d68 100644 --- a/test/wcc/data/paramshadowmod/pos_rename.ww +++ b/test/wcc/data/paramshadowmod/pos_rename.ww @@ -1,6 +1,5 @@ -// pos_rename — positive case. The fn param is renamed away from the -// imported module's bareword, so the rule doesn't fire and the body -// can call `shadowmod.say()` cleanly. Built + run; exit code = 42. +// pos_rename — control with an unrelated parameter name. The file-scope +// import remains visible in probe. Both stages build and run 42. package main; @@ -11,6 +10,6 @@ fn probe(s: str) i32 = { return shadowmod.say(); }; -export fn main() i32 = { +fn main() i32 = { return probe("hi"); }; diff --git a/test/wcc/data/paramshadowmod/shadowmod/shadowmod.ww b/test/wcc/data/paramshadowmod/shadowmod/shadowmod.ww index 54ba59e2..f9041ace 100644 --- a/test/wcc/data/paramshadowmod/shadowmod/shadowmod.ww +++ b/test/wcc/data/paramshadowmod/shadowmod/shadowmod.ww @@ -1,6 +1,6 @@ -// paramshadowmod/shadowmod — a tiny module the negative/positive -// fixtures import as `use shadowmod;`. Carries one fn so the leaf -// resolves through the module dot path when name resolution succeeds. +// paramshadowmod/shadowmod — the sibling package used by the lexical +// import-shadowing fixtures. The exported function makes each pre-binding +// qualifier occurrence observable at runtime. package shadowmod;