ww package: reject non-function main declarations

This commit is contained in:
2026-08-21 15:13:09 +09:00
parent 2b7fafde0c
commit 2387d3e2d2
6 changed files with 520 additions and 3 deletions

View File

@@ -8716,6 +8716,251 @@ fn cwdwritedata(dir: str, label: str) void = {
clean(root);
};
// A package declared `main` may reserve the entry spelling only for a function.
// This is the declaration-kind half of pinned Go's main rule; WW deliberately
// retains its C/Hare-style argument/result entry ABI. The checker owns the
// reject before assembly/linking, directory tests reject before execution, and
// declared package name remains independent from dotted/physical identity.
@test fn nonfunction_main_declarations_reject() void = {
let root: str = fresh();
let shapes: []str = [
"let main: i32 = 0;\n",
"const main: i32 = 0;\n",
"def main: i32 = 0;\n",
"type main = i32;\n",
];
let labels: []str = ["let", "const", "def", "type"];
let expected: str = "cannot declare main - must be func";
let i: i32 = 0;
for (i < shapes.len) {
let dir: str = strings.concat(root, "/invalid-", labels[i]);
assert(os.mkdir(dir, 448i32) == 0);
let src: str = strings.concat(dir, "/main.ww");
writefile(src, strings.concat("package main;\n", shapes[i]));
let output: str = strings.concat(root, "/invalid-output-", labels[i]);
let avc: []str = [driver("ww"), "build", "-o", output, dir];
let avw: []str = [driver("ww_ww"), "build", "-o", output, dir];
let outc: commandout;
let outw: commandout;
runcommand(root, strings.concat("invalid-main-c-", labels[i]), avc,
(30i64 * (time.second: i64)): time.duration, &outc);
runcommand(root, strings.concat("invalid-main-ww-", labels[i]), avw,
(30i64 * (time.second: i64)): time.duration, &outw);
expectexit(&outc, 1);
expectexit(&outw, 1);
assert(outc.stdout.len == 0 && outw.stdout.len == 0);
assert(same(primarydiagnostic(outc.stderr), expected));
assert(same(primarydiagnostic(outw.stderr), expected));
assert(same(outc.stderr, outw.stderr));
assert(has(outc.stderr, "ww: w6c failed for "));
assert(!has(outc.stderr, "w6a") && !has(outc.stderr, "w6l"));
assert(!os.exists(output));
assert(!os.exists(strings.concat(output, ".new")));
assert(!os.exists(strings.concat(output, ".install")));
assert(!os.exists(strings.concat(output, ".sepwork")));
assert(!directoryhasfragment(root, ".wwtxn."));
i += 1;
};
// Direct compiler calls pin the true owner and byte-identical frontend
// diagnostic independently from driver-generated unit paths.
let directsrc: str = strings.concat(root, "/invalid-let/main.ww");
let directc: str = strings.concat(root, "/direct-c.s");
let directw: str = strings.concat(root, "/direct-ww.s");
let dcav: []str = [driver("w6c"), "-c", "--entry", "-o", directc,
directsrc];
let dwav: []str = [driver("w6c_ww"), "-c", "--entry", "-o", directw,
directsrc];
let directco: commandout;
let directwo: commandout;
runcommand(root, "invalid-main-direct-c", dcav,
(30i64 * (time.second: i64)): time.duration, &directco);
runcommand(root, "invalid-main-direct-ww", dwav,
(30i64 * (time.second: i64)): time.duration, &directwo);
expectexit(&directco, 1);
expectexit(&directwo, 1);
assert(directco.stdout.len == 0 && directwo.stdout.len == 0);
assert(same(directco.stderr, directwo.stderr));
assert(same(primarydiagnostic(directco.stderr), expected));
assert(!os.exists(directc) && !os.exists(directw));
// The same malformed production package must not become a passing test
// package merely because the dispatcher owns a separate synthetic entry.
let testdir: str = strings.concat(root, "/invalid-test");
assert(os.mkdir(testdir, 448i32) == 0);
writefile(strings.concat(testdir, "/main.ww"),
"package main;\nlet main: i32 = 0;\n");
writefile(strings.concat(testdir, "/main_test.ww"),
"package main;\n@test fn must_not_run() void = { abort(\"ran\"); };\n");
let testdiags: []str = ["", ""];
let stages: []str = ["ww", "ww_ww"];
let tags: []str = ["c", "ww"];
i = 0;
for (i < stages.len) {
let av: []str = [driver(stages[i]), "test", testdir];
let out: commandout;
runcommand(root, strings.concat("invalid-main-test-", tags[i]), av,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(out.stdout, "FAIL\n"));
assert(!has(out.stdout, "must_not_run") && !has(out.stdout, " passed,"));
assert(!has(out.stderr, "must_not_run") && !has(out.stderr, "ok "));
assert(has(out.stderr, expected));
assert(has(out.stderr, strings.concat("FAIL ", testdir)));
testdiags[i] = strings.dup(primarydiagnostic(out.stderr));
i += 1;
};
assert(same(testdiags[0], expected) && same(testdiags[0], testdiags[1]));
// A warm semantic failure preserves every committed action byte, tool
// voucher, and public binary, then exact source restoration reuses them.
let warmdiags: []str = ["", ""];
i = 0;
for (i < stages.len) {
let warmdir: str = strings.concat(root, "/warm-", tags[i]);
let work: str = strings.concat(root, "/warm-work-", tags[i]);
let output: str = strings.concat(root, "/warm-output-", tags[i]);
assert(os.mkdir(warmdir, 448i32) == 0);
assert(os.mkdir(work, 448i32) == 0);
let source: str = strings.concat(warmdir, "/main.ww");
let valid: str = "package main;\nfn main() i32 = { return 0; };\n";
writefile(source, valid);
let av: []str = [driver(stages[i]), "build", "-w", work,
"-o", output, warmdir];
let out: commandout;
runcommand(root, strings.concat("invalid-main-warm-cold-", tags[i]), av,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0);
let action: str = localidentity(warmdir, "main");
let suffixes: []str = [".unit.ww", ".wwi", ".s", ".o", ".a",
".init.unit.ww", ".init.s", ".init.o"];
let refs: []str = alloc([], suffixes.len: u64)!;
let j: i32 = 0;
for (j < suffixes.len) {
append(refs, strings.dup(readfile(strings.concat(strings.concat(work,
"/"), strings.concat(action, suffixes[j])))));
j += 1;
};
let toolpaths: []str = ["/.wwtool.ww", "/.wwtool.w6c",
"/.wwtool.w6a", "/.wwtool.stamp"];
let toolrefs: []str = alloc([], toolpaths.len: u64)!;
j = 0;
for (j < toolpaths.len) {
append(toolrefs, strings.dup(readfile(strings.concat(work,
toolpaths[j]))));
j += 1;
};
let binref: str = strings.dup(readfile(output));
rewritefile(source, "package main;\nlet main: i32 = 0;\n");
runcommand(root, strings.concat("invalid-main-warm-reject-", tags[i]),
av, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(out.stdout.len == 0 && same(primarydiagnostic(out.stderr), expected));
warmdiags[i] = strings.dup(primarydiagnostic(out.stderr));
j = 0;
for (j < suffixes.len) {
assert(same(refs[j], readfile(strings.concat(strings.concat(work,
"/"), strings.concat(action, suffixes[j])))));
j += 1;
};
j = 0;
for (j < toolpaths.len) {
assert(same(toolrefs[j], readfile(strings.concat(work,
toolpaths[j]))));
j += 1;
};
assert(same(binref, readfile(output)));
assert(!directoryhasnew(work));
assert(!os.exists(strings.concat(output, ".new")));
assert(!os.exists(strings.concat(output, ".install")));
assert(!os.exists(strings.concat(output, ".sepwork")));
assert(!directoryhasfragment(root, ".wwtxn."));
rewritefile(source, valid);
runcommand(root, strings.concat("invalid-main-warm-restored-", tags[i]),
av, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0);
assert(same(binref, readfile(output)) && !directoryhasnew(work));
i += 1;
};
assert(same(warmdiags[0], expected) && same(warmdiags[0], warmdiags[1]));
// The declared package name, never the dotted path leaf or physical
// directory, owns the rule. A non-main package may export `main`, and a
// command may import it while retaining WW's argument/result entry ABI.
let tree: str = strings.concat(root, "/tree");
let dep: str = strings.concat(tree, "/domain/main");
let app: str = strings.concat(tree, "/app");
mkdirall(dep);
assert(os.mkdir(app, 448i32) == 0);
writefile(strings.concat(dep, "/dep.ww"),
"package utility;\nexport let main: i32 = 7;\n");
writefile(strings.concat(app, "/main.ww"), strings.concat(
"package main;\nimport domain.main;\n",
"fn main(argc: i32, argv: **u8) i32 = { return utility.main - 7; };\n"));
let binaries: []str = [strings.concat(root, "/identity-c"),
strings.concat(root, "/identity-ww")];
i = 0;
for (i < stages.len) {
let av: []str = [driver(stages[i]), "build", "-I", tree,
"-o", binaries[i], app];
let out: commandout;
runcommand(root, strings.concat("main-name-boundary-", tags[i]), av,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0);
let runav: []str = [binaries[i], "one", "two"];
runcommand(root, strings.concat("main-name-boundary-run-", tags[i]),
runav, (30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0);
i += 1;
};
assert(same(readfile(binaries[0]), readfile(binaries[1])));
let artifacts: []str = ["domain.main.unit.ww", "domain.main.wwi",
"domain.main.s", "domain.main.o", "domain.main.a"];
i = 0;
for (i < artifacts.len) {
assert(same(readfile(strings.concat(strings.concat(binaries[0],
".sepwork/"), artifacts[i])),
readfile(strings.concat(strings.concat(binaries[1], ".sepwork/"),
artifacts[i]))));
i += 1;
};
// Importing any declared-main package is still the loader-owned error and
// retains precedence over the declaration checker, even when that command
// also contains the malformed declaration.
let badcmd: str = strings.concat(tree, "/badcmd");
let client: str = strings.concat(tree, "/client");
assert(os.mkdir(badcmd, 448i32) == 0);
assert(os.mkdir(client, 448i32) == 0);
writefile(strings.concat(badcmd, "/main.ww"),
"package main;\nlet main: i32 = 0;\n");
writefile(strings.concat(client, "/client.ww"), strings.concat(
"package client;\nimport _ badcmd;\n",
"export fn value() i32 = { return 7; };\n"));
let importout: str = strings.concat(root, "/bad-import-output");
let importdiag: str =
"ww: package badcmd is a program, not an importable package\n";
i = 0;
for (i < stages.len) {
let av: []str = [driver(stages[i]), "build", "-I", tree,
"-o", importout, client];
let out: commandout;
runcommand(root, strings.concat("bad-main-import-", tags[i]), av,
(30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(out.stdout.len == 0 && same(out.stderr, importdiag));
assert(!has(out.stderr, expected));
assert(!os.exists(importout) && !os.exists(strings.concat(importout,
".sepwork")));
i += 1;
};
clean(root);
};
// Invalid @test attribute shapes reject at build with stable text on
// BOTH frontends (the -T synth checker owns them; the fixture corpus
// cannot reach -T, so these rows live here). Fragments only — the