From 277641d27eba2573505b3a85f357b088d64ce072 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sat, 29 Aug 2026 10:07:10 +0000 Subject: [PATCH] Delete the plain edge kind --- node-graph/graph-craft/src/document/value.rs | 24 +----- .../src/dynamic_executor.rs | 13 +-- .../libraries/core-types/src/registry.rs | 84 ++++++++----------- node-graph/libraries/core-types/src/value.rs | 27 ------ node-graph/node-macro/src/codegen/entries.rs | 47 ++++++----- 5 files changed, 63 insertions(+), 132 deletions(-) diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index d2a6703eff..1b52553805 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -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 { 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, 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::() { - return Ok(handle.downcast::().map_err(|e| format!("{e:?}"))?.eval(ctx).map(TaggedValue::RenderOutput)); - } // ======================= // RECORD WIRES, WHICH LAND AS THEIR ELEMENT // ======================= diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index 58eabe5199..b348637300 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -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(&self, id: NodeId, input: &I) -> Option> - where - ErasedNode: Node, - { - let (node, _path) = self.nodes.get(&id)?; - let edge = node.duplicate().downcast::().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, diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index 35993a7ce0..fa15464863 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -72,11 +72,6 @@ pub static NODE_METADATA: LazyLock = dyn for<'c> Node, Output = T> + Send + Sync; -#[cfg(target_family = "wasm")] -pub type ErasedNode = dyn for<'c> Node, 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, 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() -> Type { - Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(T))) -} - pub fn record_type() -> 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(node: std::sync::Arc>) -> Self { - Self::new_erased(node, edge_type::()) - } - pub fn new_record(node: std::sync::Arc) -> Self { Self::new_erased(node, record_edge_type::()) } @@ -295,16 +282,12 @@ impl EdgeHandle { (self.set_layout)(&mut *self.node, layout); } - pub fn downcast(self) -> Result>, ConstructionError> { - self.downcast_erased(edge_type::()) - } - pub fn downcast_record(self) -> Result, ConstructionError> { self.downcast_erased(record_edge_type::()) } /// 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> { self.node.downcast::>().ok().map(|edge| *edge) } @@ -373,16 +356,6 @@ mod tests { } } - struct ValueNode(T); - - impl Node for ValueNode { - type Output = T; - - fn eval(&self, _input: &Input) -> GPoll { - GPoll::Final(self.0.clone()) - } - } - struct LendNode(String); impl<'e, Input: Ctx + ExtractArena> Node 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::>>, _>::new(RepeatNode { content: RepeatNode { content: LevelsNode }, - }; - let erased: Box>>>> = Box::new(nested); + }); + let layout = Node::::layout(&nested).clone(); + let erased: Box = 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::>>>(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> = 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) -> Result { let mut args = args.into_iter(); - let value = args.next().ok_or(ConstructionError::Arity { expected: 1, got: 0 })?.downcast::()?; + let value = args.next().ok_or(ConstructionError::Arity { expected: 1, got: 0 })?.downcast_record::()?; drop(value); - Ok(EdgeHandle::new(Arc::new(ValueNode(0u32)) as Arc>)) + Ok(crate::value::record_value_edge(0u32)) } let entry = RegistryEntry { layout_meta: None, - io: NodeIOTypes::new(concrete!(Context), concrete!(u32), vec![edge_type::()]), + io: NodeIOTypes::new(concrete!(Context), record_type::(), vec![record_edge_type::()]), constructor: construct_strlen, }; - let owned = EdgeHandle::new(Arc::new(ValueNode("typed".to_string())) as Arc>); + 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>); + let mistyped = crate::value::record_value_edge(1.0f64); assert_eq!( construct(&entry, vec![mistyped]).unwrap_err(), ConstructionError::Type { - expected: Box::new(edge_type::()), - found: Box::new(edge_type::()), + expected: Box::new(record_edge_type::()), + found: Box::new(record_edge_type::()), } ); } @@ -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>); + let counting = crate::record::RecordLift::::new(CountingNode(AtomicU32::new(0))); + let layout = Node::::layout(&counting).clone(); + let handle = EdgeHandle::new_record::(Arc::new(counting) as Arc); let duplicate = handle.duplicate(); - assert_eq!(*duplicate.ty(), edge_type::()); + assert_eq!(*duplicate.ty(), record_edge_type::()); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { + crate::record::stack::reserve(1 << 12); + } - let first = handle.downcast::().unwrap(); - let second = duplicate.downcast::().unwrap(); - assert_eq!(first.eval(&ctx), GPoll::Final(1)); - assert_eq!(second.eval(&ctx), GPoll::Final(2)); + let first = handle.downcast_record::().unwrap(); + let second = duplicate.downcast_record::().unwrap(); + // SAFETY: each record was served at `layout`, whose element is the count. + let count = |value| unsafe { layout.rec(&value).element::() }; + 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)); } } diff --git a/node-graph/libraries/core-types/src/value.rs b/node-graph/libraries/core-types/src/value.rs index 98712adcb5..68fe2a0e30 100644 --- a/node-graph/libraries/core-types/src/value.rs +++ b/node-graph/libraries/core-types/src/value.rs @@ -1,18 +1,3 @@ -#[derive(Clone, Copy)] -pub struct ClonedNode(pub T); - -impl crate::node::Node for ClonedNode { - type Output = T; - - fn eval(&self, _input: &Input) -> crate::gpoll::GPoll { - crate::gpoll::GPoll::Final(self.0.clone()) - } -} - -pub fn value_edge(value: T) -> crate::registry::EdgeHandle { - crate::registry::EdgeHandle::new(std::sync::Arc::new(ClonedNode(value)) as std::sync::Arc>) -} - /// The node behind every value edge: clones its constant onto the record /// wire per evaluation. pub struct ValueSource { @@ -108,15 +93,3 @@ where { crate::registry::EdgeHandle::new_record::(std::sync::Arc::new(LeveledValueSource::new(values)) as std::sync::Arc) } - -impl ClonedNode { - pub const fn new(value: T) -> ClonedNode { - ClonedNode(value) - } -} - -impl From for ClonedNode { - fn from(value: T) -> Self { - ClonedNode::new(value) - } -} diff --git a/node-graph/node-macro/src/codegen/entries.rs b/node-graph/node-macro/src/codegen/entries.rs index 5a30613e19..2cec820114 100644 --- a/node-graph/node-macro/src/codegen/entries.rs +++ b/node-graph/node-macro/src/codegen/entries.rs @@ -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 = regular_fields + // non-subject value rides a record edge when it reads its layout. + let slots: Option> = 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>()?;), } });