From cec53f4fb93766c4573081685b0add809c1abc92 Mon Sep 17 00:00:00 2001 From: MOZGIII Date: Thu, 5 Mar 2026 03:43:10 +0400 Subject: [PATCH 1/5] Split the tests out of the way --- crates/lib/runner/src/expression_evaluator.rs | 390 +----------------- .../runner/src/expression_evaluator/tests.rs | 0 crates/waymark/src/waymark_core/runloop.rs | 1 + 3 files changed, 2 insertions(+), 389 deletions(-) create mode 100644 crates/lib/runner/src/expression_evaluator/tests.rs diff --git a/crates/lib/runner/src/expression_evaluator.rs b/crates/lib/runner/src/expression_evaluator.rs index dac989a9..d8dc8e0d 100644 --- a/crates/lib/runner/src/expression_evaluator.rs +++ b/crates/lib/runner/src/expression_evaluator.rs @@ -665,392 +665,4 @@ pub(crate) fn range_from_args(args: &[Value]) -> Vec { } #[cfg(test)] -mod tests { - use std::cell::RefCell; - use std::collections::{HashMap, HashSet}; - use std::rc::Rc; - use std::sync::Arc; - - use uuid::Uuid; - - use super::*; - use waymark_dag::{DAG, DAGEdge}; - use waymark_ir_parser::IRParser; - use waymark_proto::ast as ir; - use waymark_runner_state::{ - ActionCallSpec, ActionResultValue, BinaryOpValue, FunctionCallValue, LiteralValue, - RunnerState, VariableValue, value_visitor::ValueExpr, - }; - - fn parse_expr(source: &str) -> ir::Expr { - IRParser::new(" ") - .parse_expr(source) - .expect("parse expression") - } - - fn literal_int(value: i64) -> ValueExpr { - ValueExpr::Literal(LiteralValue { - value: Value::Number(value.into()), - }) - } - - fn empty_executor() -> RunnerExecutor { - let dag = Arc::new(DAG::default()); - let state = RunnerState::new(Some(Arc::clone(&dag)), None, None, false); - RunnerExecutor::new(dag, state, HashMap::new(), None) - } - - fn executor_with_assignment(name: &str, value: ValueExpr) -> RunnerExecutor { - let dag = Arc::new(DAG::default()); - let mut state = RunnerState::new(Some(Arc::clone(&dag)), None, None, false); - state - .record_assignment_value( - vec![name.to_string()], - value, - None, - Some("test assignment".to_string()), - ) - .expect("record assignment"); - RunnerExecutor::new(dag, state, HashMap::new(), None) - } - - #[test] - fn test_expr_to_value_happy_path() { - let expr = parse_expr("x + 2"); - let value = RunnerExecutor::expr_to_value(&expr).expect("convert expression"); - match value { - ValueExpr::BinaryOp(binary) => { - assert!(matches!(*binary.left, ValueExpr::Variable(_))); - assert!(matches!(*binary.right, ValueExpr::Literal(_))); - } - other => panic!("expected binary op, got {other:?}"), - } - } - - #[test] - fn test_evaluate_guard_happy_path() { - let executor = executor_with_assignment("x", literal_int(2)); - let guard = parse_expr("x > 1"); - let result = executor - .evaluate_guard(Some(&guard)) - .expect("evaluate guard"); - assert!(result); - } - - #[test] - fn test_resolve_action_kwargs_happy_path() { - let executor = executor_with_assignment("x", literal_int(10)); - let action = ActionCallSpec { - action_name: "double".to_string(), - module_name: Some("tests".to_string()), - kwargs: HashMap::from([( - "value".to_string(), - ValueExpr::Variable(VariableValue { - name: "x".to_string(), - }), - )]), - }; - let resolved = executor - .resolve_action_kwargs(Uuid::new_v4(), &action) - .expect("resolve kwargs"); - assert_eq!(resolved.get("value"), Some(&Value::Number(10.into()))); - } - - #[test] - fn test_resolve_action_kwargs_uses_data_flow_for_self_referential_targets() { - let dag = Arc::new(DAG::default()); - let mut state = RunnerState::new(Some(Arc::clone(&dag)), None, None, false); - state - .record_assignment_value( - vec!["current".to_string()], - literal_int(0), - None, - Some("current = 0".to_string()), - ) - .expect("record current"); - let action_result = state - .queue_action( - "increment", - Some(vec!["current".to_string()]), - Some(HashMap::from([( - "value".to_string(), - ValueExpr::Variable(VariableValue { - name: "current".to_string(), - }), - )])), - None, - None, - ) - .expect("queue increment"); - let action_node = state - .nodes - .get(&action_result.node_id) - .expect("action node") - .clone(); - let action_spec = action_node.action.expect("action spec"); - - let executor = RunnerExecutor::new(dag, state, HashMap::new(), None); - let resolved = executor - .resolve_action_kwargs(action_result.node_id, &action_spec) - .expect("resolve kwargs"); - assert_eq!(resolved.get("value"), Some(&Value::Number(0.into()))); - } - - #[test] - fn test_evaluate_value_expr_happy_path() { - let executor = executor_with_assignment("x", literal_int(3)); - let expr = ValueExpr::BinaryOp(waymark_runner_state::BinaryOpValue { - left: Box::new(ValueExpr::Variable(VariableValue { - name: "x".to_string(), - })), - op: ir::BinaryOperator::BinaryOpAdd as i32, - right: Box::new(literal_int(1)), - }); - let value = executor - .evaluate_value_expr(&expr) - .expect("evaluate value expression"); - assert_eq!(value, Value::Number(4.into())); - } - - #[test] - fn test_evaluate_variable_happy_path() { - let executor = executor_with_assignment("value", literal_int(5)); - let stack = Rc::new(RefCell::new(HashSet::new())); - let value = executor - .evaluate_variable_with_context(None, "value", stack) - .expect("evaluate variable"); - assert_eq!(value, Value::Number(5.into())); - } - - #[test] - fn test_evaluate_assignment_happy_path() { - let executor = executor_with_assignment("value", literal_int(9)); - let node_id = executor - .state() - .latest_assignment("value") - .expect("latest assignment"); - let stack = Rc::new(RefCell::new(HashSet::new())); - let value = executor - .evaluate_assignment(node_id, "value", stack) - .expect("evaluate assignment"); - assert_eq!(value, Value::Number(9.into())); - } - - #[test] - fn test_evaluate_assignment_uses_data_flow_for_self_referential_updates() { - let dag = Arc::new(DAG::default()); - let mut state = RunnerState::new(Some(Arc::clone(&dag)), None, None, false); - state - .record_assignment_value( - vec!["count".to_string()], - literal_int(0), - None, - Some("count = 0".to_string()), - ) - .expect("record initial count"); - state - .record_assignment_value( - vec!["count".to_string()], - ValueExpr::BinaryOp(BinaryOpValue { - left: Box::new(ValueExpr::Variable(VariableValue { - name: "count".to_string(), - })), - op: ir::BinaryOperator::BinaryOpAdd as i32, - right: Box::new(literal_int(1)), - }), - None, - Some("count = count + 1".to_string()), - ) - .expect("record updated count"); - - let executor = RunnerExecutor::new(dag, state, HashMap::new(), None); - let node_id = executor - .state() - .latest_assignment("count") - .expect("latest assignment"); - let stack = Rc::new(RefCell::new(HashSet::new())); - let value = executor - .evaluate_assignment(node_id, "count", stack) - .expect("evaluate self-referential assignment"); - assert_eq!(value, Value::Number(1.into())); - } - - #[test] - fn test_resolve_action_result_happy_path() { - let mut executor = empty_executor(); - let action_id = Uuid::new_v4(); - executor.set_action_result( - action_id, - Value::Array(vec![Value::Number(7.into()), Value::Number(8.into())]), - ); - let result = executor - .resolve_action_result(&ActionResultValue { - node_id: action_id, - action_name: "fetch".to_string(), - iteration_index: None, - result_index: Some(1), - }) - .expect("resolve action result"); - assert_eq!(result, Value::Number(8.into())); - } - - #[test] - fn test_evaluate_function_call_happy_path() { - let executor = empty_executor(); - let value = executor - .evaluate_function_call( - &FunctionCallValue { - name: "len".to_string(), - args: Vec::new(), - kwargs: HashMap::new(), - global_function: Some(ir::GlobalFunction::Len as i32), - }, - vec![Value::Array(vec![Value::Null, Value::Null])], - HashMap::new(), - ) - .expect("evaluate function call"); - assert_eq!(value, Value::Number(2.into())); - } - - #[test] - fn test_evaluate_global_function_happy_path() { - let executor = empty_executor(); - let value = executor - .evaluate_global_function( - ir::GlobalFunction::Range as i32, - vec![Value::Number(1.into()), Value::Number(4.into())], - HashMap::new(), - ) - .expect("evaluate global function"); - assert_eq!( - value, - Value::Array(vec![ - Value::Number(1.into()), - Value::Number(2.into()), - Value::Number(3.into()) - ]) - ); - } - - #[test] - fn test_apply_binary_happy_path() { - let value = RunnerExecutor::apply_binary( - ir::BinaryOperator::BinaryOpAdd as i32, - Value::Number(2.into()), - Value::Number(3.into()), - ) - .expect("apply binary"); - assert_eq!(value, Value::Number(5.into())); - } - - #[test] - fn test_apply_unary_happy_path() { - let value = - RunnerExecutor::apply_unary(ir::UnaryOperator::UnaryOpNot as i32, Value::Bool(true)) - .expect("apply unary"); - assert_eq!(value, Value::Bool(false)); - } - - #[test] - fn test_exception_matches_happy_path() { - let executor = empty_executor(); - let edge = DAGEdge::state_machine_with_exception("a", "b", vec!["ValueError".to_string()]); - let exception = serde_json::json!({ - "type": "ValueError", - "message": "boom", - }); - assert!(executor.exception_matches(&edge, &exception)); - } - - #[test] - fn test_executor_error_happy_path() { - let error = executor_error("hello"); - assert_eq!(error.0, "hello"); - } - - #[test] - fn test_int_value_happy_path() { - let value = Value::Number(7_u64.into()); - assert_eq!(int_value(&value), Some(7)); - } - - #[test] - fn test_numeric_op_happy_path() { - let value = numeric_op( - Value::Number(10.into()), - Value::Number(3.into()), - |a, b| a + b, - true, - executor_error, - ) - .expect("numeric op"); - assert_eq!(value, Value::Number(13.into())); - } - - #[test] - fn test_add_values_happy_path() { - let value = add_values( - Value::String("hello ".to_string()), - Value::String("world".to_string()), - executor_error, - ) - .expect("add values"); - assert_eq!(value, Value::String("hello world".to_string())); - } - - #[test] - fn test_compare_values_happy_path() { - let value = compare_values( - Value::Number(3.into()), - Value::Number(5.into()), - |a, b| a < b, - executor_error, - ) - .expect("compare values"); - assert_eq!(value, Value::Bool(true)); - } - - #[test] - fn test_value_in_happy_path() { - let container = Value::Array(vec![Value::Number(1.into()), Value::Number(2.into())]); - assert!(value_in(&Value::Number(2.into()), &container)); - } - - #[test] - fn test_is_truthy_happy_path() { - assert!(is_truthy(&Value::String("non-empty".to_string()))); - } - - #[test] - fn test_is_exception_value_happy_path() { - let value = serde_json::json!({ - "type": "RuntimeError", - "message": "bad", - }); - assert!(is_exception_value(&value)); - } - - #[test] - fn test_len_of_value_happy_path() { - let value = Value::Array(vec![Value::Null, Value::Null, Value::Null]); - let len = len_of_value(&value, executor_error).expect("length"); - assert_eq!(len.as_i64(), Some(3)); - } - - #[test] - fn test_range_from_args_happy_path() { - let values = range_from_args(&[ - Value::Number(0.into()), - Value::Number(5.into()), - Value::Number(2.into()), - ]); - assert_eq!( - values, - vec![ - Value::Number(0.into()), - Value::Number(2.into()), - Value::Number(4.into()) - ] - ); - } -} +mod tests; diff --git a/crates/lib/runner/src/expression_evaluator/tests.rs b/crates/lib/runner/src/expression_evaluator/tests.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/waymark/src/waymark_core/runloop.rs b/crates/waymark/src/waymark_core/runloop.rs index dc05762d..c48e1bcb 100644 --- a/crates/waymark/src/waymark_core/runloop.rs +++ b/crates/waymark/src/waymark_core/runloop.rs @@ -870,6 +870,7 @@ impl RunLoop { let event_tx = event_tx.clone(); let handle = thread::Builder::new() .name(format!("waymark-executor-{shard_id}")) + .stack_size(128 * 1024 * 1024 /* 128 MB */) .spawn(move || run_executor_shard(shard_id, backend, cmd_rx, event_tx)) .map_err(|err| { RunLoopError::Message(format!( From 5485b6e008716c988bb9e0065c6582b4002461f4 Mon Sep 17 00:00:00 2001 From: MOZGIII Date: Thu, 5 Mar 2026 04:02:03 +0400 Subject: [PATCH 2/5] Split the tests out of the way (2) --- crates/lib/runner-state/src/value_visitor.rs | 177 +----------------- .../runner-state/src/value_visitor/tests.rs | 174 +++++++++++++++++ 2 files changed, 175 insertions(+), 176 deletions(-) create mode 100644 crates/lib/runner-state/src/value_visitor/tests.rs diff --git a/crates/lib/runner-state/src/value_visitor.rs b/crates/lib/runner-state/src/value_visitor.rs index fbc7736a..8a2ba175 100644 --- a/crates/lib/runner-state/src/value_visitor.rs +++ b/crates/lib/runner-state/src/value_visitor.rs @@ -355,179 +355,4 @@ pub fn collect_value_sources( } #[cfg(test)] -mod tests { - use std::collections::{HashMap, HashSet}; - - use serde_json::Value; - use uuid::Uuid; - - use super::*; - use waymark_proto::ast as ir; - - fn literal_int(value: i64) -> ValueExpr { - ValueExpr::Literal(LiteralValue { - value: Value::Number(value.into()), - }) - } - - #[test] - fn test_value_expr_resolver_visit_happy_path() { - let mut seen = HashSet::new(); - let resolve = |name: &str, _: &mut HashSet| { - if name == "x" { - literal_int(3) - } else { - literal_int(0) - } - }; - let mut resolver = ValueExprResolver::new(&resolve, &mut seen); - let expr = ValueExpr::BinaryOp(BinaryOpValue { - left: Box::new(ValueExpr::Variable(VariableValue { - name: "x".to_string(), - })), - op: ir::BinaryOperator::BinaryOpAdd as i32, - right: Box::new(literal_int(1)), - }); - - let resolved = resolver.visit(&expr); - match resolved { - ValueExpr::BinaryOp(value) => { - assert!(matches!(*value.left, ValueExpr::Literal(_))); - assert!(matches!(*value.right, ValueExpr::Literal(_))); - } - other => panic!("expected binary value, got {other:?}"), - } - } - - #[test] - fn test_value_expr_source_collector_visit_happy_path() { - let variable_source = Uuid::new_v4(); - let action_source = Uuid::new_v4(); - let resolve = |name: &str| { - if name == "x" { - Some(variable_source) - } else { - None - } - }; - let collector = ValueExprSourceCollector::new(&resolve); - let expr = ValueExpr::BinaryOp(BinaryOpValue { - left: Box::new(ValueExpr::Variable(VariableValue { - name: "x".to_string(), - })), - op: ir::BinaryOperator::BinaryOpAdd as i32, - right: Box::new(ValueExpr::ActionResult(ActionResultValue { - node_id: action_source, - action_name: "fetch".to_string(), - iteration_index: None, - result_index: None, - })), - }); - - let sources = collector.visit(&expr); - assert!(sources.contains(&variable_source)); - assert!(sources.contains(&action_source)); - } - - #[test] - fn test_value_expr_evaluator_visit_happy_path() { - let resolve_variable = |name: &str| -> Result { - if name == "x" { - Ok(Value::Number(2.into())) - } else { - Err(format!("unknown variable: {name}")) - } - }; - let resolve_action_result = - |_value: &ActionResultValue| -> Result { Ok(Value::Number(0.into())) }; - let resolve_function_call = - |_call: &FunctionCallValue, - args: Vec, - _kwargs: HashMap| - -> Result { Ok(Value::Number((args.len() as i64).into())) }; - let apply_binary = |_op: i32, left: Value, right: Value| -> Result { - match (left.as_i64(), right.as_i64()) { - (Some(left), Some(right)) => Ok(Value::Number((left + right).into())), - _ => Err("bad operands".to_string()), - } - }; - let apply_unary = |_op: i32, value: Value| -> Result { - Ok(Value::Bool(!value.as_bool().unwrap_or(false))) - }; - let error_factory = |message: &str| message.to_string(); - - let evaluator = ValueExprEvaluator::new( - &resolve_variable, - &resolve_action_result, - &resolve_function_call, - &apply_binary, - &apply_unary, - &error_factory, - ); - let expr = ValueExpr::BinaryOp(BinaryOpValue { - left: Box::new(ValueExpr::Variable(VariableValue { - name: "x".to_string(), - })), - op: ir::BinaryOperator::BinaryOpAdd as i32, - right: Box::new(literal_int(5)), - }); - - let value = evaluator.visit(&expr).expect("evaluate expression"); - assert_eq!(value, Value::Number(7.into())); - } - - #[test] - fn test_resolve_value_tree_happy_path() { - let expr = ValueExpr::List(ListValue { - elements: vec![ValueExpr::Variable(VariableValue { - name: "user_id".to_string(), - })], - }); - let resolve = |name: &str, _seen: &mut HashSet| { - if name == "user_id" { - ValueExpr::Literal(LiteralValue { - value: Value::String("abc".to_string()), - }) - } else { - ValueExpr::Literal(LiteralValue { value: Value::Null }) - } - }; - - let resolved = resolve_value_tree(&expr, &resolve); - match resolved { - ValueExpr::List(list) => { - assert_eq!(list.elements.len(), 1); - assert!(matches!(list.elements[0], ValueExpr::Literal(_))); - } - other => panic!("expected list value, got {other:?}"), - } - } - - #[test] - fn test_collect_value_sources_happy_path() { - let source_a = Uuid::new_v4(); - let source_b = Uuid::new_v4(); - let expr = ValueExpr::FunctionCall(FunctionCallValue { - name: "sum".to_string(), - args: vec![ValueExpr::Variable(VariableValue { - name: "a".to_string(), - })], - kwargs: HashMap::from([( - "other".to_string(), - ValueExpr::ActionResult(ActionResultValue { - node_id: source_b, - action_name: "compute".to_string(), - iteration_index: None, - result_index: None, - }), - )]), - global_function: None, - }); - let resolve = |name: &str| if name == "a" { Some(source_a) } else { None }; - - let sources = collect_value_sources(&expr, &resolve); - assert_eq!(sources.len(), 2); - assert!(sources.contains(&source_a)); - assert!(sources.contains(&source_b)); - } -} +mod tests; diff --git a/crates/lib/runner-state/src/value_visitor/tests.rs b/crates/lib/runner-state/src/value_visitor/tests.rs new file mode 100644 index 00000000..653e73be --- /dev/null +++ b/crates/lib/runner-state/src/value_visitor/tests.rs @@ -0,0 +1,174 @@ +use std::collections::{HashMap, HashSet}; + +use serde_json::Value; +use uuid::Uuid; + +use super::*; +use waymark_proto::ast as ir; + +fn literal_int(value: i64) -> ValueExpr { + ValueExpr::Literal(LiteralValue { + value: Value::Number(value.into()), + }) +} + +#[test] +fn test_value_expr_resolver_visit_happy_path() { + let mut seen = HashSet::new(); + let resolve = |name: &str, _: &mut HashSet| { + if name == "x" { + literal_int(3) + } else { + literal_int(0) + } + }; + let mut resolver = ValueExprResolver::new(&resolve, &mut seen); + let expr = ValueExpr::BinaryOp(BinaryOpValue { + left: Box::new(ValueExpr::Variable(VariableValue { + name: "x".to_string(), + })), + op: ir::BinaryOperator::BinaryOpAdd as i32, + right: Box::new(literal_int(1)), + }); + + let resolved = resolver.visit(&expr); + match resolved { + ValueExpr::BinaryOp(value) => { + assert!(matches!(*value.left, ValueExpr::Literal(_))); + assert!(matches!(*value.right, ValueExpr::Literal(_))); + } + other => panic!("expected binary value, got {other:?}"), + } +} + +#[test] +fn test_value_expr_source_collector_visit_happy_path() { + let variable_source = Uuid::new_v4(); + let action_source = Uuid::new_v4(); + let resolve = |name: &str| { + if name == "x" { + Some(variable_source) + } else { + None + } + }; + let collector = ValueExprSourceCollector::new(&resolve); + let expr = ValueExpr::BinaryOp(BinaryOpValue { + left: Box::new(ValueExpr::Variable(VariableValue { + name: "x".to_string(), + })), + op: ir::BinaryOperator::BinaryOpAdd as i32, + right: Box::new(ValueExpr::ActionResult(ActionResultValue { + node_id: action_source, + action_name: "fetch".to_string(), + iteration_index: None, + result_index: None, + })), + }); + + let sources = collector.visit(&expr); + assert!(sources.contains(&variable_source)); + assert!(sources.contains(&action_source)); +} + +#[test] +fn test_value_expr_evaluator_visit_happy_path() { + let resolve_variable = |name: &str| -> Result { + if name == "x" { + Ok(Value::Number(2.into())) + } else { + Err(format!("unknown variable: {name}")) + } + }; + let resolve_action_result = + |_value: &ActionResultValue| -> Result { Ok(Value::Number(0.into())) }; + let resolve_function_call = + |_call: &FunctionCallValue, + args: Vec, + _kwargs: HashMap| + -> Result { Ok(Value::Number((args.len() as i64).into())) }; + let apply_binary = |_op: i32, left: Value, right: Value| -> Result { + match (left.as_i64(), right.as_i64()) { + (Some(left), Some(right)) => Ok(Value::Number((left + right).into())), + _ => Err("bad operands".to_string()), + } + }; + let apply_unary = |_op: i32, value: Value| -> Result { + Ok(Value::Bool(!value.as_bool().unwrap_or(false))) + }; + let error_factory = |message: &str| message.to_string(); + + let evaluator = ValueExprEvaluator::new( + &resolve_variable, + &resolve_action_result, + &resolve_function_call, + &apply_binary, + &apply_unary, + &error_factory, + ); + let expr = ValueExpr::BinaryOp(BinaryOpValue { + left: Box::new(ValueExpr::Variable(VariableValue { + name: "x".to_string(), + })), + op: ir::BinaryOperator::BinaryOpAdd as i32, + right: Box::new(literal_int(5)), + }); + + let value = evaluator.visit(&expr).expect("evaluate expression"); + assert_eq!(value, Value::Number(7.into())); +} + +#[test] +fn test_resolve_value_tree_happy_path() { + let expr = ValueExpr::List(ListValue { + elements: vec![ValueExpr::Variable(VariableValue { + name: "user_id".to_string(), + })], + }); + let resolve = |name: &str, _seen: &mut HashSet| { + if name == "user_id" { + ValueExpr::Literal(LiteralValue { + value: Value::String("abc".to_string()), + }) + } else { + ValueExpr::Literal(LiteralValue { value: Value::Null }) + } + }; + + let resolved = resolve_value_tree(&expr, &resolve); + match resolved { + ValueExpr::List(list) => { + assert_eq!(list.elements.len(), 1); + assert!(matches!(list.elements[0], ValueExpr::Literal(_))); + } + other => panic!("expected list value, got {other:?}"), + } +} + +#[test] +fn test_collect_value_sources_happy_path() { + let source_a = Uuid::new_v4(); + let source_b = Uuid::new_v4(); + let expr = ValueExpr::FunctionCall(FunctionCallValue { + name: "sum".to_string(), + args: vec![ValueExpr::Variable(VariableValue { + name: "a".to_string(), + })], + kwargs: HashMap::from([( + "other".to_string(), + ValueExpr::ActionResult(ActionResultValue { + node_id: source_b, + action_name: "compute".to_string(), + iteration_index: None, + result_index: None, + }), + )]), + global_function: None, + }); + let resolve = |name: &str| if name == "a" { Some(source_a) } else { None }; + + let sources = collect_value_sources(&expr, &resolve); + assert_eq!(sources.len(), 2); + assert!(sources.contains(&source_a)); + assert!(sources.contains(&source_b)); +} From bc8517d8b08d4f537172f34b3584b7bd4e677e0c Mon Sep 17 00:00:00 2001 From: MOZGIII Date: Thu, 5 Mar 2026 03:53:02 +0400 Subject: [PATCH 3/5] Expression evaliator rework PoC by AI --- crates/lib/runner/src/expression_evaluator.rs | 268 +++++++++-- .../runner/src/expression_evaluator/tests.rs | 431 ++++++++++++++++++ 2 files changed, 652 insertions(+), 47 deletions(-) diff --git a/crates/lib/runner/src/expression_evaluator.rs b/crates/lib/runner/src/expression_evaluator.rs index d8dc8e0d..1ac4fe8c 100644 --- a/crates/lib/runner/src/expression_evaluator.rs +++ b/crates/lib/runner/src/expression_evaluator.rs @@ -257,59 +257,233 @@ impl RunnerExecutor { target: &str, stack: Rc>>, ) -> Result { - let key = (node_id, target.to_string()); - if let Some(value) = self.eval_cache_get(&key) { + #[derive(Clone)] + enum AssignmentFrame { + Discover { + node_id: Uuid, + target: String, + }, + Evaluate { + node_id: Uuid, + target: String, + expr: ValueExpr, + }, + } + + let root_key = (node_id, target.to_string()); + if let Some(value) = self.eval_cache_get(&root_key) { return Ok(value); } - if stack.borrow().contains(&key) { - return Err(RunnerExecutorError(format!( - "recursive assignment detected for {target}" - ))); + + let mut frames = vec![AssignmentFrame::Discover { + node_id, + target: target.to_string(), + }]; + let mut computed: HashMap<(Uuid, String), Value> = HashMap::new(); + let mut activated_keys: Vec<(Uuid, String)> = Vec::new(); + + let result: Result = (|| { + while let Some(frame) = frames.pop() { + match frame { + AssignmentFrame::Discover { node_id, target } => { + let key = (node_id, target.clone()); + if computed.contains_key(&key) { + continue; + } + if let Some(value) = self.eval_cache_get(&key) { + computed.insert(key, value); + continue; + } + if stack.borrow().contains(&key) { + return Err(RunnerExecutorError(format!( + "recursive assignment detected for {target}" + ))); + } + + let expr = self + .state() + .nodes + .get(&node_id) + .and_then(|node| node.assignments.get(&target)) + .cloned() + .ok_or_else(|| { + RunnerExecutorError(format!("missing assignment for {target}")) + })?; + + stack.borrow_mut().insert(key.clone()); + activated_keys.push(key.clone()); + + let deps = self.assignment_dependencies(node_id, &expr)?; + frames.push(AssignmentFrame::Evaluate { + node_id, + target: target.clone(), + expr, + }); + for (dep_node_id, dep_target) in deps.into_iter().rev() { + let dep_key = (dep_node_id, dep_target.clone()); + if computed.contains_key(&dep_key) + || self.eval_cache_get(&dep_key).is_some() + { + continue; + } + frames.push(AssignmentFrame::Discover { + node_id: dep_node_id, + target: dep_target, + }); + } + } + AssignmentFrame::Evaluate { + node_id, + target, + expr, + } => { + let key = (node_id, target.clone()); + if let Some(value) = self.eval_cache_get(&key) { + computed.insert(key.clone(), value.clone()); + stack.borrow_mut().remove(&key); + continue; + } + + let resolve_variable = { + let this = self; + let computed = &computed; + move |name: &str| { + let dep_node_id = this + .find_variable_source_node(node_id, name) + .or_else(|| this.state().latest_assignment(name)) + .ok_or_else(|| { + RunnerExecutorError(format!("variable not found: {name}")) + })?; + let dep_key = (dep_node_id, name.to_string()); + if let Some(value) = computed.get(&dep_key) { + return Ok(value.clone()); + } + if let Some(value) = this.eval_cache_get(&dep_key) { + return Ok(value); + } + Err(RunnerExecutorError(format!( + "unresolved assignment dependency for {name}" + ))) + } + }; + let resolve_action_result = { + let this = self; + move |value: &ActionResultValue| this.resolve_action_result(value) + }; + let resolve_function_call = { + let this = self; + move |value: &FunctionCallValue, args, kwargs| { + this.evaluate_function_call(value, args, kwargs) + } + }; + let apply_binary = |op, left, right| Self::apply_binary(op, left, right); + let apply_unary = |op, operand| Self::apply_unary(op, operand); + let error_factory = + |message: &str| RunnerExecutorError(message.to_string()); + let evaluator = ValueExprEvaluator::new( + &resolve_variable, + &resolve_action_result, + &resolve_function_call, + &apply_binary, + &apply_unary, + &error_factory, + ); + let value = evaluator.visit(&expr)?; + + self.eval_cache_insert(key.clone(), value.clone()); + computed.insert(key.clone(), value); + stack.borrow_mut().remove(&key); + } + } + } + + computed + .get(&root_key) + .cloned() + .or_else(|| self.eval_cache_get(&root_key)) + .ok_or_else(|| { + RunnerExecutorError(format!("missing assignment for {}", root_key.1)) + }) + })(); + + let mut active = stack.borrow_mut(); + for key in activated_keys { + active.remove(&key); } + drop(active); - let node = self - .state() - .nodes - .get(&node_id) - .ok_or_else(|| RunnerExecutorError(format!("missing assignment for {target}")))?; - let expr = node - .assignments - .get(target) - .ok_or_else(|| RunnerExecutorError(format!("missing assignment for {target}")))?; - - stack.borrow_mut().insert(key.clone()); - let resolve_variable = { - let stack = stack.clone(); - let this = self; - move |name: &str| { - this.evaluate_variable_with_context(Some(node_id), name, stack.clone()) + result + } + + fn assignment_dependencies( + &self, + current_node_id: Uuid, + expr: &ValueExpr, + ) -> Result, RunnerExecutorError> { + let mut dependencies = Vec::new(); + let mut seen = HashSet::new(); + for name in Self::collect_variable_names(expr) { + if !seen.insert(name.clone()) { + continue; } - }; - let resolve_action_result = { - let this = self; - move |value: &ActionResultValue| this.resolve_action_result(value) - }; - let resolve_function_call = { - let this = self; - move |value: &FunctionCallValue, args, kwargs| { - this.evaluate_function_call(value, args, kwargs) + let node_id = self + .find_variable_source_node(current_node_id, &name) + .or_else(|| self.state().latest_assignment(&name)) + .ok_or_else(|| RunnerExecutorError(format!("variable not found: {name}")))?; + dependencies.push((node_id, name)); + } + Ok(dependencies) + } + + fn collect_variable_names(expr: &ValueExpr) -> Vec { + let mut names = Vec::new(); + let mut pending = vec![expr]; + while let Some(current) = pending.pop() { + match current { + ValueExpr::Variable(value) => names.push(value.name.clone()), + ValueExpr::BinaryOp(value) => { + pending.push(&value.right); + pending.push(&value.left); + } + ValueExpr::UnaryOp(value) => { + pending.push(&value.operand); + } + ValueExpr::List(value) => { + for item in &value.elements { + pending.push(item); + } + } + ValueExpr::Dict(value) => { + for entry in &value.entries { + pending.push(&entry.value); + pending.push(&entry.key); + } + } + ValueExpr::Index(value) => { + pending.push(&value.index); + pending.push(&value.object); + } + ValueExpr::Dot(value) => { + pending.push(&value.object); + } + ValueExpr::FunctionCall(value) => { + for arg in value.kwargs.values() { + pending.push(arg); + } + for arg in &value.args { + pending.push(arg); + } + } + ValueExpr::Spread(value) => { + for arg in value.action.kwargs.values() { + pending.push(arg); + } + pending.push(&value.collection); + } + ValueExpr::Literal(_) | ValueExpr::ActionResult(_) => {} } - }; - let apply_binary = |op, left, right| Self::apply_binary(op, left, right); - let apply_unary = |op, operand| Self::apply_unary(op, operand); - let error_factory = |message: &str| RunnerExecutorError(message.to_string()); - let evaluator = ValueExprEvaluator::new( - &resolve_variable, - &resolve_action_result, - &resolve_function_call, - &apply_binary, - &apply_unary, - &error_factory, - ); - let value = evaluator.visit(expr)?; - stack.borrow_mut().remove(&key); - self.eval_cache_insert(key, value.clone()); - Ok(value) + } + names } pub(super) fn resolve_action_result( diff --git a/crates/lib/runner/src/expression_evaluator/tests.rs b/crates/lib/runner/src/expression_evaluator/tests.rs index e69de29b..3b7b2b22 100644 --- a/crates/lib/runner/src/expression_evaluator/tests.rs +++ b/crates/lib/runner/src/expression_evaluator/tests.rs @@ -0,0 +1,431 @@ +use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; +use std::rc::Rc; +use std::sync::Arc; + +use uuid::Uuid; + +use super::*; +use waymark_dag::{DAG, DAGEdge}; +use waymark_ir_parser::IRParser; +use waymark_proto::ast as ir; +use waymark_runner_state::{ + ActionCallSpec, ActionResultValue, BinaryOpValue, FunctionCallValue, LiteralValue, RunnerState, + VariableValue, value_visitor::ValueExpr, +}; + +fn parse_expr(source: &str) -> ir::Expr { + IRParser::new(" ") + .parse_expr(source) + .expect("parse expression") +} + +fn literal_int(value: i64) -> ValueExpr { + ValueExpr::Literal(LiteralValue { + value: Value::Number(value.into()), + }) +} + +fn empty_executor() -> RunnerExecutor { + let dag = Arc::new(DAG::default()); + let state = RunnerState::new(Some(Arc::clone(&dag)), None, None, false); + RunnerExecutor::new(dag, state, HashMap::new(), None) +} + +fn executor_with_assignment(name: &str, value: ValueExpr) -> RunnerExecutor { + let dag = Arc::new(DAG::default()); + let mut state = RunnerState::new(Some(Arc::clone(&dag)), None, None, false); + state + .record_assignment_value( + vec![name.to_string()], + value, + None, + Some("test assignment".to_string()), + ) + .expect("record assignment"); + RunnerExecutor::new(dag, state, HashMap::new(), None) +} + +#[test] +fn test_expr_to_value_happy_path() { + let expr = parse_expr("x + 2"); + let value = RunnerExecutor::expr_to_value(&expr).expect("convert expression"); + match value { + ValueExpr::BinaryOp(binary) => { + assert!(matches!(*binary.left, ValueExpr::Variable(_))); + assert!(matches!(*binary.right, ValueExpr::Literal(_))); + } + other => panic!("expected binary op, got {other:?}"), + } +} + +#[test] +fn test_evaluate_guard_happy_path() { + let executor = executor_with_assignment("x", literal_int(2)); + let guard = parse_expr("x > 1"); + let result = executor + .evaluate_guard(Some(&guard)) + .expect("evaluate guard"); + assert!(result); +} + +#[test] +fn test_resolve_action_kwargs_happy_path() { + let executor = executor_with_assignment("x", literal_int(10)); + let action = ActionCallSpec { + action_name: "double".to_string(), + module_name: Some("tests".to_string()), + kwargs: HashMap::from([( + "value".to_string(), + ValueExpr::Variable(VariableValue { + name: "x".to_string(), + }), + )]), + }; + let resolved = executor + .resolve_action_kwargs(Uuid::new_v4(), &action) + .expect("resolve kwargs"); + assert_eq!(resolved.get("value"), Some(&Value::Number(10.into()))); +} + +#[test] +fn test_resolve_action_kwargs_uses_data_flow_for_self_referential_targets() { + let dag = Arc::new(DAG::default()); + let mut state = RunnerState::new(Some(Arc::clone(&dag)), None, None, false); + state + .record_assignment_value( + vec!["current".to_string()], + literal_int(0), + None, + Some("current = 0".to_string()), + ) + .expect("record current"); + let action_result = state + .queue_action( + "increment", + Some(vec!["current".to_string()]), + Some(HashMap::from([( + "value".to_string(), + ValueExpr::Variable(VariableValue { + name: "current".to_string(), + }), + )])), + None, + None, + ) + .expect("queue increment"); + let action_node = state + .nodes + .get(&action_result.node_id) + .expect("action node") + .clone(); + let action_spec = action_node.action.expect("action spec"); + + let executor = RunnerExecutor::new(dag, state, HashMap::new(), None); + let resolved = executor + .resolve_action_kwargs(action_result.node_id, &action_spec) + .expect("resolve kwargs"); + assert_eq!(resolved.get("value"), Some(&Value::Number(0.into()))); +} + +#[test] +fn test_evaluate_value_expr_happy_path() { + let executor = executor_with_assignment("x", literal_int(3)); + let expr = ValueExpr::BinaryOp(waymark_runner_state::BinaryOpValue { + left: Box::new(ValueExpr::Variable(VariableValue { + name: "x".to_string(), + })), + op: ir::BinaryOperator::BinaryOpAdd as i32, + right: Box::new(literal_int(1)), + }); + let value = executor + .evaluate_value_expr(&expr) + .expect("evaluate value expression"); + assert_eq!(value, Value::Number(4.into())); +} + +#[test] +fn test_evaluate_variable_happy_path() { + let executor = executor_with_assignment("value", literal_int(5)); + let stack = Rc::new(RefCell::new(HashSet::new())); + let value = executor + .evaluate_variable_with_context(None, "value", stack) + .expect("evaluate variable"); + assert_eq!(value, Value::Number(5.into())); +} + +#[test] +fn test_evaluate_assignment_happy_path() { + let executor = executor_with_assignment("value", literal_int(9)); + let node_id = executor + .state() + .latest_assignment("value") + .expect("latest assignment"); + let stack = Rc::new(RefCell::new(HashSet::new())); + let value = executor + .evaluate_assignment(node_id, "value", stack) + .expect("evaluate assignment"); + assert_eq!(value, Value::Number(9.into())); +} + +#[test] +fn test_evaluate_assignment_uses_data_flow_for_self_referential_updates() { + let dag = Arc::new(DAG::default()); + let mut state = RunnerState::new(Some(Arc::clone(&dag)), None, None, false); + state + .record_assignment_value( + vec!["count".to_string()], + literal_int(0), + None, + Some("count = 0".to_string()), + ) + .expect("record initial count"); + state + .record_assignment_value( + vec!["count".to_string()], + ValueExpr::BinaryOp(BinaryOpValue { + left: Box::new(ValueExpr::Variable(VariableValue { + name: "count".to_string(), + })), + op: ir::BinaryOperator::BinaryOpAdd as i32, + right: Box::new(literal_int(1)), + }), + None, + Some("count = count + 1".to_string()), + ) + .expect("record updated count"); + + let executor = RunnerExecutor::new(dag, state, HashMap::new(), None); + let node_id = executor + .state() + .latest_assignment("count") + .expect("latest assignment"); + let stack = Rc::new(RefCell::new(HashSet::new())); + let value = executor + .evaluate_assignment(node_id, "count", stack) + .expect("evaluate self-referential assignment"); + assert_eq!(value, Value::Number(1.into())); +} + +#[test] +fn test_evaluate_assignment_handles_deep_dependency_chain() { + let dag = Arc::new(DAG::default()); + let mut state = RunnerState::new(Some(Arc::clone(&dag)), None, None, false); + + state + .record_assignment_value( + vec!["count".to_string()], + literal_int(0), + None, + Some("count = 0".to_string()), + ) + .expect("record initial count"); + + for idx in 0..5000 { + state + .record_assignment_value( + vec!["count".to_string()], + ValueExpr::BinaryOp(BinaryOpValue { + left: Box::new(ValueExpr::Variable(VariableValue { + name: "count".to_string(), + })), + op: ir::BinaryOperator::BinaryOpAdd as i32, + right: Box::new(literal_int(1)), + }), + None, + Some(format!("count = count + 1 #{idx}")), + ) + .expect("record updated count"); + } + + let executor = RunnerExecutor::new(dag, state, HashMap::new(), None); + let node_id = executor + .state() + .latest_assignment("count") + .expect("latest assignment"); + let stack = Rc::new(RefCell::new(HashSet::new())); + let value = executor + .evaluate_assignment(node_id, "count", stack) + .expect("evaluate deep dependency chain"); + + assert_eq!(value, Value::Number(5000.into())); +} + +#[test] +fn test_resolve_action_result_happy_path() { + let mut executor = empty_executor(); + let action_id = Uuid::new_v4(); + executor.set_action_result( + action_id, + Value::Array(vec![Value::Number(7.into()), Value::Number(8.into())]), + ); + let result = executor + .resolve_action_result(&ActionResultValue { + node_id: action_id, + action_name: "fetch".to_string(), + iteration_index: None, + result_index: Some(1), + }) + .expect("resolve action result"); + assert_eq!(result, Value::Number(8.into())); +} + +#[test] +fn test_evaluate_function_call_happy_path() { + let executor = empty_executor(); + let value = executor + .evaluate_function_call( + &FunctionCallValue { + name: "len".to_string(), + args: Vec::new(), + kwargs: HashMap::new(), + global_function: Some(ir::GlobalFunction::Len as i32), + }, + vec![Value::Array(vec![Value::Null, Value::Null])], + HashMap::new(), + ) + .expect("evaluate function call"); + assert_eq!(value, Value::Number(2.into())); +} + +#[test] +fn test_evaluate_global_function_happy_path() { + let executor = empty_executor(); + let value = executor + .evaluate_global_function( + ir::GlobalFunction::Range as i32, + vec![Value::Number(1.into()), Value::Number(4.into())], + HashMap::new(), + ) + .expect("evaluate global function"); + assert_eq!( + value, + Value::Array(vec![ + Value::Number(1.into()), + Value::Number(2.into()), + Value::Number(3.into()) + ]) + ); +} + +#[test] +fn test_apply_binary_happy_path() { + let value = RunnerExecutor::apply_binary( + ir::BinaryOperator::BinaryOpAdd as i32, + Value::Number(2.into()), + Value::Number(3.into()), + ) + .expect("apply binary"); + assert_eq!(value, Value::Number(5.into())); +} + +#[test] +fn test_apply_unary_happy_path() { + let value = + RunnerExecutor::apply_unary(ir::UnaryOperator::UnaryOpNot as i32, Value::Bool(true)) + .expect("apply unary"); + assert_eq!(value, Value::Bool(false)); +} + +#[test] +fn test_exception_matches_happy_path() { + let executor = empty_executor(); + let edge = DAGEdge::state_machine_with_exception("a", "b", vec!["ValueError".to_string()]); + let exception = serde_json::json!({ + "type": "ValueError", + "message": "boom", + }); + assert!(executor.exception_matches(&edge, &exception)); +} + +#[test] +fn test_executor_error_happy_path() { + let error = executor_error("hello"); + assert_eq!(error.0, "hello"); +} + +#[test] +fn test_int_value_happy_path() { + let value = Value::Number(7_u64.into()); + assert_eq!(int_value(&value), Some(7)); +} + +#[test] +fn test_numeric_op_happy_path() { + let value = numeric_op( + Value::Number(10.into()), + Value::Number(3.into()), + |a, b| a + b, + true, + executor_error, + ) + .expect("numeric op"); + assert_eq!(value, Value::Number(13.into())); +} + +#[test] +fn test_add_values_happy_path() { + let value = add_values( + Value::String("hello ".to_string()), + Value::String("world".to_string()), + executor_error, + ) + .expect("add values"); + assert_eq!(value, Value::String("hello world".to_string())); +} + +#[test] +fn test_compare_values_happy_path() { + let value = compare_values( + Value::Number(3.into()), + Value::Number(5.into()), + |a, b| a < b, + executor_error, + ) + .expect("compare values"); + assert_eq!(value, Value::Bool(true)); +} + +#[test] +fn test_value_in_happy_path() { + let container = Value::Array(vec![Value::Number(1.into()), Value::Number(2.into())]); + assert!(value_in(&Value::Number(2.into()), &container)); +} + +#[test] +fn test_is_truthy_happy_path() { + assert!(is_truthy(&Value::String("non-empty".to_string()))); +} + +#[test] +fn test_is_exception_value_happy_path() { + let value = serde_json::json!({ + "type": "RuntimeError", + "message": "bad", + }); + assert!(is_exception_value(&value)); +} + +#[test] +fn test_len_of_value_happy_path() { + let value = Value::Array(vec![Value::Null, Value::Null, Value::Null]); + let len = len_of_value(&value, executor_error).expect("length"); + assert_eq!(len.as_i64(), Some(3)); +} + +#[test] +fn test_range_from_args_happy_path() { + let values = range_from_args(&[ + Value::Number(0.into()), + Value::Number(5.into()), + Value::Number(2.into()), + ]); + assert_eq!( + values, + vec![ + Value::Number(0.into()), + Value::Number(2.into()), + Value::Number(4.into()) + ] + ); +} From c8696a34ee6de567c7772fc78a0073f2c3047bdc Mon Sep 17 00:00:00 2001 From: MOZGIII Date: Thu, 5 Mar 2026 03:59:28 +0400 Subject: [PATCH 4/5] Visitor rework PoC by AI --- crates/lib/runner-state/src/value_visitor.rs | 264 +++++++++++++----- .../runner-state/src/value_visitor/tests.rs | 47 ++++ 2 files changed, 242 insertions(+), 69 deletions(-) diff --git a/crates/lib/runner-state/src/value_visitor.rs b/crates/lib/runner-state/src/value_visitor.rs index 8a2ba175..385abb59 100644 --- a/crates/lib/runner-state/src/value_visitor.rs +++ b/crates/lib/runner-state/src/value_visitor.rs @@ -230,84 +230,210 @@ impl<'a, E> ValueExprEvaluator<'a, E> { } pub fn visit(&self, expr: &ValueExpr) -> Result { - match expr { - ValueExpr::Literal(value) => Ok(value.value.clone()), - ValueExpr::Variable(value) => (self.resolve_variable)(&value.name), - ValueExpr::ActionResult(value) => (self.resolve_action_result)(value), - ValueExpr::BinaryOp(value) => { - let left = self.visit(&value.left)?; - let right = self.visit(&value.right)?; - (self.apply_binary)(value.op, left, right) - } - ValueExpr::UnaryOp(value) => { - let operand = self.visit(&value.operand)?; - (self.apply_unary)(value.op, operand) - } - ValueExpr::List(value) => { - let mut items = Vec::with_capacity(value.elements.len()); - for item in &value.elements { - items.push(self.visit(item)?); + enum EvalFrame<'b> { + Eval(&'b ValueExpr), + ApplyBinary(i32), + ApplyUnary(i32), + BuildList(usize), + BuildDict(usize), + ApplyIndex, + ApplyDot(String), + ApplyFunctionCall { + call: &'b FunctionCallValue, + args_len: usize, + kwarg_names: Vec, + }, + } + + let mut frames = vec![EvalFrame::Eval(expr)]; + let mut values: Vec = Vec::new(); + + while let Some(frame) = frames.pop() { + match frame { + EvalFrame::Eval(current) => match current { + ValueExpr::Literal(value) => values.push(value.value.clone()), + ValueExpr::Variable(value) => { + values.push((self.resolve_variable)(&value.name)?); + } + ValueExpr::ActionResult(value) => { + values.push((self.resolve_action_result)(value)?); + } + ValueExpr::BinaryOp(value) => { + frames.push(EvalFrame::ApplyBinary(value.op)); + frames.push(EvalFrame::Eval(&value.right)); + frames.push(EvalFrame::Eval(&value.left)); + } + ValueExpr::UnaryOp(value) => { + frames.push(EvalFrame::ApplyUnary(value.op)); + frames.push(EvalFrame::Eval(&value.operand)); + } + ValueExpr::List(value) => { + frames.push(EvalFrame::BuildList(value.elements.len())); + for item in value.elements.iter().rev() { + frames.push(EvalFrame::Eval(item)); + } + } + ValueExpr::Dict(value) => { + frames.push(EvalFrame::BuildDict(value.entries.len())); + for entry in value.entries.iter().rev() { + frames.push(EvalFrame::Eval(&entry.value)); + frames.push(EvalFrame::Eval(&entry.key)); + } + } + ValueExpr::Index(value) => { + frames.push(EvalFrame::ApplyIndex); + frames.push(EvalFrame::Eval(&value.index)); + frames.push(EvalFrame::Eval(&value.object)); + } + ValueExpr::Dot(value) => { + frames.push(EvalFrame::ApplyDot(value.attribute.clone())); + frames.push(EvalFrame::Eval(&value.object)); + } + ValueExpr::FunctionCall(value) => { + let kwarg_names: Vec = value.kwargs.keys().cloned().collect(); + frames.push(EvalFrame::ApplyFunctionCall { + call: value, + args_len: value.args.len(), + kwarg_names: kwarg_names.clone(), + }); + for name in kwarg_names.iter().rev() { + let arg = value.kwargs.get(name).ok_or_else(|| { + (self.error_factory)("function call kwargs mismatch") + })?; + frames.push(EvalFrame::Eval(arg)); + } + for arg in value.args.iter().rev() { + frames.push(EvalFrame::Eval(arg)); + } + } + ValueExpr::Spread(_) => { + return Err((self.error_factory)( + "cannot replay unresolved spread expression", + )); + } + }, + EvalFrame::ApplyBinary(op) => { + let right = values + .pop() + .ok_or_else(|| (self.error_factory)("binary op missing right operand"))?; + let left = values + .pop() + .ok_or_else(|| (self.error_factory)("binary op missing left operand"))?; + values.push((self.apply_binary)(op, left, right)?); } - Ok(serde_json::Value::Array(items)) - } - ValueExpr::Dict(value) => { - let mut map = serde_json::Map::with_capacity(value.entries.len()); - for entry in &value.entries { - let key_value = self.visit(&entry.key)?; - let key = key_value - .as_str() - .map(|value| value.to_string()) - .unwrap_or_else(|| key_value.to_string()); - let entry_value = self.visit(&entry.value)?; - map.insert(key, entry_value); + EvalFrame::ApplyUnary(op) => { + let operand = values + .pop() + .ok_or_else(|| (self.error_factory)("unary op missing operand"))?; + values.push((self.apply_unary)(op, operand)?); } - Ok(serde_json::Value::Object(map)) - } - ValueExpr::Index(value) => { - let object = self.visit(&value.object)?; - let index = self.visit(&value.index)?; - match (object, index) { - (serde_json::Value::Array(items), serde_json::Value::Number(idx)) => { - let idx = idx.as_i64().unwrap_or(-1); - if idx < 0 || idx as usize >= items.len() { - return Err((self.error_factory)("index out of range")); - } - Ok(items[idx as usize].clone()) + EvalFrame::BuildList(len) => { + let mut items = Vec::with_capacity(len); + for _ in 0..len { + items.push( + values + .pop() + .ok_or_else(|| (self.error_factory)("list missing element"))?, + ); } - (serde_json::Value::Object(map), serde_json::Value::String(key)) => map - .get(&key) - .cloned() - .or_else(|| lookup_exception_value(&map, &key)) - .ok_or_else(|| (self.error_factory)("dict has no key")), - _ => Err((self.error_factory)("unsupported index operation")), + items.reverse(); + values.push(serde_json::Value::Array(items)); } - } - ValueExpr::Dot(value) => { - let object = self.visit(&value.object)?; - if let serde_json::Value::Object(map) = object { - return map - .get(&value.attribute) - .cloned() - .or_else(|| lookup_exception_value(&map, &value.attribute)) - .ok_or_else(|| (self.error_factory)("dict has no key")); + EvalFrame::BuildDict(len) => { + let mut entries: Vec<(String, serde_json::Value)> = Vec::with_capacity(len); + for _ in 0..len { + let entry_value = values + .pop() + .ok_or_else(|| (self.error_factory)("dict missing value"))?; + let key_value = values + .pop() + .ok_or_else(|| (self.error_factory)("dict missing key"))?; + let key = key_value + .as_str() + .map(|value| value.to_string()) + .unwrap_or_else(|| key_value.to_string()); + entries.push((key, entry_value)); + } + entries.reverse(); + let mut map = serde_json::Map::with_capacity(len); + for (key, value) in entries { + map.insert(key, value); + } + values.push(serde_json::Value::Object(map)); } - Err((self.error_factory)("attribute not found")) - } - ValueExpr::FunctionCall(value) => { - let mut args = Vec::with_capacity(value.args.len()); - for arg in &value.args { - args.push(self.visit(arg)?); + EvalFrame::ApplyIndex => { + let index = values + .pop() + .ok_or_else(|| (self.error_factory)("index missing index value"))?; + let object = values + .pop() + .ok_or_else(|| (self.error_factory)("index missing object value"))?; + let resolved = match (object, index) { + (serde_json::Value::Array(items), serde_json::Value::Number(idx)) => { + let idx = idx.as_i64().unwrap_or(-1); + if idx < 0 || idx as usize >= items.len() { + return Err((self.error_factory)("index out of range")); + } + items[idx as usize].clone() + } + (serde_json::Value::Object(map), serde_json::Value::String(key)) => map + .get(&key) + .cloned() + .or_else(|| lookup_exception_value(&map, &key)) + .ok_or_else(|| (self.error_factory)("dict has no key"))?, + _ => return Err((self.error_factory)("unsupported index operation")), + }; + values.push(resolved); + } + EvalFrame::ApplyDot(attribute) => { + let object = values + .pop() + .ok_or_else(|| (self.error_factory)("dot access missing object"))?; + if let serde_json::Value::Object(map) = object { + let resolved = map + .get(&attribute) + .cloned() + .or_else(|| lookup_exception_value(&map, &attribute)) + .ok_or_else(|| (self.error_factory)("dict has no key"))?; + values.push(resolved); + } else { + return Err((self.error_factory)("attribute not found")); + } } - let mut kwargs = HashMap::new(); - for (name, arg) in &value.kwargs { - kwargs.insert(name.clone(), self.visit(arg)?); + EvalFrame::ApplyFunctionCall { + call, + args_len, + kwarg_names, + } => { + let mut kwargs = HashMap::with_capacity(kwarg_names.len()); + for name in kwarg_names.iter().rev() { + let arg_value = values + .pop() + .ok_or_else(|| (self.error_factory)("function call missing kwarg"))?; + kwargs.insert(name.clone(), arg_value); + } + let mut args = Vec::with_capacity(args_len); + for _ in 0..args_len { + args.push( + values + .pop() + .ok_or_else(|| (self.error_factory)("function call missing arg"))?, + ); + } + args.reverse(); + values.push((self.resolve_function_call)(call, args, kwargs)?); } - (self.resolve_function_call)(value, args, kwargs) } - ValueExpr::Spread(_) => Err((self.error_factory)( - "cannot replay unresolved spread expression", - )), } + + if values.len() == 1 { + return values + .pop() + .ok_or_else(|| (self.error_factory)("expression stack produced no result")); + } + Err((self.error_factory)( + "expression stack produced invalid result count", + )) } } diff --git a/crates/lib/runner-state/src/value_visitor/tests.rs b/crates/lib/runner-state/src/value_visitor/tests.rs index 653e73be..d877ca1c 100644 --- a/crates/lib/runner-state/src/value_visitor/tests.rs +++ b/crates/lib/runner-state/src/value_visitor/tests.rs @@ -118,6 +118,53 @@ fn test_value_expr_evaluator_visit_happy_path() { assert_eq!(value, Value::Number(7.into())); } +#[test] +fn test_value_expr_evaluator_handles_deep_unary_chain() { + let resolve_variable = + |_name: &str| -> Result { Err("unknown variable".to_string()) }; + let resolve_action_result = + |_value: &ActionResultValue| -> Result { Ok(Value::Number(0.into())) }; + let resolve_function_call = + |_call: &FunctionCallValue, + args: Vec, + _kwargs: HashMap| + -> Result { Ok(Value::Number((args.len() as i64).into())) }; + let apply_binary = |_op: i32, left: Value, right: Value| -> Result { + match (left.as_i64(), right.as_i64()) { + (Some(left), Some(right)) => Ok(Value::Number((left + right).into())), + _ => Err("bad operands".to_string()), + } + }; + let apply_unary = |_op: i32, value: Value| -> Result { + Ok(Value::Bool(!value.as_bool().unwrap_or(false))) + }; + let error_factory = |message: &str| message.to_string(); + + let evaluator = ValueExprEvaluator::new( + &resolve_variable, + &resolve_action_result, + &resolve_function_call, + &apply_binary, + &apply_unary, + &error_factory, + ); + + let mut expr = ValueExpr::Literal(LiteralValue { + value: Value::Bool(true), + }); + for _ in 0..10_000 { + expr = ValueExpr::UnaryOp(UnaryOpValue { + op: ir::UnaryOperator::UnaryOpNot as i32, + operand: Box::new(expr), + }); + } + + let value = evaluator + .visit(&expr) + .expect("evaluate deep unary expression"); + assert_eq!(value, Value::Bool(true)); +} + #[test] fn test_resolve_value_tree_happy_path() { let expr = ValueExpr::List(ListValue { From ec9331cca4f0d27ae71b3de40aec6c5ff0422b40 Mon Sep 17 00:00:00 2001 From: MOZGIII Date: Thu, 5 Mar 2026 04:27:50 +0400 Subject: [PATCH 5/5] More rework at value visitor --- crates/lib/runner-state/src/value_visitor.rs | 372 +++++++++++++----- .../runner-state/src/value_visitor/tests.rs | 47 +++ 2 files changed, 314 insertions(+), 105 deletions(-) diff --git a/crates/lib/runner-state/src/value_visitor.rs b/crates/lib/runner-state/src/value_visitor.rs index 385abb59..f53abd7f 100644 --- a/crates/lib/runner-state/src/value_visitor.rs +++ b/crates/lib/runner-state/src/value_visitor.rs @@ -49,69 +49,225 @@ impl<'a> ValueExprResolver<'a> { } pub fn visit(&mut self, expr: &ValueExpr) -> ValueExpr { - match expr { - ValueExpr::Literal(value) => ValueExpr::Literal(value.clone()), - ValueExpr::Variable(value) => (self.resolve_variable)(&value.name, self.seen), - ValueExpr::ActionResult(value) => ValueExpr::ActionResult(value.clone()), - ValueExpr::BinaryOp(value) => ValueExpr::BinaryOp(BinaryOpValue { - left: Box::new(self.visit(&value.left)), - op: value.op, - right: Box::new(self.visit(&value.right)), - }), - ValueExpr::UnaryOp(value) => ValueExpr::UnaryOp(UnaryOpValue { - op: value.op, - operand: Box::new(self.visit(&value.operand)), - }), - ValueExpr::List(value) => ValueExpr::List(ListValue { - elements: value.elements.iter().map(|item| self.visit(item)).collect(), - }), - ValueExpr::Dict(value) => ValueExpr::Dict(DictValue { - entries: value - .entries - .iter() - .map(|entry| DictEntryValue { - key: self.visit(&entry.key), - value: self.visit(&entry.value), - }) - .collect(), - }), - ValueExpr::Index(value) => ValueExpr::Index(IndexValue { - object: Box::new(self.visit(&value.object)), - index: Box::new(self.visit(&value.index)), - }), - ValueExpr::Dot(value) => ValueExpr::Dot(DotValue { - object: Box::new(self.visit(&value.object)), - attribute: value.attribute.clone(), - }), - ValueExpr::FunctionCall(value) => ValueExpr::FunctionCall(FunctionCallValue { - name: value.name.clone(), - args: value.args.iter().map(|arg| self.visit(arg)).collect(), - kwargs: value - .kwargs - .iter() - .map(|(name, arg)| (name.clone(), self.visit(arg))) - .collect(), - global_function: value.global_function, - }), - ValueExpr::Spread(value) => { - let kwargs = value - .action - .kwargs - .iter() - .map(|(name, arg)| (name.clone(), self.visit(arg))) - .collect::>(); - let action = ActionCallSpec { - action_name: value.action.action_name.clone(), - module_name: value.action.module_name.clone(), - kwargs, - }; - ValueExpr::Spread(SpreadValue { - collection: Box::new(self.visit(&value.collection)), - loop_var: value.loop_var.clone(), - action, - }) + enum ResolveFrame<'b> { + Resolve(&'b ValueExpr), + BuildBinary(i32), + BuildUnary(i32), + BuildList(usize), + BuildDict(usize), + BuildIndex, + BuildDot(String), + BuildFunctionCall { + name: String, + global_function: Option, + args_len: usize, + kwarg_names: Vec, + }, + BuildSpread { + loop_var: String, + action_name: String, + module_name: Option, + kwarg_names: Vec, + }, + } + + let mut frames = vec![ResolveFrame::Resolve(expr)]; + let mut values: Vec = Vec::new(); + + while let Some(frame) = frames.pop() { + match frame { + ResolveFrame::Resolve(current) => match current { + ValueExpr::Literal(value) => values.push(ValueExpr::Literal(value.clone())), + ValueExpr::Variable(value) => { + values.push((self.resolve_variable)(&value.name, self.seen)); + } + ValueExpr::ActionResult(value) => { + values.push(ValueExpr::ActionResult(value.clone())); + } + ValueExpr::BinaryOp(value) => { + frames.push(ResolveFrame::BuildBinary(value.op)); + frames.push(ResolveFrame::Resolve(&value.right)); + frames.push(ResolveFrame::Resolve(&value.left)); + } + ValueExpr::UnaryOp(value) => { + frames.push(ResolveFrame::BuildUnary(value.op)); + frames.push(ResolveFrame::Resolve(&value.operand)); + } + ValueExpr::List(value) => { + frames.push(ResolveFrame::BuildList(value.elements.len())); + for item in value.elements.iter().rev() { + frames.push(ResolveFrame::Resolve(item)); + } + } + ValueExpr::Dict(value) => { + frames.push(ResolveFrame::BuildDict(value.entries.len())); + for entry in value.entries.iter().rev() { + frames.push(ResolveFrame::Resolve(&entry.value)); + frames.push(ResolveFrame::Resolve(&entry.key)); + } + } + ValueExpr::Index(value) => { + frames.push(ResolveFrame::BuildIndex); + frames.push(ResolveFrame::Resolve(&value.index)); + frames.push(ResolveFrame::Resolve(&value.object)); + } + ValueExpr::Dot(value) => { + frames.push(ResolveFrame::BuildDot(value.attribute.clone())); + frames.push(ResolveFrame::Resolve(&value.object)); + } + ValueExpr::FunctionCall(value) => { + let kwarg_names: Vec = value.kwargs.keys().cloned().collect(); + frames.push(ResolveFrame::BuildFunctionCall { + name: value.name.clone(), + global_function: value.global_function, + args_len: value.args.len(), + kwarg_names: kwarg_names.clone(), + }); + for name in kwarg_names.iter().rev() { + if let Some(arg) = value.kwargs.get(name) { + frames.push(ResolveFrame::Resolve(arg)); + } + } + for arg in value.args.iter().rev() { + frames.push(ResolveFrame::Resolve(arg)); + } + } + ValueExpr::Spread(value) => { + let kwarg_names: Vec = + value.action.kwargs.keys().cloned().collect(); + frames.push(ResolveFrame::BuildSpread { + loop_var: value.loop_var.clone(), + action_name: value.action.action_name.clone(), + module_name: value.action.module_name.clone(), + kwarg_names: kwarg_names.clone(), + }); + frames.push(ResolveFrame::Resolve(&value.collection)); + for name in kwarg_names.iter().rev() { + if let Some(arg) = value.action.kwargs.get(name) { + frames.push(ResolveFrame::Resolve(arg)); + } + } + } + }, + ResolveFrame::BuildBinary(op) => { + let right = values + .pop() + .expect("binary resolver frame missing right operand"); + let left = values + .pop() + .expect("binary resolver frame missing left operand"); + values.push(ValueExpr::BinaryOp(BinaryOpValue { + left: Box::new(left), + op, + right: Box::new(right), + })); + } + ResolveFrame::BuildUnary(op) => { + let operand = values.pop().expect("unary resolver frame missing operand"); + values.push(ValueExpr::UnaryOp(UnaryOpValue { + op, + operand: Box::new(operand), + })); + } + ResolveFrame::BuildList(len) => { + let mut elements = Vec::with_capacity(len); + for _ in 0..len { + elements.push(values.pop().expect("list resolver frame missing element")); + } + elements.reverse(); + values.push(ValueExpr::List(ListValue { elements })); + } + ResolveFrame::BuildDict(len) => { + let mut entries = Vec::with_capacity(len); + for _ in 0..len { + let value = values.pop().expect("dict resolver frame missing value"); + let key = values.pop().expect("dict resolver frame missing key"); + entries.push(DictEntryValue { key, value }); + } + entries.reverse(); + values.push(ValueExpr::Dict(DictValue { entries })); + } + ResolveFrame::BuildIndex => { + let index = values + .pop() + .expect("index resolver frame missing index expression"); + let object = values + .pop() + .expect("index resolver frame missing object expression"); + values.push(ValueExpr::Index(IndexValue { + object: Box::new(object), + index: Box::new(index), + })); + } + ResolveFrame::BuildDot(attribute) => { + let object = values + .pop() + .expect("dot resolver frame missing object expression"); + values.push(ValueExpr::Dot(DotValue { + object: Box::new(object), + attribute, + })); + } + ResolveFrame::BuildFunctionCall { + name, + global_function, + args_len, + kwarg_names, + } => { + let mut kwargs = HashMap::with_capacity(kwarg_names.len()); + for key in kwarg_names.iter().rev() { + let value = values + .pop() + .expect("function-call resolver frame missing kwarg value"); + kwargs.insert(key.clone(), value); + } + let mut args = Vec::with_capacity(args_len); + for _ in 0..args_len { + args.push( + values + .pop() + .expect("function-call resolver frame missing arg value"), + ); + } + args.reverse(); + values.push(ValueExpr::FunctionCall(FunctionCallValue { + name, + args, + kwargs, + global_function, + })); + } + ResolveFrame::BuildSpread { + loop_var, + action_name, + module_name, + kwarg_names, + } => { + let collection = values + .pop() + .expect("spread resolver frame missing collection expression"); + let mut kwargs = HashMap::with_capacity(kwarg_names.len()); + for key in kwarg_names.iter().rev() { + let value = values + .pop() + .expect("spread resolver frame missing kwarg value"); + kwargs.insert(key.clone(), value); + } + let action = ActionCallSpec { + action_name, + module_name, + kwargs, + }; + values.push(ValueExpr::Spread(SpreadValue { + collection: Box::new(collection), + loop_var, + action, + })); + } } } + + values.pop().expect("resolver stack produced no result") } } @@ -130,57 +286,63 @@ impl<'a> ValueExprSourceCollector<'a> { } pub fn visit(&self, expr: &ValueExpr) -> HashSet { - match expr { - ValueExpr::Literal(_) => HashSet::new(), - ValueExpr::Variable(value) => { - (self.resolve_variable)(&value.name).into_iter().collect() - } - ValueExpr::ActionResult(value) => [value.node_id].into_iter().collect(), - ValueExpr::BinaryOp(value) => { - let mut sources = self.visit(&value.left); - sources.extend(self.visit(&value.right)); - sources - } - ValueExpr::UnaryOp(value) => self.visit(&value.operand), - ValueExpr::List(value) => { - let mut sources = HashSet::new(); - for item in &value.elements { - sources.extend(self.visit(item)); + let mut sources = HashSet::new(); + let mut pending: Vec<&ValueExpr> = vec![expr]; + + while let Some(current) = pending.pop() { + match current { + ValueExpr::Literal(_) => {} + ValueExpr::Variable(value) => { + if let Some(source) = (self.resolve_variable)(&value.name) { + sources.insert(source); + } } - sources - } - ValueExpr::Dict(value) => { - let mut sources = HashSet::new(); - for entry in &value.entries { - sources.extend(self.visit(&entry.key)); - sources.extend(self.visit(&entry.value)); + ValueExpr::ActionResult(value) => { + sources.insert(value.node_id); } - sources - } - ValueExpr::Index(value) => { - let mut sources = self.visit(&value.object); - sources.extend(self.visit(&value.index)); - sources - } - ValueExpr::Dot(value) => self.visit(&value.object), - ValueExpr::FunctionCall(value) => { - let mut sources = HashSet::new(); - for arg in &value.args { - sources.extend(self.visit(arg)); + ValueExpr::BinaryOp(value) => { + pending.push(&value.right); + pending.push(&value.left); } - for arg in value.kwargs.values() { - sources.extend(self.visit(arg)); + ValueExpr::UnaryOp(value) => { + pending.push(&value.operand); } - sources - } - ValueExpr::Spread(value) => { - let mut sources = self.visit(&value.collection); - for arg in value.action.kwargs.values() { - sources.extend(self.visit(arg)); + ValueExpr::List(value) => { + for item in value.elements.iter().rev() { + pending.push(item); + } + } + ValueExpr::Dict(value) => { + for entry in value.entries.iter().rev() { + pending.push(&entry.value); + pending.push(&entry.key); + } + } + ValueExpr::Index(value) => { + pending.push(&value.index); + pending.push(&value.object); + } + ValueExpr::Dot(value) => { + pending.push(&value.object); + } + ValueExpr::FunctionCall(value) => { + for arg in value.kwargs.values() { + pending.push(arg); + } + for arg in value.args.iter().rev() { + pending.push(arg); + } + } + ValueExpr::Spread(value) => { + for arg in value.action.kwargs.values() { + pending.push(arg); + } + pending.push(&value.collection); } - sources } } + + sources } } diff --git a/crates/lib/runner-state/src/value_visitor/tests.rs b/crates/lib/runner-state/src/value_visitor/tests.rs index d877ca1c..7cba94a2 100644 --- a/crates/lib/runner-state/src/value_visitor/tests.rs +++ b/crates/lib/runner-state/src/value_visitor/tests.rs @@ -41,6 +41,30 @@ fn test_value_expr_resolver_visit_happy_path() { } } +#[test] +fn test_value_expr_resolver_handles_deep_unary_chain() { + let mut seen = HashSet::new(); + let resolve = |name: &str, _: &mut HashSet| { + ValueExpr::Variable(VariableValue { + name: name.to_string(), + }) + }; + let mut resolver = ValueExprResolver::new(&resolve, &mut seen); + + let mut expr = ValueExpr::Literal(LiteralValue { + value: Value::Bool(true), + }); + for _ in 0..10_000 { + expr = ValueExpr::UnaryOp(UnaryOpValue { + op: ir::UnaryOperator::UnaryOpNot as i32, + operand: Box::new(expr), + }); + } + + let resolved = resolver.visit(&expr); + assert!(matches!(resolved, ValueExpr::UnaryOp(_))); +} + #[test] fn test_value_expr_source_collector_visit_happy_path() { let variable_source = Uuid::new_v4(); @@ -71,6 +95,29 @@ fn test_value_expr_source_collector_visit_happy_path() { assert!(sources.contains(&action_source)); } +#[test] +fn test_value_expr_source_collector_handles_deep_unary_chain() { + let source = Uuid::new_v4(); + let resolve = |name: &str| { + if name == "x" { Some(source) } else { None } + }; + let collector = ValueExprSourceCollector::new(&resolve); + + let mut expr = ValueExpr::Variable(VariableValue { + name: "x".to_string(), + }); + for _ in 0..10_000 { + expr = ValueExpr::UnaryOp(UnaryOpValue { + op: ir::UnaryOperator::UnaryOpNot as i32, + operand: Box::new(expr), + }); + } + + let sources = collector.visit(&expr); + assert_eq!(sources.len(), 1); + assert!(sources.contains(&source)); +} + #[test] fn test_value_expr_evaluator_visit_happy_path() { let resolve_variable = |name: &str| -> Result {