ww: resolve self-named import to the dir-package, not a sibling file (M4 E3, #98)

The driver searchpath is srcd-first (srcd = the entry file's directory).
A co-located black-box test lib/<mod>/<mod>test.ww makes srcd=lib/<mod>,
so resolving `import <mod>` hit the sibling-FILE branch lib/<mod>/<mod>.ww
and folded it inline into the consumer unit under the wrong module tag
("package <mod> does not match import path <importer>") — 7 lib-run tests
fail under separate compilation. The combined amalgamator tolerated the
co-location; only sep surfaced it.

Resolve a package directory-first: walk ALL searchpath entries for a
directory match, and only fall back to a file match if no directory
exists anywhere. A dir-package now beats a same-named sibling file (fixes
the self-named shadow), while a leaf package with no directory (e.g.
lib/encoding/hex) still resolves via its file. This realizes the driver's
"a module is the directory" intent; the originally-specced per-directory
suppression was rejected because it broke leaf packages (rob-pike). Both
stages (cmd/ww/main.c + selfhost twin). The dir-beats-earlier-file
precedence change is latent and loud-failing (#101).

Move-set: ww + ww_ww (driver) only; w6c_ww/wwdump_ww/w6a_ww/w6l_ww HOLD.
Gate: test/wcc/989_coloimport_sep.c (table-driven, both stages).
This commit is contained in:
2026-06-18 12:09:25 +09:00
parent eb6083e54a
commit 200f51ca94
8 changed files with 421 additions and 119 deletions

View File

@@ -3112,14 +3112,14 @@ fn importpathform(name: *u8, namelen: u64) *u8 = {
return buf.ptr;
};
// Try <dir>/<path>/ as a directory, then <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
// 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).
fn locatein(dir: *u8, dirlen: u64,
pathform: *u8, pflen: u64, isdir: *i32) *u8 = {
pathform: *u8, pflen: u64, isdir: *i32, wantdir: i32) *u8 = {
let buf: []u8 = alloc([], (os.PATH_MAX: u64))!;
let off: u64 = 0u64;
let i: u64 = 0u64;
@@ -3129,63 +3129,71 @@ fn locatein(dir: *u8, dirlen: u64,
i = 0u64;
for (i < pflen) { buf[off + i] = pathform[i]; i += 1u64; };
off += pflen;
if (wantdir != 0i32) {
buf[off] = 0u8;
let fi: os.filestat;
let r: (void | os.oserror) = os.stat(&fi, pathstr(buf.ptr));
match (r) {
case void => {
let t: u32 = (fi.mode: u32) & 61440u32; // S_IFMT
if (t == os.mode.DIR: u32) {
*isdir = 1;
return buf.ptr;
};
};
case let e: os.oserror => void;
};
return nil;
};
buf[off] = 46u8; off += 1u64; // '.'
buf[off] = 119u8; off += 1u64; // 'w'
buf[off] = 119u8; off += 1u64; // 'w'
buf[off] = 0u8;
let fi: os.filestat;
let r: (void | os.oserror) = os.stat(&fi, pathstr(buf.ptr));
let isdirhit: bool = false;
match (r) {
case void => {
let t: u32 = (fi.mode: u32) & 61440u32; // S_IFMT
if (t == os.mode.DIR: u32) { isdirhit = true; };
};
case let e: os.oserror => void;
};
if (isdirhit) {
*isdir = 1;
return buf.ptr;
};
let buf2: []u8 = alloc([], (os.PATH_MAX: u64))!;
off = 0u64;
i = 0u64;
for (i < dirlen) { buf2[off + i] = dir[i]; i += 1u64; };
off += dirlen;
buf2[off] = 47u8; off += 1u64;
i = 0u64;
for (i < pflen) { buf2[off + i] = pathform[i]; i += 1u64; };
off += pflen;
buf2[off] = 46u8; off += 1u64; // '.'
buf2[off] = 119u8; off += 1u64; // 'w'
buf2[off] = 119u8; off += 1u64; // 'w'
buf2[off] = 0u8;
if (os.access(pathstr(buf2.ptr), 0i32) == 0) {
if (os.access(pathstr(buf.ptr), 0i32) == 0) {
*isdir = 0;
return buf2.ptr;
return buf.ptr;
};
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 (under --sep) fold
// 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 = {
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 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 seglen: u64 = q - p;
if (seglen > 0u64) {
let hit: *u8 = locatein(dirs + p, seglen,
pathform, pflen, isdir);
if (hit != nil) { return hit; };
};
p = q + 1u64;
wantdir -= 1i32;
};
return nil;
};

View File

@@ -238,14 +238,14 @@ fn importpathform(name: *u8, namelen: u64) *u8 = {
return buf.ptr;
};
// Try <dir>/<path>/ as a directory, then <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
// 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).
fn locatein(dir: *u8, dirlen: u64,
pathform: *u8, pflen: u64, isdir: *i32) *u8 = {
pathform: *u8, pflen: u64, isdir: *i32, wantdir: i32) *u8 = {
let buf: []u8 = alloc([], (os.PATH_MAX: u64))!;
let off: u64 = 0u64;
let i: u64 = 0u64;
@@ -255,63 +255,71 @@ fn locatein(dir: *u8, dirlen: u64,
i = 0u64;
for (i < pflen) { buf[off + i] = pathform[i]; i += 1u64; };
off += pflen;
if (wantdir != 0i32) {
buf[off] = 0u8;
let fi: os.filestat;
let r: (void | os.oserror) = os.stat(&fi, pathstr(buf.ptr));
match (r) {
case void => {
let t: u32 = (fi.mode: u32) & 61440u32; // S_IFMT
if (t == os.mode.DIR: u32) {
*isdir = 1;
return buf.ptr;
};
};
case let e: os.oserror => void;
};
return nil;
};
buf[off] = 46u8; off += 1u64; // '.'
buf[off] = 119u8; off += 1u64; // 'w'
buf[off] = 119u8; off += 1u64; // 'w'
buf[off] = 0u8;
let fi: os.filestat;
let r: (void | os.oserror) = os.stat(&fi, pathstr(buf.ptr));
let isdirhit: bool = false;
match (r) {
case void => {
let t: u32 = (fi.mode: u32) & 61440u32; // S_IFMT
if (t == os.mode.DIR: u32) { isdirhit = true; };
};
case let e: os.oserror => void;
};
if (isdirhit) {
*isdir = 1;
return buf.ptr;
};
let buf2: []u8 = alloc([], (os.PATH_MAX: u64))!;
off = 0u64;
i = 0u64;
for (i < dirlen) { buf2[off + i] = dir[i]; i += 1u64; };
off += dirlen;
buf2[off] = 47u8; off += 1u64;
i = 0u64;
for (i < pflen) { buf2[off + i] = pathform[i]; i += 1u64; };
off += pflen;
buf2[off] = 46u8; off += 1u64; // '.'
buf2[off] = 119u8; off += 1u64; // 'w'
buf2[off] = 119u8; off += 1u64; // 'w'
buf2[off] = 0u8;
if (os.access(pathstr(buf2.ptr), 0i32) == 0) {
if (os.access(pathstr(buf.ptr), 0i32) == 0) {
*isdir = 0;
return buf2.ptr;
return buf.ptr;
};
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 (under --sep) fold
// 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 = {
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 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 seglen: u64 = q - p;
if (seglen > 0u64) {
let hit: *u8 = locatein(dirs + p, seglen,
pathform, pflen, isdir);
if (hit != nil) { return hit; };
};
p = q + 1u64;
wantdir -= 1i32;
};
return nil;
};