use std::collections::HashMap;
use oxc::allocator::Vec as OxcVec;
use oxc::ast::ast::{Argument, Expression, TSType};
use oxc::span::GetSpan;
use crate::ts_syn::abi::SpanIR;
use crate::ts_syn::declarative::{FragmentKind, MacroDef, Pattern, PatternElement, RepetitionKind};
#[derive(Debug, Clone)]
pub struct MatchResult {
pub arm_index: usize,
pub bindings: HashMap<String, Binding>,
}
#[derive(Debug, Clone)]
pub enum Binding {
Single(BoundFragment),
Sequence(Vec<BoundFragment>),
}
#[derive(Debug, Clone)]
pub struct BoundFragment {
pub kind: FragmentKind,
pub source: String,
pub span: SpanIR,
}
#[derive(Debug, Clone)]
pub enum MatchError {
NoArmMatched {
tried: Vec<String>,
},
UnsupportedFragmentKind {
kind: FragmentKind,
reason: &'static str,
},
InconsistentSequenceLength,
}
impl std::fmt::Display for MatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MatchError::NoArmMatched { tried } => {
write!(f, "no arm matched; tried {} pattern(s)", tried.len())?;
if !tried.is_empty() {
write!(f, ": {}", tried.join(" | "))?;
}
if tried.iter().any(|p| p.contains("$(...)")) {
write!(
f,
" (note: repetitions `$(...)*` / `$(...)+` / `$(...)?` are matched with bounded backtracking — the matcher tries decreasing counts until the tail elements fit; if you expected a match here, double-check that the fragment kinds after the repetition are compatible with the remaining call arguments)"
)?;
}
Ok(())
}
MatchError::UnsupportedFragmentKind { kind, reason } => {
write!(
f,
"fragment kind `{:?}` cannot appear in call-argument position: {}",
kind, reason
)
}
MatchError::InconsistentSequenceLength => {
write!(
f,
"repeated metavariables bound inside the same repetition have mismatched lengths"
)
}
}
}
}
impl std::error::Error for MatchError {}
pub fn match_invocation<'a>(
def: &MacroDef,
call_args: &'a OxcVec<'a, Argument<'a>>,
source: &str,
) -> Result<MatchResult, MatchError> {
match_invocation_against_arms(&def.arms, call_args, source).map(|(arm_index, bindings)| {
MatchResult {
arm_index,
bindings,
}
})
}
pub fn match_invocation_against_arms<'a>(
arms: &[crate::ts_syn::declarative::MacroArm],
call_args: &'a OxcVec<'a, Argument<'a>>,
source: &str,
) -> Result<(usize, HashMap<String, Binding>), MatchError> {
let mut tried = Vec::with_capacity(arms.len());
for (arm_index, arm) in arms.iter().enumerate() {
let mut bindings: HashMap<String, Binding> = HashMap::new();
let mut cursor = 0usize;
let matched = match_pattern(&arm.pattern, call_args, &mut cursor, &mut bindings, source)?;
if matched && cursor == call_args.len() {
return Ok((arm_index, bindings));
}
tried.push(describe_pattern(&arm.pattern));
}
Err(MatchError::NoArmMatched { tried })
}
fn match_pattern<'a>(
pattern: &Pattern,
args: &'a OxcVec<'a, Argument<'a>>,
cursor: &mut usize,
bindings: &mut HashMap<String, Binding>,
source: &str,
) -> Result<bool, MatchError> {
match pattern {
Pattern::Empty => Ok(*cursor == 0 && args.is_empty()),
Pattern::Sequence(elements) => match_elements(elements, args, cursor, bindings, source),
}
}
fn match_elements<'a>(
elements: &[PatternElement],
args: &'a OxcVec<'a, Argument<'a>>,
cursor: &mut usize,
bindings: &mut HashMap<String, Binding>,
source: &str,
) -> Result<bool, MatchError> {
for (idx, elem) in elements.iter().enumerate() {
match elem {
PatternElement::Literal(lit) => {
let _ = lit;
}
PatternElement::Fragment { name, kind } => {
if *cursor >= args.len() {
return Ok(false);
}
let Some(fragment) = bind_fragment(&args[*cursor], *kind, source)? else {
return Ok(false);
};
bindings.insert(name.clone(), Binding::Single(fragment));
*cursor += 1;
}
PatternElement::Repetition {
pattern,
separator: _,
kind,
} => {
let tail = &elements[idx + 1..];
return match_elements_with_repetition_backtrack(
pattern, *kind, tail, args, cursor, bindings, source,
);
}
}
}
Ok(true)
}
#[allow(clippy::too_many_arguments)]
fn match_elements_with_repetition_backtrack<'a>(
inner: &Pattern,
kind: RepetitionKind,
tail: &[PatternElement],
args: &'a OxcVec<'a, Argument<'a>>,
cursor: &mut usize,
outer_bindings: &mut HashMap<String, Binding>,
source: &str,
) -> Result<bool, MatchError> {
let mut snapshots: Vec<(usize, HashMap<String, Vec<BoundFragment>>)> = Vec::new();
snapshots.push((*cursor, HashMap::new()));
let mut collected: HashMap<String, Vec<BoundFragment>> = HashMap::new();
let mut count = 0usize;
loop {
let saved_cursor = *cursor;
let mut temp_bindings: HashMap<String, Binding> = HashMap::new();
let inner_match = match_pattern(inner, args, cursor, &mut temp_bindings, source)?;
if !inner_match {
*cursor = saved_cursor;
break;
}
for (name, binding) in temp_bindings {
let bucket = collected.entry(name).or_default();
match binding {
Binding::Single(frag) => bucket.push(frag),
Binding::Sequence(frags) => bucket.extend(frags),
}
}
count += 1;
snapshots.push((*cursor, collected.clone()));
if kind == RepetitionKind::ZeroOrOne && count >= 1 {
break;
}
if *cursor == saved_cursor {
break;
}
}
let min_count: usize = match kind {
RepetitionKind::ZeroOrMore | RepetitionKind::ZeroOrOne => 0,
RepetitionKind::OneOrMore => 1,
};
let rep_keys: Vec<String> = collected.keys().cloned().collect();
for try_count in (min_count..=count).rev() {
if kind == RepetitionKind::ZeroOrOne && try_count > 1 {
continue;
}
let (snap_cursor, snap_bindings) = &snapshots[try_count];
*cursor = *snap_cursor;
for (name, frags) in snap_bindings {
outer_bindings.insert(name.clone(), Binding::Sequence(frags.clone()));
}
for key in &rep_keys {
outer_bindings
.entry(key.clone())
.or_insert_with(|| Binding::Sequence(Vec::new()));
}
let bindings_before_tail = outer_bindings.clone();
let cursor_before_tail = *cursor;
let tail_matched = match_elements(tail, args, cursor, outer_bindings, source)?;
if tail_matched && *cursor == args.len() {
return Ok(true);
}
*outer_bindings = bindings_before_tail;
*cursor = cursor_before_tail;
for key in &rep_keys {
outer_bindings.remove(key);
}
}
Ok(false)
}
fn bind_fragment(
arg: &Argument<'_>,
kind: FragmentKind,
source: &str,
) -> Result<Option<BoundFragment>, MatchError> {
let expr = match arg.as_expression() {
Some(expr) => expr,
None => return Ok(None),
};
let shape_ok = match kind {
FragmentKind::Expr | FragmentKind::Tt => true,
FragmentKind::Ident => matches!(expr, Expression::Identifier(_)),
FragmentKind::Lit => matches!(
expr,
Expression::StringLiteral(_)
| Expression::NumericLiteral(_)
| Expression::BooleanLiteral(_)
| Expression::NullLiteral(_)
| Expression::BigIntLiteral(_)
| Expression::RegExpLiteral(_)
| Expression::TemplateLiteral(_)
),
FragmentKind::Path => matches!(
expr,
Expression::Identifier(_) | Expression::StaticMemberExpression(_)
),
FragmentKind::Block => matches!(expr, Expression::ArrowFunctionExpression(_)),
FragmentKind::Stmt => {
return Err(MatchError::UnsupportedFragmentKind {
kind,
reason: "`Stmt` matches control-flow or declaration statements; JavaScript's grammar only allows expressions as call arguments. Wrap the statement in an arrow function if you need to pass it in: `$macro(() => { ...stmts })`.",
});
}
FragmentKind::Type => {
return Err(MatchError::UnsupportedFragmentKind {
kind,
reason: "`Type` matches TypeScript type annotations, which only appear in type position (type aliases, parameter annotations, return types). For a value-position macro, use `Expr`; for a type-position macro, declare the macro with `kind: \"type\"`.",
});
}
FragmentKind::Pat => {
return Err(MatchError::UnsupportedFragmentKind {
kind,
reason: "`Pat` matches destructuring patterns, which can only appear in binding position (function parameters, `let`/`const` LHS). If you need to pass a pattern-like shape into a macro, capture the whole object expression with `Expr` instead.",
});
}
FragmentKind::Item => {
return Err(MatchError::UnsupportedFragmentKind {
kind,
reason: "`Item` matches top-level declarations (class, function, interface, enum). Declarations cannot appear in call-argument position in JavaScript. Use `Expr` to capture a function expression or class expression instead.",
});
}
FragmentKind::Decorator => {
return Err(MatchError::UnsupportedFragmentKind {
kind,
reason: "`Decorator` matches `@decorator(...)` annotations, which can only attach to classes, methods, and fields — not appear as call arguments. If you need a decorator-shaped expression as an argument, capture the call itself with `Expr`.",
});
}
};
if !shape_ok {
return Ok(None);
}
let span = expr.span();
let start = span.start as usize;
let end = span.end as usize;
let source_text = source.get(start..end).unwrap_or("").to_string();
Ok(Some(BoundFragment {
kind,
source: source_text,
span: SpanIR::new(span.start + 1, span.end + 1),
}))
}
pub fn match_type_invocation_against_arms<'a>(
arms: &[crate::ts_syn::declarative::MacroArm],
type_args: &'a OxcVec<'a, TSType<'a>>,
source: &str,
) -> Result<(usize, HashMap<String, Binding>), MatchError> {
let mut tried = Vec::with_capacity(arms.len());
for (arm_index, arm) in arms.iter().enumerate() {
let mut bindings: HashMap<String, Binding> = HashMap::new();
let mut cursor = 0usize;
let matched =
match_type_pattern(&arm.pattern, type_args, &mut cursor, &mut bindings, source)?;
if matched && cursor == type_args.len() {
return Ok((arm_index, bindings));
}
tried.push(describe_pattern(&arm.pattern));
}
Err(MatchError::NoArmMatched { tried })
}
fn match_type_pattern<'a>(
pattern: &Pattern,
args: &'a OxcVec<'a, TSType<'a>>,
cursor: &mut usize,
bindings: &mut HashMap<String, Binding>,
source: &str,
) -> Result<bool, MatchError> {
match pattern {
Pattern::Empty => Ok(*cursor == 0 && args.is_empty()),
Pattern::Sequence(elements) => {
match_type_elements(elements, args, cursor, bindings, source)
}
}
}
fn match_type_elements<'a>(
elements: &[PatternElement],
args: &'a OxcVec<'a, TSType<'a>>,
cursor: &mut usize,
bindings: &mut HashMap<String, Binding>,
source: &str,
) -> Result<bool, MatchError> {
let mut i = 0usize;
while i < elements.len() {
let elem = &elements[i];
match elem {
PatternElement::Literal(_) => {
}
PatternElement::Fragment { name, kind } => {
if *cursor >= args.len() {
return Ok(false);
}
let Some(fragment) = bind_type_fragment(&args[*cursor], *kind, source)? else {
return Ok(false);
};
bindings.insert(name.clone(), Binding::Single(fragment));
*cursor += 1;
}
PatternElement::Repetition {
pattern,
separator: _,
kind,
} => {
let mut collected: HashMap<String, Vec<BoundFragment>> = HashMap::new();
let mut count = 0usize;
loop {
let saved_cursor = *cursor;
let mut temp_bindings: HashMap<String, Binding> = HashMap::new();
let inner_match =
match_type_pattern(pattern, args, cursor, &mut temp_bindings, source)?;
if !inner_match {
*cursor = saved_cursor;
break;
}
for (name, binding) in temp_bindings {
let bucket = collected.entry(name).or_default();
match binding {
Binding::Single(frag) => bucket.push(frag),
Binding::Sequence(frags) => bucket.extend(frags),
}
}
count += 1;
if *kind == RepetitionKind::ZeroOrOne && count >= 1 {
break;
}
if *cursor == saved_cursor {
break;
}
}
match kind {
RepetitionKind::ZeroOrMore => {}
RepetitionKind::OneOrMore => {
if count == 0 {
return Ok(false);
}
}
RepetitionKind::ZeroOrOne => {
if count > 1 {
return Ok(false);
}
}
}
for (name, frags) in collected {
bindings.insert(name, Binding::Sequence(frags));
}
}
}
i += 1;
}
Ok(true)
}
fn bind_type_fragment(
ty: &TSType<'_>,
kind: FragmentKind,
source: &str,
) -> Result<Option<BoundFragment>, MatchError> {
match kind {
FragmentKind::Type | FragmentKind::Tt => {}
_ => {
return Err(MatchError::UnsupportedFragmentKind {
kind,
reason: "type-position macros only bind `Type` (or `Tt` as the structural fallback) metavariables. Expressions, identifiers, literals, patterns, and statements don't exist in TypeScript type position.",
});
}
}
let span = ty.span();
let start = span.start as usize;
let end = span.end as usize;
let source_text = source.get(start..end).unwrap_or("").to_string();
Ok(Some(BoundFragment {
kind,
source: source_text,
span: SpanIR::new(span.start + 1, span.end + 1),
}))
}
fn describe_pattern(pattern: &Pattern) -> String {
match pattern {
Pattern::Empty => "()".to_string(),
Pattern::Sequence(elems) => {
let mut out = String::from("(");
for (i, elem) in elems.iter().enumerate() {
if i > 0 {
out.push(' ');
}
match elem {
PatternElement::Literal(s) => out.push_str(s),
PatternElement::Fragment { name, kind } => {
out.push('$');
out.push_str(name);
out.push(':');
out.push_str(match kind {
FragmentKind::Expr => "Expr",
FragmentKind::Stmt => "Stmt",
FragmentKind::Block => "Block",
FragmentKind::Ident => "Ident",
FragmentKind::Type => "Type",
FragmentKind::Pat => "Pat",
FragmentKind::Lit => "Lit",
FragmentKind::Path => "Path",
FragmentKind::Item => "Item",
FragmentKind::Decorator => "Decorator",
FragmentKind::Tt => "Tt",
});
}
PatternElement::Repetition { kind, .. } => {
out.push_str("$(...)");
out.push(match kind {
RepetitionKind::ZeroOrMore => '*',
RepetitionKind::OneOrMore => '+',
RepetitionKind::ZeroOrOne => '?',
});
}
}
}
out.push(')');
out
}
}
}