diff --git a/Cargo.toml b/Cargo.toml index f6fbd0d..dfbe85f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,16 @@ case_insensitive_hashmap = "1" version = "1" features = ["derive"] +[dependencies.ordered-float] +version = "5" +default-features = false +features = ["serde"] + +[dependencies.rustc-hash] +version = "2" +default-features = false +features = ["std"] + [dev-dependencies.insta] -version = "1.44" -features = ["yaml"] +version = "1.46" +features = ["yaml", "redactions"] diff --git a/src/analysis.rs b/src/analysis.rs index 88f275e..bcfa53f 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -1,15 +1,13 @@ -use std::{ - borrow::Cow, - collections::{BTreeMap, HashSet, btree_map::Entry}, - mem, -}; - use case_insensitive_hashmap::CaseInsensitiveHashMap; +use rustc_hash::FxHashMap; use serde::{Serialize, ser::SerializeMap}; +use std::collections::hash_map::Entry; +use std::{borrow::Cow, collections::HashSet, mem}; use unicase::Ascii; +use crate::arena::ExprArena; use crate::{ - App, Attrs, Binary, Expr, Field, FunArgs, Query, Raw, Source, SourceKind, Type, Value, + App, Attrs, Binary, ExprRef, Field, FunArgs, Query, Raw, Source, SourceKind, Type, Value, error::AnalysisError, token::Operator, }; @@ -430,7 +428,7 @@ impl Default for AnalysisOptions { ), ]), }, - event_type_info: Type::Record(BTreeMap::from([ + event_type_info: Type::Record(FxHashMap::from_iter([ ("specversion".to_owned(), Type::String), ("id".to_owned(), Type::String), ("time".to_owned(), Type::DateTime), @@ -472,10 +470,11 @@ impl Default for AnalysisOptions { /// /// Returns a typed query on success, or an `AnalysisError` if type checking fails. pub fn static_analysis( + arena: &ExprArena, options: &AnalysisOptions, query: Query, ) -> AnalysisResult> { - let mut analysis = Analysis::new(options); + let mut analysis = Analysis::new(arena, options); analysis.analyze_query(query) } @@ -541,6 +540,7 @@ pub struct AnalysisContext { /// This struct maintains the analysis state including scopes and type information. /// It can be used to perform type checking on individual expressions or entire queries. pub struct Analysis<'a> { + arena: &'a ExprArena, /// The analysis options containing type information for functions and event types. options: &'a AnalysisOptions, /// Stack of previous scopes for nested scope handling. @@ -551,8 +551,9 @@ pub struct Analysis<'a> { impl<'a> Analysis<'a> { /// Creates a new analysis instance with the given options. - pub fn new(options: &'a AnalysisOptions) -> Self { + pub fn new(arena: &'a ExprArena, options: &'a AnalysisOptions) -> Self { Self { + arena, options, prev_scopes: Default::default(), scope: Scope::default(), @@ -617,11 +618,13 @@ impl<'a> Analysis<'a> { /// /// ```rust /// use eventql_parser::{parse_query, prelude::{Analysis, AnalysisOptions}}; + /// use eventql_parser::arena::ExprArena; /// - /// let query = parse_query("FROM e IN events WHERE [1,2,3] CONTAINS e.data.price PROJECT INTO e").unwrap(); + /// let mut arena = ExprArena::default(); + /// let query = parse_query(&mut arena, "FROM e IN events WHERE [1,2,3] CONTAINS e.data.price PROJECT INTO e").unwrap(); /// /// let options = AnalysisOptions::default(); - /// let mut analysis = Analysis::new(&options); + /// let mut analysis = Analysis::new(&arena, &options); /// /// let typed_query = analysis.analyze_query(query); /// assert!(typed_query.is_ok()); @@ -636,29 +639,31 @@ impl<'a> Analysis<'a> { sources.push(self.analyze_source(source)?); } - if let Some(expr) = &query.predicate { + if let Some(expr) = query.predicate.as_ref().copied() { self.analyze_expr(&mut ctx, expr, Type::Bool)?; } if let Some(group_by) = &query.group_by { - if !matches!(&group_by.expr.value, Value::Access(_)) { + let node = self.arena.get(group_by.expr); + if !matches!(node.value, Value::Access(_)) { return Err(AnalysisError::ExpectFieldLiteral( - group_by.expr.attrs.pos.line, - group_by.expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, )); } - self.analyze_expr(&mut ctx, &group_by.expr, Type::Unspecified)?; + self.analyze_expr(&mut ctx, group_by.expr, Type::Unspecified)?; - if let Some(expr) = &group_by.predicate { + if let Some(expr) = group_by.predicate.as_ref().copied() { + let node = self.arena.get(expr); ctx.allow_agg_func = true; ctx.use_agg_funcs = true; self.analyze_expr(&mut ctx, expr, Type::Bool)?; if !self.expect_agg_expr(expr)? { return Err(AnalysisError::ExpectAggExpr( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, )); } } @@ -667,18 +672,18 @@ impl<'a> Analysis<'a> { ctx.use_agg_funcs = true; } - let project = self.analyze_projection(&mut ctx, &query.projection)?; + let project = self.analyze_projection(&mut ctx, query.projection)?; if let Some(order_by) = &query.order_by { - self.analyze_expr(&mut ctx, &order_by.expr, Type::Unspecified)?; - - if query.group_by.is_none() && !matches!(&order_by.expr.value, Value::Access(_)) { + self.analyze_expr(&mut ctx, order_by.expr, Type::Unspecified)?; + let node = self.arena.get(order_by.expr); + if query.group_by.is_none() && !matches!(node.value, Value::Access(_)) { return Err(AnalysisError::ExpectFieldLiteral( - order_by.expr.attrs.pos.line, - order_by.expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, )); } else if query.group_by.is_some() { - self.expect_agg_func(&order_by.expr)?; + self.expect_agg_func(order_by.expr)?; } } @@ -741,19 +746,20 @@ impl<'a> Analysis<'a> { fn analyze_projection( &mut self, ctx: &mut AnalysisContext, - expr: &Expr, + expr: ExprRef, ) -> AnalysisResult { - match &expr.value { + let node = self.arena.get(expr); + match node.value { Value::Record(record) => { if record.is_empty() { return Err(AnalysisError::EmptyRecord( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, )); } ctx.allow_agg_func = true; - let tpe = self.analyze_expr(ctx, expr, Type::Unspecified)?; + let tpe = self.analyze_expr(ctx, node.node_ref, Type::Unspecified)?; let mut chk_ctx = CheckContext { use_agg_func: ctx.use_agg_funcs, ..Default::default() @@ -766,7 +772,7 @@ impl<'a> Analysis<'a> { Value::App(app) => { ctx.allow_agg_func = true; - let tpe = self.analyze_expr(ctx, expr, Type::Unspecified)?; + let tpe = self.analyze_expr(ctx, node.node_ref, Type::Unspecified)?; if ctx.use_agg_funcs { let mut chk_ctx = CheckContext { @@ -776,15 +782,15 @@ impl<'a> Analysis<'a> { self.check_projection_on_field_expr(&mut chk_ctx, expr)?; } else { - self.reject_constant_func(&expr.attrs, app)?; + self.reject_constant_func(node.attrs, app)?; } Ok(tpe) } Value::Id(_) if ctx.use_agg_funcs => Err(AnalysisError::ExpectAggExpr( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, )), Value::Id(id) => { @@ -792,28 +798,28 @@ impl<'a> Analysis<'a> { Ok(tpe) } else { Err(AnalysisError::VariableUndeclared( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, id.clone(), )) } } Value::Access(_) if ctx.use_agg_funcs => Err(AnalysisError::ExpectAggExpr( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, )), Value::Access(access) => { - let mut current = &access.target.value; + let mut current = self.arena.get(access.target); loop { - match current { + match current.value { Value::Id(name) => { if !self.scope.entries.contains_key(name.as_str()) { return Err(AnalysisError::VariableUndeclared( - expr.attrs.pos.line, - expr.attrs.pos.col, + current.attrs.pos.line, + current.attrs.pos.col, name.clone(), )); } @@ -821,7 +827,7 @@ impl<'a> Analysis<'a> { break; } - Value::Access(next) => current = &next.target.value, + Value::Access(next) => current = self.arena.get(next.target), _ => unreachable!(), } } @@ -830,9 +836,9 @@ impl<'a> Analysis<'a> { } _ => Err(AnalysisError::ExpectRecordOrSourcedProperty( - expr.attrs.pos.line, - expr.attrs.pos.col, - self.project_type(&expr.value), + node.attrs.pos.line, + node.attrs.pos.col, + self.project_type(expr), )), } } @@ -854,23 +860,24 @@ impl<'a> Analysis<'a> { ctx: &mut CheckContext, field: &Field, ) -> AnalysisResult<()> { - self.check_projection_on_field_expr(ctx, &field.value) + self.check_projection_on_field_expr(ctx, field.expr) } fn check_projection_on_field_expr( &mut self, ctx: &mut CheckContext, - expr: &Expr, + expr: ExprRef, ) -> AnalysisResult<()> { - match &expr.value { + let node = self.arena.get(expr); + match node.value { Value::Number(_) | Value::String(_) | Value::Bool(_) => Ok(()), Value::Id(id) => { if self.scope.entries.contains_key(id.as_str()) { if ctx.use_agg_func { return Err(AnalysisError::UnallowedAggFuncUsageWithSrcField( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, )); } @@ -881,7 +888,7 @@ impl<'a> Analysis<'a> { } Value::Array(exprs) => { - for expr in exprs { + for expr in exprs.iter().copied() { self.check_projection_on_field_expr(ctx, expr)?; } @@ -896,7 +903,7 @@ impl<'a> Analysis<'a> { Ok(()) } - Value::Access(access) => self.check_projection_on_field_expr(ctx, &access.target), + Value::Access(access) => self.check_projection_on_field_expr(ctx, access.target), Value::App(app) => { if let Some(Type::App { aggregate, .. }) = @@ -906,8 +913,8 @@ impl<'a> Analysis<'a> { if ctx.use_agg_func && ctx.use_source_based { return Err(AnalysisError::UnallowedAggFuncUsageWithSrcField( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, )); } @@ -915,7 +922,7 @@ impl<'a> Analysis<'a> { return self.expect_agg_func(expr); } - for arg in &app.args { + for arg in app.args.iter().copied() { self.invalidate_agg_func_usage(arg)?; } } @@ -924,22 +931,23 @@ impl<'a> Analysis<'a> { } Value::Binary(binary) => { - self.check_projection_on_field_expr(ctx, &binary.lhs)?; - self.check_projection_on_field_expr(ctx, &binary.rhs) + self.check_projection_on_field_expr(ctx, binary.lhs)?; + self.check_projection_on_field_expr(ctx, binary.rhs) } - Value::Unary(unary) => self.check_projection_on_field_expr(ctx, &unary.expr), - Value::Group(expr) => self.check_projection_on_field_expr(ctx, expr), + Value::Unary(unary) => self.check_projection_on_field_expr(ctx, unary.expr), + Value::Group(expr) => self.check_projection_on_field_expr(ctx, *expr), } } - fn expect_agg_func(&self, expr: &Expr) -> AnalysisResult<()> { - if let Value::App(app) = &expr.value + fn expect_agg_func(&self, expr: ExprRef) -> AnalysisResult<()> { + let node = self.arena.get(expr); + if let Value::App(app) = node.value && let Some(Type::App { aggregate: true, .. }) = self.options.default_scope.entries.get(app.func.as_str()) { - for arg in &app.args { + for arg in app.args.iter().copied() { self.ensure_agg_param_is_source_bound(arg)?; self.invalidate_agg_func_usage(arg)?; } @@ -948,38 +956,39 @@ impl<'a> Analysis<'a> { } Err(AnalysisError::ExpectAggExpr( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, )) } - fn expect_agg_expr(&self, expr: &Expr) -> AnalysisResult { - match &expr.value { + fn expect_agg_expr(&self, expr: ExprRef) -> AnalysisResult { + let node = self.arena.get(expr); + match node.value { Value::Id(id) => { if self.scope.entries.contains_key(id.as_str()) { return Err(AnalysisError::UnallowedAggFuncUsageWithSrcField( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, )); } Ok(false) } - Value::Group(expr) => self.expect_agg_expr(expr), + Value::Group(expr) => self.expect_agg_expr(*expr), Value::Binary(binary) => { - let lhs = self.expect_agg_expr(&binary.lhs)?; - let rhs = self.expect_agg_expr(&binary.rhs)?; + let lhs = self.expect_agg_expr(binary.lhs)?; + let rhs = self.expect_agg_expr(binary.rhs)?; if !lhs && !rhs { return Err(AnalysisError::ExpectAggExpr( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, )); } Ok(true) } - Value::Unary(unary) => self.expect_agg_expr(unary.expr.as_ref()), + Value::Unary(unary) => self.expect_agg_expr(unary.expr), Value::App(_) => { self.expect_agg_func(expr)?; Ok(true) @@ -989,29 +998,30 @@ impl<'a> Analysis<'a> { } } - fn ensure_agg_param_is_source_bound(&self, expr: &Expr) -> AnalysisResult<()> { - match &expr.value { + fn ensure_agg_param_is_source_bound(&self, expr: ExprRef) -> AnalysisResult<()> { + let node = self.arena.get(expr); + match node.value { Value::Id(id) if !self.options.default_scope.entries.contains_key(id.as_str()) => { Ok(()) } - Value::Access(access) => self.ensure_agg_param_is_source_bound(&access.target), - Value::Binary(binary) => self.ensure_agg_binary_op_is_source_bound(&expr.attrs, binary), - Value::Unary(unary) => self.ensure_agg_param_is_source_bound(&unary.expr), + Value::Access(access) => self.ensure_agg_param_is_source_bound(access.target), + Value::Binary(binary) => self.ensure_agg_binary_op_is_source_bound(node.attrs, *binary), + Value::Unary(unary) => self.ensure_agg_param_is_source_bound(unary.expr), _ => Err(AnalysisError::ExpectSourceBoundProperty( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, )), } } fn ensure_agg_binary_op_is_source_bound( &self, - attrs: &Attrs, - binary: &Binary, + attrs: Attrs, + binary: Binary, ) -> AnalysisResult<()> { - if !self.ensure_agg_binary_op_branch_is_source_bound(&binary.lhs) - && !self.ensure_agg_binary_op_branch_is_source_bound(&binary.rhs) + if !self.ensure_agg_binary_op_branch_is_source_bound(binary.lhs) + && !self.ensure_agg_binary_op_branch_is_source_bound(binary.rhs) { return Err(AnalysisError::ExpectSourceBoundProperty( attrs.pos.line, @@ -1022,8 +1032,9 @@ impl<'a> Analysis<'a> { Ok(()) } - fn ensure_agg_binary_op_branch_is_source_bound(&self, expr: &Expr) -> bool { - match &expr.value { + fn ensure_agg_binary_op_branch_is_source_bound(&self, expr: ExprRef) -> bool { + let node = self.arena.get(expr); + match node.value { Value::Id(id) => !self.options.default_scope.entries.contains_key(id.as_str()), Value::Array(exprs) => { if exprs.is_empty() { @@ -1032,6 +1043,7 @@ impl<'a> Analysis<'a> { exprs .iter() + .copied() .all(|expr| self.ensure_agg_binary_op_branch_is_source_bound(expr)) } Value::Record(fields) => { @@ -1041,24 +1053,25 @@ impl<'a> Analysis<'a> { fields .iter() - .all(|field| self.ensure_agg_binary_op_branch_is_source_bound(&field.value)) + .all(|field| self.ensure_agg_binary_op_branch_is_source_bound(field.expr)) } Value::Access(access) => { - self.ensure_agg_binary_op_branch_is_source_bound(&access.target) + self.ensure_agg_binary_op_branch_is_source_bound(access.target) } Value::Binary(binary) => self - .ensure_agg_binary_op_is_source_bound(&expr.attrs, binary) + .ensure_agg_binary_op_is_source_bound(node.attrs, *binary) .is_ok(), - Value::Unary(unary) => self.ensure_agg_binary_op_branch_is_source_bound(&unary.expr), - Value::Group(expr) => self.ensure_agg_binary_op_branch_is_source_bound(expr), + Value::Unary(unary) => self.ensure_agg_binary_op_branch_is_source_bound(unary.expr), + Value::Group(expr) => self.ensure_agg_binary_op_branch_is_source_bound(*expr), Value::Number(_) | Value::String(_) | Value::Bool(_) | Value::App(_) => false, } } - fn invalidate_agg_func_usage(&self, expr: &Expr) -> AnalysisResult<()> { - match &expr.value { + fn invalidate_agg_func_usage(&self, expr: ExprRef) -> AnalysisResult<()> { + let node = self.arena.get(expr); + match node.value { Value::Number(_) | Value::String(_) | Value::Bool(_) @@ -1066,7 +1079,7 @@ impl<'a> Analysis<'a> { | Value::Access(_) => Ok(()), Value::Array(exprs) => { - for expr in exprs { + for expr in exprs.iter().copied() { self.invalidate_agg_func_usage(expr)?; } @@ -1075,7 +1088,7 @@ impl<'a> Analysis<'a> { Value::Record(fields) => { for field in fields { - self.invalidate_agg_func_usage(&field.value)?; + self.invalidate_agg_func_usage(field.expr)?; } Ok(()) @@ -1087,13 +1100,13 @@ impl<'a> Analysis<'a> { && *aggregate { return Err(AnalysisError::WrongAggFunUsage( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, app.func.clone(), )); } - for arg in &app.args { + for arg in app.args.iter().copied() { self.invalidate_agg_func_usage(arg)?; } @@ -1101,16 +1114,16 @@ impl<'a> Analysis<'a> { } Value::Binary(binary) => { - self.invalidate_agg_func_usage(&binary.lhs)?; - self.invalidate_agg_func_usage(&binary.rhs) + self.invalidate_agg_func_usage(binary.lhs)?; + self.invalidate_agg_func_usage(binary.rhs) } - Value::Unary(unary) => self.invalidate_agg_func_usage(&unary.expr), - Value::Group(expr) => self.invalidate_agg_func_usage(expr), + Value::Unary(unary) => self.invalidate_agg_func_usage(unary.expr), + Value::Group(expr) => self.invalidate_agg_func_usage(*expr), } } - fn reject_constant_func(&self, attrs: &Attrs, app: &App) -> AnalysisResult<()> { + fn reject_constant_func(&self, attrs: Attrs, app: &App) -> AnalysisResult<()> { if app.args.is_empty() { return Err(AnalysisError::ConstantExprInProjectIntoClause( attrs.pos.line, @@ -1119,7 +1132,7 @@ impl<'a> Analysis<'a> { } let mut errored = None; - for arg in &app.args { + for arg in app.args.iter().copied() { if let Err(e) = self.reject_constant_expr(arg) { if errored.is_none() { errored = Some(e); @@ -1135,13 +1148,14 @@ impl<'a> Analysis<'a> { Err(errored.expect("to be defined at that point")) } - fn reject_constant_expr(&self, expr: &Expr) -> AnalysisResult<()> { - match &expr.value { + fn reject_constant_expr(&self, expr: ExprRef) -> AnalysisResult<()> { + let node = self.arena.get(expr); + match node.value { Value::Id(id) if self.scope.entries.contains_key(id.as_str()) => Ok(()), Value::Array(exprs) => { let mut errored = None; - for expr in exprs { + for expr in exprs.iter().copied() { if let Err(e) = self.reject_constant_expr(expr) { if errored.is_none() { errored = Some(e); @@ -1160,7 +1174,7 @@ impl<'a> Analysis<'a> { Value::Record(fields) => { let mut errored = None; for field in fields { - if let Err(e) = self.reject_constant_expr(&field.value) { + if let Err(e) = self.reject_constant_expr(field.expr) { if errored.is_none() { errored = Some(e); } @@ -1176,17 +1190,17 @@ impl<'a> Analysis<'a> { } Value::Binary(binary) => self - .reject_constant_expr(&binary.lhs) - .or_else(|e| self.reject_constant_expr(&binary.rhs).map_err(|_| e)), + .reject_constant_expr(binary.lhs) + .or_else(|e| self.reject_constant_expr(binary.rhs).map_err(|_| e)), - Value::Access(access) => self.reject_constant_expr(access.target.as_ref()), - Value::App(app) => self.reject_constant_func(&expr.attrs, app), - Value::Unary(unary) => self.reject_constant_expr(&unary.expr), - Value::Group(expr) => self.reject_constant_expr(expr), + Value::Access(access) => self.reject_constant_expr(access.target), + Value::App(app) => self.reject_constant_func(node.attrs, app), + Value::Unary(unary) => self.reject_constant_expr(unary.expr), + Value::Group(expr) => self.reject_constant_expr(*expr), _ => Err(AnalysisError::ConstantExprInProjectIntoClause( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, )), } } @@ -1211,38 +1225,41 @@ impl<'a> Analysis<'a> { /// /// ```rust /// use eventql_parser::prelude::{tokenize, Parser, Analysis, AnalysisContext, AnalysisOptions, Type}; + /// use eventql_parser::arena::ExprArena; /// + /// let mut arena = ExprArena::default(); /// let tokens = tokenize("1 + 2").unwrap(); - /// let expr = Parser::new(tokens.as_slice()).parse_expr().unwrap(); + /// let expr = Parser::new(&mut arena, tokens.as_slice()).parse_expr().unwrap(); /// let options = AnalysisOptions::default(); - /// let mut analysis = Analysis::new(&options); + /// let mut analysis = Analysis::new(&arena, &options); /// - /// let result = analysis.analyze_expr(&mut AnalysisContext::default(), &expr, Type::Number); + /// let result = analysis.analyze_expr(&mut AnalysisContext::default(), expr, Type::Number); /// assert!(result.is_ok()); /// ``` pub fn analyze_expr( &mut self, ctx: &mut AnalysisContext, - expr: &Expr, + expr: ExprRef, mut expect: Type, ) -> AnalysisResult { - match &expr.value { - Value::Number(_) => expect.check(&expr.attrs, Type::Number), - Value::String(_) => expect.check(&expr.attrs, Type::String), - Value::Bool(_) => expect.check(&expr.attrs, Type::Bool), + let node = self.arena.get(expr); + match node.value { + Value::Number(_) => expect.check(node.attrs, Type::Number), + Value::String(_) => expect.check(node.attrs, Type::String), + Value::Bool(_) => expect.check(node.attrs, Type::Bool), Value::Id(id) => { if let Some(tpe) = self.options.default_scope.entries.get(id.as_str()) { - expect.check(&expr.attrs, tpe.clone()) + expect.check(node.attrs, tpe.clone()) } else if let Some(tpe) = self.scope.entries.get_mut(id.as_str()) { let tmp = mem::take(tpe); - *tpe = tmp.check(&expr.attrs, expect)?; + *tpe = tmp.check(node.attrs, expect)?; Ok(tpe.clone()) } else { Err(AnalysisError::VariableUndeclared( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, id.to_owned(), )) } @@ -1250,7 +1267,7 @@ impl<'a> Analysis<'a> { Value::Array(exprs) => { if matches!(expect, Type::Unspecified) { - for expr in exprs { + for expr in exprs.iter().copied() { expect = self.analyze_expr(ctx, expr, expect)?; } @@ -1259,7 +1276,7 @@ impl<'a> Analysis<'a> { match expect { Type::Array(mut expect) => { - for expr in exprs { + for expr in exprs.iter().copied() { *expect = self.analyze_expr(ctx, expr, expect.as_ref().clone())?; } @@ -1267,22 +1284,22 @@ impl<'a> Analysis<'a> { } expect => Err(AnalysisError::TypeMismatch( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, expect, - self.project_type(&expr.value), + self.project_type(expr), )), } } Value::Record(fields) => { if matches!(expect, Type::Unspecified) { - let mut record = BTreeMap::new(); + let mut record = FxHashMap::default(); for field in fields { record.insert( field.name.clone(), - self.analyze_expr(ctx, &field.value, Type::Unspecified)?, + self.analyze_expr(ctx, field.expr, Type::Unspecified)?, ); } @@ -1295,12 +1312,12 @@ impl<'a> Analysis<'a> { if let Some(tpe) = types.remove(field.name.as_str()) { types.insert( field.name.clone(), - self.analyze_expr(ctx, &field.value, tpe)?, + self.analyze_expr(ctx, field.expr, tpe)?, ); } else { return Err(AnalysisError::FieldUndeclared( - expr.attrs.pos.line, - expr.attrs.pos.col, + field.attrs.pos.line, + field.attrs.pos.col, field.name.clone(), )); } @@ -1310,15 +1327,15 @@ impl<'a> Analysis<'a> { } expect => Err(AnalysisError::TypeMismatch( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, expect, - self.project_type(&expr.value), + self.project_type(expr), )), } } - this @ Value::Access(_) => Ok(self.analyze_access(&expr.attrs, this, expect)?), + Value::Access(_) => Ok(self.analyze_access(node.attrs, node.node_ref, expect)?), Value::App(app) => { if let Some(tpe) = self.options.default_scope.entries.get(app.func.as_str()) @@ -1330,16 +1347,16 @@ impl<'a> Analysis<'a> { { if !args.match_arg_count(app.args.len()) { return Err(AnalysisError::FunWrongArgumentCount( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, app.func.clone(), )); } if *aggregate && !ctx.allow_agg_func { return Err(AnalysisError::WrongAggFunUsage( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, app.func.clone(), )); } @@ -1348,19 +1365,19 @@ impl<'a> Analysis<'a> { ctx.use_agg_funcs = true; } - for (arg, tpe) in app.args.iter().zip(args.values.iter().cloned()) { + for (arg, tpe) in app.args.iter().copied().zip(args.values.iter().cloned()) { self.analyze_expr(ctx, arg, tpe)?; } if matches!(expect, Type::Unspecified) { Ok(result.as_ref().clone()) } else { - expect.check(&expr.attrs, result.as_ref().clone()) + expect.check(node.attrs, result.as_ref().clone()) } } else { Err(AnalysisError::FuncUndeclared( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, app.func.clone(), )) } @@ -1368,9 +1385,9 @@ impl<'a> Analysis<'a> { Value::Binary(binary) => match binary.operator { Operator::Add | Operator::Sub | Operator::Mul | Operator::Div => { - self.analyze_expr(ctx, &binary.lhs, Type::Number)?; - self.analyze_expr(ctx, &binary.rhs, Type::Number)?; - expect.check(&expr.attrs, Type::Number) + self.analyze_expr(ctx, binary.lhs, Type::Number)?; + self.analyze_expr(ctx, binary.rhs, Type::Number)?; + expect.check(node.attrs, Type::Number) } Operator::Eq @@ -1379,24 +1396,24 @@ impl<'a> Analysis<'a> { | Operator::Lte | Operator::Gt | Operator::Gte => { - let lhs_expect = self.analyze_expr(ctx, &binary.lhs, Type::Unspecified)?; - let rhs_expect = self.analyze_expr(ctx, &binary.rhs, lhs_expect.clone())?; + let lhs_expect = self.analyze_expr(ctx, binary.lhs, Type::Unspecified)?; + let rhs_expect = self.analyze_expr(ctx, binary.rhs, lhs_expect.clone())?; // If the left side didn't have enough type information while the other did, // we replay another typecheck pass on the left side if the right side was conclusive if matches!(lhs_expect, Type::Unspecified) && !matches!(rhs_expect, Type::Unspecified) { - self.analyze_expr(ctx, &binary.lhs, rhs_expect)?; + self.analyze_expr(ctx, binary.lhs, rhs_expect)?; } - expect.check(&expr.attrs, Type::Bool) + expect.check(node.attrs, Type::Bool) } Operator::Contains => { let lhs_expect = self.analyze_expr( ctx, - &binary.lhs, + binary.lhs, Type::Array(Box::new(Type::Unspecified)), )?; @@ -1404,45 +1421,46 @@ impl<'a> Analysis<'a> { Type::Array(inner) => *inner, other => { return Err(AnalysisError::ExpectArray( - expr.attrs.pos.line, - expr.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, other, )); } }; - let rhs_expect = self.analyze_expr(ctx, &binary.rhs, lhs_assumption.clone())?; + let rhs_expect = self.analyze_expr(ctx, binary.rhs, lhs_assumption.clone())?; // If the left side didn't have enough type information while the other did, // we replay another typecheck pass on the left side if the right side was conclusive if matches!(lhs_assumption, Type::Unspecified) && !matches!(rhs_expect, Type::Unspecified) { - self.analyze_expr(ctx, &binary.lhs, Type::Array(Box::new(rhs_expect)))?; + self.analyze_expr(ctx, binary.lhs, Type::Array(Box::new(rhs_expect)))?; } - expect.check(&expr.attrs, Type::Bool) + expect.check(node.attrs, Type::Bool) } Operator::And | Operator::Or | Operator::Xor => { - self.analyze_expr(ctx, &binary.lhs, Type::Bool)?; - self.analyze_expr(ctx, &binary.rhs, Type::Bool)?; + self.analyze_expr(ctx, binary.lhs, Type::Bool)?; + self.analyze_expr(ctx, binary.rhs, Type::Bool)?; - expect.check(&expr.attrs, Type::Bool) + expect.check(node.attrs, Type::Bool) } Operator::As => { - if let Value::Id(name) = &binary.rhs.value { - if let Some(tpe) = name_to_type(self.options, name) { + let rhs = self.arena.get(binary.rhs); + if let Value::Id(name) = rhs.value { + return if let Some(tpe) = name_to_type(self.options, name) { // NOTE - we could check if it's safe to convert the left branch to that type - return Ok(tpe); + Ok(tpe) } else { - return Err(AnalysisError::UnsupportedCustomType( - expr.attrs.pos.line, - expr.attrs.pos.col, + Err(AnalysisError::UnsupportedCustomType( + rhs.attrs.pos.line, + rhs.attrs.pos.col, name.clone(), - )); - } + )) + }; } unreachable!( @@ -1455,26 +1473,26 @@ impl<'a> Analysis<'a> { Value::Unary(unary) => match unary.operator { Operator::Add | Operator::Sub => { - self.analyze_expr(ctx, &unary.expr, Type::Number)?; - expect.check(&expr.attrs, Type::Number) + self.analyze_expr(ctx, unary.expr, Type::Number)?; + expect.check(node.attrs, Type::Number) } Operator::Not => { - self.analyze_expr(ctx, &unary.expr, Type::Bool)?; - expect.check(&expr.attrs, Type::Bool) + self.analyze_expr(ctx, unary.expr, Type::Bool)?; + expect.check(node.attrs, Type::Bool) } _ => unreachable!(), }, - Value::Group(expr) => Ok(self.analyze_expr(ctx, expr.as_ref(), expect)?), + Value::Group(expr) => Ok(self.analyze_expr(ctx, *expr, expect)?), } } fn analyze_access( &mut self, - attrs: &Attrs, - access: &Value, + attrs: Attrs, + access: ExprRef, expect: Type, ) -> AnalysisResult { struct State { @@ -1501,11 +1519,12 @@ impl<'a> Analysis<'a> { fn go<'a>( scope: &'a mut Scope, + arena: &'a ExprArena, sys: &'a AnalysisOptions, - attrs: &'a Attrs, - value: &'a Value, + expr: ExprRef, ) -> AnalysisResult> { - match value { + let node = arena.get(expr); + match node.value { Value::Id(id) => { if let Some(tpe) = sys.default_scope.entries.get(id.as_str()) { Ok(State::new(Def::System(tpe))) @@ -1513,14 +1532,14 @@ impl<'a> Analysis<'a> { Ok(State::new(Def::User(tpe))) } else { Err(AnalysisError::VariableUndeclared( - attrs.pos.line, - attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, id.clone(), )) } } Value::Access(access) => { - let mut state = go(scope, sys, &access.target.attrs, &access.target.value)?; + let mut state = go(scope, arena, sys, access.target)?; // TODO - we should consider make that field and depth configurable. let is_data_field = state.depth == 0 && access.field == "data"; @@ -1535,7 +1554,7 @@ impl<'a> Analysis<'a> { match state.definition { Def::User(tpe) => { if matches!(tpe, Type::Unspecified) && state.dynamic { - *tpe = Type::Record(BTreeMap::from([( + *tpe = Type::Record(FxHashMap::from_iter([( access.field.clone(), Type::Unspecified, )])); @@ -1551,7 +1570,7 @@ impl<'a> Analysis<'a> { } if let Type::Record(fields) = tpe { - match fields.entry(access.field.clone()) { + return match fields.entry(access.field.clone()) { Entry::Vacant(entry) => { if state.dynamic || is_data_field { return Ok(State { @@ -1563,11 +1582,11 @@ impl<'a> Analysis<'a> { }); } - return Err(AnalysisError::FieldUndeclared( - attrs.pos.line, - attrs.pos.col, + Err(AnalysisError::FieldUndeclared( + node.attrs.pos.line, + node.attrs.pos.col, access.field.clone(), - )); + )) } Entry::Occupied(entry) => { @@ -1577,12 +1596,12 @@ impl<'a> Analysis<'a> { ..state }); } - } + }; } Err(AnalysisError::ExpectRecord( - attrs.pos.line, - attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, tpe.clone(), )) } @@ -1606,15 +1625,15 @@ impl<'a> Analysis<'a> { } return Err(AnalysisError::FieldUndeclared( - attrs.pos.line, - attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, access.field.clone(), )); } Err(AnalysisError::ExpectRecord( - attrs.pos.line, - attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, tpe.clone(), )) } @@ -1632,7 +1651,7 @@ impl<'a> Analysis<'a> { } } - let state = go(&mut self.scope, self.options, attrs, access)?; + let state = go(&mut self.scope, self.arena, self.options, access)?; match state.definition { Def::User(tpe) => { @@ -1647,11 +1666,11 @@ impl<'a> Analysis<'a> { } fn projection_type(&self, query: &Query) -> Type { - self.project_type(&query.projection.value) + self.project_type(query.projection) } - fn project_type(&self, value: &Value) -> Type { - match value { + fn project_type(&self, node: ExprRef) -> Type { + match self.arena.get(node).value { Value::Number(_) => Type::Number, Value::String(_) => Type::String, Value::Bool(_) => Type::Bool, @@ -1667,8 +1686,8 @@ impl<'a> Analysis<'a> { Value::Array(exprs) => { let mut project = Type::Unspecified; - for expr in exprs { - let tmp = self.project_type(&expr.value); + for expr in exprs.iter().copied() { + let tmp = self.project_type(expr); if !matches!(tmp, Type::Unspecified) { project = tmp; @@ -1681,11 +1700,11 @@ impl<'a> Analysis<'a> { Value::Record(fields) => Type::Record( fields .iter() - .map(|field| (field.name.clone(), self.project_type(&field.value.value))) + .map(|field| (field.name.clone(), self.project_type(field.expr))) .collect(), ), Value::Access(access) => { - let tpe = self.project_type(&access.target.value); + let tpe = self.project_type(access.target); if let Type::Record(fields) = tpe { fields .get(access.field.as_str()) @@ -1705,7 +1724,7 @@ impl<'a> Analysis<'a> { Value::Binary(binary) => match binary.operator { Operator::Add | Operator::Sub | Operator::Mul | Operator::Div => Type::Number, Operator::As => { - if let Value::Id(n) = &binary.rhs.as_ref().value + if let Value::Id(n) = self.arena.get(binary.rhs).value && let Some(tpe) = name_to_type(self.options, n.as_str()) { tpe @@ -1742,7 +1761,7 @@ impl<'a> Analysis<'a> { | Operator::Contains | Operator::As => unreachable!(), }, - Value::Group(expr) => self.project_type(&expr.value), + Value::Group(expr) => self.project_type(*expr), } } } diff --git a/src/arena.rs b/src/arena.rs new file mode 100644 index 0000000..9695707 --- /dev/null +++ b/src/arena.rs @@ -0,0 +1,75 @@ +use crate::{Attrs, Expr, ExprKey, ExprPtr, ExprRef, Value}; +use rustc_hash::FxBuildHasher; +use serde::Serialize; +use std::hash::BuildHasher; + +#[derive(Debug, Serialize)] +struct Slot { + attrs: Attrs, + value: Value, +} + +/// An arena-based allocator for EventQL expressions. +/// +/// The `ExprArena` provides a memory-efficient way to store and manage AST nodes +/// by using a flat vector and returning lightweight [`ExprRef`] handles. +#[derive(Default, Serialize)] +pub struct ExprArena { + #[serde(skip_serializing)] + hasher: FxBuildHasher, + slots: Vec, +} + +/// A view into a single node within an [`ExprArena`]. +/// +/// This struct provides access to the attributes and value of a node +/// without transferring ownership. It's typically obtained by calling [`ExprArena::get`]. +#[derive(Debug, Copy, Clone)] +pub struct Node<'a> { + /// Metadata about this expression (e.g., source position) + pub attrs: Attrs, + /// The actual kind and value of the expression + pub value: &'a Value, + /// The stable reference to this node in the arena + pub node_ref: ExprRef, +} + +impl<'a> Node<'a> { + pub fn as_expr(&self) -> Expr { + Expr { + attrs: self.attrs, + node_ref: self.node_ref, + } + } +} + +impl ExprArena { + /// Allocates a new expression in the arena. + /// + /// This method takes an expression's attributes and value, hashes the value + /// to create a stable [`ExprKey`], and stores it in the arena. It returns + /// an [`ExprRef`] which can be used to retrieve the expression later. + pub fn alloc(&mut self, attrs: Attrs, value: Value) -> ExprRef { + let key = ExprKey(self.hasher.hash_one(&value)); + + let ptr = ExprPtr(self.slots.len()); + self.slots.push(Slot { attrs, value }); + + ExprRef { key, ptr } + } + + /// Retrieves a node from the arena using an [`ExprRef`]. + /// + /// # Panics + /// + /// Panics if the [`ExprRef`] contains an invalid pointer that is out of bounds + /// of the arena's internal storage. + pub fn get(&self, node_ref: ExprRef) -> Node<'_> { + let slot = &self.slots[node_ref.ptr.0]; + Node { + attrs: slot.attrs, + value: &slot.value, + node_ref, + } + } +} diff --git a/src/ast.rs b/src/ast.rs index 30b291c..7748e90 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -11,18 +11,20 @@ //! - [`Value`] - The various kinds of expression values (literals, operators, etc.) //! - [`Source`] - Data sources in FROM clauses //! -use std::{ - collections::BTreeMap, - fmt::{self, Display}, - mem, -}; - +use crate::arena::ExprArena; use crate::{ analysis::{AnalysisOptions, Typed, static_analysis}, error::{AnalysisError, Error}, token::{Operator, Token}, }; +use ordered_float::OrderedFloat; +use rustc_hash::FxHashMap; use serde::Serialize; +use std::hash::{Hash, Hasher}; +use std::{ + fmt::{self, Display}, + mem, +}; /// Position information for source code locations. /// @@ -174,7 +176,7 @@ pub enum Type { /// Array type Array(Box), /// Record (object) type - Record(BTreeMap), + Record(FxHashMap), /// Subject pattern type Subject, /// Function type with support for optional parameters. @@ -233,10 +235,12 @@ pub enum Type { /// /// ``` /// use eventql_parser::{parse_query, prelude::AnalysisOptions}; + /// use eventql_parser::arena::ExprArena; /// - /// let query = parse_query("FROM e IN events PROJECT INTO { ts: e.data.timestamp as CustomTimestamp }").unwrap(); + /// let mut arena = ExprArena::default(); + /// let query = parse_query(&mut arena, "FROM e IN events PROJECT INTO { ts: e.data.timestamp as CustomTimestamp }").unwrap(); /// let options = AnalysisOptions::default().add_custom_type("CustomTimestamp"); - /// let typed_query = query.run_static_analysis(&options).unwrap(); + /// let typed_query = query.run_static_analysis(&arena, &options).unwrap(); /// ``` Custom(String), } @@ -348,7 +352,7 @@ impl Display for Type { } impl Type { - pub fn as_record_or_panic_mut(&mut self) -> &mut BTreeMap { + pub fn as_record_or_panic_mut(&mut self) -> &mut FxHashMap { if let Self::Record(r) = self { return r; } @@ -360,7 +364,7 @@ impl Type { /// /// * If `self` is `Type::Unspecified` then `self` is updated to the more specific `Type`. /// * If `self` is `Type::Subject` and is checked against a `Type::String` then `self` is updated to `Type::String` - pub fn check(self, attrs: &Attrs, other: Type) -> Result { + pub fn check(self, attrs: Attrs, other: Type) -> Result { match (self, other) { (Self::Unspecified, other) => Ok(other), (this, Self::Unspecified) => Ok(this), @@ -397,8 +401,8 @@ impl Type { return Ok(Self::Record(a)); } - for (ak, bk) in a.keys().zip(b.keys()) { - if ak != bk { + for bk in b.keys() { + if !a.contains_key(bk) { return Err(AnalysisError::TypeMismatch( attrs.pos.line, attrs.pos.col, @@ -408,7 +412,8 @@ impl Type { } } - for (av, bv) in a.values_mut().zip(b.into_values()) { + for (bk, bv) in b.into_iter() { + let av = a.get_mut(&bk).unwrap(); let a = mem::take(av); *av = a.check(attrs, bv)?; } @@ -467,7 +472,7 @@ impl Type { /// /// These attributes provide metadata about an expression, including its /// position in the source code, scope information, and type information. -#[derive(Debug, Clone, Copy, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] pub struct Attrs { /// Source position of this expression pub pos: Pos, @@ -480,17 +485,53 @@ impl Attrs { } } +impl<'a> From> for Attrs { + fn from(value: Token<'a>) -> Self { + Self { pos: value.into() } + } +} + +/// Internal pointer to an expression in the arena. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize)] +pub struct ExprPtr(pub(crate) usize); + +/// Internal hash key for an expression to provide structural equality. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)] +pub struct ExprKey(pub(crate) u64); + +/// A reference to an expression stored in an [`ExprArena`]. +/// +/// This is a lightweight handle that combines a hash key for fast comparison +/// and a pointer for fast lookup. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize)] +pub struct ExprRef { + pub(crate) key: ExprKey, + pub(crate) ptr: ExprPtr, +} + +impl Hash for ExprRef { + fn hash(&self, state: &mut H) { + self.key.hash(state); + } +} + /// An expression with metadata. /// /// This is the fundamental building block of the AST. Every expression /// carries attributes (position, scope, type) and a value that determines /// what kind of expression it is. -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] pub struct Expr { /// Metadata about this expression pub attrs: Attrs, /// The value/kind of this expression - pub value: Value, + pub node_ref: ExprRef, +} + +impl Hash for Expr { + fn hash(&self, state: &mut H) { + self.node_ref.hash(state); + } } /// Field access expression (e.g., `e.data.price`). @@ -502,10 +543,10 @@ pub struct Expr { /// /// In the query `WHERE e.data.user.id == 1`, the expression `e.data.user.id` /// is parsed as nested `Access` nodes. -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] pub struct Access { /// The target expression being accessed - pub target: Box, + pub target: ExprRef, /// The name of the field being accessed pub field: String, } @@ -517,23 +558,25 @@ pub struct Access { /// # Examples /// /// In the query `WHERE count(e.items) > 5`, the `count(e.items)` is an `App` node. -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] pub struct App { /// Name of the function being called pub func: String, /// Arguments passed to the function - pub args: Vec, + pub args: Vec, } /// A field in a record literal (e.g., `{name: "Alice", age: 30}`). /// /// Represents a key-value pair in a record construction. -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] pub struct Field { + /// Field attributes + pub attrs: Attrs, /// Field name pub name: String, /// Field value expression - pub value: Expr, + pub expr: ExprRef, } /// Binary operation (e.g., `a + b`, `x == y`, `p AND q`). @@ -545,14 +588,14 @@ pub struct Field { /// /// In `WHERE e.price > 100 AND e.active == true`, there are multiple /// binary operations: `>`, `==`, and `AND`. -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] pub struct Binary { /// Left-hand side operand - pub lhs: Box, + pub lhs: ExprRef, /// The operator pub operator: Operator, /// Right-hand side operand - pub rhs: Box, + pub rhs: ExprRef, } /// Unary operation (e.g., `-x`, `NOT active`). @@ -562,22 +605,22 @@ pub struct Binary { /// # Examples /// /// In `WHERE NOT e.deleted`, the `NOT e.deleted` is a unary operation. -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] pub struct Unary { /// The operator (Add for +, Sub for -, Not for NOT) pub operator: Operator, /// The operand expression - pub expr: Box, + pub expr: ExprRef, } /// The kind of value an expression represents. /// /// This enum contains all the different types of expressions that can appear /// in an EventQL query, from simple literals to complex operations. -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] pub enum Value { /// Numeric literal (e.g., `42`, `3.14`) - Number(f64), + Number(OrderedFloat), /// String literal (e.g., `"hello"`) String(String), /// Boolean literal (`true` or `false`) @@ -585,7 +628,7 @@ pub enum Value { /// Identifier (e.g., variable name `e`, `x`) Id(String), /// Array literal (e.g., `[1, 2, 3]`) - Array(Vec), + Array(Vec), /// Record literal (e.g., `{name: "Alice", age: 30}`) Record(Vec), /// Field access (e.g., `e.data.price`) @@ -597,7 +640,7 @@ pub enum Value { /// Unary operation (e.g., `-x`, `NOT active`) Unary(Unary), /// Grouped/parenthesized expression (e.g., `(a + b)`) - Group(Box), + Group(ExprRef), } /// A source binding. A name attached to a source of events. @@ -658,7 +701,7 @@ pub enum SourceKind { #[derive(Debug, Clone, Serialize)] pub struct OrderBy { /// Expression to sort by - pub expr: Expr, + pub expr: ExprRef, /// Sort direction (ascending or descending) pub order: Order, } @@ -685,10 +728,10 @@ pub enum Order { #[derive(Debug, Clone, Serialize)] pub struct GroupBy { /// Expression to group by - pub expr: Expr, + pub expr: ExprRef, /// Predicate to filter groups after aggregation - pub predicate: Option, + pub predicate: Option, } /// Result set limit specification. @@ -736,8 +779,11 @@ pub struct Raw; /// /// ``` /// use eventql_parser::parse_query; +/// use eventql_parser::arena::ExprArena; /// +/// let mut arena = ExprArena::default(); /// let query = parse_query( +/// &mut arena, /// "FROM e IN events \ /// WHERE e.price > 100 \ /// ORDER BY e.timestamp DESC \ @@ -757,7 +803,7 @@ pub struct Query { /// FROM clause sources (must have at least one) pub sources: Vec>, /// Optional WHERE clause filter predicate - pub predicate: Option, + pub predicate: Option, /// Optional GROUP BY clause expression pub group_by: Option, /// Optional ORDER BY clause @@ -765,7 +811,7 @@ pub struct Query { /// Optional LIMIT clause (TOP or SKIP) pub limit: Option, /// PROJECT INTO clause expression (required) - pub projection: Expr, + pub projection: ExprRef, /// Remove duplicate rows from the query's results pub distinct: bool, /// Type-level metadata about the query's analysis state. @@ -804,7 +850,11 @@ impl Query { /// # Returns /// /// Returns a typed query on success, or an error if type checking fails. - pub fn run_static_analysis(self, options: &AnalysisOptions) -> crate::Result> { - static_analysis(options, self).map_err(Error::Analysis) + pub fn run_static_analysis( + self, + arena: &ExprArena, + options: &AnalysisOptions, + ) -> crate::Result> { + static_analysis(arena, options, self).map_err(Error::Analysis) } } diff --git a/src/lib.rs b/src/lib.rs index 98cf775..0df9709 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ //! designed for event sourcing systems. It allows you to parse EQL query strings into //! an abstract syntax tree (AST) that can be analyzed or executed. mod analysis; +pub mod arena; mod ast; mod error; mod lexer; @@ -12,6 +13,7 @@ mod parser; mod tests; mod token; +use crate::arena::ExprArena; use crate::prelude::{parse, tokenize}; pub use ast::*; @@ -38,13 +40,17 @@ pub type Result = std::result::Result; /// /// ``` /// use eventql_parser::parse_query; +/// use eventql_parser::arena::ExprArena; +/// +/// let mut arena = ExprArena::default(); /// /// // Parse a simple query -/// let query = parse_query("FROM e IN events WHERE e.id == 1 PROJECT INTO e").unwrap(); +/// let query = parse_query(&mut arena, "FROM e IN events WHERE e.id == 1 PROJECT INTO e").unwrap(); /// assert!(query.predicate.is_some()); /// /// // Parse with multiple clauses /// let complex = parse_query( +/// &mut arena, /// "FROM e IN events \ /// WHERE e.price > 100 \ /// ORDER BY e.timestamp DESC \ @@ -55,12 +61,12 @@ pub type Result = std::result::Result; /// assert!(complex.limit.is_some()); /// /// // Handle errors -/// match parse_query("FROM e IN events WHERE") { +/// match parse_query(&mut arena, "FROM e IN events WHERE") { /// Ok(_) => println!("Parsed successfully"), /// Err(e) => println!("Parse error: {}", e), /// } /// ``` -pub fn parse_query(input: &str) -> Result> { +pub fn parse_query(arena: &mut ExprArena, input: &str) -> Result> { let tokens = tokenize(input)?; - Ok(parse(tokens.as_slice())?) + Ok(parse(arena, tokens.as_slice())?) } diff --git a/src/parser.rs b/src/parser.rs index bb5c2c7..2b31781 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -8,13 +8,14 @@ //! //! - [`parse`] - Convert a slice of tokens into a Query AST +use crate::arena::ExprArena; use crate::ast::{ - Access, App, Attrs, Binary, Expr, Field, Limit, Order, OrderBy, Query, Source, SourceKind, - Unary, Value, + Access, App, Attrs, Binary, Field, Limit, Order, OrderBy, Query, Source, SourceKind, Unary, + Value, }; use crate::error::ParserError; use crate::token::{Operator, Sym, Symbol, Token}; -use crate::{Binding, GroupBy, Raw}; +use crate::{Binding, ExprRef, GroupBy, Raw}; /// Result type for parser operations. /// @@ -27,6 +28,7 @@ pub type ParseResult = Result; /// representing the structure of the EventQL query or expression. pub struct Parser<'a> { input: &'a [Token<'a>], + arena: &'a mut ExprArena, offset: usize, } @@ -37,12 +39,18 @@ impl<'a> Parser<'a> { /// /// ```rust /// use eventql_parser::prelude::{tokenize, Parser}; + /// use eventql_parser::arena::ExprArena; /// + /// let mut arena = ExprArena::default(); /// let tokens = tokenize("1 + 2").unwrap(); - /// let parser = Parser::new(tokens.as_slice()); + /// let parser = Parser::new(&mut arena, tokens.as_slice()); /// ``` - pub fn new(input: &'a [Token<'a>]) -> Self { - Self { input, offset: 0 } + pub fn new(arena: &'a mut ExprArena, input: &'a [Token<'a>]) -> Self { + Self { + arena, + input, + offset: 0, + } } fn peek<'b>(&'b self) -> Token<'a> { @@ -115,7 +123,7 @@ impl<'a> Parser<'a> { Ok(Source { binding, kind }) } - fn parse_where_clause(&mut self) -> ParseResult { + fn parse_where_clause(&mut self) -> ParseResult { expect_keyword(self.shift(), "where")?; self.parse_expr() } @@ -207,11 +215,13 @@ impl<'a> Parser<'a> { /// /// ```rust /// use eventql_parser::prelude::{tokenize, Parser}; + /// use eventql_parser::arena::ExprArena; /// + /// let mut arena = ExprArena::default(); /// let tokens = tokenize("NOW()").unwrap(); - /// let expr = Parser::new(tokens.as_slice()).parse_expr().unwrap(); + /// let expr = Parser::new(&mut arena, tokens.as_slice()).parse_expr().unwrap(); /// ``` - pub fn parse_expr(&mut self) -> ParseResult { + pub fn parse_expr(&mut self) -> ParseResult { let token = self.peek(); match token.sym { @@ -231,7 +241,7 @@ impl<'a> Parser<'a> { } } - fn parse_primary(&mut self) -> ParseResult { + fn parse_primary(&mut self) -> ParseResult { let token = self.shift(); let value = match token.sym { @@ -262,21 +272,16 @@ impl<'a> Parser<'a> { }) } else if matches!(self.peek().sym, Sym::Symbol(Symbol::Dot)) { self.shift(); + let attrs = token.into(); let mut access = Access { - target: Box::new(Expr { - attrs: Attrs::new(token.into()), - value: Value::Id(name.to_owned()), - }), + target: self.arena.alloc(attrs, Value::Id(name.to_owned())), field: self.parse_ident()?, }; while matches!(self.peek().sym, Sym::Symbol(Symbol::Dot)) { self.shift(); access = Access { - target: Box::new(Expr { - attrs: access.target.attrs, - value: Value::Access(access), - }), + target: self.arena.alloc(attrs, Value::Access(access)), field: self.parse_ident()?, }; } @@ -288,13 +293,13 @@ impl<'a> Parser<'a> { } Sym::String(s) => Value::String(s.to_owned()), - Sym::Number(n) => Value::Number(n), + Sym::Number(n) => Value::Number(n.into()), Sym::Symbol(Symbol::OpenParen) => { let expr = self.parse_expr()?; expect_symbol(self.shift(), Symbol::CloseParen)?; - Value::Group(Box::new(expr)) + Value::Group(expr) } Sym::Symbol(Symbol::OpenBracket) => { @@ -318,20 +323,30 @@ impl<'a> Parser<'a> { let mut fields = vec![]; if !matches!(self.peek().sym, Sym::Symbol(Symbol::CloseBrace)) { + let attrs: Attrs = self.peek().into(); let name = self.parse_ident()?; expect_symbol(self.shift(), Symbol::Colon)?; let value = self.parse_expr()?; - fields.push(Field { name, value }); + fields.push(Field { + attrs, + name, + expr: value, + }); while matches!(self.peek().sym, Sym::Symbol(Symbol::Comma)) { self.shift(); + let attrs: Attrs = self.peek().into(); let name = self.parse_ident()?; expect_symbol(self.shift(), Symbol::Colon)?; let value = self.parse_expr()?; - fields.push(Field { name, value }); + fields.push(Field { + attrs, + name, + expr: value, + }); } } @@ -343,7 +358,7 @@ impl<'a> Parser<'a> { Sym::Operator(op) if matches!(op, Operator::Add | Operator::Sub | Operator::Not) => { Value::Unary(Unary { operator: op, - expr: Box::new(self.parse_expr()?), + expr: self.parse_expr()?, }) } @@ -356,14 +371,14 @@ impl<'a> Parser<'a> { } }; - Ok(Expr { - attrs: Attrs::new(token.into()), - value, - }) + let attrs = token.into(); + + Ok(self.arena.alloc(attrs, value)) } - fn parse_binary(&mut self, min_bind: u64) -> ParseResult { + fn parse_binary(&mut self, min_bind: u64) -> ParseResult { let mut lhs = self.parse_primary()?; + let lhs_attrs = self.arena.get(lhs).attrs; loop { let token = self.peek(); @@ -381,22 +396,18 @@ impl<'a> Parser<'a> { self.shift(); let rhs = self.parse_binary(rhs_bind)?; + let node = self.arena.get(rhs); - if matches!(operator, Operator::As) && !matches!(rhs.value, Value::Id(_)) { + if matches!(operator, Operator::As) && !matches!(node.value, Value::Id(_)) { return Err(ParserError::ExpectedType( - rhs.attrs.pos.line, - rhs.attrs.pos.col, + node.attrs.pos.line, + node.attrs.pos.col, )); } - lhs = Expr { - attrs: lhs.attrs, - value: Value::Binary(Binary { - lhs: Box::new(lhs), - operator, - rhs: Box::new(rhs), - }), - }; + lhs = self + .arena + .alloc(lhs_attrs, Value::Binary(Binary { lhs, operator, rhs })); } Ok(lhs) @@ -556,8 +567,8 @@ fn binding_pow(op: Operator) -> (u64, u64) { /// 3. Additive (`+`, `-`) /// 4. Comparison (`<`, `<=`, `>`, `>=`, `==`, `!=`) /// 5. Logical (`AND`, `OR`, `XOR`) -pub fn parse<'a>(input: &'a [Token<'a>]) -> ParseResult> { - let mut parser = Parser::new(input); +pub fn parse<'a>(arena: &'a mut ExprArena, input: &'a [Token<'a>]) -> ParseResult> { + let mut parser = Parser::new(arena, input); parser.parse_query() } diff --git a/src/tests/analysis.rs b/src/tests/analysis.rs index 98d76f0..9ea7ad4 100644 --- a/src/tests/analysis.rs +++ b/src/tests/analysis.rs @@ -1,5 +1,6 @@ use crate::{ Type, + arena::ExprArena, lexer::tokenize, parse_query, parser::Parser, @@ -8,117 +9,225 @@ use crate::{ #[test] fn test_infer_wrong_where_clause_1() { - let query = parse_query(include_str!("./resources/infer_wrong_where_clause_1.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/infer_wrong_where_clause_1.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_infer_wrong_where_clause_2() { - let query = parse_query(include_str!("./resources/infer_wrong_where_clause_2.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/infer_wrong_where_clause_2.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_rename_duplicate_variable_names() { - let query = parse_query(include_str!( - "./resources/rename_duplicate_variable_names.eql" - )) - .unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/rename_duplicate_variable_names.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_rename_non_existing_variable() { - let query = parse_query(include_str!("./resources/rename_non_existing_variable.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/rename_non_existing_variable.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_rename_subquery() { - let query = parse_query(include_str!("./resources/rename_subquery.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query(&mut arena, include_str!("./resources/rename_subquery.eql")); + + insta::with_settings!({sort_maps => true}, { + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })) + }); } #[test] fn test_analyze_valid_contains() { - let query = parse_query(include_str!("./resources/valid_contains.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query(&mut arena, include_str!("./resources/valid_contains.eql")); + + insta::with_settings!({sort_maps => true}, { + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); + }) } #[test] fn test_analyze_invalid_type_contains() { - let query = parse_query(include_str!("./resources/invalid_type_contains.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/invalid_type_contains.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_analyze_valid_type_conversion() { - let query = parse_query(include_str!("./resources/valid_type_conversion.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/valid_type_conversion.eql"), + ); + insta::assert_yaml_snapshot!(query.map(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_analyze_invalid_type_conversion_custom_type() { - let query = parse_query(include_str!("./resources/type_conversion_custom_type.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/type_conversion_custom_type.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_analyze_valid_type_conversion_custom_type() { - let query = parse_query(include_str!("./resources/type_conversion_custom_type.eql")).unwrap(); - insta::assert_yaml_snapshot!( - query.run_static_analysis(&AnalysisOptions::default().add_custom_type("Foobar")) + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/type_conversion_custom_type.eql"), ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis( + &arena, + &AnalysisOptions::default().add_custom_type("Foobar"), + ) + .map(|q| q.view(&arena)) + })); } #[test] fn test_analyze_valid_type_conversion_weird_case() { - let query = parse_query(include_str!( - "./resources/valid_type_conversion-weird-case.eql" - )) - .unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/valid_type_conversion-weird-case.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_analyze_prevent_using_aggregate_with_source_based_props() { - let query = parse_query(include_str!( - "./resources/aggregate_with_sourced_bases_props.eql" - )) - .unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/aggregate_with_sourced_bases_props.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_analyze_valid_agg_usage() { - let query = parse_query(include_str!("./resources/valid_agg_usage.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query(&mut arena, include_str!("./resources/valid_agg_usage.eql")); + + insta::with_settings!({sort_maps => true}, { + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })) + }); } #[test] fn test_analyze_reject_agg_in_predicate() { - let query = parse_query(include_str!("./resources/reject_agg_in_predicate.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/reject_agg_in_predicate.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_analyze_agg_must_use_source_bound() { - let query = parse_query(include_str!("./resources/agg_must_use_source_bound.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/agg_must_use_source_bound.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_analyze_optional_param_func() { - let query = parse_query(include_str!("./resources/optional_param_func.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/optional_param_func.eql"), + ); + + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_typecheck_datetime_contravariance_1() { + let mut arena = ExprArena::default(); let tokens = tokenize("e.time").unwrap(); - let expr = Parser::new(tokens.as_slice()).parse_expr().unwrap(); + let expr = Parser::new(&mut arena, tokens.as_slice()) + .parse_expr() + .unwrap(); let options = &AnalysisOptions::default(); - let mut analysis = Analysis::new(&options); + let mut analysis = Analysis::new(&arena, &options); analysis .scope_mut() @@ -128,143 +237,237 @@ fn test_typecheck_datetime_contravariance_1() { // `e.time` is a `Type::DateTime` but it will typecheck if a `Type::Date` is expected insta::assert_yaml_snapshot!(analysis.analyze_expr( &mut AnalysisContext::default(), - &expr, + expr, Type::Date )); } #[test] fn test_typecheck_datetime_contravariance_2() { + let mut arena = ExprArena::default(); let tokens = tokenize("NOW()").unwrap(); - let expr = Parser::new(tokens.as_slice()).parse_expr().unwrap(); + let expr = Parser::new(&mut arena, tokens.as_slice()) + .parse_expr() + .unwrap(); let options = &AnalysisOptions::default(); - let mut analysis = Analysis::new(&options); + let mut analysis = Analysis::new(&arena, &options); // `NOW()` is a `Type::DateTime` but it will typecheck if a `Type::Time` is expected insta::assert_yaml_snapshot!(analysis.analyze_expr( &mut AnalysisContext::default(), - &expr, + expr, Type::Time )); } #[test] fn test_typecheck_datetime_contravariance_3() { + let mut arena = ExprArena::default(); let tokens = tokenize("YEAR(NOW())").unwrap(); - let expr = Parser::new(tokens.as_slice()).parse_expr().unwrap(); + let expr = Parser::new(&mut arena, tokens.as_slice()) + .parse_expr() + .unwrap(); let options = &AnalysisOptions::default(); - let mut analysis = Analysis::new(&options); + let mut analysis = Analysis::new(&arena, &options); insta::assert_yaml_snapshot!(analysis.analyze_expr( &mut AnalysisContext::default(), - &expr, + expr, Type::Number )); } #[test] fn test_typecheck_datetime_contravariance_4() { + let mut arena = ExprArena::default(); let tokens = tokenize("HOUR(NOW())").unwrap(); - let expr = Parser::new(tokens.as_slice()).parse_expr().unwrap(); + let expr = Parser::new(&mut arena, tokens.as_slice()) + .parse_expr() + .unwrap(); let options = &AnalysisOptions::default(); - let mut analysis = Analysis::new(&options); + let mut analysis = Analysis::new(&arena, &options); insta::assert_yaml_snapshot!(analysis.analyze_expr( &mut AnalysisContext::default(), - &expr, + expr, Type::Number )); } #[test] fn test_analyze_allow_regular_property_project_into() { - let query = parse_query(include_str!( - "./resources/allow_regular_property_project_into.eql" - )) - .unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/allow_regular_property_project_into.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_analyze_undeclared_variable_in_project_into_clause() { - let query = parse_query(include_str!( - "./resources/undeclared_variable_in_project_into_clause.eql" - )) - .unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/undeclared_variable_in_project_into_clause.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_analyze_lowercase_function() { - let query = parse_query(include_str!("./resources/lowercase_function.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/lowercase_function.eql"), + ); + + insta::with_settings!({sort_maps => true}, { + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })) + }); } #[test] fn test_analyze_project_agg_value() { - let query = parse_query(include_str!("./resources/project_agg_value.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/project_agg_value.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_analyze_reject_constant_expr_in_project_into_clause() { - let query = parse_query(include_str!("./resources/reject_constant_expr.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/reject_constant_expr.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_analyze_allow_constant_agg_func() { - let query = parse_query(include_str!("./resources/allow_constant_agg_func.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/allow_constant_agg_func.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_analyze_reject_group_by_with_order_by_no_agg() { - let query = parse_query(include_str!( - "./resources/reject_group_by_with_order_by_no_agg.eql" - )) - .unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/reject_group_by_with_order_by_no_agg.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_analyze_accept_group_by_with_order_by_with_agg() { - let query = parse_query(include_str!( - "./resources/accept_group_by_with_order_by_with_agg.eql" - )) - .unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/accept_group_by_with_order_by_with_agg.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_analyze_reject_group_by_no_agg() { - let query = parse_query(include_str!("./resources/reject_group_by_no_agg.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/reject_group_by_no_agg.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_analyze_reject_group_by_no_agg_in_rec() { - let query = parse_query(include_str!( - "./resources/reject_group_by_no_agg_in_rec.eql" - )) - .unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/reject_group_by_no_agg_in_rec.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_analyze_accept_group_by_with_agg_rec() { - let query = parse_query(include_str!("./resources/accept_group_by_with_agg_rec.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/accept_group_by_with_agg_rec.eql"), + ); + + insta::with_settings!({sort_maps => true}, { + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })) + }); } #[test] fn test_reject_invalid_having_clause() { - let query = parse_query(include_str!("./resources/reject_invalid_having_clause.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/reject_invalid_having_clause.eql"), + ); + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })); } #[test] fn test_accept_valid_having_clause() { - let query = parse_query(include_str!("./resources/valid_having_clause.eql")).unwrap(); - insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default())); + let mut arena = ExprArena::default(); + let query = parse_query( + &mut arena, + include_str!("./resources/valid_having_clause.eql"), + ); + + insta::with_settings!({sort_maps => true}, { + insta::assert_yaml_snapshot!(query.and_then(|q| { + q.run_static_analysis(&arena, &Default::default()) + .map(|q| q.view(&arena)) + })) + }); } diff --git a/src/tests/mod.rs b/src/tests/mod.rs index a826bda..0f5bb8b 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,3 +1,185 @@ +use crate::arena::ExprArena; +use crate::ast::{Binding, Limit, Order, Query}; +use crate::token::Operator; +use crate::{Attrs, ExprRef, SourceKind, Value}; +use ordered_float::OrderedFloat; +use serde::Serialize; + mod analysis; mod lexer; mod parser; + +#[derive(Debug, Serialize)] +pub struct ExprView { + pub attrs: Attrs, + pub value: ValueView, +} + +impl ExprView { + pub fn new(attrs: Attrs, value: ValueView) -> Self { + Self { attrs, value } + } +} + +#[derive(Debug, Serialize)] +pub enum ValueView { + Number(OrderedFloat), + String(String), + Bool(bool), + Id(String), + Array(Vec), + Record(Vec), + Access(AccessView), + App(AppView), + Binary(BinaryView), + Unary(UnaryView), + Group(Box), +} + +#[derive(Debug, Serialize)] +pub struct FieldView { + pub attrs: Attrs, + pub name: String, + pub value: ExprView, +} + +#[derive(Debug, Serialize)] +pub struct AccessView { + pub target: Box, + pub field: String, +} + +#[derive(Debug, Serialize)] +pub struct AppView { + pub func: String, + pub args: Vec, +} + +#[derive(Debug, Serialize)] +pub struct BinaryView { + pub lhs: Box, + pub operator: Operator, + pub rhs: Box, +} + +#[derive(Debug, Serialize)] +pub struct UnaryView { + pub operator: Operator, + pub expr: Box, +} + +#[derive(Debug, Serialize)] +pub struct QueryView { + pub attrs: Attrs, + pub sources: Vec>, + pub predicate: Option, + pub group_by: Option, + pub order_by: Option, + pub limit: Option, + pub projection: ExprView, + pub distinct: bool, + pub meta: A, +} + +#[derive(Debug, Serialize)] +pub struct SourceView { + pub binding: Binding, + pub kind: SourceKindView, +} + +#[derive(Debug, Serialize)] +pub enum SourceKindView { + Name(String), + Subject(String), + Subquery(Box>), +} + +#[derive(Debug, Serialize)] +pub struct GroupByView { + pub expr: ExprView, + pub predicate: Option, +} + +#[derive(Debug, Serialize)] +pub struct OrderByView { + pub expr: ExprView, + pub order: Order, +} + +impl ExprRef { + pub fn view(self, arena: &ExprArena) -> ExprView { + let node = arena.get(self); + let value = match node.value { + Value::Number(n) => ValueView::Number(*n), + Value::String(s) => ValueView::String(s.clone()), + Value::Bool(b) => ValueView::Bool(*b), + Value::Id(id) => ValueView::Id(id.clone()), + Value::Array(arr) => ValueView::Array(arr.iter().map(|e| e.view(arena)).collect()), + Value::Record(fields) => ValueView::Record( + fields + .iter() + .map(|f| FieldView { + attrs: f.attrs, + name: f.name.clone(), + value: f.expr.view(arena), + }) + .collect(), + ), + Value::Access(access) => ValueView::Access(AccessView { + target: Box::new(access.target.view(arena)), + field: access.field.clone(), + }), + Value::App(app) => ValueView::App(AppView { + func: app.func.clone(), + args: app.args.iter().map(|e| e.view(arena)).collect(), + }), + Value::Binary(binary) => ValueView::Binary(BinaryView { + lhs: Box::new(binary.lhs.view(arena)), + operator: binary.operator, + rhs: Box::new(binary.rhs.view(arena)), + }), + Value::Unary(unary) => ValueView::Unary(UnaryView { + operator: unary.operator, + expr: Box::new(unary.expr.view(arena)), + }), + Value::Group(expr) => ValueView::Group(Box::new(expr.view(arena))), + }; + + ExprView::new(node.attrs, value) + } +} + +impl Query { + pub fn view(self, arena: &ExprArena) -> QueryView { + QueryView { + attrs: self.attrs, + sources: self + .sources + .into_iter() + .map(|s| SourceView { + binding: s.binding.clone(), + kind: match s.kind { + SourceKind::Name(name) => SourceKindView::Name(name), + SourceKind::Subject(subject) => SourceKindView::Subject(subject), + SourceKind::Subquery(subquery) => { + SourceKindView::Subquery(Box::new(subquery.view(arena))) + } + }, + }) + .collect(), + predicate: self.predicate.map(|e| e.view(arena)), + group_by: self.group_by.map(|g| GroupByView { + expr: g.expr.view(arena), + predicate: g.predicate.map(|e| e.view(arena)), + }), + order_by: self.order_by.map(|o| OrderByView { + expr: o.expr.view(arena), + order: o.order, + }), + limit: self.limit, + projection: self.projection.view(arena), + meta: self.meta, + distinct: self.distinct, + } + } +} diff --git a/src/tests/parser.rs b/src/tests/parser.rs index 7f6afad..b516d4b 100644 --- a/src/tests/parser.rs +++ b/src/tests/parser.rs @@ -1,98 +1,113 @@ +use crate::arena::ExprArena; use crate::lexer::tokenize; use crate::parser::parse; #[test] fn test_parse_from_events_nested_data() { + let mut arena = ExprArena::default(); let tokens = tokenize(include_str!("./resources/from_events_nested_data.eql")).unwrap(); - insta::assert_yaml_snapshot!(parse(tokens.as_slice())); + insta::assert_yaml_snapshot!(parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))); } #[test] fn test_parse_from_events_using_subquery() { + let mut arena = ExprArena::default(); let tokens = tokenize(include_str!("./resources/from_events_using_subquery.eql")).unwrap(); - insta::assert_yaml_snapshot!(parse(tokens.as_slice())); + insta::assert_yaml_snapshot!(parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))); } #[test] fn test_parse_from_events_where_subject_project_record_with_count() { + let mut arena = ExprArena::default(); let tokens = tokenize(include_str!( "./resources/from_events_where_subject_project_record_with_count.eql" )) .unwrap(); - insta::assert_yaml_snapshot!(parse(tokens.as_slice())); + insta::assert_yaml_snapshot!(parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))); } #[test] fn test_parse_from_events_with_top_identity_projection() { + let mut arena = ExprArena::default(); let tokens = tokenize(include_str!( "./resources/from_events_with_top_identity_projection.eql" )) .unwrap(); - insta::assert_yaml_snapshot!(parse(tokens.as_slice())); + insta::assert_yaml_snapshot!(parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))); } #[test] fn test_parse_from_events_with_type_to_project_record() { + let mut arena = ExprArena::default(); let tokens = tokenize(include_str!( "./resources/from_events_with_type_to_project_record.eql" )) .unwrap(); - insta::assert_yaml_snapshot!(parse(tokens.as_slice())); + insta::assert_yaml_snapshot!(parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))); } #[test] fn test_parse_binary_op() { + let mut arena = ExprArena::default(); let tokens = tokenize(include_str!("./resources/parser_binary_op.eql")).unwrap(); - insta::assert_yaml_snapshot!(parse(tokens.as_slice())); + insta::assert_yaml_snapshot!(parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))); } #[test] fn test_parser_unhinged_unary_op() { + let mut arena = ExprArena::default(); let tokens = tokenize(include_str!("./resources/parser_unhinged_unary_op.eql")).unwrap(); - insta::assert_yaml_snapshot!(parse(tokens.as_slice())); + insta::assert_yaml_snapshot!(parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))); } #[test] fn test_parser_from_events_with_group_by_and_having() { + let mut arena = ExprArena::default(); let tokens = tokenize(include_str!( "./resources/from_events_with_group_by_and_having.eql" )) .unwrap(); - insta::assert_yaml_snapshot!(parse(tokens.as_slice())); + insta::assert_yaml_snapshot!(parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))); } #[test] fn test_parser_from_events_with_distinct() { + let mut arena = ExprArena::default(); let tokens = tokenize(include_str!("./resources/from_events_with_distinct.eql")).unwrap(); - insta::assert_yaml_snapshot!(parse(tokens.as_slice())); + insta::assert_yaml_snapshot!(parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))); } #[test] fn test_parser_valid_contains() { + let mut arena = ExprArena::default(); let tokens = tokenize(include_str!("./resources/valid_contains.eql")).unwrap(); - insta::assert_yaml_snapshot!(parse(tokens.as_slice())); + insta::assert_yaml_snapshot!(parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))); } #[test] fn test_parser_valid_type_conversion() { + let mut arena = ExprArena::default(); let tokens = tokenize(include_str!("./resources/valid_type_conversion.eql")).unwrap(); - insta::assert_yaml_snapshot!(parse(tokens.as_slice())); + insta::assert_yaml_snapshot!(parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))); } #[test] fn test_parser_invalid_type_conversion_expr() { + let mut arena = ExprArena::default(); let tokens = tokenize(include_str!("./resources/invalid_type_conversion_expr.eql")).unwrap(); - insta::assert_yaml_snapshot!(parse(tokens.as_slice())); + insta::assert_yaml_snapshot!(parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))); } #[test] fn test_parser_with_comment() { + let mut arena = ExprArena::default(); let tokens = tokenize(include_str!("./resources/with_comment.eql")).unwrap(); - insta::assert_yaml_snapshot!(parse(tokens.as_slice())); + insta::assert_yaml_snapshot!(parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))); } #[test] fn test_parser_order_by_no_ordering() { + let mut arena = ExprArena::default(); let tokens = tokenize(include_str!("./resources/query_order_by_no_ordering.eql")).unwrap(); - insta::assert_yaml_snapshot!(parse(tokens.as_slice())); + insta::assert_yaml_snapshot!(parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))); } diff --git a/src/tests/snapshots/eventql_parser__tests__analysis__accept_valid_having_clause.snap b/src/tests/snapshots/eventql_parser__tests__analysis__accept_valid_having_clause.snap index 1ae8186..05e257b 100644 --- a/src/tests/snapshots/eventql_parser__tests__analysis__accept_valid_having_clause.snap +++ b/src/tests/snapshots/eventql_parser__tests__analysis__accept_valid_having_clause.snap @@ -1,6 +1,6 @@ --- source: src/tests/analysis.rs -expression: "query.run_static_analysis(&Default::default())" +expression: "query.and_then(|q|\n{\n q.run_static_analysis(&arena, &Default::default()).map(|q| q.view(&arena))\n})" --- Ok: attrs: @@ -95,7 +95,11 @@ Ok: col: 14 value: Record: - - name: department + - attrs: + pos: + line: 4 + col: 5 + name: department value: attrs: pos: @@ -127,7 +131,11 @@ Ok: Id: e field: data field: department - - name: average + - attrs: + pos: + line: 5 + col: 5 + name: average value: attrs: pos: diff --git a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_accept_group_by_with_agg_rec.snap b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_accept_group_by_with_agg_rec.snap index 051e21c..6dece40 100644 --- a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_accept_group_by_with_agg_rec.snap +++ b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_accept_group_by_with_agg_rec.snap @@ -1,6 +1,6 @@ --- source: src/tests/analysis.rs -expression: "query.run_static_analysis(&Default::default())" +expression: "query.and_then(|q|\n{\n q.run_static_analysis(&arena, &Default::default()).map(|q| q.view(&arena))\n})" --- Ok: attrs: @@ -50,7 +50,11 @@ Ok: col: 14 value: Record: - - name: department + - attrs: + pos: + line: 5 + col: 2 + name: department value: attrs: pos: @@ -82,7 +86,11 @@ Ok: Id: e field: data field: department - - name: cost + - attrs: + pos: + line: 6 + col: 2 + name: cost value: attrs: pos: diff --git a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_accept_group_by_with_order_by_with_agg.snap b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_accept_group_by_with_order_by_with_agg.snap index 03356f1..da9235b 100644 --- a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_accept_group_by_with_order_by_with_agg.snap +++ b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_accept_group_by_with_order_by_with_agg.snap @@ -1,6 +1,6 @@ --- source: src/tests/analysis.rs -expression: "query.run_static_analysis(&Default::default())" +expression: "query.and_then(|q|\n{\n q.run_static_analysis(&arena, &Default::default()).map(|q| q.view(&arena))\n})" --- Ok: attrs: diff --git a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_allow_constant_agg_func.snap b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_allow_constant_agg_func.snap index 9f67853..8989905 100644 --- a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_allow_constant_agg_func.snap +++ b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_allow_constant_agg_func.snap @@ -1,6 +1,6 @@ --- source: src/tests/analysis.rs -expression: "query.run_static_analysis(&Default::default())" +expression: "query.and_then(|q|\n{\n q.run_static_analysis(&arena, &Default::default()).map(|q| q.view(&arena))\n})" --- Ok: attrs: diff --git a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_allow_regular_property_project_into.snap b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_allow_regular_property_project_into.snap index 83bf03d..50456f4 100644 --- a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_allow_regular_property_project_into.snap +++ b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_allow_regular_property_project_into.snap @@ -1,6 +1,6 @@ --- source: src/tests/analysis.rs -expression: "query.run_static_analysis(&Default::default())" +expression: "query.and_then(|q|\n{\n q.run_static_analysis(&arena, &Default::default()).map(|q| q.view(&arena))\n})" --- Ok: attrs: diff --git a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_invalid_type_conversion_custom_type.snap b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_invalid_type_conversion_custom_type.snap index 3da6eae..d939599 100644 --- a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_invalid_type_conversion_custom_type.snap +++ b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_invalid_type_conversion_custom_type.snap @@ -1,10 +1,10 @@ --- source: src/tests/analysis.rs -expression: "query.run_static_analysis(&Default::default())" +expression: "query.and_then(|q|\n{\n q.run_static_analysis(&arena, &Default::default()).map(|q| q.view(&arena))\n})" --- Err: Analysis: UnsupportedCustomType: - 2 - - 22 + - 37 - Foobar diff --git a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_lowercase_function.snap b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_lowercase_function.snap index 8749db6..0731f83 100644 --- a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_lowercase_function.snap +++ b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_lowercase_function.snap @@ -1,6 +1,6 @@ --- source: src/tests/analysis.rs -expression: "query.run_static_analysis(&Default::default())" +expression: "query.and_then(|q|\n{\n q.run_static_analysis(&arena, &Default::default()).map(|q| q.view(&arena))\n})" --- Ok: attrs: diff --git a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_optional_param_func.snap b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_optional_param_func.snap index 757748e..542938c 100644 --- a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_optional_param_func.snap +++ b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_optional_param_func.snap @@ -1,6 +1,6 @@ --- source: src/tests/analysis.rs -expression: "query.run_static_analysis(&Default::default())" +expression: "query.and_then(|q|\n{\n q.run_static_analysis(&arena, &Default::default()).map(|q| q.view(&arena))\n})" --- Ok: attrs: @@ -26,7 +26,11 @@ Ok: col: 14 value: Record: - - name: voters + - attrs: + pos: + line: 2 + col: 16 + name: voters value: attrs: pos: diff --git a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_project_agg_value.snap b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_project_agg_value.snap index 5cd0aa9..094187d 100644 --- a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_project_agg_value.snap +++ b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_project_agg_value.snap @@ -1,6 +1,6 @@ --- source: src/tests/analysis.rs -expression: "query.run_static_analysis(&Default::default())" +expression: "query.and_then(|q|\n{\n q.run_static_analysis(&arena, &Default::default()).map(|q| q.view(&arena))\n})" --- Ok: attrs: diff --git a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_valid_agg_usage.snap b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_valid_agg_usage.snap index 1d630ad..a93ecbe 100644 --- a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_valid_agg_usage.snap +++ b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_valid_agg_usage.snap @@ -1,6 +1,6 @@ --- source: src/tests/analysis.rs -expression: "query.run_static_analysis(&Default::default())" +expression: "query.and_then(|q|\n{\n q.run_static_analysis(&arena, &Default::default()).map(|q| q.view(&arena))\n})" --- Ok: attrs: @@ -26,7 +26,11 @@ Ok: col: 14 value: Record: - - name: sum + - attrs: + pos: + line: 3 + col: 2 + name: sum value: attrs: pos: @@ -58,7 +62,11 @@ Ok: Id: e field: data field: count - - name: label + - attrs: + pos: + line: 4 + col: 2 + name: label value: attrs: pos: @@ -66,7 +74,11 @@ Ok: col: 9 value: String: everything summed - - name: randomvalue + - attrs: + pos: + line: 5 + col: 2 + name: randomvalue value: attrs: pos: diff --git a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_valid_type_conversion.snap b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_valid_type_conversion.snap index fbb53bc..4ba36b0 100644 --- a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_valid_type_conversion.snap +++ b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_valid_type_conversion.snap @@ -1,73 +1,78 @@ --- source: src/tests/analysis.rs -expression: "query.run_static_analysis(&Default::default())" +expression: "query.map(|q|\n{\n q.run_static_analysis(&arena, &Default::default()).map(|q| q.view(&arena))\n})" --- Ok: - attrs: - pos: - line: 1 - col: 1 - sources: - - binding: - name: e - pos: - line: 1 - col: 6 - kind: - Name: events - predicate: ~ - group_by: ~ - order_by: ~ - limit: ~ - projection: + Ok: attrs: pos: - line: 2 - col: 14 - value: - Record: - - name: date - value: - attrs: + line: 1 + col: 1 + sources: + - binding: + name: e + pos: + line: 1 + col: 6 + kind: + Name: events + predicate: ~ + group_by: ~ + order_by: ~ + limit: ~ + projection: + attrs: + pos: + line: 2 + col: 14 + value: + Record: + - attrs: pos: line: 2 - col: 22 + col: 16 + name: date value: - Binary: - lhs: - attrs: - pos: - line: 2 - col: 22 - value: - Access: - target: - attrs: - pos: - line: 2 - col: 22 - value: - Access: - target: - attrs: - pos: - line: 2 - col: 22 - value: - Id: e - field: data - field: date - operator: As - rhs: - attrs: - pos: - line: 2 - col: 37 - value: - Id: DATETIME - distinct: false - meta: - project: - Record: - date: DateTime - aggregate: false + attrs: + pos: + line: 2 + col: 22 + value: + Binary: + lhs: + attrs: + pos: + line: 2 + col: 22 + value: + Access: + target: + attrs: + pos: + line: 2 + col: 22 + value: + Access: + target: + attrs: + pos: + line: 2 + col: 22 + value: + Id: e + field: data + field: date + operator: As + rhs: + attrs: + pos: + line: 2 + col: 37 + value: + Id: DATETIME + distinct: false + meta: + project: + Record: + date: DateTime + aggregate: false diff --git a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_valid_type_conversion_custom_type.snap b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_valid_type_conversion_custom_type.snap index 1f1cafd..4452e14 100644 --- a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_valid_type_conversion_custom_type.snap +++ b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_valid_type_conversion_custom_type.snap @@ -1,6 +1,6 @@ --- source: src/tests/analysis.rs -expression: "query.run_static_analysis(&AnalysisOptions::default().add_custom_type(\"Foobar\"))" +expression: "query.and_then(|q|\n{\n q.run_static_analysis(&arena,\n &AnalysisOptions::default().add_custom_type(\"Foobar\"),).map(|q|\n q.view(&arena))\n})" --- Ok: attrs: @@ -26,7 +26,11 @@ Ok: col: 14 value: Record: - - name: date + - attrs: + pos: + line: 2 + col: 16 + name: date value: attrs: pos: diff --git a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_valid_type_conversion_weird_case.snap b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_valid_type_conversion_weird_case.snap index 4532e98..5e90aa6 100644 --- a/src/tests/snapshots/eventql_parser__tests__analysis__analyze_valid_type_conversion_weird_case.snap +++ b/src/tests/snapshots/eventql_parser__tests__analysis__analyze_valid_type_conversion_weird_case.snap @@ -1,6 +1,6 @@ --- source: src/tests/analysis.rs -expression: "query.run_static_analysis(&Default::default())" +expression: "query.and_then(|q|\n{\n q.run_static_analysis(&arena, &Default::default()).map(|q| q.view(&arena))\n})" --- Ok: attrs: @@ -26,7 +26,11 @@ Ok: col: 14 value: Record: - - name: date + - attrs: + pos: + line: 2 + col: 16 + name: date value: attrs: pos: diff --git a/src/tests/snapshots/eventql_parser__tests__analysis__rename_subquery.snap b/src/tests/snapshots/eventql_parser__tests__analysis__rename_subquery.snap index 017220e..4927324 100644 --- a/src/tests/snapshots/eventql_parser__tests__analysis__rename_subquery.snap +++ b/src/tests/snapshots/eventql_parser__tests__analysis__rename_subquery.snap @@ -1,6 +1,6 @@ --- source: src/tests/analysis.rs -expression: "query.run_static_analysis(&Default::default())" +expression: "query.and_then(|q|\n{\n q.run_static_analysis(&arena, &Default::default()).map(|q| q.view(&arena))\n})" --- Ok: attrs: @@ -67,7 +67,11 @@ Ok: col: 16 value: Record: - - name: orderId + - attrs: + pos: + line: 4 + col: 18 + name: orderId value: attrs: pos: @@ -91,7 +95,11 @@ Ok: Id: e field: data field: foobar - - name: value + - attrs: + pos: + line: 4 + col: 42 + name: value value: attrs: pos: diff --git a/src/tests/snapshots/eventql_parser__tests__parser__parse_from_events_nested_data.snap b/src/tests/snapshots/eventql_parser__tests__parser__parse_from_events_nested_data.snap index cf96ca3..d75ed1e 100644 --- a/src/tests/snapshots/eventql_parser__tests__parser__parse_from_events_nested_data.snap +++ b/src/tests/snapshots/eventql_parser__tests__parser__parse_from_events_nested_data.snap @@ -1,6 +1,6 @@ --- source: src/tests/parser.rs -expression: parse(tokens.as_slice()) +expression: "parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))" --- Ok: attrs: @@ -63,7 +63,11 @@ Ok: col: 14 value: Record: - - name: id + - attrs: + pos: + line: 3 + col: 16 + name: id value: attrs: pos: @@ -79,7 +83,11 @@ Ok: value: Id: e field: id - - name: price + - attrs: + pos: + line: 3 + col: 26 + name: price value: attrs: pos: diff --git a/src/tests/snapshots/eventql_parser__tests__parser__parse_from_events_using_subquery.snap b/src/tests/snapshots/eventql_parser__tests__parser__parse_from_events_using_subquery.snap index f9c817c..70f3770 100644 --- a/src/tests/snapshots/eventql_parser__tests__parser__parse_from_events_using_subquery.snap +++ b/src/tests/snapshots/eventql_parser__tests__parser__parse_from_events_using_subquery.snap @@ -1,6 +1,6 @@ --- source: src/tests/parser.rs -expression: parse(tokens.as_slice()) +expression: "parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))" --- Ok: attrs: @@ -67,7 +67,11 @@ Ok: col: 16 value: Record: - - name: orderId + - attrs: + pos: + line: 4 + col: 18 + name: orderId value: attrs: pos: @@ -83,7 +87,11 @@ Ok: value: Id: e field: id - - name: value + - attrs: + pos: + line: 4 + col: 33 + name: value value: attrs: pos: diff --git a/src/tests/snapshots/eventql_parser__tests__parser__parse_from_events_where_subject_project_record_with_count.snap b/src/tests/snapshots/eventql_parser__tests__parser__parse_from_events_where_subject_project_record_with_count.snap index 4d13a52..b9ffaae 100644 --- a/src/tests/snapshots/eventql_parser__tests__parser__parse_from_events_where_subject_project_record_with_count.snap +++ b/src/tests/snapshots/eventql_parser__tests__parser__parse_from_events_where_subject_project_record_with_count.snap @@ -1,6 +1,6 @@ --- source: src/tests/parser.rs -expression: parse(tokens.as_slice()) +expression: "parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))" --- Ok: attrs: @@ -55,7 +55,11 @@ Ok: col: 14 value: Record: - - name: total + - attrs: + pos: + line: 3 + col: 16 + name: total value: attrs: pos: diff --git a/src/tests/snapshots/eventql_parser__tests__parser__parse_from_events_with_type_to_project_record.snap b/src/tests/snapshots/eventql_parser__tests__parser__parse_from_events_with_type_to_project_record.snap index f0de686..5cf4357 100644 --- a/src/tests/snapshots/eventql_parser__tests__parser__parse_from_events_with_type_to_project_record.snap +++ b/src/tests/snapshots/eventql_parser__tests__parser__parse_from_events_with_type_to_project_record.snap @@ -1,6 +1,6 @@ --- source: src/tests/parser.rs -expression: parse(tokens.as_slice()) +expression: "parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))" --- Ok: attrs: @@ -55,7 +55,11 @@ Ok: col: 14 value: Record: - - name: id + - attrs: + pos: + line: 3 + col: 16 + name: id value: attrs: pos: @@ -71,7 +75,11 @@ Ok: value: Id: e field: id - - name: book + - attrs: + pos: + line: 3 + col: 26 + name: book value: attrs: pos: diff --git a/src/tests/snapshots/eventql_parser__tests__parser__parser_from_events_with_distinct.snap b/src/tests/snapshots/eventql_parser__tests__parser__parser_from_events_with_distinct.snap index d7e2678..a8a8421 100644 --- a/src/tests/snapshots/eventql_parser__tests__parser__parser_from_events_with_distinct.snap +++ b/src/tests/snapshots/eventql_parser__tests__parser__parser_from_events_with_distinct.snap @@ -1,6 +1,6 @@ --- source: src/tests/parser.rs -expression: parse(tokens.as_slice()) +expression: "parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))" --- Ok: attrs: @@ -63,7 +63,11 @@ Ok: col: 23 value: Record: - - name: id + - attrs: + pos: + line: 3 + col: 25 + name: id value: attrs: pos: @@ -79,7 +83,11 @@ Ok: value: Id: e field: id - - name: price + - attrs: + pos: + line: 3 + col: 35 + name: price value: attrs: pos: diff --git a/src/tests/snapshots/eventql_parser__tests__parser__parser_valid_type_conversion.snap b/src/tests/snapshots/eventql_parser__tests__parser__parser_valid_type_conversion.snap index 4c0b089..61676db 100644 --- a/src/tests/snapshots/eventql_parser__tests__parser__parser_valid_type_conversion.snap +++ b/src/tests/snapshots/eventql_parser__tests__parser__parser_valid_type_conversion.snap @@ -1,6 +1,6 @@ --- source: src/tests/parser.rs -expression: parse(tokens.as_slice()) +expression: "parse(&mut arena, tokens.as_slice()).map(|q| q.view(&arena))" --- Ok: attrs: @@ -26,7 +26,11 @@ Ok: col: 14 value: Record: - - name: date + - attrs: + pos: + line: 2 + col: 16 + name: date value: attrs: pos: diff --git a/src/token.rs b/src/token.rs index 8d4ab60..5334049 100644 --- a/src/token.rs +++ b/src/token.rs @@ -64,7 +64,7 @@ impl Display for Sym<'_> { /// 3. Additive: `+`, `-` /// 4. Comparison: `<`, `<=`, `>`, `>=`, `==`, `!=` /// 5. Logical: `AND`, `OR`, `XOR` -#[derive(Clone, Debug, Copy, Serialize)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, Serialize)] pub enum Operator { /// Addition operator `+` Add,