Generics

Type parameters, interface bounds, const generic arguments, and type packs.

Generics let one declaration work with many concrete types. Zynx specializes generic functions, structs, interfaces, and type aliases for the concrete arguments used by a program.

struct Box<T> {
    value: T,
}

fn id<T>(value: T) -> T {
    return value;
}

fn main() {
    let boxed = Box<i32> { value: 41 };
    let answer = id(boxed.value + 1);

    assert answer == 42;
}

Inference

Generic arguments can be written explicitly, but the compiler often infers them from constructor and function arguments.

struct Container<T> {
    value: T,

    init(self, value: T) {
        self.value = value;
    }
}

fn main() {
    let count = Container(42);
    let label = Container("ok");

    assert count.value == 42;
    assert label.value == "ok";
}

Struct literals can infer generic arguments from their fields:

struct Wrap<T> {
    value: T,
}

fn main() {
    let wrapped = Wrap { value: 7 };

    assert wrapped.value == 7;
}

Interface Bounds

Use an interface bound when generic code needs operations beyond moving, copying, or passing values through.

fn double<T: Integer>(value: T) -> T {
    return value + value;
}

fn main() {
    assert double<i32>(21) == 42;
    assert double<u8>(5 as u8) == (10 as u8);
}

Integer, Signed, Unsigned, and Copy are compiler-provided marker interfaces from std.builtin. Integer excludes bool; Signed and Unsigned imply Integer. User structs cannot opt into the numeric markers. For user-defined capability interfaces, see Interfaces. For the ownership meaning of Copy, see Move, Copy, and Clone.

Const Generic Arguments

A generic parameter declared with a plain integer type is a const generic parameter. It accepts integer literals in generic argument lists:

struct SizedArray<T, N: usize> {
    items: [T, N],
}

fn len<T, N: usize>(items: [T, N]) -> usize {
    _ = items[0];
    return N;
}

fn main() {
    let explicit = SizedArray<i32, 3> { items: [1, 2, 3] };
    let inferred = SizedArray { items: [4, 5] };

    assert len(explicit.items) == 3;
    assert len(inferred.items) == 2;
}

Const generic arguments must be integer literals or another const generic parameter, and the literal must fit the parameter type. A type parameter expects a type argument, so Box<3> is rejected. A const parameter expects a numeric argument, so SizedArray<i32, i32> is rejected.

Const generics are most commonly used with fixed-size arrays, where [T, N] means an array whose element type and length are both part of the type.

Generic type aliases can also use const parameters:

type Fixed<T, N: usize> = [T, N];

fn first<T, N: usize>(items: Fixed<T, N>) -> T {
    return items[0];
}

Type Packs

Type packs represent a variable-length list of types. A pack parameter uses ..., must be the final generic parameter, and must be expanded only in supported positions.

fn tuple<T...>(xs: T...) -> (T...) {
    return (xs...);
}

fn main() {
    let t = tuple(1, true, "ok");

    assert t[0] == 1;
    assert t[1];
    assert t[2] == "ok";
}

Packs can also forward callable signatures:

fn call<R, T...>(f: (T...) -> R, xs: T...) -> R {
    return f(xs...);
}

Const generic packs are not supported.

Type packs are supported only as the final generic parameter of function, method, operator-method, intrinsic-function, and struct declarations. They are not supported on type aliases, interfaces, enums, unions, error sets, or const generic parameters. A declaration may have at most one type pack, and it must be final.

Valid expansion positions are final function parameter types, tuple types, callable parameter lists, generic argument lists for pack-backed declarations, tuple expressions, and call argument forwarding. Pack expansion is not valid as an array element type, array size, error set, const generic argument, or arbitrary expression.

Rules

  • Generic parameters may be types, const integer parameters, or one final type pack.
  • Type parameters can have interface bounds such as T: Copy or T: HasValue<i32>.
  • Const parameters such as N: usize cannot have interface bounds.
  • Const generic values may be integer literals or in-scope const generic parameter names; type-level arithmetic such as N + 1 is not current syntax.
  • Generic function arguments are inferred from call arguments when possible.
  • Generic struct literals can infer their arguments from field values.
  • A method generic parameter cannot shadow a generic parameter from the enclosing struct.

See Function Overloading for how generic candidates participate in overload resolution.