mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Write an attribute under a name the graph supplies
`Named<X>` in parameter position declares where a placeholder's name is wired, and the macro gives that input constant text; `Attr<Named<X, V>>` in the return writes under it. The kernel is handed the bare placeholder, since the name is spent resolving the layout and a folded offset is all the write needs, so the hot path matches a marker node's exactly. A name-generic write names its value type through the wired generic, which only an implementations row resolves, so those nodes emit their layout meta per row rather than sharing one across rows. `write_attribute` is the catalog's set half, restoring the identifier master's documents carry with its input positions. Its name input is a constant, so those documents resolve without migration, and the reset marker that stood in for the missing node retires with it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2013,6 +2013,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
let value_args = regular_fields.iter().skip(if skips_carrier { 0 } else { 1 }).map(|field| {
|
||||
let name = &field.pat_ident.ident;
|
||||
match &field.ty {
|
||||
// A name input's text is spent when the graph compiles, so the
|
||||
// kernel takes the bare placeholder, not the wired string.
|
||||
ParsedFieldType::Regular(RegularParsedField { name_source: Some(_), .. }) => quote!(::core::default::Default::default()),
|
||||
// A lend param binds an owned input; the kernel borrows the
|
||||
// evaluated value.
|
||||
ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. }) => quote!(&#name),
|
||||
@@ -2517,12 +2520,28 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
self.__layout = __resolved.layout;
|
||||
})
|
||||
} else if record_io {
|
||||
let write_installs = write_markers.iter().enumerate().map(|(index, marker)| {
|
||||
let slot = format_ident!("__write_{index}");
|
||||
quote! {
|
||||
self.#slot = __resolved.layout.offset_of(<#marker as #core_types::attribute::Attribute>::NAME, 0).expect("a written attribute is always part of the wired layout");
|
||||
}
|
||||
});
|
||||
// A name-generic write has no marker name to look up: the compiler
|
||||
// folded its name out of the graph, so the offset resolves through the
|
||||
// name the resolved layout carries.
|
||||
let mut folded = 0usize;
|
||||
let write_installs: Vec<TokenStream2> = write_markers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, marker)| {
|
||||
let slot = format_ident!("__write_{index}");
|
||||
let name = match crate::parsing::named_marker(marker).is_some() {
|
||||
true => {
|
||||
let position = folded;
|
||||
folded += 1;
|
||||
quote!(__resolved.named_writes[#position])
|
||||
}
|
||||
false => quote!(<#marker as #core_types::attribute::Attribute>::NAME),
|
||||
};
|
||||
quote! {
|
||||
self.#slot = __resolved.layout.offset_of(#name, 0).expect("a written attribute is always part of the wired layout");
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let plan = (!skips_carrier || gather_carrier).then(|| quote!(self.__plan = __resolved.plan;));
|
||||
Some(quote! {
|
||||
#(#write_installs)*
|
||||
@@ -2565,7 +2584,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
}
|
||||
None => quote!(#core_types::record::ElementSpec::Carried),
|
||||
};
|
||||
let layout_meta = crate::codegen::ir::layout_meta_tokens(&node, element_spec, core_types);
|
||||
let layout_meta = crate::codegen::ir::layout_meta_tokens(&node, element_spec, core_types, &[]);
|
||||
// A flipped shader node's struct and impl are std-gated; its layout meta must be too.
|
||||
let cfg = crate::shader_nodes::modify_cfg(&parsed.attributes);
|
||||
quote! {
|
||||
@@ -2581,8 +2600,11 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
|
||||
let record_wiring = record_io.then(|| {
|
||||
let layout_fn = format_ident!("{}_layout", fn_name);
|
||||
// A name-generic write is absent here: this free layout fn derives a
|
||||
// layout without a graph, and only the graph carries the name.
|
||||
let write_descs: Vec<TokenStream2> = write_markers
|
||||
.iter()
|
||||
.filter(|marker| crate::parsing::named_marker(marker).is_none())
|
||||
.map(|marker| quote!(#core_types::record::FieldWrite::of::<#marker>(0)))
|
||||
.collect();
|
||||
let remove_pairs: Vec<TokenStream2> = removes
|
||||
@@ -2619,7 +2641,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
}
|
||||
None => quote!(#core_types::record::ElementSpec::Carried),
|
||||
};
|
||||
let layout_meta = crate::codegen::ir::layout_meta_tokens(&node, element_spec, core_types);
|
||||
let layout_meta = crate::codegen::ir::layout_meta_tokens(&node, element_spec, core_types, &[]);
|
||||
let layout_meta_def = quote! {
|
||||
#vis fn #layout_meta_fn() -> #core_types::record::LayoutMeta {
|
||||
#layout_meta
|
||||
|
||||
@@ -157,7 +157,7 @@ fn flip_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_field
|
||||
quote!(&#layout,)
|
||||
});
|
||||
let element_spec = quote!(gcore::record::ElementSpec::Concrete({ use gcore::record::{ElementWritePickHashed as _, ElementWritePickPlain as _}; (&gcore::record::ElementWritePick::<#row_output>(::core::marker::PhantomData)).element_write() }));
|
||||
let layout_meta = crate::codegen::ir::layout_meta_tokens(&node, element_spec, &core_types);
|
||||
let layout_meta = crate::codegen::ir::layout_meta_tokens(&node, element_spec, &core_types, &assignments);
|
||||
Some(quote! {
|
||||
gcore::registry::RegistryEntry {
|
||||
layout_meta: Some(#layout_meta),
|
||||
@@ -397,7 +397,7 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields
|
||||
.collect();
|
||||
|
||||
let carried_meta = || {
|
||||
let meta = ir::layout_meta_tokens(&node, quote!(gcore::record::ElementSpec::Carried), &core_types);
|
||||
let meta = ir::layout_meta_tokens(&node, quote!(gcore::record::ElementSpec::Carried), &core_types, &[]);
|
||||
quote!(Some(#meta))
|
||||
};
|
||||
|
||||
@@ -432,7 +432,25 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields
|
||||
ir::NodeKind::RecordIo => {
|
||||
let carrier_arg = (node.inputs.first().is_some_and(|input| input.subject) && ir::materialized_levels(&node, 0) == 0).then(|| quote!(&__layout_0,));
|
||||
let layout_meta_fn = format_ident!("{}_layout_meta", fn_name);
|
||||
(quote!(), quote!(#carrier_arg #(#value_layout_args)*), quote!(Some(self::#layout_meta_fn())))
|
||||
// A name-generic write names its value type through a wired
|
||||
// generic, which only the row resolves, so such a node's
|
||||
// meta is emitted per row instead of shared across them.
|
||||
let named = node.output.shape.attrs.iter().any(|attr| crate::parsing::named_marker(&attr.marker).is_some());
|
||||
let meta = match named {
|
||||
false => quote!(Some(self::#layout_meta_fn())),
|
||||
true => {
|
||||
let element_spec = match &node.output.shape.element {
|
||||
ir::Element::Concrete(element) => {
|
||||
let ty = substitute_ident_types(element, assignments);
|
||||
quote!(gcore::record::ElementSpec::Concrete({ use gcore::record::{ElementWritePickHashed as _, ElementWritePickPlain as _}; (&gcore::record::ElementWritePick::<#ty>(::core::marker::PhantomData)).element_write() }))
|
||||
}
|
||||
_ => quote!(gcore::record::ElementSpec::Carried),
|
||||
};
|
||||
let meta = ir::layout_meta_tokens(&node, element_spec, &core_types, assignments);
|
||||
quote!(Some(#meta))
|
||||
}
|
||||
};
|
||||
(quote!(), quote!(#carrier_arg #(#value_layout_args)*), meta)
|
||||
}
|
||||
ir::NodeKind::Routing => {
|
||||
let source_layouts = base_indices.iter().map(|index| format_ident!("__layout_{index}"));
|
||||
|
||||
@@ -67,7 +67,10 @@ fn inputs(parsed: &ParsedNodeFn, fields: &[&ParsedField], generics: &[Ident]) ->
|
||||
shape: item_shape(&element, depth, &field.attribute_reads, generics),
|
||||
subject: subject(index, field, carrier_subject, routing.as_ref()),
|
||||
lend: matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. })),
|
||||
name_source: crate::parsing::named_source(&element),
|
||||
name_source: match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { name_source, .. }) => name_source.clone(),
|
||||
ParsedFieldType::Node(_) => None,
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -262,7 +265,10 @@ fn ilist_inner(ty: &Type) -> Option<Type> {
|
||||
|
||||
/// Emits the `LayoutMeta` literal from the IR. `element_spec` is supplied by the
|
||||
/// caller since it is the one row-dependent facet; the rest folds from the node.
|
||||
pub(crate) fn layout_meta_tokens(node: &Node, element_spec: TokenStream2, core_types: &TokenStream2) -> TokenStream2 {
|
||||
/// `assignments` binds the row's concrete types to the signature's generics,
|
||||
/// which a name-generic write needs: its value type is written as a projection
|
||||
/// through the wired generic, so only the row resolves it.
|
||||
pub(crate) fn layout_meta_tokens(node: &Node, element_spec: TokenStream2, core_types: &TokenStream2, assignments: &[(Ident, Type)]) -> TokenStream2 {
|
||||
let sources = layout_sources(node).into_iter().map(|index| index as u8);
|
||||
let reads = node
|
||||
.inputs
|
||||
@@ -275,7 +281,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),*] })
|
||||
});
|
||||
let writes = field_writes(&node.output.shape.attrs, core_types);
|
||||
let named_writes = named_field_writes(node, core_types);
|
||||
let named_writes = named_field_writes(node, core_types, assignments);
|
||||
let removes = node.output.removes.iter().map(|attr| {
|
||||
let marker = &attr.marker;
|
||||
let level = attr.level;
|
||||
@@ -369,13 +375,20 @@ fn field_writes(attrs: &[LevelAttr], core_types: &TokenStream2) -> Vec<TokenStre
|
||||
|
||||
/// 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> {
|
||||
fn named_field_writes(node: &Node, core_types: &TokenStream2, assignments: &[(Ident, Type)]) -> Vec<TokenStream2> {
|
||||
node.output
|
||||
.shape
|
||||
.attrs
|
||||
.iter()
|
||||
.filter_map(|attr| {
|
||||
let (placeholder, value) = crate::parsing::named_marker(&attr.marker)?;
|
||||
let value = qualify_projection(node, &crate::codegen::classify::substitute_ident_types(&value, assignments), assignments);
|
||||
// Only a row resolves a value type written through a generic, so
|
||||
// the row-free meta omits the write rather than naming a type that
|
||||
// is not in scope there.
|
||||
if node.generics.iter().any(|generic| mentions_ident(&value, &generic.ident)) {
|
||||
return None;
|
||||
}
|
||||
let input = name_input(node, &placeholder)? as u8;
|
||||
let level = attr.level;
|
||||
Some(quote!(#core_types::record::NamedWrite::of::<#placeholder, #value>(#input, #level)))
|
||||
@@ -383,6 +396,51 @@ fn named_field_writes(node: &Node, core_types: &TokenStream2) -> Vec<TokenStream
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Rewrites `V::Assoc` into `<Row as Bound>::Assoc` once the row assigns `V`.
|
||||
/// A value type reached through an associated type needs the generic's own
|
||||
/// bound to name the projection, which only the signature carries.
|
||||
fn qualify_projection(node: &Node, ty: &Type, assignments: &[(Ident, Type)]) -> Type {
|
||||
let Type::Path(path) = ty else { return ty.clone() };
|
||||
if path.qself.is_some() || path.path.segments.len() < 2 {
|
||||
return ty.clone();
|
||||
}
|
||||
let base = &path.path.segments[0].ident;
|
||||
let Some(generic) = node.generics.iter().find(|generic| &generic.ident == base) else {
|
||||
return ty.clone();
|
||||
};
|
||||
let Some((_, row)) = assignments.iter().find(|(ident, _)| ident == base) else {
|
||||
return ty.clone();
|
||||
};
|
||||
let mut bounds = generic.bounds.iter().filter_map(|bound| match bound {
|
||||
TypeParamBound::Trait(bound) => Some(&bound.path),
|
||||
_ => None,
|
||||
});
|
||||
let (Some(bound), None) = (bounds.next(), bounds.next()) else {
|
||||
return ty.clone();
|
||||
};
|
||||
let rest = path.path.segments.iter().skip(1);
|
||||
syn::parse_quote!(<#row as #bound>::#(#rest)::*)
|
||||
}
|
||||
|
||||
/// Whether `ty` names `ident` anywhere, so a type written through a generic
|
||||
/// can be told from one already concrete.
|
||||
fn mentions_ident(ty: &Type, ident: &Ident) -> bool {
|
||||
struct Search<'a> {
|
||||
ident: &'a Ident,
|
||||
found: bool,
|
||||
}
|
||||
|
||||
impl syn::visit::Visit<'_> for Search<'_> {
|
||||
fn visit_ident(&mut self, found: &Ident) {
|
||||
self.found |= found == self.ident;
|
||||
}
|
||||
}
|
||||
|
||||
let mut search = Search { ident, found: false };
|
||||
syn::visit::Visit::visit_type(&mut search, ty);
|
||||
search.found
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
@@ -772,6 +830,35 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_named_write_takes_its_name_from_the_declared_input() {
|
||||
let mut parsed = crate::parsing::parse_node_fn(
|
||||
quote!(category("")),
|
||||
quote!(
|
||||
fn tag<'e, V: WireValue>(ctx: impl Ctx + ExtractArena<'e>, content: f64, name: Named<Name0>, value: V) -> (f64, Attr<'e, Named<Name0, V::Row>>) {
|
||||
(content, Attr(value))
|
||||
}
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
parsed.replace_impl_trait_in_input();
|
||||
let node = build(&parsed);
|
||||
let attrs = &node.output.shape.attrs;
|
||||
assert_eq!(attrs.len(), 1, "the write is recorded on the output");
|
||||
let (placeholder, _) = crate::parsing::named_marker(&attrs[0].marker).expect("the marker is name-generic");
|
||||
assert_eq!(name_input(&node, &placeholder), Some(1), "the name comes from the `Named<Name0>` parameter");
|
||||
|
||||
// The row resolves the value type written through the wired generic.
|
||||
let assignments = vec![(syn::parse_quote!(V), syn::parse_quote!(f64))];
|
||||
let emitted = named_field_writes(&node, "e!(gcore), &assignments);
|
||||
assert_eq!(emitted.len(), 1, "the row carries the named write, got {emitted:?}");
|
||||
assert!(
|
||||
emitted[0].to_string().contains("WireValue"),
|
||||
"the projection is qualified by the generic's bound, got {}",
|
||||
emitted[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_record_remove() {
|
||||
assert_bridge(
|
||||
|
||||
@@ -369,6 +369,10 @@ impl Parse for NumberRange {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RegularParsedField {
|
||||
pub ty: Type,
|
||||
/// The placeholder this parameter names, written `Named<X>`. Its `ty` is
|
||||
/// rewritten to `String`, since the wire carries the name as constant text
|
||||
/// while the kernel takes only the placeholder.
|
||||
pub name_source: Option<Type>,
|
||||
/// `IList` nesting stripped from `ty` at parse; `ty` holds the element row.
|
||||
pub list_levels: u8,
|
||||
/// The original reference tokens when the parameter was written `&T`; `ty` holds the peeled inner type.
|
||||
@@ -1267,9 +1271,18 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
|
||||
}
|
||||
}
|
||||
|
||||
// A `Named<X>` parameter declares where `X`'s name is wired: the input
|
||||
// carries constant text, the kernel takes only the placeholder.
|
||||
let name_source = named_source(&ty);
|
||||
let ty = match name_source {
|
||||
Some(_) => parse_quote!(String),
|
||||
None => ty,
|
||||
};
|
||||
|
||||
Ok(ParsedField {
|
||||
pat_ident,
|
||||
ty: ParsedFieldType::Regular(RegularParsedField {
|
||||
name_source,
|
||||
exposed,
|
||||
number_soft_min,
|
||||
number_soft_max,
|
||||
@@ -1392,6 +1405,7 @@ impl ParsedNodeFn {
|
||||
widget_override: ParsedWidgetOverride::Hidden,
|
||||
ty: ParsedFieldType::Regular(RegularParsedField {
|
||||
ty,
|
||||
name_source: None,
|
||||
list_levels: 0,
|
||||
lend: None,
|
||||
exposed: false,
|
||||
@@ -1569,6 +1583,7 @@ mod tests {
|
||||
description: String::new(),
|
||||
widget_override: ParsedWidgetOverride::None,
|
||||
ty: ParsedFieldType::Regular(RegularParsedField {
|
||||
name_source: None,
|
||||
lend: None,
|
||||
list_levels: 0,
|
||||
ty: parse_quote!(f64),
|
||||
@@ -1667,6 +1682,7 @@ mod tests {
|
||||
description: String::new(),
|
||||
widget_override: ParsedWidgetOverride::None,
|
||||
ty: ParsedFieldType::Regular(RegularParsedField {
|
||||
name_source: None,
|
||||
lend: None,
|
||||
list_levels: 0,
|
||||
ty: parse_quote!(DVec2),
|
||||
@@ -1746,6 +1762,7 @@ mod tests {
|
||||
description: String::new(),
|
||||
widget_override: ParsedWidgetOverride::None,
|
||||
ty: ParsedFieldType::Regular(RegularParsedField {
|
||||
name_source: None,
|
||||
lend: None,
|
||||
list_levels: 0,
|
||||
ty: parse_quote!(f64),
|
||||
@@ -1823,6 +1840,7 @@ mod tests {
|
||||
description: String::new(),
|
||||
widget_override: ParsedWidgetOverride::None,
|
||||
ty: ParsedFieldType::Regular(RegularParsedField {
|
||||
name_source: None,
|
||||
lend: None,
|
||||
list_levels: 0,
|
||||
ty: parse_quote!(f64),
|
||||
@@ -1912,6 +1930,7 @@ mod tests {
|
||||
description: String::from("b"),
|
||||
widget_override: ParsedWidgetOverride::None,
|
||||
ty: ParsedFieldType::Regular(RegularParsedField {
|
||||
name_source: None,
|
||||
lend: None,
|
||||
list_levels: 0,
|
||||
ty: parse_quote!(f64),
|
||||
@@ -2004,6 +2023,7 @@ mod tests {
|
||||
description: String::new(),
|
||||
widget_override: ParsedWidgetOverride::None,
|
||||
ty: ParsedFieldType::Regular(RegularParsedField {
|
||||
name_source: None,
|
||||
lend: None,
|
||||
list_levels: 0,
|
||||
ty: parse_quote!(String),
|
||||
|
||||
@@ -232,6 +232,7 @@ impl PerPixelAdjustCodegen<'_> {
|
||||
widget_override: Default::default(),
|
||||
ty: ParsedFieldType::Regular(RegularParsedField {
|
||||
ty: parse_quote!(#wgpu_executor::WgpuExecutorHandle),
|
||||
name_source: None,
|
||||
list_levels: 0,
|
||||
lend: None,
|
||||
exposed: true,
|
||||
|
||||
Reference in New Issue
Block a user