Fix regression breaking Into/Convert node type coercion (#3681)

This commit is contained in:
Keavon Chambers 2026-01-25 16:26:05 -08:00 committed by GitHub
parent ede34b1b9f
commit 568831bd2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 55 additions and 58 deletions

View File

@ -648,7 +648,7 @@ impl NodeNetworkInterface {
let export_name = if !export_name.is_empty() { let export_name = if !export_name.is_empty() {
export_name export_name
} else if let Some(export_type_name) = input_type.compiled_nested_type().map(|nested| nested.to_string()) { } else if let Some(export_type_name) = input_type.compiled_nested_type().map(ToString::to_string) {
export_type_name export_type_name
} else { } else {
format!("Export #{}", export_index) format!("Export #{}", export_index)
@ -658,12 +658,19 @@ impl NodeNetworkInterface {
} }
}; };
let valid_types = self.potential_valid_input_types(input_connector, network_path).iter().map(ToString::to_string).collect::<Vec<_>>();
let valid_types = {
// Dedupe while preserving order
let mut found = HashSet::new();
valid_types.into_iter().filter(|s| found.insert(s.clone())).collect::<Vec<_>>()
};
Some(FrontendGraphInput { Some(FrontendGraphInput {
data_type, data_type,
resolved_type, resolved_type,
name, name,
description, description,
valid_types: self.potential_valid_input_types(input_connector, network_path).iter().map(|ty| ty.to_string()).collect(), valid_types,
connected_to, connected_to,
}) })
} }
@ -698,7 +705,7 @@ impl NodeNetworkInterface {
let import_name = if !import_name.is_empty() { let import_name = if !import_name.is_empty() {
import_name import_name
} else if let Some(import_type_name) = output_type.compiled_nested_type().map(|nested| nested.to_string()) { } else if let Some(import_type_name) = output_type.compiled_nested_type().map(ToString::to_string) {
import_type_name import_type_name
} else { } else {
format!("Import #{}", import_index) format!("Import #{}", import_index)

View File

@ -91,8 +91,8 @@ impl TypeSource {
/// The type to display in the tooltip label. /// The type to display in the tooltip label.
pub fn resolved_type_tooltip_string(&self) -> String { pub fn resolved_type_tooltip_string(&self) -> String {
match self { match self {
TypeSource::Compiled(compiled_type) => format!("Data Type: {:?}", compiled_type.nested_type().to_string()), TypeSource::Compiled(compiled_type) => format!("Data Type: {}", compiled_type.nested_type()),
TypeSource::TaggedValue(value_type) => format!("Data Type: {:?}", value_type.nested_type().to_string()), TypeSource::TaggedValue(value_type) => format!("Data Type: {}", value_type.nested_type()),
TypeSource::Unknown => "Unknown Data Type".to_string(), TypeSource::Unknown => "Unknown Data Type".to_string(),
TypeSource::Invalid => "Invalid Type Combination".to_string(), TypeSource::Invalid => "Invalid Type Combination".to_string(),
TypeSource::Error(_) => "Error Getting Data Type".to_string(), TypeSource::Error(_) => "Error Getting Data Type".to_string(),
@ -180,7 +180,7 @@ impl NodeNetworkInterface {
self.input_type_not_invalid(input_connector, network_path) self.input_type_not_invalid(input_connector, network_path)
} }
// Gets the default tagged value for an input. If its not compiled, then it tries to get a valid type. If there are no valid types, then it picks a random implementation /// Gets the default tagged value for an input. If its not compiled, then it tries to get a valid type. If there are no valid types, then it picks a random implementation.
pub fn tagged_value_from_input(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> TaggedValue { pub fn tagged_value_from_input(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> TaggedValue {
let guaranteed_type = match self.input_type(input_connector, network_path) { let guaranteed_type = match self.input_type(input_connector, network_path) {
TypeSource::Compiled(compiled) => compiled, TypeSource::Compiled(compiled) => compiled,
@ -190,12 +190,12 @@ impl NodeNetworkInterface {
// TODO: Add a NodeInput::Indeterminate which can be resolved at compile time to be any type that prevents an error. This may require bidirectional typing. // TODO: Add a NodeInput::Indeterminate which can be resolved at compile time to be any type that prevents an error. This may require bidirectional typing.
self.complete_valid_input_types(input_connector, network_path) self.complete_valid_input_types(input_connector, network_path)
.into_iter() .into_iter()
.min_by_key(|ty| ty.nested_type().to_string()) .min_by_key(|ty| ty.nested_type().identifier_name())
// Pick a random type from the potential valid types // Pick a random type from the potential valid types
.or_else(|| { .or_else(|| {
self.potential_valid_input_types(input_connector, network_path) self.potential_valid_input_types(input_connector, network_path)
.into_iter() .into_iter()
.min_by_key(|ty| ty.nested_type().to_string()) .min_by_key(|ty| ty.nested_type().identifier_name())
}).unwrap_or(concrete!(())) }).unwrap_or(concrete!(()))
} }
TypeSource::Error(e) => { TypeSource::Error(e) => {

View File

@ -794,7 +794,14 @@ impl TypingContext {
.into_iter() .into_iter()
.chain(&inputs) .chain(&inputs)
.enumerate() .enumerate()
.filter_map(|(i, t)| if i == 0 { None } else { Some(format!("• Input {}: {t}", i + convert_node_index_offset)) }) .filter_map(|(i, t)| {
if i == 0 {
None
} else {
let number = i + convert_node_index_offset;
Some(format!("• Input {number}: {t}"))
}
})
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n"); .join("\n");
Err(vec![GraphError::new(node, GraphErrorType::InvalidImplementations { inputs, error_inputs })]) Err(vec![GraphError::new(node, GraphErrorType::InvalidImplementations { inputs, error_inputs })])
@ -821,13 +828,13 @@ impl TypingContext {
return Ok(node_io.clone()); return Ok(node_io.clone());
} }
} }
let inputs = [call_argument].into_iter().chain(&inputs).map(|t| t.to_string()).collect::<Vec<_>>().join(", "); 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().cloned().collect();
Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })]) Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })])
} }
_ => { _ => {
let inputs = [call_argument].into_iter().chain(&inputs).map(|t| t.to_string()).collect::<Vec<_>>().join(", "); 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().cloned().collect();
Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })]) Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })])
} }
@ -871,9 +878,7 @@ fn check_generic(types: &NodeIOTypes, input: &Type, parameters: &[Type], generic
/// Returns a list of all generic types used in the node /// Returns a list of all generic types used in the node
fn replace_generics(types: &mut NodeIOTypes, lookup: &HashMap<String, Type>) { fn replace_generics(types: &mut NodeIOTypes, lookup: &HashMap<String, Type>) {
let replace = |ty: &Type| { let replace = |ty: &Type| {
let Type::Generic(ident) = ty else { let Type::Generic(ident) = ty else { return None };
return None;
};
lookup.get(ident.as_ref()).cloned() lookup.get(ident.as_ref()).cloned()
}; };
types.call_argument.replace_nested(replace); types.call_argument.replace_nested(replace);

View File

@ -118,11 +118,10 @@ impl NodeIOTypes {
impl std::fmt::Debug for NodeIOTypes { impl std::fmt::Debug for NodeIOTypes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!( let inputs = self.inputs.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ");
"node({}) → {}", let return_value = &self.return_value;
[&self.call_argument].into_iter().chain(&self.inputs).map(|input| input.to_string()).collect::<Vec<_>>().join(", "), let call_argument = &self.call_argument;
self.return_value f.write_fmt(format_args!("({inputs}) → {return_value} called with {call_argument}"))
))
} }
} }
@ -202,7 +201,7 @@ impl std::hash::Hash for TypeDescriptor {
impl std::fmt::Display for TypeDescriptor { impl std::fmt::Display for TypeDescriptor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let text = make_type_user_readable(&format_type(&self.name)); let text = make_type_user_readable(&simplify_identifier_name(&self.name));
write!(f, "{text}") write!(f, "{text}")
} }
} }
@ -337,15 +336,17 @@ impl Type {
} }
} }
pub fn to_cow_string(&self) -> Cow<'static, str> { pub fn identifier_name(&self) -> String {
match self { match self {
Type::Generic(name) => name.clone(), Type::Generic(name) => name.to_string(),
_ => Cow::Owned(self.to_string()), Type::Concrete(ty) => simplify_identifier_name(&ty.name),
Type::Fn(call_arg, return_value) => format!("{} called with {}", return_value.identifier_name(), call_arg.identifier_name()),
Type::Future(ty) => ty.identifier_name(),
} }
} }
} }
pub fn format_type(ty: &str) -> String { pub fn simplify_identifier_name(ty: &str) -> String {
ty.split('<') ty.split('<')
.map(|path| path.split(',').map(|path| path.split("::").last().unwrap_or(path)).collect::<Vec<_>>().join(",")) .map(|path| path.split(',').map(|path| path.split("::").last().unwrap_or(path)).collect::<Vec<_>>().join(","))
.collect::<Vec<_>>() .collect::<Vec<_>>()
@ -361,42 +362,26 @@ pub fn make_type_user_readable(ty: &str) -> String {
impl std::fmt::Debug for Type { impl std::fmt::Debug for Type {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let text = match self { write!(f, "{self}")
Self::Generic(name) => name.to_string(),
#[cfg(feature = "type_id_logging")]
Self::Concrete(ty) => format!("Concrete<{}, {:?}>", ty.name, ty.id),
#[cfg(not(feature = "type_id_logging"))]
Self::Concrete(ty) => format_type(&ty.name),
Self::Fn(call_arg, return_value) => format!("{return_value:?} called with {call_arg:?}"),
Self::Future(ty) => format!("{ty:?}"),
};
let text = make_type_user_readable(&text);
write!(f, "{text}")
} }
} }
// Display
impl std::fmt::Display for Type { impl std::fmt::Display for Type {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self == &concrete!(glam::DVec2) { use glam::*;
return write!(f, "vec2");
}
if self == &concrete!(glam::DAffine2) {
return write!(f, "transform");
}
if self == &concrete!(Footprint) {
return write!(f, "footprint");
}
if self == &concrete!(&str) || self == &concrete!(String) {
return write!(f, "string");
}
let text = match self { match self {
Type::Generic(name) => name.to_string(), Type::Generic(name) => write!(f, "{}", make_type_user_readable(name)),
Type::Concrete(ty) => format_type(&ty.name), Type::Concrete(ty) => match () {
Type::Fn(call_arg, return_value) => format!("{return_value} called with {call_arg}"), () if self == &concrete!(DVec2) || self == &concrete!(Vec2) || self == &concrete!(IVec2) || self == &concrete!(UVec2) => write!(f, "Vec2"),
Type::Future(ty) => ty.to_string(), () if self == &concrete!(glam::DAffine2) => write!(f, "Transform"),
}; () if self == &concrete!(Footprint) => write!(f, "Footprint"),
let text = make_type_user_readable(&text); () if self == &concrete!(&str) || self == &concrete!(String) => write!(f, "String"),
write!(f, "{text}") _ => write!(f, "{}", make_type_user_readable(&simplify_identifier_name(&ty.name))),
},
Type::Fn(call_arg, return_value) => write!(f, "{return_value} called with {call_arg}"),
Type::Future(ty) => write!(f, "{ty}"),
}
} }
} }

View File

@ -36,7 +36,7 @@ pub fn generate_node_substitutions() -> HashMap<ProtoNodeIdentifier, DocumentNod
let id = id.clone(); let id = id.clone();
let NodeMetadata { fields, .. } = metadata; let NodeMetadata { fields, .. } = metadata;
let Some(implementations) = &node_registry.get(&id) else { continue }; let Some(implementations) = node_registry.get(&id) else { continue };
let valid_call_args: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.call_argument.clone()).collect(); let valid_call_args: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.call_argument.clone()).collect();
let first_node_io = implementations.first().map(|(_, node_io)| node_io).unwrap_or(const { &NodeIOTypes::empty() }); let first_node_io = implementations.first().map(|(_, node_io)| node_io).unwrap_or(const { &NodeIOTypes::empty() });
let mut node_io_types = vec![HashSet::new(); fields.len()]; let mut node_io_types = vec![HashSet::new(); fields.len()];
@ -69,8 +69,8 @@ pub fn generate_node_substitutions() -> HashMap<ProtoNodeIdentifier, DocumentNod
let input_ty = input.nested_type(); let input_ty = input.nested_type();
let mut inputs = vec![NodeInput::import(input.clone(), i)]; let mut inputs = vec![NodeInput::import(input.clone(), i)];
let into_node_identifier = ProtoNodeIdentifier::with_owned_string(format!("graphene_core::ops::IntoNode<{}>", input_ty.clone())); let into_node_identifier = ProtoNodeIdentifier::with_owned_string(format!("graphene_core::ops::IntoNode<{}>", input_ty.identifier_name()));
let convert_node_identifier = ProtoNodeIdentifier::with_owned_string(format!("graphene_core::ops::ConvertNode<{}>", input_ty.clone())); let convert_node_identifier = ProtoNodeIdentifier::with_owned_string(format!("graphene_core::ops::ConvertNode<{}>", input_ty.identifier_name()));
let proto_node = if into_node_registry.keys().any(|ident: &ProtoNodeIdentifier| ident.as_str() == into_node_identifier.as_str()) { let proto_node = if into_node_registry.keys().any(|ident: &ProtoNodeIdentifier| ident.as_str() == into_node_identifier.as_str()) {
generated_nodes += 1; generated_nodes += 1;