Skip to main content

eventql_parser/typing/
analysis.rs

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