Grammar
The formal grammar for every Faber production, generated from the compiler's own specification. This is the authority on whether something is valid syntax; the target matrix is the authority on whether a given target supports it.
Uppercase names in the productions are lexical terminals. Grammar examples are fragments shown to illustrate a production — they are not standalone programs and are not expected to compile on their own.
Why the keywords below are in Latin. Everywhere else on this site, code
renders in your reader locale — fixum prints as const, and a Chinese
reader sees 常量. The grammar does not, because it is the canonical
definition rather than a rendering of it. Latin is Faber's canonical form
precisely because no living language has a claim on it, so no locale's
spelling has to be the one every other is defined against; each reader pack
is a projection of the productions below. The
reader locale pages give the mapping, and
faber explain <term> prints it from the compiler itself.
Program Structure#
Faber source files are raw text peeled by the driver before lexing. Optional TOML
frontmatter is not part of the token grammar. Within Faber syntax, spaces,
tabs, and newlines are trivia unless a production explicitly names NEWLINE.
Canonical forms are safe to compress onto one line. Any line-sensitive syntax is
explicitly sugar; a compressor must expand it when a lossless canonical mapping
exists, and otherwise preserve its boundary or reject compression. Line comments
remain line-oriented trivia and must be removed or relocated safely by a compressor.
fabFile := frontmatter? program
frontmatter := FRONTMATTER_DELIMITER NEWLINE TOML_LINES FRONTMATTER_DELIMITER NEWLINE?
program := statement*
statement := annotation* statementCore
statementCore := importDecl | bindingDecl | funcDecl | genusDecl | implendumDecl
| typeAliasDecl | enumDecl | discretioDecl
| ifStmt | whileStmt | iteraStmt
| eligeStmt | discerneStmt | guardStmt | curaStmt | facBlockStmt
| returnStmt | breakStmt | continueStmt | noopStmt | throwStmt
| assertStmt | outputStmt | incipitStmt | incipietStmt
| extractStmt | probandumDecl | probaStmt | blockStmt
| incDecStmt | exprStmt
bindingDecl := varDecl | sitDecl | arrayDestruct | objectDestruct
exprStmt := expression
blockStmt := '{' statement* '}'Uppercase names are lexical terminals. FRONTMATTER_DELIMITER is a line whose
trimmed content is exactly +++; TOML_LINES is the possibly empty sequence of
complete TOML lines before the closing delimiter. NON_NEWLINE_TOKEN means one
ordinary source token other than a newline. ANNOTATION_NAME and
ANNOTATION_FIELD_NAME are identifier spellings in annotation-owned contexts;
they include spellings that are keywords in other contexts. NO_NEWLINE is a
zero-width constraint requiring adjacent grammar parts to remain on the same
logical line.
File frontmatter (+++)#
When present, frontmatter must open on line 1 with exactly +++. A later line
that trims to exactly +++ ends the block. Bytes after the closing delimiter are
the Faber program. An empty body (whitespace only) is a valid empty program.
Frontmatter is parsed as a generic TOML document in the compiler driver — not
parsed as Faber statements. Authors may attach arbitrary metadata keys; tooling
reads known keys such as group, sectio, and [probanda] via accessors.
faber package tooling consumes those package keys. Package authority for
[package], [paths], and [build] remains faber.toml; conflicting
frontmatter values are rejected in package mode.
Example:
+++
group = "exempla.directiva"
sectio = "smoke"
+++
incipit {}Line-start § file directives were removed. Put file metadata in +++
frontmatter instead. Inside quoted strings, § remains the string-template hole
(see Call and Member Access below).
---
Declarations#
Variables#
varDecl := ('fixum' | 'varia') typeAnnotation IDENTIFIER (('←' expression) | ('↤' assignment inlineRecovery?))?
awaitVarDecl := ('figendum' | 'variandum') typeAnnotation IDENTIFIER '←' expression
sitDecl := 'sit' IDENTIFIER ('←' expression)?
arrayDestruct := ('fixum' | 'varia') arrayPattern '←' expression
objectDestruct := ('fixum' | 'varia') objectPattern '←' expressionfixum= immutable binding (write-once): it may be declared without an initializer and assigned exactly once later, then frozen.varia= mutable binding (reassignable), likelet.figendum/variandumawait apromissum<T>orpromissum<T ⇥ E>, bind the resolvedT, and propagate a compatible alternateE.- Use
_as the type annotation when the initializer determines the type:fixum _ name ← value sit name ← valueis sugar forfixum _ name ← value(inferred immutable local)sit name(no initializer) is sugar forfixum _ name— the inferred deferred immutable. Assign exactly once before any read.- Typed
fixum/variainitializers accept↤(fixum numerus x ↤ "42"): the written type is the conversion destination, then the binding is initialized.figendum/variandumkeep←;fixum _,sit, and untyped destructuring reject↤(no concrete destination type). - Deferred init:
fixum numerus xorsit xdeclares an uninitialized immutable slot that must be assigned exactly once before any read; a second assignment is rejected. The definite-assignment pass (semantic Phase 3a) enforces this.
Functions#
funcDecl := 'functio' IDENTIFIER genericParams? '(' paramList ')' funcModifier* callablePosture? returnClause? alternateExitClause? blockStmt
paramList := (parameter (',' parameter)*)?
genericParams := '<' genericParam (',' genericParam)* '>'
genericParam := IDENTIFIER | 'magnitudo' IDENTIFIER
callTypeArgs := '<' typeAnnotation (',' typeAnnotation)* '>'
parameter := ('de' | 'in' | 'ex')? 'ceteri'? typeAnnotation IDENTIFIER 'sponte'? ('ut' IDENTIFIER)? ('vel' expression)?
funcModifier := 'argumenta' IDENTIFIER | 'curata' IDENTIFIER ('ut' IDENTIFIER)? | 'errata' IDENTIFIER | 'exitus' (IDENTIFIER | NUMBER) | 'immutata' | 'iacit' | 'optiones' IDENTIFIER
callablePosture := 'fiet' | 'fiunt' | 'fient'
returnClause := '→' typeAnnotation
alternateExitClause := '⇥' typeAnnotation
stmtBodyJoint := 'ergo'
clausuraJoint := '∴'
clausuraExpr := compactClausuraExpr | legacyClausuraExpr
compactClausuraExpr := clausuraSignature clausuraJoint (expression | closureFacBlock)
clausuraSignature := (clausuraParam | '(' clausuraParams? ')') returnClause? alternateExitClause?
closureFacBlock := 'fac' blockStmt catchClause?
legacyClausuraExpr := 'clausura' clausuraParams? ('→' typeAnnotation)? (':' expression | blockStmt)
clausuraParams := clausuraParam (',' clausuraParam)*
clausuraParam := typeAnnotation IDENTIFIER- Return syntax:
→declares the normal success type. A bodyful function with no→is effect-only (vacuum) and must not containredde. A statement-bodied closure (fac { ... }or legacy block body) must also spell→ Tbefore it can useredde; expression-bodied closures may infer their result from the expression. - Recoverable alternate-exit syntax:
⇥declares the error-channel type. It can appear after→ Tor alone on an effect-only failable function or closure. A closure body that uses an escapingiacemust declare its own⇥ E; it cannot inherit the enclosing function's error channel. A localfac { ... } cape err { ... }may catchiacewithout an enclosing⇥. A failable function call (→ T ⇥ E) inside a⇥-declaring function propagates to the function's alternate exit without afac/capewrapper, mirroring how bare↦conversio andiacethrows already behave; the call lowers to Rust?. A closure must still declare its own⇥to propagate a failable call — the enclosing function's error channel does not cross the closure boundary. - Parameter prefixes:
de(read),in(mutate),ex(consume) - Post-name marker:
sponte(voluntary/optional provision) ceterimarks rest parametercurata NAME ('ut' LOCAL)?declares an allocator requirement;LOCALis the function-body alias.- Ordinary
functiodeclarations and genus methods require bodies. Signature-only methods belong inimplendum. errata NAMEis a legacy runtime-injectedignotumlocal, andiacitis a legacy marker with no current semantic effect. Neither declares the typed alternate-exit contract. New failable APIs should use⇥ E; whether either legacy modifier should survive is unresolved.ergois the compact statement-body joint only (one-statementsi/dum/casu/… arms).∴is the compact clausura joint only. The two are not aliases.- Compact closure block bodies must use
fac { ... }; a closure-localfacbody may attachcape, but cannot use postfixdum.
Classes#
genusDecl := 'abstractus'? 'genus' IDENTIFIER genericParams? ('sub' IDENTIFIER)? ('implet' IDENTIFIER (',' IDENTIFIER)*)? '{' genusMember* '}'
genusMember := annotation* (fieldDecl | methodDecl)
fieldDecl := 'generis'? 'nexum'? typeAnnotation IDENTIFIER 'sponte'? ('=' expression)?
methodDecl := 'functio' IDENTIFIER genericParams? '(' paramList ')' funcModifier* callablePosture? returnClause? alternateExitClause? blockStmtAnnotations#
annotation := bracedAnnotation | annotationSugar
annotationName := ANNOTATION_NAME
bracedAnnotation := '@' annotationName '{' annotationFieldList? '}'
annotationFieldList := annotationField (',' annotationField)* ','?
annotationField := ANNOTATION_FIELD_NAME '=' (expression | typeAnnotation)
annotationSugar := '@' annotationName NON_NEWLINE_TOKEN* NEWLINEBraced annotation records (@ futura { }, @ optio { binding = verbose, ... })
are canonical and compression-safe. Unbraced annotations are line-sensitive,
non-compression-safe sugar that consumes through NEWLINE; the newline is part
of this sugar grammar, not a general Faber statement separator. A compressor may
rewrite promoted families only when their named-field mapping is known. It must
otherwise preserve the line break or reject compression. Promoted sugar and
braced forms lower to the same HirAnnotation records. Unpromoted positional
families preserve raw arguments and do not yet have a lossless braced expansion.
The current Radix parser still accepts only a fixed token subset in unbraced
payloads and ends them with declaration-boundary heuristics rather than NEWLINE.
Those are implementation mismatches with this specification, not alternate
language rules.
Annotation contracts: @ annotatio (optionally @ annotatio { target = functio })
marks a top-level genus as a compile-time annotation contract. Ordinary genera
are not annotation schemas. Applications use @ ContractName { field = constant }
and resolve through local declarations or imported file-interface exports.
Resolved applications lower to HirAnnotation with contract_id: Some(DefId)
and constant field values. v1 attachment target is functio only; payload
scalars are textus, numerus, fractus, and bivalens (optional via
sponte or T ∪ nihil). No compiler-owned @ web / controller / route families.
JSON genera: @ json on a genus is a compiler-owned data-model contract,
not a generic annotation schema. Fields must be JSON-safe (textus, ascii,
numerus, fractus, bivalens, instans, nihil, lista<T>,
tabula<textus, T>, nullable T ∪ nihil, or another @ json genus). Field
metadata @ json { nomen = "wire_name" } changes the emitted object key used by
value ↦ valor, value ↦ json, and json ↦ Genus; JSON text remains a Norma
wire operation such as json.pange(value ↦ json).
@ radixis reserved for compiler-owned metadata. The historical morphology-stem meaning is retired; morphology remains a source naming discipline, not compiler-generated conjugation. Accepted directive forms are@ radix lane "air"/"mir"/"hir-direct"on top-level functions for explicit compiler-lane routing; unsupported lane/target combinations reject with diagnostics instead of being ignored.@ vertedefines codegen transformation (method name or template)@ nondum [TARGET] ["REASON"]marks a declaration as present in an interface but unavailable for the target@ cli "NAME"marks anincipitentry as a CLI program@ imperium "NAME"marks a function as a CLI command entry point@ optio NAME ...defines a CLI option; usetypus bivalensfor boolean flags@ operandus [ceteri] TYPE NAME ...defines a CLI positional argument@ futuramarks a function as async (legacy — preferfietposture word)@ cursormarks a function as generator (legacy — preferfiuntposture word)- Callable posture words (
fiet/fiunt/fient) are recognized in the signature slot after modifiers and before→/⇥/body; bare means synchronous finite @ publicaand@ privataparse as annotations but are not enforced; the compiler emitsWARN012(decorative visibility) so authors are not misled into expecting access control@ protectais reserved and rejected with a semantic diagnostic; it has no package, subclass, or sibling-file visibility meaning
sub= extends,implet= implementsgeneris= static,nexum= bound/property
Interfaces#
implendumDecl := 'implendum' IDENTIFIER genericParams? '{' implendumMethod* '}'
implendumMethod := annotation* 'functio' IDENTIFIER '(' paramList ')' funcModifier* callablePosture? returnClause? alternateExitClause?implendum is the contract construct: signature-only methods for implet
(gerundive of implere — that which must be fulfilled). Import namespaces are
.fab file boundaries; exported declarations live at file top level.
Type Aliases#
typeAliasDecl := 'typus' IDENTIFIER genericParams? '=' typeAnnotationEnums#
enumDecl := 'ordo' IDENTIFIER '{' enumMember (',' enumMember)* ','? '}'
enumMember := IDENTIFIER ('=' ('-'? NUMBER | STRING))?Tagged Unions#
discretioDecl := 'discretio' IDENTIFIER genericParams? '{' variant (',' variant)* ','? '}'
variant := IDENTIFIER ('{' variantFields '}')?
variantFields := (typeAnnotation IDENTIFIER (',' typeAnnotation IDENTIFIER)*)?Identifier Naming#
Faber keyword ownership is contextual per spelling. Outside a spelling's owning
contexts, that spelling may be an IDENTIFIER. An owning context may itself be
effectively global when its production applies everywhere a statement or
expression may begin.
Radix still emits globally reserved tokens for some spellings and selectively reinterprets them as identifiers. That is transitional implementation behavior; it does not replace the contextual language rule above.
Mixed-case lower-initial names are syntactically accepted but not
Faber-preferred for language, stdlib, host routes, or compiler-owned intrinsic APIs.
Prefer one word. If one word cannot carry the meaning, use snake_case only in
rare cases. If neither shape works, the method probably does not belong in the
core surface unless it is critical. Stdlib encode/decode uses the
mechanical verb trio pange / solve / tempta across modules — see
docs/stdlib/stdlib-mechanical-verbs.md. The public text library is
norma:chorda — see docs/stdlib/chorda-methods.md.
Imports#
importDecl := importRecord | importSugar
importRecord := 'importa' '{' importFieldList? '}'
importFieldList := importField (',' importField)* ','?
importField := importSourceField | importVisibilityField | importNameField
| importAliasField | importWildcardField
importSourceField := 'ex' '=' STRING
importVisibilityField := 'visibilitas' '=' visibility
importNameField := 'nomen' '=' IDENTIFIER
importAliasField := 'ut' '=' IDENTIFIER
importWildcardField := 'omnia' '=' IDENTIFIER
importSugar := 'importa' 'ex' STRING visibility? (namedImport | wildcardImport)?
visibility := 'privata' | 'publica'
namedImport := IDENTIFIER ('ut' IDENTIFIER)?
wildcardImport := '*' 'ut' IDENTIFIERExample:
importa ex "hono" privata Hono
importa ex "hono" privata Context
# Defaults to privata chorda.
importa ex "norma:chorda"
importa { ex = "norma:json/solve", ut = solve_mod }
importa ex "norma:consolum" privata consolum
# Kernel manifest glob.
importa ex "faber:*" privata faber
importa ex "lodash" privata * ut _
# Re-export.
importa ex "./types" publica UserMissing visibility defaults to privata. Missing named binding defaults to the
last import path segment when it is a valid, non-conflicting identifier. If the
inferred name is invalid or collides with an existing top-level binding, spell an
explicit nomen or ut binding.
importa ex "faber:*" privata faber is kernel-specific sugar: the glob lives
inside the import path string and expands the released binary's kernel manifest
into faber.<module>.<verb> calls. It is not the privata * ut name wildcard
form and does not create a runtime aggregate value.
---
Types#
typeAnnotation := ownedType ('∪' ownedType)*
ownedType := ('de' | 'in')? baseType
baseType := holeType | functionType | widthTypeSugar | qualifiedType typeArguments? | '(' typeAnnotation ')'
holeType := '_' | '∪'
qualifiedType := IDENTIFIER ('.' IDENTIFIER)*
typeArguments := '<' typeArgument (',' typeArgument)* '>'
typeArgument := typeAnnotation | NATURAL | '[' figuraList? ']'
widthTypeSugar := WIDTH_MARKER | LISTA_WIDTH_SUGAR
| (TENSOR_WIDTH_SUGAR | SPARSA_WIDTH_SUGAR | VECTOR_WIDTH_SUGAR) shapeSuffix?
| MATRIX_WIDTH_SUGAR shapeSuffix
shapeSuffix := '[' figuraList? ']'
figura := '_' | NATURAL | IDENTIFIER | '[' figuraList? ']'
figuraList := figura (',' figura)*
functionType := '(' typeList? ')' '→' typeAnnotation alternateExitClause?
typeList := typeAnnotation (',' typeAnnotation)*- Declaration parameters (
genericParams) and applied arguments (typeArguments) are distinct grammar categories. Applied arguments admit nested types and staticfiguravalues. - Type arguments admit the hole forms:
lista<∪>infers a heterogeneous element union andtabula<K, ∪>a heterogeneous value union;lista<_>keeps the monomorphic single-inhabitant hole. - Arrays are written
lista<T>. PostfixT[]is not accepted. de/inmark ownership (borrow/mut-borrow) on the immediately following union member. Parenthesize when grouping must be explicit.- Two hole kinds share the
holeTypeproduction._is the monomorphic hole ("infer exactly one inhabitant type"); the standalone∪is the union hole ("infer a finite multi-member union"). Both are legal wherever a base type is: bindings, returns, params, fields, and type arguments (lista<∪>,tabula<K, ∪>,→ ∪). - Lone-`∪` rule: a
∪hole consumes the whole type expression — any following∪is a parse error (A ∪ ∪,∪ Brejected, issueunexpected_cup_after_union_hole)._keeps today's behavior and may still appear as a binary-cup member (_ ∪ B). - Binary-cup disambiguation:
∪between two non-hole types remains the inline value-union operator (A ∪ B, nullableT ∪ nihil); the hole reading applies only when∪stands alone in a base-type position. - Inline union
T ∪ U(cup) for ad-hoc value unions;T ∪ nihilis the canonical nullable type form (lowers to Option<T>). - Unions are parsed as a flat member list; duplicates and
nihil-only cases are diagnosed in semantic lowering. sponteis a declaration marker (post-name on params/fields), never a prefix on types.- Qualified type paths such as
terminus.Terminusname a type through an imported namespace binding. The prefix must resolve to a namespace; the final segment must resolve to a type-bearing declaration.
Function types enable higher-order function signatures:
functio filtrata((T) → bivalens pred) → lista<T>
functio compose((A) → B f, (B) → C g) → (A) → C
functio apply((numerus) → numerus ⇥ textus op, numerus n) → numerus ⇥ textusPrimitive Types#
| Faber | Meaning |
|---|---|
textus | Unicode string |
ascii | ASCII-only string |
forma | captured template + params |
numerus | integer (default i64) |
modulus<W> | unsigned modular word; arithmetic wraps modulo 2^W |
fractus | float (default f64) |
bivalens | boolean |
nihil | null |
vacuum | void |
numquam | never |
ignotum | unknown |
octeti | bytes |
Sized primitives accept one optional width marker (not a user type parameter):
| Family | Markers | Invalid example |
|---|---|---|
numerus<W> | i8, i16, i32, i64, u8, u16, u32, u64 | numerus<f32> → use fractus<f32> |
fractus<W> | f16, f32, f64 | fractus<i32> → use numerus<i32>; bf16 is deferred |
modulus<W> | u8, u16, u32, u64 | modulus<i32> → signed widths are not modular words |
Bare numerus / fractus remain shorthand for numerus<i64> / fractus<f64>.
modulus<W> is a distinct semantic family: arithmetic does not mix implicitly
with numerus<W>, while explicit same-width conversion remains available.
Literals must be in 0..=2^W-1 (for modulus<u64> up to
18446744073709551615). Shift counts are themselves modular: x ⇐ W is a
full wrap. Cross-width modular arithmetic is rejected.
Generic Collections#
| Faber | Meaning |
|---|---|
lista<T> | array |
tabula<K,V> | map |
copia<T> | set |
promissum<T> | promise |
cursor<T> | iterator |
tensor<T, Figura> | dense homogeneous buffer with static shape Figura; numeric methods require numeric element types |
vector<T, N> | register-class numeric vector with static width N (single dimension, not buffer-backed) |
matrix<T, [R, C]> | register-class numeric matrix with exactly two static dimensions (not buffer-backed and not a tensor alias) |
atomic<T> | storage-sensitive atomic cell; v1 accepts i32 / u32 elements only and access must go through atomic methods |
sparsa<T, Figura> | sparse homogeneous buffer with static shape Figura; omitted coordinates equal zero; numeric methods require numeric element types |
A figura is _, a natural number, a size identifier, or a bracketed list of nested figura values; empty [] is rank-0. Bare tensor<T> is incomplete — use tensor<T, []> for rank-0 or tensor<T, _> to infer shape.
vacua for tensor<T, []> produces a rank-0 tensor (one default-initialized element slot).
vacua for sparsa<T, Figura> (any shape) produces an all-zero sparse tensor with no stored entries.
matrix<T, Figura> requires exactly two dimensions; bare matrix<T> and one- or three-axis matrix shapes are rejected.
atomic<T> requires T to be i32 or u32 in v1. Atomic cells are not interchangeable with their element type; use load, store, exchange, and compare_exchange receiver methods.
Construct multi-dimensional tensors via crea / structa / ↦.
Type(...) is not a construction form: vector<f32, 4>(...), matrix<f32, [2, 2]>(...), tensor<f32, [2, 2]>(...), and scalar forms such as numerus("42") are rejected. Use value ↦ Type, named library constructors, or Genus { field = value } records.
Tensor index/shape intrinsic slots (accipe, ponde, forma, crea, structa) accept integer lists that fit the canonical lista<numerus> / &[i64] runtime boundary at call sites (e.g. lista<u32> for GPU thread ids; not lista<u64>). This is a structural exception scoped to those slots — it does not widen the signed↔unsigned numeric lattice (see Index vector parameter policy in tensor-intrinsics.md).
Value unions use inline T ∪ U (nullable: T ∪ nihil). The standalone ∪ hole infers a multi-member union; _ infers a single inhabitant (see docs/design/type-hole-union.md). Tagged unions use discretio.
copia.unio() is a set method, not a type constructor.
Type Sugar#
Explicit long forms such as numerus<u32> and lista<numerus<u32>> are the
canonical spellings. Type sugar is an ergonomic alternate spelling for numeric
and collection types. It is type-position only and semantically identical
to the long form — the compiler treats both the same. This is the single
canonical reference for sugar; the rest of the specification uses long form.
Sugar combines a width marker with an optional one-letter family prefix. Width
markers are i8/i16/i32/i64 (signed), u8/u16/u32/u64 (unsigned),
and f16/f32/f64 (float). A bare width marker (no prefix) sugars the scalar
numeric type; a family prefix sugars a collection of that width. In the grammar,
WIDTH_MARKER is a bare marker; LISTA_WIDTH_SUGAR, TENSOR_WIDTH_SUGAR,
SPARSA_WIDTH_SUGAR, VECTOR_WIDTH_SUGAR, and MATRIX_WIDTH_SUGAR are that
marker prefixed with l, t, s, v, and m, respectively.
| Sugar | Long form | Bracket rule |
|---|---|---|
i8 … u64, f16/f32/f64 | numerus<W>, fractus<W> | none (bare marker) |
lf32, lu32, li64, … | lista<f32>, lista<u32>, lista<i64>, … | none |
tf32, tf32[2, 3], ti64[N] | tensor<f32, _>, tensor<f32, [2, 3]>, tensor<i64, [N]> | optional Figura |
sf32, sf32[2, 3], si64[N] | sparsa<f32, _>, sparsa<f32, [2, 3]>, sparsa<i64, [N]> | optional Figura |
vf32, vf32[4], vu32[3] | vector<f32, _>, vector<f32, 4>, vector<u32, 3> | optional single width |
mf32[4, 4], mf16[2, 2], mu32[3, 3] | matrix<f32, [4, 4]>, matrix<f16, [2, 2]>, matrix<u32, [3, 3]> | required, two dimensions |
Bracket shapes: [] is rank-0, [2, 3] is a fixed shape, and no bracket infers
the shape (_). Matrix requires exactly two dimensions. Sugar never uses <>.
For non-width element types (e.g. tensor<textus, [3]>), use the full form.
Sugar is reserved in type syntax only — value identifiers named tf32, lf32,
etc. are unchanged.
modulus<W> has no sugar; write modulus<u32> in full.
Spelling preference (author convention, not grammar): general Faber code tends toward long form for readability; numeric/tensor-primary modules may prefer sugar. Choose per module or file.
---
Control Flow#
Conditionals#
ifStmt := 'si' expression arm ('sin' ifStmt | elseClause)?
elseClause := 'secus' elseArm
arm := (blockStmt | stmtBodyJoint statement) catchClause?
elseArm := (blockStmt | stmtBodyJoint statement) catchClause?si= if,sin= else-if,secus= elseergofor one-statement bodies, includingergo redde,ergo iace,ergo mori, andergo tacet(∴is not accepted here)tacetfor explicit no-op (from musical notation: "it is silent")
Loops#
whileStmt := 'dum' expression (blockStmt | stmtBodyJoint statement) catchClause?
iteraStmt := 'itera' (('ex' | 'de') expression | 'ab' expression) ('fixum' | 'varia') IDENTIFIER (blockStmt | stmtBodyJoint statement) catchClause?dum= whileitera ex...fixum/itera ex...varia= for-of (values)itera de...fixum/itera de...varia= for-in (keys)itera ab range fixum/varia i= range iteration (e.g.itera ab 0‥10 per 2 fixum i { nota i };perbelongs to the range expression)
Switch/Match#
eligeStmt := 'elige' expression '{' eligeCase* defaultCase? '}' catchClause?
eligeCase := 'casu' expression (blockStmt | stmtBodyJoint statement)
defaultCase := 'ceterum' (blockStmt | stmtBodyJoint statement)Pattern Matching#
discerneStmt := 'discerne' 'omnia'? discriminants '{' variantCase* defaultCase? '}'
discriminants := expression (',' expression)*
variantCase := 'casu' patterns (blockStmt | stmtBodyJoint statement)
patterns := pattern ((',' | 'et') pattern)*
pattern := '_' | literal | (IDENTIFIER patternBind?)
patternBind := ('ut' IDENTIFIER) | (('fixum' | 'varia') patternBinding (',' patternBinding)*)
patternBinding := IDENTIFIER ('ut' IDENTIFIER)?Guards#
guardStmt := 'custodi' '{' guardClause+ '}'
guardClause := 'si' expression (blockStmt | stmtBodyJoint statement)Resource Management#
curaStmt := 'cura' STRING ('fixum' | 'varia') typeAnnotation IDENTIFIER blockStmt catchClause?Destructuring Extraction#
extractStmt := 'ex' expression ('fixum' | 'varia') extractFields
extractFields := extractField (',' extractField)* (',' restField)? | restField
extractField := IDENTIFIER ('ut' IDENTIFIER)?
restField := 'ceteri' IDENTIFIERControl Transfer#
returnStmt := 'redde' expression?
returnAwaitStmt := 'reddet' expression
awaitDiscardStmt := 'tacebit' expression
yieldStmt := 'cede' expression
breakStmt := 'rumpe'
continueStmt := 'perge'
noopStmt := 'tacet'reddetawaits a compatible promise and returns its success value from afietfunction.tacebitawaits a compatible promise to completion and discards any success value.cedeis statement-initial yield fromfiunt/fient; it is not an expression-form await.
---
Error Handling#
throwStmt := bareThrow | guardedThrowSugar
bareThrow := ('iace' | 'mori') expression
guardedThrowSugar := ('iace' | 'mori') expression NO_NEWLINE 'si' expression
catchClause := 'cape' IDENTIFIER blockStmt
assertStmt := 'adfirma' expression ('secus' expression)?capeattaches to the structured forms whose productions namecatchClause: conditional arms,dum,itera,elige,cura, andfac. It does not attach to arbitrary bare blocks.- Use the explicit do block when a standalone block needs a handler:
fac { ... } cape err { ... }. iace= throw (recoverable),mori= panic (fatal).- A same-line
si <expr>guard oniaceandmoriis line-sensitive parser sugar:iace val si conddesugars tosi cond { iace val }at parse time. Its canonical, compression-safe spelling is the expandedsiblock. A source compressor must expand this sugar before removing line breaks; the guarded shorthand remains under language review. adfirmais a runtime invariant check. It desugars conceptually tomori "msg" si !cond, with the positive condition kept in source form and the inversion applied during lowering.secusintroduces the false-path message, mirroring its role insi/secusand thesic/secusternary; this keeps the throw-family vocabulary (mori msg si cond,iace msg si cond) consistent and avoids the heterogeneous comma, which the grammar reserves for homogeneous list separators. Anadfirmafailure is fatal and uncatchable bycape(it lowers to a panic, not aResult-channel error); in test context the harness isolates eachprobaso a failed assertion ends that test without ending the suite.
---
Expressions#
Operators (by precedence, lowest to highest)#
expression := assignment
assignment := ternary ('←' assignment | '↤' assignment inlineRecovery?)?
incDecStmt := place ('⊕' | '⊖')
place := call (* semantic analysis requires an assignable target *)
ternary := or (('?' expression ':' | 'sic' expression 'secus') ternary)?
or := and (('aut') and)*
and := equality (('et') equality)*
equality := comparison equalityTail*
equalityTail := ('≡' | '≠' | '≈' | '≉' | 'est' | 'non' 'est') comparison
comparison := bitwiseOr (('<' | '>' | '≤' | '≥' | 'intra' | 'inter') bitwiseOr)*
# Ordering operators use Unicode glyphs; membership uses Latin keywords `intra`/`inter`
# (Faber prose identity). Glyph aliases such as `∈` are not in the active contract.
bitwiseOr := bitwiseXor ('∨' bitwiseXor)*
bitwiseXor := bitwiseAnd ('⊻' bitwiseAnd)*
bitwiseAnd := shift ('∧' shift)*
shift := range (('⇐' | '⇒') range)*
range := additive rangeTail?
rangeTail := ('‥' | '…' | 'ante' | 'usque') additive ('per' additive)?
additive := multiplicative (('+' | '-') multiplicative)*
multiplicative := coalesce (('*' | '/' | '%') coalesce)*
# `vel` is local nullable elimination (`T ∪ nihil vel T → T`), not logical `aut`.
# It binds tighter than arithmetic so `prefix + item vel ""` is `prefix + (item vel "")`.
# `velRhs` greedily consumes a following range tail, so `a vel b‥c` is `a vel (b‥c)`.
coalesce := unary ('vel' velRhs)*
velRhs := unary velRangeTail?
velRangeTail := ('‥' | '…' | 'ante' | 'usque') unary ('per' unary)?
unary := ('-' | '¬' | 'non') unary | fingeExpr | cast
cast := call ('∷' typeAnnotation | conversio)*
conversio := '↦' typeAnnotation inlineRecovery?
inlineRecovery := '⇥' unaryConversion-directed assignment (`↤` / conversio-assign): place ↤ value
evaluates the right side, converts it to the statically known type of the left
place through the existing ↦ route, then assigns. It binds at the same
precedence as ← and is right-associative; ⇥ inlineRecovery is **legal only
on ↤** — a ⇥ recovery after ordinary ← is rejected, and in a
right-associated ↤ chain the recovery attaches to the nearest ↤. The
operator is preserved verbatim through syntax and emission; it is never
rewritten to ← or ↦. Typed fixum/varia initializers accept ↤
(convert to the written type, then initialize); fixum _, sit, and untyped
destructuring have no concrete destination and are rejected.
est and non est inspect an existing value; they never convert it. Core type
spellings on the right perform runtime variant/type tests, while nihil,
verum, falsum, and ordinary value expressions use the value-test path. Radix
currently recognizes type targets through a fixed core-type vocabulary. Extending
that recognition to arbitrary declared types is a separate language decision.
Use ≡ / ≠ for structural value equality and ↦ for runtime conversion.
Retired predicate keywords are not prefix unary syntax. Use expr est verum,
expr est falsum, expr est nihil, expr non est nihil, expr < 0, or
expr > 0.
Static type ascription (`∷` / verte):
The ∷ glyph (U+2237, "proportion") explicitly ascribes a target type to an expression. Use it when the source expression already exists and the compiler needs a static target shape:
- Primitive/alias → cast (no runtime effect):
data ∷ textus→ TypeScript:(data as string) - Built-in collection → target-shaped collection value:
[1, 2, 3] ∷ lista<numerus> - Variant expression → enum/interface target ascription:
finge Click { x = 10 } ∷ Event
Prefer typed construction for ordinary genus values and vacua for ordinary empty collection values:
fixum _ point ← Point { x = 10 }
fixum lista<numerus> xs ← vacuaOnly the ∷ glyph is accepted as the postfix static type-ascription operator. The Latin forms qua, innatum, and novum were aliases and have been removed (see verte-alias-clean-break).
Runtime conversion (`↦` / conversio):
The ↦ glyph (U+21A6, "rightwards arrow from bar") is the runtime value conversion operator. Unlike ∷ (compile-time cast), this performs actual parsing/conversion that can fail:
"22" ↦ numerus→ Rust:"22".parse::<i64>().unwrap()"bad" ↦ numerus ⇥ 0→ Rust:"bad".parse::<i64>().unwrap_or(0)42 ↦ textus→ Rust:42.to_string()
Inline failure recovery uses ⇥ immediately after the conversio target (↦ T ⇥ recovery-expr). The unparenthesized recovery operand is a unary-precedence expression; parenthesize arithmetic, coalescing, ternary, or assignment recovery expressions. The recovery value must have type T.
Using vel as conversio recovery is rejected with a migration diagnostic. vel is local nullable elimination only (x vel y, parameter defaults) — not logical aut. A parenthesized conversio result may still combine with vel as ordinary defaulting.
Call and Member Access#
call := primary (callSuffix | memberSuffix | optionalSuffix | nonNullSuffix)*
callSuffix := callTypeArgs? '(' argumentList ')'
memberSuffix := '.' IDENTIFIER | '[' expression ']'
optionalSuffix := '?.' IDENTIFIER | '?[' expression ']' | '?(' argumentList ')'
nonNullSuffix := '!.' IDENTIFIER | '![' expression ']' | '!(' argumentList ')'
argumentList := (argument (',' argument)*)?
argument := 'sparge'? expressionString And Template Literals#
Faber uses delimiter semantics: each quote form means a different source shape. They are not interchangeable synonyms.
| Form | Type | Role |
|---|---|---|
'...' | ascii | fixed machine tokens; no §; no (...) |
"..." | textus | short Unicode line strings; (...) renders |
«...» | textus | block/multiline Unicode; (...) renders |
... | forma | captured templates; (...) captures |
{ ... } | json | compile-time object-rooted JSON document (: inside) |
\|...\| | octeti | compile-time hex bytes |
"..." ↦ regex | regex | compiled pattern from text conversion |
[ ... ] | lista<T> | Faber list (not JSON array, not bytes) |
§ (U+00A7) is a template hole in Unicode forms (", «, `). It cannot
appear in ascii literals.
Rendered templates (textus): "..."(...) and «...»(...) lower to
scriptum("...", args...).
Captured templates (forma): ...(args) captures template text and
parameters without rendering. Safe for bound SQL/URL payloads; do not use
«...»(...) for that job.
Block textus uses guillemets «...». The heavy quotation-mark
pair is retired (too visually close to " in many fonts).
Implementation status (2026-06-30):
- Shipped:
"...",«...»blocktextus,'...'→ascii,...→forma,|...|→octeti,{ ... }→json, and text/ascii↦ regex. - Pending factory delivery: slash-delimited
/.../regex literals.
Inline block example:
fixum _ tag ← «inline»Multiline block example (newline after opening «):
fixum _ blob ← «
select id, email
from accounts
»Captured template example:
fixum _ q ← `select * from accounts where id = §`(accountId)Octeti hex literal example:
fixum _ sig ← |de ad be ef|
fixum _ hello ← |48 65 6c 6c 6f|Format-Template Application#
String literal call syntax is the canonical source form for format-template application:
"status: § (§)"(sample_status(), "ok")
"status: §1 (§0)"("ok", sample_status())This lowers to the compiler's scriptum("...", args...) form. Use the string-template form in ordinary source; reserve scriptum(...) for explicit desugaring examples and compiler-facing documentation.
For textus, bracket indexing is Unicode-scalar based:
# Produces "§".
"Salve, §!"[7]
# Produces "hello".
"hello world"[0‥5]
# Produces "hello world".
"hello world"[0 usque 10]
# Produces "ace".
"abcdef"[0‥6 per 2]Text slices accept the full range form, including per.
For lista<T>, bracket indexing is a single-element access. The index must be
one integer; range slices are not accepted (use sectio(start, end) for a
copied range):
# Element at position i.
xs[i]
# Write element at position i.
xs[i] ← vLista bracket access is plain, not nullable: it returns the bare element
T and traps on out-of-bounds. This differs from tensor, whose bracket read
is accipe sugar and returns T ∪ nihil. For nullable list access, use
xs.accipe(i) → T ∪ nihil with vel.
For tensor<T, Figura>, bracket indexing is sugar over the tensor intrinsic
surface:
# vector.accipe([id])
vector[id]
# vector.ponde([id], v)
vector[id] ← v
# grid.accipe([r, c])
grid[[r, c]]
# grid.ponde([r, c], v)
grid[[r, c]] ← vReads return T ∪ nihil, matching accipe; use vel or another ordinary
option-handling form before arithmetic. Rank-1 tensors accept scalar integer
indices that fit the tensor i64 runtime boundary (u64 is rejected).
Rank-N tensors use a list-shaped index expression such as [[r, c]] or a
bound lista<integer> value. grid[r, c] is not syntax; memberSuffix still
contains exactly one expression between brackets.
octeti is a byte-buffer primitive, not an array, so bracket indexing is not
accepted on it (read or write). Byte access is method-based:
# → numerus<u8> ∪ nihil; nullable and safe on out-of-bounds.
buf.accipe(i)
# Append one byte in place.
buf.appende(b)
# Byte length.
buf.longitudoThis is deliberate. octeti is the opaque boundary byte buffer used by HAL,
crypto, and |hex| literals; its reads are nullable by default, and bracket
syntax is reserved for the trapping access model. For byte-heavy indexing, use
lista<numerus<u8>> internally (bracket read/write, trap on out-of-bounds) and
keep octeti at the boundary.
Primary Expressions#
vacua is a contextual empty-collection marker (identifier form, not a reserved keyword).
Use it with an explicit collection type: fixum lista<numerus> xs ← vacua or fixum tensor<fractus<f32>, []> t ← vacua.
literal := NUMBER | STRING | ASCII_STRING | BACKTICK_STRING | OCTETI_STRING
| 'verum' | 'falsum' | 'nihil'
primary := IDENTIFIER | literal | 'ego'
| arrayLiteral | jsonLiteral | typedConstructor | iunctaExpr
| adExpr | clausuraExpr | praefixumExpr | scriptumExpr | legeExpr
| '(' expression ')'
adExpr := 'ad' ASCII_STRING adOpener?
adOpener := '(' expression ')'
arrayLiteral := '[' argumentList? ']'
iunctaExpr := 'iuncta' typeArguments '[' argumentList? ']'
# Bare `{ ... }` is a JSON document literal. Keys are quoted JSON strings separated
# by `:`; values are JSON constants. Anonymous Faber objects (`{ key = expr }`)
# are retired (literal-family Stage 6). Genus construction uses `typedConstructor`.
jsonLiteral := '{' (jsonMember (',' jsonMember)* ','?)? '}'
jsonMember := STRING ':' jsonValue
typedConstructor := typeAnnotation '{' fieldList? '}'
fieldList := fieldInit (',' fieldInit)*
fieldInit := ('sparge' expression) | (fieldKey '=' expression) | IDENTIFIER
fieldKey := IDENTIFIER | STRING | '[' expression ']'
# JSON values: constants only (no Faber expressions, no variable references).
jsonValue := jsonObject | jsonArray | jsonString | jsonNumber | 'true' | 'false' | 'null'
jsonObject := '{' (jsonMember (',' jsonMember)* ','?)? '}'
jsonArray := '[' (jsonValue (',' jsonValue)* ','?)? ']'
jsonString := STRING
# Numerus when no decimal point or exponent is present; otherwise Fractus.
jsonNumber := NUMBERSTRING includes short strings delimited by " and block strings delimited by
« and ». '...' (ascii) and backtick
... (forma) are separate literal forms (see String And Template
Literals above).
A bare { ... } now produces an object-rooted JSON document of type json:
{ "name": "Alice", "age": 30, "active": true }. Keys are quoted JSON strings
separated by :; values are JSON constants only. Duplicate keys are an error
(second occurrence). Ascribing to tabula<K,V> lowers a real constant map.
Use ↦ valor for explicit widening to the broad dynamic carrier. Genus/variant
construction Type { field = expr } uses the Faber = grammar unchanged.
Special Expressions#
fingeExpr := 'finge' qualifiedIdent ('{' fieldList '}')? ('∷' typeAnnotation)?
qualifiedIdent := IDENTIFIER ('.' IDENTIFIER)*
praefixumExpr := 'praefixum' (blockStmt | '(' expression ')')
scriptumExpr := 'scriptum' '(' STRING (',' expression)* ')'
legeExpr := 'lege' 'lineam'?finge variant construction accepts a qualified variant path
(finge pkg.Bonum { … }), so an imported union's variants construct through
the import alias, and the ∷ cast is a full type annotation
(∷ pkg.Exitus) exactly as the general postfix ascription (uvf-u3).
∷ remains the general postfix ascription in cast. Rendered text templates
(STRING '(' argumentList ')') and captured forma templates
(BACKTICK_STRING '(' argumentList ')') use the ordinary call suffix. Regex
construction uses the ordinary conversio grammar: `(STRING | ASCII_STRING) '↦'
'regex'`.
Slash-delimited regex literals are not active grammar yet. / lexes as the
division operator, while // and /* ... */ are rejected as invalid comments.
Use "..." ↦ regex for compiled regex values.
---
Patterns#
objectPattern := '{' patternProperty (',' patternProperty)* '}'
patternProperty := 'ceteri'? IDENTIFIER ('ut' IDENTIFIER)?
arrayPattern := '[' arrayPatternElement (',' arrayPatternElement)* ']'
arrayPatternElement := '_' | 'ceteri'? IDENTIFIER---
Diagnostics#
outputStmt := ('nota' | 'vide' | 'mone' | 'scribe') expression (',' expression)*nota= neutral diagnostic note,vide= debug/inspect,mone= warnscribeis a diagnostic channel spelling; use current stdlib methods for real output
Comments#
Faber accepts line comments only: # through end of line. The # must be the
first non-whitespace token on the logical line (optional leading ASCII spaces or
tabs only — other Unicode space separators are not skipped by the lexer).
A # that follows any other token on the same line is a lex error with the
message # comments must start a line; move this comment above the code.
Valid line-start comments attach forward as leading_trivia on the following
statement or declaration (see comment-preservation). # inside string literals,
ascii literals, forma templates, and other delimited literals is not a
comment.
---
Entry Points#
entryHeader := ('argumenta' IDENTIFIER)? ('exitus' expression)?
incipitStmt := 'incipit' entryHeader blockStmt
incipietStmt := 'incipiet' entryHeader blockStmtincipit= sync entry,incipiet= async entry.argumentabinds parsed command-line arguments;exitussupplies the process exit expression. Their order is fixed byentryHeader.
---
Testing#
probandumDecl := 'probandum' STRING probaModifier* '{' probandumBody '}'
probandumBody := (praeparaBlock | probandumDecl | probaStmt)*
probaStmt := 'proba' STRING probaModifier* blockStmt
probaModifier := 'omitte' STRING | 'futurum' STRING | 'solum' | 'tag' STRING
| 'temporis' NUMBER | 'metior' | 'repete' NUMBER | 'fragilis' NUMBER
| 'requirit' STRING | 'solum_in' STRING
praeparaBlock := ('praepara' | 'praeparabit' | 'postpara' | 'postparabit') 'omnia'? blockStmt---
CLI Framework#
CLI metadata uses the ordinary reachable annotation* statementCore grammar.
The promoted cli, imperium, optio, and operandus families validate their
own named-field schemas after parsing.
Faber supports building CLI applications with automatic argument parsing and help generation.
CLI Entry Point#
@ cli "faber"
@ optio verbose longum "verbose" typus bivalens
incipit argumenta args {
# CLI framework automatically parses arguments
}CLI Options and Arguments#
@ imperium "deploy"
@ optio target brevis "t" longum "target" typus textus descriptio "Deployment target"
@ optio verbose brevis "v" longum "verbose" typus bivalens descriptio "Enable verbose output"
@ operandus textus file descriptio "File to deploy"
functio deploy() argumenta args {
# Arguments automatically parsed and passed
}---
Capability Calls#
Expression-form ad is the only supported ad surface. Legacy typed
ad "route" (args) → T { } and statement-level stream blocks
ad 'route' { meus/tuus … } are rejected at parse time.
The active adExpr production is defined under Primary Expressions. Its
ordinary postfix conversio materializes the resulting conversation handle.
- Route:
ASCII_STRING('solum:lege'), not double-quotedSTRING. - Opener: optional single
expression→ Requestdataasvalor. - Expression `ad`: blockless; evaluates to a
sermoconversation handle. Use postfix↦ T(materialization), assign tosermo, or open live directional views:s.meus<T>()(outboundda/fini) ands.tuus<T>()(inboundaccipe/cursor/exhauri/fini). Iterate inbound content frames withs.tuus<T>().cursor(), not directitera ex s.tuus<T>(). - Removed (parse error): legacy typed
ad "route"and blockmeus/tuusarms. - Types: compiler-owned
scrinium,status; opaquesermoconversation handle. sermo ↦ Tmaterializes inbound frames into one value of typeTusing the type-directed collector forT.
See docs/design/frame-stream-types.md.
---
Collection Operations#
The former ab collection pipeline DSL is retired. Collection filtering,
slicing, and aggregation are expressed through ordinary
textus/lista/tabula/copia methods and closures instead of a
grammar-level query expression. textus, numerus, fractus, lista<T>,
tabula<K,V>, and copia<T> are compiler-owned core types; their canonical
method surfaces are tracked in docs/design/textus-intrinsics.md,
docs/design/numerus-intrinsics.md, docs/design/fractus-intrinsics.md,
docs/design/lista-intrinsics.md, docs/design/tabula-intrinsics.md, and
docs/design/copia-intrinsics.md, not in Norma declarations.
prima and ultima are ordinary method names, not transform keywords. ubi is
not active collection syntax.
ex is used for iteration (itera ex items fixum x) and imports (importa ex "path").
---
Fac Block#
facBlockStmt := 'fac' blockStmt catchClause? ('dum' expression)?fac { ... }is the explicitdoblock and executes its body once.fac { ... } dum conditionis the post-test loop form; postfixdumattaches only tofac, not arbitrary preceding blocks.capeis an attachment shared by several structured forms, not a semantic mode owned byfac. A plainfacis often used when an otherwise unattached block needs a local handler:fac { ... } cape err { ... }.
---
Target Support#
Target support is not part of the grammar — this file defines only the language. For which grammar each compilation target lowers, and the runtime policy around it, see:
EBNF_MATRIX.md— generated grammar×target lowerability matrix (the official rows).docs/design/target-capability-matrix.md— runtime/contract policy (erase/warn/defer), pipeline routing, per-target contracts.
---
Keyword Reference#
| Category | Faber | Meaning |
|---|---|---|
| Declarations | discretio | tagged union |
fixum | const | |
functio | function | |
genus | class | |
implendum | interface contract | |
magnitudo | size/index generic parameter (in <> lists) | |
ordo | enum | |
sit | inferred immutable local | |
sponte | optional declaration slot (post-name) | |
typus | type alias | |
vacua | contextual empty collection marker | |
varia | let | |
| Control Flow | si / sin / secus | if / else-if / else |
custodi | guard | |
discerne | pattern match | |
dum | while | |
elige / casu | switch / case | |
fac | explicit do block / post-test loop | |
itera ex...fixum | for-of (values) | |
itera de...fixum | for-in (keys) | |
itera ab...fixum | range iteration | |
perge | continue | |
redde | return | |
rumpe | break | |
tacet | no-op (silence) | |
ergo | compact one-statement body joint | |
∴ | compact clausura joint only | |
| Error Handling | cape | structured local handler |
adfirma | assert | |
iace | throw | |
iacit | legacy marker; no current semantic effect | |
mori | panic | |
| Async | @ futura | async annotation (legacy; prefer fiet) |
@ cursor | generator annotation (legacy; prefer fiunt) | |
fiet | async finite posture | |
fiunt | sync stream posture | |
fient | async stream posture | |
figendum | await-bind immutable | |
variandum | await-bind mutable | |
reddet | await-return | |
tacebit | await-discard | |
cede | yield (fiunt/fient only) | |
| Endpoints | ad | capability call expression |
| Boolean | verum | true |
aut | or | |
et | and | |
falsum | false | |
non | not | |
vel | local nullable defaulting | |
| Objects | ego | this/self |
finge | construct variant | |
| Type Shape | ∷ | static type ascription / compile-time cast |
| Type Conversion | ↦ target | runtime value conversion |
↦ T ⇥ expr | conversio with inline recovery of type T | |
↦ numerus | parse to integer | |
↦ fractus | parse to float | |
↦ textus | convert to string | |
↦ bivalens | convert to boolean | |
| Bitwise | ∧ / ∨ / ⊻ / ¬ | and/or/xor/not |
⇐ / ⇒ | left/right shift | |
| Diagnostics | nota | neutral note |
mone | warn | |
scribe | diagnostic channel | |
vide | debug/inspect |
---
Critical Syntax Rules#
- Type-first parameters:
functio f(numerus x)NOTfunctio f(x: numerus) - Type-first declarations:
fixum textus nameNOTfixum name: textus - Iteration loops:
itera ex/de collection fixum/varia item { }oritera ab range fixum/varia item { }(verb-first, source, then binding) - Parentheses around conditions are valid but not idiomatic: prefer
si x > 0 { }orsi flag est verum { }oversi (x > 0) { } - Diagnostic keywords are statements, not functions —
nota xworks,nota(x)also works (parentheses group the expression), butnotais not a callable value