Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions node-graph/libraries/core-types/src/poll.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@ impl std::fmt::Display for NodeError {
}
}

impl Error for NodeError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.error.source()
impl<E: Error + Send + Sync + 'static> From<E> for NodeError {
Comment thread
TrueDoctor marked this conversation as resolved.
Resolved
fn from(error: E) -> Self {
Self::new(error)
}
}

Expand Down Expand Up @@ -225,7 +225,10 @@ impl Status {
}
Poll::Fallback(boxed) => {
let (value, error) = *boxed;
self.fail(error);
match error.trace.is_empty() {
true => self.raise(error),
false => self.fail(error),
}
self.finish(value)
}
Poll::Pending => self.bail(Interrupt::Pending),
Expand Down Expand Up @@ -373,8 +376,8 @@ mod tests {
let parse_error = "x".parse::<u32>().unwrap_err();
let Poll::Error(error) = add(Poll::error(parse_error), Poll::Final(2)) else { unreachable!() };
assert_eq!(error.trace(), [0]);
let error: Box<dyn Error + Send + Sync> = error;
assert_eq!(error.to_string(), "invalid digit found in string");
assert!(error.error().is::<std::num::ParseIntError>());
}

#[test]
Expand All @@ -394,6 +397,27 @@ mod tests {
assert_eq!(upstream.trace(), [0]);
}

#[test]
fn a_merged_own_fallback_carries_the_input_error_upstream() {
let cell = Status::new();
cell.record(0, Poll::fallback(1, "input")).unwrap();
let Poll::Fallback(boxed) = cell.merge(Poll::fallback(2, "own")) else { unreachable!() };
let (value, error) = *boxed;
assert_eq!(value, 2);
assert_eq!(error.to_string(), "own");
let [upstream] = error.upstream() else { unreachable!() };
assert_eq!(upstream.to_string(), "input");

let cell = Status::new();
let mut passed_on = NodeError::new("passed on");
passed_on.track(1);
cell.record(0, Poll::fallback(1, "input")).unwrap();
let Poll::Fallback(boxed) = cell.merge(Poll::Fallback(Box::new((2, passed_on)))) else {
unreachable!()
};
assert_eq!(boxed.1.to_string(), "input", "an input's fallback is passed on, not raised");
}

#[test]
fn a_failing_node_carries_the_input_error_upstream() {
let Poll::Error(error) = Status::new().run(|cell| -> Result<u32, Interrupt> {
Expand All @@ -413,6 +437,13 @@ mod tests {
assert!(error.upstream().is_empty(), "an input's error is passed on, not raised");
}

#[test]
fn a_std_error_converts_into_a_node_error() {
let error = NodeError::from("x".parse::<u32>().unwrap_err());
assert_eq!(error.to_string(), "invalid digit found in string");
assert!(error.trace().is_empty());
}

#[test]
fn an_unrecorded_input_does_not_touch_the_status() {
let switch = |take_first: bool| {
Expand Down
32 changes: 25 additions & 7 deletions node-graph/node-macro/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -552,30 +552,32 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn

let lift = |call: TokenStream2| match tier {
NodeOutputTier::Plain | NodeOutputTier::Poll => quote!(Ok(#call)),
NodeOutputTier::Interrupt => call,
NodeOutputTier::Error => quote!(Ok(#call.map_err(#core_types::NodeError::from)?)),
NodeOutputTier::Fallback => quote! {
match #call {
Ok(value) => Ok(value),
Err((value, error)) => {
__cell.raise(error);
__cell.raise(#core_types::NodeError::from(error));
Ok(value)
}
}
},
NodeOutputTier::Interrupt => call,
};

let lift_slot = |call: TokenStream2| match tier {
NodeOutputTier::Plain | NodeOutputTier::Poll => call,
NodeOutputTier::Interrupt => quote!(#call?),
NodeOutputTier::Error => quote!(#call.map_err(#core_types::NodeError::from)?),
NodeOutputTier::Fallback => quote! {
match #call {
Ok(value) => value,
Err((value, error)) => {
__cell.raise(error);
__cell.raise(#core_types::NodeError::from(error));
value
}
}
},
NodeOutputTier::Interrupt => quote!(#call?),
};

let fold = match tier {
Expand All @@ -590,13 +592,31 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn

let kernel_call = lift(quote!(self::#fn_name(__input.clone() #(, &self.#data_field_names)* #(, #regular_field_names)*)));

let landed = |call: TokenStream2| match tier {
NodeOutputTier::Plain => quote!(#core_types::Poll::Final(#call)),
NodeOutputTier::Error => quote! {
match #call {
Ok(value) => #core_types::Poll::Final(value),
Err(error) => #core_types::Poll::Error(Box::new(#core_types::NodeError::from(error))),
}
},
NodeOutputTier::Fallback => quote! {
match #call {
Ok(value) => #core_types::Poll::Final(value),
Err((value, error)) => #core_types::Poll::Fallback(Box::new((value, #core_types::NodeError::from(error)))),
}
},
NodeOutputTier::Interrupt | NodeOutputTier::Poll => unreachable!("Validation rejects async nodes returning `Interrupt` or `Poll`"),
};

let eval_impl = if parsed.is_async {
let declared_field_names: Vec<_> = fields.iter().filter(|field| !field.is_data_field).map(|field| &field.pat_ident.ident).collect();
let placeholder = match &attributes.placeholder {
Some(path) => quote!(Some(#path(#(&#declared_field_names),*))),
None => quote!(None),
};
let data_clones = data_field_names.iter().map(|name| quote!(let #name = self.#name.clone();));
let landed_call = landed(quote!(self::#fn_name(__context #(, &#data_field_names)* #(, #regular_field_names)*).await));
quote! {
type Output = #output;
#[inline]
Expand Down Expand Up @@ -626,9 +646,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let __runtime = _runtime.element().clone();
let __context = __input.clone();
#(#data_clones)*
let __state = self.slots.resolve(__key, &__runtime, __source, __placeholder, move || async move {
#core_types::Poll::Final(self::#fn_name(__context #(, &#data_field_names)* #(, #regular_field_names)*).await)
});
let __state = self.slots.resolve(__key, &__runtime, __source, __placeholder, move || async move { #landed_call });
__cell.merge(__state)
}

Expand Down
12 changes: 9 additions & 3 deletions node-graph/node-macro/src/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1256,10 +1256,12 @@ impl ParsedNodeFn {
pub(crate) enum NodeOutputTier {
/// `-> T`
Plain,
/// `-> Result<T, E>` where `E: Into<NodeError>`
Error,
/// `-> Result<T, (T, E)>` where `E: Into<NodeError>`
Fallback,
/// `-> Result<T, Interrupt>`
Interrupt,
/// `-> Result<T, (T, NodeError)>`
Fallback,
/// `-> Poll<T>`
Poll,
}
Expand All @@ -1274,7 +1276,8 @@ pub(crate) fn output_tier(output_type: &Type) -> NodeOutputTier {
("Poll", 1) => NodeOutputTier::Poll,
("Result", 2) => match arguments.args.last() {
Some(syn::GenericArgument::Type(Type::Tuple(_))) => NodeOutputTier::Fallback,
_ => NodeOutputTier::Interrupt,
Some(syn::GenericArgument::Type(Type::Path(error))) if error.path.segments.last().is_some_and(|segment| segment.ident == "Interrupt") => NodeOutputTier::Interrupt,
_ => NodeOutputTier::Error,
},
_ => NodeOutputTier::Plain,
}
Expand Down Expand Up @@ -2098,8 +2101,11 @@ mod tests {
let payload: Type = parse_quote!(Item<u32>);
for (ty, tier) in [
(parse_quote!(Item<u32>), NodeOutputTier::Plain),
(parse_quote!(Result<Item<u32>, NodeError>), NodeOutputTier::Error),
(parse_quote!(Result<Item<u32>, std::num::ParseIntError>), NodeOutputTier::Error),
(parse_quote!(Result<Item<u32>, Interrupt>), NodeOutputTier::Interrupt),
(parse_quote!(Result<Item<u32>, (Item<u32>, NodeError)>), NodeOutputTier::Fallback),
(parse_quote!(Result<Item<u32>, (Item<u32>, std::num::ParseIntError)>), NodeOutputTier::Fallback),
(parse_quote!(Poll<Item<u32>>), NodeOutputTier::Poll),
] {
let ty: Type = ty;
Expand Down
25 changes: 17 additions & 8 deletions node-graph/node-macro/src/validation.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::parsing::{Implementation, NodeParsedField, ParsedFieldType, ParsedNodeFn, RegularParsedField, output_payload};
use proc_macro_error2::emit_error;
use crate::parsing::{Implementation, NodeOutputTier, NodeParsedField, ParsedFieldType, ParsedNodeFn, RegularParsedField, output_payload, output_tier};
use proc_macro_error2::{abort_if_dirty, emit_error};
use quote::quote;
use syn::spanned::Spanned;
use syn::{GenericParam, Type};
Expand All @@ -15,12 +15,15 @@ pub fn validate_node_fn(parsed: &ParsedNodeFn) -> syn::Result<()> {
validate_no_item_parameters,
validate_element_wise,
validate_ranked_inputs,
validate_async,
];

for validator in validators {
validator(parsed);
}

abort_if_dirty();

Ok(())
}

Expand Down Expand Up @@ -72,12 +75,6 @@ fn validate_no_item_parameters(parsed: &ParsedNodeFn) {
}

fn validate_element_wise(parsed: &ParsedNodeFn) {
if parsed.is_async && parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) {
emit_error!(parsed.fn_name.span(), "An async node cannot take `impl Node` inputs");
}
if parsed.attributes.placeholder.is_some() && !parsed.is_async {
emit_error!(parsed.fn_name.span(), "Only an async node can have a `placeholder`");
}
if parsed.attributes.skip_impl {
return;
}
Expand Down Expand Up @@ -451,6 +448,18 @@ fn validate_implementations_for_generics(parsed: &ParsedNodeFn) {
}
}

fn validate_async(parsed: &ParsedNodeFn) {
if parsed.is_async && matches!(output_tier(&parsed.output_type), NodeOutputTier::Interrupt | NodeOutputTier::Poll) {
emit_error!(parsed.output_type.span(), "An async node cannot return `Result<T, Interrupt>` or `Poll<T>`");
}
if parsed.is_async && parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) {
emit_error!(parsed.fn_name.span(), "An async node cannot take `impl Node` inputs");
}
if parsed.attributes.placeholder.is_some() && !parsed.is_async {
emit_error!(parsed.fn_name.span(), "Only an async node can have a `placeholder`");
}
}

fn validate_node_implementation(impl_: &Implementation, input_type: &Type, output_type: &Type, fn_generics: &[GenericParam]) {
if contains_generic_param(&impl_.input, fn_generics) || contains_generic_param(&impl_.output, fn_generics) {
emit_error!(
Expand Down
Loading