diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 1b52553805..1b32f67b92 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -7,7 +7,6 @@ use core_types::color::SRGBA8; use core_types::context::Context; use core_types::gpoll::GPoll; use core_types::list::List; -use core_types::node::Node; use core_types::registry::EdgeHandle; use core_types::transform::Footprint; use core_types::uuid::NodeId; @@ -366,24 +365,21 @@ macro_rules! tagged_value { // RECORD WIRES, WHICH LAND AS THEIR ELEMENT // ======================= if ty == core_types::registry::record_edge_type::<()>() { - return Ok(handle.downcast_record::<()>().map_err(|e| format!("{e:?}"))?.eval(ctx).map(|_| TaggedValue::None)); + let edge = handle.downcast_record::<()>().map_err(|e| format!("{e:?}"))?; + return Ok(core_types::record::serve_edge(&edge, ctx).map(|_| TaggedValue::None)); } $( if ty == core_types::registry::record_edge_type::<$ty>() { let layout = handle.layout().clone(); - return Ok(handle - .downcast_record::<$ty>() - .map_err(|e| format!("{e:?}"))? - .eval(ctx) + let edge = handle.downcast_record::<$ty>().map_err(|e| format!("{e:?}"))?; + return Ok(core_types::record::serve_edge(&edge, ctx) .map(|value| TaggedValue::$identifier(unsafe { core_types::record::read_element::<$ty>(layout.rec(&value)) }))); } )* if ty == core_types::registry::record_edge_type::() { let layout = handle.layout().clone(); - return Ok(handle - .downcast_record::() - .map_err(|e| format!("{e:?}"))? - .eval(ctx) + let edge = handle.downcast_record::().map_err(|e| format!("{e:?}"))?; + return Ok(core_types::record::serve_edge(&edge, ctx) .map(|value| TaggedValue::RenderOutput(unsafe { core_types::record::read_element::(layout.rec(&value)) }))); } Err(format!("Cannot convert edge of type {ty} to TaggedValue")) diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index b348637300..7cdcf3af3f 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -178,7 +178,10 @@ impl DynamicExecutor { .ok_or_else(|| IntrospectError::PathNotFound(node_path.to_vec()))?; let arena = self.arena.lock().unwrap_or_else(PoisonError::into_inner); // SAFETY: between evaluations, nothing served on the stack is live. - unsafe { core_types::record::stack::reserve(self.tree.stack_need()); } let generations = self.runtime.snapshot(); + unsafe { + core_types::record::stack::reserve(self.tree.stack_need()); + } + let generations = self.runtime.snapshot(); let scope = EvalScope::new(snapshot.try_real_time(), snapshot.try_animation_time(), snapshot.try_pointer_position(), &generations, &arena); let Some(ctx) = snapshot.rehydrate(&scope) else { return Err(IntrospectError::NoData); @@ -193,10 +196,10 @@ impl DynamicExecutor { _ => None, } } else { - match core_types::node::Node::eval(&edge, &ctx) { + match core_types::record::serve_edge(&edge, &ctx) { GPoll::Final(value) | GPoll::Partial(value) => { let rec = layout.rec(&value); - // SAFETY: the eval produced one live record of the edge's layout. + // SAFETY: the serve produced one live record of the edge's layout. let batch = unsafe { core_types::node::RecordBatch::new(rec.ptr(), 1, layout) }; read(layout, batch, &arena) } @@ -621,7 +624,10 @@ mod test { let scope = EvalScope::new(None, None, None, &generations, &arena); let ctx = ContextImpl::root(&scope); // SAFETY: between evaluations, nothing served on the stack is live. - unsafe { core_types::record::stack::reserve(layout.frame_bytes()); } let GPoll::Final(value) = edge.eval(&ctx) else { + unsafe { + core_types::record::stack::reserve(layout.frame_bytes()); + } + let GPoll::Final(value) = core_types::record::serve_edge(&edge, &ctx) else { panic!("expected a final record"); }; assert_eq!(unsafe { core_types::record::read_element::(layout.rec(&value)) }, 2); @@ -666,7 +672,10 @@ mod test { let layout = handle.layout().clone(); let edge = handle.duplicate().downcast_record::().unwrap(); // SAFETY: between evaluations, nothing served on the stack is live. - unsafe { core_types::record::stack::reserve(executor.tree().stack_need()); } let GPoll::Final(value) = edge.eval(&ctx) else { + unsafe { + core_types::record::stack::reserve(executor.tree().stack_need()); + } + let GPoll::Final(value) = core_types::record::serve_edge(&edge, &ctx) else { panic!("the flipped clone must evaluate over record wires, got a non-final poll"); }; assert_eq!(unsafe { core_types::record::read_element::(layout.rec(&value)) }, 7.); @@ -693,7 +702,10 @@ mod test { let ctx = ContextImpl::root(&scope); let edge = executor.tree().get(NodeId(2)).unwrap().downcast_record::().unwrap(); // SAFETY: between evaluations, nothing served on the stack is live. - unsafe { core_types::record::stack::reserve(executor.tree().stack_need()); } let result = edge.eval(&ctx); + unsafe { + core_types::record::stack::reserve(executor.tree().stack_need()); + } + let result = core_types::record::serve_edge(&edge, &ctx); // The empty raster level folds to an empty palette: past-end at lane 0. assert!( matches!(&result, GPoll::Error(error) if error.kind == core_types::gpoll::ErrorKind::PastEnd), diff --git a/node-graph/libraries/core-types/src/node.rs b/node-graph/libraries/core-types/src/node.rs index 53b90604fd..8643af7f6e 100644 --- a/node-graph/libraries/core-types/src/node.rs +++ b/node-graph/libraries/core-types/src/node.rs @@ -17,7 +17,7 @@ pub enum BatchStatus<'a> { /// hint is as for `Lent`. Filled(RecordBatchMut<'a>, Finality, Extent), /// No batch implementation behind this edge; a driver answers with the - /// per-lane eval and copy-out loop ([`crate::record::fill_frames`]). + /// per-lane serve and copy-out loop ([`crate::record::fill_frames`]). Unbatched, Pending, Error(GraphError), @@ -314,43 +314,34 @@ impl Iterator for ListIter<'_, T> { } } -/// The output marker of a node that serves records through the caller's -/// frame claim instead of returning a plain value. It satisfies the lift -/// bounds so [`Node::serve`] stays callable and overridable on the erased -/// surface. -#[derive(Clone, Copy, Debug, Default, PartialEq, dyn_any::DynAny)] -pub struct Records; - pub trait Node { - type Output; - - fn eval(&self, input: &Input) -> GPoll; - /// Serves the node's record through the caller's claim: the writes land /// in the claim and the returned proof is mintable only by its closing /// methods, so the served record is of the claimed layout by - /// construction. The default lifts a plain output's element; record - /// servers override it. + /// construction. The caller claims the frame at [`Node::layout`] before + /// the call, so the node advances the record stack by exactly that frame. fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'l>) -> GPoll> where - Self::Output: Send + Sync + dyn_any::StaticTypeSized, - Input: crate::context::ExtractArena, - { - slot.lift_served(self.eval(input), input.arena()) - } + Input: crate::context::ExtractArena; /// The count of items at one absolute nesting level (innermost `0`). The /// leveled primitive a structure node overrides to report a pushed level's /// size; the scalar base is one item at every level. Uncertainty rides the /// `GPoll` status axis. - fn extent_at(&self, _input: &Input, _level: u8) -> GPoll { + fn extent_at<'e>(&self, _input: &Input, _level: u8) -> GPoll + where + Input: crate::context::ExtractArena, + { GPoll::Final(Extent::Exactly(1)) } /// The composite domain query derived from [`extent_at`](Node::extent_at): /// one level, the product of the levels below or above it, or the whole /// domain's flat count. Consumers query this; nodes only write `extent_at`. - fn extent(&self, input: &Input, at: Level) -> GPoll { + fn extent<'e>(&self, input: &Input, at: Level) -> GPoll + where + Input: crate::context::ExtractArena, + { let product = |range: core::ops::Range| range.fold(GPoll::Final(Extent::Exactly(1)), |acc, level| Extent::mul(acc, self.extent_at(input, level))); match at { Level::At(level) => self.extent_at(input, level), @@ -384,13 +375,13 @@ pub trait Node { /// Batched evaluation of `range` into caller-provided frame storage of /// `range.len() * layout.lane_stride()` bytes; see [`BatchStatus`]. The - /// default advertises no support and drivers fall back to per-lane eval + /// default advertises no support and drivers fall back to per-lane serves /// with copy-out ([`crate::record::fill_frames`]); overrides exist to beat /// that loop (resident lanes, direct fills, fewer erased calls), never for /// correctness. - fn eval_batch<'a>(&'a self, input: &'a Input, range: Range, scratch: Option<&'a mut [MaybeUninit]>) -> BatchStatus<'a> + fn eval_batch<'a, 'e>(&'a self, input: &'a Input, range: Range, scratch: Option<&'a mut [MaybeUninit]>) -> BatchStatus<'a> where - Input: InjectIndex + Copy, + Input: InjectIndex + Copy + crate::context::ExtractArena, { let _ = (input, range, scratch); BatchStatus::Unbatched @@ -401,13 +392,17 @@ impl Node for &N where N: Node + ?Sized, { - type Output = N::Output; - - fn eval(&self, input: &Input) -> GPoll { - (**self).eval(input) + fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'l>) -> GPoll> + where + Input: crate::context::ExtractArena, + { + (**self).serve(input, slot) } - fn extent_at(&self, input: &Input, level: u8) -> GPoll { + fn extent_at<'e>(&self, input: &Input, level: u8) -> GPoll + where + Input: crate::context::ExtractArena, + { (**self).extent_at(input, level) } @@ -419,9 +414,9 @@ where (**self).layout() } - fn eval_batch<'a>(&'a self, input: &'a Input, range: Range, scratch: Option<&'a mut [MaybeUninit]>) -> BatchStatus<'a> + fn eval_batch<'a, 'e>(&'a self, input: &'a Input, range: Range, scratch: Option<&'a mut [MaybeUninit]>) -> BatchStatus<'a> where - Input: InjectIndex + Copy, + Input: InjectIndex + Copy + crate::context::ExtractArena, { (**self).eval_batch(input, range, scratch) } @@ -431,13 +426,17 @@ impl Node for Box where N: Node + ?Sized, { - type Output = N::Output; - - fn eval(&self, input: &Input) -> GPoll { - (**self).eval(input) + fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'l>) -> GPoll> + where + Input: crate::context::ExtractArena, + { + (**self).serve(input, slot) } - fn extent_at(&self, input: &Input, level: u8) -> GPoll { + fn extent_at<'e>(&self, input: &Input, level: u8) -> GPoll + where + Input: crate::context::ExtractArena, + { (**self).extent_at(input, level) } @@ -449,9 +448,9 @@ where (**self).layout() } - fn eval_batch<'a>(&'a self, input: &'a Input, range: Range, scratch: Option<&'a mut [MaybeUninit]>) -> BatchStatus<'a> + fn eval_batch<'a, 'e>(&'a self, input: &'a Input, range: Range, scratch: Option<&'a mut [MaybeUninit]>) -> BatchStatus<'a> where - Input: InjectIndex + Copy, + Input: InjectIndex + Copy + crate::context::ExtractArena, { (**self).eval_batch(input, range, scratch) } @@ -461,13 +460,17 @@ impl Node for std::sync::Arc where N: Node + ?Sized, { - type Output = N::Output; - - fn eval(&self, input: &Input) -> GPoll { - (**self).eval(input) + fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'l>) -> GPoll> + where + Input: crate::context::ExtractArena, + { + (**self).serve(input, slot) } - fn extent_at(&self, input: &Input, level: u8) -> GPoll { + fn extent_at<'e>(&self, input: &Input, level: u8) -> GPoll + where + Input: crate::context::ExtractArena, + { (**self).extent_at(input, level) } @@ -479,9 +482,9 @@ where (**self).layout() } - fn eval_batch<'a>(&'a self, input: &'a Input, range: Range, scratch: Option<&'a mut [MaybeUninit]>) -> BatchStatus<'a> + fn eval_batch<'a, 'e>(&'a self, input: &'a Input, range: Range, scratch: Option<&'a mut [MaybeUninit]>) -> BatchStatus<'a> where - Input: InjectIndex + Copy, + Input: InjectIndex + Copy + crate::context::ExtractArena, { (**self).eval_batch(input, range, scratch) } @@ -513,20 +516,27 @@ impl StatusCell { Self { no_partial: true, ..Self::new() } } + /// Claims the edge's own frame, serves through it, and folds the poll's + /// status into the cell. The claim is the caller's, so the edge's frame is + /// entered exactly once per evaluation. #[inline(always)] - pub fn eval_input>(&self, input_index: usize, node: &N, input: &Input) -> Result { - match node.eval(input) { - GPoll::Final(value) => Ok(value), + pub fn eval_input<'e, Input, N: Node + ?Sized>(&self, input_index: usize, node: &N, input: &Input) -> Result, Interrupt> + where + Input: crate::context::ExtractArena, + { + let slot = crate::record::FrameClaim::enter(node.layout()); + match node.serve(input, slot) { + GPoll::Final(served) => Ok(served.value()), GPoll::Partial(_) if self.no_partial => Err(Interrupt::Pending), - GPoll::Partial(value) => { + GPoll::Partial(served) => { self.finality.set(Finality::Partial); - Ok(value) + Ok(served.value()) } GPoll::Fallback(boxed) => { - let (value, error) = *boxed; + let (served, error) = *boxed; let first = self.error.take(); self.error.set(first.or(Some(error.traced(input_index)))); - Ok(value) + Ok(served.value()) } GPoll::Pending => Err(Interrupt::Pending), GPoll::Error(mut error) => { @@ -590,19 +600,20 @@ impl<'a, N> LazyInput<'a, N> { } #[inline(always)] - pub fn eval(&self, ctx: &Input) -> Result + pub fn eval<'e, Input>(&self, ctx: &Input) -> Result, Interrupt> where - N: Node, + N: crate::record::DerivedRecordEdge<'e, Input>, { - self.cell.eval_input(self.input_index, self.node, ctx) + self.node.eval_derived(self.cell, self.input_index, ctx) } /// The edge's composite extent, for kernels that split or shift indices /// over their sources. #[inline(always)] - pub fn extent(&self, ctx: &Input, at: Level) -> GPoll + pub fn extent<'e, Input>(&self, ctx: &Input, at: Level) -> GPoll where N: Node, + Input: crate::context::ExtractArena, { self.node.extent(ctx, at) } @@ -612,40 +623,60 @@ impl<'a, N> LazyInput<'a, N> { mod tests { use super::*; + use crate::arena::Arena; + use crate::context::ExtractArena; + use crate::record::{LiftedSource, serve_edge}; + #[derive(Clone, Copy)] - struct TestInput { + struct TestInput<'a> { index: u64, + arena: &'a Arena, } - impl InjectIndex for TestInput { + impl InjectIndex for TestInput<'_> { fn set_index(&mut self, index: u64) { self.index = index; } } - struct Double; + impl<'a> ExtractArena for TestInput<'a> { + type ArenaRef = &'a Arena; - impl Node for Double { - type Output = u64; - - fn eval(&self, input: &TestInput) -> GPoll { - GPoll::Final(input.index * 2) + fn arena(&self) -> &'a Arena { + self.arena } } + fn double<'a>() -> LiftedSource) -> GPoll> { + LiftedSource::new(|input: &TestInput<'a>| GPoll::Final(input.index * 2)) + } + #[test] fn the_default_advertises_no_batch_support() { - let input = TestInput { index: 0 }; + let arena = Arena::new(1024).unwrap(); + let input = TestInput { index: 0, arena: &arena }; let mut scratch = [const { MaybeUninit::uninit() }; 4]; - assert!(matches!(Double.eval_batch(&input, 2..6, Some(&mut scratch)), BatchStatus::Unbatched)); - assert!(matches!(Double.eval_batch(&input, 2..6, None), BatchStatus::Unbatched)); + let node = double(); + assert!(matches!(node.eval_batch(&input, 2..6, Some(&mut scratch)), BatchStatus::Unbatched)); + assert!(matches!(node.eval_batch(&input, 2..6, None), BatchStatus::Unbatched)); } #[test] fn trait_is_object_safe_across_erased_edges() { - let erased: Box> = Box::new(Double); - let input = TestInput { index: 21 }; - assert_eq!(erased.eval(&input), GPoll::Final(42)); + let arena = Arena::new(1024).unwrap(); + let input = TestInput { index: 21, arena: &arena }; + let node = double(); + let layout = Node::::layout(&node).clone(); + let erased: Box> = Box::new(node); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { + crate::record::stack::reserve(1 << 12); + } + let GPoll::Final(value) = serve_edge(&*erased, &input) else { + panic!("the erased edge must serve a final record"); + }; + // SAFETY: the record was served at `layout`, whose element is the output. + assert_eq!(unsafe { crate::record::read_element::(layout.rec(&value)) }, 42); assert!(matches!(erased.eval_batch(&input, 0..2, None), BatchStatus::Unbatched)); } } diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index 9ad333d0d8..dbca779f2b 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -485,23 +485,30 @@ impl<'e> RecordValue<'e> { /// A record edge evaluable at a derived context, yielding the record at that /// context's lifetime. The lifetime is a trait parameter because a bound like -/// `for<'d> Node, Output = RecordValue<'d>>` is rejected: in a -/// higher-ranked bound the lifetime must appear in a constrained input -/// position, and both the `Derived` projection and the `Output` binding are -/// unconstrained ones. +/// `for<'d> Node>` cannot also say the derived context's arena +/// is at `'d`: the equality binding `ExtractArena` is an +/// unconstrained position under a higher rank. pub trait DerivedRecordEdge<'derived, C> { fn eval_derived(&self, cell: &crate::node::StatusCell, input_index: usize, ctx: &C) -> Result, crate::gpoll::Interrupt>; + /// [`serve_edge`] at the derived context, for poll kernels that carry the + /// status themselves. + fn serve_derived(&self, ctx: &C) -> GPoll>; fn extent_at_derived(&self, ctx: &C, level: u8) -> GPoll; } impl<'derived, C, N> DerivedRecordEdge<'derived, C> for N where - N: Node>, + N: Node, + C: crate::context::ExtractArena, { fn eval_derived(&self, cell: &crate::node::StatusCell, input_index: usize, ctx: &C) -> Result, crate::gpoll::Interrupt> { cell.eval_input(input_index, self, ctx) } + fn serve_derived(&self, ctx: &C) -> GPoll> { + serve_edge(self, ctx) + } + fn extent_at_derived(&self, ctx: &C, level: u8) -> GPoll { self.extent_at(ctx, level) } @@ -513,8 +520,8 @@ where /// are distinct. Frame bytes carry no drop glue, so the copy is a move. pub fn fill_frames<'a, 'e, C, N>(node: &'a N, input: &C, range: std::ops::Range, scratch: Option<&'a mut [std::mem::MaybeUninit]>) -> crate::node::BatchStatus<'a> where - C: crate::context::InjectIndex + Copy, - N: Node>, + C: crate::context::InjectIndex + Copy + crate::context::ExtractArena, + N: Node, { use crate::node::BatchStatus; let Some(scratch) = scratch else { @@ -538,7 +545,7 @@ where // SAFETY: the lane's record is copied out before the scope releases it, // and valueless exits serve nothing above the entry. let _lane_scope = unsafe { stack::ScopeGuard::enter() }; - let value = match node.eval(&local) { + let value = match serve_edge(node, &local) { GPoll::Final(value) => value, GPoll::Partial(value) => { finality = crate::gpoll::Finality::Partial; @@ -568,8 +575,8 @@ where /// scratch, and an unbatched edge falls back to the [`fill_frames`] loop. pub fn materialize_batch<'a, 'e, C, N>(node: &'a N, input: &'a C, range: std::ops::Range, arena: &'a crate::arena::Arena) -> crate::node::BatchStatus<'a> where - C: crate::context::InjectIndex + Copy, - N: Node>, + C: crate::context::InjectIndex + Copy + crate::context::ExtractArena, + N: Node, { use crate::node::BatchStatus; let Some(len) = range.end.checked_sub(range.start).and_then(|len| usize::try_from(len).ok()) else { @@ -608,8 +615,8 @@ pub enum LevelStatus<'a> { /// reducers inline the same protocol with their span offsets. pub fn materialize_level<'a, 'e, C, N>(node: &'a N, input: &'a C, arena: &'a crate::arena::Arena) -> LevelStatus<'a> where - C: crate::context::InjectIndex + Copy, - N: Node>, + C: crate::context::InjectIndex + Copy + crate::context::ExtractArena, + N: Node, { use crate::gpoll::{Extent, GraphError, Level}; use crate::node::BatchStatus; @@ -652,47 +659,6 @@ where } } -/// A record edge at a caller-chosen lifetime; the lifetime is a trait -/// parameter for the same constrained-position reason as -/// [`DerivedRecordEdge`]. -pub trait RecordEdge<'e, C>: Node> {} - -impl<'e, C, N: Node>> RecordEdge<'e, C> for N {} - -/// Builds an element-only record from a kernel's poll: inline layouts land -/// in the value, larger ones spill to the record stack, arena exhaustion of -/// a parked element reports as an error poll. -pub(crate) fn lift_poll<'e, T: Send + Sync>(poll: GPoll, layout: &Layout, arena: &'e crate::arena::Arena) -> GPoll> { - let build = |element: T| { - if layout.frame_bytes() == 0 { - let mut value = RecordValue::zeroed(); - unsafe { write_element(value.as_mut_ptr(), element, arena)? }; - Some(value) - } else { - let dst = stack::push(layout.frame_bytes()); - let written = unsafe { write_element(dst, element, arena) }; - stack::truncate_above(dst, layout.frame_bytes()); - written.map(|()| RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) })) - } - }; - let exhausted = || { - GPoll::Error(Box::new(crate::gpoll::GraphError { - kind: crate::gpoll::ErrorKind::ArenaExhausted, - trace: Vec::new(), - })) - }; - match poll { - GPoll::Final(element) => build(element).map_or_else(exhausted, GPoll::Final), - GPoll::Partial(element) => build(element).map_or_else(exhausted, GPoll::Partial), - GPoll::Fallback(boxed) => { - let (element, error) = *boxed; - build(element).map_or_else(exhausted, |value| GPoll::Fallback(Box::new((value, error)))) - } - GPoll::Pending => GPoll::Pending, - GPoll::Error(error) => GPoll::Error(error), - } -} - /// The raw lazy record edge handed to a record-opaque kernel: the wire plus /// its wiring-proven layout, the pairing the kernel's unsafe record /// operations rely on. The kernel must only pair the layout with values this @@ -711,19 +677,22 @@ impl<'a, N> RecordEdgeInput<'a, N> { self.layout } - pub fn eval<'e, C>(&self, ctx: &C) -> GPoll> + /// Serves the edge through the kernel's own claim: the kernel's output + /// layout is the edge's, so the claim it was handed is the edge's frame. + pub fn serve<'e, 'l, C>(&self, ctx: &C, slot: FrameClaim<'l>) -> GPoll> where - N: Node>, + N: Node, + C: crate::context::ExtractArena, { - self.node.eval(ctx) + self.node.serve(ctx, slot) } /// [`materialize_level`] over the edge: the wire's whole flat span as one /// batch. pub fn materialize_level<'e, 'b, C>(&'b self, ctx: &'b C, arena: &'b crate::arena::Arena) -> LevelStatus<'b> where - N: Node>, - C: crate::context::InjectIndex + Copy, + N: Node, + C: crate::context::InjectIndex + Copy + crate::context::ExtractArena, { materialize_level(self.node, ctx, arena) } @@ -765,12 +734,12 @@ impl<'a, Out, N> ElementEdge<'a, Out, N> { pub fn eval<'d, C>(&self, ctx: &C) -> GPoll where - N: Node>, + N: DerivedRecordEdge<'d, C>, { // SAFETY: the read copies out by value, so no record above the entry // (the edge's own frame) is live past the scope. let _scope = unsafe { stack::ScopeGuard::enter() }; - self.node.eval(ctx).map(|value| unsafe { (self.read)(self.layout.rec(&value), self.reads) }) + self.node.serve_derived(ctx).map(|value| unsafe { (self.read)(self.layout.rec(&value), self.reads) }) } } @@ -816,13 +785,13 @@ impl<'a, Out, N> ElementLazyInput<'a, Out, N> { pub fn eval<'d, C>(&self, ctx: &C) -> Result where - N: Node>, + N: DerivedRecordEdge<'d, C>, { // SAFETY: the read copies the element and declared attributes out by // value, so no record above the entry (the edge's own frame) is live // past the scope. let _scope = unsafe { stack::ScopeGuard::enter() }; - let value = self.cell.eval_input(self.input_index, self.node, ctx)?; + let value = self.node.eval_derived(self.cell, self.input_index, ctx)?; Ok(unsafe { (self.read)(self.layout.rec(&value), self.reads) }) } } @@ -1583,7 +1552,6 @@ pub struct SourcePlan { moves: Vec<(usize, usize, usize)>, fills: Vec<(usize, Box<[u8]>)>, source: Layout, - union: Layout, } impl SourcePlan { @@ -1602,7 +1570,6 @@ impl SourcePlan { moves, fills, source: source.clone(), - union: union.clone(), }) } @@ -1650,47 +1617,6 @@ pub unsafe fn copy_record_bytes(layout: &Layout, rec: Rec) -> Box<[u8]> { unsafe { std::slice::from_raw_parts(rec.ptr(), layout.size) }.into() } -/// Serves a record whose frame lives in storage the current evaluation does -/// not own (a cached batch, published bytes): claims the node's frame over a -/// copy of the source frame, so the contract that every node advances the -/// record stack by exactly its own frame holds for cache hits too. -/// -/// # Safety -/// `src` must point at a live record of `layout` whose parked references -/// outlive the serving evaluation. -pub unsafe fn serve_frame<'e>(layout: &Layout, src: *const u8) -> RecordValue<'e> { - if layout.frame_bytes() == 0 { - let mut value = RecordValue::zeroed(); - unsafe { std::ptr::copy_nonoverlapping(src, value.as_mut_ptr(), layout.size) }; - value - } else { - let dst = stack::push(layout.frame_bytes()); - unsafe { std::ptr::copy_nonoverlapping(src, dst, layout.size) }; - RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) }) - } -} - -/// Claims a node's frame with nothing to serve, for exits that yield no -/// record (past-end, pending): the frame contract holds on every exit, -/// error included. -pub fn claim_frame(layout: &Layout) { - if layout.frame_bytes() != 0 { - stack::push(layout.frame_bytes()); - } -} - -/// Closes an interrupted eval: rewinds to the eval's entry pointer (interrupt -/// exits carry no value, so frames claimed above it are dead) and claims the -/// node's own frame, keeping the frame contract on interrupt exits. -/// -/// # Safety -/// `entry` must be the stack pointer at the eval's entry, and no record -/// above it may be referenced after the close. -pub unsafe fn interrupt_frame(entry: usize, layout: &Layout) { - unsafe { stack::rewind(entry) }; - claim_frame(layout); -} - /// A node's own output frame, claimed at eval entry: the one closing surface /// for every exit. Writes land through it, [`Self::lift`] and [`Self::finish`] /// serve the record, and its drop releases everything claimed above the frame @@ -1806,6 +1732,37 @@ impl<'l> FrameClaim<'l> { pub fn lift_served<'e, T: Send + Sync + dyn_any::StaticTypeSized>(self, poll: GPoll, arena: &'e crate::arena::Arena) -> GPoll> { self.lift(poll, arena).map(|value| Served { value }) } + + /// [`Self::finish`] with the proof-bearing return for [`Node::serve`]. + /// + /// # Safety + /// As [`Self::finish`]. + pub unsafe fn finish_served<'e>(self) -> Served<'e> { + Served { value: unsafe { self.finish() } } + } + + /// Fills the frame from a record a forwarded wire already served, and + /// closes it: the source's frame sits above this claim and dies with its + /// drop, so the served record is this claim's own. + /// + /// # Safety + /// `value` must be a live record of this frame's layout. + pub unsafe fn forward<'e>(mut self, value: &RecordValue<'_>) -> Served<'e> { + let src = self.layout.rec(value).ptr(); + unsafe { + self.fill_copy(src); + self.finish_served() + } + } + + /// Translates a source record into the frame through a wiring-resolved plan. + /// + /// # Safety + /// `src` must be a live record of `plan`'s source layout, and `plan` must + /// translate into this frame's layout. + pub unsafe fn translate(&mut self, src: Rec, plan: &SourcePlan) { + unsafe { plan.translate(src, self.dst()) }; + } } /// The proof a record was served through a frame claim: mintable only by the @@ -1819,6 +1776,23 @@ impl<'e> Served<'e> { pub fn value(self) -> RecordValue<'e> { self.value } + + /// The served record in place, for producers that read it before passing + /// the proof on. + pub fn record(&self) -> &RecordValue<'e> { + &self.value + } +} + +/// Claims `node`'s own frame and serves through it: the caller-side half of +/// [`Node::serve`], for drivers that want the record rather than the proof. +pub fn serve_edge<'e, C, N>(node: &N, input: &C) -> GPoll> +where + N: Node + ?Sized, + C: crate::context::ExtractArena, +{ + let slot = FrameClaim::enter(node.layout()); + node.serve(input, slot).map(Served::value) } impl Drop for FrameClaim<'_> { @@ -1876,6 +1850,13 @@ impl OwnedRecord { written.map(|()| value) } + /// [`Self::replay`] into a caller's claim rather than a fresh frame; the + /// claim's layout is the one the copy was taken at. + pub fn replay_into(&self, slot: &mut FrameClaim<'_>, arena: &crate::arena::Arena) -> Option<()> { + let layout = slot.layout; + self.write_into(layout, slot.dst(), arena) + } + fn write_into(&self, layout: &Layout, dst: *mut u8, arena: &crate::arena::Arena) -> Option<()> { unsafe { std::ptr::copy_nonoverlapping(self.bytes.as_ptr(), dst, self.bytes.len()) }; if let Some(element) = &self.element { @@ -1948,12 +1929,16 @@ impl ServedRecord { /// rewinds to its entry state, so the result is owned and no stack slot /// stays claimed. Assertion scaffolding for law tests; production consumers /// read served records in place. -pub fn capture<'e, C, N: Node>>(node: &N, ctx: &C) -> GPoll { +pub fn capture<'e, C, N>(node: &N, ctx: &C) -> GPoll +where + N: Node + ?Sized, + C: crate::context::ExtractArena, +{ // SAFETY: any served record is deep-copied out inside the scope, so // nothing served above the entry escapes it. let _scope = unsafe { stack::ScopeGuard::enter() }; let layout = node.layout().clone(); - node.eval(ctx).map(|value| ServedRecord { + serve_edge(node, ctx).map(|value| ServedRecord { // SAFETY: the poll served `value` at the node's declared layout and // nothing has claimed frames since. record: unsafe { OwnedRecord::copy_out(&layout, layout.rec(&value)) }, @@ -1961,39 +1946,39 @@ pub fn capture<'e, C, N: Node>>(node: &N, ctx: &C) - }) } -/// Law-test scaffolding: wraps an arbitrary plain node onto a record wire -/// (the element lands at offset 0 of a fresh element-only record, parked when -/// it carries drop glue). No production path constructs one; value edges are +/// Law-test scaffolding: a kernel closure served onto an element-only record +/// wire (the element lands at offset 0, parked when it carries drop glue). No +/// production path constructs one; value edges are /// [`crate::value::ValueSource`]. -pub struct RecordLift { - edge: N, +pub struct LiftedSource { + kernel: F, layout: Layout, _marker: std::marker::PhantomData El>, } -impl RecordLift +impl LiftedSource where El::Static: Clone + Send + Sync, { - pub fn new(edge: N) -> Self { + pub fn new(kernel: F) -> Self { Self { - edge, + kernel, layout: Layout::default().with_writes(0, element_write::(), &[]), _marker: std::marker::PhantomData, } } } -impl<'e, C, El, N> Node for RecordLift +impl Node for LiftedSource where - C: crate::context::ExtractArena, - El: Send + Sync + 'static, - N: Node, + El: Send + Sync + dyn_any::StaticTypeSized, + F: Fn(&C) -> GPoll, { - type Output = RecordValue<'e>; - - fn eval(&self, input: &C) -> GPoll> { - lift_poll(self.edge.eval(input), &self.layout, input.arena()) + fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll> + where + C: crate::context::ExtractArena, + { + slot.lift_served((self.kernel)(input), input.arena()) } fn layout(&self) -> &Layout { @@ -2021,54 +2006,63 @@ impl RecordExtract { } } -impl<'e, C, El, N> Node for RecordExtract -where - El: Clone + 'static, - N: Node>, -{ - type Output = El; - - fn eval(&self, input: &C) -> GPoll { +impl RecordExtract { + /// The edge's element, copied out of its record. + pub fn eval<'e, C>(&self, input: &C) -> GPoll + where + N: Node, + C: crate::context::ExtractArena, + { // SAFETY: the element copies out by value, so no record above the - // entry (the edge's frame) is live past the scope; a plain output - // claims no frame itself. + // entry (the edge's frame) is live past the scope. let _scope = unsafe { stack::ScopeGuard::enter() }; - self.edge.eval(input).map(|value| unsafe { read_element::(self.layout.rec(&value)) }) + serve_edge(&self.edge, input).map(|value| unsafe { read_element::(self.layout.rec(&value)) }) } } -impl<'e, C, N> Node for RecordSource +impl Node for RecordSource where - N: Node>, + N: Node, { - type Output = RecordValue<'e>; - - fn eval(&self, input: &C) -> GPoll> { - match &self.plan { - None => self.edge.eval(input), - Some(plan) if plan.union.frame_bytes() == 0 => { - // SAFETY: the translation copies the record into the inline - // value, so no record above the entry is live past the scope. - let _scope = unsafe { stack::ScopeGuard::enter() }; - self.edge.eval(input).map(|value| { - let mut out = RecordValue::zeroed(); - unsafe { plan.translate(plan.source.rec(&value), out.as_mut_ptr()) }; - out - }) + fn serve<'e, 'l>(&self, input: &C, mut slot: FrameClaim<'l>) -> GPoll> + where + C: crate::context::ExtractArena, + { + // The source's frame is claimed above this one and dies with the + // claim's drop; the translated union record stays. + let Some(plan) = &self.plan else { + return self.edge.serve(input, slot); + }; + match serve_edge(&self.edge, input) { + GPoll::Final(value) => { + // SAFETY: the value came from this edge, so it carries the + // plan's source layout. + unsafe { slot.translate(plan.source.rec(&value), plan) }; + // SAFETY: the translation completes the union record. + GPoll::Final(unsafe { slot.finish_served() }) } - Some(plan) => { - let dst = stack::push(plan.union.frame_bytes()); - let value = self.edge.eval(input); - let result = value.map(|value| RecordValue::spilled(unsafe { plan.translate(plan.source.rec(&value), dst) })); - // The source's frame dies with the translation; the claimed - // frame above stays as this node's output. - stack::truncate_above(dst, plan.union.frame_bytes()); - result + GPoll::Partial(value) => { + // SAFETY: as for the final arm. + unsafe { slot.translate(plan.source.rec(&value), plan) }; + // SAFETY: as for the final arm. + GPoll::Partial(unsafe { slot.finish_served() }) } + GPoll::Fallback(boxed) => { + let (value, error) = *boxed; + // SAFETY: as for the final arm. + unsafe { slot.translate(plan.source.rec(&value), plan) }; + // SAFETY: as for the final arm. + GPoll::Fallback(Box::new((unsafe { slot.finish_served() }, error))) + } + GPoll::Pending => GPoll::Pending, + GPoll::Error(error) => GPoll::Error(error), } } - fn extent_at(&self, input: &C, level: u8) -> GPoll { + fn extent_at<'x>(&self, input: &C, level: u8) -> GPoll + where + C: crate::context::ExtractArena, + { self.edge.extent_at(input, level) } @@ -3034,17 +3028,17 @@ mod tests { layout: Layout, } - impl<'e> Node<&'e crate::arena::Arena> for Fixture { - type Output = RecordValue<'e>; - - fn eval(&self, arena: &&'e crate::arena::Arena) -> GPoll> { - let mut frame = FrameBuilder::new(&self.layout, arena); + impl Node for Fixture { + fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll> + where + C: crate::context::ExtractArena, + { + let mut frame = FrameBuilder::new(&self.layout, crate::context::ExtractArena::arena(input)); frame.element(String::from("parked")); frame.attr::(DAffine2::from_translation(DVec2::new(3., 4.))); - match frame.finish() { - Some(value) => GPoll::Final(value), - None => GPoll::error("arena exhausted"), - } + let Some(value) = frame.finish() else { return GPoll::error("arena exhausted") }; + // SAFETY: the builder served a record of this node's layout. + GPoll::Final(unsafe { slot.forward(&value) }) } fn layout(&self) -> &Layout { @@ -3054,9 +3048,15 @@ mod tests { let layout = Layout::default().with_writes(0, element_write::(), &[FieldWrite::of::(0), FieldWrite::of::(0)]); // SAFETY: between evaluations, nothing served on the stack is live. - unsafe { stack::reserve(1 << 10); } let arena = crate::arena::Arena::new(1024).unwrap(); + unsafe { + stack::reserve(1 << 10); + } + let arena = crate::arena::Arena::new(1024).unwrap(); + let generations = []; + let scope = crate::context::EvalScope::new(None, None, None, &generations, &arena); + let ctx = crate::context::ContextImpl::root(&scope); let mark = stack::sp(); - let GPoll::Final(served) = capture(&Fixture { layout }, &&arena) else { + let GPoll::Final(served) = capture(&Fixture { layout }, &ctx) else { panic!("the fixture serves finally"); }; assert_eq!(stack::sp(), mark, "capture returns every claimed slot"); diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index fa15464863..96f7c6f9f0 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -74,9 +74,9 @@ pub use crate::NodeIOTypes; /// Element-independent by erasure; the wire's `Type::Record(El)` keeps element reads proven at wiring. #[cfg(not(target_family = "wasm"))] -pub type ErasedRecordNode = dyn for<'c> Node, Output = crate::record::RecordValue<'c>> + Send + Sync; +pub type ErasedRecordNode = dyn for<'c> Node> + Send + Sync; #[cfg(target_family = "wasm")] -pub type ErasedRecordNode = dyn for<'c> Node, Output = crate::record::RecordValue<'c>>; +pub type ErasedRecordNode = dyn for<'c> Node>; #[cfg(not(target_family = "wasm"))] type DynEdge = dyn std::any::Any + Send + Sync; @@ -136,12 +136,13 @@ impl Node for SharedEdge where N: Node + ?Sized, { - type Output = N::Output; - - fn eval(&self, input: &Input) -> crate::gpoll::GPoll { - // Every node advances the record stack by exactly its own frame: it keeps - // its output and reclaims its inputs. A mismatch is a leaked or - // over-released frame. + fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'l>) -> crate::gpoll::GPoll> + where + Input: crate::context::ExtractArena, + { + // Every node advances the record stack by exactly its own frame: the + // caller claimed it before the call, so serving must leave the stack + // where it found it. A mismatch is a leaked or over-released frame. #[cfg(debug_assertions)] let sp_before = crate::record::stack::sp(); #[cfg(debug_assertions)] @@ -160,7 +161,7 @@ where } // SAFETY: `own` keeps the payload alive for `self`'s lifetime and Arc // payloads are address stable. - let result = unsafe { self.ptr.as_ref() }.eval(input); + let result = unsafe { self.ptr.as_ref() }.serve(input, slot); #[cfg(debug_assertions)] if trace { eprintln!("sp> exit frame_bytes {} sp {} -> {}", self.layout().frame_bytes(), sp_before, crate::record::stack::sp()); @@ -168,7 +169,7 @@ where #[cfg(debug_assertions)] debug_assert_eq!( crate::record::stack::sp(), - sp_before + self.layout().frame_bytes(), + sp_before, "{} left the record stack misaligned (frame_bytes {}, depth {}, fields [{}], poll {})", std::any::type_name::(), self.layout().frame_bytes(), @@ -185,26 +186,29 @@ where result } - fn extent_at(&self, input: &Input, level: u8) -> crate::gpoll::GPoll { - // SAFETY: as in eval. + fn extent_at<'x>(&self, input: &Input, level: u8) -> crate::gpoll::GPoll + where + Input: crate::context::ExtractArena, + { + // SAFETY: as in serve. unsafe { self.ptr.as_ref() }.extent_at(input, level) } fn serialize(&self) -> Option> { - // SAFETY: as in eval. + // SAFETY: as in serve. unsafe { self.ptr.as_ref() }.serialize() } fn layout(&self) -> &crate::record::Layout { - // SAFETY: as in eval. + // SAFETY: as in serve. unsafe { self.ptr.as_ref() }.layout() } - fn eval_batch<'a>(&'a self, input: &'a Input, range: std::ops::Range, scratch: Option<&'a mut [std::mem::MaybeUninit]>) -> crate::node::BatchStatus<'a> + fn eval_batch<'a, 'x>(&'a self, input: &'a Input, range: std::ops::Range, scratch: Option<&'a mut [std::mem::MaybeUninit]>) -> crate::node::BatchStatus<'a> where - Input: crate::context::InjectIndex + Copy, + Input: crate::context::InjectIndex + Copy + crate::context::ExtractArena, { - // SAFETY: as in eval. + // SAFETY: as in serve. unsafe { self.ptr.as_ref() }.eval_batch(input, range, scratch) } } @@ -341,186 +345,244 @@ mod tests { use super::*; use crate::SourceId; use crate::arena::Arena; - use crate::context::{Ctx, EvalScope, ExtractArena}; + use crate::context::{Ctx, EvalScope, ExtractArena, ExtractIndices}; use crate::gpoll::GPoll; use std::sync::Arc; use std::sync::atomic::{AtomicU32, Ordering}; - struct CountingNode(AtomicU32); + use crate::record::{FrameClaim, Layout, LiftedSource, Served, element_write, read_element, serve_edge, stack}; - impl Node for CountingNode { - type Output = u32; - - fn eval(&self, _input: &Input) -> GPoll { - GPoll::Final(self.0.fetch_add(1, Ordering::Relaxed) + 1) - } - } - - struct LendNode(String); - - impl<'e, Input: Ctx + ExtractArena> Node for LendNode { - type Output = &'e String; - - fn eval(&self, input: &Input) -> GPoll<&'e String> { - match input.arena().alloc(self.0.clone()) { - Some((parked, _)) => GPoll::Final(parked), - None => GPoll::arena_exhausted(), - } - } + fn counting() -> LiftedSource Fn(&ContextImpl<'c>) -> GPoll> { + let count = AtomicU32::new(0); + LiftedSource::new(move |_: &ContextImpl<'_>| GPoll::Final(count.fetch_add(1, Ordering::Relaxed) + 1)) } fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> { EvalScope::new(Some(0.5), None, None, generations, arena) } - #[test] - fn borrow_carrying_value_types_wire_through_the_general_constructor() { - struct SplitBorrow<'c>(&'c str, usize); + /// Serves a parked borrow of its own value: the element is a reference into + /// the evaluation's arena, so its lifetime is the serving one. + struct LendNode { + value: String, + layout: Layout, + } - struct SplitNode { - content: Node0, + impl LendNode { + fn new(value: &str) -> Self { + Self { + value: value.to_string(), + layout: Layout::default().with_writes(0, element_write::<&'static String>(), &[]), + } } + } - impl<'e, Input, Node0> Node for SplitNode + impl Node for LendNode { + fn serve<'e, 'l>(&self, input: &Input, slot: FrameClaim<'l>) -> GPoll> where - Input: Ctx, - Node0: Node, + Input: ExtractArena, { - type Output = SplitBorrow<'e>; - - fn eval(&self, input: &Input) -> GPoll> { - self.content.eval(input).map(|value| SplitBorrow(value, value.len())) + match input.arena().alloc(self.value.clone()) { + Some((parked, _)) => slot.lift_served(GPoll::Final(parked), input.arena()), + None => GPoll::arena_exhausted(), } } - type ErasedSplitEdge = dyn for<'c> Node, Output = SplitBorrow<'c>> + Send + Sync; + fn layout(&self) -> &Layout { + &self.layout + } + } + #[test] + fn borrow_carrying_value_types_wire_through_the_general_constructor() { let arena = Arena::new(4096).unwrap(); let generations = []; let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { + stack::reserve(1 << 12); + } - let node: Arc = Arc::new(SplitNode { - content: LendNode("held".to_string()), - }); - let handle = EdgeHandle::new_erased(node, concrete!(SplitBorrow<'static>)); - assert_eq!(*handle.ty(), concrete!(SplitBorrow<'static>)); + let node = LendNode::new("held"); + let layout = Node::::layout(&node).clone(); + let handle = EdgeHandle::new_erased(Arc::new(node) as Arc, concrete!(String)); + assert_eq!(*handle.ty(), concrete!(String)); - let wired = handle.downcast_erased::(concrete!(SplitBorrow<'static>)).unwrap(); - let GPoll::Final(split) = wired.eval(&ctx) else { - panic!("borrow-carrying output must eval through the erased edge"); + let wired = handle.downcast_erased::(concrete!(String)).unwrap(); + let GPoll::Final(value) = serve_edge(&wired, &ctx) else { + panic!("borrow-carrying output must serve through the erased edge"); }; - assert_eq!(split.0, "held"); - assert_eq!(split.1, 4); + // SAFETY: the record was served at `layout`, whose element is the borrow. + let held = unsafe { read_element::<&String>(layout.rec(&value)) }; + assert_eq!(held, "held"); + assert_eq!(held.len(), 4); + } + + /// Evaluates its content at three promoted index levels and serves the + /// collected elements. + struct RepeatNode { + content: Node0, + inner: Layout, + layout: Layout, + _marker: std::marker::PhantomData T>, + } + + impl RepeatNode + where + Vec: Clone + Send + Sync + dyn_any::StaticTypeSized, + as dyn_any::StaticTypeSized>::Static: Clone + Send + Sync, + { + fn new(content: Node0, inner: Layout) -> Self { + Self { + content, + inner, + layout: Layout::default().with_writes(0, element_write::>(), &[]), + _marker: std::marker::PhantomData, + } + } + } + + impl Node for RepeatNode + where + C: Ctx + crate::context::DeriveCtx, + T: Clone + 'static, + Vec: Send + Sync + dyn_any::StaticTypeSized, + Node0: for<'x> crate::record::DerivedRecordEdge<'x, crate::context::Derived<'x, C>>, + { + fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll> + where + C: ExtractArena, + { + let cell = crate::node::StatusCell::new(); + let spilled = input.index_head(); + let mut result = Vec::new(); + for index in 0..3 { + // SAFETY: the element copies out by value, so the content's + // frame is dead by the time the scope releases it. + let _scope = unsafe { stack::ScopeGuard::enter() }; + let derived = input.promoted(&spilled, index); + match self.content.eval_derived(&cell, 0, &derived) { + // SAFETY: the content served at its own layout, whose + // element is `T`. + Ok(value) => result.push(unsafe { read_element::(self.inner.rec(&value)) }), + Err(interrupt) => return interrupt.into(), + } + } + slot.lift_served(cell.finish(result), input.arena()) + } + + fn layout(&self) -> &Layout { + &self.layout + } } #[test] fn derive_ctx_repeat_pushes_index_levels_through_the_erased_edge() { - use crate::context::{DeriveCtx, Derived, ExtractIndex}; - - struct RepeatNode { - content: Node0, - } - - impl Node for RepeatNode - where - C: Ctx + DeriveCtx, - Node0: for<'x> Node, Output = T>, - { - type Output = Vec; - - fn eval(&self, input: &C) -> GPoll> { - let spilled = input.index_head(); - let mut result = Vec::new(); - for index in 0..3 { - let derived = input.promoted(&spilled, index); - match self.content.eval(&derived) { - GPoll::Final(value) => result.push(value), - other => return other.map(|_| Vec::new()), - } - } - GPoll::Final(result) - } - } - - struct LevelsNode; - - impl Node for LevelsNode { - type Output = Vec; - - fn eval(&self, input: &Input) -> GPoll> { - GPoll::Final(input.try_index().map(|levels| levels.collect()).unwrap_or_default()) - } - } - let arena = Arena::new(1024).unwrap(); let generations = []; let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); - let nested = crate::record::RecordLift::>>, _>::new(RepeatNode { - content: RepeatNode { content: LevelsNode }, - }); + let levels = LiftedSource::, _>::new(|input: &ContextImpl| GPoll::Final(input.try_index().map(|levels| levels.collect()).unwrap_or_default())); + let levels_layout = Node::::layout(&levels).clone(); + let inner = RepeatNode::<_, Vec>::new(levels, levels_layout); + let inner_layout = Node::::layout(&inner).clone(); + let nested = RepeatNode::<_, Vec>>::new(inner, inner_layout); let layout = Node::::layout(&nested).clone(); let erased: Box = Box::new(nested); // SAFETY: between evaluations, nothing served on the stack is live. unsafe { - crate::record::stack::reserve(1 << 12); + stack::reserve(1 << 12); } - let GPoll::Final(value) = erased.eval(&ctx) else { + let GPoll::Final(value) = serve_edge(&*erased, &ctx) else { panic!("nested repeat must evaluate"); }; // SAFETY: the record was served at `layout`, whose element is the output. - let outer = unsafe { crate::record::read_element::>>>(layout.rec(&value)) }; + let outer = unsafe { read_element::>>>(layout.rec(&value)) }; assert_eq!(outer.len(), 3); assert_eq!(outer[2][1], vec![1, 2, 0]); assert_eq!(outer[0][0], vec![0, 0, 0]); } + /// Shifts the footprint's resolution and serves its content's element under + /// the derived context. + struct ShiftFootprintNode { + content: Node0, + inner: Layout, + layout: Layout, + } + + impl ShiftFootprintNode { + fn new(content: Node0, inner: Layout) -> Self { + Self { + content, + inner, + layout: Layout::default().with_writes(0, element_write::(), &[]), + } + } + } + + impl Node for ShiftFootprintNode + where + C: Ctx + crate::context::DeriveCtx + crate::context::ExtractFootprint, + Node0: for<'x> crate::record::DerivedRecordEdge<'x, crate::context::Derived<'x, C>>, + { + fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll> + where + C: ExtractArena, + { + use crate::transform::Footprint; + let cell = crate::node::StatusCell::new(); + let mut footprint = input.try_footprint().copied().unwrap_or(Footprint::DEFAULT); + footprint.resolution.x += 7; + // SAFETY: the element copies out by value, so the content's frame is + // dead by the time the scope releases it. + let value = { + let _scope = unsafe { stack::ScopeGuard::enter() }; + let derived = input.with_footprint(&footprint); + match self.content.eval_derived(&cell, 0, &derived) { + // SAFETY: the content served at its own layout, whose + // element is the resolution. + Ok(value) => unsafe { read_element::(self.inner.rec(&value)) }, + Err(interrupt) => return interrupt.into(), + } + }; + slot.lift_served(cell.finish(value), input.arena()) + } + + fn layout(&self) -> &Layout { + &self.layout + } + } + #[test] fn derive_ctx_footprint_replace_reaches_the_content() { - use crate::context::{DeriveCtx, Derived, ExtractFootprint}; + use crate::context::ExtractFootprint; use crate::transform::Footprint; - struct ShiftFootprintNode { - content: Node0, - } - - impl Node for ShiftFootprintNode - where - C: Ctx + DeriveCtx + ExtractFootprint, - Node0: for<'x> Node, Output = T>, - { - type Output = T; - - fn eval(&self, input: &C) -> GPoll { - let mut footprint = input.try_footprint().copied().unwrap_or(Footprint::DEFAULT); - footprint.resolution.x += 7; - let derived = input.with_footprint(&footprint); - self.content.eval(&derived) - } - } - - struct ResolutionNode; - - impl Node for ResolutionNode { - type Output = u32; - - fn eval(&self, input: &Input) -> GPoll { - GPoll::Final(input.try_footprint().map(|footprint| footprint.resolution.x).unwrap_or(0)) - } - } - let arena = Arena::new(1024).unwrap(); let generations = []; let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { + stack::reserve(1 << 12); + } - let graph = ShiftFootprintNode { - content: ShiftFootprintNode { content: ResolutionNode }, + let resolution = LiftedSource::::new(|input: &ContextImpl| GPoll::Final(input.try_footprint().map(|footprint| footprint.resolution.x).unwrap_or(0))); + let resolution_layout = Node::::layout(&resolution).clone(); + let shifted = ShiftFootprintNode::new(resolution, resolution_layout); + let shifted_layout = Node::::layout(&shifted).clone(); + let graph = ShiftFootprintNode::new(shifted, shifted_layout); + let layout = Node::::layout(&graph).clone(); + + let GPoll::Final(value) = serve_edge(&graph, &ctx) else { + panic!("the footprint shift must reach the content"); }; - assert_eq!(graph.eval(&ctx), GPoll::Final(Footprint::DEFAULT.resolution.x + 14)); + // SAFETY: the record was served at `layout`, whose element is the resolution. + assert_eq!(unsafe { read_element::(layout.rec(&value)) }, Footprint::DEFAULT.resolution.x + 14); } #[test] @@ -559,24 +621,24 @@ mod tests { let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); - let counting = crate::record::RecordLift::::new(CountingNode(AtomicU32::new(0))); + let counting = counting(); let layout = Node::::layout(&counting).clone(); let handle = EdgeHandle::new_record::(Arc::new(counting) as Arc); let duplicate = handle.duplicate(); assert_eq!(*duplicate.ty(), record_edge_type::()); // SAFETY: between evaluations, nothing served on the stack is live. unsafe { - crate::record::stack::reserve(1 << 12); + stack::reserve(1 << 12); } let first = handle.downcast_record::().unwrap(); let second = duplicate.downcast_record::().unwrap(); // SAFETY: each record was served at `layout`, whose element is the count. let count = |value| unsafe { layout.rec(&value).element::() }; - assert_eq!(first.eval(&ctx).map(count), GPoll::Final(1)); - assert_eq!(second.eval(&ctx).map(count), GPoll::Final(2)); + assert_eq!(serve_edge(&first, &ctx).map(count), GPoll::Final(1)); + assert_eq!(serve_edge(&second, &ctx).map(count), GPoll::Final(2)); drop(first); - assert_eq!(second.eval(&ctx).map(count), GPoll::Final(3)); + assert_eq!(serve_edge(&second, &ctx).map(count), GPoll::Final(3)); } } diff --git a/node-graph/libraries/core-types/src/runtime.rs b/node-graph/libraries/core-types/src/runtime.rs index 39cf38642c..c0ca0ed509 100644 --- a/node-graph/libraries/core-types/src/runtime.rs +++ b/node-graph/libraries/core-types/src/runtime.rs @@ -171,7 +171,7 @@ mod tests { use crate::context::{ContextImpl, Ctx, EvalScope, ExtractFootprint, ExtractVarArgs, VarArgLink, VarArgSlots}; use crate::gpoll::GPoll; use crate::node::Node; - use crate::record::{Layout, RecordExtract, RecordLift, element_write, stack}; + use crate::record::{Layout, LiftedSource, RecordExtract, element_write, stack}; use crate::transform::Footprint; use std::sync::Mutex; use std::sync::atomic::{AtomicU32, Ordering}; @@ -248,16 +248,6 @@ mod tests { } } - struct SourceNode(T); - - impl Node for SourceNode { - type Output = T; - - fn eval(&self, _input: &Input) -> GPoll { - GPoll::Final(self.0.clone()) - } - } - fn element_layout() -> Layout where T::Static: Clone + Send + Sync, @@ -265,11 +255,11 @@ mod tests { Layout::default().with_writes(0, element_write::(), &[]) } - fn lifted(value: T) -> RecordLift> + fn lifted(value: T) -> LiftedSource Fn(&ContextImpl<'c>) -> GPoll> where T::Static: Clone + Send + Sync, { - RecordLift::new(SourceNode(value)) + LiftedSource::new(move |_: &ContextImpl<'_>| GPoll::Final(value.clone())) } fn extract>>(mut graph: N) -> RecordExtract @@ -277,7 +267,10 @@ mod tests { El::Static: Clone + Send + Sync, { // SAFETY: between evaluations, nothing served on the stack is live. - unsafe { stack::reserve(1 << 12); } let layout = element_layout::(); + unsafe { + stack::reserve(1 << 12); + } + let layout = element_layout::(); graph.set_layout(crate::record::RecordLayout { frame_bytes: layout.frame_bytes(), plan: Vec::new(), @@ -333,17 +326,11 @@ mod tests { Ok(Box::pin(async move { value + addend })) } - struct GatedSource(Arc, f64); - - impl Node for GatedSource { - type Output = f64; - - fn eval(&self, _input: &Input) -> GPoll { - match self.0.load(Ordering::Relaxed) { - true => GPoll::Final(self.1), - false => GPoll::Pending, - } - } + fn gated(gate: Arc, value: f64) -> LiftedSource Fn(&ContextImpl<'c>) -> GPoll> { + LiftedSource::new(move |_: &ContextImpl<'_>| match gate.load(Ordering::Relaxed) { + true => GPoll::Final(value), + false => GPoll::Pending, + }) } fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> { @@ -367,13 +354,13 @@ mod tests { &element_layout::(), )); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + assert_eq!(graph.eval(&ctx), GPoll::Pending); + assert_eq!(graph.eval(&ctx), GPoll::Pending); assert_eq!(SLOW_DOUBLE_RUNS.load(Ordering::Relaxed), 0); assert_eq!(runtime.drain(), vec![7]); assert_eq!(SLOW_DOUBLE_RUNS.load(Ordering::Relaxed), 1); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0)); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0)); + assert_eq!(graph.eval(&ctx), GPoll::Final(42.0)); + assert_eq!(graph.eval(&ctx), GPoll::Final(42.0)); assert_eq!(SLOW_DOUBLE_RUNS.load(Ordering::Relaxed), 1); } @@ -394,9 +381,9 @@ mod tests { &element_layout::(), )); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Partial(-1.0)); + assert_eq!(graph.eval(&ctx), GPoll::Partial(-1.0)); runtime.drain(); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0)); + assert_eq!(graph.eval(&ctx), GPoll::Final(42.0)); } #[test] @@ -416,9 +403,9 @@ mod tests { &element_layout::(), )); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + assert_eq!(graph.eval(&ctx), GPoll::Pending); runtime.drain(); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0)); + assert_eq!(graph.eval(&ctx), GPoll::Final(42.0)); } #[test] @@ -438,12 +425,12 @@ mod tests { &element_layout::(), )); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + assert_eq!(graph.eval(&ctx), GPoll::Pending); assert_eq!(STAGED_RUNS.load(Ordering::Relaxed), 1, "the prologue runs synchronously on the miss"); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + assert_eq!(graph.eval(&ctx), GPoll::Pending); assert_eq!(STAGED_RUNS.load(Ordering::Relaxed), 1, "in flight must not rerun the prologue"); assert_eq!(runtime.drain(), vec![8]); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0)); + assert_eq!(graph.eval(&ctx), GPoll::Final(42.0)); assert_eq!(STAGED_RUNS.load(Ordering::Relaxed), 1); } @@ -458,7 +445,7 @@ mod tests { let runtime = Arc::new(MockRuntime::default()); let graph = extract::(StagedSumNode::new( lifted(40.0f64), - RecordLift::::new(GatedSource(gate.clone(), 2.0)), + gated(gate.clone(), 2.0), lifted(RuntimeHandle(runtime.clone())), lifted(9u64), &element_layout::(), @@ -467,12 +454,12 @@ mod tests { &element_layout::(), )); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + assert_eq!(graph.eval(&ctx), GPoll::Pending); assert_eq!(runtime.drain(), Vec::::new(), "an interrupted prologue must not spawn or claim the slot"); gate.store(true, Ordering::Relaxed); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + assert_eq!(graph.eval(&ctx), GPoll::Pending); assert_eq!(runtime.drain(), vec![9]); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0)); + assert_eq!(graph.eval(&ctx), GPoll::Final(42.0)); } #[test] @@ -498,9 +485,9 @@ mod tests { &element_layout::(), )); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + assert_eq!(graph.eval(&ctx), GPoll::Pending); runtime.drain(); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(21.5)); + assert_eq!(graph.eval(&ctx), GPoll::Final(21.5)); } #[test] @@ -522,9 +509,9 @@ mod tests { &element_layout::(), )); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + assert_eq!(graph.eval(&ctx), GPoll::Pending); runtime.drain(); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(Footprint::DEFAULT.resolution.x)); + assert_eq!(graph.eval(&ctx), GPoll::Final(Footprint::DEFAULT.resolution.x)); } #[test] @@ -638,7 +625,7 @@ mod tests { let snapshot = runtime.snapshot(); let scope = EvalScope::new(None, None, None, &snapshot, &arena); let ctx = ContextImpl::root(&scope); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0)); + assert_eq!(graph.eval(&ctx), GPoll::Final(42.0)); assert!(!runtime.take_dirty()); assert_eq!(runtime.snapshot(), vec![(13, 0)]); assert_eq!(runtime.spawner().drain(), 0); @@ -661,7 +648,7 @@ mod tests { let snapshot = runtime.snapshot(); let scope = EvalScope::new(None, None, None, &snapshot, &arena); let ctx = ContextImpl::root(&scope); - assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + assert_eq!(graph.eval(&ctx), GPoll::Pending); assert!(!runtime.take_dirty()); assert_eq!(runtime.spawner().drain(), 1); @@ -671,7 +658,7 @@ mod tests { let bumped_scope = EvalScope::new(None, None, None, &bumped, &arena); let bumped_ctx = ContextImpl::root(&bumped_scope); - assert_eq!(Node::eval(&graph, &bumped_ctx), GPoll::Final(42.0), "the own-generation-excluded key replays the landed slot"); + assert_eq!(graph.eval(&bumped_ctx), GPoll::Final(42.0), "the own-generation-excluded key replays the landed slot"); assert_eq!(runtime.spawner().drain(), 0, "a slot hit must not respawn"); let downstream_key = crate::registry::cache_key(&ContextImpl::root(&scope)); diff --git a/node-graph/libraries/core-types/src/value.rs b/node-graph/libraries/core-types/src/value.rs index 68fe2a0e30..da652d2629 100644 --- a/node-graph/libraries/core-types/src/value.rs +++ b/node-graph/libraries/core-types/src/value.rs @@ -17,15 +17,15 @@ where } } -impl<'e, C, T> crate::node::Node for ValueSource +impl crate::node::Node for ValueSource where - C: crate::context::ExtractArena, - T: Clone + Send + Sync + 'static, + T: Clone + Send + Sync + dyn_any::StaticTypeSized, { - type Output = crate::record::RecordValue<'e>; - - fn eval(&self, input: &C) -> crate::gpoll::GPoll> { - crate::record::lift_poll(crate::gpoll::GPoll::Final(self.value.clone()), &self.layout, input.arena()) + fn serve<'e, 'l>(&self, input: &C, slot: crate::record::FrameClaim<'l>) -> crate::gpoll::GPoll> + where + C: crate::context::ExtractArena, + { + slot.lift_served(crate::gpoll::GPoll::Final(self.value.clone()), input.arena()) } fn layout(&self) -> &crate::record::Layout { @@ -60,21 +60,25 @@ where } } -impl<'e, C, T> crate::node::Node for LeveledValueSource +impl crate::node::Node for LeveledValueSource where - C: crate::context::ExtractArena + crate::context::ExtractIndex, - T: Clone + Send + Sync + 'static, + C: crate::context::ExtractIndex, + T: Clone + Send + Sync + dyn_any::StaticTypeSized, { - type Output = crate::record::RecordValue<'e>; - - fn eval(&self, input: &C) -> crate::gpoll::GPoll> { + fn serve<'e, 'l>(&self, input: &C, slot: crate::record::FrameClaim<'l>) -> crate::gpoll::GPoll> + where + C: crate::context::ExtractArena, + { let Some(value) = self.values.get(input.innermost_index() as usize) else { return crate::gpoll::GPoll::error("value level addressed past its items"); }; - crate::record::lift_poll(crate::gpoll::GPoll::Final(value.clone()), &self.layout, input.arena()) + slot.lift_served(crate::gpoll::GPoll::Final(value.clone()), input.arena()) } - fn extent_at(&self, _input: &C, level: u8) -> crate::gpoll::GPoll { + fn extent_at<'x>(&self, _input: &C, level: u8) -> crate::gpoll::GPoll + where + C: crate::context::ExtractArena, + { match level { 0 => crate::gpoll::GPoll::Final(crate::gpoll::Extent::Exactly(self.values.len())), _ => crate::gpoll::GPoll::Final(crate::gpoll::Extent::Exactly(1)), diff --git a/node-graph/libraries/graphic-types/src/boundary.rs b/node-graph/libraries/graphic-types/src/boundary.rs index d89e660862..5d618dcaba 100644 --- a/node-graph/libraries/graphic-types/src/boundary.rs +++ b/node-graph/libraries/graphic-types/src/boundary.rs @@ -10,7 +10,7 @@ use core_types::arena::Arena; use core_types::context::InjectIndex; use core_types::gpoll::{Finality, GraphError}; use core_types::node::Node; -use core_types::record::{Group, GroupItem, LevelStatus, RecordValue, materialize_level}; +use core_types::record::{Group, GroupItem, LevelStatus, materialize_level}; use core_types::uuid::NodeId; use glam::{DAffine2, DVec2}; use vector_types::GradientStops; @@ -26,8 +26,8 @@ pub enum LevelGroup<'e> { /// group over the level's records, ready for the group render bridge. pub fn materialize_group<'a, 'e, C, N>(node: &'a N, input: &'a C, arena: &'a Arena) -> LevelGroup<'a> where - C: InjectIndex + Copy, - N: Node>, + C: InjectIndex + Copy + core_types::context::ExtractArena, + N: Node, { match materialize_level(node, input, arena) { LevelStatus::Batch(batch, finality) => { diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 06e185c173..76332e241d 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -828,14 +828,30 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn ]); } - // A kernel-declared `ExtractArena<'e>` bound already carries the arena at - // its own lifetime; a second equality bound would contradict it. - let ctx_extracts_arena = ctx_param.is_some_and(|ctx_param| { - ctx_param - .bounds - .iter() - .any(|bound| matches!(bound, TypeParamBound::Trait(trait_bound) if trait_bound.path.segments.last().is_some_and(|segment| segment.ident == "ExtractArena"))) - }); + // The serving lifetime is quantified by each serving method, so the impl + // never binds the context's arena; the kernel keeps its own bound. + let extracts_arena = |bound: &TypeParamBound| matches!(bound, TypeParamBound::Trait(trait_bound) if trait_bound.path.segments.last().is_some_and(|segment| segment.ident == "ExtractArena")); + let mut impl_ctx_bounds: Vec = match ctx_param { + Some(ctx_param) => ctx_param.bounds.iter().filter(|bound| !matches!(bound, TypeParamBound::Lifetime(_)) && !extracts_arena(bound)).map(|bound| quote!(#bound)).collect(), + None => Vec::new(), + }; + if ctx_param.is_none() { + impl_ctx_bounds.push(quote!(#core_types::Ctx)); + } + if async_source && !snapshot_ctx { + impl_ctx_bounds.push(quote!(#core_types::context::DeriveCtx)); + } + if snapshot_ctx { + impl_ctx_bounds.extend([ + quote!(#core_types::context::DeriveCtx), + quote!(#core_types::context::ExtractFootprint), + quote!(#core_types::context::ExtractRealTime), + quote!(#core_types::context::ExtractAnimationTime), + quote!(#core_types::context::ExtractPointerPosition), + quote!(#core_types::context::ExtractIndex), + quote!(#core_types::context::ExtractPosition), + ]); + } let derives = ctx_param.is_some_and(|ctx_param| { ctx_param.bounds.iter().any(|bound| match bound { TypeParamBound::Trait(trait_bound) => trait_bound.path.segments.last().is_some_and(|segment| segment.ident == "DeriveCtx"), @@ -843,20 +859,40 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }) }); let derive_routing = derives && routing_generic.is_some(); + // A kernel that holds records (a forwarded routing wire, or a lazy edge it + // serves itself) names the record lifetime; unless it declared a serving + // lifetime of its own, the context binds the arena at that lifetime. + let kernel_lazy = parsed.fields.iter().any(|field| !field.is_data_field && matches!(field.ty, ParsedFieldType::Node(_))); + let wants_record_lifetime = routing_generic.is_some() || ((record_io || flip) && kernel_lazy); + let ctx_declares_arena = ctx_param.is_some_and(|ctx_param| ctx_param.bounds.iter().any(extracts_arena)); + let bind_record_arena = wants_record_lifetime && !ctx_declares_arena; + if bind_record_arena { + ctx_bounds.push(quote!(#core_types::context::ExtractArena)); + } let ctx_generic = match ctx_bounds.is_empty() { true => quote!(#ctx_ident), false => quote!(#ctx_ident: #(#ctx_bounds)+*), }; + let impl_ctx_generic = match impl_ctx_bounds.is_empty() { + true => quote!(#ctx_ident), + false => quote!(#ctx_ident: #(#impl_ctx_bounds)+*), + }; let generic_tokens = |param: &GenericParam| match param { GenericParam::Type(type_param) if Some(&type_param.ident) == ctx_param.map(|ctx_param| &ctx_param.ident) => ctx_generic.clone(), param => quote!(#param), }; + let impl_generic_tokens = |param: &GenericParam| match param { + GenericParam::Type(type_param) if Some(&type_param.ident) == ctx_param.map(|ctx_param| &ctx_param.ident) => impl_ctx_generic.clone(), + param => quote!(#param), + }; let mut generics: Vec = parsed .fn_generics .iter() .filter(|param| match param { - GenericParam::Type(type_param) => !derive_routing || Some(&type_param.ident) != routing_generic.as_ref(), + // A routing generic is the record itself, so the kernel names the + // record value rather than carrying the parameter. + GenericParam::Type(type_param) => Some(&type_param.ident) != routing_generic.as_ref(), _ => true, }) .map(|param| match param { @@ -886,52 +922,24 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn .iter() .filter(|param| match param { GenericParam::Type(type_param) => Some(&type_param.ident) != routing_generic.as_ref() && Some(&type_param.ident) != record_token.as_ref(), - // A serving lifetime stays only while the ctx bound constrains it - // (`ExtractArena<'e>`); wire types substitute its erased - // projection, which would leave it unconstrained. A flipped - // kernel's serving lifetime rebinds to the record lifetime, so - // the impl drops it entirely. - GenericParam::Lifetime(lifetime_param) => !flip && ctx_param.is_some_and(|ctx| quote!(#ctx).to_string().contains(&lifetime_param.lifetime.to_string())), + // A serving lifetime is the serve method's own, so it never rides + // the impl: an impl-level binding of the arena would contradict + // the one the method quantifies over. + GenericParam::Lifetime(_) => false, _ => true, }) - .map(&generic_tokens) + .map(&impl_generic_tokens) .collect(); if ctx_param.is_none() { generics.push(ctx_generic.clone()); - impl_generics.push(ctx_generic); - } - if routing_generic.is_some() || record_io || flip { - impl_generics.insert(0, quote!('__record)); - } - // A flipped kernel's serving lifetime is the record lifetime at the impl: - // the ctx bound rebinds under the impl's own name. - if flip { - let serving_names: Vec = parsed - .fn_generics - .iter() - .filter_map(|param| match param { - GenericParam::Lifetime(lifetime_param) => Some(lifetime_param.lifetime.ident.to_string()), - _ => None, - }) - .collect(); - if !serving_names.is_empty() { - impl_generics = impl_generics.into_iter().map(|tokens| crate::codegen::classify::rename_lifetimes_to_record(tokens, &serving_names)).collect(); - } + impl_generics.push(impl_ctx_generic.clone()); } let lazy_carrier = record_io && carrier_present && matches!(parsed.fields.iter().find(|field| !field.is_data_field).map(|field| &field.ty), Some(ParsedFieldType::Node(_))); - if derive_routing || (lazy_carrier && derives) { - generics.insert(0, quote!('__record)); - } let fn_name = &parsed.fn_name; let mod_name = format_ident!("_{}_mod", parsed.mod_name); let struct_name = format_ident!("{}Node", parsed.struct_name); let output_type = &parsed.output_type; - let trait_output = match (record_io, &routing_generic) { - (true, _) | (false, Some(_)) => syn::parse_quote!(#core_types::record::RecordValue<'__record>), - (false, None) if flip => syn::parse_quote!(#core_types::record::RecordValue<'__record>), - (false, None) => slot_value_type(&parsed.output_type), - }; let raw_lazy = matches!(*model, Dialect::Poll); let injected_name = |ident: &Ident| async_source && (ident == "_runtime" || ident == "_source"); let where_predicates: Vec = parsed.where_clause.iter().flat_map(|clause| clause.predicates.iter()).map(|predicate| quote!(#predicate)).collect(); @@ -965,49 +973,44 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }); } if flip { - let mut kernel_lazy = false; for (index, field) in regular_fields.iter().enumerate() { if matches!(&field.ty, ParsedFieldType::Node(_)) { - kernel_lazy = true; let source_generic = format_ident!("__Source{index}"); let derived_extra = derives - .then(|| quote!(+ for<'__derived> #core_types::record::RecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>)) + .then(|| quote!(+ for<'__derived> #core_types::record::DerivedRecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>)) .into_iter(); generics.push(quote! { - #source_generic: #core_types::node::Node<#ctx_ident, Output = #core_types::record::RecordValue<'__record>> #(#derived_extra)* + #source_generic: #core_types::node::Node<#ctx_ident> #(#derived_extra)* }); } } - if kernel_lazy { - generics.insert(0, quote!('__record)); - } } if record_io { - let mut kernel_lazy = false; for (index, field) in regular_fields.iter().enumerate() { if matches!(&field.ty, ParsedFieldType::Node(_)) && matches!(crate::codegen::ir::lazy_binding(&node, index), LazyBinding::Element) { - kernel_lazy = true; let source_generic = format_ident!("__Source{index}"); let derived_extra = derives - .then(|| quote!(+ for<'__derived> #core_types::record::RecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>)) + .then(|| quote!(+ for<'__derived> #core_types::record::DerivedRecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>)) .into_iter(); generics.push(quote! { - #source_generic: #core_types::node::Node<#ctx_ident, Output = #core_types::record::RecordValue<'__record>> #(#derived_extra)* + #source_generic: #core_types::node::Node<#ctx_ident> #(#derived_extra)* }); } } - if kernel_lazy && !(derive_routing || (lazy_carrier && derives)) { - generics.insert(0, quote!('__record)); - } } if opaque { for (index, field) in regular_fields.iter().enumerate() { - if let ParsedFieldType::Node(NodeParsedField { output_type, .. }) = &field.ty { + if matches!(&field.ty, ParsedFieldType::Node(_)) { let source_generic = format_ident!("__Source{index}"); - generics.push(quote!(#source_generic: #core_types::node::Node<#ctx_ident, Output = #output_type>)); + generics.push(quote!(#source_generic: #core_types::node::Node<#ctx_ident>)); } } } + // The record lifetime the kernel's wire types and arena bound name; the + // impl infers it from the serving lifetime at every call. + if wants_record_lifetime { + generics.insert(0, quote!('__record)); + } let data_names: Vec<&Ident> = data_fields.iter().map(|field| &field.pat_ident.ident).collect(); let data_params = data_fields.iter().map(|field| { @@ -1018,9 +1021,13 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote!(#pat: &#ty) }); - let lazy_bound = |output_type: &Type| match derives { - true => quote!(for<'__derived> #core_types::node::Node<#core_types::context::Derived<'__derived, #ctx_ident>, Output = #output_type>), - false => quote!(#core_types::node::Node<#ctx_ident, Output = #output_type>), + let derived_edge = quote!(for<'__derived> #core_types::record::DerivedRecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>); + let lazy_bound = || match derives { + true => { + let derived_edge = derived_edge.clone(); + quote!(#core_types::node::Node<#ctx_ident> + #derived_edge) + } + false => quote!(#core_types::node::Node<#ctx_ident>), }; let routing_source = |ty: &Type| routing_generic.as_ref().is_some_and(|generic| crate::codegen::classify::routing_source_output(ty, generic)); @@ -1074,6 +1081,8 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn (None, false) => quote!(#pat: #core_types::node::List<'_, #ty>), } } + // A routing source is the forwarded record itself, not an element. + ParsedFieldType::Regular(RegularParsedField { ty, .. }) if routing_source(ty) => quote!(#pat: #core_types::record::RecordValue<'__record>), ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => quote!(#pat: &#ty), ParsedFieldType::Regular(RegularParsedField { ty, .. }) if !field.attribute_reads.is_empty() => read_tuple_param(field, quote!(#pat), quote!(#ty)), ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#pat: #ty), @@ -1094,12 +1103,12 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let out = lazy_read_out(field, output_type); quote!(#pat: #core_types::record::ElementLazyInput<'_, #out, #source_generic>) } - (LazyBinding::Plain, true) => { - let bound = lazy_bound(output_type); + (LazyBinding::Generic, true) => { + let bound = lazy_bound(); quote!(#pat: &impl #bound) } - (LazyBinding::Plain, false) => { - let bound = lazy_bound(output_type); + (LazyBinding::Generic, false) => { + let bound = lazy_bound(); quote!(#pat: #core_types::node::LazyInput<'_, impl #bound>) } } @@ -1107,63 +1116,48 @@ 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! { - #node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>, - #node_generic: for<'__derived> #core_types::record::RecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>> + let node_bounds = regular_fields.iter().enumerate().zip(&node_generics).map(|((index, field), node_generic)| { + let plain = quote!(#node_generic: #core_types::node::Node<#ctx_ident>); + // A lazy edge the kernel evaluates at derived contexts needs the + // derived form: the derived context's arena binding is unnameable + // under a higher rank. + let derived = quote!(#node_generic: #derived_edge); + let derived_plus = quote! { + #node_generic: #core_types::node::Node<#ctx_ident>, + #node_generic: #derived_edge + }; + match &field.ty { + ParsedFieldType::Node(_) if flip => match derives { + true => derived_plus, + false => plain, }, - false => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>), - }, - ParsedFieldType::Node(_) if record_io && !skips_carrier && index == 0 => match derives { - true => quote!(#node_generic: for<'__derived> #core_types::record::DerivedRecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>), - false => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>), - }, - ParsedFieldType::Regular(_) if record_io && !skips_carrier && index == 0 => { - quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>) - } - ParsedFieldType::Regular(_) if record_io && !field.attribute_reads.is_empty() => { - quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>) - } - // An element-consuming lazy secondary rides a record edge, derivable - // when the kernel evaluates it at derived contexts. - ParsedFieldType::Node(_) if record_io && matches!(ir::lazy_binding(&node, index), LazyBinding::Element) => match derives { - true => quote! { - #node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>, - #node_generic: for<'__derived> #core_types::record::RecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>> + ParsedFieldType::Node(_) if record_io && !skips_carrier && index == 0 => match derives { + true => derived, + false => plain, }, - false => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>), - }, - ParsedFieldType::Regular(RegularParsedField { ty, .. }) if routing_source(ty) => { - quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>) - } - ParsedFieldType::Regular(_) if routing_generic.is_some() => { - 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>>), - false => { - let bound = lazy_bound(&record_value_ty); + // An element-consuming lazy secondary rides a record edge, derivable + // when the kernel evaluates it at derived contexts. + ParsedFieldType::Node(_) if record_io && matches!(ir::lazy_binding(&node, index), LazyBinding::Element) => match derives { + true => derived_plus, + false => plain, + }, + ParsedFieldType::Node(NodeParsedField { output_type, .. }) if routing_source(output_type) => match derives { + true => derived, + false => plain, + }, + ParsedFieldType::Node(_) if opaque => plain, + ParsedFieldType::Node(_) => { + let bound = lazy_bound(); quote!(#node_generic: #bound) } - }, - ParsedFieldType::Node(NodeParsedField { output_type, .. }) if opaque && is_record_value(output_type) => { - quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #output_type>) - } - ParsedFieldType::Node(NodeParsedField { output_type, .. }) => { - let bound = lazy_bound(output_type); - quote!(#node_generic: #bound) + // Every wire is a record edge; a value input's element copies out of + // the record its edge serves. + ParsedFieldType::Regular(_) => plain, } }); let mut lend_outlives: Vec = Vec::new(); - if let Type::Reference(reference) = &trait_output + if let Type::Reference(reference) = &slot_value_type(&parsed.output_type) && let Some(lifetime) = &reference.lifetime { let inner = &reference.elem; @@ -1224,29 +1218,17 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn .collect::>() }; - // A serving node's interrupt exits close through the frame claim's drop; - // a forwarding node still closes by hand, since its kernel serves the - // record and a top-level claim would double the frame. - let claims_frame = flip || record_io; - let interrupt_close = match claims_frame { - true => quote!(), - false => quote! { - unsafe { #core_types::record::interrupt_frame(_entry_sp, >::layout(self)) }; - }, + // Every exit closes through the frame claim's drop, so no exit path + // reclaims by hand. + let interrupt_close = quote!(); + let frame_entry = quote! { + #[allow(unused_mut, unused_variables)] + let mut __frame = __slot; }; - let frame_entry = match claims_frame { - true => quote! { - #[allow(unused_mut, unused_variables)] - let mut __frame = #core_types::record::FrameClaim::enter(>::layout(self)); - }, - false => quote!(let _entry_sp = #core_types::record::stack::sp();), - }; - let lane_frame_entry = match claims_frame { - true => quote! { - #[allow(unused_mut, unused_variables)] - let mut __frame = #core_types::record::FrameClaim::enter(__node_layout); - }, - false => quote!(let _entry_sp = #core_types::record::stack::sp();), + // The batch loop is not a serve, so each lane claims its own frame. + let lane_frame_entry = quote! { + #[allow(unused_mut, unused_variables)] + let mut __frame = #core_types::record::FrameClaim::enter(__node_layout); }; let bind_body = |index: usize, field: &ParsedField, batch_mode: bool| { let name = &field.pat_ident.ident; @@ -1400,16 +1382,29 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let #name: #ty = unsafe { #core_types::record::read_element(self.#slot.rec(&#name)) }; } } - ValueBinding::Plain => quote! { - let #name = match __cell.eval_input(#index, &self.#name, __input) { - Ok(value) => value, - Err(interrupt) => #interrupt_return, - }; - }, + // A plain value rides a record edge like every other input; the + // element copies out against the edge's own layout, except for a + // routing source, whose record is what the kernel forwards. + ValueBinding::Plain => { + let read = (!routing_source(ty)).then(|| { + quote! { + let #name: #ty = unsafe { + #core_types::record::read_element(#core_types::node::Node::<#ctx_ident>::layout(&self.#name).rec(&#name)) + }; + } + }); + quote! { + let #name = match __cell.eval_input(#index, &self.#name, __input) { + Ok(value) => value, + Err(interrupt) => #interrupt_return, + }; + #read + } + } }, ParsedFieldType::Node(NodeParsedField { output_type, .. }) => match (ir::lazy_binding(&node, index), raw_lazy) { // A raw poll edge is threaded straight through, so it does not bind here. - (LazyBinding::Plain, true) => quote!(), + (LazyBinding::Generic, true) => quote!(), (LazyBinding::DeriveRouting, _) => quote! { let #name = #core_types::record::RecordLazyInput::new(&self.#name, &__cell, #index, self.__layout.depth.saturating_sub(#pushed_levels)); }, @@ -1462,13 +1457,20 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn (LazyBinding::OpaqueRecord, _) => quote! { let #name = #core_types::record::RecordEdgeInput::new(&self.#name, &self.__layout); }, - (LazyBinding::Plain, false) => quote! { + (LazyBinding::Generic, false) => quote! { let #name = #core_types::node::LazyInput::new(&self.#name, &__cell, #index); }, }, } }; + // A bind whose element copies out reclaims the edge's frame; a forwarded + // record must outlive the bind, so its frame stays. + let reads_out_at = |index: usize| match ®ular_fields[index].ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => !routing_source(ty) && ir::value_binding(&node, index).reads_out(), + ParsedFieldType::Node(_) => false, + }; + let clamp_tokens = |field: &ParsedField| { let ParsedFieldType::Regular(RegularParsedField { number_hard_min, number_hard_max, .. }) = &field.ty else { return None; @@ -1493,7 +1495,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn ParsedFieldType::Regular(_) => quote!(#name), ParsedFieldType::Node(_) => match (ir::lazy_binding(&node, index), raw_lazy) { (LazyBinding::Element, true) | (LazyBinding::OpaqueRecord, _) => quote!(&#name), - (LazyBinding::Plain, true) => quote!(&self.#name), + (LazyBinding::Generic, true) => quote!(&self.#name), _ => quote!(#name), }, } @@ -1559,23 +1561,28 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let #arg = #core_types::extent::ListIn::new(&#query, &__total); } } - ValueBinding::RecordElement | ValueBinding::ReadingSecondary => { - let slot = format_ident!("__in_{index}"); + // A routing source forwards its record whole; its extents are + // the queryable quantity. + _ if routing_source(ty) => extent_edge(&query, &arg), + ValueBinding::RecordElement | ValueBinding::ReadingSecondary | ValueBinding::Plain => { + let layout = match ir::value_binding(&node, index) { + ValueBinding::Plain => quote!(#core_types::node::Node::<#ctx_ident>::layout(&self.#name)), + _ => { + let slot = format_ident!("__in_{index}"); + quote!(self.#slot) + } + }; quote! { let #query = || { // SAFETY: the element copies out by value; extent // queries leave the record stack untouched. let __scope = unsafe { #core_types::record::stack::ScopeGuard::enter() }; - #core_types::node::Node::eval(&self.#name, __input) - .map(|__value| unsafe { #core_types::record::read_element::<#ty>(self.#slot.rec(&__value)) }) + #core_types::record::serve_edge(&self.#name, __input) + .map(|__value| unsafe { #core_types::record::read_element::<#ty>(#layout.rec(&__value)) }) }; let #arg = #core_types::extent::ValueIn::new(&#query); } } - ValueBinding::Plain => quote! { - let #query = || #core_types::node::Node::eval(&self.#name, __input); - let #arg = #core_types::extent::ValueIn::new(&#query); - }, // A carrier, lent, or materialized ranked input is a record // edge; its extents are the queryable quantity. _ => extent_edge(&query, &arg), @@ -1585,7 +1592,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn arg_names.push(arg); } quote! { - fn extent_at(&self, __input: &#ctx_ident, __level: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> { + fn extent_at<'__serve>(&self, __input: &#ctx_ident, __level: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> + where + #ctx_ident: #core_types::context::ExtractArena, + { #(#arg_decls)* let __level_in = #core_types::extent::LevelIn::new(__level, >::layout(self).depth); #path(#(#arg_names,)* __level_in) @@ -1593,7 +1603,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } } else if let Some(path) = &parsed.attributes.extent_raw { quote! { - fn extent_at(&self, __input: &#ctx_ident, __level: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> { + fn extent_at<'__serve>(&self, __input: &#ctx_ident, __level: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> + where + #ctx_ident: #core_types::context::ExtractArena, + { #path(self, __input, __level) } } @@ -1620,7 +1633,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }, }; quote! { - fn extent_at(&self, __input: &#ctx_ident, __level: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> { + fn extent_at<'__serve>(&self, __input: &#ctx_ident, __level: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> + where + #ctx_ident: #core_types::context::ExtractArena, + { #query let __arg = #core_types::extent::ExtentIn::new(&__query); let __level_in = #core_types::extent::LevelIn::new(__level, >::layout(self).depth); @@ -1631,7 +1647,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn // A leveled output without an extent fn reports a lower bound; // consumers size it by draining to the past-end signal. quote! { - fn extent_at(&self, _: &#ctx_ident, _: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> { + fn extent_at<'__serve>(&self, _: &#ctx_ident, _: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> + where + #ctx_ident: #core_types::context::ExtractArena, + { #core_types::gpoll::GPoll::Final(#core_types::gpoll::Extent::AtLeast(0)) } } @@ -1652,14 +1671,14 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }; let batch_signature = quote! { - fn eval_batch<'__batch>( + fn eval_batch<'__batch, '__serve>( &'__batch self, __input: &'__batch #ctx_ident, __range: ::std::ops::Range, __scratch: Option<&'__batch mut [::std::mem::MaybeUninit]>, ) -> #core_types::node::BatchStatus<'__batch> where - #ctx_ident: #core_types::context::InjectIndex + Copy, + #ctx_ident: #core_types::context::InjectIndex + Copy + #core_types::context::ExtractArena, }; let produces_records = record_io || routing_generic.is_some() || flip; @@ -1678,18 +1697,20 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn .flatten(); let lane_lifetime = lane_injected.is_some().then(|| quote!('__lane,)); let kernel_output = lane_injected.or(attr_injected); - let kernel_output = match derive_routing { + let kernel_output = match routing_generic.is_some() { true => { - let generic = routing_generic.as_ref().expect("derive routing implies routing"); + let generic = routing_generic.as_ref().expect("guarded by the arm"); let ty = substitute_routing_record(&parsed.output_type, generic, core_types); quote!(#ty) } false => kernel_output.map(|ty| quote!(#ty)).unwrap_or_else(|| quote!(#output_type)), }; + let claim_param = parsed.claim.iter().map(|claim| quote!(, #claim)); + let claim_arg = parsed.claim.iter().map(|_| quote!(, __frame)); let kernel = match async_fn { false => quote! { #[allow(clippy::too_many_arguments, clippy::type_complexity)] - #vis fn #fn_name<#attr_lifetime #lane_lifetime #(#generics,)*>(#ctx_pat: &#ctx_ident #(, #data_params)* #(, #kernel_params)*) -> #kernel_output #fn_where #body + #vis fn #fn_name<#attr_lifetime #lane_lifetime #(#generics,)*>(#ctx_pat: &#ctx_ident #(, #data_params)* #(, #kernel_params)* #(#claim_param)*) -> #kernel_output #fn_where #body }, true => { let kernel_generics = parsed.fn_generics.iter().filter(|param| match param { @@ -1722,16 +1743,34 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn true => quote!(#core_types::node::StatusCell::no_partial()), false => quote!(#core_types::node::StatusCell::new()), }; - let kernel_call = quote!(self::#fn_name(__input #(, &self.#data_names)* #(, #call_args)*)); + let kernel_call = quote!(self::#fn_name(__input #(, &self.#data_names)* #(, #call_args)* #(#claim_arg)*)); + // A record-opaque kernel serves through the claim it was handed; every + // other forwarding kernel returns a record of this node's layout, which + // fills the claim. + let forwarded = |value: TokenStream2| match opaque { + true => value, + // SAFETY: the kernel's record is of this node's layout. + false => quote!(unsafe { __frame.forward(&#value) }), + }; let lift = match *model { - Dialect::Interrupt => quote! { - match #kernel_call { - Ok(value) => __cell.finish(value), - Err(interrupt) => { #interrupt_close interrupt.into() } + Dialect::Interrupt => { + let served = forwarded(quote!(value)); + quote! { + match #kernel_call { + Ok(value) => __cell.finish(#served), + Err(interrupt) => { #interrupt_close interrupt.into() } + } } + } + Dialect::Poll => match opaque { + true => quote!(__cell.merge(#kernel_call)), + // SAFETY: the kernel's record is of this node's layout. + false => quote!(__cell.merge(#kernel_call).map(|value| unsafe { __frame.forward(&value) })), }, - Dialect::Poll => quote!(__cell.merge(#kernel_call)), - _ => quote!(__cell.finish(#kernel_call)), + _ => { + let served = forwarded(quote!(#kernel_call)); + quote!(__cell.finish(#served)) + } }; let placeholder_value_names: Vec<&Ident> = kernel_fields @@ -1764,19 +1803,13 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn #clamp } }); - // Async slots persist plain values across evaluations; a flipped source - // lifts the slot value onto its record wire at every merge point, into - // the carried frame when the node has a carrier. - let merge_lifted = |poll: TokenStream2| match flip { - true => quote!(__cell.merge(__frame.lift(#poll, #core_types::context::ExtractArena::arena(__input)))), - false => quote!(__cell.merge(#poll)), - }; - let pending_return = match flip && carrier_flip { - true => quote! { - __frame.lift::<#slot_ty>(#core_types::gpoll::GPoll::Pending, #core_types::context::ExtractArena::arena(__input)) - }, - false => quote!({ #interrupt_close #core_types::gpoll::GPoll::Pending }), - }; + // Async slots persist plain values across evaluations; the source lifts + // the slot value onto its record wire at every merge point, into the + // carried frame when the node has a carrier. + let merge_lifted = |poll: TokenStream2| quote!(__cell.merge(__frame.lift_served(#poll, #core_types::context::ExtractArena::arena(__input)))); + // The claim drops with the frame still claimed, so a valueless exit needs + // no closing of its own. + let pending_return = quote!(#core_types::gpoll::GPoll::Pending); let inflight = match &parsed.attributes.placeholder { Some(path) => merge_lifted(quote!(#core_types::gpoll::GPoll::Partial(#path(#(&#placeholder_value_names),*)))), None => pending_return.clone(), @@ -1951,7 +1984,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn #element_store #(#attr_stores)* // SAFETY: the carry and the writes above complete the record. - let __value = unsafe { __frame.finish() }; + let __value = unsafe { __frame.finish_served() }; } }); let record_tail = record_tail_core.clone().map(|core| { @@ -1965,7 +1998,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let prelude = carried_prelude.clone().unwrap_or_default(); return quote! { #prelude - __cell.merge(__frame.lift(#kernel_call, #core_types::context::ExtractArena::arena(__input))) + __cell.merge(__frame.lift_served(#kernel_call, #core_types::context::ExtractArena::arena(__input))) }; } let kernel_value = match *model { @@ -1981,7 +2014,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote! { #prelude let __kernel_value = #kernel_value; - __cell.merge(__frame.lift(#core_types::gpoll::GPoll::Final(__kernel_value), #core_types::context::ExtractArena::arena(__input))) + __cell.merge(__frame.lift_served(#core_types::gpoll::GPoll::Final(__kernel_value), #core_types::context::ExtractArena::arena(__input))) } }); let tail_form = if async_fn { @@ -2055,14 +2088,16 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn // base lane, so the per-lane loop runs only the kernel and the carrier; // eager inputs are batch-invariant by contract (per-lane variance rides // lazy carriers). + // The fill loop copies each lane's frame out, so it needs the record, not + // the serving proof. let hoisted_lane_poll = match tail_form { Tail::Record => record_tail_core.clone().map(|core| { quote! { #core - let __poll = __cell.finish(__value); + let __poll = __cell.finish(__value).map(#core_types::record::Served::value); } }), - Tail::Forward if routing_generic.is_some() => Some(quote!(let __poll = #lift;)), + Tail::Forward if routing_generic.is_some() => Some(quote!(let __poll = #lift.map(#core_types::record::Served::value);)), _ => None, }; // A serving-lifetime element rides the per-lane fill loop: the hoisted @@ -2091,7 +2126,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn .filter(|(index, field)| matches!(field.ty, ParsedFieldType::Regular(_)) && hoists(*index)) .map(|(index, field)| { let body = bind_body(index, field, true); - match ir::value_binding(&node, index).reads_out() { + match reads_out_at(index) { false => body, true => { let mark = format_ident!("__scope_{index}"); @@ -2269,12 +2304,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }; let record_bounds: Vec = { - let arena_bound = (record_io && !ctx_extracts_arena && (skips_carrier || lazy_carrier || element_write.is_some())) || (!record_io && (derive_routing || flip)); - let mut bounds = if arena_bound { - vec![quote!(#ctx_ident: #core_types::context::ExtractArena)] - } else { - Vec::new() - }; + // The serving lifetime is the serve method's, so the arena binding + // rides there rather than on the impl. + let mut bounds: Vec = Vec::new(); // A reading secondary input's element copies out of its record, as // does a concrete carrier read. if record_io { @@ -2566,7 +2598,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let eval_body = eval_steps.iter().map(|step| match step { EvalStep::Bind(index, field) => { let body = bind_body(*index, field, false); - let reads_out = matches!(&field.ty, ParsedFieldType::Regular(_)) && ir::value_binding(&node, *index).reads_out(); + let reads_out = reads_out_at(*index); match reads_out { false => body, true => { @@ -2597,9 +2629,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn #(#flip_bounds,)* #(#where_predicates,)* { - type Output = #trait_output; - - fn eval(&self, __input: &#ctx_ident) -> #core_types::gpoll::GPoll { + fn serve<'__serve, '__slot>(&self, __input: &#ctx_ident, __slot: #core_types::record::FrameClaim<'__slot>) -> #core_types::gpoll::GPoll<#core_types::record::Served<'__serve>> + where + #ctx_ident: #core_types::context::ExtractArena, + { // The exit trace rides a guard so early returns report too, which // is what pins a frame leak to its node. #[cfg(debug_assertions)] diff --git a/node-graph/node-macro/src/codegen/classify.rs b/node-graph/node-macro/src/codegen/classify.rs index e2d9f55f8a..3d77630a7a 100644 --- a/node-graph/node-macro/src/codegen/classify.rs +++ b/node-graph/node-macro/src/codegen/classify.rs @@ -304,7 +304,7 @@ pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option { // Lazy secondaries are consumed as plain elements; raw record edges and // ranked outputs have no element binding here. let unsupported_lazy_secondary = |field: &ParsedField| match &field.ty { - ParsedFieldType::Node(NodeParsedField { output_type, .. }) => is_record_value(output_type) || crate::codegen::ir::strip_ilist(output_type).1 > 0 || !field.attribute_reads.is_empty(), + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => is_served(output_type) || crate::codegen::ir::strip_ilist(output_type).1 > 0 || !field.attribute_reads.is_empty(), ParsedFieldType::Regular(_) => false, }; if parsed.fields.iter().skip(lazy_carrier as usize).any(|field| unsupported_lazy_secondary(field)) { @@ -503,15 +503,17 @@ pub(crate) fn generic_assignment(field_ty: &Type, row_ty: &Type, generic: &Ident }) } -pub(crate) fn is_record_value(ty: &Type) -> bool { - matches!(ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "RecordValue")) +/// A whole-record position: the served proof a record-opaque kernel hands +/// back, or the subject it receives without naming an element. +pub(crate) fn is_served(ty: &Type) -> bool { + matches!(ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "Served")) } -/// Whether a kernel operates on whole records: it names `RecordValue` in its -/// output, receives raw record edges paired with the node's layout, and +/// Whether a kernel operates on whole records: it serves through the claim it +/// was handed, receives raw record edges paired with the node's layout, and /// takes on the record APIs' unsafe contracts itself. pub(crate) fn record_opaque(parsed: &ParsedNodeFn) -> bool { - is_record_value(&slot_value_type(&parsed.output_type)) + is_served(&slot_value_type(&parsed.output_type)) } pub(crate) fn routing_io(parsed: &ParsedNodeFn) -> Option { @@ -739,45 +741,9 @@ pub(crate) fn named_serving_lifetime(ty: &Type) -> Option { visitor.found } +/// Rewrites a kernel-declared `ExtractArena<'e>` bound into the equality the +/// trait names. pub(crate) fn desugar_extract_lifetime(bound: &TypeParamBound, core_types: &TokenStream2) -> TokenStream2 { - desugar_extract_lifetime_at(bound, core_types, None) -} - -/// Renames every occurrence of the named lifetimes to `'__record` in a token -/// stream: a flipped kernel's serving lifetime is the record lifetime at the -/// impl, under whichever name the author picked. -pub(crate) fn rename_lifetimes_to_record(stream: TokenStream2, names: &[String]) -> TokenStream2 { - use proc_macro2::{Group, TokenTree}; - let mut out = Vec::new(); - let mut tokens = stream.into_iter().peekable(); - while let Some(token) = tokens.next() { - match token { - TokenTree::Group(group) => { - let renamed = rename_lifetimes_to_record(group.stream(), names); - let mut fresh = Group::new(group.delimiter(), renamed); - fresh.set_span(group.span()); - out.push(TokenTree::Group(fresh)); - } - TokenTree::Punct(punct) if punct.as_char() == '\'' => { - match tokens.peek() { - Some(TokenTree::Ident(ident)) if names.iter().any(|name| ident == name) => { - let span = ident.span(); - tokens.next(); - out.push(TokenTree::Punct(punct)); - out.push(TokenTree::Ident(proc_macro2::Ident::new("__record", span))); - } - _ => out.push(TokenTree::Punct(punct)), - } - } - token => out.push(token), - } - } - out.into_iter().collect() -} - -/// As [`desugar_extract_lifetime`], with the arena lifetime overridden: a -/// flipped kernel's serving lifetime is the record lifetime at the impl. -pub(crate) fn desugar_extract_lifetime_at(bound: &TypeParamBound, core_types: &TokenStream2, at: Option) -> TokenStream2 { let TypeParamBound::Trait(trait_bound) = bound else { return quote!(#bound); }; @@ -796,7 +762,6 @@ pub(crate) fn desugar_extract_lifetime_at(bound: &TypeParamBound, core_types: &T let Some(GenericArgument::Lifetime(lifetime)) = args.args.first() else { return quote!(#bound); }; - let lifetime = at.unwrap_or_else(|| quote!(#lifetime)); quote!(#core_types::context::ExtractArena) } diff --git a/node-graph/node-macro/src/codegen/entries.rs b/node-graph/node-macro/src/codegen/entries.rs index 2cec820114..3c961ec505 100644 --- a/node-graph/node-macro/src/codegen/entries.rs +++ b/node-graph/node-macro/src/codegen/entries.rs @@ -359,12 +359,9 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields let #layout = #handle.layout().clone(); let #name = #handle.downcast_record::<#value_ty>()?; }, - SlotKind::Extracted(value_ty) => quote! { - let #handle = inputs.next().unwrap(); - let #layout = #handle.layout().clone(); - let #name = gcore::record::RecordExtract::<#value_ty, _>::new(#handle.downcast_record::<#value_ty>()?, &#layout); - }, - SlotKind::Ranked(value_ty) => quote! { + // The node reads the element off the edge's own layout, so + // neither slot rides a layout to the constructor. + SlotKind::Extracted(value_ty) | SlotKind::Ranked(value_ty) => quote! { let #name = inputs.next().unwrap().downcast_record::<#value_ty>()?; }, } diff --git a/node-graph/node-macro/src/codegen/ir.rs b/node-graph/node-macro/src/codegen/ir.rs index 34ff62b6db..08c491506c 100644 --- a/node-graph/node-macro/src/codegen/ir.rs +++ b/node-graph/node-macro/src/codegen/ir.rs @@ -2,7 +2,7 @@ #![allow(dead_code)] use crate::codegen::classify::{ - Dialect, RoutingIo, bare_ident, context_param, dialect, flip_carrier, generic_assignment, generic_extractable, is_record_value, record_shape, routing_io, slot_value_type, + Dialect, RoutingIo, bare_ident, context_param, dialect, flip_carrier, generic_assignment, generic_extractable, is_served, record_shape, routing_io, slot_value_type, }; use crate::codegen::entries::implementation_rows; use crate::parsing::{AttributeRead, NodeParsedField, ParsedField, ParsedFieldType, ParsedNodeFn, RecordWrites, RegularParsedField, record_writes}; @@ -77,7 +77,7 @@ fn inputs(parsed: &ParsedNodeFn, fields: &[&ParsedField], generics: &[Ident]) -> fn subject(index: usize, field: &ParsedField, carrier_subject: bool, routing: Option<&RoutingIo>) -> bool { match &field.ty { ParsedFieldType::Node(NodeParsedField { output_type, .. }) => { - is_record_value(output_type) || routing.is_some_and(|routing| crate::codegen::classify::routing_source_output(output_type, &routing.generic)) || (index == 0 && carrier_subject) + is_served(output_type) || routing.is_some_and(|routing| crate::codegen::classify::routing_source_output(output_type, &routing.generic)) || (index == 0 && carrier_subject) } ParsedFieldType::Regular(RegularParsedField { ty, .. }) => routing.is_some_and(|routing| bare_ident(ty) == Some(&routing.generic)) || (index == 0 && carrier_subject), } @@ -166,7 +166,7 @@ fn item_shape(element: &Type, depth: u8, reads: &[AttributeRead], generics: &[Id } fn element_of(ty: &Type, generics: &[Ident]) -> Element { - if is_record_value(ty) { + if is_served(ty) { return Element::Opaque; } match bare_ident(ty) { @@ -371,10 +371,11 @@ pub(crate) enum ValueBinding { } /// How a lazy (`impl Node`) input binds in eval. The `Poll` effect further -/// selects the borrowed vs `__cell`-driven form within `Element`/`Plain`. +/// selects the borrowed vs `__cell`-driven form within `Element`/`Generic`. pub(crate) enum LazyBinding { Element, - Plain, + /// The kernel holds the whole record behind a bare generic element. + Generic, DeriveRouting, DeriveCarrier, OpaqueRecord, @@ -383,7 +384,7 @@ pub(crate) enum LazyBinding { impl ValueBinding { /// Copies an element out of a record edge, so the frame is reclaimed after. pub(crate) fn reads_out(&self) -> bool { - matches!(self, ValueBinding::ReadingSecondary | ValueBinding::RecordElement) + matches!(self, ValueBinding::ReadingSecondary | ValueBinding::RecordElement | ValueBinding::Plain) } } @@ -484,7 +485,7 @@ pub(crate) fn lazy_binding(node: &Node, index: usize) -> LazyBinding { } else if matches!(input.shape.element, Element::Opaque) { LazyBinding::OpaqueRecord } else { - LazyBinding::Plain + LazyBinding::Generic } } @@ -654,7 +655,7 @@ mod tests { } else if kinds.opaque { let record = fields .iter() - .position(|field| matches!(&field.ty, ParsedFieldType::Node(NodeParsedField { output_type, .. }) if is_record_value(output_type))); + .position(|field| matches!(&field.ty, ParsedFieldType::Node(NodeParsedField { output_type, .. }) if is_served(output_type))); Facts { sources: record.into_iter().collect(), carried: true, @@ -770,7 +771,7 @@ mod tests { assert_bridge( quote!(category("")), quote! { - fn memo<'e>(_: impl Ctx, #[data] cache: Store, content: impl Node, Output = RecordValue<'e>>) -> GPoll> { content.eval(()) } + fn memo<'e, 'l>(_: impl Ctx, #[data] cache: Store, content: impl Node>, slot: FrameClaim<'l>) -> GPoll> { content.serve(&(), slot) } }, ); } @@ -819,7 +820,7 @@ mod tests { "flip-raw" } else if flip { "flip-lazy" - } else if opaque && raw && is_record_value(output_type) { + } else if opaque && raw && is_served(output_type) { "opaque-record" } else if raw { "raw-lazy" @@ -846,8 +847,8 @@ mod tests { (LazyBinding::OpaqueRecord, _) => "opaque-record", (LazyBinding::Element, true) => "flip-raw", (LazyBinding::Element, false) => "flip-lazy", - (LazyBinding::Plain, true) => "raw-lazy", - (LazyBinding::Plain, false) => "lazy", + (LazyBinding::Generic, true) => "raw-lazy", + (LazyBinding::Generic, false) => "lazy", }, } } @@ -1012,8 +1013,8 @@ mod tests { assert_bindings( quote!(category("")), quote!( - fn memo<'e>(_: impl Ctx, #[data] cache: Store, content: impl Node, Output = RecordValue<'e>>) -> GPoll> { - content.eval(()) + fn memo<'e, 'l>(_: impl Ctx, #[data] cache: Store, content: impl Node>, slot: FrameClaim<'l>) -> GPoll> { + content.serve(&(), slot) } ), ); diff --git a/node-graph/node-macro/src/codegen/metadata.rs b/node-graph/node-macro/src/codegen/metadata.rs index 5e2396e2c8..19e54f4b11 100644 --- a/node-graph/node-macro/src/codegen/metadata.rs +++ b/node-graph/node-macro/src/codegen/metadata.rs @@ -17,7 +17,7 @@ pub(crate) fn generate_node_input_references( for (input_index, (parsed_input, input_ident)) in parsed.fields.iter().zip(field_idents).enumerate() { // `IList` nesting is rank metadata, not part of the value type. - let mut ty = match &parsed_input.ty { + let ty = match &parsed_input.ty { ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty.clone(), ParsedFieldType::Node(NodeParsedField { output_type, .. }) => crate::codegen::ir::strip_ilist(output_type).0, }; diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index 5a5348271d..d97252b858 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -36,6 +36,9 @@ pub(crate) struct ParsedNodeFn { pub(crate) output_depth: u8, pub(crate) is_async: bool, pub(crate) fields: Vec, + /// The caller's frame claim, declared by a record-opaque kernel that + /// serves through it; not a wired input. + pub(crate) claim: Option, pub(crate) body: TokenStream2, pub(crate) description: String, } @@ -685,7 +688,7 @@ pub(crate) fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Resu let fn_generics = input_fn.sig.generics.params.into_iter().collect(); let is_async = input_fn.sig.asyncness.is_some(); - let (input, fields) = parse_inputs(&input_fn.sig.inputs)?; + let (input, fields, claim) = parse_inputs(&input_fn.sig.inputs)?; let (output_type, output_depth) = crate::codegen::ir::strip_output_rank(&parse_output(&input_fn.sig.output)?); let where_clause = input_fn.sig.generics.where_clause; let body = input_fn.block.to_token_stream(); @@ -718,15 +721,17 @@ pub(crate) fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Resu output_depth, is_async, fields, + claim, where_clause, body, description, }) } -fn parse_inputs(inputs: &Punctuated) -> syn::Result<(Input, Vec)> { +fn parse_inputs(inputs: &Punctuated) -> syn::Result<(Input, Vec, Option)> { let mut fields = Vec::new(); let mut input = None; + let mut claim = None; for (index, arg) in inputs.iter().enumerate() { if let FnArg::Typed(PatType { pat, ty, attrs, .. }) = arg { @@ -765,6 +770,17 @@ fn parse_inputs(inputs: &Punctuated) -> syn::Result<(Input, Vec)`")); } + // The claim is the caller's, not an input: it reaches the kernel + // from the serve the node is lowered into. + if is_frame_claim(ty) { + claim = Some(PatType { + attrs: Vec::new(), + pat: pat.clone(), + colon_token: Default::default(), + ty: ty.clone(), + }); + continue; + } let field = parse_field(pat_ident.clone(), (**ty).clone(), attrs).map_err(|e| Error::new_spanned(pat_ident, format!("Failed to parse argument '{}': {}", pat_ident.ident, e)))?; fields.push(field); } else if let Pat::Tuple(pat_tuple) = &**pat { @@ -789,7 +805,13 @@ fn parse_inputs(inputs: &Punctuated) -> syn::Result<(Input, Vec bool { + matches!(ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "FrameClaim")) } /// Splits a lazy input's `Output = (T, Attr<..>..)` tuple into the element @@ -1129,9 +1151,10 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul )); } - let (input_type, output_type) = node_input_type - .zip(node_output_type) - .ok_or_else(|| Error::new_spanned(&ty, "Invalid Node type. Expected `impl Node`"))?; + let input_type = node_input_type.ok_or_else(|| Error::new_spanned(&ty, "Invalid Node type. Expected `impl Node` or `impl Node`"))?; + // A subject named without an output is a whole-record wire: the kernel + // serves it through its own claim rather than reading an element. + let output_type = node_output_type.unwrap_or_else(|| syn::parse_quote!(Served<'_>)); if !matches!(&value_source, ParsedValueSource::None) { return Err(Error::new_spanned(&ty, "No default values for `impl Node` allowed")); } @@ -1485,6 +1508,7 @@ mod tests { output_type: parse_quote!(f64), output_depth: 0, is_async: false, + claim: None, fields: vec![ParsedField { pat_ident: pat_ident("b"), name: None, @@ -1565,6 +1589,7 @@ mod tests { output_type: parse_quote!(T), output_depth: 0, is_async: false, + claim: None, fields: vec![ ParsedField { pat_ident: pat_ident("transform_target"), @@ -1660,6 +1685,7 @@ mod tests { output_type: parse_quote!(Vector), output_depth: 0, is_async: false, + claim: None, fields: vec![ParsedField { pat_ident: pat_ident("radius"), name: None, @@ -1736,6 +1762,7 @@ mod tests { output_type: parse_quote!(List>), output_depth: 0, is_async: false, + claim: None, fields: vec![ParsedField { pat_ident: pat_ident("shadows"), name: None, @@ -1824,6 +1851,7 @@ mod tests { output_type: parse_quote!(f64), output_depth: 0, is_async: false, + claim: None, fields: vec![ParsedField { pat_ident: pat_ident("b"), name: None, @@ -1915,6 +1943,7 @@ mod tests { output_type: parse_quote!(List>), output_depth: 0, is_async: true, + claim: None, fields: vec![ParsedField { pat_ident: pat_ident("path"), name: None, @@ -1991,6 +2020,7 @@ mod tests { output_type: parse_quote!(i32), output_depth: 0, is_async: false, + claim: None, fields: vec![], body: TokenStream2::new(), description: String::new(), diff --git a/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs b/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs index 3ff04d957f..022af2556f 100644 --- a/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs +++ b/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs @@ -318,6 +318,7 @@ impl PerPixelAdjustCodegen<'_> { }, output_type: raster_gpu, output_depth: 0, + claim: None, is_async: false, fields, body, diff --git a/node-graph/node-macro/src/validation.rs b/node-graph/node-macro/src/validation.rs index 275221ed90..a2384a9bac 100644 --- a/node-graph/node-macro/src/validation.rs +++ b/node-graph/node-macro/src/validation.rs @@ -54,7 +54,7 @@ fn validate_record_io(parsed: &ParsedNodeFn) { for field in parsed.fields.iter().skip(1) { if let ParsedFieldType::Node(NodeParsedField { output_type, .. }) = &field.ty { // Lazy secondaries are consumed as plain elements through the wire. - if crate::codegen::classify::is_record_value(output_type) || crate::codegen::ir::strip_ilist(output_type).1 > 0 { + if crate::codegen::classify::is_served(output_type) || crate::codegen::ir::strip_ilist(output_type).1 > 0 { emit_error!(field.pat_ident.span(), "a record node's lazy inputs consume plain elements, not record or ranked wires"); } if !field.attribute_reads.is_empty() { diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index 6e4045314b..2d195de7cd 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -3,7 +3,7 @@ use core_types::context::{Ctx, CtxSnapshot, DeriveCtx, ExtractAll, ModifyIndex}; use core_types::frame_table::{FrameTable, Lookup}; use core_types::gpoll::{Finality, GPoll}; use core_types::graphene_hash::CacheHash; -use core_types::record::{LevelStatus, OwnedRecord, RecordValue, claim_frame, copy_record_bytes, serve_frame}; +use core_types::record::{FrameClaim, LevelStatus, OwnedRecord, Served, copy_record_bytes}; use core_types::registry::cache_key; use std::sync::Arc; use std::sync::Mutex; @@ -29,12 +29,12 @@ pub struct MemoLevel { /// key normalizes the addressed lane away, so per-lane pulls share one /// materialization of the content instead of re-evaluating it per lane. #[node_macro::node(category("General"), path(graphene_core::memo))] -fn memoize<'e>( +fn memoize<'e, 'l>( ctx: impl Ctx + CacheHash + DeriveCtx + ExtractArena<'e> + ModifyIndex + Copy, #[data] cache: Arc>>, - content: impl Node, Output = RecordValue<'e>>, -) -> GPoll> { - let entry_sp = core_types::record::stack::sp(); + content: impl Node>, + slot: FrameClaim<'l>, +) -> GPoll> { // A scalar wire's value may depend on the consuming lane (index readers), // so only a leveled wire, whose level covers every lane by construction, // keys with the lane normalized away. @@ -51,32 +51,34 @@ fn memoize<'e>( } false => cache_key(&ctx), }; - let finalized = |value: RecordValue<'e>, finality: &Finality| match finality { + let finalized = |value: Served<'e>, finality: &Finality| match finality { Finality::AllFinal => GPoll::Final(value), Finality::Partial => GPoll::Partial(value), }; - let serve = |entry: &MemoLevel| { + // The claim is this node's output frame: a hit fills it from the cached + // bytes, and every valueless exit drops it with the frame still claimed. + let serve = |entry: &MemoLevel, mut slot: FrameClaim<'l>| { if lane >= entry.lanes.len() { // The cached level ends here; the past-end signal serves drains. - // The frame stays claimed on every exit, valueless ones included. - claim_frame(content.layout()); return GPoll::Error(Box::new(core_types::gpoll::GraphError::past_end())); } if entry.generation == ctx.arena().generation() { // SAFETY: within the generation the materialized batch stays live, // immutable, and laid out at the recorded stride. - let value = unsafe { serve_frame(content.layout(), (entry.frames + lane * entry.stride) as *const u8) }; - return finalized(value, &entry.finality); + unsafe { slot.fill_copy((entry.frames + lane * entry.stride) as *const u8) }; + // SAFETY: the copy images a complete record of this layout. + return finalized(unsafe { slot.finish_served() }, &entry.finality); } - match entry.lanes[lane].replay(content.layout(), ctx.arena()) { - Some(value) => finalized(value, &entry.finality), + match entry.lanes[lane].replay_into(&mut slot, ctx.arena()) { + // SAFETY: the replay completes the record in the frame. + Some(()) => finalized(unsafe { slot.finish_served() }, &entry.finality), None => GPoll::arena_exhausted(), } }; if let Some(entry) = cache.lock().unwrap().as_ref() && entry.key == key { - return serve(entry); + return serve(entry, slot); } if leveled { return match content.materialize_level(&ctx, ctx.arena()) { @@ -95,28 +97,19 @@ fn memoize<'e>( lanes, finality, }; - let result = serve(&entry); + let result = serve(&entry, slot); *cache.lock().unwrap() = Some(entry); result } - // A valueless materialization caches nothing, so the frames it left - // behind have no reader and must not be counted against this node. - LevelStatus::Pending => { - // SAFETY: nothing borrows the frames above the entry mark. - unsafe { core_types::record::interrupt_frame(entry_sp, content.layout()) }; - GPoll::Pending - } - LevelStatus::Error(error) => { - // SAFETY: nothing borrows the frames above the entry mark. - unsafe { core_types::record::interrupt_frame(entry_sp, content.layout()) }; - GPoll::Error(Box::new(error)) - } + LevelStatus::Pending => GPoll::Pending, + LevelStatus::Error(error) => GPoll::Error(Box::new(error)), }; } - let result = content.eval(&ctx); + // The output layout is the content's, so the claim is the content's frame. + let result = content.serve(&ctx, slot); let publishable = match &result { - GPoll::Final(value) => Some((value, Finality::AllFinal)), - GPoll::Partial(value) => Some((value, Finality::Partial)), + GPoll::Final(served) => Some((served.record(), Finality::AllFinal)), + GPoll::Partial(served) => Some((served.record(), Finality::Partial)), GPoll::Pending | GPoll::Fallback(_) | GPoll::Error(_) => None, }; if let Some((value, finality)) = publishable { @@ -124,7 +117,7 @@ fn memoize<'e>( let copy = unsafe { OwnedRecord::copy_out(content.layout(), content.layout().rec(value)) }; *cache.lock().unwrap() = Some(MemoLevel { key, - // A scalar record replays from the deep copy; the value the eval + // A scalar record replays from the deep copy; the value the serve // returned already lives in this frame. generation: u64::MAX, frames: 0, @@ -137,11 +130,12 @@ fn memoize<'e>( } #[node_macro::node(category(""), path(graphene_core::memo))] -fn frame_memo<'e>( +fn frame_memo<'e, 'l>( ctx: impl Ctx + CacheHash + ExtractArena<'e>, #[data] cell: ArenaCell, 32>>, - content: impl Node, Output = RecordValue<'e>>, -) -> GPoll> { + content: impl Node>, + frame: FrameClaim<'l>, +) -> GPoll> { let arena = ctx.arena(); let table = match cell.load(arena) { Some(table) => table, @@ -150,35 +144,39 @@ fn frame_memo<'e>( cell.store(weak); table } - None => return content.eval(&ctx), + None => return content.serve(&ctx, frame), }, }; // SAFETY: published bytes are same-frame copies of this edge's records, - // so they carry the edge's layout with live parked references. - let revive = |bytes: &'e Box<[u8]>| unsafe { serve_frame(content.layout(), bytes.as_ptr()) }; + // so they carry the edge's layout with live parked references, and the + // claim is that layout's frame. + let revive = |mut frame: FrameClaim<'l>, bytes: &Box<[u8]>| unsafe { + frame.fill_copy(bytes.as_ptr()); + frame.finish_served() + }; match table.lookup(cache_key(ctx)) { - Lookup::Hit(Finality::AllFinal, bytes) => GPoll::Final(revive(bytes)), - Lookup::Hit(Finality::Partial, bytes) => GPoll::Partial(revive(bytes)), - Lookup::Vacant(slot) => match content.eval(&ctx) { - GPoll::Final(value) => { + Lookup::Hit(Finality::AllFinal, bytes) => GPoll::Final(revive(frame, bytes)), + Lookup::Hit(Finality::Partial, bytes) => GPoll::Partial(revive(frame, bytes)), + Lookup::Vacant(slot) => match content.serve(&ctx, frame) { + GPoll::Final(served) => { // SAFETY: the value came from this edge, so it carries the edge's layout. - // The eval's own frame serves this pull; the publish feeds later ones. - let bytes = unsafe { copy_record_bytes(content.layout(), content.layout().rec(&value)) }; + // The serve's own frame answers this pull; the publish feeds later ones. + let bytes = unsafe { copy_record_bytes(content.layout(), content.layout().rec(served.record())) }; slot.publish(bytes, Finality::AllFinal); - GPoll::Final(value) + GPoll::Final(served) } - GPoll::Partial(value) => { + GPoll::Partial(served) => { // SAFETY: as above. - let bytes = unsafe { copy_record_bytes(content.layout(), content.layout().rec(&value)) }; + let bytes = unsafe { copy_record_bytes(content.layout(), content.layout().rec(served.record())) }; slot.publish(bytes, Finality::Partial); - GPoll::Partial(value) + GPoll::Partial(served) } unpublishable => { slot.release(); unpublishable } }, - Lookup::Full => content.eval(&ctx), + Lookup::Full => content.serve(&ctx, frame), } } @@ -189,15 +187,16 @@ type MonitorValue = Arc>>; /// (context, source generations), so introspection recreates it by /// re-evaluating this edge with the rehydrated snapshot. #[node_macro::node(category(""), path(graphene_core::memo), serialize(serialize_monitor), properties("monitor_properties"))] -fn monitor<'e>( +fn monitor<'e, 'l>( ctx: impl Ctx + DeriveCtx + ExtractAll + ExtractArena<'e> + ModifyIndex + Copy, #[data] io: MonitorValue, - content: impl Node, Output = RecordValue<'e>>, -) -> GPoll> { + content: impl Node>, + slot: FrameClaim<'l>, +) -> GPoll> { if ctx.index() == 0 { *io.lock().unwrap() = Some(CtxSnapshot::capture(ctx)); } - content.eval(&ctx) + content.serve(&ctx, slot) } fn serialize_monitor(io: &MonitorValue) -> Option> { @@ -212,42 +211,33 @@ mod tests { use core_types::arena::Arena; use core_types::context::{ContextImpl, EvalScope}; use core_types::node::Node; + use core_types::record::LiftedSource; use core_types::registry::{EdgeHandle, ErasedRecordNode}; use std::sync::atomic::{AtomicU32, Ordering}; - struct CountingNode(AtomicU32); - - impl Node for CountingNode { - type Output = u32; - - fn eval(&self, _input: &Input) -> GPoll { - GPoll::Final(self.0.fetch_add(1, Ordering::Relaxed) + 1) - } + fn lifted(value: T) -> LiftedSource Fn(&ContextImpl<'c>) -> GPoll> + where + T::Static: Clone + Send + Sync, + { + LiftedSource::new(move |_: &ContextImpl<'_>| GPoll::Final(value.clone())) } - struct PartialCountingNode(AtomicU32); - - impl Node for PartialCountingNode { - type Output = u32; - - fn eval(&self, _input: &Input) -> GPoll { - GPoll::Partial(self.0.fetch_add(1, Ordering::Relaxed) + 1) - } + fn counting() -> LiftedSource Fn(&ContextImpl<'c>) -> GPoll> { + let count = AtomicU32::new(0); + LiftedSource::new(move |_: &ContextImpl<'_>| GPoll::Final(count.fetch_add(1, Ordering::Relaxed) + 1)) } - struct ValueNode(T); - - impl Node for ValueNode { - type Output = T; - - fn eval(&self, _input: &Input) -> GPoll { - GPoll::Final(self.0.clone()) - } + fn partial_counting() -> LiftedSource Fn(&ContextImpl<'c>) -> GPoll> { + let count = AtomicU32::new(0); + LiftedSource::new(move |_: &ContextImpl<'_>| GPoll::Partial(count.fetch_add(1, Ordering::Relaxed) + 1)) } fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> { // SAFETY: between evaluations, nothing served on the stack is live. - unsafe { core_types::record::stack::reserve(1 << 16); } EvalScope::new(Some(0.5), None, None, generations, arena) + unsafe { + core_types::record::stack::reserve(1 << 16); + } + EvalScope::new(Some(0.5), None, None, generations, arena) } fn element_layout() -> core_types::record::Layout @@ -265,12 +255,12 @@ mod tests { let ctx = ContextImpl::root(&scope); let layout = element_layout::(); - let monitor = MonitorNode::new(core_types::record::RecordLift::::new(ValueNode(11u32)), &layout); + let monitor = MonitorNode::new(lifted::(11u32), &layout); let handle = EdgeHandle::new_record::(Arc::new(monitor) as Arc); assert!(handle.serialize().is_none(), "no snapshot before the first eval"); let edge = handle.duplicate().downcast_record::().unwrap(); - let GPoll::Final(_) = edge.eval(&ctx) else { + let GPoll::Final(_) = core_types::record::serve_edge(&edge, &ctx) else { panic!("expected a final record"); }; @@ -296,7 +286,7 @@ mod tests { let handle = EdgeHandle::new_record::(Arc::new(monitor) as Arc); let edge = handle.duplicate().downcast_record::().unwrap(); - let GPoll::Final(_) = edge.eval(&ctx) else { + let GPoll::Final(_) = core_types::record::serve_edge(&edge, &ctx) else { panic!("expected a final record"); }; @@ -333,7 +323,7 @@ mod tests { let ctx = ContextImpl::root(&scope); let layout = element_layout::(); - let memoized = MemoizeNode::new(core_types::record::RecordLift::::new(ValueNode(Payload("deep".to_string(), 0))), &layout); + let memoized = MemoizeNode::new(lifted::(Payload("deep".to_string(), 0)), &layout); let memoized = core_types::record::RecordExtract::::new(memoized, &layout); assert_eq!(memoized.eval(&ctx), GPoll::Final(Payload("deep".to_string(), 0)), "the miss serves the live value"); @@ -348,7 +338,7 @@ mod tests { let ctx = ContextImpl::root(&scope); let layout = element_layout::(); - let memoized = MemoizeNode::new(core_types::record::RecordLift::::new(CountingNode(AtomicU32::new(0))), &layout); + let memoized = MemoizeNode::new(counting(), &layout); let memoized = core_types::record::RecordExtract::::new(memoized, &layout); assert_eq!(memoized.eval(&ctx), GPoll::Final(1)); @@ -365,7 +355,7 @@ mod tests { let scope_after = scope_fixture(&after, &arena); let layout = element_layout::(); - let memoized = MemoizeNode::new(core_types::record::RecordLift::::new(CountingNode(AtomicU32::new(0))), &layout); + let memoized = MemoizeNode::new(counting(), &layout); let memoized = core_types::record::RecordExtract::::new(memoized, &layout); assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1)); @@ -381,7 +371,7 @@ mod tests { let ctx = ContextImpl::root(&scope); let layout = element_layout::(); - let memoized = MemoizeNode::new(core_types::record::RecordLift::::new(PartialCountingNode(AtomicU32::new(0))), &layout); + let memoized = MemoizeNode::new(partial_counting(), &layout); let memoized = core_types::record::RecordExtract::::new(memoized, &layout); assert_eq!(memoized.eval(&ctx), GPoll::Partial(1)); @@ -396,7 +386,7 @@ mod tests { let ctx = ContextImpl::root(&scope); let layout = element_layout::(); - let edge = EdgeHandle::new_record::(Arc::new(core_types::record::RecordLift::::new(CountingNode(AtomicU32::new(0)))) as Arc); + let edge = EdgeHandle::new_record::(Arc::new(counting()) as Arc); let memoized = EdgeHandle::new_record::(Arc::new(MemoizeNode::new(edge.downcast_record::().unwrap(), &layout)) as Arc); let stacked = MemoizeNode::new(memoized.downcast_record::().unwrap(), &layout); let stacked = core_types::record::RecordExtract::::new(stacked, &layout); @@ -413,12 +403,12 @@ mod tests { let ctx = ContextImpl::root(&scope); let layout = element_layout::(); - let memo = FrameMemoNode::new(core_types::record::RecordLift::::new(ValueNode("lent out".to_string())), &layout); + let memo = FrameMemoNode::new(lifted::("lent out".to_string()), &layout); - let GPoll::Final(first) = memo.eval(&ctx) else { + let GPoll::Final(first) = core_types::record::serve_edge(&memo, &ctx) else { panic!("the miss must fill the frame table"); }; - let GPoll::Final(second) = memo.eval(&ctx) else { + let GPoll::Final(second) = core_types::record::serve_edge(&memo, &ctx) else { panic!("the hit must revive the published record"); }; let first: &String = unsafe { core_types::record::borrow_element(layout.rec(&first)) }; diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index c74c48b66c..beea7562d5 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -7,7 +7,7 @@ use core_types::Ctx; use core_types::attribute::{Attr, EditorLayerPath, Opacity, RemoveAttr, Transform}; -use core_types::context::{DeriveCtx, ExtractIndex, ExtractIndices, IndexLink, InjectIndex, ModifyIndex}; +use core_types::context::{DeriveCtx, ExtractIndex, IndexLink, InjectIndex, ModifyIndex}; use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn}; use core_types::gpoll::{ErrorKind, Extent, GPoll, GraphError, Interrupt, Level}; use core_types::node::Lane; @@ -398,17 +398,8 @@ mod tests { use core_types::context::{ContextImpl, EvalScope, ExtractArena}; use core_types::gpoll::GPoll; use core_types::node::Node; - use core_types::record::{Layout, Rec, RecordSource, RecordValue, stack}; - - struct ValueNode(T); - - impl Node for ValueNode { - type Output = T; - - fn eval(&self, _input: &Input) -> GPoll { - GPoll::Final(self.0.clone()) - } - } + use core_types::record::{FrameClaim, Layout, LiftedSource, Rec, RecordSource, Served, stack}; + use core_types::value::ValueSource; struct RecordSourceNode { layout: Layout, @@ -417,21 +408,28 @@ mod tests { partial: bool, } - impl<'e, E: Copy + Send + Sync + 'static> Node> for RecordSourceNode { - type Output = RecordValue<'e>; - - fn eval(&self, input: &ContextImpl<'e>) -> GPoll> { - let mut frame = core_types::record::FrameBuilder::new(&self.layout, input.arena()); + impl Node for RecordSourceNode { + fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll> + where + C: ExtractArena, + { + let mut frame = core_types::record::FrameBuilder::new(&self.layout, ExtractArena::arena(input)); frame.element(self.element); for (name, field) in &self.fields { frame.field::(name, 0, *field); } let Some(value) = frame.finish() else { return GPoll::arena_exhausted() }; + // SAFETY: the builder served a record of this node's layout. + let served = unsafe { slot.forward(&value) }; match self.partial { - true => GPoll::Partial(value), - false => GPoll::Final(value), + true => GPoll::Partial(served), + false => GPoll::Final(served), } } + + fn layout(&self) -> &Layout { + &self.layout + } } struct LeveledSourceNode { @@ -440,21 +438,26 @@ mod tests { field: Option<(&'static str, f64)>, } - impl<'e> Node> for LeveledSourceNode { - type Output = RecordValue<'e>; - - fn eval(&self, input: &ContextImpl<'e>) -> GPoll> { + impl Node for LeveledSourceNode { + fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll> + where + C: ExtractArena, + { let element = self.elements[input.innermost_index() as usize % self.elements.len()]; - let mut frame = core_types::record::FrameBuilder::new(&self.layout, input.arena()); + let mut frame = core_types::record::FrameBuilder::new(&self.layout, ExtractArena::arena(input)); frame.element(element); if let Some((name, value)) = self.field { frame.field::(name, 0, value); } let Some(value) = frame.finish() else { return GPoll::arena_exhausted() }; - GPoll::Final(value) + // SAFETY: the builder served a record of this node's layout. + GPoll::Final(unsafe { slot.forward(&value) }) } - fn extent_at(&self, _input: &ContextImpl<'e>, _level: u8) -> GPoll { + fn extent_at<'x>(&self, _input: &C, _level: u8) -> GPoll + where + C: ExtractArena, + { GPoll::Final(Extent::Exactly(self.elements.len())) } @@ -468,19 +471,24 @@ mod tests { rows: Vec<(f64, DAffine2)>, } - impl<'e> Node> for LeveledTransformSource { - type Output = RecordValue<'e>; - - fn eval(&self, input: &ContextImpl<'e>) -> GPoll> { + impl Node for LeveledTransformSource { + fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll> + where + C: ExtractArena, + { let (element, transform) = self.rows[input.innermost_index() as usize % self.rows.len()]; - let mut frame = core_types::record::FrameBuilder::new(&self.layout, input.arena()); + let mut frame = core_types::record::FrameBuilder::new(&self.layout, ExtractArena::arena(input)); frame.element(element); frame.attr::(transform); let Some(value) = frame.finish() else { return GPoll::arena_exhausted() }; - GPoll::Final(value) + // SAFETY: the builder served a record of this node's layout. + GPoll::Final(unsafe { slot.forward(&value) }) } - fn extent_at(&self, _input: &ContextImpl<'e>, _level: u8) -> GPoll { + fn extent_at<'x>(&self, _input: &C, _level: u8) -> GPoll + where + C: ExtractArena, + { GPoll::Final(Extent::Exactly(self.rows.len())) } @@ -496,21 +504,26 @@ mod tests { count: usize, } - impl<'e> Node> for DrainSourceNode { - type Output = RecordValue<'e>; - - fn eval(&self, input: &ContextImpl<'e>) -> GPoll> { + impl Node for DrainSourceNode { + fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll> + where + C: ExtractArena, + { let lane = input.innermost_index(); if lane >= self.count as u64 { return GPoll::past_end(); } - let mut frame = core_types::record::FrameBuilder::new(&self.layout, input.arena()); + let mut frame = core_types::record::FrameBuilder::new(&self.layout, ExtractArena::arena(input)); frame.element(lane as f64); let Some(value) = frame.finish() else { return GPoll::arena_exhausted() }; - GPoll::Final(value) + // SAFETY: the builder served a record of this node's layout. + GPoll::Final(unsafe { slot.forward(&value) }) } - fn extent_at(&self, _input: &ContextImpl<'e>, _level: u8) -> GPoll { + fn extent_at<'x>(&self, _input: &C, _level: u8) -> GPoll + where + C: ExtractArena, + { GPoll::Final(Extent::AtLeast(0)) } @@ -523,23 +536,32 @@ mod tests { layout: Layout, } - impl<'e> Node> for IndexSourceNode { - type Output = RecordValue<'e>; - - fn eval(&self, input: &ContextImpl<'e>) -> GPoll> { + impl Node for IndexSourceNode { + fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll> + where + C: ExtractArena, + { // Depth-0 content varying per copy: the enclosing (pushed) level's // index sits one link above the content's own innermost lane. let element = input.try_index().and_then(|mut indices| indices.nth(1)).unwrap_or(0) as f64; - let mut frame = core_types::record::FrameBuilder::new(&self.layout, input.arena()); + let mut frame = core_types::record::FrameBuilder::new(&self.layout, ExtractArena::arena(input)); frame.element(element); let Some(value) = frame.finish() else { return GPoll::arena_exhausted() }; - GPoll::Final(value) + // SAFETY: the builder served a record of this node's layout. + GPoll::Final(unsafe { slot.forward(&value) }) + } + + fn layout(&self) -> &Layout { + &self.layout } } fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> { // SAFETY: between evaluations, nothing served on the stack is live. - unsafe { stack::reserve(1 << 16); } EvalScope::new(Some(0.5), None, None, generations, arena) + unsafe { + stack::reserve(1 << 16); + } + EvalScope::new(Some(0.5), None, None, generations, arena) } fn f64_layout(names: &[&'static str]) -> Layout { @@ -604,11 +626,11 @@ mod tests { node } - fn lifted_value(value: T) -> (core_types::record::RecordLift>, Layout) + fn lifted_value(value: T) -> (ValueSource, Layout) where T::Static: Clone + Send + Sync, { - let lift = core_types::record::RecordLift::::new(ValueNode(value)); + let lift = ValueSource::new(value); let layout = Node::::layout(&lift).clone(); (lift, layout) } @@ -643,8 +665,8 @@ mod tests { let leveled = repeat_opacity_layout(&base); reserve_for(&[&base, &leveled]); - let node = install(RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(8u32), &base), repeat_opacity_layout_meta(), &[Some(&base)]); - assert_eq!(node.layout(), &leveled); + let node = install(RepeatOpacityNode::new(bare_source(&base, 7.), ValueSource::new(8u32), &base), repeat_opacity_layout_meta(), &[Some(&base)]); + assert_eq!(Node::::layout(&node), &leveled); let GPoll::Final(served) = core_types::record::capture(&node, &indexed) else { panic!("expected a final record"); }; @@ -662,7 +684,7 @@ mod tests { let base = f64_layout(&[]); reserve_for(&[&base]); - let node = install(RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(3u32), &base), repeat_opacity_layout_meta(), &[Some(&base)]); + let node = install(RepeatOpacityNode::new(bare_source(&base, 7.), ValueSource::new(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))); } @@ -804,7 +826,7 @@ mod tests { let (reverse_edge, reverse_layout) = lifted_value(false); reserve_for(&[&base, &leveled_content, &count_layout, &reverse_layout]); - let content = install(RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(3u32), &base), repeat_opacity_layout_meta(), &[Some(&base)]); + let content = install(RepeatOpacityNode::new(bare_source(&base, 7.), ValueSource::new(3u32), &base), repeat_opacity_layout_meta(), &[Some(&base)]); let meta = core_types::record::LayoutMeta { sources: vec![0], reads: vec![], @@ -861,7 +883,7 @@ mod tests { reserve_for(&[&base]); let node = install( - RepeatFadedNode::new(RecordSource::new(content, &base, &base), ValueNode(4u32), &base), + RepeatFadedNode::new(RecordSource::new(content, &base, &base), ValueSource::new(4u32), &base), repeat_faded_layout_meta(), &[Some(&base)], ); @@ -1299,7 +1321,7 @@ mod tests { let rows = [(1., 10.), (2., 30.), (3., 20.)]; let build = |keep: bool| { install( - MirrorNode::new(RecordSource::new(content(&rows), &layout, &layout), ValueNode(keep)), + MirrorNode::new(RecordSource::new(content(&rows), &layout, &layout), ValueSource::new(keep)), mirror_layout_meta(), &[Some(&layout)], ) @@ -1347,7 +1369,7 @@ mod tests { rows: rows.iter().map(|&(element, x)| (element, DAffine2::from_translation(glam::DVec2::new(x, 0.)))).collect(), }; let node = install( - ReverseLanesNode::new(RecordSource::new(content, &layout, &layout), ValueNode(0.25)), + ReverseLanesNode::new(RecordSource::new(content, &layout, &layout), ValueSource::new(0.25)), reverse_lanes_layout_meta(), &[Some(&layout)], ); @@ -1386,7 +1408,7 @@ mod tests { .map(|&(element, x)| (element, DAffine2::from_translation(glam::DVec2::new(x, 0.)))) .collect(), }; - let node = install(MirrorNode::new(RecordSource::new(content, &layout, &layout), ValueNode(true)), mirror_layout_meta(), &[Some(&layout)]); + let node = install(MirrorNode::new(RecordSource::new(content, &layout, &layout), ValueSource::new(true)), mirror_layout_meta(), &[Some(&layout)]); let out = Node::::layout(&node).clone(); let head = ctx.index_head(); let scoped = ctx.promoted(&head, 0); @@ -1415,15 +1437,11 @@ mod tests { /// a lane-varying value serves the range's first lane to all of them. #[test] fn batch_rebinds_an_eager_input_the_compiler_cannot_prove_invariant() { - struct CountingValue<'a>(bool, &'a std::cell::Cell); - - impl Node for CountingValue<'_> { - type Output = bool; - - fn eval(&self, _input: &Input) -> GPoll { - self.1.set(self.1.get() + 1); - GPoll::Final(self.0) - } + fn counting_value(value: bool, evals: &std::cell::Cell) -> LiftedSource Fn(&ContextImpl<'c>) -> GPoll + '_> { + LiftedSource::new(move |_: &ContextImpl<'_>| { + evals.set(evals.get() + 1); + GPoll::Final(value) + }) } let arena = Arena::new(1 << 16).unwrap(); @@ -1441,7 +1459,7 @@ mod tests { .collect(), }; let evals = std::cell::Cell::new(0u32); - let mut node = MirrorNode::new(RecordSource::new(content, &layout, &layout), CountingValue(true, &evals)); + let mut node = MirrorNode::new(RecordSource::new(content, &layout, &layout), counting_value(true, &evals)); let resolved = core_types::record::RecordLayout { lane_invariant: 0, ..mirror_layout_meta().resolve(&[Some(&layout)]) @@ -1462,15 +1480,11 @@ mod tests { #[test] fn batch_binds_eager_inputs_once() { - struct CountingValue<'a>(bool, &'a std::cell::Cell); - - impl Node for CountingValue<'_> { - type Output = bool; - - fn eval(&self, _input: &Input) -> GPoll { - self.1.set(self.1.get() + 1); - GPoll::Final(self.0) - } + fn counting_value(value: bool, evals: &std::cell::Cell) -> LiftedSource Fn(&ContextImpl<'c>) -> GPoll + '_> { + LiftedSource::new(move |_: &ContextImpl<'_>| { + evals.set(evals.get() + 1); + GPoll::Final(value) + }) } let arena = Arena::new(1 << 16).unwrap(); @@ -1489,7 +1503,7 @@ mod tests { }; let evals = std::cell::Cell::new(0u32); let node = install( - MirrorNode::new(RecordSource::new(content, &layout, &layout), CountingValue(true, &evals)), + MirrorNode::new(RecordSource::new(content, &layout, &layout), counting_value(true, &evals)), mirror_layout_meta(), &[Some(&layout)], ); @@ -1520,7 +1534,7 @@ mod tests { // Element = the outer copy, so the total fold sums across both copies. let content = install( - RepeatOpacityNode::new(IndexSourceNode { layout: base.clone() }, ValueNode(3u32), &base), + RepeatOpacityNode::new(IndexSourceNode { layout: base.clone() }, ValueSource::new(3u32), &base), repeat_opacity_layout_meta(), &[Some(&base)], ); @@ -1572,7 +1586,7 @@ mod tests { let out = f64_layout(&[]); reserve_for(&[&base, &leveled_content, &count_layout, &reverse_layout, &out]); - let content = install(RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(3u32), &base), repeat_opacity_layout_meta(), &[Some(&base)]); + let content = install(RepeatOpacityNode::new(bare_source(&base, 7.), ValueSource::new(3u32), &base), repeat_opacity_layout_meta(), &[Some(&base)]); let meta = core_types::record::LayoutMeta { sources: vec![0], reads: vec![], @@ -1651,7 +1665,7 @@ mod tests { }; let path = vec![NodeId(7), NodeId(8)]; let node = install( - StampLayerPathNode::new(RecordSource::new(source, &source_layout, &source_layout), ValueNode(path.clone()), &source_layout), + StampLayerPathNode::new(RecordSource::new(source, &source_layout, &source_layout), ValueSource::new(path.clone()), &source_layout), stamp_layer_path_layout_meta(), &[Some(&source_layout)], ); @@ -1661,7 +1675,7 @@ mod tests { let head = ctx.index_head(); for (lane, element) in [(0u64, 10.), (1, 11.)] { let _lane_scope = unsafe { stack::ScopeGuard::enter() }; - let GPoll::Final(value) = node.eval(&ctx.promoted(&head, lane)) else { + let GPoll::Final(value) = core_types::record::serve_edge(&node, &ctx.promoted(&head, lane)) else { panic!("expected a final record at lane {lane}"); }; let rec = out.rec(&value); @@ -1780,9 +1794,9 @@ mod tests { let out = f64_layout(&[]); reserve_for(&[&base, &leveled, &out]); - let repeat = install(RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(3u32), &base), repeat_opacity_layout_meta(), &[Some(&base)]); + let repeat = install(RepeatOpacityNode::new(bare_source(&base, 7.), ValueSource::new(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"); + assert_eq!(Node::::layout(&node).depth, 0, "the reducer collapsed the rank level"); let GPoll::Final(served) = core_types::record::capture(&node, &ctx) else { panic!("expected a final record"); @@ -1865,17 +1879,17 @@ mod tests { let chain = install( MultiplyOpacityNode::new( install( - MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout), + MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueSource::new(0.5), &source_layout), multiply_opacity_layout_meta(), &[Some(&source_layout)], ), - ValueNode(0.5), + ValueSource::new(0.5), &modified, ), multiply_opacity_layout_meta(), &[Some(&modified)], ); - assert_eq!(chain.layout(), &stacked); + assert_eq!(Node::::layout(&chain), &stacked); let GPoll::Final(served) = core_types::record::capture(&chain, &ctx) else { panic!("expected a final record"); }; @@ -1917,7 +1931,7 @@ mod tests { let chain = install( MeasureNode::new( install( - MultiplyOpacityNode::new(bare_source(&source_layout, -2.), ValueNode(0.5), &source_layout), + MultiplyOpacityNode::new(bare_source(&source_layout, -2.), ValueSource::new(0.5), &source_layout), multiply_opacity_layout_meta(), &[Some(&source_layout)], ), @@ -1954,7 +1968,7 @@ mod tests { let chain = install( ShadeNode::new( install( - MultiplyOpacityNode::new(bare_source(&source_layout, 4.), ValueNode(0.5), &source_layout), + MultiplyOpacityNode::new(bare_source(&source_layout, 4.), ValueSource::new(0.5), &source_layout), multiply_opacity_layout_meta(), &[Some(&source_layout)], ), @@ -1983,7 +1997,7 @@ mod tests { let u32_faded = fade_layout(&u32_source); reserve_for(&[&f64_source, &f64_faded, &u32_source, &u32_faded]); - let wide = install(FadeNode::new(bare_source(&f64_source, 8.), ValueNode(0.5), &f64_source), fade_layout_meta(), &[Some(&f64_source)]); + let wide = install(FadeNode::new(bare_source(&f64_source, 8.), ValueSource::new(0.5), &f64_source), fade_layout_meta(), &[Some(&f64_source)]); let GPoll::Final(served) = core_types::record::capture(&wide, &ctx) else { panic!("expected a final record"); }; @@ -1998,7 +2012,7 @@ mod tests { fields: vec![], partial: false, }, - ValueNode(0.25), + ValueSource::new(0.25), &u32_source, ), fade_layout_meta(), @@ -2021,7 +2035,7 @@ mod tests { let layout = source_opacity_layout(); reserve_for(&[&layout]); - let node = install(SourceOpacityNode::new(ValueNode(()), ValueNode(3.), ValueNode(0.25)), source_opacity_layout_meta(), &[]); + let node = install(SourceOpacityNode::new(ValueSource::new(()), ValueSource::new(3.), ValueSource::new(0.25)), source_opacity_layout_meta(), &[]); assert_eq!(Node::::layout(&node), &layout); let GPoll::Final(served) = core_types::record::capture(&node, &ctx) else { panic!("expected a final record"); @@ -2049,7 +2063,7 @@ mod tests { fields: vec![], partial: true, }, - ValueNode(0.5), + ValueSource::new(0.5), &source_layout, ), multiply_opacity_layout_meta(), @@ -2073,7 +2087,7 @@ mod tests { reserve_for(&[&source_layout, &modified]); let ok = install( - CheckedMultiplyOpacityNode::new(bare_source(&source_layout, 1.), ValueNode(0.5), &source_layout), + CheckedMultiplyOpacityNode::new(bare_source(&source_layout, 1.), ValueSource::new(0.5), &source_layout), checked_multiply_opacity_layout_meta(), &[Some(&source_layout)], ); @@ -2083,11 +2097,11 @@ mod tests { assert_eq!(served.attr::(), 0.5); let failing = install( - CheckedMultiplyOpacityNode::new(bare_source(&source_layout, 1.), ValueNode(-1.), &source_layout), + CheckedMultiplyOpacityNode::new(bare_source(&source_layout, 1.), ValueSource::new(-1.), &source_layout), checked_multiply_opacity_layout_meta(), &[Some(&source_layout)], ); - let GPoll::Error(error) = failing.eval(&ctx) else { + let GPoll::Error(error) = core_types::record::serve_edge(&failing, &ctx) else { panic!("expected an error"); }; assert!(error.kind == "negative factor"); @@ -2108,11 +2122,11 @@ mod tests { let chain = install( ScaleNode::new( install( - MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout), + MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueSource::new(0.5), &source_layout), multiply_opacity_layout_meta(), &[Some(&source_layout)], ), - ValueNode(3.), + ValueSource::new(3.), &modified, ), scale_layout_meta(), @@ -2178,7 +2192,7 @@ mod tests { let ctx = ContextImpl::root(&scope); let source_layout = f64_layout(&["opacity"]); - let factor = core_types::record::RecordLift::::new(ValueNode(3.)); + let factor = ValueSource::new(3.); let factor_layout = Node::::layout(&factor).clone(); reserve_for(&[&source_layout]); @@ -2307,14 +2321,14 @@ mod tests { let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); - let unit = core_types::record::RecordLift::<(), _>::new(ValueNode(())); + let unit = ValueSource::new(()); let unit_layout = Node::::layout(&unit).clone(); let content_layout = f64_layout(&["opacity"]); reserve_for(&[&content_layout]); let run = |opacity: Option| { let evals = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)); - let alternate = core_types::record::RecordLift::::new(CountingValue(evals.clone())); + let alternate = counting_source(evals.clone()); let alternate_layout = Node::::layout(&alternate).clone(); let (content_layout, fields) = match opacity { Some(value) => (content_layout.clone(), vec![("opacity", value)]), @@ -2322,7 +2336,7 @@ mod tests { }; let node = install_flip( FallbackNode::new( - core_types::record::RecordLift::<(), _>::new(ValueNode(())), + ValueSource::new(()), f64_record_source(&content_layout, 7., fields), alternate, &unit_layout, @@ -2331,7 +2345,7 @@ mod tests { ), &f64_layout(&[]), ); - let GPoll::Final(value) = node.eval(&ctx) else { + let GPoll::Final(value) = core_types::record::serve_edge(&node, &ctx) else { panic!("expected a final record"); }; let element = unsafe { Node::::layout(&node).rec(&value).element::() }; @@ -2362,7 +2376,7 @@ mod tests { install( StripOpacityNode::new( install( - MultiplyOpacityNode::new(bare_source(&source_layout, 4.), ValueNode(0.5), &source_layout), + MultiplyOpacityNode::new(bare_source(&source_layout, 4.), ValueSource::new(0.5), &source_layout), multiply_opacity_layout_meta(), &[Some(&source_layout)], ), @@ -2421,17 +2435,17 @@ mod tests { let chain = install( LabelNode::new( install( - LabelNode::new(bare_source(&source_layout, 1.), ValueNode(String::from("a")), &source_layout), + LabelNode::new(bare_source(&source_layout, 1.), ValueSource::new(String::from("a")), &source_layout), label_layout_meta(), &[Some(&source_layout)], ), - ValueNode(String::from("b")), + ValueSource::new(String::from("b")), &labeled, ), label_layout_meta(), &[Some(&labeled)], ); - let GPoll::Final(value) = chain.eval(&ctx) else { + let GPoll::Final(value) = core_types::record::serve_edge(&chain, &ctx) else { panic!("expected a final record"); }; let rec = relabeled.rec(&value); @@ -2560,18 +2574,24 @@ mod tests { layout: Layout, } - impl<'e> Node> for RealTimeProbe { - type Output = RecordValue<'e>; - - fn eval(&self, input: &ContextImpl<'e>) -> GPoll> { + impl Node for RealTimeProbe { + fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll> + where + C: ExtractArena, + { let element: f64 = match core_types::context::ExtractRealTime::try_real_time(input) { Some(_) => 1., None => 0., }; - let mut frame = core_types::record::FrameBuilder::new(&self.layout, input.arena()); + let mut frame = core_types::record::FrameBuilder::new(&self.layout, ExtractArena::arena(input)); frame.element(element); let Some(value) = frame.finish() else { return GPoll::arena_exhausted() }; - GPoll::Final(value) + // SAFETY: the builder served a record of this node's layout. + GPoll::Final(unsafe { slot.forward(&value) }) + } + + fn layout(&self) -> &Layout { + &self.layout } } @@ -2640,7 +2660,7 @@ mod tests { let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); - let lift = core_types::record::RecordLift::::new(ValueNode(String::from("parked"))); + let lift = ValueSource::new(String::from("parked")); let layout = Node::::layout(&lift).clone(); let chain = core_types::record::RecordExtract::::new(lift, &layout); @@ -2664,7 +2684,7 @@ mod tests { let chain = ForwardRecordNode::new(RecordSource::new(f64_record_source(&layout, 4., vec![("opacity", 0.25)]), &layout, &layout.clone()), &layout); - let GPoll::Final(value) = chain.eval(&ctx) else { + let GPoll::Final(value) = core_types::record::serve_edge(&chain, &ctx) else { panic!("expected a final record"); }; let rec = layout.rec(&value); @@ -2703,15 +2723,11 @@ mod tests { assert_eq!(served.element::(), 4.); } - struct CountingValue(std::sync::Arc); - - impl Node for CountingValue { - type Output = f64; - - fn eval(&self, _input: &Input) -> GPoll { - self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + fn counting_source(evals: std::sync::Arc) -> LiftedSource Fn(&ContextImpl<'c>) -> GPoll> { + LiftedSource::new(move |_: &ContextImpl<'_>| { + evals.fetch_add(1, std::sync::atomic::Ordering::Relaxed); GPoll::Final(21.) - } + }) } #[test] @@ -2722,7 +2738,7 @@ mod tests { let ctx = ContextImpl::root(&scope); let evals = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)); - let lift = core_types::record::RecordLift::::new(CountingValue(evals.clone())); + let lift = counting_source(evals.clone()); let layout = Node::::layout(&lift).clone(); let memo = crate::memo::MemoizeNode::new(lift, &layout); @@ -2755,7 +2771,7 @@ mod tests { }; let memo = crate::memo::MemoizeNode::new(source, &layout); - let GPoll::Partial(_) = memo.eval(&ctx) else { + let GPoll::Partial(_) = core_types::record::serve_edge(&memo, &ctx) else { panic!("expected a partial record"); }; let GPoll::Partial(served) = core_types::record::capture(&memo, &ctx) else { @@ -2773,7 +2789,7 @@ mod tests { reserve_for(&[&labeled, &labeled]); let chain = install( - LabelNode::new(bare_source(&source_layout, 1.), ValueNode(String::from("a")), &source_layout), + LabelNode::new(bare_source(&source_layout, 1.), ValueSource::new(String::from("a")), &source_layout), label_layout_meta(), &[Some(&source_layout)], ); @@ -2783,7 +2799,7 @@ mod tests { { let scope = scope_fixture(&generations, &first_arena); let ctx = ContextImpl::root(&scope); - let GPoll::Final(_) = memo.eval(&ctx) else { + let GPoll::Final(_) = core_types::record::serve_edge(&memo, &ctx) else { panic!("expected a final record"); }; } @@ -2791,7 +2807,7 @@ mod tests { let replay_arena = Arena::new(1024).unwrap(); let scope = scope_fixture(&generations, &replay_arena); let ctx = ContextImpl::root(&scope); - let GPoll::Final(value) = memo.eval(&ctx) else { + let GPoll::Final(value) = core_types::record::serve_edge(&memo, &ctx) else { panic!("expected a final record"); }; let rec = labeled.rec(&value); diff --git a/node-graph/nodes/graphic/src/record.rs b/node-graph/nodes/graphic/src/record.rs index 9db28595a0..b9ffd3c4cf 100644 --- a/node-graph/nodes/graphic/src/record.rs +++ b/node-graph/nodes/graphic/src/record.rs @@ -222,39 +222,35 @@ mod tests { use core_types::SourceId; use core_types::arena::Arena; use core_types::attribute::Attribute as AttributeMarker; - use core_types::context::{ContextImpl, EvalScope, ExtractArena, ExtractIndices}; + use core_types::context::{ContextImpl, EvalScope, ExtractArena}; use core_types::list::{Item, List}; use core_types::node::Node; - use core_types::record::{self, Layout, RecordSource, RecordValue, stack}; - - struct ValueNode(T); - - impl Node for ValueNode { - type Output = T; - - fn eval(&self, _input: &Input) -> GPoll { - GPoll::Final(self.0.clone()) - } - } + use core_types::record::{self, FrameClaim, Layout, RecordSource, Served, stack}; + use core_types::value::ValueSource; struct GraphicSource { layout: Layout, rows: Vec<(Graphic<'static>, DAffine2)>, } - impl<'e> Node> for GraphicSource { - type Output = RecordValue<'e>; - - fn eval(&self, input: &ContextImpl<'e>) -> GPoll> { + impl Node for GraphicSource { + fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll> + where + C: ExtractArena, + { let (graphic, transform) = &self.rows[input.innermost_index() as usize % self.rows.len()]; - let mut frame = record::FrameBuilder::new(&self.layout, input.arena()); + let mut frame = record::FrameBuilder::new(&self.layout, ExtractArena::arena(input)); frame.element(graphic.clone()); frame.attr::(*transform); let Some(value) = frame.finish() else { return GPoll::arena_exhausted() }; - GPoll::Final(value) + // SAFETY: the builder served a record of this node's layout. + GPoll::Final(unsafe { slot.forward(&value) }) } - fn extent_at(&self, _input: &ContextImpl<'e>, _level: u8) -> GPoll { + fn extent_at<'x>(&self, _input: &C, _level: u8) -> GPoll + where + C: ExtractArena, + { GPoll::Final(Extent::Exactly(self.rows.len())) } @@ -342,7 +338,7 @@ mod tests { &$layout, &$layout, ), - ValueNode($fully), + ValueSource::new($fully), ), flatten_layout_meta(), &[Some(&$layout)], @@ -358,31 +354,36 @@ mod tests { layout: Layout, } - fn vararg_text(input: &ContextImpl<'_>) -> Option { + fn vararg_text(input: &C) -> Option { let arg = core_types::ExtractVarArgs::vararg(input, 0).ok()?; let list = arg.downcast_ref::>()?; let Graphic::Text(text) = list.element(0)? else { return None }; Some(text.clone()) } - impl<'e> Node> for PerRowSource { - type Output = RecordValue<'e>; - - fn eval(&self, input: &ContextImpl<'e>) -> GPoll> { + impl Node for PerRowSource { + fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll> + where + C: ExtractArena, + { let Some(label) = vararg_text(input) else { return GPoll::error("the subgraph fixture expects a text vararg"); }; let lane = input.innermost_index(); let graphic = text(&format!("{label}{lane}")); let translated = DAffine2::from_translation(glam::DVec2::new(lane as f64, 0.)); - let mut frame = record::FrameBuilder::new(&self.layout, input.arena()); + let mut frame = record::FrameBuilder::new(&self.layout, ExtractArena::arena(input)); frame.element(graphic); frame.attr::(translated); let Some(value) = frame.finish() else { return GPoll::arena_exhausted() }; - GPoll::Final(value) + // SAFETY: the builder served a record of this node's layout. + GPoll::Final(unsafe { slot.forward(&value) }) } - fn extent_at(&self, input: &ContextImpl<'e>, _level: u8) -> GPoll { + fn extent_at<'x>(&self, input: &C, _level: u8) -> GPoll + where + C: ExtractArena, + { match vararg_text(input) { Some(label) => GPoll::Final(Extent::Exactly(label.len())), None => GPoll::error("the subgraph fixture expects a text vararg"), @@ -681,7 +682,7 @@ mod tests { assert_eq!(node.extent_at(&ctx, 0), GPoll::Final(Extent::Exactly(1)), "the group is the level's single lane"); let head = ctx.index_head(); - let GPoll::Final(value) = node.eval(&ctx.promoted(&head, 0)) else { + let GPoll::Final(value) = record::serve_edge(&node, &ctx.promoted(&head, 0)) else { panic!("expected a final record"); }; let Graphic::Group(group) = (unsafe { record::borrow_element::(out.rec(&value)) }) else { @@ -716,7 +717,7 @@ mod tests { let out = Node::::layout(&node).clone(); let head = ctx.index_head(); - let GPoll::Final(value) = node.eval(&ctx.promoted(&head, 0)) else { + let GPoll::Final(value) = record::serve_edge(&node, &ctx.promoted(&head, 0)) else { panic!("expected a final record"); }; let copy = unsafe { (out.element.clone_out)(out.rec(&value).ptr()) }; @@ -746,18 +747,23 @@ mod tests { colors: Vec, } - impl<'e> Node> for ColorSource { - type Output = RecordValue<'e>; - - fn eval(&self, input: &ContextImpl<'e>) -> GPoll> { + impl Node for ColorSource { + fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll> + where + C: ExtractArena, + { let color = self.colors[input.innermost_index() as usize]; - let mut frame = record::FrameBuilder::new(&self.layout, input.arena()); + let mut frame = record::FrameBuilder::new(&self.layout, ExtractArena::arena(input)); frame.element(color); let Some(value) = frame.finish() else { return GPoll::arena_exhausted() }; - GPoll::Final(value) + // SAFETY: the builder served a record of this node's layout. + GPoll::Final(unsafe { slot.forward(&value) }) } - fn extent_at(&self, _input: &ContextImpl<'e>, _level: u8) -> GPoll { + fn extent_at<'x>(&self, _input: &C, _level: u8) -> GPoll + where + C: ExtractArena, + { GPoll::Final(Extent::Exactly(self.colors.len())) } @@ -809,7 +815,7 @@ mod tests { ); let out = Node::::layout(&node).clone(); let head = ctx.index_head(); - let GPoll::Final(value) = node.eval(&ctx.promoted(&head, 0)) else { + let GPoll::Final(value) = record::serve_edge(&node, &ctx.promoted(&head, 0)) else { panic!("expected a final record"); }; let Graphic::Group(group) = (unsafe { record::borrow_element::(out.rec(&value)) }) else { @@ -844,7 +850,7 @@ mod tests { // SAFETY: the element is cloned out inside the scope, so no borrow // into the frame escapes it. let _scope = unsafe { stack::ScopeGuard::enter() }; - let GPoll::Final(value) = wrapped.eval(&ctx.promoted(&head, 0)) else { + let GPoll::Final(value) = record::serve_edge(&wrapped, &ctx.promoted(&head, 0)) else { panic!("expected a final record"); }; let group = unsafe { record::borrow_element::(wrap_out.rec(&value)) }.clone(); diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index ea6ee6679f..b1c91f3714 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -220,28 +220,22 @@ mod tests { use core_types::{ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime}; use graphene_application_io::TimingInformation; - struct ProbeNode; - - impl<'a> Node> for ProbeNode { - type Output = RenderOutput; - - fn eval(&self, ctx: &ContextImpl<'a>) -> GPoll { - let render_params = ctx.vararg(0).unwrap().downcast_ref::().expect("the vararg chain must start with RenderParams"); - assert_eq!(render_params.scale, 2.0); - assert!(matches!(ctx.vararg(1), Err(VarArgsResult::IndexOutOfBounds)), "the RenderConfig must not leak downstream"); - assert_eq!(ctx.footprint().transform, glam::DAffine2::from_scale(glam::DVec2::splat(2.0)) * Footprint::DEFAULT.transform); - assert_eq!(ctx.try_real_time(), Some(1.5)); - assert_eq!(ctx.try_animation_time(), Some(2.0)); - assert_eq!(ctx.try_pointer_position(), Some(glam::DVec2::new(3.0, 4.0))); - GPoll::Final(RenderOutput { - data: RenderOutputType::Buffer { - data: Vec::new(), - width: 0, - height: 0, - }, - metadata: RenderMetadata::default(), - }) - } + fn probe(ctx: &ContextImpl) -> GPoll { + let render_params = ctx.vararg(0).unwrap().downcast_ref::().expect("the vararg chain must start with RenderParams"); + assert_eq!(render_params.scale, 2.0); + assert!(matches!(ctx.vararg(1), Err(VarArgsResult::IndexOutOfBounds)), "the RenderConfig must not leak downstream"); + assert_eq!(ctx.footprint().transform, glam::DAffine2::from_scale(glam::DVec2::splat(2.0)) * Footprint::DEFAULT.transform); + assert_eq!(ctx.try_real_time(), Some(1.5)); + assert_eq!(ctx.try_animation_time(), Some(2.0)); + assert_eq!(ctx.try_pointer_position(), Some(glam::DVec2::new(3.0, 4.0))); + GPoll::Final(RenderOutput { + data: RenderOutputType::Buffer { + data: Vec::new(), + width: 0, + height: 0, + }, + metadata: RenderMetadata::default(), + }) } #[test] @@ -265,10 +259,13 @@ mod tests { }; let ctx = root.with_varargs(&varargs); - let probe = core_types::record::RecordLift::::new(ProbeNode); + let probe = core_types::record::LiftedSource::::new(probe); let layout = Node::::layout(&probe).clone(); // SAFETY: between evaluations, nothing served on the stack is live. - unsafe { core_types::record::stack::reserve(layout.frame_bytes().max(1 << 12)); } let mut graph = CreateContextNode::new(probe, &layout); + unsafe { + core_types::record::stack::reserve(layout.frame_bytes().max(1 << 12)); + } + let mut graph = CreateContextNode::new(probe, &layout); // The executor resolves and installs the node's own layout at wiring; // without it the flip tail writes through the default empty layout. Node::::set_layout( @@ -280,7 +277,7 @@ mod tests { lane_invariant: u32::MAX, }, ); - let GPoll::Final(result) = Node::::eval(&graph, &ctx) else { + let GPoll::Final(result) = core_types::record::serve_edge(&graph, &ctx) else { panic!("create_context must complete synchronously"); }; let output: &RenderOutput = unsafe { core_types::record::borrow_element(layout.rec(&result)) }; diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index f37998c492..92ca398dab 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -1011,34 +1011,14 @@ mod test { mod graphene_test { use super::*; use core_types::arena::Arena; - use core_types::context::{ContextImpl, EvalScope, ExtractIndex}; + use core_types::context::{ContextImpl, EvalScope}; use core_types::gpoll::{Finality, GPoll}; use core_types::node::{BatchStatus, Node}; - use core_types::record::{Layout, RecordLift, RecordValue, stack}; + use core_types::record::{Layout, LiftedSource, RecordValue, serve_edge, stack}; use core_types::registry::{ErasedRecordNode, construct}; use core_types::value::record_value_edge; use std::mem::MaybeUninit; - struct SourceNode(T); - - impl Node for SourceNode { - type Output = T; - - fn eval(&self, _input: &Input) -> GPoll { - GPoll::Final(self.0.clone()) - } - } - - struct IndexNode; - - impl Node for IndexNode { - type Output = f64; - - fn eval(&self, input: &Input) -> GPoll { - GPoll::Final(input.index() as f64) - } - } - fn scope_fixture(arena: &Arena) -> EvalScope<'_> { EvalScope::new(None, None, None, &[], arena) } @@ -1049,13 +1029,13 @@ mod graphene_test { /// Lifts a plain-element test source onto a record wire, returned beside its /// element-only layout for the generated node's constructor. - fn lifted(node: N) -> (RecordLift, Layout) + fn lifted(kernel: F) -> (LiftedSource, Layout) where T: Clone + Send + Sync + core_types::StaticTypeSized + 'static, ::Static: Clone + Send + Sync, - N: for<'c> Node, Output = T>, + F: for<'c> Fn(&ContextImpl<'c>) -> GPoll, { - let lift = RecordLift::::new(node); + let lift = LiftedSource::::new(kernel); let layout = Node::::layout(&lift).clone(); (lift, layout) } @@ -1087,13 +1067,13 @@ mod graphene_test { let scope = scope_fixture(&arena); let ctx = ContextImpl::root(&scope); - let (a, la) = lifted(SourceNode(1.0f64)); - let (b, lb) = lifted(SourceNode(2.0f64)); + let (a, la) = lifted(|_: &ContextImpl| GPoll::Final(1.0f64)); + let (b, lb) = lifted(|_: &ContextImpl| GPoll::Final(2.0f64)); let out = out_layout::(); let graph = installed(AddNode::<_, _, f64, f64>::new(a, b, &la, &lb), &out); reserve_for(&[&la, &lb, &out]); - let GPoll::Final(value) = Node::eval(&graph, &ctx) else { + let GPoll::Final(value) = serve_edge(&graph, &ctx) else { panic!("expected a final record"); }; assert_eq!(element::(&out, &value), 3.0); @@ -1105,8 +1085,8 @@ mod graphene_test { let scope = scope_fixture(&arena); let ctx = ContextImpl::root(&scope); - let (index, li) = lifted(IndexNode); - let (src, ls) = lifted(SourceNode(10.0f64)); + let (index, li) = lifted(|input: &ContextImpl| GPoll::Final(core_types::ExtractIndex::<0>::index(input) as f64)); + let (src, ls) = lifted(|_: &ContextImpl| GPoll::Final(10.0f64)); let out = out_layout::(); let node = installed(AddNode::<_, _, f64, f64>::new(index, src, &li, &ls), &out); reserve_for(&[&li, &ls, &out]); @@ -1142,7 +1122,7 @@ mod graphene_test { let edge = wired.downcast_record::().unwrap(); reserve_for(&[&layout]); - let GPoll::Final(value) = edge.eval(&ctx) else { + let GPoll::Final(value) = serve_edge(&edge, &ctx) else { panic!("expected a final record"); }; assert!(element::(&layout, &value)); @@ -1188,7 +1168,7 @@ mod graphene_test { let edge = wired.downcast_record::().unwrap(); reserve_for(&[&layout]); - let GPoll::Final(value) = edge.eval(&ctx) else { + let GPoll::Final(value) = serve_edge(&edge, &ctx) else { panic!("expected a final record"); }; assert_eq!(element::(&layout, &value), 4.0); @@ -1209,32 +1189,33 @@ mod graphene_test { use std::sync::Arc; use std::sync::atomic::{AtomicU32, Ordering}; - struct CountingSource(Arc, f64); - - impl Node for CountingSource { - type Output = f64; - - fn eval(&self, _input: &Input) -> GPoll { - self.0.fetch_add(1, Ordering::Relaxed); - GPoll::Final(self.1) - } - } - let arena = Arena::new(64).unwrap(); let scope = scope_fixture(&arena); let ctx = ContextImpl::root(&scope); let taken = Arc::new(AtomicU32::new(0)); let untaken = Arc::new(AtomicU32::new(0)); - let (cond, lc) = lifted(SourceNode(true)); - let (if_true, lt) = lifted(CountingSource(taken.clone(), 1.0)); - let (if_false, lf) = lifted(CountingSource(untaken.clone(), 2.0)); + let (cond, lc) = lifted(|_: &ContextImpl| GPoll::Final(true)); + let (if_true, lt) = lifted({ + let runs = taken.clone(); + move |_: &ContextImpl| { + runs.fetch_add(1, Ordering::Relaxed); + GPoll::Final(1.0) + } + }); + let (if_false, lf) = lifted({ + let runs = untaken.clone(); + move |_: &ContextImpl| { + runs.fetch_add(1, Ordering::Relaxed); + GPoll::Final(2.0) + } + }); let union = core_types::record::Layout::union(&[<, &lf]); let graph = SwitchNode::new(cond, if_true, if_false, &union, &lc); let out = Node::::layout(&graph).clone(); reserve_for(&[&lc, <, &lf, &out]); - let GPoll::Final(value) = Node::eval(&graph, &ctx) else { + let GPoll::Final(value) = serve_edge(&graph, &ctx) else { panic!("expected a final record"); }; assert_eq!(element::(&out, &value), 1.0); @@ -1244,44 +1225,24 @@ mod graphene_test { #[test] fn converted_switch_passes_branch_status_through() { - struct PendingSource; - - impl Node for PendingSource { - type Output = f64; - - fn eval(&self, _input: &Input) -> GPoll { - GPoll::Pending - } - } - - struct PartialSource; - - impl Node for PartialSource { - type Output = f64; - - fn eval(&self, _input: &Input) -> GPoll { - GPoll::Partial(7.0) - } - } - let arena = Arena::new(64).unwrap(); let scope = scope_fixture(&arena); let ctx = ContextImpl::root(&scope); - let (c1, lc1) = lifted(SourceNode(true)); - let (p1, lp1) = lifted(PendingSource); - let (pa1, lpa1) = lifted(PartialSource); + let (c1, lc1) = lifted(|_: &ContextImpl| GPoll::Final(true)); + let (p1, lp1) = lifted(|_: &ContextImpl| GPoll::::Pending); + let (pa1, lpa1) = lifted(|_: &ContextImpl| GPoll::Partial(7.0f64)); let pending = SwitchNode::new(c1, p1, pa1, &core_types::record::Layout::union(&[&lp1, &lpa1]), &lc1); - let (c2, lc2) = lifted(SourceNode(false)); - let (p2, lp2) = lifted(PendingSource); - let (pa2, lpa2) = lifted(PartialSource); + let (c2, lc2) = lifted(|_: &ContextImpl| GPoll::Final(false)); + let (p2, lp2) = lifted(|_: &ContextImpl| GPoll::::Pending); + let (pa2, lpa2) = lifted(|_: &ContextImpl| GPoll::Partial(7.0f64)); let partial = SwitchNode::new(c2, p2, pa2, &core_types::record::Layout::union(&[&lp2, &lpa2]), &lc2); let out = Node::::layout(&partial).clone(); reserve_for(&[&lc1, &lp1, &lpa1, &lc2, &lp2, &lpa2, &out]); - assert!(matches!(Node::eval(&pending, &ctx), GPoll::Pending)); - let GPoll::Partial(value) = Node::eval(&partial, &ctx) else { + assert!(matches!(serve_edge(&pending, &ctx), GPoll::Pending)); + let GPoll::Partial(value) = serve_edge(&partial, &ctx) else { panic!("expected a partial record"); }; assert_eq!(element::(&out, &value), 7.0); @@ -1289,29 +1250,19 @@ mod graphene_test { #[test] fn converted_switch_merges_condition_status_into_the_branch_result() { - struct PartialCondition; - - impl Node for PartialCondition { - type Output = bool; - - fn eval(&self, _input: &Input) -> GPoll { - GPoll::Partial(true) - } - } - let arena = Arena::new(64).unwrap(); let scope = scope_fixture(&arena); let ctx = ContextImpl::root(&scope); - let (cond, lc) = lifted(PartialCondition); - let (if_true, lt) = lifted(SourceNode(1.0f64)); - let (if_false, lf) = lifted(SourceNode(2.0f64)); + let (cond, lc) = lifted(|_: &ContextImpl| GPoll::Partial(true)); + let (if_true, lt) = lifted(|_: &ContextImpl| GPoll::Final(1.0f64)); + let (if_false, lf) = lifted(|_: &ContextImpl| GPoll::Final(2.0f64)); let union = core_types::record::Layout::union(&[<, &lf]); let graph = SwitchNode::new(cond, if_true, if_false, &union, &lc); let out = Node::::layout(&graph).clone(); reserve_for(&[&lc, <, &lf, &out]); - let GPoll::Partial(value) = Node::eval(&graph, &ctx) else { + let GPoll::Partial(value) = serve_edge(&graph, &ctx) else { panic!("expected a partial record"); }; assert_eq!(element::(&out, &value), 1.0); @@ -1319,27 +1270,17 @@ mod graphene_test { #[test] fn generated_eval_computes_on_stand_in_and_traces_fallback() { - struct FallbackNode; - - impl Node for FallbackNode { - type Output = f64; - - fn eval(&self, _input: &Input) -> GPoll { - GPoll::fallback(0.0, "upstream failed") - } - } - let arena = Arena::new(64).unwrap(); let scope = scope_fixture(&arena); let ctx = ContextImpl::root(&scope); - let (fallback, lfb) = lifted(FallbackNode); - let (src, ls) = lifted(SourceNode(5.0f64)); + let (fallback, lfb) = lifted(|_: &ContextImpl| GPoll::fallback(0.0f64, "upstream failed")); + let (src, ls) = lifted(|_: &ContextImpl| GPoll::Final(5.0f64)); let out = out_layout::(); let graph = installed(AddNode::<_, _, f64, f64>::new(fallback, src, &lfb, &ls), &out); reserve_for(&[&lfb, &ls, &out]); - let GPoll::Fallback(boxed) = Node::eval(&graph, &ctx) else { + let GPoll::Fallback(boxed) = serve_edge(&graph, &ctx) else { panic!("fallback must propagate with the computed stand-in"); }; assert_eq!(element::(&out, &boxed.0), 5.0); diff --git a/node-graph/nodes/repeat/src/repeat_nodes.rs b/node-graph/nodes/repeat/src/repeat_nodes.rs index 6fee231a8d..9c3e7e1754 100644 --- a/node-graph/nodes/repeat/src/repeat_nodes.rs +++ b/node-graph/nodes/repeat/src/repeat_nodes.rs @@ -181,37 +181,33 @@ mod test { use super::*; use core_types::SourceId; use core_types::arena::Arena; - use core_types::context::{ContextImpl, EvalScope}; + use core_types::context::{ContextImpl, EvalScope, ExtractArena}; use core_types::node::Node; - use core_types::record::{FieldWrite, FrameBuilder, Layout, RecordSource, RecordValue, capture, element_write, stack}; + use core_types::record::{FieldWrite, FrameBuilder, FrameClaim, Layout, RecordSource, Served, capture, element_write, stack}; + use core_types::value::ValueSource; use vector_types::subpath::Subpath; - struct ValueNode(T); - - impl Node for ValueNode { - type Output = T; - - fn eval(&self, _input: &Input) -> GPoll { - GPoll::Final(self.0.clone()) - } - } - struct TransformSource { layout: Layout, element: f64, transform: DAffine2, } - impl<'e> Node> for TransformSource { - type Output = RecordValue<'e>; - - fn eval(&self, input: &ContextImpl<'e>) -> GPoll> { - use core_types::context::ExtractArena; - let mut frame = FrameBuilder::new(&self.layout, input.arena()); + impl Node for TransformSource { + fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll> + where + C: ExtractArena, + { + let mut frame = FrameBuilder::new(&self.layout, ExtractArena::arena(input)); frame.element(self.element); frame.attr::(self.transform); let Some(value) = frame.finish() else { return GPoll::arena_exhausted() }; - GPoll::Final(value) + // SAFETY: the builder served a record of this node's layout. + GPoll::Final(unsafe { slot.forward(&value) }) + } + + fn layout(&self) -> &Layout { + &self.layout } } @@ -225,20 +221,24 @@ mod test { rows: Vec<(Vector, DAffine2)>, } - impl<'e> Node> for VectorRows { - type Output = RecordValue<'e>; - - fn eval(&self, input: &ContextImpl<'e>) -> GPoll> { - use core_types::context::{ExtractArena, ExtractIndices}; + impl Node for VectorRows { + fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll> + where + C: ExtractArena, + { let (vector, transform) = &self.rows[input.innermost_index() as usize % self.rows.len()]; - let mut frame = FrameBuilder::new(&self.layout, input.arena()); + let mut frame = FrameBuilder::new(&self.layout, ExtractArena::arena(input)); frame.element(vector.clone()); frame.attr::(*transform); let Some(value) = frame.finish() else { return GPoll::arena_exhausted() }; - GPoll::Final(value) + // SAFETY: the builder served a record of this node's layout. + GPoll::Final(unsafe { slot.forward(&value) }) } - fn extent_at(&self, _input: &ContextImpl<'e>, _level: u8) -> GPoll { + fn extent_at<'x>(&self, _input: &C, _level: u8) -> GPoll + where + C: ExtractArena, + { GPoll::Final(Extent::Exactly(self.rows.len())) } @@ -255,17 +255,18 @@ mod test { layout: Layout, } - impl<'e> Node> for PositionProbe { - type Output = RecordValue<'e>; - - fn eval(&self, input: &ContextImpl<'e>) -> GPoll> { - use core_types::context::{ExtractArena, ExtractPosition}; + impl Node for PositionProbe { + fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll> + where + C: ExtractArena, + { let position = input.try_position().and_then(|mut positions| positions.next()).unwrap_or(DVec2::ZERO); - let mut frame = FrameBuilder::new(&self.layout, input.arena()); + let mut frame = FrameBuilder::new(&self.layout, ExtractArena::arena(input)); frame.element(position.x); frame.attr::(DAffine2::IDENTITY); let Some(value) = frame.finish() else { return GPoll::arena_exhausted() }; - GPoll::Final(value) + // SAFETY: the builder served a record of this node's layout. + GPoll::Final(unsafe { slot.forward(&value) }) } fn layout(&self) -> &Layout { @@ -293,9 +294,9 @@ mod test { let mut node = RepeatArrayNode::new( RecordSource::new(content, &layout, &layout), - ValueNode(DVec2::new(10., 0.)), - ValueNode(0.0f64), - ValueNode(3u32), + ValueSource::new(DVec2::new(10., 0.)), + ValueSource::new(0.0f64), + ValueSource::new(3u32), &layout, ); Node::::set_layout(&mut node, repeat_array_layout_meta().resolve(&[Some(&layout)])); @@ -332,7 +333,7 @@ mod test { transform: local, }; - let mut node = RepeatRadialNode::new(RecordSource::new(content, &layout, &layout), ValueNode(90.0f64), ValueNode(2.0f64), ValueNode(4u32), &layout); + let mut node = RepeatRadialNode::new(RecordSource::new(content, &layout, &layout), ValueSource::new(90.0f64), ValueSource::new(2.0f64), ValueSource::new(4u32), &layout); Node::::set_layout(&mut node, repeat_radial_layout_meta().resolve(&[Some(&layout)])); assert_eq!(node.extent_at(&ctx, 0), GPoll::Final(Extent::Exactly(4))); @@ -371,7 +372,7 @@ mod test { let content_layout = transform_layout(); let content = PositionProbe { layout: content_layout.clone() }; - let mut node = RepeatOnPointsNode::new(RecordSource::new(content, &content_layout, &content_layout), points, ValueNode(false), &content_layout); + let mut node = RepeatOnPointsNode::new(RecordSource::new(content, &content_layout, &content_layout), points, ValueSource::new(false), &content_layout); Node::::set_layout(&mut node, repeat_on_points_layout_meta().resolve(&[Some(&content_layout)])); let leveled = Node::::layout(&node).clone(); assert_eq!(leveled.depth, 1); @@ -407,7 +408,7 @@ mod test { let content_layout = transform_layout(); let content = PositionProbe { layout: content_layout.clone() }; - let mut node = RepeatOnPointsNode::new(RecordSource::new(content, &content_layout, &content_layout), points, ValueNode(true), &content_layout); + let mut node = RepeatOnPointsNode::new(RecordSource::new(content, &content_layout, &content_layout), points, ValueSource::new(true), &content_layout); Node::::set_layout(&mut node, repeat_on_points_layout_meta().resolve(&[Some(&content_layout)])); let mut expected = positions.clone();