Skip to content

Generics Reference

Profile: :core Spec: SPEC-026


Type parameters in square brackets between function name and argument list:

func identity[T](x: T) -> T do
return x
end

Multiple parameters:

func zip[A, B](a: A, b: B) -> A do
return a
end
func min[T: Ord](a: T, b: T) -> T do
if a <= b do return a end
return b
end

Multiple bounds with +:

func clamp_and_add[T: Ord + Numeric](value: T, lo: T, hi: T, offset: T) -> T do
if value < lo do return lo + offset end
if value > hi do return hi + offset end
return value + offset
end

Type argument is always explicit:

let result = min[i64](3, 7)
let x = identity[i64](identity[i64](42))

trait Eq {
func eq(self, other: Self) -> bool
func ne(self, other: Self) -> bool
}

Enables == and !=. Satisfied intrinsically by all integer types, floats, and bool.

trait Ord: Eq {
func lt(self, other: Self) -> bool
func le(self, other: Self) -> bool
func gt(self, other: Self) -> bool
func ge(self, other: Self) -> bool
}

Enables <, <=, >, >=. Implies Eq. Satisfied by all integer and float types.

trait Numeric: Ord + Eq {
func add(self, other: Self) -> Self
func sub(self, other: Self) -> Self
func mul(self, other: Self) -> Self
func div(self, other: Self) -> Self
}

Enables +, -, *, /. Implies Ord and Eq.

TypeEqOrdNumeric
i8, i16, i32, i64✓✓✓
u8, u16, u32, u64✓✓✓
f32, f64✓✓✓
bool✓——

No impl blocks required for primitives. The TraitOracle resolves these intrinsically.


The TraitOracle uses fixpoint iteration: if T satisfies Numeric, it automatically satisfies Ord and Eq without any additional annotations.

Numeric → Ord → Eq

Constraint violation messages include supertrait context:

error: type `bool` does not satisfy trait bound `Ord` required for `min[T=bool]`
note: `Ord` requires `Eq` (supertrait: `trait Ord: Eq`)

Returns the smaller of a and b.

func min[T](a: T, b: T) -> T do
if a <= b do return a end
return b
end

Returns the larger of a and b.

func max[T](a: T, b: T) -> T do
if a >= b do return a end
return b
end

Constrains value to [lo, hi].

func clamp[T](value: T, lo: T, hi: T) -> T do
if value < lo do return lo end
if value > hi do return hi end
return value
end
func main() do
print_int(clamp[i64](2, 5, 10)) // 5
print_int(clamp[i64](7, 5, 10)) // 7
print_int(clamp[i64](15, 5, 10)) // 10
end

Some generic functions are backed by compiler intrinsics with type-constant injection. The lowerer resolves the concrete type from ctx.type_substitution and appends type-specific constants before LLVM emission:

func truncate[T](value: i64) -> T do
@intrinsic("truncate", value) // lowerer injects: bit_width
end
func toInt[T](value: i64) !T do
@intrinsic("int_cast_checked", value) // lowerer injects: min, max
end

See std.core.conv for full documentation.


Monomorphization in lower.zig:

  1. Call site with [T=i64] collected into a TypeSubstitution
  2. Function body lowered with T substituted throughout
  3. Mangled name emitted (e.g., min_i64)
  4. Result cached in mono_cache — each unique (name, type_args) pair lowered once

  • No type inference — [T] must be explicit at every call site
  • Single-param substitution — ctx.type_substitution uses first mapped type for intrinsic constant injection (sufficient for single-T functions)
  • Pre-existing leak — mono_cache mangled name strings not freed on teardown (tracked, non-blocking)