ww: separate package identity from declared name

This commit is contained in:
2026-08-14 03:07:58 +09:00
parent 028da6323e
commit 6efe9b70d4
37 changed files with 1678 additions and 886 deletions

View File

@@ -58,8 +58,10 @@ lookup_builtin(const char *name)
}
static const char *decl_mod(Node *file, Node *d);
static const char *use_path(Node *file, const char *curmod, const char *alias);
static int src_imports(Node *file, const char *modtag, const char *name);
static const char *use_path(Node *file, const char *curmod, int source,
const char *alias);
static int src_imports(Node *file, const char *modtag, int source,
const char *name);
static Sym *lookup_visible(Checker *c, const char *name);
static Sym *lookup_visible_type(Checker *c, const char *name);
static void resolve_typedecl(Checker *c, Node *d);
@@ -89,6 +91,7 @@ resolve_typename(Checker *c, Node *n)
/* 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,
c->cur_source,
head);
if (mk != NULL)
s = scope_lookup_in_module(c->cur, mk,
@@ -622,7 +625,8 @@ eval_def_const(Checker *c, Node *n, u64 *out, int depth)
case N_DOT: {
if (n->lhs == NULL || n->lhs->kind != N_IDENT) return 0;
/* M1 #22: map the qualifier alias to its dotted import path. */
const char *mk = use_path(c->file, c->cur_mod, n->lhs->str);
const char *mk = use_path(c->file, c->cur_mod, c->cur_source,
n->lhs->str);
if (mk == NULL) return 0;
Sym *s = scope_lookup_in_module(c->cur, mk, n->str);
if (s == NULL || s->kind != SK_DEF ||
@@ -636,12 +640,15 @@ eval_def_const(Checker *c, Node *n, u64 *out, int depth)
}
int
check_eval_const(Checker *c, Node *n, const char *owner, u64 *out)
check_eval_const(Checker *c, Node *n, const char *owner, int source, u64 *out)
{
const char *saved = c->cur_mod;
int savesource = c->cur_source;
c->cur_mod = owner;
c->cur_source = source;
int ok = eval_def_const(c, n, out, 0);
c->cur_mod = saved;
c->cur_source = savesource;
return ok;
}
@@ -1475,6 +1482,7 @@ cexpr(Checker *c, Node *n)
* import path; map the alias the user wrote to
* that path before looking up the leaf. */
const char *mk = use_path(c->file, c->cur_mod,
c->cur_source,
n->lhs->str);
if (mk == NULL) {
if (ms->kind == SK_USE)
@@ -2854,6 +2862,7 @@ check_init(Checker *c, Arena *a)
c->is_test = 0; /* #15: caller (w6c main) sets it after init */
c->is_test_package = 0;
c->test_module = "test";
c->test_target = NULL;
typesinit(a);
c->top = newscope(a, NULL);
c->cur = c->top;
@@ -2873,6 +2882,10 @@ static const char *
decl_mod(Node *file, Node *d)
{
if (d == NULL || d->module == NULL || file == NULL) return NULL;
/* Separate-compilation interfaces already carry their canonical owner.
* Visibility is checked independently against the referencing source's
* import binding; never erase semantic ownership for a transitive fact. */
if (d->imported) return d->module;
for (Node *u = file->list; u; u = u->next) {
/* M1 #22: a decl is imported iff some `use` directive's full
* dotted import path equals the decl's module (now the path,
@@ -2887,41 +2900,40 @@ decl_mod(Node *file, Node *d)
}
/*
* use_path — map a `use` alias (leaf bareword the user writes, `utf8`)
* to the full dotted import path it binds (`encoding.utf8`), for the
* module-qualified resolution and the codegen hint (M1 #22, §2.4). For
* single-level packages usepath == alias so the result is unchanged.
* use_path — map a source-file default qualifier (the imported package's
* declared name) to the full canonical import path it binds, for
* module-qualified resolution and the codegen hint (M1 #22, §2.4).
*
* The alias→path map is NOT file-global: two modules in the same
* concatenated unit may bind the same leaf alias to different paths
* (sha256's `import crypto.math` and strconv's `import math` both bind
* alias `math`). The import declared in the SAME module as the
* reference (`curmod`) is the authoritative one; requiring it closes both
* cross-module mis-resolution and accidental transitive-import visibility.
* The qualifier→path map is source-file local: two files in the same
* concatenated unit may bind the same declared name to different paths. The
* import with the reference's source ID and owner is authoritative, closing
* both cross-file mis-resolution and accidental transitive visibility.
* Returns NULL if the referencing package has no such `use`.
*/
static const char *
use_path(Node *file, const char *curmod, const char *alias)
use_path(Node *file, const char *curmod, int source, const char *alias)
{
if (file == NULL || alias == NULL) return NULL;
/* Legacy inline multi-package units may spell a package's own
* declarations as `pkg.member`. That is self-qualification, not an
* imported namespace; preserve it without reopening transitive lookup. */
if (curmod != NULL) {
if (source == 0 && curmod != NULL) {
const char *dot = strrchr(curmod, '.');
const char *leaf = dot ? dot + 1 : curmod;
if (strcmp(alias, leaf) == 0) return curmod;
}
for (Node *u = file->list; u; u = u->next) {
if (u->kind != N_USE || u->str == NULL
|| strcmp(u->str, alias) != 0)
|| u->sourceid != source || strcmp(u->str, alias) != 0)
continue;
const char *p = u->usepath ? u->usepath : u->str;
const char *um = decl_mod(file, u);
int same = (um == NULL) ? (curmod == NULL)
: (curmod != NULL && strcmp(um, curmod) == 0);
if (same)
if (same) {
u->used = 1;
return p;
}
}
return NULL;
}
@@ -2947,9 +2959,12 @@ resolve_typedecl(Checker *c, Node *d)
if (t == NULL || t->under != NULL || t->resolving) return;
t->resolving = 1;
const char *save = c->cur_mod;
int savesource = c->cur_source;
c->cur_mod = decl_mod(c->file, d);
c->cur_source = d->sourceid;
Type *under = resolve_type(c, d->lhs);
c->cur_mod = save;
c->cur_source = savesource;
/* Alias-root cycle (`type a = b; type b = a` / `type a = a`):
* checked BEFORE clearing the flag so self-aliases trip on their
* own in-progress mark. ty_err instead of the cyclic under keeps
@@ -2978,11 +2993,11 @@ resolve_typedecl(Checker *c, Node *d)
* (N_USE nodes with module == NULL).
*/
static int
src_imports(Node *file, const char *modtag, const char *name)
src_imports(Node *file, const char *modtag, int source, const char *name)
{
if (file == NULL || name == NULL || name[0] == '\0') return 0;
for (Node *u = file->list; u; u = u->next) {
if (u->kind != N_USE) continue;
if (u->kind != N_USE || u->sourceid != source) continue;
/* Skip self-imports: lib/fmt/fmt_test.ww carries `use fmt;`
* even though its module tag is also "fmt"; that directive
* doesn't introduce a foreign module bareword and lib/fmt's
@@ -3013,10 +3028,18 @@ static int
direct_module_visible(Checker *c, const char *mod)
{
if (c == NULL || mod == NULL || mod[0] == '\0') return 0;
const char *dot = strrchr(mod, '.');
const char *alias = dot ? dot + 1 : mod;
const char *path = use_path(c->file, c->cur_mod, alias);
return path != NULL && strcmp(path, mod) == 0;
for (Node *u = c->file->list; u; u = u->next) {
if (u->kind != N_USE || u->sourceid != c->cur_source) continue;
const char *um = decl_mod(c->file, u);
int same = c->cur_mod == NULL ? um == NULL
: um != NULL && strcmp(um, c->cur_mod) == 0;
const char *path = u->usepath ? u->usepath : u->str;
if (same && path != NULL && strcmp(path, mod) == 0) {
u->used = 1;
return 1;
}
}
return 0;
}
static Sym *
@@ -3035,7 +3058,7 @@ lookup_visible(Checker *c, const char *name)
* source-owned alias map is authoritative: if this package directly
* imports NAME, return the coalesced module marker only as a marker; the
* N_DOT path maps the alias to the correct full path again. */
if (use_path(c->file, c->cur_mod, name) != NULL) {
if (use_path(c->file, c->cur_mod, c->cur_source, name) != NULL) {
for (Scope *p = c->cur; p; p = p->parent)
for (Sym *b = p->first; b; b = b->next)
if (strcmp(b->name, name) == 0
@@ -3101,7 +3124,7 @@ check_module_shadow(Checker *c, const char *name, Pos pos,
}
}
if (!seen_use) return;
if (!src_imports(c->file, c->cur_mod, name)) return;
if (!src_imports(c->file, c->cur_mod, c->cur_source, name)) return;
err(c, pos, "%s '%s' shadows imported module '%s'",
kindstr, name, name);
}
@@ -3132,6 +3155,80 @@ same_import_fact(Checker *c, Node *d, const char *mod, Skind kind)
return s;
}
static int
top_decl_kind(Node *d)
{
return d != NULL && (d->kind == N_TYPEDECL || d->kind == N_DEF
|| d->kind == N_FNDECL || d->kind == N_LET);
}
static void
check_import_alt(Node *d, const char *name)
{
FILE *f = errout ? errout : stderr;
fprintf(f, "\t%s:%d:%d: other declaration of %s\n",
d->pos.file ? d->pos.file : "?", d->pos.line, d->pos.col, name);
}
/* Go's default import binding lives in the importing file's scope. Reject
* only another binding in that same source section; equal names in sibling
* files are independent even though their canonical edges are package-wide. */
static void
check_import_redeclarations(Checker *c, Node *file)
{
if (!c->sep_mode) return;
for (Node *u = file->list; u; u = u->next) {
if (u->kind != N_USE || u->imported || u->str == NULL)
continue;
for (Node *v = file->list; v != u; v = v->next) {
if (v->kind != N_USE || v->imported || v->str == NULL
|| v->sourceid != u->sourceid)
continue;
if (strcmp(v->str, u->str) == 0) {
err(c, u->pos, "%s redeclared in this block", u->str);
check_import_alt(v, u->str);
break;
}
}
}
}
/* Report file-local unused bindings before package-declaration collisions,
* matching the pinned Go resolver's stable ordering. The package declarations
* themselves are package-scoped, so they collide with an equal import name in
* any contributing source file. */
static void
check_import_usage_and_collisions(Checker *c, Node *file)
{
if (!c->sep_mode) return;
for (Node *u = file->list; u; u = u->next) {
if (u->kind != N_USE || u->imported || u->used || u->str == NULL)
continue;
const char *path = u->usepath ? u->usepath : u->str;
const char *dot = strrchr(path, '.');
const char *leaf = dot ? dot + 1 : path;
if (strcmp(u->str, leaf) == 0)
err(c, u->pos, "\"%s\" imported and not used", path);
else
err(c, u->pos, "\"%s\" imported as %s and not used",
path, u->str);
}
for (Node *d = file->list; d; d = d->next) {
if (d->imported || !top_decl_kind(d) || d->str == NULL)
continue;
for (Node *u = file->list; u; u = u->next) {
if (u->kind != N_USE || u->imported || u->str == NULL
|| strcmp(d->str, u->str) != 0)
continue;
const char *path = u->usepath ? u->usepath : u->str;
err(c, d->pos,
"%s already declared through import of package %s (\"%s\")",
d->str, u->str, path);
check_import_alt(u, d->str);
}
}
}
void
check_file(Checker *c, Node *file)
{
@@ -3148,21 +3245,30 @@ check_file(Checker *c, Node *file)
* already emits the qualified call. wwstage twin in check.ww. */
if (c->is_test) {
int present = 0;
for (Node *u = file->list; u; u = u->next)
for (Node *u = file->list; u; u = u->next) {
if (c->test_target != NULL && u->kind == N_USE
&& !u->imported && u->usepath
&& (strcmp(u->usepath, c->test_target) == 0
|| strcmp(u->usepath, c->test_module) == 0))
u->used = 1;
if (u->kind == N_USE && !u->imported
&& u->usepath && strcmp(u->usepath, c->test_module) == 0) {
present = 1;
break;
}
}
if (!present) {
Node *usenode = newnode(c->a, N_USE, file->pos);
usenode->str = c->test_module;
usenode->strlen = strlen(c->test_module);
usenode->usepath = c->test_module;
usenode->pkgname = file->pkgname;
usenode->sourceid = file->sourceid;
if (c->test_target != NULL) usenode->used = 1;
usenode->next = file->list;
file->list = usenode;
}
}
check_import_redeclarations(c, file);
/* pass 1: install names (types first, then defs/fns).
* For self-referential types we install the named-type placeholder
@@ -3172,13 +3278,10 @@ check_file(Checker *c, Node *file)
* references (`strconv.invalid`) resolve when typedecl bodies are
* walked in the next pass. */
for (Node *d = file->list; d; d = d->next) {
c->cur_source = d->sourceid;
if (d->kind == N_USE) {
/* check-(c) self-import: a package may not import
* itself. Pure owner==leaf string compare, package-
* model-independent — sound under ww's filename-keyed
* file-inclusion imports. check-(a) unused and
* (b)/(d) membership DEFERRED to task #8 (filename-
* keyed pulls lack import->file->symbol provenance). */
/* A package may not import its own canonical owner. Import
* usage and membership are checked with source-file provenance. */
const char *owner = decl_mod(file, d);
/* M1 #22: self-import ⟺ the imported path equals the
* use's own (owning) module path. Compares paths, not
@@ -3233,9 +3336,11 @@ check_file(Checker *c, Node *file)
* here. A foldable stub carries type NULL until then. A duplicate
* (prev already bound non-USE) is left for that loop to diagnose. */
c->cur_mod = NULL;
c->cur_source = 0;
for (Node *d = file->list; d; d = d->next) {
if (d->kind != N_DEF) continue;
c->cur_mod = decl_mod(file, d);
c->cur_source = d->sourceid;
const char *mod = decl_mod(file, d);
if (same_import_fact(c, d, mod, SK_DEF) != NULL)
continue;
@@ -3250,13 +3355,16 @@ check_file(Checker *c, Node *file)
}
}
c->cur_mod = NULL;
c->cur_source = 0;
for (Node *d = file->list; d; d = d->next) {
if (d->kind != N_TYPEDECL) continue;
resolve_typedecl(c, d);
}
c->cur_mod = NULL;
c->cur_source = 0;
for (Node *d = file->list; d; d = d->next) {
c->cur_mod = decl_mod(file, d);
c->cur_source = d->sourceid;
switch (d->kind) {
case N_USE:
/* already installed in pass 1; no-op here so the
@@ -3360,6 +3468,7 @@ check_file(Checker *c, Node *file)
}
}
c->cur_mod = NULL;
c->cur_source = 0;
/* Program-global uniqueness on the ENTRY `main`. M1 #32: the entry
* is the ROOT-unit main (imported==0) — it alone lowers to the bare
@@ -3487,8 +3596,15 @@ check_file(Checker *c, Node *file)
nm->strlen = strlen(d->str);
Node *id;
if (d->imported && d->module && d->module[0]) {
const char *dotp = strrchr(d->module, '.');
const char *alias = dotp ? dotp + 1 : d->module;
const char *alias = d->pkgname && d->pkgname[0]
? d->pkgname : d->module;
/* A generated dispatcher cannot bind a command test
* package as `main`: that would collide with its own entry.
* This is a compiler-owned binding, never source alias syntax;
* use the canonical target path as its private qualifier. */
if (c->test_target != NULL
&& strcmp(d->module, c->test_target) == 0)
alias = c->test_target;
id = newnode(c->a, N_DOT, fp);
id->lhs = newnode(c->a, N_IDENT, fp);
id->lhs->str = alias;
@@ -3542,6 +3658,8 @@ check_file(Checker *c, Node *file)
tab = newnode(c->a, N_LET, fp);
tab->op = TK_CONST;
tab->str = "__wwtests";
tab->pkgname = file->pkgname;
tab->sourceid = file->sourceid;
tab->lhs = tsl;
tab->rhs = arr;
/* pass 1 already ran, so install the table's name now —
@@ -3581,10 +3699,15 @@ check_file(Checker *c, Node *file)
Node *m = newnode(c->a, N_FNDECL, fp);
m->str = "main";
m->export = 1;
m->pkgname = file->pkgname;
m->sourceid = file->sourceid;
m->lhs = newnode(c->a, N_TNAME, fp);
m->lhs->str = "i32";
m->body = body;
int savesource = c->cur_source;
c->cur_source = m->sourceid;
m->type = build_fn_type(c, m);
c->cur_source = savesource;
/* pass 1 already ran, so the install loop never stamped m's
* type; set it explicitly (pass 2 below reads d->type).
* Append the table const (if any) then main to file->list. */
@@ -3603,6 +3726,7 @@ check_file(Checker *c, Node *file)
/* pass 2: check def initialisers and fn bodies */
for (Node *d = file->list; d; d = d->next) {
c->cur_mod = decl_mod(file, d);
c->cur_source = d->sourceid;
switch (d->kind) {
case N_DEF: {
if (d->rhs) {
@@ -3786,6 +3910,8 @@ check_file(Checker *c, Node *file)
}
}
c->cur_mod = NULL;
c->cur_source = 0;
check_import_usage_and_collisions(c, file);
/*
* #6 harec-fidelity (ref/harec/src/check.c:3941): a @test fn is