fix: reject bare import bindings

This commit is contained in:
2026-08-22 15:17:10 +09:00
parent 91fce80b37
commit 89f519d5cb
6 changed files with 1490 additions and 38 deletions

View File

@@ -68,12 +68,17 @@ static int src_imports(Node *file, const char *modtag, int source,
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_bare_import_binding(Checker *c, const char *name);
static int reject_bare_import_values(Checker *c, Node *n);
static int reject_bare_import_types(Checker *c, Node *n);
static void resolve_typedecl(Checker *c, Node *d);
static Type *
resolve_typename(Checker *c, Node *n)
{
const char *nm = n->str;
if (lookup_bare_import_binding(c, nm) != NULL)
return err(c, n->pos, "%s (package name) is not a type", nm);
Type *bi = lookup_builtin(nm);
if (bi) return bi;
/* #225: kind-filtered so a same-named value binding (param/let/fn)
@@ -328,6 +333,11 @@ eval_enum_value(Checker *c, Node *n, Tfield *prev, u64 *out)
if (fold_int_literal(n, out)) return 1;
switch (n->kind) {
case N_IDENT: {
if (lookup_bare_import_binding(c, n->str) != NULL) {
n->type = err(c, n->pos,
"use of package %s not in selector", n->str);
return 0;
}
for (Tfield *f = prev; f; f = f->next) {
if (f->name && n->str &&
strcmp(f->name, n->str) == 0) {
@@ -342,8 +352,12 @@ eval_enum_value(Checker *c, Node *n, Tfield *prev, u64 *out)
case N_BIN: {
u64 a, b;
if (!eval_enum_value(c, n->lhs, prev, &a) ||
!eval_enum_value(c, n->rhs, prev, &b))
!eval_enum_value(c, n->rhs, prev, &b)) {
if ((n->lhs && n->lhs->type == ty_err) ||
(n->rhs && n->rhs->type == ty_err))
n->type = ty_err;
return 0;
}
if (fold_binop(n->op, a, b, out))
return 1;
if ((n->op == TK_SLASH || n->op == TK_PERCENT) && b == 0)
@@ -355,8 +369,10 @@ eval_enum_value(Checker *c, Node *n, Tfield *prev, u64 *out)
}
case N_UN: {
u64 v;
if (!eval_enum_value(c, n->lhs, prev, &v))
if (!eval_enum_value(c, n->lhs, prev, &v)) {
if (n->lhs && n->lhs->type == ty_err) n->type = ty_err;
return 0;
}
switch (n->op) {
case TK_MINUS: *out = (u64)(-(i64)v); return 1;
case TK_TILDE: *out = ~v; return 1;
@@ -578,8 +594,12 @@ eval_def_const(Checker *c, Node *n, u64 *out, int depth)
case N_BIN: {
u64 a, b;
if (!eval_def_const(c, n->lhs, &a, depth + 1) ||
!eval_def_const(c, n->rhs, &b, depth + 1))
!eval_def_const(c, n->rhs, &b, depth + 1)) {
if ((n->lhs && n->lhs->type == ty_err) ||
(n->rhs && n->rhs->type == ty_err))
n->type = ty_err;
return 0;
}
if (fold_binop(n->op, a, b, out))
return 1;
if ((n->op == TK_SLASH || n->op == TK_PERCENT) && b == 0)
@@ -593,7 +613,10 @@ eval_def_const(Checker *c, Node *n, u64 *out, int depth)
/* fold_int_literal already covers unary-over-leaf; this
* arm catches unary over a resolved ref, e.g. `-A`. */
u64 v;
if (!eval_def_const(c, n->lhs, &v, depth + 1)) return 0;
if (!eval_def_const(c, n->lhs, &v, depth + 1)) {
if (n->lhs && n->lhs->type == ty_err) n->type = ty_err;
return 0;
}
switch (n->op) {
case TK_MINUS: *out = (u64)(-(i64)v); return 1;
case TK_TILDE: *out = ~v; return 1;
@@ -610,7 +633,10 @@ eval_def_const(Checker *c, Node *n, u64 *out, int depth)
* N_CAST arm in pass 2). Strip the cast, keeping the value;
* a narrowing cast that loses the value fails loud. */
u64 v;
if (!eval_def_const(c, n->lhs, &v, depth + 1)) return 0;
if (!eval_def_const(c, n->lhs, &v, depth + 1)) {
if (n->lhs && n->lhs->type == ty_err) n->type = ty_err;
return 0;
}
if (!def_cast_fits(n->type, v)) {
err(c, n->pos,
"def value: narrowing cast loses value");
@@ -620,6 +646,12 @@ eval_def_const(Checker *c, Node *n, u64 *out, int depth)
return 1;
}
case N_IDENT: {
if (lookup_bare_import_binding(c, n->str) != NULL) {
if (n->type != ty_err)
n->type = err(c, n->pos,
"use of package %s not in selector", n->str);
return 0;
}
Sym *s = lookup_visible(c, n->str);
if (s == NULL || s->kind != SK_DEF ||
s->decl == NULL || s->decl->rhs == NULL)
@@ -751,13 +783,16 @@ resolve_type(Checker *c, Node *n)
* sentinel; clet patches it from the initialiser. */
} else if (n->rhs->kind == N_INTLIT) {
len = n->rhs->uval;
} else if (reject_bare_import_values(c, n->rhs)) {
/* These leaves own their package-name diagnostics; do not
* replace them with a dependent constant-fold error. */
} else if (eval_def_const(c, n->rhs, &v, 0)) {
/* #141: a def-dimensioned `[MAX]u8`; fold the
* const-expr dimension (the same machinery #133's
* let-init fold uses). The err below stays for a
* genuinely non-const rhs. */
len = v;
} else {
} else if (n->rhs->type != ty_err) {
err(c, n->pos, "array length must be an integer literal");
}
Type *elem = resolve_type(c, n->lhs);
@@ -1053,6 +1088,8 @@ resolve_type(Checker *c, Node *n)
u64 val;
if (m->lhs == NULL) {
val = prev + 1;
} else if (reject_bare_import_values(c, m->lhs)) {
val = prev + 1;
} else if (!eval_enum_value(c, m->lhs, head, &val)) {
val = prev + 1;
}
@@ -1296,6 +1333,9 @@ cbinop(Checker *c, Node *n)
{
Type *l = cexpr(c, n->lhs);
Type *r = cexpr(c, n->rhs);
/* Both operands have now been checked. An invalid operand owns the
* diagnostic; do not add a dependent operator-type error. */
if (l == ty_err || r == ty_err) return ty_err;
/* #120 (B): a binop/compare with one f32 operand lowers an untyped-
* float peer to f32 — harec unifies both operands to the operand type
* (ref/harec/src/check.c:1347-1348). A comparison's result is bool, so
@@ -1360,6 +1400,7 @@ static Type *
cunop(Checker *c, Node *n)
{
Type *t = cexpr(c, n->lhs);
if (t == ty_err) return ty_err;
switch (n->op) {
case TK_MINUS: case TK_PLUS:
if (!type_isnum(t))
@@ -1442,6 +1483,9 @@ cexpr(Checker *c, Node *n)
if (n->str && n->str[0] == '\0')
return n->type = err(c, n->pos,
"`_` is only valid as a binding or discard lvalue");
if (lookup_bare_import_binding(c, n->str) != NULL)
return n->type = err(c, n->pos,
"use of package %s not in selector", n->str);
Sym *s = lookup_visible(c, n->str);
if (s == NULL)
return n->type = err(c, n->pos, "undefined: %s", n->str);
@@ -1689,6 +1733,22 @@ cexpr(Checker *c, Node *n)
type_name(c->a, base));
}
case N_CALL: {
/* Package qualifiers are not callable values, even when their local
* name has builtin spelling (len, size, alloc, ...). Diagnose before
* any builtin rewrite can erase the callee identifier. */
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str
&& lookup_bare_import_binding(c, n->lhs->str) != NULL) {
int type_args = strcmp(n->lhs->str, "size") == 0
|| strcmp(n->lhs->str, "align") == 0;
(void)cexpr(c, n->lhs);
for (Node *a = n->list; a; a = a->next) {
if (type_args)
(void)resolve_type(c, a);
else
(void)cexpr(c, a);
}
return n->type = ty_err;
}
/* Hare-style builtins: len(x), append(s, v), alloc(...).
* Recognised by name with no scope binding; we type-check
* the args ourselves and skip the normal call resolution. */
@@ -2117,12 +2177,18 @@ cexpr(Checker *c, Node *n)
* resolve_type for the synthetic-type-expr case. */
Type *t = NULL;
if (n->lhs && n->lhs->kind == N_IDENT) {
Sym *s = lookup_visible(c, n->lhs->str);
if (s == NULL || s->kind != SK_TYPE)
t = err(c, n->pos, "unknown struct type '%s'",
n->lhs->str);
else
t = s->type;
if (lookup_bare_import_binding(c, n->lhs->str) != NULL) {
t = err(c, n->lhs->pos,
"%s (package name) is not a type", n->lhs->str);
n->lhs->type = ty_err;
} else {
Sym *s = lookup_visible(c, n->lhs->str);
if (s == NULL || s->kind != SK_TYPE)
t = err(c, n->pos, "unknown struct type '%s'",
n->lhs->str);
else
t = s->type;
}
} else {
t = resolve_type(c, n->lhs);
}
@@ -3031,7 +3097,7 @@ src_imports(Node *file, const char *modtag, int source, const char *name)
{
if (file == NULL || name == NULL || name[0] == '\0') return 0;
for (Node *u = file->list; u; u = u->next) {
if (u->kind != N_USE || u->useblank
if (u->kind != N_USE || u->useblank || invalid_init_import(u)
|| u->sourceid != source) continue;
/* Skip self-imports: lib/fmt/fmt_test.ww carries `use fmt;`
* even though its module tag is also "fmt"; that directive
@@ -3062,8 +3128,11 @@ lookup_visible(Checker *c, const char *name)
* the builtin as fallback while checking direct imported declarations. */
Sym *builtin = NULL;
if (s != NULL) {
if (s->decl != NULL) return s;
builtin = s;
/* An SK_USE declaration belongs to one contributing source file,
* unlike ordinary package declarations. Defer it to the source-owned
* lookup below instead of leaking a sibling file's qualifier. */
if (s->decl != NULL && s->kind != SK_USE) return s;
if (s->decl == NULL) builtin = s;
}
/* Flat scope installation may coalesce equal qualifiers from distinct
* files. The source-owned binding is authoritative, but a bare mention is
@@ -3078,6 +3147,152 @@ lookup_visible(Checker *c, const char *name)
return builtin;
}
/* Return the effective package-name object for a bare identifier without
* marking the import used. The import edge and the visible symbol must both
* agree: the former enforces file ownership, while the latter lets an existing
* closer declaration win and retains the flat-scope use_alias bridge. */
static Sym *
lookup_bare_import_binding(Checker *c, const char *name)
{
if (c == NULL || name == NULL || name[0] == '\0') return NULL;
Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, name);
if (s == NULL || (s->kind != SK_USE && !s->use_alias)) return NULL;
if (src_imports(c->file, c->cur_mod, c->cur_source, name)) return s;
return NULL;
}
/* Walk a type subtree only far enough to classify package-name objects. This
* is deliberately not general type checking: the enum/array constant folders
* need the package diagnostic before they reduce an unsupported outer shape
* to a generic constant error. */
static int
reject_bare_import_types(Checker *c, Node *n)
{
if (n == NULL) return 0;
if (n->kind == N_IDENT || n->kind == N_TNAME) {
if (lookup_bare_import_binding(c, n->str) != NULL) {
if (n->type != ty_err)
n->type = err(c, n->pos,
"%s (package name) is not a type", n->str);
return 1;
}
/* Qualified type syntax is a legal selector use. Mark the exact
* source-owned binding even if the surrounding constant shape is
* independently invalid. */
if (n->kind == N_TNAME && n->str != NULL) {
const char *dot = strrchr(n->str, '.');
if (dot != NULL) {
char *head = astrndup(c->a, n->str,
(u64)(dot - n->str));
if (lookup_bare_import_binding(c, head) != NULL)
(void)use_path(c->file, c->cur_mod,
c->cur_source, head);
}
}
return 0;
}
int bad = 0;
if (n->kind == N_TARRAY) {
bad |= reject_bare_import_types(c, n->lhs);
bad |= reject_bare_import_values(c, n->rhs);
return bad;
}
if (n->kind == N_TENUM) {
bad |= reject_bare_import_types(c, n->lhs);
for (Node *p = n->list; p; p = p->next)
bad |= reject_bare_import_values(c, p->lhs);
return bad;
}
if (n->kind == N_TENUMMEMBER)
return reject_bare_import_values(c, n->lhs);
bad |= reject_bare_import_types(c, n->lhs);
bad |= reject_bare_import_types(c, n->rhs);
bad |= reject_bare_import_types(c, n->cond);
bad |= reject_bare_import_types(c, n->body);
bad |= reject_bare_import_types(c, n->els);
for (Node *p = n->list; p; p = p->next)
bad |= reject_bare_import_types(c, p);
return bad;
}
/* Reject every bare package-name value in a constant-expression subtree in
* source order. Direct package receivers remain legal selector qualifiers;
* cast operands, struct-literal heads, and size/align arguments retain their
* type context. */
static int
reject_bare_import_values(Checker *c, Node *n)
{
if (n == NULL) return 0;
switch (n->kind) {
case N_TPTR: case N_TSLICE: case N_TARRAY: case N_TFN:
case N_TSTRUCT: case N_TFIELD: case N_TCHAN:
case N_TTUPLE: case N_TTAGGED: case N_TBANG: case N_TENUM:
case N_TENUMMEMBER:
return reject_bare_import_types(c, n);
case N_IDENT: case N_TNAME:
if (lookup_bare_import_binding(c, n->str) != NULL) {
if (n->type != ty_err)
n->type = err(c, n->pos,
"use of package %s not in selector", n->str);
return 1;
}
if (n->kind == N_TNAME && n->str != NULL) {
const char *dot = strrchr(n->str, '.');
if (dot != NULL) {
char *head = astrndup(c->a, n->str,
(u64)(dot - n->str));
if (lookup_bare_import_binding(c, head) != NULL)
(void)use_path(c->file, c->cur_mod,
c->cur_source, head);
}
}
return 0;
case N_DOT: {
int bad = 0;
if (n->lhs && n->lhs->kind == N_IDENT
&& lookup_bare_import_binding(c, n->lhs->str) != NULL) {
(void)use_path(c->file, c->cur_mod, c->cur_source,
n->lhs->str);
} else {
bad |= reject_bare_import_values(c, n->lhs);
}
return bad;
}
case N_CAST: case N_TYPEASSERT: case N_TYPETEST: {
int bad = reject_bare_import_values(c, n->lhs);
bad |= reject_bare_import_types(c, n->rhs);
return bad;
}
case N_STRUCTLIT: {
int bad = reject_bare_import_types(c, n->lhs);
for (Node *p = n->list; p; p = p->next)
bad |= reject_bare_import_values(c, p->lhs);
return bad;
}
case N_CALL: {
int bad = reject_bare_import_values(c, n->lhs);
int typearg = n->lhs && n->lhs->kind == N_IDENT
&& (strcmp(n->lhs->str, "size") == 0
|| strcmp(n->lhs->str, "align") == 0);
for (Node *p = n->list; p; p = p->next)
bad |= typearg ? reject_bare_import_types(c, p)
: reject_bare_import_values(c, p);
return bad;
}
default:
break;
}
int bad = 0;
bad |= reject_bare_import_values(c, n->lhs);
bad |= reject_bare_import_values(c, n->rhs);
bad |= reject_bare_import_values(c, n->cond);
bad |= reject_bare_import_values(c, n->body);
bad |= reject_bare_import_values(c, n->els);
for (Node *p = n->list; p; p = p->next)
bad |= reject_bare_import_values(c, p);
return bad;
}
static Sym *
lookup_visible_type(Checker *c, const char *name)
{

View File

@@ -9545,6 +9545,221 @@ No format bump. Build workdir format remains `18`, test workdir format remains
`19`, semantic storage format remains `3`, and no test-result cache is
introduced.
### 11.48 Implemented selector-only effective import bindings
Pinned Go's applicable semantic rule is that an imported package's effective
name denotes a package-name object, not a value or type, and that object may be
used only to qualify a selector. A bare value occurrence is rejected as
`use of package BINDING not in selector`; a bare type occurrence is rejected as
`BINDING (package name) is not a type`. A bare occurrence does not mark the
import used, while a selector does. These rules and diagnostics are **behavior
directly implemented or asserted by pinned Go**.
The rule honestly applies within WW's local, dotted-import, manifest-free
model. WW already resolves every ordinary import to an effective default or
explicit file-local qualifier and already represents qualified value and type
selectors. Treating that existing qualifier as a package-name object closes a
checker hole without adding quoted, grouped, dot, generalized, network, module,
manifest, registry, or source-expression import machinery. Applying Go's
package-name-object rule to WW's already representable import binding is
**behavior derived from the pinned implementation**.
#### Pinned evidence and fact classification
The sole semantic authority is official Go 1.26.5 at
`c19862e5f8415b4f24b189d065ed739517c548ba`:
- [`ident` in `cmd/compile/internal/types2/typexpr.go`, lines
1860](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/typexpr.go#L18-L60)
resolves an identifier to its object and, in a type context, rejects an
object that is not a type name as `NAME (KIND) is not a type`;
- [the package-name value branch in that file, lines
8992](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/typexpr.go#L89-L92)
rejects a package name outside a selector as
`use of package NAME not in selector`;
- [`objectKind` in `cmd/compile/internal/types2/object.go`, lines
675680](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/object.go#L675-L680)
names a `PkgName` object's kind `package name`;
- [selector checking in `cmd/compile/internal/types2/call.go`, lines
672735](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/call.go#L672-L735),
especially lines 681691, admits a package name in the selector position and
marks that exact package-name object used;
- [import declaration in `cmd/compile/internal/types2/resolver.go`, lines
248340](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L248-L340)
creates the `PkgName` in file scope, and
[`unusedImports`, lines
706740](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L706-L740)
diagnoses each nonblank package-name object not marked by a selector;
- [compiler diagnostic sorting in `cmd/compile/internal/base/print.go`, lines
7092](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/base/print.go#L70-L92)
preserves stable source-position ordering; and
- [binary-expression checking in `cmd/compile/internal/types2/expr.go`, lines
788802](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/expr.go#L788-L802)
checks both operands before returning for either invalid result, so two bare
package-name operands retain two independently owned diagnostics.
Those source branches are **behavior directly implemented or asserted by
pinned Go**. Official assertions are
[`test/fixedbugs/issue11361.go`, lines
711](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/fixedbugs/issue11361.go#L7-L11),
which pairs an unused import with its bare package-value error;
[`src/internal/types/testdata/check/builtins0.go`, lines
613617](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/builtins0.go#L613-L617),
which rejects a package name passed as a value; and
[`src/internal/types/testdata/check/decls1.go`, lines
4573](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/decls1.go#L45-L73),
especially line 66, which asserts `math (package name) is not a type`.
From those pinned mechanisms it follows that a bare-only named import receives
both its unused-import diagnostic and its context-specific package-name
diagnostic; a legal selector removes only the unused diagnostic and never
excuses a distinct bare occurrence; an explicit alias supplies the displayed
name; and a sibling file without its own binding follows ordinary name lookup.
Those conclusions are **behavior derived from the pinned implementation**.
#### Fresh four-axis audit and direct pre-fix measurements
The fresh audit also confirmed multiple named source operands, explicit
`*_test.ww` build operands, shared top-level test-process state and fatal-abort
behavior, Go's regular-expression `-run` matching, and lexical shadowing of import
bindings as applicable but unselected differences. Ordinary directory
production selection was aligned. Grouped, quoted, and dot imports remain
inapplicable to WW's deliberately narrower syntax. Those classifications are
audit conclusions, not claims that this slice closes the unselected behavior.
Before this slice, the following observations were **directly measured WW
behavior**. With a dependency declared `package wire` and exporting a value and
type, both stages accepted legal `wire.Value` and `wire.Number` controls and
produced byte-identical 8,201-byte executables with SHA-256
`7fb0e95229aa22714d880070388ee5169697543076536267a8c709429cce0fcb`;
both ran with status 41. A bare-only value caused only the generated-unit
unused-import diagnostic in each stage. Once a legal selector marked the import
used, Cstage let a separate bare value reach the linker as
`undefined reference to 'wire'`, while WWstage let it reach code generation as
`cgident: unresolvable identifier (rule 7)`. An explicit alias reproduced the
same stage split using that alias. Reversing occurrence order or adding a
second bare occurrence did not restore checker ownership.
A bare type plus a legal selector was reported by both stages as
`unknown type 'wire'`, rather than the pinned package-name diagnostic. With no
selector, Cstage also retained the unused-import error, while WWstage emitted
only the unknown-type error. A sibling source that did not import `wire` let a
bare value reach the linker or code generator rather than following ordinary
undefined-name checking. Blank-import controls did follow ordinary undefined
lookup, missing targets retained missing-package precedence, and the valid
qualified/blank control retained artifact-byte and runtime parity. All cold
failed work directories examined were empty and no output was published.
The same checker escape was measured in same-package, external-test, honest
test-only, production-test, and imported-dependency paths: the test coordinator
reported `FAIL`, but Cstage failed in the linker while WWstage failed in code
generation. These status, stream, artifact, and cleanup results are **directly
measured WW behavior** from the pre-change Cstage/WWstage probes; they are not
attributed to Go.
The directly measured pre-fix result across the permanent axes was therefore:
- **Go-like build:** legal selector controls loaded, compiled, linked,
published, and ran byte-identically, but a bare package name escaped semantic
checking and failed at different backend phases;
- **Go-like test:** every applicable generated test source role reached the
ordinary package failure result, but its checker diagnostic and failure phase
differed between stages;
- **Go-like package:** effective qualifiers were already source-local during
legal selector resolution, but sibling and bare lookup could escape that
source-local package-name boundary; and
- **Go-like import:** unused accounting recognized some selector uses, but a
bare import binding was not consistently classified as a non-value,
non-type package-name object.
#### Ownership and complete four-axis contract
The true semantic owner is identifier and type-name resolution in the Cstage
and WWstage semantic checkers. The checker must distinguish a package-name
object belonging to the current source from ordinary lexical or package-scope
objects before generic undefined, type, code-generation, or link recovery. A
selector remains the only construct that consumes such an object and marks its
import used. A bare type name must not accidentally mark the import used. Both
implementations must produce the same normalized diagnostics at the same source
positions and retain ordinary undefined or unknown-type behavior when the
current file has no binding.
- **Go-like build:** every selected source is rejected during semantic checking
before compiler output, assembly, archive, link, or installation when it
contains a bare package-name object. Legal selectors retain the existing
build graph, actions, artifact bytes, runtime result, and publication rules.
- **Go-like test:** production, same-package, external-test, and honest
test-only sources use the same checker rule. Rejection precedes test-process
construction and execution; discovery, filters, descriptors, result
accounting, fatal/skip behavior, timeouts, and process cleanup do not change.
- **Go-like package:** the binding remains owned by exactly its source file.
Declared package names, canonical package and variant identity, selected
source membership, exported declarations, initialization, and symbol naming
do not change.
- **Go-like import:** default and explicit named bindings become consistently
selector-only package-name objects. Selectors alone satisfy unused-import
accounting. Blank imports create no binding, rejected effective `init`
imports install none, and missing-target resolution keeps precedence.
#### Lifecycle, parity, proof, and formats
Loading and source eligibility, filename ordering, package-clause validation,
canonical dotted resolution, contextual local/vendor expansion, source-role
classification, and graph-edge construction remain unchanged. The diagnostic
is a property of an already resolved file-local binding; physical directories
remain loader and presentation metadata and never acquire package, import,
graph, action, artifact, symbol, `.wwi`, publication, or persistence identity.
Semantic rejection occurs within the compiler action before code generation,
so no assembler, archiver, linker, test runtime, or installer may run for the
invalid action.
Diagnostic source order and precedence must remain stable: missing-package,
invalid effective-`init`, blank-no-binding, import collision, and ordinary
undefined-name paths keep their established owners; a valid binding receives
the exact package-name diagnostic and, unless a selector separately used it,
its independent unused-import diagnostic. Repeated invalid bare occurrences
are diagnosed independently.
Cold rejection must publish no executable, archive, interface, retained test
binary, or partial semantic action. Warm rejection must preserve the previous
public product and complete committed work generation byte-for-byte. A later
valid request must recover through existing invalidation and reuse rules.
Neither rejected source text nor its physical directory may create a new
identity or persistence key. Valid controls must retain comparable Cstage and
WWstage diagnostic and artifact-byte parity.
The invalid consumer's compiler action starts and rejects during semantic
checking, before its code-generation or downstream producer boundary. Already
valid dependency producers may also have completed before that rejection.
Assembler, archiver, linker, installer, test-runtime, and runtime-failure paths
for the invalid consumer are therefore unreachable. Existing producer/runtime
failure, rollback, publication transactions, parallel-product isolation,
concurrent-request locking, cancellation, interruption escalation, child
ownership, and descendant cleanup remain unchanged for other actions. Rejection
must remove request-owned stages and leave no active `.new`, `.install`,
`.wwtxn.*`, adjacent `.sepwork`, capture, result, scratch, or tool-stage
transaction residue.
The WW-native `bare_import_bindings_require_selectors` observer is required to
prove exact cross-stage status and normalized diagnostic parity for bare value
and type contexts, explicit aliases, selector order, repeated occurrences,
unused accounting, sibling-file isolation, blank/`init`/missing controls,
ordinary and imported builds, builtin-spelled package callees (`len`, `size`,
and `align`), nested value/type contexts, and every applicable test source role.
In particular, parser-shaped `size`/`align` type arguments must keep type
checking after the callee is classified as a package-name object and must never
escape through generic internal-expression recovery. The observer must also
prove valid artifact-byte/runtime parity, cold empty rollback, warm prior state
preservation, invalidation and recovery, and residue cleanup. Broader
producer/runtime failure, concurrency, interruption, and descendant-process
behavior retain their existing owners because this slice adds no such
boundary. These are proof requirements; validation and final post-change byte
measurements are recorded only after they are run.
No format bump. Build workdir format remains `18`, test workdir format remains
`19`, semantic storage format remains `3`, and no test-result cache is
introduced.
## 12. Candidate architectures and hard-gate decision
Five candidates were developed as coherent systems, not as feature bins.

View File

@@ -323,11 +323,25 @@ ImportPath = ident { "." ident } .
package declares `package wire;`, the importing file sees its exported names
as `wire.Name`; `codec.Name` is not an additional binding. An explicit alias
replaces only that visible qualifier: `import stable acme.codec;` exposes
`stable.Name`, not `wire.Name` or `codec.Name`. Both kinds of binding are
scoped to that source file. A sibling file must declare its own import.
`stable.Name`, not `wire.Name` or `codec.Name`. Each effective default or
explicit named binding is a file-local package-name object usable only as
the left qualifier of a selector. A bare occurrence in a value context is
rejected as `use of package BINDING not in selector`; a bare occurrence in
a type context is rejected as `BINDING (package name) is not a type`. Such a
rejected bare occurrence does not count as use of the import for
unused-import accounting. A legal selector does count as use, but does not
excuse any separate bare occurrence. Both kinds of named binding are scoped
to that source file. A sibling file must declare its own import; without one,
its otherwise equal spelling follows ordinary undefined-name or unknown-type
lookup rather than package-name diagnostics. Builtin spelling does not alter
the object: an import bound as `len`, `size`, `align`, or another builtin name
remains a selector-only package-name object.
Neither form exposes an imported declaration as a bare `Name`; ordinary
unqualified lookup remains limited to lexical, builtin, and same-package
declarations.
declarations. A blank import creates no package-name object, an effective
`init` import is rejected before installing one, and a missing target fails
during import resolution, so none of those cases acquires the bare-package
diagnostics or satisfies a named import's unused accounting.
- An import whose effective file-local qualifier is `init` is invalid. This
includes both `import init acme.codec;` and an unaliased import whose target
declares `package init;`. Each resolved occurrence is rejected at its first

View File

@@ -583,6 +583,35 @@ exact-argv, command, and persistent-workdir observers, the package suite proves
archive-only link argv and exact warm/rejection-state behavior without
duplicating those broader mechanisms in this observer.
The focused dual-stage `bare_import_bindings_require_selectors` observer is the
acceptance owner for the rule that an effective default or explicit import
binding is a file-local package-name object, not a value or type. Its generated
matrix must prove exact `use of package BINDING not in selector` value
diagnostics and `BINDING (package name) is not a type` type diagnostics; an
unused diagnostic for every named binding that has only bare occurrences; and
the absence of that unused diagnostic once a legal selector independently uses
the binding. It must cover occurrence order, repeated bare occurrences,
explicit aliases, builtin-spelled bindings including `len`, `size`, and `align`,
qualified value and type controls, nested value and type contexts, sibling-file
isolation, blank imports, rejected `init` bindings, and missing-target
precedence. Type-shaped arguments parsed for `size` and `align` must retain type
checking when either spelling is instead an import binding; neither stage may
fall into an internal expression-kind diagnostic.
The same observer must exercise ordinary root and imported builds plus
production, same-package test, external-test, and honest test-only source
roles. Cstage and WWstage must agree on status and normalized stdout/stderr,
including diagnostic order and source position, and valid controls must produce
byte-identical comparable artifacts and equal runtime results. Its lifecycle
matrix must include cold rejection with no public or retained product, warm
preservation of every prior committed work-file and public byte, imported
dependency failure, invalidation followed by rejection, subsequent valid
recovery, and exact transaction-residue checks. Legal selector controls retain
ordinary graph/action, compilation, linking, publication, persistence, and
reuse behavior. Producer/runtime failure, concurrency, interruption, process
topology, and descendant cleanup remain with their established observers
because this checker-owned rejection creates no producer or runtime boundary.
The focused dual-stage `effective_init_imports_never_enter_binding_recovery`
observer is
the acceptance owner for imports whose effective file-local qualifier is

View File

@@ -237,6 +237,7 @@ fn srcimports(file: *syntax.node, modtag: str, source: i32, name: str) bool = {
let u: *syntax.node = file.list;
for (u != nil) {
if (u.kind == syntax.nkind.N_USE && u.useblank == 0
&& !invalidinitimport(u)
&& u.sourceid == source) {
// Skip self-imports: lib/fmt/fmt_test.ww carries
// `use fmt;` while its module tag is also "fmt".
@@ -267,8 +268,10 @@ fn lookupvisible(c: *checker, name: str) *syntax.sym = {
let found: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, name);
let builtin: *syntax.sym = nil;
if (found != nil) {
if (found.decl != nil) { return found; };
builtin = found;
// A pure SK_USE belongs to one source file. Defer it to the
// source-owned path below instead of leaking a sibling qualifier.
if (found.decl != nil && found.skind != syntax.skind.SK_USE) { return found; };
if (found.decl == nil) { builtin = found; };
};
// Flat scope installation may coalesce equal qualifiers from distinct
// files. A bare mention only locates the marker; the DOT resolution owns
@@ -289,6 +292,45 @@ fn lookupvisible(c: *checker, name: str) *syntax.sym = {
return builtin;
};
// Return the source-local package-name object for a bare identifier without
// marking the import used. A closer lexical value wins; a coexisting SK_USE or
// promoted use_alias retains package-qualifier identity in the flat scope.
fn lookupbareimportbinding(c: *checker, name: str) *syntax.sym = {
if (name.len == 0) { return nil; };
let found: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, name);
if (found == nil) { return nil; };
if (found.skind != syntax.skind.SK_USE && found.use_alias == 0i32) {
let use: *syntax.sym = syntax.scopelookupuselocal(found.scope, name);
if (use != nil) { found = use; };
};
if (found.skind == syntax.skind.SK_USE || found.use_alias != 0i32) {
if (srcimports(c.file, c.curmod, c.cursource, name)) { return found; };
};
return nil;
};
fn bareimportvalueerr(c: *checker, n: *syntax.node) void = {
if (n.type_ == c.tc.tyerr: *void) { return; };
cerr(n.file); cerr(":");
cerr(strconv.i32tos(n.line, strconv.base.DEC)); cerr(":");
cerr(strconv.i32tos(n.col, strconv.base.DEC));
cerr(": error: use of package "); cerr(n.str);
cerr(" not in selector\n");
c.errs += 1;
n.type_ = c.tc.tyerr: *void;
};
fn bareimporttypeerr(c: *checker, n: *syntax.node) void = {
if (n.type_ == c.tc.tyerr: *void) { return; };
cerr(n.file); cerr(":");
cerr(strconv.i32tos(n.line, strconv.base.DEC)); cerr(":");
cerr(strconv.i32tos(n.col, strconv.base.DEC));
cerr(": error: "); cerr(n.str);
cerr(" (package name) is not a type\n");
c.errs += 1;
n.type_ = c.tc.tyerr: *void;
};
fn lookupvisibletype(c: *checker, name: str) *syntax.sym = {
// C resolves intrinsic type names before consulting package symbols.
// Select the empty-module seed here so an imported interface cannot
@@ -672,6 +714,11 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
if (c.sepmode != 0 && n.type_ != nil) { c.nresolved += 1; return; };
let nm: str = n.str;
if (nm.len > 0) {
if (lookupbareimportbinding(c, nm) != nil) {
bareimporttypeerr(c, n);
c.nresolved += 1;
return;
};
let s: *syntax.sym = lookupvisibletype(c, nm);
let builtin: bool = builtintypename(nm);
// `pkg.Type` — strip the last dot prefix and look up
@@ -1011,13 +1058,35 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
if (k == syntax.nkind.N_DOT) {
// Walk only the base; the .field name is a member, not a
// free identifier.
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
if (n.lhs != nil) {
if (n.lhs.kind == syntax.nkind.N_IDENT
&& lookupbareimportbinding(c, n.lhs.str) != nil) {
// A package receiver is a lookup target, not a value expression.
c.nresolved += 1;
} else { resolvewalk(c, n.lhs); };
};
// A.6.0: branch returns early; stamp here so the post-walk
// dispatch below sees N_DOT covered.
let _t: *syntax.node = exprtype(c, n, nil);
return;
};
if (k == syntax.nkind.N_STRUCTLIT) {
// The literal head is a type designator. A package binding there gets
// the type diagnostic, never the ordinary bare-value diagnostic.
if (n.lhs != nil) {
if (n.lhs.kind == syntax.nkind.N_IDENT
&& lookupbareimportbinding(c, n.lhs.str) != nil) {
bareimporttypeerr(c, n.lhs);
c.nresolved += 1;
} else { resolvewalk(c, n.lhs); };
};
let sf: *syntax.node = n.list;
for (sf != nil) { resolvewalk(c, sf); sf = sf.next; };
let _st: *syntax.node = exprtype(c, n, nil);
return;
};
if (k == syntax.nkind.N_FIELD) {
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
return;
@@ -2098,8 +2167,18 @@ fn evaldefconst(c: *checker, n: *syntax.node, out: *u64, depth: i32) bool = {
if (k == syntax.nkind.N_BIN) {
let a: u64 = 0u64;
let b: u64 = 0u64;
if (!evaldefconst(c, n.lhs, &a, depth + 1)) { return false; };
if (!evaldefconst(c, n.rhs, &b, depth + 1)) { return false; };
if (!evaldefconst(c, n.lhs, &a, depth + 1)) {
if (n.lhs != nil && n.lhs.type_ == c.tc.tyerr: *void) {
n.type_ = c.tc.tyerr: *void;
};
return false;
};
if (!evaldefconst(c, n.rhs, &b, depth + 1)) {
if (n.rhs != nil && n.rhs.type_ == c.tc.tyerr: *void) {
n.type_ = c.tc.tyerr: *void;
};
return false;
};
if (foldbinop(n.op, a, b, out)) { return true; };
if ((n.op == syntax.tkind.TK_SLASH || n.op == syntax.tkind.TK_PERCENT) && b == 0u64) {
deffolderr(c, n, "def value: division by zero");
@@ -2112,7 +2191,12 @@ fn evaldefconst(c: *checker, n: *syntax.node, out: *u64, depth: i32) bool = {
// foldintliteral already covers unary-over-leaf; this arm
// catches unary over a resolved ref, e.g. `-A`.
let v: u64 = 0u64;
if (!evaldefconst(c, n.lhs, &v, depth + 1)) { return false; };
if (!evaldefconst(c, n.lhs, &v, depth + 1)) {
if (n.lhs != nil && n.lhs.type_ == c.tc.tyerr: *void) {
n.type_ = c.tc.tyerr: *void;
};
return false;
};
if (n.op == syntax.tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; };
if (n.op == syntax.tkind.TK_TILDE) { *out = ~v; return true; };
if (n.op == syntax.tkind.TK_PLUS) { *out = v; return true; };
@@ -2124,7 +2208,12 @@ fn evaldefconst(c: *checker, n: *syntax.node, out: *u64, depth: i32) bool = {
// exprtype's N_CAST arm during resolvewalk). Strip the cast
// keeping the value; a narrowing cast that loses it fails loud.
let v: u64 = 0u64;
if (!evaldefconst(c, n.lhs, &v, depth + 1)) { return false; };
if (!evaldefconst(c, n.lhs, &v, depth + 1)) {
if (n.lhs != nil && n.lhs.type_ == c.tc.tyerr: *void) {
n.type_ = c.tc.tyerr: *void;
};
return false;
};
let t: *syntax.tinfo = (n.type_): *syntax.tinfo;
if (!defcastfits(t, v)) {
deffolderr(c, n, "def value: narrowing cast loses value");
@@ -2134,6 +2223,10 @@ fn evaldefconst(c: *checker, n: *syntax.node, out: *u64, depth: i32) bool = {
return true;
};
if (k == syntax.nkind.N_IDENT) {
if (lookupbareimportbinding(c, n.str) != nil) {
bareimportvalueerr(c, n);
return false;
};
let s: *syntax.sym = lookupvisible(c, n.str);
if (s == nil) { return false; };
if (s.skind != syntax.skind.SK_DEF) { return false; };
@@ -2419,6 +2512,130 @@ fn validatestructfields(c: *checker, n: *syntax.node) void = {
// resolvewalk's eager type-decl dispatch, sibling to validatestructfields
// (rule-10 symmetric with cstage's once-per-resolve_type), not from the
// per-query size/align arms.
fn bareimporttypenode(k: syntax.nkind) bool = {
return k == syntax.nkind.N_TPTR || k == syntax.nkind.N_TSLICE
|| k == syntax.nkind.N_TARRAY
|| k == syntax.nkind.N_TFN || k == syntax.nkind.N_TSTRUCT
|| k == syntax.nkind.N_TFIELD || k == syntax.nkind.N_TCHAN
|| k == syntax.nkind.N_TTUPLE || k == syntax.nkind.N_TTAGGED
|| k == syntax.nkind.N_TBANG || k == syntax.nkind.N_TENUM
|| k == syntax.nkind.N_TENUMMEMBER || k == syntax.nkind.N_TPARAM;
};
fn rejectbareimporttypes(c: *checker, n: *syntax.node) bool = {
if (n == nil) { return false; };
if (n.kind == syntax.nkind.N_IDENT || n.kind == syntax.nkind.N_TNAME) {
if (lookupbareimportbinding(c, n.str) != nil) {
bareimporttypeerr(c, n);
return true;
};
// A qualified type is a legal selector use even when its surrounding
// constant-expression shape is independently invalid.
if (n.kind == syntax.nkind.N_TNAME && strings.contains(n.str, ".")) {
let (head, leaf) = strings.rcut(n.str, ".");
if (lookupbareimportbinding(c, head) != nil) {
let marked: str = usepathfor(c.file, c.curmod, c.cursource, head);
};
};
return false;
};
let bad: bool = false;
if (n.kind == syntax.nkind.N_TARRAY) {
if (rejectbareimporttypes(c, n.lhs)) { bad = true; };
if (rejectbareimportvalues(c, n.rhs)) { bad = true; };
return bad;
};
if (n.kind == syntax.nkind.N_TENUM) {
if (rejectbareimporttypes(c, n.lhs)) { bad = true; };
let m: *syntax.node = n.list;
for (m != nil) {
if (rejectbareimportvalues(c, m.lhs)) { bad = true; };
m = m.next;
};
return bad;
};
if (n.kind == syntax.nkind.N_TENUMMEMBER) {
return rejectbareimportvalues(c, n.lhs);
};
if (rejectbareimporttypes(c, n.lhs)) { bad = true; };
if (rejectbareimporttypes(c, n.rhs)) { bad = true; };
if (rejectbareimporttypes(c, n.cond)) { bad = true; };
if (rejectbareimporttypes(c, n.body)) { bad = true; };
if (rejectbareimporttypes(c, n.els)) { bad = true; };
let p: *syntax.node = n.list;
for (p != nil) {
if (rejectbareimporttypes(c, p)) { bad = true; };
p = p.next;
};
return bad;
};
fn rejectbareimportvalues(c: *checker, n: *syntax.node) bool = {
if (n == nil) { return false; };
if (bareimporttypenode(n.kind)) { return rejectbareimporttypes(c, n); };
if (n.kind == syntax.nkind.N_IDENT || n.kind == syntax.nkind.N_TNAME) {
if (lookupbareimportbinding(c, n.str) != nil) {
bareimportvalueerr(c, n);
return true;
};
if (n.kind == syntax.nkind.N_TNAME && strings.contains(n.str, ".")) {
let (head, leaf) = strings.rcut(n.str, ".");
if (lookupbareimportbinding(c, head) != nil) {
let marked: str = usepathfor(c.file, c.curmod, c.cursource, head);
};
};
return false;
};
if (n.kind == syntax.nkind.N_DOT) {
if (n.lhs != nil && n.lhs.kind == syntax.nkind.N_IDENT
&& lookupbareimportbinding(c, n.lhs.str) != nil) {
let marked: str = usepathfor(c.file, c.curmod, c.cursource, n.lhs.str);
return false;
};
return rejectbareimportvalues(c, n.lhs);
};
if (n.kind == syntax.nkind.N_CAST || n.kind == syntax.nkind.N_TYPEASSERT
|| n.kind == syntax.nkind.N_TYPETEST) {
let bad: bool = rejectbareimportvalues(c, n.lhs);
if (rejectbareimporttypes(c, n.rhs)) { bad = true; };
return bad;
};
if (n.kind == syntax.nkind.N_STRUCTLIT) {
let bad: bool = rejectbareimporttypes(c, n.lhs);
let f: *syntax.node = n.list;
for (f != nil) {
if (rejectbareimportvalues(c, f.lhs)) { bad = true; };
f = f.next;
};
return bad;
};
if (n.kind == syntax.nkind.N_CALL) {
let bad: bool = rejectbareimportvalues(c, n.lhs);
let typearg: bool = n.lhs != nil && n.lhs.kind == syntax.nkind.N_IDENT
&& (syntax.streq(n.lhs.str, "size") || syntax.streq(n.lhs.str, "align"));
let a: *syntax.node = n.list;
for (a != nil) {
if (typearg) {
if (rejectbareimporttypes(c, a)) { bad = true; };
} else { if (rejectbareimportvalues(c, a)) { bad = true; }; };
a = a.next;
};
return bad;
};
let bad: bool = false;
if (rejectbareimportvalues(c, n.lhs)) { bad = true; };
if (rejectbareimportvalues(c, n.rhs)) { bad = true; };
if (rejectbareimportvalues(c, n.cond)) { bad = true; };
if (rejectbareimportvalues(c, n.body)) { bad = true; };
if (rejectbareimportvalues(c, n.els)) { bad = true; };
let p: *syntax.node = n.list;
for (p != nil) {
if (rejectbareimportvalues(c, p)) { bad = true; };
p = p.next;
};
return bad;
};
fn validateenummembers(c: *checker, n: *syntax.node) void = {
// storage type: cstage resolves n->lhs then gates type_isint
// (check.c:1004-1009); default storage is i32, always integer.
@@ -2464,7 +2681,8 @@ fn validateenummembers(c: *checker, n: *syntax.node) void = {
// `until = m` enforces harec's forward-only sibling-ref discipline.
if (m.lhs != nil) {
let v: u64 = 0u64;
if (!enumvalfold(n, m, m.lhs, &v)) {
let barebad: bool = rejectbareimportvalues(c, m.lhs);
if (!barebad && !enumvalfold(n, m, m.lhs, &v)) {
cerr(m.file);
cerr(": error: enum value must be a constant integer expression\n");
c.errs += 1;
@@ -2475,14 +2693,20 @@ fn validateenummembers(c: *checker, n: *syntax.node) void = {
};
// stampnilexpr — stamp nil-typed nodes in a constant expr subtree to
// `ti`. lhs/rhs cover the enum constexpr grammar enumvalfold accepts
// (literals, unary, binary, sibling backref); non-nil nodes keep the
// `ti`. The specialised fold accepts lhs/rhs, but invalid outer shapes still
// pass through the checker and must not leak nil-typed descendants into the
// final invariant walk. Non-nil nodes, including package-name errors, keep the
// type exprtype already derived.
fn stampnilexpr(n: *syntax.node, ti: *syntax.tinfo) void = {
if (n == nil) { return; };
if (n.type_ == nil) { n.type_ = ti: *void; };
stampnilexpr(n.lhs, ti);
stampnilexpr(n.rhs, ti);
stampnilexpr(n.cond, ti);
stampnilexpr(n.body, ti);
stampnilexpr(n.els, ti);
let p: *syntax.node = n.list;
for (p != nil) { stampnilexpr(p, ti); p = p.next; };
};
// #61 A.5 helper: per-element slot size when `pt` appears inside a
@@ -2624,6 +2848,13 @@ fn tinfofornode(c: *checker, n: *syntax.node) *syntax.tinfo = {
switch (k) {
case syntax.nkind.N_TNAME:
let nm: str = n.str;
// Synthetic mktname nodes have no file. Only parsed source syntax can
// denote a bare package binding; internal primitive/type stamps must not.
if (n.file.len != 0 && lookupbareimportbinding(c, nm) != nil) {
bareimporttypeerr(c, n);
syntax.tinfocachebind(c.tc, n, c.tc.tyerr);
return c.tc.tyerr;
};
if (syntax.streq(nm, "void")) { r = c.tc.tyvoid; };
if (syntax.streq(nm, "bool")) { r = c.tc.tybool; };
if (syntax.streq(nm, "rune")) { r = c.tc.tyrune; };
@@ -2760,13 +2991,16 @@ fn tinfofornode(c: *checker, n: *syntax.node) *syntax.tinfo = {
elen = n.rhs.uval;
} else {
let v: u64 = 0u64;
if (evaldefconst(c, n.rhs, &v, 0)) {
let barebad: bool = rejectbareimportvalues(c, n.rhs);
if (!barebad && evaldefconst(c, n.rhs, &v, 0)) {
elen = v;
} else {
cerr(n.file); cerr(": ");
cerr("error: array length must be an integer literal\n");
c.errs += 1;
};
} else { if (!barebad) {
if (n.rhs.type_ != c.tc.tyerr: *void) {
cerr(n.file); cerr(": ");
cerr("error: array length must be an integer literal\n");
c.errs += 1;
};
}; };
};
};
let sub: *syntax.tinfo = tinfofornode(c, n.lhs);
@@ -3352,6 +3586,13 @@ fn binoptype(c: *checker, e: *syntax.node) *syntax.node = {
let op: syntax.tkind = e.op;
let ltn: *syntax.node = exprtype(c, e.lhs, nil);
let rtn: *syntax.node = exprtype(c, e.rhs, nil);
// Both operands were checked. Preserve their causal error on the parent and
// do not manufacture an operator-type recovery diagnostic or nil stamp.
if ((e.lhs != nil && e.lhs.type_ == c.tc.tyerr: *void)
|| (e.rhs != nil && e.rhs.type_ == c.tc.tyerr: *void)) {
e.type_ = c.tc.tyerr: *void;
return nil;
};
// #120 (B): a binop/compare with one f32 operand lowers an untyped-
// float peer to f32 — harec unifies both operands to the operand type
// (ref/harec/src/check.c:1347-1348). A comparison's result is bool, so
@@ -3485,6 +3726,10 @@ fn binoptype(c: *checker, e: *syntax.node) *syntax.node = {
fn unoptype(c: *checker, e: *syntax.node) *syntax.node = {
let op: syntax.tkind = e.op;
let opt: *syntax.node = exprtype(c, e.lhs, nil);
if (e.lhs != nil && e.lhs.type_ == c.tc.tyerr: *void) {
e.type_ = c.tc.tyerr: *void;
return nil;
};
// #38/F2 (review item 5): unop operand-kind gates the wwstage checker
// elided. Mirror cstage cunop (cmd/wcc/check.c:1224-1238): unary +/-
// want a numeric operand, ~ wants an integer, ! wants a bool. A nil
@@ -3820,6 +4065,10 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
// resolvewalk visits the dot receiver before its parent. Preserve the
// first causal undefined error just as cstage's cached cexpr does.
if (e.type_ == c.tc.tyerr: *void) { return nil; };
if (lookupbareimportbinding(c, e.str) != nil) {
bareimportvalueerr(c, e);
return nil;
};
// #55: bare-leaf value-ident must prefer curmod. Flat-scope
// scopelookup bucket-walks and can bind a same-leaf symbol from
// the wrong module under a foreign curmod, dragging its decl's
@@ -3848,6 +4097,14 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
return nil;
};
if (s.decl == nil) { return nil; };
// Propagate an errored inferred initializer through its binding. Without
// this, a later `bad.field` is left nil-typed and asserttyped obscures the
// causal checker diagnostic emitted for the initializer.
if (s.decl.kind == syntax.nkind.N_LET && s.decl.lhs == nil
&& s.decl.rhs != nil && s.decl.rhs.type_ == c.tc.tyerr: *void) {
e.type_ = c.tc.tyerr: *void;
return nil;
};
e.refdecl = s.decl;
// #34: a bare fn-name rvalue types as its FN TYPE, not its return
// type. decl.lhs is the RETURN type for an N_FNDECL, so synthesize
@@ -3906,6 +4163,7 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
};
if (k == syntax.nkind.N_BIN) {
let tn: *syntax.node = binoptype(c, e);
if (e.type_ == c.tc.tyerr: *void) { return nil; };
// #59.9: checkisas pre-stamps an enum OR-fold (`(m.A|m.B) as
// u32`) TY_ENUM and folds the member N_DOTs to int literals;
// this post-order revisit re-derives from those now-untyped
@@ -3927,6 +4185,7 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
};
if (k == syntax.nkind.N_UN) {
let tn: *syntax.node = unoptype(c, e);
if (e.type_ == c.tc.tyerr: *void) { return nil; };
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
@@ -3945,6 +4204,14 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
if (e.type_ == c.tc.tyerr: *void) { return nil; };
let callee: *syntax.node = e.lhs;
if (callee == nil) { return nil; };
// Package qualifiers with builtin spelling remain package objects. Reject
// before alloc/size/len/etc. can rewrite away the callee identifier.
if (callee.kind == syntax.nkind.N_IDENT
&& lookupbareimportbinding(c, callee.str) != nil) {
bareimportvalueerr(c, callee);
e.type_ = c.tc.tyerr: *void;
return nil;
};
// A module-qualified leaf may already have been rejected while the
// N_DOT callee was checked on an earlier resolve walk. cstage caches
// that failure on the call; mirror its once-only diagnostic here rather
@@ -4534,6 +4801,9 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
e.type_ = c.tc.tyerr: *void;
return nil;
};
// Raw unresolved external selectors retain selector semantics;
// never reinterpret the package receiver as a bare value below.
if (fs == nil && ms.skind == syntax.skind.SK_USE) { return nil; };
};
// Fold case 2 inner: bare `EnumT.MEMBER` where EnumT
// is an SK_TYPE in the flat scope. Mirror cstage
@@ -4683,6 +4953,11 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
// style anonymous struct lit we don't yet parse — bail.
if (e.lhs == nil) { return nil; };
if (e.lhs.kind == syntax.nkind.N_IDENT) {
if (lookupbareimportbinding(c, e.lhs.str) != nil) {
bareimporttypeerr(c, e.lhs);
e.type_ = c.tc.tyerr: *void;
return nil;
};
let ms: *syntax.sym = lookupvisible(c, e.lhs.str);
if (ms != nil) { if (ms.skind == syntax.skind.SK_TYPE) { if (ms.decl != nil) {
let tn: *syntax.node = ms.decl.lhs;
@@ -7578,8 +7853,10 @@ fn markimportusesnode(c: *checker, n: *syntax.node, owner: str,
source: i32) void = {
if (n == nil) { return; };
if (n.kind == syntax.nkind.N_TNAME) {
let (head, leaf) = strings.rcut(n.str, ".");
if (head.len > 0) {
// strings.rcut returns the whole input as its head on a miss. Require
// a real qualifier separator so a bare package TNAME never counts as use.
if (strings.contains(n.str, ".")) {
let (head, leaf) = strings.rcut(n.str, ".");
findusepath(c.file, owner, source, head, true);
};
} else { if (n.kind == syntax.nkind.N_DOT && n.lhs != nil

View File

@@ -13267,7 +13267,7 @@ fn runtimepath(relative: str) str = {
"cmd.duplicate", "cmd.collision", "cmd.crosscollision",
"cmd.programuser"];
let rejectneedles: []str = ["undefined: codec",
"package 'wire' is not directly imported",
"undefined: wire",
"same redeclared in this block",
"same already declared through import of package same",
"same already declared through import of package same",
@@ -13297,6 +13297,7 @@ fn runtimepath(relative: str) str = {
"$WORK/cmd.collision.unit.new:3:1: error: \"scope.one\" imported as same and not used\n",
"$WORK/cmd.collision.unit.new:4:1: error: same already declared through import of package same (\"scope.one\")\n",
"\t$WORK/cmd.collision.unit.new:3:1: other declaration of same\n",
"$WORK/cmd.collision.unit.new:5:26: error: use of package same not in selector\n",
"ww: w6c failed for cmd.collision\n")));
};
if (si == 0) { diagnosticrefs[rj] = strings.dup(normalized); }
@@ -13681,7 +13682,7 @@ fn runtimepath(relative: str) str = {
"", "", "", "", "", ""];
let needles: []str = ["undefined: value", "unknown type 'Thing'",
"undefined: DEFINED", "undefined: CONSTANT", "undefined: VARIABLE",
"unknown type 'wire.Thing'", "package 'stable' is not directly imported",
"unknown type 'wire.Thing'", "undefined: stable",
"wire redeclared in this block", "stable redeclared in this block",
"wire redeclared in this block",
"stable already declared through import of package stable",
@@ -13728,6 +13729,7 @@ fn runtimepath(relative: str) str = {
"$WORK/cmd.declcollision.unit.new:3:1: error: \"pkg.one\" imported as stable and not used\n",
"$WORK/cmd.declcollision.unit.new:4:1: error: stable already declared through import of package stable (\"pkg.one\")\n",
"\t$WORK/cmd.declcollision.unit.new:3:1: other declaration of stable\n",
"$WORK/cmd.declcollision.unit.new:5:26: error: use of package stable not in selector\n",
"ww: w6c failed for cmd.declcollision\n"),
strings.concat(
"$WORK/cmd.unusedalias.unit.new:3:1: error: \"pkg.one\" imported as stable and not used\n",
@@ -19041,3 +19043,703 @@ fn runtimepath(relative: str) str = {
&& !directoryhasfragment(root, ".install"));
clean(root);
};
// An effective import name is a file-local package qualifier. It may qualify
// selectors, but it is neither a value nor a type by itself. Keep that rule at
// semantic checking, before a malformed binding can leak into code generation
// or linking, and preserve the ordinary undefined boundary in sibling files and
// after blank imports.
@test fn bare_import_bindings_require_selectors() void = {
let root: str = fresh();
let source: str = strings.concat(root, "/source");
let dependency: str = strings.concat(source, "/dep/wire");
let sizedependency: str = strings.concat(source, "/dep/size");
let cases: []str = ["bareonly", "aliasonly", "selectorbare",
"bareselector", "multiple", "typeonly", "aliastype", "typeselector",
"builtinalias", "builtincallee", "arraydim", "enumvalue",
"nestedconst", "nestedarg", "structhead", "structfieldtype",
"casttype", "builtincall", "sibling", "siblingtype", "transitive",
"blank", "initcase", "missing", "packagecalleeprim",
"packagecalleetype", "packagecalleealign", "arraypackagecallee",
"enumpackagecalleealign"];
let ci: i32 = 0;
mkdirall(dependency);
mkdirall(sizedependency);
for (ci < cases.len) {
mkdirall(strings.concat(source, "/", cases[ci]));
ci += 1;
};
writefile(strings.concat(dependency, "/wire.ww"), strings.concat(
"package wire;\n",
"export type record = struct { n: i32 };\n",
"export fn value() i32 = { return 41; };\n"));
writefile(strings.concat(sizedependency, "/size.ww"),
"package size;\nexport fn value() i32 = { return 1; };\n");
writefile(strings.concat(source, "/bareonly/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"fn main() i32 = { let bad: i32 = wire; return bad; };\n"));
writefile(strings.concat(source, "/aliasonly/main.ww"), strings.concat(
"package main;\nimport stable dep.wire;\n",
"fn main() i32 = { let bad: i32 = stable; return bad; };\n"));
writefile(strings.concat(source, "/selectorbare/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"fn main() i32 = { let ok: i32 = wire.value(); ",
"let bad: i32 = wire; return ok + bad; };\n"));
writefile(strings.concat(source, "/bareselector/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"fn main() i32 = { let bad: i32 = wire; ",
"return bad + wire.value(); };\n"));
writefile(strings.concat(source, "/multiple/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"fn main() i32 = { let ok: i32 = wire.value(); ",
"let a: i32 = wire; let b: i32 = wire; return ok + a + b; };\n"));
writefile(strings.concat(source, "/typeonly/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"fn bad(v: wire) i32 = { return 0; };\n",
"fn main() i32 = { return 0; };\n"));
writefile(strings.concat(source, "/aliastype/main.ww"), strings.concat(
"package main;\nimport stable dep.wire;\n",
"fn bad(v: stable) i32 = { return 0; };\n",
"fn main() i32 = { return 0; };\n"));
writefile(strings.concat(source, "/typeselector/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"fn bad(v: wire) i32 = { return 0; };\n",
"fn main() i32 = { return wire.value(); };\n"));
writefile(strings.concat(source, "/builtinalias/main.ww"), strings.concat(
"package main;\nimport len dep.wire;\n",
"fn main() i32 = { let ok: i32 = len.value(); ",
"let bad: i32 = len; return ok + bad; };\n"));
writefile(strings.concat(source, "/builtincallee/main.ww"), strings.concat(
"package main;\nimport len dep.wire;\n",
"fn main() i32 = { return len(\"abc\"); };\n"));
writefile(strings.concat(source, "/arraydim/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"let bad: [wire]i32;\nfn main() i32 = { return 0; };\n"));
writefile(strings.concat(source, "/enumvalue/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"type bad = enum { ZERO = wire };\n",
"fn main() i32 = { return 0; };\n"));
writefile(strings.concat(source, "/nestedconst/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"def BAD: i32 = 1 + (wire + wire);\n",
"fn main() i32 = { return 0; };\n"));
writefile(strings.concat(source, "/nestedarg/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"fn take(v: i32) i32 = { return v; };\n",
"fn main() i32 = { return take(wire.value() + (1 + wire)); };\n"));
writefile(strings.concat(source, "/structhead/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"fn main() i32 = { let bad = wire { n = 1 }; return bad.n; };\n"));
writefile(strings.concat(source, "/structfieldtype/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"type holder = struct { bad: wire };\n",
"fn main() i32 = { return 0; };\n"));
writefile(strings.concat(source, "/casttype/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"fn main() i32 = { let bad = 0: wire; return 0; };\n"));
writefile(strings.concat(source, "/builtincall/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"fn main() size = { return size(wire); };\n"));
writefile(strings.concat(source, "/sibling/a.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"fn anchor() i32 = { return wire.value(); };\n"));
writefile(strings.concat(source, "/sibling/b.ww"), strings.concat(
"package main;\n",
"fn bad() i32 = { let v: i32 = wire; return v; };\n",
"fn main() i32 = { return anchor() + bad(); };\n"));
writefile(strings.concat(source, "/siblingtype/a.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"fn anchor() i32 = { return wire.value(); };\n"));
writefile(strings.concat(source, "/siblingtype/b.ww"), strings.concat(
"package main;\nfn bad(v: wire) i32 = { return 0; };\n",
"fn main() i32 = { return anchor(); };\n"));
let middle: str = strings.concat(source, "/dep/middle");
mkdirall(middle);
writefile(strings.concat(middle, "/middle.ww"), strings.concat(
"package middle;\nimport dep.wire;\n",
"export fn value() i32 = { return wire.value(); };\n"));
writefile(strings.concat(source, "/transitive/main.ww"), strings.concat(
"package main;\nimport dep.middle;\n",
"fn main() i32 = { let ok: i32 = middle.value(); ",
"let bad: i32 = wire; return ok + bad; };\n"));
writefile(strings.concat(source, "/blank/main.ww"), strings.concat(
"package main;\nimport _ dep.wire;\n",
"fn main() i32 = { let v: i32 = wire; return v; };\n"));
writefile(strings.concat(source, "/initcase/main.ww"), strings.concat(
"package main;\nimport init dep.wire;\n",
"fn main() i32 = { return 0; };\n"));
writefile(strings.concat(source, "/missing/main.ww"), strings.concat(
"package main;\nimport stable dep.absent;\n",
"fn main() i32 = { let v: i32 = stable; return v; };\n"));
writefile(strings.concat(source, "/packagecalleeprim/main.ww"), strings.concat(
"package main;\nimport dep.size;\n",
"fn main() i32 = { return size(i32): i32; };\n"));
writefile(strings.concat(source, "/packagecalleetype/main.ww"), strings.concat(
"package main;\nimport dep.size;\nimport dep.wire;\n",
"fn main() i32 = { return size(wire): i32; };\n"));
writefile(strings.concat(source, "/packagecalleealign/main.ww"), strings.concat(
"package main;\nimport align dep.size;\nimport dep.wire;\n",
"fn main() i32 = { return align(wire): i32; };\n"));
writefile(strings.concat(source, "/arraypackagecallee/main.ww"), strings.concat(
"package main;\nimport dep.size;\nimport dep.wire;\n",
"let bad: [size(wire)]u8;\nfn main() i32 = { return 0; };\n"));
writefile(strings.concat(source, "/enumpackagecalleealign/main.ww"),
strings.concat("package main;\nimport align dep.size;\nimport dep.wire;\n",
"type bad = enum { ZERO = align(wire) };\n",
"fn main() i32 = { return 0; };\n"));
let valuecore: str = "use of package wire not in selector";
let aliascore: str = "use of package stable not in selector";
let builtinaliascore: str = "use of package len not in selector";
let sizecore: str = "use of package size not in selector";
let aligncore: str = "use of package align not in selector";
let typecore: str = "wire (package name) is not a type";
let aliastypecore: str = "stable (package name) is not a type";
let kinds: []i32 = [1, 6, 1, 1, 7, 2, 9, 2, 8, 8, 1, 1, 7, 1,
2, 2, 2, 2, 3, 10, 3, 3, 4, 5, 11, 12, 13, 12, 13];
let unusedcounts: []i32 = [1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1,
0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2];
let stages: []str = ["ww", "ww_ww"];
let tags: []str = ["c", "ww"];
let diagrefs: []str = alloc([], cases.len: u64)!;
ci = 0;
for (ci < cases.len) { append(diagrefs, ""); ci += 1; };
let si: i32 = 0;
for (si < stages.len) {
ci = 0;
for (ci < cases.len) {
let work: str = strings.concat(root, "/matrix-work-", tags[si],
"-", boundarypkgname(ci));
let output: str = strings.concat(root, "/matrix-output-", tags[si],
"-", boundarypkgname(ci));
mkdirall(work);
let av: []str = [driver(stages[si]), "build", "-w", work,
"-I", source, "-o", output, cases[ci]];
let out: commandout;
runcommand(root, strings.concat("bare-import-matrix-", tags[si],
"-", boundarypkgname(ci)), av,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(out.stdout.len == 0 && !os.exists(output)
&& !os.exists(strings.concat(output, ".new"))
&& !os.exists(strings.concat(output, ".sepwork"))
&& directoryisempty(work));
assert(!has(out.stderr, "undefined reference")
&& !has(out.stderr, "cgident: unresolvable identifier"));
if (kinds[ci] == 1) {
assert(occurrences(out.stderr, valuecore) == 1);
} else if (kinds[ci] == 2) {
assert(occurrences(out.stderr, typecore) == 1);
} else if (kinds[ci] == 3) {
assert(occurrences(out.stderr, "undefined: wire") == 1
&& !has(out.stderr, "package name) is not a type")
&& !has(out.stderr, "not in selector"));
} else if (kinds[ci] == 4) {
assert(occurrences(out.stderr,
"cannot import package as init - init must be a func") == 1
&& !has(out.stderr, "not in selector")
&& !has(out.stderr, "package name) is not a type"));
} else if (kinds[ci] == 5) {
assert(occurrences(out.stderr,
"cannot find package dep.absent") == 1
&& !has(out.stderr, "not in selector")
&& !has(out.stderr, "package name) is not a type"));
} else if (kinds[ci] == 6) {
assert(occurrences(out.stderr, aliascore) == 1);
} else if (kinds[ci] == 7) {
assert(occurrences(out.stderr, valuecore) == 2);
} else if (kinds[ci] == 8) {
assert(occurrences(out.stderr, builtinaliascore) == 1);
} else if (kinds[ci] == 10) {
assert(occurrences(out.stderr, "unknown type 'wire'") == 1
&& !has(out.stderr, "package name) is not a type")
&& !has(out.stderr, "not in selector"));
} else if (kinds[ci] == 11) {
assert(occurrences(out.stderr, sizecore) == 1
&& !has(out.stderr, "internal: unhandled expr kind")
&& !has(out.stderr, "package name) is not a type"));
} else if (kinds[ci] == 12) {
assert(occurrences(out.stderr, sizecore) == 1
&& occurrences(out.stderr, typecore) == 1
&& !has(out.stderr, "internal: unhandled expr kind"));
} else if (kinds[ci] == 13) {
assert(occurrences(out.stderr, aligncore) == 1
&& occurrences(out.stderr, typecore) == 1
&& !has(out.stderr, "internal: unhandled expr kind"));
} else {
assert(occurrences(out.stderr, aliastypecore) == 1);
};
assert(occurrences(out.stderr, "not used") == unusedcounts[ci]);
assert(!has(out.stderr, "array length must be an integer literal")
&& !has(out.stderr,
"enum value must be a constant integer expression")
&& !has(out.stderr, "arithmetic on non-numeric type")
&& !has(out.stderr, "unknown struct type")
&& !has(out.stderr, "called object is not a function")
&& !has(out.stderr, "asserttyped:"));
let normalized: str = normalizedtrace(out.stderr,
strings.concat(work, "/"), output);
if (ci == 0) {
assert(same(normalized, strings.concat(
"$WORK/bareonly.unit.new:3:1: error: ",
"\"dep.wire\" imported and not used\n",
"$WORK/bareonly.unit.new:4:34: error: ", valuecore, "\n",
"ww: w6c failed for bareonly\n")));
} else if (ci == 4) {
assert(same(normalized, strings.concat(
"$WORK/multiple.unit.new:4:60: error: ", valuecore, "\n",
"$WORK/multiple.unit.new:4:79: error: ", valuecore, "\n",
"ww: w6c failed for multiple\n")));
} else if (ci == 5) {
assert(same(normalized, strings.concat(
"$WORK/typeonly.unit.new:3:1: error: ",
"\"dep.wire\" imported and not used\n",
"$WORK/typeonly.unit.new:4:11: error: ", typecore, "\n",
"ww: w6c failed for typeonly\n")));
} else if (ci == 9) {
assert(same(normalized, strings.concat(
"$WORK/builtincallee.unit.new:3:1: error: ",
"\"dep.wire\" imported as len and not used\n",
"$WORK/builtincallee.unit.new:4:26: error: ",
builtinaliascore, "\n",
"ww: w6c failed for builtincallee\n")));
} else if (ci == 10) {
assert(same(normalized, strings.concat(
"$WORK/arraydim.unit.new:3:1: error: ",
"\"dep.wire\" imported and not used\n",
"$WORK/arraydim.unit.new:4:11: error: ", valuecore, "\n",
"ww: w6c failed for arraydim\n")));
} else if (ci == 12) {
assert(same(normalized, strings.concat(
"$WORK/nestedconst.unit.new:3:1: error: ",
"\"dep.wire\" imported and not used\n",
"$WORK/nestedconst.unit.new:4:21: error: ", valuecore, "\n",
"$WORK/nestedconst.unit.new:4:28: error: ", valuecore, "\n",
"ww: w6c failed for nestedconst\n")));
} else if (ci == 13) {
assert(same(normalized, strings.concat(
"$WORK/nestedarg.unit.new:5:51: error: ", valuecore, "\n",
"ww: w6c failed for nestedarg\n")));
} else if (ci == 18) {
assert(same(normalized, strings.concat(
"$WORK/sibling.unit.new:8:31: error: undefined: wire\n",
"ww: w6c failed for sibling\n")));
} else if (ci == 24) {
assert(same(normalized, strings.concat(
"$WORK/packagecalleeprim.unit.new:3:1: error: ",
"\"dep.size\" imported and not used\n",
"$WORK/packagecalleeprim.unit.new:4:26: error: ", sizecore,
"\nww: w6c failed for packagecalleeprim\n")));
} else if (ci == 25) {
assert(same(normalized, strings.concat(
"$WORK/packagecalleetype.unit.new:3:1: error: ",
"\"dep.size\" imported and not used\n",
"$WORK/packagecalleetype.unit.new:4:1: error: ",
"\"dep.wire\" imported and not used\n",
"$WORK/packagecalleetype.unit.new:5:26: error: ", sizecore,
"\n$WORK/packagecalleetype.unit.new:5:31: error: ", typecore,
"\nww: w6c failed for packagecalleetype\n")));
} else if (ci == 26) {
assert(same(normalized, strings.concat(
"$WORK/packagecalleealign.unit.new:3:1: error: ",
"\"dep.size\" imported as align and not used\n",
"$WORK/packagecalleealign.unit.new:4:1: error: ",
"\"dep.wire\" imported and not used\n",
"$WORK/packagecalleealign.unit.new:5:26: error: ", aligncore,
"\n$WORK/packagecalleealign.unit.new:5:32: error: ", typecore,
"\nww: w6c failed for packagecalleealign\n")));
} else if (ci == 27) {
assert(same(normalized, strings.concat(
"$WORK/arraypackagecallee.unit.new:3:1: error: ",
"\"dep.size\" imported and not used\n",
"$WORK/arraypackagecallee.unit.new:4:1: error: ",
"\"dep.wire\" imported and not used\n",
"$WORK/arraypackagecallee.unit.new:5:11: error: ", sizecore,
"\n$WORK/arraypackagecallee.unit.new:5:16: error: ", typecore,
"\nww: w6c failed for arraypackagecallee\n")));
} else if (ci == 28) {
assert(same(normalized, strings.concat(
"$WORK/enumpackagecalleealign.unit.new:3:1: error: ",
"\"dep.size\" imported as align and not used\n",
"$WORK/enumpackagecalleealign.unit.new:4:1: error: ",
"\"dep.wire\" imported and not used\n",
"$WORK/enumpackagecalleealign.unit.new:5:26: error: ", aligncore,
"\n$WORK/enumpackagecalleealign.unit.new:5:32: error: ", typecore,
"\nww: w6c failed for enumpackagecalleealign\n")));
};
if (si == 0) { diagrefs[ci] = strings.dup(normalized); }
else { assert(same(diagrefs[ci], normalized)); };
ci += 1;
};
si += 1;
};
// A raw source operand uses the same file-local import object and checker.
let raw: str = strings.concat(root, "/raw.ww");
writefile(raw, strings.concat(
"package main;\nimport dep.wire;\n",
"fn main() i32 = { let ok: i32 = wire.value(); ",
"let bad: i32 = wire; return ok + bad; };\n"));
let rawdiag: str = "";
si = 0;
for (si < stages.len) {
let work: str = strings.concat(root, "/raw-work-", tags[si]);
let output: str = strings.concat(root, "/raw-output-", tags[si]);
mkdirall(work);
let av: []str = [driver(stages[si]), "build", "-w", work,
"-I", source, "-o", output, raw];
let out: commandout;
runcommand(root, strings.concat("bare-import-raw-", tags[si]), av,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(out.stdout.len == 0 && occurrences(out.stderr, valuecore) == 1
&& !has(out.stderr, "imported as wire and not used")
&& !os.exists(output) && directoryisempty(work));
let normalized: str = normalizedtrace(out.stderr,
strings.concat(work, "/"), output);
if (si == 0) { rawdiag = strings.dup(normalized); }
else { assert(same(rawdiag, normalized)); };
si += 1;
};
// Valid selector use still owns import usage, artifact construction, and
// execution. Every comparable semantic action and public binary is exact.
let control: str = strings.concat(source, "/control");
mkdirall(control);
writefile(strings.concat(control, "/main.ww"), strings.concat(
"package main;\nimport dep.wire;\n",
"fn main() i32 = { let r: wire.record = wire.record { n = 0 }; ",
"return wire.value() + r.n; };\n"));
let suffixes: []str = [".unit.ww", ".wwi", ".s", ".o", ".a",
".init.unit.ww", ".init.s", ".init.o"];
let controlrefs: []str = alloc([], suffixes.len: u64)!;
let controlbin: str = "";
si = 0;
for (si < stages.len) {
let work: str = strings.concat(root, "/control-work-", tags[si]);
let output: str = strings.concat(root, "/control-output-", tags[si]);
mkdirall(work);
let av: []str = [driver(stages[si]), "build", "-w", work,
"-I", source, "-o", output, "control"];
let out: commandout;
runcommand(root, strings.concat("bare-import-control-", 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)
&& !directoryhasnew(work)
&& !directoryhasfragment(work, ".wwtxn."));
let runav: []str = [output];
runcommand(root, strings.concat("bare-import-control-run-", tags[si]),
runav, time.second, &out);
expectexit(&out, 41);
assert(out.stdout.len == 0 && out.stderr.len == 0);
let fi: i32 = 0;
for (fi < suffixes.len) {
let bytes: str = readfile(strings.concat(work, "/control",
suffixes[fi]));
if (si == 0) { append(controlrefs, strings.dup(bytes)); }
else { assert(same(controlrefs[fi], bytes)); };
fi += 1;
};
if (si == 0) { controlbin = strings.dup(readfile(output)); }
else { assert(same(controlbin, readfile(output))); };
si += 1;
};
// Production, same-package, external, honest test-only, a production
// function reached by a test, and an imported dependency all reject before
// test-main publication or execution. A filter cannot hide source errors.
let testtargets: []str = ["prodcase", "samecase", "externalcase",
"onlycase", "calledcase", "importedcase"];
ci = 0;
for (ci < testtargets.len) {
mkdirall(strings.concat(source, "/", testtargets[ci]));
ci += 1;
};
writefile(strings.concat(source, "/prodcase/prod.ww"), strings.concat(
"package prodcase;\nimport dep.wire;\n",
"fn bad() i32 = { let ok: i32 = wire.value(); ",
"let v: i32 = wire; return ok + v; };\n"));
writefile(strings.concat(source, "/prodcase/prod_test.ww"),
"package prodcase;\n@test fn must_not_run() void = { abort(); };\n");
writefile(strings.concat(source, "/samecase/prod.ww"),
"package samecase;\nfn value() i32 = { return 1; };\n");
writefile(strings.concat(source, "/samecase/same_test.ww"), strings.concat(
"package samecase;\nimport dep.wire;\n",
"@test fn must_not_run() void = { let ok: i32 = wire.value(); ",
"let bad: i32 = wire; abort(); };\n"));
writefile(strings.concat(source, "/externalcase/prod.ww"), strings.concat(
"package externalcase;\n",
"export fn value() i32 = { return 1; };\n"));
writefile(strings.concat(source, "/externalcase/external_test.ww"),
strings.concat("package externalcase_test;\nimport dep.wire;\n",
"@test fn must_not_run() void = { let ok: i32 = wire.value(); ",
"let bad: i32 = wire; abort(); };\n"));
writefile(strings.concat(source, "/onlycase/only_test.ww"), strings.concat(
"package onlycase;\nimport dep.wire;\n",
"@test fn must_not_run() void = { let ok: i32 = wire.value(); ",
"let bad: i32 = wire; abort(); };\n"));
writefile(strings.concat(source, "/calledcase/prod.ww"), strings.concat(
"package calledcase;\nimport dep.wire;\n",
"fn bad() i32 = { let ok: i32 = wire.value(); ",
"let v: i32 = wire; return ok + v; };\n"));
writefile(strings.concat(source, "/calledcase/called_test.ww"),
"package calledcase;\n@test fn must_not_run() void = { assert(bad() > 0); };\n");
let baddep: str = strings.concat(source, "/dep/baddep");
let buildimport: str = strings.concat(source, "/buildimport");
mkdirall(baddep);
mkdirall(buildimport);
writefile(strings.concat(baddep, "/baddep.ww"), strings.concat(
"package baddep;\nimport dep.wire;\n",
"export fn bad() i32 = { let ok: i32 = wire.value(); ",
"let v: i32 = wire; return ok + v; };\n"));
writefile(strings.concat(source, "/importedcase/prod.ww"), strings.concat(
"package importedcase;\nimport dep.baddep;\n",
"fn value() i32 = { return baddep.bad(); };\n"));
writefile(strings.concat(source, "/importedcase/imported_test.ww"),
"package importedcase;\n@test fn must_not_run() void = { assert(value() > 0); };\n");
writefile(strings.concat(buildimport, "/main.ww"), strings.concat(
"package main;\nimport dep.baddep;\n",
"fn main() i32 = { return baddep.bad(); };\n"));
let testdiagrefs: []str = alloc([], testtargets.len: u64)!;
ci = 0;
for (ci < testtargets.len) { append(testdiagrefs, ""); ci += 1; };
si = 0;
for (si < stages.len) {
ci = 0;
for (ci < testtargets.len) {
let work: str = strings.concat(root, "/test-work-", tags[si], "-",
boundarypkgname(ci));
let output: str = strings.concat(root, "/test-output-", tags[si], "-",
boundarypkgname(ci));
mkdirall(work);
let av: []str = [driver(stages[si]), "test", "-w", work,
"-I", source, "-run", "must_not_run", "-o", output,
testtargets[ci]];
let out: commandout;
runcommand(root, strings.concat("bare-import-test-", tags[si], "-",
boundarypkgname(ci)), av,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(out.stdout, "FAIL\n")
&& occurrences(out.stderr, valuecore) == 1
&& !has(out.stderr, "undefined reference")
&& !has(out.stderr, "cgident: unresolvable identifier")
&& !has(out.stdout, "must_not_run ...")
&& !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) { testdiagrefs[ci] = strings.dup(normalized); }
else { assert(same(testdiagrefs[ci], normalized)); };
ci += 1;
};
si += 1;
};
// Ordinary build reaches the same invalid dependency compiler action, and
// a genuinely empty test selection still compiles selected source before
// considering whether any runtime test would match.
let buildimportdiag: str = "";
let nomatchdiag: str = "";
si = 0;
for (si < stages.len) {
let buildwork: str = strings.concat(root, "/buildimport-work-", tags[si]);
let buildout: str = strings.concat(root, "/buildimport-output-", tags[si]);
mkdirall(buildwork);
let buildav: []str = [driver(stages[si]), "build", "-w", buildwork,
"-I", source, "-o", buildout, "buildimport"];
let out: commandout;
runcommand(root, strings.concat("bare-import-build-dependency-", tags[si]),
buildav, (120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(out.stdout.len == 0 && occurrences(out.stderr, valuecore) == 1
&& !has(out.stderr, "undefined reference")
&& !has(out.stderr, "cgident: unresolvable identifier")
&& !has(out.stderr, "asserttyped:")
&& !os.exists(buildout)
&& !os.exists(strings.concat(buildout, ".new"))
&& !os.exists(strings.concat(buildout, ".sepwork"))
&& directoryisempty(buildwork));
let buildnormalized: str = normalizedtrace(out.stderr,
strings.concat(buildwork, "/"), buildout);
if (si == 0) { buildimportdiag = strings.dup(buildnormalized); }
else { assert(same(buildimportdiag, buildnormalized)); };
let testwork: str = strings.concat(root, "/nomatch-work-", tags[si]);
let testout: str = strings.concat(root, "/nomatch-output-", tags[si]);
mkdirall(testwork);
let testav: []str = [driver(stages[si]), "test", "-w", testwork,
"-I", source, "-run", "does_not_match_any_test", "-o", testout,
"samecase"];
runcommand(root, strings.concat("bare-import-test-nomatch-", tags[si]),
testav, (120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(out.stdout, "FAIL\n")
&& occurrences(out.stderr, valuecore) == 1
&& !has(out.stdout, "testing: warning: no tests to run")
&& !has(out.stdout, " discovered, ")
&& !has(out.stdout, "must_not_run ...")
&& !has(out.stderr, "undefined reference")
&& !has(out.stderr, "cgident: unresolvable identifier")
&& !has(out.stderr, "asserttyped:")
&& !os.exists(testout)
&& !os.exists(strings.concat(testout, ".new"))
&& !os.exists(strings.concat(testout, ".sepwork"))
&& directoryisempty(testwork));
let testnormalized: str = normalizedtrace(out.stderr,
strings.concat(testwork, "/"), testout);
if (si == 0) { nomatchdiag = strings.dup(testnormalized); }
else { assert(same(nomatchdiag, testnormalized)); };
si += 1;
};
// A warm command keeps its committed graph/actions, package artifacts, and
// public binary across rejection; restoring exact source reuses the same
// semantic generation. The same publication rule protects a filtered,
// retained test binary, which remains directly executable after rejection.
let warm: str = strings.concat(source, "/warm");
mkdirall(warm);
let warmfile: str = strings.concat(warm, "/main.ww");
let warmvalid: str = strings.concat(
"package main;\nimport dep.wire;\n",
"fn main() i32 = { return wire.value(); };\n");
let warminvalid: str = strings.concat(
"package main;\nimport dep.wire;\n",
"fn main() i32 = { let ok: i32 = wire.value(); ",
"let bad: i32 = wire; return ok + bad; };\n");
writefile(warmfile, warmvalid);
let retainedcase: str = strings.concat(source, "/retainedcase");
mkdirall(retainedcase);
let retainedfile: str = strings.concat(retainedcase, "/retained_test.ww");
let retainedvalid: str = strings.concat(
"package retainedcase;\nimport dep.wire;\n",
"@test fn kept() void = { assert(wire.value() == 41); };\n");
let retainedinvalid: str = strings.concat(
"package retainedcase;\nimport dep.wire;\n",
"@test fn kept() void = { let ok: i32 = wire.value(); ",
"let bad: i32 = wire; assert(ok + bad > 0); };\n");
writefile(retainedfile, retainedvalid);
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 retainedcross: 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("bare-import-warm-cold-", tags[si]), av,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0);
let snapshots: []str = alloc([], warmpaths.len: u64)!;
let pi: i32 = 0;
for (pi < warmpaths.len) {
let bytes: str = strings.dup(readfile(strings.concat(work,
warmpaths[pi])));
append(snapshots, bytes);
if (si == 0) { append(warmcross, strings.dup(bytes)); }
else if (pi >= 4) { assert(same(warmcross[pi], bytes)); };
pi += 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("bare-import-warm-reject-", tags[si]), av,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(out.stdout.len == 0 && occurrences(out.stderr, valuecore) == 1
&& same(binbytes, readfile(output))
&& !os.exists(strings.concat(output, ".new"))
&& !os.exists(strings.concat(output, ".sepwork")));
pi = 0;
for (pi < warmpaths.len) {
assert(same(snapshots[pi], readfile(strings.concat(work,
warmpaths[pi]))));
pi += 1;
};
assert(!directoryhasnew(work)
&& !directoryhasfragment(work, ".wwtxn.")
&& !directoryhasfragment(work, ".install"));
rewritefile(warmfile, warmvalid);
runcommand(root, strings.concat("bare-import-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)));
pi = 0;
for (pi < warmpaths.len) {
assert(same(snapshots[pi], readfile(strings.concat(work,
warmpaths[pi]))));
pi += 1;
};
let testwork: str = strings.concat(root, "/retained-work-", tags[si]);
let retained: str = strings.concat(root, "/retained-", tags[si], ".test");
mkdirall(testwork);
let compileav: []str = [driver(stages[si]), "test", "-w",
testwork, "-I", source, "-run", "kept", "-o", retained,
"retainedcase"];
runcommand(root, strings.concat("bare-import-retained-cold-", tags[si]),
compileav, (120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stderr.len == 0 && os.exists(retained)
&& occurrences(out.stdout, "retainedcase.kept ... ok\n") == 1
&& occurrences(out.stdout,
"1 passed, 0 failed, 0 skipped, 0 harness errors\n") == 1);
let retainedstdout: str = strings.dup(out.stdout);
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("bare-import-retained-reject-", tags[si]),
compileav, (120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(out.stdout, "FAIL\n")
&& occurrences(out.stderr, valuecore) == 1
&& same(retainedbytes, readfile(retained))
&& !os.exists(strings.concat(retained, ".new"))
&& !os.exists(strings.concat(retained, ".sepwork"))
&& !directoryhasnew(testwork)
&& !directoryhasfragment(testwork, ".wwtxn."));
let directav: []str = [retained, "-package=retainedcase", "kept"];
runcommand(root, strings.concat("bare-import-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);
directoutrefs[si] = strings.dup(out.stdout);
directerrrefs[si] = strings.dup(out.stderr);
rewritefile(retainedfile, retainedvalid);
runcommand(root, strings.concat("bare-import-retained-restored-", tags[si]),
compileav, (120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(same(out.stdout, retainedstdout) && out.stderr.len == 0
&& same(retainedbytes, readfile(retained))
&& !directoryhasnew(testwork)
&& !directoryhasfragment(testwork, ".wwtxn.")
&& !directoryhasfragment(testwork, ".install"));
si += 1;
};
assert(same(directoutrefs[0], directoutrefs[1])
&& same(directerrrefs[0], directerrrefs[1]));
assert(!directoryhasnew(root)
&& !directoryhasfragment(root, ".wwtxn.")
&& !directoryhasfragment(root, ".install"));
clean(root);
};