From fb171cd5ac845c1f3f4ef86381753602187c3135 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Wed, 19 Aug 2026 21:43:14 +0000 Subject: [PATCH] Test the partial fold keeping its subject's outer level --- node-graph/nodes/gcore/src/record.rs | 56 +++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index b02f26f969..136cf82438 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -61,12 +61,12 @@ fn repeat_opacity(ctx: impl Ctx + ExtractIndex, element: f64, count: u32) -> ILi } #[node_macro::node(category("Test"))] -fn sum(_: impl Ctx + InjectIndex + Copy, items: IList) -> f64 { +fn sum(_: impl Ctx + ExtractIndex + InjectIndex + Copy, items: IList) -> f64 { items.into_iter().sum() } #[node_macro::node(category("Test"))] -fn sum_nested(_: impl Ctx + InjectIndex + Copy, items: IList>) -> f64 { +fn sum_nested(_: impl Ctx + ExtractIndex + InjectIndex + Copy, items: IList>) -> f64 { items.into_iter().sum() } @@ -1159,6 +1159,58 @@ mod tests { assert_eq!(evals.get(), 1, "an eager value binds once per batch"); } + #[test] + fn partial_fold_keeps_the_outer_level() { + let arena = Arena::new(1 << 16).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let base = f64_layout(&[]); + let leveled_content = repeat_opacity_layout(&base); + let (count_edge, count_layout) = lifted_value(2u32); + let (reverse_edge, reverse_layout) = lifted_value(false); + reserve_for(&[&base, &leveled_content, &count_layout, &reverse_layout]); + + // Element = the outer copy, so each outer row folds to a distinct sum. + let content = install( + RepeatOpacityNode::new(IndexSourceNode { layout: base.clone() }, ValueNode(3u32), &base), + repeat_opacity_layout_meta(), + &[Some(&base)], + ); + let meta = core_types::record::LayoutMeta { + sources: vec![0], + reads: vec![], + element: core_types::record::ElementSpec::Carried, + writes: vec![], + removes: vec![], + level_delta: 1, + folded: None, + }; + let nested = install( + RepeatNode::new(RecordSource::new(content, &leveled_content, &leveled_content), count_edge, reverse_edge, &leveled_content, &count_layout, &reverse_layout), + meta, + &[Some(&leveled_content)], + ); + let two_level = Node::::layout(&nested).clone(); + assert_eq!(two_level.depth, 2); + + let node = install(SumNode::new(nested, &two_level), sum_layout_meta(), &[Some(&two_level)]); + let out = Node::::layout(&node).clone(); + assert_eq!(out.depth, 1, "the fold keeps the subject's outer level"); + assert_eq!(node.extent_at(&ctx, 0), GPoll::Final(Extent::Exactly(2)), "the outer extent shifts down"); + + let head = ctx.index_head(); + for (lane, expected) in [(0u64, 0.), (1, 3.)] { + let mark = stack::sp(); + let GPoll::Final(value) = node.eval(&ctx.promoted(&head, lane)) else { + panic!("expected a final record"); + }; + assert_eq!(unsafe { out.rec(&value).element::() }, expected, "row {lane}"); + unsafe { stack::rewind(mark) }; + } + } + #[test] fn nested_fold_collapses_two_levels() { let arena = Arena::new(1024).unwrap();