Unify the layout metadata model and collapse the single-row entry emitters

This commit is contained in:
Dennis Kobert
2026-08-12 13:11:48 +00:00
parent b6f73b0691
commit 83fbaca6f2
4 changed files with 246 additions and 277 deletions
+34 -15
View File
@@ -249,20 +249,34 @@ pub fn empty_layout() -> &'static Layout {
/// calls it over the proto graph instead. /// calls it over the proto graph instead.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct LayoutMeta { pub struct LayoutMeta {
/// Whether the node derives its layout from a carrier input; `false` writes /// Input indices whose layouts union to form the base: `[]` writes a fresh
/// a fresh record from `Layout::default`. /// record, `[i]` derives from a single carrier, `[i, j, ..]` unions routing
pub carrier: bool, /// sources.
/// The output element: a concrete write, or carried through from the carrier. pub sources: Vec<u8>,
/// Attributes read from each input. Unused by [`fold`](LayoutMeta::fold);
/// recorded for later compiler analysis (read-offset resolution, per-name
/// cache dependencies, residency).
pub reads: Vec<InputReads>,
/// The output element: a concrete write, or carried through from the base.
pub element: ElementSpec, pub element: ElementSpec,
/// The attributes the node writes at its acting level. /// The attributes the node writes at its acting level.
pub writes: Vec<FieldWrite>, pub writes: Vec<FieldWrite>,
/// The attributes removed from the carrier's layout, as `(name, level)`. /// The attributes removed from the base layout, as `(name, level)`.
pub removes: Vec<(&'static str, u8)>, pub removes: Vec<(&'static str, u8)>,
/// The depth change the node applies to its carrier: `0` for elementwise and /// The depth change the node applies: `0` for elementwise and flip nodes,
/// flip nodes, `+1` for a creator, `-1` for a reducer. /// `+1` for a creator, `-1` for a reducer.
pub level_delta: i8, pub level_delta: i8,
} }
/// The attributes a node reads from one input, recorded on [`LayoutMeta`] for
/// later compiler analysis. A read and a write of an attribute carry the same
/// [`FieldWrite`] descriptor; the direction is the position on the node.
#[derive(Clone, Debug)]
pub struct InputReads {
pub input: u8,
pub reads: Vec<FieldWrite>,
}
/// Where a node's output element comes from, for [`LayoutMeta`]. /// Where a node's output element comes from, for [`LayoutMeta`].
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum ElementSpec { pub enum ElementSpec {
@@ -273,16 +287,21 @@ pub enum ElementSpec {
} }
impl LayoutMeta { impl LayoutMeta {
/// Folds the node's output layout from its carrier's, reproducing what the /// Folds the node's output layout from its inputs', reproducing what the
/// node's constructor derives at wiring. `carrier` is the carrier input's /// node's constructor derives at wiring. `inputs` is indexed by proto-input
/// layout, or `None` when the node writes a fresh record. /// position; [`sources`](LayoutMeta::sources) selects the base layouts, which
pub fn fold(&self, carrier: Option<&Layout>) -> Layout { /// union (empty writes a fresh record).
let carrier = carrier.filter(|_| self.carrier); pub fn fold(&self, inputs: &[Option<&Layout>]) -> Layout {
let base = carrier.map_or_else(Layout::default, |c| c.without(&self.removes)); let sources: Vec<&Layout> = self.sources.iter().map(|&i| inputs[i as usize].expect("layout fold source input has no layout")).collect();
let depth = (carrier.map_or(0, |c| c.depth) as i8 + self.level_delta).max(0) as u8; let base = match sources.as_slice() {
[] => Layout::default(),
sources => Layout::union(sources),
}
.without(&self.removes);
let depth = (base.depth as i8 + self.level_delta).max(0) as u8;
let element = match &self.element { let element = match &self.element {
ElementSpec::Concrete(element) => *element, ElementSpec::Concrete(element) => *element,
ElementSpec::Carried => carrier.map_or_else(ElementWrite::default, |c| c.element), ElementSpec::Carried => base.element,
}; };
base.with_writes(depth, element, &self.writes) base.with_writes(depth, element, &self.writes)
} }
+16 -1
View File
@@ -1747,10 +1747,25 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
Some(ty) => quote!(#core_types::record::ElementSpec::Concrete(#core_types::record::element_write::<#ty>())), Some(ty) => quote!(#core_types::record::ElementSpec::Concrete(#core_types::record::element_write::<#ty>())),
None => quote!(#core_types::record::ElementSpec::Carried), None => quote!(#core_types::record::ElementSpec::Carried),
}; };
let sources = if carrier_present { quote!(::std::vec![0u8]) } else { quote!(::std::vec![]) };
let reads_meta: Vec<TokenStream2> = regular_fields
.iter()
.enumerate()
.filter(|(_, field)| !field.attribute_reads.is_empty())
.map(|(index, field)| {
let descs = field.attribute_reads.iter().map(|read| {
let marker = &read.marker;
quote!(#core_types::record::FieldWrite::of::<#marker>(0))
});
let index = index as u8;
quote!(#core_types::record::InputReads { input: #index, reads: ::std::vec![#(#descs),*] })
})
.collect();
let layout_meta_def = quote! { let layout_meta_def = quote! {
#vis fn #layout_meta_fn() -> #core_types::record::LayoutMeta { #vis fn #layout_meta_fn() -> #core_types::record::LayoutMeta {
#core_types::record::LayoutMeta { #core_types::record::LayoutMeta {
carrier: #carrier_present, sources: #sources,
reads: ::std::vec![#(#reads_meta),*],
element: #element_spec, element: #element_spec,
writes: ::std::vec![#(#write_descs),*], writes: ::std::vec![#(#write_descs),*],
removes: ::std::vec![#(#remove_pairs),*], removes: ::std::vec![#(#remove_pairs),*],
+182 -261
View File
@@ -8,10 +8,8 @@ pub(crate) fn entries_tokens(parsed: &ParsedNodeFn, class: &Class, struct_name:
return quote!(); return quote!();
} }
match class { match class {
Class::RecordIo(_) => record_entries_tokens(parsed, struct_name, regular_fields),
Class::Routing(_) => routing_entries_tokens(parsed, struct_name, regular_fields),
Class::Flip { .. } => flip_entries_tokens(parsed, struct_name, regular_fields), Class::Flip { .. } => flip_entries_tokens(parsed, struct_name, regular_fields),
Class::Opaque => record_opaque_entries_tokens(parsed, struct_name, regular_fields), Class::RecordIo(_) | Class::Routing(_) | Class::Opaque => single_row_entries(parsed, class, struct_name, regular_fields),
} }
} }
@@ -57,6 +55,7 @@ fn flip_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_field
let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect(); 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 node_underscores: Vec<TokenStream2> = regular_fields.iter().map(|_| quote!(_)).collect();
let carrier_present = flip_carrier(parsed); let carrier_present = flip_carrier(parsed);
let sources = if carrier_present { quote!(::std::vec![0u8]) } else { quote!(::std::vec![]) };
// Shorthand associated types in the output only resolve against the // Shorthand associated types in the output only resolve against the
// generics' bounds, so rows name the output through a bounded alias. Only // generics' bounds, so rows name the output through a bounded alias. Only
@@ -146,7 +145,8 @@ fn flip_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_field
Some(quote! { Some(quote! {
gcore::registry::RegistryEntry { gcore::registry::RegistryEntry {
layout_meta: Some(gcore::record::LayoutMeta { layout_meta: Some(gcore::record::LayoutMeta {
carrier: #carrier_present, sources: #sources,
reads: ::std::vec::Vec::new(),
element: gcore::record::ElementSpec::Concrete(gcore::record::element_write::<#row_output>()), element: gcore::record::ElementSpec::Concrete(gcore::record::element_write::<#row_output>()),
writes: ::std::vec::Vec::new(), writes: ::std::vec::Vec::new(),
removes: ::std::vec::Vec::new(), removes: ::std::vec::Vec::new(),
@@ -182,303 +182,223 @@ fn flip_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_field
} }
} }
/// The registry row of a routing node: one instance covers every element, /// Which record wire an input claims and how its value is recovered. Base slots
/// sources claim generic record edges, and the constructor wraps each source /// are the record edges whose layouts form the output; value slots are record
/// in its union translation and stores the union as the node's layout. /// edges read for their layout; plain and lazy slots are ordinary edges.
fn routing_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 { enum SlotKind {
let Some(routing) = routing_io(parsed) else { /// A generic record edge whose element is only known at runtime; the runtime
return quote!(); /// type is captured for the output wrap or the union.
}; BaseGeneric(String),
let is_source = |field: &ParsedField| { /// A concrete record carrier read for its layout.
let ty = match &field.ty { BaseConcrete(Type),
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type, /// A concrete record edge read for its layout only.
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty, Value(Type),
}; /// A plain value edge.
matches!(ty, Type::Path(path) if path.path.get_ident() == Some(&routing.generic)) Plain(Type),
}; /// A lazy node edge.
let values_concrete = regular_fields.iter().filter(|field| !is_source(field)).all(|field| { Lazy(Type),
let (ty, lend) = match &field.ty { }
ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) => (ty, lend.is_some()),
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => (output_type, false), impl SlotKind {
}; fn is_base(&self) -> bool {
!contains_open_generic(parsed, ty) && (lend || !type_disqualifies(ty)) matches!(self, SlotKind::BaseGeneric(_) | SlotKind::BaseConcrete(_))
});
if !values_concrete {
return quote!();
} }
}
/// The single registry row shared by record-io, routing, and opaque nodes: one
/// instance covers the wire, each input's edge type and downcast follow its
/// slot, and the output layout folds from the base slots.
fn single_row_entries(parsed: &ParsedNodeFn, class: &Class, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 {
let fn_name = &parsed.fn_name; let fn_name = &parsed.fn_name;
let entries_name = format_ident!("{}_entries", fn_name); let lend = |field: &ParsedField| matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. }));
let arity = regular_fields.len();
let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect();
let token_name = routing.generic.to_string();
let input_types = regular_fields.iter().map(|field| { let slots: Vec<SlotKind> = match class {
if is_source(field) { Class::RecordIo(shape) => {
return quote!(gcore::registry::generic_record_edge_type(#token_name)); let carrier_in_fields = !shape.skips_carrier();
} regular_fields
match &field.ty { .iter()
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(gcore::registry::record_edge_type::<#ty>()), .enumerate()
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(gcore::registry::edge_type::<#output_type>()), .map(|(index, field)| {
} let ParsedFieldType::Regular(RegularParsedField { ty, .. }) = &field.ty else {
}); unreachable!("record nodes take no lazy inputs")
let source_layouts: Vec<Ident> = regular_fields
.iter()
.enumerate()
.filter(|(_, field)| is_source(field))
.map(|(index, _)| format_ident!("__layout_{index}"))
.collect();
let downcasts = regular_fields.iter().enumerate().map(|(index, field)| {
let name = &field.pat_ident.ident;
if is_source(field) {
let layout = format_ident!("__layout_{index}");
let handle = format_ident!("__handle_{index}");
let ty = format_ident!("__ty_{index}");
return quote! {
let #handle = inputs.next().unwrap();
let #ty = #handle.ty().clone();
let Some(#layout) = #handle.layout().cloned() else {
return Err(gcore::registry::ConstructionError::MissingLayout);
};
let #name = #handle.downcast_erased::<gcore::registry::ErasedRecordNode>(#ty.clone())?;
};
}
match &field.ty {
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => {
let handle = format_ident!("__handle_{index}");
let layout = format_ident!("__in_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>()?; if carrier_in_fields && index == 0 {
} return match &shape.carrier {
} RecordCarrier::Token(token) => SlotKind::BaseGeneric(token.to_string()),
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#output_type>()?;), RecordCarrier::Read(carrier_ty) => SlotKind::BaseConcrete(carrier_ty.clone()),
} RecordCarrier::None => unreachable!(),
}); };
let value_layout_args = regular_fields
.iter()
.enumerate()
.filter(|(_, field)| !is_source(field) && matches!(field.ty, ParsedFieldType::Regular(_)))
.map(|(index, _)| {
let layout = format_ident!("__in_layout_{index}");
quote!(&#layout,)
});
let source_wraps = regular_fields.iter().enumerate().filter(|(_, field)| is_source(field)).map(|(index, field)| {
let name = &field.pat_ident.ident;
let layout = format_ident!("__layout_{index}");
quote!(let #name = gcore::record::RecordSource::new(#name, &#layout, &__union);)
});
let first_source_ty = regular_fields
.iter()
.enumerate()
.find(|(_, field)| is_source(field))
.map(|(index, _)| format_ident!("__ty_{index}"))
.expect("routing nodes have a source");
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)))),
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(); match field.attribute_reads.is_empty() {
#(#downcasts)* false => SlotKind::Value(ty.clone()),
let __union = gcore::record::Layout::union(&[#(&#source_layouts),*]); true => SlotKind::Plain(ty.clone()),
#(#source_wraps)* }
let __node = #struct_name::new(#(#names,)* &__union, #(#value_layout_args)*); })
Ok(gcore::registry::EdgeHandle::new_erased( .collect()
::std::sync::Arc::new(__node) as ::std::sync::Arc<gcore::registry::ErasedRecordNode>,
#first_source_ty,
))
},
}]
} }
} Class::Routing(routing) => {
} let is_source = |field: &ParsedField| {
let ty = match &field.ty {
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type,
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty,
};
matches!(ty, Type::Path(path) if path.path.get_ident() == Some(&routing.generic))
};
regular_fields
.iter()
.map(|field| {
if is_source(field) {
return SlotKind::BaseGeneric(routing.generic.to_string());
}
match &field.ty {
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => SlotKind::Value(ty.clone()),
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => SlotKind::Lazy(output_type.clone()),
}
})
.collect()
}
Class::Opaque => {
let is_record = |field: &ParsedField| matches!(&field.ty, ParsedFieldType::Node(NodeParsedField { output_type, .. }) if is_record_value(output_type));
regular_fields
.iter()
.map(|field| {
if is_record(field) {
return SlotKind::BaseGeneric("T".to_string());
}
match &field.ty {
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => SlotKind::Plain(ty.clone()),
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => SlotKind::Lazy(output_type.clone()),
}
})
.collect()
}
Class::Flip { .. } => unreachable!("flip has its own multi-row emitter"),
};
fn record_opaque_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 { // Every non-base value/plain/lazy input must be concrete.
let is_record = |field: &ParsedField| matches!(&field.ty, ParsedFieldType::Node(NodeParsedField { output_type, .. }) if is_record_value(output_type)); let values_concrete = regular_fields.iter().zip(&slots).all(|(field, slot)| match slot {
let values_concrete = regular_fields.iter().filter(|field| !is_record(field)).all(|field| { SlotKind::BaseGeneric(_) | SlotKind::BaseConcrete(_) => true,
let (ty, lend) = match &field.ty { SlotKind::Value(ty) | SlotKind::Plain(ty) | SlotKind::Lazy(ty) => !contains_open_generic(parsed, ty) && (lend(field) || !type_disqualifies(ty)),
ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) => (ty, lend.is_some()),
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => (output_type, false),
};
!contains_open_generic(parsed, ty) && (lend || !type_disqualifies(ty))
}); });
if !values_concrete { if !values_concrete {
return quote!(); return quote!();
} }
let fn_name = &parsed.fn_name;
let entries_name = format_ident!("{}_entries", fn_name); let entries_name = format_ident!("{}_entries", fn_name);
let arity = regular_fields.len(); let arity = regular_fields.len();
let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect(); let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect();
let input_types = regular_fields.iter().map(|field| { let input_types = slots.iter().map(|slot| match slot {
if is_record(field) { SlotKind::BaseGeneric(name) => quote!(gcore::registry::generic_record_edge_type(#name)),
return quote!(gcore::registry::generic_record_edge_type("T")); SlotKind::BaseConcrete(ty) | SlotKind::Value(ty) => quote!(gcore::registry::record_edge_type::<#ty>()),
} SlotKind::Plain(ty) | SlotKind::Lazy(ty) => quote!(gcore::registry::edge_type::<#ty>()),
match &field.ty {
ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => quote!(gcore::registry::edge_type::<#ty>()),
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(gcore::registry::edge_type::<#ty>()),
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(gcore::registry::edge_type::<#output_type>()),
}
}); });
let downcasts = regular_fields.iter().enumerate().map(|(index, field)| {
let name = &field.pat_ident.ident; let downcasts = names.iter().zip(&slots).enumerate().map(|(index, (name, slot))| {
if is_record(field) { let handle = format_ident!("__handle_{index}");
let layout = format_ident!("__layout_{index}"); let layout = format_ident!("__layout_{index}");
let handle = format_ident!("__handle_{index}"); let ty = format_ident!("__ty_{index}");
let ty = format_ident!("__ty_{index}"); match slot {
return quote! { SlotKind::BaseGeneric(_) | SlotKind::BaseConcrete(_) => quote! {
let #handle = inputs.next().unwrap(); let #handle = inputs.next().unwrap();
let #ty = #handle.ty().clone(); let #ty = #handle.ty().clone();
let Some(#layout) = #handle.layout().cloned() else { let Some(#layout) = #handle.layout().cloned() else {
return Err(gcore::registry::ConstructionError::MissingLayout); return Err(gcore::registry::ConstructionError::MissingLayout);
}; };
let #name = #handle.downcast_erased::<gcore::registry::ErasedRecordNode>(#ty.clone())?; let #name = #handle.downcast_erased::<gcore::registry::ErasedRecordNode>(#ty.clone())?;
}; },
} SlotKind::Value(value_ty) => quote! {
match &field.ty { let #handle = inputs.next().unwrap();
ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;), let Some(#layout) = #handle.layout().cloned() else {
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;), return Err(gcore::registry::ConstructionError::MissingLayout);
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#output_type>()?;), };
let #name = #handle.downcast_record::<#value_ty>()?;
},
SlotKind::Plain(value_ty) | SlotKind::Lazy(value_ty) => quote!(let #name = inputs.next().unwrap().downcast::<#value_ty>()?;),
} }
}); });
let first_record = regular_fields.iter().position(|field| is_record(field)).expect("record-opaque nodes have a record input");
let record_layout = format_ident!("__layout_{first_record}");
let record_ty = format_ident!("__ty_{first_record}");
quote! { let base_indices: Vec<usize> = slots.iter().enumerate().filter(|(_, slot)| slot.is_base()).map(|(index, _)| index).collect();
pub fn #entries_name() -> ::std::vec::Vec<gcore::registry::RegistryEntry> { let value_indices: Vec<usize> = slots.iter().enumerate().filter(|(_, slot)| matches!(slot, SlotKind::Value(_))).map(|(index, _)| index).collect();
vec![gcore::registry::RegistryEntry { let value_layout_args = value_indices.iter().map(|index| {
layout_meta: None, let layout = format_ident!("__layout_{index}");
io: gcore::registry::NodeIOTypes::new( quote!(&#layout,)
gcore::concrete!(gcore::context::ContextImpl<'static>), });
gcore::Type::Record(Box::new(gcore::Type::Generic(::std::borrow::Cow::Borrowed("T")))),
vec![#(#input_types),*], let carried_meta = |sources: &[usize]| {
), let sources = sources.iter().map(|index| *index as u8);
constructor: |inputs| { quote! {
if inputs.len() != #arity { Some(gcore::record::LayoutMeta {
return Err(gcore::registry::ConstructionError::Arity { expected: #arity, got: inputs.len() }); sources: ::std::vec![#(#sources),*],
} reads: ::std::vec::Vec::new(),
let mut inputs = inputs.into_iter(); element: gcore::record::ElementSpec::Carried,
#(#downcasts)* writes: ::std::vec::Vec::new(),
let __node = #struct_name::new(#(#names,)* &#record_layout); removes: ::std::vec::Vec::new(),
Ok(gcore::registry::EdgeHandle::new_erased( level_delta: 0,
::std::sync::Arc::new(__node) as ::std::sync::Arc<gcore::registry::ErasedRecordNode>, })
#record_ty,
))
},
}]
} }
}
}
fn record_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 {
let Some(shape) = record_shape(parsed) else {
return quote!();
}; };
let carrier_in_fields = !shape.skips_carrier();
let values_concrete = regular_fields.iter().skip(carrier_in_fields as usize).all(|field| match &field.ty {
ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) => !contains_open_generic(parsed, ty) && (lend.is_some() || !type_disqualifies(ty)),
_ => false,
});
if !values_concrete {
return quote!();
}
let fn_name = &parsed.fn_name; let (io_output, wrap, prelude, new_layout_args, layout_meta) = match class {
let entries_name = format_ident!("{}_entries", fn_name); Class::RecordIo(shape) => {
let arity = regular_fields.len(); let carrier_arg = (!shape.skips_carrier()).then(|| quote!(&__layout_0,));
let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect(); let (io_output, wrap) = match (&shape.carrier, &shape.element_write) {
let reading_secondaries = reading_secondary_indices(regular_fields, &shape); (RecordCarrier::Token(token), _) => {
let token_name = token.to_string();
let input_types = regular_fields.iter().enumerate().map(|(index, field)| { (
let ParsedFieldType::Regular(RegularParsedField { ty, .. }) = &field.ty else { quote!(gcore::Type::Record(Box::new(gcore::Type::Generic(::std::borrow::Cow::Borrowed(#token_name))))),
unreachable!("record nodes take no lazy inputs") quote!(Ok(gcore::registry::EdgeHandle::new_erased(::std::sync::Arc::new(__node) as ::std::sync::Arc<gcore::registry::ErasedRecordNode>, __ty_0))),
}; )
if carrier_in_fields && index == 0 {
return match &shape.carrier {
RecordCarrier::Token(token) => {
let name = token.to_string();
quote!(gcore::registry::generic_record_edge_type(#name))
} }
RecordCarrier::Read(carrier_ty) => quote!(gcore::registry::record_edge_type::<#carrier_ty>()), (_, Some(element)) => (
RecordCarrier::None => unreachable!(), quote!(gcore::registry::record_type::<#element>()),
quote!(Ok(gcore::registry::EdgeHandle::new_record::<#element>(::std::sync::Arc::new(__node)))),
),
(_, None) => unreachable!("non-token record nodes write an element"),
}; };
let layout_meta_fn = format_ident!("{}_layout_meta", fn_name);
(io_output, wrap, quote!(), quote!(#carrier_arg #(#value_layout_args)*), quote!(Some(self::#layout_meta_fn())))
} }
match field.attribute_reads.is_empty() { Class::Routing(routing) => {
true => quote!(gcore::registry::edge_type::<#ty>()), let token_name = routing.generic.to_string();
false => quote!(gcore::registry::record_edge_type::<#ty>()), let source_layouts = base_indices.iter().map(|index| format_ident!("__layout_{index}"));
} let source_wraps = base_indices.iter().map(|index| {
}); let name = names[*index];
let downcasts = regular_fields.iter().enumerate().map(|(index, field)| { let layout = format_ident!("__layout_{index}");
let name = &field.pat_ident.ident; quote!(let #name = gcore::record::RecordSource::new(#name, &#layout, &__union);)
let ParsedFieldType::Regular(RegularParsedField { ty, .. }) = &field.ty else { });
unreachable!("record nodes take no lazy inputs") let first_source_ty = format_ident!("__ty_{}", base_indices[0]);
}; let prelude = quote! {
if carrier_in_fields && index == 0 { let __union = gcore::record::Layout::union(&[#(&#source_layouts),*]);
return quote! { #(#source_wraps)*
let __carrier_handle = inputs.next().unwrap();
let __carrier_ty = __carrier_handle.ty().clone();
let Some(__carrier_layout) = __carrier_handle.layout().cloned() else {
return Err(gcore::registry::ConstructionError::MissingLayout);
};
let #name = __carrier_handle.downcast_erased::<gcore::registry::ErasedRecordNode>(__carrier_ty.clone())?;
}; };
}
if !field.attribute_reads.is_empty() {
let layout_local = format_ident!("__in_layout_{index}");
return quote! {
let __in_handle = inputs.next().unwrap();
let __in_ty = __in_handle.ty().clone();
let Some(#layout_local) = __in_handle.layout().cloned() else {
return Err(gcore::registry::ConstructionError::MissingLayout);
};
let #name = __in_handle.downcast_erased::<gcore::registry::ErasedRecordNode>(__in_ty)?;
};
}
quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;)
});
let wire_layout_arg = carrier_in_fields.then(|| quote!(&__carrier_layout,)).into_iter();
let input_layout_args = reading_secondaries.iter().map(|index| {
let layout_local = format_ident!("__in_layout_{index}");
quote!(&#layout_local,)
});
let (io_output, construct_output) = match (&shape.carrier, &shape.element_write) {
(RecordCarrier::Token(token), _) => {
let name = token.to_string();
( (
quote!(gcore::Type::Record(Box::new(gcore::Type::Generic(::std::borrow::Cow::Borrowed(#name))))), quote!(gcore::Type::Record(Box::new(gcore::Type::Generic(::std::borrow::Cow::Borrowed(#token_name))))),
quote!(Ok(gcore::registry::EdgeHandle::new_erased( quote!(Ok(gcore::registry::EdgeHandle::new_erased(::std::sync::Arc::new(__node) as ::std::sync::Arc<gcore::registry::ErasedRecordNode>, #first_source_ty))),
::std::sync::Arc::new(__node) as ::std::sync::Arc<gcore::registry::ErasedRecordNode>, prelude,
__carrier_ty, quote!(&__union, #(#value_layout_args)*),
))), carried_meta(&base_indices),
) )
} }
(_, Some(element)) => ( Class::Opaque => {
quote!(gcore::registry::record_type::<#element>()), let first_record = base_indices[0];
quote!(Ok(gcore::registry::EdgeHandle::new_record::<#element>(::std::sync::Arc::new(__node)))), let record_layout = format_ident!("__layout_{first_record}");
), let record_ty = format_ident!("__ty_{first_record}");
(_, None) => unreachable!("non-token record nodes write an element"), (
quote!(gcore::Type::Record(Box::new(gcore::Type::Generic(::std::borrow::Cow::Borrowed("T"))))),
quote!(Ok(gcore::registry::EdgeHandle::new_erased(::std::sync::Arc::new(__node) as ::std::sync::Arc<gcore::registry::ErasedRecordNode>, #record_ty))),
quote!(),
quote!(&#record_layout),
carried_meta(&[first_record]),
)
}
Class::Flip { .. } => unreachable!("flip has its own multi-row emitter"),
}; };
let layout_meta_fn = format_ident!("{}_layout_meta", fn_name);
quote! { quote! {
pub fn #entries_name() -> ::std::vec::Vec<gcore::registry::RegistryEntry> { pub fn #entries_name() -> ::std::vec::Vec<gcore::registry::RegistryEntry> {
vec![gcore::registry::RegistryEntry { vec![gcore::registry::RegistryEntry {
layout_meta: Some(self::#layout_meta_fn()), layout_meta: #layout_meta,
io: gcore::registry::NodeIOTypes::new( io: gcore::registry::NodeIOTypes::new(
gcore::concrete!(gcore::context::ContextImpl<'static>), gcore::concrete!(gcore::context::ContextImpl<'static>),
#io_output, #io_output,
@@ -490,8 +410,9 @@ fn record_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fie
} }
let mut inputs = inputs.into_iter(); let mut inputs = inputs.into_iter();
#(#downcasts)* #(#downcasts)*
let __node = #struct_name::new(#(#names,)* #(#wire_layout_arg)* #(#input_layout_args)*); #prelude
#construct_output let __node = #struct_name::new(#(#names,)* #new_layout_args);
#wrap
}, },
}] }]
} }
+14
View File
@@ -221,6 +221,20 @@ mod tests {
} }
} }
#[test]
fn layout_meta_folds_to_construction() {
let base = f64_layout(&[]);
let carried = multiply_opacity_layout(&base);
assert_eq!(multiply_opacity_layout_meta().fold(&[Some(&base)]), multiply_opacity_layout(&base));
assert_eq!(multiply_opacity_layout_meta().fold(&[Some(&carried)]), multiply_opacity_layout(&carried));
assert_eq!(measure_layout_meta().fold(&[Some(&base)]), measure_layout(&base));
assert_eq!(strip_opacity_layout_meta().fold(&[Some(&carried)]), strip_opacity_layout(&carried));
assert_eq!(relength_layout_meta().fold(&[Some(&base)]), relength_layout(&base));
assert_eq!(transfer_opacity_layout_meta().fold(&[Some(&base), None]), transfer_opacity_layout(&base));
assert_eq!(source_opacity_layout_meta().fold(&[]), source_opacity_layout());
}
#[test] #[test]
fn defaults_then_modify_then_stack() { fn defaults_then_modify_then_stack() {
let arena = Arena::new(1024).unwrap(); let arena = Arena::new(1024).unwrap();