mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-20 11:28:30 +08:00
Add type checking to the node graph (#1025)
* Implement type inference Add type hints to node trait Add type annotation infrastructure Refactor type ascription infrastructure Run cargo fix Insert infer types stub Remove types from node identifier * Implement covariance * Disable rejection of generic inputs + parameters * Fix lints * Extend type checking to cover Network inputs * Implement generic specialization * Relax covariance rules * Fix type annotations for TypErasedComposeNode * Fix type checking errors * Keep connection information during node resolution * Fix TypeDescriptor PartialEq implementation * Apply review suggestions * Add documentation to type inference * Add Imaginate node to document node types * Fix whitespace in macros * Add types to imaginate node * Fix type declaration for imaginate node + add console logging * Use fully qualified type names as fallback during comparison --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
@@ -4,87 +4,16 @@ use std::hash::Hash;
|
||||
|
||||
use crate::document::value;
|
||||
use crate::document::NodeId;
|
||||
use dyn_any::DynAny;
|
||||
use graphene_core::*;
|
||||
use std::pin::Pin;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! concrete {
|
||||
($type:expr) => {
|
||||
Type::Concrete(std::borrow::Cow::Borrowed($type))
|
||||
};
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! generic {
|
||||
($type:expr) => {
|
||||
Type::Generic(std::borrow::Cow::Borrowed($type))
|
||||
};
|
||||
}
|
||||
pub type Any<'n> = Box<dyn DynAny<'n> + 'n>;
|
||||
pub type TypeErasedNode<'n> = dyn for<'i> NodeIO<'i, Any<'i>, Output = Any<'i>> + 'n + Send + Sync;
|
||||
pub type TypeErasedPinnedRef<'n> = Pin<&'n (dyn for<'i> NodeIO<'i, Any<'i>, Output = Any<'i>> + 'n + Send + Sync)>;
|
||||
pub type TypeErasedPinned<'n> = Pin<Box<dyn for<'i> NodeIO<'i, Any<'i>, Output = Any<'i>> + 'n + Send + Sync>>;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, specta::Type)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct NodeIdentifier {
|
||||
pub name: std::borrow::Cow<'static, str>,
|
||||
pub types: std::borrow::Cow<'static, [Type]>,
|
||||
}
|
||||
|
||||
impl NodeIdentifier {
|
||||
pub fn fully_qualified_name(&self) -> String {
|
||||
let mut name = String::new();
|
||||
name.push_str(self.name.as_ref());
|
||||
name.push('<');
|
||||
for t in self.types.as_ref() {
|
||||
name.push_str(t.to_string().as_str());
|
||||
name.push_str(", ");
|
||||
}
|
||||
name.pop();
|
||||
name.pop();
|
||||
name.push('>');
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, specta::Type)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Type {
|
||||
Generic(std::borrow::Cow<'static, str>),
|
||||
Concrete(std::borrow::Cow<'static, str>),
|
||||
}
|
||||
|
||||
impl From<&'static str> for Type {
|
||||
fn from(s: &'static str) -> Self {
|
||||
Type::Concrete(std::borrow::Cow::Borrowed(s))
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for Type {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Type::Generic(name) => write!(f, "{}", name),
|
||||
Type::Concrete(name) => write!(f, "{}", name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Type {
|
||||
pub const fn from_str(concrete: &'static str) -> Self {
|
||||
Type::Concrete(std::borrow::Cow::Borrowed(concrete))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for NodeIdentifier {
|
||||
fn from(s: &'static str) -> Self {
|
||||
NodeIdentifier {
|
||||
name: std::borrow::Cow::Borrowed(s),
|
||||
types: std::borrow::Cow::Borrowed(&[]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeIdentifier {
|
||||
pub const fn new(name: &'static str, types: &'static [Type]) -> Self {
|
||||
NodeIdentifier {
|
||||
name: std::borrow::Cow::Borrowed(name),
|
||||
types: std::borrow::Cow::Borrowed(types),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub type NodeConstructor = for<'a> fn(Vec<TypeErasedPinnedRef<'static>>) -> TypeErasedPinned<'static>;
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct ProtoNetwork {
|
||||
@@ -140,11 +69,10 @@ pub struct ProtoNode {
|
||||
pub identifier: NodeIdentifier,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum ProtoNodeInput {
|
||||
None,
|
||||
#[default]
|
||||
Network,
|
||||
Network(Type),
|
||||
Node(NodeId),
|
||||
}
|
||||
|
||||
@@ -161,11 +89,14 @@ impl ProtoNode {
|
||||
pub fn stable_node_id(&self) -> Option<NodeId> {
|
||||
use std::hash::Hasher;
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
self.identifier.fully_qualified_name().hash(&mut hasher);
|
||||
self.identifier.name.hash(&mut hasher);
|
||||
self.construction_args.hash(&mut hasher);
|
||||
match self.input {
|
||||
ProtoNodeInput::None => "none".hash(&mut hasher),
|
||||
ProtoNodeInput::Network => "network".hash(&mut hasher),
|
||||
ProtoNodeInput::Network(ref ty) => {
|
||||
"network".hash(&mut hasher);
|
||||
ty.hash(&mut hasher);
|
||||
}
|
||||
ProtoNodeInput::Node(id) => id.hash(&mut hasher),
|
||||
};
|
||||
Some(hasher.finish() as NodeId)
|
||||
@@ -173,7 +104,7 @@ impl ProtoNode {
|
||||
|
||||
pub fn value(value: ConstructionArgs) -> Self {
|
||||
Self {
|
||||
identifier: NodeIdentifier::new("graphene_core::value::ValueNode", &[Type::Generic(Cow::Borrowed("T"))]),
|
||||
identifier: NodeIdentifier::new("graphene_core::value::ValueNode"),
|
||||
construction_args: value,
|
||||
input: ProtoNodeInput::None,
|
||||
}
|
||||
@@ -260,20 +191,22 @@ impl ProtoNetwork {
|
||||
}
|
||||
|
||||
pub fn resolve_inputs(&mut self) {
|
||||
while !self.resolve_inputs_impl() {}
|
||||
let mut resolved = HashSet::new();
|
||||
while !self.resolve_inputs_impl(&mut resolved) {}
|
||||
}
|
||||
fn resolve_inputs_impl(&mut self) -> bool {
|
||||
fn resolve_inputs_impl(&mut self, resolved: &mut HashSet<NodeId>) -> bool {
|
||||
self.reorder_ids();
|
||||
|
||||
let mut lookup = self.nodes.iter().map(|(id, _)| (*id, *id)).collect::<HashMap<_, _>>();
|
||||
let compose_node_id = self.nodes.len() as NodeId;
|
||||
let inputs = self.nodes.iter().map(|(_, node)| node.input).collect::<Vec<_>>();
|
||||
let inputs = self.nodes.iter().map(|(_, node)| node.input.clone()).collect::<Vec<_>>();
|
||||
|
||||
if let Some((input_node, id, input)) = self.nodes.iter_mut().find_map(|(id, node)| {
|
||||
let resolved_lookup = resolved.clone();
|
||||
if let Some((input_node, id, input)) = self.nodes.iter_mut().filter(|(id, _)| !resolved_lookup.contains(id)).find_map(|(id, node)| {
|
||||
if let ProtoNodeInput::Node(input_node) = node.input {
|
||||
node.input = ProtoNodeInput::None;
|
||||
resolved.insert(*id);
|
||||
let pre_node_input = inputs.get(input_node as usize).expect("input node should exist");
|
||||
Some((input_node, *id, *pre_node_input))
|
||||
Some((input_node, *id, pre_node_input.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -283,7 +216,7 @@ impl ProtoNetwork {
|
||||
self.nodes.push((
|
||||
compose_node_id,
|
||||
ProtoNode {
|
||||
identifier: NodeIdentifier::new("graphene_core::structural::ComposeNode<_, _, _>", &[generic!("T"), generic!("U")]),
|
||||
identifier: NodeIdentifier::new("graphene_core::structural::ComposeNode<_, _, _>"),
|
||||
construction_args: ConstructionArgs::Nodes(vec![input_node, id]),
|
||||
input,
|
||||
},
|
||||
@@ -373,6 +306,172 @@ impl ProtoNetwork {
|
||||
}
|
||||
}
|
||||
|
||||
/// The `TypingContext` is used to store the types of the nodes indexed by their stable node id.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct TypingContext {
|
||||
lookup: Cow<'static, HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>>>,
|
||||
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<NodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>>) -> Self {
|
||||
Self {
|
||||
lookup: Cow::Borrowed(lookup),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the `TypingContext` wtih 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.
|
||||
pub fn update(&mut self, network: &ProtoNetwork) -> Result<(), String> {
|
||||
for (id, node) in network.nodes.iter() {
|
||||
self.infer(*id, node)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the node constructor for a given node id.
|
||||
pub fn constructor(&self, node_id: NodeId) -> Option<NodeConstructor> {
|
||||
self.constructor.get(&node_id).copied()
|
||||
}
|
||||
|
||||
/// Returns the inferred types for a given node id.
|
||||
pub fn infer(&mut self, node_id: NodeId, node: &ProtoNode) -> Result<NodeIOTypes, String> {
|
||||
let identifier = node.identifier.name.clone();
|
||||
|
||||
// Return the inferred type if it is already known
|
||||
if let Some(infered) = self.inferred.get(&node_id) {
|
||||
return Ok(infered.clone());
|
||||
}
|
||||
|
||||
let parameters = match node.construction_args {
|
||||
// If the node has a value parameter we can infer the return type from it
|
||||
ConstructionArgs::Value(ref v) => {
|
||||
assert!(matches!(node.input, ProtoNodeInput::None));
|
||||
let types = NodeIOTypes::new(concrete!(()), v.ty(), vec![]);
|
||||
self.inferred.insert(node_id, types.clone());
|
||||
return Ok(types);
|
||||
}
|
||||
// If the node has nodes as parameters we can infer the types from the node outputs
|
||||
ConstructionArgs::Nodes(ref nodes) => nodes
|
||||
.iter()
|
||||
.map(|id| {
|
||||
self.inferred
|
||||
.get(id)
|
||||
.ok_or(format!("Inferring type of {node_id} depends on {id} which is not present in the typing context"))
|
||||
.map(|node| node.output.clone())
|
||||
})
|
||||
.collect::<Result<Vec<Type>, String>>()?,
|
||||
};
|
||||
|
||||
// Get the node input type from the proto node declaration
|
||||
let input = match node.input {
|
||||
ProtoNodeInput::None => concrete!(()),
|
||||
ProtoNodeInput::Network(ref ty) => ty.clone(),
|
||||
ProtoNodeInput::Node(id) => {
|
||||
let input = self
|
||||
.inferred
|
||||
.get(&id)
|
||||
.ok_or(format!("Inferring type of {node_id} depends on {id} which is not present in the typing context"))?;
|
||||
input.output.clone()
|
||||
}
|
||||
};
|
||||
let impls = self.lookup.get(&node.identifier).ok_or(format!("No implementations found for {:?}", node.identifier))?;
|
||||
|
||||
if matches!(input, Type::Generic(_)) {
|
||||
return Err(format!("Generic types are not supported as inputs yet {:?} occured in {:?}", &input, node.identifier));
|
||||
}
|
||||
if parameters.iter().any(|p| matches!(p, Type::Generic(_))) {
|
||||
return Err(format!("Generic types are not supported in parameters: {:?} occured in {:?}", parameters, node.identifier));
|
||||
}
|
||||
let covariant = |output, input| match (&output, &input) {
|
||||
(Type::Concrete(t1), Type::Concrete(t2)) => t1 == t2,
|
||||
(Type::Concrete(_), Type::Generic(_)) => true,
|
||||
// TODO: relax this requirement when allowing generic types as inputs
|
||||
(Type::Generic(_), _) => false,
|
||||
};
|
||||
|
||||
// List of all implementations that match the input and parameter types
|
||||
let valid_output_types = impls
|
||||
.keys()
|
||||
.filter(|node_io| covariant(input.clone(), node_io.input.clone()) && parameters.iter().zip(node_io.parameters.iter()).all(|(p1, p2)| covariant(p1.clone(), p2.clone())))
|
||||
.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| {
|
||||
collect_generics(node_io)
|
||||
.iter()
|
||||
.try_for_each(|generic| check_generic(node_io, &input, ¶meters, generic).map(|_| ()))
|
||||
.map(|_| {
|
||||
if let Type::Generic(out) = &node_io.output {
|
||||
((*node_io).clone(), check_generic(node_io, &input, ¶meters, out).unwrap())
|
||||
} else {
|
||||
((*node_io).clone(), node_io.output.clone())
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Collect all substitutions that are valid
|
||||
let valid_impls = substitution_results.iter().filter_map(|result| result.as_ref().ok()).collect::<Vec<_>>();
|
||||
|
||||
match valid_impls.as_slice() {
|
||||
[] => {
|
||||
dbg!(&self.inferred);
|
||||
Err(format!(
|
||||
"No implementations found for {identifier} with \ninput: {input:?} and \nparameters: {parameters:?}.\nOther Implementations found: {:?}",
|
||||
impls,
|
||||
))
|
||||
}
|
||||
[(org_nio, output)] => {
|
||||
let node_io = NodeIOTypes::new(input, (*output).clone(), parameters);
|
||||
|
||||
// Save the inferred type
|
||||
self.inferred.insert(node_id, node_io.clone());
|
||||
self.constructor.insert(node_id, impls[org_nio]);
|
||||
Ok(node_io)
|
||||
}
|
||||
_ => Err(format!(
|
||||
"Multiple implementations found for {identifier} with input {input:?} and parameters {parameters:?} (valid types: {valid_output_types:?}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a list of all generic types used in the node
|
||||
fn collect_generics(types: &NodeIOTypes) -> Vec<Cow<'static, str>> {
|
||||
let inputs = [&types.input].into_iter().chain(types.parameters.iter());
|
||||
let mut generics = inputs
|
||||
.filter_map(|t| match t {
|
||||
Type::Generic(out) => Some(out.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if let Type::Generic(out) = &types.output {
|
||||
generics.push(out.clone());
|
||||
}
|
||||
generics.dedup();
|
||||
generics
|
||||
}
|
||||
|
||||
/// Checks if a generic type can be substituted with a concrete type and returns the concrete type
|
||||
fn check_generic(types: &NodeIOTypes, input: &Type, parameters: &[Type], generic: &str) -> Result<Type, String> {
|
||||
let inputs = [(&types.input, input)].into_iter().chain(types.parameters.iter().zip(parameters.iter()));
|
||||
let mut concrete_inputs = inputs.filter(|(ni, _)| matches!(ni, Type::Generic(input) if generic == input));
|
||||
let (_, out_ty) = concrete_inputs
|
||||
.next()
|
||||
.ok_or_else(|| format!("Generic output type {generic} is not dependent on input {input:?} or parameters {parameters:?}",))?;
|
||||
if concrete_inputs.any(|(_, ty)| ty != out_ty) {
|
||||
return Err(format!("Generic output type {generic} is dependent on multiple inputs or parameters",));
|
||||
}
|
||||
Ok(out_ty.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
@@ -436,12 +535,12 @@ mod test {
|
||||
assert_eq!(
|
||||
ids,
|
||||
vec![
|
||||
17495035641492238530,
|
||||
14931179783740213471,
|
||||
2268573767208263092,
|
||||
14616574692620381527,
|
||||
12110007198416821768,
|
||||
11185814750012198757
|
||||
15907139529964845467,
|
||||
17186311536944112733,
|
||||
1674503539363691855,
|
||||
10408773954839245246,
|
||||
1677533587730447846,
|
||||
6826908746727711035
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -471,7 +570,7 @@ mod test {
|
||||
10,
|
||||
ProtoNode {
|
||||
identifier: "cons".into(),
|
||||
input: ProtoNodeInput::Network,
|
||||
input: ProtoNodeInput::Network(concrete!(u32)),
|
||||
construction_args: ConstructionArgs::Nodes(vec![14]),
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user