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