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            /// When true means we are into dynamically type object.
1140            dynamic: bool,
1141            definition: Def,
1142        }
1143
1144        impl State {
1145            fn new(definition: Def) -> Self {
1146                Self {
1147                    depth: 0,
1148                    dynamic: false,
1149                    definition,
1150                }
1151            }
1152        }
1153
1154        #[derive(Copy, Clone)]
1155        struct Parent {
1156            record: Record,
1157            field: Option<StrRef>,
1158        }
1159
1160        enum Def {
1161            User { parent: Parent, tpe: Type },
1162            System(Type),
1163        }
1164
1165        fn go<'global>(
1166            scope: &mut Scope,
1167            arena: &'global mut Arena,
1168            sys: &'global AnalysisOptions,
1169            expr: ExprRef,
1170        ) -> AnalysisResult<State> {
1171            let node = arena.exprs.get(expr);
1172            match node.value {
1173                Value::Id(id) => {
1174                    if let Some(tpe) = sys.default_scope.get(id) {
1175                        if matches!(tpe, Type::Record(_)) {
1176                            Ok(State::new(Def::System(tpe)))
1177                        } else {
1178                            Err(AnalysisError::ExpectRecord(
1179                                node.attrs.pos.line,
1180                                node.attrs.pos.col,
1181                                display_type(arena, tpe),
1182                            ))
1183                        }
1184                    } else if let Some(tpe) = scope.get_mut(id) {
1185                        if matches!(tpe, Type::Unspecified) {
1186                            let record = arena.types.instantiate_record();
1187                            *tpe = Type::Record(record);
1188
1189                            Ok(State::new(Def::User {
1190                                parent: Parent {
1191                                    record,
1192                                    field: None,
1193                                },
1194                                tpe: Type::Record(record),
1195                            }))
1196                        } else if let Type::Record(record) = *tpe {
1197                            Ok(State::new(Def::User {
1198                                parent: Parent {
1199                                    record,
1200                                    field: None,
1201                                },
1202                                tpe: *tpe,
1203                            }))
1204                        } else {
1205                            Err(AnalysisError::ExpectRecord(
1206                                node.attrs.pos.line,
1207                                node.attrs.pos.col,
1208                                display_type(arena, *tpe),
1209                            ))
1210                        }
1211                    } else {
1212                        Err(AnalysisError::VariableUndeclared(
1213                            node.attrs.pos.line,
1214                            node.attrs.pos.col,
1215                            arena.strings.get(id).to_owned(),
1216                        ))
1217                    }
1218                }
1219                Value::Access(access) => {
1220                    let mut state = go(scope, arena, sys, access.target)?;
1221
1222                    // TODO - we should consider make that field and depth configurable.
1223                    let is_data_field =
1224                        state.depth == 0 && arena.strings.get(access.field) == "data";
1225
1226                    // TODO - we should consider make that behavior configurable.
1227                    // the `data` property is where the JSON payload is located, which means
1228                    // we should be lax if a property is not defined yet.
1229                    if !state.dynamic && is_data_field {
1230                        state.dynamic = true;
1231                    }
1232
1233                    match state.definition {
1234                        Def::User { parent, tpe } => {
1235                            if matches!(tpe, Type::Unspecified) && state.dynamic {
1236                                let record = arena.types.instantiate_record();
1237                                arena
1238                                    .types
1239                                    .record_set(record, access.field, Type::Unspecified);
1240
1241                                // TODO - this is impossible. Should return a proper error instead of panicking
1242                                if let Some(field) = parent.field {
1243                                    arena.types.record_set(
1244                                        parent.record,
1245                                        field,
1246                                        Type::Record(record),
1247                                    );
1248                                }
1249
1250                                return Ok(State {
1251                                    depth: state.depth + 1,
1252                                    definition: Def::User {
1253                                        parent: Parent {
1254                                            record,
1255                                            field: Some(access.field),
1256                                        },
1257                                        tpe: Type::Unspecified,
1258                                    },
1259                                    ..state
1260                                });
1261                            } else if let Type::Record(record) = tpe {
1262                                return if let Some(tpe) =
1263                                    arena.types.record_get(record, access.field)
1264                                {
1265                                    Ok(State {
1266                                        depth: state.depth + 1,
1267                                        definition: Def::User {
1268                                            parent: Parent {
1269                                                record,
1270                                                field: Some(access.field),
1271                                            },
1272                                            tpe,
1273                                        },
1274                                        ..state
1275                                    })
1276                                } else {
1277                                    // TODO - that test seems useless because it can't be the data field and not be dynamic
1278                                    if state.dynamic || is_data_field {
1279                                        arena.types.record_set(
1280                                            record,
1281                                            access.field,
1282                                            Type::Unspecified,
1283                                        );
1284                                        return Ok(State {
1285                                            depth: state.depth + 1,
1286                                            definition: Def::User {
1287                                                parent: Parent {
1288                                                    record,
1289                                                    field: Some(access.field),
1290                                                },
1291                                                tpe: Type::Unspecified,
1292                                            },
1293                                            ..state
1294                                        });
1295                                    }
1296
1297                                    Err(AnalysisError::FieldUndeclared(
1298                                        node.attrs.pos.line,
1299                                        node.attrs.pos.col,
1300                                        arena.strings.get(access.field).to_owned(),
1301                                    ))
1302                                };
1303                            }
1304
1305                            Err(AnalysisError::ExpectRecord(
1306                                node.attrs.pos.line,
1307                                node.attrs.pos.col,
1308                                display_type(arena, tpe),
1309                            ))
1310                        }
1311
1312                        Def::System(tpe) => {
1313                            if matches!(tpe, Type::Unspecified) && state.dynamic {
1314                                return Ok(State {
1315                                    depth: state.depth + 1,
1316                                    definition: Def::System(Type::Unspecified),
1317                                    ..state
1318                                });
1319                            }
1320
1321                            if let Type::Record(rec) = tpe {
1322                                if let Some(field) = arena.types.record_get(rec, access.field) {
1323                                    return Ok(State {
1324                                        depth: state.depth + 1,
1325                                        definition: Def::System(field),
1326                                        ..state
1327                                    });
1328                                }
1329
1330                                return Err(AnalysisError::FieldUndeclared(
1331                                    node.attrs.pos.line,
1332                                    node.attrs.pos.col,
1333                                    arena.strings.get(access.field).to_owned(),
1334                                ));
1335                            }
1336
1337                            Err(AnalysisError::ExpectRecord(
1338                                node.attrs.pos.line,
1339                                node.attrs.pos.col,
1340                                display_type(arena, tpe),
1341                            ))
1342                        }
1343                    }
1344                }
1345                Value::Number(_)
1346                | Value::String(_)
1347                | Value::Bool(_)
1348                | Value::Array(_)
1349                | Value::Record(_)
1350                | Value::App(_)
1351                | Value::Binary(_)
1352                | Value::Unary(_)
1353                | Value::Group(_) => unreachable!(),
1354            }
1355        }
1356
1357        let state = go(&mut self.scope, self.arena, self.options, access)?;
1358
1359        match state.definition {
1360            Def::User { parent, tpe } => {
1361                let new_tpe = self.arena.type_check(attrs, tpe, expect)?;
1362
1363                if let Some(field) = parent.field {
1364                    self.arena.types.record_set(parent.record, field, new_tpe);
1365                }
1366
1367                Ok(new_tpe)
1368            }
1369
1370            Def::System(tpe) => self.arena.type_check(attrs, tpe, expect),
1371        }
1372    }
1373
1374    fn projection_type(&mut self, query: &Query<Typed>) -> Type {
1375        self.project_type(query.projection)
1376    }
1377
1378    fn project_type(&mut self, node: ExprRef) -> Type {
1379        match self.arena.exprs.get(node).value {
1380            Value::Number(_) => Type::Number,
1381            Value::String(_) => Type::String,
1382            Value::Bool(_) => Type::Bool,
1383            Value::Id(id) => {
1384                if let Some(tpe) = self.options.default_scope.get(id) {
1385                    tpe
1386                } else if let Some(tpe) = self.scope.get(id) {
1387                    tpe
1388                } else {
1389                    Type::Unspecified
1390                }
1391            }
1392            Value::Array(exprs) => {
1393                let mut project = Type::Unspecified;
1394
1395                for idx in self.arena.exprs.vec_idxes(exprs) {
1396                    let expr = self.arena.exprs.vec_get(exprs, idx);
1397                    let tmp = self.project_type(expr);
1398
1399                    if !matches!(tmp, Type::Unspecified) {
1400                        project = tmp;
1401                        break;
1402                    }
1403                }
1404
1405                self.arena.types.alloc_array_of(project)
1406            }
1407            Value::Record(fields) => {
1408                let mut props = FxHashMap::default();
1409
1410                for idx in self.arena.exprs.rec_idxes(fields) {
1411                    let field = self.arena.exprs.rec_get(fields, idx);
1412                    let tpe = self.project_type(field.expr);
1413                    props.insert(field.name, tpe);
1414                }
1415
1416                Type::Record(self.arena.types.alloc_record(props))
1417            }
1418            Value::Access(access) => {
1419                let tpe = self.project_type(access.target);
1420                if let Type::Record(record) = tpe {
1421                    self.arena
1422                        .types
1423                        .record_get(record, access.field)
1424                        .unwrap_or_default()
1425                } else {
1426                    Type::Unspecified
1427                }
1428            }
1429            Value::App(app) => self.options.default_scope.get(app.func).unwrap_or_default(),
1430            Value::Binary(binary) => match binary.operator {
1431                Operator::Add | Operator::Sub | Operator::Mul | Operator::Div => Type::Number,
1432                Operator::As => {
1433                    if let Value::Id(n) = self.arena.exprs.get(binary.rhs).value
1434                        && let Some(tpe) = resolve_type(self.arena, n)
1435                    {
1436                        tpe
1437                    } else {
1438                        Type::Unspecified
1439                    }
1440                }
1441                Operator::Eq
1442                | Operator::Neq
1443                | Operator::Lt
1444                | Operator::Lte
1445                | Operator::Gt
1446                | Operator::Gte
1447                | Operator::And
1448                | Operator::Or
1449                | Operator::Xor
1450                | Operator::Not
1451                | Operator::Contains => Type::Bool,
1452            },
1453            Value::Unary(unary) => match unary.operator {
1454                Operator::Add | Operator::Sub => Type::Number,
1455                Operator::Mul
1456                | Operator::Div
1457                | Operator::Eq
1458                | Operator::Neq
1459                | Operator::Lt
1460                | Operator::Lte
1461                | Operator::Gt
1462                | Operator::Gte
1463                | Operator::And
1464                | Operator::Or
1465                | Operator::Xor
1466                | Operator::Not
1467                | Operator::Contains
1468                | Operator::As => unreachable!(),
1469            },
1470            Value::Group(expr) => self.project_type(expr),
1471        }
1472    }
1473}
1474
1475impl Arena {
1476    /// Checks if two types are the same.
1477    ///
1478    /// * If `this` is `Type::Unspecified` then `self` is updated to the more specific `Type`.
1479    /// * If `this` is `Type::Subject` and is checked against a `Type::String` then `self` is updated to `Type::String`
1480    fn type_check(&mut self, attrs: Attrs, this: Type, other: Type) -> Result<Type, AnalysisError> {
1481        match (this, other) {
1482            (Type::Unspecified, other) => Ok(other),
1483            (this, Type::Unspecified) => Ok(this),
1484            (Type::Subject, Type::Subject) => Ok(Type::Subject),
1485
1486            // Subjects are strings so there is no reason to reject a type
1487            // when compared to a string. However, when it happens, we demote
1488            // a subject to a string.
1489            (Type::Subject, Type::String) => Ok(Type::String),
1490            (Type::String, Type::Subject) => Ok(Type::String),
1491
1492            (Type::Number, Type::Number) => Ok(Type::Number),
1493            (Type::String, Type::String) => Ok(Type::String),
1494            (Type::Bool, Type::Bool) => Ok(Type::Bool),
1495            (Type::Date, Type::Date) => Ok(Type::Date),
1496            (Type::Time, Type::Time) => Ok(Type::Time),
1497            (Type::DateTime, Type::DateTime) => Ok(Type::DateTime),
1498
1499            // `DateTime` can be implicitly cast to `Date` or `Time`
1500            (Type::DateTime, Type::Date) => Ok(Type::Date),
1501            (Type::Date, Type::DateTime) => Ok(Type::Date),
1502            (Type::DateTime, Type::Time) => Ok(Type::Time),
1503            (Type::Time, Type::DateTime) => Ok(Type::Time),
1504
1505            (Type::Array(a), Type::Array(b)) => {
1506                let a = self.types.get_type(a);
1507                let b = self.types.get_type(b);
1508                let tpe = self.type_check(attrs, a, b)?;
1509
1510                Ok(self.types.alloc_array_of(tpe))
1511            }
1512
1513            (Type::Record(a), Type::Record(b)) if self.types.records_have_same_keys(a, b) => {
1514                let mut map_a = mem::take(&mut self.types.records[a.0]);
1515                let mut map_b = mem::take(&mut self.types.records[b.0]);
1516
1517                for (bk, bv) in map_b.iter_mut() {
1518                    let av = map_a.get_mut(bk).unwrap();
1519                    let new_tpe = self.type_check(attrs, *av, *bv)?;
1520
1521                    *av = new_tpe;
1522                    *bv = new_tpe;
1523                }
1524
1525                self.types.records[a.0] = map_a;
1526                self.types.records[b.0] = map_b;
1527
1528                Ok(Type::Record(a))
1529            }
1530
1531            (
1532                Type::App {
1533                    args: a_args,
1534                    result: a_res,
1535                    aggregate: a_agg,
1536                },
1537                Type::App {
1538                    args: b_args,
1539                    result: b_res,
1540                    aggregate: b_agg,
1541                },
1542            ) if self.types.get_args(a_args.values).len()
1543                == self.types.get_args(b_args.values).len()
1544                && a_agg == b_agg =>
1545            {
1546                if self.types.get_args(a_args.values).is_empty() {
1547                    let a = self.types.get_type(a_res);
1548                    let b = self.types.get_type(b_res);
1549                    let new_res = self.type_check(attrs, a, b)?;
1550
1551                    return Ok(Type::App {
1552                        args: a_args,
1553                        result: self.types.register_type(new_res),
1554                        aggregate: a_agg,
1555                    });
1556                }
1557
1558                let mut vec_a = mem::take(&mut self.types.args[a_args.values.0]);
1559                let mut vec_b = mem::take(&mut self.types.args[b_args.values.0]);
1560
1561                for (a, b) in vec_a.iter_mut().zip(vec_b.iter_mut()) {
1562                    let new_tpe = self.type_check(attrs, *a, *b)?;
1563                    *a = new_tpe;
1564                    *b = new_tpe;
1565                }
1566
1567                self.types.args[a_args.values.0] = vec_a;
1568                self.types.args[b_args.values.0] = vec_b;
1569
1570                let res_a = self.types.get_type(a_res);
1571                let res_b = self.types.get_type(b_res);
1572                let new_tpe = self.type_check(attrs, res_a, res_b)?;
1573
1574                Ok(Type::App {
1575                    args: a_args,
1576                    result: self.types.register_type(new_tpe),
1577                    aggregate: a_agg,
1578                })
1579            }
1580
1581            (this, other) => Err(AnalysisError::TypeMismatch(
1582                attrs.pos.line,
1583                attrs.pos.col,
1584                display_type(self, this),
1585                display_type(self, other),
1586            )),
1587        }
1588    }
1589}
1590
1591/// Converts a type name string to its corresponding [`Type`] variant.
1592///
1593/// This function performs case-insensitive matching for built-in type names
1594///
1595/// # Returns
1596///
1597/// * `Some(Type)` - If the name matches a built-in type
1598/// * `None` - If the name doesn't match any known type
1599///
1600/// # Built-in Type Mappings
1601///
1602/// The following type names are recognized (case-insensitive):
1603/// - `"string"` → [`Type::String`]
1604/// - `"int"` or `"float64"` → [`Type::Number`]
1605/// - `"boolean"` → [`Type::Bool`]
1606/// - `"date"` → [`Type::Date`]
1607/// - `"time"` → [`Type::Time`]
1608/// - `"datetime"` → [`Type::DateTime`]
1609pub(crate) fn resolve_type_from_str(name: &str) -> Option<Type> {
1610    if name.eq_ignore_ascii_case("string") {
1611        Some(Type::String)
1612    } else if name.eq_ignore_ascii_case("int")
1613        || name.eq_ignore_ascii_case("float64")
1614        || name.eq_ignore_ascii_case("number")
1615    {
1616        Some(Type::Number)
1617    } else if name.eq_ignore_ascii_case("boolean") || name.eq_ignore_ascii_case("bool") {
1618        Some(Type::Bool)
1619    } else if name.eq_ignore_ascii_case("date") {
1620        Some(Type::Date)
1621    } else if name.eq_ignore_ascii_case("time") {
1622        Some(Type::Time)
1623    } else if name.eq_ignore_ascii_case("datetime") {
1624        Some(Type::DateTime)
1625    } else {
1626        None
1627    }
1628}
1629
1630pub(crate) fn resolve_type(arena: &Arena, name_ref: StrRef) -> Option<Type> {
1631    let name = arena.strings.get(name_ref);
1632    resolve_type_from_str(name)
1633}
1634
1635pub(crate) fn display_type(arena: &Arena, tpe: Type) -> String {
1636    fn go(buffer: &mut String, arena: &Arena, tpe: Type) {
1637        match tpe {
1638            Type::Unspecified => buffer.push_str("Any"),
1639            Type::Number => buffer.push_str("Number"),
1640            Type::String => buffer.push_str("String"),
1641            Type::Bool => buffer.push_str("Bool"),
1642            Type::Subject => buffer.push_str("Subject"),
1643            Type::Date => buffer.push_str("Date"),
1644            Type::Time => buffer.push_str("Time"),
1645            Type::DateTime => buffer.push_str("DateTime"),
1646
1647            Type::Array(tpe) => {
1648                buffer.push_str("[]");
1649                go(buffer, arena, arena.types.get_type(tpe));
1650            }
1651
1652            Type::Record(map) => {
1653                let map = arena.types.get_record(map);
1654
1655                buffer.push_str("{ ");
1656
1657                for (idx, (name, value)) in map.iter().enumerate() {
1658                    if idx != 0 {
1659                        buffer.push_str(", ");
1660                    }
1661
1662                    buffer.push_str(arena.strings.get(*name));
1663                    buffer.push_str(": ");
1664
1665                    go(buffer, arena, *value);
1666                }
1667
1668                buffer.push_str(" }");
1669            }
1670
1671            Type::App {
1672                args,
1673                result,
1674                aggregate,
1675            } => {
1676                let fun_args = arena.types.get_args(args.values);
1677                buffer.push('(');
1678
1679                for (idx, arg) in fun_args.iter().copied().enumerate() {
1680                    if idx != 0 {
1681                        buffer.push_str(", ");
1682                    }
1683
1684                    go(buffer, arena, arg);
1685
1686                    if idx + 1 > args.needed {
1687                        buffer.push('?');
1688                    }
1689                }
1690
1691                buffer.push(')');
1692
1693                if aggregate {
1694                    buffer.push_str(" => ");
1695                } else {
1696                    buffer.push_str(" -> ");
1697                }
1698
1699                go(buffer, arena, arena.types.get_type(result));
1700            }
1701        }
1702    }
1703
1704    let mut buffer = String::new();
1705    go(&mut buffer, arena, tpe);
1706
1707    buffer
1708}