From 8eb2c36c7b64db86d552a81a6d6596eba0f3ac67 Mon Sep 17 00:00:00 2001 From: MOZGIII Date: Thu, 23 Apr 2026 04:14:22 +0400 Subject: [PATCH 1/6] Add waymark_runner_register_node_graph_nodes_len metric --- Cargo.lock | 2 ++ crates/lib/runner-state/Cargo.toml | 2 ++ crates/lib/runner-state/src/state.rs | 3 +++ 3 files changed, 7 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 52dc64cd9..fa1679836 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4636,11 +4636,13 @@ name = "waymark-runner-state" version = "0.1.0" dependencies = [ "chrono", + "metrics", "serde_json", "thiserror", "waymark-dag", "waymark-ids", "waymark-ir-conversions", + "waymark-metrics-util", "waymark-proto", "waymark-runner-execution-core", "waymark-runner-expr", diff --git a/crates/lib/runner-state/Cargo.toml b/crates/lib/runner-state/Cargo.toml index b465c29b5..46c89b496 100644 --- a/crates/lib/runner-state/Cargo.toml +++ b/crates/lib/runner-state/Cargo.toml @@ -8,11 +8,13 @@ edition = "2024" waymark-dag = { workspace = true } waymark-ids = { workspace = true } waymark-ir-conversions = { workspace = true } +waymark-metrics-util = { workspace = true } waymark-proto = { workspace = true } waymark-runner-execution-core = { workspace = true } waymark-runner-expr = { workspace = true } chrono = { workspace = true, features = ["serde", "clock"] } +metrics = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/crates/lib/runner-state/src/state.rs b/crates/lib/runner-state/src/state.rs index a55388ec5..30bb1bc17 100644 --- a/crates/lib/runner-state/src/state.rs +++ b/crates/lib/runner-state/src/state.rs @@ -451,6 +451,9 @@ impl RunnerState { self.graph.nodes.len(), )); } + metrics::histogram!("waymark_runner_register_node_graph_nodes_len") + .record(waymark_metrics_util::Val(self.graph.nodes.len())); + self.graph.nodes.insert(node.node_id, node.clone()); self.ready_queue.push(node.node_id); if node.is_action_call() { From ce5d30eaf0eaf0a165788104a788c81d7a83656f Mon Sep 17 00:00:00 2001 From: MOZGIII Date: Mon, 13 Apr 2026 18:57:06 +0400 Subject: [PATCH 2/6] Add runner-eval-core --- Cargo.lock | 11 + crates/lib/runner-eval-core/Cargo.toml | 13 + crates/lib/runner-eval-core/src/error.rs | 71 ++++ crates/lib/runner-eval-core/src/fold.rs | 77 +++++ crates/lib/runner-eval-core/src/lib.rs | 398 +++++++++++++++++++++++ crates/lib/runner-eval-core/src/util.rs | 12 + 6 files changed, 582 insertions(+) create mode 100644 crates/lib/runner-eval-core/Cargo.toml create mode 100644 crates/lib/runner-eval-core/src/error.rs create mode 100644 crates/lib/runner-eval-core/src/fold.rs create mode 100644 crates/lib/runner-eval-core/src/lib.rs create mode 100644 crates/lib/runner-eval-core/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index fa1679836..e172cc21d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4566,6 +4566,17 @@ dependencies = [ "waymark-synthetic-exception", ] +[[package]] +name = "waymark-runner-eval-core" +version = "0.1.0" +dependencies = [ + "serde_json", + "thiserror", + "waymark-ir-conversions", + "waymark-proto", + "waymark-runner-expr", +] + [[package]] name = "waymark-runner-execution-core" version = "0.1.0" diff --git a/crates/lib/runner-eval-core/Cargo.toml b/crates/lib/runner-eval-core/Cargo.toml new file mode 100644 index 000000000..e33afcf9d --- /dev/null +++ b/crates/lib/runner-eval-core/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "waymark-runner-eval-core" +edition = "2024" +version.workspace = true +publish.workspace = true + +[dependencies] +waymark-ir-conversions = { workspace = true } +waymark-proto = { workspace = true } +waymark-runner-expr = { workspace = true } + +serde_json = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/lib/runner-eval-core/src/error.rs b/crates/lib/runner-eval-core/src/error.rs new file mode 100644 index 000000000..40b22d98b --- /dev/null +++ b/crates/lib/runner-eval-core/src/error.rs @@ -0,0 +1,71 @@ +#[derive(Debug, thiserror::Error)] +pub enum BinaryOpError { + #[error("missing left")] + LeftMissing, + + #[error("missing right")] + RightMissing, +} + +#[derive(Debug, thiserror::Error)] +pub enum UnaryOpError { + #[error("missing operand")] + Missing, +} + +#[derive(Debug, thiserror::Error)] +pub enum DictEntryError { + #[error("missing key")] + MissingKey, + + #[error("missing value")] + MissingValue, +} + +#[derive(Debug, thiserror::Error)] +pub enum IndexAccessError { + #[error("missing object")] + MissingObject, + + #[error("missing index")] + MissingIndex, +} + +#[derive(Debug, thiserror::Error)] +pub enum DotAccessError { + #[error("missing object")] + MissingObject, +} + +#[derive(Debug, thiserror::Error)] +pub enum SpreadError { + #[error("collection missing")] + CollectionMissing, + + #[error("action missing")] + ActionMissing, +} + +#[derive(Debug, thiserror::Error)] +pub enum ExprToValueError { + #[error("queue action call: {0}")] + QueueActionCall(#[source] QueueActionCall), + + #[error("binary op: {0}")] + BinaryOp(#[from] BinaryOpError), + + #[error("unary op: {0}")] + UnaryOp(#[from] UnaryOpError), + + #[error("dict entry: {0}")] + DictEntry(#[from] DictEntryError), + + #[error("index access: {0}")] + IndexAccess(#[from] IndexAccessError), + + #[error("dot access: {0}")] + DotAccess(#[from] DotAccessError), + + #[error("spread: {0}")] + Spread(#[from] SpreadError), +} diff --git a/crates/lib/runner-eval-core/src/fold.rs b/crates/lib/runner-eval-core/src/fold.rs new file mode 100644 index 000000000..ab4eaf02e --- /dev/null +++ b/crates/lib/runner-eval-core/src/fold.rs @@ -0,0 +1,77 @@ +use waymark_proto::ast as ir; + +use crate::util::is_truthy; + +/// Try to fold a literal binary operation to a concrete value. +/// +/// Example: +/// - (1, 2, BINARY_OP_ADD) -> 3 +pub fn literal_binary( + op: i32, + left: &serde_json::Value, + right: &serde_json::Value, +) -> Option { + match ir::BinaryOperator::try_from(op).ok() { + Some(ir::BinaryOperator::BinaryOpAdd) => { + if let (Some(left), Some(right)) = (left.as_i64(), right.as_i64()) { + return Some(serde_json::Value::Number((left + right).into())); + } + if let (Some(left), Some(right)) = (left.as_f64(), right.as_f64()) { + return serde_json::Number::from_f64(left + right).map(serde_json::Value::Number); + } + if let (Some(left), Some(right)) = (left.as_str(), right.as_str()) { + return Some(serde_json::Value::String(format!("{left}{right}"))); + } + None + } + Some(ir::BinaryOperator::BinaryOpSub) => { + if let (Some(left), Some(right)) = (left.as_f64(), right.as_f64()) { + return serde_json::Number::from_f64(left - right).map(serde_json::Value::Number); + } + None + } + Some(ir::BinaryOperator::BinaryOpMul) => { + if let (Some(left), Some(right)) = (left.as_f64(), right.as_f64()) { + return serde_json::Number::from_f64(left * right).map(serde_json::Value::Number); + } + None + } + Some(ir::BinaryOperator::BinaryOpDiv) => { + if let (Some(left), Some(right)) = (left.as_f64(), right.as_f64()) { + return serde_json::Number::from_f64(left / right).map(serde_json::Value::Number); + } + None + } + Some(ir::BinaryOperator::BinaryOpFloorDiv) => { + if let (Some(left), Some(right)) = (left.as_f64(), right.as_f64()) { + if right == 0.0 { + return None; + } + let value = (left / right).floor(); + return serde_json::Number::from_f64(value).map(serde_json::Value::Number); + } + None + } + Some(ir::BinaryOperator::BinaryOpMod) => { + if let (Some(left), Some(right)) = (left.as_f64(), right.as_f64()) { + return serde_json::Number::from_f64(left % right).map(serde_json::Value::Number); + } + None + } + _ => None, + } +} + +/// Try to fold a literal unary operation to a concrete value. +/// +/// Example: +/// - (UNARY_OP_NEG, 4) -> -4 +pub fn literal_unary(op: i32, operand: &serde_json::Value) -> Option { + match ir::UnaryOperator::try_from(op).ok() { + Some(ir::UnaryOperator::UnaryOpNeg) => operand + .as_f64() + .and_then(|value| serde_json::Number::from_f64(-value).map(serde_json::Value::Number)), + Some(ir::UnaryOperator::UnaryOpNot) => Some(serde_json::Value::Bool(!is_truthy(operand))), + _ => None, + } +} diff --git a/crates/lib/runner-eval-core/src/lib.rs b/crates/lib/runner-eval-core/src/lib.rs new file mode 100644 index 000000000..e4a122dbb --- /dev/null +++ b/crates/lib/runner-eval-core/src/lib.rs @@ -0,0 +1,398 @@ +//! Core expression evaluator. + +mod error; +mod fold; +mod util; + +pub use self::error::*; + +use std::collections::HashMap; + +use waymark_runner_expr::*; + +use waymark_ir_conversions::literal_to_json_value; +use waymark_proto::ast as ir; + +#[derive(Debug)] +pub struct CoreEvaluator(pub SideEffectApplicator); + +#[derive(Debug)] +pub struct ActionCallParams { + pub action: ActionCallSpec, + pub targets: Option>, +} + +pub trait SideEffectApplicator { + type ActionCallError; + type NodeId: Clone; + + fn action_call( + &mut self, + iteration_index: Option, + params: ActionCallParams, + ) -> Result, Self::ActionCallError>; +} + +type ValueExprFor = + ValueExpr<::NodeId>; + +impl CoreEvaluator +where + SideEffectApplicator: self::SideEffectApplicator, +{ + /// Convert an IR expression into a symbolic ValueExpr tree. + /// + /// Use this when interpreting IR statements or DAG templates into the + /// runtime state; it queues actions and spreads as needed. + /// + /// Example IR: + /// - total = base + 1 + /// Produces BinaryOpValue(VariableValue("base"), LiteralValue(1)). + pub fn expr_to_value( + &mut self, + expr: &ir::Expr, + local_scope: Option<&HashMap>>, + ) -> Result< + ValueExprFor, + ExprToValueError, + > { + match expr.kind.as_ref() { + Some(ir::expr::Kind::Literal(lit)) => Ok(ValueExpr::Literal(LiteralValue { + value: literal_to_json_value(lit), + })), + Some(ir::expr::Kind::Variable(var)) => { + if let Some(scope) = local_scope + && let Some(value) = scope.get(&var.name) + { + return Ok(value.clone()); + } + Ok(ValueExpr::Variable(VariableValue { + name: var.name.clone(), + })) + } + Some(ir::expr::Kind::BinaryOp(op)) => { + let left = op.left.as_ref().ok_or(BinaryOpError::LeftMissing)?; + let right = op.right.as_ref().ok_or(BinaryOpError::RightMissing)?; + let left_value = self.expr_to_value(left, local_scope)?; + let right_value = self.expr_to_value(right, local_scope)?; + Ok(self.binary_op_value(op.op, left_value, right_value)) + } + Some(ir::expr::Kind::UnaryOp(op)) => { + let operand = op.operand.as_ref().ok_or(UnaryOpError::Missing)?; + let operand_value = self.expr_to_value(operand, local_scope)?; + Ok(self.unary_op_value(op.op, operand_value)) + } + Some(ir::expr::Kind::List(list)) => { + let elements = list + .elements + .iter() + .map(|item| self.expr_to_value(item, local_scope)) + .collect::, _>>()?; + Ok(ValueExpr::List(ListValue { elements })) + } + Some(ir::expr::Kind::Dict(dict_expr)) => { + let mut entries = Vec::new(); + for entry in &dict_expr.entries { + let key_expr = entry.key.as_ref().ok_or(DictEntryError::MissingKey)?; + let value_expr = entry.value.as_ref().ok_or(DictEntryError::MissingValue)?; + entries.push(DictEntryValue { + key: self.expr_to_value(key_expr, local_scope)?, + value: self.expr_to_value(value_expr, local_scope)?, + }); + } + Ok(ValueExpr::Dict(DictValue { entries })) + } + Some(ir::expr::Kind::Index(index)) => { + let object = index + .object + .as_ref() + .ok_or(IndexAccessError::MissingObject)?; + let index_expr = index.index.as_ref().ok_or(IndexAccessError::MissingIndex)?; + let object_value = self.expr_to_value(object, local_scope)?; + let index_value = self.expr_to_value(index_expr, local_scope)?; + Ok(self.index_value(object_value, index_value)) + } + Some(ir::expr::Kind::Dot(dot)) => { + let object = dot.object.as_ref().ok_or(DotAccessError::MissingObject)?; + Ok(ValueExpr::Dot(DotValue { + object: Box::new(self.expr_to_value(object, local_scope)?), + attribute: dot.attribute.clone(), + })) + } + Some(ir::expr::Kind::FunctionCall(call)) => { + let args = call + .args + .iter() + .map(|arg| self.expr_to_value(arg, local_scope)) + .collect::, _>>()?; + let mut kwargs = HashMap::new(); + for kw in &call.kwargs { + if let Some(value) = &kw.value { + kwargs.insert(kw.name.clone(), self.expr_to_value(value, local_scope)?); + } + } + let global_fn = if call.global_function != 0 { + Some(call.global_function) + } else { + None + }; + Ok(ValueExpr::FunctionCall(FunctionCallValue { + name: call.name.clone(), + args, + kwargs, + global_function: global_fn, + })) + } + Some(ir::expr::Kind::ActionCall(action)) => { + let result = self.action_call(action, None, None, local_scope)?; + Ok(ValueExpr::ActionResult(result)) + } + Some(ir::expr::Kind::ParallelExpr(parallel)) => { + let mut calls = Vec::new(); + for call in ¶llel.calls { + calls.push(self.call_to_value(call, local_scope)?); + } + Ok(ValueExpr::List(ListValue { elements: calls })) + } + Some(ir::expr::Kind::SpreadExpr(spread)) => self.spread_expr_value(spread, local_scope), + None => Ok(ValueExpr::Literal(LiteralValue { + value: serde_json::Value::Null, + })), + } + } + + /// Convert an IR call (action/function) into a ValueExpr. + /// + /// Use this for parallel expressions that contain mixed call types. + /// + /// Example IR: + /// - parallel { @double(x), helper(x) } + /// Action calls become ActionResultValue nodes; function calls become + /// FunctionCallValue expressions. + fn call_to_value( + &mut self, + call: &ir::Call, + local_scope: Option<&HashMap>>, + ) -> Result< + ValueExprFor, + ExprToValueError, + > { + match call.kind.as_ref() { + Some(ir::call::Kind::Action(action)) => Ok(ValueExpr::ActionResult(self.action_call( + action, + None, + None, + local_scope, + )?)), + Some(ir::call::Kind::Function(function)) => self.expr_to_value( + &ir::Expr { + kind: Some(ir::expr::Kind::FunctionCall(function.clone())), + span: None, + }, + local_scope, + ), + None => Ok(ValueExpr::Literal(LiteralValue { + value: serde_json::Value::Null, + })), + } + } + + /// Materialize a spread expression into concrete calls or a symbolic spread. + /// + /// Use this when converting IR spreads so known list collections unroll to + /// explicit action calls, while unknown collections stay symbolic. + /// + /// Example IR: + /// - spread [1, 2]:item -> @double(value=item) + /// Produces a ListValue of ActionResultValue entries for each item. + fn spread_expr_value( + &mut self, + spread: &ir::SpreadExpr, + local_scope: Option<&HashMap>>, + ) -> Result< + ValueExprFor, + ExprToValueError, + > { + let collection = self.expr_to_value( + spread + .collection + .as_ref() + .ok_or(SpreadError::CollectionMissing)?, + local_scope, + )?; + if let ValueExpr::List(list) = &collection { + let mut results = Vec::new(); + for (idx, item) in list.elements.iter().enumerate() { + let mut scope = HashMap::new(); + scope.insert(spread.loop_var.clone(), item.clone()); + let result = self.action_call( + spread.action.as_ref().ok_or(SpreadError::ActionMissing)?, + None, + Some(idx as i32), + Some(&scope), + )?; + results.push(ValueExpr::ActionResult(result)); + } + return Ok(ValueExpr::List(ListValue { elements: results })); + } + + let action_spec = self.action_spec_from_ir( + spread.action.as_ref().ok_or(SpreadError::ActionMissing)?, + None, + )?; + Ok(ValueExpr::Spread(SpreadValue { + collection: Box::new(collection), + loop_var: spread.loop_var.clone(), + action: action_spec, + })) + } + + /// Build a binary-op value with simple constant folding. + /// + /// Use this when converting IR so literals and list concatenations are + /// simplified early. + /// + /// Example IR: + /// - total = 1 + 2 + /// Produces LiteralValue(3) instead of a BinaryOpValue. + fn binary_op_value( + &self, + op: i32, + left: ValueExprFor, + right: ValueExprFor, + ) -> ValueExprFor { + if ir::BinaryOperator::try_from(op).ok() == Some(ir::BinaryOperator::BinaryOpAdd) + && let (ValueExpr::List(left_list), ValueExpr::List(right_list)) = (&left, &right) + { + let mut elements = left_list.elements.clone(); + elements.extend(right_list.elements.clone()); + return ValueExpr::List(ListValue { elements }); + } + if let (ValueExpr::Literal(left_val), ValueExpr::Literal(right_val)) = (&left, &right) + && let Some(folded) = fold::literal_binary(op, &left_val.value, &right_val.value) + { + return ValueExpr::Literal(LiteralValue { value: folded }); + } + ValueExpr::BinaryOp(BinaryOpValue { + left: Box::new(left), + op, + right: Box::new(right), + }) + } + + /// Build a unary-op value with constant folding for literals. + /// + /// Example IR: + /// - neg = -1 + /// Produces LiteralValue(-1) instead of UnaryOpValue. + fn unary_op_value( + &self, + op: i32, + operand: ValueExprFor, + ) -> ValueExprFor { + if let ValueExpr::Literal(lit) = &operand + && let Some(folded) = fold::literal_unary(op, &lit.value) + { + return ValueExpr::Literal(LiteralValue { value: folded }); + } + ValueExpr::UnaryOp(UnaryOpValue { + op, + operand: Box::new(operand), + }) + } + + /// Build an index value, folding list literals when possible. + /// + /// Example IR: + /// - first = [10, 20][0] + /// Produces LiteralValue(10) when the list is fully literal. + fn index_value( + &self, + object: ValueExprFor, + index: ValueExprFor, + ) -> ValueExprFor { + if let (ValueExpr::List(list), ValueExpr::Literal(idx)) = (&object, &index) + && let Some(idx) = idx.value.as_i64() + && idx >= 0 + && (idx as usize) < list.elements.len() + { + return list.elements[idx as usize].clone(); + } + ValueExpr::Index(IndexValue { + object: Box::new(object), + index: Box::new(index), + }) + } + + pub fn resolve_kwargs<'a>( + &mut self, + kwarg_exprs: impl IntoIterator, + local_scope: Option<&HashMap>>, + ) -> Result< + HashMap>, + ExprToValueError, + > { + let mut kwargs = HashMap::new(); + for (name, expr) in kwarg_exprs { + kwargs.insert(name, self.expr_to_value(expr, local_scope)?); + } + Ok(kwargs) + } + + /// Extract an action call spec from IR, applying local scope bindings. + /// + /// Example IR: + /// - @double(value=item) with local_scope["item"]=LiteralValue(2) + /// Produces kwargs {"value": LiteralValue(2)}. + pub fn action_spec_from_ir( + &mut self, + action: &ir::ActionCall, + local_scope: Option<&HashMap>>, + ) -> Result< + ActionCallSpec, + ExprToValueError, + > { + let kwargs = action + .kwargs + .iter() + .filter_map(|kw| kw.value.as_ref().map(|value| (kw.name.clone(), value))); + Ok(ActionCallSpec { + action_name: action.action_name.clone(), + module_name: action.module_name.clone(), + kwargs: self.resolve_kwargs(kwargs, local_scope)?, + }) + } + + /// Queue an action call from IR, respecting a local scope for loop vars. + /// + /// Use this during IR -> runner-state conversion (including spreads) so + /// action arguments are converted to symbolic expressions. + /// + /// Example IR: + /// - @double(value=item) + /// With local_scope={"item": LiteralValue(2)}, the queued action uses a + /// literal argument and links data-flow to the literal's source nodes. + pub fn action_call( + &mut self, + action: &ir::ActionCall, + targets: Option>, + iteration_index: Option, + local_scope: Option<&HashMap>>, + ) -> Result< + ActionResultValue, + ExprToValueError, + > { + let spec = self.action_spec_from_ir(action, local_scope)?; + let result = self + .0 + .action_call( + iteration_index, + ActionCallParams { + targets: targets.clone(), + action: spec.clone(), + }, + ) + .map_err(ExprToValueError::QueueActionCall)?; + Ok(result) + } +} diff --git a/crates/lib/runner-eval-core/src/util.rs b/crates/lib/runner-eval-core/src/util.rs new file mode 100644 index 000000000..20768070c --- /dev/null +++ b/crates/lib/runner-eval-core/src/util.rs @@ -0,0 +1,12 @@ +pub(crate) fn is_truthy(value: &serde_json::Value) -> bool { + match value { + serde_json::Value::Null => false, + serde_json::Value::Bool(value) => *value, + serde_json::Value::Number(number) => { + number.as_f64().map(|value| value != 0.0).unwrap_or(false) + } + serde_json::Value::String(value) => !value.is_empty(), + serde_json::Value::Array(values) => !values.is_empty(), + serde_json::Value::Object(map) => !map.is_empty(), + } +} From a6310cd8a55d0cdff6eac82f3d96868f87812df9 Mon Sep 17 00:00:00 2001 From: MOZGIII Date: Mon, 13 Apr 2026 22:25:56 +0400 Subject: [PATCH 3/6] Switch the core ir evaluation to runner-eval-core --- Cargo.lock | 2 +- Cargo.toml | 1 + crates/lib/runner-state/Cargo.toml | 2 +- crates/lib/runner-state/src/eval.rs | 27 ++ crates/lib/runner-state/src/lib.rs | 2 +- crates/lib/runner-state/src/state.rs | 416 ++------------------------- crates/lib/runner-state/src/util.rs | 12 - crates/lib/runner/src/executor.rs | 22 +- 8 files changed, 60 insertions(+), 424 deletions(-) create mode 100644 crates/lib/runner-state/src/eval.rs delete mode 100644 crates/lib/runner-state/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index e172cc21d..6548cae46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4652,9 +4652,9 @@ dependencies = [ "thiserror", "waymark-dag", "waymark-ids", - "waymark-ir-conversions", "waymark-metrics-util", "waymark-proto", + "waymark-runner-eval-core", "waymark-runner-execution-core", "waymark-runner-expr", ] diff --git a/Cargo.toml b/Cargo.toml index 1ac8c448d..3dba23966 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ waymark-prometheus-exporter-bringup = { path = "crates/lib/prometheus-exporter-b waymark-proto = { path = "crates/lib/proto" } waymark-runloop = { path = "crates/lib/runloop" } waymark-runner = { path = "crates/lib/runner" } +waymark-runner-eval-core = { path = "crates/lib/runner-eval-core" } waymark-runner-execution-core = { path = "crates/lib/runner-execution-core" } waymark-runner-executor-core = { path = "crates/lib/runner-executor-core" } waymark-runner-expr = { path = "crates/lib/runner-expr" } diff --git a/crates/lib/runner-state/Cargo.toml b/crates/lib/runner-state/Cargo.toml index 46c89b496..76164006d 100644 --- a/crates/lib/runner-state/Cargo.toml +++ b/crates/lib/runner-state/Cargo.toml @@ -7,9 +7,9 @@ edition = "2024" [dependencies] waymark-dag = { workspace = true } waymark-ids = { workspace = true } -waymark-ir-conversions = { workspace = true } waymark-metrics-util = { workspace = true } waymark-proto = { workspace = true } +waymark-runner-eval-core = { workspace = true } waymark-runner-execution-core = { workspace = true } waymark-runner-expr = { workspace = true } diff --git a/crates/lib/runner-state/src/eval.rs b/crates/lib/runner-state/src/eval.rs new file mode 100644 index 000000000..61bbaf35c --- /dev/null +++ b/crates/lib/runner-state/src/eval.rs @@ -0,0 +1,27 @@ +use crate::RunnerStateError; + +impl crate::RunnerState { + pub fn as_core_eval(&mut self) -> waymark_runner_eval_core::CoreEvaluator<&mut Self> { + waymark_runner_eval_core::CoreEvaluator(self) + } +} + +impl From> for RunnerStateError { + fn from(value: waymark_runner_eval_core::ExprToValueError) -> Self { + Self(value.to_string()) + } +} + +impl waymark_runner_eval_core::SideEffectApplicator for &mut crate::RunnerState { + type ActionCallError = RunnerStateError; + type NodeId = waymark_ids::ExecutionId; + + fn action_call( + &mut self, + iteration_index: Option, + params: waymark_runner_eval_core::ActionCallParams, + ) -> Result, Self::ActionCallError> { + let waymark_runner_eval_core::ActionCallParams { action, targets } = params; + self.queue_action_spec(action, targets, iteration_index) + } +} diff --git a/crates/lib/runner-state/src/lib.rs b/crates/lib/runner-state/src/lib.rs index a9f234d4e..a9652880d 100644 --- a/crates/lib/runner-state/src/lib.rs +++ b/crates/lib/runner-state/src/lib.rs @@ -1,9 +1,9 @@ mod collect_value_sources; mod error; +mod eval; mod max_nodes; mod resolve_value_tree; mod state; -mod util; mod value; pub use self::error::*; diff --git a/crates/lib/runner-state/src/state.rs b/crates/lib/runner-state/src/state.rs index 30bb1bc17..e3a21a7d5 100644 --- a/crates/lib/runner-state/src/state.rs +++ b/crates/lib/runner-state/src/state.rs @@ -11,7 +11,6 @@ use waymark_runner_execution_core::{ }; use crate::max_nodes::MAX_RUNNER_STATE_NODES; -use crate::util::is_truthy; use crate::value::*; use crate::{ ActionCallSpec, ActionResultValue, RunnerStateError, ValueExpr, collect_value_sources, @@ -21,7 +20,7 @@ use waymark_dag::{ ActionCallNode, AggregatorNode, AssignmentNode, DAG, DAGNode, EdgeType, FnCallNode, JoinNode, ReturnNode, SleepNode, }; -use waymark_ir_conversions::literal_to_json_value; + use waymark_proto::ast as ir; #[derive(Clone, Debug, Default)] @@ -189,7 +188,7 @@ impl RunnerState { template_id: Some(template_id.to_string()), targets: self.node_targets(&template), action: if let DAGNode::ActionCall(action_node) = &template { - Some(self.action_spec_from_node(action_node)) + Some(self.action_spec_from_node(action_node)?) } else { None }, @@ -274,7 +273,9 @@ impl RunnerState { iteration_index: Option, local_scope: Option<&HashMap>, ) -> Result { - let spec = self.action_spec_from_ir(action, local_scope); + let spec = self + .as_core_eval() + .action_spec_from_ir(action, local_scope)?; let node = self.queue_node( ExecutionNodeType::ActionCall.as_str(), &format!("@{}()", spec.action_name), @@ -671,7 +672,7 @@ impl RunnerState { assign_expr: Some(expr), .. }) => { - let value_expr = self.expr_to_value(expr, None)?; + let value_expr = self.as_core_eval().expr_to_value(expr, None)?; if let Some(node_mut) = self.graph.nodes.get_mut(&exec_node.node_id) { node_mut.value_expr = Some(value_expr.clone()); } @@ -719,7 +720,7 @@ impl RunnerState { duration_expr: Some(expr), .. }) => { - let value_expr = self.expr_to_value(expr, None)?; + let value_expr = self.as_core_eval().expr_to_value(expr, None)?; if let Some(node_mut) = self.graph.nodes.get_mut(&exec_node.node_id) { node_mut.value_expr = Some(value_expr.clone()); } @@ -730,7 +731,7 @@ impl RunnerState { assign_expr: Some(expr), .. }) => { - let value_expr = self.expr_to_value(expr, None)?; + let value_expr = self.as_core_eval().expr_to_value(expr, None)?; if let Some(node_mut) = self.graph.nodes.get_mut(&exec_node.node_id) { node_mut.value_expr = Some(value_expr.clone()); } @@ -748,7 +749,7 @@ impl RunnerState { target, .. }) => { - let value_expr = self.expr_to_value(expr, None)?; + let value_expr = self.as_core_eval().expr_to_value(expr, None)?; if let Some(node_mut) = self.graph.nodes.get_mut(&exec_node.node_id) { node_mut.value_expr = Some(value_expr.clone()); } @@ -1031,292 +1032,6 @@ impl RunnerState { } } - /// Convert an IR expression into a symbolic ValueExpr tree. - /// - /// Use this when interpreting IR statements or DAG templates into the - /// runtime state; it queues actions and spreads as needed. - /// - /// Example IR: - /// - total = base + 1 - /// Produces BinaryOpValue(VariableValue("base"), LiteralValue(1)). - pub fn expr_to_value( - &mut self, - expr: &ir::Expr, - local_scope: Option<&HashMap>, - ) -> Result { - match expr.kind.as_ref() { - Some(ir::expr::Kind::Literal(lit)) => Ok(ValueExpr::Literal(LiteralValue { - value: literal_to_json_value(lit), - })), - Some(ir::expr::Kind::Variable(var)) => { - if let Some(scope) = local_scope - && let Some(value) = scope.get(&var.name) - { - return Ok(value.clone()); - } - Ok(ValueExpr::Variable(VariableValue { - name: var.name.clone(), - })) - } - Some(ir::expr::Kind::BinaryOp(op)) => { - let left = op - .left - .as_ref() - .ok_or_else(|| RunnerStateError("binary op missing left".to_string()))?; - let right = op - .right - .as_ref() - .ok_or_else(|| RunnerStateError("binary op missing right".to_string()))?; - let left_value = self.expr_to_value(left, local_scope)?; - let right_value = self.expr_to_value(right, local_scope)?; - Ok(self.binary_op_value(op.op, left_value, right_value)) - } - Some(ir::expr::Kind::UnaryOp(op)) => { - let operand = op - .operand - .as_ref() - .ok_or_else(|| RunnerStateError("unary op missing operand".to_string()))?; - let operand_value = self.expr_to_value(operand, local_scope)?; - Ok(self.unary_op_value(op.op, operand_value)) - } - Some(ir::expr::Kind::List(list)) => { - let elements = list - .elements - .iter() - .map(|item| self.expr_to_value(item, local_scope)) - .collect::, RunnerStateError>>()?; - Ok(ValueExpr::List(ListValue { elements })) - } - Some(ir::expr::Kind::Dict(dict_expr)) => { - let mut entries = Vec::new(); - for entry in &dict_expr.entries { - let key_expr = entry - .key - .as_ref() - .ok_or_else(|| RunnerStateError("dict entry missing key".to_string()))?; - let value_expr = entry - .value - .as_ref() - .ok_or_else(|| RunnerStateError("dict entry missing value".to_string()))?; - entries.push(DictEntryValue { - key: self.expr_to_value(key_expr, local_scope)?, - value: self.expr_to_value(value_expr, local_scope)?, - }); - } - Ok(ValueExpr::Dict(DictValue { entries })) - } - Some(ir::expr::Kind::Index(index)) => { - let object = index - .object - .as_ref() - .ok_or_else(|| RunnerStateError("index access missing object".to_string()))?; - let index_expr = index - .index - .as_ref() - .ok_or_else(|| RunnerStateError("index access missing index".to_string()))?; - let object_value = self.expr_to_value(object, local_scope)?; - let index_value = self.expr_to_value(index_expr, local_scope)?; - Ok(self.index_value(object_value, index_value)) - } - Some(ir::expr::Kind::Dot(dot)) => { - let object = dot - .object - .as_ref() - .ok_or_else(|| RunnerStateError("dot access missing object".to_string()))?; - Ok(ValueExpr::Dot(DotValue { - object: Box::new(self.expr_to_value(object, local_scope)?), - attribute: dot.attribute.clone(), - })) - } - Some(ir::expr::Kind::FunctionCall(call)) => { - let args = call - .args - .iter() - .map(|arg| self.expr_to_value(arg, local_scope)) - .collect::, RunnerStateError>>()?; - let mut kwargs = HashMap::new(); - for kw in &call.kwargs { - if let Some(value) = &kw.value { - kwargs.insert(kw.name.clone(), self.expr_to_value(value, local_scope)?); - } - } - let global_fn = if call.global_function != 0 { - Some(call.global_function) - } else { - None - }; - Ok(ValueExpr::FunctionCall(FunctionCallValue { - name: call.name.clone(), - args, - kwargs, - global_function: global_fn, - })) - } - Some(ir::expr::Kind::ActionCall(action)) => { - let result = self.queue_action_call(action, None, None, local_scope)?; - Ok(ValueExpr::ActionResult(result)) - } - Some(ir::expr::Kind::ParallelExpr(parallel)) => { - let mut calls = Vec::new(); - for call in ¶llel.calls { - calls.push(self.call_to_value(call, local_scope)?); - } - Ok(ValueExpr::List(ListValue { elements: calls })) - } - Some(ir::expr::Kind::SpreadExpr(spread)) => self.spread_expr_value(spread, local_scope), - None => Ok(ValueExpr::Literal(LiteralValue { - value: serde_json::Value::Null, - })), - } - } - - /// Convert an IR call (action/function) into a ValueExpr. - /// - /// Use this for parallel expressions that contain mixed call types. - /// - /// Example IR: - /// - parallel { @double(x), helper(x) } - /// Action calls become ActionResultValue nodes; function calls become - /// FunctionCallValue expressions. - fn call_to_value( - &mut self, - call: &ir::Call, - local_scope: Option<&HashMap>, - ) -> Result { - match call.kind.as_ref() { - Some(ir::call::Kind::Action(action)) => Ok(ValueExpr::ActionResult( - self.queue_action_call(action, None, None, local_scope)?, - )), - Some(ir::call::Kind::Function(function)) => self.expr_to_value( - &ir::Expr { - kind: Some(ir::expr::Kind::FunctionCall(function.clone())), - span: None, - }, - local_scope, - ), - None => Ok(ValueExpr::Literal(LiteralValue { - value: serde_json::Value::Null, - })), - } - } - - /// Materialize a spread expression into concrete calls or a symbolic spread. - /// - /// Use this when converting IR spreads so known list collections unroll to - /// explicit action calls, while unknown collections stay symbolic. - /// - /// Example IR: - /// - spread [1, 2]:item -> @double(value=item) - /// Produces a ListValue of ActionResultValue entries for each item. - fn spread_expr_value( - &mut self, - spread: &ir::SpreadExpr, - local_scope: Option<&HashMap>, - ) -> Result { - let collection = self.expr_to_value( - spread - .collection - .as_ref() - .ok_or_else(|| RunnerStateError("spread collection missing".to_string()))?, - local_scope, - )?; - if let ValueExpr::List(list) = &collection { - let mut results = Vec::new(); - for (idx, item) in list.elements.iter().enumerate() { - let mut scope = HashMap::new(); - scope.insert(spread.loop_var.clone(), item.clone()); - let result = self.queue_action_call( - spread - .action - .as_ref() - .ok_or_else(|| RunnerStateError("spread action missing".to_string()))?, - None, - Some(idx as i32), - Some(&scope), - )?; - results.push(ValueExpr::ActionResult(result)); - } - return Ok(ValueExpr::List(ListValue { elements: results })); - } - - let action_spec = self.action_spec_from_ir( - spread - .action - .as_ref() - .ok_or_else(|| RunnerStateError("spread action missing".to_string()))?, - None, - ); - Ok(ValueExpr::Spread(SpreadValue { - collection: Box::new(collection), - loop_var: spread.loop_var.clone(), - action: action_spec, - })) - } - - /// Build a binary-op value with simple constant folding. - /// - /// Use this when converting IR so literals and list concatenations are - /// simplified early. - /// - /// Example IR: - /// - total = 1 + 2 - /// Produces LiteralValue(3) instead of a BinaryOpValue. - fn binary_op_value(&self, op: i32, left: ValueExpr, right: ValueExpr) -> ValueExpr { - if ir::BinaryOperator::try_from(op).ok() == Some(ir::BinaryOperator::BinaryOpAdd) - && let (ValueExpr::List(left_list), ValueExpr::List(right_list)) = (&left, &right) - { - let mut elements = left_list.elements.clone(); - elements.extend(right_list.elements.clone()); - return ValueExpr::List(ListValue { elements }); - } - if let (ValueExpr::Literal(left_val), ValueExpr::Literal(right_val)) = (&left, &right) - && let Some(folded) = fold_literal_binary(op, &left_val.value, &right_val.value) - { - return ValueExpr::Literal(LiteralValue { value: folded }); - } - ValueExpr::BinaryOp(BinaryOpValue { - left: Box::new(left), - op, - right: Box::new(right), - }) - } - - /// Build a unary-op value with constant folding for literals. - /// - /// Example IR: - /// - neg = -1 - /// Produces LiteralValue(-1) instead of UnaryOpValue. - fn unary_op_value(&self, op: i32, operand: ValueExpr) -> ValueExpr { - if let ValueExpr::Literal(lit) = &operand - && let Some(folded) = fold_literal_unary(op, &lit.value) - { - return ValueExpr::Literal(LiteralValue { value: folded }); - } - ValueExpr::UnaryOp(UnaryOpValue { - op, - operand: Box::new(operand), - }) - } - - /// Build an index value, folding list literals when possible. - /// - /// Example IR: - /// - first = [10, 20][0] - /// Produces LiteralValue(10) when the list is fully literal. - fn index_value(&self, object: ValueExpr, index: ValueExpr) -> ValueExpr { - if let (ValueExpr::List(list), ValueExpr::Literal(idx)) = (&object, &index) - && let Some(idx) = idx.value.as_i64() - && idx >= 0 - && (idx as usize) < list.elements.len() - { - return list.elements[idx as usize].clone(); - } - ValueExpr::Index(IndexValue { - object: Box::new(object), - index: Box::new(index), - }) - } - /// Extract an action call spec from a DAG node. /// /// Use this when queueing nodes from the DAG template. @@ -1324,40 +1039,19 @@ impl RunnerState { /// Example: /// - ActionCallNode(action_name="double", kwargs={"value": "$x"}) /// Produces ActionCallSpec(action_name="double", kwargs={"value": VariableValue("x")}). - fn action_spec_from_node(&mut self, node: &ActionCallNode) -> ActionCallSpec { + fn action_spec_from_node( + &mut self, + node: &ActionCallNode, + ) -> Result> { let kwargs = node .kwarg_exprs .iter() - .map(|(name, expr)| (name.clone(), self.expr_to_value(expr, None).unwrap())) - .collect(); - ActionCallSpec { + .map(|(name, expr)| (name.clone(), expr)); + Ok(ActionCallSpec { action_name: node.action_name.clone(), module_name: node.module_name.clone(), - kwargs, - } - } - - /// Extract an action call spec from IR, applying local scope bindings. - /// - /// Example IR: - /// - @double(value=item) with local_scope["item"]=LiteralValue(2) - /// Produces kwargs {"value": LiteralValue(2)}. - fn action_spec_from_ir( - &mut self, - action: &ir::ActionCall, - local_scope: Option<&HashMap>, - ) -> ActionCallSpec { - let kwargs = action - .kwargs - .iter() - .filter_map(|kw| kw.value.as_ref().map(|value| (kw.name.clone(), value))) - .map(|(name, value)| (name, self.expr_to_value(value, local_scope).unwrap())) - .collect(); - ActionCallSpec { - action_name: action.action_name.clone(), - module_name: action.module_name.clone(), - kwargs, - } + kwargs: self.as_core_eval().resolve_kwargs(kwargs, None)?, + }) } /// Queue an action call from raw parameters and return a symbolic result. @@ -1420,7 +1114,7 @@ impl RunnerState { node_id: Option, label: Option, ) -> Result { - let value_expr = self.expr_to_value(expr, None)?; + let value_expr = self.as_core_eval().expr_to_value(expr, None)?; self.record_assignment_value(targets, value_expr, node_id, label) } @@ -1503,80 +1197,6 @@ fn value_expr_contains_variable(expr: &ValueExpr, name: &str) -> bool { } } -/// Try to fold a literal binary operation to a concrete value. -/// -/// Example: -/// - (1, 2, BINARY_OP_ADD) -> 3 -fn fold_literal_binary( - op: i32, - left: &serde_json::Value, - right: &serde_json::Value, -) -> Option { - match ir::BinaryOperator::try_from(op).ok() { - Some(ir::BinaryOperator::BinaryOpAdd) => { - if let (Some(left), Some(right)) = (left.as_i64(), right.as_i64()) { - return Some(serde_json::Value::Number((left + right).into())); - } - if let (Some(left), Some(right)) = (left.as_f64(), right.as_f64()) { - return serde_json::Number::from_f64(left + right).map(serde_json::Value::Number); - } - if let (Some(left), Some(right)) = (left.as_str(), right.as_str()) { - return Some(serde_json::Value::String(format!("{left}{right}"))); - } - None - } - Some(ir::BinaryOperator::BinaryOpSub) => { - if let (Some(left), Some(right)) = (left.as_f64(), right.as_f64()) { - return serde_json::Number::from_f64(left - right).map(serde_json::Value::Number); - } - None - } - Some(ir::BinaryOperator::BinaryOpMul) => { - if let (Some(left), Some(right)) = (left.as_f64(), right.as_f64()) { - return serde_json::Number::from_f64(left * right).map(serde_json::Value::Number); - } - None - } - Some(ir::BinaryOperator::BinaryOpDiv) => { - if let (Some(left), Some(right)) = (left.as_f64(), right.as_f64()) { - return serde_json::Number::from_f64(left / right).map(serde_json::Value::Number); - } - None - } - Some(ir::BinaryOperator::BinaryOpFloorDiv) => { - if let (Some(left), Some(right)) = (left.as_f64(), right.as_f64()) { - if right == 0.0 { - return None; - } - let value = (left / right).floor(); - return serde_json::Number::from_f64(value).map(serde_json::Value::Number); - } - None - } - Some(ir::BinaryOperator::BinaryOpMod) => { - if let (Some(left), Some(right)) = (left.as_f64(), right.as_f64()) { - return serde_json::Number::from_f64(left % right).map(serde_json::Value::Number); - } - None - } - _ => None, - } -} - -/// Try to fold a literal unary operation to a concrete value. -/// -/// Example: -/// - (UNARY_OP_NEG, 4) -> -4 -fn fold_literal_unary(op: i32, operand: &serde_json::Value) -> Option { - match ir::UnaryOperator::try_from(op).ok() { - Some(ir::UnaryOperator::UnaryOpNeg) => operand - .as_f64() - .and_then(|value| serde_json::Number::from_f64(-value).map(serde_json::Value::Number)), - Some(ir::UnaryOperator::UnaryOpNot) => Some(serde_json::Value::Bool(!is_truthy(operand))), - _ => None, - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/lib/runner-state/src/util.rs b/crates/lib/runner-state/src/util.rs deleted file mode 100644 index 20768070c..000000000 --- a/crates/lib/runner-state/src/util.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub(crate) fn is_truthy(value: &serde_json::Value) -> bool { - match value { - serde_json::Value::Null => false, - serde_json::Value::Bool(value) => *value, - serde_json::Value::Number(number) => { - number.as_f64().map(|value| value != 0.0).unwrap_or(false) - } - serde_json::Value::String(value) => !value.is_empty(), - serde_json::Value::Array(values) => !values.is_empty(), - serde_json::Value::Object(map) => !map.is_empty(), - } -} diff --git a/crates/lib/runner/src/executor.rs b/crates/lib/runner/src/executor.rs index 98fba7885..02eca0781 100644 --- a/crates/lib/runner/src/executor.rs +++ b/crates/lib/runner/src/executor.rs @@ -918,17 +918,17 @@ impl RunnerExecutor local_scope: Option>, iteration_index: Option, ) -> Result { - let kwargs = template - .kwarg_exprs - .iter() - .map(|(name, expr)| { - let value = self - .state - .expr_to_value(expr, local_scope.as_ref()) - .map_err(|err| RunnerExecutorError(err.0))?; - Ok((name.clone(), value)) - }) - .collect::, RunnerExecutorError>>()?; + let kwargs = self + .state + .as_core_eval() + .resolve_kwargs( + template + .kwarg_exprs + .iter() + .map(|(name, expr)| (name.clone(), expr)), + local_scope.as_ref(), + ) + .map_err(|err| RunnerExecutorError(err.to_string()))?; let spec = ActionCallSpec { action_name: template.action_name.clone(), From fb9e38166c1b3bd2d802745ca70fdb6a6aec6deb Mon Sep 17 00:00:00 2001 From: MOZGIII Date: Thu, 23 Apr 2026 17:55:30 +0400 Subject: [PATCH 4/6] Add runner-pure-expr-eval --- Cargo.lock | 10 ++++ crates/lib/runner-pure-expr-eval/Cargo.toml | 12 +++++ crates/lib/runner-pure-expr-eval/src/lib.rs | 52 +++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 crates/lib/runner-pure-expr-eval/Cargo.toml create mode 100644 crates/lib/runner-pure-expr-eval/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 6548cae46..b394327a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4635,6 +4635,16 @@ dependencies = [ "waymark-runner-expr", ] +[[package]] +name = "waymark-runner-pure-expr-eval" +version = "0.1.0" +dependencies = [ + "thiserror", + "waymark-proto", + "waymark-runner-eval-core", + "waymark-runner-expr", +] + [[package]] name = "waymark-runner-retry-policy" version = "0.1.0" diff --git a/crates/lib/runner-pure-expr-eval/Cargo.toml b/crates/lib/runner-pure-expr-eval/Cargo.toml new file mode 100644 index 000000000..66a6805bb --- /dev/null +++ b/crates/lib/runner-pure-expr-eval/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "waymark-runner-pure-expr-eval" +edition = "2024" +version.workspace = true +publish.workspace = true + +[dependencies] +waymark-proto = { workspace = true } +waymark-runner-eval-core = { workspace = true } +waymark-runner-expr = { workspace = true } + +thiserror = { workspace = true } diff --git a/crates/lib/runner-pure-expr-eval/src/lib.rs b/crates/lib/runner-pure-expr-eval/src/lib.rs new file mode 100644 index 000000000..e518440f0 --- /dev/null +++ b/crates/lib/runner-pure-expr-eval/src/lib.rs @@ -0,0 +1,52 @@ +//! An expression evaluator that does not allow side effects. + +use std::collections::HashMap; + +use waymark_proto::ast as ir; + +/// The side effect applicator that does not allow side effect. +#[derive(Debug, Copy)] +pub struct PureApplicator(core::marker::PhantomData); + +impl Clone for PureApplicator { + fn clone(&self) -> Self { + Self(core::marker::PhantomData) + } +} + +impl Default for PureApplicator { + fn default() -> Self { + Self(core::marker::PhantomData) + } +} + +/// Error returned when any side effect is applied. +#[derive(Debug, thiserror::Error)] +#[error("side effects are not allowed in guard expressions")] +pub struct SideEffectsNotAllowed; + +impl waymark_runner_eval_core::SideEffectApplicator for PureApplicator { + type ActionCallError = SideEffectsNotAllowed; + type NodeId = NodeId; + + fn action_call( + &mut self, + _iteration_index: Option, + _params: waymark_runner_eval_core::ActionCallParams, + ) -> Result, Self::ActionCallError> { + Err(SideEffectsNotAllowed) + } +} + +/// Evaluate the expression with [`PureApplicator`] and produce a value. +pub fn expr_to_value( + expr: &ir::Expr, + local_scope: Option<&HashMap>>, +) -> Result< + waymark_runner_expr::ValueExpr, + waymark_runner_eval_core::ExprToValueError, +> { + let applicator = PureApplicator::default(); + let mut evaluator = waymark_runner_eval_core::CoreEvaluator(applicator); + evaluator.expr_to_value(expr, local_scope) +} From e799ca0608277bcc9dfe6d28308298035e4b9e0a Mon Sep 17 00:00:00 2001 From: MOZGIII Date: Thu, 23 Apr 2026 17:56:53 +0400 Subject: [PATCH 5/6] Switch runner pure condition evaluation to runner-pure-expr-eval --- Cargo.lock | 1 + Cargo.toml | 1 + crates/lib/runner/Cargo.toml | 1 + crates/lib/runner/src/expression_evaluator.rs | 119 +----------------- 4 files changed, 6 insertions(+), 116 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b394327a7..f56be3304 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4561,6 +4561,7 @@ dependencies = [ "waymark-runner-execution-core", "waymark-runner-executor-core", "waymark-runner-expr-eval", + "waymark-runner-pure-expr-eval", "waymark-runner-retry-policy", "waymark-runner-state", "waymark-synthetic-exception", diff --git a/Cargo.toml b/Cargo.toml index 3dba23966..5ea214094 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ waymark-runner-executor-core = { path = "crates/lib/runner-executor-core" } waymark-runner-expr = { path = "crates/lib/runner-expr" } waymark-runner-expr-eval = { path = "crates/lib/runner-expr-eval" } waymark-runner-expr-fmt = { path = "crates/lib/runner-expr-fmt" } +waymark-runner-pure-expr-eval = { path = "crates/lib/runner-pure-expr-eval" } waymark-runner-retry-policy = { path = "crates/lib/runner-retry-policy" } waymark-runner-state = { path = "crates/lib/runner-state" } waymark-scheduler-backend = { path = "crates/lib/scheduler-backend" } diff --git a/crates/lib/runner/Cargo.toml b/crates/lib/runner/Cargo.toml index 35e48ba47..6b516c00e 100644 --- a/crates/lib/runner/Cargo.toml +++ b/crates/lib/runner/Cargo.toml @@ -14,6 +14,7 @@ waymark-proto = { workspace = true } waymark-runner-execution-core = { workspace = true } waymark-runner-executor-core = { workspace = true } waymark-runner-expr-eval = { workspace = true } +waymark-runner-pure-expr-eval = { workspace = true } waymark-runner-retry-policy = { workspace = true } waymark-runner-state = { workspace = true } waymark-synthetic-exception = { workspace = true } diff --git a/crates/lib/runner/src/expression_evaluator.rs b/crates/lib/runner/src/expression_evaluator.rs index a6cad3da4..017b20db9 100644 --- a/crates/lib/runner/src/expression_evaluator.rs +++ b/crates/lib/runner/src/expression_evaluator.rs @@ -6,132 +6,19 @@ use serde_json::Value; use waymark_dag::{DAGEdge, EdgeType}; use waymark_ids::ExecutionId; -use waymark_ir_conversions::literal_to_json_value; use waymark_observability::obs; use waymark_proto::ast as ir; use waymark_runner_executor_core::ExecutionException; use waymark_runner_expr_eval::ValueExprEvaluator; -use waymark_runner_state::{ - ActionCallSpec, ActionResultValue, BinaryOpValue, DictEntryValue, DictValue, DotValue, - FunctionCallValue, IndexValue, ListValue, LiteralValue, UnaryOpValue, ValueExpr, VariableValue, -}; +use waymark_runner_state::{ActionCallSpec, ActionResultValue, FunctionCallValue, ValueExpr}; use super::{RunnerExecutor, RunnerExecutorError}; impl RunnerExecutor { /// Convert a pure IR expression into a ValueExpr without side effects. pub(super) fn expr_to_value(expr: &ir::Expr) -> Result { - match expr.kind.as_ref() { - Some(ir::expr::Kind::Literal(lit)) => Ok(ValueExpr::Literal(LiteralValue { - value: literal_to_json_value(lit), - })), - Some(ir::expr::Kind::Variable(var)) => Ok(ValueExpr::Variable(VariableValue { - name: var.name.clone(), - })), - Some(ir::expr::Kind::BinaryOp(op)) => { - let left = op - .left - .as_ref() - .ok_or_else(|| RunnerExecutorError("binary op missing left".to_string()))?; - let right = op - .right - .as_ref() - .ok_or_else(|| RunnerExecutorError("binary op missing right".to_string()))?; - Ok(ValueExpr::BinaryOp(BinaryOpValue { - left: Box::new(Self::expr_to_value(left)?), - op: op.op, - right: Box::new(Self::expr_to_value(right)?), - })) - } - Some(ir::expr::Kind::UnaryOp(op)) => { - let operand = op - .operand - .as_ref() - .ok_or_else(|| RunnerExecutorError("unary op missing operand".to_string()))?; - Ok(ValueExpr::UnaryOp(UnaryOpValue { - op: op.op, - operand: Box::new(Self::expr_to_value(operand)?), - })) - } - Some(ir::expr::Kind::List(list)) => { - let mut elements = Vec::new(); - for item in &list.elements { - elements.push(Self::expr_to_value(item)?); - } - Ok(ValueExpr::List(ListValue { elements })) - } - Some(ir::expr::Kind::Dict(dict_expr)) => { - let mut entries = Vec::new(); - for entry in &dict_expr.entries { - let key = entry - .key - .as_ref() - .ok_or_else(|| RunnerExecutorError("dict entry missing key".to_string()))?; - let value = entry.value.as_ref().ok_or_else(|| { - RunnerExecutorError("dict entry missing value".to_string()) - })?; - entries.push(DictEntryValue { - key: Self::expr_to_value(key)?, - value: Self::expr_to_value(value)?, - }); - } - Ok(ValueExpr::Dict(DictValue { entries })) - } - Some(ir::expr::Kind::Index(index)) => { - let object = index.object.as_ref().ok_or_else(|| { - RunnerExecutorError("index access missing object".to_string()) - })?; - let index_expr = index - .index - .as_ref() - .ok_or_else(|| RunnerExecutorError("index access missing index".to_string()))?; - Ok(ValueExpr::Index(IndexValue { - object: Box::new(Self::expr_to_value(object)?), - index: Box::new(Self::expr_to_value(index_expr)?), - })) - } - Some(ir::expr::Kind::Dot(dot)) => { - let object = dot - .object - .as_ref() - .ok_or_else(|| RunnerExecutorError("dot access missing object".to_string()))?; - Ok(ValueExpr::Dot(DotValue { - object: Box::new(Self::expr_to_value(object)?), - attribute: dot.attribute.clone(), - })) - } - Some(ir::expr::Kind::FunctionCall(call)) => { - let mut args = Vec::new(); - for arg in &call.args { - args.push(Self::expr_to_value(arg)?); - } - let mut kwargs = HashMap::new(); - for kw in &call.kwargs { - if let Some(value) = &kw.value { - kwargs.insert(kw.name.clone(), Self::expr_to_value(value)?); - } - } - let global_fn = if call.global_function != 0 { - Some(call.global_function) - } else { - None - }; - Ok(ValueExpr::FunctionCall(FunctionCallValue { - name: call.name.clone(), - args, - kwargs, - global_function: global_fn, - })) - } - Some( - ir::expr::Kind::ActionCall(_) - | ir::expr::Kind::ParallelExpr(_) - | ir::expr::Kind::SpreadExpr(_), - ) => Err(RunnerExecutorError( - "action/spread calls not allowed in guard expressions".to_string(), - )), - None => Ok(ValueExpr::Literal(LiteralValue { value: Value::Null })), - } + waymark_runner_pure_expr_eval::expr_to_value(expr, None) + .map_err(|err| RunnerExecutorError(err.to_string())) } /// Evaluate a guard expression using current symbolic assignments. From cbfc2f764d0d5597fd783a804bacabc587687e06 Mon Sep 17 00:00:00 2001 From: MOZGIII Date: Mon, 13 Apr 2026 22:27:24 +0400 Subject: [PATCH 6/6] Add runner-eval-dag --- Cargo.lock | 8 ++++++++ crates/lib/runner-eval-dag/Cargo.toml | 9 +++++++++ crates/lib/runner-eval-dag/src/lib.rs | 21 +++++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 crates/lib/runner-eval-dag/Cargo.toml create mode 100644 crates/lib/runner-eval-dag/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index f56be3304..31ccb5d50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4578,6 +4578,14 @@ dependencies = [ "waymark-runner-expr", ] +[[package]] +name = "waymark-runner-eval-dag" +version = "0.1.0" +dependencies = [ + "waymark-dag", + "waymark-runner-eval-core", +] + [[package]] name = "waymark-runner-execution-core" version = "0.1.0" diff --git a/crates/lib/runner-eval-dag/Cargo.toml b/crates/lib/runner-eval-dag/Cargo.toml new file mode 100644 index 000000000..b19c3be2a --- /dev/null +++ b/crates/lib/runner-eval-dag/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "waymark-runner-eval-dag" +edition = "2024" +version.workspace = true +publish.workspace = true + +[dependencies] +waymark-dag = { workspace = true } +waymark-runner-eval-core = { workspace = true } diff --git a/crates/lib/runner-eval-dag/src/lib.rs b/crates/lib/runner-eval-dag/src/lib.rs new file mode 100644 index 000000000..cfc124b58 --- /dev/null +++ b/crates/lib/runner-eval-dag/src/lib.rs @@ -0,0 +1,21 @@ +//! Expression evaluator for DAG-related operations. + +use std::sync::Arc; + +use waymark_dag::DAG; + +pub trait SideEffectApplicator: waymark_runner_eval_core::SideEffectApplicator {} + +#[derive(Debug)] +pub struct DagEvaluator { + /// The DAG to use. + pub dag: Arc, + + /// The applicator to apply side effects with. + pub applicator: SideEffectApplicator, +} + +impl DagEvaluator where + SideEffectApplicator: self::SideEffectApplicator +{ +}