Generics
A function or struct with compile-time parameters is generic: it is compiled
once for each distinct set of arguments (monomorphization), like C++ templates
and Zig’s comptime.
fn max(comptime T: type, a: T, b: T) -> T { if a > b { return a } return b}
fn ramp(comptime N: i64) -> [i64; N] { let out = [0; N] let i = 0 while i < N { out[i] = i i = i + 1 } return out}
fn main() { print(max(u8, 7, 200)) // instance `max.0` with T = u8 print(max(i64, -3, -7)) // instance `max.1` with T = i64 print(len(ramp(5))) // returns [i64; 5]}Generic functions
Section titled “Generic functions”-
A
comptimeparameter makes a function generic.comptime T: typetakes a type; any othercomptime x: Utakes a value of typeUcomputed at compile time on the VM. -
Type arguments are written in expression position and read back as types (or left out):
u8,*u8,[u8; 4]andfn(i64) -> i64all parse as expressions. So docell (A, B)andchan (A, B), which as expressions could make a cell or a channel: as type arguments they are the cell or channel of a tuple (Option(cell (i64, str))). -
Inside an instance,
Tstands for its type andNfor a constant, also in array lengths andsize_of. Comptime parameters cannot be assigned. -
comptime T: Ordtakes only types that implement a trait, checked at the call. Either way the body is checked per instance, and errors name the instance:error: max.jh:2:10: cannot apply `>` to str and str (in `max` with T = str) -
Callers only need an instance’s signature, so instances can call themselves. Runaway instantiation (
f(n + 1)insidef) stops after 1000 instances. -
Instances are named
name.Nin JIR (crates/jihoo-sema/src/generic.rs). Instances needed only at compile time (comptime ramp(3)) still appear in the module; natively they are internal and LLVM drops them.
A comptime parameter of a function type passes a function at compile time:
see Function values and
comptime closures.
Inferred type arguments
Section titled “Inferred type arguments”A call may leave out the type arguments, giving one argument per runtime parameter; the types are then inferred from the other arguments:
let x: u8 = 7max(x, 200) // T = u8: the literal takes it from `x`max("app", "apple") // T = strlists.map(xs, fn(v) -> str { return to_str(v) }) // T from `xs`, U from the functionlists.sort_by(words, fn(a, b) { return a < b }) // the function's types from T- Each argument with a type of its own is matched against its parameter’s type:
[T]against[u8]givesT = u8,Pair(T)againstPair(i64)givesT = i64, and likewise through tuples, pointers, function types and the other type constructors. - Arguments that take their type from where they are used, literals and
anonymous functions with types left out, wait for the arguments that decide
their parameter’s type, whatever their order, and are then checked against
it. A literal nothing decides takes its default type, as in
let:max(3, -5)is ani64instance. - An anonymous function’s types left out must be known from other arguments:
in
lists.map(xs, fn(v) { ... })nothing says whatUis, so the error asks for-> Uon the function, or for the type arguments. - Only
comptime T: typeparameters are inferred. A function with a value parameter (comptime N: i64) is always called with all its arguments. - An argument whose type contradicts an earlier one is an error that names
both:
`T` of `max` is str here, but u8 from an earlier argument.
Inferred or written out, the same types give the same instance.
Generic structs
Section titled “Generic structs”struct Vec(T: type) { data: *T len: i64 cap: i64 arena: *Arena}
fn push(comptime T: type, v: *Vec(T), x: T) { ... }
let v = vec_new(i64, &arena) // a Vec(i64)push(i64, &v, 42)- Struct parameters are always compile-time; writing
comptimeis optional. - Each distinct set of arguments is its own struct type, named like
Pair(i64)in messages and$"Pair(i64)"in JIR. Arguments are compared by value, soBuf(CAP * 2)andBuf(16)are the same type whenCAPis 8. - An instance is in JIR only when the program’s code or its other types use
it. One used only while compiling, in a macro or for
size_of, is not, so a macro of a freestanding program may useOption(str)or(str, i64); the same holds for tuple types. - Literals name the instance:
Pair(i64) { a: 1, b: 2 }. - A generic struct can point to itself (
next: *Node(T)), but not contain itself. - Like generic functions, a generic struct’s fields are checked per instance.
Enums take parameters the same way, and their arguments can often be inferred.
lib/alloc.jh puts this together for freestanding code: a bump allocator over
mmap and a growable Vec(T) built on it, all in jihoo
(Standard library,
examples/arena.jh).