/* * 989_unknowndecl_reject — F14 #55: parsefile's recovery fallback * (lib/ww/parse/parse.ww) chewed an unrecognized top-level construct to * the next ';' WITHOUT emitting an error or bumping p.errs, so a typo'd * keyword / stray token / stray decl was silently dropped from the AST * and the build succeeded rc=0 with the work gone. The C twin * (parse.c:1395-1400) emits errorf("expected top-level decl") + p->errs++ * and rejects. The fix calls errmsg in the fallback arm, aligning wwstage * UP to cstage. * * Each REJECT row carries an unknown top-level construct and must FAIL to * build on BOTH driver twins; each control is well-formed and must * build+run. The fn-drop row is the live silent-loss case (a whole * declared fn vanished with no diagnostic pre-fix). * * Rule-10: every row runs on cstage `ww` and wwstage `ww_ww`; both must * agree. wwstage rows are gated on out/bin/ww_ww. */ #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; } struct row { const char *label; const char *src; int expect_build; /* 1 = build+run to want_exit; 0 = must FAIL */ int want_exit; const char *diag; /* expected reject diagnostic (NULL for build rows) */ }; static const struct row rows[] = { /* REJECT — a misspelled `fn` keyword: the whole decl was silently * dropped pre-fix (grep -c helper of the .s gave 0). */ { "misspelled_fn", "package main;\n" "fnn helper() i32 = { return 42; };\n" "export fn main() i32 = { return 0; };\n", 0, 0, "expected top-level decl" }, /* REJECT — a stray run of bare identifiers at top level. */ { "stray_idents", "package main;\n" "bogus token here;\n" "export fn main() i32 = { return 0; };\n", 0, 0, "expected top-level decl" }, /* REJECT — a stray decl whose body has internal ';'s, exercising the * brace-balanced chew recovery (still must error, not silently skip). */ { "stray_block", "package main;\n" "gizmo foo { let a = 1; let b = 2; };\n" "export fn main() i32 = { return 0; };\n", 0, 0, "expected top-level decl" }, /* control — a clean program builds + runs. */ { "clean_ok", "package main;\n" "fn helper() i32 = { return 5; };\n" "export fn main() i32 = { return helper(); };\n", 1, 5 }, /* control — a valid def/type/fn mix builds + runs. */ { "decls_ok", "package main;\n" "def K: i32 = 3;\n" "type t = i32;\n" "export fn main() i32 = { let x: t = K; return x: i32; };\n", 1, 3 }, }; static int run_build(const char *driver, const struct row *r, int i) { char src[64], tmpdir[64], cmd[1024]; snprintf(src, sizeof src, "/tmp/und_%d_%d.ww", getpid(), i); snprintf(tmpdir, sizeof tmpdir, "/tmp/und_%d_d_%d", getpid(), i); FILE *f = fopen(src, "wb"); if (!f) return -2; fputs(r->src, f); fclose(f); mkdir(tmpdir, 0755); snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", tmpdir, driver, src); int brc = runwait(cmd); const char *base = strrchr(src, '/'); base = base ? base + 1 : src; char outbin[128]; snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); char *dot = strrchr(outbin, '.'); if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; int got = -1; if (brc == 0) got = runwait(outbin); unlink(src); unlink(outbin); rmdir(tmpdir); return brc == 0 ? got : -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 — the build must FAIL *and* emit `diag`. A crash * (segfault) wraps as a nonzero "w6c failed" with no diagnostic, so the * substring check distinguishes it from a clean reject (#20). cstage * names the offending token ("expected top-level decl, got IDENT"), * wwstage prints "parse: expected top-level decl"; the shared core is * "expected top-level decl". */ static int build_should_fail(const char *driver, const char *src, const char *diag, int i) { char s[64], tmpdir[64], cmd[1024], errf[64]; snprintf(s, sizeof s, "/tmp/undn_%d_%d.ww", getpid(), i); snprintf(tmpdir, sizeof tmpdir, "/tmp/undn_%d_d_%d", getpid(), i); snprintf(errf, sizeof errf, "/tmp/undn_%d_e_%d.txt", getpid(), i); FILE *f = fopen(s, "wb"); if (!f) return -1; fputs(src, f); fclose(f); mkdir(tmpdir, 0755); snprintf(cmd, sizeof cmd, "cd %s && %s build %s >/dev/null 2>%s", tmpdir, driver, s, errf); int rc = runwait(cmd); int hasmsg = filehas(errf, diag); unlink(s); unlink(errf); const char *base = strrchr(s, '/'); base = base ? base + 1 : s; char outbin[128]; snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); char *dot = strrchr(outbin, '.'); if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; unlink(outbin); rmdir(tmpdir); /* build must NOT succeed AND must emit its diagnostic. */ return (rc != 0 && hasmsg) ? 0 : -1; } 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 *drv; int gated; } drivers[] = { { "cstage", cdrv, 0 }, { "wwstage", wdrv, 1 }, { NULL, NULL, 0 }, }; int n = (int)(sizeof rows / sizeof rows[0]); int total = 0, fail = 0; for (int d = 0; drivers[d].name; d++) { if (drivers[d].gated && access(drivers[d].drv, X_OK) != 0) { fprintf(stderr, "unknowndecl_reject: skip %s (no %s)\n", drivers[d].name, drivers[d].drv); continue; } for (int i = 0; i < n; i++) { total++; if (rows[i].expect_build) { int got = run_build(drivers[d].drv, &rows[i], i); if (got != rows[i].want_exit) { fprintf(stderr, "unknowndecl_reject[%s][%s]: " "exit=%d want=%d\n", drivers[d].name, rows[i].label, got, rows[i].want_exit); fail++; } } else { if (build_should_fail(drivers[d].drv, rows[i].src, rows[i].diag, 100 + i) != 0) { fprintf(stderr, "unknowndecl_reject[%s][%s]: " "built ok, expected a loud reject\n", drivers[d].name, rows[i].label); fail++; } } } } if (fail) { fprintf(stderr, "unknowndecl_reject: %d/%d fixtures failed\n", fail, total); return 1; } printf("unknowndecl_reject: %d/%d ok\n", total, total); return 0; }