Errors and testing
Error handling#
Faber separates three related ideas that many languages collapse into one shape:
| Construct | Meaning |
|---|---|
→ T | Normal success return channel |
T ∪ none | Absence in the success value domain |
⇥ E | Recoverable alternate-exit channel for errors |
Normal return#
fn porta(int x) → int {
if x ≺ 0 then return 0
return x * 2
}Failable functions#
Use ⇥ when a function can leave by an error channel:
fn divide(int a, int b) → int ⇥ string {
if b ≡ 0 {
throw "division by zero"
}
return a / b
}Throwing — throw#
throw sends a value on the error channel:
fn exigePositivum(int value) → void ⇥ string {
if value ≺ 0 {
throw "negative value"
}
}Guards — require / reject#
Guard statements pair a condition with a throw in one line. require
(English reader spelling require) throws when the condition fails —
it demands that the condition hold:
fn divide(int a, int b) → int ⇥ string {
require b ≠ 0 throw "division by zero"
return a / b
}reject (English reader spelling reject) is its boolean opposite: it
throws when the condition holds. Read it as an early rejection — the
happy path is whatever the condition rules out:
fn exigePositivum(int value) → int ⇥ string {
reject value ≺ 0 throw "negative value"
return value
}Both compile to the same shape as an if around throw — `reject cond
throw err is exactly if cond { throw err } — and, like throw` itself,
they require the enclosing function to declare a ⇥ channel.
Recovery — do / catch#
Callers recover locally with a do block and a catch handler:
fn divide(int a, int b) → int {
if b ≡ 0 then return 0
return a / b
}
fn tutum(int a, int b) → int {
do {
return divide(a, b)
}
catch err {
warn err
return 0
}
}A direct failable call is not an ordinary expression. Place calls to
→ T ⇥ E functions inside an active do / catch boundary.
The alternate channel in async surfaces#
The same ⇥ E channel rides inside Faber's async types rather than as a
separate mechanism. A async function returns promise<T> — the
infallible shorthand for promise<T ⇥ numquam> — and promise<T ⇥ E>
preserves the delayed alternate alongside the eventual value. Awaiting a
failable promise is itself a failable operation: the success value binds
inside a do / catch boundary while the failure stays observable, exactly
like a failable sync call.
Async and sync streams carry the channel too: async_generator → T ⇥ E makes every
pull a promise that can yield, end, or fail (the first failure ends the
stream, handled by for from); generator → T ⇥ E makes the stream call itself
failable, recovered with do / catch.
See Functions — two-channel promises for the full treatment.
Inline conversion recovery#
⊥ specifies an inline default value on ↦ conversions (⇥ only ever names
an error type):
main {
const string raw ← "42"
const _ n ← raw ↦ int ⊥ 0
}Effect-only failable#
For functions that error but do not return a success value, omit → T:
fn exigePositivum(int value) → void ⇥ string {
if value ≺ 0 {
throw "negative value"
}
}Current status#
→, return, ⇥, throw, and do / catch are live grammar and checker
surfaces. Rust and Go lowering for full ⇥ / throw / catch runtime
behaviour is still a backend gap — these pass type-checking but do not
yet emit failable runtime code to all targets.
Inline testing#
Faber has a first-class testing framework built into the language with three
keywords: describe declares a test suite, test declares a single test
case, and assert asserts a condition. Tests live alongside the code they
test — either in the same .fab file or in colocated *.proba test-source
files — run through faber test on the MIR stepper, and support the same
compiler pipeline as production code: locale-aware, type-checked, and
target-neutral.
The three keywords#
| Keyword | Role | Approximate equivalent |
|---|---|---|
describe | Declares a named test suite | describe, #[cfg(test)] mod |
test | Declares a single test case | it, #[test] |
assert | Asserts a condition at runtime | assert!, assert_eq! |
describe — test suite#
A describe block groups related test cases. Suites can be nested to
organise tests hierarchically:
describe "arithmetica" {
test "unum plus unum" {
assert 1 + 1 ≡ 2
}
test "multiplicatio" {
assert 3 * 4 ≡ 12
}
describe "implicata" {
test "comparatio" {
const _ x ← 10
assert x ≥ 10
}
}
}test — test case#
A test block contains the test logic. It can use any Faber code —
variable bindings, function calls, control flow — and ends with one or more
assert assertions. Tests can be tagged with an optional tag marker for
selective execution:
test "echo formats operands with one space" tag "coreutils" {
assert echo_textus(["hello", "world"]) ≡ "hello world"
}assert — assertion#
assert evaluates a boolean expression and reports failure if it is false.
An optional message string provides context on failure:
main {
const _ x ← 10
# Simple assertion
assert x ≻ 0
# With custom message
assert x ≡ 10 panic "x decem esse debet"
# Multiple assertions in sequence
const _ name ← "Marcus"
assert name ≡ "Marcus"
assert name ≠ "" panic "nomen vacuum non sit"
}Workflow#
Tests run through the faber test command, which executes test cases on
the MIR stepper — no Cargo or rustc is invoked for the package:
faber test # run all tests in the current package
faber test examples/coreutils/packages/echo # run tests for a specific package
faber test . --filter smoke # substring filter on case path or title
faber test . --include math # load only *.proba sources matching a path pattern
faber test . --exclude 'nested/*' # skip *.proba sources matching a path pattern
faber test . --name my_case # select by test name
faber test . --suite suite/path # select by describe suite path
faber test . --tag slow # select by tag modifierTests can live in the same .fab file as the code they test, or in
colocated *.proba files (the preferred home for stdlib and public-contract
suites). .proba files are test-only: discovered only by faber test,
never importable from product modules, and excluded from Cista package
snapshots. There is no separate test directory structure and no test module
declaration. The compiler knows which blocks are test code and which are
production code by the keywords used — describe and test are parsed but
excluded from production builds.
Warnings can be promoted to errors with --deny-warnings or
--deny <CODE> (repeatable), on faber test and faber build alike.
Real-world example#
The coreutils echo package demonstrates the testing framework in practice.
Tests live in the same file as the implementation, covering option parsing,
escape expansion, and edge cases:
describe "echo formatting" tag "coreutils" {
test "empty operands format as empty text" {
const list<string> words ← empty
assert echo_textus(words) ≡ ""
}
test "single operand is unchanged" {
assert echo_textus(["hello"]) ≡ "hello"
}
test "-E is a leading no-op option" {
assert echo_textus(["-E", "hello", "world"]) ≡ "hello world"
}
test "-n suppresses the trailing newline flag" {
assert echo_novam_lineam(["-n", "hello"]) ≡ false
}
test "-e expands the declared escape subset" {
assert echo_textus(["-e", "a\\nb"]) ≡ "a\nb"
assert echo_textus(["-e", "a\\tb"]) ≡ "a\tb"
}
}Design notes#
Several design choices distinguish Faber's testing framework from conventional approaches:
- No separate test binary. Tests are declarations in the same source file (or in
*.probatest sources), not a separate compilation target. The compiler filters test blocks from production output. - Tags, not directories. Tests are organised by
tagmarkers rather than directory structure. A test can belong to multiple organisational axes without being moved. - Full compiler pipeline. Tests are type-checked, analysed, and locale-aware — the same
--localeflag applies to test output. - Stepper-executed.
faber testruns test cases on the MIR stepper; no generated test crate, Cargo, or Rust toolchain is required. - Target-neutral. The stepper analysis is independent of any codegen target.
- Nested suites.
describeblocks can nest, mirroring the structure of the code they test.
References#
radix/corpus/probandum/— describe exemplar filesradix/corpus/proba/— test exemplar filesradix/corpus/adfirma/— assert exemplar filesexamples/coreutils/packages/echo/src/main.fab— real-world usage with tags