Renderingen-US

Types and values

Data types#

Faber has a static, type-first type system. Every declaration places the type before the name — the string comes first, then the identifier it names, not the other way round. The type system covers scalar primitives, generic collections, sized numerics, tensors, and GPU-facing register types.

Primitive types#

TypeRoleExample literal
stringUnicode string"Salve, munde"
asciiFixed machine token'solum:lege'
i32Signed integer42
f64Floating-point3.14
boolBooleantrue, false
voidUnit / no value—
noneNull / absent (the null value is null)null
instantDuration / time instant—
jsonCompile-time JSON value{ "key": "value" }
bytesHex byte sequence\|00ff\|

Sized numeric types#

Write the width in type position. These are the numeric types — the same list as Math in the ether:

FamilyWidths
Signedi8 i16 i32 i64
Unsignedu8 u16 u32 u64
Decimald64
Unbounded integerinf
Floatingf16 bf16 f32 f64
main {
    const i32 narrow ← 7 ∷ i32
    const u64 wide ← 255 ∷ u64
    const f32 single ← 1.5 ∷ f32
}

The bare width marker is the type.

Nullable types#

Nullable values use the union syntax T ∪ none:

fn find(string key) → int ∪ none {
    return null
}

fn maybe() → string ∪ none {
    return null
}

There is no T? or Option<T> syntax in Faber. The union is explicit.

Type aliases#

type UserId = int

Generics#

Functions, type aliases, class, and interface accept type parameters with <T> syntax:

fn identitas<T>(T value) → T {
    return value
}

fn primum<T>(list<T> res) → T ∪ none {
    return res.first()
}

Explicit call-site type arguments are supported:

fn identitas<T>(T datum) → T {
    return datum
}

main {
    const int seven ← identitas<int>(7)
}

Collections#

TypeRoleSugar
list<T>Ordered dynamic collectionlf32, lu32
map<K, V>Key-value map—
tensor<T, Figura>Dense fixed-shape buffertf32[4], ti64[2,3]
sparsa<T, Figura>Sparse fixed-shape buffersf32[4], si64[2,3]
intervallumRange type—
set<T>Unordered set—
cursor<T>Lazy stream—
promise<T>Async finite result from async functions; promise<T ⇥ E> carries a delayed alternate channel—
main {
    const list<int> nums ← [1, 2, 3]
    const map<string, int> scores ← { "alice": 10, "bob": 20 }
}

Tensor types#

tensor<T, Figura> is the dense fixed-shape container:

FormMeaning
tensor<T, Figura>Canonical spelling
tensor<T, []>Rank-0 (scalar container)
tensor<T, _>Shape inference hole
tensor<T, [N]>Rank-1 vector
tensor<T, [N, M]>Rank-2 matrix
main {
    const tensor<f32, []> scalar ← empty
    const tensor<int, [4]> row ← [1, 2, 3, 4] ↦ tensor<int, [4]>
    const int ∪ none first ← row[0]
}

GPU core types#

These are recognised by the systems lane for GPU and register work. Package targets that lack hardware support reject them:

fn half(f16 x) → f16 { return x }

fn add(matrix<f32, [2, 2]> a, matrix<f32, [2, 2]> b) → matrix<f32, [2, 2]> {
    return a.addita(b)
}

fn swap(atomic<i32> cell, i32 value) → i32 {
    return cell.exchange(value)
}

Borrow markers on types#

Borrow markers (ref, mut, own) can appear on types in parameter positions to indicate how a value is passed:

# shared borrow — caller retains ownership
fn imprime(ref string label) → void {
}

# mutable borrow — caller lends mutable access
fn duplica(mut int value) → void {
}

# move — caller gives up ownership
fn consume(own string buffer) → string {
    return buffer
}

Comparison policy#

OperatorFamilyBehaviour
≡, ≠, ≢Exact equalityIdentical types required; null bypass
≅, ≇Promoted exact equalityNumeric widths join, then exact compare
≈, ≉Fuzzy equalityTolerance match — isclose defaults (rel_tol 1e-09); numeric operands only
<, ≤, >, ≥OrderingNumeric, instant, scalar text
withinRange containmentNumeric in range
betweenCollection membershipElement in collection

Variables and binding#

Faber has three variable keywords and a dedicated assignment glyph. The key distinction is between const (write-once) and var (freely reassignable), and between ← (runtime flow) and = (structural field shape).

const — immutable binding#

const bindings are write-once. They may be declared with or without an initializer; if declared without, they must be assigned exactly once before reading. A second assignment is rejected.

main {
    const int count ← 0
    const string name ← "Marcus"
    const _ inferred ← [1, 2, 3]
}

Deferred initialisation:

main {
    const int factor
    if true {
        factor ← 10
    }
    else {
        factor ← 100
    }
    print factor
}

var — mutable binding#

var bindings are freely reassignable:

main {
    var int count ← 0
    count ← count + 1
    count ← count * 2
}

let — inferred immutable sugar#

let is sugar for const _ — an immutable binding with inferred type:

main {
    const _ salve ← "Salve"
    const _ name ← "Marcus"
    const _ x ← 42

    # Deferred form
    const _ label
    label ← "deferred"
}

Runtime binding vs structural definition#

Faber splits what most languages collapse into =:

GlyphRoleUse for
←Runtime flowInitial binding, reassignment, mutation
=Structural shapeField names inside literals and metadata
class Point {
    const int x
    const int y
}

main {
    # Runtime: ← attaches a value to a name at execution time
    var int count ← 0
    var string label ← "ready"
    count ← count + 1

    # Structural: = defines field values inside a type literal
    const _ p ← Point { x = 10, y = 20 }
}

Field extraction (from)#

from extracts fields from a value into local bindings:

class Persona {
    const string name
    const int aetas
}

main {
    const _ p ← Persona { name = "Marcus", aetas = 30 }
    const string name ← p.nomen
    const int aetas ← p.aetas
    # prints "Marcus"
    print name
}

Mutable numeric updates#

Use binary + and - with runtime assignment ← to update mutable int places. Both operators take two operands; they are not postfix statements:

main {
    var int i ← 0
    # i becomes 1
    i ← i + 1
    # i becomes 0
    i ← i - 1
}

Collections#

Faber has several compiler-owned collection types. Their canonical methods live in the compiler, not in the standard library.

Lista — ordered dynamic collection#

main {
    const list<int> empty ← empty
    const _ numbers ← [1, 2, 3, 4, 5]
    const _ names ← ["Marcus", "Julia", "Gaius"]
    const _ nested ← [[1, 2], [3, 4]]
}

Spread with spread:

main {
    const list<int> a ← [1, 2, 3]
    const list<int> b ← [4, 5, 6]
    const _ combined ← [spread a, spread b]
    const _ headed ← [0, spread a, 99]
}

Key methods: longitudo, accipe, appende, sum, primus, novissimus.

Tabula — key-value map#

main {
    const map<string, int> scores ← { "alice": 10, "bob": 20 }
}

The : there is not map syntax. A bare { … } is always inline JSON — a compile-time json document whose keys are quoted strings separated by :. Declaring the binding as a map ascribes that document to a map type, which lowers it to a real constant map.

Faber's own key-value shape uses =, and it is only available on a named type: Point { x = 10 }. There is no anonymous { key = expr } object — writing one is a parse error, not a second spelling of the line above.

For a map you build up rather than declare whole, start from empty and assign by key:

main {
    var map<string, int> puncta ← empty
    puncta["alpha"] ← 1
    puncta["beta"] ← 2
    print puncta.length()
}

Tensor — dense fixed-shape buffer#

main {
    const tensor<f32, []> scalar ← empty
    const tensor<int, [4]> row ← [1, 2, 3, 4] ↦ tensor<int, [4]>
    const int ∪ none first ← row[0]
}

Tensor sugar (numeric-heavy code):

main {
    const tf32[] seed ← empty
    const tf32[4] lanes ← seed.from_flat([1.0, 2.0, 3.0, 4.0], [4])
}

Key methods: forma, accipe, ponde, crea, structa, strue, plus elementwise arithmetic, matrix multiplication (multiplicatio), and reductions (sum, productum).

Sparsa — sparse fixed-shape buffer#

main {
    const sparsa<f32, [2, 3]> sparse ← empty
    sparse.ponde([0, 1], 4.0)
    sparse.ponde([1, 2], 9.0)

    # accipe returns the stored value, here 4.0
    print sparse.accipe([0, 1])
    # count of stored entries
    print sparse.nonnihil()
}

Conversion between dense and sparse:

main {
    const tf32[2, 2] dense ← [[1.0, 0.0], [0.0, 2.0]] ↦ tensor<f32, [2, 2]>
    const sf32[2, 2] sparse ← dense ↦ sparsa<f32, [2, 2]>
    const tf32[2, 2] roundtrip ← sparse ↦ tensor<f32, [2, 2]>
}

Cursors — lazy streams#

cursor<T> is a lazy stream type. Created from collection iterators, recv views, or generator functions. Consumed via for from:

main {
    const _ items ← [1, 2, 3]
    for from items const item {
        print item
    }
}

Generator functions declare their stream posture in the signature slot: generator is a synchronous stream and async_generator an asynchronous stream; the body yields values with yield (see Functions — async and streams).

Intervallum — ranges#

# exclusive range: 0, 1, 2, 3, 4
for range 0‥5 const i {
    print i
}
# inclusive range: 0, 1, 2, 3, 4, 5
for range 0…5 const i {
    print i
}

‥ is exclusive range endpoint; … is inclusive.

String and template literals#

Faber uses delimiter semantics — each quote form means a different source shape. They are not interchangeable synonyms.

Literal forms#

FormTypeRole
'…'asciiFixed machine tokens; no §; no (…)
"…"stringShort Unicode line strings; (…) renders
«…»stringBlock/multiline Unicode; (…) renders
… formaCaptured templates; (…) captures
{ … }jsonCompile-time JSON document
`…`bytesCompile-time hex bytes
[ … ]list<T>Faber list literal

String-template application#

Faber formats text with string-template application: a "…" or «…» literal with § holes, then parenthesised arguments:

fn greet(string name) → string {
    return "Salve, §!"(name)
}

main {
    const int pagina ← 3
    const int totum ← 10
    const string code ← "200"
    const string label ← "OK"
    const _ msg ← "Page § of §"(pagina, totum)
    const _ block ← "status: § (§)"(code, label)
}

Key rules:

  • § (U+00A7) is the template hole
  • Positional holes: §0, §1, … for explicit ordering
  • Trailing ! selects display formatting: "Salve, §!"(name)
  • The (args) suffix is template application, not a function call

Block strings#

Multiline blocks use guillemets «…»:

main {
    const _ sql ← «
        select id, email
        from accounts
    »
}

Guillemets are the only block-string spelling since Radix v0.79.0 — the retired """ and ❝…❞ spellings fail as ordinary lex errors.

Captured templates (forma)#

Backtick templates capture text and parameters without rendering. Safe for bound SQL/URL payloads:

main {
    const int user_id ← 42
    const _ query ← `select * from users where id = §`(user_id)
}

Inline JSON#

A bare { … } is inline JSON: a compile-time json document, not an anonymous Faber object. Keys are quoted strings separated by :. Values are JSON constants only — no variable references, no Faber expressions. Ascribing one to a map lowers it to a real constant map; ↦ value widens it to the dynamic carrier instead:

main {
    const _ empty ← {}
    const _ user ← { "name": "Marcus", "age": 30, "active": true }
    const _ nested ← { "meta": { "version": 1 }, "tags": ["alpha", "beta"] }
}

For typed class construction, use the type name and = field shape:

class Point {
    const int x
    const int y
}

main {
    const _ p ← Point { x = 10, y = 20 }
}

Nullability and optionality#

Faber distinguishes absence in a value from optional provision at a declaration site.

Nullable values — T ∪ none#

Use T ∪ none when the value can be absent:

fn find(string key) → int ∪ none {
    return null
}

fn divide(int a, int b) → int ∪ none {
    if b ≡ 0 then return null
    return a / b
}

Optional declaration slots — optional#

Use optional after the name when a parameter or field may be omitted by the caller or constructor:

fn connect(string host, int port optional) → void {
}

class User {
    const string email optional
}

Borrow markers can combine with optional parameters:

fn process(ref int depth optional) → void {
}

Non-null assertion — !#

Use !., ![, !( to assert a nullable value is not null:

class Box {
    const int ∪ none val
}

main {
    const Box ∪ none maybe_name ← Box { val = 7 }
    const _ name ← maybe_name!.val
}

A non-null assertion on null aborts at runtime.

Nullish coalescing — vel#

main {
    const string ∪ none provided ← null
    const _ name ← provided coalesce "default"
}

unknown#

unknown is the top-level unknown type for escape hatches and incomplete knowledge. It is not a nullability mechanism.

Conversion and construction#

Two important conversion operators, one for runtime and one for compile-time:

main {
    # runtime conversion
    const _ parsed ← "42" ↦ int ⊥ 0
    # static ascription
    const int count ← 7
    const _ text ← count ∷ string
}

Runtime conversion — ↦#

Use ↦ for runtime conversion, especially parsing or coercion that may fail. Supply an inline default with ⊥:

main {
    const string input ← "9"
    const _ n ← "42" ↦ int ⊥ 0
    const _ safe ← input ↦ int ⊥ 0
}

Type-directed materialization:

main {
    const string path ← "/etc/hosts"
    const _ lanes ← [1.0, 2.0, 3.0, 4.0] ↦ vector<f32, 4>
    const _ body ← call 'solum:lege' (path) ↦ string
}

Static ascription — ∷#

Use ∷ for explicit static type ascription. It is postfix and target-type driven:

main {
    const int count ← 7
    const _ x ← 7 ∷ i32
    const _ text ← count ∷ string
}

Nullish coalescing — vel#

Use coalesce for nullish coalescing when a value is null:

main {
    const string ∪ none provided_name ← null
    const _ name ← provided_name coalesce "default"
}