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

Protocols

Cleanup(T) declares void T.cleanup(T) for explicitly owned locals. Classes select their defaults and participate through the same protocol and method rules.

A protocol declares methods that other concrete types can adopt. Its base is the type that declares those methods; a participant is a type that explicitly adopts them. The compiler selects each participant method or adapts a base method by converting its arguments and result.

The selected methods support dot calls, operators, and, for Var, calls on boxed values. Adoption works at compile time; it creates no runtime interface object. Functions with matching names do not imply adoption.

Protocols adapt values. Registration, ordering, and generated initialization code belong to decorators. Startup and shutdown operations with no receiver have no value to convert and are outside the protocol system.

Declaration and adoption

A declaration names the base type and uses a fresh name, here T, for the participant. It then declares optional associated types and the methods:

protocol Var(T) {
  associated Key = Var;
  associated Value = Var;

  String T.str(T);
  int T.truth(T);
  Value T.getindex(T, Key);
  Value T.setindex(T, Key, Value);
}

Associated types must be declared before methods. Their names and the participant name are local to the protocol body. The compiler warns if the participant name shadows a visible type, with code shadow: that may be an adoption written as a declaration by mistake.

Participation is a separate, bodyless declaration:

typedef struct Vec *Vec;
Var Vec.var(Vec value);
Vec Var.vec(Var value);
protocol Var(Vec);

Vec must name an existing type. This declaration makes it a participant in Var(T); defining converters or matching typedefs is not enough. The base itself is not a participant. Expressions with the base’s exact static type continue to use its own methods.

A typedef can inherit the nearest visible ancestor’s resolved protocol methods without its own adoption. It also inherits the associated types, conversions, and representation unchanged. The compiler creates no new adoption, adapter, alias, descriptor, tag, or registration for that use.

An explicit adoption on the typedef takes precedence and resolves those choices again. If it is invalid, the compiler reports the error; it does not fall back to an ancestor.

A type that is only a typed view of an existing Var representation can name that representation explicitly:

typedef List Row;

Var Row.var(Row value);
Row Var.row(Var value);

protocol Var(Row) as List;

Row has its own methods, function signatures, protocol participation, and conversions to and from Var. Compatible List methods satisfy Var(Row) directly, without forwarding methods on Row.

Boxed values still carry <list> and use List’s descriptor for dynamic calls; no Row descriptor is registered. Consequently, value is Row and value is List mean the same thing. This is a compile-time distinction, not a new runtime class. The type after as must already have a fixed Var tag, as List, int, and void * do.

A distinct runtime class whose type name does not encode the desired tag can name a shorter custom tag:

protocol Var(ArrayString) tag <arraystr>;

The participant and its converters still use ArrayString, but boxed values carry <arraystr>. The tag must be a Symbol literal, must not be built in, and must be unique across the process. tag registers the participant’s own descriptor; as reuses another type’s descriptor. The two modifiers cannot be combined.

Any adoption may be local to its translation unit:

static protocol Prepared(LocalPlan);

A protocol body may also be static, which keeps it private to its source file. Three visibility patterns cover most code:

ProtocolParticipantTypical spelling
publicpublicprotocol Base(Participant);
public Varprivateprotocol Var(PrivateType);
privateprivatestatic protocol PrivateBase(PrivateType);

A private type can therefore participate in the public Var(T) protocol without becoming public itself:

static typedef struct LocalPlan *LocalPlan;
static Var LocalPlan.var(LocalPlan plan) => Var.new(<localplan>, plan);
static LocalPlan Var.localplan(Var value) => (LocalPlan) value.pointer();
protocol Var(LocalPlan);

An adoption marked meta, such as meta protocol Iter(List);, also makes that conformance’s witnesses available to compile-time code, as described in native C functions.

Placement

Put an adoption beside its converters. The compiler resolves it in the declaring unit, which must see the protocol, the participant, the converters, and the participant methods that fix its associated types. A unit that includes the declaring unit reuses that result. The reference gives the linkage and visibility rules.

Conversions

An adoption needs only the conversions that its generated adapters use. Name the forward conversion Base P.base(P) and the reverse P Base.p(Base). The forward conversion must accept every participant value. The reverse may fail for a base value that holds no P. Var(T) descriptor thunks both unbox arguments and box results, so most object participants need both:

typedef struct Vec *Vec;
Var Vec.var(Vec value);
Vec Var.vec(Var value);
protocol Var(Vec);

A missing conversion is an error at the adoption, naming the method that needs it. The reference lists which adapter needs which conversion.

How members are chosen

Each adopted member resolves on its own. The participant’s own method wins, then the nearest inherited method, then the protocol’s default. An explicit adoption can use a method inherited through the participant’s typedef chain:

typedef List Domain;
protocol Iter(Domain);

Domain already iterates through its inherited List adoption. The explicit adoption selects List.iter, so iteration calls List_iter directly, with no Domain.iter forwarding method.

associated Name = Default; declares a type that each participant binds from its own signatures. Array binds an index Key to int, while Map binds it to Var. Member resolution gives the full rules, including signature conflicts and Var(T).

Operators

Operators call the same members as dot syntax: + calls add, == calls equal, < compares through compare, conditions call truth, and indexing calls getindex. A participant may also declare void T.discard(T) to release each unnamed temporary in an operator chain. The reference lists every operator and its member, with the rules for mixed operands and updates.

When protocols share a method

A participant method satisfies every protocol that declares the same compatible member. Two protocol defaults for one member are an error; implement the member on the participant to choose. The reference gives the full rule.

Before adding a protocol member, inspect the conformance dump for every participant homonym and its signature:

./builds/0/x2c translate --dump-conformance path/to/unit.x

An existing method with the same name must have both the intended meaning and a compatible signature. Rename it before extending the protocol if it means something else. Incompatible signatures are compilation errors.

Worked examples

Vec: declared boxed behavior

Vec supplies the conversions that Var(T)’s descriptor thunks use, implements only str, and explicitly adopts the protocol:

typedef struct Vec {
  int x;
  int y;
} *Vec;

Var Vec.var(Vec value) {
  return Var.new(<vec>, value);
}

Vec Var.vec(Var value) {
  return (Vec) value.pointer();
}

String Vec.str(Vec value) {
  return %"Vec(${value.x},${value.y})";
}

protocol Var(Vec);

int main(void) {
  Vec value = Scope.malloc(sizeof(struct Vec));
  value.x = 1;
  value.y = 2;
  Var boxed = value;
  printf("%s\n", boxed);
  return 0;
}

value.str() calls Vec.str; boxed.str() calls a typed descriptor thunk that unboxes once and then calls Vec.str. Vec does not gain add, indexing, or any other omitted member. Removing protocol Var(Vec); removes protocol participation even though both conversion functions still exist.

Polar: a total forward view

Polar implements magnitude and inherits the Cartesian.angle member. The generated Polar.angle needs only the total forward view:

#include <math.h>

typedef struct Cartesian {
  double x;
  double y;
} *Cartesian;

typedef struct Polar {
  double radius;
  double theta;
} *Polar;

protocol Cartesian(T) {
  double T.magnitude(T);
  double T.angle(T);
}

Cartesian Polar.cartesian(Polar value) {
  Cartesian result = Scope.malloc(sizeof(struct Cartesian));
  result.x = value.radius * cos(value.theta);
  result.y = value.radius * sin(value.theta);
  return result;
}

double Cartesian.magnitude(Cartesian value) {
  return sqrt(value.x * value.x + value.y * value.y);
}

double Cartesian.angle(Cartesian value) {
  return atan2(value.y, value.x);
}

double Polar.magnitude(Polar value) {
  return value.radius;
}

protocol Cartesian(Polar);

int main(void) {
  Polar value = Scope.malloc(sizeof(struct Polar));
  value.radius = 2.0;
  value.theta = 0.5;
  printf("%.1f %.1f\n", value.magnitude(), value.angle());
  return 0;
}

No Cartesian.polar reverse conversion is required because this conformance generates only forward calls. A same-name incompatible Polar.angle would be a hard error at protocol Cartesian(Polar);.

Bytes: ordinary defaults and punctuation

The built-in manifest declares protocol Block(Bytes); centrally. The Bytes.block forward view is declared in common.x and implemented in block.x; no reverse Block.bytes adapter is needed. Members such as len, truth, pop, free, and truncate forward through that view.

int main(void) {
  Bytes bytes = Bytes.new(sizeof(int));
  int value = 7;
  bytes.push(&value);
  printf("%zu %d\n", bytes.len(), bytes ? 1 : 0);
  bytes.pop();
  bytes.free();
  return 0;
}

bytes.truth() and truth punctuation select the same ordinary Block.truth semantics. A scalar that adopts Var(T) without implementing truth or equality gains no static method, so its native punctuation is not replaced by Var.fallback_truth or Var.fallback_equal.

Native protocols

A native protocol maps members directly to C functions:

typedef void *Handle;

protocol void *(T) {
  void T.release(T) = free;
}

protocol void *(Handle);

Native participation is declared by the same bodyless adoption form; a matching typedef alone is insufficient. Each occurrence of T is checked in the C conversion direction that the alias uses. No named bidirectional pair is required.

The participant’s translation unit emits a _Generic/_Static_assert signature check and an alias such as #define Handle_release free. A fully public adoption places them in the participant’s generated header. If the protocol, participant, native target, or adoption is private, both remain in the participant’s C file. Variadic native targets and unsafe conversion directions are rejected.