diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 0a23d0eeca..98f445a8a8 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -372,7 +372,7 @@ macro_rules! tagged_value { } $( if ty == core_types::registry::record_edge_type::<$ty>() { - let layout = handle.layout().ok_or_else(|| "a record edge must carry its layout".to_string())?.clone(); + let layout = handle.layout().clone(); return Ok(handle .downcast_record::<$ty>() .map_err(|e| format!("{e:?}"))? @@ -381,7 +381,7 @@ macro_rules! tagged_value { } )* if ty == core_types::registry::record_edge_type::() { - let layout = handle.layout().ok_or_else(|| "a record edge must carry its layout".to_string())?.clone(); + let layout = handle.layout().clone(); return Ok(handle .downcast_record::() .map_err(|e| format!("{e:?}"))? diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index b5544ccae0..a3ab97b554 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -128,7 +128,7 @@ impl ConstructionArgs { pub(crate) struct Resolved { pub io: Option, pub layout_meta: Option, - pub layout: Option, + pub layout: Option, } impl PartialEq for Resolved { @@ -221,7 +221,7 @@ impl ProtoNode { } } - pub fn resolved_layout(&self) -> Option<&core_types::record::Layout> { + pub fn resolved_layout(&self) -> Option<&core_types::record::RecordLayout> { self.resolved.layout.as_ref() } } @@ -372,10 +372,18 @@ impl ProtoNetwork { let layout = { let node = &self.nodes[index].1; match &node.construction_args { - ConstructionArgs::Value(value) => value.element_write().map(|element| core_types::record::Layout::default().with_writes(0, element, &[])), + ConstructionArgs::Value(value) => value.element_write().map(|element| { + let layout = core_types::record::Layout::default().with_writes(0, element, &[]); + core_types::record::RecordLayout { + frame_bytes: layout.frame_bytes(), + plan: Vec::new(), + layout, + } + }), ConstructionArgs::Nodes(inputs) => node.resolved.layout_meta.as_ref().and_then(|meta| { - let input_layouts: Vec> = inputs.iter().map(|input| self.nodes[input.0 as usize].1.resolved.layout.as_ref()).collect(); - meta.sources.iter().all(|&source| input_layouts[source as usize].is_some()).then(|| meta.fold(&input_layouts)) + let input_layouts: Vec> = + inputs.iter().map(|input| self.nodes[input.0 as usize].1.resolved.layout.as_ref().map(|resolved| &resolved.layout)).collect(); + meta.sources.iter().all(|&source| input_layouts[source as usize].is_some()).then(|| meta.resolve(&input_layouts)) }), ConstructionArgs::Inline(_) => None, } diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index fbdf5b45cc..62d176b85b 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -476,15 +476,16 @@ impl BorrowTree { .clone() .to_edge() .map_err(|error| vec![GraphError::new(&proto_node, GraphErrorType::ConstructionFailed(error))])?; - debug_assert!(proto_node.resolved_layout().map_or(true, |expected| node.layout() == Some(expected)), "layout pass disagrees with construction for {}", proto_node.identifier.as_str()); self.store_node(node, id, path.into()); } ConstructionArgs::Inline(_) => unimplemented!("Inline nodes are not supported yet"), ConstructionArgs::Nodes(ids) => { let construction_nodes = self.node_deps(ids); let constructor = typing_context.constructor(id).ok_or_else(|| vec![GraphError::new(&proto_node, GraphErrorType::NoConstructor)])?; - let node = constructor(construction_nodes).map_err(|error| vec![GraphError::new(&proto_node, GraphErrorType::ConstructionFailed(format!("{error:?}")))])?; - debug_assert!(proto_node.resolved_layout().map_or(true, |expected| node.layout() == Some(expected)), "layout pass disagrees with construction for {}", proto_node.identifier.as_str()); + let mut node = constructor(construction_nodes).map_err(|error| vec![GraphError::new(&proto_node, GraphErrorType::ConstructionFailed(format!("{error:?}")))])?; + if let Some(layout) = proto_node.resolved_layout() { + node.set_layout(layout.clone()); + } self.store_node(node, id, path.into()); } }; @@ -503,7 +504,7 @@ impl BorrowTree { } fn frame_bytes(&self, id: NodeId) -> usize { - self.nodes.get(&id).and_then(|(handle, _)| handle.layout()).map_or(0, |layout| layout.frame_bytes()) + self.nodes.get(&id).map_or(0, |(handle, _)| handle.layout().frame_bytes()) } } diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 3928b0d6ef..8441a696ab 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -191,9 +191,7 @@ mod node_registry_macros { } let mut inputs = inputs.into_iter(); let handle = inputs.next().unwrap(); - let Some(layout) = handle.layout().cloned() else { - return Err(ConstructionError::MissingLayout); - }; + let layout = handle.layout().clone(); let node = graphene_std::ops::IntoNode::<$to, _, $from>::new(handle.downcast_record::<$from>()?, &layout); Ok(EdgeHandle::new_record::<$to>(std::sync::Arc::new(node) as std::sync::Arc)) }, @@ -265,10 +263,8 @@ mod node_registry_macros { let mut inputs = inputs.into_iter(); let mut claim = || { let handle = inputs.next().unwrap(); - let Some(layout) = handle.layout().cloned() else { - return Err(ConstructionError::MissingLayout); - }; - Ok((handle, layout)) + let layout = handle.layout().clone(); + Ok::<_, ConstructionError>((handle, layout)) }; let (value, value_layout) = claim()?; let (converter, converter_layout) = claim()?; @@ -305,13 +301,9 @@ mod node_registry_macros { } let mut inputs = inputs.into_iter(); let value = inputs.next().unwrap(); - let Some(value_layout) = value.layout().cloned() else { - return Err(ConstructionError::MissingLayout); - }; + let value_layout = value.layout().clone(); let converter = inputs.next().unwrap(); - let Some(converter_layout) = converter.layout().cloned() else { - return Err(ConstructionError::MissingLayout); - }; + let converter_layout = converter.layout().clone(); let node = graphene_std::ops::ConvertNode::<$to, _, _, $from, $convert>::new( value.downcast_record::<$from>()?, converter.downcast_record::<$convert>()?, diff --git a/node-graph/libraries/core-types/src/node.rs b/node-graph/libraries/core-types/src/node.rs index 5a6f0be357..e3aae39356 100644 --- a/node-graph/libraries/core-types/src/node.rs +++ b/node-graph/libraries/core-types/src/node.rs @@ -278,6 +278,9 @@ pub trait Node { crate::record::empty_layout() } + /// Installs this node's resolved record layout; a no-op unless it produces records. + fn set_layout(&mut self, _layout: crate::record::RecordLayout) {} + fn eval_batch<'a>(&'a self, input: &'a Input, range: Range, scratch: Option<&'a mut [MaybeUninit]>) -> BatchStatus<'a, Self::Output> where Input: InjectIndex + Copy, diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index 896e2404b3..6bfc272079 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -243,6 +243,14 @@ pub fn empty_layout() -> &'static Layout { } /// Declarative record-io metadata for a node type, emitted by the macro into +/// A record node's output layout with the frame size and carrier copy plan derived from it. +#[derive(Clone, Debug, Default)] +pub struct RecordLayout { + pub layout: Layout, + pub frame_bytes: usize, + pub plan: Vec<(usize, usize, usize)>, +} + /// its registry entry so the compiler can fold each wire's layout without /// running the node's constructor. [`fold`](LayoutMeta::fold) reproduces the /// layout the constructor derives at wiring today; the compiler layout pass @@ -287,8 +295,7 @@ pub enum ElementSpec { } impl LayoutMeta { - /// The meta of an elementwise carrier flip that retypes input 0's element, - /// preserving its depth and attributes: what an `Into`/`Convert` coercion derives. + /// Keeps input 0's layout but replaces its element. pub fn retype(element: ElementWrite) -> Self { Self { sources: vec![0], @@ -318,6 +325,23 @@ impl LayoutMeta { }; base.with_writes(depth, element, &self.writes) } + + /// [`fold`](LayoutMeta::fold) with the frame size and carrier copy plan derived from it. + pub fn resolve(&self, inputs: &[Option<&Layout>]) -> RecordLayout { + let layout = self.fold(inputs); + let frame_bytes = layout.frame_bytes(); + let plan = match self.sources.first() { + // A reducer collapses its carrier's levels, so it writes a fresh record rather than copying fields down. + Some(&source) if self.level_delta >= 0 => { + let from = inputs[source as usize].expect("layout resolve source input has no layout"); + let carry_element = matches!(self.element, ElementSpec::Carried); + let removes: Vec<(&str, u8)> = self.removes.clone(); + copy_plan(from, &layout, carry_element, &removes) + } + _ => Vec::new(), + }; + RecordLayout { layout, frame_bytes, plan } + } } /// A view of one record: a pointer whose layout is proven at wiring. diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index 2e02bc5548..57206f83d9 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -113,7 +113,6 @@ pub fn cache_key(ctx: &C) -> u64 { pub enum ConstructionError { Arity { expected: usize, got: usize }, Type { expected: Box, found: Box }, - MissingLayout, } pub struct SharedEdge { @@ -193,7 +192,8 @@ pub struct EdgeHandle { node: Box, share: fn(&DynEdge) -> Box, serialize: fn(&DynEdge) -> Option>, - layout: fn(&DynEdge) -> Option<&crate::record::Layout>, + layout: fn(&DynEdge) -> &crate::record::Layout, + set_layout: fn(&mut DynEdge, crate::record::RecordLayout), ty: Type, } @@ -228,7 +228,12 @@ impl EdgeHandle { node: Box::new(SharedEdge::new(node)), share: |edge| Box::new(edge.downcast_ref::>().expect("share hook matches the stored edge type").share()), serialize: |edge| Node::::serialize(edge.downcast_ref::>().expect("serialize hook matches the stored edge type")), - layout: |edge| Some(Node::::layout(edge.downcast_ref::>().expect("layout hook matches the stored edge type"))), + layout: |edge| Node::::layout(edge.downcast_ref::>().expect("layout hook matches the stored edge type")), + set_layout: |edge, layout| { + let shared = edge.downcast_mut::>().expect("set_layout hook matches the stored edge type"); + let node = std::sync::Arc::get_mut(&mut shared.own).expect("layout is installed before the node is shared"); + Node::::set_layout(node, layout); + }, ty, } } @@ -243,6 +248,7 @@ impl EdgeHandle { share: self.share, serialize: self.serialize, layout: self.layout, + set_layout: self.set_layout, ty: self.ty.clone(), } } @@ -251,10 +257,14 @@ impl EdgeHandle { (self.serialize)(&*self.node) } - pub fn layout(&self) -> Option<&crate::record::Layout> { + pub fn layout(&self) -> &crate::record::Layout { (self.layout)(&*self.node) } + pub fn set_layout(&mut self, layout: crate::record::RecordLayout) { + (self.set_layout)(&mut *self.node, layout); + } + pub fn downcast(self) -> Result>, ConstructionError> { self.downcast_erased(edge_type::()) } diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 132ec44e0d..495fa26acf 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -403,19 +403,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }) .into_iter() .flatten(); - let flip_prelude = flip - .then(|| match carrier_flip { - true => quote! { - let __layout = __in_0.with_writes(__in_0.depth, gcore::record::element_write::<#slot_value_type>(), &[]); - let __plan = gcore::record::copy_plan(__in_0, &__layout, false, &[]); - let __frame_bytes = __layout.frame_bytes(); - }, - false => quote! { - let __layout = gcore::record::Layout::default().with_writes(0, gcore::record::element_write::<#slot_value_type>(), &[]); - let __frame_bytes = __layout.frame_bytes(); - }, - }) - .into_iter(); + // The output layout, frame size, and copy plan are installed by `set_layout`. let flip_read_bindings = flip .then(|| { lazy_read_fields(&struct_regular_fields).into_iter().map(|(index, field)| { @@ -441,15 +429,12 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn .flatten(); let flip_output_inits = flip .then(|| { - let plan = carrier_flip.then(|| quote!(__plan,)); - match flip_generic_idents.is_empty() { - true => quote!(__layout, __frame_bytes, #plan), - false => quote!(__layout, __frame_bytes, #plan __marker: ::core::marker::PhantomData,), - } + let plan = carrier_flip.then(|| quote!(__plan: ::std::vec::Vec::new(),)); + let marker = (!flip_generic_idents.is_empty()).then(|| quote!(__marker: ::core::marker::PhantomData,)); + quote!(__layout: ::core::default::Default::default(), __frame_bytes: 0, #plan #marker) }) .into_iter(); - // The flip prelude's `element_write` instantiates the erased glue at the - // output type, so `new` carries the bounds the glue needs. + // `new` carries the bounds the erased glue needs at the output type. let new_where = flip .then(|| { let existing = parsed.where_clause.iter().flat_map(|clause| clause.predicates.iter()); @@ -463,7 +448,6 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn { #[allow(clippy::too_many_arguments)] pub fn new(#(#new_args,)* #(#routing_layout_param)* #(#routing_in_params)* #(#flip_layout_params)*) -> Self { - #(#flip_prelude)* #(#flip_read_bindings)* Self { #(#all_field_inits,)* @@ -747,8 +731,6 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn crate::codegen::ir::Element::Concrete(ty) => Some(ty), _ => None, }); - // A creator pushes rank levels: its `IList` return raises the output depth - // above its carrier's, so the layout writes one level deeper. let subject_depth = node.inputs.iter().find(|input| input.subject).map_or(0, |input| input.shape.depth); let level_delta = node.output.shape.depth as i8 - subject_depth as i8; let pushed_levels = level_delta.max(0) as u8; @@ -1709,14 +1691,60 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn false => Vec::new(), }; + let set_layout_body = if flip { + let plan = carrier_flip.then(|| quote!(self.__plan = __resolved.plan;)); + Some(quote! { + self.__frame_bytes = __resolved.frame_bytes; + #plan + self.__layout = __resolved.layout; + }) + } else if record_io { + let write_installs = write_markers.iter().enumerate().map(|(index, marker)| { + let slot = format_ident!("__write_{index}"); + quote! { + self.#slot = __resolved.layout.offset_of(<#marker as #core_types::attribute::Attribute>::NAME, 0).expect("a written attribute is always part of the wired layout"); + } + }); + let plan = (!skips_carrier).then(|| quote!(self.__plan = __resolved.plan;)); + Some(quote! { + #(#write_installs)* + self.__frame_bytes = __resolved.frame_bytes; + #plan + self.__layout = __resolved.layout; + }) + } else { + None + }; + let set_layout_method = set_layout_body.map(|body| { + quote! { + fn set_layout(&mut self, __resolved: #core_types::record::RecordLayout) { + #body + } + } + }); let record_layout_impl = match record_io || routing_generic.is_some() || flip || opaque { true => quote! { fn layout(&self) -> &#core_types::record::Layout { &self.__layout } + #set_layout_method }, false => quote!(), }; + let flip_meta_concrete = flip && !element_write.is_some_and(|ty| crate::codegen::classify::contains_open_generic(parsed, ty)); + let flip_layout_meta_fn = flip_meta_concrete.then(|| { + let layout_meta_fn = format_ident!("{}_layout_meta", fn_name); + let element_spec = match element_write { + Some(ty) => quote!(#core_types::record::ElementSpec::Concrete(#core_types::record::element_write::<#ty>())), + None => quote!(#core_types::record::ElementSpec::Carried), + }; + let layout_meta = crate::codegen::ir::layout_meta_tokens(&node, element_spec, core_types); + quote! { + #vis fn #layout_meta_fn() -> #core_types::record::LayoutMeta { + #layout_meta + } + } + }); let entries = entries_tokens(parsed, &struct_name, &data_field_generic_idents, ®ular_fields); let cfg = crate::shader_nodes::modify_cfg(&parsed.attributes); @@ -1769,13 +1797,6 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let slot = format_ident!("__in_{index}"); quote!(#slot: &#core_types::record::Layout,) }); - let layout_binding = match skips_carrier { - true => quote!(let __layout = self::#layout_fn();), - false => quote!(let __layout = self::#layout_fn(__carrier_layout);), - }; - let carry_element = element_write.is_none(); - let plan_binding = - (!skips_carrier).then(|| quote!(let __plan = #core_types::record::copy_plan(__carrier_layout, &__layout, #carry_element, &[#(#remove_pairs),*]);)); let read_inits = flat_reads.iter().enumerate().map(|(slot, (owner, read))| { let marker = &read.marker; let slot = format_ident!("__read_{slot}"); @@ -1785,14 +1806,6 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }; quote!(let #slot = #source.offset_of(<#marker as #core_types::attribute::Attribute>::NAME, 0);) }); - let write_inits = write_markers.iter().enumerate().map(|(index, marker)| { - let slot = format_ident!("__write_{index}"); - quote! { - let #slot = __layout - .offset_of(<#marker as #core_types::attribute::Attribute>::NAME, 0) - .expect("a written attribute is always part of the wired layout"); - } - }); let data_inits = data_names.iter().map(|name| quote!(#name: ::core::default::Default::default(),)); let edge_inits = regular_fields.iter().map(|field| { let name = &field.pat_ident.ident; @@ -1803,9 +1816,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let slot = format_ident!("__in_{index}"); quote!(#slot: #slot.clone(),) }); - let plan_init = (!skips_carrier).then(|| quote!(__plan,)).into_iter(); + let plan_default = (!skips_carrier).then(|| quote!(__plan: ::std::vec::Vec::new(),)).into_iter(); let read_names = (0..flat_reads.len()).map(|index| format_ident!("__read_{index}")).map(|slot| quote!(#slot,)); - let write_names = (0..write_markers.len()).map(|index| format_ident!("__write_{index}")).map(|slot| quote!(#slot,)); + let write_defaults = (0..write_markers.len()).map(|index| format_ident!("__write_{index}")).map(|slot| quote!(#slot: 0,)); quote! { #layout_def #layout_meta_def @@ -1814,21 +1827,17 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn impl<#(#data_field_generic_idents,)* #(#node_generics,)*> #mod_name::#struct_name<#(#struct_type_params,)*> { #[allow(clippy::too_many_arguments)] #vis fn new(#(#edge_args,)* #(#carrier_layout_param)* #(#input_layout_params)*) -> Self { - #layout_binding - #plan_binding #(#read_inits)* - #(#write_inits)* - let __frame_bytes = __layout.frame_bytes(); Self { #(#data_inits)* #(#edge_inits)* #(#carrier_init)* #(#input_layout_inits)* - __layout, - #(#plan_init)* - __frame_bytes, + __layout: ::core::default::Default::default(), + #(#plan_default)* + __frame_bytes: 0, #(#read_names)* - #(#write_names)* + #(#write_defaults)* } } } @@ -1968,7 +1977,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn Ok(NodePlan { kernel, lazy_read_fns: quote!(#(#lazy_read_fns)*), - record_ctor: quote!(#record_wiring), + record_ctor: quote!(#record_wiring #flip_layout_meta_fn), node_impl: top_level, entries, ..Default::default() diff --git a/node-graph/node-macro/src/codegen/entries.rs b/node-graph/node-macro/src/codegen/entries.rs index 4935b01270..99253bb24d 100644 --- a/node-graph/node-macro/src/codegen/entries.rs +++ b/node-graph/node-macro/src/codegen/entries.rs @@ -132,9 +132,7 @@ fn flip_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_field let layout = format_ident!("__layout_{index}"); quote! { let #handle = inputs.next().unwrap(); - let Some(#layout) = #handle.layout().cloned() else { - return Err(gcore::registry::ConstructionError::MissingLayout); - }; + let #layout = #handle.layout().clone(); let #name = #handle.downcast_record::<#ty>()?; } }); @@ -261,16 +259,12 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields SlotKind::BaseGeneric(_) | SlotKind::BaseConcrete(_) => quote! { let #handle = inputs.next().unwrap(); let #ty = #handle.ty().clone(); - let Some(#layout) = #handle.layout().cloned() else { - return Err(gcore::registry::ConstructionError::MissingLayout); - }; + let #layout = #handle.layout().clone(); let #name = #handle.downcast_erased::(#ty.clone())?; }, SlotKind::Value(value_ty) => quote! { let #handle = inputs.next().unwrap(); - let Some(#layout) = #handle.layout().cloned() else { - return Err(gcore::registry::ConstructionError::MissingLayout); - }; + let #layout = #handle.layout().clone(); let #name = #handle.downcast_record::<#value_ty>()?; }, SlotKind::Plain(value_ty) | SlotKind::Lazy(value_ty) => quote!(let #name = inputs.next().unwrap().downcast::<#value_ty>()?;), diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index 51adb8dd0c..7b2a8a49d3 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -232,6 +232,21 @@ mod tests { stack::reserve(layouts.iter().map(|layout| layout.frame_bytes()).sum::().max(1 << 12)); } + fn install>>(mut node: N, meta: core_types::record::LayoutMeta, inputs: &[Option<&Layout>]) -> N { + >>::set_layout(&mut node, meta.resolve(inputs)); + node + } + + fn install_flip>>(mut node: N, layout: &Layout) -> N { + let bundle = core_types::record::RecordLayout { + frame_bytes: layout.frame_bytes(), + plan: Vec::new(), + layout: layout.clone(), + }; + >>::set_layout(&mut node, bundle); + node + } + fn lifted_value(value: T) -> (core_types::record::RecordLift>, Layout) { let lift = core_types::record::RecordLift::::new(ValueNode(value)); let layout = Node::::layout(&lift).clone(); @@ -268,7 +283,7 @@ mod tests { let leveled = repeat_opacity_layout(&base); reserve_for(&[&base, &leveled]); - let node = RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(3u32), &base); + let node = install(RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(3u32), &base), repeat_opacity_layout_meta(), &[Some(&base)]); assert_eq!(node.layout(), &leveled); let GPoll::Final(value) = node.eval(&indexed) else { panic!("expected a final record"); @@ -288,7 +303,7 @@ mod tests { let base = f64_layout(&[]); reserve_for(&[&base]); - let node = RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(3u32), &base); + let node = install(RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(3u32), &base), repeat_opacity_layout_meta(), &[Some(&base)]); // The pushed level (0, the only level) reports the copy count. assert_eq!(node.extent_at(&ctx, 0), core_types::gpoll::GPoll::Final(core_types::gpoll::Extent::Exactly(3))); } @@ -305,8 +320,8 @@ mod tests { let out = f64_layout(&[]); reserve_for(&[&base, &leveled, &out]); - let repeat = RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(3u32), &base); - let node = SumNode::new(repeat, &leveled); + let repeat = install(RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(3u32), &base), repeat_opacity_layout_meta(), &[Some(&base)]); + let node = install_flip(SumNode::new(repeat, &leveled), &out); assert_eq!(node.layout().depth, 0, "the reducer collapsed the rank level"); let GPoll::Final(value) = node.eval(&ctx) else { @@ -342,7 +357,15 @@ mod tests { let stacked = multiply_opacity_layout(&modified); reserve_for(&[&source_layout, &modified, &stacked]); - let chain = MultiplyOpacityNode::new(MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout), ValueNode(0.5), &modified); + let chain = install( + MultiplyOpacityNode::new( + install(MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout), multiply_opacity_layout_meta(), &[Some(&source_layout)]), + ValueNode(0.5), + &modified, + ), + multiply_opacity_layout_meta(), + &[Some(&modified)], + ); assert_eq!(chain.layout(), &stacked); let GPoll::Final(value) = chain.eval(&ctx) else { panic!("expected a final record"); @@ -363,7 +386,7 @@ mod tests { let measured = measure_layout(&source_layout); reserve_for(&[&source_layout, &measured]); - let chain = MeasureNode::new(bare_source(&source_layout, -2.), &source_layout); + let chain = install(MeasureNode::new(bare_source(&source_layout, -2.), &source_layout), measure_layout_meta(), &[Some(&source_layout)]); let GPoll::Final(value) = chain.eval(&ctx) else { panic!("expected a final record"); }; @@ -384,7 +407,14 @@ mod tests { let measured = measure_layout(&modified); reserve_for(&[&source_layout, &modified, &measured]); - let chain = MeasureNode::new(MultiplyOpacityNode::new(bare_source(&source_layout, -2.), ValueNode(0.5), &source_layout), &modified); + let chain = install( + MeasureNode::new( + install(MultiplyOpacityNode::new(bare_source(&source_layout, -2.), ValueNode(0.5), &source_layout), multiply_opacity_layout_meta(), &[Some(&source_layout)]), + &modified, + ), + measure_layout_meta(), + &[Some(&modified)], + ); let GPoll::Final(value) = chain.eval(&ctx) else { panic!("expected a final record"); }; @@ -405,13 +435,20 @@ mod tests { let shaded = shade_layout(&modified); reserve_for(&[&source_layout, &modified, &shaded]); - let bare = ShadeNode::new(bare_source(&source_layout, 4.), &source_layout); + let bare = install(ShadeNode::new(bare_source(&source_layout, 4.), &source_layout), shade_layout_meta(), &[Some(&source_layout)]); let GPoll::Final(value) = bare.eval(&ctx) else { panic!("expected a final record"); }; assert_eq!(unsafe { source_layout.rec(&value).element::() }, 4.); - let chain = ShadeNode::new(MultiplyOpacityNode::new(bare_source(&source_layout, 4.), ValueNode(0.5), &source_layout), &modified); + let chain = install( + ShadeNode::new( + install(MultiplyOpacityNode::new(bare_source(&source_layout, 4.), ValueNode(0.5), &source_layout), multiply_opacity_layout_meta(), &[Some(&source_layout)]), + &modified, + ), + shade_layout_meta(), + &[Some(&modified)], + ); let GPoll::Final(value) = chain.eval(&ctx) else { panic!("expected a final record"); }; @@ -433,7 +470,7 @@ mod tests { let u32_faded = fade_layout(&u32_source); reserve_for(&[&f64_source, &f64_faded, &u32_source, &u32_faded]); - let wide = FadeNode::new(bare_source(&f64_source, 8.), ValueNode(0.5), &f64_source); + let wide = install(FadeNode::new(bare_source(&f64_source, 8.), ValueNode(0.5), &f64_source), fade_layout_meta(), &[Some(&f64_source)]); let GPoll::Final(value) = wide.eval(&ctx) else { panic!("expected a final record"); }; @@ -441,15 +478,19 @@ mod tests { assert_eq!(unsafe { rec.element::() }, 8.); assert_eq!(unsafe { rec.read::(f64_faded.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5); - let narrow = FadeNode::new( - RecordSourceNode { - layout: u32_source.clone(), - element: 7u32, - fields: vec![], - partial: false, - }, - ValueNode(0.25), - &u32_source, + let narrow = install( + FadeNode::new( + RecordSourceNode { + layout: u32_source.clone(), + element: 7u32, + fields: vec![], + partial: false, + }, + ValueNode(0.25), + &u32_source, + ), + fade_layout_meta(), + &[Some(&u32_source)], ); let GPoll::Final(value) = narrow.eval(&ctx) else { panic!("expected a final record"); @@ -469,7 +510,7 @@ mod tests { let layout = source_opacity_layout(); reserve_for(&[&layout]); - let node = SourceOpacityNode::new(ValueNode(3.), ValueNode(0.25)); + let node = install(SourceOpacityNode::new(ValueNode(3.), ValueNode(0.25)), source_opacity_layout_meta(), &[]); assert_eq!(Node::::layout(&node), &layout); let GPoll::Final(value) = node.eval(&ctx) else { panic!("expected a final record"); @@ -490,15 +531,19 @@ mod tests { let modified = multiply_opacity_layout(&source_layout); reserve_for(&[&source_layout, &modified]); - let chain = MultiplyOpacityNode::new( - RecordSourceNode { - layout: source_layout.clone(), - element: 1., - fields: vec![], - partial: true, - }, - ValueNode(0.5), - &source_layout, + let chain = install( + MultiplyOpacityNode::new( + RecordSourceNode { + layout: source_layout.clone(), + element: 1., + fields: vec![], + partial: true, + }, + ValueNode(0.5), + &source_layout, + ), + multiply_opacity_layout_meta(), + &[Some(&source_layout)], ); let GPoll::Partial(value) = chain.eval(&ctx) else { panic!("expected a partial record"); @@ -517,13 +562,13 @@ mod tests { let modified = checked_multiply_opacity_layout(&source_layout); reserve_for(&[&source_layout, &modified]); - let ok = CheckedMultiplyOpacityNode::new(bare_source(&source_layout, 1.), ValueNode(0.5), &source_layout); + let ok = install(CheckedMultiplyOpacityNode::new(bare_source(&source_layout, 1.), ValueNode(0.5), &source_layout), checked_multiply_opacity_layout_meta(), &[Some(&source_layout)]); let GPoll::Final(value) = ok.eval(&ctx) else { panic!("expected a final record"); }; assert_eq!(unsafe { modified.rec(&value).read::(modified.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5); - let failing = CheckedMultiplyOpacityNode::new(bare_source(&source_layout, 1.), ValueNode(-1.), &source_layout); + let failing = install(CheckedMultiplyOpacityNode::new(bare_source(&source_layout, 1.), ValueNode(-1.), &source_layout), checked_multiply_opacity_layout_meta(), &[Some(&source_layout)]); let GPoll::Error(error) = failing.eval(&ctx) else { panic!("expected an error"); }; @@ -542,7 +587,15 @@ mod tests { let scaled = scale_layout(&modified); reserve_for(&[&source_layout, &modified, &scaled]); - let chain = ScaleNode::new(MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout), ValueNode(3.), &modified); + let chain = install( + ScaleNode::new( + install(MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout), multiply_opacity_layout_meta(), &[Some(&source_layout)]), + ValueNode(3.), + &modified, + ), + scale_layout_meta(), + &[Some(&modified)], + ); let GPoll::Final(value) = chain.eval(&ctx) else { panic!("expected a final record"); }; @@ -563,11 +616,15 @@ mod tests { let transferred = transfer_opacity_layout(&carrier_layout); reserve_for(&[&carrier_layout, &secondary_layout, &transferred]); - let chain = TransferOpacityNode::new( - f64_record_source(&carrier_layout, 2., vec![(carrier_layout.offset_of("opacity", 0).unwrap(), 0.5)]), - f64_record_source(&secondary_layout, 3., vec![(secondary_layout.offset_of("opacity", 0).unwrap(), 0.25)]), - &carrier_layout, - &secondary_layout, + let chain = install( + TransferOpacityNode::new( + f64_record_source(&carrier_layout, 2., vec![(carrier_layout.offset_of("opacity", 0).unwrap(), 0.5)]), + f64_record_source(&secondary_layout, 3., vec![(secondary_layout.offset_of("opacity", 0).unwrap(), 0.25)]), + &carrier_layout, + &secondary_layout, + ), + transfer_opacity_layout_meta(), + &[Some(&carrier_layout), None], ); let GPoll::Final(value) = chain.eval(&ctx) else { panic!("expected a final record"); @@ -577,11 +634,15 @@ mod tests { assert_eq!(unsafe { rec.read::(transferred.offset_of(Opacity::NAME, 0).unwrap()) }, 0.125); let bare_secondary = f64_layout(&[]); - let defaulted = TransferOpacityNode::new( - f64_record_source(&carrier_layout, 2., vec![(carrier_layout.offset_of("opacity", 0).unwrap(), 0.5)]), - bare_source(&bare_secondary, 3.), - &carrier_layout, - &bare_secondary, + let defaulted = install( + TransferOpacityNode::new( + f64_record_source(&carrier_layout, 2., vec![(carrier_layout.offset_of("opacity", 0).unwrap(), 0.5)]), + bare_source(&bare_secondary, 3.), + &carrier_layout, + &bare_secondary, + ), + transfer_opacity_layout_meta(), + &[Some(&carrier_layout), None], ); let GPoll::Final(value) = defaulted.eval(&ctx) else { panic!("expected a final record"); @@ -602,11 +663,15 @@ mod tests { let factor_layout = Node::::layout(&factor).clone(); reserve_for(&[&source_layout]); - let node = BoostNode::new( - f64_record_source(&source_layout, 2., vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25)]), - factor, - &source_layout, - &factor_layout, + let node = install( + BoostNode::new( + f64_record_source(&source_layout, 2., vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25)]), + factor, + &source_layout, + &factor_layout, + ), + boost_layout_meta(), + &[Some(&source_layout)], ); let out_layout = Node::::layout(&node).clone(); let opacity_offset = out_layout.offset_of(Opacity::NAME, 0).expect("the primary input's fields pass through to the output"); @@ -629,11 +694,15 @@ mod tests { let (factor, factor_layout) = lifted_value(3.); reserve_for(&[&source_layout]); - let node = BoostPollNode::new( - f64_record_source(&source_layout, 2., vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25)]), - factor, - &source_layout, - &factor_layout, + let node = install( + BoostPollNode::new( + f64_record_source(&source_layout, 2., vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25)]), + factor, + &source_layout, + &factor_layout, + ), + boost_poll_layout_meta(), + &[Some(&source_layout)], ); let out_layout = Node::::layout(&node).clone(); let opacity_offset = out_layout.offset_of(Opacity::NAME, 0).expect("the primary input's fields pass through the poll kernel"); @@ -657,11 +726,15 @@ mod tests { assert!(by_layout.frame_bytes() != 0, "the borrow must point into a spilled frame to exercise the park"); reserve_for(&[&carrier_layout, &by_layout]); - let node = OffsetNode::new( - f64_record_source(&carrier_layout, 2., vec![(carrier_layout.offset_of("opacity", 0).unwrap(), 0.25)]), - f64_record_source(&by_layout, 40., vec![]), - &carrier_layout, - &by_layout, + let node = install( + OffsetNode::new( + f64_record_source(&carrier_layout, 2., vec![(carrier_layout.offset_of("opacity", 0).unwrap(), 0.25)]), + f64_record_source(&by_layout, 40., vec![]), + &carrier_layout, + &by_layout, + ), + offset_layout_meta(), + &[Some(&carrier_layout)], ); let out_layout = Node::::layout(&node).clone(); let GPoll::Final(value) = node.eval(&ctx) else { @@ -694,13 +767,17 @@ mod tests { let (source_id, source_id_layout) = lifted_value(7 as SourceId); reserve_for(&[&source_layout]); - let node = DoubleAsyncNode::new( - f64_record_source(&source_layout, 3., vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25)]), - runtime, - source_id, - &source_layout, - &runtime_layout, - &source_id_layout, + let node = install( + DoubleAsyncNode::new( + f64_record_source(&source_layout, 3., vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25)]), + runtime, + source_id, + &source_layout, + &runtime_layout, + &source_id_layout, + ), + double_async_layout_meta(), + &[Some(&source_layout)], ); let out_layout = Node::::layout(&node).clone(); let opacity_offset = out_layout.offset_of(Opacity::NAME, 0).expect("the carrier's fields pass through the async source"); @@ -774,9 +851,20 @@ mod tests { let shaded = shade_layout(&stripped); reserve_for(&[&source_layout, &modified, &stripped, &shaded]); - let chain = ShadeNode::new( - StripOpacityNode::new(MultiplyOpacityNode::new(bare_source(&source_layout, 4.), ValueNode(0.5), &source_layout), &modified), - &stripped, + let chain = install( + ShadeNode::new( + install( + StripOpacityNode::new( + install(MultiplyOpacityNode::new(bare_source(&source_layout, 4.), ValueNode(0.5), &source_layout), multiply_opacity_layout_meta(), &[Some(&source_layout)]), + &modified, + ), + strip_opacity_layout_meta(), + &[Some(&modified)], + ), + &stripped, + ), + shade_layout_meta(), + &[Some(&stripped)], ); let GPoll::Final(value) = chain.eval(&ctx) else { panic!("expected a final record"); @@ -796,13 +884,17 @@ mod tests { assert!(relengthed.offset_of(Opacity::NAME, 0).is_none()); reserve_for(&[&source_layout, &relengthed]); - let chain = RelengthNode::new( - f64_record_source( + let chain = install( + RelengthNode::new( + f64_record_source( + &source_layout, + 3., + vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25), (source_layout.offset_of("length", 0).unwrap(), 9.)], + ), &source_layout, - 3., - vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25), (source_layout.offset_of("length", 0).unwrap(), 9.)], ), - &source_layout, + relength_layout_meta(), + &[Some(&source_layout)], ); let GPoll::Final(value) = chain.eval(&ctx) else { panic!("expected a final record"); @@ -824,10 +916,14 @@ mod tests { let relabeled = label_layout(&labeled); reserve_for(&[&source_layout, &labeled, &relabeled]); - let chain = LabelNode::new( - LabelNode::new(bare_source(&source_layout, 1.), ValueNode(String::from("a")), &source_layout), - ValueNode(String::from("b")), - &labeled, + let chain = install( + LabelNode::new( + install(LabelNode::new(bare_source(&source_layout, 1.), ValueNode(String::from("a")), &source_layout), label_layout_meta(), &[Some(&source_layout)]), + ValueNode(String::from("b")), + &labeled, + ), + label_layout_meta(), + &[Some(&labeled)], ); let GPoll::Final(value) = chain.eval(&ctx) else { panic!("expected a final record"); @@ -1187,7 +1283,7 @@ mod tests { let labeled = label_layout(&source_layout); reserve_for(&[&labeled, &labeled]); - let chain = LabelNode::new(bare_source(&source_layout, 1.), ValueNode(String::from("a")), &source_layout); + let chain = install(LabelNode::new(bare_source(&source_layout, 1.), ValueNode(String::from("a")), &source_layout), label_layout_meta(), &[Some(&source_layout)]); let memo = crate::memo::MemoizeNode::new(chain, &labeled); let first_arena = Arena::new(1024).unwrap();