From 9fc4fcc7a00053a2b9d1e40e53da1626cecd2c48 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Wed, 5 Aug 2026 17:23:09 +0000 Subject: [PATCH] Inline records of at most 16 bytes into the two-word RecordValue with layout-resolved storage --- .../src/dynamic_executor.rs | 2 +- .../interpreted-executor/src/node_registry.rs | 4 +- node-graph/libraries/core-types/src/record.rs | 139 ++++++++++++++---- node-graph/node-macro/src/codegen.rs | 20 ++- node-graph/nodes/gcore/src/record.rs | 105 ++++++++----- 5 files changed, 198 insertions(+), 72 deletions(-) diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index 5dbcef89c0..15e40585d0 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -481,7 +481,7 @@ impl BorrowTree { pub fn stack_need(&self) -> usize { self.nodes .values() - .map(|(handle, _)| handle.layout().map_or(0, |layout| layout.size.next_multiple_of(8))) + .map(|(handle, _)| handle.layout().map_or(0, |layout| layout.frame_bytes())) .sum() } } diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 9ffc7d1eda..e13c73c31d 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -825,7 +825,9 @@ mod node_registry_macros { return Err(ConstructionError::Arity { expected: 1, got: inputs.len() }); } let mut inputs = inputs.into_iter(); - let node = core_types::record::RecordExtract::<$type, _>::new(inputs.next().unwrap().downcast_record::<$type>()?); + let edge = inputs.next().unwrap(); + let layout = edge.layout().ok_or(ConstructionError::MissingLayout)?.clone(); + let node = core_types::record::RecordExtract::<$type, _>::new(edge.downcast_record::<$type>()?, &layout); Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc>)) }, }, diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index 6c2da59b11..0543f7433a 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -1,8 +1,9 @@ //! 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 +//! wiring from the upstream write set and never serialized. Records of +//! inline layouts live in the [`RecordValue`] itself; larger ones live as //! per-lane views on the per-thread record [`stack`], claimed per -//! evaluation, and kernels route them as opaque [`RecordValue`]s that carry +//! evaluation. 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. @@ -74,6 +75,23 @@ impl Layout { self.fields.iter().find(|field| field.name == name && field.level == level).map(|field| field.offset) } + pub fn is_inline(&self) -> bool { + self.size <= 16 && self.align <= 8 + } + + pub fn frame_bytes(&self) -> usize { + if self.is_inline() { 0 } else { self.size.next_multiple_of(8) } + } + + /// Resolves a value of this layout, which must be its wiring-proven one, + /// to its record bytes. + pub fn rec(&self, value: &RecordValue<'_>) -> Rec { + match self.is_inline() { + true => Rec((&raw const *value).cast()), + false => Rec(value.ptr), + } + } + /// The union of this layout's fields and `writes` over an element of /// (size, align) at `depth`, in canonical order. A (name, level) written /// at a different size is a type conflict and panics; the census keeps @@ -190,23 +208,48 @@ impl Rec { } } -/// An opaque record value: element and attributes traveling as one unit. -/// Lazy record inputs yield one per evaluation, kernels route them as -/// ordinary values, and the returned value's record is the node's output, so -/// provenance rides the value itself. The eval lifetime keeps it out of node -/// state; the field is private, so it is unforgeable and uninspectable. -#[derive(Clone, Copy, Debug)] -pub struct RecordValue<'e>(Rec, std::marker::PhantomData<&'e ()>); +/// An opaque, tagless record value: inline layouts live in the value's own +/// two words, spilled layouts ride the pointer, and only [`Layout::rec`] +/// tells them apart. Two scalar fields are load-bearing: a union or an +/// over-aligned repr demotes the return to memory. Both constructors +/// initialize all 16 bytes, and a raw pointer field accepts any bit +/// pattern, so inline bytes overlay the fields soundly. +#[derive(Clone, Copy)] +pub struct RecordValue<'e> { + ptr: *const u8, + _extra: usize, + _lifetime: std::marker::PhantomData<&'e ()>, +} + +impl std::fmt::Debug for RecordValue<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("RecordValue(..)") + } +} impl<'e> RecordValue<'e> { #[doc(hidden)] - pub fn from_rec(rec: Rec) -> Self { - RecordValue(rec, std::marker::PhantomData) + pub fn zeroed() -> Self { + RecordValue { + ptr: std::ptr::null(), + _extra: 0, + _lifetime: std::marker::PhantomData, + } + } + + /// The inline storage under construction; writes land in the value itself. + #[doc(hidden)] + pub fn as_mut_ptr(&mut self) -> *mut u8 { + (&raw mut *self).cast() } #[doc(hidden)] - pub fn rec(self) -> Rec { - self.0 + pub fn spilled(rec: Rec) -> Self { + RecordValue { + ptr: rec.ptr(), + _extra: 0, + _lifetime: std::marker::PhantomData, + } } } @@ -350,7 +393,8 @@ fn default_fill_bytes(name: &str, size: usize) -> Box<[u8]> { pub struct SourcePlan { moves: Vec<(usize, usize, usize)>, fills: Vec<(usize, Box<[u8]>)>, - union_bytes: usize, + source: Layout, + union: Layout, } impl SourcePlan { @@ -368,7 +412,8 @@ impl SourcePlan { Some(SourcePlan { moves, fills, - union_bytes: union.size, + source: source.clone(), + union: union.clone(), }) } @@ -460,7 +505,7 @@ where fn eval(&self, input: &C) -> GPoll> { let value = self.edge.eval(input); if let GPoll::Final(record) | GPoll::Partial(record) = &value { - let bytes: Box<[u8]> = unsafe { std::slice::from_raw_parts(record.rec().ptr(), self.layout.size) }.into(); + let bytes: Box<[u8]> = unsafe { std::slice::from_raw_parts(self.layout.rec(record).ptr(), self.layout.size) }.into(); let capture = input.arena().alloc(bytes).map(|(_, weak)| RecordCapture { layout: self.layout.clone(), bytes: weak, @@ -486,18 +531,14 @@ where pub struct RecordLift { edge: N, layout: Layout, - frame_bytes: usize, _marker: std::marker::PhantomData El>, } impl RecordLift { pub fn new(edge: N) -> Self { - let layout = Layout::default().with_writes(0, (size_of::(), align_of::()), &[]); - let frame_bytes = layout.size.next_multiple_of(8); Self { edge, - layout, - frame_bytes, + layout: Layout::default().with_writes(0, (size_of::(), align_of::()), &[]), _marker: std::marker::PhantomData, } } @@ -512,10 +553,17 @@ where type Output = RecordValue<'e>; fn eval(&self, input: &C) -> GPoll> { - let dst = stack::push(self.frame_bytes); + if self.layout.is_inline() { + return self.edge.eval(input).map(|element| { + let mut value = RecordValue::zeroed(); + unsafe { write_field(value.as_mut_ptr(), 0, element) }; + value + }); + } + let dst = stack::push(self.layout.frame_bytes()); let value = self.edge.eval(input).map(|element| { unsafe { write_field(dst, 0, element) }; - RecordValue::from_rec(unsafe { Rec::new(dst.cast_const()) }) + RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) }) }); stack::pop(dst); value @@ -529,13 +577,15 @@ where /// Extracts the element from a record wire for a plain consumer. pub struct RecordExtract { edge: N, + layout: Layout, _marker: std::marker::PhantomData El>, } impl RecordExtract { - pub fn new(edge: N) -> Self { + pub fn new(edge: N, layout: &Layout) -> Self { Self { edge, + layout: layout.clone(), _marker: std::marker::PhantomData, } } @@ -549,7 +599,7 @@ where type Output = El; fn eval(&self, input: &C) -> GPoll { - self.edge.eval(input).map(|value| unsafe { value.rec().element::() }) + self.edge.eval(input).map(|value| unsafe { self.layout.rec(&value).element::() }) } } @@ -562,10 +612,15 @@ where fn eval(&self, input: &C) -> GPoll> { match &self.plan { None => self.edge.eval(input), + Some(plan) if plan.union.is_inline() => self.edge.eval(input).map(|value| { + let mut out = RecordValue::zeroed(); + unsafe { plan.translate(plan.source.rec(&value), out.as_mut_ptr()) }; + out + }), Some(plan) => { - let dst = stack::push(plan.union_bytes); + let dst = stack::push(plan.union.frame_bytes()); let value = self.edge.eval(input); - value.map(|value| RecordValue::from_rec(unsafe { plan.translate(value.rec(), dst) })) + value.map(|value| RecordValue::spilled(unsafe { plan.translate(plan.source.rec(&value), dst) })) } } } @@ -632,6 +687,36 @@ mod tests { assert!(SourcePlan::new(&layout, &layout.clone()).is_none()); } + #[test] + fn record_values_are_two_words() { + assert_eq!(size_of::(), 16); + assert_eq!(align_of::(), 8); + } + + #[test] + fn layouts_resolve_inline_and_spilled_values() { + let inline = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]); + assert!(inline.is_inline()); + assert_eq!(inline.frame_bytes(), 0); + let mut value = RecordValue::zeroed(); + unsafe { + write_field(value.as_mut_ptr(), 0, 4f64); + write_field(value.as_mut_ptr(), inline.offset_of("opacity", 0).unwrap(), 0.5f64); + } + let rec = inline.rec(&value); + assert_eq!(unsafe { rec.element::() }, 4.); + assert_eq!(unsafe { rec.read::(inline.offset_of("opacity", 0).unwrap()) }, 0.5); + + let spilled = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity"), f64_field("length")]); + assert!(!spilled.is_inline()); + assert_eq!(spilled.frame_bytes(), 24); + let record = [1f64, 2., 3.]; + let value = RecordValue::spilled(unsafe { Rec::new(record.as_ptr().cast()) }); + assert_eq!(unsafe { spilled.rec(&value).element::() }, 1.); + assert_eq!(unsafe { spilled.rec(&value).read::(spilled.offset_of("length", 0).unwrap()) }, 2.); + assert_eq!(unsafe { spilled.rec(&value).read::(spilled.offset_of("opacity", 0).unwrap()) }, 3.); + } + #[test] fn stack_frames_nest_and_release() { stack::reserve(64); diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 4898f0d2d0..beadaca187 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -123,6 +123,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn Some(shape) => { let mut state = vec![quote!(pub(super) __layout: gcore::record::Layout)]; if !shape.skips_carrier() { + state.push(quote!(pub(super) __carrier: gcore::record::Layout)); state.push(quote!(pub(super) __plan: ::std::vec::Vec<(usize, usize, usize)>)); } state.push(quote!(pub(super) __frame_bytes: usize)); @@ -1113,7 +1114,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn Ok(value) => value, Err(interrupt) => return interrupt.into(), }; - let __src_rec = #core_types::record::RecordValue::rec(__src); + let __src_rec = self.__carrier.rec(&__src); } }); let carry = (!shape.skips_carrier()).then(|| quote!(unsafe { #core_types::record::apply_plan(__src_rec, __dst, &self.__plan) };)); @@ -1152,7 +1153,11 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote!(unsafe { #core_types::record::write_field(__dst, self.#slot, #binder) };) }); quote! { - let __dst = #core_types::record::stack::push(self.__frame_bytes); + let mut __value = #core_types::record::RecordValue::zeroed(); + let __dst = match self.__frame_bytes { + 0 => __value.as_mut_ptr(), + __bytes => #core_types::record::stack::push(__bytes), + }; #carrier_eval #carry #(#read_bindings)* @@ -1160,8 +1165,11 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn #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()) })) + if self.__frame_bytes != 0 { + #core_types::record::stack::pop(__dst); + __value = #core_types::record::RecordValue::spilled(unsafe { #core_types::record::Rec::new(__dst.cast_const()) }); + } + __cell.finish(__value) } }); let eval_tail = match (async_fn, future_kernel) { @@ -1288,6 +1296,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let name = &field.pat_ident.ident; quote!(#name,) }); + let carrier_init = (!shape.skips_carrier()).then(|| quote!(__carrier: __carrier_layout.clone(),)).into_iter(); 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,)); @@ -1302,10 +1311,11 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn #plan_binding #(#read_inits)* #(#write_inits)* - let __frame_bytes = __layout.size.next_multiple_of(8); + let __frame_bytes = __layout.frame_bytes(); Self { #(#data_inits)* #(#edge_inits)* + #(#carrier_init)* __layout, #(#plan_init)* __frame_bytes, diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index 8c555f7609..f0c2cfd0cc 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -104,7 +104,7 @@ mod tests { } struct RecordSourceNode { - frame_bytes: usize, + layout: Layout, element: E, fields: Vec<(usize, f64)>, partial: bool, @@ -114,15 +114,21 @@ mod tests { type Output = RecordValue<'e>; fn eval(&self, _input: &ContextImpl<'e>) -> GPoll> { - let dst = stack::push(self.frame_bytes); - let value = unsafe { - dst.cast::().write(self.element); - for (offset, value) in &self.fields { - dst.add(*offset).cast::().write(*value); - } - RecordValue::from_rec(Rec::new(dst)) + let mut value = RecordValue::zeroed(); + let dst = match self.layout.frame_bytes() { + 0 => value.as_mut_ptr(), + bytes => stack::push(bytes), }; - stack::pop(dst); + unsafe { + dst.cast::().write(self.element); + for (offset, field) in &self.fields { + dst.add(*offset).cast::().write(*field); + } + } + if self.layout.frame_bytes() != 0 { + stack::pop(dst); + value = RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) }); + } match self.partial { true => GPoll::Partial(value), false => GPoll::Final(value), @@ -148,17 +154,13 @@ mod tests { Layout::default().with_writes(0, (8, 8), &writes) } - fn frame_bytes(layout: &Layout) -> usize { - layout.size.next_multiple_of(8) - } - fn reserve_for(layouts: &[&Layout]) { - stack::reserve(layouts.iter().map(|layout| frame_bytes(layout)).sum()); + stack::reserve(layouts.iter().map(|layout| layout.frame_bytes()).sum()); } fn bare_source(layout: &Layout, element: f64) -> RecordSourceNode { RecordSourceNode { - frame_bytes: frame_bytes(layout), + layout: layout.clone(), element, fields: vec![], partial: false, @@ -186,7 +188,7 @@ mod tests { let GPoll::Final(value) = chain.eval(&ctx) else { panic!("expected a final record"); }; - let rec = value.rec(); + let rec = stacked.rec(&value); assert_eq!(unsafe { rec.element::() }, 2.); assert_eq!(unsafe { rec.read::(stacked.offset_of(Opacity::NAME, 0).unwrap()) }, 0.25); } @@ -206,7 +208,7 @@ mod tests { let GPoll::Final(value) = chain.eval(&ctx) else { panic!("expected a final record"); }; - let rec = value.rec(); + let rec = measured.rec(&value); assert_eq!(unsafe { rec.element::() }, -2.); assert_eq!(unsafe { rec.read::(measured.offset_of(Length::NAME, 0).unwrap()) }, 2.); } @@ -227,7 +229,7 @@ mod tests { let GPoll::Final(value) = chain.eval(&ctx) else { panic!("expected a final record"); }; - let rec = value.rec(); + let rec = measured.rec(&value); assert_eq!(unsafe { rec.read::(measured.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5); assert_eq!(unsafe { rec.read::(measured.offset_of(Length::NAME, 0).unwrap()) }, 2.); } @@ -248,13 +250,13 @@ mod tests { let GPoll::Final(value) = bare.eval(&ctx) else { panic!("expected a final record"); }; - assert_eq!(unsafe { value.rec().element::() }, 4.); + assert_eq!(unsafe { source_layout.rec(&value).element::() }, 4.); let chain = ShadeNode::new(MultiplyOpacityNode::new(bare_source(&source_layout, 4.), ValueNode(0.5), &source_layout), &modified); let GPoll::Final(value) = chain.eval(&ctx) else { panic!("expected a final record"); }; - let rec = value.rec(); + let rec = shaded.rec(&value); assert_eq!(unsafe { rec.element::() }, 2.); assert_eq!(unsafe { rec.read::(shaded.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5); } @@ -276,13 +278,13 @@ mod tests { let GPoll::Final(value) = wide.eval(&ctx) else { panic!("expected a final record"); }; - let rec = value.rec(); + let rec = f64_faded.rec(&value); assert_eq!(unsafe { rec.element::() }, 8.); assert_eq!(unsafe { rec.read::(f64_faded.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5); let narrow = FadeNode::new( RecordSourceNode { - frame_bytes: frame_bytes(&u32_source), + layout: u32_source.clone(), element: 7u32, fields: vec![], partial: false, @@ -293,7 +295,7 @@ mod tests { let GPoll::Final(value) = narrow.eval(&ctx) else { panic!("expected a final record"); }; - let rec = value.rec(); + let rec = u32_faded.rec(&value); assert_eq!(unsafe { rec.element::() }, 7); assert_eq!(unsafe { rec.read::(u32_faded.offset_of(Opacity::NAME, 0).unwrap()) }, 0.25); } @@ -313,7 +315,7 @@ mod tests { let GPoll::Final(value) = node.eval(&ctx) else { panic!("expected a final record"); }; - let rec = value.rec(); + let rec = layout.rec(&value); assert_eq!(unsafe { rec.element::() }, 3.); assert_eq!(unsafe { rec.read::(layout.offset_of(Opacity::NAME, 0).unwrap()) }, 0.25); } @@ -331,7 +333,7 @@ mod tests { let chain = MultiplyOpacityNode::new( RecordSourceNode { - frame_bytes: frame_bytes(&source_layout), + layout: source_layout.clone(), element: 1., fields: vec![], partial: true, @@ -342,7 +344,7 @@ mod tests { let GPoll::Partial(value) = chain.eval(&ctx) else { panic!("expected a partial record"); }; - assert_eq!(unsafe { value.rec().read::(modified.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5); + assert_eq!(unsafe { modified.rec(&value).read::(modified.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5); } #[test] @@ -360,7 +362,7 @@ mod tests { let GPoll::Final(value) = ok.eval(&ctx) else { panic!("expected a final record"); }; - assert_eq!(unsafe { value.rec().read::(modified.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5); + assert_eq!(unsafe { modified.rec(&value).read::(modified.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5); let failing = CheckedMultiplyOpacityNode::new(bare_source(&source_layout, 1.), ValueNode(-1.), &source_layout); let GPoll::Error(error) = failing.eval(&ctx) else { @@ -401,7 +403,7 @@ mod tests { let GPoll::Final(value) = chain.eval(&ctx) else { panic!("expected a final record"); }; - let rec = value.rec(); + let rec = scaled.rec(&value); assert_eq!(unsafe { rec.element::() }, 6.); assert_eq!(unsafe { rec.read::(scaled.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5); } @@ -426,7 +428,7 @@ mod tests { let GPoll::Final(value) = chain.eval(&ctx) else { panic!("expected a final record"); }; - let rec = value.rec(); + let rec = relabeled.rec(&value); assert_eq!(unsafe { rec.element::() }, 1.); assert_eq!(unsafe { rec.read::<&str>(relabeled.offset_of(Label::NAME, 0).unwrap()) }, "ab"); } @@ -446,7 +448,7 @@ mod tests { fn f64_record_source(layout: &Layout, element: f64, fields: Vec<(usize, f64)>) -> RecordSourceNode { RecordSourceNode { - frame_bytes: frame_bytes(layout), + layout: layout.clone(), element, fields, partial: false, @@ -468,7 +470,7 @@ mod tests { let GPoll::Final(value) = monitor.eval(&ctx) else { panic!("expected a final record"); }; - assert_eq!(unsafe { value.rec().element::() }, 4.); + assert_eq!(unsafe { layout.rec(&value).element::() }, 4.); } let capture = Node::::serialize(&monitor).unwrap(); @@ -505,7 +507,7 @@ mod tests { let GPoll::Final(value) = taken(false).eval(&ctx) else { panic!("expected a final record"); }; - let rec = value.rec(); + let rec = union.rec(&value); assert_eq!(unsafe { rec.element::() }, 1.); assert_eq!(unsafe { rec.read::(union.offset_of("opacity", 0).unwrap()) }, 0.5); assert_eq!(unsafe { rec.read::(union.offset_of("length", 0).unwrap()) }, 0.); @@ -513,7 +515,7 @@ mod tests { let GPoll::Final(value) = taken(true).eval(&ctx) else { panic!("expected a final record"); }; - let rec = value.rec(); + let rec = union.rec(&value); assert_eq!(unsafe { rec.element::() }, 3.); assert_eq!(unsafe { rec.read::(union.offset_of("opacity", 0).unwrap()) }, 1.); assert_eq!(unsafe { rec.read::(union.offset_of("length", 0).unwrap()) }, 3.); @@ -540,12 +542,39 @@ mod tests { let GPoll::Final(value) = chain.eval(&ctx) else { panic!("expected a final record"); }; - let rec = value.rec(); + let rec = union.rec(&value); assert_eq!(unsafe { rec.element::() }, 1.); assert_eq!(unsafe { rec.read::(union.offset_of("opacity", 0).unwrap()) }, 0.5); assert_eq!(unsafe { rec.read::(union.offset_of("length", 0).unwrap()) }, 0.); } + #[test] + fn inline_records_survive_sibling_evaluations_by_value() { + let arena = Arena::new(1024).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let layout_a = f64_layout(&["opacity"]); + let layout_b = f64_layout(&[]); + let union = Layout::union(&[&layout_a, &layout_b]); + assert!(union.is_inline()); + reserve_for(&[&layout_a, &layout_b, &union, &union]); + + let chain = HoldFirstNode::new( + ValueNode(false), + RecordSource::new(f64_record_source(&layout_a, 1., vec![(layout_a.offset_of("opacity", 0).unwrap(), 0.5)]), &layout_a, &union), + RecordSource::new(f64_record_source(&layout_b, 3., vec![]), &layout_b, &union), + ); + + let GPoll::Final(value) = chain.eval(&ctx) else { + panic!("expected a final record"); + }; + let rec = union.rec(&value); + assert_eq!(unsafe { rec.element::() }, 1.); + assert_eq!(unsafe { rec.read::(union.offset_of("opacity", 0).unwrap()) }, 0.5); + } + #[test] fn identity_layouts_forward_the_record_pointer() { let arena = Arena::new(1024).unwrap(); @@ -553,7 +582,7 @@ mod tests { let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); - let layout = f64_layout(&["opacity"]); + let layout = f64_layout(&["opacity", "length"]); reserve_for(&[&layout]); let base = stack::push(0); stack::pop(base); @@ -567,7 +596,7 @@ mod tests { let GPoll::Final(value) = chain.eval(&ctx) else { panic!("expected a final record"); }; - let rec = value.rec(); + let rec = layout.rec(&value); assert_eq!(rec.ptr(), base.cast_const()); assert_eq!(unsafe { rec.element::() }, 4.); assert_eq!(unsafe { rec.read::(layout.offset_of("opacity", 0).unwrap()) }, 0.25); @@ -585,7 +614,7 @@ mod tests { let chain = ForwardRecordNode::new(RecordSource::new( RecordSourceNode { - frame_bytes: frame_bytes(&layout), + layout: layout.clone(), element: 4., fields: vec![], partial: true, @@ -597,6 +626,6 @@ mod tests { let GPoll::Partial(value) = chain.eval(&ctx) else { panic!("expected a partial record"); }; - assert_eq!(unsafe { value.rec().element::() }, 4.); + assert_eq!(unsafe { layout.rec(&value).element::() }, 4.); } }