diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index ef57e79894..859624daf0 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -590,15 +590,16 @@ mod test { } #[test] - fn a_flipped_ref_parameter_gets_its_producer_lend_spliced() { + fn a_flipped_ref_parameter_reads_the_borrow_from_its_record_wire() { let raster_list = TaggedValue::from_type(&core_types::concrete!(graphene_std::list::List>)).unwrap(); let network = ProtoNetwork { inputs: vec![], - output: NodeId(2), + output: NodeId(3), nodes: vec![ (NodeId(0), ProtoNode::value(ConstructionArgs::Value(raster_list.into()), vec![])), (NodeId(1), ProtoNode::value(ConstructionArgs::Value(TaggedValue::U32(4).into()), vec![])), (NodeId(2), proto_node("raster_nodes::image_color_palette::ImageColorPaletteNode", vec![NodeId(0), NodeId(1)])), + (NodeId(3), proto_node("core_types::record::RecordExtractNode", vec![NodeId(2)])), ], }; @@ -607,8 +608,8 @@ mod test { let generations = []; let scope = EvalScope::new(None, None, None, &generations, &arena); let ctx = ContextImpl::root(&scope); - let result: Option>> = executor.tree().eval(NodeId(2), &ctx); - assert!(matches!(result, Some(GPoll::Final(_))), "the palette must evaluate through the spliced lend, got {result:?}"); + let result: Option>> = executor.tree().eval(NodeId(3), &ctx); + assert!(matches!(result, Some(GPoll::Final(_))), "the palette must evaluate through its record wires, got {result:?}"); } #[test] diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 88ad4e15e3..784ede6a75 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -787,7 +787,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } }) }); - let introduced_lend_lifetime = (has_lend && declared_arena_lifetime.is_none()).then(|| Lifetime::new("'__lend", proc_macro2::Span::call_site())); + let introduced_lend_lifetime = (has_lend && !flip && declared_arena_lifetime.is_none()).then(|| Lifetime::new("'__lend", proc_macro2::Span::call_site())); let lend_lifetime = declared_arena_lifetime.or_else(|| introduced_lend_lifetime.clone()); if let Some(lifetime) = &introduced_lend_lifetime { ctx_bounds.push(quote!(#core_types::context::ExtractArena)); @@ -961,6 +961,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let record_value_ty: Type = syn::parse_quote!(#core_types::record::RecordValue<'__record>); let node_bounds = regular_fields.iter().enumerate().zip(&node_generics).map(|((index, field), node_generic)| match &field.ty { + ParsedFieldType::Regular(_) if flip => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>), ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => { let lifetime = lend_lifetime.as_ref().expect("lend fields imply the lend lifetime"); quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = &#lifetime #ty>) @@ -971,7 +972,6 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn ParsedFieldType::Regular(RegularParsedField { ty, .. }) if routing_source(ty) => { quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>) } - ParsedFieldType::Regular(_) if flip => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>), ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #ty>), ParsedFieldType::Node(NodeParsedField { output_type, .. }) if routing_source(output_type) => match derives { true => quote!(#node_generic: for<'__derived> #core_types::record::DerivedRecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>), @@ -989,7 +989,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let mut lend_outlives: Vec = regular_fields .iter() .filter_map(|field| match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => { + ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) if !flip => { let lifetime = lend_lifetime.as_ref().expect("lend fields imply the lend lifetime"); Some(quote!(#ty: #lifetime)) } @@ -1037,6 +1037,17 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let name = &field.pat_ident.ident; match &field.ty { ParsedFieldType::Regular(_) if record.is_some() && !skips_carrier && index == 0 => quote!(), + ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) if flip => { + let slot = format_ident!("__in_{index}"); + let record_local = format_ident!("__record_{index}"); + quote! { + let #record_local = match __cell.eval_input(#index, &self.#name, __input) { + Ok(value) => value, + Err(interrupt) => return interrupt.into(), + }; + let #name: &#ty = unsafe { #core_types::record::borrow_element(self.#slot.rec(&#record_local)) }; + } + } ParsedFieldType::Regular(RegularParsedField { ty, .. }) if flip => { let slot = format_ident!("__in_{index}"); quote! { @@ -1436,6 +1447,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let mut bounds: Vec = regular_fields .iter() .filter_map(|field| match &field.ty { + ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. }) => None, ParsedFieldType::Regular(RegularParsedField { ty, .. }) => Some(quote!(#ty: ::core::clone::Clone)), _ => None, }) @@ -1813,7 +1825,7 @@ pub(crate) fn record_flip(parsed: &ParsedNodeFn) -> bool { if parsed.is_async || is_source_kernel(&parsed.output_type) { return false; } - if parsed.attributes.batch.is_some() || parsed.attributes.shader_node.is_some() { + if parsed.attributes.batch.is_some() || parsed.attributes.shader_node.is_some() || parsed.attributes.plain { return false; } if matches!(kernel_kind(&parsed.output_type), KernelKind::Poll(_)) { @@ -1842,10 +1854,7 @@ pub(crate) fn record_flip(parsed: &ParsedNodeFn) -> bool { GenericParam::Lifetime(_) | GenericParam::Const(_) => return false, } } - parsed.fields.iter().all(|field| match &field.ty { - ParsedFieldType::Regular(RegularParsedField { lend, .. }) => lend.is_none(), - ParsedFieldType::Node(_) => false, - }) + parsed.fields.iter().all(|field| matches!(&field.ty, ParsedFieldType::Regular(_))) } pub(crate) fn routing_io(parsed: &ParsedNodeFn) -> Option { diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index 5eaca857de..36c3a6996d 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -113,6 +113,8 @@ pub(crate) struct NodeFnAttributes { pub(crate) batch: Option, /// Whether partial upstream values are mapped to `Pending` instead of flowing into this node pub(crate) no_partial: bool, + /// Whether this node keeps the plain-wire lowering during the record transition + pub(crate) plain: bool, } #[derive(Clone, Debug, Default)] @@ -375,6 +377,7 @@ impl Parse for NodeFnAttributes { let mut extent = None; let mut batch = None; let mut no_partial = false; + let mut plain = false; let content = input; // let content; @@ -560,6 +563,13 @@ impl Parse for NodeFnAttributes { // // Example usage: // #[node_macro::node(..., no_partial, ...)] + "plain" => { + let path = meta.require_path_only()?; + if plain { + return Err(Error::new_spanned(path, "Multiple 'plain' attributes are not allowed")); + } + plain = true; + } "no_partial" => { let path = meta.require_path_only()?; if no_partial { @@ -611,6 +621,7 @@ impl Parse for NodeFnAttributes { extent, batch, no_partial, + plain, }) } } @@ -1278,6 +1289,7 @@ mod tests { extent: None, batch: None, no_partial: false, + plain: false, }, fn_name: Ident::new("add", Span::call_site()), struct_name: Ident::new("Add", Span::call_site()), @@ -1354,6 +1366,7 @@ mod tests { extent: None, batch: None, no_partial: false, + plain: false, }, fn_name: Ident::new("transform", Span::call_site()), struct_name: Ident::new("Transform", Span::call_site()), @@ -1444,6 +1457,7 @@ mod tests { extent: None, batch: None, no_partial: false, + plain: false, }, fn_name: Ident::new("circle", Span::call_site()), struct_name: Ident::new("Circle", Span::call_site()), @@ -1516,6 +1530,7 @@ mod tests { extent: None, batch: None, no_partial: false, + plain: false, }, fn_name: Ident::new("levels", Span::call_site()), struct_name: Ident::new("Levels", Span::call_site()), @@ -1600,6 +1615,7 @@ mod tests { extent: None, batch: None, no_partial: false, + plain: false, }, fn_name: Ident::new("add", Span::call_site()), struct_name: Ident::new("Add", Span::call_site()), @@ -1687,6 +1703,7 @@ mod tests { extent: None, batch: None, no_partial: false, + plain: false, }, fn_name: Ident::new("load_image", Span::call_site()), struct_name: Ident::new("LoadImage", Span::call_site()), @@ -1759,6 +1776,7 @@ mod tests { extent: None, batch: None, no_partial: false, + plain: false, }, fn_name: Ident::new("custom_node", Span::call_site()), struct_name: Ident::new("CustomNode", Span::call_site()), diff --git a/node-graph/nodes/gcore/src/debug.rs b/node-graph/nodes/gcore/src/debug.rs index fbbdf6639a..4df880a47a 100644 --- a/node-graph/nodes/gcore/src/debug.rs +++ b/node-graph/nodes/gcore/src/debug.rs @@ -29,8 +29,9 @@ fn unwrap_option(_: impl Ctx, #[implementations(Option, Option< input.unwrap_or_default() } -/// Clones the value borrowed from a lending edge. Doubles as the checker-inserted clone-out adapter. -#[node_macro::node(category("Debug"))] +/// Clones the value borrowed from a lending edge. Doubles as the checker-inserted clone-out +/// adapter, so it keeps the plain lowering while the record transition runs. +#[node_macro::node(category("Debug"), plain)] fn clone(_: impl Ctx, #[implementations(List>)] value: &T) -> T { value.clone() }