compiler: separate package exports from entry roots
This commit is contained in:
@@ -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;
|
||||
|
||||
13
cmd/w6c/gc.h
13
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 */
|
||||
|
||||
@@ -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);
|
||||
|
||||
101
cmd/w6c/wwi.c
101
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 <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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
109
cmd/wcc/check.c
109
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
|
||||
* `("<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;
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -66,6 +66,8 @@ export fn main(argc: i32, argv: **u8) i32 = {
|
||||
let wwiout: *u8 = nil; // -I <out.wwi>: 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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
// `("<name>", &<name>)` 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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; };
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user