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
- Fixed-size arrays from variadic macros โ generate `[T; N]` arrays where `N` is the argument count, unlocking stack allocation and `const` contexts.
- Compile-time arity validation โ `assert!(count_exprs!(...) == expected)` inside a `const _` block fails the build if the wrong number of items is provided.
- Plugin/dispatch tables โ build a `[fn(...); N]` array of handlers where the size is known to the compiler, enabling bounds-free indexing.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Variadic argument count | Not directly โ use lists; `List.length` at runtime | Compile-time via substitution trick or recursive `macro_rules!` |
| Fixed-size array from variadic input | Not 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