Retire the lend machinery and flip the clone node onto record wires

This commit is contained in:
Dennis Kobert
2026-08-06 11:57:18 +00:00
parent 886ff03ef2
commit 100f81c307
9 changed files with 60 additions and 499 deletions

View File

@@ -930,12 +930,8 @@ fn ref_adapter(proposed: &Type, wanted: &Type) -> Option<ProtoNodeIdentifier> {
return None;
};
match (proposed_output.as_ref(), wanted_output.as_ref()) {
(Type::Ref(inner), wanted_output @ Type::Concrete(_)) if valid_type(inner, wanted_output) => Some(ProtoNodeIdentifier::new("graphene_core::debug::CloneNode")),
(proposed_output @ Type::Concrete(_), Type::Ref(inner)) if valid_type(proposed_output, inner) => Some(ProtoNodeIdentifier::new("graphene_core::memo::LendNode")),
(Type::Record(inner), wanted_output @ Type::Concrete(_)) if valid_type(inner, wanted_output) => Some(ProtoNodeIdentifier::new("core_types::record::RecordExtractNode")),
(proposed_output @ Type::Concrete(_), Type::Record(inner)) if valid_type(proposed_output, inner) => Some(ProtoNodeIdentifier::new("core_types::record::RecordLiftNode")),
(Type::Ref(inner), Type::Record(wanted_inner)) if valid_type(inner, wanted_inner) => Some(ProtoNodeIdentifier::new("core_types::record::RecordLiftLendNode")),
(Type::Record(inner), Type::Ref(wanted_inner)) if valid_type(inner, wanted_inner) => Some(ProtoNodeIdentifier::new("core_types::record::RecordExtractLendNode")),
_ => None,
}
}
@@ -1299,7 +1295,7 @@ mod test {
}
#[cfg(test)]
mod ref_adapter_test {
mod adapter_splice_test {
use super::*;
fn adapter_lookup() -> &'static HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
@@ -1307,29 +1303,29 @@ mod ref_adapter_test {
let unused: NodeConstructor = |_| Err(ConstructionError::Arity { expected: 0, got: 0 });
[
(
ProtoNodeIdentifier::new("wants_ref"),
ProtoNodeIdentifier::new("wants_owned"),
vec![RegistryEntry {
io: NodeIOTypes::new(concrete!(Context), concrete!(String), vec![lend_edge_type::<String>()]),
io: NodeIOTypes::new(concrete!(Context), concrete!(String), vec![edge_type::<String>()]),
constructor: unused,
}],
),
(
ProtoNodeIdentifier::new("wants_ref_ambiguously"),
ProtoNodeIdentifier::new("wants_owned_ambiguously"),
vec![
RegistryEntry {
io: NodeIOTypes::new(concrete!(Context), concrete!(String), vec![lend_edge_type::<String>()]),
io: NodeIOTypes::new(concrete!(Context), concrete!(String), vec![edge_type::<String>()]),
constructor: unused,
},
RegistryEntry {
io: NodeIOTypes::new(concrete!(Context), concrete!(f64), vec![lend_edge_type::<String>()]),
io: NodeIOTypes::new(concrete!(Context), concrete!(f64), vec![edge_type::<String>()]),
constructor: unused,
},
],
),
(
ProtoNodeIdentifier::new("core_types::record::RecordExtractLendNode"),
ProtoNodeIdentifier::new("core_types::record::RecordExtractNode"),
vec![RegistryEntry {
io: NodeIOTypes::new(concrete!(Context), ref_type::<String>(), vec![record_edge_type::<String>()]),
io: NodeIOTypes::new(concrete!(Context), concrete!(String), vec![record_edge_type::<String>()]),
constructor: unused,
}],
),
@@ -1341,7 +1337,7 @@ mod ref_adapter_test {
}
fn string_value() -> ProtoNode {
ProtoNode::value(ConstructionArgs::Value(TaggedValue::String("lent".to_string()).into()), vec![])
ProtoNode::value(ConstructionArgs::Value(TaggedValue::String("landed".to_string()).into()), vec![])
}
fn consumer(identifier: &str, producer: NodeId) -> ProtoNode {
@@ -1354,11 +1350,11 @@ mod ref_adapter_test {
}
#[test]
fn a_ref_only_mismatch_splices_the_lend_adapter() {
fn a_record_only_mismatch_splices_the_extract_adapter() {
let mut network = ProtoNetwork {
inputs: vec![],
output: NodeId(1),
nodes: vec![(NodeId(0), string_value()), (NodeId(1), consumer("wants_ref", NodeId(0)))],
nodes: vec![(NodeId(0), string_value()), (NodeId(1), consumer("wants_owned", NodeId(0)))],
};
let mut typing = TypingContext::new(adapter_lookup());
@@ -1366,10 +1362,10 @@ mod ref_adapter_test {
assert_eq!(network.nodes.len(), 3);
let (adapter_id, adapter) = &network.nodes[1];
assert_eq!(adapter.identifier.as_str(), "core_types::record::RecordExtractLendNode");
assert_eq!(adapter.identifier.as_str(), "core_types::record::RecordExtractNode");
assert_eq!(adapter.unwrap_construction_nodes(), vec![NodeId(0)]);
assert_eq!(network.nodes[2].1.unwrap_construction_nodes(), vec![*adapter_id]);
assert_eq!(typing.type_of(*adapter_id).unwrap().return_value, ref_type::<String>());
assert_eq!(typing.type_of(*adapter_id).unwrap().return_value, concrete!(String));
assert_eq!(typing.type_of(NodeId(1)).unwrap().return_value, concrete!(String));
}
@@ -1380,8 +1376,8 @@ mod ref_adapter_test {
output: NodeId(2),
nodes: vec![
(NodeId(0), string_value()),
(NodeId(1), consumer("wants_ref", NodeId(0))),
(NodeId(2), consumer("wants_ref", NodeId(0))),
(NodeId(1), consumer("wants_owned", NodeId(0))),
(NodeId(2), consumer("wants_owned", NodeId(0))),
],
};
@@ -1392,7 +1388,7 @@ mod ref_adapter_test {
let adapters: Vec<NodeId> = network
.nodes
.iter()
.filter(|(_, node)| node.identifier.as_str() == "core_types::record::RecordExtractLendNode")
.filter(|(_, node)| node.identifier.as_str() == "core_types::record::RecordExtractNode")
.map(|(id, _)| *id)
.collect();
let [adapter_id] = adapters.as_slice() else {
@@ -1401,18 +1397,18 @@ mod ref_adapter_test {
let consumers: Vec<Vec<NodeId>> = network
.nodes
.iter()
.filter(|(_, node)| node.identifier.as_str() == "wants_ref")
.filter(|(_, node)| node.identifier.as_str() == "wants_owned")
.map(|(_, node)| node.unwrap_construction_nodes())
.collect();
assert_eq!(consumers, vec![vec![*adapter_id], vec![*adapter_id]]);
}
#[test]
fn an_ambiguous_ref_mismatch_stays_an_error() {
fn an_ambiguous_record_mismatch_stays_an_error() {
let mut network = ProtoNetwork {
inputs: vec![],
output: NodeId(1),
nodes: vec![(NodeId(0), string_value()), (NodeId(1), consumer("wants_ref_ambiguously", NodeId(0)))],
nodes: vec![(NodeId(0), string_value()), (NodeId(1), consumer("wants_owned_ambiguously", NodeId(0)))],
};
let mut typing = TypingContext::new(adapter_lookup());

View File

@@ -582,18 +582,25 @@ mod test {
}
#[test]
fn a_lend_adapter_is_spliced_between_a_value_and_a_ref_consumer() {
fn the_clone_node_clones_the_element_out_of_its_record_wire() {
let raster_list = TaggedValue::from_type(&core_types::concrete!(graphene_std::list::List<graphene_std::raster_types::Raster<graphene_std::raster_types::CPU>>)).unwrap();
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(1),
output: NodeId(2),
nodes: vec![
(NodeId(0), string_value("lent")),
(NodeId(0), ProtoNode::value(ConstructionArgs::Value(raster_list.into()), vec![])),
(NodeId(1), proto_node("graphene_core::debug::CloneNode", 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("lent".to_string())));
let arena = Arena::new(1 << 20).unwrap();
let generations = [];
let scope = EvalScope::new(None, None, None, &generations, &arena);
let ctx = ContextImpl::root(&scope);
let result: Option<GPoll<graphene_std::list::List<graphene_std::raster_types::Raster<graphene_std::raster_types::CPU>>>> = executor.tree().eval(NodeId(2), &ctx);
assert!(matches!(result, Some(GPoll::Final(_))), "the flipped clone must evaluate over record wires, got {result:?}");
}
#[test]
@@ -756,7 +763,7 @@ mod test {
}
#[test]
fn a_clone_out_adapter_is_spliced_between_a_lending_producer_and_an_owned_consumer() {
fn stacked_frame_memos_replay_over_record_wires() {
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(3),
@@ -764,7 +771,7 @@ mod test {
(NodeId(0), string_value("memoized")),
(NodeId(1), proto_node("graphene_core::memo::FrameMemoNode", vec![NodeId(0)])),
(NodeId(2), proto_node("graphene_core::memo::FrameMemoNode", vec![NodeId(1)])),
(NodeId(3), proto_node("graphene_core::debug::CloneNode", vec![NodeId(2)])),
(NodeId(3), proto_node("core_types::record::RecordExtractNode", vec![NodeId(2)])),
],
};
@@ -773,7 +780,7 @@ mod test {
assert_eq!(
(&executor).execute(()).unwrap(),
GPoll::Final(TaggedValue::String("memoized".to_string())),
"the frame memo lend path must replay across evaluations"
"the frame memo must replay across evaluations"
);
}
}

View File

@@ -1,8 +1,6 @@
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::application_io::PlatformEditorApi;
use graph_craft::document::DocumentNode;
use graph_craft::document::value::RenderOutput;
use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::gradient::GradientStops;
use graphene_std::list::{AttributeDyn, AttributeValueDyn, List, ListDyn};
#[cfg(target_family = "wasm")]
@@ -12,14 +10,14 @@ use graphene_std::raster::GPU;
use graphene_std::raster::color::Color;
use graphene_std::raster::*;
use graphene_std::raster::{CPU, Raster};
use graphene_std::registry::{ConstructionError, EdgeHandle, ErasedLendNode, ErasedNode, NodeIOTypes, RegistryEntry, lend_edge_type, ref_type};
use graphene_std::registry::{ConstructionError, EdgeHandle, ErasedNode, NodeIOTypes, RegistryEntry};
use graphene_std::render_node::RenderIntermediate;
use graphene_std::runtime::RuntimeHandle;
use graphene_std::transform::Footprint;
use graphene_std::uuid::NodeId;
use graphene_std::vector::Vector;
use graphene_std::{Artboard, Context, Graphic, ProtoNodeIdentifier, SourceId, concrete, fn_type};
use node_registry_macros::{clone_node, convert_node, into_node, lend_node, record_extract_node, record_lift_node};
use node_registry_macros::{convert_node, into_node, record_extract_node, record_lift_node};
use std::collections::HashMap;
#[cfg(feature = "gpu")]
use wgpu_executor::WgpuExecutorHandle;
@@ -114,58 +112,9 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
// ============
// REF ADAPTERS
// ============
lend_node!(()),
clone_node!(()),
lend_node!(RuntimeHandle),
clone_node!(RuntimeHandle),
lend_node!(SourceId),
clone_node!(SourceId),
lend_node!(bool),
clone_node!(bool),
lend_node!(List<Artboard>),
clone_node!(List<Artboard>),
lend_node!(List<Graphic>),
clone_node!(List<Graphic>),
lend_node!(List<Vector>),
clone_node!(List<Vector>),
lend_node!(List<Raster<CPU>>),
clone_node!(List<Raster<CPU>>),
lend_node!(List<Color>),
clone_node!(List<Color>),
lend_node!(Image<Color>),
clone_node!(Image<Color>),
lend_node!(List<GradientStops>),
clone_node!(List<GradientStops>),
lend_node!(List<String>),
clone_node!(List<String>),
lend_node!(List<NodeId>),
clone_node!(List<NodeId>),
lend_node!(List<f64>),
clone_node!(List<f64>),
lend_node!(List<u8>),
clone_node!(List<u8>),
lend_node!(List<bool>),
clone_node!(List<bool>),
lend_node!(List<DAffine2>),
clone_node!(List<DAffine2>),
lend_node!(List<BlendMode>),
clone_node!(List<BlendMode>),
lend_node!(List<graphene_std::vector::style::GradientType>),
clone_node!(List<graphene_std::vector::style::GradientType>),
lend_node!(List<graphene_std::vector::style::GradientSpreadMethod>),
clone_node!(List<graphene_std::vector::style::GradientSpreadMethod>),
lend_node!(AttributeDyn),
clone_node!(AttributeDyn),
lend_node!(AttributeValueDyn),
clone_node!(AttributeValueDyn),
lend_node!(ListDyn),
clone_node!(ListDyn),
#[cfg(target_family = "wasm")]
lend_node!(CanvasHandle),
#[cfg(target_family = "wasm")]
clone_node!(CanvasHandle),
#[cfg(target_family = "wasm")]
lend_node!(f64),
record_lift_node!(f64),
record_extract_node!(f64),
record_lift_node!(()),
@@ -252,129 +201,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
record_lift_node!(wgpu_executor::WgpuPipelineCache),
#[cfg(feature = "gpu")]
record_extract_node!(wgpu_executor::WgpuPipelineCache),
lend_node!(f32),
clone_node!(f32),
lend_node!(u32),
clone_node!(u32),
lend_node!(u64),
clone_node!(u64),
lend_node!(DVec2),
clone_node!(DVec2),
lend_node!(String),
clone_node!(String),
lend_node!(DAffine2),
clone_node!(DAffine2),
lend_node!(Footprint),
clone_node!(Footprint),
lend_node!(RenderOutput),
clone_node!(RenderOutput),
lend_node!(std::sync::Arc<PlatformEditorApi>),
clone_node!(std::sync::Arc<PlatformEditorApi>),
#[cfg(feature = "gpu")]
lend_node!(List<Raster<GPU>>),
#[cfg(feature = "gpu")]
clone_node!(List<Raster<GPU>>),
#[cfg(feature = "gpu")]
lend_node!(Option<f64>),
clone_node!(Option<f64>),
lend_node!(Option<Color>),
clone_node!(Option<Color>),
lend_node!(Graphic),
clone_node!(Graphic),
lend_node!(glam::f32::Vec2),
clone_node!(glam::f32::Vec2),
lend_node!(glam::f32::Affine2),
clone_node!(glam::f32::Affine2),
lend_node!(graphene_std::vector::style::Stroke),
clone_node!(graphene_std::vector::style::Stroke),
lend_node!(graphene_std::text::Font),
clone_node!(graphene_std::text::Font),
lend_node!(List<BrushStroke>),
clone_node!(List<BrushStroke>),
lend_node!(DocumentNode),
clone_node!(DocumentNode),
lend_node!(graphene_std::ContextModification),
clone_node!(graphene_std::ContextModification),
lend_node!(graphene_std::transform::Footprint),
clone_node!(graphene_std::transform::Footprint),
lend_node!(Box<graphene_std::vector::VectorModification>),
clone_node!(Box<graphene_std::vector::VectorModification>),
lend_node!(graphene_std::blending::BlendMode),
clone_node!(graphene_std::blending::BlendMode),
lend_node!(graphene_std::raster::LuminanceCalculation),
clone_node!(graphene_std::raster::LuminanceCalculation),
lend_node!(graphene_std::vector::QRCodeErrorCorrectionLevel),
clone_node!(graphene_std::vector::QRCodeErrorCorrectionLevel),
lend_node!(graphene_std::extract_xy::XY),
clone_node!(graphene_std::extract_xy::XY),
lend_node!(graphene_std::text_nodes::StringCapitalization),
clone_node!(graphene_std::text_nodes::StringCapitalization),
lend_node!(graphene_std::raster::RedGreenBlue),
clone_node!(graphene_std::raster::RedGreenBlue),
lend_node!(graphene_std::raster::RedGreenBlueAlpha),
clone_node!(graphene_std::raster::RedGreenBlueAlpha),
lend_node!(graphene_std::animation::RealTimeMode),
clone_node!(graphene_std::animation::RealTimeMode),
lend_node!(graphene_std::raster::NoiseType),
clone_node!(graphene_std::raster::NoiseType),
lend_node!(graphene_std::raster::FractalType),
clone_node!(graphene_std::raster::FractalType),
lend_node!(graphene_std::raster::CellularDistanceFunction),
clone_node!(graphene_std::raster::CellularDistanceFunction),
lend_node!(graphene_std::raster::CellularReturnType),
clone_node!(graphene_std::raster::CellularReturnType),
lend_node!(graphene_std::raster::DomainWarpType),
clone_node!(graphene_std::raster::DomainWarpType),
lend_node!(graphene_std::raster::RelativeAbsolute),
clone_node!(graphene_std::raster::RelativeAbsolute),
lend_node!(graphene_std::raster::SelectiveColorChoice),
clone_node!(graphene_std::raster::SelectiveColorChoice),
lend_node!(graphene_std::vector::misc::GridType),
clone_node!(graphene_std::vector::misc::GridType),
lend_node!(graphene_std::vector::misc::ArcType),
clone_node!(graphene_std::vector::misc::ArcType),
lend_node!(graphene_std::vector::misc::RowsOrColumns),
clone_node!(graphene_std::vector::misc::RowsOrColumns),
lend_node!(graphene_std::vector::misc::MergeByDistanceAlgorithm),
clone_node!(graphene_std::vector::misc::MergeByDistanceAlgorithm),
lend_node!(graphene_std::vector::misc::ExtrudeJoiningAlgorithm),
clone_node!(graphene_std::vector::misc::ExtrudeJoiningAlgorithm),
lend_node!(graphene_std::vector::misc::PointSpacingType),
clone_node!(graphene_std::vector::misc::PointSpacingType),
lend_node!(graphene_std::vector::style::StrokeCap),
clone_node!(graphene_std::vector::style::StrokeCap),
lend_node!(graphene_std::vector::style::StrokeJoin),
clone_node!(graphene_std::vector::style::StrokeJoin),
lend_node!(graphene_std::vector::style::StrokeAlign),
clone_node!(graphene_std::vector::style::StrokeAlign),
lend_node!(graphene_std::vector::style::PaintOrder),
clone_node!(graphene_std::vector::style::PaintOrder),
lend_node!(graphene_std::vector::style::GradientType),
clone_node!(graphene_std::vector::style::GradientType),
lend_node!(graphene_std::vector::style::GradientSpreadMethod),
clone_node!(graphene_std::vector::style::GradientSpreadMethod),
lend_node!(Option<DAffine2>),
clone_node!(Option<DAffine2>),
lend_node!(graphene_std::transform::ReferencePoint),
clone_node!(graphene_std::transform::ReferencePoint),
lend_node!(graphene_std::vector::misc::CentroidType),
clone_node!(graphene_std::vector::misc::CentroidType),
lend_node!(graphene_std::vector::misc::BooleanOperation),
clone_node!(graphene_std::vector::misc::BooleanOperation),
lend_node!(graphene_std::text::TextAlign),
clone_node!(graphene_std::text::TextAlign),
lend_node!(graphene_std::transform::ScaleType),
clone_node!(graphene_std::transform::ScaleType),
lend_node!(graphene_std::vector::misc::InterpolationDistribution),
clone_node!(graphene_std::vector::misc::InterpolationDistribution),
lend_node!(RenderIntermediate),
clone_node!(RenderIntermediate),
lend_node!(wgpu_executor::WgpuExecutorHandle),
clone_node!(wgpu_executor::WgpuExecutorHandle),
lend_node!(Option<wgpu_executor::WgpuExecutorHandle>),
clone_node!(Option<wgpu_executor::WgpuExecutorHandle>),
lend_node!(wgpu_executor::WgpuPipelineCache),
clone_node!(wgpu_executor::WgpuPipelineCache),
];
// =============
// CONVERT NODES
@@ -545,25 +371,6 @@ mod node_registry_macros {
};
}
macro_rules! lend_node {
($type:ty) => {
(
ProtoNodeIdentifier::new("graphene_core::memo::LendNode"),
RegistryEntry {
io: NodeIOTypes::new(concrete!(Context), ref_type::<$type>(), vec![fn_type!(Context, $type)]),
constructor: |inputs| {
if inputs.len() != 1 {
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
}
let mut inputs = inputs.into_iter();
let node = graphene_core::memo::LendNode::new(inputs.next().unwrap().downcast::<$type>()?);
Ok(EdgeHandle::new_ref(std::sync::Arc::new(node) as std::sync::Arc<ErasedLendNode<$type>>))
},
},
)
};
}
macro_rules! record_lift_node {
($type:ty) => {
(
@@ -604,29 +411,8 @@ mod node_registry_macros {
};
}
macro_rules! clone_node {
($type:ty) => {
(
ProtoNodeIdentifier::new("graphene_core::debug::CloneNode"),
RegistryEntry {
io: NodeIOTypes::new(concrete!(Context), concrete!($type), vec![lend_edge_type::<$type>()]),
constructor: |inputs| {
if inputs.len() != 1 {
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
}
let mut inputs = inputs.into_iter();
let node = graphene_core::debug::CloneNode::new(inputs.next().unwrap().downcast_lend::<$type>()?);
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<$type>>))
},
},
)
};
}
pub(crate) use clone_node;
pub(crate) use convert_node;
pub(crate) use into_node;
pub(crate) use lend_node;
pub(crate) use record_extract_node;
pub(crate) use record_lift_node;
}

View File

@@ -905,111 +905,6 @@ where
}
}
/// Lifts a lending producer onto a record wire: a parked element carries the
/// lent reference directly, a byte-carried one copies out of the borrow.
pub struct RecordLiftLend<El, N> {
edge: N,
layout: Layout,
_marker: std::marker::PhantomData<fn() -> El>,
}
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_write::<El>(), &[]),
_marker: std::marker::PhantomData,
}
}
}
impl<'e, C, El, N> Node<C> for RecordLiftLend<El, N>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
El: Send + Sync + 'static,
N: Node<C, Output = &'e El>,
{
type Output = RecordValue<'e>;
fn eval(&self, input: &C) -> GPoll<RecordValue<'e>> {
let build = |element: &'e El| {
let write = |dst: *mut u8| match element_parked::<El>() {
true => unsafe { dst.cast::<&El>().write(element) },
false => unsafe { std::ptr::copy_nonoverlapping((element as *const El).cast::<u8>(), dst, size_of::<El>()) },
};
if self.layout.is_inline() {
let mut value = RecordValue::zeroed();
write(value.as_mut_ptr());
value
} else {
let dst = stack::push(self.layout.frame_bytes());
write(dst);
stack::pop(dst);
RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) })
}
};
self.edge.eval(input).map(build)
}
fn layout(&self) -> Option<&Layout> {
Some(&self.layout)
}
}
/// Lends a record wire's element: a parked element lends its arena-backed
/// reference directly, a byte-carried one parks a copy so the borrow
/// outlives the record.
pub struct RecordExtractLend<El, N> {
edge: N,
layout: Layout,
_marker: std::marker::PhantomData<fn() -> El>,
}
impl<El, N> RecordExtractLend<El, N> {
pub fn new(edge: N, layout: &Layout) -> Self {
Self {
edge,
layout: layout.clone(),
_marker: std::marker::PhantomData,
}
}
}
impl<'e, C, El, N> Node<C> for RecordExtractLend<El, N>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
El: Clone + Send + Sync + 'static,
N: Node<C, Output = RecordValue<'e>>,
{
type Output = &'e El;
fn eval(&self, input: &C) -> GPoll<&'e El> {
let exhausted = || {
GPoll::Error(Box::new(crate::gpoll::GraphError {
kind: crate::gpoll::ErrorKind::ArenaExhausted,
trace: Vec::new(),
}))
};
let lend = |value: RecordValue<'e>| {
let rec = self.layout.rec(&value);
match element_parked::<El>() {
true => Some(unsafe { borrow_element::<El>(rec) }),
false => input.arena().alloc(unsafe { read_element::<El>(rec) }).map(|(parked, _)| parked),
}
};
match self.edge.eval(input) {
GPoll::Final(value) => lend(value).map_or_else(exhausted, GPoll::Final),
GPoll::Partial(value) => lend(value).map_or_else(exhausted, GPoll::Partial),
GPoll::Fallback(boxed) => {
let (value, error) = *boxed;
lend(value).map_or_else(exhausted, |element| GPoll::Fallback(Box::new((element, error))))
}
GPoll::Pending => GPoll::Pending,
GPoll::Error(error) => GPoll::Error(error),
}
}
}
/// Extracts the element from a record wire for a plain consumer, cloning out
/// of the parked reference when the element carries drop glue.
pub struct RecordExtract<El, N> {

View File

@@ -290,15 +290,13 @@ pub struct RegistryEntry {
pub constructor: NodeConstructor,
}
/// The four bridge rows of `T`: plain and lend producers onto record wires,
/// record wires into plain and lend consumers. One set exists per wire type
/// while the worlds coexist.
pub fn record_bridge_rows<T: Clone + Send + Sync + 'static>() -> [(crate::ProtoNodeIdentifier, RegistryEntry); 4] {
/// The bridge rows of `T`: a plain producer onto a record wire and a record
/// wire into a plain consumer. One pair exists per wire type while the
/// deferred plain classes (shader, batch) coexist with record wires.
pub fn record_bridge_rows<T: Clone + Send + Sync + 'static>() -> [(crate::ProtoNodeIdentifier, RegistryEntry); 2] {
[
(crate::ProtoNodeIdentifier::new("core_types::record::RecordLiftNode"), record_lift_entry::<T>()),
(crate::ProtoNodeIdentifier::new("core_types::record::RecordExtractNode"), record_extract_entry::<T>()),
(crate::ProtoNodeIdentifier::new("core_types::record::RecordLiftLendNode"), record_lift_lend_entry::<T>()),
(crate::ProtoNodeIdentifier::new("core_types::record::RecordExtractLendNode"), record_extract_lend_entry::<T>()),
]
}
@@ -335,38 +333,6 @@ pub fn record_extract_entry<T: Clone + Send + Sync + 'static>() -> RegistryEntry
}
}
/// The lend-lift bridge row for `T`: a lending producer onto a record wire.
pub fn record_lift_lend_entry<T: Clone + Send + Sync + 'static>() -> RegistryEntry {
RegistryEntry {
io: NodeIOTypes::new(concrete!(Context), record_type::<T>(), vec![lend_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 node = crate::record::RecordLiftLend::<T, _>::new(inputs.next().unwrap().downcast_lend::<T>()?);
Ok(EdgeHandle::new_record::<T>(std::sync::Arc::new(node) as std::sync::Arc<ErasedRecordNode>))
},
}
}
/// The lend-extract bridge row for `T`: a record wire into a lend consumer.
pub fn record_extract_lend_entry<T: Clone + Send + Sync + 'static>() -> RegistryEntry {
RegistryEntry {
io: NodeIOTypes::new(concrete!(Context), ref_type::<T>(), vec![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 edge = inputs.next().unwrap();
let layout = edge.layout().ok_or(ConstructionError::MissingLayout)?.clone();
let node = crate::record::RecordExtractLend::<T, _>::new(edge.downcast_record::<T>()?, &layout);
Ok(EdgeHandle::new_ref(std::sync::Arc::new(node) as std::sync::Arc<ErasedLendNode<T>>))
},
}
}
pub fn construct(entry: &RegistryEntry, inputs: Vec<EdgeHandle>) -> Result<EdgeHandle, ConstructionError> {
if inputs.len() != entry.io.inputs.len() {
return Err(ConstructionError::Arity {

View File

@@ -780,27 +780,6 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
]);
}
let has_lend = parsed.fields.iter().any(|field| matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. })));
let declared_arena_lifetime = ctx_param.and_then(|ctx_param| {
ctx_param.bounds.iter().find_map(|bound| {
let TypeParamBound::Trait(trait_bound) = bound else { return None };
let segment = trait_bound.path.segments.last()?;
if segment.ident != "ExtractArena" {
return None;
}
let PathArguments::AngleBracketed(args) = &segment.arguments else { return None };
match args.args.first() {
Some(GenericArgument::Lifetime(lifetime)) => Some(lifetime.clone()),
_ => None,
}
})
});
let introduced_lend_lifetime = (has_lend && !flip && declared_arena_lifetime.is_none()).then(|| Lifetime::new("'__lend", proc_macro2::Span::call_site()));
let lend_lifetime = declared_arena_lifetime.or_else(|| introduced_lend_lifetime.clone());
if let Some(lifetime) = &introduced_lend_lifetime {
ctx_bounds.push(quote!(#core_types::context::ExtractArena<ArenaRef = &#lifetime #core_types::arena::Arena>));
}
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"),
@@ -850,10 +829,6 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
generics.push(ctx_generic.clone());
impl_generics.push(ctx_generic);
}
if let Some(lifetime) = &introduced_lend_lifetime {
generics.insert(0, quote!(#lifetime));
impl_generics.insert(0, quote!(#lifetime));
}
if routing.is_some() || record.is_some() || flip {
impl_generics.insert(0, quote!('__record));
}
@@ -1024,10 +999,6 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
},
false => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>),
},
ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => {
let lifetime = lend_lifetime.as_ref().expect("lend fields imply the lend lifetime");
quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = &#lifetime #ty>)
}
ParsedFieldType::Regular(_) if record.is_some() && !skips_carrier && index == 0 => {
quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>)
}
@@ -1051,16 +1022,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
}
});
let mut lend_outlives: Vec<TokenStream2> = regular_fields
.iter()
.filter_map(|field| match &field.ty {
ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) if !flip => {
let lifetime = lend_lifetime.as_ref().expect("lend fields imply the lend lifetime");
Some(quote!(#ty: #lifetime))
}
_ => None,
})
.collect();
let mut lend_outlives: Vec<TokenStream2> = Vec::new();
if let Type::Reference(reference) = &trait_output
&& let Some(lifetime) = &reference.lifetime
{
@@ -1175,6 +1137,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
ParsedFieldType::Node(_) if flip && raw_lazy => quote!(&#name),
ParsedFieldType::Node(NodeParsedField { output_type, .. }) if opaque && raw_lazy && is_record_value(output_type) => quote!(&#name),
ParsedFieldType::Node(_) if raw_lazy => quote!(&self.#name),
// A lend param binds an owned edge; the kernel borrows the
// evaluated value.
ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. }) if !flip => quote!(&#name),
_ => quote!(#name),
}
});
@@ -1367,7 +1332,12 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
.into_iter();
let value_args = regular_fields.iter().skip(if shape.skips_carrier() { 0 } else { 1 }).map(|field| {
let name = &field.pat_ident.ident;
quote!(#name)
match &field.ty {
// A lend param binds an owned edge; the kernel borrows the
// evaluated value.
ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. }) => quote!(&#name),
_ => quote!(#name),
}
});
let attr_args = parsed.attribute_reads.iter().map(|read| {
let pat = &read.pat_ident.ident;
@@ -2615,7 +2585,7 @@ fn record_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fie
let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect();
let input_types = regular_fields.iter().enumerate().map(|(index, field)| {
let ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) = &field.ty else {
let ParsedFieldType::Regular(RegularParsedField { ty, .. }) = &field.ty else {
unreachable!("record nodes take no lazy inputs")
};
if carrier_in_fields && index == 0 {
@@ -2628,14 +2598,11 @@ fn record_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fie
RecordCarrier::None => unreachable!(),
};
}
match lend.is_some() {
true => quote!(gcore::registry::lend_edge_type::<#ty>()),
false => quote!(gcore::registry::edge_type::<#ty>()),
}
quote!(gcore::registry::edge_type::<#ty>())
});
let downcasts = regular_fields.iter().enumerate().map(|(index, field)| {
let name = &field.pat_ident.ident;
let ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) = &field.ty else {
let ParsedFieldType::Regular(RegularParsedField { ty, .. }) = &field.ty else {
unreachable!("record nodes take no lazy inputs")
};
if carrier_in_fields && index == 0 {
@@ -2648,10 +2615,7 @@ fn record_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fie
let #name = __carrier_handle.downcast_erased::<gcore::registry::ErasedRecordNode>(__carrier_ty.clone())?;
};
}
match lend.is_some() {
true => quote!(let #name = inputs.next().unwrap().downcast_lend::<#ty>()?;),
false => quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;),
}
quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;)
});
let wire_layout_arg = carrier_in_fields.then(|| quote!(&__carrier_layout,)).into_iter();
let (io_output, construct_output) = match (&shape.carrier, &shape.element_write) {

View File

@@ -29,9 +29,8 @@ fn unwrap_option<T: Default>(_: impl Ctx, #[implementations(Option<f64>, Option<
input.unwrap_or_default()
}
/// Clones the value borrowed from a lending edge. Doubles as the checker-inserted clone-out
/// adapter, so it keeps the plain lowering while the record transition runs.
#[node_macro::node(category("Debug"), plain)]
/// Clones the element out of its record wire.
#[node_macro::node(category("Debug"))]
fn clone<T: Clone>(_: impl Ctx, #[implementations(List<Raster<CPU>>)] value: &T) -> T {
value.clone()
}

View File

@@ -1,4 +1,4 @@
use core_types::arena::{Arena, ArenaCell};
use core_types::arena::ArenaCell;
use core_types::context::{Ctx, CtxSnapshot, DeriveCtx, ExtractAll, ExtractArena};
use core_types::frame_table::{FrameTable, Lookup};
use core_types::gpoll::{Extent, Finality, GPoll};
@@ -102,34 +102,6 @@ where
node.content.extent(ctx)
}
pub fn park<T: Send + Sync>(arena: &Arena, result: GPoll<T>) -> GPoll<&T> {
match result {
GPoll::Final(value) => match arena.alloc(value) {
Some((parked, _)) => GPoll::Final(parked),
None => GPoll::arena_exhausted(),
},
GPoll::Partial(value) => match arena.alloc(value) {
Some((parked, _)) => GPoll::Partial(parked),
None => GPoll::arena_exhausted(),
},
GPoll::Fallback(boxed) => {
let (value, error) = *boxed;
match arena.alloc(value) {
Some((parked, _)) => GPoll::Fallback(Box::new((parked, error))),
None => GPoll::arena_exhausted(),
}
}
GPoll::Pending => GPoll::Pending,
GPoll::Error(error) => GPoll::Error(error),
}
}
/// Adapts an owned edge to a lending one by parking each result in the eval arena.
#[node_macro::node(category(""), path(graphene_core::memo), skip_impl)]
fn lend<'e, T: Send + Sync>(ctx: impl Ctx + ExtractArena<'e>, value: T) -> GPoll<&'e T> {
park(ctx.arena(), GPoll::Final(value))
}
type MonitorValue = Arc<Mutex<Option<IORecord<CtxSnapshot, RecordCapture>>>>;
/// The Monitor node is used by the editor to access the data flowing through it.
@@ -156,6 +128,7 @@ fn serialize_monitor(io: &MonitorValue) -> Option<Arc<dyn std::any::Any + Send +
mod tests {
use super::*;
use core_types::SourceId;
use core_types::arena::Arena;
use core_types::context::{ContextImpl, EvalScope};
use core_types::registry::{EdgeHandle, ErasedLendNode, ErasedNode, ErasedRecordNode};
use std::sync::atomic::{AtomicU32, Ordering};

View File

@@ -81,7 +81,6 @@ fn forward_record<T>(_: impl Ctx, element: T) -> T {
element
}
#[cfg(test)]
mod tests {
use super::*;
@@ -180,11 +179,7 @@ mod tests {
let stacked = multiply_opacity_layout(&modified);
reserve_for(&[&source_layout, &modified, &stacked]);
let chain = MultiplyOpacityNode::new(
MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout),
ValueNode(0.5),
&modified,
);
let chain = MultiplyOpacityNode::new(MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout), ValueNode(0.5), &modified);
assert_eq!(chain.layout(), Some(&stacked));
let GPoll::Final(value) = chain.eval(&ctx) else {
panic!("expected a final record");
@@ -372,18 +367,6 @@ mod tests {
assert!(error.kind == "negative factor");
}
static FACTOR: f64 = 3.;
struct StaticLendNode(&'static f64);
impl<'e> Node<ContextImpl<'e>> for StaticLendNode {
type Output = &'e f64;
fn eval(&self, _input: &ContextImpl<'e>) -> GPoll<&'e f64> {
GPoll::Final(self.0)
}
}
#[test]
fn lend_value_params_wire_into_record_kernels() {
let arena = Arena::new(1024).unwrap();
@@ -396,11 +379,7 @@ mod tests {
let scaled = scale_layout(&modified);
reserve_for(&[&source_layout, &modified, &scaled]);
let chain = ScaleNode::new(
MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout),
StaticLendNode(&FACTOR),
&modified,
);
let chain = ScaleNode::new(MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout), ValueNode(3.), &modified);
let GPoll::Final(value) = chain.eval(&ctx) else {
panic!("expected a final record");
};
@@ -585,11 +564,7 @@ mod tests {
reserve_for(&[&layout]);
let probed = |features: ContextFeatures| {
let node = crate::context_modification::ContextModificationNode::new(
RealTimeProbe { layout: layout.clone() },
ValueNode(ContextModification::from_sources(features, &[])),
&layout,
);
let node = crate::context_modification::ContextModificationNode::new(RealTimeProbe { layout: layout.clone() }, ValueNode(ContextModification::from_sources(features, &[])), &layout);
assert_eq!(Node::<ContextImpl>::layout(&node), Some(&layout));
let GPoll::Final(value) = node.eval(&ctx) else {
panic!("expected a final record");