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. 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 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`."); +}