New nodes: Sum, Average, Minimum, Maximum, Any, All (#4344)

Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
Keavon Chambers
2026-07-15 23:41:46 -07:00
committed by Dennis Kobert
parent 04d6c0d5cf
commit 20eb5ccb8a
20 changed files with 1174 additions and 664 deletions

View File

@@ -33,4 +33,4 @@ pub use promote::{Promotion, assert_promoted, register_element_promote, register
pub use route::{RecordSource, SourcePlan};
pub use run::{Group, GroupItem, RunBuilder, RunColumn, RunView, run_to_owned_list};
pub use serve::{FrameClaim, MaterializedSpan, Served, SlotRun, serve_input};
pub use testkit::{LiftedSource, ServedRecord, capture, test_frames};
pub use testkit::{LiftedSource, ServedRecord, capture, fixtures as test_fixtures, test_frames};

View File

@@ -180,3 +180,330 @@ mod tests {
assert_eq!(served.attr::<Transform>(), DAffine2::from_translation(DVec2::new(3., 4.)));
}
}
/// The node-test fixtures: hand-wired record sources and layout installers,
/// so a node crate's tests drive a generated node without the compiler pass.
pub mod fixtures {
use super::super::frames::Frames;
use super::super::layout::{FieldWrite, Layout, LayoutMeta, RecordLayout, element_write};
use super::super::serve::{FrameClaim, Served};
use super::test_frames;
use crate::SourceId;
use crate::arena::Arena;
use crate::attribute::{Attribute, Opacity, Transform};
use crate::context::{ContextImpl, EvalScope, ExtractArena, ExtractIndex, ExtractIndices};
use crate::gpoll::{Extent, GPoll};
use crate::node::Node;
use crate::value::ValueSource;
use glam::DAffine2;
/// A one-record source with fixed `f64` fields, optionally served partial.
pub struct RecordSourceNode<E> {
pub layout: Layout,
pub element: E,
pub fields: Vec<(&'static str, f64)>,
pub partial: bool,
}
impl<C, E: Copy + Send + Sync + dyn_any::StaticTypeSized + 'static> Node<C> for RecordSourceNode<E> {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
let mut frame = slot;
let arena = ExtractArena::arena(input);
if frame.element(self.element, arena).is_none() {
return GPoll::arena_exhausted();
}
for (name, field) in &self.fields {
write_field_at(&mut frame, &self.layout, name, 0, *field);
}
// SAFETY: the writes above complete the record of this layout.
let served = unsafe { frame.finish_served() };
match self.partial {
true => GPoll::Partial(served),
false => GPoll::Final(served),
}
}
fn layout(&self) -> &Layout {
&self.layout
}
}
/// A level of `f64` lanes, each optionally carrying one fixed field.
pub struct LeveledSourceNode {
pub layout: Layout,
pub elements: Vec<f64>,
pub field: Option<(&'static str, f64)>,
}
impl<C: ExtractIndex> Node<C> for LeveledSourceNode {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
let element = self.elements[input.innermost_index() as usize % self.elements.len()];
let mut frame = slot;
let arena = ExtractArena::arena(input);
if frame.element(element, arena).is_none() {
return GPoll::arena_exhausted();
}
if let Some((name, value)) = self.field {
write_field_at(&mut frame, &self.layout, name, 0, value);
}
// SAFETY: the writes above complete the record of this layout.
GPoll::Final(unsafe { frame.finish_served() })
}
fn extent_at<'x>(&self, _input: &C, _level: u8, _frames: &Frames<'x>) -> GPoll<Extent>
where
C: ExtractArena<ArenaRef = &'x Arena>,
{
GPoll::Final(Extent::Exactly(self.elements.len()))
}
fn layout(&self) -> &Layout {
&self.layout
}
}
/// A level of `f64` lanes each carrying a transform.
pub struct LeveledTransformSource {
pub layout: Layout,
pub rows: Vec<(f64, DAffine2)>,
}
impl<C: ExtractIndex> Node<C> for LeveledTransformSource {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
let (element, transform) = self.rows[input.innermost_index() as usize % self.rows.len()];
let mut frame = slot;
let arena = ExtractArena::arena(input);
if frame.element(element, arena).is_none() {
return GPoll::arena_exhausted();
}
write_attr_at::<Transform>(&mut frame, &self.layout, transform);
// SAFETY: the writes above complete the record of this layout.
GPoll::Final(unsafe { frame.finish_served() })
}
fn extent_at<'x>(&self, _input: &C, _level: u8, _frames: &Frames<'x>) -> GPoll<Extent>
where
C: ExtractArena<ArenaRef = &'x Arena>,
{
GPoll::Final(Extent::Exactly(self.rows.len()))
}
fn layout(&self) -> &Layout {
&self.layout
}
}
/// Serves lanes carrying both a Transform no gather kernel declares and an
/// Opacity one does, so a carried column can be told apart from a written one.
pub struct LeveledCarriedSource {
pub layout: Layout,
pub rows: Vec<(f64, DAffine2, f64)>,
}
impl<C: ExtractIndex> Node<C> for LeveledCarriedSource {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
let (element, transform, opacity) = self.rows[input.innermost_index() as usize % self.rows.len()];
let mut frame = slot;
let arena = ExtractArena::arena(input);
if frame.element(element, arena).is_none() {
return GPoll::arena_exhausted();
}
write_attr_at::<Transform>(&mut frame, &self.layout, transform);
write_attr_at::<Opacity>(&mut frame, &self.layout, opacity);
// SAFETY: the writes above complete the record of this layout.
GPoll::Final(unsafe { frame.finish_served() })
}
fn extent_at<'x>(&self, _input: &C, _level: u8, _frames: &Frames<'x>) -> GPoll<Extent>
where
C: ExtractArena<ArenaRef = &'x Arena>,
{
GPoll::Final(Extent::Exactly(self.rows.len()))
}
fn layout(&self) -> &Layout {
&self.layout
}
}
/// A leveled source that keeps its count to itself: the extent is a lower
/// bound and lanes past the data answer the past-end signal.
pub struct DrainSourceNode {
pub layout: Layout,
pub count: usize,
}
impl<C: ExtractIndex> Node<C> for DrainSourceNode {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
let lane = input.innermost_index();
if lane >= self.count as u64 {
return GPoll::past_end();
}
let mut frame = slot;
let arena = ExtractArena::arena(input);
if frame.element(lane as f64, arena).is_none() {
return GPoll::arena_exhausted();
}
// SAFETY: the writes above complete the record of this layout.
GPoll::Final(unsafe { frame.finish_served() })
}
fn extent_at<'x>(&self, _input: &C, _level: u8, _frames: &Frames<'x>) -> GPoll<Extent>
where
C: ExtractArena<ArenaRef = &'x Arena>,
{
GPoll::Final(Extent::AtLeast(0))
}
fn layout(&self) -> &Layout {
&self.layout
}
}
/// Depth-0 content varying per copy: serves the enclosing (pushed) level's
/// index, which sits one link above the content's own innermost lane.
pub struct IndexSourceNode {
pub layout: Layout,
}
impl<C: ExtractIndex + ExtractIndices> Node<C> for IndexSourceNode {
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
let element = input.try_index().and_then(|mut indices| indices.nth(1)).unwrap_or(0) as f64;
let mut frame = slot;
let arena = ExtractArena::arena(input);
if frame.element(element, arena).is_none() {
return GPoll::arena_exhausted();
}
// SAFETY: the writes above complete the record of this layout.
GPoll::Final(unsafe { frame.finish_served() })
}
fn layout(&self) -> &Layout {
&self.layout
}
}
/// Writes a field at the layout's resolved offset, the wiring-proven pairing
/// a generated node performs.
pub fn write_field_at<T: Copy + 'static>(frame: &mut FrameClaim<'_, '_>, layout: &Layout, name: &str, level: u8, value: T) {
let field = layout
.fields
.iter()
.find(|field| field.name == name && field.level == level)
.expect("the layout carries the written field");
assert_eq!(field.type_id, std::any::TypeId::of::<T>(), "the field was declared at this value type");
// SAFETY: the offset is this layout's own, at the field's declared type.
unsafe { frame.attr_at(field.offset, value) };
}
/// [`write_field_at`] for a census marker at level 0.
pub fn write_attr_at<A: Attribute>(frame: &mut FrameClaim<'_, '_>, layout: &Layout, value: A::Value<'static>)
where
A::Value<'static>: Copy + 'static,
{
write_field_at(frame, layout, A::NAME, 0, value);
}
pub fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
EvalScope::new(Some(0.5), None, None, generations, arena)
}
fn f64_fields(names: &[&'static str]) -> Vec<FieldWrite> {
names
.iter()
.map(|name| FieldWrite {
name,
level: 0,
size: 8,
align: 8,
type_id: std::any::TypeId::of::<f64>(),
read_erased: <Opacity as Attribute>::read_erased,
repark: None,
content_hash: None,
content_eq: None,
})
.collect()
}
/// A depth-0 `f64` record layout carrying the named `f64` fields.
pub fn f64_layout(names: &[&'static str]) -> Layout {
Layout::default().with_writes(0, element_write::<f64>(), &f64_fields(names))
}
/// A one-level `f64` layout carrying the named `f64` fields.
pub fn leveled_f64_layout(names: &[&'static str]) -> Layout {
Layout::default().with_writes(1, element_write::<f64>(), &f64_fields(names))
}
/// Frame space sized for the layouts, with a floor for small fixtures.
pub fn frames_for(layouts: &[&Layout]) -> Frames<'static> {
test_frames(layouts.iter().map(|layout| layout.frame_bytes()).sum::<usize>().max(1 << 12))
}
/// Installs the layout the compiler pass would resolve for `node` over the
/// given input layouts. The fixtures wire constants into every eager input,
/// which the pass records as lane-invariant.
pub fn install<N: Node<ContextImpl<'static>>>(mut node: N, meta: LayoutMeta, inputs: &[Option<&Layout>]) -> N {
let resolved = RecordLayout {
named_writes: Vec::new(),
named_reads: Vec::new(),
named_read_defaults: Vec::new(),
lane_invariant: u32::MAX,
..meta.resolve(inputs)
};
<N as Node<ContextImpl<'static>>>::set_layout(&mut node, resolved);
node
}
/// Installs a flipped node's output layout directly.
pub fn install_flip<N: Node<ContextImpl<'static>>>(mut node: N, layout: &Layout) -> N {
let bundle = RecordLayout {
named_writes: Vec::new(),
named_reads: Vec::new(),
named_read_defaults: Vec::new(),
frame_bytes: layout.frame_bytes(),
plan: Vec::new(),
layout: layout.clone(),
lane_invariant: u32::MAX,
};
<N as Node<ContextImpl<'static>>>::set_layout(&mut node, bundle);
node
}
/// A constant as a value source with its layout.
pub fn lifted_value<T: Clone + Send + Sync + dyn_any::StaticTypeSized + 'static>(value: T) -> (ValueSource<T>, Layout)
where
T::Static: Clone + Send + Sync,
{
let lift = ValueSource::new(value);
let layout = Node::<ContextImpl>::layout(&lift).clone();
(lift, layout)
}
pub fn bare_source(layout: &Layout, element: f64) -> RecordSourceNode<f64> {
RecordSourceNode {
layout: layout.clone(),
element,
fields: vec![],
partial: false,
}
}
}

View File

@@ -102,6 +102,14 @@ pub fn cache_key<C: CacheHash + ?Sized>(ctx: &C) -> u64 {
hasher.finish()
}
/// The lane-normalized cache key and arena generation of one evaluation, so a
/// node can keep per-evaluation scratch across its lanes.
pub fn eval_key<'e, C: CacheHash + crate::context::InjectIndex + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena> + Copy>(ctx: &C) -> (u64, u64) {
let mut keyed = *ctx;
keyed.set_index(0);
(cache_key(&keyed), ctx.arena().generation())
}
#[derive(Debug, PartialEq)]
pub enum ConstructionError {
Arity { expected: usize, got: usize },