This commit is contained in:
Dennis Kobert
2026-07-30 20:48:30 +00:00
parent 07b95ba59b
commit 4556e320df
22 changed files with 813 additions and 1603 deletions

View File

@@ -1,9 +1,9 @@
use crate::node_registry;
use core_types::arena::Arena;
use core_types::context::{ContextImpl, DynSlot, EvalScope, VarArg, VarArgLink, VarArgSlots};
use core_types::gnode::GNode;
use core_types::gpoll::GPoll;
use core_types::registry::{EdgeHandle, ErasedGNode};
use core_types::node::Node;
use core_types::registry::{EdgeHandle, ErasedNode};
use core_types::runtime::{DynGraphRuntime, DynSpawner, GraphRuntime, NoopSpawner};
use graph_craft::Type;
use graph_craft::document::NodeId;
@@ -35,7 +35,6 @@ fn noop_runtime() -> Arc<DynGraphRuntime> {
Arc::new(GraphRuntime::new(Box::new(NoopSpawner) as Box<DynSpawner>))
}
impl Default for DynamicExecutor {
fn default() -> Self {
Self {
@@ -291,7 +290,7 @@ impl BorrowTree {
self.nodes.insert(id, (node, path));
}
/// Calls the `GNode::serialize` for that specific node, returning for example the captured io record for a monitor node. The node path must match the document node path.
/// Calls the `Node::serialize` for that specific node, returning for example the captured io record for a monitor node. The node path must match the document node path.
pub fn introspect(&self, node_path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, IntrospectError> {
let (id, _) = self.source_map.get(node_path).ok_or_else(|| IntrospectError::PathNotFound(node_path.to_vec()))?;
let (node, _path) = self.nodes.get(id).ok_or(IntrospectError::ProtoNodeNotFound(*id))?;
@@ -305,7 +304,7 @@ impl BorrowTree {
/// 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
ErasedGNode<T>: GNode<I, Output = T>,
ErasedNode<T>: Node<I, Output = T>,
{
let (node, _path) = self.nodes.get(&id)?;
let edge = node.duplicate().downcast::<T>().ok()?;

View File

@@ -13,7 +13,7 @@ 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, ErasedGNode, NodeIOTypes, RegistryEntry};
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;
@@ -367,7 +367,7 @@ mod node_registry_macros {
}
let mut inputs = inputs.into_iter();
let node = <$path>::new(inputs.next().unwrap().downcast::<$first>()? $(, inputs.next().unwrap().downcast::<$type>()?)*);
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedGNode<$first>>))
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<$first>>))
},
},
)
@@ -386,7 +386,7 @@ mod node_registry_macros {
}
let mut inputs = inputs.into_iter();
let node = graphene_std::ops::IntoNode::<$to, _>::new(inputs.next().unwrap().downcast::<$from>()?);
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedGNode<$to>>))
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<$to>>))
},
},
)
@@ -454,7 +454,7 @@ mod node_registry_macros {
inputs.next().unwrap().downcast::<RuntimeHandle>()?,
inputs.next().unwrap().downcast::<SourceId>()?,
);
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedGNode<$to>>))
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<$to>>))
},
},
)
@@ -470,7 +470,7 @@ mod node_registry_macros {
}
let mut inputs = inputs.into_iter();
let node = graphene_std::ops::ConvertNode::<$to, _, _>::new(inputs.next().unwrap().downcast::<$from>()?, inputs.next().unwrap().downcast::<$convert>()?);
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedGNode<$to>>))
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<$to>>))
},
},
)

View File

@@ -1,288 +0,0 @@
use std::any::Any;
use std::mem::MaybeUninit;
use std::ops::Add;
use core_types::arena::{Arena, ArenaCell};
use core_types::context::{ContextImpl, Ctx, EvalScope, ExtractArena, InjectIndex};
use core_types::gnode::{BatchStatus, GNode, StatusCell};
use core_types::gpoll::{ErrorKind, Finality, GPoll, Interrupt};
fn add<A: Add<B>, B, C: Ctx>(_ctx: &C, augend: A, addend: B) -> <A as Add<B>>::Output {
augend + addend
}
struct AddNode<Node0, Node1> {
augend: Node0,
addend: Node1,
}
impl<Node0, Node1> AddNode<Node0, Node1> {
fn new(augend: Node0, addend: Node1) -> Self {
Self { augend, addend }
}
}
impl<A, B, Input, Node0, Node1> GNode<Input> for AddNode<Node0, Node1>
where
A: Add<B>,
Input: Ctx,
Node0: GNode<Input, Output = A>,
Node1: GNode<Input, Output = B>,
{
type Output = <A as Add<B>>::Output;
fn eval(&self, input: &Input) -> GPoll<Self::Output> {
let cell = StatusCell::new();
let augend = match cell.eval_input(0, &self.augend, input) {
Ok(value) => value,
Err(interrupt) => return interrupt.into(),
};
let addend = match cell.eval_input(1, &self.addend, input) {
Ok(value) => value,
Err(interrupt) => return interrupt.into(),
};
cell.finish(add(input, augend, addend))
}
}
struct ValueNode<T>(T);
impl<T: Clone, Input> GNode<Input> for ValueNode<T> {
type Output = T;
fn eval(&self, _input: &Input) -> GPoll<T> {
GPoll::Final(self.0.clone())
}
}
struct ReadIndexNode;
impl<Input: InjectIndex + Copy + ExtractIndexValue> GNode<Input> for ReadIndexNode {
type Output = f64;
fn eval(&self, input: &Input) -> GPoll<f64> {
GPoll::Final(input.index_value() as f64)
}
}
trait ExtractIndexValue {
fn index_value(&self) -> u64;
}
impl ExtractIndexValue for ContextImpl<'_> {
fn index_value(&self) -> u64 {
self.index_head().index
}
}
fn string_length<C: Ctx>(_ctx: &C, value: &String) -> f64 {
value.len() as f64
}
struct LendStringNode {
value: String,
cell: ArenaCell<String>,
}
impl LendStringNode {
fn new(value: String) -> Self {
Self { value, cell: ArenaCell::new() }
}
}
impl<'e, Input> GNode<Input> for LendStringNode
where
Input: Ctx + ExtractArena<ArenaRef = &'e Arena>,
{
type Output = &'e String;
fn eval(&self, input: &Input) -> GPoll<&'e String> {
let arena = input.arena();
if let Some(value) = self.cell.load(arena) {
return GPoll::Final(value);
}
match arena.alloc(self.value.clone()) {
Some((value, weak)) => {
self.cell.store(weak);
GPoll::Final(value)
}
None => GPoll::arena_exhausted(),
}
}
}
struct StringLengthNode<Node0> {
value: Node0,
}
impl<Node0> StringLengthNode<Node0> {
fn new(value: Node0) -> Self {
Self { value }
}
}
impl<'e, Input, Node0> GNode<Input> for StringLengthNode<Node0>
where
Input: Ctx,
Node0: GNode<Input, Output = &'e String>,
{
type Output = f64;
fn eval(&self, input: &Input) -> GPoll<f64> {
let cell = StatusCell::new();
let value = match cell.eval_input(0, &self.value, input) {
Ok(value) => value,
Err(interrupt) => return interrupt.into(),
};
cell.finish(string_length(input, value))
}
}
type ErasedGNode<T> = dyn for<'c> GNode<ContextImpl<'c>, Output = T>;
type ErasedLendEdge = dyn for<'c> GNode<ContextImpl<'c>, Output = &'c String>;
fn string_length_constructor(args: Vec<Box<dyn Any>>) -> Result<Box<ErasedGNode<f64>>, &'static str> {
let mut args = args.into_iter();
let value = *args.next().ok_or("arity")?.downcast::<Box<ErasedLendEdge>>().map_err(|_| "type")?;
Ok(Box::new(StringLengthNode::new(value)))
}
fn add_constructor_f64(args: Vec<Box<dyn Any>>) -> Result<Box<ErasedGNode<f64>>, &'static str> {
let mut args = args.into_iter();
let augend = *args.next().ok_or("arity")?.downcast::<Box<ErasedGNode<f64>>>().map_err(|_| "type")?;
let addend = *args.next().ok_or("arity")?.downcast::<Box<ErasedGNode<f64>>>().map_err(|_| "type")?;
Ok(Box::new(AddNode::new(augend, addend)))
}
fn scope_fixture<'a>(generations: &'a [(u64, u64)], arena: &'a Arena) -> EvalScope<'a> {
EvalScope::new(Some(0.5), None, None, generations, arena)
}
#[test]
fn hand_expansion_evaluates_through_typed_erased_edges() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let augend: Box<dyn Any> = Box::new(Box::new(ValueNode(1.0f64)) as Box<ErasedGNode<f64>>);
let addend: Box<dyn Any> = Box::new(Box::new(ValueNode(2.0f64)) as Box<ErasedGNode<f64>>);
let wired = add_constructor_f64(vec![augend, addend]).unwrap();
assert_eq!(wired.eval(&ctx), GPoll::Final(3.0));
}
#[test]
fn wiring_rejects_type_and_arity_mismatches() {
let augend: Box<dyn Any> = Box::new(Box::new(ValueNode(1.0f64)) as Box<ErasedGNode<f64>>);
let addend: Box<dyn Any> = Box::new(Box::new(ValueNode(2u32)) as Box<ErasedGNode<u32>>);
assert_eq!(add_constructor_f64(vec![augend, addend]).map(|_| ()), Err("type"));
let augend: Box<dyn Any> = Box::new(Box::new(ValueNode(1.0f64)) as Box<ErasedGNode<f64>>);
assert_eq!(add_constructor_f64(vec![augend]).map(|_| ()), Err("arity"));
}
#[test]
fn spec_loop_batches_through_the_erased_edge() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let graph: Box<ErasedGNode<f64>> = Box::new(AddNode::new(ReadIndexNode, ValueNode(10.0f64)));
let mut scratch = [const { MaybeUninit::uninit() }; 4];
let status = graph.eval_batch(&ctx, 2..6, Some(&mut scratch));
let BatchStatus::Filled(lanes, finality) = status else {
panic!("expected filled, got {status:?}");
};
assert_eq!(lanes, &[12.0, 13.0, 14.0, 15.0]);
assert_eq!(finality, Finality::AllFinal);
}
#[test]
fn lending_kernel_clones_once_per_generation_and_lends_after() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let node = LendStringNode::new("lend me".to_string());
let GPoll::Final(first) = node.eval(&ctx) else {
panic!("first eval must clone into the arena and lend");
};
let GPoll::Final(second) = node.eval(&ctx) else {
panic!("second eval must hit the cell");
};
assert_eq!(first, "lend me");
assert!(std::ptr::eq(first, second));
}
#[test]
fn exhausted_arena_reports_the_operational_error() {
let arena = Arena::new(0);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let node = LendStringNode::new("too big".to_string());
let GPoll::Error(error) = node.eval(&ctx) else {
panic!("exhaustion must surface as an operational error");
};
assert_eq!(error.kind, ErrorKind::ArenaExhausted);
}
#[test]
fn lending_edges_erase_and_wire_like_owned_edges() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let value: Box<dyn Any> = Box::new(Box::new(LendStringNode::new("across the boundary".to_string())) as Box<ErasedLendEdge>);
let wired = string_length_constructor(vec![value]).unwrap();
assert_eq!(wired.eval(&ctx), GPoll::Final(19.0));
}
#[test]
fn spec_loop_batches_through_the_erased_lending_edge() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let graph: Box<ErasedLendEdge> = Box::new(LendStringNode::new("batched".to_string()));
let mut scratch = [const { MaybeUninit::uninit() }; 3];
let status = graph.eval_batch(&ctx, 0..3, Some(&mut scratch));
let BatchStatus::Filled(lanes, finality) = status else {
panic!("expected filled, got {status:?}");
};
assert_eq!(lanes.len(), 3);
assert!(lanes.iter().all(|lane| std::ptr::eq(*lane, lanes[0])));
assert_eq!(*lanes[0], "batched");
assert_eq!(finality, Finality::AllFinal);
}
#[test]
fn fallback_input_records_partiality_invisibly() {
struct FallbackNode;
impl<Input> GNode<Input> for FallbackNode {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
GPoll::fallback(0.0, "upstream failed")
}
}
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let graph = AddNode::new(FallbackNode, ValueNode(5.0f64));
let GPoll::Fallback(boxed) = graph.eval(&ctx) else {
panic!("fallback must propagate with the computed stand-in");
};
assert_eq!(boxed.0, 5.0);
assert!(boxed.1.kind == "upstream failed");
assert_eq!(boxed.1.trace, vec![0]);
}