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' |
int | Signed integer (default i64) | 42 |
float | Floating-point (default f64) | 3.14 |
bool | Boolean | true, false |
void | Unit / no value | — |
null | Null / absent | null |
instant | Duration / time instant | — |
json | Compile-time JSON value | { "key": "value" } |
bytes | Hex byte sequence | \|00ff\| |
Sized numeric types#
int and float have default widths (i64 and f64) and explicit width
forms:
const int<i32> narrow ← 7 ∷ int<i32>
const int<u64> wide ← 255 ∷ int<u64>
const f32 single ← 1.5 ∷ f32Width sugar is available in type position: i8 … u64, f16, f32, f64
are equivalent to numerus<W> / fractus<W>.
Nullable types#
Nullable values use the union syntax T ∪ nihil:
fn find(string key) → int ∪ null {
return null
}
fn maybe() → string ∪ null {
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 implendum accept type parameters with
<T> syntax:
fn identitas<T>(T valor) → T {
return valor
}
fn primum<T>(list<T> res) → T ∪ null {
return res.primus()
}Explicit call-site type arguments are supported:
fn identitas<T>(T datum) → T {
return datum
}
const int seven ← identitas<int>(7)Collections#
| Type | Role | Sugar |
|---|---|---|
lista<T> | Ordered dynamic collection | lf32, lu32 |
tabula<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 | — |
copia<T> | Unordered set | — |
cursor<T> | Lazy stream | — |
promissum<T> | Async finite result from fiet functions; promissum<T ⇥ E> carries a delayed alternate channel | — |
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 |
const tensor<f32, []> scalar ← vacua
const tensor<int, [4]> row ← [1, 2, 3, 4] ↦ tensor<int, [4]>
const int ∪ null 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<int<i32>> cell, int<i32> value) → int<i32> {
return cell.exchange(value)
}Borrow markers on types#
Borrow markers (ref, mut, from) can appear on types in parameter
positions to indicate how a value is passed:
# shared borrow — caller retains ownership
functio imprime(de textus label) → vacuum { }
# mutable borrow — caller lends mutable access
functio duplica(in numerus value) → vacuum { }
# move — caller gives up ownership
functio consume(ex textus buffer) → textus {
redde buffer
}Comparison policy#
| Operator | Family | Behaviour |
|---|---|---|
≡, ≠ | Exact equality | Identical types required; null bypass |
≈, ≉ | Numeric value equality | Numeric lattice only |
<, ≤, >, ≥ | Ordering | Numeric, instant, scalar text |
intra | Range containment | Numeric in range |
inter | 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).
fixum — 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.
const int count ← 0
const string name ← "Marcus"
const list<int> inferred ← [1, 2, 3]Deferred initialisation:
main {
const int factor
if true {
factor ← 10
}
else {
factor ← 100
}
print factor
}varia — mutable binding#
var bindings are freely reassignable:
main {
var int count ← 0
count ← count + 1
count ← count * 2
}sit — inferred immutable sugar#
sit is sugar for fixum _ — an immutable binding with inferred type:
main {
const string salve ← "Salve"
const string nomen ← "Marcus"
const int x ← 42
# Deferred form
const string 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 {
int x
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 Point p ← Point {x = 10, y = 20}
}Ex field extraction#
from extracts fields from a value into local bindings:
class Persona {
string nomen
int aetas
}
main {
const Persona p ← Persona {nomen = "Marcus", aetas = 30}
const string nomen ← p.nomen
const int aetas ← p.aetas
# prints "Marcus"
print nomen
}Postfix increment and decrement#
⊕ and ⊖ are postfix increment/decrement statements for mutable
int places. They are statement-only — no expression value, no
prefix forms:
main {
var int i ← 0
# i becomes 1
i ⊕
# i becomes 0
i ⊖
}Collections#
Faber has several compiler-owned collection types. Their canonical methods live in the compiler, not in the standard library.
Lista — ordered dynamic collection#
const list<int> empty ← vacua
const list<int> numbers ← [1, 2, 3, 4, 5]
const list<string> names ← ["Marcus", "Julia", "Gaius"]
const list<list<int>> nested ← [[1, 2], [3, 4]]Spread with sparge:
const list<int> a ← [1, 2, 3]
const list<int> b ← [4, 5, 6]
const list<int> combined ← [spread a, spread b]
const list<int> headed ← [0, spread a, 99]Key methods: longitudo, accipe, appende, summa, primus, novissimus.
Tabula — key-value map#
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 vacua and
assign by key:
main {
var map<string, int> puncta ← vacua
puncta["alpha"] ← 1
puncta["beta"] ← 2
print puncta.longitudo()
}Tensor — dense fixed-shape buffer#
const tensor<f32, []> scalar ← vacua
const tensor<int, [4]> row ← [1, 2, 3, 4] ↦ tensor<int, [4]>
const int ∪ null first ← row[0]Tensor sugar (numeric-heavy code):
const tensor<f32, []> seed ← vacua
const tensor<f32, [4]> lanes ← seed.strue([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 (summa, productum).
Sparsa — sparse fixed-shape buffer#
const sparsa<f32, [2, 3]> sparse ← vacua
main {
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:
const tensor<f32, [2, 2]> dense ← [[1.0, 0.0], [0.0, 2.0]] ↦ tensor<f32, [2, 2]>
const sparsa<f32, [2, 2]> sparse ← dense ↦ sparsa<f32, [2, 2]>
const tensor<f32, [2, 2]> roundtrip ← sparse ↦ tensor<f32, [2, 2]>Cursors — lazy streams#
cursor<T> is a lazy stream type. Created from collection iterators,
tuus views, or generator functions. Consumed via itera ex:
const list<int> items ← [1, 2, 3]
main {
for from items const item {
print item
}
}Generator functions declare their stream posture in the signature slot:
fiunt is a synchronous stream and fient an asynchronous stream; the body
yields values with cede (see Functions — async and streams).
Intervallum — ranges#
main {
# 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 |
[ … ] | lista<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 nomen) → string {
return "Salve, §!"(nomen)
}
const int pagina ← 3
const int totum ← 10
const string code ← "200"
const string label ← "OK"
const string msg ← "Page § of §"(pagina, totum)
const string block ← "status: § (§)"(code, label)Key rules:
§(U+00A7) is the template hole- Positional holes:
§0,§1, … for explicit ordering - Trailing
!selects display formatting:"Salve, §!"(nomen) - The
(args)suffix is template application, not a function call
Block strings#
Multiline blocks use guillemets «…»:
const string 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:
const int user_id ← 42
const forma 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; ↦ valor
widens it to the dynamic carrier instead:
const json empty ← {}
const json user ← { "name": "Marcus", "age": 30, "active": true }
const json nested ← { "meta": { "version": 1 }, "tags": ["alpha", "beta"] }For typed genus construction, use the type name and = field shape:
class Point {
int x
int y
}
const Point 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 ∪ nihil#
Use T ∪ nihil when the value can be absent:
fn find(string key) → int ∪ null {
return null
}
fn divide(int a, int b) → int ∪ null {
if b ≡ 0 then return null
return a / b
}Optional declaration slots — sponte#
Use sponte 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 {
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 {
int ∪ null val
}
const Box ∪ null maybe_name ← Box {val = 7}
const int ∪ null name ← maybe_name!.valA non-null assertion on null aborts at runtime.
Nullish coalescing — vel#
const string ∪ null provided ← null
const string name ← provided coalesce "default"ignotum#
ignotum 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:
# runtime conversion
const int parsed ← "42" ↦ int
# static ascription
const int count ← 7
const string text ← count ∷ stringRuntime conversion — ↦#
Use ↦ for runtime conversion, especially parsing or coercion that may
fail. Supply inline recovery with ⇥:
const string input ← "9"
const int n ← "42" ↦ int
const int safe ← input ↦ int ⇥ 0Type-directed materialization:
const string path ← "/etc/hosts"
const vector<f32, 4> lanes ← [1.0, 2.0, 3.0, 4.0] ↦ vector<f32, 4>
const string body ← call 'solum:lege' (path) ↦ stringStatic ascription — ∷#
Use ∷ for explicit static type ascription. It is postfix and
target-type driven:
const int count ← 7
const int<i32> x ← 7 ∷ int<i32>
const string text ← count ∷ stringNullish coalescing — vel#
Use vel for nullish coalescing when a value is null:
const string ∪ null provided_name ← null
const string name ← provided_name coalesce "default"