From 5176bfd3316b47d4689f23a861f9b23be54cfdbf Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Tue, 25 Aug 2026 16:36:49 +0000 Subject: [PATCH] Query per-copy extents with the same chain shape the value path builds --- node-graph/graph-craft/src/proto.rs | 2 +- .../libraries/core-types/src/context.rs | 50 +++++++++++++++---- node-graph/libraries/core-types/src/record.rs | 4 +- node-graph/node-macro/src/codegen.rs | 10 ++-- 4 files changed, 48 insertions(+), 18 deletions(-) diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index eaa8422800..803e6c8fdc 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -561,7 +561,7 @@ impl ProtoNetwork { let branch = self.find_context_dependencies(node); let mut lifted = branch.0.clone(); - lifted.index_levels = lifted.index_levels.popped(pushed_levels.get(input).copied().unwrap_or(0)); + lifted.index_levels = lifted.index_levels.lifted(0, pushed_levels.get(input).copied().unwrap_or(0)); combined_deps |= &lifted; branch_dependencies.push(branch); } diff --git a/node-graph/libraries/core-types/src/context.rs b/node-graph/libraries/core-types/src/context.rs index 2e973793cd..c8bafdbf47 100644 --- a/node-graph/libraries/core-types/src/context.rs +++ b/node-graph/libraries/core-types/src/context.rs @@ -244,19 +244,24 @@ impl IndexLevels { self.0 == u32::MAX } - /// The same requirement seen from outside `levels` pushed levels. An - /// all-levels mask stays saturated, since its reader's level is not known at - /// compile time. - /// - /// TODO: a fold overwrites the level it consumes rather than pushing one, so - /// this shift misnumbers levels above 0 across fold edges; latent until a - /// numeric level above 0 is declared. Preferred fix: folds push a level, - /// once the pass cancels the level a fold supplies. - pub const fn popped(self, levels: u8) -> Self { + /// The same requirement seen from outside an edge whose innermost `supplied` + /// levels the reading node drives itself, and whose chain sits `delta` + /// deeper than that node's. A supplied level leaves no requirement behind; + /// the rest renumber by the depth difference. An all-levels mask stays + /// saturated, since its reader's level is not known at compile time. + pub const fn lifted(self, supplied: u8, delta: u8) -> Self { match self.0 { u32::MAX => self, - _ if levels as u32 >= u32::BITS => Self(0), - mask => Self(mask >> levels), + mask => { + let kept = match supplied as u32 >= u32::BITS { + true => 0, + false => mask & !((1 << supplied) - 1), + }; + match delta as u32 >= u32::BITS { + true => Self(0), + false => Self(kept >> delta), + } + } } } @@ -1518,6 +1523,29 @@ mod context_impl_tests { core::iter::successors(Some(head), |link| link.outer).map(|link| link.index).collect() } + #[test] + fn lifting_a_fold_edge_clears_the_driven_level_in_place() { + let reads = IndexLevels::innermost().with_level(2); + assert_eq!(reads.lifted(1, 0), IndexLevels::empty().with_level(2)); + } + + #[test] + fn lifting_a_pushed_edge_renumbers_outwards() { + let reads = IndexLevels::innermost().with_level(2); + assert_eq!(reads.lifted(1, 1), IndexLevels::empty().with_level(1)); + } + + #[test] + fn lifting_a_decomposed_edge_drops_both_driven_levels() { + let reads = IndexLevels::innermost().with_level(1).with_level(3); + assert_eq!(reads.lifted(2, 1), IndexLevels::empty().with_level(2)); + } + + #[test] + fn lifting_leaves_a_saturated_mask_alone() { + assert_eq!(IndexLevels::all().lifted(2, 1), IndexLevels::all()); + } + #[test] fn nullify_drops_trailing_zeroed_levels() { let arena = Arena::new(256).unwrap(); diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index 9ad488566b..00d7ab8306 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -901,8 +901,8 @@ where B: crate::context::DeriveCtx, N: for<'d> DerivedRecordEdge<'d, crate::context::Derived<'d, B>>, { - let head = ctx.index_head(); - let derived = ctx.promoted(&head, copy); + let mut frame = crate::context::IndexLink { index: 0, outer: None }; + let derived = ctx.push_level(&mut frame, copy, 0); let mut inner: u64 = 1; for level in 0..levels { match node.extent_at_derived(&derived, level) { diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index d7a55b1beb..e3ff4d1bc8 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -1461,8 +1461,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn ParsedFieldType::Node(_) => match ir::lazy_binding(&node, index) { ir::LazyBinding::DeriveRouting | ir::LazyBinding::DeriveCarrier => quote! { let #query = |__copy: u64, __lvl: u8| { - let __head = #core_types::context::DeriveCtx::index_head(__input); - #core_types::record::DerivedRecordEdge::extent_at_derived(&self.#name, &#core_types::context::DeriveCtx::promoted(__input, &__head, __copy), __lvl) + let mut __frame = #core_types::context::IndexLink { index: 0, outer: None }; + let __derived = #core_types::context::DeriveCtx::push_level(__input, &mut __frame, __copy, 0); + #core_types::record::DerivedRecordEdge::extent_at_derived(&self.#name, &__derived, __lvl) }; let #arg = #core_types::extent::ExtentIn::new(&#query); }, @@ -1541,9 +1542,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let query = match &field.ty { ParsedFieldType::Node(_) => match ir::lazy_binding(&node, subject_index) { ir::LazyBinding::DeriveRouting | ir::LazyBinding::DeriveCarrier => quote! { - let __query = |__copy: u64, __lvl: u8| { + let __query = |_: u64, __lvl: u8| { let __head = #core_types::context::DeriveCtx::index_head(__input); - #core_types::record::DerivedRecordEdge::extent_at_derived(&self.#name, &#core_types::context::DeriveCtx::promoted(__input, &__head, __copy), __lvl) + let __derived = #core_types::context::DeriveCtx::replaced(__input, __head.index); + #core_types::record::DerivedRecordEdge::extent_at_derived(&self.#name, &__derived, __lvl) }; }, _ => quote! {