cmd: resolve source imports only as directories

Separate import lookup from raw CLI target compatibility in both production drivers. Package scans and test-support edges now create directory nodes only, while unit composition emits only the owning package's sorted sources after direct export data. Document the Go 1.26.5 ownership evidence and retained single-file root boundary.
This commit is contained in:
2026-08-12 17:12:49 +09:00
parent c7d9dc92de
commit 0af13ea501
3 changed files with 272 additions and 339 deletions

View File

@@ -284,12 +284,9 @@ fn importpathform(name: *u8, namelen: u64) *u8 = {
return buf.ptr;
};
// Try <dir>/<path>/ as a directory (wantdir != 0), else <dir>/<path>.ww
// as a file. Sets *isdir on hit. Symmetric with cstage locate_import_in
// for byte-id driver output (rule 10). The legacy <dir>/<name>/<name>.ww
// form was dropped in task #22 — directory-as-module enumeration
// replaces it, mirroring ref/hare/hare/module/srcs.ha (Hare has no
// `foo/foo.ha` fallback; a module IS the directory).
// Try <dir>/<path>/ as a directory (wantdir != 0), else
// <dir>/<path>.ww as a CLI target. Source-import loading calls only the
// directory arm; the file arm belongs exclusively to locatemodule.
fn locatein(dir: *u8, dirlen: u64,
pathform: *u8, pflen: u64, isdir: *i32, wantdir: i32) *u8 = {
let tail: u64 = 1u64; // NUL for the directory form
@@ -333,44 +330,55 @@ fn locatein(dir: *u8, dirlen: u64,
return nil;
};
// Walk a colon-separated dirlist, return first hit or nil. Sets
// *isdir on hit.
//
// #98: "a module IS the directory" — a directory-package on ANY entry
// wins over a same-named sibling FILE on an EARLIER entry. The driver
// builds the searchpath srcd-first; a co-located `lib/<mod>/<mod>test.ww`
// entry makes srcd = lib/<mod>, so a self-named `import <mod>` would
// else file-hit the sibling lib/<mod>/<mod>.ww and fold it
// inline under the wrong module-reset. Two passes — directories first,
// files only if no directory matches anywhere — let lib/<mod>/ resolve
// as the dir while a genuine leaf package with no directory (e.g.
// lib/encoding/hex imported bare as `hex`, reachable only via its file
// in srcd) still resolves in the file pass. Latent: a dir-package now
// beats an earlier-entry same-named sibling FILE — loud-failing, none
// in the corpus; tracked as #101.
fn locateimport(dirs: *u8, name: *u8, namelen: u64,
isdir: *i32) *u8 = {
// Walk every ordered root for <root>/<path>/ only. A sibling or earlier-root
// <path>.ww is neither a match nor a shadow: every source import identifies
// one canonical directory package.
fn locateimport(dirs: *u8, name: *u8, namelen: u64) *u8 = {
let pathform: *u8 = importpathform(name, namelen);
let pflen: u64 = cstrlen(pathform);
let total: u64 = cstrlen(dirs);
let wantdir: i32 = 1i32;
for (wantdir >= 0i32) {
let p: u64 = 0u64;
for (p < total) {
let q: u64 = p;
for (q < total) {
if (dirs[q] == ':') { break; };
q += 1u64;
};
let seglen: u64 = q - p;
if (seglen > 0u64) {
let hit: *u8 = locatein(dirs + p, seglen,
pathform, pflen, isdir, wantdir);
if (hit != nil) { return hit; };
};
p = q + 1u64;
let p: u64 = 0u64;
for (p < total) {
let q: u64 = p;
for (q < total) {
if (dirs[q] == ':') { break; };
q += 1u64;
};
wantdir -= 1i32;
let seglen: u64 = q - p;
if (seglen > 0u64) {
let isdir: i32 = 0;
let hit: *u8 = locatein(dirs + p, seglen,
pathform, pflen, &isdir, 1i32);
if (hit != nil) { return hit; };
};
p = q + 1u64;
};
return nil;
};
// CLI target compatibility: directory packages win globally, then a bare
// target may resolve to <root>/<path>.ww. Never called for a source import.
fn locatemodule(dirs: *u8, name: *u8, namelen: u64,
isdir: *i32) *u8 = {
let hit: *u8 = locateimport(dirs, name, namelen);
if (hit != nil) { *isdir = 1; return hit; };
let pathform: *u8 = importpathform(name, namelen);
let pflen: u64 = cstrlen(pathform);
let total: u64 = cstrlen(dirs);
let p: u64 = 0u64;
for (p < total) {
let q: u64 = p;
for (q < total) {
if (dirs[q] == ':') { break; };
q += 1u64;
};
let seglen: u64 = q - p;
if (seglen > 0u64) {
hit = locatein(dirs + p, seglen, pathform, pflen,
isdir, 0i32);
if (hit != nil) { return hit; };
};
p = q + 1u64;
};
return nil;
};
@@ -1240,79 +1248,68 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8,
cerr(": error: import path is too long (limit 255 bytes)\n");
return -1;
};
let isdir: i32 = 0;
let externalproduction: bool = sepexternalproduction(
&g.pkg[pi], idp, idn);
let ipath: *u8 = nil;
if (externalproduction) {
ipath = g.pkg[pi].entry;
isdir = 1;
} else {
ipath = locateimport(searchpath, idp, idn, &isdir);
ipath = locateimport(searchpath, idp, idn);
};
if (ipath != nil) {
if (isdir != 0) {
sepbindadd(bindings, 'D': u8, u.usepath, ipath);
let self: bool = os.samefile(pathstr(ipath),
pathstr(g.pkg[pi].entry));
if (self && sepexternalname(&g.pkg[pi], idp, idn, true)) {
externalproduction = true;
};
if (self
&& !externalproduction) {
cerrpos(u.file, u.line, u.col);
cerr(": error: self-import: package '");
if (g.pkg[pi].path[0u64] != 0u8) {
cerr(pathstr(g.pkg[pi].path));
} else { cerr(pathstr(g.pkg[pi].name)); };
cerr("' cannot import itself\n");
return -1;
};
let runtimeproduction: bool = false;
if (externalproduction) {
let gi: i32 = 0;
for (gi < g.n) {
if (g.pkg[gi].testsupport
&& syntax.streq(pathstr(g.pkg[gi].path),
u.usepath)
&& os.samefile(pathstr(g.pkg[gi].entry),
pathstr(ipath))) {
runtimeproduction = true;
};
gi += 1;
sepbindadd(bindings, 'D': u8, u.usepath, ipath);
let self: bool = os.samefile(pathstr(ipath),
pathstr(g.pkg[pi].entry));
if (self && sepexternalname(&g.pkg[pi], idp, idn, true)) {
externalproduction = true;
};
if (self && !externalproduction) {
cerrpos(u.file, u.line, u.col);
cerr(": error: self-import: package '");
if (g.pkg[pi].path[0u64] != 0u8) {
cerr(pathstr(g.pkg[pi].path));
} else { cerr(pathstr(g.pkg[pi].name)); };
cerr("' cannot import itself\n");
return -1;
};
let runtimeproduction: bool = false;
if (externalproduction) {
let gi: i32 = 0;
for (gi < g.n) {
if (g.pkg[gi].testsupport
&& syntax.streq(pathstr(g.pkg[gi].path),
u.usepath)
&& os.samefile(pathstr(g.pkg[gi].entry),
pathstr(ipath))) {
runtimeproduction = true;
};
gi += 1;
};
let nm: []u8 = alloc([], idn + 1u64)!;
let k: u64 = 0u64;
for (k < idn) { nm[k] = idp[k]; k += 1u64; };
nm[idn] = 0u8;
let di: i32 = -1;
if (externalproduction && !runtimeproduction) {
let art: *u8 = appendlit(g.pkg[pi].artifact,
"-production");
di = sepfindoraddrole(g, nm.ptr, ipath, 1,
SEP_ROLE_EXTERNAL_PRODUCTION, art);
} else {
di = sepfindoradd(g, nm.ptr, ipath, 1);
};
if (di < 0) { return -1; };
let seen: bool = false;
let m: i32 = 0;
for (m < g.pkg[pi].ndeps) {
if (g.pkg[pi].deps[m] == di) { seen = true; };
m += 1;
};
if (!seen) {
if (g.pkg[pi].ndeps >= SEP_MAXPKG) { return -1; };
g.pkg[pi].deps[g.pkg[pi].ndeps] = di;
g.pkg[pi].ndeps += 1;
};
};
let nm: []u8 = alloc([], idn + 1u64)!;
let k: u64 = 0u64;
for (k < idn) { nm[k] = idp[k]; k += 1u64; };
nm[idn] = 0u8;
let di: i32 = -1;
if (externalproduction && !runtimeproduction) {
let art: *u8 = appendlit(g.pkg[pi].artifact,
"-production");
di = sepfindoraddrole(g, nm.ptr, ipath, 1,
SEP_ROLE_EXTERNAL_PRODUCTION, art);
} else {
sepbindadd(bindings, 'F': u8, u.usepath, ipath);
if (sepscanfile(g, pi, ipath, searchpath, fv,
bindings, 0) < 0) {
return -1;
};
di = sepfindoradd(g, nm.ptr, ipath, 1);
};
if (di < 0) { return -1; };
let seen: bool = false;
let m: i32 = 0;
for (m < g.pkg[pi].ndeps) {
if (g.pkg[pi].deps[m] == di) { seen = true; };
m += 1;
};
if (!seen) {
if (g.pkg[pi].ndeps >= SEP_MAXPKG) { return -1; };
g.pkg[pi].deps[g.pkg[pi].ndeps] = di;
g.pkg[pi].ndeps += 1;
};
} else {
let lstart: u64 = 0u64;
@@ -1324,11 +1321,13 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8,
let leafp: *u8 = idp + lstart;
let leafn: u64 = idn - lstart;
let inlinepackage: bool = false;
let pm: *syntax.node = imports.body;
for (pm != nil) {
if (bytecmp(pm.nmod.ptr, pm.nmod.len: u64,
leafp, leafn) == 0) { inlinepackage = true; };
pm = pm.next;
if (g.pkg[pi].isdir == 0) {
let pm: *syntax.node = imports.body;
for (pm != nil) {
if (bytecmp(pm.nmod.ptr, pm.nmod.len: u64,
leafp, leafn) == 0) { inlinepackage = true; };
pm = pm.next;
};
};
if (!inlinepackage) {
cerrpos(u.file, u.line, u.col);
@@ -1547,10 +1546,8 @@ fn sepvalidatemoduleclosure(g: *sepgraph, order: []i32, n: i32,
return 0;
};
// Emit one of pi's own source files into the sep-unit under the
// //ww:module-reset primary boundary (so -c emits its decls, imported
// ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead);
// FILE imports fold in (intra-package split).
// Imported source never enters this body: its direct export data was
// prepended above, while this package owns exactly its sorted source set.
fn sepwriteall(fd: i32, buf: *u8, n: u64) bool = {
match (os.writeall(fd, buf, n)) {
case let wrote: i64 => return wrote == n: i64;
@@ -1558,14 +1555,7 @@ fn sepwriteall(fd: i32, buf: *u8, n: u64) bool = {
};
};
fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8,
modpath: *u8, pkg: *seppkg) i32 = {
let pview: str;
pview.ptr = path;
pview.len = cstrlen(path): i32;
let pdup: str = strings.dup(pview);
if (visitseen(visit, pdup)) { return 0; };
visitadd(visit, pdup);
fn sepemitbody(fd: i32, path: *u8, modpath: *u8) i32 = {
let bufp: *u8;
let blen: u64;
bufp, blen = slurp(path);
@@ -1573,64 +1563,6 @@ fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8,
cerr("ww: cannot read source\n");
return -1;
};
let l: syntax.lex;
syntax.lexinit(&l, pdup, bufp, blen);
let ps: syntax.parser;
syntax.parserinit(&ps, &l);
let imports: *syntax.node = syntax.parseimports(&ps);
if (l.errs > 0 || ps.errs > 0) { return -1; };
let nuse: i32 = 0;
let u: *syntax.node = imports.list;
for (u != nil) {
if (u.kind == syntax.nkind.N_USE) { nuse += 1; };
u = u.next;
};
let uses: []*syntax.node = [];
if (nuse > 0) {
let allocated: []*syntax.node = alloc([], nuse: u64)!;
uses = allocated;
uses.len = nuse;
};
let ui: i32 = 0;
u = imports.list;
for (u != nil) {
if (u.kind == syntax.nkind.N_USE) { uses[ui] = u; ui += 1; };
u = u.next;
};
let si: i32 = 1;
for (si < nuse) {
let sj: i32 = si;
for (sj > 0) {
if (strings.compare(uses[sj - 1].usepath,
uses[sj].usepath) <= 0) { sj = 0; }
else {
let t: *syntax.node = uses[sj];
uses[sj] = uses[sj - 1];
uses[sj - 1] = t;
sj -= 1;
};
};
si += 1;
};
ui = 0;
for (ui < nuse) {
u = uses[ui];
let idp: *u8 = u.usepath.ptr;
let idn: u64 = u.usepath.len: u64;
if (sepexternalproduction(pkg, idp, idn)) {
ui += 1;
continue;
};
let isdir: i32 = 0;
let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir);
if (ipath != nil) {
if (isdir == 0) {
if (sepemitbody(fd, ipath, visit, searchpath,
modpath, pkg) < 0) { return -1; };
};
};
ui += 1;
};
// #57: tag the primary body by its full dotted import path so the
// definer mangles == the importer reference; a root build (path "")
// stays a bare reset (keeps bare main).
@@ -1665,7 +1597,6 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8,
unitf: *u8) i32 = {
if (g.pkg[pi].emitcontext < 0
|| g.pkg[pi].emitcontext >= g.ncontext) { return -1; };
let searchpath: *u8 = g.context[g.pkg[pi].emitcontext].searchpath;
let u: i32 = os.open(pathstr(unitf), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
if (u < 0) {
cerr("ww: cannot open unit\n");
@@ -1696,21 +1627,15 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8,
};
k += 1;
};
let bv: expctx;
bv.out = u;
bv.dirs = searchpath;
bv.visit = nil;
let bodyrc: i32 = 0;
if (g.pkg[pi].isdir != 0) {
let i: i32 = 0;
for (i < g.pkg[pi].nsources && bodyrc == 0) {
bodyrc = sepemitbody(u, g.pkg[pi].sources[i], &bv, searchpath,
g.pkg[pi].path, &g.pkg[pi]);
bodyrc = sepemitbody(u, g.pkg[pi].sources[i], g.pkg[pi].path);
i += 1;
};
} else {
bodyrc = sepemitbody(u, g.pkg[pi].entry, &bv, searchpath,
g.pkg[pi].path, &g.pkg[pi]);
bodyrc = sepemitbody(u, g.pkg[pi].entry, g.pkg[pi].path);
};
if (os.close(u) != 0) {
cerr("ww: cannot close package unit\n");
@@ -2226,9 +2151,9 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
// when user source occupies that identity, the reserved graph alias keeps
// it distinct. The linker receives the same support archive closure.
if (istest != 0) {
let td: i32 = 0;
let td: i32 = 1;
let tp: *u8 = locateimport(toolsrcdir, "test".ptr,
"test".len: u64, &td);
"test".len: u64);
if (tp != nil) {
g.supportcontext = sepcontextfor(g, toolsrcdir, nil,
toolsrcdir);
@@ -2250,11 +2175,10 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
};
producti = 0;
for (producti < nproducts && !collision) {
let ud: i32 = 0;
let up: *u8 = locateimport(
g.context[products[producti].context].searchpath,
"test".ptr,
"test".len: u64, &ud);
"test".len: u64);
if (up != nil && !os.samefile(pathstr(tp), pathstr(up))) {
collision = true;
};
@@ -2927,7 +2851,7 @@ fn resolvemodule(selfdir: *u8, name: *u8, incs: *u8, isdir: *i32) *u8 = {
};
let search: *u8 = buildsearchpath(selfdir, incs);
return locateimport(search, name, nlen, isdir);
return locatemodule(search, name, nlen, isdir);
};
fn writeusage(fd: i32) void = {