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

Compile-time Macros

x2c macros generate source at compile time using C-like x2c syntax. They bind parsed, typed source rather than preprocessor text. The compiler constructs, binds, and types their templates and arguments as it does ordinary source. Hygiene keeps generated names from colliding with names in the caller. A reusable global macro is invoked explicitly with a $ name; a local macro uses a bare name inside its defining block. Both declare the syntax kinds they accept and produce one declared kind of source.

Choose according to what the code does:

  • a function computes with runtime values;
  • a macro constructs source at translation time;
  • a decorator transforms the expression or source item immediately following it.

Start with templates. Use compile-time Lisp only when the result requires computation.

A first expression macro

The result kind follows macro; typed holes appear in the invocation pattern:


macro Expression $project.minutes(Expr $value) => (
  $value * 60
)

int main(void) {
  int seconds = $project.minutes(2);
  return seconds == 120 ? 0 : 1;
}

Read the definition from left to right:

SourceMeaning
Expressionthe expansion must be one expression
$project.minutesthe explicit, qualified invocation name
Expr $valuebind one caller expression to $value
$value * 60insert that expression into the replacement

The $ marks macro syntax. It does not make a runtime variable dynamic. Macro names are in their own namespace, so qualification is useful for project and library names. The x2c.* namespace is reserved for compiler-shipped facilities.

The replacement preserves ordinary evaluation rules. If a hole appears twice, the caller’s expression appears twice and may be evaluated twice. Use a generated temporary when the macro promises single evaluation, or use a function when that fits better.

Local expression shorthand

Use with when the repeated source is local to one braced block and does not need a reusable macro:

typedef struct Point { int x, y; } Point;
int main(void) {
  Point point = { 0 };
with point {
  _.x = 1;
  _.y = 2;
}
  return point.x == 1 && point.y == 2 ? 0 : 1;
}

The optional as name form replaces _ with a chosen lexical name. Like a macro hole, every use substitutes the original expression and may evaluate it again; with does not create a runtime temporary. It declares no reusable generator and has no name outside its block.

Keep a generator inside one block

Use a local macro when one function or nested block needs a typed generator:

static int scaled_sum(int scale, int value) {
macro Expression scaled(Expr $input) => (
  $input * scale
)

return scaled(value);
}

The definition is visible from that point to the end of the block and through nested blocks. An inner definition of scaled temporarily shadows it; a later definition in the same block replaces it for following calls. Literal references such as scale retain the exact parameter or preceding local they named at the definition, even if an inner block later declares the same spelling. Hole arguments still retain their caller bindings.

The bare call shape belongs to the local macro before a file keyword alias or ordinary function. A use without parentheses is an ordinary identifier, and (scaled)(value) explicitly calls a function. $scaled(value) uses only a global or imported macro, so local and global definitions may share a name.

Local expression, statement, field, Map-entry, and enumerator generators keep their usual result positions. Local decorators are available when their target also occurs inside the block. Unit generators and decorators over functions or translation-unit items remain global because they have no invocation position inside the local name’s lifetime. A macro may generate a local definition; it has the same source-order visibility and capture behavior as a direct one.

with remains the smaller choice when the only need is repeated substitution of one expression. A local macro is worth defining when it needs typed holes, declarations, control flow, computed source, or more than one input.

Holes say what binds

Every hole has a syntax kind:

Hole kindAccepts
Expran expression
Namean identifier binding
Typea type
Parama function parameter
Functiona complete function definition
Declone declaration without its trailing semicolon
Statement or Blockone block item
Fieldone struct or union field
Entryone Map key: value row
Enumeratorone enum member
Unitone translation-unit item

A trailing ... makes the final hole a sequence. The replacement uses the same ellipsis to splice the captured sequence:


macro Expression $project.call(
  Expr $callee,
  Expr $arguments...
) => (
  $callee($arguments...)
)

static int add(int left, int right) {
  return left + right;
}

int main(void) {
  return $project.call(add, 20, 22) == 42 ? 0 : 1;
}

Typed holes tell the reader whether an argument is an expression, binding, type, declaration, or sequence, without requiring knowledge of the AST.

Type holes are compile-time generics

A Type hole lets one Unit macro generate the same typed interface for several concrete C types. Each invocation is checked independently and emits ordinary declarations and functions specialized to the types it receives:

typedef struct { int value; } IntValue;
typedef struct { double value; } DoubleValue;

macro Unit $value.family(Type $box, Type $item) => {
  static inline $box $box.new($item value) {
    $box box = { value };
    return box;
  }

  static inline $item $box.get($box box) {
    return box.value;
  }
}

$value.family(IntValue, int);
$value.family(DoubleValue, double);

static double measure(void) {
  IntValue count = IntValue.new(3);
  DoubleValue ratio = DoubleValue.new(0.5);
  return count.get() * ratio.get();
}

Each expansion is a distinct C type. IntValue.get returns int, DoubleValue.get returns double, and the generated C has no runtime type parameter or hidden cast. The caller supplies named concrete types, and the macro builds their shared implementation.

Result kinds match source positions

The declared result kind determines where an invocation may appear:

Result kindInvocation position
Expressionexpression
Statementstatement
Fieldstruct or union body
EntryMap literal row
Enumeratorenum body
Unittranslation unit
Decoratoras a prefix immediately before its target

An Entry macro can produce zero, one, or several comma-separated Map rows. Entry holes capture complete rows, and a trailing ... captures or inserts a row sequence. A Map literal reads quoted data, so ${...} crosses into x2c before invoking either a $handler(...) or a keyword alias:

macro Entry $project.handler(Literal $key, Expr $value) => {
  $key: $value
}

int main(void) {
  int open = 1, close = 2;
  Map handlers = %{
    ${$project.handler("open", open)},
    ${$project.handler("close", close)}
  };
  return handlers.len() == 2 ? 0 : 1;
}

Statement macros are useful for a small, repeated control-flow shape:


macro Statement $project.guard(Expr $condition) => {
  if (!$condition) return 0;
}

int positive(int value) {
  $project.guard(value > 0);
  return value;
}

An expression macro is not a statement macro, and a unit macro cannot appear inside a function. The compiler diagnoses the mismatch at the invocation.

Generated names and using

Declare scratch bindings after using when a template needs caller-local temporaries:


macro Statement $project.swap(
  Expr $left,
  Expr $right
) using $temporary => {
  $(x2c.syntax.type $left) $temporary = $left;
  $left = $right;
  $right = $temporary;
}

int main(void) {
  int left = 1;
  int right = 2;
  $project.swap(left, right);
  return left == 2 && right == 1 ? 0 : 1;
}

Each invocation receives a compiler-private binding for $temporary. Definition-local literal names resolve where the macro was defined, captured names keep their caller bindings, and generated names cannot collide with caller source. A local macro’s literal references to parameters and preceding locals also keep those exact bindings when an inner declaration uses the same spelling.

Put reusable macros in imports

An .xmacro file may contain macro definitions and top-level compile-time Lisp. Import it explicitly:

$(import "project-macros.xmacro")

Imports resolve relative to the importing file, become tracked translation dependencies, load once, and reject cycles. A normal .xlisp import executes in the same translation-unit Lisp session but does not contain macro definitions.

Prefer a qualified name such as $test.run or $project.logging.trace in a shared import. It identifies the project or library at each call and avoids ambiguous short global names.

Decorators transform one target

A decorator receives the following expression or source item as its required first parameter. Invocation arguments follow normally:


macro Decorator $project.trace(
  Function $function,
  Expr $label
) => {
  printf(
    "[%s] enter %s\n",
    $label,
    $(x2c.literal.string (x2c.function.name $function))
  );
  defer printf("[%s] leave\n", $label);
  $(x2c.function.body $function)...
}

$project.trace("request")
int answer(int value) {
  return value * 2;
}

int main(void) {
  return answer(21) == 42 ? 0 : 1;
}

There is no semicolon after the decorator invocation. Decorators stack closest-first, and each expansion must leave exactly one target for the next. A function decorator preserves the function’s original name, storage, signature, and method ownership.

A statement macro or statement decorator may be used directly as an if, else, or loop body when its production contains exactly one statement. An explicit compound statement or do { ... } while (0) counts as one; an unwrapped sequence of block items does not. The compiler never inserts braces, so the production still decides scope and control flow.

An Expr target makes the decorator a prefix expression:

static int checked(int value) {
  return value;
}
macro Decorator $project.checked(Expr $target) => (
  checked($target)
)
int main(void) {
  int value = $project.checked() (20 + 22);
  return value == 42 ? 0 : 1;
}

Expression decorators bind like a prefix unary operator. They capture the following primary/postfix, unary, or cast expression before surrounding binary, conditional, assignment, or comma operators. Parenthesize a larger target: $project.checked() value + 1 checks value, while $project.checked() (value + 1) checks the complete addition. Chained expression decorators remain closest-first.

Give macros local keyword spellings

A keyword declaration removes the qualified $ spelling while preserving the macro’s existing invocation grammar. This makes a parameterized decorator look like a new control construct without adding a new parser for its header:

macro Decorator $control.range(
  Block $body,
  Name $index,
  Expr $start,
  Expr $stop
) using $begin, $end => {
  {
    int $begin = $start, $end = $stop;
    for (int $index = $begin; $index < $end; $index++) $body
  }
}

keyword range $control.range;

static void consume(int value) { printf("%d\n", value); }
int main(void) {
range(index, 0, 3) {
  consume(index);
}
  return 0;
}

range(index, 0, 3) TARGET is exactly $control.range(index, 0, 3) TARGET. The macro still handles the typed Name and Expr arguments, hygiene for its private bounds, and parsing of the following block.

Ordinary macros can use the same declaration. Their parentheses remain mandatory, even when they take no arguments:

macro Statement $control.swap(
  Expr $left,
  Expr $right
) using $temporary => {
  $(x2c.syntax.type $left) $temporary = $left;
  $left = $right;
  $right = $temporary;
}

keyword swap $control.swap;

int main(void) {
  int left = 20, right = 22;
  swap(left, right);
  return left != 22 || right != 20;
}

Decorator arguments use the same comma-separated typed and sequence parameters as direct macro calls. A decorator with no explicit arguments uses ALIAS TARGET; one with arguments uses ALIAS(arguments) TARGET. An all-sequence decorator uses ALIAS() TARGET when the sequence is empty, so a parenthesized target of a zero-argument decorator remains unambiguous.

A Decl argument ends at the invocation’s comma or closing parenthesis, so its trailing semicolon is omitted. It accepts one declarator with an optional initializer, or flat destructuring with two or more simple identifier targets. For example, foreach(int value, values) supplies int value as one Decl argument.

The declaration and uses are source ordered and local to one .x file. An included .x file may declare and use its own aliases, including across its own includes, but those aliases do not leak into the including source.

A .xmacro import may package macro definitions with their aliases:

$(import "compiler-keywords.xmacro")

swap(left, right);

The aliases become visible at the import in that .x file only. Every source file that wants them imports the pack explicitly; including a file that uses the pack does not import its aliases. This lets a project use private keyword spellings without placing them in x2c.x or another consumer prelude.

An alias is a contextual parser match, not a globally reserved word. An ordinary or parameterized alias is claimed only as ALIAS(...), so the name may still be a type, declaration, field, label, or uncalled function. Its direct call or cast spelling is claimed by the macro. A zero-argument bare decorator is broader: the parser claims its name at every position compatible with its target, which can overlap an expression read or a declaration beginning with a same-named typedef. Prefer parenthesized aliases in shared packs and introduce bare decorators only after checking those positions.

Aliases do not create arbitrary grammar. The lexical with expression { ... } statement is contextual grammar built into the language, not a macro alias. Other forms such as loop item in values or a semicolon-separated control header would require separate parser support. Result positions, decorator targets, terminators, hygiene, expansion limits, and compile-time restrictions remain those of the aliased macro.

Use a decorator to place checking, tracing, validation, or checked foreign binding directly beside the expression or source item it affects. Do not use a decorator for type registration, protocol participation, or receiverless startup and shutdown. Those have their own declarations.

Compile-time Lisp is where computation happens

$(...) evaluates Lisp during translation. Inside a macro body, hole names refer to the captured syntax:


$(defun project-offset () 2)

macro Expression $project.answer(Expr $base) => (
  $base + $(project-offset)
)

int main(void) {
  return $project.answer(40) == 42 ? 0 : 1;
}

The compiler supplies operations for diagnostics, identifiers, types, bindings, literals, function projections, structural matching, and invocation location. The language reference lists them.

Local macro names are lexical, but compile-time Lisp is not. A local macro may define or change Lisp state, and that state remains available for the rest of the translation unit after the block ends.

x2c.type.fields lets a macro walk a complete struct or union in source order without reconstructing its declaration. Pair each returned name with x2c.expr.field when generated code needs ordinary typed access through a value or pointer receiver:

(map (lambda (field)
  (x2c.expr.field receiver (car field)))
  (x2c.type.fields (x2c.syntax.type receiver)))

x2c.method.resolve is the optional form of direct dotted method lookup. A generator can inspect the returned callee’s function Type or compose it with x2c.expr.call; a missing method returns nil instead of producing a type diagnostic:

(let ((callee (x2c.method.resolve type "write")))
  (if callee (x2c.expr.call callee receiver value) nil))

It does not search delegate fields. A delegated result would also need the field-projected receiver, which this operation does not return. Macro-generated dotted call syntax still uses delegation when ordinary expression resolution later sees the call.

When spelling matters rather than meaning, x2c.source.text reads the exact text of a complete captured argument:

macro Expression $project.spelling(Expr $value) => (
  $(x2c.literal.string (x2c.source.text $value))
)

int main(void) {
  return strcmp($project.spelling(1 /* kept */ + 2),
                "1 /* kept */ + 2");
}

Forwarding a capture through another macro preserves that text. Constructing or selecting an AST subtree does not invent source text; use standard Lisp repr when canonical AST rendering is intended.

x2c.embed.text reads a regular text file into a compile-time String and records the canonical file as a build dependency. A String path is relative to the file containing that Lisp form. A captured String literal is relative to the caller file where the literal was written:

macro Expression $project.notice(Literal $path) => (
  $(x2c.literal.string (x2c.embed.text $path))
)

String notice = $project.notice("notice.txt");

The returned value stays compile-time data; use x2c.literal.string explicitly when generated code needs a runtime String. Empty files are valid. Directories, embedded NUL bytes, unreadable files, and files too large for a String are rejected.

Most macros should not need those operations. A source-shaped template keeps the binding and generated result visible to a reader. Use Lisp when a template must compute a value, inspect a declaration, select among shapes, or generate a checked name. If the Lisp and AST manipulation is longer or harder to explain than the repeated source, keep the source.

Generate several views from one table

Compile-time Lisp can keep one table of facts from which small macros generate enums, lookup tables, switch cases, declarations, and repetitive functions. Changing the table then changes every generated form.

Use a table when the outputs repeat the same facts. Write names, types, and status values explicitly so a reader can understand the generated code without working through the Lisp. Keep unrelated outputs as direct source.

Declare methods and converters before adoption

Shallow collection loads .xmacro imports and expands their file-scope unit macros. Generated private helpers remain available to later macro invocations in the same source file, while generated public declarations and protocol rows remain visible to importing units. A generated public function definition may complete a prototype earlier in the same expansion when both have the same canonical signature and ownership.

Put converter and public method prototypes before an adoption so shallow collection can classify the conformance and downstream code can discover the functions. Imported macro definitions survive intervening preprocessor includes, so a definition can be imported before the headers its invocations need.

Choosing the smallest tool

Use:

  • a function for runtime computation;
  • a protocol for explicit participation in shared typed behavior;
  • a macro for repeated source whose invocation has one obvious expansion;
  • a decorator for uniform policy on one following expression or source item;
  • direct source when generation would hide more than it removes.

Count the macro’s definition as well as its calls: a useful macro reduces the total, preserves useful diagnostics, and makes each binding clear.

For the complete hole grammar, result validation, hygiene rules, limits, and compiler Lisp SDK, see Compile-time macros in the language reference.