diff --git a/Cargo.lock b/Cargo.lock index 47c291f..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,7 +1008,7 @@ dependencies = [ "serde", "serde_json", "taurus-core", - "tests-core", + "taurus-provider", "tokio", "tonic", "tucana", @@ -1772,6 +1771,7 @@ dependencies = [ "prost", "rand 0.10.1", "taurus-core", + "taurus-provider", "tokio", "tonic", "tonic-health", @@ -1791,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" @@ -1813,17 +1832,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..3f53c22 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", "crates/taurus-provider"] resolver = "3" [workspace.package] @@ -23,8 +23,10 @@ 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.taurus-provider] +path = "./crates/taurus-provider" -[workspace.dependencies.tests-core] -path = "./crates/tests-core" 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/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.rs b/crates/core/src/context/executor.rs deleted file mode 100644 index 60568cf..0000000 --- a/crates/core/src/context/executor.rs +++ /dev/null @@ -1,652 +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>, -} - -/// 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, - } - } - - /// 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 - } - - /// 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()); - } - - _ => 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); - } - 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 }); - sig - }; - - (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); - } - 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()) - } -} 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 c51aa92..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 stop the execution of the flow completely - // - will return the value to the adapter - 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/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/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/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/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/core/Cargo.toml b/crates/taurus-core/Cargo.toml similarity index 100% rename from crates/core/Cargo.toml rename to crates/taurus-core/Cargo.toml diff --git a/crates/taurus-core/src/ERROR_CODES.md b/crates/taurus-core/src/ERROR_CODES.md new file mode 100644 index 0000000..9d40270 --- /dev/null +++ b/crates/taurus-core/src/ERROR_CODES.md @@ -0,0 +1,39 @@ +# Taurus Runtime Error Codes + +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 + +| 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` | +| `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/core/src/context/argument.rs b/crates/taurus-core/src/handler/argument.rs similarity index 83% rename from crates/core/src/context/argument.rs rename to crates/taurus-core/src/handler/argument.rs index f5d6698..c661a15 100644 --- a/crates/core/src/context/argument.rs +++ b/crates/taurus-core/src/handler/argument.rs @@ -1,5 +1,7 @@ -use crate::context::signal::Signal; -use crate::runtime::error::RuntimeError; +//! 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}; @@ -7,26 +9,28 @@ 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 + /// Eager value that can be consumed immediately by a handler. Eval(Value), - // Thunk of NodeFunction identifier - // - used for lazy execution of nodes + /// 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::simple( + Signal::Failure(RuntimeError::new( + "T-CORE-000202", "InvalidArgumentRuntimeError", format!("{} but it was the arugment: {:?}", msg, a), )) @@ -113,7 +117,8 @@ impl TryFromArgument for ListValue { Argument::Eval(Value { kind: Some(Kind::ListValue(list)), }) => Ok(list.clone()), - _ => Err(Signal::Failure(RuntimeError::simple( + _ => Err(Signal::Failure(RuntimeError::new( + "T-CORE-000202", "InvalidArgumentRuntimeError", format!("Expected array (ListValue) but it was: {:?}", a), ))), diff --git a/crates/core/src/context/macros.rs b/crates/taurus-core/src/handler/macros.rs similarity index 77% rename from crates/core/src/context/macros.rs rename to crates/taurus-core/src/handler/macros.rs index ed4644f..f9c238d 100644 --- a/crates/core/src/context/macros.rs +++ b/crates/taurus-core/src/handler/macros.rs @@ -1,12 +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::simple(...))`. +/// 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::context::signal::Signal::Failure( - $crate::runtime::error::RuntimeError::simple( + return $crate::types::signal::Signal::Failure( + $crate::types::errors::runtime_error::RuntimeError::new("T-CORE-000201", "InvalidArgumentRuntimeError", format!("Expected {__expected} args but received {}", $args_ident.len()), ) @@ -17,7 +19,7 @@ macro_rules! args { let mut __i: usize = 0; $( let $name: $ty = match < - $ty as $crate::context::argument::TryFromArgument + $ty as $crate::handler::argument::TryFromArgument >::try_from_argument(& $args_ident[__i]) { Ok(v) => v, Err(sig) => { @@ -39,8 +41,9 @@ macro_rules! args { macro_rules! no_args { ($args_ident:ident) => { if !$args_ident.is_empty() { - return $crate::context::signal::Signal::Failure( - $crate::runtime::error::RuntimeError::simple( + return $crate::types::signal::Signal::Failure( + $crate::types::errors::runtime_error::RuntimeError::new( + "T-CORE-000201", "InvalidArgumentRuntimeError", format!("Expected 0 args but received {}", $args_ident.len()), ), 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); + } + } +} 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/runtime/engine.rs b/crates/taurus-core/src/runtime/engine.rs new file mode 100644 index 0000000..c7e4599 --- /dev/null +++ b/crates/taurus-core/src/runtime/engine.rs @@ -0,0 +1,527 @@ +//! 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, ExecutionId, 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_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, + 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) { + 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()); + } + + 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(execution_id, 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, + execution_id, + 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(execution_id, EmitType::FailedExec, err.as_value()) + } + Signal::Success(value) | Signal::Return(value) | Signal::Respond(value) => { + emitter.emit(execution_id, EmitType::FinishedExec, value.clone()) + } + Signal::Stop => emitter.emit(execution_id, 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 = |_execution_id, 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 = |_execution_id, 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 = |_execution_id, 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/engine/compiler.rs b/crates/taurus-core/src/runtime/engine/compiler.rs new file mode 100644 index 0000000..3f4ae57 --- /dev/null +++ b/crates/taurus-core/src/runtime/engine/compiler.rs @@ -0,0 +1,164 @@ +//! 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-CORE-000101", + "FlowCompileError", + format!("Duplicate node id in flow: {}", node_id), + ), + CompileError::StartNodeMissing { node_id } => RuntimeError::new( + "T-CORE-000102", + "FlowCompileError", + format!("Start node not found in flow: {}", node_id), + ), + CompileError::NextNodeMissing { + node_id, + next_node_id, + } => RuntimeError::new( + "T-CORE-000103", + "FlowCompileError", + format!( + "Node {} points to missing next node {}", + node_id, next_node_id + ), + ), + CompileError::ParameterValueMissing { + node_id, + parameter_index, + } => RuntimeError::new( + "T-CORE-000104", + "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" + || node.definition_source.starts_with("draco") + { + 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..e88600f --- /dev/null +++ b/crates/taurus-core/src/runtime/engine/emitter.rs @@ -0,0 +1,49 @@ +//! Respond emitter abstraction used by the engine. + +use std::fmt::{Display, Formatter}; + +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)] +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, execution_id: ExecutionId, emit_type: EmitType, value: Value); +} + +impl RespondEmitter for F +where + F: Fn(ExecutionId, EmitType, Value) + ?Sized, +{ + fn emit(&self, execution_id: ExecutionId, emit_type: EmitType, value: Value) { + 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-core/src/runtime/engine/executor.rs b/crates/taurus-core/src/runtime/engine/executor.rs new file mode 100644 index 0000000..c8aeb27 --- /dev/null +++ b/crates/taurus-core/src/runtime/engine/executor.rs @@ -0,0 +1,646 @@ +//! 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, ExecutionId, 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>, + execution_id: ExecutionId, + 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, + execution_id, + 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>, + execution_id: ExecutionId, + 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(self.execution_id, 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-CORE-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, 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, value_store); + + 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-CORE-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-CORE-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-CORE-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-CORE-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, value_store: &ValueStore) -> Option { + self.tracer.map(|tracer| { + 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, value_store: &ValueStore) { + 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, value_store.trace_snapshot()); + } + + 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, +} 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..d34a130 --- /dev/null +++ b/crates/taurus-core/src/runtime/execution/render.rs @@ -0,0 +1,350 @@ +//! Human-readable trace renderer (single hybrid mode). + +use std::collections::HashMap; + +use crate::runtime::execution::trace::{ + ArgKind, EdgeKind, Outcome, StoreDiff, StoreResultStatus, TraceFrame, TraceRun, +}; + +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 + .ended_at + .map(|end| end.duration_since(frame.started_at).as_micros()) +} + +/// Render trace in execution order with branch markers and spacing. +pub fn render_trace(run: &TraceRun) -> String { + let theme = TraceTheme; + let mut by_id: HashMap = HashMap::new(); + for frame in &run.frames { + by_id.insert(frame.frame_id, frame); + } + + let mut out = String::new(); + if let Some(total_us) = total_duration_us(run) { + out.push_str(&format!("Total: {}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())); + } + out +} + +fn render_frame( + frame_id: u64, + by_id: &HashMap, + prefix: &str, + is_last: bool, + theme: &TraceTheme, + step: &mut usize, + out: &mut String, +) { + 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 { + "\\- " + } 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(); + + 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, + enter = theme.enter(), + id = frame.frame_id, + node = frame.node_id, + fn_name = frame.function_name, + depth = frame.depth, + duration = duration + )); + *step += 1; + + 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; + + for arg in &frame.args { + let arg_kind = match &arg.kind { + ArgKind::Literal => "literal".to_string(), + ArgKind::Reference { reference, hit } => { + let hit_state = if *hit { "hit" } else { "miss" }; + format!("reference {:?} ({})", reference, hit_state) + } + ArgKind::Thunk { + node_id, + eager, + executed, + } => { + let mode = if *eager { "eager" } else { "lazy" }; + let executed_state = if *executed { "executed" } else { "deferred" }; + format!("thunk node={} {} {}", node_id, mode, executed_state) + } + }; + 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 = arg_kind, + preview = arg.preview + )); + *step += 1; + } + + 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 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(), + }; + 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 let Some(diff) = &frame.store_diff { + render_store_diff(&display_prefix, continuation, theme, diff, step, out); + } +} + +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; + } + + for set in &diff.result_sets { + let status = match set.status { + StoreResultStatus::Success => "success", + StoreResultStatus::Error => "error", + }; + out.push_str(&format!( + "{step:04} {prefix}{continuation}{store:<5} result.set node={} [{}] {}\n", + set.node_id, + status, + set.preview, + step = *step, + prefix = prefix, + 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; + } + + 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(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, + } + } + } +} 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..9733566 --- /dev/null +++ b/crates/taurus-core/src/runtime/execution/trace.rs @@ -0,0 +1,222 @@ +//! Trace V2 runtime execution model. +//! +//! 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. +#[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 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 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 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 one full execution run. +#[derive(Debug, Clone)] +pub struct TraceRun { + 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 new file mode 100644 index 0000000..894060d --- /dev/null +++ b/crates/taurus-core/src/runtime/execution/tracer.rs @@ -0,0 +1,186 @@ +//! In-memory Trace V2 collector. + +use std::time::Instant; + +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, 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, store_after: StoreSnapshot); +} + +/// Default trace recorder used by the runtime engine. +pub struct Tracer { + next_id: u64, + 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 TraceFrame { + 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, + store_before: StoreSnapshot, + ) -> u64 { + if self.run.is_none() { + self.run = Some(TraceRun { + started_at: Instant::now(), + ended_at: None, + frames: vec![], + 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 = TraceFrame { + frame_id, + parent_frame_id, + depth, + node_id, + function_name: function_name.to_string(), + args: vec![], + outcome: 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_frame_id == 0 { + run.root_frame_id = 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(FrameChild { + edge, + child_frame_id: 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, 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 new file mode 100644 index 0000000..20d852d --- /dev/null +++ b/crates/taurus-core/src/runtime/execution/value_store.rs @@ -0,0 +1,213 @@ +//! Mutable value store used by runtime execution to resolve references. + +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)] +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(&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() + } + + 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(", ")) + } + } +} diff --git a/crates/core/src/runtime/functions/array.rs b/crates/taurus-core/src/runtime/functions/array.rs similarity index 64% rename from crates/core/src/runtime/functions/array.rs rename to crates/taurus-core/src/runtime/functions/array.rs index 57cbcda..7686c5e 100644 --- a/crates/core/src/runtime/functions/array.rs +++ b/crates/taurus-core/src/runtime/functions/array.rs @@ -1,98 +1,191 @@ +//! 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::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::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 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)), - ] -} +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::simple_str("InvalidArgumentRuntimeError", err)), + _ => Err(RuntimeError::new( + "T-STD-00001", + "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( + _ => Err(RuntimeError::new( + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected boolean result from predicate", )), } } +fn fail(category: &str, message: impl Into) -> Signal { + Signal::Failure(RuntimeError::new("T-STD-00001", 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 Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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::simple_str( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "IndexOutOfBoundsRuntimeError", "Negative index", )); @@ -100,37 +193,37 @@ fn at( let i = index as usize; match array.values.get(i) { Some(item) => Signal::Success(item.clone()), - None => Signal::Failure(RuntimeError::simple( + None => fail( "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, + _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 Signal::Failure(RuntimeError::simple( + 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 Signal::Failure(RuntimeError::simple( + return fail( "InvalidArgumentRuntimeError", format!( "Expected two arrays as arguments but received rhs={:?}", rhs_v ), - )); + ); }; let mut result = lhs.values.clone(); @@ -143,17 +236,12 @@ fn concat( fn filter( args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> 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_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") { @@ -162,31 +250,21 @@ fn filter( }; 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 = unary_input_type(ctx); 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; - } + 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), } } - ctx.clear_input_type(input_type); Signal::Success(Value { kind: Some(Kind::ListValue(ListValue { values: out })), }) @@ -194,112 +272,70 @@ fn filter( fn find( args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> 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_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 node_id = ctx.get_current_node_id(); - let input_type = InputType { - node_id, - parameter_index: 1, - input_index: 0, - }; + let input_type = unary_input_type(ctx); 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; - } + 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), } } - ctx.clear_input_type(input_type); - Signal::Failure(RuntimeError::simple_str( + Signal::Failure(RuntimeError::new( + "T-STD-00001", "NotFoundError", "No item found that satisfies the predicate", )) } fn find_last( args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> 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 (array_v, predicate_node) = match parse_array_and_thunk("find_last", args) { + Ok(data) => data, + Err(signal) => return signal, }; - let mut array = match as_list(array_v, "Expected first argument to be an array") { + let 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, - }; + let input_type = unary_input_type(ctx); - 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; - } + 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), } } - ctx.clear_input_type(input_type); - Signal::Failure(RuntimeError::simple_str( + + Signal::Failure(RuntimeError::new( + "T-STD-00001", "NotFoundError", "No item found that satisfies the predicate", )) @@ -307,71 +343,51 @@ fn find_last( fn find_index( args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> 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_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 node_id = ctx.get_current_node_id(); - let input_type = InputType { - node_id, - parameter_index: 1, - input_index: 0, - }; + let input_type = unary_input_type(ctx); 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; - } + 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), } } - ctx.clear_input_type(input_type); - Signal::Failure(RuntimeError::simple_str( + Signal::Failure(RuntimeError::new( + "T-STD-00001", "NotFoundError", "No item found that satisfies the predicate", )) } fn first( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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::simple_str( + None => Signal::Failure(RuntimeError::new( + "T-STD-00001", "ArrayEmptyRuntimeError", "This array is empty", )), @@ -380,13 +396,14 @@ fn first( fn last( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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::simple_str( + None => Signal::Failure(RuntimeError::new( + "T-STD-00001", "ArrayEmptyRuntimeError", "This array is empty", )), @@ -395,45 +412,29 @@ fn last( fn for_each( args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> 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_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 node_id = ctx.get_current_node_id(); - let input_type = InputType { - node_id, - parameter_index: 1, - input_index: 0, - }; + let input_type = unary_input_type(ctx); 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); + let sig = run_with_unary_input(ctx, input_type, idx, item, run, transform_node); - match sig { - Signal::Success(_) => {} - other - @ (Signal::Failure(_) | Signal::Return(_) | Signal::Respond(_) | Signal::Stop) => { - return other; - } + match callback_result_value(sig) { + Ok(_) => {} + Err(other) => return other, } } - ctx.clear_input_type(input_type); Signal::Success(Value { kind: Some(Kind::NullValue(0)), }) @@ -472,17 +473,12 @@ fn format_value_json(value: &Value) -> String { fn map( args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> 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_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") { @@ -491,28 +487,16 @@ fn map( }; 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, - }; + let input_type = unary_input_type(ctx); 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; - } + 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, } } - ctx.clear_input_type(input_type); Signal::Success(Value { kind: Some(Kind::ListValue(ListValue { values: out })), }) @@ -520,12 +504,13 @@ fn map( fn push( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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::simple_str( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected first argument to be an array", )); @@ -538,12 +523,13 @@ fn push( fn pop( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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::simple_str( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected an array as an argument", )); @@ -556,12 +542,13 @@ fn pop( fn remove( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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::simple_str( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected first argument to be an array", )); @@ -573,21 +560,22 @@ fn remove( kind: Some(Kind::ListValue(array)), }) } else { - Signal::Failure(RuntimeError::simple( + fail( "ValueNotFoundRuntimeError", format!("Item {:?} not found in array", item), - )) + ) } } fn is_empty( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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::simple_str( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected an array as an argument", )); @@ -599,12 +587,13 @@ fn is_empty( fn size( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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::simple_str( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected an array as an argument", )); @@ -614,12 +603,13 @@ fn size( fn index_of( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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::simple_str( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected first argument to be an array", )); @@ -627,21 +617,22 @@ fn index_of( match array.values.iter().position(|x| *x == item) { Some(i) => Signal::Success(value_from_i64(i as i64)), - None => Signal::Failure(RuntimeError::simple( + None => fail( "ValueNotFoundRuntimeError", format!("Item {:?} not found in array", item), - )), + ), } } fn to_unique( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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::simple_str( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected an array as an argument", )); @@ -661,17 +652,12 @@ fn to_unique( fn sort( args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> 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_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") { @@ -679,87 +665,51 @@ fn sort( Err(e) => return Signal::Failure(e), }; - let mut out: Vec = Vec::new(); let node_id = ctx.get_current_node_id(); - let input_type = InputType { + let left_input = InputType { node_id, parameter_index: 1, input_index: 0, }; - let input_type_next = InputType { + let right_input = InputType { node_id, parameter_index: 1, input_index: 1, }; - let mut signals = Vec::new(); + let mut comparator_failure: Option = None; 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={}", + 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, - preview_value(a), - preview_value(b) - )); + left, + right, + run, + transform_node, + ); 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; + match comparator_ordering(signal, false) { + Ok(ordering) => ordering, + Err(signal) => { + comparator_failure = Some(signal); + Ordering::Equal } } - } - - 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, - } }); + if let Some(signal) = comparator_failure { + return signal; + } + Signal::Success(Value { kind: Some(Kind::ListValue(array)), }) @@ -767,17 +717,12 @@ fn sort( fn sort_reverse( args: &[Argument], - ctx: &mut Context, - run: &mut dyn FnMut(i64, &mut Context) -> Signal, + ctx: &mut ValueStore, + run: &mut dyn FnMut(i64, &mut ValueStore) -> 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_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") { @@ -785,89 +730,51 @@ fn sort_reverse( Err(e) => return Signal::Failure(e), }; - let mut out: Vec = Vec::new(); let node_id = ctx.get_current_node_id(); - let input_type = InputType { + let left_input = InputType { node_id, parameter_index: 1, input_index: 0, }; - let input_type_next = InputType { + let right_input = InputType { node_id, parameter_index: 1, input_index: 1, }; - let mut signals = Vec::new(); + let mut comparator_failure: Option = None; 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={}", + 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, - preview_value(a), - preview_value(b) - )); + left, + right, + run, + transform_node, + ); 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; + match comparator_ordering(signal, true) { + Ok(ordering) => ordering, + Err(signal) => { + comparator_failure = Some(signal); + Ordering::Equal } } - } - - 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, - } }); + if let Some(signal) = comparator_failure { + return signal; + } + Signal::Success(Value { kind: Some(Kind::ListValue(array)), }) @@ -875,12 +782,13 @@ fn sort_reverse( fn reverse( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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::simple_str( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected an array as an argument", )); @@ -893,12 +801,13 @@ fn reverse( fn flat( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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::simple_str( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected an array as an argument", )); @@ -919,8 +828,8 @@ fn flat( fn min( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => array: ListValue); @@ -949,7 +858,8 @@ fn min( 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( + None => Signal::Failure(RuntimeError::new( + "T-STD-00001", "ArrayEmptyRuntimeError", "Array is empty", )), @@ -958,8 +868,8 @@ fn min( fn max( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => array: ListValue); @@ -988,7 +898,8 @@ fn max( 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( + None => Signal::Failure(RuntimeError::new( + "T-STD-00001", "ArrayEmptyRuntimeError", "Array is empty", )), @@ -997,8 +908,8 @@ fn max( fn sum( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => array: ListValue); @@ -1037,8 +948,8 @@ fn sum( fn join( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => array: ListValue, separator: String); @@ -1056,7 +967,7 @@ fn join( #[cfg(test)] mod tests { use super::*; - use crate::context::context::Context; + 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}; @@ -1122,13 +1033,13 @@ mod tests { } } - fn dummy_run(_: i64, _: &mut Context) -> Signal { + 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 Context) -> Signal { + 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); @@ -1139,7 +1050,7 @@ mod tests { } } - fn run_from_values(seq: Vec) -> impl FnMut(i64, &mut Context) -> Signal { + 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 { @@ -1153,7 +1064,7 @@ mod tests { // --- at ------------------------------------------------------------------ #[test] fn test_at_success() { - let mut ctx = Context::default(); + 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)]); @@ -1181,7 +1092,7 @@ mod tests { #[test] fn test_at_error() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; let arr = v_list(vec![v_num(1.0)]); @@ -1218,7 +1129,7 @@ mod tests { // --- concat -------------------------------------------------------------- #[test] fn test_concat_success() { - let mut ctx = Context::default(); + 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)]); @@ -1230,7 +1141,7 @@ mod tests { #[test] fn test_concat_error() { - let mut ctx = Context::default(); + 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) { @@ -1254,7 +1165,7 @@ mod tests { // --- filter / find / find_last / find_index ------------------------------ #[test] fn test_filter_success() { - let mut ctx = Context::default(); + 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)); @@ -1265,7 +1176,7 @@ mod tests { #[test] fn test_filter_error() { - let mut ctx = Context::default(); + 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)]); @@ -1286,7 +1197,7 @@ mod tests { // --- first / last -------------------------------------------------------- #[test] fn test_first_success() { - let mut ctx = Context::default(); + 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!( @@ -1297,7 +1208,7 @@ mod tests { #[test] fn test_first_error() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; match first(&[a_val(v_list(vec![]))], &mut ctx, &mut run) { Signal::Failure(_) => {} @@ -1315,7 +1226,7 @@ mod tests { #[test] fn test_last_success() { - let mut ctx = Context::default(); + 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"); @@ -1323,7 +1234,7 @@ mod tests { #[test] fn test_last_error() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; match last(&[a_val(v_list(vec![]))], &mut ctx, &mut run) { Signal::Failure(_) => {} @@ -1338,9 +1249,9 @@ mod tests { // --- for_each / map ------------------------------------------------------ #[test] fn test_for_each_and_map() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut called = 0usize; - let mut run = |_, _ctx: &mut Context| { + let mut run = |_, _ctx: &mut ValueStore| { called += 1; Signal::Success(Value { kind: Some(Kind::NullValue(0)), @@ -1374,10 +1285,93 @@ mod tests { 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 = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; let out = expect_list(push( &[ @@ -1393,7 +1387,7 @@ mod tests { #[test] fn test_push_error() { - let mut ctx = Context::default(); + 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(_) => {} @@ -1411,7 +1405,7 @@ mod tests { #[test] fn test_pop_success() { - let mut ctx = Context::default(); + 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)]))], @@ -1425,7 +1419,7 @@ mod tests { #[test] fn test_pop_error() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; match pop(&[a_val(v_str("nope"))], &mut ctx, &mut run) { Signal::Failure(_) => {} @@ -1439,7 +1433,7 @@ mod tests { #[test] fn test_remove_success_and_error() { - let mut ctx = Context::default(); + 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")]); @@ -1477,7 +1471,7 @@ mod tests { // --- is_empty / size ----------------------------------------------------- #[test] fn test_is_empty_and_size() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert!(expect_bool(is_empty( &[a_val(v_list(vec![]))], @@ -1505,7 +1499,7 @@ mod tests { #[test] fn test_is_empty_error_and_size_error() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; match is_empty(&[a_val(v_str("nope"))], &mut ctx, &mut run) { Signal::Failure(_) => {} @@ -1528,7 +1522,7 @@ mod tests { // --- index_of / to_unique ------------------------------------------------ #[test] fn test_index_of_and_to_unique() { - let mut ctx = Context::default(); + 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!( @@ -1557,7 +1551,7 @@ mod tests { #[test] fn test_index_of_error() { - let mut ctx = Context::default(); + 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(_) => {} @@ -1576,7 +1570,7 @@ mod tests { // --- sort / sort_reverse ------------------------------------------------- #[test] fn test_sort_and_sort_reverse() { - let mut ctx = Context::default(); + 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")]); @@ -1593,7 +1587,7 @@ mod tests { // --- reverse / flat ------------------------------------------------------ #[test] fn test_reverse_success_and_error() { - let mut ctx = Context::default(); + 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)]))], @@ -1615,7 +1609,7 @@ mod tests { #[test] fn test_flat_success() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; let nested = v_list(vec![ v_num(1.0), @@ -1634,7 +1628,7 @@ mod tests { // --- min / max / sum ----------------------------------------------------- #[test] fn test_min_max_sum_success_and_error() { - let mut ctx = Context::default(); + 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!( @@ -1679,7 +1673,7 @@ mod tests { // --- join ---------------------------------------------------------------- #[test] fn test_join_success_and_error() { - let mut ctx = Context::default(); + 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!( diff --git a/crates/core/src/runtime/functions/boolean.rs b/crates/taurus-core/src/runtime/functions/boolean.rs similarity index 82% rename from crates/core/src/runtime/functions/boolean.rs rename to crates/taurus-core/src/runtime/functions/boolean.rs index c4b230a..c097f54 100644 --- a/crates/core/src/runtime/functions/boolean.rs +++ b/crates/taurus-core/src/runtime/functions/boolean.rs @@ -1,30 +1,30 @@ -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; +//! 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 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)), - ] -} +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 Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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 })) @@ -32,8 +32,8 @@ fn as_number( fn as_text( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: bool); Signal::Success(Value { @@ -43,8 +43,8 @@ fn as_text( fn from_number( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => number: f64); let is_zero = number == 0.0; @@ -55,16 +55,18 @@ fn from_number( fn from_text( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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::simple( + Err(_) => Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvalidArgumentRuntimeError", format!("Failed to parse boolean from string: {:?}", text), )), @@ -73,8 +75,8 @@ fn from_text( fn is_equal( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => lhs: bool, rhs: bool); Signal::Success(Value { @@ -84,8 +86,8 @@ fn is_equal( fn negate( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: bool); Signal::Success(Value { @@ -95,7 +97,7 @@ fn negate( #[cfg(test)] mod tests { use super::*; - use crate::context::context::Context; + use crate::runtime::execution::value_store::ValueStore; use crate::value::{number_to_f64, value_from_f64}; use tucana::shared::{Value, value::Kind}; @@ -141,7 +143,7 @@ mod tests { } // dummy `run` closure (unused by these handlers) - fn dummy_run(_: i64, _: &mut Context) -> Signal { + fn dummy_run(_: i64, _: &mut ValueStore) -> Signal { Signal::Success(Value { kind: Some(Kind::BoolValue(true)), }) @@ -151,7 +153,7 @@ mod tests { #[test] fn test_as_number_success() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert_eq!( expect_num(as_number(&[a_bool(true)], &mut ctx, &mut run)), @@ -167,7 +169,7 @@ mod tests { #[test] fn test_as_number_errors() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); // wrong arity: none let mut run = dummy_run; @@ -193,7 +195,7 @@ mod tests { #[test] fn test_as_text_success() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert_eq!( @@ -210,7 +212,7 @@ mod tests { #[test] fn test_as_text_errors() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; match as_text(&[], &mut ctx, &mut run) { @@ -233,7 +235,7 @@ mod tests { #[test] fn test_from_number_success() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert_eq!( @@ -263,7 +265,7 @@ mod tests { #[test] fn test_from_number_errors() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; match from_number(&[], &mut ctx, &mut run) { @@ -286,7 +288,7 @@ mod tests { #[test] fn test_from_text_success_and_errors() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); // success (case-insensitive) let mut run = dummy_run; @@ -329,7 +331,7 @@ mod tests { #[test] fn test_is_equal_and_errors() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); // equalities let mut run = dummy_run; @@ -376,7 +378,7 @@ mod tests { #[test] fn test_negate_success_and_errors() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert_eq!( 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..fd24c9b --- /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-STD-00001", + "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-STD-00001", + "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/taurus-core/src/runtime/functions/http.rs similarity index 61% rename from crates/core/src/runtime/functions/http.rs rename to crates/taurus-core/src/runtime/functions/http.rs index ab5f868..2e207e9 100644 --- a/crates/core/src/runtime/functions/http.rs +++ b/crates/taurus-core/src/runtime/functions/http.rs @@ -1,74 +1,83 @@ -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; +//! 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 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, 3)), - ] +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-STD-00001", category, message)) } fn respond( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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::simple( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvalidArgumentRuntimeError", "Missing 'headers' field".to_string(), )); }; let Some(status_code_val) = fields.get("http_status_code") else { - return Signal::Failure(RuntimeError::simple( + return fail( "InvalidArgumentRuntimeError", - "Missing 'status_code' field".to_string(), - )); + "Missing 'http_status_code' field", + ); }; let Some(payload_val) = fields.get("payload") else { - return Signal::Failure(RuntimeError::simple( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvalidArgumentRuntimeError", "Missing 'payload' field".to_string(), )); }; let Some(Kind::StructValue(_headers_struct)) = &headers_val.kind else { - return Signal::Failure(RuntimeError::simple_str( + return fail( "InvalidArgumentRuntimeError", "Expected 'headers' to be StructValue", - )); + ); }; let Some(Kind::NumberValue(_status_code_str)) = &status_code_val.kind else { - return Signal::Failure(RuntimeError::simple( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvalidArgumentRuntimeError", "Expected 'status_code' to be NumberValue".to_string(), )); }; let Some(_payload_kind) = &payload_val.kind else { - return Signal::Failure(RuntimeError::simple( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "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())), }) @@ -76,8 +85,8 @@ fn respond( fn create_request( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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(); @@ -99,8 +108,8 @@ fn create_request( fn create_response( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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(); 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/core/src/runtime/functions/number.rs b/crates/taurus-core/src/runtime/functions/number.rs similarity index 77% rename from crates/core/src/runtime/functions/number.rs rename to crates/taurus-core/src/runtime/functions/number.rs index 8eb1be7..52a0f55 100644 --- a/crates/core/src/runtime/functions/number.rs +++ b/crates/taurus-core/src/runtime/functions/number.rs @@ -1,77 +1,79 @@ +//! 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::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::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::simple_str( + Signal::Failure(RuntimeError::new( + "T-STD-00001", "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), - ), - ] -} +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 Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: NumberValue); @@ -80,7 +82,8 @@ fn has_digits( number_value::Number::Integer(_) => Signal::Success(false.to_value()), number_value::Number::Float(_) => Signal::Success(true.to_value()), }, - None => Signal::Failure(RuntimeError::simple_str( + None => Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvlaidArgumentExeption", "Had NumberValue but no inner number value (was null)", )), @@ -89,13 +92,14 @@ fn has_digits( fn remove_digits( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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::simple_str( + None => Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvlaidArgumentExeption", "Had NumberValue but no inner number value (was null)", )), @@ -104,10 +108,11 @@ fn remove_digits( fn add( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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) @@ -127,8 +132,8 @@ fn add( fn multiply( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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))) = @@ -150,8 +155,8 @@ fn multiply( fn substract( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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))) = @@ -173,8 +178,8 @@ fn substract( fn divide( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => lhs: NumberValue, rhs: NumberValue); @@ -184,7 +189,8 @@ fn divide( }; if rhs_f == 0.0 { - return Signal::Failure(RuntimeError::simple_str( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "DivisionByZero", "You cannot divide by zero", )); @@ -207,8 +213,8 @@ fn divide( fn modulo( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => lhs: NumberValue, rhs: NumberValue); @@ -218,7 +224,8 @@ fn modulo( }; if rhs_f == 0.0 { - return Signal::Failure(RuntimeError::simple_str( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "DivisionByZero", "You cannot divide by zero", )); @@ -240,8 +247,8 @@ fn modulo( fn abs( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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 @@ -258,8 +265,8 @@ fn abs( fn is_positive( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: NumberValue); let value = match num_f64(&value) { @@ -273,8 +280,8 @@ fn is_positive( fn is_greater( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => lhs: NumberValue, rhs: NumberValue); let lhs = match num_f64(&lhs) { @@ -292,8 +299,8 @@ fn is_greater( fn is_less( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => lhs: NumberValue, rhs: NumberValue); let lhs = match num_f64(&lhs) { @@ -311,8 +318,8 @@ fn is_less( fn is_zero( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: NumberValue); let value = match num_f64(&value) { @@ -326,8 +333,8 @@ fn is_zero( fn square( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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 @@ -344,8 +351,8 @@ fn square( fn exponential( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => base: NumberValue, exponent: NumberValue); match (base.number, exponent.number) { @@ -373,8 +380,8 @@ fn exponential( fn pi( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { no_args!(args); Signal::Success(value_from_f64(f64::consts::PI)) @@ -382,8 +389,8 @@ fn pi( fn euler( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { no_args!(args); Signal::Success(value_from_f64(f64::consts::E)) @@ -391,8 +398,8 @@ fn euler( fn infinity( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { no_args!(args); Signal::Success(value_from_f64(f64::INFINITY)) @@ -400,8 +407,8 @@ fn infinity( fn round_up( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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) { @@ -424,8 +431,8 @@ fn round_up( fn round_down( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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) { @@ -448,8 +455,8 @@ fn round_down( fn round( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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) { @@ -472,8 +479,8 @@ fn round( fn square_root( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: NumberValue); let value = match num_f64(&value) { @@ -485,8 +492,8 @@ fn square_root( fn root( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: NumberValue, root: NumberValue); let value = match num_f64(&value) { @@ -502,8 +509,8 @@ fn root( fn log( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: NumberValue, base: NumberValue); let value = match num_f64(&value) { @@ -519,8 +526,8 @@ fn log( fn ln( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: NumberValue); let value = match num_f64(&value) { @@ -532,8 +539,8 @@ fn ln( fn from_text( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => string_value: String); @@ -542,7 +549,8 @@ fn from_text( } match string_value.parse::() { Ok(v) => Signal::Success(value_from_f64(v)), - Err(_) => Signal::Failure(RuntimeError::simple( + Err(_) => Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvalidArgumentRuntimeError", format!("Failed to parse string as number: {}", string_value), )), @@ -551,8 +559,8 @@ fn from_text( fn as_text( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: NumberValue); let value = match num_f64(&value) { @@ -566,8 +574,8 @@ fn as_text( fn min( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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))) = @@ -588,8 +596,8 @@ fn min( fn max( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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))) = @@ -610,8 +618,8 @@ fn max( fn negate( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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 @@ -628,8 +636,8 @@ fn negate( fn random( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => min: NumberValue, max: NumberValue); @@ -643,7 +651,8 @@ fn random( }; if min_f > max_f { - return Signal::Failure(RuntimeError::simple_str( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvalidRange", "First number can't be bigger then second when creating a range for std::math::random", )); @@ -656,8 +665,8 @@ fn random( fn sin( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: NumberValue); let value = match num_f64(&value) { @@ -669,8 +678,8 @@ fn sin( fn cos( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: NumberValue); let value = match num_f64(&value) { @@ -682,8 +691,8 @@ fn cos( fn tan( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: NumberValue); let value = match num_f64(&value) { @@ -695,8 +704,8 @@ fn tan( fn arcsin( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: NumberValue); let value = match num_f64(&value) { @@ -708,8 +717,8 @@ fn arcsin( fn arccos( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: NumberValue); let value = match num_f64(&value) { @@ -721,8 +730,8 @@ fn arccos( fn arctan( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: NumberValue); let value = match num_f64(&value) { @@ -734,8 +743,8 @@ fn arctan( fn sinh( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: NumberValue); let value = match num_f64(&value) { @@ -747,8 +756,8 @@ fn sinh( fn cosh( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: NumberValue); let value = match num_f64(&value) { @@ -760,8 +769,8 @@ fn cosh( fn clamp( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: NumberValue, min: NumberValue, max: NumberValue); if let ( @@ -789,8 +798,8 @@ fn clamp( fn is_equal( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => lhs: NumberValue, rhs: NumberValue); let lhs = match num_f64(&lhs) { @@ -808,8 +817,8 @@ fn is_equal( #[cfg(test)] mod tests { use super::*; - use crate::context::argument::Argument; - use crate::context::context::Context; + 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}; @@ -866,8 +875,8 @@ mod tests { } } - // dummy runner for handlers that accept `run: &mut dyn FnMut(i64, &mut Context) -> Signal` - fn dummy_run(_: i64, _: &mut Context) -> Signal { + // 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)), }) @@ -875,7 +884,7 @@ mod tests { #[test] fn test_add_and_multiply() { - let mut ctx = Context::default(); + 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)), @@ -891,7 +900,7 @@ mod tests { #[test] fn test_has_digits_and_remove_digits() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert!(!expect_bool(has_digits(&[a_int(42)], &mut ctx, &mut run))); @@ -914,7 +923,7 @@ mod tests { #[test] fn test_substract_and_divide() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert_eq!( @@ -938,7 +947,7 @@ mod tests { #[test] fn test_modulo_and_abs() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert_eq!( @@ -959,7 +968,7 @@ mod tests { #[test] fn test_comparisons_and_zero() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert!(expect_bool(is_positive(&[a_num(5.0)], &mut ctx, &mut run))); @@ -993,7 +1002,7 @@ mod tests { #[test] fn test_powers_and_exponential() { - let mut ctx = Context::default(); + 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); @@ -1007,7 +1016,7 @@ mod tests { #[test] fn test_constants() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert!( @@ -1026,7 +1035,7 @@ mod tests { #[test] fn test_rounding() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert_eq!( @@ -1057,7 +1066,7 @@ mod tests { #[test] fn test_roots_and_logs() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert_eq!( @@ -1081,7 +1090,7 @@ mod tests { #[test] fn test_text_conversions() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert_eq!( @@ -1105,7 +1114,7 @@ mod tests { #[test] fn test_min_max_and_negate() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert_eq!( @@ -1125,7 +1134,7 @@ mod tests { #[test] fn test_random_range() { - let mut ctx = Context::default(); + 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)); @@ -1134,7 +1143,7 @@ mod tests { #[test] fn test_random_range_numbers_equal() { - let mut ctx = Context::default(); + 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)); @@ -1143,7 +1152,7 @@ mod tests { #[test] fn test_random_range_fist_bigger_then_second() { - let mut ctx = Context::default(); + 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); @@ -1152,7 +1161,7 @@ mod tests { #[test] fn test_trig_and_hyperbolic() { - let mut ctx = Context::default(); + 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)); @@ -1189,7 +1198,7 @@ mod tests { #[test] fn test_clamp_and_is_equal() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert_eq!( 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/core/src/runtime/functions/text.rs b/crates/taurus-core/src/runtime/functions/text.rs similarity index 77% rename from crates/core/src/runtime/functions/text.rs rename to crates/taurus-core/src/runtime/functions/text.rs index 6ad1f98..383642b 100644 --- a/crates/core/src/runtime/functions/text.rs +++ b/crates/taurus-core/src/runtime/functions/text.rs @@ -1,53 +1,56 @@ -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; +//! 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 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)), - ] -} +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::simple( + Signal::Failure(RuntimeError::new( + "T-STD-00001", "InvalidArgumentRuntimeError", msg.into(), )) @@ -55,8 +58,8 @@ fn arg_err>(msg: S) -> Signal { fn as_bytes( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String); @@ -73,8 +76,8 @@ fn as_bytes( fn byte_size( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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)) @@ -82,8 +85,8 @@ fn byte_size( fn capitalize( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String); @@ -109,8 +112,8 @@ fn capitalize( fn uppercase( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String); Signal::Success(Value { @@ -120,8 +123,8 @@ fn uppercase( fn lowercase( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String); Signal::Success(Value { @@ -131,8 +134,8 @@ fn lowercase( fn swapcase( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String); @@ -156,8 +159,8 @@ fn swapcase( fn trim( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String); Signal::Success(Value { @@ -167,8 +170,8 @@ fn trim( fn chars( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String); @@ -186,8 +189,8 @@ fn chars( fn at( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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) { @@ -204,7 +207,8 @@ fn at( Some(c) => Signal::Success(Value { kind: Some(Kind::StringValue(c.to_string())), }), - None => Signal::Failure(RuntimeError::simple( + None => Signal::Failure(RuntimeError::new( + "T-STD-00001", "IndexOutOfBoundsRuntimeError", format!( "Index {} is out of bounds for string of length {}", @@ -217,8 +221,8 @@ fn at( fn append( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String, suffix: String); Signal::Success(Value { @@ -228,8 +232,8 @@ fn append( fn prepend( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String, prefix: String); Signal::Success(Value { @@ -239,8 +243,8 @@ fn prepend( fn insert( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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) { @@ -253,9 +257,10 @@ fn insert( } let pos = position as usize; - // Byte-wise position (consistent with previous behavior). For char-wise, compute byte index from char idx. + // Byte-wise position is kept intentionally to match existing flow behavior. if pos > value.len() { - return Signal::Failure(RuntimeError::simple( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "IndexOutOfBoundsRuntimeError", format!("Position {} exceeds byte length {}", pos, value.len()), )); @@ -271,8 +276,8 @@ fn insert( fn length( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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)) @@ -280,8 +285,8 @@ fn length( fn remove( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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) { @@ -302,7 +307,8 @@ fn remove( let chars = value.chars().collect::>(); if from_u > chars.len() || to_u > chars.len() { - return Signal::Failure(RuntimeError::simple( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "IndexOutOfBoundsRuntimeError", format!( "Indices [{}, {}) out of bounds for length {}", @@ -327,8 +333,8 @@ fn remove( fn replace( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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); @@ -339,8 +345,8 @@ fn replace( fn replace_first( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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); @@ -351,8 +357,8 @@ fn replace_first( fn replace_last( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String, old: String, new: String); @@ -377,8 +383,8 @@ fn replace_last( fn hex( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String); @@ -395,8 +401,8 @@ fn hex( fn octal( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String); @@ -413,8 +419,8 @@ fn octal( fn index_of( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String, sub: String); @@ -426,8 +432,8 @@ fn index_of( fn contains( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String, sub: String); Signal::Success(Value { @@ -437,8 +443,8 @@ fn contains( fn split( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String, delimiter: String); @@ -456,8 +462,8 @@ fn split( fn reverse( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String); @@ -469,8 +475,8 @@ fn reverse( fn starts_with( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String, prefix: String); Signal::Success(Value { @@ -480,8 +486,8 @@ fn starts_with( fn ends_with( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String, suffix: String); Signal::Success(Value { @@ -491,8 +497,8 @@ fn ends_with( fn to_ascii( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String); @@ -508,8 +514,8 @@ fn to_ascii( fn from_ascii( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _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); @@ -539,8 +545,8 @@ fn from_ascii( // NOTE: "encode"/"decode" currently only support base64. fn encode( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String, encoding: String); @@ -558,8 +564,8 @@ fn encode( fn decode( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => value: String, encoding: String); @@ -568,14 +574,16 @@ fn decode( Ok(bytes) => match String::from_utf8(bytes) { Ok(s) => s, Err(err) => { - return Signal::Failure(RuntimeError::simple( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "DecodeError", format!("Failed to decode base64 bytes to UTF-8: {:?}", err), )); } }, Err(err) => { - return Signal::Failure(RuntimeError::simple( + return Signal::Failure(RuntimeError::new( + "T-STD-00001", "DecodeError", format!("Failed to decode base64 string: {:?}", err), )); @@ -591,8 +599,8 @@ fn decode( fn is_equal( args: &[Argument], - _ctx: &mut Context, - _run: &mut dyn FnMut(i64, &mut Context) -> Signal, + _ctx: &mut ValueStore, + _run: &mut dyn FnMut(i64, &mut ValueStore) -> Signal, ) -> Signal { args!(args => lhs: String, rhs: String); Signal::Success(Value { @@ -603,7 +611,7 @@ fn is_equal( #[cfg(test)] mod tests { use super::*; - use crate::context::context::Context; + 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}; @@ -666,8 +674,8 @@ mod tests { } } - // dummy runner for handlers that accept `run: &mut dyn FnMut(i64, &mut Context) -> Signal` - fn dummy_run(_: i64, _: &mut Context) -> Signal { + // 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)), }) @@ -677,7 +685,7 @@ mod tests { #[test] fn test_as_bytes_and_byte_size() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; // "hello" -> 5 bytes @@ -706,7 +714,7 @@ mod tests { #[test] fn test_case_ops_and_trim() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert_eq!( @@ -741,7 +749,7 @@ mod tests { #[test] fn test_chars_and_at() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; let chars_list = expect_list(chars(&[a_str("abc")], &mut ctx, &mut run)); @@ -769,7 +777,7 @@ mod tests { #[test] fn test_append_prepend_insert_length() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert_eq!( @@ -811,7 +819,7 @@ mod tests { #[test] fn test_remove_replace_variants() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); // remove uses CHAR indices [from, to) let mut run = dummy_run; @@ -857,7 +865,7 @@ mod tests { #[test] fn test_hex_octal_reverse() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert_eq!( @@ -877,7 +885,7 @@ mod tests { #[test] fn test_index_contains_split_starts_ends() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert_eq!( @@ -927,7 +935,7 @@ mod tests { #[test] fn test_to_ascii_and_from_ascii() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; let ascii_vals = expect_list(to_ascii(&[a_str("AB")], &mut ctx, &mut run)); @@ -951,7 +959,7 @@ mod tests { #[test] fn test_encode_decode_base64_and_is_equal() { - let mut ctx = Context::default(); + let mut ctx = ValueStore::default(); let mut run = dummy_run; assert_eq!( 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..bdec273 --- /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-CORE-000301".to_string(), + "Configuration".to_string(), + message, + ); + err.details.extend(details); + err + } + Error::State { message, details } => { + let mut err = RuntimeError::with_code( + "T-CORE-000302".to_string(), + "State".to_string(), + message, + ); + err.details.extend(details); + err + } + Error::Transport { + dependency, + message, + details, + } => { + let mut err = RuntimeError::with_code( + "T-CORE-000303".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-CORE-000304".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-CORE-000399".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-CORE-000303"); + 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..edafa7c --- /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 (examples: `T-STD-00001`, `T-CORE-000001`). + 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-CORE-999999", "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(_)) + ) + } +} diff --git a/crates/core/src/value.rs b/crates/taurus-core/src/value.rs similarity index 95% rename from crates/core/src/value.rs rename to crates/taurus-core/src/value.rs index 81ddf26..70ab9e3 100644 --- a/crates/core/src/value.rs +++ b/crates/taurus-core/src/value.rs @@ -1,3 +1,5 @@ +//! 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 { diff --git a/crates/manual/Cargo.toml b/crates/taurus-manual/Cargo.toml similarity index 86% rename from crates/manual/Cargo.toml rename to crates/taurus-manual/Cargo.toml index f4d78f2..799ab33 100644 --- a/crates/manual/Cargo.toml +++ b/crates/taurus-manual/Cargo.toml @@ -4,9 +4,9 @@ version.workspace = true edition.workspace = true [dependencies] -tests-core = { workspace = true } tucana = { workspace = true } taurus-core = { workspace = true } +taurus-provider = { workspace = true } log = { workspace = true } env_logger = { workspace = true } serde_json = { workspace = true } @@ -16,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 new file mode 100644 index 0000000..795f7e7 --- /dev/null +++ b/crates/taurus-manual/src/main.rs @@ -0,0 +1,196 @@ +use std::path::Path; + +use clap::{Parser, arg, command}; +use log::error; +use log::info; +use serde::Deserialize; +use taurus_core::runtime::engine::ExecutionEngine; +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; + +#[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) + } +} + +#[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 = 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), + Some(&emitter), + false, + ); + emitter.shutdown().await; + + match result { + Signal::Success(value) => { + let json = to_json_value(value); + let pretty = serde_json::to_string_pretty(&json).unwrap(); + println!("{}", pretty); + } + Signal::Return(value) => { + let json = to_json_value(value); + let pretty = serde_json::to_string_pretty(&json).unwrap(); + println!("{}", pretty); + } + Signal::Respond(value) => { + let json = to_json_value(value); + let pretty = serde_json::to_string_pretty(&json).unwrap(); + println!("{}", pretty); + } + Signal::Stop => println!("Received Stop signal"), + Signal::Failure(runtime_error) => { + println!("RuntimeError: {:?}", runtime_error); + } + } +} 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..7b75b6f --- /dev/null +++ b/crates/taurus-provider/src/providers/emitter/nats_emitter.rs @@ -0,0 +1,112 @@ +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, + worker_task: tokio::task::JoinHandle<()>, +} + +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. + 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); + + 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 + ); + } else { + log::info!( + "Published runtime emit message on '{}' (type={})", + topic, + emit_type_as_str(message.emit_type) + ); + } + } + }); + + 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); + } + } +} + +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/src/remote/mod.rs b/crates/taurus-provider/src/providers/remote/nats_remote_runtime.rs similarity index 61% rename from crates/taurus/src/remote/mod.rs rename to crates/taurus-provider/src/providers/remote/nats_remote_runtime.rs index 0651933..9ef694b 100644 --- a/crates/taurus/src/remote/mod.rs +++ b/crates/taurus-provider/src/providers/remote/nats_remote_runtime.rs @@ -1,32 +1,31 @@ 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 { +pub struct NATSRemoteRuntime { client: Client, } -impl RemoteNatsClient { +impl NATSRemoteRuntime { pub fn new(client: Client) -> Self { - RemoteNatsClient { client } + NATSRemoteRuntime { 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(); - log::info!("Publishing to topic: {}", topic); +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, @@ -35,7 +34,8 @@ impl RemoteRuntime for RemoteNatsClient { "RemoteRuntimeExeption: failed to handle NATS message: {}", err ); - return Err(RuntimeError::simple_str( + return Err(RuntimeError::new( + "T-PROV-000001", "RemoteRuntimeExeption", "Failed to receive any response messages from a remote runtime.", )); @@ -50,7 +50,8 @@ impl RemoteRuntime for RemoteNatsClient { "RemoteRuntimeExeption: failed to decode NATS message: {}", err ); - return Err(RuntimeError::simple_str( + return Err(RuntimeError::new( + "T-PROV-000002", "RemoteRuntimeExeption", "Failed to read Remote Response", )); @@ -61,16 +62,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-PROV-000003", "RemoteRuntimeExeption", "Result of Remote Response was empty.", )), 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/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 } diff --git a/crates/taurus/src/app/mod.rs b/crates/taurus/src/app/mod.rs new file mode 100644 index 0000000..0da58ca --- /dev/null +++ b/crates/taurus/src/app/mod.rs @@ -0,0 +1,226 @@ +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::emitter::nats_emitter::NATSRespondEmitter; +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 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; + + 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..7ee59b4 --- /dev/null +++ b/crates/taurus/src/app/worker.rs @@ -0,0 +1,134 @@ +use std::time::Instant; + +use futures_lite::StreamExt; +use prost::Message; +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::{ExecutionFlow, RuntimeUsage, Value}; + +use crate::client::runtime_usage::TaurusRuntimeUsageService; + +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 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, + &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, + 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) => { + log::error!( + "Failed to deserialize flow: {:?}, payload: {:?}", + err, + &message.payload + ); + return; + } + }; + + let flow_id = flow.flow_id; + // 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| { + runtime_emitter.emit(execution_id, emit_type, value); + }; + 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; + } +} + +fn execute_flow( + execution_id: ExecutionId, + flow: ExecutionFlow, + engine: &ExecutionEngine, + nats_remote: &NATSRemoteRuntime, + respond_emitter: Option<&dyn RespondEmitter>, +) -> RuntimeUsage { + let start = Instant::now(); + let flow_id = flow.flow_id; + 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; + + RuntimeUsage { + flow_id, + duration: duration_millis, + } +} + +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, + } +} 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 ea7932e..8821f47 100644 --- a/crates/taurus/src/main.rs +++ b/crates/taurus/src/main.rs @@ -1,319 +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::collections::HashMap; -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 tokio::signal; -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, -}; - -fn handle_message( - flow: ExecutionFlow, - store: &FunctionStore, - nats_remote: &RemoteNatsClient, -) -> (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 signal = Executor::new(store, node_functions) - .with_remote_runtime(nats_remote) - .execute(flow.starting_node_id, &mut context, true); - let duration_millis = start.elapsed().as_millis() as i64; - - ( - signal, - RuntimeUsage { - flow_id: flow.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 store = FunctionStore::default(); - 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 result = handle_message(flow, &store, &nats_client); - let value = match result.0 { - Signal::Failure(error) => { - log::error!( - "RuntimeError occurred, execution failed because: {:?}", - error - ); - error.as_value() - } - Signal::Success(v) => { - log::debug!("Execution ended on a success signal"); - v - } - Signal::Return(v) => { - log::debug!("Execution ended on a return signal"); - v - } - Signal::Respond(v) => { - log::debug!("Execution ended on a respond signal"); - v - } - Signal::Stop => { - log::debug!("Revied stop signal as last signal"); - Value { - kind: Some(Kind::NullValue(0)), - } - } - }; - - 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), - } - } - - if let Some(usage_service) = &runtime_usage_service { - usage_service.update_runtime_usage(result.1).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; } 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) - } -} 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); -} 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" } } ], 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" + } + } + } + ] + } + ] + } +} 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 }" + } +} 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 }" + } +}