Programming Idioms
Use the shipped classes and system macros where they
express a complete constructor or block lifetime: $auto for an owned local,
$scope for a region, $let for saved storage, and $lock for a Mutex.
These idioms combine the features introduced in the guide.
Keep native values native
Use C scalar, pointer, struct, union, and enum types when the value’s type is
known at compile time. Use Var where the value is dynamic: heterogeneous
collection elements, dynamic arithmetic, generic callbacks, or interchange
data.
Use values for local state and references for updates
Declare a record as a value when it belongs to one operation and needs no retained identity. A method that updates that record takes a reference:
class Counter { int value; };
static void Counter.step(Counter &c, int amount) => c.value += amount;
int main(void) {
Counter counter = Counter.new(2);
counter.step(3);
Counter copy = counter;
copy.step(1);
printf("%d %d\n", counter.value, copy.value);
return 0;
}
5 6
counter holds the record directly; constructing it needs no allocation for
the record. step changes the caller’s object, and its call needs no & or
explicit dereference. Copying the record creates a separate Counter. Pass a
small record by value when reading a copy is the intended contract.
class adds constructors, boxing, and other methods to the declared
representation. Use an ordinary value typedef for a context that
does not need that bundle. In either case, pass its mutable state through
T & helpers instead of introducing a pointer typedef and a second local
whose only purpose is taking the context’s address.
A trailing * selects a pointer representation:
class Node { int value; } *;. Its bare Node name is already a pointer,
and its methods take that handle. Choose it when shared or retained identity
is useful. The spelling struct before the braces is optional; short braces
work for both representations. See
Choose a representation.
Representation and storage location are separate choices. A value can be a local, a field, an array element, or part of allocated storage. A reference borrows one live caller object for a call; it does not own or extend its lifetime. Record copies are shallow: a copied Array, Buffer, or pointer field still reaches the same backing object. Keep cleanup with the owner of those resources, including when the record itself is a value.
An active transaction or builder has one owner even when its record is a
value. Copying it shares the saved maps or backing arrays; the copy is not a
second transaction or an independently owned builder. Borrow the original
through reference methods. SymTxn and MachineBuilder use this pattern;
use their begin or new operation to prepare active storage. A Job keeps a
shared handle because its recorded run and Scope finalizer must reach the
same job, while its launch options live directly inside that job.
Borrow required and optional outputs
Use a reference when a result must update a caller’s existing object:
static int take(int &remaining, int &item) {
if (!remaining) return 0;
remaining--;
item = remaining;
return 1;
}
int main(void) {
int remaining = 2, item;
while (take(remaining, item)) printf("%d\n", item);
return 0;
}
1
0
Use T &? when supplying the object is optional, and test its presence before
using it. A required reference takes an addressable value, not a pointer or
NULL. An existing pointer p supplies its object as *p. Keep pointers for
buffers and arrays, retained addresses, nullable links, native signatures,
and callback state. A nested context that borrows its mutable parent needs a
pointer field so both contexts reach the same parent. See
Reference parameters.
Adopt a protocol for one real contract
Adopt a protocol when a concrete type should support a shared interface. Define the required conversions beside the type. Similar method names are not enough: the operations must have the meanings the protocol requires.
Implement a member directly when the type already provides that operation.
Use a base default when every participant value converts to a valid base
value and the forwarded operation has the required meaning. Before extending
a protocol, use --dump-conformance to check its existing participants.
Box private records through Var(T)
A type does not need to become public because internal code stores it in a
Var, List, Array, or Map. Put both conversions beside the private type
and adopt the public Var(T) protocol there. Typed arguments, results,
assignments, and initializers then box and unbox it without repeated casts.
Keep an explicit value is Type check when a Var came from mixed or
untrusted data. Protocol adoption supplies the conversions; it does not check
the dynamic type of an arbitrary value.
Make macro calls easier to read than their expansions
A macro should replace repeated source with a call whose meaning is obvious:
macro Stmt $sample.guard(Expr $condition) {
if (!$condition) return 0;
}
int positive(int value) {
$sample.guard(value > 0);
return value;
}
Prefer a function when the job is runtime computation. Prefer direct source when a macro needs substantial compile-time Lisp or AST manipulation to save a few tokens. The macro body, imports, and generated declarations are part of the maintenance cost.
Write generated code as the code it builds
A meta function that returns code should show that code. A quotation
shows the C that will land; a %(...) List of node tags hides it. Choose the
first row that fits. Meta Functions
explains each form with examples.
| To build | Write |
|---|---|
| Code with one shape at several sites | a named macro, applied from the function as in source templates |
| Code that one function builds | a quotation: $!( expression ), $!{ statements }, or $!Unit{ ... } |
| Code that uses a field, an element, or a call result | a ${expression} hole, evaluated once where the quotation is written |
| Code in which separate quotations share one name | List name = x2c_ident("total");, then $name in each quotation |
| An expression whose type is read before it lands | a typed quotation, such as $!double{ $value * 2 } |
| Code the function inspects before it returns | a typed quotation, which builds the code where it is written |
| A pattern, a data row, or a form with no source spelling | a %(...) List |
A name that a quotation declares is private to it, so another quotation
cannot refer to it. An x2c_ident hole declares and refers to one exact
name.
When a meta function builds one statement, the macro can hand it the work
in one line. Write the Stmt macro with an arrow body and return the
statement itself:
#include "meta.x"
meta static List traced(List body) =>
$!{ { puts("enter"); $body puts("leave"); } };
macro Stmt $sample.trace(Stmt $body) => $traced($body);
int main(void) {
$sample.trace(puts("work"););
return 0;
}
Keep the braced { @helper(...) } form for a function that returns
several block items as a List.
Choose collections by mutation and identity
Use List for immutable sequences that share structure, Array for mutable
indexed sequences, and Map for mutable key/value pairs. Build text with
Buffer when repeated concatenation would create unnecessary intermediate
Strings.
List syntax = %(call print "hello");
Array work = [1, 2, 3];
Map index = {name: "x2c"};
work.push(4);
index[<count>] = work.len();
For a closed vocabulary of Symbols that needs membership tests, a dense
index, or ordered iteration, declare a SymbolSet literal once and let its
perfect-hash index replace a hand-written switch; see
Ordered Symbol sets for the literal
and its operations.
Destructure small fixed results
Flat List destructuring is clearest when a producer returns a few values in
fixed positions:
List bounds = %(0 10 2);
int (start, stop, step) = bounds;
List record = %(7 2.5 "ready");
(int id, float score, String state) = record;
Assignment destructuring returns its source unchanged, so one result can feed several flat views without another call or copy:
Var left_a, left_b, right_a, right_b;
List values = (left_a, left_b) = %(1 2);
(right_a, right_b) = values;
Use match for conditional or nested shapes. Use named accessors when only one
position matters.
Let collection methods express collection work
Use foreach when the operation is naturally sequential or can stop early.
Use map, filter, and folds for direct transformations:
List values = %(1 2 3 4);
List squares = values.map(%!(item) => item * item);
Var sum = values.foldl(0, %!(total, item) => total + item);
Choose an explicit Iter pipeline when values must stream without first
materializing a collection.
Keep lambdas small
An expression lambda suits a short operation with a few captured
values. Use a block lambda when the operation needs local declarations,
branches, or defer:
List values = %(1 -2 3);
List magnitudes = values.map(%!(Var item) => {
int value = item.int();
if (value < 0) return -value;
return value;
});
Captured automatic values are read-only snapshots unless a lambda shares the
binding with using &name. When the operation grows beyond one local task,
use a named function and an explicit state object. The collection methods
accept capturing lambdas; a C function-pointer API needs a noncapturing one.
The reference gives the capture rules.
Separate absence from data
Prefer a try_* operation when the result alone cannot represent every
outcome, as with Map.try_get for presence, Iter.try_next for exhaustion,
or String.try_long for parse success:
Map map = {"name": "x2c"};
Var key = "name";
Var value;
if (!map.try_get(key, value))
printf("missing\n");
Sentinel-returning adapters are concise only when the sentinel is impossible in the result domain and the caller does not need a failure reason.
Put cleanup beside acquisition
Scope groups allocations by lifetime and $scope() retains one region
around a statement. $auto attaches an owned local’s cleanup to its
declaration, so the acquisition and its release are one line:
$scope() {
String path = "/tmp/x2c-idioms.txt";
File output = $auto(File.open(path, "w"));
output.puts("ready\n");
}
Both lower to defer. Use defer directly where no macro fits: a native
release, a conditional rollback, a consuming parameter, or cleanup that
writes a result.
int main(void) {
int *buffer = malloc(64);
defer free(buffer);
return buffer ? 0 : 1;
}
$auto needs a type that adopts Cleanup(T), directly or through its typedef
ancestry, as the runtime’s owning types do. It releases the value it acquired,
even after the local is reassigned. Use defer when cleanup must read a
binding at exit, such as a Scope slot filled later:
int main(void) {
Scope work = NULL;
defer work.cleanup();
$scope(&work) { Array items = []; items.push(1); }
return 0;
}
Raise failures; return ordinary outcomes
Report EOF, a missing Map key, or a numeric parse failure through the
result, as the library operations do. Use raise
for a failure cause that may cross several frames, and put a filtered catch
where recovery or reporting belongs. Every intermediate resource owner must
have defer or finally cleanup in place.
Prefer the primary API tier
The generated module reference separates:
- primary operations for ordinary programs;
- advanced storage, ABI, embedding, and diagnostics APIs;
- convenience adapters with a preferred operation;
- runtime-internal callables documented only for source readers.
Start with primary APIs; use the others when you need their specific behavior.