Merge pull request #2 from Markk116/claude/fix-todos-stubs-t4TEN
Replace .unwrap() with proper proc macro error handling in codegen
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,17 +1,35 @@
|
||||
mod init;
|
||||
|
||||
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]");
|
||||
|
||||
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);
|
||||
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 <input.mrs> [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`.");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user