diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 44c418b479..df2932ce85 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -1024,12 +1024,6 @@ fn replace_optional_f64_null(input: &str) -> String { result } -/// Serialized proto identifiers of the pre-flip Merge and Artboard layer internals. -/// A document containing any of them predates the leveled-records flip and rebuilds its -/// layer definitions through the same reset mechanism as the `SourceNodeIdNode` entry in -/// `document_migration_reset_node_definition`. -pub const FLIP_RESET_NODE_MARKERS: &[&str] = &["graphic_nodes::graphic::WriteAttributeNode"]; - pub fn document_migration_reset_node_definition(document_serialized_content: &str) -> bool { // Upgrade a document being opened to use fresh copies of all nodes if document_serialized_content.contains("node_output_index") { @@ -1051,12 +1045,6 @@ pub fn document_migration_reset_node_definition(document_serialized_content: &st return true; } - // The leveled-records flip replaced the layer internals; documents from before it rebuild - // their layer definitions. - if FLIP_RESET_NODE_MARKERS.iter().any(|marker| document_serialized_content.contains(marker)) { - return true; - } - false } @@ -2616,10 +2604,12 @@ mod tests { use super::*; #[test] - fn the_flip_reset_markers_keep_the_historical_merge_internals_spelling() { - // The node itself is removed; the marker matches its spelling in - // documents saved before the flip, which must stay stable. - assert_eq!(FLIP_RESET_NODE_MARKERS, &["graphic_nodes::graphic::WriteAttributeNode"]); + fn a_written_attribute_no_longer_resets_the_layer_definitions() { + // The node resolves natively again, so a document carrying it keeps + // its own layer internals instead of rebuilding them. + assert!(!document_migration_reset_node_definition( + r#"{"implementation":{"ProtoNode":{"name":"graphic_nodes::graphic::WriteAttributeNode"}}}"# + )); } #[test] diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index 6c721465be..61189d8dfa 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -737,6 +737,101 @@ mod test { ProtoNode::value(ConstructionArgs::Value(TaggedValue::String(value.to_string()).into()), vec![]) } + /// A record source, a constant name, and a value wired into the write node. + fn write_attribute_network(name: &str, value: TaggedValue) -> ProtoNetwork { + ProtoNetwork { + stack_need: 0, + inputs: vec![], + output: NodeId(3), + nodes: vec![ + (NodeId(0), ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(7.).into()), vec![])), + (NodeId(1), string_value(name)), + (NodeId(2), ProtoNode::value(ConstructionArgs::Value(value.into()), vec![])), + (NodeId(3), proto_node("graphic_nodes::graphic::WriteAttributeNode", vec![NodeId(0), NodeId(1), NodeId(2)])), + ], + } + } + + #[test] + fn a_constant_name_folds_into_the_written_layout() { + let executor = build_executor(write_attribute_network("novel:count", TaggedValue::F64(2.5))); + let arena = Arena::new(1 << 12).unwrap(); + let generations = []; + let scope = EvalScope::new(None, None, None, &generations, &arena); + let ctx = ContextImpl::root(&scope); + let handle = executor.tree().get(NodeId(3)).unwrap(); + let layout = handle.layout().clone(); + let offset = layout.offset_of("novel:count", 0).expect("the folded name names a field of the output layout"); + let edge = handle.duplicate().downcast_record::().unwrap(); + let frames = core_types::record::test_frames(executor.tree().stack_need()); + let GPoll::Final(value) = core_types::record::serve_input(&edge, &ctx, &frames) else { + panic!("expected a final record"); + }; + let rec = layout.rec(&value); + assert_eq!(unsafe { rec.element::() }, 7., "the element passes through the write"); + assert_eq!(unsafe { rec.read::(offset) }, 2.5, "the value lands under the folded name"); + } + + #[test] + fn a_census_name_writes_at_its_declared_value_type() { + let path = vec![NodeId(7), NodeId(8)]; + let executor = build_executor(write_attribute_network("editor:layer_path", TaggedValue::NodeIdPath(path.clone()))); + let arena = Arena::new(1 << 12).unwrap(); + let generations = []; + let scope = EvalScope::new(None, None, None, &generations, &arena); + let ctx = ContextImpl::root(&scope); + let handle = executor.tree().get(NodeId(3)).unwrap(); + let layout = handle.layout().clone(); + let offset = layout.offset_of("editor:layer_path", 0).expect("a census name folds onto its census field"); + let edge = handle.duplicate().downcast_record::().unwrap(); + let frames = core_types::record::test_frames(executor.tree().stack_need()); + let GPoll::Final(value) = core_types::record::serve_input(&edge, &ctx, &frames) else { + panic!("expected a final record"); + }; + assert_eq!(unsafe { layout.rec(&value).read::<&[NodeId]>(offset) }, path.as_slice()); + } + + #[test] + fn a_runtime_attribute_name_is_refused_when_the_graph_compiles() { + // The outer node's name comes off another node rather than sitting on + // the wire as text. It still types as a string, so only the fold can + // reject it, which is the case the design rules out entirely. + let mut network = ProtoNetwork { + stack_need: 0, + inputs: vec![], + output: NodeId(6), + nodes: vec![ + (NodeId(0), ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(7.).into()), vec![])), + (NodeId(1), string_value("novel:count")), + (NodeId(2), string_value("inner:name")), + (NodeId(3), ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(1.).into()), vec![])), + // Carries a string element, so its output types as a name. + (NodeId(4), proto_node("graphic_nodes::graphic::WriteAttributeNode", vec![NodeId(1), NodeId(2), NodeId(3)])), + (NodeId(5), ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(2.5).into()), vec![])), + (NodeId(6), proto_node("graphic_nodes::graphic::WriteAttributeNode", vec![NodeId(0), NodeId(4), NodeId(5)])), + ], + }; + network.resolve_types(&node_registry::NODE_REGISTRY).unwrap(); + let errors = network.compute_layouts().expect_err("a non-constant name must be refused"); + assert!( + errors.iter().any(|error| format!("{:?}", error.error).contains("must be a constant")), + "the refusal names the constant requirement, got {errors:?}" + ); + } + + #[test] + fn one_name_carries_one_value_type() { + // `opacity` is declared `f64` in the census, so writing a path there is + // a graph error rather than a second field of the same name. + let mut network = write_attribute_network("opacity", TaggedValue::NodeIdPath(vec![NodeId(1)])); + network.resolve_types(&node_registry::NODE_REGISTRY).unwrap(); + let errors = network.compute_layouts().expect_err("a name at two value types must be refused"); + assert!( + errors.iter().any(|error| format!("{:?}", error.error).contains("one name carries one value type")), + "the refusal names the one-name-one-type rule, got {errors:?}" + ); + } + #[test] fn the_clone_node_clones_the_element_out_of_its_record_wire() { let network = ProtoNetwork { diff --git a/node-graph/libraries/core-types/src/attribute.rs b/node-graph/libraries/core-types/src/attribute.rs index c2cbbd7c62..45bc397fcb 100644 --- a/node-graph/libraries/core-types/src/attribute.rs +++ b/node-graph/libraries/core-types/src/attribute.rs @@ -193,7 +193,27 @@ pub const NAMED_PLACEHOLDER: &str = ""; /// them takes two names; `V` fixes the value type. The name is absent by /// construction: it comes from the instance's constant text input, folded /// into the layout at graph compile time, so a computed name cannot exist. -pub struct Named(PhantomData (X, V)>); +/// +/// Written `Named` in parameter position, it declares where `X`'s name is +/// wired: the macro gives that input constant text, and the kernel receives +/// only the placeholder, since a folded name is a layout fact rather than a +/// value the kernel needs. +pub struct Named(PhantomData (X, V)>); + +impl Default for Named { + fn default() -> Self { + Named(PhantomData) + } +} + +/// The placeholders a signature distinguishes its name-generic attributes by. +/// A node writing one name uses [`Name0`]; a second name on the same node +/// takes [`Name1`], and so on, which is all the placeholder has to do. +pub struct Name0; +/// The second name-generic attribute in one signature. See [`Name0`]. +pub struct Name1; +/// The third name-generic attribute in one signature. See [`Name0`]. +pub struct Name2; // SAFETY: every obligation is discharged by `V`, which carries the same // contract at the same value type; only the name differs, and the compiler @@ -214,6 +234,63 @@ unsafe impl Attribute for Named { const REPARK: Option = V::REPARK; } +/// The value a name-generic write takes off the wire, and the row it lands +/// in. A plain row is its own wire form; a reference row's wire form is the +/// owned payload the kernel parks in the arena, so the field can carry a +/// borrow of it for the evaluation. +pub trait WireValue: 'static { + /// The row this value is written at, fixing the field's value type. + type Row: AttrValue; + + /// Moves the value into `arena` where the row borrows it, or hands it back + /// unchanged where the row stores it plainly. `None` reports exhaustion. + fn park<'e>(self, arena: &'e crate::arena::Arena) -> Option<::Value<'e>>; +} + +/// Declares [`WireValue`] rows. `for T` is a plain value, carried and stored +/// as itself; `Wire => Row` parks `Wire`'s payload in the arena and stores the +/// borrow `Row` names. +#[macro_export] +macro_rules! wire_value { + () => {}; + (for $value:ty; $($rest:tt)*) => { + impl $crate::attribute::WireValue for $value { + type Row = $value; + + fn park<'e>(self, _: &'e $crate::arena::Arena) -> ::core::option::Option<$value> { + ::core::option::Option::Some(self) + } + } + + $crate::wire_value!($($rest)*); + }; + ($wire:ty => $row:ty; $($rest:tt)*) => { + impl $crate::attribute::WireValue for $wire { + type Row = $row; + + fn park<'e>(self, arena: &'e $crate::arena::Arena) -> ::core::option::Option<<$row as $crate::attribute::AttrValue>::Value<'e>> { + let (parked, _) = arena.alloc(self)?; + ::core::option::Option::Some(::std::borrow::Borrow::borrow(parked)) + } + } + + $crate::wire_value!($($rest)*); + }; +} + +wire_value! { + for f64; + for u32; + for u64; + for bool; + for DVec2; + for DAffine2; + for crate::Color; + for crate::blending::BlendMode; + ::std::vec::Vec => NodeIdPath; + ::std::string::String => Text; +} + /// Interns a folded attribute name for the `&'static str` a layout field /// holds. Census names never reach here; a document's novel names are finite /// and repeat across instances, so the leak is one allocation per name. diff --git a/node-graph/libraries/core-types/src/record/mod.rs b/node-graph/libraries/core-types/src/record/mod.rs index 069fa7822e..cc3e4c51d2 100644 --- a/node-graph/libraries/core-types/src/record/mod.rs +++ b/node-graph/libraries/core-types/src/record/mod.rs @@ -25,7 +25,7 @@ pub use access::{Rec, RecordValue, apply_plan, borrow_element, erase_static, rea pub use frames::{FrameArena, FrameScope, Frames}; pub use input::{DerivedLazyInput, DerivedRecordInput, ElementInput, ElementLazyInput, LevelStatus, RecordExtract, RecordInput, RecordLazyInput, fill_frames, materialize_batch, materialize_level}; pub use layout::{ - ElToken, ElementSpec, ElementWrite, ElementWritePick, ElementWritePickHashed, ElementWritePickPlain, FieldDesc, FieldOffset, FieldWrite, InputReads, Layout, LayoutMeta, RecordLayout, copy_plan, + ElToken, ElementSpec, ElementWrite, ElementWritePick, ElementWritePickHashed, ElementWritePickPlain, FieldDesc, FieldOffset, FieldWrite, InputReads, Layout, LayoutMeta, NamedWrite, RecordLayout, copy_plan, element_dims, element_parked, element_write, element_write_hashed, empty_layout, }; pub use owned::{OwnedRecord, deepen_field_value, has_deep_element_glue, register_deep_element_clone, register_deep_field_value, replay_field_value}; diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 4d9aa52b3d..ad944d0276 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -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 = 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 = 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 = 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 diff --git a/node-graph/node-macro/src/codegen/entries.rs b/node-graph/node-macro/src/codegen/entries.rs index 9d347b7c68..c081f43fab 100644 --- a/node-graph/node-macro/src/codegen/entries.rs +++ b/node-graph/node-macro/src/codegen/entries.rs @@ -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}")); diff --git a/node-graph/node-macro/src/codegen/ir.rs b/node-graph/node-macro/src/codegen/ir.rs index 74edf6bfde..3a2dab824d 100644 --- a/node-graph/node-macro/src/codegen/ir.rs +++ b/node-graph/node-macro/src/codegen/ir.rs @@ -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 { /// 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 Vec { +fn named_field_writes(node: &Node, core_types: &TokenStream2, assignments: &[(Ident, Type)]) -> Vec { 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::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 { @@ -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, value: V) -> (f64, Attr<'e, Named>) { + (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` 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( diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index 7f0899d286..eac29563e7 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -369,6 +369,10 @@ impl Parse for NumberRange { #[derive(Clone, Debug)] pub struct RegularParsedField { pub ty: Type, + /// The placeholder this parameter names, written `Named`. 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, /// `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` 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), diff --git a/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs b/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs index 022af2556f..008ede1430 100644 --- a/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs +++ b/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs @@ -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, diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 2f73661051..2c1dac7ce5 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -1,4 +1,4 @@ -use core_types::attribute::{Attr, EditorLayerPath, Transform as TransformAttr}; +use core_types::attribute::{Attr, EditorLayerPath, Name0, Named, Transform as TransformAttr, WireValue}; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn}; use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt, Level}; @@ -368,6 +368,25 @@ pub fn stamp_layer_path<'e, T>(ctx: impl Ctx + ExtractArena<'e>, element: T, pat Ok((element, Attr(parked.as_slice()))) } +/// Writes `value` onto each lane under the attribute `name` names. The name is +/// constant text the compiler folds into the layout when the graph compiles, so +/// the write costs exactly what a marker node's does; a name that is not +/// constant is refused there rather than resolved here. +#[node_macro::node(category("Attributes: Write"))] +pub fn write_attribute<'e, T, V: WireValue>( + ctx: impl Ctx + ExtractArena<'e>, + content: T, + /// The attribute name, which the compiler folds and the kernel never reads. + _name: Named, + #[implementations(f64, u32, u64, bool, DVec2, DAffine2, Color, Vec, String)] value: V, +) -> Result<(T, Attr<'e, Named>), Interrupt> { + let parked = value.park(ctx.arena()).ok_or(GraphError { + kind: core_types::gpoll::ErrorKind::ArenaExhausted, + trace: Vec::new(), + })?; + Ok((content, Attr(parked))) +} + /// Joins two levels of the same type, the base's lanes followed by the new's. #[node_macro::node(category("General"), extent(extend_extent))] pub fn extend(