diff --git a/Cargo.lock b/Cargo.lock index e62c3238..39b91a05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6010,6 +6010,7 @@ dependencies = [ "arc-swap", "async-trait", "base64", + "bumpalo", "criterion", "downcast-rs", "fixedbitset", diff --git a/core/engine/Cargo.toml b/core/engine/Cargo.toml index ac6d00b5..932cf1a5 100644 --- a/core/engine/Cargo.toml +++ b/core/engine/Cargo.toml @@ -15,6 +15,7 @@ doctest = false ahash = { workspace = true } anyhow = { workspace = true } base64 = "0.22" +bumpalo = { workspace = true } thiserror = { workspace = true } petgraph = { workspace = true } serde_json = { workspace = true, features = [] } diff --git a/core/engine/src/nodes/expression/mod.rs b/core/engine/src/nodes/expression/mod.rs index 40fd46ed..e1ca14ac 100644 --- a/core/engine/src/nodes/expression/mod.rs +++ b/core/engine/src/nodes/expression/mod.rs @@ -1,12 +1,15 @@ use crate::model::ExpressionNodeContent; use crate::nodes::result::NodeResult; use ahash::HashMap; +use bumpalo::Bump; use std::rc::Rc; use crate::nodes::context::{NodeContext, NodeContextExt}; use crate::nodes::definition::NodeHandler; +use zen_expression::lexer::{Identifier, Lexer, TokenKind}; use zen_expression::variable::{ToVariable, Variable}; use zen_types::decision::TransformAttributes; +use zen_types::symbol::Symbol; #[derive(Debug, Clone)] pub struct ExpressionNodeHandler; @@ -28,6 +31,7 @@ impl NodeHandler for ExpressionNodeHandler { async fn handle(&self, ctx: NodeContext) -> NodeResult { let result = Variable::empty_object(); let mut isolate = ctx.isolate(); + let mut dollar_bound = false; for expression in ctx.node.expressions.iter() { if expression.key.is_empty() || expression.value.is_empty() { @@ -39,9 +43,11 @@ impl NodeHandler for ExpressionNodeHandler { .with_node_context(&ctx, |_| { format!(r#"Failed to evaluate expression: "{}""#, &expression.value) })?; - let value = match expression.value.contains('$') { - true => value.deep_clone(), - false => value, + let value = match &value { + Variable::Object(_) | Variable::Array(_) if reads_context(&expression.value) => { + value.deep_clone() + } + _ => value, }; ctx.trace(|trace| { trace.insert( @@ -52,15 +58,69 @@ impl NodeHandler for ExpressionNodeHandler { ); }); - isolate.insert_dollar(&expression.key, value.clone()); - - result.dot_insert(&expression.key, value); + insert_at_path(&result, &expression.key, value); + if !dollar_bound { + isolate.set_local(Variable::dollar_key(), result.shallow_clone()); + dollar_bound = true; + } } ctx.success(result) } } +fn reads_context(source: &str) -> bool { + let bump = Bump::new(); + Lexer::new() + .tokenize(&bump, source) + .map(|tokens| { + tokens.iter().any(|token| { + matches!( + token.kind, + TokenKind::Identifier(Identifier::ContextReference | Identifier::RootReference) + ) + }) + }) + .unwrap_or(true) +} + +fn insert_at_path(root: &Variable, key: &str, value: Variable) { + let mut parts = key.split('.'); + let Some(last) = parts.next_back() else { + return; + }; + + let mut current = root.shallow_clone(); + for part in parts { + let Variable::Object(object) = ¤t else { + return; + }; + + let next = { + let mut map = object.borrow_mut(); + match map.get_str(part).map(Variable::shallow_clone) { + Some(Variable::Object(child)) if Rc::strong_count(&child) > 2 => { + let copy = Variable::Object(child).depth_clone(1); + map.insert(Symbol::from(part), copy.shallow_clone()); + copy + } + Some(child @ Variable::Object(_)) => child, + Some(_) => return, + None => { + let created = Variable::empty_object(); + map.insert(Symbol::from(part), created.shallow_clone()); + created + } + } + }; + current = next; + } + + if let Variable::Object(object) = ¤t { + object.borrow_mut().insert(Symbol::from(last), value); + } +} + #[derive(Debug, Clone, ToVariable)] pub struct ExpressionNodeTraceItem { result: Variable, diff --git a/core/engine/src/workspace/graph/analysis.rs b/core/engine/src/workspace/graph/analysis.rs index eb5c7ef8..969a92d8 100644 --- a/core/engine/src/workspace/graph/analysis.rs +++ b/core/engine/src/workspace/graph/analysis.rs @@ -795,7 +795,7 @@ impl<'a> GraphAnalyzer<'a> { let row_scope = Self::scope_with( scope, &[ - ("$", dollar.depth_clone(usize::MAX)), + ("$", dollar.shallow_clone()), (NODES_KEY, self.nodes_scope.shallow_clone()), ], );