diff --git a/document/graph-storage/src/tests/round_trip.rs b/document/graph-storage/src/tests/round_trip.rs index 95c605f6a8..9d7eb45c8d 100644 --- a/document/graph-storage/src/tests/round_trip.rs +++ b/document/graph-storage/src/tests/round_trip.rs @@ -14,7 +14,7 @@ use crate::{NetworkId, NodeMetadataSource, PeerId, Position, Registry}; /// Test networks with Import inputs will fail compilation (which is expected). fn verify_network_compiles(network: &NodeNetwork) -> Result<(), String> { let compiler = Compiler {}; - compiler.compile_single(network.clone()).map_err(|e| format!("Compilation failed: {:?}", e))?; + compiler.compile_single(network.clone(), None).map_err(|e| format!("Compilation failed: {:?}", e))?; Ok(()) } diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 8c3b5cc428..c4c81f5453 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -438,7 +438,7 @@ impl NodeRuntime { assert_eq!(scoped_network.exports.len(), 1, "Graph with multiple outputs not yet handled"); let c = Compiler {}; - let proto_network = match c.compile_single(scoped_network) { + let proto_network = match c.compile_single(scoped_network, None) { Ok(network) => network, Err(e) => return Err((ResolvedDocumentNodeTypesDelta::default(), e)), }; diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 7b850324c0..e71b8b6dbb 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -166,6 +166,7 @@ impl DocumentNode { original_location: self.original_location, skip_deduplication: self.skip_deduplication, context_features: self.context_features, + resolved: Default::default(), } } } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index fde3d44f62..484336a1f5 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -263,6 +263,25 @@ macro_rules! tagged_value { } } + /// `None` for values whose element type is only known dynamically ([`Self::TypeDefault`]). + pub fn element_write(&self) -> Option { + Some(match self { + Self::None => core_types::record::element_write::<()>(), + Self::TypeDefault(_) => return None, + Self::F64Array(_) => core_types::record::element_write::>(), + Self::Color(_) => core_types::record::element_write::>(), + Self::Gradient(_) => core_types::record::element_write::>(), + Self::BrushStrokes(_) => core_types::record::element_write::>(), + $( Self::$identifier(_) => core_types::record::element_write::<$ty>(), )* + Self::RenderOutput(_) => core_types::record::element_write::(), + Self::NodeIdPath(_) => core_types::record::element_write::>(), + Self::DocumentNode(_) => core_types::record::element_write::(), + Self::ContextModification(_) => core_types::record::element_write::(), + Self::EditorApi(_) => core_types::record::element_write::>(), + Self::ResourceHash(_) => core_types::record::element_write::(), + }) + } + /// Materializes the value as [`Self::to_dynany`] does, wrapped in a `ClonedNode` edge typed by [`Self::ty`]. pub fn to_edge(self) -> Result { match self { diff --git a/node-graph/graph-craft/src/graphene_compiler.rs b/node-graph/graph-craft/src/graphene_compiler.rs index fbf5be5a65..95879980bc 100644 --- a/node-graph/graph-craft/src/graphene_compiler.rs +++ b/node-graph/graph-craft/src/graphene_compiler.rs @@ -1,11 +1,11 @@ use crate::document::NodeNetwork; -use crate::proto::ProtoNetwork; +use crate::proto::{ProtoNetwork, Registry}; use std::error::Error; pub struct Compiler {} impl Compiler { - pub fn compile(&self, mut network: NodeNetwork) -> impl Iterator> { + pub fn compile<'r>(&self, mut network: NodeNetwork, registry: Option<&'r Registry>) -> impl Iterator> + 'r { network.resolve_scope_inputs(); network.generate_node_paths(&[]); let node_ids = network.nodes.keys().copied().collect::>(); @@ -19,13 +19,17 @@ impl Compiler { proto_networks.map(move |mut proto_network| { proto_network.insert_context_nullification_nodes()?; + if let Some(registry) = registry { + let _ = proto_network.resolve_types(registry); + proto_network.compute_layouts(); + } proto_network.generate_stable_node_ids(); Ok(proto_network) }) } - pub fn compile_single(&self, network: NodeNetwork) -> Result { + pub fn compile_single(&self, network: NodeNetwork, registry: Option<&Registry>) -> Result { assert_eq!(network.exports.len(), 1, "Graph with multiple outputs not yet handled"); - let Some(proto_network) = self.compile(network).next() else { + let Some(proto_network) = self.compile(network, registry).next() else { return Err("Failed to convert graph into proto graph".to_string()); }; proto_network diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 25361c3219..b5544ccae0 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -124,6 +124,20 @@ impl ConstructionArgs { } } +#[derive(Debug, Clone, Default)] +pub(crate) struct Resolved { + pub io: Option, + pub layout_meta: Option, + pub layout: Option, +} + +impl PartialEq for Resolved { + fn eq(&self, _: &Self) -> bool { + true + } +} +impl Eq for Resolved {} + #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] /// A proto node is an intermediate step between the `DocumentNode` and the boxed struct that actually runs the node (found in the [`BorrowTree`]). /// At different stages in the compilation process, this struct will be transformed into a reduced (more restricted) form acting as a subset of its original form, but that restricted form is still valid in the earlier stage in the compilation process before it was transformed. @@ -134,6 +148,8 @@ pub struct ProtoNode { pub original_location: OriginalLocation, pub skip_deduplication: bool, pub(crate) context_features: ContextDependencies, + #[serde(skip)] + pub(crate) resolved: Resolved, } impl Default for ProtoNode { @@ -145,6 +161,7 @@ impl Default for ProtoNode { original_location: OriginalLocation::default(), skip_deduplication: false, context_features: Default::default(), + resolved: Default::default(), } } } @@ -185,6 +202,7 @@ impl ProtoNode { }, skip_deduplication: false, context_features: Default::default(), + resolved: Default::default(), } } @@ -202,6 +220,10 @@ impl ProtoNode { _ => panic!("tried to unwrap nodes from non node construction args \n node: {self:#?}"), } } + + pub fn resolved_layout(&self) -> Option<&core_types::record::Layout> { + self.resolved.layout.as_ref() + } } #[derive(Clone, Copy, PartialEq)] @@ -303,6 +325,65 @@ impl ProtoNetwork { self.nodes.iter().flat_map(|(_, node)| node.context_features.sources().iter().copied()).collect() } + pub fn resolve_types(&mut self, registry: &Registry) -> Result<(), String> { + self.reorder_ids()?; + for index in 0..self.nodes.len() { + let resolved = { + let node = &self.nodes[index].1; + match &node.construction_args { + ConstructionArgs::Value(value) => Resolved { + io: Some(NodeIOTypes::new(concrete!(Context), Type::Record(Box::new(value.ty())), vec![])), + ..Default::default() + }, + _ => { + let inputs: Vec = match &node.construction_args { + ConstructionArgs::Nodes(nodes) => nodes + .iter() + .map(|input| { + self.nodes[input.0 as usize] + .1 + .resolved + .io + .as_ref() + .map(|io| io.ty()) + .ok_or_else(|| format!("input {input:?} of {} is not yet typed", node.identifier.as_str())) + }) + .collect::>()?, + ConstructionArgs::Inline(inline) => vec![inline.ty.clone()], + ConstructionArgs::Value(_) => unreachable!(), + }; + let impls = registry.get(&node.identifier).ok_or_else(|| format!("no implementations for {}", node.identifier.as_str()))?; + let (io, entry) = resolve_entry(node, &inputs, impls).map_err(|errors| format!("{errors:?}"))?; + Resolved { + io: Some(io), + layout_meta: entry.layout_meta.clone(), + layout: None, + } + } + } + }; + self.nodes[index].1.resolved = resolved; + } + Ok(()) + } + + pub fn compute_layouts(&mut self) { + for index in 0..self.nodes.len() { + let layout = { + let node = &self.nodes[index].1; + match &node.construction_args { + ConstructionArgs::Value(value) => value.element_write().map(|element| core_types::record::Layout::default().with_writes(0, element, &[])), + ConstructionArgs::Nodes(inputs) => node.resolved.layout_meta.as_ref().and_then(|meta| { + let input_layouts: Vec> = inputs.iter().map(|input| self.nodes[input.0 as usize].1.resolved.layout.as_ref()).collect(); + meta.sources.iter().all(|&source| input_layouts[source as usize].is_some()).then(|| meta.fold(&input_layouts)) + }), + ConstructionArgs::Inline(_) => None, + } + }; + self.nodes[index].1.resolved.layout = layout; + } + } + /// 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> { @@ -643,23 +724,29 @@ impl Debug for GraphError { } pub type GraphErrors = Vec; +pub type Registry = HashMap>; + /// 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>>, + lookup: Cow<'static, Registry>, inferred: HashMap, constructor: HashMap, } impl TypingContext { /// Creates a new `TypingContext` with the given lookup table. - pub fn new(lookup: &'static HashMap>) -> Self { + pub fn new(lookup: &'static Registry) -> Self { Self { lookup: Cow::Borrowed(lookup), ..Default::default() } } + pub fn registry(&self) -> &Registry { + &self.lookup + } + /// Updates the `TypingContext` with a given proto network. This will infer the types of the nodes /// and store them in the `inferred` field. The proto network has to be topologically sorted /// and contain fully resolved stable node ids. @@ -715,115 +802,113 @@ 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(); + let (node_io, entry) = resolve_entry(node, &inputs, impls)?; + self.inferred.insert(node_id, node_io.clone()); + self.constructor.insert(node_id, entry.constructor); + Ok(node_io) + } +} - if let Some(index) = inputs.iter().position(|p| { - matches!(p, - Type::Fn(_, b) if matches!(b.as_ref(), Type::Generic(_))) - }) { - return Err(vec![GraphError::new(node, GraphErrorType::UnexpectedGenerics { index, inputs })]); +/// Selects the single registry entry matching the node's resolved input types, +/// substituting generics. Stateless and stable-id-free. +fn resolve_entry<'a>(node: &ProtoNode, inputs: &[Type], impls: &'a [RegistryEntry]) -> Result<(NodeIOTypes, &'a RegistryEntry), GraphErrors> { + let call_argument = &node.call_argument; + let candidates: Vec<(NodeIOTypes, &RegistryEntry)> = impls.iter().map(|entry| (entry.io.clone(), entry)).collect(); + + if let Some(index) = inputs.iter().position(|p| { + matches!(p, + Type::Fn(_, b) if matches!(b.as_ref(), Type::Generic(_))) + }) { + return Err(vec![GraphError::new(node, GraphErrorType::UnexpectedGenerics { index, inputs: inputs.to_vec() })]); + } + + // List of all implementations that match the input types + 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::>(); + + // Attempt to substitute generic types with concrete types and save the list of results + let substitution_results = valid_output_types + .iter() + .map(|(node_io, entry)| { + let generics_lookup: Result, _> = 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 mut new_node_io = node_io.clone(); + replace_generics(&mut new_node_io, &generics_lookup); + (new_node_io, *entry) + }) + }) + .collect::>(); + + // Collect all substitutions that are valid + let valid_impls = substitution_results.iter().filter_map(|result| result.as_ref().ok()).collect::>(); + + match valid_impls.as_slice() { + [] => { + 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 &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() + .chain(inputs) + .cloned() + .zip([&node_io.call_argument].into_iter().chain(&node_io.inputs).cloned()) + .enumerate() + .filter(|(_, (p1, p2))| !valid_type(p1, p2)) + .map(|(index, expected)| (index - 1 + convert_node_index_offset, expected)) + .collect::>(); + if current_errors.len() < best_errors { + best_errors = current_errors.len(); + error_inputs.clear(); + } + if current_errors.len() <= best_errors { + error_inputs.push(current_errors); + } + } + let inputs = [call_argument] + .into_iter() + .chain(inputs) + .enumerate() + .filter_map(|(i, t)| { + if i == 0 { + None + } else { + let number = i + convert_node_index_offset; + Some(format!("• Input {number}: {t}")) + } + }) + .collect::>() + .join("\n"); + Err(vec![GraphError::new(node, GraphErrorType::InvalidImplementations { inputs, error_inputs })]) + } + [(node_io, entry)] => Ok((node_io.clone(), *entry)), + // 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, entry) in [first, second] { + if node_io.call_argument != concrete!(()) { + continue; + } + return Ok((node_io.clone(), *entry)); + } + } + let inputs = [call_argument].into_iter().chain(inputs).map(ToString::to_string).collect::>().join(", "); + let valid = valid_output_types.into_iter().map(|(node_io, _)| node_io.clone()).collect(); + Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })]) } - // List of all implementations that match the input types - 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::>(); - - // Attempt to substitute generic types with concrete types and save the list of results - let substitution_results = valid_output_types - .iter() - .map(|(node_io, constructor)| { - let generics_lookup: Result, _> = 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 mut new_node_io = node_io.clone(); - replace_generics(&mut new_node_io, &generics_lookup); - (new_node_io, *constructor) - }) - }) - .collect::>(); - - // Collect all substitutions that are valid - let valid_impls = substitution_results.iter().filter_map(|result| result.as_ref().ok()).collect::>(); - - match valid_impls.as_slice() { - [] => { - 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 &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() - .chain(&inputs) - .cloned() - .zip([&node_io.call_argument].into_iter().chain(&node_io.inputs).cloned()) - .enumerate() - .filter(|(_, (p1, p2))| !valid_type(p1, p2)) - .map(|(index, expected)| (index - 1 + convert_node_index_offset, expected)) - .collect::>(); - if current_errors.len() < best_errors { - best_errors = current_errors.len(); - error_inputs.clear(); - } - if current_errors.len() <= best_errors { - error_inputs.push(current_errors); - } - } - let inputs = [call_argument] - .into_iter() - .chain(&inputs) - .enumerate() - .filter_map(|(i, t)| { - if i == 0 { - None - } else { - let number = i + convert_node_index_offset; - Some(format!("• Input {number}: {t}")) - } - }) - .collect::>() - .join("\n"); - Err(vec![GraphError::new(node, GraphErrorType::InvalidImplementations { inputs, error_inputs })]) - } - [(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, *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, 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, *constructor); - return Ok(node_io.clone()); - } - } - let inputs = [call_argument].into_iter().chain(&inputs).map(ToString::to_string).collect::>().join(", "); - 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::>().join(", "); - 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::>().join(", "); + let valid = valid_output_types.into_iter().map(|(node_io, _)| node_io.clone()).collect(); + Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })]) } } } diff --git a/node-graph/graph-craft/src/util.rs b/node-graph/graph-craft/src/util.rs index eddeec842c..de241be5f8 100644 --- a/node-graph/graph-craft/src/util.rs +++ b/node-graph/graph-craft/src/util.rs @@ -10,7 +10,7 @@ pub fn load_network(document_string: &str) -> NodeNetwork { pub fn compile(network: NodeNetwork) -> ProtoNetwork { let compiler = Compiler {}; - compiler.compile_single(network).unwrap() + compiler.compile_single(network, None).unwrap() } pub fn load_from_name(name: &str) -> NodeNetwork { diff --git a/node-graph/graphene-cli/src/main.rs b/node-graph/graphene-cli/src/main.rs index a25a005c9d..cda152c981 100644 --- a/node-graph/graphene-cli/src/main.rs +++ b/node-graph/graphene-cli/src/main.rs @@ -314,7 +314,7 @@ fn compile_graph(network: NodeNetwork, editor_api: Arc, gdd: } let compiler = Compiler {}; - compiler.compile_single(network).map_err(|x| x.into()) + compiler.compile_single(network, Some(&interpreted_executor::node_registry::NODE_REGISTRY)).map_err(|x| x.into()) } fn create_executor(proto_network: ProtoNetwork, runtime: Arc) -> Result> { diff --git a/node-graph/interpreted-executor/src/lib.rs b/node-graph/interpreted-executor/src/lib.rs index 44fca1526a..f2f6e626c7 100644 --- a/node-graph/interpreted-executor/src/lib.rs +++ b/node-graph/interpreted-executor/src/lib.rs @@ -44,7 +44,7 @@ mod tests { use graph_craft::graphene_compiler::Compiler; let compiler = Compiler {}; - let protograph = compiler.compile_single(network).expect("Graph should be generated"); + let protograph = compiler.compile_single(network, Some(&crate::node_registry::NODE_REGISTRY)).expect("Graph should be generated"); let _exec = DynamicExecutor::new(protograph).map(|_e| panic!("The network should not type check ")).unwrap_err(); }