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:
Claude
2026-03-07 11:37:58 +00:00
commit ed62264870
11 changed files with 1152 additions and 0 deletions
+14
View File
@@ -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"
+89
View File
@@ -0,0 +1,89 @@
/// Top-level representation of a parsed .mrs file
#[derive(Debug, Clone)]
pub struct MissesFile {
pub macros: Vec<MacroDef>,
}
/// 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<PatternItem>,
}
#[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<VariadicBinding> },
/// 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<OutputItem>,
}
#[derive(Debug, Clone)]
pub enum OutputItem {
/// `@{ ... }` — raw Rust code (with $ vars rewritten to plain vars)
EscapeBlock(String),
/// `T~{ ... }` — token template (quote!)
TokenTemplate(Vec<TemplateItem>),
}
/// 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<TemplateItem>,
},
/// `#[part1 => part2 => ...]` — identifier construction
IdentConstruct(Vec<IdentPart>),
}
#[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 },
}
+381
View File
@@ -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(&quote::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 == '_')
}
+45
View File
@@ -0,0 +1,45 @@
mod ast;
mod parser;
mod codegen;
use std::path::PathBuf;
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
eprintln!("Usage: misses <input.mrs> [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);
}
}
+516
View File
@@ -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<String>) -> 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<char> {
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<String, ParseError> {
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<String, ParseError> {
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<PatternItem, ParseError> {
// 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<PatternItem, ParseError> {
// We need to parse inner bindings until we see `)` then `,*`
let mut inner: Vec<VariadicBinding> = Vec::new();
let mut name_for_variadic: Option<String> = 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<Pattern, ParseError> {
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<TemplateItem, ParseError> {
// 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<TemplateItem>
fn parse_template_items(&mut self, content: &str) -> Result<Vec<TemplateItem>, 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<OutputBody, ParseError> {
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<MacroDef, ParseError> {
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<MissesFile, ParseError> {
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<MissesFile, ParseError> {
Parser::new(src).parse_file()
}
/// Find position of first unmatched `close` char, respecting nesting of `{` `}`
fn find_matching_close(s: &str, close: char) -> Option<usize> {
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
}