mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Flip the Node trait from eval onto the frame claim's serve
This commit is contained in:
@@ -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"))
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 ®ular_fields[index].ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => !routing_source(ty) && ir::value_binding(&node, index).reads_out(),
|
||||
ParsedFieldType::Node(_) => false,
|
||||
};
|
||||
|
||||
let clamp_tokens = |field: &ParsedField| {
|
||||
let ParsedFieldType::Regular(RegularParsedField { number_hard_min, number_hard_max, .. }) = &field.ty else {
|
||||
return None;
|
||||
@@ -1493,7 +1495,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
ParsedFieldType::Regular(_) => quote!(#name),
|
||||
ParsedFieldType::Node(_) => match (ir::lazy_binding(&node, index), raw_lazy) {
|
||||
(LazyBinding::Element, true) | (LazyBinding::OpaqueRecord, _) => quote!(&#name),
|
||||
(LazyBinding::Plain, true) => quote!(&self.#name),
|
||||
(LazyBinding::Generic, true) => quote!(&self.#name),
|
||||
_ => quote!(#name),
|
||||
},
|
||||
}
|
||||
@@ -1559,23 +1561,28 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
let #arg = #core_types::extent::ListIn::new(&#query, &__total);
|
||||
}
|
||||
}
|
||||
ValueBinding::RecordElement | ValueBinding::ReadingSecondary => {
|
||||
let slot = format_ident!("__in_{index}");
|
||||
// A routing source forwards its record whole; its extents are
|
||||
// the queryable quantity.
|
||||
_ if routing_source(ty) => extent_edge(&query, &arg),
|
||||
ValueBinding::RecordElement | ValueBinding::ReadingSecondary | ValueBinding::Plain => {
|
||||
let layout = match ir::value_binding(&node, index) {
|
||||
ValueBinding::Plain => quote!(#core_types::node::Node::<#ctx_ident>::layout(&self.#name)),
|
||||
_ => {
|
||||
let slot = format_ident!("__in_{index}");
|
||||
quote!(self.#slot)
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
let #query = || {
|
||||
// SAFETY: the element copies out by value; extent
|
||||
// queries leave the record stack untouched.
|
||||
let __scope = unsafe { #core_types::record::stack::ScopeGuard::enter() };
|
||||
#core_types::node::Node::eval(&self.#name, __input)
|
||||
.map(|__value| unsafe { #core_types::record::read_element::<#ty>(self.#slot.rec(&__value)) })
|
||||
#core_types::record::serve_edge(&self.#name, __input)
|
||||
.map(|__value| unsafe { #core_types::record::read_element::<#ty>(#layout.rec(&__value)) })
|
||||
};
|
||||
let #arg = #core_types::extent::ValueIn::new(&#query);
|
||||
}
|
||||
}
|
||||
ValueBinding::Plain => quote! {
|
||||
let #query = || #core_types::node::Node::eval(&self.#name, __input);
|
||||
let #arg = #core_types::extent::ValueIn::new(&#query);
|
||||
},
|
||||
// A carrier, lent, or materialized ranked input is a record
|
||||
// edge; its extents are the queryable quantity.
|
||||
_ => extent_edge(&query, &arg),
|
||||
@@ -1585,7 +1592,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
arg_names.push(arg);
|
||||
}
|
||||
quote! {
|
||||
fn extent_at(&self, __input: &#ctx_ident, __level: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> {
|
||||
fn extent_at<'__serve>(&self, __input: &#ctx_ident, __level: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent>
|
||||
where
|
||||
#ctx_ident: #core_types::context::ExtractArena<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)]
|
||||
|
||||
@@ -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>)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>()?;
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -318,6 +318,7 @@ impl PerPixelAdjustCodegen<'_> {
|
||||
},
|
||||
output_type: raster_gpu,
|
||||
output_depth: 0,
|
||||
claim: None,
|
||||
is_async: false,
|
||||
fields,
|
||||
body,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)) };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)) };
|
||||
|
||||
@@ -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(&[<, &lf]);
|
||||
let graph = SwitchNode::new(cond, if_true, if_false, &union, &lc);
|
||||
let out = Node::<ContextImpl>::layout(&graph).clone();
|
||||
reserve_for(&[&lc, <, &lf, &out]);
|
||||
|
||||
let GPoll::Final(value) = Node::eval(&graph, &ctx) else {
|
||||
let GPoll::Final(value) = serve_edge(&graph, &ctx) else {
|
||||
panic!("expected a final record");
|
||||
};
|
||||
assert_eq!(element::<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(&[<, &lf]);
|
||||
let graph = SwitchNode::new(cond, if_true, if_false, &union, &lc);
|
||||
let out = Node::<ContextImpl>::layout(&graph).clone();
|
||||
reserve_for(&[&lc, <, &lf, &out]);
|
||||
|
||||
let GPoll::Partial(value) = Node::eval(&graph, &ctx) else {
|
||||
let GPoll::Partial(value) = serve_edge(&graph, &ctx) else {
|
||||
panic!("expected a partial record");
|
||||
};
|
||||
assert_eq!(element::<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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user