mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 07:28:11 +08:00
Fold attribute names out of their constant inputs when the graph compiles
`compute_layouts` resolves every name-from-input write against the text its name input carries, so a folded meta is indistinguishable from a marker node's and the layout holds a `&'static str` from there on. That leaves nowhere for a name to be computed, which is the whole of the rule: a name input that is not a constant is refused right here, per node, with the path the editor pins its diagnostic to. One name carries one value type, checked across the census and the names a node folds together, so no graph can declare one field at two widths. `compute_layouts` returns those refusals rather than panicking. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
0001cdf04d
commit
348e9022a6
@@ -20,7 +20,7 @@ impl Compiler {
|
|||||||
proto_networks.map(move |mut proto_network| {
|
proto_networks.map(move |mut proto_network| {
|
||||||
proto_network.insert_context_nullification_nodes()?;
|
proto_network.insert_context_nullification_nodes()?;
|
||||||
let _ = proto_network.resolve_types(registry);
|
let _ = proto_network.resolve_types(registry);
|
||||||
proto_network.compute_layouts();
|
proto_network.compute_layouts().map_err(|errors| errors.iter().map(|error| format!("{:?}", error.error)).collect::<Vec<_>>().join("\n"))?;
|
||||||
proto_network.generate_stable_node_ids();
|
proto_network.generate_stable_node_ids();
|
||||||
Ok(proto_network)
|
Ok(proto_network)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -378,7 +378,8 @@ impl ProtoNetwork {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compute_layouts(&mut self) {
|
pub fn compute_layouts(&mut self) -> Result<(), GraphErrors> {
|
||||||
|
self.fold_attribute_names()?;
|
||||||
for index in 0..self.nodes.len() {
|
for index in 0..self.nodes.len() {
|
||||||
let lane_invariant = self.nodes[index].1.lane_invariant_inputs;
|
let lane_invariant = self.nodes[index].1.lane_invariant_inputs;
|
||||||
let layout = {
|
let layout = {
|
||||||
@@ -388,6 +389,7 @@ impl ProtoNetwork {
|
|||||||
frame_bytes: layout.frame_bytes(),
|
frame_bytes: layout.frame_bytes(),
|
||||||
plan: Vec::new(),
|
plan: Vec::new(),
|
||||||
lane_invariant,
|
lane_invariant,
|
||||||
|
named_writes: Vec::new(),
|
||||||
layout,
|
layout,
|
||||||
}),
|
}),
|
||||||
ConstructionArgs::Nodes(inputs) => node.resolved.layout_meta.as_ref().and_then(|meta| {
|
ConstructionArgs::Nodes(inputs) => node.resolved.layout_meta.as_ref().and_then(|meta| {
|
||||||
@@ -412,6 +414,70 @@ impl ProtoNetwork {
|
|||||||
self.nodes[index].1.resolved.layout = layout;
|
self.nodes[index].1.resolved.layout = layout;
|
||||||
}
|
}
|
||||||
self.stack_need = self.fold_stack_peak();
|
self.stack_need = self.fold_stack_peak();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves every name-from-input write against the constant its name
|
||||||
|
/// input carries, leaving each node's meta indistinguishable from a marker
|
||||||
|
/// node's. A name input that is not a constant is refused here, which is
|
||||||
|
/// the whole of the no-runtime-names rule: past this point a name is a
|
||||||
|
/// `&'static str` in a layout, so there is nowhere for one to be computed.
|
||||||
|
fn fold_attribute_names(&mut self) -> Result<(), GraphErrors> {
|
||||||
|
let mut errors = GraphErrors::new();
|
||||||
|
for index in 0..self.nodes.len() {
|
||||||
|
let Some(meta) = &self.nodes[index].1.resolved.layout_meta else { continue };
|
||||||
|
if meta.named_writes.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ConstructionArgs::Nodes(inputs) = &self.nodes[index].1.construction_args else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let names: Vec<Result<&'static str, GraphErrorType>> = meta
|
||||||
|
.named_writes
|
||||||
|
.iter()
|
||||||
|
.map(|named| match inputs.get(named.name_input as usize).map(|input| &self.nodes[input.0 as usize].1.construction_args) {
|
||||||
|
Some(ConstructionArgs::Value(value)) => match &**value {
|
||||||
|
value::TaggedValue::String(name) => Ok(core_types::attribute::intern_name(name)),
|
||||||
|
other => Err(GraphErrorType::AttributeName(format!(
|
||||||
|
"an attribute name must be text, but input {} is {}",
|
||||||
|
named.name_input + 1,
|
||||||
|
other.ty()
|
||||||
|
))),
|
||||||
|
},
|
||||||
|
_ => Err(GraphErrorType::AttributeName(format!(
|
||||||
|
"input {} must be a constant, since attribute names resolve when the graph compiles rather than when it runs",
|
||||||
|
named.name_input + 1
|
||||||
|
))),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let node = &self.nodes[index].1;
|
||||||
|
let mut folded: Vec<(&'static str, std::any::TypeId)> = Vec::new();
|
||||||
|
let mut failed = false;
|
||||||
|
for (position, name) in names.iter().enumerate() {
|
||||||
|
match name {
|
||||||
|
Err(error) => {
|
||||||
|
errors.push(GraphError::new(node, error.clone()));
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
Ok(name) => {
|
||||||
|
let template = node.resolved.layout_meta.as_ref().expect("checked above").named_writes[position].template;
|
||||||
|
if let Some(conflict) = one_name_one_type(name, template.type_id, &folded) {
|
||||||
|
errors.push(GraphError::new(node, conflict));
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
folded.push((name, template.type_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if failed {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let meta = self.nodes[index].1.resolved.layout_meta.as_mut().expect("checked above");
|
||||||
|
for (position, name) in names.into_iter().enumerate() {
|
||||||
|
meta.fold_name(position, name.expect("every name resolved"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errors.is_empty().then_some(()).ok_or(errors)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Peak record-stack bytes for evaluating [`output`](Self::output)'s cone. A node holds its
|
/// Peak record-stack bytes for evaluating [`output`](Self::output)'s cone. A node holds its
|
||||||
@@ -737,9 +803,27 @@ impl ProtoNetwork {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// Reports a folded name that disagrees with a type the same name already
|
||||||
|
/// carries, over the census and the names this node folded together. One name
|
||||||
|
/// means one value type everywhere, so the layouts a graph folds can never
|
||||||
|
/// declare a field twice at two widths.
|
||||||
|
fn one_name_one_type(name: &str, value_type: std::any::TypeId, folded: &[(&'static str, std::any::TypeId)]) -> Option<GraphErrorType> {
|
||||||
|
let census = core_types::attribute::info(name);
|
||||||
|
let declared = census
|
||||||
|
.filter(|row| row.value_type != value_type)
|
||||||
|
.map(|row| row.value_type_name.to_string())
|
||||||
|
.or_else(|| folded.iter().find(|(other, ty)| *other == name && *ty != value_type).map(|_| "another type on this node".to_string()))?;
|
||||||
|
Some(GraphErrorType::AttributeName(format!(
|
||||||
|
"attribute `{name}` is already declared at {declared}, and one name carries one value type"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum GraphErrorType {
|
pub enum GraphErrorType {
|
||||||
NodeNotFound(NodeId),
|
NodeNotFound(NodeId),
|
||||||
|
/// A name-from-input attribute write whose name could not be resolved: it
|
||||||
|
/// is not a constant, or it disagrees with the name's declared value type.
|
||||||
|
AttributeName(String),
|
||||||
UnexpectedGenerics {
|
UnexpectedGenerics {
|
||||||
index: usize,
|
index: usize,
|
||||||
inputs: Vec<Type>,
|
inputs: Vec<Type>,
|
||||||
@@ -764,6 +848,7 @@ impl Debug for GraphErrorType {
|
|||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
GraphErrorType::NodeNotFound(id) => write!(f, "Input node {id} is not present in the typing context"),
|
GraphErrorType::NodeNotFound(id) => write!(f, "Input node {id} is not present in the typing context"),
|
||||||
|
GraphErrorType::AttributeName(error) => write!(f, "{error}"),
|
||||||
GraphErrorType::UnexpectedGenerics { index, inputs } => write!(f, "Generic inputs should not exist but found at {index}: {inputs:?}"),
|
GraphErrorType::UnexpectedGenerics { index, inputs } => write!(f, "Generic inputs should not exist but found at {index}: {inputs:?}"),
|
||||||
GraphErrorType::NoImplementations => write!(f, "No implementations found"),
|
GraphErrorType::NoImplementations => write!(f, "No implementations found"),
|
||||||
GraphErrorType::NoConstructor => write!(f, "No construct found for node"),
|
GraphErrorType::NoConstructor => write!(f, "No construct found for node"),
|
||||||
|
|||||||
@@ -721,7 +721,7 @@ mod test {
|
|||||||
|
|
||||||
fn build_executor(mut network: ProtoNetwork) -> DynamicExecutor {
|
fn build_executor(mut network: ProtoNetwork) -> DynamicExecutor {
|
||||||
network.resolve_types(&node_registry::NODE_REGISTRY).unwrap();
|
network.resolve_types(&node_registry::NODE_REGISTRY).unwrap();
|
||||||
network.compute_layouts();
|
network.compute_layouts().unwrap();
|
||||||
DynamicExecutor::new(network).unwrap()
|
DynamicExecutor::new(network).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ fn inputs(parsed: &ParsedNodeFn, fields: &[&ParsedField], generics: &[Ident]) ->
|
|||||||
shape: item_shape(&element, depth, &field.attribute_reads, generics),
|
shape: item_shape(&element, depth, &field.attribute_reads, generics),
|
||||||
subject: subject(index, field, carrier_subject, routing.as_ref()),
|
subject: subject(index, field, carrier_subject, routing.as_ref()),
|
||||||
lend: matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. })),
|
lend: matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. })),
|
||||||
|
name_source: crate::parsing::named_source(&element),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -274,6 +275,7 @@ pub(crate) fn layout_meta_tokens(node: &Node, element_spec: TokenStream2, core_t
|
|||||||
quote!(#core_types::record::InputReads { input: #index, reads: ::std::vec![#(#descs),*] })
|
quote!(#core_types::record::InputReads { input: #index, reads: ::std::vec![#(#descs),*] })
|
||||||
});
|
});
|
||||||
let writes = field_writes(&node.output.shape.attrs, core_types);
|
let writes = field_writes(&node.output.shape.attrs, core_types);
|
||||||
|
let named_writes = named_field_writes(node, core_types);
|
||||||
let removes = node.output.removes.iter().map(|attr| {
|
let removes = node.output.removes.iter().map(|attr| {
|
||||||
let marker = &attr.marker;
|
let marker = &attr.marker;
|
||||||
let level = attr.level;
|
let level = attr.level;
|
||||||
@@ -290,6 +292,8 @@ pub(crate) fn layout_meta_tokens(node: &Node, element_spec: TokenStream2, core_t
|
|||||||
reads: ::std::vec![#(#reads),*],
|
reads: ::std::vec![#(#reads),*],
|
||||||
element: #element_spec,
|
element: #element_spec,
|
||||||
writes: ::std::vec![#(#writes),*],
|
writes: ::std::vec![#(#writes),*],
|
||||||
|
named_writes: ::std::vec![#(#named_writes),*],
|
||||||
|
folded_names: ::std::vec![],
|
||||||
removes: ::std::vec![#(#removes),*],
|
removes: ::std::vec![#(#removes),*],
|
||||||
level_delta: #level_delta,
|
level_delta: #level_delta,
|
||||||
folded: #folded,
|
folded: #folded,
|
||||||
@@ -354,6 +358,7 @@ pub(crate) fn folded_subject(node: &Node) -> Option<(u8, u8)> {
|
|||||||
fn field_writes(attrs: &[LevelAttr], core_types: &TokenStream2) -> Vec<TokenStream2> {
|
fn field_writes(attrs: &[LevelAttr], core_types: &TokenStream2) -> Vec<TokenStream2> {
|
||||||
attrs
|
attrs
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter(|attr| crate::parsing::named_marker(&attr.marker).is_none())
|
||||||
.map(|attr| {
|
.map(|attr| {
|
||||||
let marker = &attr.marker;
|
let marker = &attr.marker;
|
||||||
let level = attr.level;
|
let level = attr.level;
|
||||||
@@ -362,6 +367,30 @@ fn field_writes(attrs: &[LevelAttr], core_types: &TokenStream2) -> Vec<TokenStre
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Emits one `NamedWrite` per name-generic write, pairing the template minted
|
||||||
|
/// from the concrete value type with the input its placeholder's name sits at.
|
||||||
|
fn named_field_writes(node: &Node, core_types: &TokenStream2) -> Vec<TokenStream2> {
|
||||||
|
node.output
|
||||||
|
.shape
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|attr| {
|
||||||
|
let (placeholder, value) = crate::parsing::named_marker(&attr.marker)?;
|
||||||
|
let input = name_input(node, &placeholder)? as u8;
|
||||||
|
let level = attr.level;
|
||||||
|
Some(quote!(#core_types::record::NamedWrite::of::<#placeholder, #value>(#input, #level)))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The input position carrying `placeholder`'s name, which is the parameter
|
||||||
|
/// declared at that placeholder.
|
||||||
|
pub(crate) fn name_input(node: &Node, placeholder: &Type) -> Option<usize> {
|
||||||
|
node.inputs
|
||||||
|
.iter()
|
||||||
|
.position(|input| input.name_source.as_ref().is_some_and(|declared| declared == placeholder))
|
||||||
|
}
|
||||||
|
|
||||||
fn level_delta(node: &Node) -> i8 {
|
fn level_delta(node: &Node) -> i8 {
|
||||||
// A folded subject contributes no base layout, so the delta is relative to
|
// A folded subject contributes no base layout, so the delta is relative to
|
||||||
// the fresh (empty) base.
|
// the fresh (empty) base.
|
||||||
@@ -527,6 +556,10 @@ pub(crate) struct Input {
|
|||||||
pub(crate) subject: bool,
|
pub(crate) subject: bool,
|
||||||
/// Written `&T`; the kernel borrows the evaluated element.
|
/// Written `&T`; the kernel borrows the evaluated element.
|
||||||
pub(crate) lend: bool,
|
pub(crate) lend: bool,
|
||||||
|
/// The placeholder this input names, written `Named<X>`. Such an input
|
||||||
|
/// carries constant text the compiler folds into a layout name, so it is
|
||||||
|
/// never evaluated per lane.
|
||||||
|
pub(crate) name_source: Option<Type>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `Lazy` = `impl Node<..>`, the kernel drives it.
|
/// `Lazy` = `impl Node<..>`, the kernel drives it.
|
||||||
|
|||||||
@@ -110,6 +110,42 @@ pub(crate) fn remove_attr_marker(ty: &Type) -> Option<Type> {
|
|||||||
marker_of(ty, "RemoveAttr")
|
marker_of(ty, "RemoveAttr")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Splits a `Named<X, V>` marker into its placeholder and value type. A write
|
||||||
|
/// of one takes its name from the input the placeholder is declared at rather
|
||||||
|
/// than from the marker, so the name folds at graph compile time.
|
||||||
|
pub(crate) fn named_marker(ty: &Type) -> Option<(Type, Type)> {
|
||||||
|
let mut args = named_arguments(ty)?.into_iter();
|
||||||
|
let (placeholder, value) = (args.next()?, args.next()?);
|
||||||
|
args.next().is_none().then_some((placeholder, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The placeholder a `Named<X>` parameter declares. Such a parameter is the
|
||||||
|
/// name source for every `Attr<Named<X, _>>` the signature writes, and crosses
|
||||||
|
/// the wire as constant text.
|
||||||
|
pub(crate) fn named_source(ty: &Type) -> Option<Type> {
|
||||||
|
let mut args = named_arguments(ty)?.into_iter();
|
||||||
|
let placeholder = args.next()?;
|
||||||
|
args.next().is_none().then_some(placeholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn named_arguments(ty: &Type) -> Option<Vec<Type>> {
|
||||||
|
let Type::Path(path) = ty else { return None };
|
||||||
|
let segment = path.path.segments.last()?;
|
||||||
|
if segment.ident != "Named" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let PathArguments::AngleBracketed(args) = &segment.arguments else { return None };
|
||||||
|
Some(
|
||||||
|
args.args
|
||||||
|
.iter()
|
||||||
|
.filter_map(|argument| match argument {
|
||||||
|
GenericArgument::Type(ty) => Some(ty.clone()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn marker_of(ty: &Type, wrapper: &str) -> Option<Type> {
|
fn marker_of(ty: &Type, wrapper: &str) -> Option<Type> {
|
||||||
let Type::Path(path) = ty else { return None };
|
let Type::Path(path) = ty else { return None };
|
||||||
let segment = path.path.segments.last()?;
|
let segment = path.path.segments.last()?;
|
||||||
|
|||||||
Reference in New Issue
Block a user