Comptime Basics
Zig evaluates code at compile-time using the comptime keyword. This enables powerful metaprogramming without macros or templates.
Comptime Variables
const builtin = @import ( "builtin" );
const separator = if ( builtin . os . tag == . windows ) '\\' else '/' ;
This expression is evaluated at compile-time, creating a platform-specific constant.
Comptime expressions must be fully evaluable at compile-time - they cannot depend on runtime values.
Comptime Parameters
Functions can accept compile-time parameters with the comptime keyword:
fn List ( comptime T : type ) type {
return struct {
items : [] T ,
len : usize ,
};
}
// The generic List data structure can be instantiated by passing in a type:
var buffer : [ 10 ] i32 = undefined ;
var list = List ( i32 ){
. items = & buffer ,
. len = 0 ,
};
Functions that take type parameters are generic functions evaluated at compile-time.
Generic Data Structures
Generic types are created by returning structs from functions:
fn LinkedList ( comptime T : type ) type {
return struct {
pub const Node = struct {
prev : ?* Node ,
next : ?* Node ,
data : T ,
};
first : ?* Node ,
last : ?* Node ,
len : usize ,
};
}
test "linked list" {
// Functions called at compile-time are memoized.
try expect ( LinkedList ( i32 ) == LinkedList ( i32 ));
const ListOfInts = LinkedList ( i32 );
var node = ListOfInts . Node {
. prev = null ,
. next = null ,
. data = 1234 ,
};
}
Generic functions are memoized - calling List(i32) multiple times returns the same type.
Comptime Evaluation
Compile-time code can perform complex computations:
const expect = @import ( "std" ). testing . expect ;
const CmdFn = struct {
name : [] const u8 ,
func : fn ( i32 ) i32 ,
};
const cmd_fns = [ _ ] CmdFn {
CmdFn { . name = "one" , . func = one },
CmdFn { . name = "two" , . func = two },
CmdFn { . name = "three" , . func = three },
};
fn one ( value : i32 ) i32 {
return value + 1 ;
}
fn two ( value : i32 ) i32 {
return value + 2 ;
}
fn three ( value : i32 ) i32 {
return value + 3 ;
}
fn performFn ( comptime prefix_char : u8 , start_value : i32 ) i32 {
var result : i32 = start_value ;
comptime var i = 0 ;
inline while ( i < cmd_fns . len ) : ( i += 1 ) {
if ( cmd_fns [ i ]. name [ 0 ] == prefix_char ) {
result = cmd_fns [ i ]. func ( result );
}
}
return result ;
}
test "perform fn" {
try expect ( performFn ( 't' , 1 ) == 6 );
try expect ( performFn ( 'o' , 0 ) == 1 );
try expect ( performFn ( 'w' , 99 ) == 99 );
}
Inline Loops
Loops can be unrolled at compile-time with inline:
test "inline for" {
const types = [ _ ] type { i8 , i16 , i32 , i64 };
var sum : usize = 0 ;
inline for ( types ) | T | {
sum += @sizeOf ( T );
}
try expect ( sum == 1 + 2 + 4 + 8 );
}
inline for ( items ) | item | {
// Loop is unrolled at compile-time
}
comptime var i = 0 ;
inline while ( i < 10 ) : ( i += 1 ) {
// Loop is unrolled at compile-time
}
Comptime Blocks
You can explicitly mark blocks as compile-time:
test "comptime blocks" {
comptime {
var x : i32 = 1 ;
x += 1 ;
try expect ( x == 2 );
}
}
Type Reflection
Zig provides built-in functions for type introspection:
test "type reflection" {
const T = @TypeOf ( 10 );
try expect ( T == comptime_int );
const info = @typeInfo ( i32 );
try expect ( info == . int );
}
Common Type Functions
@TypeOf
@typeInfo
@sizeOf
@alignOf
const x = 10 ;
const T = @TypeOf ( x ); // comptime_int
Comptime Assertions
You can assert conditions at compile-time:
const assert = @import ( "std" ). debug . assert ;
fn fibonacci ( comptime n : u32 ) u32 {
comptime assert ( n < 47 ); // Prevent overflow
if ( n < 2 ) return n ;
return fibonacci ( n - 1 ) + fibonacci ( n - 2 );
}
test "fibonacci" {
try expect ( fibonacci ( 10 ) == 55 );
}
Excessive compile-time computation can slow down compilation. Use it judiciously.
Generic Functions
Functions can be generic over types:
fn max ( comptime T : type , a : T , b : T ) T {
return if ( a > b ) a else b ;
}
test "generic max" {
try expect ( max ( i32 , 10 , 20 ) == 20 );
try expect ( max ( f64 , 3.14 , 2.71 ) == 3.14 );
}
Duck Typing
Compile-time duck typing allows generic code without explicit interfaces:
fn print ( comptime T : type , value : T ) void {
// If T has a print() method, call it
if ( @hasDecl ( T , "print" )) {
value . print ();
} else {
std . debug . print ( "{any} \n " , .{ value });
}
}
Comptime String Manipulation
fn concat ( comptime a : [] const u8 , comptime b : [] const u8 ) [] const u8 {
return a ++ b ;
}
const greeting = concat ( "Hello, " , "World!" );
Container-Level Comptime
Code at container level is implicitly compile-time:
const os_msg = switch ( builtin . target . os . tag ) {
. linux => "we found a linux user" ,
else => "not a linux user" ,
};
All container-level declarations are evaluated at compile-time in dependency order.
Comptime Type Manipulation
fn PointerTo ( comptime T : type ) type {
return * T ;
}
fn SliceOf ( comptime T : type ) type {
return [] T ;
}
test "type manipulation" {
const IntPtr = PointerTo ( i32 );
try expect ( IntPtr == * i32 );
const IntSlice = SliceOf ( i32 );
try expect ( IntSlice == [] i32 );
}
Inline Switch
Switch statements can be inlined for comptime values:
fn dispatch ( comptime tag : enum { add , sub , mul }) i32 {
return inline switch ( tag ) {
. add => 1 ,
. sub => 2 ,
. mul => 3 ,
};
}
Best Practices
Use comptime for type parameters and generic programming
Leverage compile-time evaluation for zero-cost abstractions
Use inline loops when you need unrolling
Prefer compile-time assertions to catch errors early
Use type reflection for flexible generic code
Remember that comptime functions are memoized
Comptime is one of Zig’s most powerful features - it replaces macros, templates, and generics from other languages with a single, unified mechanism.