๐Ÿฆ€ Functional Rust

431: Counting Elements at Compile Time

Difficulty: 3 Level: Advanced Count the number of arguments passed to a variadic macro at compile time โ€” enabling fixed-size arrays, static dispatch tables, and compile-time arity validation with no runtime overhead.

The Problem This Solves

`macro_rules!` macros accept variadic input via `$($x:expr),*`, but there's no built-in `len` or `count`. When you need to create a fixed-size array from variadic arguments, or assert that exactly N items were passed, you need to count them yourself. The naive recursive counting approach works but can hit the recursion limit for large inputs. The substitution trick โ€” replacing each token with `()` and measuring the length of the resulting slice โ€” is O(1) and the idiomatic solution for anything non-trivial. Compile-time counting also enables patterns that would otherwise require runtime bookkeeping: a dispatch table whose size is known to the compiler, a `const N: usize` derived from a list of items, or a `compile_error!` if the wrong number of arguments is passed.

The Intuition

The substitution trick is clever: replace every token in the variadic list with `()` (unit), put all those units into a slice literal `[(), (), ()]`, and call `.len()` on it. Since all the values are `()`, the compiler knows the length at compile time and optimises it to a constant. No recursion, no stack overflow, no iteration. The recursive approach (`1 + count!($($tail)*)`) reads more naturally but recurses once per element. For short lists (< 64 items) it's fine. For longer lists or when you want a guaranteed `const`, prefer the substitution trick.

How It Works in Rust

// โ”€โ”€ Recursive count (clear, limited depth) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
macro_rules! count {
 () => { 0usize };
 ($head:tt $($tail:tt)*) => { 1 + count!($($tail)*) };
}

// โ”€โ”€ Substitution trick (preferred for large lists) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
macro_rules! replace_with_unit { ($anything:tt) => { () }; }

macro_rules! count_tts {
 ($($tts:tt)*) => {
     // Replaces each token with () then measures slice length โ€” O(1), const
     <[()]>::len(&[$(replace_with_unit!($tts)),*])
 };
}

// โ”€โ”€ Count expressions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
macro_rules! count_exprs {
 () => { 0usize };
 ($e:expr $(, $rest:expr)*) => { 1 + count_exprs!($($rest),*) };
}

// โ”€โ”€ Build a fixed-size array โ€” size inferred from argument count โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
macro_rules! fixed_array {
 ($($val:expr),* $(,)?) => {{
     const N: usize = count_exprs!($($val),*);
     let arr: [i32; N] = [$($val,)*];
     arr
 }};
}

let arr = fixed_array![10, 20, 30, 40, 50];
// arr has type [i32; 5] โ€” the 5 is a compile-time constant

// โ”€โ”€ Static dispatch table with known size โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
macro_rules! dispatch_table {
 ($($name:ident : $fn:expr),* $(,)?) => {{
     const SIZE: usize = count_exprs!($($fn),*);
     let names: [&str; SIZE] = [$(stringify!($name),)*];
     let funcs: [fn(i32) -> i32; SIZE] = [$($fn,)*];
     (names, funcs)
 }};
}

let (names, funcs) = dispatch_table!(
 double: |x| x * 2,
 square: |x| x * x,
 negate: |x| -x,
);
// SIZE = 3 at compile time, no Vec allocation

What This Unlocks

Key Differences

ConceptOCamlRust
Variadic argument countNot directly โ€” use lists; `List.length` at runtimeCompile-time via substitution trick or recursive `macro_rules!`
Fixed-size array from variadic inputNot possible with standard syntax`[T; N]` where `N = count_exprs!(...)` โ€” zero runtime cost
Compile-time assertions`[@@if]` guards in ppx`const _: () = assert!(count == expected)`
Dispatch table sizing`Array.make n` at runtime`[fn; N]` with `N` as compile-time constant
//! Counting Patterns in Macros
//!
//! Techniques for counting macro arguments.

/// Count using recursion.
#[macro_export]
macro_rules! count_recursive {
    () => { 0usize };
    ($head:tt $($tail:tt)*) => { 1 + count_recursive!($($tail)*) };
}

/// Count using array trick.
#[macro_export]
macro_rules! count_array {
    (@single $_:tt) => { () };
    ($($x:tt)*) => {
        <[()]>::len(&[$(count_array!(@single $x)),*])
    };
}

/// Count expressions.
#[macro_export]
macro_rules! count_exprs {
    () => { 0 };
    ($e:expr) => { 1 };
    ($e:expr, $($rest:expr),+) => { 1 + count_exprs!($($rest),+) };
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_count_recursive_empty() {
        assert_eq!(count_recursive!(), 0);
    }

    #[test]
    fn test_count_recursive() {
        assert_eq!(count_recursive!(a b c d e), 5);
    }

    #[test]
    fn test_count_array_empty() {
        assert_eq!(count_array!(), 0);
    }

    #[test]
    fn test_count_array() {
        assert_eq!(count_array!(1 2 3), 3);
    }

    #[test]
    fn test_count_exprs() {
        assert_eq!(count_exprs!(1, 2, 3, 4), 4);
    }

    #[test]
    fn test_count_exprs_single() {
        assert_eq!(count_exprs!(42), 1);
    }
}
(* Counting elements at compile time in OCaml *)
(* OCaml uses type-level tricks for this *)

(* Phantom types for compile-time length tracking *)
type zero = Zero
type 'a succ = Succ of 'a

type ('a, 'len) vec =
  | Nil : ('a, zero) vec
  | Cons : 'a * ('a, 'n) vec -> ('a, 'n succ) vec

let vec3 = Cons (1, Cons (2, Cons (3, Nil)))
(* Type: (int, zero succ succ succ) vec *)

(* Count at "compile time" via type system *)
let rec length : type a n. (a, n) vec -> int = function
  | Nil -> 0
  | Cons (_, rest) -> 1 + length rest

(* Alternative: compile-time constant via modules *)
let n_items = 5  (* would be a compile-time constant in macro system *)
let items = Array.make n_items 0

let () =
  Printf.printf "vec3 length: %d\n" (length vec3);
  Printf.printf "items capacity: %d\n" n_items