fix: allow lexical import shadowing
This commit is contained in:
220
cmd/wcc/check.c
220
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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
15
docs/spec.md
15
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user