compiler: separate package exports from entry roots

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

View File

@@ -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) {