x-lang

Getting Started with x-lang

Build

make engine     # fetch the verified engine release this tree pins
make

The engine is a separate project, acquired rather than carried. make engine downloads one built for your platform and checks it against the digest tools/engine/engine.pin.xon records; make puts the x-bin binary where the wrapper and the test runners expect it. Nothing is compiled, and no C toolchain is required.

From nothing, in one command:

curl -fsSL https://raw.githubusercontent.com/jonruttan/x-lang/main/bootstrap.sh | sh

To build an engine from source instead, or to point this tree at one you already have, see Build in the README.

Start a Session

The simplest way to start:

sh x.sh

This loads the x-lang standard library and drops into a REPL:

> (+ 1 2)
3
> (def greeting "hello")
> greeting
"hello"

The prompt is > . Results are printed after each expression. Nil results print nothing.

Leave the session with (quit) or ctrl-d. Ctrl-c cancels a half-typed multi-line form and returns a fresh prompt (at an empty prompt it ends the session). (help) shows the documentation index — see Exploration below.

The arrow keys work, and so do the readline chords: ctrl-a and ctrl-e for the ends of the line, ctrl-w and ctrl-k to kill, ctrl-y to put it back, Up and Down for history that outlives the session, Tab to complete any documented name. What you type is coloured as you type it. It is built in — there is nothing to install and nothing to wrap the session in. docs/repl.md is the reference.

For the full-stack dialect (xenon) with the numeric tower, the compiler, and POSIX:

sh x.sh -l xe

Writing a Program

The REPL prints every result automatically. A file does not — it runs top to bottom and produces output only where you ask for it. Put this in hello.x:

; hello.x -- Hello world

(display "Hello from x-lang!")
(newline)

Run it:

sh x.sh -f hello.x
Hello from x-lang!

display writes a value without quotes; write writes it in read-back form (so strings keep their quotes); newline emits a line break. -f evaluates the file and exits. To load a file and then land in the REPL with its definitions available, use -F:

sh x.sh -F hello.x

This file is examples/x/hello.x; the rest of examples/ builds up from here.

For a single expression there is no need for a file at all. -c evaluates one and exits, and repeats to run several in order:

sh x.sh -q -c '(display "Hello from x-lang!")' -c '(newline)'

A program arriving on stdin runs the same way, which is what makes x-lang usable from a pipe:

echo '(display "Hello from x-lang!")' | sh x.sh -q

-q drops the banner. Neither form prints a result on its own — like a file, and unlike the REPL, they produce output only where you ask for it.

Basic Expressions

Values

Integers, strings, and characters are self-evaluating:

> 42
42
> "hello"
"hello"
> #\a
a

Pairs and Lists

The pair is the fundamental compound structure. A list is a chain of pairs terminated by nil ():

> (pair 1 2)
(1 . 2)
> (pair 1 (pair 2 (pair 3 ())))
(1 2 3)
> (list 1 2 3)
(1 2 3)
> (first (list 1 2 3))
1
> (rest (list 1 2 3))
(2 3)

Definitions

def binds a name in the current environment:

> (def x 10)
> (def double (fn (_ n) (* n 2)))
> (double x)
20

Functions

fn creates an applicative (a function that evaluates its arguments). The first parameter is always the self-reference (conventionally _), followed by the actual parameters:

> (def square (fn (_ n) (* n n)))
> (square 5)
25
> (def factorial
    (fn (self n)
      (if (<= n 1) 1 (* n (self (- n 1))))))
> (factorial 10)
3628800

The self-reference enables anonymous recursion — name it self (or anything) to call the function from within its own body, or _ when not needed.

Conditionals

> (if (> 3 2) "yes" "no")
"yes"
> (match
    ((> x 100) "big")
    ((> x 10) "medium")
    (#t "small"))
"small"

Sequences

do evaluates a sequence of expressions and returns the last:

> (do
    (def a 1)
    (def b 2)
    (+ a b))
3

Local Bindings

> (let ((x 10) (y 20)) (+ x y))
30

The Fexpr Model

x-lang’s evaluation model is distinctive. All C-level primitives are fexprs: they receive their arguments unevaluated and choose what to evaluate.

> (def my-if
    (op (test then else) e
      (if (eval test e)
        (eval then e)
        (eval else e))))
> (my-if (> 3 2) "yes" "no")
"yes"

op binds the unevaluated argument tree and the caller’s environment e. The body decides what to evaluate and when. This is how all core forms (if, def, match, do) are implemented – they are ordinary operatives, not special forms the evaluator knows about.

wrap and unwrap convert between applicative and operative behaviour:

> (def my-add (wrap (op (a b) e (+ (eval a e) (eval b e)))))
> (my-add 1 2)
3

Lists and Higher-Order Functions

The standard library provides a rich set of list operations:

> (map (fn (_ x) (* x x)) (list 1 2 3 4 5))
(1 4 9 16 25)
> (filter (fn (_ x) (> x 2)) (list 1 2 3 4 5))
(3 4 5)
> (fold + 0 (list 1 2 3 4 5))
15
> (List sort < (list 3 1 4 1 5 9))
(1 1 3 4 5 9)
> (List zip (list 1 2 3) (list "a" "b" "c"))
((1 . "a") (2 . "b") (3 . "c"))

Modules

Everything the default dialect ships is ready without ceremony — vectors, for instance:

> (def v (Vector make 3 0))
> v
#(0 0 0)

Capabilities the dialect does not pre-load are one import away:

> (import x/type/hash)
> (Hash ->hex (Hash fnv-1a "hello"))
"a430d84680aabd0b"

In the xenon dialect, the numeric tower is pre-loaded:

> (Num expt 2 100)
1267650600228229401496703205376
> (+ 1/3 1/6)
1/2
> (* 2.0 3.14)
6.28
> (+ 1+2i 3+4i)
4+6i

Exploration

Use help to look up documentation for any bound symbol:

> (help '+)

Use modules to list all registered modules:

> (modules)

Choosing a Dialect

Dialect Load Command Use Case
helium sh x.sh General programming, scripting, learning (the default)
xenon sh x.sh -l xe Numeric computing, full-stack applications
radon sh x.sh -l rn Systems programming, OS interaction

Next Steps