compiler: load imports and enforce package exports

This commit is contained in:
2026-08-11 22:25:59 +09:00
parent 7bc96b4e03
commit db96422f74
14 changed files with 894 additions and 118 deletions

View File

@@ -59,6 +59,9 @@ 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 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);
static Type *
@@ -69,7 +72,7 @@ resolve_typename(Checker *c, Node *n)
if (bi) return bi;
/* #225: kind-filtered so a same-named value binding (param/let/fn)
* in a closer scope can't hide the type binding it shadows. */
Sym *s = scope_lookup_type(c->cur, c->cur_mod, nm);
Sym *s = lookup_visible_type(c, nm);
if (s == NULL && nm) {
/* module-qualified: io.stream → strip the last dot prefix
* and look up the leaf, filtering on the importing module's
@@ -88,9 +91,9 @@ resolve_typename(Checker *c, Node *n)
* import path (symbols are path-keyed). */
const char *mk = use_path(c->file, c->cur_mod,
head);
if (mk == NULL) mk = head;
s = scope_lookup_in_module(c->cur, mk,
dot + 1);
if (mk != NULL)
s = scope_lookup_in_module(c->cur, mk,
dot + 1);
}
}
}
@@ -422,7 +425,7 @@ assignable_addrfn(Checker *c, Type *dst, Node *rhs)
if (rhs->kind != N_UN || rhs->op != TK_AMP) return 0;
Node *id = rhs->lhs;
if (id == NULL || id->kind != N_IDENT || id->str == NULL) return 0;
Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, id->str);
Sym *s = lookup_visible(c, id->str);
if (s == NULL || s->kind != SK_FN) return 0;
Type *fnty = s->type;
if (fnty == NULL || fnty->kind != TY_FN) return 0;
@@ -606,7 +609,7 @@ eval_def_const(Checker *c, Node *n, u64 *out, int depth)
return 1;
}
case N_IDENT: {
Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, n->str);
Sym *s = lookup_visible(c, n->str);
if (s == NULL || s->kind != SK_DEF ||
s->decl == NULL || s->decl->rhs == NULL)
return 0;
@@ -616,7 +619,7 @@ eval_def_const(Checker *c, Node *n, u64 *out, int depth)
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);
if (mk == NULL) mk = 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 ||
s->decl == NULL || s->decl->rhs == NULL)
@@ -1414,7 +1417,7 @@ cexpr(Checker *c, Node *n)
if (n->str && n->str[0] == '\0')
return n->type = err(c, n->pos,
"`_` is only valid as a binding or discard lvalue");
Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, n->str);
Sym *s = lookup_visible(c, n->str);
if (s == NULL)
return n->type = err(c, n->pos, "undefined: %s", n->str);
/* SK_USE has no concrete value type; the only legal use is
@@ -1444,8 +1447,7 @@ cexpr(Checker *c, Node *n)
* Color sitting at the head of the flat scope chain —
* symmetric with wwstage's enumlookup graduation. */
if (n->lhs && n->lhs->kind == N_IDENT) {
Sym *ms = scope_lookup_prefer(c->cur, c->cur_mod,
n->lhs->str);
Sym *ms = lookup_visible(c, n->lhs->str);
if (ms && (ms->kind == SK_USE || ms->use_alias)) {
/* Module-qualified ref. `use_alias` covers
* the self-import case where the module's
@@ -1460,17 +1462,28 @@ cexpr(Checker *c, Node *n)
* that path before looking up the leaf. */
const char *mk = use_path(c->file, c->cur_mod,
n->lhs->str);
if (mk == NULL) mk = n->lhs->str;
Sym *fs = scope_lookup_in_module(c->cur,
mk, n->str);
if (fs)
return n->type = fs->type;
if (ms->kind == SK_USE) {
/* Pure SK_USE with missing leaf:
* external declaration. Codegen
* emits CALL/MOVQ by the leaf name
* and the linker resolves it. */
return n->type = ty_err;
if (mk == NULL) {
if (ms->kind == SK_USE)
return n->type = err(c, n->pos,
"package '%s' is not directly imported",
n->lhs->str);
} else {
Sym *fs = scope_lookup_in_module(c->cur,
mk, n->str);
if (fs)
return n->type = fs->type;
/* A bare `w6c -T` intentionally leaves the
* compiler-generated test.run hook external; the
* ordinary driver supplies lib/test. This is the only
* missing direct member that is not a package export
* error. */
if (c->sep_mode && ms->kind == SK_USE
&& n != c->synth_test_run)
return n->type = err(c, n->pos,
"package '%s' has no exported declaration '%s'",
n->lhs->str, n->str);
if (ms->kind == SK_USE)
return n->type = ty_err;
}
/* SK_TYPE with use_alias=1 and no leaf
* found: fall through so the enum / type-
@@ -1616,8 +1629,7 @@ cexpr(Checker *c, Node *n)
* def-as-constant splice-index is deferred (no
* consumer). */
if (n->lhs->kind == N_IDENT) {
Sym *s = scope_lookup_prefer(c->cur,
c->cur_mod, n->lhs->str);
Sym *s = lookup_visible(c, n->lhs->str);
if (s && s->kind == SK_DEF)
return n->type = err(c, n->pos,
"cannot index a def-constant str "
@@ -1855,7 +1867,7 @@ cexpr(Checker *c, Node *n)
* (e.g. lib/os/os.ww) keeps working unchanged. */
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
strcmp(n->lhs->str, "abort") == 0 &&
scope_lookup_prefer(c->cur, c->cur_mod, "abort") == NULL) {
lookup_visible(c, "abort") == NULL) {
if (n->list) {
Type *mt = cexpr(c, n->list);
if (mt != ty_err && !type_assignable(ty_str, mt))
@@ -1870,7 +1882,7 @@ cexpr(Checker *c, Node *n)
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
strcmp(n->lhs->str, "assert") == 0 &&
n->list != NULL &&
scope_lookup_prefer(c->cur, c->cur_mod, "assert") == NULL) {
lookup_visible(c, "assert") == NULL) {
Type *ct = cexpr(c, n->list);
if (ct != ty_err && ct != ty_bool && ct != ty_untyped_bool)
err(c, n->pos, "assert: cond must be bool");
@@ -2019,8 +2031,7 @@ cexpr(Checker *c, Node *n)
return n->type = ty_void;
}
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str) {
Sym *s = scope_lookup_prefer(c->cur, c->cur_mod,
n->lhs->str);
Sym *s = lookup_visible(c, n->lhs->str);
if (s && s->is_const)
err(c, n->pos, "cannot assign to const `%s`",
n->lhs->str);
@@ -2072,8 +2083,7 @@ cexpr(Checker *c, Node *n)
* resolve_type for the synthetic-type-expr case. */
Type *t = NULL;
if (n->lhs && n->lhs->kind == N_IDENT) {
Sym *s = scope_lookup_prefer(c->cur, c->cur_mod,
n->lhs->str);
Sym *s = lookup_visible(c, n->lhs->str);
if (s == NULL || s->kind != SK_TYPE)
t = err(c, n->pos, "unknown struct type '%s'",
n->lhs->str);
@@ -2865,28 +2875,34 @@ decl_mod(Node *file, Node *d)
* 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; preferring it closes
* the cross-module mis-resolution a first-match scan caused. Falls back
* to any matching alias when the referencing module has no own import
* (single-occurrence case, unchanged). Returns NULL if no such `use`.
* reference (`curmod`) is the authoritative one; requiring it closes both
* cross-module mis-resolution and accidental transitive-import visibility.
* Returns NULL if the referencing package has no such `use`.
*/
static const char *
use_path(Node *file, const char *curmod, const char *alias)
{
if (file == NULL || alias == NULL) return NULL;
const char *any = 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) {
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)
continue;
const char *p = u->usepath ? u->usepath : u->str;
int same = (u->module == NULL) ? (curmod == NULL)
: (curmod != NULL && strcmp(u->module, curmod) == 0);
const char *um = decl_mod(file, u);
int same = (um == NULL) ? (curmod == NULL)
: (curmod != NULL && strcmp(um, curmod) == 0);
if (same)
return p;
if (any == NULL) any = p;
}
return any;
return NULL;
}
/*
@@ -2967,6 +2983,67 @@ src_imports(Node *file, const char *modtag, const char *name)
return 0;
}
/* A flattened interface symbol is source-visible as a bare name only when
* the referencing package directly imports the symbol's defining path. The
* strict scope helpers above handle lexical locals, builtins and same-package
* declarations first; this second pass restores WW's existing direct-import
* convenience without admitting a transitive interface by accident. */
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;
}
static Sym *
lookup_visible(Checker *c, const char *name)
{
Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, name);
/* A real source declaration shadows a decl-less pseudo-builtin. Keep
* the builtin as fallback while checking direct imported declarations. */
Sym *builtin = NULL;
if (s != NULL) {
if (s->decl != NULL) return s;
builtin = s;
}
/* N_USE entries are historically coalesced by leaf in the flat scope,
* so the retained symbol may carry another source package's owner. The
* 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) {
for (Scope *p = c->cur; p; p = p->parent)
for (Sym *b = p->first; b; b = b->next)
if (strcmp(b->name, name) == 0
&& (b->kind == SK_USE || b->use_alias))
return b;
}
for (Scope *p = c->cur; p; p = p->parent) {
for (Sym *b = p->first; b; b = b->next)
if (strcmp(b->name, name) == 0
&& direct_module_visible(c, b->mod))
return b;
}
return builtin;
}
static Sym *
lookup_visible_type(Checker *c, const char *name)
{
Sym *s = scope_lookup_type(c->cur, c->cur_mod, name);
if (s != NULL) return s;
for (Scope *p = c->cur; p; p = p->parent) {
for (Sym *b = p->first; b; b = b->next)
if (b->kind == SK_TYPE && strcmp(b->name, name) == 0
&& direct_module_visible(c, b->mod))
return b;
}
return NULL;
}
/*
* check_module_shadow — refuse value bindings that shadow an
* in-scope imported module bareword. "Value names and module names
@@ -3402,6 +3479,7 @@ check_file(Checker *c, Node *file)
dot->lhs = newnode(c->a, N_IDENT, fp);
dot->lhs->str = "test";
dot->str = "run";
c->synth_test_run = dot;
call->lhs = dot;
call->list = arg;
Node *ret = newnode(c->a, N_RETURN, fp);