Flip concrete value nodes onto record wires with bridge rows for every value type

This commit is contained in:
Dennis Kobert
2026-08-05 22:18:50 +00:00
parent 9680d34abf
commit 7099f056dc
6 changed files with 402 additions and 8 deletions

View File

@@ -263,6 +263,27 @@ macro_rules! tagged_value {
}
}
/// The bridge rows for every wire type a value can carry, spliced
/// while plain and record worlds coexist.
pub fn record_bridge_entries() -> Vec<(core_types::ProtoNodeIdentifier, core_types::registry::RegistryEntry)> {
let mut entries = Vec::new();
entries.extend(core_types::registry::record_bridge_rows::<()>());
entries.extend(core_types::registry::record_bridge_rows::<List<f64>>());
entries.extend(core_types::registry::record_bridge_rows::<List<Color>>());
entries.extend(core_types::registry::record_bridge_rows::<List<GradientStops>>());
entries.extend(core_types::registry::record_bridge_rows::<List<BrushStroke>>());
entries.extend(core_types::registry::record_bridge_rows::<RenderOutput>());
entries.extend(core_types::registry::record_bridge_rows::<List<NodeId>>());
entries.extend(core_types::registry::record_bridge_rows::<DocumentNode>());
entries.extend(core_types::registry::record_bridge_rows::<ContextModification>());
entries.extend(core_types::registry::record_bridge_rows::<Arc<PlatformEditorApi>>());
entries.extend(core_types::registry::record_bridge_rows::<ResourceHash>());
$(
entries.extend(core_types::registry::record_bridge_rows::<$ty>());
)*
entries
}
/// Materializes the value as [`Self::to_dynany`] does, wrapped in a `ClonedNode` edge typed by [`Self::ty`].
pub fn to_edge(self) -> Result<EdgeHandle, String> {
match self {
@@ -389,8 +410,8 @@ macro_rules! tagged_value {
pub fn from_type(input: &Type) -> Option<Self> {
match input {
Type::Generic(_) => None,
Type::Ref(_) => None,
Type::Record(_) => None,
Type::Ref(inner) => Self::from_type(inner),
Type::Record(inner) => Self::from_type(inner),
Type::Concrete(concrete_type) => {
let name = concrete_type.name.as_ref();
// TODO: Add default implementations for types such as TaggedValue::Subpaths, and use the defaults here and in document_node_types

View File

@@ -934,6 +934,8 @@ fn ref_adapter(proposed: &Type, wanted: &Type) -> Option<ProtoNodeIdentifier> {
(proposed_output @ Type::Concrete(_), Type::Ref(inner)) if valid_type(proposed_output, inner) => Some(ProtoNodeIdentifier::new("graphene_core::memo::LendNode")),
(Type::Record(inner), wanted_output @ Type::Concrete(_)) if valid_type(inner, wanted_output) => Some(ProtoNodeIdentifier::new("core_types::record::RecordExtractNode")),
(proposed_output @ Type::Concrete(_), Type::Record(inner)) if valid_type(proposed_output, inner) => Some(ProtoNodeIdentifier::new("core_types::record::RecordLiftNode")),
(Type::Ref(inner), Type::Record(wanted_inner)) if valid_type(inner, wanted_inner) => Some(ProtoNodeIdentifier::new("core_types::record::RecordLiftLendNode")),
(Type::Record(inner), Type::Ref(wanted_inner)) if valid_type(inner, wanted_inner) => Some(ProtoNodeIdentifier::new("core_types::record::RecordExtractLendNode")),
_ => None,
}
}

View File

@@ -677,6 +677,9 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
.flatten(),
);
node_types.extend(graph_craft::document::value::TaggedValue::record_bridge_entries());
node_types.extend(core_types::registry::record_bridge_rows::<graphene_std::application_io::resource::Resource>());
let mut map: HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> = HashMap::new();
let insert = |map: &mut HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>>, id: ProtoNodeIdentifier, entry: RegistryEntry| {
let rows = map.entry(id).or_default();

View File

@@ -693,6 +693,111 @@ where
}
}
/// Lifts a lending producer onto a record wire: a parked element carries the
/// lent reference directly, a byte-carried one copies out of the borrow.
pub struct RecordLiftLend<El, N> {
edge: N,
layout: Layout,
_marker: std::marker::PhantomData<fn() -> El>,
}
impl<El: 'static, N> RecordLiftLend<El, N> {
pub fn new(edge: N) -> Self {
Self {
edge,
layout: Layout::default().with_writes(0, element_dims::<El>(), &[]),
_marker: std::marker::PhantomData,
}
}
}
impl<'e, C, El, N> Node<C> for RecordLiftLend<El, N>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
El: Send + Sync + 'static,
N: Node<C, Output = &'e El>,
{
type Output = RecordValue<'e>;
fn eval(&self, input: &C) -> GPoll<RecordValue<'e>> {
let build = |element: &'e El| {
let write = |dst: *mut u8| match element_parked::<El>() {
true => unsafe { dst.cast::<&El>().write(element) },
false => unsafe { std::ptr::copy_nonoverlapping((element as *const El).cast::<u8>(), dst, size_of::<El>()) },
};
if self.layout.is_inline() {
let mut value = RecordValue::zeroed();
write(value.as_mut_ptr());
value
} else {
let dst = stack::push(self.layout.frame_bytes());
write(dst);
stack::pop(dst);
RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) })
}
};
self.edge.eval(input).map(build)
}
fn layout(&self) -> Option<&Layout> {
Some(&self.layout)
}
}
/// Lends a record wire's element: a parked element lends its arena-backed
/// reference directly, a byte-carried one parks a copy so the borrow
/// outlives the record.
pub struct RecordExtractLend<El, N> {
edge: N,
layout: Layout,
_marker: std::marker::PhantomData<fn() -> El>,
}
impl<El, N> RecordExtractLend<El, N> {
pub fn new(edge: N, layout: &Layout) -> Self {
Self {
edge,
layout: layout.clone(),
_marker: std::marker::PhantomData,
}
}
}
impl<'e, C, El, N> Node<C> for RecordExtractLend<El, N>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
El: Clone + Send + Sync + 'static,
N: Node<C, Output = RecordValue<'e>>,
{
type Output = &'e El;
fn eval(&self, input: &C) -> GPoll<&'e El> {
let exhausted = || {
GPoll::Error(Box::new(crate::gpoll::GraphError {
kind: crate::gpoll::ErrorKind::ArenaExhausted,
trace: Vec::new(),
}))
};
let lend = |value: RecordValue<'e>| {
let rec = self.layout.rec(&value);
match element_parked::<El>() {
true => Some(unsafe { borrow_element::<El>(rec) }),
false => input.arena().alloc(unsafe { read_element::<El>(rec) }).map(|(parked, _)| parked),
}
};
match self.edge.eval(input) {
GPoll::Final(value) => lend(value).map_or_else(exhausted, GPoll::Final),
GPoll::Partial(value) => lend(value).map_or_else(exhausted, GPoll::Partial),
GPoll::Fallback(boxed) => {
let (value, error) = *boxed;
lend(value).map_or_else(exhausted, |element| GPoll::Fallback(Box::new((element, error))))
}
GPoll::Pending => GPoll::Pending,
GPoll::Error(error) => GPoll::Error(error),
}
}
}
/// Extracts the element from a record wire for a plain consumer, cloning out
/// of the parked reference when the element carries drop glue.
pub struct RecordExtract<El, N> {

View File

@@ -290,6 +290,83 @@ pub struct RegistryEntry {
pub constructor: NodeConstructor,
}
/// The four bridge rows of `T`: plain and lend producers onto record wires,
/// record wires into plain and lend consumers. One set exists per wire type
/// while the worlds coexist.
pub fn record_bridge_rows<T: Clone + Send + Sync + 'static>() -> [(crate::ProtoNodeIdentifier, RegistryEntry); 4] {
[
(crate::ProtoNodeIdentifier::new("core_types::record::RecordLiftNode"), record_lift_entry::<T>()),
(crate::ProtoNodeIdentifier::new("core_types::record::RecordExtractNode"), record_extract_entry::<T>()),
(crate::ProtoNodeIdentifier::new("core_types::record::RecordLiftLendNode"), record_lift_lend_entry::<T>()),
(crate::ProtoNodeIdentifier::new("core_types::record::RecordExtractLendNode"), record_extract_lend_entry::<T>()),
]
}
/// The lift bridge row for `T`: a plain producer onto a record wire. One
/// exists per wire type while plain and record worlds coexist.
pub fn record_lift_entry<T: Clone + Send + Sync + 'static>() -> RegistryEntry {
RegistryEntry {
io: NodeIOTypes::new(concrete!(Context), record_type::<T>(), vec![edge_type::<T>()]),
constructor: |inputs| {
if inputs.len() != 1 {
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
}
let mut inputs = inputs.into_iter();
let node = crate::record::RecordLift::<T, _>::new(inputs.next().unwrap().downcast::<T>()?);
Ok(EdgeHandle::new_record::<T>(std::sync::Arc::new(node) as std::sync::Arc<ErasedRecordNode>))
},
}
}
/// The extract bridge row for `T`: a record wire into a plain consumer.
pub fn record_extract_entry<T: Clone + Send + Sync + 'static>() -> RegistryEntry {
RegistryEntry {
io: NodeIOTypes::new(concrete!(Context), concrete!(T), vec![record_edge_type::<T>()]),
constructor: |inputs| {
if inputs.len() != 1 {
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
}
let mut inputs = inputs.into_iter();
let edge = inputs.next().unwrap();
let layout = edge.layout().ok_or(ConstructionError::MissingLayout)?.clone();
let node = crate::record::RecordExtract::<T, _>::new(edge.downcast_record::<T>()?, &layout);
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<T>>))
},
}
}
/// The lend-lift bridge row for `T`: a lending producer onto a record wire.
pub fn record_lift_lend_entry<T: Clone + Send + Sync + 'static>() -> RegistryEntry {
RegistryEntry {
io: NodeIOTypes::new(concrete!(Context), record_type::<T>(), vec![lend_edge_type::<T>()]),
constructor: |inputs| {
if inputs.len() != 1 {
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
}
let mut inputs = inputs.into_iter();
let node = crate::record::RecordLiftLend::<T, _>::new(inputs.next().unwrap().downcast_lend::<T>()?);
Ok(EdgeHandle::new_record::<T>(std::sync::Arc::new(node) as std::sync::Arc<ErasedRecordNode>))
},
}
}
/// The lend-extract bridge row for `T`: a record wire into a lend consumer.
pub fn record_extract_lend_entry<T: Clone + Send + Sync + 'static>() -> RegistryEntry {
RegistryEntry {
io: NodeIOTypes::new(concrete!(Context), ref_type::<T>(), vec![record_edge_type::<T>()]),
constructor: |inputs| {
if inputs.len() != 1 {
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
}
let mut inputs = inputs.into_iter();
let edge = inputs.next().unwrap();
let layout = edge.layout().ok_or(ConstructionError::MissingLayout)?.clone();
let node = crate::record::RecordExtractLend::<T, _>::new(edge.downcast_record::<T>()?, &layout);
Ok(EdgeHandle::new_ref(std::sync::Arc::new(node) as std::sync::Arc<ErasedLendNode<T>>))
},
}
}
pub fn construct(entry: &RegistryEntry, inputs: Vec<EdgeHandle>) -> Result<EdgeHandle, ConstructionError> {
if inputs.len() != entry.io.inputs.len() {
return Err(ConstructionError::Arity {

View File

@@ -41,6 +41,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let record = record_shape(parsed);
let routing = routing_io(parsed);
let flip = record_flip(parsed);
let record_skips_carrier = record.as_ref().is_some_and(|shape| shape.skips_carrier());
// Record nodes with a `_: ()` primary input have no carrier edge; the unit
// field stays visible in the metadata but claims no struct field.
@@ -139,6 +140,14 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
state
}
None if routing.is_some() => vec![quote!(pub(super) __layout: gcore::record::Layout)],
None if flip => {
let mut state = vec![quote!(pub(super) __layout: gcore::record::Layout), quote!(pub(super) __frame_bytes: usize)];
state.extend((0..struct_regular_fields.len()).map(|index| {
let slot = format_ident!("__in_{index}");
quote!(pub(super) #slot: gcore::record::Layout)
}));
state
}
None => Vec::new(),
};
@@ -260,7 +269,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let all_field_inits = data_inits.chain(regular_inits).chain(slot_init);
// Data fields may not implement Copy, PartialEq, etc., so only derive Debug and Clone
let struct_derives = if record.is_some() || routing.is_some() {
let struct_derives = if record.is_some() || routing.is_some() || flip {
quote!(#[derive(Debug, Clone)])
} else if data_fields.is_empty() && !async_source {
quote!(#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)])
@@ -289,16 +298,46 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
// offsets from the carrier layout; `new` cannot fill that state.
let routing_layout_param = routing.is_some().then(|| quote!(__layout: &gcore::record::Layout,)).into_iter();
let routing_layout_init = routing.is_some().then(|| quote!(__layout: __layout.clone(),)).into_iter();
let flip_layout_params = flip
.then(|| {
(0..struct_regular_fields.len()).map(|index| {
let slot = format_ident!("__in_{index}");
quote!(#slot: &gcore::record::Layout,)
})
})
.into_iter()
.flatten();
let flip_layout_inits = flip
.then(|| {
(0..struct_regular_fields.len()).map(|index| {
let slot = format_ident!("__in_{index}");
quote!(#slot: #slot.clone(),)
})
})
.into_iter()
.flatten();
let flip_prelude = flip
.then(|| {
quote! {
let __layout = gcore::record::Layout::default().with_writes(0, gcore::record::element_dims::<#slot_value_type>(), &[]);
let __frame_bytes = __layout.frame_bytes();
}
})
.into_iter();
let flip_output_inits = flip.then(|| quote!(__layout, __frame_bytes,)).into_iter();
let new_impl = match record.is_none() {
true => quote! {
#[automatically_derived]
impl<'n, #(#struct_generic_params,)*> #struct_name<#(#struct_type_params,)*>
{
#[allow(clippy::too_many_arguments)]
pub fn new(#(#new_args,)* #(#routing_layout_param)*) -> Self {
pub fn new(#(#new_args,)* #(#routing_layout_param)* #(#flip_layout_params)*) -> Self {
#(#flip_prelude)*
Self {
#(#all_field_inits,)*
#(#routing_layout_init)*
#(#flip_layout_inits)*
#(#flip_output_inits)*
}
}
}
@@ -662,6 +701,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
};
let skips_carrier = record.as_ref().is_some_and(|shape| shape.skips_carrier());
let routing = routing_io(parsed);
let flip = record_flip(parsed);
let snapshot_ctx = async_fn && matches!(&parsed.input.ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "CtxSnapshot"));
let mut ctx_bounds: Vec<TokenStream2> = match ctx_param {
@@ -755,7 +795,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
generics.insert(0, quote!(#lifetime));
impl_generics.insert(0, quote!(#lifetime));
}
if routing.is_some() || record.is_some() {
if routing.is_some() || record.is_some() || flip {
impl_generics.insert(0, quote!('__record));
}
if derive_routing {
@@ -768,6 +808,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let output_type = &parsed.output_type;
let trait_output = match (&record, &routing) {
(Some(_), _) | (None, Some(_)) => syn::parse_quote!(#core_types::record::RecordValue<'__record>),
(None, None) if flip => syn::parse_quote!(#core_types::record::RecordValue<'__record>),
(None, None) => slot_value_type(&parsed.output_type),
};
let raw_lazy = matches!(kernel_kind(&parsed.output_type), KernelKind::Poll(_));
@@ -868,6 +909,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
ParsedFieldType::Regular(RegularParsedField { ty, .. }) if routing_source(ty) => {
quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>)
}
ParsedFieldType::Regular(_) if flip => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>),
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #ty>),
ParsedFieldType::Node(NodeParsedField { output_type, .. }) if routing_source(output_type) => match derives {
true => quote!(#node_generic: for<'__derived> #core_types::record::DerivedRecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>),
@@ -933,6 +975,16 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let name = &field.pat_ident.ident;
match &field.ty {
ParsedFieldType::Regular(_) if record.is_some() && !skips_carrier && index == 0 => quote!(),
ParsedFieldType::Regular(RegularParsedField { ty, .. }) if flip => {
let slot = format_ident!("__in_{index}");
quote! {
let #name = match __cell.eval_input(#index, &self.#name, __input) {
Ok(value) => value,
Err(interrupt) => return interrupt.into(),
};
let #name: #ty = unsafe { #core_types::record::read_element(self.#slot.rec(&#name)) };
}
}
ParsedFieldType::Regular(_) => quote! {
let #name = match __cell.eval_input(#index, &self.#name, __input) {
Ok(value) => value,
@@ -1223,10 +1275,41 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
__cell.finish(__value)
}
});
let flip_tail = flip.then(|| {
let kernel_value = match kernel_kind(&parsed.output_type) {
KernelKind::Interrupt(_) => quote! {
match #kernel_call {
Ok(value) => value,
Err(interrupt) => return interrupt.into(),
}
},
_ => quote!(#kernel_call),
};
quote! {
let __kernel_value = #kernel_value;
let mut __value = #core_types::record::RecordValue::zeroed();
let __dst = match self.__frame_bytes {
0 => __value.as_mut_ptr(),
__bytes => #core_types::record::stack::push(__bytes),
};
let __written = unsafe { #core_types::record::write_element(__dst, __kernel_value, #core_types::context::ExtractArena::arena(__input)) };
if self.__frame_bytes != 0 {
#core_types::record::stack::pop(__dst);
__value = #core_types::record::RecordValue::spilled(unsafe { #core_types::record::Rec::new(__dst.cast_const()) });
}
match __written {
Some(()) => __cell.finish(__value),
None => #core_types::gpoll::GPoll::Error(::std::boxed::Box::new(#core_types::gpoll::GraphError {
kind: #core_types::gpoll::ErrorKind::ArenaExhausted,
trace: ::std::vec::Vec::new(),
})),
}
}
});
let eval_tail = match (async_fn, future_kernel) {
(false, false) => match record_tail {
Some(tail) => tail,
None => lift,
None => flip_tail.unwrap_or(lift),
},
(true, _) => {
let kernel_value_names: Vec<&Ident> = kernel_fields.iter().map(|field| &field.pat_ident.ident).collect();
@@ -1280,13 +1363,13 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
Some(shape) if shape.skips_carrier() => {
vec![quote!(#ctx_ident: #core_types::context::ExtractArena<ArenaRef = &'__record #core_types::arena::Arena>)]
}
None if derive_routing => {
None if derive_routing || flip => {
vec![quote!(#ctx_ident: #core_types::context::ExtractArena<ArenaRef = &'__record #core_types::arena::Arena>)]
}
_ => Vec::new(),
};
let record_layout_impl = match record.is_some() || routing.is_some() {
let record_layout_impl = match record.is_some() || routing.is_some() || flip {
true => quote! {
fn layout(&self) -> Option<&#core_types::record::Layout> {
Some(&self.__layout)
@@ -1615,6 +1698,41 @@ pub(crate) struct RoutingIo {
pub(crate) generic: Ident,
}
/// Whether a plain node's lowering flips onto record wires: sync,
/// 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 record_flip(parsed: &ParsedNodeFn) -> bool {
if record_shape(parsed).is_some() || has_record_io(parsed) || routing_io(parsed).is_some() {
return false;
}
if parsed.is_async || is_source_kernel(&parsed.output_type) {
return false;
}
if parsed.attributes.batch.is_some() || parsed.attributes.shader_node.is_some() {
return false;
}
if matches!(kernel_kind(&parsed.output_type), KernelKind::Poll(_)) {
return false;
}
if matches!(slot_value_type(&parsed.output_type), Type::Reference(_)) {
return false;
}
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
let concrete = parsed.fn_generics.iter().all(|param| match param {
GenericParam::Type(type_param) => Some(&type_param.ident) == ctx_ident.as_ref(),
GenericParam::Lifetime(_) => false,
GenericParam::Const(_) => false,
});
if !concrete {
return false;
}
parsed.fields.iter().all(|field| match &field.ty {
ParsedFieldType::Regular(RegularParsedField { lend, .. }) => lend.is_none(),
ParsedFieldType::Node(_) => false,
})
}
pub(crate) fn routing_io(parsed: &ParsedNodeFn) -> Option<RoutingIo> {
if has_record_io(parsed) || parsed.is_async {
return None;
@@ -1821,6 +1939,9 @@ fn entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, data_field_generic
if routing_io(parsed).is_some() {
return routing_entries_tokens(parsed, struct_name, regular_fields);
}
if record_flip(parsed) {
return flip_entries_tokens(parsed, struct_name, regular_fields);
}
let Some(rows) = implementation_rows(parsed, regular_fields) else {
return quote!();
};
@@ -1903,6 +2024,71 @@ fn entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, data_field_generic
}
}
/// The registry rows of a flipped plain node: every wire is a record wire,
/// inputs resolve their layouts off the claimed handles, and the output is an
/// element-only record of the kernel's return type.
fn flip_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 {
let Some(rows) = implementation_rows(parsed, regular_fields) else {
return quote!();
};
let rows: Vec<&Vec<Type>> = rows.iter().filter(|row| row.iter().all(|ty| !type_disqualifies(ty))).collect();
if rows.is_empty() {
return quote!();
}
let output = slot_value_type(&parsed.output_type);
if type_disqualifies(&output) {
return quote!();
}
let fn_name = &parsed.fn_name;
let entries_name = format_ident!("{}_entries", fn_name);
let arity = regular_fields.len();
let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect();
let entries = rows.iter().map(|row| {
let input_types = row.iter().map(|ty| quote!(gcore::registry::record_edge_type::<#ty>()));
let downcasts = names.iter().zip(row.iter()).enumerate().map(|(index, (name, ty))| {
let handle = format_ident!("__handle_{index}");
let layout = format_ident!("__layout_{index}");
quote! {
let #handle = inputs.next().unwrap();
let Some(#layout) = #handle.layout().cloned() else {
return Err(gcore::registry::ConstructionError::MissingLayout);
};
let #name = #handle.downcast_record::<#ty>()?;
}
});
let layout_args = (0..arity).map(|index| {
let layout = format_ident!("__layout_{index}");
quote!(&#layout,)
});
quote! {
gcore::registry::RegistryEntry {
io: gcore::registry::NodeIOTypes::new(
gcore::concrete!(gcore::context::ContextImpl<'static>),
gcore::registry::record_type::<#output>(),
vec![#(#input_types),*],
),
constructor: |inputs| {
if inputs.len() != #arity {
return Err(gcore::registry::ConstructionError::Arity { expected: #arity, got: inputs.len() });
}
let mut inputs = inputs.into_iter();
#(#downcasts)*
let __node = #struct_name::new(#(#names,)* #(#layout_args)*);
Ok(gcore::registry::EdgeHandle::new_record::<#output>(::std::sync::Arc::new(__node) as ::std::sync::Arc<gcore::registry::ErasedRecordNode>))
},
}
}
});
quote! {
pub fn #entries_name() -> ::std::vec::Vec<gcore::registry::RegistryEntry> {
vec![#(#entries),*]
}
}
}
/// The registry row of a routing node: one instance covers every element,
/// sources claim generic record edges, and the constructor wraps each source
/// in its union translation and stores the union as the node's layout.