Astreum

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

NameStack effectCostDescription
+(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

NameStack effectCostDescription
<(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

NameStack effectCostDescription
&(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) · 2Bitwise NOT on Bytes

Shifts and rotates

NameStack effectCostDescription
<<(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

NameStack effectCostDescription
drop(a -- )1Discard top of stack
dup(a -- a a)a.size()Duplicate top of stack
swap(b a -- a b)1Swap top two items
dip(v (expr) -- ... v)expr.size()Remove v, evaluate expr, push v back
rot(a b c -- b c a)3Rotate top three stack values left

Expression construction

NameStack effectCostDescription
link(head tail -- link(head, tail))1Build a link pair
head(link -- head)1Extract head (NIL if missing)
tail(link -- tail)1Extract tail (NIL if missing)
is_eq(a b -- bytes)1Deep structural equality. Different types are never equal. Pushes Bytes(b"\x01") if equal, Bytes(b"\x00") otherwise.
is_atom(expr -- bytes)1Pushes 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 sizeConvert Bytes, String, Int, or fp to Symbol
str(a -- string|NIL)input or result sizeConvert 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 sizeConvert Int, fp, String, or Symbol to Bytes
id(expr -- bytes)32Push 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() + 1Wrap value in (value . ok)
err(msg -- err_tagged)msg.size() + 1Wrap msg in (msg . err)
result(val_or_cont -- processed)N/AUnwrap 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/AMatch 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)1Check 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

NameStack effectCostDescription
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)1Return element at index (byte, code point, or subtree)
map(list quotation -- new_list)cumulativeApply quotation to each element, collecting results
filter(list quotation -- filtered_list)cumulativeKeep elements for which quotation pushes truthy
each(list quotation -- )cumulativeApply quotation to each element, discarding results
fold(list init quotation -- result)cumulativeLeft fold over list elements
zip(list_a list_b -- zipped_list)cumulativePairwise zip two lists into link pairs
find(list quotation -- element|NIL)cumulativeFind the first element matching the quotation

Control flow, definitions, and functions

NameStack effectCostDescription
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 sizesEvaluate 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

NameStack effectCostDescription
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

NameStack effectCostDescription
acc.balance( -- balance)1Push 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)1Push the current block's chain_id as Int
block.height( -- height)1Push the current block's height as Int
block.previous_block_hash( -- hash)1Push the current block's previous_block_hash as Bytes (32 bytes)
block.timestamp( -- timestamp)1Push 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)1Push the current transaction's amount as Int
tx.recipient( -- recipient)1Push the current transaction's recipient public key as Bytes (32 bytes)
tx.sender( -- sender)1Push the current transaction's sender public key as Bytes (32 bytes)
tx.new(code recipient amount data -- hash|NIL)nested executionConstruct 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 feeAppend 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

FlagDescription
--tuiLaunch the interactive TUI
--headlessRun startup actions without TUI
--evalEnter evaluation mode
--consoleLaunch interactive REPL for evaluating expressions
--script <path>Load a module file; entry defaults to main
--expr "<expr>"Evaluate a postfix expression
--apiEnable 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 #;.