From 2a1e78743e5261621928109df644f843475bea96 Mon Sep 17 00:00:00 2001 From: Raphael Date: Fri, 17 Apr 2026 19:32:22 +0200 Subject: [PATCH 01/28] feat: added wip return and respond handling --- crates/core/src/context/executor.rs | 513 +++++++++++++++++++++- crates/core/src/context/signal.rs | 4 +- crates/core/src/runtime/functions/http.rs | 3 +- crates/taurus/src/main.rs | 92 +++- 4 files changed, 589 insertions(+), 23 deletions(-) diff --git a/crates/core/src/context/executor.rs b/crates/core/src/context/executor.rs index 60568cf..a490fdf 100644 --- a/crates/core/src/context/executor.rs +++ b/crates/core/src/context/executor.rs @@ -49,6 +49,8 @@ pub struct Executor<'a> { nodes: HashMap, // Connection for Remote Function Execution => Actions remote: Option<&'a dyn RemoteRuntime>, + // Optional side-effect hook triggered whenever a respond signal is emitted. + respond_emitter: Option<&'a dyn Fn(Value)>, } /// Determines whether a node should be executed remotely. @@ -77,6 +79,7 @@ impl<'a> Executor<'a> { functions, nodes, remote: None, + respond_emitter: None, } } @@ -89,6 +92,12 @@ impl<'a> Executor<'a> { self } + /// Attach a callback that is invoked for every emitted respond value. + pub fn with_respond_emitter(mut self, emitter: &'a dyn Fn(Value)) -> Self { + self.respond_emitter = Some(emitter); + self + } + /// This is now the ONLY execution entry point. /// /// - `start_node_id` is the first node in the flow. @@ -145,6 +154,24 @@ impl<'a> Executor<'a> { return (signal, call_root_frame.unwrap()); } + Signal::Respond(v) => { + let node = self.nodes.get(¤t).unwrap(); + + if let Some(emit) = self.respond_emitter { + emit(v.clone()); + } + + // Respond behaves like success for flow continuation and references. + ctx.insert_success(node.database_id, v.clone()); + + if let Some(next) = node.next_node_id { + current = next; + continue; + } + + return (Signal::Success(v), call_root_frame.unwrap()); + } + _ => return (signal, call_root_frame.unwrap()), } } @@ -418,6 +445,10 @@ impl<'a> Executor<'a> { Signal::Success(v) => { *arg = Argument::Eval(v); } + Signal::Return(v) => { + // `return` only exits the child context and yields a value upward. + *arg = Argument::Eval(v); + } s => { return Err(( s, @@ -450,7 +481,11 @@ impl<'a> Executor<'a> { let label = ctx.pop_runtime_trace_label(); let (sig, child_root) = self.execute_call(node_id, ctx, tracer); tracer.link_child(frame_id, child_root, EdgeKind::RuntimeCall { label }); - sig + match sig { + // `return` only exits the child context and is a value for the caller. + Signal::Return(v) => Signal::Success(v), + other => other, + } }; (entry.handler)(args, ctx, &mut run) @@ -551,6 +586,11 @@ impl<'a> Executor<'a> { *arg = Argument::Eval(v.clone()); values.push(v); } + Signal::Return(v) => { + // `return` only exits the child context and yields a value upward. + *arg = Argument::Eval(v.clone()); + values.push(v); + } s => { return Err(( s, @@ -650,3 +690,474 @@ fn preview_reference(r: &tucana::shared::ReferenceValue) -> String { format!("{}+paths({})", target, r.paths.len()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::context::registry::{HandlerFn, IntoFunctionEntry}; + use std::cell::RefCell; + use tucana::shared::number_value::Number; + use tucana::shared::{NodeFunction, NumberValue, ReferenceValue}; + + fn num(i: i64) -> Value { + Value { + kind: Some(Kind::NumberValue(NumberValue { + number: Some(Number::Integer(i)), + })), + } + } + + fn extract_i64(v: &Value) -> i64 { + match &v.kind { + Some(Kind::NumberValue(NumberValue { + number: Some(Number::Integer(i)), + })) => *i, + other => panic!("Expected integer number value, got {:?}", other), + } + } + + fn node(id: i64, function_id: &str, next: Option) -> NodeFunction { + NodeFunction { + database_id: id, + runtime_function_id: function_id.to_string(), + definition_source: "taurus".to_string(), + next_node_id: next, + ..Default::default() + } + } + + fn node_reference(node_id: i64) -> ReferenceValue { + ReferenceValue { + paths: vec![], + target: Some(Target::NodeId(node_id)), + } + } + + fn assert_node_success(ctx: &mut Context, node_id: i64, expected: i64) { + match ctx.get(node_reference(node_id)) { + ContextResult::Success(v) => assert_eq!(extract_i64(&v), expected), + ContextResult::Error(_) => { + panic!("Expected success result for node {}, got error", node_id) + } + ContextResult::NotFound => { + panic!( + "Expected success result for node {}, got not found", + node_id + ) + } + } + } + + fn assert_node_error(ctx: &mut Context, node_id: i64, expected_name: &str) { + match ctx.get(node_reference(node_id)) { + ContextResult::Error(err) => assert_eq!(err.name, expected_name), + ContextResult::Success(_) => { + panic!("Expected error result for node {}, got success", node_id) + } + ContextResult::NotFound => { + panic!("Expected error result for node {}, got not found", node_id) + } + } + } + + fn assert_node_not_found(ctx: &mut Context, node_id: i64) { + match ctx.get(node_reference(node_id)) { + ContextResult::NotFound => {} + ContextResult::Success(_) => { + panic!("Expected no result for node {}, got success", node_id) + } + ContextResult::Error(_) => { + panic!("Expected no result for node {}, got error", node_id) + } + } + } + + fn success_one_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ) -> Signal { + Signal::Success(num(1)) + } + + fn success_two_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ) -> Signal { + Signal::Success(num(2)) + } + + fn success_three_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ) -> Signal { + Signal::Success(num(3)) + } + + fn failure_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ) -> Signal { + Signal::Failure(RuntimeError::simple_str("TestFailure", "expected failure")) + } + + fn return_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ) -> Signal { + Signal::Return(num(9)) + } + + fn stop_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ) -> Signal { + Signal::Stop + } + + fn respond_eleven_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ) -> Signal { + Signal::Respond(num(11)) + } + + fn respond_twentytwo_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ) -> Signal { + Signal::Respond(num(22)) + } + + fn respond_fortyfour_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ) -> Signal { + Signal::Respond(num(44)) + } + + fn return_seventyseven_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ) -> Signal { + Signal::Return(num(77)) + } + + fn runtime_call_child_two_handler( + _args: &[Argument], + ctx: &mut Context, + run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ) -> Signal { + match run(2, ctx) { + Signal::Success(_) => Signal::Success(num(55)), + other => other, + } + } + + fn runtime_call_child_return_handler( + _args: &[Argument], + ctx: &mut Context, + run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ) -> Signal { + run(2, ctx) + } + + #[test] + fn success_signal_continues_through_next_nodes() { + let mut store = FunctionStore::new(); + store.populate(vec![ + ( + "test::success_one", + HandlerFn::eager(success_one_handler, 0), + ), + ( + "test::success_two", + HandlerFn::eager(success_two_handler, 0), + ), + ]); + + let mut nodes = HashMap::new(); + nodes.insert(1, node(1, "test::success_one", Some(2))); + nodes.insert(2, node(2, "test::success_two", None)); + + let mut ctx = Context::default(); + let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); + + match signal { + Signal::Success(v) => assert_eq!(extract_i64(&v), 2), + other => panic!("Expected success signal, got {:?}", other), + } + + assert_node_success(&mut ctx, 1, 1); + assert_node_success(&mut ctx, 2, 2); + } + + #[test] + fn failure_signal_stops_flow_and_persists_error() { + let mut store = FunctionStore::new(); + store.populate(vec![ + ( + "test::success_one", + HandlerFn::eager(success_one_handler, 0), + ), + ("test::failure", HandlerFn::eager(failure_handler, 0)), + ( + "test::success_three", + HandlerFn::eager(success_three_handler, 0), + ), + ]); + + let mut nodes = HashMap::new(); + nodes.insert(1, node(1, "test::success_one", Some(2))); + nodes.insert(2, node(2, "test::failure", Some(3))); + nodes.insert(3, node(3, "test::success_three", None)); + + let mut ctx = Context::default(); + let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); + + match signal { + Signal::Failure(err) => assert_eq!(err.name, "TestFailure"), + other => panic!("Expected failure signal, got {:?}", other), + } + + assert_node_success(&mut ctx, 1, 1); + assert_node_error(&mut ctx, 2, "TestFailure"); + assert_node_not_found(&mut ctx, 3); + } + + #[test] + fn return_signal_breaks_execution_and_skips_next_nodes() { + let mut store = FunctionStore::new(); + store.populate(vec![ + ("test::return", HandlerFn::eager(return_handler, 0)), + ( + "test::success_three", + HandlerFn::eager(success_three_handler, 0), + ), + ]); + + let mut nodes = HashMap::new(); + nodes.insert(1, node(1, "test::return", Some(2))); + nodes.insert(2, node(2, "test::success_three", None)); + + let mut ctx = Context::default(); + let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); + + match signal { + Signal::Return(v) => assert_eq!(extract_i64(&v), 9), + other => panic!("Expected return signal, got {:?}", other), + } + + assert_node_not_found(&mut ctx, 1); + assert_node_not_found(&mut ctx, 2); + } + + #[test] + fn stop_signal_stops_execution_and_skips_next_nodes() { + let mut store = FunctionStore::new(); + store.populate(vec![ + ("test::stop", HandlerFn::eager(stop_handler, 0)), + ( + "test::success_three", + HandlerFn::eager(success_three_handler, 0), + ), + ]); + + let mut nodes = HashMap::new(); + nodes.insert(1, node(1, "test::stop", Some(2))); + nodes.insert(2, node(2, "test::success_three", None)); + + let mut ctx = Context::default(); + let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); + + assert!(matches!(signal, Signal::Stop)); + assert_node_not_found(&mut ctx, 1); + assert_node_not_found(&mut ctx, 2); + } + + #[test] + fn respond_signal_emits_for_each_node_and_execution_continues() { + let mut store = FunctionStore::new(); + store.populate(vec![ + ( + "test::respond_eleven", + HandlerFn::eager(respond_eleven_handler, 0), + ), + ( + "test::respond_twentytwo", + HandlerFn::eager(respond_twentytwo_handler, 0), + ), + ( + "test::success_three", + HandlerFn::eager(success_three_handler, 0), + ), + ]); + + let mut nodes = HashMap::new(); + nodes.insert(1, node(1, "test::respond_eleven", Some(2))); + nodes.insert(2, node(2, "test::respond_twentytwo", Some(3))); + nodes.insert(3, node(3, "test::success_three", None)); + + let emitted = RefCell::new(Vec::::new()); + let emitter = |v: Value| { + emitted.borrow_mut().push(v); + }; + + let mut ctx = Context::default(); + let signal = Executor::new(&store, nodes) + .with_respond_emitter(&emitter) + .execute(1, &mut ctx, false); + + match signal { + Signal::Success(v) => assert_eq!(extract_i64(&v), 3), + other => panic!("Expected final success signal, got {:?}", other), + } + + let emitted_values = emitted.borrow(); + assert_eq!(emitted_values.len(), 2); + assert_eq!(extract_i64(&emitted_values[0]), 11); + assert_eq!(extract_i64(&emitted_values[1]), 22); + + assert_node_success(&mut ctx, 1, 11); + assert_node_success(&mut ctx, 2, 22); + assert_node_success(&mut ctx, 3, 3); + } + + #[test] + fn respond_signal_emits_inside_runtime_call_and_parent_continues() { + let mut store = FunctionStore::new(); + store.populate(vec![ + ( + "test::runtime_call_child_two", + HandlerFn::eager(runtime_call_child_two_handler, 0), + ), + ( + "test::respond_fortyfour", + HandlerFn::eager(respond_fortyfour_handler, 0), + ), + ]); + + let mut nodes = HashMap::new(); + nodes.insert(1, node(1, "test::runtime_call_child_two", None)); + nodes.insert(2, node(2, "test::respond_fortyfour", None)); + + let emitted = RefCell::new(Vec::::new()); + let emitter = |v: Value| { + emitted.borrow_mut().push(v); + }; + + let mut ctx = Context::default(); + let signal = Executor::new(&store, nodes) + .with_respond_emitter(&emitter) + .execute(1, &mut ctx, false); + + match signal { + Signal::Success(v) => assert_eq!(extract_i64(&v), 55), + other => panic!("Expected final success signal, got {:?}", other), + } + + let emitted_values = emitted.borrow(); + assert_eq!(emitted_values.len(), 1); + assert_eq!(extract_i64(&emitted_values[0]), 44); + + assert_node_success(&mut ctx, 1, 55); + assert_node_success(&mut ctx, 2, 44); + } + + #[test] + fn return_signal_in_child_context_continues_parent_flow() { + let mut store = FunctionStore::new(); + store.populate(vec![ + ( + "test::runtime_call_child_return", + HandlerFn::eager(runtime_call_child_return_handler, 0), + ), + ( + "test::return_seventyseven", + HandlerFn::eager(return_seventyseven_handler, 0), + ), + ( + "test::success_three", + HandlerFn::eager(success_three_handler, 0), + ), + ]); + + let mut nodes = HashMap::new(); + nodes.insert(1, node(1, "test::runtime_call_child_return", Some(3))); + nodes.insert(2, node(2, "test::return_seventyseven", None)); + nodes.insert(3, node(3, "test::success_three", None)); + + let mut ctx = Context::default(); + let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); + + match signal { + Signal::Success(v) => assert_eq!(extract_i64(&v), 3), + other => panic!("Expected final success signal, got {:?}", other), + } + + // Child return exits its own context and is treated as value by parent node. + assert_node_success(&mut ctx, 1, 77); + assert_node_not_found(&mut ctx, 2); + assert_node_success(&mut ctx, 3, 3); + } + + #[test] + fn child_return_skips_child_next_but_parent_next_runs() { + // Graph: + // node1 -> next node4, and node1 executes child node2 via run(...) + // node2 -> RETURN and has next node3 + // node3 must NOT run (because node2 returned) + // node4 must run (parent chain continues) + let mut store = FunctionStore::new(); + store.populate(vec![ + ( + "test::runtime_call_child_return", + HandlerFn::eager(runtime_call_child_return_handler, 0), + ), + ( + "test::return_seventyseven", + HandlerFn::eager(return_seventyseven_handler, 0), + ), + ( + "test::success_two", + HandlerFn::eager(success_two_handler, 0), + ), + ( + "test::success_three", + HandlerFn::eager(success_three_handler, 0), + ), + ]); + + let mut nodes = HashMap::new(); + nodes.insert(1, node(1, "test::runtime_call_child_return", Some(4))); + nodes.insert(2, node(2, "test::return_seventyseven", Some(3))); + nodes.insert(3, node(3, "test::success_two", None)); + nodes.insert(4, node(4, "test::success_three", None)); + + let mut ctx = Context::default(); + let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); + + match signal { + Signal::Success(v) => assert_eq!(extract_i64(&v), 3), + other => panic!("Expected final success signal, got {:?}", other), + } + + // node1 ran and received child return value + assert_node_success(&mut ctx, 1, 77); + // node2 returned, so it has no success/error entry in context + assert_node_not_found(&mut ctx, 2); + // node3 is child next node and must be skipped due to return in node2 + assert_node_not_found(&mut ctx, 3); + // parent next node still executes + assert_node_success(&mut ctx, 4, 3); + } +} diff --git a/crates/core/src/context/signal.rs b/crates/core/src/context/signal.rs index c51aa92..aa11e0b 100644 --- a/crates/core/src/context/signal.rs +++ b/crates/core/src/context/signal.rs @@ -16,8 +16,8 @@ pub enum Signal { // - will break the current context and return the value to the upper node Return(Value), // Will be signaled if the `respond` function has been executed - // - will stop the execution of the flow completely - // - will return the value to the adapter + // - will not stop (will continue) the execution of the flow + // - will emit a respond signal which can be handled by the runtime holder Respond(Value), // Will be signaled if the `stop` function has been executed // - will stop the execution of the flow completely diff --git a/crates/core/src/runtime/functions/http.rs b/crates/core/src/runtime/functions/http.rs index ab5f868..677c62d 100644 --- a/crates/core/src/runtime/functions/http.rs +++ b/crates/core/src/runtime/functions/http.rs @@ -15,7 +15,7 @@ pub fn collect_http_functions() -> Vec<(&'static str, HandlerFunctionEntry)> { "http::response::create", HandlerFn::eager(create_response, 4), ), - ("rest::control::respond", HandlerFn::eager(respond, 3)), + ("rest::control::respond", HandlerFn::eager(respond, 1)), ] } @@ -25,6 +25,7 @@ fn respond( _run: &mut dyn FnMut(i64, &mut Context) -> Signal, ) -> Signal { args!(args => struct_val: Struct); + let fields = &struct_val.fields; let Some(headers_val) = fields.get("headers") else { diff --git a/crates/taurus/src/main.rs b/crates/taurus/src/main.rs index ea7932e..51ab341 100644 --- a/crates/taurus/src/main.rs +++ b/crates/taurus/src/main.rs @@ -14,6 +14,8 @@ use futures_lite::StreamExt; use log::error; use prost::Message; use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::{Duration, Instant}; use taurus_core::context::context::Context; use taurus_core::context::executor::Executor; @@ -21,6 +23,7 @@ use taurus_core::context::registry::FunctionStore; use taurus_core::context::signal::Signal; use taurus_core::runtime::error::RuntimeError; use tokio::signal; +use tokio::sync::mpsc; use tokio::time::sleep; use tonic_health::pb::health_server::HealthServer; use tucana::shared::value::Kind; @@ -32,6 +35,7 @@ fn handle_message( flow: ExecutionFlow, store: &FunctionStore, nats_remote: &RemoteNatsClient, + respond_emitter: Option<&dyn Fn(Value)>, ) -> (Signal, RuntimeUsage) { let start = Instant::now(); let mut context = match flow.input_value { @@ -62,9 +66,12 @@ fn handle_message( .map(|node| (node.database_id, node)) .collect(); - let signal = Executor::new(store, node_functions) - .with_remote_runtime(nats_remote) - .execute(flow.starting_node_id, &mut context, true); + let mut executor = Executor::new(store, node_functions).with_remote_runtime(nats_remote); + if let Some(emitter) = respond_emitter { + executor = executor.with_respond_emitter(emitter); + } + + let signal = executor.execute(flow.starting_node_id, &mut context, true); let duration_millis = start.elapsed().as_millis() as i64; ( @@ -211,47 +218,94 @@ async fn main() { }; let flow_id = flow.flow_id; - let result = handle_message(flow, &store, &nats_client); - let value = match result.0 { + let reply = msg.reply.clone(); + let (respond_tx, mut respond_rx) = mpsc::unbounded_channel::(); + let respond_publisher = reply.clone().map(|reply_subject| { + let publish_client = client.clone(); + tokio::spawn(async move { + while let Some(value) = respond_rx.recv().await { + match publish_client + .publish(reply_subject.clone(), value.encode_to_vec().into()) + .await + { + Ok(_) => log::debug!("Respond signal value sent"), + Err(err) => { + log::error!("Failed to send respond signal value: {:?}", err) + } + } + } + }) + }); + + let emit_tx = respond_tx.clone(); + let respond_count = Arc::new(AtomicUsize::new(0)); + let respond_count_for_emitter = respond_count.clone(); + let respond_emitter = move |value: Value| { + respond_count_for_emitter.fetch_add(1, Ordering::Relaxed); + if let Err(err) = emit_tx.send(value) { + log::debug!( + "Dropped respond signal value because publisher is unavailable: {:?}", + err + ); + } + }; + + let (signal, runtime_usage) = + handle_message(flow, &store, &nats_client, Some(&respond_emitter)); + drop(respond_emitter); + drop(respond_tx); + + if let Some(publisher) = respond_publisher + && let Err(err) = publisher.await + { + log::error!("Respond publisher task failed: {:?}", err); + } + let has_responded = respond_count.load(Ordering::Relaxed) > 0; + + let final_value = match signal { Signal::Failure(error) => { log::error!( "RuntimeError occurred, execution failed because: {:?}", error ); - error.as_value() + Some(error.as_value()) } Signal::Success(v) => { log::debug!("Execution ended on a success signal"); - v + Some(v) } Signal::Return(v) => { log::debug!("Execution ended on a return signal"); - v + Some(v) } - Signal::Respond(v) => { + Signal::Respond(_) => { log::debug!("Execution ended on a respond signal"); - v + None } Signal::Stop => { log::debug!("Revied stop signal as last signal"); - Value { + Some(Value { kind: Some(Kind::NullValue(0)), - } + }) } }; - log::info!("For the flow_id {} returing the value {:?}", flow_id, value); + if let Some(value) = final_value + && !has_responded + { + log::info!("For the flow_id {} returing the value {:?}", flow_id, value); - // Send a response to the reply subject - if let Some(reply) = msg.reply { - match client.publish(reply, value.encode_to_vec().into()).await { - Ok(_) => log::debug!("Response sent"), - Err(err) => log::error!("Failed to send response: {:?}", err), + // Send a terminal response to the reply subject. + if let Some(reply) = reply { + match client.publish(reply, value.encode_to_vec().into()).await { + Ok(_) => log::debug!("Response sent"), + Err(err) => log::error!("Failed to send response: {:?}", err), + } } } if let Some(usage_service) = &runtime_usage_service { - usage_service.update_runtime_usage(result.1).await; + usage_service.update_runtime_usage(runtime_usage).await; } } From cfe44de9fe684b95d17ae77b16226abb5bb655b1 Mon Sep 17 00:00:00 2001 From: Raphael Date: Fri, 17 Apr 2026 19:48:11 +0200 Subject: [PATCH 02/28] ref: split executor into its own module --- crates/core/src/context/executor.rs | 1163 ----------------- crates/core/src/context/executor/arguments.rs | 242 ++++ .../src/context/executor/execution_loop.rs | 103 ++ .../core/src/context/executor/formatting.rs | 56 + crates/core/src/context/executor/mod.rs | 126 ++ .../src/context/executor/node_execution.rs | 258 ++++ crates/core/src/context/executor/tests.rs | 467 +++++++ 7 files changed, 1252 insertions(+), 1163 deletions(-) delete mode 100644 crates/core/src/context/executor.rs create mode 100644 crates/core/src/context/executor/arguments.rs create mode 100644 crates/core/src/context/executor/execution_loop.rs create mode 100644 crates/core/src/context/executor/formatting.rs create mode 100644 crates/core/src/context/executor/mod.rs create mode 100644 crates/core/src/context/executor/node_execution.rs create mode 100644 crates/core/src/context/executor/tests.rs diff --git a/crates/core/src/context/executor.rs b/crates/core/src/context/executor.rs deleted file mode 100644 index a490fdf..0000000 --- a/crates/core/src/context/executor.rs +++ /dev/null @@ -1,1163 +0,0 @@ -//! Executor for flow node execution. -//! -//! Execution model overview: -//! - The executor walks a linear "next" chain starting from `starting_node_id`. -//! - Each node can call into other nodes through lazy arguments. -//! - A node marked as remote is executed via `RemoteRuntime`. -//! - The executor is synchronous; remote calls are awaited via `block_on`. -//! -//! Remote execution: -//! - A node is considered remote based on `is_remote(&node)`. -//! - Remote args are fully resolved to concrete `Value`s before sending. -//! - The request parameters are mapped by `runtime_parameter_id`. -//! - Remote responses are mapped into `Signal::Success` or `Signal::Failure`. -//! -//! Tracing: -//! - Each node execution produces a trace frame with arguments and outcome. -//! - Child executions are linked with `EdgeKind` to reflect eager or runtime calls. -//! -//! Error behavior: -//! - Missing nodes/functions yield `Signal::Failure`. -//! - Remote failures are mapped to `RuntimeError`. -//! - The executor commits all final outcomes into the `Context`. - -use crate::context::argument::{Argument, ParameterNode}; -use crate::context::context::{Context, ContextResult}; -use crate::context::registry::{FunctionStore, HandlerFunctionEntry}; -use crate::context::signal::Signal; -use crate::debug::trace::{ArgKind, ArgTrace, EdgeKind, Outcome, ReferenceKind}; -use crate::debug::tracer::{ExecutionTracer, Tracer}; -use crate::runtime::error::RuntimeError; -use crate::runtime::remote::RemoteRuntime; - -use futures_lite::future::block_on; -use std::collections::HashMap; -use tucana::aquila::ExecutionRequest; -use tucana::shared::reference_value::Target; -use tucana::shared::value::Kind; -use tucana::shared::{NodeFunction, Struct, Value}; -use uuid::Uuid; - -/// Executes a flow graph by repeatedly evaluating nodes. -/// -/// The executor is intentionally stateless with respect to the runtime: -/// it borrows the function registry and graph, and mutates only the `Context`. -pub struct Executor<'a> { - // Registered Runtime Functions - functions: &'a FunctionStore, - // Nodes to execute - nodes: HashMap, - // Connection for Remote Function Execution => Actions - remote: Option<&'a dyn RemoteRuntime>, - // Optional side-effect hook triggered whenever a respond signal is emitted. - respond_emitter: Option<&'a dyn Fn(Value)>, -} - -/// Determines whether a node should be executed remotely. -/// -/// The current policy treats any node whose `definition_source` is not `"taurus"` -/// as a remote node. -fn is_remote(node: &NodeFunction) -> bool { - if node.definition_source.is_empty() { - log::warn!( - "Found empty definition source, taking runtime as origin for node id: {}", - node.database_id - ); - return false; - } - - node.definition_source != "taurus" -} - -impl<'a> Executor<'a> { - /// Create a new executor for the given function store and node map. - /// - /// This does not attach a remote runtime. Remote nodes will error unless - /// a runtime is provided via `with_remote_runtime`. - pub fn new(functions: &'a FunctionStore, nodes: HashMap) -> Self { - Self { - functions, - nodes, - remote: None, - respond_emitter: None, - } - } - - /// Attach a remote runtime for executing nodes marked as remote. - /// - /// This is a builder-style method for ergonomic setup: - /// `Executor::new(...).with_remote_runtime(&runtime)`. - pub fn with_remote_runtime(mut self, remote: &'a dyn RemoteRuntime) -> Self { - self.remote = Some(remote); - self - } - - /// Attach a callback that is invoked for every emitted respond value. - pub fn with_respond_emitter(mut self, emitter: &'a dyn Fn(Value)) -> Self { - self.respond_emitter = Some(emitter); - self - } - - /// This is now the ONLY execution entry point. - /// - /// - `start_node_id` is the first node in the flow. - /// - `ctx` is mutated in-place with results and errors. - /// - `with_trace` controls whether the trace is printed on completion. - pub fn execute(&self, start_node_id: i64, ctx: &mut Context, with_trace: bool) -> Signal { - let mut tracer = Tracer::new(); - - let (signal, _root_frame) = self.execute_call(start_node_id, ctx, &mut tracer); - - if with_trace && let Some(run) = tracer.take_run() { - println!("{}", crate::debug::render::render_trace(&run)); - } - signal - } - - /// Main execution loop. - /// - /// Executes nodes one-by-one along the `next_node_id` chain until a - /// non-success signal is produced or the chain ends. - fn execute_call( - &self, - start_node_id: i64, - ctx: &mut Context, - tracer: &mut dyn ExecutionTracer, - ) -> (Signal, u64) { - let mut current = start_node_id; - - let mut call_root_frame: Option = None; - let mut previous_frame: Option = None; - - loop { - let (signal, frame_id) = self.execute_single_node(current, ctx, tracer); - - if call_root_frame.is_none() { - call_root_frame = Some(frame_id); - } - - // Link linear NEXT chain - if let Some(prev) = previous_frame { - tracer.link_child(prev, frame_id, EdgeKind::Next); - } - previous_frame = Some(frame_id); - - match signal { - Signal::Success(_) => { - let node = self.nodes.get(¤t).unwrap(); - - if let Some(next) = node.next_node_id { - current = next; - continue; - } - - return (signal, call_root_frame.unwrap()); - } - - Signal::Respond(v) => { - let node = self.nodes.get(¤t).unwrap(); - - if let Some(emit) = self.respond_emitter { - emit(v.clone()); - } - - // Respond behaves like success for flow continuation and references. - ctx.insert_success(node.database_id, v.clone()); - - if let Some(next) = node.next_node_id { - current = next; - continue; - } - - return (Signal::Success(v), call_root_frame.unwrap()); - } - - _ => return (signal, call_root_frame.unwrap()), - } - } - } - - /// Execute a single node and return its signal and trace frame id. - /// - /// This handles: - /// - Node lookup - /// - Remote vs local dispatch - /// - Argument building and eager evaluation - /// - Handler invocation and result commit - fn execute_single_node( - &self, - node_id: i64, - ctx: &mut Context, - tracer: &mut dyn ExecutionTracer, - ) -> (Signal, u64) { - ctx.set_current_node_id(node_id); - let node = match self.nodes.get(&node_id) { - Some(n) => n.clone(), - None => { - let err = - RuntimeError::simple("NodeNotFound", format!("Node {} not found", node_id)); - return (Signal::Failure(err), 0); - } - }; - - if is_remote(&node) { - let remote = match self.remote { - Some(r) => r, - None => { - let err = RuntimeError::simple( - "RemoteRuntimeNotConfigured", - "Remote runtime not configured".to_string(), - ); - return (Signal::Failure(err), 0); - } - }; - - let frame_id = tracer.enter_node(node.database_id, node.runtime_function_id.as_str()); - - let mut args = match self.build_args(&node, ctx, tracer, frame_id) { - Ok(a) => a, - Err(e) => { - ctx.insert_error(node.database_id, e.clone()); - tracer.exit_node( - frame_id, - Outcome::Failure { - error_preview: format!("{:#?}", e), - }, - ); - return (Signal::Failure(e), frame_id); - } - }; - - let values = match self.resolve_remote_args(&mut args, ctx, tracer, frame_id) { - Ok(v) => v, - Err((sig, outcome)) => { - tracer.exit_node(frame_id, outcome); - return (sig, frame_id); - } - }; - - let request = match self.build_remote_request(&node, values) { - Ok(r) => r, - Err(e) => { - ctx.insert_error(node.database_id, e.clone()); - tracer.exit_node( - frame_id, - Outcome::Failure { - error_preview: format!("{:#?}", e), - }, - ); - return (Signal::Failure(e), frame_id); - } - }; - - let remote_result = - block_on(remote.execute_remote(node.definition_source.clone(), request)); - let signal = match remote_result { - Ok(value) => Signal::Success(value), - Err(err) => Signal::Failure(err), - }; - - let final_signal = self.commit_result(&node, signal, ctx, tracer, frame_id); - return (final_signal, frame_id); - } - - let entry = match self.functions.get(node.runtime_function_id.as_str()) { - Some(e) => e, - None => { - let err = RuntimeError::simple( - "FunctionNotFound", - format!("Function {} not found", node.runtime_function_id), - ); - return (Signal::Failure(err), 0); - } - }; - - let frame_id = tracer.enter_node(node.database_id, node.runtime_function_id.as_str()); - - // ---- Build args - let mut args = match self.build_args(&node, ctx, tracer, frame_id) { - Ok(a) => a, - Err(e) => { - ctx.insert_error(node.database_id, e.clone()); - tracer.exit_node( - frame_id, - Outcome::Failure { - error_preview: format!("{:#?}", e), - }, - ); - return (Signal::Failure(e), frame_id); - } - }; - - // ---- Force eager args - if let Err((sig, outcome)) = - self.force_eager_args(&node, entry, &mut args, ctx, tracer, frame_id) - { - tracer.exit_node(frame_id, outcome); - return (sig, frame_id); - } - - // ---- Invoke handler - let result = self.invoke_handler(entry, &args, ctx, tracer, frame_id); - - // ---- Commit result - let final_signal = self.commit_result(&node, result, ctx, tracer, frame_id); - - (final_signal, frame_id) - } - - /// Build arguments for a node from literals, references, or thunks. - /// - /// Arguments are recorded to the tracer for debugging and inspection. - /// Thunks are represented by the referenced node id. - fn build_args( - &self, - node: &NodeFunction, - ctx: &mut Context, - tracer: &mut dyn ExecutionTracer, - frame_id: u64, - ) -> Result, RuntimeError> { - let mut args = Vec::with_capacity(node.parameters.len()); - - for (i, param) in node.parameters.iter().enumerate() { - let node_value = param.value.as_ref().ok_or_else(|| { - RuntimeError::simple_str("NodeValueNotFound", "Missing param value") - })?; - - let value = node_value.value.as_ref().ok_or_else(|| { - RuntimeError::simple_str("NodeValueNotFound", "Missing inner value") - })?; - - match value { - tucana::shared::node_value::Value::LiteralValue(v) => { - tracer.record_arg( - frame_id, - ArgTrace { - index: i, - kind: ArgKind::Literal, - preview: preview_value(v), - }, - ); - args.push(Argument::Eval(v.clone())); - } - - tucana::shared::node_value::Value::ReferenceValue(r) => match ctx.get(r.clone()) { - ContextResult::Success(v) => { - let reference = match r.target { - Some(ref_value) => match ref_value { - tucana::shared::reference_value::Target::FlowInput(_) => { - ReferenceKind::FlowInput - } - tucana::shared::reference_value::Target::NodeId(id) => { - ReferenceKind::Result { node_id: id } - } - tucana::shared::reference_value::Target::InputType(input_type) => { - ReferenceKind::InputType { - node_id: input_type.node_id, - input_index: input_type.input_index, - parameter_index: input_type.parameter_index, - } - } - }, - None => ReferenceKind::Empty, - }; - - tracer.record_arg( - frame_id, - ArgTrace { - index: i, - kind: ArgKind::Reference { - reference, - hit: true, - }, - preview: format!( - "ctx.get({}) -> {}", - preview_reference(r), - preview_value(&v) - ), - }, - ); - args.push(Argument::Eval(v)); - } - ContextResult::Error(e) => return Err(e), - ContextResult::NotFound => { - return Err(RuntimeError::simple_str( - "ReferenceValueNotFound", - "Reference not found in context", - )); - } - }, - - tucana::shared::node_value::Value::NodeFunctionId(id) => { - tracer.record_arg( - frame_id, - ArgTrace { - index: i, - kind: ArgKind::Thunk { - node_id: *id, - eager: false, - executed: false, - }, - preview: format!("thunk({})", id), - }, - ); - args.push(Argument::Thunk(*id)); - } - } - } - - Ok(args) - } - - /// Eagerly execute any argument marked as `Eager`. - /// - /// Lazy arguments are preserved as thunks until needed by a handler. - /// If an eager child fails, the failure bubbles up immediately. - fn force_eager_args( - &self, - _node: &NodeFunction, - entry: &HandlerFunctionEntry, - args: &mut [Argument], - ctx: &mut Context, - tracer: &mut dyn ExecutionTracer, - parent_frame: u64, - ) -> Result<(), (Signal, Outcome)> { - for (i, arg) in args.iter_mut().enumerate() { - let mode = entry - .param_modes - .get(i) - .copied() - .unwrap_or(ParameterNode::Eager); - - if matches!(mode, ParameterNode::Eager) - && let Argument::Thunk(id) = *arg - { - tracer.mark_thunk(parent_frame, i, true, true); - let (child_sig, child_root) = self.execute_call(id, ctx, tracer); - - tracer.link_child( - parent_frame, - child_root, - EdgeKind::EagerCall { arg_index: i }, - ); - - match child_sig { - Signal::Success(v) => { - *arg = Argument::Eval(v); - } - Signal::Return(v) => { - // `return` only exits the child context and yields a value upward. - *arg = Argument::Eval(v); - } - s => { - return Err(( - s, - Outcome::Failure { - error_preview: "Eager child failed".into(), - }, - )); - } - } - } - } - - Ok(()) - } - - /// Invoke a local handler with a closure for lazy execution. - /// - /// The closure will evaluate a thunk node and link its trace to the - /// current execution frame. - fn invoke_handler( - &self, - entry: &HandlerFunctionEntry, - args: &[Argument], - ctx: &mut Context, - tracer: &mut dyn ExecutionTracer, - frame_id: u64, - ) -> Signal { - let mut run = |node_id: i64, ctx: &mut Context| { - tracer.mark_thunk_executed_by_node(frame_id, node_id); - let label = ctx.pop_runtime_trace_label(); - let (sig, child_root) = self.execute_call(node_id, ctx, tracer); - tracer.link_child(frame_id, child_root, EdgeKind::RuntimeCall { label }); - match sig { - // `return` only exits the child context and is a value for the caller. - Signal::Return(v) => Signal::Success(v), - other => other, - } - }; - - (entry.handler)(args, ctx, &mut run) - } - - /// Persist the final signal into the context and close the trace frame. - fn commit_result( - &self, - node: &NodeFunction, - result: Signal, - ctx: &mut Context, - tracer: &mut dyn ExecutionTracer, - frame_id: u64, - ) -> Signal { - match result { - Signal::Success(v) => { - ctx.insert_success(node.database_id, v.clone()); - - tracer.exit_node( - frame_id, - Outcome::Success { - value_preview: preview_value(&v), - }, - ); - - Signal::Success(v) - } - - Signal::Failure(e) => { - ctx.insert_error(node.database_id, e.clone()); - - tracer.exit_node( - frame_id, - Outcome::Failure { - error_preview: format!("{:#?}", e), - }, - ); - - Signal::Failure(e) - } - - Signal::Return(v) => { - tracer.exit_node( - frame_id, - Outcome::Return { - value_preview: preview_value(&v), - }, - ); - Signal::Return(v) - } - - Signal::Respond(v) => { - tracer.exit_node( - frame_id, - Outcome::Respond { - value_preview: preview_value(&v), - }, - ); - Signal::Respond(v) - } - - Signal::Stop => { - tracer.exit_node(frame_id, Outcome::Stop); - Signal::Stop - } - } - } - - /// Resolve all arguments for a remote call. - /// - /// Remote execution requires concrete values, so any thunks are executed - /// eagerly and replaced with their resulting `Value`. - fn resolve_remote_args( - &self, - args: &mut [Argument], - ctx: &mut Context, - tracer: &mut dyn ExecutionTracer, - parent_frame: u64, - ) -> Result, (Signal, Outcome)> { - let mut values = Vec::with_capacity(args.len()); - - for (i, arg) in args.iter_mut().enumerate() { - match arg { - Argument::Eval(v) => values.push(v.clone()), - Argument::Thunk(id) => { - tracer.mark_thunk(parent_frame, i, true, true); - let (child_sig, child_root) = self.execute_call(*id, ctx, tracer); - if child_root != 0 { - tracer.link_child( - parent_frame, - child_root, - EdgeKind::EagerCall { arg_index: i }, - ); - } - - match child_sig { - Signal::Success(v) => { - *arg = Argument::Eval(v.clone()); - values.push(v); - } - Signal::Return(v) => { - // `return` only exits the child context and yields a value upward. - *arg = Argument::Eval(v.clone()); - values.push(v); - } - s => { - return Err(( - s, - Outcome::Failure { - error_preview: "Eager child failed".into(), - }, - )); - } - } - } - } - } - - Ok(values) - } - - /// Build a remote execution request from a node and its resolved values. - /// - /// Values are mapped to their parameter ids for the remote runtime. - fn build_remote_request( - &self, - node: &NodeFunction, - values: Vec, - ) -> Result { - if node.parameters.len() != values.len() { - return Err(RuntimeError::simple_str( - "RemoteParameterMismatch", - "Remote parameter count mismatch", - )); - } - - let mut fields = HashMap::new(); - for (param, value) in node.parameters.iter().zip(values.into_iter()) { - fields.insert(param.runtime_parameter_id.clone(), value); - } - let id = Uuid::new_v4(); - Ok(ExecutionRequest { - execution_identifier: id.to_string(), - function_identifier: node.runtime_function_id.clone(), - parameters: Some(Struct { fields }), - project_id: 0, - }) - } -} - -fn preview_value(value: &Value) -> String { - format_value_json(value) -} - -fn format_value_json(value: &Value) -> String { - match value.kind.as_ref() { - Some(Kind::NumberValue(v)) => match v.number { - Some(kind) => match kind { - tucana::shared::number_value::Number::Integer(i) => i.to_string(), - tucana::shared::number_value::Number::Float(f) => f.to_string(), - }, - _ => "null".to_string(), - }, - Some(Kind::BoolValue(v)) => v.to_string(), - Some(Kind::StringValue(v)) => format!("{:?}", v), - Some(Kind::NullValue(_)) | None => "null".to_string(), - Some(Kind::ListValue(list)) => { - let mut parts = Vec::new(); - for item in list.values.iter() { - parts.push(format_value_json(item)); - } - format!("[{}]", parts.join(", ")) - } - Some(Kind::StructValue(struct_value)) => { - let mut keys: Vec<_> = struct_value.fields.keys().collect(); - keys.sort(); - let mut parts = Vec::new(); - for key in keys.iter() { - if let Some(v) = struct_value.fields.get(*key) { - parts.push(format!("{:?}: {}", key, format_value_json(v))); - } - } - format!("{{{}}}", parts.join(", ")) - } - } -} - -fn preview_reference(r: &tucana::shared::ReferenceValue) -> String { - let target = match &r.target { - Some(Target::FlowInput(_)) => "flow_input".to_string(), - Some(Target::NodeId(id)) => format!("node({})", id), - Some(Target::InputType(input_type)) => format!( - "input(node={},param={},input={})", - input_type.node_id, input_type.parameter_index, input_type.input_index - ), - None => "empty".to_string(), - }; - - if r.paths.is_empty() { - target - } else { - format!("{}+paths({})", target, r.paths.len()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::context::registry::{HandlerFn, IntoFunctionEntry}; - use std::cell::RefCell; - use tucana::shared::number_value::Number; - use tucana::shared::{NodeFunction, NumberValue, ReferenceValue}; - - fn num(i: i64) -> Value { - Value { - kind: Some(Kind::NumberValue(NumberValue { - number: Some(Number::Integer(i)), - })), - } - } - - fn extract_i64(v: &Value) -> i64 { - match &v.kind { - Some(Kind::NumberValue(NumberValue { - number: Some(Number::Integer(i)), - })) => *i, - other => panic!("Expected integer number value, got {:?}", other), - } - } - - fn node(id: i64, function_id: &str, next: Option) -> NodeFunction { - NodeFunction { - database_id: id, - runtime_function_id: function_id.to_string(), - definition_source: "taurus".to_string(), - next_node_id: next, - ..Default::default() - } - } - - fn node_reference(node_id: i64) -> ReferenceValue { - ReferenceValue { - paths: vec![], - target: Some(Target::NodeId(node_id)), - } - } - - fn assert_node_success(ctx: &mut Context, node_id: i64, expected: i64) { - match ctx.get(node_reference(node_id)) { - ContextResult::Success(v) => assert_eq!(extract_i64(&v), expected), - ContextResult::Error(_) => { - panic!("Expected success result for node {}, got error", node_id) - } - ContextResult::NotFound => { - panic!( - "Expected success result for node {}, got not found", - node_id - ) - } - } - } - - fn assert_node_error(ctx: &mut Context, node_id: i64, expected_name: &str) { - match ctx.get(node_reference(node_id)) { - ContextResult::Error(err) => assert_eq!(err.name, expected_name), - ContextResult::Success(_) => { - panic!("Expected error result for node {}, got success", node_id) - } - ContextResult::NotFound => { - panic!("Expected error result for node {}, got not found", node_id) - } - } - } - - fn assert_node_not_found(ctx: &mut Context, node_id: i64) { - match ctx.get(node_reference(node_id)) { - ContextResult::NotFound => {} - ContextResult::Success(_) => { - panic!("Expected no result for node {}, got success", node_id) - } - ContextResult::Error(_) => { - panic!("Expected no result for node {}, got error", node_id) - } - } - } - - fn success_one_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, - ) -> Signal { - Signal::Success(num(1)) - } - - fn success_two_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, - ) -> Signal { - Signal::Success(num(2)) - } - - fn success_three_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, - ) -> Signal { - Signal::Success(num(3)) - } - - fn failure_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, - ) -> Signal { - Signal::Failure(RuntimeError::simple_str("TestFailure", "expected failure")) - } - - fn return_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, - ) -> Signal { - Signal::Return(num(9)) - } - - fn stop_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, - ) -> Signal { - Signal::Stop - } - - fn respond_eleven_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, - ) -> Signal { - Signal::Respond(num(11)) - } - - fn respond_twentytwo_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, - ) -> Signal { - Signal::Respond(num(22)) - } - - fn respond_fortyfour_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, - ) -> Signal { - Signal::Respond(num(44)) - } - - fn return_seventyseven_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, - ) -> Signal { - Signal::Return(num(77)) - } - - fn runtime_call_child_two_handler( - _args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, - ) -> Signal { - match run(2, ctx) { - Signal::Success(_) => Signal::Success(num(55)), - other => other, - } - } - - fn runtime_call_child_return_handler( - _args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, - ) -> Signal { - run(2, ctx) - } - - #[test] - fn success_signal_continues_through_next_nodes() { - let mut store = FunctionStore::new(); - store.populate(vec![ - ( - "test::success_one", - HandlerFn::eager(success_one_handler, 0), - ), - ( - "test::success_two", - HandlerFn::eager(success_two_handler, 0), - ), - ]); - - let mut nodes = HashMap::new(); - nodes.insert(1, node(1, "test::success_one", Some(2))); - nodes.insert(2, node(2, "test::success_two", None)); - - let mut ctx = Context::default(); - let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); - - match signal { - Signal::Success(v) => assert_eq!(extract_i64(&v), 2), - other => panic!("Expected success signal, got {:?}", other), - } - - assert_node_success(&mut ctx, 1, 1); - assert_node_success(&mut ctx, 2, 2); - } - - #[test] - fn failure_signal_stops_flow_and_persists_error() { - let mut store = FunctionStore::new(); - store.populate(vec![ - ( - "test::success_one", - HandlerFn::eager(success_one_handler, 0), - ), - ("test::failure", HandlerFn::eager(failure_handler, 0)), - ( - "test::success_three", - HandlerFn::eager(success_three_handler, 0), - ), - ]); - - let mut nodes = HashMap::new(); - nodes.insert(1, node(1, "test::success_one", Some(2))); - nodes.insert(2, node(2, "test::failure", Some(3))); - nodes.insert(3, node(3, "test::success_three", None)); - - let mut ctx = Context::default(); - let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); - - match signal { - Signal::Failure(err) => assert_eq!(err.name, "TestFailure"), - other => panic!("Expected failure signal, got {:?}", other), - } - - assert_node_success(&mut ctx, 1, 1); - assert_node_error(&mut ctx, 2, "TestFailure"); - assert_node_not_found(&mut ctx, 3); - } - - #[test] - fn return_signal_breaks_execution_and_skips_next_nodes() { - let mut store = FunctionStore::new(); - store.populate(vec![ - ("test::return", HandlerFn::eager(return_handler, 0)), - ( - "test::success_three", - HandlerFn::eager(success_three_handler, 0), - ), - ]); - - let mut nodes = HashMap::new(); - nodes.insert(1, node(1, "test::return", Some(2))); - nodes.insert(2, node(2, "test::success_three", None)); - - let mut ctx = Context::default(); - let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); - - match signal { - Signal::Return(v) => assert_eq!(extract_i64(&v), 9), - other => panic!("Expected return signal, got {:?}", other), - } - - assert_node_not_found(&mut ctx, 1); - assert_node_not_found(&mut ctx, 2); - } - - #[test] - fn stop_signal_stops_execution_and_skips_next_nodes() { - let mut store = FunctionStore::new(); - store.populate(vec![ - ("test::stop", HandlerFn::eager(stop_handler, 0)), - ( - "test::success_three", - HandlerFn::eager(success_three_handler, 0), - ), - ]); - - let mut nodes = HashMap::new(); - nodes.insert(1, node(1, "test::stop", Some(2))); - nodes.insert(2, node(2, "test::success_three", None)); - - let mut ctx = Context::default(); - let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); - - assert!(matches!(signal, Signal::Stop)); - assert_node_not_found(&mut ctx, 1); - assert_node_not_found(&mut ctx, 2); - } - - #[test] - fn respond_signal_emits_for_each_node_and_execution_continues() { - let mut store = FunctionStore::new(); - store.populate(vec![ - ( - "test::respond_eleven", - HandlerFn::eager(respond_eleven_handler, 0), - ), - ( - "test::respond_twentytwo", - HandlerFn::eager(respond_twentytwo_handler, 0), - ), - ( - "test::success_three", - HandlerFn::eager(success_three_handler, 0), - ), - ]); - - let mut nodes = HashMap::new(); - nodes.insert(1, node(1, "test::respond_eleven", Some(2))); - nodes.insert(2, node(2, "test::respond_twentytwo", Some(3))); - nodes.insert(3, node(3, "test::success_three", None)); - - let emitted = RefCell::new(Vec::::new()); - let emitter = |v: Value| { - emitted.borrow_mut().push(v); - }; - - let mut ctx = Context::default(); - let signal = Executor::new(&store, nodes) - .with_respond_emitter(&emitter) - .execute(1, &mut ctx, false); - - match signal { - Signal::Success(v) => assert_eq!(extract_i64(&v), 3), - other => panic!("Expected final success signal, got {:?}", other), - } - - let emitted_values = emitted.borrow(); - assert_eq!(emitted_values.len(), 2); - assert_eq!(extract_i64(&emitted_values[0]), 11); - assert_eq!(extract_i64(&emitted_values[1]), 22); - - assert_node_success(&mut ctx, 1, 11); - assert_node_success(&mut ctx, 2, 22); - assert_node_success(&mut ctx, 3, 3); - } - - #[test] - fn respond_signal_emits_inside_runtime_call_and_parent_continues() { - let mut store = FunctionStore::new(); - store.populate(vec![ - ( - "test::runtime_call_child_two", - HandlerFn::eager(runtime_call_child_two_handler, 0), - ), - ( - "test::respond_fortyfour", - HandlerFn::eager(respond_fortyfour_handler, 0), - ), - ]); - - let mut nodes = HashMap::new(); - nodes.insert(1, node(1, "test::runtime_call_child_two", None)); - nodes.insert(2, node(2, "test::respond_fortyfour", None)); - - let emitted = RefCell::new(Vec::::new()); - let emitter = |v: Value| { - emitted.borrow_mut().push(v); - }; - - let mut ctx = Context::default(); - let signal = Executor::new(&store, nodes) - .with_respond_emitter(&emitter) - .execute(1, &mut ctx, false); - - match signal { - Signal::Success(v) => assert_eq!(extract_i64(&v), 55), - other => panic!("Expected final success signal, got {:?}", other), - } - - let emitted_values = emitted.borrow(); - assert_eq!(emitted_values.len(), 1); - assert_eq!(extract_i64(&emitted_values[0]), 44); - - assert_node_success(&mut ctx, 1, 55); - assert_node_success(&mut ctx, 2, 44); - } - - #[test] - fn return_signal_in_child_context_continues_parent_flow() { - let mut store = FunctionStore::new(); - store.populate(vec![ - ( - "test::runtime_call_child_return", - HandlerFn::eager(runtime_call_child_return_handler, 0), - ), - ( - "test::return_seventyseven", - HandlerFn::eager(return_seventyseven_handler, 0), - ), - ( - "test::success_three", - HandlerFn::eager(success_three_handler, 0), - ), - ]); - - let mut nodes = HashMap::new(); - nodes.insert(1, node(1, "test::runtime_call_child_return", Some(3))); - nodes.insert(2, node(2, "test::return_seventyseven", None)); - nodes.insert(3, node(3, "test::success_three", None)); - - let mut ctx = Context::default(); - let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); - - match signal { - Signal::Success(v) => assert_eq!(extract_i64(&v), 3), - other => panic!("Expected final success signal, got {:?}", other), - } - - // Child return exits its own context and is treated as value by parent node. - assert_node_success(&mut ctx, 1, 77); - assert_node_not_found(&mut ctx, 2); - assert_node_success(&mut ctx, 3, 3); - } - - #[test] - fn child_return_skips_child_next_but_parent_next_runs() { - // Graph: - // node1 -> next node4, and node1 executes child node2 via run(...) - // node2 -> RETURN and has next node3 - // node3 must NOT run (because node2 returned) - // node4 must run (parent chain continues) - let mut store = FunctionStore::new(); - store.populate(vec![ - ( - "test::runtime_call_child_return", - HandlerFn::eager(runtime_call_child_return_handler, 0), - ), - ( - "test::return_seventyseven", - HandlerFn::eager(return_seventyseven_handler, 0), - ), - ( - "test::success_two", - HandlerFn::eager(success_two_handler, 0), - ), - ( - "test::success_three", - HandlerFn::eager(success_three_handler, 0), - ), - ]); - - let mut nodes = HashMap::new(); - nodes.insert(1, node(1, "test::runtime_call_child_return", Some(4))); - nodes.insert(2, node(2, "test::return_seventyseven", Some(3))); - nodes.insert(3, node(3, "test::success_two", None)); - nodes.insert(4, node(4, "test::success_three", None)); - - let mut ctx = Context::default(); - let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); - - match signal { - Signal::Success(v) => assert_eq!(extract_i64(&v), 3), - other => panic!("Expected final success signal, got {:?}", other), - } - - // node1 ran and received child return value - assert_node_success(&mut ctx, 1, 77); - // node2 returned, so it has no success/error entry in context - assert_node_not_found(&mut ctx, 2); - // node3 is child next node and must be skipped due to return in node2 - assert_node_not_found(&mut ctx, 3); - // parent next node still executes - assert_node_success(&mut ctx, 4, 3); - } -} diff --git a/crates/core/src/context/executor/arguments.rs b/crates/core/src/context/executor/arguments.rs new file mode 100644 index 0000000..1d30b17 --- /dev/null +++ b/crates/core/src/context/executor/arguments.rs @@ -0,0 +1,242 @@ +use super::*; + +impl<'a> Executor<'a> { + /// Build arguments for a node from literals, references, or thunks. + /// + /// Arguments are recorded to the tracer for debugging and inspection. + /// Thunks are represented by the referenced node id. + pub(super) fn build_args( + &self, + node: &NodeFunction, + ctx: &mut Context, + tracer: &mut dyn ExecutionTracer, + frame_id: u64, + ) -> Result, RuntimeError> { + let mut args = Vec::with_capacity(node.parameters.len()); + + for (i, param) in node.parameters.iter().enumerate() { + let node_value = param.value.as_ref().ok_or_else(|| { + RuntimeError::simple_str("NodeValueNotFound", "Missing param value") + })?; + + let value = node_value.value.as_ref().ok_or_else(|| { + RuntimeError::simple_str("NodeValueNotFound", "Missing inner value") + })?; + + match value { + tucana::shared::node_value::Value::LiteralValue(v) => { + tracer.record_arg( + frame_id, + ArgTrace { + index: i, + kind: ArgKind::Literal, + preview: preview_value(v), + }, + ); + args.push(Argument::Eval(v.clone())); + } + + tucana::shared::node_value::Value::ReferenceValue(r) => match ctx.get(r.clone()) { + ContextResult::Success(v) => { + let reference = match r.target { + Some(ref_value) => match ref_value { + tucana::shared::reference_value::Target::FlowInput(_) => { + ReferenceKind::FlowInput + } + tucana::shared::reference_value::Target::NodeId(id) => { + ReferenceKind::Result { node_id: id } + } + tucana::shared::reference_value::Target::InputType(input_type) => { + ReferenceKind::InputType { + node_id: input_type.node_id, + input_index: input_type.input_index, + parameter_index: input_type.parameter_index, + } + } + }, + None => ReferenceKind::Empty, + }; + + tracer.record_arg( + frame_id, + ArgTrace { + index: i, + kind: ArgKind::Reference { + reference, + hit: true, + }, + preview: format!( + "ctx.get({}) -> {}", + preview_reference(r), + preview_value(&v) + ), + }, + ); + args.push(Argument::Eval(v)); + } + ContextResult::Error(e) => return Err(e), + ContextResult::NotFound => { + return Err(RuntimeError::simple_str( + "ReferenceValueNotFound", + "Reference not found in context", + )); + } + }, + + tucana::shared::node_value::Value::NodeFunctionId(id) => { + tracer.record_arg( + frame_id, + ArgTrace { + index: i, + kind: ArgKind::Thunk { + node_id: *id, + eager: false, + executed: false, + }, + preview: format!("thunk({})", id), + }, + ); + args.push(Argument::Thunk(*id)); + } + } + } + + Ok(args) + } + + /// Eagerly execute any argument marked as `Eager`. + /// + /// Lazy arguments are preserved as thunks until needed by a handler. + /// If an eager child fails, the failure bubbles up immediately. + pub(super) fn force_eager_args( + &self, + _node: &NodeFunction, + entry: &HandlerFunctionEntry, + args: &mut [Argument], + ctx: &mut Context, + tracer: &mut dyn ExecutionTracer, + parent_frame: u64, + ) -> Result<(), (Signal, Outcome)> { + for (i, arg) in args.iter_mut().enumerate() { + let mode = entry + .param_modes + .get(i) + .copied() + .unwrap_or(ParameterNode::Eager); + + if matches!(mode, ParameterNode::Eager) + && let Argument::Thunk(id) = *arg + { + tracer.mark_thunk(parent_frame, i, true, true); + let (child_sig, child_root) = self.execute_call(id, ctx, tracer); + + tracer.link_child( + parent_frame, + child_root, + EdgeKind::EagerCall { arg_index: i }, + ); + + match child_sig { + Signal::Success(v) => { + *arg = Argument::Eval(v); + } + Signal::Return(v) => { + // `return` only exits the child context and yields a value upward. + *arg = Argument::Eval(v); + } + s => { + return Err(( + s, + Outcome::Failure { + error_preview: "Eager child failed".into(), + }, + )); + } + } + } + } + + Ok(()) + } + + /// Resolve all arguments for a remote call. + /// + /// Remote execution requires concrete values, so any thunks are executed + /// eagerly and replaced with their resulting `Value`. + pub(super) fn resolve_remote_args( + &self, + args: &mut [Argument], + ctx: &mut Context, + tracer: &mut dyn ExecutionTracer, + parent_frame: u64, + ) -> Result, (Signal, Outcome)> { + let mut values = Vec::with_capacity(args.len()); + + for (i, arg) in args.iter_mut().enumerate() { + match arg { + Argument::Eval(v) => values.push(v.clone()), + Argument::Thunk(id) => { + tracer.mark_thunk(parent_frame, i, true, true); + let (child_sig, child_root) = self.execute_call(*id, ctx, tracer); + if child_root != 0 { + tracer.link_child( + parent_frame, + child_root, + EdgeKind::EagerCall { arg_index: i }, + ); + } + + match child_sig { + Signal::Success(v) => { + *arg = Argument::Eval(v.clone()); + values.push(v); + } + Signal::Return(v) => { + // `return` only exits the child context and yields a value upward. + *arg = Argument::Eval(v.clone()); + values.push(v); + } + s => { + return Err(( + s, + Outcome::Failure { + error_preview: "Eager child failed".into(), + }, + )); + } + } + } + } + } + + Ok(values) + } + + /// Build a remote execution request from a node and its resolved values. + /// + /// Values are mapped to their parameter ids for the remote runtime. + pub(super) fn build_remote_request( + &self, + node: &NodeFunction, + values: Vec, + ) -> Result { + if node.parameters.len() != values.len() { + return Err(RuntimeError::simple_str( + "RemoteParameterMismatch", + "Remote parameter count mismatch", + )); + } + + let mut fields = HashMap::new(); + for (param, value) in node.parameters.iter().zip(values.into_iter()) { + fields.insert(param.runtime_parameter_id.clone(), value); + } + let id = Uuid::new_v4(); + Ok(ExecutionRequest { + execution_identifier: id.to_string(), + function_identifier: node.runtime_function_id.clone(), + parameters: Some(Struct { fields }), + project_id: 0, + }) + } +} diff --git a/crates/core/src/context/executor/execution_loop.rs b/crates/core/src/context/executor/execution_loop.rs new file mode 100644 index 0000000..7c73d51 --- /dev/null +++ b/crates/core/src/context/executor/execution_loop.rs @@ -0,0 +1,103 @@ +use super::*; + +#[derive(Debug)] +struct CallState { + current_node_id: i64, + call_root_frame: Option, + previous_frame: Option, +} + +impl CallState { + fn new(start_node_id: i64) -> Self { + Self { + current_node_id: start_node_id, + call_root_frame: None, + previous_frame: None, + } + } + + fn set_root_if_missing(&mut self, frame_id: u64) { + if self.call_root_frame.is_none() { + self.call_root_frame = Some(frame_id); + } + } + + fn link_next_edge(&mut self, frame_id: u64, tracer: &mut dyn ExecutionTracer) { + if let Some(prev) = self.previous_frame { + tracer.link_child(prev, frame_id, EdgeKind::Next); + } + self.previous_frame = Some(frame_id); + } + + fn advance(&mut self, next_node_id: Option) -> bool { + if let Some(next) = next_node_id { + self.current_node_id = next; + true + } else { + false + } + } + + fn root_frame(&self) -> u64 { + self.call_root_frame + .expect("call root frame must be set after first node execution") + } +} + +impl<'a> Executor<'a> { + /// Main execution loop. + /// + /// Executes nodes one-by-one along the `next_node_id` chain until a + /// non-success signal is produced or the chain ends. + pub(super) fn execute_call( + &self, + start_node_id: i64, + ctx: &mut Context, + tracer: &mut dyn ExecutionTracer, + ) -> (Signal, u64) { + let mut state = CallState::new(start_node_id); + + loop { + let (signal, frame_id) = self.execute_single_node(state.current_node_id, ctx, tracer); + state.set_root_if_missing(frame_id); + state.link_next_edge(frame_id, tracer); + + match signal { + Signal::Success(_) => { + let node = self + .nodes + .get(&state.current_node_id) + .expect("current node must exist while executing success path"); + + if state.advance(node.next_node_id) { + continue; + } + + return (signal, state.root_frame()); + } + + Signal::Respond(v) => { + let node = self + .nodes + .get(&state.current_node_id) + .expect("current node must exist while executing respond path"); + + if let Some(emit) = self.respond_emitter { + emit(v.clone()); + } + + // Respond behaves like success for flow continuation and references. + ctx.insert_success(node.database_id, v.clone()); + + if state.advance(node.next_node_id) { + continue; + } + + return (Signal::Success(v), state.root_frame()); + } + + _ => return (signal, state.root_frame()), + } + } + } +} diff --git a/crates/core/src/context/executor/formatting.rs b/crates/core/src/context/executor/formatting.rs new file mode 100644 index 0000000..e2d912c --- /dev/null +++ b/crates/core/src/context/executor/formatting.rs @@ -0,0 +1,56 @@ +use super::*; + +pub(super) fn preview_value(value: &Value) -> String { + format_value_json(value) +} + +fn format_value_json(value: &Value) -> String { + match value.kind.as_ref() { + Some(Kind::NumberValue(v)) => match v.number { + Some(kind) => match kind { + tucana::shared::number_value::Number::Integer(i) => i.to_string(), + tucana::shared::number_value::Number::Float(f) => f.to_string(), + }, + _ => "null".to_string(), + }, + Some(Kind::BoolValue(v)) => v.to_string(), + Some(Kind::StringValue(v)) => format!("{:?}", v), + Some(Kind::NullValue(_)) | None => "null".to_string(), + Some(Kind::ListValue(list)) => { + let mut parts = Vec::new(); + for item in list.values.iter() { + parts.push(format_value_json(item)); + } + format!("[{}]", parts.join(", ")) + } + Some(Kind::StructValue(struct_value)) => { + let mut keys: Vec<_> = struct_value.fields.keys().collect(); + keys.sort(); + let mut parts = Vec::new(); + for key in keys.iter() { + if let Some(v) = struct_value.fields.get(*key) { + parts.push(format!("{:?}: {}", key, format_value_json(v))); + } + } + format!("{{{}}}", parts.join(", ")) + } + } +} + +pub(super) fn preview_reference(r: &tucana::shared::ReferenceValue) -> String { + let target = match &r.target { + Some(Target::FlowInput(_)) => "flow_input".to_string(), + Some(Target::NodeId(id)) => format!("node({})", id), + Some(Target::InputType(input_type)) => format!( + "input(node={},param={},input={})", + input_type.node_id, input_type.parameter_index, input_type.input_index + ), + None => "empty".to_string(), + }; + + if r.paths.is_empty() { + target + } else { + format!("{}+paths({})", target, r.paths.len()) + } +} diff --git a/crates/core/src/context/executor/mod.rs b/crates/core/src/context/executor/mod.rs new file mode 100644 index 0000000..5588c4e --- /dev/null +++ b/crates/core/src/context/executor/mod.rs @@ -0,0 +1,126 @@ +//! Executor for flow node execution. +//! +//! Execution model overview: +//! - The executor walks a linear "next" chain starting from `starting_node_id`. +//! - Each node can call into other nodes through lazy arguments. +//! - A node marked as remote is executed via `RemoteRuntime`. +//! - The executor is synchronous; remote calls are awaited via `block_on`. +//! +//! Remote execution: +//! - A node is considered remote based on `is_remote(&node)`. +//! - Remote args are fully resolved to concrete `Value`s before sending. +//! - The request parameters are mapped by `runtime_parameter_id`. +//! - Remote responses are mapped into `Signal::Success` or `Signal::Failure`. +//! +//! Tracing: +//! - Each node execution produces a trace frame with arguments and outcome. +//! - Child executions are linked with `EdgeKind` to reflect eager or runtime calls. +//! +//! Error behavior: +//! - Missing nodes/functions yield `Signal::Failure`. +//! - Remote failures are mapped to `RuntimeError`. +//! - The executor commits all final outcomes into the `Context`. + +use crate::context::argument::{Argument, ParameterNode}; +use crate::context::context::{Context, ContextResult}; +use crate::context::registry::{FunctionStore, HandlerFunctionEntry}; +use crate::context::signal::Signal; +use crate::debug::trace::{ArgKind, ArgTrace, EdgeKind, Outcome, ReferenceKind}; +use crate::debug::tracer::{ExecutionTracer, Tracer}; +use crate::runtime::error::RuntimeError; +use crate::runtime::remote::RemoteRuntime; + +use futures_lite::future::block_on; +use std::collections::HashMap; +use tucana::aquila::ExecutionRequest; +use tucana::shared::reference_value::Target; +use tucana::shared::value::Kind; +use tucana::shared::{NodeFunction, Struct, Value}; +use uuid::Uuid; + +mod arguments; +mod execution_loop; +mod formatting; +mod node_execution; + +use formatting::{preview_reference, preview_value}; + +#[cfg(test)] +mod tests; + +/// Executes a flow graph by repeatedly evaluating nodes. +/// +/// The executor is intentionally stateless with respect to the runtime: +/// it borrows the function registry and graph, and mutates only the `Context`. +pub struct Executor<'a> { + // Registered Runtime Functions + functions: &'a FunctionStore, + // Nodes to execute + nodes: HashMap, + // Connection for Remote Function Execution => Actions + remote: Option<&'a dyn RemoteRuntime>, + // Optional side-effect hook triggered whenever a respond signal is emitted. + respond_emitter: Option<&'a dyn Fn(Value)>, +} + +/// Determines whether a node should be executed remotely. +/// +/// The current policy treats any node whose `definition_source` is not `"taurus"` +/// as a remote node. +fn is_remote(node: &NodeFunction) -> bool { + if node.definition_source.is_empty() { + log::warn!( + "Found empty definition source, taking runtime as origin for node id: {}", + node.database_id + ); + return false; + } + + node.definition_source != "taurus" +} + +impl<'a> Executor<'a> { + /// Create a new executor for the given function store and node map. + /// + /// This does not attach a remote runtime. Remote nodes will error unless + /// a runtime is provided via `with_remote_runtime`. + pub fn new(functions: &'a FunctionStore, nodes: HashMap) -> Self { + Self { + functions, + nodes, + remote: None, + respond_emitter: None, + } + } + + /// Attach a remote runtime for executing nodes marked as remote. + /// + /// This is a builder-style method for ergonomic setup: + /// `Executor::new(...).with_remote_runtime(&runtime)`. + pub fn with_remote_runtime(mut self, remote: &'a dyn RemoteRuntime) -> Self { + self.remote = Some(remote); + self + } + + /// Attach a callback that is invoked for every emitted respond value. + pub fn with_respond_emitter(mut self, emitter: &'a dyn Fn(Value)) -> Self { + self.respond_emitter = Some(emitter); + self + } + + /// This is now the ONLY execution entry point. + /// + /// - `start_node_id` is the first node in the flow. + /// - `ctx` is mutated in-place with results and errors. + /// - `with_trace` controls whether the trace is printed on completion. + pub fn execute(&self, start_node_id: i64, ctx: &mut Context, with_trace: bool) -> Signal { + let mut tracer = Tracer::new(); + + let (signal, _root_frame) = self.execute_call(start_node_id, ctx, &mut tracer); + + if with_trace && let Some(run) = tracer.take_run() { + println!("{}", crate::debug::render::render_trace(&run)); + } + signal + } +} diff --git a/crates/core/src/context/executor/node_execution.rs b/crates/core/src/context/executor/node_execution.rs new file mode 100644 index 0000000..1c916a8 --- /dev/null +++ b/crates/core/src/context/executor/node_execution.rs @@ -0,0 +1,258 @@ +use super::*; + +struct NodeHandler<'executor, 'deps> { + executor: &'executor Executor<'deps>, + node: NodeFunction, +} + +impl<'executor, 'deps> NodeHandler<'executor, 'deps> { + fn new(executor: &'executor Executor<'deps>, node: NodeFunction) -> Self { + Self { executor, node } + } + + fn execute(&self, ctx: &mut Context, tracer: &mut dyn ExecutionTracer) -> (Signal, u64) { + if is_remote(&self.node) { + self.execute_remote(ctx, tracer) + } else { + self.execute_local(ctx, tracer) + } + } + + fn execute_remote(&self, ctx: &mut Context, tracer: &mut dyn ExecutionTracer) -> (Signal, u64) { + let remote = match self.executor.remote { + Some(r) => r, + None => { + let err = RuntimeError::simple( + "RemoteRuntimeNotConfigured", + "Remote runtime not configured".to_string(), + ); + return (Signal::Failure(err), 0); + } + }; + + let frame_id = tracer.enter_node( + self.node.database_id, + self.node.runtime_function_id.as_str(), + ); + + let mut args = match self.executor.build_args(&self.node, ctx, tracer, frame_id) { + Ok(a) => a, + Err(e) => { + ctx.insert_error(self.node.database_id, e.clone()); + tracer.exit_node( + frame_id, + Outcome::Failure { + error_preview: format!("{:#?}", e), + }, + ); + return (Signal::Failure(e), frame_id); + } + }; + + let values = match self + .executor + .resolve_remote_args(&mut args, ctx, tracer, frame_id) + { + Ok(v) => v, + Err((sig, outcome)) => { + tracer.exit_node(frame_id, outcome); + return (sig, frame_id); + } + }; + + let request = match self.executor.build_remote_request(&self.node, values) { + Ok(r) => r, + Err(e) => { + ctx.insert_error(self.node.database_id, e.clone()); + tracer.exit_node( + frame_id, + Outcome::Failure { + error_preview: format!("{:#?}", e), + }, + ); + return (Signal::Failure(e), frame_id); + } + }; + + let remote_result = + block_on(remote.execute_remote(self.node.definition_source.clone(), request)); + let signal = match remote_result { + Ok(value) => Signal::Success(value), + Err(err) => Signal::Failure(err), + }; + + let final_signal = self.commit_result(signal, ctx, tracer, frame_id); + (final_signal, frame_id) + } + + fn execute_local(&self, ctx: &mut Context, tracer: &mut dyn ExecutionTracer) -> (Signal, u64) { + let entry = match self + .executor + .functions + .get(self.node.runtime_function_id.as_str()) + { + Some(e) => e, + None => { + let err = RuntimeError::simple( + "FunctionNotFound", + format!("Function {} not found", self.node.runtime_function_id), + ); + return (Signal::Failure(err), 0); + } + }; + + let frame_id = tracer.enter_node( + self.node.database_id, + self.node.runtime_function_id.as_str(), + ); + + // ---- Build args + let mut args = match self.executor.build_args(&self.node, ctx, tracer, frame_id) { + Ok(a) => a, + Err(e) => { + ctx.insert_error(self.node.database_id, e.clone()); + tracer.exit_node( + frame_id, + Outcome::Failure { + error_preview: format!("{:#?}", e), + }, + ); + return (Signal::Failure(e), frame_id); + } + }; + + // ---- Force eager args + if let Err((sig, outcome)) = self + .executor + .force_eager_args(&self.node, entry, &mut args, ctx, tracer, frame_id) + { + tracer.exit_node(frame_id, outcome); + return (sig, frame_id); + } + + // ---- Invoke handler + let result = self.invoke_handler(entry, &args, ctx, tracer, frame_id); + + // ---- Commit result + let final_signal = self.commit_result(result, ctx, tracer, frame_id); + (final_signal, frame_id) + } + + /// Invoke a local handler with a closure for lazy execution. + /// + /// The closure will evaluate a thunk node and link its trace to the + /// current execution frame. + fn invoke_handler( + &self, + entry: &HandlerFunctionEntry, + args: &[Argument], + ctx: &mut Context, + tracer: &mut dyn ExecutionTracer, + frame_id: u64, + ) -> Signal { + let executor = self.executor; + let mut run = |node_id: i64, ctx: &mut Context| { + tracer.mark_thunk_executed_by_node(frame_id, node_id); + let label = ctx.pop_runtime_trace_label(); + let (sig, child_root) = executor.execute_call(node_id, ctx, tracer); + tracer.link_child(frame_id, child_root, EdgeKind::RuntimeCall { label }); + match sig { + // `return` only exits the child context and is a value for the caller. + Signal::Return(v) => Signal::Success(v), + other => other, + } + }; + + (entry.handler)(args, ctx, &mut run) + } + + /// Persist the final signal into the context and close the trace frame. + fn commit_result( + &self, + result: Signal, + ctx: &mut Context, + tracer: &mut dyn ExecutionTracer, + frame_id: u64, + ) -> Signal { + match result { + Signal::Success(v) => { + ctx.insert_success(self.node.database_id, v.clone()); + + tracer.exit_node( + frame_id, + Outcome::Success { + value_preview: preview_value(&v), + }, + ); + + Signal::Success(v) + } + + Signal::Failure(e) => { + ctx.insert_error(self.node.database_id, e.clone()); + + tracer.exit_node( + frame_id, + Outcome::Failure { + error_preview: format!("{:#?}", e), + }, + ); + + Signal::Failure(e) + } + + Signal::Return(v) => { + tracer.exit_node( + frame_id, + Outcome::Return { + value_preview: preview_value(&v), + }, + ); + Signal::Return(v) + } + + Signal::Respond(v) => { + tracer.exit_node( + frame_id, + Outcome::Respond { + value_preview: preview_value(&v), + }, + ); + Signal::Respond(v) + } + + Signal::Stop => { + tracer.exit_node(frame_id, Outcome::Stop); + Signal::Stop + } + } + } +} + +impl<'a> Executor<'a> { + /// Execute a single node and return its signal and trace frame id. + /// + /// This handles: + /// - Node lookup + /// - Remote vs local dispatch + /// - Argument building and eager evaluation + /// - Handler invocation and result commit + pub(super) fn execute_single_node( + &self, + node_id: i64, + ctx: &mut Context, + tracer: &mut dyn ExecutionTracer, + ) -> (Signal, u64) { + ctx.set_current_node_id(node_id); + let node = match self.nodes.get(&node_id) { + Some(n) => n.clone(), + None => { + let err = + RuntimeError::simple("NodeNotFound", format!("Node {} not found", node_id)); + return (Signal::Failure(err), 0); + } + }; + + NodeHandler::new(self, node).execute(ctx, tracer) + } +} diff --git a/crates/core/src/context/executor/tests.rs b/crates/core/src/context/executor/tests.rs new file mode 100644 index 0000000..7e23006 --- /dev/null +++ b/crates/core/src/context/executor/tests.rs @@ -0,0 +1,467 @@ +use super::*; +use crate::context::registry::{HandlerFn, IntoFunctionEntry}; +use std::cell::RefCell; +use tucana::shared::number_value::Number; +use tucana::shared::{NodeFunction, NumberValue, ReferenceValue}; + +fn num(i: i64) -> Value { + Value { + kind: Some(Kind::NumberValue(NumberValue { + number: Some(Number::Integer(i)), + })), + } +} + +fn extract_i64(v: &Value) -> i64 { + match &v.kind { + Some(Kind::NumberValue(NumberValue { + number: Some(Number::Integer(i)), + })) => *i, + other => panic!("Expected integer number value, got {:?}", other), + } +} + +fn node(id: i64, function_id: &str, next: Option) -> NodeFunction { + NodeFunction { + database_id: id, + runtime_function_id: function_id.to_string(), + definition_source: "taurus".to_string(), + next_node_id: next, + ..Default::default() + } +} + +fn node_reference(node_id: i64) -> ReferenceValue { + ReferenceValue { + paths: vec![], + target: Some(Target::NodeId(node_id)), + } +} + +fn assert_node_success(ctx: &mut Context, node_id: i64, expected: i64) { + match ctx.get(node_reference(node_id)) { + ContextResult::Success(v) => assert_eq!(extract_i64(&v), expected), + ContextResult::Error(_) => { + panic!("Expected success result for node {}, got error", node_id) + } + ContextResult::NotFound => { + panic!( + "Expected success result for node {}, got not found", + node_id + ) + } + } +} + +fn assert_node_error(ctx: &mut Context, node_id: i64, expected_name: &str) { + match ctx.get(node_reference(node_id)) { + ContextResult::Error(err) => assert_eq!(err.name, expected_name), + ContextResult::Success(_) => { + panic!("Expected error result for node {}, got success", node_id) + } + ContextResult::NotFound => { + panic!("Expected error result for node {}, got not found", node_id) + } + } +} + +fn assert_node_not_found(ctx: &mut Context, node_id: i64) { + match ctx.get(node_reference(node_id)) { + ContextResult::NotFound => {} + ContextResult::Success(_) => { + panic!("Expected no result for node {}, got success", node_id) + } + ContextResult::Error(_) => { + panic!("Expected no result for node {}, got error", node_id) + } + } +} + +fn success_one_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, +) -> Signal { + Signal::Success(num(1)) +} + +fn success_two_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, +) -> Signal { + Signal::Success(num(2)) +} + +fn success_three_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, +) -> Signal { + Signal::Success(num(3)) +} + +fn failure_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, +) -> Signal { + Signal::Failure(RuntimeError::simple_str("TestFailure", "expected failure")) +} + +fn return_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, +) -> Signal { + Signal::Return(num(9)) +} + +fn stop_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, +) -> Signal { + Signal::Stop +} + +fn respond_eleven_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, +) -> Signal { + Signal::Respond(num(11)) +} + +fn respond_twentytwo_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, +) -> Signal { + Signal::Respond(num(22)) +} + +fn respond_fortyfour_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, +) -> Signal { + Signal::Respond(num(44)) +} + +fn return_seventyseven_handler( + _args: &[Argument], + _ctx: &mut Context, + _run: &mut dyn FnMut(i64, &mut Context) -> Signal, +) -> Signal { + Signal::Return(num(77)) +} + +fn runtime_call_child_two_handler( + _args: &[Argument], + ctx: &mut Context, + run: &mut dyn FnMut(i64, &mut Context) -> Signal, +) -> Signal { + match run(2, ctx) { + Signal::Success(_) => Signal::Success(num(55)), + other => other, + } +} + +fn runtime_call_child_return_handler( + _args: &[Argument], + ctx: &mut Context, + run: &mut dyn FnMut(i64, &mut Context) -> Signal, +) -> Signal { + run(2, ctx) +} + +#[test] +fn success_signal_continues_through_next_nodes() { + let mut store = FunctionStore::new(); + store.populate(vec![ + ( + "test::success_one", + HandlerFn::eager(success_one_handler, 0), + ), + ( + "test::success_two", + HandlerFn::eager(success_two_handler, 0), + ), + ]); + + let mut nodes = HashMap::new(); + nodes.insert(1, node(1, "test::success_one", Some(2))); + nodes.insert(2, node(2, "test::success_two", None)); + + let mut ctx = Context::default(); + let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); + + match signal { + Signal::Success(v) => assert_eq!(extract_i64(&v), 2), + other => panic!("Expected success signal, got {:?}", other), + } + + assert_node_success(&mut ctx, 1, 1); + assert_node_success(&mut ctx, 2, 2); +} + +#[test] +fn failure_signal_stops_flow_and_persists_error() { + let mut store = FunctionStore::new(); + store.populate(vec![ + ( + "test::success_one", + HandlerFn::eager(success_one_handler, 0), + ), + ("test::failure", HandlerFn::eager(failure_handler, 0)), + ( + "test::success_three", + HandlerFn::eager(success_three_handler, 0), + ), + ]); + + let mut nodes = HashMap::new(); + nodes.insert(1, node(1, "test::success_one", Some(2))); + nodes.insert(2, node(2, "test::failure", Some(3))); + nodes.insert(3, node(3, "test::success_three", None)); + + let mut ctx = Context::default(); + let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); + + match signal { + Signal::Failure(err) => assert_eq!(err.name, "TestFailure"), + other => panic!("Expected failure signal, got {:?}", other), + } + + assert_node_success(&mut ctx, 1, 1); + assert_node_error(&mut ctx, 2, "TestFailure"); + assert_node_not_found(&mut ctx, 3); +} + +#[test] +fn return_signal_breaks_execution_and_skips_next_nodes() { + let mut store = FunctionStore::new(); + store.populate(vec![ + ("test::return", HandlerFn::eager(return_handler, 0)), + ( + "test::success_three", + HandlerFn::eager(success_three_handler, 0), + ), + ]); + + let mut nodes = HashMap::new(); + nodes.insert(1, node(1, "test::return", Some(2))); + nodes.insert(2, node(2, "test::success_three", None)); + + let mut ctx = Context::default(); + let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); + + match signal { + Signal::Return(v) => assert_eq!(extract_i64(&v), 9), + other => panic!("Expected return signal, got {:?}", other), + } + + assert_node_not_found(&mut ctx, 1); + assert_node_not_found(&mut ctx, 2); +} + +#[test] +fn stop_signal_stops_execution_and_skips_next_nodes() { + let mut store = FunctionStore::new(); + store.populate(vec![ + ("test::stop", HandlerFn::eager(stop_handler, 0)), + ( + "test::success_three", + HandlerFn::eager(success_three_handler, 0), + ), + ]); + + let mut nodes = HashMap::new(); + nodes.insert(1, node(1, "test::stop", Some(2))); + nodes.insert(2, node(2, "test::success_three", None)); + + let mut ctx = Context::default(); + let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); + + assert!(matches!(signal, Signal::Stop)); + assert_node_not_found(&mut ctx, 1); + assert_node_not_found(&mut ctx, 2); +} + +#[test] +fn respond_signal_emits_for_each_node_and_execution_continues() { + let mut store = FunctionStore::new(); + store.populate(vec![ + ( + "test::respond_eleven", + HandlerFn::eager(respond_eleven_handler, 0), + ), + ( + "test::respond_twentytwo", + HandlerFn::eager(respond_twentytwo_handler, 0), + ), + ( + "test::success_three", + HandlerFn::eager(success_three_handler, 0), + ), + ]); + + let mut nodes = HashMap::new(); + nodes.insert(1, node(1, "test::respond_eleven", Some(2))); + nodes.insert(2, node(2, "test::respond_twentytwo", Some(3))); + nodes.insert(3, node(3, "test::success_three", None)); + + let emitted = RefCell::new(Vec::::new()); + let emitter = |v: Value| { + emitted.borrow_mut().push(v); + }; + + let mut ctx = Context::default(); + let signal = Executor::new(&store, nodes) + .with_respond_emitter(&emitter) + .execute(1, &mut ctx, false); + + match signal { + Signal::Success(v) => assert_eq!(extract_i64(&v), 3), + other => panic!("Expected final success signal, got {:?}", other), + } + + let emitted_values = emitted.borrow(); + assert_eq!(emitted_values.len(), 2); + assert_eq!(extract_i64(&emitted_values[0]), 11); + assert_eq!(extract_i64(&emitted_values[1]), 22); + + assert_node_success(&mut ctx, 1, 11); + assert_node_success(&mut ctx, 2, 22); + assert_node_success(&mut ctx, 3, 3); +} + +#[test] +fn respond_signal_emits_inside_runtime_call_and_parent_continues() { + let mut store = FunctionStore::new(); + store.populate(vec![ + ( + "test::runtime_call_child_two", + HandlerFn::eager(runtime_call_child_two_handler, 0), + ), + ( + "test::respond_fortyfour", + HandlerFn::eager(respond_fortyfour_handler, 0), + ), + ]); + + let mut nodes = HashMap::new(); + nodes.insert(1, node(1, "test::runtime_call_child_two", None)); + nodes.insert(2, node(2, "test::respond_fortyfour", None)); + + let emitted = RefCell::new(Vec::::new()); + let emitter = |v: Value| { + emitted.borrow_mut().push(v); + }; + + let mut ctx = Context::default(); + let signal = Executor::new(&store, nodes) + .with_respond_emitter(&emitter) + .execute(1, &mut ctx, false); + + match signal { + Signal::Success(v) => assert_eq!(extract_i64(&v), 55), + other => panic!("Expected final success signal, got {:?}", other), + } + + let emitted_values = emitted.borrow(); + assert_eq!(emitted_values.len(), 1); + assert_eq!(extract_i64(&emitted_values[0]), 44); + + assert_node_success(&mut ctx, 1, 55); + assert_node_success(&mut ctx, 2, 44); +} + +#[test] +fn return_signal_in_child_context_continues_parent_flow() { + let mut store = FunctionStore::new(); + store.populate(vec![ + ( + "test::runtime_call_child_return", + HandlerFn::eager(runtime_call_child_return_handler, 0), + ), + ( + "test::return_seventyseven", + HandlerFn::eager(return_seventyseven_handler, 0), + ), + ( + "test::success_three", + HandlerFn::eager(success_three_handler, 0), + ), + ]); + + let mut nodes = HashMap::new(); + nodes.insert(1, node(1, "test::runtime_call_child_return", Some(3))); + nodes.insert(2, node(2, "test::return_seventyseven", None)); + nodes.insert(3, node(3, "test::success_three", None)); + + let mut ctx = Context::default(); + let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); + + match signal { + Signal::Success(v) => assert_eq!(extract_i64(&v), 3), + other => panic!("Expected final success signal, got {:?}", other), + } + + // Child return exits its own context and is treated as value by parent node. + assert_node_success(&mut ctx, 1, 77); + assert_node_not_found(&mut ctx, 2); + assert_node_success(&mut ctx, 3, 3); +} + +#[test] +fn child_return_skips_child_next_but_parent_next_runs() { + // Graph: + // node1 -> next node4, and node1 executes child node2 via run(...) + // node2 -> RETURN and has next node3 + // node3 must NOT run (because node2 returned) + // node4 must run (parent chain continues) + let mut store = FunctionStore::new(); + store.populate(vec![ + ( + "test::runtime_call_child_return", + HandlerFn::eager(runtime_call_child_return_handler, 0), + ), + ( + "test::return_seventyseven", + HandlerFn::eager(return_seventyseven_handler, 0), + ), + ( + "test::success_two", + HandlerFn::eager(success_two_handler, 0), + ), + ( + "test::success_three", + HandlerFn::eager(success_three_handler, 0), + ), + ]); + + let mut nodes = HashMap::new(); + nodes.insert(1, node(1, "test::runtime_call_child_return", Some(4))); + nodes.insert(2, node(2, "test::return_seventyseven", Some(3))); + nodes.insert(3, node(3, "test::success_two", None)); + nodes.insert(4, node(4, "test::success_three", None)); + + let mut ctx = Context::default(); + let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); + + match signal { + Signal::Success(v) => assert_eq!(extract_i64(&v), 3), + other => panic!("Expected final success signal, got {:?}", other), + } + + // node1 ran and received child return value + assert_node_success(&mut ctx, 1, 77); + // node2 returned, so it has no success/error entry in context + assert_node_not_found(&mut ctx, 2); + // node3 is child next node and must be skipped due to return in node2 + assert_node_not_found(&mut ctx, 3); + // parent next node still executes + assert_node_success(&mut ctx, 4, 3); +} From 643f148b410aba86922e1d16743cdbc13dde923e Mon Sep 17 00:00:00 2001 From: Raphael Date: Fri, 17 Apr 2026 19:48:19 +0200 Subject: [PATCH 03/28] fix: correct flow return value --- flows/05_if_control.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flows/05_if_control.json b/flows/05_if_control.json index 5d26fac..6aa4251 100644 --- a/flows/05_if_control.json +++ b/flows/05_if_control.json @@ -1,6 +1,6 @@ { "name": "05_if_control", - "description": "This flow expects an object as input (from http request) structured payload.test = bool which the flow will return", + "description": "This flow expects an object as input (from http request) structured payload.test = bool which the flow will return (remeber: respond does not stop the flow so only the adapter will have `Blub` or `true` but the flow always results into `Blub`)", "inputs": [ { "input": { @@ -46,7 +46,7 @@ "expected_result": { "http_status_code": 200, "headers": {}, - "payload": true + "payload": "Blub" } } ], From ccc8d4b748189226ebe92879a70badc1992100d9 Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 21 Apr 2026 17:51:13 +0200 Subject: [PATCH 04/28] feat: added test flows for `return` function --- flows/07_simple_return.json | 234 ++++++++++++++++++++++++++++++++ flows/08_flow_level_return.json | 48 +++++++ 2 files changed, 282 insertions(+) create mode 100644 flows/07_simple_return.json create mode 100644 flows/08_flow_level_return.json diff --git a/flows/07_simple_return.json b/flows/07_simple_return.json new file mode 100644 index 0000000..25111d8 --- /dev/null +++ b/flows/07_simple_return.json @@ -0,0 +1,234 @@ +{ + "name": "07_simple_return", + "description": "This Flow tries to test the control flow function `return`", + "inputs": [ + { + "input": { + "http_method": "GET", + "headers": { + "Content-Type": "text/plain" + }, + "payload": { + "user": { + "age": 42, + "email": "joe@text.com", + "username": "joe doe" + } + } + }, + "expected_result": { + "http_status_code": 200, + "headers": {}, + "payload": [null, null, "username"] + } + } + ], + "flow": { + "flowId": "4", + "projectId": "1", + "type": "REST", + "settings": [ + { + "databaseId": "10", + "flowSettingId": "httpURL", + "value": { + "stringValue": "/test4" + } + }, + { + "databaseId": "11", + "flowSettingId": "httpMethod", + "value": { + "stringValue": "POST" + } + } + ], + "startingNodeId": "11", + "nodeFunctions": [ + { + "databaseId": "11", + "runtimeFunctionId": "std::object::keys", + "parameters": [ + { + "databaseId": "21", + "runtimeParameterId": "object", + "value": { + "referenceValue": { + "paths": [ + { + "path": "payload" + }, + { + "path": "user" + } + ], + "flowInput": {} + } + } + } + ], + "nextNodeId": "9" + }, + { + "databaseId": "9", + "runtimeFunctionId": "std::list::map", + "parameters": [ + { + "databaseId": "22", + "runtimeParameterId": "list", + "value": { + "referenceValue": { + "nodeId": "11" + } + } + }, + { + "databaseId": "31", + "runtimeParameterId": "transform", + "value": { + "nodeFunctionId": "10" + } + } + ], + "nextNodeId": "8" + }, + { + "databaseId": "10", + "runtimeFunctionId": "std::text::is_equal", + "parameters": [ + { + "databaseId": "19", + "runtimeParameterId": "first", + "value": { + "referenceValue": { + "inputType": { + "nodeId": "9", + "parameterIndex": "1" + } + } + } + }, + { + "databaseId": "34", + "runtimeParameterId": "second", + "value": { + "literalValue": { + "stringValue": "username" + } + } + } + ], + "nextNodeId": "14" + }, + { + "databaseId": "14", + "runtimeFunctionId": "std::control::if", + "parameters": [ + { + "databaseId": "28", + "runtimeParameterId": "condition", + "value": { + "referenceValue": { + "nodeId": "10" + } + } + }, + { + "databaseId": "35", + "runtimeParameterId": "runnable", + "value": { + "nodeFunctionId": "7" + } + } + ], + "nextNodeId": "12" + }, + { + "databaseId": "7", + "runtimeFunctionId": "std::control::return", + "parameters": [ + { + "databaseId": "15", + "runtimeParameterId": "value", + "value": { + "referenceValue": { + "inputType": { + "nodeId": "9", + "parameterIndex": "1" + } + } + } + } + ] + }, + { + "databaseId": "12", + "runtimeFunctionId": "std::control::return", + "parameters": [ + { + "databaseId": "24", + "runtimeParameterId": "value", + "value": { + "literalValue": { + "nullValue": "NULL_VALUE" + } + } + } + ] + }, + { + "databaseId": "13", + "runtimeFunctionId": "rest::control::respond", + "parameters": [ + { + "databaseId": "25", + "runtimeParameterId": "http_response", + "value": { + "referenceValue": { + "nodeId": "8" + } + } + } + ] + }, + { + "databaseId": "8", + "runtimeFunctionId": "http::response::create", + "parameters": [ + { + "databaseId": "16", + "runtimeParameterId": "http_status_code", + "value": { + "literalValue": { + "numberValue": { + "integer": "200" + } + } + } + }, + { + "databaseId": "36", + "runtimeParameterId": "headers", + "value": { + "literalValue": { + "structValue": {} + } + } + }, + { + "databaseId": "37", + "runtimeParameterId": "payload", + "value": { + "referenceValue": { + "nodeId": "9" + } + } + } + ], + "nextNodeId": "13" + } + ], + "projectSlug": "codezero-project", + "signature": "(httpURL: HTTP_URL, httpMethod: HTTP_METHOD): { payload: { user: { username: TEXT, email: TEXT } }, headers: I }" + } +} diff --git a/flows/08_flow_level_return.json b/flows/08_flow_level_return.json new file mode 100644 index 0000000..08e7e43 --- /dev/null +++ b/flows/08_flow_level_return.json @@ -0,0 +1,48 @@ +{ + "name": "08_flow_level_return", + "description": "Checks if return on a Root level will return the flow", + "inputs": [ + { + "input": "Test", + "expected_result": "Test" + } + ], + "flow": { + "flowId": "4", + "projectId": "1", + "startingNodeId": "1", + "nodeFunctions": [ + { + "databaseId": "1", + "runtimeFunctionId": "std::control::return", + "parameters": [ + { + "databaseId": "21", + "runtimeParameterId": "object", + "value": { + "literalValue": { + "stringValue": "Test" + } + } + } + ], + "nextNodeId": "2" + }, + { + "databaseId": "2", + "runtimeFunctionId": "std::control::return", + "parameters": [ + { + "databaseId": "21", + "runtimeParameterId": "object", + "value": { + "literalValue": { + "nullValue": "NULL_VALUE" + } + } + } + ] + } + ] + } +} From e4bd922f97f6dfccd91e4366789ad05ea1193e6a Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 21 Apr 2026 17:51:26 +0200 Subject: [PATCH 05/28] ref: renamed crates --- Cargo.lock | 12 ------------ Cargo.toml | 7 +++---- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47c291f..373a9e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1009,7 +1009,6 @@ dependencies = [ "serde", "serde_json", "taurus-core", - "tests-core", "tokio", "tonic", "tucana", @@ -1813,17 +1812,6 @@ dependencies = [ "serde", "serde_json", "taurus-core", - "tests-core", - "tucana", -] - -[[package]] -name = "tests-core" -version = "0.1.0" -dependencies = [ - "log", - "serde", - "serde_json", "tucana", ] diff --git a/Cargo.toml b/Cargo.toml index cb9f790..dc17768 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [ "crates/core", "crates/manual", "crates/taurus", "crates/tests" , "crates/tests-core"] +members = [ "crates/taurus-core", "crates/taurus-manual", "crates/taurus", "crates/taurus-tests"] resolver = "3" [workspace.package] @@ -23,8 +23,7 @@ tonic = "0.14.1" serde_json = "1.0.149" serde = "1.0.228" uuid = { version = "1.23.0", features = ["v4"] } + [workspace.dependencies.taurus-core] -path = "./crates/core" +path = "./crates/taurus-core" -[workspace.dependencies.tests-core] -path = "./crates/tests-core" From 3eb9ed657fe92561e780cf4d6d6d9414dfe11e0b Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 21 Apr 2026 17:51:42 +0200 Subject: [PATCH 06/28] drop: removed tests-core crate --- crates/tests-core/Cargo.toml | 11 ---- crates/tests-core/src/lib.rs | 107 ----------------------------------- 2 files changed, 118 deletions(-) delete mode 100644 crates/tests-core/Cargo.toml delete mode 100644 crates/tests-core/src/lib.rs diff --git a/crates/tests-core/Cargo.toml b/crates/tests-core/Cargo.toml deleted file mode 100644 index 37fa8de..0000000 --- a/crates/tests-core/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "tests-core" -version.workspace = true -edition.workspace = true - -[dependencies] -tucana = { workspace = true } -log = { workspace = true } -serde_json = { workspace = true } -serde = { workspace = true } - diff --git a/crates/tests-core/src/lib.rs b/crates/tests-core/src/lib.rs deleted file mode 100644 index c0109d4..0000000 --- a/crates/tests-core/src/lib.rs +++ /dev/null @@ -1,107 +0,0 @@ -use std::path::Path; - -use log::{error, info}; -use serde::Deserialize; -use tucana::shared::ValidationFlow; - -#[derive(Clone, Deserialize)] -pub struct Input { - pub input: Option, - pub expected_result: serde_json::Value, -} - -#[derive(Clone, Deserialize)] -pub struct Case { - pub name: String, - pub description: String, - pub inputs: Vec, - pub flow: ValidationFlow, -} - -pub enum CaseResult { - Success, - Failure(Input, serde_json::Value), -} - -pub trait Testable { - fn run(&self) -> CaseResult; -} - -#[derive(Clone, Deserialize)] -pub struct Cases { - pub cases: Vec, -} - -pub fn print_success(case: &Case) { - info!("test {} ... ok", case.name); -} - -pub fn print_failure(case: &Case, input: &Input, result: serde_json::Value) { - error!("test {} ... FAILED", case.name); - error!(" input: {:?}", input.input); - error!(" expected: {:?}", input.expected_result); - error!(" real_value: {:?}", result); - error!(" message: {}", case.description); -} - -fn get_test_case + std::fmt::Debug>(path: P) -> Option { - let content = match std::fs::read_to_string(&path) { - Ok(it) => it, - Err(err) => { - log::error!("Cannot read file ({:?}): {:?}", path, err); - return None; - } - }; - - match serde_json::from_str(&content) { - Ok(it) => it, - Err(err) => { - log::error!("Cannot read json ({:?}): {:?}", path, err); - None - } - } -} - -fn get_test_cases(path: &str) -> Cases { - let mut items = Vec::new(); - let dir = match std::fs::read_dir(path) { - Ok(d) => d, - Err(err) => { - panic!("Cannot open path: {:?}", err) - } - }; - - for entry in dir { - let entry = match entry { - Ok(it) => it, - Err(err) => { - log::error!("Cannot read entry: {:?}", err); - continue; - } - }; - let file_path = entry.path(); - items.push(match get_test_case(&file_path) { - Some(it) => it, - None => { - continue; - } - }); - } - - Cases { cases: items } -} - -impl Case { - pub fn from_path(path: &str) -> Self { - match get_test_case(path) { - Some(s) => s, - None => panic!("flow was not found"), - } - } -} - -impl Cases { - pub fn from_path(path: &str) -> Self { - get_test_cases(path) - } -} From ef704122f43e98231df012650e41f3b585a4635b Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 21 Apr 2026 17:52:06 +0200 Subject: [PATCH 07/28] ref: renamed tests into taurus-tests --- crates/{tests => taurus-tests}/Cargo.toml | 1 - crates/{tests => taurus-tests}/README.md | 0 crates/taurus-tests/src/main.rs | 181 ++++++++++++++++++++++ crates/tests/src/main.rs | 91 ----------- 4 files changed, 181 insertions(+), 92 deletions(-) rename crates/{tests => taurus-tests}/Cargo.toml (89%) rename crates/{tests => taurus-tests}/README.md (100%) create mode 100644 crates/taurus-tests/src/main.rs delete mode 100644 crates/tests/src/main.rs diff --git a/crates/tests/Cargo.toml b/crates/taurus-tests/Cargo.toml similarity index 89% rename from crates/tests/Cargo.toml rename to crates/taurus-tests/Cargo.toml index 8ac7443..0e4f08a 100644 --- a/crates/tests/Cargo.toml +++ b/crates/taurus-tests/Cargo.toml @@ -9,5 +9,4 @@ taurus-core = { workspace = true } log = { workspace = true } env_logger = { workspace = true } serde_json = { workspace = true } -tests-core = { workspace = true } serde = { workspace = true } diff --git a/crates/tests/README.md b/crates/taurus-tests/README.md similarity index 100% rename from crates/tests/README.md rename to crates/taurus-tests/README.md diff --git a/crates/taurus-tests/src/main.rs b/crates/taurus-tests/src/main.rs new file mode 100644 index 0000000..2cf79d0 --- /dev/null +++ b/crates/taurus-tests/src/main.rs @@ -0,0 +1,181 @@ +use std::path::Path; + +use log::{error, info}; +use serde::Deserialize; +use serde_json::json; +use taurus_core::runtime::engine::ExecutionEngine; +use tucana::shared::{ + ValidationFlow, + helper::value::{from_json_value, to_json_value}, +}; + +#[derive(Clone, Deserialize)] +pub struct Input { + pub input: Option, + pub expected_result: serde_json::Value, +} + +#[derive(Clone, Deserialize)] +pub struct Case { + pub name: String, + pub description: String, + pub inputs: Vec, + pub flow: ValidationFlow, +} + +pub enum CaseResult { + Success, + Failure(Input, serde_json::Value), +} + +pub trait Testable { + fn run(&self) -> CaseResult; +} + +#[derive(Clone, Deserialize)] +pub struct Cases { + pub cases: Vec, +} + +pub fn print_success(case: &Case) { + info!("test {} ... ok", case.name); +} + +pub fn print_failure(case: &Case, input: &Input, result: serde_json::Value) { + error!("test {} ... FAILED", case.name); + error!(" input: {:?}", input.input); + error!(" expected: {:?}", input.expected_result); + error!(" real_value: {:?}", result); + error!(" message: {}", case.description); +} + +fn get_test_case + std::fmt::Debug>(path: P) -> Option { + let content = match std::fs::read_to_string(&path) { + Ok(it) => it, + Err(err) => { + log::error!("Cannot read file ({:?}): {:?}", path, err); + return None; + } + }; + + match serde_json::from_str(&content) { + Ok(it) => it, + Err(err) => { + log::error!("Cannot read json ({:?}): {:?}", path, err); + None + } + } +} + +fn get_test_cases(path: &str) -> Cases { + let mut items = Vec::new(); + let dir = match std::fs::read_dir(path) { + Ok(d) => d, + Err(err) => { + panic!("Cannot open path: {:?}", err) + } + }; + + for entry in dir { + let entry = match entry { + Ok(it) => it, + Err(err) => { + log::error!("Cannot read entry: {:?}", err); + continue; + } + }; + let file_path = entry.path(); + items.push(match get_test_case(&file_path) { + Some(it) => it, + None => { + continue; + } + }); + } + + Cases { cases: items } +} + +impl Case { + pub fn from_path(path: &str) -> Self { + match get_test_case(path) { + Some(s) => s, + None => panic!("flow was not found"), + } + } +} + +impl Cases { + pub fn from_path(path: &str) -> Self { + get_test_cases(path) + } +} + +fn run_tests(cases: Cases) { + for case in &cases.cases { + match case.run() { + CaseResult::Success => print_success(case), + CaseResult::Failure(input, result) => print_failure(case, &input, result), + } + } +} + +impl Testable for Case { + fn run(&self) -> CaseResult { + let engine = ExecutionEngine::new(); + + for input in self.inputs.clone() { + let flow_input = input.clone().input.map(from_json_value); + let (res, _) = engine.execute_graph( + self.flow.starting_node_id, + self.flow.node_functions.clone(), + flow_input, + None, + None, + true, + ); + + match res { + taurus_core::types::signal::Signal::Failure(err) => { + let json = json!({ + "name": err.category, + "message": err.message, + }); + if json != input.clone().expected_result { + return CaseResult::Failure(input, json); + } + } + taurus_core::types::signal::Signal::Success(value) => { + let json = to_json_value(value); + if json != input.clone().expected_result { + return CaseResult::Failure(input, json); + } + } + taurus_core::types::signal::Signal::Return(value) => { + let json = to_json_value(value); + if json != input.clone().expected_result { + return CaseResult::Failure(input, json); + } + } + taurus_core::types::signal::Signal::Respond(value) => { + let json = to_json_value(value); + if json != input.clone().expected_result { + return CaseResult::Failure(input, json); + } + } + taurus_core::types::signal::Signal::Stop => continue, + } + } + + CaseResult::Success + } +} + +fn main() { + env_logger::Builder::from_default_env() + .filter_level(log::LevelFilter::Info) + .init(); + + let cases = Cases::from_path("./flows/"); + run_tests(cases); +} diff --git a/crates/tests/src/main.rs b/crates/tests/src/main.rs deleted file mode 100644 index 61561cf..0000000 --- a/crates/tests/src/main.rs +++ /dev/null @@ -1,91 +0,0 @@ -use serde_json::json; -use std::collections::HashMap; -use taurus_core::context::{context::Context, executor::Executor, registry::FunctionStore}; -use tests_core::{Case, CaseResult, Cases, print_failure, print_success}; - -use tucana::shared::{ - NodeFunction, - helper::value::{from_json_value, to_json_value}, -}; - -pub trait Testable { - fn run(&self) -> CaseResult; -} - -fn run_tests(cases: Cases) { - for case in &cases.cases { - match case.run() { - CaseResult::Success => print_success(case), - CaseResult::Failure(input, result) => print_failure(case, &input, result), - } - } -} - -impl Testable for Case { - fn run(&self) -> CaseResult { - let store = FunctionStore::default(); - - let node_functions: HashMap = self - .clone() - .flow - .node_functions - .into_iter() - .map(|node| (node.database_id, node)) - .collect(); - - for input in self.inputs.clone() { - let mut context = match input.clone().input { - Some(inp) => Context::new(from_json_value(inp)), - None => Context::default(), - }; - - let res = Executor::new(&store, node_functions.clone()).execute( - self.flow.starting_node_id, - &mut context, - false, - ); - - match res { - taurus_core::context::signal::Signal::Failure(err) => { - let json = json!({ - "name": err.name, - "message": err.message, - }); - if json != input.clone().expected_result { - return CaseResult::Failure(input, json); - } - } - taurus_core::context::signal::Signal::Success(value) => { - let json = to_json_value(value); - if json != input.clone().expected_result { - return CaseResult::Failure(input, json); - } - } - taurus_core::context::signal::Signal::Return(value) => { - let json = to_json_value(value); - if json != input.clone().expected_result { - return CaseResult::Failure(input, json); - } - } - taurus_core::context::signal::Signal::Respond(value) => { - let json = to_json_value(value); - if json != input.clone().expected_result { - return CaseResult::Failure(input, json); - } - } - taurus_core::context::signal::Signal::Stop => continue, - } - } - - CaseResult::Success - } -} - -fn main() { - env_logger::Builder::from_default_env() - .filter_level(log::LevelFilter::Info) - .init(); - - let cases = Cases::from_path("./flows/"); - run_tests(cases); -} From 358904389e7f2fffe54f84bc9902720f9bd31e27 Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 21 Apr 2026 17:52:21 +0200 Subject: [PATCH 08/28] ref: renamed manual into taurus-manual --- crates/manual/src/main.rs | 169 ------------ crates/{manual => taurus-manual}/Cargo.toml | 1 - crates/taurus-manual/src/main.rs | 268 ++++++++++++++++++++ 3 files changed, 268 insertions(+), 170 deletions(-) delete mode 100644 crates/manual/src/main.rs rename crates/{manual => taurus-manual}/Cargo.toml (93%) create mode 100644 crates/taurus-manual/src/main.rs diff --git a/crates/manual/src/main.rs b/crates/manual/src/main.rs deleted file mode 100644 index 49e8934..0000000 --- a/crates/manual/src/main.rs +++ /dev/null @@ -1,169 +0,0 @@ -use std::collections::HashMap; - -use async_nats::Client; -use async_trait::async_trait; -use clap::{Parser, arg, command}; -use prost::Message; -use taurus_core::context::{context::Context, executor::Executor, registry::FunctionStore}; -use taurus_core::runtime::{error::RuntimeError, remote::RemoteRuntime}; -use tests_core::Case; -use tucana::shared::helper::value::to_json_value; -use tucana::shared::{NodeFunction, helper::value::from_json_value}; -use tucana::{ - aquila::{ExecutionRequest, ExecutionResult}, - shared::Value, -}; - -pub struct RemoteNatsClient { - client: Client, -} - -impl RemoteNatsClient { - pub fn new(client: Client) -> Self { - RemoteNatsClient { client } - } -} - -#[async_trait] -impl RemoteRuntime for RemoteNatsClient { - async fn execute_remote( - &self, - remote_name: String, - request: ExecutionRequest, - ) -> Result { - let topic = format!("action.{}.{}", remote_name, request.execution_identifier); - let payload = request.encode_to_vec(); - let res = self.client.request(topic.clone(), payload.into()).await; - log::info!("Publishing to topic: {}", topic); - let message = match res { - Ok(r) => r, - Err(err) => { - log::error!( - "RemoteRuntimeExeption: failed to handle NATS message: {}", - err - ); - return Err(RuntimeError::simple_str( - "RemoteRuntimeExeption", - "Failed to receive any response messages from a remote runtime.", - )); - } - }; - - let decode_result = ExecutionResult::decode(message.payload); - let execution_result = match decode_result { - Ok(r) => r, - Err(err) => { - log::error!( - "RemoteRuntimeExeption: failed to decode NATS message: {}", - err - ); - return Err(RuntimeError::simple_str( - "RemoteRuntimeExeption", - "Failed to read Remote Response", - )); - } - }; - - match execution_result.result { - Some(result) => match result { - tucana::aquila::execution_result::Result::Success(value) => Ok(value), - tucana::aquila::execution_result::Result::Error(err) => { - let name = err.code.to_string(); - let description = match err.description { - Some(string) => string, - None => "Unknown Error".to_string(), - }; - let error = RuntimeError::new(name, description, None); - Err(error) - } - }, - None => Err(RuntimeError::simple_str( - "RemoteRuntimeExeption", - "Result of Remote Response was empty.", - )), - } - } -} - -#[derive(clap::Parser, Debug)] -#[command(author, version, about)] -struct Args { - /// Index value - #[arg(short, long, default_value_t = 0)] - index: i32, - - /// NATS server URL - #[arg(short, long, default_value_t = String::from("nats://127.0.0.1:4222"))] - nats_url: String, - - /// Path value - #[arg(short, long)] - path: String, -} - -#[tokio::main] -async fn main() { - env_logger::Builder::from_default_env() - .filter_level(log::LevelFilter::Info) - .init(); - - let args = Args::parse(); - let index = args.index; - let nats_url = args.nats_url; - let path = args.path; - let case = Case::from_path(&path); - - let store = FunctionStore::default(); - - let node_functions: HashMap = case - .clone() - .flow - .node_functions - .into_iter() - .map(|node| (node.database_id, node)) - .collect(); - - let mut context = match case.inputs.get(index as usize) { - Some(inp) => match inp.input.clone() { - Some(json_input) => Context::new(from_json_value(json_input)), - None => Context::default(), - }, - None => Context::default(), - }; - - let client = match async_nats::connect(nats_url).await { - Ok(client) => { - log::info!("Connected to nats server"); - client - } - Err(err) => { - panic!("Failed to connect to NATS server: {}", err); - } - }; - let remote = RemoteNatsClient::new(client); - let result = Executor::new(&store, node_functions.clone()) - .with_remote_runtime(&remote) - .execute(case.flow.starting_node_id, &mut context, true); - - match result { - taurus_core::context::signal::Signal::Success(value) => { - let json = to_json_value(value); - let pretty = serde_json::to_string_pretty(&json).unwrap(); - println!("{}", pretty); - } - taurus_core::context::signal::Signal::Return(value) => { - let json = to_json_value(value); - let pretty = serde_json::to_string_pretty(&json).unwrap(); - println!("{}", pretty); - } - taurus_core::context::signal::Signal::Respond(value) => { - let json = to_json_value(value); - let pretty = serde_json::to_string_pretty(&json).unwrap(); - println!("{}", pretty); - } - taurus_core::context::signal::Signal::Stop => println!("Received Stop signal"), - taurus_core::context::signal::Signal::Failure(runtime_error) => { - println!("RuntimeError: {:?}", runtime_error); - } - } -} diff --git a/crates/manual/Cargo.toml b/crates/taurus-manual/Cargo.toml similarity index 93% rename from crates/manual/Cargo.toml rename to crates/taurus-manual/Cargo.toml index f4d78f2..d4655aa 100644 --- a/crates/manual/Cargo.toml +++ b/crates/taurus-manual/Cargo.toml @@ -4,7 +4,6 @@ version.workspace = true edition.workspace = true [dependencies] -tests-core = { workspace = true } tucana = { workspace = true } taurus-core = { workspace = true } log = { workspace = true } diff --git a/crates/taurus-manual/src/main.rs b/crates/taurus-manual/src/main.rs new file mode 100644 index 0000000..5a541ae --- /dev/null +++ b/crates/taurus-manual/src/main.rs @@ -0,0 +1,268 @@ +use std::path::Path; + +use async_nats::Client; +use async_trait::async_trait; +use clap::{Parser, arg, command}; +use log::{error, info}; +use prost::Message; +use serde::Deserialize; +use taurus_core::runtime::engine::ExecutionEngine; +use taurus_core::runtime::remote::{RemoteExecution, RemoteRuntime}; +use taurus_core::types::errors::runtime_error::RuntimeError; +use tucana::aquila::ExecutionResult; +use tucana::shared::helper::value::from_json_value; +use tucana::shared::helper::value::to_json_value; +use tucana::shared::{ValidationFlow, Value}; + +#[derive(Clone, Deserialize)] +pub struct Input { + pub input: Option, + pub expected_result: serde_json::Value, +} + +#[derive(Clone, Deserialize)] +pub struct Case { + pub name: String, + pub description: String, + pub inputs: Vec, + pub flow: ValidationFlow, +} + +pub enum CaseResult { + Success, + Failure(Input, serde_json::Value), +} + +pub trait Testable { + fn run(&self) -> CaseResult; +} + +#[derive(Clone, Deserialize)] +pub struct Cases { + pub cases: Vec, +} + +pub fn print_success(case: &Case) { + info!("test {} ... ok", case.name); +} + +pub fn print_failure(case: &Case, input: &Input, result: serde_json::Value) { + error!("test {} ... FAILED", case.name); + error!(" input: {:?}", input.input); + error!(" expected: {:?}", input.expected_result); + error!(" real_value: {:?}", result); + error!(" message: {}", case.description); +} + +fn get_test_case + std::fmt::Debug>(path: P) -> Option { + let content = match std::fs::read_to_string(&path) { + Ok(it) => it, + Err(err) => { + log::error!("Cannot read file ({:?}): {:?}", path, err); + return None; + } + }; + + match serde_json::from_str(&content) { + Ok(it) => it, + Err(err) => { + log::error!("Cannot read json ({:?}): {:?}", path, err); + None + } + } +} + +fn get_test_cases(path: &str) -> Cases { + let mut items = Vec::new(); + let dir = match std::fs::read_dir(path) { + Ok(d) => d, + Err(err) => { + panic!("Cannot open path: {:?}", err) + } + }; + + for entry in dir { + let entry = match entry { + Ok(it) => it, + Err(err) => { + log::error!("Cannot read entry: {:?}", err); + continue; + } + }; + let file_path = entry.path(); + items.push(match get_test_case(&file_path) { + Some(it) => it, + None => { + continue; + } + }); + } + + Cases { cases: items } +} + +impl Case { + pub fn from_path(path: &str) -> Self { + match get_test_case(path) { + Some(s) => s, + None => panic!("flow was not found"), + } + } +} + +impl Cases { + pub fn from_path(path: &str) -> Self { + get_test_cases(path) + } +} +pub struct RemoteNatsClient { + client: Client, +} + +impl RemoteNatsClient { + pub fn new(client: Client) -> Self { + RemoteNatsClient { client } + } +} + +#[async_trait] +impl RemoteRuntime for RemoteNatsClient { + async fn execute_remote(&self, execution: RemoteExecution) -> Result { + let topic = format!( + "action.{}.{}", + execution.target_service, execution.request.execution_identifier + ); + let payload = execution.request.encode_to_vec(); + let res = self.client.request(topic.clone(), payload.into()).await; + log::info!("Publishing to topic: {}", topic); + let message = match res { + Ok(r) => r, + Err(err) => { + log::error!( + "RemoteRuntimeExeption: failed to handle NATS message: {}", + err + ); + return Err(RuntimeError::new( + "T-RMT-000001", + "RemoteRuntimeExeption", + "Failed to receive any response messages from a remote runtime.", + )); + } + }; + + let decode_result = ExecutionResult::decode(message.payload); + let execution_result = match decode_result { + Ok(r) => r, + Err(err) => { + log::error!( + "RemoteRuntimeExeption: failed to decode NATS message: {}", + err + ); + return Err(RuntimeError::new( + "T-RMT-000002", + "RemoteRuntimeExeption", + "Failed to read Remote Response", + )); + } + }; + + match execution_result.result { + Some(result) => match result { + tucana::aquila::execution_result::Result::Success(value) => Ok(value), + tucana::aquila::execution_result::Result::Error(err) => { + let code = err.code.to_string(); + let description = match err.description { + Some(string) => string, + None => "Unknown Error".to_string(), + }; + let error = RuntimeError::new(code, "RemoteExecutionError", description); + Err(error) + } + }, + None => Err(RuntimeError::new( + "T-RMT-000003", + "RemoteRuntimeExeption", + "Result of Remote Response was empty.", + )), + } + } +} + +#[derive(clap::Parser, Debug)] +#[command(author, version, about)] +struct Args { + /// Index value + #[arg(short, long, default_value_t = 0)] + index: i32, + + /// NATS server URL + #[arg(short, long, default_value_t = String::from("nats://127.0.0.1:4222"))] + nats_url: String, + + /// Path value + #[arg(short, long)] + path: String, +} + +#[tokio::main] +async fn main() { + env_logger::Builder::from_default_env() + .filter_level(log::LevelFilter::Info) + .init(); + + let args = Args::parse(); + let index = args.index; + let nats_url = args.nats_url; + let path = args.path; + let case = Case::from_path(&path); + + let flow_input = match case.inputs.get(index as usize) { + Some(inp) => match inp.input.clone() { + Some(json_input) => Some(from_json_value(json_input)), + None => None, + }, + None => None, + }; + + let client = match async_nats::connect(nats_url).await { + Ok(client) => { + log::info!("Connected to nats server"); + client + } + Err(err) => { + panic!("Failed to connect to NATS server: {}", err); + } + }; + let remote = RemoteNatsClient::new(client); + let engine = ExecutionEngine::new(); + let (result, _) = engine.execute_graph( + case.flow.starting_node_id, + case.flow.node_functions.clone(), + flow_input, + Some(&remote), + None, + true, + ); + + match result { + taurus_core::types::signal::Signal::Success(value) => { + let json = to_json_value(value); + let pretty = serde_json::to_string_pretty(&json).unwrap(); + println!("{}", pretty); + } + taurus_core::types::signal::Signal::Return(value) => { + let json = to_json_value(value); + let pretty = serde_json::to_string_pretty(&json).unwrap(); + println!("{}", pretty); + } + taurus_core::types::signal::Signal::Respond(value) => { + let json = to_json_value(value); + let pretty = serde_json::to_string_pretty(&json).unwrap(); + println!("{}", pretty); + } + taurus_core::types::signal::Signal::Stop => println!("Received Stop signal"), + taurus_core::types::signal::Signal::Failure(runtime_error) => { + println!("RuntimeError: {:?}", runtime_error); + } + } +} From 150103f8bcf805cdd4c088259bd33288eb81ade4 Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 21 Apr 2026 17:52:42 +0200 Subject: [PATCH 09/28] feat: used new core crate --- crates/taurus/src/config/mod.rs | 8 ---- crates/taurus/src/main.rs | 77 ++++++++++----------------------- crates/taurus/src/remote/mod.rs | 35 +++++++-------- 3 files changed, 40 insertions(+), 80 deletions(-) diff --git a/crates/taurus/src/config/mod.rs b/crates/taurus/src/config/mod.rs index 45ffbf3..dfc2a39 100644 --- a/crates/taurus/src/config/mod.rs +++ b/crates/taurus/src/config/mod.rs @@ -1,15 +1,8 @@ use code0_flow::flow_config::env_with_default; -use code0_flow::flow_config::environment::Environment; use code0_flow::flow_config::mode::Mode; /// Struct for all relevant `Taurus` startup configurations pub struct Config { - /// Options: - /// `development` (default) - /// `staging` - /// `production` - pub environment: Environment, - /// Aquila mode /// /// Options: @@ -39,7 +32,6 @@ pub struct Config { impl Config { pub fn new() -> Self { Config { - environment: env_with_default("ENVIRONMENT", Environment::Development), mode: env_with_default("MODE", Mode::DYNAMIC), nats_url: env_with_default("NATS_URL", String::from("nats://localhost:4222")), aquila_url: env_with_default("AQUILA_URL", String::from("http://localhost:50051")), diff --git a/crates/taurus/src/main.rs b/crates/taurus/src/main.rs index 51ab341..660ab38 100644 --- a/crates/taurus/src/main.rs +++ b/crates/taurus/src/main.rs @@ -13,71 +13,33 @@ use code0_flow::flow_config::mode::Mode::DYNAMIC; use futures_lite::StreamExt; use log::error; use prost::Message; -use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::{Duration, Instant}; -use taurus_core::context::context::Context; -use taurus_core::context::executor::Executor; -use taurus_core::context::registry::FunctionStore; -use taurus_core::context::signal::Signal; -use taurus_core::runtime::error::RuntimeError; +use taurus_core::runtime::engine::{EmitType, ExecutionEngine, RespondEmitter}; +use taurus_core::types::signal::Signal; use tokio::signal; use tokio::sync::mpsc; use tokio::time::sleep; use tonic_health::pb::health_server::HealthServer; use tucana::shared::value::Kind; -use tucana::shared::{ - ExecutionFlow, NodeFunction, RuntimeFeature, RuntimeUsage, Translation, Value, -}; +use tucana::shared::{ExecutionFlow, RuntimeFeature, RuntimeUsage, Translation, Value}; fn handle_message( flow: ExecutionFlow, - store: &FunctionStore, + engine: &ExecutionEngine, nats_remote: &RemoteNatsClient, - respond_emitter: Option<&dyn Fn(Value)>, + respond_emitter: Option<&dyn RespondEmitter>, ) -> (Signal, RuntimeUsage) { let start = Instant::now(); - let mut context = match flow.input_value { - Some(v) => { - log::debug!("Input Value for flow: {:?}", v); - Context::new(v) - } - None => Context::default(), - }; - - if flow.node_functions.is_empty() { - let duration_millis = start.elapsed().as_millis() as i64; - return ( - Signal::Failure(RuntimeError::simple_str( - "InvalidFlow", - "This flow has no nodes to execute!", - )), - RuntimeUsage { - flow_id: flow.flow_id, - duration: duration_millis, - }, - ); - } - - let node_functions: HashMap = flow - .node_functions - .into_iter() - .map(|node| (node.database_id, node)) - .collect(); - - let mut executor = Executor::new(store, node_functions).with_remote_runtime(nats_remote); - if let Some(emitter) = respond_emitter { - executor = executor.with_respond_emitter(emitter); - } - - let signal = executor.execute(flow.starting_node_id, &mut context, true); + let flow_id = flow.flow_id; + let (signal, _reason) = engine.execute_flow(flow, Some(nats_remote), respond_emitter, true); let duration_millis = start.elapsed().as_millis() as i64; ( signal, RuntimeUsage { - flow_id: flow.flow_id, + flow_id, duration: duration_millis, }, ) @@ -92,7 +54,7 @@ async fn main() { load_env_file(); let config = Config::new(); - let store = FunctionStore::default(); + let engine = ExecutionEngine::new(); let mut runtime_status_service: Option = None; let mut runtime_usage_service: Option = None; @@ -240,18 +202,23 @@ async fn main() { let emit_tx = respond_tx.clone(); let respond_count = Arc::new(AtomicUsize::new(0)); let respond_count_for_emitter = respond_count.clone(); - let respond_emitter = move |value: Value| { - respond_count_for_emitter.fetch_add(1, Ordering::Relaxed); - if let Err(err) = emit_tx.send(value) { - log::debug!( - "Dropped respond signal value because publisher is unavailable: {:?}", - err - ); + let respond_emitter = move |emit_type: EmitType, value: Value| match emit_type { + EmitType::OngoingExec => { + respond_count_for_emitter.fetch_add(1, Ordering::Relaxed); + if let Err(err) = emit_tx.send(value) { + log::debug!( + "Dropped respond signal value because publisher is unavailable: {:?}", + err + ); + } } + EmitType::StartingExec => log::debug!("Flow execution started"), + EmitType::FinishedExec => log::debug!("Flow execution finished"), + EmitType::FailedExec => log::debug!("Flow execution failed"), }; let (signal, runtime_usage) = - handle_message(flow, &store, &nats_client, Some(&respond_emitter)); + handle_message(flow, &engine, &nats_client, Some(&respond_emitter)); drop(respond_emitter); drop(respond_tx); diff --git a/crates/taurus/src/remote/mod.rs b/crates/taurus/src/remote/mod.rs index 0651933..cc9f7af 100644 --- a/crates/taurus/src/remote/mod.rs +++ b/crates/taurus/src/remote/mod.rs @@ -1,11 +1,10 @@ use async_nats::Client; use prost::Message; -use taurus_core::runtime::{error::RuntimeError, remote::RemoteRuntime}; +use taurus_core::runtime::remote::{RemoteExecution, RemoteRuntime}; +use taurus_core::types::errors::runtime_error::RuntimeError; use tonic::async_trait; -use tucana::{ - aquila::{ExecutionRequest, ExecutionResult}, - shared::Value, -}; +use tucana::aquila::ExecutionResult; +use tucana::shared::Value; pub struct RemoteNatsClient { client: Client, @@ -19,13 +18,12 @@ impl RemoteNatsClient { #[async_trait] impl RemoteRuntime for RemoteNatsClient { - async fn execute_remote( - &self, - remote_name: String, - request: ExecutionRequest, - ) -> Result { - let topic = format!("action.{}.{}", remote_name, request.execution_identifier); - let payload = request.encode_to_vec(); + async fn execute_remote(&self, execution: RemoteExecution) -> Result { + let topic = format!( + "action.{}.{}", + execution.target_service, execution.request.execution_identifier + ); + let payload = execution.request.encode_to_vec(); log::info!("Publishing to topic: {}", topic); let res = self.client.request(topic, payload.into()).await; let message = match res { @@ -35,7 +33,8 @@ impl RemoteRuntime for RemoteNatsClient { "RemoteRuntimeExeption: failed to handle NATS message: {}", err ); - return Err(RuntimeError::simple_str( + return Err(RuntimeError::new( + "T-RMT-000001", "RemoteRuntimeExeption", "Failed to receive any response messages from a remote runtime.", )); @@ -50,7 +49,8 @@ impl RemoteRuntime for RemoteNatsClient { "RemoteRuntimeExeption: failed to decode NATS message: {}", err ); - return Err(RuntimeError::simple_str( + return Err(RuntimeError::new( + "T-RMT-000002", "RemoteRuntimeExeption", "Failed to read Remote Response", )); @@ -61,16 +61,17 @@ impl RemoteRuntime for RemoteNatsClient { Some(result) => match result { tucana::aquila::execution_result::Result::Success(value) => Ok(value), tucana::aquila::execution_result::Result::Error(err) => { - let name = err.code.to_string(); + let code = err.code.to_string(); let description = match err.description { Some(string) => string, None => "Unknown Error".to_string(), }; - let error = RuntimeError::new(name, description, None); + let error = RuntimeError::new(code, "RemoteExecutionError", description); Err(error) } }, - None => Err(RuntimeError::simple_str( + None => Err(RuntimeError::new( + "T-RMT-000003", "RemoteRuntimeExeption", "Result of Remote Response was empty.", )), From 3d5a8e447c43cdc3fe736f4c3e2d866638f0afdb Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 21 Apr 2026 17:52:54 +0200 Subject: [PATCH 10/28] drop: removed old core crate --- crates/core/.gitignore | 1 - crates/core/Cargo.toml | 14 - crates/core/src/context/argument.rs | 128 -- crates/core/src/context/context.rs | 148 -- crates/core/src/context/executor/arguments.rs | 242 --- .../src/context/executor/execution_loop.rs | 103 - .../core/src/context/executor/formatting.rs | 56 - crates/core/src/context/executor/mod.rs | 126 -- .../src/context/executor/node_execution.rs | 258 --- crates/core/src/context/executor/tests.rs | 467 ----- crates/core/src/context/macros.rs | 53 - crates/core/src/context/mod.rs | 6 - crates/core/src/context/registry.rs | 95 - crates/core/src/context/signal.rs | 38 - crates/core/src/debug/mod.rs | 3 - crates/core/src/debug/render.rs | 208 -- crates/core/src/debug/trace.rs | 76 - crates/core/src/debug/tracer.rs | 140 -- crates/core/src/lib.rs | 4 - crates/core/src/runtime/error/mod.rs | 77 - crates/core/src/runtime/functions/array.rs | 1709 ----------------- crates/core/src/runtime/functions/boolean.rs | 412 ---- crates/core/src/runtime/functions/control.rs | 98 - crates/core/src/runtime/functions/http.rs | 125 -- crates/core/src/runtime/functions/mod.rs | 23 - crates/core/src/runtime/functions/number.rs | 1238 ------------ crates/core/src/runtime/functions/text.rs | 996 ---------- crates/core/src/runtime/mod.rs | 3 - crates/core/src/runtime/remote/mod.rs | 13 - crates/core/src/value.rs | 55 - 30 files changed, 6915 deletions(-) delete mode 100644 crates/core/.gitignore delete mode 100644 crates/core/Cargo.toml delete mode 100644 crates/core/src/context/argument.rs delete mode 100644 crates/core/src/context/context.rs delete mode 100644 crates/core/src/context/executor/arguments.rs delete mode 100644 crates/core/src/context/executor/execution_loop.rs delete mode 100644 crates/core/src/context/executor/formatting.rs delete mode 100644 crates/core/src/context/executor/mod.rs delete mode 100644 crates/core/src/context/executor/node_execution.rs delete mode 100644 crates/core/src/context/executor/tests.rs delete mode 100644 crates/core/src/context/macros.rs delete mode 100644 crates/core/src/context/mod.rs delete mode 100644 crates/core/src/context/registry.rs delete mode 100644 crates/core/src/context/signal.rs delete mode 100644 crates/core/src/debug/mod.rs delete mode 100644 crates/core/src/debug/render.rs delete mode 100644 crates/core/src/debug/trace.rs delete mode 100644 crates/core/src/debug/tracer.rs delete mode 100644 crates/core/src/lib.rs delete mode 100644 crates/core/src/runtime/error/mod.rs delete mode 100644 crates/core/src/runtime/functions/array.rs delete mode 100644 crates/core/src/runtime/functions/boolean.rs delete mode 100644 crates/core/src/runtime/functions/control.rs delete mode 100644 crates/core/src/runtime/functions/http.rs delete mode 100644 crates/core/src/runtime/functions/mod.rs delete mode 100644 crates/core/src/runtime/functions/number.rs delete mode 100644 crates/core/src/runtime/functions/text.rs delete mode 100644 crates/core/src/runtime/mod.rs delete mode 100644 crates/core/src/runtime/remote/mod.rs delete mode 100644 crates/core/src/value.rs diff --git a/crates/core/.gitignore b/crates/core/.gitignore deleted file mode 100644 index ea8c4bf..0000000 --- a/crates/core/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml deleted file mode 100644 index fc6f7f9..0000000 --- a/crates/core/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "taurus-core" -version.workspace = true -edition.workspace = true - -[dependencies] -tucana = { workspace = true } -base64 = { workspace = true } -rand = { workspace = true } -log = { workspace = true } -futures-lite = { workspace = true } -async-trait = { workspace = true } -uuid = { workspace = true } - diff --git a/crates/core/src/context/argument.rs b/crates/core/src/context/argument.rs deleted file mode 100644 index f5d6698..0000000 --- a/crates/core/src/context/argument.rs +++ /dev/null @@ -1,128 +0,0 @@ -use crate::context::signal::Signal; -use crate::runtime::error::RuntimeError; -use std::convert::Infallible; -use tucana::shared::value::Kind; -use tucana::shared::{ListValue, NumberValue, Struct, Value}; - -use crate::value::{number_to_f64, number_to_i64_lossy}; -#[derive(Clone, Debug)] -pub enum Argument { - // Eval => Evaluated Value - // - can be consumed directly by a function - Eval(Value), - // Thunk of NodeFunction identifier - // - used for lazy execution of nodes - Thunk(i64), -} - -#[derive(Clone, Copy, Debug)] -pub enum ParameterNode { - Eager, - Lazy, -} - -pub trait TryFromArgument: Sized { - fn try_from_argument(a: &Argument) -> Result; -} - -fn type_err(msg: &str, a: &Argument) -> Signal { - Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!("{} but it was the arugment: {:?}", msg, a), - )) -} - -impl TryFromArgument for Value { - fn try_from_argument(a: &Argument) -> Result { - match a { - Argument::Eval(v) => Ok(v.clone()), - _ => Err(type_err("Expected evaluated value but got lazy thunk", a)), - } - } -} - -impl TryFromArgument for NumberValue { - fn try_from_argument(a: &Argument) -> Result { - match a { - Argument::Eval(Value { - kind: Some(Kind::NumberValue(n)), - }) => Ok(*n), - _ => Err(type_err("Expected number", a)), - } - } -} - -impl TryFromArgument for i64 { - fn try_from_argument(a: &Argument) -> Result { - match a { - Argument::Eval(Value { - kind: Some(Kind::NumberValue(n)), - }) => number_to_i64_lossy(n).ok_or_else(|| type_err("Expected number", a)), - _ => Err(type_err("Expected number", a)), - } - } -} - -impl TryFromArgument for f64 { - fn try_from_argument(a: &Argument) -> Result { - match a { - Argument::Eval(Value { - kind: Some(Kind::NumberValue(n)), - }) => number_to_f64(n).ok_or_else(|| type_err("Expected number", a)), - _ => Err(type_err("Expected number", a)), - } - } -} - -impl TryFromArgument for bool { - fn try_from_argument(a: &Argument) -> Result { - match a { - Argument::Eval(Value { - kind: Some(Kind::BoolValue(b)), - }) => Ok(*b), - _ => Err(type_err("Expected boolean", a)), - } - } -} - -impl TryFromArgument for String { - fn try_from_argument(a: &Argument) -> Result { - match a { - Argument::Eval(Value { - kind: Some(Kind::StringValue(s)), - }) => Ok(s.clone()), - _ => Err(type_err("Expected string", a)), - } - } -} - -impl TryFromArgument for Struct { - fn try_from_argument(a: &Argument) -> Result { - match a { - Argument::Eval(Value { - kind: Some(Kind::StructValue(s)), - }) => Ok(s.clone()), - _ => Err(type_err("Expected struct", a)), - } - } -} - -impl TryFromArgument for ListValue { - fn try_from_argument(a: &Argument) -> Result { - match a { - Argument::Eval(Value { - kind: Some(Kind::ListValue(list)), - }) => Ok(list.clone()), - _ => Err(Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!("Expected array (ListValue) but it was: {:?}", a), - ))), - } - } -} - -impl From for RuntimeError { - fn from(never: Infallible) -> Self { - match never {} - } -} diff --git a/crates/core/src/context/context.rs b/crates/core/src/context/context.rs deleted file mode 100644 index 206b444..0000000 --- a/crates/core/src/context/context.rs +++ /dev/null @@ -1,148 +0,0 @@ -use std::collections::HashMap; -use tucana::shared::{InputType, ReferenceValue, Value, value::Kind}; - -use crate::runtime::error::RuntimeError; - -#[derive(Clone)] -pub enum ContextResult { - Error(RuntimeError), - Success(Value), - NotFound, -} - -#[derive(Default)] -pub struct Context { - results: HashMap, - input_types: HashMap, - flow_input: Value, - current_node_id: i64, - runtime_trace_labels: Vec, -} - -impl Context { - pub fn new(flow_input: Value) -> Self { - Context { - results: HashMap::new(), - input_types: HashMap::new(), - flow_input, - current_node_id: 0, - runtime_trace_labels: Vec::new(), - } - } - - pub fn get_current_node_id(&mut self) -> i64 { - self.current_node_id - } - - pub fn set_current_node_id(&mut self, node_id: i64) { - self.current_node_id = node_id; - } - - pub fn get(&mut self, reference: ReferenceValue) -> ContextResult { - let target = match reference.target { - Some(t) => t, - None => return ContextResult::NotFound, - }; - - let res = match target { - tucana::shared::reference_value::Target::FlowInput(_) => self.get_flow_input(), - tucana::shared::reference_value::Target::NodeId(i) => self.get_result(i), - tucana::shared::reference_value::Target::InputType(input_type) => { - self.get_input_type(input_type) - } - }; - - if reference.paths.is_empty() { - return res; - } - - if let ContextResult::Success(value) = res { - let mut curr = value; - log::debug!("Tracing down value: {:?}", curr); - - for path in reference.paths { - if let Some(index) = path.array_index { - match curr.kind { - Some(ref kind) => match kind { - Kind::ListValue(list) => match list.values.get(index as usize) { - Some(x) => { - curr = x.clone(); - } - None => return ContextResult::NotFound, - }, - _ => return ContextResult::NotFound, - }, - None => return ContextResult::NotFound, - } - } - - if let Some(field_name) = path.path { - match curr.kind { - Some(ref kind) => { - if let Kind::StructValue(struct_value) = &kind { - match struct_value.fields.get(&field_name) { - Some(x) => { - log::debug!("Updating trace value to: {:?}", x); - curr = x.clone(); - } - None => return ContextResult::NotFound, - } - } - } - None => return ContextResult::NotFound, - } - } - } - - ContextResult::Success(curr) - } else { - res - } - } - - fn get_result(&mut self, id: i64) -> ContextResult { - match self.results.get(&id) { - None => ContextResult::NotFound, - Some(result) => result.clone(), - } - } - - fn get_flow_input(&mut self) -> ContextResult { - ContextResult::Success(self.flow_input.clone()) - } - - fn get_input_type(&mut self, input_type: InputType) -> ContextResult { - match self.input_types.get(&input_type) { - Some(v) => ContextResult::Success(v.clone()), - None => ContextResult::NotFound, - } - } - - pub fn clear_input_type(&mut self, input_type: InputType) { - self.input_types.remove(&input_type); - } - - pub fn insert_input_type(&mut self, input_type: InputType, value: Value) { - self.input_types.insert(input_type, value); - } - - pub fn insert_flow_input(&mut self, value: Value) { - self.flow_input = value; - } - - pub fn insert_success(&mut self, id: i64, value: Value) { - self.results.insert(id, ContextResult::Success(value)); - } - - pub fn insert_error(&mut self, id: i64, runtime_error: RuntimeError) { - self.results.insert(id, ContextResult::Error(runtime_error)); - } - - pub fn push_runtime_trace_label(&mut self, label: String) { - self.runtime_trace_labels.push(label); - } - - pub fn pop_runtime_trace_label(&mut self) -> Option { - self.runtime_trace_labels.pop() - } -} diff --git a/crates/core/src/context/executor/arguments.rs b/crates/core/src/context/executor/arguments.rs deleted file mode 100644 index 1d30b17..0000000 --- a/crates/core/src/context/executor/arguments.rs +++ /dev/null @@ -1,242 +0,0 @@ -use super::*; - -impl<'a> Executor<'a> { - /// Build arguments for a node from literals, references, or thunks. - /// - /// Arguments are recorded to the tracer for debugging and inspection. - /// Thunks are represented by the referenced node id. - pub(super) fn build_args( - &self, - node: &NodeFunction, - ctx: &mut Context, - tracer: &mut dyn ExecutionTracer, - frame_id: u64, - ) -> Result, RuntimeError> { - let mut args = Vec::with_capacity(node.parameters.len()); - - for (i, param) in node.parameters.iter().enumerate() { - let node_value = param.value.as_ref().ok_or_else(|| { - RuntimeError::simple_str("NodeValueNotFound", "Missing param value") - })?; - - let value = node_value.value.as_ref().ok_or_else(|| { - RuntimeError::simple_str("NodeValueNotFound", "Missing inner value") - })?; - - match value { - tucana::shared::node_value::Value::LiteralValue(v) => { - tracer.record_arg( - frame_id, - ArgTrace { - index: i, - kind: ArgKind::Literal, - preview: preview_value(v), - }, - ); - args.push(Argument::Eval(v.clone())); - } - - tucana::shared::node_value::Value::ReferenceValue(r) => match ctx.get(r.clone()) { - ContextResult::Success(v) => { - let reference = match r.target { - Some(ref_value) => match ref_value { - tucana::shared::reference_value::Target::FlowInput(_) => { - ReferenceKind::FlowInput - } - tucana::shared::reference_value::Target::NodeId(id) => { - ReferenceKind::Result { node_id: id } - } - tucana::shared::reference_value::Target::InputType(input_type) => { - ReferenceKind::InputType { - node_id: input_type.node_id, - input_index: input_type.input_index, - parameter_index: input_type.parameter_index, - } - } - }, - None => ReferenceKind::Empty, - }; - - tracer.record_arg( - frame_id, - ArgTrace { - index: i, - kind: ArgKind::Reference { - reference, - hit: true, - }, - preview: format!( - "ctx.get({}) -> {}", - preview_reference(r), - preview_value(&v) - ), - }, - ); - args.push(Argument::Eval(v)); - } - ContextResult::Error(e) => return Err(e), - ContextResult::NotFound => { - return Err(RuntimeError::simple_str( - "ReferenceValueNotFound", - "Reference not found in context", - )); - } - }, - - tucana::shared::node_value::Value::NodeFunctionId(id) => { - tracer.record_arg( - frame_id, - ArgTrace { - index: i, - kind: ArgKind::Thunk { - node_id: *id, - eager: false, - executed: false, - }, - preview: format!("thunk({})", id), - }, - ); - args.push(Argument::Thunk(*id)); - } - } - } - - Ok(args) - } - - /// Eagerly execute any argument marked as `Eager`. - /// - /// Lazy arguments are preserved as thunks until needed by a handler. - /// If an eager child fails, the failure bubbles up immediately. - pub(super) fn force_eager_args( - &self, - _node: &NodeFunction, - entry: &HandlerFunctionEntry, - args: &mut [Argument], - ctx: &mut Context, - tracer: &mut dyn ExecutionTracer, - parent_frame: u64, - ) -> Result<(), (Signal, Outcome)> { - for (i, arg) in args.iter_mut().enumerate() { - let mode = entry - .param_modes - .get(i) - .copied() - .unwrap_or(ParameterNode::Eager); - - if matches!(mode, ParameterNode::Eager) - && let Argument::Thunk(id) = *arg - { - tracer.mark_thunk(parent_frame, i, true, true); - let (child_sig, child_root) = self.execute_call(id, ctx, tracer); - - tracer.link_child( - parent_frame, - child_root, - EdgeKind::EagerCall { arg_index: i }, - ); - - match child_sig { - Signal::Success(v) => { - *arg = Argument::Eval(v); - } - Signal::Return(v) => { - // `return` only exits the child context and yields a value upward. - *arg = Argument::Eval(v); - } - s => { - return Err(( - s, - Outcome::Failure { - error_preview: "Eager child failed".into(), - }, - )); - } - } - } - } - - Ok(()) - } - - /// Resolve all arguments for a remote call. - /// - /// Remote execution requires concrete values, so any thunks are executed - /// eagerly and replaced with their resulting `Value`. - pub(super) fn resolve_remote_args( - &self, - args: &mut [Argument], - ctx: &mut Context, - tracer: &mut dyn ExecutionTracer, - parent_frame: u64, - ) -> Result, (Signal, Outcome)> { - let mut values = Vec::with_capacity(args.len()); - - for (i, arg) in args.iter_mut().enumerate() { - match arg { - Argument::Eval(v) => values.push(v.clone()), - Argument::Thunk(id) => { - tracer.mark_thunk(parent_frame, i, true, true); - let (child_sig, child_root) = self.execute_call(*id, ctx, tracer); - if child_root != 0 { - tracer.link_child( - parent_frame, - child_root, - EdgeKind::EagerCall { arg_index: i }, - ); - } - - match child_sig { - Signal::Success(v) => { - *arg = Argument::Eval(v.clone()); - values.push(v); - } - Signal::Return(v) => { - // `return` only exits the child context and yields a value upward. - *arg = Argument::Eval(v.clone()); - values.push(v); - } - s => { - return Err(( - s, - Outcome::Failure { - error_preview: "Eager child failed".into(), - }, - )); - } - } - } - } - } - - Ok(values) - } - - /// Build a remote execution request from a node and its resolved values. - /// - /// Values are mapped to their parameter ids for the remote runtime. - pub(super) fn build_remote_request( - &self, - node: &NodeFunction, - values: Vec, - ) -> Result { - if node.parameters.len() != values.len() { - return Err(RuntimeError::simple_str( - "RemoteParameterMismatch", - "Remote parameter count mismatch", - )); - } - - let mut fields = HashMap::new(); - for (param, value) in node.parameters.iter().zip(values.into_iter()) { - fields.insert(param.runtime_parameter_id.clone(), value); - } - let id = Uuid::new_v4(); - Ok(ExecutionRequest { - execution_identifier: id.to_string(), - function_identifier: node.runtime_function_id.clone(), - parameters: Some(Struct { fields }), - project_id: 0, - }) - } -} diff --git a/crates/core/src/context/executor/execution_loop.rs b/crates/core/src/context/executor/execution_loop.rs deleted file mode 100644 index 7c73d51..0000000 --- a/crates/core/src/context/executor/execution_loop.rs +++ /dev/null @@ -1,103 +0,0 @@ -use super::*; - -#[derive(Debug)] -struct CallState { - current_node_id: i64, - call_root_frame: Option, - previous_frame: Option, -} - -impl CallState { - fn new(start_node_id: i64) -> Self { - Self { - current_node_id: start_node_id, - call_root_frame: None, - previous_frame: None, - } - } - - fn set_root_if_missing(&mut self, frame_id: u64) { - if self.call_root_frame.is_none() { - self.call_root_frame = Some(frame_id); - } - } - - fn link_next_edge(&mut self, frame_id: u64, tracer: &mut dyn ExecutionTracer) { - if let Some(prev) = self.previous_frame { - tracer.link_child(prev, frame_id, EdgeKind::Next); - } - self.previous_frame = Some(frame_id); - } - - fn advance(&mut self, next_node_id: Option) -> bool { - if let Some(next) = next_node_id { - self.current_node_id = next; - true - } else { - false - } - } - - fn root_frame(&self) -> u64 { - self.call_root_frame - .expect("call root frame must be set after first node execution") - } -} - -impl<'a> Executor<'a> { - /// Main execution loop. - /// - /// Executes nodes one-by-one along the `next_node_id` chain until a - /// non-success signal is produced or the chain ends. - pub(super) fn execute_call( - &self, - start_node_id: i64, - ctx: &mut Context, - tracer: &mut dyn ExecutionTracer, - ) -> (Signal, u64) { - let mut state = CallState::new(start_node_id); - - loop { - let (signal, frame_id) = self.execute_single_node(state.current_node_id, ctx, tracer); - state.set_root_if_missing(frame_id); - state.link_next_edge(frame_id, tracer); - - match signal { - Signal::Success(_) => { - let node = self - .nodes - .get(&state.current_node_id) - .expect("current node must exist while executing success path"); - - if state.advance(node.next_node_id) { - continue; - } - - return (signal, state.root_frame()); - } - - Signal::Respond(v) => { - let node = self - .nodes - .get(&state.current_node_id) - .expect("current node must exist while executing respond path"); - - if let Some(emit) = self.respond_emitter { - emit(v.clone()); - } - - // Respond behaves like success for flow continuation and references. - ctx.insert_success(node.database_id, v.clone()); - - if state.advance(node.next_node_id) { - continue; - } - - return (Signal::Success(v), state.root_frame()); - } - - _ => return (signal, state.root_frame()), - } - } - } -} diff --git a/crates/core/src/context/executor/formatting.rs b/crates/core/src/context/executor/formatting.rs deleted file mode 100644 index e2d912c..0000000 --- a/crates/core/src/context/executor/formatting.rs +++ /dev/null @@ -1,56 +0,0 @@ -use super::*; - -pub(super) fn preview_value(value: &Value) -> String { - format_value_json(value) -} - -fn format_value_json(value: &Value) -> String { - match value.kind.as_ref() { - Some(Kind::NumberValue(v)) => match v.number { - Some(kind) => match kind { - tucana::shared::number_value::Number::Integer(i) => i.to_string(), - tucana::shared::number_value::Number::Float(f) => f.to_string(), - }, - _ => "null".to_string(), - }, - Some(Kind::BoolValue(v)) => v.to_string(), - Some(Kind::StringValue(v)) => format!("{:?}", v), - Some(Kind::NullValue(_)) | None => "null".to_string(), - Some(Kind::ListValue(list)) => { - let mut parts = Vec::new(); - for item in list.values.iter() { - parts.push(format_value_json(item)); - } - format!("[{}]", parts.join(", ")) - } - Some(Kind::StructValue(struct_value)) => { - let mut keys: Vec<_> = struct_value.fields.keys().collect(); - keys.sort(); - let mut parts = Vec::new(); - for key in keys.iter() { - if let Some(v) = struct_value.fields.get(*key) { - parts.push(format!("{:?}: {}", key, format_value_json(v))); - } - } - format!("{{{}}}", parts.join(", ")) - } - } -} - -pub(super) fn preview_reference(r: &tucana::shared::ReferenceValue) -> String { - let target = match &r.target { - Some(Target::FlowInput(_)) => "flow_input".to_string(), - Some(Target::NodeId(id)) => format!("node({})", id), - Some(Target::InputType(input_type)) => format!( - "input(node={},param={},input={})", - input_type.node_id, input_type.parameter_index, input_type.input_index - ), - None => "empty".to_string(), - }; - - if r.paths.is_empty() { - target - } else { - format!("{}+paths({})", target, r.paths.len()) - } -} diff --git a/crates/core/src/context/executor/mod.rs b/crates/core/src/context/executor/mod.rs deleted file mode 100644 index 5588c4e..0000000 --- a/crates/core/src/context/executor/mod.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! Executor for flow node execution. -//! -//! Execution model overview: -//! - The executor walks a linear "next" chain starting from `starting_node_id`. -//! - Each node can call into other nodes through lazy arguments. -//! - A node marked as remote is executed via `RemoteRuntime`. -//! - The executor is synchronous; remote calls are awaited via `block_on`. -//! -//! Remote execution: -//! - A node is considered remote based on `is_remote(&node)`. -//! - Remote args are fully resolved to concrete `Value`s before sending. -//! - The request parameters are mapped by `runtime_parameter_id`. -//! - Remote responses are mapped into `Signal::Success` or `Signal::Failure`. -//! -//! Tracing: -//! - Each node execution produces a trace frame with arguments and outcome. -//! - Child executions are linked with `EdgeKind` to reflect eager or runtime calls. -//! -//! Error behavior: -//! - Missing nodes/functions yield `Signal::Failure`. -//! - Remote failures are mapped to `RuntimeError`. -//! - The executor commits all final outcomes into the `Context`. - -use crate::context::argument::{Argument, ParameterNode}; -use crate::context::context::{Context, ContextResult}; -use crate::context::registry::{FunctionStore, HandlerFunctionEntry}; -use crate::context::signal::Signal; -use crate::debug::trace::{ArgKind, ArgTrace, EdgeKind, Outcome, ReferenceKind}; -use crate::debug::tracer::{ExecutionTracer, Tracer}; -use crate::runtime::error::RuntimeError; -use crate::runtime::remote::RemoteRuntime; - -use futures_lite::future::block_on; -use std::collections::HashMap; -use tucana::aquila::ExecutionRequest; -use tucana::shared::reference_value::Target; -use tucana::shared::value::Kind; -use tucana::shared::{NodeFunction, Struct, Value}; -use uuid::Uuid; - -mod arguments; -mod execution_loop; -mod formatting; -mod node_execution; - -use formatting::{preview_reference, preview_value}; - -#[cfg(test)] -mod tests; - -/// Executes a flow graph by repeatedly evaluating nodes. -/// -/// The executor is intentionally stateless with respect to the runtime: -/// it borrows the function registry and graph, and mutates only the `Context`. -pub struct Executor<'a> { - // Registered Runtime Functions - functions: &'a FunctionStore, - // Nodes to execute - nodes: HashMap, - // Connection for Remote Function Execution => Actions - remote: Option<&'a dyn RemoteRuntime>, - // Optional side-effect hook triggered whenever a respond signal is emitted. - respond_emitter: Option<&'a dyn Fn(Value)>, -} - -/// Determines whether a node should be executed remotely. -/// -/// The current policy treats any node whose `definition_source` is not `"taurus"` -/// as a remote node. -fn is_remote(node: &NodeFunction) -> bool { - if node.definition_source.is_empty() { - log::warn!( - "Found empty definition source, taking runtime as origin for node id: {}", - node.database_id - ); - return false; - } - - node.definition_source != "taurus" -} - -impl<'a> Executor<'a> { - /// Create a new executor for the given function store and node map. - /// - /// This does not attach a remote runtime. Remote nodes will error unless - /// a runtime is provided via `with_remote_runtime`. - pub fn new(functions: &'a FunctionStore, nodes: HashMap) -> Self { - Self { - functions, - nodes, - remote: None, - respond_emitter: None, - } - } - - /// Attach a remote runtime for executing nodes marked as remote. - /// - /// This is a builder-style method for ergonomic setup: - /// `Executor::new(...).with_remote_runtime(&runtime)`. - pub fn with_remote_runtime(mut self, remote: &'a dyn RemoteRuntime) -> Self { - self.remote = Some(remote); - self - } - - /// Attach a callback that is invoked for every emitted respond value. - pub fn with_respond_emitter(mut self, emitter: &'a dyn Fn(Value)) -> Self { - self.respond_emitter = Some(emitter); - self - } - - /// This is now the ONLY execution entry point. - /// - /// - `start_node_id` is the first node in the flow. - /// - `ctx` is mutated in-place with results and errors. - /// - `with_trace` controls whether the trace is printed on completion. - pub fn execute(&self, start_node_id: i64, ctx: &mut Context, with_trace: bool) -> Signal { - let mut tracer = Tracer::new(); - - let (signal, _root_frame) = self.execute_call(start_node_id, ctx, &mut tracer); - - if with_trace && let Some(run) = tracer.take_run() { - println!("{}", crate::debug::render::render_trace(&run)); - } - signal - } -} diff --git a/crates/core/src/context/executor/node_execution.rs b/crates/core/src/context/executor/node_execution.rs deleted file mode 100644 index 1c916a8..0000000 --- a/crates/core/src/context/executor/node_execution.rs +++ /dev/null @@ -1,258 +0,0 @@ -use super::*; - -struct NodeHandler<'executor, 'deps> { - executor: &'executor Executor<'deps>, - node: NodeFunction, -} - -impl<'executor, 'deps> NodeHandler<'executor, 'deps> { - fn new(executor: &'executor Executor<'deps>, node: NodeFunction) -> Self { - Self { executor, node } - } - - fn execute(&self, ctx: &mut Context, tracer: &mut dyn ExecutionTracer) -> (Signal, u64) { - if is_remote(&self.node) { - self.execute_remote(ctx, tracer) - } else { - self.execute_local(ctx, tracer) - } - } - - fn execute_remote(&self, ctx: &mut Context, tracer: &mut dyn ExecutionTracer) -> (Signal, u64) { - let remote = match self.executor.remote { - Some(r) => r, - None => { - let err = RuntimeError::simple( - "RemoteRuntimeNotConfigured", - "Remote runtime not configured".to_string(), - ); - return (Signal::Failure(err), 0); - } - }; - - let frame_id = tracer.enter_node( - self.node.database_id, - self.node.runtime_function_id.as_str(), - ); - - let mut args = match self.executor.build_args(&self.node, ctx, tracer, frame_id) { - Ok(a) => a, - Err(e) => { - ctx.insert_error(self.node.database_id, e.clone()); - tracer.exit_node( - frame_id, - Outcome::Failure { - error_preview: format!("{:#?}", e), - }, - ); - return (Signal::Failure(e), frame_id); - } - }; - - let values = match self - .executor - .resolve_remote_args(&mut args, ctx, tracer, frame_id) - { - Ok(v) => v, - Err((sig, outcome)) => { - tracer.exit_node(frame_id, outcome); - return (sig, frame_id); - } - }; - - let request = match self.executor.build_remote_request(&self.node, values) { - Ok(r) => r, - Err(e) => { - ctx.insert_error(self.node.database_id, e.clone()); - tracer.exit_node( - frame_id, - Outcome::Failure { - error_preview: format!("{:#?}", e), - }, - ); - return (Signal::Failure(e), frame_id); - } - }; - - let remote_result = - block_on(remote.execute_remote(self.node.definition_source.clone(), request)); - let signal = match remote_result { - Ok(value) => Signal::Success(value), - Err(err) => Signal::Failure(err), - }; - - let final_signal = self.commit_result(signal, ctx, tracer, frame_id); - (final_signal, frame_id) - } - - fn execute_local(&self, ctx: &mut Context, tracer: &mut dyn ExecutionTracer) -> (Signal, u64) { - let entry = match self - .executor - .functions - .get(self.node.runtime_function_id.as_str()) - { - Some(e) => e, - None => { - let err = RuntimeError::simple( - "FunctionNotFound", - format!("Function {} not found", self.node.runtime_function_id), - ); - return (Signal::Failure(err), 0); - } - }; - - let frame_id = tracer.enter_node( - self.node.database_id, - self.node.runtime_function_id.as_str(), - ); - - // ---- Build args - let mut args = match self.executor.build_args(&self.node, ctx, tracer, frame_id) { - Ok(a) => a, - Err(e) => { - ctx.insert_error(self.node.database_id, e.clone()); - tracer.exit_node( - frame_id, - Outcome::Failure { - error_preview: format!("{:#?}", e), - }, - ); - return (Signal::Failure(e), frame_id); - } - }; - - // ---- Force eager args - if let Err((sig, outcome)) = self - .executor - .force_eager_args(&self.node, entry, &mut args, ctx, tracer, frame_id) - { - tracer.exit_node(frame_id, outcome); - return (sig, frame_id); - } - - // ---- Invoke handler - let result = self.invoke_handler(entry, &args, ctx, tracer, frame_id); - - // ---- Commit result - let final_signal = self.commit_result(result, ctx, tracer, frame_id); - (final_signal, frame_id) - } - - /// Invoke a local handler with a closure for lazy execution. - /// - /// The closure will evaluate a thunk node and link its trace to the - /// current execution frame. - fn invoke_handler( - &self, - entry: &HandlerFunctionEntry, - args: &[Argument], - ctx: &mut Context, - tracer: &mut dyn ExecutionTracer, - frame_id: u64, - ) -> Signal { - let executor = self.executor; - let mut run = |node_id: i64, ctx: &mut Context| { - tracer.mark_thunk_executed_by_node(frame_id, node_id); - let label = ctx.pop_runtime_trace_label(); - let (sig, child_root) = executor.execute_call(node_id, ctx, tracer); - tracer.link_child(frame_id, child_root, EdgeKind::RuntimeCall { label }); - match sig { - // `return` only exits the child context and is a value for the caller. - Signal::Return(v) => Signal::Success(v), - other => other, - } - }; - - (entry.handler)(args, ctx, &mut run) - } - - /// Persist the final signal into the context and close the trace frame. - fn commit_result( - &self, - result: Signal, - ctx: &mut Context, - tracer: &mut dyn ExecutionTracer, - frame_id: u64, - ) -> Signal { - match result { - Signal::Success(v) => { - ctx.insert_success(self.node.database_id, v.clone()); - - tracer.exit_node( - frame_id, - Outcome::Success { - value_preview: preview_value(&v), - }, - ); - - Signal::Success(v) - } - - Signal::Failure(e) => { - ctx.insert_error(self.node.database_id, e.clone()); - - tracer.exit_node( - frame_id, - Outcome::Failure { - error_preview: format!("{:#?}", e), - }, - ); - - Signal::Failure(e) - } - - Signal::Return(v) => { - tracer.exit_node( - frame_id, - Outcome::Return { - value_preview: preview_value(&v), - }, - ); - Signal::Return(v) - } - - Signal::Respond(v) => { - tracer.exit_node( - frame_id, - Outcome::Respond { - value_preview: preview_value(&v), - }, - ); - Signal::Respond(v) - } - - Signal::Stop => { - tracer.exit_node(frame_id, Outcome::Stop); - Signal::Stop - } - } - } -} - -impl<'a> Executor<'a> { - /// Execute a single node and return its signal and trace frame id. - /// - /// This handles: - /// - Node lookup - /// - Remote vs local dispatch - /// - Argument building and eager evaluation - /// - Handler invocation and result commit - pub(super) fn execute_single_node( - &self, - node_id: i64, - ctx: &mut Context, - tracer: &mut dyn ExecutionTracer, - ) -> (Signal, u64) { - ctx.set_current_node_id(node_id); - let node = match self.nodes.get(&node_id) { - Some(n) => n.clone(), - None => { - let err = - RuntimeError::simple("NodeNotFound", format!("Node {} not found", node_id)); - return (Signal::Failure(err), 0); - } - }; - - NodeHandler::new(self, node).execute(ctx, tracer) - } -} diff --git a/crates/core/src/context/executor/tests.rs b/crates/core/src/context/executor/tests.rs deleted file mode 100644 index 7e23006..0000000 --- a/crates/core/src/context/executor/tests.rs +++ /dev/null @@ -1,467 +0,0 @@ -use super::*; -use crate::context::registry::{HandlerFn, IntoFunctionEntry}; -use std::cell::RefCell; -use tucana::shared::number_value::Number; -use tucana::shared::{NodeFunction, NumberValue, ReferenceValue}; - -fn num(i: i64) -> Value { - Value { - kind: Some(Kind::NumberValue(NumberValue { - number: Some(Number::Integer(i)), - })), - } -} - -fn extract_i64(v: &Value) -> i64 { - match &v.kind { - Some(Kind::NumberValue(NumberValue { - number: Some(Number::Integer(i)), - })) => *i, - other => panic!("Expected integer number value, got {:?}", other), - } -} - -fn node(id: i64, function_id: &str, next: Option) -> NodeFunction { - NodeFunction { - database_id: id, - runtime_function_id: function_id.to_string(), - definition_source: "taurus".to_string(), - next_node_id: next, - ..Default::default() - } -} - -fn node_reference(node_id: i64) -> ReferenceValue { - ReferenceValue { - paths: vec![], - target: Some(Target::NodeId(node_id)), - } -} - -fn assert_node_success(ctx: &mut Context, node_id: i64, expected: i64) { - match ctx.get(node_reference(node_id)) { - ContextResult::Success(v) => assert_eq!(extract_i64(&v), expected), - ContextResult::Error(_) => { - panic!("Expected success result for node {}, got error", node_id) - } - ContextResult::NotFound => { - panic!( - "Expected success result for node {}, got not found", - node_id - ) - } - } -} - -fn assert_node_error(ctx: &mut Context, node_id: i64, expected_name: &str) { - match ctx.get(node_reference(node_id)) { - ContextResult::Error(err) => assert_eq!(err.name, expected_name), - ContextResult::Success(_) => { - panic!("Expected error result for node {}, got success", node_id) - } - ContextResult::NotFound => { - panic!("Expected error result for node {}, got not found", node_id) - } - } -} - -fn assert_node_not_found(ctx: &mut Context, node_id: i64) { - match ctx.get(node_reference(node_id)) { - ContextResult::NotFound => {} - ContextResult::Success(_) => { - panic!("Expected no result for node {}, got success", node_id) - } - ContextResult::Error(_) => { - panic!("Expected no result for node {}, got error", node_id) - } - } -} - -fn success_one_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - Signal::Success(num(1)) -} - -fn success_two_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - Signal::Success(num(2)) -} - -fn success_three_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - Signal::Success(num(3)) -} - -fn failure_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - Signal::Failure(RuntimeError::simple_str("TestFailure", "expected failure")) -} - -fn return_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - Signal::Return(num(9)) -} - -fn stop_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - Signal::Stop -} - -fn respond_eleven_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - Signal::Respond(num(11)) -} - -fn respond_twentytwo_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - Signal::Respond(num(22)) -} - -fn respond_fortyfour_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - Signal::Respond(num(44)) -} - -fn return_seventyseven_handler( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - Signal::Return(num(77)) -} - -fn runtime_call_child_two_handler( - _args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - match run(2, ctx) { - Signal::Success(_) => Signal::Success(num(55)), - other => other, - } -} - -fn runtime_call_child_return_handler( - _args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - run(2, ctx) -} - -#[test] -fn success_signal_continues_through_next_nodes() { - let mut store = FunctionStore::new(); - store.populate(vec![ - ( - "test::success_one", - HandlerFn::eager(success_one_handler, 0), - ), - ( - "test::success_two", - HandlerFn::eager(success_two_handler, 0), - ), - ]); - - let mut nodes = HashMap::new(); - nodes.insert(1, node(1, "test::success_one", Some(2))); - nodes.insert(2, node(2, "test::success_two", None)); - - let mut ctx = Context::default(); - let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); - - match signal { - Signal::Success(v) => assert_eq!(extract_i64(&v), 2), - other => panic!("Expected success signal, got {:?}", other), - } - - assert_node_success(&mut ctx, 1, 1); - assert_node_success(&mut ctx, 2, 2); -} - -#[test] -fn failure_signal_stops_flow_and_persists_error() { - let mut store = FunctionStore::new(); - store.populate(vec![ - ( - "test::success_one", - HandlerFn::eager(success_one_handler, 0), - ), - ("test::failure", HandlerFn::eager(failure_handler, 0)), - ( - "test::success_three", - HandlerFn::eager(success_three_handler, 0), - ), - ]); - - let mut nodes = HashMap::new(); - nodes.insert(1, node(1, "test::success_one", Some(2))); - nodes.insert(2, node(2, "test::failure", Some(3))); - nodes.insert(3, node(3, "test::success_three", None)); - - let mut ctx = Context::default(); - let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); - - match signal { - Signal::Failure(err) => assert_eq!(err.name, "TestFailure"), - other => panic!("Expected failure signal, got {:?}", other), - } - - assert_node_success(&mut ctx, 1, 1); - assert_node_error(&mut ctx, 2, "TestFailure"); - assert_node_not_found(&mut ctx, 3); -} - -#[test] -fn return_signal_breaks_execution_and_skips_next_nodes() { - let mut store = FunctionStore::new(); - store.populate(vec![ - ("test::return", HandlerFn::eager(return_handler, 0)), - ( - "test::success_three", - HandlerFn::eager(success_three_handler, 0), - ), - ]); - - let mut nodes = HashMap::new(); - nodes.insert(1, node(1, "test::return", Some(2))); - nodes.insert(2, node(2, "test::success_three", None)); - - let mut ctx = Context::default(); - let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); - - match signal { - Signal::Return(v) => assert_eq!(extract_i64(&v), 9), - other => panic!("Expected return signal, got {:?}", other), - } - - assert_node_not_found(&mut ctx, 1); - assert_node_not_found(&mut ctx, 2); -} - -#[test] -fn stop_signal_stops_execution_and_skips_next_nodes() { - let mut store = FunctionStore::new(); - store.populate(vec![ - ("test::stop", HandlerFn::eager(stop_handler, 0)), - ( - "test::success_three", - HandlerFn::eager(success_three_handler, 0), - ), - ]); - - let mut nodes = HashMap::new(); - nodes.insert(1, node(1, "test::stop", Some(2))); - nodes.insert(2, node(2, "test::success_three", None)); - - let mut ctx = Context::default(); - let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); - - assert!(matches!(signal, Signal::Stop)); - assert_node_not_found(&mut ctx, 1); - assert_node_not_found(&mut ctx, 2); -} - -#[test] -fn respond_signal_emits_for_each_node_and_execution_continues() { - let mut store = FunctionStore::new(); - store.populate(vec![ - ( - "test::respond_eleven", - HandlerFn::eager(respond_eleven_handler, 0), - ), - ( - "test::respond_twentytwo", - HandlerFn::eager(respond_twentytwo_handler, 0), - ), - ( - "test::success_three", - HandlerFn::eager(success_three_handler, 0), - ), - ]); - - let mut nodes = HashMap::new(); - nodes.insert(1, node(1, "test::respond_eleven", Some(2))); - nodes.insert(2, node(2, "test::respond_twentytwo", Some(3))); - nodes.insert(3, node(3, "test::success_three", None)); - - let emitted = RefCell::new(Vec::::new()); - let emitter = |v: Value| { - emitted.borrow_mut().push(v); - }; - - let mut ctx = Context::default(); - let signal = Executor::new(&store, nodes) - .with_respond_emitter(&emitter) - .execute(1, &mut ctx, false); - - match signal { - Signal::Success(v) => assert_eq!(extract_i64(&v), 3), - other => panic!("Expected final success signal, got {:?}", other), - } - - let emitted_values = emitted.borrow(); - assert_eq!(emitted_values.len(), 2); - assert_eq!(extract_i64(&emitted_values[0]), 11); - assert_eq!(extract_i64(&emitted_values[1]), 22); - - assert_node_success(&mut ctx, 1, 11); - assert_node_success(&mut ctx, 2, 22); - assert_node_success(&mut ctx, 3, 3); -} - -#[test] -fn respond_signal_emits_inside_runtime_call_and_parent_continues() { - let mut store = FunctionStore::new(); - store.populate(vec![ - ( - "test::runtime_call_child_two", - HandlerFn::eager(runtime_call_child_two_handler, 0), - ), - ( - "test::respond_fortyfour", - HandlerFn::eager(respond_fortyfour_handler, 0), - ), - ]); - - let mut nodes = HashMap::new(); - nodes.insert(1, node(1, "test::runtime_call_child_two", None)); - nodes.insert(2, node(2, "test::respond_fortyfour", None)); - - let emitted = RefCell::new(Vec::::new()); - let emitter = |v: Value| { - emitted.borrow_mut().push(v); - }; - - let mut ctx = Context::default(); - let signal = Executor::new(&store, nodes) - .with_respond_emitter(&emitter) - .execute(1, &mut ctx, false); - - match signal { - Signal::Success(v) => assert_eq!(extract_i64(&v), 55), - other => panic!("Expected final success signal, got {:?}", other), - } - - let emitted_values = emitted.borrow(); - assert_eq!(emitted_values.len(), 1); - assert_eq!(extract_i64(&emitted_values[0]), 44); - - assert_node_success(&mut ctx, 1, 55); - assert_node_success(&mut ctx, 2, 44); -} - -#[test] -fn return_signal_in_child_context_continues_parent_flow() { - let mut store = FunctionStore::new(); - store.populate(vec![ - ( - "test::runtime_call_child_return", - HandlerFn::eager(runtime_call_child_return_handler, 0), - ), - ( - "test::return_seventyseven", - HandlerFn::eager(return_seventyseven_handler, 0), - ), - ( - "test::success_three", - HandlerFn::eager(success_three_handler, 0), - ), - ]); - - let mut nodes = HashMap::new(); - nodes.insert(1, node(1, "test::runtime_call_child_return", Some(3))); - nodes.insert(2, node(2, "test::return_seventyseven", None)); - nodes.insert(3, node(3, "test::success_three", None)); - - let mut ctx = Context::default(); - let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); - - match signal { - Signal::Success(v) => assert_eq!(extract_i64(&v), 3), - other => panic!("Expected final success signal, got {:?}", other), - } - - // Child return exits its own context and is treated as value by parent node. - assert_node_success(&mut ctx, 1, 77); - assert_node_not_found(&mut ctx, 2); - assert_node_success(&mut ctx, 3, 3); -} - -#[test] -fn child_return_skips_child_next_but_parent_next_runs() { - // Graph: - // node1 -> next node4, and node1 executes child node2 via run(...) - // node2 -> RETURN and has next node3 - // node3 must NOT run (because node2 returned) - // node4 must run (parent chain continues) - let mut store = FunctionStore::new(); - store.populate(vec![ - ( - "test::runtime_call_child_return", - HandlerFn::eager(runtime_call_child_return_handler, 0), - ), - ( - "test::return_seventyseven", - HandlerFn::eager(return_seventyseven_handler, 0), - ), - ( - "test::success_two", - HandlerFn::eager(success_two_handler, 0), - ), - ( - "test::success_three", - HandlerFn::eager(success_three_handler, 0), - ), - ]); - - let mut nodes = HashMap::new(); - nodes.insert(1, node(1, "test::runtime_call_child_return", Some(4))); - nodes.insert(2, node(2, "test::return_seventyseven", Some(3))); - nodes.insert(3, node(3, "test::success_two", None)); - nodes.insert(4, node(4, "test::success_three", None)); - - let mut ctx = Context::default(); - let signal = Executor::new(&store, nodes).execute(1, &mut ctx, false); - - match signal { - Signal::Success(v) => assert_eq!(extract_i64(&v), 3), - other => panic!("Expected final success signal, got {:?}", other), - } - - // node1 ran and received child return value - assert_node_success(&mut ctx, 1, 77); - // node2 returned, so it has no success/error entry in context - assert_node_not_found(&mut ctx, 2); - // node3 is child next node and must be skipped due to return in node2 - assert_node_not_found(&mut ctx, 3); - // parent next node still executes - assert_node_success(&mut ctx, 4, 3); -} diff --git a/crates/core/src/context/macros.rs b/crates/core/src/context/macros.rs deleted file mode 100644 index ed4644f..0000000 --- a/crates/core/src/context/macros.rs +++ /dev/null @@ -1,53 +0,0 @@ -/// Pulls typed parameters from a slice of `Argument` using your `TryFromArgument` -/// impls. Fails early with your `Signal::Failure(RuntimeError::simple(...))`. -macro_rules! args { - ($args_ident:ident => $( $name:ident : $ty:ty ),+ $(,)?) => { - // Arity check - let __expected: usize = 0usize $(+ { let _ = ::core::any::type_name::<$ty>(); 1usize })*; - if $args_ident.len() != __expected { - return $crate::context::signal::Signal::Failure( - $crate::runtime::error::RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!("Expected {__expected} args but received {}", $args_ident.len()), - ) - ); - } - - // Typed extraction - let mut __i: usize = 0; - $( - let $name: $ty = match < - $ty as $crate::context::argument::TryFromArgument - >::try_from_argument(& $args_ident[__i]) { - Ok(v) => v, - Err(sig) => { - log::debug!( - "Failed to parse argument '{}' (index {}, type {})", - stringify!($name), - __i, - ::core::any::type_name::<$ty>(), - ); - return sig; - } - }; - __i += 1; - )+ - }; -} - -/// Asserts there are no arguments. -macro_rules! no_args { - ($args_ident:ident) => { - if !$args_ident.is_empty() { - return $crate::context::signal::Signal::Failure( - $crate::runtime::error::RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!("Expected 0 args but received {}", $args_ident.len()), - ), - ); - } - }; -} - -pub(crate) use args; -pub(crate) use no_args; diff --git a/crates/core/src/context/mod.rs b/crates/core/src/context/mod.rs deleted file mode 100644 index 9ca0f43..0000000 --- a/crates/core/src/context/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod argument; -pub mod context; -pub mod executor; -pub mod macros; -pub mod registry; -pub mod signal; diff --git a/crates/core/src/context/registry.rs b/crates/core/src/context/registry.rs deleted file mode 100644 index 633eac2..0000000 --- a/crates/core/src/context/registry.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::context::argument::{Argument, ParameterNode}; -use crate::context::context::Context; -use crate::context::signal::Signal; -use crate::runtime::functions::collect; -use std::collections::HashMap; - -/// HandlerFm -/// - For eager params, the executor will already convert them to Argument::Eval(Value). -/// - For lazy params, the executor will pass Argument::Thunk(node_id). -/// - If a handler wants to execute a lazy arg, it calls run(node_id). -pub type HandlerFn = fn( - args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal; - -pub struct HandlerFunctionEntry { - pub handler: HandlerFn, - pub param_modes: Vec, -} - -/// Holds all registered handlers. -pub struct FunctionStore { - functions: HashMap, -} - -impl Default for FunctionStore { - fn default() -> Self { - let mut store = Self::new(); - store.populate(collect()); - store - } -} - -pub trait IntoFunctionEntry { - fn into_function_entry(self, param: Vec) -> HandlerFunctionEntry; - fn eager(self, param_amount: i8) -> HandlerFunctionEntry; - fn lazy(self, param_amount: i8) -> HandlerFunctionEntry; -} - -impl IntoFunctionEntry for HandlerFn { - fn into_function_entry(self, param: Vec) -> HandlerFunctionEntry { - HandlerFunctionEntry { - handler: self, - param_modes: param, - } - } - - fn eager(self, param_amount: i8) -> HandlerFunctionEntry { - let mut params = vec![]; - - for _ in 0..param_amount { - params.push(ParameterNode::Eager) - } - - HandlerFunctionEntry { - handler: self, - param_modes: params, - } - } - - fn lazy(self, param_amount: i8) -> HandlerFunctionEntry { - let mut params = vec![]; - - for _ in 0..param_amount { - params.push(ParameterNode::Lazy) - } - - HandlerFunctionEntry { - handler: self, - param_modes: params, - } - } -} - -impl FunctionStore { - /// Create a new, empty store. - pub fn new() -> Self { - FunctionStore { - functions: HashMap::new(), - } - } - - /// Look up a handler by its ID. - pub fn get(&self, id: &str) -> Option<&HandlerFunctionEntry> { - self.functions.get(id) - } - - /// Execute all the registration closures to populate the map. - pub fn populate(&mut self, regs: Vec<(&'static str, HandlerFunctionEntry)>) { - for (id, func) in regs { - self.functions.insert(id.to_string(), func); - } - } -} diff --git a/crates/core/src/context/signal.rs b/crates/core/src/context/signal.rs deleted file mode 100644 index aa11e0b..0000000 --- a/crates/core/src/context/signal.rs +++ /dev/null @@ -1,38 +0,0 @@ -use tucana::shared::Value; - -use crate::runtime::error::RuntimeError; - -#[derive(Debug)] -pub enum Signal { - // Will be signaled if a function has been executed successfully - Success(Value), - // Will be signaled if - // - a function receives wrong parameter - // - a function throws an error - // - taurus itself throws an error - // - will stop the execution of the flow completely - Failure(RuntimeError), - // Will be signaled if the `return` function has been executed - // - will break the current context and return the value to the upper node - Return(Value), - // Will be signaled if the `respond` function has been executed - // - will not stop (will continue) the execution of the flow - // - will emit a respond signal which can be handled by the runtime holder - Respond(Value), - // Will be signaled if the `stop` function has been executed - // - will stop the execution of the flow completely - Stop, -} - -impl PartialEq for Signal { - fn eq(&self, other: &Self) -> bool { - matches!( - (self, other), - (Signal::Success(_), Signal::Success(_)) - | (Signal::Failure(_), Signal::Failure(_)) - | (Signal::Return(_), Signal::Return(_)) - | (Signal::Stop, Signal::Stop) - | (Signal::Respond(_), Signal::Respond(_)) - ) - } -} diff --git a/crates/core/src/debug/mod.rs b/crates/core/src/debug/mod.rs deleted file mode 100644 index f30265c..0000000 --- a/crates/core/src/debug/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod render; -pub mod trace; -pub mod tracer; diff --git a/crates/core/src/debug/render.rs b/crates/core/src/debug/render.rs deleted file mode 100644 index 40b44c3..0000000 --- a/crates/core/src/debug/render.rs +++ /dev/null @@ -1,208 +0,0 @@ -use crate::debug::trace::{ArgKind, EdgeKind, ExecFrame, Outcome, TraceRun}; -use std::collections::HashMap; - -fn micros(f: &ExecFrame) -> Option { - f.end.map(|e| e.duration_since(f.start).as_micros()) -} - -pub fn render_trace(run: &TraceRun) -> String { - let mut by_id: HashMap = HashMap::new(); - for f in &run.frames { - by_id.insert(f.frame_id, f); - } - - let mut out = String::new(); - if let Some(total_us) = total_duration_us(&run.frames) { - out.push_str(&format!("Total: {}µs\n", total_us)); - } - render_frame(run.root, &by_id, "", true, &mut out); - if let Some(total_us) = total_duration_us(&run.frames) { - out.push_str(&format!("Summary: total_time={}µs\n", total_us)); - } - out -} - -fn render_frame( - id: u64, - by_id: &HashMap, - prefix: &str, - is_last: bool, - out: &mut String, -) { - let f = by_id[&id]; - - let branch = if prefix.is_empty() { - "" // root - } else if is_last { - "└─ " - } else { - "├─ " - }; - - let dur = micros(f).map(|u| format!(" ({}µs)", u)).unwrap_or_default(); - - out.push_str(&format!( - "{prefix}{branch}#{fid} node={nid} fn={name}{dur}\n", - prefix = prefix, - branch = branch, - fid = f.frame_id, - nid = f.node_id, - name = f.function_name, - dur = dur - )); - - // args - for a in &f.args { - let pfx = if prefix.is_empty() { - " " - } else if is_last { - " " - } else { - "│ " - }; - - let kind = match &a.kind { - ArgKind::Literal => "lit", - ArgKind::Reference { hit, .. } => { - if *hit { - "ref✓" - } else { - "ref✗" - } - } - ArgKind::Thunk { - eager, executed, .. - } => match (*eager, *executed) { - (true, true) => "thunk eager✓", - (true, false) => "thunk eager", - (false, true) => "thunk lazy✓", - (false, false) => "thunk lazy", - }, - }; - - out.push_str(&format!( - "{prefix}{pfx} arg[{}] {:<12} {}\n", - a.index, - kind, - a.preview, - prefix = prefix, - pfx = pfx - )); - } - - // outcome - let outcome_line = match &f.outcome { - Some(Outcome::Success { value_preview }) => { - colorize("SUCCESS", AnsiColor::Green, value_preview) - } - Some(Outcome::Failure { error_preview }) => { - colorize("FAILURE", AnsiColor::Red, error_preview) - } - Some(Outcome::Return { value_preview }) => { - colorize("RETURN", AnsiColor::Cyan, value_preview) - } - Some(Outcome::Respond { value_preview }) => { - colorize("RESPOND", AnsiColor::Blue, value_preview) - } - Some(Outcome::Stop) => colorize("STOP", AnsiColor::Yellow, "Stop"), - None => "…".to_string(), - }; - - let pfx = if prefix.is_empty() { - " " - } else if is_last { - " " - } else { - "│ " - }; - out.push_str(&format!( - "{prefix}{pfx} => {o}\n", - prefix = prefix, - pfx = pfx, - o = outcome_line - )); - - // children - let kids = &f.children; - if kids.is_empty() { - return; - } - - let mut runtime_idx = 0usize; - for (idx, (edge, child_id)) in kids.iter().enumerate() { - let last = idx + 1 == kids.len(); - - // print edge label line (nice readability) - let edge_label = match edge { - EdgeKind::Next => "→ NEXT".to_string(), - EdgeKind::EagerCall { arg_index } => format!("↳ eager(arg#{})", arg_index), - EdgeKind::RuntimeCall { label } => { - runtime_idx += 1; - match label { - Some(l) => format!("↳ runtime(call #{}) {}", runtime_idx, l), - None => format!("↳ runtime(call #{})", runtime_idx), - } - } - }; - - let edge_branch = if last { "└─ " } else { "├─ " }; - out.push_str(&format!( - "{prefix}{branch}{edge}\n", - prefix = prefix, - branch = edge_branch, - edge = edge_label - )); - - let edge_child_prefix = format!("{}{}", prefix, if last { " " } else { "│ " }); - render_frame(*child_id, by_id, &edge_child_prefix, true, out); - } -} - -enum AnsiColor { - Red, - Green, - Yellow, - Blue, - Cyan, -} - -fn colorize(label: &str, color: AnsiColor, payload: &str) -> String { - let code = match color { - AnsiColor::Red => 31, - AnsiColor::Green => 32, - AnsiColor::Yellow => 33, - AnsiColor::Blue => 34, - AnsiColor::Cyan => 36, - }; - format!("\x1b[{code}m[{label}]\x1b[0m {payload}") -} - -fn total_duration_us(frames: &[ExecFrame]) -> Option { - let mut start: Option = None; - let mut end: Option = None; - - for f in frames { - if let Some(s) = start { - if f.start < s { - start = Some(f.start); - } - } else { - start = Some(f.start); - } - - if let Some(f_end) = f.end { - if let Some(e) = end { - if f_end > e { - end = Some(f_end); - } - } else { - end = Some(f_end); - } - } - } - - match (start, end) { - (Some(s), Some(e)) => Some(e.duration_since(s).as_micros()), - _ => None, - } -} diff --git a/crates/core/src/debug/trace.rs b/crates/core/src/debug/trace.rs deleted file mode 100644 index e7b2bbb..0000000 --- a/crates/core/src/debug/trace.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::time::Instant; - -#[derive(Debug, Clone)] -pub enum EdgeKind { - /// Linear control-flow via next_node_id - Next, - /// Eager evaluation of a thunk argument (child execution) - EagerCall { arg_index: usize }, - /// Child execution triggered inside a runtime handler - RuntimeCall { label: Option }, -} - -#[derive(Debug, Clone)] -pub enum ArgKind { - Literal, - Reference { - reference: ReferenceKind, - hit: bool, - }, - Thunk { - node_id: i64, - eager: bool, - executed: bool, - }, -} - -#[derive(Debug, Clone)] -pub enum ReferenceKind { - Result { - node_id: i64, - }, - InputType { - node_id: i64, - input_index: i64, - parameter_index: i64, - }, - FlowInput, - Empty, -} - -#[derive(Debug, Clone)] -pub struct ArgTrace { - pub index: usize, - pub kind: ArgKind, - pub preview: String, -} - -#[derive(Debug, Clone)] -pub enum Outcome { - Success { value_preview: String }, - Failure { error_preview: String }, - Return { value_preview: String }, - Respond { value_preview: String }, - Stop, -} - -#[derive(Debug, Clone)] -pub struct ExecFrame { - pub frame_id: u64, // unique execution instance id - pub node_id: i64, // database_id - pub function_name: String, // runtime_function_id - pub args: Vec, - pub outcome: Option, - - pub start: Instant, - pub end: Option, - - /// Child edges to other frames (CALLs and NEXT links) - pub children: Vec<(EdgeKind, u64)>, -} - -#[derive(Debug, Clone)] -pub struct TraceRun { - pub frames: Vec, - pub root: u64, -} diff --git a/crates/core/src/debug/tracer.rs b/crates/core/src/debug/tracer.rs deleted file mode 100644 index ff11c6b..0000000 --- a/crates/core/src/debug/tracer.rs +++ /dev/null @@ -1,140 +0,0 @@ -use std::time::Instant; - -use crate::debug::trace::{ArgTrace, EdgeKind, ExecFrame, Outcome, TraceRun}; - -pub trait ExecutionTracer { - fn enter_node(&mut self, node_id: i64, function_name: &str) -> u64; - fn record_arg(&mut self, frame_id: u64, arg: ArgTrace); - fn link_child(&mut self, parent_frame: u64, child_frame: u64, edge: EdgeKind); - fn mark_thunk(&mut self, frame_id: u64, arg_index: usize, eager: bool, executed: bool); - fn mark_thunk_executed_by_node(&mut self, frame_id: u64, node_id: i64); - fn exit_node(&mut self, frame_id: u64, outcome: Outcome); -} - -pub struct Tracer { - next_id: u64, - pub run: Option, - stack: Vec, -} - -impl Default for Tracer { - fn default() -> Self { - Self::new() - } -} - -impl Tracer { - pub fn new() -> Self { - Self { - next_id: 1, - run: None, - stack: vec![], - } - } - - fn frames_mut(&mut self) -> &mut Vec { - &mut self.run.as_mut().unwrap().frames - } - - fn get_frame_mut(&mut self, frame_id: u64) -> &mut ExecFrame { - let idx = self - .frames_mut() - .iter() - .position(|f| f.frame_id == frame_id) - .expect("trace frame must exist"); - &mut self.frames_mut()[idx] - } - - pub fn take_run(self) -> Option { - self.run - } -} - -impl ExecutionTracer for Tracer { - fn enter_node(&mut self, node_id: i64, function_name: &str) -> u64 { - if self.run.is_none() { - self.run = Some(TraceRun { - frames: vec![], - root: 0, - }); - } - - let frame_id = self.next_id; - self.next_id += 1; - - let frame = ExecFrame { - frame_id, - node_id, - function_name: function_name.to_string(), - args: vec![], - outcome: None, - start: Instant::now(), - end: None, - children: vec![], - }; - - let run = self.run.as_mut().unwrap(); - if run.root == 0 { - run.root = frame_id; - } - run.frames.push(frame); - - self.stack.push(frame_id); - frame_id - } - - fn record_arg(&mut self, frame_id: u64, arg: ArgTrace) { - self.get_frame_mut(frame_id).args.push(arg); - } - - fn link_child(&mut self, parent_frame: u64, child_frame: u64, edge: EdgeKind) { - self.get_frame_mut(parent_frame) - .children - .push((edge, child_frame)); - } - - fn mark_thunk(&mut self, frame_id: u64, arg_index: usize, eager: bool, executed: bool) { - let f = self.get_frame_mut(frame_id); - if let Some(arg) = f.args.iter_mut().find(|a| a.index == arg_index) - && let ArgTrace { - kind: - crate::debug::trace::ArgKind::Thunk { - eager: e, - executed: x, - .. - }, - .. - } = arg - { - *e = eager; - *x = executed; - } - } - - fn mark_thunk_executed_by_node(&mut self, frame_id: u64, node_id: i64) { - let f = self.get_frame_mut(frame_id); - if let Some(arg) = f.args.iter_mut().find(|a| { - matches!( - a.kind, - crate::debug::trace::ArgKind::Thunk { node_id: id, executed: false, .. } - if id == node_id - ) - }) && let ArgTrace { - kind: crate::debug::trace::ArgKind::Thunk { executed: x, .. }, - .. - } = arg - { - *x = true; - } - } - - fn exit_node(&mut self, frame_id: u64, outcome: Outcome) { - let f = self.get_frame_mut(frame_id); - f.outcome = Some(outcome); - f.end = Some(Instant::now()); - - // Pop in LIFO order - let popped = self.stack.pop(); - debug_assert_eq!(popped, Some(frame_id)); - } -} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs deleted file mode 100644 index 5770cd0..0000000 --- a/crates/core/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod context; -pub mod debug; -pub mod runtime; -pub mod value; diff --git a/crates/core/src/runtime/error/mod.rs b/crates/core/src/runtime/error/mod.rs deleted file mode 100644 index 14895ba..0000000 --- a/crates/core/src/runtime/error/mod.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::collections::HashMap; -use std::{ - error::Error, - fmt::{Display, Formatter}, -}; -use tucana::shared::value::Kind::{StringValue, StructValue}; -use tucana::shared::{Struct, Value}; - -#[derive(Debug, Default, Clone)] -pub struct RuntimeError { - pub name: String, - pub message: String, - suggestion: Option, -} - -impl Error for RuntimeError {} - -impl Display for RuntimeError { - fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { - write!(f, "&self.function_name.as_str()") - } -} - -impl RuntimeError { - pub fn new(name: String, message: String, suggestion: Option) -> Self { - Self { - name, - message, - suggestion, - } - } - - pub fn simple_str(name: &str, message: &str) -> Self { - Self { - name: name.to_string(), - message: message.to_string(), - suggestion: None, - } - } - - pub fn simple(name: &str, message: String) -> Self { - Self { - name: name.to_string(), - message, - suggestion: None, - } - } - - pub fn as_value(&self) -> Value { - let suggestion = match self.suggestion { - Some(ref s) => Value { - kind: Some(StringValue(s.clone())), - }, - None => Value { kind: None }, - }; - - Value { - kind: Some(StructValue(Struct { - fields: HashMap::from([ - ( - String::from("name"), - Value { - kind: Some(StringValue(self.name.clone())), - }, - ), - ( - String::from("message"), - Value { - kind: Some(StringValue(self.message.clone())), - }, - ), - (String::from("suggestion"), suggestion), - ]), - })), - } - } -} diff --git a/crates/core/src/runtime/functions/array.rs b/crates/core/src/runtime/functions/array.rs deleted file mode 100644 index 57cbcda..0000000 --- a/crates/core/src/runtime/functions/array.rs +++ /dev/null @@ -1,1709 +0,0 @@ -use std::cmp::Ordering; - -use tucana::shared::InputType; -use tucana::shared::{ListValue, Value, value::Kind}; - -use crate::context::argument::Argument; -use crate::context::argument::ParameterNode::{Eager, Lazy}; -use crate::context::context::Context; -use crate::context::macros::args; -use crate::context::registry::{HandlerFn, HandlerFunctionEntry, IntoFunctionEntry}; -use crate::context::signal::Signal; -use crate::runtime::error::RuntimeError; -use crate::value::{number_to_f64, number_to_string, value_from_f64, value_from_i64}; - -pub fn collect_array_functions() -> Vec<(&'static str, HandlerFunctionEntry)> { - vec![ - ("std::list::at", HandlerFn::eager(at, 2)), - ("std::list::concat", HandlerFn::eager(concat, 2)), - ( - "std::list::filter", - HandlerFn::into_function_entry(filter, vec![Eager, Lazy]), - ), - ( - "std::list::find", - HandlerFn::into_function_entry(find, vec![Eager, Lazy]), - ), - ( - "std::list::find_last", - HandlerFn::into_function_entry(find_last, vec![Eager, Lazy]), - ), - ( - "std::list::find_index", - HandlerFn::into_function_entry(find_index, vec![Eager, Lazy]), - ), - ("std::list::first", HandlerFn::eager(first, 1)), - ("std::list::last", HandlerFn::eager(last, 1)), - ( - "std::list::for_each", - HandlerFn::into_function_entry(for_each, vec![Eager, Lazy]), - ), - ( - "std::list::map", - HandlerFn::into_function_entry(map, vec![Eager, Lazy]), - ), - ("std::list::push", HandlerFn::eager(push, 2)), - ("std::list::pop", HandlerFn::eager(pop, 1)), - ("std::list::remove", HandlerFn::eager(remove, 2)), - ("std::list::is_empty", HandlerFn::eager(is_empty, 1)), - ("std::list::size", HandlerFn::eager(size, 1)), - ("std::list::index_of", HandlerFn::eager(index_of, 2)), - ("std::list::to_unique", HandlerFn::eager(to_unique, 1)), - ( - "std::list::sort", - HandlerFn::into_function_entry(sort, vec![Eager, Lazy]), - ), - ( - "std::list::sort_reverse", - HandlerFn::into_function_entry(sort_reverse, vec![Eager, Lazy]), - ), - ("std::list::reverse", HandlerFn::eager(reverse, 1)), - ("std::list::flat", HandlerFn::eager(flat, 1)), - ("std::list::min", HandlerFn::eager(min, 1)), - ("std::list::max", HandlerFn::eager(max, 1)), - ("std::list::sum", HandlerFn::eager(sum, 1)), - ("std::list::join", HandlerFn::eager(join, 2)), - ] -} - -fn as_list(value: &Value, err: &'static str) -> Result { - match value.kind.clone().unwrap_or(Kind::NullValue(0)) { - Kind::ListValue(lv) => Ok(lv), - _ => Err(RuntimeError::simple_str("InvalidArgumentRuntimeError", err)), - } -} - -fn as_bool(value: &Value) -> Result { - match value.kind.clone().unwrap_or(Kind::NullValue(0)) { - Kind::BoolValue(b) => Ok(b), - _ => Err(RuntimeError::simple_str( - "InvalidArgumentRuntimeError", - "Expected boolean result from predicate", - )), - } -} - -fn at( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - // array, index - args!(args => array: ListValue, index: f64); - - if index < 0.0 { - return Signal::Failure(RuntimeError::simple_str( - "IndexOutOfBoundsRuntimeError", - "Negative index", - )); - } - let i = index as usize; - match array.values.get(i) { - Some(item) => Signal::Success(item.clone()), - None => Signal::Failure(RuntimeError::simple( - "IndexOutOfBoundsRuntimeError", - format!("Index {} out of bounds (len={})", i, array.values.len()), - )), - } -} - -fn concat( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => lhs_v: Value, rhs_v: Value); - - let Kind::ListValue(lhs) = lhs_v.kind.clone().ok_or(()).unwrap_or(Kind::NullValue(0)) else { - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!( - "Expected two arrays as arguments but received lhs={:?}", - lhs_v - ), - )); - }; - let Kind::ListValue(rhs) = rhs_v.kind.clone().ok_or(()).unwrap_or(Kind::NullValue(0)) else { - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!( - "Expected two arrays as arguments but received rhs={:?}", - rhs_v - ), - )); - }; - - let mut result = lhs.values.clone(); - result.extend(rhs.values.iter().cloned()); - - Signal::Success(Value { - kind: Some(Kind::ListValue(ListValue { values: result })), - }) -} - -fn filter( - args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - let [Argument::Eval(array_v), Argument::Thunk(predicate_node)] = args else { - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!( - "filter expects (array: eager, predicate: lazy thunk), got {:?}", - args - ), - )); - }; - - let array = match as_list(array_v, "Expected first argument to be an array") { - Ok(a) => a, - Err(e) => return Signal::Failure(e), - }; - - let mut out: Vec = Vec::new(); - let node_id = ctx.get_current_node_id(); - let input_type = InputType { - node_id, - parameter_index: 1, - input_index: 0, - }; - for (idx, item) in array.values.iter().enumerate() { - ctx.insert_input_type(input_type, item.clone()); - ctx.push_runtime_trace_label(format!("iter={} value={}", idx, preview_value(item))); - let pred_sig = run(*predicate_node, ctx); - - match pred_sig { - Signal::Success(v) => match as_bool(&v) { - Ok(true) => out.push(item.clone()), - Ok(false) => {} - Err(e) => return Signal::Failure(e), - }, - other - @ (Signal::Failure(_) | Signal::Return(_) | Signal::Respond(_) | Signal::Stop) => { - return other; - } - } - } - - ctx.clear_input_type(input_type); - Signal::Success(Value { - kind: Some(Kind::ListValue(ListValue { values: out })), - }) -} - -fn find( - args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - let [Argument::Eval(array_v), Argument::Thunk(predicate_node)] = args else { - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!( - "find expects (array: eager, predicate: lazy thunk), got {:?}", - args - ), - )); - }; - - let array = match as_list(array_v, "Expected first argument to be an array") { - Ok(a) => a, - Err(e) => return Signal::Failure(e), - }; - let node_id = ctx.get_current_node_id(); - let input_type = InputType { - node_id, - parameter_index: 1, - input_index: 0, - }; - - for (idx, item) in array.values.iter().enumerate() { - ctx.insert_input_type(input_type, item.clone()); - ctx.push_runtime_trace_label(format!("iter={} value={}", idx, preview_value(item))); - let pred_sig = run(*predicate_node, ctx); - match pred_sig { - Signal::Success(v) => match as_bool(&v) { - Ok(true) => { - ctx.clear_input_type(input_type); - return Signal::Success(item.clone()); - } - Ok(false) => continue, - Err(e) => { - ctx.clear_input_type(input_type); - return Signal::Failure(e); - } - }, - other - @ (Signal::Failure(_) | Signal::Return(_) | Signal::Respond(_) | Signal::Stop) => { - ctx.clear_input_type(input_type); - return other; - } - } - } - - ctx.clear_input_type(input_type); - Signal::Failure(RuntimeError::simple_str( - "NotFoundError", - "No item found that satisfies the predicate", - )) -} -fn find_last( - args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - let [Argument::Eval(array_v), Argument::Thunk(predicate_node)] = args else { - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!( - "find_last expects (array: eager, predicate: lazy thunk), got {:?}", - args - ), - )); - }; - - let mut array = match as_list(array_v, "Expected first argument to be an array") { - Ok(a) => a, - Err(e) => return Signal::Failure(e), - }; - array.values.reverse(); - let node_id = ctx.get_current_node_id(); - let input_type = InputType { - node_id, - parameter_index: 1, - input_index: 0, - }; - - for (idx, item) in array.values.into_iter().enumerate() { - ctx.insert_input_type(input_type, item.clone()); - ctx.push_runtime_trace_label(format!("iter={} value={}", idx, preview_value(&item))); - let pred_sig = run(*predicate_node, ctx); - match pred_sig { - Signal::Success(v) => match as_bool(&v) { - Ok(true) => { - ctx.clear_input_type(input_type); - return Signal::Success(item); - } - Ok(false) => continue, - Err(e) => { - ctx.clear_input_type(input_type); - return Signal::Failure(e); - } - }, - other - @ (Signal::Failure(_) | Signal::Return(_) | Signal::Respond(_) | Signal::Stop) => { - ctx.clear_input_type(input_type); - return other; - } - } - } - ctx.clear_input_type(input_type); - Signal::Failure(RuntimeError::simple_str( - "NotFoundError", - "No item found that satisfies the predicate", - )) -} - -fn find_index( - args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - let [Argument::Eval(array_v), Argument::Thunk(predicate_node)] = args else { - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!( - "find_index expects (array: eager, predicate: lazy thunk), got {:?}", - args - ), - )); - }; - - let array = match as_list(array_v, "Expected first argument to be an array") { - Ok(a) => a, - Err(e) => return Signal::Failure(e), - }; - let node_id = ctx.get_current_node_id(); - let input_type = InputType { - node_id, - parameter_index: 1, - input_index: 0, - }; - - for (idx, item) in array.values.iter().enumerate() { - ctx.insert_input_type(input_type, item.clone()); - ctx.push_runtime_trace_label(format!("iter={} value={}", idx, preview_value(item))); - let pred_sig = run(*predicate_node, ctx); - - match pred_sig { - Signal::Success(v) => match as_bool(&v) { - Ok(true) => { - ctx.clear_input_type(input_type); - return Signal::Success(value_from_i64(idx as i64)); - } - Ok(false) => continue, - Err(e) => { - ctx.clear_input_type(input_type); - return Signal::Failure(e); - } - }, - other - @ (Signal::Failure(_) | Signal::Return(_) | Signal::Respond(_) | Signal::Stop) => { - ctx.clear_input_type(input_type); - return other; - } - } - } - - ctx.clear_input_type(input_type); - Signal::Failure(RuntimeError::simple_str( - "NotFoundError", - "No item found that satisfies the predicate", - )) -} -fn first( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => array: ListValue); - - match array.values.first() { - Some(v) => Signal::Success(v.clone()), - None => Signal::Failure(RuntimeError::simple_str( - "ArrayEmptyRuntimeError", - "This array is empty", - )), - } -} - -fn last( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => array: ListValue); - match array.values.last() { - Some(v) => Signal::Success(v.clone()), - None => Signal::Failure(RuntimeError::simple_str( - "ArrayEmptyRuntimeError", - "This array is empty", - )), - } -} - -fn for_each( - args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - let [Argument::Eval(array_v), Argument::Thunk(transform_node)] = args else { - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!( - "map expects (array: eager, transform: lazy thunk), got {:?}", - args - ), - )); - }; - - let array = match as_list(array_v, "Expected first argument to be an array") { - Ok(a) => a, - Err(e) => return Signal::Failure(e), - }; - let node_id = ctx.get_current_node_id(); - let input_type = InputType { - node_id, - parameter_index: 1, - input_index: 0, - }; - - for (idx, item) in array.values.iter().enumerate() { - ctx.insert_input_type(input_type, item.clone()); - ctx.push_runtime_trace_label(format!("iter={} value={}", idx, preview_value(item))); - let sig = run(*transform_node, ctx); - - match sig { - Signal::Success(_) => {} - other - @ (Signal::Failure(_) | Signal::Return(_) | Signal::Respond(_) | Signal::Stop) => { - return other; - } - } - } - - ctx.clear_input_type(input_type); - Signal::Success(Value { - kind: Some(Kind::NullValue(0)), - }) -} - -fn preview_value(value: &Value) -> String { - format_value_json(value) -} - -fn format_value_json(value: &Value) -> String { - match value.kind.as_ref() { - Some(Kind::NumberValue(v)) => number_to_string(v), - Some(Kind::BoolValue(v)) => v.to_string(), - Some(Kind::StringValue(v)) => format!("{:?}", v), - Some(Kind::NullValue(_)) | None => "null".to_string(), - Some(Kind::ListValue(list)) => { - let mut parts = Vec::new(); - for item in list.values.iter() { - parts.push(format_value_json(item)); - } - format!("[{}]", parts.join(", ")) - } - Some(Kind::StructValue(struct_value)) => { - let mut keys: Vec<_> = struct_value.fields.keys().collect(); - keys.sort(); - let mut parts = Vec::new(); - for key in keys.iter() { - if let Some(v) = struct_value.fields.get(*key) { - parts.push(format!("{:?}: {}", key, format_value_json(v))); - } - } - format!("{{{}}}", parts.join(", ")) - } - } -} - -fn map( - args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - let [Argument::Eval(array_v), Argument::Thunk(transform_node)] = args else { - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!( - "map expects (array: eager, transform: lazy thunk), got {:?}", - args - ), - )); - }; - - let array = match as_list(array_v, "Expected first argument to be an array") { - Ok(a) => a, - Err(e) => return Signal::Failure(e), - }; - - let mut out: Vec = Vec::with_capacity(array.values.len()); - let node_id = ctx.get_current_node_id(); - let input_type = InputType { - node_id, - parameter_index: 1, - input_index: 0, - }; - - for (idx, item) in array.values.iter().enumerate() { - ctx.insert_input_type(input_type, item.clone()); - ctx.push_runtime_trace_label(format!("iter={} value={}", idx, preview_value(item))); - let sig = run(*transform_node, ctx); - match sig { - Signal::Success(v) => out.push(v), - other - @ (Signal::Failure(_) | Signal::Return(_) | Signal::Respond(_) | Signal::Stop) => { - ctx.clear_input_type(input_type); - return other; - } - } - } - - ctx.clear_input_type(input_type); - Signal::Success(Value { - kind: Some(Kind::ListValue(ListValue { values: out })), - }) -} - -fn push( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => array_v: Value, item: Value); - let Kind::ListValue(mut array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { - return Signal::Failure(RuntimeError::simple_str( - "InvalidArgumentRuntimeError", - "Expected first argument to be an array", - )); - }; - array.values.push(item); - Signal::Success(Value { - kind: Some(Kind::ListValue(array)), - }) -} - -fn pop( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => array_v: Value); - let Kind::ListValue(mut array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { - return Signal::Failure(RuntimeError::simple_str( - "InvalidArgumentRuntimeError", - "Expected an array as an argument", - )); - }; - array.values.pop(); - Signal::Success(Value { - kind: Some(Kind::ListValue(array)), - }) -} - -fn remove( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => array_v: Value, item: Value); - let Kind::ListValue(mut array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { - return Signal::Failure(RuntimeError::simple_str( - "InvalidArgumentRuntimeError", - "Expected first argument to be an array", - )); - }; - - if let Some(index) = array.values.iter().position(|x| *x == item) { - array.values.remove(index); - Signal::Success(Value { - kind: Some(Kind::ListValue(array)), - }) - } else { - Signal::Failure(RuntimeError::simple( - "ValueNotFoundRuntimeError", - format!("Item {:?} not found in array", item), - )) - } -} - -fn is_empty( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => array_v: Value); - let Kind::ListValue(array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { - return Signal::Failure(RuntimeError::simple_str( - "InvalidArgumentRuntimeError", - "Expected an array as an argument", - )); - }; - Signal::Success(Value { - kind: Some(Kind::BoolValue(array.values.is_empty())), - }) -} - -fn size( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => array_v: Value); - let Kind::ListValue(array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { - return Signal::Failure(RuntimeError::simple_str( - "InvalidArgumentRuntimeError", - "Expected an array as an argument", - )); - }; - Signal::Success(value_from_i64(array.values.len() as i64)) -} - -fn index_of( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => array_v: Value, item: Value); - let Kind::ListValue(array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { - return Signal::Failure(RuntimeError::simple_str( - "InvalidArgumentRuntimeError", - "Expected first argument to be an array", - )); - }; - - match array.values.iter().position(|x| *x == item) { - Some(i) => Signal::Success(value_from_i64(i as i64)), - None => Signal::Failure(RuntimeError::simple( - "ValueNotFoundRuntimeError", - format!("Item {:?} not found in array", item), - )), - } -} - -fn to_unique( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => array_v: Value); - let Kind::ListValue(array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { - return Signal::Failure(RuntimeError::simple_str( - "InvalidArgumentRuntimeError", - "Expected an array as an argument", - )); - }; - - let mut unique = Vec::::new(); - for v in &array.values { - if !unique.contains(v) { - unique.push(v.clone()); - } - } - - Signal::Success(Value { - kind: Some(Kind::ListValue(ListValue { values: unique })), - }) -} - -fn sort( - args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - let [Argument::Eval(array_v), Argument::Thunk(transform_node)] = args else { - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!( - "map expects (array: eager, transform: lazy thunk), got {:?}", - args - ), - )); - }; - - let mut array = match as_list(array_v, "Expected first argument to be an array") { - Ok(a) => a, - Err(e) => return Signal::Failure(e), - }; - - let mut out: Vec = Vec::new(); - let node_id = ctx.get_current_node_id(); - let input_type = InputType { - node_id, - parameter_index: 1, - input_index: 0, - }; - - let input_type_next = InputType { - node_id, - parameter_index: 1, - input_index: 1, - }; - - let mut signals = Vec::new(); - let mut cmp_idx = 0usize; - array.values.sort_by(|a, b| { - ctx.insert_input_type(input_type, a.clone()); - ctx.insert_input_type(input_type_next, b.clone()); - ctx.push_runtime_trace_label(format!( - "cmp#{} a={} b={}", - cmp_idx, - preview_value(a), - preview_value(b) - )); - cmp_idx += 1; - let sig = run(*transform_node, ctx); - signals.push(sig); - Ordering::Equal - }); - - for sig in signals { - match sig { - Signal::Success(v) => { - if let Value { - kind: Some(Kind::NumberValue(i)), - } = v - { - match number_to_f64(&i) { - Some(i) => out.push(i), - None => { - ctx.clear_input_type(input_type); - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!( - "expected return value of comparator to be a number but was {:?}", - v - ), - )); - } - } - } else { - ctx.clear_input_type(input_type); - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!( - "expected return value of comparator to be a number but was {:?}", - v - ), - )); - } - } - other - @ (Signal::Failure(_) | Signal::Return(_) | Signal::Respond(_) | Signal::Stop) => { - ctx.clear_input_type(input_type); - return other; - } - } - } - - let mut i = 0usize; - array.values.sort_by(|_, _| { - let comp = *out.get(i).unwrap_or(&0.0); - i += 1; - match comp { - n if n < 0.0 => Ordering::Less, - 0.0 => Ordering::Equal, - _ => Ordering::Greater, - } - }); - - Signal::Success(Value { - kind: Some(Kind::ListValue(array)), - }) -} - -fn sort_reverse( - args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - let [Argument::Eval(array_v), Argument::Thunk(transform_node)] = args else { - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!( - "map expects (array: eager, transform: lazy thunk), got {:?}", - args - ), - )); - }; - - let mut array = match as_list(array_v, "Expected first argument to be an array") { - Ok(a) => a, - Err(e) => return Signal::Failure(e), - }; - - let mut out: Vec = Vec::new(); - let node_id = ctx.get_current_node_id(); - let input_type = InputType { - node_id, - parameter_index: 1, - input_index: 0, - }; - - let input_type_next = InputType { - node_id, - parameter_index: 1, - input_index: 1, - }; - - let mut signals = Vec::new(); - let mut cmp_idx = 0usize; - array.values.sort_by(|a, b| { - ctx.insert_input_type(input_type, a.clone()); - ctx.insert_input_type(input_type_next, b.clone()); - ctx.push_runtime_trace_label(format!( - "cmp#{} a={} b={}", - cmp_idx, - preview_value(a), - preview_value(b) - )); - cmp_idx += 1; - let sig = run(*transform_node, ctx); - signals.push(sig); - Ordering::Equal - }); - - for sig in signals { - match sig { - Signal::Success(v) => { - if let Value { - kind: Some(Kind::NumberValue(i)), - } = v - { - match number_to_f64(&i) { - Some(i) => out.push(i), - None => { - ctx.clear_input_type(input_type); - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!( - "expected return value of comparator to be a number but was {:?}", - v - ), - )); - } - } - } else { - ctx.clear_input_type(input_type); - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!( - "expected return value of comparator to be a number but was {:?}", - v - ), - )); - } - } - other - @ (Signal::Failure(_) | Signal::Return(_) | Signal::Respond(_) | Signal::Stop) => { - ctx.clear_input_type(input_type); - return other; - } - } - } - - array.values.reverse(); // keep behavior consistent with original - - let mut i = 0usize; - array.values.sort_by(|_, _| { - let comp = *out.get(i).unwrap_or(&0.0); - i += 1; - match comp { - n if n < 0.0 => Ordering::Less, - 0.0 => Ordering::Equal, - _ => Ordering::Greater, - } - }); - - Signal::Success(Value { - kind: Some(Kind::ListValue(array)), - }) -} - -fn reverse( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => array_v: Value); - let Kind::ListValue(mut array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { - return Signal::Failure(RuntimeError::simple_str( - "InvalidArgumentRuntimeError", - "Expected an array as an argument", - )); - }; - array.values.reverse(); - Signal::Success(Value { - kind: Some(Kind::ListValue(array)), - }) -} - -fn flat( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => array_v: Value); - let Kind::ListValue(array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { - return Signal::Failure(RuntimeError::simple_str( - "InvalidArgumentRuntimeError", - "Expected an array as an argument", - )); - }; - - let mut out: Vec = Vec::new(); - for item in &array.values { - match &item.kind { - Some(Kind::ListValue(sub)) => out.extend(sub.values.iter().cloned()), - _ => out.push(item.clone()), - } - } - - Signal::Success(Value { - kind: Some(Kind::ListValue(ListValue { values: out })), - }) -} - -fn min( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => array: ListValue); - - let mut nums: Vec = Vec::new(); - let mut all_int = true; - let mut min_i64: Option = None; - for v in &array.values { - if let Some(Kind::NumberValue(n)) = &v.kind { - match n.number { - Some(tucana::shared::number_value::Number::Integer(i)) => { - min_i64 = Some(match min_i64 { - Some(curr) => curr.min(i), - None => i, - }); - nums.push(i as f64); - } - Some(tucana::shared::number_value::Number::Float(f)) => { - all_int = false; - nums.push(f); - } - None => {} - } - } - } - - match nums.iter().min_by(|a, b| a.total_cmp(b)) { - Some(m) if all_int => Signal::Success(value_from_i64(min_i64.unwrap_or(*m as i64))), - Some(m) => Signal::Success(value_from_f64(*m)), - None => Signal::Failure(RuntimeError::simple_str( - "ArrayEmptyRuntimeError", - "Array is empty", - )), - } -} - -fn max( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => array: ListValue); - - let mut nums: Vec = Vec::new(); - let mut all_int = true; - let mut max_i64: Option = None; - for v in &array.values { - if let Some(Kind::NumberValue(n)) = &v.kind { - match n.number { - Some(tucana::shared::number_value::Number::Integer(i)) => { - max_i64 = Some(match max_i64 { - Some(curr) => curr.max(i), - None => i, - }); - nums.push(i as f64); - } - Some(tucana::shared::number_value::Number::Float(f)) => { - all_int = false; - nums.push(f); - } - None => {} - } - } - } - - match nums.iter().max_by(|a, b| a.total_cmp(b)) { - Some(m) if all_int => Signal::Success(value_from_i64(max_i64.unwrap_or(*m as i64))), - Some(m) => Signal::Success(value_from_f64(*m)), - None => Signal::Failure(RuntimeError::simple_str( - "ArrayEmptyRuntimeError", - "Array is empty", - )), - } -} - -fn sum( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => array: ListValue); - - let mut s_f = 0.0; - let mut s_i: i64 = 0; - let mut all_int = true; - for v in &array.values { - if let Some(Kind::NumberValue(n)) = &v.kind { - match n.number { - Some(tucana::shared::number_value::Number::Integer(i)) => { - if let Some(next) = s_i.checked_add(i) { - s_i = next; - s_f += i as f64; - } else { - all_int = false; - if let Some(f) = number_to_f64(n) { - s_f += f; - } - } - } - Some(tucana::shared::number_value::Number::Float(f)) => { - all_int = false; - s_f += f; - } - None => {} - } - } - } - - if all_int { - Signal::Success(value_from_i64(s_i)) - } else { - Signal::Success(value_from_f64(s_f)) - } -} - -fn join( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => array: ListValue, separator: String); - - let mut parts: Vec = Vec::new(); - for v in &array.values { - if let Some(Kind::StringValue(s)) = &v.kind { - parts.push(s.clone()); - } - } - - Signal::Success(Value { - kind: Some(Kind::StringValue(parts.join(&separator))), - }) -} -#[cfg(test)] -mod tests { - use super::*; - use crate::context::context::Context; - use crate::value::{number_to_f64, number_value_from_f64, value_from_f64}; - use tucana::shared::{ListValue, Value, value::Kind}; - - // --- helpers ------------------------------------------------------------- - fn a_val(v: Value) -> Argument { - Argument::Eval(v) - } - fn a_thunk(id: i64) -> Argument { - Argument::Thunk(id) - } - fn v_num(n: f64) -> Value { - value_from_f64(n) - } - fn v_str(s: &str) -> Value { - Value { - kind: Some(Kind::StringValue(s.to_string())), - } - } - fn v_bool(b: bool) -> Value { - Value { - kind: Some(Kind::BoolValue(b)), - } - } - fn v_list(values: Vec) -> Value { - Value { - kind: Some(Kind::ListValue(ListValue { values })), - } - } - fn k_num(n: f64) -> Option { - Some(Kind::NumberValue(number_value_from_f64(n))) - } - - fn expect_num(sig: Signal) -> f64 { - match sig { - Signal::Success(Value { - kind: Some(Kind::NumberValue(n)), - }) => number_to_f64(&n).unwrap_or_default(), - x => panic!("Expected NumberValue, got {:?}", x), - } - } - fn expect_str(sig: Signal) -> String { - match sig { - Signal::Success(Value { - kind: Some(Kind::StringValue(s)), - }) => s, - x => panic!("Expected StringValue, got {:?}", x), - } - } - fn expect_list(sig: Signal) -> Vec { - match sig { - Signal::Success(Value { - kind: Some(Kind::ListValue(ListValue { values })), - }) => values, - x => panic!("Expected ListValue, got {:?}", x), - } - } - fn expect_bool(sig: Signal) -> bool { - match sig { - Signal::Success(Value { - kind: Some(Kind::BoolValue(b)), - }) => b, - x => panic!("Expected BoolValue, got {:?}", x), - } - } - - fn dummy_run(_: i64, _: &mut Context) -> Signal { - Signal::Success(Value { - kind: Some(Kind::NullValue(0)), - }) - } - - fn run_from_bools(seq: Vec) -> impl FnMut(i64, &mut Context) -> Signal { - let mut i = 0usize; - move |_, _| { - let b = *seq.get(i).unwrap_or(&false); - i += 1; - Signal::Success(Value { - kind: Some(Kind::BoolValue(b)), - }) - } - } - - fn run_from_values(seq: Vec) -> impl FnMut(i64, &mut Context) -> Signal { - let mut i = 0usize; - move |_, _| { - let v = seq.get(i).cloned().unwrap_or(Value { - kind: Some(Kind::NullValue(0)), - }); - i += 1; - Signal::Success(v) - } - } - - // --- at ------------------------------------------------------------------ - #[test] - fn test_at_success() { - let mut ctx = Context::default(); - let mut run = dummy_run; - let arr = v_list(vec![v_num(10.0), v_num(20.0), v_num(30.0)]); - - assert_eq!( - expect_num(at( - &[a_val(arr.clone()), a_val(v_num(0.0))], - &mut ctx, - &mut run - )), - 10.0 - ); - assert_eq!( - expect_num(at( - &[a_val(arr.clone()), a_val(v_num(1.0))], - &mut ctx, - &mut run - )), - 20.0 - ); - assert_eq!( - expect_num(at(&[a_val(arr), a_val(v_num(2.0))], &mut ctx, &mut run)), - 30.0 - ); - } - - #[test] - fn test_at_error() { - let mut ctx = Context::default(); - let mut run = dummy_run; - let arr = v_list(vec![v_num(1.0)]); - - // wrong arg count - match at(&[a_val(arr.clone())], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - // wrong type first arg - match at( - &[a_val(v_str("not_array")), a_val(v_num(0.0))], - &mut ctx, - &mut run, - ) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - // wrong type second arg - match at(&[a_val(arr.clone()), a_val(v_str("x"))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - // oob / negative - match at(&[a_val(arr.clone()), a_val(v_num(9.0))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match at(&[a_val(arr), a_val(v_num(-1.0))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - } - - // --- concat -------------------------------------------------------------- - #[test] - fn test_concat_success() { - let mut ctx = Context::default(); - let mut run = dummy_run; - let a = v_list(vec![v_num(1.0), v_num(2.0)]); - let b = v_list(vec![v_num(3.0), v_num(4.0)]); - let out = expect_list(concat(&[a_val(a), a_val(b)], &mut ctx, &mut run)); - assert_eq!(out.len(), 4); - assert_eq!(out[0].kind, k_num(1.0)); - assert_eq!(out[3].kind, k_num(4.0)); - } - - #[test] - fn test_concat_error() { - let mut ctx = Context::default(); - let mut run = dummy_run; - let arr = v_list(vec![v_num(1.0)]); - match concat(&[a_val(arr.clone())], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match concat( - &[a_val(v_str("not_array")), a_val(arr.clone())], - &mut ctx, - &mut run, - ) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match concat(&[a_val(arr), a_val(v_num(42.0))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - } - - // --- filter / find / find_last / find_index ------------------------------ - #[test] - fn test_filter_success() { - let mut ctx = Context::default(); - let array = v_list(vec![v_num(1.0), v_num(2.0), v_num(3.0)]); - let mut run = run_from_bools(vec![true, false, true]); - let out = expect_list(filter(&[a_val(array), a_thunk(1)], &mut ctx, &mut run)); - assert_eq!(out.len(), 2); - assert_eq!(out[0].kind, k_num(1.0)); - assert_eq!(out[1].kind, k_num(3.0)); - } - - #[test] - fn test_filter_error() { - let mut ctx = Context::default(); - let mut run = dummy_run; - let array = v_list(vec![v_num(1.0)]); - let _predicate = v_list(vec![v_bool(true)]); - match filter(&[a_val(array.clone())], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match filter(&[a_val(v_str("not_array")), a_thunk(1)], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match filter(&[a_val(array), a_val(v_num(1.0))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - } - - // --- first / last -------------------------------------------------------- - #[test] - fn test_first_success() { - let mut ctx = Context::default(); - let mut run = dummy_run; - let arr = v_list(vec![v_str("first"), v_str("second"), v_str("third")]); - assert_eq!( - expect_str(first(&[a_val(arr)], &mut ctx, &mut run)), - "first" - ); - } - - #[test] - fn test_first_error() { - let mut ctx = Context::default(); - let mut run = dummy_run; - match first(&[a_val(v_list(vec![]))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match first(&[a_val(v_str("not_array"))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match first(&[], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - } - - #[test] - fn test_last_success() { - let mut ctx = Context::default(); - let mut run = dummy_run; - let arr = v_list(vec![v_str("first"), v_str("second"), v_str("last")]); - assert_eq!(expect_str(last(&[a_val(arr)], &mut ctx, &mut run)), "last"); - } - - #[test] - fn test_last_error() { - let mut ctx = Context::default(); - let mut run = dummy_run; - match last(&[a_val(v_list(vec![]))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match last(&[a_val(v_str("not_array"))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - } - - // --- for_each / map ------------------------------------------------------ - #[test] - fn test_for_each_and_map() { - let mut ctx = Context::default(); - let mut called = 0usize; - let mut run = |_, _ctx: &mut Context| { - called += 1; - Signal::Success(Value { - kind: Some(Kind::NullValue(0)), - }) - }; - match for_each( - &[a_val(v_list(vec![v_num(1.0), v_num(2.0)])), a_thunk(1)], - &mut ctx, - &mut run, - ) { - Signal::Success(Value { - kind: Some(Kind::NullValue(_)), - }) => {} - x => panic!("expected NullValue, got {:?}", x), - } - assert_eq!(called, 2); - let transformed = v_list(vec![v_str("X"), v_str("Y")]); - let mut run = run_from_values(match transformed.kind.clone() { - Some(Kind::ListValue(ListValue { values })) => values, - _ => unreachable!(), - }); - let out = expect_list(map( - &[a_val(v_list(vec![v_num(1.0), v_num(2.0)])), a_thunk(2)], - &mut ctx, - &mut run, - )); - let expected = match transformed.kind { - Some(Kind::ListValue(ListValue { values })) => values, - _ => unreachable!(), - }; - assert_eq!(out, expected); - } - - // --- push / pop / remove ------------------------------------------------- - #[test] - fn test_push_success() { - let mut ctx = Context::default(); - let mut run = dummy_run; - let out = expect_list(push( - &[ - a_val(v_list(vec![v_num(1.0), v_num(2.0)])), - a_val(v_num(3.0)), - ], - &mut ctx, - &mut run, - )); - assert_eq!(out.len(), 3); - assert_eq!(out[2].kind, k_num(3.0)); - } - - #[test] - fn test_push_error() { - let mut ctx = Context::default(); - let mut run = dummy_run; - match push(&[a_val(v_list(vec![v_num(1.0)]))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match push( - &[a_val(v_str("nope")), a_val(v_num(1.0))], - &mut ctx, - &mut run, - ) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - } - - #[test] - fn test_pop_success() { - let mut ctx = Context::default(); - let mut run = dummy_run; - let out = expect_list(pop( - &[a_val(v_list(vec![v_num(1.0), v_num(2.0), v_num(3.0)]))], - &mut ctx, - &mut run, - )); - assert_eq!(out.len(), 2); - assert_eq!(out[0].kind, k_num(1.0)); - assert_eq!(out[1].kind, k_num(2.0)); - } - - #[test] - fn test_pop_error() { - let mut ctx = Context::default(); - let mut run = dummy_run; - match pop(&[a_val(v_str("nope"))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match pop(&[], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - } - - #[test] - fn test_remove_success_and_error() { - let mut ctx = Context::default(); - let mut run = dummy_run; - // success - let arr = v_list(vec![v_str("first"), v_str("second"), v_str("third")]); - let out = expect_list(remove( - &[a_val(arr), a_val(v_str("second"))], - &mut ctx, - &mut run, - )); - assert_eq!(out.len(), 2); - assert_eq!(out[0].kind, Some(Kind::StringValue("first".into()))); - assert_eq!(out[1].kind, Some(Kind::StringValue("third".into()))); - // errors - match remove(&[a_val(v_list(vec![v_num(1.0)]))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match remove( - &[a_val(v_str("nope")), a_val(v_num(0.0))], - &mut ctx, - &mut run, - ) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match remove( - &[a_val(v_list(vec![v_num(1.0)])), a_val(v_num(999.0))], - &mut ctx, - &mut run, - ) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - } - - // --- is_empty / size ----------------------------------------------------- - #[test] - fn test_is_empty_and_size() { - let mut ctx = Context::default(); - let mut run = dummy_run; - assert!(expect_bool(is_empty( - &[a_val(v_list(vec![]))], - &mut ctx, - &mut run - ))); - assert!(!expect_bool(is_empty( - &[a_val(v_list(vec![v_num(1.0)]))], - &mut ctx, - &mut run - ))); - assert_eq!( - expect_num(size(&[a_val(v_list(vec![]))], &mut ctx, &mut run)), - 0.0 - ); - assert_eq!( - expect_num(size( - &[a_val(v_list(vec![v_num(1.0), v_num(2.0), v_num(3.0)]))], - &mut ctx, - &mut run - )), - 3.0 - ); - } - - #[test] - fn test_is_empty_error_and_size_error() { - let mut ctx = Context::default(); - let mut run = dummy_run; - match is_empty(&[a_val(v_str("nope"))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match is_empty(&[], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match size(&[a_val(v_str("nope"))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match size(&[], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - } - - // --- index_of / to_unique ------------------------------------------------ - #[test] - fn test_index_of_and_to_unique() { - let mut ctx = Context::default(); - let mut run = dummy_run; - let arr = v_list(vec![v_num(10.0), v_num(42.0), v_num(30.0), v_num(42.0)]); - assert_eq!( - expect_num(index_of( - &[a_val(arr.clone()), a_val(v_num(42.0))], - &mut ctx, - &mut run - )), - 1.0 - ); - match index_of( - &[a_val(arr.clone()), a_val(v_num(999.0))], - &mut ctx, - &mut run, - ) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - - let uniq = expect_list(to_unique(&[a_val(arr)], &mut ctx, &mut run)); - assert_eq!(uniq.len(), 3); - assert_eq!(uniq[0].kind, k_num(10.0)); - assert_eq!(uniq[1].kind, k_num(42.0)); - assert_eq!(uniq[2].kind, k_num(30.0)); - } - - #[test] - fn test_index_of_error() { - let mut ctx = Context::default(); - let mut run = dummy_run; - match index_of(&[a_val(v_list(vec![v_num(1.0)]))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match index_of( - &[a_val(v_str("nope")), a_val(v_num(1.0))], - &mut ctx, - &mut run, - ) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - } - - // --- sort / sort_reverse ------------------------------------------------- - #[test] - fn test_sort_and_sort_reverse() { - let mut ctx = Context::default(); - - // We don't rely on actual values; ordering is driven by the comparator sequence. - let arr = v_list(vec![v_str("a"), v_str("b"), v_str("c"), v_str("d")]); - let comps = vec![v_num(-1.0), v_num(1.0), v_num(0.0), v_num(-1.0)]; - let mut run = run_from_values(comps.clone()); - let out = expect_list(sort(&[a_val(arr.clone()), a_thunk(1)], &mut ctx, &mut run)); - assert_eq!(out.len(), 4); - - let mut run = run_from_values(comps); - let out_r = expect_list(sort_reverse(&[a_val(arr), a_thunk(1)], &mut ctx, &mut run)); - assert_eq!(out_r.len(), 4); - } - - // --- reverse / flat ------------------------------------------------------ - #[test] - fn test_reverse_success_and_error() { - let mut ctx = Context::default(); - let mut run = dummy_run; - let out = expect_list(reverse( - &[a_val(v_list(vec![v_num(1.0), v_num(2.0), v_num(3.0)]))], - &mut ctx, - &mut run, - )); - assert_eq!(out[0].kind, k_num(3.0)); - assert_eq!(out[2].kind, k_num(1.0)); - - match reverse(&[a_val(v_str("nope"))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match reverse(&[], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - } - - #[test] - fn test_flat_success() { - let mut ctx = Context::default(); - let mut run = dummy_run; - let nested = v_list(vec![ - v_num(1.0), - v_list(vec![v_num(2.0), v_num(3.0)]), - v_list(vec![]), - v_num(4.0), - ]); - let out = expect_list(flat(&[a_val(nested)], &mut ctx, &mut run)); - assert_eq!(out.len(), 4); - assert_eq!(out[0].kind, k_num(1.0)); - assert_eq!(out[1].kind, k_num(2.0)); - assert_eq!(out[2].kind, k_num(3.0)); - assert_eq!(out[3].kind, k_num(4.0)); - } - - // --- min / max / sum ----------------------------------------------------- - #[test] - fn test_min_max_sum_success_and_error() { - let mut ctx = Context::default(); - let mut run = dummy_run; - let nums = v_list(vec![v_num(5.0), v_num(1.0), v_num(8.0), v_num(2.0)]); - assert_eq!( - expect_num(min(&[a_val(nums.clone())], &mut ctx, &mut run)), - 1.0 - ); - assert_eq!( - expect_num(max(&[a_val(nums.clone())], &mut ctx, &mut run)), - 8.0 - ); - assert_eq!(expect_num(sum(&[a_val(nums)], &mut ctx, &mut run)), 16.0); - - // empty - match min(&[a_val(v_list(vec![]))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match max(&[a_val(v_list(vec![]))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - assert_eq!( - expect_num(sum(&[a_val(v_list(vec![]))], &mut ctx, &mut run)), - 0.0 - ); - - // wrong type - match sum(&[a_val(v_str("nope"))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match min(&[a_val(v_str("nope"))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match max(&[a_val(v_str("nope"))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - } - - // --- join ---------------------------------------------------------------- - #[test] - fn test_join_success_and_error() { - let mut ctx = Context::default(); - let mut run = dummy_run; - let arr = v_list(vec![v_str("hello"), v_str("world"), v_str("test")]); - assert_eq!( - expect_str(join(&[a_val(arr), a_val(v_str(", "))], &mut ctx, &mut run)), - "hello, world, test" - ); - - // errors - let arr2 = v_list(vec![v_str("hello")]); - match join(&[a_val(arr2.clone())], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match join( - &[a_val(v_str("not_array")), a_val(v_str(","))], - &mut ctx, - &mut run, - ) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - match join(&[a_val(arr2), a_val(v_num(42.0))], &mut ctx, &mut run) { - Signal::Failure(_) => {} - x => panic!("{:?}", x), - } - } -} diff --git a/crates/core/src/runtime/functions/boolean.rs b/crates/core/src/runtime/functions/boolean.rs deleted file mode 100644 index c4b230a..0000000 --- a/crates/core/src/runtime/functions/boolean.rs +++ /dev/null @@ -1,412 +0,0 @@ -use crate::context::argument::Argument; -use crate::context::context::Context; -use crate::context::macros::args; -use crate::context::registry::{HandlerFn, HandlerFunctionEntry, IntoFunctionEntry}; -use crate::context::signal::Signal; -use crate::runtime::error::RuntimeError; -use crate::value::value_from_i64; -use tucana::shared::{Value, value::Kind}; - -pub fn collect_boolean_functions() -> Vec<(&'static str, HandlerFunctionEntry)> { - vec![ - ("std::boolean::as_number", HandlerFn::eager(as_number, 1)), - ("std::boolean::as_text", HandlerFn::eager(as_text, 1)), - ( - "std::boolean::from_number", - HandlerFn::eager(from_number, 1), - ), - ("std::boolean::from_text", HandlerFn::eager(from_text, 1)), - ("std::boolean::is_equal", HandlerFn::eager(is_equal, 2)), - ("std::boolean::negate", HandlerFn::eager(negate, 1)), - ] -} - -fn as_number( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: bool); - Signal::Success(value_from_i64(if value { 1 } else { 0 })) -} - -fn as_text( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: bool); - Signal::Success(Value { - kind: Some(Kind::StringValue(value.to_string())), - }) -} - -fn from_number( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => number: f64); - let is_zero = number == 0.0; - Signal::Success(Value { - kind: Some(Kind::BoolValue(!is_zero)), - }) -} - -fn from_text( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => text: String); - - match text.to_lowercase().parse::() { - Ok(b) => Signal::Success(Value { - kind: Some(Kind::BoolValue(b)), - }), - Err(_) => Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!("Failed to parse boolean from string: {:?}", text), - )), - } -} - -fn is_equal( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => lhs: bool, rhs: bool); - Signal::Success(Value { - kind: Some(Kind::BoolValue(lhs == rhs)), - }) -} - -fn negate( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: bool); - Signal::Success(Value { - kind: Some(Kind::BoolValue(!value)), - }) -} -#[cfg(test)] -mod tests { - use super::*; - use crate::context::context::Context; - use crate::value::{number_to_f64, value_from_f64}; - use tucana::shared::{Value, value::Kind}; - - // ---- helpers: make Arguments ---- - fn a_bool(b: bool) -> Argument { - Argument::Eval(Value { - kind: Some(Kind::BoolValue(b)), - }) - } - fn a_num(n: f64) -> Argument { - Argument::Eval(value_from_f64(n)) - } - fn a_str(s: &str) -> Argument { - Argument::Eval(Value { - kind: Some(Kind::StringValue(s.to_string())), - }) - } - - // ---- helpers: unwrap Signal ---- - fn expect_num(sig: Signal) -> f64 { - match sig { - Signal::Success(Value { - kind: Some(Kind::NumberValue(n)), - }) => number_to_f64(&n).unwrap_or_default(), - other => panic!("Expected NumberValue, got {:?}", other), - } - } - fn expect_str(sig: Signal) -> String { - match sig { - Signal::Success(Value { - kind: Some(Kind::StringValue(s)), - }) => s, - other => panic!("Expected StringValue, got {:?}", other), - } - } - fn expect_bool(sig: Signal) -> bool { - match sig { - Signal::Success(Value { - kind: Some(Kind::BoolValue(b)), - }) => b, - other => panic!("Expected BoolValue, got {:?}", other), - } - } - - // dummy `run` closure (unused by these handlers) - fn dummy_run(_: i64, _: &mut Context) -> Signal { - Signal::Success(Value { - kind: Some(Kind::BoolValue(true)), - }) - } - - // ---- tests ---- - - #[test] - fn test_as_number_success() { - let mut ctx = Context::default(); - let mut run = dummy_run; - assert_eq!( - expect_num(as_number(&[a_bool(true)], &mut ctx, &mut run)), - 1.0 - ); - - let mut run = dummy_run; - assert_eq!( - expect_num(as_number(&[a_bool(false)], &mut ctx, &mut run)), - 0.0 - ); - } - - #[test] - fn test_as_number_errors() { - let mut ctx = Context::default(); - - // wrong arity: none - let mut run = dummy_run; - match as_number(&[], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for arity 0, got {:?}", s), - } - - // wrong type - let mut run = dummy_run; - match as_number(&[a_num(1.0)], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for non-bool, got {:?}", s), - } - - // too many args - let mut run = dummy_run; - match as_number(&[a_bool(true), a_bool(false)], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for arity 2, got {:?}", s), - } - } - - #[test] - fn test_as_text_success() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert_eq!( - expect_str(as_text(&[a_bool(true)], &mut ctx, &mut run)), - "true" - ); - - let mut run = dummy_run; - assert_eq!( - expect_str(as_text(&[a_bool(false)], &mut ctx, &mut run)), - "false" - ); - } - - #[test] - fn test_as_text_errors() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - match as_text(&[], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for arity 0, got {:?}", s), - } - - let mut run = dummy_run; - match as_text(&[a_num(5.0)], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for non-bool, got {:?}", s), - } - - let mut run = dummy_run; - match as_text(&[a_bool(true), a_bool(false)], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for arity 2, got {:?}", s), - } - } - - #[test] - fn test_from_number_success() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert_eq!( - expect_bool(from_number(&[a_num(0.0)], &mut ctx, &mut run)), - false - ); - - let mut run = dummy_run; - assert_eq!( - expect_bool(from_number(&[a_num(3.5)], &mut ctx, &mut run)), - true - ); - - let mut run = dummy_run; - assert_eq!( - expect_bool(from_number(&[a_num(-2.0)], &mut ctx, &mut run)), - true - ); - - // -0.0 should behave like 0.0 - let mut run = dummy_run; - assert_eq!( - expect_bool(from_number(&[a_num(-0.0)], &mut ctx, &mut run)), - false - ); - } - - #[test] - fn test_from_number_errors() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - match from_number(&[], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for arity 0, got {:?}", s), - } - - let mut run = dummy_run; - match from_number(&[a_bool(true)], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for non-number, got {:?}", s), - } - - let mut run = dummy_run; - match from_number(&[a_num(1.0), a_num(2.0)], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for arity 2, got {:?}", s), - } - } - - #[test] - fn test_from_text_success_and_errors() { - let mut ctx = Context::default(); - - // success (case-insensitive) - let mut run = dummy_run; - assert_eq!( - expect_bool(from_text(&[a_str("true")], &mut ctx, &mut run)), - true - ); - - let mut run = dummy_run; - assert_eq!( - expect_bool(from_text(&[a_str("FALSE")], &mut ctx, &mut run)), - false - ); - - // errors - let mut run = dummy_run; - match from_text(&[], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for arity 0, got {:?}", s), - } - - let mut run = dummy_run; - match from_text(&[a_str("yes")], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for unparseable bool, got {:?}", s), - } - - let mut run = dummy_run; - match from_text(&[a_num(1.0)], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for non-string, got {:?}", s), - } - - let mut run = dummy_run; - match from_text(&[a_str("true"), a_str("false")], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for arity 2, got {:?}", s), - } - } - - #[test] - fn test_is_equal_and_errors() { - let mut ctx = Context::default(); - - // equalities - let mut run = dummy_run; - assert_eq!( - expect_bool(is_equal(&[a_bool(true), a_bool(true)], &mut ctx, &mut run)), - true - ); - - let mut run = dummy_run; - assert_eq!( - expect_bool(is_equal( - &[a_bool(false), a_bool(false)], - &mut ctx, - &mut run - )), - true - ); - - let mut run = dummy_run; - assert_eq!( - expect_bool(is_equal(&[a_bool(true), a_bool(false)], &mut ctx, &mut run)), - false - ); - - // arity/type errors - let mut run = dummy_run; - match is_equal(&[], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for arity 0, got {:?}", s), - } - - let mut run = dummy_run; - match is_equal(&[a_bool(true)], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for arity 1, got {:?}", s), - } - - let mut run = dummy_run; - match is_equal(&[a_bool(true), a_num(1.0)], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for non-bool rhs, got {:?}", s), - } - } - - #[test] - fn test_negate_success_and_errors() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert_eq!( - expect_bool(negate(&[a_bool(true)], &mut ctx, &mut run)), - false - ); - - let mut run = dummy_run; - assert_eq!( - expect_bool(negate(&[a_bool(false)], &mut ctx, &mut run)), - true - ); - - // errors - let mut run = dummy_run; - match negate(&[], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for arity 0, got {:?}", s), - } - - let mut run = dummy_run; - match negate(&[a_num(1.0)], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for non-bool, got {:?}", s), - } - - let mut run = dummy_run; - match negate(&[a_bool(true), a_bool(false)], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for arity 2, got {:?}", s), - } - } -} diff --git a/crates/core/src/runtime/functions/control.rs b/crates/core/src/runtime/functions/control.rs deleted file mode 100644 index 6be61e7..0000000 --- a/crates/core/src/runtime/functions/control.rs +++ /dev/null @@ -1,98 +0,0 @@ -use crate::context::argument::Argument; -use crate::context::argument::ParameterNode::{Eager, Lazy}; -use crate::context::context::Context; -use crate::context::macros::args; -use crate::context::registry::{HandlerFn, HandlerFunctionEntry, IntoFunctionEntry}; -use crate::context::signal::Signal; -use crate::runtime::error::RuntimeError; -use tucana::shared::Value; -use tucana::shared::value::Kind; - -pub fn collect_control_functions() -> Vec<(&'static str, HandlerFunctionEntry)> { - vec![ - ("std::control::stop", HandlerFn::eager(stop, 0)), - ("std::control::return", HandlerFn::eager(r#return, 1)), - ( - "std::control::if", - HandlerFn::into_function_entry(r#if, vec![Eager, Lazy]), - ), - ( - "std::control::if_else", - HandlerFn::into_function_entry(if_else, vec![Eager, Lazy, Lazy]), - ), - ] -} - -fn stop( - _args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - Signal::Stop -} - -fn r#return( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: Value); - Signal::Return(value) -} - -fn r#if( - args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - let [ - Argument::Eval(Value { - kind: Some(Kind::BoolValue(bool)), - }), - Argument::Thunk(if_pointer), - ] = args - else { - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!("Expected a bool value but received {:?}", args), - )); - }; - - if *bool { - ctx.push_runtime_trace_label("branch=if".to_string()); - run(*if_pointer, ctx) - } else { - ctx.push_runtime_trace_label("branch=else".to_string()); - Signal::Success(Value { - kind: Some(Kind::NullValue(0)), - }) - } -} - -fn if_else( - args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - let [ - Argument::Eval(Value { - kind: Some(Kind::BoolValue(bool)), - }), - Argument::Thunk(if_pointer), - Argument::Thunk(else_pointer), - ] = args - else { - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!("Expected a bool value but received {:?}", args), - )); - }; - - if *bool { - ctx.push_runtime_trace_label("branch=if".to_string()); - run(*if_pointer, ctx) - } else { - ctx.push_runtime_trace_label("branch=else".to_string()); - run(*else_pointer, ctx) - } -} diff --git a/crates/core/src/runtime/functions/http.rs b/crates/core/src/runtime/functions/http.rs deleted file mode 100644 index 677c62d..0000000 --- a/crates/core/src/runtime/functions/http.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::context::argument::Argument; -use crate::context::context::Context; -use crate::context::macros::args; -use crate::context::registry::{HandlerFn, HandlerFunctionEntry, IntoFunctionEntry}; -use crate::context::signal::Signal; -use crate::runtime::error::RuntimeError; -use tucana::shared::helper::value::ToValue; -use tucana::shared::value::Kind; -use tucana::shared::{Struct, Value}; - -pub fn collect_http_functions() -> Vec<(&'static str, HandlerFunctionEntry)> { - vec![ - ("http::request::create", HandlerFn::eager(create_request, 1)), - ( - "http::response::create", - HandlerFn::eager(create_response, 4), - ), - ("rest::control::respond", HandlerFn::eager(respond, 1)), - ] -} - -fn respond( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => struct_val: Struct); - - let fields = &struct_val.fields; - - let Some(headers_val) = fields.get("headers") else { - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - "Missing 'headers' field".to_string(), - )); - }; - - let Some(status_code_val) = fields.get("http_status_code") else { - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - "Missing 'status_code' field".to_string(), - )); - }; - - let Some(payload_val) = fields.get("payload") else { - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - "Missing 'payload' field".to_string(), - )); - }; - - let Some(Kind::StructValue(_headers_struct)) = &headers_val.kind else { - return Signal::Failure(RuntimeError::simple_str( - "InvalidArgumentRuntimeError", - "Expected 'headers' to be StructValue", - )); - }; - - let Some(Kind::NumberValue(_status_code_str)) = &status_code_val.kind else { - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - "Expected 'status_code' to be NumberValue".to_string(), - )); - }; - - let Some(_payload_kind) = &payload_val.kind else { - return Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - "Expected 'payload' to have a value".to_string(), - )); - }; - - Signal::Respond(Value { - kind: Some(Kind::StructValue(struct_val.clone())), - }) -} - -fn create_request( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => http_method: String, headers: Struct, http_url: String, payload: Value); - let mut fields = std::collections::HashMap::new(); - - fields.insert(String::from("http_method"), http_method.to_value()); - fields.insert(String::from("url"), http_url.to_value()); - fields.insert(String::from("payload"), payload.clone()); - fields.insert( - String::from("headers"), - Value { - kind: Some(Kind::StructValue(headers.clone())), - }, - ); - - Signal::Success(Value { - kind: Some(Kind::StructValue(Struct { fields })), - }) -} - -fn create_response( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => http_status_code: i64, headers: Struct, payload: Value); - let mut fields = std::collections::HashMap::new(); - - fields.insert( - String::from("http_status_code"), - http_status_code.to_value(), - ); - fields.insert(String::from("payload"), payload.clone()); - - fields.insert( - String::from("headers"), - Value { - kind: Some(Kind::StructValue(headers.clone())), - }, - ); - - Signal::Success(Value { - kind: Some(Kind::StructValue(Struct { fields })), - }) -} diff --git a/crates/core/src/runtime/functions/mod.rs b/crates/core/src/runtime/functions/mod.rs deleted file mode 100644 index e466e94..0000000 --- a/crates/core/src/runtime/functions/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::context::registry::HandlerFunctionEntry; - -mod array; -mod boolean; -mod control; -mod http; -mod number; -mod object; -mod text; - -pub fn collect() -> Vec<(&'static str, HandlerFunctionEntry)> { - let mut result = vec![]; - - result.extend(array::collect_array_functions()); - result.extend(number::collect_number_functions()); - result.extend(boolean::collect_boolean_functions()); - result.extend(text::collect_text_functions()); - result.extend(object::collect_object_functions()); - result.extend(control::collect_control_functions()); - result.extend(http::collect_http_functions()); - - result -} diff --git a/crates/core/src/runtime/functions/number.rs b/crates/core/src/runtime/functions/number.rs deleted file mode 100644 index 8eb1be7..0000000 --- a/crates/core/src/runtime/functions/number.rs +++ /dev/null @@ -1,1238 +0,0 @@ -use std::f64; - -use tucana::shared::helper::value::ToValue; -use tucana::shared::{NumberValue, Value, number_value, value::Kind}; - -use crate::context::argument::Argument; -use crate::context::context::Context; -use crate::context::macros::{args, no_args}; -use crate::context::registry::{HandlerFn, HandlerFunctionEntry, IntoFunctionEntry}; -use crate::context::signal::Signal; -use crate::runtime::error::RuntimeError; -use crate::value::{number_to_f64, number_to_i64_lossy, value_from_f64, value_from_i64}; - -fn num_f64(n: &NumberValue) -> Result { - number_to_f64(n).ok_or_else(|| { - Signal::Failure(RuntimeError::simple_str( - "InvalidArgumentRuntimeError", - "Expected number", - )) - }) -} - -pub fn collect_number_functions() -> Vec<(&'static str, HandlerFunctionEntry)> { - vec![ - ("std::number::add", HandlerFn::eager(add, 2)), - ("std::number::multiply", HandlerFn::eager(multiply, 2)), - ("std::number::substract", HandlerFn::eager(substract, 2)), - ("std::number::divide", HandlerFn::eager(divide, 2)), - ("std::number::modulo", HandlerFn::eager(modulo, 2)), - ("std::number::abs", HandlerFn::eager(abs, 1)), - ("std::number::is_positive", HandlerFn::eager(is_positive, 1)), - ("std::number::is_greater", HandlerFn::eager(is_greater, 2)), - ("std::number::is_less", HandlerFn::eager(is_less, 2)), - ("std::number::is_zero", HandlerFn::eager(is_zero, 1)), - ("std::number::square", HandlerFn::eager(square, 2)), - ("std::number::exponential", HandlerFn::eager(exponential, 2)), - ("std::number::pi", HandlerFn::eager(pi, 0)), - ("std::number::euler", HandlerFn::eager(euler, 0)), - ("std::number::infinity", HandlerFn::eager(infinity, 0)), - ("std::number::round_up", HandlerFn::eager(round_up, 2)), - ("std::number::round_down", HandlerFn::eager(round_down, 2)), - ("std::number::round", HandlerFn::eager(round, 2)), - ("std::number::square_root", HandlerFn::eager(square_root, 1)), - ("std::number::root", HandlerFn::eager(root, 2)), - ("std::number::log", HandlerFn::eager(log, 2)), - ("std::number::ln", HandlerFn::eager(ln, 1)), - ("std::number::from_text", HandlerFn::eager(from_text, 1)), - ("std::number::as_text", HandlerFn::eager(as_text, 1)), - ("std::number::min", HandlerFn::eager(min, 2)), - ("std::number::max", HandlerFn::eager(max, 2)), - ("std::number::negate", HandlerFn::eager(negate, 1)), - ("std::number::random_number", HandlerFn::eager(random, 2)), - ("std::number::sin", HandlerFn::eager(sin, 1)), - ("std::number::cos", HandlerFn::eager(cos, 1)), - ("std::number::tan", HandlerFn::eager(tan, 1)), - ("std::number::arcsin", HandlerFn::eager(arcsin, 1)), - ("std::number::arccos", HandlerFn::eager(arccos, 1)), - ("std::number::arctan", HandlerFn::eager(arctan, 1)), - ("std::number::sinh", HandlerFn::eager(sinh, 1)), - ("std::number::cosh", HandlerFn::eager(cosh, 1)), - ("std::number::clamp", HandlerFn::eager(clamp, 3)), - ("std::number::is_equal", HandlerFn::eager(is_equal, 2)), - ("std::number::has_digits", HandlerFn::eager(has_digits, 2)), - ( - "std::number::remove_digits", - HandlerFn::eager(remove_digits, 2), - ), - ] -} - -fn has_digits( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - - match value.number { - Some(number) => match number { - number_value::Number::Integer(_) => Signal::Success(false.to_value()), - number_value::Number::Float(_) => Signal::Success(true.to_value()), - }, - None => Signal::Failure(RuntimeError::simple_str( - "InvlaidArgumentExeption", - "Had NumberValue but no inner number value (was null)", - )), - } -} - -fn remove_digits( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - match number_to_i64_lossy(&value) { - Some(number) => Signal::Success(value_from_i64(number)), - None => Signal::Failure(RuntimeError::simple_str( - "InvlaidArgumentExeption", - "Had NumberValue but no inner number value (was null)", - )), - } -} - -fn add( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => lhs: NumberValue, rhs: NumberValue); - if let (Some(number_value::Number::Integer(a)), Some(number_value::Number::Integer(b))) = - (lhs.number, rhs.number) - && let Some(sum) = a.checked_add(b) - { - return Signal::Success(value_from_i64(sum)); - } - let lhs = match num_f64(&lhs) { - Ok(v) => v, - Err(e) => return e, - }; - let rhs = match num_f64(&rhs) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(lhs + rhs)) -} - -fn multiply( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => lhs: NumberValue, rhs: NumberValue); - if let (Some(number_value::Number::Integer(a)), Some(number_value::Number::Integer(b))) = - (lhs.number, rhs.number) - && let Some(prod) = a.checked_mul(b) - { - return Signal::Success(value_from_i64(prod)); - } - let lhs = match num_f64(&lhs) { - Ok(v) => v, - Err(e) => return e, - }; - let rhs = match num_f64(&rhs) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(lhs * rhs)) -} - -fn substract( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => lhs: NumberValue, rhs: NumberValue); - if let (Some(number_value::Number::Integer(a)), Some(number_value::Number::Integer(b))) = - (lhs.number, rhs.number) - && let Some(diff) = a.checked_sub(b) - { - return Signal::Success(value_from_i64(diff)); - } - let lhs = match num_f64(&lhs) { - Ok(v) => v, - Err(e) => return e, - }; - let rhs = match num_f64(&rhs) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(lhs - rhs)) -} - -fn divide( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => lhs: NumberValue, rhs: NumberValue); - - let rhs_f = match num_f64(&rhs) { - Ok(v) => v, - Err(e) => return e, - }; - - if rhs_f == 0.0 { - return Signal::Failure(RuntimeError::simple_str( - "DivisionByZero", - "You cannot divide by zero", - )); - } - - if let (Some(number_value::Number::Integer(a)), Some(number_value::Number::Integer(b))) = - (lhs.number, rhs.number) - && b != 0 - && a % b == 0 - { - return Signal::Success(value_from_i64(a / b)); - } - - let lhs_f = match num_f64(&lhs) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(lhs_f / rhs_f)) -} - -fn modulo( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => lhs: NumberValue, rhs: NumberValue); - - let rhs_f = match num_f64(&rhs) { - Ok(v) => v, - Err(e) => return e, - }; - - if rhs_f == 0.0 { - return Signal::Failure(RuntimeError::simple_str( - "DivisionByZero", - "You cannot divide by zero", - )); - } - - if let (Some(number_value::Number::Integer(a)), Some(number_value::Number::Integer(b))) = - (lhs.number, rhs.number) - && b != 0 - { - return Signal::Success(value_from_i64(a % b)); - } - - let lhs_f = match num_f64(&lhs) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(lhs_f % rhs_f)) -} - -fn abs( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - if let Some(number_value::Number::Integer(i)) = value.number - && let Some(abs) = i.checked_abs() - { - return Signal::Success(value_from_i64(abs)); - } - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(value.abs())) -} - -fn is_positive( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(Value { - kind: Some(Kind::BoolValue(!value.is_sign_negative())), - }) -} - -fn is_greater( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => lhs: NumberValue, rhs: NumberValue); - let lhs = match num_f64(&lhs) { - Ok(v) => v, - Err(e) => return e, - }; - let rhs = match num_f64(&rhs) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(Value { - kind: Some(Kind::BoolValue(lhs > rhs)), - }) -} - -fn is_less( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => lhs: NumberValue, rhs: NumberValue); - let lhs = match num_f64(&lhs) { - Ok(v) => v, - Err(e) => return e, - }; - let rhs = match num_f64(&rhs) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(Value { - kind: Some(Kind::BoolValue(lhs < rhs)), - }) -} - -fn is_zero( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(Value { - kind: Some(Kind::BoolValue(value == 0.0)), - }) -} - -fn square( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - if let Some(number_value::Number::Integer(i)) = value.number - && let Some(prod) = i.checked_mul(i) - { - return Signal::Success(value_from_i64(prod)); - } - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(value.powf(2.0))) -} - -fn exponential( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => base: NumberValue, exponent: NumberValue); - match (base.number, exponent.number) { - (Some(number_value::Number::Integer(b)), Some(number_value::Number::Integer(e))) - if e >= 0 => - { - if let Ok(exp) = u32::try_from(e) - && let Some(pow) = b.checked_pow(exp) - { - return Signal::Success(value_from_i64(pow)); - } - } - _ => {} - } - let base = match num_f64(&base) { - Ok(v) => v, - Err(e) => return e, - }; - let exponent = match num_f64(&exponent) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(base.powf(exponent))) -} - -fn pi( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - no_args!(args); - Signal::Success(value_from_f64(f64::consts::PI)) -} - -fn euler( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - no_args!(args); - Signal::Success(value_from_f64(f64::consts::E)) -} - -fn infinity( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - no_args!(args); - Signal::Success(value_from_f64(f64::INFINITY)) -} - -fn round_up( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue, decimal_places: NumberValue); - let decimal_places = match num_f64(&decimal_places) { - Ok(v) => v, - Err(e) => return e, - }; - match value.number { - Some(number_value::Number::Integer(i)) if decimal_places <= 0.0 => { - return Signal::Success(value_from_i64(i)); - } - _ => {} - } - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - let factor = 10_f64.powi(decimal_places as i32); - Signal::Success(value_from_f64((value * factor).ceil() / factor)) -} - -fn round_down( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue, decimal_places: NumberValue); - let decimal_places = match num_f64(&decimal_places) { - Ok(v) => v, - Err(e) => return e, - }; - match value.number { - Some(number_value::Number::Integer(i)) if decimal_places <= 0.0 => { - return Signal::Success(value_from_i64(i)); - } - _ => {} - } - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - let factor = 10_f64.powi(decimal_places as i32); - Signal::Success(value_from_f64((value * factor).floor() / factor)) -} - -fn round( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue, decimal_places: NumberValue); - let decimal_places = match num_f64(&decimal_places) { - Ok(v) => v, - Err(e) => return e, - }; - match value.number { - Some(number_value::Number::Integer(i)) if decimal_places <= 0.0 => { - return Signal::Success(value_from_i64(i)); - } - _ => {} - } - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - let factor = 10_f64.powi(decimal_places as i32); - Signal::Success(value_from_f64((value * factor).round() / factor)) -} - -fn square_root( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(value.sqrt())) -} - -fn root( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue, root: NumberValue); - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - let root = match num_f64(&root) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(value.powf(root))) -} - -fn log( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue, base: NumberValue); - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - let base = match num_f64(&base) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(value.log(base))) -} - -fn ln( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(value.ln())) -} - -fn from_text( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => string_value: String); - - if let Ok(v) = string_value.parse::() { - return Signal::Success(value_from_i64(v)); - } - match string_value.parse::() { - Ok(v) => Signal::Success(value_from_f64(v)), - Err(_) => Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - format!("Failed to parse string as number: {}", string_value), - )), - } -} - -fn as_text( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(Value { - kind: Some(Kind::StringValue(value.to_string())), - }) -} - -fn min( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => lhs: NumberValue, rhs: NumberValue); - if let (Some(number_value::Number::Integer(a)), Some(number_value::Number::Integer(b))) = - (lhs.number, rhs.number) - { - return Signal::Success(value_from_i64(a.min(b))); - } - let lhs = match num_f64(&lhs) { - Ok(v) => v, - Err(e) => return e, - }; - let rhs = match num_f64(&rhs) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(lhs.min(rhs))) -} - -fn max( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => lhs: NumberValue, rhs: NumberValue); - if let (Some(number_value::Number::Integer(a)), Some(number_value::Number::Integer(b))) = - (lhs.number, rhs.number) - { - return Signal::Success(value_from_i64(a.max(b))); - } - let lhs = match num_f64(&lhs) { - Ok(v) => v, - Err(e) => return e, - }; - let rhs = match num_f64(&rhs) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(lhs.max(rhs))) -} - -fn negate( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - if let Some(number_value::Number::Integer(i)) = value.number - && let Some(neg) = i.checked_neg() - { - return Signal::Success(value_from_i64(neg)); - } - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(-value)) -} - -fn random( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => min: NumberValue, max: NumberValue); - - let min_f = match num_f64(&min) { - Ok(v) => v, - Err(e) => return e, - }; - let max_f = match num_f64(&max) { - Ok(v) => v, - Err(e) => return e, - }; - - if min_f > max_f { - return Signal::Failure(RuntimeError::simple_str( - "InvalidRange", - "First number can't be bigger then second when creating a range for std::math::random", - )); - } - - let value = rand::random_range(min_f..=max_f); - - Signal::Success(value_from_f64(value)) -} - -fn sin( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(value.sin())) -} - -fn cos( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(value.cos())) -} - -fn tan( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(value.tan())) -} - -fn arcsin( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(value.asin())) -} - -fn arccos( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(value.acos())) -} - -fn arctan( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(value.atan())) -} - -fn sinh( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(value.sinh())) -} - -fn cosh( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue); - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(value.cosh())) -} - -fn clamp( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: NumberValue, min: NumberValue, max: NumberValue); - if let ( - Some(number_value::Number::Integer(v)), - Some(number_value::Number::Integer(min)), - Some(number_value::Number::Integer(max)), - ) = (value.number, min.number, max.number) - { - return Signal::Success(value_from_i64(v.clamp(min, max))); - } - let value = match num_f64(&value) { - Ok(v) => v, - Err(e) => return e, - }; - let min = match num_f64(&min) { - Ok(v) => v, - Err(e) => return e, - }; - let max = match num_f64(&max) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(value_from_f64(value.clamp(min, max))) -} - -fn is_equal( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => lhs: NumberValue, rhs: NumberValue); - let lhs = match num_f64(&lhs) { - Ok(v) => v, - Err(e) => return e, - }; - let rhs = match num_f64(&rhs) { - Ok(v) => v, - Err(e) => return e, - }; - Signal::Success(Value { - kind: Some(Kind::BoolValue(lhs == rhs)), - }) -} -#[cfg(test)] -mod tests { - use super::*; - use crate::context::argument::Argument; - use crate::context::context::Context; - use crate::value::{number_to_f64, value_from_f64, value_from_i64}; - use tucana::shared::{Value, number_value, value::Kind}; - - // ---- helpers: Arguments ---- - fn a_num(n: f64) -> Argument { - Argument::Eval(value_from_f64(n)) - } - fn a_int(n: i64) -> Argument { - Argument::Eval(value_from_i64(n)) - } - fn a_str(s: &str) -> Argument { - Argument::Eval(Value { - kind: Some(Kind::StringValue(s.to_string())), - }) - } - - // ---- helpers: extractors ---- - fn expect_num(sig: Signal) -> f64 { - match sig { - Signal::Success(Value { - kind: Some(Kind::NumberValue(n)), - }) => number_to_f64(&n).unwrap_or_default(), - other => panic!("Expected NumberValue, got {:?}", other), - } - } - fn expect_bool(sig: Signal) -> bool { - match sig { - Signal::Success(Value { - kind: Some(Kind::BoolValue(b)), - }) => b, - other => panic!("Expected BoolValue, got {:?}", other), - } - } - fn expect_int(sig: Signal) -> i64 { - match sig { - Signal::Success(Value { - kind: Some(Kind::NumberValue(n)), - }) => match n.number { - Some(number_value::Number::Integer(i)) => i, - Some(number_value::Number::Float(f)) => { - panic!("Expected Integer NumberValue, got Float({})", f) - } - None => panic!("Expected Integer NumberValue, got None"), - }, - other => panic!("Expected NumberValue, got {:?}", other), - } - } - fn expect_str(sig: Signal) -> String { - match sig { - Signal::Success(Value { - kind: Some(Kind::StringValue(s)), - }) => s, - other => panic!("Expected StringValue, got {:?}", other), - } - } - - // dummy runner for handlers that accept `run: &mut dyn FnMut(i64, &mut Context) -> Signal` - fn dummy_run(_: i64, _: &mut Context) -> Signal { - Signal::Success(Value { - kind: Some(Kind::NullValue(0)), - }) - } - - #[test] - fn test_add_and_multiply() { - let mut ctx = Context::default(); - let mut run = dummy_run; - assert_eq!( - expect_num(add(&[a_num(5.0), a_num(3.0)], &mut ctx, &mut run)), - 8.0 - ); - - let mut run = dummy_run; - assert_eq!( - expect_num(multiply(&[a_num(4.0), a_num(2.5)], &mut ctx, &mut run)), - 10.0 - ); - } - - #[test] - fn test_has_digits_and_remove_digits() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert!(!expect_bool(has_digits(&[a_int(42)], &mut ctx, &mut run))); - - let mut run = dummy_run; - assert!(expect_bool(has_digits(&[a_num(42.5)], &mut ctx, &mut run))); - - let mut run = dummy_run; - assert_eq!( - expect_int(remove_digits(&[a_int(123)], &mut ctx, &mut run)), - 123 - ); - - let mut run = dummy_run; - assert_eq!( - expect_int(remove_digits(&[a_num(12.99)], &mut ctx, &mut run)), - 12 - ); - } - - #[test] - fn test_substract_and_divide() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert_eq!( - expect_num(substract(&[a_num(10.0), a_num(4.0)], &mut ctx, &mut run)), - 6.0 - ); - - let mut run = dummy_run; - assert_eq!( - expect_num(divide(&[a_num(15.0), a_num(3.0)], &mut ctx, &mut run)), - 5.0 - ); - - // divide by zero -> Failure - let mut run = dummy_run; - match divide(&[a_num(10.0), a_num(0.0)], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure on divide by zero, got {:?}", s), - } - } - - #[test] - fn test_modulo_and_abs() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert_eq!( - expect_num(modulo(&[a_num(10.0), a_num(3.0)], &mut ctx, &mut run)), - 1.0 - ); - - // modulo by zero -> Failure - let mut run = dummy_run; - match modulo(&[a_num(10.0), a_num(0.0)], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure on modulo by zero, got {:?}", s), - } - - let mut run = dummy_run; - assert_eq!(expect_num(abs(&[a_num(-7.5)], &mut ctx, &mut run)), 7.5); - } - - #[test] - fn test_comparisons_and_zero() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert!(expect_bool(is_positive(&[a_num(5.0)], &mut ctx, &mut run))); - let mut run = dummy_run; - assert!(!expect_bool(is_positive( - &[a_num(-1.0)], - &mut ctx, - &mut run - ))); - let mut run = dummy_run; - assert!(expect_bool(is_positive(&[a_num(0.0)], &mut ctx, &mut run))); - - let mut run = dummy_run; - assert!(expect_bool(is_greater( - &[a_num(10.0), a_num(5.0)], - &mut ctx, - &mut run - ))); - let mut run = dummy_run; - assert!(expect_bool(is_less( - &[a_num(3.0), a_num(7.0)], - &mut ctx, - &mut run - ))); - - let mut run = dummy_run; - assert!(expect_bool(is_zero(&[a_num(0.0)], &mut ctx, &mut run))); - let mut run = dummy_run; - assert!(!expect_bool(is_zero(&[a_num(0.01)], &mut ctx, &mut run))); - } - - #[test] - fn test_powers_and_exponential() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert_eq!(expect_num(square(&[a_num(4.0)], &mut ctx, &mut run)), 16.0); - - let mut run = dummy_run; - assert_eq!( - expect_num(exponential(&[a_num(2.0), a_num(3.0)], &mut ctx, &mut run)), - 8.0 - ); - } - - #[test] - fn test_constants() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert!( - (expect_num(pi(&[], &mut ctx, &mut run)) - std::f64::consts::PI).abs() < f64::EPSILON - ); - - let mut run = dummy_run; - assert!( - (expect_num(euler(&[], &mut ctx, &mut run)) - std::f64::consts::E).abs() < f64::EPSILON - ); - - let mut run = dummy_run; - let inf = expect_num(infinity(&[], &mut ctx, &mut run)); - assert!(inf.is_infinite() && inf.is_sign_positive()); - } - - #[test] - fn test_rounding() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert_eq!( - expect_num(round_up( - &[a_num(f64::consts::PI), a_num(2.0)], - &mut ctx, - &mut run - )), - 3.15 - ); - - let mut run = dummy_run; - assert_eq!( - expect_num(round_down( - &[a_num(f64::consts::PI), a_num(2.0)], - &mut ctx, - &mut run - )), - 3.14 - ); - - let mut run = dummy_run; - assert_eq!( - expect_num(round(&[a_num(3.145), a_num(2.0)], &mut ctx, &mut run)), - 3.15 - ); - } - - #[test] - fn test_roots_and_logs() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert_eq!( - expect_num(square_root(&[a_num(16.0)], &mut ctx, &mut run)), - 4.0 - ); - - // cube root via exponent 1/3 - let mut run = dummy_run; - let r = expect_num(root(&[a_num(8.0), a_num(1.0 / 3.0)], &mut ctx, &mut run)); - assert!((r - 2.0).abs() < 1e-6); - - let mut run = dummy_run; - let lg = expect_num(log(&[a_num(100.0), a_num(10.0)], &mut ctx, &mut run)); - assert!((lg - 2.0).abs() < f64::EPSILON); - - let mut run = dummy_run; - let ln1 = expect_num(ln(&[a_num(f64::consts::E)], &mut ctx, &mut run)); - assert!((ln1 - 1.0).abs() < f64::EPSILON); - } - - #[test] - fn test_text_conversions() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert_eq!( - expect_num(from_text(&[a_str("42.5")], &mut ctx, &mut run)), - 42.5 - ); - - let mut run = dummy_run; - assert_eq!( - expect_str(as_text(&[a_num(42.5)], &mut ctx, &mut run)), - "42.5".to_string() - ); - - // from_text failure - let mut run = dummy_run; - match from_text(&[a_str("not_a_number")], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for invalid parse, got {:?}", s), - } - } - - #[test] - fn test_min_max_and_negate() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert_eq!( - expect_num(min(&[a_num(3.0), a_num(7.0)], &mut ctx, &mut run)), - 3.0 - ); - - let mut run = dummy_run; - assert_eq!( - expect_num(max(&[a_num(3.0), a_num(7.0)], &mut ctx, &mut run)), - 7.0 - ); - - let mut run = dummy_run; - assert_eq!(expect_num(negate(&[a_num(5.0)], &mut ctx, &mut run)), -5.0); - } - - #[test] - fn test_random_range() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - let r = expect_num(random(&[a_num(1.0), a_num(10.0)], &mut ctx, &mut run)); - assert!(r >= 1.0 && r < 10.0); - } - - #[test] - fn test_random_range_numbers_equal() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - let r = expect_num(random(&[a_num(1.0), a_num(1.0)], &mut ctx, &mut run)); - assert!(r == 1.0); - } - - #[test] - fn test_random_range_fist_bigger_then_second() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - let res = random(&[a_num(10.0), a_num(1.0)], &mut ctx, &mut run); - assert!(matches!(res, Signal::Failure(_))); - } - - #[test] - fn test_trig_and_hyperbolic() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - let s = expect_num(sin(&[a_num(f64::consts::PI / 2.0)], &mut ctx, &mut run)); - assert!((s - 1.0).abs() < 1e-12); - - let mut run = dummy_run; - let c = expect_num(cos(&[a_num(0.0)], &mut ctx, &mut run)); - assert!((c - 1.0).abs() < 1e-12); - - let mut run = dummy_run; - let t = expect_num(tan(&[a_num(f64::consts::PI / 4.0)], &mut ctx, &mut run)); - assert!((t - 1.0).abs() < 1e-4); - - let mut run = dummy_run; - let asn = expect_num(arcsin(&[a_num(1.0)], &mut ctx, &mut run)); - assert!((asn - f64::consts::PI / 2.0).abs() < 1e-12); - - let mut run = dummy_run; - let acs = expect_num(arccos(&[a_num(1.0)], &mut ctx, &mut run)); - assert!(acs.abs() < 1e-12); - - let mut run = dummy_run; - let atn = expect_num(arctan(&[a_num(1.0)], &mut ctx, &mut run)); - assert!((atn - f64::consts::PI / 4.0).abs() < 1e-12); - - let mut run = dummy_run; - let sh = expect_num(sinh(&[a_num(0.0)], &mut ctx, &mut run)); - assert!(sh.abs() < 1e-12); - - let mut run = dummy_run; - let ch = expect_num(cosh(&[a_num(0.0)], &mut ctx, &mut run)); - assert!((ch - 1.0).abs() < 1e-12); - } - - #[test] - fn test_clamp_and_is_equal() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert_eq!( - expect_num(clamp( - &[a_num(5.0), a_num(1.0), a_num(10.0)], - &mut ctx, - &mut run - )), - 5.0 - ); - - let mut run = dummy_run; - assert_eq!( - expect_num(clamp( - &[a_num(-5.0), a_num(1.0), a_num(10.0)], - &mut ctx, - &mut run - )), - 1.0 - ); - - let mut run = dummy_run; - assert_eq!( - expect_num(clamp( - &[a_num(15.0), a_num(1.0), a_num(10.0)], - &mut ctx, - &mut run - )), - 10.0 - ); - - let mut run = dummy_run; - assert!(expect_bool(is_equal( - &[a_num(5.0), a_num(5.0)], - &mut ctx, - &mut run - ))); - - let mut run = dummy_run; - assert!(!expect_bool(is_equal( - &[a_num(5.0), a_num(3.0)], - &mut ctx, - &mut run - ))); - } -} diff --git a/crates/core/src/runtime/functions/text.rs b/crates/core/src/runtime/functions/text.rs deleted file mode 100644 index 6ad1f98..0000000 --- a/crates/core/src/runtime/functions/text.rs +++ /dev/null @@ -1,996 +0,0 @@ -use crate::context::argument::Argument; -use crate::context::context::Context; -use crate::context::macros::args; -use crate::context::registry::{HandlerFn, HandlerFunctionEntry, IntoFunctionEntry}; -use crate::context::signal::Signal; -use crate::runtime::error::RuntimeError; -use crate::value::{number_to_f64, number_to_i64_lossy, value_from_i64}; -use base64::Engine; -use tucana::shared::{ListValue, Value, value::Kind}; - -pub fn collect_text_functions() -> Vec<(&'static str, HandlerFunctionEntry)> { - vec![ - ("std::text::as_bytes", HandlerFn::eager(as_bytes, 1)), - ("std::text::byte_size", HandlerFn::eager(byte_size, 1)), - ("std::text::capitalize", HandlerFn::eager(capitalize, 1)), - ("std::text::lowercase", HandlerFn::eager(lowercase, 1)), - ("std::text::uppercase", HandlerFn::eager(uppercase, 1)), - ("std::text::swapcase", HandlerFn::eager(swapcase, 1)), - ("std::text::trim", HandlerFn::eager(trim, 1)), - ("std::text::chars", HandlerFn::eager(chars, 1)), - ("std::text::at", HandlerFn::eager(at, 2)), - ("std::text::append", HandlerFn::eager(append, 2)), - ("std::text::prepend", HandlerFn::eager(prepend, 2)), - ("std::text::insert", HandlerFn::eager(insert, 3)), - ("std::text::length", HandlerFn::eager(length, 1)), - ("std::text::reverse", HandlerFn::eager(reverse, 1)), - ("std::text::remove", HandlerFn::eager(remove, 3)), - ("std::text::replace", HandlerFn::eager(replace, 3)), - ( - "std::text::replace_first", - HandlerFn::eager(replace_first, 3), - ), - ("std::text::replace_last", HandlerFn::eager(replace_last, 3)), - ("std::text::hex", HandlerFn::eager(hex, 1)), - ("std::text::octal", HandlerFn::eager(octal, 1)), - ("std::text::index_of", HandlerFn::eager(index_of, 2)), - ("std::text::contains", HandlerFn::eager(contains, 2)), - ("std::text::split", HandlerFn::eager(split, 2)), - ("std::text::starts_with", HandlerFn::eager(starts_with, 2)), - ("std::text::ends_with", HandlerFn::eager(ends_with, 2)), - ("std::text::to_ascii", HandlerFn::eager(to_ascii, 1)), - ("std::text::from_ascii", HandlerFn::eager(from_ascii, 1)), - ("std::text::encode", HandlerFn::eager(encode, 2)), - ("std::text::decode", HandlerFn::eager(decode, 2)), - ("std::text::is_equal", HandlerFn::eager(is_equal, 2)), - ] -} - -fn arg_err>(msg: S) -> Signal { - Signal::Failure(RuntimeError::simple( - "InvalidArgumentRuntimeError", - msg.into(), - )) -} - -fn as_bytes( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String); - - let bytes: Vec = value - .as_bytes() - .iter() - .map(|b| value_from_i64(*b as i64)) - .collect(); - - Signal::Success(Value { - kind: Some(Kind::ListValue(ListValue { values: bytes })), - }) -} - -fn byte_size( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String); - Signal::Success(value_from_i64(value.len() as i64)) -} - -fn capitalize( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String); - - let capitalized = value - .split(' ') - .map(|word| { - if word.is_empty() { - return String::from(word); - } - let mut chars = word.chars(); - match chars.next() { - Some(first) => first.to_uppercase().collect::() + chars.as_str(), - None => String::from(word), - } - }) - .collect::>() - .join(" "); - - Signal::Success(Value { - kind: Some(Kind::StringValue(capitalized)), - }) -} - -fn uppercase( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String); - Signal::Success(Value { - kind: Some(Kind::StringValue(value.to_uppercase())), - }) -} - -fn lowercase( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String); - Signal::Success(Value { - kind: Some(Kind::StringValue(value.to_lowercase())), - }) -} - -fn swapcase( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String); - - let swapped = value - .chars() - .map(|c| { - if c.is_uppercase() { - c.to_lowercase().collect::() - } else if c.is_lowercase() { - c.to_uppercase().collect::() - } else { - c.to_string() - } - }) - .collect::(); - - Signal::Success(Value { - kind: Some(Kind::StringValue(swapped)), - }) -} - -fn trim( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String); - Signal::Success(Value { - kind: Some(Kind::StringValue(value.trim().to_string())), - }) -} - -fn chars( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String); - - let list = value - .chars() - .map(|c| Value { - kind: Some(Kind::StringValue(c.to_string())), - }) - .collect::>(); - - Signal::Success(Value { - kind: Some(Kind::ListValue(ListValue { values: list })), - }) -} - -fn at( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String, index: tucana::shared::NumberValue); - let index = match number_to_i64_lossy(&index) { - Some(v) => v, - None => return arg_err("Expected a number index"), - }; - - if index < 0 { - return arg_err("Expected a non-negative index"); - } - - let idx = index as usize; - match value.chars().nth(idx) { - Some(c) => Signal::Success(Value { - kind: Some(Kind::StringValue(c.to_string())), - }), - None => Signal::Failure(RuntimeError::simple( - "IndexOutOfBoundsRuntimeError", - format!( - "Index {} is out of bounds for string of length {}", - index, - value.chars().count() - ), - )), - } -} - -fn append( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String, suffix: String); - Signal::Success(Value { - kind: Some(Kind::StringValue(value + &suffix)), - }) -} - -fn prepend( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String, prefix: String); - Signal::Success(Value { - kind: Some(Kind::StringValue(prefix + &value)), - }) -} - -fn insert( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String, position: tucana::shared::NumberValue, text: String); - let position = match number_to_i64_lossy(&position) { - Some(v) => v, - None => return arg_err("Expected a number position"), - }; - - if position < 0 { - return arg_err("Expected a non-negative position"); - } - - let pos = position as usize; - // Byte-wise position (consistent with previous behavior). For char-wise, compute byte index from char idx. - if pos > value.len() { - return Signal::Failure(RuntimeError::simple( - "IndexOutOfBoundsRuntimeError", - format!("Position {} exceeds byte length {}", pos, value.len()), - )); - } - - let mut new_value = value; - new_value.insert_str(pos, &text); - - Signal::Success(Value { - kind: Some(Kind::StringValue(new_value)), - }) -} - -fn length( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String); - Signal::Success(value_from_i64(value.chars().count() as i64)) -} - -fn remove( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String, from: tucana::shared::NumberValue, to: tucana::shared::NumberValue); - let from = match number_to_i64_lossy(&from) { - Some(v) => v, - None => return arg_err("Expected number 'from'"), - }; - let to = match number_to_i64_lossy(&to) { - Some(v) => v, - None => return arg_err("Expected number 'to'"), - }; - - if from < 0 || to < 0 { - return arg_err("Expected non-negative indices"); - } - - let from_u = from as usize; - let to_u = to as usize; - - let chars = value.chars().collect::>(); - if from_u > chars.len() || to_u > chars.len() { - return Signal::Failure(RuntimeError::simple( - "IndexOutOfBoundsRuntimeError", - format!( - "Indices [{}, {}) out of bounds for length {}", - from_u, - to_u, - chars.len() - ), - )); - } - - let new = chars - .into_iter() - .enumerate() - .filter(|&(i, _)| i < from_u || i >= to_u) - .map(|e| e.1) - .collect::(); - - Signal::Success(Value { - kind: Some(Kind::StringValue(new)), - }) -} - -fn replace( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String, old: String, new: String); - let replaced = value.replace(&old, &new); - Signal::Success(Value { - kind: Some(Kind::StringValue(replaced)), - }) -} - -fn replace_first( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String, old: String, new: String); - let replaced = value.replacen(&old, &new, 1); - Signal::Success(Value { - kind: Some(Kind::StringValue(replaced)), - }) -} - -fn replace_last( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String, old: String, new: String); - - fn replace_last_impl(haystack: &str, needle: &str, replacement: &str) -> String { - if let Some(pos) = haystack.rfind(needle) { - let mut result = - String::with_capacity(haystack.len() - needle.len() + replacement.len()); - result.push_str(&haystack[..pos]); - result.push_str(replacement); - result.push_str(&haystack[pos + needle.len()..]); - result - } else { - haystack.to_string() - } - } - - let replaced = replace_last_impl(&value, &old, &new); - Signal::Success(Value { - kind: Some(Kind::StringValue(replaced)), - }) -} - -fn hex( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String); - - let hex = value - .as_bytes() - .iter() - .map(|b| format!("{:02x}", b)) - .collect::(); - - Signal::Success(Value { - kind: Some(Kind::StringValue(hex)), - }) -} - -fn octal( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String); - - let oct = value - .as_bytes() - .iter() - .map(|b| format!("{:03o}", b)) - .collect::(); - - Signal::Success(Value { - kind: Some(Kind::StringValue(oct)), - }) -} - -fn index_of( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String, sub: String); - - match value.find(&sub) { - Some(idx) => Signal::Success(value_from_i64(idx as i64)), - None => Signal::Success(value_from_i64(-1)), - } -} - -fn contains( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String, sub: String); - Signal::Success(Value { - kind: Some(Kind::BoolValue(value.contains(&sub))), - }) -} - -fn split( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String, delimiter: String); - - let parts = value - .split(&delimiter) - .map(|s| Value { - kind: Some(Kind::StringValue(s.to_string())), - }) - .collect::>(); - - Signal::Success(Value { - kind: Some(Kind::ListValue(ListValue { values: parts })), - }) -} - -fn reverse( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String); - - let reversed = value.chars().rev().collect::(); - Signal::Success(Value { - kind: Some(Kind::StringValue(reversed)), - }) -} - -fn starts_with( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String, prefix: String); - Signal::Success(Value { - kind: Some(Kind::BoolValue(value.starts_with(&prefix))), - }) -} - -fn ends_with( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String, suffix: String); - Signal::Success(Value { - kind: Some(Kind::BoolValue(value.ends_with(&suffix))), - }) -} - -fn to_ascii( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String); - - let ascii = value - .bytes() - .map(|b| value_from_i64(b as i64)) - .collect::>(); - - Signal::Success(Value { - kind: Some(Kind::ListValue(ListValue { values: ascii })), - }) -} - -fn from_ascii( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - // Requires a TryFromArg impl for ListValue in your macro system. - args!(args => list: ListValue); - - let string = list - .values - .iter() - .map(|v| match v { - Value { - kind: Some(Kind::NumberValue(n)), - } => match number_to_f64(n) { - Some(n) if (0.0..=127.0).contains(&n) => Some(n as u8 as char), - _ => None, - }, - _ => None, - }) - .collect::>(); - - match string { - Some(s) => Signal::Success(Value { - kind: Some(Kind::StringValue(s)), - }), - None => arg_err("Expected a list of numbers between 0 and 127"), - } -} - -// NOTE: "encode"/"decode" currently only support base64. -fn encode( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String, encoding: String); - - let encoded = match encoding.to_lowercase().as_str() { - "base64" => base64::prelude::BASE64_STANDARD.encode(value), - _ => { - return arg_err(format!("Unsupported encoding: {}", encoding)); - } - }; - - Signal::Success(Value { - kind: Some(Kind::StringValue(encoded)), - }) -} - -fn decode( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => value: String, encoding: String); - - let decoded = match encoding.to_lowercase().as_str() { - "base64" => match base64::prelude::BASE64_STANDARD.decode(value) { - Ok(bytes) => match String::from_utf8(bytes) { - Ok(s) => s, - Err(err) => { - return Signal::Failure(RuntimeError::simple( - "DecodeError", - format!("Failed to decode base64 bytes to UTF-8: {:?}", err), - )); - } - }, - Err(err) => { - return Signal::Failure(RuntimeError::simple( - "DecodeError", - format!("Failed to decode base64 string: {:?}", err), - )); - } - }, - _ => return arg_err(format!("Unsupported decoding: {}", encoding)), - }; - - Signal::Success(Value { - kind: Some(Kind::StringValue(decoded)), - }) -} - -fn is_equal( - args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, -) -> Signal { - args!(args => lhs: String, rhs: String); - Signal::Success(Value { - kind: Some(Kind::BoolValue(lhs == rhs)), - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::context::context::Context; - use crate::value::{number_to_f64, value_from_f64, value_from_i64}; - use tucana::shared::{ListValue, Value, value::Kind}; - - // ---------- helpers: build Arguments ---------- - fn a_str(s: &str) -> Argument { - Argument::Eval(Value { - kind: Some(Kind::StringValue(s.to_string())), - }) - } - fn a_num(n: f64) -> Argument { - Argument::Eval(value_from_f64(n)) - } - fn a_list(vals: Vec) -> Argument { - Argument::Eval(Value { - kind: Some(Kind::ListValue(ListValue { values: vals })), - }) - } - - // ---------- helpers: build bare Values ---------- - fn v_str(s: &str) -> Value { - Value { - kind: Some(Kind::StringValue(s.to_string())), - } - } - fn v_num(n: i64) -> Value { - value_from_i64(n) - } - - // ---------- helpers: extract from Signal ---------- - fn expect_num(sig: Signal) -> f64 { - match sig { - Signal::Success(Value { - kind: Some(Kind::NumberValue(n)), - }) => number_to_f64(&n).unwrap_or_default(), - other => panic!("Expected NumberValue, got {:?}", other), - } - } - fn expect_bool(sig: Signal) -> bool { - match sig { - Signal::Success(Value { - kind: Some(Kind::BoolValue(b)), - }) => b, - other => panic!("Expected BoolValue, got {:?}", other), - } - } - fn expect_str(sig: Signal) -> String { - match sig { - Signal::Success(Value { - kind: Some(Kind::StringValue(s)), - }) => s, - other => panic!("Expected StringValue, got {:?}", other), - } - } - fn expect_list(sig: Signal) -> Vec { - match sig { - Signal::Success(Value { - kind: Some(Kind::ListValue(ListValue { values })), - }) => values, - other => panic!("Expected ListValue, got {:?}", other), - } - } - - // dummy runner for handlers that accept `run: &mut dyn FnMut(i64, &mut Context) -> Signal` - fn dummy_run(_: i64, _: &mut Context) -> Signal { - Signal::Success(Value { - kind: Some(Kind::NullValue(0)), - }) - } - - // ---------- tests ---------- - - #[test] - fn test_as_bytes_and_byte_size() { - let mut ctx = Context::default(); - let mut run = dummy_run; - - // "hello" -> 5 bytes - let bytes = expect_list(as_bytes(&[a_str("hello")], &mut ctx, &mut run)); - assert_eq!(bytes.len(), 5); - assert_eq!(bytes[0], v_num(104)); // 'h' - - let mut run = dummy_run; - assert_eq!( - expect_num(byte_size(&[a_str("hello")], &mut ctx, &mut run)), - 5.0 - ); - - // unicode: "café" -> 5 bytes, 4 chars - let mut run = dummy_run; - assert_eq!( - expect_num(byte_size(&[a_str("café")], &mut ctx, &mut run)), - 5.0 - ); - let mut run = dummy_run; - assert_eq!( - expect_num(length(&[a_str("café")], &mut ctx, &mut run)), - 4.0 - ); - } - - #[test] - fn test_case_ops_and_trim() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert_eq!( - expect_str(capitalize(&[a_str("hello world")], &mut ctx, &mut run)), - "Hello World" - ); - - let mut run = dummy_run; - assert_eq!( - expect_str(uppercase(&[a_str("Hello")], &mut ctx, &mut run)), - "HELLO" - ); - - let mut run = dummy_run; - assert_eq!( - expect_str(lowercase(&[a_str("Hello")], &mut ctx, &mut run)), - "hello" - ); - - let mut run = dummy_run; - assert_eq!( - expect_str(swapcase(&[a_str("HeLLo123")], &mut ctx, &mut run)), - "hEllO123" - ); - - let mut run = dummy_run; - assert_eq!( - expect_str(trim(&[a_str(" hi ")], &mut ctx, &mut run)), - "hi" - ); - } - - #[test] - fn test_chars_and_at() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - let chars_list = expect_list(chars(&[a_str("abc")], &mut ctx, &mut run)); - assert_eq!(chars_list, vec![v_str("a"), v_str("b"), v_str("c")]); - - let mut run = dummy_run; - assert_eq!( - expect_str(at(&[a_str("hello"), a_num(1.0)], &mut ctx, &mut run)), - "e" - ); - - // out-of-bounds - let mut run = dummy_run; - match at(&[a_str("hi"), a_num(5.0)], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure, got {:?}", s), - } - // negative - let mut run = dummy_run; - match at(&[a_str("hi"), a_num(-1.0)], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure, got {:?}", s), - } - } - - #[test] - fn test_append_prepend_insert_length() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert_eq!( - expect_str(append( - &[a_str("hello"), a_str(" world")], - &mut ctx, - &mut run - )), - "hello world" - ); - - let mut run = dummy_run; - assert_eq!( - expect_str(prepend( - &[a_str("world"), a_str("hello ")], - &mut ctx, - &mut run - )), - "hello world" - ); - - // insert uses BYTE index; for ASCII this matches char index - let mut run = dummy_run; - assert_eq!( - expect_str(insert( - &[a_str("hello"), a_num(2.0), a_str("XXX")], - &mut ctx, - &mut run - )), - "heXXXllo" - ); - - let mut run = dummy_run; - assert_eq!( - expect_num(length(&[a_str("hello")], &mut ctx, &mut run)), - 5.0 - ); - } - - #[test] - fn test_remove_replace_variants() { - let mut ctx = Context::default(); - - // remove uses CHAR indices [from, to) - let mut run = dummy_run; - assert_eq!( - expect_str(remove( - &[a_str("hello world"), a_num(2.0), a_num(7.0)], - &mut ctx, - &mut run - )), - "heorld" - ); - - let mut run = dummy_run; - assert_eq!( - expect_str(replace( - &[a_str("hello world hello"), a_str("hello"), a_str("hi")], - &mut ctx, - &mut run - )), - "hi world hi" - ); - - let mut run = dummy_run; - assert_eq!( - expect_str(replace_first( - &[a_str("one two one"), a_str("one"), a_str("1")], - &mut ctx, - &mut run - )), - "1 two one" - ); - - let mut run = dummy_run; - assert_eq!( - expect_str(replace_last( - &[a_str("one two one"), a_str("one"), a_str("1")], - &mut ctx, - &mut run - )), - "one two 1" - ); - } - - #[test] - fn test_hex_octal_reverse() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert_eq!( - expect_str(hex(&[a_str("hello")], &mut ctx, &mut run)), - "68656c6c6f" - ); - - let mut run = dummy_run; - assert_eq!(expect_str(octal(&[a_str("A")], &mut ctx, &mut run)), "101"); - - let mut run = dummy_run; - assert_eq!( - expect_str(reverse(&[a_str("hello")], &mut ctx, &mut run)), - "olleh" - ); - } - - #[test] - fn test_index_contains_split_starts_ends() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert_eq!( - expect_num(index_of( - &[a_str("hello world"), a_str("world")], - &mut ctx, - &mut run - )), - 6.0 - ); - - let mut run = dummy_run; - assert_eq!( - expect_num(index_of( - &[a_str("hello"), a_str("xyz")], - &mut ctx, - &mut run - )), - -1.0 - ); - - let mut run = dummy_run; - assert!(expect_bool(contains( - &[a_str("hello world"), a_str("world")], - &mut ctx, - &mut run - ))); - - let mut run = dummy_run; - let split_list = expect_list(split(&[a_str("a,b,c"), a_str(",")], &mut ctx, &mut run)); - assert_eq!(split_list, vec![v_str("a"), v_str("b"), v_str("c")]); - - let mut run = dummy_run; - assert!(expect_bool(starts_with( - &[a_str("hello"), a_str("he")], - &mut ctx, - &mut run - ))); - - let mut run = dummy_run; - assert!(expect_bool(ends_with( - &[a_str("hello"), a_str("lo")], - &mut ctx, - &mut run - ))); - } - - #[test] - fn test_to_ascii_and_from_ascii() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - let ascii_vals = expect_list(to_ascii(&[a_str("AB")], &mut ctx, &mut run)); - assert_eq!(ascii_vals, vec![v_num(65), v_num(66)]); - - let mut run = dummy_run; - let list_arg = a_list(vec![v_num(65), v_num(66), v_num(67)]); - assert_eq!( - expect_str(from_ascii(&[list_arg], &mut ctx, &mut run)), - "ABC" - ); - - // invalid element - let mut run = dummy_run; - let list_arg = a_list(vec![v_num(65), v_num(128)]); - match from_ascii(&[list_arg], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for invalid ASCII, got {:?}", s), - } - } - - #[test] - fn test_encode_decode_base64_and_is_equal() { - let mut ctx = Context::default(); - - let mut run = dummy_run; - assert_eq!( - expect_str(encode( - &[a_str("hello"), a_str("BASE64")], - &mut ctx, - &mut run - )), - "aGVsbG8=" - ); - - let mut run = dummy_run; - assert_eq!( - expect_str(decode( - &[a_str("aGVsbG8="), a_str("base64")], - &mut ctx, - &mut run - )), - "hello" - ); - - // unsupported codec - let mut run = dummy_run; - match encode(&[a_str("data"), a_str("gug")], &mut ctx, &mut run) { - Signal::Failure(_) => {} - s => panic!("Expected Failure for unsupported encoding, got {:?}", s), - } - - let mut run = dummy_run; - assert!(expect_bool(is_equal( - &[a_str("x"), a_str("x")], - &mut ctx, - &mut run - ))); - let mut run = dummy_run; - assert!(!expect_bool(is_equal( - &[a_str("x"), a_str("y")], - &mut ctx, - &mut run - ))); - } -} diff --git a/crates/core/src/runtime/mod.rs b/crates/core/src/runtime/mod.rs deleted file mode 100644 index 7715a61..0000000 --- a/crates/core/src/runtime/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod error; -pub mod functions; -pub mod remote; diff --git a/crates/core/src/runtime/remote/mod.rs b/crates/core/src/runtime/remote/mod.rs deleted file mode 100644 index b098a06..0000000 --- a/crates/core/src/runtime/remote/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -use async_trait::async_trait; -use tucana::{aquila::ExecutionRequest, shared::Value}; - -use crate::runtime::error::RuntimeError; - -#[async_trait] -pub trait RemoteRuntime { - async fn execute_remote( - &self, - remote_name: String, - request: ExecutionRequest, - ) -> Result; -} diff --git a/crates/core/src/value.rs b/crates/core/src/value.rs deleted file mode 100644 index 81ddf26..0000000 --- a/crates/core/src/value.rs +++ /dev/null @@ -1,55 +0,0 @@ -use tucana::shared::{NumberValue, Value, number_value, value::Kind}; - -pub fn number_value_from_f64(n: f64) -> NumberValue { - NumberValue { - number: Some(number_value::Number::Float(n)), - } -} - -pub fn number_value_from_i64(n: i64) -> NumberValue { - NumberValue { - number: Some(number_value::Number::Integer(n)), - } -} - -pub fn value_from_f64(n: f64) -> Value { - Value { - kind: Some(Kind::NumberValue(number_value_from_f64(n))), - } -} - -pub fn value_from_i64(n: i64) -> Value { - Value { - kind: Some(Kind::NumberValue(number_value_from_i64(n))), - } -} - -pub fn number_to_f64(n: &NumberValue) -> Option { - match n.number { - Some(number_value::Number::Integer(i)) => Some(i as f64), - Some(number_value::Number::Float(f)) => Some(f), - None => None, - } -} - -pub fn number_to_i64_lossy(n: &NumberValue) -> Option { - match n.number { - Some(number_value::Number::Integer(i)) => Some(i), - Some(number_value::Number::Float(f)) => { - if f.is_finite() { - Some(f as i64) - } else { - None - } - } - None => None, - } -} - -pub fn number_to_string(n: &NumberValue) -> String { - match n.number { - Some(number_value::Number::Integer(i)) => i.to_string(), - Some(number_value::Number::Float(f)) => f.to_string(), - None => "null".to_string(), - } -} From 4648b556167aa839b393ba90397a737d1b4f3c05 Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 21 Apr 2026 17:53:07 +0200 Subject: [PATCH 11/28] feat: added newest function handler impl --- crates/taurus-core/src/handler/argument.rs | 133 +++++++++++++++++++++ crates/taurus-core/src/handler/macros.rs | 56 +++++++++ crates/taurus-core/src/handler/mod.rs | 8 ++ crates/taurus-core/src/handler/registry.rs | 124 +++++++++++++++++++ 4 files changed, 321 insertions(+) create mode 100644 crates/taurus-core/src/handler/argument.rs create mode 100644 crates/taurus-core/src/handler/macros.rs create mode 100644 crates/taurus-core/src/handler/mod.rs create mode 100644 crates/taurus-core/src/handler/registry.rs diff --git a/crates/taurus-core/src/handler/argument.rs b/crates/taurus-core/src/handler/argument.rs new file mode 100644 index 0000000..139e3cb --- /dev/null +++ b/crates/taurus-core/src/handler/argument.rs @@ -0,0 +1,133 @@ +//! Handler argument representation and typed extraction contracts. + +use crate::types::errors::runtime_error::RuntimeError; +use crate::types::signal::Signal; +use std::convert::Infallible; +use tucana::shared::value::Kind; +use tucana::shared::{ListValue, NumberValue, Struct, Value}; + +use crate::value::{number_to_f64, number_to_i64_lossy}; +#[derive(Clone, Debug)] +pub enum Argument { + /// Eager value that can be consumed immediately by a handler. + Eval(Value), + /// Deferred node execution handle, evaluated by calling `run(node_id)`. + Thunk(i64), +} + +#[derive(Clone, Copy, Debug)] +pub enum ParameterNode { + /// Argument must be resolved before the handler is called. + Eager, + /// Argument is passed as a thunk and may be executed by the handler. + Lazy, +} + +/// Conversion interface used by `args!` to parse typed handler inputs. +pub trait TryFromArgument: Sized { + fn try_from_argument(a: &Argument) -> Result; +} + +fn type_err(msg: &str, a: &Argument) -> Signal { + Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + format!("{} but it was the arugment: {:?}", msg, a), + )) +} + +impl TryFromArgument for Value { + fn try_from_argument(a: &Argument) -> Result { + match a { + Argument::Eval(v) => Ok(v.clone()), + _ => Err(type_err("Expected evaluated value but got lazy thunk", a)), + } + } +} + +impl TryFromArgument for NumberValue { + fn try_from_argument(a: &Argument) -> Result { + match a { + Argument::Eval(Value { + kind: Some(Kind::NumberValue(n)), + }) => Ok(*n), + _ => Err(type_err("Expected number", a)), + } + } +} + +impl TryFromArgument for i64 { + fn try_from_argument(a: &Argument) -> Result { + match a { + Argument::Eval(Value { + kind: Some(Kind::NumberValue(n)), + }) => number_to_i64_lossy(n).ok_or_else(|| type_err("Expected number", a)), + _ => Err(type_err("Expected number", a)), + } + } +} + +impl TryFromArgument for f64 { + fn try_from_argument(a: &Argument) -> Result { + match a { + Argument::Eval(Value { + kind: Some(Kind::NumberValue(n)), + }) => number_to_f64(n).ok_or_else(|| type_err("Expected number", a)), + _ => Err(type_err("Expected number", a)), + } + } +} + +impl TryFromArgument for bool { + fn try_from_argument(a: &Argument) -> Result { + match a { + Argument::Eval(Value { + kind: Some(Kind::BoolValue(b)), + }) => Ok(*b), + _ => Err(type_err("Expected boolean", a)), + } + } +} + +impl TryFromArgument for String { + fn try_from_argument(a: &Argument) -> Result { + match a { + Argument::Eval(Value { + kind: Some(Kind::StringValue(s)), + }) => Ok(s.clone()), + _ => Err(type_err("Expected string", a)), + } + } +} + +impl TryFromArgument for Struct { + fn try_from_argument(a: &Argument) -> Result { + match a { + Argument::Eval(Value { + kind: Some(Kind::StructValue(s)), + }) => Ok(s.clone()), + _ => Err(type_err("Expected struct", a)), + } + } +} + +impl TryFromArgument for ListValue { + fn try_from_argument(a: &Argument) -> Result { + match a { + Argument::Eval(Value { + kind: Some(Kind::ListValue(list)), + }) => Ok(list.clone()), + _ => Err(Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + format!("Expected array (ListValue) but it was: {:?}", a), + ))), + } + } +} + +impl From for RuntimeError { + fn from(never: Infallible) -> Self { + match never {} + } +} diff --git a/crates/taurus-core/src/handler/macros.rs b/crates/taurus-core/src/handler/macros.rs new file mode 100644 index 0000000..c897fdf --- /dev/null +++ b/crates/taurus-core/src/handler/macros.rs @@ -0,0 +1,56 @@ +//! Handler argument parsing macros. + +/// Pulls typed parameters from a slice of `Argument` using your `TryFromArgument` +/// impls. Fails early with your `Signal::Failure(RuntimeError::new("T-RT-000000", ...))`. +macro_rules! args { + ($args_ident:ident => $( $name:ident : $ty:ty ),+ $(,)?) => { + // Arity check + let __expected: usize = 0usize $(+ { let _ = ::core::any::type_name::<$ty>(); 1usize })*; + if $args_ident.len() != __expected { + return $crate::types::signal::Signal::Failure( + $crate::types::errors::runtime_error::RuntimeError::new("T-RT-000000", + "InvalidArgumentRuntimeError", + format!("Expected {__expected} args but received {}", $args_ident.len()), + ) + ); + } + + // Typed extraction + let mut __i: usize = 0; + $( + let $name: $ty = match < + $ty as $crate::handler::argument::TryFromArgument + >::try_from_argument(& $args_ident[__i]) { + Ok(v) => v, + Err(sig) => { + log::debug!( + "Failed to parse argument '{}' (index {}, type {})", + stringify!($name), + __i, + ::core::any::type_name::<$ty>(), + ); + return sig; + } + }; + __i += 1; + )+ + }; +} + +/// Asserts there are no arguments. +macro_rules! no_args { + ($args_ident:ident) => { + if !$args_ident.is_empty() { + return $crate::types::signal::Signal::Failure( + $crate::types::errors::runtime_error::RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + format!("Expected 0 args but received {}", $args_ident.len()), + ), + ); + } + }; +} + +pub(crate) use args; +pub(crate) use no_args; diff --git a/crates/taurus-core/src/handler/mod.rs b/crates/taurus-core/src/handler/mod.rs new file mode 100644 index 0000000..f1a9bf5 --- /dev/null +++ b/crates/taurus-core/src/handler/mod.rs @@ -0,0 +1,8 @@ +//! Runtime handler-facing infrastructure. +//! +//! This module contains the argument model, extraction macros, and function +//! registry used to invoke runtime handlers. + +pub mod argument; +pub mod macros; +pub mod registry; diff --git a/crates/taurus-core/src/handler/registry.rs b/crates/taurus-core/src/handler/registry.rs new file mode 100644 index 0000000..388b566 --- /dev/null +++ b/crates/taurus-core/src/handler/registry.rs @@ -0,0 +1,124 @@ +//! Runtime handler registry and callable function signatures. + +use crate::handler::argument::{Argument, ParameterNode}; +use crate::runtime::execution::value_store::ValueStore; +use crate::runtime::functions::ALL_FUNCTION_SETS; +use crate::types::signal::Signal; +use std::collections::HashMap; + +/// Handler function type. +/// - For eager params, the executor will already convert them to Argument::Eval(Value). +/// - For lazy params, the executor will pass Argument::Thunk(node_id). +/// - If a handler wants to execute a lazy arg, it calls run(node_id). +pub type HandlerFn = fn( + args: &[Argument], + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal; + +#[derive(Clone, Copy)] +pub enum ParamSpec { + /// All parameters are evaluated eagerly. + AllEager(u8), + /// Per-parameter evaluation mode. + Explicit(&'static [ParameterNode]), +} + +impl ParamSpec { + pub fn mode_at(self, index: usize) -> ParameterNode { + match self { + ParamSpec::AllEager(_) => ParameterNode::Eager, + ParamSpec::Explicit(modes) => modes.get(index).copied().unwrap_or(ParameterNode::Eager), + } + } +} + +#[derive(Clone, Copy)] +pub struct HandlerFunctionEntry { + /// Callable implementation. + pub handler: HandlerFn, + /// Evaluation strategy for the handler parameters. + pub param_spec: ParamSpec, +} + +impl HandlerFunctionEntry { + pub const fn eager(handler: HandlerFn, param_count: u8) -> Self { + Self { + handler, + param_spec: ParamSpec::AllEager(param_count), + } + } + + pub const fn modes(handler: HandlerFn, param_modes: &'static [ParameterNode]) -> Self { + Self { + handler, + param_spec: ParamSpec::Explicit(param_modes), + } + } + + pub fn param_mode(&self, index: usize) -> ParameterNode { + self.param_spec.mode_at(index) + } +} + +#[derive(Clone, Copy)] +pub struct FunctionRegistration { + pub id: &'static str, + pub entry: HandlerFunctionEntry, +} + +impl FunctionRegistration { + pub const fn eager(id: &'static str, handler: HandlerFn, param_count: u8) -> Self { + Self { + id, + entry: HandlerFunctionEntry::eager(handler, param_count), + } + } + + pub const fn modes( + id: &'static str, + handler: HandlerFn, + param_modes: &'static [ParameterNode], + ) -> Self { + Self { + id, + entry: HandlerFunctionEntry::modes(handler, param_modes), + } + } +} + +/// Holds all registered handlers. +pub struct FunctionStore { + functions: HashMap<&'static str, HandlerFunctionEntry>, +} + +impl Default for FunctionStore { + fn default() -> Self { + let mut store = Self::new(); + for set in ALL_FUNCTION_SETS { + store.populate(set); + } + store + } +} + +impl FunctionStore { + /// Create a new, empty store. + pub fn new() -> Self { + FunctionStore { + functions: HashMap::new(), + } + } + + /// Look up a handler by its ID. + pub fn get(&self, id: &str) -> Option<&HandlerFunctionEntry> { + self.functions.get(id) + } + + /// Register a group of handlers. + pub fn populate(&mut self, regs: &[FunctionRegistration]) { + for reg in regs { + self.functions.insert(reg.id, reg.entry); + } + } +} From 46bd289e06fb2af09fbf0afbc40517fed9c13625 Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 21 Apr 2026 17:53:29 +0200 Subject: [PATCH 12/28] feat: added pre-exec step as flow compiler --- .../src/runtime/engine/compiler.rs | 161 +++++ .../taurus-core/src/runtime/engine/emitter.rs | 30 + .../src/runtime/engine/executor.rs | 639 ++++++++++++++++++ .../taurus-core/src/runtime/engine/model.rs | 47 ++ 4 files changed, 877 insertions(+) create mode 100644 crates/taurus-core/src/runtime/engine/compiler.rs create mode 100644 crates/taurus-core/src/runtime/engine/emitter.rs create mode 100644 crates/taurus-core/src/runtime/engine/executor.rs create mode 100644 crates/taurus-core/src/runtime/engine/model.rs diff --git a/crates/taurus-core/src/runtime/engine/compiler.rs b/crates/taurus-core/src/runtime/engine/compiler.rs new file mode 100644 index 0000000..6fee00b --- /dev/null +++ b/crates/taurus-core/src/runtime/engine/compiler.rs @@ -0,0 +1,161 @@ +//! Flow compiler for runtime execution plans. + +use std::collections::HashMap; + +use tucana::shared::{NodeFunction, node_value}; + +use crate::{ + runtime::engine::model::{ + CompiledArg, CompiledFlow, CompiledNode, CompiledParameter, NodeExecutionTarget, + }, + types::errors::runtime_error::RuntimeError, +}; + +#[derive(Debug)] +pub enum CompileError { + DuplicateNodeId { + node_id: i64, + }, + StartNodeMissing { + node_id: i64, + }, + NextNodeMissing { + node_id: i64, + next_node_id: i64, + }, + ParameterValueMissing { + node_id: i64, + parameter_index: usize, + }, +} + +impl CompileError { + pub fn as_runtime_error(&self) -> RuntimeError { + match self { + CompileError::DuplicateNodeId { node_id } => RuntimeError::new( + "T-RT-000000", + "FlowCompileError", + format!("Duplicate node id in flow: {}", node_id), + ), + CompileError::StartNodeMissing { node_id } => RuntimeError::new( + "T-RT-000000", + "FlowCompileError", + format!("Start node not found in flow: {}", node_id), + ), + CompileError::NextNodeMissing { + node_id, + next_node_id, + } => RuntimeError::new( + "T-RT-000000", + "FlowCompileError", + format!( + "Node {} points to missing next node {}", + node_id, next_node_id + ), + ), + CompileError::ParameterValueMissing { + node_id, + parameter_index, + } => RuntimeError::new( + "T-RT-000000", + "FlowCompileError", + format!( + "Node {} parameter {} does not contain a value", + node_id, parameter_index + ), + ), + } + } +} + +pub fn compile_flow( + start_node_id: i64, + nodes: Vec, +) -> Result { + let mut node_idx_by_id = HashMap::with_capacity(nodes.len()); + for (idx, node) in nodes.iter().enumerate() { + if node_idx_by_id.insert(node.database_id, idx).is_some() { + return Err(CompileError::DuplicateNodeId { + node_id: node.database_id, + }); + } + } + + let start_idx = match node_idx_by_id.get(&start_node_id).copied() { + Some(idx) => idx, + None => { + return Err(CompileError::StartNodeMissing { + node_id: start_node_id, + }); + } + }; + + let mut compiled_nodes = Vec::with_capacity(nodes.len()); + for node in nodes { + let next_idx = match node.next_node_id { + Some(next_id) => match node_idx_by_id.get(&next_id).copied() { + Some(idx) => Some(idx), + None => { + return Err(CompileError::NextNodeMissing { + node_id: node.database_id, + next_node_id: next_id, + }); + } + }, + None => None, + }; + + let execution_target = execution_target_for(&node); + + let mut parameters = Vec::with_capacity(node.parameters.len()); + for (parameter_index, parameter) in node.parameters.iter().enumerate() { + let Some(node_value) = parameter.value.as_ref() else { + return Err(CompileError::ParameterValueMissing { + node_id: node.database_id, + parameter_index, + }); + }; + let Some(value) = node_value.value.as_ref() else { + return Err(CompileError::ParameterValueMissing { + node_id: node.database_id, + parameter_index, + }); + }; + + let arg = match value { + node_value::Value::LiteralValue(v) => CompiledArg::Literal(v.clone()), + node_value::Value::ReferenceValue(r) => CompiledArg::Reference(r.clone()), + node_value::Value::NodeFunctionId(id) => CompiledArg::DeferredNode(*id), + }; + + parameters.push(CompiledParameter { + runtime_parameter_id: parameter.runtime_parameter_id.clone(), + arg, + }); + } + + compiled_nodes.push(CompiledNode { + id: node.database_id, + handler_id: node.runtime_function_id, + execution_target, + next_idx, + parameters, + }); + } + + Ok(CompiledFlow { + start_idx, + nodes: compiled_nodes, + node_idx_by_id, + }) +} + +fn execution_target_for(node: &NodeFunction) -> NodeExecutionTarget { + if node.definition_source.is_empty() || node.definition_source == "taurus" { + NodeExecutionTarget::Local + } else { + NodeExecutionTarget::Remote { + service: node.definition_source.clone(), + } + } +} diff --git a/crates/taurus-core/src/runtime/engine/emitter.rs b/crates/taurus-core/src/runtime/engine/emitter.rs new file mode 100644 index 0000000..b2dee74 --- /dev/null +++ b/crates/taurus-core/src/runtime/engine/emitter.rs @@ -0,0 +1,30 @@ +//! Respond emitter abstraction used by the engine. + +use tucana::shared::Value; + +/// Execution lifecycle event emitted by the runtime engine. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EmitType { + /// Top-level flow execution has started. + StartingExec, + /// An intermediate `Signal::Respond` was emitted during execution. + OngoingExec, + /// Flow execution reached a non-failure terminal state. + FinishedExec, + /// Flow execution ended with a runtime failure. + FailedExec, +} + +/// Callback interface for streaming execution lifecycle events. +pub trait RespondEmitter { + fn emit(&self, emit_type: EmitType, value: Value); +} + +impl RespondEmitter for F +where + F: Fn(EmitType, Value) + ?Sized, +{ + fn emit(&self, emit_type: EmitType, value: Value) { + self(emit_type, value); + } +} diff --git a/crates/taurus-core/src/runtime/engine/executor.rs b/crates/taurus-core/src/runtime/engine/executor.rs new file mode 100644 index 0000000..b1d305a --- /dev/null +++ b/crates/taurus-core/src/runtime/engine/executor.rs @@ -0,0 +1,639 @@ +//! Runtime engine execution loop for compiled flow plans. + +use std::cell::RefCell; +use std::collections::HashMap; + +use futures_lite::future::block_on; +use tucana::aquila::ExecutionRequest; +use tucana::shared::reference_value::Target; +use tucana::shared::value::Kind; +use tucana::shared::{Struct, Value}; +use uuid::Uuid; + +use crate::handler::argument::{Argument, ParameterNode}; +use crate::handler::registry::{FunctionStore, HandlerFunctionEntry}; +use crate::runtime::engine::emitter::{EmitType, RespondEmitter}; +use crate::runtime::engine::model::{CompiledArg, CompiledFlow, CompiledNode, NodeExecutionTarget}; +use crate::runtime::execution::trace::{ + ArgKind, ArgTrace, EdgeKind, Outcome, ReferenceKind, TraceRun, +}; +use crate::runtime::execution::tracer::{ExecutionTracer, Tracer}; +use crate::runtime::execution::value_store::{ValueStore, ValueStoreResult}; +use crate::runtime::remote::{RemoteExecution, RemoteRuntime}; +use crate::types::errors::runtime_error::RuntimeError; +use crate::types::signal::Signal; + +pub fn execute_compiled( + flow: &CompiledFlow, + handlers: &FunctionStore, + value_store: &mut ValueStore, + remote: Option<&dyn RemoteRuntime>, + respond_emitter: Option<&dyn RespondEmitter>, + with_trace: bool, +) -> (Signal, Option) { + // Keep trace allocation fully optional so the hot path stays lean when tracing is disabled. + let tracer = with_trace.then(RefCell::default); + let executor = EngineExecutor { + flow, + handlers, + remote, + respond_emitter, + tracer: tracer.as_ref(), + }; + + let result = executor.execute_from_index(flow.start_idx, value_store); + let trace = tracer.and_then(|collector| collector.into_inner().take_run()); + (result.signal, trace) +} + +/// Result of executing one linear node chain (entry node + `next` links). +/// `root_frame` is used to connect this chain into the caller frame in trace mode. +struct ExecutionResult { + signal: Signal, + root_frame: Option, +} + +/// Result of executing exactly one compiled node. +struct NodeExecutionResult { + signal: Signal, + frame_id: Option, +} + +struct EngineExecutor<'a> { + flow: &'a CompiledFlow, + handlers: &'a FunctionStore, + remote: Option<&'a dyn RemoteRuntime>, + respond_emitter: Option<&'a dyn RespondEmitter>, + tracer: Option<&'a RefCell>, +} + +impl<'a> EngineExecutor<'a> { + fn execute_from_index( + &self, + start_idx: usize, + value_store: &mut ValueStore, + ) -> ExecutionResult { + // A compiled flow is executed as a linear walk through `next_idx` pointers. + let mut current_idx = start_idx; + let mut call_root_frame = None; + let mut previous_frame = None; + + loop { + let node_id = self.flow.nodes[current_idx].id; + let next_idx = self.flow.nodes[current_idx].next_idx; + let result = self.execute_single_node(current_idx, value_store); + + if call_root_frame.is_none() { + call_root_frame = result.frame_id; + } + if let (Some(prev), Some(current)) = (previous_frame, result.frame_id) { + self.trace_link_child(prev, current, EdgeKind::Next); + } + if let Some(frame) = result.frame_id { + previous_frame = Some(frame); + } + + match result.signal { + // Only `Success` keeps walking through the current linear chain. + Signal::Success(_) => match next_idx { + Some(next) => current_idx = next, + None => { + return ExecutionResult { + signal: result.signal, + root_frame: call_root_frame, + }; + } + }, + Signal::Respond(value) => { + // `Respond` is an observable side effect; execution may still continue. + if let Some(emitter) = self.respond_emitter { + emitter.emit(EmitType::OngoingExec, value.clone()); + } + + value_store.insert_success(node_id, value.clone()); + match next_idx { + Some(next) => current_idx = next, + None => { + return ExecutionResult { + signal: Signal::Success(value), + root_frame: call_root_frame, + }; + } + } + } + // `Return`/`Stop`/`Failure` unwind immediately to the direct caller. + other => { + return ExecutionResult { + signal: other, + root_frame: call_root_frame, + }; + } + } + } + } + + fn execute_from_node_id(&self, node_id: i64, value_store: &mut ValueStore) -> ExecutionResult { + // Used by thunk execution (callbacks, branch blocks, eager parameter nodes). + match self.flow.node_idx_by_id.get(&node_id).copied() { + Some(idx) => self.execute_from_index(idx, value_store), + None => ExecutionResult { + signal: Signal::Failure(RuntimeError::new( + "T-ENG-000001", + "NodeNotFound", + format!("Node {} not found", node_id), + )), + root_frame: None, + }, + } + } + + fn execute_single_node( + &self, + node_idx: usize, + value_store: &mut ValueStore, + ) -> NodeExecutionResult { + let node = &self.flow.nodes[node_idx]; + // InputType references resolve against the currently running node. + value_store.set_current_node_id(node.id); + + let frame_id = self.trace_enter(node); + let signal = match &node.execution_target { + NodeExecutionTarget::Local => self.execute_local_node(node, value_store, frame_id), + NodeExecutionTarget::Remote { service } => { + self.execute_remote_node(node, service, value_store, frame_id) + } + }; + self.trace_exit(frame_id, &signal); + + NodeExecutionResult { signal, frame_id } + } + + fn execute_local_node( + &self, + node: &CompiledNode, + value_store: &mut ValueStore, + frame_id: Option, + ) -> Signal { + let entry = match self.handlers.get(node.handler_id.as_str()) { + Some(entry) => entry, + None => { + return Signal::Failure(RuntimeError::new( + "T-ENG-000002", + "FunctionNotFound", + format!("Function {} not found", node.handler_id), + )); + } + }; + + let mut args = match self.build_args(node, value_store, frame_id) { + Ok(args) => args, + Err(err) => { + value_store.insert_error(node.id, err.clone()); + return Signal::Failure(err); + } + }; + + if let Some(signal) = self.force_eager_args(entry, &mut args, value_store, frame_id) { + return self.commit_result(node.id, signal, value_store); + } + + // Handler-owned runtime calls (for lazy args / callbacks) re-enter the same executor. + let mut run = |node_id: i64, store: &mut ValueStore| { + self.trace_mark_thunk_executed_by_node(frame_id, node_id); + let label = store.pop_runtime_trace_label(); + let child_result = self.execute_from_node_id(node_id, store); + if let (Some(parent), Some(child)) = (frame_id, child_result.root_frame) { + self.trace_link_child(parent, child, EdgeKind::RuntimeCall { label }); + } + child_result.signal + }; + + let signal = (entry.handler)(&args, value_store, &mut run); + self.commit_result(node.id, signal, value_store) + } + + fn execute_remote_node( + &self, + node: &CompiledNode, + service: &str, + value_store: &mut ValueStore, + frame_id: Option, + ) -> Signal { + let remote_runtime = match self.remote { + Some(remote) => remote, + None => { + return Signal::Failure(RuntimeError::new( + "T-ENG-000003", + "RemoteRuntimeNotConfigured", + "Remote runtime not configured", + )); + } + }; + + let mut args = match self.build_args(node, value_store, frame_id) { + Ok(args) => args, + Err(err) => { + value_store.insert_error(node.id, err.clone()); + return Signal::Failure(err); + } + }; + + let values = match self.resolve_remote_args(&mut args, value_store, frame_id) { + Ok(values) => values, + Err(signal) => return self.commit_result(node.id, signal, value_store), + }; + + let request = match self.build_remote_request(node, values) { + Ok(request) => request, + Err(err) => { + value_store.insert_error(node.id, err.clone()); + return Signal::Failure(err); + } + }; + + let signal = match block_on(remote_runtime.execute_remote(RemoteExecution { + target_service: service.to_string(), + request, + })) { + Ok(value) => Signal::Success(value), + Err(err) => Signal::Failure(err), + }; + + self.commit_result(node.id, signal, value_store) + } + + fn build_args( + &self, + node: &CompiledNode, + value_store: &mut ValueStore, + frame_id: Option, + ) -> Result, RuntimeError> { + let mut args = Vec::with_capacity(node.parameters.len()); + + for (index, parameter) in node.parameters.iter().enumerate() { + match ¶meter.arg { + CompiledArg::Literal(value) => { + self.trace_record_arg( + frame_id, + ArgTrace { + index, + kind: ArgKind::Literal, + preview: preview_value(value), + }, + ); + args.push(Argument::Eval(value.clone())); + } + CompiledArg::Reference(reference) => match value_store.get(reference.clone()) { + ValueStoreResult::Success(value) => { + self.trace_record_arg( + frame_id, + ArgTrace { + index, + kind: ArgKind::Reference { + reference: match &reference.target { + Some(Target::FlowInput(_)) => ReferenceKind::FlowInput, + Some(Target::NodeId(id)) => { + ReferenceKind::Result { node_id: *id } + } + Some(Target::InputType(input_type)) => { + ReferenceKind::InputType { + node_id: input_type.node_id, + input_index: input_type.input_index, + parameter_index: input_type.parameter_index, + } + } + None => ReferenceKind::Empty, + }, + hit: true, + }, + preview: format!( + "store.get({}) -> {}", + preview_reference(reference), + preview_value(&value) + ), + }, + ); + args.push(Argument::Eval(value)); + } + ValueStoreResult::Error(err) => { + self.trace_record_arg( + frame_id, + ArgTrace { + index, + kind: ArgKind::Reference { + reference: match &reference.target { + Some(Target::FlowInput(_)) => ReferenceKind::FlowInput, + Some(Target::NodeId(id)) => { + ReferenceKind::Result { node_id: *id } + } + Some(Target::InputType(input_type)) => { + ReferenceKind::InputType { + node_id: input_type.node_id, + input_index: input_type.input_index, + parameter_index: input_type.parameter_index, + } + } + None => ReferenceKind::Empty, + }, + hit: false, + }, + preview: format!( + "store.get({}) -> error({}:{})", + preview_reference(reference), + err.code, + err.category + ), + }, + ); + return Err(err); + } + ValueStoreResult::NotFound => { + self.trace_record_arg( + frame_id, + ArgTrace { + index, + kind: ArgKind::Reference { + reference: match &reference.target { + Some(Target::FlowInput(_)) => ReferenceKind::FlowInput, + Some(Target::NodeId(id)) => { + ReferenceKind::Result { node_id: *id } + } + Some(Target::InputType(input_type)) => { + ReferenceKind::InputType { + node_id: input_type.node_id, + input_index: input_type.input_index, + parameter_index: input_type.parameter_index, + } + } + None => ReferenceKind::Empty, + }, + hit: false, + }, + preview: format!( + "store.get({}) -> not-found", + preview_reference(reference) + ), + }, + ); + return Err(RuntimeError::new( + "T-ENG-000004", + "ReferenceValueNotFound", + "Reference not found in execution value store", + )); + } + }, + CompiledArg::DeferredNode(node_id) => { + self.trace_record_arg( + frame_id, + ArgTrace { + index, + kind: ArgKind::Thunk { + node_id: *node_id, + eager: false, + executed: false, + }, + preview: format!("thunk({})", node_id), + }, + ); + args.push(Argument::Thunk(*node_id)); + } + } + } + + Ok(args) + } + + fn force_eager_args( + &self, + entry: &HandlerFunctionEntry, + args: &mut [Argument], + value_store: &mut ValueStore, + frame_id: Option, + ) -> Option { + for (index, argument) in args.iter_mut().enumerate() { + let mode = entry.param_mode(index); + + if matches!(mode, ParameterNode::Eager) + && let Argument::Thunk(node_id) = *argument + { + self.trace_mark_thunk(frame_id, index, true, true); + let child = self.execute_from_node_id(node_id, value_store); + if let (Some(parent), Some(child_root)) = (frame_id, child.root_frame) { + self.trace_link_child( + parent, + child_root, + EdgeKind::EagerCall { arg_index: index }, + ); + } + match child.signal { + Signal::Success(value) => { + *argument = Argument::Eval(value); + } + // Return in an eager parameter block exits only this node invocation, + // so the caller continues with its own `next` node. + Signal::Return(value) => return Some(Signal::Success(value)), + other => return Some(other), + } + } + } + + None + } + + fn resolve_remote_args( + &self, + args: &mut [Argument], + value_store: &mut ValueStore, + frame_id: Option, + ) -> Result, Signal> { + let mut values = Vec::with_capacity(args.len()); + + for (index, argument) in args.iter_mut().enumerate() { + match argument { + Argument::Eval(value) => values.push(value.clone()), + Argument::Thunk(node_id) => { + // Remote execution always receives materialized values, never thunks. + self.trace_mark_thunk(frame_id, index, true, true); + let child = self.execute_from_node_id(*node_id, value_store); + if let (Some(parent), Some(child_root)) = (frame_id, child.root_frame) { + self.trace_link_child( + parent, + child_root, + EdgeKind::EagerCall { arg_index: index }, + ); + } + match child.signal { + Signal::Success(value) => { + *argument = Argument::Eval(value.clone()); + values.push(value); + } + // Same unwind rule as local eager params: return exits this call frame only. + Signal::Return(value) => return Err(Signal::Success(value)), + other => return Err(other), + } + } + } + } + + Ok(values) + } + + fn build_remote_request( + &self, + node: &CompiledNode, + values: Vec, + ) -> Result { + if node.parameters.len() != values.len() { + return Err(RuntimeError::new( + "T-ENG-000005", + "RemoteParameterMismatch", + "Remote parameter count mismatch", + )); + } + + let mut fields = HashMap::new(); + for (parameter, value) in node.parameters.iter().zip(values.into_iter()) { + fields.insert(parameter.runtime_parameter_id.clone(), value); + } + + Ok(ExecutionRequest { + execution_identifier: Uuid::new_v4().to_string(), + function_identifier: node.handler_id.clone(), + parameters: Some(Struct { fields }), + project_id: 0, + }) + } + + fn commit_result(&self, node_id: i64, signal: Signal, value_store: &mut ValueStore) -> Signal { + match signal { + Signal::Success(value) => { + value_store.insert_success(node_id, value.clone()); + Signal::Success(value) + } + Signal::Failure(err) => { + value_store.insert_error(node_id, err.clone()); + Signal::Failure(err) + } + // Control signals are transient and should not be cached as node outputs. + other => other, + } + } + + fn trace_enter(&self, node: &CompiledNode) -> Option { + self.tracer.map(|tracer| { + tracer + .borrow_mut() + .enter_node(node.id, node.handler_id.as_str()) + }) + } + + fn trace_exit(&self, frame_id: Option, signal: &Signal) { + let Some(frame_id) = frame_id else { + return; + }; + let Some(tracer) = self.tracer else { + return; + }; + + let outcome = match signal { + Signal::Success(value) => Outcome::Success { + value_preview: preview_value(value), + }, + Signal::Failure(error) => Outcome::Failure { + error_preview: format!("{}:{} {}", error.code, error.category, error.message), + }, + Signal::Return(value) => Outcome::Return { + value_preview: preview_value(value), + }, + Signal::Respond(value) => Outcome::Respond { + value_preview: preview_value(value), + }, + Signal::Stop => Outcome::Stop, + }; + tracer.borrow_mut().exit_node(frame_id, outcome); + } + + fn trace_record_arg(&self, frame_id: Option, arg: ArgTrace) { + if let (Some(frame_id), Some(tracer)) = (frame_id, self.tracer) { + tracer.borrow_mut().record_arg(frame_id, arg); + } + } + + fn trace_link_child(&self, parent: u64, child: u64, edge: EdgeKind) { + if let Some(tracer) = self.tracer { + tracer.borrow_mut().link_child(parent, child, edge); + } + } + + fn trace_mark_thunk( + &self, + frame_id: Option, + arg_index: usize, + eager: bool, + executed: bool, + ) { + if let (Some(frame_id), Some(tracer)) = (frame_id, self.tracer) { + tracer + .borrow_mut() + .mark_thunk(frame_id, arg_index, eager, executed); + } + } + + fn trace_mark_thunk_executed_by_node(&self, frame_id: Option, node_id: i64) { + if let (Some(frame_id), Some(tracer)) = (frame_id, self.tracer) { + tracer + .borrow_mut() + .mark_thunk_executed_by_node(frame_id, node_id); + } + } +} + +fn preview_value(value: &Value) -> String { + // Trace previews are deterministic and human-readable for debugging snapshots. + format_value_json(value) +} + +fn format_value_json(value: &Value) -> String { + match value.kind.as_ref() { + Some(Kind::NumberValue(v)) => crate::value::number_to_string(v), + Some(Kind::BoolValue(v)) => v.to_string(), + Some(Kind::StringValue(v)) => format!("{:?}", v), + Some(Kind::NullValue(_)) | None => "null".to_string(), + Some(Kind::ListValue(list)) => { + let mut parts = Vec::new(); + for item in &list.values { + parts.push(format_value_json(item)); + } + format!("[{}]", parts.join(", ")) + } + Some(Kind::StructValue(struct_value)) => { + let mut keys: Vec<_> = struct_value.fields.keys().collect(); + keys.sort(); + let mut parts = Vec::new(); + for key in &keys { + if let Some(value) = struct_value.fields.get(*key) { + parts.push(format!("{:?}: {}", key, format_value_json(value))); + } + } + format!("{{{}}}", parts.join(", ")) + } + } +} + +fn preview_reference(reference: &tucana::shared::ReferenceValue) -> String { + let target = match &reference.target { + Some(Target::FlowInput(_)) => "flow_input".to_string(), + Some(Target::NodeId(id)) => format!("node({})", id), + Some(Target::InputType(input_type)) => format!( + "input(node={},param={},input={})", + input_type.node_id, input_type.parameter_index, input_type.input_index + ), + None => "empty".to_string(), + }; + + if reference.paths.is_empty() { + target + } else { + format!("{}+paths({})", target, reference.paths.len()) + } +} diff --git a/crates/taurus-core/src/runtime/engine/model.rs b/crates/taurus-core/src/runtime/engine/model.rs new file mode 100644 index 0000000..50ff1e4 --- /dev/null +++ b/crates/taurus-core/src/runtime/engine/model.rs @@ -0,0 +1,47 @@ +//! Compiled runtime plan model. +//! +//! A flow is compiled into index-addressable nodes to avoid repeated map lookups +//! in the hot execution loop. + +use std::collections::HashMap; + +use tucana::shared::{ReferenceValue, Value}; + +#[derive(Debug, Clone)] +pub enum NodeExecutionTarget { + Local, + Remote { service: String }, +} + +/// Argument expression compiled from proto node parameter values. +#[derive(Debug, Clone)] +pub enum CompiledArg { + Literal(Value), + Reference(ReferenceValue), + DeferredNode(i64), +} + +/// Compiled parameter binding. +#[derive(Debug, Clone)] +pub struct CompiledParameter { + pub runtime_parameter_id: String, + pub arg: CompiledArg, +} + +/// Compiled node representation. +#[derive(Debug, Clone)] +pub struct CompiledNode { + pub id: i64, + pub handler_id: String, + pub execution_target: NodeExecutionTarget, + pub next_idx: Option, + pub parameters: Vec, +} + +/// Compiled flow plan. +#[derive(Debug, Clone)] +pub struct CompiledFlow { + pub start_idx: usize, + pub nodes: Vec, + pub node_idx_by_id: HashMap, +} From f5c852681f28505d0bc4929acaa16bf81b0e964b Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 21 Apr 2026 17:53:38 +0200 Subject: [PATCH 13/28] feat: new exec loop --- .../taurus-core/src/runtime/execution/mod.rs | 11 ++ .../src/runtime/execution/registry.rs | 19 ++ .../src/runtime/execution/render.rs | 172 ++++++++++++++++++ .../src/runtime/execution/store.rs | 33 ++++ .../src/runtime/execution/trace.rs | 85 +++++++++ .../src/runtime/execution/tracer.rs | 157 ++++++++++++++++ .../src/runtime/execution/value_store.rs | 145 +++++++++++++++ 7 files changed, 622 insertions(+) create mode 100644 crates/taurus-core/src/runtime/execution/mod.rs create mode 100644 crates/taurus-core/src/runtime/execution/registry.rs create mode 100644 crates/taurus-core/src/runtime/execution/render.rs create mode 100644 crates/taurus-core/src/runtime/execution/store.rs create mode 100644 crates/taurus-core/src/runtime/execution/trace.rs create mode 100644 crates/taurus-core/src/runtime/execution/tracer.rs create mode 100644 crates/taurus-core/src/runtime/execution/value_store.rs diff --git a/crates/taurus-core/src/runtime/execution/mod.rs b/crates/taurus-core/src/runtime/execution/mod.rs new file mode 100644 index 0000000..753a755 --- /dev/null +++ b/crates/taurus-core/src/runtime/execution/mod.rs @@ -0,0 +1,11 @@ +//! Runtime execution internals. +//! +//! These types are owned by the execution engine lifecycle and are not part of +//! the transport-level flow contracts. + +pub mod registry; +pub mod render; +pub mod store; +pub mod trace; +pub mod tracer; +pub mod value_store; diff --git a/crates/taurus-core/src/runtime/execution/registry.rs b/crates/taurus-core/src/runtime/execution/registry.rs new file mode 100644 index 0000000..213669d --- /dev/null +++ b/crates/taurus-core/src/runtime/execution/registry.rs @@ -0,0 +1,19 @@ +//! Registry metadata types for runtime handler discovery. + +use std::collections::HashMap; + +use crate::types::execution::signature::HandlerSignature; + +/// Static registry entry metadata. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HandlerRegistration { + pub handler_id: String, + pub signature: HandlerSignature, + pub description: Option, +} + +/// Read-only handler metadata registry. +#[derive(Debug, Clone, Default)] +pub struct HandlerRegistry { + pub handlers: HashMap, +} diff --git a/crates/taurus-core/src/runtime/execution/render.rs b/crates/taurus-core/src/runtime/execution/render.rs new file mode 100644 index 0000000..4f116b5 --- /dev/null +++ b/crates/taurus-core/src/runtime/execution/render.rs @@ -0,0 +1,172 @@ +//! Human-readable trace renderer. + +use std::collections::HashMap; + +use crate::runtime::execution::trace::{ArgKind, EdgeKind, ExecFrame, Outcome, TraceRun}; + +fn frame_micros(frame: &ExecFrame) -> Option { + frame + .end + .map(|end| end.duration_since(frame.start).as_micros()) +} + +/// Render a trace as plain-text tree output. +pub fn render_trace(run: &TraceRun) -> String { + let mut by_id: HashMap = HashMap::new(); + for frame in &run.frames { + by_id.insert(frame.frame_id, frame); + } + + let mut output = String::new(); + if let Some(total_us) = total_duration_us(&run.frames) { + output.push_str(&format!("Total: {}us\n", total_us)); + } + render_frame(run.root, &by_id, "", true, &mut output); + if let Some(total_us) = total_duration_us(&run.frames) { + output.push_str(&format!("Summary: total_time={}us\n", total_us)); + } + output +} + +fn render_frame( + id: u64, + by_id: &HashMap, + prefix: &str, + is_last: bool, + output: &mut String, +) { + let frame = by_id[&id]; + + let branch = if prefix.is_empty() { + "" + } else if is_last { + "\\- " + } else { + "+- " + }; + + let duration = frame_micros(frame) + .map(|us| format!(" ({}us)", us)) + .unwrap_or_default(); + + output.push_str(&format!( + "{prefix}{branch}#{frame_id} node={node_id} fn={function}{duration}\n", + prefix = prefix, + branch = branch, + frame_id = frame.frame_id, + node_id = frame.node_id, + function = frame.function_name, + duration = duration + )); + + for arg in &frame.args { + let continuation = if prefix.is_empty() || is_last { + " " + } else { + "| " + }; + + let kind = match &arg.kind { + ArgKind::Literal => "lit".to_string(), + ArgKind::Reference { reference, hit } => { + let suffix = if *hit { "hit" } else { "miss" }; + format!("ref({:?}, {})", reference, suffix) + } + ArgKind::Thunk { + node_id, + eager, + executed, + } => { + let mode = if *eager { "eager" } else { "lazy" }; + let exec = if *executed { "executed" } else { "deferred" }; + format!("thunk(node={}, {}, {})", node_id, mode, exec) + } + }; + + output.push_str(&format!( + "{prefix}{continuation} arg[{index}] {kind:<24} {preview}\n", + prefix = prefix, + continuation = continuation, + index = arg.index, + kind = kind, + preview = arg.preview + )); + } + + let outcome = match &frame.outcome { + Some(Outcome::Success { value_preview }) => format!("[SUCCESS] {}", value_preview), + Some(Outcome::Failure { error_preview }) => format!("[FAILURE] {}", error_preview), + Some(Outcome::Return { value_preview }) => format!("[RETURN] {}", value_preview), + Some(Outcome::Respond { value_preview }) => format!("[RESPOND] {}", value_preview), + Some(Outcome::Stop) => "[STOP]".to_string(), + None => "...".to_string(), + }; + + let continuation = if prefix.is_empty() || is_last { + " " + } else { + "| " + }; + output.push_str(&format!( + "{prefix}{continuation} => {outcome}\n", + prefix = prefix, + continuation = continuation, + outcome = outcome + )); + + if frame.children.is_empty() { + return; + } + + let mut runtime_idx = 0usize; + for (idx, (edge, child_id)) in frame.children.iter().enumerate() { + let edge_last = idx + 1 == frame.children.len(); + let edge_branch = if edge_last { "\\- " } else { "+- " }; + + let edge_text = match edge { + EdgeKind::Next => "NEXT".to_string(), + EdgeKind::EagerCall { arg_index } => format!("eager(arg#{})", arg_index), + EdgeKind::RuntimeCall { label } => { + runtime_idx += 1; + match label { + Some(label_text) => format!("runtime(call #{}) {}", runtime_idx, label_text), + None => format!("runtime(call #{})", runtime_idx), + } + } + }; + + output.push_str(&format!( + "{prefix}{edge_branch}{edge_text}\n", + prefix = prefix, + edge_branch = edge_branch, + edge_text = edge_text + )); + + let child_prefix = format!("{}{}", prefix, if edge_last { " " } else { "| " }); + render_frame(*child_id, by_id, &child_prefix, true, output); + } +} + +fn total_duration_us(frames: &[ExecFrame]) -> Option { + let mut start = None; + let mut end = None; + + for frame in frames { + start = Some(match start { + Some(current) if frame.start > current => current, + Some(_) | None => frame.start, + }); + + if let Some(frame_end) = frame.end { + end = Some(match end { + Some(current) if frame_end < current => current, + Some(_) | None => frame_end, + }); + } + } + + match (start, end) { + (Some(start_time), Some(end_time)) => Some(end_time.duration_since(start_time).as_micros()), + _ => None, + } +} diff --git a/crates/taurus-core/src/runtime/execution/store.rs b/crates/taurus-core/src/runtime/execution/store.rs new file mode 100644 index 0000000..371647d --- /dev/null +++ b/crates/taurus-core/src/runtime/execution/store.rs @@ -0,0 +1,33 @@ +//! Mutable execution state for a single flow run. + +use std::collections::HashMap; + +use tucana::shared::Value; + +use crate::types::errors::runtime_error::RuntimeError; +use crate::types::execution::ids::{FrameId, NodeId}; + +/// Runtime outcome persisted per node. +#[derive(Debug, Clone)] +pub enum NodeOutcome { + Success(Value), + Failure(RuntimeError), +} + +/// Input slot key for runtime-provided temporary inputs (for iterators/predicates). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct InputSlotKey { + pub node_id: NodeId, + pub parameter_index: i32, + pub input_index: i32, +} + +/// Store that captures mutable runtime execution state. +#[derive(Debug, Clone, Default)] +pub struct ExecutionStore { + pub node_outcomes: HashMap, + pub input_slots: HashMap, + pub flow_input: Option, + pub current_node: Option, + pub frame_stack: Vec, +} diff --git a/crates/taurus-core/src/runtime/execution/trace.rs b/crates/taurus-core/src/runtime/execution/trace.rs new file mode 100644 index 0000000..0b8e1d6 --- /dev/null +++ b/crates/taurus-core/src/runtime/execution/trace.rs @@ -0,0 +1,85 @@ +//! Runtime execution trace model. +//! +//! This model captures node frames, argument resolution, control-flow edges, +//! and final outcomes for one execution run. + +use std::time::Instant; + +/// Relationship between two execution frames. +#[derive(Debug, Clone)] +pub enum EdgeKind { + /// Sequential flow transition via `next_node_id`. + Next, + /// Eager argument child execution. + EagerCall { arg_index: usize }, + /// Lazy runtime callback child execution. + RuntimeCall { label: Option }, +} + +/// Argument classification for tracing. +#[derive(Debug, Clone)] +pub enum ArgKind { + Literal, + Reference { + reference: ReferenceKind, + hit: bool, + }, + Thunk { + node_id: i64, + eager: bool, + executed: bool, + }, +} + +/// Reference source kind for argument tracing. +#[derive(Debug, Clone)] +pub enum ReferenceKind { + Result { + node_id: i64, + }, + InputType { + node_id: i64, + input_index: i64, + parameter_index: i64, + }, + FlowInput, + Empty, +} + +/// One traced argument on a frame. +#[derive(Debug, Clone)] +pub struct ArgTrace { + pub index: usize, + pub kind: ArgKind, + pub preview: String, +} + +/// Final outcome of a frame. +#[derive(Debug, Clone)] +pub enum Outcome { + Success { value_preview: String }, + Failure { error_preview: String }, + Return { value_preview: String }, + Respond { value_preview: String }, + Stop, +} + +/// One executed node invocation. +#[derive(Debug, Clone)] +pub struct ExecFrame { + pub frame_id: u64, + pub node_id: i64, + pub function_name: String, + pub args: Vec, + pub outcome: Option, + pub start: Instant, + pub end: Option, + pub children: Vec<(EdgeKind, u64)>, +} + +/// Trace data for a full execution. +#[derive(Debug, Clone)] +pub struct TraceRun { + pub frames: Vec, + pub root: u64, +} diff --git a/crates/taurus-core/src/runtime/execution/tracer.rs b/crates/taurus-core/src/runtime/execution/tracer.rs new file mode 100644 index 0000000..3fb81a0 --- /dev/null +++ b/crates/taurus-core/src/runtime/execution/tracer.rs @@ -0,0 +1,157 @@ +//! In-memory execution trace collector. + +use std::time::Instant; + +use crate::runtime::execution::trace::{ArgKind, ArgTrace, EdgeKind, ExecFrame, Outcome, TraceRun}; + +/// Trace collector interface used by the executor. +pub trait ExecutionTracer { + fn enter_node(&mut self, node_id: i64, function_name: &str) -> u64; + fn record_arg(&mut self, frame_id: u64, arg: ArgTrace); + fn link_child(&mut self, parent_frame: u64, child_frame: u64, edge: EdgeKind); + fn mark_thunk(&mut self, frame_id: u64, arg_index: usize, eager: bool, executed: bool); + fn mark_thunk_executed_by_node(&mut self, frame_id: u64, node_id: i64); + fn exit_node(&mut self, frame_id: u64, outcome: Outcome); +} + +/// Default trace recorder used by the runtime engine. +pub struct Tracer { + next_id: u64, + pub run: Option, + stack: Vec, +} + +impl Default for Tracer { + fn default() -> Self { + Self::new() + } +} + +impl Tracer { + pub fn new() -> Self { + Self { + next_id: 1, + run: None, + stack: vec![], + } + } + + fn frames_mut(&mut self) -> &mut Vec { + &mut self + .run + .as_mut() + .expect("trace run must exist before frame mutation") + .frames + } + + fn get_frame_mut(&mut self, frame_id: u64) -> &mut ExecFrame { + let idx = self + .frames_mut() + .iter() + .position(|f| f.frame_id == frame_id) + .expect("trace frame must exist"); + &mut self.frames_mut()[idx] + } + + pub fn take_run(self) -> Option { + self.run + } +} + +impl ExecutionTracer for Tracer { + fn enter_node(&mut self, node_id: i64, function_name: &str) -> u64 { + if self.run.is_none() { + self.run = Some(TraceRun { + frames: vec![], + root: 0, + }); + } + + let frame_id = self.next_id; + self.next_id += 1; + + let frame = ExecFrame { + frame_id, + node_id, + function_name: function_name.to_string(), + args: vec![], + outcome: None, + start: Instant::now(), + end: None, + children: vec![], + }; + + let run = self + .run + .as_mut() + .expect("trace run must exist before first frame"); + if run.root == 0 { + run.root = frame_id; + } + run.frames.push(frame); + + self.stack.push(frame_id); + frame_id + } + + fn record_arg(&mut self, frame_id: u64, arg: ArgTrace) { + self.get_frame_mut(frame_id).args.push(arg); + } + + fn link_child(&mut self, parent_frame: u64, child_frame: u64, edge: EdgeKind) { + self.get_frame_mut(parent_frame) + .children + .push((edge, child_frame)); + } + + fn mark_thunk(&mut self, frame_id: u64, arg_index: usize, eager: bool, executed: bool) { + let frame = self.get_frame_mut(frame_id); + if let Some(arg) = frame.args.iter_mut().find(|a| a.index == arg_index) + && let ArgTrace { + kind: + ArgKind::Thunk { + eager: current_eager, + executed: current_executed, + .. + }, + .. + } = arg + { + *current_eager = eager; + *current_executed = executed; + } + } + + fn mark_thunk_executed_by_node(&mut self, frame_id: u64, node_id: i64) { + let frame = self.get_frame_mut(frame_id); + if let Some(arg) = frame.args.iter_mut().find(|a| { + matches!( + a.kind, + ArgKind::Thunk { + node_id: current_node, + executed: false, + .. + } if current_node == node_id + ) + }) && let ArgTrace { + kind: + ArgKind::Thunk { + executed: current_executed, + .. + }, + .. + } = arg + { + *current_executed = true; + } + } + + fn exit_node(&mut self, frame_id: u64, outcome: Outcome) { + let frame = self.get_frame_mut(frame_id); + frame.outcome = Some(outcome); + frame.end = Some(Instant::now()); + + let popped = self.stack.pop(); + debug_assert_eq!(popped, Some(frame_id)); + } +} diff --git a/crates/taurus-core/src/runtime/execution/value_store.rs b/crates/taurus-core/src/runtime/execution/value_store.rs new file mode 100644 index 0000000..ac61591 --- /dev/null +++ b/crates/taurus-core/src/runtime/execution/value_store.rs @@ -0,0 +1,145 @@ +//! Mutable value store used by runtime execution to resolve references. + +use std::collections::HashMap; + +use tucana::shared::{InputType, ReferenceValue, Value, value::Kind}; + +use crate::types::errors::runtime_error::RuntimeError; + +#[derive(Clone)] +pub enum ValueStoreResult { + Error(RuntimeError), + Success(Value), + NotFound, +} + +#[derive(Default)] +pub struct ValueStore { + results: HashMap, + input_types: HashMap, + flow_input: Value, + current_node_id: i64, + runtime_trace_labels: Vec, +} + +impl ValueStore { + pub fn new(flow_input: Value) -> Self { + Self { + results: HashMap::new(), + input_types: HashMap::new(), + flow_input, + current_node_id: 0, + runtime_trace_labels: Vec::new(), + } + } + + pub fn get_current_node_id(&mut self) -> i64 { + self.current_node_id + } + + pub fn set_current_node_id(&mut self, node_id: i64) { + self.current_node_id = node_id; + } + + pub fn get(&mut self, reference: ReferenceValue) -> ValueStoreResult { + let target = match reference.target { + Some(target) => target, + None => return ValueStoreResult::NotFound, + }; + + let result = match target { + tucana::shared::reference_value::Target::FlowInput(_) => self.get_flow_input(), + tucana::shared::reference_value::Target::NodeId(id) => self.get_result(id), + tucana::shared::reference_value::Target::InputType(input_type) => { + self.get_input_type(input_type) + } + }; + + if reference.paths.is_empty() { + return result; + } + + if let ValueStoreResult::Success(value) = result { + let mut current = value; + for path in reference.paths { + if let Some(index) = path.array_index { + match current.kind { + Some(ref kind) => match kind { + Kind::ListValue(list) => match list.values.get(index as usize) { + Some(item) => current = item.clone(), + None => return ValueStoreResult::NotFound, + }, + _ => return ValueStoreResult::NotFound, + }, + None => return ValueStoreResult::NotFound, + } + } + + if let Some(field_name) = path.path { + match current.kind { + Some(ref kind) => { + if let Kind::StructValue(struct_value) = kind { + match struct_value.fields.get(&field_name) { + Some(item) => current = item.clone(), + None => return ValueStoreResult::NotFound, + } + } + } + None => return ValueStoreResult::NotFound, + } + } + } + + ValueStoreResult::Success(current) + } else { + result + } + } + + fn get_result(&mut self, id: i64) -> ValueStoreResult { + match self.results.get(&id) { + Some(result) => result.clone(), + None => ValueStoreResult::NotFound, + } + } + + fn get_flow_input(&mut self) -> ValueStoreResult { + ValueStoreResult::Success(self.flow_input.clone()) + } + + fn get_input_type(&mut self, input_type: InputType) -> ValueStoreResult { + match self.input_types.get(&input_type) { + Some(value) => ValueStoreResult::Success(value.clone()), + None => ValueStoreResult::NotFound, + } + } + + pub fn clear_input_type(&mut self, input_type: InputType) { + self.input_types.remove(&input_type); + } + + pub fn insert_input_type(&mut self, input_type: InputType, value: Value) { + self.input_types.insert(input_type, value); + } + + pub fn insert_flow_input(&mut self, value: Value) { + self.flow_input = value; + } + + pub fn insert_success(&mut self, id: i64, value: Value) { + self.results.insert(id, ValueStoreResult::Success(value)); + } + + pub fn insert_error(&mut self, id: i64, runtime_error: RuntimeError) { + self.results + .insert(id, ValueStoreResult::Error(runtime_error)); + } + + pub fn push_runtime_trace_label(&mut self, label: String) { + self.runtime_trace_labels.push(label); + } + + pub fn pop_runtime_trace_label(&mut self) -> Option { + self.runtime_trace_labels.pop() + } +} From d2b0f6017139d0de9fc91db2d6cc5001146eaaf6 Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 21 Apr 2026 17:53:56 +0200 Subject: [PATCH 14/28] feat: made func registration static --- .../src/runtime/functions/array.rs | 1703 +++++++++++++++++ .../src/runtime/functions/boolean.rs | 414 ++++ .../src/runtime/functions/control.rs | 98 + .../taurus-core/src/runtime/functions/http.rs | 133 ++ .../taurus-core/src/runtime/functions/mod.rs | 23 + .../src/runtime/functions/number.rs | 1247 ++++++++++++ .../src/runtime/functions/object.rs | 415 ++++ .../taurus-core/src/runtime/functions/text.rs | 1004 ++++++++++ 8 files changed, 5037 insertions(+) create mode 100644 crates/taurus-core/src/runtime/functions/array.rs create mode 100644 crates/taurus-core/src/runtime/functions/boolean.rs create mode 100644 crates/taurus-core/src/runtime/functions/control.rs create mode 100644 crates/taurus-core/src/runtime/functions/http.rs create mode 100644 crates/taurus-core/src/runtime/functions/mod.rs create mode 100644 crates/taurus-core/src/runtime/functions/number.rs create mode 100644 crates/taurus-core/src/runtime/functions/object.rs create mode 100644 crates/taurus-core/src/runtime/functions/text.rs diff --git a/crates/taurus-core/src/runtime/functions/array.rs b/crates/taurus-core/src/runtime/functions/array.rs new file mode 100644 index 0000000..03f2797 --- /dev/null +++ b/crates/taurus-core/src/runtime/functions/array.rs @@ -0,0 +1,1703 @@ +//! List/array runtime handlers. +//! +//! This module includes both pure list transforms and callback-driven handlers +//! (`map`, `filter`, `find`, `for_each`, sort comparators). +//! Callback signals are normalized so `Return(value)` is treated as the +//! callback result value for that iteration. + +use std::cmp::Ordering; + +use tucana::shared::InputType; +use tucana::shared::{ListValue, Value, value::Kind}; + +use crate::handler::argument::Argument; +use crate::handler::argument::ParameterNode::{Eager, Lazy}; +use crate::handler::macros::args; +use crate::handler::registry::FunctionRegistration; +use crate::runtime::execution::value_store::ValueStore; +use crate::types::errors::runtime_error::RuntimeError; +use crate::types::signal::Signal; +use crate::value::{number_to_f64, number_to_string, value_from_f64, value_from_i64}; + +pub(crate) const FUNCTIONS: &[FunctionRegistration] = &[ + FunctionRegistration::eager("std::list::at", at, 2), + FunctionRegistration::eager("std::list::concat", concat, 2), + FunctionRegistration::modes("std::list::filter", filter, &[Eager, Lazy]), + FunctionRegistration::modes("std::list::find", find, &[Eager, Lazy]), + FunctionRegistration::modes("std::list::find_last", find_last, &[Eager, Lazy]), + FunctionRegistration::modes("std::list::find_index", find_index, &[Eager, Lazy]), + FunctionRegistration::eager("std::list::first", first, 1), + FunctionRegistration::eager("std::list::last", last, 1), + FunctionRegistration::modes("std::list::for_each", for_each, &[Eager, Lazy]), + FunctionRegistration::modes("std::list::map", map, &[Eager, Lazy]), + FunctionRegistration::eager("std::list::push", push, 2), + FunctionRegistration::eager("std::list::pop", pop, 1), + FunctionRegistration::eager("std::list::remove", remove, 2), + FunctionRegistration::eager("std::list::is_empty", is_empty, 1), + FunctionRegistration::eager("std::list::size", size, 1), + FunctionRegistration::eager("std::list::index_of", index_of, 2), + FunctionRegistration::eager("std::list::to_unique", to_unique, 1), + FunctionRegistration::modes("std::list::sort", sort, &[Eager, Lazy]), + FunctionRegistration::modes("std::list::sort_reverse", sort_reverse, &[Eager, Lazy]), + FunctionRegistration::eager("std::list::reverse", reverse, 1), + FunctionRegistration::eager("std::list::flat", flat, 1), + FunctionRegistration::eager("std::list::min", min, 1), + FunctionRegistration::eager("std::list::max", max, 1), + FunctionRegistration::eager("std::list::sum", sum, 1), + FunctionRegistration::eager("std::list::join", join, 2), +]; + +fn as_list(value: &Value, err: &'static str) -> Result { + match value.kind.clone().unwrap_or(Kind::NullValue(0)) { + Kind::ListValue(lv) => Ok(lv), + _ => Err(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + err, + )), + } +} + +fn as_bool(value: &Value) -> Result { + match value.kind.clone().unwrap_or(Kind::NullValue(0)) { + Kind::BoolValue(b) => Ok(b), + _ => Err(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + "Expected boolean result from predicate", + )), + } +} + +fn fail(category: &str, message: impl Into) -> Signal { + Signal::Failure(RuntimeError::new("T-LST-000000", category, message)) +} + +fn parse_array_and_thunk<'a>( + op_name: &str, + args: &'a [Argument], +) -> Result<(&'a Value, i64), Signal> { + match args { + [Argument::Eval(array_v), Argument::Thunk(thunk)] => Ok((array_v, *thunk)), + _ => Err(fail( + "InvalidArgumentRuntimeError", + format!( + "{op_name} expects (array: eager, callback: lazy thunk), got {:?}", + args + ), + )), + } +} + +fn unary_input_type(ctx: &mut ValueStore) -> InputType { + let node_id = ctx.get_current_node_id(); + InputType { + node_id, + parameter_index: 1, + input_index: 0, + } +} + +fn run_with_unary_input( + ctx: &mut ValueStore, + input_type: InputType, + iter_index: usize, + item: &Value, + run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, + thunk_node: i64, +) -> Signal { + ctx.insert_input_type(input_type, item.clone()); + ctx.push_runtime_trace_label(format!("iter={} value={}", iter_index, preview_value(item))); + let signal = run(thunk_node, ctx); + ctx.clear_input_type(input_type); + signal +} + +fn run_with_binary_inputs( + ctx: &mut ValueStore, + left_input: InputType, + right_input: InputType, + cmp_index: usize, + left: &Value, + right: &Value, + run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, + thunk_node: i64, +) -> Signal { + ctx.insert_input_type(left_input, left.clone()); + ctx.insert_input_type(right_input, right.clone()); + ctx.push_runtime_trace_label(format!( + "cmp#{} a={} b={}", + cmp_index, + preview_value(left), + preview_value(right) + )); + let signal = run(thunk_node, ctx); + ctx.clear_input_type(left_input); + ctx.clear_input_type(right_input); + signal +} + +fn callback_result_value(signal: Signal) -> Result { + match signal { + Signal::Success(value) | Signal::Return(value) => Ok(value), + other @ (Signal::Failure(_) | Signal::Respond(_) | Signal::Stop) => Err(other), + } +} + +fn comparator_ordering(signal: Signal, reverse: bool) -> Result { + let value = callback_result_value(signal)?; + + let ord = match value { + Value { + kind: Some(Kind::NumberValue(number)), + } => match number_to_f64(&number) { + Some(value) if value < 0.0 => Ordering::Less, + Some(value) if value > 0.0 => Ordering::Greater, + Some(_) => Ordering::Equal, + None => { + return Err(fail( + "InvalidArgumentRuntimeError", + "Comparator must return a finite number", + )); + } + }, + value => { + return Err(fail( + "InvalidArgumentRuntimeError", + format!( + "Expected comparator to return NumberValue, received {:?}", + value + ), + )); + } + }; + + if reverse { Ok(ord.reverse()) } else { Ok(ord) } +} + +fn at( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + // array, index + args!(args => array: ListValue, index: f64); + + if index < 0.0 { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "IndexOutOfBoundsRuntimeError", + "Negative index", + )); + } + let i = index as usize; + match array.values.get(i) { + Some(item) => Signal::Success(item.clone()), + None => fail( + "IndexOutOfBoundsRuntimeError", + format!("Index {} out of bounds (len={})", i, array.values.len()), + ), + } +} + +fn concat( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => lhs_v: Value, rhs_v: Value); + + let Kind::ListValue(lhs) = lhs_v.kind.clone().ok_or(()).unwrap_or(Kind::NullValue(0)) else { + return fail( + "InvalidArgumentRuntimeError", + format!( + "Expected two arrays as arguments but received lhs={:?}", + lhs_v + ), + ); + }; + let Kind::ListValue(rhs) = rhs_v.kind.clone().ok_or(()).unwrap_or(Kind::NullValue(0)) else { + return fail( + "InvalidArgumentRuntimeError", + format!( + "Expected two arrays as arguments but received rhs={:?}", + rhs_v + ), + ); + }; + + let mut result = lhs.values.clone(); + result.extend(rhs.values.iter().cloned()); + + Signal::Success(Value { + kind: Some(Kind::ListValue(ListValue { values: result })), + }) +} + +fn filter( + args: &[Argument], + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + let (array_v, predicate_node) = match parse_array_and_thunk("filter", args) { + Ok(data) => data, + Err(signal) => return signal, + }; + + let array = match as_list(array_v, "Expected first argument to be an array") { + Ok(a) => a, + Err(e) => return Signal::Failure(e), + }; + + let mut out: Vec = Vec::new(); + let input_type = unary_input_type(ctx); + for (idx, item) in array.values.iter().enumerate() { + let pred_sig = run_with_unary_input(ctx, input_type, idx, item, run, predicate_node); + let predicate_value = match callback_result_value(pred_sig) { + Ok(v) => v, + Err(other) => return other, + }; + + match as_bool(&predicate_value) { + Ok(true) => out.push(item.clone()), + Ok(false) => {} + Err(e) => return Signal::Failure(e), + } + } + + Signal::Success(Value { + kind: Some(Kind::ListValue(ListValue { values: out })), + }) +} + +fn find( + args: &[Argument], + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + let (array_v, predicate_node) = match parse_array_and_thunk("find", args) { + Ok(data) => data, + Err(signal) => return signal, + }; + + let array = match as_list(array_v, "Expected first argument to be an array") { + Ok(a) => a, + Err(e) => return Signal::Failure(e), + }; + let input_type = unary_input_type(ctx); + + for (idx, item) in array.values.iter().enumerate() { + let pred_sig = run_with_unary_input(ctx, input_type, idx, item, run, predicate_node); + let predicate_value = match callback_result_value(pred_sig) { + Ok(v) => v, + Err(other) => return other, + }; + match as_bool(&predicate_value) { + Ok(true) => return Signal::Success(item.clone()), + Ok(false) => continue, + Err(e) => return Signal::Failure(e), + } + } + + Signal::Failure(RuntimeError::new( + "T-RT-000000", + "NotFoundError", + "No item found that satisfies the predicate", + )) +} +fn find_last( + args: &[Argument], + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + let (array_v, predicate_node) = match parse_array_and_thunk("find_last", args) { + Ok(data) => data, + Err(signal) => return signal, + }; + + let array = match as_list(array_v, "Expected first argument to be an array") { + Ok(a) => a, + Err(e) => return Signal::Failure(e), + }; + let input_type = unary_input_type(ctx); + + for (idx, item) in array.values.iter().enumerate().rev() { + let pred_sig = run_with_unary_input(ctx, input_type, idx, item, run, predicate_node); + let predicate_value = match callback_result_value(pred_sig) { + Ok(v) => v, + Err(other) => return other, + }; + match as_bool(&predicate_value) { + Ok(true) => return Signal::Success(item.clone()), + Ok(false) => continue, + Err(e) => return Signal::Failure(e), + } + } + + Signal::Failure(RuntimeError::new( + "T-RT-000000", + "NotFoundError", + "No item found that satisfies the predicate", + )) +} + +fn find_index( + args: &[Argument], + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + let (array_v, predicate_node) = match parse_array_and_thunk("find_index", args) { + Ok(data) => data, + Err(signal) => return signal, + }; + + let array = match as_list(array_v, "Expected first argument to be an array") { + Ok(a) => a, + Err(e) => return Signal::Failure(e), + }; + let input_type = unary_input_type(ctx); + + for (idx, item) in array.values.iter().enumerate() { + let pred_sig = run_with_unary_input(ctx, input_type, idx, item, run, predicate_node); + let predicate_value = match callback_result_value(pred_sig) { + Ok(v) => v, + Err(other) => return other, + }; + + match as_bool(&predicate_value) { + Ok(true) => return Signal::Success(value_from_i64(idx as i64)), + Ok(false) => continue, + Err(e) => return Signal::Failure(e), + } + } + + Signal::Failure(RuntimeError::new( + "T-RT-000000", + "NotFoundError", + "No item found that satisfies the predicate", + )) +} +fn first( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => array: ListValue); + + match array.values.first() { + Some(v) => Signal::Success(v.clone()), + None => Signal::Failure(RuntimeError::new( + "T-RT-000000", + "ArrayEmptyRuntimeError", + "This array is empty", + )), + } +} + +fn last( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => array: ListValue); + match array.values.last() { + Some(v) => Signal::Success(v.clone()), + None => Signal::Failure(RuntimeError::new( + "T-RT-000000", + "ArrayEmptyRuntimeError", + "This array is empty", + )), + } +} + +fn for_each( + args: &[Argument], + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + let (array_v, transform_node) = match parse_array_and_thunk("for_each", args) { + Ok(data) => data, + Err(signal) => return signal, + }; + + let array = match as_list(array_v, "Expected first argument to be an array") { + Ok(a) => a, + Err(e) => return Signal::Failure(e), + }; + let input_type = unary_input_type(ctx); + + for (idx, item) in array.values.iter().enumerate() { + let sig = run_with_unary_input(ctx, input_type, idx, item, run, transform_node); + + match callback_result_value(sig) { + Ok(_) => {} + Err(other) => return other, + } + } + + Signal::Success(Value { + kind: Some(Kind::NullValue(0)), + }) +} + +fn preview_value(value: &Value) -> String { + format_value_json(value) +} + +fn format_value_json(value: &Value) -> String { + match value.kind.as_ref() { + Some(Kind::NumberValue(v)) => number_to_string(v), + Some(Kind::BoolValue(v)) => v.to_string(), + Some(Kind::StringValue(v)) => format!("{:?}", v), + Some(Kind::NullValue(_)) | None => "null".to_string(), + Some(Kind::ListValue(list)) => { + let mut parts = Vec::new(); + for item in list.values.iter() { + parts.push(format_value_json(item)); + } + format!("[{}]", parts.join(", ")) + } + Some(Kind::StructValue(struct_value)) => { + let mut keys: Vec<_> = struct_value.fields.keys().collect(); + keys.sort(); + let mut parts = Vec::new(); + for key in keys.iter() { + if let Some(v) = struct_value.fields.get(*key) { + parts.push(format!("{:?}: {}", key, format_value_json(v))); + } + } + format!("{{{}}}", parts.join(", ")) + } + } +} + +fn map( + args: &[Argument], + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + let (array_v, transform_node) = match parse_array_and_thunk("map", args) { + Ok(data) => data, + Err(signal) => return signal, + }; + + let array = match as_list(array_v, "Expected first argument to be an array") { + Ok(a) => a, + Err(e) => return Signal::Failure(e), + }; + + let mut out: Vec = Vec::with_capacity(array.values.len()); + let input_type = unary_input_type(ctx); + + for (idx, item) in array.values.iter().enumerate() { + let sig = run_with_unary_input(ctx, input_type, idx, item, run, transform_node); + match callback_result_value(sig) { + Ok(v) => out.push(v), + Err(other) => return other, + } + } + + Signal::Success(Value { + kind: Some(Kind::ListValue(ListValue { values: out })), + }) +} + +fn push( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => array_v: Value, item: Value); + let Kind::ListValue(mut array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + "Expected first argument to be an array", + )); + }; + array.values.push(item); + Signal::Success(Value { + kind: Some(Kind::ListValue(array)), + }) +} + +fn pop( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => array_v: Value); + let Kind::ListValue(mut array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + "Expected an array as an argument", + )); + }; + array.values.pop(); + Signal::Success(Value { + kind: Some(Kind::ListValue(array)), + }) +} + +fn remove( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => array_v: Value, item: Value); + let Kind::ListValue(mut array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + "Expected first argument to be an array", + )); + }; + + if let Some(index) = array.values.iter().position(|x| *x == item) { + array.values.remove(index); + Signal::Success(Value { + kind: Some(Kind::ListValue(array)), + }) + } else { + fail( + "ValueNotFoundRuntimeError", + format!("Item {:?} not found in array", item), + ) + } +} + +fn is_empty( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => array_v: Value); + let Kind::ListValue(array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + "Expected an array as an argument", + )); + }; + Signal::Success(Value { + kind: Some(Kind::BoolValue(array.values.is_empty())), + }) +} + +fn size( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => array_v: Value); + let Kind::ListValue(array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + "Expected an array as an argument", + )); + }; + Signal::Success(value_from_i64(array.values.len() as i64)) +} + +fn index_of( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => array_v: Value, item: Value); + let Kind::ListValue(array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + "Expected first argument to be an array", + )); + }; + + match array.values.iter().position(|x| *x == item) { + Some(i) => Signal::Success(value_from_i64(i as i64)), + None => fail( + "ValueNotFoundRuntimeError", + format!("Item {:?} not found in array", item), + ), + } +} + +fn to_unique( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => array_v: Value); + let Kind::ListValue(array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + "Expected an array as an argument", + )); + }; + + let mut unique = Vec::::new(); + for v in &array.values { + if !unique.contains(v) { + unique.push(v.clone()); + } + } + + Signal::Success(Value { + kind: Some(Kind::ListValue(ListValue { values: unique })), + }) +} + +fn sort( + args: &[Argument], + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + let (array_v, transform_node) = match parse_array_and_thunk("sort", args) { + Ok(data) => data, + Err(signal) => return signal, + }; + + let mut array = match as_list(array_v, "Expected first argument to be an array") { + Ok(a) => a, + Err(e) => return Signal::Failure(e), + }; + + let node_id = ctx.get_current_node_id(); + let left_input = InputType { + node_id, + parameter_index: 1, + input_index: 0, + }; + + let right_input = InputType { + node_id, + parameter_index: 1, + input_index: 1, + }; + + let mut comparator_failure: Option = None; + let mut cmp_idx = 0usize; + array.values.sort_by(|left, right| { + if comparator_failure.is_some() { + return Ordering::Equal; + } + + let signal = run_with_binary_inputs( + ctx, + left_input, + right_input, + cmp_idx, + left, + right, + run, + transform_node, + ); + cmp_idx += 1; + + match comparator_ordering(signal, false) { + Ok(ordering) => ordering, + Err(signal) => { + comparator_failure = Some(signal); + Ordering::Equal + } + } + }); + + if let Some(signal) = comparator_failure { + return signal; + } + + Signal::Success(Value { + kind: Some(Kind::ListValue(array)), + }) +} + +fn sort_reverse( + args: &[Argument], + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + let (array_v, transform_node) = match parse_array_and_thunk("sort_reverse", args) { + Ok(data) => data, + Err(signal) => return signal, + }; + + let mut array = match as_list(array_v, "Expected first argument to be an array") { + Ok(a) => a, + Err(e) => return Signal::Failure(e), + }; + + let node_id = ctx.get_current_node_id(); + let left_input = InputType { + node_id, + parameter_index: 1, + input_index: 0, + }; + + let right_input = InputType { + node_id, + parameter_index: 1, + input_index: 1, + }; + + let mut comparator_failure: Option = None; + let mut cmp_idx = 0usize; + array.values.sort_by(|left, right| { + if comparator_failure.is_some() { + return Ordering::Equal; + } + + let signal = run_with_binary_inputs( + ctx, + left_input, + right_input, + cmp_idx, + left, + right, + run, + transform_node, + ); + cmp_idx += 1; + + match comparator_ordering(signal, true) { + Ok(ordering) => ordering, + Err(signal) => { + comparator_failure = Some(signal); + Ordering::Equal + } + } + }); + + if let Some(signal) = comparator_failure { + return signal; + } + + Signal::Success(Value { + kind: Some(Kind::ListValue(array)), + }) +} + +fn reverse( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => array_v: Value); + let Kind::ListValue(mut array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + "Expected an array as an argument", + )); + }; + array.values.reverse(); + Signal::Success(Value { + kind: Some(Kind::ListValue(array)), + }) +} + +fn flat( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => array_v: Value); + let Kind::ListValue(array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + "Expected an array as an argument", + )); + }; + + let mut out: Vec = Vec::new(); + for item in &array.values { + match &item.kind { + Some(Kind::ListValue(sub)) => out.extend(sub.values.iter().cloned()), + _ => out.push(item.clone()), + } + } + + Signal::Success(Value { + kind: Some(Kind::ListValue(ListValue { values: out })), + }) +} + +fn min( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => array: ListValue); + + let mut nums: Vec = Vec::new(); + let mut all_int = true; + let mut min_i64: Option = None; + for v in &array.values { + if let Some(Kind::NumberValue(n)) = &v.kind { + match n.number { + Some(tucana::shared::number_value::Number::Integer(i)) => { + min_i64 = Some(match min_i64 { + Some(curr) => curr.min(i), + None => i, + }); + nums.push(i as f64); + } + Some(tucana::shared::number_value::Number::Float(f)) => { + all_int = false; + nums.push(f); + } + None => {} + } + } + } + + match nums.iter().min_by(|a, b| a.total_cmp(b)) { + Some(m) if all_int => Signal::Success(value_from_i64(min_i64.unwrap_or(*m as i64))), + Some(m) => Signal::Success(value_from_f64(*m)), + None => Signal::Failure(RuntimeError::new( + "T-RT-000000", + "ArrayEmptyRuntimeError", + "Array is empty", + )), + } +} + +fn max( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => array: ListValue); + + let mut nums: Vec = Vec::new(); + let mut all_int = true; + let mut max_i64: Option = None; + for v in &array.values { + if let Some(Kind::NumberValue(n)) = &v.kind { + match n.number { + Some(tucana::shared::number_value::Number::Integer(i)) => { + max_i64 = Some(match max_i64 { + Some(curr) => curr.max(i), + None => i, + }); + nums.push(i as f64); + } + Some(tucana::shared::number_value::Number::Float(f)) => { + all_int = false; + nums.push(f); + } + None => {} + } + } + } + + match nums.iter().max_by(|a, b| a.total_cmp(b)) { + Some(m) if all_int => Signal::Success(value_from_i64(max_i64.unwrap_or(*m as i64))), + Some(m) => Signal::Success(value_from_f64(*m)), + None => Signal::Failure(RuntimeError::new( + "T-RT-000000", + "ArrayEmptyRuntimeError", + "Array is empty", + )), + } +} + +fn sum( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => array: ListValue); + + let mut s_f = 0.0; + let mut s_i: i64 = 0; + let mut all_int = true; + for v in &array.values { + if let Some(Kind::NumberValue(n)) = &v.kind { + match n.number { + Some(tucana::shared::number_value::Number::Integer(i)) => { + if let Some(next) = s_i.checked_add(i) { + s_i = next; + s_f += i as f64; + } else { + all_int = false; + if let Some(f) = number_to_f64(n) { + s_f += f; + } + } + } + Some(tucana::shared::number_value::Number::Float(f)) => { + all_int = false; + s_f += f; + } + None => {} + } + } + } + + if all_int { + Signal::Success(value_from_i64(s_i)) + } else { + Signal::Success(value_from_f64(s_f)) + } +} + +fn join( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => array: ListValue, separator: String); + + let mut parts: Vec = Vec::new(); + for v in &array.values { + if let Some(Kind::StringValue(s)) = &v.kind { + parts.push(s.clone()); + } + } + + Signal::Success(Value { + kind: Some(Kind::StringValue(parts.join(&separator))), + }) +} +#[cfg(test)] +mod tests { + use super::*; + use crate::runtime::execution::value_store::ValueStore; + use crate::value::{number_to_f64, number_value_from_f64, value_from_f64}; + use tucana::shared::{ListValue, Value, value::Kind}; + + // --- helpers ------------------------------------------------------------- + fn a_val(v: Value) -> Argument { + Argument::Eval(v) + } + fn a_thunk(id: i64) -> Argument { + Argument::Thunk(id) + } + fn v_num(n: f64) -> Value { + value_from_f64(n) + } + fn v_str(s: &str) -> Value { + Value { + kind: Some(Kind::StringValue(s.to_string())), + } + } + fn v_bool(b: bool) -> Value { + Value { + kind: Some(Kind::BoolValue(b)), + } + } + fn v_list(values: Vec) -> Value { + Value { + kind: Some(Kind::ListValue(ListValue { values })), + } + } + fn k_num(n: f64) -> Option { + Some(Kind::NumberValue(number_value_from_f64(n))) + } + + fn expect_num(sig: Signal) -> f64 { + match sig { + Signal::Success(Value { + kind: Some(Kind::NumberValue(n)), + }) => number_to_f64(&n).unwrap_or_default(), + x => panic!("Expected NumberValue, got {:?}", x), + } + } + fn expect_str(sig: Signal) -> String { + match sig { + Signal::Success(Value { + kind: Some(Kind::StringValue(s)), + }) => s, + x => panic!("Expected StringValue, got {:?}", x), + } + } + fn expect_list(sig: Signal) -> Vec { + match sig { + Signal::Success(Value { + kind: Some(Kind::ListValue(ListValue { values })), + }) => values, + x => panic!("Expected ListValue, got {:?}", x), + } + } + fn expect_bool(sig: Signal) -> bool { + match sig { + Signal::Success(Value { + kind: Some(Kind::BoolValue(b)), + }) => b, + x => panic!("Expected BoolValue, got {:?}", x), + } + } + + fn dummy_run(_: i64, _: &mut ValueStore) -> Signal { + Signal::Success(Value { + kind: Some(Kind::NullValue(0)), + }) + } + + fn run_from_bools(seq: Vec) -> impl FnMut(i64, &mut ValueStore) -> Signal { + let mut i = 0usize; + move |_, _| { + let b = *seq.get(i).unwrap_or(&false); + i += 1; + Signal::Success(Value { + kind: Some(Kind::BoolValue(b)), + }) + } + } + + fn run_from_values(seq: Vec) -> impl FnMut(i64, &mut ValueStore) -> Signal { + let mut i = 0usize; + move |_, _| { + let v = seq.get(i).cloned().unwrap_or(Value { + kind: Some(Kind::NullValue(0)), + }); + i += 1; + Signal::Success(v) + } + } + + // --- at ------------------------------------------------------------------ + #[test] + fn test_at_success() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + let arr = v_list(vec![v_num(10.0), v_num(20.0), v_num(30.0)]); + + assert_eq!( + expect_num(at( + &[a_val(arr.clone()), a_val(v_num(0.0))], + &mut ctx, + &mut run + )), + 10.0 + ); + assert_eq!( + expect_num(at( + &[a_val(arr.clone()), a_val(v_num(1.0))], + &mut ctx, + &mut run + )), + 20.0 + ); + assert_eq!( + expect_num(at(&[a_val(arr), a_val(v_num(2.0))], &mut ctx, &mut run)), + 30.0 + ); + } + + #[test] + fn test_at_error() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + let arr = v_list(vec![v_num(1.0)]); + + // wrong arg count + match at(&[a_val(arr.clone())], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + // wrong type first arg + match at( + &[a_val(v_str("not_array")), a_val(v_num(0.0))], + &mut ctx, + &mut run, + ) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + // wrong type second arg + match at(&[a_val(arr.clone()), a_val(v_str("x"))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + // oob / negative + match at(&[a_val(arr.clone()), a_val(v_num(9.0))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match at(&[a_val(arr), a_val(v_num(-1.0))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + } + + // --- concat -------------------------------------------------------------- + #[test] + fn test_concat_success() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + let a = v_list(vec![v_num(1.0), v_num(2.0)]); + let b = v_list(vec![v_num(3.0), v_num(4.0)]); + let out = expect_list(concat(&[a_val(a), a_val(b)], &mut ctx, &mut run)); + assert_eq!(out.len(), 4); + assert_eq!(out[0].kind, k_num(1.0)); + assert_eq!(out[3].kind, k_num(4.0)); + } + + #[test] + fn test_concat_error() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + let arr = v_list(vec![v_num(1.0)]); + match concat(&[a_val(arr.clone())], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match concat( + &[a_val(v_str("not_array")), a_val(arr.clone())], + &mut ctx, + &mut run, + ) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match concat(&[a_val(arr), a_val(v_num(42.0))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + } + + // --- filter / find / find_last / find_index ------------------------------ + #[test] + fn test_filter_success() { + let mut ctx = ValueStore::default(); + let array = v_list(vec![v_num(1.0), v_num(2.0), v_num(3.0)]); + let mut run = run_from_bools(vec![true, false, true]); + let out = expect_list(filter(&[a_val(array), a_thunk(1)], &mut ctx, &mut run)); + assert_eq!(out.len(), 2); + assert_eq!(out[0].kind, k_num(1.0)); + assert_eq!(out[1].kind, k_num(3.0)); + } + + #[test] + fn test_filter_error() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + let array = v_list(vec![v_num(1.0)]); + let _predicate = v_list(vec![v_bool(true)]); + match filter(&[a_val(array.clone())], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match filter(&[a_val(v_str("not_array")), a_thunk(1)], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match filter(&[a_val(array), a_val(v_num(1.0))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + } + + // --- first / last -------------------------------------------------------- + #[test] + fn test_first_success() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + let arr = v_list(vec![v_str("first"), v_str("second"), v_str("third")]); + assert_eq!( + expect_str(first(&[a_val(arr)], &mut ctx, &mut run)), + "first" + ); + } + + #[test] + fn test_first_error() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + match first(&[a_val(v_list(vec![]))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match first(&[a_val(v_str("not_array"))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match first(&[], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + } + + #[test] + fn test_last_success() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + let arr = v_list(vec![v_str("first"), v_str("second"), v_str("last")]); + assert_eq!(expect_str(last(&[a_val(arr)], &mut ctx, &mut run)), "last"); + } + + #[test] + fn test_last_error() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + match last(&[a_val(v_list(vec![]))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match last(&[a_val(v_str("not_array"))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + } + + // --- for_each / map ------------------------------------------------------ + #[test] + fn test_for_each_and_map() { + let mut ctx = ValueStore::default(); + let mut called = 0usize; + let mut run = |_, _ctx: &mut ValueStore| { + called += 1; + Signal::Success(Value { + kind: Some(Kind::NullValue(0)), + }) + }; + match for_each( + &[a_val(v_list(vec![v_num(1.0), v_num(2.0)])), a_thunk(1)], + &mut ctx, + &mut run, + ) { + Signal::Success(Value { + kind: Some(Kind::NullValue(_)), + }) => {} + x => panic!("expected NullValue, got {:?}", x), + } + assert_eq!(called, 2); + let transformed = v_list(vec![v_str("X"), v_str("Y")]); + let mut run = run_from_values(match transformed.kind.clone() { + Some(Kind::ListValue(ListValue { values })) => values, + _ => unreachable!(), + }); + let out = expect_list(map( + &[a_val(v_list(vec![v_num(1.0), v_num(2.0)])), a_thunk(2)], + &mut ctx, + &mut run, + )); + let expected = match transformed.kind { + Some(Kind::ListValue(ListValue { values })) => values, + _ => unreachable!(), + }; + assert_eq!(out, expected); + } + + #[test] + fn test_for_each_and_map_treat_callback_return_as_local_result() { + let mut ctx = ValueStore::default(); + let return_value = v_str("early_return"); + + let mut for_each_calls = 0usize; + let mut for_each_run = |_, _ctx: &mut ValueStore| { + for_each_calls += 1; + Signal::Return(return_value.clone()) + }; + let for_each_result = for_each( + &[a_val(v_list(vec![v_num(1.0), v_num(2.0)])), a_thunk(1)], + &mut ctx, + &mut for_each_run, + ); + match for_each_result { + Signal::Success(Value { + kind: Some(Kind::NullValue(_)), + }) => {} + other => panic!("expected Success(NullValue), got {:?}", other), + } + assert_eq!(for_each_calls, 2); + + let mut map_calls = 0usize; + let mut map_run = |_, _ctx: &mut ValueStore| { + map_calls += 1; + Signal::Return(return_value.clone()) + }; + let map_result = map( + &[a_val(v_list(vec![v_num(1.0), v_num(2.0)])), a_thunk(2)], + &mut ctx, + &mut map_run, + ); + match map_result { + Signal::Success(Value { + kind: Some(Kind::ListValue(ListValue { values })), + }) => { + assert_eq!(values, vec![return_value.clone(), return_value.clone()]); + } + other => panic!( + "expected Success([return_value, return_value]), got {:?}", + other + ), + } + assert_eq!(map_calls, 2); + } + + #[test] + fn test_filter_and_find_use_return_as_callback_value() { + let mut ctx = ValueStore::default(); + + let mut filter_index = 0usize; + let filter_returns = [true, false, true]; + let mut filter_run = |_, _ctx: &mut ValueStore| { + let out = filter_returns.get(filter_index).copied().unwrap_or(false); + filter_index += 1; + Signal::Return(v_bool(out)) + }; + let filtered = expect_list(filter( + &[ + a_val(v_list(vec![v_num(1.0), v_num(2.0), v_num(3.0)])), + a_thunk(1), + ], + &mut ctx, + &mut filter_run, + )); + assert_eq!(filtered, vec![v_num(1.0), v_num(3.0)]); + + let mut find_index = 0usize; + let find_returns = [false, true]; + let mut find_run = |_, _ctx: &mut ValueStore| { + let out = find_returns.get(find_index).copied().unwrap_or(false); + find_index += 1; + Signal::Return(v_bool(out)) + }; + let found = find( + &[a_val(v_list(vec![v_str("A"), v_str("B")])), a_thunk(2)], + &mut ctx, + &mut find_run, + ); + assert_eq!(expect_str(found), "B"); + } + + // --- push / pop / remove ------------------------------------------------- + #[test] + fn test_push_success() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + let out = expect_list(push( + &[ + a_val(v_list(vec![v_num(1.0), v_num(2.0)])), + a_val(v_num(3.0)), + ], + &mut ctx, + &mut run, + )); + assert_eq!(out.len(), 3); + assert_eq!(out[2].kind, k_num(3.0)); + } + + #[test] + fn test_push_error() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + match push(&[a_val(v_list(vec![v_num(1.0)]))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match push( + &[a_val(v_str("nope")), a_val(v_num(1.0))], + &mut ctx, + &mut run, + ) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + } + + #[test] + fn test_pop_success() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + let out = expect_list(pop( + &[a_val(v_list(vec![v_num(1.0), v_num(2.0), v_num(3.0)]))], + &mut ctx, + &mut run, + )); + assert_eq!(out.len(), 2); + assert_eq!(out[0].kind, k_num(1.0)); + assert_eq!(out[1].kind, k_num(2.0)); + } + + #[test] + fn test_pop_error() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + match pop(&[a_val(v_str("nope"))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match pop(&[], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + } + + #[test] + fn test_remove_success_and_error() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + // success + let arr = v_list(vec![v_str("first"), v_str("second"), v_str("third")]); + let out = expect_list(remove( + &[a_val(arr), a_val(v_str("second"))], + &mut ctx, + &mut run, + )); + assert_eq!(out.len(), 2); + assert_eq!(out[0].kind, Some(Kind::StringValue("first".into()))); + assert_eq!(out[1].kind, Some(Kind::StringValue("third".into()))); + // errors + match remove(&[a_val(v_list(vec![v_num(1.0)]))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match remove( + &[a_val(v_str("nope")), a_val(v_num(0.0))], + &mut ctx, + &mut run, + ) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match remove( + &[a_val(v_list(vec![v_num(1.0)])), a_val(v_num(999.0))], + &mut ctx, + &mut run, + ) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + } + + // --- is_empty / size ----------------------------------------------------- + #[test] + fn test_is_empty_and_size() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + assert!(expect_bool(is_empty( + &[a_val(v_list(vec![]))], + &mut ctx, + &mut run + ))); + assert!(!expect_bool(is_empty( + &[a_val(v_list(vec![v_num(1.0)]))], + &mut ctx, + &mut run + ))); + assert_eq!( + expect_num(size(&[a_val(v_list(vec![]))], &mut ctx, &mut run)), + 0.0 + ); + assert_eq!( + expect_num(size( + &[a_val(v_list(vec![v_num(1.0), v_num(2.0), v_num(3.0)]))], + &mut ctx, + &mut run + )), + 3.0 + ); + } + + #[test] + fn test_is_empty_error_and_size_error() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + match is_empty(&[a_val(v_str("nope"))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match is_empty(&[], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match size(&[a_val(v_str("nope"))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match size(&[], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + } + + // --- index_of / to_unique ------------------------------------------------ + #[test] + fn test_index_of_and_to_unique() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + let arr = v_list(vec![v_num(10.0), v_num(42.0), v_num(30.0), v_num(42.0)]); + assert_eq!( + expect_num(index_of( + &[a_val(arr.clone()), a_val(v_num(42.0))], + &mut ctx, + &mut run + )), + 1.0 + ); + match index_of( + &[a_val(arr.clone()), a_val(v_num(999.0))], + &mut ctx, + &mut run, + ) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + + let uniq = expect_list(to_unique(&[a_val(arr)], &mut ctx, &mut run)); + assert_eq!(uniq.len(), 3); + assert_eq!(uniq[0].kind, k_num(10.0)); + assert_eq!(uniq[1].kind, k_num(42.0)); + assert_eq!(uniq[2].kind, k_num(30.0)); + } + + #[test] + fn test_index_of_error() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + match index_of(&[a_val(v_list(vec![v_num(1.0)]))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match index_of( + &[a_val(v_str("nope")), a_val(v_num(1.0))], + &mut ctx, + &mut run, + ) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + } + + // --- sort / sort_reverse ------------------------------------------------- + #[test] + fn test_sort_and_sort_reverse() { + let mut ctx = ValueStore::default(); + + // We don't rely on actual values; ordering is driven by the comparator sequence. + let arr = v_list(vec![v_str("a"), v_str("b"), v_str("c"), v_str("d")]); + let comps = vec![v_num(-1.0), v_num(1.0), v_num(0.0), v_num(-1.0)]; + let mut run = run_from_values(comps.clone()); + let out = expect_list(sort(&[a_val(arr.clone()), a_thunk(1)], &mut ctx, &mut run)); + assert_eq!(out.len(), 4); + + let mut run = run_from_values(comps); + let out_r = expect_list(sort_reverse(&[a_val(arr), a_thunk(1)], &mut ctx, &mut run)); + assert_eq!(out_r.len(), 4); + } + + // --- reverse / flat ------------------------------------------------------ + #[test] + fn test_reverse_success_and_error() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + let out = expect_list(reverse( + &[a_val(v_list(vec![v_num(1.0), v_num(2.0), v_num(3.0)]))], + &mut ctx, + &mut run, + )); + assert_eq!(out[0].kind, k_num(3.0)); + assert_eq!(out[2].kind, k_num(1.0)); + + match reverse(&[a_val(v_str("nope"))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match reverse(&[], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + } + + #[test] + fn test_flat_success() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + let nested = v_list(vec![ + v_num(1.0), + v_list(vec![v_num(2.0), v_num(3.0)]), + v_list(vec![]), + v_num(4.0), + ]); + let out = expect_list(flat(&[a_val(nested)], &mut ctx, &mut run)); + assert_eq!(out.len(), 4); + assert_eq!(out[0].kind, k_num(1.0)); + assert_eq!(out[1].kind, k_num(2.0)); + assert_eq!(out[2].kind, k_num(3.0)); + assert_eq!(out[3].kind, k_num(4.0)); + } + + // --- min / max / sum ----------------------------------------------------- + #[test] + fn test_min_max_sum_success_and_error() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + let nums = v_list(vec![v_num(5.0), v_num(1.0), v_num(8.0), v_num(2.0)]); + assert_eq!( + expect_num(min(&[a_val(nums.clone())], &mut ctx, &mut run)), + 1.0 + ); + assert_eq!( + expect_num(max(&[a_val(nums.clone())], &mut ctx, &mut run)), + 8.0 + ); + assert_eq!(expect_num(sum(&[a_val(nums)], &mut ctx, &mut run)), 16.0); + + // empty + match min(&[a_val(v_list(vec![]))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match max(&[a_val(v_list(vec![]))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + assert_eq!( + expect_num(sum(&[a_val(v_list(vec![]))], &mut ctx, &mut run)), + 0.0 + ); + + // wrong type + match sum(&[a_val(v_str("nope"))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match min(&[a_val(v_str("nope"))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match max(&[a_val(v_str("nope"))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + } + + // --- join ---------------------------------------------------------------- + #[test] + fn test_join_success_and_error() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + let arr = v_list(vec![v_str("hello"), v_str("world"), v_str("test")]); + assert_eq!( + expect_str(join(&[a_val(arr), a_val(v_str(", "))], &mut ctx, &mut run)), + "hello, world, test" + ); + + // errors + let arr2 = v_list(vec![v_str("hello")]); + match join(&[a_val(arr2.clone())], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match join( + &[a_val(v_str("not_array")), a_val(v_str(","))], + &mut ctx, + &mut run, + ) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + match join(&[a_val(arr2), a_val(v_num(42.0))], &mut ctx, &mut run) { + Signal::Failure(_) => {} + x => panic!("{:?}", x), + } + } +} diff --git a/crates/taurus-core/src/runtime/functions/boolean.rs b/crates/taurus-core/src/runtime/functions/boolean.rs new file mode 100644 index 0000000..4c99f10 --- /dev/null +++ b/crates/taurus-core/src/runtime/functions/boolean.rs @@ -0,0 +1,414 @@ +//! Boolean standard-library handlers. +//! +//! This module keeps conversions explicit (`from_text`, `as_number`, ...) so flow behavior +//! stays predictable across local and remote execution targets. + +use crate::handler::argument::Argument; +use crate::handler::macros::args; +use crate::handler::registry::FunctionRegistration; +use crate::runtime::execution::value_store::ValueStore; +use crate::types::errors::runtime_error::RuntimeError; +use crate::types::signal::Signal; +use crate::value::value_from_i64; +use tucana::shared::{Value, value::Kind}; + +pub(crate) const FUNCTIONS: &[FunctionRegistration] = &[ + FunctionRegistration::eager("std::boolean::as_number", as_number, 1), + FunctionRegistration::eager("std::boolean::as_text", as_text, 1), + FunctionRegistration::eager("std::boolean::from_number", from_number, 1), + FunctionRegistration::eager("std::boolean::from_text", from_text, 1), + FunctionRegistration::eager("std::boolean::is_equal", is_equal, 2), + FunctionRegistration::eager("std::boolean::negate", negate, 1), +]; + +fn as_number( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: bool); + Signal::Success(value_from_i64(if value { 1 } else { 0 })) +} + +fn as_text( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: bool); + Signal::Success(Value { + kind: Some(Kind::StringValue(value.to_string())), + }) +} + +fn from_number( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => number: f64); + let is_zero = number == 0.0; + Signal::Success(Value { + kind: Some(Kind::BoolValue(!is_zero)), + }) +} + +fn from_text( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => text: String); + + // Keep parsing strict to Rust's bool format (`true` / `false`) after lowercase normalization. + match text.to_lowercase().parse::() { + Ok(b) => Signal::Success(Value { + kind: Some(Kind::BoolValue(b)), + }), + Err(_) => Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + format!("Failed to parse boolean from string: {:?}", text), + )), + } +} + +fn is_equal( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => lhs: bool, rhs: bool); + Signal::Success(Value { + kind: Some(Kind::BoolValue(lhs == rhs)), + }) +} + +fn negate( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: bool); + Signal::Success(Value { + kind: Some(Kind::BoolValue(!value)), + }) +} +#[cfg(test)] +mod tests { + use super::*; + use crate::runtime::execution::value_store::ValueStore; + use crate::value::{number_to_f64, value_from_f64}; + use tucana::shared::{Value, value::Kind}; + + // ---- helpers: make Arguments ---- + fn a_bool(b: bool) -> Argument { + Argument::Eval(Value { + kind: Some(Kind::BoolValue(b)), + }) + } + fn a_num(n: f64) -> Argument { + Argument::Eval(value_from_f64(n)) + } + fn a_str(s: &str) -> Argument { + Argument::Eval(Value { + kind: Some(Kind::StringValue(s.to_string())), + }) + } + + // ---- helpers: unwrap Signal ---- + fn expect_num(sig: Signal) -> f64 { + match sig { + Signal::Success(Value { + kind: Some(Kind::NumberValue(n)), + }) => number_to_f64(&n).unwrap_or_default(), + other => panic!("Expected NumberValue, got {:?}", other), + } + } + fn expect_str(sig: Signal) -> String { + match sig { + Signal::Success(Value { + kind: Some(Kind::StringValue(s)), + }) => s, + other => panic!("Expected StringValue, got {:?}", other), + } + } + fn expect_bool(sig: Signal) -> bool { + match sig { + Signal::Success(Value { + kind: Some(Kind::BoolValue(b)), + }) => b, + other => panic!("Expected BoolValue, got {:?}", other), + } + } + + // dummy `run` closure (unused by these handlers) + fn dummy_run(_: i64, _: &mut ValueStore) -> Signal { + Signal::Success(Value { + kind: Some(Kind::BoolValue(true)), + }) + } + + // ---- tests ---- + + #[test] + fn test_as_number_success() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + assert_eq!( + expect_num(as_number(&[a_bool(true)], &mut ctx, &mut run)), + 1.0 + ); + + let mut run = dummy_run; + assert_eq!( + expect_num(as_number(&[a_bool(false)], &mut ctx, &mut run)), + 0.0 + ); + } + + #[test] + fn test_as_number_errors() { + let mut ctx = ValueStore::default(); + + // wrong arity: none + let mut run = dummy_run; + match as_number(&[], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for arity 0, got {:?}", s), + } + + // wrong type + let mut run = dummy_run; + match as_number(&[a_num(1.0)], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for non-bool, got {:?}", s), + } + + // too many args + let mut run = dummy_run; + match as_number(&[a_bool(true), a_bool(false)], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for arity 2, got {:?}", s), + } + } + + #[test] + fn test_as_text_success() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert_eq!( + expect_str(as_text(&[a_bool(true)], &mut ctx, &mut run)), + "true" + ); + + let mut run = dummy_run; + assert_eq!( + expect_str(as_text(&[a_bool(false)], &mut ctx, &mut run)), + "false" + ); + } + + #[test] + fn test_as_text_errors() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + match as_text(&[], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for arity 0, got {:?}", s), + } + + let mut run = dummy_run; + match as_text(&[a_num(5.0)], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for non-bool, got {:?}", s), + } + + let mut run = dummy_run; + match as_text(&[a_bool(true), a_bool(false)], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for arity 2, got {:?}", s), + } + } + + #[test] + fn test_from_number_success() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert_eq!( + expect_bool(from_number(&[a_num(0.0)], &mut ctx, &mut run)), + false + ); + + let mut run = dummy_run; + assert_eq!( + expect_bool(from_number(&[a_num(3.5)], &mut ctx, &mut run)), + true + ); + + let mut run = dummy_run; + assert_eq!( + expect_bool(from_number(&[a_num(-2.0)], &mut ctx, &mut run)), + true + ); + + // -0.0 should behave like 0.0 + let mut run = dummy_run; + assert_eq!( + expect_bool(from_number(&[a_num(-0.0)], &mut ctx, &mut run)), + false + ); + } + + #[test] + fn test_from_number_errors() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + match from_number(&[], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for arity 0, got {:?}", s), + } + + let mut run = dummy_run; + match from_number(&[a_bool(true)], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for non-number, got {:?}", s), + } + + let mut run = dummy_run; + match from_number(&[a_num(1.0), a_num(2.0)], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for arity 2, got {:?}", s), + } + } + + #[test] + fn test_from_text_success_and_errors() { + let mut ctx = ValueStore::default(); + + // success (case-insensitive) + let mut run = dummy_run; + assert_eq!( + expect_bool(from_text(&[a_str("true")], &mut ctx, &mut run)), + true + ); + + let mut run = dummy_run; + assert_eq!( + expect_bool(from_text(&[a_str("FALSE")], &mut ctx, &mut run)), + false + ); + + // errors + let mut run = dummy_run; + match from_text(&[], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for arity 0, got {:?}", s), + } + + let mut run = dummy_run; + match from_text(&[a_str("yes")], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for unparseable bool, got {:?}", s), + } + + let mut run = dummy_run; + match from_text(&[a_num(1.0)], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for non-string, got {:?}", s), + } + + let mut run = dummy_run; + match from_text(&[a_str("true"), a_str("false")], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for arity 2, got {:?}", s), + } + } + + #[test] + fn test_is_equal_and_errors() { + let mut ctx = ValueStore::default(); + + // equalities + let mut run = dummy_run; + assert_eq!( + expect_bool(is_equal(&[a_bool(true), a_bool(true)], &mut ctx, &mut run)), + true + ); + + let mut run = dummy_run; + assert_eq!( + expect_bool(is_equal( + &[a_bool(false), a_bool(false)], + &mut ctx, + &mut run + )), + true + ); + + let mut run = dummy_run; + assert_eq!( + expect_bool(is_equal(&[a_bool(true), a_bool(false)], &mut ctx, &mut run)), + false + ); + + // arity/type errors + let mut run = dummy_run; + match is_equal(&[], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for arity 0, got {:?}", s), + } + + let mut run = dummy_run; + match is_equal(&[a_bool(true)], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for arity 1, got {:?}", s), + } + + let mut run = dummy_run; + match is_equal(&[a_bool(true), a_num(1.0)], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for non-bool rhs, got {:?}", s), + } + } + + #[test] + fn test_negate_success_and_errors() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert_eq!( + expect_bool(negate(&[a_bool(true)], &mut ctx, &mut run)), + false + ); + + let mut run = dummy_run; + assert_eq!( + expect_bool(negate(&[a_bool(false)], &mut ctx, &mut run)), + true + ); + + // errors + let mut run = dummy_run; + match negate(&[], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for arity 0, got {:?}", s), + } + + let mut run = dummy_run; + match negate(&[a_num(1.0)], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for non-bool, got {:?}", s), + } + + let mut run = dummy_run; + match negate(&[a_bool(true), a_bool(false)], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for arity 2, got {:?}", s), + } + } +} diff --git a/crates/taurus-core/src/runtime/functions/control.rs b/crates/taurus-core/src/runtime/functions/control.rs new file mode 100644 index 0000000..f6e976c --- /dev/null +++ b/crates/taurus-core/src/runtime/functions/control.rs @@ -0,0 +1,98 @@ +//! Control-flow handlers (`if`, `if_else`, `return`, `stop`). +//! +//! `if`/`if_else` execute branch nodes via runtime callbacks and forward their resulting signals. +//! This is required for block-style return semantics where `return` exits only the current call frame. + +use crate::handler::argument::Argument; +use crate::handler::argument::ParameterNode::{Eager, Lazy}; +use crate::handler::macros::args; +use crate::handler::registry::FunctionRegistration; +use crate::runtime::execution::value_store::ValueStore; +use crate::types::errors::runtime_error::RuntimeError; +use crate::types::signal::Signal; +use tucana::shared::Value; +use tucana::shared::value::Kind; + +pub(crate) const FUNCTIONS: &[FunctionRegistration] = &[ + FunctionRegistration::eager("std::control::stop", stop, 0), + FunctionRegistration::eager("std::control::return", r#return, 1), + FunctionRegistration::modes("std::control::if", r#if, &[Eager, Lazy]), + FunctionRegistration::modes("std::control::if_else", if_else, &[Eager, Lazy, Lazy]), +]; + +fn stop( + _args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + Signal::Stop +} + +fn r#return( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: Value); + // The executor decides how far this return unwinds (one frame). + Signal::Return(value) +} + +fn r#if( + args: &[Argument], + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + let [ + Argument::Eval(Value { + kind: Some(Kind::BoolValue(bool)), + }), + Argument::Thunk(if_pointer), + ] = args + else { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + format!("Expected a bool value but received {:?}", args), + )); + }; + + if *bool { + // Branch execution is delegated to the executor through `run`. + ctx.push_runtime_trace_label("branch=if".to_string()); + run(*if_pointer, ctx) + } else { + Signal::Success(Value { + kind: Some(Kind::NullValue(0)), + }) + } +} + +fn if_else( + args: &[Argument], + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + let [ + Argument::Eval(Value { + kind: Some(Kind::BoolValue(bool)), + }), + Argument::Thunk(if_pointer), + Argument::Thunk(else_pointer), + ] = args + else { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + format!("Expected a bool value but received {:?}", args), + )); + }; + + if *bool { + ctx.push_runtime_trace_label("branch=if".to_string()); + run(*if_pointer, ctx) + } else { + ctx.push_runtime_trace_label("branch=else".to_string()); + run(*else_pointer, ctx) + } +} diff --git a/crates/taurus-core/src/runtime/functions/http.rs b/crates/taurus-core/src/runtime/functions/http.rs new file mode 100644 index 0000000..012df77 --- /dev/null +++ b/crates/taurus-core/src/runtime/functions/http.rs @@ -0,0 +1,133 @@ +//! HTTP and REST-oriented helper handlers. +//! +//! These functions build/validate plain struct payloads that the runtime treats as regular values. + +use crate::handler::argument::Argument; +use crate::handler::macros::args; +use crate::handler::registry::FunctionRegistration; +use crate::runtime::execution::value_store::ValueStore; +use crate::types::errors::runtime_error::RuntimeError; +use crate::types::signal::Signal; +use tucana::shared::helper::value::ToValue; +use tucana::shared::value::Kind; +use tucana::shared::{Struct, Value}; + +pub(crate) const FUNCTIONS: &[FunctionRegistration] = &[ + FunctionRegistration::eager("http::request::create", create_request, 4), + FunctionRegistration::eager("http::response::create", create_response, 3), + FunctionRegistration::eager("rest::control::respond", respond, 1), +]; + +fn fail(category: &str, message: impl Into) -> Signal { + Signal::Failure(RuntimeError::new("T-HTTP-000000", category, message)) +} + +fn respond( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => struct_val: Struct); + + let fields = &struct_val.fields; + + let Some(headers_val) = fields.get("headers") else { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + "Missing 'headers' field".to_string(), + )); + }; + + let Some(status_code_val) = fields.get("http_status_code") else { + return fail( + "InvalidArgumentRuntimeError", + "Missing 'http_status_code' field", + ); + }; + + let Some(payload_val) = fields.get("payload") else { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + "Missing 'payload' field".to_string(), + )); + }; + + let Some(Kind::StructValue(_headers_struct)) = &headers_val.kind else { + return fail( + "InvalidArgumentRuntimeError", + "Expected 'headers' to be StructValue", + ); + }; + + let Some(Kind::NumberValue(_status_code_str)) = &status_code_val.kind else { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + "Expected 'status_code' to be NumberValue".to_string(), + )); + }; + + let Some(_payload_kind) = &payload_val.kind else { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + "Expected 'payload' to have a value".to_string(), + )); + }; + + // `Respond` is a control signal; the executor can still continue with `next` if present. + Signal::Respond(Value { + kind: Some(Kind::StructValue(struct_val.clone())), + }) +} + +fn create_request( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => http_method: String, headers: Struct, http_url: String, payload: Value); + let mut fields = std::collections::HashMap::new(); + + fields.insert(String::from("http_method"), http_method.to_value()); + fields.insert(String::from("url"), http_url.to_value()); + fields.insert(String::from("payload"), payload.clone()); + fields.insert( + String::from("headers"), + Value { + kind: Some(Kind::StructValue(headers.clone())), + }, + ); + + Signal::Success(Value { + kind: Some(Kind::StructValue(Struct { fields })), + }) +} + +fn create_response( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => http_status_code: i64, headers: Struct, payload: Value); + let mut fields = std::collections::HashMap::new(); + + fields.insert( + String::from("http_status_code"), + http_status_code.to_value(), + ); + fields.insert(String::from("payload"), payload.clone()); + + fields.insert( + String::from("headers"), + Value { + kind: Some(Kind::StructValue(headers.clone())), + }, + ); + + Signal::Success(Value { + kind: Some(Kind::StructValue(Struct { fields })), + }) +} diff --git a/crates/taurus-core/src/runtime/functions/mod.rs b/crates/taurus-core/src/runtime/functions/mod.rs new file mode 100644 index 0000000..2c9b16b --- /dev/null +++ b/crates/taurus-core/src/runtime/functions/mod.rs @@ -0,0 +1,23 @@ +//! Built-in runtime function catalog. +//! +//! Each submodule registers handler implementations under stable runtime ids. + +use crate::handler::registry::FunctionRegistration; + +mod array; +mod boolean; +mod control; +mod http; +mod number; +mod object; +mod text; + +pub const ALL_FUNCTION_SETS: &[&[FunctionRegistration]] = &[ + array::FUNCTIONS, + number::FUNCTIONS, + boolean::FUNCTIONS, + text::FUNCTIONS, + object::FUNCTIONS, + control::FUNCTIONS, + http::FUNCTIONS, +]; diff --git a/crates/taurus-core/src/runtime/functions/number.rs b/crates/taurus-core/src/runtime/functions/number.rs new file mode 100644 index 0000000..fc5f674 --- /dev/null +++ b/crates/taurus-core/src/runtime/functions/number.rs @@ -0,0 +1,1247 @@ +//! Numeric standard-library handlers. +//! +//! Most operators keep an integer fast-path (checked ops) and fall back to `f64` arithmetic +//! when needed so common integer-heavy flows avoid unnecessary float conversion. + +use std::f64; + +use tucana::shared::helper::value::ToValue; +use tucana::shared::{NumberValue, Value, number_value, value::Kind}; + +use crate::handler::argument::Argument; +use crate::handler::macros::{args, no_args}; +use crate::handler::registry::FunctionRegistration; +use crate::runtime::execution::value_store::ValueStore; +use crate::types::errors::runtime_error::RuntimeError; +use crate::types::signal::Signal; +use crate::value::{number_to_f64, number_to_i64_lossy, value_from_f64, value_from_i64}; + +fn num_f64(n: &NumberValue) -> Result { + // Centralized conversion keeps all numeric argument failures consistent. + number_to_f64(n).ok_or_else(|| { + Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + "Expected number", + )) + }) +} + +pub(crate) const FUNCTIONS: &[FunctionRegistration] = &[ + FunctionRegistration::eager("std::number::add", add, 2), + FunctionRegistration::eager("std::number::multiply", multiply, 2), + FunctionRegistration::eager("std::number::substract", substract, 2), + FunctionRegistration::eager("std::number::divide", divide, 2), + FunctionRegistration::eager("std::number::modulo", modulo, 2), + FunctionRegistration::eager("std::number::abs", abs, 1), + FunctionRegistration::eager("std::number::is_positive", is_positive, 1), + FunctionRegistration::eager("std::number::is_greater", is_greater, 2), + FunctionRegistration::eager("std::number::is_less", is_less, 2), + FunctionRegistration::eager("std::number::is_zero", is_zero, 1), + FunctionRegistration::eager("std::number::square", square, 2), + FunctionRegistration::eager("std::number::exponential", exponential, 2), + FunctionRegistration::eager("std::number::pi", pi, 0), + FunctionRegistration::eager("std::number::euler", euler, 0), + FunctionRegistration::eager("std::number::infinity", infinity, 0), + FunctionRegistration::eager("std::number::round_up", round_up, 2), + FunctionRegistration::eager("std::number::round_down", round_down, 2), + FunctionRegistration::eager("std::number::round", round, 2), + FunctionRegistration::eager("std::number::square_root", square_root, 1), + FunctionRegistration::eager("std::number::root", root, 2), + FunctionRegistration::eager("std::number::log", log, 2), + FunctionRegistration::eager("std::number::ln", ln, 1), + FunctionRegistration::eager("std::number::from_text", from_text, 1), + FunctionRegistration::eager("std::number::as_text", as_text, 1), + FunctionRegistration::eager("std::number::min", min, 2), + FunctionRegistration::eager("std::number::max", max, 2), + FunctionRegistration::eager("std::number::negate", negate, 1), + FunctionRegistration::eager("std::number::random_number", random, 2), + FunctionRegistration::eager("std::number::sin", sin, 1), + FunctionRegistration::eager("std::number::cos", cos, 1), + FunctionRegistration::eager("std::number::tan", tan, 1), + FunctionRegistration::eager("std::number::arcsin", arcsin, 1), + FunctionRegistration::eager("std::number::arccos", arccos, 1), + FunctionRegistration::eager("std::number::arctan", arctan, 1), + FunctionRegistration::eager("std::number::sinh", sinh, 1), + FunctionRegistration::eager("std::number::cosh", cosh, 1), + FunctionRegistration::eager("std::number::clamp", clamp, 3), + FunctionRegistration::eager("std::number::is_equal", is_equal, 2), + FunctionRegistration::eager("std::number::has_digits", has_digits, 2), + FunctionRegistration::eager("std::number::remove_digits", remove_digits, 2), +]; + +fn has_digits( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + + match value.number { + Some(number) => match number { + number_value::Number::Integer(_) => Signal::Success(false.to_value()), + number_value::Number::Float(_) => Signal::Success(true.to_value()), + }, + None => Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvlaidArgumentExeption", + "Had NumberValue but no inner number value (was null)", + )), + } +} + +fn remove_digits( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + match number_to_i64_lossy(&value) { + Some(number) => Signal::Success(value_from_i64(number)), + None => Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvlaidArgumentExeption", + "Had NumberValue but no inner number value (was null)", + )), + } +} + +fn add( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => lhs: NumberValue, rhs: NumberValue); + // Preserve integer precision and overflow checks when both operands are integers. + if let (Some(number_value::Number::Integer(a)), Some(number_value::Number::Integer(b))) = + (lhs.number, rhs.number) + && let Some(sum) = a.checked_add(b) + { + return Signal::Success(value_from_i64(sum)); + } + let lhs = match num_f64(&lhs) { + Ok(v) => v, + Err(e) => return e, + }; + let rhs = match num_f64(&rhs) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(lhs + rhs)) +} + +fn multiply( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => lhs: NumberValue, rhs: NumberValue); + if let (Some(number_value::Number::Integer(a)), Some(number_value::Number::Integer(b))) = + (lhs.number, rhs.number) + && let Some(prod) = a.checked_mul(b) + { + return Signal::Success(value_from_i64(prod)); + } + let lhs = match num_f64(&lhs) { + Ok(v) => v, + Err(e) => return e, + }; + let rhs = match num_f64(&rhs) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(lhs * rhs)) +} + +fn substract( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => lhs: NumberValue, rhs: NumberValue); + if let (Some(number_value::Number::Integer(a)), Some(number_value::Number::Integer(b))) = + (lhs.number, rhs.number) + && let Some(diff) = a.checked_sub(b) + { + return Signal::Success(value_from_i64(diff)); + } + let lhs = match num_f64(&lhs) { + Ok(v) => v, + Err(e) => return e, + }; + let rhs = match num_f64(&rhs) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(lhs - rhs)) +} + +fn divide( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => lhs: NumberValue, rhs: NumberValue); + + let rhs_f = match num_f64(&rhs) { + Ok(v) => v, + Err(e) => return e, + }; + + if rhs_f == 0.0 { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "DivisionByZero", + "You cannot divide by zero", + )); + } + + if let (Some(number_value::Number::Integer(a)), Some(number_value::Number::Integer(b))) = + (lhs.number, rhs.number) + && b != 0 + && a % b == 0 + { + return Signal::Success(value_from_i64(a / b)); + } + + let lhs_f = match num_f64(&lhs) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(lhs_f / rhs_f)) +} + +fn modulo( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => lhs: NumberValue, rhs: NumberValue); + + let rhs_f = match num_f64(&rhs) { + Ok(v) => v, + Err(e) => return e, + }; + + if rhs_f == 0.0 { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "DivisionByZero", + "You cannot divide by zero", + )); + } + + if let (Some(number_value::Number::Integer(a)), Some(number_value::Number::Integer(b))) = + (lhs.number, rhs.number) + && b != 0 + { + return Signal::Success(value_from_i64(a % b)); + } + + let lhs_f = match num_f64(&lhs) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(lhs_f % rhs_f)) +} + +fn abs( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + if let Some(number_value::Number::Integer(i)) = value.number + && let Some(abs) = i.checked_abs() + { + return Signal::Success(value_from_i64(abs)); + } + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(value.abs())) +} + +fn is_positive( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(Value { + kind: Some(Kind::BoolValue(!value.is_sign_negative())), + }) +} + +fn is_greater( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => lhs: NumberValue, rhs: NumberValue); + let lhs = match num_f64(&lhs) { + Ok(v) => v, + Err(e) => return e, + }; + let rhs = match num_f64(&rhs) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(Value { + kind: Some(Kind::BoolValue(lhs > rhs)), + }) +} + +fn is_less( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => lhs: NumberValue, rhs: NumberValue); + let lhs = match num_f64(&lhs) { + Ok(v) => v, + Err(e) => return e, + }; + let rhs = match num_f64(&rhs) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(Value { + kind: Some(Kind::BoolValue(lhs < rhs)), + }) +} + +fn is_zero( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(Value { + kind: Some(Kind::BoolValue(value == 0.0)), + }) +} + +fn square( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + if let Some(number_value::Number::Integer(i)) = value.number + && let Some(prod) = i.checked_mul(i) + { + return Signal::Success(value_from_i64(prod)); + } + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(value.powf(2.0))) +} + +fn exponential( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => base: NumberValue, exponent: NumberValue); + match (base.number, exponent.number) { + (Some(number_value::Number::Integer(b)), Some(number_value::Number::Integer(e))) + if e >= 0 => + { + if let Ok(exp) = u32::try_from(e) + && let Some(pow) = b.checked_pow(exp) + { + return Signal::Success(value_from_i64(pow)); + } + } + _ => {} + } + let base = match num_f64(&base) { + Ok(v) => v, + Err(e) => return e, + }; + let exponent = match num_f64(&exponent) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(base.powf(exponent))) +} + +fn pi( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + no_args!(args); + Signal::Success(value_from_f64(f64::consts::PI)) +} + +fn euler( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + no_args!(args); + Signal::Success(value_from_f64(f64::consts::E)) +} + +fn infinity( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + no_args!(args); + Signal::Success(value_from_f64(f64::INFINITY)) +} + +fn round_up( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue, decimal_places: NumberValue); + let decimal_places = match num_f64(&decimal_places) { + Ok(v) => v, + Err(e) => return e, + }; + match value.number { + Some(number_value::Number::Integer(i)) if decimal_places <= 0.0 => { + return Signal::Success(value_from_i64(i)); + } + _ => {} + } + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + let factor = 10_f64.powi(decimal_places as i32); + Signal::Success(value_from_f64((value * factor).ceil() / factor)) +} + +fn round_down( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue, decimal_places: NumberValue); + let decimal_places = match num_f64(&decimal_places) { + Ok(v) => v, + Err(e) => return e, + }; + match value.number { + Some(number_value::Number::Integer(i)) if decimal_places <= 0.0 => { + return Signal::Success(value_from_i64(i)); + } + _ => {} + } + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + let factor = 10_f64.powi(decimal_places as i32); + Signal::Success(value_from_f64((value * factor).floor() / factor)) +} + +fn round( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue, decimal_places: NumberValue); + let decimal_places = match num_f64(&decimal_places) { + Ok(v) => v, + Err(e) => return e, + }; + match value.number { + Some(number_value::Number::Integer(i)) if decimal_places <= 0.0 => { + return Signal::Success(value_from_i64(i)); + } + _ => {} + } + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + let factor = 10_f64.powi(decimal_places as i32); + Signal::Success(value_from_f64((value * factor).round() / factor)) +} + +fn square_root( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(value.sqrt())) +} + +fn root( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue, root: NumberValue); + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + let root = match num_f64(&root) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(value.powf(root))) +} + +fn log( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue, base: NumberValue); + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + let base = match num_f64(&base) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(value.log(base))) +} + +fn ln( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(value.ln())) +} + +fn from_text( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => string_value: String); + + if let Ok(v) = string_value.parse::() { + return Signal::Success(value_from_i64(v)); + } + match string_value.parse::() { + Ok(v) => Signal::Success(value_from_f64(v)), + Err(_) => Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + format!("Failed to parse string as number: {}", string_value), + )), + } +} + +fn as_text( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(Value { + kind: Some(Kind::StringValue(value.to_string())), + }) +} + +fn min( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => lhs: NumberValue, rhs: NumberValue); + if let (Some(number_value::Number::Integer(a)), Some(number_value::Number::Integer(b))) = + (lhs.number, rhs.number) + { + return Signal::Success(value_from_i64(a.min(b))); + } + let lhs = match num_f64(&lhs) { + Ok(v) => v, + Err(e) => return e, + }; + let rhs = match num_f64(&rhs) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(lhs.min(rhs))) +} + +fn max( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => lhs: NumberValue, rhs: NumberValue); + if let (Some(number_value::Number::Integer(a)), Some(number_value::Number::Integer(b))) = + (lhs.number, rhs.number) + { + return Signal::Success(value_from_i64(a.max(b))); + } + let lhs = match num_f64(&lhs) { + Ok(v) => v, + Err(e) => return e, + }; + let rhs = match num_f64(&rhs) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(lhs.max(rhs))) +} + +fn negate( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + if let Some(number_value::Number::Integer(i)) = value.number + && let Some(neg) = i.checked_neg() + { + return Signal::Success(value_from_i64(neg)); + } + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(-value)) +} + +fn random( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => min: NumberValue, max: NumberValue); + + let min_f = match num_f64(&min) { + Ok(v) => v, + Err(e) => return e, + }; + let max_f = match num_f64(&max) { + Ok(v) => v, + Err(e) => return e, + }; + + if min_f > max_f { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidRange", + "First number can't be bigger then second when creating a range for std::math::random", + )); + } + + let value = rand::random_range(min_f..=max_f); + + Signal::Success(value_from_f64(value)) +} + +fn sin( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(value.sin())) +} + +fn cos( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(value.cos())) +} + +fn tan( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(value.tan())) +} + +fn arcsin( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(value.asin())) +} + +fn arccos( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(value.acos())) +} + +fn arctan( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(value.atan())) +} + +fn sinh( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(value.sinh())) +} + +fn cosh( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue); + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(value.cosh())) +} + +fn clamp( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: NumberValue, min: NumberValue, max: NumberValue); + if let ( + Some(number_value::Number::Integer(v)), + Some(number_value::Number::Integer(min)), + Some(number_value::Number::Integer(max)), + ) = (value.number, min.number, max.number) + { + return Signal::Success(value_from_i64(v.clamp(min, max))); + } + let value = match num_f64(&value) { + Ok(v) => v, + Err(e) => return e, + }; + let min = match num_f64(&min) { + Ok(v) => v, + Err(e) => return e, + }; + let max = match num_f64(&max) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(value_from_f64(value.clamp(min, max))) +} + +fn is_equal( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => lhs: NumberValue, rhs: NumberValue); + let lhs = match num_f64(&lhs) { + Ok(v) => v, + Err(e) => return e, + }; + let rhs = match num_f64(&rhs) { + Ok(v) => v, + Err(e) => return e, + }; + Signal::Success(Value { + kind: Some(Kind::BoolValue(lhs == rhs)), + }) +} +#[cfg(test)] +mod tests { + use super::*; + use crate::handler::argument::Argument; + use crate::runtime::execution::value_store::ValueStore; + use crate::value::{number_to_f64, value_from_f64, value_from_i64}; + use tucana::shared::{Value, number_value, value::Kind}; + + // ---- helpers: Arguments ---- + fn a_num(n: f64) -> Argument { + Argument::Eval(value_from_f64(n)) + } + fn a_int(n: i64) -> Argument { + Argument::Eval(value_from_i64(n)) + } + fn a_str(s: &str) -> Argument { + Argument::Eval(Value { + kind: Some(Kind::StringValue(s.to_string())), + }) + } + + // ---- helpers: extractors ---- + fn expect_num(sig: Signal) -> f64 { + match sig { + Signal::Success(Value { + kind: Some(Kind::NumberValue(n)), + }) => number_to_f64(&n).unwrap_or_default(), + other => panic!("Expected NumberValue, got {:?}", other), + } + } + fn expect_bool(sig: Signal) -> bool { + match sig { + Signal::Success(Value { + kind: Some(Kind::BoolValue(b)), + }) => b, + other => panic!("Expected BoolValue, got {:?}", other), + } + } + fn expect_int(sig: Signal) -> i64 { + match sig { + Signal::Success(Value { + kind: Some(Kind::NumberValue(n)), + }) => match n.number { + Some(number_value::Number::Integer(i)) => i, + Some(number_value::Number::Float(f)) => { + panic!("Expected Integer NumberValue, got Float({})", f) + } + None => panic!("Expected Integer NumberValue, got None"), + }, + other => panic!("Expected NumberValue, got {:?}", other), + } + } + fn expect_str(sig: Signal) -> String { + match sig { + Signal::Success(Value { + kind: Some(Kind::StringValue(s)), + }) => s, + other => panic!("Expected StringValue, got {:?}", other), + } + } + + // dummy runner for handlers that accept `run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal` + fn dummy_run(_: i64, _: &mut ValueStore) -> Signal { + Signal::Success(Value { + kind: Some(Kind::NullValue(0)), + }) + } + + #[test] + fn test_add_and_multiply() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + assert_eq!( + expect_num(add(&[a_num(5.0), a_num(3.0)], &mut ctx, &mut run)), + 8.0 + ); + + let mut run = dummy_run; + assert_eq!( + expect_num(multiply(&[a_num(4.0), a_num(2.5)], &mut ctx, &mut run)), + 10.0 + ); + } + + #[test] + fn test_has_digits_and_remove_digits() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert!(!expect_bool(has_digits(&[a_int(42)], &mut ctx, &mut run))); + + let mut run = dummy_run; + assert!(expect_bool(has_digits(&[a_num(42.5)], &mut ctx, &mut run))); + + let mut run = dummy_run; + assert_eq!( + expect_int(remove_digits(&[a_int(123)], &mut ctx, &mut run)), + 123 + ); + + let mut run = dummy_run; + assert_eq!( + expect_int(remove_digits(&[a_num(12.99)], &mut ctx, &mut run)), + 12 + ); + } + + #[test] + fn test_substract_and_divide() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert_eq!( + expect_num(substract(&[a_num(10.0), a_num(4.0)], &mut ctx, &mut run)), + 6.0 + ); + + let mut run = dummy_run; + assert_eq!( + expect_num(divide(&[a_num(15.0), a_num(3.0)], &mut ctx, &mut run)), + 5.0 + ); + + // divide by zero -> Failure + let mut run = dummy_run; + match divide(&[a_num(10.0), a_num(0.0)], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure on divide by zero, got {:?}", s), + } + } + + #[test] + fn test_modulo_and_abs() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert_eq!( + expect_num(modulo(&[a_num(10.0), a_num(3.0)], &mut ctx, &mut run)), + 1.0 + ); + + // modulo by zero -> Failure + let mut run = dummy_run; + match modulo(&[a_num(10.0), a_num(0.0)], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure on modulo by zero, got {:?}", s), + } + + let mut run = dummy_run; + assert_eq!(expect_num(abs(&[a_num(-7.5)], &mut ctx, &mut run)), 7.5); + } + + #[test] + fn test_comparisons_and_zero() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert!(expect_bool(is_positive(&[a_num(5.0)], &mut ctx, &mut run))); + let mut run = dummy_run; + assert!(!expect_bool(is_positive( + &[a_num(-1.0)], + &mut ctx, + &mut run + ))); + let mut run = dummy_run; + assert!(expect_bool(is_positive(&[a_num(0.0)], &mut ctx, &mut run))); + + let mut run = dummy_run; + assert!(expect_bool(is_greater( + &[a_num(10.0), a_num(5.0)], + &mut ctx, + &mut run + ))); + let mut run = dummy_run; + assert!(expect_bool(is_less( + &[a_num(3.0), a_num(7.0)], + &mut ctx, + &mut run + ))); + + let mut run = dummy_run; + assert!(expect_bool(is_zero(&[a_num(0.0)], &mut ctx, &mut run))); + let mut run = dummy_run; + assert!(!expect_bool(is_zero(&[a_num(0.01)], &mut ctx, &mut run))); + } + + #[test] + fn test_powers_and_exponential() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert_eq!(expect_num(square(&[a_num(4.0)], &mut ctx, &mut run)), 16.0); + + let mut run = dummy_run; + assert_eq!( + expect_num(exponential(&[a_num(2.0), a_num(3.0)], &mut ctx, &mut run)), + 8.0 + ); + } + + #[test] + fn test_constants() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert!( + (expect_num(pi(&[], &mut ctx, &mut run)) - std::f64::consts::PI).abs() < f64::EPSILON + ); + + let mut run = dummy_run; + assert!( + (expect_num(euler(&[], &mut ctx, &mut run)) - std::f64::consts::E).abs() < f64::EPSILON + ); + + let mut run = dummy_run; + let inf = expect_num(infinity(&[], &mut ctx, &mut run)); + assert!(inf.is_infinite() && inf.is_sign_positive()); + } + + #[test] + fn test_rounding() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert_eq!( + expect_num(round_up( + &[a_num(f64::consts::PI), a_num(2.0)], + &mut ctx, + &mut run + )), + 3.15 + ); + + let mut run = dummy_run; + assert_eq!( + expect_num(round_down( + &[a_num(f64::consts::PI), a_num(2.0)], + &mut ctx, + &mut run + )), + 3.14 + ); + + let mut run = dummy_run; + assert_eq!( + expect_num(round(&[a_num(3.145), a_num(2.0)], &mut ctx, &mut run)), + 3.15 + ); + } + + #[test] + fn test_roots_and_logs() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert_eq!( + expect_num(square_root(&[a_num(16.0)], &mut ctx, &mut run)), + 4.0 + ); + + // cube root via exponent 1/3 + let mut run = dummy_run; + let r = expect_num(root(&[a_num(8.0), a_num(1.0 / 3.0)], &mut ctx, &mut run)); + assert!((r - 2.0).abs() < 1e-6); + + let mut run = dummy_run; + let lg = expect_num(log(&[a_num(100.0), a_num(10.0)], &mut ctx, &mut run)); + assert!((lg - 2.0).abs() < f64::EPSILON); + + let mut run = dummy_run; + let ln1 = expect_num(ln(&[a_num(f64::consts::E)], &mut ctx, &mut run)); + assert!((ln1 - 1.0).abs() < f64::EPSILON); + } + + #[test] + fn test_text_conversions() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert_eq!( + expect_num(from_text(&[a_str("42.5")], &mut ctx, &mut run)), + 42.5 + ); + + let mut run = dummy_run; + assert_eq!( + expect_str(as_text(&[a_num(42.5)], &mut ctx, &mut run)), + "42.5".to_string() + ); + + // from_text failure + let mut run = dummy_run; + match from_text(&[a_str("not_a_number")], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for invalid parse, got {:?}", s), + } + } + + #[test] + fn test_min_max_and_negate() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert_eq!( + expect_num(min(&[a_num(3.0), a_num(7.0)], &mut ctx, &mut run)), + 3.0 + ); + + let mut run = dummy_run; + assert_eq!( + expect_num(max(&[a_num(3.0), a_num(7.0)], &mut ctx, &mut run)), + 7.0 + ); + + let mut run = dummy_run; + assert_eq!(expect_num(negate(&[a_num(5.0)], &mut ctx, &mut run)), -5.0); + } + + #[test] + fn test_random_range() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + let r = expect_num(random(&[a_num(1.0), a_num(10.0)], &mut ctx, &mut run)); + assert!(r >= 1.0 && r < 10.0); + } + + #[test] + fn test_random_range_numbers_equal() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + let r = expect_num(random(&[a_num(1.0), a_num(1.0)], &mut ctx, &mut run)); + assert!(r == 1.0); + } + + #[test] + fn test_random_range_fist_bigger_then_second() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + let res = random(&[a_num(10.0), a_num(1.0)], &mut ctx, &mut run); + assert!(matches!(res, Signal::Failure(_))); + } + + #[test] + fn test_trig_and_hyperbolic() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + let s = expect_num(sin(&[a_num(f64::consts::PI / 2.0)], &mut ctx, &mut run)); + assert!((s - 1.0).abs() < 1e-12); + + let mut run = dummy_run; + let c = expect_num(cos(&[a_num(0.0)], &mut ctx, &mut run)); + assert!((c - 1.0).abs() < 1e-12); + + let mut run = dummy_run; + let t = expect_num(tan(&[a_num(f64::consts::PI / 4.0)], &mut ctx, &mut run)); + assert!((t - 1.0).abs() < 1e-4); + + let mut run = dummy_run; + let asn = expect_num(arcsin(&[a_num(1.0)], &mut ctx, &mut run)); + assert!((asn - f64::consts::PI / 2.0).abs() < 1e-12); + + let mut run = dummy_run; + let acs = expect_num(arccos(&[a_num(1.0)], &mut ctx, &mut run)); + assert!(acs.abs() < 1e-12); + + let mut run = dummy_run; + let atn = expect_num(arctan(&[a_num(1.0)], &mut ctx, &mut run)); + assert!((atn - f64::consts::PI / 4.0).abs() < 1e-12); + + let mut run = dummy_run; + let sh = expect_num(sinh(&[a_num(0.0)], &mut ctx, &mut run)); + assert!(sh.abs() < 1e-12); + + let mut run = dummy_run; + let ch = expect_num(cosh(&[a_num(0.0)], &mut ctx, &mut run)); + assert!((ch - 1.0).abs() < 1e-12); + } + + #[test] + fn test_clamp_and_is_equal() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert_eq!( + expect_num(clamp( + &[a_num(5.0), a_num(1.0), a_num(10.0)], + &mut ctx, + &mut run + )), + 5.0 + ); + + let mut run = dummy_run; + assert_eq!( + expect_num(clamp( + &[a_num(-5.0), a_num(1.0), a_num(10.0)], + &mut ctx, + &mut run + )), + 1.0 + ); + + let mut run = dummy_run; + assert_eq!( + expect_num(clamp( + &[a_num(15.0), a_num(1.0), a_num(10.0)], + &mut ctx, + &mut run + )), + 10.0 + ); + + let mut run = dummy_run; + assert!(expect_bool(is_equal( + &[a_num(5.0), a_num(5.0)], + &mut ctx, + &mut run + ))); + + let mut run = dummy_run; + assert!(!expect_bool(is_equal( + &[a_num(5.0), a_num(3.0)], + &mut ctx, + &mut run + ))); + } +} diff --git a/crates/taurus-core/src/runtime/functions/object.rs b/crates/taurus-core/src/runtime/functions/object.rs new file mode 100644 index 0000000..eccf995 --- /dev/null +++ b/crates/taurus-core/src/runtime/functions/object.rs @@ -0,0 +1,415 @@ +//! Object/struct utility handlers. +//! +//! `keys` returns sorted field names to keep downstream list operations deterministic. + +use tucana::shared::{Struct, Value, value::Kind}; + +use crate::handler::argument::Argument; +use crate::handler::macros::args; +use crate::handler::registry::FunctionRegistration; +use crate::runtime::execution::value_store::ValueStore; +use crate::types::signal::Signal; +use crate::value::value_from_i64; + +pub(crate) const FUNCTIONS: &[FunctionRegistration] = &[ + FunctionRegistration::eager("std::object::contains_key", contains_key, 2), + FunctionRegistration::eager("std::object::keys", keys, 1), + FunctionRegistration::eager("std::object::size", size, 1), + FunctionRegistration::eager("std::object::set", set, 3), +]; + +fn contains_key( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => object: Struct, key: String); + let contains = object.fields.contains_key(&key); + + Signal::Success(Value { + kind: Some(Kind::BoolValue(contains)), + }) +} + +fn size( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => object: Struct); + Signal::Success(value_from_i64(object.fields.len() as i64)) +} + +fn keys( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => object: Struct); + + // Stable ordering is important for reproducible traces and tests. + let mut keys = object.fields.keys().cloned().collect::>(); + keys.sort(); + + let keys = keys + .into_iter() + .map(|key| Value { + kind: Some(Kind::StringValue(key.clone())), + }) + .collect::>(); + + Signal::Success(Value { + kind: Some(Kind::ListValue(tucana::shared::ListValue { values: keys })), + }) +} + +fn set( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => object: Struct, key: String, value: Value); + let mut new_object = object.clone(); + new_object.fields.insert(key.clone(), value.clone()); + + Signal::Success(Value { + kind: Some(Kind::StructValue(new_object)), + }) +} +#[cfg(test)] +mod tests { + use super::*; + use crate::handler::argument::Argument; + use crate::runtime::execution::value_store::ValueStore; + use crate::value::{number_to_f64, value_from_f64}; + use std::collections::HashMap; + use tucana::shared::{Struct as TcStruct, Value, value::Kind}; + + // ---- helpers: Value builders ---- + fn v_string(s: &str) -> Value { + Value { + kind: Some(Kind::StringValue(s.to_string())), + } + } + fn v_number(n: f64) -> Value { + value_from_f64(n) + } + fn v_bool(b: bool) -> Value { + Value { + kind: Some(Kind::BoolValue(b)), + } + } + fn v_struct(fields: HashMap) -> Value { + Value { + kind: Some(Kind::StructValue(TcStruct { fields })), + } + } + + // ---- helpers: Struct builders (for args that expect Struct) ---- + fn s_empty() -> TcStruct { + TcStruct { + fields: HashMap::new(), + } + } + fn s_from(mut kv: Vec<(&str, Value)>) -> TcStruct { + let mut map = HashMap::::new(); + for (k, v) in kv.drain(..) { + map.insert(k.to_string(), v); + } + TcStruct { fields: map } + } + fn s_test() -> TcStruct { + s_from(vec![ + ("name", v_string("John")), + ("age", v_number(30.0)), + ("active", v_bool(true)), + ]) + } + + // ---- helpers: Argument builders ---- + #[allow(dead_code)] + fn a_value(v: Value) -> Argument { + Argument::Eval(v) + } + fn a_string(s: &str) -> Argument { + Argument::Eval(Value { + kind: Some(Kind::StringValue(s.to_string())), + }) + } + fn a_struct(s: TcStruct) -> Argument { + Argument::Eval(Value { + kind: Some(Kind::StructValue(s)), + }) + } + + // dummy runner for handlers that accept `run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal` + fn dummy_run(_: i64, _: &mut ValueStore) -> Signal { + Signal::Success(Value { + kind: Some(Kind::NullValue(0)), + }) + } + + #[test] + fn test_contains_key_success() { + let mut ctx = ValueStore::default(); + + // existing key + let mut run = dummy_run; + let args = vec![a_struct(s_test()), a_string("name")]; + let signal = contains_key(&args, &mut ctx, &mut run); + let v = match signal { + Signal::Success(v) => v, + _ => panic!("Expected Success"), + }; + match v.kind { + Some(Kind::BoolValue(b)) => assert!(b), + _ => panic!("Expected BoolValue"), + } + + // non-existing key + let mut run = dummy_run; + let args = vec![a_struct(s_test()), a_string("nonexistent")]; + let signal = contains_key(&args, &mut ctx, &mut run); + let v = match signal { + Signal::Success(v) => v, + _ => panic!("Expected Success"), + }; + match v.kind { + Some(Kind::BoolValue(b)) => assert!(!b), + _ => panic!("Expected BoolValue"), + } + + // empty object + let mut run = dummy_run; + let args = vec![a_struct(s_empty()), a_string("any_key")]; + let signal = contains_key(&args, &mut ctx, &mut run); + let v = match signal { + Signal::Success(v) => v, + _ => panic!("Expected Success"), + }; + match v.kind { + Some(Kind::BoolValue(b)) => assert!(!b), + _ => panic!("Expected BoolValue"), + } + } + + #[test] + fn test_size_success() { + let mut ctx = ValueStore::default(); + + // non-empty object + let mut run = dummy_run; + let args = vec![a_struct(s_test())]; + let signal = size(&args, &mut ctx, &mut run); + let v = match signal { + Signal::Success(v) => v, + _ => panic!("Expected Success"), + }; + match v.kind { + Some(Kind::NumberValue(n)) => assert_eq!(number_to_f64(&n).unwrap_or_default(), 3.0), + _ => panic!("Expected NumberValue"), + } + + // empty object + let mut run = dummy_run; + let args = vec![a_struct(s_empty())]; + let signal = size(&args, &mut ctx, &mut run); + let v = match signal { + Signal::Success(v) => v, + _ => panic!("Expected Success"), + }; + match v.kind { + Some(Kind::NumberValue(n)) => assert_eq!(number_to_f64(&n).unwrap_or_default(), 0.0), + _ => panic!("Expected NumberValue"), + } + } + + #[test] + fn test_keys_success() { + let mut ctx = ValueStore::default(); + + // with fields + let mut run = dummy_run; + let args = vec![a_struct(s_test())]; + let signal = keys(&args, &mut ctx, &mut run); + let v = match signal { + Signal::Success(v) => v, + _ => panic!("Expected Success"), + }; + match v.kind { + Some(Kind::ListValue(list)) => { + let mut got: Vec = list + .values + .iter() + .filter_map(|v| { + if let Some(Kind::StringValue(s)) = &v.kind { + Some(s.clone()) + } else { + None + } + }) + .collect(); + got.sort(); + + let mut expected = + vec!["active".to_string(), "age".to_string(), "name".to_string()]; + expected.sort(); + assert_eq!(got, expected); + } + _ => panic!("Expected ListValue"), + } + + // empty object => empty list + let mut run = dummy_run; + let args = vec![a_struct(s_empty())]; + let signal = keys(&args, &mut ctx, &mut run); + let v = match signal { + Signal::Success(v) => v, + _ => panic!("Expected Success"), + }; + match v.kind { + Some(Kind::ListValue(list)) => assert_eq!(list.values.len(), 0), + _ => panic!("Expected ListValue"), + } + } + + #[test] + fn test_set_success_and_overwrite() { + let mut ctx = ValueStore::default(); + + // set new key + let mut run = dummy_run; + let args = vec![ + a_struct(s_test()), + a_string("email"), + Argument::Eval(v_string("john@example.com")), + ]; + let signal = set(&args, &mut ctx, &mut run); + let v = match signal { + Signal::Success(v) => v, + _ => panic!("Expected Success"), + }; + match v.kind { + Some(Kind::StructValue(st)) => { + assert_eq!(st.fields.len(), 4); + match st.fields.get("email") { + Some(Value { + kind: Some(Kind::StringValue(s)), + .. + }) => assert_eq!(s, "john@example.com"), + _ => panic!("Expected email to be a string"), + } + } + _ => panic!("Expected StructValue"), + } + + // overwrite existing key + let mut run = dummy_run; + let args = vec![ + a_struct(s_test()), + a_string("age"), + Argument::Eval(v_number(31.0)), + ]; + let signal = set(&args, &mut ctx, &mut run); + let v = match signal { + Signal::Success(v) => v, + _ => panic!("Expected Success"), + }; + match v.kind { + Some(Kind::StructValue(st)) => { + assert_eq!(st.fields.len(), 3); + match st.fields.get("age") { + Some(Value { + kind: Some(Kind::NumberValue(n)), + .. + }) => assert_eq!(number_to_f64(n).unwrap_or_default(), 31.0), + _ => panic!("Expected age to be a number"), + } + } + _ => panic!("Expected StructValue"), + } + } + + #[test] + fn test_set_with_empty_object_and_nested() { + let mut ctx = ValueStore::default(); + + // empty object -> add first key + let mut run = dummy_run; + let args = vec![ + a_struct(s_empty()), + a_string("first_key"), + Argument::Eval(v_bool(true)), + ]; + let signal = set(&args, &mut ctx, &mut run); + let v = match signal { + Signal::Success(v) => v, + _ => panic!("Expected Success"), + }; + match v.kind { + Some(Kind::StructValue(st)) => { + assert_eq!(st.fields.len(), 1); + match st.fields.get("first_key") { + Some(Value { + kind: Some(Kind::BoolValue(b)), + .. + }) => assert_eq!(*b, true), + _ => panic!("Expected first_key to be a bool"), + } + } + _ => panic!("Expected StructValue"), + } + + // nested object value + let nested = { + let mut nf = HashMap::new(); + nf.insert("street".to_string(), v_string("123 Main St")); + v_struct(nf) + }; + let mut run = dummy_run; + let args = vec![ + a_struct(s_test()), + a_string("address"), + Argument::Eval(nested), + ]; + let signal = set(&args, &mut ctx, &mut run); + let v = match signal { + Signal::Success(v) => v, + _ => panic!("Expected Success"), + }; + match v.kind { + Some(Kind::StructValue(st)) => { + match st.fields.get("address") { + Some(Value { + kind: Some(Kind::StructValue(_)), + .. + }) => { /* ok */ } + _ => panic!("Expected address to be a struct"), + } + } + _ => panic!("Expected StructValue"), + } + } + + #[test] + fn test_set_preserves_original_struct() { + let mut ctx = ValueStore::default(); + let original = s_test(); + let original_len = original.fields.len(); + + // keep a clone to assert immutability + let orig_clone = original.clone(); + + let mut run = dummy_run; + let args = vec![ + a_struct(original), + a_string("new_key"), + Argument::Eval(v_string("new_val")), + ]; + let _ = set(&args, &mut ctx, &mut run); + + // ensure original (captured clone) unchanged + assert_eq!(orig_clone.fields.len(), original_len); + assert!(!orig_clone.fields.contains_key("new_key")); + } +} diff --git a/crates/taurus-core/src/runtime/functions/text.rs b/crates/taurus-core/src/runtime/functions/text.rs new file mode 100644 index 0000000..4ff0930 --- /dev/null +++ b/crates/taurus-core/src/runtime/functions/text.rs @@ -0,0 +1,1004 @@ +//! Text/string standard-library handlers. +//! +//! Index semantics are explicit per operation: +//! - character-based for access-oriented operations like `at` +//! - byte-based where direct string insertion/removal APIs are used +//! (to preserve historical runtime behavior) + +use crate::handler::argument::Argument; +use crate::handler::macros::args; +use crate::handler::registry::FunctionRegistration; +use crate::runtime::execution::value_store::ValueStore; +use crate::types::errors::runtime_error::RuntimeError; +use crate::types::signal::Signal; +use crate::value::{number_to_f64, number_to_i64_lossy, value_from_i64}; +use base64::Engine; +use tucana::shared::{ListValue, Value, value::Kind}; + +pub(crate) const FUNCTIONS: &[FunctionRegistration] = &[ + FunctionRegistration::eager("std::text::as_bytes", as_bytes, 1), + FunctionRegistration::eager("std::text::byte_size", byte_size, 1), + FunctionRegistration::eager("std::text::capitalize", capitalize, 1), + FunctionRegistration::eager("std::text::lowercase", lowercase, 1), + FunctionRegistration::eager("std::text::uppercase", uppercase, 1), + FunctionRegistration::eager("std::text::swapcase", swapcase, 1), + FunctionRegistration::eager("std::text::trim", trim, 1), + FunctionRegistration::eager("std::text::chars", chars, 1), + FunctionRegistration::eager("std::text::at", at, 2), + FunctionRegistration::eager("std::text::append", append, 2), + FunctionRegistration::eager("std::text::prepend", prepend, 2), + FunctionRegistration::eager("std::text::insert", insert, 3), + FunctionRegistration::eager("std::text::length", length, 1), + FunctionRegistration::eager("std::text::reverse", reverse, 1), + FunctionRegistration::eager("std::text::remove", remove, 3), + FunctionRegistration::eager("std::text::replace", replace, 3), + FunctionRegistration::eager("std::text::replace_first", replace_first, 3), + FunctionRegistration::eager("std::text::replace_last", replace_last, 3), + FunctionRegistration::eager("std::text::hex", hex, 1), + FunctionRegistration::eager("std::text::octal", octal, 1), + FunctionRegistration::eager("std::text::index_of", index_of, 2), + FunctionRegistration::eager("std::text::contains", contains, 2), + FunctionRegistration::eager("std::text::split", split, 2), + FunctionRegistration::eager("std::text::starts_with", starts_with, 2), + FunctionRegistration::eager("std::text::ends_with", ends_with, 2), + FunctionRegistration::eager("std::text::to_ascii", to_ascii, 1), + FunctionRegistration::eager("std::text::from_ascii", from_ascii, 1), + FunctionRegistration::eager("std::text::encode", encode, 2), + FunctionRegistration::eager("std::text::decode", decode, 2), + FunctionRegistration::eager("std::text::is_equal", is_equal, 2), +]; + +fn arg_err>(msg: S) -> Signal { + Signal::Failure(RuntimeError::new( + "T-RT-000000", + "InvalidArgumentRuntimeError", + msg.into(), + )) +} + +fn as_bytes( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String); + + let bytes: Vec = value + .as_bytes() + .iter() + .map(|b| value_from_i64(*b as i64)) + .collect(); + + Signal::Success(Value { + kind: Some(Kind::ListValue(ListValue { values: bytes })), + }) +} + +fn byte_size( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String); + Signal::Success(value_from_i64(value.len() as i64)) +} + +fn capitalize( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String); + + let capitalized = value + .split(' ') + .map(|word| { + if word.is_empty() { + return String::from(word); + } + let mut chars = word.chars(); + match chars.next() { + Some(first) => first.to_uppercase().collect::() + chars.as_str(), + None => String::from(word), + } + }) + .collect::>() + .join(" "); + + Signal::Success(Value { + kind: Some(Kind::StringValue(capitalized)), + }) +} + +fn uppercase( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String); + Signal::Success(Value { + kind: Some(Kind::StringValue(value.to_uppercase())), + }) +} + +fn lowercase( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String); + Signal::Success(Value { + kind: Some(Kind::StringValue(value.to_lowercase())), + }) +} + +fn swapcase( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String); + + let swapped = value + .chars() + .map(|c| { + if c.is_uppercase() { + c.to_lowercase().collect::() + } else if c.is_lowercase() { + c.to_uppercase().collect::() + } else { + c.to_string() + } + }) + .collect::(); + + Signal::Success(Value { + kind: Some(Kind::StringValue(swapped)), + }) +} + +fn trim( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String); + Signal::Success(Value { + kind: Some(Kind::StringValue(value.trim().to_string())), + }) +} + +fn chars( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String); + + let list = value + .chars() + .map(|c| Value { + kind: Some(Kind::StringValue(c.to_string())), + }) + .collect::>(); + + Signal::Success(Value { + kind: Some(Kind::ListValue(ListValue { values: list })), + }) +} + +fn at( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String, index: tucana::shared::NumberValue); + let index = match number_to_i64_lossy(&index) { + Some(v) => v, + None => return arg_err("Expected a number index"), + }; + + if index < 0 { + return arg_err("Expected a non-negative index"); + } + + let idx = index as usize; + match value.chars().nth(idx) { + Some(c) => Signal::Success(Value { + kind: Some(Kind::StringValue(c.to_string())), + }), + None => Signal::Failure(RuntimeError::new( + "T-RT-000000", + "IndexOutOfBoundsRuntimeError", + format!( + "Index {} is out of bounds for string of length {}", + index, + value.chars().count() + ), + )), + } +} + +fn append( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String, suffix: String); + Signal::Success(Value { + kind: Some(Kind::StringValue(value + &suffix)), + }) +} + +fn prepend( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String, prefix: String); + Signal::Success(Value { + kind: Some(Kind::StringValue(prefix + &value)), + }) +} + +fn insert( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String, position: tucana::shared::NumberValue, text: String); + let position = match number_to_i64_lossy(&position) { + Some(v) => v, + None => return arg_err("Expected a number position"), + }; + + if position < 0 { + return arg_err("Expected a non-negative position"); + } + + let pos = position as usize; + // Byte-wise position is kept intentionally to match existing flow behavior. + if pos > value.len() { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "IndexOutOfBoundsRuntimeError", + format!("Position {} exceeds byte length {}", pos, value.len()), + )); + } + + let mut new_value = value; + new_value.insert_str(pos, &text); + + Signal::Success(Value { + kind: Some(Kind::StringValue(new_value)), + }) +} + +fn length( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String); + Signal::Success(value_from_i64(value.chars().count() as i64)) +} + +fn remove( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String, from: tucana::shared::NumberValue, to: tucana::shared::NumberValue); + let from = match number_to_i64_lossy(&from) { + Some(v) => v, + None => return arg_err("Expected number 'from'"), + }; + let to = match number_to_i64_lossy(&to) { + Some(v) => v, + None => return arg_err("Expected number 'to'"), + }; + + if from < 0 || to < 0 { + return arg_err("Expected non-negative indices"); + } + + let from_u = from as usize; + let to_u = to as usize; + + let chars = value.chars().collect::>(); + if from_u > chars.len() || to_u > chars.len() { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "IndexOutOfBoundsRuntimeError", + format!( + "Indices [{}, {}) out of bounds for length {}", + from_u, + to_u, + chars.len() + ), + )); + } + + let new = chars + .into_iter() + .enumerate() + .filter(|&(i, _)| i < from_u || i >= to_u) + .map(|e| e.1) + .collect::(); + + Signal::Success(Value { + kind: Some(Kind::StringValue(new)), + }) +} + +fn replace( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String, old: String, new: String); + let replaced = value.replace(&old, &new); + Signal::Success(Value { + kind: Some(Kind::StringValue(replaced)), + }) +} + +fn replace_first( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String, old: String, new: String); + let replaced = value.replacen(&old, &new, 1); + Signal::Success(Value { + kind: Some(Kind::StringValue(replaced)), + }) +} + +fn replace_last( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String, old: String, new: String); + + fn replace_last_impl(haystack: &str, needle: &str, replacement: &str) -> String { + if let Some(pos) = haystack.rfind(needle) { + let mut result = + String::with_capacity(haystack.len() - needle.len() + replacement.len()); + result.push_str(&haystack[..pos]); + result.push_str(replacement); + result.push_str(&haystack[pos + needle.len()..]); + result + } else { + haystack.to_string() + } + } + + let replaced = replace_last_impl(&value, &old, &new); + Signal::Success(Value { + kind: Some(Kind::StringValue(replaced)), + }) +} + +fn hex( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String); + + let hex = value + .as_bytes() + .iter() + .map(|b| format!("{:02x}", b)) + .collect::(); + + Signal::Success(Value { + kind: Some(Kind::StringValue(hex)), + }) +} + +fn octal( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String); + + let oct = value + .as_bytes() + .iter() + .map(|b| format!("{:03o}", b)) + .collect::(); + + Signal::Success(Value { + kind: Some(Kind::StringValue(oct)), + }) +} + +fn index_of( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String, sub: String); + + match value.find(&sub) { + Some(idx) => Signal::Success(value_from_i64(idx as i64)), + None => Signal::Success(value_from_i64(-1)), + } +} + +fn contains( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String, sub: String); + Signal::Success(Value { + kind: Some(Kind::BoolValue(value.contains(&sub))), + }) +} + +fn split( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String, delimiter: String); + + let parts = value + .split(&delimiter) + .map(|s| Value { + kind: Some(Kind::StringValue(s.to_string())), + }) + .collect::>(); + + Signal::Success(Value { + kind: Some(Kind::ListValue(ListValue { values: parts })), + }) +} + +fn reverse( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String); + + let reversed = value.chars().rev().collect::(); + Signal::Success(Value { + kind: Some(Kind::StringValue(reversed)), + }) +} + +fn starts_with( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String, prefix: String); + Signal::Success(Value { + kind: Some(Kind::BoolValue(value.starts_with(&prefix))), + }) +} + +fn ends_with( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String, suffix: String); + Signal::Success(Value { + kind: Some(Kind::BoolValue(value.ends_with(&suffix))), + }) +} + +fn to_ascii( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String); + + let ascii = value + .bytes() + .map(|b| value_from_i64(b as i64)) + .collect::>(); + + Signal::Success(Value { + kind: Some(Kind::ListValue(ListValue { values: ascii })), + }) +} + +fn from_ascii( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + // Requires a TryFromArg impl for ListValue in your macro system. + args!(args => list: ListValue); + + let string = list + .values + .iter() + .map(|v| match v { + Value { + kind: Some(Kind::NumberValue(n)), + } => match number_to_f64(n) { + Some(n) if (0.0..=127.0).contains(&n) => Some(n as u8 as char), + _ => None, + }, + _ => None, + }) + .collect::>(); + + match string { + Some(s) => Signal::Success(Value { + kind: Some(Kind::StringValue(s)), + }), + None => arg_err("Expected a list of numbers between 0 and 127"), + } +} + +// NOTE: "encode"/"decode" currently only support base64. +fn encode( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String, encoding: String); + + let encoded = match encoding.to_lowercase().as_str() { + "base64" => base64::prelude::BASE64_STANDARD.encode(value), + _ => { + return arg_err(format!("Unsupported encoding: {}", encoding)); + } + }; + + Signal::Success(Value { + kind: Some(Kind::StringValue(encoded)), + }) +} + +fn decode( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => value: String, encoding: String); + + let decoded = match encoding.to_lowercase().as_str() { + "base64" => match base64::prelude::BASE64_STANDARD.decode(value) { + Ok(bytes) => match String::from_utf8(bytes) { + Ok(s) => s, + Err(err) => { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "DecodeError", + format!("Failed to decode base64 bytes to UTF-8: {:?}", err), + )); + } + }, + Err(err) => { + return Signal::Failure(RuntimeError::new( + "T-RT-000000", + "DecodeError", + format!("Failed to decode base64 string: {:?}", err), + )); + } + }, + _ => return arg_err(format!("Unsupported decoding: {}", encoding)), + }; + + Signal::Success(Value { + kind: Some(Kind::StringValue(decoded)), + }) +} + +fn is_equal( + args: &[Argument], + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, +) -> Signal { + args!(args => lhs: String, rhs: String); + Signal::Success(Value { + kind: Some(Kind::BoolValue(lhs == rhs)), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::runtime::execution::value_store::ValueStore; + use crate::value::{number_to_f64, value_from_f64, value_from_i64}; + use tucana::shared::{ListValue, Value, value::Kind}; + + // ---------- helpers: build Arguments ---------- + fn a_str(s: &str) -> Argument { + Argument::Eval(Value { + kind: Some(Kind::StringValue(s.to_string())), + }) + } + fn a_num(n: f64) -> Argument { + Argument::Eval(value_from_f64(n)) + } + fn a_list(vals: Vec) -> Argument { + Argument::Eval(Value { + kind: Some(Kind::ListValue(ListValue { values: vals })), + }) + } + + // ---------- helpers: build bare Values ---------- + fn v_str(s: &str) -> Value { + Value { + kind: Some(Kind::StringValue(s.to_string())), + } + } + fn v_num(n: i64) -> Value { + value_from_i64(n) + } + + // ---------- helpers: extract from Signal ---------- + fn expect_num(sig: Signal) -> f64 { + match sig { + Signal::Success(Value { + kind: Some(Kind::NumberValue(n)), + }) => number_to_f64(&n).unwrap_or_default(), + other => panic!("Expected NumberValue, got {:?}", other), + } + } + fn expect_bool(sig: Signal) -> bool { + match sig { + Signal::Success(Value { + kind: Some(Kind::BoolValue(b)), + }) => b, + other => panic!("Expected BoolValue, got {:?}", other), + } + } + fn expect_str(sig: Signal) -> String { + match sig { + Signal::Success(Value { + kind: Some(Kind::StringValue(s)), + }) => s, + other => panic!("Expected StringValue, got {:?}", other), + } + } + fn expect_list(sig: Signal) -> Vec { + match sig { + Signal::Success(Value { + kind: Some(Kind::ListValue(ListValue { values })), + }) => values, + other => panic!("Expected ListValue, got {:?}", other), + } + } + + // dummy runner for handlers that accept `run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal` + fn dummy_run(_: i64, _: &mut ValueStore) -> Signal { + Signal::Success(Value { + kind: Some(Kind::NullValue(0)), + }) + } + + // ---------- tests ---------- + + #[test] + fn test_as_bytes_and_byte_size() { + let mut ctx = ValueStore::default(); + let mut run = dummy_run; + + // "hello" -> 5 bytes + let bytes = expect_list(as_bytes(&[a_str("hello")], &mut ctx, &mut run)); + assert_eq!(bytes.len(), 5); + assert_eq!(bytes[0], v_num(104)); // 'h' + + let mut run = dummy_run; + assert_eq!( + expect_num(byte_size(&[a_str("hello")], &mut ctx, &mut run)), + 5.0 + ); + + // unicode: "café" -> 5 bytes, 4 chars + let mut run = dummy_run; + assert_eq!( + expect_num(byte_size(&[a_str("café")], &mut ctx, &mut run)), + 5.0 + ); + let mut run = dummy_run; + assert_eq!( + expect_num(length(&[a_str("café")], &mut ctx, &mut run)), + 4.0 + ); + } + + #[test] + fn test_case_ops_and_trim() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert_eq!( + expect_str(capitalize(&[a_str("hello world")], &mut ctx, &mut run)), + "Hello World" + ); + + let mut run = dummy_run; + assert_eq!( + expect_str(uppercase(&[a_str("Hello")], &mut ctx, &mut run)), + "HELLO" + ); + + let mut run = dummy_run; + assert_eq!( + expect_str(lowercase(&[a_str("Hello")], &mut ctx, &mut run)), + "hello" + ); + + let mut run = dummy_run; + assert_eq!( + expect_str(swapcase(&[a_str("HeLLo123")], &mut ctx, &mut run)), + "hEllO123" + ); + + let mut run = dummy_run; + assert_eq!( + expect_str(trim(&[a_str(" hi ")], &mut ctx, &mut run)), + "hi" + ); + } + + #[test] + fn test_chars_and_at() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + let chars_list = expect_list(chars(&[a_str("abc")], &mut ctx, &mut run)); + assert_eq!(chars_list, vec![v_str("a"), v_str("b"), v_str("c")]); + + let mut run = dummy_run; + assert_eq!( + expect_str(at(&[a_str("hello"), a_num(1.0)], &mut ctx, &mut run)), + "e" + ); + + // out-of-bounds + let mut run = dummy_run; + match at(&[a_str("hi"), a_num(5.0)], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure, got {:?}", s), + } + // negative + let mut run = dummy_run; + match at(&[a_str("hi"), a_num(-1.0)], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure, got {:?}", s), + } + } + + #[test] + fn test_append_prepend_insert_length() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert_eq!( + expect_str(append( + &[a_str("hello"), a_str(" world")], + &mut ctx, + &mut run + )), + "hello world" + ); + + let mut run = dummy_run; + assert_eq!( + expect_str(prepend( + &[a_str("world"), a_str("hello ")], + &mut ctx, + &mut run + )), + "hello world" + ); + + // insert uses BYTE index; for ASCII this matches char index + let mut run = dummy_run; + assert_eq!( + expect_str(insert( + &[a_str("hello"), a_num(2.0), a_str("XXX")], + &mut ctx, + &mut run + )), + "heXXXllo" + ); + + let mut run = dummy_run; + assert_eq!( + expect_num(length(&[a_str("hello")], &mut ctx, &mut run)), + 5.0 + ); + } + + #[test] + fn test_remove_replace_variants() { + let mut ctx = ValueStore::default(); + + // remove uses CHAR indices [from, to) + let mut run = dummy_run; + assert_eq!( + expect_str(remove( + &[a_str("hello world"), a_num(2.0), a_num(7.0)], + &mut ctx, + &mut run + )), + "heorld" + ); + + let mut run = dummy_run; + assert_eq!( + expect_str(replace( + &[a_str("hello world hello"), a_str("hello"), a_str("hi")], + &mut ctx, + &mut run + )), + "hi world hi" + ); + + let mut run = dummy_run; + assert_eq!( + expect_str(replace_first( + &[a_str("one two one"), a_str("one"), a_str("1")], + &mut ctx, + &mut run + )), + "1 two one" + ); + + let mut run = dummy_run; + assert_eq!( + expect_str(replace_last( + &[a_str("one two one"), a_str("one"), a_str("1")], + &mut ctx, + &mut run + )), + "one two 1" + ); + } + + #[test] + fn test_hex_octal_reverse() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert_eq!( + expect_str(hex(&[a_str("hello")], &mut ctx, &mut run)), + "68656c6c6f" + ); + + let mut run = dummy_run; + assert_eq!(expect_str(octal(&[a_str("A")], &mut ctx, &mut run)), "101"); + + let mut run = dummy_run; + assert_eq!( + expect_str(reverse(&[a_str("hello")], &mut ctx, &mut run)), + "olleh" + ); + } + + #[test] + fn test_index_contains_split_starts_ends() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert_eq!( + expect_num(index_of( + &[a_str("hello world"), a_str("world")], + &mut ctx, + &mut run + )), + 6.0 + ); + + let mut run = dummy_run; + assert_eq!( + expect_num(index_of( + &[a_str("hello"), a_str("xyz")], + &mut ctx, + &mut run + )), + -1.0 + ); + + let mut run = dummy_run; + assert!(expect_bool(contains( + &[a_str("hello world"), a_str("world")], + &mut ctx, + &mut run + ))); + + let mut run = dummy_run; + let split_list = expect_list(split(&[a_str("a,b,c"), a_str(",")], &mut ctx, &mut run)); + assert_eq!(split_list, vec![v_str("a"), v_str("b"), v_str("c")]); + + let mut run = dummy_run; + assert!(expect_bool(starts_with( + &[a_str("hello"), a_str("he")], + &mut ctx, + &mut run + ))); + + let mut run = dummy_run; + assert!(expect_bool(ends_with( + &[a_str("hello"), a_str("lo")], + &mut ctx, + &mut run + ))); + } + + #[test] + fn test_to_ascii_and_from_ascii() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + let ascii_vals = expect_list(to_ascii(&[a_str("AB")], &mut ctx, &mut run)); + assert_eq!(ascii_vals, vec![v_num(65), v_num(66)]); + + let mut run = dummy_run; + let list_arg = a_list(vec![v_num(65), v_num(66), v_num(67)]); + assert_eq!( + expect_str(from_ascii(&[list_arg], &mut ctx, &mut run)), + "ABC" + ); + + // invalid element + let mut run = dummy_run; + let list_arg = a_list(vec![v_num(65), v_num(128)]); + match from_ascii(&[list_arg], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for invalid ASCII, got {:?}", s), + } + } + + #[test] + fn test_encode_decode_base64_and_is_equal() { + let mut ctx = ValueStore::default(); + + let mut run = dummy_run; + assert_eq!( + expect_str(encode( + &[a_str("hello"), a_str("BASE64")], + &mut ctx, + &mut run + )), + "aGVsbG8=" + ); + + let mut run = dummy_run; + assert_eq!( + expect_str(decode( + &[a_str("aGVsbG8="), a_str("base64")], + &mut ctx, + &mut run + )), + "hello" + ); + + // unsupported codec + let mut run = dummy_run; + match encode(&[a_str("data"), a_str("gug")], &mut ctx, &mut run) { + Signal::Failure(_) => {} + s => panic!("Expected Failure for unsupported encoding, got {:?}", s), + } + + let mut run = dummy_run; + assert!(expect_bool(is_equal( + &[a_str("x"), a_str("x")], + &mut ctx, + &mut run + ))); + let mut run = dummy_run; + assert!(!expect_bool(is_equal( + &[a_str("x"), a_str("y")], + &mut ctx, + &mut run + ))); + } +} From 3af5e36f9919804409bcf3adaa4cbb89bdded1fd Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 21 Apr 2026 17:54:11 +0200 Subject: [PATCH 15/28] feat: added core types --- crates/taurus-core/src/runtime/engine.rs | 483 ++++++++++++++++++ crates/taurus-core/src/runtime/mod.rs | 9 + crates/taurus-core/src/runtime/remote/mod.rs | 22 + crates/taurus-core/src/types/errors/error.rs | 236 +++++++++ crates/taurus-core/src/types/errors/mod.rs | 4 + .../src/types/errors/runtime_error.rs | 164 ++++++ .../src/types/execution/bindings.rs | 54 ++ .../src/types/execution/flow_ir.rs | 33 ++ crates/taurus-core/src/types/execution/ids.rs | 53 ++ crates/taurus-core/src/types/execution/mod.rs | 11 + .../src/types/execution/signature.rs | 28 + crates/taurus-core/src/types/exit_reason.rs | 46 ++ crates/taurus-core/src/types/mod.rs | 8 + crates/taurus-core/src/types/signal.rs | 104 ++++ 14 files changed, 1255 insertions(+) create mode 100644 crates/taurus-core/src/runtime/engine.rs create mode 100644 crates/taurus-core/src/runtime/mod.rs create mode 100644 crates/taurus-core/src/runtime/remote/mod.rs create mode 100644 crates/taurus-core/src/types/errors/error.rs create mode 100644 crates/taurus-core/src/types/errors/mod.rs create mode 100644 crates/taurus-core/src/types/errors/runtime_error.rs create mode 100644 crates/taurus-core/src/types/execution/bindings.rs create mode 100644 crates/taurus-core/src/types/execution/flow_ir.rs create mode 100644 crates/taurus-core/src/types/execution/ids.rs create mode 100644 crates/taurus-core/src/types/execution/mod.rs create mode 100644 crates/taurus-core/src/types/execution/signature.rs create mode 100644 crates/taurus-core/src/types/exit_reason.rs create mode 100644 crates/taurus-core/src/types/mod.rs create mode 100644 crates/taurus-core/src/types/signal.rs diff --git a/crates/taurus-core/src/runtime/engine.rs b/crates/taurus-core/src/runtime/engine.rs new file mode 100644 index 0000000..1e0dbd7 --- /dev/null +++ b/crates/taurus-core/src/runtime/engine.rs @@ -0,0 +1,483 @@ +//! Public runtime execution API. +//! +//! This module is the new entrypoint for flow execution from external crates. +//! It executes compiled flow plans via the runtime engine executor loop. + +mod compiler; +mod emitter; +mod executor; +mod model; + +use tucana::shared::{ExecutionFlow, NodeFunction, Value}; + +use crate::handler::registry::FunctionStore; +use crate::runtime::execution::value_store::ValueStore; +use crate::runtime::remote::RemoteRuntime; +use crate::types::exit_reason::ExitReason; +use crate::types::signal::Signal; +use compiler::compile_flow; +pub use emitter::{EmitType, RespondEmitter}; + +fn null_value() -> Value { + Value { + kind: Some(tucana::shared::value::Kind::NullValue(0)), + } +} + +/// Runtime engine entrypoint used by runtime binaries and CLI tools. +pub struct ExecutionEngine { + handlers: FunctionStore, +} + +impl Default for ExecutionEngine { + fn default() -> Self { + Self::new() + } +} + +impl ExecutionEngine { + /// Build a new execution engine with default handler registry. + pub fn new() -> Self { + Self { + handlers: FunctionStore::default(), + } + } + + /// Execute an `ExecutionFlow`. + pub fn execute_flow( + &self, + flow: ExecutionFlow, + remote: Option<&dyn RemoteRuntime>, + respond_emitter: Option<&dyn RespondEmitter>, + with_trace: bool, + ) -> (Signal, ExitReason) { + self.execute_graph( + flow.starting_node_id, + flow.node_functions, + flow.input_value, + remote, + respond_emitter, + with_trace, + ) + } + + /// Execute a graph described by node list and start node. + pub fn execute_graph( + &self, + start_node_id: i64, + node_functions: Vec, + flow_input: Option, + remote: Option<&dyn RemoteRuntime>, + respond_emitter: Option<&dyn RespondEmitter>, + with_trace: bool, + ) -> (Signal, ExitReason) { + if let Some(emitter) = respond_emitter { + emitter.emit(EmitType::StartingExec, null_value()); + } + + let mut value_store = match flow_input { + Some(v) => ValueStore::new(v), + None => ValueStore::default(), + }; + + let compiled = match compile_flow(start_node_id, node_functions) { + Ok(plan) => plan, + Err(err) => { + let runtime_error = err.as_runtime_error(); + if let Some(emitter) = respond_emitter { + emitter.emit(EmitType::FailedExec, runtime_error.as_value()); + } + let signal = Signal::Failure(runtime_error); + return (signal, ExitReason::Failure); + } + }; + + let (signal, trace_run) = executor::execute_compiled( + &compiled, + &self.handlers, + &mut value_store, + remote, + respond_emitter, + with_trace, + ); + if with_trace && let Some(trace_run) = trace_run { + println!( + "{}", + crate::runtime::execution::render::render_trace(&trace_run) + ); + } + if let Some(emitter) = respond_emitter { + match &signal { + Signal::Failure(err) => emitter.emit(EmitType::FailedExec, err.as_value()), + Signal::Success(value) | Signal::Return(value) | Signal::Respond(value) => { + emitter.emit(EmitType::FinishedExec, value.clone()) + } + Signal::Stop => emitter.emit(EmitType::FinishedExec, null_value()), + } + } + let exit_reason = signal.exit_reason(); + (signal, exit_reason) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::exit_reason::ExitReason; + use std::cell::RefCell; + use tucana::shared::{ + InputType, ListValue, NodeParameter, NodeValue, ReferenceValue, Struct, Value, node_value, + reference_value, value::Kind, + }; + + fn literal_param(database_id: i64, runtime_parameter_id: &str, value: Value) -> NodeParameter { + NodeParameter { + database_id, + runtime_parameter_id: runtime_parameter_id.to_string(), + value: Some(NodeValue { + value: Some(node_value::Value::LiteralValue(value)), + }), + } + } + + fn thunk_param(database_id: i64, runtime_parameter_id: &str, node_id: i64) -> NodeParameter { + NodeParameter { + database_id, + runtime_parameter_id: runtime_parameter_id.to_string(), + value: Some(NodeValue { + value: Some(node_value::Value::NodeFunctionId(node_id)), + }), + } + } + + fn node_result_ref_param( + database_id: i64, + runtime_parameter_id: &str, + node_id: i64, + ) -> NodeParameter { + NodeParameter { + database_id, + runtime_parameter_id: runtime_parameter_id.to_string(), + value: Some(NodeValue { + value: Some(node_value::Value::ReferenceValue(ReferenceValue { + target: Some(reference_value::Target::NodeId(node_id)), + paths: Vec::new(), + })), + }), + } + } + + fn node( + database_id: i64, + runtime_function_id: &str, + parameters: Vec, + next_node_id: Option, + ) -> NodeFunction { + NodeFunction { + database_id, + runtime_function_id: runtime_function_id.to_string(), + parameters, + next_node_id, + definition_source: "taurus".to_string(), + } + } + + fn int_value(value: i64) -> Value { + crate::value::value_from_i64(value) + } + + fn string_value(value: &str) -> Value { + Value { + kind: Some(Kind::StringValue(value.to_string())), + } + } + + fn null_value() -> Value { + Value { + kind: Some(Kind::NullValue(0)), + } + } + + fn empty_struct_value() -> Value { + Value { + kind: Some(Kind::StructValue(Struct { + fields: std::collections::HashMap::new(), + })), + } + } + + fn list_value(values: Vec) -> Value { + Value { + kind: Some(Kind::ListValue(ListValue { values })), + } + } + + fn input_type_ref_param( + database_id: i64, + runtime_parameter_id: &str, + node_id: i64, + parameter_index: i64, + input_index: i64, + ) -> NodeParameter { + NodeParameter { + database_id, + runtime_parameter_id: runtime_parameter_id.to_string(), + value: Some(NodeValue { + value: Some(node_value::Value::ReferenceValue(ReferenceValue { + target: Some(reference_value::Target::InputType(InputType { + node_id, + parameter_index, + input_index, + })), + paths: Vec::new(), + })), + }), + } + } + + #[test] + fn eager_thunk_return_unwinds_one_level_and_continues_with_parent_next() { + let engine = ExecutionEngine::new(); + + // Node 10 is used as eager parameter thunk by node 2. + // It returns 42 and must not continue to its own next node. + let return_node = node( + 10, + "std::control::return", + vec![literal_param(100, "value", int_value(42))], + Some(12), + ); + + // If this node ever executes, the test expectation below will fail. + let unreachable_after_return = node(12, "std::number::add", vec![], None); + + // Parent node A (id=2): eager arg is node 10. + let parent = node( + 2, + "std::number::add", + vec![ + thunk_param(200, "lhs", 10), + literal_param(201, "rhs", int_value(1)), + ], + Some(3), + ); + + // Next node B (id=3): depends on A result and adds 1. + let next = node( + 3, + "std::number::add", + vec![ + node_result_ref_param(300, "lhs", 2), + literal_param(301, "rhs", int_value(1)), + ], + None, + ); + + let (signal, reason) = engine.execute_graph( + 2, + vec![parent, next, return_node, unreachable_after_return], + None, + None, + None, + false, + ); + + assert_eq!(reason, ExitReason::Success); + match signal { + Signal::Success(Value { + kind: Some(Kind::NumberValue(number)), + }) => match number.number { + Some(tucana::shared::number_value::Number::Integer(v)) => assert_eq!(v, 43), + other => panic!("expected integer result 43, got {:?}", other), + }, + other => panic!("expected success with value 43, got {:?}", other), + } + } + + #[test] + fn return_inside_map_callback_returns_callback_value_only() { + let engine = ExecutionEngine::new(); + + let map_node = node( + 1, + "std::list::map", + vec![ + literal_param( + 100, + "list", + list_value(vec![ + string_value("age"), + string_value("email"), + string_value("username"), + ]), + ), + thunk_param(101, "transform", 2), + ], + None, + ); + + let is_equal_node = node( + 2, + "std::text::is_equal", + vec![ + input_type_ref_param(200, "first", 1, 1, 0), + literal_param(201, "second", string_value("username")), + ], + Some(3), + ); + + let if_node = node( + 3, + "std::control::if", + vec![ + node_result_ref_param(300, "condition", 2), + thunk_param(301, "runnable", 4), + ], + Some(5), + ); + + let return_item_node = node( + 4, + "std::control::return", + vec![input_type_ref_param(400, "value", 1, 1, 0)], + None, + ); + + let return_null_node = node( + 5, + "std::control::return", + vec![literal_param(500, "value", null_value())], + None, + ); + + let (signal, reason) = engine.execute_graph( + 1, + vec![ + map_node, + is_equal_node, + if_node, + return_item_node, + return_null_node, + ], + None, + None, + None, + false, + ); + + assert_eq!(reason, ExitReason::Success); + match signal { + Signal::Success(Value { + kind: Some(Kind::ListValue(ListValue { values })), + }) => { + assert_eq!( + values, + vec![null_value(), null_value(), string_value("username")] + ); + } + other => panic!( + "expected Success([null, null, \"username\"]), got {:?}", + other + ), + } + } + + #[test] + fn emitter_emits_start_and_finish_for_successful_execution() { + let engine = ExecutionEngine::new(); + let events = RefCell::new(Vec::::new()); + let emitter = |emit_type: EmitType, _value: Value| { + events.borrow_mut().push(emit_type); + }; + + let add_node = node( + 1, + "std::number::add", + vec![ + literal_param(100, "lhs", int_value(1)), + literal_param(101, "rhs", int_value(2)), + ], + None, + ); + + let (_signal, reason) = + engine.execute_graph(1, vec![add_node], None, None, Some(&emitter), false); + assert_eq!(reason, ExitReason::Success); + assert_eq!( + *events.borrow(), + vec![EmitType::StartingExec, EmitType::FinishedExec] + ); + } + + #[test] + fn emitter_emits_ongoing_for_intermediate_respond() { + let engine = ExecutionEngine::new(); + let events = RefCell::new(Vec::::new()); + let emitter = |emit_type: EmitType, _value: Value| { + events.borrow_mut().push(emit_type); + }; + + let create_response_node = node( + 1, + "http::response::create", + vec![ + literal_param(100, "http_status_code", int_value(200)), + literal_param(101, "headers", empty_struct_value()), + literal_param(102, "payload", string_value("hello")), + ], + Some(2), + ); + let respond_node = node( + 2, + "rest::control::respond", + vec![node_result_ref_param(200, "response", 1)], + Some(3), + ); + let finish_node = node( + 3, + "std::number::add", + vec![ + literal_param(300, "lhs", int_value(1)), + literal_param(301, "rhs", int_value(1)), + ], + None, + ); + + let (_signal, reason) = engine.execute_graph( + 1, + vec![create_response_node, respond_node, finish_node], + None, + None, + Some(&emitter), + false, + ); + assert_eq!(reason, ExitReason::Success); + assert_eq!( + *events.borrow(), + vec![ + EmitType::StartingExec, + EmitType::OngoingExec, + EmitType::FinishedExec + ] + ); + } + + #[test] + fn emitter_emits_failed_for_runtime_failure() { + let engine = ExecutionEngine::new(); + let events = RefCell::new(Vec::::new()); + let emitter = |emit_type: EmitType, _value: Value| { + events.borrow_mut().push(emit_type); + }; + + let invalid_add_node = node(1, "std::number::add", vec![], None); + + let (_signal, reason) = + engine.execute_graph(1, vec![invalid_add_node], None, None, Some(&emitter), false); + assert_eq!(reason, ExitReason::Failure); + assert_eq!( + *events.borrow(), + vec![EmitType::StartingExec, EmitType::FailedExec] + ); + } +} diff --git a/crates/taurus-core/src/runtime/mod.rs b/crates/taurus-core/src/runtime/mod.rs new file mode 100644 index 0000000..aeeb76d --- /dev/null +++ b/crates/taurus-core/src/runtime/mod.rs @@ -0,0 +1,9 @@ +//! Runtime execution surfaces. +//! +//! `engine` is the public execution API. The remaining modules contain runtime +//! internals and transport-specific abstractions used by the engine. + +pub mod engine; +pub mod execution; +pub mod functions; +pub mod remote; diff --git a/crates/taurus-core/src/runtime/remote/mod.rs b/crates/taurus-core/src/runtime/remote/mod.rs new file mode 100644 index 0000000..f5ab662 --- /dev/null +++ b/crates/taurus-core/src/runtime/remote/mod.rs @@ -0,0 +1,22 @@ +//! Remote runtime execution interface. +//! +//! Local runtime nodes can delegate execution to remote services through this +//! trait without coupling the core engine to a specific transport. + +use async_trait::async_trait; +use tucana::{aquila::ExecutionRequest, shared::Value}; + +use crate::types::errors::runtime_error::RuntimeError; + +pub struct RemoteExecution { + /// Remote service identifier to route the call. + pub target_service: String, + /// Execution request payload expected by the remote runtime. + pub request: ExecutionRequest, +} + +#[async_trait] +pub trait RemoteRuntime { + /// Execute a remote node invocation and return its resulting value. + async fn execute_remote(&self, execution: RemoteExecution) -> Result; +} diff --git a/crates/taurus-core/src/types/errors/error.rs b/crates/taurus-core/src/types/errors/error.rs new file mode 100644 index 0000000..baf2957 --- /dev/null +++ b/crates/taurus-core/src/types/errors/error.rs @@ -0,0 +1,236 @@ +//! General (application-level) errors that can occur outside pure node execution. +//! +//! Every variant can be converted into a [`RuntimeError`] to unify reporting. + +use std::collections::HashMap; +use std::error::Error as StdError; +use std::fmt::{Display, Formatter}; + +use tucana::shared::Value; +use tucana::shared::value::Kind::StringValue; + +use crate::types::errors::runtime_error::RuntimeError; + +/// Application-layer failures that should still be reportable as runtime failures. +#[derive(Debug, Clone)] +pub enum Error { + /// Invalid or missing runtime configuration. + Configuration { + message: String, + details: HashMap, + }, + /// Invalid application state transition. + State { + message: String, + details: HashMap, + }, + /// Failed communication with dependency or transport layer. + Transport { + dependency: String, + message: String, + details: HashMap, + }, + /// Failed serialization/deserialization. + Serialization { + format: String, + message: String, + details: HashMap, + }, + /// Catch-all internal application error. + Internal { + message: String, + details: HashMap, + }, +} + +impl Error { + /// Build a configuration error with optional structured details. + pub fn configuration(message: impl Into) -> Self { + Self::Configuration { + message: message.into(), + details: HashMap::new(), + } + } + + /// Build an invalid state error with optional structured details. + pub fn state(message: impl Into) -> Self { + Self::State { + message: message.into(), + details: HashMap::new(), + } + } + + /// Build a transport/dependency error with optional structured details. + pub fn transport(dependency: impl Into, message: impl Into) -> Self { + Self::Transport { + dependency: dependency.into(), + message: message.into(), + details: HashMap::new(), + } + } + + /// Build a serialization error with optional structured details. + pub fn serialization(format: impl Into, message: impl Into) -> Self { + Self::Serialization { + format: format.into(), + message: message.into(), + details: HashMap::new(), + } + } + + /// Build an internal application error with optional structured details. + pub fn internal(message: impl Into) -> Self { + Self::Internal { + message: message.into(), + details: HashMap::new(), + } + } + + /// Attach a detail entry to this error. + pub fn with_detail(mut self, key: impl Into, value: Value) -> Self { + match &mut self { + Error::Configuration { details, .. } + | Error::State { details, .. } + | Error::Transport { details, .. } + | Error::Serialization { details, .. } + | Error::Internal { details, .. } => { + details.insert(key.into(), value); + } + } + self + } +} + +impl From for RuntimeError { + fn from(value: Error) -> Self { + match value { + Error::Configuration { message, details } => { + let mut err = RuntimeError::with_code( + "T-APP-000001".to_string(), + "Configuration".to_string(), + message, + ); + err.details.extend(details); + err + } + Error::State { message, details } => { + let mut err = RuntimeError::with_code( + "T-APP-000002".to_string(), + "State".to_string(), + message, + ); + err.details.extend(details); + err + } + Error::Transport { + dependency, + message, + details, + } => { + let mut err = RuntimeError::with_code( + "T-APP-000003".to_string(), + "Transport".to_string(), + message, + ) + .with_detail( + "dependency".to_string(), + Value { + kind: Some(StringValue(dependency)), + }, + ); + err.details.extend(details); + err + } + Error::Serialization { + format, + message, + details, + } => { + let mut err = RuntimeError::with_code( + "T-APP-000004".to_string(), + "Serialization".to_string(), + message, + ) + .with_detail( + "format".to_string(), + Value { + kind: Some(StringValue(format)), + }, + ); + err.details.extend(details); + err + } + Error::Internal { message, details } => { + let mut err = RuntimeError::with_code( + "T-APP-999999".to_string(), + "Internal".to_string(), + message, + ); + err.details.extend(details); + err + } + } + } +} + +impl StdError for Error {} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Error::Configuration { message, .. } => write!(f, "Configuration error: {message}"), + Error::State { message, .. } => write!(f, "State error: {message}"), + Error::Transport { + dependency, + message, + .. + } => write!(f, "Transport error ({dependency}): {message}"), + Error::Serialization { + format, message, .. + } => write!(f, "Serialization error ({format}): {message}"), + Error::Internal { message, .. } => write!(f, "Internal error: {message}"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tucana::shared::{Struct, value::Kind::StructValue}; + + #[test] + fn app_error_converts_to_runtime_error_with_expected_code_and_category() { + let app_err = Error::transport("nats", "connection lost").with_detail( + "subject", + Value { + kind: Some(StringValue("execution.*".to_string())), + }, + ); + + let runtime_error: RuntimeError = app_err.into(); + + assert_eq!(runtime_error.code, "T-APP-000003"); + assert_eq!(runtime_error.category, "Transport"); + assert_eq!(runtime_error.message, "connection lost"); + assert!(runtime_error.details.contains_key("dependency")); + assert!(runtime_error.details.contains_key("subject")); + } + + #[test] + fn runtime_error_value_contains_required_struct_fields() { + let runtime_error: RuntimeError = Error::internal("boom").into(); + let value = runtime_error.as_value(); + + let Some(StructValue(Struct { fields })) = value.kind else { + panic!("expected struct value"); + }; + + assert!(fields.contains_key("code")); + assert!(fields.contains_key("category")); + assert!(fields.contains_key("message")); + assert!(fields.contains_key("timestamp")); + assert!(fields.contains_key("version")); + assert!(fields.contains_key("dependencies")); + assert!(fields.contains_key("details")); + } +} diff --git a/crates/taurus-core/src/types/errors/mod.rs b/crates/taurus-core/src/types/errors/mod.rs new file mode 100644 index 0000000..4e6145c --- /dev/null +++ b/crates/taurus-core/src/types/errors/mod.rs @@ -0,0 +1,4 @@ +//! Error types shared across Taurus runtime execution and application layers. + +pub mod error; +pub mod runtime_error; diff --git a/crates/taurus-core/src/types/errors/runtime_error.rs b/crates/taurus-core/src/types/errors/runtime_error.rs new file mode 100644 index 0000000..b4772ce --- /dev/null +++ b/crates/taurus-core/src/types/errors/runtime_error.rs @@ -0,0 +1,164 @@ +//! Runtime-facing error object used as execution failure payload. +//! +//! Format goals: +//! - Stable machine fields (`code`, `category`) for filtering and analytics +//! - Human-readable `message` +//! - Timestamp and version for support/debugging +//! - Optional dependency map and structured details + +use std::collections::HashMap; +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use tucana::shared::value::Kind::{NumberValue, StringValue, StructValue}; +use tucana::shared::{NumberValue as ProtoNumberValue, Struct, Value, number_value}; + +/// Runtime execution failure representation. +#[derive(Debug, Clone, PartialEq)] +pub struct RuntimeError { + /// Three-part error code (example: `T-STD-000123`). + pub code: String, + /// Logical category of the error (example: `InvalidArgument`). + pub category: String, + /// Human-readable diagnostic message. + pub message: String, + /// Unix timestamp in milliseconds when this error object was created. + pub timestamp_unix_ms: u64, + /// Runtime version identifier. + pub version: String, + /// Dependency versions relevant to this runtime. + pub dependencies: HashMap, + /// Additional structured context. + pub details: HashMap, +} + +impl RuntimeError { + /// Build a runtime error from explicit code/category/message. + pub fn new( + code: impl Into, + category: impl Into, + message: impl Into, + ) -> Self { + Self { + code: code.into(), + category: category.into(), + message: message.into(), + timestamp_unix_ms: now_unix_ms(), + version: env!("CARGO_PKG_VERSION").to_string(), + dependencies: HashMap::new(), + details: HashMap::new(), + } + } + + /// Build a runtime error from explicit owned fields. + pub fn with_code(code: String, category: String, message: String) -> Self { + Self::new(code, category, message) + } + + /// Attach or overwrite a dependency version entry. + pub fn with_dependency(mut self, name: String, version: String) -> Self { + self.dependencies.insert(name, version); + self + } + + /// Attach or overwrite a structured detail entry. + pub fn with_detail(mut self, key: String, value: Value) -> Self { + self.details.insert(key, value); + self + } + + /// Convert to proto `Value` for transport back to callers. + pub fn as_value(&self) -> Value { + let dependencies = self + .dependencies + .iter() + .map(|(name, version)| { + ( + name.clone(), + Value { + kind: Some(StringValue(version.clone())), + }, + ) + }) + .collect::>(); + + Value { + kind: Some(StructValue(Struct { + fields: HashMap::from([ + ( + String::from("code"), + Value { + kind: Some(StringValue(self.code.clone())), + }, + ), + ( + String::from("category"), + Value { + kind: Some(StringValue(self.category.clone())), + }, + ), + ( + String::from("message"), + Value { + kind: Some(StringValue(self.message.clone())), + }, + ), + ( + String::from("timestamp"), + Value { + kind: Some(NumberValue(ProtoNumberValue { + number: Some(number_value::Number::Integer( + self.timestamp_unix_ms as i64, + )), + })), + }, + ), + ( + String::from("version"), + Value { + kind: Some(StringValue(self.version.clone())), + }, + ), + ( + String::from("dependencies"), + Value { + kind: Some(StructValue(Struct { + fields: dependencies, + })), + }, + ), + ( + String::from("details"), + Value { + kind: Some(StructValue(Struct { + fields: self.details.clone(), + })), + }, + ), + ]), + })), + } + } +} + +impl Default for RuntimeError { + fn default() -> Self { + Self::new("T-RT-000000", "RuntimeError", "Unknown runtime error") + } +} + +impl Error for RuntimeError {} + +impl Display for RuntimeError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}] {}: {}", self.code, self.category, self.message) + } +} + +fn now_unix_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|it| it.as_millis() as u64) + .unwrap_or(0) +} diff --git a/crates/taurus-core/src/types/execution/bindings.rs b/crates/taurus-core/src/types/execution/bindings.rs new file mode 100644 index 0000000..25efcfb --- /dev/null +++ b/crates/taurus-core/src/types/execution/bindings.rs @@ -0,0 +1,54 @@ +//! Argument binding and expression model for node execution. + +use tucana::shared::Value; + +use crate::types::execution::ids::{NodeId, ParameterId}; + +/// Path segment for nested lookups inside values. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ValuePathSegment { + /// Select list element by index. + Index(usize), + /// Select object field by key. + Field(String), +} + +/// Source reference used by argument expressions. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ReferenceSource { + /// Input value of the overall flow. + FlowInput, + /// Output result of another node. + NodeResult(NodeId), + /// Runtime input slot (used by iterators/predicates). + InputSlot { + node_id: NodeId, + parameter_index: i32, + input_index: i32, + }, +} + +/// Read expression from execution state. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ValueReference { + pub source: ReferenceSource, + pub path: Vec, +} + +/// Argument expression bound to a node parameter. +#[derive(Debug, Clone, PartialEq)] +pub enum ArgumentExpr { + /// Constant value literal. + ValueLiteral(Value), + /// Value resolved from runtime references. + Reference(ValueReference), + /// Deferred execution entry point (lazy function parameter). + DeferredCall(NodeId), +} + +/// Argument binding for one parameter. +#[derive(Debug, Clone, PartialEq)] +pub struct InputBinding { + pub parameter_id: ParameterId, + pub expression: ArgumentExpr, +} diff --git a/crates/taurus-core/src/types/execution/flow_ir.rs b/crates/taurus-core/src/types/execution/flow_ir.rs new file mode 100644 index 0000000..32d4305 --- /dev/null +++ b/crates/taurus-core/src/types/execution/flow_ir.rs @@ -0,0 +1,33 @@ +//! Static flow graph representation used by the execution engine. + +use std::collections::HashMap; + +use crate::types::execution::bindings::InputBinding; +use crate::types::execution::ids::{FlowId, NodeId}; + +/// Node execution location kind. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NodeKind { + /// Node executes in the local Taurus runtime. + Local, + /// Node executes in a remote runtime/service. + Remote { service: String }, +} + +/// Node invocation metadata and wiring. +#[derive(Debug, Clone, PartialEq)] +pub struct FlowNode { + pub id: NodeId, + pub kind: NodeKind, + pub handler_id: String, + pub next: Option, + pub bindings: Vec, +} + +/// Immutable flow graph passed to the executor. +#[derive(Debug, Clone, PartialEq)] +pub struct FlowGraph { + pub id: FlowId, + pub start_node: NodeId, + pub nodes: HashMap, +} diff --git a/crates/taurus-core/src/types/execution/ids.rs b/crates/taurus-core/src/types/execution/ids.rs new file mode 100644 index 0000000..1fa558e --- /dev/null +++ b/crates/taurus-core/src/types/execution/ids.rs @@ -0,0 +1,53 @@ +//! Identifier newtypes for execution-domain entities. + +use std::fmt::{Display, Formatter}; + +/// Flow identifier. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct FlowId(pub i64); + +/// Node identifier in a flow graph. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct NodeId(pub i64); + +/// Frame identifier for nested call execution. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct FrameId(pub u64); + +/// Parameter identifier for handler signatures. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ParameterId(pub i32); + +/// Unique execution run identifier. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ExecutionId(pub String); + +impl Display for FlowId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Display for NodeId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Display for FrameId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Display for ParameterId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Display for ExecutionId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/crates/taurus-core/src/types/execution/mod.rs b/crates/taurus-core/src/types/execution/mod.rs new file mode 100644 index 0000000..bb081a8 --- /dev/null +++ b/crates/taurus-core/src/types/execution/mod.rs @@ -0,0 +1,11 @@ +//! Structured execution model types. +//! +//! This module provides a clean, runtime-oriented vocabulary for: +//! - flow graph representation +//! - parameter signatures and argument bindings +//! - stable identifiers + +pub mod bindings; +pub mod flow_ir; +pub mod ids; +pub mod signature; diff --git a/crates/taurus-core/src/types/execution/signature.rs b/crates/taurus-core/src/types/execution/signature.rs new file mode 100644 index 0000000..44a46ac --- /dev/null +++ b/crates/taurus-core/src/types/execution/signature.rs @@ -0,0 +1,28 @@ +//! Handler/function signature model for runtime execution. + +use crate::types::execution::ids::ParameterId; + +/// How a parameter argument should be evaluated by the executor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvaluationMode { + /// Argument is resolved before invoking the handler. + Eager, + /// Argument is provided as deferred executable call/input expression. + Deferred, +} + +/// Single parameter contract for a handler. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParameterSpec { + pub id: ParameterId, + pub name: String, + pub evaluation_mode: EvaluationMode, + pub required: bool, +} + +/// Complete handler contract used by registry and runtime checks. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HandlerSignature { + pub handler_id: String, + pub parameters: Vec, +} diff --git a/crates/taurus-core/src/types/exit_reason.rs b/crates/taurus-core/src/types/exit_reason.rs new file mode 100644 index 0000000..e44fec2 --- /dev/null +++ b/crates/taurus-core/src/types/exit_reason.rs @@ -0,0 +1,46 @@ +//! Final execution exit reason for runtime calls. +//! +//! This captures why an execution boundary (flow run or nested call frame) +//! ended. It is intentionally payload-free and stable for logging, metrics, +//! and control decisions. + +use std::fmt::{Display, Formatter}; + +/// Why execution ended at an execution boundary. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExitReason { + /// Execution reached normal completion with a success value. + Success, + /// Execution ended with a runtime failure. + Failure, + /// Execution ended due to an explicit `return`. + Return, + /// A `respond` signal reached this boundary. + /// + /// Note: in the top-level flow loop, `respond` is currently normalized to + /// success for continuation semantics. + Respond, + /// Execution ended due to an explicit `stop`. + Stop, +} + +impl ExitReason { + /// True when execution ended in an error state. + pub const fn is_failure(self) -> bool { + matches!(self, ExitReason::Failure) + } +} + +impl Display for ExitReason { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let label = match self { + ExitReason::Success => "success", + ExitReason::Failure => "failure", + ExitReason::Return => "return", + ExitReason::Respond => "respond", + ExitReason::Stop => "stop", + }; + + write!(f, "{label}") + } +} diff --git a/crates/taurus-core/src/types/mod.rs b/crates/taurus-core/src/types/mod.rs new file mode 100644 index 0000000..376ff00 --- /dev/null +++ b/crates/taurus-core/src/types/mod.rs @@ -0,0 +1,8 @@ +//! Shared runtime domain types. +//! +//! Split by concern: execution model, signal vocabulary, and error contracts. + +pub mod errors; +pub mod execution; +pub mod exit_reason; +pub mod signal; diff --git a/crates/taurus-core/src/types/signal.rs b/crates/taurus-core/src/types/signal.rs new file mode 100644 index 0000000..4183277 --- /dev/null +++ b/crates/taurus-core/src/types/signal.rs @@ -0,0 +1,104 @@ +//! Control-flow and result signals exchanged between runtime handlers and executor. +//! +//! This module defines the canonical signal vocabulary for Taurus runtime execution. + +use std::fmt::{Display, Formatter}; + +use tucana::shared::Value; + +use crate::types::errors::runtime_error::RuntimeError; +use crate::types::exit_reason::ExitReason; + +/// Runtime control signal emitted by function handlers and consumed by the executor. +/// +/// These signals model both value production and control-flow decisions. +/// The executor interprets each variant as follows: +/// +/// - [`Signal::Success`]: normal node completion; execution continues through `next_node_id`. +/// - [`Signal::Failure`]: terminal error; current flow execution stops. +/// - [`Signal::Return`]: exits only the current call context. When returned from a lazily +/// executed child flow, the parent receives it as a successful value. +/// - [`Signal::Respond`]: out-of-band emission used for streaming replies; executor emits the +/// value via a configured callback and then continues the flow. +/// - [`Signal::Stop`]: explicit hard stop; execution ends immediately. +#[derive(Debug, Clone)] +pub enum Signal { + /// Node execution completed successfully with a value. + Success(Value), + /// Node execution failed with a runtime error. + Failure(RuntimeError), + /// Return from the current call frame with a value. + Return(Value), + /// Emit an intermediate response value without terminating execution. + Respond(Value), + /// Stop execution immediately. + Stop, +} + +impl Signal { + /// Return the terminal exit reason represented by this signal. + pub const fn exit_reason(&self) -> ExitReason { + match self { + Signal::Success(_) => ExitReason::Success, + Signal::Failure(_) => ExitReason::Failure, + Signal::Return(_) => ExitReason::Return, + Signal::Respond(_) => ExitReason::Respond, + Signal::Stop => ExitReason::Stop, + } + } + + /// True when the signal ends the current call-frame execution loop. + pub const fn is_terminal_in_frame(&self) -> bool { + matches!(self, Signal::Failure(_) | Signal::Return(_) | Signal::Stop) + } + + /// Borrow the value payload for value-carrying signals. + pub const fn value(&self) -> Option<&Value> { + match self { + Signal::Success(v) | Signal::Return(v) | Signal::Respond(v) => Some(v), + Signal::Failure(_) | Signal::Stop => None, + } + } + + /// Borrow the error payload when this is a failure signal. + pub const fn error(&self) -> Option<&RuntimeError> { + match self { + Signal::Failure(err) => Some(err), + Signal::Success(_) | Signal::Return(_) | Signal::Respond(_) | Signal::Stop => None, + } + } +} + +impl Display for Signal { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Signal::Success(_) => write!(f, "Signal(success)"), + Signal::Failure(err) => { + write!( + f, + "Signal(failure, code={}, category={}, message={})", + err.code, err.category, err.message + ) + } + Signal::Return(_) => write!(f, "Signal(return)"), + Signal::Respond(_) => write!(f, "Signal(respond)"), + Signal::Stop => write!(f, "Signal(stop)"), + } + } +} + +/// Partial equality by signal kind. +/// +/// Payload values are intentionally ignored to keep tests focused on control-flow shape. +impl PartialEq for Signal { + fn eq(&self, other: &Self) -> bool { + matches!( + (self, other), + (Signal::Success(_), Signal::Success(_)) + | (Signal::Failure(_), Signal::Failure(_)) + | (Signal::Return(_), Signal::Return(_)) + | (Signal::Stop, Signal::Stop) + | (Signal::Respond(_), Signal::Respond(_)) + ) + } +} From 930c48aa9b8aa2104d07d61c1dc6d3b2536c0985 Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 21 Apr 2026 17:54:31 +0200 Subject: [PATCH 16/28] feat: added taurus-core crate --- crates/taurus-core/Cargo.toml | 14 ++++++++ crates/taurus-core/src/lib.rs | 9 ++++++ crates/taurus-core/src/value.rs | 57 +++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 crates/taurus-core/Cargo.toml create mode 100644 crates/taurus-core/src/lib.rs create mode 100644 crates/taurus-core/src/value.rs diff --git a/crates/taurus-core/Cargo.toml b/crates/taurus-core/Cargo.toml new file mode 100644 index 0000000..fc6f7f9 --- /dev/null +++ b/crates/taurus-core/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "taurus-core" +version.workspace = true +edition.workspace = true + +[dependencies] +tucana = { workspace = true } +base64 = { workspace = true } +rand = { workspace = true } +log = { workspace = true } +futures-lite = { workspace = true } +async-trait = { workspace = true } +uuid = { workspace = true } + diff --git a/crates/taurus-core/src/lib.rs b/crates/taurus-core/src/lib.rs new file mode 100644 index 0000000..d742c93 --- /dev/null +++ b/crates/taurus-core/src/lib.rs @@ -0,0 +1,9 @@ +//! Taurus core runtime library. +//! +//! Exposes the runtime engine, strongly-typed execution contracts, and shared +//! value/error utilities used by runtime binaries. + +mod handler; +pub mod runtime; +pub mod types; +pub mod value; diff --git a/crates/taurus-core/src/value.rs b/crates/taurus-core/src/value.rs new file mode 100644 index 0000000..70ab9e3 --- /dev/null +++ b/crates/taurus-core/src/value.rs @@ -0,0 +1,57 @@ +//! Numeric/value conversion helpers used by runtime handlers. + +use tucana::shared::{NumberValue, Value, number_value, value::Kind}; + +pub fn number_value_from_f64(n: f64) -> NumberValue { + NumberValue { + number: Some(number_value::Number::Float(n)), + } +} + +pub fn number_value_from_i64(n: i64) -> NumberValue { + NumberValue { + number: Some(number_value::Number::Integer(n)), + } +} + +pub fn value_from_f64(n: f64) -> Value { + Value { + kind: Some(Kind::NumberValue(number_value_from_f64(n))), + } +} + +pub fn value_from_i64(n: i64) -> Value { + Value { + kind: Some(Kind::NumberValue(number_value_from_i64(n))), + } +} + +pub fn number_to_f64(n: &NumberValue) -> Option { + match n.number { + Some(number_value::Number::Integer(i)) => Some(i as f64), + Some(number_value::Number::Float(f)) => Some(f), + None => None, + } +} + +pub fn number_to_i64_lossy(n: &NumberValue) -> Option { + match n.number { + Some(number_value::Number::Integer(i)) => Some(i), + Some(number_value::Number::Float(f)) => { + if f.is_finite() { + Some(f as i64) + } else { + None + } + } + None => None, + } +} + +pub fn number_to_string(n: &NumberValue) -> String { + match n.number { + Some(number_value::Number::Integer(i)) => i.to_string(), + Some(number_value::Number::Float(f)) => f.to_string(), + None => "null".to_string(), + } +} From e24f964a4f47afeffd9e5fba204162c2bf9726f7 Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 21 Apr 2026 18:27:05 +0200 Subject: [PATCH 17/28] feat: redid tracer to be more readable --- .../src/runtime/engine/executor.rs | 20 +- .../src/runtime/execution/render.rs | 380 +++++++++++++----- .../src/runtime/execution/trace.rs | 159 +++++++- .../src/runtime/execution/tracer.rs | 71 +++- .../src/runtime/execution/value_store.rs | 70 +++- 5 files changed, 558 insertions(+), 142 deletions(-) diff --git a/crates/taurus-core/src/runtime/engine/executor.rs b/crates/taurus-core/src/runtime/engine/executor.rs index b1d305a..30d6c0a 100644 --- a/crates/taurus-core/src/runtime/engine/executor.rs +++ b/crates/taurus-core/src/runtime/engine/executor.rs @@ -156,14 +156,14 @@ impl<'a> EngineExecutor<'a> { // InputType references resolve against the currently running node. value_store.set_current_node_id(node.id); - let frame_id = self.trace_enter(node); + let frame_id = self.trace_enter(node, value_store); let signal = match &node.execution_target { NodeExecutionTarget::Local => self.execute_local_node(node, value_store, frame_id), NodeExecutionTarget::Remote { service } => { self.execute_remote_node(node, service, value_store, frame_id) } }; - self.trace_exit(frame_id, &signal); + self.trace_exit(frame_id, &signal, value_store); NodeExecutionResult { signal, frame_id } } @@ -519,15 +519,17 @@ impl<'a> EngineExecutor<'a> { } } - fn trace_enter(&self, node: &CompiledNode) -> Option { + fn trace_enter(&self, node: &CompiledNode, value_store: &ValueStore) -> Option { self.tracer.map(|tracer| { - tracer - .borrow_mut() - .enter_node(node.id, node.handler_id.as_str()) + tracer.borrow_mut().enter_node( + node.id, + node.handler_id.as_str(), + value_store.trace_snapshot(), + ) }) } - fn trace_exit(&self, frame_id: Option, signal: &Signal) { + fn trace_exit(&self, frame_id: Option, signal: &Signal, value_store: &ValueStore) { let Some(frame_id) = frame_id else { return; }; @@ -550,7 +552,9 @@ impl<'a> EngineExecutor<'a> { }, Signal::Stop => Outcome::Stop, }; - tracer.borrow_mut().exit_node(frame_id, outcome); + tracer + .borrow_mut() + .exit_node(frame_id, outcome, value_store.trace_snapshot()); } fn trace_record_arg(&self, frame_id: Option, arg: ArgTrace) { diff --git a/crates/taurus-core/src/runtime/execution/render.rs b/crates/taurus-core/src/runtime/execution/render.rs index 4f116b5..d34a130 100644 --- a/crates/taurus-core/src/runtime/execution/render.rs +++ b/crates/taurus-core/src/runtime/execution/render.rs @@ -1,42 +1,107 @@ -//! Human-readable trace renderer. +//! Human-readable trace renderer (single hybrid mode). use std::collections::HashMap; -use crate::runtime::execution::trace::{ArgKind, EdgeKind, ExecFrame, Outcome, TraceRun}; +use crate::runtime::execution::trace::{ + ArgKind, EdgeKind, Outcome, StoreDiff, StoreResultStatus, TraceFrame, TraceRun, +}; -fn frame_micros(frame: &ExecFrame) -> Option { +struct TraceTheme; + +impl TraceTheme { + fn paint(&self, text: &str, code: &str) -> String { + format!("\x1b[{}m{}\x1b[0m", code, text) + } + + fn enter(&self) -> String { + self.paint("ENTER", "36") + } + fn ctx(&self) -> String { + self.paint("CTX", "90") + } + fn arg(&self) -> String { + self.paint("ARG", "94") + } + fn call(&self) -> String { + self.paint("CALL", "35") + } + fn exit(&self) -> String { + self.paint("EXIT", "33") + } + fn store(&self) -> String { + self.paint("STORE", "32") + } + fn success(&self, value: &str) -> String { + format!("{} {}", self.paint("SUCCESS", "32"), value) + } + fn failure(&self, value: &str) -> String { + format!("{} {}", self.paint("FAILURE", "31"), value) + } + fn returned(&self, value: &str) -> String { + format!("{} {}", self.paint("RETURN", "33"), value) + } + fn respond(&self, value: &str) -> String { + format!("{} {}", self.paint("RESPOND", "35"), value) + } + fn stop(&self) -> String { + self.paint("STOP", "31") + } +} + +fn frame_micros(frame: &TraceFrame) -> Option { frame - .end - .map(|end| end.duration_since(frame.start).as_micros()) + .ended_at + .map(|end| end.duration_since(frame.started_at).as_micros()) } -/// Render a trace as plain-text tree output. +/// Render trace in execution order with branch markers and spacing. pub fn render_trace(run: &TraceRun) -> String { - let mut by_id: HashMap = HashMap::new(); + let theme = TraceTheme; + let mut by_id: HashMap = HashMap::new(); for frame in &run.frames { by_id.insert(frame.frame_id, frame); } - let mut output = String::new(); - if let Some(total_us) = total_duration_us(&run.frames) { - output.push_str(&format!("Total: {}us\n", total_us)); + let mut out = String::new(); + if let Some(total_us) = total_duration_us(run) { + out.push_str(&format!("Total: {}us\n", total_us)); } - render_frame(run.root, &by_id, "", true, &mut output); - if let Some(total_us) = total_duration_us(&run.frames) { - output.push_str(&format!("Summary: total_time={}us\n", total_us)); + + let mut step = 1usize; + render_frame( + run.root_frame_id, + &by_id, + "", + true, + &theme, + &mut step, + &mut out, + ); + + if let Some(total_us) = total_duration_us(run) { + out.push_str(&format!( + "Summary: total_time={}us frames={}\n", + total_us, + run.frames.len() + )); + } else { + out.push_str(&format!("Summary: frames={}\n", run.frames.len())); } - output + out } fn render_frame( - id: u64, - by_id: &HashMap, + frame_id: u64, + by_id: &HashMap, prefix: &str, is_last: bool, - output: &mut String, + theme: &TraceTheme, + step: &mut usize, + out: &mut String, ) { - let frame = by_id[&id]; - + let frame = by_id[&frame_id]; + let depth_indent = " ".repeat(frame.depth); + let display_prefix = format!("{}{}", depth_indent, prefix); let branch = if prefix.is_empty() { "" } else if is_last { @@ -44,33 +109,53 @@ fn render_frame( } else { "+- " }; - + let continuation = if prefix.is_empty() { + "" + } else if is_last { + " " + } else { + "| " + }; let duration = frame_micros(frame) .map(|us| format!(" ({}us)", us)) .unwrap_or_default(); - output.push_str(&format!( - "{prefix}{branch}#{frame_id} node={node_id} fn={function}{duration}\n", - prefix = prefix, + if frame.depth == 0 && *step > 1 { + out.push('\n'); + } + + out.push_str(&format!( + "{step:04} {display_prefix}{branch}{enter:<5} #{id} node={node} fn={fn_name} depth={depth}{duration}\n", + step = *step, + display_prefix = display_prefix, branch = branch, - frame_id = frame.frame_id, - node_id = frame.node_id, - function = frame.function_name, + enter = theme.enter(), + id = frame.frame_id, + node = frame.node_id, + fn_name = frame.function_name, + depth = frame.depth, duration = duration )); + *step += 1; - for arg in &frame.args { - let continuation = if prefix.is_empty() || is_last { - " " - } else { - "| " - }; + out.push_str(&format!( + "{step:04} {display_prefix}{continuation}{ctx:<5} current_node={} results={} input_slots={}\n", + frame.store_before.current_node_id, + frame.store_before.results.len(), + frame.store_before.input_slots.len(), + step = *step, + display_prefix = display_prefix, + continuation = continuation, + ctx = theme.ctx() + )); + *step += 1; - let kind = match &arg.kind { - ArgKind::Literal => "lit".to_string(), + for arg in &frame.args { + let arg_kind = match &arg.kind { + ArgKind::Literal => "literal".to_string(), ArgKind::Reference { reference, hit } => { - let suffix = if *hit { "hit" } else { "miss" }; - format!("ref({:?}, {})", reference, suffix) + let hit_state = if *hit { "hit" } else { "miss" }; + format!("reference {:?} ({})", reference, hit_state) } ArgKind::Thunk { node_id, @@ -78,95 +163,188 @@ fn render_frame( executed, } => { let mode = if *eager { "eager" } else { "lazy" }; - let exec = if *executed { "executed" } else { "deferred" }; - format!("thunk(node={}, {}, {})", node_id, mode, exec) + let executed_state = if *executed { "executed" } else { "deferred" }; + format!("thunk node={} {} {}", node_id, mode, executed_state) } }; - - output.push_str(&format!( - "{prefix}{continuation} arg[{index}] {kind:<24} {preview}\n", - prefix = prefix, + out.push_str(&format!( + "{step:04} {display_prefix}{continuation}{arg:<5} [{index}] {kind} => {preview}\n", + step = *step, + display_prefix = display_prefix, continuation = continuation, + arg = theme.arg(), index = arg.index, - kind = kind, + kind = arg_kind, preview = arg.preview )); + *step += 1; } - let outcome = match &frame.outcome { - Some(Outcome::Success { value_preview }) => format!("[SUCCESS] {}", value_preview), - Some(Outcome::Failure { error_preview }) => format!("[FAILURE] {}", error_preview), - Some(Outcome::Return { value_preview }) => format!("[RETURN] {}", value_preview), - Some(Outcome::Respond { value_preview }) => format!("[RESPOND] {}", value_preview), - Some(Outcome::Stop) => "[STOP]".to_string(), - None => "...".to_string(), - }; + let mut runtime_idx = 0usize; + for (idx, child) in frame.children.iter().enumerate() { + let child_is_last = idx + 1 == frame.children.len(); + let edge_branch = if child_is_last { "\\- " } else { "+- " }; + let edge_text = match &child.edge { + EdgeKind::Next => "next".to_string(), + EdgeKind::EagerCall { arg_index } => format!("eager arg#{}", arg_index), + EdgeKind::RuntimeCall { label } => { + runtime_idx += 1; + match label { + Some(label) => format!("runtime call #{} {}", runtime_idx, label), + None => format!("runtime call #{}", runtime_idx), + } + } + }; + out.push_str(&format!( + "{step:04} {display_prefix}{continuation}{edge_branch}{call:<5} #{parent} -> #{child} ({edge})\n", + step = *step, + display_prefix = display_prefix, + continuation = continuation, + edge_branch = edge_branch, + call = theme.call(), + parent = frame.frame_id, + child = child.child_frame_id, + edge = edge_text + )); + *step += 1; - let continuation = if prefix.is_empty() || is_last { - " " - } else { - "| " + let child_prefix = format!("{}{}", prefix, continuation); + render_frame( + child.child_frame_id, + by_id, + &child_prefix, + child_is_last, + theme, + step, + out, + ); + } + + let outcome = match &frame.outcome { + Some(Outcome::Success { value_preview }) => theme.success(value_preview), + Some(Outcome::Failure { error_preview }) => theme.failure(error_preview), + Some(Outcome::Return { value_preview }) => theme.returned(value_preview), + Some(Outcome::Respond { value_preview }) => theme.respond(value_preview), + Some(Outcome::Stop) => theme.stop(), + None => "INCOMPLETE".to_string(), }; - output.push_str(&format!( - "{prefix}{continuation} => {outcome}\n", - prefix = prefix, + out.push_str(&format!( + "{step:04} {display_prefix}{continuation}{exit:<5} #{id} signal={outcome}\n", + step = *step, + display_prefix = display_prefix, continuation = continuation, + exit = theme.exit(), + id = frame.frame_id, outcome = outcome )); + *step += 1; - if frame.children.is_empty() { - return; + if let Some(diff) = &frame.store_diff { + render_store_diff(&display_prefix, continuation, theme, diff, step, out); } +} - let mut runtime_idx = 0usize; - for (idx, (edge, child_id)) in frame.children.iter().enumerate() { - let edge_last = idx + 1 == frame.children.len(); - let edge_branch = if edge_last { "\\- " } else { "+- " }; +fn render_store_diff( + prefix: &str, + continuation: &str, + theme: &TraceTheme, + diff: &StoreDiff, + step: &mut usize, + out: &mut String, +) { + if let Some((from, to)) = diff.current_node_changed { + out.push_str(&format!( + "{step:04} {prefix}{continuation}{store:<5} current_node: {} -> {}\n", + from, + to, + step = *step, + prefix = prefix, + continuation = continuation, + store = theme.store() + )); + *step += 1; + } - let edge_text = match edge { - EdgeKind::Next => "NEXT".to_string(), - EdgeKind::EagerCall { arg_index } => format!("eager(arg#{})", arg_index), - EdgeKind::RuntimeCall { label } => { - runtime_idx += 1; - match label { - Some(label_text) => format!("runtime(call #{}) {}", runtime_idx, label_text), - None => format!("runtime(call #{})", runtime_idx), - } - } + for set in &diff.result_sets { + let status = match set.status { + StoreResultStatus::Success => "success", + StoreResultStatus::Error => "error", }; - - output.push_str(&format!( - "{prefix}{edge_branch}{edge_text}\n", + out.push_str(&format!( + "{step:04} {prefix}{continuation}{store:<5} result.set node={} [{}] {}\n", + set.node_id, + status, + set.preview, + step = *step, prefix = prefix, - edge_branch = edge_branch, - edge_text = edge_text + continuation = continuation, + store = theme.store() )); + *step += 1; + } + for cleared in &diff.result_clears { + out.push_str(&format!( + "{step:04} {prefix}{continuation}{store:<5} result.clear node={}\n", + cleared, + step = *step, + prefix = prefix, + continuation = continuation, + store = theme.store() + )); + *step += 1; + } - let child_prefix = format!("{}{}", prefix, if edge_last { " " } else { "| " }); - render_frame(*child_id, by_id, &child_prefix, true, output); + for set in &diff.input_slot_sets { + out.push_str(&format!( + "{step:04} {prefix}{continuation}{store:<5} input.set node={} param={} input={} {}\n", + set.node_id, + set.parameter_index, + set.input_index, + set.preview, + step = *step, + prefix = prefix, + continuation = continuation, + store = theme.store() + )); + *step += 1; + } + for (node_id, parameter_index, input_index) in &diff.input_slot_clears { + out.push_str(&format!( + "{step:04} {prefix}{continuation}{store:<5} input.clear node={} param={} input={}\n", + node_id, + parameter_index, + input_index, + step = *step, + prefix = prefix, + continuation = continuation, + store = theme.store() + )); + *step += 1; } } -fn total_duration_us(frames: &[ExecFrame]) -> Option { - let mut start = None; - let mut end = None; - - for frame in frames { - start = Some(match start { - Some(current) if frame.start > current => current, - Some(_) | None => frame.start, - }); - - if let Some(frame_end) = frame.end { - end = Some(match end { - Some(current) if frame_end < current => current, - Some(_) | None => frame_end, - }); +fn total_duration_us(run: &TraceRun) -> Option { + match run.ended_at { + Some(end) => Some(end.duration_since(run.started_at).as_micros()), + None => { + let mut start = None; + let mut end = None; + for frame in &run.frames { + start = Some(match start { + Some(current) if frame.started_at > current => current, + Some(_) | None => frame.started_at, + }); + if let Some(frame_end) = frame.ended_at { + end = Some(match end { + Some(current) if frame_end < current => current, + Some(_) | None => frame_end, + }); + } + } + match (start, end) { + (Some(start), Some(end)) => Some(end.duration_since(start).as_micros()), + _ => None, + } } } - - match (start, end) { - (Some(start_time), Some(end_time)) => Some(end_time.duration_since(start_time).as_micros()), - _ => None, - } } diff --git a/crates/taurus-core/src/runtime/execution/trace.rs b/crates/taurus-core/src/runtime/execution/trace.rs index 0b8e1d6..9733566 100644 --- a/crates/taurus-core/src/runtime/execution/trace.rs +++ b/crates/taurus-core/src/runtime/execution/trace.rs @@ -1,8 +1,12 @@ -//! Runtime execution trace model. +//! Trace V2 runtime execution model. //! -//! This model captures node frames, argument resolution, control-flow edges, -//! and final outcomes for one execution run. +//! The model is frame-centric and stores: +//! - resolved arguments +//! - parent/child control-flow edges +//! - before/after execution store snapshots +//! - computed store diff for each frame +use std::collections::HashMap; use std::time::Instant; /// Relationship between two execution frames. @@ -64,22 +68,155 @@ pub enum Outcome { Stop, } -/// One executed node invocation. +/// One stored node result entry at snapshot time. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StoreResultEntry { + pub node_id: i64, + pub status: StoreResultStatus, + pub preview: String, +} + +/// Result status in the value store. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StoreResultStatus { + Success, + Error, +} + +/// One temporary input slot entry at snapshot time. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StoreInputSlotEntry { + pub node_id: i64, + pub parameter_index: i64, + pub input_index: i64, + pub preview: String, +} + +/// Value store snapshot attached to a frame boundary. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StoreSnapshot { + pub current_node_id: i64, + pub flow_input_preview: String, + pub results: Vec, + pub input_slots: Vec, +} + +/// Per-frame store changes between `store_before` and `store_after`. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct StoreDiff { + pub current_node_changed: Option<(i64, i64)>, + pub result_sets: Vec, + pub result_clears: Vec, + pub input_slot_sets: Vec, + pub input_slot_clears: Vec<(i64, i64, i64)>, +} + +impl StoreDiff { + pub fn from(before: &StoreSnapshot, after: &StoreSnapshot) -> Self { + let current_node_changed = (before.current_node_id != after.current_node_id) + .then_some((before.current_node_id, after.current_node_id)); + + let mut before_results: HashMap = HashMap::new(); + for entry in &before.results { + before_results.insert(entry.node_id, entry); + } + let mut after_results: HashMap = HashMap::new(); + for entry in &after.results { + after_results.insert(entry.node_id, entry); + } + + let mut result_sets = Vec::new(); + for (node_id, after_entry) in &after_results { + match before_results.get(node_id) { + Some(before_entry) + if before_entry.status == after_entry.status + && before_entry.preview == after_entry.preview => {} + _ => result_sets.push((*after_entry).clone()), + } + } + result_sets.sort_by_key(|entry| entry.node_id); + + let mut result_clears = Vec::new(); + for node_id in before_results.keys() { + if !after_results.contains_key(node_id) { + result_clears.push(*node_id); + } + } + result_clears.sort_unstable(); + + let mut before_inputs: HashMap<(i64, i64, i64), &StoreInputSlotEntry> = HashMap::new(); + for entry in &before.input_slots { + before_inputs.insert( + (entry.node_id, entry.parameter_index, entry.input_index), + entry, + ); + } + let mut after_inputs: HashMap<(i64, i64, i64), &StoreInputSlotEntry> = HashMap::new(); + for entry in &after.input_slots { + after_inputs.insert( + (entry.node_id, entry.parameter_index, entry.input_index), + entry, + ); + } + + let mut input_slot_sets = Vec::new(); + for (key, after_entry) in &after_inputs { + match before_inputs.get(key) { + Some(before_entry) if before_entry.preview == after_entry.preview => {} + _ => input_slot_sets.push((*after_entry).clone()), + } + } + input_slot_sets + .sort_by_key(|entry| (entry.node_id, entry.parameter_index, entry.input_index)); + + let mut input_slot_clears = Vec::new(); + for key in before_inputs.keys() { + if !after_inputs.contains_key(key) { + input_slot_clears.push(*key); + } + } + input_slot_clears.sort_unstable(); + + Self { + current_node_changed, + result_sets, + result_clears, + input_slot_sets, + input_slot_clears, + } + } +} + +/// Child link entry for one frame. +#[derive(Debug, Clone)] +pub struct FrameChild { + pub edge: EdgeKind, + pub child_frame_id: u64, +} + +/// One executed node invocation frame. #[derive(Debug, Clone)] -pub struct ExecFrame { +pub struct TraceFrame { pub frame_id: u64, + pub parent_frame_id: Option, + pub depth: usize, pub node_id: i64, pub function_name: String, pub args: Vec, pub outcome: Option, - pub start: Instant, - pub end: Option, - pub children: Vec<(EdgeKind, u64)>, + pub started_at: Instant, + pub ended_at: Option, + pub children: Vec, + pub store_before: StoreSnapshot, + pub store_after: Option, + pub store_diff: Option, } -/// Trace data for a full execution. +/// Trace data for one full execution run. #[derive(Debug, Clone)] pub struct TraceRun { - pub frames: Vec, - pub root: u64, + pub started_at: Instant, + pub ended_at: Option, + pub root_frame_id: u64, + pub frames: Vec, } diff --git a/crates/taurus-core/src/runtime/execution/tracer.rs b/crates/taurus-core/src/runtime/execution/tracer.rs index 3fb81a0..894060d 100644 --- a/crates/taurus-core/src/runtime/execution/tracer.rs +++ b/crates/taurus-core/src/runtime/execution/tracer.rs @@ -1,23 +1,27 @@ -//! In-memory execution trace collector. +//! In-memory Trace V2 collector. use std::time::Instant; -use crate::runtime::execution::trace::{ArgKind, ArgTrace, EdgeKind, ExecFrame, Outcome, TraceRun}; +use crate::runtime::execution::trace::{ + ArgKind, ArgTrace, EdgeKind, FrameChild, Outcome, StoreDiff, StoreSnapshot, TraceFrame, + TraceRun, +}; /// Trace collector interface used by the executor. pub trait ExecutionTracer { - fn enter_node(&mut self, node_id: i64, function_name: &str) -> u64; + fn enter_node(&mut self, node_id: i64, function_name: &str, store_before: StoreSnapshot) + -> u64; fn record_arg(&mut self, frame_id: u64, arg: ArgTrace); fn link_child(&mut self, parent_frame: u64, child_frame: u64, edge: EdgeKind); fn mark_thunk(&mut self, frame_id: u64, arg_index: usize, eager: bool, executed: bool); fn mark_thunk_executed_by_node(&mut self, frame_id: u64, node_id: i64); - fn exit_node(&mut self, frame_id: u64, outcome: Outcome); + fn exit_node(&mut self, frame_id: u64, outcome: Outcome, store_after: StoreSnapshot); } /// Default trace recorder used by the runtime engine. pub struct Tracer { next_id: u64, - pub run: Option, + run: Option, stack: Vec, } @@ -36,7 +40,7 @@ impl Tracer { } } - fn frames_mut(&mut self) -> &mut Vec { + fn frames_mut(&mut self) -> &mut Vec { &mut self .run .as_mut() @@ -44,7 +48,7 @@ impl Tracer { .frames } - fn get_frame_mut(&mut self, frame_id: u64) -> &mut ExecFrame { + fn get_frame_mut(&mut self, frame_id: u64) -> &mut TraceFrame { let idx = self .frames_mut() .iter() @@ -59,34 +63,48 @@ impl Tracer { } impl ExecutionTracer for Tracer { - fn enter_node(&mut self, node_id: i64, function_name: &str) -> u64 { + fn enter_node( + &mut self, + node_id: i64, + function_name: &str, + store_before: StoreSnapshot, + ) -> u64 { if self.run.is_none() { self.run = Some(TraceRun { + started_at: Instant::now(), + ended_at: None, frames: vec![], - root: 0, + root_frame_id: 0, }); } let frame_id = self.next_id; self.next_id += 1; + let parent_frame_id = self.stack.last().copied(); + let depth = self.stack.len(); - let frame = ExecFrame { + let frame = TraceFrame { frame_id, + parent_frame_id, + depth, node_id, function_name: function_name.to_string(), args: vec![], outcome: None, - start: Instant::now(), - end: None, + started_at: Instant::now(), + ended_at: None, children: vec![], + store_before, + store_after: None, + store_diff: None, }; let run = self .run .as_mut() .expect("trace run must exist before first frame"); - if run.root == 0 { - run.root = frame_id; + if run.root_frame_id == 0 { + run.root_frame_id = frame_id; } run.frames.push(frame); @@ -99,9 +117,10 @@ impl ExecutionTracer for Tracer { } fn link_child(&mut self, parent_frame: u64, child_frame: u64, edge: EdgeKind) { - self.get_frame_mut(parent_frame) - .children - .push((edge, child_frame)); + self.get_frame_mut(parent_frame).children.push(FrameChild { + edge, + child_frame_id: child_frame, + }); } fn mark_thunk(&mut self, frame_id: u64, arg_index: usize, eager: bool, executed: bool) { @@ -146,12 +165,22 @@ impl ExecutionTracer for Tracer { } } - fn exit_node(&mut self, frame_id: u64, outcome: Outcome) { - let frame = self.get_frame_mut(frame_id); - frame.outcome = Some(outcome); - frame.end = Some(Instant::now()); + fn exit_node(&mut self, frame_id: u64, outcome: Outcome, store_after: StoreSnapshot) { + { + let frame = self.get_frame_mut(frame_id); + frame.outcome = Some(outcome); + frame.ended_at = Some(Instant::now()); + frame.store_diff = Some(StoreDiff::from(&frame.store_before, &store_after)); + frame.store_after = Some(store_after); + } let popped = self.stack.pop(); debug_assert_eq!(popped, Some(frame_id)); + + if self.stack.is_empty() + && let Some(run) = self.run.as_mut() + { + run.ended_at = Some(Instant::now()); + } } } diff --git a/crates/taurus-core/src/runtime/execution/value_store.rs b/crates/taurus-core/src/runtime/execution/value_store.rs index ac61591..20d852d 100644 --- a/crates/taurus-core/src/runtime/execution/value_store.rs +++ b/crates/taurus-core/src/runtime/execution/value_store.rs @@ -4,6 +4,9 @@ use std::collections::HashMap; use tucana::shared::{InputType, ReferenceValue, Value, value::Kind}; +use crate::runtime::execution::trace::{ + StoreInputSlotEntry, StoreResultEntry, StoreResultStatus, StoreSnapshot, +}; use crate::types::errors::runtime_error::RuntimeError; #[derive(Clone)] @@ -33,7 +36,7 @@ impl ValueStore { } } - pub fn get_current_node_id(&mut self) -> i64 { + pub fn get_current_node_id(&self) -> i64 { self.current_node_id } @@ -142,4 +145,69 @@ impl ValueStore { pub fn pop_runtime_trace_label(&mut self) -> Option { self.runtime_trace_labels.pop() } + + pub fn trace_snapshot(&self) -> StoreSnapshot { + let mut results = Vec::with_capacity(self.results.len()); + for (node_id, result) in &self.results { + match result { + ValueStoreResult::Success(value) => results.push(StoreResultEntry { + node_id: *node_id, + status: StoreResultStatus::Success, + preview: preview_value(value), + }), + ValueStoreResult::Error(err) => results.push(StoreResultEntry { + node_id: *node_id, + status: StoreResultStatus::Error, + preview: format!("{}:{} {}", err.code, err.category, err.message), + }), + ValueStoreResult::NotFound => {} + } + } + results.sort_by_key(|entry| entry.node_id); + + let mut input_slots = Vec::with_capacity(self.input_types.len()); + for (input, value) in &self.input_types { + input_slots.push(StoreInputSlotEntry { + node_id: input.node_id, + parameter_index: input.parameter_index, + input_index: input.input_index, + preview: preview_value(value), + }); + } + input_slots.sort_by_key(|entry| (entry.node_id, entry.parameter_index, entry.input_index)); + + StoreSnapshot { + current_node_id: self.current_node_id, + flow_input_preview: preview_value(&self.flow_input), + results, + input_slots, + } + } +} + +fn preview_value(value: &Value) -> String { + match value.kind.as_ref() { + Some(Kind::NumberValue(v)) => crate::value::number_to_string(v), + Some(Kind::BoolValue(v)) => v.to_string(), + Some(Kind::StringValue(v)) => format!("{:?}", v), + Some(Kind::NullValue(_)) | None => "null".to_string(), + Some(Kind::ListValue(list)) => { + let mut parts = Vec::with_capacity(list.values.len()); + for item in &list.values { + parts.push(preview_value(item)); + } + format!("[{}]", parts.join(", ")) + } + Some(Kind::StructValue(struct_value)) => { + let mut keys: Vec<_> = struct_value.fields.keys().collect(); + keys.sort(); + let mut parts = Vec::with_capacity(keys.len()); + for key in keys { + if let Some(field_value) = struct_value.fields.get(key) { + parts.push(format!("{:?}: {}", key, preview_value(field_value))); + } + } + format!("{{{}}}", parts.join(", ")) + } + } } From 1deddc6e12b778024ed12c8f90a3a468a12f6d26 Mon Sep 17 00:00:00 2001 From: Raphael Date: Wed, 22 Apr 2026 18:36:42 +0200 Subject: [PATCH 18/28] feat: added draco as runtime local function --- crates/taurus-core/src/runtime/engine/compiler.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/taurus-core/src/runtime/engine/compiler.rs b/crates/taurus-core/src/runtime/engine/compiler.rs index 6fee00b..9118087 100644 --- a/crates/taurus-core/src/runtime/engine/compiler.rs +++ b/crates/taurus-core/src/runtime/engine/compiler.rs @@ -151,7 +151,10 @@ pub fn compile_flow( } fn execution_target_for(node: &NodeFunction) -> NodeExecutionTarget { - if node.definition_source.is_empty() || node.definition_source == "taurus" { + if node.definition_source.is_empty() + || node.definition_source == "taurus" + || node.definition_source.starts_with("draco") + { NodeExecutionTarget::Local } else { NodeExecutionTarget::Remote { From f50b637e75ffd8173a7eb03c0b7cb22ae10cc0ac Mon Sep 17 00:00:00 2001 From: Raphael Date: Wed, 22 Apr 2026 18:36:54 +0200 Subject: [PATCH 19/28] feat: added return inside filter test# --- flows/09_filter_return.json | 363 ++++++++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 flows/09_filter_return.json diff --git a/flows/09_filter_return.json b/flows/09_filter_return.json new file mode 100644 index 0000000..aa3cb79 --- /dev/null +++ b/flows/09_filter_return.json @@ -0,0 +1,363 @@ +{ + "name": "09_filter_return", + "description": "Checks if `return` works inside of a iterator", + "inputs": [ + { + "input": { + "http_method": "GET", + "headers": { + "Content-Type": "text/plain" + }, + "payload": { + "users": [ + { + "age": 22, + "email": "joe@text.com", + "name": "joe doe" + }, + { + "age": 56, + "email": "tom@text.com", + "name": "timmy doe" + }, + { + "age": 88, + "email": "willy@text.com", + "name": "willy wokner" + }, + { + "age": 12, + "email": "jeffy@text.com", + "name": "jeff jefferson" + } + ] + } + }, + "expected_result": { + "headers": { + "x": "y" + }, + "http_status_code": 200, + "payload": [ + "timmy doe", + "willy wokner" + ] + } + }, + { + "input": { + "http_method": "GET", + "headers": { + "Content-Type": "text/plain" + }, + "payload": { + "users": [ + { + "age": 11, + "email": "tom@text.com", + "name": "timmy doe" + }, + { + "age": 2, + "email": "willy@text.com", + "name": "willy wokner" + } + ] + } + }, + "expected_result": { + "headers": { + "x": "y" + }, + "http_status_code": 200, + "payload": [] + } + } + ], + "flow": { + "flowId": "1", + "projectId": "1", + "type": "REST", + "settings": [ + { + "databaseId": "1", + "flowSettingId": "httpURL", + "value": { + "stringValue": "/10" + } + }, + { + "databaseId": "2", + "flowSettingId": "httpMethod", + "value": { + "stringValue": "POST" + } + } + ], + "startingNodeId": "7", + "nodeFunctions": [ + { + "databaseId": "7", + "runtimeFunctionId": "std::list::filter", + "parameters": [ + { + "databaseId": "14", + "runtimeParameterId": "list", + "value": { + "referenceValue": { + "paths": [ + { + "path": "payload" + }, + { + "path": "users" + } + ], + "flowInput": {} + } + } + }, + { + "databaseId": "15", + "runtimeParameterId": "predicate", + "value": { + "nodeFunctionId": "8" + } + } + ], + "nextNodeId": "2" + }, + { + "databaseId": "8", + "runtimeFunctionId": "std::number::is_greater", + "parameters": [ + { + "databaseId": "16", + "runtimeParameterId": "first", + "value": { + "referenceValue": { + "paths": [ + { + "path": "age" + } + ], + "inputType": { + "nodeId": "7", + "parameterIndex": "1" + } + } + } + }, + { + "databaseId": "20", + "runtimeParameterId": "second", + "value": { + "literalValue": { + "numberValue": { + "integer": "35" + } + } + } + } + ], + "nextNodeId": "9" + }, + { + "databaseId": "9", + "runtimeFunctionId": "std::control::if_else", + "parameters": [ + { + "databaseId": "17", + "runtimeParameterId": "condition", + "value": { + "referenceValue": { + "nodeId": "8" + } + } + }, + { + "databaseId": "21", + "runtimeParameterId": "runnable", + "value": { + "nodeFunctionId": "1" + } + }, + { + "databaseId": "22", + "runtimeParameterId": "else_runnable", + "value": { + "nodeFunctionId": "3" + } + } + ], + "nextNodeId": "5" + }, + { + "databaseId": "1", + "runtimeFunctionId": "std::control::return", + "parameters": [ + { + "databaseId": "1", + "runtimeParameterId": "value", + "value": { + "literalValue": { + "boolValue": true + } + } + } + ] + }, + { + "databaseId": "3", + "runtimeFunctionId": "std::control::return", + "parameters": [ + { + "databaseId": "6", + "runtimeParameterId": "value", + "value": { + "literalValue": { + "boolValue": false + } + } + } + ] + }, + { + "databaseId": "5", + "runtimeFunctionId": "std::control::return", + "parameters": [ + { + "databaseId": "10", + "runtimeParameterId": "value", + "value": { + "literalValue": { + "boolValue": true + } + } + } + ] + }, + { + "databaseId": "2", + "runtimeFunctionId": "std::list::size", + "parameters": [ + { + "databaseId": "3", + "runtimeParameterId": "list", + "value": { + "referenceValue": { + "nodeId": "7" + } + } + } + ], + "nextNodeId": "10" + }, + { + "databaseId": "6", + "runtimeFunctionId": "std::control::return", + "parameters": [ + { + "databaseId": "13", + "runtimeParameterId": "value", + "value": { + "referenceValue": { + "paths": [ + { + "path": "name" + } + ], + "inputType": { + "nodeId": "10", + "parameterIndex": "1" + } + } + } + } + ] + }, + { + "databaseId": "10", + "runtimeFunctionId": "std::list::map", + "parameters": [ + { + "databaseId": "23", + "runtimeParameterId": "list", + "value": { + "referenceValue": { + "nodeId": "7" + } + } + }, + { + "databaseId": "24", + "runtimeParameterId": "transform", + "value": { + "nodeFunctionId": "6" + } + } + ], + "nextNodeId": "11" + }, + { + "databaseId": "11", + "runtimeFunctionId": "http::response::create", + "parameters": [ + { + "databaseId": "27", + "runtimeParameterId": "http_status_code", + "value": { + "literalValue": { + "numberValue": { + "integer": "200" + } + } + } + }, + { + "databaseId": "30", + "runtimeParameterId": "headers", + "value": { + "literalValue": { + "structValue": { + "fields": { + "x": { + "stringValue": "y" + } + } + } + } + } + }, + { + "databaseId": "31", + "runtimeParameterId": "payload", + "value": { + "referenceValue": { + "nodeId": "10" + } + } + } + ], + "nextNodeId": "4" + }, + { + "databaseId": "4", + "runtimeFunctionId": "rest::control::respond", + "parameters": [ + { + "databaseId": "8", + "runtimeParameterId": "http_response", + "value": { + "referenceValue": { + "nodeId": "11" + } + } + } + ] + } + ], + "projectSlug": "project", + "signature": "(httpURL: HTTP_URL, httpMethod: HTTP_METHOD): { payload: { users: { name: TEXT, email: TEXT, age: NUMBER }[] }, headers: I }" + } +} From 1eda2b292f5eebe5adc854ad76b0ff83a93d551c Mon Sep 17 00:00:00 2001 From: Raphael Date: Wed, 22 Apr 2026 19:14:50 +0200 Subject: [PATCH 20/28] feat: correct error codes --- crates/taurus-core/src/ERROR_CODES.md | 31 ++++++++++++++ crates/taurus-core/src/handler/argument.rs | 4 +- crates/taurus-core/src/handler/macros.rs | 6 +-- .../src/runtime/engine/compiler.rs | 8 ++-- .../src/runtime/engine/executor.rs | 10 ++--- .../src/runtime/functions/array.rs | 40 +++++++++---------- .../src/runtime/functions/boolean.rs | 2 +- .../src/runtime/functions/control.rs | 4 +- .../taurus-core/src/runtime/functions/http.rs | 10 ++--- .../src/runtime/functions/number.rs | 14 +++---- .../taurus-core/src/runtime/functions/text.rs | 12 +++--- crates/taurus-core/src/types/errors/error.rs | 12 +++--- .../src/types/errors/runtime_error.rs | 4 +- 13 files changed, 94 insertions(+), 63 deletions(-) create mode 100644 crates/taurus-core/src/ERROR_CODES.md diff --git a/crates/taurus-core/src/ERROR_CODES.md b/crates/taurus-core/src/ERROR_CODES.md new file mode 100644 index 0000000..3274c7e --- /dev/null +++ b/crates/taurus-core/src/ERROR_CODES.md @@ -0,0 +1,31 @@ +# Taurus Runtime Error Codes + +This document is the canonical catalog for runtime error codes emitted by `taurus-core`. + +## Code Format + +- `T-STD-XXXXX`: Errors originating inside standard function implementations under `runtime/functions/*`. +- `T-CORE-XXXXXX`: Errors originating from core runtime infrastructure (`engine`, `handler`, type conversion, app-layer mapping). + +## Code Table + +| Code | Layer | Description | Typical Trigger | Primary Source | +| --- | --- | --- | --- | --- | +| `T-STD-00001` | Standard Functions | A standard runtime function failed due to invalid input shape/type, unsupported value semantics, or function-specific runtime constraints. | Wrong argument type, invalid value conversion, out-of-range operation, malformed function input. | `runtime/functions/*` | +| `T-CORE-000001` | Engine | Requested node id does not exist in the compiled flow plan. | Thunk/reference points to a node id not present in `CompiledFlow`. | `runtime/engine/executor.rs` | +| `T-CORE-000002` | Engine | Handler registry has no implementation for the node's runtime function id. | Function id was not registered in `FunctionStore`. | `runtime/engine/executor.rs` | +| `T-CORE-000003` | Engine | Flow requires remote execution but no remote runtime adapter was configured. | Node execution target is remote while `RemoteRuntime` is `None`. | `runtime/engine/executor.rs` | +| `T-CORE-000004` | Engine | Reference lookup failed in the execution value store. | Missing prior node result, missing flow input path, or unresolved input reference. | `runtime/engine/executor.rs` | +| `T-CORE-000005` | Engine | Remote request cannot be assembled because parameter metadata and resolved values diverge. | Parameter count mismatch during remote request materialization. | `runtime/engine/executor.rs` | +| `T-CORE-000101` | Compiler | Flow compilation failed because a node id appears more than once. | Duplicate `database_id` in input nodes. | `runtime/engine/compiler.rs` | +| `T-CORE-000102` | Compiler | Flow compilation failed because the declared start node is absent. | `start_node_id` not found in node list. | `runtime/engine/compiler.rs` | +| `T-CORE-000103` | Compiler | Flow compilation failed because a `next` edge points to a missing node. | `next_node_id` references unknown node id. | `runtime/engine/compiler.rs` | +| `T-CORE-000104` | Compiler | Flow compilation failed because a parameter is structurally incomplete. | Parameter has no value payload in IR. | `runtime/engine/compiler.rs` | +| `T-CORE-000201` | Handler | Handler argument arity contract was violated before function execution began. | `args!`/`no_args!` macro expected different argument count. | `handler/macros.rs` | +| `T-CORE-000202` | Handler | Handler argument type conversion failed during typed extraction. | `TryFromArgument` expected type does not match provided argument. | `handler/argument.rs` | +| `T-CORE-000301` | App Error Mapping | Application configuration failure mapped into runtime error format. | Invalid/missing runtime config surfaced as `Error::Configuration`. | `types/errors/error.rs` | +| `T-CORE-000302` | App Error Mapping | Invalid application state mapped into runtime error format. | Illegal lifecycle/state transition surfaced as `Error::State`. | `types/errors/error.rs` | +| `T-CORE-000303` | App Error Mapping | Transport/dependency communication failure mapped into runtime error format. | Network/broker/downstream call failure surfaced as `Error::Transport`. | `types/errors/error.rs` | +| `T-CORE-000304` | App Error Mapping | Serialization/deserialization failure mapped into runtime error format. | Encoding/decoding/parsing failure surfaced as `Error::Serialization`. | `types/errors/error.rs` | +| `T-CORE-000399` | App Error Mapping | Internal application failure mapped into runtime error format. | Catch-all non-domain internal failure surfaced as `Error::Internal`. | `types/errors/error.rs` | +| `T-CORE-999999` | Runtime Error Fallback | Default fallback runtime error code when no explicit mapping is provided. | `RuntimeError::default()` used as defensive fallback. | `types/errors/runtime_error.rs` | diff --git a/crates/taurus-core/src/handler/argument.rs b/crates/taurus-core/src/handler/argument.rs index 139e3cb..c661a15 100644 --- a/crates/taurus-core/src/handler/argument.rs +++ b/crates/taurus-core/src/handler/argument.rs @@ -30,7 +30,7 @@ pub trait TryFromArgument: Sized { fn type_err(msg: &str, a: &Argument) -> Signal { Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-CORE-000202", "InvalidArgumentRuntimeError", format!("{} but it was the arugment: {:?}", msg, a), )) @@ -118,7 +118,7 @@ impl TryFromArgument for ListValue { kind: Some(Kind::ListValue(list)), }) => Ok(list.clone()), _ => Err(Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-CORE-000202", "InvalidArgumentRuntimeError", format!("Expected array (ListValue) but it was: {:?}", a), ))), diff --git a/crates/taurus-core/src/handler/macros.rs b/crates/taurus-core/src/handler/macros.rs index c897fdf..f9c238d 100644 --- a/crates/taurus-core/src/handler/macros.rs +++ b/crates/taurus-core/src/handler/macros.rs @@ -1,14 +1,14 @@ //! Handler argument parsing macros. /// Pulls typed parameters from a slice of `Argument` using your `TryFromArgument` -/// impls. Fails early with your `Signal::Failure(RuntimeError::new("T-RT-000000", ...))`. +/// impls. Fails early with your `Signal::Failure(RuntimeError::new("T-CORE-000201", ...))`. macro_rules! args { ($args_ident:ident => $( $name:ident : $ty:ty ),+ $(,)?) => { // Arity check let __expected: usize = 0usize $(+ { let _ = ::core::any::type_name::<$ty>(); 1usize })*; if $args_ident.len() != __expected { return $crate::types::signal::Signal::Failure( - $crate::types::errors::runtime_error::RuntimeError::new("T-RT-000000", + $crate::types::errors::runtime_error::RuntimeError::new("T-CORE-000201", "InvalidArgumentRuntimeError", format!("Expected {__expected} args but received {}", $args_ident.len()), ) @@ -43,7 +43,7 @@ macro_rules! no_args { if !$args_ident.is_empty() { return $crate::types::signal::Signal::Failure( $crate::types::errors::runtime_error::RuntimeError::new( - "T-RT-000000", + "T-CORE-000201", "InvalidArgumentRuntimeError", format!("Expected 0 args but received {}", $args_ident.len()), ), diff --git a/crates/taurus-core/src/runtime/engine/compiler.rs b/crates/taurus-core/src/runtime/engine/compiler.rs index 9118087..3f4ae57 100644 --- a/crates/taurus-core/src/runtime/engine/compiler.rs +++ b/crates/taurus-core/src/runtime/engine/compiler.rs @@ -33,12 +33,12 @@ impl CompileError { pub fn as_runtime_error(&self) -> RuntimeError { match self { CompileError::DuplicateNodeId { node_id } => RuntimeError::new( - "T-RT-000000", + "T-CORE-000101", "FlowCompileError", format!("Duplicate node id in flow: {}", node_id), ), CompileError::StartNodeMissing { node_id } => RuntimeError::new( - "T-RT-000000", + "T-CORE-000102", "FlowCompileError", format!("Start node not found in flow: {}", node_id), ), @@ -46,7 +46,7 @@ impl CompileError { node_id, next_node_id, } => RuntimeError::new( - "T-RT-000000", + "T-CORE-000103", "FlowCompileError", format!( "Node {} points to missing next node {}", @@ -57,7 +57,7 @@ impl CompileError { node_id, parameter_index, } => RuntimeError::new( - "T-RT-000000", + "T-CORE-000104", "FlowCompileError", format!( "Node {} parameter {} does not contain a value", diff --git a/crates/taurus-core/src/runtime/engine/executor.rs b/crates/taurus-core/src/runtime/engine/executor.rs index 30d6c0a..60325ae 100644 --- a/crates/taurus-core/src/runtime/engine/executor.rs +++ b/crates/taurus-core/src/runtime/engine/executor.rs @@ -138,7 +138,7 @@ impl<'a> EngineExecutor<'a> { Some(idx) => self.execute_from_index(idx, value_store), None => ExecutionResult { signal: Signal::Failure(RuntimeError::new( - "T-ENG-000001", + "T-CORE-000001", "NodeNotFound", format!("Node {} not found", node_id), )), @@ -178,7 +178,7 @@ impl<'a> EngineExecutor<'a> { Some(entry) => entry, None => { return Signal::Failure(RuntimeError::new( - "T-ENG-000002", + "T-CORE-000002", "FunctionNotFound", format!("Function {} not found", node.handler_id), )); @@ -223,7 +223,7 @@ impl<'a> EngineExecutor<'a> { Some(remote) => remote, None => { return Signal::Failure(RuntimeError::new( - "T-ENG-000003", + "T-CORE-000003", "RemoteRuntimeNotConfigured", "Remote runtime not configured", )); @@ -376,7 +376,7 @@ impl<'a> EngineExecutor<'a> { }, ); return Err(RuntimeError::new( - "T-ENG-000004", + "T-CORE-000004", "ReferenceValueNotFound", "Reference not found in execution value store", )); @@ -485,7 +485,7 @@ impl<'a> EngineExecutor<'a> { ) -> Result { if node.parameters.len() != values.len() { return Err(RuntimeError::new( - "T-ENG-000005", + "T-CORE-000005", "RemoteParameterMismatch", "Remote parameter count mismatch", )); diff --git a/crates/taurus-core/src/runtime/functions/array.rs b/crates/taurus-core/src/runtime/functions/array.rs index 03f2797..7686c5e 100644 --- a/crates/taurus-core/src/runtime/functions/array.rs +++ b/crates/taurus-core/src/runtime/functions/array.rs @@ -51,7 +51,7 @@ fn as_list(value: &Value, err: &'static str) -> Result match value.kind.clone().unwrap_or(Kind::NullValue(0)) { Kind::ListValue(lv) => Ok(lv), _ => Err(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", err, )), @@ -62,7 +62,7 @@ fn as_bool(value: &Value) -> Result { match value.kind.clone().unwrap_or(Kind::NullValue(0)) { Kind::BoolValue(b) => Ok(b), _ => Err(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected boolean result from predicate", )), @@ -70,7 +70,7 @@ fn as_bool(value: &Value) -> Result { } fn fail(category: &str, message: impl Into) -> Signal { - Signal::Failure(RuntimeError::new("T-LST-000000", category, message)) + Signal::Failure(RuntimeError::new("T-STD-00001", category, message)) } fn parse_array_and_thunk<'a>( @@ -185,7 +185,7 @@ fn at( if index < 0.0 { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "IndexOutOfBoundsRuntimeError", "Negative index", )); @@ -300,7 +300,7 @@ fn find( } Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "NotFoundError", "No item found that satisfies the predicate", )) @@ -335,7 +335,7 @@ fn find_last( } Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "NotFoundError", "No item found that satisfies the predicate", )) @@ -372,7 +372,7 @@ fn find_index( } Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "NotFoundError", "No item found that satisfies the predicate", )) @@ -387,7 +387,7 @@ fn first( match array.values.first() { Some(v) => Signal::Success(v.clone()), None => Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "ArrayEmptyRuntimeError", "This array is empty", )), @@ -403,7 +403,7 @@ fn last( match array.values.last() { Some(v) => Signal::Success(v.clone()), None => Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "ArrayEmptyRuntimeError", "This array is empty", )), @@ -510,7 +510,7 @@ fn push( args!(args => array_v: Value, item: Value); let Kind::ListValue(mut array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected first argument to be an array", )); @@ -529,7 +529,7 @@ fn pop( args!(args => array_v: Value); let Kind::ListValue(mut array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected an array as an argument", )); @@ -548,7 +548,7 @@ fn remove( args!(args => array_v: Value, item: Value); let Kind::ListValue(mut array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected first argument to be an array", )); @@ -575,7 +575,7 @@ fn is_empty( args!(args => array_v: Value); let Kind::ListValue(array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected an array as an argument", )); @@ -593,7 +593,7 @@ fn size( args!(args => array_v: Value); let Kind::ListValue(array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected an array as an argument", )); @@ -609,7 +609,7 @@ fn index_of( args!(args => array_v: Value, item: Value); let Kind::ListValue(array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected first argument to be an array", )); @@ -632,7 +632,7 @@ fn to_unique( args!(args => array_v: Value); let Kind::ListValue(array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected an array as an argument", )); @@ -788,7 +788,7 @@ fn reverse( args!(args => array_v: Value); let Kind::ListValue(mut array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected an array as an argument", )); @@ -807,7 +807,7 @@ fn flat( args!(args => array_v: Value); let Kind::ListValue(array) = array_v.kind.ok_or(()).unwrap_or(Kind::NullValue(0)) else { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected an array as an argument", )); @@ -859,7 +859,7 @@ fn min( Some(m) if all_int => Signal::Success(value_from_i64(min_i64.unwrap_or(*m as i64))), Some(m) => Signal::Success(value_from_f64(*m)), None => Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "ArrayEmptyRuntimeError", "Array is empty", )), @@ -899,7 +899,7 @@ fn max( Some(m) if all_int => Signal::Success(value_from_i64(max_i64.unwrap_or(*m as i64))), Some(m) => Signal::Success(value_from_f64(*m)), None => Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "ArrayEmptyRuntimeError", "Array is empty", )), diff --git a/crates/taurus-core/src/runtime/functions/boolean.rs b/crates/taurus-core/src/runtime/functions/boolean.rs index 4c99f10..c097f54 100644 --- a/crates/taurus-core/src/runtime/functions/boolean.rs +++ b/crates/taurus-core/src/runtime/functions/boolean.rs @@ -66,7 +66,7 @@ fn from_text( kind: Some(Kind::BoolValue(b)), }), Err(_) => Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", format!("Failed to parse boolean from string: {:?}", text), )), diff --git a/crates/taurus-core/src/runtime/functions/control.rs b/crates/taurus-core/src/runtime/functions/control.rs index f6e976c..fd24c9b 100644 --- a/crates/taurus-core/src/runtime/functions/control.rs +++ b/crates/taurus-core/src/runtime/functions/control.rs @@ -51,7 +51,7 @@ fn r#if( ] = args else { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", format!("Expected a bool value but received {:?}", args), )); @@ -82,7 +82,7 @@ fn if_else( ] = args else { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", format!("Expected a bool value but received {:?}", args), )); diff --git a/crates/taurus-core/src/runtime/functions/http.rs b/crates/taurus-core/src/runtime/functions/http.rs index 012df77..2e207e9 100644 --- a/crates/taurus-core/src/runtime/functions/http.rs +++ b/crates/taurus-core/src/runtime/functions/http.rs @@ -19,7 +19,7 @@ pub(crate) const FUNCTIONS: &[FunctionRegistration] = &[ ]; fn fail(category: &str, message: impl Into) -> Signal { - Signal::Failure(RuntimeError::new("T-HTTP-000000", category, message)) + Signal::Failure(RuntimeError::new("T-STD-00001", category, message)) } fn respond( @@ -33,7 +33,7 @@ fn respond( let Some(headers_val) = fields.get("headers") else { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", "Missing 'headers' field".to_string(), )); @@ -48,7 +48,7 @@ fn respond( let Some(payload_val) = fields.get("payload") else { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", "Missing 'payload' field".to_string(), )); @@ -63,7 +63,7 @@ fn respond( let Some(Kind::NumberValue(_status_code_str)) = &status_code_val.kind else { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected 'status_code' to be NumberValue".to_string(), )); @@ -71,7 +71,7 @@ fn respond( let Some(_payload_kind) = &payload_val.kind else { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected 'payload' to have a value".to_string(), )); diff --git a/crates/taurus-core/src/runtime/functions/number.rs b/crates/taurus-core/src/runtime/functions/number.rs index fc5f674..52a0f55 100644 --- a/crates/taurus-core/src/runtime/functions/number.rs +++ b/crates/taurus-core/src/runtime/functions/number.rs @@ -20,7 +20,7 @@ fn num_f64(n: &NumberValue) -> Result { // Centralized conversion keeps all numeric argument failures consistent. number_to_f64(n).ok_or_else(|| { Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected number", )) @@ -83,7 +83,7 @@ fn has_digits( number_value::Number::Float(_) => Signal::Success(true.to_value()), }, None => Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvlaidArgumentExeption", "Had NumberValue but no inner number value (was null)", )), @@ -99,7 +99,7 @@ fn remove_digits( match number_to_i64_lossy(&value) { Some(number) => Signal::Success(value_from_i64(number)), None => Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvlaidArgumentExeption", "Had NumberValue but no inner number value (was null)", )), @@ -190,7 +190,7 @@ fn divide( if rhs_f == 0.0 { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "DivisionByZero", "You cannot divide by zero", )); @@ -225,7 +225,7 @@ fn modulo( if rhs_f == 0.0 { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "DivisionByZero", "You cannot divide by zero", )); @@ -550,7 +550,7 @@ fn from_text( match string_value.parse::() { Ok(v) => Signal::Success(value_from_f64(v)), Err(_) => Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", format!("Failed to parse string as number: {}", string_value), )), @@ -652,7 +652,7 @@ fn random( if min_f > max_f { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidRange", "First number can't be bigger then second when creating a range for std::math::random", )); diff --git a/crates/taurus-core/src/runtime/functions/text.rs b/crates/taurus-core/src/runtime/functions/text.rs index 4ff0930..383642b 100644 --- a/crates/taurus-core/src/runtime/functions/text.rs +++ b/crates/taurus-core/src/runtime/functions/text.rs @@ -50,7 +50,7 @@ pub(crate) const FUNCTIONS: &[FunctionRegistration] = &[ fn arg_err>(msg: S) -> Signal { Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "InvalidArgumentRuntimeError", msg.into(), )) @@ -208,7 +208,7 @@ fn at( kind: Some(Kind::StringValue(c.to_string())), }), None => Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "IndexOutOfBoundsRuntimeError", format!( "Index {} is out of bounds for string of length {}", @@ -260,7 +260,7 @@ fn insert( // Byte-wise position is kept intentionally to match existing flow behavior. if pos > value.len() { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "IndexOutOfBoundsRuntimeError", format!("Position {} exceeds byte length {}", pos, value.len()), )); @@ -308,7 +308,7 @@ fn remove( let chars = value.chars().collect::>(); if from_u > chars.len() || to_u > chars.len() { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "IndexOutOfBoundsRuntimeError", format!( "Indices [{}, {}) out of bounds for length {}", @@ -575,7 +575,7 @@ fn decode( Ok(s) => s, Err(err) => { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "DecodeError", format!("Failed to decode base64 bytes to UTF-8: {:?}", err), )); @@ -583,7 +583,7 @@ fn decode( }, Err(err) => { return Signal::Failure(RuntimeError::new( - "T-RT-000000", + "T-STD-00001", "DecodeError", format!("Failed to decode base64 string: {:?}", err), )); diff --git a/crates/taurus-core/src/types/errors/error.rs b/crates/taurus-core/src/types/errors/error.rs index baf2957..bdec273 100644 --- a/crates/taurus-core/src/types/errors/error.rs +++ b/crates/taurus-core/src/types/errors/error.rs @@ -106,7 +106,7 @@ impl From for RuntimeError { match value { Error::Configuration { message, details } => { let mut err = RuntimeError::with_code( - "T-APP-000001".to_string(), + "T-CORE-000301".to_string(), "Configuration".to_string(), message, ); @@ -115,7 +115,7 @@ impl From for RuntimeError { } Error::State { message, details } => { let mut err = RuntimeError::with_code( - "T-APP-000002".to_string(), + "T-CORE-000302".to_string(), "State".to_string(), message, ); @@ -128,7 +128,7 @@ impl From for RuntimeError { details, } => { let mut err = RuntimeError::with_code( - "T-APP-000003".to_string(), + "T-CORE-000303".to_string(), "Transport".to_string(), message, ) @@ -147,7 +147,7 @@ impl From for RuntimeError { details, } => { let mut err = RuntimeError::with_code( - "T-APP-000004".to_string(), + "T-CORE-000304".to_string(), "Serialization".to_string(), message, ) @@ -162,7 +162,7 @@ impl From for RuntimeError { } Error::Internal { message, details } => { let mut err = RuntimeError::with_code( - "T-APP-999999".to_string(), + "T-CORE-000399".to_string(), "Internal".to_string(), message, ); @@ -209,7 +209,7 @@ mod tests { let runtime_error: RuntimeError = app_err.into(); - assert_eq!(runtime_error.code, "T-APP-000003"); + assert_eq!(runtime_error.code, "T-CORE-000303"); assert_eq!(runtime_error.category, "Transport"); assert_eq!(runtime_error.message, "connection lost"); assert!(runtime_error.details.contains_key("dependency")); diff --git a/crates/taurus-core/src/types/errors/runtime_error.rs b/crates/taurus-core/src/types/errors/runtime_error.rs index b4772ce..edafa7c 100644 --- a/crates/taurus-core/src/types/errors/runtime_error.rs +++ b/crates/taurus-core/src/types/errors/runtime_error.rs @@ -17,7 +17,7 @@ use tucana::shared::{NumberValue as ProtoNumberValue, Struct, Value, number_valu /// Runtime execution failure representation. #[derive(Debug, Clone, PartialEq)] pub struct RuntimeError { - /// Three-part error code (example: `T-STD-000123`). + /// Three-part error code (examples: `T-STD-00001`, `T-CORE-000001`). pub code: String, /// Logical category of the error (example: `InvalidArgument`). pub category: String, @@ -144,7 +144,7 @@ impl RuntimeError { impl Default for RuntimeError { fn default() -> Self { - Self::new("T-RT-000000", "RuntimeError", "Unknown runtime error") + Self::new("T-CORE-999999", "RuntimeError", "Unknown runtime error") } } From a929f9eb63424754cc200eccde0152cdc9796bf2 Mon Sep 17 00:00:00 2001 From: Raphael Date: Thu, 23 Apr 2026 17:39:52 +0200 Subject: [PATCH 21/28] feat: added new provider crate --- Cargo.lock | 20 ++++++++++++++++++++ Cargo.toml | 5 ++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 373a9e3..9c7d026 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1771,6 +1771,7 @@ dependencies = [ "prost", "rand 0.10.1", "taurus-core", + "taurus-provider", "tokio", "tonic", "tonic-health", @@ -1790,6 +1791,25 @@ dependencies = [ "uuid", ] +[[package]] +name = "taurus-provider" +version = "0.1.0" +dependencies = [ + "async-nats", + "base64", + "code0-flow", + "env_logger", + "futures-lite", + "log", + "prost", + "rand 0.10.1", + "taurus-core", + "tokio", + "tonic", + "tonic-health", + "tucana", +] + [[package]] name = "tempfile" version = "3.27.0" diff --git a/Cargo.toml b/Cargo.toml index dc17768..3f53c22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [ "crates/taurus-core", "crates/taurus-manual", "crates/taurus", "crates/taurus-tests"] +members = [ "crates/taurus-core", "crates/taurus-manual", "crates/taurus", "crates/taurus-tests", "crates/taurus-provider"] resolver = "3" [workspace.package] @@ -27,3 +27,6 @@ uuid = { version = "1.23.0", features = ["v4"] } [workspace.dependencies.taurus-core] path = "./crates/taurus-core" +[workspace.dependencies.taurus-provider] +path = "./crates/taurus-provider" + From b5ba1738d6b458aaefe9578679486be80405136f Mon Sep 17 00:00:00 2001 From: Raphael Date: Thu, 23 Apr 2026 17:46:06 +0200 Subject: [PATCH 22/28] ref: split taurus main file into app module --- crates/taurus/src/app/mod.rs | 218 +++++++++++++++++++++ crates/taurus/src/app/worker.rs | 161 +++++++++++++++ crates/taurus/src/main.rs | 336 +------------------------------- 3 files changed, 381 insertions(+), 334 deletions(-) create mode 100644 crates/taurus/src/app/mod.rs create mode 100644 crates/taurus/src/app/worker.rs diff --git a/crates/taurus/src/app/mod.rs b/crates/taurus/src/app/mod.rs new file mode 100644 index 0000000..333d8af --- /dev/null +++ b/crates/taurus/src/app/mod.rs @@ -0,0 +1,218 @@ +mod worker; + +use std::time::Duration; + +use code0_flow::flow_config::load_env_file; +use code0_flow::flow_config::mode::Mode::DYNAMIC; +use code0_flow::flow_service::FlowUpdateService; +use taurus_core::runtime::engine::ExecutionEngine; +use taurus_provider::providers::remote::nats_remote_runtime::NATSRemoteRuntime; +use tokio::signal; +use tokio::task::JoinHandle; +use tokio::time::sleep; +use tonic_health::pb::health_server::HealthServer; +use tucana::shared::{RuntimeFeature, Translation}; + +use crate::client::runtime_status::TaurusRuntimeStatusService; +use crate::client::runtime_usage::TaurusRuntimeUsageService; +use crate::config::Config; + +pub async fn run() { + init_logging(); + load_env_file(); + + let config = Config::new(); + let engine = ExecutionEngine::new(); + let client = connect_nats(&config).await; + + let mut health_task = spawn_health_task(&config); + let (runtime_status_service, runtime_usage_service) = + setup_dynamic_services_if_needed(&config).await; + + let nats_remote = NATSRemoteRuntime::new(client.clone()); + let mut worker_task = worker::spawn_worker(client, engine, nats_remote, runtime_usage_service); + + wait_for_shutdown(&mut worker_task, &mut health_task).await; + update_stopped_status(runtime_status_service.as_ref()).await; + + log::info!("Taurus shutdown complete"); +} + +fn init_logging() { + env_logger::Builder::from_default_env() + .filter_level(log::LevelFilter::Debug) + .init(); +} + +async fn connect_nats(config: &Config) -> async_nats::Client { + match async_nats::connect(config.nats_url.clone()).await { + Ok(client) => { + log::info!("Connected to NATS server"); + client + } + Err(err) => { + panic!("Failed to connect to NATS server: {}", err); + } + } +} + +fn spawn_health_task(config: &Config) -> Option> { + if !config.with_health_service { + return None; + } + + let health_service = code0_flow::flow_health::HealthService::new(config.nats_url.clone()); + let address = match format!("{}:{}", config.grpc_host, config.grpc_port).parse() { + Ok(address) => address, + Err(err) => { + log::error!("Failed to parse gRPC address: {:?}", err); + return None; + } + }; + + log::info!("Health server starting at {}", address); + Some(tokio::spawn(async move { + if let Err(err) = tonic::transport::Server::builder() + .add_service(HealthServer::new(health_service)) + .serve(address) + .await + { + log::error!("Health server error: {:?}", err); + } else { + log::info!("Health server stopped gracefully"); + } + })) +} + +async fn setup_dynamic_services_if_needed( + config: &Config, +) -> ( + Option, + Option, +) { + if config.mode != DYNAMIC { + return (None, None); + } + + push_definitions_until_success(config).await; + + let runtime_usage_service = Some( + TaurusRuntimeUsageService::from_url(config.aquila_url.clone(), config.aquila_token.clone()) + .await, + ); + + let runtime_status_service = Some( + TaurusRuntimeStatusService::from_url( + config.aquila_url.clone(), + config.aquila_token.clone(), + "taurus".into(), + runtime_features(), + ) + .await, + ); + + if let Some(status_service) = runtime_status_service.as_ref() { + status_service + .update_runtime_status(tucana::shared::execution_runtime_status::Status::Running) + .await; + } + + (runtime_status_service, runtime_usage_service) +} + +async fn push_definitions_until_success(config: &Config) { + let definition_service = FlowUpdateService::from_url( + config.aquila_url.clone(), + config.definitions.as_str(), + config.aquila_token.clone(), + ) + .await; + + let mut retry_count = 1; + loop { + if definition_service.send_with_status().await { + break; + } + + log::warn!( + "Updating definitions failed, trying again in 3 seconds (retry #{})", + retry_count + ); + retry_count += 1; + sleep(Duration::from_secs(3)).await; + } +} + +fn runtime_features() -> Vec { + vec![RuntimeFeature { + name: vec![Translation { + code: "en-US".to_string(), + content: "Runtime".to_string(), + }], + description: vec![Translation { + code: "en-US".to_string(), + content: "Will execute incoming flows.".to_string(), + }], + }] +} + +async fn update_stopped_status(runtime_status_service: Option<&TaurusRuntimeStatusService>) { + if let Some(status_service) = runtime_status_service { + status_service + .update_runtime_status(tucana::shared::execution_runtime_status::Status::Stopped) + .await; + } +} + +async fn wait_for_shutdown( + worker_task: &mut JoinHandle<()>, + health_task: &mut Option>, +) { + #[cfg(unix)] + let sigterm = async { + use tokio::signal::unix::{SignalKind, signal}; + + let mut term = signal(SignalKind::terminate()).expect("failed to install SIGTERM handler"); + term.recv().await; + }; + + #[cfg(not(unix))] + let sigterm = std::future::pending::<()>(); + + if let Some(health_task) = health_task.as_mut() { + tokio::select! { + _ = &mut *worker_task => { + log::warn!("NATS worker task finished, shutting down"); + health_task.abort(); + } + _ = &mut *health_task => { + log::warn!("Health server task finished, shutting down"); + worker_task.abort(); + } + _ = signal::ctrl_c() => { + log::info!("Ctrl+C/Exit signal received, shutting down"); + worker_task.abort(); + health_task.abort(); + } + _ = sigterm => { + log::info!("SIGTERM received, shutting down"); + worker_task.abort(); + health_task.abort(); + } + } + } else { + tokio::select! { + _ = &mut *worker_task => { + log::warn!("NATS worker task finished, shutting down"); + } + _ = signal::ctrl_c() => { + log::info!("Ctrl+C/Exit signal received, shutting down"); + worker_task.abort(); + } + _ = sigterm => { + log::info!("SIGTERM received, shutting down"); + worker_task.abort(); + } + } + } +} diff --git a/crates/taurus/src/app/worker.rs b/crates/taurus/src/app/worker.rs new file mode 100644 index 0000000..e2c2781 --- /dev/null +++ b/crates/taurus/src/app/worker.rs @@ -0,0 +1,161 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Instant; + +use futures_lite::StreamExt; +use prost::Message; +use taurus_core::runtime::engine::{EmitType, ExecutionEngine, RespondEmitter}; +use taurus_core::types::signal::Signal; +use taurus_provider::providers::emitter::nats_emitter::NATSRespondEmitter; +use taurus_provider::providers::remote::nats_remote_runtime::NATSRemoteRuntime; +use tokio::task::JoinHandle; +use tucana::shared::value::Kind; +use tucana::shared::{ExecutionFlow, RuntimeUsage, Value}; + +use crate::client::runtime_usage::TaurusRuntimeUsageService; + +pub fn spawn_worker( + client: async_nats::Client, + engine: ExecutionEngine, + nats_remote: NATSRemoteRuntime, + runtime_usage_service: Option, +) -> JoinHandle<()> { + tokio::spawn(async move { + let runtime_emitter = NATSRespondEmitter::new(client.clone()); + + let mut subscription = match client + .queue_subscribe(String::from("execution.*"), "taurus".into()) + .await + { + Ok(subscription) => { + log::info!("Subscribed to 'execution.*'"); + subscription + } + Err(err) => { + log::error!("Failed to subscribe to 'execution.*': {:?}", err); + return; + } + }; + + while let Some(message) = subscription.next().await { + process_message( + message, + &client, + &engine, + &nats_remote, + &runtime_emitter, + runtime_usage_service.as_ref(), + ) + .await; + } + + log::info!("NATS worker loop ended"); + }) +} + +async fn process_message( + message: async_nats::Message, + client: &async_nats::Client, + engine: &ExecutionEngine, + nats_remote: &NATSRemoteRuntime, + runtime_emitter: &NATSRespondEmitter, + runtime_usage_service: Option<&TaurusRuntimeUsageService>, +) { + let flow: ExecutionFlow = match ExecutionFlow::decode(&*message.payload) { + Ok(flow) => flow, + Err(err) => { + log::error!( + "Failed to deserialize flow: {:?}, payload: {:?}", + err, + &message.payload + ); + return; + } + }; + + let flow_id = flow.flow_id; + let reply_subject = message.reply.clone(); + let respond_count = Arc::new(AtomicUsize::new(0)); + let respond_count_for_emitter = respond_count.clone(); + let respond_emitter = |execution_id, emit_type: EmitType, value: Value| { + match emit_type { + EmitType::OngoingExec => { + respond_count_for_emitter.fetch_add(1, Ordering::Relaxed); + } + EmitType::StartingExec => log::debug!("Flow execution started"), + EmitType::FinishedExec => log::debug!("Flow execution finished"), + EmitType::FailedExec => log::debug!("Flow execution failed"), + } + runtime_emitter.emit(execution_id, emit_type, value); + }; + let (signal, runtime_usage) = execute_flow(flow, engine, nats_remote, Some(&respond_emitter)); + + let has_responded = respond_count.load(Ordering::Relaxed) > 0; + let final_value = signal_to_terminal_value(signal); + + // Stream contract: if we already emitted intermediate values, do not send an extra terminal + // payload on the same reply subject. + if let Some(value) = final_value + && !has_responded + && let Some(reply_subject) = reply_subject + { + log::info!("Returning value for flow_id {}: {:?}", flow_id, value); + if let Err(err) = client + .publish(reply_subject, value.encode_to_vec().into()) + .await + { + log::error!("Failed to send response: {:?}", err); + } + } + + if let Some(usage_service) = runtime_usage_service { + usage_service.update_runtime_usage(runtime_usage).await; + } +} + +fn execute_flow( + flow: ExecutionFlow, + engine: &ExecutionEngine, + nats_remote: &NATSRemoteRuntime, + respond_emitter: Option<&dyn RespondEmitter>, +) -> (Signal, RuntimeUsage) { + let start = Instant::now(); + let flow_id = flow.flow_id; + let (signal, _reason) = engine.execute_flow(flow, Some(nats_remote), respond_emitter, true); + let duration_millis = start.elapsed().as_millis() as i64; + + ( + signal, + RuntimeUsage { + flow_id, + duration: duration_millis, + }, + ) +} + +fn signal_to_terminal_value(signal: Signal) -> Option { + match signal { + Signal::Failure(error) => { + log::error!("Runtime error occurred: {:?}", error); + Some(error.as_value()) + } + Signal::Success(value) => { + log::debug!("Execution ended with success signal"); + Some(value) + } + Signal::Return(value) => { + log::debug!("Execution ended with return signal"); + Some(value) + } + Signal::Respond(_) => { + log::debug!("Execution ended with respond signal"); + None + } + Signal::Stop => { + log::debug!("Received stop signal as last signal"); + Some(Value { + kind: Some(Kind::NullValue(0)), + }) + } + } +} diff --git a/crates/taurus/src/main.rs b/crates/taurus/src/main.rs index 660ab38..8821f47 100644 --- a/crates/taurus/src/main.rs +++ b/crates/taurus/src/main.rs @@ -1,340 +1,8 @@ +mod app; mod client; mod config; -mod remote; - -use crate::client::runtime_status::TaurusRuntimeStatusService; -use crate::client::runtime_usage::TaurusRuntimeUsageService; -use crate::config::Config; -use crate::remote::RemoteNatsClient; -use code0_flow::flow_service::FlowUpdateService; - -use code0_flow::flow_config::load_env_file; -use code0_flow::flow_config::mode::Mode::DYNAMIC; -use futures_lite::StreamExt; -use log::error; -use prost::Message; -use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::time::{Duration, Instant}; -use taurus_core::runtime::engine::{EmitType, ExecutionEngine, RespondEmitter}; -use taurus_core::types::signal::Signal; -use tokio::signal; -use tokio::sync::mpsc; -use tokio::time::sleep; -use tonic_health::pb::health_server::HealthServer; -use tucana::shared::value::Kind; -use tucana::shared::{ExecutionFlow, RuntimeFeature, RuntimeUsage, Translation, Value}; - -fn handle_message( - flow: ExecutionFlow, - engine: &ExecutionEngine, - nats_remote: &RemoteNatsClient, - respond_emitter: Option<&dyn RespondEmitter>, -) -> (Signal, RuntimeUsage) { - let start = Instant::now(); - let flow_id = flow.flow_id; - let (signal, _reason) = engine.execute_flow(flow, Some(nats_remote), respond_emitter, true); - let duration_millis = start.elapsed().as_millis() as i64; - - ( - signal, - RuntimeUsage { - flow_id, - duration: duration_millis, - }, - ) -} #[tokio::main] async fn main() { - env_logger::Builder::from_default_env() - .filter_level(log::LevelFilter::Debug) - .init(); - - load_env_file(); - - let config = Config::new(); - let engine = ExecutionEngine::new(); - let mut runtime_status_service: Option = None; - let mut runtime_usage_service: Option = None; - - let client = match async_nats::connect(config.nats_url.clone()).await { - Ok(client) => { - log::info!("Connected to nats server"); - client - } - Err(err) => { - panic!("Failed to connect to NATS server: {}", err); - } - }; - - // Optional health service task - let health_task = if config.with_health_service { - let health_service = code0_flow::flow_health::HealthService::new(config.nats_url.clone()); - let address = match format!("{}:{}", config.grpc_host, config.grpc_port).parse() { - Ok(address) => address, - Err(err) => { - error!("Failed to parse grpc address: {:?}", err); - return; - } - }; - - log::info!("Health server starting at {}", address); - - Some(tokio::spawn(async move { - if let Err(err) = tonic::transport::Server::builder() - .add_service(HealthServer::new(health_service)) - .serve(address) - .await - { - log::error!("Health server error: {:?}", err); - } else { - log::info!("Health server stopped gracefully."); - } - })) - } else { - None - }; - - if config.mode == DYNAMIC { - let definition_service = FlowUpdateService::from_url( - config.aquila_url.clone(), - config.definitions.clone().as_str(), - config.aquila_token.clone(), - ) - .await; - - let mut success = false; - let mut count = 1; - while !success { - success = definition_service.send_with_status().await; - if success { - break; - } - - log::warn!( - "Updating definitions failed, trying again in 2 secs (retry number {})", - count - ); - count += 1; - sleep(Duration::from_secs(3)).await; - } - - let usage_service = TaurusRuntimeUsageService::from_url( - config.aquila_url.clone(), - config.aquila_token.clone(), - ) - .await; - runtime_usage_service = Some(usage_service); - - let status_service = TaurusRuntimeStatusService::from_url( - config.aquila_url.clone(), - config.aquila_token.clone(), - "taurus".into(), - vec![RuntimeFeature { - name: vec![Translation { - code: "en-US".to_string(), - content: "Runtime".to_string(), - }], - description: vec![Translation { - code: "en-US".to_string(), - content: "Will execute incoming flows.".to_string(), - }], - }], - ) - .await; - - status_service - .update_runtime_status(tucana::shared::execution_runtime_status::Status::Running) - .await; - runtime_status_service = Some(status_service); - } - - let nats_client = RemoteNatsClient::new(client.clone()); - let mut worker_task = tokio::spawn(async move { - let mut sub = match client - .queue_subscribe(String::from("execution.*"), "taurus".into()) - .await - { - Ok(sub) => { - log::info!("Subscribed to 'execution.*'"); - sub - } - Err(err) => { - log::error!("Failed to subscribe to 'execution.*': {:?}", err); - return; - } - }; - - while let Some(msg) = sub.next().await { - let flow: ExecutionFlow = match ExecutionFlow::decode(&*msg.payload) { - Ok(flow) => flow, - Err(err) => { - log::error!( - "Failed to deserialize flow: {:?}, payload: {:?}", - err, - &msg.payload - ); - continue; - } - }; - - let flow_id = flow.flow_id; - let reply = msg.reply.clone(); - let (respond_tx, mut respond_rx) = mpsc::unbounded_channel::(); - let respond_publisher = reply.clone().map(|reply_subject| { - let publish_client = client.clone(); - tokio::spawn(async move { - while let Some(value) = respond_rx.recv().await { - match publish_client - .publish(reply_subject.clone(), value.encode_to_vec().into()) - .await - { - Ok(_) => log::debug!("Respond signal value sent"), - Err(err) => { - log::error!("Failed to send respond signal value: {:?}", err) - } - } - } - }) - }); - - let emit_tx = respond_tx.clone(); - let respond_count = Arc::new(AtomicUsize::new(0)); - let respond_count_for_emitter = respond_count.clone(); - let respond_emitter = move |emit_type: EmitType, value: Value| match emit_type { - EmitType::OngoingExec => { - respond_count_for_emitter.fetch_add(1, Ordering::Relaxed); - if let Err(err) = emit_tx.send(value) { - log::debug!( - "Dropped respond signal value because publisher is unavailable: {:?}", - err - ); - } - } - EmitType::StartingExec => log::debug!("Flow execution started"), - EmitType::FinishedExec => log::debug!("Flow execution finished"), - EmitType::FailedExec => log::debug!("Flow execution failed"), - }; - - let (signal, runtime_usage) = - handle_message(flow, &engine, &nats_client, Some(&respond_emitter)); - drop(respond_emitter); - drop(respond_tx); - - if let Some(publisher) = respond_publisher - && let Err(err) = publisher.await - { - log::error!("Respond publisher task failed: {:?}", err); - } - let has_responded = respond_count.load(Ordering::Relaxed) > 0; - - let final_value = match signal { - Signal::Failure(error) => { - log::error!( - "RuntimeError occurred, execution failed because: {:?}", - error - ); - Some(error.as_value()) - } - Signal::Success(v) => { - log::debug!("Execution ended on a success signal"); - Some(v) - } - Signal::Return(v) => { - log::debug!("Execution ended on a return signal"); - Some(v) - } - Signal::Respond(_) => { - log::debug!("Execution ended on a respond signal"); - None - } - Signal::Stop => { - log::debug!("Revied stop signal as last signal"); - Some(Value { - kind: Some(Kind::NullValue(0)), - }) - } - }; - - if let Some(value) = final_value - && !has_responded - { - log::info!("For the flow_id {} returing the value {:?}", flow_id, value); - - // Send a terminal response to the reply subject. - if let Some(reply) = reply { - match client.publish(reply, value.encode_to_vec().into()).await { - Ok(_) => log::debug!("Response sent"), - Err(err) => log::error!("Failed to send response: {:?}", err), - } - } - } - - if let Some(usage_service) = &runtime_usage_service { - usage_service.update_runtime_usage(runtime_usage).await; - } - } - - log::info!("NATS worker loop ended"); - }); - - #[cfg(unix)] - let sigterm = async { - use tokio::signal::unix::{SignalKind, signal}; - - let mut term = signal(SignalKind::terminate()).expect("failed to install SIGTERM handler"); - term.recv().await; - }; - - #[cfg(not(unix))] - let sigterm = std::future::pending::<()>(); - - match health_task { - Some(mut health_task) => { - tokio::select! { - _ = &mut worker_task => { - log::warn!("NATS worker task finished, shutting down"); - health_task.abort(); - } - _ = &mut health_task => { - log::warn!("Health server task finished, shutting down"); - worker_task.abort(); - } - _ = signal::ctrl_c() => { - log::info!("Ctrl+C/Exit signal received, shutting down"); - worker_task.abort(); - health_task.abort(); - } - _ = sigterm => { - log::info!("SIGTERM received, shutting down"); - worker_task.abort(); - health_task.abort(); - } - } - } - None => { - tokio::select! { - _ = &mut worker_task => { - log::warn!("NATS worker task finished, shutting down"); - } - _ = signal::ctrl_c() => { - log::info!("Ctrl+C/Exit signal received, shutting down"); - worker_task.abort(); - } - _ = sigterm => { - log::info!("SIGTERM received, shutting down"); - worker_task.abort(); - } - } - } - } - - if let Some(status_service) = &runtime_status_service { - status_service - .update_runtime_status(tucana::shared::execution_runtime_status::Status::Stopped) - .await; - }; - - log::info!("Taurus shutdown complete"); + app::run().await; } From 54bc8d9a2157099cd816b8d80a7c7070df9996e1 Mon Sep 17 00:00:00 2001 From: Raphael Date: Thu, 23 Apr 2026 17:46:17 +0200 Subject: [PATCH 23/28] drop: removed remote nats runtime and moved into provider --- crates/taurus/src/remote/mod.rs | 80 --------------------------------- 1 file changed, 80 deletions(-) delete mode 100644 crates/taurus/src/remote/mod.rs diff --git a/crates/taurus/src/remote/mod.rs b/crates/taurus/src/remote/mod.rs deleted file mode 100644 index cc9f7af..0000000 --- a/crates/taurus/src/remote/mod.rs +++ /dev/null @@ -1,80 +0,0 @@ -use async_nats::Client; -use prost::Message; -use taurus_core::runtime::remote::{RemoteExecution, RemoteRuntime}; -use taurus_core::types::errors::runtime_error::RuntimeError; -use tonic::async_trait; -use tucana::aquila::ExecutionResult; -use tucana::shared::Value; - -pub struct RemoteNatsClient { - client: Client, -} - -impl RemoteNatsClient { - pub fn new(client: Client) -> Self { - RemoteNatsClient { client } - } -} - -#[async_trait] -impl RemoteRuntime for RemoteNatsClient { - async fn execute_remote(&self, execution: RemoteExecution) -> Result { - let topic = format!( - "action.{}.{}", - execution.target_service, execution.request.execution_identifier - ); - let payload = execution.request.encode_to_vec(); - log::info!("Publishing to topic: {}", topic); - let res = self.client.request(topic, payload.into()).await; - let message = match res { - Ok(r) => r, - Err(err) => { - log::error!( - "RemoteRuntimeExeption: failed to handle NATS message: {}", - err - ); - return Err(RuntimeError::new( - "T-RMT-000001", - "RemoteRuntimeExeption", - "Failed to receive any response messages from a remote runtime.", - )); - } - }; - - let decode_result = ExecutionResult::decode(message.payload); - let execution_result = match decode_result { - Ok(r) => r, - Err(err) => { - log::error!( - "RemoteRuntimeExeption: failed to decode NATS message: {}", - err - ); - return Err(RuntimeError::new( - "T-RMT-000002", - "RemoteRuntimeExeption", - "Failed to read Remote Response", - )); - } - }; - - match execution_result.result { - Some(result) => match result { - tucana::aquila::execution_result::Result::Success(value) => Ok(value), - tucana::aquila::execution_result::Result::Error(err) => { - let code = err.code.to_string(); - let description = match err.description { - Some(string) => string, - None => "Unknown Error".to_string(), - }; - let error = RuntimeError::new(code, "RemoteExecutionError", description); - Err(error) - } - }, - None => Err(RuntimeError::new( - "T-RMT-000003", - "RemoteRuntimeExeption", - "Result of Remote Response was empty.", - )), - } - } -} From 32443b9a288a5cc3da34c2094e6a29aa18e7b8c5 Mon Sep 17 00:00:00 2001 From: Raphael Date: Thu, 23 Apr 2026 17:46:31 +0200 Subject: [PATCH 24/28] feat: adjusted emitter api --- crates/taurus-core/src/runtime/engine.rs | 23 +++++++++++-------- .../taurus-core/src/runtime/engine/emitter.rs | 12 ++++++---- .../src/runtime/engine/executor.rs | 7 ++++-- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/crates/taurus-core/src/runtime/engine.rs b/crates/taurus-core/src/runtime/engine.rs index 1e0dbd7..385df5c 100644 --- a/crates/taurus-core/src/runtime/engine.rs +++ b/crates/taurus-core/src/runtime/engine.rs @@ -16,7 +16,7 @@ use crate::runtime::remote::RemoteRuntime; use crate::types::exit_reason::ExitReason; use crate::types::signal::Signal; use compiler::compile_flow; -pub use emitter::{EmitType, RespondEmitter}; +pub use emitter::{EmitType, ExecutionId, RespondEmitter}; fn null_value() -> Value { Value { @@ -71,8 +71,10 @@ impl ExecutionEngine { respond_emitter: Option<&dyn RespondEmitter>, with_trace: bool, ) -> (Signal, ExitReason) { + let execution_id = ExecutionId::new_v4(); + if let Some(emitter) = respond_emitter { - emitter.emit(EmitType::StartingExec, null_value()); + emitter.emit(execution_id, EmitType::StartingExec, null_value()); } let mut value_store = match flow_input { @@ -85,7 +87,7 @@ impl ExecutionEngine { Err(err) => { let runtime_error = err.as_runtime_error(); if let Some(emitter) = respond_emitter { - emitter.emit(EmitType::FailedExec, runtime_error.as_value()); + emitter.emit(execution_id, EmitType::FailedExec, runtime_error.as_value()); } let signal = Signal::Failure(runtime_error); return (signal, ExitReason::Failure); @@ -97,6 +99,7 @@ impl ExecutionEngine { &self.handlers, &mut value_store, remote, + execution_id, respond_emitter, with_trace, ); @@ -108,11 +111,13 @@ impl ExecutionEngine { } if let Some(emitter) = respond_emitter { match &signal { - Signal::Failure(err) => emitter.emit(EmitType::FailedExec, err.as_value()), + Signal::Failure(err) => { + emitter.emit(execution_id, EmitType::FailedExec, err.as_value()) + } Signal::Success(value) | Signal::Return(value) | Signal::Respond(value) => { - emitter.emit(EmitType::FinishedExec, value.clone()) + emitter.emit(execution_id, EmitType::FinishedExec, value.clone()) } - Signal::Stop => emitter.emit(EmitType::FinishedExec, null_value()), + Signal::Stop => emitter.emit(execution_id, EmitType::FinishedExec, null_value()), } } let exit_reason = signal.exit_reason(); @@ -386,7 +391,7 @@ mod tests { fn emitter_emits_start_and_finish_for_successful_execution() { let engine = ExecutionEngine::new(); let events = RefCell::new(Vec::::new()); - let emitter = |emit_type: EmitType, _value: Value| { + let emitter = |_execution_id, emit_type: EmitType, _value: Value| { events.borrow_mut().push(emit_type); }; @@ -413,7 +418,7 @@ mod tests { fn emitter_emits_ongoing_for_intermediate_respond() { let engine = ExecutionEngine::new(); let events = RefCell::new(Vec::::new()); - let emitter = |emit_type: EmitType, _value: Value| { + let emitter = |_execution_id, emit_type: EmitType, _value: Value| { events.borrow_mut().push(emit_type); }; @@ -466,7 +471,7 @@ mod tests { fn emitter_emits_failed_for_runtime_failure() { let engine = ExecutionEngine::new(); let events = RefCell::new(Vec::::new()); - let emitter = |emit_type: EmitType, _value: Value| { + let emitter = |_execution_id, emit_type: EmitType, _value: Value| { events.borrow_mut().push(emit_type); }; diff --git a/crates/taurus-core/src/runtime/engine/emitter.rs b/crates/taurus-core/src/runtime/engine/emitter.rs index b2dee74..992db26 100644 --- a/crates/taurus-core/src/runtime/engine/emitter.rs +++ b/crates/taurus-core/src/runtime/engine/emitter.rs @@ -1,6 +1,10 @@ //! Respond emitter abstraction used by the engine. use tucana::shared::Value; +use uuid::Uuid; + +/// Unique identifier for one top-level flow execution. +pub type ExecutionId = Uuid; /// Execution lifecycle event emitted by the runtime engine. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -17,14 +21,14 @@ pub enum EmitType { /// Callback interface for streaming execution lifecycle events. pub trait RespondEmitter { - fn emit(&self, emit_type: EmitType, value: Value); + fn emit(&self, execution_id: ExecutionId, emit_type: EmitType, value: Value); } impl RespondEmitter for F where - F: Fn(EmitType, Value) + ?Sized, + F: Fn(ExecutionId, EmitType, Value) + ?Sized, { - fn emit(&self, emit_type: EmitType, value: Value) { - self(emit_type, value); + fn emit(&self, execution_id: ExecutionId, emit_type: EmitType, value: Value) { + self(execution_id, emit_type, value); } } diff --git a/crates/taurus-core/src/runtime/engine/executor.rs b/crates/taurus-core/src/runtime/engine/executor.rs index 60325ae..c8aeb27 100644 --- a/crates/taurus-core/src/runtime/engine/executor.rs +++ b/crates/taurus-core/src/runtime/engine/executor.rs @@ -12,7 +12,7 @@ use uuid::Uuid; use crate::handler::argument::{Argument, ParameterNode}; use crate::handler::registry::{FunctionStore, HandlerFunctionEntry}; -use crate::runtime::engine::emitter::{EmitType, RespondEmitter}; +use crate::runtime::engine::emitter::{EmitType, ExecutionId, RespondEmitter}; use crate::runtime::engine::model::{CompiledArg, CompiledFlow, CompiledNode, NodeExecutionTarget}; use crate::runtime::execution::trace::{ ArgKind, ArgTrace, EdgeKind, Outcome, ReferenceKind, TraceRun, @@ -28,6 +28,7 @@ pub fn execute_compiled( handlers: &FunctionStore, value_store: &mut ValueStore, remote: Option<&dyn RemoteRuntime>, + execution_id: ExecutionId, respond_emitter: Option<&dyn RespondEmitter>, with_trace: bool, ) -> (Signal, Option) { @@ -37,6 +38,7 @@ pub fn execute_compiled( flow, handlers, remote, + execution_id, respond_emitter, tracer: tracer.as_ref(), }; @@ -63,6 +65,7 @@ struct EngineExecutor<'a> { flow: &'a CompiledFlow, handlers: &'a FunctionStore, remote: Option<&'a dyn RemoteRuntime>, + execution_id: ExecutionId, respond_emitter: Option<&'a dyn RespondEmitter>, tracer: Option<&'a RefCell>, } @@ -107,7 +110,7 @@ impl<'a> EngineExecutor<'a> { Signal::Respond(value) => { // `Respond` is an observable side effect; execution may still continue. if let Some(emitter) = self.respond_emitter { - emitter.emit(EmitType::OngoingExec, value.clone()); + emitter.emit(self.execution_id, EmitType::OngoingExec, value.clone()); } value_store.insert_success(node_id, value.clone()); From ec6d7e997b4edfc4dbf847a73c7cde3665daac66 Mon Sep 17 00:00:00 2001 From: Raphael Date: Thu, 23 Apr 2026 17:46:48 +0200 Subject: [PATCH 25/28] feat: added new provider crate for emitters and remote runtime --- crates/taurus-provider/Cargo.toml | 19 ++++ crates/taurus-provider/src/lib.rs | 1 + .../src/providers/emitter/mod.rs | 1 + .../src/providers/emitter/nats_emitter.rs | 93 +++++++++++++++++++ crates/taurus-provider/src/providers/mod.rs | 5 + .../src/providers/remote/mod.rs | 1 + .../providers/remote/nats_remote_runtime.rs | 81 ++++++++++++++++ crates/taurus/Cargo.toml | 1 + 8 files changed, 202 insertions(+) create mode 100644 crates/taurus-provider/Cargo.toml create mode 100644 crates/taurus-provider/src/lib.rs create mode 100644 crates/taurus-provider/src/providers/emitter/mod.rs create mode 100644 crates/taurus-provider/src/providers/emitter/nats_emitter.rs create mode 100644 crates/taurus-provider/src/providers/mod.rs create mode 100644 crates/taurus-provider/src/providers/remote/mod.rs create mode 100644 crates/taurus-provider/src/providers/remote/nats_remote_runtime.rs diff --git a/crates/taurus-provider/Cargo.toml b/crates/taurus-provider/Cargo.toml new file mode 100644 index 0000000..395ea7c --- /dev/null +++ b/crates/taurus-provider/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "taurus-provider" +version.workspace = true +edition.workspace = true + +[dependencies] +code0-flow = { workspace = true, features = ["flow_service"] } +tucana = { workspace = true } +tokio = { workspace = true } +log = { workspace = true } +futures-lite = { workspace = true } +rand = { workspace = true } +base64 = { workspace = true } +env_logger = { workspace = true } +async-nats = { workspace = true } +prost = { workspace = true } +tonic-health = { workspace = true } +tonic = { workspace = true } +taurus-core = { workspace = true } diff --git a/crates/taurus-provider/src/lib.rs b/crates/taurus-provider/src/lib.rs new file mode 100644 index 0000000..9ce92fb --- /dev/null +++ b/crates/taurus-provider/src/lib.rs @@ -0,0 +1 @@ +pub mod providers; diff --git a/crates/taurus-provider/src/providers/emitter/mod.rs b/crates/taurus-provider/src/providers/emitter/mod.rs new file mode 100644 index 0000000..f3bb57e --- /dev/null +++ b/crates/taurus-provider/src/providers/emitter/mod.rs @@ -0,0 +1 @@ +pub mod nats_emitter; diff --git a/crates/taurus-provider/src/providers/emitter/nats_emitter.rs b/crates/taurus-provider/src/providers/emitter/nats_emitter.rs new file mode 100644 index 0000000..2ed83ec --- /dev/null +++ b/crates/taurus-provider/src/providers/emitter/nats_emitter.rs @@ -0,0 +1,93 @@ +use async_nats::Client; +use prost::Message; +use std::collections::HashMap; +use taurus_core::runtime::engine::{EmitType, ExecutionId, RespondEmitter}; +use tokio::sync::mpsc; +use tucana::shared::value::Kind::{StringValue, StructValue}; +use tucana::shared::{Struct, Value}; + +const DEFAULT_TOPIC_PREFIX: &str = "runtime.emitter"; + +pub struct NATSRespondEmitter { + tx: mpsc::UnboundedSender, +} + +struct NATSEmitMessage { + execution_id: ExecutionId, + emit_type: EmitType, + value: Value, +} + +impl NATSRespondEmitter { + pub fn new(client: Client) -> Self { + Self::with_topic_prefix(client, DEFAULT_TOPIC_PREFIX) + } + + pub fn with_topic_prefix(client: Client, topic_prefix: impl Into) -> Self { + let topic_prefix = topic_prefix.into(); + let (tx, mut rx) = mpsc::unbounded_channel::(); + + // Keep the public emitter API synchronous while publishing asynchronously. + // This worker serializes outbound lifecycle events to one NATS topic per execution: + // `.`. + // Event type is embedded in the payload so subscribers do not need four topic bindings. + tokio::spawn(async move { + while let Some(message) = rx.recv().await { + let topic = format!("{}.{}", topic_prefix, message.execution_id); + let encoded_payload = encode_emit_message(message.emit_type, message.value); + + if let Err(err) = client + .publish(topic.clone(), encoded_payload.encode_to_vec().into()) + .await + { + log::error!( + "Failed to publish runtime emit message on '{}': {:?}", + topic, + err + ); + } + } + }); + + Self { tx } + } +} + +impl RespondEmitter for NATSRespondEmitter { + fn emit(&self, execution_id: ExecutionId, emit_type: EmitType, value: Value) { + if let Err(err) = self.tx.send(NATSEmitMessage { + execution_id, + emit_type, + value, + }) { + log::debug!( + "Dropped runtime emit message because NATS emitter worker is unavailable: {:?}", + err + ); + } + } +} + +fn encode_emit_message(emit_type: EmitType, payload: Value) -> Value { + let emit_type_value = Value { + kind: Some(StringValue(emit_type_as_str(emit_type).to_string())), + }; + + Value { + kind: Some(StructValue(Struct { + fields: HashMap::from([ + ("emit_type".to_string(), emit_type_value), + ("payload".to_string(), payload), + ]), + })), + } +} + +fn emit_type_as_str(emit_type: EmitType) -> &'static str { + match emit_type { + EmitType::StartingExec => "starting", + EmitType::OngoingExec => "ongoing", + EmitType::FinishedExec => "finished", + EmitType::FailedExec => "failed", + } +} diff --git a/crates/taurus-provider/src/providers/mod.rs b/crates/taurus-provider/src/providers/mod.rs new file mode 100644 index 0000000..8501ddc --- /dev/null +++ b/crates/taurus-provider/src/providers/mod.rs @@ -0,0 +1,5 @@ +/// Provider for Remote Runtimes +pub mod remote; + +/// Provider for Remote Emitters +pub mod emitter; diff --git a/crates/taurus-provider/src/providers/remote/mod.rs b/crates/taurus-provider/src/providers/remote/mod.rs new file mode 100644 index 0000000..e862955 --- /dev/null +++ b/crates/taurus-provider/src/providers/remote/mod.rs @@ -0,0 +1 @@ +pub mod nats_remote_runtime; diff --git a/crates/taurus-provider/src/providers/remote/nats_remote_runtime.rs b/crates/taurus-provider/src/providers/remote/nats_remote_runtime.rs new file mode 100644 index 0000000..9ef694b --- /dev/null +++ b/crates/taurus-provider/src/providers/remote/nats_remote_runtime.rs @@ -0,0 +1,81 @@ +use async_nats::Client; +use prost::Message; +use taurus_core::runtime::remote::{RemoteExecution, RemoteRuntime}; +use taurus_core::types::errors::runtime_error::RuntimeError; +use tonic::async_trait; +use tucana::aquila::ExecutionResult; +use tucana::shared::Value; + +pub struct NATSRemoteRuntime { + client: Client, +} + +impl NATSRemoteRuntime { + pub fn new(client: Client) -> Self { + NATSRemoteRuntime { client } + } +} + +#[async_trait] +impl RemoteRuntime for NATSRemoteRuntime { + async fn execute_remote(&self, execution: RemoteExecution) -> Result { + let topic = format!( + "action.{}.{}", + execution.target_service, execution.request.execution_identifier + ); + let payload = execution.request.encode_to_vec(); + + log::info!("Request Remote Runtime Execution with topic: : {}", topic); + let res = self.client.request(topic, payload.into()).await; + let message = match res { + Ok(r) => r, + Err(err) => { + log::error!( + "RemoteRuntimeExeption: failed to handle NATS message: {}", + err + ); + return Err(RuntimeError::new( + "T-PROV-000001", + "RemoteRuntimeExeption", + "Failed to receive any response messages from a remote runtime.", + )); + } + }; + + let decode_result = ExecutionResult::decode(message.payload); + let execution_result = match decode_result { + Ok(r) => r, + Err(err) => { + log::error!( + "RemoteRuntimeExeption: failed to decode NATS message: {}", + err + ); + return Err(RuntimeError::new( + "T-PROV-000002", + "RemoteRuntimeExeption", + "Failed to read Remote Response", + )); + } + }; + + match execution_result.result { + Some(result) => match result { + tucana::aquila::execution_result::Result::Success(value) => Ok(value), + tucana::aquila::execution_result::Result::Error(err) => { + let code = err.code.to_string(); + let description = match err.description { + Some(string) => string, + None => "Unknown Error".to_string(), + }; + let error = RuntimeError::new(code, "RemoteExecutionError", description); + Err(error) + } + }, + None => Err(RuntimeError::new( + "T-PROV-000003", + "RemoteRuntimeExeption", + "Result of Remote Response was empty.", + )), + } + } +} diff --git a/crates/taurus/Cargo.toml b/crates/taurus/Cargo.toml index 02b838a..b6cb26a 100644 --- a/crates/taurus/Cargo.toml +++ b/crates/taurus/Cargo.toml @@ -17,3 +17,4 @@ prost = { workspace = true } tonic-health = { workspace = true } tonic = { workspace = true } taurus-core = { workspace = true } +taurus-provider = { workspace = true } From 5c417def930b6d4e03e40460927c168c93ff91d1 Mon Sep 17 00:00:00 2001 From: Raphael Date: Thu, 23 Apr 2026 19:00:15 +0200 Subject: [PATCH 26/28] fix: minor fixes --- crates/taurus-core/src/ERROR_CODES.md | 10 +- crates/taurus-core/src/runtime/engine.rs | 43 ++++++- .../taurus-core/src/runtime/engine/emitter.rs | 15 +++ crates/taurus-manual/Cargo.toml | 2 +- crates/taurus-manual/src/main.rs | 104 +++------------- .../src/providers/emitter/nats_emitter.rs | 23 +++- crates/taurus/src/app/mod.rs | 10 +- crates/taurus/src/app/worker.rs | 117 +++++++----------- 8 files changed, 157 insertions(+), 167 deletions(-) diff --git a/crates/taurus-core/src/ERROR_CODES.md b/crates/taurus-core/src/ERROR_CODES.md index 3274c7e..9d40270 100644 --- a/crates/taurus-core/src/ERROR_CODES.md +++ b/crates/taurus-core/src/ERROR_CODES.md @@ -1,11 +1,12 @@ # Taurus Runtime Error Codes -This document is the canonical catalog for runtime error codes emitted by `taurus-core`. +This document is the canonical catalog for runtime error codes emitted by Taurus runtime crates (`taurus-core` and `taurus-provider`). ## Code Format - `T-STD-XXXXX`: Errors originating inside standard function implementations under `runtime/functions/*`. - `T-CORE-XXXXXX`: Errors originating from core runtime infrastructure (`engine`, `handler`, type conversion, app-layer mapping). +- `T-PROV-XXXXXX`: Errors originating from provider integrations (transport adapters, remote runtime connectors). ## Code Table @@ -29,3 +30,10 @@ This document is the canonical catalog for runtime error codes emitted by `tauru | `T-CORE-000304` | App Error Mapping | Serialization/deserialization failure mapped into runtime error format. | Encoding/decoding/parsing failure surfaced as `Error::Serialization`. | `types/errors/error.rs` | | `T-CORE-000399` | App Error Mapping | Internal application failure mapped into runtime error format. | Catch-all non-domain internal failure surfaced as `Error::Internal`. | `types/errors/error.rs` | | `T-CORE-999999` | Runtime Error Fallback | Default fallback runtime error code when no explicit mapping is provided. | `RuntimeError::default()` used as defensive fallback. | `types/errors/runtime_error.rs` | +| `T-PROV-000001` | Provider Remote Runtime | Remote request to NATS did not yield a valid response message. | NATS request failed or timed out while waiting for remote runtime answer. | `taurus-provider/providers/remote/nats_remote_runtime.rs` | +| `T-PROV-000002` | Provider Remote Runtime | Remote runtime response could not be decoded into expected protobuf structure. | Received payload is malformed, truncated, or schema-incompatible for `ExecutionResult`. | `taurus-provider/providers/remote/nats_remote_runtime.rs` | +| `T-PROV-000003` | Provider Remote Runtime | Remote runtime response decoded, but contained no concrete result field. | `ExecutionResult` exists but `result` is `None` (protocol contract violation). | `taurus-provider/providers/remote/nats_remote_runtime.rs` | + +## Provider Note + +`taurus-provider` can also forward remote service errors with service-owned codes (for example codes returned inside Aquila `ExecutionResult::Error`). Those are intentionally preserved instead of remapped, so they are not enumerated as static Taurus provider codes here. diff --git a/crates/taurus-core/src/runtime/engine.rs b/crates/taurus-core/src/runtime/engine.rs index 385df5c..c7e4599 100644 --- a/crates/taurus-core/src/runtime/engine.rs +++ b/crates/taurus-core/src/runtime/engine.rs @@ -51,7 +51,26 @@ impl ExecutionEngine { respond_emitter: Option<&dyn RespondEmitter>, with_trace: bool, ) -> (Signal, ExitReason) { - self.execute_graph( + self.execute_flow_with_execution_id( + ExecutionId::new_v4(), + flow, + remote, + respond_emitter, + with_trace, + ) + } + + /// Execute an `ExecutionFlow` with a caller-provided execution id. + pub fn execute_flow_with_execution_id( + &self, + execution_id: ExecutionId, + flow: ExecutionFlow, + remote: Option<&dyn RemoteRuntime>, + respond_emitter: Option<&dyn RespondEmitter>, + with_trace: bool, + ) -> (Signal, ExitReason) { + self.execute_graph_with_execution_id( + execution_id, flow.starting_node_id, flow.node_functions, flow.input_value, @@ -71,8 +90,28 @@ impl ExecutionEngine { respond_emitter: Option<&dyn RespondEmitter>, with_trace: bool, ) -> (Signal, ExitReason) { - let execution_id = ExecutionId::new_v4(); + self.execute_graph_with_execution_id( + ExecutionId::new_v4(), + start_node_id, + node_functions, + flow_input, + remote, + respond_emitter, + with_trace, + ) + } + /// Execute a graph described by node list and start node with a caller-provided execution id. + pub fn execute_graph_with_execution_id( + &self, + execution_id: ExecutionId, + start_node_id: i64, + node_functions: Vec, + flow_input: Option, + remote: Option<&dyn RemoteRuntime>, + respond_emitter: Option<&dyn RespondEmitter>, + with_trace: bool, + ) -> (Signal, ExitReason) { if let Some(emitter) = respond_emitter { emitter.emit(execution_id, EmitType::StartingExec, null_value()); } diff --git a/crates/taurus-core/src/runtime/engine/emitter.rs b/crates/taurus-core/src/runtime/engine/emitter.rs index 992db26..e88600f 100644 --- a/crates/taurus-core/src/runtime/engine/emitter.rs +++ b/crates/taurus-core/src/runtime/engine/emitter.rs @@ -1,5 +1,7 @@ //! Respond emitter abstraction used by the engine. +use std::fmt::{Display, Formatter}; + use tucana::shared::Value; use uuid::Uuid; @@ -32,3 +34,16 @@ where self(execution_id, emit_type, value); } } + +impl Display for EmitType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let label = match self { + EmitType::StartingExec => "started_execution", + EmitType::OngoingExec => "ongoing_execution", + EmitType::FinishedExec => "finished_execution", + EmitType::FailedExec => "failed_execution", + }; + + write!(f, "{label}") + } +} diff --git a/crates/taurus-manual/Cargo.toml b/crates/taurus-manual/Cargo.toml index d4655aa..799ab33 100644 --- a/crates/taurus-manual/Cargo.toml +++ b/crates/taurus-manual/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true [dependencies] tucana = { workspace = true } taurus-core = { workspace = true } +taurus-provider = { workspace = true } log = { workspace = true } env_logger = { workspace = true } serde_json = { workspace = true } @@ -15,4 +16,3 @@ tonic = { workspace = true } tokio = { workspace = true } async-nats = { workspace = true } clap = { version = "4.6.0", features= ["derive"] } -async-trait = { workspace = true } diff --git a/crates/taurus-manual/src/main.rs b/crates/taurus-manual/src/main.rs index 5a541ae..795f7e7 100644 --- a/crates/taurus-manual/src/main.rs +++ b/crates/taurus-manual/src/main.rs @@ -1,18 +1,16 @@ use std::path::Path; -use async_nats::Client; -use async_trait::async_trait; use clap::{Parser, arg, command}; -use log::{error, info}; -use prost::Message; +use log::error; +use log::info; use serde::Deserialize; use taurus_core::runtime::engine::ExecutionEngine; -use taurus_core::runtime::remote::{RemoteExecution, RemoteRuntime}; -use taurus_core::types::errors::runtime_error::RuntimeError; -use tucana::aquila::ExecutionResult; +use taurus_core::types::signal::Signal; +use taurus_provider::providers::emitter::nats_emitter::NATSRespondEmitter; +use taurus_provider::providers::remote::nats_remote_runtime::NATSRemoteRuntime; +use tucana::shared::ValidationFlow; use tucana::shared::helper::value::from_json_value; use tucana::shared::helper::value::to_json_value; -use tucana::shared::{ValidationFlow, Value}; #[derive(Clone, Deserialize)] pub struct Input { @@ -115,78 +113,6 @@ impl Cases { get_test_cases(path) } } -pub struct RemoteNatsClient { - client: Client, -} - -impl RemoteNatsClient { - pub fn new(client: Client) -> Self { - RemoteNatsClient { client } - } -} - -#[async_trait] -impl RemoteRuntime for RemoteNatsClient { - async fn execute_remote(&self, execution: RemoteExecution) -> Result { - let topic = format!( - "action.{}.{}", - execution.target_service, execution.request.execution_identifier - ); - let payload = execution.request.encode_to_vec(); - let res = self.client.request(topic.clone(), payload.into()).await; - log::info!("Publishing to topic: {}", topic); - let message = match res { - Ok(r) => r, - Err(err) => { - log::error!( - "RemoteRuntimeExeption: failed to handle NATS message: {}", - err - ); - return Err(RuntimeError::new( - "T-RMT-000001", - "RemoteRuntimeExeption", - "Failed to receive any response messages from a remote runtime.", - )); - } - }; - - let decode_result = ExecutionResult::decode(message.payload); - let execution_result = match decode_result { - Ok(r) => r, - Err(err) => { - log::error!( - "RemoteRuntimeExeption: failed to decode NATS message: {}", - err - ); - return Err(RuntimeError::new( - "T-RMT-000002", - "RemoteRuntimeExeption", - "Failed to read Remote Response", - )); - } - }; - - match execution_result.result { - Some(result) => match result { - tucana::aquila::execution_result::Result::Success(value) => Ok(value), - tucana::aquila::execution_result::Result::Error(err) => { - let code = err.code.to_string(); - let description = match err.description { - Some(string) => string, - None => "Unknown Error".to_string(), - }; - let error = RuntimeError::new(code, "RemoteExecutionError", description); - Err(error) - } - }, - None => Err(RuntimeError::new( - "T-RMT-000003", - "RemoteRuntimeExeption", - "Result of Remote Response was empty.", - )), - } - } -} #[derive(clap::Parser, Debug)] #[command(author, version, about)] @@ -233,35 +159,37 @@ async fn main() { panic!("Failed to connect to NATS server: {}", err); } }; - let remote = RemoteNatsClient::new(client); + let remote = NATSRemoteRuntime::new(client.clone()); + let emitter = NATSRespondEmitter::new(client); let engine = ExecutionEngine::new(); let (result, _) = engine.execute_graph( case.flow.starting_node_id, case.flow.node_functions.clone(), flow_input, Some(&remote), - None, - true, + Some(&emitter), + false, ); + emitter.shutdown().await; match result { - taurus_core::types::signal::Signal::Success(value) => { + Signal::Success(value) => { let json = to_json_value(value); let pretty = serde_json::to_string_pretty(&json).unwrap(); println!("{}", pretty); } - taurus_core::types::signal::Signal::Return(value) => { + Signal::Return(value) => { let json = to_json_value(value); let pretty = serde_json::to_string_pretty(&json).unwrap(); println!("{}", pretty); } - taurus_core::types::signal::Signal::Respond(value) => { + Signal::Respond(value) => { let json = to_json_value(value); let pretty = serde_json::to_string_pretty(&json).unwrap(); println!("{}", pretty); } - taurus_core::types::signal::Signal::Stop => println!("Received Stop signal"), - taurus_core::types::signal::Signal::Failure(runtime_error) => { + Signal::Stop => println!("Received Stop signal"), + Signal::Failure(runtime_error) => { println!("RuntimeError: {:?}", runtime_error); } } diff --git a/crates/taurus-provider/src/providers/emitter/nats_emitter.rs b/crates/taurus-provider/src/providers/emitter/nats_emitter.rs index 2ed83ec..7b75b6f 100644 --- a/crates/taurus-provider/src/providers/emitter/nats_emitter.rs +++ b/crates/taurus-provider/src/providers/emitter/nats_emitter.rs @@ -10,6 +10,7 @@ const DEFAULT_TOPIC_PREFIX: &str = "runtime.emitter"; pub struct NATSRespondEmitter { tx: mpsc::UnboundedSender, + worker_task: tokio::task::JoinHandle<()>, } struct NATSEmitMessage { @@ -31,8 +32,12 @@ impl NATSRespondEmitter { // This worker serializes outbound lifecycle events to one NATS topic per execution: // `.`. // Event type is embedded in the payload so subscribers do not need four topic bindings. - tokio::spawn(async move { + let worker_task = tokio::spawn(async move { while let Some(message) = rx.recv().await { + log::debug!( + "Emitter has been called for Emitter Signal: {}", + message.emit_type + ); let topic = format!("{}.{}", topic_prefix, message.execution_id); let encoded_payload = encode_emit_message(message.emit_type, message.value); @@ -45,11 +50,25 @@ impl NATSRespondEmitter { topic, err ); + } else { + log::info!( + "Published runtime emit message on '{}' (type={})", + topic, + emit_type_as_str(message.emit_type) + ); } } }); - Self { tx } + Self { tx, worker_task } + } + + /// Gracefully stop the emitter worker after all queued messages are published. + pub async fn shutdown(self) { + drop(self.tx); + if let Err(err) = self.worker_task.await { + log::error!("NATS emitter worker join failed: {:?}", err); + } } } diff --git a/crates/taurus/src/app/mod.rs b/crates/taurus/src/app/mod.rs index 333d8af..0da58ca 100644 --- a/crates/taurus/src/app/mod.rs +++ b/crates/taurus/src/app/mod.rs @@ -6,6 +6,7 @@ use code0_flow::flow_config::load_env_file; use code0_flow::flow_config::mode::Mode::DYNAMIC; use code0_flow::flow_service::FlowUpdateService; use taurus_core::runtime::engine::ExecutionEngine; +use taurus_provider::providers::emitter::nats_emitter::NATSRespondEmitter; use taurus_provider::providers::remote::nats_remote_runtime::NATSRemoteRuntime; use tokio::signal; use tokio::task::JoinHandle; @@ -30,7 +31,14 @@ pub async fn run() { setup_dynamic_services_if_needed(&config).await; let nats_remote = NATSRemoteRuntime::new(client.clone()); - let mut worker_task = worker::spawn_worker(client, engine, nats_remote, runtime_usage_service); + let runtime_emitter = NATSRespondEmitter::new(client.clone()); + let mut worker_task = worker::spawn_worker( + client, + engine, + nats_remote, + runtime_emitter, + runtime_usage_service, + ); wait_for_shutdown(&mut worker_task, &mut health_task).await; update_stopped_status(runtime_status_service.as_ref()).await; diff --git a/crates/taurus/src/app/worker.rs b/crates/taurus/src/app/worker.rs index e2c2781..7ee59b4 100644 --- a/crates/taurus/src/app/worker.rs +++ b/crates/taurus/src/app/worker.rs @@ -1,15 +1,11 @@ -use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Instant; use futures_lite::StreamExt; use prost::Message; -use taurus_core::runtime::engine::{EmitType, ExecutionEngine, RespondEmitter}; -use taurus_core::types::signal::Signal; +use taurus_core::runtime::engine::{EmitType, ExecutionEngine, ExecutionId, RespondEmitter}; use taurus_provider::providers::emitter::nats_emitter::NATSRespondEmitter; use taurus_provider::providers::remote::nats_remote_runtime::NATSRemoteRuntime; use tokio::task::JoinHandle; -use tucana::shared::value::Kind; use tucana::shared::{ExecutionFlow, RuntimeUsage, Value}; use crate::client::runtime_usage::TaurusRuntimeUsageService; @@ -18,11 +14,10 @@ pub fn spawn_worker( client: async_nats::Client, engine: ExecutionEngine, nats_remote: NATSRemoteRuntime, + runtime_emitter: NATSRespondEmitter, runtime_usage_service: Option, ) -> JoinHandle<()> { tokio::spawn(async move { - let runtime_emitter = NATSRespondEmitter::new(client.clone()); - let mut subscription = match client .queue_subscribe(String::from("execution.*"), "taurus".into()) .await @@ -40,7 +35,6 @@ pub fn spawn_worker( while let Some(message) = subscription.next().await { process_message( message, - &client, &engine, &nats_remote, &runtime_emitter, @@ -55,12 +49,22 @@ pub fn spawn_worker( async fn process_message( message: async_nats::Message, - client: &async_nats::Client, engine: &ExecutionEngine, nats_remote: &NATSRemoteRuntime, runtime_emitter: &NATSRespondEmitter, runtime_usage_service: Option<&TaurusRuntimeUsageService>, ) { + let requested_execution_id = + parse_execution_id_from_subject(&message.subject).unwrap_or_else(|| { + let generated = ExecutionId::new_v4(); + log::warn!( + "Expected subject format 'execution.', got '{}'; generated execution id {}", + message.subject, + generated + ); + generated + }); + let flow: ExecutionFlow = match ExecutionFlow::decode(&*message.payload) { Ok(flow) => flow, Err(err) => { @@ -74,39 +78,22 @@ async fn process_message( }; let flow_id = flow.flow_id; - let reply_subject = message.reply.clone(); - let respond_count = Arc::new(AtomicUsize::new(0)); - let respond_count_for_emitter = respond_count.clone(); + // Taurus app forwards all lifecycle events to emitter. + // Direct request/reply responses remain disabled; delivery is emitter-only. let respond_emitter = |execution_id, emit_type: EmitType, value: Value| { - match emit_type { - EmitType::OngoingExec => { - respond_count_for_emitter.fetch_add(1, Ordering::Relaxed); - } - EmitType::StartingExec => log::debug!("Flow execution started"), - EmitType::FinishedExec => log::debug!("Flow execution finished"), - EmitType::FailedExec => log::debug!("Flow execution failed"), - } runtime_emitter.emit(execution_id, emit_type, value); }; - let (signal, runtime_usage) = execute_flow(flow, engine, nats_remote, Some(&respond_emitter)); - - let has_responded = respond_count.load(Ordering::Relaxed) > 0; - let final_value = signal_to_terminal_value(signal); - - // Stream contract: if we already emitted intermediate values, do not send an extra terminal - // payload on the same reply subject. - if let Some(value) = final_value - && !has_responded - && let Some(reply_subject) = reply_subject - { - log::info!("Returning value for flow_id {}: {:?}", flow_id, value); - if let Err(err) = client - .publish(reply_subject, value.encode_to_vec().into()) - .await - { - log::error!("Failed to send response: {:?}", err); - } - } + let runtime_usage = execute_flow( + requested_execution_id, + flow, + engine, + nats_remote, + Some(&respond_emitter), + ); + log::debug!( + "Flow {} execution completed; no direct reply message published", + flow_id + ); if let Some(usage_service) = runtime_usage_service { usage_service.update_runtime_usage(runtime_usage).await; @@ -114,48 +101,34 @@ async fn process_message( } fn execute_flow( + execution_id: ExecutionId, flow: ExecutionFlow, engine: &ExecutionEngine, nats_remote: &NATSRemoteRuntime, respond_emitter: Option<&dyn RespondEmitter>, -) -> (Signal, RuntimeUsage) { +) -> RuntimeUsage { let start = Instant::now(); let flow_id = flow.flow_id; - let (signal, _reason) = engine.execute_flow(flow, Some(nats_remote), respond_emitter, true); + let (_signal, _reason) = engine.execute_flow_with_execution_id( + execution_id, + flow, + Some(nats_remote), + respond_emitter, + true, + ); let duration_millis = start.elapsed().as_millis() as i64; - ( - signal, - RuntimeUsage { - flow_id, - duration: duration_millis, - }, - ) + RuntimeUsage { + flow_id, + duration: duration_millis, + } } -fn signal_to_terminal_value(signal: Signal) -> Option { - match signal { - Signal::Failure(error) => { - log::error!("Runtime error occurred: {:?}", error); - Some(error.as_value()) - } - Signal::Success(value) => { - log::debug!("Execution ended with success signal"); - Some(value) - } - Signal::Return(value) => { - log::debug!("Execution ended with return signal"); - Some(value) - } - Signal::Respond(_) => { - log::debug!("Execution ended with respond signal"); - None - } - Signal::Stop => { - log::debug!("Received stop signal as last signal"); - Some(Value { - kind: Some(Kind::NullValue(0)), - }) - } +fn parse_execution_id_from_subject(subject: &async_nats::Subject) -> Option { + let raw = subject.as_str(); + let mut parts = raw.split('.'); + match (parts.next(), parts.next(), parts.next()) { + (Some("execution"), Some(uuid), None) => ExecutionId::parse_str(uuid).ok(), + _ => None, } } From 009f8a513b5ef16385ea22b3b1ba18f432a16af6 Mon Sep 17 00:00:00 2001 From: Raphael Date: Thu, 23 Apr 2026 19:00:25 +0200 Subject: [PATCH 27/28] feat: added flow to test new emitter --- flows/10_multiple_respond.json | 398 +++++++++++++++++++++++++++++++++ 1 file changed, 398 insertions(+) create mode 100644 flows/10_multiple_respond.json diff --git a/flows/10_multiple_respond.json b/flows/10_multiple_respond.json new file mode 100644 index 0000000..6332d09 --- /dev/null +++ b/flows/10_multiple_respond.json @@ -0,0 +1,398 @@ +{ + "name": "10_multiple_respond", + "description": "This flow checks if the emitter is working correctly, only works with taurus-manual", + "inputs": [ + { + "input": { + "http_method": "GET", + "headers": { + "Content-Type": "text/plain" + }, + "payload": { + "users": [ + { + "age": 22, + "email": "joe@text.com", + "name": "joe doe" + }, + { + "age": 56, + "email": "tom@text.com", + "name": "timmy doe" + }, + { + "age": 88, + "email": "willy@text.com", + "name": "willy wokner" + }, + { + "age": 12, + "email": "jeffy@text.com", + "name": "jeff jefferson" + } + ] + } + }, + "expected_result": { + "headers": { + "x": "y" + }, + "http_status_code": 200, + "payload": [ + "timmy doe", + "willy wokner" + ] + } + }, + { + "input": { + "http_method": "GET", + "headers": { + "Content-Type": "text/plain" + }, + "payload": null + }, + "expected_result": { + "headers": { + "x": "y" + }, + "http_status_code": 200, + "payload": [] + } + } + ], + "flow": { + "flowId": "1", + "projectId": "1", + "type": "REST", + "settings": [ + { + "databaseId": "1", + "flowSettingId": "httpURL", + "value": { + "stringValue": "/10" + } + }, + { + "databaseId": "2", + "flowSettingId": "httpMethod", + "value": { + "stringValue": "POST" + } + } + ], + "startingNodeId": "7", + "nodeFunctions": [ + { + "databaseId": "7", + "runtimeFunctionId": "std::list::filter", + "parameters": [ + { + "databaseId": "14", + "runtimeParameterId": "list", + "value": { + "referenceValue": { + "paths": [ + { + "path": "payload" + }, + { + "path": "users" + } + ], + "flowInput": {} + } + } + }, + { + "databaseId": "15", + "runtimeParameterId": "predicate", + "value": { + "nodeFunctionId": "8" + } + } + ], + "nextNodeId": "2" + }, + { + "databaseId": "8", + "runtimeFunctionId": "std::number::is_greater", + "parameters": [ + { + "databaseId": "16", + "runtimeParameterId": "first", + "value": { + "referenceValue": { + "paths": [ + { + "path": "age" + } + ], + "inputType": { + "nodeId": "7", + "parameterIndex": "1" + } + } + } + }, + { + "databaseId": "20", + "runtimeParameterId": "second", + "value": { + "literalValue": { + "numberValue": { + "integer": "35" + } + } + } + } + ], + "nextNodeId": "9" + }, + { + "databaseId": "9", + "runtimeFunctionId": "std::control::if_else", + "parameters": [ + { + "databaseId": "17", + "runtimeParameterId": "condition", + "value": { + "referenceValue": { + "nodeId": "8" + } + } + }, + { + "databaseId": "21", + "runtimeParameterId": "runnable", + "value": { + "nodeFunctionId": "1" + } + }, + { + "databaseId": "22", + "runtimeParameterId": "else_runnable", + "value": { + "nodeFunctionId": "3" + } + } + ], + "nextNodeId": "5" + }, + { + "databaseId": "1", + "runtimeFunctionId": "std::control::return", + "parameters": [ + { + "databaseId": "1", + "runtimeParameterId": "value", + "value": { + "literalValue": { + "boolValue": true + } + } + } + ] + }, + { + "databaseId": "3", + "runtimeFunctionId": "std::control::return", + "parameters": [ + { + "databaseId": "6", + "runtimeParameterId": "value", + "value": { + "literalValue": { + "boolValue": false + } + } + } + ] + }, + { + "databaseId": "5", + "runtimeFunctionId": "std::control::return", + "parameters": [ + { + "databaseId": "10", + "runtimeParameterId": "value", + "value": { + "literalValue": { + "boolValue": true + } + } + } + ] + }, + { + "databaseId": "2", + "runtimeFunctionId": "std::list::size", + "parameters": [ + { + "databaseId": "3", + "runtimeParameterId": "list", + "value": { + "referenceValue": { + "nodeId": "7" + } + } + } + ], + "nextNodeId": "10" + }, + { + "databaseId": "6", + "runtimeFunctionId": "std::control::return", + "parameters": [ + { + "databaseId": "13", + "runtimeParameterId": "value", + "value": { + "referenceValue": { + "paths": [ + { + "path": "name" + } + ], + "inputType": { + "nodeId": "10", + "parameterIndex": "1" + } + } + } + } + ] + }, + { + "databaseId": "10", + "runtimeFunctionId": "std::list::map", + "parameters": [ + { + "databaseId": "23", + "runtimeParameterId": "list", + "value": { + "referenceValue": { + "nodeId": "7" + } + } + }, + { + "databaseId": "24", + "runtimeParameterId": "transform", + "value": { + "nodeFunctionId": "6" + } + } + ], + "nextNodeId": "11" + }, + { + "databaseId": "11", + "runtimeFunctionId": "http::response::create", + "parameters": [ + { + "databaseId": "27", + "runtimeParameterId": "http_status_code", + "value": { + "literalValue": { + "numberValue": { + "integer": "200" + } + } + } + }, + { + "databaseId": "30", + "runtimeParameterId": "headers", + "value": { + "literalValue": { + "structValue": { + "fields": { + "x": { + "stringValue": "y" + } + } + } + } + } + }, + { + "databaseId": "31", + "runtimeParameterId": "payload", + "value": { + "referenceValue": { + "nodeId": "10" + } + } + } + ], + "nextNodeId": "4" + }, + { + "databaseId": "4", + "nextNodeId": "20", + "runtimeFunctionId": "rest::control::respond", + "parameters": [ + { + "databaseId": "8", + "runtimeParameterId": "http_response", + "value": { + "referenceValue": { + "nodeId": "11" + } + } + } + ] + }, + { + "nextNodeId": "21", + "databaseId": "20", + "runtimeFunctionId": "rest::control::respond", + "parameters": [ + { + "databaseId": "8", + "runtimeParameterId": "http_response", + "value": { + "referenceValue": { + "nodeId": "11" + } + } + } + ] + }, + { + "databaseId": "21", + "nextNodeId": "22", + "runtimeFunctionId": "rest::control::respond", + "parameters": [ + { + "databaseId": "8", + "runtimeParameterId": "http_response", + "value": { + "referenceValue": { + "nodeId": "11" + } + } + } + ] + }, + { + "databaseId": "22", + "runtimeFunctionId": "std::control::return", + "parameters": [ + { + "databaseId": "8", + "runtimeParameterId": "http_response", + "value": { + "referenceValue": { + "nodeId": "11" + } + } + } + ] + } + ], + "projectSlug": "project", + "signature": "(httpURL: HTTP_URL, httpMethod: HTTP_METHOD): { payload: { users: { name: TEXT, email: TEXT, age: NUMBER }[] }, headers: I }" + } +} From 57b1db042592088a126f9aaa291edd161853fa14 Mon Sep 17 00:00:00 2001 From: Raphael Date: Thu, 23 Apr 2026 19:00:43 +0200 Subject: [PATCH 28/28] deps: added taurus-provider --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 9c7d026..ce0a0ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1001,7 +1001,6 @@ name = "manual" version = "0.1.0" dependencies = [ "async-nats", - "async-trait", "clap", "env_logger", "log", @@ -1009,6 +1008,7 @@ dependencies = [ "serde", "serde_json", "taurus-core", + "taurus-provider", "tokio", "tonic", "tucana",