Renderingen-US

Errors as values, tests as declarations

Faber keeps failure in the type system instead of in an exception runtime. Three ideas that many languages collapse into one are written differently:

ConstructMeaning
→ Tthe normal success channel
T ∪ noneabsence inside the success value domain
⇥ Ea recoverable alternate exit — the error channel

A function that can fail declares the second channel after the arrow. It does not return a wrapper type the caller must unwrap; it returns its value by → and throws by ⇥. throw sends a value on the channel, and the guards require / reject are the one-line forms: require cond throw err throws when the condition fails, and reject is its boolean opposite. Callers recover locally with a do / catch pair, so a failure stays visible at the call site rather than travelling up an invisible stack.

What it looks like#

A fallible division, and two callers — one that succeeds, one that catches:

fn divide(int a, int b) → int ⇥ string {
    require b ≠ 0 throw "division by zero"
    return a / b
}

main {
    do {
        print divide(10, 2)
    }
    catch err {
        print err
    }
    do {
        print divide(1, 0)
    }
    catch err {
        print err
    }
}
$ faber run
5
division by zero

Nothing throws past the caller: the error value binds as err and the program keeps going. The compiler knows which call sites can fail, because the ⇥ channel is part of the function's type.

Tests are declarations#

The same "make it explicit" stance applies to tests. There is no separate test binary and no test module tree. Three keywords declare tests in the same file as the code — or in colocated *.proba sources — and faber test runs them on the MIR stepper, with no Cargo or rustc involved:

KeywordRole
describea named test suite, nestable
testone test case
assertan assertion that must hold
fn saturate(int x) → int {
    if x ≺ 0 then return 0
    if x ≻ 255 then return 255
    return x
}

describe "saturate" {
    test "clamps low" {
        assert saturate(-1) ≡ 0
    }

    test "clamps high" {
        assert saturate(300) ≡ 255
    }
}
$ faber test .
ok   src/main.fab::saturate/clamps low (0 ms)
ok   src/main.fab::saturate/clamps high (0 ms)
test result: ok. 2 passed; 0 failed; 0 blocked; 0 skipped

(The transcript shortens each case's path to the package root.)

Tests are type-checked and analysed by the same front end as production code, and the test blocks are filtered out of a production build. Because the runner is the stepper, a suite is target-neutral: the same tests run whatever backend the package is emitted for.

The full treatment — async error channels, inline conversion defaults, the faber test flags — is in Errors and testing.