Skip to main content

eventql_parser/typing/
analysis.rs

1use rustc_hash::FxHashMap;
2use serde::Serialize;
3use std::{borrow::Cow, collections::HashSet, mem};
4use unicase::Ascii;
5
6use crate::arena::Arena;
7use crate::typing::{Record, Type};
8use crate::{
9    App, Attrs, Binary, ExprRef, Field, Query, Raw, RecRef, Source, SourceKind, StrRef, Value,
10    error::AnalysisError, token::Operator,
11};
12
13/// Represents the state of a query that has been statically analyzed.
14///
15/// This type is used as a marker to indicate that a query has successfully
16/// passed static analysis. It contains metadata about the query's type
17/// information and variable scope after type checking.
18///
19/// All variables in a typed query are guaranteed to be:
20/// - Properly declared and in scope
21/// - Type-safe with sound type assignments
22#[derive(Debug, Clone, Serialize)]
23pub struct Typed {
24    /// The inferred type of the query's projection (PROJECT INTO clause).
25    ///
26    /// This represents the shape and types of the data that will be
27    /// returned by the query.
28    pub project: Type,
29
30    /// The variable scope after static analysis.
31    ///
32    /// Contains all variables that were in scope during type checking,
33    /// including bindings from FROM clauses and their associated types.
34    #[serde(skip)]
35    pub scope: Scope,
36
37    /// Indicates if the query uses aggregate functions.
38    pub aggregate: bool,
39}
40
41/// Result type for static analysis operations.
42///
43/// This is a convenience type alias for `Result<A, AnalysisError>` used throughout
44/// the static analysis module.
45pub type AnalysisResult<A> = Result<A, AnalysisError>;
46
47/// Configuration options for static analysis.
48///
49/// This structure contains the type information needed to perform static analysis
50/// on EventQL queries, including the default scope with built-in functions and
51/// the type information for event records.
52pub struct AnalysisOptions {
53    /// The default scope containing built-in functions and their type signatures.
54    pub default_scope: Scope,
55    /// Type information for event records being queried.
56    pub event_type_info: Type,
57    /// Custom types that are not defined in the EventQL reference.
58    ///
59    /// This set allows users to register custom type names that can be used
60    /// in type conversion expressions (e.g., `field AS CustomType`). Custom
61    /// type names are case-insensitive.
62    pub custom_types: HashSet<Ascii<String>>,
63}
64
65impl AnalysisOptions {
66    /// Adds a custom type name to the analysis options.
67    ///
68    /// Custom types allow you to use type conversion syntax with types that are
69    /// not part of the standard EventQL type system. The type name is stored
70    /// case-insensitively.
71    ///
72    /// # Arguments
73    ///
74    /// * `value` - The custom type name to register
75    ///
76    /// # Returns
77    ///
78    /// Returns `self` to allow for method chaining.
79    pub fn add_custom_type<'a>(mut self, value: impl Into<Cow<'a, str>>) -> Self {
80        match value.into() {
81            Cow::Borrowed(t) => self.custom_types.insert(Ascii::new(t.to_owned())),
82            Cow::Owned(t) => self.custom_types.insert(Ascii::new(t)),
83        };
84
85        self
86    }
87
88    /// Creates empty analysis options with no functions, no event type, and no custom types.
89    pub fn empty() -> Self {
90        Self {
91            default_scope: Scope::default(),
92            event_type_info: Type::default(),
93            custom_types: HashSet::default(),
94        }
95    }
96}
97
98/// Represents a variable scope during static analysis.
99///
100/// A scope tracks the variables and their types that are currently in scope
101/// during type checking. This is used to resolve variable references and
102/// ensure type correctness.
103#[derive(Default, Clone, Serialize, Debug)]
104pub struct Scope {
105    #[serde(skip_serializing)]
106    entries: FxHashMap<StrRef, Type>,
107}
108
109impl Scope {
110    /// Checks if the scope contains no entries.
111    pub fn is_empty(&self) -> bool {
112        self.entries.is_empty()
113    }
114
115    /// Declares a new variable binding in this scope.
116    ///
117    /// Returns `true` if the binding was newly inserted, or `false` if a binding
118    /// with the same name already existed (in which case the old value is replaced).
119    pub fn declare(&mut self, name: StrRef, tpe: Type) -> bool {
120        self.entries.insert(name, tpe).is_none()
121    }
122
123    /// Looks up the type of a variable by name.
124    ///
125    /// Returns `None` if the variable is not declared in this scope.
126    pub fn get(&self, name: StrRef) -> Option<Type> {
127        self.entries.get(&name).copied()
128    }
129
130    /// Returns a mutable reference to the type of a variable, allowing in-place updates.
131    ///
132    /// Returns `None` if the variable is not declared in this scope.
133    pub fn get_mut(&mut self, name: StrRef) -> Option<&mut Type> {
134        self.entries.get_mut(&name)
135    }
136
137    /// Returns `true` if a variable with the given name is declared in this scope.
138    pub fn exists(&self, name: StrRef) -> bool {
139        self.entries.contains_key(&name)
140    }
141}
142
143#[derive(Default)]
144struct CheckContext {
145    use_agg_func: bool,
146    use_source_based: bool,
147}
148
149/// Context for controlling analysis behavior.
150///
151/// This struct allows you to configure how expressions are analyzed,
152/// such as whether aggregate functions are allowed in the current context.
153#[derive(Default)]
154pub struct AnalysisContext {
155    /// Controls whether aggregate functions (like COUNT, SUM, AVG) are allowed
156    /// in the current analysis context.
157    ///
158    /// Set to `true` to allow aggregate functions, `false` to reject them.
159    /// Defaults to `false`.
160    pub allow_agg_func: bool,
161
162    /// Indicates if the query uses aggregate functions.
163    pub use_agg_funcs: bool,
164}
165
166/// A type checker and static analyzer for EventQL expressions.
167///
168/// This struct maintains the analysis state including scopes and type information.
169/// It can be used to perform type checking on individual expressions or entire queries.
170pub struct Analysis<'a> {
171    arena: &'a mut Arena,
172    /// The analysis options containing type information for functions and event types.
173    options: &'a AnalysisOptions,
174    /// Stack of previous scopes for nested scope handling.
175    prev_scopes: Vec<Scope>,
176    /// The current scope containing variable bindings and their types.
177    scope: Scope,
178}
179
180impl<'a> Analysis<'a> {
181    /// Creates a new analysis instance with the given options.
182    pub fn new(arena: &'a mut Arena, options: &'a AnalysisOptions) -> Self {
183        Self {
184            arena,
185            options,
186            prev_scopes: Default::default(),
187            scope: Scope::default(),
188        }
189    }
190
191    /// Returns a reference to the current scope.
192    ///
193    /// The scope contains variable bindings and their types for the current
194    /// analysis context. Note that this only includes local variable bindings
195    /// and does not include global definitions such as built-in functions
196    /// (e.g., `COUNT`, `NOW`) or event type information, which are stored
197    /// in the `AnalysisOptions`.
198    pub fn scope(&self) -> &Scope {
199        &self.scope
200    }
201
202    /// Returns a mutable reference to the current scope.
203    ///
204    /// This allows you to modify the scope by adding or removing variable bindings.
205    /// This is useful when you need to set up custom type environments before
206    /// analyzing expressions. Note that this only provides access to local variable
207    /// bindings; global definitions like built-in functions are managed through
208    /// `AnalysisOptions` and cannot be modified via the scope.
209    pub fn scope_mut(&mut self) -> &mut Scope {
210        &mut self.scope
211    }
212
213    fn enter_scope(&mut self) {
214        if self.scope.is_empty() {
215            return;
216        }
217
218        let prev = mem::take(&mut self.scope);
219        self.prev_scopes.push(prev);
220    }
221
222    fn exit_scope(&mut self) -> Scope {
223        if let Some(prev) = self.prev_scopes.pop() {
224            mem::replace(&mut self.scope, prev)
225        } else {
226            mem::take(&mut self.scope)
227        }
228    }
229
230    #[cfg(test)]
231    pub fn test_declare(&mut self, name: &str, tpe: Type) -> bool {
232        let name = self.arena.strings.alloc_no_case(name);
233        self.scope.declare(name, tpe)
234    }
235
236    /// Performs static analysis on a parsed query.
237    ///
238    /// This method analyzes an entire EventQL query, performing type checking on all
239    /// clauses including sources, predicates, group by, order by, and projections.
240    /// It returns a typed version of the query with type information attached.
241    ///
242    /// # Arguments
243    ///
244    /// * `query` - A parsed query in its raw (untyped) form
245    ///
246    /// # Returns
247    ///
248    /// Returns a typed query with all type information resolved, or an error if
249    /// type checking fails for any part of the query.
250    ///
251    /// # Example
252    ///
253    /// ```rust
254    /// use eventql_parser::Session;
255    ///
256    /// let mut session = Session::builder().build();
257    /// let query = session.parse("FROM e IN events WHERE [1,2,3] CONTAINS e.data.price PROJECT INTO e").unwrap();
258    ///
259    /// let typed_query = session.run_static_analysis(query);
260    /// assert!(typed_query.is_ok());
261    /// ```
262    pub fn analyze_query(&mut self, query: Query<Raw>) -> AnalysisResult<Query<Typed>> {
263        self.enter_scope();
264
265        let mut sources = Vec::with_capacity(query.sources.len());
266        let mut ctx = AnalysisContext::default();
267
268        for source in query.sources {
269            sources.push(self.analyze_source(source)?);
270        }
271
272        if let Some(expr) = query.predicate.as_ref().copied() {
273            self.analyze_expr(&mut ctx, expr, Type::Bool)?;
274        }
275
276        if let Some(group_by) = &query.group_by {
277            let node = self.arena.exprs.get(group_by.expr);
278            if !matches!(node.value, Value::Access(_)) {
279                return Err(AnalysisError::ExpectFieldLiteral(
280                    node.attrs.pos.line,
281                    node.attrs.pos.col,
282                ));
283            }
284
285            self.analyze_expr(&mut ctx, group_by.expr, Type::Unspecified)?;
286
287            if let Some(expr) = group_by.predicate.as_ref().copied() {
288                ctx.allow_agg_func = true;
289                ctx.use_agg_funcs = true;
290
291                self.analyze_expr(&mut ctx, expr, Type::Bool)?;
292
293                let node = self.arena.exprs.get(expr);
294                if !self.expect_agg_expr(expr)? {
295                    return Err(AnalysisError::ExpectAggExpr(
296                        node.attrs.pos.line,
297                        node.attrs.pos.col,
298                    ));
299                }
300            }
301
302            ctx.allow_agg_func = true;
303            ctx.use_agg_funcs = true;
304        }
305
306        let project = self.analyze_projection(&mut ctx, query.projection)?;
307
308        if let Some(order_by) = &query.order_by {
309            self.analyze_expr(&mut ctx, order_by.expr, Type::Unspecified)?;
310            let node = self.arena.exprs.get(order_by.expr);
311            if query.group_by.is_none() && !matches!(node.value, Value::Access(_)) {
312                return Err(AnalysisError::ExpectFieldLiteral(
313                    node.attrs.pos.line,
314                    node.attrs.pos.col,
315                ));
316            } else if query.group_by.is_some() {
317                self.expect_agg_func(order_by.expr)?;
318            }
319        }
320
321        let scope = self.exit_scope();
322
323        Ok(Query {
324            attrs: query.attrs,
325            sources,
326            predicate: query.predicate,
327            group_by: query.group_by,
328            order_by: query.order_by,
329            limit: query.limit,
330            projection: query.projection,
331            distinct: query.distinct,
332            meta: Typed {
333                project,
334                scope,
335                aggregate: ctx.use_agg_funcs,
336            },
337        })
338    }
339
340    fn analyze_source(&mut self, source: Source<Raw>) -> AnalysisResult<Source<Typed>> {
341        let kind = self.analyze_source_kind(source.kind)?;
342        let tpe = match &kind {
343            SourceKind::Name(_) | SourceKind::Subject(_) => {
344                self.arena.types.alloc_type(self.options.event_type_info)
345            }
346            SourceKind::Subquery(query) => self.projection_type(query),
347        };
348
349        if !self.scope.declare(source.binding.name, tpe) {
350            return Err(AnalysisError::BindingAlreadyExists(
351                source.binding.pos.line,
352                source.binding.pos.col,
353                self.arena.strings.get(source.binding.name).to_owned(),
354            ));
355        }
356
357        Ok(Source {
358            binding: source.binding,
359            kind,
360        })
361    }
362
363    fn analyze_source_kind(&mut self, kind: SourceKind<Raw>) -> AnalysisResult<SourceKind<Typed>> {
364        match kind {
365            SourceKind::Name(n) => Ok(SourceKind::Name(n)),
366            SourceKind::Subject(s) => Ok(SourceKind::Subject(s)),
367            SourceKind::Subquery(query) => {
368                let query = self.analyze_query(*query)?;
369                Ok(SourceKind::Subquery(Box::new(query)))
370            }
371        }
372    }
373
374    fn analyze_projection(
375        &mut self,
376        ctx: &mut AnalysisContext,
377        expr: ExprRef,
378    ) -> AnalysisResult<Type> {
379        let node = self.arena.exprs.get(expr);
380        match node.value {
381            Value::Record(record) => {
382                if self.arena.exprs.rec(record).is_empty() {
383                    return Err(AnalysisError::EmptyRecord(
384                        node.attrs.pos.line,
385                        node.attrs.pos.col,
386                    ));
387                }
388
389                ctx.allow_agg_func = true;
390                let tpe = self.analyze_expr(ctx, expr, Type::Unspecified)?;
391                let mut chk_ctx = CheckContext {
392                    use_agg_func: ctx.use_agg_funcs,
393                    ..Default::default()
394                };
395
396                self.check_projection_on_record(&mut chk_ctx, record)?;
397                Ok(tpe)
398            }
399
400            Value::App(app) => {
401                ctx.allow_agg_func = true;
402
403                let tpe = self.analyze_expr(ctx, expr, Type::Unspecified)?;
404
405                if ctx.use_agg_funcs {
406                    let mut chk_ctx = CheckContext {
407                        use_agg_func: ctx.use_agg_funcs,
408                        ..Default::default()
409                    };
410
411                    self.check_projection_on_field_expr(&mut chk_ctx, expr)?;
412                } else {
413                    self.reject_constant_func(node.attrs, &app)?;
414                }
415
416                Ok(tpe)
417            }
418
419            Value::Id(_) if ctx.use_agg_funcs => Err(AnalysisError::ExpectAggExpr(
420                node.attrs.pos.line,
421                node.attrs.pos.col,
422            )),
423
424            Value::Id(id) => {
425                if let Some(tpe) = self.scope.get(id) {
426                    Ok(tpe)
427                } else {
428                    Err(AnalysisError::VariableUndeclared(
429                        node.attrs.pos.line,
430                        node.attrs.pos.col,
431                        self.arena.strings.get(id).to_owned(),
432                    ))
433                }
434            }
435
436            Value::Access(_) if ctx.use_agg_funcs => Err(AnalysisError::ExpectAggExpr(
437                node.attrs.pos.line,
438                node.attrs.pos.col,
439            )),
440
441            Value::Access(access) => {
442                let mut current = self.arena.exprs.get(access.target);
443
444                loop {
445                    match current.value {
446                        Value::Id(name) => {
447                            if !self.scope.exists(name) {
448                                return Err(AnalysisError::VariableUndeclared(
449                                    current.attrs.pos.line,
450                                    current.attrs.pos.col,
451                                    self.arena.strings.get(name).to_owned(),
452                                ));
453                            }
454
455                            break;
456                        }
457
458                        Value::Access(next) => current = self.arena.exprs.get(next.target),
459                        _ => unreachable!(),
460                    }
461                }
462
463                self.analyze_expr(ctx, expr, Type::Unspecified)
464            }
465
466            _ => {
467                let tpe = self.project_type(expr);
468
469                Err(AnalysisError::ExpectRecordOrSourcedProperty(
470                    node.attrs.pos.line,
471                    node.attrs.pos.col,
472                    display_type(self.arena, tpe),
473                ))
474            }
475        }
476    }
477
478    fn check_projection_on_record(
479        &mut self,
480        ctx: &mut CheckContext,
481        record: RecRef,
482    ) -> AnalysisResult<()> {
483        for idx in 0..self.arena.exprs.rec(record).len() {
484            let field = self.arena.exprs.rec_get(record, idx);
485
486            self.check_projection_on_field(ctx, &field)?;
487        }
488
489        Ok(())
490    }
491
492    fn check_projection_on_field(
493        &mut self,
494        ctx: &mut CheckContext,
495        field: &Field,
496    ) -> AnalysisResult<()> {
497        self.check_projection_on_field_expr(ctx, field.expr)
498    }
499
500    fn check_projection_on_field_expr(
501        &mut self,
502        ctx: &mut CheckContext,
503        expr: ExprRef,
504    ) -> AnalysisResult<()> {
505        let node = self.arena.exprs.get(expr);
506        match node.value {
507            Value::Number(_) | Value::String(_) | Value::Bool(_) => Ok(()),
508
509            Value::Id(id) => {
510                if self.scope.exists(id) {
511                    if ctx.use_agg_func {
512                        return Err(AnalysisError::UnallowedAggFuncUsageWithSrcField(
513                            node.attrs.pos.line,
514                            node.attrs.pos.col,
515                        ));
516                    }
517
518                    ctx.use_source_based = true;
519                }
520
521                Ok(())
522            }
523
524            Value::Array(exprs) => {
525                for idx in self.arena.exprs.vec_idxes(exprs) {
526                    let expr = self.arena.exprs.vec_get(exprs, idx);
527
528                    self.check_projection_on_field_expr(ctx, expr)?;
529                }
530
531                Ok(())
532            }
533
534            Value::Record(fields) => {
535                for idx in self.arena.exprs.rec_idxes(fields) {
536                    let field = self.arena.exprs.rec_get(fields, idx);
537
538                    self.check_projection_on_field(ctx, &field)?;
539                }
540
541                Ok(())
542            }
543
544            Value::Access(access) => self.check_projection_on_field_expr(ctx, access.target),
545
546            Value::App(app) => {
547                if let Some(Type::App { aggregate, .. }) = self.options.default_scope.get(app.func)
548                {
549                    ctx.use_agg_func |= aggregate;
550
551                    if ctx.use_agg_func && ctx.use_source_based {
552                        return Err(AnalysisError::UnallowedAggFuncUsageWithSrcField(
553                            node.attrs.pos.line,
554                            node.attrs.pos.col,
555                        ));
556                    }
557
558                    if aggregate {
559                        return self.expect_agg_func(expr);
560                    }
561
562                    for idx in self.arena.exprs.vec_idxes(app.args) {
563                        let arg = self.arena.exprs.vec_get(app.args, idx);
564
565                        self.invalidate_agg_func_usage(arg)?;
566                    }
567                }
568
569                Ok(())
570            }
571
572            Value::Binary(binary) => {
573                self.check_projection_on_field_expr(ctx, binary.lhs)?;
574                self.check_projection_on_field_expr(ctx, binary.rhs)
575            }
576
577            Value::Unary(unary) => self.check_projection_on_field_expr(ctx, unary.expr),
578            Value::Group(expr) => self.check_projection_on_field_expr(ctx, expr),
579        }
580    }
581
582    fn expect_agg_func(&self, expr: ExprRef) -> AnalysisResult<()> {
583        let node = self.arena.exprs.get(expr);
584        if let Value::App(app) = node.value
585            && let Some(Type::App {
586                aggregate: true, ..
587            }) = self.options.default_scope.get(app.func)
588        {
589            for idx in 0..self.arena.exprs.vec(app.args).len() {
590                let arg = self.arena.exprs.vec_get(app.args, idx);
591
592                self.ensure_agg_param_is_source_bound(arg)?;
593                self.invalidate_agg_func_usage(arg)?;
594            }
595
596            return Ok(());
597        }
598
599        Err(AnalysisError::ExpectAggExpr(
600            node.attrs.pos.line,
601            node.attrs.pos.col,
602        ))
603    }
604
605    fn expect_agg_expr(&self, expr: ExprRef) -> AnalysisResult<bool> {
606        let node = self.arena.exprs.get(expr);
607        match node.value {
608            Value::Id(id) => {
609                if self.scope.exists(id) {
610                    return Err(AnalysisError::UnallowedAggFuncUsageWithSrcField(
611                        node.attrs.pos.line,
612                        node.attrs.pos.col,
613                    ));
614                }
615
616                Ok(false)
617            }
618            Value::Group(expr) => self.expect_agg_expr(expr),
619            Value::Binary(binary) => {
620                let lhs = self.expect_agg_expr(binary.lhs)?;
621                let rhs = self.expect_agg_expr(binary.rhs)?;
622
623                if !lhs && !rhs {
624                    return Err(AnalysisError::ExpectAggExpr(
625                        node.attrs.pos.line,
626                        node.attrs.pos.col,
627                    ));
628                }
629
630                Ok(true)
631            }
632            Value::Unary(unary) => self.expect_agg_expr(unary.expr),
633            Value::App(_) => {
634                self.expect_agg_func(expr)?;
635                Ok(true)
636            }
637
638            _ => Ok(false),
639        }
640    }
641
642    fn ensure_agg_param_is_source_bound(&self, expr: ExprRef) -> AnalysisResult<()> {
643        let node = self.arena.exprs.get(expr);
644        match node.value {
645            Value::Id(id) if !self.options.default_scope.exists(id) => Ok(()),
646            Value::Access(access) => self.ensure_agg_param_is_source_bound(access.target),
647            Value::Binary(binary) => self.ensure_agg_binary_op_is_source_bound(node.attrs, binary),
648            Value::Unary(unary) => self.ensure_agg_param_is_source_bound(unary.expr),
649
650            _ => Err(AnalysisError::ExpectSourceBoundProperty(
651                node.attrs.pos.line,
652                node.attrs.pos.col,
653            )),
654        }
655    }
656
657    fn ensure_agg_binary_op_is_source_bound(
658        &self,
659        attrs: Attrs,
660        binary: Binary,
661    ) -> AnalysisResult<()> {
662        if !self.ensure_agg_binary_op_branch_is_source_bound(binary.lhs)
663            && !self.ensure_agg_binary_op_branch_is_source_bound(binary.rhs)
664        {
665            return Err(AnalysisError::ExpectSourceBoundProperty(
666                attrs.pos.line,
667                attrs.pos.col,
668            ));
669        }
670
671        Ok(())
672    }
673
674    fn ensure_agg_binary_op_branch_is_source_bound(&self, expr: ExprRef) -> bool {
675        let node = self.arena.exprs.get(expr);
676        match node.value {
677            Value::Id(id) => !self.options.default_scope.exists(id),
678            Value::Array(exprs) => {
679                if self.arena.exprs.vec(exprs).is_empty() {
680                    return false;
681                }
682
683                for idx in 0..self.arena.exprs.vec(exprs).len() {
684                    let expr = self.arena.exprs.vec_get(exprs, idx);
685
686                    if !self.ensure_agg_binary_op_branch_is_source_bound(expr) {
687                        return false;
688                    }
689                }
690
691                true
692            }
693            Value::Record(fields) => {
694                if self.arena.exprs.rec(fields).is_empty() {
695                    return false;
696                }
697
698                for idx in 0..self.arena.exprs.rec(fields).len() {
699                    let field = self.arena.exprs.rec_get(fields, idx);
700
701                    if !self.ensure_agg_binary_op_branch_is_source_bound(field.expr) {
702                        return false;
703                    }
704                }
705
706                true
707            }
708
709            Value::Access(access) => {
710                self.ensure_agg_binary_op_branch_is_source_bound(access.target)
711            }
712
713            Value::Binary(binary) => self
714                .ensure_agg_binary_op_is_source_bound(node.attrs, binary)
715                .is_ok(),
716            Value::Unary(unary) => self.ensure_agg_binary_op_branch_is_source_bound(unary.expr),
717            Value::Group(expr) => self.ensure_agg_binary_op_branch_is_source_bound(expr),
718
719            Value::Number(_) | Value::String(_) | Value::Bool(_) | Value::App(_) => false,
720        }
721    }
722
723    fn invalidate_agg_func_usage(&self, expr: ExprRef) -> AnalysisResult<()> {
724        let node = self.arena.exprs.get(expr);
725        match node.value {
726            Value::Number(_)
727            | Value::String(_)
728            | Value::Bool(_)
729            | Value::Id(_)
730            | Value::Access(_) => Ok(()),
731
732            Value::Array(exprs) => {
733                for idx in 0..self.arena.exprs.vec(exprs).len() {
734                    let expr = self.arena.exprs.vec_get(exprs, idx);
735
736                    self.invalidate_agg_func_usage(expr)?;
737                }
738
739                Ok(())
740            }
741
742            Value::Record(fields) => {
743                for idx in 0..self.arena.exprs.rec(fields).len() {
744                    let field = self.arena.exprs.rec_get(fields, idx);
745
746                    self.invalidate_agg_func_usage(field.expr)?;
747                }
748
749                Ok(())
750            }
751
752            Value::App(app) => {
753                if let Some(Type::App { aggregate, .. }) = self.options.default_scope.get(app.func)
754                    && aggregate
755                {
756                    return Err(AnalysisError::WrongAggFunUsage(
757                        node.attrs.pos.line,
758                        node.attrs.pos.col,
759                        self.arena.strings.get(app.func).to_owned(),
760                    ));
761                }
762
763                for idx in 0..self.arena.exprs.vec(app.args).len() {
764                    let arg = self.arena.exprs.vec_get(app.args, idx);
765                    self.invalidate_agg_func_usage(arg)?;
766                }
767
768                Ok(())
769            }
770
771            Value::Binary(binary) => {
772                self.invalidate_agg_func_usage(binary.lhs)?;
773                self.invalidate_agg_func_usage(binary.rhs)
774            }
775
776            Value::Unary(unary) => self.invalidate_agg_func_usage(unary.expr),
777            Value::Group(expr) => self.invalidate_agg_func_usage(expr),
778        }
779    }
780
781    fn reject_constant_func(&self, attrs: Attrs, app: &App) -> AnalysisResult<()> {
782        if self.arena.exprs.vec(app.args).is_empty() {
783            return Err(AnalysisError::ConstantExprInProjectIntoClause(
784                attrs.pos.line,
785                attrs.pos.col,
786            ));
787        }
788
789        let mut errored = None;
790        for idx in 0..self.arena.exprs.vec(app.args).len() {
791            let arg = self.arena.exprs.vec_get(app.args, idx);
792
793            if let Err(e) = self.reject_constant_expr(arg) {
794                if errored.is_none() {
795                    errored = Some(e);
796                }
797
798                continue;
799            }
800
801            // if at least one arg is sourced-bound is ok
802            return Ok(());
803        }
804
805        Err(errored.expect("to be defined at that point"))
806    }
807
808    fn reject_constant_expr(&self, expr: ExprRef) -> AnalysisResult<()> {
809        let node = self.arena.exprs.get(expr);
810        match node.value {
811            Value::Id(id) if self.scope.exists(id) => Ok(()),
812            Value::Array(exprs) => {
813                let mut errored = None;
814                for idx in 0..self.arena.exprs.vec(exprs).len() {
815                    let expr = self.arena.exprs.vec_get(exprs, idx);
816
817                    if let Err(e) = self.reject_constant_expr(expr) {
818                        if errored.is_none() {
819                            errored = Some(e);
820                        }
821
822                        continue;
823                    }
824
825                    // if at least one arg is sourced-bound is ok
826                    return Ok(());
827                }
828
829                Err(errored.expect("to be defined at that point"))
830            }
831
832            Value::Record(fields) => {
833                let mut errored = None;
834                for idx in 0..self.arena.exprs.rec(fields).len() {
835                    let field = self.arena.exprs.rec_get(fields, idx);
836
837                    if let Err(e) = self.reject_constant_expr(field.expr) {
838                        if errored.is_none() {
839                            errored = Some(e);
840                        }
841
842                        continue;
843                    }
844
845                    // if at least one arg is sourced-bound is ok
846                    return Ok(());
847                }
848
849                Err(errored.expect("to be defined at that point"))
850            }
851
852            Value::Binary(binary) => self
853                .reject_constant_expr(binary.lhs)
854                .or_else(|e| self.reject_constant_expr(binary.rhs).map_err(|_| e)),
855
856            Value::Access(access) => self.reject_constant_expr(access.target),
857            Value::App(app) => self.reject_constant_func(node.attrs, &app),
858            Value::Unary(unary) => self.reject_constant_expr(unary.expr),
859            Value::Group(expr) => self.reject_constant_expr(expr),
860
861            _ => Err(AnalysisError::ConstantExprInProjectIntoClause(
862                node.attrs.pos.line,
863                node.attrs.pos.col,
864            )),
865        }
866    }
867
868    /// Analyzes an expression and checks it against an expected type.
869    ///
870    /// This method performs type checking on an expression, verifying that all operations
871    /// are type-safe and that the expression's type is compatible with the expected type.
872    ///
873    /// # Arguments
874    ///
875    /// * `ctx` - The analysis context controlling analysis behavior
876    /// * `expr` - The expression to analyze
877    /// * `expect` - The expected type of the expression
878    ///
879    /// # Returns
880    ///
881    /// Returns the actual type of the expression after checking compatibility with the expected type,
882    /// or an error if type checking fails.
883    ///
884    /// # Example
885    ///
886    /// ```rust
887    /// use eventql_parser::Session;
888    ///
889    /// let mut session = Session::builder().build();
890    /// let query = session.parse("FROM e IN events PROJECT INTO { price: 1 + 2 }").unwrap();
891    ///
892    /// let result = session.run_static_analysis(query);
893    /// assert!(result.is_ok());
894    /// ```
895    pub fn analyze_expr(
896        &mut self,
897        ctx: &mut AnalysisContext,
898        expr: ExprRef,
899        mut expect: Type,
900    ) -> AnalysisResult<Type> {
901        let node = self.arena.exprs.get(expr);
902        match node.value {
903            Value::Number(_) => self.arena.type_check(node.attrs, expect, Type::Number),
904            Value::String(_) => self.arena.type_check(node.attrs, expect, Type::String),
905            Value::Bool(_) => self.arena.type_check(node.attrs, expect, Type::Bool),
906
907            Value::Id(id) => {
908                if let Some(tpe) = self.options.default_scope.get(id) {
909                    self.arena.type_check(node.attrs, expect, tpe)
910                } else if let Some(tpe) = self.scope.get_mut(id) {
911                    *tpe = self.arena.type_check(node.attrs, mem::take(tpe), expect)?;
912
913                    Ok(*tpe)
914                } else {
915                    Err(AnalysisError::VariableUndeclared(
916                        node.attrs.pos.line,
917                        node.attrs.pos.col,
918                        self.arena.strings.get(id).to_owned(),
919                    ))
920                }
921            }
922
923            Value::Array(exprs) => {
924                if matches!(expect, Type::Unspecified) {
925                    for idx in self.arena.exprs.vec_idxes(exprs) {
926                        let expr = self.arena.exprs.vec_get(exprs, idx);
927
928                        expect = self.analyze_expr(ctx, expr, expect)?;
929                    }
930
931                    return Ok(self.arena.types.alloc_array_of(expect));
932                }
933
934                match expect {
935                    Type::Array(expect) => {
936                        let mut expect = self.arena.types.get_type(expect);
937                        for idx in 0..self.arena.exprs.vec(exprs).len() {
938                            let expr = self.arena.exprs.vec_get(exprs, idx);
939                            expect = self.analyze_expr(ctx, expr, expect)?;
940                        }
941
942                        Ok(self.arena.types.alloc_array_of(expect))
943                    }
944
945                    expect => {
946                        let tpe = self.project_type(expr);
947
948                        Err(AnalysisError::TypeMismatch(
949                            node.attrs.pos.line,
950                            node.attrs.pos.col,
951                            display_type(self.arena, expect),
952                            display_type(self.arena, tpe),
953                        ))
954                    }
955                }
956            }
957
958            Value::Record(fields) => {
959                if matches!(expect, Type::Unspecified) {
960                    let mut record = FxHashMap::default();
961
962                    for idx in 0..self.arena.exprs.rec(fields).len() {
963                        let field = self.arena.exprs.rec_get(fields, idx);
964
965                        record.insert(
966                            field.name,
967                            self.analyze_expr(ctx, field.expr, Type::Unspecified)?,
968                        );
969                    }
970
971                    return Ok(Type::Record(self.arena.types.alloc_record(record)));
972                }
973
974                if let Type::Record(rec) = expect
975                    && self.arena.types.record_len(rec) == self.arena.exprs.rec(fields).len()
976                {
977                    for idx in self.arena.exprs.rec_idxes(fields) {
978                        let field = self.arena.exprs.rec_get(fields, idx);
979
980                        if let Some(tpe) = self.arena.types.record_get(rec, field.name) {
981                            let new_tpe = self.analyze_expr(ctx, field.expr, tpe)?;
982                            self.arena.types.record_set(rec, field.name, new_tpe);
983                            continue;
984                        }
985
986                        return Err(AnalysisError::FieldUndeclared(
987                            field.attrs.pos.line,
988                            field.attrs.pos.col,
989                            self.arena.strings.get(field.name).to_owned(),
990                        ));
991                    }
992
993                    return Ok(expect);
994                }
995
996                let tpe = self.project_type(expr);
997
998                Err(AnalysisError::TypeMismatch(
999                    node.attrs.pos.line,
1000                    node.attrs.pos.col,
1001                    display_type(self.arena, expect),
1002                    display_type(self.arena, tpe),
1003                ))
1004            }
1005
1006            Value::Access(_) => Ok(self.analyze_access(node.attrs, expr, expect)?),
1007
1008            Value::App(app) => {
1009                if let Some(tpe) = self.options.default_scope.get(app.func)
1010                    && let Type::App {
1011                        args,
1012                        result,
1013                        aggregate,
1014                    } = tpe
1015                {
1016                    let args_actual_len = self.arena.exprs.vec(app.args).len();
1017                    let args_decl_len = self.arena.types.get_args(args.values).len();
1018
1019                    if !(args_actual_len >= args.needed && args_actual_len <= args_decl_len) {
1020                        return Err(AnalysisError::FunWrongArgumentCount(
1021                            node.attrs.pos.line,
1022                            node.attrs.pos.col,
1023                            self.arena.strings.get(app.func).to_owned(),
1024                        ));
1025                    }
1026
1027                    if aggregate && !ctx.allow_agg_func {
1028                        return Err(AnalysisError::WrongAggFunUsage(
1029                            node.attrs.pos.line,
1030                            node.attrs.pos.col,
1031                            self.arena.strings.get(app.func).to_owned(),
1032                        ));
1033                    }
1034
1035                    if aggregate && ctx.allow_agg_func {
1036                        ctx.use_agg_funcs = true;
1037                    }
1038
1039                    let arg_types = self.arena.types.args_idxes(args.values);
1040                    let args_idxes = self.arena.exprs.vec_idxes(app.args);
1041                    for (val_idx, tpe_idx) in args_idxes.zip(arg_types) {
1042                        let arg = self.arena.exprs.vec_get(app.args, val_idx);
1043                        let tpe = self.arena.types.args_get(args.values, tpe_idx);
1044
1045                        self.analyze_expr(ctx, arg, tpe)?;
1046                    }
1047
1048                    if matches!(expect, Type::Unspecified) {
1049                        Ok(self.arena.types.get_type(result))
1050                    } else {
1051                        self.arena
1052                            .type_check(node.attrs, expect, self.arena.types.get_type(result))
1053                    }
1054                } else {
1055                    Err(AnalysisError::FuncUndeclared(
1056                        node.attrs.pos.line,
1057                        node.attrs.pos.col,
1058                        self.arena.strings.get(app.func).to_owned(),
1059                    ))
1060                }
1061            }
1062
1063            Value::Binary(binary) => match binary.operator {
1064                Operator::Add | Operator::Sub | Operator::Mul | Operator::Div => {
1065                    self.analyze_expr(ctx, binary.lhs, Type::Number)?;
1066                    self.analyze_expr(ctx, binary.rhs, Type::Number)?;
1067                    self.arena.type_check(node.attrs, expect, Type::Number)
1068                }
1069
1070                Operator::Eq
1071                | Operator::Neq
1072                | Operator::Lt
1073                | Operator::Lte
1074                | Operator::Gt
1075                | Operator::Gte => {
1076                    let lhs_expect = self.analyze_expr(ctx, binary.lhs, Type::Unspecified)?;
1077                    let rhs_expect = self.analyze_expr(ctx, binary.rhs, lhs_expect)?;
1078
1079                    // If the left side didn't have enough type information while the other did,
1080                    // we replay another typecheck pass on the left side if the right side was conclusive
1081                    if matches!(lhs_expect, Type::Unspecified)
1082                        && !matches!(rhs_expect, Type::Unspecified)
1083                    {
1084                        self.analyze_expr(ctx, binary.lhs, rhs_expect)?;
1085                    }
1086
1087                    self.arena.type_check(node.attrs, expect, Type::Bool)
1088                }
1089
1090                Operator::Contains => {
1091                    let new_expect = self.arena.types.alloc_array_of(Type::Unspecified);
1092                    let lhs_expect = self.analyze_expr(ctx, binary.lhs, new_expect)?;
1093
1094                    let lhs_assumption = match lhs_expect {
1095                        Type::Array(inner) => self.arena.types.get_type(inner),
1096                        other => {
1097                            return Err(AnalysisError::ExpectArray(
1098                                node.attrs.pos.line,
1099                                node.attrs.pos.col,
1100                                display_type(self.arena, other),
1101                            ));
1102                        }
1103                    };
1104
1105                    let rhs_expect = self.analyze_expr(ctx, binary.rhs, lhs_assumption)?;
1106
1107                    // If the left side didn't have enough type information while the other did,
1108                    // we replay another typecheck pass on the left side if the right side was conclusive
1109                    if matches!(lhs_assumption, Type::Unspecified)
1110                        && !matches!(rhs_expect, Type::Unspecified)
1111                    {
1112                        let new_expect = self.arena.types.alloc_array_of(rhs_expect);
1113                        self.analyze_expr(ctx, binary.lhs, new_expect)?;
1114                    }
1115
1116                    self.arena.type_check(node.attrs, expect, Type::Bool)
1117                }
1118
1119                Operator::And | Operator::Or | Operator::Xor => {
1120                    self.analyze_expr(ctx, binary.lhs, Type::Bool)?;
1121                    self.analyze_expr(ctx, binary.rhs, Type::Bool)?;
1122                    self.arena.type_check(node.attrs, expect, Type::Bool)
1123                }
1124
1125                Operator::As => {
1126                    let rhs = self.arena.exprs.get(binary.rhs);
1127                    if let Value::Id(name) = rhs.value {
1128                        return if let Some(tpe) = name_to_type(self.arena, self.options, name) {
1129                            // NOTE - we could check if it's safe to convert the left branch to that type
1130                            Ok(tpe)
1131                        } else {
1132                            Err(AnalysisError::UnsupportedCustomType(
1133                                rhs.attrs.pos.line,
1134                                rhs.attrs.pos.col,
1135                                self.arena.strings.get(name).to_owned(),
1136                            ))
1137                        };
1138                    }
1139
1140                    unreachable!(
1141                        "we already made sure during parsing that we can only have an ID symbol at this point"
1142                    )
1143                }
1144
1145                Operator::Not => unreachable!(),
1146            },
1147
1148            Value::Unary(unary) => match unary.operator {
1149                Operator::Add | Operator::Sub => {
1150                    self.analyze_expr(ctx, unary.expr, Type::Number)?;
1151                    self.arena.type_check(node.attrs, expect, Type::Number)
1152                }
1153
1154                Operator::Not => {
1155                    self.analyze_expr(ctx, unary.expr, Type::Bool)?;
1156                    self.arena.type_check(node.attrs, expect, Type::Bool)
1157                }
1158
1159                _ => unreachable!(),
1160            },
1161
1162            Value::Group(expr) => Ok(self.analyze_expr(ctx, expr, expect)?),
1163        }
1164    }
1165
1166    fn analyze_access(
1167        &mut self,
1168        attrs: Attrs,
1169        access: ExprRef,
1170        expect: Type,
1171    ) -> AnalysisResult<Type> {
1172        struct State {
1173            depth: u8,
1174            /// When true means we are into dynamically type object.
1175            dynamic: bool,
1176            definition: Def,
1177        }
1178
1179        impl State {
1180            fn new(definition: Def) -> Self {
1181                Self {
1182                    depth: 0,
1183                    dynamic: false,
1184                    definition,
1185                }
1186            }
1187        }
1188
1189        #[derive(Copy, Clone)]
1190        struct Parent {
1191            record: Record,
1192            field: Option<StrRef>,
1193        }
1194
1195        enum Def {
1196            User { parent: Parent, tpe: Type },
1197            System(Type),
1198        }
1199
1200        fn go<'global>(
1201            scope: &mut Scope,
1202            arena: &'global mut Arena,
1203            sys: &'global AnalysisOptions,
1204            expr: ExprRef,
1205        ) -> AnalysisResult<State> {
1206            let node = arena.exprs.get(expr);
1207            match node.value {
1208                Value::Id(id) => {
1209                    if let Some(tpe) = sys.default_scope.get(id) {
1210                        if matches!(tpe, Type::Record(_)) {
1211                            Ok(State::new(Def::System(tpe)))
1212                        } else {
1213                            Err(AnalysisError::ExpectRecord(
1214                                node.attrs.pos.line,
1215                                node.attrs.pos.col,
1216                                display_type(arena, tpe),
1217                            ))
1218                        }
1219                    } else if let Some(tpe) = scope.get_mut(id) {
1220                        if matches!(tpe, Type::Unspecified) {
1221                            let record = arena.types.instantiate_record();
1222                            *tpe = Type::Record(record);
1223
1224                            Ok(State::new(Def::User {
1225                                parent: Parent {
1226                                    record,
1227                                    field: None,
1228                                },
1229                                tpe: Type::Record(record),
1230                            }))
1231                        } else if let Type::Record(record) = *tpe {
1232                            Ok(State::new(Def::User {
1233                                parent: Parent {
1234                                    record,
1235                                    field: None,
1236                                },
1237                                tpe: *tpe,
1238                            }))
1239                        } else {
1240                            Err(AnalysisError::ExpectRecord(
1241                                node.attrs.pos.line,
1242                                node.attrs.pos.col,
1243                                display_type(arena, *tpe),
1244                            ))
1245                        }
1246                    } else {
1247                        Err(AnalysisError::VariableUndeclared(
1248                            node.attrs.pos.line,
1249                            node.attrs.pos.col,
1250                            arena.strings.get(id).to_owned(),
1251                        ))
1252                    }
1253                }
1254                Value::Access(access) => {
1255                    let mut state = go(scope, arena, sys, access.target)?;
1256
1257                    // TODO - we should consider make that field and depth configurable.
1258                    let is_data_field =
1259                        state.depth == 0 && arena.strings.get(access.field) == "data";
1260
1261                    // TODO - we should consider make that behavior configurable.
1262                    // the `data` property is where the JSON payload is located, which means
1263                    // we should be lax if a property is not defined yet.
1264                    if !state.dynamic && is_data_field {
1265                        state.dynamic = true;
1266                    }
1267
1268                    match state.definition {
1269                        Def::User { parent, tpe } => {
1270                            if matches!(tpe, Type::Unspecified) && state.dynamic {
1271                                let record = arena.types.instantiate_record();
1272                                arena
1273                                    .types
1274                                    .record_set(record, access.field, Type::Unspecified);
1275
1276                                // TODO - this is impossible. Should return a proper error instead of panicking
1277                                if let Some(field) = parent.field {
1278                                    arena.types.record_set(
1279                                        parent.record,
1280                                        field,
1281                                        Type::Record(record),
1282                                    );
1283                                }
1284
1285                                return Ok(State {
1286                                    depth: state.depth + 1,
1287                                    definition: Def::User {
1288                                        parent: Parent {
1289                                            record,
1290                                            field: Some(access.field),
1291                                        },
1292                                        tpe: Type::Unspecified,
1293                                    },
1294                                    ..state
1295                                });
1296                            } else if let Type::Record(record) = tpe {
1297                                return if let Some(tpe) =
1298                                    arena.types.record_get(record, access.field)
1299                                {
1300                                    Ok(State {
1301                                        depth: state.depth + 1,
1302                                        definition: Def::User {
1303                                            parent: Parent {
1304                                                record,
1305                                                field: Some(access.field),
1306                                            },
1307                                            tpe,
1308                                        },
1309                                        ..state
1310                                    })
1311                                } else {
1312                                    // TODO - that test seems useless because it can't be the data field and not be dynamic
1313                                    if state.dynamic || is_data_field {
1314                                        arena.types.record_set(
1315                                            record,
1316                                            access.field,
1317                                            Type::Unspecified,
1318                                        );
1319                                        return Ok(State {
1320                                            depth: state.depth + 1,
1321                                            definition: Def::User {
1322                                                parent: Parent {
1323                                                    record,
1324                                                    field: Some(access.field),
1325                                                },
1326                                                tpe: Type::Unspecified,
1327                                            },
1328                                            ..state
1329                                        });
1330                                    }
1331
1332                                    Err(AnalysisError::FieldUndeclared(
1333                                        node.attrs.pos.line,
1334                                        node.attrs.pos.col,
1335                                        arena.strings.get(access.field).to_owned(),
1336                                    ))
1337                                };
1338                            }
1339
1340                            Err(AnalysisError::ExpectRecord(
1341                                node.attrs.pos.line,
1342                                node.attrs.pos.col,
1343                                display_type(arena, tpe),
1344                            ))
1345                        }
1346
1347                        Def::System(tpe) => {
1348                            if matches!(tpe, Type::Unspecified) && state.dynamic {
1349                                return Ok(State {
1350                                    depth: state.depth + 1,
1351                                    definition: Def::System(Type::Unspecified),
1352                                    ..state
1353                                });
1354                            }
1355
1356                            if let Type::Record(rec) = tpe {
1357                                if let Some(field) = arena.types.record_get(rec, access.field) {
1358                                    return Ok(State {
1359                                        depth: state.depth + 1,
1360                                        definition: Def::System(field),
1361                                        ..state
1362                                    });
1363                                }
1364
1365                                return Err(AnalysisError::FieldUndeclared(
1366                                    node.attrs.pos.line,
1367                                    node.attrs.pos.col,
1368                                    arena.strings.get(access.field).to_owned(),
1369                                ));
1370                            }
1371
1372                            Err(AnalysisError::ExpectRecord(
1373                                node.attrs.pos.line,
1374                                node.attrs.pos.col,
1375                                display_type(arena, tpe),
1376                            ))
1377                        }
1378                    }
1379                }
1380                Value::Number(_)
1381                | Value::String(_)
1382                | Value::Bool(_)
1383                | Value::Array(_)
1384                | Value::Record(_)
1385                | Value::App(_)
1386                | Value::Binary(_)
1387                | Value::Unary(_)
1388                | Value::Group(_) => unreachable!(),
1389            }
1390        }
1391
1392        let state = go(&mut self.scope, self.arena, self.options, access)?;
1393
1394        match state.definition {
1395            Def::User { parent, tpe } => {
1396                let new_tpe = self.arena.type_check(attrs, tpe, expect)?;
1397
1398                if let Some(field) = parent.field {
1399                    self.arena.types.record_set(parent.record, field, new_tpe);
1400                }
1401
1402                Ok(new_tpe)
1403            }
1404
1405            Def::System(tpe) => self.arena.type_check(attrs, tpe, expect),
1406        }
1407    }
1408
1409    fn projection_type(&mut self, query: &Query<Typed>) -> Type {
1410        self.project_type(query.projection)
1411    }
1412
1413    fn project_type(&mut self, node: ExprRef) -> Type {
1414        match self.arena.exprs.get(node).value {
1415            Value::Number(_) => Type::Number,
1416            Value::String(_) => Type::String,
1417            Value::Bool(_) => Type::Bool,
1418            Value::Id(id) => {
1419                if let Some(tpe) = self.options.default_scope.get(id) {
1420                    tpe
1421                } else if let Some(tpe) = self.scope.get(id) {
1422                    tpe
1423                } else {
1424                    Type::Unspecified
1425                }
1426            }
1427            Value::Array(exprs) => {
1428                let mut project = Type::Unspecified;
1429
1430                for idx in self.arena.exprs.vec_idxes(exprs) {
1431                    let expr = self.arena.exprs.vec_get(exprs, idx);
1432                    let tmp = self.project_type(expr);
1433
1434                    if !matches!(tmp, Type::Unspecified) {
1435                        project = tmp;
1436                        break;
1437                    }
1438                }
1439
1440                self.arena.types.alloc_array_of(project)
1441            }
1442            Value::Record(fields) => {
1443                let mut props = FxHashMap::default();
1444
1445                for idx in self.arena.exprs.rec_idxes(fields) {
1446                    let field = self.arena.exprs.rec_get(fields, idx);
1447                    let tpe = self.project_type(field.expr);
1448                    props.insert(field.name, tpe);
1449                }
1450
1451                Type::Record(self.arena.types.alloc_record(props))
1452            }
1453            Value::Access(access) => {
1454                let tpe = self.project_type(access.target);
1455                if let Type::Record(record) = tpe {
1456                    self.arena
1457                        .types
1458                        .record_get(record, access.field)
1459                        .unwrap_or_default()
1460                } else {
1461                    Type::Unspecified
1462                }
1463            }
1464            Value::App(app) => self.options.default_scope.get(app.func).unwrap_or_default(),
1465            Value::Binary(binary) => match binary.operator {
1466                Operator::Add | Operator::Sub | Operator::Mul | Operator::Div => Type::Number,
1467                Operator::As => {
1468                    if let Value::Id(n) = self.arena.exprs.get(binary.rhs).value
1469                        && let Some(tpe) = name_to_type(self.arena, self.options, n)
1470                    {
1471                        tpe
1472                    } else {
1473                        Type::Unspecified
1474                    }
1475                }
1476                Operator::Eq
1477                | Operator::Neq
1478                | Operator::Lt
1479                | Operator::Lte
1480                | Operator::Gt
1481                | Operator::Gte
1482                | Operator::And
1483                | Operator::Or
1484                | Operator::Xor
1485                | Operator::Not
1486                | Operator::Contains => Type::Bool,
1487            },
1488            Value::Unary(unary) => match unary.operator {
1489                Operator::Add | Operator::Sub => Type::Number,
1490                Operator::Mul
1491                | Operator::Div
1492                | Operator::Eq
1493                | Operator::Neq
1494                | Operator::Lt
1495                | Operator::Lte
1496                | Operator::Gt
1497                | Operator::Gte
1498                | Operator::And
1499                | Operator::Or
1500                | Operator::Xor
1501                | Operator::Not
1502                | Operator::Contains
1503                | Operator::As => unreachable!(),
1504            },
1505            Value::Group(expr) => self.project_type(expr),
1506        }
1507    }
1508}
1509
1510impl Arena {
1511    /// Checks if two types are the same.
1512    ///
1513    /// * If `this` is `Type::Unspecified` then `self` is updated to the more specific `Type`.
1514    /// * If `this` is `Type::Subject` and is checked against a `Type::String` then `self` is updated to `Type::String`
1515    fn type_check(&mut self, attrs: Attrs, this: Type, other: Type) -> Result<Type, AnalysisError> {
1516        match (this, other) {
1517            (Type::Unspecified, other) => Ok(other),
1518            (this, Type::Unspecified) => Ok(this),
1519            (Type::Subject, Type::Subject) => Ok(Type::Subject),
1520
1521            // Subjects are strings so there is no reason to reject a type
1522            // when compared to a string. However, when it happens, we demote
1523            // a subject to a string.
1524            (Type::Subject, Type::String) => Ok(Type::String),
1525            (Type::String, Type::Subject) => Ok(Type::String),
1526
1527            (Type::Number, Type::Number) => Ok(Type::Number),
1528            (Type::String, Type::String) => Ok(Type::String),
1529            (Type::Bool, Type::Bool) => Ok(Type::Bool),
1530            (Type::Date, Type::Date) => Ok(Type::Date),
1531            (Type::Time, Type::Time) => Ok(Type::Time),
1532            (Type::DateTime, Type::DateTime) => Ok(Type::DateTime),
1533
1534            // `DateTime` can be implicitly cast to `Date` or `Time`
1535            (Type::DateTime, Type::Date) => Ok(Type::Date),
1536            (Type::Date, Type::DateTime) => Ok(Type::Date),
1537            (Type::DateTime, Type::Time) => Ok(Type::Time),
1538            (Type::Time, Type::DateTime) => Ok(Type::Time),
1539            (Type::Custom(a), Type::Custom(b)) if self.strings.eq_ignore_ascii_case(a, b) => {
1540                Ok(Type::Custom(a))
1541            }
1542            (Type::Array(a), Type::Array(b)) => {
1543                let a = self.types.get_type(a);
1544                let b = self.types.get_type(b);
1545                let tpe = self.type_check(attrs, a, b)?;
1546
1547                Ok(self.types.alloc_array_of(tpe))
1548            }
1549
1550            (Type::Record(a), Type::Record(b)) if self.types.records_have_same_keys(a, b) => {
1551                let mut map_a = mem::take(&mut self.types.records[a.0]);
1552                let mut map_b = mem::take(&mut self.types.records[b.0]);
1553
1554                for (bk, bv) in map_b.iter_mut() {
1555                    let av = map_a.get_mut(bk).unwrap();
1556                    let new_tpe = self.type_check(attrs, *av, *bv)?;
1557
1558                    *av = new_tpe;
1559                    *bv = new_tpe;
1560                }
1561
1562                self.types.records[a.0] = map_a;
1563                self.types.records[b.0] = map_b;
1564
1565                Ok(Type::Record(a))
1566            }
1567
1568            (
1569                Type::App {
1570                    args: a_args,
1571                    result: a_res,
1572                    aggregate: a_agg,
1573                },
1574                Type::App {
1575                    args: b_args,
1576                    result: b_res,
1577                    aggregate: b_agg,
1578                },
1579            ) if self.types.get_args(a_args.values).len()
1580                == self.types.get_args(b_args.values).len()
1581                && a_agg == b_agg =>
1582            {
1583                if self.types.get_args(a_args.values).is_empty() {
1584                    let a = self.types.get_type(a_res);
1585                    let b = self.types.get_type(b_res);
1586                    let new_res = self.type_check(attrs, a, b)?;
1587
1588                    return Ok(Type::App {
1589                        args: a_args,
1590                        result: self.types.register_type(new_res),
1591                        aggregate: a_agg,
1592                    });
1593                }
1594
1595                let mut vec_a = mem::take(&mut self.types.args[a_args.values.0]);
1596                let mut vec_b = mem::take(&mut self.types.args[b_args.values.0]);
1597
1598                for (a, b) in vec_a.iter_mut().zip(vec_b.iter_mut()) {
1599                    let new_tpe = self.type_check(attrs, *a, *b)?;
1600                    *a = new_tpe;
1601                    *b = new_tpe;
1602                }
1603
1604                self.types.args[a_args.values.0] = vec_a;
1605                self.types.args[b_args.values.0] = vec_b;
1606
1607                let res_a = self.types.get_type(a_res);
1608                let res_b = self.types.get_type(b_res);
1609                let new_tpe = self.type_check(attrs, res_a, res_b)?;
1610
1611                Ok(Type::App {
1612                    args: a_args,
1613                    result: self.types.register_type(new_tpe),
1614                    aggregate: a_agg,
1615                })
1616            }
1617
1618            (this, other) => Err(AnalysisError::TypeMismatch(
1619                attrs.pos.line,
1620                attrs.pos.col,
1621                display_type(self, this),
1622                display_type(self, other),
1623            )),
1624        }
1625    }
1626}
1627
1628/// Converts a type name string to its corresponding [`Type`] variant.
1629///
1630/// This function performs case-insensitive matching for built-in type names and checks
1631/// against custom types defined in the analysis options.
1632///
1633/// # Returns
1634///
1635/// * `Some(Type)` - If the name matches a built-in type or custom type
1636/// * `None` - If the name doesn't match any known type
1637///
1638/// # Built-in Type Mappings
1639///
1640/// The following type names are recognized (case-insensitive):
1641/// - `"string"` → [`Type::String`]
1642/// - `"int"` or `"float64"` → [`Type::Number`]
1643/// - `"boolean"` → [`Type::Bool`]
1644/// - `"date"` → [`Type::Date`]
1645/// - `"time"` → [`Type::Time`]
1646/// - `"datetime"` → [`Type::DateTime`]
1647pub(crate) fn name_to_type(
1648    arena: &Arena,
1649    opts: &AnalysisOptions,
1650    name_ref: StrRef,
1651) -> Option<Type> {
1652    let name = arena.strings.get(name_ref);
1653
1654    if name.eq_ignore_ascii_case("string") {
1655        Some(Type::String)
1656    } else if name.eq_ignore_ascii_case("int") || name.eq_ignore_ascii_case("float64") {
1657        Some(Type::Number)
1658    } else if name.eq_ignore_ascii_case("boolean") {
1659        Some(Type::Bool)
1660    } else if name.eq_ignore_ascii_case("date") {
1661        Some(Type::Date)
1662    } else if name.eq_ignore_ascii_case("time") {
1663        Some(Type::Time)
1664    } else if name.eq_ignore_ascii_case("datetime") {
1665        Some(Type::DateTime)
1666    } else if opts.custom_types.contains(&Ascii::new(name.to_owned())) {
1667        // ^ Sad we have to allocate here for no reason
1668
1669        Some(Type::Custom(name_ref))
1670    } else {
1671        None
1672    }
1673}
1674
1675pub(crate) fn display_type(arena: &Arena, tpe: Type) -> String {
1676    fn go(buffer: &mut String, arena: &Arena, tpe: Type) {
1677        match tpe {
1678            Type::Unspecified => buffer.push_str("Any"),
1679            Type::Number => buffer.push_str("Number"),
1680            Type::String => buffer.push_str("String"),
1681            Type::Bool => buffer.push_str("Bool"),
1682            Type::Subject => buffer.push_str("Subject"),
1683            Type::Date => buffer.push_str("Date"),
1684            Type::Time => buffer.push_str("Time"),
1685            Type::DateTime => buffer.push_str("DateTime"),
1686            Type::Custom(n) => buffer.push_str(arena.strings.get(n)),
1687
1688            Type::Array(tpe) => {
1689                buffer.push_str("[]");
1690                go(buffer, arena, arena.types.get_type(tpe));
1691            }
1692
1693            Type::Record(map) => {
1694                let map = arena.types.get_record(map);
1695
1696                buffer.push_str("{ ");
1697
1698                for (idx, (name, value)) in map.iter().enumerate() {
1699                    if idx != 0 {
1700                        buffer.push_str(", ");
1701                    }
1702
1703                    buffer.push_str(arena.strings.get(*name));
1704                    buffer.push_str(": ");
1705
1706                    go(buffer, arena, *value);
1707                }
1708
1709                buffer.push_str(" }");
1710            }
1711
1712            Type::App {
1713                args,
1714                result,
1715                aggregate,
1716            } => {
1717                let fun_args = arena.types.get_args(args.values);
1718                buffer.push('(');
1719
1720                for (idx, arg) in fun_args.iter().copied().enumerate() {
1721                    if idx != 0 {
1722                        buffer.push_str(", ");
1723                    }
1724
1725                    go(buffer, arena, arg);
1726
1727                    if idx + 1 > args.needed {
1728                        buffer.push('?');
1729                    }
1730                }
1731
1732                buffer.push(')');
1733
1734                if aggregate {
1735                    buffer.push_str(" => ");
1736                } else {
1737                    buffer.push_str(" -> ");
1738                }
1739
1740                go(buffer, arena, arena.types.get_type(result));
1741            }
1742        }
1743    }
1744
1745    let mut buffer = String::new();
1746    go(&mut buffer, arena, tpe);
1747
1748    buffer
1749}