diff --git a/libraries/dyn-any/derive/src/lib.rs b/libraries/dyn-any/derive/src/lib.rs index 23fa506fb4..59c76c73cf 100644 --- a/libraries/dyn-any/derive/src/lib.rs +++ b/libraries/dyn-any/derive/src/lib.rs @@ -5,7 +5,7 @@ extern crate proc_macro; use proc_macro::TokenStream; use proc_macro2::Span; use quote::quote; -use syn::{DeriveInput, GenericParam, Lifetime, LifetimeParam, TypeParamBound, parse_macro_input}; +use syn::{DeriveInput, GenericParam, Lifetime, TypeParamBound, parse_macro_input}; /// Derives an implementation for the [`DynAny`] trait. /// @@ -40,37 +40,45 @@ pub fn system_desc_derive(input: TokenStream) -> TokenStream { let struct_name = &ast.ident; let generics = &ast.generics; - let static_params = replace_lifetimes(generics, "'static"); - let dyn_params = replace_lifetimes(generics, "'dyn_any"); + let static_params = generic_arguments(generics, "'static"); + let dyn_params = generic_arguments(generics, "'dyn_any"); - let old_params = &generics.params.iter().collect::>(); + let impl_params = generics.params.iter().map(|param| match param { + GenericParam::Type(t) => { + let mut t = t.clone(); + t.bounds.push(TypeParamBound::Lifetime(Lifetime::new("'static", Span::call_site()))); + quote! {#t} + } + param => quote! {#param}, + }); quote! { - unsafe impl<'dyn_any, #(#old_params,)*> dyn_any::StaticType for #struct_name <#(#dyn_params,)*> { + unsafe impl<'dyn_any, #(#impl_params,)*> dyn_any::StaticType for #struct_name <#(#dyn_params,)*> { type Static = #struct_name <#(#static_params,)*>; } } .into() } -fn replace_lifetimes(generics: &syn::Generics, replacement: &str) -> Vec { +/// The struct's generic parameters as argument tokens: bare idents for type +/// and const parameters (bounds are illegal in argument position), the +/// replacement for lifetimes. +fn generic_arguments(generics: &syn::Generics, replacement: &str) -> Vec { generics .params .iter() - .map(|param| { - let param = match param { - GenericParam::Lifetime(_) => GenericParam::Lifetime(LifetimeParam::new(Lifetime::new(replacement, Span::call_site()))), - GenericParam::Type(t) => { - let mut t = t.clone(); - t.bounds.iter_mut().for_each(|bond| { - if let TypeParamBound::Lifetime(t) = bond { - *t = Lifetime::new(replacement, Span::call_site()) - } - }); - GenericParam::Type(t.clone()) - } - c => c.clone(), - }; - quote! {#param} + .map(|param| match param { + GenericParam::Lifetime(_) => { + let lifetime = Lifetime::new(replacement, Span::call_site()); + quote! {#lifetime} + } + GenericParam::Type(t) => { + let ident = &t.ident; + quote! {#ident} + } + GenericParam::Const(c) => { + let ident = &c.ident; + quote! {#ident} + } }) .collect::>() } diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index 4163ea5147..58eabe5199 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -178,14 +178,16 @@ impl DynamicExecutor { .and_then(EdgeHandle::record_edge) .ok_or_else(|| IntrospectError::PathNotFound(node_path.to_vec()))?; let arena = self.arena.lock().unwrap_or_else(PoisonError::into_inner); - core_types::record::stack::reserve(self.tree.stack_need()); - let generations = self.runtime.snapshot(); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { core_types::record::stack::reserve(self.tree.stack_need()); } let generations = self.runtime.snapshot(); let scope = EvalScope::new(snapshot.try_real_time(), snapshot.try_animation_time(), snapshot.try_pointer_position(), &generations, &arena); let Some(ctx) = snapshot.rehydrate(&scope) else { return Err(IntrospectError::NoData); }; let layout = core_types::node::Node::::layout(&edge); - let mark = core_types::record::stack::sp(); + // SAFETY: the read closure finishes inside the scope, so no record + // above the entry survives it. + let _scope = unsafe { core_types::record::stack::ScopeGuard::enter() }; let result = if layout.depth > 0 { match core_types::record::materialize_level(&edge, &ctx, &arena) { core_types::record::LevelStatus::Batch(batch, _) => read(layout, batch, &arena), @@ -209,8 +211,6 @@ impl DynamicExecutor { _ => None, } }; - // SAFETY: the read finished, so no record above the mark is live. - unsafe { core_types::record::stack::rewind(mark) }; result.ok_or(IntrospectError::NoData) } @@ -246,8 +246,8 @@ where return Err("Output node not found in executor".into()); }; let mut arena = self.arena.lock().unwrap_or_else(PoisonError::into_inner); - core_types::record::stack::reserve(self.tree.stack_need()); - let result = eval_root(&mut arena, &self.runtime, &input, |ctx| match TaggedValue::from_edge(handle.duplicate(), ctx) { + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { core_types::record::stack::reserve(self.tree.stack_need()); } let result = eval_root(&mut arena, &self.runtime, &input, |ctx| match TaggedValue::from_edge(handle.duplicate(), ctx) { Ok(poll) => poll.map(Ok), Err(error) => GPoll::Final(Err(error)), }); @@ -631,8 +631,8 @@ mod test { let generations = []; let scope = EvalScope::new(None, None, None, &generations, &arena); let ctx = ContextImpl::root(&scope); - core_types::record::stack::reserve(layout.frame_bytes()); - let GPoll::Final(value) = edge.eval(&ctx) else { + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { core_types::record::stack::reserve(layout.frame_bytes()); } let GPoll::Final(value) = edge.eval(&ctx) else { panic!("expected a final record"); }; assert_eq!(unsafe { core_types::record::read_element::(layout.rec(&value)) }, 2); @@ -676,8 +676,8 @@ mod test { let handle = executor.tree().get(NodeId(1)).unwrap(); let layout = handle.layout().clone(); let edge = handle.duplicate().downcast_record::().unwrap(); - core_types::record::stack::reserve(executor.tree().stack_need()); - let GPoll::Final(value) = edge.eval(&ctx) else { + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { core_types::record::stack::reserve(executor.tree().stack_need()); } let GPoll::Final(value) = edge.eval(&ctx) else { panic!("the flipped clone must evaluate over record wires, got a non-final poll"); }; assert_eq!(unsafe { core_types::record::read_element::(layout.rec(&value)) }, 7.); @@ -703,8 +703,8 @@ mod test { let scope = EvalScope::new(None, None, None, &generations, &arena); let ctx = ContextImpl::root(&scope); let edge = executor.tree().get(NodeId(2)).unwrap().downcast_record::().unwrap(); - core_types::record::stack::reserve(executor.tree().stack_need()); - let result = edge.eval(&ctx); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { core_types::record::stack::reserve(executor.tree().stack_need()); } let result = edge.eval(&ctx); // The empty raster level folds to an empty palette: past-end at lane 0. assert!( matches!(&result, GPoll::Error(error) if error.kind == core_types::gpoll::ErrorKind::PastEnd), diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index 209d8e10bb..16738faa47 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -535,7 +535,9 @@ where let mut hint = crate::gpoll::Extent::AtLeast(range.end as usize); for lane in 0..len { local.set_index(range.start + lane as u64); - let mark = stack::sp(); + // SAFETY: the lane's record is copied out before the scope releases it, + // and valueless exits serve nothing above the entry. + let _lane_scope = unsafe { stack::ScopeGuard::enter() }; let value = match node.eval(&local) { GPoll::Final(value) => value, GPoll::Partial(value) => { @@ -549,19 +551,13 @@ where GPoll::Error(error) if error.kind == crate::gpoll::ErrorKind::PastEnd => { filled = lane; hint = crate::gpoll::Extent::Exactly(range.start as usize + lane); - // SAFETY: the failed lane produced no record, so nothing above - // its mark is live. - unsafe { stack::rewind(mark) }; break; } GPoll::Error(error) => return BatchStatus::Error(*error), }; // SAFETY: the lane region is in-bounds by the scratch check, and the - // frame is fully copied out before the rewind releases it. - unsafe { - std::ptr::copy_nonoverlapping(layout.rec(&value).ptr(), base.add(lane * stride), stride); - stack::rewind(mark); - } + // frame is fully copied out before the lane scope releases it. + unsafe { std::ptr::copy_nonoverlapping(layout.rec(&value).ptr(), base.add(lane * stride), stride) }; } // SAFETY: the first `filled` lanes were filled above with records of `layout`. BatchStatus::Filled(unsafe { crate::node::RecordBatchMut::new(scratch, filled, layout) }, finality, hint) @@ -771,14 +767,10 @@ impl<'a, Out, N> ElementEdge<'a, Out, N> { where N: Node>, { - let mark = stack::sp(); - self.node.eval(ctx).map(|value| { - let out = unsafe { (self.read)(self.layout.rec(&value), self.reads) }; - // SAFETY: the read copied out by value, so no record above `mark` (the - // edge's own frame) is live. - unsafe { stack::rewind(mark) }; - out - }) + // SAFETY: the read copies out by value, so no record above the entry + // (the edge's own frame) is live past the scope. + let _scope = unsafe { stack::ScopeGuard::enter() }; + self.node.eval(ctx).map(|value| unsafe { (self.read)(self.layout.rec(&value), self.reads) }) } } @@ -826,13 +818,12 @@ impl<'a, Out, N> ElementLazyInput<'a, Out, N> { where N: Node>, { - let mark = stack::sp(); + // SAFETY: the read copies the element and declared attributes out by + // value, so no record above the entry (the edge's own frame) is live + // past the scope. + let _scope = unsafe { stack::ScopeGuard::enter() }; let value = self.cell.eval_input(self.input_index, self.node, ctx)?; - let out = unsafe { (self.read)(self.layout.rec(&value), self.reads) }; - // SAFETY: the read copied the element and declared attributes out by value, - // so no record above `mark` (the edge's own frame) is live. - unsafe { stack::rewind(mark) }; - Ok(out) + Ok(unsafe { (self.read)(self.layout.rec(&value), self.reads) }) } } @@ -919,13 +910,12 @@ where let cell = crate::node::StatusCell::new(); let mut count: u64 = 0; loop { - let mark = stack::sp(); + // SAFETY: the probed record is discarded, so nothing above the entry + // is live past the scope. + let _scope = unsafe { stack::ScopeGuard::enter() }; let mut frame = crate::context::IndexLink { index: 0, outer: None }; let probe = ctx.push_level(&mut frame, copy, count); let result = node.eval_derived(&cell, input_index, &probe); - // SAFETY: the probed record is discarded, so nothing above the mark - // is live. - unsafe { stack::rewind(mark) }; match result { Ok(_) => count += 1, Err(crate::gpoll::Interrupt::Error(error)) if error.kind == crate::gpoll::ErrorKind::PastEnd => return Ok(count), @@ -1036,7 +1026,11 @@ pub mod stack { /// 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) { + /// + /// # Safety + /// No record served on this thread's stack may be live: growth frees the + /// buffer and the reset releases every claimed frame. + pub unsafe fn reserve(bytes: usize) { STACK.with(|stack| { stack.sp.set(0); if stack.capacity.get() >= bytes { @@ -1106,6 +1100,45 @@ pub mod stack { stack.sp.set(mark); }); } + + /// Rewinds to the entry stack pointer on drop, unwind included: the + /// structured form of a mark/rewind pair. A forgotten guard leaks its + /// region until the next reserve rather than releasing it. + pub struct ScopeGuard { + mark: usize, + } + + impl ScopeGuard { + /// # Safety + /// No `Rec` or `RecordValue` served above the entry point may be used + /// after the guard drops; copy out everything that survives the scope. + pub unsafe fn enter() -> Self { + Self { mark: sp() } + } + } + + impl Drop for ScopeGuard { + fn drop(&mut self) { + STACK.with(|stack| { + // An interrupt close may already have rewound below the entry; + // the scope only ever releases, never re-claims. + if self.mark < stack.sp.get() { + stack.sp.set(self.mark); + } + }); + } + } + + /// Runs `body` under a [`ScopeGuard`]: every frame it claims releases on + /// return. + /// + /// # Safety + /// As [`ScopeGuard::enter`]: nothing served inside the scope may escape + /// it, through the return value or a captured location. + pub unsafe fn scoped(body: impl FnOnce() -> R) -> R { + let _scope = unsafe { ScopeGuard::enter() }; + body() + } } /// Reclaims the frames an inline node's inputs push. An inline node returns @@ -1114,7 +1147,7 @@ pub mod stack { /// pointer and rewinds to it on drop instead. Inactive (a no-op) for spilled /// nodes, which release their inputs through their own frame. pub struct ReclaimGuard { - target: usize, + scope: Option, } impl ReclaimGuard { @@ -1124,17 +1157,7 @@ impl ReclaimGuard { /// returns and the guard rewinds. pub unsafe fn new(active: bool) -> Self { Self { - target: if active { stack::sp() } else { usize::MAX }, - } - } -} - -impl Drop for ReclaimGuard { - fn drop(&mut self) { - if self.target != usize::MAX { - // SAFETY: an inline node returns its output by value, so no record into - // the reclaimed region is live once its eval returns. - unsafe { stack::rewind(self.target) }; + scope: active.then(|| unsafe { stack::ScopeGuard::enter() }), } } } @@ -1814,18 +1837,16 @@ impl ServedRecord { /// stays claimed. Assertion scaffolding for law tests; production consumers /// read served records in place. pub fn capture<'e, C, N: Node>>(node: &N, ctx: &C) -> GPoll { - let mark = stack::sp(); + // SAFETY: any served record is deep-copied out inside the scope, so + // nothing served above the entry escapes it. + let _scope = unsafe { stack::ScopeGuard::enter() }; let layout = node.layout().clone(); - let result = node.eval(ctx).map(|value| ServedRecord { + node.eval(ctx).map(|value| ServedRecord { // SAFETY: the poll served `value` at the node's declared layout and // nothing has claimed frames since. record: unsafe { OwnedRecord::copy_out(&layout, layout.rec(&value)) }, layout: layout.clone(), - }); - // SAFETY: any served record was deep-copied out above, so nothing above - // the mark is live. - unsafe { stack::rewind(mark) }; - result + }) } /// Law-test scaffolding: wraps an arbitrary plain node onto a record wire @@ -1896,12 +1917,11 @@ where type Output = El; fn eval(&self, input: &C) -> GPoll { - let mark = stack::sp(); - let result = self.edge.eval(input).map(|value| unsafe { read_element::(self.layout.rec(&value)) }); - // SAFETY: the element copied out by value, so no record above the mark - // (the edge's frame) is live; a plain output claims no frame itself. - unsafe { stack::rewind(mark) }; - result + // SAFETY: the element copies out by value, so no record above the + // entry (the edge's frame) is live past the scope; a plain output + // claims no frame itself. + let _scope = unsafe { stack::ScopeGuard::enter() }; + self.edge.eval(input).map(|value| unsafe { read_element::(self.layout.rec(&value)) }) } } @@ -1915,16 +1935,14 @@ where match &self.plan { None => self.edge.eval(input), Some(plan) if plan.union.frame_bytes() == 0 => { - let mark = stack::sp(); - let result = self.edge.eval(input).map(|value| { + // SAFETY: the translation copies the record into the inline + // value, so no record above the entry is live past the scope. + let _scope = unsafe { stack::ScopeGuard::enter() }; + self.edge.eval(input).map(|value| { let mut out = RecordValue::zeroed(); unsafe { plan.translate(plan.source.rec(&value), out.as_mut_ptr()) }; out - }); - // SAFETY: the translation copied the record into the inline - // value, so no record above the mark is live. - unsafe { stack::rewind(mark) }; - result + }) } Some(plan) => { let dst = stack::push(plan.union.frame_bytes()); @@ -2720,8 +2738,8 @@ mod tests { buffer.fill(u64::MAX); let replay_arena = crate::arena::Arena::new(1024).unwrap(); - stack::reserve(layout.frame_bytes()); - let value = copy.replay(&layout, &replay_arena).unwrap(); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { stack::reserve(layout.frame_bytes()); } let value = copy.replay(&layout, &replay_arena).unwrap(); let rec = layout.rec(&value); assert_eq!(unsafe { read_element::(rec) }, "element"); assert_eq!(unsafe { rec.read::<&str>(layout.offset_of("name", 0).unwrap()) }, "field"); @@ -2858,8 +2876,8 @@ mod tests { #[test] fn stack_frames_nest_and_release() { - stack::reserve(64); - let outer = stack::push(24); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { 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); @@ -2869,8 +2887,8 @@ mod tests { #[test] fn stack_rounds_frames_to_word_alignment() { - stack::reserve(64); - let first = stack::push(21); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { 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); @@ -2878,14 +2896,14 @@ mod tests { #[test] fn each_thread_gets_its_own_stack() { - stack::reserve(64); - let here = stack::push(8); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { 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); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { stack::reserve(64); } let there = stack::push(8); assert_ne!(here_address, there as usize, "stacks are per thread"); stack::pop(there); }) @@ -2923,8 +2941,8 @@ mod tests { } let layout = Layout::default().with_writes(0, element_write::(), &[FieldWrite::of::(0), FieldWrite::of::(0)]); - stack::reserve(1 << 10); - let arena = crate::arena::Arena::new(1024).unwrap(); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { stack::reserve(1 << 10); } let arena = crate::arena::Arena::new(1024).unwrap(); let mark = stack::sp(); let GPoll::Final(served) = capture(&Fixture { layout }, &&arena) else { panic!("the fixture serves finally"); @@ -2938,8 +2956,8 @@ mod tests { #[test] #[should_panic(expected = "the served element must match the layout's element type")] fn a_mistyped_element_is_rejected_at_the_write() { - stack::reserve(1 << 10); - let arena = crate::arena::Arena::new(256).unwrap(); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { stack::reserve(1 << 10); } let arena = crate::arena::Arena::new(256).unwrap(); let layout = Layout::default().with_writes(0, element_write::(), &[]); FrameBuilder::new(&layout, &arena).element(1u32); } diff --git a/node-graph/libraries/core-types/src/runtime.rs b/node-graph/libraries/core-types/src/runtime.rs index 1aad4f5838..39cf38642c 100644 --- a/node-graph/libraries/core-types/src/runtime.rs +++ b/node-graph/libraries/core-types/src/runtime.rs @@ -276,8 +276,8 @@ mod tests { where El::Static: Clone + Send + Sync, { - stack::reserve(1 << 12); - let layout = element_layout::(); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { stack::reserve(1 << 12); } let layout = element_layout::(); graph.set_layout(crate::record::RecordLayout { frame_bytes: layout.frame_bytes(), plan: Vec::new(), diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 64c02e5d46..0559bc9a0d 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -1647,8 +1647,8 @@ mod run_tests { drop(source); let arena = core_types::arena::Arena::new(1 << 16).unwrap(); - core_types::record::stack::reserve(layout.frame_bytes()); - let value = owned.replay(&layout, &arena).expect("the arena holds the replay"); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { core_types::record::stack::reserve(layout.frame_bytes()); } let value = owned.replay(&layout, &arena).expect("the arena holds the replay"); // SAFETY: the replay wrote a record of `layout`. let served = unsafe { layout.rec(&value).read::>>(offset) }.expect("the fill replays present"); assert_eq!(map_groups_to_legacy(served.element(0).unwrap()), expected); diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 01ebc0f523..88dafb564c 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -1546,13 +1546,11 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let slot = format_ident!("__in_{index}"); quote! { let #query = || { - let __mark = #core_types::record::stack::sp(); - let __result = #core_types::node::Node::eval(&self.#name, __input) - .map(|__value| unsafe { #core_types::record::read_element::<#ty>(self.#slot.rec(&__value)) }); - // SAFETY: the element copied out by value; extent + // SAFETY: the element copies out by value; extent // queries leave the record stack untouched. - unsafe { #core_types::record::stack::rewind(__mark) }; - __result + let __scope = unsafe { #core_types::record::stack::ScopeGuard::enter() }; + #core_types::node::Node::eval(&self.#name, __input) + .map(|__value| unsafe { #core_types::record::read_element::<#ty>(self.#slot.rec(&__value)) }) }; let #arg = #core_types::extent::ValueIn::new(&#query); } @@ -2117,11 +2115,13 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn match ir::value_binding(&node, index).reads_out() { false => body, true => { - let mark = format_ident!("__mark_{index}"); + let mark = format_ident!("__scope_{index}"); quote! { - let #mark = #core_types::record::stack::sp(); + // SAFETY: the bind copies its reads out by value; the + // drop point matches the old rewind. + let #mark = unsafe { #core_types::record::stack::ScopeGuard::enter() }; #body - unsafe { #core_types::record::stack::rewind(#mark) }; + drop(#mark); } } } @@ -2198,8 +2198,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn for __lane in 0..__len { #core_types::context::InjectIndex::set_index(&mut __lane_ctx, __range.start + __lane as u64); let __input = &__lane_ctx; - let __lane_mark = #core_types::record::stack::sp(); - let _entry_sp = __lane_mark; + // SAFETY: the lane's record is copied out before the scope + // releases it, and valueless exits serve nothing above it. + let __lane_scope = unsafe { #core_types::record::stack::ScopeGuard::enter() }; + let _entry_sp = #core_types::record::stack::sp(); let __cell = __cell.snapshot(); #(#rebinds)* #(#binds)* @@ -2217,21 +2219,16 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn #core_types::gpoll::GPoll::Error(__error) if __error.kind == #core_types::gpoll::ErrorKind::PastEnd => { __filled = __lane; __hint = #core_types::gpoll::Extent::Exactly(__range.start as usize + __lane); - unsafe { #core_types::record::stack::rewind(__lane_mark) }; break; } #core_types::gpoll::GPoll::Error(__error) => return #core_types::node::BatchStatus::Error(*__error), }; // SAFETY: the lane region is in-bounds by the scratch check, - // and the frame is fully copied out before the rewind. - unsafe { - ::core::ptr::copy_nonoverlapping(__node_layout.rec(&__value).ptr(), __frames.add(__lane * __stride), __stride); - #core_types::record::stack::rewind(__lane_mark); - } + // and the frame is fully copied out before the lane scope + // releases it. + unsafe { ::core::ptr::copy_nonoverlapping(__node_layout.rec(&__value).ptr(), __frames.add(__lane * __stride), __stride) }; } - // SAFETY: every lane was copied into the caller's scratch, so - // nothing above the entry mark is live. - unsafe { #core_types::record::stack::rewind(__entry_mark) }; + drop(__entry_scope); // SAFETY: the first `__filled` lanes were filled above with // records of the node's layout. #core_types::node::BatchStatus::Filled(unsafe { #core_types::node::RecordBatchMut::new(__scratch, __filled, __node_layout) }, __finality, __hint) @@ -2269,8 +2266,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn if __scratch.len() * 8 < __len * __stride { return #core_types::node::BatchStatus::InvalidRange; } - let __entry_mark = #core_types::record::stack::sp(); - let _entry_sp = __entry_mark; + // SAFETY: every lane copies into the caller's scratch, so + // nothing served above the entry escapes the batch scope. + let __entry_scope = unsafe { #core_types::record::stack::ScopeGuard::enter() }; + let _entry_sp = #core_types::record::stack::sp(); let __cell = #cell_constructor; let __base_ctx = { let mut __ctx = *__input; @@ -2616,11 +2615,13 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn match reads_out { false => body, true => { - let mark = format_ident!("__mark_{index}"); + let mark = format_ident!("__scope_{index}"); quote! { - let #mark = #core_types::record::stack::sp(); + // SAFETY: the bind copies its reads out by value; the drop + // point matches the old rewind. + let #mark = unsafe { #core_types::record::stack::ScopeGuard::enter() }; #body - unsafe { #core_types::record::stack::rewind(#mark) }; + drop(#mark); } } } diff --git a/node-graph/nodes/brush/src/brush.rs b/node-graph/nodes/brush/src/brush.rs index e09f6e3a2f..d1292285d9 100644 --- a/node-graph/nodes/brush/src/brush.rs +++ b/node-graph/nodes/brush/src/brush.rs @@ -19,21 +19,13 @@ use raster_types::BitmapMut; use raster_types::Image; use raster_types::{CPU, Raster}; -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, dyn_any::DynAny)] pub struct BrushStampGenerator { color: P, feather_exponent: f32, transform: DAffine2, } -// SAFETY: `Static` is `Self` with `P` at its own static projection. -unsafe impl dyn_any::StaticType for BrushStampGenerator

-where - P::Static: Pixel + Alpha, -{ - type Static = BrushStampGenerator; -} - impl Transform for BrushStampGenerator

{ fn transform(&self) -> DAffine2 { self.transform diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index 333f3d0683..6e4045314b 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -246,8 +246,8 @@ mod tests { } fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> { - core_types::record::stack::reserve(1 << 16); - EvalScope::new(Some(0.5), None, None, generations, arena) + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { core_types::record::stack::reserve(1 << 16); } EvalScope::new(Some(0.5), None, None, generations, arena) } fn element_layout() -> core_types::record::Layout diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index 0e982d67d4..b898884e05 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -538,8 +538,8 @@ mod tests { } fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> { - stack::reserve(1 << 16); - EvalScope::new(Some(0.5), None, None, generations, arena) + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { stack::reserve(1 << 16); } EvalScope::new(Some(0.5), None, None, generations, arena) } fn f64_layout(names: &[&'static str]) -> Layout { @@ -579,8 +579,8 @@ mod tests { } fn reserve_for(layouts: &[&Layout]) { - stack::reserve(layouts.iter().map(|layout| layout.frame_bytes()).sum::().max(1 << 12)); - } + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { stack::reserve(layouts.iter().map(|layout| layout.frame_bytes()).sum::().max(1 << 12)); } } fn install>>(mut node: N, meta: core_types::record::LayoutMeta, inputs: &[Option<&Layout>]) -> N { // The fixtures wire constants into every eager input, which the compiler @@ -1660,14 +1660,13 @@ mod tests { let head = ctx.index_head(); for (lane, element) in [(0u64, 10.), (1, 11.)] { - let mark = stack::sp(); + let _lane_scope = unsafe { stack::ScopeGuard::enter() }; let GPoll::Final(value) = node.eval(&ctx.promoted(&head, lane)) else { panic!("expected a final record at lane {lane}"); }; let rec = out.rec(&value); assert_eq!(unsafe { rec.element::() }, element, "lane {lane}"); assert_eq!(unsafe { rec.read::<&[NodeId]>(offset) }, path.as_slice(), "lane {lane}"); - unsafe { stack::rewind(mark) }; } } diff --git a/node-graph/nodes/graphic/src/record.rs b/node-graph/nodes/graphic/src/record.rs index 57a45e05b6..9db28595a0 100644 --- a/node-graph/nodes/graphic/src/record.rs +++ b/node-graph/nodes/graphic/src/record.rs @@ -264,8 +264,8 @@ mod tests { } fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> { - stack::reserve(1 << 16); - EvalScope::new(Some(0.5), None, None, generations, arena) + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { stack::reserve(1 << 16); } EvalScope::new(Some(0.5), None, None, generations, arena) } fn install>>(mut node: N, meta: record::LayoutMeta, inputs: &[Option<&Layout>]) -> N { @@ -841,13 +841,13 @@ mod tests { let wrap_out = Node::::layout(&wrapped).clone(); let head = ctx.index_head(); let group = { - let mark = stack::sp(); + // SAFETY: the element is cloned out inside the scope, so no borrow + // into the frame escapes it. + let _scope = unsafe { stack::ScopeGuard::enter() }; let GPoll::Final(value) = wrapped.eval(&ctx.promoted(&head, 0)) else { panic!("expected a final record"); }; let group = unsafe { record::borrow_element::(wrap_out.rec(&value)) }.clone(); - // SAFETY: the element was cloned out above, so no borrow into the frame remains. - unsafe { stack::rewind(mark) }; group }; diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index d4c4b503fa..ea6ee6679f 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -267,8 +267,8 @@ mod tests { let probe = core_types::record::RecordLift::::new(ProbeNode); let layout = Node::::layout(&probe).clone(); - core_types::record::stack::reserve(layout.frame_bytes().max(1 << 12)); - let mut graph = CreateContextNode::new(probe, &layout); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { core_types::record::stack::reserve(layout.frame_bytes().max(1 << 12)); } let mut graph = CreateContextNode::new(probe, &layout); // The executor resolves and installs the node's own layout at wiring; // without it the flip tail writes through the default empty layout. Node::::set_layout( diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index 258760c0a5..d893994e0d 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -1044,8 +1044,8 @@ mod graphene_test { } fn reserve_for(layouts: &[&Layout]) { - stack::reserve(layouts.iter().map(|layout| layout.frame_bytes()).sum::().max(1 << 12)); - } + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { stack::reserve(layouts.iter().map(|layout| layout.frame_bytes()).sum::().max(1 << 12)); } } /// Lifts a plain-element test source onto a record wire, returned beside its /// element-only layout for the generated node's constructor. diff --git a/node-graph/nodes/raster/src/image_color_palette.rs b/node-graph/nodes/raster/src/image_color_palette.rs index b00c5dac68..1c29b4c73e 100644 --- a/node-graph/nodes/raster/src/image_color_palette.rs +++ b/node-graph/nodes/raster/src/image_color_palette.rs @@ -70,8 +70,8 @@ mod test { #[test] fn test_image_color_palette() { - core_types::record::stack::reserve(1 << 16); - let arena = core_types::arena::Arena::new(1 << 22).unwrap(); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { core_types::record::stack::reserve(1 << 16); } let arena = core_types::arena::Arena::new(1 << 22).unwrap(); let generations = []; let scope = core_types::context::EvalScope::new(None, None, None, &generations, &arena); let ctx = core_types::context::ContextImpl::root(&scope); diff --git a/node-graph/nodes/repeat/src/repeat_nodes.rs b/node-graph/nodes/repeat/src/repeat_nodes.rs index ce9ddfa224..6fee231a8d 100644 --- a/node-graph/nodes/repeat/src/repeat_nodes.rs +++ b/node-graph/nodes/repeat/src/repeat_nodes.rs @@ -216,8 +216,8 @@ mod test { } fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> { - stack::reserve(1 << 12); - EvalScope::new(Some(0.5), None, None, generations, arena) + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { stack::reserve(1 << 12); } EvalScope::new(Some(0.5), None, None, generations, arena) } struct VectorRows { diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 38e76c8678..9f747abc60 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -3870,8 +3870,8 @@ mod test { } #[test] fn path_length() { - core_types::record::stack::reserve(1 << 16); - let arena = core_types::arena::Arena::new(1 << 20).unwrap(); + // SAFETY: between evaluations, nothing served on the stack is live. + unsafe { core_types::record::stack::reserve(1 << 16); } let arena = core_types::arena::Arena::new(1 << 20).unwrap(); let generations = []; let scope = core_types::context::EvalScope::new(None, None, None, &generations, &arena); let ctx = core_types::context::ContextImpl::root(&scope);