From 0ac3a473d492dcb95a99554b2e9141397e2fb092 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Mar 2026 09:11:41 +0000 Subject: [PATCH 1/3] Replace .unwrap() with proper proc macro error handling in codegen The generated proc macro code was using .unwrap() for parse failures, causing panics instead of emitting proper Rust compile errors. Replace both unwrap calls with match expressions that call e.into_compile_error().into() to surface parse failures as compiler diagnostics in the generated proc macros. https://claude.ai/code/session_01D2g6zGJsBeUEzxBb7AvFDt --- misses-compiler/src/codegen.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misses-compiler/src/codegen.rs b/misses-compiler/src/codegen.rs index 4e176ed..758d219 100644 --- a/misses-compiler/src/codegen.rs +++ b/misses-compiler/src/codegen.rs @@ -89,7 +89,7 @@ fn codegen_pattern_parse(pattern: &Pattern) -> String { 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", + " let {name}: {ty} = match syn::parse2(input.clone()) {{ Ok(v) => v, Err(e) => return e.into_compile_error().into() }};\n", name = b.name, ty = ty ); @@ -149,7 +149,7 @@ fn codegen_structural_pattern(pattern: &Pattern, bindings: &[BindingInfo]) -> St }; 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(" match __parser.parse2(input.clone()) { Ok(v) => v, Err(e) => return e.into_compile_error().into() }\n"); out.push_str(" };\n"); out From 3be88ec21fd3a4f31525ab9fcf272d18158926bb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Mar 2026 09:30:14 +0000 Subject: [PATCH 2/3] Add `misses init` command with binary and library scaffold modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `misses init [--lib] [dir]` scaffolds a new proc-macro crate that uses misses as its build toolchain. Two modes are supported: Binary mode (default): The .mrs source files live in src/macros/ and are compiled to $OUT_DIR at build time via a generated build.rs. The generated Rust code is never committed — users must have misses installed to build from source. Library mode (--lib): The generated code is committed to generated/ so downstream users of the crate need no misses install at all. The .mrs source and build.rs are still present for the library author, who can regenerate via `cargo build --features misses-source`. The misses-source feature makes misses an optional build-dependency and gates the regeneration path in build.rs with #[cfg(feature)]. The command also teaches `main.rs` a proper subcommand dispatch so that `misses ` (compile) and `misses init` are cleanly separated. https://claude.ai/code/session_01D2g6zGJsBeUEzxBb7AvFDt --- misses-compiler/src/init.rs | 222 ++++++++++++++++++++++++++++++++++++ misses-compiler/src/main.rs | 52 +++++++-- 2 files changed, 262 insertions(+), 12 deletions(-) create mode 100644 misses-compiler/src/init.rs diff --git a/misses-compiler/src/init.rs b/misses-compiler/src/init.rs new file mode 100644 index 0000000..98ee60e --- /dev/null +++ b/misses-compiler/src/init.rs @@ -0,0 +1,222 @@ +use std::path::Path; + +/// Starter macro source written into every scaffolded project. +const HELLO_MRS: &str = r#"// hello.mrs — created by `misses init` +// Edit this file, then run `misses src/macros/hello.mrs src/generated/hello.rs` +// (or `cargo build --features misses-source`) to regenerate. + +macro hello! { + $name:ident => { + T~{ + pub fn hello() -> &'static str { + "hello from macro!" + } + } + } +} +"#; + +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- + +pub fn run(dir: &str, is_lib: bool) { + let dir = Path::new(dir); + let crate_name = infer_crate_name(dir); + + let src_dir = dir.join("src"); + let src_macros = src_dir.join("macros"); + let gen_dir = dir.join("generated"); + + create_dir(&src_macros); + if is_lib { + create_dir(&gen_dir); + } + + write_file(&dir.join("Cargo.toml"), &cargo_toml(&crate_name, is_lib)); + write_file(&dir.join("build.rs"), if is_lib { lib_build_rs() } else { bin_build_rs() }); + write_file(&src_macros.join("hello.mrs"), HELLO_MRS); + + if is_lib { + // Pre-generate hello.rs so users can build without the misses-source feature. + let ast = misses::parse(HELLO_MRS) + .unwrap_or_else(|e| die(&format!("internal: failed to parse starter macro: {e}"))); + let generated = misses::codegen(&ast); + write_file(&gen_dir.join("hello.rs"), &generated); + write_file(&src_dir.join("lib.rs"), lib_rs_library_mode()); + } else { + write_file(&src_dir.join("lib.rs"), bin_rs_binary_mode()); + } + + println!("Scaffolded misses project in `{}`", dir.display()); + if is_lib { + println!(); + println!(" Mode: library"); + println!(" - generated/ is committed so downstream users need no misses install."); + println!(" - To rebuild from .mrs source:"); + println!(" cargo build --features misses-source"); + } else { + println!(); + println!(" Mode: binary"); + println!(" - .mrs sources live in src/macros/ and are compiled at build time."); + println!(" - Generated Rust code goes to $OUT_DIR (never committed)."); + } +} + +// --------------------------------------------------------------------------- +// Scaffold file contents +// --------------------------------------------------------------------------- + +fn cargo_toml(crate_name: &str, is_lib: bool) -> String { + if is_lib { + format!( + r#"[package] +name = "{crate_name}" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +# Enable this feature to rebuild the generated code from .mrs source files. +# Downstream users of your crate do NOT need this feature; they use the +# pre-generated code in generated/ that you commit alongside your source. +[features] +misses-source = ["dep:misses"] + +[build-dependencies] +misses = {{ version = "0.1", optional = true }} +"#, + crate_name = crate_name + ) + } else { + format!( + r#"[package] +name = "{crate_name}" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[build-dependencies] +misses = "0.1" +"#, + crate_name = crate_name + ) + } +} + +/// build.rs for binary mode: always compile .mrs → OUT_DIR at build time. +fn bin_build_rs() -> &'static str { + r#"fn main() { + let src_dir = std::path::Path::new("src/macros"); + let out_dir = std::env::var("OUT_DIR").unwrap(); + let out_dir = std::path::Path::new(&out_dir); + + for entry in std::fs::read_dir(src_dir).expect("src/macros not found") { + let path = entry.unwrap().path(); + if path.extension().and_then(|e| e.to_str()) == Some("mrs") { + let stem = path.file_stem().unwrap().to_str().unwrap().to_owned(); + let src = std::fs::read_to_string(&path).unwrap(); + let ast = misses::parse(&src) + .unwrap_or_else(|e| panic!("failed to parse {}: {e}", path.display())); + let generated = misses::codegen(&ast); + std::fs::write(out_dir.join(format!("{stem}.rs")), generated).unwrap(); + println!("cargo:rerun-if-changed={}", path.display()); + } + } + println!("cargo:rerun-if-changed=build.rs"); +} +"# +} + +/// build.rs for library mode: only regenerate when the `misses-source` feature is active. +fn lib_build_rs() -> &'static str { + r#"fn main() { + // Only re-generate when the author explicitly opts in with --features misses-source. + // Downstream users build against the pre-generated files in generated/. + #[cfg(feature = "misses-source")] + regen(); +} + +#[cfg(feature = "misses-source")] +fn regen() { + let src_dir = std::path::Path::new("src/macros"); + let gen_dir = std::path::Path::new("generated"); + std::fs::create_dir_all(gen_dir).unwrap(); + + for entry in std::fs::read_dir(src_dir).expect("src/macros not found") { + let path = entry.unwrap().path(); + if path.extension().and_then(|e| e.to_str()) == Some("mrs") { + let stem = path.file_stem().unwrap().to_str().unwrap().to_owned(); + let src = std::fs::read_to_string(&path).unwrap(); + let ast = misses::parse(&src) + .unwrap_or_else(|e| panic!("failed to parse {}: {e}", path.display())); + let generated = misses::codegen(&ast); + std::fs::write(gen_dir.join(format!("{stem}.rs")), &generated).unwrap(); + println!("cargo:rerun-if-changed={}", path.display()); + } + } + println!("cargo:rerun-if-changed=build.rs"); +} +"# +} + +/// lib.rs for library mode: include from the committed generated/ directory. +fn lib_rs_library_mode() -> &'static str { + r#"// This file is generated by misses. The generated/ directory is committed +// so that downstream users do not need misses installed. +// To regenerate: cargo build --features misses-source + +include!(concat!(env!("CARGO_MANIFEST_DIR"), "/generated/hello.rs")); +"# +} + +/// lib.rs for binary mode: include from Cargo's OUT_DIR (never committed). +fn bin_rs_binary_mode() -> &'static str { + r#"// This file is generated by misses via build.rs. +// The generated code lives in $OUT_DIR and is never committed to git. + +include!(concat!(env!("OUT_DIR"), "/hello.rs")); +"# +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn infer_crate_name(dir: &Path) -> String { + let name = if dir == Path::new(".") { + std::env::current_dir() + .unwrap() + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_owned() + } else { + dir.file_name() + .unwrap_or_else(|| die("cannot determine crate name from path")) + .to_str() + .unwrap() + .to_owned() + }; + // Cargo convention: hyphens in package names, underscores in crate names. + name.replace('-', "_") +} + +fn create_dir(path: &Path) { + std::fs::create_dir_all(path).unwrap_or_else(|e| die(&format!("create {}: {e}", path.display()))); +} + +fn write_file(path: &Path, contents: &str) { + std::fs::write(path, contents) + .unwrap_or_else(|e| die(&format!("write {}: {e}", path.display()))); + println!(" wrote {}", path.display()); +} + +fn die(msg: &str) -> ! { + eprintln!("error: {msg}"); + std::process::exit(1); +} diff --git a/misses-compiler/src/main.rs b/misses-compiler/src/main.rs index 66331d4..a770219 100644 --- a/misses-compiler/src/main.rs +++ b/misses-compiler/src/main.rs @@ -1,17 +1,35 @@ +mod init; + 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); - } + match args.get(1).map(String::as_str) { + Some("init") => { + let is_lib = args.iter().any(|a| a == "--lib"); + let dir = args + .iter() + .skip(2) + .find(|a| !a.starts_with('-')) + .map(String::as_str) + .unwrap_or("."); + init::run(dir, is_lib); + } + Some(_) => compile(&args), + None => { + print_usage(); + std::process::exit(1); + } + } +} + +fn compile(args: &[String]) { 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); + eprintln!("error reading {}: {}", input_path.display(), e); std::process::exit(1); } }; @@ -19,7 +37,7 @@ fn main() { let ast = match misses::parse(&src) { Ok(a) => a, Err(e) => { - eprintln!("Parse error: {}", e); + eprintln!("parse error: {}", e); std::process::exit(1); } }; @@ -28,14 +46,24 @@ fn main() { 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); - } + if let Err(e) = std::fs::write(&output_path, &generated) { + eprintln!("error writing {}: {}", output_path.display(), e); + std::process::exit(1); } } else { print!("{}", generated); } } + +fn print_usage() { + eprintln!("Usage:"); + eprintln!(" misses [output.rs] Compile a .mrs file to Rust"); + eprintln!(" misses init [--lib] [dir] Scaffold a new misses project"); + eprintln!(); + eprintln!("Modes for `misses init`:"); + eprintln!(" (default) Binary — .mrs sources are compiled at build time via build.rs;"); + eprintln!(" generated code lives in \\$OUT_DIR and is never committed."); + eprintln!(" --lib Library — generated code is committed to generated/ so"); + eprintln!(" downstream users need no misses install. Rebuild with"); + eprintln!(" `cargo build --features misses-source`."); +} From a86ba9760c77b6f5c9a31ed514e21a730102d13e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Mar 2026 09:34:43 +0000 Subject: [PATCH 3/3] Update README with misses init docs and error handling note - Rewrite Quick start to show `cargo install misses` and the new `misses init` subcommand as the primary entry point - Add a dedicated `misses init` section explaining binary mode (build-time codegen via OUT_DIR, nothing committed) and library mode (--lib: generated/ committed, misses-source feature to rebuild, zero misses dependency for downstream users) - Update project layout to include init.rs - Add note that parse errors surface as compiler diagnostics via into_compile_error() rather than panicking https://claude.ai/code/session_01D2g6zGJsBeUEzxBb7AvFDt --- README.md | 89 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 78 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ad3bece..d8d9413 100644 --- a/README.md +++ b/README.md @@ -7,22 +7,76 @@ ## Quick start ```bash -# Build everything -cargo build +# Install the CLI +cargo install misses -# Run a bundled example (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 +# 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 -cargo run --bin misses -- path/to/my_macro.mrs > src/generated.rs +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: @@ -137,6 +191,16 @@ macro declare_table! { } ``` +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 @@ -194,13 +258,14 @@ let val = kv.get("name"); ``` misses/ -├── misses-compiler/ # The .mrs → Rust transpiler +├── 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 +│ │ └── main.rs # CLI binary (compile + init subcommands) │ └── examples/ # Runnable .mrs demonstrations │ ├── make_id_type.{mrs,rs} │ ├── derive_builder.{mrs,rs} @@ -226,4 +291,6 @@ For each `.mrs` macro, the compiler generates: 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.