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::{
22    BinaryOp, ComprehensionData, Expr, ListElement, MapEntry, Span, Spanned, SpannedExpr, UnaryOp,
23};
24
25/// Indicates whether a macro is called as a global function or as a method on a receiver.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum MacroStyle {
28    /// Global function call: `macro_name(args...)`
29    Global,
30    /// Receiver-style method call: `receiver.macro_name(args...)`
31    Receiver,
32}
33
34/// Specifies the expected argument count for a macro.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum ArgCount {
37    /// Exact number of arguments required.
38    Exact(usize),
39    /// Variable arguments with a minimum count.
40    VarArg(usize),
41}
42
43impl ArgCount {
44    /// Check if the given argument count matches this specification.
45    pub fn matches(&self, count: usize) -> bool {
46        match self {
47            ArgCount::Exact(n) => count == *n,
48            ArgCount::VarArg(min) => count >= *min,
49        }
50    }
51
52    /// Get the count value (exact count or minimum for vararg).
53    pub fn count(&self) -> usize {
54        match self {
55            ArgCount::Exact(n) => *n,
56            ArgCount::VarArg(min) => *min,
57        }
58    }
59
60    /// Returns true if this is a vararg specification.
61    pub fn is_vararg(&self) -> bool {
62        matches!(self, ArgCount::VarArg(_))
63    }
64}
65
66/// Result of macro expansion.
67#[derive(Debug)]
68pub enum MacroExpansion {
69    /// Macro was successfully expanded to this expression.
70    Expanded(SpannedExpr),
71    /// Macro signature matched but expansion failed (e.g., invalid arguments).
72    /// The string contains an error message.
73    Error(String),
74}
75
76/// Context provided to macro expanders for creating AST nodes.
77///
78/// This provides the necessary state for creating synthetic AST nodes
79/// during macro expansion, including ID allocation and error reporting.
80pub struct MacroContext<'a> {
81    /// Function to allocate the next unique node ID.
82    next_id_fn: &'a mut dyn FnMut() -> i64,
83    /// Accumulated errors during expansion.
84    errors: Vec<(String, Span)>,
85    /// Function to store macro call for IDE features.
86    store_macro_call_fn: Option<MacroCallStoreFn<'a>>,
87}
88
89type MacroCallStoreFn<'a> = &'a mut dyn FnMut(i64, &Span, &SpannedExpr, &str, &[SpannedExpr]);
90
91impl<'a> MacroContext<'a> {
92    /// Create a new macro context.
93    pub fn new(
94        next_id_fn: &'a mut dyn FnMut() -> i64,
95        store_macro_call_fn: Option<MacroCallStoreFn<'a>>,
96    ) -> Self {
97        Self {
98            next_id_fn,
99            errors: Vec::new(),
100            store_macro_call_fn,
101        }
102    }
103
104    /// Allocate the next unique node ID.
105    pub fn next_id(&mut self) -> i64 {
106        (self.next_id_fn)()
107    }
108
109    /// Add an error message.
110    pub fn add_error(&mut self, message: String, span: Span) {
111        self.errors.push((message, span));
112    }
113
114    /// Take accumulated errors.
115    pub fn take_errors(&mut self) -> Vec<(String, Span)> {
116        std::mem::take(&mut self.errors)
117    }
118
119    /// Store the original macro call expression for IDE features.
120    pub fn store_macro_call(
121        &mut self,
122        call_id: i64,
123        span: &Span,
124        receiver: &SpannedExpr,
125        method_name: &str,
126        args: &[SpannedExpr],
127    ) {
128        if let Some(f) = &mut self.store_macro_call_fn {
129            f(call_id, span, receiver, method_name, args);
130        }
131    }
132}
133
134/// Type alias for macro expander functions.
135///
136/// # Parameters
137/// - `ctx`: Macro context for ID allocation and error reporting
138/// - `span`: Source span of the entire call expression
139/// - `receiver`: The receiver expression for receiver-style macros, None for global macros
140/// - `args`: The arguments passed to the macro
141///
142/// # Returns
143/// - `MacroExpansion::Expanded(expr)` on successful expansion
144/// - `MacroExpansion::Error(msg)` if expansion fails
145pub type MacroExpander = fn(
146    ctx: &mut MacroContext,
147    span: Span,
148    receiver: Option<SpannedExpr>,
149    args: Vec<SpannedExpr>,
150) -> MacroExpansion;
151
152/// Definition of a single macro.
153#[derive(Clone)]
154pub struct Macro {
155    /// The macro name (e.g., "all", "has", "map").
156    pub name: &'static str,
157    /// Whether this is a global or receiver-style macro.
158    pub style: MacroStyle,
159    /// The expected argument count.
160    pub arg_count: ArgCount,
161    /// The expansion function.
162    pub expander: MacroExpander,
163    /// Optional description for documentation/IDE features.
164    pub description: Option<&'static str>,
165}
166
167impl Macro {
168    /// Create a new macro definition.
169    pub const fn new(
170        name: &'static str,
171        style: MacroStyle,
172        arg_count: ArgCount,
173        expander: MacroExpander,
174    ) -> Self {
175        Self {
176            name,
177            style,
178            arg_count,
179            expander,
180            description: None,
181        }
182    }
183
184    /// Create a new macro definition with a description.
185    pub const fn with_description(
186        name: &'static str,
187        style: MacroStyle,
188        arg_count: ArgCount,
189        expander: MacroExpander,
190        description: &'static str,
191    ) -> Self {
192        Self {
193            name,
194            style,
195            arg_count,
196            expander,
197            description: Some(description),
198        }
199    }
200
201    /// Generate the lookup key for this macro.
202    pub fn key(&self) -> String {
203        make_key(
204            self.name,
205            self.arg_count.count(),
206            self.style == MacroStyle::Receiver,
207        )
208    }
209}
210
211impl std::fmt::Debug for Macro {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        f.debug_struct("Macro")
214            .field("name", &self.name)
215            .field("style", &self.style)
216            .field("arg_count", &self.arg_count)
217            .field("description", &self.description)
218            .finish_non_exhaustive()
219    }
220}
221
222/// Generate a lookup key for a macro.
223fn make_key(name: &str, arg_count: usize, is_receiver: bool) -> String {
224    format!("{}:{}:{}", name, arg_count, is_receiver)
225}
226
227/// Registry of macros with efficient lookup.
228///
229/// Macros are keyed by `name:arg_count:is_receiver`.
230/// Lookup tries the exact key first, then falls back to a vararg key.
231#[derive(Debug, Clone)]
232pub struct MacroRegistry {
233    /// Map from key to macro definition.
234    macros: HashMap<String, Macro>,
235    /// Track vararg macros by name:is_receiver for fallback lookup.
236    vararg_keys: HashMap<String, usize>,
237}
238
239impl Default for MacroRegistry {
240    fn default() -> Self {
241        Self::new()
242    }
243}
244
245impl MacroRegistry {
246    /// Create an empty macro registry.
247    pub fn new() -> Self {
248        Self {
249            macros: HashMap::new(),
250            vararg_keys: HashMap::new(),
251        }
252    }
253
254    /// Create a registry with the standard CEL macros.
255    pub fn standard() -> Self {
256        let mut registry = Self::new();
257        for macro_def in STANDARD_MACROS {
258            registry.register(macro_def.clone());
259        }
260        registry
261    }
262
263    /// Register a macro in the registry.
264    pub fn register(&mut self, macro_def: Macro) {
265        let key = macro_def.key();
266
267        // Track vararg macros for fallback lookup
268        if macro_def.arg_count.is_vararg() {
269            let vararg_key = format!(
270                "{}:{}",
271                macro_def.name,
272                macro_def.style == MacroStyle::Receiver
273            );
274            self.vararg_keys
275                .insert(vararg_key, macro_def.arg_count.count());
276        }
277
278        self.macros.insert(key, macro_def);
279    }
280
281    /// Look up a macro by name, argument count, and receiver style.
282    ///
283    /// First tries exact match, then falls back to vararg match if applicable.
284    pub fn lookup(&self, name: &str, arg_count: usize, is_receiver: bool) -> Option<&Macro> {
285        // Try exact match first
286        let exact_key = make_key(name, arg_count, is_receiver);
287        if let Some(m) = self.macros.get(&exact_key) {
288            return Some(m);
289        }
290
291        // Try vararg fallback
292        let vararg_lookup_key = format!("{}:{}", name, is_receiver);
293        if let Some(&min_args) = self.vararg_keys.get(&vararg_lookup_key) {
294            if arg_count >= min_args {
295                let vararg_key = make_key(name, min_args, is_receiver);
296                return self.macros.get(&vararg_key);
297            }
298        }
299
300        None
301    }
302
303    /// Check if the registry contains a macro with the given name.
304    pub fn contains(&self, name: &str) -> bool {
305        self.macros.values().any(|m| m.name == name)
306    }
307
308    /// Get an iterator over all registered macros.
309    pub fn iter(&self) -> impl Iterator<Item = &Macro> {
310        self.macros.values()
311    }
312
313    /// Get the number of registered macros.
314    pub fn len(&self) -> usize {
315        self.macros.len()
316    }
317
318    /// Check if the registry is empty.
319    pub fn is_empty(&self) -> bool {
320        self.macros.is_empty()
321    }
322}
323
324// ============================================================================
325// Standard CEL Macros
326// ============================================================================
327
328/// Accumulator variable name used in comprehension expansions.
329const ACCU_VAR: &str = "__result__";
330
331/// Standard CEL macros.
332pub static STANDARD_MACROS: &[Macro] = &[
333    // has(m.x) - global, 1 arg
334    Macro::with_description(
335        "has",
336        MacroStyle::Global,
337        ArgCount::Exact(1),
338        expand_has,
339        "Tests whether a field is set on a message",
340    ),
341    // all - receiver, 2 or 3 args
342    Macro::with_description(
343        "all",
344        MacroStyle::Receiver,
345        ArgCount::Exact(2),
346        expand_all_2arg,
347        "Tests whether all elements satisfy a condition",
348    ),
349    Macro::with_description(
350        "all",
351        MacroStyle::Receiver,
352        ArgCount::Exact(3),
353        expand_all_3arg,
354        "Tests whether all elements satisfy a condition (two-variable form)",
355    ),
356    // exists - receiver, 2 or 3 args
357    Macro::with_description(
358        "exists",
359        MacroStyle::Receiver,
360        ArgCount::Exact(2),
361        expand_exists_2arg,
362        "Tests whether any element satisfies a condition",
363    ),
364    Macro::with_description(
365        "exists",
366        MacroStyle::Receiver,
367        ArgCount::Exact(3),
368        expand_exists_3arg,
369        "Tests whether any element satisfies a condition (two-variable form)",
370    ),
371    // exists_one - receiver, 2 or 3 args
372    Macro::with_description(
373        "exists_one",
374        MacroStyle::Receiver,
375        ArgCount::Exact(2),
376        expand_exists_one_2arg,
377        "Tests whether exactly one element satisfies a condition",
378    ),
379    Macro::with_description(
380        "exists_one",
381        MacroStyle::Receiver,
382        ArgCount::Exact(3),
383        expand_exists_one_3arg,
384        "Tests whether exactly one element satisfies a condition (two-variable form)",
385    ),
386    // existsOne - camelCase alias (cel-go compatibility)
387    Macro::with_description(
388        "existsOne",
389        MacroStyle::Receiver,
390        ArgCount::Exact(2),
391        expand_exists_one_2arg,
392        "Tests whether exactly one element satisfies a condition",
393    ),
394    Macro::with_description(
395        "existsOne",
396        MacroStyle::Receiver,
397        ArgCount::Exact(3),
398        expand_exists_one_3arg,
399        "Tests whether exactly one element satisfies a condition (two-variable form)",
400    ),
401    // map - receiver, 2 or 3 args
402    Macro::with_description(
403        "map",
404        MacroStyle::Receiver,
405        ArgCount::Exact(2),
406        expand_map_2arg,
407        "Transforms elements of a list",
408    ),
409    Macro::with_description(
410        "map",
411        MacroStyle::Receiver,
412        ArgCount::Exact(3),
413        expand_map_3arg,
414        "Transforms elements of a list with filtering",
415    ),
416    // filter - receiver, 2 args
417    Macro::with_description(
418        "filter",
419        MacroStyle::Receiver,
420        ArgCount::Exact(2),
421        expand_filter,
422        "Filters elements of a list by a condition",
423    ),
424    // transformList - receiver, 3 or 4 args
425    Macro::with_description(
426        "transformList",
427        MacroStyle::Receiver,
428        ArgCount::Exact(3),
429        expand_transform_list_3arg,
430        "Transforms list elements with index and value variables",
431    ),
432    Macro::with_description(
433        "transformList",
434        MacroStyle::Receiver,
435        ArgCount::Exact(4),
436        expand_transform_list_4arg,
437        "Transforms list elements with index, value, and filter",
438    ),
439    // transformMap - receiver, 3 or 4 args
440    Macro::with_description(
441        "transformMap",
442        MacroStyle::Receiver,
443        ArgCount::Exact(3),
444        expand_transform_map_3arg,
445        "Transforms map entries with key and value variables",
446    ),
447    Macro::with_description(
448        "transformMap",
449        MacroStyle::Receiver,
450        ArgCount::Exact(4),
451        expand_transform_map_4arg,
452        "Transforms map entries with key, value, and filter",
453    ),
454    // cel.bind - global, 3 args
455    Macro::with_description(
456        "cel.bind",
457        MacroStyle::Global,
458        ArgCount::Exact(3),
459        expand_bind,
460        "Binds a variable to a value for use in an expression",
461    ),
462    // optMap - receiver, 2 args
463    Macro::with_description(
464        "optMap",
465        MacroStyle::Receiver,
466        ArgCount::Exact(2),
467        expand_opt_map,
468        "Transforms an optional value if present",
469    ),
470    // optFlatMap - receiver, 2 args
471    Macro::with_description(
472        "optFlatMap",
473        MacroStyle::Receiver,
474        ArgCount::Exact(2),
475        expand_opt_flat_map,
476        "Chains optional operations",
477    ),
478    // proto.hasExt - global, 2 args
479    Macro::with_description(
480        "proto.hasExt",
481        MacroStyle::Global,
482        ArgCount::Exact(2),
483        expand_proto_has_ext,
484        "Tests whether a proto extension field is set",
485    ),
486    // proto.getExt - global, 2 args
487    Macro::with_description(
488        "proto.getExt",
489        MacroStyle::Global,
490        ArgCount::Exact(2),
491        expand_proto_get_ext,
492        "Gets the value of a proto extension field",
493    ),
494    // cel.block - global, 2 args
495    Macro::with_description(
496        "cel.block",
497        MacroStyle::Global,
498        ArgCount::Exact(2),
499        expand_cel_block,
500        "Binds a list of slot expressions for common subexpression elimination",
501    ),
502    // cel.index - global, 1 arg
503    Macro::with_description(
504        "cel.index",
505        MacroStyle::Global,
506        ArgCount::Exact(1),
507        expand_cel_index,
508        "References a slot variable by index in a cel.block",
509    ),
510    // cel.iterVar - global, 2 args
511    Macro::with_description(
512        "cel.iterVar",
513        MacroStyle::Global,
514        ArgCount::Exact(2),
515        expand_cel_iter_var,
516        "References an iterator variable for nested comprehensions",
517    ),
518    // cel.accuVar - global, 2 args
519    Macro::with_description(
520        "cel.accuVar",
521        MacroStyle::Global,
522        ArgCount::Exact(2),
523        expand_cel_accu_var,
524        "References an accumulator variable for nested comprehensions",
525    ),
526];
527
528// === Helper Functions ===
529
530/// Create a synthetic spanned expression.
531fn synthetic(ctx: &mut MacroContext, node: Expr, span: Span) -> SpannedExpr {
532    Spanned::new(ctx.next_id(), node, span)
533}
534
535/// Extract iteration variable name from an expression.
536/// Returns None and adds error if not a simple identifier.
537fn extract_iter_var(ctx: &mut MacroContext, expr: &SpannedExpr) -> Option<String> {
538    match &expr.node {
539        Expr::Ident(name) => Some(name.clone()),
540        _ => {
541            ctx.add_error(
542                "iteration variable must be an identifier".to_string(),
543                expr.span.clone(),
544            );
545            None
546        }
547    }
548}
549
550// === has() Macro ===
551
552/// Expand `has(m.x)` to `MemberTestOnly { expr: m, field: x }`.
553fn expand_has(
554    ctx: &mut MacroContext,
555    span: Span,
556    _receiver: Option<SpannedExpr>,
557    args: Vec<SpannedExpr>,
558) -> MacroExpansion {
559    if args.len() != 1 {
560        return MacroExpansion::Error(format!("has() requires 1 argument, got {}", args.len()));
561    }
562
563    let arg = args.into_iter().next().unwrap();
564
565    match arg.node {
566        Expr::Member { expr, field, .. } => {
567            let result = Spanned::new(ctx.next_id(), Expr::MemberTestOnly { expr, field }, span);
568            MacroExpansion::Expanded(result)
569        }
570        _ => MacroExpansion::Error(
571            "has() argument must be a field selection (e.g., has(m.x))".to_string(),
572        ),
573    }
574}
575
576// === all() Macro ===
577
578fn expand_all_2arg(
579    ctx: &mut MacroContext,
580    span: Span,
581    receiver: Option<SpannedExpr>,
582    args: Vec<SpannedExpr>,
583) -> MacroExpansion {
584    let receiver = match receiver {
585        Some(r) => r,
586        None => return MacroExpansion::Error("all() requires a receiver".to_string()),
587    };
588
589    let iter_var = match extract_iter_var(ctx, &args[0]) {
590        Some(v) => v,
591        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
592    };
593    let cond = args[1].clone();
594
595    expand_all_impl(ctx, span, receiver, iter_var, String::new(), cond, &args)
596}
597
598fn expand_all_3arg(
599    ctx: &mut MacroContext,
600    span: Span,
601    receiver: Option<SpannedExpr>,
602    args: Vec<SpannedExpr>,
603) -> MacroExpansion {
604    let receiver = match receiver {
605        Some(r) => r,
606        None => return MacroExpansion::Error("all() requires a receiver".to_string()),
607    };
608
609    let iter_var = match extract_iter_var(ctx, &args[0]) {
610        Some(v) => v,
611        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
612    };
613    let iter_var2 = match extract_iter_var(ctx, &args[1]) {
614        Some(v) => v,
615        None => return MacroExpansion::Error("second argument must be an identifier".to_string()),
616    };
617    let cond = args[2].clone();
618
619    expand_all_impl(ctx, span, receiver, iter_var, iter_var2, cond, &args)
620}
621
622fn expand_all_impl(
623    ctx: &mut MacroContext,
624    span: Span,
625    receiver: SpannedExpr,
626    iter_var: String,
627    iter_var2: String,
628    cond: SpannedExpr,
629    args: &[SpannedExpr],
630) -> MacroExpansion {
631    let call_id = ctx.next_id();
632    ctx.store_macro_call(call_id, &span, &receiver, "all", args);
633
634    let accu_var = ACCU_VAR.to_string();
635    let accu_init = synthetic(ctx, Expr::Bool(true), span.clone());
636    let loop_condition = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
637
638    let accu_ref = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
639    let loop_step = synthetic(
640        ctx,
641        Expr::Binary {
642            op: BinaryOp::And,
643            left: Box::new(cond),
644            right: Box::new(accu_ref),
645        },
646        span.clone(),
647    );
648
649    let result = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
650
651    MacroExpansion::Expanded(Spanned::new(
652        call_id,
653        Expr::Comprehension(ComprehensionData {
654            iter_var,
655            iter_var2,
656            iter_range: Box::new(receiver),
657            accu_var,
658            accu_init: Box::new(accu_init),
659            loop_condition: Box::new(loop_condition),
660            loop_step: Box::new(loop_step),
661            result: Box::new(result),
662        }),
663        span,
664    ))
665}
666
667// === exists() Macro ===
668
669fn expand_exists_2arg(
670    ctx: &mut MacroContext,
671    span: Span,
672    receiver: Option<SpannedExpr>,
673    args: Vec<SpannedExpr>,
674) -> MacroExpansion {
675    let receiver = match receiver {
676        Some(r) => r,
677        None => return MacroExpansion::Error("exists() requires a receiver".to_string()),
678    };
679
680    let iter_var = match extract_iter_var(ctx, &args[0]) {
681        Some(v) => v,
682        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
683    };
684    let cond = args[1].clone();
685
686    expand_exists_impl(ctx, span, receiver, iter_var, String::new(), cond, &args)
687}
688
689fn expand_exists_3arg(
690    ctx: &mut MacroContext,
691    span: Span,
692    receiver: Option<SpannedExpr>,
693    args: Vec<SpannedExpr>,
694) -> MacroExpansion {
695    let receiver = match receiver {
696        Some(r) => r,
697        None => return MacroExpansion::Error("exists() requires a receiver".to_string()),
698    };
699
700    let iter_var = match extract_iter_var(ctx, &args[0]) {
701        Some(v) => v,
702        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
703    };
704    let iter_var2 = match extract_iter_var(ctx, &args[1]) {
705        Some(v) => v,
706        None => return MacroExpansion::Error("second argument must be an identifier".to_string()),
707    };
708    let cond = args[2].clone();
709
710    expand_exists_impl(ctx, span, receiver, iter_var, iter_var2, cond, &args)
711}
712
713fn expand_exists_impl(
714    ctx: &mut MacroContext,
715    span: Span,
716    receiver: SpannedExpr,
717    iter_var: String,
718    iter_var2: String,
719    cond: SpannedExpr,
720    args: &[SpannedExpr],
721) -> MacroExpansion {
722    let call_id = ctx.next_id();
723    ctx.store_macro_call(call_id, &span, &receiver, "exists", args);
724
725    let accu_var = ACCU_VAR.to_string();
726    let accu_init = synthetic(ctx, Expr::Bool(false), span.clone());
727
728    let accu_ref_cond = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
729    let loop_condition = synthetic(
730        ctx,
731        Expr::Unary {
732            op: UnaryOp::Not,
733            expr: Box::new(accu_ref_cond),
734        },
735        span.clone(),
736    );
737
738    let accu_ref_step = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
739    let loop_step = synthetic(
740        ctx,
741        Expr::Binary {
742            op: BinaryOp::Or,
743            left: Box::new(cond),
744            right: Box::new(accu_ref_step),
745        },
746        span.clone(),
747    );
748
749    let result = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
750
751    MacroExpansion::Expanded(Spanned::new(
752        call_id,
753        Expr::Comprehension(ComprehensionData {
754            iter_var,
755            iter_var2,
756            iter_range: Box::new(receiver),
757            accu_var,
758            accu_init: Box::new(accu_init),
759            loop_condition: Box::new(loop_condition),
760            loop_step: Box::new(loop_step),
761            result: Box::new(result),
762        }),
763        span,
764    ))
765}
766
767// === exists_one() Macro ===
768
769fn expand_exists_one_2arg(
770    ctx: &mut MacroContext,
771    span: Span,
772    receiver: Option<SpannedExpr>,
773    args: Vec<SpannedExpr>,
774) -> MacroExpansion {
775    let receiver = match receiver {
776        Some(r) => r,
777        None => return MacroExpansion::Error("exists_one() requires a receiver".to_string()),
778    };
779
780    let iter_var = match extract_iter_var(ctx, &args[0]) {
781        Some(v) => v,
782        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
783    };
784    let cond = args[1].clone();
785
786    expand_exists_one_impl(ctx, span, receiver, iter_var, String::new(), cond, &args)
787}
788
789fn expand_exists_one_3arg(
790    ctx: &mut MacroContext,
791    span: Span,
792    receiver: Option<SpannedExpr>,
793    args: Vec<SpannedExpr>,
794) -> MacroExpansion {
795    let receiver = match receiver {
796        Some(r) => r,
797        None => return MacroExpansion::Error("exists_one() requires a receiver".to_string()),
798    };
799
800    let iter_var = match extract_iter_var(ctx, &args[0]) {
801        Some(v) => v,
802        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
803    };
804    let iter_var2 = match extract_iter_var(ctx, &args[1]) {
805        Some(v) => v,
806        None => return MacroExpansion::Error("second argument must be an identifier".to_string()),
807    };
808    let cond = args[2].clone();
809
810    expand_exists_one_impl(ctx, span, receiver, iter_var, iter_var2, cond, &args)
811}
812
813fn expand_exists_one_impl(
814    ctx: &mut MacroContext,
815    span: Span,
816    receiver: SpannedExpr,
817    iter_var: String,
818    iter_var2: String,
819    cond: SpannedExpr,
820    args: &[SpannedExpr],
821) -> MacroExpansion {
822    let call_id = ctx.next_id();
823    ctx.store_macro_call(call_id, &span, &receiver, "exists_one", args);
824
825    let accu_var = ACCU_VAR.to_string();
826    let accu_init = synthetic(ctx, Expr::Int(0), span.clone());
827
828    let loop_condition = synthetic(ctx, Expr::Bool(true), span.clone());
829
830    let accu_ref_then = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
831    let one_step = synthetic(ctx, Expr::Int(1), span.clone());
832    let increment = synthetic(
833        ctx,
834        Expr::Binary {
835            op: BinaryOp::Add,
836            left: Box::new(accu_ref_then),
837            right: Box::new(one_step),
838        },
839        span.clone(),
840    );
841    let accu_ref_else = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
842    let loop_step = synthetic(
843        ctx,
844        Expr::Ternary {
845            cond: Box::new(cond),
846            then_expr: Box::new(increment),
847            else_expr: Box::new(accu_ref_else),
848        },
849        span.clone(),
850    );
851
852    let accu_ref_result = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
853    let one_result = synthetic(ctx, Expr::Int(1), span.clone());
854    let result = synthetic(
855        ctx,
856        Expr::Binary {
857            op: BinaryOp::Eq,
858            left: Box::new(accu_ref_result),
859            right: Box::new(one_result),
860        },
861        span.clone(),
862    );
863
864    MacroExpansion::Expanded(Spanned::new(
865        call_id,
866        Expr::Comprehension(ComprehensionData {
867            iter_var,
868            iter_var2,
869            iter_range: Box::new(receiver),
870            accu_var,
871            accu_init: Box::new(accu_init),
872            loop_condition: Box::new(loop_condition),
873            loop_step: Box::new(loop_step),
874            result: Box::new(result),
875        }),
876        span,
877    ))
878}
879
880// === map() Macro ===
881
882fn expand_map_2arg(
883    ctx: &mut MacroContext,
884    span: Span,
885    receiver: Option<SpannedExpr>,
886    args: Vec<SpannedExpr>,
887) -> MacroExpansion {
888    let receiver = match receiver {
889        Some(r) => r,
890        None => return MacroExpansion::Error("map() requires a receiver".to_string()),
891    };
892
893    let iter_var = match extract_iter_var(ctx, &args[0]) {
894        Some(v) => v,
895        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
896    };
897    let transform = args[1].clone();
898
899    expand_map_impl(ctx, span, receiver, iter_var, None, transform, &args)
900}
901
902fn expand_map_3arg(
903    ctx: &mut MacroContext,
904    span: Span,
905    receiver: Option<SpannedExpr>,
906    args: Vec<SpannedExpr>,
907) -> MacroExpansion {
908    let receiver = match receiver {
909        Some(r) => r,
910        None => return MacroExpansion::Error("map() requires a receiver".to_string()),
911    };
912
913    let iter_var = match extract_iter_var(ctx, &args[0]) {
914        Some(v) => v,
915        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
916    };
917    let filter = args[1].clone();
918    let transform = args[2].clone();
919
920    expand_map_impl(
921        ctx,
922        span,
923        receiver,
924        iter_var,
925        Some(filter),
926        transform,
927        &args,
928    )
929}
930
931fn expand_map_impl(
932    ctx: &mut MacroContext,
933    span: Span,
934    receiver: SpannedExpr,
935    iter_var: String,
936    filter_cond: Option<SpannedExpr>,
937    transform: SpannedExpr,
938    args: &[SpannedExpr],
939) -> MacroExpansion {
940    let call_id = ctx.next_id();
941    ctx.store_macro_call(call_id, &span, &receiver, "map", args);
942
943    let accu_var = ACCU_VAR.to_string();
944    let accu_init = synthetic(ctx, Expr::List(vec![]), span.clone());
945    let loop_condition = synthetic(ctx, Expr::Bool(true), span.clone());
946
947    let transformed_list = synthetic(
948        ctx,
949        Expr::List(vec![ListElement {
950            expr: transform,
951            optional: false,
952        }]),
953        span.clone(),
954    );
955    let accu_ref_step = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
956    let append_step = synthetic(
957        ctx,
958        Expr::Binary {
959            op: BinaryOp::Add,
960            left: Box::new(accu_ref_step),
961            right: Box::new(transformed_list),
962        },
963        span.clone(),
964    );
965
966    let loop_step = if let Some(filter) = filter_cond {
967        let accu_ref_else = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
968        synthetic(
969            ctx,
970            Expr::Ternary {
971                cond: Box::new(filter),
972                then_expr: Box::new(append_step),
973                else_expr: Box::new(accu_ref_else),
974            },
975            span.clone(),
976        )
977    } else {
978        append_step
979    };
980
981    let result = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
982
983    MacroExpansion::Expanded(Spanned::new(
984        call_id,
985        Expr::Comprehension(ComprehensionData {
986            iter_var,
987            iter_var2: String::new(),
988            iter_range: Box::new(receiver),
989            accu_var,
990            accu_init: Box::new(accu_init),
991            loop_condition: Box::new(loop_condition),
992            loop_step: Box::new(loop_step),
993            result: Box::new(result),
994        }),
995        span,
996    ))
997}
998
999// === filter() Macro ===
1000
1001fn expand_filter(
1002    ctx: &mut MacroContext,
1003    span: Span,
1004    receiver: Option<SpannedExpr>,
1005    args: Vec<SpannedExpr>,
1006) -> MacroExpansion {
1007    let receiver = match receiver {
1008        Some(r) => r,
1009        None => return MacroExpansion::Error("filter() requires a receiver".to_string()),
1010    };
1011
1012    let iter_var = match extract_iter_var(ctx, &args[0]) {
1013        Some(v) => v,
1014        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
1015    };
1016    let cond = args[1].clone();
1017
1018    let call_id = ctx.next_id();
1019    ctx.store_macro_call(call_id, &span, &receiver, "filter", &args);
1020
1021    let accu_var = ACCU_VAR.to_string();
1022    let accu_init = synthetic(ctx, Expr::List(vec![]), span.clone());
1023    let loop_condition = synthetic(ctx, Expr::Bool(true), span.clone());
1024
1025    let iter_ref = synthetic(ctx, Expr::Ident(iter_var.clone()), span.clone());
1026    let element_list = synthetic(
1027        ctx,
1028        Expr::List(vec![ListElement {
1029            expr: iter_ref,
1030            optional: false,
1031        }]),
1032        span.clone(),
1033    );
1034
1035    let accu_ref_then = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
1036    let append_step = synthetic(
1037        ctx,
1038        Expr::Binary {
1039            op: BinaryOp::Add,
1040            left: Box::new(accu_ref_then),
1041            right: Box::new(element_list),
1042        },
1043        span.clone(),
1044    );
1045
1046    let accu_ref_else = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
1047    let loop_step = synthetic(
1048        ctx,
1049        Expr::Ternary {
1050            cond: Box::new(cond),
1051            then_expr: Box::new(append_step),
1052            else_expr: Box::new(accu_ref_else),
1053        },
1054        span.clone(),
1055    );
1056
1057    let result = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
1058
1059    MacroExpansion::Expanded(Spanned::new(
1060        call_id,
1061        Expr::Comprehension(ComprehensionData {
1062            iter_var,
1063            iter_var2: String::new(),
1064            iter_range: Box::new(receiver),
1065            accu_var,
1066            accu_init: Box::new(accu_init),
1067            loop_condition: Box::new(loop_condition),
1068            loop_step: Box::new(loop_step),
1069            result: Box::new(result),
1070        }),
1071        span,
1072    ))
1073}
1074
1075struct TransformParams {
1076    iter_var: String,
1077    iter_var2: String,
1078    filter_cond: Option<SpannedExpr>,
1079    transform: SpannedExpr,
1080}
1081
1082// === transformList() Macro ===
1083
1084fn expand_transform_list_3arg(
1085    ctx: &mut MacroContext,
1086    span: Span,
1087    receiver: Option<SpannedExpr>,
1088    args: Vec<SpannedExpr>,
1089) -> MacroExpansion {
1090    let receiver = match receiver {
1091        Some(r) => r,
1092        None => return MacroExpansion::Error("transformList() requires a receiver".to_string()),
1093    };
1094
1095    let iter_var = match extract_iter_var(ctx, &args[0]) {
1096        Some(v) => v,
1097        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
1098    };
1099    let iter_var2 = match extract_iter_var(ctx, &args[1]) {
1100        Some(v) => v,
1101        None => return MacroExpansion::Error("second argument must be an identifier".to_string()),
1102    };
1103    let transform = args[2].clone();
1104
1105    expand_transform_list_impl(
1106        ctx,
1107        span,
1108        receiver,
1109        TransformParams {
1110            iter_var,
1111            iter_var2,
1112            filter_cond: None,
1113            transform,
1114        },
1115        &args,
1116    )
1117}
1118
1119fn expand_transform_list_4arg(
1120    ctx: &mut MacroContext,
1121    span: Span,
1122    receiver: Option<SpannedExpr>,
1123    args: Vec<SpannedExpr>,
1124) -> MacroExpansion {
1125    let receiver = match receiver {
1126        Some(r) => r,
1127        None => return MacroExpansion::Error("transformList() requires a receiver".to_string()),
1128    };
1129
1130    let iter_var = match extract_iter_var(ctx, &args[0]) {
1131        Some(v) => v,
1132        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
1133    };
1134    let iter_var2 = match extract_iter_var(ctx, &args[1]) {
1135        Some(v) => v,
1136        None => return MacroExpansion::Error("second argument must be an identifier".to_string()),
1137    };
1138    let filter = args[2].clone();
1139    let transform = args[3].clone();
1140
1141    expand_transform_list_impl(
1142        ctx,
1143        span,
1144        receiver,
1145        TransformParams {
1146            iter_var,
1147            iter_var2,
1148            filter_cond: Some(filter),
1149            transform,
1150        },
1151        &args,
1152    )
1153}
1154
1155fn expand_transform_list_impl(
1156    ctx: &mut MacroContext,
1157    span: Span,
1158    receiver: SpannedExpr,
1159    params: TransformParams,
1160    args: &[SpannedExpr],
1161) -> MacroExpansion {
1162    let call_id = ctx.next_id();
1163    ctx.store_macro_call(call_id, &span, &receiver, "transformList", args);
1164
1165    let accu_var = ACCU_VAR.to_string();
1166    let accu_init = synthetic(ctx, Expr::List(vec![]), span.clone());
1167    let loop_condition = synthetic(ctx, Expr::Bool(true), span.clone());
1168
1169    let transformed_list = synthetic(
1170        ctx,
1171        Expr::List(vec![ListElement {
1172            expr: params.transform,
1173            optional: false,
1174        }]),
1175        span.clone(),
1176    );
1177    let accu_ref_step = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
1178    let append_step = synthetic(
1179        ctx,
1180        Expr::Binary {
1181            op: BinaryOp::Add,
1182            left: Box::new(accu_ref_step),
1183            right: Box::new(transformed_list),
1184        },
1185        span.clone(),
1186    );
1187
1188    let loop_step = if let Some(filter) = params.filter_cond {
1189        let accu_ref_else = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
1190        synthetic(
1191            ctx,
1192            Expr::Ternary {
1193                cond: Box::new(filter),
1194                then_expr: Box::new(append_step),
1195                else_expr: Box::new(accu_ref_else),
1196            },
1197            span.clone(),
1198        )
1199    } else {
1200        append_step
1201    };
1202
1203    let result = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
1204
1205    MacroExpansion::Expanded(Spanned::new(
1206        call_id,
1207        Expr::Comprehension(ComprehensionData {
1208            iter_var: params.iter_var,
1209            iter_var2: params.iter_var2,
1210            iter_range: Box::new(receiver),
1211            accu_var,
1212            accu_init: Box::new(accu_init),
1213            loop_condition: Box::new(loop_condition),
1214            loop_step: Box::new(loop_step),
1215            result: Box::new(result),
1216        }),
1217        span,
1218    ))
1219}
1220
1221// === transformMap() Macro ===
1222
1223fn expand_transform_map_3arg(
1224    ctx: &mut MacroContext,
1225    span: Span,
1226    receiver: Option<SpannedExpr>,
1227    args: Vec<SpannedExpr>,
1228) -> MacroExpansion {
1229    let receiver = match receiver {
1230        Some(r) => r,
1231        None => return MacroExpansion::Error("transformMap() requires a receiver".to_string()),
1232    };
1233
1234    let iter_var = match extract_iter_var(ctx, &args[0]) {
1235        Some(v) => v,
1236        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
1237    };
1238    let iter_var2 = match extract_iter_var(ctx, &args[1]) {
1239        Some(v) => v,
1240        None => return MacroExpansion::Error("second argument must be an identifier".to_string()),
1241    };
1242    let transform = args[2].clone();
1243
1244    expand_transform_map_impl(
1245        ctx,
1246        span,
1247        receiver,
1248        TransformParams {
1249            iter_var,
1250            iter_var2,
1251            filter_cond: None,
1252            transform,
1253        },
1254        &args,
1255    )
1256}
1257
1258fn expand_transform_map_4arg(
1259    ctx: &mut MacroContext,
1260    span: Span,
1261    receiver: Option<SpannedExpr>,
1262    args: Vec<SpannedExpr>,
1263) -> MacroExpansion {
1264    let receiver = match receiver {
1265        Some(r) => r,
1266        None => return MacroExpansion::Error("transformMap() requires a receiver".to_string()),
1267    };
1268
1269    let iter_var = match extract_iter_var(ctx, &args[0]) {
1270        Some(v) => v,
1271        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
1272    };
1273    let iter_var2 = match extract_iter_var(ctx, &args[1]) {
1274        Some(v) => v,
1275        None => return MacroExpansion::Error("second argument must be an identifier".to_string()),
1276    };
1277    let filter = args[2].clone();
1278    let transform = args[3].clone();
1279
1280    expand_transform_map_impl(
1281        ctx,
1282        span,
1283        receiver,
1284        TransformParams {
1285            iter_var,
1286            iter_var2,
1287            filter_cond: Some(filter),
1288            transform,
1289        },
1290        &args,
1291    )
1292}
1293
1294fn expand_transform_map_impl(
1295    ctx: &mut MacroContext,
1296    span: Span,
1297    receiver: SpannedExpr,
1298    params: TransformParams,
1299    args: &[SpannedExpr],
1300) -> MacroExpansion {
1301    let call_id = ctx.next_id();
1302    ctx.store_macro_call(call_id, &span, &receiver, "transformMap", args);
1303
1304    let accu_var = ACCU_VAR.to_string();
1305    let accu_init = synthetic(ctx, Expr::Map(vec![]), span.clone());
1306    let loop_condition = synthetic(ctx, Expr::Bool(true), span.clone());
1307
1308    let key_ref = synthetic(ctx, Expr::Ident(params.iter_var.clone()), span.clone());
1309    let transformed_map = synthetic(
1310        ctx,
1311        Expr::Map(vec![MapEntry {
1312            key: key_ref,
1313            value: params.transform,
1314            optional: false,
1315        }]),
1316        span.clone(),
1317    );
1318    let accu_ref_step = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
1319    let append_step = synthetic(
1320        ctx,
1321        Expr::Binary {
1322            op: BinaryOp::Add,
1323            left: Box::new(accu_ref_step),
1324            right: Box::new(transformed_map),
1325        },
1326        span.clone(),
1327    );
1328
1329    let loop_step = if let Some(filter) = params.filter_cond {
1330        let accu_ref_else = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
1331        synthetic(
1332            ctx,
1333            Expr::Ternary {
1334                cond: Box::new(filter),
1335                then_expr: Box::new(append_step),
1336                else_expr: Box::new(accu_ref_else),
1337            },
1338            span.clone(),
1339        )
1340    } else {
1341        append_step
1342    };
1343
1344    let result = synthetic(ctx, Expr::Ident(accu_var.clone()), span.clone());
1345
1346    MacroExpansion::Expanded(Spanned::new(
1347        call_id,
1348        Expr::Comprehension(ComprehensionData {
1349            iter_var: params.iter_var,
1350            iter_var2: params.iter_var2,
1351            iter_range: Box::new(receiver),
1352            accu_var,
1353            accu_init: Box::new(accu_init),
1354            loop_condition: Box::new(loop_condition),
1355            loop_step: Box::new(loop_step),
1356            result: Box::new(result),
1357        }),
1358        span,
1359    ))
1360}
1361
1362// === cel.bind() Macro ===
1363
1364/// Expand `cel.bind(var, init, body)` to `Expr::Bind`.
1365///
1366/// This macro binds a variable to a value for use within a scoped expression.
1367/// The variable is only visible within the body expression.
1368///
1369/// # Example
1370/// `cel.bind(msg, "hello", msg + msg)` evaluates to `"hellohello"`
1371fn expand_bind(
1372    ctx: &mut MacroContext,
1373    span: Span,
1374    _receiver: Option<SpannedExpr>,
1375    args: Vec<SpannedExpr>,
1376) -> MacroExpansion {
1377    if args.len() != 3 {
1378        return MacroExpansion::Error(format!(
1379            "cel.bind() requires 3 arguments, got {}",
1380            args.len()
1381        ));
1382    }
1383
1384    // First argument must be an identifier (the variable name)
1385    let var_name = match &args[0].node {
1386        Expr::Ident(name) => name.clone(),
1387        _ => {
1388            return MacroExpansion::Error(
1389                "cel.bind() first argument must be an identifier".to_string(),
1390            )
1391        }
1392    };
1393
1394    // Second argument is the initializer expression
1395    let init = args[1].clone();
1396
1397    // Third argument is the body expression
1398    let body = args[2].clone();
1399
1400    let call_id = ctx.next_id();
1401    ctx.store_macro_call(call_id, &span, &args[0], "cel.bind", &args);
1402
1403    MacroExpansion::Expanded(Spanned::new(
1404        call_id,
1405        Expr::Bind {
1406            var_name,
1407            init: Box::new(init),
1408            body: Box::new(body),
1409        },
1410        span,
1411    ))
1412}
1413
1414// === optMap() and optFlatMap() Helper Functions ===
1415
1416/// Build a method call: receiver.method(args)
1417fn build_method_call(
1418    ctx: &mut MacroContext,
1419    span: &Span,
1420    receiver: SpannedExpr,
1421    method: &str,
1422    args: Vec<SpannedExpr>,
1423) -> SpannedExpr {
1424    let member = synthetic(
1425        ctx,
1426        Expr::Member {
1427            expr: Box::new(receiver),
1428            field: method.to_string(),
1429            optional: false,
1430        },
1431        span.clone(),
1432    );
1433
1434    synthetic(
1435        ctx,
1436        Expr::Call {
1437            expr: Box::new(member),
1438            args,
1439        },
1440        span.clone(),
1441    )
1442}
1443
1444/// Build: optional.of(expr)
1445fn build_optional_of(ctx: &mut MacroContext, span: &Span, expr: SpannedExpr) -> SpannedExpr {
1446    let optional_ident = synthetic(ctx, Expr::Ident("optional".to_string()), span.clone());
1447    let optional_of_member = synthetic(
1448        ctx,
1449        Expr::Member {
1450            expr: Box::new(optional_ident),
1451            field: "of".to_string(),
1452            optional: false,
1453        },
1454        span.clone(),
1455    );
1456
1457    synthetic(
1458        ctx,
1459        Expr::Call {
1460            expr: Box::new(optional_of_member),
1461            args: vec![expr],
1462        },
1463        span.clone(),
1464    )
1465}
1466
1467/// Build: optional.none()
1468fn build_optional_none(ctx: &mut MacroContext, span: &Span) -> SpannedExpr {
1469    let optional_ident = synthetic(ctx, Expr::Ident("optional".to_string()), span.clone());
1470    let optional_none_member = synthetic(
1471        ctx,
1472        Expr::Member {
1473            expr: Box::new(optional_ident),
1474            field: "none".to_string(),
1475            optional: false,
1476        },
1477        span.clone(),
1478    );
1479
1480    synthetic(
1481        ctx,
1482        Expr::Call {
1483            expr: Box::new(optional_none_member),
1484            args: vec![],
1485        },
1486        span.clone(),
1487    )
1488}
1489
1490// === optMap() Macro ===
1491
1492/// Expand `optional.optMap(var, expr)` to:
1493/// ```text
1494/// receiver.hasValue()
1495///     ? cel.bind(var, receiver.value(), optional.of(expr))
1496///     : optional.none()
1497/// ```
1498fn expand_opt_map(
1499    ctx: &mut MacroContext,
1500    span: Span,
1501    receiver: Option<SpannedExpr>,
1502    args: Vec<SpannedExpr>,
1503) -> MacroExpansion {
1504    let receiver = match receiver {
1505        Some(r) => r,
1506        None => return MacroExpansion::Error("optMap() requires a receiver".to_string()),
1507    };
1508
1509    let iter_var = match extract_iter_var(ctx, &args[0]) {
1510        Some(v) => v,
1511        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
1512    };
1513    let transform = args[1].clone();
1514
1515    let call_id = ctx.next_id();
1516    ctx.store_macro_call(call_id, &span, &receiver, "optMap", &args);
1517
1518    // Build: receiver.hasValue()
1519    let has_value_call = build_method_call(ctx, &span, receiver.clone(), "hasValue", vec![]);
1520
1521    // Build: receiver.value()
1522    let value_call = build_method_call(ctx, &span, receiver, "value", vec![]);
1523
1524    // Build: optional.of(transform)
1525    let wrapped_result = build_optional_of(ctx, &span, transform);
1526
1527    // Build: cel.bind(var, receiver.value(), optional.of(transform))
1528    let bind_expr = synthetic(
1529        ctx,
1530        Expr::Bind {
1531            var_name: iter_var,
1532            init: Box::new(value_call),
1533            body: Box::new(wrapped_result),
1534        },
1535        span.clone(),
1536    );
1537
1538    // Build: optional.none()
1539    let none_call = build_optional_none(ctx, &span);
1540
1541    // Build ternary: hasValue ? bind : none
1542    MacroExpansion::Expanded(Spanned::new(
1543        call_id,
1544        Expr::Ternary {
1545            cond: Box::new(has_value_call),
1546            then_expr: Box::new(bind_expr),
1547            else_expr: Box::new(none_call),
1548        },
1549        span,
1550    ))
1551}
1552
1553// === optFlatMap() Macro ===
1554
1555/// Expand `optional.optFlatMap(var, expr)` to:
1556/// ```text
1557/// receiver.hasValue()
1558///     ? cel.bind(var, receiver.value(), expr)
1559///     : optional.none()
1560/// ```
1561///
1562/// Note: Unlike optMap, this does NOT wrap the result in optional.of()
1563/// since expr is expected to already return an optional.
1564fn expand_opt_flat_map(
1565    ctx: &mut MacroContext,
1566    span: Span,
1567    receiver: Option<SpannedExpr>,
1568    args: Vec<SpannedExpr>,
1569) -> MacroExpansion {
1570    let receiver = match receiver {
1571        Some(r) => r,
1572        None => return MacroExpansion::Error("optFlatMap() requires a receiver".to_string()),
1573    };
1574
1575    let iter_var = match extract_iter_var(ctx, &args[0]) {
1576        Some(v) => v,
1577        None => return MacroExpansion::Error("first argument must be an identifier".to_string()),
1578    };
1579    let transform = args[1].clone();
1580
1581    let call_id = ctx.next_id();
1582    ctx.store_macro_call(call_id, &span, &receiver, "optFlatMap", &args);
1583
1584    // Build: receiver.hasValue()
1585    let has_value_call = build_method_call(ctx, &span, receiver.clone(), "hasValue", vec![]);
1586
1587    // Build: receiver.value()
1588    let value_call = build_method_call(ctx, &span, receiver, "value", vec![]);
1589
1590    // Build: cel.bind(var, receiver.value(), transform)
1591    // Note: transform is NOT wrapped in optional.of()
1592    let bind_expr = synthetic(
1593        ctx,
1594        Expr::Bind {
1595            var_name: iter_var,
1596            init: Box::new(value_call),
1597            body: Box::new(transform),
1598        },
1599        span.clone(),
1600    );
1601
1602    // Build: optional.none()
1603    let none_call = build_optional_none(ctx, &span);
1604
1605    // Build ternary: hasValue ? bind : none
1606    MacroExpansion::Expanded(Spanned::new(
1607        call_id,
1608        Expr::Ternary {
1609            cond: Box::new(has_value_call),
1610            then_expr: Box::new(bind_expr),
1611            else_expr: Box::new(none_call),
1612        },
1613        span,
1614    ))
1615}
1616
1617// === proto.hasExt() and proto.getExt() Macros ===
1618
1619/// Recursively walk a select expression chain to produce a fully-qualified name.
1620///
1621/// - `Expr::Ident("a")` → `"a"`
1622/// - `Expr::Member { expr: Ident("a"), field: "b" }` → `"a.b"`
1623/// - Nested members: `a.b.c.d` → `"a.b.c.d"`
1624///
1625/// Returns `None` for anything other than identifiers and member accesses.
1626fn validate_qualified_identifier(expr: &SpannedExpr) -> Option<String> {
1627    match &expr.node {
1628        Expr::Ident(name) => Some(name.clone()),
1629        Expr::Member { expr, field, .. } => {
1630            let prefix = validate_qualified_identifier(expr)?;
1631            Some(format!("{}.{}", prefix, field))
1632        }
1633        _ => None,
1634    }
1635}
1636
1637/// Expand `proto.getExt(msg, pkg.ExtField)` to `msg.pkg.ExtField` (a select expression).
1638fn expand_proto_get_ext(
1639    ctx: &mut MacroContext,
1640    span: Span,
1641    _receiver: Option<SpannedExpr>,
1642    args: Vec<SpannedExpr>,
1643) -> MacroExpansion {
1644    if args.len() != 2 {
1645        return MacroExpansion::Error(format!(
1646            "proto.getExt() requires 2 arguments, got {}",
1647            args.len()
1648        ));
1649    }
1650
1651    let ext_name = match validate_qualified_identifier(&args[1]) {
1652        Some(name) => name,
1653        None => return MacroExpansion::Error(
1654            "proto.getExt() second argument must be a qualified identifier (e.g., pkg.ExtField)"
1655                .to_string(),
1656        ),
1657    };
1658
1659    let msg = args[0].clone();
1660    let call_id = ctx.next_id();
1661    ctx.store_macro_call(call_id, &span, &msg, "proto.getExt", &args);
1662
1663    MacroExpansion::Expanded(Spanned::new(
1664        call_id,
1665        Expr::Member {
1666            expr: Box::new(msg),
1667            field: ext_name,
1668            optional: false,
1669        },
1670        span,
1671    ))
1672}
1673
1674/// Expand `proto.hasExt(msg, pkg.ExtField)` to `has(msg.pkg.ExtField)` (a presence test).
1675fn expand_proto_has_ext(
1676    ctx: &mut MacroContext,
1677    span: Span,
1678    _receiver: Option<SpannedExpr>,
1679    args: Vec<SpannedExpr>,
1680) -> MacroExpansion {
1681    if args.len() != 2 {
1682        return MacroExpansion::Error(format!(
1683            "proto.hasExt() requires 2 arguments, got {}",
1684            args.len()
1685        ));
1686    }
1687
1688    let ext_name = match validate_qualified_identifier(&args[1]) {
1689        Some(name) => name,
1690        None => return MacroExpansion::Error(
1691            "proto.hasExt() second argument must be a qualified identifier (e.g., pkg.ExtField)"
1692                .to_string(),
1693        ),
1694    };
1695
1696    let msg = args[0].clone();
1697    let call_id = ctx.next_id();
1698    ctx.store_macro_call(call_id, &span, &msg, "proto.hasExt", &args);
1699
1700    MacroExpansion::Expanded(Spanned::new(
1701        call_id,
1702        Expr::MemberTestOnly {
1703            expr: Box::new(msg),
1704            field: ext_name,
1705        },
1706        span,
1707    ))
1708}
1709
1710// === cel.block Extension Macros ===
1711//
1712// cel.block provides slot-based variable binding for common subexpression elimination.
1713// We expand `cel.block([e0, e1, e2], result)` into nested `Expr::Bind` nodes:
1714//
1715//   Bind { var: "@index0", init: e0,
1716//     body: Bind { var: "@index1", init: e1,
1717//       body: Bind { var: "@index2", init: e2,
1718//         body: result } } }
1719//
1720// This reuses existing Bind infrastructure (parser, checker, evaluator) with no new AST nodes.
1721//
1722// Future optimization path (equivalent to cel-go's dynamicBlock):
1723// - Add Expr::Block with flat Vec<(String, SpannedExpr)> + result expression
1724// - Add BlockActivation with Vec<Option<Value>> for O(1) lookup + lazy evaluation
1725// - Current approach eagerly evaluates all slots and has O(N) lookup for inner slots
1726
1727/// Expand `cel.block([e0, e1, ...], result)` into nested `Expr::Bind` nodes.
1728fn expand_cel_block(
1729    ctx: &mut MacroContext,
1730    span: Span,
1731    _receiver: Option<SpannedExpr>,
1732    args: Vec<SpannedExpr>,
1733) -> MacroExpansion {
1734    if args.len() != 2 {
1735        return MacroExpansion::Error(format!(
1736            "cel.block() requires 2 arguments, got {}",
1737            args.len()
1738        ));
1739    }
1740
1741    let slots = match &args[0].node {
1742        Expr::List(elements) => {
1743            if elements.is_empty() {
1744                return MacroExpansion::Error(
1745                    "cel.block() first argument must be a non-empty list".to_string(),
1746                );
1747            }
1748            elements.clone()
1749        }
1750        _ => {
1751            return MacroExpansion::Error(
1752                "cel.block() first argument must be a list literal".to_string(),
1753            )
1754        }
1755    };
1756
1757    let result = args[1].clone();
1758    let call_id = ctx.next_id();
1759    ctx.store_macro_call(call_id, &span, &args[0], "cel.block", &args);
1760
1761    // Build nested Bind from innermost to outermost
1762    let mut body = result;
1763    for (i, slot) in slots.into_iter().enumerate().rev() {
1764        let var_name = format!("@index{}", i);
1765        body = synthetic(
1766            ctx,
1767            Expr::Bind {
1768                var_name,
1769                init: Box::new(slot.expr),
1770                body: Box::new(body),
1771            },
1772            span.clone(),
1773        );
1774    }
1775
1776    // Replace the outermost node's ID with the call_id for macro tracking
1777    body.id = call_id;
1778    MacroExpansion::Expanded(body)
1779}
1780
1781/// Extract an integer literal value from an expression.
1782fn extract_int_literal(expr: &SpannedExpr) -> Option<i64> {
1783    match &expr.node {
1784        Expr::Int(n) => Some(*n),
1785        _ => None,
1786    }
1787}
1788
1789/// Expand `cel.index(N)` to `Expr::Ident("@indexN")`.
1790fn expand_cel_index(
1791    ctx: &mut MacroContext,
1792    span: Span,
1793    _receiver: Option<SpannedExpr>,
1794    args: Vec<SpannedExpr>,
1795) -> MacroExpansion {
1796    if args.len() != 1 {
1797        return MacroExpansion::Error(format!(
1798            "cel.index() requires 1 argument, got {}",
1799            args.len()
1800        ));
1801    }
1802
1803    let index = match extract_int_literal(&args[0]) {
1804        Some(n) => n,
1805        None => {
1806            return MacroExpansion::Error(
1807                "cel.index() argument must be an integer literal".to_string(),
1808            )
1809        }
1810    };
1811
1812    let call_id = ctx.next_id();
1813    ctx.store_macro_call(call_id, &span, &args[0], "cel.index", &args);
1814
1815    MacroExpansion::Expanded(Spanned::new(
1816        call_id,
1817        Expr::Ident(format!("@index{}", index)),
1818        span,
1819    ))
1820}
1821
1822/// Expand `cel.iterVar(N, M)` to `Expr::Ident("@it:N:M")`.
1823fn expand_cel_iter_var(
1824    ctx: &mut MacroContext,
1825    span: Span,
1826    _receiver: Option<SpannedExpr>,
1827    args: Vec<SpannedExpr>,
1828) -> MacroExpansion {
1829    if args.len() != 2 {
1830        return MacroExpansion::Error(format!(
1831            "cel.iterVar() requires 2 arguments, got {}",
1832            args.len()
1833        ));
1834    }
1835
1836    let n = match extract_int_literal(&args[0]) {
1837        Some(n) => n,
1838        None => {
1839            return MacroExpansion::Error(
1840                "cel.iterVar() first argument must be an integer literal".to_string(),
1841            )
1842        }
1843    };
1844    let m = match extract_int_literal(&args[1]) {
1845        Some(m) => m,
1846        None => {
1847            return MacroExpansion::Error(
1848                "cel.iterVar() second argument must be an integer literal".to_string(),
1849            )
1850        }
1851    };
1852
1853    let call_id = ctx.next_id();
1854    ctx.store_macro_call(call_id, &span, &args[0], "cel.iterVar", &args);
1855
1856    MacroExpansion::Expanded(Spanned::new(
1857        call_id,
1858        Expr::Ident(format!("@it:{}:{}", n, m)),
1859        span,
1860    ))
1861}
1862
1863/// Expand `cel.accuVar(N, M)` to `Expr::Ident("@ac:N:M")`.
1864fn expand_cel_accu_var(
1865    ctx: &mut MacroContext,
1866    span: Span,
1867    _receiver: Option<SpannedExpr>,
1868    args: Vec<SpannedExpr>,
1869) -> MacroExpansion {
1870    if args.len() != 2 {
1871        return MacroExpansion::Error(format!(
1872            "cel.accuVar() requires 2 arguments, got {}",
1873            args.len()
1874        ));
1875    }
1876
1877    let n = match extract_int_literal(&args[0]) {
1878        Some(n) => n,
1879        None => {
1880            return MacroExpansion::Error(
1881                "cel.accuVar() first argument must be an integer literal".to_string(),
1882            )
1883        }
1884    };
1885    let m = match extract_int_literal(&args[1]) {
1886        Some(m) => m,
1887        None => {
1888            return MacroExpansion::Error(
1889                "cel.accuVar() second argument must be an integer literal".to_string(),
1890            )
1891        }
1892    };
1893
1894    let call_id = ctx.next_id();
1895    ctx.store_macro_call(call_id, &span, &args[0], "cel.accuVar", &args);
1896
1897    MacroExpansion::Expanded(Spanned::new(
1898        call_id,
1899        Expr::Ident(format!("@ac:{}:{}", n, m)),
1900        span,
1901    ))
1902}
1903
1904#[cfg(test)]
1905mod tests {
1906    use super::*;
1907
1908    fn dummy_expander(
1909        _ctx: &mut MacroContext,
1910        _span: Span,
1911        _receiver: Option<SpannedExpr>,
1912        _args: Vec<SpannedExpr>,
1913    ) -> MacroExpansion {
1914        MacroExpansion::Error("dummy".to_string())
1915    }
1916
1917    #[test]
1918    fn test_arg_count_exact() {
1919        let exact = ArgCount::Exact(2);
1920        assert!(exact.matches(2));
1921        assert!(!exact.matches(1));
1922        assert!(!exact.matches(3));
1923        assert_eq!(exact.count(), 2);
1924        assert!(!exact.is_vararg());
1925    }
1926
1927    #[test]
1928    fn test_arg_count_vararg() {
1929        let vararg = ArgCount::VarArg(2);
1930        assert!(vararg.matches(2));
1931        assert!(vararg.matches(3));
1932        assert!(vararg.matches(10));
1933        assert!(!vararg.matches(1));
1934        assert_eq!(vararg.count(), 2);
1935        assert!(vararg.is_vararg());
1936    }
1937
1938    #[test]
1939    fn test_macro_key() {
1940        let m = Macro::new(
1941            "all",
1942            MacroStyle::Receiver,
1943            ArgCount::Exact(2),
1944            dummy_expander,
1945        );
1946        assert_eq!(m.key(), "all:2:true");
1947
1948        let m2 = Macro::new(
1949            "has",
1950            MacroStyle::Global,
1951            ArgCount::Exact(1),
1952            dummy_expander,
1953        );
1954        assert_eq!(m2.key(), "has:1:false");
1955    }
1956
1957    #[test]
1958    fn test_registry_lookup_exact() {
1959        let mut registry = MacroRegistry::new();
1960        registry.register(Macro::new(
1961            "all",
1962            MacroStyle::Receiver,
1963            ArgCount::Exact(2),
1964            dummy_expander,
1965        ));
1966        registry.register(Macro::new(
1967            "all",
1968            MacroStyle::Receiver,
1969            ArgCount::Exact(3),
1970            dummy_expander,
1971        ));
1972
1973        assert!(registry.lookup("all", 2, true).is_some());
1974        assert!(registry.lookup("all", 3, true).is_some());
1975        assert!(registry.lookup("all", 4, true).is_none());
1976        assert!(registry.lookup("all", 2, false).is_none());
1977    }
1978
1979    #[test]
1980    fn test_registry_lookup_vararg() {
1981        let mut registry = MacroRegistry::new();
1982        registry.register(Macro::new(
1983            "custom",
1984            MacroStyle::Receiver,
1985            ArgCount::VarArg(2),
1986            dummy_expander,
1987        ));
1988
1989        assert!(registry.lookup("custom", 2, true).is_some());
1990        assert!(registry.lookup("custom", 3, true).is_some());
1991        assert!(registry.lookup("custom", 10, true).is_some());
1992        assert!(registry.lookup("custom", 1, true).is_none());
1993    }
1994
1995    #[test]
1996    fn test_registry_standard() {
1997        let registry = MacroRegistry::standard();
1998
1999        assert!(registry.lookup("has", 1, false).is_some());
2000        assert!(registry.lookup("all", 2, true).is_some());
2001        assert!(registry.lookup("all", 3, true).is_some());
2002        assert!(registry.lookup("exists", 2, true).is_some());
2003        assert!(registry.lookup("exists", 3, true).is_some());
2004        assert!(registry.lookup("exists_one", 2, true).is_some());
2005        assert!(registry.lookup("exists_one", 3, true).is_some());
2006        assert!(registry.lookup("map", 2, true).is_some());
2007        assert!(registry.lookup("map", 3, true).is_some());
2008        assert!(registry.lookup("filter", 2, true).is_some());
2009    }
2010
2011    #[test]
2012    fn test_registry_contains() {
2013        let registry = MacroRegistry::standard();
2014        assert!(registry.contains("has"));
2015        assert!(registry.contains("all"));
2016        assert!(registry.contains("cel.bind"));
2017        assert!(!registry.contains("nonexistent"));
2018    }
2019
2020    #[test]
2021    fn test_cel_bind_macro_registered() {
2022        let registry = MacroRegistry::standard();
2023        assert!(registry.lookup("cel.bind", 3, false).is_some());
2024        // cel.bind is global, not a receiver macro
2025        assert!(registry.lookup("cel.bind", 3, true).is_none());
2026    }
2027
2028    #[test]
2029    fn test_opt_map_macro_registered() {
2030        let registry = MacroRegistry::standard();
2031        // optMap is a receiver macro with 2 args
2032        assert!(registry.lookup("optMap", 2, true).is_some());
2033        // Should not be found as global
2034        assert!(registry.lookup("optMap", 2, false).is_none());
2035        // Should not match wrong arg count
2036        assert!(registry.lookup("optMap", 1, true).is_none());
2037        assert!(registry.lookup("optMap", 3, true).is_none());
2038    }
2039
2040    #[test]
2041    fn test_opt_flat_map_macro_registered() {
2042        let registry = MacroRegistry::standard();
2043        // optFlatMap is a receiver macro with 2 args
2044        assert!(registry.lookup("optFlatMap", 2, true).is_some());
2045        // Should not be found as global
2046        assert!(registry.lookup("optFlatMap", 2, false).is_none());
2047        // Should not match wrong arg count
2048        assert!(registry.lookup("optFlatMap", 1, true).is_none());
2049        assert!(registry.lookup("optFlatMap", 3, true).is_none());
2050    }
2051
2052    #[test]
2053    fn test_registry_contains_opt_macros() {
2054        let registry = MacroRegistry::standard();
2055        assert!(registry.contains("optMap"));
2056        assert!(registry.contains("optFlatMap"));
2057    }
2058
2059    #[test]
2060    fn test_proto_has_ext_macro_registered() {
2061        let registry = MacroRegistry::standard();
2062        assert!(registry.lookup("proto.hasExt", 2, false).is_some());
2063        assert!(registry.lookup("proto.hasExt", 2, true).is_none());
2064        assert!(registry.lookup("proto.hasExt", 1, false).is_none());
2065    }
2066
2067    #[test]
2068    fn test_proto_get_ext_macro_registered() {
2069        let registry = MacroRegistry::standard();
2070        assert!(registry.lookup("proto.getExt", 2, false).is_some());
2071        assert!(registry.lookup("proto.getExt", 2, true).is_none());
2072        assert!(registry.lookup("proto.getExt", 1, false).is_none());
2073    }
2074
2075    #[test]
2076    fn test_validate_qualified_identifier() {
2077        // Simple ident
2078        let ident = Spanned::new(1, Expr::Ident("a".to_string()), 0..1);
2079        assert_eq!(validate_qualified_identifier(&ident), Some("a".to_string()));
2080
2081        // Dotted ident: a.b
2082        let dotted = Spanned::new(
2083            2,
2084            Expr::Member {
2085                expr: Box::new(Spanned::new(1, Expr::Ident("a".to_string()), 0..1)),
2086                field: "b".to_string(),
2087                optional: false,
2088            },
2089            0..3,
2090        );
2091        assert_eq!(
2092            validate_qualified_identifier(&dotted),
2093            Some("a.b".to_string())
2094        );
2095
2096        // Deeply nested: a.b.c.d
2097        let deep = Spanned::new(
2098            4,
2099            Expr::Member {
2100                expr: Box::new(Spanned::new(
2101                    3,
2102                    Expr::Member {
2103                        expr: Box::new(Spanned::new(
2104                            2,
2105                            Expr::Member {
2106                                expr: Box::new(Spanned::new(1, Expr::Ident("a".to_string()), 0..1)),
2107                                field: "b".to_string(),
2108                                optional: false,
2109                            },
2110                            0..3,
2111                        )),
2112                        field: "c".to_string(),
2113                        optional: false,
2114                    },
2115                    0..5,
2116                )),
2117                field: "d".to_string(),
2118                optional: false,
2119            },
2120            0..7,
2121        );
2122        assert_eq!(
2123            validate_qualified_identifier(&deep),
2124            Some("a.b.c.d".to_string())
2125        );
2126
2127        // Non-identifier returns None
2128        let non_ident = Spanned::new(1, Expr::Int(42), 0..2);
2129        assert_eq!(validate_qualified_identifier(&non_ident), None);
2130    }
2131
2132    #[test]
2133    fn test_proto_get_ext_expansion() {
2134        let mut id = 10i64;
2135        let mut next_id = || -> i64 {
2136            id += 1;
2137            id
2138        };
2139        let mut ctx = MacroContext::new(&mut next_id, None);
2140
2141        let msg = Spanned::new(1, Expr::Ident("msg".to_string()), 0..3);
2142        let ext = Spanned::new(
2143            2,
2144            Expr::Member {
2145                expr: Box::new(Spanned::new(1, Expr::Ident("pkg".to_string()), 4..7)),
2146                field: "ExtField".to_string(),
2147                optional: false,
2148            },
2149            4..16,
2150        );
2151
2152        let result = expand_proto_get_ext(&mut ctx, 0..20, None, vec![msg, ext]);
2153        match result {
2154            MacroExpansion::Expanded(expr) => match &expr.node {
2155                Expr::Member {
2156                    expr,
2157                    field,
2158                    optional,
2159                } => {
2160                    assert_eq!(field, "pkg.ExtField");
2161                    assert!(!optional);
2162                    assert!(matches!(&expr.node, Expr::Ident(name) if name == "msg"));
2163                }
2164                other => panic!("expected Member, got {:?}", other),
2165            },
2166            MacroExpansion::Error(e) => panic!("unexpected error: {}", e),
2167        }
2168    }
2169
2170    #[test]
2171    fn test_proto_has_ext_expansion() {
2172        let mut id = 10i64;
2173        let mut next_id = || -> i64 {
2174            id += 1;
2175            id
2176        };
2177        let mut ctx = MacroContext::new(&mut next_id, None);
2178
2179        let msg = Spanned::new(1, Expr::Ident("msg".to_string()), 0..3);
2180        let ext = Spanned::new(
2181            2,
2182            Expr::Member {
2183                expr: Box::new(Spanned::new(1, Expr::Ident("pkg".to_string()), 4..7)),
2184                field: "ExtField".to_string(),
2185                optional: false,
2186            },
2187            4..16,
2188        );
2189
2190        let result = expand_proto_has_ext(&mut ctx, 0..20, None, vec![msg, ext]);
2191        match result {
2192            MacroExpansion::Expanded(expr) => match &expr.node {
2193                Expr::MemberTestOnly { expr, field } => {
2194                    assert_eq!(field, "pkg.ExtField");
2195                    assert!(matches!(&expr.node, Expr::Ident(name) if name == "msg"));
2196                }
2197                other => panic!("expected MemberTestOnly, got {:?}", other),
2198            },
2199            MacroExpansion::Error(e) => panic!("unexpected error: {}", e),
2200        }
2201    }
2202
2203    #[test]
2204    fn test_proto_ext_error_non_qualified() {
2205        let mut id = 10i64;
2206        let mut next_id = || -> i64 {
2207            id += 1;
2208            id
2209        };
2210        let mut ctx = MacroContext::new(&mut next_id, None);
2211
2212        let msg = Spanned::new(1, Expr::Ident("msg".to_string()), 0..3);
2213        let bad_arg = Spanned::new(2, Expr::Int(42), 4..6);
2214
2215        let result =
2216            expand_proto_get_ext(&mut ctx, 0..10, None, vec![msg.clone(), bad_arg.clone()]);
2217        assert!(matches!(result, MacroExpansion::Error(_)));
2218
2219        let result = expand_proto_has_ext(&mut ctx, 0..10, None, vec![msg, bad_arg]);
2220        assert!(matches!(result, MacroExpansion::Error(_)));
2221    }
2222}