Skip to main content

cel_parser/
ty.rs

1//! Checks source expressions using complete recursive array, tuple, and callable shapes.
2//!
3//! Use [`check_expr`] with a free-identifier resolver for builtin-only expressions, or
4//! [`check_expr_with_type_resolver`] with a host registry and operation metadata.
5//! [`Ty::Any`] retains incomplete editor expressions; it never authorizes executable storage.
6
7use std::any::TypeId;
8use std::borrow::Cow;
9
10use crate::op_table::{builtin_operand_types, cast_source_types};
11use crate::type_expr::{BuiltinTypeResolver, ResolvedType, TypeExpr, TypeResolver};
12use crate::{Expr, ExprSpan, Literal, OpLookup, ParseError, ResolvedLeafType};
13
14/// A complete recursive source type, or an unresolved editor value.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub enum Ty {
17    /// `i8`.
18    I8,
19    /// `i16`.
20    I16,
21    /// `i32`.
22    I32,
23    /// `i64`.
24    I64,
25    /// `i128`.
26    I128,
27    /// `isize`.
28    Isize,
29    /// `u8`.
30    U8,
31    /// `u16`.
32    U16,
33    /// `u32`.
34    U32,
35    /// `u64`.
36    U64,
37    /// `u128`.
38    U128,
39    /// `usize`.
40    Usize,
41    /// `f32`.
42    F32,
43    /// `f64`.
44    F64,
45    /// `bool`.
46    Bool,
47    /// `String`.
48    String,
49    /// A canonical homogeneous array of the boxed element type, which may
50    /// itself be an array (`[[i32]]`) or [`Ty::Any`] (an array whose element type isn't
51    /// statically known — e.g. `[f(), g()]`, whose elements are unresolved call results).
52    Array(Box<Ty>),
53    /// A source-ordered tuple, including unit and singleton tuples.
54    Tuple(Vec<Ty>),
55    /// A callable with complete parameter and result types.
56    Callable(Vec<Ty>, Box<Ty>),
57    /// A known leaf, retaining its concrete identity and complete runtime descriptor.
58    /// Registered function markers are callable only through their registered identity;
59    /// other leaves, including builtin ranges and foreign values, are not callable.
60    Registered(ResolvedLeafType),
61    /// Anything not statically known: an unannotated cell, an
62    /// unresolved identifier, or a call without available metadata.
63    /// Unifies silently with every other `Ty`, in both directions.
64    Any,
65}
66
67impl Ty {
68    /// Preserves a resolved leaf descriptor, using primitive and recursive array variants.
69    ///
70    /// - Complexity: O(d + l) in descriptor depth d and leaf display-name length l.
71    ///
72    /// # Examples
73    /// ```
74    /// use cel_parser::{ResolvedLeafType, Ty};
75    /// use cel_runtime::ValueType;
76    /// let leaf = ResolvedLeafType::new("custom", ValueType::leaf::<()>());
77    /// assert_eq!(Ty::from_leaf(&leaf), Ty::Registered(leaf));
78    /// ```
79    pub fn from_leaf(leaf: &ResolvedLeafType) -> Self {
80        match Self::from_type_id(leaf.type_id()) {
81            Ty::Any | Ty::Array(_) => Ty::Registered(leaf.clone()),
82            primitive => primitive,
83        }
84    }
85
86    /// Maps a resolved [`Literal`] to its exact primitive or registered leaf identity.
87    ///
88    /// # Examples
89    ///
90    /// ```rust
91    /// use cel_parser::{Literal, Ty};
92    ///
93    /// assert_eq!(Ty::from_literal(&Literal::I32(1)), Ty::I32);
94    /// assert_eq!(Ty::from_literal(&Literal::Unit).type_id(), Some(std::any::TypeId::of::<()>()));
95    /// ```
96    pub fn from_literal(lit: &Literal) -> Ty {
97        match lit {
98            Literal::I8(_) => Ty::I8,
99            Literal::I16(_) => Ty::I16,
100            Literal::I32(_) => Ty::I32,
101            Literal::I64(_) => Ty::I64,
102            Literal::I128(_) => Ty::I128,
103            Literal::Isize(_) => Ty::Isize,
104            Literal::U8(_) => Ty::U8,
105            Literal::U16(_) => Ty::U16,
106            Literal::U32(_) => Ty::U32,
107            Literal::U64(_) => Ty::U64,
108            Literal::U128(_) => Ty::U128,
109            Literal::Usize(_) => Ty::Usize,
110            Literal::F32(_) => Ty::F32,
111            Literal::F64(_) => Ty::F64,
112            Literal::Bool(_) => Ty::Bool,
113            Literal::Str(_) => Ty::String,
114            Literal::Char(_) => Self::known_leaf::<char>("char"),
115            Literal::ByteStr(_) => Self::known_leaf::<Vec<u8>>("byte string"),
116            Literal::CStr(_) => Self::known_leaf::<std::ffi::CString>("C string"),
117            Literal::Unit => Ty::Tuple(vec![]),
118        }
119    }
120
121    /// Describes a statically known non-array leaf.
122    ///
123    /// - Precondition: T describes an opaque host leaf, not erased whole-value ownership.
124    fn known_leaf<T: 'static>(name: &'static str) -> Ty {
125        Ty::Registered(ResolvedLeafType::new(
126            name,
127            cel_runtime::ValueType::leaf::<T>(),
128        ))
129    }
130
131    /// Returns whether this type contains an unresolved leaf.
132    ///
133    /// - Complexity: O(n) in recursive type nodes.
134    fn contains_any(&self) -> bool {
135        match self {
136            Ty::Any => true,
137            Ty::Array(element) => element.contains_any(),
138            Ty::Tuple(elements) => elements.iter().any(Self::contains_any),
139            Ty::Callable(parameters, result) => {
140                parameters.iter().any(Self::contains_any) || result.contains_any()
141            }
142            _ => false,
143        }
144    }
145
146    /// Maps a `TypeId` (e.g. from `adam_lang::TypeRegistry::TypeEntry::type_id`) to its [`Ty`].
147    /// An unrecognized `TypeId` maps to [`Ty::Any`] — not an error, matching adam-lang/CEL's
148    /// extensible type system (a host binary's custom registered types are invisible here).
149    /// A flat `TypeId` never establishes a structural array shape; structural
150    /// identities require recursive descriptors instead.
151    ///
152    /// # Examples
153    ///
154    /// ```rust
155    /// use std::any::TypeId;
156    /// use cel_parser::Ty;
157    ///
158    /// assert_eq!(Ty::from_type_id(TypeId::of::<i32>()), Ty::I32);
159    /// assert_eq!(Ty::from_type_id(TypeId::of::<Vec<u8>>()), Ty::Any);
160    /// assert_eq!(
161    ///     Ty::from_type_id(TypeId::of::<cel_runtime::OwnedValue>()),
162    ///     Ty::Any
163    /// );
164    /// ```
165    pub fn from_type_id(id: TypeId) -> Ty {
166        if id == TypeId::of::<i8>() {
167            Ty::I8
168        } else if id == TypeId::of::<i16>() {
169            Ty::I16
170        } else if id == TypeId::of::<i32>() {
171            Ty::I32
172        } else if id == TypeId::of::<i64>() {
173            Ty::I64
174        } else if id == TypeId::of::<i128>() {
175            Ty::I128
176        } else if id == TypeId::of::<isize>() {
177            Ty::Isize
178        } else if id == TypeId::of::<u8>() {
179            Ty::U8
180        } else if id == TypeId::of::<u16>() {
181            Ty::U16
182        } else if id == TypeId::of::<u32>() {
183            Ty::U32
184        } else if id == TypeId::of::<u64>() {
185            Ty::U64
186        } else if id == TypeId::of::<u128>() {
187            Ty::U128
188        } else if id == TypeId::of::<usize>() {
189            Ty::Usize
190        } else if id == TypeId::of::<f32>() {
191            Ty::F32
192        } else if id == TypeId::of::<f64>() {
193            Ty::F64
194        } else if id == TypeId::of::<bool>() {
195            Ty::Bool
196        } else if id == TypeId::of::<String>() {
197            Ty::String
198        } else {
199            Ty::Any
200        }
201    }
202
203    /// Returns this type's `TypeId`, or `None` for [`Ty::Any`] and nonempty tuples (whose
204    /// storage needs a complete shape), and arrays, whose elements also need a complete shape.
205    ///
206    /// # Examples
207    ///
208    /// ```rust
209    /// use std::any::TypeId;
210    /// use cel_parser::Ty;
211    ///
212    /// assert_eq!(Ty::I32.type_id(), Some(TypeId::of::<i32>()));
213    /// assert_eq!(Ty::Any.type_id(), None);
214    /// assert_eq!(
215    ///     Ty::Array(Box::new(Ty::I32)).type_id(),
216    ///     None
217    /// );
218    /// ```
219    pub fn type_id(&self) -> Option<TypeId> {
220        Some(match self {
221            Ty::I8 => TypeId::of::<i8>(),
222            Ty::I16 => TypeId::of::<i16>(),
223            Ty::I32 => TypeId::of::<i32>(),
224            Ty::I64 => TypeId::of::<i64>(),
225            Ty::I128 => TypeId::of::<i128>(),
226            Ty::Isize => TypeId::of::<isize>(),
227            Ty::U8 => TypeId::of::<u8>(),
228            Ty::U16 => TypeId::of::<u16>(),
229            Ty::U32 => TypeId::of::<u32>(),
230            Ty::U64 => TypeId::of::<u64>(),
231            Ty::U128 => TypeId::of::<u128>(),
232            Ty::Usize => TypeId::of::<usize>(),
233            Ty::F32 => TypeId::of::<f32>(),
234            Ty::F64 => TypeId::of::<f64>(),
235            Ty::Bool => TypeId::of::<bool>(),
236            Ty::String => TypeId::of::<String>(),
237            Ty::Array(_) => return None,
238            Ty::Registered(leaf) => leaf.type_id(),
239            Ty::Tuple(elements) if elements.is_empty() => TypeId::of::<()>(),
240            Ty::Tuple(_) => return None,
241            Ty::Callable(..) => TypeId::of::<cel_runtime::DynClosure>(),
242            Ty::Any => return None,
243        })
244    }
245
246    /// Returns a human-readable name for diagnostics, borrowing primitive and leaf names.
247    ///
248    /// - Complexity: O(n * l) time and O(l) space for n recursive nodes and l output bytes;
249    ///   primitive and registered leaf names take O(1).
250    ///
251    /// # Examples
252    ///
253    /// ```rust
254    /// use cel_parser::Ty;
255    ///
256    /// assert_eq!(Ty::I32.name(), "i32");
257    /// assert_eq!(Ty::Any.name(), "<any>");
258    /// assert_eq!(Ty::Array(Box::new(Ty::Array(Box::new(Ty::I32)))).name(), "[[i32]]");
259    /// ```
260    pub fn name(&self) -> Cow<'_, str> {
261        Cow::Borrowed(match self {
262            Ty::I8 => "i8",
263            Ty::I16 => "i16",
264            Ty::I32 => "i32",
265            Ty::I64 => "i64",
266            Ty::I128 => "i128",
267            Ty::Isize => "isize",
268            Ty::U8 => "u8",
269            Ty::U16 => "u16",
270            Ty::U32 => "u32",
271            Ty::U64 => "u64",
272            Ty::U128 => "u128",
273            Ty::Usize => "usize",
274            Ty::F32 => "f32",
275            Ty::F64 => "f64",
276            Ty::Bool => "bool",
277            Ty::String => "String",
278            Ty::Array(element) => return Cow::Owned(format!("[{}]", element.name())),
279            Ty::Tuple(elements) => {
280                return Cow::Owned(format!(
281                    "({}{})",
282                    elements
283                        .iter()
284                        .map(|ty| ty.name())
285                        .collect::<Vec<_>>()
286                        .join(", "),
287                    if elements.len() == 1 { "," } else { "" }
288                ));
289            }
290            Ty::Callable(parameters, result) => {
291                return Cow::Owned(format!(
292                    "Fn(({}{}), {})",
293                    parameters
294                        .iter()
295                        .map(|ty| ty.name())
296                        .collect::<Vec<_>>()
297                        .join(", "),
298                    if parameters.len() == 1 { "," } else { "" },
299                    result.name()
300                ));
301            }
302            Ty::Registered(leaf) => leaf.type_name(),
303            Ty::Any => "<any>",
304        })
305    }
306
307    /// Maps a type's bare name (as written in source, e.g. in a cast's target position) to its
308    /// [`Ty`], or `None` if `name` isn't a recognized primitive - the inverse of [`Ty::name`],
309    /// except it has no input that maps to [`Ty::Any`] (nothing is spelled `<any>` in source).
310    ///
311    /// # Examples
312    ///
313    /// ```rust
314    /// use cel_parser::Ty;
315    ///
316    /// assert_eq!(Ty::from_name("i32"), Some(Ty::I32));
317    /// assert_eq!(Ty::from_name("nonsense"), None);
318    /// ```
319    pub fn from_name(name: &str) -> Option<Ty> {
320        match name {
321            "i8" => Some(Ty::I8),
322            "i16" => Some(Ty::I16),
323            "i32" => Some(Ty::I32),
324            "i64" => Some(Ty::I64),
325            "i128" => Some(Ty::I128),
326            "isize" => Some(Ty::Isize),
327            "u8" => Some(Ty::U8),
328            "u16" => Some(Ty::U16),
329            "u32" => Some(Ty::U32),
330            "u64" => Some(Ty::U64),
331            "u128" => Some(Ty::U128),
332            "usize" => Some(Ty::Usize),
333            "f32" => Some(Ty::F32),
334            "f64" => Some(Ty::F64),
335            "bool" => Some(Ty::Bool),
336            "String" => Some(Ty::String),
337            _ => None,
338        }
339    }
340
341    /// Returns `true` if `self` and `other` have compatible recursive shapes.
342    /// `Ty::Any` unifying silently with everything (in both directions and at every nesting level) is the
343    /// load-bearing property that lets unresolved inputs produce zero false-positive diagnostics.
344    /// Registered leaves unify only when their complete descriptors agree, regardless of aliases.
345    ///
346    /// - Complexity: O(n + l) time and space for n recursive nodes and l registered name bytes.
347    ///
348    /// # Examples
349    ///
350    /// ```rust
351    /// use cel_parser::Ty;
352    ///
353    /// assert!(Ty::Any.unifies_with(&Ty::I32));
354    /// assert!(!Ty::I32.unifies_with(&Ty::F64));
355    /// assert!(Ty::Array(Box::new(Ty::Any)).unifies_with(&Ty::Array(Box::new(Ty::I32))));
356    /// assert!(!Ty::Array(Box::new(Ty::I32)).unifies_with(&Ty::Array(Box::new(Ty::F64))));
357    /// ```
358    pub fn unifies_with(&self, other: &Ty) -> bool {
359        self.unify(other).is_some()
360    }
361
362    /// The most specific type both `self` and `other` describe, or `None` if they're
363    /// incompatible: [`Ty::Any`] yields the other side (it constrains nothing); arrays,
364    /// tuples, and callables unify recursively with equal arity; equal leaves yield themselves.
365    ///
366    /// - Postcondition: `self.unify(other).is_some() == self.unifies_with(other)`, and the result
367    ///   (when `Some`) unifies with both `self` and `other`.
368    /// - Complexity: O(n + l) time and space for n recursive nodes and l registered name bytes.
369    ///
370    /// # Examples
371    ///
372    /// ```rust
373    /// use cel_parser::Ty;
374    ///
375    /// assert_eq!(Ty::Any.unify(&Ty::I32), Some(Ty::I32));
376    /// assert_eq!(
377    ///     Ty::Array(Box::new(Ty::Any)).unify(&Ty::Array(Box::new(Ty::I32))),
378    ///     Some(Ty::Array(Box::new(Ty::I32)))
379    /// );
380    /// assert_eq!(Ty::I32.unify(&Ty::F64), None);
381    /// ```
382    pub fn unify(&self, other: &Ty) -> Option<Ty> {
383        match (self, other) {
384            (Ty::Any, other) => Some(other.clone()),
385            (this, Ty::Any) => Some(this.clone()),
386            (Ty::Array(lhs), Ty::Array(rhs)) => Some(Ty::Array(Box::new(lhs.unify(rhs)?))),
387            (Ty::Tuple(lhs), Ty::Tuple(rhs)) if lhs.len() == rhs.len() => Some(Ty::Tuple(
388                lhs.iter()
389                    .zip(rhs)
390                    .map(|(a, b)| a.unify(b))
391                    .collect::<Option<_>>()?,
392            )),
393            (Ty::Callable(lhs, lr), Ty::Callable(rhs, rr)) if lhs.len() == rhs.len() => {
394                Some(Ty::Callable(
395                    lhs.iter()
396                        .zip(rhs)
397                        .map(|(a, b)| a.unify(b))
398                        .collect::<Option<_>>()?,
399                    Box::new(lr.unify(rr)?),
400                ))
401            }
402            (Ty::Registered(lhs), Ty::Registered(rhs))
403                if lhs.element_type().same_shape(rhs.element_type()) =>
404            {
405                Some(self.clone())
406            }
407            (this, other) if this == other => Some(this.clone()),
408            _ => None,
409        }
410    }
411}
412
413/// Infers `expr`'s type against `resolve_ident` (looks up a free identifier's declared type, or
414/// `Ty::Any` if unknown — e.g. a bare CEL builtin name, or an adam-lang cell with no `: type`
415/// annotation), returning the expression's inferred type plus every type diagnostic found.
416///
417/// Uses an empty [`OpLookup`]; metadata-aware hosts use [`check_expr_with_type_resolver`].
418/// Checks every child, including closure bodies under parameter-local bindings.
419/// Arrays require homogeneous complete shapes, tuples retain source order, and `if` joins
420/// compatible branch types. Callable applications check parameter and result types.
421/// Calls without metadata and free identifiers without declarations remain unresolved.
422/// Registered function values retain concrete marker identity through aliases, rather than
423/// using the alias's spelling to select a signature. Builtin range expressions retain their
424/// concrete range result, not their endpoint type.
425///
426/// - Complexity: O(n) in the number of nodes in `expr`.
427///
428/// # Examples
429///
430/// ```rust
431/// use cel_parser::{Expr, ExprSpan, Literal, Ty};
432/// use cel_parser::ty::check_expr;
433///
434/// let span = ExprSpan {
435///     start: proc_macro2::Span::call_site(),
436///     end: proc_macro2::Span::call_site(),
437/// };
438/// let expr = Expr::Literal {
439///     value: Literal::I32(1),
440///     span,
441/// };
442/// let (ty, diagnostics) = check_expr(&expr, &|_name| Ty::Any);
443/// assert_eq!(ty, Ty::I32);
444/// assert!(diagnostics.is_empty());
445/// ```
446pub fn check_expr(expr: &Expr, resolve_ident: &dyn Fn(&str) -> Ty) -> (Ty, Vec<ParseError>) {
447    check_expr_with_type_resolver(expr, resolve_ident, &BuiltinTypeResolver, &OpLookup::new())
448}
449
450/// Infers `expr`'s type using value/type resolvers and registered operation/record metadata.
451///
452/// Resolves constructors through [`OpLookup::record_schema`] and members through
453/// [`OpLookup::operation_signatures`], never through same-named values. Checks every child even
454/// when a constructor is unknown or an operand is unresolved. Known fields and operands match
455/// exactly, without numeric coercion. Invalid constructors/operations infer as [`Ty::Any`] to
456/// avoid cascading errors. Custom calls without signatures remain best-effort.
457///
458/// - Complexity: O(n + Σ(f² + s · a · d)), for n expression nodes, f schema fields per
459///   constructor, s overloads and a operands per operation, and descriptor nesting depth d,
460///   plus resolver and type-unification costs.
461///
462/// # Examples
463///
464/// ```rust
465/// use cel_parser::{AstContext, OpLookup, Parser, Ty};
466/// use cel_parser::ty::check_expr_with_type_resolver;
467///
468/// let mut parser = Parser::<AstContext>::new(OpLookup::new());
469/// let expr = parser.parse_str_ast("[]: [i32]").unwrap();
470/// let (ty, diagnostics) =
471///     check_expr_with_type_resolver(&expr, &|_name| Ty::Any, &[(
472///         "i32",
473///         cel_parser::ResolvedLeafType::new(
474///             "i32",
475///             cel_runtime::ValueType::leaf::<i32>(),
476///         ),
477///     )], &OpLookup::new());
478/// assert_eq!(ty, Ty::Array(Box::new(Ty::I32)));
479/// assert!(diagnostics.is_empty());
480/// ```
481pub fn check_expr_with_type_resolver(
482    expr: &Expr,
483    resolve_ident: &dyn Fn(&str) -> Ty,
484    resolve_type: &dyn TypeResolver,
485    lookup: &OpLookup,
486) -> (Ty, Vec<ParseError>) {
487    match expr {
488        Expr::Literal { value, .. } => (Ty::from_literal(value), Vec::new()),
489        Expr::Ident { name, .. } => {
490            let ty = resolve_ident(name);
491            let ty = if ty == Ty::Any {
492                lookup
493                    .registered_function_type(name)
494                    .map(Ty::from_leaf)
495                    .or_else(|| matching_registered_type(name, &[], lookup))
496                    .unwrap_or(ty)
497            } else {
498                ty
499            };
500            (ty, Vec::new())
501        }
502        Expr::Struct {
503            name,
504            name_span,
505            fields,
506            ..
507        } => {
508            let mut diagnostics = Vec::new();
509            let schema = lookup.record_schema(name);
510            let mut seen = std::collections::HashSet::new();
511            for field in fields {
512                let (actual, child_diags) = check_expr_with_type_resolver(
513                    &field.value,
514                    resolve_ident,
515                    resolve_type,
516                    lookup,
517                );
518                let valid = child_diags.is_empty();
519                diagnostics.extend(child_diags);
520                let Some(schema) = schema else { continue };
521                let error = if !seen.insert(field.name.as_str()) {
522                    Some(format!("duplicate field `{}`", field.name))
523                } else if let Some(expected) = schema.fields.iter().find(|f| f.name == field.name) {
524                    let expected = resolved_type_to_element_ty(&expected.ty);
525                    (valid && !expected.unifies_with(&actual)).then(|| {
526                        format!(
527                            "field `{}` expects `{}`, found `{}`",
528                            field.name,
529                            expected.name(),
530                            actual.name(),
531                        )
532                    })
533                } else {
534                    Some(format!("unknown field `{}` for `{name}`", field.name))
535                };
536                if let Some(error) = error {
537                    diagnostics.push(ParseError::new_range(
538                        error,
539                        field.name_span.start,
540                        field.value.span().end,
541                    ));
542                }
543            }
544            if let Some(schema) = schema {
545                for field in &schema.fields {
546                    if !seen.contains(field.name) {
547                        diagnostics.push(ParseError::new_range(
548                            format!("missing field `{}` for `{name}`", field.name),
549                            name_span.start,
550                            name_span.end,
551                        ));
552                    }
553                }
554                let ty = if diagnostics.is_empty() {
555                    Ty::from_leaf(&schema.ty)
556                } else {
557                    Ty::Any
558                };
559                (ty, diagnostics)
560            } else {
561                diagnostics.push(ParseError::new_range(
562                    format!("unknown record type `{name}`"),
563                    name_span.start,
564                    name_span.end,
565                ));
566                (Ty::Any, diagnostics)
567            }
568        }
569        Expr::Op {
570            name,
571            operands,
572            span,
573        } => check_op(name, operands, *span, resolve_ident, resolve_type, lookup),
574        Expr::Logical { lhs, rhs, span, .. } => {
575            check_logical(lhs, rhs, *span, resolve_ident, resolve_type, lookup)
576        }
577        Expr::Apply { callee, args, span } => {
578            let (callee_ty, mut diagnostics) =
579                check_expr_with_type_resolver(callee, resolve_ident, resolve_type, lookup);
580            let mut types = Vec::with_capacity(args.len());
581            for arg in args {
582                let (ty, errors) =
583                    check_expr_with_type_resolver(arg, resolve_ident, resolve_type, lookup);
584                types.push(ty);
585                diagnostics.extend(errors);
586            }
587            if let Ty::Callable(parameters, result) = callee_ty {
588                if parameters.len() == types.len()
589                    && parameters
590                        .iter()
591                        .zip(&types)
592                        .all(|(a, b)| a.unifies_with(b))
593                {
594                    return (*result, diagnostics);
595                }
596                diagnostics.push(ParseError::new_range(
597                    "callable argument type mismatch",
598                    span.start,
599                    span.end,
600                ));
601            } else {
602                let function_name = callee_ty
603                    .type_id()
604                    .and_then(|id| lookup.registered_function_name(id))
605                    .or_else(|| match (&callee_ty, callee.as_ref()) {
606                        (Ty::Any, Expr::Ident { name, .. }) => Some(name.as_str()),
607                        _ => None,
608                    });
609                if let Some(name) = function_name
610                    && let Some(ty) =
611                        registered_operation_ty(name, &types, *span, lookup, &mut diagnostics)
612                {
613                    return (ty, diagnostics);
614                }
615                if function_name.is_none() && callee_ty != Ty::Any {
616                    diagnostics.push(ParseError::new_range(
617                        "value is not callable",
618                        span.start,
619                        span.end,
620                    ));
621                }
622            }
623            (Ty::Any, diagnostics)
624        }
625        Expr::Member {
626            receiver,
627            name,
628            args,
629            span,
630            ..
631        } => {
632            let mut diagnostics = Vec::new();
633            let operands = std::iter::once(receiver.as_ref()).chain(args.iter().flatten());
634            let types: Vec<_> = operands
635                .map(|expr| {
636                    let (ty, child_diags) =
637                        check_expr_with_type_resolver(expr, resolve_ident, resolve_type, lookup);
638                    let valid = child_diags.is_empty();
639                    diagnostics.extend(child_diags);
640                    if valid { ty } else { Ty::Any }
641                })
642                .collect();
643            let ty = registered_operation_ty(name, &types, *span, lookup, &mut diagnostics)
644                .unwrap_or(Ty::Any);
645            (ty, diagnostics)
646        }
647        Expr::Tuple { elements, .. } => {
648            let mut diagnostics = Vec::new();
649            let types = elements
650                .iter()
651                .map(|element| {
652                    let (ty, errors) =
653                        check_expr_with_type_resolver(element, resolve_ident, resolve_type, lookup);
654                    diagnostics.extend(errors);
655                    ty
656                })
657                .collect();
658            (Ty::Tuple(types), diagnostics)
659        }
660        // An array literal's own type is `Array` of its elements' unified type; see `check_array`.
661        Expr::Array {
662            elements,
663            type_annotation,
664            annotation_span,
665            ..
666        } => check_array(
667            elements,
668            type_annotation.as_ref(),
669            *annotation_span,
670            resolve_ident,
671            resolve_type,
672            lookup,
673        ),
674        Expr::TupleIndex { base, index, span } => {
675            let (ty, mut diagnostics) =
676                check_expr_with_type_resolver(base, resolve_ident, resolve_type, lookup);
677            if let Ty::Tuple(elements) = ty {
678                if let Some(element) = elements.get(*index) {
679                    return (element.clone(), diagnostics);
680                }
681                diagnostics.push(ParseError::new_range(
682                    "tuple index out of bounds",
683                    span.start,
684                    span.end,
685                ));
686            } else if ty != Ty::Any {
687                diagnostics.push(ParseError::new_range(
688                    "tuple index requires a tuple",
689                    span.start,
690                    span.end,
691                ));
692            }
693            (Ty::Any, diagnostics)
694        }
695        Expr::Cast {
696            expr,
697            type_name,
698            span,
699        } => check_cast(expr, type_name, *span, resolve_ident, resolve_type, lookup),
700        Expr::If {
701            cond,
702            then_branch,
703            else_branch,
704            span,
705        } => {
706            let (condition, mut diagnostics) =
707                check_expr_with_type_resolver(cond, resolve_ident, resolve_type, lookup);
708            if !condition.unifies_with(&Ty::Bool) {
709                diagnostics.push(ParseError::new_range(
710                    "if condition must be bool",
711                    cond.span().start,
712                    cond.span().end,
713                ));
714            }
715            let (then_ty, errors) =
716                check_expr_with_type_resolver(then_branch, resolve_ident, resolve_type, lookup);
717            diagnostics.extend(errors);
718            let (else_ty, errors) = else_branch
719                .as_ref()
720                .map(|branch| {
721                    check_expr_with_type_resolver(branch, resolve_ident, resolve_type, lookup)
722                })
723                .unwrap_or((Ty::Tuple(vec![]), vec![]));
724            diagnostics.extend(errors);
725            let ty = then_ty.unify(&else_ty).unwrap_or_else(|| {
726                diagnostics.push(ParseError::new_range(
727                    "if branch type mismatch",
728                    span.start,
729                    span.end,
730                ));
731                Ty::Any
732            });
733            (ty, diagnostics)
734        }
735        Expr::Closure { params, body, .. } => {
736            let mut annotation_errors = Vec::new();
737            let parameters: Vec<_> = params
738                .iter()
739                .map(|p| match p.type_expr.resolve(resolve_type) {
740                    Ok(resolved) => resolved_type_to_element_ty(&resolved),
741                    Err(error) => {
742                        annotation_errors.push(error);
743                        Ty::Any
744                    }
745                })
746                .collect();
747            let resolve_with_params = |name: &str| -> Ty {
748                if let Some(index) = params.iter().position(|p| p.name == name) {
749                    parameters[index].clone()
750                } else {
751                    resolve_ident(name)
752                }
753            };
754            let (result, mut diagnostics) =
755                check_expr_with_type_resolver(body, &resolve_with_params, resolve_type, lookup);
756            diagnostics.extend(annotation_errors);
757            (Ty::Callable(parameters, Box::new(result)), diagnostics)
758        }
759    }
760}
761
762/// Checks an [`Expr::Array`] node: infers every element (retaining all of their own nested
763/// diagnostics, whatever the fold below finds), then folds the element types left to right with
764/// [`Ty::unify`] — so [`Ty::Any`] elements (an unresolved identifier, a call result) adopt the
765/// other elements' concrete type instead of defeating inference, and nested arrays unify
766/// recursively. The first element that doesn't unify with the accumulated type gets one
767/// diagnostic, reported at that element's own span, and folding stops there: a heterogeneous
768/// literal yields exactly one conflict diagnostic, naming the first conflicting element, matching
769/// the runtime's own "first conflicting index" report. Later elements are
770/// still fully checked, so their own internal diagnostics still surface.
771///
772/// A literal whose elements conflict infers as `Array(Any)` rather than `Any`: the node is
773/// certainly an array, just one whose element type isn't known, and `Any` inside keeps the
774/// mismatch from cascading into further diagnostics at enclosing nodes. An element-less
775/// `Expr::Array` (which the grammar rejects, see <https://github.com/stlab/cel-rs/issues/212>,
776/// but which can still be built directly) infers as `Array(Any)` with no diagnostic.
777///
778/// - Postcondition: the returned type is always a [`Ty::Array`].
779/// - Complexity: O(n · d) where n is the number of elements and d is their array nesting depth,
780///   plus the cost of checking each element (see [`check_expr`]'s own complexity note).
781fn check_array(
782    elements: &[Expr],
783    type_annotation: Option<&TypeExpr>,
784    annotation_span: Option<ExprSpan>,
785    resolve_ident: &dyn Fn(&str) -> Ty,
786    resolve_type: &dyn TypeResolver,
787    lookup: &OpLookup,
788) -> (Ty, Vec<ParseError>) {
789    let mut diagnostics = Vec::new();
790    let element_tys: Vec<Ty> = elements
791        .iter()
792        .map(|element| {
793            let (ty, element_diags) =
794                check_expr_with_type_resolver(element, resolve_ident, resolve_type, lookup);
795            diagnostics.extend(element_diags);
796            ty
797        })
798        .collect();
799    let Some(type_annotation) = type_annotation else {
800        let mut unified = Ty::Any;
801        for (element, ty) in elements.iter().zip(&element_tys) {
802            match unified.unify(ty) {
803                Some(merged) => unified = merged,
804                None => {
805                    diagnostics.push(ParseError::new_range(
806                        format!(
807                            "array elements must share one type: expected `{}`, found `{}`",
808                            unified.name(),
809                            ty.name()
810                        ),
811                        element.span().start,
812                        element.span().end,
813                    ));
814                    return (Ty::Array(Box::new(Ty::Any)), diagnostics);
815                }
816            }
817        }
818        return (Ty::Array(Box::new(unified)), diagnostics);
819    };
820
821    let (resolved_annotation, declared_ty, mut annotation_diags) =
822        check_array_annotation(type_annotation, annotation_span, resolve_type);
823    diagnostics.append(&mut annotation_diags);
824    let (Some(ResolvedType::Array { element: _ }), Ty::Array(declared_element_ty)) =
825        (&resolved_annotation, &declared_ty)
826    else {
827        return (Ty::Array(Box::new(Ty::Any)), diagnostics);
828    };
829
830    for (element, actual_ty) in elements.iter().zip(&element_tys) {
831        if !declared_element_ty.unifies_with(actual_ty) {
832            diagnostics.push(ParseError::new_range(
833                format!(
834                    "array elements must match the annotation exactly: expected `{}`, found `{}`",
835                    declared_element_ty.name(),
836                    actual_ty.name()
837                ),
838                element.span().start,
839                element.span().end,
840            ));
841            break;
842        }
843    }
844    (declared_ty, diagnostics)
845}
846
847/// Resolves one array annotation to both its recursive descriptor tree and its best-effort `Ty`.
848///
849/// # Errors
850///
851/// Returns one diagnostic when the annotation names an unknown type, does not name a complete
852/// array type.
853///
854/// - Complexity: O(n) in the annotation tree plus resolver and descriptor-copying costs.
855fn check_array_annotation(
856    type_annotation: &TypeExpr,
857    annotation_span: Option<ExprSpan>,
858    resolve_type: &dyn TypeResolver,
859) -> (Option<ResolvedType>, Ty, Vec<ParseError>) {
860    let resolved = match type_annotation.resolve(resolve_type) {
861        Ok(resolved) => resolved,
862        Err(err) => return (None, Ty::Array(Box::new(Ty::Any)), vec![err]),
863    };
864    match resolved_type_to_array_ty(
865        &resolved,
866        annotation_span.unwrap_or_else(|| type_annotation.span()),
867    ) {
868        Ok(array_ty) => (Some(resolved), array_ty, Vec::new()),
869        Err(err) => (Some(resolved), Ty::Array(Box::new(Ty::Any)), vec![err]),
870    }
871}
872
873/// Converts a resolved array annotation, rejecting non-array types.
874///
875/// - Complexity: O(d) in descriptor nesting depth.
876///
877/// # Errors
878/// Returns an error for non-array annotations.
879fn resolved_type_to_array_ty(resolved: &ResolvedType, span: ExprSpan) -> crate::Result<Ty> {
880    match resolved {
881        ResolvedType::Array { element } => {
882            Ok(Ty::Array(Box::new(resolved_type_to_element_ty(element))))
883        }
884        other => Err(ParseError::new_range(
885            format!(
886                "array annotations must name a complete array type, found `{}`",
887                other.display_name()
888            ),
889            span.start,
890            span.end,
891        )),
892    }
893}
894
895/// Preserves every complete resolved shape in the static model.
896///
897/// - Complexity: O(n + l) in recursive descriptor nodes n and total leaf-name length l.
898///
899fn resolved_type_to_element_ty(resolved: &ResolvedType) -> Ty {
900    match resolved {
901        ResolvedType::Scalar(leaf) => Ty::from_leaf(leaf),
902        ResolvedType::Array { element } => {
903            Ty::Array(Box::new(resolved_type_to_element_ty(element)))
904        }
905        ResolvedType::Tuple { elements } => {
906            Ty::Tuple(elements.iter().map(resolved_type_to_element_ty).collect())
907        }
908        ResolvedType::Callable { parameters, result } => Ty::Callable(
909            parameters.iter().map(resolved_type_to_element_ty).collect(),
910            Box::new(resolved_type_to_element_ty(result)),
911        ),
912    }
913}
914
915/// Checks an [`Expr::Op`] node by matching known operands against registered signatures
916/// before [`builtin_operand_types`]. Both sources accept unresolved operands only when
917/// a signature accepts all known operands; such applications retain an unresolved result.
918/// Known matches preserve their exact result and registered signatures take precedence.
919/// Invalid operand expressions retain their diagnostics without a cascading operator mismatch.
920/// An operator neither metadata source recognizes can't be checked and infers as `Ty::Any`.
921///
922/// - Complexity: O(s · a · d) for s registered and built-in signatures, a operands and
923///   descriptor depth d, plus the cost of checking each operand (see [`check_expr`]).
924fn check_op(
925    name: &str,
926    operands: &[Expr],
927    span: ExprSpan,
928    resolve_ident: &dyn Fn(&str) -> Ty,
929    resolve_type: &dyn TypeResolver,
930    lookup: &OpLookup,
931) -> (Ty, Vec<ParseError>) {
932    let mut diagnostics = Vec::new();
933    let operand_tys: Vec<Ty> = operands
934        .iter()
935        .map(|operand| {
936            let (ty, operand_diags) =
937                check_expr_with_type_resolver(operand, resolve_ident, resolve_type, lookup);
938            diagnostics.extend(operand_diags);
939            ty
940        })
941        .collect();
942    if !diagnostics.is_empty() {
943        return (Ty::Any, diagnostics);
944    }
945    if let Some(ty) = matching_registered_type(name, &operand_tys, lookup) {
946        return (ty, diagnostics);
947    }
948    if name == "range_full" && operand_tys.is_empty() {
949        let builtin = crate::op_table::builtin_generic_type_0("RangeFull")
950            .expect("builtin full range descriptor");
951        return (
952            Ty::from_leaf(&ResolvedLeafType::new(
953                builtin.type_name,
954                (builtin.value_type)(),
955            )),
956            diagnostics,
957        );
958    }
959    let signatures = builtin_operand_types(name);
960    let matched = signatures.iter().find(|sig| {
961        sig.arity as usize == operand_tys.len()
962            && (operand_tys[0] == Ty::Any || operand_tys[0].type_id() == Some(sig.lhs))
963            && (operand_tys.len() < 2
964                || operand_tys[1] == Ty::Any
965                || operand_tys[1].type_id() == Some(sig.rhs))
966    });
967    if matched.is_some() {
968        let ty = if operand_tys.iter().any(Ty::contains_any) {
969            Ty::Any
970        } else {
971            result_ty_for_op(name, &operand_tys[0])
972        };
973        return (ty, diagnostics);
974    }
975    if let Some(ty) = registered_operation_ty(name, &operand_tys, span, lookup, &mut diagnostics) {
976        return (ty, diagnostics);
977    }
978    if !signatures.is_empty() {
979        let described = operand_tys
980            .iter()
981            .map(Ty::name)
982            .collect::<Vec<_>>()
983            .join(", ");
984        diagnostics.push(ParseError::new_range(
985            format!("`{name}` is not defined for operand type(s) `{described}`"),
986            span.start,
987            span.end,
988        ));
989    }
990    (Ty::Any, diagnostics)
991}
992
993/// Resolves a named metadata overload without consulting same-named value bindings.
994///
995/// Returns `None` only when no metadata exists. Checks known operands exactly; unresolved
996/// operands keep the result unknown. A mismatch produces one diagnostic and an unknown result.
997///
998/// - Complexity: O(s · a · d) for s signatures, a operands and descriptor depth d.
999fn registered_operation_ty(
1000    name: &str,
1001    operands: &[Ty],
1002    span: ExprSpan,
1003    lookup: &OpLookup,
1004    diagnostics: &mut Vec<ParseError>,
1005) -> Option<Ty> {
1006    lookup.operation_signatures(name).next()?;
1007    if let Some(ty) = matching_registered_type(name, operands, lookup) {
1008        return Some(ty);
1009    }
1010    let described = operands.iter().map(Ty::name).collect::<Vec<_>>().join(", ");
1011    diagnostics.push(ParseError::new_range(
1012        format!("`{name}` is not defined for operand type(s) `{described}`"),
1013        span.start,
1014        span.end,
1015    ));
1016    Some(Ty::Any)
1017}
1018
1019/// Returns the first compatible registered result, or `Any` when an operand is unresolved.
1020///
1021/// Returns `None` when no signature accepts the known operand constraints.
1022///
1023/// - Complexity: O(s · a · d) for s signatures, a operands and descriptor depth d.
1024fn matching_registered_type(name: &str, operands: &[Ty], lookup: &OpLookup) -> Option<Ty> {
1025    let signature = lookup.operation_signatures(name).find(|signature| {
1026        signature.operands.len() == operands.len()
1027            && signature
1028                .operands
1029                .iter()
1030                .zip(operands)
1031                .all(|(expected, actual)| {
1032                    resolved_type_to_element_ty(expected).unifies_with(actual)
1033                })
1034    })?;
1035    Some(if operands.iter().any(Ty::contains_any) {
1036        Ty::Any
1037    } else {
1038        resolved_type_to_element_ty(&signature.result)
1039    })
1040}
1041
1042/// Returns the result type of a matched built-in operator application: `Ty::Bool` for the
1043/// comparison operators, the concrete range descriptor for range constructors, or the
1044/// matched LHS type for arithmetic, bitwise, shifts, unary negation and logical not.
1045fn result_ty_for_op(name: &str, operand_ty: &Ty) -> Ty {
1046    let range = match name {
1047        "range" => Some("Range"),
1048        "range_inclusive" => Some("RangeInclusive"),
1049        "range_from" => Some("RangeFrom"),
1050        "range_to" => Some("RangeTo"),
1051        "range_to_inclusive" => Some("RangeToInclusive"),
1052        _ => None,
1053    };
1054    if let Some(range) = range {
1055        let builtin = crate::op_table::builtin_generic_type(range, &operand_ty.name())
1056            .expect("matched builtin range endpoint has a concrete descriptor");
1057        return Ty::from_leaf(&ResolvedLeafType::new(
1058            builtin.type_name,
1059            (builtin.value_type)(),
1060        ));
1061    }
1062    match name {
1063        "==" | "!=" | "<" | "<=" | ">" | ">=" => Ty::Bool,
1064        _ => operand_ty.clone(),
1065    }
1066}
1067
1068/// Checks an [`Expr::Cast`] node (`expr as type_name`): infers `expr`'s type, then (only if it
1069/// resolved to a concrete type) checks that a conversion to `type_name` is registered via
1070/// [`cast_source_types`]. Unlike [`check_op`], the node always infers as the target type once
1071/// `type_name` itself resolves - a cast declares its own result type - regardless of whether a
1072/// diagnostic was recorded for the source, matching [`check_logical`]'s pattern.
1073///
1074/// - Complexity: O(s) where s is the number of registered sources [`cast_source_types`] returns
1075///   for `type_name`, plus the cost of checking `expr` (see [`check_expr`]'s own complexity
1076///   note).
1077fn check_cast(
1078    expr: &Expr,
1079    type_name: &str,
1080    span: ExprSpan,
1081    resolve_ident: &dyn Fn(&str) -> Ty,
1082    resolve_type: &dyn TypeResolver,
1083    lookup: &OpLookup,
1084) -> (Ty, Vec<ParseError>) {
1085    let (expr_ty, mut diagnostics) =
1086        check_expr_with_type_resolver(expr, resolve_ident, resolve_type, lookup);
1087    let Some(target_ty) = Ty::from_name(type_name) else {
1088        diagnostics.push(ParseError::new_range(
1089            format!("unknown type `{type_name}`"),
1090            span.start,
1091            span.end,
1092        ));
1093        return (Ty::Any, diagnostics);
1094    };
1095    if expr_ty != Ty::Any
1096        && !expr_ty.type_id().is_some_and(|source_type_id| {
1097            cast_source_types(type_name).any(|id| id == source_type_id)
1098        })
1099    {
1100        diagnostics.push(ParseError::new_range(
1101            format!("no cast from `{}` to `{type_name}`", expr_ty.name()),
1102            span.start,
1103            span.end,
1104        ));
1105    }
1106    (target_ty, diagnostics)
1107}
1108
1109/// Checks an [`Expr::Logical`] (`&&`/`||`) node: both operands should unify with `Ty::Bool` (CEL's
1110/// fixed short-circuit semantics, not table-driven like [`Expr::Op`]); the node's own type is
1111/// always `Ty::Bool` regardless of whether a diagnostic was recorded.
1112///
1113/// - Complexity: the sum of both operands' recursive type-checking costs.
1114fn check_logical(
1115    lhs: &Expr,
1116    rhs: &Expr,
1117    span: ExprSpan,
1118    resolve_ident: &dyn Fn(&str) -> Ty,
1119    resolve_type: &dyn TypeResolver,
1120    lookup: &OpLookup,
1121) -> (Ty, Vec<ParseError>) {
1122    let (lhs_ty, mut diagnostics) =
1123        check_expr_with_type_resolver(lhs, resolve_ident, resolve_type, lookup);
1124    let (rhs_ty, rhs_diags) =
1125        check_expr_with_type_resolver(rhs, resolve_ident, resolve_type, lookup);
1126    diagnostics.extend(rhs_diags);
1127    for (side, ty) in [("left", lhs_ty), ("right", rhs_ty)] {
1128        if !ty.unifies_with(&Ty::Bool) {
1129            diagnostics.push(ParseError::new_range(
1130                format!(
1131                    "`&&`/`||` requires `bool`, found `{}` on the {side}",
1132                    ty.name()
1133                ),
1134                span.start,
1135                span.end,
1136            ));
1137        }
1138    }
1139    (Ty::Bool, diagnostics)
1140}
1141
1142#[cfg(test)]
1143mod tests {
1144    use super::*;
1145    use crate::LogicalOp;
1146
1147    #[test]
1148    fn from_literal_maps_every_concrete_variant() {
1149        assert_eq!(Ty::from_literal(&Literal::I8(1)), Ty::I8);
1150        assert_eq!(Ty::from_literal(&Literal::I16(1)), Ty::I16);
1151        assert_eq!(Ty::from_literal(&Literal::I32(1)), Ty::I32);
1152        assert_eq!(Ty::from_literal(&Literal::I64(1)), Ty::I64);
1153        assert_eq!(Ty::from_literal(&Literal::I128(1)), Ty::I128);
1154        assert_eq!(Ty::from_literal(&Literal::Isize(1)), Ty::Isize);
1155        assert_eq!(Ty::from_literal(&Literal::U8(1)), Ty::U8);
1156        assert_eq!(Ty::from_literal(&Literal::U16(1)), Ty::U16);
1157        assert_eq!(Ty::from_literal(&Literal::U32(1)), Ty::U32);
1158        assert_eq!(Ty::from_literal(&Literal::U64(1)), Ty::U64);
1159        assert_eq!(Ty::from_literal(&Literal::U128(1)), Ty::U128);
1160        assert_eq!(Ty::from_literal(&Literal::Usize(1)), Ty::Usize);
1161        assert_eq!(Ty::from_literal(&Literal::F32(1.0)), Ty::F32);
1162        assert_eq!(Ty::from_literal(&Literal::F64(1.0)), Ty::F64);
1163        assert_eq!(Ty::from_literal(&Literal::Bool(true)), Ty::Bool);
1164        assert_eq!(Ty::from_literal(&Literal::Str("s".to_string())), Ty::String);
1165    }
1166
1167    #[test]
1168    fn from_literal_preserves_nonprimitive_leaf_identity() {
1169        for (literal, id) in [
1170            (Literal::Char('a'), TypeId::of::<char>()),
1171            (Literal::ByteStr(vec![1]), TypeId::of::<Vec<u8>>()),
1172            (
1173                Literal::CStr(std::ffi::CString::new("a").unwrap()),
1174                TypeId::of::<std::ffi::CString>(),
1175            ),
1176            (Literal::Unit, TypeId::of::<()>()),
1177        ] {
1178            assert_eq!(Ty::from_literal(&literal).type_id(), Some(id));
1179            assert!(!Ty::from_literal(&literal).unifies_with(&Ty::F64));
1180        }
1181    }
1182
1183    #[test]
1184    fn type_id_round_trips_through_from_type_id_for_every_concrete_variant() {
1185        for ty in [
1186            Ty::I8,
1187            Ty::I16,
1188            Ty::I32,
1189            Ty::I64,
1190            Ty::I128,
1191            Ty::Isize,
1192            Ty::U8,
1193            Ty::U16,
1194            Ty::U32,
1195            Ty::U64,
1196            Ty::U128,
1197            Ty::Usize,
1198            Ty::F32,
1199            Ty::F64,
1200            Ty::Bool,
1201            Ty::String,
1202        ] {
1203            let id = ty.type_id().expect("concrete Ty has a TypeId");
1204            assert_eq!(Ty::from_type_id(id), ty);
1205        }
1206    }
1207
1208    #[test]
1209    fn any_has_no_type_id() {
1210        assert_eq!(Ty::Any.type_id(), None);
1211    }
1212
1213    #[test]
1214    fn from_type_id_maps_an_unregistered_type_id_to_any() {
1215        assert_eq!(Ty::from_type_id(TypeId::of::<Vec<u8>>()), Ty::Any);
1216    }
1217
1218    #[test]
1219    fn any_unifies_with_every_concrete_type_in_both_directions() {
1220        assert!(Ty::Any.unifies_with(&Ty::I32));
1221        assert!(Ty::I32.unifies_with(&Ty::Any));
1222        assert!(Ty::Any.unifies_with(&Ty::Any));
1223    }
1224
1225    #[test]
1226    fn identical_concrete_types_unify() {
1227        assert!(Ty::F64.unifies_with(&Ty::F64));
1228    }
1229
1230    #[test]
1231    fn distinct_concrete_types_do_not_unify() {
1232        assert!(!Ty::I32.unifies_with(&Ty::F64));
1233        assert!(!Ty::I32.unifies_with(&Ty::Bool));
1234    }
1235
1236    /// Every concrete (non-[`Ty::Array`], non-[`Ty::Any`]) variant, in declaration order.
1237    fn concrete_tys() -> [Ty; 16] {
1238        [
1239            Ty::I8,
1240            Ty::I16,
1241            Ty::I32,
1242            Ty::I64,
1243            Ty::I128,
1244            Ty::Isize,
1245            Ty::U8,
1246            Ty::U16,
1247            Ty::U32,
1248            Ty::U64,
1249            Ty::U128,
1250            Ty::Usize,
1251            Ty::F32,
1252            Ty::F64,
1253            Ty::Bool,
1254            Ty::String,
1255        ]
1256    }
1257
1258    #[test]
1259    fn every_builtin_scalar_table_agrees_on_the_same_names_and_types() {
1260        use crate::op_table::{BUILTIN_SCALAR_NAMES, builtin_scalar_type};
1261        use crate::type_expr::TypeResolver;
1262
1263        for ty in concrete_tys() {
1264            let name = ty.name();
1265            let expected_id = ty.type_id().expect("a concrete Ty has a TypeId");
1266            assert!(
1267                BUILTIN_SCALAR_NAMES.contains(&&*name),
1268                "`{name}` must be one of the built-in scalar names"
1269            );
1270            assert_eq!(
1271                Ty::from_name(&name),
1272                Some(ty.clone()),
1273                "`{name}` must round-trip through Ty::from_name"
1274            );
1275            let scalar = builtin_scalar_type(&name)
1276                .unwrap_or_else(|| panic!("`{name}` must resolve as a built-in scalar"));
1277            assert_eq!((scalar.value_type)().type_id(), expected_id);
1278            assert_eq!(scalar.type_name, name);
1279            let leaf = BuiltinTypeResolver
1280                .resolve_named_type(&name, &[])
1281                .unwrap_or_else(|| panic!("`{name}` must resolve as a CEL leaf type"));
1282            assert_eq!(leaf.type_id(), expected_id);
1283            assert_eq!(leaf.type_name(), name);
1284            assert_eq!(leaf.element_type().type_id(), expected_id);
1285            assert_eq!(
1286                leaf.element_type().type_name(),
1287                name,
1288                "the runtime element descriptor must carry the source-level name"
1289            );
1290            assert_eq!(leaf.element_type().size(), (scalar.value_type)().size());
1291            assert_eq!(leaf.element_type().align(), (scalar.value_type)().align());
1292        }
1293    }
1294
1295    #[test]
1296    fn every_builtin_scalar_name_has_a_concrete_ty() {
1297        use crate::op_table::BUILTIN_SCALAR_NAMES;
1298
1299        let types = concrete_tys();
1300        let ty_names: Vec<_> = types.iter().map(Ty::name).collect();
1301        for name in BUILTIN_SCALAR_NAMES {
1302            assert!(
1303                ty_names.iter().any(|ty_name| ty_name == name),
1304                "the built-in scalar `{name}` has no concrete Ty variant"
1305            );
1306        }
1307    }
1308
1309    #[test]
1310    fn name_is_distinct_per_type() {
1311        let types = [
1312            Ty::I32,
1313            Ty::F64,
1314            Ty::Bool,
1315            Ty::String,
1316            Ty::Array(Box::new(Ty::I32)),
1317            Ty::Array(Box::new(Ty::Array(Box::new(Ty::I32)))),
1318            Ty::Any,
1319        ];
1320        let names: Vec<_> = types.iter().map(Ty::name).collect();
1321        let mut unique = names.clone();
1322        unique.sort_unstable();
1323        unique.dedup();
1324        assert_eq!(
1325            names.len(),
1326            unique.len(),
1327            "every listed Ty has a distinct name"
1328        );
1329    }
1330
1331    fn any_resolver(_name: &str) -> Ty {
1332        Ty::Any
1333    }
1334
1335    fn point(span: proc_macro2::Span) -> ExprSpan {
1336        ExprSpan {
1337            start: span,
1338            end: span,
1339        }
1340    }
1341
1342    fn lit_i32(v: i32) -> Expr {
1343        Expr::Literal {
1344            value: Literal::I32(v),
1345            span: point(proc_macro2::Span::call_site()),
1346        }
1347    }
1348
1349    fn lit_f64(v: f64) -> Expr {
1350        Expr::Literal {
1351            value: Literal::F64(v),
1352            span: point(proc_macro2::Span::call_site()),
1353        }
1354    }
1355
1356    fn lit_bool(v: bool) -> Expr {
1357        Expr::Literal {
1358            value: Literal::Bool(v),
1359            span: point(proc_macro2::Span::call_site()),
1360        }
1361    }
1362
1363    fn lit_str(v: &str) -> Expr {
1364        Expr::Literal {
1365            value: Literal::Str(v.to_string()),
1366            span: point(proc_macro2::Span::call_site()),
1367        }
1368    }
1369
1370    fn op(name: &str, operands: Vec<Expr>) -> Expr {
1371        Expr::Op {
1372            name: name.to_string(),
1373            operands,
1374            span: point(proc_macro2::Span::call_site()),
1375        }
1376    }
1377
1378    fn array(elements: Vec<Expr>) -> Expr {
1379        Expr::Array {
1380            elements,
1381            type_annotation: None,
1382            annotation_span: None,
1383            span: point(proc_macro2::Span::call_site()),
1384        }
1385    }
1386
1387    fn call(callee: &str) -> Expr {
1388        Expr::Apply {
1389            callee: Box::new(Expr::Ident {
1390                name: callee.to_string(),
1391                span: point(proc_macro2::Span::call_site()),
1392            }),
1393            args: Vec::new(),
1394            span: point(proc_macro2::Span::call_site()),
1395        }
1396    }
1397
1398    #[test]
1399    fn literal_infers_its_own_type() {
1400        let (ty, diags) = check_expr(&lit_i32(1), &any_resolver);
1401        assert_eq!(ty, Ty::I32);
1402        assert!(diags.is_empty());
1403    }
1404
1405    #[test]
1406    fn ident_resolves_via_the_supplied_resolver() {
1407        let expr = Expr::Ident {
1408            name: "width".to_string(),
1409            span: point(proc_macro2::Span::call_site()),
1410        };
1411        let (ty, diags) = check_expr(&expr, &|name| {
1412            if name == "width" { Ty::F64 } else { Ty::Any }
1413        });
1414        assert_eq!(ty, Ty::F64);
1415        assert!(diags.is_empty());
1416    }
1417
1418    #[test]
1419    fn unknown_ident_is_any_and_not_a_diagnostic() {
1420        let expr = Expr::Ident {
1421            name: "mystery".to_string(),
1422            span: point(proc_macro2::Span::call_site()),
1423        };
1424        let (ty, diags) = check_expr(&expr, &any_resolver);
1425        assert_eq!(ty, Ty::Any);
1426        assert!(diags.is_empty());
1427    }
1428
1429    #[test]
1430    fn op_with_matching_signature_infers_the_operand_type() {
1431        let expr = op("+", vec![lit_i32(1), lit_i32(2)]);
1432        let (ty, diags) = check_expr(&expr, &any_resolver);
1433        assert_eq!(ty, Ty::I32);
1434        assert!(diags.is_empty());
1435    }
1436
1437    #[test]
1438    fn comparison_op_always_infers_bool() {
1439        let expr = op("==", vec![lit_i32(1), lit_i32(2)]);
1440        let (ty, diags) = check_expr(&expr, &any_resolver);
1441        assert_eq!(ty, Ty::Bool);
1442        assert!(diags.is_empty());
1443    }
1444
1445    #[test]
1446    fn unary_negation_preserves_the_operand_type() {
1447        let expr = op("-", vec![lit_i32(1)]);
1448        let (ty, diags) = check_expr(&expr, &any_resolver);
1449        assert_eq!(ty, Ty::I32);
1450        assert!(diags.is_empty());
1451    }
1452
1453    #[test]
1454    fn op_with_mismatched_operand_types_produces_one_diagnostic_and_infers_any() {
1455        let expr = op("+", vec![lit_i32(1), lit_str("s")]);
1456        let (ty, diags) = check_expr(&expr, &any_resolver);
1457        assert_eq!(ty, Ty::Any);
1458        assert_eq!(diags.len(), 1);
1459    }
1460
1461    #[test]
1462    fn op_with_an_any_operand_produces_no_diagnostic() {
1463        let expr = op(
1464            "+",
1465            vec![
1466                Expr::Ident {
1467                    name: "mystery".to_string(),
1468                    span: point(proc_macro2::Span::call_site()),
1469                },
1470                lit_i32(1),
1471            ],
1472        );
1473        let (ty, diags) = check_expr(&expr, &any_resolver);
1474        assert_eq!(ty, Ty::Any);
1475        assert!(diags.is_empty());
1476    }
1477
1478    #[test]
1479    fn unregistered_operator_name_is_any_and_not_a_diagnostic() {
1480        // "greet" is only ever registered at runtime via OpLookup::register_tuple_op; the static
1481        // checker can't see it and must not guess.
1482        let expr = op("greet", vec![lit_i32(1)]);
1483        let (ty, diags) = check_expr(&expr, &any_resolver);
1484        assert_eq!(ty, Ty::Any);
1485        assert!(diags.is_empty());
1486    }
1487
1488    #[test]
1489    fn logical_and_with_bool_operands_infers_bool_with_no_diagnostic() {
1490        let expr = Expr::Logical {
1491            op: LogicalOp::And,
1492            lhs: Box::new(lit_bool(true)),
1493            rhs: Box::new(lit_bool(false)),
1494            span: point(proc_macro2::Span::call_site()),
1495        };
1496        let (ty, diags) = check_expr(&expr, &any_resolver);
1497        assert_eq!(ty, Ty::Bool);
1498        assert!(diags.is_empty());
1499    }
1500
1501    #[test]
1502    fn logical_or_with_a_non_bool_operand_produces_a_diagnostic_but_still_infers_bool() {
1503        let expr = Expr::Logical {
1504            op: LogicalOp::Or,
1505            lhs: Box::new(lit_i32(1)),
1506            rhs: Box::new(lit_bool(true)),
1507            span: point(proc_macro2::Span::call_site()),
1508        };
1509        let (ty, diags) = check_expr(&expr, &any_resolver);
1510        assert_eq!(ty, Ty::Bool);
1511        assert_eq!(diags.len(), 1);
1512    }
1513
1514    #[test]
1515    fn a_broken_op_nested_inside_a_tuple_still_surfaces_a_diagnostic() {
1516        let expr = Expr::Tuple {
1517            elements: vec![op("+", vec![lit_i32(1), lit_str("s")]), lit_i32(2)],
1518            span: point(proc_macro2::Span::call_site()),
1519        };
1520        let (ty, diags) = check_expr(&expr, &any_resolver);
1521        assert_eq!(ty, Ty::Tuple(vec![Ty::Any, Ty::I32]));
1522        assert_eq!(diags.len(), 1);
1523    }
1524
1525    #[test]
1526    fn a_broken_op_nested_inside_an_if_condition_still_surfaces_a_diagnostic() {
1527        let expr = Expr::If {
1528            cond: Box::new(op("+", vec![lit_i32(1), lit_str("s")])),
1529            then_branch: Box::new(lit_i32(1)),
1530            else_branch: Some(Box::new(lit_i32(2))),
1531            span: point(proc_macro2::Span::call_site()),
1532        };
1533        let (ty, diags) = check_expr(&expr, &any_resolver);
1534        assert_eq!(ty, Ty::I32);
1535        assert_eq!(diags.len(), 1);
1536    }
1537
1538    #[test]
1539    fn a_broken_op_nested_inside_a_call_argument_still_surfaces_a_diagnostic() {
1540        let expr = Expr::Apply {
1541            callee: Box::new(Expr::Ident {
1542                name: "f".to_string(),
1543                span: point(proc_macro2::Span::call_site()),
1544            }),
1545            args: vec![op("+", vec![lit_i32(1), lit_str("s")])],
1546            span: point(proc_macro2::Span::call_site()),
1547        };
1548        let (ty, diags) = check_expr(&expr, &any_resolver);
1549        assert_eq!(ty, Ty::Any, "Apply itself is not type-checked in v1");
1550        assert_eq!(diags.len(), 1);
1551    }
1552
1553    fn cast(inner: Expr, type_name: &str) -> Expr {
1554        Expr::Cast {
1555            expr: Box::new(inner),
1556            type_name: type_name.to_string(),
1557            span: point(proc_macro2::Span::call_site()),
1558        }
1559    }
1560
1561    #[test]
1562    fn cast_to_a_registered_numeric_target_infers_the_target_type_with_no_diagnostic() {
1563        let (ty, diags) = check_expr(&cast(lit_i32(1), "f64"), &any_resolver);
1564        assert_eq!(ty, Ty::F64);
1565        assert!(diags.is_empty());
1566    }
1567
1568    #[test]
1569    fn cast_from_bool_to_bool_produces_no_diagnostic() {
1570        // Regression test for the `ty.rs`/`op_table.rs` "no cast from ... to bool" vs. "unknown
1571        // type" inconsistency: bool is the one type Rust's own `as` allows as a source for `as
1572        // bool` (a no-op identity conversion), so this must not be a diagnostic.
1573        let (ty, diags) = check_expr(&cast(lit_bool(true), "bool"), &any_resolver);
1574        assert_eq!(ty, Ty::Bool);
1575        assert!(diags.is_empty());
1576    }
1577
1578    #[test]
1579    fn cast_from_a_number_to_bool_is_a_diagnostic_not_an_unknown_type_error() {
1580        // `bool` is a recognized cast target (matches [`op_table::signatures_for_cast`]), just one
1581        // with no registered `i32` source - same "no cast from `i32` to `bool`" diagnostic
1582        // `OpLookup::lookup_cast` would report at execution time, not "unknown type `bool`".
1583        let (ty, diags) = check_expr(&cast(lit_i32(1), "bool"), &any_resolver);
1584        assert_eq!(ty, Ty::Bool);
1585        assert_eq!(diags.len(), 1);
1586        assert!(diags[0].message().contains("no cast from"));
1587    }
1588
1589    #[test]
1590    fn cast_to_an_unrecognized_type_name_is_a_diagnostic_and_infers_any() {
1591        let (ty, diags) = check_expr(&cast(lit_i32(1), "nonsense"), &any_resolver);
1592        assert_eq!(ty, Ty::Any);
1593        assert_eq!(diags.len(), 1);
1594    }
1595
1596    #[test]
1597    fn cast_of_an_any_typed_operand_produces_no_diagnostic() {
1598        let expr = Expr::Ident {
1599            name: "mystery".to_string(),
1600            span: point(proc_macro2::Span::call_site()),
1601        };
1602        let (ty, diags) = check_expr(&cast(expr, "i32"), &any_resolver);
1603        assert_eq!(ty, Ty::I32);
1604        assert!(diags.is_empty());
1605    }
1606
1607    fn closure_param(name: &str, type_name: &str) -> crate::ClosureParam {
1608        crate::ClosureParam {
1609            name: name.to_string(),
1610            name_span: point(proc_macro2::Span::call_site()),
1611            type_expr: crate::TypeExpr::Named {
1612                name: type_name.to_string(),
1613                args: Vec::new(),
1614                span: point(proc_macro2::Span::call_site()),
1615            },
1616        }
1617    }
1618
1619    fn closure(params: Vec<crate::ClosureParam>, body: Expr) -> Expr {
1620        Expr::Closure {
1621            params,
1622            body: Box::new(body),
1623            span: point(proc_macro2::Span::call_site()),
1624        }
1625    }
1626
1627    #[test]
1628    fn closure_literal_preserves_its_signature() {
1629        let expr = closure(
1630            vec![closure_param("x", "i32")],
1631            Expr::Ident {
1632                name: "x".to_string(),
1633                span: point(proc_macro2::Span::call_site()),
1634            },
1635        );
1636        let (ty, diags) = check_expr(&expr, &any_resolver);
1637        assert_eq!(ty, Ty::Callable(vec![Ty::I32], Box::new(Ty::I32)));
1638        assert!(diags.is_empty());
1639    }
1640
1641    #[test]
1642    fn closure_body_type_error_surfaces_through_the_closure() {
1643        let body = op(
1644            "+",
1645            vec![
1646                Expr::Ident {
1647                    name: "x".to_string(),
1648                    span: point(proc_macro2::Span::call_site()),
1649                },
1650                lit_str("s"),
1651            ],
1652        );
1653        let expr = closure(vec![closure_param("x", "i32")], body);
1654        let (ty, diags) = check_expr(&expr, &any_resolver);
1655        assert_eq!(ty, Ty::Callable(vec![Ty::I32], Box::new(Ty::Any)));
1656        assert_eq!(diags.len(), 1, "the body's i32 + String mismatch surfaces");
1657    }
1658
1659    #[test]
1660    fn closure_param_shadows_the_outer_resolver() {
1661        // Outer resolver claims "x" is a String; the closure's own "x: i32" parameter must win
1662        // inside the body, so `x + 1i32` type-checks cleanly with no diagnostic.
1663        let outer_resolver = |_: &str| Ty::String;
1664        let body = op(
1665            "+",
1666            vec![
1667                Expr::Ident {
1668                    name: "x".to_string(),
1669                    span: point(proc_macro2::Span::call_site()),
1670                },
1671                lit_i32(1),
1672            ],
1673        );
1674        let expr = closure(vec![closure_param("x", "i32")], body);
1675        let (_, diags) = check_expr(&expr, &outer_resolver);
1676        assert!(diags.is_empty());
1677    }
1678
1679    #[test]
1680    fn array_type_id_requires_complete_shape_instead_of_a_flat_marker() {
1681        assert_eq!(Ty::Array(Box::new(Ty::I32)).type_id(), None);
1682        assert_eq!(Ty::Array(Box::new(Ty::Any)).type_id(), None);
1683    }
1684
1685    #[test]
1686    fn from_type_id_does_not_invent_structural_shape_from_an_owner() {
1687        assert_eq!(
1688            Ty::from_type_id(TypeId::of::<cel_runtime::OwnedValue>()),
1689            Ty::Any
1690        );
1691    }
1692
1693    /// Registered aliases unify by complete concrete identity, not diagnostic spelling.
1694    #[test]
1695    fn registered_leaf_aliases_unify_independently_of_names_and_capabilities() {
1696        /// A foreign leaf whose optional clone capability does not change identity.
1697        #[derive(Clone)]
1698        struct Foreign;
1699        let first = Ty::from_leaf(&ResolvedLeafType::new(
1700            "FirstAlias",
1701            cel_runtime::ValueType::leaf::<Foreign>(),
1702        ));
1703        let second = Ty::from_leaf(&ResolvedLeafType::new(
1704            "SecondAlias",
1705            cel_runtime::ValueType::cloneable_leaf::<Foreign>(),
1706        ));
1707        assert_eq!(first.unify(&second), Some(first.clone()));
1708    }
1709
1710    #[test]
1711    fn array_name_renders_bracket_syntax_recursively() {
1712        assert_eq!(Ty::Array(Box::new(Ty::I32)).name(), "[i32]");
1713        assert_eq!(
1714            Ty::Array(Box::new(Ty::Array(Box::new(Ty::I32)))).name(),
1715            "[[i32]]"
1716        );
1717        assert_eq!(Ty::Array(Box::new(Ty::Any)).name(), "[<any>]");
1718    }
1719
1720    #[test]
1721    fn arrays_unify_only_when_their_element_types_unify_recursively() {
1722        assert!(Ty::Array(Box::new(Ty::I32)).unifies_with(&Ty::Array(Box::new(Ty::I32))));
1723        assert!(Ty::Array(Box::new(Ty::Any)).unifies_with(&Ty::Array(Box::new(Ty::I32))));
1724        assert!(!Ty::Array(Box::new(Ty::I32)).unifies_with(&Ty::Array(Box::new(Ty::F64))));
1725        assert!(
1726            !Ty::Array(Box::new(Ty::Array(Box::new(Ty::I32))))
1727                .unifies_with(&Ty::Array(Box::new(Ty::I32))),
1728            "differing nesting depth does not unify"
1729        );
1730        assert!(!Ty::Array(Box::new(Ty::I32)).unifies_with(&Ty::I32));
1731        assert!(Ty::Any.unifies_with(&Ty::Array(Box::new(Ty::I32))));
1732        assert!(Ty::Array(Box::new(Ty::I32)).unifies_with(&Ty::Any));
1733    }
1734
1735    #[test]
1736    fn unify_keeps_the_more_concrete_side_at_every_nesting_level() {
1737        assert_eq!(Ty::Any.unify(&Ty::I32), Some(Ty::I32));
1738        assert_eq!(Ty::I32.unify(&Ty::Any), Some(Ty::I32));
1739        assert_eq!(
1740            Ty::Array(Box::new(Ty::Any)).unify(&Ty::Array(Box::new(Ty::Array(Box::new(Ty::I32))))),
1741            Some(Ty::Array(Box::new(Ty::Array(Box::new(Ty::I32)))))
1742        );
1743    }
1744
1745    #[test]
1746    fn unify_of_incompatible_types_is_none() {
1747        assert_eq!(Ty::I32.unify(&Ty::F64), None);
1748        assert_eq!(Ty::Array(Box::new(Ty::I32)).unify(&Ty::I32), None);
1749        assert_eq!(
1750            Ty::Array(Box::new(Ty::I32)).unify(&Ty::Array(Box::new(Ty::F64))),
1751            None
1752        );
1753    }
1754
1755    #[test]
1756    fn a_unified_type_unifies_with_both_of_its_sides() {
1757        let lhs = Ty::Array(Box::new(Ty::Any));
1758        let rhs = Ty::Array(Box::new(Ty::I32));
1759        let merged = lhs.unify(&rhs).expect("compatible types unify");
1760        assert!(merged.unifies_with(&lhs));
1761        assert!(merged.unifies_with(&rhs));
1762    }
1763
1764    #[test]
1765    fn homogeneous_array_infers_its_element_type() {
1766        let (ty, diags) = check_expr(&array(vec![lit_i32(1), lit_i32(2)]), &any_resolver);
1767        assert_eq!(ty, Ty::Array(Box::new(Ty::I32)));
1768        assert!(diags.is_empty());
1769    }
1770
1771    #[test]
1772    fn single_element_array_infers_that_element_type() {
1773        let (ty, diags) = check_expr(&array(vec![lit_str("s")]), &any_resolver);
1774        assert_eq!(ty, Ty::Array(Box::new(Ty::String)));
1775        assert!(diags.is_empty());
1776    }
1777
1778    #[test]
1779    fn nested_array_types_unify_recursively() {
1780        let expr = array(vec![array(vec![lit_i32(1)]), array(vec![lit_i32(2)])]);
1781        let (ty, diags) = check_expr(&expr, &any_resolver);
1782        assert_eq!(ty, Ty::Array(Box::new(Ty::Array(Box::new(Ty::I32)))));
1783        assert!(diags.is_empty());
1784    }
1785
1786    #[test]
1787    fn heterogeneous_array_reports_the_conflicting_element() {
1788        let (ty, diags) = check_expr(&array(vec![lit_i32(1), lit_f64(2.0)]), &any_resolver);
1789        assert_eq!(diags.len(), 1);
1790        assert!(
1791            diags[0].message().contains("expected `i32`, found `f64`"),
1792            "got: {}",
1793            diags[0].message()
1794        );
1795        assert_eq!(
1796            ty,
1797            Ty::Array(Box::new(Ty::Any)),
1798            "a conflicting literal is still an array, of an unknown element type"
1799        );
1800    }
1801
1802    #[test]
1803    fn heterogeneous_nested_array_reports_the_recursive_element_types() {
1804        let expr = array(vec![array(vec![lit_i32(1)]), array(vec![lit_f64(1.0)])]);
1805        let (ty, diags) = check_expr(&expr, &any_resolver);
1806        assert_eq!(diags.len(), 1);
1807        assert!(
1808            diags[0]
1809                .message()
1810                .contains("expected `[i32]`, found `[f64]`"),
1811            "got: {}",
1812            diags[0].message()
1813        );
1814        assert_eq!(ty, Ty::Array(Box::new(Ty::Any)));
1815    }
1816
1817    #[test]
1818    fn mismatched_nesting_depth_is_a_diagnostic() {
1819        let expr = array(vec![array(vec![lit_i32(1)]), lit_i32(2)]);
1820        let (_, diags) = check_expr(&expr, &any_resolver);
1821        assert_eq!(diags.len(), 1);
1822        assert!(
1823            diags[0].message().contains("expected `[i32]`, found `i32`"),
1824            "got: {}",
1825            diags[0].message()
1826        );
1827    }
1828
1829    #[test]
1830    fn array_of_unresolved_call_results_infers_an_array_of_any_with_no_diagnostic() {
1831        // `Apply` results are deliberately `Ty::Any` in v1, so `[f(), g()]` cannot be proven
1832        // homogeneous - and must not be reported as heterogeneous either.
1833        let expr = array(vec![call("f"), call("g")]);
1834        let (ty, diags) = check_expr(&expr, &any_resolver);
1835        assert_eq!(ty, Ty::Array(Box::new(Ty::Any)));
1836        assert!(diags.is_empty());
1837    }
1838
1839    #[test]
1840    fn an_any_element_keeps_the_concrete_element_type() {
1841        let expr = array(vec![call("f"), lit_i32(1)]);
1842        let (ty, diags) = check_expr(&expr, &any_resolver);
1843        assert_eq!(ty, Ty::Array(Box::new(Ty::I32)));
1844        assert!(diags.is_empty());
1845    }
1846
1847    #[test]
1848    fn an_any_nested_element_keeps_the_concrete_recursive_element_type() {
1849        let expr = array(vec![array(vec![call("f")]), array(vec![lit_i32(1)])]);
1850        let (ty, diags) = check_expr(&expr, &any_resolver);
1851        assert_eq!(ty, Ty::Array(Box::new(Ty::Array(Box::new(Ty::I32)))));
1852        assert!(diags.is_empty());
1853    }
1854
1855    #[test]
1856    fn every_elements_own_diagnostics_are_retained() {
1857        let expr = array(vec![
1858            op("+", vec![lit_i32(1), lit_str("s")]),
1859            op("+", vec![lit_i32(2), lit_str("t")]),
1860        ]);
1861        let (_, diags) = check_expr(&expr, &any_resolver);
1862        assert_eq!(diags.len(), 2, "both broken elements are reported");
1863    }
1864
1865    #[test]
1866    fn a_later_elements_own_diagnostic_survives_an_earlier_element_conflict() {
1867        let expr = array(vec![
1868            lit_i32(1),
1869            lit_f64(2.0),
1870            op("+", vec![lit_i32(3), lit_str("s")]),
1871        ]);
1872        let (ty, diags) = check_expr(&expr, &any_resolver);
1873        assert_eq!(
1874            diags.len(),
1875            2,
1876            "one element-conflict diagnostic plus the third element's own operator diagnostic"
1877        );
1878        assert_eq!(ty, Ty::Array(Box::new(Ty::Any)));
1879    }
1880
1881    #[test]
1882    fn only_the_first_conflicting_element_is_reported() {
1883        let expr = array(vec![lit_i32(1), lit_f64(2.0), lit_bool(true)]);
1884        let (_, diags) = check_expr(&expr, &any_resolver);
1885        assert_eq!(diags.len(), 1, "one diagnostic per array literal");
1886        assert!(diags[0].message().contains("found `f64`"), "the first one");
1887    }
1888
1889    #[test]
1890    fn a_conflicting_elements_diagnostic_is_anchored_at_that_element() {
1891        // Every other array test builds spans with `Span::call_site()`, where "this element's
1892        // span", "the whole literal's span", and "some other element's span" are all
1893        // indistinguishable. Parsing real source (proc-macro2's `span-locations` feature is on)
1894        // gives each token its own line/column, making the anchoring observable: the conflict is
1895        // reported at the element on line 4, not at the enclosing `[`...`]` starting on line 1,
1896        // and not at a fixed earlier position such as line 2 or 3.
1897        let source = "[\n    1i32,\n    2i32,\n    3.0f64,\n    4i32\n]";
1898        let mut parser = crate::Parser::<crate::AstContext>::new(crate::OpLookup::new());
1899        let expr = parser.parse_str_ast(source).expect("source parses");
1900        let (ty, diags) = check_expr(&expr, &any_resolver);
1901        assert_eq!(ty, Ty::Array(Box::new(Ty::Any)));
1902        assert_eq!(diags.len(), 1);
1903        assert!(
1904            diags[0].message().contains("expected `i32`, found `f64`"),
1905            "got: {}",
1906            diags[0].message()
1907        );
1908        let start = diags[0].span().start();
1909        let end = diags[0]
1910            .end_span()
1911            .expect("the conflict diagnostic spans a range")
1912            .end();
1913        assert_eq!(start.line, 4, "anchored at the conflicting element's line");
1914        assert_eq!(start.column, 4, "at the element, not the enclosing group");
1915        assert_eq!(end.line, 4);
1916        assert_eq!(end.column, 4 + "3.0f64".len());
1917    }
1918
1919    #[test]
1920    fn a_conflict_inside_a_nested_array_does_not_cascade_to_the_outer_literal() {
1921        // `[[1i32, 2.0f64], [3i32, 4i32]]`: the inner `[1i32, 2.0f64]` conflicts and falls back
1922        // to `Array(Any)`, which still unifies with the valid inner array's `Array(i32)` - so the
1923        // outer fold stays silent and the outer literal keeps the concrete nested element type.
1924        let expr = array(vec![
1925            array(vec![lit_i32(1), lit_f64(2.0)]),
1926            array(vec![lit_i32(3), lit_i32(4)]),
1927        ]);
1928        let (ty, diags) = check_expr(&expr, &any_resolver);
1929        assert_eq!(diags.len(), 1, "only the inner conflict is reported");
1930        assert!(
1931            diags[0].message().contains("expected `i32`, found `f64`"),
1932            "got: {}",
1933            diags[0].message()
1934        );
1935        assert_eq!(ty, Ty::Array(Box::new(Ty::Array(Box::new(Ty::I32)))));
1936    }
1937
1938    #[test]
1939    fn typed_array_annotation_returns_the_declared_array_type() {
1940        let mut parser = crate::Parser::<crate::AstContext>::new(crate::OpLookup::new());
1941        let expr = parser
1942            .parse_str_ast("[0, 1]: [i32]")
1943            .expect("source parses");
1944        let (ty, diags) = check_expr(&expr, &any_resolver);
1945        assert_eq!(ty, Ty::Array(Box::new(Ty::I32)));
1946        assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
1947    }
1948
1949    #[test]
1950    fn typed_empty_array_annotation_returns_the_declared_nested_array_type() {
1951        let mut parser = crate::Parser::<crate::AstContext>::new(crate::OpLookup::new());
1952        let expr = parser.parse_str_ast("[]: [[i32]]").expect("source parses");
1953        let (ty, diags) = check_expr(&expr, &any_resolver);
1954        assert_eq!(ty, Ty::Array(Box::new(Ty::Array(Box::new(Ty::I32)))));
1955        assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
1956    }
1957
1958    #[test]
1959    fn typed_array_annotation_reports_exact_element_type_mismatches() {
1960        let mut parser = crate::Parser::<crate::AstContext>::new(crate::OpLookup::new());
1961        let expr = parser
1962            .parse_str_ast("[0, 1]: [f64]")
1963            .expect("source parses");
1964        let (ty, diags) = check_expr(&expr, &any_resolver);
1965        assert_eq!(ty, Ty::Array(Box::new(Ty::F64)));
1966        assert_eq!(diags.len(), 1);
1967        assert!(
1968            diags[0].message().contains("expected `f64`, found `i32`"),
1969            "got: {}",
1970            diags[0].message()
1971        );
1972    }
1973
1974    #[test]
1975    fn typed_array_annotation_reports_exact_custom_element_type_mismatches() {
1976        #[derive(Clone)]
1977        struct Custom;
1978
1979        let mut parser = crate::Parser::<crate::AstContext>::new(crate::OpLookup::new());
1980        let expr = parser
1981            .parse_str_ast("[0]: [Custom]")
1982            .expect("source parses");
1983        let (ty, diags) = check_expr_with_type_resolver(
1984            &expr,
1985            &any_resolver,
1986            &[(
1987                "Custom",
1988                crate::ResolvedLeafType::new("Custom", cel_runtime::ValueType::leaf::<Custom>()),
1989            )],
1990            &OpLookup::new(),
1991        );
1992        assert_eq!(
1993            ty,
1994            Ty::Array(Box::new(Ty::Registered(crate::ResolvedLeafType::new(
1995                "Custom",
1996                cel_runtime::ValueType::leaf::<Custom>()
1997            ))))
1998        );
1999        assert_eq!(diags.len(), 1);
2000        assert!(
2001            diags[0]
2002                .message()
2003                .contains("expected `Custom`, found `i32`"),
2004            "got: {}",
2005            diags[0].message()
2006        );
2007    }
2008
2009    /// A nested literal's own annotation is part of what it means: `[[]: [CustomA]]` is an array
2010    /// of `[CustomA]`, which the runtime rejects against `[[CustomB]]`.
2011    #[test]
2012    fn nested_annotated_array_reports_a_custom_element_mismatch() {
2013        let mut parser = crate::Parser::<crate::AstContext>::new(crate::OpLookup::new());
2014        let expr = parser
2015            .parse_str_ast("[[]: [CustomA]]: [[CustomB]]")
2016            .expect("source parses");
2017        let (ty, diags) = check_expr_with_type_resolver(
2018            &expr,
2019            &any_resolver,
2020            &custom_ab_resolver(),
2021            &OpLookup::new(),
2022        );
2023
2024        assert_eq!(
2025            ty,
2026            Ty::Array(Box::new(Ty::Array(Box::new(Ty::Registered(
2027                custom_ab_resolver()[1].1.clone()
2028            )))))
2029        );
2030        assert_eq!(diags.len(), 1, "unexpected diagnostics: {diags:?}");
2031        assert!(
2032            diags[0]
2033                .message()
2034                .contains("expected `[CustomB]`, found `[CustomA]`"),
2035            "got: {}",
2036            diags[0].message()
2037        );
2038    }
2039
2040    /// The same nesting with agreeing annotations is accepted with no diagnostic.
2041    #[test]
2042    fn nested_annotated_array_accepts_a_matching_custom_element_annotation() {
2043        let mut parser = crate::Parser::<crate::AstContext>::new(crate::OpLookup::new());
2044        let expr = parser
2045            .parse_str_ast("[[]: [CustomA]]: [[CustomA]]")
2046            .expect("source parses");
2047        let (ty, diags) = check_expr_with_type_resolver(
2048            &expr,
2049            &any_resolver,
2050            &custom_ab_resolver(),
2051            &OpLookup::new(),
2052        );
2053
2054        assert_eq!(
2055            ty,
2056            Ty::Array(Box::new(Ty::Array(Box::new(Ty::Registered(
2057                custom_ab_resolver()[0].1.clone()
2058            )))))
2059        );
2060        assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
2061    }
2062
2063    /// A resolver registering two distinct custom leaf types.
2064    fn custom_ab_resolver() -> [(&'static str, crate::ResolvedLeafType); 2] {
2065        #[derive(Clone)]
2066        struct CustomA;
2067        #[derive(Clone)]
2068        struct CustomB;
2069
2070        [
2071            (
2072                "CustomA",
2073                crate::ResolvedLeafType::new("CustomA", cel_runtime::ValueType::leaf::<CustomA>()),
2074            ),
2075            (
2076                "CustomB",
2077                crate::ResolvedLeafType::new("CustomB", cel_runtime::ValueType::leaf::<CustomB>()),
2078            ),
2079        ]
2080    }
2081
2082    #[test]
2083    fn typed_array_annotation_diagnostics_are_anchored_at_the_annotation_span() {
2084        let source = "[0]: i32";
2085        let mut parser = crate::Parser::<crate::AstContext>::new(crate::OpLookup::new());
2086        let expr = parser.parse_str_ast(source).expect("source parses");
2087        let (_, diags) = check_expr(&expr, &any_resolver);
2088        assert_eq!(diags.len(), 1);
2089        assert!(
2090            diags[0]
2091                .message()
2092                .contains("array annotations must name a complete array type"),
2093            "got: {}",
2094            diags[0].message()
2095        );
2096        let start = diags[0].span().start();
2097        let end = diags[0]
2098            .end_span()
2099            .expect("annotation diagnostics span a range")
2100            .end();
2101        assert_eq!(&source[start.column..end.column], ": i32");
2102    }
2103
2104    #[test]
2105    fn typed_array_annotation_preserves_tuple_array_element_types() {
2106        let mut parser = crate::Parser::<crate::AstContext>::new(crate::OpLookup::new());
2107        let expr = parser
2108            .parse_str_ast("[]: [(i32, f64)]")
2109            .expect("source parses");
2110        let (ty, diags) = check_expr(&expr, &any_resolver);
2111        assert_eq!(ty, Ty::Array(Box::new(Ty::Tuple(vec![Ty::I32, Ty::F64]))));
2112        assert!(diags.is_empty());
2113    }
2114
2115    #[test]
2116    fn an_empty_array_expression_infers_an_array_of_any() {
2117        // The grammar rejects `[]` outright (see #212), but an `Expr::Array` built directly still
2118        // has to infer conservatively rather than panic.
2119        let (ty, diags) = check_expr(&array(Vec::new()), &any_resolver);
2120        assert_eq!(ty, Ty::Array(Box::new(Ty::Any)));
2121        assert!(diags.is_empty());
2122    }
2123}