Custom syntax
So far the tokens inside your macro have been ordinary Rust — a list of expressions. The real
reason to reach for a function-like macro is that the input doesn’t have to be valid Rust at all.
You can invent your own little language and parse it however you like. This is how
html! accepts <div> tags and
the declarative quote! accepts
#interpolation — syntax the Rust grammar has never heard of.
To parse custom syntax you implement Parse
for your own types. A Parse impl is handed a
ParseStream — a cursor over the
remaining tokens — and pulls items off it one at a time, in the order your grammar dictates.
Why not
darlinghere? In chapter 4 you useddarlingto parse a derive’s attributes, and in chapter 6 you’ll use it again for an attribute macro’s arguments. It doesn’t apply to the macros in this chapter, though:darlingonly understands the attribute meta-list grammar (key = value, flags), but a function-like macro receives arbitrary tokens. For custom syntax like=>, custom keywords, or anything that isn’t akey = valuelist, you write aParseimpl by hand — that’s what the rest of this chapter does. The one exception: if your macro’s input is just akey = valuelist, you can reuse theFromMetaapproach from chapter 6 verbatim.
A worked example: hashmap!
Let’s parse a small DSL: a map literal whose entries are written key => value.
#![allow(unused)]
fn main() {
let m = hashmap! {
"one" => 1,
"two" => 1 + 1,
};
}
First, a type for a single entry, with a hand-written Parse impl that spells out the grammar
“an expression, then =>, then another expression”. Both the key and the value are parsed as
Expr — not just string literals — so any expression works on either side, exactly like the
keys and values a real HashMap
accepts:
#![allow(unused)]
fn main() {
use syn::{
Expr, Token,
parse::{Parse, ParseStream},
punctuated::Punctuated,
};
struct Entry {
key: Expr,
value: Expr,
}
impl Parse for Entry {
fn parse(input: ParseStream) -> syn::Result<Self> {
let key: Expr = input.parse()?; // parse the key expression
input.parse::<Token![=>]>()?; // expect and consume `=>`
let value: Expr = input.parse()?; // parse the value expression
Ok(Entry { key, value })
}
}
}
input.parse::<T>() parses one T and advances the cursor; Token![=>] names the => token
type. If the => isn’t there, parse returns an Err and syn turns it into a compiler error
pointing at the offending token — you get good diagnostics for free.
The whole macro is then a comma-separated list of Entry, parsed with the same
parse_terminated you saw for min! (it works for any T: Parse, not just Expr):
#![allow(unused)]
fn main() {
struct Map {
entries: Punctuated<Entry, Token![,]>,
}
impl Parse for Map {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Map { entries: Punctuated::parse_terminated(input)? })
}
}
#[proc_macro]
pub fn hashmap(input: TokenStream) -> TokenStream {
let Map { entries } = parse_macro_input!(input as Map);
let inserts = entries.iter().map(|Entry { key, value }| {
quote! { __map.insert(#key, #value); }
});
quote! {
{
let mut __map = ::std::collections::HashMap::new();
#( #inserts )*
__map
}
}
.into()
}
}
Custom keywords
Entry reused Rust’s own Expr and =>. Often your DSL needs words that look like keywords but
aren’t real ones — asc, desc, state, on. syn won’t recognise them out of the box, so you
declare them with custom_keyword!,
conventionally inside a mod kw. Say you’re parsing a sort direction:
#![allow(unused)]
fn main() {
mod kw {
syn::custom_keyword!(asc);
syn::custom_keyword!(desc);
}
}
Each call generates a token type (kw::asc) you can parse and, crucially, peek. Peeking lets
you look at the next token without consuming it, so you can branch on what comes next:
#![allow(unused)]
fn main() {
// inside a `Parse` impl, deciding which keyword (if any) comes next:
let direction = if input.peek(kw::asc) {
input.parse::<kw::asc>()?; // consume it now that we know it's there
"ASC"
} else if input.peek(kw::desc) {
input.parse::<kw::desc>()?;
"DESC"
} else {
// Nothing we recognise — produce a clear error at the current position.
return Err(input.error("expected `asc` or `desc`"));
};
}
input.error(msg) builds a syn::Error spanned at the cursor; returning it makes misuse a
compile error that points exactly where parsing got stuck. This is the parsing-side equivalent of
the error reporting you did with syn::Error in chapter 4.
Exercise
The exercise for this section is located in 05_function_like/02_custom_syntax
Build a methods! macro for declaring HTTP endpoints:
#![allow(unused)]
fn main() {
let routes = methods! {
get "/",
post "/users",
};
// -> vec![("GET", "/"), ("POST", "/users")]
}
Each entry starts with a custom keyword — get or post — followed by a string-literal path. The
macro expands to a Vec<(&'static str, &'static str)> of (method, path) pairs. The list
parsing, the keyword declarations, and the code generation are written for you; your job is the
Parse impl for a single entry: peek for get/post, consume the right keyword, parse the path,
and reject anything else with a helpful error. The tests/fail case checks that a bad method is a
clean compile error, so don’t forget the else branch.