ExamplesBy LevelBy TopicLearning Paths
127 Intermediate

Const Functions — Compile-Time Computation

Functional Programming

Tutorial

The Problem

Lookup tables, mathematical constants, and protocol values are often computed once and used throughout a program's lifetime. Computing them at runtime wastes startup time; hand-coding them as literals is error-prone and hard to maintain. Rust's const fn evaluates functions at compile time — the result is baked into the binary as a constant, with zero runtime cost and full verification that the computation is correct at build time.

🎯 Learning Outcomes

  • • Understand what const fn is and the restrictions it operates under (no heap allocation, limited control flow)
  • • Learn how to compute Fibonacci numbers, powers, and lookup tables at compile time
  • • See how const items computed by const fn appear in binaries as read-only data
  • • Understand the practical use cases: cryptographic S-boxes, CRC tables, sine lookup tables
  • Code Example

    pub const fn fibonacci(n: u64) -> u64 {
        let mut a = 0u64;
        let mut b = 1u64;
        let mut i = 0u64;
        while i < n {
            let temp = b;
            b = a + b;
            a = temp;
            i += 1;
        }
        a
    }
    
    // Evaluated at compile time — value baked into the binary
    pub const FIB_10: u64 = fibonacci(10);
    pub const FIB_20: u64 = fibonacci(20);

    Key Differences

  • Evaluation timing: Rust const fn results are embedded in the binary at compile time; OCaml module-level expressions run at program startup.
  • Restrictions: Rust const fn cannot allocate heap memory, call non-const functions, or use floating point (in stable const contexts); OCaml has no such restrictions on startup code.
  • Verification: Rust compile-time evaluation catches panics (overflow, divide-by-zero) as compile errors; OCaml startup panics only at runtime.
  • Lookup tables: Rust can build [T; N] tables in const fn; OCaml requires runtime initialization of arrays.
  • OCaml Approach

    OCaml evaluates module-level expressions at program startup (runtime initialization), not at compile time. let fib_10 = fibonacci 10 runs when the module is first loaded, not when the binary is built. OCaml's ppx_const or meta-programming via camlp4 can move some computation to compile time, but these are third-party tools rather than a built-in language feature.

    Full Source

    #![allow(clippy::all)]
    // Example 127: Const Functions — Compile-Time Computation
    // Rust's `const fn` enables true compile-time evaluation, unlike OCaml which
    // evaluates module-level expressions at program start (runtime initialization).
    
    // Approach 1: Iterative const fn — const contexts cannot use recursion with stack growth,
    // so we use loops (allowed in const fn since Rust 1.46).
    pub const fn fibonacci(n: u64) -> u64 {
        let mut a = 0u64;
        let mut b = 1u64;
        let mut i = 0u64;
        while i < n {
            let temp = b;
            b = a + b;
            a = temp;
            i += 1;
        }
        a
    }
    
    // These are evaluated at compile time — values baked directly into the binary.
    pub const FIB_10: u64 = fibonacci(10);
    pub const FIB_20: u64 = fibonacci(20);
    pub const FIB_30: u64 = fibonacci(30);
    
    // Approach 2: const fn for integer exponentiation using binary exponentiation.
    pub const fn pow_int(mut base: u64, mut exp: u32) -> u64 {
        let mut acc = 1u64;
        while exp > 0 {
            if exp % 2 == 1 {
                acc *= base;
            }
            base *= base;
            exp /= 2;
        }
        acc
    }
    
    pub const TWO_TO_16: u64 = pow_int(2, 16);
    pub const TEN_TO_6: u64 = pow_int(10, 6);
    
    // Approach 3: Build a lookup table at compile time.
    // const fn can populate fixed-size arrays — the result lives in read-only memory.
    pub const fn build_square_table() -> [u32; 256] {
        let mut table = [0u32; 256];
        let mut i = 0usize;
        while i < 256 {
            table[i] = (i * i) as u32;
            i += 1;
        }
        table
    }
    
    pub const SQUARE_TABLE: [u32; 256] = build_square_table();
    
    // Approach 4: const fn for buffer-size computations — common in embedded/protocol code.
    pub const fn align_up(size: usize, align: usize) -> usize {
        (size + align - 1) & !(align - 1)
    }
    
    pub const PACKET_SIZE: usize = 1024;
    pub const ALIGNED_PACKET: usize = align_up(PACKET_SIZE, 64);
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_fibonacci_known_values() {
            assert_eq!(fibonacci(0), 0);
            assert_eq!(fibonacci(1), 1);
            assert_eq!(fibonacci(10), 55);
            assert_eq!(fibonacci(20), 6765);
        }
    
        #[test]
        fn test_fibonacci_constants_match_runtime() {
            // The constants were computed at compile time; verify they match runtime calls.
            assert_eq!(FIB_10, fibonacci(10));
            assert_eq!(FIB_20, fibonacci(20));
            assert_eq!(FIB_30, fibonacci(30));
        }
    
        #[test]
        fn test_pow_int() {
            assert_eq!(pow_int(2, 0), 1);
            assert_eq!(pow_int(2, 10), 1024);
            assert_eq!(pow_int(10, 3), 1000);
            assert_eq!(TWO_TO_16, 65536);
            assert_eq!(TEN_TO_6, 1_000_000);
        }
    
        #[test]
        fn test_square_table() {
            assert_eq!(SQUARE_TABLE[0], 0);
            assert_eq!(SQUARE_TABLE[1], 1);
            assert_eq!(SQUARE_TABLE[15], 225);
            assert_eq!(SQUARE_TABLE[255], 65025);
            // All entries must satisfy the square property.
            for (i, &val) in SQUARE_TABLE.iter().enumerate() {
                assert_eq!(val, (i * i) as u32, "mismatch at index {i}");
            }
        }
    
        #[test]
        fn test_align_up() {
            assert_eq!(align_up(0, 64), 0);
            assert_eq!(align_up(1, 64), 64);
            assert_eq!(align_up(64, 64), 64);
            assert_eq!(align_up(65, 64), 128);
            assert_eq!(ALIGNED_PACKET, 1024); // 1024 is already 64-byte aligned
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_fibonacci_known_values() {
            assert_eq!(fibonacci(0), 0);
            assert_eq!(fibonacci(1), 1);
            assert_eq!(fibonacci(10), 55);
            assert_eq!(fibonacci(20), 6765);
        }
    
        #[test]
        fn test_fibonacci_constants_match_runtime() {
            // The constants were computed at compile time; verify they match runtime calls.
            assert_eq!(FIB_10, fibonacci(10));
            assert_eq!(FIB_20, fibonacci(20));
            assert_eq!(FIB_30, fibonacci(30));
        }
    
        #[test]
        fn test_pow_int() {
            assert_eq!(pow_int(2, 0), 1);
            assert_eq!(pow_int(2, 10), 1024);
            assert_eq!(pow_int(10, 3), 1000);
            assert_eq!(TWO_TO_16, 65536);
            assert_eq!(TEN_TO_6, 1_000_000);
        }
    
        #[test]
        fn test_square_table() {
            assert_eq!(SQUARE_TABLE[0], 0);
            assert_eq!(SQUARE_TABLE[1], 1);
            assert_eq!(SQUARE_TABLE[15], 225);
            assert_eq!(SQUARE_TABLE[255], 65025);
            // All entries must satisfy the square property.
            for (i, &val) in SQUARE_TABLE.iter().enumerate() {
                assert_eq!(val, (i * i) as u32, "mismatch at index {i}");
            }
        }
    
        #[test]
        fn test_align_up() {
            assert_eq!(align_up(0, 64), 0);
            assert_eq!(align_up(1, 64), 64);
            assert_eq!(align_up(64, 64), 64);
            assert_eq!(align_up(65, 64), 128);
            assert_eq!(ALIGNED_PACKET, 1024); // 1024 is already 64-byte aligned
        }
    }

    Deep Comparison

    OCaml vs Rust: Const Functions (Compile-Time Computation)

    Side-by-Side Code

    OCaml

    (* Module-level bindings are evaluated at program startup, not compile time *)
    let rec fibonacci n =
      match n with
      | 0 -> 0
      | 1 -> 1
      | n -> fibonacci (n - 1) + fibonacci (n - 2)
    
    (* Computed when the module loads — runtime initialization *)
    let fib_10 = fibonacci 10
    let fib_20 = fibonacci 20
    
    (* Binary exponentiation — pure, but still runtime *)
    let pow_int base exp =
      let rec go acc b e =
        if e = 0 then acc
        else if e mod 2 = 1 then go (acc * b) (b * b) (e / 2)
        else go acc (b * b) (e / 2)
      in
      go 1 base exp
    
    (* Lookup table built at startup *)
    let square_table = Array.init 256 (fun i -> i * i)
    
    let () =
      assert (fib_10 = 55);
      assert (fib_20 = 6765);
      assert (pow_int 2 16 = 65536);
      assert (square_table.(15) = 225);
      print_endline "ok"
    

    Rust (idiomatic — const fn)

    pub const fn fibonacci(n: u64) -> u64 {
        let mut a = 0u64;
        let mut b = 1u64;
        let mut i = 0u64;
        while i < n {
            let temp = b;
            b = a + b;
            a = temp;
            i += 1;
        }
        a
    }
    
    // Evaluated at compile time — value baked into the binary
    pub const FIB_10: u64 = fibonacci(10);
    pub const FIB_20: u64 = fibonacci(20);
    

    Rust (functional/recursive — NOT const-compatible)

    // Recursive fib works at runtime but cannot be used in const context
    // because const fn recursion depth isn't statically bounded.
    pub fn fibonacci_recursive(n: u64) -> u64 {
        match n {
            0 => 0,
            1 => 1,
            n => fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2),
        }
    }
    

    Rust (compile-time lookup table)

    pub const fn build_square_table() -> [u32; 256] {
        let mut table = [0u32; 256];
        let mut i = 0usize;
        while i < 256 {
            table[i] = (i * i) as u32;
            i += 1;
        }
        table
    }
    
    // Entire 256-entry table lives in read-only binary data — zero runtime init cost
    pub const SQUARE_TABLE: [u32; 256] = build_square_table();
    

    Type Signatures

    ConceptOCamlRust
    Fibonacci functionval fibonacci : int -> intconst fn fibonacci(n: u64) -> u64
    Compile-time constant(not possible without PPX)const FIB_10: u64 = fibonacci(10)
    Mutable accumulatorlet rec go acc b e = ... (immutable params)let mut a = 0u64; while ... (loop required)
    Lookup arrayArray.t (heap-allocated)[u32; 256] (stack / static memory)
    Alignment helperlet align_up size align = ...pub const fn align_up(size: usize, align: usize) -> usize

    Key Insights

  • True compile-time vs startup-time: OCaml module-level let bindings are evaluated when the module loads at program startup — they are fast but still runtime. Rust const items evaluated in a const fn context are fully resolved by the compiler; the result is a literal in the binary with zero runtime cost.
  • No recursion in const fn (in practice): Rust's const evaluator supports recursion technically, but the compiler enforces a recursion limit. Idiomatic const fn uses while loops instead, which maps to OCaml's tail-recursive helper pattern — both avoid stack blowup, but for different reasons.
  • Static arrays vs heap arrays: OCaml's Array.init produces a heap-allocated array initialized at runtime. Rust's const fn can return [T; N] fixed-size arrays that live in the binary's read-only data segment — no heap, no initialization, ideal for embedded targets with no allocator.
  • Dual-mode functions: A Rust const fn works in both compile-time and runtime contexts with no code duplication. OCaml has no direct equivalent; you'd need a PPX preprocessor or a separate build script to achieve true compile-time constants from computed values.
  • **Embedded and no_std relevance:** const fn evaluation happens entirely inside the compiler, with no OS or allocator. This makes it the primary tool for #![no_std] embedded Rust — protocol buffer sizes, register masks, and CRC tables are all computable this way, replacing what C developers do with complex preprocessor macros or build-time code generators.
  • When to Use Each Style

    **Use const fn with loops when:** you need a value or table embedded in the binary with zero runtime overhead — Fibonacci indices, CRC polynomials, alignment masks, or any mathematically fixed constant derived from other constants.

    Use runtime functions when: the computation depends on runtime inputs, involves heap allocation, or uses types/operations not yet supported in const contexts (e.g., floating-point math, trait object dispatch).

    Exercises

  • Write a const fn that computes the nth triangular number and define TRIANGULAR_100 as a compile-time constant.
  • Build a CRC-32 lookup table as a const [u32; 256] array using const fn.
  • Verify that size_of_val(&SQUARE_TABLE) equals 1024 bytes (256 × 4) and that it lives in the binary's read-only section using cargo objdump.
  • Open Source Repos