use std::borrow::Cow; use std::collections::HashMap; use core_types::memo::MemoHash; use core_types::uuid::NodeId as RuntimeNodeId; use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput as GraphCraftNodeInput, NodeNetwork}; use graph_craft::{ProtoNodeIdentifier, Type, concrete}; use rustc_hash::{FxHashMap, FxHashSet}; use crate::attr::*; use crate::metadata_source::{InputMetadataEntry, NetworkMetadataEntry, NodeMetadataEntry}; use crate::{AttributesRead, Implementation, NetworkId, Node, NodeId, NodeInput, Position, ProtoNode, ROOT_NETWORK, Registry, ResourceId}; #[derive(Debug, thiserror::Error)] pub enum ConversionError { #[error("Network {0} not found")] NetworkNotFound(NetworkId), #[error("Node {0} not found")] NodeNotFound(NodeId), #[error("ProtoNode declaration {0} not found in provided declarations")] DeclarationNotFound(ResourceId), #[error("Deserialization error: {0}")] DeserializationError(String), #[error("Network {network} has two nodes mapping to runtime ID {runtime_id}")] DuplicateRuntimeNodeId { network: NetworkId, runtime_id: u64 }, #[error("Network {network} references node {referenced}, which lives in a different network")] CrossNetworkReference { network: NetworkId, referenced: NodeId }, #[error("Scope injection {key:?} in network {network} references node {referenced}, which is missing or in a different network")] DanglingScopeInjection { network: NetworkId, key: String, referenced: NodeId }, #[error("Network {0} is reachable from itself through nested implementations, forming a cycle")] CyclicNetwork(NetworkId), } /// Resolved proto-node declarations, keyed by the `ResourceId` that `Implementation::ProtoNode` /// references. The caller resolves these from its byte store (`ResourceId` → `ResourceHash` → /// stored `ProtoNode` bytes) before converting, since `document-graph-storage` holds only references. pub type Declarations = std::collections::HashMap; impl Registry { /// Returns the network plus per-node metadata entries (one per node carrying any `ui::*` attribute). pub fn to_runtime_with_metadata(&self, declarations: &Declarations) -> Result<(NodeNetwork, Vec), ConversionError> { let (network, node_entries, _) = self.to_runtime_with_full_metadata(declarations)?; Ok((network, node_entries)) } /// Like `to_runtime_with_metadata` but also returns per-network entries (navigation, previewing). /// Used by the editor's full-rebuild path. pub fn to_runtime_with_full_metadata(&self, declarations: &Declarations) -> Result<(NodeNetwork, Vec, Vec), ConversionError> { let mut node_metadata = Some(Vec::new()); let mut network_metadata = Some(Vec::new()); // Group nodes by their owning network in one pass, so each `convert_network` call (one per // network, including nested ones) takes its node list by lookup instead of rescanning the whole // flat `node_instances` map, which would be quadratic on graphs with many networks. let mut nodes_by_network: FxHashMap> = FxHashMap::default(); for (&global_id, node) in &self.node_instances { nodes_by_network.entry(node.network).or_default().push((global_id, node)); } let context = ConversionContext { registry: self, declarations, nodes_by_network, }; // Reject cycles up front so the recursive conversion below can assume the network reference // graph is acyclic and never blow the stack on a self-referential `Implementation::Network`. detect_network_cycle(&context, ROOT_NETWORK)?; let network = convert_network(&context, ROOT_NETWORK, &[], &mut node_metadata, &mut network_metadata)?; Ok((network, node_metadata.expect("seeded above"), network_metadata.expect("seeded above"))) } /// Rebuild the runtime [`ResourceRegistry`](graphene_resource::ResourceRegistry) from the stored /// `resources`. Each entry's source chain is restored in priority order (the chain is kept /// sorted by key) with bodies decoded from their type-erased `serde_json::Value` form back to /// `DataSource`; the resolved hash, if any, is restored last. Inverse of `convert_resources` in /// `from_runtime`. pub fn to_resource_registry(&self) -> Result { let mut registry = graphene_resource::ResourceRegistry::new(); for (id, entry) in &self.resources { for (_, source) in &entry.sources { let decoded: graphene_resource::DataSource = serde_json::from_value(source.source.clone()).map_err(|error| ConversionError::DeserializationError(error.to_string()))?; registry.push_source_back(id, decoded); } if let Some(hash) = entry.hash { registry.resolve(id, hash); } } Ok(registry) } } /// Immutable shared context threaded through the recursive conversion. `nodes_by_network` is the /// one-pass grouping of `registry.node_instances` by owning network, so each network's nodes are an /// O(1) lookup rather than a full rescan. struct ConversionContext<'a> { registry: &'a Registry, declarations: &'a Declarations, nodes_by_network: FxHashMap>, } /// Converts a single network. Recurses through `Implementation::Network` owning nodes. /// /// **ID remapping:** Registry uses globally hashed IDs; runtime networks need local IDs. We pull /// the original local ID from `attr::ORIGINAL_NODE_ID` on each node and on each `NodeInput::Node` /// reference. References only point within the same network, so per-network lookup suffices. /// /// **Exports:** the storage-side `Vec` is sparse (`None` slots are valid). Compacted /// here into the runtime's dense `Vec` — slot stability is a storage-side concern. /// /// `metadata_path` is the owning-node chain naming *this* network (empty for the root). /// Walk the network reference graph (edges are `Implementation::Network` references between a /// network and the networks its nodes embed) and reject any cycle, so the recursive `convert_network` /// can't recurse forever and overflow the stack. Iterative DFS with an explicit stack and a gray set /// for the active path; a child already on the active path is a back edge, i.e. a cycle. fn detect_network_cycle(context: &ConversionContext, root: NetworkId) -> Result<(), ConversionError> { // Networks reachable from `root` that referenced networks, used by an embedded node, are pushed in // reverse so the natural processing order matches a recursive walk. `Enter`/`Leave` frames let us // maintain the gray (active-path) set with an explicit stack. enum Frame { Enter(NetworkId), Leave(NetworkId), } let mut stack = vec![Frame::Enter(root)]; let mut on_path: FxHashSet = FxHashSet::default(); let mut fully_explored: FxHashSet = FxHashSet::default(); while let Some(frame) = stack.pop() { match frame { Frame::Leave(network_id) => { on_path.remove(&network_id); fully_explored.insert(network_id); } Frame::Enter(network_id) => { if fully_explored.contains(&network_id) { continue; } if !on_path.insert(network_id) { return Err(ConversionError::CyclicNetwork(network_id)); } stack.push(Frame::Leave(network_id)); for &(_, node) in context.nodes_by_network.get(&network_id).map(Vec::as_slice).unwrap_or_default() { if let Implementation::Network(child) = node.implementation { stack.push(Frame::Enter(child)); } } } } } Ok(()) } fn convert_network( context: &ConversionContext, network_id: NetworkId, metadata_path: &[RuntimeNodeId], node_collector: &mut Option>, network_collector: &mut Option>, ) -> Result { let network = context.registry.networks.get(&network_id).ok_or(ConversionError::NetworkNotFound(network_id))?; if let Some(collector) = network_collector.as_mut() { collector.push(extract_network_metadata(&network.attributes, metadata_path, network_id)); } let mut nodes: FxHashMap = FxHashMap::default(); for &(global_id, node) in context.nodes_by_network.get(&network_id).map(Vec::as_slice).unwrap_or_default() { let local_id = node.attributes.get(node::ORIGINAL_NODE_ID).and_then(|v| v.value.as_u64()).unwrap_or(global_id.0); let runtime_id = RuntimeNodeId(local_id); if let Some(collector) = node_collector.as_mut() && let Some(entry) = extract_ui_metadata(node, metadata_path, runtime_id) { collector.push(entry); } let doc_node = convert_node(context, node, metadata_path, runtime_id, node_collector, network_collector)?; // Two storage nodes resolving to the same runtime ID would silently collapse into one on // insert, dropping a node from the reconstructed graph. if nodes.insert(runtime_id, doc_node).is_some() { return Err(ConversionError::DuplicateRuntimeNodeId { network: network_id, runtime_id: local_id, }); } } // Input attributes aren't round-tripped for exports — Reflection/Import inputs don't appear there. let empty_attrs = crate::Attributes::new(); let exports: Vec = network .exports .iter() .filter_map(|slot| slot.target.as_ref()) .map(|input| convert_input(context.registry, network_id, input, &empty_attrs)) .collect::, _>>()?; let scope_injections = read_scope_injections(context.registry, network_id, &network.attributes)?; Ok(NodeNetwork { exports, nodes, scope_injections, generated: false, }) } /// Rebuild a network's `scope_injections` from its serialized attribute blob, resolving each stored /// storage node ID back to its runtime-local ID. Mirrors `from_runtime::write_scope_injections`. fn read_scope_injections(registry: &Registry, network_id: NetworkId, attributes: &crate::Attributes) -> Result, ConversionError> { let Some(stored) = attributes.get_typed::>(network::SCOPE_INJECTIONS) else { return Ok(FxHashMap::default()); }; stored .into_iter() .map(|(key, (storage_id, ty))| { // The injection must point at a node in this same network, like any `NodeInput::Node`. let referenced = registry.node_instances.get(&storage_id).filter(|node| node.network == network_id); let Some(referenced) = referenced else { return Err(ConversionError::DanglingScopeInjection { network: network_id, key, referenced: storage_id, }); }; let local_id = referenced.attributes.get(node::ORIGINAL_NODE_ID).and_then(|v| v.value.as_u64()).unwrap_or(storage_id.0); Ok((key, (RuntimeNodeId(local_id), ty))) }) .collect() } /// Returns `None` when the node has no `ui::*` attributes at all so callers don't end up with /// empty entries for unconverted-from-runtime nodes. `input_metadata` is always sized to match /// `node.inputs.len()` for a strict slot-by-slot rebuild; empty slots use `InputMetadataEntry::default()`. fn extract_ui_metadata(node: &crate::Node, network_path: &[RuntimeNodeId], local_id: RuntimeNodeId) -> Option { let position: Option = node.attributes.get_typed(node::ui::POSITION); let is_layer = node.attributes.get_or(node::ui::IS_LAYER, false); let display_name: Option = node.attributes.get_typed(node::ui::DISPLAY_NAME); let locked = node.attributes.get_or(node::ui::LOCKED, false); let pinned = node.attributes.get_or(node::ui::PINNED, false); let output_names: Vec = node.attributes.get_or_default(node::ui::OUTPUT_NAMES); let input_metadata: Vec = node.inputs.iter().map(|slot| &slot.attributes).map(extract_input_metadata).collect(); let entry = NodeMetadataEntry { network_path: network_path.to_vec(), local_id, position, is_layer, display_name, locked, pinned, input_metadata, output_names, }; (!entry.is_empty()).then_some(entry) } fn extract_network_metadata(attributes: &crate::Attributes, network_path: &[RuntimeNodeId], network_id: NetworkId) -> NetworkMetadataEntry { NetworkMetadataEntry { network_path: network_path.to_vec(), network_id, reference: attributes.get_typed(node::ui::REFERENCE), } } /// Reassembles `input_data` by scanning every attribute under `ui::input_data::` and stripping the prefix. fn extract_input_metadata(attributes: &crate::Attributes) -> InputMetadataEntry { let input_data: HashMap = attributes .iter() .filter_map(|(key, value)| key.strip_prefix(node::input::ui::DATA_PREFIX).map(|sub_key| (sub_key.to_owned(), value.value.clone()))) .collect(); InputMetadataEntry { input_name: attributes.get_typed(node::input::ui::NAME), input_description: attributes.get_typed(node::input::ui::DESCRIPTION), widget_override: attributes.get_typed(node::input::ui::WIDGET_OVERRIDE), input_data, } } fn convert_node( context: &ConversionContext, node: &crate::Node, metadata_path: &[RuntimeNodeId], runtime_node_id: RuntimeNodeId, node_collector: &mut Option>, network_collector: &mut Option>, ) -> Result { let inputs = node .inputs .iter() .map(|slot| convert_input(context.registry, node.network, &slot.input, &slot.attributes)) .collect::, _>>()?; // Defaults must match `DocumentNode::default()` (and the `set_if_not_default` calls in `from_runtime`). Ok(DocumentNode { inputs, call_argument: node.attributes.get_or(node::CALL_ARGUMENT, concrete!(core_types::Context)), implementation: convert_implementation(context, &node.implementation, metadata_path, runtime_node_id, node_collector, network_collector)?, visible: node.attributes.get_or(node::VISIBLE, true), skip_deduplication: node.attributes.get_or(node::SKIP_DEDUPLICATION, false), // Regenerated during compilation; not stored. context_features: Default::default(), original_location: Default::default(), }) } fn convert_input(registry: &Registry, network_id: NetworkId, input: &NodeInput, input_attributes: &crate::Attributes) -> Result { Ok(match input { NodeInput::Node { id: node_id, index: output_index } => { let referenced = registry.node_instances.get(node_id).ok_or(ConversionError::NodeNotFound(*node_id))?; // Runtime references are local to one network. A cross-network reference would remap to a // local ID that doesn't exist in the current runtime network, so reject it. if referenced.network != network_id { return Err(ConversionError::CrossNetworkReference { network: network_id, referenced: *node_id, }); } let local_id = referenced.attributes.get(node::ORIGINAL_NODE_ID).and_then(|v| v.value.as_u64()).unwrap_or(node_id.0); GraphCraftNodeInput::Node { node_id: RuntimeNodeId(local_id), output_index: *output_index as usize, } } NodeInput::Value { value, exposed } => { let tagged_value: TaggedValue = serde_json::from_value(value.clone()).map_err(|e| ConversionError::DeserializationError(format!("TaggedValue: {e:?}")))?; GraphCraftNodeInput::Value { tagged_value: MemoHash::new(tagged_value), exposed: *exposed, } } NodeInput::Scope(s) => GraphCraftNodeInput::Scope(s.clone()), NodeInput::Import { index: import_idx } => GraphCraftNodeInput::Import { import_type: input_attributes.get_or(node::input::IMPORT_TYPE, Type::Generic(Cow::Borrowed("T"))), import_index: *import_idx as usize, }, NodeInput::Reflection => GraphCraftNodeInput::Reflection( input_attributes .get_typed(node::REFLECTION_METADATA) .ok_or_else(|| ConversionError::DeserializationError("Missing reflection_metadata in input_attributes".to_string()))?, ), NodeInput::Other => return Err(ConversionError::DeserializationError("Cannot convert NodeInput::Other to a runtime input".to_string())), }) } fn convert_implementation( context: &ConversionContext, implementation: &Implementation, parent_metadata_path: &[RuntimeNodeId], owning_runtime_id: RuntimeNodeId, node_collector: &mut Option>, network_collector: &mut Option>, ) -> Result { Ok(match implementation { Implementation::ProtoNode(id) => { let proto = context.declarations.get(id).ok_or(ConversionError::DeclarationNotFound(*id))?; DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::with_owned_string(proto.identifier.clone())) } Implementation::Network(net_id) => { let mut child_path = Vec::with_capacity(parent_metadata_path.len() + 1); child_path.extend_from_slice(parent_metadata_path); child_path.push(owning_runtime_id); DocumentNodeImplementation::Network(convert_network(context, *net_id, &child_path, node_collector, network_collector)?) } }) }