From e091dfbdbe757a9a05a954643339a7a17ad5eb16 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 22:07:13 +0900 Subject: [PATCH] =?UTF-8?q?wcc-ww:=20assert/abort=20builtins=20=E2=80=94?= =?UTF-8?q?=20checker=20tag=20+=20cgen=20rt=5Fabort=20lowering=20(#58)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wwstage had no EXPR_ASSERT-family intercept: the checker left bare assert/abort calls untyped (asserttyped gate 4 skipped them by design) and cgcall fell through to the regular call path, emitting CALL assert(SB) for a symbol that exists nowhere — link-fail. cstage was already correct (tag ty_err at check.c:1536-1572, lower inline via rt_abort at cgen.c:6618-6663). Mirror the same tag-then-lower pair: exprtype N_CALL stamps the call void and the callee TY_ERR behind the scopelookupprefer no-shadow gate (the isassertfam predicate), with the cstage arg diagnostics (cond must be bool, msg must be str, arity caps); cgcall keys on the TY_ERR tag and emits the identical CMPQ/JNE/rt_abort sequence. A user-shadowed assert/abort (same-module or cross-module, the #45 shape, task #14) stays untagged on the regular call path — byte-id for all existing lib code preserved. The cond check does NOT alias-peel: cstage compares ty_bool by identity (check.c:1560), so `type myb = bool` is rejected there; wwstage aligns down per rule 10 (a resolvealias here was accepting it — cs/ww accept-reject divergence). Widening both stages together belongs to the alias-peel choke-point arc (task #5, #47/#68). The resolvewalk N_IDENT resolution counter learns the builtin shape: an unshadowed abort/assert ident binds no sym BY DESIGN, so wwdump -r's zero-unresolved gate (990 probe 4) counts it resolved instead of failing builtin-using units. test 957: 13 rows — pass/fail/msg/bare abort (run exit + rt_abort stderr content; no-msg rows pin EMPTY stderr = the (NULL,0) shape), assert in an imported module, same-module + cross-module shadow controls, 5 checker rejects pinned on diagnostic CONTENT (shared substring; cstage prefixes pos, wwstage cerr is bare) incl. the alias-of-bool cond row pinning the rule-10 down-alignment; each positive row pins cstage run exit + cs==ww byte-id. On pre-fix master 11/13 rows trip (survivors = the two shadow controls). Residual (separate root, deferred diagnostic class): zero-arg assert() is not intercepted by either stage; cstage rejects via the generic undefined-ident path, wwstage's undefined-callee diagnostic is the class deferred behind wiring checkfile into w6c_ww. --- Makefile | 6 + selfhost/cmd/w6c/main.combined.ww | 127 +++++++++- selfhost/cmd/wcc/cgenexpr.ww | 40 +++ selfhost/cmd/wcc/check.ww | 87 ++++++- selfhost/cmd/wwdump/main.combined.ww | 127 +++++++++- test/wcc/957_assert_builtin_run.c | 348 +++++++++++++++++++++++++++ 6 files changed, 720 insertions(+), 15 deletions(-) create mode 100644 test/wcc/957_assert_builtin_run.c diff --git a/Makefile b/Makefile index 8205771f..c065cfcd 100644 --- a/Makefile +++ b/Makefile @@ -459,6 +459,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_f32stamp_run \ $(BIN)/test_f32arg_run \ $(BIN)/test_tuprecv_f64_run \ + $(BIN)/test_assert_builtin_run \ $(BIN)/test_modqualdestr_run \ $(BIN)/test_tupparam_run \ $(BIN)/test_structparam_run \ @@ -2023,6 +2024,11 @@ $(BIN)/test_tuprecv_f64_run: test/wcc/956_tuprecv_f64_run.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_assert_builtin_run: test/wcc/957_assert_builtin_run.c $(BIN)/ww \ + $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_modqualdestr_run: test/wcc/956_modqualdestr_run.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 05d7a073..f024c7e1 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -10657,11 +10657,20 @@ fn resolvewalk(c: *checker, n: *node) void = { if (nm.len > 0) { let s: *sym = scopelookupprefer(c.cur, c.curmod, nm); if (s == nil) { - c.nunresolved += 1; - if (c.verbose != 0) { - cerr(" unresolved id: "); - cerr(nm); - cerr("\n"); + // Unshadowed abort/assert binds no sym BY + // DESIGN (the EXPR_ASSERT family has no callee + // object — see isassertfam); count it resolved + // so wwdump -r's zero-unresolved gate holds + // over builtin-using lib code (#58 respell). + if (isassertfam(c, n)) { + c.nresolved += 1; + } else { + c.nunresolved += 1; + if (c.verbose != 0) { + cerr(" unresolved id: "); + cerr(nm); + cerr("\n"); + }; }; } else { c.nresolved += 1; }; }; @@ -12915,6 +12924,74 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // Hare-faithful path. This is the intercept-shim minimum to // unblock #15 (A.6.2.1e assertion enable). if (callee.kind == nkind.N_IDENT) { + // abort([msg]) / assert(cond[, msg]) — the EXPR_ASSERT + // family (harec ref/harec/src/check.c:877,893), lowered + // to rt_abort. Builtin only when no user symbol shadows + // the name — the same scopelookupprefer gate as cstage + // cmd/wcc/check.c:1536-1572 and isassertfam; a shadowed + // call stays on the regular path (#58; the #45 + // flat-scope root is filed as task #14). + // The TY_ERR stamp on the callee is cgen's routing key + // (cstage spells it `n->lhs->type = ty_err`). + if (streq(callee.str, "abort") + && scopelookupprefer(c.cur, c.curmod, "abort") == nil) { + if (e.list != nil) { + let mt: *node = exprtype(c, e.list, nil); + let conf: bool = false; + if (!isassignable(c, mktname(c, "str"), mt, &conf)) { + cerr("abort: message must be str\n"); + c.errs += 1; + }; + if (e.list.next != nil) { + cerr("abort: at most one arg\n"); + c.errs += 1; + }; + }; + let tn: *node = mktname(c, "void"); + e.type_ = tinfofornode(c, tn): *void; + callee.type_ = c.tc.tyerr: *void; + return tn; + }; + if (streq(callee.str, "assert") && e.list != nil + && scopelookupprefer(c.cur, c.curmod, "assert") == nil) { + let ct: *node = exprtype(c, e.list, nil); + if (ct != nil) { + // No alias peel: cstage compares ty_bool by + // IDENTITY (cmd/wcc/check.c:1560), so a + // `type myb = bool` cond is rejected there — + // align down (rule 10). Widening belongs to + // the alias-peel choke-point arc (task #5, + // #47/#68), both stages together. + let cu: *node = unwrapbang(ct); + let condok: bool = false; + if (cu.kind == nkind.N_TNAME) { + if (streq(cu.str, "bool") + || streq(cu.str, "untyped_bool")) { + condok = true; + }; + }; + if (!condok) { + cerr("assert: cond must be bool\n"); + c.errs += 1; + }; + }; + if (e.list.next != nil) { + let mt: *node = exprtype(c, e.list.next, nil); + let conf: bool = false; + if (!isassignable(c, mktname(c, "str"), mt, &conf)) { + cerr("assert: message must be str\n"); + c.errs += 1; + }; + if (e.list.next.next != nil) { + cerr("assert: at most two args\n"); + c.errs += 1; + }; + }; + let tn: *node = mktname(c, "void"); + e.type_ = tinfofornode(c, tn): *void; + callee.type_ = c.tc.tyerr: *void; + return tn; + }; if (streq(callee.str, "len")) { let tn: *node = mktname(c, "i32"); e.type_ = tinfofornode(c, tn): *void; @@ -26006,6 +26083,46 @@ fn cgcall(c: *cgen, n: *node) void = { let callee: *node = n.lhs; if (callee != nil) { if (callee.kind == nkind.N_IDENT) { + // abort([msg]) / assert(cond[, msg]) — checker-tagged + // builtins (callee TY_ERR is the routing key, cstage's + // `lhs->type == ty_err` at cmd/w6c/cgen.c:6618,6645); + // lower to rt_abort. A user-shadowed abort/assert is + // untagged and stays on the regular call path (#58). + let bti: *tinfo = callee.type_: *tinfo; + if (bti != nil && bti.kind == tykind.TY_ERR) { + if (streq(callee.str, "abort")) { + if (n.list != nil) { + cgexpr(c, n.list); + emitline("\tMOVQ\tAX, DI\n"); + emitline("\tMOVQ\tBX, SI\n"); + } else { + emitline("\tMOVQ\t$0, DI\n"); + emitline("\tMOVQ\t$0, SI\n"); + }; + emitline("\tCALL\trt_abort(SB)\n"); + return; + }; + if (streq(callee.str, "assert") && n.list != nil) { + cgexpr(c, n.list); + let skip: str = mklabel(c, "as"); + emitline("\tCMPQ\t$0, AX\n"); + emitline("\tJNE\t"); + emitline(skip); + emitline("\n"); + let msg: *node = n.list.next; + if (msg != nil) { + cgexpr(c, msg); + emitline("\tMOVQ\tAX, DI\n"); + emitline("\tMOVQ\tBX, SI\n"); + } else { + emitline("\tMOVQ\t$0, DI\n"); + emitline("\tMOVQ\t$0, SI\n"); + }; + emitline("\tCALL\trt_abort(SB)\n"); + emitlabel(skip); + return; + }; + }; if (streq(callee.str, "append")) { if (n.list != nil) { if (n.list.next != nil) { diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 492ccc91..0a2ce29e 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -5634,6 +5634,46 @@ fn cgcall(c: *cgen, n: *node) void = { let callee: *node = n.lhs; if (callee != nil) { if (callee.kind == nkind.N_IDENT) { + // abort([msg]) / assert(cond[, msg]) — checker-tagged + // builtins (callee TY_ERR is the routing key, cstage's + // `lhs->type == ty_err` at cmd/w6c/cgen.c:6618,6645); + // lower to rt_abort. A user-shadowed abort/assert is + // untagged and stays on the regular call path (#58). + let bti: *tinfo = callee.type_: *tinfo; + if (bti != nil && bti.kind == tykind.TY_ERR) { + if (streq(callee.str, "abort")) { + if (n.list != nil) { + cgexpr(c, n.list); + emitline("\tMOVQ\tAX, DI\n"); + emitline("\tMOVQ\tBX, SI\n"); + } else { + emitline("\tMOVQ\t$0, DI\n"); + emitline("\tMOVQ\t$0, SI\n"); + }; + emitline("\tCALL\trt_abort(SB)\n"); + return; + }; + if (streq(callee.str, "assert") && n.list != nil) { + cgexpr(c, n.list); + let skip: str = mklabel(c, "as"); + emitline("\tCMPQ\t$0, AX\n"); + emitline("\tJNE\t"); + emitline(skip); + emitline("\n"); + let msg: *node = n.list.next; + if (msg != nil) { + cgexpr(c, msg); + emitline("\tMOVQ\tAX, DI\n"); + emitline("\tMOVQ\tBX, SI\n"); + } else { + emitline("\tMOVQ\t$0, DI\n"); + emitline("\tMOVQ\t$0, SI\n"); + }; + emitline("\tCALL\trt_abort(SB)\n"); + emitlabel(skip); + return; + }; + }; if (streq(callee.str, "append")) { if (n.list != nil) { if (n.list.next != nil) { diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 0a45d12c..b22b8b5a 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -330,11 +330,20 @@ fn resolvewalk(c: *checker, n: *node) void = { if (nm.len > 0) { let s: *sym = scopelookupprefer(c.cur, c.curmod, nm); if (s == nil) { - c.nunresolved += 1; - if (c.verbose != 0) { - cerr(" unresolved id: "); - cerr(nm); - cerr("\n"); + // Unshadowed abort/assert binds no sym BY + // DESIGN (the EXPR_ASSERT family has no callee + // object — see isassertfam); count it resolved + // so wwdump -r's zero-unresolved gate holds + // over builtin-using lib code (#58 respell). + if (isassertfam(c, n)) { + c.nresolved += 1; + } else { + c.nunresolved += 1; + if (c.verbose != 0) { + cerr(" unresolved id: "); + cerr(nm); + cerr("\n"); + }; }; } else { c.nresolved += 1; }; }; @@ -2588,6 +2597,74 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // Hare-faithful path. This is the intercept-shim minimum to // unblock #15 (A.6.2.1e assertion enable). if (callee.kind == nkind.N_IDENT) { + // abort([msg]) / assert(cond[, msg]) — the EXPR_ASSERT + // family (harec ref/harec/src/check.c:877,893), lowered + // to rt_abort. Builtin only when no user symbol shadows + // the name — the same scopelookupprefer gate as cstage + // cmd/wcc/check.c:1536-1572 and isassertfam; a shadowed + // call stays on the regular path (#58; the #45 + // flat-scope root is filed as task #14). + // The TY_ERR stamp on the callee is cgen's routing key + // (cstage spells it `n->lhs->type = ty_err`). + if (streq(callee.str, "abort") + && scopelookupprefer(c.cur, c.curmod, "abort") == nil) { + if (e.list != nil) { + let mt: *node = exprtype(c, e.list, nil); + let conf: bool = false; + if (!isassignable(c, mktname(c, "str"), mt, &conf)) { + cerr("abort: message must be str\n"); + c.errs += 1; + }; + if (e.list.next != nil) { + cerr("abort: at most one arg\n"); + c.errs += 1; + }; + }; + let tn: *node = mktname(c, "void"); + e.type_ = tinfofornode(c, tn): *void; + callee.type_ = c.tc.tyerr: *void; + return tn; + }; + if (streq(callee.str, "assert") && e.list != nil + && scopelookupprefer(c.cur, c.curmod, "assert") == nil) { + let ct: *node = exprtype(c, e.list, nil); + if (ct != nil) { + // No alias peel: cstage compares ty_bool by + // IDENTITY (cmd/wcc/check.c:1560), so a + // `type myb = bool` cond is rejected there — + // align down (rule 10). Widening belongs to + // the alias-peel choke-point arc (task #5, + // #47/#68), both stages together. + let cu: *node = unwrapbang(ct); + let condok: bool = false; + if (cu.kind == nkind.N_TNAME) { + if (streq(cu.str, "bool") + || streq(cu.str, "untyped_bool")) { + condok = true; + }; + }; + if (!condok) { + cerr("assert: cond must be bool\n"); + c.errs += 1; + }; + }; + if (e.list.next != nil) { + let mt: *node = exprtype(c, e.list.next, nil); + let conf: bool = false; + if (!isassignable(c, mktname(c, "str"), mt, &conf)) { + cerr("assert: message must be str\n"); + c.errs += 1; + }; + if (e.list.next.next != nil) { + cerr("assert: at most two args\n"); + c.errs += 1; + }; + }; + let tn: *node = mktname(c, "void"); + e.type_ = tinfofornode(c, tn): *void; + callee.type_ = c.tc.tyerr: *void; + return tn; + }; if (streq(callee.str, "len")) { let tn: *node = mktname(c, "i32"); e.type_ = tinfofornode(c, tn): *void; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index bec47cea..cbf36e17 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -10657,11 +10657,20 @@ fn resolvewalk(c: *checker, n: *node) void = { if (nm.len > 0) { let s: *sym = scopelookupprefer(c.cur, c.curmod, nm); if (s == nil) { - c.nunresolved += 1; - if (c.verbose != 0) { - cerr(" unresolved id: "); - cerr(nm); - cerr("\n"); + // Unshadowed abort/assert binds no sym BY + // DESIGN (the EXPR_ASSERT family has no callee + // object — see isassertfam); count it resolved + // so wwdump -r's zero-unresolved gate holds + // over builtin-using lib code (#58 respell). + if (isassertfam(c, n)) { + c.nresolved += 1; + } else { + c.nunresolved += 1; + if (c.verbose != 0) { + cerr(" unresolved id: "); + cerr(nm); + cerr("\n"); + }; }; } else { c.nresolved += 1; }; }; @@ -12915,6 +12924,74 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // Hare-faithful path. This is the intercept-shim minimum to // unblock #15 (A.6.2.1e assertion enable). if (callee.kind == nkind.N_IDENT) { + // abort([msg]) / assert(cond[, msg]) — the EXPR_ASSERT + // family (harec ref/harec/src/check.c:877,893), lowered + // to rt_abort. Builtin only when no user symbol shadows + // the name — the same scopelookupprefer gate as cstage + // cmd/wcc/check.c:1536-1572 and isassertfam; a shadowed + // call stays on the regular path (#58; the #45 + // flat-scope root is filed as task #14). + // The TY_ERR stamp on the callee is cgen's routing key + // (cstage spells it `n->lhs->type = ty_err`). + if (streq(callee.str, "abort") + && scopelookupprefer(c.cur, c.curmod, "abort") == nil) { + if (e.list != nil) { + let mt: *node = exprtype(c, e.list, nil); + let conf: bool = false; + if (!isassignable(c, mktname(c, "str"), mt, &conf)) { + cerr("abort: message must be str\n"); + c.errs += 1; + }; + if (e.list.next != nil) { + cerr("abort: at most one arg\n"); + c.errs += 1; + }; + }; + let tn: *node = mktname(c, "void"); + e.type_ = tinfofornode(c, tn): *void; + callee.type_ = c.tc.tyerr: *void; + return tn; + }; + if (streq(callee.str, "assert") && e.list != nil + && scopelookupprefer(c.cur, c.curmod, "assert") == nil) { + let ct: *node = exprtype(c, e.list, nil); + if (ct != nil) { + // No alias peel: cstage compares ty_bool by + // IDENTITY (cmd/wcc/check.c:1560), so a + // `type myb = bool` cond is rejected there — + // align down (rule 10). Widening belongs to + // the alias-peel choke-point arc (task #5, + // #47/#68), both stages together. + let cu: *node = unwrapbang(ct); + let condok: bool = false; + if (cu.kind == nkind.N_TNAME) { + if (streq(cu.str, "bool") + || streq(cu.str, "untyped_bool")) { + condok = true; + }; + }; + if (!condok) { + cerr("assert: cond must be bool\n"); + c.errs += 1; + }; + }; + if (e.list.next != nil) { + let mt: *node = exprtype(c, e.list.next, nil); + let conf: bool = false; + if (!isassignable(c, mktname(c, "str"), mt, &conf)) { + cerr("assert: message must be str\n"); + c.errs += 1; + }; + if (e.list.next.next != nil) { + cerr("assert: at most two args\n"); + c.errs += 1; + }; + }; + let tn: *node = mktname(c, "void"); + e.type_ = tinfofornode(c, tn): *void; + callee.type_ = c.tc.tyerr: *void; + return tn; + }; if (streq(callee.str, "len")) { let tn: *node = mktname(c, "i32"); e.type_ = tinfofornode(c, tn): *void; @@ -26006,6 +26083,46 @@ fn cgcall(c: *cgen, n: *node) void = { let callee: *node = n.lhs; if (callee != nil) { if (callee.kind == nkind.N_IDENT) { + // abort([msg]) / assert(cond[, msg]) — checker-tagged + // builtins (callee TY_ERR is the routing key, cstage's + // `lhs->type == ty_err` at cmd/w6c/cgen.c:6618,6645); + // lower to rt_abort. A user-shadowed abort/assert is + // untagged and stays on the regular call path (#58). + let bti: *tinfo = callee.type_: *tinfo; + if (bti != nil && bti.kind == tykind.TY_ERR) { + if (streq(callee.str, "abort")) { + if (n.list != nil) { + cgexpr(c, n.list); + emitline("\tMOVQ\tAX, DI\n"); + emitline("\tMOVQ\tBX, SI\n"); + } else { + emitline("\tMOVQ\t$0, DI\n"); + emitline("\tMOVQ\t$0, SI\n"); + }; + emitline("\tCALL\trt_abort(SB)\n"); + return; + }; + if (streq(callee.str, "assert") && n.list != nil) { + cgexpr(c, n.list); + let skip: str = mklabel(c, "as"); + emitline("\tCMPQ\t$0, AX\n"); + emitline("\tJNE\t"); + emitline(skip); + emitline("\n"); + let msg: *node = n.list.next; + if (msg != nil) { + cgexpr(c, msg); + emitline("\tMOVQ\tAX, DI\n"); + emitline("\tMOVQ\tBX, SI\n"); + } else { + emitline("\tMOVQ\t$0, DI\n"); + emitline("\tMOVQ\t$0, SI\n"); + }; + emitline("\tCALL\trt_abort(SB)\n"); + emitlabel(skip); + return; + }; + }; if (streq(callee.str, "append")) { if (n.list != nil) { if (n.list.next != nil) { diff --git a/test/wcc/957_assert_builtin_run.c b/test/wcc/957_assert_builtin_run.c new file mode 100644 index 00000000..d93bef85 --- /dev/null +++ b/test/wcc/957_assert_builtin_run.c @@ -0,0 +1,348 @@ +/* + * 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; +}