Materialize a reducer's ranked input into a List and fold it

This commit is contained in:
Dennis Kobert
2026-08-14 21:19:20 +00:00
parent 65e0969721
commit fdf13f91cb
9 changed files with 168 additions and 14 deletions

View File

@@ -54,7 +54,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let model = analyze(parsed);
let node = crate::codegen::ir::build(parsed);
let kind = model.as_ref().map(|_| crate::codegen::ir::node_kind(&node));
let carrier_present = node.inputs.first().is_some_and(|input| input.subject);
let carrier_present = matches!(node.inputs.first(), Some(input) if input.subject && crate::codegen::ir::materialized_levels(&node, 0) == 0);
let record_io = matches!(kind, Some(crate::codegen::ir::NodeKind::RecordIo));
let flip = matches!(kind, Some(crate::codegen::ir::NodeKind::Flip));
let carrier_flip = flip && carrier_present;
@@ -722,7 +722,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let async_source = async_fn || future_kernel;
let node = crate::codegen::ir::build(parsed);
let kind = crate::codegen::ir::node_kind(&node);
let carrier_present = node.inputs.first().is_some_and(|input| input.subject);
let carrier_present = matches!(node.inputs.first(), Some(input) if input.subject && crate::codegen::ir::materialized_levels(&node, 0) == 0);
let flip = matches!(kind, crate::codegen::ir::NodeKind::Flip);
let carrier_flip = flip && carrier_present;
let opaque = matches!(kind, crate::codegen::ir::NodeKind::Opaque);
@@ -951,6 +951,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
.map(|(index, field)| {
let pat = &field.pat_ident;
match &field.ty {
ParsedFieldType::Regular(RegularParsedField { ty, .. }) if ir::materialized_levels(&node, index) > 0 => {
quote!(#pat: #core_types::node::List<'_, '_, #ty>)
}
ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => quote!(#pat: &#ty),
ParsedFieldType::Regular(RegularParsedField { ty, .. }) if !field.attribute_reads.is_empty() => read_tuple_param(field, quote!(#pat), quote!(#ty)),
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#pat: #ty),
@@ -1088,6 +1091,28 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
// A carrier primary evaluates beyond the node's own frame (in the
// record/flip tail), so it does not bind here.
ValueBinding::Carrier => quote!(),
ValueBinding::Materialized => {
let levels = ir::materialized_levels(&node, index);
quote! {
let __arena = #core_types::context::ExtractArena::arena(__input);
let __count = match #core_types::node::Node::extent(&self.#name, __input, #core_types::gpoll::Level::Below(#levels)) {
#core_types::gpoll::GPoll::Final(#core_types::gpoll::Extent::Exactly(__count)) => __count,
#core_types::gpoll::GPoll::Pending => return #core_types::gpoll::GPoll::Pending,
_ => return #core_types::gpoll::GPoll::Error(::std::boxed::Box::new(#core_types::gpoll::GraphError::new("reduce over a non-exact extent"))),
};
let __scratch = match __arena.alloc_scratch::<#core_types::record::RecordValue<'__record>>(__count) {
Some(__scratch) => __scratch,
None => return #core_types::gpoll::GPoll::Error(::std::boxed::Box::new(#core_types::gpoll::GraphError::new("reduce scratch allocation failed"))),
};
let __batch = match #core_types::node::Node::eval_batch(&self.#name, __input, 0..__count as u64, Some(__scratch)) {
#core_types::node::BatchStatus::Lent(__batch, _) | #core_types::node::BatchStatus::Filled(__batch, _) => __batch,
#core_types::node::BatchStatus::Pending => return #core_types::gpoll::GPoll::Pending,
#core_types::node::BatchStatus::Error(__error) => return #core_types::gpoll::GPoll::Error(::std::boxed::Box::new(__error)),
_ => return #core_types::gpoll::GPoll::Error(::std::boxed::Box::new(#core_types::gpoll::GraphError::new("reduce batch failed"))),
};
let #name = unsafe { #core_types::node::List::<#ty>::new(__batch) };
}
}
// A reading secondary input claims a record edge: the element and
// the declared reads copy out right after its eval, before any
// later sibling eval can reuse the record stack.
@@ -1668,7 +1693,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
true => {
let mut bounds: Vec<TokenStream2> = regular_fields
.iter()
.filter_map(|field| match &field.ty {
.enumerate()
.filter(|(index, _)| ir::materialized_levels(&node, *index) == 0)
.filter_map(|(_, field)| match &field.ty {
// The conditional arena-park moves a lend element once.
ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => Some(quote!(#ty: ::core::marker::Send + ::core::marker::Sync + 'static)),
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => Some(quote!(#ty: ::core::clone::Clone)),

View File

@@ -63,7 +63,7 @@ pub(crate) fn analyze(parsed: &ParsedNodeFn) -> Option<Dialect> {
} else if has_record_io(parsed) {
return None;
} else {
routing_io(parsed).is_some() || record_flip(parsed) || record_opaque(parsed)
routing_io(parsed).is_some() || record_flip(parsed) || record_opaque(parsed) || has_materialized_input(parsed)
};
supported.then(|| dialect(parsed))
}
@@ -367,6 +367,13 @@ pub(crate) fn flip_carrier(parsed: &ParsedNodeFn) -> bool {
/// fully-concrete value-input nodes in this cut; batch, shader, async, lend,
/// lazy, and generic nodes keep the plain lowering until their record forms
/// land.
pub(crate) fn has_materialized_input(parsed: &ParsedNodeFn) -> bool {
parsed
.fields
.iter()
.any(|field| matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { list_levels, .. }) if *list_levels > 0))
}
pub(crate) fn record_flip(parsed: &ParsedNodeFn) -> bool {
if record_shape(parsed).is_some() || has_record_io(parsed) || routing_io(parsed).is_some() {
return false;

View File

@@ -59,10 +59,14 @@ fn inputs(parsed: &ParsedNodeFn, fields: &[&ParsedField], generics: &[Ident]) ->
ParsedFieldType::Node(_) => Evaluation::Lazy,
ParsedFieldType::Regular(_) => Evaluation::Eager,
};
let (element, depth) = match &field.ty {
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => strip_ilist(output_type),
ParsedFieldType::Regular(RegularParsedField { ty, list_levels, .. }) => (ty.clone(), *list_levels),
};
Input {
ident: field.pat_ident.ident.clone(),
evaluation,
shape: item_shape(field_element_type(field), &field.attribute_reads, generics),
shape: item_shape(&element, depth, &field.attribute_reads, generics),
subject: subject(index, field, carrier_subject, routing.as_ref()),
lend: matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. })),
}
@@ -135,10 +139,9 @@ fn field_element_type(field: &ParsedField) -> &Type {
}
}
fn item_shape(element: &Type, reads: &[AttributeRead], generics: &[Ident]) -> ItemShape {
let (element, depth) = strip_ilist(element);
fn item_shape(element: &Type, depth: u8, reads: &[AttributeRead], generics: &[Ident]) -> ItemShape {
ItemShape {
element: element_of(&element, generics),
element: element_of(element, generics),
depth,
attrs: reads.iter().map(|read| LevelAttr { marker: read.marker.clone(), level: 0 }).collect(),
}
@@ -226,6 +229,7 @@ fn level_delta(node: &Node) -> i8 {
/// How an eager value input binds in eval.
pub(crate) enum ValueBinding {
Carrier,
Materialized,
Lend,
ReadingSecondary,
RecordElement,
@@ -282,10 +286,18 @@ fn has_attr_io(node: &Node) -> bool {
node.inputs.iter().any(|input| matches!(input.evaluation, Evaluation::Eager) && !input.shape.attrs.is_empty()) || !node.output.shape.attrs.is_empty() || !node.output.removes.is_empty()
}
/// Levels of `input[index]` the output does not carry; `> 0` folds the input
/// into a `List` before the kernel.
pub(crate) fn materialized_levels(node: &Node, index: usize) -> u8 {
node.inputs[index].shape.depth.saturating_sub(node.output.shape.depth)
}
pub(crate) fn value_binding(node: &Node, index: usize) -> ValueBinding {
let input = &node.inputs[index];
let kind = node_kind(node);
if matches!(kind, NodeKind::RecordIo | NodeKind::Flip) && index == 0 && input.subject {
if materialized_levels(node, index) > 0 {
ValueBinding::Materialized
} else if matches!(kind, NodeKind::RecordIo | NodeKind::Flip) && index == 0 && input.subject {
ValueBinding::Carrier
} else if matches!(kind, NodeKind::Flip) && input.lend {
ValueBinding::Lend
@@ -620,6 +632,7 @@ mod tests {
match &field.ty {
ParsedFieldType::Regular(_) => match value_binding(node, index) {
ValueBinding::Carrier => "carrier",
ValueBinding::Materialized => "materialized",
ValueBinding::Lend => "lend",
ValueBinding::ReadingSecondary => "reading",
ValueBinding::RecordElement => "record",

View File

@@ -17,10 +17,9 @@ pub(crate) fn generate_node_input_references(
for (input_index, (parsed_input, input_ident)) in parsed.fields.iter().zip(field_idents).enumerate() {
let mut ty = match &parsed_input.ty {
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty,
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type,
}
.clone();
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty.clone(),
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type.clone(),
};
// We only want the necessary generics.
let used = generic_collector.filter_unnecessary_generics(&mut modified, &mut ty);

View File

@@ -313,6 +313,8 @@ impl Parse for NumberRange {
#[derive(Clone, Debug)]
pub struct RegularParsedField {
pub ty: Type,
/// `IList` nesting stripped from `ty` at parse; `ty` holds the element row.
pub list_levels: u8,
/// The original reference tokens when the parameter was written `&T`; `ty` holds the peeled inner type.
pub lend: Option<syn::TypeReference>,
pub exposed: bool,
@@ -911,6 +913,7 @@ fn parse_node_implementations<T: Parse>(attr: &Attribute, name: &Ident) -> syn::
}
fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Result<ParsedField> {
let (ty, list_levels) = crate::codegen::ir::strip_ilist(&ty);
let ident = &pat_ident.ident;
// Checks for the #[data] attribute, indicating that this is a data field rather than an input parameter to the node.
@@ -1122,6 +1125,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
number_hard_max,
number_mode_range,
ty,
list_levels,
lend,
value_source,
implementations,
@@ -1236,6 +1240,7 @@ impl ParsedNodeFn {
widget_override: ParsedWidgetOverride::Hidden,
ty: ParsedFieldType::Regular(RegularParsedField {
ty,
list_levels: 0,
lend: None,
exposed: false,
value_source,
@@ -1410,6 +1415,7 @@ mod tests {
widget_override: ParsedWidgetOverride::None,
ty: ParsedFieldType::Regular(RegularParsedField {
lend: None,
list_levels: 0,
ty: parse_quote!(f64),
exposed: false,
value_source: ParsedValueSource::None,
@@ -1504,6 +1510,7 @@ mod tests {
widget_override: ParsedWidgetOverride::None,
ty: ParsedFieldType::Regular(RegularParsedField {
lend: None,
list_levels: 0,
ty: parse_quote!(DVec2),
exposed: false,
value_source: ParsedValueSource::None,
@@ -1579,6 +1586,7 @@ mod tests {
widget_override: ParsedWidgetOverride::None,
ty: ParsedFieldType::Regular(RegularParsedField {
lend: None,
list_levels: 0,
ty: parse_quote!(f64),
exposed: false,
value_source: ParsedValueSource::Default(quote!(50.)),
@@ -1652,6 +1660,7 @@ mod tests {
widget_override: ParsedWidgetOverride::None,
ty: ParsedFieldType::Regular(RegularParsedField {
lend: None,
list_levels: 0,
ty: parse_quote!(f64),
exposed: false,
value_source: ParsedValueSource::None,
@@ -1737,6 +1746,7 @@ mod tests {
widget_override: ParsedWidgetOverride::None,
ty: ParsedFieldType::Regular(RegularParsedField {
lend: None,
list_levels: 0,
ty: parse_quote!(f64),
exposed: false,
value_source: ParsedValueSource::None,
@@ -1825,6 +1835,7 @@ mod tests {
widget_override: ParsedWidgetOverride::None,
ty: ParsedFieldType::Regular(RegularParsedField {
lend: None,
list_levels: 0,
ty: parse_quote!(String),
exposed: true,
value_source: ParsedValueSource::None,

View File

@@ -232,6 +232,7 @@ impl PerPixelAdjustCodegen<'_> {
widget_override: Default::default(),
ty: ParsedFieldType::Regular(RegularParsedField {
ty: parse_quote!(#wgpu_executor::WgpuExecutorHandle),
list_levels: 0,
lend: None,
exposed: true,
value_source: ParsedValueSource::Scope(Box::new(parse_quote!("graphene_std::platform_application_io::WgpuExecutorNode"))),