Emit declarative LayoutMeta from the node macro into registry entries

This commit is contained in:
Dennis Kobert
2026-08-11 20:08:45 +00:00
parent 557f8020bb
commit b6f73b0691
5 changed files with 89 additions and 0 deletions

View File

@@ -179,6 +179,7 @@ mod node_registry_macros {
(
ProtoNodeIdentifier::new(concat!["graphene_core::ops::IntoNode<", stringify!($to), ">"]),
RegistryEntry {
layout_meta: None,
io: NodeIOTypes::new(
concrete!(Context),
core_types::registry::record_type::<$to>(),
@@ -246,6 +247,7 @@ mod node_registry_macros {
(
ProtoNodeIdentifier::new(concat!["graphene_core::ops::ConvertNode<", stringify!($to), ">"]),
RegistryEntry {
layout_meta: None,
io: NodeIOTypes::new(
concrete!(Context),
core_types::registry::record_type::<$to>(),
@@ -291,6 +293,7 @@ mod node_registry_macros {
(
ProtoNodeIdentifier::new(concat!["graphene_core::ops::ConvertNode<", stringify!($to), ">"]),
RegistryEntry {
layout_meta: None,
io: NodeIOTypes::new(
concrete!(Context),
core_types::registry::record_type::<$to>(),

View File

@@ -242,6 +242,52 @@ pub fn empty_layout() -> &'static Layout {
EMPTY.get_or_init(Layout::default)
}
/// Declarative record-io metadata for a node type, emitted by the macro into
/// its registry entry so the compiler can fold each wire's layout without
/// running the node's constructor. [`fold`](LayoutMeta::fold) reproduces the
/// layout the constructor derives at wiring today; the compiler layout pass
/// calls it over the proto graph instead.
#[derive(Clone, Debug)]
pub struct LayoutMeta {
/// Whether the node derives its layout from a carrier input; `false` writes
/// a fresh record from `Layout::default`.
pub carrier: bool,
/// The output element: a concrete write, or carried through from the carrier.
pub element: ElementSpec,
/// The attributes the node writes at its acting level.
pub writes: Vec<FieldWrite>,
/// The attributes removed from the carrier's layout, as `(name, level)`.
pub removes: Vec<(&'static str, u8)>,
/// The depth change the node applies to its carrier: `0` for elementwise and
/// flip nodes, `+1` for a creator, `-1` for a reducer.
pub level_delta: i8,
}
/// Where a node's output element comes from, for [`LayoutMeta`].
#[derive(Clone, Debug)]
pub enum ElementSpec {
/// The node writes this concrete element.
Concrete(ElementWrite),
/// The node carries the carrier's element through unchanged.
Carried,
}
impl LayoutMeta {
/// Folds the node's output layout from its carrier's, reproducing what the
/// node's constructor derives at wiring. `carrier` is the carrier input's
/// layout, or `None` when the node writes a fresh record.
pub fn fold(&self, carrier: Option<&Layout>) -> Layout {
let carrier = carrier.filter(|_| self.carrier);
let base = carrier.map_or_else(Layout::default, |c| c.without(&self.removes));
let depth = (carrier.map_or(0, |c| c.depth) as i8 + self.level_delta).max(0) as u8;
let element = match &self.element {
ElementSpec::Concrete(element) => *element,
ElementSpec::Carried => carrier.map_or_else(ElementWrite::default, |c| c.element),
};
base.with_writes(depth, element, &self.writes)
}
}
/// A view of one record: a pointer whose layout is proven at wiring.
#[derive(Clone, Copy, Debug)]
pub struct Rec(*const u8);

View File

@@ -170,6 +170,11 @@ where
unsafe { self.ptr.as_ref() }.extent(input)
}
fn extent_at(&self, input: &Input, level: u8) -> crate::gpoll::GPoll<crate::gpoll::Extent> {
// SAFETY: as in eval.
unsafe { self.ptr.as_ref() }.extent_at(input, level)
}
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
// SAFETY: as in eval.
unsafe { self.ptr.as_ref() }.serialize()
@@ -278,6 +283,10 @@ pub type NodeConstructor = fn(Vec<EdgeHandle>) -> Result<EdgeHandle, Constructio
pub struct RegistryEntry {
pub io: NodeIOTypes,
pub constructor: NodeConstructor,
/// Declarative record-io metadata for the compiler layout pass; `None` for
/// nodes whose layout the pass does not yet fold (routing/opaque, hand-written
/// rows), which keep the construction-time path.
pub layout_meta: Option<crate::record::LayoutMeta>,
}
pub fn construct(entry: &RegistryEntry, inputs: Vec<EdgeHandle>) -> Result<EdgeHandle, ConstructionError> {
@@ -502,6 +511,7 @@ mod tests {
Ok(EdgeHandle::new(Arc::new(ValueNode(0u32)) as Arc<ErasedNode<u32>>))
}
let entry = RegistryEntry {
layout_meta: None,
io: NodeIOTypes::new(concrete!(Context), concrete!(u32), vec![edge_type::<String>()]),
constructor: construct_strlen,
};

View File

@@ -1741,6 +1741,23 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
}
},
};
let layout_meta_fn = format_ident!("{}_layout_meta", fn_name);
let carrier_present = !shape.skips_carrier();
let element_spec = match &shape.element_write {
Some(ty) => quote!(#core_types::record::ElementSpec::Concrete(#core_types::record::element_write::<#ty>())),
None => quote!(#core_types::record::ElementSpec::Carried),
};
let layout_meta_def = quote! {
#vis fn #layout_meta_fn() -> #core_types::record::LayoutMeta {
#core_types::record::LayoutMeta {
carrier: #carrier_present,
element: #element_spec,
writes: ::std::vec![#(#write_descs),*],
removes: ::std::vec![#(#remove_pairs),*],
level_delta: 0,
}
}
};
let reading_secondaries = reading_secondary_indices(&regular_fields, shape);
let edge_args = regular_fields.iter().zip(&node_generics).map(|(field, generic)| {
let name = &field.pat_ident.ident;
@@ -1790,6 +1807,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let write_names = (0..shape.write_markers.len()).map(|index| format_ident!("__write_{index}")).map(|slot| quote!(#slot,));
quote! {
#layout_def
#layout_meta_def
#[automatically_derived]
impl<#(#data_field_generic_idents,)* #(#node_generics,)*> #mod_name::#struct_name<#(#struct_type_params,)*> {

View File

@@ -56,6 +56,7 @@ fn flip_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_field
let arity = regular_fields.len();
let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect();
let node_underscores: Vec<TokenStream2> = regular_fields.iter().map(|_| quote!(_)).collect();
let carrier_present = flip_carrier(parsed);
// Shorthand associated types in the output only resolve against the
// generics' bounds, so rows name the output through a bounded alias. Only
@@ -144,6 +145,13 @@ fn flip_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_field
});
Some(quote! {
gcore::registry::RegistryEntry {
layout_meta: Some(gcore::record::LayoutMeta {
carrier: #carrier_present,
element: gcore::record::ElementSpec::Concrete(gcore::record::element_write::<#row_output>()),
writes: ::std::vec::Vec::new(),
removes: ::std::vec::Vec::new(),
level_delta: 0,
}),
io: gcore::registry::NodeIOTypes::new(
gcore::concrete!(gcore::context::ContextImpl<'static>),
gcore::registry::record_type::<#row_output>(),
@@ -273,6 +281,7 @@ fn routing_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fi
quote! {
pub fn #entries_name() -> ::std::vec::Vec<gcore::registry::RegistryEntry> {
vec![gcore::registry::RegistryEntry {
layout_meta: None,
io: gcore::registry::NodeIOTypes::new(
gcore::concrete!(gcore::context::ContextImpl<'static>),
gcore::Type::Record(Box::new(gcore::Type::Generic(::std::borrow::Cow::Borrowed(#token_name)))),
@@ -353,6 +362,7 @@ fn record_opaque_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regu
quote! {
pub fn #entries_name() -> ::std::vec::Vec<gcore::registry::RegistryEntry> {
vec![gcore::registry::RegistryEntry {
layout_meta: None,
io: gcore::registry::NodeIOTypes::new(
gcore::concrete!(gcore::context::ContextImpl<'static>),
gcore::Type::Record(Box::new(gcore::Type::Generic(::std::borrow::Cow::Borrowed("T")))),
@@ -464,9 +474,11 @@ fn record_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fie
(_, None) => unreachable!("non-token record nodes write an element"),
};
let layout_meta_fn = format_ident!("{}_layout_meta", fn_name);
quote! {
pub fn #entries_name() -> ::std::vec::Vec<gcore::registry::RegistryEntry> {
vec![gcore::registry::RegistryEntry {
layout_meta: Some(self::#layout_meta_fn()),
io: gcore::registry::NodeIOTypes::new(
gcore::concrete!(gcore::context::ContextImpl<'static>),
#io_output,