/* * 841_dup_main_reject — cstage and wwstage LOUD-REJECT a program that * declares more than one top-level `main` (F-D; rob ruling 2026-06-14). * * `main` lowers to ONE bare entry symbol. A second top-level decl named * `main` (any kind, any package) collides with the entry at link time: * the `let main` DATA symbol and the entry `fn main` TEXT symbol clash. * Pre-fix BOTH stages built the program rc=0 and SEGFAULTED at runtime * (the w6l silent-dup-symbol latent, task #31); align both UP to a loud * compile-time reject. Correct multi-main mangling (entry stays bare, * every other `main` module-qualifies) is the deferred root-unit entry * detection (task #32) — until then the collision is rejected, never * silently miscompiled (rule 7). * * The reject is program-GLOBAL, name-only, CROSS-MODULE: it counts * top-level decls named "main" across the fully-bundled program. The * existing duplicate-decl rejects key on (name, MODULE), so they read a * cross-package `foo.main` and the bare entry `main` as DISTINCT and * miss this clash. The trigger is COUNT("main") > 1, NOT package * identity — ww has no package-main convention (examples/cmatrix, lisp, * mandelbrot are working entries with `fn main` in a non-main package). * * Each reject row is a GENUINE cross-module case: a real `import foo;` * binds package foo, so decl_mod stamps foo's `main` with module "foo" * and the entry `main` with module NULL — DISTINCT keys the existing * (name, module) reject reads apart and lets through. Without the * import both `main`s collapse to module NULL and the OLD `duplicate fn * main` reject already fires, so the import is what exercises THIS * check (verified: pre-fix base 7a6b67f builds every row rc=0). * * row | shape | expect * ----------------+-----------------------------------------+-------- * let_main | import foo; foo `let main` + entry main | REJECT * fn_main | import foo; foo `fn main` + entry main | REJECT * def_main | import foo; foo `def main` + entry main | REJECT * type_main | import foo; foo `type main`+ entry main | REJECT * single_nonmain | one `fn main` in a NON-main package | BUILD, * | (cmatrix-style) — the corpus-safety | run 7 * | guard: a lone main must STILL compile. | * * DISCRIMINATION (verified against a clean 7a6b67f base): pre-fix the * let_main/def_main rows built rc=0 then exited 139 (SIGSEGV) and * fn_main/type_main built rc=0 and ran the WRONG entry (two bare `TEXT * main` symbols, the w6l silent-dup latent task #31); post-fix every * row loud-rejects at compile (build rc!=0, no binary), both stages. * The single_nonmain row proves the reject did not eat the working * single-`main` corpus. */ #include #include #include #include #include #include static int runwait(const char *cmd) { int rc = system(cmd); if (rc == -1) return -1; if (WIFEXITED(rc)) return WEXITSTATUS(rc); return -1; } static int filehas(const char *path, const char *needle) { char buf[8192]; FILE *f = fopen(path, "rb"); if (!f) return 0; size_t n = fread(buf, 1, sizeof buf - 1, f); fclose(f); buf[n] = '\0'; return strstr(buf, needle) != NULL; } /* build_should_fail — a dup-`main` program must error on `driver` * (loud reject); returns 0 when the build FAILS *and* emits `diag`. * This test drives the full `ww` driver, so a crash inside w6c also * surfaces as the driver's "w6c failed" exit 1 — the diagnostic-substring * check is the ONLY way to tell a crash from a clean reject here (#20). * Intermediates land in tmpdir, never the source tree (rule 14). */ static int build_should_fail(const char *driver, const char *label, const char *src, const char *diag, int i) { char s[128], tmpdir[64], outbin[128], errf[128], rmcmd[160], cmd[1024]; snprintf(tmpdir, sizeof tmpdir, "/tmp/dupmain_%d_d_%d", getpid(), i); mkdir(tmpdir, 0755); snprintf(s, sizeof s, "%s/dupmain_%d_%d.ww", tmpdir, getpid(), i); snprintf(outbin, sizeof outbin, "%s/out", tmpdir); snprintf(errf, sizeof errf, "%s/err.txt", tmpdir); snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir); FILE *f = fopen(s, "wb"); if (!f) { runwait(rmcmd); return -1; } fputs(src, f); fclose(f); snprintf(cmd, sizeof cmd, "%s build -o %s %s >/dev/null 2>%s", driver, outbin, s, errf); int rc = runwait(cmd); int hasmsg = filehas(errf, diag); runwait(rmcmd); if (rc == 0) { fprintf(stderr, "dup_main[%s][%s]: built ok, expected a loud reject\n", driver, label); return -1; } if (!hasmsg) { fprintf(stderr, "dup_main[%s][%s]: nonzero exit but missing " "diagnostic \"%s\" (a crash, not a clean reject)\n", driver, label, diag); return -1; } return 0; } /* run_build — build a program that MUST compile, run it, return its * exit code (the corpus-safety guard). */ static int run_build(const char *driver, const char *label, const char *src, int i) { char s[128], tmpdir[64], outbin[128], rmcmd[160], cmd[1024]; snprintf(tmpdir, sizeof tmpdir, "/tmp/dupmain_ok_%d_d_%d", getpid(), i); mkdir(tmpdir, 0755); snprintf(s, sizeof s, "%s/dupmain_ok_%d_%d.ww", tmpdir, getpid(), i); snprintf(outbin, sizeof outbin, "%s/dupmain_ok_%d_%d", tmpdir, getpid(), i); snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir); FILE *f = fopen(s, "wb"); if (!f) { runwait(rmcmd); return -1; } fputs(src, f); fclose(f); snprintf(cmd, sizeof cmd, "%s build -o %s %s 2>/dev/null", driver, outbin, s); if (runwait(cmd) != 0) { fprintf(stderr, "dup_main[%s][%s]: build failed (corpus " "regression — a lone main must compile)\n", driver, label); runwait(rmcmd); return -1; } int got = runwait(outbin); runwait(rmcmd); return got; } struct rejrow { const char *label; const char *src; const char *diag; }; /* Each program declares a second top-level `main` (in package foo) * alongside the entry `fn main` (in package main), bound by a REAL * `import foo;` — so decl_mod stamps the two `main`s with DISTINCT * modules ("foo" vs NULL) and the existing (name, module) reject reads * them apart and misses the clash; only THIS program-global name-only * reject fires. The driver bundles both packages into one unit; foo * also exports `anchor` so the import is used and foo resolves. */ static const struct rejrow reject_rows[] = { { "let_main", "package foo;\n" "export let main: i32 = 99;\n" "export fn anchor() i32 = { return main; };\n" "package main;\n" "import foo;\n" "fn main() i32 = { return foo.anchor(); };\n", "duplicate entry main" }, { "fn_main", "package foo;\n" "export fn main() i32 = { return 1; };\n" "export fn anchor() i32 = { return 2; };\n" "package main;\n" "import foo;\n" "fn main() i32 = { return foo.anchor(); };\n", "duplicate entry main" }, { "def_main", "package foo;\n" "export def main: i32 = 5;\n" "export fn anchor() i32 = { return main; };\n" "package main;\n" "import foo;\n" "fn main() i32 = { return foo.anchor(); };\n", "duplicate entry main" }, { "type_main", "package foo;\n" "export type main = i32;\n" "export fn anchor() i32 = { return 3; };\n" "package main;\n" "import foo;\n" "fn main() i32 = { return foo.anchor(); };\n", "duplicate entry main" }, }; struct okrow { const char *label; const char *src; int want; }; /* The corpus-safety guard: a SINGLE `fn main` in a NON-main package * (cmatrix/lisp/mandelbrot style) MUST still compile and run. */ static const struct okrow ok_rows[] = { { "single_nonmain", "package cmatrix;\n" "fn main() i32 = { return 7; };\n", 7 }, }; int main(void) { const char *bin = getenv("BIN"); if (!bin) bin = "out/bin"; char absbin[1024]; if (bin[0] != '/') { char cwd[1024]; if (getcwd(cwd, sizeof cwd) == NULL) return 1; snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); bin = absbin; } char cdrv[1024], wdrv[1024]; snprintf(cdrv, sizeof cdrv, "%s/ww", bin); snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); struct { const char *name; const char *path; int gated; } drivers[] = { { "cstage", cdrv, 0 }, { "wwstage", wdrv, 1 }, { NULL, NULL, 0 }, }; int nrej = (int)(sizeof reject_rows / sizeof reject_rows[0]); int nok = (int)(sizeof ok_rows / sizeof ok_rows[0]); int total = 0, fail = 0; for (int d = 0; drivers[d].name; d++) { if (drivers[d].gated && access(drivers[d].path, X_OK) != 0) { fprintf(stderr, "dup_main: skip %s (no %s)\n", drivers[d].name, drivers[d].path); continue; } for (int i = 0; i < nrej; i++) { total++; if (build_should_fail(drivers[d].path, reject_rows[i].label, reject_rows[i].src, reject_rows[i].diag, d * 100 + i) != 0) fail++; } for (int i = 0; i < nok; i++) { total++; int got = run_build(drivers[d].path, ok_rows[i].label, ok_rows[i].src, d * 100 + i); if (got != ok_rows[i].want) { fprintf(stderr, "dup_main[%s][%s]: exit=%d want=%d\n", drivers[d].name, ok_rows[i].label, got, ok_rows[i].want); fail++; } } } if (fail) { fprintf(stderr, "dup_main: %d/%d fixtures failed\n", fail, total); return 1; } printf("dup_main: %d/%d ok\n", total, total); return 0; }