Skip to main content

tatara_rust_macro_rules/
lib.rs

1//! `tatara-rust-macro-rules` — L2 authoring surface for **declarative** macros.
2//!
3//! One typed [`MacroRulesSpec`] value → one normal library crate that
4//! exposes `pub macro_rules! my_macro { … }`. No proc-macro plumbing
5//! required; this is a `[lib]` not a `[lib] proc-macro = true`.
6//!
7//! Each arm carries raw matcher + transcriber token text. Authoring
8//! shape:
9//!
10//! ```
11//! use tatara_rust_ast::{CompileToCrate, Ident};
12//! use tatara_rust_macro_rules::{MacroArm, MacroRulesSpec};
13//!
14//! let spec = MacroRulesSpec {
15//!     macro_name: Ident::new("my_vec"),
16//!     arms: vec![MacroArm {
17//!         matcher: "( $($e:expr),* $(,)? )".into(),
18//!         transcriber: "{ ::std::vec![ $($e),* ] }".into(),
19//!     }],
20//! };
21//! let scaffold = spec.compile_to_crate("my-vec-macros").unwrap();
22//! ```
23
24use serde::{Deserialize, Serialize};
25use tatara_rust_ast::{AstError, CompileToCrate, CrateScaffold, Ident};
26
27#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
28pub struct MacroRulesSpec {
29    pub macro_name: Ident,
30    pub arms: Vec<MacroArm>,
31}
32
33#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
34pub struct MacroArm {
35    /// LHS pattern, including the outer delimiter. Raw token text;
36    /// re-parsed by rustc at consumer compile time.
37    pub matcher: String,
38    /// RHS expansion, including the outer delimiter.
39    pub transcriber: String,
40}
41
42impl CompileToCrate for MacroRulesSpec {
43    fn compile_to_crate(&self, crate_name: &str) -> Result<CrateScaffold, AstError> {
44        let mut scaffold = CrateScaffold::new(crate_name, "0.1.0");
45        scaffold.add_file("Cargo.toml", render_cargo_toml(crate_name));
46        scaffold.add_file("src/lib.rs", render_lib_rs(self));
47        Ok(scaffold)
48    }
49}
50
51fn render_cargo_toml(crate_name: &str) -> String {
52    format!(
53        r#"[package]
54name = "{crate_name}"
55version = "0.1.0"
56edition = "2024"
57license = "MIT"
58description = "Declarative-macro crate emitted from a tatara-rust-macro-rules MacroRulesSpec."
59
60[lib]
61"#
62    )
63}
64
65fn render_lib_rs(spec: &MacroRulesSpec) -> String {
66    let mac = &spec.macro_name.0;
67    let arms = spec
68        .arms
69        .iter()
70        .map(|a| format!("    {} => {};", a.matcher, a.transcriber))
71        .collect::<Vec<_>>()
72        .join("\n");
73    format!(
74        r#"// GENERATED by tatara-rust-macro-rules from a MacroRulesSpec.
75#[macro_export]
76macro_rules! {mac} {{
77{arms}
78}}
79"#
80    )
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    fn sample() -> MacroRulesSpec {
88        MacroRulesSpec {
89            macro_name: Ident::new("my_vec"),
90            arms: vec![MacroArm {
91                matcher: "( $($e:expr),* $(,)? )".into(),
92                transcriber: "{ ::std::vec![ $($e),* ] }".into(),
93            }],
94        }
95    }
96
97    #[test]
98    fn compiles_to_lib_and_cargo() {
99        let scaffold = sample().compile_to_crate("my-vec-macros").unwrap();
100        let files = scaffold.to_files();
101        assert!(files.contains_key("Cargo.toml"));
102        assert!(files.contains_key("src/lib.rs"));
103    }
104
105    #[test]
106    fn lib_rs_emits_macro_export() {
107        let scaffold = sample().compile_to_crate("my-vec-macros").unwrap();
108        let lib = scaffold.to_files().get("src/lib.rs").unwrap().clone();
109        assert!(lib.contains("#[macro_export]"));
110        assert!(lib.contains("macro_rules! my_vec"));
111        assert!(lib.contains("( $($e:expr),* $(,)? )"));
112        assert!(lib.contains("::std::vec!"));
113    }
114
115    #[test]
116    fn cargo_toml_is_normal_lib_not_proc_macro() {
117        let scaffold = sample().compile_to_crate("my-vec-macros").unwrap();
118        let toml = scaffold.to_files().get("Cargo.toml").unwrap().clone();
119        // No proc-macro line — declarative macros ship in normal libs.
120        assert!(!toml.contains("proc-macro"));
121    }
122
123    #[test]
124    fn multiple_arms_emit_in_order() {
125        let spec = MacroRulesSpec {
126            macro_name: Ident::new("two"),
127            arms: vec![
128                MacroArm {
129                    matcher: "()".into(),
130                    transcriber: "{ () }".into(),
131                },
132                MacroArm {
133                    matcher: "($e:expr)".into(),
134                    transcriber: "{ $e }".into(),
135                },
136            ],
137        };
138        let scaffold = spec.compile_to_crate("two-macros").unwrap();
139        let lib = scaffold.to_files().get("src/lib.rs").unwrap().clone();
140        assert!(lib.find("()").unwrap() < lib.find("($e:expr)").unwrap());
141    }
142
143    #[test]
144    fn serde_roundtrip() {
145        let s = sample();
146        let j = serde_json::to_string(&s).unwrap();
147        let back: MacroRulesSpec = serde_json::from_str(&j).unwrap();
148        assert_eq!(s, back);
149    }
150}