wcc/ww: tag sep-built dotted-path packages by full path not leaf (#57)

A separately-compiled package's primary body was emitted under a bare
`//ww:module-reset`, so its own `package <leaf>;` clause set curmod to
the leaf (e.g. utf8) while the importer spliced the .wwi under the full
`//ww:module encoding.utf8` — definer mangled `utf8.X`, importer wanted
`encoding.utf8.X`, unresolved. Thread the dotted path through the
directive: `//ww:module-reset <path>` sets curmod to the dotted path
(imported stays 0, so the root `fn main` stays bare per #32), and the
body's package clause is demoted to a leaf==last-component assertion
instead of overwriting curmod. Aligns sep-build to the M1 path-mangle
model; only the SEP emitter changes (the combined build_one arm is
untouched, so all combined byte-id gates hold). Both stages mirrored.

Commit-6 broad-soak prerequisite. Gate 989_sepdotpath_run sep-builds a
2-level dotted package and proves definer==importer qualification +
single-component non-vacuity, cs==ww.
This commit is contained in:
2026-06-16 16:36:03 +09:00
parent 7b36f961a9
commit 747475174a
12 changed files with 639 additions and 58 deletions

View File

@@ -0,0 +1,268 @@
/*
* 989_sepdotpath_run — sep-build of a DOTTED-path (multi-component) package
* (#57). Commit-6 broad-soak prereq: the real toolchain sep-builds packages
* like `encoding.utf8`, whose body must mangle on the full dotted import path
* (`encoding.utf8.X`), not the leaf `package` clause (`utf8.X`).
*
* Root cause it guards: the sep producer splices a package's own body under
* `//ww:module-reset` (so `-c` keeps imported==0). Pre-#57 that reset carried
* NO path, so the body's `package <leaf>;` clause set curmod to the LEAF —
* the DEFINER mangled `b.val` while every importer (spliced under
* `//ww:module a.b`) referenced `a.b.val` → unresolved at link. #57 threads
* the dotted path through the reset (`//ww:module-reset a.b`), demoting the
* leaf clause to an assertion. Single-COMPONENT packages were always clean
* (leaf == dotted path) — that invariant is the regression guard below.
*
* Graph (smallest that mixes both kinds): root -> { a.b (dotted), c (single) }.
*
* Asserts (all COLD — `<stem>.sepwork` scratch is wiped each run):
* 1. Build + run, BOTH stages → exit EXPECT_EXIT (the dotted-package symbol
* resolves + links; the program runs). Pre-#57 the link failed.
* 2. cs==ww (rule 10): per-package `.s`/`.wwi`/`.unit.ww` AND the final
* binary are byte-identical between `ww --sep` and `ww_ww --sep`.
* 3. NON-VACUITY (a) — definer==importer on the DOTTED form: the producer's
* `a.b.s` DEFINES `a.b.val` and the root's `__root.s` REFERENCES
* `a.b.val`. Pre-#57 the definer emitted the leaf `b.val` → the strings
* mismatched; this row would fail (and assert 1's link would fail).
* The reset directive in `a.b.unit.ww` carries the dotted path.
* 4. NON-VACUITY (b) — single-component regression guard: package `c`
* sep-builds in the SAME graph, defines `c.cval` (leaf == dotted), and
* cs==ww (covered by assert 2). Cross-commit byte-id of single-component
* output is additionally pinned by 989_sepbuild_run + the 990-997 gates.
*
* Light wwstage-driver test (CLAUDE.md rule 14): all intermediates are
* `-o`-redirected to /tmp, so it is phase-1 parallel-safe. Models
* 989_sepbuild_run.c 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>
#define EXPECT_EXIT 37
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;
}
/* 0 if `needle` occurs in the file at `path`, 1 if absent, -1 on read err. */
static int
file_contains(const char *path, const char *needle)
{
char *b = NULL;
size_t n = 0;
if (slurp(path, &b, &n) < 0) return -1;
int found = (strstr(b, needle) != NULL);
free(b);
return found ? 0 : 1;
}
static int
write_file(const char *path, const char *body)
{
FILE *f = fopen(path, "wb");
if (!f) return -1;
fputs(body, f);
fclose(f);
return 0;
}
int
main(void)
{
const char *bin = absbin();
if (!bin) return 1;
char td[64], cmd[8192], p[1024];
int fail = 0;
snprintf(td, sizeof td, "/tmp/wwsepdot_%d", getpid());
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
runwait(cmd);
mkdir(td, 0755);
/* lib/a/b — a 2-level dotted package (clause `package b;`, dir a/b). */
snprintf(p, sizeof p, "%s/lib", td); mkdir(p, 0755);
snprintf(p, sizeof p, "%s/lib/a", td); mkdir(p, 0755);
snprintf(p, sizeof p, "%s/lib/a/b", td); mkdir(p, 0755);
snprintf(p, sizeof p, "%s/lib/c", td); mkdir(p, 0755);
snprintf(p, sizeof p, "%s/lib/a/b/mod.ww", td);
if (write_file(p, "package b;\nexport fn val() i32 = { return 42; };\n"))
{ fail++; goto out; }
snprintf(p, sizeof p, "%s/lib/c/mod.ww", td);
if (write_file(p, "package c;\nexport fn cval() i32 = { return 5; };\n"))
{ fail++; goto out; }
char rootww[1024];
snprintf(rootww, sizeof rootww, "%s/root.ww", td);
if (write_file(rootww,
"package main;\n"
"import a.b;\n"
"import c;\n"
"fn main() i32 = { return b.val() - c.cval(); };\n"))
{ fail++; goto out; }
/* Two driver stages and their scratch dirs (paths rebuilt from the
* small fixed `td` + stage tag → provably non-truncating snprintfs). */
struct { const char *drv, *tag; char prog[1024]; }
stg[] = { { "ww", "cs", {0} }, { "ww_ww", "ww", {0} } };
for (int s = 0; s < 2; s++) {
snprintf(stg[s].prog, sizeof stg[s].prog, "%s/prog.%s", td, stg[s].tag);
/* Per-stage fresh WW_PKGCACHE → every package compiles COLD, so the
* `.s`/`.unit.ww` this gate inspects are always produced (a warm
* shared out/.pkgcache hit would skip them). Mirrors 989_pkgcache. */
snprintf(cmd, sizeof cmd,
"WW_PKGCACHE='%s/cache.%s' timeout 240 %s/%s build --sep "
"-I %s/lib -o %s %s >/dev/null 2>&1",
td, stg[s].tag, bin, stg[s].drv, td, stg[s].prog, rootww);
if (runwait(cmd) != 0) {
fprintf(stderr, "sepdotpath FAIL: %s build --sep\n", stg[s].drv);
fail++;
continue;
}
int rc = runwait(stg[s].prog);
if (rc != EXPECT_EXIT) {
fprintf(stderr, "sepdotpath FAIL: %s prog exit=%d expected %d\n",
stg[s].drv, rc, EXPECT_EXIT);
fail++;
}
}
/* The discovered package set materialized (dotted + single + root). */
const char *pkgs[] = { "a.b", "c", "__root" };
for (int i = 0; i < 3; i++) {
snprintf(p, sizeof p, "%s/prog.cs.sepwork/%s.wwi", td, pkgs[i]);
if (access(p, 0) != 0) {
fprintf(stderr, "sepdotpath FAIL: missing %s.wwi (discovery)\n",
pkgs[i]);
fail++;
}
}
/* cs==ww (rule 10): per-package .s/.wwi/.unit.ww + final binary. */
for (int i = 0; i < 3; i++) {
const char *suf[] = { ".s", ".wwi", ".unit.ww" };
for (int k = 0; k < 3; k++) {
char a[1024], b[1024];
snprintf(a, sizeof a, "%s/prog.%s.sepwork/%s%s",
td, stg[0].tag, pkgs[i], suf[k]);
snprintf(b, sizeof b, "%s/prog.%s.sepwork/%s%s",
td, stg[1].tag, pkgs[i], suf[k]);
if (files_eq(a, b) != 0) {
fprintf(stderr, "sepdotpath FAIL: cs!=ww for %s%s (rule 10)\n",
pkgs[i], suf[k]);
fail++;
}
}
}
if (files_eq(stg[0].prog, stg[1].prog) != 0) {
fprintf(stderr, "sepdotpath FAIL: cs exe != ww exe (rule 10)\n");
fail++;
}
/* NON-VACUITY (a): definer == importer on the DOTTED qualification.
* Pre-#57 the definer emitted the leaf `b.val` while the importer
* referenced `a.b.val` → mismatch + unresolved link. */
{
char def_s[1024], ref_s[1024], unit[1024];
snprintf(def_s, sizeof def_s, "%s/prog.cs.sepwork/a.b.s", td);
snprintf(ref_s, sizeof ref_s, "%s/prog.cs.sepwork/__root.s", td);
snprintf(unit, sizeof unit, "%s/prog.cs.sepwork/a.b.unit.ww", td);
if (file_contains(def_s, "TEXT a.b.val") != 0) {
fprintf(stderr, "sepdotpath FAIL: definer a.b.s lacks dotted "
"`TEXT a.b.val` (leaf-clause regression)\n");
fail++;
}
if (file_contains(ref_s, "a.b.val") != 0) {
fprintf(stderr, "sepdotpath FAIL: importer __root.s lacks "
"reference to a.b.val\n");
fail++;
}
if (file_contains(unit, "//ww:module-reset a.b") != 0) {
fprintf(stderr, "sepdotpath FAIL: a.b.unit.ww reset directive "
"does not carry the dotted path\n");
fail++;
}
}
/* NON-VACUITY (b): single-component `c` sep-builds in the SAME graph and
* defines its leaf==dotted symbol `c.cval` (curmod resolves to the same
* string with or without #57 → byte-id preserved). cs==ww already
* asserted above; cross-commit byte-id pinned by 989_sepbuild + 990-997. */
{
char c_s[1024];
snprintf(c_s, sizeof c_s, "%s/prog.cs.sepwork/c.s", td);
if (file_contains(c_s, "TEXT c.cval") != 0) {
fprintf(stderr, "sepdotpath FAIL: single-component c.s lacks "
"`TEXT c.cval` (regression)\n");
fail++;
}
}
out:
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
runwait(cmd);
if (fail) {
fprintf(stderr, "sepdotpath: %d check(s) failed\n", fail);
return 1;
}
printf("sepdotpath: dotted `a.b` + single `c` via build_one_sep — "
"build+run (exit %d) + cs==ww per-pkg .s/.wwi/.unit + final binary "
"+ definer==importer on a.b.val + single-component c.cval intact\n",
EXPECT_EXIT);
return 0;
}