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 @@
|
|||||||
|
target/
|
||||||
Generated
+58
@@ -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"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[workspace]
|
||||||
|
members = ["misses-compiler"]
|
||||||
|
resolver = "2"
|
||||||
@@ -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() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
macro hello! {
|
||||||
|
$name:ident => {
|
||||||
|
T~{
|
||||||
|
pub fn #[say => hello]() -> &'static str {
|
||||||
|
${ format!("hello, {}!", stringify!(name)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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 },
|
||||||
|
}
|
||||||
@@ -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 == '_')
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user