wandres.dev
MACROS DECLARATIVAS · macro_rules!

Especificadores de fragmento: expr, ident, ty, tt, pat, block

Cada metavariable de una macro declara qué trozo de gramática captura: expr una expresión, ident un nombre, ty un tipo, tt un árbol de tokens, pat un patrón, block un bloque. Elegir el especificador correcto decide qué acepta la macro, cómo se puede continuar el patrón y qué tan opaco queda lo capturado.

⏱ 20 min

Cuando escribes $x:expr, ese :expr no es decoración: es una instrucción para el analizador sintáctico. Le dice qué clase de fragmento gramatical debe leer y empaquetar bajo el nombre $x. Rust ofrece una docena larga de estos especificadores de fragmento, y cada uno captura una categoría distinta del lenguaje —una expresión, un identificador, un tipo, un patrón, un bloque, un simple árbol de tokens—. Elegir el correcto no es un tecnicismo: determina qué sintaxis acepta tu macro, cómo de opaco queda lo capturado y, sorprendentemente, qué tokens pueden aparecer después del fragmento en el patrón. Dominar este vocabulario es dominar la mitad de macro_rules!.

🎯 Al terminar esta lección sabrás
  • Conocer los especificadores clave: expr, ident, ty, tt, pat, block y compañía.
  • Saber qué captura cada uno y en qué contexto de la macro conviene.
  • Entender que un fragmento capturado es un nodo opaco, no una tira de tokens.
  • Comprender las restricciones de continuación y el papel especial de tt.

El vocabulario de especificadores

Un especificador nombra una categoría de la gramática de Rust. Estos son los que se usan a diario:

🧮

expr · ty · block

expr captura una expresión completa (2 + 2, f(x), un if); ty un tipo (Vec<u8>, &str); block un bloque entre llaves. Son los ladrillos de casi toda macro.

🏷️

ident · lifetime · literal

ident captura un identificador o palabra clave (un nombre a secas); lifetime una etiqueta como 'a; literal un literal (42, "hola", true).

🧩

pat · path · stmt

pat captura un patrón de match o let; path una ruta como std::mem::swap; stmt una sentencia suelta.

🌳

tt · item · meta · vis

tt captura un árbol de tokens crudo (el más flexible); item un ítem entero (una fn, un struct); meta el interior de un atributo; vis un calificador de visibilidad, quizá vacío.

Un ejemplo que combina varios para generar una constante con nombre, tipo y valor tomados de la invocación:

macro_rules! definir_constante {
    ($nombre:ident : $tipo:ty = $valor:expr) => {
        const $nombre: $tipo = $valor;
    };
}

definir_constante!(LIMITE : u32 = 1_000);
definir_constante!(SALUDO : &str = "hola");

El $nombre:ident exige exactamente un identificador —por eso puede aparecer a la izquierda de const—, $tipo:ty acepta cualquier tipo bien formado y $valor:expr cualquier expresión. Cada uno valida su fragmento en la frontera de la macro: si escribes definir_constante!(3 : u32 = 1), el error salta ahí mismo, porque 3 no es un ident.

ℹ️
El conjunto de especificadores es cerrado y depende de la edición

No puedes inventar especificadores: la lista la fija el compilador y es cerrada —expr, ident, ty, tt, pat, pat_param, block, path, stmt, literal, lifetime, meta, item y vis—. Y la edición influye en lo que capturan. Además del cambio de pat en 2021, la edición 2024 introduce expr_2021 para preservar el comportamiento antiguo de expr mientras este pasa a aceptar más formas sintácticas. Si una macro tuya deja de compilar al migrar de edición, sospecha de estas fronteras: el mismo :expr no captura exactamente el mismo lenguaje en 2015 que en 2024, y esa diferencia es intencionada.

Un fragmento es un nodo opaco, no tokens sueltos

Aquí está la propiedad que hace seguras a las macros de Rust y que ya asomó con la precedencia. Cuando el analizador captura un fragmento como expr, ty o pat, lo guarda como un nodo del árbol de sintaxis ya analizado, no como la lista de tokens que lo formaban. A partir de ese momento el fragmento es opaco: la macro no puede mirar dentro, ni partirlo, ni reinterpretarlo. Se comporta como una única unidad atómica, y por eso se inserta con su precedencia y su estructura intactas.

Esta opacidad tiene una consecuencia práctica famosa: las restricciones de continuación. Como un expr puede terminar de muchas formas, el analizador necesita saber sin ambigüedad dónde acaba, así que solo permite que le siga un conjunto reducido de tokens: =>, una coma o un punto y coma. Esto no compila:

// error: `$a:expr` no puede ir seguido de otra `expr`
macro_rules! dos_seguidas {
    ($a:expr $b:expr) => { $a + $b };
}

No hay forma de que el analizador sepa dónde termina $a y empieza $b. La solución es separarlos con uno de los tokens permitidos, típicamente una coma:

macro_rules! suma {
    ($a:expr, $b:expr) => { $a + $b };
}

Cada especificador tiene su propio conjunto de continuadores válidos: tras un ty o un pat pueden ir =>, ,, =, |, ; y unos pocos más; tras un ident puede ir casi cualquier cosa, porque un identificador siempre termina limpio. Conocer estas reglas evita horas de perplejidad ante errores del tipo “no está permitido tras una fragmentación de expresión”.

⚠️
pat en 2024 incluye patrones-o; pat_param no

Desde la edición 2021, y por tanto en 2024, :pat captura también patrones con alternativas de primer nivel como A | B. Si necesitas la versión restrictiva que no traga un | superior —por ejemplo, para poner tú mismo el separador | en la repetición—, usa :pat_param. Es un cambio de edición que rompe macros antiguas escritas suponiendo que :pat se detenía en el primer |: si portas código pre-2021, revisa esos patrones.

tt: el comodín que no analiza

tt es el especificador especial. No corresponde a una categoría semántica: captura un único árbol de tokens, que es o bien un token suelto (+, foo, 42) o bien un grupo completo entre (), [] o {} con todo su contenido. Como no impone una interpretación gramatical, casi cualquier cosa puede seguir a un tt, lo que lo convierte en la pieza más flexible del sistema y en la base de dos técnicas centrales: el reenvío de tokens y la recursión de macros (el patrón tt muncher, que iremos viendo).

macro_rules! primero {
    ($primero:tt $($resto:tt)*) => {
        // captura el primer arbol de tokens y descarta el resto
        stringify!($primero)
    };
}

assert_eq!(primero!(a b c), "a");
assert_eq!(primero!((1 + 2) * 3), "(1 + 2)"); // un grupo entre parentesis es UN tt

La flexibilidad de tt es también su debilidad: como no analiza, no valida nada en la frontera y no lleva la protección semántica de un expr. La regla práctica es clara: usa el especificador más específico que sirva —expr, ty, ident— porque da mejores mensajes de error y captura tu intención; reserva tt para cuando de verdad necesitas transportar sintaxis en bruto de una regla a otra.

flowchart TD
inv[Tokens de la invocacion] --> par[El analizador lee segun el especificador]
par --> esp[expr ty pat ident se guardan como nodo opaco]
par --> tt[tt se guarda como arbol de tokens crudo]
esp --> val[Validado en la frontera y con continuacion restringida]
tt --> flex[Casi todo puede seguirle util para reenviar]
style inv fill:#cba6f7,color:#11111b
style esp fill:#a6e3a1,color:#11111b
style tt fill:#f9e2af,color:#11111b
El especificador es un contrato con el analizador sintáctico

Elegir un especificador de fragmento es, sin exagerar, negociar con el analizador sintáctico de Rust en tus propios términos. Cuando escribes :expr, no estás diciendo simplemente “aquí va algo”: estás firmando un contrato según el cual lo que llegue será una expresión válida, será analizado en el sitio de la llamada con toda la maquinaria del compilador, quedará empaquetado como un nodo indivisible y se insertará después con su precedencia y su significado a salvo de cualquier reinterpretación. Ese contrato es lo que distingue a una macro higiénica de un buscar-y-reemplazar. En C no existe tal cosa: el argumento de un #define es un charco de caracteres sin categoría, y toda la disciplina —parentizar, no evaluar dos veces, rezar por el ámbito— recae sobre el programador, que la olvidará tarde o temprano. Rust traslada esa carga al sistema, y el precio de esa comodidad es aprender un vocabulario: saber que ident es lo que puede nombrar una variable o encabezar una definición, que ty es lo que puede seguir a los dos puntos de una anotación, que pat es lo que sabe deconstruir un valor, que block es una expresión de cuerpo, y que tt es la vía de escape para cuando quieres mover sintaxis sin comprometerte a interpretarla. Las restricciones de continuación, que al principio parecen una pedantería del analizador, son en realidad la prueba de que la macro razona sobre gramática y no sobre texto: no puede haber dos expresiones seguidas sin separador porque una expresión, al ser opaca, no dice dónde termina. Interioriza esta idea y las macros dejan de ser conjuros. Cada :algo es una promesa sobre la forma de lo que entra, y esa promesa es exactamente lo que permite que el compilador te ayude en vez de limitarse a pegar tokens y confiar en tu suerte.

⚔️ Domina el vocabulario de fragmentos
  1. Escribe definir_constante! y úsala con tres tipos distintos; luego pásale un número donde va el ident y lee dónde y por qué falla.
  2. Reproduce el error de dos expr seguidas sin separador, y arréglalo con una coma; explica qué restricción de continuación violabas.
  3. Crea una macro que reciba $p:pat y genere un if let $p = valor { ... }; pruébala con un patrón-o como Some(1) | Some(2) y observa que en 2024 se acepta.
  4. Escribe una macro con $b:block que envuelva el bloque en una medición de tiempo y compárala con una que use $e:expr: razona qué acepta cada una.
  5. Usa tt para capturar el primer árbol de tokens de una lista y muestra que (1 + 2) cuenta como uno solo; explica por qué tt no valida como lo haría expr.