Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Classes and System Macros

The compiler ships a small set of macros for declaring ordinary types and managing block lifetimes. They use the same methods, protocols, allocation, and defer rules as handwritten x2c. Their definitions are available without an import; runtime operations still require their ordinary declarations, usually supplied by #include "x2c.x".

FacilityAvailabilityPurpose
classShipped keyword aliasDeclare a type and its applicable defaults
$scopeShipped decoratorRetain a region or select a Scope destination
$letShipped decoratorTemporarily replace one captured storage location
$lockShipped decoratorHold a Mutex while a statement runs
$autoShipped expression macroAttach cleanup to an initialized local
foreachShipped keyword aliasTraverse through the ordinary Iter protocol
withContextual grammarSubstitute a source expression within a block
$switch, $dedent, $assert, $todo, $unreachable, $timeShipped, explicit importRemove boilerplate C leaves to every caller
Project macrosExplicit importSupply project-specific syntax and policy

Only class and foreach have bare keyword aliases. Use the $ spelling for $scope, $let, $lock, and $auto. For authoring your own macros, continue with Compile-time Macros.

Classes are ordinary named types

Use class to give a named type the supporting operations you would otherwise write yourself: construction, conversion to and from Var, comparison and hashing, and readable output. These operations let your values work with generic containers and protocol-based code. You choose the representation; the class supplies applicable defaults, which explicit methods can replace.

For example, a small value record can be constructed, used as a Map key, and printed without writing its constructor, converters, hash, or printer:

class Point { int x; int y; };
int main(void) {
Point point = Point.new(3, 4);
Map labels = $auto({});
labels[point] = "origin";
printf("%s: %s\n", point.repr(), labels[Point.new(3, 4)]);
  return 0;
}

Which methods does a class supply?

For a class named T, the possible defaults are listed below. A new class means one declaring a representation, rather than an alias of an existing named type. Defaults depend on that representation; not every class generates every method.

MethodDefault behavior and applicability
T.newConstructs a scalar, value record, or pointer object. An alias forwards the applicable constructor of its underlying named type. Some layouts require an explicit constructor or init, as described below.
T.freeReleases a new pointer class’s Scope allocation early. It does not free fields recursively.
T.cleanupCalls the selected free for a new pointer class and supplies Cleanup participation for $auto.
T.varConverts a new class to Var: a scalar uses its underlying representation, a pointer boxes identity, and a value record boxes a Scope-owned copy.
Var.tConverts a Var back to the new class; for example, Var.point for Point.
T.equal, T.hashUse identity for new pointer classes and fields for supported flat value records. Other value records require compatible explicit methods.
T.str, T.reprProvide string output for new pointer and record classes. Pointer str shows identity; value-record str delegates to repr. Record repr shows fields.
T.write_str, T.write_reprWrite the corresponding output into a Buffer for new pointer and record classes.

Scalar classes retain their underlying type’s comparison, hashing, and output operations. Aliases of existing named types inherit supporting operations, including cleanup, rather than generating a second implementation. Explicit methods replace applicable defaults; the output methods can be customized independently, as described under Boxing and readable output.

init is not generated. Some constructors require you to supply it. Generated cleanup releases the object’s allocation; ownership of resources stored in its fields requires your own cleanup code.

Choose a representation

The name comes first. The remaining declaration specifies its representation:

class Count int;
class IntPointer int *;
class ValuePointer Var *;
class Values Array;
class Point { int x; int y; };
class HeapPoint struct { int x; int y; } *;
int main(void) {
  Point point = Point.new(3, 4);
  return point.x == 3 && point.y == 4 ? 0 : 1;
}

Point is a value aggregate; the short braces abbreviate a struct definition. HeapPoint explicitly names a pointer representation. A value may live in a local, a field, an array, or allocated storage. Values follows the existing Array type chain and constructor; it does not acquire a second allocation layout. A forward declaration such as class Point; reserves the identity until its visible definition supplies the representation.

An enum cannot be a class’s value representation, because a Var has no fixed tag for a type whose integer width C leaves to the implementation. Give the enum a typedef and name that typedef, as class Tone Shade; does for typedef enum Shade { LIGHT, DARK } Shade;. A pointer to an enum is an ordinary pointer representation and needs no typedef.

Construction and early release

A scalar class constructor accepts its scalar value. A class pointing to an eligible value allocates and copies one pointee. A flat record of supported value fields accepts positional arguments in field order:

class Point { int x; int y; };
class HeapPoint struct { int x; int y; } *;
int main(void) {
Point value = Point.new(3, 4);
HeapPoint object = HeapPoint.new(3, 4);
printf("%d %d\n", value.x, object.y);
object.free();
  return 0;
}

The heap constructor allocates in the current Scope. Its generated free allows early release; otherwise the Scope reclaims that allocation. Generated cleanup does not recursively free fields and is not a Scope finalizer. Positional fields include native numeric values and enums, Symbol, Var/Atom, String, List, and their aliases. Var slots are shallow copies.

A record containing a mutable handle such as Array, Map, Iter, or another heap class instead gets a no-argument constructor that zeroes the object and calls its required init method. Nested records and arrays also use this branch. Heap initialization receives the handle; value initialization receives its address:

class History struct { int count; Array items; } *;
void History.init(History self) {
  self.items = [];
}
void History.free(History self) {
  self.items.free();
  Scope.free(self);
}
int main(void) {
History history = $auto(History.new());
history.items.push(42);
  return history.count == 0 ? 0 : 1;
}

An explicit new replaces the default and may take different arguments. It also replaces the default constructor’s obligation to call init. Explicit methods can appear later in the owning source; only the selected default or explicit method is emitted. Two ordinary definitions still conflict, and an importing consumer cannot replace a provider’s defaults.

Derived classes forward an existing parent’s constructor with its arguments and retain parent cleanup. For example, Array’s constructor still chooses storage for Var elements. A variadic parent requires an explicit constructor when its arguments cannot be forwarded by an existing facility. A custom constructor that changes a heap class’s allocation contract must also supply compatible cleanup. A failed init returns no object; it does not roll back arbitrary side effects inside the initializer.

Boxing and readable output

Heap classes box the existing object identity. A copied value aggregate boxes a Scope-owned copy and unboxes by value. The box never points at the original local’s stack storage:

class Point { int x; int y; };
int main(void) {
Point point = Point.new(3, 4);
Var first = point, second = Point.new(3, 4);
Map labels = $auto({});
labels[first] = "origin";
printf("%s\n", labels[second]);
  return second in labels ? 0 : 1;
}

Supported value fields receive compatible field-based equality and hashing, so independently boxed equal values work as the same Map key. Opaque layouts require explicit compatible methods. Equal values must have equal hashes. Heap classes retain identity equality and hashing.

str and repr are independently replaceable. A heap object’s default str prints pointer identity. A new value aggregate’s str delegates to repr; scalar and derived value classes retain their parent’s printing. Record repr visits fields in declaration order, using readable value output and an address fallback for opaque fields. It does not dereference unknown pointers.

Nested Array, Map, List, and class rendering shares an active rendering path. A repeated identity on that path prints its pointer form; a repeated reference outside the active path prints fully again. Custom printers that recurse outside these operations must manage their own recursion. Readable output containing addresses is not a serialization format.

Class boxing uses the existing Var descriptor registry: it has 32 custom rows and freezes when worker startup freezes registration. Every record and heap class reserves one row for the life of the program, boxed or not, so the 32 rows are a budget shared with hand-written tagged protocol Var(T) adoptions and the typed container families; a program that declares more classes than remaining rows fails during startup registration. Scalar and alias classes reserve nothing. Class tags derive from the canonical source file and full name, keeping private classes in different files distinct. Tags use deterministic compact spellings, while diagnostics retain the full name. Registration failure and tag collisions are errors; classes do not remove these runtime limits.

Retain or select a Scope

Without an argument, $scope() retains one region around the following statement. Its expansion deliberately keeps the body inside the deferred release:

int main(void) {
$scope() { Array values = []; values.push(1); }
  return 0;
}

The block placement is equivalent to:

int main(void) {
{
  Scope.retain();
  {
    defer Scope.release();
    { Array values = []; values.push(1); }
  }
}
  return 0;
}

Place the decorator around a loop for one region, or around its body for one region per iteration. No hidden loop changes the meaning of break or continue:

int main(void) {
$scope() for (int i = 0; i < 3; i++) { Scope.malloc(8); }
for (int i = 0; i < 3; i++) $scope() { Scope.malloc(8); }
  return 0;
}

Place it before a file-scope function definition to retain one region around that whole function, without indenting the body a further level:

$scope() static int distinct(List words) {
  Map seen = {};
  foreach (String word, words) seen[word] = 1;
  return seen.len();
}
int main(void) { return distinct(%("ab" "cd" "ab")) == 2 ? 0 : 1; }

The Map belongs to the retained region, and the region closes on every exit, so the return releases it. $lock and $let decorate a function the same way.

With one Scope-pointer argument, $scope evaluates the argument once, pushes that destination, and restores the previous destination on exit:

int main(void) {
Scope destination = $auto(Scope.new());
$scope(&destination) { Scope.malloc(8); }
  return 0;
}

The corresponding body placement is:

int main(void) {
Scope destination = $auto(Scope.new());
{
  Scope.push(&destination);
  {
    defer Scope.pop();
    { Scope.malloc(8); }
  }
}
  return 0;
}

pop restores the destination; it does not release the selected Scope. return and error transfer use the ordinary defer boundaries in both forms.

Temporarily change one location

$let(place, value) captures the address of an assignable place, saves its previous value, installs the new value, and restores that same storage on exit. Both locating the place and computing the new value happen once:

int main(void) {
int levels[2] = { 1, 2 }, index = 0;
$let(levels[index++], 7) {
  printf("%d\n", levels[0]);
  index = 1;
}
  return levels[0] == 1 && levels[1] == 2 ? 0 : 1;
}

Changing index does not redirect restoration. The place’s storage must outlive the body. Ordinary typing rejects const assignment or taking a bitfield’s address. By comparison, with substitutes its source expression on each use and creates no temporary; it is not a saved-value binding.

Hold a Mutex

$lock(mutex) evaluates one Mutex expression, acquires it, and defers unlock until the decorated statement exits. Unlock is registered only after lock succeeds:

int main(void) {
Mutex mutex = $auto(Mutex.new());
$lock(mutex) { printf("held\n"); }
  return 0;
}

This preserves Mutex’s error behavior. It does not replace descriptor locks, file-stream locks, or a project’s conditional-acquisition policy. Logger’s private synchronized decorator keeps its own policy.

Clean up an initialized local

$auto is the complete initializer of an ordinary local declaration:

int main(void) {
Array items = $auto([]);
Buffer text = $auto(Buffer.new(0));
items.push(42);
text.write(items.repr());
printf("%s\n", text.str());
  return 0;
}

Each declaration lowers to the original initialization followed by defer local.cleanup() in the same enclosing block. The initializer runs once, and cleanup is registered only after it succeeds. Compound declarations preserve acquisition order, with each successful acquisition protected before the next initializer runs. Cleanup runs in reverse registration order.

Cleanup(T) declares void T.cleanup(T). Participation is explicit, follows the existing typedef/protocol ancestry, and keeps ordinary method precedence:

TypesCleanup action
Block, Bytes, Array, typed ArraysRelease the backing Block allocation
Map and typed MapsRelease the record and both backing Blocks
Buffer, MutexTheir existing free operation
File, ContextTheir existing close operation
Scope, LispTheir existing destroy operation
Generated heap classesTheir selected free operation
Other typesAn explicitly adopted compatible cleanup method

The deferred call observes the binding at cleanup time. For example, assigning a new Array to an $auto local does not free the earlier Array:

int main(void) {
Array current = $auto([]);
Array earlier = current;
current = [];
earlier.free();
  return 0;
}

The second Array is cleaned up at block exit. Returning or storing an alias does not cancel cleanup; the caller must choose an appropriate lifetime. There is no move or recursive ownership system. Var itself has no owning cleanup contract, and Pool’s free operation requires a separate allocation argument.

Only an initialized automatic local that is a compound-statement item accepts $auto. Parentheses and macro construction preserve that position. Static or threaded storage, fields, assignment, returns, call arguments, for-header declarations, and wrappers nested inside arithmetic or conditionals do not supply this enclosing-block lifetime.

Import the rest of the set

class, foreach, $scope, $let, $lock, and $auto need no import. A second set ships in lib/system-macros.xmacro and becomes available with one line:

$(import "system-macros.xmacro")

Give every case its own block and its own break

C makes you write the block and the break yourself, and forgetting either is a familiar defect. $switch takes the condition and the body, keeps each run of case and default labels where you wrote it, and turns the statements after a run into one block carrying that run’s break:

$(import "system-macros.xmacro")
int main(void) {
  int code = 2;
  String reached = "none";
  $switch(code)
  {
    case 1:
    case 2:
      String shared = "low";
      reached = shared;
    case 3:
      String shared = "three";
      reached = shared;
    default:
      reached = "other";
  }
  return reached == "low" ? 0 : 1;
}

Both cases declare shared, which plain C rejects without hand-written braces. A run that already transfers gets no added break, so a case ending in return stays free of unreachable code. Items that are not labels pass through untouched, including a declaration before the first case, a goto label, and a preprocessor directive. Deliberate fallthrough is written by nesting an ordinary switch.

Write indented text without indenting the text

$dedent removes the indentation a block of text was written with. The prefix is the run of spaces and tabs opening the first content line, after one leading newline is dropped, so indentation past that prefix survives and the block renormalizes as a unit:

$(import "system-macros.xmacro")
int main(void) {
  String usage = $dedent(%"
    usage: report [options]
      --verbose   explain each step
      --quiet     report only failures
  ");
  return usage.startswith("usage:") ? 0 : 1;
}

A literal whose spelling carries no escape and no interpolation hole is dedented during translation and reaches C as a finished constant. Every other form calls String.dedent at run time, so an interpolated block means the same thing and costs one call. Use String.dedent directly on text that arrives from a file or an argument.

Report the check you actually wrote

$assert raises <invariant> carrying the failing expression as written and the source line it was written on. It transfers to a catch like any other x2c error, and NDEBUG does not remove it:

$(import "system-macros.xmacro")
int main(void) {
  int limit = 50;
  String written = NULL;
  try $assert(limit > 0 && limit < 10);
  catch %(invariant (check ?check) (at ?where)): written = check;
  return written == "limit > 0 && limit < 10" ? 0 : 1;
}

$todo and $unreachable mark a path that is unwritten or excluded by the surrounding logic. Both raise <invariant> with a note and the same location detail, so an unfinished path reports where it is rather than returning something plausible.

Measure a statement in place

$time wraps its target in a monotonic clock pair and reports the duration on stderr. The caller includes <time.h>:

#include <time.h>
$(import "system-macros.xmacro")
int main(void) {
  long total = 0;
  $time("sum")
  {
    for (int i = 0; i < 1000; i++) total += i;
  }
  return total == 499500 ? 0 : 1;
}

The report runs from a defer, so decorating a whole function body still reports when that body returns or a cause transfers out of it.

A complete resource-using program

This program collects line lengths from a file. The retained region owns all allocations; the managed locals close the file and release the mutable history before that region ends. The History method makes ownership of its Array explicit.

class History struct { Array lengths; } *;

void History.init(History self) {
  self.lengths = [];
}

void History.free(History self) {
  self.lengths.free();
  Scope.free(self);
}

int main(int argc, char **argv) {
  if (argc != 2) return 0;
  $scope() {
    History history = $auto(History.new());
    File input = $auto(File.open(argv[1], "r"));
    for (String line; (line = input.readline()) != NULL; )
      history.lengths.push(line.len());
    printf("%s\n", history.lengths.repr());
  }
  return 0;
}