Carry erased element and re-park glue on layouts and collapse memoize to a generic record row

This commit is contained in:
Dennis Kobert
2026-08-06 08:01:19 +00:00
parent 83fda4f08c
commit 98be2b8c79
7 changed files with 422 additions and 138 deletions

View File

@@ -655,6 +655,27 @@ mod test {
assert!(fields.is_empty(), "an element-only record has no attribute fields");
}
#[test]
fn a_memoize_row_wires_generically_and_replays_over_record_wires() {
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(2),
nodes: vec![
(NodeId(0), ProtoNode::value(ConstructionArgs::Value(TaggedValue::String(String::from("cached")).into()), vec![])),
(NodeId(1), proto_node("graphene_core::memo::MemoizeNode", vec![NodeId(0)])),
(NodeId(2), proto_node("core_types::record::RecordExtractNode", vec![NodeId(1)])),
],
};
let executor = DynamicExecutor::new(network).unwrap();
assert_eq!((&executor).execute(()).unwrap(), GPoll::Final(TaggedValue::String(String::from("cached"))));
assert_eq!(
(&executor).execute(()).unwrap(),
GPoll::Final(TaggedValue::String(String::from("cached"))),
"the second execution replays the deep copy against a reset arena"
);
}
fn modification_value() -> ProtoNode {
let modification = core_types::ContextModification::from_sources(core_types::context::ContextFeatures::all(), &[]);
ProtoNode::value(ConstructionArgs::Value(TaggedValue::ContextModification(modification).into()), vec![])

View File

@@ -189,93 +189,30 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
// ==========
// MEMO NODES
// ==========
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => ()]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RuntimeHandle]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => SourceId]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => bool]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Artboard>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Graphic>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Vector>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Raster<CPU>>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Color>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Image<Color>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientStops>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<String>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<NodeId>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<f64>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<u8>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<bool>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<DAffine2>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<BlendMode>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<graphene_std::vector::style::GradientType>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<graphene_std::vector::style::GradientSpreadMethod>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => AttributeDyn]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => AttributeValueDyn]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => ListDyn]),
#[cfg(target_family = "wasm")]
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => CanvasHandle]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => f64]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => f32]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => u32]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => u64]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => DVec2]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => String]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => DAffine2]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Footprint]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RenderOutput]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => std::sync::Arc<PlatformEditorApi>]),
#[cfg(feature = "gpu")]
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Raster<GPU>>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option<f64>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option<Color>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Graphic]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => glam::f32::Vec2]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => glam::f32::Affine2]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Stroke]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::Font]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<BrushStroke>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => DocumentNode]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::ContextModification]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::Footprint]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Box<graphene_std::vector::VectorModification>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::blending::BlendMode]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::LuminanceCalculation]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::QRCodeErrorCorrectionLevel]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::extract_xy::XY]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::text_nodes::StringCapitalization]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RedGreenBlue]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RedGreenBlueAlpha]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::animation::RealTimeMode]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::NoiseType]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::FractalType]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::CellularDistanceFunction]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::CellularReturnType]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::DomainWarpType]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RelativeAbsolute]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::SelectiveColorChoice]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::RowsOrColumns]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeCap]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeJoin]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeAlign]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::PaintOrder]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientSpreadMethod]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option<DAffine2>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::BooleanOperation]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::TextAlign]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ScaleType]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::InterpolationDistribution]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuExecutorHandle]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option<wgpu_executor::WgpuExecutorHandle>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuPipelineCache]),
(
ProtoNodeIdentifier::new("graphene_core::memo::MemoizeNode"),
RegistryEntry {
io: NodeIOTypes::new(
concrete!(Context),
core_types::Type::Record(Box::new(core_types::Type::Generic(std::borrow::Cow::Borrowed("T")))),
vec![core_types::registry::generic_record_edge_type("T")],
),
constructor: |inputs| {
if inputs.len() != 1 {
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
}
let mut inputs = inputs.into_iter();
let handle = inputs.next().unwrap();
let ty = handle.ty().clone();
let Some(layout) = handle.layout().cloned() else {
return Err(ConstructionError::MissingLayout);
};
let edge = handle.downcast_erased::<core_types::registry::ErasedRecordNode>(ty.clone())?;
let node = core_types::record::RecordMemo::new(edge, &layout);
Ok(EdgeHandle::new_erased(std::sync::Arc::new(node) as std::sync::Arc<core_types::registry::ErasedRecordNode>, ty))
},
},
),
// ============
// REF ADAPTERS
// ============

View File

@@ -37,6 +37,10 @@ pub trait Attribute: 'static {
/// # Safety
/// `ptr` must point at a live field of this marker's value type.
unsafe fn read_erased(ptr: *const u8) -> Box<dyn AnyAttributeValue>;
/// Re-parks the owned clone [`Self::read_erased`] produced into fresh
/// field storage; `None` for plain values, which ride the byte copy.
const REPARK: Option<unsafe fn(&dyn AnyAttributeValue, *mut u8, &crate::arena::Arena) -> Option<()>> = None;
}
/// A kernel-facing attribute value. A parameter `Attr<A>` is a read of `A`
@@ -162,6 +166,16 @@ macro_rules! attribute {
unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box<dyn $crate::list::AnyAttributeValue> {
::std::boxed::Box::new(unsafe { ptr.cast::<&$value>().read() }.to_owned())
}
const REPARK: ::core::option::Option<unsafe fn(&dyn $crate::list::AnyAttributeValue, *mut u8, &$crate::arena::Arena) -> ::core::option::Option<()>> = {
unsafe fn repark(value: &dyn $crate::list::AnyAttributeValue, dst: *mut u8, arena: &$crate::arena::Arena) -> ::core::option::Option<()> {
let owned: &<$value as ::std::borrow::ToOwned>::Owned = value.as_any().downcast_ref().expect("a reference attribute replays its owned clone");
let (parked, _) = arena.alloc(<$value as ::std::borrow::ToOwned>::to_owned(::std::borrow::Borrow::borrow(owned)))?;
unsafe { dst.cast::<&$value>().write(::std::borrow::Borrow::borrow(parked)) };
::core::option::Option::Some(())
}
::core::option::Option::Some(repark)
};
}
$crate::attribute!(@register $marker);

View File

@@ -20,6 +20,7 @@ pub struct FieldWrite {
pub size: usize,
pub align: usize,
pub read_erased: unsafe fn(*const u8) -> Box<dyn crate::list::AnyAttributeValue>,
pub repark: Option<unsafe fn(&dyn crate::list::AnyAttributeValue, *mut u8, &crate::arena::Arena) -> Option<()>>,
}
impl FieldWrite {
@@ -30,6 +31,7 @@ impl FieldWrite {
size: size_of::<A::Value<'static>>(),
align: align_of::<A::Value<'static>>(),
read_erased: A::read_erased,
repark: A::REPARK,
}
}
}
@@ -47,6 +49,7 @@ pub struct FieldDesc {
pub size: usize,
pub align: usize,
pub read_erased: unsafe fn(*const u8) -> Box<dyn crate::list::AnyAttributeValue>,
pub repark: Option<unsafe fn(&dyn crate::list::AnyAttributeValue, *mut u8, &crate::arena::Arena) -> Option<()>>,
}
impl PartialEq for FieldDesc {
@@ -57,14 +60,52 @@ impl PartialEq for FieldDesc {
impl Eq for FieldDesc {}
/// The element slot of a layout: its dimensions plus erased glue bound where
/// the element type is statically known, so generic consumers read or
/// deep-copy the element without it. Equality is structural: glue pointers
/// are excluded for the same reason as [`FieldDesc`]'s.
#[derive(Clone, Copy, Debug)]
pub struct ElementWrite {
pub size: usize,
pub align: usize,
pub parked: bool,
pub clone_out: unsafe fn(*const u8) -> Box<dyn std::any::Any + Send + Sync>,
pub repark: unsafe fn(&(dyn std::any::Any + Send + Sync), *mut u8, &crate::arena::Arena) -> Option<()>,
}
impl PartialEq for ElementWrite {
fn eq(&self, other: &Self) -> bool {
(self.size, self.align, self.parked) == (other.size, other.align, other.parked)
}
}
impl Eq for ElementWrite {}
impl Default for ElementWrite {
fn default() -> Self {
unsafe fn clone_out(_ptr: *const u8) -> Box<dyn std::any::Any + Send + Sync> {
Box::new(())
}
unsafe fn repark(_value: &(dyn std::any::Any + Send + Sync), _dst: *mut u8, _arena: &crate::arena::Arena) -> Option<()> {
Some(())
}
Self {
size: 0,
align: 0,
parked: false,
clone_out,
repark,
}
}
}
/// A record layout: the element at offset 0, then the written attributes in
/// canonical order (descending alignment, then size, then name, then level).
/// Layouts are derived data, a pure function of the upstream write set.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Layout {
pub depth: u8,
pub element_size: usize,
pub element_align: usize,
pub element: ElementWrite,
pub fields: Vec<FieldDesc>,
pub size: usize,
pub align: usize,
@@ -92,11 +133,11 @@ impl Layout {
}
}
/// The union of this layout's fields and `writes` over an element of
/// (size, align) at `depth`, in canonical order. A (name, level) written
/// at a different size is a type conflict and panics; the census keeps
/// declared names to one type, so this only fires on wiring bugs.
pub fn with_writes(&self, depth: u8, element: (usize, usize), writes: &[FieldWrite]) -> Layout {
/// The union of this layout's fields and `writes` over `element` at
/// `depth`, in canonical order. A (name, level) written at a different
/// size is a type conflict and panics; the census keeps declared names to
/// one type, so this only fires on wiring bugs.
pub fn with_writes(&self, depth: u8, element: ElementWrite, writes: &[FieldWrite]) -> Layout {
let mut merged: Vec<FieldWrite> = self
.fields
.iter()
@@ -106,6 +147,7 @@ impl Layout {
size: field.size,
align: field.align,
read_erased: field.read_erased,
repark: field.repark,
})
.collect();
for &write in writes {
@@ -115,9 +157,8 @@ impl Layout {
}
}
merged.sort_by(|a, b| b.align.cmp(&a.align).then(b.size.cmp(&a.size)).then(a.name.cmp(b.name)).then(a.level.cmp(&b.level)));
let (element_size, element_align) = element;
let mut offset = element_size;
let mut align = element_align.max(1);
let mut offset = element.size;
let mut align = element.align.max(1);
let fields = merged
.into_iter()
.map(|write| {
@@ -130,6 +171,7 @@ impl Layout {
size: write.size,
align: write.align,
read_erased: write.read_erased,
repark: write.repark,
};
offset += write.size;
desc
@@ -137,8 +179,7 @@ impl Layout {
.collect();
Layout {
depth,
element_size,
element_align,
element,
fields,
size: offset,
align,
@@ -148,10 +189,9 @@ impl Layout {
/// The union of several layouts over the same element and depth.
pub fn union(layouts: &[&Layout]) -> Layout {
let first = layouts.first().expect("a union needs at least one layout");
let mut union = Layout::default().with_writes(first.depth, (first.element_size, first.element_align), &[]);
let mut union = Layout::default().with_writes(first.depth, first.element, &[]);
for layout in layouts {
assert_eq!(union.element_size, layout.element_size, "union layouts must share the element size");
assert_eq!(union.element_align, layout.element_align, "union layouts must share the element alignment");
assert_eq!(union.element, layout.element, "union layouts must share the element");
assert_eq!(union.depth, layout.depth, "union layouts must share the depth");
let writes: Vec<FieldWrite> = layout
.fields
@@ -162,9 +202,10 @@ impl Layout {
size: field.size,
align: field.align,
read_erased: field.read_erased,
repark: field.repark,
})
.collect();
union = union.with_writes(union.depth, (union.element_size, union.element_align), &writes);
union = union.with_writes(union.depth, union.element, &writes);
}
union
}
@@ -500,9 +541,9 @@ pub mod stack {
pub fn copy_plan(from: &Layout, to: &Layout, carry_element: bool) -> Vec<(usize, usize, usize)> {
let mut plan = Vec::new();
if carry_element {
assert_eq!(from.element_size, to.element_size, "a carried element must keep its size");
if from.element_size > 0 {
plan.push((0, 0, from.element_size));
assert_eq!(from.element.size, to.element.size, "a carried element must keep its size");
if from.element.size > 0 {
plan.push((0, 0, from.element.size));
}
}
for field in &from.fields {
@@ -534,6 +575,26 @@ pub fn element_dims<T>() -> (usize, usize) {
}
}
/// The element slot a record wire of `T` carries, its erased glue bound at
/// the statically-known type.
pub fn element_write<T: Clone + Send + Sync + 'static>() -> ElementWrite {
unsafe fn clone_out<T: Clone + Send + Sync + 'static>(ptr: *const u8) -> Box<dyn std::any::Any + Send + Sync> {
Box::new(unsafe { read_element::<T>(Rec::new(ptr)) })
}
unsafe fn repark<T: Clone + Send + Sync + 'static>(value: &(dyn std::any::Any + Send + Sync), dst: *mut u8, arena: &crate::arena::Arena) -> Option<()> {
let value = value.downcast_ref::<T>().expect("an element replays at its own type");
unsafe { write_element(dst, value.clone(), arena) }
}
let (size, align) = element_dims::<T>();
ElementWrite {
size,
align,
parked: element_parked::<T>(),
clone_out: clone_out::<T>,
repark: repark::<T>,
}
}
/// # Safety
/// The record's element must be a `T` in the form [`element_parked`] picks,
/// and the borrow is only valid while the record is.
@@ -728,6 +789,128 @@ where
}
}
/// A record deep-copied out of its evaluation: the packed bytes plus owned
/// clones of every parked payload, replayable into a later evaluation's
/// storage through the layout's erased glue. The layout stays with the
/// holder, which proved it at wiring.
pub struct OwnedRecord {
bytes: Box<[u8]>,
element: Option<Box<dyn std::any::Any + Send + Sync>>,
fields: Vec<(usize, Box<dyn crate::list::AnyAttributeValue>)>,
}
impl OwnedRecord {
/// # Safety
/// `rec` must be a live record of `layout`.
pub unsafe fn copy_out(layout: &Layout, rec: Rec) -> OwnedRecord {
let bytes: Box<[u8]> = unsafe { std::slice::from_raw_parts(rec.ptr(), layout.size) }.into();
let element = layout.element.parked.then(|| unsafe { (layout.element.clone_out)(rec.ptr()) });
let fields = layout
.fields
.iter()
.enumerate()
.filter(|(_, field)| field.repark.is_some())
.map(|(index, field)| (index, unsafe { (field.read_erased)(rec.ptr().add(field.offset)) }))
.collect();
OwnedRecord { bytes, element, fields }
}
/// Replays the copy into fresh storage of `layout`, the layout it was
/// copied out under, re-parking droppable payloads against `arena`;
/// `None` reports arena exhaustion.
pub fn replay<'e>(&self, layout: &Layout, arena: &'e crate::arena::Arena) -> Option<RecordValue<'e>> {
let mut value = RecordValue::zeroed();
let dst = match layout.frame_bytes() {
0 => value.as_mut_ptr(),
bytes => stack::push(bytes),
};
let written = self.write_into(layout, dst, arena);
if layout.frame_bytes() != 0 {
stack::pop(dst);
value = RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) });
}
written.map(|()| value)
}
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 {
unsafe { (layout.element.repark)(&**element, dst, arena) }?;
}
for (index, value) in &self.fields {
let field = &layout.fields[*index];
let repark = field.repark.expect("copied fields carry re-park glue");
unsafe { repark(&**value, dst.add(field.offset), arena) }?;
}
Some(())
}
}
// TODO: Convert to a `#[node_macro::node]` node with the monitor, once the
// macro grows a capture capability.
/// Memoizes a record wire: a hit replays the deep copy into the current
/// evaluation, a miss evaluates the edge and copies the record out.
pub struct RecordMemo<N> {
edge: N,
layout: Layout,
cache: std::sync::Mutex<Option<(u64, OwnedRecord, crate::gpoll::Finality)>>,
}
impl<N> RecordMemo<N> {
pub fn new(edge: N, layout: &Layout) -> Self {
Self {
edge,
layout: layout.clone(),
cache: std::sync::Mutex::new(None),
}
}
}
impl<'e, C, N> Node<C> for RecordMemo<N>
where
C: crate::graphene_hash::CacheHash + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
N: Node<C, Output = RecordValue<'e>>,
{
type Output = RecordValue<'e>;
fn eval(&self, input: &C) -> GPoll<RecordValue<'e>> {
let key = crate::registry::cache_key(input);
{
let cache = self.cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some((hash, copy, finality)) = cache.as_ref()
&& *hash == key
{
return match copy.replay(&self.layout, input.arena()) {
Some(value) => match finality {
crate::gpoll::Finality::AllFinal => GPoll::Final(value),
crate::gpoll::Finality::Partial => GPoll::Partial(value),
},
None => GPoll::arena_exhausted(),
};
}
}
let result = self.edge.eval(input);
let publishable = match &result {
GPoll::Final(record) => Some((record, crate::gpoll::Finality::AllFinal)),
GPoll::Partial(record) => Some((record, crate::gpoll::Finality::Partial)),
_ => None,
};
if let Some((record, finality)) = publishable {
let copy = unsafe { OwnedRecord::copy_out(&self.layout, self.layout.rec(record)) };
*self.cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = Some((key, copy, finality));
}
result
}
fn extent(&self, input: &C) -> GPoll<crate::gpoll::Extent> {
self.edge.extent(input)
}
fn layout(&self) -> Option<&Layout> {
Some(&self.layout)
}
}
/// Lifts a plain producer onto a record wire: the element lands at offset 0
/// of a fresh element-only record, parked when it carries drop glue.
pub struct RecordLift<El, N> {
@@ -736,11 +919,11 @@ pub struct RecordLift<El, N> {
_marker: std::marker::PhantomData<fn() -> El>,
}
impl<El: 'static, N> RecordLift<El, N> {
impl<El: Clone + Send + Sync + 'static, N> RecordLift<El, N> {
pub fn new(edge: N) -> Self {
Self {
edge,
layout: Layout::default().with_writes(0, element_dims::<El>(), &[]),
layout: Layout::default().with_writes(0, element_write::<El>(), &[]),
_marker: std::marker::PhantomData,
}
}
@@ -771,11 +954,11 @@ pub struct RecordLiftLend<El, N> {
_marker: std::marker::PhantomData<fn() -> El>,
}
impl<El: 'static, N> RecordLiftLend<El, N> {
impl<El: Clone + Send + Sync + 'static, N> RecordLiftLend<El, N> {
pub fn new(edge: N) -> Self {
Self {
edge,
layout: Layout::default().with_writes(0, element_dims::<El>(), &[]),
layout: Layout::default().with_writes(0, element_write::<El>(), &[]),
_marker: std::marker::PhantomData,
}
}
@@ -930,7 +1113,14 @@ mod tests {
}
fn sized_field(name: &'static str, size: usize, align: usize) -> FieldWrite {
FieldWrite { name, level: 0, size, align, read_erased: unread }
FieldWrite {
name,
level: 0,
size,
align,
read_erased: unread,
repark: None,
}
}
fn f64_field(name: &'static str) -> FieldWrite {
@@ -939,7 +1129,7 @@ mod tests {
#[test]
fn canonical_order_and_offsets() {
let layout = Layout::default().with_writes(0, (8, 8), &[sized_field("tint", 4, 4), f64_field("opacity"), sized_field("flag", 1, 1)]);
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[sized_field("tint", 4, 4), f64_field("opacity"), sized_field("flag", 1, 1)]);
assert_eq!(layout.offset_of("opacity", 0), Some(8));
assert_eq!(layout.offset_of("tint", 0), Some(16));
assert_eq!(layout.offset_of("flag", 0), Some(20));
@@ -950,22 +1140,22 @@ mod tests {
#[test]
#[should_panic(expected = "two different sizes")]
fn size_conflicts_panic() {
let layout = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]);
layout.with_writes(0, (8, 8), &[sized_field("opacity", 4, 4)]);
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("opacity")]);
layout.with_writes(0, element_write::<f64>(), &[sized_field("opacity", 4, 4)]);
}
#[test]
fn union_is_order_independent() {
let a = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]);
let b = Layout::default().with_writes(0, (8, 8), &[f64_field("length")]);
let a = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("opacity")]);
let b = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("length")]);
assert_eq!(Layout::union(&[&a, &b]), Layout::union(&[&b, &a]));
assert!(Layout::union(&[&a, &b]).offset_of("length", 0).is_some());
}
#[test]
fn translation_moves_fields_and_fills_census_defaults() {
let source = Layout::default().with_writes(0, (8, 8), &[f64_field("length")]);
let union = Layout::union(&[&source, &Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")])]);
let source = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("length")]);
let union = Layout::union(&[&source, &Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("opacity")])]);
let plan = SourcePlan::new(&source, &union).unwrap();
let record = [5f64, 7f64];
@@ -978,7 +1168,7 @@ mod tests {
#[test]
fn identity_layouts_forward() {
let layout = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]);
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("opacity")]);
assert!(SourcePlan::new(&layout, &layout.clone()).is_none());
}
@@ -1009,6 +1199,29 @@ mod tests {
assert_eq!(unsafe { read_element::<String>(rec) }, "moved once");
}
#[test]
fn owned_records_replay_re_parked_payloads_after_the_source_dies() {
let layout = Layout::default().with_writes(0, element_write::<String>(), &[FieldWrite::of::<crate::attribute::Name>(0)]);
let mut buffer = vec![0u64; layout.size.div_ceil(8)];
let base: *mut u8 = buffer.as_mut_ptr().cast();
let copy = {
let arena = crate::arena::Arena::new(1024).unwrap();
unsafe { write_element(base, String::from("element"), &arena) }.unwrap();
let (name, _) = arena.alloc(String::from("field")).unwrap();
unsafe { write_field::<&str>(base, layout.offset_of("name", 0).unwrap(), name.as_str()) };
unsafe { OwnedRecord::copy_out(&layout, Rec::new(base)) }
};
buffer.fill(u64::MAX);
let replay_arena = crate::arena::Arena::new(1024).unwrap();
stack::reserve(layout.frame_bytes());
let value = copy.replay(&layout, &replay_arena).unwrap();
let rec = layout.rec(&value);
assert_eq!(unsafe { read_element::<String>(rec) }, "element");
assert_eq!(unsafe { rec.read::<&str>(layout.offset_of("name", 0).unwrap()) }, "field");
}
#[test]
fn record_values_are_two_words() {
assert_eq!(size_of::<RecordValue>(), 16);
@@ -1017,7 +1230,7 @@ mod tests {
#[test]
fn layouts_resolve_inline_and_spilled_values() {
let inline = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]);
let inline = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("opacity")]);
assert!(inline.is_inline());
assert_eq!(inline.frame_bytes(), 0);
let mut value = RecordValue::zeroed();
@@ -1029,7 +1242,7 @@ mod tests {
assert_eq!(unsafe { rec.element::<f64>() }, 4.);
assert_eq!(unsafe { rec.read::<f64>(inline.offset_of("opacity", 0).unwrap()) }, 0.5);
let spilled = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity"), f64_field("length")]);
let spilled = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("opacity"), f64_field("length")]);
assert!(!spilled.is_inline());
assert_eq!(spilled.frame_bytes(), 24);
let record = [1f64, 2., 3.];

View File

@@ -171,7 +171,7 @@ mod tests {
use crate::context::{ContextImpl, Ctx, CtxSnapshot, EvalScope, ExtractFootprint, ExtractVarArgs, VarArgLink, VarArgSlots};
use crate::gpoll::GPoll;
use crate::node::Node;
use crate::record::{Layout, RecordExtract, RecordLift, element_dims};
use crate::record::{Layout, RecordExtract, RecordLift, element_write};
use crate::transform::Footprint;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU32, Ordering};
@@ -258,15 +258,15 @@ mod tests {
}
}
fn element_layout<T: 'static>() -> Layout {
Layout::default().with_writes(0, element_dims::<T>(), &[])
fn element_layout<T: Clone + Send + Sync + 'static>() -> Layout {
Layout::default().with_writes(0, element_write::<T>(), &[])
}
fn lifted<T: Send + Sync + 'static>(value: T) -> RecordLift<T, SourceNode<T>> {
fn lifted<T: Clone + Send + Sync + 'static>(value: T) -> RecordLift<T, SourceNode<T>> {
RecordLift::new(SourceNode(value))
}
fn extract<El: Clone + 'static, N>(graph: N) -> RecordExtract<El, N> {
fn extract<El: Clone + Send + Sync + 'static, N>(graph: N) -> RecordExtract<El, N> {
RecordExtract::new(graph, &element_layout::<El>())
}

View File

@@ -354,7 +354,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let flip_prelude = flip
.then(|| {
quote! {
let __layout = gcore::record::Layout::default().with_writes(0, gcore::record::element_dims::<#slot_value_type>(), &[]);
let __layout = gcore::record::Layout::default().with_writes(0, gcore::record::element_write::<#slot_value_type>(), &[]);
let __frame_bytes = __layout.frame_bytes();
}
})
@@ -365,8 +365,14 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
false => quote!(__layout, __frame_bytes, __marker: ::core::marker::PhantomData,),
})
.into_iter();
let fn_where = &parsed.where_clause;
let new_where = flip.then(|| quote!(#fn_where)).into_iter();
// The flip prelude's `element_write` instantiates the erased glue at the
// output type, so `new` carries the bounds the glue needs.
let new_where = flip
.then(|| {
let existing = parsed.where_clause.iter().flat_map(|clause| clause.predicates.iter());
quote!(where #(#existing,)* #slot_value_type: ::core::clone::Clone + ::core::marker::Send + ::core::marker::Sync + 'static)
})
.into_iter();
let new_impl = match record.is_none() {
true => quote! {
#[automatically_derived]
@@ -1547,19 +1553,19 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
.iter()
.map(|marker| quote!(#core_types::record::FieldWrite::of::<#marker>(0)))
.collect();
let element_dims = match &shape.element_write {
Some(ty) => quote!((::core::mem::size_of::<#ty>(), ::core::mem::align_of::<#ty>())),
None => quote!((__carrier.element_size, __carrier.element_align)),
let element = match &shape.element_write {
Some(ty) => quote!(#core_types::record::element_write::<#ty>()),
None => quote!(__carrier.element),
};
let layout_def = match shape.skips_carrier() {
true => quote! {
#vis fn #layout_fn() -> #core_types::record::Layout {
#core_types::record::Layout::default().with_writes(0, #element_dims, &[#(#write_descs),*])
#core_types::record::Layout::default().with_writes(0, #element, &[#(#write_descs),*])
}
},
false => quote! {
#vis fn #layout_fn(__carrier: &#core_types::record::Layout) -> #core_types::record::Layout {
__carrier.with_writes(__carrier.depth, #element_dims, &[#(#write_descs),*])
__carrier.with_writes(__carrier.depth, #element, &[#(#write_descs),*])
}
},
};
@@ -2302,7 +2308,7 @@ fn flip_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_field
let output_alias = format_ident!("__{}_output", fn_name);
let alias_def = match alias_param_tokens.is_empty() {
true => quote!(#[allow(non_camel_case_types)] type #output_alias = #output;),
false => quote!(#[allow(non_camel_case_types)] type #output_alias<#(#alias_param_tokens,)*> = #output;),
false => quote!(#[allow(non_camel_case_types, type_alias_bounds)] type #output_alias<#(#alias_param_tokens,)*> = #output;),
};
let entries = rows.iter().filter_map(|row| {

View File

@@ -149,9 +149,10 @@ mod tests {
size: 8,
align: 8,
read_erased: <Opacity as AttributeMarker>::read_erased,
repark: None,
})
.collect();
Layout::default().with_writes(0, (8, 8), &writes)
Layout::default().with_writes(0, core_types::record::element_write::<f64>(), &writes)
}
fn reserve_for(layouts: &[&Layout]) {
@@ -270,7 +271,7 @@ mod tests {
let f64_source = f64_layout(&[]);
let f64_faded = fade_layout(&f64_source);
let u32_source = Layout::default().with_writes(0, (4, 4), &[]);
let u32_source = Layout::default().with_writes(0, core_types::record::element_write::<u32>(), &[]);
let u32_faded = fade_layout(&u32_source);
reserve_for(&[&f64_source, &f64_faded, &u32_source, &u32_faded]);
@@ -436,7 +437,7 @@ mod tests {
#[test]
fn census_fills_reference_defaults_from_static_data() {
let source = f64_layout(&[]);
let labeled = Layout::default().with_writes(0, (8, 8), &[core_types::record::FieldWrite::of::<Label>(0)]);
let labeled = Layout::default().with_writes(0, core_types::record::element_write::<f64>(), &[core_types::record::FieldWrite::of::<Label>(0)]);
let plan = core_types::record::SourcePlan::new(&source, &labeled).unwrap();
let record = [5f64];
@@ -729,4 +730,96 @@ mod tests {
};
assert_eq!(unsafe { layout.rec(&value).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);
GPoll::Final(21.)
}
}
#[test]
fn record_memo_replays_the_deep_copy_on_a_context_hit() {
let arena = Arena::new(1024).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
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 layout = Node::<ContextImpl>::layout(&lift).unwrap().clone();
let memo = core_types::record::RecordMemo::new(lift, &layout);
let GPoll::Final(value) = memo.eval(&ctx) else {
panic!("expected a final record");
};
assert_eq!(unsafe { layout.rec(&value).element::<f64>() }, 21.);
let GPoll::Final(value) = memo.eval(&ctx) else {
panic!("expected a final record");
};
assert_eq!(unsafe { layout.rec(&value).element::<f64>() }, 21.);
assert_eq!(evals.load(std::sync::atomic::Ordering::Relaxed), 1, "a context hit must not re-evaluate the edge");
}
#[test]
fn record_memo_caches_partial_finality() {
let arena = Arena::new(1024).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let layout = f64_layout(&["opacity"]);
reserve_for(&[&layout, &layout]);
let source = RecordSourceNode {
layout: layout.clone(),
element: 4.,
fields: vec![(layout.offset_of("opacity", 0).unwrap(), 0.5)],
partial: true,
};
let memo = core_types::record::RecordMemo::new(source, &layout);
let GPoll::Partial(_) = memo.eval(&ctx) else {
panic!("expected a partial record");
};
let GPoll::Partial(value) = memo.eval(&ctx) else {
panic!("expected the replay to keep the partial finality");
};
assert_eq!(unsafe { layout.rec(&value).read::<f64>(layout.offset_of("opacity", 0).unwrap()) }, 0.5);
}
#[test]
fn record_memo_re_parks_droppable_payloads_on_replay() {
let generations = [];
let source_layout = f64_layout(&[]);
let labeled = label_layout(&source_layout);
reserve_for(&[&labeled, &labeled]);
let chain = LabelNode::new(bare_source(&source_layout, 1.), ValueNode(String::from("a")), &source_layout);
let memo = core_types::record::RecordMemo::new(chain, &labeled);
let first_arena = Arena::new(1024).unwrap();
{
let scope = scope_fixture(&generations, &first_arena);
let ctx = ContextImpl::root(&scope);
let GPoll::Final(_) = memo.eval(&ctx) else {
panic!("expected a final record");
};
}
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 {
panic!("expected a final record");
};
let rec = labeled.rec(&value);
assert_eq!(unsafe { rec.element::<f64>() }, 1.);
assert_eq!(unsafe { rec.read::<&str>(labeled.offset_of(Label::NAME, 0).unwrap()) }, "a");
}
}