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