Functions and control flow
Functions#
Functions in Faber are declared with fn, using type-first parameter
syntax and a glyph return type.
Basic syntax#
fn twice(int n) → int {
return n
}With an error channel:
fn parse(string input) → int ⇥ string {
return 0
}Examples#
# No parameters, no return
fn saluta() → void {
print "Salve, Mundus!"
}
# Parameter, no explicit return
fn dic(string verbum) → void {
print verbum
}
# Parameter and return type
fn duplica(int n) → int {
return n * 2
}
# Multiple parameters
fn adde(int a, int b) → int {
return a + b
}Return values#
Use return for normal returns:
fn porta(int x) → int {
if x ≺ 0 then return 0
return x * 2
}Bare return for void return type:
fn tace() → void {
return
}Async and streams#
Callable posture is a signature slot after modifiers and before → / ⇥
or the body. A bare function is synchronous finite; the posture words
declare the execution mode:
| Posture | Meaning | Typical return |
|---|---|---|
| (none) | Synchronous finite | T |
async | Asynchronous finite | promise<T> or promise<T ⇥ E> |
generator | Synchronous stream (yields via yield) | cursor values, optionally ⇥ E |
async_generator | Asynchronous stream (yields via yield) | async cursor values, optionally ⇥ E |
# Async finite — returns a promise
fn responde() async → int {
return 42
}
# Synchronous stream — yields values
fn stream() generator → int {
yield 1
yield 2
}Await forms bind or consume the eventual value:
| Form | Role |
|---|---|
await_const T x ← future | Await-bind immutable |
await_var T x ← future | Await-bind mutable |
return_await future | Await-return (async functions only) |
await future | Await and discard |
yield value | Yield a value (generator / async_generator only) |
fn responde() async → int {
return 42
}
async_main {
await_const int responsum ← responde()
await responde()
print "done"
}promise<T> is infallible shorthand for promise<T ⇥ numquam>;
promise<T ⇥ E> preserves an alternate error channel. Infallible widens
to failable; failable does not narrow.
The @ future and @ cursor annotations remain accepted compatibility
spellings, but the posture words above are the canonical surface — prefer
async over @ future, and generator / async_generator over @ cursor.
Two-channel promises#
A async function returns a promise — but the promise carries both
channels, not just the eventual value. promise<T> is the infallible
form: shorthand for promise<T ⇥ numquam>. promise<T ⇥ E> keeps the
delayed alternate channel alongside the success value, so a failable
async call fails exactly like a failable sync call — the error is delivered
with the result, not through a separate callback, channel, or thrown
exception. Awaiting a failable promise is itself a failable operation, so it
happens inside a do / catch boundary:
fn computa(int densitas) async → int ⇥ string {
if densitas ≺ 0 {
throw "invalid input"
}
return 7
}
async_main {
do {
await_const int value ← computa(3)
print value
}
catch err {
print err
}
}The await forms bind or consume both channels: await_const / await_var
await-bind the success value, return_await re-emits the promise from an async
function, and await awaits and discards either outcome.
Promises in streams#
The two-channel shape composes with generators. An asynchronous stream may
declare async_generator → T ⇥ E: every pull is itself a promise that either yields
T, ends, or fails with E, and the first failure ends the stream.
Iteration with for from handles the channel:
fn poll() async_generator → int ⇥ string {
yield 1
throw "link lost"
}
async_main {
for from poll() const lectio {
print lectio
}
}A synchronous stream may also carry an alternate channel (generator → T ⇥ E);
the stream call is then failable, so the consuming code handles it with
do / catch:
fn stream() generator → int ⇥ string {
yield 1
}
main {
do {
for from stream() const item {
print item
}
}
catch err {
print err
}
}Borrowing and mutability (ref, mut, from)#
Faber marks how a value is passed with short prepositions on parameters:
| Marker | Intent | Typical Rust lowering |
|---|---|---|
| (none) | Owned value | T by value |
ref | Shared borrow (read-only) | &T |
mut | Mutable borrow | &mut T |
own | Consume (move into callee) | T by move |
# Shared borrow
fn imprime(ref string label) → void {
print label
}
# Mutable borrow
fn duplica(mut int value) → void {
value ← value * 2
}
# Consume
fn consume(own string buffer) → string {
return buffer
}
# Owned
fn salve(string name) → string {
return "Salve, §!"(name)
}The same words (ref, from) are reused in other constructs — do not read
every from as "consume":
| Surface | Role |
|---|---|
ref string name on parameter | Shared borrow |
mut int count on parameter | Mutable borrow |
own string buffer on parameter | Move into callee |
for from items const item | Iterate values |
for ref map const key | Iterate keys |
from source const x, rest rest | Destructure fields |
import from "path" | Import from module |
Entry point#
The program entry point is main:
main {
print "ingressus"
}async_main is the async entry point — the body may await (await_const,
await_var, return_await, await) and call async / async_generator functions.
CLI entry point#
For CLI programs, main args receives parsed command arguments:
@ cli { name = "echo" }
@ description "Prints text"
@ operand { rest = true, type = string, binding = words }
main args args {
for from args.words const word {
print word
}
}Passing mode — optional#
optional marks a parameter that may be omitted by the caller:
fn connect(string host, int port optional) → void {
print host
}Control flow#
Conditional branching#
if / elif / else#
main {
const _ condition ← true
if condition {
# truthy branch
print "matched"
}
}With else-if and else:
main {
const _ score ← 85
if score ≥ 90 {
print "A"
}
elif score ≥ 80 {
print "B"
}
else {
print "C"
}
}Compact branch with then#
A single-statement branch body uses then:
fn classify(int b, bool ready, int value) → int ∪ none {
if b ≡ 0 then return null
if ready then return value
return null
}Iteration#
Values — for from#
fn inveni(list<int> items, int target) → int ∪ none {
for from items const item {
if item ≡ target then return item
}
return null
}Keys — for ref#
main {
const map<string, int> map ← { "unus": 1, "duo": 2 }
for ref map const key {
print key
}
}Range — for range#
for range 0‥10 const i {
print i
}While loops#
main {
const _ condition ← true
while condition {
# body
pass
}
}Guard sections — guard#
guard groups early-exit checks before a function's main body.
Each if clause is a sequential guard:
fn divide(int a, int b) → int {
guard {
if b ≡ 0 {
return 0
}
}
return a / b
}guard is not breakable in v1 — it is a guard rail, not a loop.
Pattern matching — switch#
switch selects the first matching arm:
fn describe(int value) → string {
switch value {
case 1 {
return "one"
}
case 2 {
return "two"
}
default {
return "many"
}
}
}Tagged union matching — match#
match exhaustively matches union variants:
union Exitus {
Bonum {
string nuntius
},
Malum {
string causa
}
}
fn refer(Exitus eventus) → string {
match eventus {
case Bonum const nuntius {
return nuntius
}
case Malum const causa {
return "Error: §"(causa)
}
}
}Try blocks — do / catch#
do opens a block that may throw, and catch recovers:
fn divide(int a, int b) → int {
return a / b
}
fn tutus(int a, int b) → int {
do {
return divide(a, b)
}
catch err {
warn err
return 0
}
}Generics#
Functions, type aliases, class, and interface accept type parameters
with <T> syntax.
Generic functions#
fn identitas<T>(T value) → T {
return value
}
fn primum<T>(list<T> res) → T ∪ none {
return res.first()
}Explicit call-site type arguments#
fn identitas<T>(T value) → T {
return value
}
fn primum<T>(list<T> res) → T ∪ none {
return null
}
main {
const _ seven ← identitas<int>(7)
const _ maybe ← primum<int>([seven])
}Generic class#
class Par<T> {
const T primus
const T secundus
}Size parameters#
size declares a size/index parameter in generic parameter lists:
fn crea<T, size N>() → tensor<T, [N]> {
return empty
}