#[path = "../syntax.rs"]
mod syntax;
#[path = "../tokens.rs"]
mod tokens;
use std::error::Error;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use proc_macro2::TokenStream;
use syn::Item;
const HEADER: &str =
"// @generated by `cargo run -p m2-syn-macros --bin generate`; do not edit.\n\n";
fn main() -> Result<(), Box<dyn Error>> {
let check = match std::env::args().skip(1).collect::<Vec<_>>().as_slice() {
[] => false,
[argument] if argument == "--check" => true,
_ => return Err("usage: generate [--check]".into()),
};
let repository = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("macro crate is inside the repository")
.to_owned();
let source_path = repository.join("src/nodes.rs");
let source = fs::read_to_string(&source_path)?;
let input = syntax_input(&source, &source_path)?;
let generated = syntax::generate(input)
.map_err(|error| format!("failed to parse syntax schema: {error}"))?;
let directory = repository.join("src/gen");
if !check {
fs::create_dir_all(&directory)?;
}
let mut stale = Vec::new();
for (name, tokens) in generated.files() {
let contents = rustfmt(&format!("{HEADER}{tokens}"))?;
let path = directory.join(name);
if check {
if fs::read_to_string(&path).ok().as_deref() != Some(&contents) {
stale.push(path);
}
} else {
fs::write(path, contents)?;
}
}
if stale.is_empty() {
Ok(())
} else {
let paths = stale
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(", ");
Err(format!("generated files are stale: {paths}").into())
}
}
fn syntax_input(source: &str, path: &Path) -> Result<TokenStream, Box<dyn Error>> {
let file = syn::parse_file(source)
.map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
file.items
.into_iter()
.find_map(|item| match item {
Item::Macro(item) if item.mac.path.is_ident("syntax_schema") => Some(item.mac.tokens),
_ => None,
})
.ok_or_else(|| {
format!(
"{} does not contain a `syntax_schema!` block",
path.display()
)
.into()
})
}
fn rustfmt(source: &str) -> Result<String, Box<dyn Error>> {
let source = source
.replace("macro_rules !", "macro_rules!")
.replace("matches ! (", "matches!(")
.replace("compile_error ! (", "compile_error!(")
.replace("$ crate", "$crate")
.replace(":: m2_syn :: Spanned", "::m2_syn::Spanned")
.replace("Token ! [", "Token![")
.replace("Delimiter ! [", "Delimiter![")
.replace("Delimiter![< | | >]", "Delimiter![<| |>]")
.replace("Token![_ =]", "Token![_=]")
.replace("Token![_ <]", "Token![_<]")
.replace("Token![_ <=]", "Token![_<=]")
.replace("Token![_ >]", "Token![_>]")
.replace("Token![_ >=]", "Token![_>=]")
.replace("Token![| _]", "Token![|_]")
.replace("Token![| _ =]", "Token![|_=]")
.replace("Token![_ !]", "Token![_!]")
.replace("Token![_ *]", "Token![_*]")
.replace("Token![_ ~]", "Token![_~]");
let source = format_macro_rules(&source);
let mut child = Command::new("rustfmt")
.args(["--edition", "2024", "--emit", "stdout"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
child
.stdin
.take()
.expect("piped rustfmt stdin")
.write_all(source.as_bytes())?;
let output = child.wait_with_output()?;
if !output.status.success() {
return Err(format!(
"rustfmt failed: {}",
String::from_utf8_lossy(&output.stderr)
)
.into());
}
Ok(String::from_utf8(output.stdout)?)
}
fn format_macro_rules(source: &str) -> String {
let mut formatted = String::with_capacity(source.len());
let mut cursor = 0;
while let Some(relative_start) = source[cursor..].find("macro_rules!") {
let start = cursor + relative_start;
let Some(relative_opening) = source[start..].find('{') else {
break;
};
let opening = start + relative_opening;
let Some(closing) = matching_brace(source, opening) else {
break;
};
formatted.push_str(&source[cursor..=opening]);
formatted.push('\n');
for arm in macro_arms(&source[opening + 1..closing]) {
formatted.push_str(" ");
formatted.push_str(&normalize_macro_spacing(arm));
formatted.push_str(";\n");
}
formatted.push('}');
cursor = closing + 1;
}
formatted.push_str(&source[cursor..]);
formatted
}
fn matching_brace(source: &str, opening: usize) -> Option<usize> {
let mut depth = 0;
let mut quoted = false;
let mut escaped = false;
for (offset, character) in source[opening..].char_indices() {
if quoted {
escaped = character == '\\' && !escaped;
if character == '"' && !escaped {
quoted = false;
}
if character != '\\' {
escaped = false;
}
continue;
}
if character == '"' {
quoted = true;
} else if character == '{' {
depth += 1;
} else if character == '}' {
depth -= 1;
if depth == 0 {
return Some(opening + offset);
}
}
}
None
}
fn macro_arms(body: &str) -> Vec<&str> {
let mut arms = Vec::new();
let mut start = 0;
let (mut braces, mut brackets, mut parentheses) = (0, 0, 0);
let mut quoted = false;
let mut escaped = false;
for (offset, character) in body.char_indices() {
if quoted {
escaped = character == '\\' && !escaped;
if character == '"' && !escaped {
quoted = false;
}
if character != '\\' {
escaped = false;
}
continue;
}
match character {
'"' => quoted = true,
'{' => braces += 1,
'}' => braces -= 1,
'[' => brackets += 1,
']' => brackets -= 1,
'(' => parentheses += 1,
')' => parentheses -= 1,
';' if braces == 0 && brackets == 0 && parentheses == 0 => {
let arm = body[start..offset].trim();
if !arm.is_empty() {
arms.push(arm);
}
start = offset + 1;
}
_ => {}
}
}
arms
}
fn normalize_macro_spacing(arm: &str) -> String {
arm.replace("$crate :: ", "$crate::")
.replace("$ (", "$(")
.replace("$ spelling", "$spelling")
.replace("$ parse_info", "$parse_info")
.replace("$ unary_action", "$unary_action")
.replace("$ statement_kind", "$statement_kind")
.replace("$ unsupported", "$unsupported")
.replace(" : expr", ":expr")
.replace(" : ident", ":ident")
.replace(" : tt", ":tt")
.replace(") *", ")*")
.replace("compile_error ! (", "compile_error!(")
.replace("[{ }]", "[{}]")
.replace("[< | | >]", "[<| |>]")
.replace("[| _ =]", "[|_=]")
.replace("[_ ", "[_")
.replace(" _]", "_]")
}
#[cfg(test)]
mod tests {
use super::format_macro_rules;
#[test]
fn generated_macro_arms_are_split_and_compact() {
let source = "macro_rules ! Example { [_ =] => { $ crate :: Item } ; \
[$ ($ unsupported : tt) *] => { compile_error ! (\"nope\") } ; }";
assert_eq!(
format_macro_rules(
&source
.replace("macro_rules !", "macro_rules!")
.replace("$ crate", "$crate")
),
"macro_rules! Example {\n [_=] => { $crate::Item };\n \
[$($unsupported:tt)*] => { compile_error!(\"nope\") };\n}"
);
}
}