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<X, V>` 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 <noreply@anthropic.com>
This commit is contained in:
Dennis Kobert
2026-09-09 08:41:53 +00:00
parent 8266a61c52
commit 4f12de68a7
2 changed files with 249 additions and 0 deletions

View File

@@ -155,6 +155,198 @@ impl<A: Attribute> Default for RemoveAttr<A> {
}
}
/// 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<Self::Value<'a>>;
/// # Safety
/// `ptr` must point at a live field of this row's value type.
unsafe fn read_erased(ptr: *const u8) -> Box<dyn AnyAttributeValue>;
/// Re-parks an owned clone into fresh field storage, as [`Attribute::REPARK`].
const REPARK: Option<crate::list::ReparkFn> = 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<X, V>(PhantomData<fn() -> (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<X: 'static, V: AttrValue> Attribute for Named<X, V> {
const NAME: &'static str = NAMED_PLACEHOLDER;
type Value<'e> = V::Value<'e>;
fn from_stored<'a>(stored: &'a dyn std::any::Any) -> Option<Self::Value<'a>> {
V::from_stored(stored)
}
unsafe fn read_erased(ptr: *const u8) -> Box<dyn AnyAttributeValue> {
// SAFETY: the caller's contract, at `V`'s own value type.
unsafe { V::read_erased(ptr) }
}
const REPARK: Option<crate::list::ReparkFn> = 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<Mutex<std::collections::HashSet<&'static str>>> = 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<Self::Value<'a>> {
stored.downcast_ref::<$value>().copied()
}
unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box<dyn $crate::list::AnyAttributeValue> {
::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<Self::Value<'a>> {
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<dyn $crate::list::AnyAttributeValue> {
::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<Self::Value<'a>> {
stored.downcast_ref::<<$value as ::std::borrow::ToOwned>::Owned>().map(::std::borrow::Borrow::borrow)
}
unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box<dyn $crate::list::AnyAttributeValue> {
::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 {

View File

@@ -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<X: 'static, V: crate::attribute::AttrValue>(name_input: u8, level: u8) -> Self
where
V::Value<'static>: graphene_hash::CacheHash + PartialEq + 'static,
{
Self {
name_input,
template: FieldWrite::of::<crate::attribute::Named<X, V>>(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<FieldWrite>,
/// 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<NamedWrite>,
/// 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