wcc/ww: ww build --sep separate-compilation driver (M3-tail c3, #46)

build_one_sep (both stages + ww/main.combined.ww regen): discover_deps
(transitive directory-package set, dotted-path identity), tri-color
reverse-topo (cycle bails; loud reject is commit 4), the transitive
producer loop (one w6c -c -I pass per package, dep-first, each both
consumer and producer of its .wwi), and a flat w6l of the .o set.
combined.ww stays the DEFAULT live path; --sep is additive.

Every dep is tagged by its full dotted import path on prepend
(//ww:module <path>), so the definer's qualified symbol (#53) == the
consumer's qualified reference (#40) and the sep .o set links. The
prepend is the TRANSITIVE closure (lead-ratified, superseding
rob-c3-spec §1.3 direct-deps): a dep's interface can name a transitive
dep's type (os exposes time.instant), so the consuming unit needs the
whole closure for resolution — direct-deps-only does not type-check.
Consistent with the current flat-unit transitive-namespace model (the
visibility tighten is #45, post-M4).

Gate 989_sepbuild_run drives ww + ww_ww --sep on the real chain
root->os->{rt,time}: build+run (exit 7) + cs==ww per-pkg .s/.wwi/.unit
+ final binary + transitive-topo discovery + keystone bodies==.wwi
(os,root) through the real driver. Cold scratch; -o-redirected.
This commit is contained in:
2026-06-16 03:29:16 +09:00
parent 0c4a5ecea0
commit e37886b27d
5 changed files with 1948 additions and 12 deletions

View File

@@ -570,6 +570,451 @@ build_one(const char *src, int entry_is_dir, const char *out,
return 0;
}
/* ====================================================================
* ww build --sep — M3-tail separate-compilation driver (task #46/c3).
* ====================================================================
* The `--sep` path materializes each imported package's `.wwi`
* interface and compiles every package on its own (`w6c -c`), then
* flat-links the `.o` set. combined.ww stays the DEFAULT live path;
* --sep is purely additive (no existing invocation reaches it).
*
* Each w6c pass is BOTH consumer (reads dep `.wwi` as import scope) AND
* producer (writes this package's `.wwi` for its importers via -I), so
* a package's interface is materialized as a side effect of compiling
* it. Reverse-topo order guarantees a package's deps' `.wwi` exist
* before it compiles.
*
* The load-bearing rule (ken #56, rob-resolved): every dep is tagged by
* its FULL DOTTED import path on prepend (`//ww:module <path>`), so the
* definer's qualified symbol (#53) equals the consumer's qualified
* reference (#40) and the sep `.o`s link. A dep is NEVER bare-embedded.
*
* The prepend is the TRANSITIVE closure of a package's deps (lead-
* ratified, superseding rob-c3-spec §1.3 "direct deps"): a dep's public
* interface can name a transitive dep's type (os exposes time.instant),
* so the consuming unit needs the whole closure for name RESOLUTION —
* direct-deps-only does not type-check. This mirrors harec reading the
* transitive `.td` closure and is consistent with the current flat-unit
* transitive-namespace model (the visibility tighten is task #45,
* deferred post-M4).
*/
#define SEP_MAXPKG 256
struct seppkg {
char path[256]; /* dotted import path; "" == root/primary */
char entry[1024]; /* resolved package dir (or file, for a file root) */
int is_dir;
int deps[SEP_MAXPKG]; /* direct-dep indices into sepgraph.pkg */
int ndeps;
int color; /* tri-color DFS: 0 white, 1 gray, 2 black */
};
struct sepgraph {
struct seppkg pkg[SEP_MAXPKG];
int n;
};
/* Find a package by dotted path, or add it. Returns its index, -1 full. */
static int
sep_find_or_add(struct sepgraph *g, const char *path, const char *entry,
int is_dir)
{
for (int i = 0; i < g->n; i++)
if (strcmp(g->pkg[i].path, path) == 0) return i;
if (g->n >= SEP_MAXPKG) {
fprintf(stderr, "ww --sep: too many packages (limit %d)\n",
SEP_MAXPKG);
return -1;
}
struct seppkg *p = &g->pkg[g->n];
snprintf(p->path, sizeof p->path, "%s", path);
snprintf(p->entry, sizeof p->entry, "%s", entry);
p->is_dir = is_dir;
p->ndeps = 0;
p->color = 0;
return g->n++;
}
/* Sanitize a package's dotted path into a scratch-file basename. Dots
* stay (legal in filenames); the root's empty path becomes "__root". */
static void
sep_fname(const struct sepgraph *g, int pi, const char *scratch,
const char *suffix, char *out, size_t outsz)
{
const char *base = g->pkg[pi].path[0] ? g->pkg[pi].path : "__root";
snprintf(out, outsz, "%s/%s%s", scratch, base, suffix);
}
/* Scan one source file for top-level `import IDENT;`, resolving each. A
* DIRECTORY import is a package boundary: add it as a direct dep of pkg
* `pi`. A FILE import is an intra-package split — fold its imports into
* `pi` (its bytes join pi's body at emit time). Mirrors expand's scan
* but collects package PATHS instead of concatenating bytes (§1.1). */
static int
sep_scan_file(struct sepgraph *g, int pi, const char *file,
const char *searchpath, struct ImportSet *filevisit)
{
if (import_seen(filevisit, file)) return 0;
import_add(filevisit, file);
FILE *in = fopen(file, "rb");
if (in == NULL) {
fprintf(stderr, "ww --sep: cannot read %s\n", file);
return -1;
}
char line[2048];
int rc = 0;
while (fgets(line, sizeof line, in)) {
const char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (strncmp(p, "import ", 7) != 0 && strncmp(p, "import\t", 7) != 0)
continue;
p += 7;
while (*p == ' ' || *p == '\t') p++;
char name[256] = {0};
int j = 0;
while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')
|| *p == '_' || *p == '.' || (*p >= '0' && *p <= '9'))
if (j + 1 < (int)sizeof name) name[j++] = *p++;
if (j == 0) continue;
char path_form[256];
import_path_form(name, path_form, sizeof path_form);
char ipath[1024];
int is_dir = 0;
/* a locate-miss is an inline-satisfied (single-file multi-
* package) import; --sep targets directory packages, so the
* combined path owns that case. Skip, mirroring expand. */
if (!locate_import(searchpath, path_form, ipath, sizeof ipath,
&is_dir))
continue;
if (is_dir) {
int di = sep_find_or_add(g, name, ipath, 1);
if (di < 0) { rc = -1; break; }
int seen = 0;
for (int k = 0; k < g->pkg[pi].ndeps; k++)
if (g->pkg[pi].deps[k] == di) { seen = 1; break; }
if (!seen) {
if (g->pkg[pi].ndeps >= SEP_MAXPKG) { rc = -1; break; }
g->pkg[pi].deps[g->pkg[pi].ndeps++] = di;
}
} else if (sep_scan_file(g, pi, ipath, searchpath, filevisit) < 0) {
rc = -1; break;
}
}
fclose(in);
return rc;
}
/* Discover pkg pi's direct deps + recurse. Enumerate the package's own
* source files (dir → *.ww less *test.ww; file → the file) and scan
* each. `color` doubles as a scanned-marker here (2 == scanned); it is
* reset to white before the topo pass. */
static int
sep_scan_pkg(struct sepgraph *g, int pi, const char *searchpath)
{
if (g->pkg[pi].color == 2) return 0;
g->pkg[pi].color = 2;
struct ImportSet fv = {0};
int rc = 0;
if (g->pkg[pi].is_dir) {
char **files = NULL;
int n = enumerate_dir_ww(g->pkg[pi].entry, &files);
for (int i = 0; i < n && rc == 0; i++) {
char fp[1024];
snprintf(fp, sizeof fp, "%s/%s", g->pkg[pi].entry, files[i]);
rc = sep_scan_file(g, pi, fp, searchpath, &fv);
}
for (int i = 0; i < n; i++) free(files[i]);
free(files);
} else {
rc = sep_scan_file(g, pi, g->pkg[pi].entry, searchpath, &fv);
}
for (int i = 0; i < fv.n; i++) free(fv.paths[i]);
free(fv.paths);
if (rc < 0) return rc;
/* recurse into freshly-added deps (sep_find_or_add may have grown
* g->n during the scan; iterate by index). */
for (int k = 0; k < g->pkg[pi].ndeps; k++)
if (sep_scan_pkg(g, g->pkg[pi].deps[k], searchpath) < 0)
return -1;
return 0;
}
/* DFS post-order over the dep DAG → reverse-topo (deps before importer),
* cite Hare gather (deps.ha:123). Tri-color: a back-edge BAILS rather
* than spinning. Commit 3 assumes acyclic; the LOUD cycle reject is
* commit 4 (this just must not infinite-loop). */
static int
sep_topo_visit(struct sepgraph *g, int pi, int *order, int *no)
{
if (g->pkg[pi].color == 2) return 0;
if (g->pkg[pi].color == 1) {
fprintf(stderr,
"ww --sep: import cycle (loud reject lands commit 4)\n");
return -1;
}
g->pkg[pi].color = 1;
for (int k = 0; k < g->pkg[pi].ndeps; k++)
if (sep_topo_visit(g, g->pkg[pi].deps[k], order, no) < 0)
return -1;
g->pkg[pi].color = 2;
order[(*no)++] = pi;
return 0;
}
/* Mark pi's transitive deps (excluding pi) in inset[]. */
static void
sep_mark_deps(struct sepgraph *g, int pi, char *inset)
{
for (int k = 0; k < g->pkg[pi].ndeps; k++) {
int di = g->pkg[pi].deps[k];
if (!inset[di]) { inset[di] = 1; sep_mark_deps(g, di, inset); }
}
}
/* 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 of the
* body); FILE imports fold in (intra-package split). */
static void
sep_emit_body(FILE *out, const char *path, struct ImportSet *visited,
const char *searchpath)
{
if (import_seen(visited, path)) return;
import_add(visited, path);
FILE *in = fopen(path, "rb");
if (in == NULL) {
fprintf(stderr, "ww --sep: cannot read %s\n", path);
return;
}
char line[2048];
while (fgets(line, sizeof line, in)) {
const char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (strncmp(p, "import ", 7) != 0 && strncmp(p, "import\t", 7) != 0)
continue;
p += 7;
while (*p == ' ' || *p == '\t') p++;
char name[256] = {0};
int j = 0;
while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')
|| *p == '_' || *p == '.' || (*p >= '0' && *p <= '9'))
if (j + 1 < (int)sizeof name) name[j++] = *p++;
if (j == 0) continue;
char path_form[256];
import_path_form(name, path_form, sizeof path_form);
char ipath[1024];
int is_dir = 0;
if (!locate_import(searchpath, path_form, ipath, sizeof ipath,
&is_dir))
continue;
if (!is_dir)
sep_emit_body(out, ipath, visited, searchpath);
}
fputs("//ww:module-reset\n", out);
rewind(in);
int ch;
while ((ch = fgetc(in)) != EOF) fputc(ch, out);
fputc('\n', out);
fclose(in);
}
static void
sep_emit_dir_body(FILE *out, const char *dir, struct ImportSet *visited,
const char *searchpath)
{
char **files = NULL;
int n = enumerate_dir_ww(dir, &files);
for (int i = 0; i < n; i++) {
char fp[1024];
snprintf(fp, sizeof fp, "%s/%s", dir, files[i]);
sep_emit_body(out, fp, visited, searchpath);
free(files[i]);
}
free(files);
}
/* Compose pi's sep-unit at `unitf`: the transitive-closure `.wwi`s
* (reverse-topo order, each tagged by its dotted path), then pi's own
* body under //ww:module-reset. */
static int
sep_compose_unit(struct sepgraph *g, int pi, const char *scratch,
const int *order, int norder, const char *searchpath, const char *unitf)
{
FILE *u = fopen(unitf, "wb");
if (u == NULL) {
fprintf(stderr, "ww --sep: cannot open %s\n", unitf);
return -1;
}
char inset[SEP_MAXPKG] = {0};
sep_mark_deps(g, pi, inset);
for (int oi = 0; oi < norder; oi++) {
int dj = order[oi];
if (dj == pi || !inset[dj]) continue;
char wwi[1024];
sep_fname(g, dj, scratch, ".wwi", wwi, sizeof wwi);
FILE *wf = fopen(wwi, "rb");
if (wf == NULL) {
fprintf(stderr, "ww --sep: missing %s\n", wwi);
fclose(u);
return -1;
}
fprintf(u, "//ww:module %s\n", g->pkg[dj].path);
int ch;
while ((ch = fgetc(wf)) != EOF) fputc(ch, u);
fputc('\n', u);
fclose(wf);
}
struct ImportSet bodyvisit = {0};
if (g->pkg[pi].is_dir)
sep_emit_dir_body(u, g->pkg[pi].entry, &bodyvisit, searchpath);
else
sep_emit_body(u, g->pkg[pi].entry, &bodyvisit, searchpath);
for (int i = 0; i < bodyvisit.n; i++) free(bodyvisit.paths[i]);
free(bodyvisit.paths);
fclose(u);
return 0;
}
/* build_one_sep — the --sep orchestration: discover_deps, reverse_topo,
* the transitive producer loop (one `w6c -c -I` per package, dep-first),
* then a flat `w6l` of the `.o` set (per-pkg `.a` + multi-archive link
* is commit 4). Side files land in a cold `<stem>.sepwork` scratch dir
* (the structured cache is commit 5). */
static int
build_one_sep(const char *src, int entry_is_dir, const char *out,
const char *objstem, const char *extra_includes, const char *extra_libs,
const char *extra_libdirs)
{
const char *c6 = toolpath("WW_W6C", "w6c");
const char *a6 = toolpath("WW_W6A", "w6a");
const char *l6 = toolpath("WW_W6L", "w6l");
const char *libdir = getenv("WW_LIB");
if (libdir == NULL || libdir[0] == 0) {
static char libbuf[1024];
snprintf(libbuf, sizeof libbuf, "%s/../lib", self_dir);
libdir = libbuf;
}
const char *srcdir = getenv("WW_SRCLIB");
static char srcbuf[1024];
if (srcdir == NULL || srcdir[0] == 0) {
snprintf(srcbuf, sizeof srcbuf, "%s/../../lib", self_dir);
if (access(srcbuf, 0) == 0) srcdir = srcbuf;
else if (access("lib", 0) == 0) srcdir = "lib";
else srcdir = libdir;
}
/* search path: source-dir, then -I dirs, then srcdir (mirrors
* build_one). */
char srcd[1024];
if (entry_is_dir) {
snprintf(srcd, sizeof srcd, "%s", src);
size_t n = strlen(srcd);
while (n > 1 && srcd[n-1] == '/') srcd[--n] = '\0';
} else {
const char *slash = strrchr(src, '/');
if (slash) {
size_t n = (size_t)(slash - src);
if (n >= sizeof srcd) n = sizeof srcd - 1;
memcpy(srcd, src, n);
srcd[n] = '\0';
} else { srcd[0] = '.'; srcd[1] = '\0'; }
}
static char searchpath[4096];
if (extra_includes && extra_includes[0])
snprintf(searchpath, sizeof searchpath, "%s:%s:%s",
srcd, extra_includes, srcdir);
else
snprintf(searchpath, sizeof searchpath, "%s:%s", srcd, srcdir);
srcdir = searchpath;
char stem[1024];
if (entry_is_dir) {
const char *b = strrchr(srcd, '/');
const char *base = b ? b + 1 : srcd;
snprintf(stem, sizeof stem, "%s/%s", srcd, base);
} else {
snprintf(stem, sizeof stem, "%s", src);
char *dot = strrchr(stem, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
}
const char *ostem = (objstem && objstem[0]) ? objstem : stem;
char scratch[1100];
snprintf(scratch, sizeof scratch, "%s.sepwork", ostem);
{ char m[1200]; snprintf(m, sizeof m, "rm -rf %s", scratch); run(m); }
if (mkdir(scratch, 0755) != 0) {
fprintf(stderr, "ww --sep: cannot create scratch %s\n", scratch);
return 1;
}
struct sepgraph *g = calloc(1, sizeof *g);
if (g == NULL) return 1;
int root = sep_find_or_add(g, "", src, entry_is_dir);
if (root < 0 || sep_scan_pkg(g, root, srcdir) < 0) { free(g); return 1; }
for (int i = 0; i < g->n; i++) g->pkg[i].color = 0;
int *order = calloc((size_t)g->n, sizeof *order);
int norder = 0;
if (order == NULL || sep_topo_visit(g, root, order, &norder) < 0) {
free(order); free(g); return 1;
}
/* producer loop — dep-first, one `w6c -c -I` pass per package. */
for (int oi = 0; oi < norder; oi++) {
int pi = order[oi];
char unitf[1024], wwi[1024], asmf[1024], obj[1024], cmd[8192];
sep_fname(g, pi, scratch, ".unit.ww", unitf, sizeof unitf);
sep_fname(g, pi, scratch, ".wwi", wwi, sizeof wwi);
sep_fname(g, pi, scratch, ".s", asmf, sizeof asmf);
sep_fname(g, pi, scratch, ".o", obj, sizeof obj);
if (sep_compose_unit(g, pi, scratch, order, norder, srcdir,
unitf) < 0) { free(order); free(g); return 1; }
snprintf(cmd, sizeof cmd, "%s -c -I %s -o %s %s",
c6, wwi, asmf, unitf);
if (run(cmd) != 0) {
fprintf(stderr, "ww --sep: w6c failed for %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
free(order); free(g); return 1;
}
snprintf(cmd, sizeof cmd, "%s -o %s %s", a6, obj, asmf);
if (run(cmd) != 0) {
fprintf(stderr, "ww --sep: w6a failed for %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
free(order); free(g); return 1;
}
}
/* flat link: root.o first (order[norder-1]), deps after; runtime
* archive selectively pulls only undefined runtime symbols (commit 4
* adds per-pkg `.a` + multi-archive reverse-topo link). */
char rtargs[2048] = {0};
char rtpath[1024];
snprintf(rtpath, sizeof rtpath, "%s/libwwrt.a", libdir);
if (access(rtpath, 0) == 0) {
snprintf(rtargs, sizeof rtargs, "%s", rtpath);
} else {
char a1[1024], a2[1024];
snprintf(a1, sizeof a1, "%s/../obj/rt/start.o", self_dir);
snprintf(a2, sizeof a2, "%s/../obj/rt/syscall.o", self_dir);
snprintf(rtargs, sizeof rtargs, "%s %s", a1, a2);
}
char objs[8192] = {0};
for (int oi = norder - 1; oi >= 0; oi--) {
char obj[1024];
sep_fname(g, order[oi], scratch, ".o", obj, sizeof obj);
size_t n = strlen(objs);
snprintf(objs + n, sizeof objs - n, "%s%s", n ? " " : "", obj);
}
const char *libargs = (extra_libs && extra_libs[0]) ? extra_libs : "";
const char *libdirset = (extra_libdirs && extra_libdirs[0]) ? extra_libdirs : "";
char cmd[16384];
snprintf(cmd, sizeof cmd, "%s -o %s %s %s%s%s%s%s",
l6, out, objs, rtargs,
libdirset[0] ? " " : "", libdirset,
libargs[0] ? " " : "", libargs);
int rc = run(cmd);
free(order); free(g);
if (rc != 0) { fprintf(stderr, "ww --sep: w6l failed\n"); return 1; }
return 0;
}
static int
do_version(void)
{
@@ -694,12 +1139,25 @@ parse_build_flags(const char *cmd, int argc, char **argv,
char *libdirs, size_t libdirsz,
char *libs, size_t libsz,
char *outpath, size_t outsz,
const char **src_out)
const char **src_out, int *want_sep)
{
*src_out = NULL;
int i = 0;
for (; i < argc; i++) {
if (strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) {
if (strcmp(argv[i], "--sep") == 0) {
/* M3-tail c3: separate-compilation path, build-only.
* `run` has no sep-compile-then-run path (out of #46
* commit-3 scope), so it LOUD-REJECTS rather than
* silently swallowing a typed flag — both stages reject
* identically (rule 10; want_sep==NULL marks the run
* caller). */
if (want_sep == NULL) {
fprintf(stderr,
"ww %s: --sep is only valid with build\n", cmd);
return -1;
}
*want_sep = 1;
} else if (strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) {
size_t n = strlen(libs);
snprintf(libs + n, libsz - n,
"%s%s", n ? " " : "", argv[i]);
@@ -764,9 +1222,10 @@ do_build(int argc, char **argv)
char libdirs[2048] = {0};
char incs[2048] = {0};
char outflag[1024] = {0};
int want_sep = 0;
if (parse_build_flags("build", argc, argv, incs, sizeof incs,
libdirs, sizeof libdirs, libs, sizeof libs,
outflag, sizeof outflag, &src) < 0)
outflag, sizeof outflag, &src, &want_sep) < 0)
return 2;
if (src == NULL) src = "."; /* default: build cwd */
char resolved[1024];
@@ -792,6 +1251,9 @@ do_build(int argc, char **argv)
} else {
basename_no_ext(resolved, out, sizeof out);
}
if (want_sep)
return build_one_sep(resolved, is_dir, out, objstem, incs,
libs, libdirs);
return build_one(resolved, is_dir, out, objstem, incs, libs, libdirs, 0);
}
@@ -805,7 +1267,7 @@ do_run(int argc, char **argv)
char outflag[1024] = {0}; /* -o accepted+ignored: run always uses the temp */
int next = parse_build_flags("run", argc, argv, incs, sizeof incs,
libdirs, sizeof libdirs, libs, sizeof libs,
outflag, sizeof outflag, &src);
outflag, sizeof outflag, &src, NULL);
if (next < 0) return 2;
if (src == NULL) src = ".";
char resolved[1024];