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

@@ -1407,18 +1407,16 @@ parsefile(Parser *p)
Pos pp = { p->l->file, 1, 1 };
Node *file = newnode(p->a, N_FILE, pp);
Node *head = NULL, *tail = NULL;
int sawpackage = 0;
while (p->cur.kind != TK_EOF) {
/* `package foo;` — directory-as-module declaration. Each
* .ww file's section in a concatenated stream begins with
* one; a single-file or fragment input may omit it (curmod
* stays NULL and decls are treated as primary).
*
* Retained divergence from brief: the strict missing-`package`
* error was softened to silent-default to keep 63 inline-source
* test wrappers (200_parse, 100_lex, 300_check, ...) parsing.
* 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->cur.kind == TK_MODULE) {
sawpackage = 1;
advance(p);
const char *name = expectident(p);
expect(p, TK_SEMI);
@@ -1457,6 +1455,7 @@ parsefile(Parser *p)
* leaf `package` clause, and are flagged imported (gates the
* root-only bare-`main` rule, #32). */
if (p->cur.kind == TK_MODPATH) {
sawpackage = 0;
p->pathmod = p->cur.text;
p->curmod = p->cur.text;
p->resetmod = NULL;
@@ -1472,6 +1471,7 @@ parsefile(Parser *p)
* directive after a mid-file `package` would strip subsequent
* decls to bare — that usage is deliberate-only. */
if (p->cur.kind == 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 NULL) so -c
@@ -1498,6 +1498,16 @@ parsefile(Parser *p)
}
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 == NULL && p->resetmod == NULL && !sawpackage) {
errorf(p->cur.pos, "missing package clause");
p->errs++;
sawpackage = 1;
}
Node *attrs = parseattrs(p);
int exp = accept(p, TK_EXPORT);
Node *d = NULL;