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