From 64adde1ced1a9a802d206b44746b1ef711521de8 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 14 Sep 2026 01:25:04 +0200 Subject: [PATCH] Add a generic map over a graphic's leaves, reaching every depth --- .../libraries/core-types/src/record/run.rs | 21 ++ .../graphic-types/src/graphic/map.rs | 234 ++++++++++++++++++ .../graphic-types/src/graphic/mod.rs | 10 + node-graph/node-macro/src/codegen.rs | 25 +- node-graph/node-macro/src/codegen/entries.rs | 11 +- node-graph/node-macro/src/codegen/ir.rs | 113 ++++++++- 6 files changed, 402 insertions(+), 12 deletions(-) create mode 100644 node-graph/libraries/graphic-types/src/graphic/map.rs diff --git a/node-graph/libraries/core-types/src/record/run.rs b/node-graph/libraries/core-types/src/record/run.rs index 0d04ecf0fe..1d509a10ca 100644 --- a/node-graph/libraries/core-types/src/record/run.rs +++ b/node-graph/libraries/core-types/src/record/run.rs @@ -155,6 +155,27 @@ impl<'e> RunBuilder<'e> { unsafe { (info.write_stored)(value, self.frames.add(lane * self.layout.lane_stride() + offset), self.arena) } } + /// Copies a source lane's fields onto an already pushed lane through `plan`. + /// The columns move as bytes rather than through a read-write round trip, so a + /// parked payload carries as its arena reference and stays valid for the + /// evaluation, exactly as a lane's own carry does. + /// + /// # Safety + /// `src` must be a live record of `plan`'s source layout, `plan` must target + /// this builder's layout, and `src` must not overlap the frames. + pub unsafe fn carry(&mut self, lane: usize, src: crate::record::Rec<'_>, plan: &[(usize, usize, usize)]) { + assert!(lane < self.pushed, "a carry lands on a pushed lane"); + // SAFETY: the lane is below `pushed`, so its frame is within the allocation. + let dst = unsafe { self.frames.add(lane * self.layout.lane_stride()) }; + // SAFETY: the caller's contract. + unsafe { crate::record::apply_plan(src, dst, plan) }; + } + + /// The layout the finished run carries, for computing a carry plan into it. + pub fn layout(&self) -> &Layout { + &self.layout + } + /// The finished run. Panics unless every lane was pushed, since an /// unwritten parked element slot must never become readable. pub fn finish(self) -> GroupItem<'e> { diff --git a/node-graph/libraries/graphic-types/src/graphic/map.rs b/node-graph/libraries/graphic-types/src/graphic/map.rs new file mode 100644 index 0000000000..017981a69a --- /dev/null +++ b/node-graph/libraries/graphic-types/src/graphic/map.rs @@ -0,0 +1,234 @@ +//! The element-wise map over a graphic's leaves: a modifier defined on one leaf +//! type reaches every leaf of that type in the tree, whatever its depth. + +use super::{Graphic, TryFromGraphic}; +use core_types::ATTR_TRANSFORM; +use core_types::arena::Arena; +use core_types::attribute::{Attribute, Transform as TransformAttr}; +use core_types::graphene_hash::CacheHash; + +use core_types::lane::LaneColumn; +use core_types::record::{FieldDesc, FieldWrite, Group, GroupItem, RunBuilder, copy_plan, element_write_hashed}; +use dyn_any::Relift; +use glam::DAffine2; +use vector_types::Vector; + +/// A leaf a graphic can be mapped over: its `Graphic` variant, plus the record +/// glue a rebuilt run needs to store it. +pub trait MappableLeaf: TryFromGraphic + Clone + Send + Sync + CacheHash + PartialEq + dyn_any::StaticTypeSized + 'static +where + Self::Static: Clone + Send + Sync, +{ +} + +impl MappableLeaf for T +where + T: TryFromGraphic + Clone + Send + Sync + CacheHash + PartialEq + dyn_any::StaticTypeSized + 'static, + T::Static: Clone + Send + Sync, +{ +} + +impl<'e> Graphic<'e> { + /// Applies `map` to every `T` leaf reachable from the graphic, handing each leaf + /// the transform of the lane holding it and taking back the one it returns. + /// Groups recurse, since geometry is never inherited, and leaves of other types + /// pass through untouched. Returns this graphic's own transform, which only a + /// mapped leaf changes. `None` reports arena exhaustion while rebuilding a group. + /// + /// A rebuilt group stays resident in `arena`, the form a consumer can read: the + /// owned form refuses lane reads until something replays it, and an ordinary + /// element write parks a value rather than running the re-park glue. + pub fn map(&mut self, arena: &'e Arena, transform: DAffine2, map: &mut impl FnMut(T, DAffine2) -> (T, DAffine2)) -> Option + where + T::Static: Clone + Send + Sync, + { + if let Some(leaf) = T::leaf_mut(self) { + let (mapped, transform) = map(leaf.clone(), transform); + *leaf = mapped; + return Some(transform); + } + + match self { + // The legacy interior is owned outright, so its lanes map in place. + Graphic::Graphic(children) => { + for row in 0..children.len() { + let lane_transform: DAffine2 = children.attribute_cloned_or_default(ATTR_TRANSFORM, row); + let Some(child) = children.element_mut(row) else { continue }; + let mapped = child.map::(arena, lane_transform, map)?; + if mapped != lane_transform { + children.set_attribute(ATTR_TRANSFORM, row, mapped); + } + } + } + // A run is shared and cannot be written through, so a mapped lane rebuilds it. + Graphic::Group(group) => { + let content = map_run::(&group.content, arena, map)?; + *self = Graphic::Group(Group { row: group.row.clone(), content }); + } + _ => {} + } + + Some(transform) + } + + /// [`Graphic::map`] over the vector leaves, the shape the vector modifier nodes use. + pub fn map_vectors(&mut self, arena: &'e Arena, transform: DAffine2, map: &mut impl FnMut(Vector, DAffine2) -> (Vector, DAffine2)) -> Option { + self.map::(arena, transform, map) + } +} + +/// Content a vector modifier runs over: a bare vector maps directly, a graphic maps +/// every vector leaf it reaches. One kernel then serves both of a modifier's rows. +/// The mapped content lands at the arena's lifetime, since a rebuilt group is resident +/// there, so the result is the content's own type re-stated at that lifetime: exactly what +/// [`Relift`] names. Tying it to the arena is what keeps the mapping safe (the content +/// cannot outlive the frames it now points into), and going through `Relift` rather than a +/// bespoke associated type is what lets the node macro know the mapped element erases to +/// the same static type, so a registry row can name it. +pub trait MapVectorContent: Relift + Sized { + /// `None` reports arena exhaustion while rebuilding a group. + fn map_vector_content<'a>(self, arena: &'a Arena, transform: DAffine2, map: &mut impl FnMut(Vector, DAffine2) -> (Vector, DAffine2)) -> Option<(Self::Live<'a>, DAffine2)>; +} + +impl MapVectorContent for Vector { + fn map_vector_content(self, _arena: &Arena, transform: DAffine2, map: &mut impl FnMut(Vector, DAffine2) -> (Vector, DAffine2)) -> Option<(Vector, DAffine2)> { + Some(map(self, transform)) + } +} + +impl MapVectorContent for Graphic<'static> { + fn map_vector_content<'a>(self, arena: &'a Arena, transform: DAffine2, map: &mut impl FnMut(Vector, DAffine2) -> (Vector, DAffine2)) -> Option<(Graphic<'a>, DAffine2)> { + // `Graphic` is covariant in its lifetime, so the node's `'static` spelling narrows + // to the arena's without a cast; the rebuilt run then lands at that same lifetime. + let mut content: Graphic<'a> = self; + let transform = content.map_vectors(arena, transform, map)?; + + Some((content, transform)) + } +} + +/// The run with every reachable `T` leaf mapped: a run of `T` maps its own lanes, +/// a run of graphics recurses, and any other element type is left alone. +fn map_run<'e, T: MappableLeaf>(item: &GroupItem<'e>, arena: &'e Arena, map: &mut impl FnMut(T, DAffine2) -> (T, DAffine2)) -> Option> +where + T::Static: Clone + Send + Sync, +{ + let transforms = core_types::record::RunColumn::::of(item); + let lane_transform = |lane: usize| transforms.try_get(lane).unwrap_or(DAffine2::IDENTITY); + + if let Some(lanes) = item.typed_lanes::() { + let mapped: Vec<(T, DAffine2)> = (0..lanes.len()).map(|lane| map(lanes.element_ref(lane).clone(), lane_transform(lane))).collect(); + return rebuild_run(item, arena, mapped); + } + + if let Some(lanes) = item.typed_lanes::>() { + let mut mapped = Vec::with_capacity(lanes.len()); + for lane in 0..lanes.len() { + let mut child = lanes.element_ref(lane).clone(); + let transform = child.map::(arena, lane_transform(lane), map)?; + mapped.push((child, transform)); + } + return rebuild_run(item, arena, mapped); + } + + Some(item.clone()) +} + +/// A fresh run over `mapped`, carrying the source's columns across and writing each +/// lane's returned transform. Only the elements change, so the columns move as bytes +/// rather than through the census: a parked payload carries as its arena reference +/// and stays valid. `None` reports arena exhaustion. +fn rebuild_run<'e, E>(item: &GroupItem<'e>, arena: &'e Arena, mapped: Vec<(E, DAffine2)>) -> Option> +where + E: Clone + Send + Sync + CacheHash + PartialEq + dyn_any::StaticTypeSized, + E::Static: Clone + Send + Sync, +{ + // A lane whose transform the map changed needs the column even where the source run carried none. + let mut writes: Vec = item.layout().fields.iter().map(FieldDesc::as_write).collect(); + if !writes.iter().any(|write| write.name == TransformAttr::NAME && write.level == 0) { + writes.push(FieldWrite::of::(0)); + } + + let mut builder = RunBuilder::new(arena, element_write_hashed::(), &writes, mapped.len())?; + // The element is written by the push, so the plan carries the columns alone. + let plan = copy_plan(item.layout(), builder.layout(), false, &[]); + + for (lane, (element, transform)) in mapped.into_iter().enumerate() { + builder.push(element)?; + // SAFETY: the plan runs from the source run's own layout into the builder's, and + // the fresh frames cannot overlap the source. + unsafe { builder.carry(lane, item.lanes().get(lane).rec(), &plan) }; + builder.attr::(lane, transform); + } + + Some(builder.finish()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::graphic::test_support::unit_square_at; + use core_types::attribute::Opacity; + use core_types::lane::LaneSource; + use core_types::list::{Item, List}; + use core_types::record::{FieldWrite, RunView}; + use glam::DVec2; + + /// A rebuilt run keeps the columns the map never touched, so a modifier + /// cannot silently drop a lane's blending or layer routing. + #[test] + fn a_mapped_run_keeps_its_untouched_columns() { + let arena = Arena::new(1 << 16).unwrap(); + let mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[FieldWrite::of::(0), FieldWrite::of::(0)], 2).unwrap(); + for lane in 0..2 { + builder.push(unit_square_at(DVec2::ZERO)).unwrap(); + builder.attr::(lane, DAffine2::from_translation(DVec2::new(lane as f64, 0.))); + } + builder.attr::(1, 0.25); + let content = builder.finish(); + + let mut graphic = Graphic::Group(Group { row: None, content }); + let mut seen = Vec::new(); + graphic + .map_vectors(&arena, DAffine2::IDENTITY, &mut |vector, transform| { + seen.push(transform); + (vector, transform * DAffine2::from_scale(DVec2::splat(2.))) + }) + .expect("the rebuild fits the arena"); + + assert_eq!(seen.len(), 2, "every lane of the run is mapped"); + assert_eq!(seen[1], DAffine2::from_translation(DVec2::new(1., 0.)), "each lane is handed its own transform"); + + let Graphic::Group(group) = &graphic else { panic!("the group form survives the map") }; + let view = RunView::::new(&group.content).expect("the run still holds vectors"); + assert_eq!( + view.attr::(1), + DAffine2::from_translation(DVec2::new(1., 0.)) * DAffine2::from_scale(DVec2::splat(2.)), + "the returned transform is written back" + ); + assert_eq!(view.attr::(1), 0.25, "a column the map never touched survives the rebuild"); + } + + /// A leaf type the map does not target is left alone, so a vector modifier + /// cannot disturb raster or color content sharing the tree. + #[test] + fn a_map_skips_the_leaves_of_other_types() { + let arena = Arena::new(1 << 16).unwrap(); + let mut children = List::new(); + children.push(Item::new_from_element(Graphic::Color(core_types::Color::WHITE))); + children.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::ZERO)))); + let mut graphic = Graphic::Graphic(children); + + let mut mapped = 0; + graphic + .map_vectors(&arena, DAffine2::IDENTITY, &mut |vector, transform| { + mapped += 1; + (vector, transform) + }) + .expect("no rebuild is needed"); + + assert_eq!(mapped, 1, "only the vector leaf is mapped"); + let Graphic::Graphic(children) = &graphic else { panic!("the list form survives") }; + assert!(matches!(children.element(0), Some(Graphic::Color(_))), "the color leaf passes through untouched"); + } +} diff --git a/node-graph/libraries/graphic-types/src/graphic/mod.rs b/node-graph/libraries/graphic-types/src/graphic/mod.rs index ffe32b010d..2285191b86 100644 --- a/node-graph/libraries/graphic-types/src/graphic/mod.rs +++ b/node-graph/libraries/graphic-types/src/graphic/mod.rs @@ -1,8 +1,11 @@ mod glue; mod legacy; +mod map; mod paint; mod walk; +pub use map::{MapVectorContent, MappableLeaf}; + pub(crate) use glue::{list_contains_groups, map_attribute_groups_to_owned, map_attribute_groups_to_persistent, map_attribute_groups_to_resident}; pub use glue::{map_groups_to_owned, map_groups_to_persistent, map_groups_to_resident}; pub(crate) use legacy::run_to_legacy_list; @@ -256,6 +259,9 @@ pub trait TryFromGraphic: Clone + Sized { /// The leaf's element, borrowed, where `graphic` is this type's variant. fn leaf_of<'a>(graphic: &'a Graphic<'_>) -> Option<&'a Self>; + + /// The leaf's element, mutably, where `graphic` is this type's variant. + fn leaf_mut<'a>(graphic: &'a mut Graphic<'_>) -> Option<&'a mut Self>; } macro_rules! try_from_graphic { @@ -269,6 +275,10 @@ macro_rules! try_from_graphic { fn leaf_of<'a>(graphic: &'a Graphic<'_>) -> Option<&'a Self> { if let Graphic::$variant(t) = graphic { Some(t) } else { None } } + + fn leaf_mut<'a>(graphic: &'a mut Graphic<'_>) -> Option<&'a mut Self> { + if let Graphic::$variant(t) = graphic { Some(t) } else { None } + } } )* }; diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index f4f471590a..8760a8b26c 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -842,10 +842,12 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn // replacement where there is one and carries the source bytes where there is // not. A gathered element that is still generic keeps the plan's byte carry // and cannot be substituted. - let element_write: Option<&Type> = match &node.output.shape.element { - crate::codegen::ir::Element::Concrete(ty) => Some(ty), - _ => None, - }; + // A monomorphized generic element is written too: each row knows its own element + // type, so the kernel's value lands in the frame rather than the source's bytes + // being carried over it. Only an element with no row to resolve it (an opaque or + // token-carried one) genuinely carries. + let element_write: Option = crate::codegen::ir::writes_element(&node, parsed); + let element_write = element_write.as_ref(); let carrier_read_ty: Option = node.inputs.first().filter(|input| input.subject).and_then(|input| match &input.shape.element { crate::codegen::ir::Element::Concrete(ty) => Some(ty.clone()), crate::codegen::ir::Element::Generic(ident) if carrier_rows => Some(syn::parse_quote!(#ident)), @@ -2512,12 +2514,11 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote!(#ty: ::core::clone::Clone) }); } - // The element store parks droppable elements in the arena. + // The element store parks droppable elements in the arena. A `Live` + // projection is written at whichever lifetime the frame was claimed for, so + // its bound quantifies over that lifetime rather than pinning it to `'static`. if let Some(ty) = element_write { - bounds.push({ - let ty = &crate::codegen::classify::substitute_lifetimes(ty, "'static"); - quote!(#ty: ::core::marker::Send + ::core::marker::Sync + #core_types::StaticTypeSized + 'static) - }); + bounds.push(crate::codegen::classify::flip_output_bound(ty, declared_arena_lifetime.is_some(), core_types)); } } // A routing node's value elements copy out of their records. @@ -2708,6 +2709,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }; // A gather carrier's base is the gathered subject's layout, so its free // layout fn takes that layout even though the subject materializes. + // A generic element cannot be named outside the impl's scope, so the free + // layout fns are emitted only for a concrete one; the registry rows carry the + // generic case, where each row substitutes its own element type. + let element_generic = element_write.is_some_and(|ty| crate::codegen::classify::contains_open_generic(parsed, ty)); let layout_def = match skips_carrier && !gather_carrier { true => quote! { #vis fn #layout_fn() -> #core_types::record::Layout { @@ -2815,6 +2820,8 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }) .collect(); let marker_init = (!carried_type_params.is_empty()).then(|| quote!(__marker: ::core::marker::PhantomData,)).into_iter(); + let layout_def = (!element_generic).then_some(layout_def); + let layout_meta_def = (!element_generic).then_some(layout_meta_def); quote! { #layout_def #layout_meta_def diff --git a/node-graph/node-macro/src/codegen/entries.rs b/node-graph/node-macro/src/codegen/entries.rs index 9b130080b7..ccc4d97bc9 100644 --- a/node-graph/node-macro/src/codegen/entries.rs +++ b/node-graph/node-macro/src/codegen/entries.rs @@ -444,7 +444,10 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields // generic, which only the row resolves, so such a node's // meta is emitted per row instead of shared across them. let named = node.output.shape.attrs.iter().any(|attr| crate::parsing::named_marker(&attr.marker).is_some()); - let meta = match named { + // A generic element is likewise per-row: only the row knows which type + // the element resolves to, so no shared meta can name it. + let generic_element = matches!(&node.output.shape.element, ir::Element::Generic(_)) && ir::writes_element(&node, parsed).is_some(); + let meta = match named || generic_element { false => quote!(Some(self::#layout_meta_fn())), true => { let element_spec = match &node.output.shape.element { @@ -452,6 +455,12 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields let ty = substitute_ident_types(element, assignments); quote!(gcore::record::ElementSpec::Concrete({ use gcore::record::{ElementWritePickHashed as _, ElementWritePickPlain as _}; (&gcore::record::ElementWritePick::<#ty>(::core::marker::PhantomData)).element_write() })) } + // `Relift` promises the `Live` projection erases to the generic's + // own static type, so the row's element is its assignment for it. + ir::Element::Generic(ident) if generic_element => { + let ty = assignments.iter().find(|(generic, _)| generic == ident).map(|(_, ty)| ty.clone()).unwrap_or_else(|| syn::parse_quote!(#ident)); + quote!(gcore::record::ElementSpec::Concrete({ use gcore::record::{ElementWritePickHashed as _, ElementWritePickPlain as _}; (&gcore::record::ElementWritePick::<#ty>(::core::marker::PhantomData)).element_write() })) + } _ => quote!(gcore::record::ElementSpec::Carried), }; let meta = ir::layout_meta_tokens(&node, element_spec, &core_types, assignments); diff --git a/node-graph/node-macro/src/codegen/ir.rs b/node-graph/node-macro/src/codegen/ir.rs index 68ae1b7d5a..d669fe888d 100644 --- a/node-graph/node-macro/src/codegen/ir.rs +++ b/node-graph/node-macro/src/codegen/ir.rs @@ -116,6 +116,38 @@ fn output(parsed: &ParsedNodeFn, generics: &[Ident]) -> Output { } } +/// The element type the row writes into its frame, or `None` where the element is +/// carried from the source's bytes instead. +/// +/// A `Live` projection is the case worth naming: the kernel produced a fresh element at +/// the serving lifetime, so the row must WRITE it. Classifying such an output as a +/// carried generic makes the tail copy the input over the kernel's result, turning the +/// node into a silent no-op, which is exactly the regression this rule exists to prevent. +/// Any other generic element is the lane's own and genuinely carries. +pub(crate) fn writes_element(node: &Node, parsed: &ParsedNodeFn) -> Option { + let declared = written_element_type(parsed); + let written = match &node.output.shape.element { + Element::Concrete(_) => true, + Element::Generic(_) => { + let generics: Vec = node.generics.iter().map(|generic| generic.ident.clone()).collect(); + !node.monomorphizations.is_empty() && declared.as_ref().is_some_and(|ty| relifted_generic(ty, &generics).is_some()) + } + Element::Opaque => false, + }; + + written.then_some(declared).flatten() +} + +/// The element type the output row declares, before classification: the row itself, or +/// what remains once the attribute writes and any `Lane` gather wrapper are stripped. +/// This is the spelling the serve body writes the element at, `Live` projection and all. +pub(crate) fn written_element_type(parsed: &ParsedNodeFn) -> Option { + let row = slot_value_type(&parsed.output_type); + let element = record_writes(&row).map_or(row, |writes| writes.element); + + Some(lane_inner(&element).unwrap_or(element)) +} + fn monomorphizations(parsed: &ParsedNodeFn, fields: &[&ParsedField], generics: &[Ident]) -> Vec { if generics.is_empty() { return Vec::new(); @@ -183,10 +215,30 @@ fn element_of(ty: &Type, generics: &[Ident]) -> Element { } match bare_ident(ty) { Some(ident) if generics.contains(ident) => Element::Generic(ident.clone()), - _ => Element::Concrete(ty.clone()), + _ => match relifted_generic(ty, generics) { + Some(ident) => Element::Generic(ident), + None => Element::Concrete(ty.clone()), + }, } } +/// The generic a `Relift::Live` projection is taken off, as in `V::Live<'a>`. +/// +/// Only `Live` counts, and that is the whole point: `Relift`'s safety contract says +/// `Live<'a>` is the type itself with its lifetimes re-stated, so it erases to the same +/// static type. That is what lets a registry row name the element as the generic's own +/// monomorphization while the serve body types it at the serving lifetime. An arbitrary +/// associated type carries no such promise, so it stays a concrete element. +pub(crate) fn relifted_generic(ty: &Type, generics: &[Ident]) -> Option { + let Type::Path(path) = ty else { return None }; + if path.qself.is_some() || path.path.segments.len() != 2 || path.path.segments.last()?.ident != "Live" { + return None; + } + let head = &path.path.segments.first()?.ident; + + generics.contains(head).then(|| head.clone()) +} + pub(crate) fn strip_ilist(ty: &Type) -> (Type, u8) { let mut element = ty.clone(); let mut depth = 0; @@ -239,7 +291,7 @@ fn gathered_element(parsed: &ParsedNodeFn) -> Option { } /// The element type inside a `Lane` position, lifetime argument skipped. -fn lane_inner(ty: &Type) -> Option { +pub(crate) fn lane_inner(ty: &Type) -> Option { let Type::Path(path) = ty else { return None }; let segment = path.path.segments.last()?; if segment.ident != "Lane" { @@ -1471,6 +1523,63 @@ mod tests { assert!(opaque_swallows_columns(&node), "so the shape is refused rather than lowered"); } + #[test] + fn a_live_projection_is_written_not_carried() { + // A modifier returning its content re-stated at the arena's lifetime. The kernel + // computed a new element, so the row must write it: classifying this as a carried + // generic makes the tail copy the input over the result and the node silently + // returns its input unchanged. + let node = node_of(quote!( + fn resample<'e, V: MapVectorContent + Clone + Send + Sync + CacheHash + 'static>( + ctx: impl Ctx + ExtractArena<'e>, + #[implementations(Graphic, Vector)] (content, transform): (V, Attr), + ) -> Result<(V::Live<'e>, Attr), Interrupt> { + todo!() + } + )); + let mut parsed = parse_node_fn( + quote!(category("")), + quote!( + fn resample<'e, V: MapVectorContent + Clone + Send + Sync + CacheHash + 'static>( + ctx: impl Ctx + ExtractArena<'e>, + #[implementations(Graphic, Vector)] (content, transform): (V, Attr), + ) -> Result<(V::Live<'e>, Attr), Interrupt> { + todo!() + } + ), + ) + .unwrap(); + parsed.replace_impl_trait_in_input(); + + assert!(matches!(node.output.shape.element, Element::Generic(_)), "the projection rides the generic"); + assert!(!node.monomorphizations.is_empty(), "the implementations give each row a concrete element"); + let written = writes_element(&node, &parsed).expect("a `Live` projection is written, not carried"); + assert_eq!(quote!(#written).to_string(), "V :: Live < 'e >", "written at the declared projection"); + } + + #[test] + fn a_gathered_generic_element_still_carries() { + // A lane rearranger hands back the lane it was given, so its element is the + // source's own bytes and the plan carries it. + let node = node_of(quote!( + fn reorder(_: impl Ctx, #[implementations(f64, Vector)] list: IList) -> IList> { + todo!() + } + )); + let mut parsed = parse_node_fn( + quote!(category("")), + quote!( + fn reorder(_: impl Ctx, #[implementations(f64, Vector)] list: IList) -> IList> { + todo!() + } + ), + ) + .unwrap(); + parsed.replace_impl_trait_in_input(); + + assert!(writes_element(&node, &parsed).is_none(), "a gathered generic element carries rather than being written"); + } + #[test] fn the_deepest_layout_source_is_the_delta_base() { let mut parsed = parse_node_fn(