eventql_parser/
analysis.rs

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