Delete the plain edge kind

This commit is contained in:
Dennis Kobert
2026-08-29 10:07:10 +00:00
parent f3e945c5d8
commit 277641d27e
5 changed files with 63 additions and 132 deletions

View File

@@ -8,7 +8,7 @@ use core_types::context::Context;
use core_types::gpoll::GPoll;
use core_types::list::List;
use core_types::node::Node;
use core_types::registry::{EdgeHandle, edge_type};
use core_types::registry::EdgeHandle;
use core_types::transform::Footprint;
use core_types::uuid::NodeId;
use core_types::value::{leveled_record_value_edge, record_value_edge};
@@ -310,7 +310,7 @@ macro_rules! tagged_value {
}
}
/// Materializes the value as [`Self::to_dynany`] does, wrapped in a `ClonedNode` edge typed by [`Self::ty`].
/// Materializes the value as [`Self::to_dynany`] does, wrapped in a value edge typed by [`Self::ty`].
pub fn to_edge(self) -> Result<EdgeHandle, String> {
match self {
// ===============
@@ -362,26 +362,6 @@ macro_rules! tagged_value {
/// Evaluates a typed edge and converts the landed value into a tagged value, with the coverage of [`Self::try_from_any`].
pub fn from_edge(handle: EdgeHandle, ctx: &Context) -> Result<GPoll<Self>, String> {
let ty = handle.ty().clone();
// ===============
// MANUAL VARIANTS
// ===============
if ty == edge_type::<()>() {
return Ok(handle.downcast::<()>().map_err(|e| format!("{e:?}"))?.eval(ctx).map(|_| TaggedValue::None));
}
// =======================
// AUTO-GENERATED VARIANTS
// =======================
$(
if ty == edge_type::<$ty>() {
return Ok(handle.downcast::<$ty>().map_err(|e| format!("{e:?}"))?.eval(ctx).map(TaggedValue::$identifier));
}
)*
// =======================
// NON-SERIALIZED VARIANTS
// =======================
if ty == edge_type::<RenderOutput>() {
return Ok(handle.downcast::<RenderOutput>().map_err(|e| format!("{e:?}"))?.eval(ctx).map(TaggedValue::RenderOutput));
}
// =======================
// RECORD WIRES, WHICH LAND AS THEIR ELEMENT
// =======================

View File

@@ -2,8 +2,7 @@ use crate::node_registry;
use core_types::arena::Arena;
use core_types::context::{ContextImpl, DynSlot, EvalScope, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, VarArg, VarArgLink, VarArgSlots};
use core_types::gpoll::GPoll;
use core_types::node::Node;
use core_types::registry::{EdgeHandle, ErasedNode};
use core_types::registry::EdgeHandle;
use core_types::runtime::{DynGraphRuntime, DynSpawner, GraphRuntime, NoopSpawner};
use graph_craft::Type;
use graph_craft::document::NodeId;
@@ -386,16 +385,6 @@ impl BorrowTree {
self.get(*id)
}
/// Evaluate a node of the [`BorrowTree`], downcasting its edge to the expected output type.
pub fn eval<I, T: 'static>(&self, id: NodeId, input: &I) -> Option<GPoll<T>>
where
ErasedNode<T>: Node<I, Output = T>,
{
let (node, _path) = self.nodes.get(&id)?;
let edge = node.duplicate().downcast::<T>().ok()?;
Some(edge.eval(input))
}
/// Removes a node from the [`BorrowTree`] and returns its associated path.
///
/// This method removes the specified node from both the `nodes` HashMap and,

View File

@@ -72,11 +72,6 @@ pub static NODE_METADATA: LazyLock<Mutex<HashMap<ProtoNodeIdentifier, NodeMetada
pub use crate::NodeIOTypes;
#[cfg(not(target_family = "wasm"))]
pub type ErasedNode<T> = dyn for<'c> Node<ContextImpl<'c>, Output = T> + Send + Sync;
#[cfg(target_family = "wasm")]
pub type ErasedNode<T> = dyn for<'c> Node<ContextImpl<'c>, Output = T>;
/// Element-independent by erasure; the wire's `Type::Record(El)` keeps element reads proven at wiring.
#[cfg(not(target_family = "wasm"))]
pub type ErasedRecordNode = dyn for<'c> Node<ContextImpl<'c>, Output = crate::record::RecordValue<'c>> + Send + Sync;
@@ -88,10 +83,6 @@ type DynEdge = dyn std::any::Any + Send + Sync;
#[cfg(target_family = "wasm")]
type DynEdge = dyn std::any::Any;
pub fn edge_type<T: 'static>() -> Type {
Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(T)))
}
pub fn record_type<T: 'static>() -> Type {
Type::Record(Box::new(concrete!(T)))
}
@@ -241,10 +232,6 @@ unsafe impl Send for EdgeHandle {}
unsafe impl Sync for EdgeHandle {}
impl EdgeHandle {
pub fn new<T: 'static>(node: std::sync::Arc<ErasedNode<T>>) -> Self {
Self::new_erased(node, edge_type::<T>())
}
pub fn new_record<T: 'static>(node: std::sync::Arc<ErasedRecordNode>) -> Self {
Self::new_erased(node, record_edge_type::<T>())
}
@@ -295,16 +282,12 @@ impl EdgeHandle {
(self.set_layout)(&mut *self.node, layout);
}
pub fn downcast<T: 'static>(self) -> Result<SharedEdge<ErasedNode<T>>, ConstructionError> {
self.downcast_erased(edge_type::<T>())
}
pub fn downcast_record<T: 'static>(self) -> Result<SharedEdge<ErasedRecordNode>, ConstructionError> {
self.downcast_erased(record_edge_type::<T>())
}
/// The erased record edge, for callers that dispatch on the layout rather
/// than a static element type. `None` for a plain (non-record) edge.
/// than a static element type. `None` for an edge erased to another node type.
pub fn record_edge(self) -> Option<SharedEdge<ErasedRecordNode>> {
self.node.downcast::<SharedEdge<ErasedRecordNode>>().ok().map(|edge| *edge)
}
@@ -373,16 +356,6 @@ mod tests {
}
}
struct ValueNode<T>(T);
impl<T: Clone, Input> Node<Input> for ValueNode<T> {
type Output = T;
fn eval(&self, _input: &Input) -> GPoll<T> {
GPoll::Final(self.0.clone())
}
}
struct LendNode(String);
impl<'e, Input: Ctx + ExtractArena<ArenaRef = &'e Arena>> Node<Input> for LendNode {
@@ -485,14 +458,21 @@ mod tests {
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let nested = RepeatNode {
let nested = crate::record::RecordLift::<Vec<Vec<Vec<usize>>>, _>::new(RepeatNode {
content: RepeatNode { content: LevelsNode },
};
let erased: Box<ErasedNode<Vec<Vec<Vec<usize>>>>> = Box::new(nested);
});
let layout = Node::<ContextImpl>::layout(&nested).clone();
let erased: Box<ErasedRecordNode> = Box::new(nested);
// SAFETY: between evaluations, nothing served on the stack is live.
unsafe {
crate::record::stack::reserve(1 << 12);
}
let GPoll::Final(outer) = erased.eval(&ctx) else {
let GPoll::Final(value) = erased.eval(&ctx) else {
panic!("nested repeat must evaluate");
};
// SAFETY: the record was served at `layout`, whose element is the output.
let outer = unsafe { crate::record::read_element::<Vec<Vec<Vec<usize>>>>(layout.rec(&value)) };
assert_eq!(outer.len(), 3);
assert_eq!(outer[2][1], vec![1, 2, 0]);
assert_eq!(outer[0][0], vec![0, 0, 0]);
@@ -537,9 +517,9 @@ mod tests {
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let graph: Box<ErasedNode<u32>> = Box::new(ShiftFootprintNode {
let graph = ShiftFootprintNode {
content: ShiftFootprintNode { content: ResolutionNode },
});
};
assert_eq!(graph.eval(&ctx), GPoll::Final(Footprint::DEFAULT.resolution.x + 14));
}
@@ -547,27 +527,27 @@ mod tests {
fn construct_checks_arity_and_types() {
fn construct_strlen(args: Vec<EdgeHandle>) -> Result<EdgeHandle, ConstructionError> {
let mut args = args.into_iter();
let value = args.next().ok_or(ConstructionError::Arity { expected: 1, got: 0 })?.downcast::<String>()?;
let value = args.next().ok_or(ConstructionError::Arity { expected: 1, got: 0 })?.downcast_record::<String>()?;
drop(value);
Ok(EdgeHandle::new(Arc::new(ValueNode(0u32)) as Arc<ErasedNode<u32>>))
Ok(crate::value::record_value_edge(0u32))
}
let entry = RegistryEntry {
layout_meta: None,
io: NodeIOTypes::new(concrete!(Context), concrete!(u32), vec![edge_type::<String>()]),
io: NodeIOTypes::new(concrete!(Context), record_type::<u32>(), vec![record_edge_type::<String>()]),
constructor: construct_strlen,
};
let owned = EdgeHandle::new(Arc::new(ValueNode("typed".to_string())) as Arc<ErasedNode<String>>);
let owned = crate::value::record_value_edge("typed".to_string());
assert!(construct(&entry, vec![owned]).is_ok());
assert_eq!(construct(&entry, vec![]).unwrap_err(), ConstructionError::Arity { expected: 1, got: 0 });
let mistyped = EdgeHandle::new(Arc::new(ValueNode(1.0f64)) as Arc<ErasedNode<f64>>);
let mistyped = crate::value::record_value_edge(1.0f64);
assert_eq!(
construct(&entry, vec![mistyped]).unwrap_err(),
ConstructionError::Type {
expected: Box::new(edge_type::<String>()),
found: Box::new(edge_type::<f64>()),
expected: Box::new(record_edge_type::<String>()),
found: Box::new(record_edge_type::<f64>()),
}
);
}
@@ -579,16 +559,24 @@ mod tests {
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let handle = EdgeHandle::new(Arc::new(CountingNode(AtomicU32::new(0))) as Arc<ErasedNode<u32>>);
let counting = crate::record::RecordLift::<u32, _>::new(CountingNode(AtomicU32::new(0)));
let layout = Node::<ContextImpl>::layout(&counting).clone();
let handle = EdgeHandle::new_record::<u32>(Arc::new(counting) as Arc<ErasedRecordNode>);
let duplicate = handle.duplicate();
assert_eq!(*duplicate.ty(), edge_type::<u32>());
assert_eq!(*duplicate.ty(), record_edge_type::<u32>());
// SAFETY: between evaluations, nothing served on the stack is live.
unsafe {
crate::record::stack::reserve(1 << 12);
}
let first = handle.downcast::<u32>().unwrap();
let second = duplicate.downcast::<u32>().unwrap();
assert_eq!(first.eval(&ctx), GPoll::Final(1));
assert_eq!(second.eval(&ctx), GPoll::Final(2));
let first = handle.downcast_record::<u32>().unwrap();
let second = duplicate.downcast_record::<u32>().unwrap();
// SAFETY: each record was served at `layout`, whose element is the count.
let count = |value| unsafe { layout.rec(&value).element::<u32>() };
assert_eq!(first.eval(&ctx).map(count), GPoll::Final(1));
assert_eq!(second.eval(&ctx).map(count), GPoll::Final(2));
drop(first);
assert_eq!(second.eval(&ctx), GPoll::Final(3));
assert_eq!(second.eval(&ctx).map(count), GPoll::Final(3));
}
}

View File

@@ -1,18 +1,3 @@
#[derive(Clone, Copy)]
pub struct ClonedNode<T: Clone>(pub T);
impl<T: Clone, Input> crate::node::Node<Input> for ClonedNode<T> {
type Output = T;
fn eval(&self, _input: &Input) -> crate::gpoll::GPoll<T> {
crate::gpoll::GPoll::Final(self.0.clone())
}
}
pub fn value_edge<T: Clone + crate::WasmNotSend + crate::WasmNotSync + 'static>(value: T) -> crate::registry::EdgeHandle {
crate::registry::EdgeHandle::new(std::sync::Arc::new(ClonedNode(value)) as std::sync::Arc<crate::registry::ErasedNode<T>>)
}
/// The node behind every value edge: clones its constant onto the record
/// wire per evaluation.
pub struct ValueSource<T> {
@@ -108,15 +93,3 @@ where
{
crate::registry::EdgeHandle::new_record::<T>(std::sync::Arc::new(LeveledValueSource::new(values)) as std::sync::Arc<crate::registry::ErasedRecordNode>)
}
impl<T: Clone> ClonedNode<T> {
pub const fn new(value: T) -> ClonedNode<T> {
ClonedNode(value)
}
}
impl<T: Clone> From<T> for ClonedNode<T> {
fn from(value: T) -> Self {
ClonedNode::new(value)
}
}

View File

@@ -1,6 +1,8 @@
use super::*;
use proc_macro2::TokenStream as TokenStream2;
use proc_macro_error2::emit_error;
use quote::{format_ident, quote};
use syn::spanned::Spanned;
use syn::{GenericParam, Ident, Type};
pub(crate) fn entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, data_field_generic_idents: &[Ident], regular_fields: &[&ParsedField]) -> TokenStream2 {
@@ -179,7 +181,7 @@ fn flip_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_field
/// Which record wire an input claims and how its value is recovered. Base slots
/// are the record edges whose layouts form the output; value slots are record
/// edges read for their layout; plain and lazy slots are ordinary edges.
/// edges read for their layout.
enum SlotKind {
/// A generic record edge whose element is only known at runtime; the runtime
/// type is captured for the output wrap or the union.
@@ -192,10 +194,6 @@ enum SlotKind {
Extracted(Type),
/// A ranked record edge consumed whole; no layout rides to the constructor.
Ranked(Type),
/// A plain value edge.
Plain(Type),
/// A lazy node edge.
Lazy(Type),
}
impl SlotKind {
@@ -215,18 +213,18 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields
let lend = |field: &ParsedField| matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. }));
// A subject is its record edge (concrete carrier or erased generic); a
// non-subject value rides a record edge when it reads its layout, else plain.
let slots: Vec<SlotKind> = regular_fields
// non-subject value rides a record edge when it reads its layout.
let slots: Option<Vec<SlotKind>> = regular_fields
.iter()
.enumerate()
.map(|(index, field)| {
let input = &node.inputs[index];
if input.subject {
return match &input.shape.element {
return Some(match &input.shape.element {
ir::Element::Concrete(ty) => SlotKind::BaseConcrete(ty.clone()),
ir::Element::Generic(ident) => SlotKind::BaseGeneric(ident.to_string()),
ir::Element::Opaque => SlotKind::BaseGeneric("T".to_string()),
};
});
}
match &field.ty {
// An element-consuming lazy secondary of a record node rides a
@@ -234,20 +232,29 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields
ParsedFieldType::Node(NodeParsedField { output_type, .. })
if matches!(ir::node_kind(&node), ir::NodeKind::RecordIo) && matches!(ir::lazy_binding(&node, index), ir::LazyBinding::Element) =>
{
SlotKind::Value(output_type.clone())
Some(SlotKind::Value(output_type.clone()))
}
ParsedFieldType::Node(_) => {
emit_error!(field.pat_ident.span(), "plain (non-record) io is unsupported: this lazy input needs a record edge");
None
}
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => SlotKind::Lazy(output_type.clone()),
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => match ir::value_binding(&node, index) {
ir::ValueBinding::Materialized => SlotKind::Ranked(ty.clone()),
ir::ValueBinding::ReadingSecondary | ir::ValueBinding::RecordElement => SlotKind::Value(ty.clone()),
ir::ValueBinding::Materialized => Some(SlotKind::Ranked(ty.clone())),
ir::ValueBinding::ReadingSecondary | ir::ValueBinding::RecordElement => Some(SlotKind::Value(ty.clone())),
// One wire kind: a record node's plain value still rides a
// record edge, extracted to its element at construction.
_ if matches!(ir::node_kind(&node), ir::NodeKind::RecordIo) => SlotKind::Extracted(ty.clone()),
_ => SlotKind::Plain(ty.clone()),
_ if matches!(ir::node_kind(&node), ir::NodeKind::RecordIo) => Some(SlotKind::Extracted(ty.clone())),
_ => {
emit_error!(field.pat_ident.span(), "plain (non-record) io is unsupported: this value input needs a record edge");
None
}
},
}
})
.collect();
let Some(slots) = slots else {
return quote!();
};
// A ranked input's element generic monomorphizes the kernel, so its
// implementations expand to one registry row each; every other slot
@@ -319,17 +326,13 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields
SlotKind::Value(ty) => SlotKind::Value(substitute_lifetimes(&substitute_ident_types(ty, assignments), "'static")),
SlotKind::Extracted(ty) => SlotKind::Extracted(substitute_lifetimes(&substitute_ident_types(ty, assignments), "'static")),
SlotKind::Ranked(ty) => SlotKind::Ranked(substitute_lifetimes(&substitute_ident_types(ty, assignments), "'static")),
SlotKind::Plain(ty) => SlotKind::Plain(substitute_lifetimes(&substitute_ident_types(ty, assignments), "'static")),
SlotKind::Lazy(ty) => SlotKind::Lazy(substitute_lifetimes(&substitute_ident_types(ty, assignments), "'static")),
})
.collect();
// Every non-base value/plain/lazy input must be concrete.
// Every non-base value input must be concrete.
let values_concrete = regular_fields.iter().zip(&slots).all(|(field, slot)| match slot {
SlotKind::BaseGeneric(_) | SlotKind::BaseConcrete(_) => true,
SlotKind::Value(ty) | SlotKind::Extracted(ty) | SlotKind::Ranked(ty) | SlotKind::Plain(ty) | SlotKind::Lazy(ty) => {
!contains_open_generic(parsed, ty) && (lend(field) || !type_disqualifies(ty))
}
SlotKind::Value(ty) | SlotKind::Extracted(ty) | SlotKind::Ranked(ty) => !contains_open_generic(parsed, ty) && (lend(field) || !type_disqualifies(ty)),
});
if !values_concrete {
return None;
@@ -338,7 +341,6 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields
let input_types = slots.iter().map(|slot| match slot {
SlotKind::BaseGeneric(name) => quote!(gcore::registry::generic_record_edge_type(#name)),
SlotKind::BaseConcrete(ty) | SlotKind::Value(ty) | SlotKind::Extracted(ty) | SlotKind::Ranked(ty) => quote!(gcore::registry::record_edge_type::<#ty>()),
SlotKind::Plain(ty) | SlotKind::Lazy(ty) => quote!(gcore::registry::edge_type::<#ty>()),
});
let downcasts = names.iter().zip(&slots).enumerate().map(|(index, (name, slot))| {
@@ -365,7 +367,6 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields
SlotKind::Ranked(value_ty) => quote! {
let #name = inputs.next().unwrap().downcast_record::<#value_ty>()?;
},
SlotKind::Plain(value_ty) | SlotKind::Lazy(value_ty) => quote!(let #name = inputs.next().unwrap().downcast::<#value_ty>()?;),
}
});