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.

Leave a Comment

Your email address will not be published. Required fields are marked *