Specification
A compact but complete description of mu: the whole language, on one page. Where a detail is elided, the full specification and grammar in the repository are the normative text.
Lexical structure
Source is UTF-8. // comments run to end of line, /* … */ may span lines, and
a #! shebang is allowed on the first line only. Semicolons are optional where a
newline or closing delimiter already separates statements.
Thirteen keywords, and that is the entire list:
fn if else while return continue break
import quote unquote true false nil
Identifiers start with a letter or _, then letters, digits or _.
Literals. Integers are 64-bit signed and may be written in decimal, hex
(0x2a) or binary (0b101010). Decimal literals are a digit run, a ., and at
least one further digit — 3.14, 0.1 — and are exact fixed-point, not binary
floating point. Strings are double-quoted and support \a \b \t \n \v \f \r \0 \\ \";
a backslash before anything else is kept verbatim, so "\q" is two characters.
There are no numeric or \u escapes — build other code points with chr(n).
Values
| Type | Notes |
|---|---|
INTEGER |
64-bit signed. Truncating division; division by zero is an error. |
BOOLEAN |
true / false, distinct from integers. |
STRING |
Immutable, UTF-8, byte-indexed. + concatenates; < compares lexicographically. |
LIST |
Ordered, zero-indexed. Out-of-bounds read yields nil. |
MAP |
Keys may be integers, strings, booleans, or anything with __ops["key"]. |
BUFFER |
Mutable bytes from buf(n). b[i] is an integer 0–255. |
CLOSURE |
Functions are closures. They cannot be compared for equality. |
ERROR |
A value, not an exception. |
CODE |
Parsed mu syntax from quote { … }. Data, not executable. |
NULL |
nil, the absence of a value. |
LIB, PTR |
Opaque FFI handles from dlopen / dlsym. |
TASK, CHAN |
Concurrency handles. |
A map carrying a string __type member reports that instead of "MAP", which
is how type(3.14) is "Decimal".
Map traversal is insertion-ordered and stable. keys, values and inspect
walk the same order, so values(m)[i] is always m[keys(m)[i]]. Reassigning a
key keeps its position; deleting and re-adding moves it to the end. Since an
absent key and a nil value both index to nil, use has(m, k) to tell them
apart.
[!NOTE] Traversal order is a guarantee, not an implementation detail. Before this was pinned, the Go VM answered a different
keys()order on every call, andkeysandvaluesdisagreed with each other inside a single call.
Truthiness. Falsey: false, nil, 0, "", empty list, empty map, empty
buffer. Everything else is truthy. A value with a bool entry in __ops answers
for itself — which is why 0.0 is falsey despite being a non-empty map.
Expressions and operators
Precedence, loosest first; all groups associate left:
||&&==!=<<=>>=+-|^*/%<<>>&**- unary
!-~, andunquote - primaries — literals, identifiers,
(…), function literals, list/map literals, indexing, calls, member access
[!WARNING] Bitwise
|,^and&bind at arithmetic level, not below comparison as in C.a & b == cparses asa & (b == c). Parenthesise when mixing them.
Statements
x := 1 // declare; shadows any outer x
x = 2 // assign to nearest existing x
x += 3 // update in place
x -= 1
x++ // increment by one
x-- // decrement by one
Assigning with = to a name that does not exist is a compile error, as is
assigning to a builtin.
if is an expression: the taken block’s last value is its result, and nil
if no branch runs.
status := if ready { "go" } else { "wait" }
while is the only loop, and yields no value. continue re-tests the condition;
break leaves the nearest loop. Both are compile errors outside a loop.
return exits a function immediately; bare return yields nil. return at
top level is illegal.
Functions and closures
fn add(a, b) { return a + b }
double := fn(x) { return x * 2 }
// variadic: rest is a list of the extras
fn sum(first, rest...) { … }
Top-level fn declarations are visible before their definition, so mutual
recursion works without forward declarations.
Closures capture variables, not values. A nested function shares the binding with the function that declares it, so writes are visible in both directions:
fn totalise(items) {
total := 0
i := 0
while i < len(items) {
total = total + items[i]
i = i + 1
}
return total // the sum
}
A closure that outlives its declaring function keeps the binding alive, which is what makes a counter work, and a variable assigned after a closure is created is still seen by it — which is how a recursive lambda refers to itself:
fact := nil
fact = fn(n) {
if n <= 1 { return 1 }
return n * fact(n - 1)
}
[!NOTE] This was not always true. Closures used to capture the value at creation time, so the first example returned 0 and the second crashed. Variables that are captured and assigned are now shared; ones that are only read still use the cheaper by-value capture, which is indistinguishable because the value never changes.
Because the binding is shared, closures made inside a loop all see the
variable’s final value — there is one variable, and while introduces no
per-iteration binding. Declare inside the body if you want one per iteration:
while i < 3 {
fs = append(fs, fn() { return i }) // every closure returns 3
i = i + 1
}
while i < 3 {
j := i
fs = append(fs, fn() { return j }) // 0, 1, 2
i = i + 1
}
Errors are values
There are no exceptions. error(msg) and builtin misuse produce a value of type
"ERROR" that flows through your program until you check it:
import "io"
data := io.read_file("missing.txt")
if type(data) == "ERROR" {
print("failed:", data)
}
inspect and print render an error as its message. Indexing with the wrong
type, calling with the wrong arity, calling a non-function value, invalid
operators, and I/O failures are all error values.
A smaller set of mistakes rejects or halts the program instead: using an
undefined variable, assigning to one, assigning to a builtin, a macro-expansion
failure, a concurrency deadlock, or an explicit panic.
The operator protocol
A map carrying an __ops table defines what operators mean for its own values:
fn add(a, b) {
return Money(a.cents + b.cents)
}
fn show(a) {
return "$" + str(a.cents / 100)
}
Money := fn(cents) {
return {
"__type": "Money",
"cents": cents,
"__ops": { "+": add, "str": show },
}
}
print(str(Money(500) + Money(250)))
// $7
Entries cover arithmetic, comparison, equality, neg, str, bool and key
(the scalar a value hashes as). The host consults __ops only where an
operation would otherwise have failed, so ordinary arithmetic pays nothing for
it. This is the only __dunder__ convention in mu, and it is opt-in.
Decimals are built entirely from this protocol — the language itself knows
nothing about them. A bare / on decimals keeps 15 fractional digits and rounds
half-even; decimal.ratio(a, b, scale, mode) chooses both.
Macros
quote { … } produces a CODE value; unquote x is a hole filled from the
enclosing scope, and unquote xs... splices a list of code values. A macro is an
ordinary function from code to code, registered with macro.define, and it runs
on the build machine before bytecode exists — so macros cost nothing at runtime.
import "macro"
macro.define("unless", fn(cond, body) {
return quote {
if !unquote cond {
unquote body
}
}
})
A quote is always block code; unwrapping happens at the splice site, where a block holding exactly one expression unwraps and anything else is an error. Identifiers written literally inside a quote are marked as quote-introduced, so a macro’s own bindings cannot capture its caller’s.
Modules
import "strings"
import "path/to/module" as mod
import is only legal at the outermost level. Each import binds exactly one
name, and every export lives behind a dot: strings.split(…). Namespaces are
immutable, and imports never leak into the global scope.
Resolution order: MULIB if set, then the embedded lib/ tree, then the
filesystem relative to the importing file.
The µ-builtins module lives at lib/builtins.mu and supplies
unqualified globals: print, input, panic, test, exit, plus bool, values,
has, int, insert, is_error, must and try, which are written
in mu rather than in the host. A MULIB override containing an empty
builtins.mu removes them.
[!TIP] The µ-builtins module is ambient, not importable. Use
print(...)orhas(m, k)directly;import "builtins"is refused.
Execution model
Source is lexed and parsed to an AST, compiled to bytecode through a small IR, and executed on a stack machine. The same bytecode either runs on the VM or is lowered to machine code by the native backend; the VM is the reference for semantics.
Concurrency is cooperative: tasks yield only at blocking operations (send,
recv, wait, sleep, I/O), and runnable tasks resume FIFO, so scheduling is
deterministic. It runs through the Go VM, Go native backend, self-hosted VM, and
self-hosted native backend on that backend’s supported targets.
Builtins
27 callable host builtins, and nothing else is privileged:
data len append pop del keys
types type inspect str error
chars ord chr
host syscall buf platform
args env
ffi dlopen dlsym dlcall
tasks task wait chan
send recv poll push
repl help
Ambient µ-builtins come from lib/builtins.mu, not the host:
print input panic test
exit bool values has
int insert is_error must
try // macro
A capability earns a place in the host list only when mu genuinely cannot
express it — it needs the kernel, raw memory, the loader, the scheduler, the
runtime type tag, or a mutation mu has no other way to perform. Everything else
lives in mu, in the µ-builtins layer or in stdlib modules.
help() prints the live list.
The normative documents
- docs/Specification.md — the full language contract
- docs/Grammar.md — formal grammar and precedence
- docs/Implementation.md — compiler and VM architecture
- docs/ir.md — IR and assembler
- docs/MacroOverhaulRFC.md — the macro design and its rejected alternatives
Next steps
- Start with the language tour for a practical overview.
- Review the runtime guide for execution details.
- Explore the standard library modules.