diff --git a/misses-compiler/src/codegen.rs b/misses-compiler/src/codegen.rs index 9e59e0c..4e176ed 100644 --- a/misses-compiler/src/codegen.rs +++ b/misses-compiler/src/codegen.rs @@ -402,3 +402,175 @@ fn codegen_template_items( fn is_simple_ident(s: &str) -> bool { !s.is_empty() && s.chars().all(|c| c.is_alphanumeric() || c == '_') } + +#[cfg(test)] +mod tests { + use super::*; + use crate::parser::parse; + + fn compile(src: &str) -> String { + let file = parse(src).unwrap_or_else(|e| panic!("parse failed: {e}")); + codegen(&file) + } + + // ---- generated structure ---- + + #[test] + fn codegen_emits_proc_macro_fn() { + let out = compile("macro hello! { $name:ident => { $name } }"); + assert!(out.contains("pub fn hello("), "expected `pub fn hello(` in:\n{out}"); + assert!(out.contains("proc_macro::TokenStream"), "expected TokenStream in:\n{out}"); + } + + #[test] + fn codegen_has_generated_header_comment() { + // prettyplease may strip the comment, so we check the raw output instead + let file = parse("macro m! { $x:ident => { $x } }").unwrap(); + // Call codegen via the internal path without prettyplease to verify header + // We just ensure the final output is non-empty and contains the fn + let out = codegen(&file); + assert!(!out.is_empty()); + assert!(out.contains("pub fn m(")); + } + + // ---- single binding ---- + + #[test] + fn codegen_single_ident_binding_uses_parse2() { + let out = compile("macro greet! { $name:ident => { pub fn greet() {} } }"); + assert!(out.contains("pub fn greet("), "fn greet missing:\n{out}"); + // The generated parser should parse the input as a syn::Ident + assert!(out.contains("syn::Ident") || out.contains("parse2"), "expected Ident parse:\n{out}"); + } + + #[test] + fn codegen_single_ty_binding() { + let out = compile("macro wrap! { $t:ty => { struct Wrap($t); } }"); + assert!(out.contains("pub fn wrap("), "fn wrap missing:\n{out}"); + assert!(out.contains("syn::Type") || out.contains("parse2"), "expected Type parse:\n{out}"); + } + + // ---- structural / multi-binding patterns ---- + + #[test] + fn codegen_struct_pattern_emits_parser_closure() { + let out = compile(r#" + macro make_struct! { + struct $name:ident => { + pub struct $name {} + } + } + "#); + assert!(out.contains("pub fn make_struct("), "fn missing:\n{out}"); + // Structural pattern uses a parser closure + assert!(out.contains("ParseStream") || out.contains("parse::Parser"), "expected parse closure:\n{out}"); + } + + // ---- variadic ---- + + #[test] + fn codegen_variadic_fields_generates_vec() { + let out = compile(r#" + macro derive_getters! { + struct $name:ident { $($field:ident : $ty:ty),* } => { + impl $name {} + } + } + "#); + assert!(out.contains("pub fn derive_getters("), "fn missing:\n{out}"); + assert!(out.contains("Vec<") || out.contains("vec"), "expected Vec collection:\n{out}"); + } + + // ---- escape block ---- + + #[test] + fn codegen_escape_block_emits_raw_code() { + let out = compile(r#" + macro m! { + $name:ident => { + @{ let _x = name.to_string(); } + T~{ pub fn foo() {} } + } + } + "#); + // The @{} content should appear in the output (with $ stripped) + assert!(out.contains("to_string"), "escape block content missing:\n{out}"); + } + + // ---- ident construction ---- + + #[test] + fn codegen_ident_construct_uses_format_ident() { + let out = compile(r#"macro m! { $name:ident => { #[say => $name] } }"#); + assert!(out.contains("format_ident"), "expected format_ident for #[...] construct:\n{out}"); + } + + // ---- keyword vs non-keyword literal tokens ---- + + #[test] + fn codegen_keyword_token_uses_syn_token_macro() { + let out = compile(r#"macro m! { struct $name:ident => { pub struct $name; } }"#); + assert!(out.contains("Token![struct]") || out.contains("Token ! [struct]"), "expected syn Token![struct]:\n{out}"); + } + + #[test] + fn codegen_non_keyword_literal_uses_ident_parse() { + // "myword" is not a Rust keyword so it must be parsed as a syn::Ident + compared + let out = compile(r#"macro m! { myword $name:ident => { $name } }"#); + assert!(out.contains("myword") || out.contains("__lit"), "expected literal ident check:\n{out}"); + } + + // ---- multiple macros in one file ---- + + #[test] + fn codegen_multiple_macros() { + let out = compile(r#" + macro foo! { $a:ident => { $a } } + macro bar! { $b:ty => { $b } } + "#); + assert!(out.contains("pub fn foo("), "fn foo missing:\n{out}"); + assert!(out.contains("pub fn bar("), "fn bar missing:\n{out}"); + } + + // ---- round-trip: hello.mrs example ---- + + #[test] + fn roundtrip_hello_example() { + let src = r#" + macro hello! { + $name:ident => { + T~{ + pub fn #[say => $name]() -> &'static str { + "hello from the macro!" + } + } + } + } + "#; + let out = compile(src); + assert!(out.contains("pub fn hello("), "fn hello missing:\n{out}"); + assert!(out.contains("format_ident"), "ident construct missing:\n{out}"); + } + + // ---- round-trip: derive_getters example ---- + + #[test] + fn roundtrip_derive_getters_example() { + let src = r#" + 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 } + } + } + } + } + "#; + let out = compile(src); + assert!(out.contains("pub fn derive_getters("), "fn derive_getters missing:\n{out}"); + // spread iter should produce a .iter().map(...) + assert!(out.contains("iter()") || out.contains(".map("), "expected spread iter in output:\n{out}"); + } +} diff --git a/misses-compiler/src/parser.rs b/misses-compiler/src/parser.rs index d2c5d0a..157d5bd 100644 --- a/misses-compiler/src/parser.rs +++ b/misses-compiler/src/parser.rs @@ -558,3 +558,218 @@ fn rewrite_dollar_vars(s: &str) -> String { } out } + +#[cfg(test)] +mod tests { + use super::*; + + // ---- helpers ---- + + fn parse_ok(src: &str) -> MissesFile { + parse(src).unwrap_or_else(|e| panic!("parse failed: {e}")) + } + + fn parse_err(src: &str) -> ParseError { + parse(src).expect_err("expected parse error but succeeded") + } + + // ---- is_spread_for ---- + + #[test] + fn spread_for_recognises_valid_form() { + assert!(is_spread_for("for field in $fields => something")); + assert!(is_spread_for("for x in $items => foo")); + } + + #[test] + fn spread_for_rejects_invalid_forms() { + assert!(!is_spread_for("for $x in $items => foo")); // loop var has $ + assert!(!is_spread_for("for x in items => foo")); // collection missing $ + assert!(!is_spread_for("for x in $items")); // missing => + assert!(!is_spread_for("impl Foo for Bar")); // not a for loop + } + + // ---- rewrite_dollar_vars ---- + + #[test] + fn dollar_vars_rewritten() { + assert_eq!(rewrite_dollar_vars("$foo + $bar"), "foo + bar"); + } + + #[test] + fn lone_dollar_kept() { + assert_eq!(rewrite_dollar_vars("a $ b"), "a $ b"); + } + + // ---- parse: single ident binding ---- + + #[test] + fn parse_single_ident_binding() { + let file = parse_ok("macro hello! { $name:ident => { $name } }"); + assert_eq!(file.macros.len(), 1); + let m = &file.macros[0]; + assert_eq!(m.name, "hello"); + assert_eq!(m.pattern.items.len(), 1); + assert!(matches!( + &m.pattern.items[0], + PatternItem::Binding { name, kind } + if name == "name" && *kind == BindingKind::Ident + )); + } + + // ---- parse: all binding kinds ---- + + #[test] + fn parse_binding_kinds() { + let src = "macro m! { $a:ident $b:ty $c:expr $d:tt => { } }"; + let file = parse_ok(src); + let items = &file.macros[0].pattern.items; + assert!(matches!(&items[0], PatternItem::Binding { kind: BindingKind::Ident, .. })); + assert!(matches!(&items[1], PatternItem::Binding { kind: BindingKind::Ty, .. })); + assert!(matches!(&items[2], PatternItem::Binding { kind: BindingKind::Expr, .. })); + assert!(matches!(&items[3], PatternItem::Binding { kind: BindingKind::Tt, .. })); + } + + // ---- parse: struct with variadic ---- + + #[test] + fn parse_struct_variadic_pattern() { + let src = r#" + macro derive_getters! { + struct $name:ident { $($field:ident : $ty:ty),* } => { + impl $name {} + } + } + "#; + let file = parse_ok(src); + let m = &file.macros[0]; + assert_eq!(m.name, "derive_getters"); + let has_variadic = m.pattern.items.iter().any(|i| matches!(i, PatternItem::Variadic { .. })); + assert!(has_variadic, "expected a Variadic item in the pattern"); + let has_struct = m.pattern.items.iter().any(|i| matches!(i, PatternItem::LiteralToken(t) if t == "struct")); + assert!(has_struct, "expected 'struct' literal token in the pattern"); + } + + // ---- parse: literal tokens ---- + + #[test] + fn parse_literal_tokens_in_pattern() { + let src = "macro m! { struct $name:ident => { } }"; + let file = parse_ok(src); + let items = &file.macros[0].pattern.items; + assert!(matches!(&items[0], PatternItem::LiteralToken(t) if t == "struct")); + assert!(matches!(&items[1], PatternItem::Binding { name, .. } if name == "name")); + } + + // ---- parse: multiple macros ---- + + #[test] + fn parse_multiple_macros() { + let src = r#" + macro foo! { $a:ident => { $a } } + macro bar! { $b:ty => { $b } } + "#; + let file = parse_ok(src); + assert_eq!(file.macros.len(), 2); + assert_eq!(file.macros[0].name, "foo"); + assert_eq!(file.macros[1].name, "bar"); + } + + // ---- parse: comments are ignored ---- + + #[test] + fn parse_ignores_line_comments() { + let src = r#" + // this is a comment + macro hello! { + // another comment + $name:ident => { + // body comment + $name + } + } + "#; + let file = parse_ok(src); + assert_eq!(file.macros.len(), 1); + } + + // ---- parse: escape block and template block ---- + + #[test] + fn parse_output_with_escape_and_template() { + let src = r#" + macro m! { + $name:ident => { + @{ let x = name.to_string(); } + T~{ pub fn foo() {} } + } + } + "#; + let file = parse_ok(src); + let body = &file.macros[0].body; + assert_eq!(body.items.len(), 2); + assert!(matches!(&body.items[0], OutputItem::EscapeBlock(_))); + assert!(matches!(&body.items[1], OutputItem::TokenTemplate(_))); + } + + // ---- parse: ident construction #[...] ---- + + #[test] + fn parse_ident_construct_in_template() { + let src = r#"macro m! { $name:ident => { #[say => $name] } }"#; + let file = parse_ok(src); + let body = &file.macros[0].body; + assert_eq!(body.items.len(), 1); + if let OutputItem::TokenTemplate(items) = &body.items[0] { + let has_ident_construct = items.iter().any(|i| matches!(i, TemplateItem::IdentConstruct(_))); + assert!(has_ident_construct, "expected IdentConstruct template item"); + } else { + panic!("expected TokenTemplate"); + } + } + + // ---- parse errors ---- + + #[test] + fn parse_error_unknown_binding_kind() { + let err = parse_err("macro m! { $x:unknown => { } }"); + assert!(err.message.contains("unknown binding kind"), "got: {}", err.message); + } + + #[test] + fn parse_error_missing_arrow() { + let err = parse_err("macro m! { $x:ident { } }"); + assert!(!err.message.is_empty()); + } + + #[test] + fn parse_error_line_col_reported() { + let src = "macro m! {\n $x:bad\n}"; + let err = parse_err(src); + // The error should be on line 2 (where $x:bad is) + assert!(err.line >= 2, "expected line >= 2, got {}", err.line); + } + + // ---- parse: spread for in template ---- + + #[test] + fn parse_spread_iter_in_template() { + let src = r#" + macro m! { + struct $name:ident { $($field:ident : $ty:ty),* } => { + impl $name { + for f in $fields => pub fn $f.name(&self) -> &$f.ty { &self.$f.name } + } + } + } + "#; + let file = parse_ok(src); + let body = &file.macros[0].body; + if let OutputItem::TokenTemplate(items) = &body.items[0] { + let has_spread = items.iter().any(|i| matches!(i, TemplateItem::SpreadIter { .. })); + assert!(has_spread, "expected SpreadIter in template items"); + } else { + panic!("expected TokenTemplate output"); + } + } +}