Mojo Compile Time Parameters
Compile-time parameters let you pass values and types into functions and structs that are fixed when the compiler builds your code — not when the program runs. The compiler uses them to generate specialized, optimized machine code for each specific combination of values. This zero-overhead abstraction is one of Mojo's most powerful performance mechanisms.
Runtime vs Compile Time
Runtime parameter (known while the program runs):
fn add(x: Int, y: Int) -> Int: ← x and y set by caller at runtime
return x + y
Compile-time parameter (known when the compiler runs):
fn make_vector[size: Int]() -> SIMD[DType.float32, size]:
return SIMD[DType.float32, size].splat(0.0)
make_vector[4]() ← compiler generates specialized code for size=4
make_vector[8]() ← compiler generates separate code for size=8
Defining Compile-Time Parameters
Compile-time parameters appear in square brackets before the regular parentheses. They use the same syntax as type annotations.
fn repeat_print[count: Int](message: String):
for _ in range(count):
print(message)
fn main():
repeat_print[3]("Hello") # prints "Hello" exactly 3 times
repeat_print[1]("Once") # prints "Once" exactly 1 time
What the compiler does: repeat_print[3] → generates a loop with 3 iterations (may unroll) repeat_print[1] → generates a single print call (no loop needed) These are two different compiled functions, each optimal for its count.
Type Parameters
Types themselves can be compile-time parameters, letting you write one function that works correctly and efficiently for any type.
fn fill_and_sum[dtype: DType, width: Int](value: Scalar[dtype]) -> Scalar[dtype]:
var v = SIMD[dtype, width].splat(value)
return v.reduce_add()
fn main():
# Works for Float32 with 4 lanes
var f32_sum = fill_and_sum[DType.float32, 4](3.0)
print(f32_sum) # 12.0 (3.0 × 4 lanes)
# Works for Int32 with 8 lanes — completely different machine code
var i32_sum = fill_and_sum[DType.int32, 8](2)
print(i32_sum) # 16 (2 × 8 lanes)
alias — Naming Compile-Time Constants
The alias keyword defines a compile-time constant. Unlike let, which is a runtime constant, alias values are substituted by the compiler before the program runs.
alias MAX_SIZE = 1024
alias TILE_WIDTH = 32
alias DEFAULT_DTYPE = DType.float32
fn main():
var buffer = SIMD[DEFAULT_DTYPE, TILE_WIDTH].splat(0.0)
print(len(buffer)) # 32
# MAX_SIZE is a compile-time constant — no memory used at runtime
alias total_cells = MAX_SIZE * MAX_SIZE
print(total_cells) # 1048576
alias vs let vs var: alias N = 100 ← known at compile time, zero runtime cost let n = 100 ← known at runtime, immutable, small runtime cost var n = 100 ← known at runtime, mutable
Struct with Compile-Time Parameters
struct FixedArray[dtype: DType, size: Int]:
var data: SIMD[dtype, size]
fn __init__(inout self, value: Scalar[dtype]):
self.data = SIMD[dtype, size].splat(value)
fn get(self, i: Int) -> Scalar[dtype]:
return self.data[i]
fn sum(self) -> Scalar[dtype]:
return self.data.reduce_add()
fn main():
var a = FixedArray[DType.float32, 4](1.0)
var b = FixedArray[DType.int32, 8](3)
print(a.sum()) # 4.0 (1.0 × 4)
print(b.sum()) # 24 (3 × 8)
The compiler generates two completely separate structs: FixedArray[DType.float32, 4] → 4 × 32-bit floats = 16 bytes FixedArray[DType.int32, 8] → 8 × 32-bit ints = 32 bytes No runtime branching, no type checks — just raw optimized code.
@parameter Decorator
The @parameter decorator marks a nested function or if-statement as compile-time evaluated. The compiler completely eliminates branches that are false at compile time.
fn do_work[use_fast_path: Bool](data: Float64) -> Float64:
@parameter
if use_fast_path:
return data * 2.0 # fast path: simple multiplication
else:
return data * data + data # slow path: two operations
fn main():
# Compiler generates only the fast path code for this call
var result = do_work[True](5.0)
print(result) # 10.0
# Compiler generates only the slow path code for this call
var other = do_work[False](5.0)
print(other) # 30.0
@parameter if — compile-time branch elimination:
do_work[True] compiles to:
return data * 2.0 ← else branch never exists in the binary
do_work[False] compiles to:
return data * data + data ← if branch never exists in the binary
Practical Example: Generic Activation Function
alias RELU = 0
alias SIGMOID = 1
alias TANH_ACT = 2
fn activate[func: Int](x: Float64) -> Float64:
@parameter
if func == RELU:
return x if x > 0.0 else 0.0
elif func == SIGMOID:
return 1.0 / (1.0 + math.exp(-x))
else:
return math.tanh(x)
fn main():
print(activate[RELU](2.5)) # 2.5
print(activate[RELU](-1.0)) # 0.0
print(activate[SIGMOID](0.0)) # 0.5
Key Takeaways
Compile-time parameters appear in square brackets and are resolved by the compiler before the program runs. They enable zero-overhead generic functions and structs — the compiler generates specialized code for each unique combination. Use alias to define named compile-time constants. Type parameters let one function implementation cover all numeric types without runtime branching. The @parameter decorator marks if-statements and nested functions for compile-time evaluation, eliminating dead branches from the compiled binary entirely.
