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