From 04afada5b3afef6007a72ddcb84ef4d41428051d Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Fri, 14 Aug 2026 08:22:29 +0000 Subject: [PATCH] Lower a creator's IList return to a level-pushing record --- node-graph/node-macro/src/codegen.rs | 39 ++++++++++++++++++++++--- node-graph/node-macro/src/codegen/ir.rs | 2 +- node-graph/nodes/gcore/src/record.rs | 18 +++++++++++- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index db5c8fca90..dac91119b9 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -681,6 +681,31 @@ pub(crate) struct NodeFields<'a> { pub(crate) struct_type_params: Vec, } +/// Rewrites a creator kernel's `emit(a, b, ..)` tail into the row tuple `(a, b, ..)`. +/// `emit`'s parentheses double as the tuple constructor, so it is pure sugar. +fn rewrite_emit(body: &TokenStream2) -> TokenStream2 { + let Ok(mut block) = syn::parse2::(body.clone()) else { + return body.clone(); + }; + struct EmitToTuple; + impl VisitMut for EmitToTuple { + fn visit_expr_mut(&mut self, expr: &mut syn::Expr) { + if let syn::Expr::Call(call) = expr + && matches!(&*call.func, syn::Expr::Path(path) if path.path.is_ident("emit")) + { + *expr = syn::Expr::Tuple(syn::ExprTuple { + attrs: Vec::new(), + paren_token: Default::default(), + elems: call.args.clone(), + }); + } + syn::visit_mut::visit_expr_mut(self, expr); + } + } + EmitToTuple.visit_block_mut(&mut block); + block.to_token_stream() +} + pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn, model: &Option, fields: NodeFields) -> syn::Result { let core_types = crate_ident.gcore()?; @@ -722,6 +747,12 @@ 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; + let output_row = crate::codegen::ir::strip_ilist(&slot_value_type(&parsed.output_type)).0; 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 { @@ -1241,13 +1272,13 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let ctx_pat = &parsed.input.pat_ident; let fn_where = &parsed.where_clause; - let body = &parsed.body; + let body = if level_delta > 0 { rewrite_emit(&parsed.body) } else { parsed.body.clone() }; let vis = &parsed.vis; let kernel_fields: Vec<&&ParsedField> = regular_fields.iter().filter(|field| !injected_name(&field.pat_ident.ident)).collect(); // A bare `Attr` in the return type cannot elide its lifetime, so the // kernel gets a fresh one; reference-valued writes name their real // lifetime explicitly and pass through untouched. - let kernel_output = record_io.then(|| inject_attr_lifetimes(&parsed.output_type)).flatten(); + let kernel_output = record_io.then(|| inject_attr_lifetimes(&crate::codegen::ir::strip_ilist(&parsed.output_type).0)).flatten(); let attr_lifetime = kernel_output.is_some().then(|| quote!('__attr,)); let kernel_output = match derive_routing { true => { @@ -1461,7 +1492,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn // next write binder, a `RemoveAttr` binds nothing. let slot_binders: Vec = { let mut binders = attr_binders.iter(); - match slot_value_type(&parsed.output_type) { + match output_row.clone() { Type::Tuple(tuple) => tuple .elems .iter() @@ -1701,7 +1732,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }, false => quote! { #vis fn #layout_fn(__carrier: &#core_types::record::Layout) -> #core_types::record::Layout { - __carrier #subtraction.with_writes(__carrier.depth, #element, &[#(#write_descs),*]) + __carrier #subtraction.with_writes(__carrier.depth + #pushed_levels, #element, &[#(#write_descs),*]) } }, }; diff --git a/node-graph/node-macro/src/codegen/ir.rs b/node-graph/node-macro/src/codegen/ir.rs index 01aaa0767b..918c06c363 100644 --- a/node-graph/node-macro/src/codegen/ir.rs +++ b/node-graph/node-macro/src/codegen/ir.rs @@ -154,7 +154,7 @@ fn element_of(ty: &Type, generics: &[Ident]) -> Element { } } -fn strip_ilist(ty: &Type) -> (Type, u8) { +pub(crate) fn strip_ilist(ty: &Type) -> (Type, u8) { let mut element = ty.clone(); let mut depth = 0; while let Some(inner) = ilist_inner(&element) { diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index d20a278696..76de71931f 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -6,7 +6,7 @@ //! wiring is by hand until the compiler pass constructs layouts. use core_types::attribute::{Attr, Opacity, RemoveAttr}; -use core_types::context::ExtractArena; +use core_types::context::{ExtractArena, ExtractIndex}; use core_types::gpoll::{ErrorKind, GraphError, Interrupt}; use core_types::{Context, Ctx}; @@ -50,6 +50,14 @@ fn fade(_: impl Ctx, (element, opacity): (T, Attr), factor: f64) -> (element, Attr(*opacity * factor)) } +/// Test-only structure creator: the `IList` return pushes one rank level and +/// writes a per-copy opacity indexed by the copy's own index. +#[node_macro::node(category("Test"))] +fn repeat_opacity(ctx: impl Ctx + ExtractIndex, element: f64, count: u32) -> IList<(f64, Attr)> { + let _ = count; + emit(element, Attr(ctx.innermost_index() as f64)) +} + #[node_macro::node(category("Test"))] fn source_opacity(_: impl Ctx, _: (), element: f64, opacity: f64) -> (f64, Attr) { (element, Attr(opacity)) @@ -221,6 +229,14 @@ mod tests { } } + #[test] + fn creator_pushes_a_rank_level() { + let base = f64_layout(&[]); + let leveled = repeat_opacity_layout(&base); + assert_eq!(leveled.depth, 1, "the IList return pushed one rank level above the depth-0 carrier"); + assert_eq!(repeat_opacity_layout_meta().fold(&[Some(&base)]), leveled, "the level_delta metadata folds to the same leveled layout"); + } + #[test] fn layout_meta_folds_to_construction() { let base = f64_layout(&[]);