x-lang

x-lang Primitives and Core Forms

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.

  1. The C instruction set — implemented in C, bound under a bare name.
  2. Coordinates — implemented in C, reachable only through prim-ref or a class.
  3. What boots on top — operatives and procedures written in x-lang, which most of this file used to call primitives.

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}.


The C instruction set

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.

Quoting

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 -> 'abc

Pairs

pair

(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)) -> 1

rest

(rest p) -> obj

Returns the rest element (cdr) of pair p.

(rest (pair 1 2)) -> 2

Equality

eq?

(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

Comparison

<

(< a b) -> #t | #f

Returns #t if integer a is strictly less than integer b.

(< 1 2) -> #t
(< 2 1) -> #f

Arithmetic

+

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

Bitwise

~

(~ 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) -> 3

Binding

def

(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) -> 120

set!

(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 symbol

Control

match

(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)) -> 20

Functions

fn

(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))) -> 1

eval

(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)) -> 3

wrap

(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)) -> 3

unwrap

(unwrap applicative) -> combiner

Extracts the underlying combiner from an applicative created by wrap.

(unwrap (wrap (op (x) e x))) -> <operative>

Errors

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 oops

error

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

Coordinates

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") -> 5

Base, 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).

Types

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) -> #t

Type 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.


I/O

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>

Strings

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)) "") -> 0

str 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) -> #\o

str 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) -> -1

str ->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"

Meta

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)) -> 3

Base 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) -> 42

System

heap 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))) -> ()

What boots on top

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.

Comparison

>

(> 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) -> #f

Control

if

(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)) -> 2

let

(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) -> 10

Predicates

null?

(null? x) -> #t | #f

Returns #t if x evaluates to nil (()); #f otherwise.

(null? ()) -> #t
(null? 1) -> #f

pair?

(pair? x) -> #t | #f

Returns #t if x evaluates to a list pair; #f otherwise.

(pair? (list 1 2)) -> #t
(pair? 1) -> #f

atom?

(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)) -> #f

not

(not x) -> #t | #f

Logical negation. Returns #t if x evaluates to nil; #f otherwise. Equivalent to null?.

(not ()) -> #t
(not 1) -> #f

number?

(number? x) -> #t | #f

Returns #t if x evaluates to an integer; #f otherwise.

(number? 42) -> #t
(number? "hello") -> #f

str?

(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) -> #f

symbol?

(symbol? x) -> #t | #f

Returns #t if x evaluates to a symbol; #f otherwise.

(symbol? 'x) -> #t
(symbol? 42) -> #f

procedure?

(procedure? x) -> #t | #f

Returns #t if x evaluates to a callable (closure or C primitive); #f otherwise.

(procedure? +) -> #t
(procedure? 42) -> #f

char?

(char? x) -> #t | #f

Returns #t if x evaluates to a character object; #f otherwise.

(char? (Io read-char)) -> #t
(char? 42) -> #f

Lists

list

(list a ...) -> (a ...)

Constructs a proper list from zero or more evaluated arguments. (list) returns ().

(list 1 2 3) -> (1 2 3)
(list) -> ()

Logic

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) -> #t

or

(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) -> ()

I/O

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 42

newline

(newline) -> ()

Outputs a newline character to stdout. Takes no arguments. Returns ().

(newline) -> ()  ; prints \n

Quasiquote

quasi

(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)

Types

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"