Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 darling here? In chapter 4 you used darling to 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: darling only 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 a key = value list, you write a Parse impl by hand — that’s what the rest of this chapter does. The one exception: if your macro’s input is just a key = value list, you can reuse the FromMeta approach 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.