diff --git a/Makefile b/Makefile index 18544b3d..9d7da2cc 100644 --- a/Makefile +++ b/Makefile @@ -409,7 +409,8 @@ OBJECT_WW_TARGETS = $(OBJECT_WW_TESTS:%=wwtest/%) # under test-compiler. MISC_WW_TESTS = test/misc/arrglob_test.ww test/misc/packedwwi_test.ww \ test/misc/reject_test.ww test/misc/heldasym_test.ww \ - test/misc/stampdiag_test.ww test/misc/qualast_test.ww + test/misc/stampdiag_test.ww test/misc/qualast_test.ww \ + test/misc/abortmsg_test.ww MISC_WW_TARGETS = $(MISC_WW_TESTS:%=wwtest/%) BOOTSTRAP_WRAPPER_SOURCES = test/wcc/950_selfcheck.c \ test/wcc/991_w6a_ww.c \ diff --git a/test/misc/abortmsg_test.ww b/test/misc/abortmsg_test.ww new file mode 100644 index 00000000..a522a194 --- /dev/null +++ b/test/misc/abortmsg_test.ww @@ -0,0 +1,65 @@ +package abortmsg_test; + +// Run-STDERR content net for the #58 assert/abort builtins, ported +// from the retired native carrier test/wcc/957_assert_builtin_run.c. +// The eight exit-code rows and the checker rejects are owned verbatim +// by the r957_* fixtures, and the w6c-vs-w6c_ww byte-id by the +// test-data-byteid blanket; the fixture protocol checks run EXIT +// only, so the run-stderr dimension — msg content for the msg forms, +// MANDATORY EMPTINESS for the bare forms (rt_abort(NULL, 0) writes +// nothing) — lives here. The four abort-shaped fixture sources are +// built IN PLACE so fixture and probe can never drift; the cstage +// driver suffices, byte-id extends the lowering to wwstage (the +// carrier's own argument). + +import os; +import os.exec; +import strings; +import testenv; +import time; + +fn fail(label: str, why: str) void = { + let m: str = strings.concat("abortmsg FAIL: ", label, " -- ", why, + "\n"); + os.write(2, m.ptr, m.len: u64); + assert(false); +}; + +fn tmo() time.duration = { + return (240i64 * (time.second: i64)): time.duration; +}; + +// needle "" = run stderr must be EXACTLY empty. +fn stderrrow(label: str, fixture: str, needle: str) void = { + let src: str = strings.concat(testenv.repo(), "/test/wcc/data/", + fixture, "/case.ww"); + let td: str = testenv.fresh(); + let co: testenv.commandout; + let bav: []str = [testenv.driver("ww"), "build", "-o", "bin", src]; + testenv.runcommand(td, td, "build", bav, tmo(), &co); + if (co.termination != exec.termination.EXIT || co.code != 0) { + fail(label, "cstage build failed"); + }; + let rav: []str = [strings.concat(td, "/bin")]; + testenv.runcommand(td, td, "run", rav, tmo(), &co); + if (co.termination != exec.termination.EXIT || co.code != 1) { + fail(label, "run-exit != 1"); + }; + if (needle.len != 0) { + if (!testenv.has(co.stderr, needle)) { + fail(label, strings.concat("run stderr missing \"", + needle, "\"")); + }; + } else if (co.stderr.len != 0) { + fail(label, strings.concat("run stderr not empty ", + "(rt_abort no-msg must be (NULL, 0))")); + }; + testenv.clean(td); +}; + +@test fn abortstderr() void = { + stderrrow("fail_nomsg", "r957_assert_fail_nomsg", ""); + stderrrow("fail_msg", "r957_assert_fail_msg", "assert fired"); + stderrrow("abort_msg", "r957_abort_msg", "boom"); + stderrrow("abort_bare", "r957_abort_bare", ""); +}; diff --git a/test/wcc/957_assert_builtin_run.c b/test/wcc/957_assert_builtin_run.c deleted file mode 100644 index c4e8a56e..00000000 --- a/test/wcc/957_assert_builtin_run.c +++ /dev/null @@ -1,291 +0,0 @@ -/* - * 957_assert_builtin_run — stderr + byte-id 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. - * - * Shared runtime and reject contracts live in r957_* fixtures. This carrier - * retains runtime stderr for aborting rows and w6c vs w6c_ww `.s` identity. - * The latter 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, its fixture should change polarity with it. - * - * The migrated reject fixtures retain the checker diagnostics and the - * alias-cond rule-10 down-alignment. - */ -#include -#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 observe_output; -}; - -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 }, - /* 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, 1 }, - /* 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", 1 }, - /* 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", 1 }, - /* 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, 1 }, - /* 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 }, - /* 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 }, - /* 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, NULL, 0, NULL, 0 } -}; - -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 tmpdir[64] = "/tmp/wwasrt_XXXXXX"; - if (mkdtemp(tmpdir) == NULL) { - perror("assert_builtin: mkdtemp"); - fail++; - continue; - } - char src[128], errf[128], outbin[128], sepwork[160]; - char cs_s[128], ws_s[128], rmcmd[192]; - snprintf(src, sizeof src, "%s/wwasrt_%d_%d.ww", - tmpdir, getpid(), i); - snprintf(errf, sizeof errf, "%s/wwasrt_%d_%d.err", - tmpdir, getpid(), i); - snprintf(outbin, sizeof outbin, "%s/wwasrt_%d_%d", - tmpdir, getpid(), i); - snprintf(sepwork, sizeof sepwork, "%s.sepwork", outbin); - snprintf(cs_s, sizeof cs_s, "%s/wwasrt_%d_%d_cs.s", - tmpdir, getpid(), i); - snprintf(ws_s, sizeof ws_s, "%s/wwasrt_%d_%d_ww.s", - tmpdir, getpid(), i); - snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", sepwork); - FILE *f = fopen(src, "wb"); - if (f == NULL) { fail++; goto cleanup; } - fputs(rows[i].src, f); - fclose(f); - - char cmd[2048]; - if (rows[i].observe_output) { - snprintf(cmd, sizeof cmd, "%s/ww build -o %s %s " - ">/dev/null 2>&1", bin, outbin, src); - if (runwait(cmd) != 0) { - fprintf(stderr, "row[%s]: cstage build failed\n", - rows[i].label); - fail++; - goto cleanup; - } - - 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++; - } - } - - /* cs==ww byte-id gate. */ - 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++; - goto cleanup; - } - 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++; - goto cleanup; - } - 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++; - } - -cleanup: { - int cleanbad = 0; - if (unlink(src) != 0 && errno != ENOENT) cleanbad = 1; - if (unlink(errf) != 0 && errno != ENOENT) cleanbad = 1; - if (unlink(outbin) != 0 && errno != ENOENT) cleanbad = 1; - if (unlink(cs_s) != 0 && errno != ENOENT) cleanbad = 1; - if (unlink(ws_s) != 0 && errno != ENOENT) cleanbad = 1; - if (runwait(rmcmd) != 0) cleanbad = 1; - if (rmdir(tmpdir) != 0) cleanbad = 1; - if (cleanbad) { - fprintf(stderr, "row[%s]: temporary cleanup failed\n", - rows[i].label); - fail++; - } - } - } - - if (fail) { - fprintf(stderr, "%d/%d assert-builtin tests failed\n", - fail, n); - return 1; - } - printf("assert_builtin: %d/%d ok (stderr + cs==ww byte-id)\n", - n, n); - return 0; -}