test: group examples into per-category folders

Move examples/*.sx and their expected/ snapshots into per-category
subfolders (examples/<category>/...). Folder = leading filename token,
with ffi-objc/ffi-jni kept whole; filenames are unchanged. The corpus
runner and LSP sweep now discover each category's expected/ dir, while
issues/ stays flat. Example 1058's repo-root-relative companion import
is made file-relative. Path strings embedded in 164 snapshots were
regenerated (path-only changes). Test-layout docs in CLAUDE.md updated.
This commit is contained in:
agra
2026-06-21 14:41:34 +03:00
parent 6d1409bc1f
commit 66bdc70bf1
3357 changed files with 456 additions and 363 deletions

View File

@@ -0,0 +1,24 @@
// Error-set declarations + `error.X` tag values + enum-like `==` typing
// (ERR step E1.1). A declared `error { ... }` set is a real type with a u32
// runtime layout; `error.X` is its tag value — the named set when context
// provides one (membership-checked), else the raw global u32 id. Tags compare
// with an `error.X` literal or another error-set value. The rejections live in
// `examples/218-error-set-typing.sx`.
#import "modules/std.sx";
ParseErr :: error { BadDigit, Overflow, Empty }
main :: () -> i32 {
c : ParseErr = error.BadDigit;
d : ParseErr = error.Overflow;
r : i32 = 0;
if c == error.BadDigit { r = r + 1; } // true -> +1
if c == error.Overflow { r = r + 2; } // false
if c == d { r = r + 4; } // false (BadDigit != Overflow)
if d == error.Overflow { r = r + 8; } // true -> +8
tag : u32 = error.Empty; // u32 context -> raw global tag id
if tag != 0 { r = r + 16; } // tag ids are >= 1 -> +16
print("error-set result: {}\n", r); // -> 25
return r;
}

View File

@@ -0,0 +1,16 @@
// Error-set value + `==` typing rejections (ERR step E1.1):
// - an `error.X` literal must name a tag that is in the destination set,
// - an error-set value compares only with an `error.X` tag or another
// error-set value; comparing to a raw integer is a type error
// (coerce with `xx` to compare the raw id).
// The positive cases live in `examples/217-error-sets.sx`.
#import "modules/std.sx";
ParseErr :: error { BadDigit, Overflow }
main :: () -> i32 {
c : ParseErr = error.NotInSet; // error: NotInSet not in ParseErr
if c == 42 { return 1; } // error: error-set value vs raw integer
return 0;
}

View File

@@ -0,0 +1,26 @@
// First runnable `raise` (ERR step E1.3). A `-> !Named` (pure failable)
// function terminates via the error channel with `raise error.X`; a plain
// `return;` is the success exit (error slot 0). The caller binds the result
// and inspects it with the enum-like `==`. The value-carrying `-> (T, !)`
// shape lands with the error-channel tuple ABI in ERR phase E2.
#import "modules/std.sx";
ParseErr :: error { BadDigit, Overflow, Empty }
// Pure failable: raises on bad input, otherwise succeeds (error slot 0).
check :: (n: i32) -> !ParseErr {
if n < 0 { raise error.BadDigit; }
return; // success — no error
}
main :: () -> i32 {
good := check(7); // success path -> no error
bad := check(-1); // raise path -> BadDigit
r : i32 = 0;
if bad == error.BadDigit { r = r + 8; } // true -> +8
if good == error.BadDigit { r = r + 1; } // false (success = no error)
if bad == error.Overflow { r = r + 2; } // false (raised BadDigit)
print("raise result: {}\n", r); // -> 8
return r;
}

View File

@@ -0,0 +1,32 @@
// `raise` rejections (ERR step E1.3):
// - `raise` is only valid inside a failable function,
// - a literal `raise error.X` must name a tag in the function's set,
// - a variable `raise e` must carry a set that is a subset of the
// function's set.
// The positive case lives in `examples/219-raise.sx`. Parse-time rejections
// (`raise` in expression position / inside `defer` / `onfail`) are covered by
// the inline parser tests.
#import "modules/std.sx";
ParseErr :: error { BadDigit, Overflow }
OtherErr :: error { Weird }
// Literal tag not in the declared set.
bad_tag :: () -> !ParseErr {
raise error.NotInSet; // error: NotInSet not in ParseErr
}
// Variable whose error set is not a subset of the function's set.
makes_other :: () -> !OtherErr { return; }
relay :: () -> !ParseErr {
e := makes_other(); // e : OtherErr
raise e; // error: OtherErr not subset of ParseErr
}
main :: () -> i32 {
x := bad_tag(); // force bad_tag to lower
y := relay(); // force relay to lower
raise error.BadDigit; // error: main (-> i32) is not failable
return 0;
}

View File

@@ -0,0 +1,32 @@
// First runnable `try` (ERR step E1.4a). The STANDALONE form: a failable
// expression whose failure propagates to the enclosing function's error
// return (Zig-style). `outer` calls `try inner(n)` — on `inner`'s failure
// `outer` returns that error; on success it continues. Both are pure
// failable (`-> !E`). The error-channel tuple ABI for value-carrying
// `-> (T, !)` and `try` in an `or` chain land in ERR E1.4b/E2.
#import "modules/std.sx";
E :: error { Bad, Worse }
inner :: (n: i32) -> !E {
if n < 0 { raise error.Bad; }
return; // success — no error
}
// Propagates inner's error (standalone `try`, target = function return).
outer :: (n: i32) -> !E {
try inner(n);
return;
}
main :: () -> i32 {
bad := outer(-1); // inner raises Bad -> outer propagates
good := outer(7); // inner succeeds -> outer succeeds
r : i32 = 0;
if bad == error.Bad { r = r + 5; } // true -> +5
if good == error.Bad { r = r + 1; } // false (success = no error)
if bad == error.Worse { r = r + 2; } // false (propagated Bad)
print("try result: {}\n", r); // -> 5
return r;
}

View File

@@ -0,0 +1,41 @@
// `try` rejections (ERR step E1.4a):
// - `try` is only valid inside a failable function,
// - the operand must be failable (the sole failable-operand check —
// the parser imposes none),
// - propagating a `try` whose callee's error set is not a subset of the
// caller's named set is rejected (widening at a function-propagation site).
// The positive case lives in `examples/221-try.sx`.
#import "modules/std.sx";
A :: error { Xa }
B :: error { Yb }
ga :: () -> !A { return; }
gb :: () -> !B { return; }
plain :: () -> i32 { return 0; }
// `try` in a non-failable function.
bad_ctx :: () -> i32 {
try ga(); // error: `try` outside a failable function
return 0;
}
// `try` on a non-failable operand.
bad_operand :: () -> !A {
try plain(); // error: operand has type i32 (not failable)
return;
}
// Callee's set (B = {Yb}) is not a subset of the caller's set (A = {Xa}).
widen :: () -> !A {
try gb(); // error: Yb not in caller's error set A
return;
}
main :: () -> i32 {
a := bad_ctx(); // force bad_ctx to lower
b := bad_operand(); // force bad_operand to lower
c := widen(); // force widen to lower
return 0;
}

View File

@@ -0,0 +1,40 @@
// Whole-program inferred error sets (ERR step E1.4b). A bare `-> !` function's
// error set is INFERRED: the union of the tags it raises directly plus the
// sets of the failable functions it `try`s, converged across the whole call
// graph by a fix-point pass. Here `leaf` raises {Foo}; `mid` try-propagates
// leaf AND raises Bar, so `mid` converges to {Foo, Bar}; the named caller
// `run :: -> !A` then type-checks because mid's converged set is a subset of
// A. The rejection (a converged tag NOT in the caller's set) lives in
// `examples/224-inferred-widening-reject.sx`.
#import "modules/std.sx";
A :: error { Foo, Bar }
leaf :: (n: i32) -> ! {
if n < 0 { raise error.Foo; }
return;
}
// Inferred set converges to {Foo, Bar}: {Foo} absorbed from `try leaf` plus
// the directly-raised Bar.
mid :: (n: i32) -> ! {
try leaf(n);
if n == 100 { raise error.Bar; }
return;
}
// Named caller: mid's converged {Foo, Bar} is a subset of A -> widening OK.
run :: (n: i32) -> !A {
try mid(n);
return;
}
main :: () -> i32 {
e := run(-1); // leaf raises Foo -> propagates out
r : i32 = 0;
if e == error.Foo { r = r + 7; } // true -> +7
if e == error.Bar { r = r + 1; } // false (Foo escaped, not Bar)
print("inferred result: {}\n", r); // -> 7
return r;
}

View File

@@ -0,0 +1,30 @@
// Inferred-set widening rejection (ERR step E1.4b). When a named caller
// (`-> !A`) `try`s a bare-`!` callee, the callee's WHOLE-PROGRAM-CONVERGED
// inferred set must be a subset of A. Before the SCC pass this was a
// false-negative (the bare-`!` placeholder was empty, so the check trivially
// passed); now the converged tags are checked. `deep`'s converged set is
// {Foo} (raised transitively through `via`), which is not in A = {Bar}.
// The positive case lives in `examples/223-inferred-error-sets.sx`.
#import "modules/std.sx";
A :: error { Bar }
deep :: () -> ! {
raise error.Foo; // deep's inferred set = {Foo}
}
via :: () -> ! {
try deep(); // via absorbs {Foo}
return;
}
caller :: () -> !A {
try via(); // error: Foo (via's converged set) not in A
return;
}
main :: () -> i32 {
e := caller();
return 0;
}

View File

@@ -0,0 +1,40 @@
// Regression for issue 0057: a match (`if subject == { case ... }`) whose arms
// ALL diverge (each `return`s) used to fail LLVM verification (a `void` phi +
// "terminator in the middle of a basic block") — lowerMatch emitted a
// value-merge phi and a fallback `const` into arm blocks that had already
// terminated. Now a fully-diverging match produces no merge phi, and a mixed
// match (some arms diverge, some yield values) materializes the merge only
// from the value-producing arms.
#import "modules/std.sx";
// All arms diverge — the match is `noreturn`, no merge phi.
classify :: (n: i32) -> i32 {
if n == {
case 0: return 10;
case 1: return 20;
else: return 90;
}
return 0; // unreachable
}
// Mixed: value arms + a diverging arm.
pick :: (n: i32) -> i32 {
v := if n == {
case 0: 1;
case 1: return 100; // diverging arm — no fallback const after its `ret`
else: 3;
};
return v + 5;
}
main :: () -> i32 {
r : i32 = 0;
r = r + classify(0); // 10
r = r + classify(1); // 20
r = r + classify(7); // 90
r = r + pick(0); // 1 + 5 = 6
r = r + pick(9); // 3 + 5 = 8 (else arm)
print("match result: {}\n", r); // 10+20+90+6+8 = 134
return r;
}

View File

@@ -0,0 +1,64 @@
// `catch` on a pure-failable LHS (ERR step E1.5). `expr catch [e] BODY`
// consumes the error inline: on failure it binds the tag to `e` (optional)
// and runs BODY; on success the result is void (a `-> !` LHS has no success
// value). BODY may diverge (`return` / `raise` — typed `noreturn`, E1.4c) or
// fall through. `catch` needs no failable *enclosing* function — it handles
// the error locally. All four body forms appear below: block, no-binding
// block, match-body (`== { case ... }`), and the selective handle + re-raise
// pattern. Value-carrying `-> (T, !)` catch (binding the success value) lands
// with the tuple ABI in E2.
#import "modules/std.sx";
E :: error { Bad, Empty }
must :: (n: i32) -> !E {
if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; }
return;
}
// Diverging body — returns from `classify` on error.
classify :: (n: i32) -> i32 {
must(n) catch (e) {
if e == error.Bad { return 1; }
if e == error.Empty { return 2; }
return 9;
};
return 0; // must(n) succeeded
}
// Match-body form — sugar for `catch (e) { if e == { case ... } }`.
mclassify :: (n: i32) -> i32 {
must(n) catch (e) == {
case .Bad: return 11;
case .Empty: return 22;
else: return 99;
};
return 0;
}
// Selective handle + re-raise (failable enclosing fn; `raise e` is the
// variable form). Swallows Bad → success; re-raises everything else.
handle_some :: (n: i32) -> !E {
must(n) catch (e) {
if e == error.Bad { return; } // swallow → success
raise e; // re-raise the rest
};
return;
}
main :: () -> i32 {
r : i32 = 0;
must(-1) catch (e) { if e == error.Bad { r = r + 1; } }; // Bad → +1
must(5) catch { r = r + 100; }; // success → body skipped
r = r + classify(0); // Empty → 2
r = r + classify(8); // success → 0
he := handle_some(0); // Empty re-raised
if he == error.Empty { r = r + 4; } // +4
hb := handle_some(-1); // Bad swallowed → success
if hb == error.Bad { r = r + 50; } // not taken
r = r + mclassify(-1); // Bad → 11
print("catch result: {}\n", r); // 1+2+4+11 = 18
return r;
}

View File

@@ -0,0 +1,13 @@
// `catch` rejection (ERR step E1.5): the operand must be failable. Unlike
// `try` / `raise`, `catch` needs no failable enclosing function — it consumes
// the error locally — so the only stable rejection is a non-failable operand.
// The positive cases live in `examples/226-catch.sx`.
#import "modules/std.sx";
plain :: () -> i32 { return 0; }
main :: () -> i32 {
plain() catch (e) { return 1; }; // error: operand has type i32 (not failable)
return 0;
}

View File

@@ -0,0 +1,36 @@
// Value-carrying failable functions (ERR step E2.1a — the producer side of the
// error-channel tuple ABI). A `-> (T, !E)` function returns EITHER a value OR
// an error: `return v;` yields the success tuple `{v, 0}` (the compiler appends
// the no-error slot), and `raise error.X` yields `{undef, tag}` (value slot
// undefined, error slot = the tag). Today the result is consumed by
// destructuring `v, err := f()` (which extracts both slots); the value-carrying
// `try` / `catch` consumers land in E2.1b.
#import "modules/std.sx";
E :: error { Bad, Empty }
parse :: (n: i32) -> (i32, !E) {
if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; }
return n * 10; // success → {n*10, 0}
}
main :: () -> i32 {
r : i32 = 0;
// The value slot is live only where the error is proven absent (ERR E1.8):
// read `v1` under an `if !e1` guard, not after a bare tag-compare.
v1, e1 := parse(5); // success → v1 = 50, e1 = no error
if !e1 { r = r + v1; } // success → +50
v2, e2 := parse(-1); // Bad
if e2 == error.Bad { r = r + 7; } // true → +7
if e2 == error.Empty { r = r + 200; } // false
v3, e3 := parse(0); // Empty
if e3 == error.Empty { r = r + 3; } // true → +3
print("value-failable result: {}\n", r); // 50 + 7 + 3 = 60
return r;
}

View File

@@ -0,0 +1,60 @@
// Consuming value-carrying failables with `try` and `catch` (ERR step E2.1b —
// the consumer side of the error-channel tuple ABI). `try f()` on a
// `-> (T, !E)` callee binds the value slot on success and propagates the error
// on failure (a pure-failable caller returns the tag; a value-carrying caller
// returns `{undef, tag}`). `f() catch (e) BODY` yields the value slot on success
// or the handler body's value on failure, merged through a block parameter.
// The producer side is `examples/228-value-failable.sx`.
#import "modules/std.sx";
E :: error { Bad, Empty }
parse :: (n: i32) -> (i32, !E) {
if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; }
return n * 2;
}
// value-carrying `try` in a value-carrying caller — propagates {undef, tag}.
inc :: (n: i32) -> (i32, !E) {
v := try parse(n);
return v + 1;
}
// value-carrying `try` in a pure-failable caller — propagates the tag.
relay :: (n: i32) -> !E {
v := try parse(n);
if v < 0 { raise error.Bad; }
return;
}
// value-carrying `catch`, bare-expression fallback.
safe :: (n: i32) -> i32 {
return parse(n) catch (e) 0;
}
// value-carrying `catch`, match-body value.
classify :: (n: i32) -> i32 {
return parse(n) catch (e) == {
case .Bad: 1;
case .Empty: 2;
else: 3
};
}
main :: () -> i32 {
r : i32 = 0;
a, ea := inc(5); // parse(5)=10 → v=10 → 11
if !ea { r = r + a; } // success → +11 (value live only when proven ok)
b, eb := inc(-1); // parse(-1)=Bad → propagate {undef, Bad}
if eb == error.Bad { r = r + 4; } // true → +4
er := relay(3); // parse(3)=6 ok → relay ok
if er == error.Bad { r = r + 50; } // false
r = r + safe(7); // parse(7)=14 → +14
r = r + safe(-1); // Bad → catch → 0
r = r + classify(-1); // Bad → 1
r = r + classify(0); // Empty → 2
print("consume result: {}\n", r); // 11+4+14+0+1+2 = 32
return r;
}

View File

@@ -0,0 +1,19 @@
// Value-carrying `catch` rejection (ERR step E2.1b): when the failable LHS
// carries a value, a non-diverging catch handler must produce a value of the
// success type — a value-less (void) body is a type error (otherwise the
// success and error paths couldn't merge to one value). Diverge instead
// (`return` / `raise`) or yield a value. Positives: `examples/229-value-failable-consume.sx`.
#import "modules/std.sx";
E :: error { Bad }
parse :: (n: i32) -> (i32, !E) {
if n < 0 { raise error.Bad; }
return n;
}
main :: () -> i32 {
x := parse(-1) catch (e) { print("oops\n") }; // error: body yields no value
return x;
}

View File

@@ -0,0 +1,25 @@
// Failable `or` value-terminator (ERR step E2.4a). `lhs or value` where `lhs`
// is a value-carrying failable (`-> (T, !E)`): on success the result is the
// LHS value; on failure the LHS error is discarded and the result is the
// terminator value. The whole expression is non-failable (type T). The chain
// form (`try a or try b`) needs fallback-target routing and lands in E2.4b.
// Rejections: `examples/232-failable-or-reject.sx`.
#import "modules/std.sx";
E :: error { Bad, Empty }
parse :: (n: i32) -> (i32, !E) {
if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; }
return n * 2;
}
main :: () -> i32 {
a := parse(5) or 0; // success → 10
b := parse(-1) or 99; // Bad → 99 (terminator)
c := parse(0) or 7; // Empty → 7 (terminator)
r := a + b + c; // 10 + 99 + 7 = 116
print("or result: {}\n", r);
return r;
}

View File

@@ -0,0 +1,19 @@
// Failable `or` rejection (ERR step E2.4a): the value-terminator form
// (`lhs or value`) requires a value-carrying failable LHS — a pure failable
// (`-> !`) has no success value to fall back to, so `or value` is rejected
// (use `catch` to absorb the error). The positive cases live in
// `examples/231-failable-or.sx`.
#import "modules/std.sx";
E :: error { Bad }
must :: (n: i32) -> !E {
if n < 0 { raise error.Bad; }
return;
}
main :: () -> i32 {
x := must(-1) or 0; // error: `-> !` has no success value to fall back to
return 0;
}

View File

@@ -0,0 +1,42 @@
// `onfail` — cleanup that runs only when an error LEAVES the enclosing block
// (ERR step E1.7). Unlike `defer` (which runs on every exit), `onfail` fires
// on an error exit — a `raise` or a propagating `try` — and is skipped on
// success. On an error exit `defer` and `onfail` run interleaved in reverse
// declaration order. `onfail (e) { … }` binds the in-flight error tag.
// (Per-attempt-`try` gating and `or`-chain absorption refine this in E2.4b.)
#import "modules/std.sx";
E :: error { Bad }
inner :: (n: i32) -> !E {
if n < 0 { raise error.Bad; }
return;
}
// defer + onfail interleave on the error path; only defers on success.
run :: (n: i32) -> !E {
defer print("defer A\n");
onfail print("onfail B\n");
defer print("defer C\n");
try inner(n); // n<0 → propagates → onfail fires
return;
}
// `onfail e` binds the tag.
classify :: (n: i32) -> !E {
onfail (e) { if e == error.Bad { print("cleanup: bad\n"); } }
if n < 0 { raise error.Bad; }
return;
}
main :: () -> i32 {
print("[fail]\n");
a := run(-1); // error → defer C, onfail B, defer A
print("[ok]\n");
b := run(7); // success → defer C, defer A (no onfail)
print("[bound]\n");
c := classify(-1); // onfail binding sees Bad
print("[done]\n");
return 0;
}

View File

@@ -0,0 +1,15 @@
// `onfail` rejection (ERR step E1.7): `onfail` is only valid inside a failable
// function. A non-failable function never error-exits, so an `onfail` could
// never fire — use `defer` for unconditional cleanup. The positive cases live
// in `examples/233-onfail.sx`.
#import "modules/std.sx";
non_failable :: () -> i32 {
onfail print("never fires\n"); // error: onfail outside a failable function
return 0;
}
main :: () -> i32 {
return non_failable();
}

View File

@@ -0,0 +1,74 @@
// Multi-value value-carrying failables (ERR — the multi-value error-channel
// ABI). A `-> (T1, T2, !E)` function returns EITHER a value-tuple OR an error:
// `return (a, b)` yields the success tuple `{a, b, 0}` (the compiler appends the
// no-error slot) and `raise error.X` yields `{undef, undef, tag}`. Every consumer
// generalizes from the single-value shape: a destructure binds every slot
// INCLUDING the error (dropping it is the spec'd discard error — bind it and
// inspect); `try` binds the value-tuple on success and propagates `{undef..., tag}`
// on failure; `catch` / `or` absorb the failure and merge the value-tuple or the
// handler/terminator value. Single-value `-> (T, !E)` is examples/228-231.
#import "modules/std.sx";
E :: error { Bad, Empty }
parse :: (n: i32) -> (i32, i32, !E) {
if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; }
return (n * 2, n + 1); // success → {n*2, n+1, 0}
}
// Multi-value `try` in a multi-value caller — propagates {undef, undef, tag}.
inc :: (n: i32) -> (i32, i32, !E) {
v, b := try parse(n);
return (v + 1, b + 1);
}
// Multi-value `catch`, bare-expression tuple fallback (absorbs the failure).
safe :: (n: i32) -> i32 {
v, b := parse(n) catch (e) (40, 50);
return v + b;
}
// Multi-value `catch` match-body — per-tag dispatch, each arm a value-tuple.
classify :: (n: i32) -> i32 {
v, b := parse(n) catch (e) == {
case .Bad: (1, 1);
case .Empty: (2, 2);
else: (9, 9);
};
return v + b;
}
// Multi-value `or (tuple)` value-terminator (absorbs the failure).
ortest :: (n: i32) -> i32 {
v, b := parse(n) or (7, 8);
return v + b;
}
main :: () -> i32 {
r : i32 = 0;
// Destructure binds EVERY slot including the error tag (e1 / e2 / e3) —
// the error is treated, never dropped.
v1, b1, e1 := parse(5); // success → (10, 6, no-error)
if !e1 { r = r + v1 + b1; } // success → +16 (slots live only when proven ok)
v2, b2, e2 := parse(-1); // Bad → {undef, undef, Bad}
if e2 == error.Bad { r = r + 4; } // +4
a, c, ea := inc(5); // parse(5)=(10,6) → (11, 7, no-error)
if !ea { r = r + a + c; } // success → +18
a2, c2, e3 := inc(-1); // try parse(-1)=Bad → propagate {undef, undef, Bad}
if e3 == error.Bad { r = r + 5; } // +5
r = r + safe(5); // (10, 6) → 16
r = r + safe(-1); // Bad → catch → (40, 50) → 90
r = r + classify(-1); // Bad → match-body → (1, 1) → 2
r = r + classify(0); // Empty → match-body → (2, 2) → 4
r = r + ortest(0); // Empty → or → (7, 8) → 15
print("multi-value result: {}\n", r); // 16+4+18+5+16+90+2+4+15 = 170
return r;
}

View File

@@ -0,0 +1,30 @@
// Failable error-slot discard rejection (ERR step E1.8 — discard slice). The
// error slot of a value-carrying failable cannot be dropped on a bare
// destructure: it must be bound (`v, err := …`) and handled, or the failure
// routed through `try` / `catch` / `or value` (all of which strip the error
// channel, so they don't reach this check). Two rejected shapes here:
// (1) omitting the error slot entirely (fewer names than slots), and
// (2) binding it to `_`.
// This file is expected to FAIL compilation (exit 1).
//
// Run: ./zig-out/bin/sx run examples/236-failable-discard-reject.sx
#import "modules/std.sx";
E :: error { Bad, Empty }
pair :: (n: i32) -> (i32, i32, !E) {
if n < 0 { raise error.Bad; }
return (n, n + 1);
}
parse :: (n: i32) -> (i32, !E) {
if n < 0 { raise error.Bad; }
return n * 2;
}
main :: () -> i32 {
a, b := pair(5); // ERROR: error slot omitted (3 slots, 2 names)
v, _ := parse(5); // ERROR: error slot discarded with `_`
return a + b + v;
}

View File

@@ -0,0 +1,26 @@
// Cleanup-body control-flow restrictions (ERR step E1.7 follow-up). A `defer`
// or `onfail` body runs while the block/function is already exiting, so it has
// no target to transfer control to: `raise` / `try` / `return` / `break` /
// `continue` are all rejected inside one. The ban is transitive through nested
// `catch` bodies and loops, but NOT through a nested closure (its own function
// boundary). `raise` was already banned (E1.3); this adds the other four.
// This file is expected to FAIL compilation (exit 1).
//
// Run: ./zig-out/bin/sx run examples/237-cleanup-body-restrictions.sx
#import "modules/std.sx";
E :: error { Bad }
g :: () -> !E { return; }
f :: () -> !E {
defer { return; } // ERROR: return in defer body
onfail { try g(); } // ERROR: try in onfail body
defer { for 0..1 (i) { break; } } // ERROR: break in defer body (transitive through loop)
onfail (e) { if e == error.Bad { continue; } } // ERROR: continue in onfail body
try g();
return;
}
main :: () -> i32 { return 0; }

View File

@@ -0,0 +1,14 @@
// Entry-point exit-code truncation (ERR step E4.2, non-failable integer main).
// `main :: () -> T` (integer) exits with the return value truncated to u8 —
// matching C `main` / the OS exit-status byte, so the JIT (`sx run`) and an AOT
// binary agree. Here 1105 & 0xFF == 81, so this program exits 81 (NOT 1, which
// was the old buggy "out of 0..255 range -> failure" behavior).
//
// Run: ./zig-out/bin/sx run examples/238-main-exit-truncation.sx ; echo $? # 81
#import "modules/std.sx";
main :: () -> i32 {
print("returning 1105 -> exit {}\n", 1105 & 0xFF); // 81
return 1105;
}

View File

@@ -0,0 +1,15 @@
// Entry-point signature gate (ERR step E4.2). `main` must take no parameters
// and have one of: void, an integer (POSIX exit code), `-> !` (failable, no
// value), or `-> (int, !)` (failable + integer exit code). Anything else is a
// clean diagnostic — previously `main :: () -> string` SEGFAULTED (the JIT
// calls main as `() -> i32`, so a string return is read as garbage). Accepted
// shapes are exercised elsewhere (238 integer-exit truncation, 244 `-> !`,
// 245 `-> (int, !)`). This file is expected to FAIL compilation (exit 1).
//
// Run: ./zig-out/bin/sx run examples/239-main-signature-reject.sx
#import "modules/std.sx";
main :: () -> string { // ERROR: return type must be void, an integer, or `!`
return "not an exit code";
}

View File

@@ -0,0 +1,29 @@
// Error-tag `{}` interpolation (ERR step E3 — tag-name table). Formatting an
// error-set value with `{}` renders the tag NAME (`BadDigit`), not the raw id,
// reusing the `any_to_string` dispatch (new `error_set` category → the
// `error_tag_name` builtin → the always-linked tag-name table indexed by global
// tag id). Works for a bound tag, a re-raised/caught tag, and inside text.
#import "modules/std.sx";
E :: error { BadDigit, Empty, Overflow }
parse :: (n: i32) -> (i32, !E) {
if n < 0 { raise error.BadDigit; }
if n == 0 { raise error.Empty; }
return n * 2;
}
main :: () -> i32 {
a : E = error.BadDigit;
b : E = error.Overflow;
print("a={} b={}\n", a, b); // a=BadDigit b=Overflow
// A tag bound by `catch` interpolates too (diverging handler).
v := parse(0) catch (e) {
print("parse failed with {}\n", e); // parse failed with Empty
return 0;
};
print("v={}\n", v); // not reached (parse(0) raises Empty)
return 0;
}

View File

@@ -0,0 +1,40 @@
// Error return-trace buffer push/clear wiring (ERR step E3.2). A `raise` and a
// propagating `try` each push a frame; an absorbing site (`catch`, `or value`,
// a destructure that binds the error) clears the buffer. In debug builds
// (`sx run` defaults to -O0) these calls are emitted; in release they're
// skipped entirely (zero overhead). Until E3.3 ships `trace.print_current`,
// this example observes the buffer directly via the runtime's `sx_trace_len`
// (linked in for the JIT) — a white-box probe, not the eventual public API.
#import "modules/std.sx";
// Internal runtime symbol (library/vendors/sx_trace_runtime/sx_trace.c).
sx_trace_len :: () -> u32 extern;
E :: error { Bad }
fail :: (n: i32) -> !E {
if n < 0 { raise error.Bad; } // pushes a frame
return;
}
propagate :: (n: i32) -> !E {
try fail(n); // on failure: pushes a frame, propagates
return;
}
main :: () -> i32 {
// `catch` lets the handler INSPECT the trace, then absorbs: the buffer is
// cleared when the handler completes (a non-diverging exit), not on entry.
// So inside the handler the frames are still visible (here: the `raise` in
// `fail` + the `try fail` propagation in `propagate` = 2 frames)...
propagate(-1) catch (e) {
print("in catch: len={}\n", sx_trace_len()); // 2 (handler sees the chain)
};
print("after catch: len={}\n", sx_trace_len()); // 0 (absorbed at handler exit)
// A success leaves the buffer empty (nothing pushed).
propagate(1) catch (e) { };
print("after success: len={}\n", sx_trace_len()); // 0
return 0;
}

View File

@@ -0,0 +1,36 @@
// Error return-trace formatting (ERR step E3.3). `library/modules/std/trace.sx`
// reads the trace buffer (E3.1, populated by E3.2's raise/try push wiring) and
// renders it. `trace.print_current()` writes the trace to stderr; the catch
// handler sees the full chain because the absorption clear fires at handler
// EXIT, not entry. Frame locations are placeholders until DWARF (ERR E3.0)
// resolves PCs to file:line; the count + ordering are already meaningful.
//
// Note: the trace goes to stderr. The test runner merges stderr+stdout, so the
// snapshot shows the trace lines interleaved with the `print` (stdout) lines.
#import "modules/std.sx";
trace :: #import "modules/std/trace.sx";
// Buffer length probe (the runtime symbol; public read API is the trace module).
sx_trace_len :: () -> u32 extern;
E :: error { BadInput, Overflow }
leaf :: (n: i32) -> !E {
if n < 0 { raise error.BadInput; } // pushes frame 0
return;
}
mid :: (n: i32) -> !E {
try leaf(n); // propagation pushes frame 1
return;
}
main :: () -> i32 {
mid(-1) catch (e) {
print("[stdout] caught {}\n", e); // tag name via the always-linked table
trace.print_current(); // [stderr] the 2-frame trace
};
print("[stdout] recovered; trace buffer now empty (len {})\n", sx_trace_len());
return 0;
}

View File

@@ -0,0 +1,29 @@
// Failable `-> !` main entry-point wrapper (ERR step E4.2). A pure-failable
// main that lets an error reach the function boundary exits 1 and prints the
// unhandled-error header (with the tag name, via the always-linked tag-name
// table) plus the return trace to stderr — instead of the old behavior of
// returning the raw tag id as the exit code with no diagnostic. A successful
// run (no escaping error) exits 0.
//
// Note: the header + trace go to stderr. The test runner merges stderr+stdout,
// so the snapshot shows them interleaved with the `print` (stdout) lines.
// Frame locations are placeholders until DWARF (ERR E3.0); count + ordering +
// the tag name are already meaningful. Expected exit code: 1.
#import "modules/std.sx";
ParseErr :: error { Empty, BadDigit };
inner :: (n: i32) -> (i32, !ParseErr) {
if n == 0 { raise error.Empty; } // pushes a frame
if n < 0 { raise error.BadDigit; }
return n * 2;
}
main :: () -> !ParseErr {
v := try inner(5); // succeeds → v = 10
print("v = {}\n", v);
w := try inner(0); // raises Empty → propagates to main
print("w = {}\n", w); // never reached
return;
}

View File

@@ -0,0 +1,22 @@
// Value-carrying failable main `-> (int, !)` (ERR step E4.2). The entry-point
// wrapper extracts the `{value, error}` tuple main returns: on success it exits
// with the integer value (truncated to u8, like a plain integer main); on an
// escaping error it prints the header + trace to stderr and exits 1 (the same
// reporter as the pure `-> !` form — see 244). This run takes the success path.
// Expected exit code: 64 (the returned value).
#import "modules/std.sx";
ParseErr :: error { Empty, BadDigit };
inner :: (n: i32) -> (i32, !ParseErr) {
if n == 0 { raise error.Empty; }
if n < 0 { raise error.BadDigit; }
return n * 2;
}
main :: () -> (i32, !ParseErr) {
v := try inner(32); // succeeds → v = 64
print("v = {}\n", v);
return v; // success → exit code 64
}

View File

@@ -0,0 +1,39 @@
// Failable `or` chains (ERR step E2.4b). `lhs or rhs` with failable operands
// is a left-to-right, short-circuit chain: each failing attempt routes to the
// next operand; the chain resolves when an operand succeeds (or a value
// terminator absorbs). `try` marks an operand whose failure is visible routing
// (path-marker rule); a bare failable operand is allowed when a downstream
// terminator absorbs it. A `catch` over a parenthesized chain redirects the
// chain's total failure to the handler instead of the function. Absorbed
// failures clear the trace buffer; `onfail` does not fire for a failure that
// never leaves its block. This run takes only absorbed paths → exit 120.
#import "modules/std.sx";
E :: error { A, B };
fa :: (n: i32) -> (i32, !E) {
if n == 0 { raise error.A; }
if n < 0 { raise error.B; }
return n;
}
fv :: (n: i32) -> !E { // void (pure) failable
if n == 0 { raise error.A; }
return;
}
main :: () -> (i32, !E) {
onfail print("onfail fired (BUG)\n"); // must NOT fire — every chain below absorbs
r : i32 = 0;
r = r + (try fa(0) or try fa(7)); // a fails → b succeeds → 7
r = r + (try fa(0) or try fa(0) or try fa(3)); // first two fail → third → +3 = 10
r = r + (fa(0) or fa(0) or 96); // bare chain + value terminator → +96 = 106
r = r + ((try fa(0) or try fa(0)) catch (e) 5); // both fail → catch handler → +5 = 111
r = r + ((try fa(0) or try fa(9)) catch (e) 0); // second succeeds → catch skipped → +9 = 120
try fv(0) or try fv(1); // void chain: first fails → second succeeds
return r; // success → exit 120; onfail skipped
}

View File

@@ -0,0 +1,21 @@
// Failable `or` chain propagation (ERR step E2.4b). When every operand of a
// `try … or try …` chain fails and there is no value terminator, the final
// failure propagates to the enclosing function — here `main`, so the E4.2
// entry-point wrapper prints the unhandled-error header + return trace to
// stderr and exits 1. Each failed attempt contributes its `raise` frame plus
// the chain-attempt frame, so three all-failing attempts leave six frames
// (locations are placeholders until DWARF / E3.0). Expected exit code: 1.
#import "modules/std.sx";
E :: error { A };
fa :: (n: i32) -> (i32, !E) {
if n == 0 { raise error.A; }
return n;
}
main :: () -> (i32, !E) {
v := try fa(0) or try fa(0) or try fa(0); // all fail → propagate to main
return v;
}

View File

@@ -0,0 +1,31 @@
// `log.sx` leveled logging + the `is_comptime()` builtin (ERR step E4.1,
// slice 1). `log.{warn,info,err,debug}` interpolate like `print` and write
// `LEVEL: <msg>` to stderr. `is_comptime()` is `true` under `#run` (the
// comptime interpreter) and folds to `false` in compiled code, so a gated
// branch dead-codes out of the runtime binary.
//
// (`log.error` is spelled `log.err` — `error` is a reserved keyword. The
// `process.exit` / `assert` part of E4.1 is blocked on `noreturn` codegen,
// issue 0058.)
//
// The test runner merges stderr+stdout; the log lines (stderr) precede the
// single stdout line. Expected exit code: 0.
#import "modules/std.sx";
log :: #import "modules/std/log.sx";
probe :: () -> i32 {
if is_comptime() { return 1; } // comptime interpreter path
return 2; // compiled-code path
}
CT :: #run probe(); // folds to 1 (run in the interpreter)
main :: () -> i32 {
log.warn("disk {}% full", 91);
log.info("user {} connected", "alice");
log.err("bad fd {}", 7);
log.debug("trace x={}", 42);
print("[stdout] is_comptime runtime={} comptime={}\n", probe(), CT);
return 0;
}

View File

@@ -0,0 +1,15 @@
// `process.exit` (ERR step E4.1): immediate process termination with an exit
// code. No `defer` / `onfail` cleanup runs and no error-trace frames are pushed
// — it's POSIX `_exit(2)`. Here a runtime call exits 42; the line after never
// runs. (sx `print` writes unbuffered via `write(2)`, so the "starting" line
// still appears despite `_exit` skipping the stdio flush.) Expected exit: 42.
#import "modules/std.sx";
proc :: #import "modules/std/process.sx";
main :: () -> i32 {
print("starting\n");
proc.exit(42);
print("unreachable\n");
return 0;
}

View File

@@ -0,0 +1,15 @@
// `assert` (ERR steps E4.1 / E4.1b): a false condition prints `ASSERTION
// FAILED at <file>:<line>: <msg>` (the caller's location, via the
// `#caller_location` default param) and exits 1; a true condition is a no-op.
// Built on `process.exit`. Expected exit code: 1.
#import "modules/std.sx";
proc :: #import "modules/std/process.sx";
main :: () -> i32 {
proc.assert(2 + 2 == 4, "arithmetic"); // passes → no-op
print("first assert passed\n");
proc.assert(2 + 2 == 5, "two plus two is not five"); // fails → abort
print("unreachable\n");
return 0;
}

View File

@@ -0,0 +1,22 @@
// `#caller_location` (ERR step E4.1b). As a parameter's default value it
// resolves to a `Source_Location` of the CALL site — file, line:col, and the
// enclosing function — rather than the callee's signature. Explicitly
// forwarding a `Source_Location` through an inner call preserves the outermost
// site (so a logging wrapper reports where IT was called). Expected exit: 0.
#import "modules/std.sx";
note :: (loc: Source_Location = #caller_location) {
print("note from {} (line {})\n", loc.func, loc.line);
}
// Forwards its own caller location through to `note`.
wrap :: (loc: Source_Location = #caller_location) {
note(loc);
}
main :: () -> i32 {
note(); // call site → func main
wrap(); // forwarded → still reports this line in main
return 0;
}

View File

@@ -0,0 +1,23 @@
// `trace.print_interpreter_frames()` (ERR step E4.1). At comptime (`#run`) it
// dumps the interpreter's active sx call-frame chain (most recent call last) to
// the build output; in compiled code it folds to nothing (no interpreter stack
// — the only real caller is `process.exit`'s dead `is_comptime()` branch). The
// dump frame itself is omitted; frame source locations await IR-offset
// resolution, so only names print today. Expected exit: 0.
#import "modules/std.sx";
trace :: #import "modules/std/trace.sx";
probe :: () {
trace.print_interpreter_frames(); // dumps the chain: __run_0 → inner → probe
}
inner :: () {
probe();
}
#run inner(); // top-level #run drives the chain
main :: () -> i32 {
return 0;
}

View File

@@ -0,0 +1,33 @@
// Comptime return-trace resolution (ERR E3.0 slice 3b). A `#run` block that
// raises, propagates via `try`, and catches the error, then formats the trace
// with `trace.print_current()`. At comptime a frame is a packed
// `(func_id, span.start)` (not a `*Frame`); the interpreter's `.trace_resolve`
// unpacks it and resolves `file:line:col` via the module + source map — so the
// comptime trace prints the same `func at file:line:col` form as a runtime one.
// Expected exit: 0 (the error is caught; the trace is printed during the build).
#import "modules/std.sx";
trace :: #import "modules/std/trace.sx";
TErr :: error { Bad };
leaf :: () -> !TErr {
raise error.Bad;
}
mid :: () -> !TErr {
try leaf();
}
probe :: () {
mid() catch (e) {
print("comptime caught {}\n", e);
trace.print_current();
};
}
#run probe();
main :: () -> i32 {
return 0;
}

View File

@@ -0,0 +1,134 @@
// End-to-end error handling: failable functions (named + inferred sets) consumed
// through every absorbing form — destructure, `try` (in helpers), `catch` (bare-
// expr / match-body / diverging block / no-binding), `or` value-terminator,
// `onfail` cleanup interleaved with `defer`, plus `error.X` as a value and `{}`
// tag-name interpolation.
#import "modules/std.sx";
SmokeErr :: error { Empty, BadDigit, Overflow }
// value-carrying, named set
sm_parse :: (n: i32) -> (i32, !SmokeErr) {
if n < 0 { raise error.BadDigit; }
if n == 0 { raise error.Empty; }
if n > 99 { raise error.Overflow; }
return n * 2;
}
// pure failable, inferred set (ad-hoc tag)
sm_check :: (ok: bool) -> ! {
if !ok { raise error.NotReady; }
return;
}
// multi-value, inferred set: `try` propagates; SCC absorbs SmokeErr
sm_pair :: (a: i32, b: i32) -> (i32, i32, !) {
x := try sm_parse(a);
y := try sm_parse(b);
return (x, y);
}
// catch with a diverging block body
sm_or_default :: (n: i32) -> i32 {
return sm_parse(n) catch (e) {
print(" logged {}\n", e);
return -1;
};
}
// onfail + defer interleave: cleanup runs only on the error path
sm_acquire :: (fail: bool) -> (i32, !) {
defer print(" defer A\n");
onfail print(" onfail B\n");
if fail { raise error.Acquire; }
return 7;
}
// or-chain: try a, fall to try b; propagate if both fail
sm_first :: (a: i32, b: i32) -> (i32, !) {
v := try sm_parse(a) or try sm_parse(b);
return v;
}
// --- Composition (ERR E5.1): failable closures, widening, generics ---
// Closure(...) param, try-propagated (the env is carried)
sm_run :: (cb: Closure(i32) -> (i32, !SmokeErr), n: i32) -> (i32, !SmokeErr) {
return try cb(n);
}
// bare fn-type param: a NON-failable closure literal widens into the failable
// slot (the ∅-widening adapter wraps `{value, 0}`)
sm_widen :: (cb: (i32) -> (i32, !SmokeErr), n: i32) -> i32 {
return cb(n) catch (e) -1;
}
// generic ($T) value-carrying failable composition, monomorphized per call
sm_wrap :: ($T: Type, f: Closure() -> (T, !SmokeErr)) -> (T, !SmokeErr) {
return try f();
}
main :: () {
// error.X as a typed value + {} interpolation renders the tag name
e0 : SmokeErr = error.BadDigit;
print("tag: {}\n", e0);
// success destructure + error inspect
v1, err1 := sm_parse(5);
if !err1 { print("parsed: {}\n", v1); }
v2, err2 := sm_parse(-1);
if err2 == error.BadDigit { print("got: {}\n", err2); }
// catch — bare-expr body
ce := sm_parse(0) catch (e) 100;
print("catch-expr: {}\n", ce);
// catch — match-body per-tag dispatch
cm := sm_parse(200) catch (e) == {
case .Overflow: 1;
case .Empty: 2;
else: 3;
};
print("catch-match: {}\n", cm);
// catch — diverging block (in helper)
print("or-default ok: {}\n", sm_or_default(5));
print("or-default err: {}\n", sm_or_default(-5));
// or — value terminator (bare LHS; non-failable result)
ov := sm_parse(0) or 55;
print("or-value: {}\n", ov);
// or-chain via helper (first ok wins; propagation absorbed by destructure)
g, gerr := sm_first(0, 8);
if !gerr { print("or-chain: {}\n", g); }
// multi-value failable consumed by catch (tuple body)
p, q := sm_pair(0, 3) catch (e) (0, 0);
print("pair-catch: {} {}\n", p, q);
p2, q2 := sm_pair(4, 5) catch (e) (0, 0);
print("pair-ok: {} {}\n", p2, q2);
// pure failable: absorb with no-binding catch
sm_check(false) catch { print("check absorbed\n"); };
// onfail/defer interleave on error vs success
print("acquire fail:\n");
hv, herr := sm_acquire(true);
print("acquire ok:\n");
iv, ierr := sm_acquire(false);
// composition: inline failable closure literal through a Closure(...) param
cl := sm_run(closure((x: i32) -> (i32, !SmokeErr) { if x < 0 { raise error.BadDigit; } return x * 2; }), 6) catch (e) -1;
print("closure-run: {}\n", cl); // 12
print("closure-run-err: {}\n", sm_run(closure((x: i32) -> (i32, !SmokeErr) { raise error.Empty; }), 1) catch (e) -9); // -9
// non-failable closure literal widened into the failable bare slot
print("widen: {}\n", sm_widen(closure((x: i32) -> i32 => x + 1), 9)); // 10
// generic failable composition (monomorphized at i32)
print("wrap: {}\n", sm_wrap(i32, closure(() -> (i32, !SmokeErr) { return 42; })) catch (e) 0); // 42
print("errors ok\n");
}

View File

@@ -0,0 +1,18 @@
// Comptime `#run` of a failable whose error ESCAPES (no `catch` / `or`): the
// compiler reports the raised tag name + the return trace at the `#run` site and
// halts with a non-zero exit (E5.2). Before this, a bare failable `#run`
// segfaulted (const form) or silently succeeded (statement form).
#import "modules/std.sx";
E :: error { Bad, Empty }
parse :: (n: i32) -> (i32, !E) {
if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; }
return n * 2;
}
x :: #run parse(-1); // error.Bad escapes → comptime diagnostic + halt
main :: () -> i32 { return x; }

View File

@@ -0,0 +1,31 @@
// Comptime `#run` of a failable composes with the handlers exactly as at
// runtime: `catch` absorbs, `or` terminates, a successful bare `#run` yields the
// value (error channel stripped), and an `onfail` in the evaluated body still
// runs during comptime unwinding (E5.2).
#import "modules/std.sx";
E :: error { Bad, Empty }
parse :: (n: i32) -> (i32, !E) {
if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; }
return n * 2;
}
guard :: (ok: bool) -> !E {
onfail print("comptime cleanup\n");
if !ok { raise error.Bad; }
return;
}
ok_v :: #run parse(5); // success → 10 (value, error stripped)
caught :: #run parse(-1) catch (e) 99; // Bad → 99
ored :: #run parse(0) or 55; // Empty → 55
#run guard(false) catch (e) { }; // onfail fires during the comptime unwind
main :: () -> i32 {
print("ok={} caught={} ored={}\n", ok_v, caught, ored);
return ok_v + caught + ored; // 10 + 99 + 55 = 164
}

View File

@@ -0,0 +1,24 @@
// Failable closure literals (ERR E5.1): a `closure(...)` literal may declare a
// failable return type — `-> (T, !)` / `-> !Named` — in both block and arrow
// body forms, and `raise` inside. Called directly through the bound local, the
// error channel is consumed by `catch` / `or`; passed as a `Closure(...)`
// parameter, it composes through the callee (here absorbed with `catch`).
// (A capturing closure into a bare `(T)->U` slot, and a failable closure into a
// non-failable slot, are rejected — see issue 0060 / the FFI-boundary rule.)
#import "modules/std.sx";
E :: error { Neg }
runwith :: (cb: Closure(i64) -> (i64, !E), n: i64) -> i64 { return cb(n) catch (e) -1; }
main :: () -> i32 {
// block-body and arrow-body failable closures, called directly
m := closure((x: i64) -> (i64, !E) { if x < 0 { raise error.Neg; } return x * 2; });
n := closure((x: i64) -> (i64, !E) => x + 1);
print("{} {} {} {}\n", m(5) catch (e) 0, m(-1) catch (e) 99, m(-1) or 7, n(40) catch (e) 0); // 10 99 7 41
// failable closure passed as a Closure(...) parameter
print("param ok={} err={}\n", runwith(m, 5), runwith(m, -1)); // 10 -1
return 0;
}

View File

@@ -0,0 +1,34 @@
// Failable closure composition (ERR E5.1): a closure LITERAL passed as a
// function-type argument and called inside the callee. Covers a bare failable
// fn-type param (`cb: (T) -> (U, !)`), the idiomatic `Closure(...)` param
// (try-propagated), and ∅-widening of a NON-failable closure literal into a
// failable slot (the generated adapter wraps the value into `{value, 0}`).
//
// NOTE: the adapter is generated when the closure LITERAL flows directly into
// the bare-fn slot. Passing a pre-bound closure *variable* into a bare-fn slot
// is a separate coercion-site path, not yet handled — see CHECKPOINT-ERR.
#import "modules/std.sx";
E :: error { Neg }
bare :: (cb: (i64) -> (i64, !E), n: i64) -> i64 { return cb(n) catch (e) -1; }
chain :: (cb: Closure(i64) -> (i64, !E), n: i64) -> (i64, !E) { return try cb(n); }
dbl :: (x: i64) -> (i64, !E) { if x < 0 { raise error.Neg; } return x * 2; }
main :: () -> i32 {
// failable closure literal through a bare fn-type param (matching ABI)
print("bare ok={} err={}\n",
bare(closure((x: i64) -> (i64, !E) { if x < 0 { raise error.Neg; } return x * 2; }), 5),
bare(closure((x: i64) -> (i64, !E) => x * 2), -1)); // ok=10; err: arrow never raises → cb(-1) = -2
// Closure(...) param, try-propagated, then caught at the call site
print("chain ok={} err={}\n",
chain(closure((x: i64) -> (i64, !E) => x + 6), 4) catch (e) 0, // 10
chain(closure((x: i64) -> (i64, !E) { raise error.Neg; }), 1) catch (e) 0); // 0
// NON-failable closure literal widened into the failable bare slot
print("widen={}\n", bare(closure((x: i64) -> i64 => x + 1), 9)); // 10
return 0;
}

View File

@@ -0,0 +1,36 @@
// Program-wide inferred-`!` union per closure shape (ERR E5.1 sub-feature 2).
// All occurrences of `Closure(i32) -> (i32, !)` share ONE inferred error set;
// every bare-`!` closure literal of that shape unions its raised tags in. A
// `try slot(x)` against any matching-shape slot widens against that union — so
// a caller whose named set covers { Negative, Other } type-checks, and the
// error channel actually carries each closure's own tag at runtime.
#import "modules/std.sx";
All :: error { Negative, Other }
// `h` is a bare-`!` Closure slot; the caller declares the union as `!All`.
dispatch :: (h: Closure(i32) -> (i32, !), x: i32) -> (i32, !All) {
return try h(x);
}
main :: () -> i32 {
gpa := GPA.init();
push Context.{ allocator = xx gpa } {
// Two literals of the SAME shape raising DIFFERENT tags both feed the
// one shared `Closure(i32)->(i32,!)` union node.
handlers : List(Closure(i32) -> (i32, !)) = .{};
handlers.append(closure((x: i32) -> (i32, !) { if x < 0 { raise error.Negative; } return x * 2; }));
handlers.append(closure((x: i32) -> (i32, !) { if x == 0 { raise error.Other; } return x + 100; }));
// success paths
print("ok0={}\n", dispatch(handlers.items[0], 5) catch (e) 0); // 10
print("ok1={}\n", dispatch(handlers.items[1], 7) catch (e) 0); // 107
// failure paths: each closure raises its own tag, which propagates
// through `try` and is absorbed by the call-site `catch` fallback
print("err0={}\n", dispatch(handlers.items[0], -1) catch (e) -1); // raised Negative → -1
print("err1={}\n", dispatch(handlers.items[1], 0) catch (e) -2); // raised Other → -2
}
return 0;
}

View File

@@ -0,0 +1,25 @@
// Program-wide closure-shape union — widening REJECTION (ERR E5.1 sub-feature 2).
// Two closure literals of shape `Closure(i32)->(i32,!)` raise `Negative` /
// `Other`; the shared inferred-`!` node for that shape is { Negative, Other }.
// A caller that `try`s a slot of this shape but declares only `!Small` (which
// omits both tags) is rejected — the union is checked against the caller's set
// even though the call goes through a slot with no static function name.
#import "modules/std.sx";
Small :: error { Unrelated }
reject :: (h: Closure(i32) -> (i32, !), x: i32) -> (i32, !Small) {
return try h(x); // Negative, Other ∉ Small → two diagnostics
}
main :: () -> i32 {
gpa := GPA.init();
push Context.{ allocator = xx gpa } {
handlers : List(Closure(i32) -> (i32, !)) = .{};
handlers.append(closure((x: i32) -> (i32, !) { if x < 0 { raise error.Negative; } return x; }));
handlers.append(closure((x: i32) -> (i32, !) { if x == 0 { raise error.Other; } return x; }));
print("r={}\n", reject(handlers.items[0], 5) catch (e) 0);
}
return 0;
}

View File

@@ -0,0 +1,20 @@
// A closure literal whose body `raise`s but is annotated non-failable (or has
// no `!` in its return) gets a LAMBDA-SPECIFIC diagnostic telling the user to
// declare the failable return explicitly (ERR E5.1 sub-feature 1). This is the
// closure analog of the top-level "raise is only valid inside a failable
// function" error — failability is never inferred for a lambda, it must be
// declared, so a raising lambda with no `!` is a hard error pointing at the fix.
#import "modules/std.sx";
E :: error { Neg }
take :: (cb: Closure(i32) -> (i32, !E), x: i32) -> i32 { return cb(x) catch (e) -1; }
main :: () -> i32 {
// `-> i32` (non-failable) but the body raises → lambda-specific hint:
// "lambda body raises; declare its return type explicitly with
// `-> (T, !)` or `-> (T, !Named)`"
print("{}\n", take(closure((x: i32) -> i32 { if x < 0 { raise error.Neg; } return x; }), -1));
return 0;
}

View File

@@ -0,0 +1,29 @@
// Generic function with a value-carrying `!` return composes (ERR E5.1
// sub-feature 8). A `$T: Type` generic whose return is `(T, !E)` monomorphizes
// per call: `return try f()` propagates the closure's error, and each
// monomorphization's success value flows through as the concrete `T`.
// (Regression: confirms issue 0062 was an invalid-syntax repro — the bug only
// appeared with the non-generic `T: Type` form; the `$T` form works.)
#import "modules/std.sx";
E :: error { Bad }
wrap :: ($T: Type, f: Closure() -> (T, !E)) -> (T, !E) { return try f(); }
main :: () -> i32 {
// success, consumed by catch
print("catch={}\n", wrap(i32, closure(() -> (i32, !E) { return 7; })) catch (e) -1); // 7
// success, consumed by destructure (binds value + error slot); the value
// slot is read only under an `if !err` guard (ERR E1.8 path-sensitivity)
r, err := wrap(i32, closure(() -> (i32, !E) { return 9; }));
if !err { print("destr={} ok=true\n", r); } // destr=9 ok=true
// failure path: the raised tag propagates through the generic `try`
print("fail={}\n", wrap(i32, closure(() -> (i32, !E) { raise error.Bad; }) ) catch (e) -1); // -1
// a second monomorphization at a different T
print("u8={}\n", wrap(u8, closure(() -> (u8, !E) { return 200; })) catch (e) 0); // 200
return 0;
}

View File

@@ -0,0 +1,27 @@
// A closure VALUE (a pre-bound variable) cannot be passed into a bare
// function-pointer slot `(...) -> ...` (ERR E5.1). The bare ABI calls
// `fn_ptr(ctx, args)` with no env channel, so a closure's environment can't be
// carried — passing one is unsound (drops env / shifts args / segfaults on a
// capturing closure). Only a closure LITERAL crosses this boundary (lowerLambda
// emits a static adapter); a variable is rejected with a pointer to the idiom.
//
// The fix for these is either to pass the literal directly, or to type the
// parameter `Closure(...)` so the environment is carried (the idiomatic form).
#import "modules/std.sx";
E :: error { Z }
bare :: (cb: (i64) -> i64, n: i64) -> i64 { return cb(n); }
baref :: (cb: (i64) -> (i64, !E), n: i64) -> i64 { return cb(n) catch (e) -1; }
main :: () -> i32 {
inc := closure((x: i64) -> i64 => x + 1); // capture-free closure var
base := 100;
add := closure((x: i64) -> i64 => x + base); // CAPTURING closure var
_ := bare(inc, 9); // reject: closure value → bare slot
_ := baref(inc, 9); // reject: also the ∅-widening crossing
_ := bare(add, 9); // reject: capturing closure → bare slot
return 0;
}

View File

@@ -0,0 +1,60 @@
// Path-sensitive value-slot liveness (ERR step E1.8). After `v, err := f()`, the
// value slot `v` is "live only where `err` is proven absent". Every read of `v`
// below sits on a path where the compiler can prove `err == null`:
//
// • `if !err { … v … }` — proven inside the guard
// • `if err { return } … v …` — proven on the fall-through
// • `if err { raise } … v …` — fall-through in a failable function
// • `if err { … } else { … v … }` — proven in the else branch
// • `!err and <reads v>` — short-circuit keeps the proof
//
// A bare tag-compare (`if err == error.X`) proves NOTHING about absence — see the
// rejection regression in 1047. (Regression for the E1.8 path-sensitive slice.)
#import "modules/std.sx";
E :: error { Bad, Empty }
parse :: (n: i32) -> (i32, !E) {
if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; }
return n * 10;
}
// Early-return guard: the fall-through proves `err` absent.
guarded :: (n: i32) -> i32 {
v, err := parse(n);
if err { return -1; }
return v; // err proven absent here
}
// `if err { raise }` in a failable function: same fall-through proof.
relay :: (n: i32) -> (i32, !E) {
v, err := parse(n);
if err { raise err; }
return v + 1; // err proven absent here
}
main :: () -> i32 {
total : i32 = 0;
// (1) proven inside `if !err`
v1, e1 := parse(5);
if !e1 { total = total + v1; } // +50
// (2) proven in the else branch
v2, e2 := parse(7);
if e2 { total = total + 1; } else { total = total + v2; } // +70
// (3) short-circuit `&&` keeps the proof for the rhs
v3, e3 := parse(3);
if !e3 and v3 > 0 { total = total + v3; } // +30
// (4) early-return / raise helpers
total = total + guarded(4); // +40
total = total + guarded(-1); // -1
total = total + (relay(2) catch (e) 0); // parse(2)=20 → +1 = 21
print("liveness total: {}\n", total); // 50+70+30+40-1+21 = 210
return total;
}

View File

@@ -0,0 +1,35 @@
// Rejection counterpart to 1046 (ERR step E1.8). Reading a failable's value slot
// where its error is NOT proven absent is a compile error. Two unproven shapes:
//
// (A) reading the value inside the `if err { … }` error path itself
// (B) reading the value after a bare tag-compare (`if err == error.X`), which
// narrows the tag but proves nothing about absence
//
// Each read is rejected with the E1.8 diagnostic; the program never runs (exit 1).
#import "modules/std.sx";
E :: error { Bad }
parse :: (n: i32) -> (i32, !E) {
if n < 0 { raise error.Bad; }
return n * 10;
}
// (A) the read sits on the error path — `err` is present here, not absent.
bad_a :: () -> i32 {
v, err := parse(5);
if err { return v; } // REJECTED: err present on this path
return 0;
}
// (B) a tag-compare narrows which error, but does not prove there is none.
bad_b :: () -> i32 {
v, err := parse(5);
if err == error.Bad { return 1; }
return v; // REJECTED: err not proven absent
}
main :: () -> i32 {
return bad_a() + bad_b();
}

View File

@@ -0,0 +1,28 @@
// Failable calls in cleanup bodies must be absorbed locally (ERR step E1.7). A
// `defer` / `onfail` body runs while the block is already exiting, so a failable
// it calls has nowhere to propagate — it must be handled in place with `catch`
// or an `or <value>` terminator. This file shows the accepted forms; the bare
// (un-absorbed) form is rejected in 1049.
#import "modules/std.sx";
E :: error { Bad }
failing :: () -> !E { raise error.Bad; }
recover :: () -> (i32, !E) { raise error.Bad; }
work :: (n: i32) -> !E {
defer print("defer: always\n"); // plain cleanup
onfail { failing() catch (e) print("onfail: caught (catch)\n"); } // catch absorbs
onfail { x := recover() or 7; print("onfail: x={} (or)\n", x); } // or-value absorbs
if n < 0 { raise error.Bad; }
return;
}
main :: () -> i32 {
print("[error]\n");
a := work(-1); // raises → onfail bodies fire, then defer (reverse decl order)
print("[ok]\n");
b := work(2); // success → only defer fires
return 0;
}

View File

@@ -0,0 +1,23 @@
// Rejection counterpart to 1048 (ERR step E1.7). A bare (un-absorbed) failable
// call in a `defer` / `onfail` body is a compile error — the block is already
// exiting, so the error has nowhere to propagate. It must be absorbed locally
// with `catch` or `or <value>`. Both a `defer` and an `onfail` bare call are
// flagged; the program never runs (exit 1).
#import "modules/std.sx";
E :: error { Bad }
failing :: () -> !E { raise error.Bad; }
work :: (n: i32) -> !E {
defer failing(); // REJECTED: bare failable in a defer body
onfail { failing(); } // REJECTED: bare failable in an onfail body
if n < 0 { raise error.Bad; }
return;
}
main :: () -> i32 {
a := work(-1);
return 0;
}

View File

@@ -0,0 +1,27 @@
// A braced `defer { … }` body parses as a full statement block (like `onfail`),
// so it supports every statement form — a destructure decl, a `catch`-statement,
// nested var decls — not just a single bare expression. Previously `defer { … }`
// routed through the expression parser and rejected those with "expected ';'".
//
// Regression (issue 0065).
#import "modules/std.sx";
E :: error { Bad }
probe :: () -> (i32, !E) { return 21; }
failing :: () -> !E { raise error.Bad; }
run :: () {
defer {
v, e := probe(); // destructure decl
if !e { print("defer: v={}\n", v); } // value live under the guard
failing() catch (x) print("defer: caught\n"); // catch-statement absorbs
}
print("body\n");
}
main :: () -> i32 {
run();
return 0;
}

View File

@@ -0,0 +1,48 @@
// A closure literal inside a `defer` / `onfail` body is its OWN function
// boundary (ERR step E1.7). Two boundary effects, both pinned here:
//
// (a) `checkCleanupNode` sees a bare lambda STATEMENT as a `.lambda` node and
// STOPS — it does not descend into the lambda body. So the bare failable
// inside the lambda is the lambda's concern, not a cleanup violation
// (were the `.lambda` arm to recurse, this bare `failing()` would reject
// like the ones in 1052).
//
// (b) value-slot liveness (E1.8) is analysed per-boundary: `flowExpr` recurses
// into the lambda via `analyzeFnBody`, so a value slot read inside the
// lambda must prove its own error absent — `v` here is live under its
// `if !err` guard. (The rejecting counterpart is 1053.)
//
// Also: `try` is legal inside the lambda (it propagates through the lambda's own
// `!E` channel) even though it is parser-banned in the cleanup body directly.
//
// Locks the closure-boundary arms of the error-flow pass before A5.2 extracts it
// into its own module. Constructible since issue 0073 (closure literal in a
// `defer` body no longer segfaults lowering — see 0310).
#import "modules/std.sx";
E :: error { Bad }
failing :: () -> !E { raise error.Bad; }
recover :: () -> (i32, !E) { return 21; }
work :: () {
defer {
// (a) bare lambda statement — checkCleanupNode stops at the `.lambda`.
() -> !E { failing(); };
// (b) called closure — its body is analysed as its own boundary.
emit := () -> !E {
v, err := recover();
if !err { print("defer closure: v={}\n", v); } // E1.8: live under guard
try failing();
};
emit() catch (e) print("defer closure: raised\n");
}
print("body\n");
}
main :: () -> i32 {
work();
return 0;
}

View File

@@ -0,0 +1,36 @@
// The cleanup-absorption check (ERR step E1.7) is TRANSITIVE: a bare,
// un-absorbed failable call is rejected no matter how deeply it is nested
// inside a `defer` / `onfail` body's control flow — through `if` (both
// branches), nested blocks, and loops. 1049 covers the direct-body case; this
// pins the recursive arms of `checkCleanupNode` (`.if_expr`, `.block`,
// `.while_expr`) before A5.2 extracts the pass into its own module.
//
// Three bare failables, three rejections; the program never runs (exit 1).
#import "modules/std.sx";
E :: error { Bad }
failing :: () -> !E { raise error.Bad; }
work :: (n: i32) -> !E {
defer {
if n > 0 {
failing(); // REJECTED: nested in the `if` then-branch
} else {
{ failing(); } // REJECTED: nested block in the else-branch
}
}
onfail {
while n > 0 {
failing(); // REJECTED: nested in the `while` body
}
}
if n < 0 { raise error.Bad; }
return;
}
main :: () -> i32 {
a := work(-1);
return 0;
}

View File

@@ -0,0 +1,31 @@
// Value-slot liveness (ERR step E1.8) is analysed inside a nested lambda as its
// OWN boundary: `flowExpr` recurses into a lambda literal via `analyzeFnBody`.
// Reading a failable's value slot inside the lambda where its error is NOT
// proven absent is rejected — even though the lambda is never called and the
// outer function proves nothing for it.
//
// Negative counterpart to 1051(b): were `flowExpr`'s `.lambda` recursion
// removed, the lambda body would go un-analysed and this read would slip
// through. The program never runs (exit 1).
#import "modules/std.sx";
E :: error { Bad }
parse :: (n: i32) -> (i32, !E) {
if n < 0 { raise error.Bad; }
return n * 10;
}
build :: () {
emit := () -> i32 {
v, err := parse(5);
return v; // REJECTED: err not proven absent (inside lambda)
};
print("unreached\n");
}
main :: () -> i32 {
build();
return 0;
}

View File

@@ -0,0 +1,40 @@
// Backtick raw identifier as the error-tag binding of `catch` and `onfail`. A
// reserved type-name spelling (`i2`, `u8`) is a value name when backticked, so
// it is accepted as the tag binding and a later reference resolves to it. A
// *bare* reserved spelling in the same position is still rejected (see
// examples/1123), so the backtick escape is the only way to spell these tags.
// Regression (issue 0089 — attempt-2 catch/onfail coverage).
#import "modules/std.sx";
E :: error { Bad, Empty }
parse :: (n: i32) -> (i32, !E) {
if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; }
return n * 2;
}
// `catch` tag binding spelled `i2`, referenced in the match body.
classify :: (n: i32) -> i32 {
return parse(n) catch (`i2) == {
case .Bad: 1;
case .Empty: 2;
else: 3
};
}
// `onfail` tag binding spelled `u8`, referenced in the cleanup body.
cleanup :: (n: i32) -> !E {
onfail (`u8) { if `u8 == error.Bad { print("cleanup: bad\n"); } }
if n < 0 { raise error.Bad; }
return;
}
main :: () -> i32 {
print("classify(-1) = {}\n", classify(-1));
print("classify(0) = {}\n", classify(0));
print("classify(5) = {}\n", classify(5));
c := cleanup(-1);
print("done\n");
return 0;
}

View File

@@ -0,0 +1,44 @@
// Enum-valued value-carrying failable: the SUCCESS path must zero the trailing
// error slot. Regression (issue 0097). A `-> (Enum, !E)` `return .variant`
// resolves the enum literal against the function's VALUE type (the enum), not
// the failable tuple — otherwise the literal mis-resolves (tag 0) and is stamped
// with the tuple type, which the success-return lowering mistakes for a forwarded
// full tuple and leaves the error slot UNDEFINED (read back as garbage nonzero).
//
// This pins the slot at RUNTIME on the success path (cast(i64) e, bare `if e`,
// and `e == error.X`) — not only via the `if !e` proof that the compiler can
// fold away. It also exercises a non-zero ordinal (`.blue` = 2) so a value slot
// that collapses to 0 is caught, and asserts the error PATH still carries the
// right tag and `error_tag_name`.
#import "modules/std.sx";
Color :: enum { red; green; blue; }
E :: error { Nope }
pick :: (s: string) -> (Color, !E) {
if s == "red" { return .red; }
if s == "blue" { return .blue; } // non-zero ordinal (2)
raise error.Nope;
}
main :: () -> i32 {
// ── success path: error slot MUST read 0 at runtime ──
c, e := pick("red");
print("success err int = {}\n", cast(i64) e); // 0
if e { print("bare-if e: ERROR (WRONG)\n"); } else { print("bare-if e: ok\n"); }
if e == error.Nope { print("e == Nope (WRONG)\n"); } else { print("e != Nope (ok)\n"); }
if !e { print("guard !e: c = {}\n", cast(i64) c); } // 0 (red)
// ── non-zero ordinal: value slot must carry the real ordinal ──
c2, e2 := pick("blue");
if !e2 { print("blue: err int = {}, c = {}\n", cast(i64) e2, cast(i64) c2); } // 0, 2
// ── error path: the right tag flows through ──
c3, e3 := pick("xxx");
print("error err nonzero = {}\n", cast(i64) e3 != 0); // true (ordinal is program-global, not pinned)
if e3 == error.Nope { print("error: is Nope (ok)\n"); } else { print("error: not Nope (WRONG)\n"); }
print("error tag name = {}\n", error_tag_name(e3)); // Nope
return 0;
}

View File

@@ -0,0 +1,66 @@
// Enum-valued value-carrying failables, two paths the bare-success fix (issue
// 0097) did NOT originally cover. Regression (issue 0097, attempt-2 review F1+F2).
//
// F1 — EXPLICIT full failable tuple return `return (.v, error.X)`. The bug was
// the inverse of 0097: narrowing the return target to the value type for ALL
// value-failables broke the explicit-tuple form (the trailing error element no
// longer resolved against the error set → `.unresolved` field → LLVM-emit
// panic). The target must stay the FULL failable tuple for an explicit tuple
// literal of full arity, and narrow to the value type only for a BARE value.
// This pins both branches in one fn: bare-value success + explicit-tuple error.
//
// F2 — COMPTIME-PARAM ($n) value-failable. The body is INLINED at the call
// site (lowerComptimeCall), so the success return takes the inline-return path,
// which the original fix skipped — it stored `{value, undef}` (error slot
// undefined) on success. Read the error slot at RUNTIME on the success path
// (cast, bare `if`, `== error.X`) so an undef slot is caught, not masked by the
// `if !e` proof.
#import "modules/std.sx";
Color :: enum { red; green; blue; }
E :: error { Nope }
// F1: bare-value success path AND explicit-tuple error path in one function.
classify :: (s: string) -> (Color, !E) {
if s == "ok" { return .blue; } // bare value → {2, 0}
return (.red, error.Nope); // explicit full tuple → {0, 1}
}
// F2: comptime parameter forces inline lowering of the body.
ct_pick :: ($n: i32, s: string) -> (Color, !E) {
if s == "red" { return .red; } // bare value, inline path → {0, 0}
if s == "blue" { return .blue; } // bare value, inline path → {2, 0}
raise error.Nope; // inline error path → {undef, 1}
}
main :: () -> i32 {
// ── F1 success (bare value, explicit-tuple error fn): error slot 0 ──
c, e := classify("ok");
print("F1 ok: err int = {}\n", cast(i64) e); // 0
if e { print("F1 ok bare-if: ERROR (WRONG)\n"); } else { print("F1 ok bare-if: ok\n"); }
if !e { print("F1 ok guard: c = {}\n", cast(i64) c); } // 2 (blue)
// ── F1 error (explicit tuple): right tag flows, no panic ──
c2, e2 := classify("bad");
print("F1 bad: err nonzero = {}\n", cast(i64) e2 != 0); // true (ordinal is program-global, not pinned)
if e2 == error.Nope { print("F1 bad: is Nope (ok)\n"); } else { print("F1 bad: not Nope (WRONG)\n"); }
print("F1 bad: tag name = {}\n", error_tag_name(e2)); // Nope
// ── F2 success (comptime-param, inline path): error slot 0 at runtime ──
c3, e3 := ct_pick(7, "red");
print("F2 red: err int = {}\n", cast(i64) e3); // 0
if e3 { print("F2 red bare-if: ERROR (WRONG)\n"); } else { print("F2 red bare-if: ok\n"); }
if e3 == error.Nope { print("F2 red == Nope (WRONG)\n"); } else { print("F2 red != Nope (ok)\n"); }
if !e3 { print("F2 red guard: c = {}\n", cast(i64) c3); } // 0
c4, e4 := ct_pick(7, "blue");
if !e4 { print("F2 blue: err int = {}, c = {}\n", cast(i64) e4, cast(i64) c4); } // 0, 2
// ── F2 error (comptime-param, inline error path): right tag ──
c5, e5 := ct_pick(7, "x");
print("F2 err: err nonzero = {}\n", cast(i64) e5 != 0); // true (ordinal is program-global, not pinned)
if e5 == error.Nope { print("F2 err: is Nope (ok)\n"); } else { print("F2 err: not Nope (WRONG)\n"); }
return 0;
}

View File

@@ -0,0 +1,36 @@
// `!` on an error binding is the truthiness complement of `if e` (issue
// 0129). Pre-fix, `!` lowered as a bitwise not, so a nonzero error tag
// stayed nonzero and `if !e` held even on a SET error — with the success
// value read as garbage. Integer operands get the same `!x ≡ x == 0`
// semantics.
#import "modules/std.sx";
E :: error { Boom }
f :: (fail: bool) -> (i64, !E) {
if fail { raise error.Boom; }
return 42;
}
main :: () -> i32 {
// set error: `if e` holds, `if !e` must NOT
v, e := f(true);
took_e := false;
if e { took_e = true; }
if !e { print("BUG: !e held on a set error (v={})\n", v); return 1; }
if !took_e { print("BUG: if e did not hold on a set error\n"); return 2; }
// success: `if !e` holds and the value is real
v2, e2 := f(false);
if e2 { print("BUG: e2 set on success\n"); return 3; }
if !e2 { print("ok: !e2 on success, v2={}\n", v2); }
// integers: `!n` is `n == 0`, not a bit flip
n := 7;
if !n { print("BUG: !7 held\n"); return 4; }
z := 0;
if !z { print("ok: !0 holds\n"); }
print("done\n");
return 0;
}

View File

@@ -0,0 +1,23 @@
// A generic value-failable fn `($R, !E)` reached through a RE-EXPORT alias
// keeps its `!` error channel at the call site — the result types as a
// value-failable, so `or` / `try` accept it. Mirrors std.sx's
// `await :: io_mod.await` (+ `IoErr :: io_mod.IoErr`) re-export.
// Regression (issue 0153): the planned call-result type was resolved in the
// CALL-SITE module (where `LE` is a re-export alias → a non-`.error_set`
// TypeId), so `errorChannelOf` saw a plain tuple and `b.get() or {…}` built a
// malformed i1 PHI. The fix pins return-type resolution to the fn's defining
// module, matching `monomorphizeFunction`. Needs BOTH generic + re-export.
#import "modules/std.sx";
lib :: #import "1058-errors-reexport-value-failable-channel/lib.sx";
// Re-export the generic fn AND its error set (the std.sx facade pattern).
Box :: lib.Box;
get :: lib.get;
LE :: lib.LE;
main :: () -> i32 {
b : Box(i64) = .{ v = 42 };
r := b.get() or { -1 }; // value-failable channel preserved → r=42
print("r={}\n", r);
return 0;
}

View File

@@ -0,0 +1,8 @@
// Implementation module: a generic value-failable `ufcs` fn + its error set.
#import "modules/std.sx";
LE :: error { Bad }
Box :: struct ($R: Type) { v: R; }
// Returns `($R, !LE)` — a value-failable. `$R` is inferred from the arg.
get :: ufcs (b: *Box($R)) -> ($R, !LE) { return b.v; }

View File

@@ -0,0 +1,39 @@
// issue 0134 — a same-name `error` set collapses into a namespaced import's
// set (error sets lack per-decl nominal identity).
//
// `EventErr` is declared locally as `error { Boom }`, but
// `#import "modules/std.sx"` also carries `event.EventErr` (an error set with
// tags Init/Register/Wait). Because error-set DECLARATIONS are not given
// per-decl nominal identity (unlike struct/enum/union under E6a) —
// `registerErrorSetDecl` registers via the flat `findByName`-dedup path — the
// local `EventErr` collapses into the imported one, losing its own `Boom` tag.
//
// So `raise error.Boom` / `r == error.Boom` are checked against the IMPORTED
// set, which has no `Boom`.
//
// EXPECT (today): build FAILS —
// error: error tag 'error.Boom' is not in error set 'EventErr'
// EXPECT (after fix): prints `own EventErr.Boom`, exit 0.
//
// Proof it's the collision: rename `EventErr` -> `MyErr` and it compiles and
// prints. The reference side (`!EventErr` → resolveNominalLeaf) is already
// visibility-aware from issue 0132's broader fix, but it is dormant until the
// local declaration gets its own TypeId. See the .md.
#import "modules/std.sx";
EventErr :: error { Boom }
fail :: () -> !EventErr {
raise error.Boom;
}
main :: () -> i32 {
r := fail();
if r == error.Boom {
print("own EventErr.Boom\n");
return 0;
}
print("wrong set\n");
return 1;
}

View File

@@ -0,0 +1 @@
25

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
error-set result: 25

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,11 @@
error: error tag 'error.NotInSet' is not in error set 'ParseErr'
--> examples/errors/1001-errors-set-typing.sx:13:20
|
13 | c : ParseErr = error.NotInSet; // error: NotInSet not in ParseErr
| ^^^^^^^^^^^^^^
error: an error-set value compares only with an `error.X` tag or another error-set value; coerce with `xx` to compare the raw id
--> examples/errors/1001-errors-set-typing.sx:14:8
|
14 | if c == 42 { return 1; } // error: error-set value vs raw integer
| ^

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
8

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
raise result: 8

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,17 @@
error: error tag 'error.NotInSet' is not in error set 'ParseErr'
--> examples/errors/1003-errors-raise-rejections.sx:17:11
|
17 | raise error.NotInSet; // error: NotInSet not in ParseErr
| ^^^^^^^^^^^^^^
error: error tag 'error.Weird' is not in caller's error set 'ParseErr'
--> examples/errors/1003-errors-raise-rejections.sx:24:5
|
24 | raise e; // error: OtherErr not subset of ParseErr
| ^^^^^^^^
error: `raise` is only valid inside a failable function (a return type with `!` or `!Named`)
--> examples/errors/1003-errors-raise-rejections.sx:30:5
|
30 | raise error.BadDigit; // error: main (-> i32) is not failable
| ^^^^^^^^^^^^^^^^^^^^^

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
5

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
try result: 5

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,17 @@
error: `try` is only valid inside a failable function (a return type with `!` or `!Named`)
--> examples/errors/1005-errors-try-rejections.sx:20:5
|
20 | try ga(); // error: `try` outside a failable function
| ^^^^^^^^
error: `try` requires a failable expression; operand has type 'i32'
--> examples/errors/1005-errors-try-rejections.sx:26:5
|
26 | try plain(); // error: operand has type i32 (not failable)
| ^^^^^^^^^^^
error: error tag 'error.Yb' is not in caller's error set 'A'
--> examples/errors/1005-errors-try-rejections.sx:32:5
|
32 | try gb(); // error: Yb not in caller's error set A
| ^^^^^^^^

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
7

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
inferred result: 7

View File

@@ -0,0 +1,5 @@
error: error tag 'error.Foo' is not in caller's error set 'A'
--> examples/errors/1007-errors-inferred-widening-reject.sx:23:5
|
23 | try via(); // error: Foo (via's converged set) not in A
| ^^^^^^^^^

View File

@@ -0,0 +1 @@
134

View File

@@ -0,0 +1 @@
match result: 134

View File

@@ -0,0 +1 @@
18

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
catch result: 18

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: `catch` requires a failable expression; operand has type 'i32'
--> examples/errors/1010-errors-catch-rejections.sx:11:5
|
11 | plain() catch (e) { return 1; }; // error: operand has type i32 (not failable)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
60

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
value-failable result: 60

Some files were not shown because too many files have changed in this diff Show More