Language Reference
This chapter specifies x2c’s syntax, behavior, and current limitations.
The runtime API is described in the standard library overview, and advice about which construct to use is in idioms. Flags and the dump options that expose each phase are in compiler options. If you have not built the compiler yet, the website’s install page is the quickest route, and building the compiler covers the self-host stages behind it.
The syntax specifications catalog the source rules and provide an extracted EBNF grammar and lexical transition specification.
Source files and visibility
An .x file combines declarations and definitions. Translation produces a
header and a C source file.
A source file whose first line begins with #! is a
script unit; every other file is an ordinary unit.
Declarations are public unless marked static. A public function contributes
its prototype to the generated header, or its whole body when inline. A public
object has one definition in the generated C and an extern declaration in
the header. An object of an anonymous struct, union, or enum type has no
repeatable declaration to publish.
The compiler reserves _x2c_gensym_*, _init_guard_, _file_init_, and
_ followed by digits for generated support.
Source declarations may use _x2c_* outside that subspace. The repository
uses those names for shared implementation functions, but the prefix does
not change linkage or enforce private access. static controls visibility.
Standalone types are public by default. static typedef, static struct,
static union, static enum, and static class declare private types. A type
needed by a public declaration becomes public automatically. Its complete
definition and field types are published when the interface requires that
layout, such as for a value type or field access in a public inline body.
A public forward pointer type can keep a static aggregate body private.
A static type used only by static functions remains private.
The static marker on a type controls publication and emits no C storage.
module in place of static declares the same private types; see
module blocks.
An ordinary #include "module.x" makes that module’s public declarations,
macro definitions, keyword aliases, and meta functions available. The same
rule applies through transitive includes. Compile-time definitions become
available at the include’s source position. Static definitions belong to
the declaring source file. A C header is not an x2c unit, so its
static inline functions still belong to every C translation unit that
includes it.
Every translated header starts with #pragma once and also carries a
conventional include guard, so ordinary .x programs need neither directive.
The generated source preserves native includes at their source positions.
A late native include enters the header when a later public declaration needs
its types.
The advanced --cpp-symbols and --live-symbols modes run the host
preprocessor over raw .x include graphs. A module used with those modes needs
a source-level #pragma once only when its own .x includes form a cycle.
Ordinary translation resolves includes itself and terminates cycles without
it. These modes and --dump-cpp reject module units and indented units,
because the host preprocessor reads them as C. --cpp-symbols and
--live-symbols also reject script units, because the #! line is not C.
Every ordinary .x translation unit implicitly loads the x2c.x standard
runtime prelude. The compiler also emits #include "x2c.h" in its generated
header. An explicit #include "x2c.x" is also accepted; it does not change
the semantic environment or generated runtime dependency.
Optional modules shipped with x2c are outside that prelude. They require an
explicit source include, such as #include "typed-array.x". Third-party code
uses the package import described below.
Module blocks
A module block splits one large source file across a directory without
splitting the translation unit. The entry file lists its member files, and
the compiler reads each member’s text in place of the block. The result
translates as one unit with two private storage classes: static is visible
in the file that declares it, and module is visible in every file of the
unit.
The entry calculations/calculations.x lists members beside it:
/* calculations/calculations.x */
#include <stdio.h>
module {
"implementation.x",
}
int main(void) {
printf("%d\n", helper(40) + counter);
return 0;
}
/* calculations/implementation.x */
module int counter = 2;
module int helper(int value) { return value + counter; }
The block is module {, comma-separated string paths with an optional
trailing comma, and }, with no final semicolon. It appears at file scope,
once per file. Before a file-scope declaration module is a storage class,
described below. Elsewhere it is an ordinary identifier, and
struct module { declares a tag. Each path is relative to the file that holds
the block. Members may be siblings, descendants, or files elsewhere; no
same-named companion directory is required. A member that is already being
spliced, an unreadable member, a malformed block, or a second block is a driver
error at the block.
The block is spliced before C preprocessing, so a preprocessor condition does
not choose members. x2c skips a block only inside a conditional arm that C
never takes by x2c’s own reading: #if 0, #ifdef or #if defined of a
name that x2c output never defines, such as __cplusplus, and the #else
arm of #ifndef of such a name. Every other arm counts. After
#define OFF 0, a block under #if OFF still reads its members, and blocks
in both arms of #if 1 and #else are a second block. The spliced text,
including each member’s own directives, then reaches C as one unit in
splice order.
The entry’s syntax mode applies to all spliced text. In a .xp entry every
member is read with indentation layout, and in a .x entry with braces, so
a member must use its entry’s syntax.
A member’s text stands where the block stood, in the listed order. All members share the entry’s translation unit. Function declarations are collected across that complete unit, so a function can call a later-defined function without an explicit prototype. Other declarations retain their ordinary placement rules. A member may hold its own block, whose paths are relative to that member; their text is spliced in turn, and the namespace stays flat.
The module storage class declares a file-scope name for every file of the
module unit. It is written where static would be, before an object,
function, typedef, or class declaration:
/* calculations/state.x */
module typedef struct Tally { int total; } Tally;
module Tally tally;
module void add(int value) { tally.total += value; }
A later member and the entry after the block use Tally, tally, and add
by those names. The declaration parses as if static were written. The
object and function are static in the generated C, the type stays private,
and the unit’s header and interface publish none of them. Outside a module
unit module means static.
Inside a module unit, static keeps its C meaning: the name is visible in the
file that declares it and in no other file of the unit. That holds for
objects, functions, static typedef, static class, and the struct,
union, or enum tag such a typedef declares. Two members may each declare
static int counter or static typedef struct Node { ... } Node, and each
file sees its own, and so do the enumerators of a static typedef enum. The
enumerators of an enum declared without a typedef stay unit-wide, even under
static. A later declaration without static refers to the file’s static, as
in C. Examples are the definition after a static prototype and a block-scope
extern declaration or prototype. A file’s statics take a prefix in the
generated C: the file’s path relative to the entry’s directory without its
extension, with each character that cannot continue a C identifier made _,
then ___. The prefix starts with f, the normalized path’s length, and _;
a path that starts with a digit gets a leading x before its length is
measured. Under the entry parser/parser.x, a static advance in
parser/statements/loops.x emits as f16_statements_loops___advance, and one
in the entry itself as f6_parser___advance. Trailing _ of the path’s
spelling are dropped first, so a.x and a-.x give one prefix. A package
prefix ends in __, so a package entry’s statics stay apart from the
package’s public names. Generated names are compiler details; source never
spells them, and --dump-definitions displays the source name. A static type
adopted as a class keeps its short name as its Var tag and descriptor name, so
two files of one unit cannot each adopt a class under the same short name.
Compile-time declarations, static macro, static $(, static protocol, and
meta functions, keep their unit-wide behavior. Macro free names follow
hygiene.
Three cases are errors. Two files whose paths give one prefix, such as
a_b.x and a/b.x, or a member named like its entry, are a driver error
at the block. A static object, function, typedef, or tag whose name is a
module name of the same C namespace, in either order of declaration, is a
parse error at the later declaration, because a module name means one
thing in every file. Two files adopting a class under one short name is a
parse error at the second. A file-scope typedef named module keeps that
word an identifier.
A quoted include of a file beside the member includes that file, in collection and in the generated C. Any other include resolves as it would at the block’s position in the entry.
Members are not translation units. A build never lists them; the entry’s generated C and header contain everything. The entry’s depfile and interface depend on each member’s content, so editing a member translates the entry again and changes what an including unit collects. Translated on its own, a member is an ordinary file.
Diagnostics, --source-map line directives, dump locations, and editor
positions report the member’s file and line. A unit that includes the entry
collects the public declarations of every member. A unit that includes a
member directly collects only that member, as it would any file. A file
without a block reads exactly as before.
Script units
A script unit is a source file whose first line begins with #!, usually
#!/usr/bin/env -S x2c script so that x2c script
runs it. The compiler reads that first line as #include "scripting.x",
which brings in args.x, diff.x, digest.x, path.x, process.x, and
regex.x. Every other line keeps its line number.
A script unit takes one of two forms, chosen by whether it defines a
function named main at file scope:
- With
main, it is an ordinary program. Its declarations, including initialized ones such asint count = 0;, stay at file scope andmainruns. Adding a shebang line turns a program into a script. - Without
main, its top-level statements run in order as the program.
A script unit that defines main and also has a top-level statement, such
as an expression, control flow, a with block, or a statement macro, is an
error.
In the second form these top-level forms stay at file scope:
- preprocessor lines,
import, and protocol declarations and adoptions; macroandkeyworddefinitions, top-level$(...)Lisp, and invocations of macros and keyword aliases whose result is file-scope syntax, including decorators of functions and types;typedef,static,module,extern, andmetadeclarations and static assertions;- type definitions such as
struct Point { int x; };and classes; - function definitions, including expression-bodied ones, and prototypes, meaning declarations that end with a parameter list.
Every other top-level form is a statement, including declarations with an
initializer or none, such as int total = 0; or Point p;. The statements,
in source order, become the body of a function that the generated main
calls with argc, argv, and args, a List of the argument Strings
after argv[0]. Statements can call the script’s functions wherever they
are defined, and those functions can call one another. The declarations
among the statements are locals of that body, so functions cannot refer to
them. Lambdas capture them as usual. Write static for a variable that
functions share.
Falling off the end of the statements returns zero, and return among them
sets the exit status. A <cmd-fail> that no statement catches prints the
command and its status on standard error, and that status becomes the exit
status. Any other uncaught Error prints its cause and details on standard
error and exits with status 1.
#!/usr/bin/env -S x2c script
static String greeting(String name) => %"hello, $name";
String name = args ? args.car().string() : "world";
printf("%s\n", greeting(name));
if (!args) return 1;
Indentation syntax
A file can group statements by indentation and end them at line breaks
instead of writing braces and semicolons. The file is in the indentation
syntax when its name ends in .xp, or #pragma indent appears before its
first line of code. Only comments,
blank lines, and other directives may come before the pragma, and it does
not reach the generated C. A .xp unit is otherwise an ordinary unit:
#include "shapes.xp" includes its generated shapes.h, and builds,
packages, and x2c script accept .xp wherever they accept .x.
The indented form is a spelling of the brace form. The tokenizer inserts the braces, semicolons, and condition parentheses, and every later stage reads the same tokens as it would for the brace form:
#pragma indent
int sum_positive(int *xs, int n):
int total = 0
for (int i = 0; i < n; i++):
if xs[i] < 0: continue
total += xs[i]
return total
int main(void):
int xs[] = {3, -1, 4}
printf("%d\n", sum_positive(xs, 3))
return 0
7
- Lines. A logical line ends at a line break outside brackets. A
deeper line continues the previous one unless that line ends in a
:that opens a block, and a line that starts with.continues a method chain. The:of a?:expression never opens a block. - Blocks. A line that ends in
:before a deeper line opens a block, and each return to an outer indentation closes one.if,else if,while,foreach,switch, andmatchtake their condition with or without parentheses, andforkeeps the parentheses around its three clauses. The condition belongs to the last of these keywords on the line, so an unbracedforeach (...)can continue into anif x:block. Acase,default, orcatchlabel keeps its colon and opens a block of its own. Astruct,union, orenumbody closes with};, unless its header starts withtypedef, and enumerators keep their commas. - One-line bodies.
if cond: statementputs the body after the first colon outside brackets that does not belong to a?:. - Statements. Every other line ends with an implied
;. Directives, enum bodies, a line that is one whole$(...)or@(...)form, and a statement that is only a macro hole, alone or after a control header, take none. - Bare blocks.
do:whose block is not followed by awhileline at the same indentation is a bare{ ... }scope. With that trailer it is ado-whileloop. - Decorators. A decorator invocation is written with a
leading
@, as in@$time("load"), on the line before the statement it wraps. Without the@, the line would end with;as a statement macro call does. - Explicit forms. Braces and semicolons written explicitly keep their meaning, and braces that make values, such as initializers and Map literals, never open blocks.
The tokenizer rewrites each indented form into these brace-form tokens. A block is the deeper lines that follow its header, and it closes where the indentation returns:
| Indented form | Brace form the parser reads |
|---|---|
int f(int x): and a block | int f(int x) { … } |
if x: and a block | if (x) { … } |
if (x): and a block | if (x) { … } |
else if x: and a block | else if (x) { … } |
else: and a block | else { … } |
while x:, switch x:, match x: | while (x) {, switch (x) {, match (x) { |
foreach T v in c: and a block | foreach (T v in c) { … } |
for (a; b; c): and a block | for (a; b; c) { … } |
if x: s | if (x) s; |
else: s | else s; |
case X:, default:, catch P: and a block | case X: { … } |
struct S: and a block | struct S { … }; |
typedef struct S:, a block, then S | typedef struct S { … } S; |
enum E: and enumerators | enum E { enumerators … };, commas as written |
do: and a block | { … } |
do:, a block, then while (c) | do { … } while (c); |
@$m(args) | $m(args), with no ; |
| any other statement line | the line followed by ; |
#pragma indent | nothing |
Inserted tokens have no width and take the position of the token beside
them, so diagnostics and source spans report lines of the indented file. At
the start of a statement line, % and < begin a literal, as in
%(ls).job().run() or <done>, even when the line before ends in an
operand; on a deeper continuation line they keep their operator meaning.
Indentation uses spaces. A tab in indentation, or a return to an
indentation that matches no open block, is an inconsistent indentation
error at that line.
Packages and import
A package is a directory whose name is a valid C identifier. Its entry unit is
<dir>/src/<name>.x, or <dir>/<name>.x for a single-file package. The
package is everything that entry unit includes. Nonstatic declarations and
compile-time definitions are public. There is no manifest or export list.
--package-dir <root> registers a directory of packages; a target in
x2c.toml may set package-dirs instead.
An import declaration gives a package, binds a local alias, and may list
individual members:
import "<package>" [as <alias>] [with <Name> [as <Local>] {, ...}] ;
import "geo"; // alias: geo
import "geo" as g; // alias: g
import "geo" with Vec, span; // plus bare Vec and span
import "geo" with Vec as V; // plus bare V
import "geo" as g with Vec; // both spellings
The alias changes how names are written in x2c. g.Vec, g.Vec.new(2.0, 3.0),
and g.span(p) all compile to the package’s C names, which carry the package
prefix: geo__Vec, geo__Vec_new, geo__span. Two packages may therefore
publish the same type or function name in one program. An existing local or
file-scope binding takes precedence over an alias of the same name.
An import ... with name changes only the source spelling. The C symbol keeps
its package prefix. Vec v = Vec.new(2.0, 3.0) emits geo__Vec and
geo__Vec_new. Any public name the package exports may be listed, including
free functions. The alias is registered either way, and the clause only adds
shortcuts. A name the package does not export is an error at that name, and a
local spelling that another binding or a declaration already uses is reported
as package name 'V' is already bound or package name 'V' collides with a declared name. A declaration shadows a with name as it shadows an alias.
Names derived from a package type carry the prefix as well. A Var(T)
participant’s tag is <geo__vec>, and its converters are geo__Vec_var and
geo__Var_vec. A package’s own sources write all of these bare and let the
compiler add the prefix. Only a Symbol literal, a global interned identity,
is written out in full.
A package may publish a method on a type it does not define, such as
Var.json, or on a type its own vendored header defines. Imported methods
take part in dot-call lookup. The receiverless form
Type.member(...) and protocol punctuation resolve through the same imported
names, so a package that adopts one of its own protocols for a foreign type
gives the consumer that operator. One imported match is callable. More than
one is an error at the call, with the packages listed by name; imports that
are never called do not conflict. An alias-qualified function call such as
json.Var_json(value) selects one package explicitly.
A package’s own sources are <dir>/src/** and the single-file
<dir>/<name>.x. Other files in the package directory, including its tests
and examples, are ordinary consumers that reach it through import.
An import exposes:
- nonstatic functions, objects, standalone types, and types needed by public declarations;
- public macro definitions, keyword aliases, and meta functions;
- public protocol declarations and adoptions; and
- the public declarations of the files the package entry includes.
Compile-time definitions arrive in the order an include of the entry source would deliver them. Static declarations remain in their source files. Macro names retain their declared spellings. One program uses one version of a package.
A public bodyless meta prototype crosses the import like any function
declaration. When the package has built its native module,
<dir>/builds/<name>.module, the import loads it, so compile-time code in
the consumer can call the function. Modules named by --native-module or
native-modules are consulted first. A module built by another compiler is
an error at the import; see
packages with compile-time parts.
Names from a C header remain unprefixed, as if the consumer included that header directly. A package prefixes only the names it declares.
An unprefixed public declaration from an x2c file outside the package directory
would enter the consumer’s namespace unchanged. The compiler rejects it as
package 'geo' exposes unprefixed top-level declaration '...'. Including
the foreign file directly in the consumer avoids that package-prefix conflict.
A runtime module, like a C header, crosses unprefixed. A consumer that
declares a name in an imported package’s geo__ space is reported as 'geo__x' is reserved for imported package 'geo'.
C foundation
x2c keeps C declarations, expressions, operators, functions, structs, unions, enums, pointers, arrays, and control flow. It adds language forms around that foundation and keeps C’s object model.
Use #include to include source and headers. Avoid other C preprocessor
constructs in x2c source; use x2c macros and language features instead.
Intermixing #define with x2c transformations is unsupported. The compiler
preserves some native macro behavior on a best-effort basis, but does not
guarantee that native macro text follows renamed or file-private bindings.
Member access uses . wherever x2c parsed the struct, whether the receiver is
a value or a pointer, and whether the declaration came from x2c source or from
a C header x2c reads. -> remains accepted. It is required in two places. One
is a pointer to a struct whose layout x2c does not read, such as one from a
system header or an unresolved third-party header. The other is a #define
body. A member of an anonymous union or struct belongs to its enclosing
aggregate, and . reaches it through a pointer like any other member. .
reaches through one pointer level, so a pointer to a pointer typedef keeps
(*pointer).field. x2c also recognizes method-style calls such as
list.len(). The namespaced function is selected from the static receiver
type. When that function declares its first parameter T * and the receiver
is an addressable T, the receiver’s address is passed, so rec.bump(4)
reaches void Rec.bump(Rec *rec, int by) without an explicit &. A receiver
that is a pointer to the declared parameter is one level too far and is a type
error; write (*pointer).method(). Normal pointer conversion rules still
apply, including preservation of const and other qualifiers. A qualifier on
the receiver itself does not change which methods and operators the type has,
so a const String has the same len() and + as a String. Method syntax
does not make every value dynamically dispatchable; only registered callbacks
define custom behavior.
A receiver spelled struct T or union T uses the methods of the typedef
that names the same aggregate. A same-named typedef takes precedence;
otherwise the aggregate must have one direct typedef. An unrelated variable
with the tag’s spelling does not supply a method owner. Generic functions
named struct_method or union_method do not participate in method lookup.
Another module may define methods for the type; this rule selects the
receiver’s type and does not restrict who can define its methods. Type
inspection and member completion follow the same lookup.
A method may use Self in its result and parameter types to keep the
receiver’s static typedef:
Self List.cdr(Self values);
Self List.append(Self left, Self right);
List List.map(List values, Func fn);
For a ListInt receiver, dotted lookup treats the first two signatures as
ListInt -> ListInt and requires a ListInt second argument to append.
Ordinary conversion still applies, so a plain List reaches the existing
validating ListInt converter. map remains List -> List; x2c never infers
covariance for an unmarked method.
Self is contextual syntax, valid only in a dotted method declaration or
definition with a concrete owner and a compatible first parameter. It may be
qualified or nested under pointers. Every occurrence is bound to the original
static receiver typedef, even when lookup crosses several typedefs. A direct
call to List_cdr retains the concrete List -> List signature, and generated
C keeps that ABI. Prototypes and definitions must agree on both their concrete
types and the positions marked Self.
Expression-bodied functions
A function that returns one expression may use =>:
typedef struct Pair { int left, right; } Pair;
static int twice(int value) => value * 2;
String Pair.describe(Pair pair) =>
%"${pair.left}, ${pair.right}";
This is shorthand for a compound body containing one return statement. The
expression uses the function’s parameter and body scopes and follows the same
conversion, cleanup, and lifetime rules as return expression;. The semicolon
terminates the body; nested compound literals, collection literals, and lambda
bodies do not terminate it.
A void function has no value to return, so its expression body is the
shorthand for a compound body containing one expression statement:
typedef struct Counter { int value; } Counter;
static void Counter.step(Counter &self) => self.value++;
An expression is required; =>; is invalid. The rules for writing return in
a void function are unchanged. Use a compound body when a function needs
declarations, several statements, or a comment inside the body.
Statement expressions
x2c accepts the GNU statement expression, which generated C already requires
a GNU-compatible compiler to compile. ( followed by { begins one when a
; stands at the top level of the braces, outside any nested parentheses,
brackets, or braces; otherwise the brace keeps its meaning as a braced
initializer or a Map literal, so ({}), ({1}), and ({a: 1}) are
unchanged:
int main(void) {
int area = ({ int width = 6; width * 7; });
String name = ({ String first = "Ada"; first + " Lovelace"; });
printf("%d %s\n", area, name);
return 0;
}
42 Ada Lovelace
Its value and type are those of its final expression statement; a statement
expression that ends any other way is void. Declarations inside it belong
to its block. A template may contain one, and its locals are renamed like any
other template local:
macro Expression $plus_one(Expr $value) => ({ int t = $value; t + 1; });
In a macro body or quotation, a statement hole stands without a ; of its
own, so a hole at the top level of the braces also begins a statement
expression unless a comma there makes the braces data. ({ $first $second })
and ({ $only }) are code; data with one element states its type, as in
(Point){ $x }, or omits the parentheses where its destination gives the
type.
A defer, or a managed declaration, directly inside a statement expression is
an error: its cleanup region would enclose the final statement, and C would
lose the value. Either may stand in a block nested inside the statement
expression. Leaving a statement expression runs cleanup as described in
Errors and cleanup. A statement expression is an
ordinary sizeof operand, so sizeof({ ...; }) measures its value’s type.
Reference parameters
A function parameter declared T &name aliases an addressable T supplied by
the caller. The parameter name is an ordinary T lvalue inside the function,
so reading it reads the caller’s object and assigning it changes that object:
static void swap(int &left, int &right) {
int temporary = left;
left = right;
right = temporary;
}
int main(void) {
int first = 4, second = 9;
swap(first, second);
printf("%d %d\n", first, second);
return 0;
}
The call does not write &; x2c takes each argument’s address. Passing a
reference parameter to another reference parameter forwards the same object.
The argument must be an addressable lvalue of the referenced type whose
storage remains live for the call. A pointer, NULL or 0 is a compile
error for a required reference; to pass the object a pointer p addresses,
write *p.
An optional reference parameter is written T &?name. It accepts either an
addressable T or NULL (or 0). A null optional reference means that no
caller object was supplied; it is distinct from a caller’s pointer-typed
object whose value is null. Test the optional reference before reading or
writing its object:
static int bump_if_present(int &?value) {
if (!value) return 0;
value++;
return value;
}
In the nonnull arm of a direct test, or after a null arm that returns or
raises, the parameter is an ordinary T lvalue: reads, writes, and reference
forwarding use the caller’s storage transparently. Outside such a proven
nonnull path it remains a nullable address and can be tested or forwarded to
another optional reference. Required references still reject null at compile
time.
Generated C uses a pointer parameter and explicit address-taking and
dereferencing. Reference parameters add no runtime representation or ownership
behavior. The transparent & form is supported only on parameters; reference
locals, globals, and return types are not language features. Declarations in
those positions are compile errors.
Delegate fields
delegate marks a named struct or union field as a fallback for dotted method
calls:
typedef struct Reader {
int value;
} Reader;
typedef struct Document {
delegate Reader reader;
} *Document;
int Reader.read(Reader reader);
If normal lookup finds no applicable Document.read, document.read() uses
the field’s static type and emits the same direct call as
document.reader.read(): Reader_read(document->reader). The original
receiver expression occurs once. Pointer and value fields follow the ordinary
rules for . or ->, addressability, qualifiers, conversions, and null
values. Delegation emits the field access and direct call; it adds no wrapper,
temporary, runtime check, symbol, or dispatch table.
Ordinary dot-call lookup runs first. Delegate fields are
searched only if those lookups fail, and a member rewrite a program registers
for the receiver’s own type is tried before them. The shipped component
src/components/delegate-members.x does this search. $rewrite registers
it with the receiver pattern %(expr <delegate> *). It runs only when the
receiver’s aggregate declares a delegate field. The search visits marked
fields in source order to produce consistent diagnostics. One resolving path
is used; two or more are an ambiguity error, regardless of field order. An
explicit outer method therefore resolves an otherwise ambiguous pair.
Delegate fields may chain without a numeric depth limit. Lookup tracks the aggregate types on the current field path. A cycle is reported only when lookup needs that path and finds no valid candidate. One valid path is used even if another contains a cycle; multiple valid paths remain ambiguous.
The terminal field type determines method typing. Self parameters and results
mean that field’s static type, never the outer aggregate, and a method
returning Reader still returns Reader. Delegation does not make the outer
type a subtype, adopt a protocol, acquire a conversion or Var identity,
expose the field’s fields, or forward punctuation, indexing, operators, or
receiverless calls. Type.resolve_member is also direct-only. Its method
result cannot represent a projected receiver path.
Thread-local storage
threaded is x2c’s spelling of C’s thread-local storage class, and gives each
thread its own copy of an object:
threaded int depth;
static threaded Scope current;
It pairs with static or extern, in either order, which is the only pairing
C allows. _Thread_local and thread_local mean the same thing and are
accepted, so C that already writes it either way passes through unchanged; all
three emit _Thread_local.
Pointer type mismatches
Where C reports a pointer type mismatch as a warning, x2c reports an error:
passing a struct Rec * to a parameter declared List, String, Map, or
Array is cannot convert (* "Rec") to ("List"). Typedef spelling is not a
mismatch, so Ast and List are the same type here. The rule applies only
when both sides point at a named struct, union, or enum or at a builtin scalar;
void *, function pointers, arrays, and types from system headers convert as C
defines. A cast still allows the conversion.
Protocols
Protocols declare a compile-time relationship between a concrete base and explicit participant types. The grammar is:
protocol-declaration := [ static ] protocol type-name ( identifier ) {
associated-declaration*
protocol-member*
}
protocol-adoption := [ meta ] [ static ] protocol type-name ( identifier )
[ as type-name | tag symbol-literal ] ;
associated-declaration
:= associated identifier = type-name ;
protocol-member := type-name identifier . identifier
( parameter-list ) [ = c-identifier ] ;
The identifier in a declaration body is a fresh type-variable binder. The identifier in a bodyless adoption must name an existing type:
protocol Var(T) {
associated Key = Var;
Value T.getindex(T, Key);
}
protocol Var(Array);
The optional as form applies only to Var and says that the participant
uses an existing fixed runtime tag:
typedef List Row;
Var Row.var(Row);
Row Var.row(Var);
protocol Var(Row) as List;
Row retains its own typed conversions, methods, signatures, and protocol
participation. Compatible List methods inherited through its typedef chain
may satisfy Var(Row) without Row forwarding methods. A boxed Row has the
tag <list> and uses List for dynamic dispatch. Runtime tests therefore
cannot distinguish it from another list: value is Row and value is List
test the same tag. The type after as must be a scalar, pointer, or fixed
runtime class that already has a compiler-known Var tag.
The optional tag form also applies only to Var, but keeps a distinct
descriptor under the supplied custom tag:
protocol Var(ArrayString) tag <arraystr>;
The tag must be a Symbol literal, cannot be built in, and must be unique
across the process. The compiler records the full lowercase participant name
and rejects a second type with the same tag. tag and as are mutually
exclusive: use tag for a distinct runtime class and as for a typed view of
an existing representation.
Associated declarations precede members. A declaration binder that shadows a visible type receives a warning; use the bodyless form to adopt that type.
static may mark a protocol body or a concrete adoption:
static protocol Prepared(LocalPlan);
That adoption applies only within the translation unit that declares it. It forces local generation even when the complete adoption is public. It is also valid when a private dependency already implies locality; it then records the same local relationship explicitly and does not change linkage.
An adoption is local when its protocol body, participant typedef, required
converter, required native target, or adoption row is private. A protocol body
declared as static protocol BASE(T) { ... } is private to its source file.
Local ordinary adapters are static inline, appear only in generated C, and
are omitted from generated headers. Local native aliases and their signature
checks are likewise source-only.
Descriptor-producing adoptions such as Var(T) may be local. Their converters
and generated thunks remain internal, but descriptor registration is
process-wide so boxed dispatch still works. A Var(T) as R adoption emits no
descriptor for T; boxed dispatch uses R’s existing descriptor.
One generated C method cannot have incompatible local and external linkage. The compiler reports that conflict at its source location. A protocol whose dependencies are all public retains its public behavior.
Protocol declarations and adoptions appear at the top level. A declaration
creates no runtime interface object; a Var(T) adoption still emits thunks
and registers a descriptor. Each translation unit collects them with its
other declarations, and reading source, cached declarations, or a unit
interface gives the same result. A byte-identical repeat has no effect, and
a conflicting declaration is an error at its location. A fully public native
adoption emits its aliases in the header; a local one keeps them in the
participant’s C file.
Participation must be declared; conversion names and matching typedefs do not imply it. The compiler then determines the adoption’s visibility. An adoption resolves in the unit that contains it, so the protocol, participant, required converters, native targets, and constraining participant members must be visible there. A unit that includes it reuses that conformance from the declaring unit’s interface; members visible only to the including unit do not change it. When no current interface exists, collection resolves the adoption from the declaring unit’s own declarations, so the result does not depend on build order. A declaring unit that imports a package records no conformance. A unit that includes it resolves those adoptions from its own visible declarations.
A typedef descendant with no exact adoption may use the nearest visible ancestor’s resolved conformance. It reuses the ancestor’s methods, associated types, conversions, and representation without creating an adoption, adapter, native alias, descriptor, runtime tag, registration, or conformance output for the descendant. An exact descendant adoption resolves a new conformance and takes precedence. If that exact adoption is invalid, it is diagnosed and lookup does not fall back to an older ancestor. A local ancestor adoption is inherited only in translation units where that adoption is visible.
Common source forms are a plain adoption for public protocols and public
participants, a plain protocol Var(PrivateType); for an internal type that
crosses Var, and an explicit static protocol PrivateBase(PrivateType); for
a fully private relationship.
Member resolution
Each adopted member resolves independently. Omitting one does not prevent the others from working.
| Resolution | Dot call | Mapped punctuation | Descriptor-backed box |
|---|---|---|---|
implemented | participant member | participant member | typed thunk |
native | checked native alias | checked native alias | not implied |
ordinary base-default | generated forwarding member | forwarding member | routes through the same winner |
no-member | no | no | another protocol’s owner, else the base fallback |
sig-conflict | compile error | compile error | compile error |
An ordinary base default calls a base implementation after converting the
participant through its total forward view. Dot syntax and punctuation use
that same semantic member, and Var(T) publishes it on T’s descriptor even
though Var(T) itself resolves the member as no-member. A Var fallback
such as Var.fallback_truth runs only for a descriptor slot that no protocol
filled. It adds no static participant method and does not change native C
punctuation.
An explicit adoption may resolve an ordinary implementation through the
participant’s typedef chain. For example,
typedef List Domain; protocol Iter(Domain); selects List.iter directly;
it does not require a forwarding Domain.iter method.
An adoption selects each member in this order: the participant’s own or
imported-package method, then the nearest ancestor’s own or imported-package
method, then an ordinary base default. Reaching the protocol base uses the
same default rule. When checking a method, the compiler treats parameters
with the method’s exact receiver type as participant parameters. Thus
List.equal(List, List) can satisfy int T.equal(T, T) for a List typedef.
A concrete result keeps its declared type; a Self result preserves the
participant’s static typedef.
Var(T) normally remains exact: an ordinary adoption does not search the
participant’s typedef chain. protocol Var(P) as R is the exception. When P
inherits from the explicitly named representation R, compatible methods of
R satisfy the adoption without P forwarding methods. Boxed values already
use R’s descriptor, so compile-time conformance uses the same implementation
without changing runtime dispatch.
A participant method with the right name but an incompatible signature is a
sig-conflict. The compiler reports an error at the adoption and identifies
the protocol member and conflicting definition.
Associated types
associated Name = Default; declares a type variable local to the protocol.
A generated member has one owner. The compiler infers a participant’s
binding by unifying the member signatures
the participant declares. Every occurrence must resolve to one type, and the
default applies only when no declared member constrains the name.
Calls through a boxed value pass associated arguments and results as Var;
the generated thunk converts each exactly once. Direct calls keep the
participant’s static signature.
Dot-call lookup
A dot call visits the receiver’s static type, then each typedef parent. At
each type it tries the type’s ordinary method, then a method an imported
package adds to that type, then the member that the type’s protocol
conformance selects. When the nearest adopted conformance, exact or
inherited, resolves the member as anything other than implemented, the
conformance’s member is used, and the type’s own method with that name is
not. When that conformance is inherited from an ancestor, a call that the
type’s own method would otherwise answer is an error: adopt the protocol for
that type so its own method is selected.
Conversions required by adapters
The selected methods determine which conversions an adoption needs. The compiler generates adapters for those methods and requires only the conversions each adapter uses:
| Adapter | Required conversion |
|---|---|
Ordinary default parameter exactly T | participant to base |
Ordinary default result exactly T | base to participant |
Dispatcher thunk parameter of T | base to participant |
Dispatcher thunk result of T | participant to base |
Native alias occurrence of T | the implicit C direction at that position |
For a participant P and base Base, name the forward conversion:
Base P.base(P)
The forward converter is a total view: every value of the declared
participant must produce a valid base value. A reverse converter has the
conventional spelling P Base.p(Base), with Base.as_p reserved for a real
name collision. It may be checked and partial because not every base value
must contain a P. Signatures cannot express this totality distinction, so it
is part of the protocol’s rules.
Var(T) descriptor thunks both unbox arguments and box results, so most
object participants need both directions.
A forwarding protocol whose defaults use T only as a parameter may need
only the forward conversion. A default returning T also needs the reverse
conversion to turn its base result into the participant type. The adapter
generator does not convert T inside compound parameter or result types, so
those types are rejected. A missing or unsupported conversion produces an
error at the adoption, naming the method that needs it. For an ordinary
protocol, a member the participant implements needs no adapter, so its
signature may use T inside a compound type, as in int T_area(T *self).
Operators
Operators call the same resolved methods as dot syntax. The mappings include:
| Syntax | Member |
|---|---|
+ - * / % | add sub mul div mod |
direct += -= *= /= %= | add sub mul div mod |
direct prefix/postfix ++ -- | add sub with a converted 1 |
unary - | neg |
== != | equal |
< <= > >= | compare against zero |
conditions, !, &&, ` | |
needle in value | contains, with the right operand as receiver |
| indexing and indexed mutation | getindex, setindex, updateindex, postfixindex |
Matrix multiplication uses the ordinary left.matmul(right) method.
Var.matmul dispatches a registered matmul witness. The @ prefix
splices macro syntax; @ and @= are not arithmetic operators.
An operand with no x2c type, such as a preprocessor macro’s name, cannot
convert for a participant that implements the operator; images - MEAN with
#define MEAN 0.1307 is a compile-time type error. Cast the macro, (double) MEAN, or bind its value to a local.
A participant may also declare void T.discard(T). The compiler calls it on
an unnamed temporary it made for one operator, such as the product in
a * b + c or the converted 2.0 in 2.0 * x, immediately after the
operator that consumes the temporary returns. A value bound to a name is
never discarded. A handle type whose values own native storage uses this to
release each intermediate of a long operator chain before the enclosing
scope ends; a value type needs no such member.
A method or function returning a scalar or void also discards such arguments
after the call. An ordinary call returning a pointer or aggregate may borrow
its input, return an interior pointer, or contain a borrowed handle. Unless
the callee is a known fresh operator or numeric converter, its temporary
arguments remain alive in their enclosing scope. A temporary argument does not
make the call’s result fresh.
An implemented or native member is eligible. An ordinary base default is also
eligible because it is an inherited member reached through a total view. An
empty Var descriptor slot is not a resolved static member. If no eligible
member exists, valid native C behavior still applies; otherwise the compiler
reports the ordinary operator error. When exactly one operand of a binary
operator is a struct or union participant with the member, or a handle typedef
pointing at one, the other operand converts to that type through its declared
converter, so meters + 2.0 and 10.0 - meters resolve when double.meters
exists. Scalar pointers and numeric participants keep native C behavior for a
mixed operand, so text + 1 remains pointer arithmetic. The runtime uses the
same rows itself: String.add implements add for protocol Var(String), so
String + and += are ordinary resolved punctuation.
Logical operators preserve C short-circuit evaluation: each operand’s truth
method runs only when C would evaluate that operand. Indexed compound and
postfix operations evaluate the receiver, key, and value once, then call the
update method once.
Direct compound assignment, ++, and -- follow
protocol-backed direct updates.
Plain =, ===, and !== are never overloaded. === and !== retain
native identity for static participants. Boxed identity is in dynamic
truthiness and binary operators.
Members shared by several protocols
A participant implementation satisfies every adopted protocol that declares the same compatible member, and no protocol generates that member.
Two ordinary defaults are an error. The diagnostic names both protocols. Implement the member on the participant to choose its semantics. Declaration order does not decide.
A Var(T) adoption puts that same generated member on T’s descriptor when
Var(T) declares the member and T does not implement it, so static and
boxed behavior agree on one generated C name.
C initializers and static assertions
Array and aggregate initializers accept chained index and field designators. Positional values continue from the designated subobject using C’s ordinary brace-elision rules:
String labels[4] = {[1] = "first", "second"};
int grid[2][3] = {[0][1] = 7, 8, 9};
typedef struct Record { String names[2]; Var last; } Record;
Record record = {.names[1] = "second", "last"};
Each supplied value converts to its destination element or field type. After
.names[1] = "second" above, the next value initializes last, including its
ordinary conversion to Var. With .names = "first", "second", "last",
brace elision fills both array elements before continuing to last. Explicit
braces delimit a nested initializer. Typedefs and compound literals retain
the ordinary destination conversions, including C literals to String.
The native compiler checks index constant expressions, array dimensions, and
bounds.
_Static_assert(condition, "message"); is accepted at file scope, block scope,
and within a struct or union. It emits an ordinary C static assertion and adds
no field, binding, or runtime operation:
_Static_assert(sizeof(int) >= 2, "int is at least 16 bits");
A file-scope assertion stays in its implementation. An aggregate assertion is emitted with the aggregate definition. The native compiler rejects a false or nonconstant assertion. Translation and source analysis alone do not perform that check.
Generic selection
_Generic(controlling, type: value, ..., default: value) is a C11 generic
selection. x2c resolves the controlling expression and each value, then emits
the selection unchanged. The native compiler chooses the association and
reports constraint errors. The selection has no x2c type, so it works where C
accepts the selected value directly:
int size = _Generic(sizeof(int), size_t: 1, default: 2);
A selection does not convert to Var or take part in method calls. A cast
gives it a type: Var kind = (int)_Generic(text, char *: 1, default: 2);.
An association type is a type name of specifiers, qualifiers, and pointers. Name a function-pointer or array type through a typedef.
Mixed declaration rows
A semicolon-terminated declaration at file scope, block scope, or inside a struct or union may restart its declaration specifiers after a comma:
int i, float x, char c;
int first, second, const char *name, *alias;
Each fresh type starts a declaration with the same scope and source order as if
it were on a separate row. Declarators that share a type keep the C spelling:
first and second are int, while name and alias are const char * in
the second example.
Valid C declarators retain their meaning. In particular, if T is a typedef,
int i, T; still declares an int named T and hides the typedef. A spelling
such as int i, T value; restarts at T, because the second identifier cannot
continue the int declarator. Function parameter lists already give each
parameter its own type; mixed rows do not extend for initializers, foreach
binders, or macro Decl arguments.
Flat List destructuring
Declarations and assignment expressions may destructure the first elements of
a List into flat identifier targets:
List values = %(1 2 3);
List numeric_values = %(0 10 2);
Var (a, b, c) = values;
int (start, stop, step) = numeric_values;
(int index, float weight, char code) = %(1 2.5 ${'x'});
Var first, second;
(first, second) = values;
(first) = values;
List copy = (first, second) = values;
The leading declaration specifier in int (start, stop, step) applies to
every name. The parenthesized typed form gives each target a type, written as
it would be in a parameter declaration. Both forms declare their names in the
enclosing block. Storage classes and per-target initializers are not accepted
inside the parentheses. Inside a %() List literal, use ${} to insert a C
character expression because the List reader reads a bare single quote as
quote.
The singleton assignment destructures. It assigns element zero; the left side is not a parenthesized scalar assignment.
The source expression is evaluated once. Elements are then assigned from left
to right through the same conversions used by ordinary initialization or
assignment. Extra source elements are ignored. If the List is short,
List.getindex supplies void; a Var target receives it, while a typed
target follows its existing Var conversion and failure behavior.
Assignment destructuring has the same static type, value, and identity as its
right side, so it may appear in an initializer, argument, conditional arm,
comma expression, return, or another destructuring right side. The right side
is still evaluated only once; returning it does not copy the List.
The form is flat. Targets must be simple identifiers. Nested targets, members, indexed targets, dereferenced pointer targets, and rest captures are not supported.
Compile-time macros
Macros are explicitly invoked, hygienic compile-time code generators. They
differ from C preprocessor macros. Holes bind parsed syntax, definitions use
C-like x2c source, and every result is validated for its typed source position
before C generation. They make no promise about the runtime semantics of the
code they emit; for example, substituting an argument twice may evaluate it
twice. A definition’s left side looks like its invocation, and its body is x2c
with $hole markers:
macro Expression $twice($value) => $value + $value;
int main(void) {
printf("%d\n", $twice(21));
return 0;
}
Global definitions are top-level items. A compound statement may instead
contain a local definition whose name has no $. Both forms emit no runtime
declaration. macro is a contextual introducer only when the surrounding
grammar accepts a definition and a result kind and macro name follow it. It
remains legal as an ordinary typedef, variable, parameter, field, or function
name everywhere else. A braced definition places { ... } directly after the
signature. An expression definition places => expression; after it,
including the trailing semicolon. A Stmt macro may also use the expression
form; its body is then one statement. That statement is an invocation of a
Stmt macro or a statement decorator, such as => $other(args);, or else
the expression statement expression;. When that expression is a meta
call, the call may return a statement, as described under
Macro-visible syntax.
Here guarded returns an if statement, which an expression position
would reject:
#include "meta.x"
meta static List guarded(List condition, List body) =>
$!{ if ($condition) $body };
macro Stmt $when(Expr $condition, Stmt $body) =>
$guarded($condition, $body);
int main(void) {
int n = 3;
$when(n > 2, printf("%d\n", n););
return 0;
}
3
For source compatibility, the compiler also accepts the former => { ... }
braced form and a complete parenthesized expression body written as
=> (expression) without a semicolon. New code should use the forms above.
macro Stmt $legacy.guard(Expr $condition) => {
if (!$condition) return 0;
}
macro Expression $legacy.twice(Expr $value) => ($value * 2)
Ordinary global invocation is $qualified.name(arguments). A local macro is
invoked as name(arguments). A source file may also give a visible global
macro an identifier spelling through a keyword declaration, described under
Decorators. Macro names are separate from C identifiers and
global names may be qualified, as in $project.logging.trace. A part after a
dot may be a keyword, as in $error.arg.void. The x2c.* and
lisp.* macro and Lisp namespaces are reserved for compiler-shipped
facilities; $lisp.bind, $lisp.binding, and $lisp.install are the shipped
native-binding macros.
Definitions and includes become visible in source order. A definition must precede its first use. A later definition of the same name shadows the earlier definition for later invocations. Earlier expansions keep their original definition, including through transitive includes.
Local definitions
A local definition uses one bare identifier and is a compound-statement item:
static int scaled_sum(int scale, int value) {
macro Expression scaled(Expr $input) => $input * scale;
return scaled(value);
}
Its name becomes visible at the definition and remains visible to the end of
that block and in nested blocks. An inner definition of the same name shadows
it until the inner block ends. A later definition in the same block replaces
it for following source. Fixed C and x2c keywords are not identifiers and
cannot be local macro names; with is also reserved because it introduces the
lexical with statement.
A bare name(...) selects the innermost local macro before a file-local
keyword alias or an ordinary call or typedef-style cast. A bare name
without parentheses remains an ordinary identifier. (name)(arguments) is an
explicit ordinary call, while $name(arguments) selects only a global macro
or a meta function. A local and global definition may therefore share a name
without ambiguity.
A local definition follows the hygiene rules
of every macro. scale above reads the parameter of scaled_sum, where the
expansion lands.
Local Expression, Stmt, Field, Entry, and Enumerator
results use their usual positions. A local decorator may target any syntax
whose invocation position is reachable before the defining block ends. A local
Unit result and a local decorator targeting Function or Unit syntax are
rejected. Their invocation positions are outside that lifetime. A
macro-generated local definition is published at block position with the same
visibility and shadowing behavior as a direct one.
Compile-time Lisp remains one translation-unit session. A local definition may read or change that session, but Lisp definitions, imports, globals, and other effects do not disappear when the local macro name leaves scope.
Holes and sequences
A hole binds parsed syntax, not source tokens. Its kind is normally inferred from every position where it appears in the body: an operand needs an expression, a cast needs a type, and a declarator needs a name. If those uses do not imply exactly one kind, including when a hole appears only inside compile-time Lisp, the definition must annotate it in the invocation pattern.
Hole kinds, result kinds, and decorator targets are one vocabulary of
syntactic categories. Each name below selects the same category wherever it
appears, spellings match without regard to case, and Expression and
Expr are synonyms. These are the complete
hole-kind annotations:
| Annotation | Bound syntax | Example argument |
|---|---|---|
Expr or Expression | expression | count + 1 |
Type | type | FILE * |
NamedType | name followed by a complete type definition | Point { int x; int y; }; |
Decl | non-function declaration | static int value |
DeclaratorRow | declarator with optional initializer | second = first + 2 |
Function | function definition | int f(int x) => x; |
Name | identifier | checksum |
Literal | one literal | 42 or <char> |
Param | parameter declaration | const char *name |
Stmt | block item | return value; |
Field | field declaration | unsigned ready : 1; |
Entry | Map row | key: value |
Enumerator | enum member | ready = 1 |
Unit | top-level C declaration or definition | int value; |
Catch | one try catch arm: pattern and body | supplied by compile-time code |
Captures | a lambda’s complete capture clause | supplied by compile-time code |
MatchRow | one match arm or default | case 1: return one; |
A Catch @arms hole fills the catch arms of a try template, as in
try $body catch @arms finally $cleanup. Captures and MatchRow are
described below with the templates that use them. Compile-time code supplies
Catch and Captures rows by applying the macro as a value; a source
invocation has no argument syntax for them.
Because the definition is already known, an invocation parses each argument
according to its hole kind. A type such as FILE * therefore works as an
argument even though it is not an expression.
An Expr argument stays one operand. With the argument value > 0, the body
!$condition means !(value > 0). The same holds for a macro’s own result:
macro Expression $twice($value) => $value + $value; makes $twice(21) * 2
mean (21 + 21) * 2. Generated C has these parentheses only where C
precedence would otherwise regroup the expression.
A Decl argument captures one declaration without a trailing semicolon. Its
comma or closing parenthesis belongs to the macro invocation. The declaration
may have an initializer or use flat destructuring with two or more simple
identifier targets, but it may not contain multiple comma-separated
declarators.
A DeclaratorRow argument has no leading type or trailing semicolon. A
declaration template supplies the shared type, and a trailing
DeclaratorRow @rows hole forwards comma-separated declarators in order:
macro Stmt $group(Type $type, DeclaratorRow @rows) {
$type @rows;
}
int main(void) {
$group(int, first = 1, second = first + 2);
return second - 3;
}
An expression hole may be the receiver of ordinary postfix syntax in the body,
including $value.field, $value.method(), and $value[index]. The hole
ends at its registered identifier; this does not shorten qualified macro names
such as $project.logging.trace(). A singular Name hole may also appear
after . or ->, as in $value.$member or $pointer->$member. It supplies
the captured member spelling unchanged.
A registered Type hole may likewise supply the type name before . in a
method declaration in a Unit template, as in inline Var $type.$method($type value). A literal registered type may use the same member form, as in
Logger.$method. Both expansions use the Type_method declaration identity.
Repeated Unit-macro applications over Type holes are how x2c writes
compile-time generic code. Every supplied type must already be a valid named C
type, and every expansion produces separately typed declarations and
definitions. There is no runtime type argument, erased element representation,
or parameterized type spelling such as Box<T>. A macro generates named
concrete families such as IntValue and DoubleValue.
During shallow symbol collection, every file-scope Unit macro must expand.
Collection evaluates preceding top-level Lisp contributions before expanding
that producer. Its result is retained for the full parse, which binds the
retained function bodies without running the producer again. Static generated
declarations stay in their owner; public declarations and protocol rows reach
includers. A public function definition can follow its prototype inside the
same expansion.
A local Unit macro must expand during collection if it contains protocol or
adoption rows. Other local Unit macros are tried. A failed attempt contributes
nothing, and the full parse reports its errors at the invocation.
Protocol declarations and adoption rows are collection-time compiler
declarations rather than C declarations. A unit macro may emit them, and
importing units receive the retained rows. A Unit macro may generate
converter and method function definitions before the adoption in the same
expansion. Those functions need no separate prototypes when their definitions
are already visible. The compiler uses them to resolve the adoption and make
the functions visible to later code. Collection discards private declarations
with generated bodies,
so a private converter needed only within the defining unit can be generated
before the adoption.
@name is a sequence hole. It captures zero or more arguments of one
element kind, must be the final argument, and @name in the body is the
splice point:
static int sum(int a, int b) {
return a + b;
}
macro Expression $call(Expr $callee, Expr @arguments) =>
$callee(@arguments);
static int answer(void) {
return $call(sum, 19, 23);
}
The element kind is inferred in the same way as a singular hole and may be
overridden, for example Field @members. Argument-hole names must be
unique, and a name cannot be both singular and sequence-valued.
In a quoted Array template, an Expr sequence hole occupies an element
position and inserts its captured expressions in order. It may be empty:
#include "x2c.x"
macro Expression $values(Expr @items) =>
%[${10}, @items, ${40}];
int main(void) {
Array values = $values(20, 30);
Array empty = $values();
return values.len() == 4 && empty.len() == 2 ? 0 : 1;
}
The @items position is part of the macro template; ${expression}
inside an ordinary quoted Array still inserts one value.
Entry holes capture one key: value row; Entry @rows captures and
forwards zero or more complete rows.
A Captures hole holds a complete lambda capture clause as one value.
In a template, %!(@params) using $captures => $body preserves its
value and reference capture rows, with Param @params as the trailing
sequence. Apply this template as a macro value with the captured rows;
recognition binds those rows with *captures. Like Catch rows, capture
rows retain their canonical structure when supplied by compile-time code.
MatchRow @rows holds complete match arms in a Stmt template:
match ($subject) { @rows }. Each captured row retains its pattern,
guarded body, typed captures, and position; preprocessor directive rows stay
interleaved with the arms. A default arm is one row. Source macro calls may
pass comma-separated case and default arms; compile-time code may pass a
List of canonical rows to a macro value. The ordinary match binder handles
the rows when the template is expanded.
Result kinds and invocation positions
A macro has one result kind as well as argument kinds:
| Annotation | Body | Legal invocation position |
|---|---|---|
Expression | => expression ; | expression |
Stmt | braced block items, or => one statement | statement |
Field | braced field declarations | struct or union body |
Entry | braced, comma-separated key: value rows | Map literal |
Enumerator | braced, comma-separated enumerators | enum body |
Unit | braced declarations and definitions | file scope |
Declaration | braced declarations with retained public signatures | file scope |
The result kind is required between macro and the $ name. An arrow body
requires Expression or Stmt. An anonymous macro’s arrow body omits that
statement’s final ;, as in macro Stmt(Expr $v) => $again() $hit($v).
Braced bodies require Stmt, Field, Entry, Enumerator, Unit, or
Declaration. Inside a compound statement, a Stmt macro may produce zero or
more block items. Where the grammar requires one statement, such as an if,
else, or loop body, the expansion must contain exactly one statement. An
explicit { ... } or do { ... } while (0) in the production satisfies that
requirement; the compiler does not add braces. The invocation must occur in
the matching position:
#include "meta.x"
meta static List swap_type(TypeInfo type) => type.assoc(<type>);
macro Stmt $swap(Expr $left, Expr $right) {
$swap_type($left) temporary = $left;
$left = $right;
$right = temporary;
}
macro Field $timestamps() {
long created_at;
long updated_at;
}
static void reorder(void) {
int first = 1, second = 2;
$swap(first, second);
}
typedef struct Record {
$timestamps();
} Record;
An Enumerator body contains zero or more enum members. Members may have
explicit initializers, and commas separate literal members and sequence
splices:
#include "meta.x"
meta static List public_name(String name) => x2c_ident(name);
macro Enumerator $status_values() {
private_start = 3,
$public_name("STATUS_READY") = private_start + 1,
$public_name("STATUS_DONE")
}
typedef enum Status {
STATUS_UNKNOWN,
$status_values(),
STATUS_COUNT
} Status;
Literal names such as private_start are hygienic and may be referenced by
later generated initializers in the same expansion. x2c_ident publishes the
requested name through the generated-name slot. Generated members participate
in enum ordering and automatic value assignment, and successful exact members
are available to following source. Duplicate generated names and collisions
with source members are rejected. A malformed or failed sequence publishes none of its provisional
members.
An Entry body contains zero or more comma-separated Map rows:
macro Entry $handler(Literal $key, Expr $value) {
$key: $value
}
int main(void) {
int open = 1, close = 2;
Map handlers = {$handler("open", open), $handler("close", close)};
return handlers.len() == 2 ? 0 : 1;
}
An entry invocation occupies one comma-delimited row position but may expand to
any number of rows. In a bare {...} literal a direct or keyword-alias
invocation is written as a row. In the quoted %{...} literal it must sit
inside ${...}, which leaves the quoted Map syntax to parse x2c. Elsewhere in
an entry macro body, ordinary expression grammar applies, so an expression
macro may generate a key. @(form) inserts a List of explicit (map-entry KEY VALUE) nodes; singular $(form): value constructs a generated key.
A Stmt, Field, or Unit invocation ends with its own ;.
Enumerator and entry invocations occupy comma-delimited positions and have no
semicolon. A result used in the wrong position is rejected at the invocation.
Decorators
A decorator is a macro whose required first parameter is supplied implicitly from the expression or source item following its application:
macro Decorator $trace(
Function $function,
Expr $channel
) {
printf(
"[%s] %s\n",
$channel,
$Code.name($function)
);
@Code.body($function)
}
$trace("request")
static int answer(void) => 42;
The call supplies "request" to $channel; the compiler supplies the
following function to $function. The first parameter must be named,
singular, explicitly annotated, and one of:
| Target kind | Following source | Replacement |
|---|---|---|
Expr | one cast expression | one expression |
Function | file-scope function definition | block items replacing its body |
Stmt | one statement or local declaration | zero or more block items |
Field | one struct or union field | zero or more fields |
Unit | one top-level declaration or definition | zero or more top-level items |
NamedType | name and type definition, ending in ; | retained top-level declarations |
Parameters after the target are explicit invocation arguments. A decorator application has no semicolon; that omission pairs it with the following target. Only whitespace and comments may separate them. When a statement decorator appears where one statement is required, its production must likewise yield exactly one statement. A compound statement may contain the captured target plus any additional block items without requiring braces at the invocation.
A Stmt decorator standing before a file-scope function definition decorates
that function’s body. The body is parsed as usual, and the decorator’s
production replaces it. The same function is then rebuilt around the result.
Therefore parameters resolve inside the produced items, and a return from
within the body runs the decorator’s deferred cleanup before leaving:
#include "x2c.x"
$scope() static int total(List values, int bias) {
int sum = bias;
foreach (Var value, values) sum += value.int();
return sum;
}
int main(void) { return total(%(1 2 3), 10) == 16 ? 0 : 1; }
An expression body works the same way. A decorator written this way before a declaration that is not a function definition is rejected.
Block items the production places after the captured target are ordinary
statements, so a body that returns or that a cause transfers out of never
reaches them. A decorator with work to do once the body is finished puts that
work in a defer written before the target; $scope, $lock, and $time
are all written this way.
A .x file may give a visible macro or decorator an identifier spelling:
keyword ALIAS $QUALIFIED_MACRO;
keyword is contextual at file scope. It starts this declaration only when
followed by an identifier and a $ macro name, and remains an ordinary
identifier elsewhere. ALIAS must be a C identifier. A fixed C or x2c keyword
does not tokenize as an identifier and cannot be used.
The named definition must already be visible. A macro alias keeps the direct invocation’s parentheses, comma-separated typed arguments, sequence arguments, and terminator:
keyword swap $project.swap;
swap(left, right);
This is $project.swap(left, right);. Parentheses remain mandatory for a
zero-argument macro: generate().
A decorator with explicit arguments places them before its following target:
keyword range $project.range;
range(index, 0, count) {
consume(index);
}
This is $project.range(index, 0, count) TARGET. A decorator with no explicit
arguments retains the shorter ALIAS TARGET spelling. If every explicit
parameter is a sequence, ALIAS() TARGET supplies an empty sequence; the
parentheses distinguish the argument list from a parenthesized target.
Aliases do not add a second argument grammar. Arguments remain comma-separated and are parsed by the macro’s own hole kinds. An alias cannot introduce a semicolon-separated control header or capture arbitrary tokens.
The declaration captures the visible macro definition itself, so redefining
the same macro name later does not retarget the alias. A later keyword
declaration for the same alias replaces it only for following source.
Registration and use are source ordered. Expression, statement, field,
entry, enumerator, and unit macro aliases retain their result positions.
Decorator aliases retain their captured Expr, Function, Stmt,
Field, Unit, or NamedType target. An invocation in the wrong position
receives the same result or target diagnostic as its direct spelling.
Aliases do not globally reserve their identifiers. A macro or a decorator with
explicit arguments is recognized only when the next significant token is (.
The same spelling remains available as a type, declaration, field, label, or
function name, but a direct call or typedef-style cast has the invocation shape
and is claimed by the alias. A zero-explicit-argument decorator has no
parenthesized delimiter. Therefore its spelling is claimed at each parser
position that fits its target kind:
- A bare expression decorator can capture a read of a same-named expression.
- A bare block or function decorator can overlap a declaration that begins with a same-named typedef.
A nonstatic keyword declaration becomes available to including files at the
include position, together with the macro it names. static keyword ALIAS $macro; keeps the alias in its declaring file. A reusable ordinary .x module
may hold both macro definitions and aliases; include that module wherever its
syntax is needed.
Macro templates are parsed when defined, so a later alias does not reinterpret
an earlier template. Generated Lists and strings are not reparsed as
alias-bearing source.
An alias adds only a spelling. A decorator cannot turn a block that reads local variables into a C callback. It may wrap an existing callback or a block that captures nothing.
An Expr decorator has an arrow-expression body and is parsed as a prefix
unary form. Its target is the following C cast expression, including primary
and postfix expressions, unary expressions, and casts. It therefore binds
before binary, conditional, assignment, and comma operators. Parentheses widen
the target explicitly:
macro Decorator $nonzero(Expr $target) => $target != 0;
int main(void) {
int value = 1;
int left = $nonzero() value + 1;
int whole = $nonzero() (value + 1);
return left && whole ? 0 : 1;
}
The first initializer applies $nonzero to value; the second applies it to
the complete addition. The replacement is bound as one typed expression and
remains parenthesized at its call site. Surrounding C precedence cannot change
its meaning.
Decorators stack closest-first. The inner expansion must leave exactly one target of the required kind for the outer decorator:
#include "meta.x"
meta static List old_body(List fn) => Code.body(fn);
macro Decorator $logged(Function $function) {
@old_body($function)
}
macro Decorator $validated(Function $function) {
@old_body($function)
}
$logged()
$validated()
int answer(int value) {
return value * 2;
}
A Function decorator preserves the original name, storage, qualifiers, return
type, parameters, binding, and method identity. It may splice the old body,
replace it, or produce an empty body. A general Unit decorator can transform
a complete function or declaration, but a public target must retain the same
public binding and contract and cannot gain new public siblings. Private
targets and hygienically private siblings may be rewritten freely.
Decorator adjacency still collects the unchanged source target. A public
target’s captured source must participate in the decorator’s Match
replacement, and dropping it makes the match fail. Imported decorators work
without an extra prototype.
Macro values
A macro is also a compile-time value of type Macro, available to meta
functions. The same value builds code and recognizes it:
#include "meta.x"
macro Expression $sum(Expr $left, Expr $right) => $left + $right;
meta static List build_left_twice(List left, List right) {
Macro sum = $sum;
List result = sum(left, right);
Macro twice = macro Expression(Expr $value) => $value + $value;
match (result) {
case sum(?a, ?b): return twice(a);
}
return result;
}
macro Expression $left_twice(Expr $left, Expr $right) =>
$build_left_twice($left, $right);
int main(void) {
printf("%d\n", $left_twice(21, 99));
return 0;
}
42
$name written without an argument list selects the macro as a value.
Applying a Macro returns a pending invocation. The compiler expands it
where the result is bound, so a producer can return it as ordinary syntax.
A case on a Macro value recognizes both the pending invocation and its
expanded body, and it binds the same holes in either case. A named case such
as case $sum(?left, ?right): selects the macro directly. An anonymous
macro Kind(parameters) => body or macro Kind(parameters) { body } is a
value without a global name.
A quotation is an anonymous macro applied where it is written: $!( expression ) for an expression, $!{ items } for statements, and $!Kind{ ... } for
another category, such as $!Unit{ ... }. Each $name in its body refers to
the visible local name. Its first use declares a hole, and the hole’s
position sets its kind. The quotation applies to the locals’ values in that
order. @name is a sequence. A local declared Type, which lib/meta.x
declares as a List of type syntax such as %(double), fills a type hole, so
$!{ $type value = $initial; } declares value with the type the local
holds. Type syntax spells C type keywords and * as Symbols and every other
type name as a String, as in %(unsigned long), %("String"), and %(* "Point"). A type whose name is a Symbol, such as %(String), is an error
where it fills a type. A String local fills an expression hole as a string
literal and an int local as an integer literal. A $name that matches no
local is a macro invocation or a meta call, as in a template, so
@rows($items) splices a meta function’s result where the quotation
expands. A ${expression}, with { directly after $, is a hole whose value
is an expression of the function that contains the quotation. It means the
same as a local declared just before the quotation and named with $. The
function evaluates each such expression once, where the quotation is written,
in the order the holes are written, before the quotation builds its code.
Names in the braces resolve at the quotation; names the body declares are not
visible there. The expression’s declared type gives the hole its kind, as a
local’s type does: a Type fills a type, a String, int, or Symbol
inserts a literal, and a List inserts its syntax. @{expression} splices a
List. Each ${...} or @{...} is a separate hole and belongs to the
innermost quotation, so ${$!( ... )} inserts a quotation built first. Inside
a %(...), %[...], %{...}, or %"..." literal in the body, ${...}
keeps its literal meaning and inserts when the built code runs;
${${expression}} there inserts a value computed at the quotation. For a
template Array, %[${@{items}}] splices a sequence computed while the
quotation is built. It does not add runtime Array splicing. In a List,
@{expression} retains runtime splicing and does not evaluate during template
construction. Literal strings such as "$ @", percent strings such as %"\$ @" or %"$$ @", and quoted atom escapes such as %(\$ \@) preserve sigils
as data. After case, ${$name(...)} is still a macro pattern. These holes
exist only in quotations; in a named template’s code, ${ still reports a
missing macro name. See Quoting
code for an example.
A typed quotation is written $!T{ expression }, where T is one identifier
that names a type, or $!(T){ expression }, where T is any type, a Type
local, or a ${...} hole of type Type. An identifier after $! that spells
a macro kind in any case, such as Unit or Type, always names that kind, so
a type with that spelling uses the parenthesized form. A $!( ... ) group
followed directly by { is a typed quotation; otherwise it is an expression
quotation. A typed quotation builds (expr T CONTENT) where it is written. It
binds nothing, counts no expansion, and opens no transaction. Its holes take
the values a rebuild gives them:
- Syntax is used unchanged.
- A binding identity or
x2c.identspelling becomes an identifier expression. - An
x2c.identin a member position becomes its spelling. In a declarator, it becomes the name the declarator declares. - A number becomes a literal of its own type with its exact value.
- A String or Symbol becomes a literal.
Each number, String, or Symbol item of an expression sequence also
becomes its literal. A String is a String literal when the whole expression is
the hole and T is String; elsewhere it is a C string literal. Inner nodes
keep the placeholder type of unbound syntax. Free names bind where the code is
bound. A typed quotation may not declare a name that its own code spells or
apply a template, and inside another quotation it is written as a hole,
${$!T{ ... }}. The compiler accepts T as the expression’s type without
checking it. A typed quotation states the type the code has; it converts
nothing, so write a cast inside the braces when the code needs one. See Code
that knows its type.
$!Type{ T } builds the type T, written as a cast writes a type name.
$!Param{ P } builds one parameter declaration. A Type local or a
${...} hole of type Type may stand for a type or for the base a
declarator modifies. Both build where they are written: they bind nothing,
declare no name the braces spell, and apply no template. Type and Param
after $! always name these kinds. Decl names a hole kind only. See
Types and parameters.
When one Name hole appears in a declaration, a reference, and a member
selection, its case checks that the declaration and reference use the same
binding and that the member has the binding’s source spelling. This also
recognizes a local that another macro renamed for hygiene. A different
same-spelled binding or a different member does not match. The case captures
the original binding syntax.
A template can call a meta function in a slot and pass it a sequence hole
written with $name; the function receives the captured items as one List.
Applying a macro value with one List of syntax as the last argument passes
that List as the whole trailing sequence, so outer(head, items) and
outer(head, a, b) build the same code. A List of numbers or Strings, such
as %(1 2 3), passes the whole sequence too, and each item becomes its
literal. A case on a macro value captures what a spliced slot built. The
capture is bound to the slot’s sequence hole, with one element per statement.
Each element can then be recognized with a case on the macro that built it.
The pattern a literal macro value derives is prepared once per process.
A macro’s pattern is also a pattern wherever patterns go. In a case,
${$name(P, ...)} is the pattern of code $name builds with each argument
pattern in its hole, derived while the compiler reads the case. Each
argument is a binder or wildcard, another $name(...) pattern, or a
%(...) List pattern; a sequence or Type hole splices its List pattern’s
elements. The pattern matches the typed expression or its bare content.
Inside a %(...) pattern the same form is an element, and in the content
position of an (expr TYPE ...) shell it is the content alone, so the shell
captures the expression’s type:
#include "meta.x"
macro Expression $neg(Expr $value) => -$value;
macro Expression $add(Expr $left, Expr $right) => $left + $right;
meta static List classify(List code) {
match (code) {
case $add(?left, $neg(?right)): return $!int{ 1 };
case %(expr ?type ${$add(?left, ?right)}):
return $!int{ ${type.repr() == "(int)" ? 2 : 3} };
}
return $!int{ 0 };
}
macro Expression $kind(Expr $code) => $classify($code);
int main(void) {
int x = 1, y = 2;
double z = 1.5;
printf("%d %d %d %d\n", $kind(x + -y), $kind(x + y), $kind(z + y),
$kind(x * y));
return 0;
}
1 2 3 0
A named case whose arguments are all binders, such as case $add(?left, ?right):, keeps the recognition described above, including pending
invocations. One with a nested pattern among its arguments, such as
case $add(?left, $neg(?right)):, uses the derived pattern.
Hygiene and generated names
An expansion means what the same code would mean if it were written where
the expansion lands. There are two exceptions. The names its body declares
are private to that expansion. A name the macro lists with using keeps the
file-scope declaration where the macro is written.
Arguments are parsed and typed where the invocation is written, and the names
a caller writes in them also resolve where the expansion lands. A Name
argument is a spelling. A Name hole can declare a new caller-visible
variable or refer to an existing one. When the expansion declares the caller’s
Name, the caller’s other uses of that spelling refer to that declaration.
$repeat(k, 3, k * 2, total) below shows this. A name the body writes is
private, so it never supplies a caller’s name.
A declaration written in a body receives a fresh binding identity and a private generated C spelling for each expansion. The spelling of a file-scope declaration with external or no linkage also names its unit’s file name and the outermost invocation, so units that include one another can expand the same macro. A field keeps its spelling, because C scopes it to its aggregate, and an anonymous aggregate is a distinct type in each expansion.
A literal name that a body passes to a nested macro’s Name hole is also
the body’s own name. A declaration the nested macro makes from it belongs to
this expansion, and the body’s other references to that spelling reach it.
A reference that no such declaration reaches resolves where the expansion
lands. A local macro or an anonymous macro written inside a body shares the
body’s names the same way: its literal reference reaches a private
declaration of the enclosing body.
A name the body reads without declaring resolves where the expansion lands,
like any name written there: a caller’s declaration of that spelling
supplies it, including a local, and a local macro reads the locals around
it. A name that nothing declares anywhere is left to C, as a native macro
such as errno is. To read a caller’s binding of a name by design, take the
name as a Name argument.
macro Stmt $repeat(Name $i, Expr $count, Expr $value, Name $sum) {
for (int $i = 0; $i < $count; $i++) $sum += $value;
}
macro Stmt $print_doubles() {
int total = 0;
$repeat(k, 3, k * 2, total);
printf("%d\n", total);
}
macro Stmt $add_tens(Name $target) {
$repeat(k, 3, 10, $target);
}
int main(void) {
int k = 100;
$print_doubles();
$add_tens(k);
printf("%d\n", k);
return 0;
}
6
130
In $print_doubles, k * 2 reads the loop variable that $repeat declares
from the body’s literal k. In $add_tens, the caller’s k arrives through
$target; the loop variable comes from $add_tens’s own literal k, so the
assignment reaches the caller’s variable.
Keeping a file-scope name with using
A macro keeps a file-scope declaration only when it lists the name with
using. A braced body lists names as its first item, using name, other;.
Any macro may write the list after its signature instead, and an arrow body
must:
static int scale = 3;
macro Expression $scaled(Expr $value) using scale => $value * scale;
macro Expression $landed(Expr $value) => $value * scale;
int main(void) {
int scale = 10;
printf("%d %d %d\n", $scaled(2), $landed(2), scale);
return 0;
}
6 20 10
Each listed name is a function, object, or enumerator. A file-scope
declaration in the macro’s file or a header it includes supplies it. A listed
name may also be a native name, such as a C library function, that no x2c
declaration supplies. File scope is the whole file, so the declaration may
follow the macro. The expansion reads the declaration with its type. A
caller’s local of the same spelling receives a separate generated C
spelling, so the expansion still reaches the file-scope name. Only the
locals visible where the expansion lands are respelled; locals of other
scopes keep their C spellings for the native macros that name them. A listed
name resolves to that declaration in every unit that applies the macro,
including through a Macro value. A name with internal linkage, such as a
static function, is reachable only from its own unit, so a macro applied
in other units lists nonstatic names, or module names within a module
unit. Type names cannot be listed; a type name in a body resolves where the
expansion lands, in a declaration, a cast, sizeof, and _Alignof alike.
using name keeps a declaration; using $name (below) creates a fresh
private name; a lambda’s using &name shares a captured local by reference.
An anonymous macro keeps one kind of value from where it is written: a
Macro value that its body applies. Templates compose by value, so a later
application applies the same children.
A case on a macro recognizes a name the macro lists with using only by
its file-scope declaration. It recognizes any other free name by its
spelling, because the code the macro builds binds that name where it lands.
A struct, union, or enum tag that a body defines or declares is private
to each expansion, like a typedef, and so are the enumerators it lists. Tags
are a separate namespace, so struct tm tm; names the outside tag and a
private variable. Declarations in nested blocks may shadow same-spelled body
locals and tags; references use the nearest lexical declaration. A tag the
body only references keeps its outside meaning.
To publish a tag, spell it
through a Name hole or an $x2c_ident call:
#include "meta.x"
macro Unit $handler_type(name $tag) {
struct $tag { int depth; };
typedef struct $x2c_ident("Frame") { int line; } $x2c_ident("Frame");
}
$handler_type(Handler);
struct Handler handler = {1};
struct Frame frame = {2};
Most generated declarations need no directive: writing int temporary or a
destructuring Var (key, value) in a body makes both that declaration and its
literal references hygienic. When
compile-time Lisp needs a private name before an ordinary declaration can
introduce it, place using $name, $other; at the start of the braced body.
Each name is a compiler-allocated singular Name hole. Every occurrence
within one expansion receives the same binding, while different expansions
receive different bindings. A generated name may not duplicate an argument
hole or another generated name and takes no kind annotation or @ prefix. The
directives must precede all other body items.
A body marks an exact public spelling with a call such as
$x2c_ident("Frame"), which meta.x declares; compile-time Lisp spells it
$(x2c.ident "Frame"). A body that declares a public parameter writes the
call at the declaration and at each reference, as in
int $function(int *$x2c_ident("values")) with
$x2c_ident("values")[0] in its body. In a
declarator slot that spelling creates a declaration and is normally rejected if
it is already bound in the same target scope. The one completion case is a
function definition produced by a Unit macro after a same-scope prototype,
including one earlier in that expansion. Canonical return and parameter types,
qualifiers, linkage, and any method owner/member identity must match exactly.
The definition reuses that prototype’s binding once; a mismatch, a second
definition, or a collision with another binding kind is rejected at the
invocation. A nested scope may still shadow an outer declaration normally. In
an identifier expression the spelling resolves to the existing declaration,
with its type, and an unknown one is rejected. A bare Lisp String
does not become a name when returned into a name-capable position.
The compiler parses and types arguments before expanding a macro. It then matches syntax, evaluates compile-time Lisp, and binds the generated syntax. Generated declarations become visible to following source only if expansion succeeds; a failed expansion leaves no provisional symbols or bindings. Expansion is limited to 64 nested applications and 10,000 applications per translation unit; an identical recursive application is rejected immediately. Templates without nested applications or definitions do not count toward the unit limit if each hole occurs at most once. Statements authored by a macro are attributed to the invocation site; captured statements retain their original source locations.
Operators in a macro expansion use the same protocol dispatch as operators
written directly, including derived ==, !=, and relational operations.
Compile-time Lisp and imports
Use $helper(...) to execute an x2c meta function. The $(...) form enters
compile-time Lisp for Lisp interoperability, imports and session setup. At top
level its result is discarded. Inside a macro body, $name refers to the
hole’s exact AST, and the result is inserted according to the recorded body
position. A syntax List inserts syntax, a scalar becomes an expression
literal, a semantic Type List fills a type slot, and a syntax sequence
splices only at a sequence splice point. Strings are values, never source
text to be reparsed.
In a percent literal, ${$helper(...)} and ${$(...)} reach compile-time
code; see percent literals.
$(import "helpers.xlisp")
#include "project-macros.x"
macro Expression $computed(Expr $value) => $(car (list $value));
.xlisp files execute in the translation unit’s Lisp session. $(import ...)
is for Lisp files; ordinary .x modules containing macros and meta functions
use #include. Lisp import paths are relative to the importing file,
canonicalized, loaded once, and checked for cycles. Direct Lisp file operations
remain available but are not tracked as compiler dependencies.
A free name in a Lisp lambda or defmacro body reads the definitions the
body was written next to, and then the session’s globals. The environment the
call was written in is not part of that chain, so a caller that binds the same
name changes nothing about what the body reads. A lambda written inside a
binding form reads that form’s bindings and keeps the values it was made with.
A macro that needs a value from its caller takes it as an argument.
A let binding is not visible inside its own initializer. A lambda a let
binds therefore cannot call itself by that name, and the self-call reports
(unbound (name h)). Write such a helper as a defun.
(let ((h (lambda (n) (h n)))) (h 3)) error: (unbound (name h))
(defun h (n) (h n)) reads its own name
The session that translates a unit inherits the compile-time library and
cannot replace one of its definitions. def on an inherited name raises
(bad-state (operation "def") (reason "inherited") (name NAME)), which the
compiler reports as a failed compile-time evaluation at the defining form.
This covers defun and defmacro, which are def. The library defines
many ordinary words, including filter, last, map, search, len and
apply, so a macro file needs its own names for its own definitions. A
unit’s own definitions live in its own session, so one unit never changes
what another reads. Lisp definitions treat an inherited binding as final.
The host API Lisp.set_global can shadow an inherited name in the child’s
own bindings without changing the ancestor. Lisp def still rejects that
name, including after a host binding shadows it.
eval is an ordinary procedure. The form it is given is evaluated in the
session’s globals, not in the bindings around the call, so
(let ((z 7)) (eval (quote (add z 1)))) reports (unbound (name z)). Build
the form with the value in it when a local has to reach eval.
Compile-time Lisp is trusted code. It runs with the compiler user’s authority,
including the existing native bindings and file operations; there is no
sandbox. Lisp may also construct canonical AST Lists directly; see
macro-visible syntax.
The compiler supplies these contextual Lisp operations:
(Code.type syntax)
(Code.value literal)
(Code.source_text syntax)
(Code.binding_spelling syntax)
(Code.name function)
(Code.parameter function name)
(Code.body function)
(Code.arguments parameters)
(Type.fields type)
(Type.layout type)
(Type.resolve type)
(Type.is_value type)
(Type.parts type)
(Type.parameters type)
(Type.return_type type)
(Type.element type)
(Type.members type)
(Type.resolve_member type name call)
(Type.tag_name name)
(Type.reverse_name base participant)
(x2c.ident spelling)
(x2c.binding.fresh role)
(x2c.binding.for key role)
(x2c.literal.string value)
(x2c.literal.int value)
(x2c.literal.symbol value)
(x2c.embed.text path)
(x2c.invocation.file)
(x2c.invocation.line)
(x2c.invocation.column)
(x2c.enclosing what)
(x2c.place where code)
(x2c.diagnostic.fail message notes)
(x2c.diagnostic.fail.at node category message notes)
(x2c.diagnostic.warn message notes)
An operation on captured code or a semantic type is a method of Code or
Type, spelled Code.member or Type.member. Any other supported operation
is spelled x2c.<noun>.<verb>. A name beginning _x2c. is a compiler
internal with no compatibility promise. Every Code and Type method in
lib/meta.x has this Lisp form, including those not listed here.
Code.type returns the canonical semantic Type for supported typed
syntax. During expansion, syntax a template left untyped is resolved where the
expansion stands, so a template’s locals, casts, and method results report
their declared types.
Code.binding_spelling accepts a name hole’s valid identifier String or
compiler-issued identifier and binding syntax, and returns its source spelling
without exposing the numeric identity. Unknown, malformed, and forged binding
identities are rejected.
Code.source_text returns the exact source spelling of one complete captured
macro argument or decorator target, including its interior whitespace,
comments, parentheses, newlines, and literal escapes. Imported macros still
read the caller’s source, and syntax forwarded through another macro retains
where it was written. Constructed syntax, derived subtrees, name values, and
calls outside expansion are rejected. Standard Lisp repr renders a canonical
AST rather than its source spelling.
x2c.diagnostic.fail reports a macro diagnostic at its source location with
String notes and does not return.
x2c.ident returns the tagged name value described above and accepts a
String that is a valid identifier spelling.
x2c.binding.fresh returns a new binding whose C spelling is built from the
identifier role, as for the compiler’s own generated declarations. Code that
the macro places can declare the binding, and its result can refer to it.
x2c.binding.for returns the binding the unit gave key, or the first time a
new one named from role, so code placed once under (unit-support KEY) and
each result that refers to it name the same binding.
x2c.invocation.file, .line, and .column return the caller’s x2c source
location while a macro body is expanding. Lines and columns are one-based. For
an included macro, they still give the caller’s location. For example,
$(x2c.invocation.line) produces an integer expression for the invocation
line.
Code.name, Code.parameter, and Code.body inspect a decorator’s captured
function. name returns its free-function or dotted method name as written
in source, parameter resolves a named parameter to bound identifier syntax,
and body returns the block-item sequence for an explicit @ splice. Use
Code.type on a resolved parameter when its canonical Type is needed.
Code.arguments accepts either a (params ...) node or a List of
parameter nodes and returns their bound identifier expressions. These
operations do not construct or mutate functions.
Type.resolve_member performs ordinary member lookup for a Type and
identifier String, as . does. It returns (field ACCESS TYPE) for a
field, (method BINDING SIGNATURE) for a method when call is nonzero,
(ambiguous PACKAGE...) when imported packages each provide the method, or
nil. A method’s callee expression is (expr SIGNATURE (ident BINDING));
callers supply arguments according to its function Type. It does not
search delegate fields.
Type.fields resolves a typedef or qualified Type to a complete struct
or union and returns its named fields in source order as (("name" DECLARED_TYPE) ...). Unnamed fields are omitted; incomplete and non-aggregate
Types are rejected. Each declared Type retains pointer, array, qualifier,
and bitfield modifiers.
Type.resolve follows the ordinary typedef chain and returns its canonical
type representation. Type.layout returns the ordered field records of
that representation, including unnamed members and padding. Each record is
(NAME DECLARED_TYPE); an unnamed field has an empty name. Use fields for
named-member access and layout when every declared field affects a decision.
Type.parts separates a semantic type into its declaration base and
declarator modifiers as (BASE MODIFIERS). Type.parameters and
Type.return_type split a function type into its parameter types and its
result. Type.element returns a pointer or array element type, and
Type.is_integral and Type.is_pointer, from lib/type.x, classify a
type without resolving it.
Type.protocol_member returns the protocol callable selected for member
name of a type, or nil when none is available in the current function.
Code.value returns a captured constant’s value: for a literal, a String,
a number, or a Symbol, according to the literal. It is the inverse of
x2c.literal.string, which builds a literal from a value. A literal whose text
the parser has already consumed, such as a multi-line interpolated %"...", is
rejected; read its spelling with Code.source_text instead.
Type.members returns an enum’s members in source order as (("NAME" VALUE) ...). VALUE is one of these:
nilwhen the member takes its position’s value;- the literal’s exact spelling as a
Stringwhen the initializer is one literal, including its base, suffix, and character quotes; - otherwise the initializer’s expression node, which a generator can splice or walk.
Test it with string? when both kinds can occur. A non-enum Type is
rejected.
The Lisp operation x2c.diagnostic.warn reports a macro warning at the
invocation with String notes and returns nil, so expansion continues.
x2c.enclosing answers the innermost syntax of one kind around the
invocation, or nil when there is none. <declarator> is the initialized
declarator whose initializer directly holds the invocation, answered as its
one-declarator declaration (declare BASE (bindings (bind NAME MODIFIERS)));
BASE keeps the storage class, and Code.type reads its declared
type. A block item begun inside that initializer, as in a statement
expression, hides the declarator. <statement> is the enclosing block item,
answered as its origin anchor (at N (seq)), which x2c.diagnostic.fail.at
reports at. <function> is the enclosing function definition with the empty
body (seq), which Code.name, Code.parameter, and Code.body read. <unit> is a
String expression holding the unit’s path. Another kind is rejected.
x2c.place contributes code beyond the expansion’s result and returns nil.
It applies under the expansion’s semantic transaction, as the effects of a
code-value carrier do. Placed code that declares a name the result uses
takes it from x2c.binding.fresh. WHERE is one of these Lists:
(after-statement)places code after the initialized declarator thatx2c.enclosinganswers, or otherwise after the enclosing block item. A declaration split after the declarator keeps the declarators before it in their own declaration, its shared inline type, and initialization order. The placing expansion’s result must be that declarator’s complete initializer, through parentheses and typed expression shells. Otherwise, or when the declarator is not an item of a block, as in aforheader, the compiler reportsmanaged initializer requires a complete block-local initializerat the block item. Code placed outside any block item is rejected.(block-exit)runs code on every transfer that leaves the enclosing block after that declarator or block item, asdefer CODEplaced there with(after-statement)runs, under the same rules.(function-entry)places code at the entry of the enclosing function definition, after its parameters and before its first statement, in placement order. It applies to a macro, which runs while the parser reads the body, and to a rewrite rule, which runs while the body lowers. Code a macro places from a lambda body enters the function that holds the lambda; code a rule places there enters the lambda’s own function. Code placed outside any function is rejected.(unit-support)adds declarations among the unit’s file-scope support code;(unit-support KEY)adds them only for the first placement underKEY. Such code takes the names it shares with each result fromx2c.binding.for.(unit-init)adds statements to thefinisharea of the unit’s initialization, and(unit-init AREA)to theprotocol,prepare,statics, orfinisharea, which run in that order.
Any other WHERE is rejected as an unknown placement.
Type.is_value recognizes numeric scalars and enums, Symbol, Var, Atom,
String, List, and their typedef aliases. Pointer-shaped runtime handles such
as Array are not classified as values by this operation. Type.tag_name
returns a round-tripping compact Symbol from the full type-name String and
the current owning source file, relative to the compiler root when applicable.
This keeps private types with the same spelling in different files distinct.
Descriptor registration still checks collisions and capacity; a compact tag
does not establish type equality.
Type.reverse_name returns the conventional reverse-converter spelling
for base and participant name Strings, using the protocol registry’s package
naming rules. For example, base "Var" and a registered package participant
"geometry__Point" produce "geometry__Var_point".
Lisp has no quotation, so the Lisp library builds the literal a $(...) form
returns for a value. x2c.literal.string turns a compile-time String into a
runtime String literal expression, and x2c.literal.int and
x2c.literal.symbol do the same for a number and a Symbol. Other code a
Lisp form returns is a quasiquoted List.
x2c.embed.text reads a regular text file exactly, returns its contents as a
compile-time String, and records its canonical path as a translation
dependency. It accepts either a Lisp String path, resolved relative to the
file containing the Lisp form, or complete captured String-literal syntax,
resolved relative to the caller file where the literal was written. Absolute
paths remain absolute. A zero-byte file is valid. Invalid paths, non-regular
files, read failures, embedded NUL bytes, and String size overflow are
diagnosed. Runtime embedding remains explicit through x2c.literal.string.
Names under x2c.* or _x2c.* with a component beginning _ are private
implementation details.
Sharing compile-time definitions
Keep runtime code and compile-time definitions in the same ordinary source module:
#include "geometry.x"
Nonstatic macros, decorators, keyword aliases, and meta functions become available at the include line. They also reach files that include this one. Each canonical include path contributes once per translation unit, including when an include cycle reaches a file again.
Public inline bodies can use ordinary function signatures declared later in an include cycle. If a declaration producer creates a required signature, place that production before the include that needs it. Looking ahead does not execute later Lisp forms or declaration producers.
A definition marked static stays in its declaring source file. For example,
static macro Expression $helper(...) declares a private macro, and
meta static int helper(int value) declares a private meta function.
Nonstatic top-level Lisp definitions and Lisp imports also follow the
ordinary include interface; a static $(...) form stays in its file.
Definition order determines which macro or alias later source uses. Macro bodies retain their definition-time syntax; free runtime names follow the expansion-site rules under macro hygiene.
meta functions
A function definition may be marked meta. The marker precedes the whole
declaration, ahead of any storage class:
meta int poly(int n) => n * n + 3 * n + 1;
meta static String label(String stem, int n) => %"$stem-$n";
int main(void) {
printf("%d %s\n", $poly(7), $label("item", 2));
return 0;
}
71 item-2
meta is contextual. It marks a function definition, a bodyless function
prototype, a native function definition, an initialized file-static value, or
a protocol adoption.
Everywhere else it is an ordinary identifier, including as a file-scope name,
an assignment target, and a struct field. Types need no marker: compile-time
code can use any type the compiler sees.
A bodyless meta prototype declares a native function that the compiler
itself links, such as meta double sin(double);. Compile-time code then
calls the compiler’s copy. The declared signature must match that function
exactly. A function the compiler does not link can come from a loaded
native module. A prototype for
a function that neither supplies is accepted, and compile-time code that
calls it is diagnosed. meta native before a function definition declares
that function native in the same way, and its body is compiled only for the
program. See
native C functions.
A meta protocol adoption, such as meta protocol Iter(List);, adopts the
protocol and declares each of that conformance’s witnesses the way a bodyless
meta prototype would. Availability stays explicit for each conformance: a
protocol body cannot be marked, and a witness the compiler does not link is
not available.
Marking a function meta has three consequences.
- The function joins the project meta module. Before translation, the
compiler emits the bodied
metafunctions the inputs reach through the ordinary backend, compiles them with the host C compiler (--meta-cc), and links them into one cached helper program. Each$call runs in that helper; see How a meta function runs. It is callable during translation by$name(args)from an x2c body, and Lisp code can call it with$(name args). Inside ametabody,$f(x)is an ordinary call off, because the whole body runs at compile time. - The function is also emitted as C and behaves as an ordinary function at run time, unless it reaches a compiler query or a source-template constructor, directly or through another meta function. Such a function has no runtime form and none is emitted for it.
- A call without
$always calls the emitted function, even with constant arguments.
A dollar-prefixed function call requires compile-time evaluation. Each
argument is a constant, arithmetic over constants, captured macro syntax, or
another dollar call; any other expression, such as a program variable, is
diagnosed. Visible macros retain precedence over meta functions with the same
name. In a macro body, a captured hole passed directly to a $ call passes
its code. The runtime expression is not evaluated.
A directly forwarded hole retains its declared capture kind, including
Function, and complete captures retain their source text and path context.
Inside a macro template, an explicit call can supply a type, generated name
or syntax sequence as well as an expression; the ordinary slot binder checks
the returned value for that position.
Inside meta bodies, Unit and Stmt source macros used in expression positions construct deferred invocation Lists from computed arguments. Normal binding expands them when inserted into a program. Existing Expression macros still expand normally. See source templates.
A meta body is ordinary x2c compiled as C, so every construct the unit
can compile runs at compile time with its C meaning; see
C objects during compilation.
A body that lets the address of its own storage outlive the call is
rejected where the address leaves.
Meta functions may pass and return represented values such as wide integers,
floating values, Strings and collections during compile-time execution.
Explicit $name(args) insertion, and the optional $(name args) Lisp form,
accept numeric values with their native Var family, computed strings as C
string literals, Symbols, identifiers
and nonempty expression-code Lists. Arrays and Maps are reconstructed as
fresh runtime collections, including nested collections; cycles and shared
mutable collections are rejected. Callable values and compiler addresses
cannot be materialized directly. A parameter or result
with no Var form, such as a struct, a C bool or a pointer, cannot cross a
dollar call. This boundary does not limit internal
returns to int; see
results.
Compile-time objects belong to the compiler. A file-scope variable that a
meta body reads is the compiled group’s own instance, initialized from the
same source initializer; it is never the program’s variable. A meta static
value lives in its provider’s compiled group, and the helper re-initializes
it for each translation unit, as described below.
The project helper runs mutable meta static initializers when a compile-time
call first reaches their provider in each translation unit. Including an
unused provider does not run those initializers.
A meta body runs as native code, with these consequences:
- A
metafunction defined by a macro expansion or a$call is an error; one written in an ordinary.xfile joins the project meta module. - A project
metabody receives compiler facts as arguments and can query the compiler while its helper call waits. - A raise in a body is a diagnostic at the
$call. A crash, exit, or stack overflow ends the helper and is reported at the call; the compiler continues. The Lisp step and depth budgets do not apply to a native body; a call longer thanX2C_META_TIMEOUTseconds (60 by default) is stopped instead.
A nonstatic meta function in an included .x module is callable during
translation and has an ordinary runtime declaration. Its provider source
contains the runtime definition. Repeated includes contribute the public
declaration once.
See Meta Functions for the authoring guide.
The same operations from x2c
Every public operation above is also declared in x2c, in the optional module
lib/meta.x, so a meta function that includes it calls the compiler
directly and a macro’s implementation does not have to be written in Lisp. The
declarations are the signatures; the semantics are the ones described above.
A project meta function’s queries, such as Code.type and Type.fields,
ask the compiler while the call waits; see
What the compiler answers.
The compiler binds each operation under its x2c name and derives the Lisp
name from it. A method keeps its spelling: Code.type and Type.is_value
are the same names in Lisp, and a predicate method answers int, 1 or 0, in
both. In an x2c_ helper each _ becomes .: x2c_invocation_line is
x2c.invocation.line, and x2c_ident is x2c.ident. Where the Lisp library
defines the derived name itself, that definition stands. x2c code builds
literals with typed quotations, so x2c.literal.string, x2c.literal.int,
and x2c.literal.symbol have no x2c declaration.
The operations are bodyless meta prototypes supplied by the compiler
itself, which generates its table of them from those declarations.
x2c_meta_definition_hashes() returns a Map with one entry for each function
or initialized file-static value the unit has defined so far. The key is the
definition’s name. The value is a String hash of the definition’s code tokens,
from the first token after any meta marker to the end of its body or
initializer. The compiler carries native copies of the meta functions in the
source modules it ships (src/linked-meta.x, generated from them) and records
their hashes with it. An included meta definition runs as its native copy
when its hash matches the copy’s. The hash of every unit definition it reaches
through references must match too. An edited definition, or one that reaches an
edited definition, is compiled with the unit’s group like any other.
A meta function that makes an explicit $ call or uses a source-template
constructor is also compile-time-only. A run-time call to a compile-time-only
function is diagnosed where it is written. The diagnostic gives the function
and the reason.
Compiler facilities load before author imports, and macro code cannot redefine them.
Standard Lisp exposes the List matcher of Pattern
Matching as six names. match tests a whole subject. It
returns association-list bindings, nil on failure, and truthy (()) for a
match with no named binders. A pattern describes a List shape, so a subject
that is not a List fails without raising. bound reads one binder out of
that alist. search finds the first matching subtree. match-replace and
search-replace rewrite the whole subject or the first matching subtree from a
template. match-case tries each clause’s pattern in order and evaluates the
first body whose pattern matched, with that pattern’s binders in scope as
variables; a final (else body) clause runs when nothing matched.
(match-case form
((add ?a ?b) (+ ?a ?b))
((neg ?a) (- 0 ?a))
(else 0))
A clause’s pattern is quoted implicitly and written as List syntax, with the
same ?binder and *binder spellings. A binder keeps its ? where the body
reads it. Without an else clause an exhausted match-case is nil.
Compile-time Lisp also reaches the core value types directly. A Lisp value in
a session is an x2c Var: a Lisp list is a List, a Lisp string is a
String, and a quoted name is a Symbol. The operations below are the
library’s own, so a macro and the program it generates compute with the same
List, String, Map, and Array.
List.getindex List.last List.index List.contains
List.get List.assoc List.array List.sort
Array.new Array.len Array.push Array.getindex Array.setindex
Array.take_last Array.shift Array.unshift Array.insert Array.remove
Array.find Array.contains Array.count Array.join Array.list
Map.new Map.len Map.get Map.getindex Map.setindex
Map.contains Map.del Map.getdefault Map.setdefault Map.list
String.len String.find String.rfind String.count String.contains
String.startswith String.endswith String.getindex String.getslice
String.add String.lower String.upper String.capitalize
String.repeat String.replace String.join String.split String.split_lines
String.partition String.rpartition String.find_all
String.remove_prefix String.remove_suffix String.escape String.unescape
Var.tag Var.kind Var.is Var.parse Var.convert
Var.integer Var.floating Var.is_null
Symbol.len Symbol.str Symbol.compare
Each name takes the arguments its library entry
documents and behaves identically. Var.parse reads a String as an int,
double, string, symbol, or char, and Var.convert changes a value’s
numeric tag, which is how compile-time code narrows a double to an integer.
Map.list returns a map’s entries as (key value) pairs, because a Map
cursor has no Lisp representation.
The Lisp core keeps its own spellings for cons-cell and text work. car,
cdr, cons, length, reverse, string-append, substring, lower,
str, and repr are unchanged and reach the same code. Note that Lisp’s
assoc returns the matching pair while List.assoc returns its second
value, as the library documents.
An operation whose parameters or result have no Var representation is not
available, which excludes Iter cursors, pointer out-parameters, variadic
entry points, and char * parameters. A program embedding its own session
loads the same names from etc/lisp-values.xlisp.
Macro-visible syntax
Lisp sees the canonical parsed, typed List AST used by --dump-ast, with
source (at ID NODE) wrappers removed. Tags and semantic Types remain
Lists. Binding records are visible so syntax can be preserved and compared,
but their numeric IDs are opaque and must not be forged. Use
Code.binding_spelling when text is required.
The compiler records where syntax was written separately from its AST value.
Code.source_text reads it, as described above. Diagnostics retain the
definition, invocation, import, and generated ancestry even after a macro
returns a new List. Returned syntax must be valid for its expression, field,
enumerator, block-item, or translation-unit position. A meta call written as
a whole expression statement holds that statement’s block-item or statement
position, so it may return a statement as well as an expression. This holds in
an ordinary function and in an expansion alike, such as the body of macro Stmt $same(Stmt $code) => $same_of($code);. A meta call inside a larger
expression holds an expression position. Inside an expansion every List
result is code. Elsewhere a List result is code when it is an expression
node, a macro application, a quotation, or an identifier; any other List
remains a runtime value. Protocol declarations, macro definitions,
preprocessor nodes, and other compile-time-only source items are retained at
translation-unit position and apply their source-order effect. The compiler
rejects forms that are malformed or invalid in that position. Lisp may
construct canonical AST Lists directly, including static declarations. The
compiler accepts a valid structure whether it came from the parser, a Match
capture, a template, or handwritten Lisp. It does not verify every type or
binding annotation in a constructed List or reject it because of its origin.
Like C pointer operations, this metaprogramming is unchecked. Prefer literal
templates and the contextual x2c.* operations when you want the compiler to
construct syntax for you.
Named types and declaration production
NamedType captures NAME TYPE; or the forward form NAME;. The name comes
first, with no equals sign. { FIELDS } abbreviates a value struct; { FIELDS } * declares a pointer representation, with struct optional before the
braces. Qualifiers, fields, arrays, and pointers use the ordinary type and
declarator grammar. The name is reserved before its fields are parsed, and the
complete definition supplies its representation. A forward declaration does
not imply a pointer. Layout must be complete wherever the ordinary type rules
require it.
The captured form is (named-type NAME TYPE), where NAME is a String and TYPE
is the complete canonical type syntax; a forward uses an empty TYPE. This form
is constructible by any macro or Lisp producer. Binding it publishes the same
ordinary typedef and aggregate declarations as the captured source.
A Declaration macro, or a NamedType decorator, produces declarations once
while the owning source’s public declarations are collected. Its result is
retained for full binding; the producer is not evaluated again to obtain its
bodies. Nested declaration producers share this rule. Static publication and
ordinary dependency invalidation apply to the retained result.
Imported and cached declarations publish the selected signatures of their
owning source.
The canonical container is (declaration-bundle (rows ITEM ...)). Rows may
include ordinary top-level syntax and these constructible forms:
(default FUNCTION)supplies a function candidate. An ordinary declaration of that exact function in the owning source wins, including a later one. Only the selected body is bound. A candidate produced by a static type declaration has static linkage. Two ordinary definitions still conflict.(declaration-recipe CALLBACK ARGUMENTS)defers a Lisp producer until the owning source’s declarations are available. It is evaluated once, and its declarations join the same bundle.(default-forward CHILD PARENT MEMBER FALLBACK)selects an ordinary parent method after signatures are collected and supplies a child forwarding method. FALLBACK is an optional function candidate when no parent applies.(syntax-recipe CALLBACK ARGUMENTS)supplies syntax when a retained body is bound, allowing it to use the selected method signatures.
Retained bodies also carry the usual macro bindings and source locations for diagnostics. A consumer cannot replace a provider’s exported default by defining another function with its name.
Class declarations
The shipped class keyword aliases the $class NamedType decorator. See
Classes and System Macros
for the complete default-method inventory, construction, and lifetime examples.
A class preserves its explicit representation and ordinary
typedef ancestry. It adds replaceable methods through declaration defaults.
An explicit new suppresses its generated constructor and init requirement.
Derived classes forward the nearest applicable constructor; variadic forwarding
requires an explicit constructor.
An aggregate class gets a generated new when it defines one of these
initializers:
void T.init(T &, ...)for a value, which also accepts the existingT *receiver;void T.init(T, ...)orint T.init(T, ...)for a heap pointer.
That new takes the parameters after the receiver. It calls init on
zero-initialized storage, which a heap class obtains from its replaceable
T.alloc. A zero result from an int initializer releases that storage and
makes new return NULL. Otherwise flat value fields produce positional
constructors in declaration order, with unnamed bitfield padding omitted, and
non-flat or resource-containing aggregates require init. Heap defaults
allocate through Scope and provide early free, which accepts NULL and calls
void T.drop(T) first when the class defines it; they do not otherwise own
fields. Scalar and derived aliases retain their ordinary Var representation.
Heap classes box identity; aggregate values box a Scope-owned copy and require
compatible equal/hash operations, generated for supported value fields.
str and repr are independently replaceable. Aggregate value str delegates
to repr; heap str prints identity. Generated repr traverses printable fields
and uses addresses for opaque pointers and fixed arrays. Repeated identities
on the active rendering path print their pointer form. Descriptor registration retains the
runtime’s worker-start freeze rule. Overflow descriptors extend the compact
descriptor table’s capacity.
Managed initializers
$auto(value) is an ordinary expression macro. Its expansion asks
x2c.enclosing for the initialized declarator, places a release after that
declarator with x2c.place, and returns value unchanged. Forwarding through
another macro or parentheses keeps that result the complete initializer. When
the declaration finishes, the compiler checks that the result is the
declarator’s complete initializer. Then it binds the release. The release
requires automatic storage and Cleanup participation. It saves the initialized
value in a private const local and defers that saved value’s cleanup call.
The declaration keeps its type, binding identity, initialization conversions,
and enclosing scope.
The declared type must participate in Cleanup(T), whose member is
void T.cleanup(T), directly or through its ordinary typedef ancestry.
Initializers run once in source order. Each successful initializer registers
its cleanup before the following initializer runs, including within a compound
declaration. Cleanup releases the acquired value even if the declared binding
is reassigned. Reassignment does not release that value early, and returning
or storing an alias does not cancel cleanup. An argument that is itself an
$auto form, or a plain name of a local that an earlier $auto manages, is
rejected, since the value would be released twice. Use an explicit defer when
cleanup must instead read the binding’s value at exit, such as a lazily filled
Scope slot or a staging buffer cleared after ownership transfer.
Static, external, or threaded storage, field initializers, for-header declarations, assignments, returns, call arguments, and forms nested inside operators are outside this enclosing-block position. There is no runtime expression helper or expression-exit cleanup.
Checked foreign aliases
The reserved target macro $x2c.foreign.alias binds one top-level X2C
function name directly to an ABI-compatible C function:
$x2c.foreign.alias(fclose)
inline int File.close(File file);
The target must be one fixed-arity function declaration without a body or
initializer, and the argument must be one direct C identifier. Static aliases
remain private; non-static aliases are published in the generated header like
any other declaration. The generated C uses _Static_assert and _Generic to
check the exact function-pointer type before defining the lowered X2C name as
the native name. Calls and address-taking therefore use the native function
itself; no wrapper object, thunk, argument mapping, or initializer is emitted.
Variadic aliases are rejected.
Typed callback adapters
The reserved expression macro $x2c.callback.adapt adapts one direct function
to an explicitly named callback typedef:
typedef String (*StringCallback)(Var);
static StringCallback array_string =
$x2c.callback.adapt(StringCallback, Array.str);
The target must name a fixed, non-variadic function-pointer typedef. The
source must be a direct free-function or Type.method designator, not a
function-pointer variable, lambda, bound receiver, or conditional expression.
Target and source must have the same arity and exact non-void return type.
Parameters may be exact, or a target Var parameter may be extracted to the
pointer/typedef owner or Symbol required by the source. The adapter performs
no numeric or general coercion and does not insert, drop, reorder, or default
arguments.
The compiler emits one translation-unit-local, statically typed thunk for each source/target pair and reuses it within that translation unit. The thunk has a prototype before any file-scope initializer that references it, and no incompatible function-pointer cast is emitted. The expression therefore suits private descriptor tables and other fixed C callback positions.
Values and literals
The tagged value model specified here is introduced gradually in values and Var.
Array and Map literals
In operand position, a bare [ begins an Array literal. Its
comma-separated elements are ordinary x2c expressions, each converted to
Var, and a trailing comma is accepted:
int n = 4;
Array values = [1, n * 10, "text", <sym>, [n]];
A { whose first entry is a Map entry begins a Map literal. An entry is
KEY: VALUE, or an Entry macro invocation. A key that is a bare identifier
is an Atom, with the same spelling rules as a bare collection Atom. Every
other key, and every value, is an ordinary x2c expression. Parenthesize an
expression key that is a single identifier:
int n = 4;
String name = "ada";
Map ages = {ada: 36, grace: n + 41, "text": 1, <sym>: 2, (name): 3};
A Map entry is identified by its first : that is outside nested brackets
and is not part of a ?: conditional. A brace without such an entry remains a
C initializer. Inside an initializer, a bracketed index followed by
=, ., or [ remains a designator; any other bracket is an Array
literal. Both literals build a fresh object at each evaluation, as %[] and
%{} do, and a declared typed Array or Map family builds its own
representation. Nested [...] and {...} are evaluated literals of the same
kinds; nested List data is written %(...).
Elements, keys, and values are evaluated once each, left to right in source order, as in the percent forms below.
[] is a fresh empty Array. The empty brace {} is a fresh empty Map when
its destination is a Map, a Var, an alias of either, or a type that
converts from Map. The same rule applies to Array. For any other
destination, {} is the native zero initializer. An element or value of a bare
[...] or {...} literal is the exception: there {} is an empty Map, so
[{}] holds one Map. Outside a declaration initializer, a brace that remains
an initializer becomes a compound literal of its destination, as an argument, a
return value, an assignment, or a ?: arm: Map m = ready ? {} : NULL;
builds a Map, Var v = ready ? {} : NULL; holds a Map when ready, and
return {3, 4}; in a function returning a struct builds that struct. An
anonymous struct or union destination has no compound-literal spelling and is
rejected.
The percent forms %[...] and %{...} below keep their quoted grammar. The
bare forms differ only in evaluating elements, values, and non-identifier
keys.
Percent literals, quote, and unquote
In operand position, % followed by a literal delimiter is the quoting sigil.
The delimiter selects the literal grammar for the contents. Here, quoting
means a change of parser context. Some percent forms evaluate their contents.
Binary % remains the modulo operator, and %! remains the lambda-literal
prefix.
%", %[, and %<< open literals after any token. Modulo by a string literal
is invalid C, and neither [ nor << begins an expression, so a cast may
precede these literals: return (Path) %"$base/$name"; quotes. A cast may
precede a bare [...] Array literal as well: (Var) [1, 2].
In %(, %{, and %! after an operand, including a closing parenthesis or
brace, the % is the modulo operator, except where a statement begins:
- after the parenthesized condition of
if,while,for, orswitch, soif (dirty) %(git stash).job().run();quotes; - after the
}of a compound statement or declaration body, so a%(...)statement may follow the closing brace of aforeachbody.
A brace counts as a body when the token before its { is one of these:
;,:,{,},], or an identifier;else,do,try,finally, ordefer;- a
)that closes a control condition, amatchsubject, or a parenthesized list after an identifier, as in a function header,foreach,with, or a decorator.
A compound literal or initializer brace follows an operator or a cast, so %
after it stays modulo: (int){9} %(4) is 1.
The tokenizer has no type information, so it cannot distinguish a cast from a
parenthesized operand. After any other ), %(, %{, and %! are modulo,
as in (a) %(b). Parenthesize such a literal after a cast:
(List) (%(echo done)).
The collection and string literal forms are:
| Form | Static type | Element syntax |
|---|---|---|
%(a b c) | List | quoted values separated by whitespace |
%<<a b c>> | SymbolSet | literal compact Symbols in dense order |
%[a, b, c] | Array | quoted values separated by commas |
%{a: b, c: d} | Map | quoted key/value pairs |
%"text" | String | quoted text plus unquote |
The static type in that table is the default. The declared target may instead
be one of the optional typed families: a packed Array from typed-array.x, a
packed Map from typed-map.x, or a typed cons chain from typed-list.x. The
literal then builds that representation directly, and an element that does not
convert raises <no-convert>. A null source stays null.
The empty forms are %(), %<<>>, %[], %{}, and %"". Each %[] or
%{} evaluation creates a fresh allocated object that can be mutated
immediately; null is not an empty Array or Map.
Inside a List, Array, or Map, a bare spelling is a case-sensitive Atom.
It is a compact Symbol when Symbol encoding reproduces the spelling
exactly; otherwise it uses <lsym>. Consequently, %(name), %[name], and
the key in %{name: 1} hold the same value as an explicit <name>, and the
angles are redundant. Use angles when bare collection syntax would read the
text as another value, as in <1> for the Symbol 1 rather than the integer
1, or when compact Symbol representation is required. The empty Symbol
must likewise be written <""> because a bare Atom cannot be empty. An
angle spelling that does not fit is rejected instead of falling back to
<lsym>.
Integer, floating, signed numeric, and character literals retain their
concrete types. A signed integer token is one value, not C’s unary minus: it
selects the first signed type among its suffix’s choices that holds the
signed value, in any radix, so %(-0x80000001) is the long value
-2147483649 and %(-2147483648) is an int. A u suffix keeps the unsigned
type C would choose and its modular negation, so %(-1u) is the unsigned
value 4294967295.
$name inserts one identifier expression into a quoted collection.
${expression} inserts one arbitrary x2c expression, including calls, member
and index expressions, operators, casts, compound literals, NULL, enum
constants, and comma expressions. @name and @{expression} splice a List
into a surrounding List; Arrays and Maps have no splice form. The inserted
and spliced expressions of one %(), %[], %{}, or %"" literal are
evaluated once each, left to right in source order. The arguments of an
ordinary function call keep C’s unspecified order. The identifier forms are
the short versions of the braced expression forms. The
collections chapter walks through building and
using each one.
For Lists that will be evaluated as Lisp, ', `, ,, and ,@ are the
short forms of quote, quasiquote, unquote, and unquote-splicing. Each
wraps the one element that follows it, wherever it appears, so %(...) and
the Lisp reader construct the same List data from the same text. Reader
punctuation also ends a bare Atom: %(a,b) is a followed by
(unquote b). These spellings do not replace x2c’s $ and @: those still
insert or splice x2c values while the List itself is being constructed.
A @ followed by whitespace, ), or = is ordinary @ or @=
atom data. %(op @ a b) remains a List of atoms; it does not authorize
matrix operator syntax.
%<<...>> is an immutable ordered SymbolSet. Bare entries are compact
Symbols rather than Atoms, and their source positions are their numeric
indexes. Entries must be literal spellings; interpolation, splicing, and
runtime expressions are not accepted. Every entry must survive compact Symbol
encoding exactly. Duplicate encoded Symbols are a
compile-time error. SymbolSet.index returns the source-order index or -1,
contains tests membership, getindex performs the reverse mapping, and the
Iter protocol traverses members in source order. The compiler emits the
perfect hash and ordered membership table as static bytes, with no runtime
construction or allocation.
Inside %(), %[], %{}, and %"", $ is the unquoting sigil. It leaves
the selected literal grammar, parses and evaluates one x2c expression, inserts
the converted result, and returns to the literal grammar. In %(), @ makes
the same crossing but splices the evaluated List’s elements. Collection
insertion converts to the representation required by the literal; String
insertion accepts String, Symbol, declared converters, Var, aliases of
Var, and supported numeric values under the existing conversion rules. The
shipped component src/components/interpolation.x joins a %""
literal’s parts, a translator that $rewrite(%(expr ("String") (segments *)))
registers on the literal’s segments head. A program’s rule for String.join
receives only the calls that the program writes, including one inside a $
insertion. It does not receive the call that the shipped rule writes.
The braces are part of the unquote and contain one complete x2c expression,
including nested calls, indexing, casts, assignments, conditionals, comma
expressions, and nested literals. In a collection, $name(...) instead inserts
$name and then reads (...) as a nested symbolic List. In %"", the
parentheses after $name are text. A runtime call in any quoted form is
therefore ${name(...)}. In %"" and in a quoted "..." element, $$ and
\$ each produce one literal $: %"a$$" is a$.
Nested (...), [...], and {...} remain quoted List, Array, and Map
data, and nested "..." selects the x2c String grammar with its interpolation
and escape rules. None repeats the % sigil, because a bare % inside quoted
collection data is an Atom: %(k %"txt") reads as the three elements k,
%, and "txt". ${"text"} instead inserts an ordinary C string expression.
Unescaped comma, colon, ], and } terminate collection Atoms; backslash
escaping or angle syntax expresses those bytes as data. %<<...>> accepts
literal Symbols only and permits no interpolation or splicing.
$(...) remains compile-time Lisp in ordinary x2c code. Inside a quoted
collection, ${$(+ 40 2)} first unquotes into x2c and then enters compile-time
Lisp.
$name(...) remains a macro invocation in x2c. These parser contexts do not
change Lisp quote or quasiquote.
Percent strings are byte strings: they may span physical lines and their
\\x, \\u, and \\U escapes consume one or two hexadecimal digits.
Ordinary C string and character literals instead follow C escape widths,
including exactly four digits for \\u, eight for \\U, and one or more for
\\x; an unescaped newline does not continue an ordinary C literal.
Adjacent ordinary C string literals form one expression, including across
comments or newlines. Their separate escape boundaries are preserved:
"\\x41" "B" contains A followed by B. The result keeps ordinary C string
typing and converts to String when its context requires it. A context whose
type is a class or typedef alias reaching String, such as
class Path String;, requires the same conversion. A literal used as the
receiver of a method with no char * definition converts to String first,
so "hello".len() is 5. foreach iterates such a literal as that
String, and a raise detail accepts it as an immutable String. In these
three positions a parenthesized literal, a ?: whose arms are both literals,
and an object-like macro defined to a string literal, #define NAME "x", are
literals too, so (ready ? "on" : "off").len() and NAME.len() work. A macro
name is a literal only while every definition it has is a string literal and
no #undef has dropped it. A name written next to a literal is one of the
adjacent words when it is such a macro or when this unit cannot resolve it,
as a header’s PRId64 is, so printf("%" PRId64 "\n", count) works. This
adjacency rule does not combine percent strings or change quoted collection
syntax.
Immutable literal construction is cached for the process lifetime. This
includes an ordinary C string literal when its context promotes it to String,
as in String name = "x2c";; a dynamic char * or char[] value still
converts when the expression is evaluated. The ordinary literal keeps its exact
C escape spelling and decoding, and does not acquire percent-string escape
rules.
Cached Strings, boxed Var values, and canonical List graphs are
allocated eagerly during translation-unit initialization and retained for the
process lifetime. A cached literal in a public inline function uses private
storage in each C translation unit that includes the generated header;
canonicalizers still make equal String and List values share their value
identity. A guarded call on the inline entry is the fallback on hosts where
eager constructors do not run. Therefore, an allocation failure for a cached
literal can occur before main runs.
Caching does not change the other identity rules: Lists and non-empty
Strings are canonical when constructed through their canonicalizers; Arrays
and Maps are mutable identity-bearing objects and are constructed at each
evaluation. Non-empty Array and Map literals use counted construction.
File-static String, List, Array, and Map declarations may use their
percent literals directly. A file-static Var may likewise use one of those
literal values:
static String child = %"$root/child";
static String root = %"root";
static List names = %($root $child);
static Var literal_index = %{root: $root, child: $child};
The compiler leaves each C declaration zero-initialized and moves its
runtime assignment into the translation unit’s guarded initializer. Literal
caches run first, followed by the assignments in dependency order; a
declaration may therefore depend on a later file-static declaration.
Independent assignments retain source order. Every object referenced by one
of these initializers must itself be file-static, and a dependency cycle is a
compile-time error. Native initializer operands expand at the original source
position: later macro definitions do not change them, __COUNTER__ keeps
source order, and explicit inline tags remain visible at file scope. Native
macro invocations retain their normal stringizing and token-pasting rules.
Tags hidden inside an opaque native macro remain subject to native C scope.
A function-local static declaration with a runtime-valued initializer runs
once when control first reaches it. This includes aggregates containing
String or Var values, function calls, and values from function arguments.
Concurrent calls wait for the initializing call to publish the whole object.
If initialization raises an Error, a later call retries; side effects already
performed by the initializer remain. Recursive initialization raises
bad-state. This includes a cycle between initializing threads. A static threaded declaration follows the same rule separately in each thread. Native
constant initializers retain native C static storage; a function name,
including a $x2c.callback.adapt result, is constant.
Runtime-initialized local objects preserve their declared type, qualifiers,
array shape, and address across calls. Their storage lasts until process
shutdown, or thread teardown for threaded objects. This storage does not
extend the lifetime of values it refers to: an initializer allocating a Map
inside a temporary Scope still gives that Map the ordinary Scope lifetime.
Use an owner that outlives every use of the stored value. Failed initialization
retains the reserved address; the next attempt starts with zeroed storage.
Only successful initialization publishes the value, and retry does not change
referent ownership.
A goto or switch dispatch cannot bypass a runtime static declaration and
enter its remaining block. Put the declaration before the switch, or put it
inside a case’s own block. A nested switch reached after initialization is
valid.
Native macro calls can be runtime initializers. A bare unknown native macro name, however, retains native C initializer rules: its expansion may be either a constant or a call, which x2c does not inspect. Use an explicit function call when a runtime expansion needs first-use initialization. Native macro bodies also cannot name a lowered local object implicitly; pass the object as a macro argument so its ordinary bound expression is preserved.
Symbols
<name> and <"punctuated name"> produce a 64-bit immediate Symbol.
Symbols are encoded, not allocated or entered in an intern table.
The encoding is selected from the payload:
- the restricted 5-bit alphabet preserves at most ten characters;
- the general 7-bit encoding preserves at most seven characters.
A source literal is accepted only when decoding the encoded value reproduces
its exact spelling. Case folding, _/- folding, and truncation are compile
errors in literals. Symbol.new remains the runtime encoding API and
truncates or normalizes dynamic input to the selected capacity.
Symbol.try_new instead reports whether dynamic input has an exact compact
representation and leaves its output untouched when it does not. Equality
compares the encoded value.
Symbols also name outcomes and states. For example, Lisp.read returns
<value> or <eof>. It raises <incomplete> or <malformed>. Error
causes such as <bad-sig>, <no-symbol>, <bad-arity>, <bad-types>, and
<bad-result> are Symbols too. Numeric enums remain appropriate when their
values are indexes, packed fields, arithmetic inputs, or external numeric
encodings.
For Lisp input, <incomplete> means more source can complete the current form:
an open list, string, block comment, or symbol literal, a reader prefix without
its form, or an escape cut off by end of input. <malformed> means the bytes
already prove the form invalid, such as a bad escape, raw string newline,
malformed number, invalid closed symbol literal, or stray closing parenthesis.
The shared Tokenizer classifies lexemes; Lisp.read checks structural form
balance and preserves the form-start cursor on either failure.
Atoms
Atom is the canonical exact-name type used by List literals and Lisp. It is
Var-shaped and has two representations:
- an immediate compact
<symbol>when encoding and decoding reproduces the exact spelling bytes; - a private
<lsym>whose payload is the canonicalStringpointer otherwise.
Atom.intern is the only function that maps a spelling to a representation.
Repeated construction of one spelling has identical Var bits. Long Atom
equality therefore completes in the existing raw-Var fast path, and its hash
is the String’s cached hash. Atom.str returns the exact bytes, and Atom
representation escapes delimiters, numeric-looking prefixes, comment openers,
whitespace, controls, and backslashes so both %() and Lisp.read recover the
same value.
Long Atoms follow the canonical String pool lifetime, which is described in
scopes and lifetime. Promoting a List also promotes
long Atom payloads contained in it; a standalone long Atom that must escape
a child String pool can promote its Atom.str before that pool is released.
For a side-by-side introduction to the two types, see symbols and atoms.
Var, Null, and void
Var is the tagged value used by heterogeneous collections and generic
runtime APIs. Supported native source types round-trip through their matching
Var tags. long, unsigned long, long long,
unsigned long long, and long double use immutable scope-owned boxes while
Var itself remains eight bytes. Their tags are <long>, <ulong>,
<llong>, <ullong>, and <ldouble> respectively; the runtime derives their
widths from those C types. Explicit i48/u48 tags complete the numeric family
set. The same names with * or ** identify their pointer families.
Raw Null is the all-zero Var. A typed empty String and nil/List
preserve their type tag when boxed. Arrays and Maps instead require
allocated objects even when empty; boxing a null pointer with either value tag
is an invariant violation. void is the all-ones terminal/tombstone value used
by APIs for exhaustion or absence. Status-bearing Iter and Map APIs
separate success from payload bits. Iter.next, Map.get, and Map.del
use void for exhaustion or absence. Malformed representations, including null
wide boxes and null Array or Map values, are rejected. Every accepted
nonnull pointer must name a live object established by its constructor.
Collection construction, Array.push, and Map.set reject void; raw
Null is valid collection data.
Lowercase void is contextual. In a type position it retains C’s no-result
type, including function returns, (void) parameter lists, void *, casts,
and sizeof(void). In an expression position it is a Var literal for the
all-ones sentinel. Parentheses do not change that distinction: (void) is a
parenthesized sentinel expression when it stands alone, while (void) call()
and (void *) pointer are casts because an operand follows the complete type.
Two sentinel operands compare equal with == and identical with ===;
!= and !== are their exact inverses. A sentinel and any ordinary value
compare unequal. Var.equal, Var.fallback_equal, Var.same, and the four
equality and identity operations accepted by Var.binary follow those rules.
Var.str, Var.repr, Var.write_str, and Var.write_repr all render the
sentinel as lowercase void.
Hashing, ordering, truthiness, iteration, conversion, arithmetic, compound
updates, and increment or decrement raise <void-op> when they receive it.
Membership with in
The contextual in operator tests membership in a collection. It is the
operator spelling of the receiver’s contains member, selected through the
Var(T) protocol, and it compiles to that same call:
Map ages = {ada: 36};
List names = %(ada grace);
Array counts = [1, 2, 3];
String text = "hello";
int keyed = <ada> in ages;
int element = <grace> in names;
int value = 2 in counts;
int substring = "ell" in text;
A Map tests its keys, a List or Array its elements, and a String a
substring. The left operand converts to the member’s parameter type, so a
Var holding the key works as well as a literal. A receiver whose type does
not implement a contains member is rejected with
operator 'in' requires an implemented contains member. The member comes
from a protocol the receiver adopts: Var(T) declares one, and the
one-member Contains(T) protocol gives membership to a type that holds
members without boxing them, as SymbolSet does. There is no not in
spelling; write !(name in ages).
in is a keyword only between two operands, so struct buffer *in and
int in = 0 remain ordinary names.
Exact Var-tag tests with is and is not
The contextual is operator tests a Var value against a source type or an
exact tag Symbol. Add not after is to invert the result without wrapping
the test in !():
Var value = 42;
int integer = value is int;
int byte = value is u8;
int sequence = value is List;
int exact = value is <i32>;
int pointer = value is (Var *);
int absent = value is void;
int not_byte = value is not u8;
int present = value is not void;
The left operand must have static type Var or a file-scope alias of Var.
The result is int. The operator has relational precedence, and its left
operand is evaluated exactly once. A Symbol expression on the right is also
evaluated exactly once. A type selector is compile-time syntax and is not
evaluated. is not has the same precedence and evaluation rules, then
logically negates the exact-tag result. is and not remain ordinary
identifiers in all other positions, so existing variables with either name
and calls such as value.is(<i32>) are unchanged.
The test compares exact Var tags. It does not test numeric convertibility,
protocol ancestry, or C type identity. Qualifiers and aliases are resolved to
the tag family used by boxing, which means source types that share one tag
cannot be distinguished. For example, char and signed char both select
<i8>, while int, int32_t, and i32 all select <i32>.
The right operand may be either a type or a Symbol expression. Accepted types
include a supported scalar or system numeric typedef, a builtin runtime type
such as String, List, Symbol, or Var, a visible type with a declared
protocol Var(T) conversion, or a package-qualified form of one of those
types. Supported pointer and double-pointer types are also accepted, but
declarator-shaped types must be parenthesized:
static int pointer_tags(void) {
int number = 0;
int *pointer = &number;
int **double_pointer = &pointer;
Var pointer_value = pointer;
Var double_pointer_value = double_pointer;
return pointer_value is (int *) &&
double_pointer_value is (int **);
}
An unparenthesized pointer selector is rejected with the parenthesized
spelling. Arrays, function types, unsupported pointer depths, unknown types,
and aggregates without a Var(T) adoption have no tag to select and are
rejected. Enum selectors are rejected because enum values box as the shared
<i32> family and retain no nominal enum identity.
A literal or computed Symbol is used directly as the exact tag selector:
Var value = 42;
Symbol wanted = <i32>;
int literal = value is <i32>;
int computed = value is wanted;
An unregistered Symbol cannot match a valid Var tag, so the result is
false. No other expression type is accepted on the right: numeric, pointer,
object, and Var-valued expressions do not stand for types.
Lowercase void is the only selector that is both a type spelling and a
value. It tests the all-ones absence sentinel through Var.is_void: void is void is true, while the sentinel does not match any ordinary tag or type. Raw
Null has the pointer-family <p48> tag, so it matches (void *) and does
not match void; there is no Null selector.
A Type hole or a Symbol-valued Expr hole in an Expression macro may
occupy the selector position. An expanded type is resolved in the invocation
context, including pointer types:
macro Expression $has_type(Expr $value, Type $T) => $value is $T;
macro Expression $has_tag(Expr $value, Expr $tag) => $value is $tag;
static int has_pointer_type(void) {
int number = 0;
int *pointer = &number;
Var value = pointer;
return $has_type(value, int *) && $has_tag(value, <i32*>);
}
Dynamic numeric conversion
The public numeric conversion domain contains all 15 Var families: i8/u8,
i16/u16, i32/u32, i48/u48, long/ulong, llong/ullong, and
f32/f64/ldouble. Var.convert(value, tag) returns the converted value and
raises the specific Error cause when no conversion exists, the target is
invalid, the value is out of range, or the Var encoding is invalid.
Compiler-inserted Var-to-native numeric conversion passes through the same
function before using the target extractor. The scalar-named readers
Var.int, Var.double, and their siblings apply the same rules directly. A
matching tag reads its payload, and any other numeric tag converts through
Var.convert. The raw payload readers Var.integer and Var.floating and
the wide *_value functions remain exact low-level decoders.
Integer-to-integer conversion never uses a floating intermediary. It retains
the destination-width low bits; signed destinations interpret those bits as
two’s-complement. Floating-to-integer conversion truncates toward zero when the
truncated result is representable. NaN, infinities, and out-of-range results
report <conv-range>. Integer-to-floating and floating
narrowing use normal host floating-point behavior.
These numeric rules do not define conversions between nonnumeric tags.
String, Symbol, collection, pointer, and custom-object values use their
declared conversions. Numeric-to-String interpolation boxes through the
verified numeric tags and renders with Var.str. A structurally valid
nonnumeric Var may convert to its own tag unchanged; conversions to other
tags must be defined separately.
A printf-family call with a static format can also format a Var. Numeric
format specifiers convert it to the required promoted C type through
Var.convert; %s uses Var.str. The recognized families are printf,
fprintf, sprintf, snprintf, String.printf, File.printf, and
Buffer.printf. A call with a Var variadic argument requires one direct
static format literal. Automatic lowering covers numeric conversions, %c,
%s, * width and precision, and the hh, h, l, ll, and L modifiers.
Other modifiers, pointer and count conversions, wide strings and characters,
and positional formats require explicit native arguments. This lowering is
the compiler component src/components/printf-format.x, which registers a $rewrite
call pattern for each family member. A project can register its own call
pattern the same way, for example for its own logging function.
Dynamic truthiness and binary operators
When a condition has static type Var or a file-scope alias of Var, the
compiler applies the Var.truth base member in if, while, do, classic
for, ?:, and unary !. A static protocol participant uses its eligible
truth member: an implementation, native alias, or base default. The compiler
applies the member independently to operands of && and ||, leaving C’s
short-circuit evaluation intact.
Truthiness is false for numeric or Symbol zero, Null and null pointers,
canonical empty String/List, and empty Array, Map, Block, Bytes, or
Buffer values. It is true for nonzero values, NaN, infinities, nonempty
containers, and other nonnull objects. A nonnull Iter is true even when
exhausted; the test does not probe its producer.
If either operand of +, -, *, /, %, <<, >>, &, ^, or
| has static Var identity and the other operand is Var or statically numeric,
both operands convert to Var and the result is Var. A nonnumeric operand is
rejected at compile time if its type is known statically, or at runtime if it
is held in a Var. Native-only expressions remain native C. Integer operations
promote their operands to a common integer type. Arithmetic and left shift
retain the low bits that fit that type; signed results interpret those bits as
two’s-complement. Signed right shift fills the sign bits. A negative shift
count or a count at least the promoted left width fails, as does integer
division or remainder by zero. String-tagged Var values additionally accept
+ with a String or raw C string operand. The result is a canonical
String; no other runtime tag is stringified implicitly.
Floating operands support +, -, *, and / in the widest participating
floating family. Host floating behavior includes infinities or NaN from
floating division by zero. Remainder, shifts, and bitwise operators reject a
floating operand. Direct Var lvalues support prefix and postfix ++/--;
prefix returns the updated Var and postfix returns its original value. Unary
- uses an eligible neg protocol member for a static participant or the
Var base member for a dynamic value. Comparisons dispatch separately.
For a static participant, ==/!= use an eligible equal member,
relational punctuation derives from compare, and ===/!== remain
unconditional identity tests. Eligible means implemented, native, or a base
default. When the operands of a protocol operator are two different typedef
names that share an ancestor, such as an alias of String and a String, or
two aliases of String, the operator uses the eligible member of their
nearest shared ancestor. Equality, identity, and total ordering do not become
binary-arithmetic operations.
A comparison with a Var operand converts both operands to Var. A
comparison of two numeric Vars behaves as the same comparison between their
native values in C, after the usual arithmetic conversions. So 3 == 3L and
-1 == 4294967295u hold, -1 < 1u does not, and a NaN is unequal to and
unordered with every value. Other Var operands compare with Var.equal,
and relational punctuation orders them with Var.compare. === on Var
operands compares their bits, and two numbers are identical when they have
the same type and the same bits. So NaN === NaN holds and
+0.0 === -0.0 does not. Map keys compare with Var.equal and sorting
uses Var.compare, so both keep numeric types apart.
For ==, !=, <, >, <=, and >=, a C string literal opposite an
operand of static type String, or an alias reaching String, converts to
that type before ordinary protocol comparison. This applies in either operand
order, through parentheses around the literal, and to a ?: whose arms are
both literals. Other C pointer expressions, including variables and casts,
retain their native comparison behavior. === and !== do not perform this literal conversion.
For +, when each operand is a String, an alias reaching String, a
char array, a char *, or a const char *, both convert to String and
the result is the String that String.add returns, so path + ".o"
concatenates.
The shipped component src/components/dynamic-operators.x implements these
lowerings. It also implements the dynamic compound assignments and increments
below, and unary fallback rejection after protocol resolution. It registers
each translator with $rewrite and a Match pattern typed Var, such as
%(expr ("Var") (op (!or + - (!quote *) / % ...) ? ?)), which keys the rule
by each operator the pattern names and Var. A pattern that types the
operation or an operand Var, or an alias of it, takes the dynamic
operations. They run after protocol operators resolve, only when an operand
has Var identity, so native operands never reach them; each operation
arrives typed Var. A program’s rule registered for the same operator is
tried first. The holes in a macro pattern select the operands that its rule
receives, so a rule for $!Money{...} with typedef Var Money leaves other
Var operands to the shipped rules. The same order applies to every shipped
component: a program’s rule for the same head is tried first.
Direct runtime calls to Var.binary additionally accept comparisons and eager
&&/||. The compiler does not use that eager logical path. It converts each
Var operand at its original C short-circuit position.
Protocol-backed direct updates
A direct participant lvalue supports +=, -=, *=, /=, or %=
when its protocol-resolved add, sub, mul, div, or mod
member has signature
Participant member(Participant, RHS). The right operand is converted to
RHS. Prefix and postfix ++/-- use add or sub with the integer 1
converted to RHS.
The lvalue is evaluated once. Its current value is passed to the member once,
and the returned Participant is stored once. Compound assignment and prefix
forms return the stored value; postfix forms return the original value.
Dynamic compound assignment
Native-only compound assignment remains native C. If a numeric compound
assignment involves Var, the compiler takes the address of an addressable
lvalue once. The runtime loads its current value, performs the dynamic
operation, converts the result to the declared target family, and stores once
only after every step succeeds. The expression result is that converted value.
A failed conversion, divide, remainder, or shift leaves the target unchanged.
This rule covers native scalar numeric and Var lvalues, including C indexing
and member access. The compiler selects a typed adapter from the resolved
storage family; plain char and signed char remain distinct storage types
even though both box as <i8>. Enum and bitfield targets are excluded.
String-tagged Var values accept += with String or raw C string
operands. Native String lvalues accept the same += spelling as
concatenate-and-rebind. Concatenation finishes before the binding is changed.
Array and Map elements support all ten numeric compound operators and
prefix/postfix ++/--. The runtime performs the element read, operation,
stored-tag conversion, and write in one call. A missing Map key or
out-of-range Array index fails without insertion, growth, or mutation.
Numeric += is the Map exception. A missing destination key is inserted with
the right-hand side as its initial value, as though the prior value were
numeric zero, and the inserted value keeps the right-hand side’s numeric tag.
The read, modify, and write happen in one call; that is not a thread-safety
guarantee.
Additive initialization does not make void numeric. A missing or void
right-hand side still fails, as do nonnumeric += and every other compound
operator when the destination key is absent. Successful insertion is a
structural Map change and may invalidate outstanding traversal state.
A direct, optionally parenthesized Array or Map indexed right-hand side
uses one typed cross-container helper. The source value is captured before the
destination is committed, including same-container and same-slot cases. A cast
or larger expression reads the source and then performs one destination update.
Each base, selector, and value expression is evaluated once, and C’s operand
evaluation order remains unchanged.
Dynamic conversion, operators, and updates do not have parallel error-code
APIs. A failure raises its specific cause through the ambient Error channel.
Each of these causes is a shared cause, so it never returns to the operation.
Only a matching filtered catch receives it, and an interrupted update leaves
its target unchanged.
The try_* prefix remains for status results whose payload alone cannot
represent every successful result. Examples include Map.try_get for
presence, Iter.try_next for exhaustion, String.try_long for parse success,
and match/search operations for no-match. Those integer results answer whether
a result exists or an operation applies. Failure causes still go through the
ambient Error channel.
Indexing and slicing
Native C arrays and pointers retain C indexing. Array, List, String, and
Map also define indexed access.
A typedef of one of these four collections inherits its bracket reads. An
exact getindex method on the alias takes precedence over the inherited
getter. A typedef of an ordinary C pointer keeps native indexing unless it
defines its own getindex method. Indexed writes and updates continue to use
the collection’s mutation operations; a read override does not replace them.
-
Array,List, andStringaccept negative indices. -
An
Array,List, orStringindex is anint; a wider value is outside the supported domain and converts as anyVar-to-intconversion does. -
Slice bounds and steps share that
intdomain; a wider value is truncated, so a step of 2^32 becomes 0. -
An out-of-range
ArrayorListread returnsvoid. -
An out-of-range
Stringread returns-1; its indexed result is a byte represented as anint, not a one-byteString. -
A missing
Mapkey returnsvoidthroughgetand bracket reads;Map.try_getreports presence separately. -
ArrayandMapindexed assignment are supported. -
ArrayandMapindexed compound assignment and prefix/postfix++/--are supported as single-call updates. -
Numeric
map[key] += valueinitializes an absent key fromvalue; other indexed updates require an existing destination. -
Listindexed assignment is not supported. -
Stringindexed assignment is not supported:Stringis immutable, and an in-place write would corrupt every equalStringsharing its canonical storage. Use the copy-producingString.withindexresult, or bind a transientString.mallocbuffer to achar *and write that natively.
The optional typed families have different indexing rules:
- A packed
Arrayfromtyped-array.xhas a raw bracket: reads, writes, and compound updates index a concrete element pointer with no null test, no bounds test, and no negative-index normalization. An index outside the elements is undefined as it is for the equivalent C pointer.try_getis the checked read, and it does normalize a negative index. - A packed
Mapfromtyped-map.xadoptsprotocol Var, so itsgetindex,setindex,updateindex, andpostfixindexmembers provide bracket reads, writes, compound updates, and numeric postfix updates. - A typed cons chain from
typed-list.x, such asListInt, is aListtypedef. Its bracket reads behave asListreads, including negative indices, and it rejects indexed updates asListdoes.
Array, List, and String support value[start:stop:step]. Bounds and step
may be omitted. Negative bounds and reverse steps are normalized consistently.
A zero step is invalid.
List indexed updates and String character updates remain unsupported.
Array-level and Map-level += are also unsupported; update an element
instead of the container binding.
Iterator destination omission
Iterator sources and lazy operations take a final Iter destination pointer.
That argument may be omitted for calls nested in an iterator expression that
is consumed immediately by try_next, next, list, array,
foldl, any, all, find, count, sum, product, min, max, or
foreach.
For every missing destination, x2c passes a distinct zero-initialized
struct Iter compound literal. Its automatic lifetime is the enclosing block,
so every source remains valid while the final consumer runs. Explicit final
destinations are preserved, including a mixture of explicit and omitted
destinations in one chain. Calls nested in iterator-valued arguments, such as
both inputs to zip or map2, are completed recursively.
Only iterator destinations can be omitted. x2c has no general default
arguments. An iterator assigned to a local, returned from a function, boxed, or
passed to an arbitrary call still requires explicit storage at every stage.
unzip also remains explicit because its two result iterators share one
caller-owned UnzipShared buffer.
Lambdas
The literal forms are %!(parameters) => expression and
%!(parameters) => { statements }. An optional using &name, &other clause
between the parameters and => captures those surrounding bindings by
reference.
An expression body parses through assignment precedence. An unparenthesized
comma separates arguments of an enclosing call; write (first, second) when
the comma expression itself is the lambda body.
Current guarantees:
- an expression body produces its value; a native C
voidexpression runs once and producesvoid; - a block body accepts statements, including declarations, control
flow,
defer, and nested lambdas.return expression;converts the result toVar; barereturn;and fallthrough producevoid; - bare parameters are
Varvalues; - nullary and multi-parameter forms work;
- unlisted automatic values referenced by the body become read-only snapshots
when the lambda is created, provided their types convert to and from
Var; - assignment, updates, taking a captured binding’s address, and passing it by
reference require
using &name. Explicit readers and writers share the original binding; adding another lambda cannot change a snapshot; - globals remain directly accessible.
int value = 1;
Func read = %!() => value;
Func bump = %!() using &value => ++value;
value = 2;
printf("snapshot=%ld shared=%ld\n", read().integer(), bump().integer());
// snapshot=1 shared=3
Clause names resolve in the surrounding scope. Parameters and local variables still shadow them. Repeated entries are redundant, and an entry unused by the body creates no capture.
Typed parameter syntax retains the complete parameter declarator. A parameter
such as int &value aliases its caller’s lvalue just as it does in a function;
a dynamic Func call checks the source type and qualifiers before passing the
address to its generated adapter. Value parameters are evaluated once and
boxed, while reference parameters take the lvalue’s address without first
reading it.
A noncapturing lambda remains a generated C function and can adapt to a
supported C callback type, including int (*)(int), in an initializer,
assignment, argument, or return. A local typedef of the callback type has the
same behavior. Parentheses around the lambda preserve this adaptation.
Where a Func is expected, including a (Func) cast, a fixed nonvariadic
function or function-pointer value converts implicitly when the Func conversion supports
its parameters and result. A lambda after a cast needs its own parentheses,
as in (Func) (%!(int a) => a * 13), because %! after ) reads as modulo.
Value parameters and results need a lossless Var
conversion, and reference
parameters retain their typed lvalue address. A pointer parameter whose type
has no Var tag of its own, such as const char * or struct timespec *,
accepts any pointer or String argument. A struct parameter taken by value
receives a <p48> pointer to the bytes to copy, and a struct result arrives
as a <p48> pointer to a copy of its bytes in the active Scope. Native code
passes such an address as a void *, because a struct pointer has no lossless
Var conversion of its own. A direct function or noncapturing
lambda has one file-static Func handle that every conversion reuses. The
handle lives for the program; operations such as Lisp.bind that take
ownership of a Func leave it in place. A
function-pointer expression is evaluated once and its exact pointer is copied
into a new Func; later assignment to the source pointer does not retarget it.
A null function pointer instead produces null Func without allocating a
binding. Variadic functions do not convert implicitly; use the advanced
Func.new_rest API when a native operation consumes a rest List.
A capturing lambda has static type Func; calling it directly returns Var.
The same is true after a noncapturing lambda converts to Func. Successful
effect-only calls and explicit void results produce void through either
route. Func.apply preserves that no-value result. A value parameter declared
as Var may receive it; a concrete typed value parameter rejects it. Rest
bindings reject it before constructing their argument List. A Func cannot
be passed as a context-free C callback because that ABI has nowhere to carry
its captured values. List, Array, String, and Iter higher-order
methods accept Func instead, so they can use captured callbacks without a
second API.
Each snapshot conversion executes once, in body first-use order, when the
Func is constructed. A reference capture uses a shared typed cell allocated
where the original binding is declared, preserving initializer order and
single evaluation. Calling the Func allocates no capture storage.
A default capture of an outer reference parameter snapshots its current
referent. using &name retains the caller’s alias without another cell.
Reference captures retain the source’s qualifiers: capturing a const object
by reference does not make it writable.
Nested lambdas capture from their enclosing lexical environment. Sharing an
original variable requires using &name at every enclosing lambda. An inner
reference clause cannot reach through an enclosing snapshot. A nested
snapshot records the value when that inner lambda is constructed.
Capturing a pointer, Array, or Map copies its pointer value. The binding is
read-only, but its pointed-to contents remain mutable; later rebinding the
original variable does not retarget the capture. An aggregate without Var
conversions cannot be captured by value; capture a pointer or explicitly
capture the aggregate by reference.
Captured values and cells remain valid only as long as their owning Scope.
Caller objects and pointer-backed contents keep their existing lifetimes;
reference capture extends neither and adds no synchronization. Reused direct
function handles have file-static lifetime.
Statements
C control flow
x2c accepts C-style if, switch, while, do, classic for, labels,
goto, return, break, and continue statements.
A switch whose selector has static type String, or an alias of String,
selects its case by content. Each case label of that switch is a string
literal, including adjacent literals and a native macro that expands to one;
any other label is an error. A label matches when selector == "label" holds
under the ordinary String comparison, so case "" matches the empty
String, which is null. The selector is evaluated once. Adjacent labels,
default in any position, fallthrough, break, an enclosing loop’s
continue, nested switches, and labels inside nested blocks behave as in an
integer switch. A char *, const char *, or Var selector keeps C’s
integer switch.
typedef String Color;
int classify(Color color) {
switch (color) {
case "red": return 1;
case "green":
case "blue": return 2;
case "": return 3;
default: return 0;
}
}
int main(void) {
printf("%d %d %d %d\n", classify("red"), classify("blue"), classify(NULL),
classify("purple"));
return 0;
}
1 2 3 0
System block decorators
The built-in macro module installs $scope, $let, and $lock without a
per-source import. They retain the normal runtime declaration requirements
and macro collision policy. None has a bare keyword alias.
$scope() retains one region around its following statement and defers
release inside an inner block containing that statement. $scope(pointer)
evaluates the Scope-pointer expression once, pushes that destination, and
uses the same inner-block placement for a deferred pop. Pop restores the
previous destination without destroying the selected Scope. More than one
argument is an invocation error. A decorator around a loop creates one
region; a decorator around its body creates one per iteration.
$let(place, value) captures the address of the place once, saves its value,
registers restoration, assigns the new value once, and runs its body. The
place must be addressable and assignable, and its storage must outlive the
body. Restoration uses the captured address even when later changes would
cause the original expression to name different storage.
$lock(mutex) evaluates a Mutex expression once, calls lock, then defers
unlock around its body. A failed acquisition registers no unlock. These
forms add ordinary blocks and defers and no hidden loops.
See Classes and System Macros for examples and expansions.
With
with expression [as name] { ... } gives an expression a short lexical name.
The name defaults to _:
typedef struct Point { int x, y, z; } Point;
int main(void) {
Point point = { 0 };
with point {
_.x = 1;
_.y = 2;
_.z = 3;
}
return point.x == 1 && point.y == 2 && point.z == 3 ? 0 : 1;
}
The expression may be any x2c expression. Each use acts as a parenthesized copy of that expression. A call used twice is called twice, and an unused expression is not evaluated.
The name is available only in the required braced body and only in expression positions. It does not replace field names, labels, declaration names, type syntax, or literal atoms. A declaration of the same name shadows it from that declaration onward.
with nests lexically. An inner use of the same name shadows the outer one,
and the outer meaning resumes after the inner block. Naming the outer
expression keeps it available through an inner default shorthand:
typedef struct Point { int x, y; } Point;
typedef struct Pair { Point left, right; } Pair;
int main(void) {
Pair pair = { 0 };
with pair.left as left {
with pair.right {
_.x = left.x;
}
}
return 0;
}
At the beginning of a statement, with always introduces this construct. It
remains a contextual identifier in declarations and other expression positions.
A statement that calls a function named with uses the spelling
(with)(arguments);.
Foreach
foreach (declaration in expression) statement evaluates the expression
once and visits its elements. A comma may take the place of in. The
declaration has no trailing semicolon and may bind one name or destructure a
key and value:
int main(void) {
List values = %(1 2 3);
Map map = {1: 10, 2: 20};
int total = 0;
foreach (int value in values) total += value;
foreach (int key in map.keys()) printf("%d\n", key);
foreach (Var (key, value) in map)
printf("%ld=%ld\n", key.integer(), value.integer());
foreach (int value, values) total -= value;
return total == 0 ? 0 : 1;
}
Cursor-backed collections use their typed try_next member directly. Other
values convert to Iter, and each yielded Var converts to the declared loop
type when that conversion exists. Iteration covers
the loop forms and the Iter protocol in tutorial order.
Foreach uses Iter.try_next. A boxed Var without a registered iterator
adapter produces an already-exhausted iterator. A receiver with a statically
known type and no visible exact or inherited Iter conformance is rejected as
not iterable.
Match
match (expression) { case pattern: statement ... default: statement }
evaluates its subject once and executes the first matching case.
Pattern matching introduces the pattern language with
worked examples.
Patterns use List-literal notation. ? and * are anonymous wildcards.
Named binders are ?IDENT and *IDENT, where IDENT is
[A-Za-z_][A-Za-z0-9_]*. Binder keys are canonical Atoms, so long and
case-sensitive identifiers work in recursive, prepared, and compiled match
paths. A sigil-leading Atom with any other suffix is a malformed pattern:
recursive matching fails, MatchPlan reports binder-name, and compiled
match syntax reports a positioned diagnostic.
Match operators and predicate names are compact Symbols. The
?binder?, *binder?, and !op? spellings are reserved control vocabulary
only in their !is predicate operand positions; they are not named
binders. Nested List patterns, literal comparison, default selection, and the
!not, !or, !and, !set, !quote, and !is forms are supported. break
exits the match; continue targets an enclosing loop.
Runtime-built patterns may interpolate an interned Match operator in head
position and retain the literal operator’s semantics. Source match arms
require literal operators so the compiler can prove binder availability. Named
binders inside !not operands are not definitely assigned; binders under
!or or membership-style !set must occur in every alternative; !quote is
opaque.
Arm binders are semantic Var or List locals and support method syntax.
?(typename name) declares a native typed capture. Its exact Var tag must
match the tag tested by value is typename; mismatches fail the pattern without
conversion. The type applies to every unquoted occurrence of that binder in
the arm, including ?name and alternative branches. Repeated names retain
their equality constraint. The shorthand lowers to existing !is predicates
and ordinary local declarations; explicit !is captures remain Var locals.
The opener ?( is adjacent; ? (String text) is a wildcard followed by
a sublist pattern.
Inside ?(, typename is ordinary source type syntax rather than literal
List data: write ?(String text) or ?(List payload). The compiler stores
named types such as String as Strings in type syntax. The type List
%("String") therefore differs from %(String), which contains an Atom;
see macro values. Typedefs use their underlying Var tag,
so ?(Type type), with typedef List Type, tests for a List payload without
validating its contents as type syntax. The tag test precedes the native
local declaration.
An arm may place if (expression) before its colon. The expression runs
after matching, with captures visible, and uses ordinary truth conversion.
False tries the next arm; errors propagate normally. A successful guard runs
the body once.
The shipped component src/components/pattern-match.x lowers match.
$rewrite($matched) registers it on the match statement head; a rule a
program registers there runs first. A program’s switch rule receives the
statements in the arms. It does not receive the switch that the lowering
writes.
Errors and cleanup
try requires a following filtered catch, finally, or both.
raise %(CODE (KEY VALUE)...); records one structured Error. Every KEY is
a bare Symbol read by this syntax, and CODE is either a bare Symbol or
$name, a variable converted to Symbol; each VALUE is one expression.
Values are restricted recursively to Null/nil, numeric and enum values,
Symbols, Atoms, Strings, and Lists of permitted values. void,
pointers, mutable containers, resources, custom objects, and other
identity-bearing values are invalid. A value whose own type is invalid is a
compiler error; invalid contents of a List or Var value reach Error’s
<bad-types> raw floor when the raise runs. The shipped component
src/components/raise.x performs the conversions and the check.
$rewrite($raised) registers on the raise statement head. The compiler
evaluates the converted code and values once each, left to right, and builds
their literals when the raise runs. A try may have adjacent catch arms:
try block.reserve(n);
catch %(alloc-fail * (bytes ?count) *): return 0;
catch %(bad-arg *rest): report(rest);
catch: return -1;
The newest Error is matched as %(CODE @DETAIL). Filtered arms use the same
pattern and binder rules as match; ?name declares a Var, *name
declares a List, and the first matching arm runs. The code position takes a
bare Symbol, a binder, or any single pattern, so
%((!or ?code bad-enc conv-range) *cause) selects either code and binds it. A bare catch: is the
optional default and must be last. Every filter expression is evaluated once
when the try is entered.
Selecting a filtered arm consumes the errors accumulated since that arm was
registered and transfers through each intervening cleanup frame. Its
finally and defer cleanup runs before the selected arm. A finally body
may not define a label, because its statements are repeated on each path that
leaves the region. The transferring registration is removed before the arm
executes, so raising a replacement Error continues outward instead of
re-entering the same arm. When no arm
matches, the Error continues outward unchanged. Catch bindings are borrowed
through the selected arm and must be copied with Error.snapshot to outlive
it.
Every cause in the shared table never returns to its raising call. Error
keeps their policies at <abort> and observing handlers cannot consume them;
only a matching filtered catch transfers control. For a literal raise of
one of them, generated C places __builtin_unreachable() after the runtime
raise call so C control flow has the same rule.
These are the shared causes. The policy of each is <abort>, and a process
cannot change it:
| Code | Cause |
|---|---|
<alloc-fail> | allocation failed |
<size-limit> | a size, length, or capacity is unrepresentable |
<invariant> | an impossible internal state was reached |
<bad-state> | a valid object is in the wrong lifecycle state |
<init-fail> | process-owner initialization failed |
<bad-arg> | an API precondition failed with no better code |
<bad-enc> | a Var encoding is invalid |
<void-op> | void was used as a dynamic value |
<bad-types> | operand types do not support the operation |
<bad-target> | a requested conversion target is invalid |
<no-convert> | no requested conversion exists |
<conv-range> | conversion exists but the value is out of range |
<bad-op> | an operator is unsupported |
<div-zero> | integer division or remainder by zero |
<bad-shift> | a shift count is invalid |
<no-member> | a dynamic value has no such member |
<bad-sig> | a native or Lisp callable signature is invalid |
<bad-arity> | a callable received the wrong argument count |
<no-symbol> | a native symbol cannot be resolved |
<bad-result> | a native result cannot be represented |
<not-call> | a value cannot be called |
<call-stack> | evaluation exceeds its call budget or stack depth |
<interrupt> | the evaluation session has been interrupted |
<unbound> | a program name has no binding |
<malformed> | external syntax or serialized input is invalid |
<incomplete> | external input needs more data |
<format> | formatting failed without output |
<not-found> | an external resource does not exist |
<io-fail> | another external I/O operation failed |
<cmd-fail> | a command or pipeline exited with a nonzero status |
<join-fail> | a worker transferred captured errors at join |
Unknown codes also default to <abort>. A process may set a policy only for
unknown and user-defined codes.
The defer statement schedules the statement for the current block exit.
Multiple defer statements unwind in last-in, first-out order. Cleanup runs
for normal scope exit, return, goto, and Error transfer to a selected
catch, all the way out to the function boundary. It does not run when an
uncaught Error aborts the program, or when native code calls exit or
longjmp. break and continue unwind cleanups only up to the
innermost enclosing loop or switch (whichever the jump targets) and stop
there; a defer or cleanup frame registered outside that boundary is left for
a later normal exit, return, or Error transfer to reach. The errors and
cleanup chapter shows how the two are combined.
A return expression is evaluated and saved before its cleanups run; the
saved result is returned afterward. A goto within the same cleanup ancestry
runs no cleanup. An outward goto runs every cleanup region it exits. A
goto into a protected cleanup region, or into a sibling protected region,
is rejected at compile time, and so is a case or default label that
would enter a region its switch body opened before it.
Generated Error transfer preserves the automatic locals and parameters a
protected body modifies under the repository’s optimized build, whether the
body assigns them by name or writes through a pointer it holds the address in.
The compiler supplies the required volatile C representation for those values
and for the transfer state its frames carry; source code does not need
optimization-specific qualifiers for ordinary assignments in try, catch,
or finally. Error transfer does not restore the process signal mask; code
that changes a signal mask must restore it.
Type-owned initialization
A translation unit may define one top-level initializer with the exact
signature void TYPE.initialize(void), where TYPE is a typedef name. The
compiler calls it lazily at every non-static function boundary and guards it so
direct or recursive calls still execute its body once. Static functions are
internal helpers. They trust a non-static entry point or the initializer that
called them and do not repeat the generated guard.
Compiler-owned literal and static-runtime initialization runs after the guard is set and before the method body. This makes calls from the initializer back into static helpers in the same translation unit safe. Translation units that need only literal caching retain a private synthetic initializer.
When the initializer is defined only inside conditional groups, the non-static
functions call a private synthetic initializer instead. It repeats the
conditions around the initializer’s definition to call the initializer where
it is compiled, and otherwise runs the compiler-owned initialization itself.
It sits before the unit’s first function, so a #define or #undef between
that function and the initializer’s definition that changes those conditions
makes the two disagree. The initializer’s body then never runs, or the
generated C calls an initializer that was compiled out.
Other initialization statements and runtime-valued static assignments belong in the method body. Eligible file-static percent literals use the generated sequence described under Values and literals. Top-level decorators are rejected.
Type-owned shutdown
A translation unit may likewise define one top-level shutdown function with the
exact signature void TYPE.shutdown(void). The compiler registers it once
through Scope.shutdown_hook when that unit initializes. When the unit also
defines TYPE.initialize, that guarded function registers it after its
complete generated initialization sequence; otherwise the unit’s private
synthetic initializer registers it.
Shutdown hooks run in reverse registration order. A type initializer that
acquires process-lifetime resources therefore registers its matching shutdown
only after acquisition succeeds. Calling TYPE.shutdown directly still passes
through the unit’s initialization guard.
Types and conversions
x2c accepts normal C type forms plus runtime types such as Var, List,
Array, Map, String, Symbol, and Iter.
Typedef declarations are supported at file scope and inside compound statements. Local aliases follow lexical scope and shadowing. Their uses are resolved while that scope is available, preserving the existing meaning of file-scope types, including their methods and converters. Local typedefs stay inside their C blocks and are not exported. A locally defined aggregate also stays in its block; it cannot supply a type to a lambda helper lifted outside that block.
Direct and chained file-scope aliases of Var behave like Var in boxing,
extraction, initialization, assignment, function arguments, returns, and
comparisons. Declarations and signatures retain the source alias. This rule
is specific to Var; other file-scope typedefs gain only the methods and
converters declared by their own ancestry.
A typedef and the type it names may substitute for each other in either
direction. An Array can therefore be used as a Block, and an Ast as a
List, without conversion. Two typedefs of a common type cannot substitute
for each other this way: they may describe different contents despite sharing
a C representation. Thus ArrayInt counts = someArrayDbl; is a type error
whose message gives the required sibling converter, ArrayDbl.arrayint. The
existing Array.arrayint instead converts from the common Array ancestor. A
cast still permits reinterpretation.
The compiler warns with code conversion when source spells a
conversion that its destination already performs. Translation continues.
A cast is reported when its operand already has the cast type, qualifiers
included. The comparison uses the declared x2c type, so casts between a
typedef and its underlying type, or between two typedefs of one C type, are
not reported. Neither are (void) casts, or casts of an operand whose C type
x2c does not track, such as a pointer difference or a character constant. A
cast of a C string literal is not reported. The cast keeps the literal native
where x2c would otherwise promote it to a String.
A converter call is reported when its destination would make the same call.
A converter is a method that takes only its receiver and is named for its
result, with str for String. The destinations are the value of an
initializer, an assignment, a return, a declared call argument, an
interpolation hole, and a Var value that a printf-family format consumes.
So String name = sym.str();, f(x.var()), and printf("%s", v.str()) are
reported. A printf-family value is a destination only where the format is a
literal the compiler reads; a computed format, such as a const char *
variable or a name an object macro defines, converts nothing.
The destination makes the same call when either side is Var,
both types share one C type, or the receiver declares the converter. A
method receiver, an operator operand, and a parameter with a qualifier the
result lacks, such as const char *, are not destinations.
These explicit calls differ from the implicit crossing and are not reported:
.str()on aVarat a typed destination, and any other converter call on aVarin an interpolation hole or a printf-family value. Holes and formats render aVarthroughVar.str, which displays any value. The implicit crossing toStringdisplays any value butvoidandNull, which it reads as NULL, while.string()reads the String payload and raises<no-convert>for another tag.- An explicit reader such as
.list()or.map()at a destination of its type. The implicit crossing reads the payload or NULL throughVar.list_payloadand its relatives, while the explicit reader raises<no-convert>for another tag. - A
Varread into a numeric type other thanSymbol, such asv.int()orv.integer(). The implicit crossing callsVar.convertand then reads the result.Var.intalready converts, andVar.integerreturns 0 whereVar.convertraises. - A reader other than
strintoStringon another type, such asFile.string. The implicit crossing calls the type’sstrconverter. - A call bound from macro-constructed syntax, and a call inside the function
that implements the crossing, such as
value.list()insideVar.rowfor aListtypedefRow.
Supported primitive C specifiers are order-independent and normalize to one
compiler spelling. For example, double long becomes long double, and
long unsigned long becomes unsigned long long. Invalid combinations such
as long float are structured type errors at the original source token.
Numeric literal radix, magnitude, and suffix determine the annotated native
family; integer promotions and the usual arithmetic conversions then operate
on that canonical identity. A literal beyond the supported native integer
families is a structured type error.
Conversions remain context-sensitive outside the numeric domain. A source
expression may be boxed to Var, converted through an established target
conversion, or rejected. Scalar typedefs retain their declared annotation but
use the underlying scalar family for arithmetic and conversion to or from
Var.
A converter is the source method named for its target: Source.target for a
plain target and Source.str for String. Lookup uses the source type’s exact
converter when it declares one; otherwise it walks the source’s typedef chain
and uses the nearest converter declaration. Thus a typedef List Row may be
assigned or interpolated as a String through List.str without a forwarding
Row.str. The generated call still names List_str, and direct C calls retain
the converter’s declared ABI. Lookup stops without a call when the target
itself occurs in the chain, since typedef identity already handles that
crossing.
Type qualifiers take part in that decision. const, volatile, and
restrict are retained on the declared type of a function result, a global
object, a record field, and a local, including one imported from a C header.
Handing such a value to a target that is the same type without one of those
qualifiers is a structured type error, because that conversion passes the
same address on unchanged. A conversion that copies through a converter is
unaffected: const char * still becomes a String through String_new. A
qualifier on the value being copied, as opposed to what it points at, is not
part of the comparison, so char *const converts to char *.
A pointer to void is held to the same rule even though its base type differs
from the source’s, because it also passes the same address on unchanged.
void * therefore does not accept a const char *; const void * does.
Host preprocessing
When preprocessing is required, x2c passes the source pathname and every
include directory to the host cc as separate argv elements. x2c does not
interpret spaces or shell metacharacters in these paths. The host process
receives the source path, so quoted includes and its diagnostics retain
source-file context.
The active preprocessed stream is shallow-parsed only to establish the global
environment. Full parsing, diagnostics, and emitted source still come from the
original token stream. Directives remain AST nodes in source order at top
level and inside compound statements. This includes a trailing directive
before }; a directive between if (...), else, while (...), for (...), do, switch (...), defer, try, catch ...:, finally, a match
arm’s :, or a statement macro such as foreach and the statement that
follows it; and a directive before the else, do-while while, catch, or
finally that continues a statement. A conditional group opened there also
takes the statement of each later arm and the closing directive, so foreach
keeps a group of one statement per arm inside its loop. A later statement in
the same arm follows the governed statement, as in C. Directives between match
arms stay between those arms, so an arm inside a conditional group is present
exactly when C compiles the branch that holds it.
An include that x2c cannot resolve remains in the emitted C. Translation
without host preprocessing can therefore succeed with an active missing
header; native compilation rejects it. Explicit host preprocessing also
rejects an active missing header. A missing include in an inactive branch
such as #if 0 does not prevent compilation.
x2c does not fully preprocess its source. The original tokens must still form syntax that x2c can parse. Macro expansion that supplies grammar, or inactive branches containing otherwise unparseable source, can require adjustment even when the host C compiler accepts them.
A C source file renamed from .c to .x is an x2c translation unit. A C
header stays a .h file reached with #include; x2c collects its
declarations and the native compiler reads it from the generated C.
Every branch of a conditional is parsed except the branches C never compiles. x2c output is compiled as C by a GNU-style compiler. Therefore x2c skips these branches:
- the branch under
#ifdef __cplusplus, or under#if defined(__cplusplus)alone or first in a&&conjunction with no||; - the same forms for
_MSC_VER; - the
#elsebranch of#ifndef __cplusplusor#if !defined(__cplusplus); #if 0.
Their tokens are trivia. The directives around them stay in place and are
emitted. A compiler built for a host other than Windows treats _WIN32,
_WIN64, and __CYGWIN__ the same way. An #include in a skipped branch is
not read. A function defined in two arms of one conditional is one definition.
At file scope, extern "C" { opens a linkage group and a } closes it, even
with an #include between them; the group’s declarations are file-scope
declarations, and its braces are not emitted. Any string literal is accepted
as the linkage name. extern "C" before a single declaration is read as
extern.
A unit or a collected header may place a macro before or among a
declaration’s specifiers. The macro’s definition determines what the name
contributes: nothing for an empty body or an attribute (#define RLAPI,
#define WEAK __attribute__((weak))), its storage class for static,
extern, or inline (#define JSMN_API static), builtin type words for a
body such as signed int, and, for a body that is the name of another prefix
macro, that macro’s reading. A function-like macro whose body is its parameter
amid such prefixes wraps a type: in CJSON_PUBLIC(const char *) f(void); the
type is the one inside the parentheses. A macro whose words are a storage
class reads as that storage after the type as well, as in
int LOCAL f(void). Where a name is defined differently in several
conditional branches, a static definition takes precedence, then any
prefix without a qualifier, then a prefix with one, such as zlib’s
z_const, which is const in one branch and nothing in another. A name defined to any other text is a typedef name.
In a collected header, a name with no visible definition directly before a
builtin type word, qualifier, storage class, or inline, as in
API int f(void); with API defined on the C compile line, contributes
nothing.
A GNU attribute or a function-like attribute macro after a declarator or
parameter, int a __attribute__((unused)) = 1, b = 2; or
int f(int x __attribute__((unused)));, is kept as text after that
declarator and applies to it alone, as in C. An attribute on the prototype
of a function the unit defines is written before the type on both the
prototype and the definition x2c generates. Attributes are not part of the
signature compared between a prototype and its definition. A definition
without static after a static prototype keeps the prototype’s internal
linkage. __inline, __inline__, __restrict, and __restrict__ are the
standard keywords. A collected header may also place attributes after
struct, union, or enum and after an aggregate’s closing brace, as in
struct S { char c; int i; } __attribute__((packed));. Collection reads
past them, and C applies them. A unit does not accept these two positions.
in and match are keywords only where their x2c forms can occur: in
between two operands, and match as match (...) followed by case or
{. Elsewhere both are identifiers. A field of a foreign struct whose type
x2c cannot resolve can be indexed; the C compiler alone types the result.
A public struct tag { ... } name; publishes the tag body and
extern struct tag name; in the header and defines name in the source.
A public prototype that uses struct tag is preceded in the header by
struct tag; when the header has not declared the tag. A function body that
follows the declarations keeps the conditional arm it was written in. It is
emitted before a later #undef and before any conditional group that
contains one, so the macro definitions in effect where it was written apply
to it.
A failed host preprocess prints its captured stderr, reports a structured x2c
driver diagnostic at the first directive, and exits nonzero. Partial host
stdout is never parsed after failure. --no-cpp bypasses this discovery pass;
--dump-cpp-text exposes the successful host output.
Diagnostics
Command-line options, including the dump flags that expose an individual phase and the include and output directory switches, are listed in compiler options.
Translation stops a unit after 20 errors by default.
Compiler diagnostics describes --max-errors
and the line that reports the stop. Parsing
recovers at top-level declarations. A rejected declaration is skipped whole
and parsing resumes at the next one, so independent errors in separate
declarations are reported together in source order, and one declaration
contributes at most one error. Tokenization, symbol collection,
transformation, generation, and emission stop at their first error. Warnings
do not count toward the bound, and a report identical to an earlier one is not
repeated. --diagnostics-file writes the same diagnostics as JSON Lines; the
command-line reference describes the fields.
Locations retain file, one-based line and column, token
length, and absolute byte position. The source renderer uses length for
the caret width.
Region warnings
Three codes come from the region check described in Scopes and Lifetime. Each is a warning: translation continues and the program still compiles.
| Code | Reported for |
|---|---|
region | A value allocated inside a region is reachable after the region ends. The message gives the way the value leaves, and the note gives the line that opened the region. |
after-free | A local is read after a call freed it, such as Scope.free, |
Scope.realloc, Array.free, Array.list_free, or a cleanup. | |
bad-free | Scope.free or Scope.realloc is given a literal, a local’s address, or other storage no Scope allocator returned. |
The check analyzes regions lexically and summarizes each function within its
own unit, so a unit gets the same warnings whatever else is translated with
it. For calls into another unit, the check uses only a fixed table of runtime
operations. It does not cover storage from plain malloc or a C library,
raw pointer arithmetic, values reached through a field of a stack struct,
callbacks, or Context regions.