A small language with six JSON-shaped value kinds, one loop, no functions,
and two output modes that are required to behave identically: code run
interprets, code build compiles through LLVM to a native binary.
user = { name = "ada", wins = [1, 2, 3] }
emit Length { value = user.wins } to core get n
assert n.value = 3
name = user.name
rounds = n.value
link "native_modules/console.so" as term
emit Print { value = "$name won $rounds rounds" } to term
Working on this repo (human or AI)? Read
AGENTS.mdfirst — build/test invariants, the native-module ABI, both hosting models, and the hard-won gotchas that are not derivable from the source.
old/is an archive. The directoryold/holds a different, earlier language that happened to share the name — constraints,∈, particles with declared schemas. It is kept for reference only. Nothing outsideold/refers to it, andold/README.mddocuments that archived language, not this one. If you arrived at a description ofcodeinvolving constraints or particles-with-schemas, you were reading the wrong file.
- Running it
- The language
- Handlers
- Errors
- Modules
- Tests
- One canonical layout
- What the language deliberately does not have
- The two output modes
- Repository layout
Prebuilt Linux x86_64 tarballs are attached to each release. To build from source you need Rust and LLVM 17:
cargo build --release # target/release/code
cargo test --workspace # runs every tests/*.code fixture in both modescode init # scaffold here; `code init demo` in ./demo
code run program.code # interpret a file
code run # ...or a project: ./main.code
code build program.code # -> ./build/program
code build # -> ./build/<this directory>
code build program.code --target wasm # -t; exe | shared | static | wasm
# ...also writes host.mjs beside it
code build program.code -o out/thing # --output is the same flag
code build program.code --release # -r; -O2, the default is unoptimized
code test # run every fixture in ./tests
code test tests/parser.code # ...or just the ones you name
code format src/ program.code # canonical layout, rewritten in place
code format --check tests/ # writes nothing; non-zero if any differ
code install console # fetch a module into ./.code/modules
code install dom --platform wasm32 # ...the archive a browser build links in
code list # what's installed
code uninstall console
code --help # or `code help build`, `code build -h`
code --versionrun and build take either a file or a directory, and default to
.. A directory means its main.code. Artifacts always go in a build/
directory beside what you named, called after it — code build src/x.code
writes src/build/x, code build demo writes demo/build/demo — so a build
is never loose next to the source and deleting one is deleting one
directory.
code init writes three files and nothing else: a main.code that runs
as written (the obvious template prints, printing needs a module, and a new
project whose first act is a failed link is a bad first minute), an empty
.code/lock.json — .code/ is what marks the project root that link and
code install resolve against — and a one-line .gitignore for the
installed binaries and build/, keeping the committed lockfile and dropping
what it can reproduce. An existing file is a
refusal, never a merge.
The LLVM backend is a Cargo feature (llvm, on by default). Without it you
get an interpreter-only build — which is what
crates/code-wasm is, the engine behind the
playground. code build also needs a
system cc at runtime, since it hands off final linking.
| to end of line. One character, because there is nothing to tell it apart
from — | is not an operator in this language.
| this is a comment
x = 1 - 2
Six kinds, exactly JSON's: Number (an f64), String, Boolean,
Null, Array, Object. There are no type keywords, and nothing a
declaration says about a kind is checked; a binding holds whatever it was
last assigned. Those six names are the language's whole vocabulary for a
kind — they are what ∈ tests, what a type mismatch reports, and the only
names a particle may not be given.
That correspondence is a commitment, not a coincidence: the set is closed,
and it stays JSON's. New capability is expressed with these six rather
than beside them — a particle is an Object carrying a _class field, a
linked native module's alias is an Object of its constants, a core handler's answer is
an Object. There is no seventh kind coming, and the two containers stay two:
folding Array and Object into a single ordered map was considered and
rejected (docs/todo/README.md), because +, serialization and the native
layout all still have to ask which one they are holding.
n = 2.5
s = "hi"
b = true
z = null
xs = [1, "two", [3]]
obj = { x = 1, nested = { y = 2 } }
An object field is written name = value. The name is bare when it looks
like an identifier, quoted when it does not, and built while the program
runs when the quotes contain an interpolation:
header = "Content-Type"
request = {
url = "https://example.com",
"X-Count" = 3,
"$header" = "application/json"
}
assert request["Content-Type"] = "application/json"
One rule underneath the three spellings: a key is a name, and a name is text. Values are any expression, so unlike strict JSON an object literal can reference variables — and since 2026-08-29 so can a key.
The value model is still exactly JSON's, but the syntax is no longer JSON's:
"$obj" renders {"name":"ada"}, which is what a JSON reader wants and no
longer what this parser reads. That was the trade — an object literal reads
like the record it is rather than like a wire format quoted into source.
Objects keep insertion order — iteration and equality both depend on it. Literals may span lines:
nums = [
1,
2
]
name = expr is the whole of it: it assigns the visible binding of that
name, and introduces one in the current scope if there is none.
Nothing may shadow a name already in scope. That is what makes one form
enough — the two readings are never both available, so no keyword has to
choose between them and no reader has to work out which happened. There was a
let until 2026-09-09, and forbidding the shadow is what retired it.
A declaration may carry a kind, and so may an object field or a handler's field list:
port ∈ Number = 8080
request = { url ∈ String = "https://example.com", retries ∈ Number = 3 }
Greet { who ∈ String } =>
return Greeting { text ∈ String = "hello, $who" }
Nothing checks it. port ∈ Number = "8080" runs, and port is a
String — the annotation is read, required to be a name, and dropped, and the
value's own kind is the only one that decides anything (owner's call,
2026-08-29). It is there for whoever reads the line, which means it can be
wrong the way a comment can be wrong, and nothing will ever say so. Use ∈
as an expression when you want an answer:
assert port ∈ Number
x = 1
if true
x = 2 | reaches out: `x` is visible here
assert x = 2
if true
inner = 9 | not visible outside, so this introduces one
assert inner = 9
| `inner` is gone here, and naming it is an error
The one place the rule can bite through no fault of the line's author is a
binder — a handler's field list, a loop's variables, a get. Those names
come from the particle or the container, not from whoever wrote the line, so
as is there to rename them:
email = "the file's own"
Change { email as e } =>
return R { who = e }
Without the as, that handler is refused before the program runs — by both
output modes, so neither accepts what the other rejects.
$name inside a double-quoted string splices that variable in. Escapes are
\n, \t, \", \\, and \$ for a literal dollar sign.
who = "ada"
n = 3
assert "hi $who, $n rounds" = "hi ada, 3 rounds"
assert "costs \$5" = "costs \$5"
A $ that is not followed by an identifier is a lex error, not literal
text — so a stray dollar is reported rather than silently printed.
Fields are read, as deep as they are written: "$box.lid.colour" is the
colour. A field that is not there is null, exactly as . is outside a string.
Until 1.8.7 only the name was read and .lid was left as literal text, which
nobody ever meant and which said nothing about it.
A dot that does not begin a name is still text, so a sentence can end:
"$total." is "3.". What is read is a path, never a whole expression —
there is no "$a + b" and no "$items[0]".
A String splices in bare; every other kind renders as compact JSON, which means a string nested inside an interpolated array or object keeps its quotes. Interpolation is total — no value is uninterpolable.
s = "hi"
arr = [1, "a"]
whole = 3
assert "$s" = "hi"
assert "$arr" = "[1,\"a\"]"
assert "$whole" = "3" | numbers: shortest form that round-trips
| Tier | Operators | Notes |
|---|---|---|
or |
or |
short-circuits |
and |
and |
short-circuits, binds tighter than or
|
not |
not |
prefix |
| comparison |
= ≠ < > ≤ ≥
|
non-associative |
∈ |
∈ ∉
|
see ∈ |
| additive |
+ -
|
|
| multiplicative |
* /
|
|
| unary | - |
negation |
| postfix |
.field [index]
|
Every comparison operator is exactly one character: ≠, ≤ and ≥ are
the real spellings, and ==, !=, <=, >= are rejected with a message
saying so. The only two-character operator in the language is +=.
= is both the equality operator and the separator in x = ….
They cannot collide: a statement's [let] NAME = prefix is consumed before
expression parsing starts, so every = the expression grammar sees is an
equality.
Comparison matches at most one operator, so 1 < 2 < 3 is a parse error
rather than quietly grouping as (1 < 2) < 3.
Operand rules. Ordering (< > ≤ ≥) is Number-only — strings included,
comparing them is an error. Equality (= ≠) is the opposite: defined for any
two values, so mismatched kinds are simply unequal — see
Equality for how it reads a container. - * / require Numbers;
and/or/not require Bools; a type mismatch is an error, in both modes.
+ is the exception — see below. Division by zero is an error too — the
value model is JSON, which has no way to spell infinity.
"An error" here means what it means everywhere in this language: the frame
ends and answers with an Exception, rather than the program stopping. See
Errors.
+ is overloaded by operand kind:
assert 1 + 2 = 3
assert "a" + "b" = "ab"
assert "n=" + 3 = "n=3" | a string on either side: the other operand
assert 3 + "!" = "3!" | renders as it would inside "$…", and joins
assert "x" + null = "xnull"
assert [1] + [2] = [1, 2] | two arrays concatenate
assert [1, 2] + 3 = [1, 2, 3] | one array: the other side is an element
assert 0 + [1, 2] = [0, 1, 2] | appended or prepended by which side it's on
assert {a = 1} + {b = 2} = {a = 1, b = 2} | two objects merge
assert {a = 1, b = 2} + {a = 9} = {a = 9, b = 2} | right wins, in place
A Str on either side wins over everything except a container: "x" + [1]
is still ["x", 1] (the array-element rule below), and "x" + {a = 1} is
still an error — an object has no bare form to splice in. Otherwise the
non-string operand is rendered exactly as "$it" would render it and the
two are joined.
The two containers each combine with themselves, and neither borrows the
other's rule. A field both objects name takes the right value in the
left position, and merging is one level deep, never recursive. The
position rule is about what loop and printing show, not about identity:
equality goes by field name. There is no one-object-operand form to match the array one:
an array can absorb any value as an element, but an object has no key to
file a bare value under, so {a = 1} + 3 is an error. With one array and
one object, the array rule wins and the object is simply an element.
Merging is how you copy a particle and change a field, which is the shape most handler chains want:
edited = received + {text = "ok"}
name += expr is exactly name = name + expr, so it means whatever +
means for those values. It is a statement form only, and like a bare
assignment it needs an existing binding.
= and ≠ are defined for any two values and never fail; mismatched kinds
are simply unequal. Containers compare all the way down, and the two of them
do not follow the same rule:
- An array compares by position. Its order is its identity.
- An object compares by field name. Its order is not.
assert { a = 1, b = 2 } = { b = 2, a = 1 }
assert [1, 2] ≠ [2, 1]
That split is JSON's: an array is a sequence, an object is a set of members
with no order of their own. The language still keeps the order a field was
written in — loop walks the fields in that order, and printing shows it —
but keeping an order and comparing by it are two different things, and only
the first was ever wanted here.
It compared by position until 2026-09-09, which was less a decision than the
representation showing through. It cost real time: a module that rebuilds a
result out of JSON hands the fields back in whatever order its parser chose,
and an otherwise correct assert v = { a, b } failed on nothing at all.
.field and [index] read; there is no write-through — obj.f = v does not
exist.
point = { x = 1 }
nums = [10, 20]
assert point.x = 1
assert nums[0] = 10
assert nums[1 - 1] = 10 | the index is an expression
Two different rules, deliberately:
-
Wrong operand kind is an error.
.requires an Object;[]requires an Array or an Object."abc"[0]and"abc".lengthboth fail loudly rather than quietly answering null — loudly meaning the frame ends with anException(see Errors), not that the program stops. -
An absent member is null.
obj.nope,obj["nope"],nums[99], a non-Number index into an array, a non-Str key into an object — all null. The operand kind was right; the lookup just found nothing.
That second half is load-bearing: a linked module's alias is an object with nothing in it, so reading any name through it is a missing field, and answers null.
An array is keyed by Number, an object by String — the same split
loop uses.
length, inside an index, is the length of what is being indexed:
xs = [1, 2, 3]
assert xs[length - 1] = 3
It is not a reserved word — RandomCode { length = 12 } is a field name in
several module contracts — so it means this only here, and a variable of
that name is refused instead. It answers for a string (characters, not bytes)
and an object (its field count) as well as an array.
With a second bound it is a range, and the brackets say which ends are
included — the interval notation, so [a, b] takes both and [a, b) leaves
b out. The comma is the same one that separates two of anything else on one
line:
assert xs[0, length) = [1, 2, 3] | up to but not including
assert xs[0, length - 1] = [1, 2, 3] | the same, said closed
assert xs[0, length - 1) = [1, 2] | everything but the last
assert xs(0, length) = [2, 3] | everything but the first
A ( can only mean this: there are no calls in the language, so nothing else
can follow an operand with one. One bound in round brackets is refused — a
single element is xs[i].
Both bounds clamp rather than fail: a single index past the end already
answers null, so a range past the end answers the part that is there, and
from at or after to is empty.
A string indexes and ranges too, in characters rather than bytes — the
same rule Length counts by:
s = "héllo"
assert s[1] = "é"
assert s[0, 2) = "hé"
assert s[1, length) = "éllo"
One character comes back as a one-character String; there is no character
kind, and there are only six. Out of range is null, an empty range is "".
Between them these are pop, shift, take and drop, which is why none of those is a core handler.
assert <expr> continues if the expression is true and fails otherwise. A
non-Bool is an error, not a falsy value — a condition is a Bool here, and
nothing converts.
Failing does not necessarily end the program: inside a handler it ends that
handler, which returns an Exception (see Errors). At the top
level, where there is no handler to end, it does end the program.
assert 1 < 2
assert [1, 2] = [1, 2]
assert not false
Programs are otherwise silent — there is no print statement in the language
(see emit) — so assert is how a fixture states what it means. Every
file in tests/ is a real program that asserts its own expectations.
Under code run, a failure points at the statement it came from:
error: assertion failed
--> demo.code:3:1
|
3 | assert a = b
| ^
The caret finds the top-level statement, so a failure inside an if or
loop body names the enclosing loop rather than the inner line.
if x < 10
...
A block is the indented run of lines under its header. There are no
braces on it: { } means an object, everywhere, and nothing else. The body
ends where the indentation goes back.
There is no else, and there never will be. The condition is read for
its condition, which must be a Bool. The body is a scope, following the
bindings and scope rule above.
A block may hold its one statement on the header's own line, after a comma:
if score ≥ 90, return G { letter = "A" }
if score ≥ 80, return G { letter = "B" }
return G { letter = "F" }
which is what makes a run of guards writable. With no else, that run is
the multi-way conditional here.
Indentation is structure only outside brackets. Once inside {, [ or
(, the closer is what ends the construct, so a multi-line literal lays
itself out however it reads best and nothing it does opens or closes a
block:
emit Store {
key = "user_" + email,
value = found
} to store get written
if not written
return Unavailable {}
Blank lines and comment-only lines never open or close a block either, so a comment can sit wherever it reads best. Indent with spaces: a tab is not a width, it is a request that every reader's editor agree about one.
A newline separates; a comma is how you stay on one line. That is the whole rule, and it is the same in all four places a list of things appears:
a = 1, b = 2 | two statements
if x, return Y {} | a header and its body
{ a = 1, b = 2 } | an object's fields
[1, 2, 3] | an array's elements
Write them across lines and the commas are not needed:
apps = [
{
name = "cart-web"
title = "Cart"
}
{
name = "ping-web"
title = "Ping"
}
]
A comma a newline already separated is refused. The comma has exactly
one job, so one with a line break behind it is a second spelling of the
separator that is already there — the same ground ; was removed on. There
is one way to write each of the two shapes, not two:
{ a = 1, b = 2 } | one line: the comma separates
{ | across lines: the newline does
a = 1
b = 2
}
{
a = 1, | refused
b = 2
}
A trailing comma is refused for a different reason: the comma joins two things, so one with nothing after it is a line someone did not finish.
A block written on one line takes exactly one statement, so
if x, a = 1, b = 2 runs b = 2 either way — which is what the same code
written across lines would show.
There is no ;, and typing one says so.
An empty body has no spelling, because a block is a run of statements and an empty run is nothing at all. Write the nearest thing — a body that does nothing — after the comma.
One iteration construct, in three shapes.
Over a container — an Array or an Object:
loop item over [10, 20, 30]
...
With two names, the first is the key and the second the value. The law is
X[k] = v for either container, so an array yields a zero-based Number key
and an object yields its field name:
loop i, color over ["red", "green"] | i = 0, 1
...
loop name, score over { alice = 10 } | name = "alice"
...
Names right-align against (key, value), so one name always binds the
value, whichever container you are iterating.
Unbounded — a bare loop has no iterable and no bound; only break
leaves it. This is how you write what other languages spell while:
i = 0
loop
i = i + 1
if i = 5, break
Accumulating — there is no form for it, and that is the point. A loop's
body assigns names that reach outward like any other body's, so what survives
a loop is an ordinary binding declared before it. No collect form, no
yield, and no accumulator clause:
sum = 0
loop x over [1, 2, 3]
sum = sum + x
assert sum = 6
doubled = []
loop x over [1, 2, 3]
doubled += x * 2
assert doubled = [2, 4, 6]
loop … get out = init said this in the header until 2026-09-09 and meant
exactly the line above — same scope, same body assignment, same behaviour
nested. get now means one thing in the language: an emit's
answer.
break exits the innermost loop, continue starts its next iteration. Both
reach out through any number of enclosing if bodies — they act on the
enclosing loop, not the enclosing block. Outside a loop, either is a parse
error.
ClassName { fields } — any uppercase-first name — is pure parser sugar
for an object literal with a "_class" field prepended. No new value kind,
no schema, no validation.
log = Log { message = "hi" }
assert log._class = "Log"
assert log = { _class = "Log", message = "hi" }
The brace is optional, and the rule is total: an uppercase-first name is a particle wherever it is read. With no fields it is the empty one of that class, so a handler answering with one says it in a word:
Check {} =>
return Checked
assert Checked = Checked {}
ready = Ready
assert ready ∈ Ready
That works because an uppercase name can no longer be a binding. A
variable, a get, a field list's name and a loop's variable all need a
lowercase one — binding an uppercase name would make a name nothing could
ever read back, so it is refused at the binder rather than left as a silent
dead end.
Because it is only sugar, a particle is structurally equal to a hand-written object with the same fields. There is no hidden tag.
There is no print statement, no file I/O, and no core library of functions.
The way a program reaches the outside world is to emit a particle to a
handler:
emit Length { value = [1, 2, 3] } to core get n
assert n._class = "LengthResult"
assert n.value = 3
-
to coredispatches to a handler compiled into the runtime itself. Core stays deliberately minimal:Length(of an Array, or of a Str in characters — not bytes),Timestamp(Unix seconds), andLinked(whether this run is a module somebody linked, rather than a program of its own). Every core result comes back as a particle, never a bare value. -
to thisdispatches to a handler the program defines itself. -
to <alias>dispatches to a linked module. -
get <name>binds the result. Without it the result is discarded —getis optional, and an emit sent for its effect names nothing.
get also takes a field list, the same one a
handler declares — the two sides of an emit ask the same
question of the same particle, so they ask it in the same words:
emit Length { value = [1, 2, 3] } to core get { value }
assert value = 3
emit Length { value = "abcd" } to core get { value as size }
assert size = 4
Taking a field apart this way is exactly .field: a field the answer does
not carry is null, and an answer that is not an object is an error. It
follows that a failed emit destructures into nulls rather than announcing
itself — an Exception is an object with none of the fields you asked for.
Take the answer whole when whether it worked is the point (assert n, or
n ∈ Exception — see Errors).
Dispatch is by the particle's runtime _class, not by the name written at
the call site — so a particle built elsewhere and passed in a variable
dispatches to the same handler.
A bare uppercase name is the empty particle of that class, here as anywhere
else: emit Timestamp to core is exactly emit Timestamp {} to core. See
Particles.
Note get is not as: get names the result of an emit, while as names
a linked module — or, inside a field list, renames one field.
expr ∈ Name asks one of two questions, told apart by the name. ∉ asks
the opposite, and is exactly not (expr ∈ Name) — the parser builds that
tree, so there is no second rule to keep in step. It is one character for the
same reason ≠ is one to ='s one, and it exists because the spelled-out
form read badly: not binds looser than ∈, so not r ∈ Exception makes
the eye work out that the membership is what is negated rather than r.
Which kind — the six of them, and the only place the language names a type at all:
assert 3 ∈ Number
assert "hi" ∈ String
assert true ∈ Boolean
assert null ∈ Null
assert [1] ∈ Array
assert { a = 1 } ∈ Object
What a particle is tagged — true when expr is an object whose _class
field holds that name:
emit Timestamp to core get t
assert t ∈ TimestampResult
A particle is an Object, so both are true of the same value: t ∈ Object
and t ∈ TimestampResult. And because that would make a particle named
after a kind unanswerable, the six names cannot name a particle — a
handler or literal called Number is refused before the program runs.
∈ is never an error. A wrong class, a missing _class, a non-object, the
wrong kind — all simply answer false, the same spirit as = being
well-defined across mismatched kinds. The right side is a bare name, not an
expression: which question is being asked is a lexical fact.
A handler is the only thing in the language that resembles a function, and
emit is the only way to reach one. Core provides two; a native module
provides its own; and a program can define its own with =>:
Greet { who } =>
return Greeting { text = "hi $who" }
emit Greet { who = "ada" } to this get r
assert r ∈ Greeting
assert r.text = "hi ada"
The field list is not optional decoration. There are no types here to
declare a particle's shape, so without it a body's who would be the one
name in the language that appears from nowhere. Listing the fields mirrors
the literal that constructs the particle and gives every name a declaration
site. Anything not listed is simply unreachable from the body.
A listed field the particle doesn't carry is null — the same answer .field
gives for an absent member.
as renames a field for the body. The field's own name is the sender's:
it has to match what the particle carries. The name the body reads it under
is the reader's, and a body is entitled to a word that fits it:
DoChangePassword { email, current as current_password, password as new_password } =>
...
The sender still sends current and password. Renaming rather than
adding: only the new name is in scope, so current is undefined in that
body. The same field list, and the same as, is what get { … } uses on
the other side of an emit.
The rest of the rules:
-
Top level only, like
link— dispatch is one program-wide table, and a linked module's handlers join it. A second definition of the same class is an error. -
returnmust yield a particle, so every result has a class to test with∈. A body that never returns yields null, which is fine: plenty of handlers exist for their effect rather than their answer. -
No matching handler is null, not an error — the same answer
to coreand a native module give. Emitting is not a demand: whether to act on a particle is the recipient's business, so a class nothing handles simply produces nothing. This reversed on 2026-08-28; seedocs/todo/errors-as-particles.mdfor the model it is the first step of. -
The body's enclosing scope is the top level, never the caller's. It
reads and reassigns top-level bindings and linked module aliases (it must:
linkis top-level too, so otherwise a handler could never print), but a caller's locals are invisible to it. Ordinary scope rules apply inside. - The handler call graph must be acyclic — see below.
A handler may emit to another handler, but no handler may re-enter one that is already running: not itself, and not around a longer loop.
Third { n } =>
return Done { value = n + 1 }
Second { n } =>
emit Third { n = n } to this get t
return Done { value = t.value }
First { n } =>
emit Second { n = n } to this get s
return Done { value = s.value }
That chain is fine, and so is calling the same handler twice in a row or from inside a loop — the first call has returned before the next begins. What is rejected is a cycle:
Down { n } =>
emit Down { n = n - 1 } to this get inner | error, before it runs:
return Done { value = 0 } | handler cycle: Down -> Down
This is what keeps handler calls bounded. With no cycle, the deepest a chain can reach is the number of distinct handlers in the program, so the stack cannot run away — where allowing recursion meant a program could overflow it, which in a compiled binary arrived as a bare segfault with no message.
Cycles are caught before the program runs, in both output modes, and
reported as the whole path (handler cycle: A -> B -> C -> A) — a refusal,
like any other pre-run error. Because dispatch is by the particle's runtime
_class, a particle held in a variable names a handler no static pass can
resolve; those are caught at runtime instead, and a runtime catch is an
answer rather than a refusal: the emit that tried to re-enter gets an
Exception back, and the invocation already running is untouched.
A runtime error does not end the program. It ends the frame — the handler
it happened in — which returns an Exception instead of whatever it meant to
return.
Divide { a, b } =>
return Quotient { value = a / b }
emit Divide { a = 10, b = 0 } to this get r
assert r ∈ Exception
assert r.message = "division by zero"
∈ is the whole check. There is no try, no catch, and nothing new to
learn, because an Exception is an ordinary particle:
Exception { source, message, innerException }
source names who could not do the work — "core" for the language's own
failures, the module's own name for a module's. It is the one field worth
branching on; message is prose for a person to read. innerException
carries the failure underneath this one, or null.
Receiving one is not itself an error. There is no automatic propagation: if
something you emitted to returns an Exception and you do not look, you carry
on from where you were.
Outer { } =>
emit Divide { a = 1, b = 0 } to this get r | r is an Exception
emit Print { value = "still here" } to term | and this still runs
return Report { inner = r } | pass it on, or don't
Only the frame where the failure happened unwinds — which makes this a
result-returning model rather than exceptions with unwinding, closer to a
Result than to try/catch.
All three emit targets answer the same way. A handler you wrote, a linked
module, and core each return an Exception when they cannot do the work.
None of them can end your program; a module in particular is held to that as a
hard rule (see Modules).
At the top level there is no frame to return into, so a failure there ends
the program with a non-zero status — which is what "returned an Exception
from the outermost call" amounts to.
assert 1 = 2 | error: assertion failed, and the program stops
No handler is refused over the fields a particle does not carry. A field that
is not there reads as null — exactly as .field does everywhere else — and the
handler runs and answers on that basis.
emit Length { } to core get a
emit Length { value = null } to core get b
assert a.message = b.message | the same particle, so the same answer
There is no separate "you did not supply it" complaint, because there is
nothing that could have supplied it: Length { } is
Length { value = null }, and null has no length.
Errors found before the first statement runs are refusals, not values: a parse
error, an undefined name, a link that cannot be resolved, a duplicate
handler, a handler cycle a static pass can see. Both output modes refuse the
same programs, and refusing early is preferred to failing halfway through,
after a program has already had effects.
Code modules are .code files. A module's names are its own — all of
them. What a link reaches is the module's handlers, and nothing else:
| greeter.code
greeting = "hello"
Greet { who } =>
return Reply { text = greeting + " " + who }
link "greeter"
emit Greet { who = "ada" } to this get r
assert r.text = "hello ada"
as still names the link, and the alias is an empty object — a field off
it answers null the way any missing field does:
link "greeter" as m
assert m = {}
assert m.greeting = null
There was an export keyword until 2026-09-09. A survey of every program
written in this language found no file that read another's exported name:
each one either read it inside its own file, where export meant nothing, or
did not read it at all. So the keyword went, and with it the question of
which half of a module is public.
A link has a direction, and now it is a wall in both directions. A module
cannot name anything in the file that linked it, and does not know it was
linked at all; nothing of its own travels up either. Its one way back up is
emit ... to base, which reaches handlers, never names.
That world is the module's, and it is where its handlers live:
| counter.code
count = 0 | private, and it survives the link
Bump { by } =>
count = count + by | the file it was written in
return Bumped { total = count }
link "counter"
emit Bump { by = 2 } to this get r
assert r.total = 2
A handler belongs to the file it was written in, and that file's top level
is its whole world — still there long after the link that ran it, because
the statements are over and the handlers are not. Two modules can each keep
a count and neither can reach the other's.
link is top-level only for a source module. Cycles and duplicate links
are errors. (An module may also be linked from inside a handler, while
the program runs — see Linking while the program runs.)
Native modules are shared libraries that provide handlers, written in C
against src/code_abi.h or in Rust against the
code-native crate. They require an
alias, and are reached by emit:
link "native_modules/console.so" as term
emit Print { value = "hello" } to term get r
assert r.value = 5 | bytes written
link "native_modules/math.so" as m
emit Sum { value = [1, 2, 3] } to m get n
assert n.value = 6
A module has state — its settings, its connection — so linking one is not attaching a piece of code, it is bringing something into being. Two names are therefore two of them:
link "jwt.so" as issuer
link "jwt.so" as verifier
emit Config { secret = "one" } to issuer get _
emit Config { secret = "two" } to verifier get _
Two modules, two secrets, neither aware of the other. An application wanting two databases writes two links and gets two.
And a name is only ever one module: linking another under a name already taken is refused before the program starts.
Both halves of that were wrong until 2026-09-04, and neither said so. A repeated name silently replaced the earlier link, so particles went to whichever won and answered null for every class the other one handled. And linking one file twice gave two names for a single module — it looked like two and behaved like one, so configuring the second changed what the first had already been set up to do.
Under the hood each link is loaded from its own in-memory image of the same file: the file on disk stays the single copy, nothing is written anywhere, and there is no limit beyond ordinary memory. On a system without that facility the older behaviour remains — one instance, shared — so a second link is a second name rather than a second module.
A native module may also export variables (constants), read as ordinary
fields on the alias. They are deep-copied into the host at link time, so
m.answer is a plain value rather than a live reference into the module:
link "native_modules/test_math.so" as m
assert m.answer = 42
assert m.factors = [2, 3, 5]
A .so works in both output modes — the compiled binary dlopens the
very same library the interpreter does. A .a static archive is
code build only, since there is no dlopen for an archive; those fixtures
are named buildonly_*.
On wasm, .a is the only kind there is, and that is what puts a whole
application in one file. --target wasm links the program, the runtime and
every .a it linked into a single module, with nothing left to load. A .so
is refused, because opening a library while the program runs is not something
wasm can do — the only way to reach a second wasm module is for the host to
instantiate it and wire the two together, which is the host's business and
not a link.
A module can be built for both, and console is the one that is:
crate-type stays cdylib for the .so, and the wasm archive is asked for
on the command line, because a cdylib for wasm32 is a whole module of its
own and fails on the very imports an archive is supposed to leave open:
cargo rustc --target wasm32-unknown-unknown --release --crate-type staticlibTwo things differ inside such a module, both by cfg: where its output
goes, and the names of its entry points — unprefixed for a .so, prefixed
for a .a. console prints to stdout on a machine and through one
imported function in a browser, and an application prints without knowing
which. A second module called console would have made every program
choose.
Build a code-native module for wasm with LTO on. Measured on one small
application, the same source each time:
| the app's module | .wasm |
gzipped |
|---|---|---|
hand-written, no_std
|
50 KB | 24 KB |
console on code-native
|
1.66 MB | 370 KB |
the same, CARGO_PROFILE_RELEASE_LTO=fat and OPT_LEVEL=z
|
245 KB | 88 KB |
Without LTO the archive's standard library comes along whole; --gc-sections
at link time does not help, which was measured rather than assumed. A
module that needs nothing from std should say #![no_std] and costs
almost nothing at all.
Rust modules link too, and no_std ones cost nothing. Measured on the
same one-line module, built three ways and run under Node:
| module | .wasm |
gzipped |
|---|---|---|
| C | 25.2 KB | 12.4 KB |
Rust, no_std
|
24.7 KB | 12.3 KB |
Rust, with std
|
99.3 KB | 33.8 KB |
Rust's standard library and the freestanding runtime coexist in one module
without colliding — the worry that they would not is simply wrong. What
std costs is size: about 75 KB, and once, not per module, since the
second Rust module linked reuses what the first pulled in. A module that
needs nothing from std should still say #![no_std], at which point Rust
is no heavier than C.
A module built for wasm is compiled for wasm32-unknown-unknown and must
not include src/wasm_shim.h: that is the runtime's own private libc and
it defines memset, so a module that includes it defines a second one and
the link fails on the duplicate. Leave those undefined and the runtime in the
same module answers them. Discovering the module's prefix needs a symbol
reader that understands wasm objects — the system nm reads a native .a
and not this one, so llvm-nm is tried after it.
A native module does not have to be written in another language. code build --target shared (or static) builds a .code file as one: its handlers
become code_module_dispatch,
and another program links the result exactly as it links a C or Rust module.
code build greet.code --target shared # -> build/libgreet.so
link "libgreet.so" as g
emit Greet { who = "ada" } to g get r
Asking for the container is asking for the library — there is no separate
flag. Its names stay its own, the same rule a source link follows, so the
library reports no values at all and the alias is the same empty object.
link inside a handler body opens a module the program only worked out
while running. The path is an expression rather than a quoted literal, and
the name it binds is an ordinary variable holding an address rather than
a compile-time alias — so it can be kept, passed around, and stored:
Start { path } =>
link path as app | the path is a value
emit Ping { who = "ada" } to app get r
unlink app | and it can be closed again
return r
This is how one program holds another. Build an application with --target shared, and a host can start it, talk to it, and stop it — without the
application knowing it is a guest. It is the same source either way: run it
on its own, or hand its .so to a host.
unlink is what makes stopping mean something. It calls the module's
release point (code_abi.h item 9) and unloads it, so a
.code guest gives back every block it owned. A guest still linked when the
program ends is released the same way, as part of the same sweep that
releases everything else.
It refuses while anything the module holds is still working. Unmapping
code a thread is running in is not a risk to weigh, it is a crash — so
unlink asks first, and the question is the same one that keeps a program
alive past its last statement, asked of a held application rather than of an
module. Its answer is an observation, not a promise: a door turns its own
to no as the last act of its accepting thread, after that loop has exited.
A refusal, not a silent skip. Told nothing, a host would mark something stopped that is still answering on its own port.
Only the application knows what it opened, so a host that wants it gone tells
it and lets it close its own modules. And stopping a door is not
instantaneous — Stop asks, and the thread finishes shortly after — so a
host expecting to unload asks again rather than assuming.
Four things are worth knowing before reaching for it:
-
Modules only. A
.codesource would mean adding handlers while the program runs, and a.ais already part of the binary. Only a.so. -
The path is a path, taken as written and relative to the working
directory. A top-level
linkis resolved against the file that says it, which cannot work here: the path does not exist until the program runs, and a compiled binary carries no source tree to resolve against. - Two links are two modules, even of the same file — see A name is a module. So each has its own address, and stopping one leaves the other running.
-
A module that speaks first is heard. Its queue joins the same list
a top-level
linkadds to, and leaves it again onunlink— so a door opened while the program runs is drained by the same loop, and holds the program open the same way. This was refused until 1.7.1, when choosing a door at runtime became the point.
Everything that can go wrong here is a value, not the end of the program — a missing file, a stale address, the wrong kind of value. A program that opens modules it worked out at runtime has to survive the ones it cannot open.
A guest owns its modules, unless the host says otherwise. By default a hosted application opens its own — its own file, its own settings, isolated, exactly as it would running alone. Two applications wanting two databases get two, and neither can reach the other's.
And it hears them. A module may speak without being asked, into a queue that a program's loop empties; a guest is a library whose stream ran once and returned, so nothing of its own ever would. Its pushes wake the host instead, and the host's own drain hands each guest its turn. One loop, no polling, and nothing at all while everyone is idle.
A host that wants a say takes it by answering. Define an Offer handler
and the host decides what a guest gets — the host's own copy, a stand-in, or
nothing. Write no such handler and the host furnishes nothing, which is how a
host stays out of an application's business without saying so.
The one thing a host has to answer for is the guest's door, because that
is the one module an application cannot own and still be held: a door has
a thread, a thread that outlives the application cannot be unloaded, and an
application that cannot be unloaded never gives its memory back. So an
application built to be held names membrane where
it would have named net_server — the same particles, the same genes, one
word in its manifest — and its host stands behind that name.
The host answers in its own handlers. A guest's link arrives as
Offer { app, name }, and each emit to what it was given arrives as
Module { app, name, particle }:
Offer { app, name } =>
if name = "net_server", return Offered { }
return Denied { }
Module { app, name, particle } =>
if particle._class = "Listen", return ListenResult { ok = true, port = 0 }
emit particle to net get answer
return answer
app says which guest is asking, so one may be offered what another is
denied. A module the host does not offer is not a failed link — it is an
module that refuses: the guest links it and gets an Exception on first
use, the way it would from a network that is not there. A host is never ended
by its own policy.
A page answers the same two questions. There is no dlopen in a browser
and no process to be one of, so what a shell holds there is a .wasm it
fetched and a container it drew it into — but Offer and Module are the
same words, app is the same name, and an application is built once and
runs either way. The module that does it is
guest; what differs, and why, is written
down there.
The one thing an application can ask about its own situation, and it is not a question about hosting: am I a module somebody linked, or am I the program?
emit Linked to core get me
if me.value
link "membrane.so" as door | a module: my linker stands behind it
if not me.value
link "net_server.so" as door | the program: open the port
One source, one binary each way, both lives — link inside an if is what
makes it a choice, since the answer is an ordinary value.
Answered from the build, not from anything at runtime. A --target shared build is the only thing a linker reaches into, and it says so in its
own start-up, before its first statement. Nothing has to be installed, nobody
has to tell it, and no state is kept. code run always says no: an
interpreted run is a program.
Ask it when the answer changes what is correct, which is a short list. A module that leaves a thread running past its release point can never be unloaded, so a door of your own is the usual reason. Ending the process and reading command-line arguments are the same kind of thing — right for a program, wrong for a part of one.
It deliberately does not say whether anyone is standing behind you. That
is not this layer's question. A module finds that out by asking the thing
that would need an answer: membrane tells a
program plainly when no host is there.
This is the hard rule modules are held to. Whatever goes wrong inside one —
bad input, a failed request, a bug in the module itself — the answer is an
Exception handed back to the program (see Errors), never an
exit. A class the module does not handle is null, not a complaint; a field
the particle does not carry is null, so there is nothing for a module to
refuse an emit over.
For a Rust module the rule is enforced, not merely asked for: code-native
wraps every dispatch in a catch, so even a panic — an unwrap on None, an
index past the end — comes back as an Exception and the program keeps
running. For a C module it is policy only, because a forgotten NULL check
segfaults and an integer 100 / 0 raises SIGFPE, and nothing can catch
either. Rust is therefore the recommended path for anything published; C
remains the ABI's reference implementation.
A module can also speak first. If it exports code_module_set_inbound,
the host hands it a queue at link time and it may push particles the program
never asked for — which is what an event loop is made of. Those go to the
program's own handlers, not back into the module:
link "native_modules/events.so" as ev
Tick { value } =>
...
emit Start { value = 3 } to ev get started | module queues three Ticks
| by here they have all been handled
Queued particles are dispatched after each top-level statement — and after each loop iteration, which is a statement boundary too — in the order pushed. The handler's return value goes back to the module that pushed, so a module can ask a question rather than only announce something:
link "http_server.so" as srv
Request { method, path } =>
return Response { status = 200, body = "hi from $path" }
emit Config { port = 8080 } to srv get _
emit Listen { } to srv get l
Nothing new is written on this side — a pushed particle is answered exactly
as any other, by returning one. A module that wants the answer exports
code_module_inbound_reply; most do not, and hear nothing. A pushed class the program has no handler for is dropped, not an
error: the module chose to speak, so a message nobody asked to hear is not a
mistake by the program. That is what lets a module report a problem without
every program that links it having to care — http_client pushes Exception and
Log, and net_unreachable.code handles
neither and passes. Since 2026-08-28 the outbound direction gives the same
answer — emit with no matching handler is null — so the two agree rather
than contrast. The cost, accepted deliberately: a module pushing a
mistyped class now goes unnoticed.
The queue is bounded at 256 per module, dropping the oldest — a module that outruns the program costs bounded memory.
A module may push from a thread of its own, not only from inside a dispatch call it was asked on: a timer, a socket accept loop, a terminal reading keys. A program that wants to receive those has to stay up — and it says so by starting the thing that pushes, not by writing anything to wait with. Nothing in the program causes the particles; they arrive because something else is putting them there:
link "modules/timer.so" as timer
Tick { value } =>
...
emit Start { value = 3 } to timer get started
Notice what is not there: no keep-alive loop. A program does not end at
its last statement while a linked module is still expecting to speak. A
module says so by exporting code_module_serving (see
code_abi.h); while any linked module answers non-zero,
the runtime parks, wakes on a push, dispatches it, and parks again. It is the
rule a JVM follows for a non-daemon thread, and it costs nothing while idle —
the wait ends on a real push, never on a guessed interval. http_server's
Stop { } is how a program of that shape shuts itself down: it ends the
accept thread, nothing holds the program open any more, and main finishes.
A module that exports nothing there holds nothing open, so a script that
links console and prints a line still ends exactly where it always did.
Waiting is still the module's job, never the runtime's. The runtime blocks
on its own queue, which is exact; it never sleeps on your behalf or guesses
how long you meant to wait. A module that is an event source blocks inside its
own code_module_dispatch (a condvar, a recv, an epoll) and returns when
it has something — http_client does this for an HTTP round trip.
A module cannot simply not return, though. Making Listen join its own
accept thread — the obvious way to keep a program alive — parks it one frame
below the handler that would answer: the request reaches the queue, the
drain never runs, and the connection times out. Measured, not assumed. A
pushed particle is dispatched between the program's own statements, so a
blocking module has to return and let the host do the waiting.
Two things such a module owes its callers. Bound the block — a timeout
field, as http_client has: nothing in the ABI can stop a module that blocks
forever inside a dispatch. Expect a backlog — while one module is parked,
another's pushes queue up behind it, and past 256 the oldest are dropped.
A bare loop still works and still means what it always did, for a program
that wants to drive its own iterations. It also still spins a core, exactly
as loop {} does in Rust.
The drain stops at a handler's edge. A loop inside a handler does not drain, because handing a particle over while a handler is running is re-entry, and handlers may not re-enter.
A module that pushes cannot know who will receive it, and a program's handler should be its own definition rather than something shaped by which modules happen to be linked. So the agreement has to live in the particle, and two of them are common vocabulary:
Log { source, level, message } | level: Info | Warn | Error | Debug
Exception { source, message, innerException }
Exception is the same particle a failed frame returns (see
Errors) — pushing one and returning one are the same vocabulary,
reached two different ways. Log has no returned counterpart: it exists only
to be pushed.
source is the module's own name, and it is the module's data — not
something the host adds. It exists so one handler can serve every module
without naming any of them:
Log { source, level, message } =>
emit Print { value = "[$source] $message" } to term
That handler works for http_client today and for a module written next year, with
no branching and nothing to update when a link is added.
Extension is additive. A module may carry extra fields — a handler that doesn't list them simply never sees them. What breaks the agreement is renaming the common ones.
If your shape is not the common one, your name should not be either. A
module with its own kind of record gives it its own class name
(NetTrace, not a private Log), and a program handles it separately or
not at all — an unhandled push is dropped, so a module's own vocabulary
costs nothing to a program that isn't interested.
This is a convention, not a mechanism. Nothing enforces it, exactly as
nothing enforces _class itself. Two modules that both send Log with
different shapes will silently mismatch — the second one's fields arrive as
null — which is a bug in the module that ignored the vocabulary, not a
question the language answers. http_client is the reference: see
crates/modules/http_client.
templates/module/ is a working module — a handler, its
fixture, and the CI workflow that publishes it. Copy it, rename greet,
replace the handler. tests/module_template.rs builds it and runs its
fixture through both output modes on every CI run, so it cannot quietly stop
working against the ABI it is written for.
A module is GPL-3.0, and that is not a free choice: every native module
embeds this project's runtime.c — that is how the ABI's value-lifetime
contract works — so it is a derivative work. Fine for most people, but worth
knowing before writing one rather than after.
Publishing needs nothing central: tag the repo, CI attaches the artifact and
its module.json to a GitHub Release, and a consumer runs code install <url>. See the template's README for the whole
flow and for what to keep when you replace the handler — guarded, null for
a class you do not handle, and failures returned as values are the three
rules that make a module unable to break someone else's program.
First-party modules today: console (print one line to wherever this
program's output goes — stdout on a machine, the page's console in a
browser), dom (a page drawn from a value: a tree of tags, attributes and
text, with its stylesheet in the same particle and nothing else in either —
see its README), guest (one application
running inside another in a browser — the same two questions a machine host
answers, where there is no dlopen and a container stands in for a process;
see its README),
math, strings,
env (the environment, so a port or a secret comes from the deployment
rather than the source — see its README),
json (parse JSON text, or pretty-print it — the two things string
interpolation's compact rendering can't do; see
its README), crypto (bcrypt password
hashing and verification, and random codes — see
its README), jwt (sign and verify
HS256 JSON Web Tokens — see its README),
markdown (CommonMark + GFM to HTML, with a table of contents and a
split-by-heading — see its README),
fs (files and directories under a sandboxed base directory — see
its README), json_store (a file-backed
key-value store, one readable JSON file per key — see
its README), process (run a command
and capture its output, or spawn and track a child — see
its README), git (init, clone, commit,
push and status over the system git, with a Config that checks the
repository's state first — see its README),
mailer (send email over SMTP, any provider — see
its README), oauth (the OAuth 2.0
authorization-code flow for one provider — see
its README), mongodb (documents and a
key/value layer over a MongoDB collection — see
its README), blob_storage (put, get,
list and delete objects in S3-compatible storage — see
its README), cloud_drive (Google
Drive: the OAuth flow, quota, upload, download, list, delete — see
its README), localai (chat
completions and audio transcription over an OpenAI-compatible endpoint — see
its README), http_client (the seven HTTP
methods, and Exception/Log pushed back — see
its README), http_server
(requests pushed in, answered by what a Request handler returns — see
its README), and the net_server /
net_client pair (a configured destination, then particles sent to it and
their answers back, with no protocol of their own and no policy — authentication
and authorization are a chain of handlers, because that is where a user and
their permissions can be read; see
net_server and
net_client).
Seven of these ship a <name>_mock twin — mailer_mock, oauth_mock,
mongodb_mock, blob_storage_mock, cloud_drive_mock, git_mock,
localai_mock — same particles and results, but no SMTP server, no
provider, no database, no object store, no Google, no git, no model
server: state lives in memory for the life of the process. Link one in place
of the real module to run an app with zero credentials.
code install <name> fetches one into ./.code/modules/, pinned by sha256
in ./.code/lock.json; --global puts it in ~/.code/modules/ instead. An
installed module is linked by name — link "console.so" as term — and the
lockfile maps that to the platform asset it pinned; link also resolves
against a fixed chain — the script's own directory, then the nearest
project's .code/modules/, then $CODE_MODULE_PATH, then ~/.code/modules/
— so where a module came from is always answerable.
Each of these is a decision, not an omission waiting to be filled:
-
No functions. Handlers, reached by
emit, are the only call-like construct — and the only unit of reuse. They take a particle and return one; there are no parameters lists, no return-type declarations, and no way to hold one as a value. -
No
else. Write a secondif. -
No
while. A bareloopwithbreakis the unbounded loop. -
No bare block. A scope comes with a header —
if,loop, or a handler.{ }is an object and never a scope, which is what makes every brace in a file mean one thing. -
No mutation of a constructed value.
.field/[index]read only; rebuild the value instead. -
No type checking. There are no type keywords, and a declaration that
names a kind (
let port ∈ Number = 8080) is read and dropped — it is there for whoever reads the line, and it can be wrong the way a comment can be wrong. - No core I/O. Reaching the outside world goes through a module, which keeps the runtime itself small and the dependency explicit in the source.
Everything currently known to be missing or imperfect is written up, one file
per task, in docs/todo/.
code run interprets; code build compiles through LLVM and links a native
binary with the system cc. The rule binding them: every feature must
behave identically in both.
That is enforced, not aspirational. tests/run_language_tests.rs
discovers every tests/*.code file and runs it through both paths:
- a plain
foo.codemust succeed in both, and the compiled binary must leak nothing — it runs withCODE_CHECK_LEAKS=1, so the runtime aborts at exit if any heap block survives; - a
fail_foo.codemust fail in both, whether at compile time or at run time; - a
buildonly_foo.codeis the one sanctioned exception — a.a-linked module, which must fail undercode runand succeed undercode build.
The fixtures are the specification. Each asserts its own expected values, so "what does this construct do" is answered by an executable file rather than by prose that can drift.
The invariant is written as behaviour, but since 2026-08-28 the two modes
agree on their error text as well, down to the line, column and caret —
tests/message_parity.rs runs failing programs
through both and compares the whole report. That is not politeness: a failed
frame returns an Exception whose message the program can read (see
Errors), so two backends wording a failure differently would be a
difference in what a program computes.
code test interprets every *.code file under ./tests and reports it.
There is nothing to declare and no framework to learn: a fixture passes by
running to the end, and a fixture whose file name starts with fail_ passes
by not getting there.
$ code test
ok tests/loops.code
ok tests/fail_type_mismatch.code
FAIL tests/handlers.code
assertion failed
--> tests/handlers.code:12:1
|
12 | assert reply.text = "hi"
| ^
2 passed, 1 failed
That is the same convention this repository's own suite runs on, and it works
because assert is already the language's way of saying what should hold — a
test is just a program, so a runner only has to say which programs stopped.
It interprets, and does not also build. This repository's own suite runs its fixtures through both output modes, because those fixtures exist to prove the two modes agree; your fixtures assert what your program computes, and are entitled to assume what the language already guarantees.
A path may be a directory (walked for *.code) or a single fixture. link
resolves relative to the file doing the linking, so a fixture under tests/
reaches a module or another source file by its own relative path, wherever
you run from.
code format gives .code source a single layout the way cargo fmt does
for the Rust half of this repo, and the same CI step enforces it. Editors get
it through crates/code-lsp, which serves the identical
function over textDocument/formatting.
It formats the token stream, never the AST — which is not an
implementation detail but the reason it is safe to run on your files. The AST
is desugared by design: comments are gone by the time it exists, n += 1 has
become n = n + 1, Timestamp {} has become an object literal, and 1.50
has become an f64. A formatter built on it would silently rewrite all four.
Working from tokens, every piece of output is a slice of the input, so
literals keep their spelling and comments survive verbatim.
Hard line breaks stay yours. There is no maximum width and no re-flow: a
{ x = 1 } written inline stays inline, and a multi-line array stays
multi-line. What gets normalized is indentation, spacing between tokens, and
runs of blank lines.
Three properties are checked over every fixture in
tests/, in tests/format_fixtures.rs:
the token stream is identical before and after (so the meaning cannot have
changed), every comment survives in order, and formatting twice is the same
as formatting once.
A file that does not parse is reported and left alone, never half-rewritten.
src/ the language: lexer, parser, ast, interpreter, codegen (LLVM),
loader (modules), native (dlopen), runtime.c + code_abi.h
tests/ *.code fixtures (the spec) + the harnesses that run them
crates/
code-wasm/ interpreter-only build for the browser playground (npm)
code-native/ the crate for writing native modules in Rust (crates.io)
code-lsp/ diagnostics, semantic tokens and formatting, over the real
lexer/parser and the same `code format` the CLI runs
modules/ first-party modules: console, dom, guest, math, strings, env, json,
json_store, crypto, jwt, markdown, fs, process, git, mailer,
oauth, mongodb, blob_storage, cloud_drive, localai,
http_client, http_server, net_client, net_server — plus
<name>_mock twins for
mailer, oauth, mongodb, blob_storage, cloud_drive, git, localai
site/ the playground; build.py embeds tests/*.code as examples
templates/ module/ — a working starting point for publishing your own
docs/todo/ open tasks, one file each, written to be picked up cold
old/ archived earlier language — reference only, nothing links to it
src/ast.rs carries the design decisions and their reasons per construct;
src/runtime.c's header comments cover the compiled value model and its
refcounting rules.
GPL-3.0. See LICENSE.