/* * 957_assert_builtin_run — runtime + byte-id + reject net for #58: the * `assert(cond[, msg])` / `abort([msg])` builtins (the harec * EXPR_ASSERT family) in BOTH stages. * * Root: wwstage had NO builtin intercept for assert/abort — the checker * left the call untyped (asserttyped gate 4 deliberately skipped it) * and cgcall fell through to the regular call path, emitting * `CALL assert(SB)` for a symbol that exists nowhere → link-fail. * cstage lowers both inline via rt_abort (checker tags the callee * ty_err at cmd/wcc/check.c:1536-1572, cgen keys on the tag at * cmd/w6c/cgen.c:6618-6663). The fix mirrors that tag-then-lower pair * into check.ww exprtype N_CALL + cgenexpr.ww cgcall. * * THIS TEST MUST CATCH A WWSTAGE-ONLY DIVERGENCE, so every positive * row carries both dimensions (modelled on 953/954): * (a) cstage `ww build` + run, asserting the exit code AND the * runtime stderr (rt_abort writes msg verbatim; a NULL want_msg * pins the (NULL, 0) no-msg shape as EMPTY stderr). * (b) w6c vs w6c_ww `.s` cmp — FAILS if the stages diverge. On * pre-fix master every assert/abort row diverges (wwstage emits * the bogus CALL). * * Shadow rows pin the gate's other half: a user fn named assert/abort * (same-module or cross-module, the #45 shape) suppresses the builtin * and routes the regular call path in BOTH stages. The cross-module * row pins the CURRENT flat-scope semantics — if the #45 root (filed * as task #14: cross-module bare resolution of a foreign decl) is * later ruled a reject, that row flips to expect_fail with it. * * Reject rows pin the checker mirror ON CONTENT: non-bool cond / * non-str msg / arity overflows fail the build in BOTH stages with the * shared diagnostic substring (cstage prefixes pos, wwstage's cerr is * bare — strstr covers both). The alias-cond row pins the rule-10 * down-alignment: cstage compares ty_bool by identity (check.c:1560), * so wwstage must not alias-peel either (widening = task #5 alias arc). */ #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 long slurp(const char *path, char *buf, long cap) { FILE *f = fopen(path, "rb"); if (!f) return -1; long n = (long)fread(buf, 1, cap - 1, f); fclose(f); if (n < 0) n = 0; buf[n] = '\0'; return n; } struct row { const char *label; const char *src; int want_exit; const char *want_msg; /* run stderr must contain this; NULL = must be empty */ int expect_fail; /* 1 = BOTH stages must reject; want_exit/want_msg ignored */ const char *diag; /* reject rows: both stages' stderr must contain this */ }; static const struct row rows[] = { /* Passing asserts (bare and with msg) fall through; exit 42. */ { "pass", "package main;\n" "export fn main() i32 = {\n" "\tassert(1 + 1 == 2);\n" "\tassert(2 > 1, \"never fires\");\n" "\treturn 42;\n" "};\n", 42, NULL, 0, NULL }, /* Failing assert without msg: rt_abort(NULL, 0), exit 1, silent. */ { "fail_nomsg", "package main;\n" "export fn main() i32 = {\n" "\tassert(1 == 2);\n" "\treturn 0;\n" "};\n", 1, NULL, 0, NULL }, /* Failing assert with msg: rt_abort(ptr, len) writes it to stderr. */ { "fail_msg", "package main;\n" "export fn main() i32 = {\n" "\tassert(false, \"assert fired\\n\");\n" "\treturn 0;\n" "};\n", 1, "assert fired", 0, NULL }, /* abort with msg: unconditional rt_abort, exit 1, msg on stderr. */ { "abort_msg", "package main;\n" "export fn main() i32 = {\n" "\tabort(\"boom\\n\");\n" "\treturn 0;\n" "};\n", 1, "boom", 0, NULL }, /* bare abort(): rt_abort(NULL, 0), exit 1, silent. */ { "abort_bare", "package main;\n" "export fn main() i32 = {\n" "\tabort();\n" "\treturn 0;\n" "};\n", 1, NULL, 0, NULL }, /* assert inside an IMPORTED module's fn (single-file multi-package * form, like 953) — the regex fold-5 consumer shape. */ { "imported_mod", "package m;\n" "export fn f() i32 = {\n" "\tassert(3 > 2, \"m.f invariant\");\n" "\treturn 42;\n" "};\n" "package main;\n" "import m;\n" "export fn main() i32 = { return m.f(); };\n", 42, NULL, 0, NULL }, /* SHADOW control, same-module: a user fn `assert` suppresses the * builtin; the call routes the regular path (no-op fn), so the * false cond does NOT abort. Exit 7 proves the user fn ran. */ { "shadow_samemod", "package main;\n" "let g: i32 = 0;\n" "fn assert(b: bool) void = { g = 7; };\n" "export fn main() i32 = {\n" "\tassert(false);\n" "\treturn g;\n" "};\n", 7, NULL, 0, NULL }, /* SHADOW control, cross-module (#45 shape, task #14): a foreign * exported fn `assert` in the flat scope also suppresses the * builtin; bare `assert(false, ...)` binds it and does NOT abort. */ { "shadow_xmod", "package m;\n" "export fn assert(b: bool, msg: str) void = { };\n" "package main;\n" "import m;\n" "export fn main() i32 = {\n" "\tassert(false, \"routed to m.assert\");\n" "\treturn 42;\n" "};\n", 42, NULL, 0, NULL }, /* Checker rejects, mirrored both stages — pinned on diagnostic * CONTENT, not just a non-zero exit. */ { "reject_nonbool", "package main;\n" "export fn main() i32 = {\n" "\tassert(\"not a bool\");\n" "\treturn 0;\n" "};\n", 0, NULL, 1, "assert: cond must be bool" }, /* Alias-of-bool cond: ty_bool identity in cstage (check.c:1560) * → reject; wwstage aligned down (no resolvealias peel). */ { "reject_alias_cond", "package main;\n" "type myb = bool;\n" "export fn main() i32 = {\n" "\tlet b: myb = true;\n" "\tassert(b);\n" "\treturn 0;\n" "};\n", 0, NULL, 1, "assert: cond must be bool" }, { "reject_assert_arity", "package main;\n" "export fn main() i32 = {\n" "\tassert(true, \"m\", \"extra\");\n" "\treturn 0;\n" "};\n", 0, NULL, 1, "assert: at most two args" }, { "reject_abort_nonstr", "package main;\n" "export fn main() i32 = {\n" "\tabort(42);\n" "\treturn 0;\n" "};\n", 0, NULL, 1, "abort: message must be str" }, { "reject_abort_arity", "package main;\n" "export fn main() i32 = {\n" "\tabort(\"a\", \"b\");\n" "\treturn 0;\n" "};\n", 0, NULL, 1, "abort: at most one arg" }, { NULL, NULL, 0, NULL, 0, NULL } }; static int slurp_eq(const char *a, const char *b) { FILE *fa = fopen(a, "rb"); FILE *fb = fopen(b, "rb"); if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } int rc = 0; for (;;) { int ca = fgetc(fa); int cb = fgetc(fb); if (ca != cb) { rc = -1; break; } if (ca == EOF) break; } fclose(fa); fclose(fb); return rc; } 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 w6c[1100], w6c_ww[1100]; snprintf(w6c, sizeof w6c, "%s/w6c", bin); snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); if (access(w6c_ww, X_OK) != 0) { fprintf(stderr, "assert_builtin: w6c_ww missing — cannot run " "the cs==ww byte-id gate (the whole point of #58)\n"); return 1; } char errbuf[4096]; int n = 0, fail = 0; for (int i = 0; rows[i].src; i++, n++) { char src[64], errf[64]; snprintf(src, sizeof src, "/tmp/wwasrt_%d_%d.ww", getpid(), i); snprintf(errf, sizeof errf, "/tmp/wwasrt_%d_%d.err", getpid(), i); FILE *f = fopen(src, "wb"); if (f == NULL) { fail++; continue; } fputs(rows[i].src, f); fclose(f); if (rows[i].expect_fail) { char cmd[2048]; const char *stage[2] = { w6c, w6c_ww }; const char *sname[2] = { "cstage", "wwstage" }; for (int s = 0; s < 2; s++) { snprintf(cmd, sizeof cmd, "%s -o /dev/null %s 2>%s", stage[s], src, errf); if (runwait(cmd) == 0) { fprintf(stderr, "row[%s]: %s accepted, " "expected reject\n", rows[i].label, sname[s]); fail++; continue; } if (slurp(errf, errbuf, sizeof errbuf) < 0 || strstr(errbuf, rows[i].diag) == NULL) { fprintf(stderr, "row[%s]: %s rejected " "without \"%s\" on stderr\n", rows[i].label, sname[s], rows[i].diag); fail++; } } unlink(src); unlink(errf); continue; } /* (a) cstage build + run in a scratch dir. */ char tmpdir[64]; snprintf(tmpdir, sizeof tmpdir, "/tmp/wwasrt_%d_d_%d", getpid(), i); mkdir(tmpdir, 0755); char cmd[2048]; snprintf(cmd, sizeof cmd, "cd %s && %s/ww build %s " ">/dev/null 2>&1", tmpdir, bin, src); if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: cstage build failed\n", rows[i].label); fail++; unlink(src); rmdir(tmpdir); continue; } char outbin[128]; const char *base = strrchr(src, '/'); base = base ? base + 1 : src; snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); char *dot = strrchr(outbin, '.'); if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; snprintf(cmd, sizeof cmd, "%s >/dev/null 2>%s", outbin, errf); int got = runwait(cmd); if (got != rows[i].want_exit) { fprintf(stderr, "row[%s]: cstage exit %d, want %d\n", rows[i].label, got, rows[i].want_exit); fail++; } long elen = slurp(errf, errbuf, sizeof errbuf); if (rows[i].want_msg) { if (elen < 0 || strstr(errbuf, rows[i].want_msg) == NULL) { fprintf(stderr, "row[%s]: run stderr missing " "\"%s\"\n", rows[i].label, rows[i].want_msg); fail++; } } else if (elen != 0) { fprintf(stderr, "row[%s]: run stderr not empty " "(rt_abort no-msg must be (NULL, 0))\n", rows[i].label); fail++; } unlink(outbin); rmdir(tmpdir); unlink(errf); /* (b) cs==ww byte-id gate. */ char cs_s[64], ws_s[64]; snprintf(cs_s, sizeof cs_s, "/tmp/wwasrt_%d_%d_cs.s", getpid(), i); snprintf(ws_s, sizeof ws_s, "/tmp/wwasrt_%d_%d_ww.s", getpid(), i); snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, cs_s, src); if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label); fail++; unlink(src); continue; } snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c_ww, ws_s, src); if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: w6c_ww failed\n", rows[i].label); fail++; unlink(src); unlink(cs_s); continue; } if (slurp_eq(cs_s, ws_s) != 0) { fprintf(stderr, "row[%s]: cstage/wwstage .s DIFFER (rule-10 " "byte-id violation)\n", rows[i].label); fail++; } unlink(src); unlink(cs_s); unlink(ws_s); } if (fail) { fprintf(stderr, "%d/%d assert-builtin tests failed\n", fail, n); return 1; } printf("assert_builtin: %d/%d ok (cstage run+stderr + cs==ww byte-id + reject diags)\n", n, n); return 0; }