๐Ÿฆ€ Functional Rust

413: Macro Fragment Specifiers

Difficulty: 3 Level: Advanced The type system for macro inputs โ€” `expr`, `ident`, `ty`, `pat`, `literal`, `block`, `stmt`, `tt` each capture different kinds of syntax.

The Problem This Solves

A `macro_rules!` pattern like `($x)` would match anything โ€” but the macro wouldn't know what kind of thing it captured. Can it call `$x()` as a function? Use it as a type? Pass it to another macro? Without type information, macro expansion is unsafe โ€” misusing a captured fragment leads to cryptic errors deep in expanded code. Fragment specifiers solve this by tagging what category of syntax a capture must match. `$x:expr` matches any expression. `$name:ident` matches only identifiers. `$t:ty` matches type syntax. `$p:pat` matches patterns. The compiler validates the specifier at macro definition time and at call sites. If you pass a type where an expression is expected, you get a clear error at the call site, not inside the expansion. Choosing the right fragment specifier also determines what you can do with the captured fragment in the template. An `ident` can name a function or field. A `ty` can appear in a type position. An `expr` can appear where a value is needed. Using the wrong one produces a compiler error.

The Intuition

Think of fragment specifiers as the "types" of macro parameters. Just as a function signature `fn f(x: i32)` constrains what you can pass, `($x:expr)` constrains the macro input. Each specifier has a specific syntactic grammar it matches and a set of positions where it can be used in the template. The most flexible specifier is `tt` (token tree) โ€” it matches any single token or any `()`, `[]`, `{}` group. It's the escape hatch: if no specifier fits, use `tt` and let the inner expansion handle parsing. But it gives up the structure that narrower specifiers provide.

How It Works in Rust

// expr: any expression โ€” evaluate and show its source text and value
macro_rules! dbg_expr {
 ($e:expr) => {
     {
         let val = $e;
         println!("  {} = {:?}", stringify!($e), val);  // stringify! gets source text
         val
     }
 };
}

// ident: identifiers โ€” use to generate field names and function names
macro_rules! make_getter {
 ($field:ident : $ty:ty) => {
     fn $field(&self) -> &$ty { &self.$field }  // $field becomes a method name
 };
}

// ty: types โ€” use in type position
macro_rules! make_default_fn {
 ($name:ident -> $ret:ty) => {
     fn $name() -> $ret { Default::default() }  // $ret is used as a return type
 };
}

// literal: constant literal values โ€” strings, numbers, booleans
macro_rules! repeat_str {
 ($s:literal, $n:literal) => { $s.repeat($n) }
}

// block: a { ... } block expression
macro_rules! time_block {
 ($name:literal, $block:block) => {
     {
         let t = std::time::Instant::now();
         let result = $block;  // $block expands as-is
         println!("'{}' took {:?}", $name, t.elapsed());
         result
     }
 };
}

// pat: patterns โ€” use in match arms and if let
macro_rules! matches_variant {
 ($val:expr, $pat:pat) => {
     matches!($val, $pat)
 };
}

struct Person { name: String, age: u32 }
impl Person {
 fn new(name: &str, age: u32) -> Self { Person { name: name.to_string(), age } }
 make_getter!(name: String);  // generates: fn name(&self) -> &String
 make_getter!(age: u32);      // generates: fn age(&self) -> &u32
}

make_default_fn!(empty_string -> String);  // generates: fn empty_string() -> String

fn main() {
 let x = dbg_expr!(2 + 3 * 4);     // prints "2 + 3 * 4 = 14"
 dbg_expr!(x > 10);                 // prints "x > 10 = true"

 let p = Person::new("Alice", 30);
 println!("name={}, age={}", p.name(), p.age());

 println!("default: {:?}", empty_string());

 let opt: Option<i32> = Some(42);
 println!("is Some: {}", matches_variant!(opt, Some(_)));
 println!("is None: {}", matches_variant!(opt, None));

 println!("{}", repeat_str!("ab", 3));  // "ababab"

 let sum = time_block!("sum_block", {
     (1..=1000i64).sum::<i64>()  // entire block is captured as $block
 });
 println!("sum = {}", sum);
}
Fragment specifier reference:
SpecifierMatchesUsable as
`expr`Any expressionvalue, in expression positions
`ident`Identifierfunction name, field name, variable name
`ty`Type syntaxtype annotations, return types
`pat`Pattern`match` arms, `if let`, destructuring
`literal`Literal valuecompile-time constants
`block``{ ... }` blockexpression positions
`stmt`Statementstatement positions
`tt`Any token treeanything โ€” most flexible, least typed

What This Unlocks

Key Differences

ConceptOCamlRust
Macro input categoriesPPX handles AST nodes โ€” complex, typed, extensible`macro_rules!` fragment specifiers โ€” simpler, built-in set, hygienic
Identifier capturePPX: `Ast.Longident.t``$name:ident` โ€” captured and reusable in template
Type capturePPX: `Ast.core_type``$t:ty` โ€” used directly in type positions
Pattern capturePPX: `Ast.pattern``$p:pat` โ€” used in `match` arms and `if let`
// Fragment specifiers in macro_rules!
use std::fmt;

// expr: captures any expression
macro_rules! dbg_expr {
    ($e:expr) => {
        { let val = $e; println!("  {} = {:?}", stringify!($e), val); val }
    };
}

// ident: captures an identifier (variable name, function name, etc.)
macro_rules! make_getter {
    ($field:ident : $ty:ty) => {
        fn $field(&self) -> &$ty { &self.$field }
    };
}

// ty: captures a type
macro_rules! make_default_fn {
    ($name:ident -> $ret:ty) => {
        fn $name() -> $ret { Default::default() }
    };
}

// pat: captures a pattern
macro_rules! matches_pat {
    ($val:expr, $pat:pat) => {
        matches!($val, $pat)  // delegates to std matches! macro
    };
}

// literal: captures a literal value
macro_rules! repeat_str {
    ($s:literal, $n:literal) => {
        $s.repeat($n)
    };
}

// block: captures a block expression
macro_rules! time_block {
    ($name:literal, $block:block) => {
        {
            let t = std::time::Instant::now();
            let result = $block;
            println!("Block '{}' took {:?}", $name, t.elapsed());
            result
        }
    };
}

// stmt: captures statements
macro_rules! with_logging {
    ($($stmt:stmt;)*) => {
        {
            println!("--- begin ---");
            $($stmt;)*
            println!("--- end ---");
        }
    };
}

struct Person { name: String, age: u32 }

impl Person {
    fn new(name: &str, age: u32) -> Self { Person { name: name.to_string(), age } }
    make_getter!(name: String);
    make_getter!(age: u32);
}

make_default_fn!(default_string -> String);
make_default_fn!(default_i32 -> i32);

fn main() {
    println!("=== expr ===");
    let x = dbg_expr!(2 + 3 * 4);
    dbg_expr!(x > 10);

    println!("\n=== ident/ty getters ===");
    let p = Person::new("Alice", 30);
    println!("name={}, age={}", p.name(), p.age());

    println!("\n=== ty default ===");
    println!("default_string: {:?}", default_string());
    println!("default_i32: {}", default_i32());

    println!("\n=== pat ===");
    let opt: Option<i32> = Some(42);
    println!("is Some: {}", matches_pat!(opt, Some(_)));
    println!("is None: {}", matches_pat!(opt, None));

    println!("\n=== literal ===");
    println!("{}", repeat_str!("ab", 3));

    println!("\n=== block ===");
    let sum = time_block!("sum", { (1..=1000i64).sum::<i64>() });
    println!("sum = {}", sum);
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_dbg_expr() {
        let v = dbg_expr!(1 + 1);
        assert_eq!(v, 2);
    }

    #[test]
    fn test_repeat_str() {
        assert_eq!(repeat_str!("xy", 3), "xyxyxy");
    }
}
(* Fragment specifiers concept in OCaml โ€” ppx extensions *)
(* We show what each fragment would capture conceptually *)

(* expr โ€” any expression *)
let eval_twice expr = expr + expr  (* conceptually *)
(* ty โ€” type annotation *)
(* ident โ€” identifier, e.g., field name *)
(* pat โ€” pattern, e.g., Some(x) *)

(* Simulate: generate getters using ident-like patterns *)
type person = {
  name: string;
  age: int;
  email: string;
}

let get_name p = p.name
let get_age p = p.age
let get_email p = p.email

let () =
  let p = {name="Alice"; age=30; email="alice@example.com"} in
  Printf.printf "name=%s age=%d email=%s\n"
    (get_name p) (get_age p) (get_email p)