Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Standard Library by Task

Every .x translation unit receives the x2c runtime prelude. Each section below names the type to use for one task. The generated module reference lists every signature and its behavior, grouped by API tier.

The implicit prelude is the standard library. Optional x2c system modules are shipped, built, and tested with x2c, and need an explicit include. Runtime internals are implementation details. Third-party libraries are under packages/ and use import.

Hold a value whose type varies

Use Var when you do not know the type of a value until the program runs. Use the C type everywhere else.

Var value = 42;
value = "forty-two";

if (value is String)
  printf("%s\n", value);

Dynamic arithmetic and truthiness are in Var operations. Checked conversion is in Var conversion. void means missing or exhausted, and collections and iterators never carry it as a payload.

Choose a collection

NeedTypeImportant contract
persistent heterogeneous sequenceListcanonical and immutable
growing indexed sequenceArraymutable object identity
growing keyed lookupMapmutable object identity
canonical byte textStringinterned, NUL-terminated
streaming traversalItercaller-owned iterator storage

List and String values are canonical. Array and Map are distinct allocated objects even when empty.

Build and transform text

String does interpolation, search, split, replace, partition, case conversion, padding, escaping, slicing, joining, and checked numeric parsing. Its operations work on bytes; they do not decode Unicode.

Use Buffer for incremental text:

Buffer output = $auto(Buffer.new(0));
output.write("count=");
output.printf("%d", 4);
String text = output;
printf("%s\n", text);

Use Symbol for compact immediate tags and Atom for exact names that may be longer.

Read or write a file

File wraps C streams. Iterate a text file by line:

File input = tmpfile();
input.puts("first\nsecond\n");
input.rewind();
foreach(String line, input)
  printf("%s", line);
input.close();

Use File.readline_into and File.read_into for raw bytes with separate data, EOF, and error status. Use File.write_all and File.copy_to when a partial write must be finished or reported.

Transform a sequence

Every collection supports iteration. Use foreach to walk one:

List values = %(1 2 3);
foreach(int value, values)
  printf("%d\n", value);

Use Iter to process values as they are requested. Use a collection’s map, filter, and fold operations to compute the result immediately. Iter.try_next separates a value from exhaustion and never yields void as data.

Match structured Lists

The language match statement and Match use the same List patterns:

List event = %(ready 42);
match (event) {
  case %(ready ?value):
    printf("%ld\n", value);
}

Use the match statement for control flow and List.try_match or the other runtime matching functions when the patterns are themselves data.

Own allocation lifetime

Scope groups runtime allocations:

$scope() {
  String text = "temporary";
  Array values = [text, 1, 2, 3];
  printf("%s\n", values.repr());
}

Use Block for fixed-width growable storage and Buffer for text construction. External resources such as Files still close explicitly or through defer.

Each Scope.release must match a Scope.retain on the same active slot. See scopes and lifetime.

Bound temporary work

Context combines a Scope lifetime with independent Error, cleanup, and Match state. An isolated Context also gives temporary String and List values private pool branches:

Context work = Context.open_isolated_named("request");
List temporary = %(answer 42);
List result = work.export(temporary);
work.close();

printf("%s\n", result.repr());

Closing reclaims everything that was not exported. Close Contexts in last-opened, first-closed order. See contexts and threads.

Run work on native threads

Thread runs a fixed-signature callback in an isolated Context. It copies the input structure, but pointers inside that structure still point into shared process memory. A worker can read parent Strings and Lists directly. Use Mutex when a shared value may be written:

typedef struct CounterWork {
  Mutex mutex;
  int *counter;
  String label;
} CounterWork;

static Var increment(const void *input, size_t input_size) {
  if (input_size != sizeof(CounterWork)) raise %(bad-arg);
  const CounterWork *work = input;
  $lock(work.mutex) (*work.counter)++;
  return work.label;
}

int main(void) {
  int counter = 0;
  Mutex mutex = Mutex.new();
  CounterWork work = { mutex, &counter, "parent label" };
  Thread first = Thread.start(increment, &work, sizeof(work));
  Thread second = Thread.start(increment, &work, sizeof(work));
  String a = first.join();
  String b = second.join();
  printf("%s, %s: %d\n", a, b, counter);
  first.free();
  second.free();
  mutex.free();
  return 0;
}

Every started Thread needs one successful Thread.join, then Thread.free. Contexts and threads gives the sharing and result rules.

Handle absence and failure

These operations report absence separately from data:

  • Map.try_get and Map.try_del report whether the key was present, separately from its value.
  • Iter.try_next separates a value from exhaustion.
  • String.try_long and String.try_double separate zero from parse failure.
  • raw File readers separate data, EOF, and error.

An operation that fails raises its cause through Error, and a filtered catch or the cause’s policy handles it. Absence, exhaustion, and parse results come back as return values.

Use Error transfer and cleanup

Language raise, try, filtered catch, finally, and defer are backed by Error and the cleanup-frame runtime. Application code uses those statements and leaves the runtime frame functions alone. See Errors and Cleanup.

Bind native functions

Func binds an already-linked native function with a checked signature. Construction, binding, conversion, and application failures raise their cause through Error; none of those causes return to the call. A fixed nonvariadic function or function-pointer value with a supported signature converts implicitly where a Func is expected. Direct conversions reuse a file-static binding that lives for the program, so an operation that takes ownership of a Func, such as Lisp.bind, leaves it in place. Pointer conversions snapshot the pointer in a binding held by the current Scope. Call a C function directly when its name is known at compile time.

Log structured events

Logger does level filtering and writes to ordered stderr, file, memory, and custom sinks. A category labels an event; it never suppresses one. The global log_* functions are the simplest interface; use a Logger object to configure logging and manage its lifetime.

Embed Lisp

Lisp provides a reader, evaluator, lexical closures, macros, global bindings, native Func integration, host-side value application, and file/string evaluation:

static String greet(String name) {
  return %"hello $name";
}

int main(void) {
Lisp lisp = Lisp.new();
defer lisp.destroy();
$lisp.bind(lisp, "greet", greet);
String result = lisp.eval(%(greet "Ada"));
  return result == "hello Ada" ? 0 : 1;
}

Lisp.new() creates an isolated session and loads etc/init.xlisp. That file supplies defun, short-circuit control, higher-order List operations, numeric comparisons, basic String operations, and the pattern matching described in the language reference. Lisp.kernel() creates the evaluator primitives alone, for hosts that build their own environment. etc/lisp-values.xlisp binds the core List, Array, Map, String, Var, and Symbol operations; the compiler loads it into every compile-time session, and an embedding host loads it by name. etc/lisp-io.xlisp stays optional and must be loaded by name. It adds read-file, write-file, sort, fib, and subst.

Use $lisp.binding(group, "name") on several direct functions and $lisp.install(lisp, group) when a unit has a reusable set. Functions must appear before the install call, and names in one group must be unique. Installing the same group into two sessions creates two sets of Func values, and each session releases its own in Lisp.destroy().

A group compiles into the unit that declares it. A package can therefore publish its bindings behind one public installer that a consumer calls after importing it. See Wrapping a C Library.

$lisp.bind and grouped bindings accept the parameter and result types Func supports, including untagged pointers and structs passed as <p48> addresses. Native functions are trusted host code. A package-specific opaque object needs its own Var adoption.

Load a Lisp program with Lisp.eval_file, then evaluate runtime List forms with Lisp.eval. $ inside %(...) inserts live x2c values; it is unrelated to compile-time $(...) Lisp. Within a List literal, ', `, ,, and ,@ write quote, quasiquote, unquote, and unquote-splicing just as they do in Lisp source. A flat List result destructures through the same typed conversions as any other value:

int main(void) {
Lisp lisp = Lisp.new();
defer lisp.destroy();
String role = "editor";
List outcome = lisp.eval(%(decide (quote ((role $role)))));
String decision;
int limit;
(decision, limit) = outcome;
return 0;
}

Look up an exact operation

The module reference is generated from runtime source and compiler-verified signatures.