Skip to content

Specification §1

Overview and philosophy

language-design.md §1 · 102 lines · 15 min read

A compiled systems language, focused on:

  • High availability and fault resilience (BEAM/Erlang inspiration)
  • Granular memory control without Rust’s cognitive cost
  • First-class concurrency that is simple and readable (Go inspiration)
  • Explicitness in everything: errors, memory, communication, ownership
  • Specification simplicity: Go and Zig prove that a simple spec equals an adoptable language

The language does not run on the BEAM VM. The compiler ships with its own runtime that natively implements process isolation, supervision and fault tolerance.

Inspirations:

Feature Inspiration
Fault tolerance / process isolation BEAM/Erlang
Concurrency and channels Go
Type system expressiveness Rust (without lifetimes)
Async/IO, comptime, integer support Zig
Dispatch by type Odin
General simplicity Go + Zig

The opt-in test (the anti-Rust-2 criterion)

Section titled “The opt-in test (the anti-Rust-2 criterion)”

The language is sophisticated where it needs to be (comptime, contracts, strategic memory management), and the natural question is how this does not become a “Rust 2”. The answer is a criterion applied to every new concept. What makes Rust expensive is not the number of concepts. It is that they leak into every program, all the time: lifetimes infect every signature, async colors everything, the borrow checker has an opinion on every line. They are inescapable and omnipresent.

The opt-in test: a concept is expensive if the programmer who does not use it still has to understand it in order to read someone else’s code or write their own. A concept you can ignore until you need it, and that does not appear in the code of those who do not invoke it, is cheap, no matter how sophisticated it is underneath. That is why unsafe, assert/assume, @requires and post-conditions (sections 5 and 14) can exist without turning the language into a Rust 2: they are all opt-in and none leak. Whoever writes safe code never meets them; they show up only when you go down to the metal, and stay invisible until then. The test separates “sophistication you summon” from “complexity that chases you”.

An honest caveat about the test itself: it measures what leaks into the code of those who do not use it, not what leaks into the mental model. You never write @mm, but to read any code you have to know that a pointer is born only from a var on the heap, that immutable aliases for free, that mut does not escape. That is memory semantics on the default path, without annotation, which every program silently carries. Some theory of each system (memory, processes) always leaks into the mental model: it is the inevitable floor, distinct from the leak-into-code that the test actually prevents. The test still holds; it just is not the whole story.

An honest distinction too: opt-in reduces cost, not size. The specification is small and consistent (few keywords, unified constructs, one rule per symbol); the language is large, with a lot of capability (comptime, contracts, strategic management, FFI, extensions). What opt-in guarantees is not that the language is simple. It is that it is consistent, and that what you do not use does not cost you. Selling “simple” would be a lie; what is sold is “consistent, and you only pay for what you summon”. And part of that honesty is naming the accepted frictions instead of discovering them later: catch has no short default that silently discards the error (you handle it or propagate it, verbose on purpose, section 7); decl costs a little on a quick skim compared to a dedicated struct/enum (the construct’s kind comes from the body, not the word, mitigated by tooling, section 2). These are chosen prices, not accidents.

The non-poisoning principle (minimal namespace)

Section titled “The non-poisoning principle (minimal namespace)”

Sibling of the opt-in test, one level below. The opt-in test protects whoever does not use a feature; non-poisoning protects whoever does not use a name. Everything that lives in global scope (every keyword, every builtin type, every always-available function) is imposed on every program, including those that never invoke it: it is cognitive cost and autocomplete/LSP noise for everyone. So global scope contains only the inevitable: what is woven into the grammar (keywords) and the types the core syntax presupposes (Result, Optional, Channel, the primitives). Everything that can be imported, is imported, functions included: panic lives in runtime, print in io, and a pure-computation lib never sees them. There are no exceptions “too fundamental to import”. An exception in the core is a design failure, and a well-thought-out core has almost none (it is part of what sickened languages that mixed a handful of builtins with “everything else is import”). It is the opt-in test taken to the namespace.

The core is designed complete and stabilizes slowly. The language is designed entirely before 1.0 precisely so as not to repeat the mistake of adding central pieces late, when a language constrains its own philosophy too much and then something new (generics arriving years later, for example) comes in polluting, because the rest was designed assuming that thing would never exist. Here the core is born closed; post-1.0, the language grows in libraries, not in syntax. The core changes little and slowly, and when it changes in an incompatible way, the migration tool (section 22) applies the mechanical fix. This pairs with the opt-in test: both protect the language’s coherence over time, one in space (features do not leak), the other in time (the core does not bloat).

We say “systems language” all the time and almost never define what a system is. The word is broad: a system can be the operating one (kernel, userspace), it can be the application (backend, frontend, database), it can be an AI system. Every language that calls itself “systems” answers that question in some way, almost always in an implicit way, never stating it. So the question comes before the label: what is a system? Only after answering it does “systems language” mean anything, and the answer is each one’s own.

Ours is deliberate, and it is precisely the thing that usually slips by unnoticed: a language is, itself, composed of several systems. Memory (MemorySource/MemoryManager, section 5), concurrency (supervision, processes, section 3), execution (async / sync, section 12), the type system, the FFI with C (section 17), the FFI with assembly (section 18), and the UI, which is the first compiler extension (section 20). Each one is separate and contained, a system apart. We are a systems language in the sense that we treat our own systems as separate.

And each system has its colors. Execution has two (async, sync); the type one has two (generic, non-generic); memory has several (the source, the strategy). Tainted coloring is when one system’s colors leak into another. It is what happens with async in almost every language: execution’s colors stain the type system’s. Now there exist async-types and non-async-types, in addition to generic and non-generic, and the type system carries a load that belonged to execution. This is exactly what the opt-in test and non-poisoning were protecting, now said in full: each system carries only its own colors. You only see a system’s colors when you interact with that system, not before, not in passing.

That is why we are a language of boundaries, and that is why it shows up in the interfaces philosophy (section 9): when one system touches another, the barrier is neutral, it carries no color. The memory interfaces do not say which memory; the async/IO ones do not say how it suspends. It is what lets UI and low-level FFI coexist without knowing each other: they are systems that do not touch, and when they do touch, they touch across a clean boundary.

The two failures we avoid are opposite. Rust leaks. Several systems spill into one another, and the bill arrives on reading: because the language is all tainted, the cognitive load is high right away. The start of the Rust book is a clothesline of “we’ll see this later”, and you carry on with a handful of poorly-explained concepts in memory, demystifying each one only near the end. It is not a language where you compose knowledge; it is one where you undo the confusion you already accumulated. It tires, and it is our answer to “why not a Rust 2”. Go fuses. It treats the language as a single “simple system” instead of several subsystems (the philosophy of being simple to the point where the least experienced programmer can already produce), and the price is rigidity: extending (generics, generators) forces you to touch the whole language, because what should be one system bumps into concurrency and execution, which were never isolated from it.

The core is the root from which the subsystems come out, and that is why we fight to keep it minimal: an addition to the core (a keyword, a primitive type) impacts all systems at once. And boundary discipline is not decoration: it is what lets you update one system (concurrency, say) without shaking the others, and what would let a new system, in the future, come in without rewriting the ones that already exist.

When several keywords name the same concept and differ only in the object on which the concept is exercised, they become one keyword (the concept) and the object disambiguates. match/switch/select all have the same form, keyword {object} { cases }, and the same act, commuting: match commutes structure, switch commutes values, select commutes outputs. What changes is who the object is and how it is written. So it is a single keyword, match, and the object (or its absence, in select) says the rest. The same held for for/while/loop (one loop, the condition decides) and for struct/enum/interface (one decl, the body decides, section 2); and the same move, in the decorators, fused @register/@register_group and the asm directives into @register and @asm.

The criterion is narrow, and getting it wrong breaks the language in both directions. “Same concept” means same act and same system, not same form. Form deceives at both ends: struct, enum and interface have bodies of different shapes and still fuse, because they are the same act (declaring a type) in the same system (the type one); union has a body identical to the product’s and still does not fuse, because it is another act (reinterpreting bytes) in another system (the unsafe one). Unifying never crosses the boundary between two systems: that would be, again, one color staining another.

And there is the opposite mistake, which looks like unification and is not. let/var/const, pub/priv, @pin/@unpin look like families ready to fuse: small, neighboring, from the same domain. But they are not N names for one concept; they are one axis with a default. let/var/const is the mutability axis; pub/priv, the visibility one; @pin/@unpin, the pinning one. Fusing an axis does not simplify: it erases the distinction it carries. Hence the two outcomes of the test: if it is the same act with different objects, unify and let the object disambiguate; if it is one act with an axis of modes, preserve the axis and let the names mark the positions.

Explicitness and ergonomics look like a tug-of-war (more of one, less of the other), but they do not share the same scale. Explicitness is marking intent without ambiguity; verbosity is how many characters. A short glyph marks intent as well as a long keyword, as long as it carries exactly one intent. That is why the language uses symbols that are terse and explicit: _ is “absence” (a single intent, in _ :=, _ =>, timeout(_)); and where there are two intents there are two symbols: assert (“I check the safety”) and assume (“I assume the safety”), never a single keyword sometimes checking and sometimes not (section 5). The test is not “word vs symbol”, it is: does this symbol carry one intent, without ambiguity? If so, it is short and explicit at the same time.

It is from this that the exact form of no-implicit-return (section 14) comes: what is forbidden is not “failing to write return”, it is the return by position, having to reason “this expression is the return because it is the last one”. The exit markers are all explicit, and not all are the keyword return: return (word), the ?: of the ternary and the => of the lambda (glyphs) mark the exit without depending on position. Delivering-by-glyph is as explicit as delivering-by-word; what is forbidden is delivering by position, and that does not happen anywhere.

The balance between explicit and ergonomic, then, is not paid by sacrificing one for the other: it is paid by requiring that each symbol have one intent, without ambiguity. Terseness comes from good symbols; explicitness comes from each symbol meaning one thing only.

The @ is the label of a subsystem (@mm = memory, @supervisor = concurrency, @cimport = FFI), not a convenient annotation for any feature. Hence the rule that contains proliferation: before adding a decorator to the core, the question is “is this really a subsystem with a boundary, or a feature dressed as an annotation?”. If it is not a subsystem, it does not get a core decorator: it becomes a keyword (if it is a core concept), a construct, or an ordinary function. The set of core decorators stays curated and small by construction: each one marks a real boundary.

And the growth of domain-specific decorators does not happen in the core; it happens in the extensions (section 20). The UI brings @state/@component; a future framework would bring its own; each one comes from the extension that defines and realizes it, declaring its capabilities. The defense against the explosion is then twofold: the core filters (only a subsystem becomes a core decorator) and the expansion lives in extensions (opt-in, capability-declared, outside the core), not in additions to the core. You never face them all at once: you face those of the subsystem you are in, or those of the extension you turned on.

The language has already stated that the core is born closed and stabilizes slowly and that we grow in libraries, not in syntax (non-poisoning, above). This hides a question worth stating in full: if not everything changes at the same speed, what decides the speed? The answer organizes the whole ecosystem into stability layers (the tiers), and the axis that separates them is not age nor area: it is how freely one can touch the contract. Time is only the symptom (what has an untouchable contract tends to last decades, what has a contract that churns lasts years), but the ruler is the contract. Each tier is a different promise about what can change, and about who owns the change.

Tier 0, the core. The floor: the patterns and algorithms that computer science and systems engineering have not changed in half a century. IEEE floats, integers, the TCP/IP protocol, the algorithm of a linked list, UTF-8 encoding, things whose contract is a consolidated standard of the field, not a choice of ours. You do not revise IEEE 754 nor rewrite what “linked list” means; what you do is add a new structure, a new protocol, never edit what is already set. You do not change TCP/IP: you create a new protocol alongside it. That is why tier 0 is add-only: it does not change because what it holds, by nature, does not change. And the language itself is the bedrock of this tier: the discipline of editions is add-only taken to syntax. You add form, essentially never break the old, and when the incompatible is inevitable the migration tool (section 22) applies the mechanical fix.

But “add-only” is about the observable contract, not the source code. A bugfix that makes the implementation start matching the already-promised contract is allowed; without that there would be no way to fix a security flaw in the core. What is forbidden is the opposite: changing a behavior someone already depends on, or removing what exists. Tier 0’s immutability is that of the promise, not the binary: the source fixes, the promise does not retreat.

Tier 1, the official libraries. A step above: what has undergone revision in the last ~three decades because it is specification, not standard. HTTP had three versions in that interval; TLS churns versions; a regex engine switches algorithm. They are not eternal standards like floats; they are contracts that evolve, so they patch faster than the core and have their own cadence (semver, changelog, bugfix). It is where http, regex, containers live. They come in the toolchain as a known-good snapshot (opt-in, zero weight until the use), but they update independently of it through the first-party registry (the distribution this requires is the tooling phase, section 22).

Tier 2, the frameworks. More volatile still: what reinvents itself on the scale of a decade, web frameworks, UI frameworks. It is where web (over http) and the UI superset (section 20) live. The compatibility promise here is short on purpose: whoever is in this layer accepts churning fast in exchange for expressive power.

Tier 3, third parties. The public packages, whose volatility is not ours to control. And here the axis changes in nature: the tier 2 tier 3 boundary is not “even more volatile”, it is a turn of ownership. From tier 0 to 2 it is we who give the compatibility promise; in tier 3 the promise is someone else’s, the semver is theirs. The correlation with volatility continues (a third party tends to churn more), but the real axis is who owns the promise: a third-party package can be even more stable than our tier 2 framework, and still be tier 3, because the guarantee does not come from us.

Tier What it is Churns in Who promises Examples
0, core field patterns/algorithms decades (add-only) us, untouchable the language, IEEE floats, TCP/IP, List/Map/Set, UTF-8
1, official libs specs that revise years (own cadence) us, versioned http, regex, containers, TLS, IANA
2, frameworks what reinvents itself fast us, short promise web, UI superset
3, third parties public packages beyond our control them, their semver any registry dep

Two refinements the time-axis hides. The first separates equally old things into different tiers: closed set vs open roster. collections (List/Map/Set) and containers (B-tree, ART, deque, ring buffer…) are both full of algorithms frozen for decades; a B-tree is as old as a list. What separates them is not the age of the algorithm, it is that List/Map/Set is a complete and closed set (there will never be a “fourth fundamental collection”), while containers is a roster that grows (ARTs today, another structure tomorrow). So containers is a tier 1 packaging tier 0 content: the roster changes (tier 1), each algorithm inside is frozen (tier 0), and it is precisely this that makes it an official lib and not core.

The second refinement: the tier cuts through the inside of one same concept, separating mechanism from data. UTF-8 encoding is frozen (tier 0); the Unicode tables, which gain characters every year, are tier 1. The UTC/offset clock is tier 0; the IANA timezone names, which change with countries’ policy, are tier 1. The sockets and IP of net are tier 0; the TLS versions on top, tier 1. The same concept splits: the part that is mechanism stays on the floor, the part that is mutable data moves up a step. It is no accident that this cut predicts decisions already made by instinct, the time core with UTC/offset and IANA as opt-in/heavy: when the model predicts choices you already made, it is a sign that it is real, not imposed.

And the stratification gives for free a dependency rule: tiers only depend downward. http (1) goes down to net/io/crypto (0); web (2) builds on http (1); the UI (2) on web; never the reverse. The core never imports an official lib; an official lib never sees a framework; and nothing of ours (0 to 2) depends on a third-party package (3). This is not folder tidying: it is the same boundary discipline from “Systems and boundaries”, now on the time axis. Just as one system does not leak color into another, a layer does not leak dependency upward. It is what keeps the core updatable without shaking what is above, and what lets a new layer come in tomorrow without rewriting the ones below.