Install record layouts from the pass via set_layout

This commit is contained in:
Dennis Kobert
2026-08-15 11:20:01 +00:00
parent bfa48439c8
commit f9e30c4c77
10 changed files with 302 additions and 165 deletions

View File

@@ -372,7 +372,7 @@ macro_rules! tagged_value {
}
$(
if ty == core_types::registry::record_edge_type::<$ty>() {
let layout = handle.layout().ok_or_else(|| "a record edge must carry its layout".to_string())?.clone();
let layout = handle.layout().clone();
return Ok(handle
.downcast_record::<$ty>()
.map_err(|e| format!("{e:?}"))?
@@ -381,7 +381,7 @@ macro_rules! tagged_value {
}
)*
if ty == core_types::registry::record_edge_type::<RenderOutput>() {
let layout = handle.layout().ok_or_else(|| "a record edge must carry its layout".to_string())?.clone();
let layout = handle.layout().clone();
return Ok(handle
.downcast_record::<RenderOutput>()
.map_err(|e| format!("{e:?}"))?

View File

@@ -128,7 +128,7 @@ impl ConstructionArgs {
pub(crate) struct Resolved {
pub io: Option<NodeIOTypes>,
pub layout_meta: Option<core_types::record::LayoutMeta>,
pub layout: Option<core_types::record::Layout>,
pub layout: Option<core_types::record::RecordLayout>,
}
impl PartialEq for Resolved {
@@ -221,7 +221,7 @@ impl ProtoNode {
}
}
pub fn resolved_layout(&self) -> Option<&core_types::record::Layout> {
pub fn resolved_layout(&self) -> Option<&core_types::record::RecordLayout> {
self.resolved.layout.as_ref()
}
}
@@ -372,10 +372,18 @@ impl ProtoNetwork {
let layout = {
let node = &self.nodes[index].1;
match &node.construction_args {
ConstructionArgs::Value(value) => value.element_write().map(|element| core_types::record::Layout::default().with_writes(0, element, &[])),
ConstructionArgs::Value(value) => value.element_write().map(|element| {
let layout = core_types::record::Layout::default().with_writes(0, element, &[]);
core_types::record::RecordLayout {
frame_bytes: layout.frame_bytes(),
plan: Vec::new(),
layout,
}
}),
ConstructionArgs::Nodes(inputs) => node.resolved.layout_meta.as_ref().and_then(|meta| {
let input_layouts: Vec<Option<&core_types::record::Layout>> = inputs.iter().map(|input| self.nodes[input.0 as usize].1.resolved.layout.as_ref()).collect();
meta.sources.iter().all(|&source| input_layouts[source as usize].is_some()).then(|| meta.fold(&input_layouts))
let input_layouts: Vec<Option<&core_types::record::Layout>> =
inputs.iter().map(|input| self.nodes[input.0 as usize].1.resolved.layout.as_ref().map(|resolved| &resolved.layout)).collect();
meta.sources.iter().all(|&source| input_layouts[source as usize].is_some()).then(|| meta.resolve(&input_layouts))
}),
ConstructionArgs::Inline(_) => None,
}

View File

@@ -476,15 +476,16 @@ impl BorrowTree {
.clone()
.to_edge()
.map_err(|error| vec![GraphError::new(&proto_node, GraphErrorType::ConstructionFailed(error))])?;
debug_assert!(proto_node.resolved_layout().map_or(true, |expected| node.layout() == Some(expected)), "layout pass disagrees with construction for {}", proto_node.identifier.as_str());
self.store_node(node, id, path.into());
}
ConstructionArgs::Inline(_) => unimplemented!("Inline nodes are not supported yet"),
ConstructionArgs::Nodes(ids) => {
let construction_nodes = self.node_deps(ids);
let constructor = typing_context.constructor(id).ok_or_else(|| vec![GraphError::new(&proto_node, GraphErrorType::NoConstructor)])?;
let node = constructor(construction_nodes).map_err(|error| vec![GraphError::new(&proto_node, GraphErrorType::ConstructionFailed(format!("{error:?}")))])?;
debug_assert!(proto_node.resolved_layout().map_or(true, |expected| node.layout() == Some(expected)), "layout pass disagrees with construction for {}", proto_node.identifier.as_str());
let mut node = constructor(construction_nodes).map_err(|error| vec![GraphError::new(&proto_node, GraphErrorType::ConstructionFailed(format!("{error:?}")))])?;
if let Some(layout) = proto_node.resolved_layout() {
node.set_layout(layout.clone());
}
self.store_node(node, id, path.into());
}
};
@@ -503,7 +504,7 @@ impl BorrowTree {
}
fn frame_bytes(&self, id: NodeId) -> usize {
self.nodes.get(&id).and_then(|(handle, _)| handle.layout()).map_or(0, |layout| layout.frame_bytes())
self.nodes.get(&id).map_or(0, |(handle, _)| handle.layout().frame_bytes())
}
}

View File

@@ -191,9 +191,7 @@ mod node_registry_macros {
}
let mut inputs = inputs.into_iter();
let handle = inputs.next().unwrap();
let Some(layout) = handle.layout().cloned() else {
return Err(ConstructionError::MissingLayout);
};
let layout = handle.layout().clone();
let node = graphene_std::ops::IntoNode::<$to, _, $from>::new(handle.downcast_record::<$from>()?, &layout);
Ok(EdgeHandle::new_record::<$to>(std::sync::Arc::new(node) as std::sync::Arc<core_types::registry::ErasedRecordNode>))
},
@@ -265,10 +263,8 @@ mod node_registry_macros {
let mut inputs = inputs.into_iter();
let mut claim = || {
let handle = inputs.next().unwrap();
let Some(layout) = handle.layout().cloned() else {
return Err(ConstructionError::MissingLayout);
};
Ok((handle, layout))
let layout = handle.layout().clone();
Ok::<_, ConstructionError>((handle, layout))
};
let (value, value_layout) = claim()?;
let (converter, converter_layout) = claim()?;
@@ -305,13 +301,9 @@ mod node_registry_macros {
}
let mut inputs = inputs.into_iter();
let value = inputs.next().unwrap();
let Some(value_layout) = value.layout().cloned() else {
return Err(ConstructionError::MissingLayout);
};
let value_layout = value.layout().clone();
let converter = inputs.next().unwrap();
let Some(converter_layout) = converter.layout().cloned() else {
return Err(ConstructionError::MissingLayout);
};
let converter_layout = converter.layout().clone();
let node = graphene_std::ops::ConvertNode::<$to, _, _, $from, $convert>::new(
value.downcast_record::<$from>()?,
converter.downcast_record::<$convert>()?,

View File

@@ -278,6 +278,9 @@ pub trait Node<Input> {
crate::record::empty_layout()
}
/// Installs this node's resolved record layout; a no-op unless it produces records.
fn set_layout(&mut self, _layout: crate::record::RecordLayout) {}
fn eval_batch<'a>(&'a self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<Self::Output>]>) -> BatchStatus<'a, Self::Output>
where
Input: InjectIndex + Copy,

View File

@@ -243,6 +243,14 @@ pub fn empty_layout() -> &'static Layout {
}
/// Declarative record-io metadata for a node type, emitted by the macro into
/// A record node's output layout with the frame size and carrier copy plan derived from it.
#[derive(Clone, Debug, Default)]
pub struct RecordLayout {
pub layout: Layout,
pub frame_bytes: usize,
pub plan: Vec<(usize, usize, usize)>,
}
/// its registry entry so the compiler can fold each wire's layout without
/// running the node's constructor. [`fold`](LayoutMeta::fold) reproduces the
/// layout the constructor derives at wiring today; the compiler layout pass
@@ -287,8 +295,7 @@ pub enum ElementSpec {
}
impl LayoutMeta {
/// The meta of an elementwise carrier flip that retypes input 0's element,
/// preserving its depth and attributes: what an `Into`/`Convert` coercion derives.
/// Keeps input 0's layout but replaces its element.
pub fn retype(element: ElementWrite) -> Self {
Self {
sources: vec![0],
@@ -318,6 +325,23 @@ impl LayoutMeta {
};
base.with_writes(depth, element, &self.writes)
}
/// [`fold`](LayoutMeta::fold) with the frame size and carrier copy plan derived from it.
pub fn resolve(&self, inputs: &[Option<&Layout>]) -> RecordLayout {
let layout = self.fold(inputs);
let frame_bytes = layout.frame_bytes();
let plan = match self.sources.first() {
// A reducer collapses its carrier's levels, so it writes a fresh record rather than copying fields down.
Some(&source) if self.level_delta >= 0 => {
let from = inputs[source as usize].expect("layout resolve source input has no layout");
let carry_element = matches!(self.element, ElementSpec::Carried);
let removes: Vec<(&str, u8)> = self.removes.clone();
copy_plan(from, &layout, carry_element, &removes)
}
_ => Vec::new(),
};
RecordLayout { layout, frame_bytes, plan }
}
}
/// A view of one record: a pointer whose layout is proven at wiring.

View File

@@ -113,7 +113,6 @@ pub fn cache_key<C: CacheHash + ?Sized>(ctx: &C) -> u64 {
pub enum ConstructionError {
Arity { expected: usize, got: usize },
Type { expected: Box<Type>, found: Box<Type> },
MissingLayout,
}
pub struct SharedEdge<N: ?Sized> {
@@ -193,7 +192,8 @@ pub struct EdgeHandle {
node: Box<DynEdge>,
share: fn(&DynEdge) -> Box<DynEdge>,
serialize: fn(&DynEdge) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
layout: fn(&DynEdge) -> Option<&crate::record::Layout>,
layout: fn(&DynEdge) -> &crate::record::Layout,
set_layout: fn(&mut DynEdge, crate::record::RecordLayout),
ty: Type,
}
@@ -228,7 +228,12 @@ impl EdgeHandle {
node: Box::new(SharedEdge::new(node)),
share: |edge| Box::new(edge.downcast_ref::<SharedEdge<N>>().expect("share hook matches the stored edge type").share()),
serialize: |edge| Node::<ContextImpl>::serialize(edge.downcast_ref::<SharedEdge<N>>().expect("serialize hook matches the stored edge type")),
layout: |edge| Some(Node::<ContextImpl>::layout(edge.downcast_ref::<SharedEdge<N>>().expect("layout hook matches the stored edge type"))),
layout: |edge| Node::<ContextImpl>::layout(edge.downcast_ref::<SharedEdge<N>>().expect("layout hook matches the stored edge type")),
set_layout: |edge, layout| {
let shared = edge.downcast_mut::<SharedEdge<N>>().expect("set_layout hook matches the stored edge type");
let node = std::sync::Arc::get_mut(&mut shared.own).expect("layout is installed before the node is shared");
Node::<ContextImpl>::set_layout(node, layout);
},
ty,
}
}
@@ -243,6 +248,7 @@ impl EdgeHandle {
share: self.share,
serialize: self.serialize,
layout: self.layout,
set_layout: self.set_layout,
ty: self.ty.clone(),
}
}
@@ -251,10 +257,14 @@ impl EdgeHandle {
(self.serialize)(&*self.node)
}
pub fn layout(&self) -> Option<&crate::record::Layout> {
pub fn layout(&self) -> &crate::record::Layout {
(self.layout)(&*self.node)
}
pub fn set_layout(&mut self, layout: crate::record::RecordLayout) {
(self.set_layout)(&mut *self.node, layout);
}
pub fn downcast<T: 'static>(self) -> Result<SharedEdge<ErasedNode<T>>, ConstructionError> {
self.downcast_erased(edge_type::<T>())
}

View File

@@ -403,19 +403,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
})
.into_iter()
.flatten();
let flip_prelude = flip
.then(|| match carrier_flip {
true => quote! {
let __layout = __in_0.with_writes(__in_0.depth, gcore::record::element_write::<#slot_value_type>(), &[]);
let __plan = gcore::record::copy_plan(__in_0, &__layout, false, &[]);
let __frame_bytes = __layout.frame_bytes();
},
false => quote! {
let __layout = gcore::record::Layout::default().with_writes(0, gcore::record::element_write::<#slot_value_type>(), &[]);
let __frame_bytes = __layout.frame_bytes();
},
})
.into_iter();
// The output layout, frame size, and copy plan are installed by `set_layout`.
let flip_read_bindings = flip
.then(|| {
lazy_read_fields(&struct_regular_fields).into_iter().map(|(index, field)| {
@@ -441,15 +429,12 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
.flatten();
let flip_output_inits = flip
.then(|| {
let plan = carrier_flip.then(|| quote!(__plan,));
match flip_generic_idents.is_empty() {
true => quote!(__layout, __frame_bytes, #plan),
false => quote!(__layout, __frame_bytes, #plan __marker: ::core::marker::PhantomData,),
}
let plan = carrier_flip.then(|| quote!(__plan: ::std::vec::Vec::new(),));
let marker = (!flip_generic_idents.is_empty()).then(|| quote!(__marker: ::core::marker::PhantomData,));
quote!(__layout: ::core::default::Default::default(), __frame_bytes: 0, #plan #marker)
})
.into_iter();
// The flip prelude's `element_write` instantiates the erased glue at the
// output type, so `new` carries the bounds the glue needs.
// `new` carries the bounds the erased glue needs at the output type.
let new_where = flip
.then(|| {
let existing = parsed.where_clause.iter().flat_map(|clause| clause.predicates.iter());
@@ -463,7 +448,6 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
{
#[allow(clippy::too_many_arguments)]
pub fn new(#(#new_args,)* #(#routing_layout_param)* #(#routing_in_params)* #(#flip_layout_params)*) -> Self {
#(#flip_prelude)*
#(#flip_read_bindings)*
Self {
#(#all_field_inits,)*
@@ -747,8 +731,6 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
crate::codegen::ir::Element::Concrete(ty) => Some(ty),
_ => None,
});
// A creator pushes rank levels: its `IList` return raises the output depth
// above its carrier's, so the layout writes one level deeper.
let subject_depth = node.inputs.iter().find(|input| input.subject).map_or(0, |input| input.shape.depth);
let level_delta = node.output.shape.depth as i8 - subject_depth as i8;
let pushed_levels = level_delta.max(0) as u8;
@@ -1709,14 +1691,60 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
false => Vec::new(),
};
let set_layout_body = if flip {
let plan = carrier_flip.then(|| quote!(self.__plan = __resolved.plan;));
Some(quote! {
self.__frame_bytes = __resolved.frame_bytes;
#plan
self.__layout = __resolved.layout;
})
} else if record_io {
let write_installs = write_markers.iter().enumerate().map(|(index, marker)| {
let slot = format_ident!("__write_{index}");
quote! {
self.#slot = __resolved.layout.offset_of(<#marker as #core_types::attribute::Attribute>::NAME, 0).expect("a written attribute is always part of the wired layout");
}
});
let plan = (!skips_carrier).then(|| quote!(self.__plan = __resolved.plan;));
Some(quote! {
#(#write_installs)*
self.__frame_bytes = __resolved.frame_bytes;
#plan
self.__layout = __resolved.layout;
})
} else {
None
};
let set_layout_method = set_layout_body.map(|body| {
quote! {
fn set_layout(&mut self, __resolved: #core_types::record::RecordLayout) {
#body
}
}
});
let record_layout_impl = match record_io || routing_generic.is_some() || flip || opaque {
true => quote! {
fn layout(&self) -> &#core_types::record::Layout {
&self.__layout
}
#set_layout_method
},
false => quote!(),
};
let flip_meta_concrete = flip && !element_write.is_some_and(|ty| crate::codegen::classify::contains_open_generic(parsed, ty));
let flip_layout_meta_fn = flip_meta_concrete.then(|| {
let layout_meta_fn = format_ident!("{}_layout_meta", fn_name);
let element_spec = match element_write {
Some(ty) => quote!(#core_types::record::ElementSpec::Concrete(#core_types::record::element_write::<#ty>())),
None => quote!(#core_types::record::ElementSpec::Carried),
};
let layout_meta = crate::codegen::ir::layout_meta_tokens(&node, element_spec, core_types);
quote! {
#vis fn #layout_meta_fn() -> #core_types::record::LayoutMeta {
#layout_meta
}
}
});
let entries = entries_tokens(parsed, &struct_name, &data_field_generic_idents, &regular_fields);
let cfg = crate::shader_nodes::modify_cfg(&parsed.attributes);
@@ -1769,13 +1797,6 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let slot = format_ident!("__in_{index}");
quote!(#slot: &#core_types::record::Layout,)
});
let layout_binding = match skips_carrier {
true => quote!(let __layout = self::#layout_fn();),
false => quote!(let __layout = self::#layout_fn(__carrier_layout);),
};
let carry_element = element_write.is_none();
let plan_binding =
(!skips_carrier).then(|| quote!(let __plan = #core_types::record::copy_plan(__carrier_layout, &__layout, #carry_element, &[#(#remove_pairs),*]);));
let read_inits = flat_reads.iter().enumerate().map(|(slot, (owner, read))| {
let marker = &read.marker;
let slot = format_ident!("__read_{slot}");
@@ -1785,14 +1806,6 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
};
quote!(let #slot = #source.offset_of(<#marker as #core_types::attribute::Attribute>::NAME, 0);)
});
let write_inits = 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;
@@ -1803,9 +1816,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let slot = format_ident!("__in_{index}");
quote!(#slot: #slot.clone(),)
});
let plan_init = (!skips_carrier).then(|| quote!(__plan,)).into_iter();
let plan_default = (!skips_carrier).then(|| quote!(__plan: ::std::vec::Vec::new(),)).into_iter();
let read_names = (0..flat_reads.len()).map(|index| format_ident!("__read_{index}")).map(|slot| quote!(#slot,));
let write_names = (0..write_markers.len()).map(|index| format_ident!("__write_{index}")).map(|slot| quote!(#slot,));
let write_defaults = (0..write_markers.len()).map(|index| format_ident!("__write_{index}")).map(|slot| quote!(#slot: 0,));
quote! {
#layout_def
#layout_meta_def
@@ -1814,21 +1827,17 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
impl<#(#data_field_generic_idents,)* #(#node_generics,)*> #mod_name::#struct_name<#(#struct_type_params,)*> {
#[allow(clippy::too_many_arguments)]
#vis fn new(#(#edge_args,)* #(#carrier_layout_param)* #(#input_layout_params)*) -> Self {
#layout_binding
#plan_binding
#(#read_inits)*
#(#write_inits)*
let __frame_bytes = __layout.frame_bytes();
Self {
#(#data_inits)*
#(#edge_inits)*
#(#carrier_init)*
#(#input_layout_inits)*
__layout,
#(#plan_init)*
__frame_bytes,
__layout: ::core::default::Default::default(),
#(#plan_default)*
__frame_bytes: 0,
#(#read_names)*
#(#write_names)*
#(#write_defaults)*
}
}
}
@@ -1968,7 +1977,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
Ok(NodePlan {
kernel,
lazy_read_fns: quote!(#(#lazy_read_fns)*),
record_ctor: quote!(#record_wiring),
record_ctor: quote!(#record_wiring #flip_layout_meta_fn),
node_impl: top_level,
entries,
..Default::default()

View File

@@ -132,9 +132,7 @@ fn flip_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_field
let layout = format_ident!("__layout_{index}");
quote! {
let #handle = inputs.next().unwrap();
let Some(#layout) = #handle.layout().cloned() else {
return Err(gcore::registry::ConstructionError::MissingLayout);
};
let #layout = #handle.layout().clone();
let #name = #handle.downcast_record::<#ty>()?;
}
});
@@ -261,16 +259,12 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields
SlotKind::BaseGeneric(_) | SlotKind::BaseConcrete(_) => 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 #layout = #handle.layout().clone();
let #name = #handle.downcast_erased::<gcore::registry::ErasedRecordNode>(#ty.clone())?;
},
SlotKind::Value(value_ty) => quote! {
let #handle = inputs.next().unwrap();
let Some(#layout) = #handle.layout().cloned() else {
return Err(gcore::registry::ConstructionError::MissingLayout);
};
let #layout = #handle.layout().clone();
let #name = #handle.downcast_record::<#value_ty>()?;
},
SlotKind::Plain(value_ty) | SlotKind::Lazy(value_ty) => quote!(let #name = inputs.next().unwrap().downcast::<#value_ty>()?;),

View File

@@ -232,6 +232,21 @@ mod tests {
stack::reserve(layouts.iter().map(|layout| layout.frame_bytes()).sum::<usize>().max(1 << 12));
}
fn install<N: Node<ContextImpl<'static>>>(mut node: N, meta: core_types::record::LayoutMeta, inputs: &[Option<&Layout>]) -> N {
<N as Node<ContextImpl<'static>>>::set_layout(&mut node, meta.resolve(inputs));
node
}
fn install_flip<N: Node<ContextImpl<'static>>>(mut node: N, layout: &Layout) -> N {
let bundle = core_types::record::RecordLayout {
frame_bytes: layout.frame_bytes(),
plan: Vec::new(),
layout: layout.clone(),
};
<N as Node<ContextImpl<'static>>>::set_layout(&mut node, bundle);
node
}
fn lifted_value<T: Clone + Send + Sync + 'static>(value: T) -> (core_types::record::RecordLift<T, ValueNode<T>>, Layout) {
let lift = core_types::record::RecordLift::<T, _>::new(ValueNode(value));
let layout = Node::<ContextImpl>::layout(&lift).clone();
@@ -268,7 +283,7 @@ mod tests {
let leveled = repeat_opacity_layout(&base);
reserve_for(&[&base, &leveled]);
let node = RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(3u32), &base);
let node = install(RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(3u32), &base), repeat_opacity_layout_meta(), &[Some(&base)]);
assert_eq!(node.layout(), &leveled);
let GPoll::Final(value) = node.eval(&indexed) else {
panic!("expected a final record");
@@ -288,7 +303,7 @@ mod tests {
let base = f64_layout(&[]);
reserve_for(&[&base]);
let node = RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(3u32), &base);
let node = install(RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(3u32), &base), repeat_opacity_layout_meta(), &[Some(&base)]);
// The pushed level (0, the only level) reports the copy count.
assert_eq!(node.extent_at(&ctx, 0), core_types::gpoll::GPoll::Final(core_types::gpoll::Extent::Exactly(3)));
}
@@ -305,8 +320,8 @@ mod tests {
let out = f64_layout(&[]);
reserve_for(&[&base, &leveled, &out]);
let repeat = RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(3u32), &base);
let node = SumNode::new(repeat, &leveled);
let repeat = install(RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(3u32), &base), repeat_opacity_layout_meta(), &[Some(&base)]);
let node = install_flip(SumNode::new(repeat, &leveled), &out);
assert_eq!(node.layout().depth, 0, "the reducer collapsed the rank level");
let GPoll::Final(value) = node.eval(&ctx) else {
@@ -342,7 +357,15 @@ mod tests {
let stacked = multiply_opacity_layout(&modified);
reserve_for(&[&source_layout, &modified, &stacked]);
let chain = MultiplyOpacityNode::new(MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout), ValueNode(0.5), &modified);
let chain = install(
MultiplyOpacityNode::new(
install(MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout), multiply_opacity_layout_meta(), &[Some(&source_layout)]),
ValueNode(0.5),
&modified,
),
multiply_opacity_layout_meta(),
&[Some(&modified)],
);
assert_eq!(chain.layout(), &stacked);
let GPoll::Final(value) = chain.eval(&ctx) else {
panic!("expected a final record");
@@ -363,7 +386,7 @@ mod tests {
let measured = measure_layout(&source_layout);
reserve_for(&[&source_layout, &measured]);
let chain = MeasureNode::new(bare_source(&source_layout, -2.), &source_layout);
let chain = install(MeasureNode::new(bare_source(&source_layout, -2.), &source_layout), measure_layout_meta(), &[Some(&source_layout)]);
let GPoll::Final(value) = chain.eval(&ctx) else {
panic!("expected a final record");
};
@@ -384,7 +407,14 @@ mod tests {
let measured = measure_layout(&modified);
reserve_for(&[&source_layout, &modified, &measured]);
let chain = MeasureNode::new(MultiplyOpacityNode::new(bare_source(&source_layout, -2.), ValueNode(0.5), &source_layout), &modified);
let chain = install(
MeasureNode::new(
install(MultiplyOpacityNode::new(bare_source(&source_layout, -2.), ValueNode(0.5), &source_layout), multiply_opacity_layout_meta(), &[Some(&source_layout)]),
&modified,
),
measure_layout_meta(),
&[Some(&modified)],
);
let GPoll::Final(value) = chain.eval(&ctx) else {
panic!("expected a final record");
};
@@ -405,13 +435,20 @@ mod tests {
let shaded = shade_layout(&modified);
reserve_for(&[&source_layout, &modified, &shaded]);
let bare = ShadeNode::new(bare_source(&source_layout, 4.), &source_layout);
let bare = install(ShadeNode::new(bare_source(&source_layout, 4.), &source_layout), shade_layout_meta(), &[Some(&source_layout)]);
let GPoll::Final(value) = bare.eval(&ctx) else {
panic!("expected a final record");
};
assert_eq!(unsafe { source_layout.rec(&value).element::<f64>() }, 4.);
let chain = ShadeNode::new(MultiplyOpacityNode::new(bare_source(&source_layout, 4.), ValueNode(0.5), &source_layout), &modified);
let chain = install(
ShadeNode::new(
install(MultiplyOpacityNode::new(bare_source(&source_layout, 4.), ValueNode(0.5), &source_layout), multiply_opacity_layout_meta(), &[Some(&source_layout)]),
&modified,
),
shade_layout_meta(),
&[Some(&modified)],
);
let GPoll::Final(value) = chain.eval(&ctx) else {
panic!("expected a final record");
};
@@ -433,7 +470,7 @@ mod tests {
let u32_faded = fade_layout(&u32_source);
reserve_for(&[&f64_source, &f64_faded, &u32_source, &u32_faded]);
let wide = FadeNode::new(bare_source(&f64_source, 8.), ValueNode(0.5), &f64_source);
let wide = install(FadeNode::new(bare_source(&f64_source, 8.), ValueNode(0.5), &f64_source), fade_layout_meta(), &[Some(&f64_source)]);
let GPoll::Final(value) = wide.eval(&ctx) else {
panic!("expected a final record");
};
@@ -441,15 +478,19 @@ mod tests {
assert_eq!(unsafe { rec.element::<f64>() }, 8.);
assert_eq!(unsafe { rec.read::<f64>(f64_faded.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5);
let narrow = FadeNode::new(
RecordSourceNode {
layout: u32_source.clone(),
element: 7u32,
fields: vec![],
partial: false,
},
ValueNode(0.25),
&u32_source,
let narrow = install(
FadeNode::new(
RecordSourceNode {
layout: u32_source.clone(),
element: 7u32,
fields: vec![],
partial: false,
},
ValueNode(0.25),
&u32_source,
),
fade_layout_meta(),
&[Some(&u32_source)],
);
let GPoll::Final(value) = narrow.eval(&ctx) else {
panic!("expected a final record");
@@ -469,7 +510,7 @@ mod tests {
let layout = source_opacity_layout();
reserve_for(&[&layout]);
let node = SourceOpacityNode::new(ValueNode(3.), ValueNode(0.25));
let node = install(SourceOpacityNode::new(ValueNode(3.), ValueNode(0.25)), source_opacity_layout_meta(), &[]);
assert_eq!(Node::<ContextImpl>::layout(&node), &layout);
let GPoll::Final(value) = node.eval(&ctx) else {
panic!("expected a final record");
@@ -490,15 +531,19 @@ mod tests {
let modified = multiply_opacity_layout(&source_layout);
reserve_for(&[&source_layout, &modified]);
let chain = MultiplyOpacityNode::new(
RecordSourceNode {
layout: source_layout.clone(),
element: 1.,
fields: vec![],
partial: true,
},
ValueNode(0.5),
&source_layout,
let chain = install(
MultiplyOpacityNode::new(
RecordSourceNode {
layout: source_layout.clone(),
element: 1.,
fields: vec![],
partial: true,
},
ValueNode(0.5),
&source_layout,
),
multiply_opacity_layout_meta(),
&[Some(&source_layout)],
);
let GPoll::Partial(value) = chain.eval(&ctx) else {
panic!("expected a partial record");
@@ -517,13 +562,13 @@ mod tests {
let modified = checked_multiply_opacity_layout(&source_layout);
reserve_for(&[&source_layout, &modified]);
let ok = CheckedMultiplyOpacityNode::new(bare_source(&source_layout, 1.), ValueNode(0.5), &source_layout);
let ok = install(CheckedMultiplyOpacityNode::new(bare_source(&source_layout, 1.), ValueNode(0.5), &source_layout), checked_multiply_opacity_layout_meta(), &[Some(&source_layout)]);
let GPoll::Final(value) = ok.eval(&ctx) else {
panic!("expected a final record");
};
assert_eq!(unsafe { modified.rec(&value).read::<f64>(modified.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5);
let failing = CheckedMultiplyOpacityNode::new(bare_source(&source_layout, 1.), ValueNode(-1.), &source_layout);
let failing = install(CheckedMultiplyOpacityNode::new(bare_source(&source_layout, 1.), ValueNode(-1.), &source_layout), checked_multiply_opacity_layout_meta(), &[Some(&source_layout)]);
let GPoll::Error(error) = failing.eval(&ctx) else {
panic!("expected an error");
};
@@ -542,7 +587,15 @@ mod tests {
let scaled = scale_layout(&modified);
reserve_for(&[&source_layout, &modified, &scaled]);
let chain = ScaleNode::new(MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout), ValueNode(3.), &modified);
let chain = install(
ScaleNode::new(
install(MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout), multiply_opacity_layout_meta(), &[Some(&source_layout)]),
ValueNode(3.),
&modified,
),
scale_layout_meta(),
&[Some(&modified)],
);
let GPoll::Final(value) = chain.eval(&ctx) else {
panic!("expected a final record");
};
@@ -563,11 +616,15 @@ mod tests {
let transferred = transfer_opacity_layout(&carrier_layout);
reserve_for(&[&carrier_layout, &secondary_layout, &transferred]);
let chain = TransferOpacityNode::new(
f64_record_source(&carrier_layout, 2., vec![(carrier_layout.offset_of("opacity", 0).unwrap(), 0.5)]),
f64_record_source(&secondary_layout, 3., vec![(secondary_layout.offset_of("opacity", 0).unwrap(), 0.25)]),
&carrier_layout,
&secondary_layout,
let chain = install(
TransferOpacityNode::new(
f64_record_source(&carrier_layout, 2., vec![(carrier_layout.offset_of("opacity", 0).unwrap(), 0.5)]),
f64_record_source(&secondary_layout, 3., vec![(secondary_layout.offset_of("opacity", 0).unwrap(), 0.25)]),
&carrier_layout,
&secondary_layout,
),
transfer_opacity_layout_meta(),
&[Some(&carrier_layout), None],
);
let GPoll::Final(value) = chain.eval(&ctx) else {
panic!("expected a final record");
@@ -577,11 +634,15 @@ mod tests {
assert_eq!(unsafe { rec.read::<f64>(transferred.offset_of(Opacity::NAME, 0).unwrap()) }, 0.125);
let bare_secondary = f64_layout(&[]);
let defaulted = TransferOpacityNode::new(
f64_record_source(&carrier_layout, 2., vec![(carrier_layout.offset_of("opacity", 0).unwrap(), 0.5)]),
bare_source(&bare_secondary, 3.),
&carrier_layout,
&bare_secondary,
let defaulted = install(
TransferOpacityNode::new(
f64_record_source(&carrier_layout, 2., vec![(carrier_layout.offset_of("opacity", 0).unwrap(), 0.5)]),
bare_source(&bare_secondary, 3.),
&carrier_layout,
&bare_secondary,
),
transfer_opacity_layout_meta(),
&[Some(&carrier_layout), None],
);
let GPoll::Final(value) = defaulted.eval(&ctx) else {
panic!("expected a final record");
@@ -602,11 +663,15 @@ mod tests {
let factor_layout = Node::<ContextImpl>::layout(&factor).clone();
reserve_for(&[&source_layout]);
let node = BoostNode::new(
f64_record_source(&source_layout, 2., vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25)]),
factor,
&source_layout,
&factor_layout,
let node = install(
BoostNode::new(
f64_record_source(&source_layout, 2., vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25)]),
factor,
&source_layout,
&factor_layout,
),
boost_layout_meta(),
&[Some(&source_layout)],
);
let out_layout = Node::<ContextImpl>::layout(&node).clone();
let opacity_offset = out_layout.offset_of(Opacity::NAME, 0).expect("the primary input's fields pass through to the output");
@@ -629,11 +694,15 @@ mod tests {
let (factor, factor_layout) = lifted_value(3.);
reserve_for(&[&source_layout]);
let node = BoostPollNode::new(
f64_record_source(&source_layout, 2., vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25)]),
factor,
&source_layout,
&factor_layout,
let node = install(
BoostPollNode::new(
f64_record_source(&source_layout, 2., vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25)]),
factor,
&source_layout,
&factor_layout,
),
boost_poll_layout_meta(),
&[Some(&source_layout)],
);
let out_layout = Node::<ContextImpl>::layout(&node).clone();
let opacity_offset = out_layout.offset_of(Opacity::NAME, 0).expect("the primary input's fields pass through the poll kernel");
@@ -657,11 +726,15 @@ mod tests {
assert!(by_layout.frame_bytes() != 0, "the borrow must point into a spilled frame to exercise the park");
reserve_for(&[&carrier_layout, &by_layout]);
let node = OffsetNode::new(
f64_record_source(&carrier_layout, 2., vec![(carrier_layout.offset_of("opacity", 0).unwrap(), 0.25)]),
f64_record_source(&by_layout, 40., vec![]),
&carrier_layout,
&by_layout,
let node = install(
OffsetNode::new(
f64_record_source(&carrier_layout, 2., vec![(carrier_layout.offset_of("opacity", 0).unwrap(), 0.25)]),
f64_record_source(&by_layout, 40., vec![]),
&carrier_layout,
&by_layout,
),
offset_layout_meta(),
&[Some(&carrier_layout)],
);
let out_layout = Node::<ContextImpl>::layout(&node).clone();
let GPoll::Final(value) = node.eval(&ctx) else {
@@ -694,13 +767,17 @@ mod tests {
let (source_id, source_id_layout) = lifted_value(7 as SourceId);
reserve_for(&[&source_layout]);
let node = DoubleAsyncNode::new(
f64_record_source(&source_layout, 3., vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25)]),
runtime,
source_id,
&source_layout,
&runtime_layout,
&source_id_layout,
let node = install(
DoubleAsyncNode::new(
f64_record_source(&source_layout, 3., vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25)]),
runtime,
source_id,
&source_layout,
&runtime_layout,
&source_id_layout,
),
double_async_layout_meta(),
&[Some(&source_layout)],
);
let out_layout = Node::<ContextImpl>::layout(&node).clone();
let opacity_offset = out_layout.offset_of(Opacity::NAME, 0).expect("the carrier's fields pass through the async source");
@@ -774,9 +851,20 @@ mod tests {
let shaded = shade_layout(&stripped);
reserve_for(&[&source_layout, &modified, &stripped, &shaded]);
let chain = ShadeNode::new(
StripOpacityNode::new(MultiplyOpacityNode::new(bare_source(&source_layout, 4.), ValueNode(0.5), &source_layout), &modified),
&stripped,
let chain = install(
ShadeNode::new(
install(
StripOpacityNode::new(
install(MultiplyOpacityNode::new(bare_source(&source_layout, 4.), ValueNode(0.5), &source_layout), multiply_opacity_layout_meta(), &[Some(&source_layout)]),
&modified,
),
strip_opacity_layout_meta(),
&[Some(&modified)],
),
&stripped,
),
shade_layout_meta(),
&[Some(&stripped)],
);
let GPoll::Final(value) = chain.eval(&ctx) else {
panic!("expected a final record");
@@ -796,13 +884,17 @@ mod tests {
assert!(relengthed.offset_of(Opacity::NAME, 0).is_none());
reserve_for(&[&source_layout, &relengthed]);
let chain = RelengthNode::new(
f64_record_source(
let chain = install(
RelengthNode::new(
f64_record_source(
&source_layout,
3.,
vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25), (source_layout.offset_of("length", 0).unwrap(), 9.)],
),
&source_layout,
3.,
vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25), (source_layout.offset_of("length", 0).unwrap(), 9.)],
),
&source_layout,
relength_layout_meta(),
&[Some(&source_layout)],
);
let GPoll::Final(value) = chain.eval(&ctx) else {
panic!("expected a final record");
@@ -824,10 +916,14 @@ mod tests {
let relabeled = label_layout(&labeled);
reserve_for(&[&source_layout, &labeled, &relabeled]);
let chain = LabelNode::new(
LabelNode::new(bare_source(&source_layout, 1.), ValueNode(String::from("a")), &source_layout),
ValueNode(String::from("b")),
&labeled,
let chain = install(
LabelNode::new(
install(LabelNode::new(bare_source(&source_layout, 1.), ValueNode(String::from("a")), &source_layout), label_layout_meta(), &[Some(&source_layout)]),
ValueNode(String::from("b")),
&labeled,
),
label_layout_meta(),
&[Some(&labeled)],
);
let GPoll::Final(value) = chain.eval(&ctx) else {
panic!("expected a final record");
@@ -1187,7 +1283,7 @@ mod tests {
let labeled = label_layout(&source_layout);
reserve_for(&[&labeled, &labeled]);
let chain = LabelNode::new(bare_source(&source_layout, 1.), ValueNode(String::from("a")), &source_layout);
let chain = install(LabelNode::new(bare_source(&source_layout, 1.), ValueNode(String::from("a")), &source_layout), label_layout_meta(), &[Some(&source_layout)]);
let memo = crate::memo::MemoizeNode::new(chain, &labeled);
let first_arena = Arena::new(1024).unwrap();