Skip to main content

eventql_parser/typing/
analysis.rs

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