Const Functions — Compile-Time Computation
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
const fn is and the restrictions it operates under (no heap allocation, limited control flow)const items computed by const fn appear in binaries as read-only dataCode 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
const fn results are embedded in the binary at compile time; OCaml module-level expressions run at program startup.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.[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
}
}#[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
| Concept | OCaml | Rust |
|---|---|---|
| Fibonacci function | val fibonacci : int -> int | const fn fibonacci(n: u64) -> u64 |
| Compile-time constant | (not possible without PPX) | const FIB_10: u64 = fibonacci(10) |
| Mutable accumulator | let rec go acc b e = ... (immutable params) | let mut a = 0u64; while ... (loop required) |
| Lookup array | Array.t (heap-allocated) | [u32; 256] (stack / static memory) |
| Alignment helper | let align_up size align = ... | pub const fn align_up(size: usize, align: usize) -> usize |
Key Insights
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.const fn uses while loops instead, which maps to OCaml's tail-recursive helper pattern — both avoid stack blowup, but for different reasons.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.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.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
const fn that computes the nth triangular number and define TRIANGULAR_100 as a compile-time constant.const [u32; 256] array using const fn.size_of_val(&SQUARE_TABLE) equals 1024 bytes (256 × 4) and that it lives in the binary's read-only section using cargo objdump.