413: Macro Fragment Types (Designators)
Tutorial Video
Text description (accessibility)
This video demonstrates the "413: Macro Fragment Types (Designators)" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. `macro_rules!` patterns match different syntactic categories through designators: `$e:expr` matches any expression, `$i:ident` matches an identifier, `$t:ty` matches a type, `$p:pat` matches a pattern, `$b:block` matches a block. Key difference from OCaml: 1. **Token vs. AST**: Rust macros work on token streams; OCaml PPX works on the parsed AST. Rust is more flexible but requires manual parsing; OCaml has structure but requires AST knowledge.
Tutorial
The Problem
macro_rules! patterns match different syntactic categories through designators: $e:expr matches any expression, $i:ident matches an identifier, $t:ty matches a type, $p:pat matches a pattern, $b:block matches a block. Choosing the right designator is critical — expr captures the full expression including operators, while tt captures a single token tree. Misusing designators leads to confusing parse errors or overly restrictive macros that refuse valid inputs.
Understanding fragment types is essential for writing robust macros that accept natural Rust syntax rather than awkward restricted subsets.
🎯 Learning Outcomes
expr, ident, ty, pat, literal, block, stmt, item, meta, tt, lifetimeident enables generating method names, field names, and identifiersty accepts full type expressions including genericstt (token tree) for maximum flexibilityCode Example
macro_rules! dbg_expr {
($e:expr) => {
println!("{} = {:?}", stringify!($e), $e);
};
}
dbg_expr!(2 + 3 * 4); // "2 + 3 * 4 = 14"Key Differences
expr, only ,/;/) can appear); OCaml PPX has no such restrictions.$ident:ident to capture and reuse identifiers; OCaml PPX uses Ast_builder.evar and Ast_builder.pvar to create identifier nodes.$t:ty captures full type syntax; OCaml PPX receives core_type which is already parsed into an AST node.OCaml Approach
OCaml PPX extensions work on the AST directly: [%ppx_gen field_name] receives the parsed Parsetree.expression or Parsetree.pattern value rather than tokens. PPX has direct access to parsed types (core_type), patterns (pattern), and expressions (expression) — more structured than Rust's token-level matching, but requiring knowledge of OCaml's AST types.
Full Source
#![allow(clippy::all)]
//! Macro Fragment Types
//!
//! Different designators: expr, ident, ty, pat, literal, block, tt.
/// Debug expression and return its value.
#[macro_export]
macro_rules! dbg_expr {
($e:expr) => {{
let val = $e;
println!("{} = {:?}", stringify!($e), val);
val
}};
}
/// Generate a getter method.
#[macro_export]
macro_rules! make_getter {
($field:ident : $ty:ty) => {
pub fn $field(&self) -> &$ty {
&self.$field
}
};
}
/// Generate a setter method.
#[macro_export]
macro_rules! make_setter {
($field:ident : $ty:ty) => {
paste::item! {
pub fn [<set_ $field>](&mut self, value: $ty) {
self.$field = value;
}
}
};
}
/// Generate a default function for a type.
#[macro_export]
macro_rules! make_default_fn {
($name:ident -> $ret:ty) => {
pub fn $name() -> $ret {
Default::default()
}
};
}
/// Check if value matches pattern.
#[macro_export]
macro_rules! matches_pattern {
($val:expr, $pat:pat) => {
matches!($val, $pat)
};
}
/// Repeat a literal string.
#[macro_export]
macro_rules! repeat_lit {
($s:literal, $n:literal) => {
$s.repeat($n)
};
}
/// Time a block and return result.
#[macro_export]
macro_rules! timed {
($label:literal, $block:block) => {{
let start = ::std::time::Instant::now();
let result = $block;
let elapsed = start.elapsed();
(result, elapsed)
}};
}
/// Execute statements with prefix/suffix.
#[macro_export]
macro_rules! with_setup {
(setup: $setup:block, body: $body:block, teardown: $teardown:block) => {{
$setup
let result = $body;
$teardown
result
}};
}
/// Create an enum from identifiers.
#[macro_export]
macro_rules! make_enum {
($name:ident { $($variant:ident),* $(,)? }) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum $name {
$($variant,)*
}
};
}
make_enum!(Color { Red, Green, Blue });
make_enum!(Status {
Active,
Inactive,
Pending
});
/// A demo struct to show getter generation.
pub struct Person {
name: String,
age: u32,
}
impl Person {
pub 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_vec -> Vec<i32>);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dbg_expr() {
let v = dbg_expr!(2 + 3);
assert_eq!(v, 5);
}
#[test]
fn test_make_getter() {
let p = Person::new("Alice", 30);
assert_eq!(p.name(), "Alice");
assert_eq!(*p.age(), 30);
}
#[test]
fn test_make_default_fn() {
assert_eq!(default_string(), "");
assert_eq!(default_vec(), Vec::<i32>::new());
}
#[test]
fn test_matches_pattern_some() {
let opt: Option<i32> = Some(42);
assert!(matches_pattern!(opt, Some(_)));
assert!(!matches_pattern!(opt, None));
}
#[test]
fn test_matches_pattern_none() {
let opt: Option<i32> = None;
assert!(matches_pattern!(opt, None));
assert!(!matches_pattern!(opt, Some(_)));
}
#[test]
fn test_repeat_lit() {
assert_eq!(repeat_lit!("ab", 3), "ababab");
assert_eq!(repeat_lit!("x", 5), "xxxxx");
}
#[test]
fn test_timed() {
let (result, _elapsed) = timed!("sum", { (1..=100).sum::<i32>() });
assert_eq!(result, 5050);
}
#[test]
fn test_make_enum() {
assert_eq!(Color::Red, Color::Red);
assert_ne!(Color::Red, Color::Blue);
assert_eq!(Status::Active, Status::Active);
}
#[test]
fn test_with_setup() {
let mut counter = 0;
let result = with_setup!(
setup: { counter += 1; },
body: { counter * 10 },
teardown: { counter += 1; }
);
assert_eq!(result, 10);
assert_eq!(counter, 2);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dbg_expr() {
let v = dbg_expr!(2 + 3);
assert_eq!(v, 5);
}
#[test]
fn test_make_getter() {
let p = Person::new("Alice", 30);
assert_eq!(p.name(), "Alice");
assert_eq!(*p.age(), 30);
}
#[test]
fn test_make_default_fn() {
assert_eq!(default_string(), "");
assert_eq!(default_vec(), Vec::<i32>::new());
}
#[test]
fn test_matches_pattern_some() {
let opt: Option<i32> = Some(42);
assert!(matches_pattern!(opt, Some(_)));
assert!(!matches_pattern!(opt, None));
}
#[test]
fn test_matches_pattern_none() {
let opt: Option<i32> = None;
assert!(matches_pattern!(opt, None));
assert!(!matches_pattern!(opt, Some(_)));
}
#[test]
fn test_repeat_lit() {
assert_eq!(repeat_lit!("ab", 3), "ababab");
assert_eq!(repeat_lit!("x", 5), "xxxxx");
}
#[test]
fn test_timed() {
let (result, _elapsed) = timed!("sum", { (1..=100).sum::<i32>() });
assert_eq!(result, 5050);
}
#[test]
fn test_make_enum() {
assert_eq!(Color::Red, Color::Red);
assert_ne!(Color::Red, Color::Blue);
assert_eq!(Status::Active, Status::Active);
}
#[test]
fn test_with_setup() {
let mut counter = 0;
let result = with_setup!(
setup: { counter += 1; },
body: { counter * 10 },
teardown: { counter += 1; }
);
assert_eq!(result, 10);
assert_eq!(counter, 2);
}
}
Deep Comparison
OCaml vs Rust: Macro Fragment Types
Fragment Specifiers
| Designator | Captures | Example |
|---|---|---|
expr | Expression | 2 + 3, foo() |
ident | Identifier | my_var, MyType |
ty | Type | i32, Vec<String> |
pat | Pattern | Some(x), _ |
literal | Literal | "hello", 42 |
block | Block | { statements } |
stmt | Statement | let x = 1; |
tt | Token tree | Anything |
Examples
expr — Any expression
macro_rules! dbg_expr {
($e:expr) => {
println!("{} = {:?}", stringify!($e), $e);
};
}
dbg_expr!(2 + 3 * 4); // "2 + 3 * 4 = 14"
ident — Identifiers for code generation
macro_rules! make_getter {
($field:ident : $ty:ty) => {
fn $field(&self) -> &$ty { &self.$field }
};
}
struct Point { x: i32, y: i32 }
impl Point {
make_getter!(x: i32);
make_getter!(y: i32);
}
ty — Type names
macro_rules! make_default {
($name:ident -> $ret:ty) => {
fn $name() -> $ret { Default::default() }
};
}
make_default!(empty_vec -> Vec<i32>);
pat — Patterns
macro_rules! matches_pat {
($val:expr, $pat:pat) => { matches!($val, $pat) };
}
matches_pat!(Some(42), Some(_)); // true
OCaml Equivalent
OCaml uses ppx for metaprogramming:
(* No direct equivalent to designators *)
(* ppx_deriving generates code from attributes *)
[@@deriving show, eq]
type point = { x: int; y: int }
5 Takeaways
expr is most common — captures any expression.**Works for math, function calls, variables.
ident enables code generation.**Generate getters, setters, function names.
ty captures type annotations.**Useful for generic macro-generated code.
literal is more restrictive than expr.**Only actual literals, not variables.
tt is the escape hatch.**Captures anything when other designators fail.
Exercises
property!($name:ident : $ty:ty = $default:expr) that generates a struct field, a getter returning &$ty, a setter set_$name(&mut self, val: $ty), and a default value initializer.make_enum_from!(EnumName { Variant1(Type1), Variant2(Type2) }) that generates both From<Type1> for EnumName and From<Type2> for EnumName implementations.typed_dbg!($e:expr) that prints "{expr_text}: {type_name} = {value:?}". Use std::any::type_name::<T>() inside the expansion to show the inferred type name.