commit ed62264870e9e1a0bfe4569633b11fa5752c08f4 Author: Claude Date: Sat Mar 7 11:37:58 2026 +0000 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5b5a2ae --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,58 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "misses" +version = "0.1.0" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d0af0f8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["misses-compiler"] +resolver = "2" diff --git a/examples/declare_table.mrs b/examples/declare_table.mrs new file mode 100644 index 0000000..a0b325b --- /dev/null +++ b/examples/declare_table.mrs @@ -0,0 +1,15 @@ +macro declare_table! { + $name:ident { $body:tt } => { + @{ + let table_str = misses_rt::to_table_name(&name); + } + T~{ + pub struct $name { + pub id: u64, + } + impl $name { + pub const TABLE: &'static str = ${ table_str.as_str() }; + } + } + } +} diff --git a/examples/derive_builder.mrs b/examples/derive_builder.mrs new file mode 100644 index 0000000..a1d5de0 --- /dev/null +++ b/examples/derive_builder.mrs @@ -0,0 +1,21 @@ +macro derive_builder! { + struct $name:ident { $($field:ident : $ty:ty),* } => { + @{ + let builder_name = format_ident!("{}_Builder", name.to_string()); + } + T~{ + pub struct ${ builder_name } { + for field in $fields => + pub $field.name: Option<$field.ty>, + } + impl ${ builder_name } { + pub fn build(self) -> $name { + $name { + for field in $fields => + $field.name: self.$field.name.unwrap(), + } + } + } + } + } +} diff --git a/examples/hello.mrs b/examples/hello.mrs new file mode 100644 index 0000000..f4bc66b --- /dev/null +++ b/examples/hello.mrs @@ -0,0 +1,9 @@ +macro hello! { + $name:ident => { + T~{ + pub fn #[say => hello]() -> &'static str { + ${ format!("hello, {}!", stringify!(name)) } + } + } + } +} diff --git a/misses-compiler/Cargo.toml b/misses-compiler/Cargo.toml new file mode 100644 index 0000000..e4f9164 --- /dev/null +++ b/misses-compiler/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "misses" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "misses" +path = "src/main.rs" + +[dependencies] +syn = { version = "2", features = ["full", "extra-traits"] } +quote = "1" +proc-macro2 = "1" +prettyplease = "0.2" diff --git a/misses-compiler/src/ast.rs b/misses-compiler/src/ast.rs new file mode 100644 index 0000000..43ff167 --- /dev/null +++ b/misses-compiler/src/ast.rs @@ -0,0 +1,89 @@ +/// Top-level representation of a parsed .mrs file +#[derive(Debug, Clone)] +pub struct MissesFile { + pub macros: Vec, +} + +/// A single macro definition: `macro name! { pattern => { body } }` +#[derive(Debug, Clone)] +pub struct MacroDef { + pub name: String, + pub pattern: Pattern, + pub body: OutputBody, +} + +/// The pattern (left-hand side of =>) +#[derive(Debug, Clone)] +pub struct Pattern { + pub items: Vec, +} + +#[derive(Debug, Clone)] +pub enum PatternItem { + /// `$name:ident`, `$name:ty`, `$name:expr`, `$name:tt` + Binding { name: String, kind: BindingKind }, + /// `$($field:ident : $ty:ty),*` + Variadic { name: String, inner: Vec }, + /// A literal token in the pattern (e.g. `struct`, `{`, `}`, `,`, `:`) + LiteralToken(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum BindingKind { + Ident, + Ty, + Expr, + Tt, +} + +/// One binding inside a variadic `$($field:ident : $ty:ty),*` +#[derive(Debug, Clone)] +pub struct VariadicBinding { + pub name: String, + pub kind: BindingKind, +} + +/// The output body: a sequence of escape blocks and token templates +#[derive(Debug, Clone)] +pub struct OutputBody { + pub items: Vec, +} + +#[derive(Debug, Clone)] +pub enum OutputItem { + /// `@{ ... }` — raw Rust code (with $ vars rewritten to plain vars) + EscapeBlock(String), + /// `T~{ ... }` — token template (quote!) + TokenTemplate(Vec), +} + +/// Items inside a T~{ } block (or the implicit top-level template) +#[derive(Debug, Clone)] +pub enum TemplateItem { + /// Literal Rust-looking code (passthrough into quote!) + Literal(String), + /// `$name` — reference to a pattern variable + VarRef(String), + /// `$field.prop` — field access on a variadic var + FieldAccess { var: String, prop: String }, + /// `${ expr }` — expression injection + Injection(String), + /// `for var in $fields => template` + SpreadIter { + var: String, + collection: String, + body: Vec, + }, + /// `#[part1 => part2 => ...]` — identifier construction + IdentConstruct(Vec), +} + +#[derive(Debug, Clone)] +pub enum IdentPart { + /// A literal string token + Literal(String), + /// `$name` — a pattern variable + VarRef(String), + /// `$name.prop` — field access + FieldAccess { var: String, prop: String }, +} diff --git a/misses-compiler/src/codegen.rs b/misses-compiler/src/codegen.rs new file mode 100644 index 0000000..648ee39 --- /dev/null +++ b/misses-compiler/src/codegen.rs @@ -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 { + 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 = 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 { + 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::()?;", + tok = tok + )); + } else if tok == "," { + stmts.push("_input.parse::()?;".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::()?; + let __ft: syn::Type = __brace_content.parse()?; + __f.push((__fn, __ft)); + if __brace_content.peek(syn::Token![,]) {{ + __brace_content.parse::()?; + }} + }} + __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) { + let mut preamble: Vec = 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 = Vec::new(); + let mut fmt_args: Vec = 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 == '_') +} diff --git a/misses-compiler/src/main.rs b/misses-compiler/src/main.rs new file mode 100644 index 0000000..d225165 --- /dev/null +++ b/misses-compiler/src/main.rs @@ -0,0 +1,45 @@ +mod ast; +mod parser; +mod codegen; + +use std::path::PathBuf; + +fn main() { + let args: Vec = std::env::args().collect(); + if args.len() < 2 { + eprintln!("Usage: misses [output.rs]"); + std::process::exit(1); + } + + let input_path = PathBuf::from(&args[1]); + let src = match std::fs::read_to_string(&input_path) { + Ok(s) => s, + Err(e) => { + eprintln!("Error reading {:?}: {}", input_path, e); + std::process::exit(1); + } + }; + + let ast = match parser::parse(&src) { + Ok(a) => a, + Err(e) => { + eprintln!("Parse error: {}", e); + std::process::exit(1); + } + }; + + let generated = codegen::codegen(&ast); + + if args.len() >= 3 { + let output_path = PathBuf::from(&args[2]); + match std::fs::write(&output_path, &generated) { + Ok(_) => {} + Err(e) => { + eprintln!("Error writing {:?}: {}", output_path, e); + std::process::exit(1); + } + } + } else { + print!("{}", generated); + } +} diff --git a/misses-compiler/src/parser.rs b/misses-compiler/src/parser.rs new file mode 100644 index 0000000..8869d24 --- /dev/null +++ b/misses-compiler/src/parser.rs @@ -0,0 +1,516 @@ +use crate::ast::*; + +#[derive(Debug)] +pub struct ParseError { + pub message: String, + pub line: usize, + pub col: usize, +} + +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "parse error at {}:{}: {}", self.line, self.col, self.message) + } +} + +pub struct Parser<'a> { + src: &'a str, + pos: usize, +} + +impl<'a> Parser<'a> { + fn new(src: &'a str) -> Self { + Parser { src, pos: 0 } + } + + fn line_col(&self) -> (usize, usize) { + let before = &self.src[..self.pos]; + let line = before.chars().filter(|&c| c == '\n').count() + 1; + let col = before.rfind('\n').map(|i| self.pos - i).unwrap_or(self.pos + 1); + (line, col) + } + + fn err(&self, msg: impl Into) -> ParseError { + let (line, col) = self.line_col(); + ParseError { message: msg.into(), line, col } + } + + fn rest(&self) -> &str { + &self.src[self.pos..] + } + + fn peek(&self) -> Option { + self.rest().chars().next() + } + + fn advance(&mut self, n: usize) { + self.pos += n; + } + + fn skip_whitespace_and_comments(&mut self) { + loop { + // skip whitespace + let ws: usize = self.rest().chars().take_while(|c| c.is_whitespace()).map(|c| c.len_utf8()).sum(); + if ws > 0 { + self.advance(ws); + continue; + } + // skip line comments + if self.rest().starts_with("//") { + let end = self.rest().find('\n').map(|i| i + 1).unwrap_or(self.rest().len()); + self.advance(end); + continue; + } + break; + } + } + + fn expect_str(&mut self, s: &str) -> Result<(), ParseError> { + self.skip_whitespace_and_comments(); + if self.rest().starts_with(s) { + self.advance(s.len()); + Ok(()) + } else { + Err(self.err(format!("expected {:?}, got {:?}", s, &self.rest()[..s.len().min(self.rest().len())]))) + } + } + + fn peek_str(&self, s: &str) -> bool { + self.rest().starts_with(s) + } + + fn peek_str_skip_ws(&mut self, s: &str) -> bool { + let saved = self.pos; + self.skip_whitespace_and_comments(); + let result = self.rest().starts_with(s); + self.pos = saved; + result + } + + fn parse_ident(&mut self) -> Result { + self.skip_whitespace_and_comments(); + let rest = self.rest(); + let len: usize = rest.chars() + .take_while(|&c| c.is_alphanumeric() || c == '_') + .map(|c| c.len_utf8()) + .sum(); + if len == 0 { + return Err(self.err(format!("expected identifier, got {:?}", &rest[..rest.len().min(10)]))); + } + let ident = rest[..len].to_string(); + self.advance(len); + Ok(ident) + } + + /// Consume a block `{ ... }` returning the inner content (brace-depth-aware) + fn consume_braced_block(&mut self) -> Result { + self.skip_whitespace_and_comments(); + if !self.rest().starts_with('{') { + return Err(self.err(format!("expected '{{', got {:?}", &self.rest()[..self.rest().len().min(5)]))); + } + self.advance(1); // consume { + let start = self.pos; + let mut depth = 1usize; + let mut i = self.pos; + let bytes = self.src.as_bytes(); + while i < self.src.len() { + match bytes[i] { + b'{' => { depth += 1; i += 1; } + b'}' => { + depth -= 1; + if depth == 0 { + let inner = self.src[start..i].to_string(); + self.pos = i + 1; + return Ok(inner); + } + i += 1; + } + _ => { i += 1; } + } + } + Err(self.err("unclosed '{'")) + } + + /// Parse `$name:kind` binding + fn parse_binding(&mut self) -> Result { + // already consumed $ + let name = self.parse_ident()?; + self.skip_whitespace_and_comments(); + if !self.rest().starts_with(':') { + return Err(self.err("expected ':' after binding name")); + } + self.advance(1); + let kind_str = self.parse_ident()?; + let kind = match kind_str.as_str() { + "ident" => BindingKind::Ident, + "ty" => BindingKind::Ty, + "expr" => BindingKind::Expr, + "tt" => BindingKind::Tt, + other => return Err(self.err(format!("unknown binding kind {:?}", other))), + }; + Ok(PatternItem::Binding { name, kind }) + } + + /// Parse `$($field:ident : $ty:ty),*` — called after consuming `$(` + fn parse_variadic(&mut self) -> Result { + // We need to parse inner bindings until we see `)` then `,*` + let mut inner: Vec = Vec::new(); + let mut name_for_variadic: Option = None; + + loop { + self.skip_whitespace_and_comments(); + if self.rest().starts_with(')') { + self.advance(1); + break; + } + if self.rest().starts_with('$') { + self.advance(1); + let vname = self.parse_ident()?; + if name_for_variadic.is_none() { + name_for_variadic = Some(vname.clone()); + } + self.skip_whitespace_and_comments(); + self.expect_str(":")?; + let kind_str = self.parse_ident()?; + let kind = match kind_str.as_str() { + "ident" => BindingKind::Ident, + "ty" => BindingKind::Ty, + "expr" => BindingKind::Expr, + "tt" => BindingKind::Tt, + other => return Err(self.err(format!("unknown binding kind {:?}", other))), + }; + inner.push(VariadicBinding { name: vname, kind }); + } else { + // literal separator inside variadic (e.g. `:`) + let c = self.peek().ok_or_else(|| self.err("unexpected EOF in variadic"))?; + self.advance(c.len_utf8()); + } + } + + // expect `,*` + self.skip_whitespace_and_comments(); + if self.rest().starts_with(",*") { + self.advance(2); + } else if self.rest().starts_with('*') { + self.advance(1); + } + + let name = name_for_variadic.unwrap_or_else(|| "fields".to_string()); + Ok(PatternItem::Variadic { name, inner }) + } + + /// Parse the pattern (everything before `=>` inside the macro body) + fn parse_pattern(&mut self) -> Result { + let mut items = Vec::new(); + + loop { + self.skip_whitespace_and_comments(); + let rest = self.rest(); + + // Check for => terminator + if rest.starts_with("=>") { + break; + } + + if rest.is_empty() { + return Err(self.err("unexpected EOF in pattern")); + } + + if rest.starts_with("$(") { + // variadic + self.advance(2); + items.push(self.parse_variadic()?); + } else if rest.starts_with('$') { + self.advance(1); + items.push(self.parse_binding()?); + } else { + // literal token: collect until whitespace or $ or => + let len: usize = rest.chars() + .take_while(|&c| !c.is_whitespace() && c != '$') + .map(|c| c.len_utf8()) + .sum(); + // check if it starts with => + let tok = &rest[..len]; + if tok.is_empty() { + break; + } + // Check if this token starts with => + if tok.starts_with("=>") { + break; + } + items.push(PatternItem::LiteralToken(tok.to_string())); + self.advance(len); + } + } + + Ok(Pattern { items }) + } + + /// Parse content of an @{} block: raw Rust with $var references + fn parse_escape_block_content(&mut self, content: &str) -> String { + // Just strip leading/trailing whitespace; $vars in @{} become plain vars + // We rewrite $name -> name at this stage + rewrite_dollar_vars(content) + } + + /// Parse `#[part => part => ...]` ident construction + fn parse_ident_construct(&mut self) -> Result { + // Already consumed `#[` + let mut parts = Vec::new(); + loop { + self.skip_whitespace_and_comments(); + if self.rest().starts_with(']') { + self.advance(1); + break; + } + if self.rest().starts_with("=>") { + self.advance(2); + continue; + } + if self.rest().starts_with('$') { + self.advance(1); + let var = self.parse_ident()?; + if self.rest().starts_with('.') { + self.advance(1); + let prop = self.parse_ident()?; + parts.push(IdentPart::FieldAccess { var, prop }); + } else { + parts.push(IdentPart::VarRef(var)); + } + } else { + // literal identifier token + let len: usize = self.rest().chars() + .take_while(|&c| !c.is_whitespace() && c != '=' && c != ']' && c != '$') + .map(|c| c.len_utf8()) + .sum(); + if len == 0 { + let c = self.peek().ok_or_else(|| self.err("unexpected EOF in #[...]"))?; + return Err(self.err(format!("unexpected char {:?} in #[...]", c))); + } + let lit = self.rest()[..len].to_string(); + self.advance(len); + parts.push(IdentPart::Literal(lit)); + } + } + Ok(TemplateItem::IdentConstruct(parts)) + } + + /// Parse content inside T~{ } — returns Vec + fn parse_template_items(&mut self, content: &str) -> Result, ParseError> { + let mut sub = Parser::new(content); + let mut items = Vec::new(); + let mut literal_buf = String::new(); + + macro_rules! flush_literal { + () => { + if !literal_buf.is_empty() { + items.push(TemplateItem::Literal(literal_buf.trim_end().to_string())); + literal_buf.clear(); + } + }; + } + + loop { + let rest = sub.rest(); + if rest.is_empty() { + break; + } + + // for $var in $collection => template (spread iteration) + if rest.trim_start().starts_with("for ") { + // flush + flush_literal!(); + sub.skip_whitespace_and_comments(); + sub.advance(4); // "for " + sub.skip_whitespace_and_comments(); + let var = sub.parse_ident()?; + sub.skip_whitespace_and_comments(); + sub.expect_str("in")?; + sub.skip_whitespace_and_comments(); + sub.expect_str("$")?; + let collection = sub.parse_ident()?; + sub.skip_whitespace_and_comments(); + sub.expect_str("=>")?; + // rest of line is the body (until newline or closing }) + sub.skip_whitespace_and_comments(); + let body_start = sub.pos; + // collect until next newline that isn't inside braces + let body_src = { + let r = sub.rest(); + // find end of line + let end = r.find('\n').unwrap_or(r.len()); + &r[..end] + }; + let body_items = self.parse_template_items(body_src)?; + sub.advance(body_src.len()); + items.push(TemplateItem::SpreadIter { + var, + collection, + body: body_items, + }); + continue; + } + + // #[ ident construct + if rest.starts_with("#[") { + flush_literal!(); + sub.advance(2); + items.push(sub.parse_ident_construct()?); + continue; + } + + // ${ expr } injection + if rest.starts_with("${") { + flush_literal!(); + sub.advance(2); + // find matching } + let inner_end = find_matching_close(sub.rest(), '}') + .ok_or_else(|| sub.err("unclosed '${...'"))?; + let expr = sub.rest()[..inner_end].trim().to_string(); + sub.advance(inner_end + 1); + items.push(TemplateItem::Injection(expr)); + continue; + } + + // $name or $name.prop variable reference + if rest.starts_with('$') { + flush_literal!(); + sub.advance(1); + let var = sub.parse_ident()?; + if sub.rest().starts_with('.') { + sub.advance(1); + let prop = sub.parse_ident()?; + items.push(TemplateItem::FieldAccess { var, prop }); + } else { + items.push(TemplateItem::VarRef(var)); + } + continue; + } + + // otherwise accumulate literal character + let c = rest.chars().next().unwrap(); + literal_buf.push(c); + sub.advance(c.len_utf8()); + } + + flush_literal!(); + Ok(items) + } + + /// Parse the output body (right of =>), inside the outer `{ }` block + fn parse_output_body(&mut self, content: &str) -> Result { + let mut sub = Parser::new(content); + let mut items = Vec::new(); + + // The top-level is an implicit T~{ } unless the content starts with explicit blocks + // Detect if content has @{ or T~{ at top level + let has_explicit_blocks = { + let trimmed = content.trim(); + trimmed.starts_with("@{") || trimmed.starts_with("T~{") + }; + + if !has_explicit_blocks { + // entire body is implicit token template + let template_items = self.parse_template_items(content)?; + items.push(OutputItem::TokenTemplate(template_items)); + } else { + loop { + sub.skip_whitespace_and_comments(); + if sub.rest().is_empty() { + break; + } + if sub.rest().starts_with("@{") { + sub.advance(1); // consume @ + let block = sub.consume_braced_block()?; + let rust_code = self.parse_escape_block_content(&block); + items.push(OutputItem::EscapeBlock(rust_code)); + } else if sub.rest().starts_with("T~{") { + sub.advance(2); // consume T~ + let block = sub.consume_braced_block()?; + let template_items = self.parse_template_items(&block)?; + items.push(OutputItem::TokenTemplate(template_items)); + } else { + return Err(sub.err(format!("expected @{{ or T~{{, got {:?}", &sub.rest()[..sub.rest().len().min(10)]))); + } + } + } + + Ok(OutputBody { items }) + } + + /// Parse one `macro name! { ... }` definition + fn parse_macro_def(&mut self) -> Result { + self.expect_str("macro")?; + self.skip_whitespace_and_comments(); + let name = self.parse_ident()?; + // consume the `!` + self.skip_whitespace_and_comments(); + self.expect_str("!")?; + + // outer braces + let outer = self.consume_braced_block()?; + + // Inside outer: parse pattern => { body } + let mut inner = Parser::new(&outer); + let pattern = inner.parse_pattern()?; + inner.expect_str("=>")?; + let body_src = inner.consume_braced_block()?; + let body = self.parse_output_body(&body_src)?; + + Ok(MacroDef { name, pattern, body }) + } + + pub fn parse_file(&mut self) -> Result { + let mut macros = Vec::new(); + loop { + self.skip_whitespace_and_comments(); + if self.rest().is_empty() { + break; + } + macros.push(self.parse_macro_def()?); + } + Ok(MissesFile { macros }) + } +} + +pub fn parse(src: &str) -> Result { + Parser::new(src).parse_file() +} + +/// Find position of first unmatched `close` char, respecting nesting of `{` `}` +fn find_matching_close(s: &str, close: char) -> Option { + let open = '{'; + let mut depth = 0usize; + for (i, c) in s.char_indices() { + if c == open { + depth += 1; + } else if c == close { + if depth == 0 { + return Some(i); + } + depth -= 1; + } + } + None +} + +/// Rewrite `$name` -> `name` in raw Rust code (for @{} blocks) +fn rewrite_dollar_vars(s: &str) -> String { + let mut out = String::new(); + let mut chars = s.char_indices().peekable(); + while let Some((i, c)) = chars.next() { + if c == '$' { + // check if next char starts an ident + if let Some(&(_, nc)) = chars.peek() { + if nc.is_alphabetic() || nc == '_' { + // skip the $ and copy the ident as-is + continue; + } + } + out.push(c); + } else { + out.push(c); + } + } + out +}