Replace terminology "primary" with "call argument" and "parameter" with "secondary input"

This commit is contained in:
Keavon Chambers 2024-09-25 20:05:00 -07:00
parent f8c7ada572
commit c738b4a1f9
15 changed files with 142 additions and 128 deletions

View file

@ -98,22 +98,34 @@ pub struct DocumentNode {
/// - A [`NodeInput::Network`] which specifies that this input is from outside the graph, which is resolved in the graph flattening step in the case of nested networks.
///
/// In the root network, it is resolved when evaluating the borrow tree.
/// Ensure the click target in the encapsulating network is updated when the inputs cause the node shape to change (currently only when exposing/hiding an input) by using network.update_click_target(node_id).
/// Ensure the click target in the encapsulating network is updated when the inputs cause the node shape to change (currently only when exposing/hiding an input)
/// by using network.update_click_target(node_id).
#[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_inputs"))]
pub inputs: Vec<NodeInput>,
/// Manual composition is a way to override the default composition flow of one node into another.
/// Manual composition is the methodology by which most nodes are implemented, involving a call argument and upstream inputs.
/// By contrast, automatic composition is an alternative way to handle the composition of nodes as they execute in the graph.
/// Normally, the program (the compiled graph) builds up its call stack, with each node calling its upstream predecessor to acquire its input data.
/// When the document graph becomes the proto graph, that conceptual model changes into a model that's unique to the proto graph.
/// Automatic composition allows a document node to be translated into its place in the proto graph differently, such that
/// the node doesn't participate in that process of being called with a call argument and calling its upstream predecessor.
/// Instead, it is called directly with its input data from the upstream node, skipping the call stack building process.
/// The abstraction is provided by the compiler for nodes which opt for automatic composition. It works by inserting a `ComposeNode`
/// into the proto graph, which does the job of calling the upstream node and feeding its output into the downstream node's first input.
/// That first input is typically used by manual composition nodes as the call argument, but for automatic composition nodes,
/// that first input becomes the input data from the upstream node passed in by the `ComposeNode`.
///
/// Through the usual node composition flow, the upstream node providing the primary input for a node is evaluated before the node itself is run.
/// - Abstract example: upstream node `G` is evaluated and its data feeds into the primary input of downstream node `F`,
/// Through automatic composition, the upstream node providing the first input for a proto node is evaluated before the proto node itself is run.
/// (That first input is usually the call argument when manual composition is used.)
/// - Abstract example: upstream node `G` is evaluated and its data feeds into the first input of downstream node `F`,
/// just like function composition where function `G` is evaluated and its result is fed into function `F`.
/// - Concrete example: a node that takes an image as primary input will get that image data from an upstream node that produces image output data and is evaluated first before being fed downstream.
/// - Concrete example: a node that takes an image as its first input will get that image data from an upstream node that produces image output data and is evaluated first before being fed downstream.
///
/// This is achieved by automatically inserting `ComposeNode`s, which run the first node with the overall input and then feed the resulting output into the second node.
/// The `ComposeNode` is basically a function composition operator: the parentheses in `F(G(x))` or circle math operator in `(F ∘ G)(x)`.
/// For flexibility, instead of being a language construct, Graphene splits out composition itself as its own low-level node so that behavior can be overridden.
/// The `ComposeNode`s are then inserted during the graph rewriting step for nodes that don't opt out with `manual_composition`.
/// Instead of node `G` feeding into node `F` feeding as the result back to the caller,
/// the graph is rewritten so nodes `G` and `F` both feed as lambdas into the parameters of a `ComposeNode` which calls `F(G(input))` and returns the result to the caller.
/// the graph is rewritten so nodes `G` and `F` both feed as lambdas into the inputs of a `ComposeNode` which calls `F(G(input))` and returns the result to the caller.
///
/// A node's manual composition input represents an input that is not resolved through graph rewriting with a `ComposeNode`,
/// and is instead just passed in when evaluating this node within the borrow tree.
@ -305,15 +317,15 @@ impl DocumentNode {
NodeInput::Reflection(_) => unreachable!("Reflection input was not resolved"),
}
};
assert!(!self.inputs.iter().any(|input| matches!(input, NodeInput::Network { .. })), "received non resolved parameter");
assert!(!self.inputs.iter().any(|input| matches!(input, NodeInput::Network { .. })), "received non-resolved input");
assert!(
!self.inputs.iter().any(|input| matches!(input, NodeInput::Value { .. })),
"received value as parameter. inputs: {:#?}, construction_args: {:#?}",
"received value as input. inputs: {:#?}, construction_args: {:#?}",
self.inputs,
args
);
// If we have one parameter of the type inline, set it as the construction args
// If we have one input of the type inline, set it as the construction args
if let &[NodeInput::Inline(ref inline)] = self.inputs.as_slice() {
args = ConstructionArgs::Inline(inline.clone());
}

View file

@ -40,7 +40,7 @@ impl core::fmt::Display for ProtoNetwork {
f.write_str("{\n")?;
f.write_str(&"\t".repeat(indent + 1))?;
f.write_str("Primary input: ")?;
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:?})"))?,
@ -80,11 +80,11 @@ impl core::fmt::Display for ProtoNetwork {
pub enum ConstructionArgs {
/// A value of a type that is known, allowing serialization (serde::Deserialize is not object safe)
Value(MemoHash<value::TaggedValue>),
// TODO: use a struct for clearer naming.
/// A list of nodes used as inputs to the constructor function in `node_registry.rs`.
/// The bool indicates whether to treat the node as lambda node.
// TODO: use a struct for clearer naming.
Nodes(Vec<(NodeId, bool)>),
// TODO: What?
/// Used for GPU computation to work around the limitations of rust-gpu.
Inline(InlineRust),
}
@ -136,7 +136,8 @@ impl ConstructionArgs {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
/// A proto node is an intermediate step between the `DocumentNode` and the boxed struct that actually runs the node (found in the [`BorrowTree`]). It has one primary input and several secondary inputs in [`ConstructionArgs`].
/// 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.
pub struct ProtoNode {
pub construction_args: ConstructionArgs,
pub input: ProtoNodeInput,
@ -157,14 +158,13 @@ impl Default for ProtoNode {
}
}
/// A ProtoNodeInput represents the primary input of a node in a ProtoNetwork.
/// Similar to [`crate::document::NodeInput`].
/// Similar to the document node's [`crate::document::NodeInput`].
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ProtoNodeInput {
/// [`ProtoNode`]s do not require any input, e.g. the value node just takes in [`ConstructionArgs`].
None,
/// A ManualComposition input represents an input that opts out of being resolved through the default `ComposeNode`, which first runs the previous (upstream) node, then passes that evaluated
/// 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.
@ -536,11 +536,11 @@ impl ProtoNetwork {
pub enum GraphErrorType {
NodeNotFound(NodeId),
InputNodeNotFound(NodeId),
UnexpectedGenerics { index: usize, parameters: Vec<Type> },
UnexpectedGenerics { index: usize, inputs: Vec<Type> },
NoImplementations,
NoConstructor,
InvalidImplementations { parameters: String, error_inputs: Vec<Vec<(usize, (Type, Type))>> },
MultipleImplementations { parameters: String, valid: Vec<NodeIOTypes> },
InvalidImplementations { inputs: String, error_inputs: Vec<Vec<(usize, (Type, Type))>> },
MultipleImplementations { inputs: String, valid: Vec<NodeIOTypes> },
}
impl core::fmt::Debug for GraphErrorType {
// TODO: format with the document graph context so the input index is the same as in the graph UI.
@ -548,17 +548,17 @@ impl core::fmt::Debug for GraphErrorType {
match self {
GraphErrorType::NodeNotFound(id) => write!(f, "Input node {id} is not present in the typing context"),
GraphErrorType::InputNodeNotFound(id) => write!(f, "Input node {id} is not present in the typing context"),
GraphErrorType::UnexpectedGenerics { index, parameters } => write!(f, "Generic parameters should not exist but found at {index}: {parameters:?}"),
GraphErrorType::UnexpectedGenerics { index, inputs } => write!(f, "Generic inputs should not exist but found at {index}: {inputs:?}"),
GraphErrorType::NoImplementations => write!(f, "No implementations found"),
GraphErrorType::NoConstructor => write!(f, "No construct found for node"),
GraphErrorType::InvalidImplementations { parameters, error_inputs } => {
GraphErrorType::InvalidImplementations { inputs, error_inputs } => {
let ordinal = |x: usize| match x.to_string().as_str() {
x if x.ends_with('1') && !x.ends_with("11") => format!("{x}st"),
x if x.ends_with('2') && !x.ends_with("12") => format!("{x}nd"),
x if x.ends_with('3') && !x.ends_with("13") => format!("{x}rd"),
x => format!("{x}th"),
};
let format_index = |index: usize| if index == 0 { "primary".to_string() } else { format!("{} parameter", ordinal(index)) };
let format_index = |index: usize| if index == 0 { "primary".to_string() } else { format!("{} secondary", ordinal(index)) };
let format_error = |(index, (real, expected)): &(usize, (Type, Type))| format!("• The {} input expected {} but found {}", format_index(*index), expected, real);
let format_error_list = |errors: &Vec<(usize, (Type, Type))>| errors.iter().map(format_error).collect::<Vec<_>>().join("\n");
let errors = error_inputs.iter().map(format_error_list).collect::<Vec<_>>();
@ -568,7 +568,7 @@ impl core::fmt::Debug for GraphErrorType {
consider using undo to go back and try another way to connect the nodes.\n\
\n\
No node implementation exists for type:\n\
({parameters})\n\
({inputs})\n\
\n\
Caused by{}:\n\
{}",
@ -576,7 +576,7 @@ impl core::fmt::Debug for GraphErrorType {
errors.join("\n")
)
}
GraphErrorType::MultipleImplementations { parameters, valid } => write!(f, "Multiple implementations found ({parameters}):\n{valid:#?}"),
GraphErrorType::MultipleImplementations { inputs, valid } => write!(f, "Multiple implementations found ({inputs}):\n{valid:#?}"),
}
}
}
@ -656,8 +656,8 @@ impl TypingContext {
return Ok(inferred.clone());
}
let parameters = match node.construction_args {
// If the node has a value parameter we can infer the return type from it
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));
// TODO: This should return a reference to the value
@ -665,7 +665,7 @@ impl TypingContext {
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
// If the node has nodes as inputs we can infer the types from the node outputs
ConstructionArgs::Nodes(ref nodes) => nodes
.iter()
.map(|(id, _)| {
@ -684,19 +684,19 @@ impl TypingContext {
ProtoNodeInput::ManualComposition(ref ty) => ty.clone(),
ProtoNodeInput::Node(id) | ProtoNodeInput::NodeLambda(id) => {
let input = self.inferred.get(&id).ok_or_else(|| vec![GraphError::new(node, GraphErrorType::InputNodeNotFound(id))])?;
input.output.clone()
input.return_value.clone()
}
};
let impls = self.lookup.get(&node.identifier).ok_or_else(|| vec![GraphError::new(node, GraphErrorType::NoImplementations)])?;
if let Some(index) = parameters.iter().position(|p| {
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, parameters })]);
return Err(vec![GraphError::new(node, GraphErrorType::UnexpectedGenerics { index, inputs })]);
}
/// Checks if a proposed input to a particular (primary or secondary) input is valid for its type signature.
/// Checks if a proposed input to a particular (primary or secondary) input connector is valid for its type signature.
/// `from` indicates the value given to a input, `to` indicates the input's allowed type as specified by its type signature.
fn valid_subtype(from: &Type, to: &Type) -> bool {
match (from, to) {
@ -721,10 +721,10 @@ impl TypingContext {
}
}
// List of all implementations that match the input and parameter types
// List of all implementations that match the input types
let valid_output_types = impls
.keys()
.filter(|node_io| valid_subtype(&input, &node_io.input) && parameters.iter().zip(node_io.parameters.iter()).all(|(p1, p2)| valid_subtype(p1, p2)))
.filter(|node_io| valid_subtype(&input, &node_io.call_argument) && inputs.iter().zip(node_io.inputs.iter()).all(|(p1, p2)| valid_subtype(p1, p2)))
.collect::<Vec<_>>();
// Attempt to substitute generic types with concrete types and save the list of results
@ -733,12 +733,12 @@ impl TypingContext {
.map(|node_io| {
collect_generics(node_io)
.iter()
.try_for_each(|generic| check_generic(node_io, &input, &parameters, generic).map(|_| ()))
.try_for_each(|generic| check_generic(node_io, &input, &inputs, generic).map(|_| ()))
.map(|_| {
if let Type::Generic(out) = &node_io.output {
((*node_io).clone(), check_generic(node_io, &input, &parameters, out).unwrap())
if let Type::Generic(out) = &node_io.return_value {
((*node_io).clone(), check_generic(node_io, &input, &inputs, out).unwrap())
} else {
((*node_io).clone(), node_io.output.clone())
((*node_io).clone(), node_io.return_value.clone())
}
})
})
@ -754,9 +754,9 @@ impl TypingContext {
for node_io in impls.keys() {
let current_errors = [&input]
.into_iter()
.chain(&parameters)
.chain(&inputs)
.cloned()
.zip([&node_io.input].into_iter().chain(&node_io.parameters).cloned())
.zip([&node_io.call_argument].into_iter().chain(&node_io.inputs).cloned())
.enumerate()
.filter(|(_, (p1, p2))| !valid_subtype(p1, p2))
.map(|(index, ty)| (node.original_location.inputs(index).min_by_key(|s| s.node.len()).map(|s| s.index).unwrap_or(index), ty))
@ -769,12 +769,12 @@ impl TypingContext {
error_inputs.push(current_errors);
}
}
let parameters = [&input].into_iter().chain(&parameters).map(|t| t.to_string()).collect::<Vec<_>>().join(", ");
Err(vec![GraphError::new(node, GraphErrorType::InvalidImplementations { parameters, error_inputs })])
let inputs = [&input].into_iter().chain(&inputs).map(|t| t.to_string()).collect::<Vec<_>>().join(", ");
Err(vec![GraphError::new(node, GraphErrorType::InvalidImplementations { inputs, error_inputs })])
}
[(org_nio, output)] => {
// TODO: Fix unsoundness caused by generic parameters not getting cleaned up
let node_io = NodeIOTypes::new(input, (*output).clone(), parameters);
let node_io = NodeIOTypes::new(input, (*output).clone(), inputs);
// Save the inferred type
self.inferred.insert(node_id, node_io.clone());
@ -783,12 +783,12 @@ impl TypingContext {
}
// If two types are available and one of them accepts () an input, always choose that one
[first, second] => {
if first.0.input != second.0.input {
if first.0.call_argument != second.0.call_argument {
for (org_nio, output) in [first, second] {
if org_nio.input != concrete!(()) {
if org_nio.call_argument != concrete!(()) {
continue;
}
let node_io = NodeIOTypes::new(input, (*output).clone(), parameters);
let node_io = NodeIOTypes::new(input, (*output).clone(), inputs);
// Save the inferred type
self.inferred.insert(node_id, node_io.clone());
@ -796,15 +796,15 @@ impl TypingContext {
return Ok(node_io);
}
}
let parameters = [&input].into_iter().chain(&parameters).map(|t| t.to_string()).collect::<Vec<_>>().join(", ");
let inputs = [&input].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 { parameters, valid })])
Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })])
}
_ => {
let parameters = [&input].into_iter().chain(&parameters).map(|t| t.to_string()).collect::<Vec<_>>().join(", ");
let inputs = [&input].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 { parameters, valid })])
Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })])
}
}
}
@ -812,14 +812,14 @@ impl TypingContext {
/// 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().flat_map(|x| x.fn_output()));
let inputs = [&types.call_argument].into_iter().chain(types.inputs.iter().flat_map(|x| x.fn_output()));
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 {
if let Type::Generic(out) = &types.return_value {
generics.push(out.clone());
}
generics.dedup();
@ -828,9 +828,9 @@ fn collect_generics(types: &NodeIOTypes) -> Vec<Cow<'static, str>> {
/// 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 = [(Some(&types.input), Some(input))]
let inputs = [(Some(&types.call_argument), Some(input))]
.into_iter()
.chain(types.parameters.iter().map(|x| x.fn_output()).zip(parameters.iter().map(|x| x.fn_output())));
.chain(types.inputs.iter().map(|x| x.fn_output()).zip(parameters.iter().map(|x| x.fn_output())));
let concrete_inputs = inputs.filter(|(ni, _)| matches!(ni, Some(Type::Generic(input)) if generic == input));
let mut outputs = concrete_inputs.flat_map(|(_, out)| out);
let out_ty = outputs