x-lang: computational expressions over a minimal, type-agnostic engine.
This file documents the vocabulary the language answers to once it has booted, in three parts, because it is three different things and a single list said otherwise. What separates them is not style — it is where the code lives, which decides how a form behaves when it goes wrong, what shadows it, and whether it exists at all in a bare base.
prim-ref or a class.The C surface has one source of truth, and it is not this document:
tools/contract/isa.x in the engine, which make check-isa diffs against the
C source on every engine build. tools/check/primitives-doc.sh holds this
file against the same manifest, so a form documented in the wrong part fails
the gate rather than misleading a reader for a year.
Boolean true is #t; boolean false is #f. Nil is () (the empty list), and
falsy is exactly {(), #f}.
Every entry below is a C function the engine binds under a bare name — the
%isa-bare and %isa-keep blocks of isa.x. Primitives receive unevaluated
arguments (fexpr-style) and evaluate what they need internally, which is why
fn can be built on top of them; wrap supplies applicative semantics.
Each ISA entry carries a label justifying why it must be C at all — spine (the
evaluator itself), alloc, gc, raw-mem, raw-op. An entry that cannot
honestly take one is a migration candidate, not a fixture.
A bare name here is not always what a caller reaches. The %isa-keep
entries — the arithmetic, bitwise and comparison operators — bind bare even
when their catalogue namespace is de-registered, and the library then shadows
twelve of the sixteen with tower-aware generics. So + and < reach x-lang
code that dispatches over the numeric tower, while =, eq?, same? and
call/cc reach the C primitive directly. The entries below describe the C
behaviour; where a tower generic shadows one, the tower’s rules apply first.
A continuation that outlives the frames that captured it – a generator, a
coroutine – is captured through (Cont capture f) (x/sys/cont), which marks
what those frames hold while it waits; bare call/cc serves an escape.
lit(lit expr) -> expr
Returns expr unevaluated. This is the quoting primitive; the argument is never evaluated. The reader provides 'expr as shorthand.
(lit (+ 1 2)) -> ('+ 1 2)
'abc -> 'abcpair(pair a b) -> (a . b)
Constructs a pair (cons cell) from evaluated a and b.
(pair 1 2) -> (1 . 2)first(first p) -> obj
Returns the first element (car) of pair p.
(first (pair 1 2)) -> 1rest(rest p) -> obj
Returns the rest element (cdr) of pair p.
(rest (pair 1 2)) -> 2eq?(eq? a b) -> #t | #f
Tests whether evaluated a and b hold the same value word, whatever their types: an integer, a character or a symbol by its value, any other object by its first data word. Two distinct pairs whose first elements are the same object are eq?; same? is object identity. The rule is normative in the Specification.
(eq? 'x 'x) -> #t
(eq? "a" "a") -> #f
(eq? (pair 'x 1) (pair 'x 9)) -> #t=(= a b) -> #t | #f
Tests numeric value equality of evaluated a and b. Compares the integer values regardless of object identity.
(= 1 1) -> #t
(= 1 2) -> #f<(< a b) -> #t | #f
Returns #t if integer a is strictly less than integer b.
(< 1 2) -> #t
(< 2 1) -> #f+(+ a ...) -> integer
Variadic addition. Evaluates all arguments and returns their sum. Identity element is 0; (+) returns 0.
(+ 1 2 3) -> 6
(+) -> 0-(- a ...) -> integer
Variadic subtraction. With one argument, negates it. With two or more, subtracts all subsequent values from the first. With no arguments, returns 0.
(- 5 3) -> 2
(- 5) -> -5
(- 10 3 2) -> 5
(-) -> 0*(* a ...) -> integer
Variadic multiplication. Evaluates all arguments and returns their product. Identity element is 1; (*) returns 1.
(* 2 3 4) -> 24
(*) -> 1/(/ a ...) -> integer
Variadic integer division. With no arguments, returns 1 (identity). With one argument, returns that value unchanged. With two or more, divides the first by each subsequent value left to right.
(/ 10 2) -> 5
(/ 100 2 5) -> 10
(/) -> 1%(% a ...) -> integer
Variadic integer modulo. With one argument, returns that value unchanged; with
two or more, applies modulo left to right. With no arguments it is an error
(%: needs at least one argument) — unlike + and *, which have identities
to return, and unlike /, which answers 1.
(% 10 3) -> 1
(% 17 10 3) -> 1
(% 5) -> 5~(~ n) -> integer
Bitwise NOT (one’s complement) of integer n.
(~ 0) -> -1
(~ -1) -> 0&(& a b) -> integer
Bitwise AND of integers a and b.
(& 6 3) -> 2
(& 255 15) -> 15|(| a b) -> integer
Bitwise OR of integers a and b.
(| 6 3) -> 7
(| 0 5) -> 5^(^ a b) -> integer
Bitwise XOR of integers a and b.
(^ 6 3) -> 5
(^ 5 5) -> 0<<(<< a b) -> integer
Left bit shift of integer a by b positions.
(<< 1 4) -> 16
(<< 3 2) -> 12>>(>> a b) -> integer
Right bit shift of integer a by b positions (arithmetic shift).
(>> 16 4) -> 1
(>> 12 2) -> 3def(def name expr) -> value
Binds name (unevaluated symbol) to the result of evaluating expr in the current environment. expr is evaluated before the binding is created, so it cannot read the binding being defined; recursive functions still work because a closure body resolves names at call time. To reference the name while expr itself evaluates, forward-declare with (def name ()) then (set! name expr).
(def x 42) -> 42
(def fact (fn (_ n) (if (= n 0) 1 (* n (fact (- n 1))))))
(fact 5) -> 120set!(set! name expr) -> value
Mutates an existing binding of name to the result of evaluating expr. Signals an error if name is not already bound in the current environment.
(def x 1) -> 1
(set! x 2) -> 2
(set! unbound 0) -> error: Unbound symbolmatch(match (test expr) ...) -> value
Multi-branch conditional (cond-style). Evaluates each test in order; for the first truthy test, tail-evaluates the corresponding expr and returns it. Returns () if no test succeeds.
(match
((= 1 2) 10)
((= 1 1) 20)
(#t 30)) -> 20fn(fn (params ...) body ...) -> procedure
Creates a closure (applicative, lexically scoped). params are not evaluated; they name the formal parameters. Every closure receives itself as an implicit first argument — by convention the first formal is _ when unused, or self when the body recurses through it. Supports variadic binding: if params is a single symbol instead of a list, it captures the entire argument list, whose head is the closure itself.
(def add (fn (_ a b) (+ a b)))
(add 1 2) -> 3
(def fact (fn (self n) (if (= n 0) 1 (* n (self (- n 1))))))
(fact 5) -> 120
(def id (fn args (rest args)))
(id 1 2 3) -> (1 2 3)op(op formals env-param body ...) -> operative
Creates an operative (user-level fexpr). Like fn, but receives arguments unevaluated. formals binds the raw argument tree, and env-param binds the dynamic environment, giving the operative manual control over evaluation.
(def my-quote (op (x) e x))
(my-quote (+ 1 2)) -> ('+ 1 2)apply(apply f args) -> value
Calls callable f with a pre-evaluated list of arguments args. Works with both closures and C primitives. Arguments in the list are not re-evaluated.
The library binds apply over this primitive so that a value whose type has a call handler applies through it too, and a value with no handler raises a type error; see Call dispatch in the type system guide.
(apply + (list 1 2 3)) -> 6
(apply first (list (list 1 2))) -> 1eval(eval expr [env]) -> value
Evaluates the already-evaluated expression expr. With an optional env argument, evaluates expr in that environment instead of the current one. The environment is restored after evaluation.
(eval '(+ 1 2)) -> 3wrap(wrap combiner) -> applicative
Wraps a combiner (operative or primitive) to create an applicative that evaluates its arguments before passing them to the underlying combiner.
(def my-op (op (x) e x))
(def my-fn (wrap my-op))
(my-fn (+ 1 2)) -> 3unwrap(unwrap applicative) -> combiner
Extracts the underlying combiner from an applicative created by wrap.
(unwrap (wrap (op (x) e x))) -> <operative>guard(guard (var handler-body ...) body ...) -> value
Error recovery form. Evaluates body forms in sequence. If an error is signalled during evaluation, binds the error value to var and evaluates handler-body forms instead. The environment is restored to its state before body after an error. Handlers can be nested.
(guard (e (display e) (newline) 0)
(error "oops")) -> 0 ; prints oopserror(error message) -> <does not return>
Signals an error with the evaluated message. If a guard handler is installed, the error is caught and message is bound to the handler variable. If no handler is installed, the error is fatal. message may be a string or any object.
(error "something went wrong") -> <error signalled>These are C primitives with no bare name at all. They are filed in the
prims catalogue under a namespace, and the namespaces str, mem, heap, io,
sym and type are de-registered — so prim-ref is the door:
((prim-ref (lit str) (lit byte-len)) "hello") -> 5Base, Type and Io are classes, not prims: the spellings below are the
class’s own selectors, and the class reaches the same catalogue coordinate on
your behalf. Both routes are documented here because both are how the surface
is actually used.
A coordinate reached through a class also gets the class’s argument handling. That is not cosmetic: a malformed call through the class raises, where the same primitive reached raw can walk off unchecked data — the C layer is a CPU and checks nothing (see Errors).
Type ?(Type ? obj type-handle) -> #t | #f
Returns #t if the runtime type of obj matches type-handle (as returned by (Type make …)); #f otherwise. Returns #f for nil or objects without a type.
(def my-t (Type make "my-type" (list)))
(Type ? (Type make-instance my-t 42) my-t) -> #tType make(Type make name handlers) -> type-handle
Creates a new runtime type with string name and an association list of handlers. Supported handler keys include call, write, analyse, read, iter, from, to, and ops, each mapping to a closure. Returns a type handle atom used to create instances and check types.
(def my-type (Type make "my-type" (list (pair 'call (fn (_ obj . args) args))))) -> <type-handle>Type make-instance(Type make-instance type-handle data) -> instance
Creates a new instance of the runtime type identified by type-handle, storing data as its contents. Returns () if the type handle is not registered.
(def my-t (Type make "my-type" (list)))
(Type make-instance my-t 42) -> <instance>Type of(Type of value) -> type-handle | ()
Returns the runtime type handle of value (() for nil). The handle is the interned name atom; conversions and dispatch key on it.
Io read(Io read) -> obj
Reads and parses one s-expression from stdin. Returns the parsed object.
(Io read) -> <parsed s-expression from stdin>Io read-char(Io read-char) -> char | ()
Reads a single character from stdin. Returns a character object, or () on end-of-input.
(Io read-char) -> <char>str byte-len((prim-ref (lit str) (lit byte-len)) str) -> integer
The length of str in bytes.
((prim-ref (lit str) (lit byte-len)) "hello") -> 5
((prim-ref (lit str) (lit byte-len)) "") -> 0str byte-ref((prim-ref (lit str) (lit byte-ref)) str index) -> char
The character at the zero-based byte index.
((prim-ref (lit str) (lit byte-ref)) "hello" 0) -> #\h
((prim-ref (lit str) (lit byte-ref)) "hello" 4) -> #\ostr append((prim-ref (lit str) (lit append)) str1 str2) -> string
Concatenates two strings, returning a new one.
((prim-ref (lit str) (lit append)) "hello" " world") -> "hello world"str byte-sub((prim-ref (lit str) (lit byte-sub)) str start len) -> string
A new string of len bytes starting at zero-based start.
The third argument is a LENGTH, not an end index. Both readings agree at
start 0, which is why a library caller passed (+ off n) here for months and
only fields at a non-zero offset came back wrong (found by the conformance
suite, which uses a non-zero start on purpose).
((prim-ref (lit str) (lit byte-sub)) "hello" 1 3) -> "ell"mem cmp((prim-ref (lit mem) (lit cmp)) a b n) -> 0 | -1 | 1
Block comparison over n bytes: a true memcmp, so NULs do not terminate it.
This is what string equality bottoms out in; there is no string=? primitive.
((prim-ref (lit mem) (lit cmp)) "abc" "abc" 3) -> 0
((prim-ref (lit mem) (lit cmp)) "abc" "xyz" 3) -> -1str ->sym / sym ->str((prim-ref (lit str) (lit ->sym)) str) -> symbol
((prim-ref (lit sym) (lit ->str)) sym) -> string
Interning, both ways. Symbols intern per base, so the same spelling on either
side of a base make boundary gives two different objects.
((prim-ref (lit str) (lit ->sym)) "hello") -> 'hello
((prim-ref (lit sym) (lit ->str)) (lit hello)) -> "hello"Base make(Base make) -> base
Creates a fresh, sandboxed interpreter base environment with all built-in types and primitives registered. The new base has its own environment, type registry, and read buffer.
(def b (Base make)) -> <base>Base eval(Base eval base expr) -> value
Evaluates expression expr in the target base environment. List nil terminators are rewritten to match the target base. Errors in the target base propagate to the calling base if a guard handler is installed.
(def b (Base make))
(Base eval b '(+ 1 2)) -> 3Base bind(Base bind base name value) -> value
Binds name to value in the target base environment. List values are rewritten to use the target base’s nil. All arguments are evaluated in the calling environment before binding in the target.
(def b (Base make))
(Base bind b 'x 42) -> 42heap collect((prim-ref (lit heap) (lit collect))) -> ()
Triggers garbage collection by marking all objects reachable from the base
environment. Returns ().
There is no bare gc: collection is a coordinate in the heap namespace,
which is de-registered, so prim-ref is the door. The sibling coordinates —
heap count, heap mark, heap pin!, heap mark-hook!, heap free-hook! —
reach the collector the same way.
((prim-ref (lit heap) (lit collect))) -> ()Everything else the booted language answers to is written in x-lang, in
lib/x/boot/ and lib/x/core/, and appears in no block of isa.x: if,
do, let, and, or, quasi, write and display (operatives, which
receive their arguments unevaluated like a prim), the predicates, list and
newline (procedures), the comparisons, and the classes. It is library code,
with the consequences that follow: it can be shadowed, it is absent from a
bare base that has not loaded it, and its errors come from x-lang rather than
C.
Each of those carries a (doc …) form in the source, and the
x-lang API Reference
is generated from them; (help if) answers from the same form at the prompt.
Standard Library says how the library is laid out.