use crate::ts_syn::abi::SpanIR;
use super::BuildMode;
use super::discovery::{discover, resolve_cross_file_imports};
use super::expander::{ExpansionContext, expand_body};
use super::matcher::{Binding, BoundFragment, MatchError, match_invocation};
use super::project_registry::ProjectDeclarativeRegistry;
use super::registry::{DeclarativeMacroRegistry, RegistryError};
use super::rewriter::rewrite;
use crate::ts_syn::declarative::{BodyToken, FragmentKind, MacroDef, MacroMode};
use oxc::allocator::Allocator;
use oxc::parser::Parser;
use oxc::span::SourceType;
use std::collections::HashMap;
fn parse_program<'a>(allocator: &'a Allocator, source: &'a str) -> oxc::parser::ParserReturn<'a> {
Parser::new(allocator, source, SourceType::ts()).parse()
}
#[test]
fn discovers_simple_macro() {
let source = r#"import { macroRules } from "macroforge/rules";
const $vec = macroRules`
() => []
`;
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
assert!(
parsed.errors.is_empty(),
"parse errors: {:?}",
parsed.errors
);
let defs = discover(&parsed.program, source).expect("discover");
assert_eq!(defs.len(), 1);
assert_eq!(defs[0].def.name, "vec");
assert_eq!(defs[0].def.arms.len(), 1);
}
#[test]
fn skips_files_without_rules_import() {
let source = r#"const $vec = macroRules`() => []`;"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
let defs = discover(&parsed.program, source).expect("discover");
assert!(defs.is_empty());
}
#[test]
fn registry_rejects_duplicates() {
let mut registry = DeclarativeMacroRegistry::new();
let def1 = MacroDef::from_arms(
"vec".into(),
vec![],
MacroMode::ExpandOnly,
SpanIR::new(0, 10),
);
let def2 = def1.clone();
registry.register(def1).unwrap();
assert!(matches!(
registry.register(def2),
Err(RegistryError::DuplicateName(_))
));
}
#[test]
fn matcher_binds_single_fragment() {
let source = r#"import { macroRules } from "macroforge/rules";
const $id = macroRules`
($x:Expr) => $x
`;
$id(1 + 2);
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
let discovered = discover(&parsed.program, source).unwrap();
assert_eq!(discovered.len(), 1);
let def = &discovered[0].def;
let call = find_first_call(&parsed.program).expect("call");
let result = match_invocation(def, &call.arguments, source).expect("match");
assert_eq!(result.arm_index, 0);
assert!(result.bindings.contains_key("x"));
match result.bindings.get("x").unwrap() {
Binding::Single(frag) => assert_eq!(frag.source, "1 + 2"),
_ => panic!("expected Single binding"),
}
}
#[test]
fn matcher_repetition_collects_sequence() {
let source = r#"import { macroRules } from "macroforge/rules";
const $vec = macroRules`
($($x:Expr),+) => [$($x),+]
`;
$vec(1, 2, 3);
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
let discovered = discover(&parsed.program, source).unwrap();
let def = &discovered[0].def;
let call = find_first_call(&parsed.program).expect("call");
let result = match_invocation(def, &call.arguments, source).expect("match");
match result.bindings.get("x").unwrap() {
Binding::Sequence(frags) => {
assert_eq!(frags.len(), 3);
assert_eq!(frags[0].source, "1");
assert_eq!(frags[1].source, "2");
assert_eq!(frags[2].source, "3");
}
_ => panic!("expected Sequence"),
}
}
#[test]
fn matcher_no_arm_matches_returns_error() {
let source = r#"import { macroRules } from "macroforge/rules";
const $only = macroRules`
($x:Expr) => $x
`;
$only(1, 2);
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
let discovered = discover(&parsed.program, source).unwrap();
let def = &discovered[0].def;
let call = find_first_call(&parsed.program).expect("call");
let err = match_invocation(def, &call.arguments, source).unwrap_err();
assert!(matches!(err, MatchError::NoArmMatched { .. }));
}
#[test]
fn expander_single_substitution() {
use crate::ts_syn::declarative::Body;
let body = Body(vec![
BodyToken::Literal("return ".to_string()),
BodyToken::Substitution("x".to_string()),
BodyToken::Literal(" + 1".to_string()),
]);
let mut bindings = HashMap::new();
bindings.insert(
"x".to_string(),
Binding::Single(BoundFragment {
kind: FragmentKind::Expr,
source: "5".to_string(),
span: SpanIR::new(0, 0),
}),
);
let out = expand_body(&body, &bindings, 7, ExpansionContext::Statement, 0).unwrap();
assert_eq!(out, "return 5 + 1");
}
#[test]
fn expander_hygiene_rewrites_double_underscore_idents() {
use crate::ts_syn::declarative::Body;
let body = Body(vec![BodyToken::Literal(
"const __v = 1; __v + 2".to_string(),
)]);
let bindings = HashMap::new();
let out = expand_body(&body, &bindings, 7, ExpansionContext::Statement, 0).unwrap();
assert!(out.contains("__v$7"), "got: {}", out);
assert!(!out.contains(" __v "), "unrenamed __v in: {}", out);
}
#[test]
fn expander_expression_context_wraps_block_in_iife() {
use crate::ts_syn::declarative::Body;
let body = Body(vec![BodyToken::Literal("{ return 1; }".to_string())]);
let bindings = HashMap::new();
let out = expand_body(&body, &bindings, 1, ExpansionContext::Expression, 0).unwrap();
assert!(out.starts_with("(() => "), "got: {}", out);
assert!(out.ends_with(")()"), "got: {}", out);
}
#[test]
fn expander_statement_context_no_iife() {
use crate::ts_syn::declarative::Body;
let body = Body(vec![BodyToken::Literal("{ return 1; }".to_string())]);
let bindings = HashMap::new();
let out = expand_body(&body, &bindings, 1, ExpansionContext::Statement, 0).unwrap();
assert_eq!(out, "{ return 1; }");
}
#[test]
fn expander_repetition_unrolls_sequence() {
use crate::ts_syn::declarative::{Body, RepetitionKind};
let body = Body(vec![BodyToken::Repetition {
body: vec![
BodyToken::Literal("push(".to_string()),
BodyToken::Substitution("x".to_string()),
BodyToken::Literal(");".to_string()),
],
separator: Some(" ".to_string()),
kind: RepetitionKind::OneOrMore,
}]);
let mut bindings = HashMap::new();
bindings.insert(
"x".to_string(),
Binding::Sequence(vec![
BoundFragment {
kind: FragmentKind::Expr,
source: "1".to_string(),
span: SpanIR::new(0, 0),
},
BoundFragment {
kind: FragmentKind::Expr,
source: "2".to_string(),
span: SpanIR::new(0, 0),
},
BoundFragment {
kind: FragmentKind::Expr,
source: "3".to_string(),
span: SpanIR::new(0, 0),
},
]),
);
let out = expand_body(&body, &bindings, 1, ExpansionContext::Statement, 0).unwrap();
assert_eq!(out, "push(1); push(2); push(3);");
}
#[test]
fn composition_simple_two_macros() {
let source = r#"import { macroRules } from "macroforge/rules";
const $double = macroRules`($x:Expr) => ($x * 2)`;
const $quad = macroRules`($x:Expr) => $double($double($x))`;
const result = $quad(3);
"#;
let out = rewrite_source(source, BuildMode::dev());
assert!(out.diagnostics.is_empty(), "diag: {:?}", out.diagnostics);
let replace = out
.patches
.iter()
.find_map(|p| match p {
crate::ts_syn::abi::Patch::Replace { code, .. } => code.as_text(),
_ => None,
})
.expect("expected a Replace patch for the call site");
assert!(
replace.contains("3"),
"expected the literal 3 in the composed expansion: {}",
replace
);
assert!(
replace.contains("* 2"),
"expected the doubling to appear: {}",
replace
);
}
#[test]
fn composition_unknown_callee_errors() {
let source = r#"import { macroRules } from "macroforge/rules";
const $caller = macroRules`($x:Expr) => $nonexistent($x)`;
const result = $caller(1);
"#;
let out = rewrite_source(source, BuildMode::dev());
let has_error = out.diagnostics.iter().any(|d| {
matches!(d.level, crate::ts_syn::abi::DiagnosticLevel::Error)
&& d.message.contains("nonexistent")
});
assert!(
has_error,
"expected an unknown-macro error, got: {:?}",
out.diagnostics
);
}
#[test]
fn topological_sort_orders_callee_before_caller() {
use crate::host::declarative::registry::DeclarativeMacroRegistry;
use crate::ts_syn::declarative::parse_macro_def;
let mut registry = DeclarativeMacroRegistry::new();
let mut caller = parse_macro_def("($x:Expr) => $callee($x)", SpanIR::new(0, 24)).unwrap();
caller.name = "caller".into();
registry.register(caller).unwrap();
let mut callee = parse_macro_def("($x:Expr) => ($x + 1)", SpanIR::new(0, 21)).unwrap();
callee.name = "callee".into();
registry.register(callee).unwrap();
let sorted = registry.topological_order().expect("sort should succeed");
assert_eq!(sorted.len(), 2);
assert_eq!(sorted[0].name, "callee");
assert_eq!(sorted[1].name, "caller");
}
#[test]
fn topological_sort_detects_cycle() {
use crate::host::declarative::registry::DeclarativeMacroRegistry;
use crate::ts_syn::declarative::parse_macro_def;
let mut registry = DeclarativeMacroRegistry::new();
let mut a = parse_macro_def("($x:Expr) => $b($x)", SpanIR::new(0, 20)).unwrap();
a.name = "a".into();
registry.register(a).unwrap();
let mut b = parse_macro_def("($x:Expr) => $a($x)", SpanIR::new(0, 20)).unwrap();
b.name = "b".into();
registry.register(b).unwrap();
let err = registry.topological_order().unwrap_err();
assert_eq!(err.names.len(), 2);
}
#[test]
fn topological_sort_ignores_unknown_callees() {
use crate::host::declarative::registry::DeclarativeMacroRegistry;
use crate::ts_syn::declarative::parse_macro_def;
let mut registry = DeclarativeMacroRegistry::new();
let mut m = parse_macro_def("($x:Expr) => $cross_file_import($x)", SpanIR::new(0, 36)).unwrap();
m.name = "caller".into();
registry.register(m).unwrap();
let sorted = registry.topological_order().expect("should sort cleanly");
assert_eq!(sorted.len(), 1);
assert_eq!(sorted[0].name, "caller");
}
#[test]
fn expander_recursion_limit_trips_at_max_depth() {
use crate::ts_syn::declarative::Body;
let body = Body(vec![BodyToken::Literal("ok".to_string())]);
let bindings = HashMap::new();
let err = expand_body(
&body,
&bindings,
1,
ExpansionContext::Statement,
super::expander::MAX_EXPANSION_DEPTH + 1,
)
.unwrap_err();
assert!(
matches!(
err,
crate::host::declarative::expander::ExpandError::RecursionLimit(_)
),
"expected RecursionLimit, got {:?}",
err
);
}
#[test]
fn rewriter_end_to_end_vec_basic() {
let source = r#"import { macroRules } from "macroforge/rules";
const $vec = macroRules`
() => []
($($x:Expr),+) => [$($x),+]
`;
const xs = $vec(1, 2, 3);
const ys = $vec();
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
assert!(parsed.errors.is_empty());
let discovered = discover(&parsed.program, source).unwrap();
let mut registry = DeclarativeMacroRegistry::new();
for dm in &discovered {
registry.register(dm.def.clone()).unwrap();
}
let out = rewrite(
&parsed.program,
source,
®istry,
&discovered,
BuildMode::dev(),
None,
None,
);
assert_eq!(out.patches.len(), 4, "patches: {:#?}", out.patches);
assert!(out.diagnostics.is_empty(), "diag: {:?}", out.diagnostics);
}
fn library_registry() -> (ProjectDeclarativeRegistry, std::path::PathBuf) {
use crate::ts_syn::declarative::parse_macro_def;
let lib_src = "($($x:Expr),+) => [$($x),+]";
let span = SpanIR::new(0, lib_src.len() as u32);
let mut def = parse_macro_def(lib_src, span).expect("parse");
def.name = "vec".to_string();
let mut registry = ProjectDeclarativeRegistry::new();
let lib_path = std::path::PathBuf::from("/project/src/macros.ts");
registry.insert_file(lib_path.to_string_lossy().to_string(), vec![def]);
(registry, lib_path)
}
#[test]
fn resolve_cross_file_happy_path() {
let (registry, _lib_path) = library_registry();
let consumer_src = r#"/** import macro { $vec } from "./macros" */
const xs = $vec(1, 2, 3);
"#;
let consumer_path = std::path::PathBuf::from("/project/src/consumer.ts");
let resolved = resolve_cross_file_imports(consumer_src, &consumer_path, ®istry);
assert_eq!(resolved.imported.len(), 1);
assert_eq!(resolved.imported[0].def.name, "vec");
assert!(
resolved.diagnostics.is_empty(),
"expected no diagnostics, got: {:?}",
resolved.diagnostics
);
}
#[test]
fn resolve_cross_file_unresolved_bare_package_skips_silently() {
let (registry, _) = library_registry();
let consumer_src = r#"/** import macro { $vec } from "@external/macros" */"#;
let consumer_path = std::path::PathBuf::from("/project/src/consumer.ts");
let resolved = resolve_cross_file_imports(consumer_src, &consumer_path, ®istry);
assert!(resolved.imported.is_empty());
assert!(
resolved.diagnostics.is_empty(),
"expected no diagnostics for unresolved bare package (proc macro fallback), got: {:?}",
resolved.diagnostics
);
}
#[test]
fn resolve_cross_file_unresolved_relative_path_reports_diagnostic() {
let (registry, _) = library_registry();
let consumer_src = r#"/** import macro { $vec } from "./nonexistent" */"#;
let consumer_path = std::path::PathBuf::from("/project/src/consumer.ts");
let resolved = resolve_cross_file_imports(consumer_src, &consumer_path, ®istry);
assert!(resolved.imported.is_empty());
assert_eq!(resolved.diagnostics.len(), 1);
assert!(
resolved.diagnostics[0].message.contains("cannot resolve"),
"expected `cannot resolve` diagnostic, got: {:?}",
resolved.diagnostics
);
}
#[test]
fn resolve_cross_file_missing_macro_name_reports_diagnostic() {
let (registry, _) = library_registry();
let consumer_src = r#"/** import macro { $missing } from "./macros" */"#;
let consumer_path = std::path::PathBuf::from("/project/src/consumer.ts");
let resolved = resolve_cross_file_imports(consumer_src, &consumer_path, ®istry);
assert!(resolved.imported.is_empty());
assert_eq!(resolved.diagnostics.len(), 1);
assert!(
resolved.diagnostics[0].message.contains("not defined"),
"got: {}",
resolved.diagnostics[0].message
);
}
#[test]
fn resolve_cross_file_ignores_bare_names() {
let (registry, _) = library_registry();
let consumer_src = r#"/** import macro { Debug, $vec } from "./macros" */"#;
let consumer_path = std::path::PathBuf::from("/project/src/consumer.ts");
let resolved = resolve_cross_file_imports(consumer_src, &consumer_path, ®istry);
assert_eq!(resolved.imported.len(), 1);
assert_eq!(resolved.imported[0].def.name, "vec");
}
#[test]
fn project_registry_resolves_ts_extension() {
let (registry, _) = library_registry();
let importer = std::path::PathBuf::from("/project/src/consumer.ts");
let resolved = registry.resolve_specifier(&importer, "./macros");
assert!(resolved.is_some(), "expected ./macros to resolve");
}
#[test]
fn project_registry_json_roundtrip() {
let (registry, _) = library_registry();
let json = registry.to_json().expect("serialize");
let parsed = ProjectDeclarativeRegistry::from_json(&json).expect("deserialize");
assert_eq!(parsed.file_count(), 1);
assert_eq!(parsed.macro_count(), 1);
}
#[test]
fn object_form_explicit_share_only() {
let source = r#"import { macroRules } from "macroforge/rules";
const $serialize = macroRules({
mode: "share-only",
expand: macroRules`
($x:Expr) => __inline_fallback($x)
`,
runtime: "function __serialize(value, schema) { return { value, schema }; }",
call: macroRules`
($x:Expr) => __serialize($x, [])
`,
});
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
assert!(
parsed.errors.is_empty(),
"parse errors: {:?}",
parsed.errors
);
let defs = discover(&parsed.program, source).expect("discover");
assert_eq!(defs.len(), 1);
let def = &defs[0].def;
assert_eq!(def.name, "serialize");
assert_eq!(def.mode, MacroMode::ShareOnly);
assert!(def.runtime.is_some());
assert!(def.runtime.as_ref().unwrap().contains("__serialize"));
assert!(def.call_arms.is_some());
assert_eq!(def.call_arms.as_ref().unwrap().len(), 1);
assert_eq!(def.arms.len(), 1); }
#[test]
fn object_form_auto_mode() {
let source = r#"import { macroRules } from "macroforge/rules";
const $vec = macroRules({
mode: "auto",
expand: macroRules`
() => []
($($x:Expr),+) => [$($x),+]
`,
runtime: "function __vec(args) { return args; }",
call: macroRules`
($($x:Expr),+) => __vec([$($x),+])
`,
});
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
let defs = discover(&parsed.program, source).expect("discover");
assert_eq!(defs.len(), 1);
assert_eq!(defs[0].def.mode, MacroMode::Auto);
assert_eq!(defs[0].def.arms.len(), 2);
}
#[test]
fn object_form_defaults_to_expand_only_when_no_runtime() {
let source = r#"import { macroRules } from "macroforge/rules";
const $id = macroRules({
expand: macroRules`
($x:Expr) => $x
`,
});
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
let defs = discover(&parsed.program, source).expect("discover");
assert_eq!(defs.len(), 1);
assert_eq!(defs[0].def.mode, MacroMode::ExpandOnly);
assert!(defs[0].def.runtime.is_none());
assert!(defs[0].def.call_arms.is_none());
}
#[test]
fn object_form_defaults_to_auto_with_runtime_and_call() {
let source = r#"import { macroRules } from "macroforge/rules";
const $x = macroRules({
expand: macroRules`($y:Expr) => $y`,
runtime: "function __h(v) { return v; }",
call: macroRules`($y:Expr) => __h($y)`,
});
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
let defs = discover(&parsed.program, source).expect("discover");
assert_eq!(defs[0].def.mode, MacroMode::Auto);
}
#[test]
fn object_form_requires_expand_field() {
let source = r#"import { macroRules } from "macroforge/rules";
const $broken = macroRules({
mode: "expand-only",
});
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
let err = discover(&parsed.program, source).unwrap_err();
assert!(
err.message.contains("`expand`"),
"expected error about missing expand, got: {}",
err.message
);
}
#[test]
fn object_form_share_mode_requires_runtime_and_call() {
let source = r#"import { macroRules } from "macroforge/rules";
const $broken = macroRules({
mode: "share-only",
expand: macroRules`($x:Expr) => $x`,
});
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
let err = discover(&parsed.program, source).unwrap_err();
assert!(
err.message.contains("runtime") && err.message.contains("call"),
"expected error about missing runtime/call, got: {}",
err.message
);
}
#[test]
fn object_form_rejects_unknown_mode_string() {
let source = r#"import { macroRules } from "macroforge/rules";
const $broken = macroRules({
mode: "yolo",
expand: macroRules`($x:Expr) => $x`,
});
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
let err = discover(&parsed.program, source).unwrap_err();
assert!(
err.message.contains("yolo") && err.message.contains("auto"),
"expected error listing valid modes, got: {}",
err.message
);
}
#[test]
fn object_form_rejects_unknown_option_key() {
let source = r#"import { macroRules } from "macroforge/rules";
const $broken = macroRules({
expand: macroRules`($x:Expr) => $x`,
mystery: "what is this",
});
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
let err = discover(&parsed.program, source).unwrap_err();
assert!(
err.message.contains("mystery"),
"expected error listing the unknown key, got: {}",
err.message
);
}
#[test]
fn object_form_accepts_custom_megamorphism_threshold() {
let source = r#"import { macroRules } from "macroforge/rules";
const $x = macroRules({
mode: "auto",
expand: macroRules`($y:Expr) => $y`,
runtime: "function __h(v) { return v; }",
call: macroRules`($y:Expr) => __h($y)`,
megamorphismThreshold: 8,
});
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
let defs = discover(&parsed.program, source).expect("discover");
assert_eq!(defs[0].def.megamorphism_threshold, 8);
}
#[test]
fn type_macro_simple_replaces_type_reference() {
let source = r#"import { macroRules } from "macroforge/rules";
const $Wrap = macroRules({
kind: "type",
expand: macroRules`($t:Type) => { wrapped: $t }`,
});
type Result = $Wrap<string>;
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
assert!(
parsed.errors.is_empty(),
"parse errors: {:?}",
parsed.errors
);
let discovered = discover(&parsed.program, source).expect("discover");
assert_eq!(discovered.len(), 1, "should discover one type macro");
assert_eq!(
discovered[0].def.kind,
crate::ts_syn::declarative::MacroKind::Type
);
let mut registry = DeclarativeMacroRegistry::new();
for dm in &discovered {
registry.register(dm.def.clone()).unwrap();
}
let out = rewrite(
&parsed.program,
source,
®istry,
&discovered,
BuildMode::dev(),
None,
None,
);
let applied = crate::host::patch_applicator::PatchApplicator::new(source, out.patches.clone())
.apply()
.expect("apply");
assert!(
applied.contains("{ wrapped: string }"),
"expected expanded type body, got:\n{}",
applied
);
assert!(
!applied.contains("$Wrap"),
"expanded output still has `$Wrap`:\n{}",
applied
);
assert!(
out.diagnostics.is_empty(),
"unexpected diagnostics: {:#?}",
out.diagnostics
);
}
#[test]
fn type_macro_repetition_expands_tuple() {
let source = r#"import { macroRules } from "macroforge/rules";
const $Tup = macroRules({
kind: "type",
expand: macroRules`($($t:Type),+) => [$($t),+]`,
});
type T = $Tup<string, number>;
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
assert!(
parsed.errors.is_empty(),
"parse errors: {:?}",
parsed.errors
);
let discovered = discover(&parsed.program, source).expect("discover");
let mut registry = DeclarativeMacroRegistry::new();
for dm in &discovered {
registry.register(dm.def.clone()).unwrap();
}
let out = rewrite(
&parsed.program,
source,
®istry,
&discovered,
BuildMode::dev(),
None,
None,
);
let applied = crate::host::patch_applicator::PatchApplicator::new(source, out.patches.clone())
.apply()
.expect("apply");
assert!(
applied.contains("[string,number]") || applied.contains("[string, number]"),
"expected tuple expansion in output:\n{}",
applied
);
assert!(
out.diagnostics.is_empty(),
"unexpected diagnostics: {:#?}",
out.diagnostics
);
}
#[test]
fn type_macro_rejects_sharing_mode() {
let source = r#"import { macroRules } from "macroforge/rules";
const $Bad = macroRules({
kind: "type",
mode: "share-only",
expand: macroRules`($t:Type) => $t`,
runtime: `function __bad() {}`,
call: macroRules`($t:Type) => $t`,
});
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
let err = discover(&parsed.program, source).unwrap_err();
assert!(
err.message
.contains("type-position macros cannot use sharing modes"),
"expected sharing-mode rejection, got: {}",
err.message
);
}
#[test]
fn type_macro_used_in_value_position_emits_error() {
let source = r#"import { macroRules } from "macroforge/rules";
const $Foo = macroRules({
kind: "type",
expand: macroRules`($t:Type) => $t`,
});
const x = $Foo(1);
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
let discovered = discover(&parsed.program, source).expect("discover");
let mut registry = DeclarativeMacroRegistry::new();
for dm in &discovered {
registry.register(dm.def.clone()).unwrap();
}
let out = rewrite(
&parsed.program,
source,
®istry,
&discovered,
BuildMode::dev(),
None,
None,
);
let _ = out;
}
fn rewrite_source(source: &str, build_mode: BuildMode) -> super::rewriter::RewriteOutput {
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
assert!(
parsed.errors.is_empty(),
"parse errors: {:?}",
parsed.errors
);
let discovered = discover(&parsed.program, source).expect("discover");
let mut registry = DeclarativeMacroRegistry::new();
for dm in &discovered {
registry
.register_scoped(dm.def.clone(), dm.scope_span)
.unwrap();
}
rewrite(
&parsed.program,
source,
®istry,
&discovered,
build_mode,
None,
None,
)
}
#[test]
fn share_only_emits_runtime_once_per_file() {
let source = r#"import { macroRules } from "macroforge/rules";
const $serialize = macroRules({
mode: "share-only",
expand: macroRules`
($x:Expr) => __inline_fallback($x)
`,
runtime: "function __serialize(value, schema) { return value; }",
call: macroRules`
($x:Expr) => __serialize($x, [])
`,
});
const a = $serialize(user);
const b = $serialize(admin);
"#;
let out = rewrite_source(source, BuildMode::dev());
assert!(out.diagnostics.is_empty(), "diag: {:?}", out.diagnostics);
let runtime_inserts = out
.patches
.iter()
.filter(|p| matches!(p, crate::ts_syn::abi::Patch::Insert { .. }))
.count();
assert_eq!(
runtime_inserts, 1,
"expected exactly 1 runtime insert, got {}: {:#?}",
runtime_inserts, out.patches
);
let replaces = out
.patches
.iter()
.filter(|p| matches!(p, crate::ts_syn::abi::Patch::Replace { .. }))
.count();
assert_eq!(
replaces, 2,
"expected 2 call-site replaces, got {}",
replaces
);
}
#[test]
fn share_only_uses_call_arms_not_expand_arms() {
let source = r#"import { macroRules } from "macroforge/rules";
const $serialize = macroRules({
mode: "share-only",
expand: macroRules`
($x:Expr) => __inline_fallback($x)
`,
runtime: "function __serialize(value) { return value; }",
call: macroRules`
($x:Expr) => __serialize($x)
`,
});
const result = $serialize(user);
"#;
let out = rewrite_source(source, BuildMode::dev());
assert!(out.diagnostics.is_empty(), "diag: {:?}", out.diagnostics);
let replace = out
.patches
.iter()
.find_map(|p| match p {
crate::ts_syn::abi::Patch::Replace { code, .. } => code.as_text(),
_ => None,
})
.expect("expected at least one Replace patch");
assert!(
replace.contains("__serialize"),
"expected call-arms expansion (__serialize), got: {}",
replace
);
assert!(
!replace.contains("__inline_fallback"),
"expand-mode fallback leaked into share output: {}",
replace
);
}
#[test]
fn expand_only_tag_form_ignores_build_mode() {
let source = r#"import { macroRules } from "macroforge/rules";
const $vec = macroRules`
() => []
($($x:Expr),+) => [$($x),+]
`;
const xs = $vec(1, 2, 3);
"#;
let dev = rewrite_source(source, BuildMode::dev());
let prod = rewrite_source(source, BuildMode::Prod);
assert_eq!(dev.patches.len(), prod.patches.len());
let dev_runtime_inserts = dev
.patches
.iter()
.filter(|p| matches!(p, crate::ts_syn::abi::Patch::Insert { .. }))
.count();
assert_eq!(dev_runtime_inserts, 0);
}
#[test]
fn auto_mode_dev_behaves_like_expand_only() {
let source = r#"import { macroRules } from "macroforge/rules";
const $id = macroRules({
mode: "auto",
expand: macroRules`
($x:Expr) => $x
`,
runtime: "function __id(v) { return v; }",
call: macroRules`
($x:Expr) => __id($x)
`,
});
const a = $id(42);
"#;
let out = rewrite_source(source, BuildMode::dev());
let runtime_inserts = out
.patches
.iter()
.filter(|p| matches!(p, crate::ts_syn::abi::Patch::Insert { .. }))
.count();
assert_eq!(runtime_inserts, 0, "Dev Auto should not emit runtime");
let has_id_call = out.patches.iter().any(|p| match p {
crate::ts_syn::abi::Patch::Replace { code, .. } => {
code.as_text().is_some_and(|t| t.contains("__id"))
}
_ => false,
});
assert!(!has_id_call, "Dev Auto should not call runtime helper");
}
#[test]
fn auto_mode_prod_emits_megamorphism_warning_for_many_shapes() {
let source = r#"import { macroRules } from "macroforge/rules";
class UserA {}
class UserB {}
class UserC {}
class UserD {}
class UserE {}
class UserF {}
const $serialize = macroRules({
mode: "auto",
expand: macroRules`($x:Expr) => __inline($x)`,
runtime: "function __serialize(v) { return v; }",
call: macroRules`($x:Expr) => __serialize($x)`,
});
export const a = $serialize(UserA);
export const b = $serialize(UserB);
export const c = $serialize(UserC);
export const d = $serialize(UserD);
export const e = $serialize(UserE);
export const f = $serialize(UserF);
"#;
let out = rewrite_source(source, BuildMode::Prod);
let warning = out
.diagnostics
.iter()
.find(|d| matches!(d.level, crate::ts_syn::abi::DiagnosticLevel::Warning))
.expect("expected a megamorphism warning");
assert!(
warning.message.contains("serialize"),
"warning should mention the macro name: {}",
warning.message
);
}
#[test]
fn auto_mode_prod_shares_for_few_shapes() {
let source = r#"import { macroRules } from "macroforge/rules";
class User {}
class Admin {}
class Guest {}
const $serialize = macroRules({
mode: "auto",
expand: macroRules`($x:Expr) => __inline($x)`,
runtime: "function __serialize(v) { return v; }",
call: macroRules`($x:Expr) => __serialize($x)`,
});
export const a = $serialize(User);
export const b = $serialize(Admin);
export const c = $serialize(Guest);
"#;
let out = rewrite_source(source, BuildMode::Prod);
let runtime_inserts = out
.patches
.iter()
.filter(|p| matches!(p, crate::ts_syn::abi::Patch::Insert { .. }))
.count();
assert_eq!(
runtime_inserts, 1,
"expected exactly 1 runtime insert for Auto + shareable shapes"
);
let warnings = out
.diagnostics
.iter()
.filter(|d| matches!(d.level, crate::ts_syn::abi::DiagnosticLevel::Warning))
.count();
assert_eq!(warnings, 0);
}
#[test]
fn auto_mode_prod_shares_by_default() {
let source = r#"import { macroRules } from "macroforge/rules";
const $id = macroRules({
mode: "auto",
expand: macroRules`
($x:Expr) => $x
`,
runtime: "function __id(v) { return v; }",
call: macroRules`
($x:Expr) => __id($x)
`,
});
const a = $id(42);
const b = $id(99);
"#;
let out = rewrite_source(source, BuildMode::Prod);
let runtime_inserts = out
.patches
.iter()
.filter(|p| matches!(p, crate::ts_syn::abi::Patch::Insert { .. }))
.count();
assert_eq!(runtime_inserts, 1, "Prod Auto should emit runtime once");
}
#[test]
fn tag_form_still_works_after_object_form_support() {
let source = r#"import { macroRules } from "macroforge/rules";
const $vec = macroRules`
() => []
($($x:Expr),+) => [$($x),+]
`;
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
let defs = discover(&parsed.program, source).expect("discover");
assert_eq!(defs.len(), 1);
assert_eq!(defs[0].def.mode, MacroMode::ExpandOnly);
assert!(defs[0].def.runtime.is_none());
assert!(defs[0].def.call_arms.is_none());
assert_eq!(defs[0].def.megamorphism_threshold, 4);
}
fn find_first_call<'a>(
program: &'a oxc::ast::ast::Program<'a>,
) -> Option<&'a oxc::ast::ast::CallExpression<'a>> {
use oxc::ast::ast::{Expression, Statement};
for stmt in &program.body {
if let Statement::ExpressionStatement(es) = stmt
&& let Expression::CallExpression(call) = &es.expression
{
return Some(call);
}
}
None
}
fn rewrite_tsx_source(source: &str, build_mode: BuildMode) -> super::rewriter::RewriteOutput {
let allocator = Allocator::default();
let parsed = Parser::new(&allocator, source, SourceType::tsx()).parse();
assert!(
parsed.errors.is_empty(),
"parse errors: {:?}",
parsed.errors
);
let discovered = discover(&parsed.program, source).expect("discover");
let mut registry = DeclarativeMacroRegistry::new();
for dm in &discovered {
registry.register(dm.def.clone()).unwrap();
}
rewrite(
&parsed.program,
source,
®istry,
&discovered,
build_mode,
None,
None,
)
}
#[test]
fn rewrites_macro_call_inside_jsx_expression_container() {
let source = r#"import { macroRules } from "macroforge/rules";
const $id = macroRules`($x:Expr) => $x`;
const el = <div prop={$id(42)} />;
"#;
let out = rewrite_tsx_source(source, BuildMode::dev());
assert!(out.diagnostics.is_empty(), "diag: {:?}", out.diagnostics);
let replaces: Vec<&crate::ts_syn::abi::Patch> = out
.patches
.iter()
.filter(|p| matches!(p, crate::ts_syn::abi::Patch::Replace { .. }))
.collect();
assert_eq!(
replaces.len(),
1,
"expected exactly 1 Replace inside JSX prop, got: {:#?}",
replaces
);
if let crate::ts_syn::abi::Patch::Replace { code, .. } = replaces[0]
&& let Some(text) = code.as_text()
{
assert!(
text.contains("42"),
"expansion should contain the literal: {}",
text
);
}
}
#[test]
fn rewrites_macro_call_inside_class_field_initializer() {
let source = r#"import { macroRules } from "macroforge/rules";
const $double = macroRules`($x:Expr) => ($x * 2)`;
class Box {
value = $double(7);
}
"#;
let out = rewrite_source(source, BuildMode::dev());
assert!(out.diagnostics.is_empty(), "diag: {:?}", out.diagnostics);
let replaces: Vec<&crate::ts_syn::abi::Patch> = out
.patches
.iter()
.filter(|p| matches!(p, crate::ts_syn::abi::Patch::Replace { .. }))
.collect();
assert_eq!(
replaces.len(),
1,
"expected macro in class field initializer to be rewritten, got: {:#?}",
out.patches
);
}
#[test]
fn rewrites_macro_call_inside_decorator_argument() {
let source = r#"import { macroRules } from "macroforge/rules";
const $lit = macroRules`($x:Expr) => $x`;
function deco(_x: unknown) {
return (target: unknown) => target;
}
@deco($lit("hello"))
class Target {}
"#;
let out = rewrite_source(source, BuildMode::dev());
assert!(out.diagnostics.is_empty(), "diag: {:?}", out.diagnostics);
let replaces: Vec<&crate::ts_syn::abi::Patch> = out
.patches
.iter()
.filter(|p| matches!(p, crate::ts_syn::abi::Patch::Replace { .. }))
.collect();
assert_eq!(
replaces.len(),
1,
"expected macro inside decorator argument to be rewritten, got: {:#?}",
out.patches
);
}
#[test]
fn hygiene_preserves_string_and_comment_mentions_of_declared_ident() {
let source = r#"import { macroRules } from "macroforge/rules";
const $tricky = macroRules`
() => {
const __v = 1;
// __v should stay literal in this comment
const note = "original __v value";
__v + 2
}
`;
const x = $tricky();
"#;
let out = rewrite_source(source, BuildMode::dev());
assert!(out.diagnostics.is_empty(), "diag: {:?}", out.diagnostics);
let expanded = out
.patches
.iter()
.find_map(|p| match p {
crate::ts_syn::abi::Patch::Replace { code, .. } => code.as_text(),
_ => None,
})
.expect("expected a Replace patch for $tricky()");
assert!(
expanded.contains("// __v should stay literal in this comment"),
"comment text was modified: {}",
expanded
);
assert!(
expanded.contains("\"original __v value\""),
"string literal was modified: {}",
expanded
);
assert!(
expanded.contains("__v$"),
"expected the declaration to be renamed with a suffix: {}",
expanded
);
}
#[test]
fn rewrites_type_macro_inside_tuple_element() {
let source = r#"import { macroRules } from "macroforge/rules";
const $wrap = macroRules({
kind: "type",
expand: macroRules`($t:Type) => { wrapped: $t }`,
});
type T = [$wrap<string>, number];
"#;
let out = rewrite_source(source, BuildMode::dev());
assert!(
out.diagnostics.is_empty(),
"expected no diagnostics, got: {:?}",
out.diagnostics
);
let replaces: Vec<&crate::ts_syn::abi::Patch> = out
.patches
.iter()
.filter(|p| matches!(p, crate::ts_syn::abi::Patch::Replace { .. }))
.collect();
assert_eq!(
replaces.len(),
1,
"expected type-macro in tuple element to be rewritten, got: {:#?}",
out.patches
);
if let crate::ts_syn::abi::Patch::Replace { code, .. } = replaces[0]
&& let Some(text) = code.as_text()
{
assert!(
text.contains("wrapped"),
"expansion should contain the wrapper shape: {}",
text
);
}
}
#[test]
fn validate_expanded_source_returns_empty_on_valid_input() {
let source = "const x: number = 1 + 2;";
let mut mapping = crate::ts_syn::abi::SourceMapping::new();
mapping.add_generated(crate::ts_syn::abi::GeneratedRegion::new(
0,
source.len() as u32,
"$fake",
));
let diags = crate::host::declarative::validate_expanded_source(source, &mapping, false);
assert!(
diags.is_empty(),
"expected no diagnostics, got: {:?}",
diags
);
}
#[test]
fn validate_expanded_source_attributes_to_generating_macro() {
let prefix = "const ok = 1;\n";
let invalid = "const x = ;"; let source = format!("{}{}", prefix, invalid);
let mut mapping = crate::ts_syn::abi::SourceMapping::new();
mapping.add_segment(crate::ts_syn::abi::MappingSegment::new(
0,
prefix.len() as u32,
0,
prefix.len() as u32,
));
mapping.add_generated(crate::ts_syn::abi::GeneratedRegion::new(
prefix.len() as u32,
source.len() as u32,
"$bad_macro",
));
let diags = crate::host::declarative::validate_expanded_source(&source, &mapping, false);
assert!(
!diags.is_empty(),
"expected at least one diagnostic for invalid source"
);
let attributed = diags.iter().any(|d| d.message.contains("$bad_macro"));
assert!(
attributed,
"expected diagnostic to blame `$bad_macro`, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn dev_mode_default_still_inlines_auto_macros() {
let source = r#"import { macroRules } from "macroforge/rules";
const $h = macroRules({
mode: "auto",
expand: macroRules`($x:Expr) => ($x + 1)`,
runtime: "function __h(x) { return x; }",
call: macroRules`($x:Expr) => __h($x)`,
});
const a = $h(User);
const b = $h(User);
"#;
let out = rewrite_source(source, BuildMode::dev());
let inserts = out
.patches
.iter()
.filter(|p| matches!(p, crate::ts_syn::abi::Patch::Insert { .. }))
.count();
assert_eq!(
inserts, 0,
"expected zero runtime inserts in plain dev mode (Auto should inline), got: {}",
inserts
);
}
#[test]
fn dev_mode_force_share_produces_share_mode_emission() {
let source = r#"import { macroRules } from "macroforge/rules";
const $h = macroRules({
mode: "auto",
expand: macroRules`($x:Expr) => ($x + 1)`,
runtime: "function __h(x) { return x; }",
call: macroRules`($x:Expr) => __h($x)`,
});
const a = $h(User);
const b = $h(User);
"#;
let build_mode = crate::host::declarative::BuildMode::Dev {
analyzer_telemetry: false,
force_share: true,
};
let out = rewrite_source(source, build_mode);
let inserts = out
.patches
.iter()
.filter(|p| matches!(p, crate::ts_syn::abi::Patch::Insert { .. }))
.count();
assert_eq!(
inserts, 1,
"expected exactly one runtime insert in force_share dev mode, got: {}",
inserts
);
let replaces: Vec<&str> = out
.patches
.iter()
.filter_map(|p| match p {
crate::ts_syn::abi::Patch::Replace { code, .. } => code.as_text(),
_ => None,
})
.collect();
assert_eq!(replaces.len(), 2, "got: {:?}", replaces);
for r in &replaces {
assert!(
r.contains("__h("),
"expected call_arms expansion to reference the helper, got: {}",
r
);
}
}
#[test]
fn cluster_id_appears_in_attribution_for_clustered_emissions() {
let source = r#"import { macroRules } from "macroforge/rules";
const $serialize = macroRules({
mode: "auto",
expand: macroRules`($x:Expr) => __inline_fallback($x)`,
runtime: "function __serialize(value) { return value; }",
runtimeName: "__serialize_$__cluster__",
call: macroRules`($x:Expr) => __serialize_$__cluster__($x)`,
megamorphismThreshold: 2,
});
const a1 = $serialize(Alice);
const b1 = $serialize(Barbara);
const b2 = $serialize(Bert);
"#;
let out = rewrite_source(source, BuildMode::prod());
let attributions: Vec<String> = out
.patches
.iter()
.filter_map(|p| match p {
crate::ts_syn::abi::Patch::Insert { source_macro, .. } => source_macro.clone(),
crate::ts_syn::abi::Patch::Replace { source_macro, .. } => source_macro.clone(),
_ => None,
})
.collect();
let clustered: Vec<&String> = attributions
.iter()
.filter(|a| a.starts_with("$serialize@"))
.collect();
assert!(
clustered.len() >= 2,
"expected at least two cluster-attributed patches (got: {:?})",
attributions
);
let distinct_cluster_ids: std::collections::HashSet<&str> = clustered
.iter()
.filter_map(|a| a.strip_prefix("$serialize@"))
.collect();
assert!(
distinct_cluster_ids.len() >= 2,
"expected at least 2 distinct cluster ids, got: {:?}",
distinct_cluster_ids
);
}
#[test]
fn attribution_has_no_cluster_suffix_for_non_clustered_emissions() {
let source = r#"import { macroRules } from "macroforge/rules";
const $h = macroRules({
mode: "share-only",
expand: macroRules`($x:Expr) => __inline_fallback($x)`,
runtime: "function __h(x) { return x; }",
call: macroRules`($x:Expr) => __h($x)`,
});
const a = $h(User);
"#;
let out = rewrite_source(source, BuildMode::prod());
for patch in &out.patches {
let attr = match patch {
crate::ts_syn::abi::Patch::Insert { source_macro, .. } => source_macro.clone(),
crate::ts_syn::abi::Patch::Replace { source_macro, .. } => source_macro.clone(),
_ => None,
};
if let Some(attr) = attr {
assert!(
!attr.contains('@'),
"non-clustered attribution should not contain `@`, got: {}",
attr
);
}
}
}
#[test]
fn analyzer_telemetry_emits_info_diagnostic_per_macro() {
let source = r#"import { macroRules } from "macroforge/rules";
const $h = macroRules({
mode: "auto",
expand: macroRules`($x:Expr) => $x`,
runtime: "function __h(x) { return x; }",
call: macroRules`($x:Expr) => __h($x)`,
});
const a = $h(User);
const b = $h(User);
"#;
let build_mode = crate::host::declarative::BuildMode::Dev {
analyzer_telemetry: true,
force_share: true,
};
let out = rewrite_source(source, build_mode);
let info_diag = out
.diagnostics
.iter()
.find(|d| {
matches!(d.level, crate::ts_syn::abi::DiagnosticLevel::Info)
&& d.message.contains("analyzer decision for macro `$h`")
})
.expect("expected analyzer-decision Info diagnostic");
assert!(
info_diag.message.contains("Share"),
"expected `Share` in telemetry, got: {}",
info_diag.message
);
}
#[test]
fn silent_fallback_notice_emitted_when_no_type_registry() {
let source = r#"import { macroRules } from "macroforge/rules";
const $h = macroRules({
mode: "auto",
expand: macroRules`($x:Expr) => $x`,
runtime: "function __h(x) { return x; }",
call: macroRules`($x:Expr) => __h($x)`,
});
const a = $h(User);
"#;
let out = rewrite_source(source, BuildMode::prod());
let fallback_diags: Vec<_> = out
.diagnostics
.iter()
.filter(|d| {
matches!(d.level, crate::ts_syn::abi::DiagnosticLevel::Info)
&& d.message.contains("without a type registry")
})
.collect();
assert_eq!(
fallback_diags.len(),
1,
"expected exactly one type-registry-fallback notice, got: {:?}",
fallback_diags
);
}
#[test]
fn nested_macro_definition_inside_function_body_is_discovered() {
let source = r#"import { macroRules } from "macroforge/rules";
function factory() {
const $local = macroRules`($x:Expr) => ($x + 100)`;
const result = $local(7);
return result;
}
"#;
let out = rewrite_source(source, BuildMode::dev());
assert!(
out.diagnostics.is_empty(),
"expected no diagnostics, got: {:?}",
out.diagnostics
);
let replaces: Vec<&str> = out
.patches
.iter()
.filter_map(|p| match p {
crate::ts_syn::abi::Patch::Replace { code, .. } => code.as_text(),
_ => None,
})
.collect();
assert_eq!(
replaces.len(),
1,
"expected nested call site to be rewritten, got: {:#?}",
out.patches
);
assert!(
replaces[0].contains("7") && replaces[0].contains("100"),
"expected the inner expansion `(7 + 100)`, got: {}",
replaces[0]
);
}
#[test]
fn nested_macro_shadows_outer_with_same_name() {
let source = r#"import { macroRules } from "macroforge/rules";
const $foo = macroRules`($x:Expr) => ($x + 1)`;
const outer = $foo(10);
function inner() {
const $foo = macroRules`($x:Expr) => ($x * 100)`;
const shadowed = $foo(2);
return shadowed;
}
"#;
let out = rewrite_source(source, BuildMode::dev());
assert!(out.diagnostics.is_empty(), "diag: {:?}", out.diagnostics);
let replaces: Vec<&str> = out
.patches
.iter()
.filter_map(|p| match p {
crate::ts_syn::abi::Patch::Replace { code, .. } => code.as_text(),
_ => None,
})
.collect();
assert_eq!(replaces.len(), 2, "got: {:?}", replaces);
let has_outer = replaces
.iter()
.any(|r| r.contains("10") && r.contains("+ 1"));
let has_inner = replaces
.iter()
.any(|r| r.contains("2") && r.contains("* 100"));
assert!(
has_outer,
"outer call should use outer def, got: {:?}",
replaces
);
assert!(
has_inner,
"inner call should use inner def (shadowing), got: {:?}",
replaces
);
}
#[test]
fn duplicate_nested_declarations_in_same_block_error() {
let source = r#"import { macroRules } from "macroforge/rules";
function f() {
const $x = macroRules`() => 1`;
const $x = macroRules`() => 2`;
}
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
let discovered = discover(&parsed.program, source).expect("discover");
let mut registry = DeclarativeMacroRegistry::new();
registry
.register_scoped(discovered[0].def.clone(), discovered[0].scope_span)
.expect("first registration should succeed");
let result = registry.register_scoped(discovered[1].def.clone(), discovered[1].scope_span);
assert!(
matches!(result, Err(RegistryError::DuplicateName(_))),
"expected DuplicateName error, got: {:?}",
result
);
}
#[test]
fn disjoint_nested_scopes_allow_same_name() {
let source = r#"import { macroRules } from "macroforge/rules";
function alpha() {
const $helper = macroRules`($x:Expr) => ($x + 1)`;
return $helper(10);
}
function beta() {
const $helper = macroRules`($x:Expr) => ($x + 2)`;
return $helper(20);
}
"#;
let out = rewrite_source(source, BuildMode::dev());
assert!(
out.diagnostics.is_empty(),
"expected no diagnostics — disjoint scopes should not collide, got: {:?}",
out.diagnostics
);
let replaces: Vec<&str> = out
.patches
.iter()
.filter_map(|p| match p {
crate::ts_syn::abi::Patch::Replace { code, .. } => code.as_text(),
_ => None,
})
.collect();
assert_eq!(replaces.len(), 2, "got: {:?}", replaces);
assert!(
replaces
.iter()
.any(|r| r.contains("10") && r.contains("+ 1")),
"alpha() should expand with `+ 1`, got: {:?}",
replaces
);
assert!(
replaces
.iter()
.any(|r| r.contains("20") && r.contains("+ 2")),
"beta() should expand with `+ 2`, got: {:?}",
replaces
);
}
#[test]
fn repetition_with_tail_matches_via_backtracking() {
let source = r#"import { macroRules } from "macroforge/rules";
const $splitLast = macroRules`
($($x:Expr),* $last:Expr) => { last: $last, rest: [$($x),*] }
`;
const out = $splitLast(1, 2, 3);
"#;
let out = rewrite_source(source, BuildMode::dev());
assert!(
out.diagnostics.is_empty(),
"expected no diagnostics (backtracking should succeed), got: {:?}",
out.diagnostics
);
let expansion = out
.patches
.iter()
.find_map(|p| match p {
crate::ts_syn::abi::Patch::Replace { code, .. } => code.as_text(),
_ => None,
})
.expect("expected a Replace patch for the call site");
assert!(
expansion.contains("last: 3"),
"expected `$last` bound to 3, got: {}",
expansion
);
assert!(
expansion.contains("rest: [1,2]") || expansion.contains("rest: [1, 2]"),
"expected `$x` bound to [1, 2], got: {}",
expansion
);
}
#[test]
fn repetition_plus_tail_at_minimum_count_still_matches() {
let source = r#"import { macroRules } from "macroforge/rules";
const $oneThenLast = macroRules`
($($x:Expr),+ $last:Expr) => { first: $($x),+, final: $last }
`;
const out = $oneThenLast(1, 2);
"#;
let out = rewrite_source(source, BuildMode::dev());
assert!(out.diagnostics.is_empty(), "diag: {:?}", out.diagnostics);
}
#[test]
fn repetition_plus_tail_rejects_insufficient_args() {
let source = r#"import { macroRules } from "macroforge/rules";
const $oneThenLast = macroRules`
($($x:Expr),+ $last:Expr) => { first: $($x),+, final: $last }
`;
const out = $oneThenLast(1);
"#;
let out = rewrite_source(source, BuildMode::dev());
let has_error = out.diagnostics.iter().any(|d| {
matches!(d.level, crate::ts_syn::abi::DiagnosticLevel::Error)
&& d.message.contains("did not match any arm")
});
assert!(
has_error,
"expected match-failure diagnostic, got: {:?}",
out.diagnostics
);
}
#[test]
fn deep_composition_chain_expands_quickly() {
let source = r#"import { macroRules } from "macroforge/rules";
const $double = macroRules`($x:Expr) => ($x * 2)`;
const $q = macroRules`($x:Expr) => $double($double($double($double($double($double($double($double($double($double($double($double($double($double($double($double($x))))))))))))))))`;
const result = $q(1);
"#;
let start = std::time::Instant::now();
let out = rewrite_source(source, BuildMode::dev());
let elapsed = start.elapsed();
assert!(
out.diagnostics.is_empty(),
"expected no diagnostics, got: {:?}",
out.diagnostics
);
assert!(
elapsed.as_millis() < 500,
"deep composition took too long: {:?} (regression?)",
elapsed
);
}
#[test]
fn cluster_aware_auto_macro_emits_one_helper_per_cluster_in_prod() {
let source = r#"import { macroRules } from "macroforge/rules";
const $serialize = macroRules({
mode: "auto",
expand: macroRules`($x:Expr) => __inline_fallback($x)`,
runtime: "function __serialize(value) { return value; }",
runtimeName: "__serialize_$__cluster__",
call: macroRules`($x:Expr) => __serialize_$__cluster__($x)`,
megamorphismThreshold: 2,
});
const a1 = $serialize(Alice);
const b1 = $serialize(Barbara);
const b2 = $serialize(Bert);
"#;
let out = rewrite_source(source, BuildMode::Prod);
let error_diags: Vec<_> = out
.diagnostics
.iter()
.filter(|d| matches!(d.level, crate::ts_syn::abi::DiagnosticLevel::Error))
.collect();
assert!(
error_diags.is_empty(),
"expected no errors, got: {:?}",
error_diags
);
let inserts: Vec<&crate::ts_syn::abi::Patch> = out
.patches
.iter()
.filter(|p| matches!(p, crate::ts_syn::abi::Patch::Insert { .. }))
.collect();
assert_eq!(
inserts.len(),
2,
"expected exactly 2 cluster-specialized runtime helpers, got {}: {:#?}",
inserts.len(),
inserts
);
let combined: String = inserts
.iter()
.filter_map(|p| match p {
crate::ts_syn::abi::Patch::Insert { code, .. } => code.as_text(),
_ => None,
})
.collect::<Vec<_>>()
.join("\n");
assert!(
combined.contains("__serialize_a"),
"expected helper specialized for cluster `a`, got: {}",
combined
);
assert!(
combined.contains("__serialize_b"),
"expected helper specialized for cluster `b`, got: {}",
combined
);
}
#[test]
fn cluster_aware_call_sites_reference_their_own_cluster_helper() {
let source = r#"import { macroRules } from "macroforge/rules";
const $h = macroRules({
mode: "auto",
expand: macroRules`($x:Expr) => __inline_fallback($x)`,
runtime: "function __h(x) { return x; }",
runtimeName: "__h_$__cluster__",
call: macroRules`($x:Expr) => __h_$__cluster__($x)`,
megamorphismThreshold: 1,
});
const p = $h(Alpha);
const q = $h(Bravo);
"#;
let out = rewrite_source(source, BuildMode::Prod);
let replaces: Vec<&str> = out
.patches
.iter()
.filter_map(|p| match p {
crate::ts_syn::abi::Patch::Replace { code, .. } => code.as_text(),
_ => None,
})
.collect();
assert_eq!(
replaces.len(),
2,
"expected 2 call-site replacements, got: {:#?}",
replaces
);
let all: String = replaces.join("\n");
assert!(
all.contains("__h_a"),
"expected one call site to target `__h_a`, got: {}",
all
);
assert!(
all.contains("__h_b"),
"expected one call site to target `__h_b`, got: {}",
all
);
}
#[test]
fn share_single_path_still_emits_exactly_one_helper() {
let source = r#"import { macroRules } from "macroforge/rules";
const $h = macroRules({
mode: "share-only",
expand: macroRules`($x:Expr) => __inline_fallback($x)`,
runtime: "function __h(x) { return x; }",
call: macroRules`($x:Expr) => __h($x)`,
});
const a = $h(User);
const b = $h(Admin);
const c = $h(Guest);
"#;
let out = rewrite_source(source, BuildMode::Prod);
let inserts = out
.patches
.iter()
.filter(|p| matches!(p, crate::ts_syn::abi::Patch::Insert { .. }))
.count();
assert_eq!(inserts, 1, "expected exactly 1 helper, got: {}", inserts);
}
#[test]
fn runtime_name_template_missing_cluster_placeholder_is_rejected_at_discovery() {
let source = r#"import { macroRules } from "macroforge/rules";
const $broken = macroRules({
mode: "auto",
expand: macroRules`($x:Expr) => $x`,
runtime: "function __h(v) { return v; }",
runtimeName: "__h_static",
call: macroRules`($x:Expr) => __h_static($x)`,
});
"#;
let allocator = Allocator::default();
let parsed = parse_program(&allocator, source);
let err = discover(&parsed.program, source).unwrap_err();
assert!(
err.message.contains("$__cluster__"),
"expected error mentioning `$__cluster__`, got: {}",
err.message
);
assert!(
err.help.as_deref().unwrap_or("").contains("$__cluster__"),
"expected help hint to demonstrate `$__cluster__` usage, got: {:?}",
err.help
);
}
#[test]
fn validate_expanded_source_generic_message_when_not_in_generated_region() {
let source = "const x = ;";
let mut mapping = crate::ts_syn::abi::SourceMapping::new();
mapping.add_segment(crate::ts_syn::abi::MappingSegment::new(
0,
source.len() as u32,
0,
source.len() as u32,
));
let diags = crate::host::declarative::validate_expanded_source(source, &mapping, false);
assert!(!diags.is_empty());
assert!(
diags
.iter()
.all(|d| !d.message.contains('$') || d.message.contains("declarative macro expansion")),
"expected generic message, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn pr19_multi_arg_auto_macro_with_clustering_end_to_end() {
let source = r#"import { macroRules } from "macroforge/rules";
const $serialize = macroRules({
mode: "auto",
expand: macroRules`($v:Expr, $o:Expr) => __inline_fallback($v)`,
runtime: "function __serialize(value, opts) { return value; }",
runtimeName: "__serialize_$__cluster__",
call: macroRules`($v:Expr, $o:Expr) => __serialize_$__cluster__($v, $o)`,
megamorphismThreshold: 2,
});
const a1 = $serialize(Alice, Cfg);
const b1 = $serialize(Barbara, Cfg);
const b2 = $serialize(Bert, Cfg);
"#;
let out = rewrite_source(source, BuildMode::prod());
let error_diags: Vec<_> = out
.diagnostics
.iter()
.filter(|d| matches!(d.level, crate::ts_syn::abi::DiagnosticLevel::Error))
.collect();
assert!(
error_diags.is_empty(),
"expected no errors, got: {:?}",
error_diags
);
let inserts: Vec<&str> = out
.patches
.iter()
.filter_map(|p| match p {
crate::ts_syn::abi::Patch::Insert { code, .. } => code.as_text(),
_ => None,
})
.collect();
assert_eq!(
inserts.len(),
2,
"expected 2 cluster-specialized helpers, got: {:#?}",
inserts
);
let replaces: Vec<&str> = out
.patches
.iter()
.filter_map(|p| match p {
crate::ts_syn::abi::Patch::Replace { code, .. } => code.as_text(),
_ => None,
})
.collect();
assert_eq!(replaces.len(), 3, "got: {:?}", replaces);
}
#[test]
fn pr19_type_macro_composition() {
let source = r#"import { macroRules } from "macroforge/rules";
const $Box = macroRules({
kind: "type",
expand: macroRules`($t:Type) => { value: $t }`,
});
const $Result = macroRules({
kind: "type",
expand: macroRules`($t:Type, $e:Type) => $Box<$t> | $e`,
});
type R = $Result<string, Error>;
"#;
let out = rewrite_source(source, BuildMode::dev());
assert!(
out.diagnostics.is_empty(),
"expected no diagnostics, got: {:?}",
out.diagnostics
);
let expansion = out
.patches
.iter()
.find_map(|p| match p {
crate::ts_syn::abi::Patch::Replace { code, .. } => code.as_text(),
_ => None,
})
.expect("expected a Replace patch for $Result<string, Error>");
assert!(
expansion.contains("value") && expansion.contains("string"),
"expected `$Box<string>` to expand to `{{ value: string }}`, got: {}",
expansion
);
assert!(
expansion.contains("Error"),
"expected the `| Error` tail to survive: {}",
expansion
);
}
#[test]
fn pr19_repetition_inside_template_literal_expansion() {
let source = r#"import { macroRules } from "macroforge/rules";
const $list = macroRules`
($($x:Expr),+) => [$( $x ),+]
`;
const r = $list(1, 2, 3);
"#;
let out = rewrite_source(source, BuildMode::dev());
assert!(out.diagnostics.is_empty(), "diag: {:?}", out.diagnostics);
let expansion = out
.patches
.iter()
.find_map(|p| match p {
crate::ts_syn::abi::Patch::Replace { code, .. } => code.as_text(),
_ => None,
})
.expect("expected a Replace patch for $list(...)");
assert!(
expansion.contains('1') && expansion.contains('2') && expansion.contains('3'),
"expected `[1, 2, 3]`-shaped expansion, got: {}",
expansion
);
}
#[test]
fn pr19_nested_macro_call_inside_repetition_body() {
let source = r#"import { macroRules } from "macroforge/rules";
const $double = macroRules`($x:Expr) => ($x * 2)`;
const $apply_each = macroRules`
($($x:Expr),+) => { results: [$( $double($x) ),+] }
`;
const r = $apply_each(1, 2, 3);
"#;
let out = rewrite_source(source, BuildMode::dev());
assert!(out.diagnostics.is_empty(), "diag: {:?}", out.diagnostics);
let expansion = out
.patches
.iter()
.find_map(|p| match p {
crate::ts_syn::abi::Patch::Replace {
code, source_macro, ..
} if source_macro.as_deref() == Some("$apply_each") => code.as_text(),
_ => None,
})
.expect("expected a Replace patch for $apply_each(...)");
for n in [1, 2, 3] {
let needle = format!("({} * 2)", n);
assert!(
expansion.contains(&needle),
"expected `{}` in expansion, got: {}",
needle,
expansion
);
}
}
#[test]
fn pr19_call_arms_referencing_unbound_metavariable_errors() {
let source = r#"import { macroRules } from "macroforge/rules";
const $broken = macroRules({
mode: "share-only",
expand: macroRules`($x:Expr) => __inline($x)`,
runtime: "function __broken(v) { return v; }",
call: macroRules`($x:Expr) => __broken($y)`,
});
const r = $broken(1);
"#;
let out = rewrite_source(source, BuildMode::dev());
let has_error = out.diagnostics.iter().any(|d| {
matches!(d.level, crate::ts_syn::abi::DiagnosticLevel::Error)
&& (d.message.contains("unbound") || d.message.contains("error expanding"))
});
assert!(
has_error,
"expected an error diagnostic for unbound metavariable, got: {:?}",
out.diagnostics
.iter()
.map(|d| (&d.level, &d.message))
.collect::<Vec<_>>()
);
}
#[test]
fn pr19_value_macro_invoked_in_type_position_emits_error() {
let source = r#"import { macroRules } from "macroforge/rules";
const $foo = macroRules`($x:Expr) => $x`;
type T = $foo<string>;
"#;
let out = rewrite_source(source, BuildMode::dev());
let has_kind_error = out.diagnostics.iter().any(|d| {
matches!(d.level, crate::ts_syn::abi::DiagnosticLevel::Error)
&& d.message.contains("value-only")
});
assert!(
has_kind_error,
"expected `value-only` kind error, got: {:?}",
out.diagnostics
.iter()
.map(|d| (&d.level, &d.message))
.collect::<Vec<_>>()
);
}
#[test]
fn pr19_type_macro_invoked_in_value_position_falls_through() {
let source = r#"import { macroRules } from "macroforge/rules";
const $T = macroRules({
kind: "type",
expand: macroRules`($t:Type) => { wrapped: $t }`,
});
const x = $T(string);
"#;
let out = rewrite_source(source, BuildMode::dev());
let kind_diags: Vec<_> = out
.diagnostics
.iter()
.filter(|d| {
matches!(d.level, crate::ts_syn::abi::DiagnosticLevel::Error)
&& d.message.contains("$T")
})
.collect();
let _ = kind_diags;
}
#[test]
fn pr19_user_can_declare_macro_called_dollar_cluster() {
let source = r#"import { macroRules } from "macroforge/rules";
const $cluster = macroRules`($x:Expr) => ($x + 1)`;
const r = $cluster(5);
"#;
let out = rewrite_source(source, BuildMode::dev());
assert!(out.diagnostics.is_empty(), "diag: {:?}", out.diagnostics);
let expansion = out
.patches
.iter()
.find_map(|p| match p {
crate::ts_syn::abi::Patch::Replace { code, .. } => code.as_text(),
_ => None,
})
.expect("expected a Replace patch for $cluster(5)");
assert!(
expansion.contains('5') && expansion.contains("+ 1"),
"expected `(5 + 1)` expansion, got: {}",
expansion
);
}
#[test]
fn pr19_topological_sort_handles_macros_with_call_chains() {
use crate::host::declarative::registry::DeclarativeMacroRegistry;
use crate::ts_syn::declarative::parse_macro_def;
let mut registry = DeclarativeMacroRegistry::new();
let mut leaf = parse_macro_def("($x:Expr) => ($x + 1)", SpanIR::new(0, 22)).unwrap();
leaf.name = "leaf".into();
registry.register(leaf).unwrap();
let mut mid = parse_macro_def("($x:Expr) => $leaf($x)", SpanIR::new(0, 22)).unwrap();
mid.name = "mid".into();
registry.register(mid).unwrap();
let mut top = parse_macro_def("($x:Expr) => $mid($x)", SpanIR::new(0, 21)).unwrap();
top.name = "top".into();
registry.register(top).unwrap();
let sorted = registry.topological_order().expect("sort should succeed");
let names: Vec<&str> = sorted.iter().map(|d| d.name.as_str()).collect();
let leaf_idx = names.iter().position(|n| *n == "leaf").unwrap();
let mid_idx = names.iter().position(|n| *n == "mid").unwrap();
let top_idx = names.iter().position(|n| *n == "top").unwrap();
assert!(leaf_idx < mid_idx, "leaf must precede mid: {:?}", names);
assert!(mid_idx < top_idx, "mid must precede top: {:?}", names);
}