Files

386 lines
11 KiB
Markdown

# 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`
```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 }
}
}
}
}
```
<details>
<summary>
<b>Generated output</b>
</summary>
```rust
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()
}
```
</details>
---
## Quick start
```bash
# 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)
```bash
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`)
```bash
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:
```bash
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:
```bash
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:
```toml
[dependencies]
misses-rt = { path = "../misses-rt" }
```
### Name helpers
```rust
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
```rust
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
```rust
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
```rust
// 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](https://github.com/dtolnay/prettyplease) for readable output.