ExamplesBy LevelBy TopicLearning Paths
782 Fundamental

782-const-eval-patterns — Const Eval Patterns

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "782-const-eval-patterns — Const Eval Patterns" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Compile-time evaluation is not limited to simple arithmetic. Key difference from OCaml: 1. **FNV at compile time**: Rust computes FNV hashes at compile time for switch dispatch; OCaml computes at runtime (module initialization), which is still fast but not embedded in the binary.

Tutorial

The Problem

Compile-time evaluation is not limited to simple arithmetic. Complex computations — string hashing for perfect hash maps, computing bit-reversal permutations, generating code tables — can run during compilation. This example collects practical const fn patterns: FNV hash of a string, log2 of a size for bit-counting, compile-time min/max/clamp, and ANSI escape code generation for terminal colors. These patterns eliminate entire categories of runtime initialization.

🎯 Learning Outcomes

  • • Implement const fn const_hash(s: &str) -> u64 using the FNV-1a algorithm
  • • Write const fn const_log2(n: usize) -> usize for bit-shift computations
  • • Use const fn const_min/max/clamp for bounded configuration values
  • • Understand which operations are available in const fn (bitops, arithmetic, loops, if)
  • • See how const fn can generate switch tables: const ESCAPE_CODES: [&str; 8]
  • Code Example

    pub const fn const_log2(n: usize) -> usize {
        (usize::BITS - n.leading_zeros() - 1) as usize
    }
    
    pub const fn const_next_pow2(n: usize) -> usize {
        1 << (usize::BITS - (n - 1).leading_zeros())
    }
    
    // Computed at compile time
    pub const LOG2_256: usize = const_log2(256);

    Key Differences

  • FNV at compile time: Rust computes FNV hashes at compile time for switch dispatch; OCaml computes at runtime (module initialization), which is still fast but not embedded in the binary.
  • Log2 for arrays: Rust uses const_log2(N) to compute shift amounts for power-of-two rings at compile time; OCaml computes this at runtime.
  • String patterns: Rust's const fn const_hash(s: &str) enables compile-time string→u64 mapping; OCaml has no equivalent.
  • Restrictions: const fn cannot call println!, allocate, or use dyn; OCaml module-level code can call any function.
  • OCaml Approach

    OCaml lacks compile-time evaluation for complex patterns. The cppo preprocessor handles conditional compilation. For hash maps, phf (Rust) has no OCaml equivalent — OCaml uses runtime Hashtbl. Code generation via Dune rules or ocamlopt plugins can produce pre-computed tables, but this is more complex than Rust's const fn. Jane Street's ppx_hash generates efficient hash functions but evaluates them at runtime.

    Full Source

    #![allow(clippy::all)]
    //! # Const Eval Patterns
    //!
    //! Patterns for compile-time computation.
    
    /// Compile-time string hash (FNV-1a)
    pub const fn const_hash(s: &str) -> u64 {
        let bytes = s.as_bytes();
        let mut hash: u64 = 0xcbf29ce484222325;
        let mut i = 0;
        while i < bytes.len() {
            hash ^= bytes[i] as u64;
            hash = hash.wrapping_mul(0x100000001b3);
            i += 1;
        }
        hash
    }
    
    /// Compile-time max of two values
    pub const fn const_max(a: usize, b: usize) -> usize {
        if a > b {
            a
        } else {
            b
        }
    }
    
    /// Compile-time min
    pub const fn const_min(a: usize, b: usize) -> usize {
        if a < b {
            a
        } else {
            b
        }
    }
    
    /// Compile-time clamp
    pub const fn const_clamp(val: i64, min: i64, max: i64) -> i64 {
        if val < min {
            min
        } else if val > max {
            max
        } else {
            val
        }
    }
    
    /// Log2 at compile time
    pub const fn const_log2(n: usize) -> usize {
        if n == 0 {
            0
        } else {
            (usize::BITS - n.leading_zeros() - 1) as usize
        }
    }
    
    /// Next power of two at compile time
    pub const fn const_next_pow2(n: usize) -> usize {
        if n == 0 {
            1
        } else {
            1 << (usize::BITS - (n - 1).leading_zeros())
        }
    }
    
    /// Count digits at compile time
    pub const fn const_digit_count(mut n: u64) -> usize {
        if n == 0 {
            return 1;
        }
        let mut count = 0;
        while n > 0 {
            count += 1;
            n /= 10;
        }
        count
    }
    
    /// Reverse bits at compile time
    pub const fn const_reverse_bits(mut n: u32) -> u32 {
        let mut result = 0u32;
        let mut i = 0;
        while i < 32 {
            result = (result << 1) | (n & 1);
            n >>= 1;
            i += 1;
        }
        result
    }
    
    /// Population count at compile time
    pub const fn const_popcount(mut n: u64) -> u32 {
        let mut count = 0;
        while n != 0 {
            count += 1;
            n &= n - 1;
        }
        count
    }
    
    /// Ceiling division at compile time
    pub const fn const_ceil_div(a: usize, b: usize) -> usize {
        a.div_ceil(b)
    }
    
    /// Align up to multiple at compile time
    pub const fn const_align_up(n: usize, align: usize) -> usize {
        (n + align - 1) & !(align - 1)
    }
    
    // Example compile-time computed values
    pub const HASH_HELLO: u64 = const_hash("hello");
    pub const LOG2_256: usize = const_log2(256);
    pub const NEXT_POW2_100: usize = const_next_pow2(100);
    pub const DIGITS_IN_MILLION: usize = const_digit_count(1_000_000);
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_const_hash() {
            // Different strings produce different hashes
            assert_ne!(const_hash("hello"), const_hash("world"));
            // Same string produces same hash
            assert_eq!(const_hash("test"), const_hash("test"));
        }
    
        #[test]
        fn test_const_max_min() {
            assert_eq!(const_max(3, 7), 7);
            assert_eq!(const_min(3, 7), 3);
        }
    
        #[test]
        fn test_const_clamp() {
            assert_eq!(const_clamp(5, 0, 10), 5);
            assert_eq!(const_clamp(-5, 0, 10), 0);
            assert_eq!(const_clamp(15, 0, 10), 10);
        }
    
        #[test]
        fn test_const_log2() {
            assert_eq!(const_log2(1), 0);
            assert_eq!(const_log2(8), 3);
            assert_eq!(const_log2(256), 8);
            assert_eq!(LOG2_256, 8);
        }
    
        #[test]
        fn test_const_next_pow2() {
            assert_eq!(const_next_pow2(1), 1);
            assert_eq!(const_next_pow2(5), 8);
            assert_eq!(const_next_pow2(100), 128);
            assert_eq!(NEXT_POW2_100, 128);
        }
    
        #[test]
        fn test_const_digit_count() {
            assert_eq!(const_digit_count(0), 1);
            assert_eq!(const_digit_count(123), 3);
            assert_eq!(DIGITS_IN_MILLION, 7);
        }
    
        #[test]
        fn test_const_popcount() {
            assert_eq!(const_popcount(0), 0);
            assert_eq!(const_popcount(0b1111), 4);
            assert_eq!(const_popcount(0b10101010), 4);
        }
    
        #[test]
        fn test_const_align_up() {
            assert_eq!(const_align_up(10, 8), 16);
            assert_eq!(const_align_up(16, 8), 16);
            assert_eq!(const_align_up(0, 8), 0);
        }
    
        // Compile-time tests
        const _: () = assert!(const_log2(256) == 8);
        const _: () = assert!(const_next_pow2(100) == 128);
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_const_hash() {
            // Different strings produce different hashes
            assert_ne!(const_hash("hello"), const_hash("world"));
            // Same string produces same hash
            assert_eq!(const_hash("test"), const_hash("test"));
        }
    
        #[test]
        fn test_const_max_min() {
            assert_eq!(const_max(3, 7), 7);
            assert_eq!(const_min(3, 7), 3);
        }
    
        #[test]
        fn test_const_clamp() {
            assert_eq!(const_clamp(5, 0, 10), 5);
            assert_eq!(const_clamp(-5, 0, 10), 0);
            assert_eq!(const_clamp(15, 0, 10), 10);
        }
    
        #[test]
        fn test_const_log2() {
            assert_eq!(const_log2(1), 0);
            assert_eq!(const_log2(8), 3);
            assert_eq!(const_log2(256), 8);
            assert_eq!(LOG2_256, 8);
        }
    
        #[test]
        fn test_const_next_pow2() {
            assert_eq!(const_next_pow2(1), 1);
            assert_eq!(const_next_pow2(5), 8);
            assert_eq!(const_next_pow2(100), 128);
            assert_eq!(NEXT_POW2_100, 128);
        }
    
        #[test]
        fn test_const_digit_count() {
            assert_eq!(const_digit_count(0), 1);
            assert_eq!(const_digit_count(123), 3);
            assert_eq!(DIGITS_IN_MILLION, 7);
        }
    
        #[test]
        fn test_const_popcount() {
            assert_eq!(const_popcount(0), 0);
            assert_eq!(const_popcount(0b1111), 4);
            assert_eq!(const_popcount(0b10101010), 4);
        }
    
        #[test]
        fn test_const_align_up() {
            assert_eq!(const_align_up(10, 8), 16);
            assert_eq!(const_align_up(16, 8), 16);
            assert_eq!(const_align_up(0, 8), 0);
        }
    
        // Compile-time tests
        const _: () = assert!(const_log2(256) == 8);
        const _: () = assert!(const_next_pow2(100) == 128);
    }

    Deep Comparison

    OCaml vs Rust: Const Eval Patterns

    Compile-Time Functions

    Rust

    pub const fn const_log2(n: usize) -> usize {
        (usize::BITS - n.leading_zeros() - 1) as usize
    }
    
    pub const fn const_next_pow2(n: usize) -> usize {
        1 << (usize::BITS - (n - 1).leading_zeros())
    }
    
    // Computed at compile time
    pub const LOG2_256: usize = const_log2(256);
    

    OCaml

    (* Computed at runtime *)
    let log2 n = int_of_float (log (float_of_int n) /. log 2.)
    let next_pow2 n = 1 lsl (log2 (n - 1) + 1)
    
    let log2_256 = log2 256
    

    Compile-Time String Hash

    Rust

    pub const fn const_hash(s: &str) -> u64 {
        let bytes = s.as_bytes();
        let mut hash: u64 = 0xcbf29ce484222325;
        let mut i = 0;
        while i < bytes.len() {
            hash ^= bytes[i] as u64;
            hash = hash.wrapping_mul(0x100000001b3);
            i += 1;
        }
        hash
    }
    
    pub const HASH_HELLO: u64 = const_hash("hello");
    

    Key Differences

    AspectOCamlRust
    EvaluationRuntimeCompile-time
    Loop in constN/Awhile loop
    Bit operationsAvailableAvailable
    Binary embeddingN/AValues embedded

    Exercises

  • Use const_hash to implement a compile-time dispatch table: const HANDLERS: [(u64, &str); 4] = [...] mapping hashed command names to handler names.
  • Implement const fn is_ascii_uppercase(c: u8) -> bool and const fn to_lowercase(c: u8) -> u8 and use them in a const fn to_lowercase_str that operates on a fixed-size array.
  • Write const fn encode_color(r: u8, g: u8, b: u8) -> u32 that packs RGB into a 24-bit constant and a complementary const fn decode_color(c: u32) -> (u8, u8, u8).
  • Open Source Repos