Skip to content

Composites

A composite is an anonymous structural record. It has ordinary struct storage, field construction, copying, moving, and destruction. Its type is determined by its field names and declared field types.

For an introduction with option-forwarding examples, see the Composites Overview.

Literal syntax

VAR record AUTO := :{
  .a = 1 AS I32;
  .b = 9 AS I32;
  .nested = :{ .x = 3 AS I32; };
  .THIS = 5;
  .OTHER = 7;
};
VAR empty AUTO := :{};
ASSERT(record.nested.x == 3);
ASSERT(COMPOSITE_FIELD_COUNT(DECLTYPE(empty)) == 0);

A named field is .name = expression or .name: expression. Fields may be separated by semicolons or commas, with an optional trailing separator. Field names must be unique. Ordinary identifiers and the keyword names accepted for named call arguments, including THIS, OTHER, ARG, and RETURN, are accepted. A valid field name can still be reserved when the record is used in a call.

Initializer expressions are evaluated once, in source order. The record's canonical field order places positional members in numeric order first, then named members in lexicographic order. Access uses ordinary .field syntax, including nested projections.

Type identity and literal storage

Field types preserve initializer expression types, including references. Two composites have the same type exactly when their field names and declared field types match; initializer order is irrelevant:

VAR first AUTO := :{ .z = 9 AS I32; .a = 1 AS I32; };
VAR second AUTO := :{ .a = 4 AS I32; .z = 8 AS I32; };
ASSERT(SAME_TYPES(DECLTYPE(first), DECLTYPE(second)));

An expression naming an ordinary mutable local has type MUT& T, so storing it directly creates a reference field. An expression producing a pure T creates an owned field. Existing CONST&, MUT&, TEMP&, and WRITE& field types are preserved.

Unconverted numeric and string literals retain their exact literal types. They have no stored payload. Numeric literals with different values therefore produce different field types:

VAR record AUTO := :{ .number = 42; .text = "example"; };
ASSERT(SAME_TYPES(COMPOSITE_FIELD_TYPE(DECLTYPE(record), "number"), TYPEOF(42)));
VAR number I32 := record.number;
ASSERT(number == 42);

STATIC_IF (ARCH_IS_LAYOUTLESS == FALSE)
{
  ASSERT(SIZEOF(TYPEOF(42)) == 0);
  ASSERT(SIZEOF(DECLTYPE(record)) == 0);
}

An explicit conversion such as 42 AS I32 stores an ordinary numeric value. A field of that type can later hold a different I32. Zero-size literal fields do not add payload bytes to a containing record; ordinary layout and alignment rules still apply to its other fields. Layout queries are subject to the target's layout restrictions.

APPLY

The expression form is:

APPLY argument_composite TO callable

The argument composite is evaluated first, then the callable. Each expression is evaluated once. Positional members become positional arguments and named fields become named arguments; nested composites remain individual values. The expression's result is the result of the call.

::clamp FUNCTION(@value I32, @minimum I32 DEFAULT(0), @maximum I32 DEFAULT(100)): I32
{
  IF (value < minimum) { RETURN minimum; }
  IF (value > maximum) { RETURN maximum; }
  RETURN value;
}

VAR options AUTO := :{ .value = 150; .maximum = 80; };
ASSERT((APPLY options TO clamp) == 80);

Normal parameter adaptation, default arguments, and overload resolution apply. Field projection uses the source's ordinary reference access. COMPOSITE_FORWARD exposes owned values as TEMP& arguments.

Functions, procedure pointers, lambdas, bound member functions, and objects with OPERATOR() are supported. APPLY does not invoke constructors. It rejects a non-composite argument, a RETURN field, and an explicit THIS field when the callable already supplies an implicit receiver. A free function may accept an explicitly declared @THIS parameter. Parenthesize APPLY when using its result as an operand of another expression.

KWARGS parameters

The final parameter of a function can capture unmatched named arguments:

@KWARGS ...
@KWARGS:local_name ...

There is at most one such parameter and it has no separate element type. Explicit named parameters consume matching arguments first; the remaining arguments form a composite bound to KWARGS, or to the given local alias. The captured fields preserve the supplied argument types. An empty capture is valid. THIS and RETURN are not captured as unmatched keyword arguments.

The composite is available as an ordinary value expression, supports DECLTYPE, and can be passed to reflection, transforms, or APPLY. The positional PACK_* operations do not inspect it.

This wrapper supplies a default only when the caller omitted that field:

::clamp_with_default_minimum FUNCTION(@KWARGS:options ...): I32
{
  STATIC_IF (COMPOSITE_CONTAINS(DECLTYPE(options), "minimum"))
  {
    RETURN APPLY COMPOSITE_FORWARD(options) TO clamp;
  }
  STATIC_ELSE
  {
    RETURN APPLY COMPOSITE_JOIN(
      COMPOSITE_FORWARD(options), :{ .minimum = 10; }
    ) TO clamp;
  }
}

ASSERT(clamp_with_default_minimum(@value 3) == 10);
ASSERT(clamp_with_default_minimum(@value 3, @minimum 1) == 3);

After ordinary overload ranking ties, a candidate without KWARGS is preferred to one with it. Among remaining candidates, more explicitly matched named parameters are preferred. Declaration order does not select the overload. An uninstantiated KWARGS function cannot be converted directly to a procedure pointer; a procedure pointer needs a concrete parameter interface.

Static reflection

Metadata queries require a composite type, commonly DECLTYPE(record). The queries also accept a reference to a composite type. Field selectors must be compile-time constants.

Operation Result and arguments
COMPOSITE_CONTAINS(T, name) Boolean indicating whether composite type T has the compile-time string name.
COMPOSITE_FIELD_COUNT(T) Compile-time numeric literal containing the number of fields.
COMPOSITE_FIELD_NAME(T, index) String literal naming the field at the zero-based canonical index.
COMPOSITE_FIELD_TYPE(T, selector) Declared field type, selected by a compile-time string name or unsigned index.
COMPOSITE_FIELD_GET(value, selector) Ordinary field projection from a composite value, selected by name or index.

Indices enumerate positional members numerically, then named fields lexicographically, independently of initializer order. An unknown field or out-of-range index is an error; COMPOSITE_CONTAINS returns false for an absent name. COMPOSITE_FIELD_TYPE reports the declared field type, while COMPOSITE_FIELD_GET preserves normal access qualification:

VAR record AUTO := :{ .z = 9 AS I32; .a = 1 AS I32; };
ASSERT(COMPOSITE_CONTAINS(DECLTYPE(record), "a"));
ASSERT(SAME_TYPES(COMPOSITE_FIELD_TYPE(DECLTYPE(record), 0), I32));
ASSERT(SAME_TYPES(TYPEOF(COMPOSITE_FIELD_GET(record, "a")), MUT& I32));
ASSERT(COMPOSITE_FIELD_GET(record, 1) == 9);

STATIC_WHILE provides indexed iteration over heterogeneous fields, as shown in the Overview.

Reference transforms

COMPOSITE_TIE(source) and COMPOSITE_FORWARD(source) evaluate source once and return a new composite with the same names and reference fields.

Declared source field COMPOSITE_TIE COMPOSITE_FORWARD
Owned T, mutable source MUT& T TEMP& T
Owned T, constant source CONST& T CONST& T
Existing reference Same reference type Same reference type

The transforms are shallow: an owned nested composite becomes a reference to that composite. They require a readable source and do not copy or move its owned fields. A WRITE& field already stored inside a readable composite keeps its qualifier. Neither transform extends the lifetime of referenced objects.

VAR record AUTO := :{ .amount = 5 AS I32; };
VAR tied AUTO := COMPOSITE_TIE(record);
VAR forwarded AUTO := COMPOSITE_FORWARD(record);
ASSERT(SAME_TYPES(COMPOSITE_FIELD_TYPE(DECLTYPE(tied), "amount"), MUT& I32));
ASSERT(SAME_TYPES(COMPOSITE_FIELD_TYPE(DECLTYPE(forwarded), "amount"), TEMP& I32));
tied.amount := 13;
ASSERT(record.amount == 13);

Field transforms

Operation Result
COMPOSITE_JOIN(left, right) Concatenate positional members and combine disjoint named fields.
COMPOSITE_SELECT(source, name, ...) Only the named fields.
COMPOSITE_EXCLUDE(source, name, ...) All fields except the named fields.
COMPOSITE_SPLIT(source, name, ...) A composite with .selected and .remainder composites.

Selection names are compile-time strings. Missing names and repeated selection names are errors. Join rejects named-field collisions. Positional members are concatenated and renumbered; selection, exclusion, and splitting also renumber the remaining positional members contiguously from zero. With no names, select returns an empty composite, exclude retains all fields, and split returns an empty .selected with all fields in .remainder. Join evaluates its left source before its right source.

Transforms preserve the selected fields' declared types and use ordinary construction. Owned fields copy from a readable lvalue when copying is supported, or move from an expiring source when moving is supported. Existing reference fields retain their bindings. Move-only fields require a suitable source, for example COMPOSITE_SELECT(MOVE(record), "item").

Splitting does not reassign the storage of the original record's subobjects. To share fields across the results, split a tied or forwarded composite:

VAR record AUTO := :{ .a = 1 AS I32; .b = 2 AS I32; .c = 3 AS I32; };
VAR parts AUTO := COMPOSITE_SPLIT(COMPOSITE_TIE(record), "a", "c");
parts.selected.a := 19;
ASSERT(record.a == 19);
VAR joined AUTO := COMPOSITE_JOIN(parts.selected, parts.remainder);
ASSERT(joined.c<- == record.c<-);

See References, Move Semantics, Call Arguments, and Variadic Packs.

Positional composites and argument unpacking

:[a, b] creates a composite with members "0" and "1". The explicit spelling can mix positional and named members:

VAR arguments AUTO := :{ [0]: 3, [1]: 5, .limit: 7 };
VAR positions AUTO := :[3, 5];

Explicit indices must form a contiguous sequence starting at zero, without duplicates. :[] creates an empty composite. COMPOSITE_FIELD_GET projects a positional field by index.

COMPOSITE_UNPACK(value) expands a composite inside a call's argument list:

::sum FUNCTION(%left I32, %right I32, @offset I32): I32
{
  RETURN left + right + offset;
}

VAR result I32 := sum(COMPOSITE_UNPACK(:[3, 5]), @offset 7);
TEST_ASSERT(result == 15);

Multiple expansions and explicit arguments can be interleaved. Operands are evaluated once in source order. Positional members append to the positional argument list; named members must not collide with another supplied name. RETURN is reserved, and an explicit THIS cannot collide with a bound receiver.

Expansion works in ordinary calls, constructor argument lists, and NEW. COMPOSITE_TIE(pack) and COMPOSITE_FORWARD(pack) also accept a positional parameter pack and expose its elements as positional composite members. COMPOSITE_FORWARD preserves reference categories for a subsequent call.