LANGUAGE REFERENCE
Language Reference
Concise reference for grammar, operators, module loading, CLI flags, and naming rules.
Grammar
program ::= expr
expr ::= symbol | bytes | int | fp | string | list
list ::= "(" {expr} ")"
quote ::= "'" expr
module ::= "(" {definition} ")"
definition ::= "(" expr expr ("def" | "import") ")"
symbol ::= non-whitespace, non-paren token
bytes ::= "0x" hex-digits
int ::= decimal integer literal
fp ::= decimal fp literal (see Specification for the full e4m3/e5m2/fp16/bf16/fp32/fp64 grammar)
string ::= double-quoted text
comment ::= ";" to end of line | "#;" expr
Evaluation is postfix: the last symbol in a list drives dispatch.
Operators
Operator names in the machine evaluator. The Cost column shows the byte cost charged to the meter per operation. Appending ? to any operator name wraps the result as (ok . value) on success or (err . "reason") on error.
Seven operators are blocked in deterministic (consensus) execution: spawn, send, receive, ref, load, print, println. In deterministic mode they push NIL at a cost of 1 byte. eval, closure, and apply execute normally.
Arithmetic
| Name | Stack effect | Cost | Description |
+ | (a b -- sum) | result.size() | Int/Int -> Int, fp64/fp64 -> fp64, mixed Int/fp64 -> fp64 |
- | (a b -- diff) | result.size() | Same type rules as + |
* | (a b -- product) | result.size() | Same type rules as + |
/ | (a b -- quotient) | result.size() | Int/Int uses integer division, fp64/fp64 uses fp64 division |
% | (a b -- remainder) | result.size() | Int only |
sqrt | (a -- sqrt) | result.size() | fp64 only |
abs | (a -- abs(a)) | result.size() | Int or fp64. Raises OpError on non-numeric. |
Comparison
| Name | Stack effect | Cost | Description |
< | (a b -- 0|1) | result.size() | Less than (Int/Int or fp64/fp64). Pushes Bytes(b"\x01") if true, else Bytes(b"\x00"). Raises OpError on type mismatch. |
> | (a b -- 0|1) | result.size() | Greater than. Same type rules as <. |
<= | (a b -- 0|1) | result.size() | Less than or equal. Same type rules as <. |
>= | (a b -- 0|1) | result.size() | Greater than or equal. Same type rules as <. |
Bitwise
| Name | Stack effect | Cost | Description |
& | (a b -- a & b) | max(len(a), len(b)) | Bitwise AND on Bytes |
| | (a b -- a | b) | max(len(a), len(b)) | Bitwise OR on Bytes |
^ | (a b -- a ^ b) | max(len(a), len(b)) | Bitwise XOR on Bytes |
~ | (a -- ~a) | len(a) · 2 | Bitwise NOT on Bytes |
Shifts and rotates
| Name | Stack effect | Cost | Description |
<< | (value shifts -- result) | 1 (zero); byte-width / value.size() | Shift: Bytes (logical) or Int (arithmetic). >0 = left, <0 = right. |
<<< | (value shifts -- result) | 1 (zero); byte-width / value.size() | Rotate: Bytes or Int. >0 = left, <0 = right. Width byte-rounded for Int. |
Stack operations
| Name | Stack effect | Cost | Description |
drop | (a -- ) | 1 | Discard top of stack |
dup | (a -- a a) | a.size() | Duplicate top of stack |
swap | (b a -- a b) | 1 | Swap top two items |
dip | (v (expr) -- ... v) | expr.size() | Remove v, evaluate expr, push v back |
rot | (a b c -- b c a) | 3 | Rotate top three stack values left |
Expression construction
| Name | Stack effect | Cost | Description |
link | (head tail -- link(head, tail)) | 1 | Build a link pair |
head | (link -- head) | 1 | Extract head (NIL if missing) |
tail | (link -- tail) | 1 | Extract tail (NIL if missing) |
is_eq | (a b -- bytes) | 1 | Deep structural equality. Different types are never equal. Pushes Bytes(b"\x01") if equal, Bytes(b"\x00") otherwise. |
is_atom | (expr -- bytes) | 1 | Pushes Bytes(b"\x01") if the value is not a link, else Bytes(b"\x00"). |
init | (value 'tag -- expr) | value.size() + tag.size() | Wrap value in a typed Expr with tag tag. Idempotent for matching tags. |
type | (expr -- symbol) | result.size() | Return the tag of expr as a Symbol |
eval | (expr -- evaluated) | val.size() | Re-enter evaluator on the value |
quote | (a -- (' a)) | v.size() | Wrap a value in a quotation |
symbol | (a -- symbol|NIL) | input or result size | Convert Bytes, String, Int, or fp to Symbol |
str | (a -- string|NIL) | input or result size | Convert any atom to String |
e4m3 | (a -- e4m3|NIL) | v.size() + result.size() | Convert Int, Bytes, String, or Symbol to e4m3 fp |
e5m2 | (a -- e5m2|NIL) | v.size() + result.size() | Convert Int, Bytes, String, or Symbol to e5m2 fp |
fp16 | (a -- fp16|NIL) | v.size() + result.size() | Convert Int, Bytes, String, or Symbol to fp16 |
bf16 | (a -- bf16|NIL) | v.size() + result.size() | Convert Int, Bytes, String, or Symbol to bf16 |
fp32 | (a -- fp32|NIL) | v.size() + result.size() | Convert Int, Bytes, String, or Symbol to fp32 |
fp64 | (a -- fp64|NIL) | v.size() + result.size() | Convert Int, Bytes, String, or Symbol to fp64 |
int | (a -- int|NIL) | v.size() + result.size() | Convert Bytes, String, Symbol, or fp to Int |
bytes | (a -- bytes|NIL) | input or result size | Convert Int, fp, String, or Symbol to Bytes |
id | (expr -- bytes) | 32 | Push the blake3 content hash of expr |
parse | (string -- expr|NIL) | len(string) | Parse a source string as a single expression |
ok | (value -- ok_tagged) | value.size() + 1 | Wrap value in (value . ok) |
err | (msg -- err_tagged) | msg.size() + 1 | Wrap msg in (msg . err) |
result | (val_or_cont -- processed) | N/A | Unwrap tagged success or propagate error. If err-tagged, preserve; if other tagged, extract head; otherwise treat as continuation. |
match | (val tag_succ succ_cl fail_cl -- branch_result) | N/A | Match a tagged value. If val is (v . tag_succ), push v and evaluate succ_cl; otherwise push val and evaluate fail_cl. |
is | (val tag_sym -- bytes) | 1 | Check if val is tagged with tag_sym. Pushes Bytes(b"\x01") if match, else Bytes(b"\x00"). |
ref | (hash -- expr|NIL) | 1 (hit); 70 + resolved.size() (miss) | Resolve a 32-byte hash via content lookup |
load | (hash -- full_expr|NIL) | 1 (zero32); resolved.size() · 2 (hit) | Deep-resolve a hash recursively |
Sequence operations
| Name | Stack effect | Cost | Description |
count | (value -- length) | result.size() | Length of Bytes, String, or link chain as Int |
concat | (a b -- concatenation) | result.size() | Concatenate two Bytes, String, or link values |
reverse | (value -- reversed) | value.size() | Reverse Bytes, String, or link chain |
split | (value index -- link(left, right)) | left.size() + right.size() | Split Bytes or String at index |
index | (value index -- element) | 1 | Return element at index (byte, code point, or subtree) |
map | (list quotation -- new_list) | cumulative | Apply quotation to each element, collecting results |
filter | (list quotation -- filtered_list) | cumulative | Keep elements for which quotation pushes truthy |
each | (list quotation -- ) | cumulative | Apply quotation to each element, discarding results |
fold | (list init quotation -- result) | cumulative | Left fold over list elements |
zip | (list_a list_b -- zipped_list) | cumulative | Pairwise zip two lists into link pairs |
find | (list quotation -- element|NIL) | cumulative | Find the first element matching the quotation |
Control flow, definitions, and functions
| Name | Stack effect | Cost | Description |
lambda | (body params -- lambda_val) | params.size() + body.size() | Pop params (link of symbols) and body (expression). Produce a tagged link value that apply can invoke, with a snapshotted parent environment. |
apply | (argN ... arg1 lambda_val -- result) | Σ arg.size() | Pop lambda_val, then pop N arguments from the stack matching its param list. Tag determines parent: lambda = creation-time snapshot; fn = call-site lexical env; box = no parent. Construct fn / box tagged values manually via link: (body params 'fn link). |
if | (cond then else -- result) | condition.size() | Evaluate the condition quotation, then select a branch by truthiness |
def | (name value -- ) | name.size() + value.size() | Bind value under name in the current environment. Raises OpError if name already exists. |
rec | (pred then rec1 rec2 -- result) | cumulative sub-expr sizes | Evaluate pred; if truthy evaluate then and stop; otherwise evaluate rec1 and repeat. After the loop evaluate rec2 once per rec1 iteration. |
The fn and box tags are recognised by apply but have no dedicated operator. Construct manually: (body params 'fn link) for a call-site-lexical function or (body params 'box link) for a fully isolated snapshot.
Actors
| Name | Stack effect | Cost | Description |
spawn | (body name -- name|NIL) | name.size() | Spawn actor thread running body in a child environment |
send | (target msg -- ) | target.size() + msg.size() | Send msg to target's mailbox |
receive | (target -- msg|NIL) | target.size() + msg.size() | Block until a message arrives |
Consensus, context, and transaction operators
| Name | Stack effect | Cost | Description |
acc.balance | ( -- balance) | 1 | Push the expression account's (tx.recipient) balance as Int |
acc.get | (key -- value|NIL) | key.size() | Look up key in the expression account's data store |
acc.put | (key value -- ) | key.size() + value.size() | Store value under key in the expression account's data. Sender pays storage fee. |
acc.pay | (recipient amount -- ) | recipient.size() | Pay amount from expression account to recipient. Creates recipient if missing. |
block.chain_id | ( -- chain_id) | 1 | Push the current block's chain_id as Int |
block.height | ( -- height) | 1 | Push the current block's height as Int |
block.previous_block_hash | ( -- hash) | 1 | Push the current block's previous_block_hash as Bytes (32 bytes) |
block.timestamp | ( -- timestamp) | 1 | Push the current block's timestamp as Int |
block.bloom.insert | (value -- ) | 8 (storage) | Record value.hash() as a bloom search key. Deduped per-tx and per-block. |
tx.amount | ( -- amount) | 1 | Push the current transaction's amount as Int |
tx.recipient | ( -- recipient) | 1 | Push the current transaction's recipient public key as Bytes (32 bytes) |
tx.sender | ( -- sender) | 1 | Push the current transaction's sender public key as Bytes (32 bytes) |
tx.new | (code recipient amount data -- hash|NIL) | nested execution | Construct an internal transaction and apply its effects inline. Pushes the nested tx's blake3 hash (32 bytes) or NIL on failure. |
tx.log | (value -- ) | storage fee | Append value to the transaction's log. Sender pays storage. |
Special form
quote is handled inline in the evaluator loop, not dispatched as an operator. The ' symbol at the head of a list behaves the same way.
Module system
- Module files use
.aex by convention. Each file is a parenthesized sequence of definitions.
- Each definition must be a 3-element form:
(value name def) or (prefix path import).
def stores the raw expression under the name with no runtime evaluation at load time.
import accepts a filesystem path that may be absolute or relative to the importing file's directory.
- Reference imports use
(prefix (0x... ref) import) to load from expression storage by content hash.
- Imported symbols are qualified under the given prefix, and intra-module references are rewritten to fully qualified names.
- Circular imports are rejected via an active-stack guard.
- Use
compile(node, script, target) to load a module programmatically — returns an Env with tree-shaken dependencies.
CLI flags
| Flag | Description |
--tui | Launch the interactive TUI |
--headless | Run startup actions without TUI |
--eval | Enter evaluation mode |
--console | Launch interactive REPL for evaluating expressions |
--script <path> | Load a module file; entry defaults to main |
--expr "<expr>" | Evaluate a postfix expression |
--api | Enable the HTTP API server |
--api-port <n> | HTTP API port |
--api-host <addr> | HTTP API bind address |
--node-default-seed <s> | Override the node default seed |
Four modes are mutually exclusive: --tui, --headless, --eval, --console. Use --eval with at least one of --script or --expr.
Use --cli-<key> <value> and --node-<key> <value> to override saved settings for one run. Kebab-case keys map to snake_case config keys; a flag with no value defaults to true. Literals true, false, none, null, integers, floats, and 0x-prefixed hex are coerced automatically. Example: --headless --node-verbose false --cli-on-startup-connect-node. Pass --node-default-seed none to clear the default seed.
Reserved words and naming rules
- All operator names are reserved.
- Operator names suffixed with
? are also reserved (e.g. +?, drop?).
quote is also reserved as a special form.
- Tag symbols
fn and box are reserved (used by apply).
- Module keywords
def, import, and ref are reserved.
- Names are UTF-8 symbols, optionally dot-qualified, such as
math.sum.
- Unbound symbols silently push NIL.
- Integers in source become
Expr("int", …) literals.
- Line comments start with
; and run to end of line. Block comments use #;.