mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-20 03:18:06 +08:00
Deprecate automatic composition (#3088)
* Make manual_compositon non optional and rename to call_argument * Fix clippy warnings * Remove automatic composition compiler infrastructure * Implement document migration * Fix tests * Fix compilation on web * Fix doble number test * Remove extra parens * Cleanup * Update demo artwork * Remove last compose node mention * Remove last mention of manual composition
This commit is contained in:
@@ -37,11 +37,7 @@ impl core::fmt::Display for ProtoNetwork {
|
||||
|
||||
f.write_str(&"\t".repeat(indent + 1))?;
|
||||
f.write_str("Input: ")?;
|
||||
match &node.input {
|
||||
ProtoNodeInput::None => f.write_str("None")?,
|
||||
ProtoNodeInput::ManualComposition(ty) => f.write_fmt(format_args!("Manual Composition (type = {ty:?})"))?,
|
||||
ProtoNodeInput::Node(_) => f.write_str("Node")?,
|
||||
}
|
||||
f.write_fmt(format_args!("Call Argument (type = {:?})", node.call_argument))?;
|
||||
f.write_str("\n")?;
|
||||
|
||||
match &node.construction_args {
|
||||
@@ -132,7 +128,7 @@ impl ConstructionArgs {
|
||||
/// 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.
|
||||
pub struct ProtoNode {
|
||||
pub construction_args: ConstructionArgs,
|
||||
pub input: ProtoNodeInput,
|
||||
pub call_argument: Type,
|
||||
pub identifier: ProtoNodeIdentifier,
|
||||
pub original_location: OriginalLocation,
|
||||
pub skip_deduplication: bool,
|
||||
@@ -143,37 +139,13 @@ impl Default for ProtoNode {
|
||||
Self {
|
||||
identifier: ProtoNodeIdentifier::new("graphene_core::ops::IdentityNode"),
|
||||
construction_args: ConstructionArgs::Value(value::TaggedValue::U32(0).into()),
|
||||
input: ProtoNodeInput::None,
|
||||
call_argument: concrete!(()),
|
||||
original_location: OriginalLocation::default(),
|
||||
skip_deduplication: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Similar to the document node's [`crate::document::NodeInput`].
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ProtoNodeInput {
|
||||
/// This input will be converted to `()` as the call argument.
|
||||
None,
|
||||
/// A ManualComposition input represents an input that opts out of being resolved through the `ComposeNode`, which first runs the previous (upstream) node, then passes that evaluated
|
||||
/// result to this node. Instead, ManualComposition lets this node actually consume the provided input instead of passing it to its predecessor.
|
||||
///
|
||||
/// Say we have the network `a -> b -> c` where `c` is the output node and `a` is the input node.
|
||||
/// We would expect `a` to get input from the network, `b` to get input from `a`, and `c` to get input from `b`.
|
||||
/// This could be represented as `f(x) = c(b(a(x)))`. `a` is run with input `x` from the network. `b` is run with input from `a`. `c` is run with input from `b`.
|
||||
///
|
||||
/// However if `b`'s input is using manual composition, this means it would instead be `f(x) = c(b(x))`. This means that `b` actually gets input from the network, and `a` is not automatically
|
||||
/// executed as it would be using the default ComposeNode flow. Now `b` can use its own logic to decide when or if it wants to run `a` and how to use its output. For example, the CacheNode can
|
||||
/// look up `x` in its cache and return the result, or otherwise call `a`, cache the result, and return it.
|
||||
ManualComposition(Type),
|
||||
/// The previous node where automatic (not manual) composition occurs when compiled. The entire network, of which the node is the output, is fed as input.
|
||||
///
|
||||
/// Grayscale example:
|
||||
///
|
||||
/// We're interested in receiving an input of the desaturated image data which has been fed through a grayscale filter.
|
||||
Node(NodeId),
|
||||
}
|
||||
|
||||
impl ProtoNode {
|
||||
/// A stable node ID is a hash of a node that should stay constant. This is used in order to remove duplicates from the graph.
|
||||
/// In the case of `skip_deduplication`, the `document_node_path` is also hashed in order to avoid duplicate monitor nodes from being removed (which would make it impossible to load thumbnails).
|
||||
@@ -187,14 +159,8 @@ impl ProtoNode {
|
||||
self.original_location.path.hash(&mut hasher);
|
||||
}
|
||||
|
||||
std::mem::discriminant(&self.input).hash(&mut hasher);
|
||||
match self.input {
|
||||
ProtoNodeInput::None => (),
|
||||
ProtoNodeInput::ManualComposition(ref ty) => {
|
||||
ty.hash(&mut hasher);
|
||||
}
|
||||
ProtoNodeInput::Node(id) => id.hash(&mut hasher),
|
||||
};
|
||||
std::mem::discriminant(&self.call_argument).hash(&mut hasher);
|
||||
self.call_argument.hash(&mut hasher);
|
||||
|
||||
Some(NodeId(hasher.finish()))
|
||||
}
|
||||
@@ -208,7 +174,7 @@ impl ProtoNode {
|
||||
Self {
|
||||
identifier: ProtoNodeIdentifier::new("graphene_core::value::ClonedNode"),
|
||||
construction_args: value,
|
||||
input: ProtoNodeInput::ManualComposition(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
original_location: OriginalLocation {
|
||||
path: Some(path),
|
||||
inputs_exposed: vec![false; inputs_exposed],
|
||||
@@ -221,10 +187,6 @@ impl ProtoNode {
|
||||
/// Converts all references to other node IDs into new IDs by running the specified function on them.
|
||||
/// This can be used when changing the IDs of the nodes, for example in the case of generating stable IDs.
|
||||
pub fn map_ids(&mut self, f: impl Fn(NodeId) -> NodeId) {
|
||||
if let ProtoNodeInput::Node(id) = self.input {
|
||||
self.input = ProtoNodeInput::Node(f(id))
|
||||
}
|
||||
|
||||
if let ConstructionArgs::Nodes(ids) = &mut self.construction_args {
|
||||
ids.iter_mut().for_each(|id| *id = f(*id));
|
||||
}
|
||||
@@ -269,11 +231,6 @@ impl ProtoNetwork {
|
||||
pub fn collect_outwards_edges(&self) -> HashMap<NodeId, Vec<NodeId>> {
|
||||
let mut edges: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
|
||||
for (id, node) in &self.nodes {
|
||||
if let ProtoNodeInput::Node(ref_id) = &node.input {
|
||||
self.check_ref(ref_id, id);
|
||||
edges.entry(*ref_id).or_default().push(*id)
|
||||
}
|
||||
|
||||
if let ConstructionArgs::Nodes(ref_nodes) = &node.construction_args {
|
||||
for ref_id in ref_nodes {
|
||||
self.check_ref(ref_id, id);
|
||||
@@ -304,11 +261,6 @@ impl ProtoNetwork {
|
||||
pub fn collect_inwards_edges(&self) -> HashMap<NodeId, Vec<NodeId>> {
|
||||
let mut edges: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
|
||||
for (id, node) in &self.nodes {
|
||||
if let ProtoNodeInput::Node(ref_id) = &node.input {
|
||||
self.check_ref(ref_id, id);
|
||||
edges.entry(*id).or_default().push(*ref_id)
|
||||
}
|
||||
|
||||
if let ConstructionArgs::Nodes(ref_nodes) = &node.construction_args {
|
||||
for ref_id in ref_nodes {
|
||||
self.check_ref(ref_id, id);
|
||||
@@ -326,10 +278,6 @@ impl ProtoNetwork {
|
||||
let mut inwards_edges = vec![Vec::new(); self.nodes.len()];
|
||||
for (node_id, node) in &self.nodes {
|
||||
let node_index = id_map[node_id];
|
||||
if let ProtoNodeInput::Node(ref_id) = &node.input {
|
||||
self.check_ref(ref_id, &NodeId(node_index as u64));
|
||||
inwards_edges[node_index].push(id_map[ref_id]);
|
||||
}
|
||||
|
||||
if let ConstructionArgs::Nodes(ref_nodes) = &node.construction_args {
|
||||
for ref_id in ref_nodes {
|
||||
@@ -342,70 +290,31 @@ impl ProtoNetwork {
|
||||
(inwards_edges, id_map)
|
||||
}
|
||||
|
||||
/// Inserts a [`structural::ComposeNode`] for each node that has a [`ProtoNodeInput::Node`]. The compose node evaluates the first node, and then sends the result into the second node.
|
||||
/// Performs topological sort and reorders ids.
|
||||
pub fn resolve_inputs(&mut self) -> Result<(), String> {
|
||||
// Perform topological sort once
|
||||
self.reorder_ids()?;
|
||||
|
||||
let max_id = self.nodes.len() as u64 - 1;
|
||||
|
||||
// Collect outward edges once
|
||||
let outwards_edges = self.collect_outwards_edges();
|
||||
|
||||
// Iterate over nodes in topological order
|
||||
for node_id in 0..=max_id {
|
||||
let node_id = NodeId(node_id);
|
||||
|
||||
let (_, node) = &mut self.nodes[node_id.0 as usize];
|
||||
|
||||
if let ProtoNodeInput::Node(input_node_id) = node.input {
|
||||
// Create a new node that composes the current node and its input node
|
||||
let compose_node_id = NodeId(self.nodes.len() as u64);
|
||||
|
||||
let (_, input_node_id_proto) = &self.nodes[input_node_id.0 as usize];
|
||||
|
||||
let input = input_node_id_proto.input.clone();
|
||||
|
||||
let mut path = input_node_id_proto.original_location.path.clone();
|
||||
if let Some(path) = &mut path {
|
||||
path.push(node_id);
|
||||
}
|
||||
|
||||
self.nodes.push((
|
||||
compose_node_id,
|
||||
ProtoNode {
|
||||
identifier: ProtoNodeIdentifier::new("graphene_core::structural::ComposeNode"),
|
||||
construction_args: ConstructionArgs::Nodes(vec![input_node_id, node_id]),
|
||||
input,
|
||||
original_location: OriginalLocation { path, ..Default::default() },
|
||||
skip_deduplication: false,
|
||||
},
|
||||
));
|
||||
|
||||
self.replace_node_id(&outwards_edges, node_id, compose_node_id);
|
||||
}
|
||||
}
|
||||
self.reorder_ids()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update all of the references to a node ID in the graph with a new ID named `compose_node_id`.
|
||||
fn replace_node_id(&mut self, outwards_edges: &HashMap<NodeId, Vec<NodeId>>, node_id: NodeId, compose_node_id: NodeId) {
|
||||
// Update references in other nodes to use the new compose node
|
||||
/// Update all of the references to a node ID in the graph with a new ID named `replacement_node_id`.
|
||||
fn replace_node_id(&mut self, outwards_edges: &HashMap<NodeId, Vec<NodeId>>, node_id: NodeId, replacement_node_id: NodeId) {
|
||||
// Update references in other nodes to use the new node
|
||||
if let Some(referring_nodes) = outwards_edges.get(&node_id) {
|
||||
for &referring_node_id in referring_nodes {
|
||||
let (_, referring_node) = &mut self.nodes[referring_node_id.0 as usize];
|
||||
referring_node.map_ids(|id| if id == node_id { compose_node_id } else { id })
|
||||
referring_node.map_ids(|id| if id == node_id { replacement_node_id } else { id })
|
||||
}
|
||||
}
|
||||
|
||||
if self.output == node_id {
|
||||
self.output = compose_node_id;
|
||||
self.output = replacement_node_id;
|
||||
}
|
||||
|
||||
self.inputs.iter_mut().for_each(|id| {
|
||||
if *id == node_id {
|
||||
*id = compose_node_id;
|
||||
*id = replacement_node_id;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -636,7 +545,6 @@ impl TypingContext {
|
||||
let inputs = match node.construction_args {
|
||||
// If the node has a value input we can infer the return type from it
|
||||
ConstructionArgs::Value(ref v) => {
|
||||
assert!(matches!(node.input, ProtoNodeInput::None) || matches!(node.input, ProtoNodeInput::ManualComposition(ref x) if x == &concrete!(Context)));
|
||||
// TODO: This should return a reference to the value
|
||||
let types = NodeIOTypes::new(concrete!(Context), Type::Future(Box::new(v.ty())), vec![]);
|
||||
self.inferred.insert(node_id, types.clone());
|
||||
@@ -656,16 +564,7 @@ impl TypingContext {
|
||||
};
|
||||
|
||||
// Get the node input type from the proto node declaration
|
||||
// TODO: When removing automatic composition, rename this to just `call_argument`
|
||||
let primary_input_or_call_argument = match node.input {
|
||||
ProtoNodeInput::None => concrete!(()),
|
||||
ProtoNodeInput::ManualComposition(ref ty) => ty.clone(),
|
||||
ProtoNodeInput::Node(id) => {
|
||||
let input = self.inferred.get(&id).ok_or_else(|| vec![GraphError::new(node, GraphErrorType::InputNodeNotFound(id))])?;
|
||||
input.return_value.clone()
|
||||
}
|
||||
};
|
||||
let using_manual_composition = matches!(node.input, ProtoNodeInput::ManualComposition(_) | ProtoNodeInput::None);
|
||||
let call_argument = &node.call_argument;
|
||||
let impls = self.lookup.get(&node.identifier).ok_or_else(|| vec![GraphError::new(node, GraphErrorType::NoImplementations)])?;
|
||||
|
||||
if let Some(index) = inputs.iter().position(|p| {
|
||||
@@ -707,7 +606,7 @@ 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, &primary_input_or_call_argument) && inputs.iter().zip(node_io.inputs.iter()).all(|(p1, p2)| valid_type(p1, p2)))
|
||||
.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
|
||||
@@ -716,7 +615,7 @@ impl TypingContext {
|
||||
.map(|node_io| {
|
||||
let generics_lookup: Result<HashMap<_, _>, _> = collect_generics(node_io)
|
||||
.iter()
|
||||
.map(|generic| check_generic(node_io, &primary_input_or_call_argument, &inputs, generic).map(|x| (generic.to_string(), x)))
|
||||
.map(|generic| check_generic(node_io, call_argument, &inputs, generic).map(|x| (generic.to_string(), x)))
|
||||
.collect();
|
||||
|
||||
generics_lookup.map(|generics_lookup| {
|
||||
@@ -736,7 +635,7 @@ impl TypingContext {
|
||||
let mut best_errors = usize::MAX;
|
||||
let mut error_inputs = Vec::new();
|
||||
for node_io in impls.keys() {
|
||||
let current_errors = [&primary_input_or_call_argument]
|
||||
let current_errors = [call_argument]
|
||||
.into_iter()
|
||||
.chain(&inputs)
|
||||
.cloned()
|
||||
@@ -745,7 +644,6 @@ impl TypingContext {
|
||||
.filter(|(_, (p1, p2))| !valid_type(p1, p2))
|
||||
.map(|(index, ty)| {
|
||||
let i = node.original_location.inputs(index).min_by_key(|s| s.node.len()).map(|s| s.index).unwrap_or(index);
|
||||
let i = if using_manual_composition { i } else { i + 1 };
|
||||
(i, ty)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -757,15 +655,11 @@ impl TypingContext {
|
||||
error_inputs.push(current_errors);
|
||||
}
|
||||
}
|
||||
let inputs = [&primary_input_or_call_argument]
|
||||
let inputs = [call_argument]
|
||||
.into_iter()
|
||||
.chain(&inputs)
|
||||
.enumerate()
|
||||
// TODO: Make the following line's if statement conditional on being a call argument or primary input
|
||||
.filter_map(|(i, t)| {
|
||||
let i = if using_manual_composition { i } else { i + 1 };
|
||||
if i == 0 { None } else { Some(format!("• Input {i}: {t}")) }
|
||||
})
|
||||
.filter_map(|(i, t)| if i == 0 { None } else { Some(format!("• Input {i}: {t}")) })
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
Err(vec![GraphError::new(node, GraphErrorType::InvalidImplementations { inputs, error_inputs })])
|
||||
@@ -792,13 +686,13 @@ impl TypingContext {
|
||||
return Ok(node_io.clone());
|
||||
}
|
||||
}
|
||||
let inputs = [&primary_input_or_call_argument].into_iter().chain(&inputs).map(|t| t.to_string()).collect::<Vec<_>>().join(", ");
|
||||
let inputs = [call_argument].into_iter().chain(&inputs).map(|t| t.to_string()).collect::<Vec<_>>().join(", ");
|
||||
let valid = valid_output_types.into_iter().cloned().collect();
|
||||
Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })])
|
||||
}
|
||||
|
||||
_ => {
|
||||
let inputs = [&primary_input_or_call_argument].into_iter().chain(&inputs).map(|t| t.to_string()).collect::<Vec<_>>().join(", ");
|
||||
let inputs = [call_argument].into_iter().chain(&inputs).map(|t| t.to_string()).collect::<Vec<_>>().join(", ");
|
||||
let valid = valid_output_types.into_iter().cloned().collect();
|
||||
Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })])
|
||||
}
|
||||
@@ -857,7 +751,7 @@ fn replace_generics(types: &mut NodeIOTypes, lookup: &HashMap<String, Type>) {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::proto::{ConstructionArgs, ProtoNetwork, ProtoNode, ProtoNodeInput};
|
||||
use crate::proto::{ConstructionArgs, ProtoNetwork, ProtoNode};
|
||||
|
||||
#[test]
|
||||
fn topological_sort() {
|
||||
@@ -904,16 +798,6 @@ mod test {
|
||||
assert_eq!(ids, vec![NodeId(0), NodeId(1), NodeId(2), NodeId(3)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_resolution() {
|
||||
let mut construction_network = test_network();
|
||||
construction_network.resolve_inputs().expect("Error when calling 'resolve_inputs' on 'construction_network.");
|
||||
println!("{construction_network:#?}");
|
||||
assert_eq!(construction_network.nodes[0].1.identifier.name.as_ref(), "value");
|
||||
assert_eq!(construction_network.nodes.len(), 6);
|
||||
assert_eq!(construction_network.nodes[5].1.construction_args, ConstructionArgs::Nodes(vec![(NodeId(3)), (NodeId(4))]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stable_node_id_generation() {
|
||||
let mut construction_network = test_network();
|
||||
@@ -923,14 +807,7 @@ mod test {
|
||||
let ids: Vec<_> = construction_network.nodes.iter().map(|(id, _)| *id).collect();
|
||||
assert_eq!(
|
||||
ids,
|
||||
vec![
|
||||
NodeId(16997244687192517417),
|
||||
NodeId(7064939117677356327),
|
||||
NodeId(10605314923684175783),
|
||||
NodeId(6550828352538976747),
|
||||
NodeId(277515424782779520),
|
||||
NodeId(8855802688584342558)
|
||||
]
|
||||
vec![NodeId(13743208144182721472), NodeId(4607569396187877965), NodeId(16950305885390329527), NodeId(15151181027373658932)]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -943,8 +820,8 @@ mod test {
|
||||
NodeId(7),
|
||||
ProtoNode {
|
||||
identifier: "id".into(),
|
||||
input: ProtoNodeInput::Node(NodeId(11)),
|
||||
construction_args: ConstructionArgs::Nodes(vec![]),
|
||||
call_argument: concrete!(()),
|
||||
construction_args: ConstructionArgs::Nodes(vec![NodeId(11)]),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
@@ -952,8 +829,8 @@ mod test {
|
||||
NodeId(1),
|
||||
ProtoNode {
|
||||
identifier: "id".into(),
|
||||
input: ProtoNodeInput::Node(NodeId(11)),
|
||||
construction_args: ConstructionArgs::Nodes(vec![]),
|
||||
call_argument: concrete!(()),
|
||||
construction_args: ConstructionArgs::Nodes(vec![NodeId(11)]),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
@@ -961,7 +838,7 @@ mod test {
|
||||
NodeId(10),
|
||||
ProtoNode {
|
||||
identifier: "cons".into(),
|
||||
input: ProtoNodeInput::ManualComposition(concrete!(u32)),
|
||||
call_argument: concrete!(u32),
|
||||
construction_args: ConstructionArgs::Nodes(vec![NodeId(14)]),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -970,8 +847,8 @@ mod test {
|
||||
NodeId(11),
|
||||
ProtoNode {
|
||||
identifier: "add".into(),
|
||||
input: ProtoNodeInput::Node(NodeId(10)),
|
||||
construction_args: ConstructionArgs::Nodes(vec![]),
|
||||
call_argument: concrete!(()),
|
||||
construction_args: ConstructionArgs::Nodes(vec![NodeId(10)]),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
@@ -979,7 +856,7 @@ mod test {
|
||||
NodeId(14),
|
||||
ProtoNode {
|
||||
identifier: "value".into(),
|
||||
input: ProtoNodeInput::None,
|
||||
call_argument: concrete!(()),
|
||||
construction_args: ConstructionArgs::Value(value::TaggedValue::U32(2).into()),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -999,8 +876,8 @@ mod test {
|
||||
NodeId(1),
|
||||
ProtoNode {
|
||||
identifier: "id".into(),
|
||||
input: ProtoNodeInput::Node(NodeId(2)),
|
||||
construction_args: ConstructionArgs::Nodes(vec![]),
|
||||
call_argument: concrete!(()),
|
||||
construction_args: ConstructionArgs::Nodes(vec![NodeId(2)]),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
@@ -1008,8 +885,8 @@ mod test {
|
||||
NodeId(2),
|
||||
ProtoNode {
|
||||
identifier: "id".into(),
|
||||
input: ProtoNodeInput::Node(NodeId(1)),
|
||||
construction_args: ConstructionArgs::Nodes(vec![]),
|
||||
call_argument: concrete!(()),
|
||||
construction_args: ConstructionArgs::Nodes(vec![NodeId(1)]),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user