From a2de2a9c09e0ca64ccd3ec9ee4138e6c2a8c0bb9 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 6 Aug 2026 08:28:52 +0000 Subject: [PATCH] Add the record-opaque kernel class to the node macro and define memoize through it --- .../interpreted-executor/src/node_registry.rs | 24 ---- node-graph/libraries/core-types/src/record.rs | 97 +++++--------- node-graph/node-macro/src/codegen.rs | 118 +++++++++++++++++- node-graph/nodes/gcore/src/memo.rs | 70 +++++++---- node-graph/nodes/gcore/src/record.rs | 6 +- 5 files changed, 196 insertions(+), 119 deletions(-) diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 358102875a..1636695e15 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -189,30 +189,6 @@ fn node_registry() -> HashMap> { // ========== // MEMO NODES // ========== - ( - ProtoNodeIdentifier::new("graphene_core::memo::MemoizeNode"), - RegistryEntry { - io: NodeIOTypes::new( - concrete!(Context), - core_types::Type::Record(Box::new(core_types::Type::Generic(std::borrow::Cow::Borrowed("T")))), - vec![core_types::registry::generic_record_edge_type("T")], - ), - constructor: |inputs| { - if inputs.len() != 1 { - return Err(ConstructionError::Arity { expected: 1, got: inputs.len() }); - } - let mut inputs = inputs.into_iter(); - let handle = inputs.next().unwrap(); - let ty = handle.ty().clone(); - let Some(layout) = handle.layout().cloned() else { - return Err(ConstructionError::MissingLayout); - }; - let edge = handle.downcast_erased::(ty.clone())?; - let node = core_types::record::RecordMemo::new(edge, &layout); - Ok(EdgeHandle::new_erased(std::sync::Arc::new(node) as std::sync::Arc, ty)) - }, - }, - ), // ============ // REF ADAPTERS // ============ diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index ed3761fb1c..8353e82734 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -364,6 +364,32 @@ pub fn lift_poll<'e, T: Send + Sync + 'static>(poll: GPoll, layout: &Layout, } } +/// The raw lazy record edge handed to a record-opaque kernel: the wire plus +/// its wiring-proven layout, the pairing the kernel's unsafe record +/// operations rely on. The kernel must only pair the layout with values this +/// edge produced. +pub struct RecordEdgeInput<'a, N> { + node: &'a N, + layout: &'a Layout, +} + +impl<'a, N> RecordEdgeInput<'a, N> { + pub fn new(node: &'a N, layout: &'a Layout) -> Self { + Self { node, layout } + } + + pub fn layout(&self) -> &Layout { + self.layout + } + + pub fn eval<'e, C>(&self, ctx: &C) -> GPoll> + where + N: Node>, + { + self.node.eval(ctx) + } +} + /// The raw lazy edge handed to a poll kernel whose wire rides records while /// the kernel consumes the plain element. pub struct ElementEdge<'a, El, N> { @@ -799,6 +825,12 @@ pub struct OwnedRecord { fields: Vec<(usize, Box)>, } +impl std::fmt::Debug for OwnedRecord { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("OwnedRecord(..)") + } +} + impl OwnedRecord { /// # Safety /// `rec` must be a live record of `layout`. @@ -846,71 +878,6 @@ impl OwnedRecord { } } -// TODO: Convert to a `#[node_macro::node]` node with the monitor, once the -// macro grows a capture capability. -/// Memoizes a record wire: a hit replays the deep copy into the current -/// evaluation, a miss evaluates the edge and copies the record out. -pub struct RecordMemo { - edge: N, - layout: Layout, - cache: std::sync::Mutex>, -} - -impl RecordMemo { - pub fn new(edge: N, layout: &Layout) -> Self { - Self { - edge, - layout: layout.clone(), - cache: std::sync::Mutex::new(None), - } - } -} - -impl<'e, C, N> Node for RecordMemo -where - C: crate::graphene_hash::CacheHash + crate::context::ExtractArena, - N: Node>, -{ - type Output = RecordValue<'e>; - - fn eval(&self, input: &C) -> GPoll> { - let key = crate::registry::cache_key(input); - { - let cache = self.cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner); - if let Some((hash, copy, finality)) = cache.as_ref() - && *hash == key - { - return match copy.replay(&self.layout, input.arena()) { - Some(value) => match finality { - crate::gpoll::Finality::AllFinal => GPoll::Final(value), - crate::gpoll::Finality::Partial => GPoll::Partial(value), - }, - None => GPoll::arena_exhausted(), - }; - } - } - let result = self.edge.eval(input); - let publishable = match &result { - GPoll::Final(record) => Some((record, crate::gpoll::Finality::AllFinal)), - GPoll::Partial(record) => Some((record, crate::gpoll::Finality::Partial)), - _ => None, - }; - if let Some((record, finality)) = publishable { - let copy = unsafe { OwnedRecord::copy_out(&self.layout, self.layout.rec(record)) }; - *self.cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = Some((key, copy, finality)); - } - result - } - - fn extent(&self, input: &C) -> GPoll { - self.edge.extent(input) - } - - fn layout(&self) -> Option<&Layout> { - Some(&self.layout) - } -} - /// Lifts a plain producer onto a record wire: the element lands at offset 0 /// of a fresh element-only record, parked when it carries drop glue. pub struct RecordLift { diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 968f6c6505..690931f980 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -42,6 +42,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let record = record_shape(parsed); let routing = routing_io(parsed); let flip = record_flip(parsed); + let opaque = record_opaque(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. @@ -171,7 +172,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn })); state } - None if routing.is_some() => vec![quote!(pub(super) __layout: gcore::record::Layout)], + None if routing.is_some() || opaque => vec![quote!(pub(super) __layout: gcore::record::Layout)], None if flip => { let mut state = vec![quote!(pub(super) __layout: gcore::record::Layout), quote!(pub(super) __frame_bytes: usize)]; state.extend((0..struct_regular_fields.len()).map(|index| { @@ -331,8 +332,8 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }; // Record nodes construct through the generated `wire` fn, which resolves // offsets from the carrier layout; `new` cannot fill that state. - let routing_layout_param = routing.is_some().then(|| quote!(__layout: &gcore::record::Layout,)).into_iter(); - let routing_layout_init = routing.is_some().then(|| quote!(__layout: __layout.clone(),)).into_iter(); + let routing_layout_param = (routing.is_some() || opaque).then(|| quote!(__layout: &gcore::record::Layout,)).into_iter(); + let routing_layout_init = (routing.is_some() || opaque).then(|| quote!(__layout: __layout.clone(),)).into_iter(); let flip_layout_params = flip .then(|| { (0..struct_regular_fields.len()).map(|index| { @@ -750,6 +751,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let skips_carrier = record.as_ref().is_some_and(|shape| shape.skips_carrier()); let routing = routing_io(parsed); let flip = record_flip(parsed); + let opaque = record_opaque(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")); let mut ctx_bounds: Vec = match ctx_param { @@ -907,6 +909,14 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn generics.insert(0, quote!('__record)); } } + if opaque { + for (index, field) in regular_fields.iter().enumerate() { + if let ParsedFieldType::Node(NodeParsedField { output_type, .. }) = &field.ty { + let source_generic = format_ident!("__Source{index}"); + generics.push(quote!(#source_generic: #core_types::node::Node<#ctx_ident, Output = #output_type>)); + } + } + } let data_field_generic_idents: Vec = parsed .fn_generics @@ -980,6 +990,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let source_generic = format_ident!("__Source{index}"); quote!(#pat: #core_types::record::RecordLazyInput<'_, '__record, #source_generic>) } + ParsedFieldType::Node(NodeParsedField { output_type, .. }) if opaque && raw_lazy && is_record_value(output_type) => { + let source_generic = format_ident!("__Source{index}"); + quote!(#pat: &#core_types::record::RecordEdgeInput<'_, #source_generic>) + } ParsedFieldType::Node(NodeParsedField { output_type, .. }) if flip && raw_lazy => { let source_generic = format_ident!("__Source{index}"); quote!(#pat: &#core_types::record::ElementEdge<'_, #output_type, #source_generic>) @@ -1127,6 +1141,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let #name = #core_types::record::ElementLazyInput::<#output_type, _>::new(&self.#name, &__cell, #index, &self.#slot); } } + ParsedFieldType::Node(NodeParsedField { output_type, .. }) if opaque && raw_lazy && is_record_value(output_type) => quote! { + let #name = #core_types::record::RecordEdgeInput::new(&self.#name, &self.__layout); + }, ParsedFieldType::Node(_) if raw_lazy => quote!(), ParsedFieldType::Node(_) => quote! { let #name = #core_types::node::LazyInput::new(&self.#name, &__cell, #index); @@ -1153,6 +1170,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let name = &field.pat_ident.ident; match &field.ty { ParsedFieldType::Node(_) if flip && raw_lazy => quote!(&#name), + ParsedFieldType::Node(NodeParsedField { output_type, .. }) if opaque && raw_lazy && is_record_value(output_type) => quote!(&#name), ParsedFieldType::Node(_) if raw_lazy => quote!(&self.#name), _ => quote!(#name), } @@ -1534,7 +1552,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn false => Vec::new(), }; - let record_layout_impl = match record.is_some() || routing.is_some() || flip { + let record_layout_impl = match record.is_some() || routing.is_some() || flip || opaque { true => quote! { fn layout(&self) -> Option<&#core_types::record::Layout> { Some(&self.__layout) @@ -1927,6 +1945,17 @@ pub(crate) fn record_flip(parsed: &ParsedNodeFn) -> bool { true } +pub(crate) fn is_record_value(ty: &Type) -> bool { + matches!(ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "RecordValue")) +} + +/// Whether a kernel operates on whole records: it names `RecordValue` in its +/// output, receives raw record edges paired with the node's layout, and +/// takes on the record APIs' unsafe contracts itself. +pub(crate) fn record_opaque(parsed: &ParsedNodeFn) -> bool { + is_record_value(&slot_value_type(&parsed.output_type)) +} + pub(crate) fn routing_io(parsed: &ParsedNodeFn) -> Option { if has_record_io(parsed) || parsed.is_async { return None; @@ -2136,6 +2165,9 @@ fn entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, data_field_generic if record_flip(parsed) { return flip_entries_tokens(parsed, struct_name, regular_fields); } + if record_opaque(parsed) { + return record_opaque_entries_tokens(parsed, struct_name, regular_fields); + } let Some(rows) = implementation_rows(parsed, regular_fields) else { return quote!(); }; @@ -2483,6 +2515,84 @@ fn routing_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fi } } +fn record_opaque_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 { + let is_record = |field: &ParsedField| matches!(&field.ty, ParsedFieldType::Node(NodeParsedField { output_type, .. }) if is_record_value(output_type)); + let values_concrete = regular_fields.iter().filter(|field| !is_record(field)).all(|field| { + let (ty, lend) = match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) => (ty, lend.is_some()), + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => (output_type, false), + }; + !contains_open_generic(parsed, ty) && (lend || !type_disqualifies(ty)) + }); + if !values_concrete { + return quote!(); + } + + let fn_name = &parsed.fn_name; + let entries_name = format_ident!("{}_entries", fn_name); + let arity = regular_fields.len(); + let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect(); + + let input_types = regular_fields.iter().map(|field| { + if is_record(field) { + return quote!(gcore::registry::generic_record_edge_type("T")); + } + match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => quote!(gcore::registry::lend_edge_type::<#ty>()), + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(gcore::registry::edge_type::<#ty>()), + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(gcore::registry::edge_type::<#output_type>()), + } + }); + let downcasts = regular_fields.iter().enumerate().map(|(index, field)| { + let name = &field.pat_ident.ident; + if is_record(field) { + let layout = format_ident!("__layout_{index}"); + let handle = format_ident!("__handle_{index}"); + let ty = format_ident!("__ty_{index}"); + return quote! { + let #handle = inputs.next().unwrap(); + let #ty = #handle.ty().clone(); + let Some(#layout) = #handle.layout().cloned() else { + return Err(gcore::registry::ConstructionError::MissingLayout); + }; + let #name = #handle.downcast_erased::(#ty.clone())?; + }; + } + match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => quote!(let #name = inputs.next().unwrap().downcast_lend::<#ty>()?;), + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;), + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#output_type>()?;), + } + }); + let first_record = regular_fields.iter().position(|field| is_record(field)).expect("record-opaque nodes have a record input"); + let record_layout = format_ident!("__layout_{first_record}"); + let record_ty = format_ident!("__ty_{first_record}"); + + quote! { + pub fn #entries_name() -> ::std::vec::Vec { + vec![gcore::registry::RegistryEntry { + io: gcore::registry::NodeIOTypes::new( + gcore::concrete!(gcore::context::ContextImpl<'static>), + gcore::Type::Record(Box::new(gcore::Type::Generic(::std::borrow::Cow::Borrowed("T")))), + vec![#(#input_types),*], + ), + constructor: |inputs| { + if inputs.len() != #arity { + return Err(gcore::registry::ConstructionError::Arity { expected: #arity, got: inputs.len() }); + } + let mut inputs = inputs.into_iter(); + #(#downcasts)* + let __node = #struct_name::new(#(#names,)* &#record_layout); + Ok(gcore::registry::EdgeHandle::new_erased( + ::std::sync::Arc::new(__node) as ::std::sync::Arc, + #record_ty, + )) + }, + }] + } + } +} + fn record_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 { let Some(shape) = record_shape(parsed) else { return quote!(); diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index b470f6dbdb..36bf5cb474 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -5,37 +5,49 @@ use core_types::gpoll::{Extent, Finality, GPoll, Interrupt}; use core_types::graphene_hash::CacheHash; use core_types::memo::*; use core_types::node::Node; +use core_types::record::{OwnedRecord, RecordValue}; use core_types::registry::cache_key; use std::sync::Arc; use std::sync::Mutex; /// Helps speed up repeated renders in a computationally-heavy part of the node graph. /// -/// Stores the last evaluated data that flowed through this node and immediately returns that data on subsequent renders if the context has not changed. -#[node_macro::node(category("General"), path(graphene_core::memo), skip_impl, plain, extent(memoize_extent))] -fn memoize(input: I, #[data] cache: Arc>>, content: impl Node) -> GPoll { - let key = cache_key(&input); - if let Some((hash, value, finality)) = cache.lock().unwrap().as_ref() +/// Stores a deep copy of the last record that flowed through this node and replays it on subsequent renders if the context has not changed. +#[node_macro::node(category("General"), path(graphene_core::memo), extent(memoize_extent))] +fn memoize<'e>( + ctx: impl Ctx + CacheHash + ExtractArena<'e>, + #[data] cache: Arc>>, + content: impl Node, Output = RecordValue<'e>>, +) -> GPoll> { + let key = cache_key(&ctx); + if let Some((hash, copy, finality)) = cache.lock().unwrap().as_ref() && *hash == key { - return match finality { - Finality::AllFinal => GPoll::Final(value.clone()), - Finality::Partial => GPoll::Partial(value.clone()), + return match copy.replay(content.layout(), ctx.arena()) { + Some(value) => match finality { + Finality::AllFinal => GPoll::Final(value), + Finality::Partial => GPoll::Partial(value), + }, + None => GPoll::arena_exhausted(), }; } - let result = content.eval(input); - match &result { - GPoll::Final(value) => *cache.lock().unwrap() = Some((key, value.clone(), Finality::AllFinal)), - GPoll::Partial(value) => *cache.lock().unwrap() = Some((key, value.clone(), Finality::Partial)), - GPoll::Pending | GPoll::Fallback(_) | GPoll::Error(_) => {} + let result = content.eval(&ctx); + let publishable = match &result { + GPoll::Final(value) => Some((value, Finality::AllFinal)), + GPoll::Partial(value) => Some((value, Finality::Partial)), + GPoll::Pending | GPoll::Fallback(_) | GPoll::Error(_) => None, + }; + if let Some((value, finality)) = publishable { + // SAFETY: the value came from this edge, so it carries the edge's layout. + let copy = unsafe { OwnedRecord::copy_out(content.layout(), content.layout().rec(value)) }; + *cache.lock().unwrap() = Some((key, copy, finality)); } result } -fn memoize_extent(node: &MemoizeNode, ctx: &C) -> GPoll +fn memoize_extent(node: &MemoizeNode, ctx: &C) -> GPoll where - T: Clone, - NodeContent: Node, + NodeContent: Node, { node.content.extent(ctx) } @@ -137,7 +149,7 @@ mod tests { use super::*; use core_types::SourceId; use core_types::context::{ContextImpl, EvalScope}; - use core_types::registry::{EdgeHandle, ErasedLendNode, ErasedNode}; + use core_types::registry::{EdgeHandle, ErasedLendNode, ErasedNode, ErasedRecordNode}; use std::sync::atomic::{AtomicU32, Ordering}; struct CountingNode(AtomicU32); @@ -174,6 +186,10 @@ mod tests { EvalScope::new(Some(0.5), None, None, generations, arena) } + fn element_layout() -> core_types::record::Layout { + core_types::record::Layout::default().with_writes(0, core_types::record::element_write::(), &[]) + } + #[test] fn monitor_serialize_exposes_the_io_record_through_the_edge() { let arena = Arena::new(1024).unwrap(); @@ -199,7 +215,9 @@ mod tests { let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); - let memoized = MemoizeNode::new(CountingNode(AtomicU32::new(0))); + let layout = element_layout::(); + let memoized = MemoizeNode::new(core_types::record::RecordLift::::new(CountingNode(AtomicU32::new(0))), &layout); + let memoized = core_types::record::RecordExtract::::new(memoized, &layout); assert_eq!(memoized.eval(&ctx), GPoll::Final(1)); assert_eq!(memoized.eval(&ctx), GPoll::Final(1)); @@ -214,7 +232,9 @@ mod tests { let scope_before = scope_fixture(&before, &arena); let scope_after = scope_fixture(&after, &arena); - let memoized = MemoizeNode::new(CountingNode(AtomicU32::new(0))); + let layout = element_layout::(); + let memoized = MemoizeNode::new(core_types::record::RecordLift::::new(CountingNode(AtomicU32::new(0))), &layout); + let memoized = core_types::record::RecordExtract::::new(memoized, &layout); assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1)); assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1)); @@ -228,7 +248,9 @@ mod tests { let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); - let memoized = MemoizeNode::new(PartialCountingNode(AtomicU32::new(0))); + let layout = element_layout::(); + let memoized = MemoizeNode::new(core_types::record::RecordLift::::new(PartialCountingNode(AtomicU32::new(0))), &layout); + let memoized = core_types::record::RecordExtract::::new(memoized, &layout); assert_eq!(memoized.eval(&ctx), GPoll::Partial(1)); assert_eq!(memoized.eval(&ctx), GPoll::Partial(1)); @@ -241,9 +263,11 @@ mod tests { let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); - let edge = EdgeHandle::new(Arc::new(CountingNode(AtomicU32::new(0))) as Arc>); - let memoized = EdgeHandle::new(Arc::new(MemoizeNode::new(edge.downcast::().unwrap())) as Arc>); - let stacked = MemoizeNode::new(memoized.downcast::().unwrap()); + let layout = element_layout::(); + let edge = EdgeHandle::new_record::(Arc::new(core_types::record::RecordLift::::new(CountingNode(AtomicU32::new(0)))) as Arc); + let memoized = EdgeHandle::new_record::(Arc::new(MemoizeNode::new(edge.downcast_record::().unwrap(), &layout)) as Arc); + let stacked = MemoizeNode::new(memoized.downcast_record::().unwrap(), &layout); + let stacked = core_types::record::RecordExtract::::new(stacked, &layout); assert_eq!(stacked.eval(&ctx), GPoll::Final(1)); assert_eq!(stacked.eval(&ctx), GPoll::Final(1)); diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index 577367ad95..6dc929b096 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -752,7 +752,7 @@ mod tests { let evals = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)); let lift = core_types::record::RecordLift::::new(CountingValue(evals.clone())); let layout = Node::::layout(&lift).unwrap().clone(); - let memo = core_types::record::RecordMemo::new(lift, &layout); + let memo = crate::memo::MemoizeNode::new(lift, &layout); let GPoll::Final(value) = memo.eval(&ctx) else { panic!("expected a final record"); @@ -781,7 +781,7 @@ mod tests { fields: vec![(layout.offset_of("opacity", 0).unwrap(), 0.5)], partial: true, }; - let memo = core_types::record::RecordMemo::new(source, &layout); + let memo = crate::memo::MemoizeNode::new(source, &layout); let GPoll::Partial(_) = memo.eval(&ctx) else { panic!("expected a partial record"); @@ -801,7 +801,7 @@ mod tests { reserve_for(&[&labeled, &labeled]); let chain = LabelNode::new(bare_source(&source_layout, 1.), ValueNode(String::from("a")), &source_layout); - let memo = core_types::record::RecordMemo::new(chain, &labeled); + let memo = crate::memo::MemoizeNode::new(chain, &labeled); let first_arena = Arena::new(1024).unwrap(); {