Faber 1.14.0
| Field | Value |
|---|---|
| Product | Faber |
| Version | 1.14.0 |
| Tag | faber-v1.14.0 |
| GitHub | faber-v1.14.0 |
| Published | 2026-10-16 |
| License | MIT OR Apache-2.0 |
Install this version#
Pinned download for Faber 1.14.0. For the current release, use Start instead.
| Platform | Archive | Size | Checksum |
|---|---|---|---|
| macOS arm64 | faber-v1.14.0-aarch64-apple-darwin.tar.gz | 19.5 MB | sha256 |
| Linux x64 | faber-v1.14.0-x86_64-unknown-linux-gnu.tar.gz | 22.2 MB | sha256 |
curl -fsSL -o faber.tgz \
https://github.com/faberlang/releases/releases/download/faber-v1.14.0/faber-v1.14.0-aarch64-apple-darwin.tar.gz
tar -xzf faber.tgz
# The archive holds one file; the reader packs are built in.
sudo mv bin/faber /usr/local/bin/faber
faber --versionRelease notes#
Status: final — tagged
faber/v1.14.0on 2026-10-16
Faber 1.14 is the first release after the source went public. Programs can now touch the outside world: the file system, console, process, clock and entropy modules run under faber run and in generated Rust, and their failures are values with a kind and a path. The language gets typed holes and open records, an insertion-ordered ledger map, tighter escapes and templates, bytes as list<b8>, and checked axis markers on tensor row operations. Several of these are breaking changes; read that section before you upgrade.
Experimental through version 1. The language changes constantly, and breaking changes ship in minor releases. Faber 1.14 carries no stability promise, and neither does any other 1.x release. A 2.0 release is planned for when the language is largely stable.
Language#
Failures are values. The standard library reports failure as ⇥ IoFault, a union with one variant per kind of failure (NotFound, Permission, ConnectionRefused, TimedOut, and so on). A variant carries the path or address the program passed and no message text; platform text appears only in debug output. A caught fault prints as IoFault.NotFound { path = "/nonexistent/x" } on the runner and in generated Rust alike. Absence is not a failure: the end of input and an unset variable are ∪ none.
Holes and open records. A typed cell can solve its own holes: list<_>[1, 2] solves the element type, tuple<_, string>[1, "x"] solves one slot, and a bare [..] or _ [..] into a set cell builds the set. record<*> declares a record whose whole field list is left to inference, and a bare { field = value } literal with no cell to name it is such a record. (record<g: f32, *>, a partial list with a hole, is PARSE001 ratio_field_list_hole_not_sole.) An untyped empty literal, {} or [] with nothing to name its type, is a named error. The stated keyed constructor Type { "k": v } builds JSON and map<K, V> values.
main {
let list<int> xs ← list<_>[1, 2]
let tuple<int, string> pair ← tuple<_, string>[1, "x"]
let set<int> seen ← [1, 2, 2]
print xs.length(), seen.length()
print "§0 and §0 and §1"("a", "b")
print «say "hi"»
print pair
}2 2
a and a and b
say "hi"
tuple [1, "x"]Ledger. ledger<K, V> is an insertion-ordered map. The runner constructs it from a keyed literal, indexes, stores, deletes, counts, tests membership, loops in insertion order, compares (order matters for ≡), and prints it. Conversions, sort and the other wrapper operations are refused by name (SEM010 ledger_operation_not_implemented). Every other target refuses the type (CODEGEN001 ledger_type_unsupported_on_target).
main {
mut ledger<string, int> tally ← { "b": 2, "a": 1 }
tally["c"] ← 3
tally["a"] ← 10
for from tally at [name] let count {
print name, count
}
print tally.length()
}b 2
a 10
c 3
3Bytes are `list<b8>`. list<b8> and the bounded list<b8, N> are the byte types in the standard library, the corpus, the re-emitter and the grammar; faber format rewrites the older byte-string spellings to them. The scalar words byte and byte are retired (SEM008 retired_byte_word): write b8 for raw bits or u8 for a number. Converting a list to a bounded list checks the length and reports a recoverable failure. Converting a sized number to a bounded byte list is n ↦ list<b8, 4> via Le (or via Be), bit-exact on the runner and in generated Rust; the other targets refuse the row by name. x ↦ f32 rounds to binary32, and so does storing a float into a bare f32 local on the runner.
Text and escapes. A string literal may escape only its own delimiter, the backslash, n, t, r and u{…}; any other escape is LEX003 unknown_escape. A string that contains quotes can use guillemets, «say "hi"», with no escapes, and the lint WARN034 suggests that rewrite. Regex literals (regex /pattern/, or _ /pattern/ where the type is known) keep their body raw and round-trip through faber format. The pattern is not checked by faber check: an invalid one fails when the program runs.
Templates. A numbered hole §N selects argument N, counting from 0, and may repeat; a repeated hole evaluates its argument once on every target that supports templates. Every position from §0 up to the highest hole must be read: a gap in the numbering is SEM008 template_hole_index_gap, and an argument that no hole reads is SEM008 template_application_arity_mismatch.
Enums. ≺ ≻ ≤ ≥ order enum values by declaration order, and for from Color let c walks the members of an enum in that order.
enum Color { Red, Green, Blue }
main {
let Color a ← Color.Red
let Color b ← Color.Blue
print a ≺ b, a ≻ b, b ≥ a
for from Color let c {
print c
}
}true false true
Color.Red
Color.Green
Color.BlueInstants. ≅ and ≇ compare instants of different precisions.
Range containment. x ∈ 1‥5 and x ∉ 1‥5 now run on the runner and emit on every target except Haskell, which still refuses ranges.
Tensor row operations. sum, mean and softmax take the axis marker at the call: t.sum(axis 1), t.sum(axis 1, axis 2), or a named constant, t.sum(axis K). softmax takes an optional axis and only the last axis is legal (SEM010 softmax_axis_not_last). norm_rms and norm_layer require the marker too, as in input.norm_rms(axis 1, 1e-5, weight). rope_half and softmax_causal are source methods.
Grouped reductions. reduce … per [i, j] groups by a bracketed coordinate list, one key per result axis. The result extents come from the expected type, and a key list whose length differs from the result rank is SEM010 reducta_group_rank_mismatch. A key is either a coordinate, which folds that axis away, or a key tensor read at the loop coordinate, as in per [rows[i], cols[i]], which scatters each element into its bucket; a bucket nothing falls into keeps the operator's identity. t.sum(axis n) is sugar for this form and is rank-free, so the same call works on a rank-3 and a rank-4 tensor.
Sizes in calls. A generic function with size parameters takes them in the call's angle brackets, as natural numbers: take<2, 2>(x). const size N ← n lifts a run-time int into a size at the declaration (checked to be at least 0, and a snapshot of n); a size name reads back as an int and sits in extent positions, as in tf32[ROWS, N].
Ascribed literals. { field = value } ∷ Class builds the class and is the same value as Class { field = value }. A bare { field = value } stored into a cell of class type takes the cell's class, and a field the literal leaves out keeps its declared default. ∷ states a fact the compiler must prove: when the source type is not the target type and no declared contract relates them, the result is SEM010 nominal_ascription_not_provable.
Convert-assign with `via`. The convert-assign arrow takes the same via clause as ↦, so n ↤ x via Floor stores x into the int n rounded the way x ↦ int via Floor would.
fn take<size M, size N>(tensor<f32, [M, N]> value) → tensor<f32, [M, N]> {
return value
}
fn per_cell(tf32[2, 2, 3] cube) → tf32[2, 2] {
return reduce via sum from cube at [i, j, k] per [i, j] let s { return s }
}
class Calc {
mut bool flag = false
mut int ∪ none bonus = null
}
main {
let list<f32> flat ← [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0]
let tf32[2, 2, 3] cube ← flat ↦ tf32[2, 2, 3]
let tensor<f32, [2, 2]> sums ← take<2, 2>(per_cell(cube))
print sums
print sums.shape()[0]
let Calc a ← { flag = true, bonus = null } ∷ Calc
let Calc b ← { flag = false }
mut int n ← 0
let f64 x ← 3.7
n ↤ x via Floor
print a.flag, b.flag, a.bonus, n
}[6.0, 15.0, 24.0, 33.0]
2
true false none 3Comptime across modules. A comptime { … } initializer folds an imported constant and a call to an imported pure function. In 1.13 an import inside comptime was SEM064 praefixum_import_unavailable. A plain = initializer that calls an imported function is still constant_initializer_not_constant.
Diagnostics. The six translated reader packs (ar, hi, th-TH, vi, zh-Hans, zh-Hant) now translate the rows that used to repeat the English text, and the parser's "expected X" errors carry named issues instead of prose. New codes include LEX003 unknown_escape, WARN034, SEM008 template_hole_index_gap and PKG001 target_not_in_this_build. Smaller additions: LEX001 empty_regex_literal and unterminated_regex_literal; LEX002 float_overflow and float_underflow, for a float literal that overflows to infinity or underflows to zero; PARSE002 enum_value_out_of_range and json_integer_overflow, for an integer outside the signed 64-bit range; PARSE010 const_member_assign_initializer, ref_local_requires_initializer, ref_local_requires_name and ref_local_value_marker, for a malformed const member or ref local; PARSE030 expression_too_long, for an expression with more operators than the parser limit; SEM008 conversio_signature, for a @ conversion function that does not take one parameter and return one type; and SEM010 axis_reduction_repeated.
Tooling#
Compiled programs can do I/O. The standard library's file-system, console, process, clock and entropy modules run through native leaves, both under faber run and in a Rust package built with faber build --target rust; TCP, an HTTP client and a WebSocket client run under faber run. The 1.13 gate that stopped such builds (PKG001 package_host_selection_required) no longer fires for these modules. time.now() works under faber run. Text, encoding, TOML, digests and HMAC are ordinary Faber. No standard-library function is marked deferred any more, and the stub modules cache, compression, database, model, optimizer and yaml are deleted. The limits of this route are under Known issues.
`faber check --lower`. Checking a program normally stops at semantic analysis. With --lower the program is also compiled to Faber's reference form without being run, which reports the errors semantic analysis cannot see: a program that checks but cannot run. It needs a file or package path, not standard input.
Library location. --library-home <dir> on the command line and [paths] library_home in faber.toml select where optional libraries are found; the standard library is embedded and needs neither. The FABER_LIBRARY_HOME environment variable is ignored, and so are the other FABER_* and RADIX_* variables that used to change behaviour (cache home, support-path override, shuffle seed, Metal and device debug switches); each has a flag or a manifest key. There is still no package manager or library install command.
New and changed flags. faber run --shuffle-seed N, faber run --debug, faber run --threads N, faber test --solum-in NAME and faber corpus --filter TEXT. --threads is accepted only with --device cpu, and until the worker device lands it has no effect (faber prints a note saying so). The corpus runner reports a rejects outcome for programs that are meant to be refused, and timeout_secs now bounds only the run phase; setup phases have their own bound.
Generated code spells names differently. Parameters and locals are emitted with p_ and l_ prefixes in Rust, Go, TypeScript, Swift, Python and Haskell so that a Faber name can never collide with a target keyword; device kernels are k<id> and their buffers b<N>. Regenerate generated code; older output and this release's runtimes do not mix.
Targets. Go gets lazy generators (a range-over-function iterator), imported generators, and package builds that emit declarations from sibling modules; Go and TypeScript use the exporter's canonical spelling for imported names. Swift emits @escaping where needed, copies owned collection and class parameters on entry, and imports Darwin or Glibc by platform. Python gets maps, sets and set operations, anonymous records, interval values, the ascii and bytes conversions, and the reference Unicode script and property tables in its regex carrier. TypeScript bounded lists are native arrays with a length check, and every mut parameter is boxed so that a projected mut argument is written back through its place. LLVM reads shared fields of @ shared tagged unions. WebAssembly lowers scalar sqrt inline and handles recovered float-to-integer conversions. The Go runtime library gains stdio, process, clock and entropy leaves, which faber build --target go does not use yet. Metal and WGSL max and min reductions seed from element 0.
Performance. The reference runner compiles each regex literal once and borrows block terminators instead of cloning them. The C emitter writes an integer match as a switch, deduplicates interned literals and emits a one-piece template without a join, and the C runtime keeps a position index beside maps and sets, stores the byte and code-point counts of text in a header, and appends lists in bulk. The Cranelift runtime looks maps up by index and builds text in one allocation, MIR lowering mints one check helper per signature, and FHIR and FMIR artifacts are parsed and encoded without the extra round trips. These are code-level improvements checked against the corpus; no throughput figure is claimed.
`PKG001 target_not_in_this_build`. Asking a build for a target it was compiled without now answers PKG001 target_not_in_this_build instead of failing later. The released binary carries Rust, Faber, TypeScript, Go, Swift, Python, Haskell, FHIR, FMIR, WebAssembly, LLVM, Metal, WGSL and S-expression emitters. It also carries two preview targets. -t c writes readable C from a checked program, rejects constructs it cannot express by name, and builds with GCC or Clang; it now covers maps, sets, closures with the higher-order list methods, f32, optional chaining, standard-input reads, and ref and mut scalar parameters. -t clif writes a native object through Cranelift for a fail-closed subset, and faber build --target clif links it. The experimental Pascal, COBOL, Fortran, Odin and BASIC targets are not part of the binary.
GPU#
faber run --device cpu, --device metal and --device cuda run @ kernel functions; the Metal route works on Apple silicon (a two-element kernel gave the same result on both). The CUDA route is new: it launches kernels in process through the NVIDIA driver, with PTX that clang (NVPTX target) builds from the compiler's NVVM output at run time, and it needs no CUDA toolkit. On an RTX 5070 it ran the whole-tensor kernel programs (elementwise, matmul, scalar arguments and scalar returns) with output equal to --device reference. Lane-form kernels (control flow inside a lane, class parameters, tensor extents, math calls inside a lane) are not lowered for CUDA yet; a call to one fails closed and names the kernel. Without the NVIDIA driver, --device cuda fails closed with E_DEVICE_UNAVAILABLE. In this release the device gate admits class locals (constructed, read and written) inside lane-form kernels, Metal lowers elementwise add, subtract, multiply and negate in a kernel, a rank-1 right operand is admitted for matmul, and the max and min reductions on Metal and WGSL seed correctly. --device reference runs a kernel as plain sequential code on the runner, the oracle that device results are compared with. CPU, FMIR and runner timings are not GPU performance; no GPU throughput claim is made here.
Breaking changes#
- Binding words. Four words now say how a name is bound.
| Word | Meaning | Latin |
|---|---|---|
let T x ← e | a fixed name | let |
mut T x ← e | re-pointable and writable | mut |
ref T x ← e | re-pointable and read-only | ref |
const T X = e | a compile-time fact, set with = | const |
Every binding word writes its type slot. There is no short form: let x ← 3 is PARSE010 binding_type_slot_required. Write the type, let int x ← 3, or _ to have it inferred, let _ x ← 3. Rewrite old code by this table: a run-time const T x ← e becomes let T x ← e, and var T x ← e becomes mut T x ← e. A compile-time const T X = e stays as it is. The old spellings are errors: var is PARSE010 var_retired, and a run-time const T x ← e (Latin const) is PARSE010 runtime_const_retired. The product does not ship a migration tool, and faber format refuses a file that still holds the old words, so run faber check, which names every site, and rewrite by hand. A bare parameter is ref: the function may read it and may not write it. To write a parameter, mark it mut in the signature and at the call, grow(mut xs). A ref or mut name may hold T ∪ none when the other member is a reference type (a class, a collection or text). Over a number or other scalar it is SEM008 scalar_access_marker, bare or optional. The binders of a loop, from, match, sort, reduce and destructuring take let or mut. A const binder is PARSE010 binder_fixum_retired, and a ref binder is PARSE010 ref_binder_refused.
class Node {
const int LIMIT = 3
let string tag = "n"
mut int value
mut Node next optional
}
fn grow(mut list<int> xs) → void {
xs.append(9)
}
fn last(Node head) → int {
ref Node ∪ none cursor ← head
while cursor not is none {
if cursor.next is none {
return cursor.value
}
cursor ← cursor.next
}
return head.value
}
main {
let Node b ← Node { value = 2 }
let Node a ← Node { value = 1, next = b }
mut list<int> xs ← [1]
grow(mut xs)
print last(a), Node.LIMIT, xs.length()
} 2 3 2- Class fields. An instance field is
let T x = e(fixed after construction) ormut T x = e(writable). A field markedrefisPARSE010 ref_field_refused. The old read-only fieldconst T xwith no initializer isPARSE010 const_member_requires_initializer, andvar T xisPARSE010 field_modifier_missing; writeletandmut. A field writtenconst T X = eis now a type-level constant, not an instance field (see the next entry). - Class-level `static` members.
static(Latinstatic) members of a class are retired:static T X = ethere isPARSE010 static_member_retired. A type-level constant isconst T X = ein the class body, and you read it through the type,Tab.X. Storing into it isSEM020 assignment_to_const_member. State that changes belongs in amutfield. - Environment variables.
FABER_LIBRARY_HOMEand the otherFABER_*andRADIX_*switches listed under Tooling no longer do anything; use--library-home,[paths] library_homeand the new flags. - Unknown escapes. An unknown backslash escape in a string literal is
LEX003 unknown_escape; the old pass-through is gone. Use a guillemet string for text with quotes. - Empty literals.
{}and[]need a typed cell (a declared type or an inference site);let _ a ← {}isSEM016 inferred_constructor_no_expected_typeandlet list<_> a ← []isSEM014 cannot_infer_empty_list_element_type. - Templates. Numbered holes count from 0; a template that skips a number or leaves an argument unread is now an error.
- Row operations need the axis marker.
norm_rms(1, …)andnorm_layer(1, …)with a bare integer areSEM057 axis_marker_required; writeaxis 1.sum(1)andmean(1)with a bare integer are unknown methods. - Renamed methods.
rms_normisnorm_rms,rope_normisnorm_rope,layer_normisnorm_layer; the Vector, Matrix and Tensor elementwise methods aresubtracted,multipliedanddivided(the oldsubtract,multiplyanddivideon those types are unknown methods); the set methodsymmetric_differenceisdifference_symmetric, and the sparsenonzero_countiscount_nonzero. - Standard library signatures. File-system, process, console, net, HTTP and WebSocket calls report
⇥ IoFault; end of input (console.read_line,console.read_bytes) and an unset variable (process.read_env) are∪ none.process.spawnandprocess.detachno longer takedirectoryandenvironment.time.intervalreturns aTickerthat you read withtime.tick. Callers that matched on the old error types must be updated. - Removed library modules.
norma:cache,norma:compression,norma:database,norma:model,norma:optimizerandnorma:yamlare gone; importing one isPKG001 unknown_library_module. - Generated identifiers. Emitted names carry
p_andl_prefixes (see Tooling); anything that scraped generated source must follow.
Known issues#
Carried from 1.13 and re-checked against this build. Two 1.13 items are fixed (range containment on the MIR targets, and faber convert on files that import library types), and compiled programs can now do I/O, with the edges listed first below.
- Libraries cannot be installed. The graphics, autograd and UI libraries still have no install command and
faber doctorreports them not found;--library-homeonly points at a directory. - The I/O route has edges.
- Under
faber run, the*_asyncfile, clock and TCP wrappers are refused during lowering, an HTTP server and client in one program (a loopback) is refused, and a WebSocket client and server in one program can hang instead of finishing. - In generated Rust, a package that uses TCP, HTTP or WebSocket does not compile (
faber build --target rusthands rustc a program it rejects), and a few catch forms over an importedIoFaultdo not compile either. - Go and TypeScript packages build for pure code, but a package that uses the I/O modules is refused (
PKG001 leaf_unsupported_on_targetfor Go; the TypeScript build fails in code generation).faber targetsstill lists Go and TypeScript package compilation as unsupported even though pure-code packages build. - Records without a declared type do not run. A bare
{ field = value }literal checks and emits in Rust, but the runner stops withmissing type information for MIR lowering; writerecord<g: f32, w: int> { g = 1.0, w = 2 }to run one.faber check --lowerreports the program as one that checks but does not lower. - Printing a `list<b8, N>` on the runner writes the raw bytes instead of
[0x01, 0x02, …]; an unboundedlist<b8>prints correctly. - Some shipped example files are stale. Most of the loose example files under the standard library's
exempladirectory (thechordaandstdlib-nativumgroups) failfaber checkbecause they import module names that no longer exist, such asnorma:solum; three more fail on tensor calls or a missing export. The example packages there check clean. - Device limits. The full-size flood simulation completes on Metal, but its accuracy comparison against the donor program was withdrawn because no correct run can meet it; the reduced grid is still checked to 0.01 m. A throwing kernel can throw only a constant text per failure site. The CPU worker count has no effect yet.
- Regex engine gaps by target (carried, and narrower for Python): Go has no Unicode
\b; TypeScript clears a quantified group's captures at each iteration; Swift differs on line terminators under(?m), on\bnext to combining marks and on some case folds; Python rebinds the capture of a nullable repeated group to the empty text after an extra empty iteration, where the reference keeps the earlier value. TypeScript, Swift and Python are backtracking engines and can take exponential time on hostile patterns. Racket (sexp) and Haskell refuse regex constructs by name. - Haskell also refuses string templates and ranges.
- WebAssembly types a 32-bit float local as
f64. - Ledger runs only on the runner, and
ledger<K, V> {}with an empty brace body is refused as an unfinished operation; use a keyed literal.
Not in this release#
- A package manager and library install command. Cista has no published release.
- Ratio, plus-minus and superscript-power types and the set-algebra glyphs are designed but not built (
x²,±and∖are lexical errors). trap { }still returns a bare union of its result and error types. The built-intrapped<R, T>result type is registered in the compiler, but its name cannot be written in source yet.- The C and Cranelift targets are previews: they cover a subset of the language and refuse the rest by name. Neither runs under
faber run. - CUDA lane-form kernels. The flood simulation and the lane-form acceptance packages run on Metal and
--device reference, not on CUDA. - Radix stays at 0.84.0 (no separate Radix release).
- Public publication of the Go runtime module and
@faber/runtimeis still pending; neither is on its package registry.
Open source#
Faber's source is public at <https://github.com/faberlang/faber> under MIT OR Apache-2.0. The repository holds the compiler front end, the reference runner, the standard library (Norma), the grammar and locale packs, the corpus and a public test suite, the FHIR and FMIR artifact formats, the C generator and the Cranelift native backend, the two coding-agent skills (skills/faber for writing Faber and skills/faberlang for changing the repository), and the scripts that regenerate the grammar files and the compatibility matrices, with measurement for the columns whose backends are public (faber and runner), and it builds a working faber that checks, formats and runs programs. The released binary also carries the code generators that are not part of the public source (Rust, Go, TypeScript, Swift, Python, Haskell, LLVM, WebAssembly and the GPU emitters), which is why installing this release, not building the public tree, is the way to get every target. The repository takes no outside pull requests. Faber 1.14 is part of the experimental 1.x series, so treat the public source the same way: expect it to keep changing, and pin an exact version.
The previous public repository is now faberlang/faber-1.x and is archived. Its v1.x tags were carried into the new repository, so projects pinned to a 1.x tag keep resolving. History in the new repository starts on 2026-08-02. Releases continue to be published at <https://github.com/faberlang/releases>.
---