Astreum

LANGUAGE TOUR

Language Tour

Walk through the core language constructs with short snippets. The evaluator reads postfix expressions left to right: each value pushes onto a stack, then operators pop their operands and push results. Matches lib-py 0.21.4.

Stack and arithmetic

Numbers now use unified arithmetic operators.

(1 2 +)      ; -> 3
(1 2 *)      ; -> 2
(5 3 -)      ; -> 2
(10 3 /)     ; -> 3
(10 3 %)     ; -> 1
(4.0 sqrt)   ; -> 2.0
(-5 abs)     ; -> 5
(1.5 2.5 +)  ; -> 4.0
(1 2 3 rot)  ; -> 2 3 1
(2 (1 2 +) dip) ; evaluate (1 2 +) without losing the saved 2 -> 3 2

Content hashes (id)

id pushes the 32-byte Blake3 content hash of any expression. The hash is the content-addressed identity of the expression.

((1 2 +) id)                ; -> 32-byte Blake3 hash
(42 id)                     ; -> 32-byte hash of Int(42)

Comparisons

Comparison operators work on Int/Int or Float/Float. Pushes Bytes(b"\x01") for true, Bytes(b"\x00") for false.

(5 3 <)   ; -> 0x00 (false)
(5 3 >)   ; -> 0x01 (true)
(5 5 <=)  ; -> 0x01 (true)
(5 5 >=)  ; -> 0x01 (true)
(3.0 2.5 >) ; -> 0x01 (true)

Console I/O

print and println write repr(expr) to stdout. Both are void — they do not push to the stack. Blocked in deterministic mode.

(42 print)           ; writes "42" to stdout, stack unchanged
("hello" println)    ; writes '"hello"\n' to stdout
(println)            ; writes just "\n"

Bitwise and shifts

Bitwise operators work on Bytes values. Use hex literals when you want raw byte input.

(0x05 0x03 &)    ; -> 0x01
(0x05 0x03 |)    ; -> 0x07
(0x05 0x03 ^)    ; -> 0x06
(0x05 ~)         ; one's complement within the byte width
(0x01 0x04 <<)  ; logical left shift -> 0x10
(0x10 -0x04 <<) ; logical right shift -> 0x01
(0x03 0x01 <<<) ; rotate left -> 0x06
(0x03 -0x01 <<<); rotate right -> 0x81

Defining variables with def

(value name def) evaluates the value, then binds the name in the current environment.

(10 x def)    ; binds x to Int(10)
(x)           ; evaluates to 10

Unbound symbols silently push NIL rather than raising an error.

Functions: closure, dyn, pure

closure is the operator that produces a function value. It pops params and body from the stack, captures the current environment, and pushes a tagged link pair. Use apply to invoke.

(
  (x y +) (a b) closure
) sum def
(5 6 sum apply)   ; -> 11

The 'dyn and 'pure tags are recognised by apply but have no dedicated operator — they are constructed manually with link. The tag determines the parent environment:

  • 'lex — restores the captured environment snapshot (stored by UUID in the machine library).
  • 'dyn — uses the call-site lexical environment as parent (live, not snapshot).
  • 'pure — no parent environment (completely closed scope).
(5 3 '(x y +) '(x y) link 'dyn link apply)   ; live env (dyn tag)
(5 3 '(x y +) '(x y) link 'pure link apply)  ; closed env (pure tag)

Conditionals with if

Write conditionals as (cond then else if). The condition is evaluated first. Truthiness is non-zero Bytes, non-zero Int, non-zero fp64, or a non-NIL link whose head is not Symbol("err").

(0 1 0 if)      ; false -> 0
(0 1 1 if)      ; true -> 1
(0 1 1.0 if)    ; Float truthiness also works
(0 (2 3 +) 1 if) ; true branch is a list expression -> 5

Recursion with rec

rec is a general recursion combinator. It evaluates a predicate; if truthy it runs a then-branch, otherwise it runs a pre-recursion step, recurses, then runs a post-recursion step on return.

; countdown from 3
(
  (0 >)             ; pred: still positive?
  (print)           ; then: print current value
  ()                ; rec1: nothing before recurse
  (1 -)             ; rec2: decrement after recurse
  rec
) countdown def

Quote and eval

quote prevents evaluation of its argument, while eval re-enters the evaluator on a value.

(' (1 2 +))          ; pushes the list (1 2 +) unevaluated
( (1 2 +) quote eval ) ; same as evaluating (1 2 +) -> 3

Parsing strings (parse)

parse tokenizes and parses a string as an Astreum S-expression at runtime. Combine with str to round-trip expressions through text.

("(1 2 +)" str parse) ; -> link(1, link(2, NIL))
(1 2 + str parse)       ; -> 3 (same as evaluating (1 2 +))

Expression construction

link, head, tail, and is_eq let you build and compare list expressions. Sequence operations like count are polymorphic over bytes, str, and link — they replace the old bytes-only size.

(1 2 link)         ; -> link(1, 2)
(1 2 link head)    ; -> 1
(1 2 link tail)    ; -> 2
(1 1 is_eq)        ; -> 0x01 (deep equality)
(1 2 is_eq)        ; -> 0x00
("42" symbol)      ; String -> Symbol "42"
("42" int)         ; String -> Int(42)
(42 str)           ; any atom -> String("42")
(42 fp64)          ; Int -> fp64(42.0)
(42 bytes)         ; Int -> Bytes(b"*")
( (1 2 link) ref ) ; resolve content-hash of (1 2) from storage
( (1 2 link) load ) ; deep-resolve the full sub-tree

User types with init and type

init wraps a value with a type tag. type returns the tag as a Symbol. Type names can also be used as constructors.

(3 5 link 'point init) ; -> Expr("point", value=link(3, 5))
((3 5 link 'point init) type) ; -> Symbol("point")

; type-name-as-constructor
'( (x y link 'point init) (x y) closure ) 'point def
(3 5 point apply)        ; -> Expr("point", value=link(3, 5))
(5 3 point apply)        ; -> Expr("point", value=link(5, 3))

init is idempotent — re-tagging a value that already bears the target tag is a no-op.

Byte-string and sequence operations

Sequence operators are polymorphic over Bytes, str, and link. concat, split, index, count, and reverse work on all three types. Higher-order operators accept a quoted link body (treated as a concatenative program with element values pre-pushed on the stack) or a tagged function value.

(0x01 0x02 concat)          ; -> 0x0102
(0x0A0B 1 split)            ; split at index 1 -> link(0x0A, 0x0B)
(0x0102 count)              ; -> 2
(0x0102 0 index)            ; byte at index 0 -> 0x01
(0x0102 1 index)            ; byte at index 1 -> 0x02
(0x01 0x02 0x03 link link 0 count) ; -> 3 (link length)
("abc" 1 split)             ; -> link("a", "bc")
("abc" count)               ; -> 3

Higher-order sequence operators take a closure:

(1 2 3 (2 *) map)               ; -> link(2, link(4, link(6, NIL)))
(1 2 3 4 5 (3 >) filter)        ; -> link(4, link(5, NIL))
(1 2 3 (dup) each)               ; applies side-effect, returns original
(1 2 3 4 0 (+) fold)             ; left fold -> 10
(1 2 3 (4 5) zip)                ; -> link(link(1, 4), link(2, 5), link(3, NIL))

Comments

(1 2 +) ; line comment to end of line
(1 #;(2 3 +) 4 +) ; #; skips the middle expression

Semicolon starts a line comment. #; skips the following complete expression, even if nested.

Working with modules

Create a helper at modules/math.aex:

(
  (1 version def)
  (x y +) (a b) closure sum def
)

Import it from src/main.aex:

(
  (math "../modules/math.aex" import)
  (math.sum main def)
)

Import qualifies all symbols under the prefix. Intra-module references are rewritten at load time, so sum becomes math.sum.

Actors

spawn creates a named actor with its own mailbox and daemon thread. send enqueues a message; receive blocks until a message arrives. Actor names must be Symbols, so use symbol to construct them explicitly.

((1 2 +) "worker" symbol spawn)
(42 "worker" symbol send)
("worker" symbol receive)

Actors run in background threads. In deterministic mode, spawn, send, receive, ref, load, print, and println push NIL instead of executing. eval, closure, and apply work normally.

Tagged results and monad primitives

lib-py provides operators for explicit tagged-result construction (ok, err), pattern matching (result, match, is), and the ? suffix that wraps any operator's outcome as a tagged pair. The tag is always in the tail (terminal) position:

(42 ok)              ; success -> (42 . ok)
("bad" err)          ; error -> ("bad" . err)
(7 8 +?)             ; success -> (15 . ok)
(drop?)              ; error -> ("stack underflow" . err)

result unwraps a tagged value — if it is err it passes through, if ok the head value is extracted. Use match for arbitrary tag dispatch:

((42 . ok) result)         ; -> 42
((42 . ok) 'int succ fail match) ; tag-check then dispatch

is checks whether a value's terminal tag matches:

(42 'int is)   ; -> 0x01
(42 'str is)   ; -> 0x00
((42 . ok) 'ok is) ; -> 0x01

Bare operators (without ?) push NIL on error instead — see the def section for an example of redefinition being handled this way.

Putting it together

A small module that defines a sum helper and then uses it from a script:

; modules/math.aex
(
  (1 version def)
  (x y +) (a b) closure sum def
)
; src/main.aex
(
  (math "../modules/math.aex" import)
  (3 4 math.sum apply)
)

The imported helper is available as math.sum and evaluates to 7 when called with 3 and 4.