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

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

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.

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

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

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