Add a generic map over a graphic's leaves, reaching every depth

This commit is contained in:
Dennis Kobert
2026-09-14 01:25:04 +02:00
parent d7f35259c9
commit 64adde1ced
6 changed files with 402 additions and 12 deletions

View File

@@ -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<Type> = crate::codegen::ir::writes_element(&node, parsed);
let element_write = element_write.as_ref();
let carrier_read_ty: Option<Type> = 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

View File

@@ -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);

View File

@@ -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<Type> {
let declared = written_element_type(parsed);
let written = match &node.output.shape.element {
Element::Concrete(_) => true,
Element::Generic(_) => {
let generics: Vec<Ident> = 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<Type> {
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<ImplRow> {
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<Ident> {
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<Type> {
}
/// The element type inside a `Lane<T>` position, lifetime argument skipped.
fn lane_inner(ty: &Type) -> Option<Type> {
pub(crate) fn lane_inner(ty: &Type) -> Option<Type> {
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<TransformAttr>),
) -> Result<(V::Live<'e>, Attr<TransformAttr>), 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<TransformAttr>),
) -> Result<(V::Live<'e>, Attr<TransformAttr>), 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<T: Clone + Send + Sync + CacheHash + 'static>(_: impl Ctx, #[implementations(f64, Vector)] list: IList<T>) -> IList<Lane<T>> {
todo!()
}
));
let mut parsed = parse_node_fn(
quote!(category("")),
quote!(
fn reorder<T: Clone + Send + Sync + CacheHash + 'static>(_: impl Ctx, #[implementations(f64, Vector)] list: IList<T>) -> IList<Lane<T>> {
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(