build: pass direct exports to package compilers

This commit is contained in:
2026-08-12 18:42:22 +09:00
parent b373c445bd
commit edd3f4ee83
13 changed files with 678 additions and 611 deletions

View File

@@ -22,6 +22,41 @@ slurp(const char *path, char **outbuf, u64 *outlen)
return 0;
}
struct importin {
const char *path;
const char *file;
char *buf;
u64 len;
};
static Node *
parseinput(Arena *a, const char *file, char *buf, u64 len,
const char *mod, const char *testsupport, int *bad)
{
Lex l;
Parser p;
lexinit(&l, a, file, buf, len);
parserinit(&p, a, &l);
p.testmodule = testsupport;
if (mod != NULL) {
p.pathmod = mod;
p.curmod = mod;
}
Node *f = parsefile(&p);
*bad = l.errs || p.errs;
return f;
}
static void
appendnodes(Node **head, Node **tail, Node *list)
{
if (list == NULL) return;
if (*head == NULL) *head = list;
else (*tail)->next = list;
while (list->next != NULL) list = list->next;
*tail = list;
}
int
main(int argc, char **argv)
{
@@ -31,8 +66,14 @@ main(int argc, char **argv)
const char *testsupport = NULL;
int testmode = 0;
int sepmode = 0; /* -c: #22 M3 separate-compile / primary-
* only codegen (emit imported==0 decls
* only; treat `.wwi` deps as external) */
* only codegen (emit imported==0 decls
* only; treat `.wwi` deps as external) */
struct importin *imports = calloc((size_t)argc, sizeof *imports);
if (imports == NULL) {
fputs("w6c: out of memory\n", stderr);
return 1;
}
int nimports = 0;
for (int i = 1; i < argc; i++) {
const char *a = argv[i];
if (strcmp(a, "-o") == 0 && i + 1 < argc) {
@@ -49,6 +90,14 @@ main(int argc, char **argv)
testsupport = argv[++i];
} else if (strcmp(a, "-c") == 0) {
sepmode = 1;
} else if (strcmp(a, "--import") == 0) {
if (i + 2 >= argc) {
fputs("w6c: --import requires path and file\n", stderr);
return 2;
}
imports[nimports].path = argv[++i];
imports[nimports].file = argv[++i];
nimports++;
} else if (a[0] == '-') {
fprintf(stderr, "w6c: unknown flag %s\n", a);
return 2;
@@ -60,9 +109,24 @@ main(int argc, char **argv)
}
}
if (src == NULL) {
fputs("usage: w6c [-T] [-c] [-I out.wwi] [-o out.s] file.ww\n", stderr);
fputs("usage: w6c [-T] [-c] [-I out.wwi] "
"[--import path dep.wwi]... [-o out.s] file.ww\n", stderr);
return 2;
}
if (nimports > 0 && !sepmode) {
fputs("w6c: --import requires -c\n", stderr);
return 2;
}
for (int i = 0; i < nimports; i++) {
if (imports[i].path[0] == '\0') {
fputs("w6c: --import path is empty\n", stderr);
return 2;
}
if (i > 0 && strcmp(imports[i-1].path, imports[i].path) >= 0) {
fputs("w6c: --import paths must be sorted and unique\n", stderr);
return 2;
}
}
if (testsupport != NULL && (!sepmode
|| (strcmp(testsupport, "test") != 0
&& strcmp(testsupport, "__wwtest") != 0))) {
@@ -70,24 +134,38 @@ main(int argc, char **argv)
return 2;
}
Arena *a = newarena();
Checker c;
Cg cg;
Node *head = NULL, *tail = NULL;
for (int i = 0; i < nimports; i++) {
if (slurp(imports[i].file, &imports[i].buf,
&imports[i].len) < 0) {
fprintf(stderr, "w6c: import %s: cannot read %s\n",
imports[i].path, imports[i].file);
return 1;
}
int bad = 0;
Node *f = parseinput(a, imports[i].file, imports[i].buf,
imports[i].len, imports[i].path, testsupport, &bad);
if (bad) return 1;
appendnodes(&head, &tail, f->list);
}
char *buf;
u64 len;
if (slurp(src, &buf, &len) < 0) {
fprintf(stderr, "w6c: %s: cannot read\n", src);
return 1;
}
Arena *a = newarena();
Lex l;
Parser p;
Checker c;
Cg cg;
lexinit(&l, a, src, buf, len);
parserinit(&p, a, &l);
p.testmodule = testsupport;
Node *file = parsefile(&p);
if (l.errs || p.errs) return 1;
int bad = 0;
Node *file = parseinput(a, src, buf, len, NULL, testsupport, &bad);
if (bad) return 1;
if (head != NULL) {
tail->next = file->list;
file->list = head;
}
check_init(&c, a);
c.is_test = testmode;
@@ -128,6 +206,8 @@ main(int argc, char **argv)
if (of != stdout) fclose(of);
freearena(a);
for (int i = 0; i < nimports; i++) free(imports[i].buf);
free(imports);
free(buf);
return 0;
}

View File

@@ -2,8 +2,8 @@
* wwi.c — `.wwi` export-data producer (w6c -I): a re-parseable ww-prototype
* rendering of a package's EXPORTED surface. Since the sep-compile flip
* (epic #22) this is the LIVE import path — the driver runs one `w6c -c -I`
* per package and feeds each dep's `.wwi` to its importers as import scope;
* the combined.ww amalgamator is gone.
* per package and feeds each dep's `.wwi` to its importers through a separate
* canonical `--import` input; the combined.ww amalgamator is gone.
*
* Specs: .ai/rob-M2-spec.md (producer) + .ai/drew-M2-checkexported.md
* (check_exported_type). Two load-bearing choices follow them:

View File

@@ -495,21 +495,15 @@ enumerate_dir_ww(const char *dirpath, int variant, const char *test_package,
* package's `.wwi` interface is materialized and every package is
* compiled on its own (`w6c -c`), then the `.o` set is flat-linked.
*
* 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.
* Each w6c pass is BOTH consumer (reads each direct dep `.wwi` through a
* separate --import argument) AND producer (writes this package's `.wwi`
* for its importers via -I). 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.
*
* Only DIRECT dependency artifacts are prepended. A `.wwi` relocates the
* recursively reachable public foreign type/const facts required by its own
* API, retaining their origin modules without turning them into source
* imports. Full transitive reachability remains a linker concern.
* Only DIRECT dependency artifacts enter a compile action. The canonical
* import path paired with each `.wwi` preserves qualified symbol identity;
* a `.wwi` relocates the public foreign type/const facts required by its own
* API. Full transitive reachability remains a linker concern.
*/
#define SEP_MAXPKG 256
@@ -529,6 +523,7 @@ struct seppkg {
int failed; /* discovery/compile failure reaches this action */
int test_support; /* compiler-generated -T support package */
int loaded; /* directory membership/name loaded exactly once */
int export_changed; /* staged export differs from committed export */
int emit_context; /* first verified resolution context */
unsigned char context_state[SEP_MAXCONTEXT]; /* 0 new, 1 active, 2 checked */
struct ImportSet bindings; /* first context's canonical import bindings */
@@ -701,6 +696,7 @@ sep_find_or_add_variant(struct sepgraph *g, const char *path,
p->failed = 0;
p->test_support = 0;
p->loaded = 0;
p->export_changed = 0;
p->emit_context = -1;
memset(p->context_state, 0, sizeof p->context_state);
p->bindings.paths = NULL;
@@ -1315,9 +1311,8 @@ sep_validate_module_closure(struct sepgraph *g, const int *order, int n,
}
/* Emit one of pi's own source files into the sep-unit under the
* //ww:module-reset primary boundary. Imports were already resolved to
* directory-package edges and their direct `.wwi` files were prepended;
* no source outside pi's sorted owned-source set may enter this unit. */
* //ww:module-reset primary boundary. No source or export outside pi's
* sorted owned-source set may enter this unit. */
static int
sep_emit_body(FILE *out, const char *path, const char *modpath)
{
@@ -1348,13 +1343,11 @@ sep_emit_body(FILE *out, const char *path, const char *modpath)
return 0;
}
/* Compose pi's sep-unit at `unitf`: its byte-sorted DIRECT dependency
* `.wwi`s, each tagged by its dotted path, then pi's own body under
* //ww:module-reset. Compiler exports are self-contained for public type
* facts; the linker separately retains the reachable archive closure. */
/* Compose pi's sep-unit at `unitf` from only pi's byte-sorted sources.
* Direct exports are separate compiler inputs; the linker separately retains
* the reachable archive closure. */
static int
sep_compose_unit(struct sepgraph *g, int pi, const char *scratch,
const char *unitf)
sep_compose_unit(struct sepgraph *g, int pi, const char *unitf)
{
if (g->pkg[pi].emit_context < 0
|| g->pkg[pi].emit_context >= g->ncontext)
@@ -1364,28 +1357,6 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *scratch,
fprintf(stderr, "ww: cannot open %s\n", unitf);
return -1;
}
for (int k = 0; k < g->pkg[pi].ndeps; k++) {
int dj = g->pkg[pi].deps[k];
char wwi[SEP_ARTIFACT_MAX];
sep_fname(g, dj, scratch, ".wwi", wwi, sizeof wwi);
FILE *wf = fopen(wwi, "rb");
if (wf == NULL) {
fprintf(stderr, "ww: missing %s\n", wwi);
fclose(u);
return -1;
}
int bad = fprintf(u, "//ww:module %s\n", g->pkg[dj].path) < 0;
int ch;
while (!bad && (ch = fgetc(wf)) != EOF)
if (fputc(ch, u) == EOF) bad = 1;
if (ferror(wf) || fputc('\n', u) == EOF) bad = 1;
if (fclose(wf) != 0) bad = 1;
if (bad) {
fprintf(stderr, "ww: cannot compose package unit %s\n", unitf);
fclose(u);
return -1;
}
}
int bodyrc = 0;
if (g->pkg[pi].is_dir) {
for (int i = 0; i < g->pkg[pi].nsources && bodyrc == 0; i++)
@@ -1467,11 +1438,11 @@ archive_o(const char *objpath, const char *apath)
/* -w workdir freshness: a `-w DIR` workdir is a caller-owned persistent
* package-artifact tree that replaces the fresh `.sepwork` scratch.
* Staleness is pure content
* identity, never mtime: a package is reused only when its freshly
* composed unit byte-equals the committed unit AND the driver/tool copies
* recorded in the dir byte-equal the live executables — every decision is
* reproducible by hand with cmp(1) against plain files. Artifacts commit
* Staleness is pure content identity, never mtime: a package is reused only
* when its freshly composed unit byte-equals the committed unit, no direct
* dependency emitted a changed export, AND the driver/tool copies recorded in
* the dir byte-equal the live executables — every decision is reproducible by
* hand with cmp(1) against plain files. Artifacts commit
* via temp + rename with the unit renamed last, so a killed build can
* never leave a committed unit vouching for uncommitted artifacts. The
* caller serializes invocations per workdir (Make target = one workdir)
@@ -1565,7 +1536,7 @@ static void
workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm)
{
snprintf(buf, bufsz, "ww workdir fmt %d mode %s asm %d\n",
is_test ? 6 : 5, is_test ? "test" : "build", emit_asm);
is_test ? 7 : 6, is_test ? "test" : "build", emit_asm);
}
/* A stale global builder identity invalidates every committed unit voucher in
@@ -1601,8 +1572,8 @@ invalidate_workdir_units(const char *scratch)
/* build_sep_plan — discover dependencies for every requested product in one
* package universe, compile the dependency-first union once, then link each
* root from its own complete reachable archive closure. The transitive
* producer loop (one `w6c -c -I` per package, dep-first,
* root from its own complete reachable archive closure. The dependency-first
* producer loop (one `w6c -c -I` per package,
* each DEP `.o` wrapped in its own deterministic `.a`), then a
* reverse-topo `w6l` of each root `.o` + dep `.a` set + libwwrt.a. Side
* files land in a cold `<stem>.sepwork` dir, or under the persistent
@@ -1913,12 +1884,17 @@ build_one_sep_impl(const char *src, int entry_is_dir,
const char *ca = warm ? anew : apath;
int needs_export = !g->pkg[pi].root || root_package;
int needs_archive = !g->pkg[pi].root || root_package;
if (sep_compose_unit(g, pi, scratch, cu) < 0) {
if (sep_compose_unit(g, pi, cu) < 0) {
g->pkg[pi].failed = 1;
any_failed = 1;
continue;
}
if (warm && !stale_all && file_equal(unitnew, unitf)
int deps_changed = 0;
for (int k = 0; k < g->pkg[pi].ndeps; k++)
if (g->pkg[g->pkg[pi].deps[k]].export_changed)
deps_changed = 1;
if (warm && !stale_all && !deps_changed
&& file_equal(unitnew, unitf)
&& file_is_reg(asmf)
&& (!needs_export || file_is_reg(wwi))
&& (emit_asm || (file_size_nonzero(obj)
@@ -1938,7 +1914,20 @@ build_one_sep_impl(const char *src, int entry_is_dir,
* LOCAL type (the root is never imported), which the
* export-check rejects. Skip -I for the root; its `.wwi`
* is never consumed. */
char *cargv[12];
size_t cargvcap = (size_t)(12 + 3 * g->pkg[pi].ndeps);
char **cargv = calloc(cargvcap, sizeof *cargv);
char (*importfiles)[SEP_ARTIFACT_MAX] = NULL;
if (g->pkg[pi].ndeps > 0)
importfiles = calloc((size_t)g->pkg[pi].ndeps,
sizeof *importfiles);
if (cargv == NULL || (g->pkg[pi].ndeps > 0
&& importfiles == NULL)) {
fprintf(stderr, "ww: out of memory\n");
free(importfiles);
free(cargv);
free(order);
return 1;
}
int cpos = 0;
cargv[cpos++] = "w6c";
if (!needs_export && is_test && g->pkg[pi].root) {
@@ -1953,6 +1942,14 @@ build_one_sep_impl(const char *src, int entry_is_dir,
cargv[cpos++] = (char *)test_support_module;
}
cargv[cpos++] = "-c";
for (int k = 0; k < g->pkg[pi].ndeps; k++) {
int dj = g->pkg[pi].deps[k];
sep_fname(g, dj, scratch, ".wwi", importfiles[k],
sizeof importfiles[k]);
cargv[cpos++] = "--import";
cargv[cpos++] = g->pkg[dj].path;
cargv[cpos++] = importfiles[k];
}
if (needs_export) {
cargv[cpos++] = "-I";
cargv[cpos++] = (char *)cw;
@@ -1961,13 +1958,19 @@ build_one_sep_impl(const char *src, int entry_is_dir,
cargv[cpos++] = (char *)cs;
cargv[cpos++] = (char *)cu;
cargv[cpos] = NULL;
if (run_argv(c6, cargv) != 0) {
int compilerc = run_argv(c6, cargv);
free(importfiles);
free(cargv);
if (compilerc != 0) {
fprintf(stderr, "ww: w6c failed for %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
g->pkg[pi].failed = 1;
any_failed = 1;
continue;
}
if (needs_export)
g->pkg[pi].export_changed = !warm
|| !file_equal(wwinew, wwi);
if (!emit_asm) {
char *aargv[] = {"w6a", "-o", (char *)co,
(char *)cs, NULL};