mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 23:08:05 +08:00
Add a generic map over a graphic's leaves, reaching every depth
This commit is contained in:
@@ -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> {
|
||||
|
||||
234
node-graph/libraries/graphic-types/src/graphic/map.rs
Normal file
234
node-graph/libraries/graphic-types/src/graphic/map.rs
Normal file
@@ -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<T> 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<T: MappableLeaf>(&mut self, arena: &'e Arena, transform: DAffine2, map: &mut impl FnMut(T, DAffine2) -> (T, DAffine2)) -> Option<DAffine2>
|
||||
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::<T>(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::<T>(&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<DAffine2> {
|
||||
self.map::<Vector>(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<GroupItem<'e>>
|
||||
where
|
||||
T::Static: Clone + Send + Sync,
|
||||
{
|
||||
let transforms = core_types::record::RunColumn::<TransformAttr>::of(item);
|
||||
let lane_transform = |lane: usize| transforms.try_get(lane).unwrap_or(DAffine2::IDENTITY);
|
||||
|
||||
if let Some(lanes) = item.typed_lanes::<T>() {
|
||||
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::<Graphic<'e>>() {
|
||||
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::<T>(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<GroupItem<'e>>
|
||||
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<FieldWrite> = 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::<TransformAttr>(0));
|
||||
}
|
||||
|
||||
let mut builder = RunBuilder::new(arena, element_write_hashed::<E>(), &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::<TransformAttr>(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::<Vector>(), &[FieldWrite::of::<TransformAttr>(0), FieldWrite::of::<Opacity>(0)], 2).unwrap();
|
||||
for lane in 0..2 {
|
||||
builder.push(unit_square_at(DVec2::ZERO)).unwrap();
|
||||
builder.attr::<TransformAttr>(lane, DAffine2::from_translation(DVec2::new(lane as f64, 0.)));
|
||||
}
|
||||
builder.attr::<Opacity>(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::<Vector>::new(&group.content).expect("the run still holds vectors");
|
||||
assert_eq!(
|
||||
view.attr::<TransformAttr>(1),
|
||||
DAffine2::from_translation(DVec2::new(1., 0.)) * DAffine2::from_scale(DVec2::splat(2.)),
|
||||
"the returned transform is written back"
|
||||
);
|
||||
assert_eq!(view.attr::<Opacity>(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");
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user