diff --git a/cmd/w6c/main.c b/cmd/w6c/main.c index 63955a11..58dc4a9c 100644 --- a/cmd/w6c/main.c +++ b/cmd/w6c/main.c @@ -282,62 +282,92 @@ consider_pkgname(const char *path, const char *candidate, *name = candidate; } -/* A paired interface owns PATH's declared name. Compiler-private names keep - * transitive fact sections semantic and are ignored when a real name exists. */ -static const char * +enum pkgname_state { + PKGNAME_MISSING, + PKGNAME_VALID, + PKGNAME_CONFLICT, + PKGNAME_INVALID, +}; + +/* Standalone interfaces may carry closure facts for several canonical + * packages. Marker.module, rather than the containing --import path, owns + * each declared name. Compiler-private names keep transitive fact sections + * semantic and are ignored when a real name exists. */ +static enum pkgname_state import_pkgname(struct importin *imports, int nimports, const char *path, - Node *primary, int *conflict) + Node *primary, const char **resolved) { const char *name = NULL; const char *placeholder = NULL; + int conflict = 0; for (int i = 0; i < nimports; i++) { - if (strcmp(imports[i].path, path) != 0) continue; Node *file = imports[i].ast; for (Node *p = file ? file->body : NULL; p; p = p->next) { if (p->module == NULL || p->pkgname == NULL || strcmp(p->module, path) != 0) continue; consider_pkgname(path, p->pkgname, &name, &placeholder, - conflict); - if (*conflict) return NULL; + &conflict); + if (conflict) return PKGNAME_CONFLICT; } } for (Node *p = primary ? primary->body : NULL; p; p = p->next) { if (p->module == NULL || p->pkgname == NULL || strcmp(p->module, path) != 0) continue; - consider_pkgname(path, p->pkgname, &name, &placeholder, conflict); - if (*conflict) return NULL; + consider_pkgname(path, p->pkgname, &name, &placeholder, &conflict); + if (conflict) return PKGNAME_CONFLICT; } - return name ? name : placeholder; + *resolved = name ? name : placeholder; + if (*resolved == NULL) return PKGNAME_MISSING; + if (strcmp(*resolved, "_") == 0) return PKGNAME_INVALID; + return PKGNAME_VALID; +} + +static const char * +path_leaf(const char *path) +{ + const char *dot = path ? strrchr(path, '.') : NULL; + return dot ? dot + 1 : path; } static int bind_import_names(Node *list, struct importin *imports, int nimports, - Node *primary, const char *testsupport) + Node *primary, const char *testsupport, Node *external_support) { for (Node *u = list; u; u = u->next) { if (u->kind != N_USE || u->usepath == NULL) continue; /* The reserved test-support spelling is a compiler-owned alias, not * source default-import syntax. */ - if (testsupport != NULL && strcmp(testsupport, "__wwtest") == 0 - && strcmp(u->usepath, testsupport) == 0) - continue; - int conflict = 0; - const char *name = import_pkgname(imports, nimports, u->usepath, - primary, &conflict); - if (conflict) { + int reserved = testsupport != NULL + && strcmp(testsupport, "__wwtest") == 0 + && strcmp(u->usepath, testsupport) == 0; + const char *name = NULL; + enum pkgname_state state = import_pkgname(imports, nimports, + u->usepath, primary, &name); + if (state == PKGNAME_CONFLICT) { fprintf(stderr, "w6c: package %s has conflicting declared names in export data\n", u->usepath); return -1; } - if (name != NULL) { + if (state == PKGNAME_VALID || state == PKGNAME_INVALID) { u->usepkgname = name; - if (!u->useblank) { + if (state == PKGNAME_INVALID) { + u->used = 1; + if (!u->useblank && !reserved) { + u->str = u->usealias ? u->usealias + : path_leaf(u->usepath); + u->strlen = strlen(u->str); + } + } else if (!u->useblank && !reserved) { u->str = u->usealias ? u->usealias : name; u->strlen = strlen(u->str); } + } else if (u == external_support) { + /* A bare direct -T compiler invocation deliberately leaves the + * compiler-generated support hook external. */ + u->used = 1; } else if (!u->imported) { fprintf(stderr, "w6c: import %s has no declared package name in direct export data\n", @@ -356,6 +386,121 @@ bind_import_names(Node *list, struct importin *imports, int nimports, return 0; } +struct pathset { + const char **v; + int n; + int cap; +}; + +static int +pathset_has(const struct pathset *s, const char *path) +{ + if (path == NULL) return 0; + for (int i = 0; i < s->n; i++) + if (strcmp(s->v[i], path) == 0) return 1; + return 0; +} + +static int +pathset_add(struct pathset *s, const char *path) +{ + if (path == NULL || pathset_has(s, path)) return 0; + if (s->n == s->cap) { + int cap = s->cap ? s->cap * 2 : 16; + const char **v = realloc(s->v, (size_t)cap * sizeof *v); + if (v == NULL) return -1; + s->v = v; + s->cap = cap; + } + s->v[s->n++] = path; + return 1; +} + +static Node * +materialize_test_support(Arena *a, Node *file, int testmode, + const char *testsupport) +{ + if (!testmode || testsupport == NULL) return NULL; + for (Node *u = file->list; u; u = u->next) { + if (u->kind != N_USE || u->imported + || u->sourceid != file->sourceid || u->usepath == NULL + || strcmp(u->usepath, testsupport) != 0) + continue; + u->used = 1; + return NULL; + } + Node *u = newnode(a, N_USE, file->pos); + u->str = testsupport; + u->strlen = strlen(testsupport); + u->usesource = testsupport; + u->usepath = testsupport; + u->usefile = file->pos.file; + u->useline = file->pos.line; + u->usecol = file->pos.col; + u->usepathfile = file->pos.file; + u->usepathline = file->pos.line; + u->usepathcol = file->pos.col; + u->pkgname = file->pkgname; + u->sourceid = file->sourceid; + u->used = 1; + u->next = file->list; + file->list = u; + return u; +} + +static int +compute_reached_imports(struct pathset *reached, Node *primary, + struct importin *imports, int nimports) +{ + for (Node *u = primary ? primary->list : NULL; u; u = u->next) { + if (u->kind != N_USE || u->imported || u->usepath == NULL) continue; + if (pathset_add(reached, u->usepath) < 0) return -1; + } + int changed; + do { + changed = 0; + for (int i = 0; i < nimports; i++) { + for (Node *u = imports[i].ast ? imports[i].ast->list : NULL; + u; u = u->next) { + if (u->kind != N_USE || !u->imported + || u->module == NULL || u->usepath == NULL + || !pathset_has(reached, u->module)) + continue; + const char *name = NULL; + if (import_pkgname(imports, nimports, u->module, + NULL, &name) != PKGNAME_VALID) + continue; + int added = pathset_add(reached, u->usepath); + if (added < 0) return -1; + if (added) changed = 1; + } + } + } while (changed); + return 0; +} + +static void +filter_reached_imports(Node **list, const struct pathset *reached, + struct importin *imports, int nimports) +{ + Node *prev = NULL; + for (Node *d = *list; d; ) { + Node *next = d->next; + const char *name = NULL; + int keep = d->imported && d->module != NULL + && pathset_has(reached, d->module) + && import_pkgname(imports, nimports, d->module, NULL, + &name) == PKGNAME_VALID; + if (keep) + prev = d; + else if (prev == NULL) + *list = next; + else + prev->next = next; + d = next; + } +} + static void appendnodes(Node **head, Node **tail, Node *list) { @@ -563,7 +708,6 @@ main(int argc, char **argv) Checker c; Cg cg; - Node *head = NULL, *tail = NULL; for (int i = 0; i < nimports; i++) { if (slurp(imports[i].file, &imports[i].buf, &imports[i].len) < 0) { @@ -583,10 +727,6 @@ main(int argc, char **argv) imports[i].len, imports[i].path, testsupport, 0, &bad); if (bad) return 1; imports[i].ast = f; - if (bind_import_names(f->list, imports, i + 1, NULL, - testsupport) < 0) - return 1; - appendnodes(&head, &tail, f->list); } char *buf; @@ -619,14 +759,33 @@ main(int argc, char **argv) fputs("w6c: --import-map source is not in primary input\n", stderr); return 2; } - /* Later direct interfaces may supply names for origin sections referenced - * by an earlier interface, so perform one complete metadata pass now. */ + /* The checker normally synthesizes this compiler-required edge. Make it + * an explicit primary root before interface reachability and name + * resolution so a supplied support interface cannot bypass validation. */ + Node *external_support = materialize_test_support(a, file, testmode, + testsupport); + + /* Interface containers and embedded origin sections are metadata, not + * roots. Only primary uses seed the closure, and only a reached valid + * owner may contribute its transitive uses. */ + struct pathset reached = {0}; + if (compute_reached_imports(&reached, file, imports, nimports) < 0) { + fputs("w6c: out of memory\n", stderr); + return 1; + } + for (int i = 0; i < nimports; i++) + filter_reached_imports(&imports[i].ast->list, &reached, imports, + nimports); + + /* Bind only retained facts, while consulting the complete package-marker + * metadata. Keeping the standalone lists separate here prevents one + * interface from escaping its reachability boundary through concatenation. */ for (int i = 0; i < nimports; i++) if (bind_import_names(imports[i].ast->list, imports, nimports, - NULL, testsupport) < 0) + NULL, testsupport, NULL) < 0) return 1; if (bind_import_names(file->list, imports, nimports, file, - testsupport) < 0) + testsupport, external_support) < 0) return 1; for (int ti = 0; ti < ntesttargets; ti++) { int seen = 0; @@ -634,8 +793,14 @@ main(int argc, char **argv) if (u->kind != N_USE || u->imported || u->usepath == NULL || strcmp(u->usepath, testtargets[ti]) != 0) continue; - u->str = u->usepath; - u->strlen = strlen(u->usepath); + /* Valid generated target imports use their canonical path as + * the compiler-owned qualifier. Preserve an invalid provider's + * fake explicit-or-leaf spelling for source-local recovery. */ + if (u->usepkgname == NULL + || strcmp(u->usepkgname, "_") != 0) { + u->str = u->usepath; + u->strlen = strlen(u->usepath); + } seen++; } if (seen != 1) { @@ -644,6 +809,11 @@ main(int argc, char **argv) return 2; } } + free(reached.v); + + Node *head = NULL, *tail = NULL; + for (int i = 0; i < nimports; i++) + appendnodes(&head, &tail, imports[i].ast->list); if (head != NULL) { tail->next = file->list; file->list = head; diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 6aeb0394..9fa2618e 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -62,6 +62,8 @@ static const char *use_path(Node *file, const char *curmod, int source, const char *alias); static const char *find_use_path(Node *file, const char *curmod, int source, const char *alias, int mark); +static Node *find_use_binding(Node *file, const char *curmod, int source, + const char *alias, int mark); static int src_imports(Node *file, const char *modtag, int source, const char *name); static Sym *lookup_visible(Checker *c, const char *name); @@ -96,6 +98,11 @@ resolve_typename(Checker *c, Node *n) char *head = astrndup(c->a, nm, hl); Sym *m = scope_lookup(c->cur, head); if (m && (m->kind == SK_USE || m->use_alias)) { + Node *u = find_use_binding(c->file, c->cur_mod, + c->cur_source, head, 1); + if (u != NULL && u->usepkgname != NULL + && strcmp(u->usepkgname, "_") == 0) + return ty_err; /* M1 #22: map the qualifier alias to its dotted * import path (symbols are path-keyed). */ const char *mk = use_path(c->file, c->cur_mod, @@ -1521,6 +1528,11 @@ cexpr(Checker *c, Node *n) if (n->lhs && n->lhs->kind == N_IDENT) { Sym *ms = lookup_visible(c, n->lhs->str); if (ms && (ms->kind == SK_USE || ms->use_alias)) { + Node *u = find_use_binding(c->file, c->cur_mod, + c->cur_source, n->lhs->str, 1); + if (u != NULL && u->usepkgname != NULL + && strcmp(u->usepkgname, "_") == 0) + return n->type = ty_err; /* Module-qualified ref. `use_alias` covers * the self-import case where the module's * type name shadowed the SK_USE; the leaf @@ -3016,6 +3028,18 @@ import_binding_pos(Node *u) return p; } +static Pos +import_path_pos(Node *u) +{ + Pos p = u->pos; + if (u->usepathfile != NULL) { + p.file = u->usepathfile; + p.line = u->usepathline; + p.col = u->usepathcol; + } + return p; +} + /* * use_path — map a source-file default qualifier (the imported package's * declared name) to the full canonical import path it binds, for @@ -3056,6 +3080,33 @@ find_use_path(Node *file, const char *curmod, int source, const char *alias, return NULL; } +static Node * +find_use_binding(Node *file, const char *curmod, int source, + const char *alias, int mark) +{ + if (file == NULL || alias == NULL) return NULL; + /* Source-zero self-qualification wins before import lookup in + * find_use_path and therefore cannot denote an imported package object. */ + if (source == 0 && curmod != NULL) { + const char *dot = strrchr(curmod, '.'); + const char *leaf = dot ? dot + 1 : curmod; + if (strcmp(alias, leaf) == 0) return NULL; + } + for (Node *u = file->list; u; u = u->next) { + if (u->kind != N_USE || u->str == NULL || invalid_init_import(u) + || u->sourceid != source || strcmp(u->str, alias) != 0) + continue; + const char *um = decl_mod(file, u); + int same = (um == NULL) ? (curmod == NULL) + : (curmod != NULL && strcmp(um, curmod) == 0); + if (same) { + if (mark) u->used = 1; + return u; + } + } + return NULL; +} + static const char * use_path(Node *file, const char *curmod, int source, const char *alias) { @@ -4379,11 +4430,48 @@ check_test_target(const Checker *c, const char *path) return 0; } +static void +reject_blank_package_names(Checker *c, Node *file) +{ + for (Node *p = file->body; p; p = p->next) + if (p->kind == N_FILE && p->pkgname != NULL + && strcmp(p->pkgname, "_") == 0) + err(c, p->pos, "invalid package name _"); +} + +static void +reject_invalid_imports(Checker *c, Node *file) +{ + for (Node *u = file->list; u; u = u->next) { + if (u->kind != N_USE || u->usepath == NULL + || u->usepkgname == NULL + || strcmp(u->usepkgname, "_") != 0) + continue; + u->used = 1; + int seen = 0; + for (Node *v = file->list; v != u; v = v->next) { + if (v->kind == N_USE && v->usepath != NULL + && v->usepkgname != NULL + && strcmp(v->usepkgname, "_") == 0 + && strcmp(v->usepath, u->usepath) == 0) { + seen = 1; + break; + } + } + if (!seen) + err(c, import_path_pos(u), + "could not import %s (invalid package name: \"_\")", + u->usepath); + } +} + void check_file(Checker *c, Node *file) { if (file == NULL || file->kind != N_FILE) return; c->file = file; + reject_blank_package_names(c, file); + reject_invalid_imports(c, file); /* Under -T, prepend the dispatcher support import before pass 1 so * decl_mod keys the runner under the selected support module. A pure diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index 37029d62..9f91aae9 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -92,6 +92,24 @@ expectident(Parser *p) return s; } +/* Package declarations admit the blank identifier syntactically. Keep this + * private to the package-name slot: every ordinary identifier production must + * retain expectident's rejection of TK_UNDER. The checker owns the semantic + * BlankPkgName rejection. */ +static const char * +expectpackagename(Parser *p) +{ + if (p->cur.kind != TK_IDENT && p->cur.kind != TK_UNDER) { + errorf(p->cur.pos, "expected identifier, got %s", + tokname(p->cur.kind)); + p->errs++; + return ""; + } + const char *s = p->cur.text; + advance(p); + return s; +} + /* Like expectident but also accepts a bare `_` discard marker. The * returned string is the empty string "" so the checker skips * scope_define. Callers that care can detect this with `s[0] == '\0'`. */ @@ -1340,19 +1358,25 @@ parseuse(Parser *p) n->usefile = p->cur.pos.file; n->useline = p->cur.pos.line; n->usecol = p->cur.pos.col; + Pos pathpos = p->cur.pos; const char *alias = NULL; const char *first; if (p->cur.kind == TK_UNDER) { n->useblank = 1; advance(p); + pathpos = p->cur.pos; first = expectident(p); } else { first = expectident(p); if (p->cur.kind == TK_IDENT) { alias = first; + pathpos = p->cur.pos; first = expectident(p); } } + n->usepathfile = pathpos.file; + n->usepathline = pathpos.line; + n->usepathcol = pathpos.col; const char *leaf = first; const char *path = leaf; while (accept(p, TK_DOT)) { @@ -1383,11 +1407,13 @@ parseheaderuse(Parser *p) n->usefile = p->cur.pos.file; n->useline = p->cur.pos.line; n->usecol = p->cur.pos.col; + Pos pathpos = p->cur.pos; const char *alias = NULL; const char *first; if (p->cur.kind == TK_UNDER) { n->useblank = 1; advance(p); + pathpos = p->cur.pos; } else if (p->cur.kind != TK_IDENT) { errorf(p->cur.pos, "expected identifier, got %s", tokname(p->cur.kind)); @@ -1404,9 +1430,13 @@ parseheaderuse(Parser *p) advance(p); if (!n->useblank && p->cur.kind == TK_IDENT) { alias = first; + pathpos = p->cur.pos; first = p->cur.text; advance(p); } + n->usepathfile = pathpos.file; + n->usepathline = pathpos.line; + n->usepathcol = pathpos.col; const char *leaf = first; const char *path = leaf; while (p->cur.kind == TK_DOT) { @@ -1467,13 +1497,12 @@ parsepackageheader(Parser *p) } Pos pp = p->cur.pos; advance(p); - if (p->cur.kind != TK_IDENT) { + if (p->cur.kind != TK_IDENT && p->cur.kind != TK_UNDER) { errorf(p->cur.pos, "invalid or missing package clause"); p->errs++; return file; } - const char *name = p->cur.text; - advance(p); + const char *name = expectpackagename(p); if (p->cur.kind != TK_SEMI) { errorf(p->cur.pos, "expected ';' after package name"); p->errs++; @@ -1548,14 +1577,14 @@ parseimports(Parser *p) Pos pp = p->cur.pos; previmport = 1; advance(p); - if (p->cur.kind != TK_IDENT) { + if (p->cur.kind != TK_IDENT && p->cur.kind != TK_UNDER) { errorf(p->cur.pos, "invalid or missing package clause"); p->errs++; sawpackage = 1; skipdecl(p); continue; } - const char *name = expectident(p); + const char *name = expectpackagename(p); expect(p, TK_SEMI); p->curpkg = name; if (p->pathmod == NULL && p->resetmod == NULL) @@ -1754,13 +1783,16 @@ parsefile(Parser *p) sawpackage = 1; previmport = 1; advance(p); - const char *name = expectident(p); + Pos namepos = p->cur.pos; + const char *name = expectpackagename(p); expect(p, TK_SEMI); p->curpkg = name; if (p->pathmod == NULL && p->resetmod == NULL) { p->curmod = name; } - Node *package = newnode(p->a, N_FILE, packagepos); + Pos markerpos = strcmp(name, "_") == 0 + ? namepos : packagepos; + Node *package = newnode(p->a, N_FILE, markerpos); package->module = p->curmod; package->pkgname = name; package->sourceid = p->sourceid; diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index db319c4d..71597c3e 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -354,6 +354,10 @@ struct Node { * alias when explicit, path otherwise. */ int useline; int usecol; + const char *usepathfile; /* N_USE: first path-token position, + * independent of an explicit/blank alias. */ + int usepathline; + int usepathcol; const char *usepkgname; /* N_USE: imported declared package name, * independent of the visible binding in `str`. */ int useblank; /* N_USE: `_` spelling; no source binding. */ diff --git a/docs/build-system.md b/docs/build-system.md index f09ab8d8..baf1d6d5 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -6982,8 +6982,11 @@ test-runtime gap: with official anchors in [`internal/types/testdata/check/blank.go`](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/blank.go#L1-L5) and [`test/blank1.go`](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/blank1.go#L1-L10). - Direct production, imported, and test-only probes were rejected identically - by both WW stages. This candidate was aligned. + Direct production, imported, and test-only probes were rejected by both WW + stages, but only through the loader's generic `invalid or missing package + clause`; the direct compilers also produced different syntax-recovery + streams. Equal rejection was not semantic alignment. The gap was different + and is completed in section 11.56. - **Import:** pinned `unusedImports` requires every nonblank import binding to be used ([`cmd/compile/internal/types2/resolver.go`, lines 706–740](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L706-L740)); official @@ -10354,11 +10357,11 @@ a raw source and does not trigger the non-directory test-source omission in - **directly measured WW behavior** — direct `SIGTERM` of either driver while a selected warm build is blocked in compilation is an inherited lifecycle non-effect, not part of the classifier change. Both stages terminate with - shell status 143, reap the complete owned process group, and preserve the - prior public output and committed semantic bytes, but the existing directory - machinery leaves three request-private `.new` files. That independently - verified cleanup gap remains open and this slice does not describe it as - fixed. + shell status 143 and preserve the prior public output and committed semantic + bytes, but leave the directly spawned compiler alive and exactly three + fixed-name `.new` files. A later persistent request rejects the existing + `.unit.new`. That independently verified supervision/cleanup gap remains open + and this slice does not describe it as fixed. - **directly measured WW behavior** — Cstage and WWstage agree byte-for-byte on status, stdout, stderr, diagnostics, public output, and every semantic artifact for selected success and failure rows. Complete @@ -10852,6 +10855,319 @@ every other package name remain ordinary. remains `3`; no action descriptor, cache/result record, transaction marker, manifest, database, or lock is added. +### 11.56 Implemented blank declared package-name checking + +The exact declared name in `package _;` is now valid package-clause syntax and +an invalid package name. Loaders retain the clause, its imports, and an +otherwise coherent source family long enough to construct the applicable +ordinary action; the compiler checker then reports exactly +`invalid package name _` at the underscore token and continues checking that +source. The underscore is accepted only in this package-name grammar slot. It +does not become an ordinary identifier, import alias, qualifier, canonical +package name, or successful exported identity. + +#### Pinned authority, tests, and applicability + +The sole authority is official Go 1.26.5 at commit +`c19862e5f8415b4f24b189d065ed739517c548ba`: + +- **behavior directly implemented or asserted by pinned Go** — the compiler + scanner admits `_` to identifier scanning, dispatches it through the name + path, and returns it as a name token; the parser accepts and stores that token + in the package-name position + ([`cmd/compile/internal/syntax/scanner.go`, lines 88–107](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/scanner.go#L88-L107), + [368–394](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/scanner.go#L368-L394), + and + [437–439](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/scanner.go#L437-L439), + [`parser.go`, lines 397–420](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/parser.go#L397-L420) + and + [2751–2763](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/parser.go#L2751-L2763)). + The types2 checker rejects the retained node as `invalid package name _` + and continues file initialization + ([`cmd/compile/internal/types2/check.go`, lines 336–355](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/check.go#L336-L355)); + syntax errors prevent types2 from running + ([`cmd/compile/internal/noder/noder.go`, lines 45–77](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/noder/noder.go#L45-L77), + [`irgen.go`, lines 23–99](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/noder/irgen.go#L23-L99)). +- **behavior directly implemented or asserted by pinned Go** — `go/build` + reads `_` without declaration-error parsing, classifies production and test + roles, and records imports before cmd/go builds package actions + ([`go/build/read.go`, lines 55–56](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/read.go#L55-L56), + [187–198](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/read.go#L187-L198), + and + [265–340](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/read.go#L265-L340), + [`go/build/build.go`, lines 931–1039](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L931-L1039)). + Named files use one synthetic package loader + ([`cmd/go/internal/load/pkg.go`, lines 3244–3315](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L3244-L3315)); + test synthesis augments production with internal-test files rather than + treating them as unrelated packages + ([`cmd/go/internal/load/test.go`, lines 175–226](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/test.go#L175-L226)). +- **behavior directly implemented or asserted by pinned Go** — + [`test/blank1.go`, lines 1–31](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/blank1.go#L1-L31) + asserts the blank-name error plus later checker errors, directly proving + continued checking. + [`internal/types/testdata/check/blank.go`, lines 1–5](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/blank.go#L1-L5) + separately asserts only the blank-name error. Official role controls + [`build_test_only.txt`, lines 1–18](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_test_only.txt#L1-L18) + and + [`build_no_go.txt`, lines 1–30](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_no_go.txt#L1-L30) + anchor the surrounding test-only selection rules. The pinned repository has + no official cmd/go test matrix for named, multiple, mixed, + test-only, artifact, rollback, concurrency, or interruption forms of + `package _`; those are WW-native proofs, not attributed to an absent Go + script. +- **behavior directly implemented or asserted by pinned Go** — after primary + syntax succeeds, types2 validates the imported package object's name before + consulting a local alias. Name `_` reports + `could not import PATH (invalid package name: "_")` at the import path; + an empty name instead quotes the actual empty value as + `invalid package name: ""`. The resolver + installs and caches a path-leaf-named fake package, marks the occurrence + used, and continues checking + ([`cmd/compile/internal/types2/resolver.go`, lines 125–180 and 248–335](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L125-L180)). + The public checker twin implements the same rule + ([`go/types/resolver.go`, lines 157–190 and 263–350](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/types/resolver.go#L157-L190)). +- No official pinned test directly supplies an imported `Package` whose name + is `_`. The two primary-source blank-name tests above do not assert this + importer-result validation. Its imported-interface matrix is therefore + WW-native proof grounded in the pinned resolver implementation, not a claim + about absent official testdata. +- **behavior derived from the pinned implementation** — the diagnostic owns + the underscore position, normally line 1 column 9, and belongs after source + loading, graph construction, complete-file syntax, and dependency-eligible + producer ordering. WW has the same explicit package clause, blank token, + syntax/check split, package families, imports, and package actions, so the + rule applies without adding modules, manifests, registries, generalized + imports, or a new identity model. + +Before this slice, **directly measured WW behavior** was a generic positioned +`invalid or missing package clause` from both public stages before any +producer. Direct Cstage `w6c` emitted four package-clause recovery diagnostics +while WWstage `w6c_ww` emitted three. A visible named `_test.ww` ordinary build +was rejected at its header instead of being validated and omitted. These facts +were a real package/build/test/import difference despite stage-equal public +rejection. After the primary-source parser/checker repair and before the +imported-interface completion, **directly measured WW behavior** also accepted +an owner-matched hand-authored `.wwi` declaring `package _;`: default, +explicit, and blank controls accepted and published usable semantic facts, and +the explicit alias control produced successful byte-identical Cstage/WWstage +assembly and interface output. Ordinary producers could not create that +metadata, but direct `--import` and caller-owned persistent interfaces made the +gap externally observable. + +#### Semantic ownership + +Primary package-slot syntax and marker positions remain owned by the C/WW +parser twins `cmd/wcc/parse.c` and `lib/ww/syntax/parse.ww`; primary and +imported blank-name checker diagnostics and fake-package resolution are owned +by `cmd/wcc/check.c` and `selfhost/cmd/wcc/check.ww`. Imported export-data +materialization, four-state package metadata, primary-rooted reachability, +owner filtering, recovery qualifier assignment, and delayed concatenation are +owned by `cmd/w6c/main.c` and `selfhost/cmd/w6c/main.ww`. Exact import-path +token positions are private `N_USE` state owned by `cmd/wcc/ww.h`, +`lib/ww/syntax/ast.ww`, both parser twins, and +`lib/ww/syntax/decl.ww`. + +The public drivers and package coordinator already supply canonical direct +interface arguments and inherit the validation without edits. No loader, +assembler, archiver, linker, runtime, package-identity, or import-syntax owner +changes in this completion. This complete ownership replaces the earlier +transitional assumption that the slice had only four syntax/checker owners. + +#### Four-axis behavior and phase order + +- **Build:** an eligible blank production source passes the loader, contributes + its ordinary package action and import closure, and fails in checker entry. + A visible literal named `_test.ww` still follows section 11.51: after its + header and contiguous imports are valid, ordinary build omits it before body + parsing or action construction, including when the declared name is `_`. + `ww run` retains its earlier MainOnly boundary: a blank root is not `main`, + so a successfully loaded root is rejected before any producer or compiler. +- **Test:** an all-blank production, internal-test, or honest test-only family + reaches its applicable test-package compiler action and fails there. A valid + production `p` plus blank test `_`, or blank production `_` plus unrelated + `p`/`p_test`, remains a coordinator family mismatch before tools. Existing + raw/directory `FAIL` placement, compile-only empty stdout, and directory + `-S` CLI-shape diagnostics are unchanged; no failed blank product runs. +- **Package:** `_` is retained only as a declared source-family observation. + It is not canonical identity. In an all-blank selected action, the checker + emits one positioned BlankPkgName diagnostic per retained source marker in + deterministic source-section order and continues later checking. Mixed + declared names retain loader conflict precedence. +- **Import:** a valid contiguous import in a blank source is a normal source + occurrence and graph edge. Missing or invalid recursive dependencies can + therefore fail before the parent checker. An ordinary blank source provider + cannot publish a successful interface or archive. A reached hand-authored or + corrupted `.wwi` that declares `_` is nevertheless parsed and defensively + rejected by the consuming compiler; an unused interface is inert. Default, + explicit, and blank aliases cannot mask the invalid provider name. + Visibility, vendor, cycle, and initialization rules do not change. + +The complete observable phase order is: source eligibility and loader-visible +header/family checks; recursive import loading; MainOnly rejection for `run`; +eligible dependency producers for build/test or a run root that passed +MainOnly; the parent compiler's complete-file syntax; BlankPkgName checking; +reached imported-package validation; later checker diagnostics; then existing +driver/coordinator failure trailers. +Consequently malformed contiguous imports precede graph construction, a +missing dependency can suppress both a later body syntax error and +BlankPkgName, and full-file syntax suppresses checker diagnostics. A blank run +root never starts even valid dependency producers because MainOnly is earlier. + +#### Test graph, actions, and identity boundaries + +A production-only blank package under `ww test` has one ordinary production +compiler action. Production `_` plus a same-package `_test.ww` also declared +`_` forms one internal-test family: its augmented action owns both source sets, +and `sep_recompile_for_test` substitutes it throughout the product closure, so +the separate production node is not independently compiled for that product. +One compiler invocation reports one BlankPkgName per retained marker. A +test-only blank `_test.ww` forms one test-only internal action and reports once. +A blank production plus the actually related external name `__test` passes +family classification, but its blank production dependency fails before the +external action, generated main, link, or runtime can complete. + +Direct raw files retain `__root.*`; dotted directories and providers retain +their canonical dotted package/import/action/artifact identities; production, +internal, external, recompiled, support, and generated-main actions retain +their existing distinctions. Physical paths, parents, source spellings, and +symlink targets remain loader or diagnostic observations. No successful +ordinary producer `.wwi` can advertise `_`; a supplied or corrupted `.wwi` +that does is invalid input, not an alternate symbol, publication, or +persistence identity. + +#### Imported `.wwi` validation, reachability, and recovery + +The rules in this subsection are **behavior derived from the pinned +implementation** for WW's supported source-like export-data channel. They do +not claim an official imported-blank fixture that does not exist. + +Every sorted `--import CANONICAL FILE.wwi` is first read, owner-checked, and +syntax-parsed into its own standalone AST. Interface read, owner, syntax, and +structural import-map errors keep their existing precedence. The compiler then +syntax-parses the primary input. Any primary syntax error returns before +imported-package semantic validation, so it is never accompanied by the +broken-import diagnostic. + +After successful primary parsing and import-map application, compiler test +mode materializes any required support `N_USE` before resolution. The node has +the primary owner/source ID, is marked used, uses visible name `test` or the +collision-safe reserved `__wwtest`, and is positioned at the generated primary +root because no source path token exists. An existing equivalent primary +occurrence prevents duplication. When no support interface is supplied, the +node retains the established raw external-support fallback; a supplied +blank-named support interface is validated like every source import. Reserved +`__wwtest` preserves its compiler-selected visible spelling after valid +resolution but cannot skip provider-name validation. Existing +`--test-target-package` roots already require a primary occurrence and add no +second node. + +A metadata-only pass classifies each represented canonical package as valid, +missing, conflicting, or invalid; invalid means its one nonconflicting real +declared name is exactly `_`. Compiler-private placeholder packages remain +valid recovery metadata. Reachability is seeded only by canonical primary and +compiler-required `N_USE.usepath` occurrences, then reaches a fixed point over +standalone interface lists. An imported use contributes an edge only when its +owning canonical package is already reached and valid. Interface containers, +arbitrary embedded origins, and invalid, missing, conflicting, or unreachable +owners are never roots or traversal sources. + +Before binding or concatenation, each standalone list is filtered to nodes +whose canonical owner is both reached and valid. Invalid-owner, unreachable, +and ownerless hand-authored facts are discarded. Thus an unused invalid +interface is wholly inert even when it contains an embedded valid-origin +section that imports the invalid path: it emits no diagnostic, installs no +declaration or scope, contributes no output or serialized fact, and leaves an +otherwise valid primary byte-equivalent to the no-interface control. If the +primary independently reaches that valid origin, its retained import can then +reach and diagnose the invalid provider. Binding runs on those filtered lists +and the primary list before the lists are concatenated, so unreachable +internal uses cannot manufacture missing, conflict, or invalid effects. + +Every retained occurrence resolving to an invalid provider is marked used and +records declared provider name `_`. A nonblank occurrence receives a recovery +package binding using its explicit alias or, by default, the final component of +the canonical dotted path; a blank occurrence installs no visible binding. +The fake package has an empty scope. Qualified value, call, and type gateways +therefore recover as the error type without missing-member, +unknown-type/export, or calling-nonfunction cascades, while a lexically closer +value binding still shadows the recovery qualifier normally. + +At checker entry, immediately after primary BlankPkgName diagnostics, the +first retained invalid occurrence of each canonical path emits exactly +`could not import PATH (invalid package name: "_")`; later occurrences of the +same path are deduplicated, while distinct paths diagnose in retained +occurrence order. Independent checker diagnostics continue afterward. +Deduplication uses canonical dotted path because WW has no Go source-directory +import-key component. Default, explicit, and blank alias forms all position +this diagnostic at the path's first identifier, never at an explicit alias. +The existing alias-or-path first-spec position remains unchanged for every +other binding diagnostic. Full parsing, imports-only parsing, and named-source +header parsing all retain both position families. + +Public persistent build and test actions consume the same supported interface +channel. If a reached caller-owned committed `.wwi` is corrupted to declare +`package _;`, the consumer compiler fails after primary syntax and resolution; +that action's assembler and downstream archive, link, retention, or runtime do +not complete. A package-action failure prevents generated main; a generated-main +action that is itself the consumer performs the same validation before its own +assembly. Raw, production, internal, external, test-only, generated-main, `-c`, +and applicable `-S` consumer actions use the same rule. A same-named `.ww` file +remains an import decoy rather than a provider. An +unreferenced corrupt interface remains inert and does not invalidate or alter +the consumer. A failing direct compiler returns status 1 with empty stdout; +public test presentation retains its existing running `FAIL` and +package-trailer rules, while compile-only and assembly-only forms retain empty +stdout. + +#### Artifacts, rollback, concurrency, parity, and formats + +The failed primary blank action and a consumer rejecting a reached blank-named +interface emit neither compiler assembly nor `.wwi`, so their downstream +assembler, archiver, linker, test harness, and user runtime do not run. Valid +dependencies or test support that precede either failure may execute their +ordinary producers, but request rollback removes every request-owned stage and +commits no failed generation, public product, retained test binary, unit, +interface, assembly, object, archive, executable, stamp, or status. Direct +named `-o`/`-I` outputs, public outputs, retained tests, and committed semantic +bytes remain byte-identical. Restoring the exact valid source or `.wwi` bytes +uses the existing content-identity reuse path; invalid bytes never publish a +replacement consumer generation. Reached interface bytes already participate +through the existing compiler input and invalidation rules; semantic validation +adds no graph, action, or persistence key. A build-omitted named `_test.ww` +contributes no action or invalidation key. + +Blank-package state, imported metadata, reachability sets, deduplication, and +fake bindings are compiler/checker-process-local. Independent concurrent +requests cannot share diagnostics, graph state, staging, or cleanup. Normal +failure is waited and rolled back through the existing transaction owner and +leaves no anonymous descriptor, `.new`, `.old`, `.install`, `.wwtxn.*`, +capture, result, request scratch, or child. +Direct `w6c` and `w6c_ww`, and public `ww` and `ww_ww`, agree on status, +stdout, exact path-token positions and diagnostic order, fake recovery, +output absence, prior-byte preservation, and every comparable semantic +dependency artifact. Producer provenance remains the established intentional +stage difference. + +No signal-supervision behavior changed. Direct external `SIGTERM` during a +blocked persistent compilation still preserves prior committed/public bytes +but can leave the spawned compiler and fixed-name `.new` staging that poisons a +later request. That independently verified gap remains open and is not claimed +fixed by normal BlankPkgName rollback. + +No serialized representation changed. The full parser changes only the +in-memory position of a blank package marker to the underscore token; outer +file/header/import-only markers and valid package markers keep their former +positions. Each twin's private in-memory `Node` gains only +`usepathfile/usepathline/usepathcol`; AST enum values, AST printing, `.wwi` +schema, build workdir format `18`, test workdir format `19`, and semantic +storage format `3` remain unchanged. No cache, result record, manifest, action +descriptor, transaction marker, database, or lock is added. This closes one +coherent semantic slice across all four axes; +it does not complete the remaining suffix-first run front, multiple named +source packages, shared test-process state and failure topology, RE2-compatible +flat `-run`, finite special-source handling, or external-driver interruption +recovery. + ## 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 399ba896..213d6add 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -273,7 +273,8 @@ does not return (e.g. a call to `abort`). ``` SourceFile = PackageClause { ImportDecl } { TopDecl } . -PackageClause = "package" ident ";" . +PackageClause = "package" PackageName ";" . +PackageName = ident . ImportDecl = "import" ( ImportPath | ImportName ImportPath ) ";" . ImportName = ident . ImportPath = ident { "." ident } . @@ -285,6 +286,101 @@ ImportPath = ident { "." ident } . use the related `p_test`, and the actions remain separate even though one canonical directory owns their test product. The declared name need not equal the directory name or the final component of its canonical import identity. +- The discard identifier `_` is syntactically valid as `PackageName`, but it + is never a valid declared package name. A complete source with + `package _;` reaches checker initialization, which reports exactly + `invalid package name _` at the underscore token and continues checking the + retained file. The package-clause parser admits the discard token only in + this grammar slot; no other identifier position is broadened. A malformed or + missing package name remains a parser error, and any complete-file syntax + error prevents this checker diagnostic. + + Loading may retain `_` transiently to compare declared source families, + record imports, and construct the applicable action, but it is not canonical + package, import, graph, action, symbol, `.wwi`, artifact, publication, or + persistence identity. Direct sources retain `__root`; dotted directories + and providers retain their dotted identities. Selected files with distinct + declared names remain a loader/family conflict. In an all-blank action the + checker emits one blank-name diagnostic per retained package marker in + deterministic source order. + + Observable ordering is source eligibility and loader-visible header/family + validation, recursive import loading, the `ww run` main-package check, + eligible dependency producers, complete parent-source parsing, then the + blank-name, reached imported-package, and later checker diagnostics. Thus + missing or invalid imports may precede the parent check; a blank run root is + not `main` and starts no producer; and a full-source syntax error suppresses + the blank-name error. An + ordinary blank source provider cannot publish an interface or archive for an + importer. A supplied or caller-corrupted `.wwi` can nevertheless contain + that spelling and is defensively validated when reached. + + Import interfaces are read, owner-checked, and syntax-parsed as separate + lists before the primary source is parsed. Interface structural errors keep + their existing precedence, but a primary syntax error returns before + imported-package semantic checking and therefore suppresses every + blank-provider import diagnostic. After successful primary syntax, compiler + test mode first materializes its required `test` or collision-safe + `__wwtest` support occurrence unless an equivalent primary occurrence + exists. With no matching interface that occurrence retains the external + support fallback; with a matching interface its provider name is validated, + and the reserved spelling does not bypass the check. + + Each represented canonical interface package is classified as valid, + missing, conflicting, or invalid, where invalid means one nonconflicting + real declared name `_`. Imported-interface reachability is rooted only at + canonical uses in the primary and compiler-required lists. A reached valid + interface owner may contribute its imported uses transitively; an invalid, + missing, conflicting, unreachable, or ownerless section may not. Before + interface facts are bound or merged, every declaration and use whose owner + is not both reached and valid is discarded. An unused invalid interface is + therefore wholly inert, even when it embeds a valid-origin section that + imports the invalid path: it emits no diagnostic, installs no scope or + declaration, changes no output, and is byte-equivalent to supplying no such + interface. If primary source separately reaches that valid origin, its + retained edge may legitimately reach and diagnose the invalid provider. + + A retained use of an invalid provider is marked used. A nonblank use receives + a fake empty-scope package binding under its explicit alias or, without one, + the canonical path leaf; a blank use creates no visible binding. This + recovery prevents qualified values, calls, and types from producing + missing-member, unknown-type, export, or calling-nonfunction cascades. + Ordinary lexical shadowing of a nonblank recovery alias still applies. + + Immediately after primary blank-name diagnostics, the first retained use of + each invalid canonical path reports exactly + `could not import PATH (invalid package name: "_")`; later uses of that path + are deduplicated, distinct paths retain occurrence order, and independent + checker errors continue. Default, explicit, and blank import forms all + position this error at the path's first identifier rather than at the alias. + Canonical path, source spelling, alias, declared provider name, owner marker, + placeholder, edge, and binding remain separate facts. A physical `.wwi` + path is observation metadata and a same-named `.ww` file remains an import + decoy. + + Ordinary `ww build` still omits a valid visible literal `*_test.ww` after + header loading, so `package _;` in that omitted role has no action or + diagnostic. Under `ww test`, production plus a same-package blank test forms + one augmented internal-test action; the test recompile substitutes for the + separate production node and one compiler invocation diagnoses every + retained blank marker. A test-only blank source has one test-package action. + Mixed valid/blank production and test names retain family-mismatch + precedence, and no failed blank product reaches generated main, link, or + runtime. + + Normal blank-package failure publishes no assembly, interface, object, + archive, executable, retained test, or new semantic generation. Existing + request rollback removes owned stages and preserves prior public and + committed bytes; a public build or test consuming a reached corrupted + committed interface likewise publishes no replacement consumer generation, + retained test, or downstream artifact. Exact valid-interface restoration + follows ordinary reuse. Independent requests share no blank-name, + reachability, deduplication, or fake-binding state. Cstage and WWstage have + the same path-positioned diagnostic stream and artifact outcome. The private + in-memory AST adds only path-position fields; AST enum/printing, `.wwi` + schema, build workdir format 18, test workdir format 19, and semantic storage + format 3 do not change. External driver interruption is unchanged; the fixed + `.new` residue and later persistent request poisoning remain open. - Each source file has one contiguous import section immediately after its package clause. Once a non-import top-level declaration begins, a later `import` is rejected as `imports must appear before other declarations`. @@ -643,10 +739,10 @@ ImportPath = ident { "." ident } . interruption preserve existing directory contents and remove only request-created prefixes and stages. Direct external `SIGTERM` of a build driver is a verified-open exception: both stages preserve public and - committed work bytes and reap their process group, but may leave `.new` - staging files that make a later persistent-work request reject until those - files are removed. A non-directory output retains the single-product - file/archive rule. + committed work bytes, but leave the directly spawned compiler alive and + exactly three fixed-name `.new` staging files; the existing `.unit.new` + makes a later persistent-work request reject. A non-directory output retains + the single-product file/archive rule. If a lone command's synthesized default basename already names a directory, loading and graph validation complete and the build rejects before tools without diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 5b8a1e16..a53afb92 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -339,6 +339,146 @@ Build workdir format remains 18, test workdir format remains 19, and semantic storage format remains 3; no test-result cache, schema, action descriptor, transaction marker, or lock is introduced. +The blank declared package name has a different contract from documentation +suppression. The sole authority is official Go 1.26.5 at commit +`c19862e5f8415b4f24b189d065ed739517c548ba`: + +- **behavior directly implemented or asserted by pinned Go** — the compiler + scanner admits and dispatches `_` through its name path + (`cmd/compile/internal/syntax/scanner.go:88–107,368–394,437–439`), the parser + accepts it in package syntax (`parser.go:397–420,2751–2763`), and types2 + rejects the retained node as `invalid package name _` + (`cmd/compile/internal/types2/check.go:336–355`). +- **behavior directly implemented or asserted by pinned Go** — official + `test/blank1.go:1–31` asserts the blank-name error and later checker errors, + proving continuation. `internal/types/testdata/check/blank.go:1–5` asserts + only the blank-name error. `cmd/go/testdata/script/build_test_only.txt:1–18` + and `build_no_go.txt:1–30` are surrounding source-role controls. +- The official tree contains no cmd/go blank-name matrix for named, multiple, + mixed, imported, test-only, action, artifact, rollback, concurrency, or + interruption cases; the focused WW package observer owns those proofs. +- **behavior directly implemented or asserted by pinned Go** — types2 + validates an importer-supplied package object before applying its local + alias. Provider name `_` emits + `could not import PATH (invalid package name: "_")` at the source import + path, caches a path-leaf-named fake package, marks the use, and continues + (`cmd/compile/internal/types2/resolver.go:125–180,248–335`; public twin + `go/types/resolver.go:157–190,263–350`). An empty provider name quotes the + actual empty value instead. +- No official pinned test directly supplies an imported `Package` named `_`. + **behavior derived from the pinned implementation** — the + imported-interface rows are WW-native proof of applying those resolver + semantics to WW's supported source-like interface channel. + +The slice is not owned only by the four primary parser/checker files. The two +`w6c` command fronts own test-support materialization, imported-package +metadata, reachability/filtering, fake qualifier assignment, and delayed AST +concatenation; the checker twins own deduplicated diagnostics and empty-scope +fake recovery; the AST/parser twins own the independent path-token position. +The package coordinator and public drivers exercise the same interface channel +without redefining package or action identity. + +The coordinator treats `_` as a syntactically loaded declared name, not a +generic missing-clause error. A production-only blank package under `ww test` +selects its one ordinary production compiler action and fails there. Blank +production plus a same-package blank `_test.ww` selects one augmented internal +test action containing both source sets. `sep_recompile_for_test` substitutes +that action for the separate production node throughout the product closure, +so it is compiled once and emits one `invalid package name _` per retained +source marker in deterministic unit order. A test-only blank `_test.ww` forms +one test-only internal action and reports once. Valid production `p` plus blank +test `_`, or blank production `_` plus an unrelated `p` or `p_test`, keeps the +existing family-mismatch rejection before tools. Blank production plus the +actually related external name `__test` passes family classification, but the +production dependency fails before the external action, support/generated +main, link, or runtime completes. + +The observable test phase order is selected-source validation and package/test +family classification, recursive import loading, eligible dependency and +support producers, complete parent-source parsing, blank-name checking, +imported-package validation, later checker diagnostics, and the +existing package failure trailer. A missing or invalid dependency may +therefore suppress both a later body syntax diagnostic and BlankPkgName; a +complete-file syntax error suppresses checker diagnostics. +Raw and directory running requests keep their established command-owned +`FAIL\n` placement, while `-c` and applicable `-S` paths retain empty stdout. +Directory `-S` option-shape errors remain earlier than source selection. No +blank product begins a test or user runtime. + +The imported-interface observer uses the supported repeatable +`w6c -c --import PATH FILE.wwi` channel and its public persistent build/test +equivalent. Each compiler first read/owner/syntax-checks sorted standalone +interfaces, then parses primary source. Primary syntax failure precedes +imported-package semantic validation. After successful primary syntax, `-T` +materializes a missing compiler-required `test` or collision-safe `__wwtest` +use in the primary list before resolution. It is marked used and rooted at the +generated primary file position. With no matching import it keeps the existing +external-support fallback; with a matching blank-named interface it receives +the ordinary invalid-provider diagnostic. Existing explicit test-target roots +already have a primary occurrence and are not duplicated. + +The observer requires metadata classification as valid, missing, conflicting, +or invalid, followed by reachability seeded solely by canonical primary and +compiler-required uses. Only a reached valid interface owner may contribute +transitive imports. Before binding and delayed concatenation, invalid-owner, +unreachable, and ownerless standalone facts are removed. The unused-interface +control includes a mixed-origin invalid interface whose embedded valid origin +imports the bad path: because that origin is not independently rooted, the +whole interface is inert and the primary outputs equal the no-interface +control. A companion primary-reached origin proves that legitimate traversal +does reach and diagnose the bad provider. + +For every retained bad use, default, explicit, and blank aliases all validate +the provider name. Nonblank uses receive an explicit-alias or canonical-path- +leaf fake binding with empty scope; blank uses install none. All are marked +used. Qualified value/call/type recovery produces no missing-member, +unknown-type, export, or calling-nonfunction cascade, while lexical shadowing +still applies. One canonical bad path emits one diagnostic despite repeated +occurrences; two paths emit one each in retained order, and an independent +checker diagnostic follows. Every source-created form points at the path's +first identifier, not the explicit alias. The compiler-generated support use +uses its generated root position because no path token exists. + +Public proof corrupts a caller-owned committed provider `.wwi`, then exercises +build and test consumers through the existing canonical interface action. +Reached corruption fails the affected compiler action before its assembler and +downstream archive, link, retention, or runtime. A package-action failure +prevents generated main; a generated-main action that is itself the consumer +performs the same validation before its assembly. A same-named `.ww` file is +still an import decoy. Raw, production, internal, external, test-only, +generated-main, `-c`, and applicable `-S` shapes share the same validation. +Prior public, retained, and semantic bytes survive, invalid bytes cannot commit +a replacement consumer, and restoring valid interface bytes follows ordinary +reuse. An unreferenced corrupt interface remains inert and causes no +invalidation. + +The package-name token does not alter production/internal/external/recompiled, +support, or generated-main identities. Direct actions retain `__root`, dotted +actions retain dotted identity, and no successful ordinary producer interface +can advertise a blank provider. A supplied or corrupted interface that does is +invalid consumer input. A failed primary blank action or reached invalid +interface consumer emits no assembly or interface, so its assembler, archiver, +linker, generated main, harness, and runtime do not run; earlier valid +dependency/support producers may run normally. Request rollback removes owned +stages and commits no failed unit, interface, assembly, object, archive, binary, +status, retained output, or tool stamp. Warm prior public and semantic bytes +remain unchanged, exact source/interface restoration uses ordinary reuse, and +concurrent requests keep parser/checker reachability, deduplication, fake +bindings, and cleanup independent. + +Direct `w6c`/`w6c_ww` and public `ww`/`ww_ww` are required to agree on status, +stdout, exact path-token positions and diagnostic order, fake recovery, output +absence, and prior-byte preservation for the full role/import matrix. The +syntax/check change adds no test-process topology, test-result cache, schema, +action descriptor, or stored identity. Private in-memory `N_USE` nodes gain +only path-position fields; AST enum/printing and `.wwi` serialization are +unchanged. Build workdir format remains 18, test workdir format remains 19, and +semantic storage format remains 3. Normal checker failure leaves no anonymous +descriptor, adjacent stage, request residue, or child. External-driver SIGTERM +supervision is unchanged: the known orphan compiler, fixed `.new` staging, and +later persistent-request poisoning remain open and are not credited to this +slice. + An existing local directory whose requested build basename ends `.ww` (including a visible `_test.ww` symlink to a directory) remains a directory package, not a raw named test source. WWstage `ww build` now uses the same @@ -1144,8 +1284,15 @@ timeout policy in this architecture. ## Open driver work -None; directory-package `-c` and `-o` now have the applicable Go 1.26.5 -retention, naming, fan-out, execution, and publication behavior. +Directory-package `-c` and `-o` have the applicable Go 1.26.5 retention, +naming, fan-out, execution, and publication behavior. Direct external SIGTERM +of either driver during blocked persistent compilation remains open: prior +public and committed semantic bytes survive, but the directly spawned compiler +can survive with three fixed-name `.new` stages, and a later persistent request +then rejects the existing `.unit.new`. The required repair is driver-owned +child-process-group supervision and normal request rollback before re-raising +the original signal; blind removal of `.new` files cannot distinguish foreign +or concurrent stages. ## Validation policy diff --git a/lib/ww/syntax/ast.ww b/lib/ww/syntax/ast.ww index afa00298..8e328cbf 100644 --- a/lib/ww/syntax/ast.ww +++ b/lib/ww/syntax/ast.ww @@ -133,6 +133,9 @@ export type node = struct { usefile: str, // N_USE: first spec token (alias, otherwise path) useline: i32, usecol: i32, + usepathfile: str,// N_USE: first path token, independent of alias + usepathline: i32, + usepathcol: i32, usepkgname: str,// N_USE: imported declared package name useblank: i32, // N_USE: `_` spelling; no source binding pkgname: str, // declared package name; independent of canonical nmod @@ -153,7 +156,7 @@ export fn newnode(k: nkind, file: str, line: i32, col: i32) *node = { // fval cast-init: 990's wwdump TK_FLOAT diff requires this file // to tokenise identically through C and ww (lex.ww:382 has the // same workaround for the cstage %g-formats vs ww-skips divergence). - let n: *node = alloc(node{kind=k, file=file, line=line, col=col, op=tkind.TK_NONE, str="", uval=0u64, fval=0: f64, lhs=nil, rhs=nil, cond=nil, body=nil, els=nil, list=nil, next=nil, attr=nil, exported=0, packed=0, type_=nil, tsuffix="", nmod="", usesource="", usepath="", usealias="", usefile="", useline=0, usecol=0, usepkgname="", useblank=0, pkgname="", sourceid=0, used=0, initfn=0, initsynthetic=0, runtimeinit=0, initorder=0u64, linksym="", refdecl=nil, initmark=0u64, imported=0})!; + let n: *node = alloc(node{kind=k, file=file, line=line, col=col, op=tkind.TK_NONE, str="", uval=0u64, fval=0: f64, lhs=nil, rhs=nil, cond=nil, body=nil, els=nil, list=nil, next=nil, attr=nil, exported=0, packed=0, type_=nil, tsuffix="", nmod="", usesource="", usepath="", usealias="", usefile="", useline=0, usecol=0, usepathfile="", usepathline=0, usepathcol=0, usepkgname="", useblank=0, pkgname="", sourceid=0, used=0, initfn=0, initsynthetic=0, runtimeinit=0, initorder=0u64, linksym="", refdecl=nil, initmark=0u64, imported=0})!; return n; }; diff --git a/lib/ww/syntax/decl.ww b/lib/ww/syntax/decl.ww index 3b74672f..6e6292e6 100644 --- a/lib/ww/syntax/decl.ww +++ b/lib/ww/syntax/decl.ww @@ -19,19 +19,31 @@ fn parseuse(p: *parser) *node = { n.usefile = p.curfile; n.useline = p.curline; n.usecol = p.curcol; + let pathfile: str = p.curfile; + let pathline: i32 = p.curline; + let pathcol: i32 = p.curcol; let alias: str; let first: str; if (p.curkind == tkind.TK_UNDER) { n.useblank = 1; advance(p); + pathfile = p.curfile; + pathline = p.curline; + pathcol = p.curcol; expectident(p, &first); } else { expectident(p, &first); if (p.curkind == tkind.TK_IDENT) { alias = first; + pathfile = p.curfile; + pathline = p.curline; + pathcol = p.curcol; expectident(p, &first); }; }; + n.usepathfile = pathfile; + n.usepathline = pathline; + n.usepathcol = pathcol; let leaf: str = first; let path: str = leaf; for (p.curkind == tkind.TK_DOT) { diff --git a/lib/ww/syntax/parse.ww b/lib/ww/syntax/parse.ww index 6764aa73..3df2af27 100644 --- a/lib/ww/syntax/parse.ww +++ b/lib/ww/syntax/parse.ww @@ -145,6 +145,19 @@ fn expectident(p: *parser, into: *str) bool = { return true; }; +// Package declarations admit the blank identifier syntactically. Keep this +// private to the package-name slot; the checker owns BlankPkgName rejection. +fn expectpackagename(p: *parser, into: *str) bool = { + if (p.curkind != tkind.TK_IDENT && p.curkind != tkind.TK_UNDER) { + errmsg(p, strings.concat("expected identifier, got ", + tokname(p.curkind))); + return false; + }; + *into = p.curtext; + advance(p); + return true; +}; + // On `_`, returns "" so the checker skips scope_define for the binding. fn expectbindname(p: *parser, into: *str) bool = { if (p.curkind == tkind.TK_UNDER) { @@ -513,11 +526,17 @@ fn parseheaderuse(p: *parser) *node = { n.usefile = p.curfile; n.useline = p.curline; n.usecol = p.curcol; + let pathfile: str = p.curfile; + let pathline: i32 = p.curline; + let pathcol: i32 = p.curcol; let alias: str; let first: str; if (p.curkind == tkind.TK_UNDER) { n.useblank = 1; advance(p); + pathfile = p.curfile; + pathline = p.curline; + pathcol = p.curcol; }; if (p.curkind != tkind.TK_IDENT) { errmsg(p, strings.concat("expected identifier, got ", @@ -528,9 +547,15 @@ fn parseheaderuse(p: *parser) *node = { advance(p); if (n.useblank == 0 && p.curkind == tkind.TK_IDENT) { alias = first; + pathfile = p.curfile; + pathline = p.curline; + pathcol = p.curcol; first = p.curtext; advance(p); }; + n.usepathfile = pathfile; + n.usepathline = pathline; + n.usepathcol = pathcol; let leaf: str = first; let path: str = leaf; for (p.curkind == tkind.TK_DOT) { @@ -585,12 +610,12 @@ export fn parsepackageheader(p: *parser) *node = { let pl: i32 = p.curline; let pc: i32 = p.curcol; advance(p); - if (p.curkind != tkind.TK_IDENT) { + if (p.curkind != tkind.TK_IDENT && p.curkind != tkind.TK_UNDER) { errmsg(p, "invalid or missing package clause"); return f; }; - let name: str = p.curtext; - advance(p); + let name: str; + expectpackagename(p, &name); if (p.curkind != tkind.TK_SEMI) { errmsg(p, "expected ';' after package name"); return f; @@ -659,14 +684,15 @@ export fn parseimports(p: *parser) *node = { let pc: i32 = p.curcol; previmport = true; advance(p); - if (p.curkind != tkind.TK_IDENT) { + if (p.curkind != tkind.TK_IDENT + && p.curkind != tkind.TK_UNDER) { errmsg(p, "invalid or missing package clause"); sawpackage = true; skipimportdecl(p); continue; }; let name: str; - expectident(p, &name); + expectpackagename(p, &name); expecttok(p, tkind.TK_SEMI, "expected ';' after module name"); p.curpkg = name; if (p.pathmod.len == 0 && p.resetmod.len == 0) { @@ -771,14 +797,25 @@ export fn parsefile(p: *parser) *node = { sawpackage = 1; previmport = true; advance(p); + let nf: str = p.curfile; + let nl: i32 = p.curline; + let nc: i32 = p.curcol; let name: str; - expectident(p, &name); + expectpackagename(p, &name); expecttok(p, tkind.TK_SEMI, "expected ';' after module name"); p.curpkg = name; if (p.pathmod.len == 0 && p.resetmod.len == 0) { p.curmod = name; }; - let pm: *node = newnode(nkind.N_FILE, pf, pl, pc); + let mf: str = pf; + let ml: i32 = pl; + let mc: i32 = pc; + if (streq(name, "_")) { + mf = nf; + ml = nl; + mc = nc; + }; + let pm: *node = newnode(nkind.N_FILE, mf, ml, mc); pm.nmod = p.curmod; pm.pkgname = name; pm.sourceid = p.sourceid; diff --git a/selfhost/cmd/w6c/main.ww b/selfhost/cmd/w6c/main.ww index 3efe3924..b0aea289 100644 --- a/selfhost/cmd/w6c/main.ww +++ b/selfhost/cmd/w6c/main.ww @@ -360,15 +360,23 @@ fn canonicalpkgname(path: str, name: str) bool = { return true; }; -// A paired interface owns PATH's declared name. Compiler-private names keep -// transitive fact sections semantic and are ignored when a real name exists. +// Package-marker facts are canonical-origin keyed across all supplied +// interfaces; an interface may embed transitive origins besides its container +// owner. Compiler-private names remain recovery metadata and are ignored when +// a real declared name exists. +type pkgnamestate = enum i32 { + PKGNAME_MISSING = 0, + PKGNAME_VALID = 1, + PKGNAME_CONFLICT = 2, + PKGNAME_INVALID = 3, +}; + fn importpkgname(asts: []*syntax.node, paths: []*u8, nasts: i32, path: str, - primary: *syntax.node, conflict: *bool) str = { + primary: *syntax.node, resolved: *str) pkgnamestate = { let name: str; let placeholder: str; let i: i32 = 0; for (i < nasts) { - if (!cstreq(paths[i], path)) { i += 1; continue; }; let f: *syntax.node = asts[i]; let p: *syntax.node = nil; if (f != nil) { p = f.body; }; @@ -379,9 +387,7 @@ fn importpkgname(asts: []*syntax.node, paths: []*u8, nasts: i32, path: str, placeholder = p.pkgname; } else { if (name.len > 0 && !syntax.streq(name, p.pkgname)) { - *conflict = true; - let empty: str; - return empty; + return pkgnamestate.PKGNAME_CONFLICT; } else { name = p.pkgname; }; }; }; p = p.next; @@ -396,45 +402,70 @@ fn importpkgname(asts: []*syntax.node, paths: []*u8, nasts: i32, path: str, if (canonicalpkgname(path, p.pkgname)) { placeholder = p.pkgname; } else { if (name.len > 0 && !syntax.streq(name, p.pkgname)) { - *conflict = true; - let empty: str; - return empty; + return pkgnamestate.PKGNAME_CONFLICT; } else { name = p.pkgname; }; }; }; p = p.next; }; if (name.len == 0) { name = placeholder; }; - return name; + *resolved = name; + if (name.len == 0) { return pkgnamestate.PKGNAME_MISSING; }; + if (syntax.streq(name, "_")) { return pkgnamestate.PKGNAME_INVALID; }; + return pkgnamestate.PKGNAME_VALID; +}; + +fn pathleaf(path: str) str = { + let start: i32 = 0; + let i: i32 = 0; + for (i < path.len) { + if (path[i] == '.') { start = i + 1; }; + i += 1; + }; + let leaf: str; + leaf.ptr = path.ptr + (start: u64); + leaf.len = path.len - start; + return leaf; }; fn bindimportnames(list: *syntax.node, asts: []*syntax.node, paths: []*u8, nasts: i32, - primary: *syntax.node, testsupport: *u8) bool = { + primary: *syntax.node, testsupport: *u8, + externalsupport: *syntax.node) bool = { let u: *syntax.node = list; for (u != nil) { if (u.kind == syntax.nkind.N_USE && u.usepath.len > 0) { let reserved: bool = testsupport != nil && cstreq(testsupport, "__wwtest") && syntax.streq(u.usepath, "__wwtest"); - if (!reserved) { - let conflict: bool = false; - let name: str = importpkgname(asts, paths, nasts, u.usepath, - primary, &conflict); - if (conflict) { + let name: str; + let state: pkgnamestate = importpkgname(asts, paths, nasts, + u.usepath, primary, &name); + if (state == pkgnamestate.PKGNAME_CONFLICT) { let pre: str = "w6c: package "; let post: str = " has conflicting declared names in export data\n"; os.write(2, pre.ptr, pre.len: u64); os.write(2, u.usepath.ptr, u.usepath.len: u64); os.write(2, post.ptr, post.len: u64); return false; - }; - if (name.len > 0) { - u.usepkgname = name; - if (u.useblank == 0) { + }; + if (state == pkgnamestate.PKGNAME_VALID + || state == pkgnamestate.PKGNAME_INVALID) { + u.usepkgname = name; + if (state == pkgnamestate.PKGNAME_INVALID) { + u.used = 1; + if (u.useblank == 0 && !reserved) { if (u.usealias.len > 0) { u.str = u.usealias; } - else { u.str = name; }; + else { u.str = pathleaf(u.usepath); }; }; - } else { if (u.imported == 0) { + } else { if (u.useblank == 0 && !reserved) { + if (u.usealias.len > 0) { u.str = u.usealias; } + else { u.str = name; }; + }; }; + } else { if (u == externalsupport) { + // A bare direct -T compiler invocation deliberately leaves the + // compiler-generated support hook external. + u.used = 1; + } else { if (u.imported == 0) { let pre: str = "w6c: import "; let post: str = " has no declared package name in direct export data\n"; os.write(2, pre.ptr, pre.len: u64); @@ -449,14 +480,114 @@ fn bindimportnames(list: *syntax.node, asts: []*syntax.node, paths: []*u8, if (u.usealias.len > 0) { u.str = u.usealias; } else { u.str = u.usepath; }; }; - }; }; - }; + }; }; }; }; u = u.next; }; return true; }; +fn pathsethas(paths: []str, path: str) bool = { + if (path.len == 0) { return false; }; + let i: i32 = 0; + for (i < paths.len) { + if (syntax.streq(paths[i], path)) { return true; }; + i += 1; + }; + return false; +}; + +fn pathsetadd(paths: *[]str, path: str) bool = { + if (path.len == 0 || pathsethas(*paths, path)) { return false; }; + append(*paths, path); + return true; +}; + +fn materializetestsupport(file: *syntax.node, testmode: i32, + testsupport: *u8) *syntax.node = { + if (testmode == 0 || testsupport == nil) { return nil; }; + let path: str = pathstr(testsupport); + let u: *syntax.node = file.list; + for (u != nil) { + if (u.kind == syntax.nkind.N_USE && u.imported == 0 + && u.sourceid == file.sourceid + && syntax.streq(u.usepath, path)) { + u.used = 1; + return nil; + }; + u = u.next; + }; + u = syntax.newnode(syntax.nkind.N_USE, file.file, file.line, file.col); + u.str = path; + u.usesource = path; + u.usepath = path; + u.usefile = file.file; + u.useline = file.line; + u.usecol = file.col; + u.usepathfile = file.file; + u.usepathline = file.line; + u.usepathcol = file.col; + u.pkgname = file.pkgname; + u.sourceid = file.sourceid; + u.used = 1; + u.next = file.list; + file.list = u; + return u; +}; + +fn computereachedimports(reached: *[]str, primary: *syntax.node, + asts: []*syntax.node, paths: []*u8, nasts: i32) void = { + let u: *syntax.node = nil; + if (primary != nil) { u = primary.list; }; + for (u != nil) { + if (u.kind == syntax.nkind.N_USE && u.imported == 0) { + pathsetadd(reached, u.usepath); + }; + u = u.next; + }; + let changed: bool = true; + for (changed) { + changed = false; + let i: i32 = 0; + for (i < nasts) { + u = nil; + if (asts[i] != nil) { u = asts[i].list; }; + for (u != nil) { + if (u.kind == syntax.nkind.N_USE && u.imported != 0 + && u.nmod.len > 0 && u.usepath.len > 0 + && pathsethas(*reached, u.nmod)) { + let name: str; + if (importpkgname(asts, paths, nasts, u.nmod, nil, + &name) == pkgnamestate.PKGNAME_VALID + && pathsetadd(reached, u.usepath)) { + changed = true; + }; + }; + u = u.next; + }; + i += 1; + }; + }; +}; + +fn filterreachedimports(file: *syntax.node, reached: []str, + asts: []*syntax.node, paths: []*u8, nasts: i32) void = { + let prev: *syntax.node = nil; + let d: *syntax.node = file.list; + for (d != nil) { + let next: *syntax.node = d.next; + let name: str; + let keep: bool = d.imported != 0 && d.nmod.len > 0 + && pathsethas(reached, d.nmod) + && importpkgname(asts, paths, nasts, d.nmod, nil, + &name) == pkgnamestate.PKGNAME_VALID; + if (keep) { prev = d; } + else { if (prev == nil) { file.list = next; } + else { prev.next = next; }; }; + d = next; + }; +}; + export fn main(argc: i32, argv: **u8) i32 = { let src: *u8 = nil; let out: *u8 = nil; @@ -758,8 +889,6 @@ export fn main(argc: i32, argv: **u8) i32 = { targeti += 1; }; - let importhead: *syntax.node = nil; - let importtail: *syntax.node = nil; importi = 0; for (importi < nimports) { let ibuf: *u8; @@ -800,15 +929,6 @@ export fn main(argc: i32, argv: **u8) i32 = { let imported: *syntax.node = syntax.parsefile(&ips); if (il.errs > 0 || ips.errs > 0) { return 1; }; importasts[importi] = imported; - if (!bindimportnames(imported.list, importasts, importpaths, importi + 1, - nil, testsupport)) { return 1; }; - let d: *syntax.node = imported.list; - if (d != nil) { - if (importhead == nil) { importhead = d; } - else { importtail.next = d; }; - for (d.next != nil) { d = d.next; }; - importtail = d; - }; importi += 1; }; @@ -860,17 +980,34 @@ export fn main(argc: i32, argv: **u8) i32 = { }; mapi += 1; }; + // Test compilation has a compiler-required support edge even when no + // equivalent source import occurrence exists. Make that primary root + // explicit before resolving interface metadata. + let externalsupport: *syntax.node = materializetestsupport(f, testmode, + testsupport); + // Standalone interfaces are metadata containers, never graph roots. Reach + // only from primary/compiler-required uses and traverse uses owned by an + // already-reached package whose declared name is valid. + let reached: []str = alloc([], 16u64)!; + computereachedimports(&reached, f, importasts, importpaths, nimports); + importi = 0; + for (importi < nimports) { + filterreachedimports(importasts[importi], reached, importasts, + importpaths, nimports); + importi += 1; + }; // A later direct interface may supply metadata for an origin section used - // by an earlier interface, so bind once more against the complete set. + // by an earlier interface. Bind each now-bounded reached list against the + // complete metadata set, before any concatenation can widen that list. importi = 0; for (importi < nimports) { if (!bindimportnames(importasts[importi].list, importasts, importpaths, nimports, - nil, testsupport)) { return 1; }; + nil, testsupport, nil)) { return 1; }; importi += 1; }; if (!bindimportnames(f.list, importasts, importpaths, nimports, f, - testsupport)) { + testsupport, externalsupport)) { return 1; }; targeti = 0; @@ -881,7 +1018,12 @@ export fn main(argc: i32, argv: **u8) i32 = { if (targetuse.kind == syntax.nkind.N_USE && targetuse.imported == 0 && syntax.streq(targetuse.usepath, testtargets[targeti])) { - targetuse.str = targetuse.usepath; + // Valid generated targets use their canonical path as the + // compiler-owned qualifier. Preserve an invalid provider's fake + // explicit-or-leaf spelling for source-local recovery. + if (!syntax.streq(targetuse.usepkgname, "_")) { + targetuse.str = targetuse.usepath; + }; seen += 1; }; targetuse = targetuse.next; @@ -893,6 +1035,19 @@ export fn main(argc: i32, argv: **u8) i32 = { }; targeti += 1; }; + let importhead: *syntax.node = nil; + let importtail: *syntax.node = nil; + importi = 0; + for (importi < nimports) { + let d: *syntax.node = importasts[importi].list; + if (d != nil) { + if (importhead == nil) { importhead = d; } + else { importtail.next = d; }; + for (d.next != nil) { d = d.next; }; + importtail = d; + }; + importi += 1; + }; if (importhead != nil) { importtail.next = f.list; f.list = importhead; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 5a2da2be..7d2ac97d 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -179,19 +179,18 @@ fn invalidinitimport(u: *syntax.node) bool = { && syntax.streq(u.str, "init"); }; -// Map a source-file qualifier to canonical identity. Looking up the marker for -// a possible DOT must not itself count as usage; only qualified resolution +// Map a source-file qualifier to its exact retained import occurrence. Looking +// up a possible DOT must not itself count as usage; only qualified resolution // marks the owning occurrence. -fn findusepath(file: *syntax.node, modtag: str, source: i32, alias: str, - mark: bool) str = { - let empty: str; - if (file == nil) { return empty; }; - if (alias.len == 0) { return empty; }; +fn findusebinding(file: *syntax.node, modtag: str, source: i32, alias: str, + mark: bool) *syntax.node = { + if (file == nil) { return nil; }; + if (alias.len == 0) { return nil; }; if (source == 0 && modtag.len != 0) { let (prefix, suffix) = strings.rcut(modtag, "."); let leaf: str = suffix; if (leaf.len == 0) { leaf = modtag; }; - if (syntax.streq(alias, leaf)) { return modtag; }; + if (syntax.streq(alias, leaf)) { return nil; }; }; let u: *syntax.node = file.list; for (u != nil) { @@ -204,15 +203,33 @@ fn findusepath(file: *syntax.node, modtag: str, source: i32, alias: str, if (modtag.len == 0) { if (um.len == 0) { same = true; }; } else { if (syntax.streq(um, modtag)) { same = true; }; }; - if (same) { - if (mark) { u.used = 1; }; - if (u.usepath.len != 0) { return u.usepath; }; - return u.str; + if (same) { + if (mark) { u.used = 1; }; + return u; }; }; }; u = u.next; }; + return nil; +}; + +fn findusepath(file: *syntax.node, modtag: str, source: i32, alias: str, + mark: bool) str = { + let empty: str; + if (file == nil) { return empty; }; + if (alias.len == 0) { return empty; }; + if (source == 0 && modtag.len != 0) { + let (prefix, suffix) = strings.rcut(modtag, "."); + let leaf: str = suffix; + if (leaf.len == 0) { leaf = modtag; }; + if (syntax.streq(alias, leaf)) { return modtag; }; + }; + let u: *syntax.node = findusebinding(file, modtag, source, alias, mark); + if (u != nil) { + if (u.usepath.len != 0) { return u.usepath; }; + return u.str; + }; return empty; }; @@ -298,6 +315,13 @@ fn localshadowsimport(c: *checker, name: str) bool = { return false; }; +fn fakeimportalias(c: *checker, alias: str) bool = { + if (localshadowsimport(c, alias)) { return false; }; + let u: *syntax.node = findusebinding(c.file, c.curmod, c.cursource, + alias, true); + return u != nil && syntax.streq(u.usepkgname, "_"); +}; + fn lookupvisible(c: *checker, name: str) *syntax.sym = { let found: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, name); let builtin: *syntax.sym = nil; @@ -726,6 +750,14 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { c.nresolved += 1; return; }; + if (strings.contains(nm, ".")) { + let (head, leaf) = strings.rcut(nm, "."); + if (fakeimportalias(c, head)) { + n.type_ = c.tc.tyerr: *void; + 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 @@ -2860,6 +2892,13 @@ fn tinfofornode(c: *checker, n: *syntax.node) *syntax.tinfo = { syntax.tinfocachebind(c.tc, n, c.tc.tyerr); return c.tc.tyerr; }; + if (n.file.len != 0 && strings.contains(nm, ".")) { + let (head, leaf) = strings.rcut(nm, "."); + if (fakeimportalias(c, head)) { + 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; }; @@ -4217,6 +4256,14 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { e.type_ = c.tc.tyerr: *void; return nil; }; + if (callee.kind == syntax.nkind.N_DOT && callee.lhs != nil + && callee.lhs.kind == syntax.nkind.N_IDENT + && fakeimportalias(c, callee.lhs.str)) { + callee.lhs.type_ = c.tc.tyerr: *void; + callee.type_ = c.tc.tyerr: *void; + 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 @@ -4738,6 +4785,12 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { // harec's enum-resolve constexpr set at // ref/harec/src/check.c:4419-4434. let lhsn: *syntax.node = e.lhs; + if (lhsn != nil && lhsn.kind == syntax.nkind.N_IDENT + && fakeimportalias(c, lhsn.str)) { + lhsn.type_ = c.tc.tyerr: *void; + e.type_ = c.tc.tyerr: *void; + return nil; + }; if (lhsn != nil) { if (lhsn.kind == syntax.nkind.N_IDENT) { let ms: *syntax.sym = lookupvisible(c, lhsn.str); if (ms != nil && ms.skind != syntax.skind.SK_USE) { @@ -7446,6 +7499,14 @@ fn exprtypeoftry(c: *checker, e: *syntax.node) *syntax.node = { // nkind.N_DOT). We need the fn-decl's lhs (return-type AST). let callee: *syntax.node = e.lhs; if (callee == nil) { return nil; }; + if (callee.kind == syntax.nkind.N_DOT && callee.lhs != nil + && callee.lhs.kind == syntax.nkind.N_IDENT + && fakeimportalias(c, callee.lhs.str)) { + callee.lhs.type_ = c.tc.tyerr: *void; + callee.type_ = c.tc.tyerr: *void; + e.type_ = c.tc.tyerr: *void; + return nil; + }; let nm: str; nm.ptr = nil; nm.len = 0; if (callee.kind == syntax.nkind.N_IDENT) { nm = callee.str; }; @@ -7837,6 +7898,20 @@ fn importbindingdiagprefix(n: *syntax.node) void = { cerr(strconv.i32tos(col, strconv.base.DEC)); cerr(": error: "); }; +fn importpathdiagprefix(n: *syntax.node) void = { + let file: str = n.usepathfile; + let line: i32 = n.usepathline; + let col: i32 = n.usepathcol; + if (file.len == 0) { + file = n.file; + line = n.line; + col = n.col; + }; + cerr(file); cerr(":"); + cerr(strconv.i32tos(line, strconv.base.DEC)); cerr(":"); + cerr(strconv.i32tos(col, strconv.base.DEC)); cerr(": error: "); +}; + fn importdiagalt(n: *syntax.node, name: str) void = { cerr("\t"); cerr(n.file); cerr(":"); cerr(strconv.i32tos(n.line, strconv.base.DEC)); cerr(":"); @@ -7860,6 +7935,35 @@ fn rejectinitimports(c: *checker, file: *syntax.node) void = { }; }; +fn rejectinvalidimports(c: *checker, file: *syntax.node) void = { + let u: *syntax.node = file.list; + for (u != nil) { + if (u.kind == syntax.nkind.N_USE + && u.usepath.len != 0 + && syntax.streq(u.usepkgname, "_")) { + u.used = 1; + let duplicate: bool = false; + let p: *syntax.node = file.list; + for (p != u) { + if (p.kind == syntax.nkind.N_USE && p.usepath.len != 0 + && syntax.streq(p.usepkgname, "_") + && syntax.streq(p.usepath, u.usepath)) { + duplicate = true; + break; + }; + p = p.next; + }; + if (!duplicate) { + importpathdiagprefix(u); + cerr("could not import "); cerr(u.usepath); + cerr(" (invalid package name: \"_\")\n"); + c.errs += 1; + }; + }; + u = u.next; + }; +}; + fn topdeclkind(d: *syntax.node) bool = { return d != nil && (d.kind == syntax.nkind.N_TYPEDECL || d.kind == syntax.nkind.N_DEF || d.kind == syntax.nkind.N_FNDECL @@ -9051,10 +9155,25 @@ fn checktesttarget(c: *checker, path: str) bool = { return false; }; +fn rejectblankpackagenames(c: *checker, file: *syntax.node) void = { + let p: *syntax.node = file.body; + for (p != nil) { + if (p.kind == syntax.nkind.N_FILE + && syntax.streq(p.pkgname, "_")) { + importdiagprefix(p); + cerr("invalid package name _\n"); + c.errs += 1; + }; + p = p.next; + }; +}; + fn checkfile(c: *checker, file: *syntax.node) void = { if (file == nil) { return; }; if (file.kind != syntax.nkind.N_FILE) { return; }; c.file = file; + rejectblankpackagenames(c, file); + rejectinvalidimports(c, file); // Under -T, prepend the dispatcher support import before Pass 1 so // declmod keys the runner under the selected support module. This is a diff --git a/test/package/package_test.ww b/test/package/package_test.ww index e7aefc51..18ae009e 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -1047,6 +1047,1353 @@ fn wrongsuffixlogicalcapture(root: str, label: str, stage: str, clean(root); }; +fn blankpackagebaseenv() []str = { + let inherited: []str = os.getenvs(); + let env: []str = alloc([], inherited.len: u64)!; + let i: i32 = 0; + for (i < inherited.len) { + if (!strings.hasprefix(inherited[i], "WW_W6C=") + && !strings.hasprefix(inherited[i], "WW_W6A=") + && !strings.hasprefix(inherited[i], "WW_W6L=") + && !strings.hasprefix(inherited[i], "WW_BLANK_PACKAGE_")) { + append(env, inherited[i]); + }; + i += 1; + }; + return env; +}; + +fn blankpackageenv(compilerwrapper: str, assemblerwrapper: str, + linkerwrapper: str, compiler: str, assembler: str, linker: str, + ctrace: str, atrace: str, ltrace: str) []str = { + let env: []str = blankpackagebaseenv(); + append(env, strings.concat("WW_W6C=", compilerwrapper)); + append(env, strings.concat("WW_W6A=", assemblerwrapper)); + append(env, strings.concat("WW_W6L=", linkerwrapper)); + append(env, strings.concat("WW_BLANK_PACKAGE_CTRACE=", ctrace)); + append(env, strings.concat("WW_BLANK_PACKAGE_ATRACE=", atrace)); + append(env, strings.concat("WW_BLANK_PACKAGE_LTRACE=", ltrace)); + append(env, strings.concat("WW_BLANK_PACKAGE_REAL_C=", compiler)); + append(env, strings.concat("WW_BLANK_PACKAGE_REAL_A=", assembler)); + append(env, strings.concat("WW_BLANK_PACKAGE_REAL_L=", linker)); + return env; +}; + +fn blankpackageresettraces(ctrace: str, atrace: str, ltrace: str) void = { + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); +}; + +fn blankpackagenoresidue(root: str) void = { + assert(!wrongsuffixpathfragment(root, ".new") + && !wrongsuffixpathfragment(root, ".old") + && !wrongsuffixpathfragment(root, ".wwtxn.") + && !wrongsuffixpathfragment(root, ".install") + && !wrongsuffixpathfragment(root, ".sepwork") + && !wrongsuffixpathfragment(root, ".capture") + && !wrongsuffixpathfragment(root, ".result") + && !wrongsuffixpathfragment(root, ".request")); +}; + +fn blankpackageassert(out: *commandout, stdout: str, count: i32) void = { + expectexit(out, 1); + assert(same(out.stdout, stdout) + && occurrences(out.stderr, "invalid package name _") == count + && !has(out.stderr, "invalid or missing package clause")); +}; + +// A blank declared package name is syntax, not a malformed package clause. +// The compiler checker owns its one exact diagnostic, while named-source +// routing, MainOnly, omitted test-source, and full-syntax precedence retain +// their existing driver boundaries in both bootstrap stages. +@test fn blank_declared_package_name_compiler_and_named_routes() void = { + let root: str = fresh(); + let simple: str = strings.concat(root, "/simple.ww"); + let continued: str = strings.concat(root, "/continued.ww"); + let command: str = strings.concat(root, "/command.ww"); + let omitted: str = strings.concat(root, "/omitted_test.ww"); + let rawtest: str = strings.concat(root, "/raw_test.ww"); + let rawi: str = strings.concat(root, "/raw-i.ww"); + let malformedheader: str = strings.concat(root, "/malformed-header.ww"); + let malformedimport: str = strings.concat(root, "/malformed-import.ww"); + writefile(simple, strings.concat( + "package _;\n", + "export fn value() i32 = { return 17; };\n")); + writefile(continued, strings.concat( + "package _;\n", + "export fn value() i32 = { return missing; };\n")); + writefile(command, + "package _;\nfn main() i32 = { return 19; };\n"); + writefile(omitted, strings.concat( + "package _;\n", + "this malformed body is omitted by ordinary build;\n")); + writefile(rawtest, strings.concat( + "package _;\n", + "@test fn blank_test() void = { assert(true); };\n")); + writefile(rawi, "package _;\ni this_is_not_import;\n"); + writefile(malformedheader, "package ;\n"); + writefile(malformedimport, "package _;\nimport ;\n"); + + // Direct compiler ownership is exact, including the name-token position, + // diagnostic continuation, and atomic preservation of both publications. + let compilers: []str = ["w6c", "w6c_ww"]; + let directdiag: str = ""; + let continueddiag: str = ""; + let i: i32 = 0; + for (i < compilers.len) { + let asmout: str = strings.concat(root, "/direct-", compilers[i], ".s"); + let wwiout: str = strings.concat(root, "/direct-", compilers[i], ".wwi"); + writefile(asmout, "prior assembly bytes\n"); + writefile(wwiout, "prior interface bytes\n"); + let av: []str = [driver(compilers[i]), "-c", "-o", asmout, + "-I", wwiout, simple]; + let out: commandout; + runcommand(root, strings.concat("blank-direct-", compilers[i]), av, + (30i64 * (time.second: i64)): time.duration, &out); + blankpackageassert(&out, "", 1); + let want: str = strings.concat(simple, + ":1:9: error: invalid package name _\n"); + assert(same(out.stderr, want) + && same(readfile(asmout), "prior assembly bytes\n") + && same(readfile(wwiout), "prior interface bytes\n")); + if (i == 0) { directdiag = strings.dup(out.stderr); } + else { assert(same(directdiag, out.stderr)); }; + + let continuedout: str = strings.concat(root, "/continued-", + compilers[i], ".s"); + let continuedav: []str = [driver(compilers[i]), "-c", "-o", + continuedout, continued]; + runcommand(root, strings.concat("blank-continued-", compilers[i]), + continuedav, (30i64 * (time.second: i64)): time.duration, &out); + blankpackageassert(&out, "", 1); + assert(has(out.stderr, "undefined: missing") + && pos(out.stderr, "invalid package name _") + < pos(out.stderr, "undefined: missing") + && !os.exists(continuedout)); + if (i == 0) { continueddiag = strings.dup(out.stderr); } + else { assert(same(continueddiag, out.stderr)); }; + + let rawout: str = strings.concat(root, "/raw-i-", compilers[i], ".s"); + let rawav: []str = [driver(compilers[i]), "-o", rawout, rawi]; + runcommand(root, strings.concat("blank-raw-i-direct-", compilers[i]), + rawav, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && !os.exists(rawout) + && !has(out.stderr, "invalid package name _") + && has(out.stderr, "expected top-level decl")); + i += 1; + }; + + let compilerwrapper: str = strings.concat(root, "/blank-w6c.sh"); + let assemblerwrapper: str = strings.concat(root, "/blank-w6a.sh"); + let linkerwrapper: str = strings.concat(root, "/blank-w6l.sh"); + writeexecutable(compilerwrapper, strings.concat( + "#!/bin/sh\nprintf 'BEGIN' >> \"$WW_BLANK_PACKAGE_CTRACE\"\n", + "for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ", + "\"$WW_BLANK_PACKAGE_CTRACE\"; done\n", + "printf '\\n' >> \"$WW_BLANK_PACKAGE_CTRACE\"\n", + "exec \"$WW_BLANK_PACKAGE_REAL_C\" \"$@\"\n")); + writeexecutable(assemblerwrapper, strings.concat( + "#!/bin/sh\nprintf 'assemble\\n' >> \"$WW_BLANK_PACKAGE_ATRACE\"\n", + "exec \"$WW_BLANK_PACKAGE_REAL_A\" \"$@\"\n")); + writeexecutable(linkerwrapper, strings.concat( + "#!/bin/sh\nprintf 'link\\n' >> \"$WW_BLANK_PACKAGE_LTRACE\"\n", + "exec \"$WW_BLANK_PACKAGE_REAL_L\" \"$@\"\n")); + let stages: []str = ["ww", "ww_ww"]; + let assemblers: []str = ["w6a", "w6a_ww"]; + let linkers: []str = ["w6l", "w6l_ww"]; + let tags: []str = ["c", "ww"]; + let namedrefs: []str = ["", "", "", "", "", ""]; + i = 0; + for (i < stages.len) { + let ctrace: str = strings.concat(root, "/named-c-", tags[i], ".trace"); + let atrace: str = strings.concat(root, "/named-a-", tags[i], ".trace"); + let ltrace: str = strings.concat(root, "/named-l-", tags[i], ".trace"); + writefile(ctrace, ""); writefile(atrace, ""); writefile(ltrace, ""); + let env: []str = blankpackageenv(compilerwrapper, assemblerwrapper, + linkerwrapper, driver(compilers[i]), driver(assemblers[i]), + driver(linkers[i]), ctrace, atrace, ltrace); + let out: commandout; + + let work: str = strings.concat(root, "/named-work-", tags[i]); + let public: str = strings.concat(root, "/named-output-", tags[i]); + mkdirall(work); writefile(public, "prior public bytes\n"); + let buildav: []str = [driver(stages[i]), "build", "-w", work, + "-o", public, simple]; + runcommandenv(root, strings.concat("blank-named-build-", tags[i]), + buildav, env, (60i64 * (time.second: i64)): time.duration, &out); + blankpackageassert(&out, "", 1); + assert(same(readfile(public), "prior public bytes\n") + && occurrences(readfile(ctrace), "BEGIN") == 1 + && readfile(atrace).len == 0 && readfile(ltrace).len == 0 + && has(out.stderr, "ww: w6c failed for (root)\n")); + wrongsuffixfamilyabsent(work, "__root"); + let normalized: str = normalizedtrace(out.stderr, + strings.concat(work, "/"), public); + if (i == 0) { namedrefs[0] = strings.dup(normalized); } + else { assert(same(namedrefs[0], normalized)); }; + + blankpackageresettraces(ctrace, atrace, ltrace); + let runav: []str = [driver(stages[i]), "run", command]; + runcommandenv(root, strings.concat("blank-named-run-", tags[i]), runav, + env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + let runwant: str = strings.concat("ww: package ", command, + " is not a main package\n"); + assert(out.stdout.len == 0 && same(out.stderr, runwant) + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + if (i == 0) { namedrefs[1] = strings.dup(out.stderr); } + else { assert(same(namedrefs[1], out.stderr)); }; + + // Header/import validation occurs, but the malformed body and blank + // declared name of a named test source do not enter an ordinary build. + blankpackageresettraces(ctrace, atrace, ltrace); + let omitwork: str = strings.concat(root, "/omit-work-", tags[i]); + let omitav: []str = [driver(stages[i]), "build", "-w", omitwork, + omitted]; + runcommandenv(root, strings.concat("blank-omitted-", tags[i]), omitav, + env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && !os.exists(omitwork) && readfile(ctrace).len == 0 + && readfile(atrace).len == 0 && readfile(ltrace).len == 0); + + let modes: []str = ["run", "compile", "assembly"]; + let mode: i32 = 0; + for (mode < modes.len) { + blankpackageresettraces(ctrace, atrace, ltrace); + let testwork: str = strings.concat(root, "/raw-test-", tags[i], "-", + modes[mode]); + let testout: str = strings.concat(root, "/raw-output-", tags[i], "-", + modes[mode]); + mkdirall(testwork); + let av: []str = alloc([], 9u64)!; + append(av, driver(stages[i])); append(av, "test"); + if (mode == 1) { append(av, "-c"); } + else if (mode == 2) { append(av, "-S"); }; + append(av, "-w"); append(av, testwork); + if (mode != 0) { append(av, "-o"); append(av, testout); }; + append(av, rawtest); + runcommandenv(root, strings.concat("blank-raw-test-", tags[i], "-", + modes[mode]), av, env, + (90i64 * (time.second: i64)): time.duration, &out); + let stdout: str = ""; + if (mode == 0) { stdout = "FAIL\n"; }; + blankpackageassert(&out, stdout, 1); + assert(!os.exists(testout) && !has(out.stdout, "blank_test") + && !has(out.stderr, "blank_test")); + wrongsuffixfamilyabsent(testwork, "__root"); + let index: i32 = 2 + mode; + let norm: str = normalizedtrace(out.stderr, + strings.concat(testwork, "/"), testout); + if (i == 0) { namedrefs[index] = strings.dup(norm); } + else { assert(same(namedrefs[index], norm)); }; + mode += 1; + }; + + // Header syntax prevents a compiler action. Raw `i` is a body token, + // so full compiler syntax instead suppresses the semantic blank error. + blankpackageresettraces(ctrace, atrace, ltrace); + let headerwork: str = strings.concat(root, "/header-work-", tags[i]); + let headerout: str = strings.concat(root, "/header-out-", tags[i]); + mkdirall(headerwork); + let headerav: []str = [driver(stages[i]), "build", "-w", headerwork, + "-o", headerout, malformedimport]; + runcommandenv(root, strings.concat("blank-malformed-import-", tags[i]), + headerav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + let headerwant: str = strings.concat(malformedimport, + ":2:8: error: expected identifier, got ;\n"); + assert(out.stdout.len == 0 && same(out.stderr, headerwant) + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0 && !os.exists(headerout)); + if (i == 0) { namedrefs[5] = strings.dup(out.stderr); } + else { assert(same(namedrefs[5], out.stderr)); }; + blankpackageresettraces(ctrace, atrace, ltrace); + let badheaderwork: str = strings.concat(root, "/bad-header-work-", + tags[i]); + mkdirall(badheaderwork); + let badheaderav: []str = [driver(stages[i]), "build", "-w", + badheaderwork, malformedheader]; + runcommandenv(root, strings.concat("blank-malformed-header-", tags[i]), + badheaderav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && same(out.stderr, + strings.concat(malformedheader, + ":1:9: error: invalid or missing package clause\n")) + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + + blankpackageresettraces(ctrace, atrace, ltrace); + let rawiwork: str = strings.concat(root, "/raw-i-work-", tags[i]); + mkdirall(rawiwork); + let rawiav: []str = [driver(stages[i]), "build", "-w", rawiwork, + rawi]; + runcommandenv(root, strings.concat("blank-raw-i-", tags[i]), rawiav, + env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && !has(out.stderr, "invalid package name _") + && has(out.stderr, "expected top-level decl") + && readfile(ctrace).len != 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + i += 1; + }; + blankpackagenoresidue(root); + clean(root); +}; + +// Directory loading retains `_` long enough to form ordinary package/import +// and test-family graphs. Checker failure then rolls back the whole request; +// loader, dependency, mixed-family, MainOnly, and syntax errors keep their +// established precedence, without inventing blank identity or artifacts. +@test fn blank_declared_package_name_graph_test_import_and_rollback() void = { + let root: str = fresh(); + let include: str = strings.concat(root, "/include"); + let allblank: str = strings.concat(include, "/allblank"); + let prodonly: str = strings.concat(include, "/prodonly"); + let testonly: str = strings.concat(include, "/testonly"); + let validblanktest: str = strings.concat(include, "/validblanktest"); + let blankvalidtest: str = strings.concat(include, "/blankvalidtest"); + let blankexternal: str = strings.concat(include, "/blankexternal"); + let blankprovider: str = strings.concat(include, "/deps/blank"); + let gooddep: str = strings.concat(include, "/deps/good"); + let brokendep: str = strings.concat(include, "/deps/broken"); + let importer: str = strings.concat(include, "/importer"); + let blankparent: str = strings.concat(include, "/blankparent"); + let missingparent: str = strings.concat(include, "/missingparent"); + let brokenparent: str = strings.concat(include, "/brokenparent"); + let warm: str = strings.concat(include, "/warm"); + let dirs: []str = [allblank, prodonly, testonly, validblanktest, + blankvalidtest, blankexternal, blankprovider, gooddep, brokendep, + importer, blankparent, missingparent, brokenparent, warm]; + let i: i32 = 0; + for (i < dirs.len) { mkdirall(dirs[i]); i += 1; }; + writefile(strings.concat(allblank, "/a.ww"), + "package _;\nexport fn a() i32 = { return 1; };\n"); + writefile(strings.concat(allblank, "/b.ww"), + "package _;\nexport fn b() i32 = { return 2; };\n"); + writefile(strings.concat(allblank, "/a_test.ww"), + "package _;\n@test fn a_test() void = { assert(true); };\n"); + writefile(strings.concat(prodonly, "/prod.ww"), + "package _;\nexport fn value() i32 = { return 1; };\n"); + writefile(strings.concat(testonly, "/only_test.ww"), + "package _;\n@test fn only() void = { assert(true); };\n"); + writefile(strings.concat(validblanktest, "/prod.ww"), + "package validblanktest;\nfn value() i32 = { return 1; };\n"); + writefile(strings.concat(validblanktest, "/prod_test.ww"), + "package _;\n@test fn must_not_run() void = { abort(); };\n"); + writefile(strings.concat(blankvalidtest, "/prod.ww"), + "package _;\nfn value() i32 = { return 1; };\n"); + writefile(strings.concat(blankvalidtest, "/prod_test.ww"), + "package blankvalidtest;\n@test fn must_not_run() void = { abort(); };\n"); + writefile(strings.concat(blankexternal, "/prod.ww"), + "package _;\nexport fn value() i32 = { return 1; };\n"); + writefile(strings.concat(blankexternal, "/external_test.ww"), + "package __test;\n@test fn must_not_run() void = { abort(); };\n"); + writefile(strings.concat(blankprovider, "/provider.ww"), + "package _;\nexport fn value() i32 = { return 7; };\n"); + writefile(strings.concat(gooddep, "/good.ww"), + "package good;\nexport fn value() i32 = { return 11; };\n"); + writefile(strings.concat(brokendep, "/broken.ww"), strings.concat( + "package broken;\n", + "export fn value() i32 = { return missing; };\n")); + writefile(strings.concat(importer, "/main.ww"), strings.concat( + "package main;\nimport deps.blank;\n", + "fn main() i32 = { return blank.value(); };\n")); + writefile(strings.concat(blankparent, "/parent.ww"), strings.concat( + "package _;\nimport deps.good;\n", + "export fn value() i32 = { return good.value(); };\n")); + writefile(strings.concat(missingparent, "/parent.ww"), strings.concat( + "package _;\nimport absent.pkg;\n", + "export fn value() i32 = { return 1; };\n")); + writefile(strings.concat(brokenparent, "/parent.ww"), strings.concat( + "package _;\nimport deps.broken;\n", + "export fn value() i32 = { return broken.value(); };\n")); + let warmfile: str = strings.concat(warm, "/main.ww"); + let warmvalid: str = + "package main;\nfn main() i32 = { return 23; };\n"; + let warminvalid: str = + "package _;\nfn main() i32 = { return 23; };\n"; + writefile(warmfile, warmvalid); + + let compilerwrapper: str = strings.concat(root, "/graph-w6c.sh"); + let assemblerwrapper: str = strings.concat(root, "/graph-w6a.sh"); + let linkerwrapper: str = strings.concat(root, "/graph-w6l.sh"); + writeexecutable(compilerwrapper, strings.concat( + "#!/bin/sh\nprintf 'BEGIN' >> \"$WW_BLANK_PACKAGE_CTRACE\"\n", + "for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ", + "\"$WW_BLANK_PACKAGE_CTRACE\"; done\n", + "printf '\\n' >> \"$WW_BLANK_PACKAGE_CTRACE\"\n", + "exec \"$WW_BLANK_PACKAGE_REAL_C\" \"$@\"\n")); + writeexecutable(assemblerwrapper, strings.concat( + "#!/bin/sh\nprintf 'assemble\\n' >> \"$WW_BLANK_PACKAGE_ATRACE\"\n", + "exec \"$WW_BLANK_PACKAGE_REAL_A\" \"$@\"\n")); + writeexecutable(linkerwrapper, strings.concat( + "#!/bin/sh\nprintf 'link\\n' >> \"$WW_BLANK_PACKAGE_LTRACE\"\n", + "exec \"$WW_BLANK_PACKAGE_REAL_L\" \"$@\"\n")); + let stages: []str = ["ww", "ww_ww"]; + let compilers: []str = ["w6c", "w6c_ww"]; + let assemblers: []str = ["w6a", "w6a_ww"]; + let linkers: []str = ["w6l", "w6l_ww"]; + let tags: []str = ["c", "ww"]; + let allblankdiag: str = ""; + let familydiag: []str = ["", ""]; + let importdiag: str = ""; + let validdependencyartifacts: str = ""; + let warmbinref: str = ""; + i = 0; + for (i < stages.len) { + let ctrace: str = strings.concat(root, "/graph-c-", tags[i], ".trace"); + let atrace: str = strings.concat(root, "/graph-a-", tags[i], ".trace"); + let ltrace: str = strings.concat(root, "/graph-l-", tags[i], ".trace"); + writefile(ctrace, ""); writefile(atrace, ""); writefile(ltrace, ""); + let env: []str = blankpackageenv(compilerwrapper, assemblerwrapper, + linkerwrapper, driver(compilers[i]), driver(assemblers[i]), + driver(linkers[i]), ctrace, atrace, ltrace); + let out: commandout; + + let allbuildwork: str = strings.concat(root, "/allbuild-work-", tags[i]); + mkdirall(allbuildwork); + let allbuildav: []str = [driver(stages[i]), "build", "-w", + allbuildwork, "-I", include, allblank]; + runcommandenv(root, strings.concat("blank-all-build-", tags[i]), + allbuildav, env, (60i64 * (time.second: i64)): time.duration, &out); + blankpackageassert(&out, "", 2); + assert(occurrences(readfile(ctrace), "BEGIN") == 1 + && readfile(atrace).len == 0 && readfile(ltrace).len == 0); + wrongsuffixfamilyabsent(allbuildwork, "allblank"); + blankpackageresettraces(ctrace, atrace, ltrace); + + let allwork: str = strings.concat(root, "/allblank-work-", tags[i]); + mkdirall(allwork); + let allav: []str = [driver(stages[i]), "test", "-w", allwork, + "-I", include, allblank]; + runcommandenv(root, strings.concat("blank-all-test-", tags[i]), allav, + env, (120i64 * (time.second: i64)): time.duration, &out); + blankpackageassert(&out, "FAIL\n", 3); + assert(has(out.stderr, strings.concat("FAIL ", allblank, " [_] ")) + && occurrences(readfile(ctrace), "-internal-test.unit.new") == 1 + && !has(out.stdout, "a_test") && readfile(ltrace).len == 0); + wrongsuffixfamilyabsent(allwork, "allblank-internal-test"); + let diag: str = normalizedtrace(out.stderr, + strings.concat(allwork, "/"), + strings.concat(root, "/unused-allblank-output")); + if (i == 0) { allblankdiag = strings.dup(diag); } + else { assert(same(allblankdiag, diag)); }; + + // Compile-only uses the same substituted internal-test action: one + // compiler invocation owns all three retained package markers. + blankpackageresettraces(ctrace, atrace, ltrace); + let allcompilework: str = strings.concat(root, "/allcompile-work-", + tags[i]); + let allcompileout: str = strings.concat(root, "/allcompile-", tags[i]); + mkdirall(allcompilework); + let allcompileav: []str = [driver(stages[i]), "test", "-c", "-w", + allcompilework, "-I", include, "-o", allcompileout, allblank]; + runcommandenv(root, strings.concat("blank-all-compile-", tags[i]), + allcompileav, env, + (120i64 * (time.second: i64)): time.duration, &out); + blankpackageassert(&out, "", 3); + assert(occurrences(readfile(ctrace), "-internal-test.unit.new") == 1 + && !os.exists(allcompileout) && readfile(ltrace).len == 0); + wrongsuffixfamilyabsent(allcompilework, "allblank-internal-test"); + + let testdirs: []str = [prodonly, testonly]; + let ti: i32 = 0; + for (ti < testdirs.len) { + blankpackageresettraces(ctrace, atrace, ltrace); + let testwork: str = strings.concat(root, "/single-test-work-", + tags[i], "-", boundarypkgname(ti)); + mkdirall(testwork); + let testout: str = strings.concat(root, "/single-test-output-", + tags[i], "-", boundarypkgname(ti)); + let testav: []str = alloc([], 10u64)!; + append(testav, driver(stages[i])); append(testav, "test"); + if (ti == 1) { append(testav, "-c"); }; + append(testav, "-w"); append(testav, testwork); + append(testav, "-I"); append(testav, include); + if (ti == 1) { append(testav, "-o"); append(testav, testout); }; + append(testav, testdirs[ti]); + runcommandenv(root, strings.concat("blank-single-test-", tags[i], "-", + boundarypkgname(ti)), testav, env, + (120i64 * (time.second: i64)): time.duration, &out); + let teststdout: str = "FAIL\n"; + if (ti == 1) { teststdout = ""; }; + blankpackageassert(&out, teststdout, 1); + assert(!has(out.stdout, " [no test files]\n") + && !os.exists(testout) && readfile(ltrace).len == 0); + if (ti == 0) { wrongsuffixfamilyabsent(testwork, "prodonly"); } + else { wrongsuffixfamilyabsent(testwork, "testonly-internal-test"); }; + ti += 1; + }; + + // Mixed families remain coordinator-owned and invoke no compiler. + let mixed: []str = [validblanktest, blankvalidtest]; + ti = 0; + for (ti < mixed.len) { + blankpackageresettraces(ctrace, atrace, ltrace); + let mixedav: []str = [driver(stages[i]), "test", "-I", include, + mixed[ti]]; + runcommandenv(root, strings.concat("blank-mixed-", tags[i], "-", + boundarypkgname(ti)), mixedav, env, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(out.stdout, "FAIL\n") + && has(out.stderr, + "test package must match production package or _test\n") + && !has(out.stderr, "invalid package name _") + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + if (i == 0) { familydiag[ti] = strings.dup(out.stderr); } + else { assert(same(familydiag[ti], out.stderr)); }; + ti += 1; + }; + + // `__test` is the actual external family of `_`; it passes family + // validation and is stopped by the production blank dependency. + blankpackageresettraces(ctrace, atrace, ltrace); + let externalav: []str = [driver(stages[i]), "test", "-I", include, + blankexternal]; + runcommandenv(root, strings.concat("blank-external-", tags[i]), + externalav, env, (120i64 * (time.second: i64)): time.duration, &out); + blankpackageassert(&out, "FAIL\n", 1); + assert(!has(out.stderr, "test package must match production package") + && !has(out.stdout, "must_not_run") && readfile(ltrace).len == 0); + + // Imported blank providers preserve dotted action/import identity and + // fail before their importer can compile or publish. + blankpackageresettraces(ctrace, atrace, ltrace); + let importwork: str = strings.concat(root, "/import-work-", tags[i]); + let importout: str = strings.concat(root, "/import-output-", tags[i]); + mkdirall(importwork); + let importav: []str = [driver(stages[i]), "build", "-w", importwork, + "-I", include, "-o", importout, "importer"]; + runcommandenv(root, strings.concat("blank-import-provider-", tags[i]), + importav, env, (90i64 * (time.second: i64)): time.duration, &out); + blankpackageassert(&out, "", 1); + assert(has(out.stderr, "ww: w6c failed for deps.blank\n") + && !has(readfile(ctrace), "/importer.unit.new") + && !os.exists(importout) && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + wrongsuffixfamilyabsent(importwork, "deps.blank"); + wrongsuffixfamilyabsent(importwork, "importer"); + let inorm: str = normalizedtrace(out.stderr, + strings.concat(importwork, "/"), importout); + if (i == 0) { importdiag = strings.dup(inorm); } + else { assert(same(importdiag, inorm)); }; + + // A valid dependency is produced first, but request rollback leaves no + // committed dependency or parent generation after the parent checker. + blankpackageresettraces(ctrace, atrace, ltrace); + let parentwork: str = strings.concat(root, "/parent-work-", tags[i]); + mkdirall(parentwork); + let parentav: []str = [driver(stages[i]), "build", "-w", parentwork, + "-I", include, "blankparent"]; + runcommandenv(root, strings.concat("blank-valid-dependency-", tags[i]), + parentav, env, (90i64 * (time.second: i64)): time.duration, &out); + blankpackageassert(&out, "", 1); + assert(occurrences(readfile(ctrace), "BEGIN") == 2 + && readfile(atrace).len != 0 && readfile(ltrace).len == 0 + && !os.exists(strings.concat(parentwork, "/deps.good.wwi")) + && !os.exists(strings.concat(parentwork, "/blankparent.wwi"))); + wrongsuffixfamilyabsent(parentwork, "deps.good"); + wrongsuffixfamilyabsent(parentwork, "blankparent"); + let depstate: str = artifacttreesnapshot(parentwork); + if (i == 0) { validdependencyartifacts = strings.dup(depstate); } + else { assert(same(validdependencyartifacts, depstate)); }; + + // Missing dependency load and dependency producer failure each win + // before the parent compiler's blank-name check. + let precedence: []str = [missingparent, brokenparent]; + ti = 0; + for (ti < precedence.len) { + blankpackageresettraces(ctrace, atrace, ltrace); + let precedencework: str = strings.concat(root, "/precedence-work-", + tags[i], "-", boundarypkgname(ti)); + mkdirall(precedencework); + let precedenceav: []str = [driver(stages[i]), "build", "-w", + precedencework, "-I", include, precedence[ti]]; + runcommandenv(root, strings.concat("blank-precedence-", tags[i], "-", + boundarypkgname(ti)), precedenceav, env, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 + && !has(out.stderr, "invalid package name _")); + if (ti == 0) { + assert(has(out.stderr, "cannot find package absent.pkg") + && readfile(ctrace).len == 0); + } else { + assert(has(out.stderr, "undefined: missing") + && has(out.stderr, "ww: w6c failed for deps.broken\n") + && !has(readfile(ctrace), "/brokenparent.unit.new")); + }; + ti += 1; + }; + + // Run performs complete dependency loading, then MainOnly, before any + // producer. A missing import still precedes that command-kind boundary. + blankpackageresettraces(ctrace, atrace, ltrace); + let runav: []str = [driver(stages[i]), "run", "-I", include, + "blankparent"]; + runcommandenv(root, strings.concat("blank-run-mainonly-", tags[i]), + runav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 + && same(out.stderr, + "ww: package blankparent is not a main package\n") + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + blankpackageresettraces(ctrace, atrace, ltrace); + let runmissingav: []str = [driver(stages[i]), "run", "-I", include, + "missingparent"]; + runcommandenv(root, strings.concat("blank-run-missing-", tags[i]), + runmissingav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && has(out.stderr, + "cannot find package absent.pkg") + && !has(out.stderr, "is not a main package") + && !has(out.stderr, "invalid package name _") + && readfile(ctrace).len == 0); + + // Warm invalidation is source-byte based. Failed replacement preserves + // the entire generation and public binary; exact restoration reuses it. + let warmwork: str = strings.concat(root, "/warm-work-", tags[i]); + let warmout: str = strings.concat(root, "/warm-output-", tags[i]); + mkdirall(warmwork); + let warmav: []str = [driver(stages[i]), "build", "-w", warmwork, + "-I", include, "-o", warmout, "warm"]; + blankpackageresettraces(ctrace, atrace, ltrace); + runcommandenv(root, strings.concat("blank-warm-seed-", tags[i]), warmav, + env, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && os.exists(warmout) && readfile(ctrace).len != 0 + && readfile(atrace).len != 0 && readfile(ltrace).len != 0); + let warmbytes: str = strings.dup(readfile(warmout)); + let warmstate: str = strings.dup(treesnapshot(warmwork)); + if (i == 0) { warmbinref = strings.dup(warmbytes); } + else { assert(same(warmbinref, warmbytes)); }; + rewritefile(warmfile, warminvalid); + blankpackageresettraces(ctrace, atrace, ltrace); + runcommandenv(root, strings.concat("blank-warm-invalid-", tags[i]), + warmav, env, (90i64 * (time.second: i64)): time.duration, &out); + blankpackageassert(&out, "", 1); + assert(same(warmbytes, readfile(warmout)) + && same(warmstate, treesnapshot(warmwork)) + && readfile(ctrace).len != 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + rewritefile(warmfile, warmvalid); + blankpackageresettraces(ctrace, atrace, ltrace); + runcommandenv(root, strings.concat("blank-warm-restored-", tags[i]), + warmav, env, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && same(warmbytes, readfile(warmout)) + && same(warmstate, treesnapshot(warmwork)) + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len != 0); + i += 1; + }; + + // Concurrent stages read the same blank package but own disjoint work and + // public paths; each receives only its own one checker diagnostic. + let pcwork: str = strings.concat(root, "/parallel-c-work"); + let pwwork: str = strings.concat(root, "/parallel-ww-work"); + let pcout: str = strings.concat(root, "/parallel-c-output"); + let pwout: str = strings.concat(root, "/parallel-ww-output"); + mkdirall(pcwork); mkdirall(pwwork); + let pcctrace: str = strings.concat(root, "/parallel-c-compiler.trace"); + let pcatrace: str = strings.concat(root, "/parallel-c-assembler.trace"); + let pcltrace: str = strings.concat(root, "/parallel-c-linker.trace"); + let pwctrace: str = strings.concat(root, "/parallel-ww-compiler.trace"); + let pwatrace: str = strings.concat(root, "/parallel-ww-assembler.trace"); + let pwltrace: str = strings.concat(root, "/parallel-ww-linker.trace"); + writefile(pcctrace, ""); writefile(pcatrace, ""); writefile(pcltrace, ""); + writefile(pwctrace, ""); writefile(pwatrace, ""); writefile(pwltrace, ""); + let pcenv: []str = blankpackageenv(compilerwrapper, assemblerwrapper, + linkerwrapper, driver("w6c"), driver("w6a"), driver("w6l"), + pcctrace, pcatrace, pcltrace); + let pwenv: []str = blankpackageenv(compilerwrapper, assemblerwrapper, + linkerwrapper, driver("w6c_ww"), driver("w6a_ww"), driver("w6l_ww"), + pwctrace, pwatrace, pwltrace); + let pcav: []str = [driver("ww"), "build", "-w", pcwork, "-I", include, + "-o", pcout, "prodonly"]; + let pwav: []str = [driver("ww_ww"), "build", "-w", pwwork, "-I", + include, "-o", pwout, "prodonly"]; + let cc: exec.command; + cc.path = pcav[0]; cc.argv = pcav; cc.env = pcenv; cc.dir = repo(); + cc.stdoutpath = strings.concat(root, "/blank-parallel-c.stdout"); + cc.stderrpath = strings.concat(root, "/blank-parallel-c.stderr"); + cc.deadline = time.add(time.now(time.clock.monotonic), + (60i64 * (time.second: i64)): time.duration); + cc.grace = (100i64 * (time.millisecond: i64)): time.duration; + let wc: exec.command; + wc.path = pwav[0]; wc.argv = pwav; wc.env = pwenv; wc.dir = repo(); + wc.stdoutpath = strings.concat(root, "/blank-parallel-ww.stdout"); + wc.stderrpath = strings.concat(root, "/blank-parallel-ww.stderr"); + wc.deadline = time.add(time.now(time.clock.monotonic), + (60i64 * (time.second: i64)): time.duration); + wc.grace = (100i64 * (time.millisecond: i64)): time.duration; + let cp: exec.process; + let wp: exec.process; + exec.start(&cp, &cc); exec.start(&wp, &wc); + let cdone: bool = false; + let wdone: bool = false; + for (!cdone || !wdone) { + if (!cdone) { cdone = exec.poll(&cp); }; + if (!wdone) { wdone = exec.poll(&wp); }; + if (!cdone || !wdone) { + time.sleep(time.millisecond, time.clock.monotonic); + }; + }; + let pcerr: str = readfile(cc.stderrpath); + let pwerr: str = readfile(wc.stderrpath); + assert(cp.result.errno == 0 && cp.result.cleanuperrno == 0 + && cp.result.termination == exec.termination.EXIT && cp.result.code == 1 + && wp.result.errno == 0 && wp.result.cleanuperrno == 0 + && wp.result.termination == exec.termination.EXIT && wp.result.code == 1 + && readfile(cc.stdoutpath).len == 0 && readfile(wc.stdoutpath).len == 0 + && occurrences(pcerr, "invalid package name _") == 1 + && occurrences(pwerr, "invalid package name _") == 1 + && same(normalizedtrace(pcerr, strings.concat(pcwork, "/"), pcout), + normalizedtrace(pwerr, strings.concat(pwwork, "/"), pwout)) + && occurrences(readfile(pcctrace), "BEGIN") == 1 + && occurrences(readfile(pwctrace), "BEGIN") == 1 + && readfile(pcatrace).len == 0 && readfile(pcltrace).len == 0 + && readfile(pwatrace).len == 0 && readfile(pwltrace).len == 0 + && !os.exists(pcout) && !os.exists(pwout)); + wrongsuffixfamilyabsent(pcwork, "prodonly"); + wrongsuffixfamilyabsent(pwwork, "prodonly"); + blankpackagenoresidue(root); + clean(root); +}; + +fn blankwwi(path: str, pkg: str, declarations: str) str = { + return strings.concat("//ww:module ", path, "\npackage ", pkg, + ";\n", declarations); +}; + +fn blankwwiinvalid(out: *commandout, path: str, count: i32) void = { + expectexit(out, 1); + assert(out.stdout.len == 0 + && occurrences(out.stderr, strings.concat("could not import ", path, + " (invalid package name: \"_\")")) == count + && !has(out.stderr, "invalid package name _") + && !has(out.stderr, "imported and not used")); +}; + +fn blankwwinocascade(stderr: str) void = { + assert(!has(stderr, "selector '") + && !has(stderr, "unknown type '") + && !has(stderr, "calling non-function") + && !has(stderr, "is not exported")); +}; + +// Imported package-object names are validated after complete primary syntax, +// at the source import's path token. Invalid providers get one cached fake +// package per canonical path: aliases cannot hide them, selectors do not +// cascade, and unreachable interface origin facts are wholly inert. +@test fn blank_imported_package_name_direct_resolution() void = { + let root: str = fresh(); + let one: str = strings.concat(root, "/bad-one.wwi"); + let two: str = strings.concat(root, "/bad-two.wwi"); + let mixed: str = strings.concat(root, "/mixed.wwi"); + let support: str = strings.concat(root, "/test.wwi"); + let reservedsupport: str = strings.concat(root, "/__wwtest.wwi"); + let validtarget: str = strings.concat(root, "/good-target.wwi"); + let declarations: str = strings.concat( + "export type Thing = i32;\n", + "export let VALUE: i32;\n", + "export fn call() i32;\n", + "export let hidden: i32;\n"); + writefile(one, blankwwi("bad.one", "_", declarations)); + writefile(two, blankwwi("bad.two", "_", declarations)); + writefile(mixed, strings.concat( + blankwwi("bad.one", "_", "export fn hidden() i32;\n"), + "//ww:module bridge.good\n", + "package bridge;\n", + "import bad.one;\n", + "export fn run() i32;\n")); + writefile(support, blankwwi("test", "_", + "export fn run() void;\n")); + writefile(reservedsupport, blankwwi("__wwtest", "_", + "export fn run() void;\n")); + writefile(validtarget, blankwwi("good.one", "targetpkg", + "@test fn imported_test() void;\n")); + + let defaults: str = strings.concat(root, "/default.ww"); + let explicit: str = strings.concat(root, "/explicit.ww"); + let blank: str = strings.concat(root, "/blank.ww"); + let shadow: str = strings.concat(root, "/shadow.ww"); + writefile(defaults, strings.concat( + "package main;\n", + "import bad.one;\n", + "fn qualified(x: one.Thing) i32 = { ", + "return one.VALUE + one.call(); };\n", + // This unqualified use would resolve if invalid-owner declarations + // escaped the reached-owner filter. + "fn independent() i32 = { return hidden; };\n")); + writefile(explicit, strings.concat( + "package main;\n", + "import local bad.one;\n", + "fn qualified(x: local.Thing) i32 = { ", + "return local.VALUE + local.call(); };\n", + "fn independent() i32 = { return missing_explicit; };\n")); + writefile(blank, strings.concat( + "package main;\n", + "import _ bad.one;\n", + "fn independent() i32 = { return missing_blank; };\n")); + writefile(shadow, strings.concat( + "package main;\n", + "import local bad.one;\n", + "fn shadow(local: i32) i32 = { return local.value; };\n")); + let sources: []str = [defaults, explicit, blank]; + let missing: []str = ["hidden", "missing_explicit", + "missing_blank"]; + let missinglines: []str = ["4", "4", "3"]; + let compilers: []str = ["w6c", "w6c_ww"]; + let tags: []str = ["c", "ww"]; + let aliasdiags: []str = ["", "", ""]; + let shadowdiag: str = ""; + let si: i32 = 0; + for (si < compilers.len) { + let ci: i32 = 0; + for (ci < sources.len) { + let asmout: str = strings.concat(root, "/alias-", tags[si], "-", + boundarypkgname(ci), ".s"); + let wwiout: str = strings.concat(root, "/alias-", tags[si], "-", + boundarypkgname(ci), ".wwi"); + if (ci == 1) { + writefile(asmout, "prior imported assembly\n"); + writefile(wwiout, "prior imported interface\n"); + }; + let av: []str = [driver(compilers[si]), "-c", "--import", + "bad.one", one, "-o", asmout, "-I", wwiout, sources[ci]]; + let out: commandout; + runcommand(root, strings.concat("blank-wwi-alias-", tags[si], "-", + boundarypkgname(ci)), av, + (30i64 * (time.second: i64)): time.duration, &out); + blankwwiinvalid(&out, "bad.one", 1); + let want: str = ""; + if (ci == 0) { want = strings.concat(sources[ci], + ":2:8: error: could not import bad.one ", + "(invalid package name: \"_\")\n"); } + else if (ci == 1) { want = strings.concat(sources[ci], + ":2:14: error: could not import bad.one ", + "(invalid package name: \"_\")\n"); } + else { want = strings.concat(sources[ci], + ":2:10: error: could not import bad.one ", + "(invalid package name: \"_\")\n"); }; + let exact: str = strings.concat(want, sources[ci], ":", + missinglines[ci], ":33: error: undefined: ", missing[ci], "\n"); + assert(same(out.stderr, exact)); + blankwwinocascade(out.stderr); + if (ci == 1) { + assert(same(readfile(asmout), "prior imported assembly\n") + && same(readfile(wwiout), "prior imported interface\n")); + } else { + assert(!os.exists(asmout) && !os.exists(wwiout)); + }; + if (si == 0) { aliasdiags[ci] = strings.dup(out.stderr); } + else { assert(same(aliasdiags[ci], out.stderr)); }; + ci += 1; + }; + let shadowout: str = strings.concat(root, "/shadow-", tags[si], ".s"); + let shadowav: []str = [driver(compilers[si]), "-c", "--import", + "bad.one", one, "-o", shadowout, shadow]; + let shadowresult: commandout; + runcommand(root, strings.concat("blank-wwi-shadow-", tags[si]), shadowav, + (30i64 * (time.second: i64)): time.duration, &shadowresult); + blankwwiinvalid(&shadowresult, "bad.one", 1); + assert(has(shadowresult.stderr, "selector 'value' undefined") + && !has(shadowresult.stderr, "package 'local'") + && !os.exists(shadowout)); + if (si == 0) { shadowdiag = strings.dup(shadowresult.stderr); } + else { assert(same(shadowdiag, shadowresult.stderr)); }; + si += 1; + }; + + let repeated: str = strings.concat(root, "/repeated.ww"); + let ordered: str = strings.concat(root, "/ordered.ww"); + let syntaxbad: str = strings.concat(root, "/syntax-bad.ww"); + let bridge: str = strings.concat(root, "/bridge.ww"); + let inert: str = strings.concat(root, "/inert.ww"); + let testsource: str = strings.concat(root, "/support_test.ww"); + let badtargetsource: str = strings.concat(root, "/bad-target_test.ww"); + let goodtargetsource: str = strings.concat(root, "/good-target.ww"); + writefile(repeated, strings.concat( + "package main;\n", + "import first bad.one;\n", + "import second bad.one;\n", + "fn independent() i32 = { return after_repeat; };\n")); + writefile(ordered, strings.concat( + "package main;\n", + "import second bad.two;\n", + "import first bad.one;\n", + "fn independent() i32 = { return after_order; };\n")); + writefile(syntaxbad, + "package main;\nimport bad.one;\nimport ;\n"); + writefile(bridge, strings.concat( + "package main;\nimport bridge.good;\n", + "fn main() i32 = { return bridge.run(); };\n")); + writefile(inert, + "package main;\nfn main() i32 = { return 0; };\n"); + writefile(testsource, + "package supportcase;\n@test fn selected() void = { assert(true); };\n"); + writefile(badtargetsource, strings.concat( + "package targetcase;\nimport bad.one;\n", + "@test fn selected() void = { let x: i32 = one.VALUE; assert(x == 0); };\n")); + writefile(goodtargetsource, "package main;\nimport good.one;\n"); + let repeatedref: str = ""; + let orderedref: str = ""; + let bridgeref: str = ""; + let inertasmref: str = ""; + let inertwwiref: str = ""; + let fallbackasmrefs: []str = ["", ""]; + let fallbackwwirefs: []str = ["", ""]; + let targetasmref: str = ""; + let targetwwiref: str = ""; + si = 0; + for (si < compilers.len) { + let out: commandout; + let repeatout: str = strings.concat(root, "/repeat-", tags[si], ".s"); + let repeatav: []str = [driver(compilers[si]), "-c", "--import", + "bad.one", one, "-o", repeatout, repeated]; + runcommand(root, strings.concat("blank-wwi-repeat-", tags[si]), repeatav, + (30i64 * (time.second: i64)): time.duration, &out); + blankwwiinvalid(&out, "bad.one", 1); + let repeatexact: str = strings.concat(repeated, + ":2:14: error: could not import bad.one ", + "(invalid package name: \"_\")\n", repeated, + ":4:33: error: undefined: after_repeat\n"); + assert(same(out.stderr, repeatexact) && !os.exists(repeatout)); + if (si == 0) { repeatedref = strings.dup(out.stderr); } + else { assert(same(repeatedref, out.stderr)); }; + + let orderout: str = strings.concat(root, "/order-", tags[si], ".s"); + let orderav: []str = [driver(compilers[si]), "-c", "--import", + "bad.one", one, "--import", "bad.two", two, "-o", orderout, + ordered]; + runcommand(root, strings.concat("blank-wwi-order-", tags[si]), orderav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + let twodiag: str = "could not import bad.two (invalid package name: \"_\")"; + let onediag: str = "could not import bad.one (invalid package name: \"_\")"; + let orderexact: str = strings.concat(ordered, + ":2:15: error: ", twodiag, "\n", ordered, + ":3:14: error: ", onediag, "\n", ordered, + ":4:33: error: undefined: after_order\n"); + assert(out.stdout.len == 0 && same(out.stderr, orderexact) + && occurrences(out.stderr, twodiag) == 1 + && occurrences(out.stderr, onediag) == 1 + && pos(out.stderr, twodiag) < pos(out.stderr, onediag) + && pos(out.stderr, onediag) < pos(out.stderr, "undefined: after_order") + && !os.exists(orderout)); + blankwwinocascade(out.stderr); + if (si == 0) { orderedref = strings.dup(out.stderr); } + else { assert(same(orderedref, out.stderr)); }; + + let syntaxout: str = strings.concat(root, "/syntax-", tags[si], ".s"); + let syntaxav: []str = [driver(compilers[si]), "-c", "--import", + "bad.one", one, "-o", syntaxout, syntaxbad]; + runcommand(root, strings.concat("blank-wwi-syntax-", tags[si]), syntaxav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && has(out.stderr, "expected identifier") + && !has(out.stderr, "could not import") && !os.exists(syntaxout)); + + // The mixed interface is not a reachability root. Its invalid owner, + // valid embedded origin, import edge, and declarations are byte-inert. + let noasm: str = strings.concat(root, "/inert-no-", tags[si], ".s"); + let nowwi: str = strings.concat(root, "/inert-no-", tags[si], ".wwi"); + let withasm: str = strings.concat(root, "/inert-with-", tags[si], ".s"); + let withwwi: str = strings.concat(root, "/inert-with-", tags[si], ".wwi"); + let noav: []str = [driver(compilers[si]), "-c", "-o", noasm, + "-I", nowwi, inert]; + let withav: []str = [driver(compilers[si]), "-c", "--import", + "bad.one", mixed, "-o", withasm, "-I", withwwi, inert]; + runcommand(root, strings.concat("blank-wwi-inert-no-", tags[si]), noav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + runcommand(root, strings.concat("blank-wwi-inert-with-", tags[si]), withav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && same(readfile(noasm), readfile(withasm)) + && same(readfile(nowwi), readfile(withwwi))); + if (si == 0) { + inertasmref = strings.dup(readfile(noasm)); + inertwwiref = strings.dup(readfile(nowwi)); + } else { + assert(same(inertasmref, readfile(noasm)) + && same(inertwwiref, readfile(nowwi))); + }; + + // Reaching the valid embedded origin separately makes its bad.one + // edge live; the diagnostic belongs to the interface import token. + let bridgeout: str = strings.concat(root, "/bridge-", tags[si], ".s"); + let bridgeav: []str = [driver(compilers[si]), "-c", "--import", + "bad.one", mixed, "-o", bridgeout, bridge]; + runcommand(root, strings.concat("blank-wwi-bridge-", tags[si]), bridgeav, + (30i64 * (time.second: i64)): time.duration, &out); + blankwwiinvalid(&out, "bad.one", 1); + assert(same(out.stderr, strings.concat(mixed, + ":6:8: error: could not import bad.one ", + "(invalid package name: \"_\")\n")) + && !os.exists(bridgeout)); + blankwwinocascade(out.stderr); + if (si == 0) { bridgeref = strings.dup(out.stderr); } + else { assert(same(bridgeref, out.stderr)); }; + + let supportpaths: []str = ["test", "__wwtest"]; + let supportfiles: []str = [support, reservedsupport]; + let ti: i32 = 0; + for (ti < supportpaths.len) { + let supportout: str = strings.concat(root, "/support-bad-", tags[si], + "-", boundarypkgname(ti), ".s"); + let supportav: []str = [driver(compilers[si]), "-c", "-T", + "--test-support-module", supportpaths[ti], + "--import", supportpaths[ti], supportfiles[ti], "-o", supportout, + testsource]; + runcommand(root, strings.concat("blank-wwi-support-bad-", tags[si], + "-", boundarypkgname(ti)), supportav, + (30i64 * (time.second: i64)): time.duration, &out); + blankwwiinvalid(&out, supportpaths[ti], 1); + let supportwant: str = strings.concat(testsource, + ":1:1: error: could not import ", supportpaths[ti], + " (invalid package name: \"_\")\n"); + assert(same(out.stderr, supportwant) && !os.exists(supportout)); + + // With no supplied support interface, both compiler-owned support + // spellings retain the established raw external fallback. + let fallbackasm: str = strings.concat(root, "/support-ok-", tags[si], + "-", boundarypkgname(ti), ".s"); + let fallbackwwi: str = strings.concat(root, "/support-ok-", tags[si], + "-", boundarypkgname(ti), ".wwi"); + let fallbackav: []str = [driver(compilers[si]), "-c", "-T", + "--test-support-module", supportpaths[ti], "-o", fallbackasm, + "-I", fallbackwwi, testsource]; + runcommand(root, strings.concat("blank-wwi-support-ok-", tags[si], + "-", boundarypkgname(ti)), fallbackav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && os.exists(fallbackasm) && os.exists(fallbackwwi)); + if (si == 0) { + fallbackasmrefs[ti] = strings.dup(readfile(fallbackasm)); + fallbackwwirefs[ti] = strings.dup(readfile(fallbackwwi)); + } else { + assert(same(fallbackasmrefs[ti], readfile(fallbackasm)) + && same(fallbackwwirefs[ti], readfile(fallbackwwi))); + }; + ti += 1; + }; + + // Generated test-target canonicalization must not overwrite an invalid + // provider's path-leaf fake qualifier. The valid control still publishes + // with the compiler-owned full canonical target spelling. + let badtargetout: str = strings.concat(root, "/bad-target-", tags[si], ".s"); + let badtargetav: []str = [driver(compilers[si]), "-c", "-T", + "--test-support-module", "test", "--test-target-package", "bad.one", + "--import", "bad.one", one, "-o", badtargetout, badtargetsource]; + runcommand(root, strings.concat("blank-wwi-bad-target-", tags[si]), + badtargetav, (30i64 * (time.second: i64)): time.duration, &out); + blankwwiinvalid(&out, "bad.one", 1); + let badtargetwant: str = strings.concat(badtargetsource, + ":2:8: error: could not import bad.one ", + "(invalid package name: \"_\")\n"); + assert(same(out.stderr, badtargetwant) && !os.exists(badtargetout)); + + let goodtargetasm: str = strings.concat(root, "/good-target-", tags[si], ".s"); + let goodtargetwwi: str = strings.concat(root, "/good-target-", tags[si], ".wwi"); + let goodtargetav: []str = [driver(compilers[si]), "-c", "-T", + "--test-support-module", "test", "--test-target-package", "good.one", + "--import", "good.one", validtarget, "-o", goodtargetasm, + "-I", goodtargetwwi, goodtargetsource]; + runcommand(root, strings.concat("blank-wwi-good-target-", tags[si]), + goodtargetav, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && os.exists(goodtargetasm) && os.exists(goodtargetwwi)); + if (si == 0) { + targetasmref = strings.dup(readfile(goodtargetasm)); + targetwwiref = strings.dup(readfile(goodtargetwwi)); + } else { + assert(same(targetasmref, readfile(goodtargetasm)) + && same(targetwwiref, readfile(goodtargetwwi))); + }; + si += 1; + }; + + // Independent compiler processes do not share fake-package or dedupe + // state. Both preserve disjoint output paths and return the same stream. + let cconout: str = strings.concat(root, "/concurrent-c.s"); + let wconout: str = strings.concat(root, "/concurrent-ww.s"); + let cav: []str = [driver("w6c"), "-c", "--import", "bad.one", one, + "-o", cconout, defaults]; + let wav: []str = [driver("w6c_ww"), "-c", "--import", "bad.one", one, + "-o", wconout, defaults]; + let concurrentenv: []str = blankpackagebaseenv(); + let cc: exec.command; + cc.path = cav[0]; cc.argv = cav; cc.env = concurrentenv; cc.dir = repo(); + cc.stdoutpath = strings.concat(root, "/blank-wwi-concurrent-c.stdout"); + cc.stderrpath = strings.concat(root, "/blank-wwi-concurrent-c.stderr"); + cc.deadline = time.add(time.now(time.clock.monotonic), + (30i64 * (time.second: i64)): time.duration); + cc.grace = (100i64 * (time.millisecond: i64)): time.duration; + let wc: exec.command; + wc.path = wav[0]; wc.argv = wav; wc.env = concurrentenv; wc.dir = repo(); + wc.stdoutpath = strings.concat(root, "/blank-wwi-concurrent-ww.stdout"); + wc.stderrpath = strings.concat(root, "/blank-wwi-concurrent-ww.stderr"); + wc.deadline = time.add(time.now(time.clock.monotonic), + (30i64 * (time.second: i64)): time.duration); + wc.grace = (100i64 * (time.millisecond: i64)): time.duration; + let cp: exec.process; + let wp: exec.process; + exec.start(&cp, &cc); exec.start(&wp, &wc); + let cdone: bool = false; + let wdone: bool = false; + for (!cdone || !wdone) { + if (!cdone) { cdone = exec.poll(&cp); }; + if (!wdone) { wdone = exec.poll(&wp); }; + if (!cdone || !wdone) { + time.sleep(time.millisecond, time.clock.monotonic); + }; + }; + assert(cp.result.errno == 0 && cp.result.cleanuperrno == 0 + && cp.result.termination == exec.termination.EXIT && cp.result.code == 1 + && wp.result.errno == 0 && wp.result.cleanuperrno == 0 + && wp.result.termination == exec.termination.EXIT && wp.result.code == 1 + && readfile(cc.stdoutpath).len == 0 && readfile(wc.stdoutpath).len == 0 + && same(readfile(cc.stderrpath), readfile(wc.stderrpath)) + && occurrences(readfile(cc.stderrpath), "could not import bad.one") == 1 + && !os.exists(cconout) && !os.exists(wconout)); + blankpackagenoresidue(root); + clean(root); +}; + +fn blankwwicorrupt(valid: str) str = { + assert(occurrences(valid, "package renamed;") == 1); + match (strings.replace(valid, "package renamed;", "package _;")) { + case let corrupted: str => return corrupted; + case nomem => abort("blank interface corruption allocation failed"); + }; +}; + +// Persistent `.wwi` bytes are compiler inputs, never canonical package +// identity. A caller-corrupted provider interface invalidates only consumers, +// preserves every prior committed/public byte, and becomes reusable again +// after exact restoration in both build and test workdir formats. +@test fn blank_imported_package_name_public_persistence() void = { + let root: str = fresh(); + let source: str = strings.concat(root, "/source"); + let dep: str = strings.concat(source, "/dep"); + let app: str = strings.concat(source, "/app"); + mkdirall(dep); mkdirall(app); + writefile(strings.concat(dep, "/provider.ww"), strings.concat( + "package renamed;\n", + "export fn value() i32 = { return 31; };\n")); + // A same-named physical source remains an import decoy; only the dotted + // directory provider may produce dep's interface and action. + writefile(strings.concat(source, "/dep.ww"), strings.concat( + "package _;\n", + "export fn decoy() i32 = { return 99; };\n")); + writefile(strings.concat(app, "/main.ww"), strings.concat( + "package main;\nimport wire dep;\n", + "fn dependency() i32 = { return wire.value(); };\n", + "fn main() i32 = { return dependency() - 31; };\n")); + writefile(strings.concat(app, "/main_test.ww"), strings.concat( + "package main;\n", + "@test fn imported_value() void = { assert(dependency() == 31); };\n")); + + let compilerwrapper: str = strings.concat(root, "/public-w6c.sh"); + let assemblerwrapper: str = strings.concat(root, "/public-w6a.sh"); + let linkerwrapper: str = strings.concat(root, "/public-w6l.sh"); + writeexecutable(compilerwrapper, strings.concat( + "#!/bin/sh\nprintf 'BEGIN' >> \"$WW_BLANK_PACKAGE_CTRACE\"\n", + "for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ", + "\"$WW_BLANK_PACKAGE_CTRACE\"; done\n", + "printf '\\n' >> \"$WW_BLANK_PACKAGE_CTRACE\"\n", + "exec \"$WW_BLANK_PACKAGE_REAL_C\" \"$@\"\n")); + writeexecutable(assemblerwrapper, strings.concat( + "#!/bin/sh\nprintf 'assemble\\n' >> \"$WW_BLANK_PACKAGE_ATRACE\"\n", + "exec \"$WW_BLANK_PACKAGE_REAL_A\" \"$@\"\n")); + writeexecutable(linkerwrapper, strings.concat( + "#!/bin/sh\nprintf 'link\\n' >> \"$WW_BLANK_PACKAGE_LTRACE\"\n", + "exec \"$WW_BLANK_PACKAGE_REAL_L\" \"$@\"\n")); + let stages: []str = ["ww", "ww_ww"]; + let compilers: []str = ["w6c", "w6c_ww"]; + let assemblers: []str = ["w6a", "w6a_ww"]; + let linkers: []str = ["w6l", "w6l_ww"]; + let tags: []str = ["c", "ww"]; + let buildbinref: str = ""; + let buildartifactsref: str = ""; + let builddiagref: str = ""; + let testbinref: str = ""; + let testartifactsref: str = ""; + let testdiagref: str = ""; + let runtestdiagref: str = ""; + let testcontrolref: str = ""; + let si: i32 = 0; + for (si < stages.len) { + let ctrace: str = strings.concat(root, "/public-c-", tags[si], ".trace"); + let atrace: str = strings.concat(root, "/public-a-", tags[si], ".trace"); + let ltrace: str = strings.concat(root, "/public-l-", tags[si], ".trace"); + writefile(ctrace, ""); writefile(atrace, ""); writefile(ltrace, ""); + let env: []str = blankpackageenv(compilerwrapper, assemblerwrapper, + linkerwrapper, driver(compilers[si]), driver(assemblers[si]), + driver(linkers[si]), ctrace, atrace, ltrace); + let out: commandout; + + let work: str = strings.concat(root, "/public-build-work-", tags[si]); + let output: str = strings.concat(root, "/public-build-output-", tags[si]); + mkdirall(work); + let buildav: []str = [driver(stages[si]), "build", "-w", work, + "-I", source, "-o", output, "app"]; + runcommandenv(root, strings.concat("blank-wwi-build-seed-", tags[si]), + buildav, env, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 && os.exists(output) + && occurrences(readfile(ctrace), "BEGIN") == 2 + && readfile(atrace).len != 0 && readfile(ltrace).len != 0 + && !has(readfile(strings.concat(work, "/app.unit.ww")), "decoy")); + let runav: []str = [output]; + runcommand(root, strings.concat("blank-wwi-build-run-", tags[si]), runav, + (10i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + let validinterface: str = strings.dup(readfile(strings.concat(work, + "/dep.wwi"))); + let corruptinterface: str = blankwwicorrupt(validinterface); + let built: str = strings.dup(readfile(output)); + let validstate: str = strings.dup(treesnapshot(work)); + let validartifacts: str = strings.dup(artifacttreesnapshot(work)); + if (si == 0) { + buildbinref = strings.dup(built); + buildartifactsref = strings.dup(validartifacts); + } else { + assert(same(buildbinref, built) + && same(buildartifactsref, validartifacts)); + }; + + rewritefile(strings.concat(work, "/dep.wwi"), corruptinterface); + let corruptstate: str = strings.dup(treesnapshot(work)); + blankpackageresettraces(ctrace, atrace, ltrace); + runcommandenv(root, strings.concat("blank-wwi-build-corrupt-", tags[si]), + buildav, env, (120i64 * (time.second: i64)): time.duration, &out); + blankwwiinvalid(&out, "dep", 1); + blankwwinocascade(out.stderr); + assert(same(built, readfile(output)) + && same(corruptstate, treesnapshot(work)) + && occurrences(readfile(ctrace), "BEGIN") == 1 + && has(readfile(ctrace), "/app.unit.new") + && !has(readfile(ctrace), "/dep.unit.new") + && readfile(atrace).len == 0 && readfile(ltrace).len == 0); + let builddiag: str = normalizedtrace(out.stderr, + strings.concat(work, "/"), output); + if (si == 0) { builddiagref = strings.dup(builddiag); } + else { assert(same(builddiagref, builddiag)); }; + rewritefile(strings.concat(work, "/dep.wwi"), validinterface); + blankpackageresettraces(ctrace, atrace, ltrace); + runcommandenv(root, strings.concat("blank-wwi-build-restored-", tags[si]), + buildav, env, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && same(built, readfile(output)) + && same(validstate, treesnapshot(work)) + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len != 0); + + // The same corrupted committed interface fails the internal-test + // consumer and preserves a previously retained test executable. + let testwork: str = strings.concat(root, "/public-test-work-", tags[si]); + let testbin: str = strings.concat(root, "/public-test-output-", tags[si]); + mkdirall(testwork); + let testav: []str = [driver(stages[si]), "test", "-c", "-w", + testwork, "-I", source, "-o", testbin, "app"]; + blankpackageresettraces(ctrace, atrace, ltrace); + runcommandenv(root, strings.concat("blank-wwi-test-seed-", tags[si]), + testav, env, (180i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 && os.exists(testbin)); + let testcontrolav: []str = [testbin]; + runcommand(root, strings.concat("blank-wwi-test-control-", tags[si]), + testcontrolav, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0 + && has(out.stdout, "imported_value ... ok\n")); + if (si == 0) { testcontrolref = strings.dup(out.stdout); } + else { assert(same(testcontrolref, out.stdout)); }; + let validtestinterface: str = strings.dup(readfile(strings.concat(testwork, + "/dep.wwi"))); + let corrupttestinterface: str = blankwwicorrupt(validtestinterface); + let testbytes: str = strings.dup(readfile(testbin)); + let validteststate: str = strings.dup(treesnapshot(testwork)); + let validtestartifacts: str = strings.dup( + artifacttreesnapshot(testwork)); + if (si == 0) { + testbinref = strings.dup(testbytes); + testartifactsref = strings.dup(validtestartifacts); + } else { + assert(same(testbinref, testbytes) + && same(testartifactsref, validtestartifacts)); + }; + rewritefile(strings.concat(testwork, "/dep.wwi"), + corrupttestinterface); + let corruptteststate: str = strings.dup(treesnapshot(testwork)); + blankpackageresettraces(ctrace, atrace, ltrace); + runcommandenv(root, strings.concat("blank-wwi-test-corrupt-", tags[si]), + testav, env, (180i64 * (time.second: i64)): time.duration, &out); + blankwwiinvalid(&out, "dep", 1); + blankwwinocascade(out.stderr); + assert(same(testbytes, readfile(testbin)) + && same(corruptteststate, treesnapshot(testwork)) + && occurrences(readfile(ctrace), "BEGIN") == 1 + && has(readfile(ctrace), "app-internal-test.unit.new") + && !has(readfile(ctrace), "/dep.unit.new") + && readfile(atrace).len == 0 && readfile(ltrace).len == 0); + let testdiag: str = normalizedtrace(out.stderr, + strings.concat(testwork, "/"), testbin); + if (si == 0) { testdiagref = strings.dup(testdiag); } + else { assert(same(testdiagref, testdiag)); }; + + blankpackageresettraces(ctrace, atrace, ltrace); + let runtestav: []str = [driver(stages[si]), "test", "-w", testwork, + "-I", source, "-o", testbin, "app"]; + runcommandenv(root, strings.concat("blank-wwi-test-run-corrupt-", tags[si]), + runtestav, env, (180i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(out.stdout, "FAIL\n") + && occurrences(out.stderr, + "could not import dep (invalid package name: \"_\")") == 1 + && !has(out.stderr, "invalid package name _") + && !has(out.stderr, "imported and not used") + && same(testbytes, readfile(testbin)) + && same(corruptteststate, treesnapshot(testwork)) + && occurrences(readfile(ctrace), "BEGIN") == 1 + && readfile(atrace).len == 0 && readfile(ltrace).len == 0); + let runtestdiag: str = normalizedtrace(out.stderr, + strings.concat(testwork, "/"), testbin); + if (si == 0) { runtestdiagref = strings.dup(runtestdiag); } + else { assert(same(runtestdiagref, runtestdiag)); }; + + rewritefile(strings.concat(testwork, "/dep.wwi"), validtestinterface); + blankpackageresettraces(ctrace, atrace, ltrace); + runcommandenv(root, strings.concat("blank-wwi-test-restored-", tags[si]), + testav, env, (180i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && same(testbytes, readfile(testbin)) + && same(validteststate, treesnapshot(testwork)) + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len != 0); + si += 1; + }; + blankpackagenoresidue(root); + clean(root); +}; + fn envrequired(name: str) str = { match (os.getenv(name)) { case let value: str => { diff --git a/test/wcc/738_module_decl.c b/test/wcc/738_module_decl.c index 9fae3954..e0fd6fc5 100644 --- a/test/wcc/738_module_decl.c +++ b/test/wcc/738_module_decl.c @@ -63,6 +63,167 @@ check_module_stamps(const char *src, const char *want_first, const char *want_la return ok; } +enum packagemode { + PACKAGE_FULL, + PACKAGE_IMPORTS, + PACKAGE_HEADER, +}; + +static const char * +packagemodename(enum packagemode mode) +{ + switch (mode) { + case PACKAGE_FULL: return "full"; + case PACKAGE_IMPORTS: return "imports"; + case PACKAGE_HEADER: return "header"; + } + return "unknown"; +} + +static Node * +parsepackage(Parser *p, enum packagemode mode) +{ + switch (mode) { + case PACKAGE_FULL: return parsefile(p); + case PACKAGE_IMPORTS: return parseimports(p); + case PACKAGE_HEADER: return parsepackageheader(p); + } + return NULL; +} + +/* + * The three package-clause parsers intentionally expose different AST + * boundaries. Header/imports outer nodes stay on the package keyword; the + * full parser's outer file node retains its historical 1:1 root position. + * Imports-only parsing keeps its package marker on the keyword, while full + * parsing moves only a blank package marker to the underscore so the checker + * can diagnose the name token. An ordinary package marker never moves. + */ +static int +check_package_shape(enum packagemode mode, const char *name, + int markerline, int markercol) +{ + Arena *a = newarena(); + Lex l; + Parser p; + char src[160]; + snprintf(src, sizeof src, + "// leading comment\npackage %s;\nimport alpha;\n" + "fn x() void = {};\n", name); + lexinit(&l, a, "shape.ww", src, strlen(src)); + parserinit(&p, a, &l); + Node *file = parsepackage(&p, mode); + Node *marker = file ? file->body : NULL; + int wantmarker = mode != PACKAGE_HEADER; + int outerline = mode == PACKAGE_FULL ? 1 : 2; + int ok = file != NULL && file->kind == N_FILE + && p.errs == 0 && l.errs == 0 + && file->pkgname != NULL && strcmp(file->pkgname, name) == 0 + && file->pos.line == outerline && file->pos.col == 1 + && ((mode == PACKAGE_FULL) + || (file->module != NULL && strcmp(file->module, name) == 0)) + && ((!wantmarker && marker == NULL) + || (wantmarker && marker != NULL && marker->next == NULL + && marker->kind == N_FILE + && marker->pkgname != NULL + && strcmp(marker->pkgname, name) == 0 + && marker->pos.line == markerline + && marker->pos.col == markercol)); + if (!ok) { + fprintf(stderr, + "package shape mismatch: mode=%s name=%s " + "errs=%d/%d outer=%d:%d marker=%d:%d\n", + packagemodename(mode), name, p.errs, l.errs, + file ? file->pos.line : 0, file ? file->pos.col : 0, + marker ? marker->pos.line : 0, marker ? marker->pos.col : 0); + } + freearena(a); + return ok; +} + +static int +rejects_malformed_package(enum packagemode mode) +{ + Arena *a = newarena(); + Lex l; + Parser p; + const char *src = "package ;\nfn x() void = {};\n"; + char *diag = NULL; + size_t diaglen = 0; + FILE *prev = errout; + FILE *capture = open_memstream(&diag, &diaglen); + if (capture != NULL) + errout = capture; + lexinit(&l, a, "malformed.ww", src, strlen(src)); + parserinit(&p, a, &l); + Node *file = parsepackage(&p, mode); + int ok = file != NULL && p.errs + l.errs > 0; + if (capture != NULL) { + fclose(capture); + errout = prev; + } + if (!ok) + fprintf(stderr, "malformed package accepted in %s mode\n", + packagemodename(mode)); + free(diag); + freearena(a); + return ok; +} + +/* + * N_USE keeps the first import-spec token (alias when present) separately + * from the first dotted-path token. That distinction is checker-visible for + * import errors, so pin it in each parser boundary rather than inferring the + * path position later from the binding spelling. + */ +static int +check_import_path_position(enum packagemode mode, const char *spec, + int speccol, int pathcol, const char *alias, const char *binding, + int blank) +{ + Arena *a = newarena(); + Lex l; + Parser p; + char src[192]; + snprintf(src, sizeof src, + "package foo;\nimport %s;\nfn x() void = {};\n", spec); + lexinit(&l, a, "usepath.ww", src, strlen(src)); + parserinit(&p, a, &l); + Node *file = parsepackage(&p, mode); + Node *u = file ? file->list : NULL; + while (u != NULL && u->kind != N_USE) + u = u->next; + int ok = file != NULL && p.errs == 0 && l.errs == 0 + && u != NULL + && u->pos.file != NULL && strcmp(u->pos.file, "usepath.ww") == 0 + && u->pos.line == 2 && u->pos.col == 1 + && u->usefile != NULL && strcmp(u->usefile, "usepath.ww") == 0 + && u->useline == 2 && u->usecol == speccol + && u->usepathfile != NULL + && strcmp(u->usepathfile, "usepath.ww") == 0 + && u->usepathline == 2 && u->usepathcol == pathcol + && u->usesource != NULL + && strcmp(u->usesource, "alpha.beta") == 0 + && u->usepath != NULL && strcmp(u->usepath, "alpha.beta") == 0 + && ((alias == NULL && u->usealias == NULL) + || (alias != NULL && u->usealias != NULL + && strcmp(u->usealias, alias) == 0)) + && ((binding == NULL && u->str == NULL) + || (binding != NULL && u->str != NULL + && strcmp(u->str, binding) == 0)) + && u->useblank == blank; + if (!ok) { + fprintf(stderr, + "import path position mismatch: mode=%s spec=%s " + "errs=%d/%d specpos=%d:%d pathpos=%d:%d\n", + packagemodename(mode), spec, p.errs, l.errs, + u ? u->useline : 0, u ? u->usecol : 0, + u ? u->usepathline : 0, u ? u->usepathcol : 0); + } + freearena(a); + return ok; +} + int main(void) { @@ -114,6 +275,69 @@ main(void) else { fprintf(stderr, "738[6] dotted import leaf-store FAILED\n"); fail++; } } + /* `package _;` is syntax in every loader/compiler parser mode. */ + if (check_package_shape(PACKAGE_HEADER, "_", 0, 0)) pass++; + else { fprintf(stderr, "738[7] blank package header shape FAILED\n"); fail++; } + + if (check_package_shape(PACKAGE_IMPORTS, "_", 2, 1)) pass++; + else { fprintf(stderr, "738[8] blank imports-only shape FAILED\n"); fail++; } + + if (check_package_shape(PACKAGE_FULL, "_", 2, 9)) pass++; + else { fprintf(stderr, "738[9] blank full-parser shape FAILED\n"); fail++; } + + /* Nonblank package-marker positions remain on the package keyword. */ + if (check_package_shape(PACKAGE_HEADER, "foo", 0, 0)) pass++; + else { fprintf(stderr, "738[10] named package header shape FAILED\n"); fail++; } + + if (check_package_shape(PACKAGE_IMPORTS, "foo", 2, 1)) pass++; + else { fprintf(stderr, "738[11] named imports-only shape FAILED\n"); fail++; } + + if (check_package_shape(PACKAGE_FULL, "foo", 2, 1)) pass++; + else { fprintf(stderr, "738[12] named full-parser shape FAILED\n"); fail++; } + + /* Extending the name slot to `_` must not accept a missing name. */ + if (rejects_malformed_package(PACKAGE_HEADER)) pass++; + else { fprintf(stderr, "738[13] malformed package header FAILED\n"); fail++; } + + if (rejects_malformed_package(PACKAGE_IMPORTS)) pass++; + else { fprintf(stderr, "738[14] malformed imports-only package FAILED\n"); fail++; } + + if (rejects_malformed_package(PACKAGE_FULL)) pass++; + else { fprintf(stderr, "738[15] malformed full-parser package FAILED\n"); fail++; } + + /* Default imports use the first dotted-path identifier for both slots. */ + for (enum packagemode mode = PACKAGE_FULL; mode <= PACKAGE_HEADER; mode++) { + if (check_import_path_position(mode, "alpha.beta", 8, 8, + NULL, "beta", 0)) pass++; + else { + fprintf(stderr, "738 default import path position FAILED (%s)\n", + packagemodename(mode)); + fail++; + } + } + + /* Explicit aliases keep the binding/spec token at col 8, path at 14. */ + for (enum packagemode mode = PACKAGE_FULL; mode <= PACKAGE_HEADER; mode++) { + if (check_import_path_position(mode, "local alpha.beta", 8, 14, + "local", "local", 0)) pass++; + else { + fprintf(stderr, "738 explicit import path position FAILED (%s)\n", + packagemodename(mode)); + fail++; + } + } + + /* A blank alias likewise keeps `_` at col 8 and the path at col 10. */ + for (enum packagemode mode = PACKAGE_FULL; mode <= PACKAGE_HEADER; mode++) { + if (check_import_path_position(mode, "_ alpha.beta", 8, 10, + NULL, NULL, 1)) pass++; + else { + fprintf(stderr, "738 blank import path position FAILED (%s)\n", + packagemodename(mode)); + fail++; + } + } + printf("738_module_decl: %d pass, %d fail\n", pass, fail); return fail == 0 ? 0 : 1; }