Give extent overrides a typed input surface instead of the raw node form

This commit is contained in:
Dennis Kobert
2026-08-16 15:55:17 +00:00
parent 3859bd713e
commit 131abf5883
6 changed files with 171 additions and 41 deletions

View File

@@ -1221,13 +1221,74 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
// The extent override is the leveled `extent_at`; consumers query the
// composite `extent(ctx, Level)`, which the trait derives from it. A node
// without `extent = fn` keeps the scalar default (one item at every level).
let extent_impl = match &parsed.attributes.extent {
Some(path) => quote! {
// The typed extent surface: the node's inputs in declaration order (values
// readable without unsafe, edges as per-level extent queries, derived
// content promoted per copy), then the level paired with the node's depth.
let extent_impl = if let Some(path) = &parsed.attributes.extent {
let mut arg_decls: Vec<TokenStream2> = Vec::new();
let mut arg_names: Vec<Ident> = Vec::new();
for (index, field) in regular_fields.iter().enumerate() {
let name = &field.pat_ident.ident;
if injected_name(name) {
continue;
}
let arg = format_ident!("__extent_arg_{index}");
let query = format_ident!("__extent_query_{index}");
let extent_edge = |query: &Ident, arg: &Ident| {
quote! {
let #query = |_: u64, __lvl: u8| #core_types::node::Node::extent_at(&self.#name, __input, __lvl);
let #arg = #core_types::extent::ExtentIn::new(&#query);
}
};
let decl = match &field.ty {
ParsedFieldType::Node(_) => match ir::lazy_binding(&node, index) {
ir::LazyBinding::DeriveRouting => quote! {
let #query = |__copy: u64, __lvl: u8| {
let __head = #core_types::context::DeriveCtx::index_head(__input);
#core_types::record::DerivedRecordEdge::extent_at_derived(&self.#name, &#core_types::context::DeriveCtx::promoted(__input, &__head, __copy), __lvl)
};
let #arg = #core_types::extent::ExtentIn::new(&#query);
},
_ => extent_edge(&query, &arg),
},
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => match ir::value_binding(&node, index) {
ValueBinding::RecordElement | ValueBinding::ReadingSecondary => {
let slot = format_ident!("__in_{index}");
quote! {
let #query = || {
#core_types::node::Node::eval(&self.#name, __input)
.map(|__value| unsafe { #core_types::record::read_element::<#ty>(self.#slot.rec(&__value)) })
};
let #arg = #core_types::extent::ValueIn::new(&#query);
}
}
ValueBinding::Plain => quote! {
let #query = || #core_types::node::Node::eval(&self.#name, __input);
let #arg = #core_types::extent::ValueIn::new(&#query);
},
// A carrier, lent, or materialized ranked input is a record
// edge; its extents are the queryable quantity.
_ => extent_edge(&query, &arg),
},
};
arg_decls.push(decl);
arg_names.push(arg);
}
quote! {
fn extent_at(&self, __input: &#ctx_ident, __level: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> {
#(#arg_decls)*
let __level_in = #core_types::extent::LevelIn::new(__level, <Self as #core_types::node::Node<#ctx_ident>>::layout(self).depth);
#path(#(#arg_names,)* __level_in)
}
}
} else if let Some(path) = &parsed.attributes.extent_raw {
quote! {
fn extent_at(&self, __input: &#ctx_ident, __level: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> {
#path(self, __input, __level)
}
},
None => quote!(),
}
} else {
quote!()
};
let serialize_impl = match &parsed.attributes.serialize {

View File

@@ -129,6 +129,8 @@ pub(crate) struct NodeFnAttributes {
pub(crate) placeholder: Option<Path>,
/// Function overriding the generated `extent` method
pub(crate) extent: Option<Path>,
/// Function overriding the generated `extent` method with the raw node/ctx/level form
pub(crate) extent_raw: Option<Path>,
/// Function overriding the generated `eval_batch` method
pub(crate) batch: Option<Path>,
/// Whether partial upstream values are mapped to `Pending` instead of flowing into this node
@@ -400,6 +402,7 @@ impl Parse for NodeFnAttributes {
let mut inject_scope = false;
let mut placeholder = None;
let mut extent = None;
let mut extent_raw = None;
let mut batch = None;
let mut no_partial = false;
let mut plain = false;
@@ -572,6 +575,19 @@ impl Parse for NodeFnAttributes {
let parsed_path: Path = meta.parse_args().map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'extent', e.g., extent(my_extent)"))?;
extent = Some(parsed_path);
}
// Escape hatch for extent overrides needing arbitrary context access: the raw
// `(node, ctx, level)` form instead of the typed `extent(fn)` input surface.
//
// Example usage:
// #[node_macro::node(..., extent_raw(my_extent), ...)]
"extent_raw" => {
let meta = meta.require_list()?;
if extent_raw.is_some() {
return Err(Error::new_spanned(meta, "Multiple 'extent_raw' attributes are not allowed"));
}
let parsed_path: Path = meta.parse_args().map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'extent_raw', e.g., extent_raw(my_extent)"))?;
extent_raw = Some(parsed_path);
}
// Function overriding the generated `eval_batch` method, replacing the trait's per-lane spec loop.
//
// Example usage:
@@ -608,7 +624,7 @@ impl Parse for NodeFnAttributes {
indoc!(
r#"
Unsupported attribute in `node`.
Supported attributes are 'category', 'name', 'path', 'skip_impl', 'properties', 'cfg', 'shader_node', 'serialize', 'memoize', 'inject_scope', 'placeholder', 'extent', 'batch', and 'no_partial'.
Supported attributes are 'category', 'name', 'path', 'skip_impl', 'properties', 'cfg', 'shader_node', 'serialize', 'memoize', 'inject_scope', 'placeholder', 'extent', 'extent_raw', 'batch', and 'no_partial'.
Example usage:
#[node_macro::node(..., name("Test Node"), ...)]
"#
@@ -631,6 +647,10 @@ impl Parse for NodeFnAttributes {
));
}
if let (Some(_), Some(raw)) = (&extent, &extent_raw) {
return Err(Error::new_spanned(raw, "'extent' and 'extent_raw' are mutually exclusive"));
}
Ok(NodeFnAttributes {
category,
display_name,
@@ -644,6 +664,7 @@ impl Parse for NodeFnAttributes {
inject_scope,
placeholder,
extent,
extent_raw,
batch,
no_partial,
plain,
@@ -1393,6 +1414,7 @@ mod tests {
inject_scope: false,
placeholder: None,
extent: None,
extent_raw: None,
batch: None,
no_partial: false,
plain: false,
@@ -1472,6 +1494,7 @@ mod tests {
inject_scope: false,
placeholder: None,
extent: None,
extent_raw: None,
batch: None,
no_partial: false,
plain: false,
@@ -1566,6 +1589,7 @@ mod tests {
inject_scope: false,
placeholder: None,
extent: None,
extent_raw: None,
batch: None,
no_partial: false,
plain: false,
@@ -1641,6 +1665,7 @@ mod tests {
inject_scope: false,
placeholder: None,
extent: None,
extent_raw: None,
batch: None,
no_partial: false,
plain: false,
@@ -1728,6 +1753,7 @@ mod tests {
inject_scope: false,
placeholder: None,
extent: None,
extent_raw: None,
batch: None,
no_partial: false,
plain: false,
@@ -1818,6 +1844,7 @@ mod tests {
inject_scope: false,
placeholder: None,
extent: None,
extent_raw: None,
batch: None,
no_partial: false,
plain: false,
@@ -1893,6 +1920,7 @@ mod tests {
inject_scope: false,
placeholder: None,
extent: None,
extent_raw: None,
batch: None,
no_partial: false,
plain: false,