Files

11 KiB

Misses

Misses is a DSL compiler that transforms .mrs macro definitions into ready-to-use Rust proc macro source files. Write your macro once in a concise, readable syntax; Misses generates the full syn/quote boilerplate for you.


Why Misses?

Writing proc macros means wrestling with syn parsing, quote! boilerplate, error handling, and variadic patterns. You write the same patterns over and over.

Misses lets you write the macro once, in readable syntax, and generates all the boilerplate for you.

Here's a real example:

derive_getters.mrs

macro derive_getters! {
    struct $name:ident { $($field:ident : $ty:ty),* } => {
        T~{
            impl $name {
                for field in $fields =>
                    pub fn $field.name(&self) -> &$field.ty { &self.$field.name }
            }
        }
    }
}
Generated output

extern crate proc_macro;
#[proc_macro]
pub fn derive_rpc(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = proc_macro2::TokenStream::from(input);
    use quote::format_ident;
    let (name, params, ret) = {
        use syn::parse::Parser as _;
        let __parser = |
            _input: syn::parse::ParseStream,
        | -> syn::Result<(syn::Ident, Vec<(syn::Ident, syn::Type)>, syn::Type)> {
            let name: syn::Ident = _input.parse()?;
            let params = {
                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
            };
            let ret: syn::Type = _input.parse()?;
            Ok((name, params, ret))
        };
        match __parser.parse2(input.clone()) {
            Ok(v) => v,
            Err(e) => return e.into_compile_error().into(),
        }
    };
    let req_name = format_ident!("{}Request", name);
    let res_name = format_ident!("{}Response", name);
    let __spread_0: proc_macro2::TokenStream = params
        .iter()
        .map(|(__field_name, __field_ty)| {
            quote::quote! {
                pub # __field_name : # __field_ty,
            }
        })
        .collect();
    let __output = quote::quote! {
        pub struct # req_name { # __spread_0 } pub struct # res_name { pub result : #
        ret, }
    };
    __output.into()
}


Quick start

# Install the CLI
cargo install misses

# Scaffold a new proc-macro crate (binary mode)
misses init my-macros

# Scaffold a library-friendly crate (generated code committed, no misses required downstream)
misses init --lib my-macros

# Compile a .mrs file to Rust
misses path/to/my_macro.mrs > src/generated.rs
misses path/to/my_macro.mrs src/generated.rs

misses init — project scaffolding

misses init creates a ready-to-build proc-macro crate. Two modes are available depending on whether you're shipping a binary or a library.

Binary mode (default)

misses init my-macros

Use this when you control the build environment (e.g. an application binary). The .mrs sources are compiled to Rust at build time via build.rs; the generated code lives in Cargo's $OUT_DIR and is never committed.

my-macros/
├── Cargo.toml            # misses as a plain [build-dependencies]
├── build.rs              # compiles src/macros/*.mrs → $OUT_DIR/*.rs
└── src/
    ├── lib.rs            # include!(concat!(env!("OUT_DIR"), "/hello.rs"))
    └── macros/
        └── hello.mrs     # your macro source (committed)

Users only need misses installed to build from source, which is fine for an app you control.

Library mode (--lib)

misses init --lib my-macros

Use this when you're publishing a library crate. Downstream users of your crate should not be forced to install misses. The generated Rust code is committed to generated/ and shipped on crates.io.

my-macros/
├── Cargo.toml            # misses behind [features] misses-source = ["dep:misses"]
├── build.rs              # no-op unless --features misses-source
├── generated/
│   └── hello.rs          # pre-generated, committed to git
└── src/
    ├── lib.rs            # include!(concat!(env!("CARGO_MANIFEST_DIR"), "/generated/hello.rs"))
    └── macros/
        └── hello.mrs     # your macro source (committed)

When you modify a .mrs file, regenerate with:

cargo build --features misses-source

The misses-source feature makes misses an optional build-dependency and gates regeneration in build.rs. Downstream users build normally with no misses dependency.


The .mrs syntax

A .mrs file contains one or more macro definitions:

macro macro_name! {
    <pattern> => {
        <output>
    }
}

Pattern bindings

Syntax Parsed as
$name:ident A single identifier
$expr:expr Any Rust expression
$ty:ty A Rust type
$body:tt A raw token-tree (brace group)
$($field:ident : $ty:ty),* Variadic — zero-or-more comma-separated pairs

Literal tokens in the pattern are matched verbatim. Rust keywords use syn::Token![…]; arbitrary identifiers are matched by value.

Output blocks

An output block may contain any mix of:

Escape block @{ … } — arbitrary Rust code executed at macro-expansion time:

@{
    let builder_name = format_ident!("{}_Builder", name.to_string());
}

Token template T~{ … } — generates the quote::quote! { … } call. Inside a template you may use:

Syntax Meaning
$name Interpolate the bound variable name
$field.name / $field.ty Access a field of a variadic element
${ expr } Evaluate an arbitrary Rust expression and interpolate the result
for x in $xs => … Spread — iterate the variadic collection and repeat a line
#[a => b_$name] Construct an identifier from parts (uses format_ident!)

Examples

make_id_type.mrs — newtype ID wrapper

macro make_id_type! {
    $name:ident => {
        T~{
            #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
            pub struct $name(pub u64);

            impl $name {
                pub fn new(val: u64) -> Self { Self(val) }
                pub fn value(self) -> u64 { self.0 }
            }
        }
    }
}

derive_builder.mrs — builder pattern

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(),
                    }
                }
            }
        }
    }
}

declare_table.mrs — DSL to struct with runtime helper

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() };
            }
        }
    }
}

Run the bundled examples to compare .mrs source with generated Rust:

cargo run --example make_id_type
cargo run --example derive_builder
cargo run --example derive_getters
cargo run --example declare_table
cargo run --example component

The misses-rt runtime library

misses-rt provides helpers that generated proc macro code can call at expansion time. Add it to your proc macro crate:

[dependencies]
misses-rt = { path = "../misses-rt" }

Name helpers

misses_rt::to_table_name(&ident)  // UsersAccount → "users_accounts"
misses_rt::to_snake_case(&ident)  // MyType → "my_type"
misses_rt::to_snake_case_str(s)   // "MyType" → "my_type"
misses_rt::to_pascal_case(s)      // "my_type" → MyType (Ident)
misses_rt::concat_ident(&[a, b])  // ["foo", "Bar"] → fooBar (Ident)

Type helpers

misses_rt::is_option(&ty)         // Option<T> → true
misses_rt::unwrap_option(&ty)     // Option<T> → Some(&T)
misses_rt::to_sql_type(&ty)       // syn::Type → "TEXT", "INTEGER", …

HTML template helpers

let mut buf = misses_rt::Buffer::new();
buf.push_str("<b>");
buf.push_escaped("<script>"); // → "&lt;script&gt;"
let markup: misses_rt::Markup = misses_rt::Markup::from_buffer(buf);
println!("{}", markup.into_string());

Section / key-value parsing

// Parse `props { … } slots { … }` blocks from a token stream
let map = misses_rt::parse_sections(tokens, &["props", "slots"]);
let props_tokens = map.get("props");

// Parse `key = value, …` pairs
let kv = misses_rt::parse_kv(tokens);
let val = kv.get("name");

Project layout

misses/
├── misses-compiler/       # The .mrs → Rust transpiler and CLI
│   ├── src/
│   │   ├── ast.rs         # Full AST types
│   │   ├── parser.rs      # Hand-written recursive descent parser
│   │   ├── codegen.rs     # AST → Rust source string
│   │   ├── init.rs        # `misses init` scaffolding logic
│   │   ├── lib.rs         # Library API (parse + codegen)
│   │   └── main.rs        # CLI binary (compile + init subcommands)
│   └── examples/          # Runnable .mrs demonstrations
│       ├── make_id_type.{mrs,rs}
│       ├── derive_builder.{mrs,rs}
│       ├── derive_getters.{mrs,rs}
│       ├── declare_table.{mrs,rs}
│       └── component.{mrs,rs}
└── misses-rt/             # Runtime helpers for generated proc macros
    └── src/
        ├── lib.rs         # Buffer, Markup, name/type helpers
        ├── parse_helpers.rs
        └── template.rs    # HTML template AST + code-gen

How code generation works

For each .mrs macro, the compiler generates:

  1. A #[proc_macro] function that receives proc_macro::TokenStream.
  2. Parser statements that destructure the input according to the pattern.
  3. Any @{} escape-block statements, executed inline.
  4. A quote::quote! { … } call whose body mirrors the T~{} template, with spread iterators for variadic bindings.
  5. The quote! result is returned as proc_macro::TokenStream.

Parse errors are surfaced as proper Rust compiler diagnostics via syn::Error::into_compile_error() rather than panicking.

The generated file is formatted with prettyplease for readable output.