diff --git a/node-graph/libraries/core-types/src/extent.rs b/node-graph/libraries/core-types/src/extent.rs index e00830e629..39b2117b9e 100644 --- a/node-graph/libraries/core-types/src/extent.rs +++ b/node-graph/libraries/core-types/src/extent.rs @@ -42,6 +42,23 @@ impl<'a> ExtentIn<'a> { } } +/// A ranked (`IList`) input materialized whole: `get` drives the batch and +/// yields the level as a [`List`](crate::node::List), for extents that depend +/// on the input's data rather than its counts alone. +pub struct ListIn<'a, T> { + get: &'a dyn Fn() -> GPoll>, +} + +impl<'a, T> ListIn<'a, T> { + pub fn new(get: &'a dyn Fn() -> GPoll>) -> Self { + Self { get } + } + + pub fn get(&self) -> GPoll> { + (self.get)() + } +} + /// The queried absolute level (innermost `0`), paired with the node's depth. #[derive(Clone, Copy, Debug)] pub struct LevelIn { diff --git a/node-graph/libraries/core-types/src/node.rs b/node-graph/libraries/core-types/src/node.rs index 2623349eef..3c289cbd64 100644 --- a/node-graph/libraries/core-types/src/node.rs +++ b/node-graph/libraries/core-types/src/node.rs @@ -177,7 +177,7 @@ pub struct List<'a, T> { _element: PhantomData, } -impl<'a, T: Copy> List<'a, T> { +impl<'a, T> List<'a, T> { /// # Safety /// `T` must be the batch's record element type, proven at the consumer's wiring. pub unsafe fn new(batch: RecordBatch<'a>) -> Self { @@ -192,12 +192,30 @@ impl<'a, T: Copy> List<'a, T> { self.batch.is_empty() } - pub fn get(&self, index: usize) -> T { + pub fn get(&self, index: usize) -> T + where + T: Copy, + { // SAFETY: `List::new` established that `T` is the batch's element type. unsafe { self.batch.get(index).element::() } } - pub fn iter(&self) -> impl Iterator + '_ { + /// Borrows lane `index`'s element, through the park for droppable types. + pub fn element_ref(&self, index: usize) -> &T { + // SAFETY: `List::new` established that `T` is the batch's element type, + // and the borrow lives within the batch's own lifetime. + unsafe { crate::record::borrow_element::(self.batch.get(index).rec()) } + } + + /// Lane `index`'s record, for attribute reads beside the element. + pub fn lane(&self, index: usize) -> RecordLane<'a> { + self.batch.get(index) + } + + pub fn iter(&self) -> impl Iterator + '_ + where + T: Copy, + { (0..self.len()).map(move |index| self.get(index)) } } diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 068cb36576..a3aa6af8c9 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -978,6 +978,9 @@ 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 { + // A ranked input rides a record edge whatever the node kind; the + // materialized batch reads its lanes. + ParsedFieldType::Regular(_) if ir::materialized_levels(&node, index) > 0 => 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::Node(_) if flip => match derives { true => quote! { @@ -1283,6 +1286,29 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn _ => extent_edge(&query, &arg), }, ParsedFieldType::Regular(RegularParsedField { ty, .. }) => match ir::value_binding(&node, index) { + // A ranked input materializes whole, so a data-dependent + // extent can walk its lanes. + ValueBinding::Materialized => { + let levels = ir::materialized_levels(&node, index); + quote! { + let #query = || { + let __arena = #core_types::context::ExtractArena::arena(__input); + let __count = match #core_types::node::Node::extent(&self.#name, __input, #core_types::gpoll::Level::Below(#levels)) { + #core_types::gpoll::GPoll::Final(#core_types::gpoll::Extent::Exactly(__count)) => __count, + #core_types::gpoll::GPoll::Pending => return #core_types::gpoll::GPoll::Pending, + _ => return #core_types::gpoll::GPoll::Error(::std::boxed::Box::new(#core_types::gpoll::GraphError::new("extent over a non-exact ranked input"))), + }; + match #core_types::record::materialize_batch(&self.#name, __input, 0..__count as u64, __arena) { + #core_types::node::BatchStatus::Lent(__batch, _) => #core_types::gpoll::GPoll::Final(unsafe { #core_types::node::List::<#ty>::new(__batch) }), + #core_types::node::BatchStatus::Filled(__batch, _) => #core_types::gpoll::GPoll::Final(unsafe { #core_types::node::List::<#ty>::new(__batch.into_shared()) }), + #core_types::node::BatchStatus::Pending => #core_types::gpoll::GPoll::Pending, + #core_types::node::BatchStatus::Error(__error) => #core_types::gpoll::GPoll::Error(::std::boxed::Box::new(__error)), + _ => #core_types::gpoll::GPoll::Error(::std::boxed::Box::new(#core_types::gpoll::GraphError::new("extent could not materialize a ranked input"))), + } + }; + let #arg = #core_types::extent::ListIn::new(&#query); + } + } ValueBinding::RecordElement | ValueBinding::ReadingSecondary => { let slot = format_ident!("__in_{index}"); quote! { diff --git a/node-graph/node-macro/src/codegen/entries.rs b/node-graph/node-macro/src/codegen/entries.rs index 853ba56ce8..ab1e4ff1a8 100644 --- a/node-graph/node-macro/src/codegen/entries.rs +++ b/node-graph/node-macro/src/codegen/entries.rs @@ -188,6 +188,8 @@ enum SlotKind { Value(Type), /// A record edge whose element extracts to the node's plain value input. Extracted(Type), + /// A ranked record edge consumed whole; no layout rides to the constructor. + Ranked(Type), /// A plain value edge. Plain(Type), /// A lazy node edge. @@ -227,6 +229,7 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields match &field.ty { ParsedFieldType::Node(NodeParsedField { output_type, .. }) => SlotKind::Lazy(output_type.clone()), ParsedFieldType::Regular(RegularParsedField { ty, .. }) => match ir::value_binding(&node, index) { + ir::ValueBinding::Materialized => SlotKind::Ranked(ty.clone()), ir::ValueBinding::ReadingSecondary | ir::ValueBinding::RecordElement => SlotKind::Value(ty.clone()), // One wire kind: a record node's plain value still rides a // record edge, extracted to its element at construction. @@ -240,7 +243,9 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields // Every non-base value/plain/lazy input must be concrete. let values_concrete = regular_fields.iter().zip(&slots).all(|(field, slot)| match slot { SlotKind::BaseGeneric(_) | SlotKind::BaseConcrete(_) => true, - SlotKind::Value(ty) | SlotKind::Extracted(ty) | SlotKind::Plain(ty) | SlotKind::Lazy(ty) => !contains_open_generic(parsed, ty) && (lend(field) || !type_disqualifies(ty)), + SlotKind::Value(ty) | SlotKind::Extracted(ty) | SlotKind::Ranked(ty) | SlotKind::Plain(ty) | SlotKind::Lazy(ty) => { + !contains_open_generic(parsed, ty) && (lend(field) || !type_disqualifies(ty)) + } }); if !values_concrete { return quote!(); @@ -252,7 +257,7 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields let input_types = slots.iter().map(|slot| match slot { SlotKind::BaseGeneric(name) => quote!(gcore::registry::generic_record_edge_type(#name)), - SlotKind::BaseConcrete(ty) | SlotKind::Value(ty) | SlotKind::Extracted(ty) => quote!(gcore::registry::record_edge_type::<#ty>()), + SlotKind::BaseConcrete(ty) | SlotKind::Value(ty) | SlotKind::Extracted(ty) | SlotKind::Ranked(ty) => quote!(gcore::registry::record_edge_type::<#ty>()), SlotKind::Plain(ty) | SlotKind::Lazy(ty) => quote!(gcore::registry::edge_type::<#ty>()), }); @@ -277,6 +282,9 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields let #layout = #handle.layout().clone(); let #name = gcore::record::RecordExtract::<#value_ty, _>::new(#handle.downcast_record::<#value_ty>()?, &#layout); }, + SlotKind::Ranked(value_ty) => quote! { + let #name = inputs.next().unwrap().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/node-macro/src/codegen/ir.rs b/node-graph/node-macro/src/codegen/ir.rs index 69da8ab77f..fee5d38533 100644 --- a/node-graph/node-macro/src/codegen/ir.rs +++ b/node-graph/node-macro/src/codegen/ir.rs @@ -322,7 +322,13 @@ fn has_attr_io(node: &Node) -> bool { /// Levels of `input[index]` the output does not carry; `> 0` folds the input /// into a `List` before the kernel. pub(crate) fn materialized_levels(node: &Node, index: usize) -> u8 { - node.inputs[index].shape.depth.saturating_sub(node.output.shape.depth) + let input = &node.inputs[index]; + // A ranked subject folds the levels the output collapses; a ranked + // non-subject input is consumed whole. + match input.subject { + true => input.shape.depth.saturating_sub(node.output.shape.depth), + false => input.shape.depth, + } } pub(crate) fn value_binding(node: &Node, index: usize) -> ValueBinding {