wcc+w6c_ww: loud-gate try-propagation over multi-success unions (F8/F9 interim)

? and ! assume ONE success member end-to-end: the checker collapses
the result to the first non-error variant (check.c tagged_success_type
/ check.ww exprtype) and cgen emits a single tag compare, so any other
success member is silently mistaken for an error — ? propagates it to
the caller (p11h: []capture read back as nomem, exit 21), ! aborts on
it. Until the honest subset-union result typing lands (task #14, harec
check.c:2759-2835), both stages loud-reject |success| > 1 at the
checker choke-points (one per stage), identical diagnostic, both ops
per rob's one-class ruling (#133 precedent). (T|err1|err2) — one
success, many errors — stays legal (925 canary + new accept rows).

F9 rides along (task #12): wwstage scruttype only resolves IDENT/DOT,
so the direct forms f()? is T / match(f()?) / f()! is T slipped its
lenient-miss contract and were silently ACCEPTED where cstage rejects
(cs!=ww, gate-blind). checkisas/checkmatchexhaust now resolve the
try-result via exprtype, keyed on the RESOLVED success type — a named
tagged success ((ab|nomem)? is i32) keeps being accepted, matching
cstage's verdict empirically.

test/wcc/806: 11 rows x dual driver + byte-id accepts (26 fixtures);
reject rows pin exact per-stage diagnostic text; p11h + q_card2_unw
graduated to rejects; call-arg-position reject + void-success accept
pin position-independence and the dominant lib/ (void|err)? shape.
Tasks #5 + #12; #14 lifts both gates together.
This commit is contained in:
2026-06-04 10:10:35 +09:00
parent cfc2985c61
commit 48df04a8ca
6 changed files with 686 additions and 20 deletions

View File

@@ -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 \

View File

@@ -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;

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
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;
}