๐Ÿฆ€ Functional Rust

426: Function-like Proc Macros

Difficulty: 4 Level: Expert Macros that look like function calls but run at compile time with full AST access โ€” for DSLs, compile-time validation, and code generation from structured input.

The Problem This Solves

`macro_rules!` works on token patterns but can't understand their meaning. `sql!("SELECT * FROM users WHERE id = ?")` needs to parse SQL, validate it against a schema, and generate type-safe query code. `html! { <div class="main">...</div> }` needs to parse HTML syntax and generate `VNode` constructor calls. These require semantic understanding of structured input, not just token shuffling. Function-like proc macros look like `name!(...)` โ€” identical syntax to declarative macros โ€” but the implementation is a Rust function with full access to `syn`, `quote`, and anything else you can import. The entire content inside the parens arrives as a `TokenStream` for you to interpret however you need. Real examples: `regex!("[a-z]+")` compiles the regex at compile time; `include_proto!("schema.proto")` generates Rust types from protobuf; `query!("SELECT id FROM users")` in `sqlx` validates SQL and generates typed structs. All of these are function-like proc macros.

The Intuition

A function-like proc macro receives everything inside `macro!(...)` as a token stream and returns any Rust code โ€” it's a compile-time code generator with full Rust as its implementation language.

How It Works in Rust

// In proc-macro crate:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, LitStr, LitInt};

// Simple: generate a constant from a string literal
#[proc_macro]
pub fn make_greeting(input: TokenStream) -> TokenStream {
 let name = parse_macro_input!(input as LitStr);
 let greeting = format!("Hello, {}!", name.value());
 quote! { #greeting }.into()
}

// Usage: const MSG: &str = make_greeting!("World");
// Expands to: const MSG: &str = "Hello, World!";

// Custom syntax: parse key=value pairs
#[proc_macro]
pub fn config(input: TokenStream) -> TokenStream {
 // Parse custom DSL: config!(host = "localhost", port = 8080)
 // Validate at compile time, generate Config struct construction
 // ...
 input  // simplified
}

// Compile-time regex compilation
#[proc_macro]
pub fn regex(input: TokenStream) -> TokenStream {
 let pattern = parse_macro_input!(input as LitStr);
 let pat = pattern.value();
 // Validate regex at compile time โ€” build error if invalid!
 if let Err(e) = regex_syntax::Parser::new().parse(&pat) {
     return syn::Error::new(pattern.span(), e.to_string())
         .to_compile_error().into();
 }
 quote! { ::regex::Regex::new(#pat).unwrap() }.into()
}
1. `#[proc_macro]` on a public function in a proc-macro crate. 2. Function signature: `pub fn name(input: TokenStream) -> TokenStream`. 3. Parse `input` with `syn` โ€” could be a literal, a custom syntax, or any token sequence. 4. Validate at compile time โ€” emit `syn::Error::to_compile_error()` for user-friendly build errors. 5. Generate output with `quote!`.

What This Unlocks

Key Differences

ConceptOCamlRust
Compile-time DSLCamlp4 / `[%extension_point]` via PPX`#[proc_macro]` function-like macro
Custom syntaxCamlp4 grammar extensionsParse `TokenStream` with `syn`
Compile-time validationPPX with `Location.raise_errorf``syn::Error::new(span, msg).to_compile_error()`
Meta-programming entry pointPPX driver (`ppxlib.runner`)`#[proc_macro]` โ€” registered by crate type
Usage syntax`[%myext ...]``myext!(...)` โ€” indistinguishable from built-ins
//! Function-like Proc Macros
//!
//! Macros invoked like function calls.

/// Example: what sql!("SELECT...") might do
pub fn parse_sql(query: &str) -> Vec<&str> {
    query.split_whitespace().collect()
}

/// Example: what html!(<div>Hello</div>) might generate
pub struct HtmlElement {
    pub tag: String,
    pub content: String,
}

impl HtmlElement {
    pub fn new(tag: &str, content: &str) -> Self {
        HtmlElement {
            tag: tag.to_string(),
            content: content.to_string(),
        }
    }

    pub fn render(&self) -> String {
        format!("<{}>{}</{}>", self.tag, self.content, self.tag)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_sql() {
        let parts = parse_sql("SELECT * FROM users");
        assert_eq!(parts, vec!["SELECT", "*", "FROM", "users"]);
    }

    #[test]
    fn test_html_render() {
        let el = HtmlElement::new("div", "Hello");
        assert_eq!(el.render(), "<div>Hello</div>");
    }

    #[test]
    fn test_html_span() {
        let el = HtmlElement::new("span", "World");
        assert_eq!(el.render(), "<span>World</span>");
    }

    #[test]
    fn test_sql_count() {
        let parts = parse_sql("SELECT COUNT(*) FROM items WHERE active = true");
        assert_eq!(parts.len(), 8);
    }

    #[test]
    fn test_html_empty() {
        let el = HtmlElement::new("br", "");
        assert_eq!(el.render(), "<br></br>");
    }
}
(* Function-like macro concepts in OCaml *)

(* PPX-based custom syntax for SQL-like DSL *)

(* Simulate sql! macro *)
let validate_sql sql =
  (* Simple validation *)
  let required = ["SELECT"; "FROM"] in
  let upper = String.uppercase_ascii sql in
  List.for_all (fun kw -> try
    let _ = Str.search_forward (Str.regexp_string kw) upper 0 in true
    with Not_found -> false) required

(* Simulate html! macro *)
let make_element tag content =
  Printf.sprintf "<%s>%s</%s>" tag content tag

let () =
  let sql = "SELECT id, name FROM users WHERE active = true" in
  if validate_sql sql then
    Printf.printf "Valid SQL: %s\n" sql
  else
    Printf.printf "Invalid SQL!\n";

  let html = make_element "div"
    (make_element "h1" "Hello" ^
     make_element "p" "World") in
  Printf.printf "%s\n" html