From 0a4e03beb92823d73823aaab74e0b1d5cd396b25 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 14 Sep 2026 15:51:28 +0200 Subject: [PATCH] Quantify a flip node's output bound over the lifetime it is served at --- node-graph/node-macro/src/codegen.rs | 4 +- node-graph/node-macro/src/codegen/classify.rs | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 3f2d420f1f..84fb354ac7 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -2539,8 +2539,8 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } }) .collect(); - let out = crate::codegen::classify::substitute_lifetimes(&slot_value_type(&parsed.output_type), "'static"); - bounds.push(quote!(#out: ::core::marker::Send + ::core::marker::Sync + #core_types::StaticTypeSized + 'static)); + let output_row = slot_value_type(&parsed.output_type); + bounds.push(crate::codegen::classify::flip_output_bound(&output_row, declared_arena_lifetime.is_some(), core_types)); bounds } false => Vec::new(), diff --git a/node-graph/node-macro/src/codegen/classify.rs b/node-graph/node-macro/src/codegen/classify.rs index 835ebc06e9..844e4f7f45 100644 --- a/node-graph/node-macro/src/codegen/classify.rs +++ b/node-graph/node-macro/src/codegen/classify.rs @@ -790,6 +790,8 @@ pub(crate) fn substitute_lifetimes(ty: &Type, replacement: &str) -> Type { let replacement = match replacement { "'static" => "'static", + // The bound lifetime of a higher-ranked where-clause, which `'_` cannot spell. + "'__any" => "'__any", _ => "'_", }; let mut ty = ty.clone(); @@ -841,6 +843,24 @@ pub(crate) fn desugar_extract_lifetime(bound: &TypeParamBound, core_types: &Toke quote!(#core_types::context::ExtractArena) } +/// The bound a flip node's output row must satisfy to be lifted into a frame. +/// An output naming the kernel's arena lifetime is served at whichever lifetime +/// the frame was claimed for, so the bound quantifies over that lifetime instead +/// of pinning it to `'static`, which would demand the serving borrow outlive the +/// program and rules out the arena residency the output was written for. +pub(crate) fn flip_output_bound(output_row: &Type, kernel_declares_arena: bool, core_types: &TokenStream2) -> TokenStream2 { + match kernel_declares_arena && named_serving_lifetime(output_row).is_some() { + true => { + let out = substitute_lifetimes(output_row, "'__any"); + quote!(for<'__any> #out: ::core::marker::Send + ::core::marker::Sync + #core_types::StaticTypeSized) + } + false => { + let out = substitute_lifetimes(output_row, "'static"); + quote!(#out: ::core::marker::Send + ::core::marker::Sync + #core_types::StaticTypeSized + 'static) + } + } +} + #[cfg(test)] mod lifetime_subst_tests { use super::*; @@ -851,4 +871,28 @@ mod lifetime_subst_tests { let erased = substitute_lifetimes(&ty, "'static"); assert_eq!(quote::quote!(#erased).to_string(), "Graphic < 'static >"); } + + #[test] + fn an_arena_served_flip_output_is_bounded_for_every_lifetime() { + let output: Type = syn::parse_quote!(::Live<'e>); + let bound = flip_output_bound(&output, true, "e!(gcore)).to_string(); + assert!(bound.starts_with("for < '__any >"), "the serving lifetime is quantified, got {bound}"); + assert!(!bound.contains("'static"), "no part of the bound pins the serving lifetime, got {bound}"); + } + + #[test] + fn a_lifetime_free_flip_output_keeps_the_static_bound() { + let output: Type = syn::parse_quote!(Vector); + let bound = flip_output_bound(&output, true, "e!(gcore)).to_string(); + assert!(!bound.contains("for <"), "an output that names no lifetime needs no quantifier, got {bound}"); + assert!(bound.contains("'static"), "it keeps the plain bound, got {bound}"); + } + + #[test] + fn a_kernel_without_an_arena_keeps_the_static_bound() { + // The lifetime is some other borrow, which the serving claim does not supply. + let output: Type = syn::parse_quote!(Cow<'a, str>); + let bound = flip_output_bound(&output, false, "e!(gcore)).to_string(); + assert!(bound.starts_with("Cow < 'static , str >"), "the lifetime erases as before, got {bound}"); + } }