diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 7b8e13358a2..deb2acf4978 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -3830,10 +3830,7 @@ impl NodeGraphMessageHandler { .evaluation_errors .iter() .filter(|error| inside(&error.node_path)) - .map(|error| match error.node_path.len() == depth + 1 { - true => (error.node_path[depth], error.message.clone()), - false => (error.node_path[depth], "Node graph error within this node".to_string()), - }) + .map(|error| (error.node_path[depth], error.message.clone())) .collect(), false => graph_errors, }; diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 60edc692c13..1638a140c83 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -48,12 +48,28 @@ pub struct ExecutionResponse { pub enum ExecutionResult { Pending, Value(TaggedValue), - Fallback(TaggedValue, EvaluationError), - Error(EvaluationError), + Fallback(TaggedValue, EvaluationErrors), + Error(EvaluationErrors), Panic(Box), } -pub type EvaluationError = Box<[(Arc, Box<[NodeId]>)]>; +#[derive(Debug)] +pub struct EvaluationErrors(Box<[(Arc, NodeErrorTrace)]>); +pub type NodeErrorTrace = Box<[NodePaths]>; +pub type NodePaths = Box<[Box<[NodeId]>]>; + +impl EvaluationErrors { + fn iter(&self) -> impl Iterator { + self.0.iter().map(|(error, trace)| (&**error, trace)) + } +} + +impl std::fmt::Display for EvaluationErrors { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let messages: Vec<_> = self.iter().map(|(error, _)| error.to_string()).collect(); + write!(f, "{}", messages.join("\n")) + } +} #[derive(serde::Serialize, serde::Deserialize)] pub struct CompilationResponse { @@ -462,13 +478,24 @@ impl NodeGraphExecutor { // A pending graph keeps the last frame, and the errors shown with it if execution_context.export_config.is_none() && !matches!(result, ExecutionResult::Pending) { let errors = match &result { - ExecutionResult::Fallback(_, errors) | ExecutionResult::Error(errors) => errors - .iter() - .map(|(error, node_path)| NodeEvaluationError { - node_path: node_path.to_vec(), - message: error.to_string(), - }) - .collect(), + ExecutionResult::Fallback(_, errors) | ExecutionResult::Error(errors) => { + let in_document = |path: &&[NodeId]| path.split_last().is_some_and(|(node, network)| document.network_interface.document_node(node, network).is_some()); + errors + .iter() + .flat_map(|(error, trace)| { + let paths = trace + .iter() + .rev() + .map(|paths| paths.iter().map(|path| &**path).filter(in_document).collect::>()) + .find(|paths| !paths.is_empty()) + .unwrap_or_default(); + paths.into_iter().map(|node_path| NodeEvaluationError { + node_path: node_path.to_vec(), + message: error.to_string(), + }) + }) + .collect() + } ExecutionResult::Pending | ExecutionResult::Value(_) | ExecutionResult::Panic(_) => Vec::new(), }; responses.add(NodeGraphMessage::UpdateEvaluationErrors { errors }); @@ -477,7 +504,7 @@ impl NodeGraphExecutor { let node_graph_output = match result { ExecutionResult::Pending => Ok(None), ExecutionResult::Value(value) | ExecutionResult::Fallback(value, _) => Ok(Some(value)), - ExecutionResult::Error(errors) => Err(errors.iter().map(|(error, _)| error.to_string()).collect::>().join("\n")), + ExecutionResult::Error(errors) => Err(errors.to_string()), ExecutionResult::Panic(payload) => { let message = payload.downcast_ref::<&str>().copied().or_else(|| payload.downcast_ref::().map(String::as_str)); Err(format!("Panicked: {}", message.unwrap_or("no message"))) diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 3c68ab724cc..63aec1d2e51 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -285,7 +285,7 @@ impl NodeRuntime { ExecutionResult::Fallback(value, self.resolve_errors(&error)) } Ok(Ok(Poll::Error(error))) => ExecutionResult::Error(self.resolve_errors(&error)), - Ok(Err(error)) => ExecutionResult::Error(Box::new([(Arc::clone(NodeError::new(error).error()), Box::default())])), + Ok(Err(error)) => ExecutionResult::Error(EvaluationErrors(Box::new([(Arc::clone(NodeError::new(error).error()), NodeErrorTrace::default())]))), Err(payload) => { let response = ExecutionResponse { execution_id, @@ -450,23 +450,24 @@ impl NodeRuntime { } } - fn resolve_errors(&self, error: &NodeError) -> EvaluationError { + fn resolve_errors(&self, error: &NodeError) -> EvaluationErrors { let source_map = self.executor.tree().source_map(); + let paths = |node: NodeId| -> NodePaths { + let mut paths: Vec<_> = source_map.iter().filter(|(_, (id, _))| *id == node).map(|(path, _)| path.clone()).collect(); + paths.sort_unstable(); + paths.into() + }; let mut errors = Vec::new(); - let mut pending = vec![(Some(self.executor.output()), error)]; - while let Some((node, error)) = pending.pop() { - let origin = node.and_then(|node| self.executor.find_origin(node, error)); - let start = errors.len(); - if let Some(origin) = origin { - errors.extend(source_map.iter().filter(|(_, (node, _))| *node == origin).map(|(path, _)| (Arc::clone(error.error()), path.clone()))); - errors[start..].sort_unstable_by(|(_, a), (_, b)| a.cmp(b)); - } - if errors.len() == start { - errors.push((Arc::clone(error.error()), Box::default())); - } - pending.extend(error.upstream().iter().rev().map(|upstream| (origin, upstream))); + let output = self.executor.output(); + let mut pending = vec![(vec![paths(output)], output, error)]; + while let Some((mut trace, node, error)) = pending.pop() { + let passed = self.executor.trace_error(node, error); + let origin = passed.last().copied().unwrap_or(node); + trace.extend(passed.into_iter().map(&paths)); + pending.extend(error.upstream().iter().rev().map(|upstream| (trace.clone(), origin, upstream))); + errors.push((Arc::clone(error.error()), trace.into())); } - errors.into_boxed_slice() + EvaluationErrors(errors.into()) } /// Updates state data, giving back the layers whose thumbnails were dropped because they're no longer monitored. diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index e976d56a495..67baf301733 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -144,7 +144,8 @@ impl DynamicExecutor { // Only really needed due to the runtime not knowing about rank adapters. // Once we reworked that we should be able to move this resolution into the runtime. // Also remember to remove `BorrowTree::nodes.3` at that point - pub fn find_origin(&self, mut node: NodeId, error: &NodeError) -> Option { + pub fn trace_error(&self, mut node: NodeId, error: &NodeError) -> Box<[NodeId]> { + let mut passed = Vec::new(); let mut trace = error.trace().iter().rev(); while let Some(&index) = trace.next() { // Hack to undo rank promotion while traversing the graph @@ -152,9 +153,13 @@ impl DynamicExecutor { trace.next(); } - node = *self.tree.nodes.get(&node).map(|(_, _, inputs)| &**inputs)?.get(index)?; + let Some(&input) = self.tree.nodes.get(&node).and_then(|(_, _, inputs)| inputs.get(index)) else { + break; + }; + node = input; + passed.push(node); } - Some(node) + passed.into_boxed_slice() } } diff --git a/node-graph/interpreted-executor/src/dynamic_executor/test.rs b/node-graph/interpreted-executor/src/dynamic_executor/test.rs index 580d34ac816..4e2c27f7289 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor/test.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor/test.rs @@ -877,11 +877,14 @@ fn error_traced_through(indices: &[usize]) -> NodeError { fn a_trace_leads_to_the_node_that_raised_the_error() { let executor = DynamicExecutor::new(bounding_box_network(TaggedValue::TypeDefault(item!(Vector)))).unwrap(); - assert_eq!(executor.find_origin(executor.output(), &error_traced_through(&[])), Some(NodeId(1))); - assert_eq!(executor.find_origin(executor.output(), &error_traced_through(&[0])), Some(NodeId(0))); - assert_eq!(executor.find_origin(executor.output(), &error_traced_through(&[0, 0])), None, "a value node has no inputs"); - assert_eq!(executor.find_origin(NodeId(0), &error_traced_through(&[])), Some(NodeId(0))); - assert_eq!(executor.find_origin(NodeId(0), &error_traced_through(&[0])), None, "an upstream trace starts at its parent's origin"); + assert!(executor.trace_error(executor.output(), &error_traced_through(&[])).is_empty()); + assert_eq!(*executor.trace_error(executor.output(), &error_traced_through(&[0])), [NodeId(0)]); + assert_eq!(*executor.trace_error(executor.output(), &error_traced_through(&[0, 0])), [NodeId(0)], "a value node has no inputs"); + assert!(executor.trace_error(NodeId(0), &error_traced_through(&[])).is_empty()); + assert!( + executor.trace_error(NodeId(0), &error_traced_through(&[0])).is_empty(), + "an upstream trace starts at its parent's origin" + ); } #[test] @@ -896,7 +899,7 @@ fn a_trace_steps_over_the_index_of_a_promotion_adapter() { }) .unwrap(); - assert_eq!(executor.find_origin(executor.output(), &error_traced_through(&[0, 0])), Some(NodeId(0))); + assert_eq!(*executor.trace_error(executor.output(), &error_traced_through(&[0, 0])), [NodeId(0)]); } #[test] @@ -923,16 +926,14 @@ fn executor_panics_reach_the_host_with_the_original_payload() { #[test] fn chained_fallbacks_both_reach_the_root() { - let value_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Number(1.).into()), vec![NodeId(0)]); - let fail_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Bool(true).into()), vec![NodeId(1)]); - let mut first = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0), NodeId(1)]), vec![NodeId(2)]); - first.identifier = graphene_std::poll_debug::fallback::IDENTIFIER; - let mut second = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(2), NodeId(1)]), vec![NodeId(3)]); - second.identifier = graphene_std::poll_debug::fallback::IDENTIFIER; let executor = DynamicExecutor::new(ProtoNetwork { inputs: vec![], - output: NodeId(3), - nodes: vec![(NodeId(0), value_node), (NodeId(1), fail_node), (NodeId(2), first), (NodeId(3), second)], + output: NodeId(4), + nodes: [ + throw_error_inputs(), + vec![(NodeId(3), throw_error_node([0, 1, 2, 1], 3)), (NodeId(4), throw_error_node([3, 1, 2, 1], 4))], + ] + .concat(), }) .unwrap(); @@ -941,25 +942,27 @@ fn chained_fallbacks_both_reach_the_root() { panic!("Both nodes fall back, so the root should too") }; let error = &boxed.1; - assert_eq!(executor.find_origin(executor.output(), error), Some(NodeId(3))); + assert!(executor.trace_error(executor.output(), error).is_empty()); let [upstream] = error.upstream() else { panic!("The second node's error should carry the first node's error") }; - assert_eq!(executor.find_origin(NodeId(3), upstream), Some(NodeId(2))); + assert_eq!(*executor.trace_error(NodeId(4), upstream), [NodeId(3)]); } #[test] fn an_error_after_a_fallback_keeps_both() { - let value_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Number(1.).into()), vec![NodeId(0)]); - let fail_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Bool(true).into()), vec![NodeId(1)]); - let mut fallback = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0), NodeId(1)]), vec![NodeId(2)]); - fallback.identifier = graphene_std::poll_debug::fallback::IDENTIFIER; - let mut error = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(2), NodeId(1)]), vec![NodeId(3)]); - error.identifier = graphene_std::poll_debug::error::IDENTIFIER; let executor = DynamicExecutor::new(ProtoNetwork { inputs: vec![], - output: NodeId(3), - nodes: vec![(NodeId(0), value_node), (NodeId(1), fail_node), (NodeId(2), fallback), (NodeId(3), error)], + output: NodeId(5), + nodes: [ + throw_error_inputs(), + vec![ + (NodeId(3), throw_error_node([0, 1, 2, 1], 3)), + value_node(4, TaggedValue::Bool(false)), + (NodeId(5), throw_error_node([3, 1, 2, 4], 5)), + ], + ] + .concat(), }) .unwrap(); @@ -967,34 +970,36 @@ fn an_error_after_a_fallback_keeps_both() { let Ok(Poll::Error(error)) = (&executor).execute(context) else { panic!("The failing node should stop the frame") }; - assert_eq!(executor.find_origin(executor.output(), &error), Some(NodeId(3))); + assert!(executor.trace_error(executor.output(), &error).is_empty()); let [upstream] = error.upstream() else { panic!("The failing node's error should carry the fallback before it") }; - assert_eq!(executor.find_origin(NodeId(3), upstream), Some(NodeId(2))); + assert_eq!(*executor.trace_error(NodeId(5), upstream), [NodeId(3)]); } #[test] fn a_node_error_names_the_node_that_raised_it() { - let value_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Number(1.).into()), vec![NodeId(0)]); - let fail_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Bool(true).into()), vec![NodeId(1)]); - let mut error_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0), NodeId(1)]), vec![NodeId(2)]); - error_node.identifier = graphene_std::poll_debug::error::IDENTIFIER; - let mut count_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(2)]), vec![NodeId(3)]); - count_node.identifier = graphene_std::poll_debug::evaluation_count::IDENTIFIER; let executor = DynamicExecutor::new(ProtoNetwork { inputs: vec![], - output: NodeId(3), - nodes: vec![(NodeId(0), value_node), (NodeId(1), fail_node), (NodeId(2), error_node), (NodeId(3), count_node)], + output: NodeId(5), + nodes: [ + throw_error_inputs(), + vec![ + value_node(3, TaggedValue::Bool(false)), + (NodeId(4), throw_error_node([0, 1, 2, 3], 4)), + (NodeId(5), throw_error_node([4, 3, 2, 3], 5)), + ], + ] + .concat(), }) - .expect("The debug nodes should resolve"); + .expect("The Throw Error nodes should resolve"); let context: Context = None; let Ok(Poll::Error(error)) = (&executor).execute(context) else { panic!("The failing node should stop the frame") }; - let origin = executor.find_origin(executor.output(), &error); - assert_eq!(origin, Some(NodeId(2)), "The error should name the failing node, not the root that forwarded it"); + let origin = executor.trace_error(executor.output(), &error).last().copied(); + assert_eq!(origin, Some(NodeId(4)), "The error should name the failing node, not the root that forwarded it"); let paths: Vec<_> = executor .tree() .source_map() @@ -1002,7 +1007,78 @@ fn a_node_error_names_the_node_that_raised_it() { .filter(|(_, (node, _))| Some(*node) == origin) .map(|(path, _)| path.to_vec()) .collect(); - assert_eq!(paths, [vec![NodeId(2)]]); + assert_eq!(paths, [vec![NodeId(4)]]); +} + +fn value_node(id: u64, value: TaggedValue) -> (NodeId, ProtoNode) { + (NodeId(id), ProtoNode::value(ConstructionArgs::Value(value.into()), vec![NodeId(id)])) +} + +fn throw_error_inputs() -> Vec<(NodeId, ProtoNode)> { + vec![ + value_node(0, TaggedValue::Number(1.)), + value_node(1, TaggedValue::Bool(true)), + value_node(2, TaggedValue::String("too small".to_string())), + ] +} + +fn throw_error_node(inputs: [u64; 4], id: u64) -> ProtoNode { + let mut node = ProtoNode::value(ConstructionArgs::Nodes(inputs.into_iter().map(NodeId).collect()), vec![NodeId(id)]); + node.identifier = graphene_core::poll::throw_error::IDENTIFIER; + node +} + +fn handled_error_network(handler: ProtoNodeIdentifier, handler_inputs: &[u64], keep_value: bool) -> DynamicExecutor { + let mut handler_node = ProtoNode::value(ConstructionArgs::Nodes(handler_inputs.iter().copied().map(NodeId).collect()), vec![NodeId(6)]); + handler_node.identifier = handler; + + DynamicExecutor::new(ProtoNetwork { + inputs: vec![], + output: NodeId(6), + nodes: [ + throw_error_inputs(), + vec![ + value_node(3, TaggedValue::Bool(keep_value)), + (NodeId(4), throw_error_node([0, 1, 2, 3], 4)), + value_node(5, TaggedValue::Number(2.)), + (NodeId(6), handler_node), + ], + ] + .concat(), + }) + .expect("The error handling nodes should resolve") +} + +#[test] +fn a_caught_error_does_not_reach_the_output() { + let context: Context = None; + for keep_value in [false, true] { + let executor = handled_error_network(graphene_core::poll::catch::IDENTIFIER, &[4, 5], keep_value); + assert!(matches!((&executor).execute(context.clone()), Ok(Poll::Final(_))), "Catch should hide the error"); + } +} + +#[test] +fn suppress_error_hides_only_an_error_with_a_value() { + let context: Context = None; + let executor = handled_error_network(graphene_core::poll::suppress_error::IDENTIFIER, &[4], true); + assert!( + matches!((&executor).execute(context.clone()), Ok(Poll::Final(_))), + "Suppress Error should keep the value and hide the error" + ); + let executor = handled_error_network(graphene_core::poll::suppress_error::IDENTIFIER, &[4], false); + assert!(matches!((&executor).execute(context), Ok(Poll::Error(_))), "An error without a value should still stop the frame"); +} + +#[test] +fn a_fallback_keeps_the_error_on_the_node_that_raised_it() { + let executor = handled_error_network(graphene_core::poll::fallback::IDENTIFIER, &[4, 5], false); + let context: Context = None; + let Ok(Poll::Fallback(boxed)) = (&executor).execute(context) else { + panic!("Fallback should keep the frame going with the error") + }; + assert_eq!(boxed.1.to_string(), "too small"); + assert_eq!(*executor.trace_error(executor.output(), &boxed.1), [NodeId(4)], "The error should stay on the Throw Error node"); } #[test] diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index b0b98513787..1031c295c56 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -996,7 +996,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote! { #(#description_doc_attrs)* #[inline] - #[allow(clippy::too_many_arguments)] + #[allow(clippy::too_many_arguments, clippy::type_complexity)] #vis #async_keyword fn #fn_name <#(#fn_generics,)*> (#input_ident: #input_type #(, #data_field_idents: #data_field_types)* #(, #field_idents: #field_types)*) -> #output_type #where_clause #body } }; diff --git a/node-graph/nodes/gcore/src/lib.rs b/node-graph/nodes/gcore/src/lib.rs index 5f74dd3cd2c..31bd99246b9 100644 --- a/node-graph/nodes/gcore/src/lib.rs +++ b/node-graph/nodes/gcore/src/lib.rs @@ -5,6 +5,7 @@ pub mod debug; pub mod extract_xy; pub mod memo; pub mod ops; +pub mod poll; // Re-export all nodes pub use animation::*; @@ -14,3 +15,4 @@ pub use debug::*; pub use extract_xy::*; pub use memo::*; pub use ops::*; +pub use poll::*; diff --git a/node-graph/nodes/gcore/src/poll.rs b/node-graph/nodes/gcore/src/poll.rs new file mode 100644 index 00000000000..d21b77c16f7 --- /dev/null +++ b/node-graph/nodes/gcore/src/poll.rs @@ -0,0 +1,417 @@ +use core_types::context::{CloneVarArgs, ExtractAll}; +use core_types::list::{Bundle, Item}; +use core_types::{Color, Context, Ctx, OwnedContextImpl, Poll}; +use glam::{DAffine2, DVec2}; +use graphic_types::vector_types::Gradient; +use graphic_types::{Artboard, Graphic, Vector}; +use raster_types::{CPU, GPU, Raster}; + +/// Shows the placeholder until the content has finished loading. +#[node_macro::node(category("Routing"))] +fn placeholder( + ctx: impl Ctx + CloneVarArgs + ExtractAll, + #[expose] + #[implementations( + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item>, + Context -> Item>, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>>, + Context -> Item>>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + )] + content: impl Node, Output = T>, + #[implementations( + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item>, + Context -> Item>, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>>, + Context -> Item>>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + )] + placeholder: impl Node, Output = T>, +) -> Poll { + let ctx = OwnedContextImpl::from(ctx).into_context(); + + match content.eval(&ctx) { + Poll::Pending | Poll::Partial(_) => match placeholder.eval(&ctx) { + Poll::Final(value) | Poll::Partial(value) => Poll::Partial(value), + Poll::Fallback(boxed) => Poll::Partial(boxed.0), + state => state, + }, + state => state, + } +} + +/// Keeps rendering with the fallback where the content failed, and still shows the error. +#[node_macro::node(category("Routing"))] +fn fallback( + ctx: impl Ctx + CloneVarArgs + ExtractAll, + #[expose] + #[implementations( + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item>, + Context -> Item>, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>>, + Context -> Item>>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + )] + content: impl Node, Output = T>, + #[implementations( + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item>, + Context -> Item>, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>>, + Context -> Item>>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + )] + fallback: impl Node, Output = T>, +) -> Poll { + let ctx = OwnedContextImpl::from(ctx).into_context(); + + let error = match content.eval(&ctx) { + Poll::Fallback(boxed) => boxed.1, + Poll::Error(error) => *error, + state => return state, + }; + match fallback.eval(&ctx) { + Poll::Final(value) => Poll::Fallback(Box::new((value, error))), + Poll::Fallback(boxed) => Poll::Fallback(Box::new((boxed.0, error))), + state => state, + } +} + +/// Uses the alternative where the content failed, and hides the error. +#[node_macro::node(category("Routing"))] +fn catch( + ctx: impl Ctx + CloneVarArgs + ExtractAll, + #[expose] + #[implementations( + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item>, + Context -> Item>, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>>, + Context -> Item>>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + )] + content: impl Node, Output = T>, + #[implementations( + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item>, + Context -> Item>, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>>, + Context -> Item>>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + )] + alternative: impl Node, Output = T>, +) -> Poll { + let ctx = OwnedContextImpl::from(ctx).into_context(); + + match content.eval(&ctx) { + Poll::Fallback(_) | Poll::Error(_) => alternative.eval(&ctx), + state => state, + } +} + +/// Keeps the value a failed node still gave, and hides its error. A failure without a value still stops everything that depends on it. +#[node_macro::node(category("Routing"))] +fn suppress_error( + ctx: impl Ctx + CloneVarArgs + ExtractAll, + #[expose] + #[implementations( + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item>, + Context -> Item>, + Context -> Item, + Context -> Item, + Context -> Item, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + Context -> Item>>, + Context -> Item>>, + Context -> Item>, + Context -> Item>, + Context -> Item>, + )] + content: impl Node, Output = T>, +) -> Poll { + match content.eval(&OwnedContextImpl::from(ctx).into_context()) { + Poll::Fallback(boxed) => Poll::Final(boxed.0), + state => state, + } +} + +/// Raises an error with the message while the condition is true, so a custom node can reject input it can't work with. +#[node_macro::node(category("Routing"))] +fn throw_error( + _: impl Ctx, + #[implementations( + Item, + Item, + Item, + Item, + Item, + Item, + Item, + Item, + Item>, + Item>, + Item, + Item, + Item, + Item>, + Item>, + Item>, + Item>, + Item>, + Item>, + Item>, + Item>, + Item>>, + Item>>, + Item>, + Item>, + Item>, + )] + value: T, + condition: Item, + #[default("Error")] message: Item, + keep_value: Item, +) -> Poll { + if !*condition.element() { + return Poll::Final(value); + } + + let message = message.into_element(); + if *keep_value.element() { Poll::fallback(value, message) } else { Poll::error(message) } +} + +#[cfg(test)] +mod test { + use super::*; + use core_types::Node; + + struct Fixed(Poll>); + + impl Node> for Fixed { + type Output = Item; + fn eval(&self, _: &Context<'static>) -> Poll> { + self.0.clone() + } + } + + fn item(value: f64) -> Item { + Item::new_from_element(value) + } + + fn eval>>(node: N) -> Poll { + node.eval(&Context::default()) + } + + fn error_of(state: Poll) -> (String, Vec) { + let error = match state { + Poll::Fallback(boxed) => boxed.1, + Poll::Error(error) => *error, + _ => panic!("Expected an error"), + }; + (error.to_string(), error.trace().to_vec()) + } + + #[test] + fn placeholder_replaces_only_unfinished_content() { + let placeholder = |content| eval(PlaceholderNode::new(Fixed(content), Fixed(Poll::Final(item(2.))))); + assert_eq!(placeholder(Poll::Pending), Poll::Partial(item(2.))); + assert_eq!(placeholder(Poll::Partial(item(1.))), Poll::Partial(item(2.))); + assert_eq!(placeholder(Poll::Final(item(1.))), Poll::Final(item(1.))); + assert_eq!(error_of(placeholder(Poll::error("failed"))), ("failed".to_string(), vec![0])); + } + + #[test] + fn fallback_keeps_the_content_error() { + let fallback = |content| eval(FallbackNode::new(Fixed(content), Fixed(Poll::Final(item(2.))))); + assert_eq!(fallback(Poll::Final(item(1.))), Poll::Final(item(1.))); + assert_eq!(fallback(Poll::Pending), Poll::Pending); + + let Poll::Fallback(boxed) = fallback(Poll::error("failed")) else { + panic!("A failure should fall back") + }; + assert_eq!(boxed.0, item(2.)); + assert_eq!(error_of(Poll::Fallback(boxed)), ("failed".to_string(), vec![0])); + + let Poll::Fallback(boxed) = fallback(Poll::fallback(item(1.), "own stand-in")) else { + panic!("A failure should fall back") + }; + assert_eq!(boxed.0, item(2.), "The fallback replaces the failed node's own stand-in"); + assert_eq!(error_of(Poll::Fallback(boxed)), ("own stand-in".to_string(), vec![0])); + + let provisional = eval(FallbackNode::new(Fixed(Poll::error("failed")), Fixed(Poll::Partial(item(2.))))); + assert_eq!(provisional, Poll::Partial(item(2.)), "An error with a provisional value is held back"); + } + + #[test] + fn catch_uses_the_alternative_where_the_content_failed() { + let catch = |content| eval(CatchNode::new(Fixed(content), Fixed(Poll::Final(item(2.))))); + assert_eq!(catch(Poll::Final(item(1.))), Poll::Final(item(1.))); + assert_eq!(catch(Poll::Partial(item(1.))), Poll::Partial(item(1.))); + assert_eq!(catch(Poll::Pending), Poll::Pending); + assert_eq!(catch(Poll::fallback(item(1.), "stand-in")), Poll::Final(item(2.))); + assert_eq!(catch(Poll::error("failed")), Poll::Final(item(2.))); + + let failing_alternative = eval(CatchNode::new(Fixed(Poll::error("first")), Fixed(Poll::error("second")))); + assert_eq!(error_of(failing_alternative), ("second".to_string(), vec![1])); + } + + #[test] + fn suppress_error_hides_only_an_error_with_a_value() { + let suppress = |content| eval(SuppressErrorNode::new(Fixed(content))); + assert_eq!(suppress(Poll::fallback(item(1.), "stand-in")), Poll::Final(item(1.))); + assert_eq!(suppress(Poll::Partial(item(1.))), Poll::Partial(item(1.))); + assert_eq!(error_of(suppress(Poll::error("failed"))), ("failed".to_string(), vec![0])); + } + + #[test] + fn throw_error_stops_or_keeps_the_value() { + let message = || Item::new_from_element("too small".to_string()); + let flag = Item::new_from_element; + + assert_eq!(throw_error((), item(1.), flag(false), message(), flag(false)), Poll::Final(item(1.))); + assert_eq!(error_of(throw_error((), item(1.), flag(true), message(), flag(false))), ("too small".to_string(), vec![])); + let Poll::Fallback(boxed) = throw_error((), item(1.), flag(true), message(), flag(true)) else { + panic!("Keeping the value should fall back") + }; + assert_eq!(boxed.0, item(1.)); + assert_eq!(boxed.1.to_string(), "too small"); + } +} diff --git a/node-graph/nodes/gstd/src/poll_debug.rs b/node-graph/nodes/gstd/src/poll_debug.rs index cd40cb3c3d9..f6958b2b544 100644 --- a/node-graph/nodes/gstd/src/poll_debug.rs +++ b/node-graph/nodes/gstd/src/poll_debug.rs @@ -1,27 +1,9 @@ +use core_types::Ctx; use core_types::list::Item; -use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractGenerations, Interrupt, NodeError, OwnedContextImpl, Poll}; use graphic_types::{Graphic, Vector}; -use std::sync::Arc; -use std::sync::atomic::{AtomicU64, Ordering}; - -#[node_macro::node(category("Debug"), placeholder = pending_delay)] -async fn delay( - _: impl Ctx, - #[implementations(f64, String, Vector, Graphic)] value: Item, - #[default(1.)] - #[unit(" sec")] - seconds: Item, -) -> Item { - sleep(*seconds.element()).await; - value -} - -fn pending_delay(_value: &Item, _seconds: &Item) -> Item { - Item::default() -} #[node_macro::node(category("Debug"))] -async fn hold( +async fn delay( _: impl Ctx, #[implementations(f64, String, Vector, Graphic)] value: Item, #[default(1.)] @@ -32,63 +14,17 @@ async fn hold( value } -#[node_macro::node(category("Debug"))] -fn error(_: impl Ctx, #[implementations(f64, String, Vector, Graphic)] value: Item, #[default(true)] fail: Item) -> Result, Interrupt> { - match *fail.element() { - true => Err(NodeError::new("debug error").into()), - false => Ok(value), - } -} - -#[node_macro::node(category("Debug"))] -fn fallback(_: impl Ctx, #[implementations(f64, String, Vector, Graphic)] value: Item, #[default(true)] fail: Item) -> Result, (Item, NodeError)> { - match *fail.element() { - true => Err((Item::default(), NodeError::new("debug fallback"))), - false => Ok(value), - } -} - #[node_macro::node(category("Debug"), name("Panic"))] fn unwind(_: impl Ctx, #[implementations(f64, String, Vector, Graphic)] value: Item, fail: Item) -> Item { if *fail.element() { - panic!("debug panic"); + panic!("panic"); } value } -#[node_macro::node(category("Debug"))] -fn evaluation_count(_: impl Ctx, #[implementations(f64, String, Vector, Graphic)] value: Item, #[data] count: Arc) -> Item { - let count = count.fetch_add(1, Ordering::Relaxed) + 1; - log::info!("Debug evaluation count: {count}"); - value -} - -#[node_macro::node(category("Debug"))] -fn generations(ctx: impl Ctx + ExtractGenerations, value: Item) -> Item { - let generations = ctx - .try_generations() - .map(|generations| generations.iter().map(|(source, generation)| format!("{:x}={generation}", source.0)).collect::>().join(", ")) - .unwrap_or_default(); - Item::new_from_element(format!("{} [{generations}]", value.element())) -} - -#[node_macro::node(category("Debug"), name("Stand-In"))] -fn stand_in( - ctx: impl Ctx + ExtractAll + CloneVarArgs, - #[expose] - #[implementations(Context -> Item, Context -> Item, Context -> Item, Context -> Item)] - content: impl Node, Output = T>, - #[implementations(Item, Item, Item, Item)] placeholder: T, -) -> Poll { - match content.eval(&OwnedContextImpl::from(ctx).into_context()) { - Poll::Pending | Poll::Partial(_) => Poll::Partial(placeholder), - finished => finished, - } -} - #[cfg(not(target_family = "wasm"))] async fn sleep(seconds: f64) { - use std::sync::Mutex; + use std::sync::{Arc, Mutex}; use std::task::{Poll, Waker}; #[derive(Default)] @@ -109,7 +45,7 @@ async fn sleep(seconds: f64) { let timer = Timer::default(); let state = Arc::clone(&timer.0); std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_secs_f64(seconds.max(0.))); + std::thread::sleep(std::time::Duration::try_from_secs_f64(seconds.max(0.)).unwrap_or(std::time::Duration::MAX)); let mut state = state.lock().unwrap(); state.0 = true; if let Some(waker) = state.1.take() {