compiler: separate package exports from entry roots

This commit is contained in:
2026-08-12 21:59:51 +09:00
parent a88078faf8
commit fc4bde703e
15 changed files with 420 additions and 190 deletions

View File

@@ -1519,8 +1519,8 @@ mod_collect(Cg *c, Node *file)
* #99: under sep a dep unit's main is imported==0 too (its body
* is composed with a path-carrying `//ww:module-reset`, #57), so
* imported==0 no longer means "root unit" per-unit. Gate on
* !sep_isdep (wwiout==NULL <=> root/link-entry unit, #69) so only
* the root's main stays bare; a dep's main mangles on its path. */
* !sep_isdep, which the explicit --entry mode controls independently
* of export output, so only the root's main stays bare. */
if (d->str && strcmp(d->str, "main") == 0 && !d->imported
&& !c->sep_isdep)
continue;

View File

@@ -52,13 +52,12 @@ struct Cg {
* Off on the combined path (every existing
* invocation) so M3 is a pure addition. */
int sep_isdep; /* #99: this unit is a sep DEPENDENCY, not the
* root/link-entry unit. Set from `wwiout != NULL`
* in main: the producer passes -I (.wwi output)
* to DEP units only — the root's .wwi is stripped
* (#69), so wwiout==NULL <=> root/link-entry unit.
* Gates the bare-`main` carve-out: a dep's `fn
* main` must mangle on its path like any decl;
* only the root entry stays bare. */
* root/link-entry unit. The compiler's explicit
* --entry flag clears this independently of -I:
* every package action may emit export data. Gates
* the bare-`main` carve-out: a dep's `fn main` must
* mangle on its path like any decl; only the root
* entry stays bare. */
};
/* cgen.c */

View File

@@ -65,6 +65,8 @@ main(int argc, char **argv)
const char *wwiout = NULL; /* -I <out.wwi>: M2 export-data producer */
const char *testsupport = NULL;
int testmode = 0;
int testpackage = 0;
int entrymode = 0;
int sepmode = 0; /* -c: #22 M3 separate-compile / primary-
* only codegen (emit imported==0 decls
* only; treat `.wwi` deps as external) */
@@ -82,6 +84,10 @@ main(int argc, char **argv)
wwiout = argv[++i];
} else if (strcmp(a, "-T") == 0) {
testmode = 1;
} else if (strcmp(a, "--test-package") == 0) {
testpackage = 1;
} else if (strcmp(a, "--entry") == 0) {
entrymode = 1;
} else if (strcmp(a, "--test-support-module") == 0) {
if (i + 1 >= argc) {
fputs("w6c: --test-support-module requires arg\n", stderr);
@@ -109,7 +115,7 @@ main(int argc, char **argv)
}
}
if (src == NULL) {
fputs("usage: w6c [-T] [-c] [-I out.wwi] "
fputs("usage: w6c [-T|--test-package] [--entry] [-c] [-I out.wwi] "
"[--import path dep.wwi]... [-o out.s] file.ww\n", stderr);
return 2;
}
@@ -117,6 +123,14 @@ main(int argc, char **argv)
fputs("w6c: --import requires -c\n", stderr);
return 2;
}
if ((entrymode || testpackage) && !sepmode) {
fputs("w6c: --entry and --test-package require -c\n", stderr);
return 2;
}
if (testmode && testpackage) {
fputs("w6c: -T and --test-package are mutually exclusive\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);
@@ -169,6 +183,7 @@ main(int argc, char **argv)
check_init(&c, a);
c.is_test = testmode;
c.is_test_package = testpackage;
if (testsupport != NULL) c.test_module = testsupport;
c.sep_mode = sepmode;
check_file(&c, file);
@@ -198,10 +213,10 @@ main(int argc, char **argv)
cg_init(&cg, a);
cg.sep_mode = sepmode;
/* #99: wwiout != NULL <=> this is a sep DEP unit (the producer passes
* -I to deps only; the root's .wwi is stripped per #69). Gates the
* bare-`main` carve-out so only the root/link-entry main stays bare. */
cg.sep_isdep = (wwiout != NULL);
/* Export production and entry identity are independent package-action
* properties. Legacy raw invocations without -I remain entry-like; every
* driver package now supplies -I, and only link roots add --entry. */
cg.sep_isdep = (wwiout != NULL && !entrymode);
cg_file(&cg, of, file);
if (of != stdout) fclose(of);

View File

@@ -1,6 +1,7 @@
/*
* 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
* rendering of a package's exported surface and compiler-private closure.
* 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 through a separate
* canonical `--import` input; the combined.ww amalgamator is gone.
@@ -11,9 +12,9 @@
* - Unparse walks the AST type-expr subtree (the N_T* nodes), NOT the
* checked Type* — tinfo collapses nominal pkg.Name identity (MEMORY
* tinfo_lossy_nominal). The AST preserves surface syntax + names.
* - check_exported_type rides the producer entry (flag-gated), so M2
* stays dead on the normal `.s` path. It rejects exactly one thing:
* an exported signature naming a non-exported nominal type.
* - Reachable owner-private nominal types are encoded without `export`.
* They let a consumer reconstruct public signatures without making the
* private spelling source-importable.
*
* A `.wwi` is ONE package's self-contained interface. The primary section
* is followed by compiler-owned `//ww:module <path>` sections for exported
@@ -128,19 +129,8 @@ wwi_check_type(Checker *c, const char *owner, Pos loc, Node *t)
return 0;
switch (t->kind) {
case N_TNAME: {
Sym *s = wwi_typesym(c, owner, t->str);
/* Sym.exported is vestigial (the checker never sets it); the
* nominal's export status lives on its decl node, parser-set.
* No file-guard twin to wwstage's: cstage resolves `nomem` and
* the primitives through lookup_builtin (no scope SK_TYPE), so
* scope_lookup_type returns NULL for them and they never reach
* this reject — harec's STORAGE_NOMEM leaf-arm, by construction. */
if (s && s->decl && s->decl->kind == N_TYPEDECL
&& s->decl->export == 0) {
errorf(loc, "exported declaration references "
"unexported type '%s'", t->str);
bad = 1;
}
/* Nominal references, including private ones, are collected into
* the self-contained fact closure below. */
break;
}
case N_TPTR:
@@ -429,12 +419,12 @@ wwi_expr(FILE *of, Node *e)
* fficollect — dropping it makes sep-compile emit `CALL malloc` for a
* `@symbol("rt_malloc")` fn). Named for the class so @align/@offset would
* slot in here IF ww ever grows field-layout attributes — it has none
* today (task #47 report). @test never reaches a `.wwi` (test fns are not
* export-marked), so it needs no exclusion arm. */
* today (task #47 report). @test is compiler-private package-test metadata. */
static int
wwi_attr_relevant(const char *nm)
{
return nm && strcmp(nm, "symbol") == 0;
return nm && (strcmp(nm, "symbol") == 0
|| strcmp(nm, "test") == 0);
}
static void
@@ -463,7 +453,7 @@ wwi_decl(FILE *of, Node *d)
switch (d->kind) {
case N_FNDECL:
wwi_attrs(of, d);
fputs("export fn ", of);
fputs(d->export ? "export fn " : "fn ", of);
fputs(d->str, of);
fputc('(', of);
for (Node *p = d->list; p; p = p->next) {
@@ -475,14 +465,14 @@ wwi_decl(FILE *of, Node *d)
fputs(";\n", of);
break;
case N_TYPEDECL:
fputs("export type ", of);
fputs(d->export ? "export type " : "type ", of);
fputs(d->str, of);
fputs(" = ", of);
wwi_type(of, d->lhs);
fputs(";\n", of);
break;
case N_DEF:
fputs("export def ", of);
fputs(d->export ? "export def " : "def ", of);
fputs(d->str, of);
fputs(": ", of);
wwi_type(of, d->lhs);
@@ -506,7 +496,7 @@ wwi_decl(FILE *of, Node *d)
}
break;
case N_LET:
fputs("export let ", of);
fputs(d->export ? "export let " : "let ", of);
fputs(d->str, of);
fputs(": ", of);
if (d->lhs == NULL)
@@ -528,6 +518,8 @@ struct factent { Node *d; const char *mod; int idx; };
struct factset {
Checker *c;
struct declent *privatefacts;
int nprivate, capprivate;
struct factent *facts;
int nfacts, capfacts;
struct factent *seen;
@@ -535,6 +527,18 @@ struct factset {
int bad;
};
static void
wwi_decl_grow(struct declent **v, int *cap, int need)
{
if (*cap >= need) return;
int ncap = *cap ? *cap * 2 : 16;
while (ncap < need) ncap *= 2;
struct declent *nv = realloc(*v, (size_t)ncap * sizeof *nv);
if (nv == NULL) fatal("wwi: out of memory");
*v = nv;
*cap = ncap;
}
static int
declcmp(const void *a, const void *b)
{
@@ -574,6 +578,15 @@ wwi_is_decl(Node *d)
|| d->kind == N_DEF || d->kind == N_LET;
}
static int
wwi_has_attr(Node *d, const char *name)
{
for (Node *a = d ? d->attr : NULL; a; a = a->next)
if (a->kind == N_ATTR && a->str && strcmp(a->str, name) == 0)
return 1;
return 0;
}
static int
wwi_fact_same(struct factent *f, const char *mod, Node *d)
{
@@ -715,8 +728,20 @@ wwi_collect_decl(struct factset *fs, const char *owner, Node *d)
fs->seen[fs->nseen] = (struct factent){d, owner, fs->nseen};
fs->nseen++;
if (owner != NULL) {
if (!d->export) {
if (owner == NULL && !d->export) {
if (d->kind != N_TYPEDECL) {
errorf(d->pos, "exported declaration references unexported "
"def '%s'", d->str);
fs->bad = 1;
return;
}
wwi_decl_grow(&fs->privatefacts, &fs->capprivate,
fs->nprivate + 1);
fs->privatefacts[fs->nprivate] =
(struct declent){d, fs->nprivate};
fs->nprivate++;
} else if (owner != NULL) {
if (!d->export && d->kind != N_TYPEDECL) {
errorf(d->pos, "exported declaration references unexported "
"%s '%s'", d->kind == N_TYPEDECL ? "type" : "def",
d->str);
@@ -805,10 +830,14 @@ wwi_emit(Checker *c, FILE *of, Node *file)
wwi_collect_decl(&fs, NULL, d);
}
if (fs.bad) {
free(fs.privatefacts);
free(fs.facts);
free(fs.seen);
return 1;
}
if (fs.nprivate > 1)
qsort(fs.privatefacts, (size_t)fs.nprivate,
sizeof *fs.privatefacts, declcmp);
if (fs.nfacts > 1)
qsort(fs.facts, (size_t)fs.nfacts, sizeof *fs.facts, factcmp);
@@ -826,7 +855,7 @@ wwi_emit(Checker *c, FILE *of, Node *file)
/* #11: a decl-less / export-less primary body carries no
* module-tagged decl, so the scan above finds nothing; fall back to
* the primary module identity stamped on the N_FILE node at parse
* time. A real root `package main` arrives via a bare module-reset
* time. A raw single-file `package main` root arrives via a bare reset
* and leaves file->module NULL, so it stays "main". The detector is
* scan-miss (`!found`), NOT pkg=="main": a body whose first tagged
* decl legitimately leafs to "main" must keep that, and must match
@@ -863,6 +892,23 @@ wwi_emit(Checker *c, FILE *of, Node *file)
free(us);
}
/* Compiler-private primary nominal facts precede the public declarations.
* They are parseable within this package export but lack `export`, so a
* source qualifier cannot name them. */
for (int i = 0; i < fs.nprivate; i++)
wwi_decl(of, fs.privatefacts[i].d);
/* Package-test metadata is private, deterministic, and owned by the test
* variant. A distinct generated-main compile consumes it through --import. */
if (c->is_test_package) {
/* Unit source paths and declarations are already deterministically
* ordered; preserve that order as the test runner's user-facing order. */
for (Node *d = file->list; d; d = d->next)
if (wwi_primary(d) && d->kind == N_FNDECL
&& !d->export && wwi_has_attr(d, "test"))
wwi_decl(of, d);
}
/* decls — exported primary, byte-sorted by symbol name. */
int ndecl = 0;
for (Node *d = file->list; d; d = d->next)
@@ -900,6 +946,7 @@ wwi_emit(Checker *c, FILE *of, Node *file)
}
wwi_decl(of, f->d);
}
free(fs.privatefacts);
free(fs.facts);
free(fs.seen);
return 0;

View File

@@ -101,12 +101,11 @@ main(int argc, char **argv)
}
Lnk l = {0};
/* Seed the symbol table with the entry point so archive pulls
* include the .o that defines it. Without this, a libwwrt.a
* containing start.o is silently skipped if no user .o
* references _start, and the entry falls back to main — which
* has no proper exit path. */
/* Seed both accepted entry symbols before archive loading. The root
* package archive then participates in the same selective member-pull
* protocol as every dependency; libwwrt.a still supplies _start. */
(void)l_intern(&l, "_start");
(void)l_intern(&l, "main");
for (int i = 0; i < ninputs; i++) {
if (l_load(&l, inputs[i]) != 0) return 1;
}

View File

@@ -91,9 +91,14 @@ resolve_typename(Checker *c, Node *n)
* import path (symbols are path-keyed). */
const char *mk = use_path(c->file, c->cur_mod,
head);
if (mk != NULL)
s = scope_lookup_in_module(c->cur, mk,
dot + 1);
if (mk != NULL)
s = scope_lookup_in_module(c->cur, mk,
dot + 1);
if (s && s->decl && s->decl->imported
&& !s->decl->export)
return err(c, n->pos,
"package '%s' has no exported declaration '%s'",
head, dot + 1);
}
}
}
@@ -1480,6 +1485,11 @@ cexpr(Checker *c, Node *n)
} else {
Sym *fs = scope_lookup_in_module(c->cur,
mk, n->str);
if (fs && fs->decl && fs->decl->imported
&& !fs->decl->export && !n->imported)
return n->type = err(c, n->pos,
"package '%s' has no exported declaration '%s'",
n->lhs->str, n->str);
if (fs)
return n->type = fs->type;
/* A bare `w6c -T` intentionally leaves the
@@ -2843,6 +2853,7 @@ check_init(Checker *c, Arena *a)
memset(c, 0, sizeof *c);
c->a = a;
c->is_test = 0; /* #15: caller (w6c main) sets it after init */
c->is_test_package = 0;
c->test_module = "test";
typesinit(a);
c->top = newscope(a, NULL);
@@ -3103,14 +3114,19 @@ check_module_shadow(Checker *c, const char *name, Pos pos,
static Sym *
same_import_fact(Checker *c, Node *d, const char *mod, Skind kind)
{
if (!c->sep_mode || d == NULL || !d->imported || !d->export
if (!c->sep_mode || d == NULL || !d->imported
|| mod == NULL || mod[0] == '\0')
return NULL;
if (kind != SK_TYPE && kind != SK_DEF)
return NULL;
/* Compiler-private nominal facts may recur through a self-contained
* export diamond. Private defs are never valid closure facts. */
if (!d->export && kind != SK_TYPE)
return NULL;
Sym *s = scope_lookup_in_module(c->cur, mod, d->str);
if (s == NULL || s->kind != kind || s->decl == NULL || s->decl == d
|| !s->decl->imported || !s->decl->export)
|| !s->decl->imported
|| (!s->decl->export && kind != SK_TYPE))
return NULL;
return s;
}
@@ -3130,12 +3146,21 @@ check_file(Checker *c, Node *file)
* (the //ww:module directive, mod_collect), NOT this scope keying — cgen
* already emits the qualified call. wwstage twin in check.ww. */
if (c->is_test) {
Node *usenode = newnode(c->a, N_USE, file->pos);
usenode->str = c->test_module;
usenode->strlen = strlen(c->test_module);
usenode->usepath = c->test_module;
usenode->next = file->list;
file->list = usenode;
int present = 0;
for (Node *u = file->list; u; u = u->next)
if (u->kind == N_USE && !u->imported
&& u->usepath && strcmp(u->usepath, c->test_module) == 0) {
present = 1;
break;
}
if (!present) {
Node *usenode = newnode(c->a, N_USE, file->pos);
usenode->str = c->test_module;
usenode->strlen = strlen(c->test_module);
usenode->usepath = c->test_module;
usenode->next = file->list;
file->list = usenode;
}
}
/* pass 1: install names (types first, then defs/fns).
@@ -3391,24 +3416,23 @@ check_file(Checker *c, Node *file)
* synth runs post-pass-1, so lib/test's `run` sits in the same flat ""
* bucket as the @test fns — bare, like the @test calls themselves).
*/
if (c->is_test) {
if (c->is_test || c->is_test_package) {
Pos fp = file->pos;
/* (b) the synth entry OWNS `main` — loud-reject a user one. */
for (Node *d = file->list; d; d = d->next)
if (d->kind == N_FNDECL && d->str
&& strcmp(d->str, "main") == 0 && d->body != NULL)
err(c, d->pos, "test mode: main is synthesized "
"by -T; remove the explicit main");
/* #24(b): the synth table OWNS `__wwtests` — loud-reject a user
* decl of that name (twin of the `main` reservation above). A user
* __wwtests whose type happens to match run()'s [](str,*fn()void)
* param slips the general call-arg check but still silently shadows
* the synth table; reserve the NAME so the collision is loud
* regardless of type. Any decl kind. */
for (Node *d = file->list; d; d = d->next)
if (d->str && strcmp(d->str, "__wwtests") == 0)
err(c, d->pos, "test mode: __wwtests is reserved "
"by -T; rename the declaration");
if (c->is_test) {
/* (b) the synth entry OWNS `main` — loud-reject a user one. */
for (Node *d = file->list; d; d = d->next)
if (!d->imported && d->kind == N_FNDECL && d->str
&& strcmp(d->str, "main") == 0 && d->body != NULL)
err(c, d->pos, "test mode: main is synthesized "
"by -T; remove the explicit main");
/* The generated table name is reserved only in the owning source,
* never by an unrelated declaration carried in dependency exports. */
for (Node *d = file->list; d; d = d->next)
if (!d->imported && d->str
&& strcmp(d->str, "__wwtests") == 0)
err(c, d->pos, "test mode: __wwtests is reserved "
"by -T; rename the declaration");
}
/* (c) collect @test fns in file->list order; build one table row
* `("<name>", &<name>)` per validated @test fn. */
Node *rhead = NULL, *rtail = NULL;
@@ -3436,7 +3460,7 @@ check_file(Checker *c, Node *file)
d->str);
continue;
}
if (d->body == NULL) {
if (d->body == NULL && !d->imported) {
err(c, d->pos, "@test fn '%s' needs a body",
d->str);
continue;
@@ -3452,11 +3476,30 @@ check_file(Checker *c, Node *file)
d->str);
continue;
}
/* A package-test variant validates and retains the body. Its
* interface records this declaration as compiler-private metadata;
* only a separate -T generated-main action consumes that metadata. */
if (!c->is_test)
continue;
Node *nm = newnode(c->a, N_STRLIT, fp);
nm->str = d->str;
nm->strlen = strlen(d->str);
Node *id = newnode(c->a, N_IDENT, fp);
id->str = d->str;
Node *id;
if (d->imported && d->module && d->module[0]) {
const char *dotp = strrchr(d->module, '.');
const char *alias = dotp ? dotp + 1 : d->module;
id = newnode(c->a, N_DOT, fp);
id->lhs = newnode(c->a, N_IDENT, fp);
id->lhs->str = alias;
id->str = d->str;
/* Nested nodes never acquire imported from parsing. This marks
* the compiler-generated private metadata reference so ordinary
* source qualification remains export-checked. */
id->imported = 1;
} else {
id = newnode(c->a, N_IDENT, fp);
id->str = d->str;
}
Node *amp = newnode(c->a, N_UN, fp);
amp->op = TK_AMP;
amp->lhs = id;
@@ -3469,6 +3512,7 @@ check_file(Checker *c, Node *file)
ntest++;
}
if (c->is_test) {
Node *body = newnode(c->a, N_BLOCK, fp);
Node *tab = NULL;
if (ntest == 0) {
@@ -3552,6 +3596,7 @@ check_file(Checker *c, Node *file)
if (tab) { tl->next = tab; tab->next = m; }
else tl->next = m;
}
}
}
/* pass 2: check def initialisers and fn bodies */
@@ -3754,7 +3799,7 @@ check_file(Checker *c, Node *file)
* synth main calls the @test fns, so they must remain. Prereq for
* in-package @test colocation (#9). Twin: selfhost/cmd/wcc/check.ww.
*/
if (!c->is_test) {
if (!c->is_test && !c->is_test_package) {
Node *prev = NULL;
for (Node *d = file->list; d; ) {
int istest = 0;

View File

@@ -589,7 +589,10 @@ struct Checker {
int matcharms; /* nesting count for yield */
int errs;
int is_test; /* #15: `w6c -T` — collect @test fns + synth
* the entry; loud-reject a user main. */
* the entry; loud-reject a user main. */
int is_test_package; /* package-test variant: validate/retain @test
* bodies and export compiler-private metadata,
* but do not synthesize an entry. */
const char *test_module; /* generated dispatcher support qualifier */
int sep_mode; /* -c package compilation: imported interfaces are
* present, so absent members are hard export errors. */