Implement Misses DSL compiler (parser + codegen)
- ast.rs: Full AST for .mrs files (MacroDef, Pattern, OutputBody, TemplateItem) - parser.rs: Hand-written recursive descent parser with line/col error reporting - codegen.rs: Transforms AST to Rust proc macro code using syn/quote/proc_macro2 - main.rs: CLI reads .mrs file, runs parse→codegen, writes to stdout or file - Cargo.toml: Workspace with syn, quote, proc-macro2, prettyplease deps - examples/: hello.mrs, derive_builder.mrs, declare_table.mrs reference examples All three reference examples parse and produce prettyplease-formatted Rust output. Generated code uses syn::parse::Parser closures for structural patterns, quote! for token templates, spread iteration via .iter().map().collect(), and format_ident! for identifier construction. https://claude.ai/code/session_01Tppkab4eLsL21asHGjQJoF
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
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.chars().all(|c| c.is_alphanumeric() || c == '_') {
|
||||
stmts.push(format!(
|
||||
"_input.parse::<syn::Token![{tok}]>()?;",
|
||||
tok = tok
|
||||
));
|
||||
} else if tok == "," {
|
||||
stmts.push("_input.parse::<syn::Token![,]>()?;".to_string());
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
/// 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 == '_')
|
||||
}
|
||||
Reference in New Issue
Block a user