Skip to main content

cel_core/parser/
macros.rs

1//! Macro system for CEL parser.
2//!
3//! Macros in CEL are syntactic transformations that expand at parse time.
4//! They transform specific call patterns (like `list.all(x, cond)`) into
5//! expanded AST nodes (like `Comprehension`).
6//!
7//! This module provides:
8//! - [`Macro`] - Definition of a single macro
9//! - [`MacroRegistry`] - Collection of macros with lookup by key
10//! - [`MacroExpander`] - The expansion function type
11//! - [`MacroContext`] - Context passed to expanders for node creation
12//!
13//! # Architecture
14//!
15//! Macros are keyed by `name:arg_count:is_receiver` (e.g., `"all:2:true"`).
16//! This allows separate definitions for different argument counts.
17//! Lookup tries the exact key first, then falls back to a var-arg key.
18
19use std::collections::HashMap;
20
21use crate::types::{BinaryOp, Expr, ListElement, MapEntry, Span, Spanned, SpannedExpr, UnaryOp};
22
23/// Indicates whether a macro is called as a global function or as a method on a receiver.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum MacroStyle {
26    /// Global function call: `macro_name(args...)`
27    Global,
28    /// Receiver-style method call: `receiver.macro_name(args...)`
29    Receiver,
30}
31
32/// Specifies the expected argument count for a macro.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ArgCount {
35    /// Exact number of arguments required.
36    Exact(usize),
37    /// Variable arguments with a minimum count.
38    VarArg(usize),
39}
40
41impl ArgCount {
42    /// Check if the given argument count matches this specification.
43    pub fn matches(&self, count: usize) -> bool {
44        match self {
45            ArgCount::Exact(n) => count == *n,
46            ArgCount::VarArg(min) => count >= *min,
47        }
48    }
49
50    /// Get the count value (exact count or minimum for vararg).
51    pub fn count(&self) -> usize {
52        match self {
53            ArgCount::Exact(n) => *n,
54            ArgCount::VarArg(min) => *min,
55        }
56    }
57
58    /// Returns true if this is a vararg specification.
59    pub fn is_vararg(&self) -> bool {
60        matches!(self, ArgCount::VarArg(_))
61    }
62}
63
64/// Result of macro expansion.
65#[derive(Debug)]
66pub enum MacroExpansion {
67    /// Macro was successfully expanded to this expression.
68    Expanded(SpannedExpr),
69    /// Macro signature matched but expansion failed (e.g., invalid arguments).
70    /// The string contains an error message.
71    Error(String),
72}
73
74/// Context provided to macro expanders for creating AST nodes.
75///
76/// This provides the necessary state for creating synthetic AST nodes
77/// during macro expansion, including ID allocation and error reporting.
78pub struct MacroContext<'a> {
79    /// Function to allocate the next unique node ID.
80    next_id_fn: &'a mut dyn FnMut() -> i64,
81    /// Accumulated errors during expansion.
82    errors: Vec<(String, Span)>,
83    /// Function to store macro call for IDE features.
84    store_macro_call_fn: Option<&'a mut dyn FnMut(i64, &Span, &SpannedExpr, &str, &[SpannedExpr])>,
85}
86
87impl<'a> MacroContext<'a> {
88    /// Create a new macro context.
89    pub fn new(
90        next_id_fn: &'a mut dyn FnMut() -> i64,
91        store_macro_call_fn: Option<&'a mut dyn FnMut(i64, &Span, &SpannedExpr, &str, &[SpannedExpr])>,
92    ) -> Self {
93        Self {
94            next_id_fn,
95            errors: Vec::new(),
96            store_macro_call_fn,
97        }
98    }
99
100    /// Allocate the next unique node ID.
101    pub fn next_id(&mut self) -> i64 {
102        (self.next_id_fn)()
103    }
104
105    /// Add an error message.
106    pub fn add_error(&mut self, message: String, span: Span) {
107        self.errors.push((message, span));
108    }
109
110    /// Take accumulated errors.
111    pub fn take_errors(&mut self) -> Vec<(String, Span)> {
112        std::mem::take(&mut self.errors)
113    }
114
115    /// Store the original macro call expression for IDE features.
116    pub fn store_macro_call(
117        &mut self,
118        call_id: i64,
119        span: &Span,
120        receiver: &SpannedExpr,
121        method_name: &str,
122        args: &[SpannedExpr],
123    ) {
124        if let Some(f) = &mut self.store_macro_call_fn {
125            f(call_id, span, receiver, method_name, args);
126        }
127    }
128}
129
130/// Type alias for macro expander functions.
131///
132/// # Parameters
133/// - `ctx`: Macro context for ID allocation and error reporting
134/// - `span`: Source span of the entire call expression
135/// - `receiver`: The receiver expression for receiver-style macros, None for global macros
136/// - `args`: The arguments passed to the macro
137///
138/// # Returns
139/// - `MacroExpansion::Expanded(expr)` on successful expansion
140/// - `MacroExpansion::Error(msg)` if expansion fails
141pub type MacroExpander = fn(
142    ctx: &mut MacroContext,
143    span: Span,
144    receiver: Option<SpannedExpr>,
145    args: Vec<SpannedExpr>,
146) -> MacroExpansion;
147
148/// Definition of a single macro.
149#[derive(Clone)]
150pub struct Macro {
151    /// The macro name (e.g., "all", "has", "map").
152    pub name: &'static str,
153    /// Whether this is a global or receiver-style macro.
154    pub style: MacroStyle,
155    /// The expected argument count.
156    pub arg_count: ArgCount,
157    /// The expansion function.
158    pub expander: MacroExpander,
159    /// Optional description for documentation/IDE features.
160    pub description: Option<&'static str>,
161}
162
163impl Macro {
164    /// Create a new macro definition.
165    pub const fn new(
166        name: &'static str,
167        style: MacroStyle,
168        arg_count: ArgCount,
169        expander: MacroExpander,
170    ) -> Self {
171        Self {
172            name,
173            style,
174            arg_count,
175            expander,
176            description: None,
177        }
178    }
179
180    /// Create a new macro definition with a description.
181    pub const fn with_description(
182        name: &'static str,
183        style: MacroStyle,
184        arg_count: ArgCount,
185        expander: MacroExpander,
186        description: &'static str,
187    ) -> Self {
188        Self {
189            name,
190            style,
191            arg_count,
192            expander,
193            description: Some(description),
194        }
195    }
196
197    /// Generate the lookup key for this macro.
198    pub fn key(&self) -> String {
199        make_key(self.name, self.arg_count.count(), self.style == MacroStyle::Receiver)
200    }
201}
202
203impl std::fmt::Debug for Macro {
204    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205        f.debug_struct("Macro")
206            .field("name", &self.name)
207            .field("style", &self.style)
208            .field("arg_count", &self.arg_count)
209            .field("description", &self.description)
210            .finish_non_exhaustive()
211    }
212}
213
214/// Generate a lookup key for a macro.
215fn make_key(name: &str, arg_count: usize, is_receiver: bool) -> String {
216    format!("{}:{}:{}", name, arg_count, is_receiver)
217}
218
219/// Registry of macros with efficient lookup.
220///
221/// Macros are keyed by `name:arg_count:is_receiver`.
222/// Lookup tries the exact key first, then falls back to a vararg key.
223#[derive(Debug, Clone)]
224pub struct MacroRegistry {
225    /// Map from key to macro definition.
226    macros: HashMap<String, Macro>,
227    /// Track vararg macros by name:is_receiver for fallback lookup.
228    vararg_keys: HashMap<String, usize>,
229}
230
231impl Default for MacroRegistry {
232    fn default() -> Self {
233        Self::new()
234    }
235}
236
237impl MacroRegistry {
238    /// Create an empty macro registry.
239    pub fn new() -> Self {
240        Self {
241            macros: HashMap::new(),
242            vararg_keys: HashMap::new(),
243        }
244    }
245
246    /// Create a registry with the standard CEL macros.
247    pub fn standard() -> Self {
248        let mut registry = Self::new();
249        for macro_def in STANDARD_MACROS {
250            registry.register(macro_def.clone());
251        }
252        registry
253    }
254
255    /// Register a macro in the registry.
256    pub fn register(&mut self, macro_def: Macro) {
257        let key = macro_def.key();
258
259        // Track vararg macros for fallback lookup
260        if macro_def.arg_count.is_vararg() {
261            let vararg_key = format!("{}:{}", macro_def.name, macro_def.style == MacroStyle::Receiver);
262            self.vararg_keys.insert(vararg_key, macro_def.arg_count.count());
263        }
264
265        self.macros.insert(key, macro_def);
266    }
267
268    /// Look up a macro by name, argument count, and receiver style.
269    ///
270    /// First tries exact match, then falls back to vararg match if applicable.
271    pub fn lookup(&self, name: &str, arg_count: usize, is_receiver: bool) -> Option<&Macro> {
272        // Try exact match first
273        let exact_key = make_key(name, arg_count, is_receiver);
274        if let Some(m) = self.macros.get(&exact_key) {
275            return Some(m);
276        }
277
278        // Try vararg fallback
279        let vararg_lookup_key = format!("{}:{}", name, is_receiver);
280        if let Some(&min_args) = self.vararg_keys.get(&vararg_lookup_key) {
281            if arg_count >= min_args {
282                let vararg_key = make_key(name, min_args, is_receiver);
283                return self.macros.get(&vararg_key);
284            }
285        }
286
287        None
288    }
289
290    /// Check if the registry contains a macro with the given name.
291    pub fn contains(&self, name: &str) -> bool {
292        self.macros.values().any(|m| m.name == name)
293    }
294
295    /// Get an iterator over all registered macros.
296    pub fn iter(&self) -> impl Iterator<Item = &Macro> {
297        self.macros.values()
298    }
299
300    /// Get the number of registered macros.
301    pub fn len(&self) -> usize {
302        self.macros.len()
303    }
304
305    /// Check if the registry is empty.
306    pub fn is_empty(&self) -> bool {
307        self.macros.is_empty()
308    }
309}
310
311// ============================================================================
312// Standard CEL Macros
313// ============================================================================
314
315/// Accumulator variable name used in comprehension expansions.
316const ACCU_VAR: &str = "__result__";
317
318/// Standard CEL macros.
319pub static STANDARD_MACROS: &[Macro] = &[
320    // has(m.x) - global, 1 arg
321    Macro::with_description(
322        "has",
323        MacroStyle::Global,
324        ArgCount::Exact(1),
325        expand_has,
326        "Tests whether a field is set on a message",
327    ),
328
329    // all - receiver, 2 or 3 args
330    Macro::with_description(
331        "all",
332        MacroStyle::Receiver,
333        ArgCount::Exact(2),
334        expand_all_2arg,
335        "Tests whether all elements satisfy a condition",
336    ),
337    Macro::with_description(
338        "all",
339        MacroStyle::Receiver,
340        ArgCount::Exact(3),
341        expand_all_3arg,
342        "Tests whether all elements satisfy a condition (two-variable form)",
343    ),
344
345    // exists - receiver, 2 or 3 args
346    Macro::with_description(
347        "exists",
348        MacroStyle::Receiver,
349        ArgCount::Exact(2),
350        expand_exists_2arg,
351        "Tests whether any element satisfies a condition",
352    ),
353    Macro::with_description(
354        "exists",
355        MacroStyle::Receiver,
356        ArgCount::Exact(3),
357        expand_exists_3arg,
358        "Tests whether any element satisfies a condition (two-variable form)",
359    ),
360
361    // exists_one - receiver, 2 or 3 args
362    Macro::with_description(
363        "exists_one",
364        MacroStyle::Receiver,
365        ArgCount::Exact(2),
366        expand_exists_one_2arg,
367        "Tests whether exactly one element satisfies a condition",
368    ),
369    Macro::with_description(
370        "exists_one",
371        MacroStyle::Receiver,
372        ArgCount::Exact(3),
373        expand_exists_one_3arg,
374        "Tests whether exactly one element satisfies a condition (two-variable form)",
375    ),
376
377    // map - receiver, 2 or 3 args
378    Macro::with_description(
379        "map",
380        MacroStyle::Receiver,
381        ArgCount::Exact(2),
382        expand_map_2arg,
383        "Transforms elements of a list",
384    ),
385    Macro::with_description(
386        "map",
387        MacroStyle::Receiver,
388        ArgCount::Exact(3),
389        expand_map_3arg,
390        "Transforms elements of a list with filtering",
391    ),
392
393    // filter - receiver, 2 args
394    Macro::with_description(
395        "filter",
396        MacroStyle::Receiver,
397        ArgCount::Exact(2),
398        expand_filter,
399        "Filters elements of a list by a condition",
400    ),
401
402    // transformList - receiver, 3 or 4 args
403    Macro::with_description(
404        "transformList",
405        MacroStyle::Receiver,
406        ArgCount::Exact(3),
407        expand_transform_list_3arg,
408        "Transforms list elements with index and value variables",
409    ),
410    Macro::with_description(
411        "transformList",
412        MacroStyle::Receiver,
413        ArgCount::Exact(4),
414        expand_transform_list_4arg,
415        "Transforms list elements with index, value, and filter",
416    ),
417
418    // transformMap - receiver, 3 or 4 args
419    Macro::with_description(
420        "transformMap",
421        MacroStyle::Receiver,
422        ArgCount::Exact(3),
423        expand_transform_map_3arg,
424        "Transforms map entries with key and value variables",
425    ),
426    Macro::with_description(
427        "transformMap",
428        MacroStyle::Receiver,
429        ArgCount::Exact(4),
430        expand_transform_map_4arg,
431        "Transforms map entries with key, value, and filter",
432    ),
433    // cel.bind - global, 3 args
434    Macro::with_description(
435        "cel.bind",
436        MacroStyle::Global,
437        ArgCount::Exact(3),
438        expand_bind,
439        "Binds a variable to a value for use in an expression",
440    ),
441    // optMap - receiver, 2 args
442    Macro::with_description(
443        "optMap",
444        MacroStyle::Receiver,
445        ArgCount::Exact(2),
446        expand_opt_map,
447        "Transforms an optional value if present",
448    ),
449    // optFlatMap - receiver, 2 args
450    Macro::with_description(
451        "optFlatMap",
452        MacroStyle::Receiver,
453        ArgCount::Exact(2),
454        expand_opt_flat_map,
455        "Chains optional operations",
456    ),
457];
458
459// === Helper Functions ===
460
461/// Create a synthetic spanned expression.
462fn synthetic(ctx: &mut MacroContext, node: Expr, span: Span) -> SpannedExpr {
463    Spanned::new(ctx.next_id(), node, span)
464}
465
466/// Extract iteration variable name from an expression.
467/// Returns None and adds error if not a simple identifier.
468fn extract_iter_var(ctx: &mut MacroContext, expr: &SpannedExpr) -> Option<String> {
469    match &expr.node {
470        Expr::Ident(name) => Some(name.clone()),
471        _ => {
472            ctx.add_error(
473                "iteration variable must be an identifier".to_string(),
474                expr.span.clone(),
475            );
476            None
477        }
478    }
479}
480
481// === has() Macro ===
482
483/// Expand `has(m.x)` to `MemberTestOnly { expr: m, field: x }`.
484fn expand_has(
485    ctx: &mut MacroContext,
486    span: Span,
487    _receiver: Option<SpannedExpr>,
488    args: Vec<SpannedExpr>,
489) -> MacroExpansion {
490    if args.len() != 1 {
491        return MacroExpansion::Error(format!("has() requires 1 argument, got {}", args.len()));
492    }
493
494    let arg = args.into_iter().next().unwrap();
495
496    match arg.node {
497        Expr::Member { expr, field, .. } => {
498            let result = Spanned::new(
499                ctx.next_id(),
500                Expr::MemberTestOnly { expr, field },
501                span,
502            );
503            MacroExpansion::Expanded(result)
504        }
505        _ => MacroExpansion::Error(
506            "has() argument must be a field selection (e.g., has(m.x))".to_string(),
507        ),
508    }
509}
510
511// === all() Macro ===
512
513fn expand_all_2arg(
514    ctx: &mut MacroContext,
515    span: Span,
516    receiver: Option<SpannedExpr>,
517    args: Vec<SpannedExpr>,
518) -> MacroExpansion {
519    let receiver = match receiver {
520        Some(r) => r,
521        None => return MacroExpansion::Error("all() requires a receiver".to_string()),
522    };
523
524    let iter_var = match extract_iter_var(ctx, &args[0]) {
525        Some(v) => v,
526        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
527    };
528    let cond = args[1].clone();
529
530    expand_all_impl(ctx, span, receiver, iter_var, String::new(), cond, &args)
531}
532
533fn expand_all_3arg(
534    ctx: &mut MacroContext,
535    span: Span,
536    receiver: Option<SpannedExpr>,
537    args: Vec<SpannedExpr>,
538) -> MacroExpansion {
539    let receiver = match receiver {
540        Some(r) => r,
541        None => return MacroExpansion::Error("all() requires a receiver".to_string()),
542    };
543
544    let iter_var = match extract_iter_var(ctx, &args[0]) {
545        Some(v) => v,
546        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
547    };
548    let iter_var2 = match extract_iter_var(ctx, &args[1]) {
549        Some(v) => v,
550        None => return MacroExpansion::Error("second argument must be an identifier".to_string()),
551    };
552    let cond = args[2].clone();
553
554    expand_all_impl(ctx, span, receiver, iter_var, iter_var2, cond, &args)
555}
556
557fn expand_all_impl(
558    ctx: &mut MacroContext,
559    span: Span,
560    receiver: SpannedExpr,
561    iter_var: String,
562    iter_var2: String,
563    cond: SpannedExpr,
564    args: &[SpannedExpr],
565) -> MacroExpansion {
566    let call_id = ctx.next_id();
567    ctx.store_macro_call(call_id, &span, &receiver, "all", args);
568
569    let accu_var = ACCU_VAR.to_string();
570    let accu_init = synthetic(ctx, Expr::Bool(true), span.clone());
571    let loop_condition = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
572
573    let accu_ref = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
574    let loop_step = synthetic(
575        ctx,
576        Expr::Binary {
577            op: BinaryOp::And,
578            left: Box::new(cond),
579            right: Box::new(accu_ref),
580        },
581        span.clone(),
582    );
583
584    let result = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
585
586    MacroExpansion::Expanded(Spanned::new(
587        call_id,
588        Expr::Comprehension {
589            iter_var,
590            iter_var2,
591            iter_range: Box::new(receiver),
592            accu_var,
593            accu_init: Box::new(accu_init),
594            loop_condition: Box::new(loop_condition),
595            loop_step: Box::new(loop_step),
596            result: Box::new(result),
597        },
598        span,
599    ))
600}
601
602// === exists() Macro ===
603
604fn expand_exists_2arg(
605    ctx: &mut MacroContext,
606    span: Span,
607    receiver: Option<SpannedExpr>,
608    args: Vec<SpannedExpr>,
609) -> MacroExpansion {
610    let receiver = match receiver {
611        Some(r) => r,
612        None => return MacroExpansion::Error("exists() requires a receiver".to_string()),
613    };
614
615    let iter_var = match extract_iter_var(ctx, &args[0]) {
616        Some(v) => v,
617        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
618    };
619    let cond = args[1].clone();
620
621    expand_exists_impl(ctx, span, receiver, iter_var, String::new(), cond, &args)
622}
623
624fn expand_exists_3arg(
625    ctx: &mut MacroContext,
626    span: Span,
627    receiver: Option<SpannedExpr>,
628    args: Vec<SpannedExpr>,
629) -> MacroExpansion {
630    let receiver = match receiver {
631        Some(r) => r,
632        None => return MacroExpansion::Error("exists() requires a receiver".to_string()),
633    };
634
635    let iter_var = match extract_iter_var(ctx, &args[0]) {
636        Some(v) => v,
637        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
638    };
639    let iter_var2 = match extract_iter_var(ctx, &args[1]) {
640        Some(v) => v,
641        None => return MacroExpansion::Error("second argument must be an identifier".to_string()),
642    };
643    let cond = args[2].clone();
644
645    expand_exists_impl(ctx, span, receiver, iter_var, iter_var2, cond, &args)
646}
647
648fn expand_exists_impl(
649    ctx: &mut MacroContext,
650    span: Span,
651    receiver: SpannedExpr,
652    iter_var: String,
653    iter_var2: String,
654    cond: SpannedExpr,
655    args: &[SpannedExpr],
656) -> MacroExpansion {
657    let call_id = ctx.next_id();
658    ctx.store_macro_call(call_id, &span, &receiver, "exists", args);
659
660    let accu_var = ACCU_VAR.to_string();
661    let accu_init = synthetic(ctx, Expr::Bool(false), span.clone());
662
663    let accu_ref_cond = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
664    let loop_condition = synthetic(
665        ctx,
666        Expr::Unary {
667            op: UnaryOp::Not,
668            expr: Box::new(accu_ref_cond),
669        },
670        span.clone(),
671    );
672
673    let accu_ref_step = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
674    let loop_step = synthetic(
675        ctx,
676        Expr::Binary {
677            op: BinaryOp::Or,
678            left: Box::new(cond),
679            right: Box::new(accu_ref_step),
680        },
681        span.clone(),
682    );
683
684    let result = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
685
686    MacroExpansion::Expanded(Spanned::new(
687        call_id,
688        Expr::Comprehension {
689            iter_var,
690            iter_var2,
691            iter_range: Box::new(receiver),
692            accu_var,
693            accu_init: Box::new(accu_init),
694            loop_condition: Box::new(loop_condition),
695            loop_step: Box::new(loop_step),
696            result: Box::new(result),
697        },
698        span,
699    ))
700}
701
702// === exists_one() Macro ===
703
704fn expand_exists_one_2arg(
705    ctx: &mut MacroContext,
706    span: Span,
707    receiver: Option<SpannedExpr>,
708    args: Vec<SpannedExpr>,
709) -> MacroExpansion {
710    let receiver = match receiver {
711        Some(r) => r,
712        None => return MacroExpansion::Error("exists_one() requires a receiver".to_string()),
713    };
714
715    let iter_var = match extract_iter_var(ctx, &args[0]) {
716        Some(v) => v,
717        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
718    };
719    let cond = args[1].clone();
720
721    expand_exists_one_impl(ctx, span, receiver, iter_var, String::new(), cond, &args)
722}
723
724fn expand_exists_one_3arg(
725    ctx: &mut MacroContext,
726    span: Span,
727    receiver: Option<SpannedExpr>,
728    args: Vec<SpannedExpr>,
729) -> MacroExpansion {
730    let receiver = match receiver {
731        Some(r) => r,
732        None => return MacroExpansion::Error("exists_one() requires a receiver".to_string()),
733    };
734
735    let iter_var = match extract_iter_var(ctx, &args[0]) {
736        Some(v) => v,
737        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
738    };
739    let iter_var2 = match extract_iter_var(ctx, &args[1]) {
740        Some(v) => v,
741        None => return MacroExpansion::Error("second argument must be an identifier".to_string()),
742    };
743    let cond = args[2].clone();
744
745    expand_exists_one_impl(ctx, span, receiver, iter_var, iter_var2, cond, &args)
746}
747
748fn expand_exists_one_impl(
749    ctx: &mut MacroContext,
750    span: Span,
751    receiver: SpannedExpr,
752    iter_var: String,
753    iter_var2: String,
754    cond: SpannedExpr,
755    args: &[SpannedExpr],
756) -> MacroExpansion {
757    let call_id = ctx.next_id();
758    ctx.store_macro_call(call_id, &span, &receiver, "exists_one", args);
759
760    let accu_var = ACCU_VAR.to_string();
761    let accu_init = synthetic(ctx, Expr::Int(0), span.clone());
762
763    let accu_ref_cond = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
764    let one_cond = synthetic(ctx, Expr::Int(1), span.clone());
765    let loop_condition = synthetic(
766        ctx,
767        Expr::Binary {
768            op: BinaryOp::Le,
769            left: Box::new(accu_ref_cond),
770            right: Box::new(one_cond),
771        },
772        span.clone(),
773    );
774
775    let accu_ref_then = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
776    let one_step = synthetic(ctx, Expr::Int(1), span.clone());
777    let increment = synthetic(
778        ctx,
779        Expr::Binary {
780            op: BinaryOp::Add,
781            left: Box::new(accu_ref_then),
782            right: Box::new(one_step),
783        },
784        span.clone(),
785    );
786    let accu_ref_else = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
787    let loop_step = synthetic(
788        ctx,
789        Expr::Ternary {
790            cond: Box::new(cond),
791            then_expr: Box::new(increment),
792            else_expr: Box::new(accu_ref_else),
793        },
794        span.clone(),
795    );
796
797    let accu_ref_result = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
798    let one_result = synthetic(ctx, Expr::Int(1), span.clone());
799    let result = synthetic(
800        ctx,
801        Expr::Binary {
802            op: BinaryOp::Eq,
803            left: Box::new(accu_ref_result),
804            right: Box::new(one_result),
805        },
806        span.clone(),
807    );
808
809    MacroExpansion::Expanded(Spanned::new(
810        call_id,
811        Expr::Comprehension {
812            iter_var,
813            iter_var2,
814            iter_range: Box::new(receiver),
815            accu_var,
816            accu_init: Box::new(accu_init),
817            loop_condition: Box::new(loop_condition),
818            loop_step: Box::new(loop_step),
819            result: Box::new(result),
820        },
821        span,
822    ))
823}
824
825// === map() Macro ===
826
827fn expand_map_2arg(
828    ctx: &mut MacroContext,
829    span: Span,
830    receiver: Option<SpannedExpr>,
831    args: Vec<SpannedExpr>,
832) -> MacroExpansion {
833    let receiver = match receiver {
834        Some(r) => r,
835        None => return MacroExpansion::Error("map() requires a receiver".to_string()),
836    };
837
838    let iter_var = match extract_iter_var(ctx, &args[0]) {
839        Some(v) => v,
840        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
841    };
842    let transform = args[1].clone();
843
844    expand_map_impl(ctx, span, receiver, iter_var, None, transform, &args)
845}
846
847fn expand_map_3arg(
848    ctx: &mut MacroContext,
849    span: Span,
850    receiver: Option<SpannedExpr>,
851    args: Vec<SpannedExpr>,
852) -> MacroExpansion {
853    let receiver = match receiver {
854        Some(r) => r,
855        None => return MacroExpansion::Error("map() requires a receiver".to_string()),
856    };
857
858    let iter_var = match extract_iter_var(ctx, &args[0]) {
859        Some(v) => v,
860        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
861    };
862    let filter = args[1].clone();
863    let transform = args[2].clone();
864
865    expand_map_impl(ctx, span, receiver, iter_var, Some(filter), transform, &args)
866}
867
868fn expand_map_impl(
869    ctx: &mut MacroContext,
870    span: Span,
871    receiver: SpannedExpr,
872    iter_var: String,
873    filter_cond: Option<SpannedExpr>,
874    transform: SpannedExpr,
875    args: &[SpannedExpr],
876) -> MacroExpansion {
877    let call_id = ctx.next_id();
878    ctx.store_macro_call(call_id, &span, &receiver, "map", args);
879
880    let accu_var = ACCU_VAR.to_string();
881    let accu_init = synthetic(ctx, Expr::List(vec![]), span.clone());
882    let loop_condition = synthetic(ctx, Expr::Bool(true), span.clone());
883
884    let transformed_list = synthetic(ctx, Expr::List(vec![ListElement { expr: transform, optional: false }]), span.clone());
885    let accu_ref_step = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
886    let append_step = synthetic(
887        ctx,
888        Expr::Binary {
889            op: BinaryOp::Add,
890            left: Box::new(accu_ref_step),
891            right: Box::new(transformed_list),
892        },
893        span.clone(),
894    );
895
896    let loop_step = if let Some(filter) = filter_cond {
897        let accu_ref_else = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
898        synthetic(
899            ctx,
900            Expr::Ternary {
901                cond: Box::new(filter),
902                then_expr: Box::new(append_step),
903                else_expr: Box::new(accu_ref_else),
904            },
905            span.clone(),
906        )
907    } else {
908        append_step
909    };
910
911    let result = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
912
913    MacroExpansion::Expanded(Spanned::new(
914        call_id,
915        Expr::Comprehension {
916            iter_var,
917            iter_var2: String::new(),
918            iter_range: Box::new(receiver),
919            accu_var,
920            accu_init: Box::new(accu_init),
921            loop_condition: Box::new(loop_condition),
922            loop_step: Box::new(loop_step),
923            result: Box::new(result),
924        },
925        span,
926    ))
927}
928
929// === filter() Macro ===
930
931fn expand_filter(
932    ctx: &mut MacroContext,
933    span: Span,
934    receiver: Option<SpannedExpr>,
935    args: Vec<SpannedExpr>,
936) -> MacroExpansion {
937    let receiver = match receiver {
938        Some(r) => r,
939        None => return MacroExpansion::Error("filter() requires a receiver".to_string()),
940    };
941
942    let iter_var = match extract_iter_var(ctx, &args[0]) {
943        Some(v) => v,
944        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
945    };
946    let cond = args[1].clone();
947
948    let call_id = ctx.next_id();
949    ctx.store_macro_call(call_id, &span, &receiver, "filter", &args);
950
951    let accu_var = ACCU_VAR.to_string();
952    let accu_init = synthetic(ctx, Expr::List(vec![]), span.clone());
953    let loop_condition = synthetic(ctx, Expr::Bool(true), span.clone());
954
955    let iter_ref = synthetic(ctx, Expr::Ident(iter_var.clone()), span.clone());
956    let element_list = synthetic(ctx, Expr::List(vec![ListElement { expr: iter_ref, optional: false }]), span.clone());
957
958    let accu_ref_then = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
959    let append_step = synthetic(
960        ctx,
961        Expr::Binary {
962            op: BinaryOp::Add,
963            left: Box::new(accu_ref_then),
964            right: Box::new(element_list),
965        },
966        span.clone(),
967    );
968
969    let accu_ref_else = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
970    let loop_step = synthetic(
971        ctx,
972        Expr::Ternary {
973            cond: Box::new(cond),
974            then_expr: Box::new(append_step),
975            else_expr: Box::new(accu_ref_else),
976        },
977        span.clone(),
978    );
979
980    let result = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
981
982    MacroExpansion::Expanded(Spanned::new(
983        call_id,
984        Expr::Comprehension {
985            iter_var,
986            iter_var2: String::new(),
987            iter_range: Box::new(receiver),
988            accu_var,
989            accu_init: Box::new(accu_init),
990            loop_condition: Box::new(loop_condition),
991            loop_step: Box::new(loop_step),
992            result: Box::new(result),
993        },
994        span,
995    ))
996}
997
998// === transformList() Macro ===
999
1000fn expand_transform_list_3arg(
1001    ctx: &mut MacroContext,
1002    span: Span,
1003    receiver: Option<SpannedExpr>,
1004    args: Vec<SpannedExpr>,
1005) -> MacroExpansion {
1006    let receiver = match receiver {
1007        Some(r) => r,
1008        None => return MacroExpansion::Error("transformList() requires a receiver".to_string()),
1009    };
1010
1011    let iter_var = match extract_iter_var(ctx, &args[0]) {
1012        Some(v) => v,
1013        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
1014    };
1015    let iter_var2 = match extract_iter_var(ctx, &args[1]) {
1016        Some(v) => v,
1017        None => return MacroExpansion::Error("second argument must be an identifier".to_string()),
1018    };
1019    let transform = args[2].clone();
1020
1021    expand_transform_list_impl(ctx, span, receiver, iter_var, iter_var2, None, transform, &args)
1022}
1023
1024fn expand_transform_list_4arg(
1025    ctx: &mut MacroContext,
1026    span: Span,
1027    receiver: Option<SpannedExpr>,
1028    args: Vec<SpannedExpr>,
1029) -> MacroExpansion {
1030    let receiver = match receiver {
1031        Some(r) => r,
1032        None => return MacroExpansion::Error("transformList() requires a receiver".to_string()),
1033    };
1034
1035    let iter_var = match extract_iter_var(ctx, &args[0]) {
1036        Some(v) => v,
1037        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
1038    };
1039    let iter_var2 = match extract_iter_var(ctx, &args[1]) {
1040        Some(v) => v,
1041        None => return MacroExpansion::Error("second argument must be an identifier".to_string()),
1042    };
1043    let filter = args[2].clone();
1044    let transform = args[3].clone();
1045
1046    expand_transform_list_impl(ctx, span, receiver, iter_var, iter_var2, Some(filter), transform, &args)
1047}
1048
1049fn expand_transform_list_impl(
1050    ctx: &mut MacroContext,
1051    span: Span,
1052    receiver: SpannedExpr,
1053    iter_var: String,
1054    iter_var2: String,
1055    filter_cond: Option<SpannedExpr>,
1056    transform: SpannedExpr,
1057    args: &[SpannedExpr],
1058) -> MacroExpansion {
1059    let call_id = ctx.next_id();
1060    ctx.store_macro_call(call_id, &span, &receiver, "transformList", args);
1061
1062    let accu_var = ACCU_VAR.to_string();
1063    let accu_init = synthetic(ctx, Expr::List(vec![]), span.clone());
1064    let loop_condition = synthetic(ctx, Expr::Bool(true), span.clone());
1065
1066    let transformed_list = synthetic(ctx, Expr::List(vec![ListElement { expr: transform, optional: false }]), span.clone());
1067    let accu_ref_step = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
1068    let append_step = synthetic(
1069        ctx,
1070        Expr::Binary {
1071            op: BinaryOp::Add,
1072            left: Box::new(accu_ref_step),
1073            right: Box::new(transformed_list),
1074        },
1075        span.clone(),
1076    );
1077
1078    let loop_step = if let Some(filter) = filter_cond {
1079        let accu_ref_else = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
1080        synthetic(
1081            ctx,
1082            Expr::Ternary {
1083                cond: Box::new(filter),
1084                then_expr: Box::new(append_step),
1085                else_expr: Box::new(accu_ref_else),
1086            },
1087            span.clone(),
1088        )
1089    } else {
1090        append_step
1091    };
1092
1093    let result = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
1094
1095    MacroExpansion::Expanded(Spanned::new(
1096        call_id,
1097        Expr::Comprehension {
1098            iter_var,
1099            iter_var2,
1100            iter_range: Box::new(receiver),
1101            accu_var,
1102            accu_init: Box::new(accu_init),
1103            loop_condition: Box::new(loop_condition),
1104            loop_step: Box::new(loop_step),
1105            result: Box::new(result),
1106        },
1107        span,
1108    ))
1109}
1110
1111// === transformMap() Macro ===
1112
1113fn expand_transform_map_3arg(
1114    ctx: &mut MacroContext,
1115    span: Span,
1116    receiver: Option<SpannedExpr>,
1117    args: Vec<SpannedExpr>,
1118) -> MacroExpansion {
1119    let receiver = match receiver {
1120        Some(r) => r,
1121        None => return MacroExpansion::Error("transformMap() requires a receiver".to_string()),
1122    };
1123
1124    let iter_var = match extract_iter_var(ctx, &args[0]) {
1125        Some(v) => v,
1126        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
1127    };
1128    let iter_var2 = match extract_iter_var(ctx, &args[1]) {
1129        Some(v) => v,
1130        None => return MacroExpansion::Error("second argument must be an identifier".to_string()),
1131    };
1132    let transform = args[2].clone();
1133
1134    expand_transform_map_impl(ctx, span, receiver, iter_var, iter_var2, None, transform, &args)
1135}
1136
1137fn expand_transform_map_4arg(
1138    ctx: &mut MacroContext,
1139    span: Span,
1140    receiver: Option<SpannedExpr>,
1141    args: Vec<SpannedExpr>,
1142) -> MacroExpansion {
1143    let receiver = match receiver {
1144        Some(r) => r,
1145        None => return MacroExpansion::Error("transformMap() requires a receiver".to_string()),
1146    };
1147
1148    let iter_var = match extract_iter_var(ctx, &args[0]) {
1149        Some(v) => v,
1150        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
1151    };
1152    let iter_var2 = match extract_iter_var(ctx, &args[1]) {
1153        Some(v) => v,
1154        None => return MacroExpansion::Error("second argument must be an identifier".to_string()),
1155    };
1156    let filter = args[2].clone();
1157    let transform = args[3].clone();
1158
1159    expand_transform_map_impl(ctx, span, receiver, iter_var, iter_var2, Some(filter), transform, &args)
1160}
1161
1162fn expand_transform_map_impl(
1163    ctx: &mut MacroContext,
1164    span: Span,
1165    receiver: SpannedExpr,
1166    iter_var: String,
1167    iter_var2: String,
1168    filter_cond: Option<SpannedExpr>,
1169    transform: SpannedExpr,
1170    args: &[SpannedExpr],
1171) -> MacroExpansion {
1172    let call_id = ctx.next_id();
1173    ctx.store_macro_call(call_id, &span, &receiver, "transformMap", args);
1174
1175    let accu_var = ACCU_VAR.to_string();
1176    let accu_init = synthetic(ctx, Expr::Map(vec![]), span.clone());
1177    let loop_condition = synthetic(ctx, Expr::Bool(true), span.clone());
1178
1179    let key_ref = synthetic(ctx, Expr::Ident(iter_var.clone()), span.clone());
1180    let transformed_map = synthetic(ctx, Expr::Map(vec![MapEntry { key: key_ref, value: transform, optional: false }]), span.clone());
1181    let accu_ref_step = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
1182    let append_step = synthetic(
1183        ctx,
1184        Expr::Binary {
1185            op: BinaryOp::Add,
1186            left: Box::new(accu_ref_step),
1187            right: Box::new(transformed_map),
1188        },
1189        span.clone(),
1190    );
1191
1192    let loop_step = if let Some(filter) = filter_cond {
1193        let accu_ref_else = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
1194        synthetic(
1195            ctx,
1196            Expr::Ternary {
1197                cond: Box::new(filter),
1198                then_expr: Box::new(append_step),
1199                else_expr: Box::new(accu_ref_else),
1200            },
1201            span.clone(),
1202        )
1203    } else {
1204        append_step
1205    };
1206
1207    let result = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
1208
1209    MacroExpansion::Expanded(Spanned::new(
1210        call_id,
1211        Expr::Comprehension {
1212            iter_var,
1213            iter_var2,
1214            iter_range: Box::new(receiver),
1215            accu_var,
1216            accu_init: Box::new(accu_init),
1217            loop_condition: Box::new(loop_condition),
1218            loop_step: Box::new(loop_step),
1219            result: Box::new(result),
1220        },
1221        span,
1222    ))
1223}
1224
1225// === cel.bind() Macro ===
1226
1227/// Expand `cel.bind(var, init, body)` to `Expr::Bind`.
1228///
1229/// This macro binds a variable to a value for use within a scoped expression.
1230/// The variable is only visible within the body expression.
1231///
1232/// # Example
1233/// `cel.bind(msg, "hello", msg + msg)` evaluates to `"hellohello"`
1234fn expand_bind(
1235    ctx: &mut MacroContext,
1236    span: Span,
1237    _receiver: Option<SpannedExpr>,
1238    args: Vec<SpannedExpr>,
1239) -> MacroExpansion {
1240    if args.len() != 3 {
1241        return MacroExpansion::Error(format!(
1242            "cel.bind() requires 3 arguments, got {}",
1243            args.len()
1244        ));
1245    }
1246
1247    // First argument must be an identifier (the variable name)
1248    let var_name = match &args[0].node {
1249        Expr::Ident(name) => name.clone(),
1250        _ => {
1251            return MacroExpansion::Error(
1252                "cel.bind() first argument must be an identifier".to_string(),
1253            )
1254        }
1255    };
1256
1257    // Second argument is the initializer expression
1258    let init = args[1].clone();
1259
1260    // Third argument is the body expression
1261    let body = args[2].clone();
1262
1263    let call_id = ctx.next_id();
1264    ctx.store_macro_call(call_id, &span, &args[0], "cel.bind", &args);
1265
1266    MacroExpansion::Expanded(Spanned::new(
1267        call_id,
1268        Expr::Bind {
1269            var_name,
1270            init: Box::new(init),
1271            body: Box::new(body),
1272        },
1273        span,
1274    ))
1275}
1276
1277// === optMap() and optFlatMap() Helper Functions ===
1278
1279/// Build a method call: receiver.method(args)
1280fn build_method_call(
1281    ctx: &mut MacroContext,
1282    span: &Span,
1283    receiver: SpannedExpr,
1284    method: &str,
1285    args: Vec<SpannedExpr>,
1286) -> SpannedExpr {
1287    let member = synthetic(
1288        ctx,
1289        Expr::Member {
1290            expr: Box::new(receiver),
1291            field: method.to_string(),
1292            optional: false,
1293        },
1294        span.clone(),
1295    );
1296
1297    synthetic(
1298        ctx,
1299        Expr::Call {
1300            expr: Box::new(member),
1301            args,
1302        },
1303        span.clone(),
1304    )
1305}
1306
1307/// Build: optional.of(expr)
1308fn build_optional_of(ctx: &mut MacroContext, span: &Span, expr: SpannedExpr) -> SpannedExpr {
1309    let optional_ident = synthetic(ctx, Expr::Ident("optional".to_string()), span.clone());
1310    let optional_of_member = synthetic(
1311        ctx,
1312        Expr::Member {
1313            expr: Box::new(optional_ident),
1314            field: "of".to_string(),
1315            optional: false,
1316        },
1317        span.clone(),
1318    );
1319
1320    synthetic(
1321        ctx,
1322        Expr::Call {
1323            expr: Box::new(optional_of_member),
1324            args: vec![expr],
1325        },
1326        span.clone(),
1327    )
1328}
1329
1330/// Build: optional.none()
1331fn build_optional_none(ctx: &mut MacroContext, span: &Span) -> SpannedExpr {
1332    let optional_ident = synthetic(ctx, Expr::Ident("optional".to_string()), span.clone());
1333    let optional_none_member = synthetic(
1334        ctx,
1335        Expr::Member {
1336            expr: Box::new(optional_ident),
1337            field: "none".to_string(),
1338            optional: false,
1339        },
1340        span.clone(),
1341    );
1342
1343    synthetic(
1344        ctx,
1345        Expr::Call {
1346            expr: Box::new(optional_none_member),
1347            args: vec![],
1348        },
1349        span.clone(),
1350    )
1351}
1352
1353// === optMap() Macro ===
1354
1355/// Expand `optional.optMap(var, expr)` to:
1356/// ```text
1357/// receiver.hasValue()
1358///     ? cel.bind(var, receiver.value(), optional.of(expr))
1359///     : optional.none()
1360/// ```
1361fn expand_opt_map(
1362    ctx: &mut MacroContext,
1363    span: Span,
1364    receiver: Option<SpannedExpr>,
1365    args: Vec<SpannedExpr>,
1366) -> MacroExpansion {
1367    let receiver = match receiver {
1368        Some(r) => r,
1369        None => return MacroExpansion::Error("optMap() requires a receiver".to_string()),
1370    };
1371
1372    let iter_var = match extract_iter_var(ctx, &args[0]) {
1373        Some(v) => v,
1374        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
1375    };
1376    let transform = args[1].clone();
1377
1378    let call_id = ctx.next_id();
1379    ctx.store_macro_call(call_id, &span, &receiver, "optMap", &args);
1380
1381    // Build: receiver.hasValue()
1382    let has_value_call = build_method_call(ctx, &span, receiver.clone(), "hasValue", vec![]);
1383
1384    // Build: receiver.value()
1385    let value_call = build_method_call(ctx, &span, receiver, "value", vec![]);
1386
1387    // Build: optional.of(transform)
1388    let wrapped_result = build_optional_of(ctx, &span, transform);
1389
1390    // Build: cel.bind(var, receiver.value(), optional.of(transform))
1391    let bind_expr = synthetic(
1392        ctx,
1393        Expr::Bind {
1394            var_name: iter_var,
1395            init: Box::new(value_call),
1396            body: Box::new(wrapped_result),
1397        },
1398        span.clone(),
1399    );
1400
1401    // Build: optional.none()
1402    let none_call = build_optional_none(ctx, &span);
1403
1404    // Build ternary: hasValue ? bind : none
1405    MacroExpansion::Expanded(Spanned::new(
1406        call_id,
1407        Expr::Ternary {
1408            cond: Box::new(has_value_call),
1409            then_expr: Box::new(bind_expr),
1410            else_expr: Box::new(none_call),
1411        },
1412        span,
1413    ))
1414}
1415
1416// === optFlatMap() Macro ===
1417
1418/// Expand `optional.optFlatMap(var, expr)` to:
1419/// ```text
1420/// receiver.hasValue()
1421///     ? cel.bind(var, receiver.value(), expr)
1422///     : optional.none()
1423/// ```
1424///
1425/// Note: Unlike optMap, this does NOT wrap the result in optional.of()
1426/// since expr is expected to already return an optional.
1427fn expand_opt_flat_map(
1428    ctx: &mut MacroContext,
1429    span: Span,
1430    receiver: Option<SpannedExpr>,
1431    args: Vec<SpannedExpr>,
1432) -> MacroExpansion {
1433    let receiver = match receiver {
1434        Some(r) => r,
1435        None => return MacroExpansion::Error("optFlatMap() requires a receiver".to_string()),
1436    };
1437
1438    let iter_var = match extract_iter_var(ctx, &args[0]) {
1439        Some(v) => v,
1440        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
1441    };
1442    let transform = args[1].clone();
1443
1444    let call_id = ctx.next_id();
1445    ctx.store_macro_call(call_id, &span, &receiver, "optFlatMap", &args);
1446
1447    // Build: receiver.hasValue()
1448    let has_value_call = build_method_call(ctx, &span, receiver.clone(), "hasValue", vec![]);
1449
1450    // Build: receiver.value()
1451    let value_call = build_method_call(ctx, &span, receiver, "value", vec![]);
1452
1453    // Build: cel.bind(var, receiver.value(), transform)
1454    // Note: transform is NOT wrapped in optional.of()
1455    let bind_expr = synthetic(
1456        ctx,
1457        Expr::Bind {
1458            var_name: iter_var,
1459            init: Box::new(value_call),
1460            body: Box::new(transform),
1461        },
1462        span.clone(),
1463    );
1464
1465    // Build: optional.none()
1466    let none_call = build_optional_none(ctx, &span);
1467
1468    // Build ternary: hasValue ? bind : none
1469    MacroExpansion::Expanded(Spanned::new(
1470        call_id,
1471        Expr::Ternary {
1472            cond: Box::new(has_value_call),
1473            then_expr: Box::new(bind_expr),
1474            else_expr: Box::new(none_call),
1475        },
1476        span,
1477    ))
1478}
1479
1480#[cfg(test)]
1481mod tests {
1482    use super::*;
1483
1484    fn dummy_expander(
1485        _ctx: &mut MacroContext,
1486        _span: Span,
1487        _receiver: Option<SpannedExpr>,
1488        _args: Vec<SpannedExpr>,
1489    ) -> MacroExpansion {
1490        MacroExpansion::Error("dummy".to_string())
1491    }
1492
1493    #[test]
1494    fn test_arg_count_exact() {
1495        let exact = ArgCount::Exact(2);
1496        assert!(exact.matches(2));
1497        assert!(!exact.matches(1));
1498        assert!(!exact.matches(3));
1499        assert_eq!(exact.count(), 2);
1500        assert!(!exact.is_vararg());
1501    }
1502
1503    #[test]
1504    fn test_arg_count_vararg() {
1505        let vararg = ArgCount::VarArg(2);
1506        assert!(vararg.matches(2));
1507        assert!(vararg.matches(3));
1508        assert!(vararg.matches(10));
1509        assert!(!vararg.matches(1));
1510        assert_eq!(vararg.count(), 2);
1511        assert!(vararg.is_vararg());
1512    }
1513
1514    #[test]
1515    fn test_macro_key() {
1516        let m = Macro::new("all", MacroStyle::Receiver, ArgCount::Exact(2), dummy_expander);
1517        assert_eq!(m.key(), "all:2:true");
1518
1519        let m2 = Macro::new("has", MacroStyle::Global, ArgCount::Exact(1), dummy_expander);
1520        assert_eq!(m2.key(), "has:1:false");
1521    }
1522
1523    #[test]
1524    fn test_registry_lookup_exact() {
1525        let mut registry = MacroRegistry::new();
1526        registry.register(Macro::new("all", MacroStyle::Receiver, ArgCount::Exact(2), dummy_expander));
1527        registry.register(Macro::new("all", MacroStyle::Receiver, ArgCount::Exact(3), dummy_expander));
1528
1529        assert!(registry.lookup("all", 2, true).is_some());
1530        assert!(registry.lookup("all", 3, true).is_some());
1531        assert!(registry.lookup("all", 4, true).is_none());
1532        assert!(registry.lookup("all", 2, false).is_none());
1533    }
1534
1535    #[test]
1536    fn test_registry_lookup_vararg() {
1537        let mut registry = MacroRegistry::new();
1538        registry.register(Macro::new("custom", MacroStyle::Receiver, ArgCount::VarArg(2), dummy_expander));
1539
1540        assert!(registry.lookup("custom", 2, true).is_some());
1541        assert!(registry.lookup("custom", 3, true).is_some());
1542        assert!(registry.lookup("custom", 10, true).is_some());
1543        assert!(registry.lookup("custom", 1, true).is_none());
1544    }
1545
1546    #[test]
1547    fn test_registry_standard() {
1548        let registry = MacroRegistry::standard();
1549
1550        assert!(registry.lookup("has", 1, false).is_some());
1551        assert!(registry.lookup("all", 2, true).is_some());
1552        assert!(registry.lookup("all", 3, true).is_some());
1553        assert!(registry.lookup("exists", 2, true).is_some());
1554        assert!(registry.lookup("exists", 3, true).is_some());
1555        assert!(registry.lookup("exists_one", 2, true).is_some());
1556        assert!(registry.lookup("exists_one", 3, true).is_some());
1557        assert!(registry.lookup("map", 2, true).is_some());
1558        assert!(registry.lookup("map", 3, true).is_some());
1559        assert!(registry.lookup("filter", 2, true).is_some());
1560    }
1561
1562    #[test]
1563    fn test_registry_contains() {
1564        let registry = MacroRegistry::standard();
1565        assert!(registry.contains("has"));
1566        assert!(registry.contains("all"));
1567        assert!(registry.contains("cel.bind"));
1568        assert!(!registry.contains("nonexistent"));
1569    }
1570
1571    #[test]
1572    fn test_cel_bind_macro_registered() {
1573        let registry = MacroRegistry::standard();
1574        assert!(registry.lookup("cel.bind", 3, false).is_some());
1575        // cel.bind is global, not a receiver macro
1576        assert!(registry.lookup("cel.bind", 3, true).is_none());
1577    }
1578
1579    #[test]
1580    fn test_opt_map_macro_registered() {
1581        let registry = MacroRegistry::standard();
1582        // optMap is a receiver macro with 2 args
1583        assert!(registry.lookup("optMap", 2, true).is_some());
1584        // Should not be found as global
1585        assert!(registry.lookup("optMap", 2, false).is_none());
1586        // Should not match wrong arg count
1587        assert!(registry.lookup("optMap", 1, true).is_none());
1588        assert!(registry.lookup("optMap", 3, true).is_none());
1589    }
1590
1591    #[test]
1592    fn test_opt_flat_map_macro_registered() {
1593        let registry = MacroRegistry::standard();
1594        // optFlatMap is a receiver macro with 2 args
1595        assert!(registry.lookup("optFlatMap", 2, true).is_some());
1596        // Should not be found as global
1597        assert!(registry.lookup("optFlatMap", 2, false).is_none());
1598        // Should not match wrong arg count
1599        assert!(registry.lookup("optFlatMap", 1, true).is_none());
1600        assert!(registry.lookup("optFlatMap", 3, true).is_none());
1601    }
1602
1603    #[test]
1604    fn test_registry_contains_opt_macros() {
1605        let registry = MacroRegistry::standard();
1606        assert!(registry.contains("optMap"));
1607        assert!(registry.contains("optFlatMap"));
1608    }
1609}