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#
| Type | Role | Example literal |
|---|---|---|
string | Unicode string | "Salve, munde" |
ascii | Fixed machine token | 'solum:lege' |
i32 | Signed integer | 42 |
f64 | Floating-point | 3.14 |
bool | Boolean | true, false |
void | Unit / no value | — |
none | Null / absent (the null value is null) | null |
instant | Duration / time instant | — |
json | Compile-time JSON value | { "key": "value" } |
bytes | Hex 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:
| Family | Widths |
|---|---|
| Signed | i8 i16 i32 i64 |
| Unsigned | u8 u16 u32 u64 |
| Decimal | d64 |
| Unbounded integer | inf |
| Floating | f16 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 = intGenerics#
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#
| Type | Role | Sugar |
|---|---|---|
list<T> | Ordered dynamic collection | lf32, lu32 |
map<K, V> | Key-value map | — |
tensor<T, Figura> | Dense fixed-shape buffer | tf32[4], ti64[2,3] |
sparsa<T, Figura> | Sparse fixed-shape buffer | sf32[4], si64[2,3] |
intervallum | Range 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:
| Form | Meaning |
|---|---|
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#
| Operator | Family | Behaviour |
|---|---|---|
≡, ≠, ≢ | Exact equality | Identical types required; null bypass |
≅, ≇ | Promoted exact equality | Numeric widths join, then exact compare |
≈, ≉ | Fuzzy equality | Tolerance match — isclose defaults (rel_tol 1e-09); numeric operands only |
<, ≤, >, ≥ | Ordering | Numeric, instant, scalar text |
within | Range containment | Numeric in range |
between | Collection membership | Element 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 =:
| Glyph | Role | Use for |
|---|---|---|
← | Runtime flow | Initial binding, reassignment, mutation |
= | Structural shape | Field 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#
| Form | Type | Role | ||
|---|---|---|---|---|
'…' | ascii | Fixed machine tokens; no §; no (…) | ||
"…" | string | Short Unicode line strings; (…) renders | ||
«…» | string | Block/multiline Unicode; (…) renders | ||
… | forma | Captured templates; (…) captures | ||
{ … } | json | Compile-time JSON document | ||
| ` | … | ` | bytes | Compile-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"
}