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