Flip the Node trait from eval onto the frame claim's serve

This commit is contained in:
Dennis Kobert
2026-08-29 12:26:46 +00:00
parent 277641d27e
commit 3af6834d3c
22 changed files with 1226 additions and 1156 deletions

View File

@@ -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::<RenderOutput>() {
let layout = handle.layout().clone();
return Ok(handle
.downcast_record::<RenderOutput>()
.map_err(|e| format!("{e:?}"))?
.eval(ctx)
let edge = handle.downcast_record::<RenderOutput>().map_err(|e| format!("{e:?}"))?;
return Ok(core_types::record::serve_edge(&edge, ctx)
.map(|value| TaggedValue::RenderOutput(unsafe { core_types::record::read_element::<RenderOutput>(layout.rec(&value)) })));
}
Err(format!("Cannot convert edge of type {ty} to TaggedValue"))

View File

@@ -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::<u32>(layout.rec(&value)) }, 2);
@@ -666,7 +672,10 @@ mod test {
let layout = handle.layout().clone();
let edge = handle.duplicate().downcast_record::<f64>().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::<f64>(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::<graphene_std::raster::color::Color>().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),

View File

@@ -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<T: Copy> 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<Input> {
type Output;
fn eval(&self, input: &Input) -> GPoll<Self::Output>;
/// 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<crate::record::Served<'e>>
where
Self::Output: Send + Sync + dyn_any::StaticTypeSized,
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
slot.lift_served(self.eval(input), input.arena())
}
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>;
/// 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<Extent> {
fn extent_at<'e>(&self, _input: &Input, _level: u8) -> GPoll<Extent>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
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<Extent> {
fn extent<'e>(&self, input: &Input, at: Level) -> GPoll<Extent>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
let product = |range: core::ops::Range<u8>| 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<Input> {
/// 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<u64>, scratch: Option<&'a mut [MaybeUninit<u64>]>) -> BatchStatus<'a>
fn eval_batch<'a, 'e>(&'a self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<u64>]>) -> BatchStatus<'a>
where
Input: InjectIndex + Copy,
Input: InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
let _ = (input, range, scratch);
BatchStatus::Unbatched
@@ -401,13 +392,17 @@ impl<Input, N> Node<Input> for &N
where
N: Node<Input> + ?Sized,
{
type Output = N::Output;
fn eval(&self, input: &Input) -> GPoll<Self::Output> {
(**self).eval(input)
fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'l>) -> GPoll<crate::record::Served<'e>>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).serve(input, slot)
}
fn extent_at(&self, input: &Input, level: u8) -> GPoll<Extent> {
fn extent_at<'e>(&self, input: &Input, level: u8) -> GPoll<Extent>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).extent_at(input, level)
}
@@ -419,9 +414,9 @@ where
(**self).layout()
}
fn eval_batch<'a>(&'a self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<u64>]>) -> BatchStatus<'a>
fn eval_batch<'a, 'e>(&'a self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<u64>]>) -> BatchStatus<'a>
where
Input: InjectIndex + Copy,
Input: InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).eval_batch(input, range, scratch)
}
@@ -431,13 +426,17 @@ impl<Input, N> Node<Input> for Box<N>
where
N: Node<Input> + ?Sized,
{
type Output = N::Output;
fn eval(&self, input: &Input) -> GPoll<Self::Output> {
(**self).eval(input)
fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'l>) -> GPoll<crate::record::Served<'e>>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).serve(input, slot)
}
fn extent_at(&self, input: &Input, level: u8) -> GPoll<Extent> {
fn extent_at<'e>(&self, input: &Input, level: u8) -> GPoll<Extent>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).extent_at(input, level)
}
@@ -449,9 +448,9 @@ where
(**self).layout()
}
fn eval_batch<'a>(&'a self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<u64>]>) -> BatchStatus<'a>
fn eval_batch<'a, 'e>(&'a self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<u64>]>) -> BatchStatus<'a>
where
Input: InjectIndex + Copy,
Input: InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).eval_batch(input, range, scratch)
}
@@ -461,13 +460,17 @@ impl<Input, N> Node<Input> for std::sync::Arc<N>
where
N: Node<Input> + ?Sized,
{
type Output = N::Output;
fn eval(&self, input: &Input) -> GPoll<Self::Output> {
(**self).eval(input)
fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'l>) -> GPoll<crate::record::Served<'e>>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).serve(input, slot)
}
fn extent_at(&self, input: &Input, level: u8) -> GPoll<Extent> {
fn extent_at<'e>(&self, input: &Input, level: u8) -> GPoll<Extent>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).extent_at(input, level)
}
@@ -479,9 +482,9 @@ where
(**self).layout()
}
fn eval_batch<'a>(&'a self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<u64>]>) -> BatchStatus<'a>
fn eval_batch<'a, 'e>(&'a self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<u64>]>) -> BatchStatus<'a>
where
Input: InjectIndex + Copy,
Input: InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**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<Input, N: Node<Input>>(&self, input_index: usize, node: &N, input: &Input) -> Result<N::Output, Interrupt> {
match node.eval(input) {
GPoll::Final(value) => Ok(value),
pub fn eval_input<'e, Input, N: Node<Input> + ?Sized>(&self, input_index: usize, node: &N, input: &Input) -> Result<crate::record::RecordValue<'e>, Interrupt>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
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<Input>(&self, ctx: &Input) -> Result<N::Output, Interrupt>
pub fn eval<'e, Input>(&self, ctx: &Input) -> Result<crate::record::RecordValue<'e>, Interrupt>
where
N: Node<Input>,
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<Input>(&self, ctx: &Input, at: Level) -> GPoll<Extent>
pub fn extent<'e, Input>(&self, ctx: &Input, at: Level) -> GPoll<Extent>
where
N: Node<Input>,
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
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<TestInput> for Double {
type Output = u64;
fn eval(&self, input: &TestInput) -> GPoll<u64> {
GPoll::Final(input.index * 2)
fn arena(&self) -> &'a Arena {
self.arena
}
}
fn double<'a>() -> LiftedSource<u64, impl Fn(&TestInput<'a>) -> GPoll<u64>> {
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<dyn Node<TestInput, Output = u64>> = 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::<TestInput>::layout(&node).clone();
let erased: Box<dyn Node<TestInput>> = 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::<u64>(layout.rec(&value)) }, 42);
assert!(matches!(erased.eval_batch(&input, 0..2, None), BatchStatus::Unbatched));
}
}

View File

@@ -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<Derived<'d, C>, 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<Derived<'d, C>>` cannot also say the derived context's arena
/// is at `'d`: the equality binding `ExtractArena<ArenaRef = &'d Arena>` 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<RecordValue<'derived>, crate::gpoll::Interrupt>;
/// [`serve_edge`] at the derived context, for poll kernels that carry the
/// status themselves.
fn serve_derived(&self, ctx: &C) -> GPoll<RecordValue<'derived>>;
fn extent_at_derived(&self, ctx: &C, level: u8) -> GPoll<crate::gpoll::Extent>;
}
impl<'derived, C, N> DerivedRecordEdge<'derived, C> for N
where
N: Node<C, Output = RecordValue<'derived>>,
N: Node<C>,
C: crate::context::ExtractArena<ArenaRef = &'derived crate::arena::Arena>,
{
fn eval_derived(&self, cell: &crate::node::StatusCell, input_index: usize, ctx: &C) -> Result<RecordValue<'derived>, crate::gpoll::Interrupt> {
cell.eval_input(input_index, self, ctx)
}
fn serve_derived(&self, ctx: &C) -> GPoll<RecordValue<'derived>> {
serve_edge(self, ctx)
}
fn extent_at_derived(&self, ctx: &C, level: u8) -> GPoll<crate::gpoll::Extent> {
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<u64>, scratch: Option<&'a mut [std::mem::MaybeUninit<u64>]>) -> crate::node::BatchStatus<'a>
where
C: crate::context::InjectIndex + Copy,
N: Node<C, Output = RecordValue<'e>>,
C: crate::context::InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
N: Node<C>,
{
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<u64>, arena: &'a crate::arena::Arena) -> crate::node::BatchStatus<'a>
where
C: crate::context::InjectIndex + Copy,
N: Node<C, Output = RecordValue<'e>>,
C: crate::context::InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
N: Node<C>,
{
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, Output = RecordValue<'e>>,
C: crate::context::InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
N: Node<C>,
{
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<C, Output = RecordValue<'e>> {}
impl<'e, C, N: Node<C, Output = RecordValue<'e>>> 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<T>, layout: &Layout, arena: &'e crate::arena::Arena) -> GPoll<RecordValue<'e>> {
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<RecordValue<'e>>
/// 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<Served<'e>>
where
N: Node<C, Output = RecordValue<'e>>,
N: Node<C>,
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
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, Output = RecordValue<'e>>,
C: crate::context::InjectIndex + Copy,
N: Node<C>,
C: crate::context::InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
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<Out>
where
N: Node<C, Output = RecordValue<'d>>,
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<Out, crate::gpoll::Interrupt>
where
N: Node<C, Output = RecordValue<'d>>,
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<T>, arena: &'e crate::arena::Arena) -> GPoll<Served<'e>> {
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<RecordValue<'e>>
where
N: Node<C> + ?Sized,
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
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<C, Output = RecordValue<'e>>>(node: &N, ctx: &C) -> GPoll<ServedRecord> {
pub fn capture<'e, C, N>(node: &N, ctx: &C) -> GPoll<ServedRecord>
where
N: Node<C> + ?Sized,
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
// 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<C, Output = RecordValue<'e>>>(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<El, N> {
edge: N,
pub struct LiftedSource<El, F> {
kernel: F,
layout: Layout,
_marker: std::marker::PhantomData<fn() -> El>,
}
impl<El: Clone + Send + Sync + dyn_any::StaticTypeSized, N> RecordLift<El, N>
impl<El: Clone + Send + Sync + dyn_any::StaticTypeSized, F> LiftedSource<El, F>
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::<El>(), &[]),
_marker: std::marker::PhantomData,
}
}
}
impl<'e, C, El, N> Node<C> for RecordLift<El, N>
impl<C, El, F> Node<C> for LiftedSource<El, F>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
El: Send + Sync + 'static,
N: Node<C, Output = El>,
El: Send + Sync + dyn_any::StaticTypeSized,
F: Fn(&C) -> GPoll<El>,
{
type Output = RecordValue<'e>;
fn eval(&self, input: &C) -> GPoll<RecordValue<'e>> {
lift_poll(self.edge.eval(input), &self.layout, input.arena())
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll<Served<'e>>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
slot.lift_served((self.kernel)(input), input.arena())
}
fn layout(&self) -> &Layout {
@@ -2021,54 +2006,63 @@ impl<El, N> RecordExtract<El, N> {
}
}
impl<'e, C, El, N> Node<C> for RecordExtract<El, N>
where
El: Clone + 'static,
N: Node<C, Output = RecordValue<'e>>,
{
type Output = El;
fn eval(&self, input: &C) -> GPoll<El> {
impl<El: Clone + 'static, N> RecordExtract<El, N> {
/// The edge's element, copied out of its record.
pub fn eval<'e, C>(&self, input: &C) -> GPoll<El>
where
N: Node<C>,
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
// 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::<El>(self.layout.rec(&value)) })
serve_edge(&self.edge, input).map(|value| unsafe { read_element::<El>(self.layout.rec(&value)) })
}
}
impl<'e, C, N> Node<C> for RecordSource<N>
impl<C, N> Node<C> for RecordSource<N>
where
N: Node<C, Output = RecordValue<'e>>,
N: Node<C>,
{
type Output = RecordValue<'e>;
fn eval(&self, input: &C) -> GPoll<RecordValue<'e>> {
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<Served<'e>>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
// 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<crate::gpoll::Extent> {
fn extent_at<'x>(&self, input: &C, level: u8) -> GPoll<crate::gpoll::Extent>
where
C: crate::context::ExtractArena<ArenaRef = &'x crate::arena::Arena>,
{
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<RecordValue<'e>> {
let mut frame = FrameBuilder::new(&self.layout, arena);
impl<C> Node<C> for Fixture {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll<Served<'e>>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
let mut frame = FrameBuilder::new(&self.layout, crate::context::ExtractArena::arena(input));
frame.element(String::from("parked"));
frame.attr::<Transform>(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::<String>(), &[FieldWrite::of::<Transform>(0), FieldWrite::of::<Opacity>(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");

View File

@@ -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<ContextImpl<'c>, Output = crate::record::RecordValue<'c>> + Send + Sync;
pub type ErasedRecordNode = dyn for<'c> Node<ContextImpl<'c>> + Send + Sync;
#[cfg(target_family = "wasm")]
pub type ErasedRecordNode = dyn for<'c> Node<ContextImpl<'c>, Output = crate::record::RecordValue<'c>>;
pub type ErasedRecordNode = dyn for<'c> Node<ContextImpl<'c>>;
#[cfg(not(target_family = "wasm"))]
type DynEdge = dyn std::any::Any + Send + Sync;
@@ -136,12 +136,13 @@ impl<Input, N> Node<Input> for SharedEdge<N>
where
N: Node<Input> + ?Sized,
{
type Output = N::Output;
fn eval(&self, input: &Input) -> crate::gpoll::GPoll<Self::Output> {
// 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<crate::record::Served<'e>>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
// 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::<N>(),
self.layout().frame_bytes(),
@@ -185,26 +186,29 @@ where
result
}
fn extent_at(&self, input: &Input, level: u8) -> crate::gpoll::GPoll<crate::gpoll::Extent> {
// SAFETY: as in eval.
fn extent_at<'x>(&self, input: &Input, level: u8) -> crate::gpoll::GPoll<crate::gpoll::Extent>
where
Input: crate::context::ExtractArena<ArenaRef = &'x crate::arena::Arena>,
{
// SAFETY: as in serve.
unsafe { self.ptr.as_ref() }.extent_at(input, level)
}
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
// 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<u64>, scratch: Option<&'a mut [std::mem::MaybeUninit<u64>]>) -> crate::node::BatchStatus<'a>
fn eval_batch<'a, 'x>(&'a self, input: &'a Input, range: std::ops::Range<u64>, scratch: Option<&'a mut [std::mem::MaybeUninit<u64>]>) -> crate::node::BatchStatus<'a>
where
Input: crate::context::InjectIndex + Copy,
Input: crate::context::InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'x crate::arena::Arena>,
{
// 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<Input> Node<Input> for CountingNode {
type Output = u32;
fn eval(&self, _input: &Input) -> GPoll<u32> {
GPoll::Final(self.0.fetch_add(1, Ordering::Relaxed) + 1)
}
}
struct LendNode(String);
impl<'e, Input: Ctx + ExtractArena<ArenaRef = &'e Arena>> Node<Input> 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<u32, impl for<'c> Fn(&ContextImpl<'c>) -> GPoll<u32>> {
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<Node0> {
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<Input> for SplitNode<Node0>
impl<Input: Ctx> Node<Input> for LendNode {
fn serve<'e, 'l>(&self, input: &Input, slot: FrameClaim<'l>) -> GPoll<Served<'e>>
where
Input: Ctx,
Node0: Node<Input, Output = &'e String>,
Input: ExtractArena<ArenaRef = &'e Arena>,
{
type Output = SplitBorrow<'e>;
fn eval(&self, input: &Input) -> GPoll<SplitBorrow<'e>> {
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<ContextImpl<'c>, 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<ErasedSplitEdge> = 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::<ContextImpl>::layout(&node).clone();
let handle = EdgeHandle::new_erased(Arc::new(node) as Arc<ErasedRecordNode>, concrete!(String));
assert_eq!(*handle.ty(), concrete!(String));
let wired = handle.downcast_erased::<ErasedSplitEdge>(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::<ErasedRecordNode>(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<Node0, T> {
content: Node0,
inner: Layout,
layout: Layout,
_marker: std::marker::PhantomData<fn() -> T>,
}
impl<Node0, T: Clone + Send + Sync + dyn_any::StaticTypeSized> RepeatNode<Node0, T>
where
Vec<T>: Clone + Send + Sync + dyn_any::StaticTypeSized,
<Vec<T> 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::<Vec<T>>(), &[]),
_marker: std::marker::PhantomData,
}
}
}
impl<C, T, Node0> Node<C> for RepeatNode<Node0, T>
where
C: Ctx + crate::context::DeriveCtx,
T: Clone + 'static,
Vec<T>: 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<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
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::<T>(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<Node0> {
content: Node0,
}
impl<C, T, Node0> Node<C> for RepeatNode<Node0>
where
C: Ctx + DeriveCtx,
Node0: for<'x> Node<Derived<'x, C>, Output = T>,
{
type Output = Vec<T>;
fn eval(&self, input: &C) -> GPoll<Vec<T>> {
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<Input: ExtractIndex> Node<Input> for LevelsNode {
type Output = Vec<usize>;
fn eval(&self, input: &Input) -> GPoll<Vec<usize>> {
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::<Vec<Vec<Vec<usize>>>, _>::new(RepeatNode {
content: RepeatNode { content: LevelsNode },
});
let levels = LiftedSource::<Vec<usize>, _>::new(|input: &ContextImpl| GPoll::Final(input.try_index().map(|levels| levels.collect()).unwrap_or_default()));
let levels_layout = Node::<ContextImpl>::layout(&levels).clone();
let inner = RepeatNode::<_, Vec<usize>>::new(levels, levels_layout);
let inner_layout = Node::<ContextImpl>::layout(&inner).clone();
let nested = RepeatNode::<_, Vec<Vec<usize>>>::new(inner, inner_layout);
let layout = Node::<ContextImpl>::layout(&nested).clone();
let erased: Box<ErasedRecordNode> = 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::<Vec<Vec<Vec<usize>>>>(layout.rec(&value)) };
let outer = unsafe { read_element::<Vec<Vec<Vec<usize>>>>(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<Node0> {
content: Node0,
inner: Layout,
layout: Layout,
}
impl<Node0> ShiftFootprintNode<Node0> {
fn new(content: Node0, inner: Layout) -> Self {
Self {
content,
inner,
layout: Layout::default().with_writes(0, element_write::<u32>(), &[]),
}
}
}
impl<C, Node0> Node<C> for ShiftFootprintNode<Node0>
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<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
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::<u32>(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<Node0> {
content: Node0,
}
impl<C, T, Node0> Node<C> for ShiftFootprintNode<Node0>
where
C: Ctx + DeriveCtx + ExtractFootprint,
Node0: for<'x> Node<Derived<'x, C>, Output = T>,
{
type Output = T;
fn eval(&self, input: &C) -> GPoll<T> {
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<Input: ExtractFootprint> Node<Input> for ResolutionNode {
type Output = u32;
fn eval(&self, input: &Input) -> GPoll<u32> {
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::<u32, _>::new(|input: &ContextImpl| GPoll::Final(input.try_footprint().map(|footprint| footprint.resolution.x).unwrap_or(0)));
let resolution_layout = Node::<ContextImpl>::layout(&resolution).clone();
let shifted = ShiftFootprintNode::new(resolution, resolution_layout);
let shifted_layout = Node::<ContextImpl>::layout(&shifted).clone();
let graph = ShiftFootprintNode::new(shifted, shifted_layout);
let layout = Node::<ContextImpl>::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::<u32>(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::<u32, _>::new(CountingNode(AtomicU32::new(0)));
let counting = counting();
let layout = Node::<ContextImpl>::layout(&counting).clone();
let handle = EdgeHandle::new_record::<u32>(Arc::new(counting) as Arc<ErasedRecordNode>);
let duplicate = handle.duplicate();
assert_eq!(*duplicate.ty(), record_edge_type::<u32>());
// 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::<u32>().unwrap();
let second = duplicate.downcast_record::<u32>().unwrap();
// SAFETY: each record was served at `layout`, whose element is the count.
let count = |value| unsafe { layout.rec(&value).element::<u32>() };
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));
}
}

View File

@@ -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>(T);
impl<T: Clone, Input> Node<Input> for SourceNode<T> {
type Output = T;
fn eval(&self, _input: &Input) -> GPoll<T> {
GPoll::Final(self.0.clone())
}
}
fn element_layout<T: Clone + Send + Sync + dyn_any::StaticTypeSized>() -> Layout
where
T::Static: Clone + Send + Sync,
@@ -265,11 +255,11 @@ mod tests {
Layout::default().with_writes(0, element_write::<T>(), &[])
}
fn lifted<T: Clone + Send + Sync + dyn_any::StaticTypeSized>(value: T) -> RecordLift<T, SourceNode<T>>
fn lifted<T: Clone + Send + Sync + dyn_any::StaticTypeSized>(value: T) -> LiftedSource<T, impl for<'c> Fn(&ContextImpl<'c>) -> GPoll<T>>
where
T::Static: Clone + Send + Sync,
{
RecordLift::new(SourceNode(value))
LiftedSource::new(move |_: &ContextImpl<'_>| GPoll::Final(value.clone()))
}
fn extract<El: Clone + Send + Sync + dyn_any::StaticTypeSized, N: Node<ContextImpl<'static>>>(mut graph: N) -> RecordExtract<El, N>
@@ -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::<El>();
unsafe {
stack::reserve(1 << 12);
}
let layout = element_layout::<El>();
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<std::sync::atomic::AtomicBool>, f64);
impl<Input> Node<Input> for GatedSource {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
match self.0.load(Ordering::Relaxed) {
true => GPoll::Final(self.1),
false => GPoll::Pending,
}
}
fn gated(gate: Arc<std::sync::atomic::AtomicBool>, value: f64) -> LiftedSource<f64, impl for<'c> Fn(&ContextImpl<'c>) -> GPoll<f64>> {
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::<u64>(),
));
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::<u64>(),
));
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::<u64>(),
));
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::<u64>(),
));
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::<f64, _>(StagedSumNode::new(
lifted(40.0f64),
RecordLift::<f64, _>::new(GatedSource(gate.clone(), 2.0)),
gated(gate.clone(), 2.0),
lifted(RuntimeHandle(runtime.clone())),
lifted(9u64),
&element_layout::<f64>(),
@@ -467,12 +454,12 @@ mod tests {
&element_layout::<u64>(),
));
assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending);
assert_eq!(graph.eval(&ctx), GPoll::Pending);
assert_eq!(runtime.drain(), Vec::<SourceId>::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::<u64>(),
));
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::<u64>(),
));
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));

View File

@@ -17,15 +17,15 @@ where
}
}
impl<'e, C, T> crate::node::Node<C> for ValueSource<T>
impl<C, T> crate::node::Node<C> for ValueSource<T>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
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::RecordValue<'e>> {
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<crate::record::Served<'e>>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
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<C> for LeveledValueSource<T>
impl<C, T> crate::node::Node<C> for LeveledValueSource<T>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena> + 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<crate::record::RecordValue<'e>> {
fn serve<'e, 'l>(&self, input: &C, slot: crate::record::FrameClaim<'l>) -> crate::gpoll::GPoll<crate::record::Served<'e>>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
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<crate::gpoll::Extent> {
fn extent_at<'x>(&self, _input: &C, level: u8) -> crate::gpoll::GPoll<crate::gpoll::Extent>
where
C: crate::context::ExtractArena<ArenaRef = &'x crate::arena::Arena>,
{
match level {
0 => crate::gpoll::GPoll::Final(crate::gpoll::Extent::Exactly(self.values.len())),
_ => crate::gpoll::GPoll::Final(crate::gpoll::Extent::Exactly(1)),

View File

@@ -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, Output = RecordValue<'e>>,
C: InjectIndex + Copy + core_types::context::ExtractArena<ArenaRef = &'e Arena>,
N: Node<C>,
{
match materialize_level(node, input, arena) {
LevelStatus::Batch(batch, finality) => {

View File

@@ -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<TokenStream2> = 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<ArenaRef = &'__record #core_types::arena::Arena>));
}
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<TokenStream2> = 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<String> = 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<TokenStream2> = 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<TokenStream2> = 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::<Vec<(usize, &AttributeRead)>>()
};
// 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, <Self as #core_types::node::Node<#ctx_ident>>::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(<Self as #core_types::node::Node<#ctx_ident>>::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 &regular_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<ArenaRef = &'__serve #core_types::arena::Arena>,
{
#(#arg_decls)*
let __level_in = #core_types::extent::LevelIn::new(__level, <Self as #core_types::node::Node<#ctx_ident>>::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<ArenaRef = &'__serve #core_types::arena::Arena>,
{
#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<ArenaRef = &'__serve #core_types::arena::Arena>,
{
#query
let __arg = #core_types::extent::ExtentIn::new(&__query);
let __level_in = #core_types::extent::LevelIn::new(__level, <Self as #core_types::node::Node<#ctx_ident>>::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<ArenaRef = &'__serve #core_types::arena::Arena>,
{
#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<u64>,
__scratch: Option<&'__batch mut [::std::mem::MaybeUninit<u64>]>,
) -> #core_types::node::BatchStatus<'__batch>
where
#ctx_ident: #core_types::context::InjectIndex + Copy,
#ctx_ident: #core_types::context::InjectIndex + Copy + #core_types::context::ExtractArena<ArenaRef = &'__serve #core_types::arena::Arena>,
};
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<TokenStream2> = {
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<ArenaRef = &'__record #core_types::arena::Arena>)]
} 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<TokenStream2> = 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<Self::Output> {
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<ArenaRef = &'__serve #core_types::arena::Arena>,
{
// The exit trace rides a guard so early returns report too, which
// is what pins a frame leak to its node.
#[cfg(debug_assertions)]

View File

@@ -304,7 +304,7 @@ pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option<RecordShape> {
// 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<RoutingIo> {
@@ -739,45 +741,9 @@ pub(crate) fn named_serving_lifetime(ty: &Type) -> Option<Lifetime> {
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>) -> 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<ArenaRef = &#lifetime #core_types::arena::Arena>)
}

View File

@@ -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>()?;
},
}

View File

@@ -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<Context<'_>, Output = RecordValue<'e>>) -> GPoll<RecordValue<'e>> { content.eval(()) }
fn memo<'e, 'l>(_: impl Ctx, #[data] cache: Store, content: impl Node<Context<'_>>, slot: FrameClaim<'l>) -> GPoll<Served<'e>> { 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<Context<'_>, Output = RecordValue<'e>>) -> GPoll<RecordValue<'e>> {
content.eval(())
fn memo<'e, 'l>(_: impl Ctx, #[data] cache: Store, content: impl Node<Context<'_>>, slot: FrameClaim<'l>) -> GPoll<Served<'e>> {
content.serve(&(), slot)
}
),
);

View File

@@ -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,
};

View File

@@ -36,6 +36,9 @@ pub(crate) struct ParsedNodeFn {
pub(crate) output_depth: u8,
pub(crate) is_async: bool,
pub(crate) fields: Vec<ParsedField>,
/// The caller's frame claim, declared by a record-opaque kernel that
/// serves through it; not a wired input.
pub(crate) claim: Option<PatType>,
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<FnArg, Comma>) -> syn::Result<(Input, Vec<ParsedField>)> {
fn parse_inputs(inputs: &Punctuated<FnArg, Comma>) -> syn::Result<(Input, Vec<ParsedField>, Option<PatType>)> {
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<FnArg, Comma>) -> syn::Result<(Input, Vec<Pa
if attr_marker(ty).is_some() {
return Err(Error::new_spanned(pat_ident, "an attribute read binds to an input: destructure it as `(value, Attr<..>)`"));
}
// 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<FnArg, Comma>) -> syn::Result<(Input, Vec<Pa
}
let input = input.ok_or_else(|| Error::new_spanned(inputs, "Expected at least one input argument. The first argument should be the node input type."))?;
Ok((input, fields))
Ok((input, fields, claim))
}
/// Whether the parameter is the caller-provided frame claim a record-opaque
/// kernel serves through.
fn is_frame_claim(ty: &Type) -> 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<Input, Output = OutputType>`"))?;
let input_type = node_input_type.ok_or_else(|| Error::new_spanned(&ty, "Invalid Node type. Expected `impl Node<Input>` or `impl Node<Input, Output = OutputType>`"))?;
// 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<Raster<P>>),
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<Raster<CPU>>),
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(),

View File

@@ -318,6 +318,7 @@ impl PerPixelAdjustCodegen<'_> {
},
output_type: raster_gpu,
output_depth: 0,
claim: None,
is_async: false,
fields,
body,

View File

@@ -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() {

View File

@@ -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<Mutex<Option<MemoLevel>>>,
content: impl Node<Context<'_>, Output = RecordValue<'e>>,
) -> GPoll<RecordValue<'e>> {
let entry_sp = core_types::record::stack::sp();
content: impl Node<Context<'_>>,
slot: FrameClaim<'l>,
) -> GPoll<Served<'e>> {
// 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<FrameTable<Box<[u8]>, 32>>,
content: impl Node<Context<'_>, Output = RecordValue<'e>>,
) -> GPoll<RecordValue<'e>> {
content: impl Node<Context<'_>>,
frame: FrameClaim<'l>,
) -> GPoll<Served<'e>> {
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<Mutex<Option<CtxSnapshot>>>;
/// (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<Context<'_>, Output = RecordValue<'e>>,
) -> GPoll<RecordValue<'e>> {
content: impl Node<Context<'_>>,
slot: FrameClaim<'l>,
) -> GPoll<Served<'e>> {
if ctx.index() == 0 {
*io.lock().unwrap() = Some(CtxSnapshot::capture(ctx));
}
content.eval(&ctx)
content.serve(&ctx, slot)
}
fn serialize_monitor(io: &MonitorValue) -> Option<Arc<dyn std::any::Any + Send + Sync>> {
@@ -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<Input> Node<Input> for CountingNode {
type Output = u32;
fn eval(&self, _input: &Input) -> GPoll<u32> {
GPoll::Final(self.0.fetch_add(1, Ordering::Relaxed) + 1)
}
fn lifted<T: Clone + Send + Sync + core_types::StaticTypeSized>(value: T) -> LiftedSource<T, impl for<'c> Fn(&ContextImpl<'c>) -> GPoll<T>>
where
T::Static: Clone + Send + Sync,
{
LiftedSource::new(move |_: &ContextImpl<'_>| GPoll::Final(value.clone()))
}
struct PartialCountingNode(AtomicU32);
impl<Input> Node<Input> for PartialCountingNode {
type Output = u32;
fn eval(&self, _input: &Input) -> GPoll<u32> {
GPoll::Partial(self.0.fetch_add(1, Ordering::Relaxed) + 1)
}
fn counting() -> LiftedSource<u32, impl for<'c> Fn(&ContextImpl<'c>) -> GPoll<u32>> {
let count = AtomicU32::new(0);
LiftedSource::new(move |_: &ContextImpl<'_>| GPoll::Final(count.fetch_add(1, Ordering::Relaxed) + 1))
}
struct ValueNode<T>(T);
impl<T: Clone, Input> Node<Input> for ValueNode<T> {
type Output = T;
fn eval(&self, _input: &Input) -> GPoll<T> {
GPoll::Final(self.0.clone())
}
fn partial_counting() -> LiftedSource<u32, impl for<'c> Fn(&ContextImpl<'c>) -> GPoll<u32>> {
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<T: Clone + Send + Sync + core_types::StaticTypeSized>() -> core_types::record::Layout
@@ -265,12 +255,12 @@ mod tests {
let ctx = ContextImpl::root(&scope);
let layout = element_layout::<u32>();
let monitor = MonitorNode::new(core_types::record::RecordLift::<u32, _>::new(ValueNode(11u32)), &layout);
let monitor = MonitorNode::new(lifted::<u32>(11u32), &layout);
let handle = EdgeHandle::new_record::<u32>(Arc::new(monitor) as Arc<ErasedRecordNode>);
assert!(handle.serialize().is_none(), "no snapshot before the first eval");
let edge = handle.duplicate().downcast_record::<u32>().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::<u32>(Arc::new(monitor) as Arc<ErasedRecordNode>);
let edge = handle.duplicate().downcast_record::<u32>().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::<Payload>();
let memoized = MemoizeNode::new(core_types::record::RecordLift::<Payload, _>::new(ValueNode(Payload("deep".to_string(), 0))), &layout);
let memoized = MemoizeNode::new(lifted::<Payload>(Payload("deep".to_string(), 0)), &layout);
let memoized = core_types::record::RecordExtract::<Payload, _>::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::<u32>();
let memoized = MemoizeNode::new(core_types::record::RecordLift::<u32, _>::new(CountingNode(AtomicU32::new(0))), &layout);
let memoized = MemoizeNode::new(counting(), &layout);
let memoized = core_types::record::RecordExtract::<u32, _>::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::<u32>();
let memoized = MemoizeNode::new(core_types::record::RecordLift::<u32, _>::new(CountingNode(AtomicU32::new(0))), &layout);
let memoized = MemoizeNode::new(counting(), &layout);
let memoized = core_types::record::RecordExtract::<u32, _>::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::<u32>();
let memoized = MemoizeNode::new(core_types::record::RecordLift::<u32, _>::new(PartialCountingNode(AtomicU32::new(0))), &layout);
let memoized = MemoizeNode::new(partial_counting(), &layout);
let memoized = core_types::record::RecordExtract::<u32, _>::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::<u32>();
let edge = EdgeHandle::new_record::<u32>(Arc::new(core_types::record::RecordLift::<u32, _>::new(CountingNode(AtomicU32::new(0)))) as Arc<ErasedRecordNode>);
let edge = EdgeHandle::new_record::<u32>(Arc::new(counting()) as Arc<ErasedRecordNode>);
let memoized = EdgeHandle::new_record::<u32>(Arc::new(MemoizeNode::new(edge.downcast_record::<u32>().unwrap(), &layout)) as Arc<ErasedRecordNode>);
let stacked = MemoizeNode::new(memoized.downcast_record::<u32>().unwrap(), &layout);
let stacked = core_types::record::RecordExtract::<u32, _>::new(stacked, &layout);
@@ -413,12 +403,12 @@ mod tests {
let ctx = ContextImpl::root(&scope);
let layout = element_layout::<String>();
let memo = FrameMemoNode::new(core_types::record::RecordLift::<String, _>::new(ValueNode("lent out".to_string())), &layout);
let memo = FrameMemoNode::new(lifted::<String>("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)) };

View File

@@ -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>(T);
impl<T: Clone, Input> Node<Input> for ValueNode<T> {
type Output = T;
fn eval(&self, _input: &Input) -> GPoll<T> {
GPoll::Final(self.0.clone())
}
}
use core_types::record::{FrameClaim, Layout, LiftedSource, Rec, RecordSource, Served, stack};
use core_types::value::ValueSource;
struct RecordSourceNode<E> {
layout: Layout,
@@ -417,21 +408,28 @@ mod tests {
partial: bool,
}
impl<'e, E: Copy + Send + Sync + 'static> Node<ContextImpl<'e>> for RecordSourceNode<E> {
type Output = RecordValue<'e>;
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
let mut frame = core_types::record::FrameBuilder::new(&self.layout, input.arena());
impl<C, E: Copy + Send + Sync + 'static> Node<C> for RecordSourceNode<E> {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
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::<f64>(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<ContextImpl<'e>> for LeveledSourceNode {
type Output = RecordValue<'e>;
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
impl<C: ExtractIndex> Node<C> for LeveledSourceNode {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
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::<f64>(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<Extent> {
fn extent_at<'x>(&self, _input: &C, _level: u8) -> GPoll<Extent>
where
C: ExtractArena<ArenaRef = &'x Arena>,
{
GPoll::Final(Extent::Exactly(self.elements.len()))
}
@@ -468,19 +471,24 @@ mod tests {
rows: Vec<(f64, DAffine2)>,
}
impl<'e> Node<ContextImpl<'e>> for LeveledTransformSource {
type Output = RecordValue<'e>;
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
impl<C: ExtractIndex> Node<C> for LeveledTransformSource {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
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>(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<Extent> {
fn extent_at<'x>(&self, _input: &C, _level: u8) -> GPoll<Extent>
where
C: ExtractArena<ArenaRef = &'x Arena>,
{
GPoll::Final(Extent::Exactly(self.rows.len()))
}
@@ -496,21 +504,26 @@ mod tests {
count: usize,
}
impl<'e> Node<ContextImpl<'e>> for DrainSourceNode {
type Output = RecordValue<'e>;
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
impl<C: ExtractIndex> Node<C> for DrainSourceNode {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
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<Extent> {
fn extent_at<'x>(&self, _input: &C, _level: u8) -> GPoll<Extent>
where
C: ExtractArena<ArenaRef = &'x Arena>,
{
GPoll::Final(Extent::AtLeast(0))
}
@@ -523,23 +536,32 @@ mod tests {
layout: Layout,
}
impl<'e> Node<ContextImpl<'e>> for IndexSourceNode {
type Output = RecordValue<'e>;
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
impl<C: ExtractIndex + core_types::context::ExtractIndices> Node<C> for IndexSourceNode {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
// 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<T: Clone + Send + Sync + core_types::StaticTypeSized + 'static>(value: T) -> (core_types::record::RecordLift<T, ValueNode<T>>, Layout)
fn lifted_value<T: Clone + Send + Sync + core_types::StaticTypeSized + 'static>(value: T) -> (ValueSource<T>, Layout)
where
T::Static: Clone + Send + Sync,
{
let lift = core_types::record::RecordLift::<T, _>::new(ValueNode(value));
let lift = ValueSource::new(value);
let layout = Node::<ContextImpl>::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::<ContextImpl>::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::<ContextImpl>::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<u32>);
impl<Input> Node<Input> for CountingValue<'_> {
type Output = bool;
fn eval(&self, _input: &Input) -> GPoll<bool> {
self.1.set(self.1.get() + 1);
GPoll::Final(self.0)
}
fn counting_value(value: bool, evals: &std::cell::Cell<u32>) -> LiftedSource<bool, impl for<'c> Fn(&ContextImpl<'c>) -> GPoll<bool> + '_> {
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<u32>);
impl<Input> Node<Input> for CountingValue<'_> {
type Output = bool;
fn eval(&self, _input: &Input) -> GPoll<bool> {
self.1.set(self.1.get() + 1);
GPoll::Final(self.0)
}
fn counting_value(value: bool, evals: &std::cell::Cell<u32>) -> LiftedSource<bool, impl for<'c> Fn(&ContextImpl<'c>) -> GPoll<bool> + '_> {
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::<ContextImpl>::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::<ContextImpl>::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::<ContextImpl>::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::<Opacity>(), 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::<f64, _>::new(ValueNode(3.));
let factor = ValueSource::new(3.);
let factor_layout = Node::<ContextImpl>::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::<ContextImpl>::layout(&unit).clone();
let content_layout = f64_layout(&["opacity"]);
reserve_for(&[&content_layout]);
let run = |opacity: Option<f64>| {
let evals = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
let alternate = core_types::record::RecordLift::<f64, _>::new(CountingValue(evals.clone()));
let alternate = counting_source(evals.clone());
let alternate_layout = Node::<ContextImpl>::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::<ContextImpl>::layout(&node).rec(&value).element::<f64>() };
@@ -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<ContextImpl<'e>> for RealTimeProbe {
type Output = RecordValue<'e>;
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
impl<C: ExtractIndex + core_types::ExtractRealTime> Node<C> for RealTimeProbe {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
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::<String, _>::new(ValueNode(String::from("parked")));
let lift = ValueSource::new(String::from("parked"));
let layout = Node::<ContextImpl>::layout(&lift).clone();
let chain = core_types::record::RecordExtract::<String, _>::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::<f64>(), 4.);
}
struct CountingValue(std::sync::Arc<std::sync::atomic::AtomicU32>);
impl<Input> Node<Input> for CountingValue {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
fn counting_source(evals: std::sync::Arc<std::sync::atomic::AtomicU32>) -> LiftedSource<f64, impl for<'c> Fn(&ContextImpl<'c>) -> GPoll<f64>> {
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::<f64, _>::new(CountingValue(evals.clone()));
let lift = counting_source(evals.clone());
let layout = Node::<ContextImpl>::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);

View File

@@ -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>(T);
impl<T: Clone, Input> Node<Input> for ValueNode<T> {
type Output = T;
fn eval(&self, _input: &Input) -> GPoll<T> {
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<ContextImpl<'e>> for GraphicSource {
type Output = RecordValue<'e>;
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
impl<C: ExtractIndex> Node<C> for GraphicSource {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
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>(*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<Extent> {
fn extent_at<'x>(&self, _input: &C, _level: u8) -> GPoll<Extent>
where
C: ExtractArena<ArenaRef = &'x Arena>,
{
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<String> {
fn vararg_text<C: core_types::ExtractVarArgs>(input: &C) -> Option<String> {
let arg = core_types::ExtractVarArgs::vararg(input, 0).ok()?;
let list = arg.downcast_ref::<core_types::list::List<Graphic>>()?;
let Graphic::Text(text) = list.element(0)? else { return None };
Some(text.clone())
}
impl<'e> Node<ContextImpl<'e>> for PerRowSource {
type Output = RecordValue<'e>;
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
impl<C: ExtractIndex + core_types::ExtractVarArgs> Node<C> for PerRowSource {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
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::<Transform>(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<Extent> {
fn extent_at<'x>(&self, input: &C, _level: u8) -> GPoll<Extent>
where
C: ExtractArena<ArenaRef = &'x Arena>,
{
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::<Graphic>(out.rec(&value)) }) else {
@@ -716,7 +717,7 @@ mod tests {
let out = Node::<ContextImpl>::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<Color>,
}
impl<'e> Node<ContextImpl<'e>> for ColorSource {
type Output = RecordValue<'e>;
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
impl<C: ExtractIndex> Node<C> for ColorSource {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
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<Extent> {
fn extent_at<'x>(&self, _input: &C, _level: u8) -> GPoll<Extent>
where
C: ExtractArena<ArenaRef = &'x Arena>,
{
GPoll::Final(Extent::Exactly(self.colors.len()))
}
@@ -809,7 +815,7 @@ mod tests {
);
let out = Node::<ContextImpl>::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::<Graphic>(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::<Graphic>(wrap_out.rec(&value)) }.clone();

View File

@@ -220,28 +220,22 @@ mod tests {
use core_types::{ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime};
use graphene_application_io::TimingInformation;
struct ProbeNode;
impl<'a> Node<ContextImpl<'a>> for ProbeNode {
type Output = RenderOutput;
fn eval(&self, ctx: &ContextImpl<'a>) -> GPoll<RenderOutput> {
let render_params = ctx.vararg(0).unwrap().downcast_ref::<RenderParams>().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<RenderOutput> {
let render_params = ctx.vararg(0).unwrap().downcast_ref::<RenderParams>().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::<RenderOutput, _>::new(ProbeNode);
let probe = core_types::record::LiftedSource::<RenderOutput, _>::new(probe);
let layout = Node::<ContextImpl>::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::<ContextImpl>::set_layout(
@@ -280,7 +277,7 @@ mod tests {
lane_invariant: u32::MAX,
},
);
let GPoll::Final(result) = Node::<ContextImpl>::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)) };

View File

@@ -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>(T);
impl<T: Clone, Input> Node<Input> for SourceNode<T> {
type Output = T;
fn eval(&self, _input: &Input) -> GPoll<T> {
GPoll::Final(self.0.clone())
}
}
struct IndexNode;
impl<Input: ExtractIndex> Node<Input> for IndexNode {
type Output = f64;
fn eval(&self, input: &Input) -> GPoll<f64> {
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<T, N>(node: N) -> (RecordLift<T, N>, Layout)
fn lifted<T, F>(kernel: F) -> (LiftedSource<T, F>, Layout)
where
T: Clone + Send + Sync + core_types::StaticTypeSized + 'static,
<T as core_types::StaticTypeSized>::Static: Clone + Send + Sync,
N: for<'c> Node<ContextImpl<'c>, Output = T>,
F: for<'c> Fn(&ContextImpl<'c>) -> GPoll<T>,
{
let lift = RecordLift::<T, _>::new(node);
let lift = LiftedSource::<T, _>::new(kernel);
let layout = Node::<ContextImpl>::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::<f64>();
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::<f64>(&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::<f64>();
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::<bool>().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::<bool>(&layout, &value));
@@ -1188,7 +1168,7 @@ mod graphene_test {
let edge = wired.downcast_record::<f64>().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::<f64>(&layout, &value), 4.0);
@@ -1209,32 +1189,33 @@ mod graphene_test {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
struct CountingSource(Arc<AtomicU32>, f64);
impl<Input> Node<Input> for CountingSource {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
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(&[&lt, &lf]);
let graph = SwitchNode::new(cond, if_true, if_false, &union, &lc);
let out = Node::<ContextImpl>::layout(&graph).clone();
reserve_for(&[&lc, &lt, &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::<f64>(&out, &value), 1.0);
@@ -1244,44 +1225,24 @@ mod graphene_test {
#[test]
fn converted_switch_passes_branch_status_through() {
struct PendingSource;
impl<Input> Node<Input> for PendingSource {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
GPoll::Pending
}
}
struct PartialSource;
impl<Input> Node<Input> for PartialSource {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
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::<f64>::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::<f64>::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::<ContextImpl>::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::<f64>(&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<Input> Node<Input> for PartialCondition {
type Output = bool;
fn eval(&self, _input: &Input) -> GPoll<bool> {
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(&[&lt, &lf]);
let graph = SwitchNode::new(cond, if_true, if_false, &union, &lc);
let out = Node::<ContextImpl>::layout(&graph).clone();
reserve_for(&[&lc, &lt, &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::<f64>(&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<Input> Node<Input> for FallbackNode {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
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::<f64>();
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::<f64>(&out, &boxed.0), 5.0);

View File

@@ -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>(T);
impl<T: Clone, Input> Node<Input> for ValueNode<T> {
type Output = T;
fn eval(&self, _input: &Input) -> GPoll<T> {
GPoll::Final(self.0.clone())
}
}
struct TransformSource {
layout: Layout,
element: f64,
transform: DAffine2,
}
impl<'e> Node<ContextImpl<'e>> for TransformSource {
type Output = RecordValue<'e>;
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
use core_types::context::ExtractArena;
let mut frame = FrameBuilder::new(&self.layout, input.arena());
impl<C: ExtractIndex> Node<C> for TransformSource {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
let mut frame = FrameBuilder::new(&self.layout, ExtractArena::arena(input));
frame.element(self.element);
frame.attr::<TransformAttr>(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<ContextImpl<'e>> for VectorRows {
type Output = RecordValue<'e>;
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
use core_types::context::{ExtractArena, ExtractIndices};
impl<C: ExtractIndex> Node<C> for VectorRows {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
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::<TransformAttr>(*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<Extent> {
fn extent_at<'x>(&self, _input: &C, _level: u8) -> GPoll<Extent>
where
C: ExtractArena<ArenaRef = &'x Arena>,
{
GPoll::Final(Extent::Exactly(self.rows.len()))
}
@@ -255,17 +255,18 @@ mod test {
layout: Layout,
}
impl<'e> Node<ContextImpl<'e>> for PositionProbe {
type Output = RecordValue<'e>;
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
use core_types::context::{ExtractArena, ExtractPosition};
impl<C: ExtractIndex + core_types::context::ExtractPosition> Node<C> for PositionProbe {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
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::<TransformAttr>(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::<ContextImpl>::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::<ContextImpl>::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::<ContextImpl>::set_layout(&mut node, repeat_on_points_layout_meta().resolve(&[Some(&content_layout)]));
let leveled = Node::<ContextImpl>::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::<ContextImpl>::set_layout(&mut node, repeat_on_points_layout_meta().resolve(&[Some(&content_layout)]));
let mut expected = positions.clone();