Cut over to the graphene execution model

This commit is contained in:
Dennis Kobert
2026-08-04 13:15:21 +02:00
parent 7623b68318
commit 76ec799496
71 changed files with 3544 additions and 2378 deletions
+9 -4
View File
@@ -9,7 +9,7 @@ pub use graphene_application_io::ApplicationIo;
#[derive(Default)]
pub struct PlatformApplicationIo {
#[cfg(feature = "wgpu")]
gpu_executor: Option<WgpuExecutor>,
gpu_executor: Option<std::sync::Arc<WgpuExecutor>>,
resources: Option<Box<dyn resource::LoadResource>>,
}
@@ -26,7 +26,7 @@ impl PlatformApplicationIo {
Self {
#[cfg(feature = "wgpu")]
gpu_executor: executor,
gpu_executor: executor.map(std::sync::Arc::new),
resources: None,
}
}
@@ -39,7 +39,7 @@ impl PlatformApplicationIo {
set_wgpu_available(wgpu_available);
Self {
gpu_executor: executor,
gpu_executor: executor.map(std::sync::Arc::new),
resources: None,
}
}
@@ -57,7 +57,12 @@ impl ApplicationIo for PlatformApplicationIo {
#[cfg(feature = "wgpu")]
fn gpu_executor(&self) -> Option<&Self::Executor> {
self.gpu_executor.as_ref()
self.gpu_executor.as_deref()
}
#[cfg(feature = "wgpu")]
fn gpu_executor_arc(&self) -> Option<std::sync::Arc<Self::Executor>> {
self.gpu_executor.clone()
}
fn load_resource(&self, hash: resource::ResourceHash) -> resource::ResourceFuture<'_> {
+21 -3
View File
@@ -8,7 +8,6 @@ pub use core_types::uuid::generate_uuid;
use core_types::{Context, ContextDependencies, Cow, MemoHash, ProtoNodeIdentifier, Type};
use dyn_any::DynAny;
use glam::IVec2;
use log::Metadata;
use rustc_hash::FxHashMap;
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
@@ -215,12 +214,14 @@ impl InlineRust {
#[derive(Debug, Clone, PartialEq, Hash, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
pub enum DocumentNodeMetadata {
DocumentNodePath,
SourceId,
}
impl DocumentNodeMetadata {
pub fn ty(&self) -> Type {
match self {
DocumentNodeMetadata::DocumentNodePath => concrete!(core_types::list::List<NodeId>),
DocumentNodeMetadata::SourceId => concrete!(u64),
}
}
}
@@ -273,7 +274,7 @@ impl NodeInput {
NodeInput::Import { import_type, .. } => import_type.clone(),
NodeInput::Inline(_) => panic!("ty() called on NodeInput::Inline"),
NodeInput::Scope(_) => panic!("ty() called on NodeInput::Scope"),
NodeInput::Reflection(_) => concrete!(Metadata),
NodeInput::Reflection(metadata) => metadata.ty(),
}
}
@@ -879,7 +880,7 @@ impl NodeNetwork {
// Replace value inputs with dedicated value nodes
if node.implementation != DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("core_types::value::ClonedNode")) {
Self::replace_value_inputs_with_nodes(&mut node.inputs, &mut self.nodes, &path, gen_id, map_ids, id);
Self::replace_value_inputs_with_nodes(&mut node.inputs, &mut self.nodes, &path, gen_id, map_ids, id, Some(&mut node.context_features));
}
let DocumentNodeImplementation::Network(mut inner_network) = node.implementation else {
@@ -898,6 +899,7 @@ impl NodeNetwork {
gen_id,
map_ids,
id,
None,
);
// Connect all network inputs to either the parent network nodes, or newly created value nodes for the parent node.
@@ -978,6 +980,12 @@ impl NodeNetwork {
}
}
fn source_id_for_path(path: &[NodeId]) -> u64 {
let mut hasher = graphene_hash::FxHasher64::new();
path.hash(&mut hasher);
hasher.finish()
}
#[inline(never)]
fn replace_value_inputs_with_nodes(
inputs: &mut [NodeInput],
@@ -986,6 +994,7 @@ impl NodeNetwork {
gen_id: impl Fn() -> NodeId + Copy,
map_ids: impl Fn(NodeId, NodeId) -> NodeId + Copy,
id: NodeId,
mut context_features: Option<&mut ContextDependencies>,
) {
// Replace value exports and imports with value nodes, added inside the nested network
for export in inputs {
@@ -996,6 +1005,13 @@ impl NodeNetwork {
NodeInput::Value { tagged_value, exposed } => (tagged_value, exposed),
NodeInput::Reflection(reflect) => match reflect {
DocumentNodeMetadata::DocumentNodePath => (TaggedValue::NodeIdPath(path.to_vec()).into(), false),
DocumentNodeMetadata::SourceId => {
let source_id = Self::source_id_for_path(path);
if let Some(context_features) = context_features.as_deref_mut() {
core_types::context::merge_sorted_sources(&mut context_features.sources, &[source_id]);
}
(TaggedValue::U64(source_id).into(), false)
}
},
previous_export => {
*export = previous_export;
@@ -1208,7 +1224,9 @@ fn migrate_call_argument<'de, D: serde::Deserializer<'de>>(deserializer: D) -> R
Old(Option<Type>),
}
// TODO: Eventually remove this migration document upgrade code
Ok(match CallArg::deserialize(deserializer)? {
CallArg::New(Type::Concrete(descriptor)) if descriptor.name.ends_with("OwnedContextImpl>>") => concrete!(Context),
CallArg::New(ty) => ty,
CallArg::Old(ty) => ty.unwrap_or_default(),
})
+90 -43
View File
@@ -1,13 +1,18 @@
use super::DocumentNode;
use crate::application_io::PlatformEditorApi;
use crate::application_io::resource::Resource;
use crate::proto::{Any as DAny, FutureAny};
use crate::proto::Any as DAny;
use brush_nodes::brush_stroke::BrushStroke;
use core_types::color::SRGBA8;
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::transform::Footprint;
use core_types::uuid::NodeId;
use core_types::{CacheHash, Color, ContextFeatures, MemoHash, Node, Type, TypeDescriptor};
use core_types::value::value_edge;
use core_types::{CacheHash, Color, ContextModification, MemoHash, Type, TypeDescriptor};
use dyn_any::DynAny;
pub use dyn_any::StaticType;
pub use glam::{DAffine2, DVec2, IVec2, UVec2};
@@ -19,7 +24,6 @@ use graphic_types::{Artboard, Graphic, Vector};
use rendering::RenderMetadata;
use std::fmt::Display;
use std::hash::Hash;
use std::marker::PhantomData;
use std::str::FromStr;
pub use std::sync::Arc;
use text_nodes::Font;
@@ -90,7 +94,7 @@ macro_rules! tagged_value {
DocumentNode(DocumentNode),
/// Carried by context nullification proto nodes constructed at proto node compilation time in `insert_context_nullification_nodes`.
#[serde(skip)]
ContextFeatures(ContextFeatures),
ContextModification(ContextModification),
#[serde(skip)]
EditorApi(Arc<PlatformEditorApi>),
/// Only used by the `resource` node, should never be serialized
@@ -120,7 +124,7 @@ macro_rules! tagged_value {
// =======================
Self::NodeIdPath(path) => path.hash(state),
Self::DocumentNode(node) => node.cache_hash(state),
Self::ContextFeatures(features) => features.cache_hash(state),
Self::ContextModification(modification) => modification.cache_hash(state),
Self::RenderOutput(x) => x.cache_hash(state),
Self::EditorApi(x) => x.cache_hash(state),
Self::ResourceHash(x) => x.cache_hash(state),
@@ -175,7 +179,7 @@ macro_rules! tagged_value {
Box::new(list)
}
Self::DocumentNode(node) => Box::new(node),
Self::ContextFeatures(features) => Box::new(features),
Self::ContextModification(modification) => Box::new(modification),
Self::EditorApi(x) => Box::new(x),
Self::ResourceHash(x) => Box::new(x),
}
@@ -225,7 +229,7 @@ macro_rules! tagged_value {
Arc::new(list)
}
Self::DocumentNode(node) => Arc::new(node),
Self::ContextFeatures(features) => Arc::new(features),
Self::ContextModification(modification) => Arc::new(modification),
Self::EditorApi(x) => Arc::new(x),
Self::ResourceHash(x) => Arc::new(x),
}
@@ -253,12 +257,88 @@ macro_rules! tagged_value {
Self::RenderOutput(_) => concrete!(RenderOutput),
Self::NodeIdPath(_) => concrete!(List<NodeId>),
Self::DocumentNode(_) => concrete!(DocumentNode),
Self::ContextFeatures(_) => concrete!(ContextFeatures),
Self::EditorApi(_) => concrete!(&PlatformEditorApi),
Self::ContextModification(_) => concrete!(ContextModification),
Self::EditorApi(_) => concrete!(Arc<PlatformEditorApi>),
Self::ResourceHash(_) => concrete!(ResourceHash),
}
}
/// Materializes the value as [`Self::to_dynany`] does, wrapped in a `ClonedNode` edge typed by [`Self::ty`].
pub fn to_edge(self) -> Result<EdgeHandle, String> {
match self {
// ===============
// MANUAL VARIANTS
// ===============
Self::None => Ok(value_edge(())),
Self::TypeDefault(td) => {
// Same direct-construction path as `to_dynany` for the same reason as in `to_dynany`.
let name = td.name.as_ref();
macro_rules! check {
($type_default:ty) => {
if name == std::any::type_name::<$type_default>() { return Ok(value_edge(<$type_default>::default())); }
};
}
for_each_type_default!(check);
Self::from_type_or_none(&Type::Concrete(td)).to_edge()
}
Self::F64Array(values) => {
let list: List<f64> = values.into_iter().map(core_types::list::Item::new_from_element).collect();
Ok(value_edge(list))
}
Self::Color(color) => {
let list: List<Color> = color.into_iter().map(core_types::list::Item::new_from_element).collect();
Ok(value_edge(list))
}
Self::Gradient(stops) => Ok(value_edge(List::<GradientStops>::new_from_element(stops))),
Self::BrushStrokes(strokes) => {
let list: List<BrushStroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
Ok(value_edge(list))
}
// =======================
// AUTO-GENERATED VARIANTS
// =======================
$( Self::$identifier(x) => Ok(value_edge(x)), )*
// =======================
// NON-SERIALIZED VARIANTS
// =======================
Self::RenderOutput(x) => Ok(value_edge(x)),
Self::NodeIdPath(path) => {
let list: List<NodeId> = path.into_iter().map(core_types::list::Item::new_from_element).collect();
Ok(value_edge(list))
}
Self::DocumentNode(node) => Ok(value_edge(node)),
Self::ContextModification(modification) => Ok(value_edge(modification)),
Self::EditorApi(x) => Ok(value_edge(x)),
Self::ResourceHash(x) => Ok(value_edge(x)),
}
}
/// 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));
}
Err(format!("Cannot convert edge of type {ty} to TaggedValue"))
}
/// Attempts to downcast the dynamic type to a tagged value
pub fn try_from_any(input: Box<dyn DynAny<'a> + 'a>) -> Result<Self, String> {
use dyn_any::downcast;
@@ -360,7 +440,7 @@ macro_rules! tagged_value {
Self::RenderOutput(_) => "RenderOutput".to_string(),
Self::NodeIdPath(path) => format!("NodeIdPath({path:?})"),
Self::DocumentNode(node) => format!("DocumentNode({node:?})"),
Self::ContextFeatures(features) => format!("ContextFeatures({features:?})"),
Self::ContextModification(modification) => format!("ContextModification({modification:?})"),
Self::EditorApi(_) => "PlatformEditorApi".to_string(),
Self::ResourceHash(hash) => format!("ResourceHash({hash:?})"),
}
@@ -686,39 +766,6 @@ impl Display for TaggedValue {
}
}
pub struct UpcastNode {
value: MemoHash<TaggedValue>,
}
impl<'input> Node<'input, DAny<'input>> for UpcastNode {
type Output = FutureAny<'input>;
fn eval(&'input self, _: DAny<'input>) -> Self::Output {
let memo_clone = MemoHash::clone(&self.value);
Box::pin(async move { memo_clone.into_inner().as_ref().clone().to_dynany() })
}
}
impl UpcastNode {
pub fn new(value: MemoHash<TaggedValue>) -> Self {
Self { value }
}
}
#[derive(Default, Debug, Clone, Copy)]
pub struct UpcastAsRefNode<T: AsRef<U> + Sync + Send, U: Sync + Send>(pub T, PhantomData<U>);
impl<'i, T: 'i + AsRef<U> + Sync + Send, U: 'i + StaticType + Sync + Send> Node<'i, DAny<'i>> for UpcastAsRefNode<T, U> {
type Output = FutureAny<'i>;
#[inline(always)]
fn eval(&'i self, _: DAny<'i>) -> Self::Output {
Box::pin(async move { Box::new(self.0.as_ref()) as DAny<'i> })
}
}
impl<T: AsRef<U> + Sync + Send, U: Sync + Send> UpcastAsRefNode<T, U> {
pub const fn new(value: T) -> UpcastAsRefNode<T, U> {
UpcastAsRefNode(value, PhantomData)
}
}
#[derive(Debug, Clone, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)]
pub struct RenderOutput {
pub data: RenderOutputType,
@@ -1,5 +1,5 @@
use crate::document::NodeNetwork;
use crate::proto::{LocalFuture, ProtoNetwork};
use crate::proto::ProtoNetwork;
use std::error::Error;
pub struct Compiler {}
@@ -33,5 +33,5 @@ impl Compiler {
}
pub trait Executor<I, O> {
fn execute(&self, input: I) -> LocalFuture<'_, Result<O, Box<dyn Error>>>;
fn execute(&self, input: I) -> Result<O, Box<dyn Error>>;
}
+153 -35
View File
@@ -294,6 +294,10 @@ impl ProtoNetwork {
(inwards_edges, id_map)
}
pub fn source_ids(&self) -> Vec<SourceId> {
self.nodes.iter().flat_map(|(_, node)| node.context_features.sources().iter().copied()).collect()
}
/// Inserts context nullification nodes to optimize caching.
/// This analysis is performed after topological sorting to ensure proper dependency tracking.
pub fn insert_context_nullification_nodes(&mut self) -> Result<(), String> {
@@ -308,7 +312,7 @@ impl ProtoNetwork {
Ok(())
}
fn insert_context_nullification_node(&mut self, node_id: NodeId, context_deps: ContextFeatures) -> NodeId {
fn insert_context_nullification_node(&mut self, node_id: NodeId, context_deps: ContextModification) -> NodeId {
let (_, node) = &self.nodes[node_id.0 as usize];
let mut path = node.original_location.path.clone();
@@ -338,7 +342,7 @@ impl ProtoNetwork {
self.nodes.push((
nullification_value_node_id,
ProtoNode {
construction_args: ConstructionArgs::Value(MemoHash::new(TaggedValue::ContextFeatures(context_deps))),
construction_args: ConstructionArgs::Value(MemoHash::new(TaggedValue::ContextModification(context_deps))),
call_argument: concrete!(Context),
identifier: ProtoNodeIdentifier::new("core_types::value::ClonedNode"),
original_location: OriginalLocation {
@@ -365,42 +369,43 @@ impl ProtoNetwork {
nullification_node_id
}
fn find_context_dependencies(&mut self, id: NodeId) -> (ContextFeatures, Option<NodeId>) {
fn find_context_dependencies(&mut self, id: NodeId) -> (ContextModification, Option<NodeId>) {
let mut branch_dependencies = Vec::new();
let mut combined_deps = ContextFeatures::default();
let mut combined_deps = ContextModification::default();
let node_index = id.0 as usize;
let (extract, inject) = {
let (extract, inject, own_deps) = {
let dependencies = &self.nodes[node_index].1.context_features;
(dependencies.extract, dependencies.inject)
let own_deps = ContextModification::from_sources(dependencies.extract, dependencies.sources());
(dependencies.extract, dependencies.inject, own_deps)
};
let mut inputs = match &self.nodes[node_index].1.construction_args {
// We pretend like we have already placed context modification nodes after ourselves because value nodes don't need to be cached
ConstructionArgs::Value(_) => return (extract, Some(id)),
ConstructionArgs::Value(_) => return (own_deps, Some(id)),
ConstructionArgs::Nodes(items) => items.clone(),
ConstructionArgs::Inline(_) => return (extract, Some(id)),
ConstructionArgs::Inline(_) => return (own_deps, Some(id)),
};
// Compute the dependencies for each branch and combine all of them
for &node in &inputs {
let branch = self.find_context_dependencies(node);
combined_deps |= &branch.0;
branch_dependencies.push(branch);
combined_deps |= branch.0;
}
let mut new_deps = combined_deps;
let mut new_deps = combined_deps.clone();
// Remove requirements which this node provides
new_deps &= !inject;
// Add requirements we have
new_deps |= extract;
new_deps |= own_deps;
// If we either introduce new dependencies, we can cache all children which don't yet need that dependency
let we_introduce_new_deps = !combined_deps.contains(new_deps);
let we_introduce_new_deps = !combined_deps.contains(&new_deps);
// For diverging branches, we can add a cache node for all branches which don't reqire all dependencies
for (child_node, (deps, new_id)) in inputs.iter_mut().zip(branch_dependencies.into_iter()) {
for (child_node, (deps, new_id)) in inputs.iter_mut().zip(branch_dependencies) {
if let Some(new_id) = new_id {
*child_node = new_id;
} else if we_introduce_new_deps || deps != combined_deps {
@@ -413,15 +418,15 @@ impl ProtoNetwork {
let net_injections = inject.difference(extract);
// Which dependencies still need to be met after this node?
let remaining_deps_from_children = combined_deps.difference(net_injections);
let remaining_deps_from_children = combined_deps.features.difference(net_injections);
// Do we satisfy any existing dependencies?
let we_supply_existing_deps = !combined_deps.difference(remaining_deps_from_children).is_empty();
let we_supply_existing_deps = !combined_deps.features.difference(remaining_deps_from_children).is_empty();
let mut new_id = None;
if we_supply_existing_deps {
// Our set of context dependencies has shrunk so we can add a cache node after the current node
new_id = Some(self.insert_context_nullification_node(id, new_deps));
new_id = Some(self.insert_context_nullification_node(id, new_deps.clone()));
}
(new_deps, new_id)
@@ -548,6 +553,7 @@ pub enum GraphErrorType {
},
NoImplementations,
NoConstructor,
ConstructionFailed(String),
/// The `inputs` represents a formatted list of input indices corresponding to their types.
/// Each element in `error_inputs` represents a valid `NodeIOTypes` implementation.
/// The inner Vec stores the inputs which need to be changed and what type each needs to be changed to.
@@ -568,6 +574,7 @@ impl Debug for GraphErrorType {
GraphErrorType::UnexpectedGenerics { index, inputs } => write!(f, "Generic inputs should not exist but found at {index}: {inputs:?}"),
GraphErrorType::NoImplementations => write!(f, "No implementations found"),
GraphErrorType::NoConstructor => write!(f, "No construct found for node"),
GraphErrorType::ConstructionFailed(error) => write!(f, "Construction failed: {error}"),
GraphErrorType::InvalidImplementations { inputs, error_inputs } => {
let format_error = |(index, (found, expected)): &(usize, (Type, Type))| {
let index = index + 1;
@@ -634,14 +641,14 @@ pub type GraphErrors = Vec<GraphError>;
/// The `TypingContext` is used to store the types of the nodes indexed by their stable node id.
#[derive(Default, Clone, dyn_any::DynAny)]
pub struct TypingContext {
lookup: Cow<'static, HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>>>,
lookup: Cow<'static, HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>>>,
inferred: HashMap<NodeId, NodeIOTypes>,
constructor: HashMap<NodeId, NodeConstructor>,
}
impl TypingContext {
/// Creates a new `TypingContext` with the given lookup table.
pub fn new(lookup: &'static HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>>) -> Self {
pub fn new(lookup: &'static HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>>) -> Self {
Self {
lookup: Cow::Borrowed(lookup),
..Default::default()
@@ -685,7 +692,7 @@ impl TypingContext {
// If the node has a value input we can infer the return type from it
ConstructionArgs::Value(ref v) => {
// TODO: This should return a reference to the value
let types = NodeIOTypes::new(concrete!(Context), Type::Future(Box::new(v.ty())), vec![]);
let types = NodeIOTypes::new(concrete!(Context), v.ty(), vec![]);
self.inferred.insert(node_id, types.clone());
return Ok(types);
}
@@ -705,6 +712,7 @@ impl TypingContext {
// Get the node input type from the proto node declaration
let call_argument = &node.call_argument;
let impls = self.lookup.get(&node.identifier).ok_or_else(|| vec![GraphError::new(node, GraphErrorType::NoImplementations)])?;
let candidates: Vec<(NodeIOTypes, NodeConstructor)> = impls.iter().map(|entry| (entry.io.clone(), entry.constructor)).collect();
if let Some(index) = inputs.iter().position(|p| {
matches!(p,
@@ -719,8 +727,6 @@ impl TypingContext {
match (from, to) {
// Direct comparison of two concrete types.
(Type::Concrete(type1), Type::Concrete(type2)) => type1 == type2,
// Check inner type for futures
(Type::Future(type1), Type::Future(type2)) => valid_type(type1, type2),
// Direct comparison of two function types.
// Note: in the presence of subtyping, functions are considered on a "greater than or equal to" basis of its function type's generality.
// That means we compare their types with a contravariant relationship, which means that a more general type signature may be substituted for a more specific type signature.
@@ -743,25 +749,24 @@ impl TypingContext {
}
// List of all implementations that match the input types
let valid_output_types = impls
.keys()
.filter(|node_io| valid_type(&node_io.call_argument, call_argument) && inputs.iter().zip(node_io.inputs.iter()).all(|(p1, p2)| valid_type(p1, p2)))
let valid_output_types = candidates
.iter()
.filter(|(node_io, _)| valid_type(&node_io.call_argument, call_argument) && inputs.iter().zip(node_io.inputs.iter()).all(|(p1, p2)| valid_type(p1, p2)))
.collect::<Vec<_>>();
// Attempt to substitute generic types with concrete types and save the list of results
let substitution_results = valid_output_types
.iter()
.map(|node_io| {
.map(|(node_io, constructor)| {
let generics_lookup: Result<HashMap<_, _>, _> = collect_generics(node_io)
.iter()
.map(|generic| check_generic(node_io, call_argument, &inputs, generic).map(|x| (generic.to_string(), x)))
.collect();
generics_lookup.map(|generics_lookup| {
let orig_node_io = (*node_io).clone();
let mut new_node_io = orig_node_io.clone();
let mut new_node_io = node_io.clone();
replace_generics(&mut new_node_io, &generics_lookup);
(new_node_io, orig_node_io)
(new_node_io, *constructor)
})
})
.collect::<Vec<_>>();
@@ -774,7 +779,7 @@ impl TypingContext {
let convert_node_index_offset = node.original_location.auto_convert_index.unwrap_or(0);
let mut best_errors = usize::MAX;
let mut error_inputs = Vec::new();
for node_io in impls.keys() {
for (node_io, _) in &candidates {
// For errors on Convert nodes, offset the input index so it correctly corresponds to the node it is connected to.
let current_errors = [call_argument]
.into_iter()
@@ -809,36 +814,36 @@ impl TypingContext {
.join("\n");
Err(vec![GraphError::new(node, GraphErrorType::InvalidImplementations { inputs, error_inputs })])
}
[(node_io, org_nio)] => {
[(node_io, constructor)] => {
let node_io = node_io.clone();
// Save the inferred type
self.inferred.insert(node_id, node_io.clone());
self.constructor.insert(node_id, impls[org_nio]);
self.constructor.insert(node_id, *constructor);
Ok(node_io)
}
// If two types are available and one of them accepts () an input, always choose that one
[first, second] => {
if first.0.call_argument != second.0.call_argument {
for (node_io, orig_nio) in [first, second] {
for (node_io, constructor) in [first, second] {
if node_io.call_argument != concrete!(()) {
continue;
}
// Save the inferred type
self.inferred.insert(node_id, node_io.clone());
self.constructor.insert(node_id, impls[orig_nio]);
self.constructor.insert(node_id, *constructor);
return Ok(node_io.clone());
}
}
let inputs = [call_argument].into_iter().chain(&inputs).map(ToString::to_string).collect::<Vec<_>>().join(", ");
let valid = valid_output_types.into_iter().cloned().collect();
let valid = valid_output_types.into_iter().map(|(node_io, _)| node_io.clone()).collect();
Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })])
}
_ => {
let inputs = [call_argument].into_iter().chain(&inputs).map(ToString::to_string).collect::<Vec<_>>().join(", ");
let valid = valid_output_types.into_iter().cloned().collect();
let valid = valid_output_types.into_iter().map(|(node_io, _)| node_io.clone()).collect();
Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })])
}
}
@@ -958,6 +963,81 @@ mod test {
);
}
#[test]
fn retain_filter_placement_on_source_free_branch() {
let mut network = source_branch_network(vec![1], vec![]);
network.insert_context_nullification_nodes().expect("Error when calling 'insert_context_nullification_nodes'");
let filters = nullification_filters(&network);
assert_eq!(filters.len(), 1, "only the source-free branch gets a filter");
let (filter_id, wrapped, retained) = &filters[0];
assert_eq!(wrapped, "source_b");
assert!(retained.is_empty(), "the source-free branch retains no sources");
let (source_a_id, _) = find_node(&network, "source_a");
let (_, join) = find_node(&network, "join");
let ConstructionArgs::Nodes(join_args) = &join.construction_args else {
panic!("join args must be nodes")
};
assert_eq!(join_args, &vec![source_a_id, *filter_id], "the source branch stays direct, the filter replaces the source-free branch");
}
#[test]
fn diverging_source_sets_filter_each_branch() {
let mut network = source_branch_network(vec![1], vec![2]);
network.insert_context_nullification_nodes().expect("Error when calling 'insert_context_nullification_nodes'");
let mut filters = nullification_filters(&network);
filters.sort_by(|(_, a, _), (_, b, _)| a.cmp(b));
let summary: Vec<_> = filters.iter().map(|(_, wrapped, retained)| (wrapped.as_str(), retained.as_slice())).collect();
assert_eq!(
summary,
vec![("source_a", &[1u64][..]), ("source_b", &[2u64][..])],
"each diverging branch is filtered down to its own source set"
);
}
#[test]
fn matching_source_sets_insert_no_filter() {
let mut network = source_branch_network(vec![1], vec![1]);
network.insert_context_nullification_nodes().expect("Error when calling 'insert_context_nullification_nodes'");
assert!(nullification_filters(&network).is_empty(), "equal branch source sets need no filter");
}
fn find_node<'a>(network: &'a ProtoNetwork, name: &str) -> (NodeId, &'a ProtoNode) {
network
.nodes
.iter()
.find(|(_, node)| node.identifier.as_str() == name)
.map(|(id, node)| (*id, node))
.unwrap_or_else(|| panic!("node {name} not found"))
}
fn nullification_filters(network: &ProtoNetwork) -> Vec<(NodeId, String, Vec<SourceId>)> {
let node = |id: NodeId| &network.nodes[id.0 as usize].1;
network
.nodes
.iter()
.filter(|(_, candidate)| candidate.identifier.as_str() == graphene_core::context_modification::context_modification::IDENTIFIER.as_str())
.map(|(id, candidate)| {
let ConstructionArgs::Nodes(args) = &candidate.construction_args else {
panic!("filter args must be nodes")
};
let ConstructionArgs::Nodes(memoized) = &node(args[0]).construction_args else {
panic!("filter memoize args must be nodes")
};
let ConstructionArgs::Value(value) = &node(args[1]).construction_args else {
panic!("filter payload must be a value")
};
let value::TaggedValue::ContextModification(modification) = &**value else {
panic!("filter payload must be a context modification")
};
(*id, node(memoized[0]).identifier.as_str().to_string(), modification.sources().to_vec())
})
.collect()
}
fn test_network() -> ProtoNetwork {
ProtoNetwork {
inputs: vec![NodeId(10)],
@@ -1014,6 +1094,44 @@ mod test {
}
}
fn source_branch_network(branch_a_sources: Vec<SourceId>, branch_b_sources: Vec<SourceId>) -> ProtoNetwork {
let branch = |name: &str, sources: Vec<SourceId>| ProtoNode {
identifier: ProtoNodeIdentifier::with_owned_string(name.to_string()),
call_argument: concrete!(()),
construction_args: ConstructionArgs::Nodes(vec![NodeId(0)]),
context_features: ContextDependencies::from_sources(&sources),
..Default::default()
};
ProtoNetwork {
inputs: vec![],
output: NodeId(3),
nodes: [
(
NodeId(0),
ProtoNode {
identifier: ProtoNodeIdentifier::new("value"),
call_argument: concrete!(()),
construction_args: ConstructionArgs::Value(value::TaggedValue::U32(2).into()),
..Default::default()
},
),
(NodeId(1), branch("source_a", branch_a_sources)),
(NodeId(2), branch("source_b", branch_b_sources)),
(
NodeId(3),
ProtoNode {
identifier: ProtoNodeIdentifier::new("join"),
call_argument: concrete!(()),
construction_args: ConstructionArgs::Nodes(vec![NodeId(1), NodeId(2)]),
..Default::default()
},
),
]
.into_iter()
.collect(),
}
}
fn test_network_with_cycles() -> ProtoNetwork {
ProtoNetwork {
inputs: vec![NodeId(1)],