compiler: implement blank package name semantics

This commit is contained in:
2026-08-23 15:33:59 +09:00
parent e67b8e1bc4
commit 0b24b11d65
14 changed files with 2862 additions and 112 deletions

View File

@@ -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;

View File

@@ -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

View File

@@ -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 "<err>";
}
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;

View File

@@ -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. */

View File

@@ -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 706740](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 88107](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/scanner.go#L88-L107),
[368394](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/scanner.go#L368-L394),
and
[437439](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/scanner.go#L437-L439),
[`parser.go`, lines 397420](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/parser.go#L397-L420)
and
[27512763](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 336355](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 4577](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/noder/noder.go#L45-L77),
[`irgen.go`, lines 2399](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 5556](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/read.go#L55-L56),
[187198](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/read.go#L187-L198),
and
[265340](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/read.go#L265-L340),
[`go/build/build.go`, lines 9311039](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 32443315](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 175226](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 131](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 15](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 118](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_test_only.txt#L1-L18)
and
[`build_no_go.txt`, lines 130](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 125180 and 248335](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 157190 and 263350](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.

View File

@@ -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

View File

@@ -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:88107,368394,437439`), the parser
accepts it in package syntax (`parser.go:397420,27512763`), and types2
rejects the retained node as `invalid package name _`
(`cmd/compile/internal/types2/check.go:336355`).
- **behavior directly implemented or asserted by pinned Go** — official
`test/blank1.go:131` asserts the blank-name error and later checker errors,
proving continuation. `internal/types/testdata/check/blank.go:15` asserts
only the blank-name error. `cmd/go/testdata/script/build_test_only.txt:118`
and `build_no_go.txt:130` 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:125180,248335`; public twin
`go/types/resolver.go:157190,263350`). 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

View File

@@ -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;
};

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}