parse: enforce strict-package — reject package-less files (#24a)

Flip the soft-default to a hard "missing package clause" error symmetrically in
both stages (cmd/wcc/parse.c + lib/ww/syntax/parse.ww): the first real decl of a
primary section with empty pathmod/resetmod and no seen clause is now rejected.
Closes the documented soft-default divergence (the 63-wrapper carve-out).

The gate flip can't be split from the migration it breaks, so this is one atomic
commit: ~80 test/wcc wrappers gain `package main;` via a shared wwtestpkg.h
helper, 6 data fixtures plus 17 asm-grep assertions update for the bare->main.<leaf>
root-helper mangle shift, and rt/ declares `package rt;` with @symbol pinning the
bare rt_ensure/rt_malloc linker names.

Root mangling narrows: the executable entry `main` stays bare (existing
carve-out), but root helper symbols become main.X. The #84 cluster is rewritten
to assert main.run distinct from aa.run/test.run; its cgen fix and bare machinery
are retained — still load-bearing for package-less module-reset deps. New
table-driven test 782_strict_package.c (6 rows, both stages).

Retiring //ww:module-reset is deferred to #24b: it is load-bearing (clears the
.wwi pathmod so the body's package clause asserts), not a vestige; fusing its
removal here would be a silent mismatch.

All byte-id gates green; full make test reports "all 335 tests passed".
This commit is contained in:
2026-06-29 03:55:26 +09:00
parent d34c90d932
commit 7b9488706b
95 changed files with 579 additions and 223 deletions

View File

@@ -416,17 +416,16 @@ export fn parsefile(p: *parser) *node = {
let f = newnode(nkind.N_FILE, p.curfile, p.curline, p.curcol);
let head: *node = nil;
let tail: *node = nil;
let sawpackage: i32 = 0;
for (p.curkind != tkind.TK_EOF) {
// `package foo;` — each contributing source's section in a
// concatenated stream begins with one. Single-file inputs
// may omit it (curmod stays empty; decls treated as primary).
//
// Retained divergence from brief: strict missing-`package`
// error softened to silent-default — 63 inline-source test
// wrappers depend on the soft behavior. See task #23 for
// the wrapper migration that unblocks the strict check.
// Rule 7 + rule 8 documentation.
// `package foo;` — directory-as-module declaration. Every
// primary section opens with one (`package main;` for an
// executable); a missing clause on the first real decl is a
// hard error (strict-package, #24a). Imported (pathmod) and
// sep primary-reset (resetmod) regions carry identity
// out-of-band and are exempt.
if (p.curkind == tkind.TK_MODULE) {
sawpackage = 1;
advance(p);
let name: str;
expectident(p, &name);
@@ -462,6 +461,7 @@ export fn parsefile(p: *parser) *node = {
// leaf `package` clause, and are flagged imported (gates the
// root-only bare-`main` rule, #32).
if (p.curkind == tkind.TK_MODPATH) {
sawpackage = 0;
p.pathmod = p.curtext;
p.curmod = p.curtext;
p.resetmod = "";
@@ -477,6 +477,7 @@ export fn parsefile(p: *parser) *node = {
// directive after a mid-file `package` would strip subsequent
// decls to bare — that usage is deliberate-only.
if (p.curkind == tkind.TK_MODRESET) {
sawpackage = 0;
// #57: a path-carrying reset (sep primary body) mangles decls
// on the dotted path so definer == importer, but leaves
// imported==0 (curmod set, pathmod "") so -c primary-ness and
@@ -503,6 +504,15 @@ export fn parsefile(p: *parser) *node = {
};
continue;
};
// strict-package: a primary section's first real decl must be
// preceded by a `package` clause. Imported (pathmod) and sep
// primary-reset (resetmod) regions carry identity out-of-band,
// so they are exempt. Wording mirrors Go's missing-`package`
// diagnostic (rule 5; Hare has no clause to port).
if (p.pathmod.len == 0 && p.resetmod.len == 0 && sawpackage == 0) {
errmsg(p, "missing package clause");
sawpackage = 1;
};
let attrs = parseattrs(p);
let exported: i32 = 0;
if (p.curkind == tkind.TK_EXPORT) { exported = 1; advance(p); };