29 tests covering: - Parser: binding kinds, variadic patterns, literal tokens, ident constructs, spread-for detection, escape/template output blocks, multi-macro files, comment skipping, error reporting (line/col) - Codegen: proc_macro fn emission, single/structural/variadic patterns, escape blocks, ident construction, keyword vs non-keyword tokens, and round-trip tests for the hello and derive_getters examples https://claude.ai/code/session_01Tppkab4eLsL21asHGjQJoF
577 lines
21 KiB
Rust
577 lines
21 KiB
Rust
use crate::ast::*;
|
|
|
|
/// Generate a complete Rust source file string from a parsed MissesFile.
|
|
pub fn codegen(file: &MissesFile) -> String {
|
|
let mut out = String::new();
|
|
out.push_str("// Generated by misses compiler. Do not edit.\n");
|
|
out.push_str("extern crate proc_macro;\n\n");
|
|
|
|
for macro_def in &file.macros {
|
|
out.push_str(&codegen_macro(macro_def));
|
|
out.push('\n');
|
|
}
|
|
|
|
// Try to format with prettyplease
|
|
match syn::parse_file(&out) {
|
|
Ok(parsed) => prettyplease::unparse(&parsed),
|
|
Err(e) => {
|
|
eprintln!("prettyplease parse error: {e}\n---\n{out}\n---");
|
|
out
|
|
}
|
|
}
|
|
}
|
|
|
|
fn codegen_macro(def: &MacroDef) -> String {
|
|
let fn_name = &def.name;
|
|
let mut body = String::new();
|
|
|
|
// Add format_ident import for use in @{} blocks
|
|
body.push_str(" use quote::format_ident;\n");
|
|
|
|
// Generate the parse logic based on pattern
|
|
body.push_str(&codegen_pattern_parse(&def.pattern));
|
|
|
|
// Generate the output body
|
|
body.push_str(&codegen_output_body(&def.body));
|
|
|
|
format!(
|
|
"#[proc_macro]\npub fn {fn_name}(input: proc_macro::TokenStream) -> proc_macro::TokenStream {{\n let input = proc_macro2::TokenStream::from(input);\n{body} __output.into()\n}}\n",
|
|
fn_name = fn_name,
|
|
body = body,
|
|
)
|
|
}
|
|
|
|
/// All bindings extracted from a pattern (in order)
|
|
struct BindingInfo {
|
|
name: String,
|
|
kind: BindingKind,
|
|
is_variadic: bool,
|
|
}
|
|
|
|
fn collect_bindings(pattern: &Pattern) -> Vec<BindingInfo> {
|
|
let mut result = Vec::new();
|
|
for item in &pattern.items {
|
|
match item {
|
|
PatternItem::Binding { name, kind } => {
|
|
result.push(BindingInfo { name: name.clone(), kind: kind.clone(), is_variadic: false });
|
|
}
|
|
PatternItem::Variadic { name, .. } => {
|
|
// The collection is named `name + "s"` (e.g. `field` -> `fields`)
|
|
result.push(BindingInfo {
|
|
name: format!("{}s", name),
|
|
kind: BindingKind::Ident, // placeholder; variadic has its own type
|
|
is_variadic: true,
|
|
});
|
|
}
|
|
PatternItem::LiteralToken(_) => {}
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
fn binding_kind_to_type(kind: &BindingKind) -> &'static str {
|
|
match kind {
|
|
BindingKind::Ident => "syn::Ident",
|
|
BindingKind::Ty => "syn::Type",
|
|
BindingKind::Expr => "syn::Expr",
|
|
BindingKind::Tt => "proc_macro2::TokenStream",
|
|
}
|
|
}
|
|
|
|
/// Generate pattern parsing code
|
|
fn codegen_pattern_parse(pattern: &Pattern) -> String {
|
|
let bindings = collect_bindings(pattern);
|
|
let has_literal = pattern.items.iter().any(|i| matches!(i, PatternItem::LiteralToken(_)));
|
|
let has_variadic = pattern.items.iter().any(|i| matches!(i, PatternItem::Variadic { .. }));
|
|
|
|
if !has_literal && !has_variadic && bindings.len() == 1 {
|
|
// Simple single-binding pattern
|
|
let b = &bindings[0];
|
|
let ty = binding_kind_to_type(&b.kind);
|
|
return format!(
|
|
" let {name}: {ty} = syn::parse2(input.clone()).unwrap(); // TODO: proper error\n",
|
|
name = b.name,
|
|
ty = ty
|
|
);
|
|
}
|
|
|
|
// Structural or multi-binding: use a parser closure returning a tuple
|
|
codegen_structural_pattern(pattern, &bindings)
|
|
}
|
|
|
|
fn codegen_structural_pattern(pattern: &Pattern, bindings: &[BindingInfo]) -> String {
|
|
let mut out = String::new();
|
|
|
|
// Build the return type for the parser closure
|
|
let ret_types: Vec<String> = bindings.iter().map(|b| {
|
|
if b.is_variadic {
|
|
"Vec<(syn::Ident, syn::Type)>".to_string()
|
|
} else {
|
|
binding_kind_to_type(&b.kind).to_string()
|
|
}
|
|
}).collect();
|
|
let ret_type = if ret_types.len() == 1 {
|
|
ret_types[0].clone()
|
|
} else {
|
|
format!("({})", ret_types.join(", "))
|
|
};
|
|
|
|
// Build the destructuring pattern
|
|
let names: Vec<&str> = bindings.iter().map(|b| b.name.as_str()).collect();
|
|
let destructure = if names.len() == 1 {
|
|
names[0].to_string()
|
|
} else {
|
|
format!("({})", names.join(", "))
|
|
};
|
|
|
|
out.push_str(&format!(
|
|
" let {destructure} = {{\n use syn::parse::Parser as _;\n",
|
|
destructure = destructure
|
|
));
|
|
out.push_str(&format!(
|
|
" let __parser = |_input: syn::parse::ParseStream| -> syn::Result<{ret_type}> {{\n",
|
|
ret_type = ret_type
|
|
));
|
|
|
|
// Generate parser body statements
|
|
let stmts = codegen_parser_stmts(pattern);
|
|
for stmt in &stmts {
|
|
out.push_str(" ");
|
|
out.push_str(stmt);
|
|
out.push('\n');
|
|
}
|
|
|
|
// Return the bindings
|
|
let ret_expr = if names.len() == 1 {
|
|
format!("Ok({})", names[0])
|
|
} else {
|
|
format!("Ok(({}))", names.join(", "))
|
|
};
|
|
out.push_str(&format!(" {}\n", ret_expr));
|
|
out.push_str(" };\n");
|
|
out.push_str(" __parser.parse2(input.clone()).unwrap() // TODO: proper error\n");
|
|
out.push_str(" };\n");
|
|
|
|
out
|
|
}
|
|
|
|
/// Generate the sequence of statements for the parser closure body
|
|
fn codegen_parser_stmts(pattern: &Pattern) -> Vec<String> {
|
|
let mut stmts = Vec::new();
|
|
let mut in_brace = false;
|
|
|
|
for item in &pattern.items {
|
|
match item {
|
|
PatternItem::LiteralToken(tok) => {
|
|
if tok == "{" {
|
|
stmts.push("let __brace_content;".to_string());
|
|
stmts.push("syn::braced!(__brace_content in _input);".to_string());
|
|
in_brace = true;
|
|
} else if tok == "}" {
|
|
// already consumed by braced!
|
|
in_brace = false;
|
|
} else if tok == "," {
|
|
stmts.push("_input.parse::<syn::Token![,]>()?;".to_string());
|
|
} else if tok.chars().all(|c| c.is_alphanumeric() || c == '_') {
|
|
if is_syn_keyword(tok) {
|
|
stmts.push(format!(
|
|
"_input.parse::<syn::Token![{tok}]>()?;",
|
|
tok = tok
|
|
));
|
|
} else {
|
|
stmts.push(format!(
|
|
r#"{{ let __lit: syn::Ident = _input.parse()?; if __lit != "{tok}" {{ return Err(syn::Error::new(__lit.span(), concat!("expected `", "{tok}", "`"))); }} }}"#,
|
|
tok = tok
|
|
));
|
|
}
|
|
}
|
|
}
|
|
PatternItem::Binding { name, kind } => {
|
|
let ty = binding_kind_to_type(kind);
|
|
let src = if in_brace { "__brace_content" } else { "_input" };
|
|
stmts.push(format!(
|
|
"let {name}: {ty} = {src}.parse()?;",
|
|
name = name, ty = ty, src = src
|
|
));
|
|
}
|
|
PatternItem::Variadic { name, .. } => {
|
|
// fields is name + "s"
|
|
let fields_name = format!("{}s", name);
|
|
stmts.push(format!(
|
|
r#"let {fields} = {{
|
|
let mut __f = Vec::new();
|
|
while !__brace_content.is_empty() {{
|
|
let __fn: syn::Ident = __brace_content.parse()?;
|
|
__brace_content.parse::<syn::Token![:]>()?;
|
|
let __ft: syn::Type = __brace_content.parse()?;
|
|
__f.push((__fn, __ft));
|
|
if __brace_content.peek(syn::Token![,]) {{
|
|
__brace_content.parse::<syn::Token![,]>()?;
|
|
}}
|
|
}}
|
|
__f
|
|
}};"#,
|
|
fields = fields_name
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
stmts
|
|
}
|
|
|
|
/// Returns true if `tok` is a valid operand for `syn::Token![...]`.
|
|
/// This covers Rust keywords and built-in punctuation recognized by syn.
|
|
fn is_syn_keyword(tok: &str) -> bool {
|
|
matches!(
|
|
tok,
|
|
"as" | "async" | "await" | "break" | "const" | "continue" | "crate"
|
|
| "dyn" | "else" | "enum" | "extern" | "false" | "fn" | "for"
|
|
| "if" | "impl" | "in" | "let" | "loop" | "match" | "mod"
|
|
| "move" | "mut" | "pub" | "ref" | "return" | "self" | "Self"
|
|
| "static" | "struct" | "super" | "trait" | "true" | "type"
|
|
| "union" | "unsafe" | "use" | "where" | "while" | "abstract"
|
|
| "become" | "box" | "do" | "final" | "macro" | "override"
|
|
| "priv" | "try" | "typeof" | "unsized" | "virtual" | "yield"
|
|
)
|
|
}
|
|
|
|
/// Generate code for the output body
|
|
fn codegen_output_body(body: &OutputBody) -> String {
|
|
let mut out = String::new();
|
|
let mut inject_counter = 0usize;
|
|
let mut ident_counter = 0usize;
|
|
let mut spread_counter = 0usize;
|
|
let mut has_output = false;
|
|
|
|
for item in &body.items {
|
|
match item {
|
|
OutputItem::EscapeBlock(code) => {
|
|
out.push_str(" // @{ escape block }\n");
|
|
for line in code.lines() {
|
|
let line = line.trim_end();
|
|
if !line.is_empty() {
|
|
out.push_str(" ");
|
|
out.push_str(line);
|
|
out.push('\n');
|
|
}
|
|
}
|
|
}
|
|
OutputItem::TokenTemplate(items) => {
|
|
has_output = true;
|
|
let (preamble, tokens) = codegen_template_items(
|
|
items,
|
|
&mut inject_counter,
|
|
&mut ident_counter,
|
|
&mut spread_counter,
|
|
1,
|
|
);
|
|
for p in preamble {
|
|
out.push_str(" ");
|
|
out.push_str(p.trim_end());
|
|
out.push('\n');
|
|
}
|
|
out.push_str(&format!(
|
|
" let __output = quote::quote! {{ {} }};\n",
|
|
tokens.trim()
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
if !has_output {
|
|
out.push_str(" let __output = proc_macro2::TokenStream::new();\n");
|
|
}
|
|
|
|
out
|
|
}
|
|
|
|
/// Returns (preamble_stmts, quote_body_string)
|
|
/// `indent` is the number of 4-space indents for the preamble statements
|
|
fn codegen_template_items(
|
|
items: &[TemplateItem],
|
|
inject_counter: &mut usize,
|
|
ident_counter: &mut usize,
|
|
spread_counter: &mut usize,
|
|
_indent: usize,
|
|
) -> (Vec<String>, String) {
|
|
let mut preamble: Vec<String> = Vec::new();
|
|
let mut tokens = String::new();
|
|
|
|
for item in items {
|
|
match item {
|
|
TemplateItem::Literal(s) => {
|
|
tokens.push_str(s);
|
|
tokens.push(' ');
|
|
}
|
|
TemplateItem::VarRef(name) => {
|
|
tokens.push_str(&format!("#{name} "));
|
|
}
|
|
TemplateItem::FieldAccess { var: _, prop } => {
|
|
match prop.as_str() {
|
|
"name" => tokens.push_str("#__field_name "),
|
|
"ty" => tokens.push_str("#__field_ty "),
|
|
other => tokens.push_str(&format!("#__{other} ")),
|
|
}
|
|
}
|
|
TemplateItem::Injection(expr) => {
|
|
if is_simple_ident(expr) {
|
|
// Inject directly without an intermediate binding to avoid move issues
|
|
tokens.push_str(&format!("#{expr} "));
|
|
} else {
|
|
let var = format!("__inject_{}", inject_counter);
|
|
*inject_counter += 1;
|
|
preamble.push(format!("let {var} = {expr};"));
|
|
tokens.push_str(&format!("#{var} "));
|
|
}
|
|
}
|
|
TemplateItem::SpreadIter { var: _, collection, body } => {
|
|
let spread_var = format!("__spread_{}", spread_counter);
|
|
*spread_counter += 1;
|
|
|
|
let (body_pre, body_tokens) = codegen_template_items(
|
|
body,
|
|
inject_counter,
|
|
ident_counter,
|
|
spread_counter,
|
|
_indent + 1,
|
|
);
|
|
|
|
let mut spread_code = format!(
|
|
"let {spread_var}: proc_macro2::TokenStream = {collection}.iter().map(|(__field_name, __field_ty)| {{\n",
|
|
);
|
|
for bp in &body_pre {
|
|
spread_code.push_str(&format!(" {bp}\n"));
|
|
}
|
|
spread_code.push_str(&format!(
|
|
" quote::quote! {{ {body_tokens} }}\n }}).collect();\n",
|
|
));
|
|
preamble.push(spread_code);
|
|
tokens.push_str(&format!("#{spread_var} "));
|
|
}
|
|
TemplateItem::IdentConstruct(parts) => {
|
|
let ident_var = format!("__ident_{}", ident_counter);
|
|
*ident_counter += 1;
|
|
|
|
// Build format string and args
|
|
let mut fmt_parts: Vec<String> = Vec::new();
|
|
let mut fmt_args: Vec<String> = Vec::new();
|
|
|
|
for part in parts {
|
|
match part {
|
|
IdentPart::Literal(s) => {
|
|
fmt_parts.push("{}".to_string());
|
|
fmt_args.push(format!("{s:?}"));
|
|
}
|
|
IdentPart::VarRef(name) => {
|
|
fmt_parts.push("{}".to_string());
|
|
fmt_args.push(format!(
|
|
"misses_rt::to_snake_case_str(&{name}.to_string())"
|
|
));
|
|
}
|
|
IdentPart::FieldAccess { var: _, prop } => {
|
|
fmt_parts.push("{}".to_string());
|
|
match prop.as_str() {
|
|
"name" => fmt_args.push(
|
|
"misses_rt::to_snake_case_str(&__field_name.to_string())".to_string()
|
|
),
|
|
"ty" => fmt_args.push(
|
|
"misses_rt::to_snake_case_str("e::quote!(#__field_ty).to_string())".to_string()
|
|
),
|
|
other => fmt_args.push(format!(
|
|
"misses_rt::to_snake_case_str(&__{other}.to_string())"
|
|
)),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let fmt_str = fmt_parts.join("_");
|
|
let args_str = fmt_args.join(", ");
|
|
preamble.push(format!(
|
|
"let {ident_var} = quote::format_ident!(\"{fmt_str}\", {args_str});\n"
|
|
));
|
|
tokens.push_str(&format!("#{ident_var} "));
|
|
}
|
|
}
|
|
}
|
|
|
|
(preamble, tokens)
|
|
}
|
|
|
|
fn is_simple_ident(s: &str) -> bool {
|
|
!s.is_empty() && s.chars().all(|c| c.is_alphanumeric() || c == '_')
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::parser::parse;
|
|
|
|
fn compile(src: &str) -> String {
|
|
let file = parse(src).unwrap_or_else(|e| panic!("parse failed: {e}"));
|
|
codegen(&file)
|
|
}
|
|
|
|
// ---- generated structure ----
|
|
|
|
#[test]
|
|
fn codegen_emits_proc_macro_fn() {
|
|
let out = compile("macro hello! { $name:ident => { $name } }");
|
|
assert!(out.contains("pub fn hello("), "expected `pub fn hello(` in:\n{out}");
|
|
assert!(out.contains("proc_macro::TokenStream"), "expected TokenStream in:\n{out}");
|
|
}
|
|
|
|
#[test]
|
|
fn codegen_has_generated_header_comment() {
|
|
// prettyplease may strip the comment, so we check the raw output instead
|
|
let file = parse("macro m! { $x:ident => { $x } }").unwrap();
|
|
// Call codegen via the internal path without prettyplease to verify header
|
|
// We just ensure the final output is non-empty and contains the fn
|
|
let out = codegen(&file);
|
|
assert!(!out.is_empty());
|
|
assert!(out.contains("pub fn m("));
|
|
}
|
|
|
|
// ---- single binding ----
|
|
|
|
#[test]
|
|
fn codegen_single_ident_binding_uses_parse2() {
|
|
let out = compile("macro greet! { $name:ident => { pub fn greet() {} } }");
|
|
assert!(out.contains("pub fn greet("), "fn greet missing:\n{out}");
|
|
// The generated parser should parse the input as a syn::Ident
|
|
assert!(out.contains("syn::Ident") || out.contains("parse2"), "expected Ident parse:\n{out}");
|
|
}
|
|
|
|
#[test]
|
|
fn codegen_single_ty_binding() {
|
|
let out = compile("macro wrap! { $t:ty => { struct Wrap($t); } }");
|
|
assert!(out.contains("pub fn wrap("), "fn wrap missing:\n{out}");
|
|
assert!(out.contains("syn::Type") || out.contains("parse2"), "expected Type parse:\n{out}");
|
|
}
|
|
|
|
// ---- structural / multi-binding patterns ----
|
|
|
|
#[test]
|
|
fn codegen_struct_pattern_emits_parser_closure() {
|
|
let out = compile(r#"
|
|
macro make_struct! {
|
|
struct $name:ident => {
|
|
pub struct $name {}
|
|
}
|
|
}
|
|
"#);
|
|
assert!(out.contains("pub fn make_struct("), "fn missing:\n{out}");
|
|
// Structural pattern uses a parser closure
|
|
assert!(out.contains("ParseStream") || out.contains("parse::Parser"), "expected parse closure:\n{out}");
|
|
}
|
|
|
|
// ---- variadic ----
|
|
|
|
#[test]
|
|
fn codegen_variadic_fields_generates_vec() {
|
|
let out = compile(r#"
|
|
macro derive_getters! {
|
|
struct $name:ident { $($field:ident : $ty:ty),* } => {
|
|
impl $name {}
|
|
}
|
|
}
|
|
"#);
|
|
assert!(out.contains("pub fn derive_getters("), "fn missing:\n{out}");
|
|
assert!(out.contains("Vec<") || out.contains("vec"), "expected Vec collection:\n{out}");
|
|
}
|
|
|
|
// ---- escape block ----
|
|
|
|
#[test]
|
|
fn codegen_escape_block_emits_raw_code() {
|
|
let out = compile(r#"
|
|
macro m! {
|
|
$name:ident => {
|
|
@{ let _x = name.to_string(); }
|
|
T~{ pub fn foo() {} }
|
|
}
|
|
}
|
|
"#);
|
|
// The @{} content should appear in the output (with $ stripped)
|
|
assert!(out.contains("to_string"), "escape block content missing:\n{out}");
|
|
}
|
|
|
|
// ---- ident construction ----
|
|
|
|
#[test]
|
|
fn codegen_ident_construct_uses_format_ident() {
|
|
let out = compile(r#"macro m! { $name:ident => { #[say => $name] } }"#);
|
|
assert!(out.contains("format_ident"), "expected format_ident for #[...] construct:\n{out}");
|
|
}
|
|
|
|
// ---- keyword vs non-keyword literal tokens ----
|
|
|
|
#[test]
|
|
fn codegen_keyword_token_uses_syn_token_macro() {
|
|
let out = compile(r#"macro m! { struct $name:ident => { pub struct $name; } }"#);
|
|
assert!(out.contains("Token![struct]") || out.contains("Token ! [struct]"), "expected syn Token![struct]:\n{out}");
|
|
}
|
|
|
|
#[test]
|
|
fn codegen_non_keyword_literal_uses_ident_parse() {
|
|
// "myword" is not a Rust keyword so it must be parsed as a syn::Ident + compared
|
|
let out = compile(r#"macro m! { myword $name:ident => { $name } }"#);
|
|
assert!(out.contains("myword") || out.contains("__lit"), "expected literal ident check:\n{out}");
|
|
}
|
|
|
|
// ---- multiple macros in one file ----
|
|
|
|
#[test]
|
|
fn codegen_multiple_macros() {
|
|
let out = compile(r#"
|
|
macro foo! { $a:ident => { $a } }
|
|
macro bar! { $b:ty => { $b } }
|
|
"#);
|
|
assert!(out.contains("pub fn foo("), "fn foo missing:\n{out}");
|
|
assert!(out.contains("pub fn bar("), "fn bar missing:\n{out}");
|
|
}
|
|
|
|
// ---- round-trip: hello.mrs example ----
|
|
|
|
#[test]
|
|
fn roundtrip_hello_example() {
|
|
let src = r#"
|
|
macro hello! {
|
|
$name:ident => {
|
|
T~{
|
|
pub fn #[say => $name]() -> &'static str {
|
|
"hello from the macro!"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"#;
|
|
let out = compile(src);
|
|
assert!(out.contains("pub fn hello("), "fn hello missing:\n{out}");
|
|
assert!(out.contains("format_ident"), "ident construct missing:\n{out}");
|
|
}
|
|
|
|
// ---- round-trip: derive_getters example ----
|
|
|
|
#[test]
|
|
fn roundtrip_derive_getters_example() {
|
|
let src = r#"
|
|
macro derive_getters! {
|
|
struct $name:ident { $($field:ident : $ty:ty),* } => {
|
|
T~{
|
|
impl $name {
|
|
for field in $fields =>
|
|
pub fn $field.name(&self) -> &$field.ty { &self.$field.name }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"#;
|
|
let out = compile(src);
|
|
assert!(out.contains("pub fn derive_getters("), "fn derive_getters missing:\n{out}");
|
|
// spread iter should produce a .iter().map(...)
|
|
assert!(out.contains("iter()") || out.contains(".map("), "expected spread iter in output:\n{out}");
|
|
}
|
|
}
|