Glyphs and Latin
Glyphs and operators#
Faber uses glyphs where the symbol is structural. Below is the full inventory of source glyphs recognised by the lexer.
Value flow#
| Glyph | Meaning |
|---|---|
← | Runtime binding, reassignment, and mutation |
= | Compile-time assignment — values known while compiling |
→ | Function return type |
⇥ | Alternate exit — error-channel type |
⊥ | Default — inline value used when a conversion fails (↦ int ⊥ 0) |
∴ | Lambda joint — connects closure body to signature ((a, b) → T ∴ a + b) |
Type shape#
| Glyph | Meaning |
|---|---|
∷ | Static type ascription (compile-time cast) |
↦ | Runtime conversion (can-fail parse/coerce) |
∪ | Inline union type (T ∪ none) |
Comparison#
| Glyph | Meaning |
|---|---|
≡ ≠ | Exact equality and inequality |
≅ ≇ | Promoted exact equality — same value, compatible numeric types |
< > ≤ ≥ | Ordering |
≈ ≉ | Fuzzy equality — tolerance match with isclose defaults |
Logical and bitwise#
| Glyph | Meaning |
|---|---|
∧ ∨ ⊻ ¬ | And, or, xor, not |
⇐ ⇒ | Left and right bit shift |
Assignment updates#
| Glyph | Meaning |
|---|---|
← | Runtime assignment — the only assignment operator in expressions |
= | Compile-time assignment — a value fixed and known while compiling |
⊕ ⊖ | Postfix increment/decrement statements (mutable int only) |
Both are assignment. They differ in when the value is decided.
← stores a value at execution time: bindings, reassignment, mutation. =
attaches a value the compiler already knows — field shape inside a literal,
declaration metadata, annotation fields. Most languages spell both with =
and leave the reader to infer which is meant; Faber splits them, so the glyph
itself tells you whether anything happens at runtime.
class Punctum {
const int x
const int y
}
main {
const _ p ← Punctum { x = 10, y = 20 }
var int count ← 0
count ← count + 1
print p.x, count
}p ← … runs. x = 10 does not — it is the shape of the value being built,
settled before the program starts.
Worked through in full under The binding convention matters.
Optional chaining and non-null assertion#
| Glyph | Meaning |
|---|---|
? ?. ?[ ?( | Optional chaining |
! !. ![ !( | Non-null assertion |
Ranges#
| Glyph | Meaning |
|---|---|
‥ | Exclusive range endpoint |
… | Inclusive range endpoint |
Literal delimiters#
| Glyph | Type | Role | |
|---|---|---|---|
' | ascii | Fixed machine tokens | |
" | string | Line string | |
« » | string | Block string (guillemets) | |
` | forma | Captured template | |
| ` | ` | bytes | Hex literal |
§ | template hole | Placeholder inside "…", «…», … |
Punctuation#
| Glyph | Role |
|---|---|
( ) | Grouping and call |
{ } | Block, class literal, or JSON document |
[ ] | List literal and indexing |
. | Member access |
, | Separator |
; | Statement separator |
: | JSON field separator |
= | Structural field shape (not runtime assignment) |
@ | Annotation marker |
# | Line comment |
Vocabulary and structural glyphs#
Three signal choices that make Faber source recognisable at a glance.
Faber makes three deliberate signal choices that work together to produce source with stable grammatical shape. A reader can see the semantic role of every construct before knowing which target backend the code will be compiled to.
The three signals#
| Signal | Examples | Role |
|---|---|---|
| Type-first declarations | string name, int age | Shape reads toward binding — type, then name. |
| Behavioural words | fn, class, if, return, const | Declarations, statements, lifecycle, and behavioural intent. |
| Structural glyphs | ← → ∴ ≡ ∪ ⇥ | Value flow, type flow, and structural joints — universal, never localise. |
These three are designed to be mutually reinforcing. A reader who knows Faber in one locale can read it in any locale because the glyphs and structure never change. A reader who knows the Rust backend can still recognise the Faber source because the keyword vocabulary and type-first order produce a distinct visual register.
Type-first declarations#
Faber puts the type before the name in every declaration. This is the opposite of mainstream C-family syntax, and it is deliberate:
| Construct | C-family habit | Faber |
|---|---|---|
| Variable | int count = 0 | int count ← 0 |
| Function | fn greet(name: String) → String | fn salve(string name) → string |
| Parameter | (String name) | (string name) |
Type-first declarations mean the shape of data is the first thing the reader sees. This aligns naturally with languages that read left-to-right for semantic breadth — Chinese, Hindi, and Arabic declarations follow the same order.
fn divide(int a, int b) → int ∪ none {
if b ≡ 0 then return null
return a / b
}The behavioural vocabulary#
Faber gives every construct that has behavioural or grammatical shape a word.
The English reader surface is what the tables below show; the compiler's
canonical corpus keeps the same vocabulary under Latin spellings, which
faber convert --to la prints — a small, regular set drawn from a single
classical source rather than the mixed etymologies of most programming
languages.
Declarations#
| Keyword | Role | Approximate equivalent |
|---|---|---|
fn | Declares a named function or method | def, function |
class | Declares a concrete type with fields | class, struct |
interface | Declares a behavioural contract | interface, trait |
type | Declares a type alias | typedef, type |
union | Declares a tagged union | enum, sum type |
Bindings and transfer#
| Keyword | Role | Approximate equivalent |
|---|---|---|
const | Immutable binding (write-once) | const, final |
var | Mutable binding | let mut, var |
let | Concise inferred immutable | val, auto |
return | Return a value from a function | return |
throw | Throw on the error channel | throw, raise |
panic | Deferred — behaviour not yet expressible | unimplemented!, todo |
Control flow#
| Keyword | Role | Approximate equivalent |
|---|---|---|
if | Conditional branch | if |
elif | Else-if branch | else if |
else | Else branch | else |
while | While loop | while |
for | Iteration (values, keys, or range) | for |
switch | Pattern-match (first arm wins) | match, switch |
do | Try block with error recovery | try, do |
catch | Error handler for do | catch |
The vocabulary is bindable — it ships with the canonical pack but can be remapped through reader locale. A Thai programmer sees
ถ้าforif; a Chinese programmer sees函数forfn. The vocabulary is not privileged; only the grammar is.
Keywords are contextual: since Radix v0.79.0 there is no global reserved
keyword table. The lexer emits identifiers for all words and the parser
recognizes a keyword by spelling in its grammar position. A word is only a
keyword in the slot where it is expected; everywhere else it is an ordinary
identifier — you can name a variable if, a function fn, or a
parameter coalesce. A small residual set (catch, guard, for, yields,
throw, panic, assert, yield, main, async_main, import, from)
is still globally reserved.
Structural glyphs#
Where behavioural vocabulary uses your reader locale's words, structural meaning uses universal glyphs. These never localise and never change their meaning across renderings. They are the visual anchor that makes Faber source recognisable regardless of which human language the keywords are rendered in.
Value flow#
| Glyph | Meaning |
|---|---|
← | Runtime binding, reassignment, and mutation — the only assignment operator |
→ | Function return type declaration |
⇥ | Alternate exit: error-channel type |
⊥ | Default: inline value used when a conversion fails |
∴ | Lambda joint — connects a closure body to its signature |
Type shape#
| Glyph | Meaning |
|---|---|
∷ | Static type ascription — compile-time assertion about a value's type |
↦ | Runtime conversion — parsing or coercion that may fail |
∪ | Inline union type — connects two types (as in T ∪ none) |
Comparison and logic#
| Glyph | Meaning |
|---|---|
≡ ≠ ≢ | Exact equality and inequality — strict type match required |
≅ ≇ | Promoted exact equality — numeric widths join, then exact compare |
≈ ≉ | Fuzzy equality — tolerance match with isclose defaults |
< > ≤ ≥ | Ordering comparisons |
∧ ∨ ⊻ ¬ | Logical and bitwise: and, or, xor, not |
The binding convention matters#
One glyph choice deserves special attention because it is the most common point of confusion for new readers:
| Glyph | Role | Use for |
|---|---|---|
← | Runtime flow | Initial binding, reassignment, and mutation at execution time |
= | Structural shape | Field names inside literals and declaration metadata — not runtime stores |
Most languages overload = for both "define this field in a type"
and "put a runtime value in this variable." Faber splits those jobs. Every
← is live data flow; every = inside Type { … }
is class field layout.
# Runtime binding: ← attaches a value to a name
const int count ← 0
var string label ← "ready"
count ← count + 1
# Structural shape: = defines field values inside a literal
const _ p ← Point {
x = 10,
y = 20
}Compared to mainstream languages#
The table below shows how common programming language patterns map to Faber's three-signal system. The Faber column uses a different glyph or keyword for each distinct semantic job — no overloading.
| Semantic job | Common in other languages | Faber |
|---|---|---|
| Parameter type declaration | name: String | string name |
| Return type | → String, : String | → string |
| Runtime assignment | x = value | ← |
| Equality test | == | ≡ |
| Nullability | T?, Option<T> | T ∪ none |
| Branch + one statement | if (cond) return x | if cond then return x |
| Type cast | (T)value, value as T | value ∷ T |
| Conversion (may fail) | try_into() | value ↦ T |
References#
- EBNF grammar — full glyph and keyword inventory
- radix/corpus/ — language corpus with 304 exemplar files across all keywords
- radix/corpus/operatores/ — operator and glyph exemplars
- Commandments — the nine design laws that preserve these signals
Canonical vs sugar surfaces#
Multiple parseable surfaces, one semantic shape.
A recurring pattern in Faber's design: the language defines one canonical spelling for each construct, but accepts multiple sugar spellings that are semantically identical. The compiler does not prefer one over the other — both parse to the same AST node. The formatter decides which spelling to emit based on context and mode.
The rule: Sugar spellings are semantically identical to long form. Multiple surfaces parse to the same
HirAnnotationor type node.faber format --locale lare-emits canonical spellings; author mode preserves the sugar the author wrote.
Numeric type sugar#
Sized numeric types have one canonical spelling: the bare width marker
(i8 … u64, d64, inf, f16, bf16, f32, f64). The compact sugar
families below build on the markers. The choice between a container's long
form and its sugar is
per-module, not per-repository — a CLI package may use long form everywhere,
while a tensor kernel module uses sugar:
| Sugar | Canonical form | Domain |
|---|---|---|
tf32, tf32[4], ti64[2, 3] | tensor<f32, _>, tensor<f32, [4]> | Dense tensor — t + width + optional shape |
sf32, sf32[2, 3], si64[N] | sparsa<f32, _>, sparsa<f32, [2, 3]> | Sparse tensor — s + width + optional shape |
mf32[4, 4], mu32[3, 3] | matrix<f32, [4, 4]> | Register-class matrix — m + width + shape |
lf32, lu32, li64 | list<f32>, list<u32> | List — l + width |
General Faber (prefer long form):
main {
const list<f32> values ← empty
const tensor<f32, [2, 3]> grid ← empty
const i32 narrow ← 7
}Numeric modules (prefer sugar):
main {
const lf32 values ← empty
const tf32[2, 3] grid ← empty
const i32 narrow ← 7
}Sugar is type-position only. Value identifiers named f32,
tf32, or mf32 are unchanged — the compiler only
interprets these as sugar when they appear in type positions. A file that
consistently uses sugar should say so once at the top:
# STYLE: numeric sugar (tf32, mf32, sf32, lf32, lu32)Annotation sugar#
Faber annotations follow the same dual-surface model as numeric types.
Annotations are compiler-owned metadata attached to declarations — like
@ option for CLI option definitions or @ future for async functions
(legacy — prefer the async posture word in the signature slot).
Canonical form: a braced record with explicit field names:
@ option {
binding = verbose,
short = "v",
long = "verbose",
type = bool,
global = true,
description = "Enable verbose output"
}Sugar form: positional arguments and named aliases:
@ option verbose short "v" long "verbose" type bool global description "Enable verbose output"Both forms produce the same HirAnnotation record. The canonical
form is explicit and self-documenting; the sugar form is concise for
frequently-used annotations where the field order is well-known.
faber format --locale la re-emits canonical braced records; author mode
preserves the author's chosen form.
Author vs canonical formatting#
The faber format command operates in two modes that mirror the
canonical-vs-sugar principle:
| Mode | Command | Input | Output |
|---|---|---|---|
| Author | faber format | Parsed AST + leading trivia | Faber source preserving # comments, blank lines, and sugar spellings |
| Canonical | faber format --locale la | Analysed HIR + TypeTable | Normalised Faber — no comments, canonical spellings, no sugar |
Both modes run through the compiler's full front half (lex, parse, analyse for canonical). Invalid source produces compiler diagnostics — the formatter does not silently format broken input.
Key rules for both modes:
- Four-space indentation
- Stroustrup braces: opening
{on the same line as the controlling header - Author mode preserves the presence of blank lines but collapses runs of more than one
- Author mode does not insert blank lines the source did not contain
- Canonical mode normalises container sugar to long form (
tf32[4]→tensor<f32, [4]>) and annotations to braced records; width markers such asi32are already canonical - Canonical mode emits
T ∪ nonefor nullable unions,optionalfor optional parameters
Design principle#
The canonical-vs-sugar pattern appears in multiple places because it is a deliberate design principle, not a collection of one-off conveniences:
| Domain | Canonical | Sugar |
|---|---|---|
| Tensor types | tensor<f32, [4]> | tf32[4] |
| Annotations | @ option { binding = verbose } | @ option verbose ... |
| Formatting | faber format --locale la | faber format (author mode) |
| Reader locale | Latin (la) | Any locale pack |
The pattern serves two goals. First, it lowers the barrier to entry — new
users can write tf32[4] without typing
tensor<f32, [4]>. Second, it keeps the
canonical language unambiguous — when precision matters, the long form says
exactly what it means. The formatter bridges the two: authors write sugar,
reviewers can request canonical, and CI can enforce either.
References#
radix/docs/design/numeric-type-sugar.md— full sugar families, spelling preferencesradix/docs/design/annotation-sugar.md— dual-surface annotation modelradix/docs/design/faber-canonical-surface.md— author vs canonical format policyfaber/docs/EBNF.md— grammar tables for sugar forms