Skip to content

Generics & Bounds

Generics let one declaration work with multiple types. Generic parameters are written after the declaration name. Type parameters use a name such as T; const generic parameters use const Type Name.

T id<T>(T value) {
return value;
}
Int a = id<Int>(42);
_ b = id(42);
@region(a, b)
struct Pair<A, B> {
@life(a)
A first;
@life(b)
B second;
}
Pair<Int, UInt8[]&> pair = Pair<Int, UInt8[]&>(first: 1, second: "one");

Generic fields may be instantiated with lifetime-carrying types, so a generic nominal type declares and binds the public regions exposed by those fields.

Type arguments may include suffixes:

Pair<Int?, UInt8[]&> maybe_pair;
Pair<Int&!, Int[3]^> mutable_ref_and_owner;

Const generic parameters are compile-time values:

struct StaticCount<N: const Int> {
Int value;
}
Int const_value<N: const Int>() {
N
}

Const generic names are value-level names. They can be used in expressions, but not as type names. The inline form lowers to the same canonical predicate as a leading constraint:

@where(N: const Int)
struct CanonicalCount<N> {
Int value;
}

@where(...) is a leading annotation:

@where(T: Numeric)
T add<T>(T left, T right) {
return left + right;
}

Multiple constraints use commas:

@where(T: Hashable, U: Equatable)
Pair<T, U> make_pair<T, U>(T left, U right);

Intersection bounds use &:

@where(T: Hashable & Equatable)
UInt hash_key<T>(T value);

@alias(Name = Type) creates a local type alias for the current declaration. The alias is visible only in that declaration’s signature, constraints, members, and function body. It does not leak into the module namespace:

@alias(Item = Int)
@alias(Cmp = Fn<Bool, Item, Item>)
Bool compare_with(Cmp cmp, Item left, Item right) {
cmp(left, right)
}

Leading annotations are applied in source order. Generic parameters for the current declaration are available before annotations are applied. Later annotations can use aliases introduced by earlier @alias annotations, but earlier annotations cannot use aliases introduced later. One @alias(...) can contain multiple comma-separated bindings. This is equivalent to splitting them into multiple ordered @alias annotations:

@alias(Items = T[]&, Cmp = Fn<Bool, T, T>)
@where(T: Hashable)
Bool contains_match<T>(Items values, Cmp cmp, T left, T right) {
cmp(left, right)
}

Generic parameter equality:

@where(T == UInt8)
Int use_byte<T>(T value);

Use != to reject a concrete type shape:

@where(T != _^)
struct PlainSlot<T> {
T value;
}

_ inside a type shape is an anonymous placeholder. It does not introduce a generic name; it only means that any type argument is accepted in that position. For example, _^ matches every owning pointer type, and Vector<_> matches a Vector with any element type:

@where(T == Vector<_>)
Int vector_only<T>(T value);
@where(T != _?)
struct NonOptional<T> {
T value;
}

Use : for trait bounds. Use == or != for concrete type-shape matching:

@where(T: Sequence<Element = UInt8>)
Int count_bytes<T>(T value);
@where(T == _^)
Int box_only<T>(T value);

Negative trait bounds use ! before the trait name:

@where(T: !Mutable)
struct ImmutableOnly<T> {
T value;
}

Projected associated type equality uses T.[Trait].Assoc == Type:

@where(T.[Sequence].Element == UInt8)
Int count_bytes<T>(T value);

Trait bounds can bind associated types:

@where(T: Sequence<Element = UInt8>)
Int count<T>(T value);

Use the built-in Mutable bound when a type parameter must carry a type-level write capability:

@where(T: Mutable)
struct Slot<T> {
T value;
}
Slot<Int*!> slot;

Mutable currently matches T&! and T*!. Binding mutability such as Int value! is not a type argument and therefore is not checked by generic bounds.

Generic functions can return T@E in result position:

@where(T: Numeric)
T@ParseErr parse_number<T>(UInt8[]& text);

If the success value is optional, put ? on the success type:

T?@ParseErr parse_optional<T>(UInt8[]& text);
  • Generic type parameters accept types. Const generic parameters accept compile-time values with an explicit type, such as N: const Int.
  • @where(...) is written before the declaration, not after it.
  • Associated type projection constraints use the T.[Trait].Name == Type form.