Pass frame space by argument and delete the record stack

This commit is contained in:
Dennis Kobert
2026-08-29 18:33:23 +00:00
parent 3af6834d3c
commit 97bc10e4c3
20 changed files with 1319 additions and 1260 deletions

View File

@@ -175,6 +175,15 @@ impl Arena {
Some(unsafe { std::slice::from_raw_parts_mut(ptr, len) })
}
/// The generation-checked handle for a region this arena holds, `None` for
/// a pointer from anywhere else. The handle keeps the region's provenance,
/// so a cache stores one where it would otherwise launder an address.
pub fn handle_at(&self, ptr: *const u8) -> Option<ArenaWeak<u8>> {
let offset = (ptr as usize).checked_sub(self.base() as usize)?;
(offset < self.buf.len()).then_some(())?;
ArenaWeak::new(self.generation(), offset)
}
/// `false` once generations are exhausted, parking the arena on [`PARKED_GENERATION`]
/// where every handle misses and further allocation is refused.
pub fn reset(&mut self) -> bool {

View File

@@ -105,10 +105,9 @@ pub struct RecordBatchMut<'a> {
}
impl<'a> RecordBatchMut<'a> {
/// # Safety
/// `scratch` must start with `len` initialized records of `layout`, packed
/// at `layout.lane_stride()` stride.
pub unsafe fn new(scratch: &'a mut [MaybeUninit<u64>], len: usize, layout: &'a crate::record::Layout) -> Self {
/// Minted only by a [`crate::record::SlotRun`] finishing its served lanes,
/// which is what makes the initialized prefix a fact rather than a contract.
pub(crate) fn new(scratch: &'a mut [MaybeUninit<u64>], len: usize, layout: &'a crate::record::Layout) -> Self {
debug_assert!(len * layout.lane_stride() <= scratch.len() * 8);
Self { scratch, len, layout }
}
@@ -318,9 +317,10 @@ pub trait Node<Input> {
/// 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 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>>
/// construction. The caller claims the frame at [`Node::layout`] out of
/// its own frame space, and the claim carries what is left, so the node
/// takes exactly its own frame out of the caller's free space.
fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'e, 'l>) -> GPoll<crate::record::Served<'e>>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>;
@@ -328,7 +328,7 @@ pub trait Node<Input> {
/// 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<'e>(&self, _input: &Input, _level: u8) -> GPoll<Extent>
fn extent_at<'e>(&self, _input: &Input, _level: u8, _frames: &crate::record::Frames<'e>) -> GPoll<Extent>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
@@ -338,16 +338,17 @@ pub trait Node<Input> {
/// 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<'e>(&self, input: &Input, at: Level) -> GPoll<Extent>
fn extent<'e>(&self, input: &Input, at: Level, frames: &crate::record::Frames<'e>) -> 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)));
let product =
|range: core::ops::Range<u8>, frames: &crate::record::Frames<'e>| range.fold(GPoll::Final(Extent::Exactly(1)), |acc, level| Extent::mul(acc, self.extent_at(input, level, frames)));
match at {
Level::At(level) => self.extent_at(input, level),
Level::Below(level) => product(0..level),
Level::Above(level) => product((level + 1)..self.depth()),
Level::Total => product(0..self.depth()),
Level::At(level) => self.extent_at(input, level, frames),
Level::Below(level) => product(0..level, frames),
Level::Above(level) => product((level + 1)..self.depth(), frames),
Level::Total => product(0..self.depth(), frames),
}
}
@@ -364,8 +365,8 @@ pub trait Node<Input> {
/// The record layout of this node's output; the shared empty layout for
/// element-only producers. Consumers read their carrier's layout through
/// this at wiring, and the wiring layer derives stack sizing from the same
/// layouts, in the dynamic executor and exported source alike.
/// this at wiring, and the wiring layer derives the root buffer's sizing from
/// the same layouts, in the dynamic executor and exported source alike.
fn layout(&self) -> &crate::record::Layout {
crate::record::empty_layout()
}
@@ -379,11 +380,11 @@ pub trait Node<Input> {
/// 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, 'e>(&'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>]>, frames: &crate::record::Frames<'e>) -> BatchStatus<'a>
where
Input: InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
let _ = (input, range, scratch);
let _ = (input, range, scratch, frames);
BatchStatus::Unbatched
}
}
@@ -392,18 +393,18 @@ impl<Input, N> Node<Input> for &N
where
N: Node<Input> + ?Sized,
{
fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'l>) -> GPoll<crate::record::Served<'e>>
fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'e, 'l>) -> GPoll<crate::record::Served<'e>>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).serve(input, slot)
}
fn extent_at<'e>(&self, input: &Input, level: u8) -> GPoll<Extent>
fn extent_at<'e>(&self, input: &Input, level: u8, frames: &crate::record::Frames<'e>) -> GPoll<Extent>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).extent_at(input, level)
(**self).extent_at(input, level, frames)
}
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
@@ -414,11 +415,11 @@ where
(**self).layout()
}
fn eval_batch<'a, 'e>(&'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>]>, frames: &crate::record::Frames<'e>) -> BatchStatus<'a>
where
Input: InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).eval_batch(input, range, scratch)
(**self).eval_batch(input, range, scratch, frames)
}
}
@@ -426,18 +427,18 @@ impl<Input, N> Node<Input> for Box<N>
where
N: Node<Input> + ?Sized,
{
fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'l>) -> GPoll<crate::record::Served<'e>>
fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'e, 'l>) -> GPoll<crate::record::Served<'e>>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).serve(input, slot)
}
fn extent_at<'e>(&self, input: &Input, level: u8) -> GPoll<Extent>
fn extent_at<'e>(&self, input: &Input, level: u8, frames: &crate::record::Frames<'e>) -> GPoll<Extent>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).extent_at(input, level)
(**self).extent_at(input, level, frames)
}
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
@@ -448,11 +449,11 @@ where
(**self).layout()
}
fn eval_batch<'a, 'e>(&'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>]>, frames: &crate::record::Frames<'e>) -> BatchStatus<'a>
where
Input: InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).eval_batch(input, range, scratch)
(**self).eval_batch(input, range, scratch, frames)
}
}
@@ -460,18 +461,18 @@ impl<Input, N> Node<Input> for std::sync::Arc<N>
where
N: Node<Input> + ?Sized,
{
fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'l>) -> GPoll<crate::record::Served<'e>>
fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'e, 'l>) -> GPoll<crate::record::Served<'e>>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).serve(input, slot)
}
fn extent_at<'e>(&self, input: &Input, level: u8) -> GPoll<Extent>
fn extent_at<'e>(&self, input: &Input, level: u8, frames: &crate::record::Frames<'e>) -> GPoll<Extent>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).extent_at(input, level)
(**self).extent_at(input, level, frames)
}
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
@@ -482,11 +483,11 @@ where
(**self).layout()
}
fn eval_batch<'a, 'e>(&'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>]>, frames: &crate::record::Frames<'e>) -> BatchStatus<'a>
where
Input: InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).eval_batch(input, range, scratch)
(**self).eval_batch(input, range, scratch, frames)
}
}
@@ -516,15 +517,15 @@ 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.
/// Claims the edge's own frame out of `frames`, serves through it, and
/// folds the poll's status into the cell. The claim is the caller's, so
/// the edge's frame is claimed exactly once per evaluation.
#[inline(always)]
pub fn eval_input<'e, Input, N: Node<Input> + ?Sized>(&self, input_index: usize, node: &N, input: &Input) -> Result<crate::record::RecordValue<'e>, Interrupt>
pub fn eval_input<'e, Input, N: Node<Input> + ?Sized>(&self, input_index: usize, node: &N, input: &Input, frames: &crate::record::Frames<'e>) -> Result<crate::record::RecordValue<'e>, Interrupt>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
let slot = crate::record::FrameClaim::enter(node.layout());
let slot = frames.claim(node.layout());
match node.serve(input, slot) {
GPoll::Final(served) => Ok(served.value()),
GPoll::Partial(_) if self.no_partial => Err(Interrupt::Pending),
@@ -588,23 +589,25 @@ impl StatusCell {
}
#[derive(Clone, Copy)]
pub struct LazyInput<'a, N> {
pub struct LazyInput<'a, 'f, N> {
node: &'a N,
cell: &'a StatusCell,
input_index: usize,
frames: &'a crate::record::Frames<'f>,
}
impl<'a, N> LazyInput<'a, N> {
pub fn new(node: &'a N, cell: &'a StatusCell, input_index: usize) -> Self {
Self { node, cell, input_index }
impl<'a, 'f, N> LazyInput<'a, 'f, N> {
pub fn new(node: &'a N, cell: &'a StatusCell, input_index: usize, frames: &'a crate::record::Frames<'f>) -> Self {
Self { node, cell, input_index, frames }
}
#[inline(always)]
pub fn eval<'e, Input>(&self, ctx: &Input) -> Result<crate::record::RecordValue<'e>, Interrupt>
where
N: crate::record::DerivedRecordEdge<'e, Input>,
'f: 'e,
{
self.node.eval_derived(self.cell, self.input_index, ctx)
self.node.eval_derived(self.cell, self.input_index, ctx, self.frames)
}
/// The edge's composite extent, for kernels that split or shift indices
@@ -614,8 +617,9 @@ impl<'a, N> LazyInput<'a, N> {
where
N: Node<Input>,
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
'f: 'e,
{
self.node.extent(ctx, at)
self.node.extent(ctx, at, self.frames)
}
}
@@ -653,12 +657,13 @@ mod tests {
#[test]
fn the_default_advertises_no_batch_support() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(1024).unwrap();
let input = TestInput { index: 0, arena: &arena };
let mut scratch = [const { MaybeUninit::uninit() }; 4];
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));
assert!(matches!(node.eval_batch(&input, 2..6, Some(&mut scratch), &frames), BatchStatus::Unbatched));
assert!(matches!(node.eval_batch(&input, 2..6, None, &frames), BatchStatus::Unbatched));
}
#[test]
@@ -668,15 +673,12 @@ mod tests {
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 {
let frames = crate::record::test_frames(1 << 12);
let GPoll::Final(value) = serve_edge(&*erased, &input, &frames) 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));
assert!(matches!(erased.eval_batch(&input, 0..2, None, &frames), BatchStatus::Unbatched));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -136,62 +136,24 @@ impl<Input, N> Node<Input> for SharedEdge<N>
where
N: Node<Input> + ?Sized,
{
fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'l>) -> crate::gpoll::GPoll<crate::record::Served<'e>>
/// A node takes exactly its own frame out of its caller's free space: the
/// caller minted the claim and kept the cursor, so the frame accounting is
/// structural here and asserted where a claim is split.
fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'e, '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)]
let trace = {
static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*TRACE.get_or_init(|| std::env::var_os("GRAPHENE_SP_DEBUG").is_some())
};
#[cfg(debug_assertions)]
if trace {
eprintln!(
"sp> enter frame_bytes {} fields [{}] sp {}",
self.layout().frame_bytes(),
self.layout().fields.iter().map(|field| field.name.to_string()).collect::<Vec<_>>().join(", "),
sp_before,
);
}
// SAFETY: `own` keeps the payload alive for `self`'s lifetime and Arc
// payloads are address stable.
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());
}
#[cfg(debug_assertions)]
debug_assert_eq!(
crate::record::stack::sp(),
sp_before,
"{} left the record stack misaligned (frame_bytes {}, depth {}, fields [{}], poll {})",
std::any::type_name::<N>(),
self.layout().frame_bytes(),
self.layout().depth,
self.layout().fields.iter().map(|field| field.name.to_string()).collect::<Vec<_>>().join(", "),
match &result {
crate::gpoll::GPoll::Final(_) => "Final",
crate::gpoll::GPoll::Partial(_) => "Partial",
crate::gpoll::GPoll::Fallback(_) => "Fallback",
crate::gpoll::GPoll::Pending => "Pending",
crate::gpoll::GPoll::Error(_) => "Error",
},
);
result
unsafe { self.ptr.as_ref() }.serve(input, slot)
}
fn extent_at<'x>(&self, input: &Input, level: u8) -> crate::gpoll::GPoll<crate::gpoll::Extent>
fn extent_at<'x>(&self, input: &Input, level: u8, frames: &crate::record::Frames<'x>) -> 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)
unsafe { self.ptr.as_ref() }.extent_at(input, level, frames)
}
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
@@ -204,12 +166,18 @@ where
unsafe { self.ptr.as_ref() }.layout()
}
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>
fn eval_batch<'a, 'x>(
&'a self,
input: &'a Input,
range: std::ops::Range<u64>,
scratch: Option<&'a mut [std::mem::MaybeUninit<u64>]>,
frames: &crate::record::Frames<'x>,
) -> crate::node::BatchStatus<'a>
where
Input: crate::context::InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'x crate::arena::Arena>,
{
// SAFETY: as in serve.
unsafe { self.ptr.as_ref() }.eval_batch(input, range, scratch)
unsafe { self.ptr.as_ref() }.eval_batch(input, range, scratch, frames)
}
}
@@ -350,7 +318,7 @@ mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use crate::record::{FrameClaim, Layout, LiftedSource, Served, element_write, read_element, serve_edge, stack};
use crate::record::{FrameClaim, Layout, LiftedSource, Served, element_write, read_element, serve_edge};
fn counting() -> LiftedSource<u32, impl for<'c> Fn(&ContextImpl<'c>) -> GPoll<u32>> {
let count = AtomicU32::new(0);
@@ -378,7 +346,7 @@ mod tests {
}
impl<Input: Ctx> Node<Input> for LendNode {
fn serve<'e, 'l>(&self, input: &Input, slot: FrameClaim<'l>) -> GPoll<Served<'e>>
fn serve<'e, 'l>(&self, input: &Input, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
where
Input: ExtractArena<ArenaRef = &'e Arena>,
{
@@ -399,10 +367,7 @@ mod tests {
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 frames = crate::record::test_frames(1 << 12);
let node = LendNode::new("held");
let layout = Node::<ContextImpl>::layout(&node).clone();
@@ -410,7 +375,7 @@ mod tests {
assert_eq!(*handle.ty(), concrete!(String));
let wired = handle.downcast_erased::<ErasedRecordNode>(concrete!(String)).unwrap();
let GPoll::Final(value) = serve_edge(&wired, &ctx) else {
let GPoll::Final(value) = serve_edge(&wired, &ctx, &frames) else {
panic!("borrow-carrying output must serve through the erased edge");
};
// SAFETY: the record was served at `layout`, whose element is the borrow.
@@ -450,7 +415,7 @@ mod tests {
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>>
fn serve<'e, 'l>(&self, input: &C, mut slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
@@ -458,11 +423,11 @@ mod tests {
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() };
// The element copies out by value, so the content's frame is
// dead when the scope ends.
let scope = slot.frames().scope();
let derived = input.promoted(&spilled, index);
match self.content.eval_derived(&cell, 0, &derived) {
match self.content.eval_derived(&cell, 0, &derived, &scope) {
// SAFETY: the content served at its own layout, whose
// element is `T`.
Ok(value) => result.push(unsafe { read_element::<T>(self.inner.rec(&value)) }),
@@ -491,12 +456,9 @@ mod tests {
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 {
stack::reserve(1 << 12);
}
let frames = crate::record::test_frames(1 << 12);
let GPoll::Final(value) = serve_edge(&*erased, &ctx) else {
let GPoll::Final(value) = serve_edge(&*erased, &ctx, &frames) else {
panic!("nested repeat must evaluate");
};
// SAFETY: the record was served at `layout`, whose element is the output.
@@ -529,7 +491,7 @@ mod tests {
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>>
fn serve<'e, 'l>(&self, input: &C, mut slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
@@ -537,12 +499,12 @@ mod tests {
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.
// The element copies out by value, so the content's frame is dead
// when the scope ends.
let value = {
let _scope = unsafe { stack::ScopeGuard::enter() };
let scope = slot.frames().scope();
let derived = input.with_footprint(&footprint);
match self.content.eval_derived(&cell, 0, &derived) {
match self.content.eval_derived(&cell, 0, &derived, &scope) {
// SAFETY: the content served at its own layout, whose
// element is the resolution.
Ok(value) => unsafe { read_element::<u32>(self.inner.rec(&value)) },
@@ -566,10 +528,7 @@ mod tests {
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 frames = crate::record::test_frames(1 << 12);
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();
@@ -578,7 +537,7 @@ mod tests {
let graph = ShiftFootprintNode::new(shifted, shifted_layout);
let layout = Node::<ContextImpl>::layout(&graph).clone();
let GPoll::Final(value) = serve_edge(&graph, &ctx) else {
let GPoll::Final(value) = serve_edge(&graph, &ctx, &frames) else {
panic!("the footprint shift must reach the content");
};
// SAFETY: the record was served at `layout`, whose element is the resolution.
@@ -626,19 +585,16 @@ mod tests {
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 {
stack::reserve(1 << 12);
}
let frames = crate::record::test_frames(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!(serve_edge(&first, &ctx).map(count), GPoll::Final(1));
assert_eq!(serve_edge(&second, &ctx).map(count), GPoll::Final(2));
assert_eq!(serve_edge(&first, &ctx, &frames).map(count), GPoll::Final(1));
assert_eq!(serve_edge(&second, &ctx, &frames).map(count), GPoll::Final(2));
drop(first);
assert_eq!(serve_edge(&second, &ctx).map(count), GPoll::Final(3));
assert_eq!(serve_edge(&second, &ctx, &frames).map(count), GPoll::Final(3));
}
}

View File

@@ -171,7 +171,7 @@ mod tests {
use crate::context::{ContextImpl, Ctx, EvalScope, ExtractFootprint, ExtractVarArgs, VarArgLink, VarArgSlots};
use crate::gpoll::GPoll;
use crate::node::Node;
use crate::record::{Layout, LiftedSource, RecordExtract, element_write, stack};
use crate::record::{Layout, LiftedSource, RecordExtract, element_write};
use crate::transform::Footprint;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU32, Ordering};
@@ -266,10 +266,6 @@ mod tests {
where
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>();
graph.set_layout(crate::record::RecordLayout {
frame_bytes: layout.frame_bytes(),
@@ -339,6 +335,7 @@ mod tests {
#[test]
fn async_source_spawns_once_and_lands_via_the_slot() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
@@ -354,18 +351,19 @@ mod tests {
&element_layout::<u64>(),
));
assert_eq!(graph.eval(&ctx), GPoll::Pending);
assert_eq!(graph.eval(&ctx), GPoll::Pending);
assert_eq!(graph.eval(&ctx, &frames), GPoll::Pending);
assert_eq!(graph.eval(&ctx, &frames), 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!(graph.eval(&ctx), GPoll::Final(42.0));
assert_eq!(graph.eval(&ctx), GPoll::Final(42.0));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(42.0));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(42.0));
assert_eq!(SLOW_DOUBLE_RUNS.load(Ordering::Relaxed), 1);
}
#[test]
fn async_source_reports_the_placeholder_while_in_flight() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
@@ -381,13 +379,14 @@ mod tests {
&element_layout::<u64>(),
));
assert_eq!(graph.eval(&ctx), GPoll::Partial(-1.0));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Partial(-1.0));
runtime.drain();
assert_eq!(graph.eval(&ctx), GPoll::Final(42.0));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(42.0));
}
#[test]
fn no_partial_maps_the_placeholder_frame_to_pending() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
@@ -403,13 +402,14 @@ mod tests {
&element_layout::<u64>(),
));
assert_eq!(graph.eval(&ctx), GPoll::Pending);
assert_eq!(graph.eval(&ctx, &frames), GPoll::Pending);
runtime.drain();
assert_eq!(graph.eval(&ctx), GPoll::Final(42.0));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(42.0));
}
#[test]
fn prologue_runs_sync_and_spawns_once() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
@@ -425,17 +425,18 @@ mod tests {
&element_layout::<u64>(),
));
assert_eq!(graph.eval(&ctx), GPoll::Pending);
assert_eq!(graph.eval(&ctx, &frames), GPoll::Pending);
assert_eq!(STAGED_RUNS.load(Ordering::Relaxed), 1, "the prologue runs synchronously on the miss");
assert_eq!(graph.eval(&ctx), GPoll::Pending);
assert_eq!(graph.eval(&ctx, &frames), 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!(graph.eval(&ctx), GPoll::Final(42.0));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(42.0));
assert_eq!(STAGED_RUNS.load(Ordering::Relaxed), 1);
}
#[test]
fn prologue_interrupt_defers_the_spawn() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
@@ -454,16 +455,17 @@ mod tests {
&element_layout::<u64>(),
));
assert_eq!(graph.eval(&ctx), GPoll::Pending);
assert_eq!(graph.eval(&ctx, &frames), 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!(graph.eval(&ctx), GPoll::Pending);
assert_eq!(graph.eval(&ctx, &frames), GPoll::Pending);
assert_eq!(runtime.drain(), vec![9]);
assert_eq!(graph.eval(&ctx), GPoll::Final(42.0));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(42.0));
}
#[test]
fn async_kernels_read_captured_varargs() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
@@ -485,13 +487,14 @@ mod tests {
&element_layout::<u64>(),
));
assert_eq!(graph.eval(&ctx), GPoll::Pending);
assert_eq!(graph.eval(&ctx, &frames), GPoll::Pending);
runtime.drain();
assert_eq!(graph.eval(&ctx), GPoll::Final(21.5));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(21.5));
}
#[test]
fn async_kernels_read_the_captured_context_snapshot() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
@@ -509,9 +512,9 @@ mod tests {
&element_layout::<u64>(),
));
assert_eq!(graph.eval(&ctx), GPoll::Pending);
assert_eq!(graph.eval(&ctx, &frames), GPoll::Pending);
runtime.drain();
assert_eq!(graph.eval(&ctx), GPoll::Final(Footprint::DEFAULT.resolution.x));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(Footprint::DEFAULT.resolution.x));
}
#[test]
@@ -610,6 +613,7 @@ mod tests {
#[test]
fn an_immediately_ready_kernel_returns_final_on_the_first_eval() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let runtime = Arc::new(GraphRuntime::new(CollectSpawner::default()));
runtime.retain_sources(&[13]);
@@ -625,7 +629,7 @@ mod tests {
let snapshot = runtime.snapshot();
let scope = EvalScope::new(None, None, None, &snapshot, &arena);
let ctx = ContextImpl::root(&scope);
assert_eq!(graph.eval(&ctx), GPoll::Final(42.0));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(42.0));
assert!(!runtime.take_dirty());
assert_eq!(runtime.snapshot(), vec![(13, 0)]);
assert_eq!(runtime.spawner().drain(), 0);
@@ -633,6 +637,7 @@ mod tests {
#[test]
fn a_source_slot_lands_through_the_runtime_while_downstream_keys_invalidate() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let runtime = Arc::new(GraphRuntime::new(CollectSpawner::default()));
runtime.retain_sources(&[11]);
@@ -648,7 +653,7 @@ mod tests {
let snapshot = runtime.snapshot();
let scope = EvalScope::new(None, None, None, &snapshot, &arena);
let ctx = ContextImpl::root(&scope);
assert_eq!(graph.eval(&ctx), GPoll::Pending);
assert_eq!(graph.eval(&ctx, &frames), GPoll::Pending);
assert!(!runtime.take_dirty());
assert_eq!(runtime.spawner().drain(), 1);
@@ -658,7 +663,7 @@ mod tests {
let bumped_scope = EvalScope::new(None, None, None, &bumped, &arena);
let bumped_ctx = ContextImpl::root(&bumped_scope);
assert_eq!(graph.eval(&bumped_ctx), GPoll::Final(42.0), "the own-generation-excluded key replays the landed slot");
assert_eq!(graph.eval(&bumped_ctx, &frames), GPoll::Final(42.0), "the own-generation-excluded key replays the landed slot");
assert_eq!(runtime.spawner().drain(), 0, "a slot hit must not respawn");
let downstream_key = crate::registry::cache_key(&ContextImpl::root(&scope));

View File

@@ -21,7 +21,7 @@ impl<C, T> crate::node::Node<C> for ValueSource<T>
where
T: Clone + Send + Sync + dyn_any::StaticTypeSized,
{
fn serve<'e, 'l>(&self, input: &C, slot: crate::record::FrameClaim<'l>) -> crate::gpoll::GPoll<crate::record::Served<'e>>
fn serve<'e, 'l>(&self, input: &C, slot: crate::record::FrameClaim<'e, 'l>) -> crate::gpoll::GPoll<crate::record::Served<'e>>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
@@ -65,7 +65,7 @@ where
C: crate::context::ExtractIndex,
T: Clone + Send + Sync + dyn_any::StaticTypeSized,
{
fn serve<'e, 'l>(&self, input: &C, slot: crate::record::FrameClaim<'l>) -> crate::gpoll::GPoll<crate::record::Served<'e>>
fn serve<'e, 'l>(&self, input: &C, slot: crate::record::FrameClaim<'e, 'l>) -> crate::gpoll::GPoll<crate::record::Served<'e>>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
@@ -75,7 +75,7 @@ where
slot.lift_served(crate::gpoll::GPoll::Final(value.clone()), input.arena())
}
fn extent_at<'x>(&self, _input: &C, level: u8) -> crate::gpoll::GPoll<crate::gpoll::Extent>
fn extent_at<'x>(&self, _input: &C, level: u8, _frames: &crate::record::Frames<'x>) -> crate::gpoll::GPoll<crate::gpoll::Extent>
where
C: crate::context::ExtractArena<ArenaRef = &'x crate::arena::Arena>,
{

View File

@@ -24,12 +24,12 @@ pub enum LevelGroup<'e> {
/// The renderer's flip form: the wire's whole extent materialized into a
/// 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>
pub fn materialize_group<'a, 'e, C, N>(node: &'a N, input: &'a C, arena: &'a Arena, frames: &core_types::record::Frames<'e>) -> LevelGroup<'a>
where
C: InjectIndex + Copy + core_types::context::ExtractArena<ArenaRef = &'e Arena>,
N: Node<C>,
{
match materialize_level(node, input, arena) {
match materialize_level(node, input, arena, frames) {
LevelStatus::Batch(batch, finality) => {
// SAFETY: a materialized batch's frames are arena-resident.
let item = unsafe { GroupItem::from_resident(batch) };

View File

@@ -1647,8 +1647,11 @@ mod run_tests {
drop(source);
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
// SAFETY: between evaluations, nothing served on the stack is live.
unsafe { core_types::record::stack::reserve(layout.frame_bytes()); } let value = owned.replay(&layout, &arena).expect("the arena holds the replay");
let frames = core_types::record::test_frames(layout.frame_bytes());
let mut slot = frames.claim(&layout);
owned.replay_into(&mut slot, &arena).expect("the arena holds the replay");
// SAFETY: the replay completes the record in the claimed frame.
let value = unsafe { slot.finish() };
// SAFETY: the replay wrote a record of `layout`.
let served = unsafe { layout.rec(&value).read::<Option<&List<Graphic>>>(offset) }.expect("the fill replays present");
assert_eq!(map_groups_to_legacy(served.element(0).unwrap()), expected);