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

@@ -0,0 +1,236 @@
/*
* 989_coloimport_sep — #98 E3 flip-blocker: a co-located `_test` entry must
* not shadow the dir-package it imports under `ww test --sep`.
*
* The driver builds the sep searchpath srcd-first (srcd = the entry file's
* own directory). When the entry is a co-located black-box test
* `lib/<mod>/<mod>test.ww`, srcd = `lib/<mod>`, so a transitive `import
* <mod>` (reached from another package in the closure) hits the sibling
* FILE `lib/<mod>/<mod>.ww` (is_dir=0) FIRST — treated as an intra-package
* split, folded INLINE into the importer's unit under that importer's
* module-reset → w6c rejects `package <mod> does not match import path
* <importer>`. locate_import / locateimport now walk the searchpath in
* TWO passes — pass 1 seeks a DIRECTORY on any entry, pass 2 (only if no
* directory matches anywhere) seeks a FILE — so the dir-package `<mod>/`
* on a later entry beats the same-named sibling FILE on the srcd entry,
* while a genuine leaf package with no directory still resolves in the
* file pass. This gate stands for the 7 lib-run tests
* (904/966/967/972/974/976/977) the bug blocked under sep.
*
* Fixture (test/wcc/data/colo98/, the exact srcd-shadow shape, minimised):
* widget/widget.ww package widget — the sibling primary file
* that shadows the dir.
* widget/widgettest.ww package widget_test, import gadget — the
* co-located test ENTRY;
* srcd = .../widget.
* gadget/gadget.ww package gadget, import widget — the NON-root
* importer compiled with -I,
* whose self-named `import
* widget` triggers the fold.
* `-I test/wcc/data/colo98` puts widget's parent on the fallback path,
* mirroring how `lib` holds the real `lib/<mod>/` dirs.
*
* Asserts (both driver stages, COLD per-stage WW_PKGCACHE):
* 1. RUN-EXIT 0 — the @test resolves, builds, links, runs. Pre-fix the
* non-root importer's fold makes w6c reject → non-zero; this is the
* non-vacuity teeth (verified: revert the two-pass walker → reddens
* with `package widget does not match import path gadget`).
* 2. REGISTERED — widget.unit.ww + widget.wwi appear in .sepwork: the
* import resolved to the DIR-package, not the sibling file. Pre-fix
* widget is never registered (folds into the importer), so both are
* absent — the "right module tag" signal.
* 3. cs==ww (rule 10) — per-package .s/.wwi byte-identical across the
* cstage `ww` and wwstage `ww_ww` sep-drivers, proving the `locatein`
* port is symmetric with `locate_import_in`.
*
* Light wwstage-driver test (CLAUDE.md rule 14): every intermediate is
* `-o`-redirected to /tmp, so it is phase-1 parallel-safe. Models
* 989_septest_run conventions; 989 prefix per the sep-gate precedent.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <dirent.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return 1;
}
static const char *
absbin(void)
{
const char *b = getenv("BIN");
if (!b) b = "out/bin";
if (b[0] == '/') return b;
static char buf[2048];
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return NULL;
snprintf(buf, sizeof buf, "%s/%s", cwd, b);
return buf;
}
static int
slurp(const char *path, char **outbuf, size_t *outlen)
{
FILE *f = fopen(path, "rb");
if (!f) return -1;
fseek(f, 0, SEEK_END);
long n = ftell(f);
fseek(f, 0, SEEK_SET);
if (n < 0) { fclose(f); return -1; }
char *b = malloc((size_t)n + 1);
if (!b) { fclose(f); return -1; }
if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; }
b[n] = '\0';
fclose(f);
*outbuf = b;
*outlen = (size_t)n;
return 0;
}
static int
files_eq(const char *a, const char *b)
{
char *ba = NULL, *bb = NULL;
size_t na = 0, nb = 0;
if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) {
free(ba); free(bb);
return -1;
}
int eq = (na == nb && memcmp(ba, bb, na) == 0);
free(ba); free(bb);
return eq ? 0 : 1;
}
/* cs==ww over a sep build's per-package output: every .s/.wwi the cstage
* driver produced in `csdir` must be byte-identical to the wwstage
* driver's same-named file in `wwdir`. Returns the count of mismatches. */
static int
cmp_sepwork(const char *csdir, const char *wwdir, const char *label)
{
DIR *d = opendir(csdir);
if (!d) {
fprintf(stderr, "coloimport FAIL: %s — no cs sepwork %s\n", label, csdir);
return 1;
}
int bad = 0, seen = 0;
struct dirent *ent;
while ((ent = readdir(d)) != NULL) {
const char *nm = ent->d_name;
size_t nl = strlen(nm);
int is_s = (nl > 2 && strcmp(nm + nl - 2, ".s") == 0);
int is_wwi = (nl > 4 && strcmp(nm + nl - 4, ".wwi") == 0);
if (!is_s && !is_wwi) continue;
seen++;
char a[2048], b[2048];
snprintf(a, sizeof a, "%s/%s", csdir, nm);
snprintf(b, sizeof b, "%s/%s", wwdir, nm);
if (files_eq(a, b) != 0) {
fprintf(stderr, "coloimport FAIL: %s — cs!=ww for %s (rule 10)\n",
label, nm);
bad++;
}
}
closedir(d);
if (seen == 0) {
fprintf(stderr, "coloimport FAIL: %s — no .s/.wwi in %s\n", label, csdir);
bad++;
}
return bad;
}
struct tcase {
const char *label;
const char *entry; /* the co-located `_test` entry built under --sep */
const char *incdir; /* -I dir (parent that holds the dir-package) */
const char *regpkg; /* the dir-package that must register its own unit */
};
static struct tcase cases[] = {
/* the transitive self-named-import shadow — the exact lib-run shape. */
{ "widget", "test/wcc/data/colo98/widget/widgettest.ww",
"test/wcc/data/colo98", "widget" },
{ NULL, NULL, NULL, NULL },
};
int
main(void)
{
const char *bin = absbin();
if (!bin) return 1;
char td[64], cmd[8192];
int fail = 0;
snprintf(td, sizeof td, "/tmp/wwcoloimport_%d", getpid());
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
runwait(cmd);
mkdir(td, 0755);
for (int i = 0; cases[i].label; i++) {
struct tcase *t = &cases[i];
struct { const char *drv, *tag; int rc; }
stg[] = { { "ww", "cs", -1 }, { "ww_ww", "ww", -1 } };
for (int s = 0; s < 2; s++) {
char prog[1024];
snprintf(prog, sizeof prog, "%s/%s.%s.bin", td, t->label, stg[s].tag);
/* COLD per-(case,stage) cache: every package compiles fresh so
* the per-pkg .s/.wwi this gate inspects are produced. */
snprintf(cmd, sizeof cmd,
"WW_PKGCACHE='%s/cache.%s.%s' %s/%s test --sep -I %s -o %s %s "
">/dev/null 2>&1",
td, t->label, stg[s].tag, bin, stg[s].drv, t->incdir, prog,
t->entry);
stg[s].rc = runwait(cmd);
}
/* 1. RUN-EXIT 0 on BOTH stages (pre-fix: w6c rejects the importer). */
if (stg[0].rc != 0 || stg[1].rc != 0) {
fprintf(stderr, "coloimport FAIL: %s exits cs=%d ww=%d (expected 0)\n",
t->label, stg[0].rc, stg[1].rc);
fail++;
continue;
}
char csdir[1024], wwdir[1024];
snprintf(csdir, sizeof csdir, "%s/%s.cs.bin.sepwork", td, t->label);
snprintf(wwdir, sizeof wwdir, "%s/%s.ww.bin.sepwork", td, t->label);
/* 2. REGISTERED: the dir-package owns its unit + interface (pre-fix
* it folds into the importer and these are absent). */
char u[2048], w[2048];
snprintf(u, sizeof u, "%s/%s.unit.ww", csdir, t->regpkg);
snprintf(w, sizeof w, "%s/%s.wwi", csdir, t->regpkg);
if (access(u, 0) != 0) {
fprintf(stderr, "coloimport FAIL: %s — %s missing (import folded "
"to sibling file, not the dir-package)\n", t->label, u);
fail++;
}
if (access(w, 0) != 0) {
fprintf(stderr, "coloimport FAIL: %s — %s missing\n", t->label, w);
fail++;
}
/* 3. cs==ww (rule 10): per-package .s/.wwi byte-identical. */
fail += cmp_sepwork(csdir, wwdir, t->label);
}
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
runwait(cmd);
if (fail) {
fprintf(stderr, "coloimport: %d check(s) failed\n", fail);
return 1;
}
printf("coloimport: co-located `_test` entry resolves `import <mod>` to the "
"dir-package (not the sibling file) under `ww test --sep` on both driver "
"stages + cs==ww per-pkg .s/.wwi\n");
return 0;
}

View File

@@ -0,0 +1,7 @@
package gadget;
import widget;
export fn frob(x: i32) i32 = {
return widget.bump(x) + 1;
};

View File

@@ -0,0 +1,5 @@
package widget;
export fn bump(x: i32) i32 = {
return x + 1;
};

View File

@@ -0,0 +1,7 @@
package widget_test;
import gadget;
@test fn t_frob() void = {
if (gadget.frob(40) != 42) { abort(); };
};