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 tag 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 catalog 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.
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 scalar-value identity of evaluated a and b: the same object, or two scalars (integers, characters) carrying the same value, compare #t. Use same? for strict object identity.
(eq? 'x 'x) -> #t
(eq? 1 1) -> #t
(eq? "a" "a") -> #f=(= 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.
(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 catalog 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 catalog 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 below is written in x-lang, in lib/x/boot/ and lib/x/core/,
and appears in no block of isa.x. It is documented here because it is core
vocabulary — if and let are not optional extras — but 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.
if, do, let, and, or, quasi, write and display are
operatives (they receive their arguments unevaluated, like a prim); the
predicates, list and newline are procedures. The rest of the library —
List, Str, Num, Vector and the classes generally — is documented in
standard-library.md.
>(> a b) -> #t | #f
Returns #t if integer a is strictly greater than integer b.
(> 2 1) -> #t
(> 1 2) -> #f<=(<= a b) -> #t | #f
Returns #t if integer a is less than or equal to integer b.
(<= 1 1) -> #t
(<= 2 1) -> #f>=(>= a b) -> #t | #f
Returns #t if integer a is greater than or equal to integer b.
(>= 1 1) -> #t
(>= 0 1) -> #fif(if cond then [else]) -> value
Evaluates cond. If truthy (not ()), tail-evaluates then. If falsy, tail-evaluates else when provided, or returns (). Uses tail-call optimization for the selected branch.
(if #t 1 2) -> 1
(if () 1 2) -> 2
(if () 1) -> ()do(do form ...) -> value
Evaluates each form in sequence and returns the value of the last one. The final form is tail-evaluated for TCO. With no arguments, returns ().
(do 1 2 3) -> 3
(do (def x 1) (+ x 1)) -> 2let(let ((name val) ...) body ...) -> value
Creates local bindings by evaluating each val in the current environment, then evaluates body forms in the extended environment. The final body form is tail-evaluated. Environment is restored after let completes.
(let ((x 1) (y 2)) (+ x y)) -> 3
(let ((x 10)) x) -> 10null?(null? x) -> #t | #f
Returns #t if x evaluates to nil (()); #f otherwise.
(null? ()) -> #t
(null? 1) -> #fpair?(pair? x) -> #t | #f
Returns #t if x evaluates to a list pair; #f otherwise.
(pair? (list 1 2)) -> #t
(pair? 1) -> #fatom?(atom? x) -> #t | #f
Returns #t if x evaluates to a non-pair (atom); #f if it is a list pair. The inverse of pair?.
(atom? 1) -> #t
(atom? (list 1 2)) -> #fnot(not x) -> #t | #f
Logical negation. Returns #t if x evaluates to nil; #f otherwise. Equivalent to null?.
(not ()) -> #t
(not 1) -> #fnumber?(number? x) -> #t | #f
Returns #t if x evaluates to an integer; #f otherwise.
(number? 42) -> #t
(number? "hello") -> #fstr?(str? x) -> #t | #f
Returns #t if x evaluates to a string; #f otherwise. The spelling is
str?, matching the str namespace the string coordinates live in — there is
no string?.
(str? "hello") -> #t
(str? 42) -> #fsymbol?(symbol? x) -> #t | #f
Returns #t if x evaluates to a symbol; #f otherwise.
(symbol? 'x) -> #t
(symbol? 42) -> #fprocedure?(procedure? x) -> #t | #f
Returns #t if x evaluates to a callable (closure or C primitive); #f otherwise.
(procedure? +) -> #t
(procedure? 42) -> #fchar?(char? x) -> #t | #f
Returns #t if x evaluates to a character object; #f otherwise.
(char? (Io read-char)) -> #t
(char? 42) -> #flist(list a ...) -> (a ...)
Constructs a proper list from zero or more evaluated arguments. (list) returns ().
(list 1 2 3) -> (1 2 3)
(list) -> ()and(and expr ...) -> value
Short-circuit logical AND. Evaluates each expr left to right. Short-circuits
to #f — not to the falsy value that stopped it. If every value is truthy,
returns the last one; with no arguments, returns #t.
The last expression is returned as-is, so a falsy value in final position comes
back unchanged: (and 1 ()) is (), while (and 1 () 3) is #f. Falsy is
{(), #f}, and only a short circuit normalizes it.
(and 1 2 3) -> 3
(and 1 () 3) -> #f
(and 1 ()) -> ()
(and) -> #tor(or expr ...) -> value
Short-circuit logical OR. Evaluates each expr left to right, returning the
first truthy value. If every value is falsy it returns the last one, not a
normalized (): (or () #f) is #f and (or #f ()) is (). With no
arguments, returns ().
(or () () 3) -> 3
(or 1 2) -> 1
(or () #f) -> #f
(or) -> ()write(write obj) -> ()
Outputs the s-expression representation of evaluated obj to stdout (strings are quoted, special characters escaped). Returns ().
(write "hello") -> () ; prints "hello" (with quotes)display(display obj) -> ()
Outputs the human-readable representation of evaluated obj to stdout. Strings are printed without surrounding quotes; all other types use s-expression format. Returns ().
(display "hello") -> () ; prints hello (without quotes)
(display 42) -> () ; prints 42newline(newline) -> ()
Outputs a newline character to stdout. Takes no arguments. Returns ().
(newline) -> () ; prints \nquasi(quasi template) -> obj
Quasiquote expansion. Returns template with unquote and unquote-splicing forms evaluated. Atoms and non-list values are returned as-is. (unquote expr) within the template is replaced by the evaluated expr. (unquote-splicing expr) splices the evaluated list into the surrounding list.
(def x 1)
(quasi (a (unquote x) b)) -> ('a 1 'b)
(def xs (list 2 3))
(quasi (1 (unquote-splicing xs) 4)) -> (1 2 3 4)Type name(Type name obj-or-handle) -> string | ()
Returns the name string of obj’s runtime type, or of a type handle directly. Returns () if obj is nil or has no type.
(def my-t (Type make "my-type" (list)))
(Type name (Type make-instance my-t 42)) -> "my-type"