From fc4bde703ed84a467469ab284a1f4246e847eee0 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 12 Aug 2026 21:59:51 +0900 Subject: [PATCH] compiler: separate package exports from entry roots --- cmd/w6c/cgen.c | 4 +- cmd/w6c/gc.h | 13 ++-- cmd/w6c/main.c | 25 ++++++-- cmd/w6c/wwi.c | 101 +++++++++++++++++++++-------- cmd/w6l/main.c | 9 ++- cmd/wcc/check.c | 109 ++++++++++++++++++++++---------- cmd/wcc/ww.h | 5 +- selfhost/cmd/w6c/main.ww | 23 ++++++- selfhost/cmd/w6l/main.ww | 4 +- selfhost/cmd/wcc/api.ww | 7 +- selfhost/cmd/wcc/cgen.ww | 10 ++- selfhost/cmd/wcc/check.ww | 123 ++++++++++++++++++++++++++---------- selfhost/cmd/wcc/wwi.ww | 110 +++++++++++++++++++++----------- selfhost/cmd/wwdump/main.ww | 2 +- test/byteid/wwi_test.ww | 65 +++++++++++-------- 15 files changed, 420 insertions(+), 190 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 1a184429..78345a59 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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; diff --git a/cmd/w6c/gc.h b/cmd/w6c/gc.h index 239ee783..c28061e3 100644 --- a/cmd/w6c/gc.h +++ b/cmd/w6c/gc.h @@ -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 */ diff --git a/cmd/w6c/main.c b/cmd/w6c/main.c index 287aabf0..220cd9c4 100644 --- a/cmd/w6c/main.c +++ b/cmd/w6c/main.c @@ -65,6 +65,8 @@ main(int argc, char **argv) const char *wwiout = NULL; /* -I : 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); diff --git a/cmd/w6c/wwi.c b/cmd/w6c/wwi.c index d7c783bc..a89ac4f2 100644 --- a/cmd/w6c/wwi.c +++ b/cmd/w6c/wwi.c @@ -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 ` 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; diff --git a/cmd/w6l/main.c b/cmd/w6l/main.c index 343e5a12..f88eb78f 100644 --- a/cmd/w6l/main.c +++ b/cmd/w6l/main.c @@ -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; } diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index c5763923..3270df0d 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -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 * `("", &)` 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; diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index f92eeff2..c3f5123f 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -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. */ diff --git a/selfhost/cmd/w6c/main.ww b/selfhost/cmd/w6c/main.ww index 7167b382..419303ee 100644 --- a/selfhost/cmd/w6c/main.ww +++ b/selfhost/cmd/w6c/main.ww @@ -66,6 +66,8 @@ export fn main(argc: i32, argv: **u8) i32 = { let wwiout: *u8 = nil; // -I : M2 export-data producer let testsupport: *u8 = nil; let testmode: i32 = 0i32; // #15: `-T` test-mode + let testpackage: i32 = 0i32; + let entrymode: i32 = 0i32; let sepmode: i32 = 0i32; // -c: #22 M3 separate-compile / primary- // only codegen (emit imported==0 decls // only; treat `.wwi` deps as external) @@ -96,6 +98,10 @@ export fn main(argc: i32, argv: **u8) i32 = { wwiout = argv[i]; } else { if (cstreq(a, "-T")) { testmode = 1i32; + } else { if (cstreq(a, "--test-package")) { + testpackage = 1i32; + } else { if (cstreq(a, "--entry")) { + entrymode = 1i32; } else { if (cstreq(a, "--test-support-module")) { i += 1; if (i >= argc) { @@ -127,12 +133,12 @@ export fn main(argc: i32, argv: **u8) i32 = { return 2; }; src = a; - }; }; }; }; }; }; }; + }; }; }; }; }; }; }; }; }; i += 1; }; if (src == nil) { - let m: str = "usage: w6c_ww [-T] [-c] [-I out.wwi] [--import path dep.wwi]... [-o out.s] file.ww\n"; + let m: str = "usage: w6c_ww [-T|--test-package] [--entry] [-c] [-I out.wwi] [--import path dep.wwi]... [-o out.s] file.ww\n"; os.write(2, m.ptr, m.len: u64); return 2; }; @@ -141,6 +147,16 @@ export fn main(argc: i32, argv: **u8) i32 = { os.write(2, m.ptr, m.len: u64); return 2; }; + if ((entrymode != 0 || testpackage != 0) && sepmode == 0) { + let m: str = "w6c: --entry and --test-package require -c\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; + if (testmode != 0 && testpackage != 0) { + let m: str = "w6c: -T and --test-package are mutually exclusive\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; let importi: i32 = 0; for (importi < nimports) { if (importpaths[importi][0u64] == 0u8) { @@ -249,5 +265,6 @@ export fn main(argc: i32, argv: **u8) i32 = { if (testsupport != nil) { testmodule = pathstr(testsupport); }; let interfaceout: str; if (wwiout != nil) { interfaceout = pathstr(wwiout); }; - return wcc.compilefile(f, testmode, testmodule, sepmode, interfaceout); + return wcc.compilefile(f, testmode, testpackage, testmodule, sepmode, + interfaceout, entrymode); }; diff --git a/selfhost/cmd/w6l/main.ww b/selfhost/cmd/w6l/main.ww index aaeb92aa..c8dc52ae 100644 --- a/selfhost/cmd/w6l/main.ww +++ b/selfhost/cmd/w6l/main.ww @@ -249,8 +249,10 @@ export fn main(argc: i32, argv: **u8) i32 = { let l: *lnk = mklnk(); - // Seed _start so libwwrt-style start.o is recognised as wanted. + // Seed both accepted entries before archive loading. This lets the root + // package arrive as an ordinary selective-pull archive. intern(l, "_start"); + intern(l, "main"); // Load positional inputs first (preserving order). let k: i32 = 0; diff --git a/selfhost/cmd/wcc/api.ww b/selfhost/cmd/wcc/api.ww index 0d39b17e..d104b2ff 100644 --- a/selfhost/cmd/wcc/api.ww +++ b/selfhost/cmd/wcc/api.ww @@ -2,13 +2,14 @@ package wcc; import syntax; -export fn compilefile(file: *syntax.node, testmode: i32, testmodule: str, - sepmode: i32, wwiout: str) i32 = { +export fn compilefile(file: *syntax.node, testmode: i32, testpackage: i32, + testmodule: str, sepmode: i32, wwiout: str, entrymode: i32) i32 = { let tc: syntax.tctx; syntax.typesinit(&tc); let ck: checker; checkinit(&ck, &tc); ck.istest = testmode; + ck.istestpackage = testpackage; if (testmodule.len > 0) { ck.testmodule = testmodule; }; ck.sepmode = sepmode; checkfile(&ck, file); @@ -20,7 +21,7 @@ export fn compilefile(file: *syntax.node, testmode: i32, testmodule: str, let cg: cgen; cgeninit(&cg); cg.sepmode = sepmode; - if (wwiout.len > 0) { cg.sepisdep = 1i32; }; + if (wwiout.len > 0 && entrymode == 0) { cg.sepisdep = 1i32; }; cgfile(&cg, file); return 0; }; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index e369fd7a..9d3009de 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -502,10 +502,8 @@ type cgen = struct { // like strlits/ffis. Symmetric with cstage Cg.sep_mode. sepmode: i32, // #99: this unit is a sep DEPENDENCY, not the root/link-entry unit. - // Set from `wwiout != nil` in main — the producer passes -I (.wwi - // output) to DEP units only; the root's .wwi is stripped (#69), so - // wwiout==nil <=> root/link-entry unit. Gates the bare-`main` carve- - // out: a dep's `fn main` mangles on its path like any decl; only the + // The explicit entry mode is independent of `.wwi` production. It gates + // the bare-`main` carve-out: a dep's `fn main` mangles like any decl; only the // root entry stays bare. Like sepmode, NOT reset by cgeninit (per-fn). // Symmetric with cstage Cg.sep_isdep. sepisdep: i32, @@ -3810,8 +3808,8 @@ fn collectmods(c: *cgen, file: *syntax.node) void = { // #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. - // sepisdep (wwiout==nil <=> root/link-entry unit, #69) - // re-mangles a dep's main; only the root's stays bare. + // Explicit entry mode clears sepisdep independently of export + // production; every other package's main remains mangled. if (!syntax.streq(d.str, "main") || d.imported != 0 || c.sepisdep != 0) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; c.mods = m; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index d32e3fe2..163c29c4 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -19,7 +19,9 @@ type checker = struct { matcharms: i32, // yield match-arm-nesting guard; cstage // twin (c->matcharms) istest: i32, // #15: `w6c_ww -T` — collect @test fns + - // synth the entry; loud-reject a user main. + // synth the entry; loud-reject a user main. + istestpackage: i32, // validate/retain package-owned @test bodies and + // export compiler-private metadata; no entry synth testmodule: str, // generated dispatcher support qualifier sepmode: i32, // -c package compilation: imported interfaces are // present, so absent members are hard export errors. @@ -523,12 +525,14 @@ fn installtop(c: *checker, d: *syntax.node, nm: str, mod: str, k: syntax.skind, // fact through both arms of a dependency diamond. In -c package mode, // reuse the first exact compiler-export binding so all references obtain // one nominal tinfo identity. Raw w6c keeps strict duplicate diagnostics. - if (c.sepmode != 0 && d.imported != 0 && d.exported != 0 && mod.len > 0) { + if (c.sepmode != 0 && d.imported != 0 && mod.len > 0 + && (d.exported != 0 || k == syntax.skind.SK_TYPE)) { if (k == syntax.skind.SK_TYPE || k == syntax.skind.SK_DEF) { let same: *syntax.sym = syntax.scopesamekeysym(c.top, nm, mod); if (same != nil) { if (same.skind == k && same.decl != nil && same.decl != d - && same.decl.imported != 0 && same.decl.exported != 0) { + && same.decl.imported != 0 + && (same.decl.exported != 0 || k == syntax.skind.SK_TYPE)) { return; }; }; @@ -723,6 +727,13 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { let mk: str = modkeyfor(c, head); if (mk.len != 0) { s = syntax.scopelookupinmodule(c.cur, mk, leaf); + if (s != nil && s.decl != nil + && s.decl.imported != 0 && s.decl.exported == 0) { + packageaccesserr(c, n, head, leaf, true); + n.type_ = c.tc.tyerr: *void; + c.nresolved += 1; + return; + }; }; }; }; @@ -4345,6 +4356,13 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { } else { s = syntax.scopelookupinmodule(c.cur, mk, nm); }; + if (s != nil && s.decl != nil && s.decl.imported != 0 + && s.decl.exported == 0 && callee.imported == 0) { + packageaccesserr(c, callee, callee.lhs.str, nm, true); + e.type_ = c.tc.tyerr: *void; + callee.type_ = c.tc.tyerr: *void; + return nil; + }; // Module-qualified callee whose leaf isn't scope-keyed // under its module: align to cstage, which stamps ty_err // here and lets cgen emit the call (cmd/wcc/check.c:1834- @@ -4456,6 +4474,12 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { if (mk.len != 0) { fs = syntax.scopelookupinmodule(c.cur, mk, e.str); }; + if (fs != nil && fs.decl != nil && fs.decl.imported != 0 + && fs.decl.exported == 0 && e.imported == 0) { + packageaccesserr(c, e, lhsn.str, e.str, true); + e.type_ = c.tc.tyerr: *void; + return nil; + }; if (fs != nil) { if (fs.decl != nil) { // #34: a module-qualified bare fn rvalue `mod.fn` types as // its FN TYPE (twin of the N_IDENT arm, :2688); decl.lhs is @@ -7481,6 +7505,7 @@ fn checkinit(c: *checker, tc: *syntax.tctx) void = { c.nunresolved = 0; c.errs = 0; c.istest = 0i32; // #15: caller (w6c main) sets it after init + c.istestpackage = 0i32; c.testmodule = "test"; c.sepmode = 0i32; // caller (w6c main) sets it from -c c.synthtestrun = nil; @@ -7508,11 +7533,23 @@ fn checkfile(c: *checker, file: *syntax.node) void = { // scope keying — cgen already emits the qualified call. Twin of // cstage cmd/wcc/check.c. if (c.istest != 0) { - let usenode: *syntax.node = syntax.newnode(syntax.nkind.N_USE, file.file, file.line, file.col); - usenode.str = c.testmodule; - usenode.usepath = c.testmodule; - usenode.next = file.list; - file.list = usenode; + let present: bool = false; + let su: *syntax.node = file.list; + for (su != nil) { + if (su.kind == syntax.nkind.N_USE && su.imported == 0 + && syntax.streq(su.usepath, c.testmodule)) { + present = true; + break; + }; + su = su.next; + }; + if (!present) { + let usenode: *syntax.node = syntax.newnode(syntax.nkind.N_USE, file.file, file.line, file.col); + usenode.str = c.testmodule; + usenode.usepath = c.testmodule; + usenode.next = file.list; + file.list = usenode; + }; }; // Pass 1: install all top-level names. @@ -7571,33 +7608,27 @@ fn checkfile(c: *checker, file: *syntax.node) void = { // Table rides cgen's #117 slice-of-tuple-global path; `run` resolves // bare against the auto-bundled lib/test (synth runs post-pass-1, so // run sits in the same flat "" bucket as the @test fns). - if (c.istest != 0) { + if (c.istest != 0 || c.istestpackage != 0) { let pf: str = file.file; let pl: i32 = file.line; let pc: i32 = file.col; - // (b) the synth entry OWNS `main` — loud-reject a user one. - let u: *syntax.node = file.list; - for (u != nil) { - if (u.kind == syntax.nkind.N_FNDECL && u.body != nil - && syntax.streq(u.str, "main")) { - cerr(u.file); - cerr(": error: test mode: main is synthesized by -T; remove the explicit main\n"); - c.errs += 1; + if (c.istest != 0) { + // The generated entry owns these names only in its own source. + let u: *syntax.node = file.list; + for (u != nil) { + if (u.imported == 0 && u.kind == syntax.nkind.N_FNDECL + && u.body != nil && syntax.streq(u.str, "main")) { + cerr(u.file); + cerr(": error: test mode: main is synthesized by -T; remove the explicit main\n"); + c.errs += 1; + }; + if (u.imported == 0 && syntax.streq(u.str, "__wwtests")) { + cerr(u.file); + cerr(": error: test mode: __wwtests is reserved by -T; rename the declaration\n"); + c.errs += 1; + }; + u = u.next; }; - // #24(b): the synth table OWNS `__wwtests` — loud-reject a user - // decl of that name (mirror the `main` reservation; cstage - // cmd/wcc/check.c). A user `__wwtests` whose type HAPPENS to - // match run()'s `[](str, *fn()void)` param slips the general - // call-arg check (a) but still silently shadows the synth table, - // so the synth `run(__wwtests)` iterates the user's table, not - // the collected @tests — reserve the NAME so the collision is - // loud regardless of type. Any decl kind (const/let/fn). - if (syntax.streq(u.str, "__wwtests")) { - cerr(u.file); - cerr(": error: test mode: __wwtests is reserved by -T; rename the declaration\n"); - c.errs += 1; - }; - u = u.next; }; // (c) collect @test fns in file.list order; build one table row // `("", &)` per validated @test fn. @@ -7632,7 +7663,7 @@ fn checkfile(c: *checker, file: *syntax.node) void = { cerr(t.str); cerr("' cannot be exported\n"); c.errs += 1; - } else { if (ntestattr == 1 && t.body == nil) { + } else { if (ntestattr == 1 && t.body == nil && t.imported == 0) { cerr(t.file); cerr(": error: @test fn '"); cerr(t.str); @@ -7652,10 +7683,30 @@ fn checkfile(c: *checker, file: *syntax.node) void = { cerr("' must be fn() void\n"); c.errs += 1; } else { + // Package variants retain and export compiler-private test + // metadata; only the generated-main action builds rows. + if (c.istest == 0) { + t = t.next; + continue; + }; let nm: *syntax.node = syntax.newnode(syntax.nkind.N_STRLIT, pf, pl, pc); nm.str = t.str; - let id: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc); - id.str = t.str; + let id: *syntax.node; + if (t.imported != 0 && t.nmod.len > 0) { + let (prefix, suffix) = strings.rcut(t.nmod, "."); + let alias: str = suffix; + if (alias.len == 0) { alias = t.nmod; }; + id = syntax.newnode(syntax.nkind.N_DOT, pf, pl, pc); + id.lhs = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc); + id.lhs.str = alias; + id.str = t.str; + // Parser-created nested expressions never carry imported=1. + // This marks the compiler-generated private metadata use. + id.imported = 1i32; + } else { + id = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc); + id.str = t.str; + }; let amp: *syntax.node = syntax.newnode(syntax.nkind.N_UN, pf, pl, pc); amp.op = syntax.tkind.TK_AMP; amp.lhs = id; @@ -7672,6 +7723,7 @@ fn checkfile(c: *checker, file: *syntax.node) void = { t = t.next; }; + if (c.istest != 0) { let body: *syntax.node = syntax.newnode(syntax.nkind.N_BLOCK, pf, pl, pc); let tab: *syntax.node = nil; if (ntest == 0i32) { @@ -7760,6 +7812,7 @@ fn checkfile(c: *checker, file: *syntax.node) void = { if (tab != nil) { tl.next = tab; tab.next = m; } else { tl.next = m; }; }; + }; }; // Pass 2: walk decl bodies/types and resolve identifiers. @@ -7884,7 +7937,7 @@ fn checkfile(c: *checker, file: *syntax.node) void = { // passes — they stay checked, never reach cgen. The -T path is // untouched: its synth main calls the @test fns, so they must remain. // Twin: cmd/wcc/check.c. - if (c.istest == 0) { + if (c.istest == 0 && c.istestpackage == 0) { let prev: *syntax.node = nil; let e: *syntax.node = file.list; for (e != nil) { diff --git a/selfhost/cmd/wcc/wwi.ww b/selfhost/cmd/wcc/wwi.ww index e8272a80..7a439b55 100644 --- a/selfhost/cmd/wcc/wwi.ww +++ b/selfhost/cmd/wcc/wwi.ww @@ -1,5 +1,6 @@ // wwi.ww — `.wwi` export-data producer (w6c_ww -I): a re-parseable -// ww-prototype rendering of a package's EXPORTED surface. Since the +// ww-prototype 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. @@ -10,9 +11,9 @@ // // - Unparse walks the AST type-expr subtree (the N_T* nodes), NOT the // tinfo — tinfo collapses nominal pkg.Name identity. -// - check_exported_type rides the producer entry (flag-gated), off 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`. +// Consumers reconstruct public signatures from them, but source cannot +// qualify those private spellings. // - A `.wwi` is ONE package's self-contained interface. Its primary // section is followed by compiler-owned origin sections containing the // exported foreign type/const facts reachable from the public surface. @@ -196,28 +197,8 @@ fn wwichecktype(c: *checker, owner: str, d: *syntax.node, t: *syntax.node) i32 = if (t.kind == syntax.nkind.N_TPARAM) { return wwichecktype(c, owner, d, t.lhs); }; let bad: i32 = 0; if (t.kind == syntax.nkind.N_TNAME) { - let s: *syntax.sym = wwitypesym(c, owner, t.str); - // sym.exported is vestigial (never set); the nominal's export - // status lives on its decl node, parser-set. - if (s != nil) { - if (s.decl != nil) { - if (s.decl.kind == syntax.nkind.N_TYPEDECL) { - // file.len == 0 ⇒ a checkinit-synthesized - // predeclared builtin (the ONLY empty-source - // decls: nomemdecl check.ww:126, the `void` - // TNAME check.ww:122), not a user nominal — ww's - // analogue of harec's STORAGE_NOMEM leaf-arm, and - // why cstage (lookup_builtin, no scope SK_TYPE) - // needs no such guard. - if (s.decl.file.len > 0) { - if (s.decl.exported == 0) { - wwireject(d, t.str); - bad = 1; - }; - }; - }; - }; - }; + // Nominal references, including private ones, are collected into + // the self-contained fact closure below. } else { if ( t.kind == syntax.nkind.N_TPTR || t.kind == syntax.nkind.N_TSLICE || @@ -513,10 +494,9 @@ fn wwitype(fd: i32, t: *syntax.node) void = { // 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. fn wwiattrrelevant(nm: str) bool = { - return syntax.streq(nm, "symbol"); + return syntax.streq(nm, "symbol") || syntax.streq(nm, "test"); }; fn wwiattrs(fd: i32, d: *syntax.node) void = { @@ -544,7 +524,8 @@ fn wwiattrs(fd: i32, d: *syntax.node) void = { fn wwidecl(fd: i32, d: *syntax.node) void = { if (d.kind == syntax.nkind.N_FNDECL) { wwiattrs(fd, d); - wputs(fd, "export fn "); + if (d.exported != 0) { wputs(fd, "export fn "); } + else { wputs(fd, "fn "); }; wputs(fd, d.str); wputb(fd, '('); let p: *syntax.node = d.list; @@ -557,13 +538,15 @@ fn wwidecl(fd: i32, d: *syntax.node) void = { wwitype(fd, d.lhs); wputs(fd, ";\n"); } else { if (d.kind == syntax.nkind.N_TYPEDECL) { - wputs(fd, "export type "); + if (d.exported != 0) { wputs(fd, "export type "); } + else { wputs(fd, "type "); }; wputs(fd, d.str); wputs(fd, " = "); wwitype(fd, d.lhs); wputs(fd, ";\n"); } else { if (d.kind == syntax.nkind.N_DEF) { - wputs(fd, "export def "); + if (d.exported != 0) { wputs(fd, "export def "); } + else { wputs(fd, "def "); }; wputs(fd, d.str); wputs(fd, ": "); wwitype(fd, d.lhs); @@ -585,7 +568,8 @@ fn wwidecl(fd: i32, d: *syntax.node) void = { wputs(fd, ";\n"); }; } else { if (d.kind == syntax.nkind.N_LET) { - wputs(fd, "export let "); + if (d.exported != 0) { wputs(fd, "export let "); } + else { wputs(fd, "let "); }; wputs(fd, d.str); wputs(fd, ": "); if (d.lhs == nil) { @@ -609,6 +593,17 @@ fn wwiisdecl(d: *syntax.node) bool = { d.kind == syntax.nkind.N_DEF || d.kind == syntax.nkind.N_LET; }; +fn wwihasattr(d: *syntax.node, name: str) bool = { + let a: *syntax.node = d.attr; + for (a != nil) { + if (a.kind == syntax.nkind.N_ATTR && syntax.streq(a.str, name)) { + return true; + }; + a = a.next; + }; + return false; +}; + // Deterministic ordering (rob §3): byte-lexicographic, mirror C strcmp // sign (<0/0/>0). Both stages key the sort identically, so the `.wwi` // order is deterministic. @@ -648,6 +643,9 @@ fn wwisortdecls(keys: []str, nodes: []*syntax.node, n: i32) void = { // representation narrow and make the deterministic ordering explicit. type wwifactset = struct { c: *checker, + privatenodes: []*syntax.node, + privatekeys: []str, + nprivate: i32, factnodes: []*syntax.node, factmods: []str, factranks: []i32, @@ -730,8 +728,21 @@ fn wwicollectdecl(fs: *wwifactset, owner: str, d: *syntax.node) void = { fs.seenranks[fs.nseen] = rank; fs.nseen += 1; - if (owner.len > 0) { - if (d.exported == 0) { + if (owner.len == 0 && d.exported == 0) { + // checkinit's synthetic nomem/void declarations have no source file. + // They are builtins, not owner-private package facts; cstage resolves + // these through lookup_builtin and therefore never collects them. + if (d.file.len == 0) { return; }; + if (d.kind != syntax.nkind.N_TYPEDECL) { + wwifactreject(d, "def", d.str); + fs.bad = 1; + return; + }; + fs.privatenodes[fs.nprivate] = d; + fs.privatekeys[fs.nprivate] = d.str; + fs.nprivate += 1; + } else { if (owner.len > 0) { + if (d.exported == 0 && d.kind != syntax.nkind.N_TYPEDECL) { let kind: str = "def"; if (d.kind == syntax.nkind.N_TYPEDECL) { kind = "type"; }; wwifactreject(d, kind, d.str); @@ -742,7 +753,7 @@ fn wwicollectdecl(fs: *wwifactset, owner: str, d: *syntax.node) void = { fs.factmods[fs.nfacts] = owner; fs.factranks[fs.nfacts] = rank; fs.nfacts += 1; - }; + }; }; if (d.kind == syntax.nkind.N_FNDECL) { let p: *syntax.node = d.list; @@ -925,12 +936,15 @@ fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = { if (nall == 0) { nall = 1; }; let fs: wwifactset; fs.c = c; + let privatenodes: []*syntax.node = alloc([], nall: u64)!; privatenodes.len = nall; + let privatekeys: []str = alloc([], nall: u64)!; privatekeys.len = nall; let factnodes: []*syntax.node = alloc([], nall: u64)!; factnodes.len = nall; let factmods: []str = alloc([], nall: u64)!; factmods.len = nall; let factranks: []i32 = alloc([], nall: u64)!; factranks.len = nall; let seennodes: []*syntax.node = alloc([], nall: u64)!; seennodes.len = nall; let seenmods: []str = alloc([], nall: u64)!; seenmods.len = nall; let seenranks: []i32 = alloc([], nall: u64)!; seenranks.len = nall; + fs.privatenodes = privatenodes; fs.privatekeys = privatekeys; fs.factnodes = factnodes; fs.factmods = factmods; fs.factranks = factranks; fs.seennodes = seennodes; fs.seenmods = seenmods; fs.seenranks = seenranks; let primary: str; @@ -942,6 +956,7 @@ fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = { d = d.next; }; if (fs.bad != 0) { return 1i32; }; + wwisortdecls(fs.privatekeys, fs.privatenodes, fs.nprivate); wwisortfacts(&fs); let fd: i32 = os.open(path, @@ -982,7 +997,7 @@ fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = { // #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.nmod empty, so it stays "main". The detector is // scan-miss (found==0), NOT pkg=="main", to match cstage byte-for- // byte (rule 10) when a tagged decl legitimately leafs to "main". @@ -1042,6 +1057,27 @@ fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = { }; }; + // Compiler-private owner nominals precede public declarations. They are + // available to export decoding but remain absent from source visibility. + let pri: i32 = 0; + for (pri < fs.nprivate) { + wwidecl(fd, fs.privatenodes[pri]); + pri += 1; + }; + + // Package-test metadata is private and deterministic. Only the distinct + // generated-main package consumes these declarations through --import. + if (c.istestpackage != 0) { + d = file.list; + for (d != nil) { + if (wwiprimary(d) && d.kind == syntax.nkind.N_FNDECL + && d.exported == 0 && wwihasattr(d, "test")) { + wwidecl(fd, d); + }; + d = d.next; + }; + }; + // decls — exported primary, byte-sorted by symbol name. let ndecl: i32 = 0; d = file.list; diff --git a/selfhost/cmd/wwdump/main.ww b/selfhost/cmd/wwdump/main.ww index 1a5686c5..812bef0a 100644 --- a/selfhost/cmd/wwdump/main.ww +++ b/selfhost/cmd/wwdump/main.ww @@ -148,7 +148,7 @@ export fn main(argc: i32, argv: **u8) i32 = { // silently). Mirrors w6c main.ww:162 / cmd/w6c/main.c. if (l.errs > 0 || ps.errs > 0) { return 1; }; let empty: str; - if (wcc.compilefile(f, 0, empty, 0, empty) != 0) { return 1; }; + if (wcc.compilefile(f, 0, 0, empty, 0, empty, 0) != 0) { return 1; }; };};};}; if (l.errs > 0) { return 1; }; diff --git a/test/byteid/wwi_test.ww b/test/byteid/wwi_test.ww index c674f620..77923a36 100644 --- a/test/byteid/wwi_test.ww +++ b/test/byteid/wwi_test.ww @@ -15,11 +15,10 @@ package wwi_test; // stressor. A synth fixture covers the decl-kinds + type-nodes no lib // package reaches (def const-expr fold, let global, [N]T, fn-ptr, !T, // tuple, storage-less enum, and the #47 @symbol round-trip); the types -// gate proves the exported limit defs reach the interface. NEGATIVE: an -// exported fn naming a private nominal must be LOUD-REJECTED by -// check_exported_type identically on both stages (nonzero exit + -// byte-identical diagnostic) — without it a vacuous no-op check would -// pass the positive gate silently. +// gate proves the exported limit defs reach the interface. PRIVATE CLOSURE: +// an exported fn naming a private nominal carries that nominal without +// `export`, remains byte-identical and re-parseable, and still rejects source +// qualification of the compiler-private name on both stages. // // wwileaf — BUG-C (#11) regression pin: a decl-less / export-less // primary module's `.wwi` `package` line must carry the module's real @@ -273,9 +272,9 @@ fn m2positive(pkg: str) void = { testenv.clean(td); }; -// negative: an exported fn naming a private nominal must be rejected -// identically (exit + diagnostic) by BOTH stages. -@test fn m2negative() void = { +// A public declaration may reach a private nominal as compiler export data, +// but that private spelling must not become source-importable. +@test fn m2privateclosure() void = { let td: str = testenv.fresh(); let src: str = strings.concat( "package leaktest;\n", @@ -284,24 +283,40 @@ fn m2positive(pkg: str) void = { "export fn clean(a: i32) i32 = { return a; };\n"); testenv.writefile(strings.concat(td, "/leak.ww"), src); - // relative leak.ww under td so both stages report the bare - // `leak.ww:L:C:` prefix. - let cav: []str = [testenv.driver("w6c"), "-I", "out.wwi", "leak.ww"]; - let cco: testenv.commandout; - testenv.runcommand(td, td, "cs", cav, lifetime(), &cco); - let wav: []str = [testenv.driver("w6c_ww"), "-I", "out.wwi", "leak.ww"]; - let wco: testenv.commandout; - testenv.runcommand(td, td, "ws", wav, lifetime(), &wco); - - let csok: bool = cco.termination == exec.termination.EXIT - && cco.code == 0; - let wsok: bool = wco.termination == exec.termination.EXIT - && wco.code == 0; - if (csok || wsok) { - fail("negative", "check_exported_type is vacuous (a stage accepted the private-type leak)"); + let cav: []str = [testenv.driver("w6c"), "-I", "cs.wwi", "leak.ww"]; + let wav: []str = [testenv.driver("w6c_ww"), "-I", "ww.wwi", "leak.ww"]; + if (!runok(td, "cs", cav) || !runok(td, "ws", wav)) { + fail("private-closure", "a stage rejected reachable private type data"); }; - if (!testenv.same(cco.stderr, wco.stderr)) { - fail("negative", "reject diagnostics differ across stages (rule-10)"); + let csbody: str = testenv.readfile(strings.concat(td, "/cs.wwi")); + let wsbody: str = testenv.readfile(strings.concat(td, "/ww.wwi")); + if (!testenv.same(csbody, wsbody) + || !testenv.has(csbody, "type secret = struct { x: i32 };") + || testenv.has(csbody, "export type secret") + || !testenv.has(csbody, "export fn leaks(s: secret) i32;")) { + fail("private-closure", "private compiler fact bytes are wrong"); + }; + let dav: []str = [testenv.driver("wwdump"), "-a", "cs.wwi"]; + if (!runok(td, "private-reparse", dav)) { + fail("private-closure", "self-contained export does not re-parse"); + }; + testenv.writefile(strings.concat(td, "/consumer.ww"), strings.concat( + "package consumer;\nimport leaktest;\n", + "fn forbidden(s: leaktest.secret) void = {};\n")); + let ccav: []str = [testenv.driver("w6c"), "-c", "--import", + "leaktest", "cs.wwi", "-o", "cs.s", "consumer.ww"]; + let wcav: []str = [testenv.driver("w6c_ww"), "-c", "--import", + "leaktest", "ww.wwi", "-o", "ww.s", "consumer.ww"]; + let cco: testenv.commandout; + let wco: testenv.commandout; + testenv.runcommand(td, td, "private-cs", ccav, lifetime(), &cco); + testenv.runcommand(td, td, "private-ws", wcav, lifetime(), &wco); + if (cco.termination != exec.termination.EXIT || cco.code == 0 + || wco.termination != exec.termination.EXIT || wco.code == 0 + || !testenv.same(cco.stderr, wco.stderr) + || !testenv.has(cco.stderr, + "package 'leaktest' has no exported declaration 'secret'")) { + fail("private-closure", "private nominal became source-visible"); }; testenv.clean(td); };