ww: add file-scoped import aliases

This commit is contained in:
2026-08-14 10:56:34 +09:00
parent 351f4a25bc
commit 792b6ecbe5
14 changed files with 587 additions and 430 deletions

View File

@@ -173,12 +173,11 @@ fn declmod(file: *syntax.node, d: *syntax.node) str = {
return empty;
};
// usepath — 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 codegen hint (M1 #22). Only an import owned
// by the referencing file is visible; a matching
// alias carried by a transitive interface is deliberately ignored.
fn usepathfor(file: *syntax.node, modtag: str, source: i32, alias: str) str = {
// 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
// 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; };
@@ -198,7 +197,7 @@ fn usepathfor(file: *syntax.node, modtag: str, source: i32, alias: str) str = {
if (um.len == 0) { same = true; };
} else { if (syntax.streq(um, modtag)) { same = true; }; };
if (same) {
u.used = 1;
if (mark) { u.used = 1; };
if (u.usepath.len != 0) { return u.usepath; };
return u.str;
};
@@ -209,6 +208,10 @@ fn usepathfor(file: *syntax.node, modtag: str, source: i32, alias: str) str = {
return empty;
};
fn usepathfor(file: *syntax.node, modtag: str, source: i32, alias: str) str = {
return findusepath(file, modtag, source, alias, true);
};
// modkeyfor — the module key for a directly imported alias. Empty means
// the referencing package did not itself declare that import.
fn modkeyfor(c: *checker, alias: str) str = {
@@ -250,31 +253,6 @@ fn srcimports(file: *syntax.node, modtag: str, source: i32, name: str) bool = {
return false;
};
// Bare imported declarations remain WW source syntax, but only across a
// direct edge. The syntax scope helpers resolve lexical locals, builtins and
// same-package declarations first; this pass admits a flattened interface
// symbol iff the referencing source package itself imported its module path.
fn directmodvisible(c: *checker, mod: str) bool = {
if (mod.len == 0) { return false; };
let u: *syntax.node = c.file.list;
for (u != nil) {
if (u.kind == syntax.nkind.N_USE && u.sourceid == c.cursource) {
let um: str = declmod(c.file, u);
let same: bool = false;
if (c.curmod.len == 0) { same = um.len == 0; }
else { same = syntax.streq(um, c.curmod); };
let path: str = u.usepath;
if (path.len == 0) { path = u.str; };
if (same && syntax.streq(path, mod)) {
u.used = 1;
return true;
};
};
u = u.next;
};
return false;
};
fn lookupvisible(c: *checker, name: str) *syntax.sym = {
let found: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, name);
let builtin: *syntax.sym = nil;
@@ -282,10 +260,10 @@ fn lookupvisible(c: *checker, name: str) *syntax.sym = {
if (found.decl != nil) { return found; };
builtin = found;
};
// Flat scope installation coalesces same-leaf N_USE entries. The
// source-owned alias map, not the retained marker's mod field, decides
// whether this package can use the qualifier.
if (usepathfor(c.file, c.curmod, c.cursource, name).len != 0) {
// Flat scope installation may coalesce equal qualifiers from distinct
// files. A bare mention only locates the marker; the DOT resolution owns
// usage marking.
if (findusepath(c.file, c.curmod, c.cursource, name, false).len != 0) {
let q: *syntax.scope = c.cur;
for (q != nil) {
let u: *syntax.sym = q.first;
@@ -298,17 +276,6 @@ fn lookupvisible(c: *checker, name: str) *syntax.sym = {
q = q.parent;
};
};
let p: *syntax.scope = c.cur;
for (p != nil) {
let b: *syntax.sym = p.first;
for (b != nil) {
if (syntax.streq(b.name, name) && directmodvisible(c, b.mod)) {
return b;
};
b = b.snext;
};
p = p.parent;
};
return builtin;
};
@@ -323,19 +290,6 @@ fn lookupvisibletype(c: *checker, name: str) *syntax.sym = {
};
let found: *syntax.sym = syntax.scopelookuptype(c.cur, c.curmod, name);
if (found != nil) { return found; };
let p: *syntax.scope = c.cur;
for (p != nil) {
let b: *syntax.sym = p.first;
for (b != nil) {
if (b.skind == syntax.skind.SK_TYPE
&& syntax.streq(b.name, name)
&& directmodvisible(c, b.mod)
&& (b.decl == nil || b.decl.imported == 0
|| b.decl.exported != 0)) { return b; };
b = b.snext;
};
p = p.parent;
};
return nil;
};
@@ -7332,6 +7286,22 @@ fn hascvariadic(params: *syntax.node) bool = {
return false;
};
// Normalize a Hare-style variadic parameter before either signature stamping
// or parameter installation. Doing this in the signature pass means an inline
// aggregate element is walked and validated once, under its declaring package,
// rather than being revisited through a newly created slice in Pass 2.
fn normalizevariadicparam(p: *syntax.node) void = {
if (p == nil || p.kind != syntax.nkind.N_PARAM
|| p.op != syntax.tkind.TK_ELLIPSIS || p.lhs == nil
|| p.lhs.kind == syntax.nkind.N_TSLICE) { return; };
let sl: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, "", 0, 0);
sl.lhs = p.lhs;
// op marks this compiler normalization so typeeqast and the interface
// writer can peel exactly this wrapper back to the surface `T...` form.
sl.op = syntax.tkind.TK_ELLIPSIS;
p.lhs = sl;
};
// TODO(#11): cstage check.c (post-#32) errors `param '%s' redeclared`
// when two params share a name. The fn body's scope IS fresh here
// (resolvefnbody opens it before calling us), so guarding scopedefine's
@@ -7350,21 +7320,7 @@ fn installparams(c: *checker, params: *syntax.node) void = {
// `tp->type = type_slice(c->a, pt)` and harec
// check_func_type. Surface-fidelity preserved: wwdump
// -a runs parser only and never reaches this mutation.
if (p.op == syntax.tkind.TK_ELLIPSIS) {
if (p.lhs != nil && p.lhs.kind != syntax.nkind.N_TSLICE) {
let sl: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, "", 0, 0);
sl.lhs = p.lhs;
// op marks the wrapper as THIS normalization, not
// surface syntax, so typeeqast can peel exactly it
// when comparing against an unnormalized fn TYPE
// expr (`let f: fn(args: i64...) void = sum` — the
// decl side reads []i64 here, the let side i64).
// Param-lhs position never carries a tagged spread
// marker, so the op reads stay disjoint.
sl.op = syntax.tkind.TK_ELLIPSIS;
p.lhs = sl;
};
};
normalizevariadicparam(p);
let nm: str = p.str;
if (nm.len > 0) {
checkmoduleshadow(c, nm, "param");
@@ -7390,7 +7346,9 @@ fn resolvefnbody(c: *checker, fnnode: *syntax.node) void = {
let p: *syntax.node = fnnode.list;
for (p != nil) {
if (p.kind == syntax.nkind.N_PARAM) {
if (p.lhs != nil) { resolvewalk(c, p.lhs); };
if (p.lhs != nil && p.lhs.type_ == nil) {
resolvewalk(c, p.lhs);
};
};
p = p.next;
};
@@ -7550,6 +7508,42 @@ fn topdeclkind(d: *syntax.node) bool = {
|| d.kind == syntax.nkind.N_LET);
};
// Record only qualified syntax in each owning source before name resolution.
// This preserves source-position import diagnostics and never lets a failed
// bare lookup consume an ordinary import.
fn markimportusesnode(c: *checker, n: *syntax.node, owner: str,
source: i32) void = {
if (n == nil) { return; };
if (n.kind == syntax.nkind.N_TNAME) {
let (head, leaf) = strings.rcut(n.str, ".");
if (head.len > 0) {
findusepath(c.file, owner, source, head, true);
};
} else { if (n.kind == syntax.nkind.N_DOT && n.lhs != nil
&& n.lhs.kind == syntax.nkind.N_IDENT) {
findusepath(c.file, owner, source, n.lhs.str, true);
}; };
let p: *syntax.node = n.attr;
for (p != nil) { markimportusesnode(c, p, owner, source); p = p.next; };
markimportusesnode(c, n.lhs, owner, source);
markimportusesnode(c, n.rhs, owner, source);
markimportusesnode(c, n.cond, owner, source);
markimportusesnode(c, n.body, owner, source);
markimportusesnode(c, n.els, owner, source);
p = n.list;
for (p != nil) { markimportusesnode(c, p, owner, source); p = p.next; };
};
fn markimportuses(c: *checker, file: *syntax.node) void = {
let d: *syntax.node = file.list;
for (d != nil) {
if (d.kind != syntax.nkind.N_USE) {
markimportusesnode(c, d, declmod(file, d), d.sourceid);
};
d = d.next;
};
};
fn checkimportredeclarations(c: *checker, file: *syntax.node) void = {
if (c.sepmode == 0) { return; };
let u: *syntax.node = file.list;
@@ -7577,7 +7571,8 @@ fn checkimportusageandcollisions(c: *checker, file: *syntax.node) void = {
let u: *syntax.node = file.list;
for (u != nil) {
if (u.kind == syntax.nkind.N_USE && u.imported == 0 && u.used == 0) {
let path: str = u.usepath;
let path: str = u.usesource;
if (path.len == 0) { path = u.usepath; };
if (path.len == 0) { path = u.str; };
let (prefix, suffix) = strings.rcut(path, ".");
let leaf: str = suffix;
@@ -7600,7 +7595,8 @@ fn checkimportusageandcollisions(c: *checker, file: *syntax.node) void = {
for (u != nil) {
if (u.kind == syntax.nkind.N_USE && u.imported == 0
&& syntax.streq(d.str, u.str)) {
let path: str = u.usepath;
let path: str = u.usesource;
if (path.len == 0) { path = u.usepath; };
if (path.len == 0) { path = u.str; };
importdiagprefix(d); cerr(d.str);
cerr(" already declared through import of package ");
@@ -7659,10 +7655,11 @@ fn checkfile(c: *checker, file: *syntax.node) void = {
// earlier module-reset section neither supplies nor uses its binding.
let su: *syntax.node = file.list;
for (su != nil) {
if (c.testtarget.len > 0 && su.kind == syntax.nkind.N_USE
if (su.kind == syntax.nkind.N_USE
&& su.imported == 0 && su.sourceid == file.sourceid
&& (syntax.streq(su.usepath, c.testtarget)
|| syntax.streq(su.usepath, c.testmodule))) {
&& (syntax.streq(su.usepath, c.testmodule)
|| (c.testtarget.len > 0
&& syntax.streq(su.usepath, c.testtarget)))) {
su.used = 1i32;
};
if (su.kind == syntax.nkind.N_USE && su.imported == 0
@@ -7675,15 +7672,18 @@ fn checkfile(c: *checker, file: *syntax.node) void = {
if (!present) {
let usenode: *syntax.node = syntax.newnode(syntax.nkind.N_USE, file.file, file.line, file.col);
usenode.str = c.testmodule;
usenode.usesource = c.testmodule;
usenode.usepath = c.testmodule;
usenode.pkgname = file.pkgname;
usenode.sourceid = file.sourceid;
if (c.testtarget.len > 0) { usenode.used = 1i32; };
usenode.used = 1i32;
usenode.next = file.list;
file.list = usenode;
};
};
markimportuses(c, file);
checkimportredeclarations(c, file);
checkimportusageandcollisions(c, file);
// Pass 1: install all top-level names.
let d: *syntax.node = file.list;
@@ -7956,6 +7956,39 @@ fn checkfile(c: *checker, file: *syntax.node) void = {
};
};
// Resolve every function signature in its declaring package before any
// body can consume it. Cstage's declaration pass builds all function
// types before its body-check pass. Without the same split here, a call
// from an earlier package section to a later declaration can first visit
// the later function's bare same-package return/parameter names while the
// caller's module is current. The historical implicit-import fallback
// happened to hide that owner error when the caller imported the callee;
// ordinary imports must not. The later body walk is idempotent over these
// checker-stamped type nodes. Variadics are normalized before stamping and
// Pass 2 skips those same roots, so inline enum/struct validators still run
// exactly once.
d = file.list;
for (d != nil) {
if (d.kind == syntax.nkind.N_FNDECL) {
c.curmod = declmod(file, d);
c.cursource = d.sourceid;
if (d.lhs != nil && d.lhs.type_ == nil) {
resolvewalk(c, d.lhs);
};
let fp: *syntax.node = d.list;
for (fp != nil) {
if (fp.kind == syntax.nkind.N_PARAM) {
normalizevariadicparam(fp);
if (fp.lhs != nil && fp.lhs.type_ == nil) {
resolvewalk(c, fp.lhs);
};
};
fp = fp.next;
};
};
d = d.next;
};
// Pass 2: walk decl bodies/types and resolve identifiers.
// Track the per-decl module bareword so bare-leaf lookups inside
// the body prefer same-module entries over alphabetically-earlier
@@ -7984,7 +8017,9 @@ fn checkfile(c: *checker, file: *syntax.node) void = {
cerr(": error: C-style variadic '...' requires a bodiless declaration\n");
c.errs += 1;
};
if (d.lhs != nil) { resolvewalk(c, d.lhs); }; // return type
if (d.lhs != nil && d.lhs.type_ == nil) {
resolvewalk(c, d.lhs); // return type
};
resolvefnbody(c, d);
case syntax.nkind.N_DEF:
// #11: a module-level `def xs: [_]T = arrlit;` must infer
@@ -8058,8 +8093,6 @@ fn checkfile(c: *checker, file: *syntax.node) void = {
};
d = d.next;
};
checkimportusageandcollisions(c, file);
// Pass 3 (#15, A.6.2.1e): post-checker invariant gate. Walks each
// decl with its curmod set so asserttyped's gate lookups resolve
// against the same module context exprtype saw during pass 2.