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:
24
examples/errors/1000-errors-sets.sx
Normal file
24
examples/errors/1000-errors-sets.sx
Normal 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;
|
||||
}
|
||||
16
examples/errors/1001-errors-set-typing.sx
Normal file
16
examples/errors/1001-errors-set-typing.sx
Normal 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;
|
||||
}
|
||||
26
examples/errors/1002-errors-raise.sx
Normal file
26
examples/errors/1002-errors-raise.sx
Normal 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;
|
||||
}
|
||||
32
examples/errors/1003-errors-raise-rejections.sx
Normal file
32
examples/errors/1003-errors-raise-rejections.sx
Normal 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;
|
||||
}
|
||||
32
examples/errors/1004-errors-try.sx
Normal file
32
examples/errors/1004-errors-try.sx
Normal 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;
|
||||
}
|
||||
41
examples/errors/1005-errors-try-rejections.sx
Normal file
41
examples/errors/1005-errors-try-rejections.sx
Normal 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;
|
||||
}
|
||||
40
examples/errors/1006-errors-inferred-error-sets.sx
Normal file
40
examples/errors/1006-errors-inferred-error-sets.sx
Normal 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;
|
||||
}
|
||||
30
examples/errors/1007-errors-inferred-widening-reject.sx
Normal file
30
examples/errors/1007-errors-inferred-widening-reject.sx
Normal 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;
|
||||
}
|
||||
40
examples/errors/1008-errors-match-diverging-arms.sx
Normal file
40
examples/errors/1008-errors-match-diverging-arms.sx
Normal 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;
|
||||
}
|
||||
64
examples/errors/1009-errors-catch.sx
Normal file
64
examples/errors/1009-errors-catch.sx
Normal 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;
|
||||
}
|
||||
13
examples/errors/1010-errors-catch-rejections.sx
Normal file
13
examples/errors/1010-errors-catch-rejections.sx
Normal 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;
|
||||
}
|
||||
36
examples/errors/1011-errors-value-failable.sx
Normal file
36
examples/errors/1011-errors-value-failable.sx
Normal 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;
|
||||
}
|
||||
60
examples/errors/1012-errors-value-failable-consume.sx
Normal file
60
examples/errors/1012-errors-value-failable-consume.sx
Normal 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;
|
||||
}
|
||||
19
examples/errors/1013-errors-value-failable-reject.sx
Normal file
19
examples/errors/1013-errors-value-failable-reject.sx
Normal 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;
|
||||
}
|
||||
25
examples/errors/1014-errors-failable-or.sx
Normal file
25
examples/errors/1014-errors-failable-or.sx
Normal 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;
|
||||
}
|
||||
19
examples/errors/1015-errors-failable-or-reject.sx
Normal file
19
examples/errors/1015-errors-failable-or-reject.sx
Normal 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;
|
||||
}
|
||||
42
examples/errors/1016-errors-onfail.sx
Normal file
42
examples/errors/1016-errors-onfail.sx
Normal 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;
|
||||
}
|
||||
15
examples/errors/1017-errors-onfail-reject.sx
Normal file
15
examples/errors/1017-errors-onfail-reject.sx
Normal 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();
|
||||
}
|
||||
74
examples/errors/1018-errors-multi-value-failable.sx
Normal file
74
examples/errors/1018-errors-multi-value-failable.sx
Normal 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;
|
||||
}
|
||||
30
examples/errors/1019-errors-failable-discard-reject.sx
Normal file
30
examples/errors/1019-errors-failable-discard-reject.sx
Normal 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;
|
||||
}
|
||||
26
examples/errors/1020-errors-cleanup-body-restrictions.sx
Normal file
26
examples/errors/1020-errors-cleanup-body-restrictions.sx
Normal 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; }
|
||||
14
examples/errors/1021-errors-main-exit-truncation.sx
Normal file
14
examples/errors/1021-errors-main-exit-truncation.sx
Normal 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;
|
||||
}
|
||||
15
examples/errors/1022-errors-main-signature-reject.sx
Normal file
15
examples/errors/1022-errors-main-signature-reject.sx
Normal 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";
|
||||
}
|
||||
29
examples/errors/1023-errors-tag-interpolation.sx
Normal file
29
examples/errors/1023-errors-tag-interpolation.sx
Normal 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;
|
||||
}
|
||||
40
examples/errors/1024-errors-trace-buffer.sx
Normal file
40
examples/errors/1024-errors-trace-buffer.sx
Normal 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;
|
||||
}
|
||||
36
examples/errors/1025-errors-trace-format.sx
Normal file
36
examples/errors/1025-errors-trace-format.sx
Normal 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;
|
||||
}
|
||||
29
examples/errors/1026-errors-failable-main.sx
Normal file
29
examples/errors/1026-errors-failable-main.sx
Normal 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;
|
||||
}
|
||||
22
examples/errors/1027-errors-failable-main-value.sx
Normal file
22
examples/errors/1027-errors-failable-main-value.sx
Normal 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
|
||||
}
|
||||
39
examples/errors/1028-errors-failable-or-chain.sx
Normal file
39
examples/errors/1028-errors-failable-or-chain.sx
Normal 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
|
||||
}
|
||||
21
examples/errors/1029-errors-failable-or-chain-propagate.sx
Normal file
21
examples/errors/1029-errors-failable-or-chain-propagate.sx
Normal 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;
|
||||
}
|
||||
31
examples/errors/1030-errors-log-and-comptime.sx
Normal file
31
examples/errors/1030-errors-log-and-comptime.sx
Normal 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;
|
||||
}
|
||||
15
examples/errors/1031-errors-process-exit.sx
Normal file
15
examples/errors/1031-errors-process-exit.sx
Normal 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;
|
||||
}
|
||||
15
examples/errors/1032-errors-assert.sx
Normal file
15
examples/errors/1032-errors-assert.sx
Normal 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;
|
||||
}
|
||||
22
examples/errors/1033-errors-caller-location.sx
Normal file
22
examples/errors/1033-errors-caller-location.sx
Normal 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;
|
||||
}
|
||||
23
examples/errors/1034-errors-interp-frames.sx
Normal file
23
examples/errors/1034-errors-interp-frames.sx
Normal 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;
|
||||
}
|
||||
33
examples/errors/1035-errors-comptime-trace.sx
Normal file
33
examples/errors/1035-errors-comptime-trace.sx
Normal 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;
|
||||
}
|
||||
134
examples/errors/1036-errors-failable-smoke.sx
Normal file
134
examples/errors/1036-errors-failable-smoke.sx
Normal 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");
|
||||
}
|
||||
18
examples/errors/1037-errors-comptime-run-escape.sx
Normal file
18
examples/errors/1037-errors-comptime-run-escape.sx
Normal 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; }
|
||||
31
examples/errors/1038-errors-comptime-run-handled.sx
Normal file
31
examples/errors/1038-errors-comptime-run-handled.sx
Normal 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
|
||||
}
|
||||
24
examples/errors/1039-errors-failable-closure-literal.sx
Normal file
24
examples/errors/1039-errors-failable-closure-literal.sx
Normal 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;
|
||||
}
|
||||
34
examples/errors/1040-errors-failable-closure-composition.sx
Normal file
34
examples/errors/1040-errors-failable-closure-composition.sx
Normal 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;
|
||||
}
|
||||
36
examples/errors/1041-errors-failable-closure-shape-union.sx
Normal file
36
examples/errors/1041-errors-failable-closure-shape-union.sx
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
20
examples/errors/1043-errors-lambda-raise-annotation-hint.sx
Normal file
20
examples/errors/1043-errors-lambda-raise-annotation-hint.sx
Normal 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;
|
||||
}
|
||||
29
examples/errors/1044-errors-generic-failable-composition.sx
Normal file
29
examples/errors/1044-errors-generic-failable-composition.sx
Normal 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;
|
||||
}
|
||||
27
examples/errors/1045-errors-closure-var-bare-slot-reject.sx
Normal file
27
examples/errors/1045-errors-closure-var-bare-slot-reject.sx
Normal 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;
|
||||
}
|
||||
60
examples/errors/1046-errors-value-slot-liveness.sx
Normal file
60
examples/errors/1046-errors-value-slot-liveness.sx
Normal 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;
|
||||
}
|
||||
35
examples/errors/1047-errors-value-slot-liveness-reject.sx
Normal file
35
examples/errors/1047-errors-value-slot-liveness-reject.sx
Normal 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();
|
||||
}
|
||||
28
examples/errors/1048-errors-cleanup-absorption.sx
Normal file
28
examples/errors/1048-errors-cleanup-absorption.sx
Normal 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;
|
||||
}
|
||||
23
examples/errors/1049-errors-cleanup-absorption-reject.sx
Normal file
23
examples/errors/1049-errors-cleanup-absorption-reject.sx
Normal 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;
|
||||
}
|
||||
27
examples/errors/1050-errors-defer-block-body.sx
Normal file
27
examples/errors/1050-errors-defer-block-body.sx
Normal 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;
|
||||
}
|
||||
48
examples/errors/1051-errors-cleanup-closure-boundary.sx
Normal file
48
examples/errors/1051-errors-cleanup-closure-boundary.sx
Normal 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;
|
||||
}
|
||||
36
examples/errors/1052-errors-cleanup-transitive-reject.sx
Normal file
36
examples/errors/1052-errors-cleanup-transitive-reject.sx
Normal 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;
|
||||
}
|
||||
31
examples/errors/1053-errors-nested-lambda-liveness-reject.sx
Normal file
31
examples/errors/1053-errors-nested-lambda-liveness-reject.sx
Normal 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;
|
||||
}
|
||||
40
examples/errors/1054-errors-backtick-reserved-binding.sx
Normal file
40
examples/errors/1054-errors-backtick-reserved-binding.sx
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
36
examples/errors/1057-errors-negated-error-binding.sx
Normal file
36
examples/errors/1057-errors-negated-error-binding.sx
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
39
examples/errors/1059-errors-same-name-error-set-own-wins.sx
Normal file
39
examples/errors/1059-errors-same-name-error-set-own-wins.sx
Normal 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;
|
||||
}
|
||||
1
examples/errors/expected/1000-errors-sets.exit
Normal file
1
examples/errors/expected/1000-errors-sets.exit
Normal file
@@ -0,0 +1 @@
|
||||
25
|
||||
1
examples/errors/expected/1000-errors-sets.stderr
Normal file
1
examples/errors/expected/1000-errors-sets.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
examples/errors/expected/1000-errors-sets.stdout
Normal file
1
examples/errors/expected/1000-errors-sets.stdout
Normal file
@@ -0,0 +1 @@
|
||||
error-set result: 25
|
||||
1
examples/errors/expected/1001-errors-set-typing.exit
Normal file
1
examples/errors/expected/1001-errors-set-typing.exit
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
11
examples/errors/expected/1001-errors-set-typing.stderr
Normal file
11
examples/errors/expected/1001-errors-set-typing.stderr
Normal 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
|
||||
| ^
|
||||
1
examples/errors/expected/1001-errors-set-typing.stdout
Normal file
1
examples/errors/expected/1001-errors-set-typing.stdout
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
examples/errors/expected/1002-errors-raise.exit
Normal file
1
examples/errors/expected/1002-errors-raise.exit
Normal file
@@ -0,0 +1 @@
|
||||
8
|
||||
1
examples/errors/expected/1002-errors-raise.stderr
Normal file
1
examples/errors/expected/1002-errors-raise.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
examples/errors/expected/1002-errors-raise.stdout
Normal file
1
examples/errors/expected/1002-errors-raise.stdout
Normal file
@@ -0,0 +1 @@
|
||||
raise result: 8
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
17
examples/errors/expected/1003-errors-raise-rejections.stderr
Normal file
17
examples/errors/expected/1003-errors-raise-rejections.stderr
Normal 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
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
1
examples/errors/expected/1004-errors-try.exit
Normal file
1
examples/errors/expected/1004-errors-try.exit
Normal file
@@ -0,0 +1 @@
|
||||
5
|
||||
16037
examples/errors/expected/1004-errors-try.ir
Normal file
16037
examples/errors/expected/1004-errors-try.ir
Normal file
File diff suppressed because one or more lines are too long
1
examples/errors/expected/1004-errors-try.stderr
Normal file
1
examples/errors/expected/1004-errors-try.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
examples/errors/expected/1004-errors-try.stdout
Normal file
1
examples/errors/expected/1004-errors-try.stdout
Normal file
@@ -0,0 +1 @@
|
||||
try result: 5
|
||||
1
examples/errors/expected/1005-errors-try-rejections.exit
Normal file
1
examples/errors/expected/1005-errors-try-rejections.exit
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
17
examples/errors/expected/1005-errors-try-rejections.stderr
Normal file
17
examples/errors/expected/1005-errors-try-rejections.stderr
Normal 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
|
||||
| ^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
7
|
||||
16054
examples/errors/expected/1006-errors-inferred-error-sets.ir
Normal file
16054
examples/errors/expected/1006-errors-inferred-error-sets.ir
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
inferred result: 7
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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
|
||||
| ^^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
134
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
match result: 134
|
||||
1
examples/errors/expected/1009-errors-catch.exit
Normal file
1
examples/errors/expected/1009-errors-catch.exit
Normal file
@@ -0,0 +1 @@
|
||||
18
|
||||
16148
examples/errors/expected/1009-errors-catch.ir
Normal file
16148
examples/errors/expected/1009-errors-catch.ir
Normal file
File diff suppressed because one or more lines are too long
1
examples/errors/expected/1009-errors-catch.stderr
Normal file
1
examples/errors/expected/1009-errors-catch.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
examples/errors/expected/1009-errors-catch.stdout
Normal file
1
examples/errors/expected/1009-errors-catch.stdout
Normal file
@@ -0,0 +1 @@
|
||||
catch result: 18
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
1
examples/errors/expected/1011-errors-value-failable.exit
Normal file
1
examples/errors/expected/1011-errors-value-failable.exit
Normal file
@@ -0,0 +1 @@
|
||||
60
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
value-failable result: 60
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user