From 4f12de68a7d24d49fed493cf380ca6b26d52acbf Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Wed, 9 Sep 2026 08:41:53 +0000 Subject: [PATCH] Carry a name-generic attribute's value type as its own row An attribute's name and its value type separate: `AttrValue` declares everything `Attribute` does except the name and its name-specific default, and `Named` pairs one with a name the graph supplies. The value type stays concrete in the signature while the name varies per instance, so a computed name has nowhere to come from. `LayoutMeta` gains the name-from-input variant the fold consumes: a `FieldWrite` template with every facet minted from the concrete type and only the name left as a placeholder, plus the input position the name's constant sits at. `fold_name` resolves one, preferring the census row's descriptor where the name is declared so a known name keeps its census default. `RecordLayout` carries the folded names so `set_layout` can resolve offsets without a marker's `NAME`. Co-Authored-By: Claude Fable 5 --- .../libraries/core-types/src/attribute.rs | 192 ++++++++++++++++++ .../libraries/core-types/src/record/layout.rs | 57 ++++++ 2 files changed, 249 insertions(+) diff --git a/node-graph/libraries/core-types/src/attribute.rs b/node-graph/libraries/core-types/src/attribute.rs index 1a727e965c..c2cbbd7c62 100644 --- a/node-graph/libraries/core-types/src/attribute.rs +++ b/node-graph/libraries/core-types/src/attribute.rs @@ -155,6 +155,198 @@ impl Default for RemoveAttr { } } +/// The value-type half of an attribute: everything [`Attribute`] declares +/// except the name and its name-specific default. A name-generic write pairs +/// one of these with a name taken from the graph, so the value type stays +/// concrete in the signature while the name varies per instance. +/// +/// # Safety +/// +/// The obligations are [`Attribute`]'s, at this trait's value type: +/// [`REPARK`](Self::REPARK) must be `Some` for every row whose +/// [`Value<'e>`](Self::Value) can carry a borrow shorter than `'static`, and +/// [`from_stored`](Self::from_stored) and [`read_erased`](Self::read_erased) +/// must be each other's inverse. +pub unsafe trait AttrValue: 'static { + /// The value type every write of this row shares, as [`Attribute::Value`]. + type Value<'e>: Copy + Default + std::fmt::Debug + Send + Sync + 'e; + + /// Borrows the value out of legacy list storage, as [`Attribute::from_stored`]. + fn from_stored<'a>(stored: &'a dyn std::any::Any) -> Option>; + + /// # Safety + /// `ptr` must point at a live field of this row's value type. + unsafe fn read_erased(ptr: *const u8) -> Box; + + /// Re-parks an owned clone into fresh field storage, as [`Attribute::REPARK`]. + const REPARK: Option = None; +} + +/// The [`Attribute::NAME`] a [`Named`] marker carries before the compiler +/// fills it. A layout never holds it: the fold replaces it with the +/// instance's constant, and a node whose name input is not constant is +/// refused at that same point. +pub const NAMED_PLACEHOLDER: &str = ""; + +/// A name-generic attribute write. `X` is a placeholder that distinguishes +/// name-generic attributes within one signature, so a node writing two of +/// 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)>); + +// SAFETY: every obligation is discharged by `V`, which carries the same +// contract at the same value type; only the name differs, and the compiler +// fold replaces the placeholder before a layout sees it. +unsafe impl Attribute for Named { + const NAME: &'static str = NAMED_PLACEHOLDER; + type Value<'e> = V::Value<'e>; + + fn from_stored<'a>(stored: &'a dyn std::any::Any) -> Option> { + V::from_stored(stored) + } + + unsafe fn read_erased(ptr: *const u8) -> Box { + // SAFETY: the caller's contract, at `V`'s own value type. + unsafe { V::read_erased(ptr) } + } + + const REPARK: Option = V::REPARK; +} + +/// 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. +pub fn intern_name(name: &str) -> &'static str { + static NAMES: LazyLock>> = LazyLock::new(|| Mutex::new(std::collections::HashSet::new())); + let mut names = NAMES.lock().unwrap(); + if let Some(interned) = names.get(name) { + return interned; + } + let interned: &'static str = Box::leak(name.to_owned().into_boxed_str()); + names.insert(interned); + interned +} + +/// Declares [`AttrValue`] rows, the value types a name-generic attribute can +/// be written at. `for T` implements the row on an existing plain value type; +/// `Row: &T` and `Row: Option<&T>` declare a token naming a reference value, +/// whose payload the writing kernel parks in the arena. +/// +/// ``` +/// core_types::named_value! { +/// /// Plain rows, implemented on the value type itself. +/// for f64; +/// /// A reference row, whose token names the borrowed value. +/// pub Text: &str; +/// } +/// ``` +#[macro_export] +macro_rules! named_value { + () => {}; + ($(#[$meta:meta])* for $value:ty; $($rest:tt)*) => { + // SAFETY: a plain value type cannot name `'e`, so no re-park is owed, + // and the two glue fns below are each other's inverse at `$value`. + unsafe impl $crate::attribute::AttrValue for $value { + type Value<'e> = $value; + + fn from_stored<'a>(stored: &'a dyn ::std::any::Any) -> ::core::option::Option> { + stored.downcast_ref::<$value>().copied() + } + + unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box { + ::std::boxed::Box::new(unsafe { ptr.cast::<$value>().read() }) + } + } + + $crate::named_value!($($rest)*); + }; + ($(#[$meta:meta])* $vis:vis $row:ident: Option<&$value:ty>; $($rest:tt)*) => { + $(#[$meta])* + $vis struct $row; + + // SAFETY: the reference arm emits `REPARK`, and the two glue fns below + // are each other's inverse at `Option<&$value>`. + unsafe impl $crate::attribute::AttrValue for $row { + type Value<'e> = ::core::option::Option<&'e $value>; + + fn from_stored<'a>(stored: &'a dyn ::std::any::Any) -> ::core::option::Option> { + stored + .downcast_ref::<::core::option::Option<<$value as ::std::borrow::ToOwned>::Owned>>() + .map(|owned| owned.as_ref().map(::std::borrow::Borrow::borrow)) + } + + unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box { + ::std::boxed::Box::new(unsafe { ptr.cast::<::core::option::Option<&$value>>().read() }.map(|value| <$value as ::std::borrow::ToOwned>::to_owned(value))) + } + + const REPARK: ::core::option::Option<$crate::list::ReparkFn> = { + unsafe fn repark(value: &dyn $crate::list::AnyAttributeValue, dst: *mut u8, arena: &$crate::arena::Arena) -> ::core::option::Option<()> { + let owned: &::core::option::Option<<$value as ::std::borrow::ToOwned>::Owned> = + value.as_any().downcast_ref().expect("an optional reference row replays its owned clone"); + let parked = match owned { + ::core::option::Option::Some(owned) => { + let (parked, _) = arena.alloc(<$value as ::std::borrow::ToOwned>::to_owned(::std::borrow::Borrow::borrow(owned)))?; + ::core::option::Option::Some(::std::borrow::Borrow::borrow(parked)) + } + ::core::option::Option::None => ::core::option::Option::None, + }; + unsafe { dst.cast::<::core::option::Option<&$value>>().write(parked) }; + ::core::option::Option::Some(()) + } + ::core::option::Option::Some(repark) + }; + } + + $crate::named_value!($($rest)*); + }; + ($(#[$meta:meta])* $vis:vis $row:ident: &$value:ty; $($rest:tt)*) => { + $(#[$meta])* + $vis struct $row; + + // SAFETY: the reference arm emits `REPARK`, and the two glue fns below + // are each other's inverse at `&$value`. + unsafe impl $crate::attribute::AttrValue for $row { + type Value<'e> = &'e $value; + + fn from_stored<'a>(stored: &'a dyn ::std::any::Any) -> ::core::option::Option> { + stored.downcast_ref::<<$value as ::std::borrow::ToOwned>::Owned>().map(::std::borrow::Borrow::borrow) + } + + unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box { + ::std::boxed::Box::new(unsafe { ptr.cast::<&$value>().read() }.to_owned()) + } + + const REPARK: ::core::option::Option<$crate::list::ReparkFn> = { + unsafe fn repark(value: &dyn $crate::list::AnyAttributeValue, dst: *mut u8, arena: &$crate::arena::Arena) -> ::core::option::Option<()> { + let owned: &<$value as ::std::borrow::ToOwned>::Owned = value.as_any().downcast_ref().expect("a reference row replays its owned clone"); + let (parked, _) = arena.alloc(<$value as ::std::borrow::ToOwned>::to_owned(::std::borrow::Borrow::borrow(owned)))?; + unsafe { dst.cast::<&$value>().write(::std::borrow::Borrow::borrow(parked)) }; + ::core::option::Option::Some(()) + } + ::core::option::Option::Some(repark) + }; + } + + $crate::named_value!($($rest)*); + }; +} + +named_value! { + for f64; + for u32; + for u64; + for bool; + for DVec2; + for DAffine2; + for crate::Color; + for crate::blending::BlendMode; + /// A document node path, the value type of `editor:layer_path`. + pub NodeIdPath: &[crate::uuid::NodeId]; + /// Free text, parked in the arena by the writing kernel. + pub Text: &str; +} + /// A census row: what is known about one declared attribute name. #[derive(Clone, Copy, Debug)] pub struct AttributeInfo { diff --git a/node-graph/libraries/core-types/src/record/layout.rs b/node-graph/libraries/core-types/src/record/layout.rs index 8946895ced..10c7763f61 100644 --- a/node-graph/libraries/core-types/src/record/layout.rs +++ b/node-graph/libraries/core-types/src/record/layout.rs @@ -366,6 +366,37 @@ pub struct RecordLayout { /// over input positions. Empty is the safe default: an uninstalled layout /// rebinds every input per lane. pub lane_invariant: u32, + /// The names the fold gave this node's name-from-input writes, in the + /// order the signature declares its placeholders. A marker-only node + /// leaves it empty; `set_layout` resolves its offsets through these + /// instead of through a marker's `NAME`. + pub named_writes: Vec<&'static str>, +} + +/// A write whose name comes from the graph rather than from a marker: the +/// input carrying the name, and the field descriptor with every facet but the +/// name already minted from the concrete value type. +#[derive(Clone, Copy, Debug)] +pub struct NamedWrite { + /// The proto input position holding the name's constant text. + pub name_input: u8, + /// The write this becomes once the fold supplies the name. + pub template: FieldWrite, +} + +impl NamedWrite { + /// The template for a name-generic marker's write at `level`. Every facet + /// but the name is the concrete value type's, exactly as for a census + /// marker; [`FieldWrite::name`] holds the placeholder until the fold runs. + pub fn of(name_input: u8, level: u8) -> Self + where + V::Value<'static>: graphene_hash::CacheHash + PartialEq + 'static, + { + Self { + name_input, + template: FieldWrite::of::>(level), + } + } } /// Declarative record-io metadata for a node type, emitted by the macro into @@ -387,6 +418,14 @@ pub struct LayoutMeta { pub element: ElementSpec, /// The attributes the node writes at its acting level. pub writes: Vec, + /// The attributes the node writes under a name taken from the graph. The + /// compiler fold turns each into a [`writes`](Self::writes) entry and + /// empties this, so a folded meta is indistinguishable from a marker + /// node's; one still carrying an entry has not been folded. + pub named_writes: Vec, + /// The names the fold gave [`named_writes`](Self::named_writes), in + /// placeholder order. Empty until the fold runs. + pub folded_names: Vec<&'static str>, /// The attributes removed from the base layout, as `(name, level)`. pub removes: Vec<(&'static str, u8)>, /// The depth change the node applies: `0` for elementwise and flip nodes, @@ -423,6 +462,8 @@ impl LayoutMeta { reads: Vec::new(), element: ElementSpec::Concrete(element), writes: Vec::new(), + named_writes: Vec::new(), + folded_names: Vec::new(), removes: Vec::new(), level_delta: 0, folded: None, @@ -472,8 +513,24 @@ impl LayoutMeta { frame_bytes, plan, lane_invariant: 0, + named_writes: self.folded_names.clone(), } } + + /// Folds `name` into the name-from-input write at `index`, taking the + /// census row's descriptor where the name is declared so a known name + /// keeps its census default, and the template's otherwise. The write + /// joins [`writes`](Self::writes) in placeholder order. + pub fn fold_name(&mut self, index: usize, name: &'static str) { + let named = self.named_writes[index]; + let census = attribute::info(name).map(|row| (row.field_write_at)(named.template.level)); + let write = match census { + Some(write) if write.type_id == named.template.type_id => write, + _ => FieldWrite { name, ..named.template }, + }; + self.writes.push(write); + self.folded_names.push(name); + } } /// Field-by-field carry from `from`'s layout into `to`'s, computed at