diff --git a/Makefile b/Makefile index 27fb1a1e..d4f1bb6a 100644 --- a/Makefile +++ b/Makefile @@ -391,6 +391,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_append_wide_elem \ $(BIN)/test_delete_elem \ $(BIN)/test_placeaddr_store \ + $(BIN)/test_tryprop_multisuccess \ $(BIN)/test_struct_tuple_field_slot \ $(BIN)/test_widen_pad_zero_run \ $(BIN)/test_named_ptr_alias_variant_widen \ @@ -1023,6 +1024,17 @@ $(BIN)/test_placeaddr_store: test/wcc/805_placeaddr_store.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# F8+F9 (tasks #5/#12, regex fold-2b): `?` interim single-success gate +# (|success| > 1 loud-rejected on BOTH stages until task #14's +# subset-union typing) + direct `f()? is T` / `match (f()?)` reject +# parity — reject rows w/ exact per-stage diagnostic text, accept rows +# runtime + cs==ww asm byte-id. +$(BIN)/test_tryprop_multisuccess: test/wcc/806_tryprop_multisuccess.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # #237: a tuple-typed struct field must contribute its real slot width to # the enclosing struct's slotsize — pure cs==ww .s byte-id (frame size). $(BIN)/test_struct_tuple_field_slot: test/wcc/930_struct_tuple_field_slot.c \ diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 0804e5ab..37daceb8 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -1887,11 +1887,26 @@ cexpr(Checker *c, Node *n) * (so the caller can match on it). cgen does the tag remap. * For ! : no propagation, so no subset check. */ Type *succ = tagged_success_type(u); - int has_errors = 0; - for (Tparam *p = u->params; p; p = p->next) - if (tagged_is_error_variant(u, p->type)) { - has_errors = 1; break; - } + int has_errors = 0, nsucc = 0; + for (Tparam *p = u->params; p; p = p->next) { + if (tagged_is_error_variant(u, p->type)) + has_errors = 1; + else + nsucc++; + } + /* F8 interim gate (task #5): try-propagation assumes ONE + * success member end-to-end — succ collapses to the first + * non-error variant and cgen emits a single tag compare, so + * any OTHER success member is silently mistaken for an error + * (? propagates it; ! aborts on it). One class, both ops + * (#133 precedent). Until the honest subset-union result + * typing lands (task #14, harec check.c:2759-2835), reject + * loud. */ + if (nsucc > 1) { + return n->type = err(c, n->pos, + "%s: multi-success union unwired (task #14): bind and match instead", + n->kind == N_TRYPROP ? "?" : "!"); + } if (n->kind == N_TRYPROP && has_errors) { Type *r = c->ret; Type *ru = (r && r->kind == TY_NAMED) ? r->under : r; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index d3444a96..362d8f54 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -10642,6 +10642,7 @@ fn resolvewalk(c: *checker, n: *node) void = { // is examined before the arm bodies install new bindings. if (k == nkind.N_MATCH) { checkmatchexhaust(c, n); }; if (k == nkind.N_TRYPROP) { checktryprop(c, n); }; + if (k == nkind.N_TRYUNW) { checktryprop(c, n); }; if (k == nkind.N_TYPETEST) { checkisas(c, n); }; if (k == nkind.N_TYPEASSERT) { checkisas(c, n); }; if (k == nkind.N_LET) { checkletassign(c, n); }; @@ -14029,9 +14030,30 @@ fn checkmatchexhaust(c: *checker, n: *node) void = { if (n == nil) { return; }; if (n.lhs == nil) { return; }; let st: *node = scruttype(c, n.lhs); + // F9 (task #12): direct `match (expr?)` / `match (expr!)` — cstage + // types the try-result as the success variant and rejects when it + // is not itself a tagged union (check.c "match on non-tagged- + // union"); scruttype's IDENT/DOT-only resolution let the form slip + // through silently (cs≠ww). The reject below is gated on the + // try-form so the lenient-miss contract for other unresolvable + // scrutinees is untouched; a tagged success keeps flowing into the + // normal exhaustiveness walk, matching cstage's accept. + let istry: bool = false; + if (st == nil) { + if (n.lhs.kind == nkind.N_TRYPROP || n.lhs.kind == nkind.N_TRYUNW) { + istry = true; + st = exprtype(c, n.lhs, nil); + }; + }; let u: *node = resolvealias(c, unwrapbang(st)); if (u == nil) { return; }; - if (u.kind != nkind.N_TTAGGED) { return; }; + if (u.kind != nkind.N_TTAGGED) { + if (istry) { + cerr("match on non-tagged-union try-result\n"); + c.errs += 1; + }; + return; + }; // #13: the union's defining module, so a bare body variant can be // matched against a module-qualified cross-module case pattern (and // a foreign-qualifier pattern correctly rejected). See @@ -14610,6 +14632,19 @@ fn checkisas(c: *checker, n: *node) void = { if (n == nil) { return; }; // e is in n.lhs (value), T is in n.rhs (type expr). let st: *node = scruttype(c, n.lhs); + // F9 (task #12): direct `expr? is T` / `expr! is T` — cstage cexpr + // types the ?/! result as the success variant and the non-tagged + // gate below then rejects (check.c "is on non-tagged-union"). + // scruttype only resolves IDENT/DOT, so the direct try-form slipped + // through the lenient-miss contract and wwstage silently ACCEPTED + // (cs≠ww). Resolve the try-result here; a tagged success (named + // union variant) flows on into the variant checks, matching + // cstage's accept. + if (st == nil && n.lhs != nil) { + if (n.lhs.kind == nkind.N_TRYPROP || n.lhs.kind == nkind.N_TRYUNW) { + st = exprtype(c, n.lhs, nil); + }; + }; let u: *node = resolvealias(c, unwrapbang(st)); if (u == nil) { return; }; // #52: enum ↔ int reinterpret (`enum as intT` / `intT as enum`). @@ -14666,8 +14701,10 @@ fn checkisas(c: *checker, n: *node) void = { // // For `expr?`, the operand's error subset must be a subset of the // enclosing fn's return-type variants. Mirrors C check.c. Operand -// is nkind.N_TRYPROP; its lhs is the value-bearing expr; we look at the -// expr's *declared* type for nkind.N_IDENT/nkind.N_CALL cases. +// is nkind.N_TRYPROP or nkind.N_TRYUNW (the F8 cardinality gate covers +// both; the subset walk is ?-only); its lhs is the value-bearing expr; +// we look at the expr's *declared* type for nkind.N_IDENT/nkind.N_CALL +// cases. fn exprtypeoftry(c: *checker, e: *node) *node = { if (e == nil) { return nil; }; @@ -14702,13 +14739,30 @@ fn checktryprop(c: *checker, n: *node) void = { let u: *node = resolvealias(c, unwrapbang(t)); if (u == nil) { return; }; if (u.kind != nkind.N_TTAGGED) { return; }; - // Does the operand have any error variants? + // F8 interim gate (task #5): try-propagation assumes ONE success + // member end-to-end — exprtype collapses to the first non-error + // variant and cgen emits a single tag compare, so any OTHER + // success member is silently mistaken for an error (? propagates + // it; ! aborts on it). One class, both ops (#133 precedent). + // Until the honest subset-union result typing lands (task #14, + // harec check.c:2759-2835), reject loud. Mirrors cstage check.c + // N_TRYPROP/N_TRYUNW. let haserr: bool = false; + let nsucc: int = 0; let v: *node = u.list; for (v != nil) { - if (iserrvariant(c, u, v)) { haserr = true; }; + if (iserrvariant(c, u, v)) { haserr = true; } else { nsucc += 1; }; v = v.next; }; + if (nsucc > 1) { + if (n.kind == nkind.N_TRYPROP) { cerr("?"); } else { cerr("!"); }; + cerr(": multi-success union unwired (task #14): bind and match instead\n"); + c.errs += 1; + return; + }; + // `!` has no propagation, so no error-subset check (mirrors + // cstage's N_TRYPROP-only guard on the subset walk). + if (n.kind != nkind.N_TRYPROP) { return; }; if (!haserr) { return; }; // Enclosing fn must return a tagged union with each operand // error variant present. diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 64c8131e..a5cc1568 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -315,6 +315,7 @@ fn resolvewalk(c: *checker, n: *node) void = { // is examined before the arm bodies install new bindings. if (k == nkind.N_MATCH) { checkmatchexhaust(c, n); }; if (k == nkind.N_TRYPROP) { checktryprop(c, n); }; + if (k == nkind.N_TRYUNW) { checktryprop(c, n); }; if (k == nkind.N_TYPETEST) { checkisas(c, n); }; if (k == nkind.N_TYPEASSERT) { checkisas(c, n); }; if (k == nkind.N_LET) { checkletassign(c, n); }; @@ -3702,9 +3703,30 @@ fn checkmatchexhaust(c: *checker, n: *node) void = { if (n == nil) { return; }; if (n.lhs == nil) { return; }; let st: *node = scruttype(c, n.lhs); + // F9 (task #12): direct `match (expr?)` / `match (expr!)` — cstage + // types the try-result as the success variant and rejects when it + // is not itself a tagged union (check.c "match on non-tagged- + // union"); scruttype's IDENT/DOT-only resolution let the form slip + // through silently (cs≠ww). The reject below is gated on the + // try-form so the lenient-miss contract for other unresolvable + // scrutinees is untouched; a tagged success keeps flowing into the + // normal exhaustiveness walk, matching cstage's accept. + let istry: bool = false; + if (st == nil) { + if (n.lhs.kind == nkind.N_TRYPROP || n.lhs.kind == nkind.N_TRYUNW) { + istry = true; + st = exprtype(c, n.lhs, nil); + }; + }; let u: *node = resolvealias(c, unwrapbang(st)); if (u == nil) { return; }; - if (u.kind != nkind.N_TTAGGED) { return; }; + if (u.kind != nkind.N_TTAGGED) { + if (istry) { + cerr("match on non-tagged-union try-result\n"); + c.errs += 1; + }; + return; + }; // #13: the union's defining module, so a bare body variant can be // matched against a module-qualified cross-module case pattern (and // a foreign-qualifier pattern correctly rejected). See @@ -4283,6 +4305,19 @@ fn checkisas(c: *checker, n: *node) void = { if (n == nil) { return; }; // e is in n.lhs (value), T is in n.rhs (type expr). let st: *node = scruttype(c, n.lhs); + // F9 (task #12): direct `expr? is T` / `expr! is T` — cstage cexpr + // types the ?/! result as the success variant and the non-tagged + // gate below then rejects (check.c "is on non-tagged-union"). + // scruttype only resolves IDENT/DOT, so the direct try-form slipped + // through the lenient-miss contract and wwstage silently ACCEPTED + // (cs≠ww). Resolve the try-result here; a tagged success (named + // union variant) flows on into the variant checks, matching + // cstage's accept. + if (st == nil && n.lhs != nil) { + if (n.lhs.kind == nkind.N_TRYPROP || n.lhs.kind == nkind.N_TRYUNW) { + st = exprtype(c, n.lhs, nil); + }; + }; let u: *node = resolvealias(c, unwrapbang(st)); if (u == nil) { return; }; // #52: enum ↔ int reinterpret (`enum as intT` / `intT as enum`). @@ -4339,8 +4374,10 @@ fn checkisas(c: *checker, n: *node) void = { // // For `expr?`, the operand's error subset must be a subset of the // enclosing fn's return-type variants. Mirrors C check.c. Operand -// is nkind.N_TRYPROP; its lhs is the value-bearing expr; we look at the -// expr's *declared* type for nkind.N_IDENT/nkind.N_CALL cases. +// is nkind.N_TRYPROP or nkind.N_TRYUNW (the F8 cardinality gate covers +// both; the subset walk is ?-only); its lhs is the value-bearing expr; +// we look at the expr's *declared* type for nkind.N_IDENT/nkind.N_CALL +// cases. fn exprtypeoftry(c: *checker, e: *node) *node = { if (e == nil) { return nil; }; @@ -4375,13 +4412,30 @@ fn checktryprop(c: *checker, n: *node) void = { let u: *node = resolvealias(c, unwrapbang(t)); if (u == nil) { return; }; if (u.kind != nkind.N_TTAGGED) { return; }; - // Does the operand have any error variants? + // F8 interim gate (task #5): try-propagation assumes ONE success + // member end-to-end — exprtype collapses to the first non-error + // variant and cgen emits a single tag compare, so any OTHER + // success member is silently mistaken for an error (? propagates + // it; ! aborts on it). One class, both ops (#133 precedent). + // Until the honest subset-union result typing lands (task #14, + // harec check.c:2759-2835), reject loud. Mirrors cstage check.c + // N_TRYPROP/N_TRYUNW. let haserr: bool = false; + let nsucc: int = 0; let v: *node = u.list; for (v != nil) { - if (iserrvariant(c, u, v)) { haserr = true; }; + if (iserrvariant(c, u, v)) { haserr = true; } else { nsucc += 1; }; v = v.next; }; + if (nsucc > 1) { + if (n.kind == nkind.N_TRYPROP) { cerr("?"); } else { cerr("!"); }; + cerr(": multi-success union unwired (task #14): bind and match instead\n"); + c.errs += 1; + return; + }; + // `!` has no propagation, so no error-subset check (mirrors + // cstage's N_TRYPROP-only guard on the subset walk). + if (n.kind != nkind.N_TRYPROP) { return; }; if (!haserr) { return; }; // Enclosing fn must return a tagged union with each operand // error variant present. diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 6aa82e64..f157d3f4 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -10642,6 +10642,7 @@ fn resolvewalk(c: *checker, n: *node) void = { // is examined before the arm bodies install new bindings. if (k == nkind.N_MATCH) { checkmatchexhaust(c, n); }; if (k == nkind.N_TRYPROP) { checktryprop(c, n); }; + if (k == nkind.N_TRYUNW) { checktryprop(c, n); }; if (k == nkind.N_TYPETEST) { checkisas(c, n); }; if (k == nkind.N_TYPEASSERT) { checkisas(c, n); }; if (k == nkind.N_LET) { checkletassign(c, n); }; @@ -14029,9 +14030,30 @@ fn checkmatchexhaust(c: *checker, n: *node) void = { if (n == nil) { return; }; if (n.lhs == nil) { return; }; let st: *node = scruttype(c, n.lhs); + // F9 (task #12): direct `match (expr?)` / `match (expr!)` — cstage + // types the try-result as the success variant and rejects when it + // is not itself a tagged union (check.c "match on non-tagged- + // union"); scruttype's IDENT/DOT-only resolution let the form slip + // through silently (cs≠ww). The reject below is gated on the + // try-form so the lenient-miss contract for other unresolvable + // scrutinees is untouched; a tagged success keeps flowing into the + // normal exhaustiveness walk, matching cstage's accept. + let istry: bool = false; + if (st == nil) { + if (n.lhs.kind == nkind.N_TRYPROP || n.lhs.kind == nkind.N_TRYUNW) { + istry = true; + st = exprtype(c, n.lhs, nil); + }; + }; let u: *node = resolvealias(c, unwrapbang(st)); if (u == nil) { return; }; - if (u.kind != nkind.N_TTAGGED) { return; }; + if (u.kind != nkind.N_TTAGGED) { + if (istry) { + cerr("match on non-tagged-union try-result\n"); + c.errs += 1; + }; + return; + }; // #13: the union's defining module, so a bare body variant can be // matched against a module-qualified cross-module case pattern (and // a foreign-qualifier pattern correctly rejected). See @@ -14610,6 +14632,19 @@ fn checkisas(c: *checker, n: *node) void = { if (n == nil) { return; }; // e is in n.lhs (value), T is in n.rhs (type expr). let st: *node = scruttype(c, n.lhs); + // F9 (task #12): direct `expr? is T` / `expr! is T` — cstage cexpr + // types the ?/! result as the success variant and the non-tagged + // gate below then rejects (check.c "is on non-tagged-union"). + // scruttype only resolves IDENT/DOT, so the direct try-form slipped + // through the lenient-miss contract and wwstage silently ACCEPTED + // (cs≠ww). Resolve the try-result here; a tagged success (named + // union variant) flows on into the variant checks, matching + // cstage's accept. + if (st == nil && n.lhs != nil) { + if (n.lhs.kind == nkind.N_TRYPROP || n.lhs.kind == nkind.N_TRYUNW) { + st = exprtype(c, n.lhs, nil); + }; + }; let u: *node = resolvealias(c, unwrapbang(st)); if (u == nil) { return; }; // #52: enum ↔ int reinterpret (`enum as intT` / `intT as enum`). @@ -14666,8 +14701,10 @@ fn checkisas(c: *checker, n: *node) void = { // // For `expr?`, the operand's error subset must be a subset of the // enclosing fn's return-type variants. Mirrors C check.c. Operand -// is nkind.N_TRYPROP; its lhs is the value-bearing expr; we look at the -// expr's *declared* type for nkind.N_IDENT/nkind.N_CALL cases. +// is nkind.N_TRYPROP or nkind.N_TRYUNW (the F8 cardinality gate covers +// both; the subset walk is ?-only); its lhs is the value-bearing expr; +// we look at the expr's *declared* type for nkind.N_IDENT/nkind.N_CALL +// cases. fn exprtypeoftry(c: *checker, e: *node) *node = { if (e == nil) { return nil; }; @@ -14702,13 +14739,30 @@ fn checktryprop(c: *checker, n: *node) void = { let u: *node = resolvealias(c, unwrapbang(t)); if (u == nil) { return; }; if (u.kind != nkind.N_TTAGGED) { return; }; - // Does the operand have any error variants? + // F8 interim gate (task #5): try-propagation assumes ONE success + // member end-to-end — exprtype collapses to the first non-error + // variant and cgen emits a single tag compare, so any OTHER + // success member is silently mistaken for an error (? propagates + // it; ! aborts on it). One class, both ops (#133 precedent). + // Until the honest subset-union result typing lands (task #14, + // harec check.c:2759-2835), reject loud. Mirrors cstage check.c + // N_TRYPROP/N_TRYUNW. let haserr: bool = false; + let nsucc: int = 0; let v: *node = u.list; for (v != nil) { - if (iserrvariant(c, u, v)) { haserr = true; }; + if (iserrvariant(c, u, v)) { haserr = true; } else { nsucc += 1; }; v = v.next; }; + if (nsucc > 1) { + if (n.kind == nkind.N_TRYPROP) { cerr("?"); } else { cerr("!"); }; + cerr(": multi-success union unwired (task #14): bind and match instead\n"); + c.errs += 1; + return; + }; + // `!` has no propagation, so no error-subset check (mirrors + // cstage's N_TRYPROP-only guard on the subset walk). + if (n.kind != nkind.N_TRYPROP) { return; }; if (!haserr) { return; }; // Enclosing fn must return a tagged union with each operand // error variant present. diff --git a/test/wcc/806_tryprop_multisuccess.c b/test/wcc/806_tryprop_multisuccess.c new file mode 100644 index 00000000..612ec7cb --- /dev/null +++ b/test/wcc/806_tryprop_multisuccess.c @@ -0,0 +1,477 @@ +/* + * 806_tryprop_multisuccess — the `?` operator's interim single-success + * gate (F8, task #5) and the direct try-form is/match reject parity + * (F9, task #12). + * + * F8: ww's `?` assumes ONE success member end-to-end — the checker + * collapses the result to the first non-error variant and cgen emits a + * single tag compare — so `f()?` over (A|B|err) silently PROPAGATED + * the other success member to the caller as if it were an error + * (scratch/fold2b_probes/p11h, exit 21; graduated below as + * reject_success2). Until the honest subset-union result typing lands + * (task #14, harec check.c:2759-2835), BOTH stages loud-reject + * |success| > 1 at the checker; (T|err1|err2) — one success, many + * errors — stays legal (runtime canary: 925_tryprop_tag_remap_run). + * + * F9: cstage types `f()?` as the success variant, so the direct forms + * `f()? is T` / `match (f()?)` hit its non-tagged is/match gates and + * reject — while wwstage's scruttype (IDENT/DOT-only) silently + * ACCEPTED the same files (cs≠ww, gate-blind). Align-richer-DOWN: + * wwstage gains the same verdicts (text differs per per-stage diag + * conventions, asserted exactly per stage below). A success variant + * that is itself a named tagged union must KEEP being accepted + * (accept_nested_tagged — guards the mirror against over-rejecting). + * + * row | shape | want + * ----------------------+--------------------------------------+------ + * reject_success2 | (void|[]capture|nomem)? — p11h | BUILD_FAIL + * reject_success3 | (i32|bool|u64|nomem)? | BUILD_FAIL + * reject_try_is | f()? is i32, success = i32 | BUILD_FAIL + * reject_try_match | match (f()?), success = i32 | BUILD_FAIL + * reject_unw_success2 | (void|[]capture|nomem)! — `!` is the | BUILD_FAIL + * | same class (rob ruling, #133) | + * reject_tryunw_is | f()! is i32 — kind-agnostic sibling | BUILD_FAIL + * reject_callarg | g(f()?) — gate in call-arg position | BUILD_FAIL + * accept_two_member | (i32|nomem)? unwrap runtime | 0 + * accept_unw_multi_error| (i32|e1|e2)! unwrap runtime | 0 + * accept_void_success | (void|nomem)? statement runtime | 0 + * accept_nested_tagged | (ab|nomem)? is i32, ab = (i32|bool) | 0 + * + * BUILD_FAIL rows assert the diagnostic TEXT per stage (a build that + * fails for any other reason is a vacuous reject and fails the row). + * Accept rows also assert cstage/wwstage asm byte-id. + */ +#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; +} + +/* want == BUILD_FAIL: the row must FAIL to build on both stages AND + * emit the per-stage expected diagnostic on stderr (rule 7 — never a + * silent acceptance). */ +#define BUILD_FAIL (-2147483647 - 1) + +struct row { + const char *label; + const char *src; + int want; + const char *expect_cs; /* BUILD_FAIL: required cstage stderr substring */ + const char *expect_ww; /* BUILD_FAIL: required wwstage stderr substring */ +}; + +#define MULTISUCC_DIAG \ + "?: multi-success union unwired (task #14): bind and match instead" + +static const struct row rows[] = { + /* p11h graduated: pre-gate this BUILT and exited 21 (the []capture + * success wrongly propagated and read back as nomem). */ + { "reject_success2", + "package main;\n" + "type capture = struct { content: str, start: size, end: size };\n" + "fn search2(k: i32) (void | []capture | nomem) = {\n" + "\tif (k == 0) { return; };\n" + "\tlet caps: []capture = [];\n" + "\tappend(caps, capture { content = \"xy\", start = 1, end = 3 });\n" + "\treturn caps;\n" + "};\n" + "fn tb(k: i32) (i32 | nomem) = {\n" + "\tlet r: (void | []capture) = search2(k)?;\n" + "\tif (r is []capture) { return 7; };\n" + "\treturn 8;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet x: (i32 | nomem) = tb(1);\n" + "\tif (x is nomem) { return 21; };\n" + "\tif (x is i32) { return 22; };\n" + "\treturn 23;\n" + "};\n", + BUILD_FAIL, MULTISUCC_DIAG, MULTISUCC_DIAG }, + + { "reject_success3", + "package main;\n" + "fn f(k: i32) (i32 | bool | u64 | nomem) = { return 7; };\n" + "fn g() (i32 | nomem) = {\n" + "\tlet v: i32 = f(1)?;\n" + "\treturn v;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet x: (i32 | nomem) = g();\n" + "\tif (x is i32) { return 0; };\n" + "\treturn 1;\n" + "};\n", + BUILD_FAIL, MULTISUCC_DIAG, MULTISUCC_DIAG }, + + /* F9 graduation rows: the ?-result is bare i32, so `is`/`match` + * over it must reject on BOTH stages (pre-fix wwstage accepted). */ + { "reject_try_is", + "package main;\n" + "fn f(k: i32) (i32 | nomem) = { return 7; };\n" + "fn g() (i32 | nomem) = {\n" + "\tif (f(1)? is i32) { return 1; };\n" + "\treturn 0;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet x: (i32 | nomem) = g();\n" + "\tif (x is i32) { return 0; };\n" + "\treturn 1;\n" + "};\n", + BUILD_FAIL, + "is on non-tagged-union", + "is/as: operand is not a tagged union" }, + + { "reject_try_match", + "package main;\n" + "fn f(k: i32) (i32 | nomem) = { return 7; };\n" + "fn g() (i32 | nomem) = {\n" + "\tmatch (f(1)?) {\n" + "\tcase i32 => return 1;\n" + "\t};\n" + "\treturn 0;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet x: (i32 | nomem) = g();\n" + "\tif (x is i32) { return 0; };\n" + "\treturn 1;\n" + "};\n", + BUILD_FAIL, + "match on non-tagged-union", + "match on non-tagged-union try-result" }, + + /* `!` shares the single-success collapse — one class, both ops + * (rob ruling, #133 precedent): pre-gate this BUILT on BOTH stages + * and silently aborted-on-success at runtime (probe q_card2_unw, + * graduated). */ + { "reject_unw_success2", + "package main;\n" + "type capture = struct { content: str, start: size, end: size };\n" + "fn search2(k: i32) (void | []capture | nomem) = {\n" + "\tif (k == 0) { return; };\n" + "\tlet caps: []capture = [];\n" + "\tappend(caps, capture { content = \"xy\", start = 1, end = 3 });\n" + "\treturn caps;\n" + "};\n" + "fn tb(k: i32) i32 = {\n" + "\tlet r: (void | []capture) = search2(k)!;\n" + "\tif (r is []capture) { return 7; };\n" + "\treturn 8;\n" + "};\n" + "export fn main() i32 = { return tb(1); };\n", + BUILD_FAIL, + "!: multi-success union unwired (task #14): bind and match instead", + "!: multi-success union unwired (task #14): bind and match instead" }, + + /* cstage's reject derives from the typed result, not the operator + * kind — `!` collapses identically, so its direct form must match + * verdicts too (pre-fix wwstage built this and misbehaved). */ + { "reject_tryunw_is", + "package main;\n" + "fn f(k: i32) (i32 | nomem) = { return 7; };\n" + "fn g() i32 = {\n" + "\tif (f(1)! is i32) { return 1; };\n" + "\treturn 0;\n" + "};\n" + "export fn main() i32 = { return g() - 1; };\n", + BUILD_FAIL, + "is on non-tagged-union", + "is/as: operand is not a tagged union" }, + + /* `?` nested in a CALL-ARG: the gate keys on the operand's type at + * the checker expression walk, so position must not matter — guards + * against a walker path that types call args without visiting the + * try node. */ + { "reject_callarg", + "package main;\n" + "fn f(k: i32) (i32 | bool | nomem) = { return 7; };\n" + "fn g(v: i32) i32 = { return v; };\n" + "fn h() (i32 | nomem) = {\n" + "\treturn g(f(1)?);\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet x: (i32 | nomem) = h();\n" + "\tif (x is i32) { return 0; };\n" + "\treturn 1;\n" + "};\n", + BUILD_FAIL, MULTISUCC_DIAG, MULTISUCC_DIAG }, + + /* |success| == 1: the gate must not fire and the unwrap must keep + * working at runtime. The 3-member single-success shape + * (i32|e1|e2) is pinned by 925_tryprop_tag_remap_run. */ + { "accept_two_member", + "package main;\n" + "fn f(k: i32) (i32 | nomem) = { return 7; };\n" + "fn g() (i32 | nomem) = {\n" + "\tlet v: i32 = f(1)?;\n" + "\treturn v + 1;\n" + "};\n" + "export fn main() i32 = {\n" + "\tmatch (g()) {\n" + "\tcase let v: i32 => return v - 8;\n" + "\tcase nomem => return 2;\n" + "\t};\n" + "\treturn 3;\n" + "};\n", + 0, NULL, NULL }, + + /* |success| == 1 with MULTIPLE errors under `!`: the gate must not + * fire and the success unwrap must keep working at runtime (the + * `!` twin of 925's ? canary shape). */ + { "accept_unw_multi_error", + "package main;\n" + "type e1 = !void;\n" + "type e2 = !void;\n" + "fn g(which: i32) (i32 | e1 | e2) = {\n" + "\tif (which == 1) { return e1{}; };\n" + "\tif (which == 2) { return e2{}; };\n" + "\treturn 100;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet v: i32 = g(0)!;\n" + "\treturn v - 100;\n" + "};\n", + 0, NULL, NULL }, + + /* VOID success: (void|nomem)? as an expression-statement is the + * dominant lib/ shape (io writes etc.) — void counts as the one + * success member, nsucc == 1, the gate must not fire. */ + { "accept_void_success", + "package main;\n" + "fn f(k: i32) (void | nomem) = {\n" + "\treturn;\n" + "};\n" + "fn g() (i32 | nomem) = {\n" + "\tf(1)?;\n" + "\treturn 5;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet x: (i32 | nomem) = g();\n" + "\tmatch (x) {\n" + "\tcase let v: i32 => return v - 5;\n" + "\tcase nomem => return 2;\n" + "\t};\n" + "\treturn 3;\n" + "};\n", + 0, NULL, NULL }, + + /* Success variant is itself a NAMED tagged union: `f()? is i32` + * is `is` over (i32|bool) — tagged — and cstage ACCEPTS. The + * wwstage F9 mirror must key on the resolved success type, not + * on the try syntax, or this over-rejects. */ + { "accept_nested_tagged", + "package main;\n" + "type ab = (i32 | bool);\n" + "fn f(k: i32) (ab | nomem) = {\n" + "\tlet v: ab = 7;\n" + "\treturn v;\n" + "};\n" + "fn g() (i32 | nomem) = {\n" + "\tif (f(1)? is i32) { return 0; };\n" + "\treturn 1;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet x: (i32 | nomem) = g();\n" + "\tmatch (x) {\n" + "\tcase let v: i32 => return v;\n" + "\tcase nomem => return 2;\n" + "\t};\n" + "\treturn 3;\n" + "};\n", + 0, NULL, NULL }, +}; + +/* errlog_has — the build-failure stderr must carry the row's expected + * diagnostic; any other failure (parse error, crash) is a vacuous + * reject and must not pass. */ +static int +errlog_has(const char *path, const char *needle) +{ + FILE *f = fopen(path, "rb"); + if (!f) return 0; + char buf[8192]; + size_t got = fread(buf, 1, sizeof buf - 1, f); + fclose(f); + buf[got] = '\0'; + return strstr(buf, needle) != NULL; +} + +static int +run_driver(const char *driver, const char *expect, const struct row *r, int i) +{ + char src[64], tmpdir[64], errlog[80], cmd[1200]; + snprintf(src, sizeof src, "/tmp/tpms_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/tpms_%d_d_%d", getpid(), i); + snprintf(errlog, sizeof errlog, "%s.err", src); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>%s", + tmpdir, driver, src, errlog); + if (runwait(cmd) != 0) { + int rc = -1; + if (r->want != BUILD_FAIL) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + } else if (expect && !errlog_has(errlog, expect)) { + fprintf(stderr, "row[%s]: %s build failed without " + "expected diagnostic \"%s\"\n", + r->label, driver, expect); + rc = -3; /* failed, but for the wrong reason */ + } + unlink(src); unlink(errlog); rmdir(tmpdir); + return rc; + } + + 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 = runwait(outbin); + + unlink(src); unlink(errlog); unlink(outbin); rmdir(tmpdir); + return got; +} + +/* asm_byte_identical — generate .s via cstage's w6c and wwstage's + * w6c_ww and diff (rule 10 on the accept rows). */ +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[64], cs[64], ws[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/tpms_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/tpms_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/tpms_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c errored\n", r->label); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); + unlink(src); unlink(cs); + return -1; + } + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + int rc = 0; + if (!fc || !fw) { + rc = -1; + } else { + for (;;) { + int a = fgetc(fc); + int b = fgetc(fw); + if (a != b) { rc = -1; break; } + if (a == EOF) break; + } + } + if (fc) fclose(fc); + if (fw) fclose(fw); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + unlink(src); unlink(cs); unlink(ws); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[2080]; + 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[2120]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[2120]; + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { + const char *name; + const char *path; + int is_ww; + int gated_on_existence; + } drivers[] = { + { "cstage", cdrv, 0, 0 }, + { "wwstage", wdrv, 1, 1 }, + { NULL, NULL, 0, 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_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, + "tryprop_multisuccess: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + const char *expect = drivers[d].is_ww + ? rows[i].expect_ww : rows[i].expect_cs; + int got = run_driver(drivers[d].path, expect, + &rows[i], i); + total++; + int bad = rows[i].want == BUILD_FAIL + ? (got != -1) : (got != rows[i].want); + if (bad) { + fprintf(stderr, + "tryprop_multisuccess[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + if (rows[i].want == BUILD_FAIL) + continue; + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, + "tryprop_multisuccess: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("tryprop_multisuccess: %d fixtures passed\n", total); + return 0; +}