Version: 0.5.2
This document is the normative specification for x-lang. Each section maps 1:1
to a test file in tests/x/specs/. Behavior described here MUST be implemented
and tested. Items marked TBD have uncertain semantics and need investigation.
All primitives receive unevaluated arguments (fexpr-style) and evaluate what
they need internally. Boolean true is #t; boolean false is #f. Nil is ()
(the empty list).
x-lang uses the fexpr evaluation model: every combiner at the C level receives
its arguments unevaluated. Applicative semantics (automatic argument evaluation)
are provided by fn, which wraps a closure. This is the inverse of most Lisps
where functions evaluate arguments by default and macros are the special case.
In x-lang, operatives (op) are the default and applicatives (fn) are the
special case. This follows the Kernel language design.
Integers, strings, and characters evaluate to themselves.
42 -> 42
"hello" -> "hello"
#\a -> #\aA symbol evaluates to the value bound to it in the current environment. An unbound symbol signals an error.
(def x 10)
x -> 10A list (f args ...) evaluates f to obtain a combiner, then applies it. For
applicatives (created by fn or wrap), each argument is evaluated before the
call. For operatives (created by op or C primitives), arguments are passed
unevaluated.
Calling an applicative with an IMPROPER argument list – (list 1 . 5) – is
an error: the argument walk raises call: improper argument list (dotted
tail) rather than reading past the spine (#69 ruled). Operatives are
untouched: they receive their spines raw, and a dotted parameter spec binds a
dotted tail legitimately. When f evaluates to a non-callable value the form
is DATA, not a call, and echoes back unchanged – proper or dotted (see
Lists, section 10).
(guard (e "caught") (list 1 . 5)) -> "caught"
(1 . 2) -> (1 . 2)The empty list () is nil. Nil, the empty list, and absence are ONE value —
the pun is deliberate: a list is its first pair or the absence of one, and
nil’s representation (the null object) is what makes every list-termination
test free. The boolean false value is #f, a distinct canonical atom carried
on the base. () self-evaluates.
() -> ()Truthiness: exactly two values are falsy — nil and #f. Everything else
is truthy, including 0, "", and empty vectors/dicts (which are real
objects, distinct from nil; only lists pun empty with absence).
This model (Model A) is a settled, adjudicated decision. The design record —
including the priced migration analysis for the nil ≠ () alternative
(Model C), kept for any future major version — is issue #41; do not relitigate
the model without reading it.
The absence discipline (normative for the standard library):
#t/#f, never a useful value.() — never #f.#f must offer a presence
door — has?, or a presence-based -or/-or-else default — never a
value sentinel. (Internal walkers use one-element boxes.)() like every other miss. Negative indexes
are valid positions — they count from the end on strict indexed
collections (Gen excepted: a lazy stream has no end) — so no number is
ever a miss, and checked refs reject a nil index loudly. (OS-boundary
tables keep the OS’s own -1 invalid marker, per rule 5.)null; ()
crossing a boundary always means the empty sequence.and/or are value operators: and normalizes failure to #f (it
answers “did all pass”); or returns its first truthy value, and when
nothing is truthy it passes its LAST operand through unchanged – so
(or () #f) is #f and (or #f ()) is (). (or) is (). Use
not if you need a normalized answer rather than the operand.The following forms evaluate their final expression in tail position:
if: the selected branchdo: the last body formmatch: the body of the matching clauselet: the last body formfn: the last body formand: the last expressionor: the last expressionapply: the last body form of the applied procedureProper tail calls MUST NOT grow the stack. A tail-recursive loop MUST be able to iterate without limit.
(def loop (fn (self n) (if (= n 0) #t (self (- n 1)))))
(loop 1000000) -> #tMutually tail-recursive functions MUST also run in constant stack space.
(def even-tc (fn (_ n) (if (= n 0) #t (odd-tc (- n 1)))))
(def odd-tc (fn (_ n) (if (= n 0) #f (even-tc (- n 1)))))
(even-tc 100000) -> #tlit(lit expr) -> expr
Returns expr unevaluated. This is the quoting primitive. The reader provides
'expr as shorthand for (lit expr) (see core/quote-reader.spec.md).
(lit (+ 1 2)) -> ('+ 1 2)
(lit abc) -> 'abc
'abc -> 'abc
'(1 2 3) -> (1 2 3)pair(pair a b) -> (a . b)
Constructs a pair from evaluated a and b.
(pair 1 2) -> (1 . 2)
(pair 1 (pair 2 ())) -> (1 2)first(first p) -> obj
Returns the first element of pair p. Calling (first ()) is undefined — in
practice it dereferences nil and takes the process down. Importing
x/tool/safe-access shadows first and rest with guarded closures so both
raise instead; it is opt-in because the library walks its own lists through
the same globals and the guard costs 1.4x–1.7x.
(first (pair 1 2)) -> 1
(first (list 10 20 30)) -> 10rest(rest p) -> obj
Returns the rest element of pair p. Calling (rest ()) is undefined; see
first for the opt-in guard.
(rest (pair 1 2)) -> 2
(rest (list 10 20 30)) -> (20 30)list(list a ...) -> (a ...)
Constructs a proper list from zero or more evaluated arguments.
(list 1 2 3) -> (1 2 3)
(list) -> ()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 reference the binding being defined. Recursive functions still work: a
closure body resolves names when the closure is called, by which time the
def has completed. For a definition that needs the name while expr itself
evaluates, forward-declare it: (def name ()) then (set! name expr). def
always creates a new binding; it shadows any existing binding with the same
name rather than replacing it.
(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.
Walks the scope chain to find the nearest enclosing binding of name and
modifies it in place. Signals an error if name is not bound in any scope.
(def x 1)
(set! x 2)
x -> 2if(if cond then [else]) -> value
Evaluates cond. If truthy, tail-evaluates then. If falsy, tail-evaluates
else (or returns () if omitted).
(if #t 1 2) -> 1
(if () 1 2) -> 2
(if () 1) -> ()do(do form ...) -> value
Evaluates each form in sequence and returns the last value. The final form is
tail-evaluated. With no arguments, returns ().
(do 1 2 3) -> 3
(do (def x 1) (+ x 1)) -> 2match(match (test expr) ...) -> value
Multi-branch conditional. Evaluates each test in order; for the first truthy
test, tail-evaluates the corresponding expr and returns it. Returns () if
no test succeeds. Each clause has exactly ONE body form; for multiple
expressions, wrap in do.
(match
((= 1 2) 10)
((= 1 1) 20)
(#t 30)) -> 20let(let ((name val) ...) body ...) -> value
Creates local bindings, evaluates body forms in the extended environment, and
returns the last value. 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) -> 10Lists support direct indexing when called as functions. A single integer
argument returns the element at that zero-based index. Negative indices count
from the end. Two integer arguments (lst start len) return a sublist.
(def xs (list 10 20 30 40))
(xs 0) -> 10
(xs 2) -> 30
(xs -1) -> 40
(xs 1 2) -> (20 30)fn(fn (params ...) body ...) -> procedure
Creates a closure (applicative). Arguments are evaluated before binding.
Every closure receives itself as an implicit first argument: a call
(add 1 2) binds the first formal to the add closure itself, the second
to 1, and the third to 2. By convention the first formal is named _
when unused, or self when the body recurses through it — self-recursion
needs no global name and survives rebinding. Supports variadic: if params
is a single symbol, it captures the entire argument list, whose head is the
closure itself. A dotted-pair parameter list (_ a . rest) binds named
parameters and collects remaining arguments into rest.
(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)
(def f (fn (_ a b . rest) rest))
(f 1 2 3 4 5) -> (3 4 5)Closures capture their lexical environment:
(def make-adder (fn (_ n) (fn (_ x) (+ n x))))
((make-adder 10) 5) -> 15op(op formals env-param body ...) -> operative
Creates an operative (fexpr). Like fn, but arguments are NOT evaluated.
formals binds the raw argument tree, env-param binds the dynamic
environment.
(def my-quote (op (x) e x))
(my-quote (+ 1 2)) -> ('+ 1 2)The environment parameter can be used for selective evaluation:
(def my-if (op (c t f) e (if (eval c e) (eval t e) (eval f e))))
(my-if (= 1 1) "yes" "no") -> "yes"wrap(wrap combiner) -> applicative
Wraps a combiner to create an applicative that evaluates 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. Calling unwrap on a
value that was not created by wrap is undefined behaviour.
(def my-op (op (x) e x))
(def my-fn (wrap my-op))
((unwrap my-fn) (+ 1 2)) -> ('+ 1 2)apply(apply f args) -> value
Calls callable f with a pre-evaluated list of arguments. Arguments are not
re-evaluated. When applying a C primitive, the arguments must be self-evaluating
values (integers, strings, etc.) since primitives may internally evaluate their
arguments.
(apply + (list 1 2 3)) -> 6
(apply list (list 1 2 3)) -> (1 2 3)eval(eval expr [env]) -> value
Evaluates expression expr. With optional env, evaluates in that environment.
(eval '(+ 1 2)) -> 3and(and expr ...) -> value
Short-circuit logical AND. Evaluates each expr left to right. Normalizes
failure to #f at the first falsy value – see the absence discipline in
section 1, which this section previously contradicted. If all truthy, returns
the last value. (and) returns #t.
(and 1 2 3) -> 3
(and 1 () 3) -> #f
(and) -> #tor(or expr ...) -> value
Short-circuit logical OR. Returns the first truthy value. If every operand is
falsy, the LAST operand passes through unchanged – or does not normalize
its failure the way and does, so the result is whichever of () or #f you
supplied last. (or) returns ().
(or () () 3) -> 3
(or 1 2) -> 1
(or () #f) -> #f
(or #f ()) -> ()
(or) -> ()not(not x) -> #t | #f
Logical negation. Returns #t if x is falsy; #f otherwise.
(not ()) -> #t
(not 1) -> #f
(not #t) -> #fguard(guard (var handler-body ...) body ...) -> value
Error recovery. Evaluates body forms. If an error is signalled, binds the
error value to var and evaluates handler-body instead. Handlers nest.
The error value depends on the raiser: (error x) delivers x itself,
(Err raise ...) delivers an Err instance, and C-raised errors (an unbound
symbol, the built-in guards) deliver a message-carrying atom that prints as
its message – (display (guard (e e) nosuchsym)) shows
Unbound SYMBOL 'nosuchsym', and an uncaught one prints the same text after
Error: (#54).
(guard (e e) (error "oops")) -> "oops"
(guard (e "caught") (+ 1 2)) -> 3Nested guards:
(guard (e (Str8 append "outer: " e))
(guard (e (error (Str8 append "re: " e)))
(error "inner"))) -> "outer: re: inner"error(error message) -> <does not return>
Signals an error. If a guard handler is installed, the error is caught.
Without a handler, error terminates the process.
(guard (e e) (error "fail")) -> "fail"All arithmetic operators are variadic and evaluate their arguments.
+Arithmetic operands must be numbers. A NIL operand raises (the prims share
eq?’s nil-safety convention), and a non-numeric operand – string,
list, pair, vector – raises err:type through the same registry that
dispatches the numeric tower: each of those types registers refusal handlers
for + - * / % <, so (+ 1 "abc") errors instead of reading the string’s
pointer as an integer (#52). CHARACTERS are exempt by
contract: a char IS its code point arithmetically – (- #\3 #\0) is 3 –
and the regex engine, utf8 decode, and the printer all depend on it. One
documented residual: symbols (their type slot is the interning tree; the registry cannot carry ops for
them) still fall through to machine arithmetic. The booleans are a real
BOOL type (#101) – (Type of #t) answers, and (+ #t 1) refuses through
the same registry.
(Type name (Type of #t)) -> "BOOL"
(guard (e "caught") (+ #t 1)) -> "caught"(~ & | ^ << >>) is stricter: integer or char operands only, enforced in its
wrappers.
(guard (e "caught") (+ 1 ())) -> "caught"
(guard (e "caught") (+ 1 "abc")) -> "caught"
(guard (e "caught") (& "a" 1)) -> "caught"(+ a ...) -> integer
Addition. Identity: 0.
(+ 1 2 3) -> 6
(+) -> 0
(+ 5) -> 5-(- a ...) -> integer
Subtraction. One argument: negation. Zero arguments: 0.
(- 5 3) -> 2
(- 5) -> -5
(- 10 3 2) -> 5
(-) -> 0*(* a ...) -> integer
Multiplication. Identity: 1.
(* 2 3 4) -> 24
(*) -> 1/(/ a ...) -> integer
Integer division. Identity: 1. Division by an integer zero raises an error
(#80); it was previously undefined, and in practice took an uncatchable
hardware trap. Boxed tower zeros are not affected — a float divisor keeps
IEEE semantics, a rational zero keeps the rational constructor’s own error.
(/ 10 2) -> 5
(/ 100 2 5) -> 10
(/) -> 1%(% a ...) -> integer
Integer modulo. Unlike + - * /, % has no identity element: calling it
with no arguments is an error, not 0 (#72). One argument passes through.
Modulo by an integer zero raises an error (#80), as with /.
(% 10 3) -> 1
(% 17 10 3) -> 1
(% 7) -> 7~(~ n) -> integer
Bitwise NOT (one’s complement).
(~ 0) -> -1
(~ -1) -> 0&(& a b) -> integer
Bitwise AND.
(& 6 3) -> 2
(& 255 15) -> 15|(| a b) -> integer
Bitwise OR.
(| 6 3) -> 7
(| 0 5) -> 5^(^ a b) -> integer
Bitwise XOR.
(^ 6 3) -> 5
(^ 5 5) -> 0<<(<< a b) -> integer
Left shift.
(<< 1 4) -> 16
(<< 3 2) -> 12>>(>> a b) -> integer
Right shift (arithmetic).
(>> 16 4) -> 1
(>> 12 2) -> 3eq?(eq? a b) -> #t | #f
Scalar-value identity: the same object, or two scalars (integers,
characters) carrying the same value. Symbols with the same name are
interned and thus eq?. Use same? for strict object identity.
(eq? 'x 'x) -> #t
(eq? 1 1) -> #t
(eq? "a" "a") -> #f=(= a b) -> #t | #f
Numeric/value equality. Compares integer values (and characters by code point). Comparing values of different types is undefined.
(= 1 1) -> #t
(= 1 2) -> #f<(< a b) -> #t | #f
(< 1 2) -> #t
(< 2 1) -> #f>(> a b) -> #t | #f
(> 2 1) -> #t
(> 1 2) -> #f<=(<= a b) -> #t | #f
(<= 1 1) -> #t
(<= 2 1) -> #f>=(>= a b) -> #t | #f
(>= 1 1) -> #t
(>= 0 1) -> #fnull?(null? x) -> #t | #f
Returns #t if x is nil.
(null? ()) -> #t
(null? 1) -> #fpair?(pair? x) -> #t | #f
Returns #t if x is a pair.
(pair? (list 1 2)) -> #t
(pair? 1) -> #fatom?(atom? x) -> #t | #f
Returns #t if x is not a pair. Inverse of pair?.
(atom? 1) -> #t
(atom? (list 1 2)) -> #fnumber?(number? x) -> #t | #f
(number? 42) -> #t
(number? "hello") -> #fstr?(str? x) -> #t | #f
(str? "hello") -> #t
(str? 42) -> #fsymbol?(symbol? x) -> #t | #f
(symbol? 'x) -> #t
(symbol? 42) -> #fprocedure?(procedure? x) -> #t | #f
Returns #t if x is a fn closure, a wrap applicative, or a C primitive.
Returns #f for op operatives and all other values.
(procedure? +) -> #t
(procedure? (fn (_ x) x)) -> #t
(procedure? 42) -> #fchar?(char? x) -> #t | #f
Returns #t if x is a character object.
(char? #\a) -> #t
(char? 42) -> #fChar ->int(Char ->int c) -> integer (the bare char->integer global was de-registered
in R5; the class — or (prim-ref 'char '->int) for load-time/hot fetches —
is the surface)
Returns the integer code point of character c. Passing a non-character value
is undefined.
(Char ->int #\a) -> 97
(Char ->int #\A) -> 65Char from-int(Char from-int n) -> char (the bare integer->char global was de-registered
in R5; the class — or (prim-ref 'int '->char) — is the surface)
Returns the character with code point n.
(Char from-int 97) -> #\a
(Char from-int 65) -> #\A
(= (Char from-int 97) #\a) -> #tStr8 length(Str8 length str) -> integer
Returns the byte length of string str (not character count; x-lang strings
are byte arrays with no encoding awareness). The bare str-length spelling
is retired (#108): the boot layer keeps it %-private; the class is the
surface.
(Str8 length "hello") -> 5
(Str8 length "") -> 0Str8 ref(Str8 ref index str) -> char
Returns the character at zero-based index in str (index first, the
adjudicated seat order; negative counts from the end). The bare str-ref
spelling is retired (#108).
(Str8 ref 0 "hello") -> #\h
(Str8 ref 4 "hello") -> #\oStr8 append(Str8 append str1 str2 ...) -> string (ns str is de-registered: the class – or (prim-ref 'str 'append) for load-time/hot fetches – is the surface)
Concatenates exactly two strings. For multiple strings, use Str append (variadic).
(Str8 append "hello" " world") -> "hello world"
(Str8 append "" "x") -> "x"Str8 sub(Str8 sub start len str) -> string
Extracts len bytes starting at byte offset start (count-first seats, the
adjudicated order; start+length, not start+end). The bare substring
spelling is retired (#108).
(Str8 sub 1 2 "hello") -> "el"
(Str8 sub 0 5 "hello") -> "hello"str=?(str=? str1 str2) -> #t | #f
String content equality.
(str=? "abc" "abc") -> #t
(str=? "abc" "xyz") -> #fStr8 ->sym(Str8 ->sym str) -> symbol
Converts a string to an interned symbol.
(Str8 ->sym "hello") -> 'hellosymbol->str(symbol->str sym) -> string
Converts a symbol to a string.
(symbol->str 'hello) -> "hello"(Convert to n (Type of "")) -> string
Converts an integer to its decimal string representation through the
conversion catalog. The bare number->str spelling is retired (#108); the
boot layer keeps it %-private for the printer’s hot path.
(Convert to 42 (Type of "")) -> "42"
(Convert to -1 (Type of "")) -> "-1"(Convert to str (Type of 0) [radix]) -> integer | ()
Parses string as an integer. A 0x/0X prefix selects hex, matching the
reader’s literal; the sign parses first, so "-0xff" is -255. An explicit
radix argument disables prefix detection – the caller controls
interpretation, and digits run to radix 36. Non-numeric strings miss with
(), like every other miss; 0 would be indistinguishable from parsing
"0". (This section previously claimed a 0 fallback – stale text
predating the nil model – and documented the hex prefix before it was
implemented; #76 ruled it in.)
(Convert to "42" (Type of 0)) -> 42
(Convert to "0xff" (Type of 0)) -> 255
(Convert to "-0xff" (Type of 0)) -> -255
(Convert to "ff" (Type of 0) 16) -> 255
(Convert to "abc" (Type of 0)) -> ()
(guard (e "caught") (Convert to "12345678901234567890" (Type of 0))) -> "caught"Digits that overflow the machine integer RAISE rather than wrapping – a
literal parse silently becoming a different number corrupted 64-bit IDs in
JSON (#52). Accumulation is negative-domain, so INT_MIN parses exactly.
write(write obj) -> ()
Outputs the s-expression representation of obj to stdout. Strings are quoted,
special characters escaped. Returns ().
(write "hello") ; outputs: "hello"
(write 42) ; outputs: 42
(write (list 1 2)) ; outputs: (1 2)display(display obj) -> ()
Outputs human-readable representation. Strings are printed without quotes.
Returns ().
(display "hello") ; outputs: hello
(display 42) ; outputs: 42newline(newline) -> ()
Outputs a newline character.
Io read(Io read) -> obj
Reads and parses one s-expression from stdin. Behavior at EOF is implementation-dependent.
Io read-char(Io read-char) -> char | ()
Reads a single character from stdin. Returns () on end-of-input.
Heap collect(Heap collect) -> integer
Triggers garbage collection.
quasi(quasi template) -> obj
Quasiquote. Returns template with unquote and unquote-splicing forms
evaluated.
(def x 1)
(quasi (a (unquote x) b)) -> ('a 1 'b)unquote(unquote expr) – only valid inside quasi.
Evaluates expr and substitutes the result.
(def x 42)
(quasi (unquote x)) -> 42unquote-splicing(unquote-splicing expr) – only valid inside quasi.
Evaluates expr (must produce a list) and splices it into the surrounding list.
(def xs (list 2 3))
(quasi (1 (unquote-splicing xs) 4)) -> (1 2 3 4)Nested quasiquote is depth-tracked: each quasi deepens by one, each
unquote returns one level, and only a depth-1 payload evaluates – so the
inner form survives one wrapping as syntax while the innermost value is
substituted. The printer renders the surviving quasi form with the reader’s
shorthand:
(quasi (quasi (unquote (unquote 'x)))) -> `,'xThe reader converts text into s-expressions. The following syntactic forms are supported:
Sequences of digits, optionally preceded by - for negative numbers.
42 -> 42
-7 -> -7
0 -> 0Delimited by ". Strings support C-style backslash escape sequences:
| Escape | Byte | Name |
|---|---|---|
\" |
0x22 |
double quote |
\\ |
0x5C |
backslash |
\n |
0x0A |
newline |
\t |
0x09 |
tab |
\r |
0x0D |
carriage return |
\0 |
0x00 |
null |
\xHH |
HH | hex byte |
Escape sequences are processed at read time: "\n" is a one-character string
containing a newline byte. Unknown escape sequences (e.g., \q) preserve the
literal backslash and following character. Invalid \x sequences (not followed
by two hex digits) also preserve the literal characters.
The write function re-escapes special characters so that the output is a valid
string literal: (write "\n") prints "\n", not a raw newline.
Note: \0 produces a null byte, which terminates the string for all operations
that use byte-length (e.g., str-length, (Str8 append)).
"hello" -> "hello"
"" -> ""
"a\"b" -> "a\"b"
"a\\b" -> "a\\b"Sequences of non-whitespace, non-parenthesis, non-quote characters that don’t parse as integers.
abc -> <symbol>
+ -> <symbol>
my-var? -> <symbol>#\c where c is a single character. Named characters are also supported:
| Syntax | Character | Code |
|---|---|---|
#\space |
space | 32 |
#\newline |
newline (LF) | 10 |
#\tab |
horizontal tab | 9 |
#\a -> #\a
(Char ->int #\space) -> 32
(Char ->int #\newline) -> 10
(Char ->int #\tab) -> 9(a b c) creates a proper list. (a b . c) creates a dotted pair where c
is the tail. Bare data forms – proper or dotted – evaluate to themselves
through the non-callable pass-through (#69 ruled: a non-callable head was
never a call, so data echoes back; see List evaluation in section 1 for the
callable half of that ruling).
(1 2 3) -> (1 2 3)
(1 . 2) -> (1 . 2)
(1 2 . 3) -> (1 2 . 3)'expr is sugar for (lit expr).
'abc -> 'abc
'(1 2 3) -> (1 2 3)`expr is sugar for (quasi expr).
,expr is sugar for (unquote expr).
,@expr is sugar for (unquote-splicing expr).
; begins a line comment; everything until end-of-line is ignored.
; this is a comment
42 ; this is also a comment -> 42#(a b c) creates a vector.
#(1 2 3) -> #(1 2 3)#/pattern/ creates a regex.
#/abc/ -> #/abc/
#/a.*b/ -> #/a.*b/make-type(Type make name handlers) -> type-handle
Creates a new runtime type with string name and an alist of handlers.
Supported handler keys: call, write, length, analyse, delimit.
Returns a type handle used with make-instance and type?.
(def my-t (Type make "MY-T" (list)))make-instance(Type make-instance type-handle data) -> instance
Creates a new instance of the type. Data is stored and accessible via
(first instance).
(def my-t (Type make "MY-T" (list)))
(def obj (Type make-instance my-t 42))
(first obj) -> 42Custom type instances self-evaluate:
(def obj (Type make-instance my-t 42))
obj -> <instance>type?(Type ? obj type-handle) -> #t | #f
Returns #t if obj’s runtime type matches type-handle.
(Type ? obj my-t) -> #t
(Type ? 42 my-t) -> #ftype-name(Type name obj) -> string | ()
Returns the name string of obj’s type, or () if no type.
(Type name obj) -> "MY-T"
(Type name 42) -> "INTEGER"
(Type name "hi") -> "STRING"score-match(score-match score length reader) -> score
Sets the score fields for the tokenizer protocol. length is the match length,
reader is the read function to call. Used internally by custom type readers.
(score-match score 5 my-reader) -> ...(Illustrative: score and my-reader come from the worked reader above, so
the result is the mutated score object, not a printable literal.)
When a typed instance is called as a function, the call handler is invoked
with the instance followed by the arguments. Like every closure, the handler
also receives itself as implicit argument 0, so the instance binds as the
second formal.
(def counter-t (Type make "COUNTER"
(list (pair 'call (fn (_ self . args) (first self))))))
(def c (Type make-instance counter-t 42))
(c) -> 42When write or display outputs a typed instance, the write handler is
called with the instance (after the closure’s implicit self slot).
(def my-t (Type make "SHOW"
(list (pair 'write (fn (_ self) (display "[") (display (first self)) (display "]"))))))(Type wrap t) -> instance
Clothes a type handle (from Type of) or the type itself (from Type by-atom)
as an interactive Type instance carrying both forms: the handle member is
the name atom, raw the struct the wiring statics consume.
((Type wrap (Type of 0)) name) -> "INTEGER"
((Type wrap (Type by-atom (Type of 0))) name) -> "INTEGER"(t cell 'field-name) walks the layout contract
(engine/tools/contract/base-paths.x) to the object the type-rooted row for
field-name addresses — handler stacks, the conversion catalog cells, the
generic-operator alist. (t fields) lists the row names. A name whose row is
not type-rooted is refused: a base-rooted path stepped from a type
would address arbitrary spine words.
(null? ((Type wrap (Type of 0)) cell 'type-ops-stack)) -> #f
(null? (List filter (fn (_ n) (eq? n 'type-iter)) ((Type wrap (Type of 0)) fields))) -> #f
(guard (e 'refused) ((Type wrap (Type of 0)) cell 'line)) -> 'refusedThe push verbs wire handlers through the instance — push-write,
push-display, push-call, push-op — shadowing the current handler, and
(Type pop-write (t raw)) restores the write stack. Restyling a shared
built-in is a shadow-then-pop round trip (illustrative; the round trip is
pinned by tests/x/specs/lib/type.spec.md):
(def t-int (Type wrap (Type of 0)))
(t-int push-write (fn (_ n) (display (Str8 append "0x" (%number->str n 16)))))
; the write/echo mode now renders integers as 0x2a ...
(Type pop-write (t-int raw))
; ... and is restored byte-for-byteBase make(Base make) -> instance
Creates a fresh, sandboxed interpreter — all built-in types and C primitives,
no library — wrapped as a Base instance. The raw C base object rides the
instance’s raw member; every Base static accepts either form, and
(Base raw-of v) unwraps. A fresh child is the bare C ISA: no output verbs,
no catalog protocol, no reader macros — reach in with parent closures or
bind.
(def b (Base make))
(Base base? b) -> #t
(Base base? (b raw)) -> #f
(Base base? 5) -> #fBase eval(Base eval base expr) -> value
Evaluates expr in the target base environment.
(def b (Base make))
(Base eval b '(+ 1 2)) -> 3Bases are isolated:
(def b (Base make))
(Base eval b '(def x 42))
(Base eval b 'x) -> 42Base bind(Base bind base name value) -> value
Binds name to value in the target base.
(def b (Base make))
(Base bind b 'x 42)
(Base eval b 'x) -> 42The instance answers eval, bind, and make-type directly — the receiver
is the base:
(def b (Base make))
(b eval '(* 6 7)) -> 42(def b (Base make))
(b bind 'x 5)
(b eval 'x) -> 5The statics keep working on raw bases from the catalog prims — plumbing that holds a raw base passes it straight through:
(def rb ((prim-ref 'base 'make)))
(Base eval rb '(+ 40 2)) -> 42(b cell 'field-name) walks the layout contract
(engine/tools/contract/base-paths.x) to the object the base-rooted row for
field-name addresses; (Base fields) lists the row names. A cell-kind
field’s value sits in the cell’s first slot. A name whose row is not
base-rooted is refused — a type-rooted path stepped from a base spine would
address arbitrary interpreter state.
(def b (Base make))
(null? (List filter (fn (_ n) (eq? n 'type-alist)) (b fields))) -> #f(def b (Base make))
(b bind 'marker 77)
(list (b eval 'marker) (null? (rest (b cell 'env-root)))) -> (77 #t)(def b (Base make))
(guard (e 'refused) (b cell 'type-iter)) -> 'refusedStandard library functions for function composition and transformation.
Fn identity(Fn identity x) -> x
Returns its argument unchanged.
(Fn identity 42) -> 42Fn const(Fn const x) -> (fn (_ y) x)
Returns a function that always returns x.
((Fn const 5) 99) -> 5Fn compose(Fn compose f g) -> (fn (_ x) (f (g x)))
Right-to-left function composition.
((Fn compose (method-ref Num inc) (method-ref Num inc)) 3) -> 5Fn pipe(Fn pipe f g) -> (fn (_ x) (g (f x)))
Left-to-right function composition.
((Fn pipe (method-ref Num inc) (method-ref Num inc)) 3) -> 5Fn curry(Fn curry f x) -> (fn (_ y) (f x y))
Partially applies a two-argument function by fixing its first argument.
((Fn curry + 10) 5) -> 15Fn flip(Fn flip f) -> (fn (_ a b) (f b a))
Reverses the arguments of a binary function.
((Fn flip -) 1 10) -> 9Fn tap(Fn tap f) -> (fn (_ x) ...x)
Returns a function that applies f for side effects, then returns the argument.
((Fn tap write) 42) -> 42Fn complement(Fn complement pred) -> function
((Fn complement (method-ref Num even?)) 3) -> #tFn partial(Fn partial f . bound) -> function
((Fn partial + 10) 5) -> 15Fn juxt(Fn juxt . fns) -> function
((Fn juxt (method-ref Num inc) (method-ref Num dec)) 5) -> (6 4)Fn both(Fn both f g) -> function
((Fn both (method-ref Num positive?) (method-ref Num even?)) 4) -> #t
((Fn both (method-ref Num positive?) (method-ref Num even?)) 3) -> #fFn either(Fn either f g) -> function
((Fn either (method-ref Num positive?) (method-ref Num even?)) -2) -> #tFn all-pass(Fn all-pass preds) -> function
((Fn all-pass (list (method-ref Num positive?) (method-ref Num even?))) 4) -> #tFn any-pass(Fn any-pass preds) -> function
((Fn any-pass (list (method-ref Num positive?) (method-ref Num even?))) -2) -> #tNum inc(Num inc n) -> integer
(Num inc 5) -> 6
(Num inc -1) -> 0Num dec(Num dec n) -> integer
(Num dec 5) -> 4
(Num dec 0) -> -1Num negate(Num negate n) -> integer
(Num negate 7) -> -7
(Num negate -3) -> 3Num abs(Num abs n) -> integer
(Num abs -3) -> 3
(Num abs 3) -> 3Num min(Num min a b) -> integer
(Num min 3 7) -> 3Num max(Num max a b) -> integer
(Num max 3 7) -> 7Num clamp(Num clamp lo hi n) -> integer
Clamps n to the range [lo, hi].
(Num clamp 0 10 15) -> 10
(Num clamp 0 10 -5) -> 0
(Num clamp 0 10 5) -> 5Num min-by(Num min-by f a b) -> a | b
Returns whichever of a, b has the smaller (f x).
(Num min-by (method-ref Num abs) -5 3) -> 3Num max-by(Num max-by f a b) -> a | b
Returns whichever of a, b has the larger (f x).
(Num max-by (method-ref Num abs) -5 3) -> -5Num zero?(Num zero? n) -> #t | #f
(Num zero? 0) -> #t
(Num zero? 1) -> #fNum positive?(Num positive? n) -> #t | #f
(Num positive? 5) -> #t
(Num positive? -1) -> #f
(Num positive? 0) -> #fNum negative?(Num negative? n) -> #t | #f
(Num negative? -3) -> #t
(Num negative? 0) -> #fNum even?(Num even? n) -> #t | #f
(Num even? 4) -> #t
(Num even? 3) -> #fNum odd?(Num odd? n) -> #t | #f
(Num odd? 3) -> #t
(Num odd? 4) -> #fList sum(List sum lst) -> integer
(List sum (list 1 2 3)) -> 6List product(List product lst) -> integer
(List product (list 2 3 4)) -> 24boolean?(boolean? x) -> #t | #f
Returns #t if x is #t or #f.
(boolean? #t) -> #t
(boolean? #f) -> #t
(boolean? 1) -> #fFn default-to(Fn default-to d x) -> x | d
Returns x if non-nil, otherwise d.
(Fn default-to 0 ()) -> 0
(Fn default-to 0 42) -> 42Fn until(Fn until pred f x) -> value
Repeatedly applies f to x until pred is true.
(Fn until (fn (_ n) (> n 10)) (method-ref Num inc) 1) -> 11equal?(equal? a b) -> #t | #f
Value equality: numbers by value, strings by content, and structural for
pairs, lists and vectors (vectors via the %equal-others handler cell). Falls
back to identity (eq?) for everything else. Instances stay identity-compared.
(equal? 3 3) -> #t
(equal? "abc" "abc") -> #t
(equal? (list 1) (list 1)) -> #tfold(List fold f init lst) -> value
Left fold.
(List fold + 0 (list 1 2 3)) -> 6
(List fold (fn (_ acc x) (pair x acc)) () (list 1 2 3)) -> (3 2 1)List reduce(List reduce f lst) -> value
Left fold using first element as initial value.
(List reduce + (list 1 2 3)) -> 6List scan(List scan f init lst) -> list
Like fold but collects intermediate values.
(List scan + 0 (list 1 2 3)) -> (0 1 3 6)length(List length lst) -> integer
(List length (list 1 2 3)) -> 3
(List length ()) -> 0List ref(List ref n lst) -> value
Zero-based index.
(List ref 0 (list 10 20 30)) -> 10
(List ref 2 (list 10 20 30)) -> 30List last(List last lst) -> value
(List last (list 1 2 3)) -> 3List init(List init lst) -> list
All elements except the last.
(List init (list 1 2 3)) -> (1 2)append(List append a b) -> list
(List append (list 1 2) (list 3 4)) -> (1 2 3 4)List prepend(List prepend x lst) -> list
(List prepend 0 (list 1 2)) -> (0 1 2)reverse(List reverse lst) -> list
(List reverse (list 1 2 3)) -> (3 2 1)List flatten(List flatten lst) -> list
(List flatten (list 1 (list 2 (list 3)))) -> (1 2 3)map(List map f lst) -> list
(List map (method-ref Num inc) (list 1 2 3)) -> (2 3 4)filter(List filter pred lst) -> list
(List filter (method-ref Num even?) (list 1 2 3 4)) -> (2 4)for-each(List for-each f lst) -> ()
Applies f to each element for side effects only.
(List for-each display (list 1 2 3)) -> ()List flat-map(List flat-map f lst) -> list
Maps then flattens one level.
(List flat-map (fn (_ x) (list x x)) (list 1 2)) -> (1 1 2 2)List any?(List any? pred lst) -> #t | #f
(List any? (method-ref Num even?) (list 1 3 4)) -> #t
(List any? (method-ref Num even?) (list 1 3 5)) -> #fList all?(List all? pred lst) -> #t | #f
(List all? (method-ref Num even?) (list 2 4 6)) -> #t
(List all? (method-ref Num even?) (list 2 3 6)) -> #fList none?(List none? pred lst) -> #t | #f
(List none? (method-ref Num even?) (list 1 3 5)) -> #tList empty?(List empty? lst) -> #t | #f
(List empty? ()) -> #t
(List empty? (list 1)) -> #fList reject(List reject pred lst) -> list
Complement of filter.
(List reject (method-ref Num even?) (list 1 2 3 4)) -> (1 3)List find(List find pred lst) -> value | ()
(List find (method-ref Num even?) (list 1 3 4 6)) -> 4
(List find (method-ref Num even?) (list 1 3 5)) -> ()List find-index(List find-index pred lst) -> integer | ()
Misses return () like every other miss (negative indexes are valid
from-the-end positions, so no number can mark absence).
(List find-index (method-ref Num even?) (list 1 3 4)) -> 2
(List find-index (method-ref Num even?) (list 1 3 5)) -> ()List index-of(List index-of x lst) -> integer | ()
Misses return ().
(List index-of 3 (list 1 2 3 4)) -> 2List includes?(List includes? x lst) -> #t | #f
(List includes? 3 (list 1 2 3)) -> #t
(List includes? 9 (list 1 2 3)) -> #fList count-if(List count-if pred lst) -> integer
(List count-if (method-ref Num even?) (list 1 2 3 4)) -> 2List take(List take n lst) -> list
(List take 2 (list 1 2 3 4)) -> (1 2)List drop(List drop n lst) -> list
(List drop 2 (list 1 2 3 4)) -> (3 4)List take-while(List take-while pred lst) -> list
(List take-while (method-ref Num odd?) (list 1 3 4 5)) -> (1 3)List drop-while(List drop-while pred lst) -> list
(List drop-while (method-ref Num odd?) (list 1 3 4 5)) -> (4 5)List split-at(List split-at n lst) -> (list list)
(List split-at 2 (list 1 2 3 4)) -> ((1 2) (3 4))List slice(List slice start end lst) -> list
(List slice 1 3 (list 10 20 30 40)) -> (20 30)List range(List range start end) -> list
(List range 0 5) -> (0 1 2 3 4)List repeat(List repeat n x) -> list
(List repeat 3 0) -> (0 0 0)List times(List times n f) -> list
(List times 4 (method-ref Fn identity)) -> (0 1 2 3)List unfold(List unfold pred f g seed) -> list
(List unfold (fn (_ x) (> x 3)) (method-ref Fn identity) (method-ref Num inc) 1) -> (1 2 3)List iterate(List iterate f n x) -> list
(List iterate (method-ref Num inc) 4 0) -> (0 1 2 3)List zip(List zip a b) -> alist
Pairs corresponding elements as assocs; the result is an alist.
(List zip (list 1 2 3) (list 4 5 6)) -> ((1 . 4) (2 . 5) (3 . 6))List zip-with(List zip-with f a b) -> list
(List zip-with + (list 1 2 3) (list 10 20 30)) -> (11 22 33)List partition(List partition pred lst) -> (list list)
(List partition (method-ref Num even?) (list 1 2 3 4)) -> ((2 4) (1 3))List group-by(List group-by f lst) -> alist
(List group-by (method-ref Num even?) (list 1 2 3 4)) -> ((#f 1 3) (#t 2 4))List sort(List sort cmp lst) -> list
Merge sort.
(List sort < (list 3 1 2)) -> (1 2 3)List sort-by(List sort-by f lst) -> list
(List sort-by (method-ref Num abs) (list -3 1 -2)) -> (1 -2 -3)List uniq(List uniq lst) -> list
Removes consecutive duplicates.
(List uniq (list 1 1 2 2 3)) -> (1 2 3)List uniq-by(List uniq-by f lst) -> list
(List uniq-by (method-ref Num abs) (list 1 -1 2 -2 3)) -> (1 2 3)List intersperse(List intersperse sep lst) -> list
(List intersperse 0 (list 1 2 3)) -> (1 0 2 0 3)List transpose(List transpose lsts) -> list
(List transpose (list (list 1 2) (list 3 4))) -> ((1 3) (2 4))List update(List update n val lst) -> list
(List update 1 99 (list 1 2 3)) -> (1 99 3)List insert(List insert n val lst) -> list
(List insert 1 99 (list 1 2 3)) -> (1 99 2 3)List remove(List remove start n lst) -> list
(List remove 1 2 (list 1 2 3 4)) -> (1 4)List adjust(List adjust n f lst) -> list
(List adjust 1 (method-ref Num inc) (list 10 20 30)) -> (10 21 30)Association lists are lists of pairs ((key . val) ...). Keys are compared
with eq?.
assoc-get(Assoc get key alist) -> value | ()
(Assoc get 'b (list (pair 'a 1) (pair 'b 2))) -> 2
(Assoc get 'z (list (pair 'a 1))) -> ()Assoc get-or(Assoc get-or d key alist) -> value
(Assoc get-or 0 'z (list (pair 'a 1))) -> 0assoc-has?(Assoc has? key alist) -> #t | #f
(Assoc has? 'a (list (pair 'a 1))) -> #t
(Assoc has? 'z (list (pair 'a 1))) -> #fassoc-del(Assoc del key alist) -> alist
(Assoc del 'a (list (pair 'a 1) (pair 'b 2))) -> (('b . 2))assoc-put(Assoc put key val alist) -> alist
(Assoc put 'a 99 (list (pair 'a 1) (pair 'b 2))) -> (('a . 99) ('b . 2))assoc-keys(Assoc keys alist) -> list
(Assoc keys (list (pair 'a 1) (pair 'b 2))) -> ('a 'b)Assoc vals(Assoc vals alist) -> list
(Assoc vals (list (pair 'a 1) (pair 'b 2))) -> (1 2)Assoc map(Assoc map f alist) -> alist
Applies f to each value.
(Assoc map (method-ref Num inc) (list (pair 'a 1) (pair 'b 2))) -> (('a . 2) ('b . 3))Assoc filter(Assoc filter pred alist) -> alist
Filters entries by predicate applied to each (key . val) pair.
(Assoc filter (fn (_ e) (> (rest e) 1)) (list (pair 'a 1) (pair 'b 2))) -> (('b . 2))Assoc merge(Assoc merge a b) -> alist
Merges b into a, keeping a’s entries on collision.
(Assoc merge (list (pair 'a 1)) (list (pair 'a 9) (pair 'b 2))) -> (('a . 1) ('b . 2))Assoc pick(Assoc pick keys alist) -> alist
Returns entries whose keys appear in keys.
(Assoc pick (list 'a) (list (pair 'a 1) (pair 'b 2))) -> (('a . 1))Assoc omit(Assoc omit keys alist) -> alist
Returns entries whose keys are NOT in keys.
(Assoc omit (list 'a) (list (pair 'a 1) (pair 'b 2))) -> (('b . 2))Assoc from-bindings(Assoc from-bindings bindings) -> alist
Converts a bindings list – ((key value) ...) two-element lists, the let shape – to an alist of assocs.
(Assoc from-bindings (list (list 'a 1) (list 'b 2))) -> (('a . 1) ('b . 2))Assoc ->bindings(Assoc ->bindings alist) -> list
Converts an alist of assocs to a bindings list of two-element lists.
(Assoc ->bindings (list (pair 'a 1) (pair 'b 2))) -> (('a 1) ('b 2))Assoc evolve(Assoc evolve fns alist) -> alist
Applies transformation functions to matching keys.
(Assoc evolve (list (pair 'a (method-ref Num inc))) (list (pair 'a 1) (pair 'b 2))) -> (('a . 2) ('b . 2))Str empty?(Str empty? s) -> #t | #f
(Str empty? "") -> #t
(Str empty? "a") -> #fStr join(Str join sep lst) -> string
(Str join ", " (list "a" "b" "c")) -> "a, b, c"
(Str join "" (list "a" "b")) -> "ab"Str repeat(Str repeat n s) -> string
(Str repeat 3 "ab") -> "ababab"
(Str repeat 0 "x") -> ""Str includes?(Str includes? sub s) -> #t | #f
(Str includes? "ell" "hello") -> #t
(Str includes? "xyz" "hello") -> #fStr starts?(Str starts? pfx s) -> #t | #f
(Str starts? "he" "hello") -> #t
(Str starts? "lo" "hello") -> #fStr ends?(Str ends? sfx s) -> #t | #f
(Str ends? "lo" "hello") -> #t
(Str ends? "he" "hello") -> #fStr reverse(Str reverse s) -> string
(Str reverse "hello") -> "olleh"
(Str reverse "") -> ""Vectors are fixed-size indexed collections backed by lists. They display as
#(...).
Vector of(Vector of . args) -> vector
(Vector of 1 2 3) -> #(1 2 3)
(Vector of) -> #()Vector vector?(Vector vector? x) -> #t | #f
(Vector vector? (Vector of 1 2)) -> #t
(Vector vector? (list 1 2)) -> #fVector ref(Vector ref i v) -> value
(Vector ref 1 (Vector of 10 20 30)) -> 20Vector length(Vector length v) -> integer
(Vector length (Vector of 1 2 3)) -> 3Vector ->list(Vector ->list v) -> list
(Vector ->list (Vector of 1 2 3)) -> (1 2 3)Vector from-list(Vector from-list lst) -> vector
(Vector from-list (list 1 2 3)) -> #(1 2 3)Vector make(Vector make n fill) -> vector
(Vector make 3 0) -> #(0 0 0)Regex values are created with the #/pattern/ literal syntax. They compile
the pattern into an AST at read time and match against strings at runtime.
Regex regex?(Regex regex? x) -> #t | #f
(Regex regex? #/abc/) -> #t
(Regex regex? "abc") -> #f(write #/abc/) -> #/abc/
(write #//) -> #//
(write #/ab*c/) -> #/ab*c/
(write #/a\.b/) -> #/a\.b/A regex called as a function performs a full match against a string. Returns
#t on match, #f on no match.
(#/abc/ "abc") -> #t
(#/abc/ "abd") -> #f
(#/abc/ "ab") -> #f
(#/abc/ "abcd") -> #f. matches any single character.
(#/./ "x") -> #t
(#/a.c/ "abc") -> #t
(#/a.c/ "axc") -> #t
(#/./ "") -> #f* matches zero or more of the preceding element.
(#/ab*c/ "ac") -> #t
(#/ab*c/ "abc") -> #t
(#/ab*c/ "abbbc") -> #t
(#/a*/ "") -> #t+ matches one or more of the preceding element.
(#/ab+c/ "abc") -> #t
(#/ab+c/ "abbbc") -> #t
(#/ab+c/ "ac") -> #f? matches zero or one of the preceding element.
(#/ab?c/ "abc") -> #t
(#/ab?c/ "ac") -> #t
(#/ab?c/ "abbc") -> #f\ escapes the following character, treating it as a literal. Note the
asymmetry with strings: a regex literal is read directly, so one backslash
escapes – #/a\.b/ is the three-element pattern a . b – while the
STRING being matched uses source-level escaping, so the one-backslash string
is written "a\\b". (This section previously doubled the regex escapes as
if they were strings; those patterns matched a literal backslash and did not
demonstrate escaping at all.)
(#/a\.b/ "a.b") -> #t
(#/a\.b/ "axb") -> #f
(#/a\\b/ "a\\b") -> #t
(#/a\*b/ "a*b") -> #tThe * quantifier is greedy but backtracks to find a match.
(#/a.*b/ "axxb") -> #t
(#/.*b/ "aab") -> #t
(#/a.*b/ "axx") -> #f(#/a.*/ "abcdef") -> #t
(#/a.b*c/ "axbbc") -> #t
(#/.+/ "abc") -> #t
(#/.+/ "") -> #ftype-name(Type name #/abc/) -> "REGEX"