From 89f519d5cb6a1327374bc059e51539146784dd64 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 22 Aug 2026 15:17:10 +0900 Subject: [PATCH] fix: reject bare import bindings --- cmd/wcc/check.c | 245 +++++++++++- docs/build-system.md | 215 +++++++++++ docs/spec.md | 20 +- docs/test-system-v2.md | 29 ++ selfhost/cmd/wcc/check.ww | 313 +++++++++++++++- test/package/package_test.ww | 706 ++++++++++++++++++++++++++++++++++- 6 files changed, 1490 insertions(+), 38 deletions(-) diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 408e79d5..03cce54b 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -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) { diff --git a/docs/build-system.md b/docs/build-system.md index e7b7d475..dd7e01f3 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -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 + 18–60](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 + 89–92](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 + 675–680](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 + 672–735](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/call.go#L672-L735), + especially lines 681–691, 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 + 248–340](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 + 706–740](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 + 70–92](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 + 788–802](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 +7–11](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 +613–617](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 +45–73](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. diff --git a/docs/spec.md b/docs/spec.md index 0ce51581..8809c721 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -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 diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 0f3128a7..31c36387 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -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 diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 2abf725c..f7410d2c 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -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 diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 7b72a894..2c8d40ad 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -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); +};