diff --git a/node-graph/libraries/core-types/src/attribute.rs b/node-graph/libraries/core-types/src/attribute.rs index 2328060032..c002054ff9 100644 --- a/node-graph/libraries/core-types/src/attribute.rs +++ b/node-graph/libraries/core-types/src/attribute.rs @@ -3,6 +3,11 @@ //! collects every declaration so name resolution, defaults, and diagnostics //! run at graph compile time. One name belongs to one marker, so a name can //! never mean two different types. +//! +//! Values are `Copy` and pack directly into record fields. Data with drop +//! glue rides the arena instead: the marker declares a reference value +//! (`&str`), the writing kernel parks the payload in the arena, and the +//! record field carries the eval-lifetime reference. use crate::list::AnyAttributeValue; use glam::{DAffine2, DVec2}; @@ -18,11 +23,15 @@ use std::sync::{LazyLock, Mutex}; pub trait Attribute: 'static { /// The name as it appears in documents and diagnostics. const NAME: &'static str; - /// The value type every read and write of this name shares. - type Value: AnyAttributeValue + Clone + Default + std::fmt::Debug; + /// The value type every read and write of this name shares. The lifetime + /// is the evaluation the value flows in; non-reference values ignore it. + type Value<'e>: Copy + Default + std::fmt::Debug; /// The name-specific default, filled where an item lacks the attribute. - fn default() -> Self::Value { - Self::Value::default() + /// Producing a value for any `'e` from no inputs, reference defaults can + /// only point at `'static` data, which is what lets the census fill them + /// as plain bytes. + fn default<'e>() -> Self::Value<'e> { + Default::default() } } @@ -30,23 +39,25 @@ pub trait Attribute: 'static { /// (yielding the declared default where the attribute is absent upstream), an /// `Attr` in the return tuple is a write, and the same marker on both /// sides is a modify. -pub struct Attr(pub A::Value); +pub struct Attr<'e, A: Attribute>(pub A::Value<'e>); -impl Deref for Attr { - type Target = A::Value; +impl<'e, A: Attribute> Deref for Attr<'e, A> { + type Target = A::Value<'e>; - fn deref(&self) -> &A::Value { + fn deref(&self) -> &A::Value<'e> { &self.0 } } -impl Clone for Attr { +impl<'e, A: Attribute> Clone for Attr<'e, A> { fn clone(&self) -> Self { - Attr(self.0.clone()) + *self } } -impl std::fmt::Debug for Attr { +impl<'e, A: Attribute> Copy for Attr<'e, A> {} + +impl<'e, A: Attribute> std::fmt::Debug for Attr<'e, A> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple(A::NAME).field(&self.0).finish() } @@ -61,20 +72,14 @@ pub struct AttributeInfo { pub default: fn() -> Box, pub size: usize, pub align: usize, - /// Whether the value is eligible for packed-record fields: no drop glue, - /// so its bytes copy freely. Droppable values stay on the erased path - /// until the per-type clone/drop glue lands. - pub packable: bool, /// Writes the declared default's bytes into a `size`-long slice. - /// Meaningful only when `packable`. pub write_default_bytes: fn(&mut [u8]), } fn write_default_bytes(out: &mut [u8]) { - assert!(!std::mem::needs_drop::(), "default bytes exist only for packable values"); - assert_eq!(out.len(), size_of::()); - let value = A::default(); - unsafe { std::ptr::copy_nonoverlapping((&raw const value).cast::(), out.as_mut_ptr(), size_of::()) }; + assert_eq!(out.len(), size_of::>()); + let value: A::Value<'static> = A::default(); + unsafe { std::ptr::copy_nonoverlapping((&raw const value).cast::(), out.as_mut_ptr(), size_of::>()) }; } /// All declared attribute names, keyed by name. @@ -84,15 +89,17 @@ pub static ATTRIBUTE_REGISTRY: LazyLock() { +pub fn register() +where + A::Value<'static>: AnyAttributeValue, +{ let info = AttributeInfo { name: A::NAME, - value_type: TypeId::of::(), - value_type_name: std::any::type_name::(), + value_type: TypeId::of::>(), + value_type_name: std::any::type_name::>(), default: || Box::new(A::default()), - size: size_of::(), - align: align_of::(), - packable: !std::mem::needs_drop::(), + size: size_of::>(), + align: align_of::>(), write_default_bytes: write_default_bytes::, }; let conflict = match ATTRIBUTE_REGISTRY.lock().unwrap().entry(A::NAME) { @@ -124,44 +131,65 @@ pub fn default_value(name: &str) -> Option> { /// core_types::attribute! { /// /// How visible the content is. /// pub Opacity("opacity"): f64 = 1.; -/// /// The item's transformation. -/// pub Transform("transform"): glam::DAffine2; +/// /// The item's label, parked in the arena by the writer. +/// pub Label("label"): &str; /// } /// ``` /// /// The trailing `= expr` is the name-specific default; without it the value -/// type's `Default` applies. +/// type's `Default` applies. A `&T` value carries the eval lifetime, so its +/// default must be `'static` data. #[macro_export] macro_rules! attribute { - ($($(#[$meta:meta])* $vis:vis $marker:ident($name:literal): $value:ty $(= $default:expr)?;)+) => { - $( - $(#[$meta])* - $vis struct $marker; + () => {}; + ($(#[$meta:meta])* $vis:vis $marker:ident($name:literal): &$value:ty $(= $default:expr)?; $($rest:tt)*) => { + $(#[$meta])* + $vis struct $marker; - impl $crate::attribute::Attribute for $marker { - const NAME: &'static str = $name; - type Value = $value; - $( - fn default() -> $value { - $default - } - )? + impl $crate::attribute::Attribute for $marker { + const NAME: &'static str = $name; + type Value<'e> = &'e $value; + $( + fn default<'e>() -> Self::Value<'e> { + $default + } + )? + } + + $crate::attribute!(@register $marker); + $crate::attribute!($($rest)*); + }; + ($(#[$meta:meta])* $vis:vis $marker:ident($name:literal): $value:ty $(= $default:expr)?; $($rest:tt)*) => { + $(#[$meta])* + $vis struct $marker; + + impl $crate::attribute::Attribute for $marker { + const NAME: &'static str = $name; + type Value<'e> = $value; + $( + fn default<'e>() -> Self::Value<'e> { + $default + } + )? + } + + $crate::attribute!(@register $marker); + $crate::attribute!($($rest)*); + }; + (@register $marker:ident) => { + const _: () = { + #[cfg(not(target_family = "wasm"))] + #[$crate::ctor::ctor] + fn register() { + $crate::attribute::register::<$marker>(); } - const _: () = { - #[cfg(not(target_family = "wasm"))] - #[$crate::ctor::ctor] - fn register() { - $crate::attribute::register::<$marker>(); - } - - #[cfg(target_family = "wasm")] - #[unsafe(export_name = concat!("__node_registry_attribute_", stringify!($marker)))] - extern "C" fn register() { - $crate::attribute::register::<$marker>(); - } - }; - )+ + #[cfg(target_family = "wasm")] + #[unsafe(export_name = concat!("__node_registry_attribute_", stringify!($marker)))] + extern "C" fn register() { + $crate::attribute::register::<$marker>(); + } + }; }; } @@ -180,7 +208,7 @@ attribute! { /// Artboard's top-left corner in document coordinates. pub Location("location"): DVec2; /// A regex named-capture-group's name, or empty for unnamed groups. - pub Name("name"): String; + pub Name("name"): &str; } #[cfg(test)] @@ -198,7 +226,7 @@ mod tests { #[test] fn name_specific_default_overrides_the_type_default() { assert_eq!(::default(), 1.); - assert_eq!(::default(), String::new()); + assert_eq!(::default(), ""); } #[test] @@ -207,6 +235,13 @@ mod tests { assert_eq!(*value.as_any().downcast_ref::().unwrap(), 1.); } + #[test] + fn reference_values_register_at_the_static_instantiation() { + let row = info("name").unwrap(); + assert_eq!(row.value_type, TypeId::of::<&'static str>()); + assert_eq!(row.size, size_of::<&str>()); + } + #[test] fn reregistration_at_the_same_type_is_idempotent() { register::(); @@ -220,7 +255,7 @@ mod tests { struct Conflict; impl Attribute for Conflict { const NAME: &'static str = "opacity"; - type Value = bool; + type Value<'e> = bool; } register::(); } diff --git a/node-graph/libraries/core-types/src/context.rs b/node-graph/libraries/core-types/src/context.rs index e3247be208..b122f673d1 100644 --- a/node-graph/libraries/core-types/src/context.rs +++ b/node-graph/libraries/core-types/src/context.rs @@ -624,7 +624,6 @@ pub struct EvalScope<'a> { pointer_position: Option, generations: &'a [(SourceId, u64)], arena: &'a Arena, - frame: Option<&'a crate::record::Frame>, hash: u64, } @@ -636,19 +635,12 @@ impl<'a> EvalScope<'a> { pointer_position, generations, arena, - frame: None, hash: 0, }; scope.hash = scope.compute_hash(|_| true); scope } - /// Attaches the record frame. Operational like the arena: not part of the - /// scope hash. - pub fn with_frame(&self, frame: &'a crate::record::Frame) -> EvalScope<'a> { - EvalScope { frame: Some(frame), ..*self } - } - pub fn with_real_time(&self, real_time: Option) -> EvalScope<'a> { let mut scope = EvalScope { real_time, ..*self }; scope.hash = scope.compute_hash(|_| true); @@ -708,23 +700,6 @@ impl<'a> EvalScope<'a> { pub fn arena(&self) -> &'a Arena { self.arena } - - pub fn frame(&self) -> Option<&'a crate::record::Frame> { - self.frame - } -} - -/// Read access to the record frame, the operational sibling of -/// [`ExtractArena`]. `None` on contexts whose scope carries no frame (a -/// graph without record edges allocates none). -pub trait ExtractFrame<'e> { - fn frame(&self) -> Option<&'e crate::record::Frame>; -} - -impl<'a> ExtractFrame<'a> for ContextImpl<'a> { - fn frame(&self) -> Option<&'a crate::record::Frame> { - self.scope.frame() - } } pub trait ExtractArena { diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index e52edcded3..b0d5888115 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -954,15 +954,6 @@ impl List { } } - /// Creates a list from element values with no attributes. - pub fn from_element_values(element: Vec) -> Self { - let len = element.len(); - Self { - element, - attributes: Attributes::with_len(len), - } - } - /// Creates a list containing a single item from the given [`Item`], preserving its attributes. pub fn new_from_item(item: Item) -> Self { let mut attributes = Attributes::new(); @@ -1009,11 +1000,6 @@ impl List { self.element.iter() } - /// Consumes the list, returning its element values and dropping its attributes. - pub fn into_element_values(self) -> Vec { - self.element - } - /// Returns an iterator over mutable references to all element values. pub fn iter_element_values_mut(&mut self) -> std::slice::IterMut<'_, T> { self.element.iter_mut() @@ -1101,20 +1087,6 @@ impl List { } } - /// Removes and returns the attribute column for the given key, if present. - pub fn take_attribute_dyn(&mut self, key: &str) -> Option { - let position = self.attributes.attributes.iter().position(|(k, _)| k == key)?; - Some(AttributeDyn(self.attributes.attributes.remove(position).1)) - } - - /// Replaces (or adds) an attribute, taking ownership of the column, whose length must equal this list's item count. - pub fn insert_attribute_dyn(&mut self, key: impl Into, column: AttributeDyn) { - assert_eq!(column.len(), self.element.len(), "attribute column length must match the list's item count"); - let key = key.into(); - self.attributes.attributes.retain(|(k, _)| k != &key); - self.attributes.attributes.push((key, column.0)); - } - /// Removes the entire attribute for the given key, if present. pub fn remove_attribute(&mut self, key: &str) { self.attributes.remove_attribute(key); diff --git a/node-graph/libraries/core-types/src/node.rs b/node-graph/libraries/core-types/src/node.rs index 23abf38d7c..b660c1d5f7 100644 --- a/node-graph/libraries/core-types/src/node.rs +++ b/node-graph/libraries/core-types/src/node.rs @@ -71,6 +71,14 @@ pub trait Node { None } + /// The record layout of this node's output; `None` for element-only + /// producers. Consumers read their carrier's layout through this at + /// wiring, and the wiring layer derives stack sizing from the same + /// layouts, in the dynamic executor and exported source alike. + fn layout(&self) -> Option<&crate::record::Layout> { + None + } + fn eval_batch<'a>(&self, input: &'a Input, range: Range, scratch: Option<&'a mut [MaybeUninit]>) -> BatchStatus<'a, Self::Output> where Input: InjectIndex + Copy, @@ -133,6 +141,10 @@ where (**self).serialize() } + fn layout(&self) -> Option<&crate::record::Layout> { + (**self).layout() + } + fn eval_batch<'a>(&self, input: &'a Input, range: Range, scratch: Option<&'a mut [MaybeUninit]>) -> BatchStatus<'a, Self::Output> where Input: InjectIndex + Copy, @@ -159,6 +171,10 @@ where (**self).serialize() } + fn layout(&self) -> Option<&crate::record::Layout> { + (**self).layout() + } + fn eval_batch<'a>(&self, input: &'a Input, range: Range, scratch: Option<&'a mut [MaybeUninit]>) -> BatchStatus<'a, Self::Output> where Input: InjectIndex + Copy, diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index bcb07acbaf..82e68c3a11 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -1,15 +1,14 @@ //! The packed-record tier at rank 0. A record is the element at offset 0 //! plus one field per written attribute; its [`Layout`] is computed at //! wiring from the upstream write set and never serialized. Records live as -//! per-lane views in a per-worker [`Frame`] whose slots are assigned at -//! wiring, and kernels route them as opaque [`RecordValue`]s that carry +//! per-lane views on the per-thread record [`stack`], claimed per +//! evaluation, and kernels route them as opaque [`RecordValue`]s that carry //! their provenance. Only generated or wiring code touches offsets, so a //! safe kernel cannot misalign a field. use crate::attribute; use crate::gpoll::GPoll; use crate::node::Node; -use std::cell::UnsafeCell; /// One field of a [`Layout`]: a (name, level) key resolved to an offset. /// Levels are numbered innermost-out; only level 0 exists at rank 0. @@ -97,6 +96,13 @@ impl Layout { } } +/// The stand-in fed to a kernel's unbounded `element: T` parameter. The type +/// system forces the kernel to route it to the element position of its return +/// tuple, so the passthrough is explicit in the signature while the lowering +/// carries the element bytes untyped through the copy plan. +#[derive(Clone, Copy, Debug, Default)] +pub struct ElToken; + /// A view of one record: a pointer whose layout is proven at wiring. #[derive(Clone, Copy, Debug)] pub struct Rec(*const u8); @@ -148,58 +154,89 @@ impl<'e> RecordValue<'e> { } } -/// Assigns frame slots at wiring: a bump allocator over slot sizes, aligned -/// to at most 8 (record layouts never exceed word alignment). -#[derive(Debug, Default)] -pub struct FrameLayout { - size: usize, -} +/// The per-thread record stack: every record evaluation claims its activation +/// frame at the stack pointer and evaluates its carrier beyond it, so slot +/// addresses are a property of the evaluating thread and no global assignment +/// exists. Thread-local by construction, so access is single-threaded without +/// claims or gates. Records are overwritten per lane and never touch the +/// arena. +pub mod stack { + use std::cell::Cell; -impl FrameLayout { - pub fn slot(&mut self, layout: &Layout) -> usize { - assert!(layout.align <= 8, "record layouts align to at most 8"); - self.size = self.size.next_multiple_of(layout.align.max(1)); - let offset = self.size; - self.size += layout.size; - offset + struct Stack { + base: Cell<*mut u8>, + capacity: Cell, + sp: Cell, } - pub fn size(&self) -> usize { - self.size - } -} - -/// The per-worker record frame: every record-producing slot lives at a -/// wiring-assigned offset. Slots are overwritten per lane and never touch -/// the arena. -pub struct Frame { - words: Box<[UnsafeCell]>, -} - -// SAFETY: a frame belongs to one worker; slot writes happen only inside that -// worker's evaluation, and wiring assigns disjoint offsets per slot. -unsafe impl Send for Frame {} -unsafe impl Sync for Frame {} - -impl Frame { - pub fn new(size: usize) -> Self { - Self { - words: (0..size.div_ceil(8).max(1)).map(|_| UnsafeCell::new(0)).collect(), + impl Stack { + fn free(&self) { + let base = self.base.get(); + if !base.is_null() { + drop(unsafe { Vec::from_raw_parts(base.cast::(), 0, self.capacity.get() / 8) }); + } } } - /// # Safety - /// `offset` must be a slot offset assigned by [`FrameLayout`] for this - /// frame, and the caller must be the slot's owning node evaluation. - pub unsafe fn slot(&self, offset: usize) -> *mut u8 { - debug_assert!(offset <= self.words.len() * 8); - unsafe { self.words.as_ptr().cast::().cast_mut().add(offset) } + impl Drop for Stack { + fn drop(&mut self) { + self.free(); + } } -} -impl std::fmt::Debug for Frame { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Frame").field("bytes", &(self.words.len() * 8)).finish() + thread_local! { + static STACK: Stack = const { + Stack { + base: Cell::new(std::ptr::null_mut()), + capacity: Cell::new(0), + sp: Cell::new(0), + } + }; + } + + /// Ensures the calling thread's stack holds `bytes`, the root's wiring- + /// derived stack need, and resets the stack pointer. Called only between + /// evaluations, like the arena reset: nothing survives it, so frames + /// leaked by an interrupted evaluation are reclaimed here. + pub fn reserve(bytes: usize) { + STACK.with(|stack| { + stack.sp.set(0); + if stack.capacity.get() >= bytes { + return; + } + let words = bytes.div_ceil(8).max(1); + let mut memory = vec![0u64; words]; + let base = memory.as_mut_ptr().cast::(); + std::mem::forget(memory); + stack.free(); + stack.base.set(base); + stack.capacity.set(words * 8); + }); + } + + /// Claims `bytes` (rounded to word alignment) at the stack pointer and + /// advances past them. The region stays claimed until [`pop`], and stays + /// readable until the next `push`. `reserve` derived from the root's + /// stack need makes the capacity bound exact, so overflow is a debug + /// assertion, not a hot-path branch. + pub fn push(bytes: usize) -> *mut u8 { + STACK.with(|stack| { + let sp = stack.sp.get(); + let next = sp + bytes.next_multiple_of(8); + debug_assert!(next <= stack.capacity.get(), "record stack overflow: reserve() must cover the root's stack need"); + stack.sp.set(next); + unsafe { stack.base.get().add(sp) } + }) + } + + /// Returns the stack pointer to `frame`, a pointer earlier returned by + /// [`push`] on this thread, releasing everything above it. + pub fn pop(frame: *mut u8) { + STACK.with(|stack| { + let offset = frame as usize - stack.base.get() as usize; + debug_assert!(offset <= stack.sp.get(), "pop target must lie within the claimed stack"); + stack.sp.set(offset); + }); } } @@ -221,6 +258,13 @@ pub fn copy_plan(from: &Layout, to: &Layout, carry_element: bool) -> Vec<(usize, plan } +/// # Safety +/// `offset` must be a field offset of the layout of the record under +/// construction at `dst` and `T` the field's type; both are proven at wiring. +pub unsafe fn write_field(dst: *mut u8, offset: usize, value: T) { + unsafe { dst.add(offset).cast::().write(value) } +} + /// # Safety /// `src` must be a record of the plan's source layout and `dst` a buffer of /// the plan's target layout; both are proven at wiring. @@ -235,7 +279,6 @@ pub unsafe fn apply_plan(src: Rec, dst: *mut u8, plan: &[(usize, usize, usize)]) fn default_fill_bytes(name: &str, size: usize) -> Box<[u8]> { let mut bytes = vec![0u8; size].into_boxed_slice(); if let Some(info) = attribute::info(name) - && info.packable && info.size == size { (info.write_default_bytes)(&mut bytes); @@ -244,18 +287,18 @@ fn default_fill_bytes(name: &str, size: usize) -> Box<[u8]> { } /// A routing source's wiring-resolved translation: field moves into the -/// consumer's frame buffer plus census default fill for union fields the -/// source lacks. Absent when the source's layout already equals the union, -/// in which case the record pointer forwards untouched. +/// union layout plus census default fill for union fields the source lacks. +/// Absent when the source's layout already equals the union, in which case +/// the record pointer forwards untouched. #[derive(Debug)] pub struct SourcePlan { moves: Vec<(usize, usize, usize)>, fills: Vec<(usize, Box<[u8]>)>, - slot: usize, + union_bytes: usize, } impl SourcePlan { - pub fn new(source: &Layout, union: &Layout, slot: usize) -> Option { + pub fn new(source: &Layout, union: &Layout) -> Option { if source == union { return None; } @@ -266,15 +309,18 @@ impl SourcePlan { .filter(|field| source.offset_of(field.name, field.level).is_none()) .map(|field| (field.offset, default_fill_bytes(field.name, field.size))) .collect(); - Some(SourcePlan { moves, fills, slot }) + Some(SourcePlan { + moves, + fills, + union_bytes: union.size, + }) } /// # Safety - /// `src` must be a record of this plan's source layout and `frame` the - /// frame whose slot was assigned to this plan at wiring. - pub unsafe fn translate(&self, src: Rec, frame: &Frame) -> Rec { + /// `src` must be a record of this plan's source layout and `dst` a + /// buffer of the plan's union layout. + pub unsafe fn translate(&self, src: Rec, dst: *mut u8) -> Rec { unsafe { - let dst = frame.slot(self.slot); apply_plan(src, dst, &self.moves); for (offset, bytes) in &self.fills { std::ptr::copy_nonoverlapping(bytes.as_ptr(), dst.add(*offset), bytes.len()); @@ -288,36 +334,37 @@ impl SourcePlan { /// Evaluating it yields the source's record translated to the union layout /// (or forwarded untouched when the layouts already agree), so the kernel /// holds and returns record values without ever seeing the representation. +/// A translation claims its landing region from the record stack without +/// popping, so the value survives sibling evaluations; the region is +/// released with the enclosing frame, which bounds claims at one per source +/// evaluation the kernel performs. pub struct RecordSource { edge: N, plan: Option, } impl RecordSource { - pub fn wire(edge: N, source: &Layout, union: &Layout, slot: usize) -> Self { + pub fn wire(edge: N, source: &Layout, union: &Layout) -> Self { Self { edge, - plan: SourcePlan::new(source, union, slot), + plan: SourcePlan::new(source, union), } } } impl<'e, C, N> Node for RecordSource where - C: crate::context::ExtractFrame<'e>, N: Node>, { type Output = RecordValue<'e>; fn eval(&self, input: &C) -> GPoll> { - let value = self.edge.eval(input); match &self.plan { - None => value, + None => self.edge.eval(input), Some(plan) => { - let Some(frame) = crate::context::ExtractFrame::frame(input) else { - return GPoll::error("record frame missing"); - }; - value.map(|value| RecordValue::from_rec(unsafe { plan.translate(value.rec(), frame) })) + let dst = stack::push(plan.union_bytes); + let value = self.edge.eval(input); + value.map(|value| RecordValue::from_rec(unsafe { plan.translate(value.rec(), dst) })) } } } @@ -356,27 +403,15 @@ mod tests { assert!(Layout::union(&[&a, &b]).offset_of("length", 0).is_some()); } - #[test] - fn frame_slots_bump_aligned() { - let flag = Layout::default().with_writes(0, (1, 1), &[]); - let wide = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]); - let mut frame = FrameLayout::default(); - assert_eq!(frame.slot(&flag), 0); - assert_eq!(frame.slot(&wide), 8); - assert_eq!(frame.size(), 24); - } - #[test] fn translation_moves_fields_and_fills_census_defaults() { let source = Layout::default().with_writes(0, (8, 8), &[f64_field("length")]); let union = Layout::union(&[&source, &Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")])]); - let mut frame_layout = FrameLayout::default(); - let slot = frame_layout.slot(&union); - let frame = Frame::new(frame_layout.size()); - let plan = SourcePlan::new(&source, &union, slot).unwrap(); + let plan = SourcePlan::new(&source, &union).unwrap(); let record = [5f64, 7f64]; - let translated = unsafe { plan.translate(Rec::new(record.as_ptr().cast()), &frame) }; + let mut buffer = vec![0u64; union.size.div_ceil(8)]; + let translated = unsafe { plan.translate(Rec::new(record.as_ptr().cast()), buffer.as_mut_ptr().cast()) }; assert_eq!(unsafe { translated.element::() }, 5.); assert_eq!(unsafe { translated.read::(union.offset_of("length", 0).unwrap()) }, 7.); assert_eq!(unsafe { translated.read::(union.offset_of("opacity", 0).unwrap()) }, 1.); @@ -385,6 +420,45 @@ mod tests { #[test] fn identity_layouts_forward() { let layout = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]); - assert!(SourcePlan::new(&layout, &layout.clone(), 0).is_none()); + assert!(SourcePlan::new(&layout, &layout.clone()).is_none()); + } + + #[test] + fn stack_frames_nest_and_release() { + stack::reserve(64); + let outer = stack::push(24); + let inner = stack::push(8); + assert_eq!(inner as usize - outer as usize, 24); + stack::pop(outer); + assert_eq!(stack::push(8), outer); + stack::pop(outer); + } + + #[test] + fn stack_rounds_frames_to_word_alignment() { + stack::reserve(64); + let first = stack::push(21); + let second = stack::push(8); + assert_eq!(second as usize - first as usize, 24); + stack::pop(first); + } + + #[test] + fn each_thread_gets_its_own_stack() { + stack::reserve(64); + let here = stack::push(8); + let here_address = here as usize; + std::thread::scope(|scope| { + scope + .spawn(move || { + stack::reserve(64); + let there = stack::push(8); + assert_ne!(here_address, there as usize, "stacks are per thread"); + stack::pop(there); + }) + .join() + .unwrap(); + }); + stack::pop(here); } } diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index fb42062fb1..bfc0db53f1 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -154,6 +154,11 @@ where unsafe { self.ptr.as_ref() }.serialize() } + fn layout(&self) -> Option<&crate::record::Layout> { + // SAFETY: as in eval. + unsafe { self.ptr.as_ref() }.layout() + } + fn eval_batch<'a>(&self, input: &'a Input, range: std::ops::Range, scratch: Option<&'a mut [std::mem::MaybeUninit]>) -> crate::node::BatchStatus<'a, Self::Output> where Input: crate::context::InjectIndex + Copy, @@ -167,6 +172,7 @@ pub struct EdgeHandle { node: Box, share: fn(&DynEdge) -> Box, serialize: fn(&DynEdge) -> Option>, + layout: fn(&DynEdge) -> Option<&crate::record::Layout>, ty: Type, } @@ -201,6 +207,7 @@ impl EdgeHandle { node: Box::new(SharedEdge::new(node)), share: |edge| Box::new(edge.downcast_ref::>().expect("share hook matches the stored edge type").share()), serialize: |edge| Node::::serialize(edge.downcast_ref::>().expect("serialize hook matches the stored edge type")), + layout: |edge| Node::::layout(edge.downcast_ref::>().expect("layout hook matches the stored edge type")), ty, } } @@ -214,6 +221,7 @@ impl EdgeHandle { node: (self.share)(&*self.node), share: self.share, serialize: self.serialize, + layout: self.layout, ty: self.ty.clone(), } } @@ -222,6 +230,10 @@ impl EdgeHandle { (self.serialize)(&*self.node) } + pub fn layout(&self) -> Option<&crate::record::Layout> { + (self.layout)(&*self.node) + } + pub fn downcast(self) -> Result>, ConstructionError> { self.downcast_erased(edge_type::()) } diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index d287a2c3bd..85ee74ae5d 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -39,6 +39,13 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn // Separate data fields from regular fields let (data_fields, regular_fields): (Vec<_>, Vec<_>) = fields.iter().partition(|f| f.is_data_field); + let record = record_shape(parsed); + let record_skips_carrier = record.as_ref().is_some_and(|shape| shape.skips_carrier()); + // Record nodes with a `_: ()` primary input have no carrier edge; the unit + // field stays visible in the metadata but claims no struct field. + let struct_regular_fields: Vec<_> = regular_fields.iter().skip(record_skips_carrier as usize).copied().collect(); + let struct_regular_field_names: Vec<_> = struct_regular_fields.iter().map(|f| &f.pat_ident.ident).collect(); + // Extract function generics used by data fields let data_field_generics: Vec<_> = fn_generics .iter() @@ -58,7 +65,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn .collect(); // Node generics for regular fields (Node0, Node1, ...) - let node_generics: Vec = regular_fields.iter().enumerate().map(|(i, _)| format_ident!("Node{}", i)).collect(); + let node_generics: Vec = struct_regular_fields.iter().enumerate().map(|(i, _)| format_ident!("Node{}", i)).collect(); // Extract just the idents from data_field_generics for struct type parameters let data_field_generic_idents: Vec = data_field_generics @@ -108,16 +115,36 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote! { pub(super) #name: #ty } }); - let regular_field_defs = regular_field_names.iter().zip(node_generics.iter()).map(|(name, r#gen)| { + let regular_field_defs = struct_regular_field_names.iter().zip(node_generics.iter()).map(|(name, r#gen)| { quote! { pub(super) #name: #r#gen } }); + let record_state_fields: Vec = match &record { + Some(shape) => { + let mut state = vec![quote!(pub(super) __layout: gcore::record::Layout)]; + if !shape.skips_carrier() { + state.push(quote!(pub(super) __plan: ::std::vec::Vec<(usize, usize, usize)>)); + } + state.push(quote!(pub(super) __frame_bytes: usize)); + state.extend((0..parsed.attribute_reads.len()).map(|index| { + let slot = format_ident!("__read_{index}"); + quote!(pub(super) #slot: Option) + })); + state.extend((0..shape.write_markers.len()).map(|index| { + let slot = format_ident!("__write_{index}"); + quote!(pub(super) #slot: usize) + })); + state + } + None => Vec::new(), + }; + let async_source = parsed.injects_async_source_fields(); let slot_value_type = slot_value_type(output_type); let slot_field = async_source .then(|| quote! { pub(super) slot: std::sync::Arc>>>> }) .into_iter(); - let struct_fields = data_field_defs.chain(regular_field_defs).chain(slot_field); + let struct_fields = data_field_defs.chain(regular_field_defs).chain(record_state_fields.iter().cloned()).chain(slot_field); // Only regular fields have UI metadata (data fields are internal state) let widget_override: Vec<_> = regular_fields @@ -215,7 +242,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let all_implementation_types = all_implementation_types.chain(input.implementations.iter().cloned()); // Only regular fields are parameters to new() - let new_args = node_generics.iter().zip(regular_field_names.iter()).map(|(r#gen, name)| { + let new_args = node_generics.iter().zip(struct_regular_field_names.iter()).map(|(r#gen, name)| { quote! { #name: #r#gen } }); @@ -223,14 +250,16 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let data_inits = data_field_names.iter().map(|name| { quote! { #name: Default::default() } }); - let regular_inits = regular_field_names.iter().map(|name| { + let regular_inits = struct_regular_field_names.iter().map(|name| { quote! { #name } }); let slot_init = async_source.then(|| quote! { slot: Default::default() }).into_iter(); let all_field_inits = data_inits.chain(regular_inits).chain(slot_init); // Data fields may not implement Copy, PartialEq, etc., so only derive Debug and Clone - let struct_derives = if data_fields.is_empty() && !async_source { + let struct_derives = if record.is_some() { + quote!(#[derive(Debug, Clone)]) + } else if data_fields.is_empty() && !async_source { quote!(#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]) } else { quote!(#[derive(Debug, Clone)]) @@ -253,6 +282,24 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn register_metadata(); } }; + // Record nodes construct through the generated `wire` fn, which resolves + // offsets from the carrier layout; `new` cannot fill that state. + let new_impl = match record.is_none() { + true => quote! { + #[automatically_derived] + impl<'n, #(#struct_generic_params,)*> #struct_name<#(#struct_type_params,)*> + { + #[allow(clippy::too_many_arguments)] + pub fn new(#(#new_args,)*) -> Self { + Self { + #(#all_field_inits,)* + } + } + } + }, + false => quote!(), + }; + let import_name = format_ident!("_IMPORT_STUB_{}", mod_name.to_string().to_case(Case::UpperSnake)); let node = generate_node_impl(crate_ident, parsed)?; let node_in_mod = node.in_mod; @@ -330,16 +377,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn #(#struct_fields,)* } - #[automatically_derived] - impl<'n, #(#struct_generic_params,)*> #struct_name<#(#struct_type_params,)*> - { - #[allow(clippy::too_many_arguments)] - pub fn new(#(#new_args,)*) -> Self { - Self { - #(#all_field_inits,)* - } - } - } + #new_impl #node_in_mod @@ -612,6 +650,11 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn top_level: quote!(), }); } + let record_token = match record.as_ref().map(|shape| &shape.carrier) { + Some(RecordCarrier::Token(token)) => Some(token.clone()), + _ => None, + }; + let skips_carrier = record.as_ref().is_some_and(|shape| shape.skips_carrier()); let routing = routing_io(parsed); let snapshot_ctx = async_fn && matches!(&parsed.input.ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "CtxSnapshot")); @@ -681,8 +724,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let mut impl_generics: Vec = parsed .fn_generics .iter() - .filter(|param| match (param, &routing) { - (GenericParam::Type(type_param), Some(routing)) => type_param.ident != routing.generic, + .filter(|param| match param { + GenericParam::Type(type_param) => { + Some(&type_param.ident) != routing.as_ref().map(|routing| &routing.generic) && Some(&type_param.ident) != record_token.as_ref() + } _ => true, }) .map(&generic_tokens) @@ -695,7 +740,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn generics.insert(0, quote!(#lifetime)); impl_generics.insert(0, quote!(#lifetime)); } - if routing.is_some() { + if routing.is_some() || record.is_some() { impl_generics.insert(0, quote!('__record)); } @@ -704,11 +749,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let struct_name = format_ident!("{}Node", parsed.struct_name); let output_type = &parsed.output_type; let trait_output = match (&record, &routing) { - (Some(shape), _) => { - let element_out = &shape.element_out; - syn::parse_quote!(#core_types::list::List<#element_out>) - } - (None, Some(_)) => syn::parse_quote!(#core_types::record::RecordValue<'__record>), + (Some(_), _) | (None, Some(_)) => syn::parse_quote!(#core_types::record::RecordValue<'__record>), (None, None) => slot_value_type(&parsed.output_type), }; let raw_lazy = matches!(kernel_kind(&parsed.output_type), KernelKind::Poll(_)); @@ -716,6 +757,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let where_predicates: Vec = parsed.where_clause.iter().flat_map(|clause| clause.predicates.iter()).map(|predicate| quote!(#predicate)).collect(); let (data_fields, regular_fields): (Vec<_>, Vec<_>) = parsed.fields.iter().partition(|field| field.is_data_field); + let regular_fields: Vec<_> = regular_fields.into_iter().skip(skips_carrier as usize).collect(); let data_field_generic_idents: Vec = parsed .fn_generics @@ -781,8 +823,8 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let lifetime = lend_lifetime.as_ref().expect("lend fields imply the lend lifetime"); quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = &#lifetime #ty>) } - ParsedFieldType::Regular(RegularParsedField { ty, .. }) if record.is_some() && index == 0 => { - quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #core_types::list::List<#ty>>) + ParsedFieldType::Regular(_) if record.is_some() && !skips_carrier && index == 0 => { + quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>) } ParsedFieldType::Regular(RegularParsedField { ty, .. }) if routing_source(ty) => { quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>) @@ -848,12 +890,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let eval_values = regular_fields.iter().enumerate().map(|(index, field)| { let name = &field.pat_ident.ident; match &field.ty { - ParsedFieldType::Regular(_) if record.is_some() && index == 0 => quote! { - let mut __record_list = match __cell.eval_input(#index, &self.#name, __input) { - Ok(value) => value, - Err(interrupt) => return interrupt.into(), - }; - }, + ParsedFieldType::Regular(_) if record.is_some() && !skips_carrier && index == 0 => quote!(), ParsedFieldType::Regular(_) => quote! { let #name = match __cell.eval_input(#index, &self.#name, __input) { Ok(value) => value, @@ -951,10 +988,16 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let body = &parsed.body; let vis = &parsed.vis; let kernel_fields: Vec<&&ParsedField> = regular_fields.iter().filter(|field| !injected_name(&field.pat_ident.ident)).collect(); + // A bare `Attr` in the return type cannot elide its lifetime, so the + // kernel gets a fresh one; reference-valued writes name their real + // lifetime explicitly and pass through untouched. + let kernel_output = record.as_ref().and_then(|_| inject_attr_lifetimes(&parsed.output_type)); + let attr_lifetime = kernel_output.is_some().then(|| quote!('__attr,)); + let kernel_output = kernel_output.map(|ty| quote!(#ty)).unwrap_or_else(|| quote!(#output_type)); let kernel = match async_fn { false => quote! { #[allow(clippy::too_many_arguments)] - #vis fn #fn_name<#(#generics,)*>(#ctx_pat: &#ctx_ident #(, #data_params)* #(, #kernel_params)*) -> #output_type #fn_where #body + #vis fn #fn_name<#attr_lifetime #(#generics,)*>(#ctx_pat: &#ctx_ident #(, #data_params)* #(, #kernel_params)*) -> #kernel_output #fn_where #body }, true => { let kernel_generics = parsed.fn_generics.iter().filter(|param| match param { @@ -1048,61 +1091,43 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } }; let record_tail = record.as_ref().map(|shape| { - let record_call_args = regular_fields.iter().enumerate().map(|(index, field)| { + let carrier_arg = match &shape.carrier { + RecordCarrier::None => None, + RecordCarrier::Token(_) => Some(quote!(#core_types::record::ElToken)), + RecordCarrier::Read(ty) => Some(quote!(unsafe { __src_rec.element::<#ty>() })), + } + .into_iter(); + let value_args = regular_fields.iter().skip(if shape.skips_carrier() { 0 } else { 1 }).map(|field| { let name = &field.pat_ident.ident; - match (index, &field.ty) { - (0, _) => quote!(__element), - (_, ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. })) => quote!(#name), - _ => quote!(#name.clone()), - } + quote!(#name) }); - let attr_call_args = parsed.attribute_reads.iter().map(|read| { + let attr_args = parsed.attribute_reads.iter().map(|read| { let pat = &read.pat_ident.ident; quote!(#pat) }); - let record_kernel_call = quote!(self::#fn_name(__input #(, &self.#data_names)* #(, #record_call_args)* #(, #attr_call_args)*)); - let read_columns = parsed.attribute_reads.iter().enumerate().map(|(index, read)| { - let marker = &read.marker; - let column = format_ident!("__read_{index}"); + let record_kernel_call = quote!(self::#fn_name(__input #(, &self.#data_names)* #(, #carrier_arg)* #(, #value_args)* #(, #attr_args)*)); + let carrier_eval = (!shape.skips_carrier()).then(|| { + let name = ®ular_fields[0].pat_ident.ident; quote! { - let #column = __record_list - .iter_attribute_values::<<#marker as #core_types::attribute::Attribute>::Value>(<#marker as #core_types::attribute::Attribute>::NAME) - .map(|__values| __values.cloned().collect::<::std::vec::Vec<_>>()); + let __src = match __cell.eval_input(0, &self.#name, __input) { + Ok(value) => value, + Err(interrupt) => return interrupt.into(), + }; + let __src_rec = #core_types::record::RecordValue::rec(__src); } }); + let carry = (!shape.skips_carrier()).then(|| quote!(unsafe { #core_types::record::apply_plan(__src_rec, __dst, &self.__plan) };)); let read_bindings = parsed.attribute_reads.iter().enumerate().map(|(index, read)| { let pat = &read.pat_ident; let marker = &read.marker; - let column = format_ident!("__read_{index}"); + let slot = format_ident!("__read_{index}"); quote! { - let #pat = #core_types::attribute::Attr::<#marker>(match &#column { - Some(__values) => __values[__index].clone(), + let #pat = #core_types::attribute::Attr::<#marker>(match self.#slot { + Some(__offset) => unsafe { __src_rec.read(__offset) }, None => <#marker as #core_types::attribute::Attribute>::default(), }); } }); - let write_markers = &shape.write_markers; - let write_columns: Vec = (0..write_markers.len()).map(|index| format_ident!("__write_{index}")).collect(); - let write_column_decls = write_markers.iter().zip(&write_columns).map(|(marker, column)| { - quote! { - let mut #column: ::std::vec::Vec<<#marker as #core_types::attribute::Attribute>::Value> = ::std::vec::Vec::with_capacity(__len); - } - }); - let write_pats: Vec = (0..write_markers.len()).map(|index| format_ident!("__written_{index}")).collect(); - let destructure = match write_markers.is_empty() { - true => quote!(let __element_out = __kernel_value;), - false => quote!(let (__element_out #(, #core_types::attribute::Attr(#write_pats))*) = __kernel_value;), - }; - let write_pushes = write_columns.iter().zip(&write_pats).map(|(column, pat)| quote!(#column.push(#pat);)); - let written_names = write_markers.iter().map(|marker| quote!(<#marker as #core_types::attribute::Attribute>::NAME)); - let write_inserts = write_markers.iter().zip(&write_columns).map(|(marker, column)| { - quote! { - __out.insert_attribute_dyn( - <#marker as #core_types::attribute::Attribute>::NAME, - #core_types::list::AttributeDyn(::std::boxed::Box::new(#core_types::list::Attribute(#column))), - ); - } - }); let kernel_value = match shape.dialect { RecordDialect::Plain => quote!(#record_kernel_call), RecordDialect::Interrupt => quote! { @@ -1112,37 +1137,31 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } }, }; - let element_out = &shape.element_out; + let attr_binders: Vec = (0..shape.write_markers.len()).map(|index| format_ident!("__attr_{index}")).collect(); + let element_binder = match &shape.element_write { + Some(_) => quote!(__element), + None => quote!(_), + }; + let destructure = match attr_binders.is_empty() { + true => quote!(let #element_binder = __kernel_value;), + false => quote!(let (#element_binder #(, #core_types::attribute::Attr(#attr_binders))*) = __kernel_value;), + }; + let element_store = shape.element_write.as_ref().map(|ty| quote!(unsafe { #core_types::record::write_field::<#ty>(__dst, 0, __element) };)); + let attr_stores = attr_binders.iter().enumerate().map(|(index, binder)| { + let slot = format_ident!("__write_{index}"); + quote!(unsafe { #core_types::record::write_field(__dst, self.#slot, #binder) };) + }); quote! { - let __len = __record_list.len(); - #(#read_columns)* - let __written_names: &[&str] = &[#(#written_names),*]; - let __carried_keys: ::std::vec::Vec<::std::string::String> = __record_list - .attribute_keys() - .filter(|__key| !__written_names.contains(__key)) - .map(::std::string::String::from) - .collect(); - let mut __carried: ::std::vec::Vec<(::std::string::String, #core_types::list::AttributeDyn)> = ::std::vec::Vec::with_capacity(__carried_keys.len()); - for __key in __carried_keys { - if let Some(__column) = __record_list.take_attribute_dyn(&__key) { - __carried.push((__key, __column)); - } - } - #(#write_column_decls)* - let mut __out_elements: ::std::vec::Vec<#element_out> = ::std::vec::Vec::with_capacity(__len); - for (__index, __element) in __record_list.into_element_values().into_iter().enumerate() { - #(#read_bindings)* - let __kernel_value = #kernel_value; - #destructure - __out_elements.push(__element_out); - #(#write_pushes)* - } - let mut __out = #core_types::list::List::from_element_values(__out_elements); - for (__key, __column) in __carried { - __out.insert_attribute_dyn(__key, __column); - } - #(#write_inserts)* - __cell.finish(__out) + let __dst = #core_types::record::stack::push(self.__frame_bytes); + #carrier_eval + #carry + #(#read_bindings)* + let __kernel_value = #kernel_value; + #destructure + #element_store + #(#attr_stores)* + #core_types::record::stack::pop(__dst); + __cell.finish(#core_types::record::RecordValue::from_rec(unsafe { #core_types::record::Rec::new(__dst.cast_const()) })) } }); let eval_tail = match (async_fn, future_kernel) { @@ -1199,21 +1218,114 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }; let record_bounds: Vec = match &record { - Some(_) => regular_fields - .iter() - .skip(1) - .filter_map(|field| match &field.ty { - ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. }) => None, - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => Some(quote!(#ty: Clone)), - _ => None, - }) - .collect(), - None => Vec::new(), + Some(shape) if shape.skips_carrier() => { + vec![quote!(#ctx_ident: #core_types::context::ExtractArena)] + } + _ => Vec::new(), + }; + + let record_layout_impl = match &record { + Some(_) => quote! { + fn layout(&self) -> Option<&#core_types::record::Layout> { + Some(&self.__layout) + } + }, + None => quote!(), }; let entries = entries_tokens(parsed, &struct_name, &data_field_generic_idents, ®ular_fields); let cfg = crate::shader_nodes::modify_cfg(&parsed.attributes); + let record_wiring = record.as_ref().map(|shape| { + let layout_fn = format_ident!("{}_layout", fn_name); + let write_descs: Vec = shape + .write_markers + .iter() + .map(|marker| { + quote! { + ( + <#marker as #core_types::attribute::Attribute>::NAME, + 0u8, + ::core::mem::size_of::<<#marker as #core_types::attribute::Attribute>::Value<'static>>(), + ::core::mem::align_of::<<#marker as #core_types::attribute::Attribute>::Value<'static>>(), + ) + } + }) + .collect(); + let element_dims = match &shape.element_write { + Some(ty) => quote!((::core::mem::size_of::<#ty>(), ::core::mem::align_of::<#ty>())), + None => quote!((__carrier.element_size, __carrier.element_align)), + }; + let layout_def = match shape.skips_carrier() { + true => quote! { + #vis fn #layout_fn() -> #core_types::record::Layout { + #core_types::record::Layout::default().with_writes(0, #element_dims, &[#(#write_descs),*]) + } + }, + false => quote! { + #vis fn #layout_fn(__carrier: &#core_types::record::Layout) -> #core_types::record::Layout { + __carrier.with_writes(__carrier.depth, #element_dims, &[#(#write_descs),*]) + } + }, + }; + let edge_args = regular_fields.iter().zip(&node_generics).map(|(field, generic)| { + let name = &field.pat_ident.ident; + quote!(#name: #generic) + }); + let carrier_layout_param = (!shape.skips_carrier()).then(|| quote!(__carrier_layout: &#core_types::record::Layout,)).into_iter(); + let layout_binding = match shape.skips_carrier() { + true => quote!(let __layout = self::#layout_fn();), + false => quote!(let __layout = self::#layout_fn(__carrier_layout);), + }; + let carry_element = shape.carries_element(); + let plan_binding = (!shape.skips_carrier()).then(|| quote!(let __plan = #core_types::record::copy_plan(__carrier_layout, &__layout, #carry_element);)); + let read_inits = parsed.attribute_reads.iter().enumerate().map(|(index, read)| { + let marker = &read.marker; + let slot = format_ident!("__read_{index}"); + quote!(let #slot = __carrier_layout.offset_of(<#marker as #core_types::attribute::Attribute>::NAME, 0);) + }); + let write_inits = shape.write_markers.iter().enumerate().map(|(index, marker)| { + let slot = format_ident!("__write_{index}"); + quote! { + let #slot = __layout + .offset_of(<#marker as #core_types::attribute::Attribute>::NAME, 0) + .expect("a written attribute is always part of the wired layout"); + } + }); + let data_inits = data_names.iter().map(|name| quote!(#name: ::core::default::Default::default(),)); + let edge_inits = regular_fields.iter().map(|field| { + let name = &field.pat_ident.ident; + quote!(#name,) + }); + let plan_init = (!shape.skips_carrier()).then(|| quote!(__plan,)).into_iter(); + let read_names = (0..parsed.attribute_reads.len()).map(|index| format_ident!("__read_{index}")).map(|slot| quote!(#slot,)); + let write_names = (0..shape.write_markers.len()).map(|index| format_ident!("__write_{index}")).map(|slot| quote!(#slot,)); + quote! { + #layout_def + + #[automatically_derived] + impl<#(#data_field_generic_idents,)* #(#node_generics,)*> #mod_name::#struct_name<#(#struct_type_params,)*> { + #[allow(clippy::too_many_arguments)] + #vis fn wire(#(#edge_args,)* #(#carrier_layout_param)*) -> Self { + #layout_binding + #plan_binding + #(#read_inits)* + #(#write_inits)* + let __frame_bytes = __layout.size.next_multiple_of(8); + Self { + #(#data_inits)* + #(#edge_inits)* + __layout, + #(#plan_init)* + __frame_bytes, + #(#read_names)* + #(#write_names)* + } + } + } + } + }); + let top_level = quote! { #cfg #[automatically_derived] @@ -1239,6 +1351,8 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn #serialize_impl + #record_layout_impl + #batch_impl } }; @@ -1248,6 +1362,8 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn top_level: quote! { #kernel + #record_wiring + #top_level }, }) @@ -1258,20 +1374,90 @@ pub(crate) enum RecordDialect { Interrupt, } -/// The record io of a node fn: the output element type and the written -/// markers. Present exactly when the signature declares attribute reads or -/// writes in a shape the driver supports (the carrier is the first field); -/// malformed record io is reported by validation and generates no node impl. +/// How a record node's primary input lowers. +pub(crate) enum RecordCarrier { + /// `_: ()`: no carrier edge, the kernel writes a fresh record. + None, + /// An unbounded generic returned in the element position: the element + /// bytes carry through the copy plan and the kernel sees `ElToken`. + Token(Ident), + /// An element type read at offset 0, monomorphized per its + /// implementations list where generic. + Read(Type), +} + +/// The record io of a node fn: how the carrier lowers, the element write, +/// and the written markers. Present exactly when the signature declares +/// attribute reads or writes in a shape the record tier supports; malformed +/// record io is reported by validation and generates no node impl. pub(crate) struct RecordShape { - pub(crate) element_out: Type, + pub(crate) carrier: RecordCarrier, + pub(crate) element_write: Option, pub(crate) write_markers: Vec, pub(crate) dialect: RecordDialect, } +impl RecordShape { + pub(crate) fn skips_carrier(&self) -> bool { + matches!(self.carrier, RecordCarrier::None) + } + + pub(crate) fn carries_element(&self) -> bool { + self.element_write.is_none() + } +} + pub(crate) fn has_record_io(parsed: &ParsedNodeFn) -> bool { !parsed.attribute_reads.is_empty() || record_writes(&slot_value_type(&parsed.output_type)).is_some() } +fn inject_attr_lifetimes(output: &Type) -> Option { + struct Injector { + changed: bool, + } + + impl VisitMut for Injector { + fn visit_path_segment_mut(&mut self, segment: &mut syn::PathSegment) { + if segment.ident == "Attr" + && let PathArguments::AngleBracketed(args) = &mut segment.arguments + && !args.args.iter().any(|arg| matches!(arg, GenericArgument::Lifetime(_))) + { + args.args.insert(0, GenericArgument::Lifetime(Lifetime::new("'__attr", proc_macro2::Span::call_site()))); + self.changed = true; + } + syn::visit_mut::visit_path_segment_mut(self, segment); + } + } + + let mut ty = output.clone(); + let mut injector = Injector { changed: false }; + injector.visit_type_mut(&mut ty); + injector.changed.then_some(ty) +} + +pub(crate) fn contains_open_generic(parsed: &ParsedNodeFn, ty: &Type) -> bool { + let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone()); + parsed + .fn_generics + .iter() + .any(|param| matches!(param, GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() && type_contains_ident(ty, &type_param.ident))) +} + +pub(crate) fn unbounded_generic(parsed: &ParsedNodeFn, ty: &Type) -> Option { + let ident = bare_ident(ty)?.clone(); + let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone()); + parsed + .fn_generics + .iter() + .find(|param| matches!(param, GenericParam::Type(type_param) if type_param.ident == ident && type_param.bounds.is_empty() && Some(&type_param.ident) != ctx_ident.as_ref()))?; + if let Some(where_clause) = &parsed.where_clause + && tokens_contain_ident(where_clause.to_token_stream(), &ident) + { + return None; + } + Some(ident) +} + pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option { let (value, dialect) = match kernel_kind(&parsed.output_type) { KernelKind::Plain => (parsed.output_type.clone(), RecordDialect::Plain), @@ -1282,28 +1468,50 @@ pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option { if parsed.attribute_reads.is_empty() && writes.is_none() { return None; } - if parsed.is_async { + if parsed.is_async || parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) { return None; } - let carrier = parsed.fields.first()?; - if carrier.is_data_field { + let carrier_field = parsed.fields.first()?; + if carrier_field.is_data_field { return None; } - let ParsedFieldType::Regular(RegularParsedField { ty, lend: None, .. }) = &carrier.ty else { + let ParsedFieldType::Regular(RegularParsedField { ty, lend: None, implementations, .. }) = &carrier_field.ty else { return None; }; - if matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty()) { - return None; - } - if parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) { - return None; - } - let (element_out, write_markers) = match writes { + let carrier = match ty { + Type::Tuple(tuple) if tuple.elems.is_empty() => RecordCarrier::None, + ty => match implementations.is_empty().then(|| unbounded_generic(parsed, ty)).flatten() { + Some(token) => RecordCarrier::Token(token), + None => { + if contains_open_generic(parsed, ty) { + return None; + } + RecordCarrier::Read(ty.clone()) + } + }, + }; + let (element, write_markers) = match writes { Some(RecordWrites { element, markers }) => (element, markers), None => (value, Vec::new()), }; + let element_write = match &carrier { + RecordCarrier::Token(token) => match bare_ident(&element) { + Some(ident) if ident == token => None, + _ => return None, + }, + _ => { + if contains_open_generic(parsed, &element) { + return None; + } + Some(element) + } + }; + if matches!(carrier, RecordCarrier::None) && !parsed.attribute_reads.is_empty() { + return None; + } Some(RecordShape { - element_out, + carrier, + element_write, write_markers, dialect, }) @@ -1374,7 +1582,7 @@ pub(crate) fn routing_io(parsed: &ParsedNodeFn) -> Option { (sources > 0).then(|| RoutingIo { generic: ident }) } -fn bare_ident(ty: &Type) -> Option<&Ident> { +pub(crate) fn bare_ident(ty: &Type) -> Option<&Ident> { let Type::Path(path) = ty else { return None }; path.path.get_ident() } @@ -1522,6 +1730,9 @@ fn entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, data_field_generic if !data_field_generic_idents.is_empty() { return quote!(); } + if has_record_io(parsed) { + return quote!(); + } let Some(rows) = implementation_rows(parsed, regular_fields) else { return quote!(); }; @@ -1554,17 +1765,14 @@ fn entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, data_field_generic .map(|field| matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. }))) .collect(); - let record_carrier = record_shape(parsed).is_some(); let entries = rows.iter().map(|row| { - let input_types = row.iter().enumerate().zip(&lend_flags).map(|((index, ty), lend)| match (lend, record_carrier && index == 0) { - (true, _) => quote!(gcore::registry::lend_edge_type::<#ty>()), - (false, true) => quote!(gcore::registry::edge_type::>()), - (false, false) => quote!(gcore::registry::edge_type::<#ty>()), + let input_types = row.iter().zip(&lend_flags).map(|(ty, lend)| match lend { + true => quote!(gcore::registry::lend_edge_type::<#ty>()), + false => quote!(gcore::registry::edge_type::<#ty>()), }); - let edge_types = row.iter().enumerate().zip(&lend_flags).map(|((index, ty), lend)| match (lend, record_carrier && index == 0) { - (true, _) => quote!(gcore::registry::SharedEdge>), - (false, true) => quote!(gcore::registry::SharedEdge>>), - (false, false) => quote!(gcore::registry::SharedEdge>), + let edge_types = row.iter().zip(&lend_flags).map(|(ty, lend)| match lend { + true => quote!(gcore::registry::SharedEdge>), + false => quote!(gcore::registry::SharedEdge>), }); let output = quote!(<#struct_name<#(#edge_types),*> as gcore::node::Node>>::Output); let (io_output, construct) = match &ref_output_inner { @@ -1577,10 +1785,9 @@ fn entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, data_field_generic quote!(Ok(gcore::registry::EdgeHandle::new(::std::sync::Arc::new(#struct_name::new(#(#names),*)) as ::std::sync::Arc>))), ), }; - let downcasts = names.iter().zip(row.iter().enumerate()).zip(&lend_flags).map(|((name, (index, ty)), lend)| match (lend, record_carrier && index == 0) { - (true, _) => quote!(let #name = inputs.next().unwrap().downcast_lend::<#ty>()?;), - (false, true) => quote!(let #name = inputs.next().unwrap().downcast::>()?;), - (false, false) => quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;), + let downcasts = names.iter().zip(row.iter()).zip(&lend_flags).map(|((name, ty), lend)| match lend { + true => quote!(let #name = inputs.next().unwrap().downcast_lend::<#ty>()?;), + false => quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;), }); quote! { gcore::registry::RegistryEntry { diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index 6d2e15d2fe..5eaca857de 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -716,6 +716,16 @@ fn parse_inputs(inputs: &Punctuated) -> syn::Result<(Input, Vec` kernel, not a `GPoll` one"); } - match parsed.fields.first() { - None => emit_error!( - parsed.fn_name.span(), - "attribute io needs a value carrier as the first parameter after the context" - ), - Some(carrier) => { - let valid = !carrier.is_data_field - && matches!(&carrier.ty, ParsedFieldType::Regular(RegularParsedField { ty, lend: None, .. }) if !matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty())); - if !valid { - emit_error!( - carrier.pat_ident.span(), - "attribute io needs a value carrier as the first parameter after the context: an owned element type, not `()`, `#[data]`, `&T`, or `impl Node`" - ); - } - } - } for field in parsed.fields.iter().skip(1) { if matches!(field.ty, ParsedFieldType::Node(_)) { emit_error!(field.pat_ident.span(), "record nodes take no lazy inputs yet"); } } + let Some(carrier) = parsed.fields.first() else { + emit_error!( + parsed.fn_name.span(), + "attribute io needs a primary input as the first parameter after the context (`_: ()` for none)" + ); + return; + }; + let carrier_ty = match &carrier.ty { + ParsedFieldType::Regular(RegularParsedField { ty, lend: None, .. }) if !carrier.is_data_field => Some(ty), + _ => None, + }; + let Some(carrier_ty) = carrier_ty else { + emit_error!( + carrier.pat_ident.span(), + "a record node's primary input is an owned element, an unbounded passthrough generic, or `_: ()`; not `#[data]`, `&T`, or `impl Node`" + ); + return; + }; + + let no_carrier = matches!(carrier_ty, Type::Tuple(tuple) if tuple.elems.is_empty()); + if no_carrier && !parsed.attribute_reads.is_empty() { + emit_error!(carrier.pat_ident.span(), "a node without a primary input has no attributes to read"); + } + let token = match (no_carrier, &carrier.ty) { + (false, ParsedFieldType::Regular(RegularParsedField { ty, implementations, .. })) if implementations.is_empty() => crate::codegen::unbounded_generic(parsed, ty), + _ => None, + }; + let element = writes.as_ref().map(|writes| &writes.element).unwrap_or(&value); + match &token { + Some(token) => { + if !matches!(crate::codegen::bare_ident(element), Some(ident) if ident == token) { + emit_error!( + parsed.output_type.span(), + "a generic element passes through unchanged: return `{}` in the first tuple position", + token + ); + } + } + None => { + if let Some(ident) = crate::codegen::unbounded_generic(parsed, element) { + emit_error!(parsed.output_type.span(), "the returned generic element `{}` has no matching input", ident); + } else if !no_carrier && crate::codegen::contains_open_generic(parsed, carrier_ty) { + emit_error!( + carrier.pat_ident.span(), + "record element reads are monomorphic for now; use a concrete element type or an unbounded passthrough generic" + ); + } else if crate::codegen::contains_open_generic(parsed, element) { + emit_error!(parsed.output_type.span(), "a written element must be a concrete type"); + } + } + } + let mut seen_reads: Vec = Vec::new(); for read in &parsed.attribute_reads { let marker = read.marker.to_token_stream().to_string(); @@ -276,7 +312,17 @@ fn validate_primary_input_expose(parsed: &ParsedNodeFn) { fn validate_implementations_for_generics(parsed: &ParsedNodeFn) { let has_skip_impl = parsed.attributes.skip_impl; let routing = crate::codegen::routing_io(parsed); - let routing_source = |ty: &Type| matches!((&routing, ty), (Some(routing), Type::Path(path)) if path.path.get_ident() == Some(&routing.generic)); + let record_token = crate::codegen::record_shape(parsed).and_then(|shape| match shape.carrier { + crate::codegen::RecordCarrier::Token(token) => Some(token), + _ => None, + }); + let opaque_record_generic = |ty: &Type| { + let ident = match ty { + Type::Path(path) => path.path.get_ident(), + _ => None, + }; + ident.is_some() && (ident == routing.as_ref().map(|routing| &routing.generic) || ident == record_token.as_ref()) + }; if !has_skip_impl && !parsed.fn_generics.is_empty() { for field in &parsed.fields { @@ -288,7 +334,7 @@ fn validate_implementations_for_generics(parsed: &ParsedNodeFn) { let pat_ident = &field.pat_ident; match &field.ty { ParsedFieldType::Regular(RegularParsedField { ty, implementations, .. }) => { - if routing_source(ty) { + if opaque_record_generic(ty) { continue; } if contains_generic_param(ty, &parsed.fn_generics) && implementations.is_empty() { @@ -308,7 +354,7 @@ fn validate_implementations_for_generics(parsed: &ParsedNodeFn) { implementations, .. }) => { - if routing_source(output_type) { + if opaque_record_generic(output_type) { continue; } if (contains_generic_param(input_type, &parsed.fn_generics) || contains_generic_param(output_type, &parsed.fn_generics)) && implementations.is_empty() { diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index 7d9dbcb4c0..b7fd544f1b 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -1,15 +1,19 @@ -//! Pilot record nodes exercising the macro's attribute io over materialized -//! lists: `Attr` reads, tuple writes, column carry, and the census -//! defaults. These are the flat-wave law tests; the node forms are the -//! production authoring surface, the list driver behind them is interim. +//! Pilot record nodes exercising the macro's record-tier attribute io: +//! offset reads and writes against record edges, the ElToken byte-carry for +//! passthrough elements, and the `_: ()` no-carrier form. These are the +//! flat-wave law tests; the node forms are the production authoring surface, +//! and the wiring is by hand until the compiler pass constructs layouts. use core_types::attribute::{Attr, Opacity}; -use core_types::gpoll::{GraphError, Interrupt}; +use core_types::context::ExtractArena; +use core_types::gpoll::{ErrorKind, GraphError, Interrupt}; use core_types::{Context, Ctx}; core_types::attribute! { /// Test-only measured length of an element. pub Length("length"): f64; + /// Test-only label parked in the arena by its writer. + pub Label("label"): &str; } #[node_macro::node(category("Test"))] @@ -40,6 +44,26 @@ fn scale(_: impl Ctx, element: f64, factor: &f64, opacity: Attr) -> (f6 (element * *factor, Attr(*opacity)) } +#[node_macro::node(category("Test"))] +fn fade(_: impl Ctx, element: T, factor: f64, opacity: Attr) -> (T, Attr) { + (element, Attr(*opacity * factor)) +} + +#[node_macro::node(category("Test"))] +fn source_opacity(_: impl Ctx, _: (), element: f64, opacity: f64) -> (f64, Attr) { + (element, Attr(opacity)) +} + +#[node_macro::node(category("Test"))] +fn label<'e>(ctx: impl Ctx + ExtractArena<'e>, element: f64, text: String, label: Attr