Serve group items at the evaluation lifetime

This commit is contained in:
Dennis Kobert
2026-08-28 13:30:05 +00:00
parent c6b669fc88
commit 1e5b2c7f62
2 changed files with 65 additions and 37 deletions
+11 -1
View File
@@ -230,11 +230,21 @@ impl<T> Copy for List<'_, T> {}
impl<'a, T> List<'a, T> { impl<'a, T> List<'a, T> {
/// # Safety /// # Safety
/// `T` must be the batch's record element type, proven at the consumer's wiring. /// `T` must be the batch's record element type, proven at the consumer's
/// wiring, and the batch's frames must stay valid for the evaluation:
/// arena-resident batches qualify, caller stack scratch does not.
pub unsafe fn new(batch: RecordBatch<'a>) -> Self { pub unsafe fn new(batch: RecordBatch<'a>) -> Self {
Self { batch, _element: PhantomData } Self { batch, _element: PhantomData }
} }
/// The level as a group item over the same frames, without copying.
/// Panics where a parked element or field lacks content glue.
pub fn as_group_item(&self) -> crate::record::GroupItem<'a> {
// SAFETY: `List::new` established the frames stay valid for the
// evaluation.
unsafe { crate::record::GroupItem::from_resident(self.batch) }
}
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.batch.len() self.batch.len()
} }
+54 -36
View File
@@ -2034,26 +2034,30 @@ impl<'e> RunBuilder<'e> {
/// The finished run. Panics unless every lane was pushed, since an /// The finished run. Panics unless every lane was pushed, since an
/// unwritten parked element slot must never become readable. /// unwritten parked element slot must never become readable.
pub fn finish(self) -> GroupItem { pub fn finish(self) -> GroupItem<'e> {
assert_eq!(self.pushed, self.len, "every lane pushes before the run finishes"); assert_eq!(self.pushed, self.len, "every lane pushes before the run finishes");
GroupItem { GroupItem {
layout: self.layout, layout: self.layout,
storage: ItemStorage::Resident(self.frames.cast_const()), storage: ItemStorage::Resident(self.frames.cast_const()),
len: self.len, len: self.len,
_arena: std::marker::PhantomData,
} }
} }
} }
/// `len` records stored in the arena at `layout`'s stride. The layout is /// `len` records stored in the arena at `layout`'s stride. The layout is
/// owned by the value and identifies the run's element type. The records are /// owned by the value and identifies the run's element type. A resident item
/// valid for the current evaluation, like every arena payload. An owned item /// borrows the serving arena at `'e`, so it cannot outlive the evaluation. An
/// ([`Self::copy_out`]) survives the generation instead, and must /// owned item ([`Self::copy_out`]) is the `'static` form: it survives the
/// [`Self::replay`] into a serving arena before any read. /// generation, refuses lane reads, and re-enters a serving arena through
/// [`Self::replay`]. Safe code cannot mint a resident `GroupItem<'static>`,
/// so the lifetime instantiation is the resident/owned distinction.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct GroupItem { pub struct GroupItem<'e> {
layout: Layout, layout: Layout,
storage: ItemStorage, storage: ItemStorage,
len: usize, len: usize,
_arena: std::marker::PhantomData<&'e ()>,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -2083,16 +2087,16 @@ struct OwnedLanes {
// SAFETY: the same argument as for `RecordValue`. The element bounds and the // SAFETY: the same argument as for `RecordValue`. The element bounds and the
// parking discipline make the record bytes thread-safe, and their validity // parking discipline make the record bytes thread-safe, and their validity
// is tied to the shared arena. // is tied to the shared arena.
unsafe impl Send for GroupItem {} unsafe impl Send for GroupItem<'_> {}
// SAFETY: as `Send`. // SAFETY: as `Send`.
unsafe impl Sync for GroupItem {} unsafe impl Sync for GroupItem<'_> {}
impl GroupItem { impl<'e> GroupItem<'e> {
/// Copies the batch's lanes into the arena and clones its layout. /// Copies the batch's lanes into the arena and clones its layout.
/// Returns `None` when the arena is exhausted. Parked regions must carry /// Returns `None` when the arena is exhausted. Parked regions must carry
/// the content glue, so equality and hashing never fall back to pointer /// the content glue, so equality and hashing never fall back to pointer
/// bytes. /// bytes.
pub fn adopt(batch: crate::node::RecordBatch<'_>, arena: &crate::arena::Arena) -> Option<Self> { pub fn adopt(batch: crate::node::RecordBatch<'_>, arena: &'e crate::arena::Arena) -> Option<Self> {
let layout = batch.layout().clone(); let layout = batch.layout().clone();
assert!(!layout.element.parked || layout.element.content_hash.is_some(), "a parked element adopts only with content glue"); assert!(!layout.element.parked || layout.element.content_hash.is_some(), "a parked element adopts only with content glue");
for field in &layout.fields { for field in &layout.fields {
@@ -2109,6 +2113,7 @@ impl GroupItem {
layout, layout,
storage: ItemStorage::Resident(frames.cast_const()), storage: ItemStorage::Resident(frames.cast_const()),
len: batch.len(), len: batch.len(),
_arena: std::marker::PhantomData,
}) })
} }
@@ -2117,7 +2122,10 @@ impl GroupItem {
/// form. An undeclared key has no field form and is dropped; a wrong-typed /// form. An undeclared key has no field form and is dropped; a wrong-typed
/// stored value leaves its field's default, matching the legacy read. /// stored value leaves its field's default, matching the legacy read.
/// `None` reports arena exhaustion. /// `None` reports arena exhaustion.
pub fn from_list<T: Clone + Send + Sync + graphene_hash::CacheHash + PartialEq + 'static>(list: crate::list::List<T>, arena: &crate::arena::Arena) -> Option<GroupItem> { pub fn from_list<T: Clone + Send + Sync + graphene_hash::CacheHash + PartialEq + dyn_any::StaticTypeSized>(list: crate::list::List<T>, arena: &'e crate::arena::Arena) -> Option<GroupItem<'e>>
where
T::Static: Clone + Send + Sync,
{
let declared: Vec<crate::attribute::AttributeInfo> = list.attribute_keys().filter_map(crate::attribute::info).collect(); let declared: Vec<crate::attribute::AttributeInfo> = list.attribute_keys().filter_map(crate::attribute::info).collect();
let writes: Vec<FieldWrite> = declared.iter().map(|info| (info.field_write_at)(0)).collect(); let writes: Vec<FieldWrite> = declared.iter().map(|info| (info.field_write_at)(0)).collect();
let mut builder = RunBuilder::new(arena, element_write_hashed::<T>(), &writes, list.len())?; let mut builder = RunBuilder::new(arena, element_write_hashed::<T>(), &writes, list.len())?;
@@ -2157,7 +2165,7 @@ impl GroupItem {
/// # Safety /// # Safety
/// The frames must stay valid for the evaluation. Arena-resident batches /// The frames must stay valid for the evaluation. Arena-resident batches
/// qualify, caller stack scratch does not. /// qualify, caller stack scratch does not.
pub unsafe fn from_resident(batch: crate::node::RecordBatch<'_>) -> Self { pub unsafe fn from_resident(batch: crate::node::RecordBatch<'e>) -> Self {
let layout = batch.layout().clone(); let layout = batch.layout().clone();
assert!(!layout.element.parked || layout.element.content_hash.is_some(), "a parked element adopts only with content glue"); assert!(!layout.element.parked || layout.element.content_hash.is_some(), "a parked element adopts only with content glue");
for field in &layout.fields { for field in &layout.fields {
@@ -2167,6 +2175,7 @@ impl GroupItem {
storage: ItemStorage::Resident(batch.frames_ptr()), storage: ItemStorage::Resident(batch.frames_ptr()),
len: batch.len(), len: batch.len(),
layout, layout,
_arena: std::marker::PhantomData,
} }
} }
@@ -2181,9 +2190,14 @@ impl GroupItem {
/// The item deep-copied out of its evaluation: lane bytes plus owned /// The item deep-copied out of its evaluation: lane bytes plus owned
/// clones of every parked payload, for storage that outlives the arena /// clones of every parked payload, for storage that outlives the arena
/// generation. An owned item cannot be read until it replays. /// generation. An owned item cannot be read until it replays.
pub fn copy_out(&self) -> GroupItem { pub fn copy_out(&self) -> GroupItem<'static> {
if let ItemStorage::Owned(_) = &self.storage { if let ItemStorage::Owned(_) = &self.storage {
return self.clone(); return GroupItem {
layout: self.layout.clone(),
storage: self.storage.clone(),
len: self.len,
_arena: std::marker::PhantomData,
};
} }
let stride = self.layout.lane_stride(); let stride = self.layout.lane_stride();
let frames = self.frames(); let frames = self.frames();
@@ -2217,15 +2231,18 @@ impl GroupItem {
layout: self.layout.clone(), layout: self.layout.clone(),
storage: ItemStorage::Owned(std::sync::Arc::new(OwnedLanes { bytes, elements, fields })), storage: ItemStorage::Owned(std::sync::Arc::new(OwnedLanes { bytes, elements, fields })),
len: self.len, len: self.len,
_arena: std::marker::PhantomData,
} }
} }
/// Re-parks an owned item's lanes into `arena`, restoring the resident /// Re-parks an owned item's lanes into `arena`, restoring the resident
/// form; `None` reports arena exhaustion. A resident item returns a plain /// form; `None` reports arena exhaustion. A resident item returns a plain
/// clone. /// clone.
pub fn replay(&self, arena: &crate::arena::Arena) -> Option<GroupItem> { pub fn replay<'a>(&self, arena: &'a crate::arena::Arena) -> Option<GroupItem<'a>> {
let ItemStorage::Owned(owned) = &self.storage else { let ItemStorage::Owned(owned) = &self.storage else {
return Some(self.clone()); // A resident item re-serves at the target arena's own lifetime,
// so its lanes copy rather than relabeling the borrow.
return GroupItem::adopt(self.lanes(), arena);
}; };
let stride = self.layout.lane_stride(); let stride = self.layout.lane_stride();
let scratch = arena.alloc_scratch::<u64>((self.len * stride).div_ceil(8))?; let scratch = arena.alloc_scratch::<u64>((self.len * stride).div_ceil(8))?;
@@ -2256,13 +2273,14 @@ impl GroupItem {
layout: self.layout.clone(), layout: self.layout.clone(),
storage: ItemStorage::Resident(frames.cast_const()), storage: ItemStorage::Resident(frames.cast_const()),
len: self.len, len: self.len,
_arena: std::marker::PhantomData,
}) })
} }
/// A typed view over the stored records, checked against the layout's /// A typed view over the stored records, checked against the layout's
/// element type. /// element type.
pub fn typed_lanes<T: 'static>(&self) -> Option<crate::node::List<'_, T>> { pub fn typed_lanes<T: dyn_any::StaticTypeSized>(&self) -> Option<crate::node::List<'_, T>> {
match self.layout.element.type_id == std::any::TypeId::of::<T>() { match self.layout.element.type_id == std::any::TypeId::of::<T::Static>() {
// SAFETY: the layout records the element type the lanes hold. // SAFETY: the layout records the element type the lanes hold.
true => Some(unsafe { crate::node::List::new(self.lanes()) }), true => Some(unsafe { crate::node::List::new(self.lanes()) }),
false => None, false => None,
@@ -2273,27 +2291,27 @@ impl GroupItem {
/// A run read at its element type. Record fields hold each marker's value /// A run read at its element type. Record fields hold each marker's value
/// verbatim, so lane reads are plain typed reads at a hoisted offset. /// verbatim, so lane reads are plain typed reads at a hoisted offset.
pub struct RunView<'a, T> { pub struct RunView<'a, T> {
item: &'a GroupItem, item: &'a GroupItem<'a>,
lanes: crate::node::List<'a, T>, lanes: crate::node::List<'a, T>,
} }
impl<'a, T: 'static> RunView<'a, T> { impl<'a, T: dyn_any::StaticTypeSized> RunView<'a, T> {
/// `None` where the run holds another element type. /// `None` where the run holds another element type.
pub fn new(item: &'a GroupItem) -> Option<Self> { pub fn new(item: &'a GroupItem<'a>) -> Option<Self> {
item.typed_lanes::<T>().map(|lanes| Self { item, lanes }) item.typed_lanes::<T>().map(|lanes| Self { item, lanes })
} }
} }
/// A marker's field on a run, its offset resolved once. /// A marker's field on a run, its offset resolved once.
pub struct RunColumn<'a, A: crate::attribute::Attribute> { pub struct RunColumn<'a, A: crate::attribute::Attribute> {
item: &'a GroupItem, item: &'a GroupItem<'a>,
offset: Option<usize>, offset: Option<usize>,
marker: std::marker::PhantomData<A>, marker: std::marker::PhantomData<A>,
} }
impl<'a, A: crate::attribute::Attribute> RunColumn<'a, A> { impl<'a, A: crate::attribute::Attribute> RunColumn<'a, A> {
/// The marker's column on an item, its offset resolved once. /// The marker's column on an item, its offset resolved once.
pub fn of(item: &'a GroupItem) -> Self { pub fn of(item: &'a GroupItem<'a>) -> Self {
Self { Self {
item, item,
offset: item.layout().offset_of(A::NAME, 0), offset: item.layout().offset_of(A::NAME, 0),
@@ -2310,7 +2328,7 @@ impl<'a, A: crate::attribute::Attribute> crate::lane::LaneColumn<'a, A> for RunC
} }
} }
impl<'a, T: 'static> crate::lane::LaneSource for RunView<'a, T> { impl<'a, T: dyn_any::StaticTypeSized> crate::lane::LaneSource for RunView<'a, T> {
type Element = T; type Element = T;
type Column<'b, A: crate::attribute::Attribute> type Column<'b, A: crate::attribute::Attribute>
= RunColumn<'b, A> = RunColumn<'b, A>
@@ -2330,14 +2348,14 @@ impl<'a, T: 'static> crate::lane::LaneSource for RunView<'a, T> {
} }
} }
impl<T: crate::render_complexity::RenderComplexity + 'static> crate::render_complexity::RenderComplexity for RunView<'_, T> { impl<T: crate::render_complexity::RenderComplexity + dyn_any::StaticTypeSized> crate::render_complexity::RenderComplexity for RunView<'_, T> {
fn render_complexity(&self) -> usize { fn render_complexity(&self) -> usize {
use crate::lane::LaneSource; use crate::lane::LaneSource;
(0..self.lane_count()).filter_map(|lane| self.element(lane)).map(crate::render_complexity::RenderComplexity::render_complexity).sum() (0..self.lane_count()).filter_map(|lane| self.element(lane)).map(crate::render_complexity::RenderComplexity::render_complexity).sum()
} }
} }
impl<T: crate::bounds::BoundingBox + 'static> crate::bounds::BoundingBox for RunView<'_, T> { impl<T: crate::bounds::BoundingBox + dyn_any::StaticTypeSized> crate::bounds::BoundingBox for RunView<'_, T> {
fn bounding_box(&self, transform: glam::DAffine2, include_stroke: bool) -> crate::bounds::RenderBoundingBox { fn bounding_box(&self, transform: glam::DAffine2, include_stroke: bool) -> crate::bounds::RenderBoundingBox {
crate::bounds::lane_bounding_box(self, transform, include_stroke) crate::bounds::lane_bounding_box(self, transform, include_stroke)
} }
@@ -2352,14 +2370,14 @@ impl<T: crate::bounds::BoundingBox + 'static> crate::bounds::BoundingBox for Run
/// it `None`, because that lane's record carries the attributes. The typed /// it `None`, because that lane's record carries the attributes. The typed
/// segment stack returns when merge constructs segments. /// segment stack returns when merge constructs segments.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Group { pub struct Group<'e> {
pub row: Option<GroupItem>, pub row: Option<GroupItem<'e>>,
pub content: GroupItem, pub content: GroupItem<'e>,
} }
impl Group { impl<'e> Group<'e> {
/// The group deep-copied out of its evaluation, every run in owned form. /// The group deep-copied out of its evaluation, every run in owned form.
pub fn copy_out(&self) -> Group { pub fn copy_out(&self) -> Group<'static> {
Group { Group {
row: self.row.as_ref().map(GroupItem::copy_out), row: self.row.as_ref().map(GroupItem::copy_out),
content: self.content.copy_out(), content: self.content.copy_out(),
@@ -2368,7 +2386,7 @@ impl Group {
/// Re-parks an owned group's runs into `arena`; `None` reports arena /// Re-parks an owned group's runs into `arena`; `None` reports arena
/// exhaustion. /// exhaustion.
pub fn replay(&self, arena: &crate::arena::Arena) -> Option<Group> { pub fn replay<'a>(&self, arena: &'a crate::arena::Arena) -> Option<Group<'a>> {
let row = match &self.row { let row = match &self.row {
Some(row) => Some(row.replay(arena)?), Some(row) => Some(row.replay(arena)?),
None => None, None => None,
@@ -2421,7 +2439,7 @@ fn layout_shape_hash(layout: &Layout, state: &mut dyn core::hash::Hasher) {
} }
} }
impl PartialEq for GroupItem { impl PartialEq for GroupItem<'_> {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
if self.layout != other.layout || self.len != other.len { if self.layout != other.layout || self.len != other.len {
return false; return false;
@@ -2433,7 +2451,7 @@ impl PartialEq for GroupItem {
} }
} }
impl graphene_hash::CacheHash for GroupItem { impl graphene_hash::CacheHash for GroupItem<'_> {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) { fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
layout_shape_hash(&self.layout, state); layout_shape_hash(&self.layout, state);
state.write_usize(self.len); state.write_usize(self.len);
@@ -2446,13 +2464,13 @@ impl graphene_hash::CacheHash for GroupItem {
} }
} }
impl PartialEq for Group { impl PartialEq for Group<'_> {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.row == other.row && self.content == other.content self.row == other.row && self.content == other.content
} }
} }
impl graphene_hash::CacheHash for Group { impl graphene_hash::CacheHash for Group<'_> {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) { fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
state.write_u8(self.row.is_some() as u8); state.write_u8(self.row.is_some() as u8);
if let Some(row) = &self.row { if let Some(row) = &self.row {