mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Split the node macro codegen into classify, entries, and metadata modules
This commit is contained in:
File diff suppressed because it is too large
Load Diff
715
node-graph/node-macro/src/codegen/classify.rs
Normal file
715
node-graph/node-macro/src/codegen/classify.rs
Normal file
@@ -0,0 +1,715 @@
|
||||
use super::*;
|
||||
|
||||
/// How a record node's primary input lowers.
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum RecordCarrier {
|
||||
/// `_: ()`: no carrier edge, the kernel writes a fresh record.
|
||||
None,
|
||||
/// An unbounded generic returned in the element position: the element
|
||||
/// bytes carry through the copy plan and the kernel sees `ElToken`.
|
||||
Token(Ident),
|
||||
/// An element type read at offset 0, monomorphized per its
|
||||
/// implementations list where generic.
|
||||
Read(Type),
|
||||
}
|
||||
|
||||
/// The record io of a node fn: how the carrier lowers, the element write,
|
||||
/// and the markers written and removed. Present exactly when the signature
|
||||
/// declares attribute reads or writes in a shape the record tier supports;
|
||||
/// malformed record io is reported by validation and generates no node impl.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RecordShape {
|
||||
pub(crate) carrier: RecordCarrier,
|
||||
pub(crate) element_write: Option<Type>,
|
||||
pub(crate) write_markers: Vec<Type>,
|
||||
pub(crate) removes: Vec<Type>,
|
||||
}
|
||||
|
||||
impl RecordShape {
|
||||
pub(crate) fn skips_carrier(&self) -> bool {
|
||||
matches!(self.carrier, RecordCarrier::None)
|
||||
}
|
||||
|
||||
pub(crate) fn carries_element(&self) -> bool {
|
||||
self.element_write.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// The record-tier lowering a node fn resolves to. Exactly one class per node,
|
||||
/// computed once by [`analyze`]; every downstream fragment reads the class
|
||||
/// instead of recomputing the classification predicates.
|
||||
pub(crate) enum Class {
|
||||
RecordIo(RecordShape),
|
||||
Routing(RoutingIo),
|
||||
Flip { carrier: bool },
|
||||
Opaque,
|
||||
}
|
||||
|
||||
/// The effect/return axis of a node's kernel, resolved once from the signature.
|
||||
/// Orthogonal to [`Class`]: it selects the eval tail (finish / merge / spawn)
|
||||
/// and the kernel signature wrapping across every class.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(crate) enum Dialect {
|
||||
Sync,
|
||||
Interrupt,
|
||||
Poll,
|
||||
AsyncFn,
|
||||
Future,
|
||||
FutureInterrupt,
|
||||
}
|
||||
|
||||
pub(crate) fn dialect(parsed: &ParsedNodeFn) -> Dialect {
|
||||
if parsed.is_async {
|
||||
return Dialect::AsyncFn;
|
||||
}
|
||||
match kernel_kind(&parsed.output_type) {
|
||||
KernelKind::Plain => Dialect::Sync,
|
||||
KernelKind::Interrupt(_) => Dialect::Interrupt,
|
||||
KernelKind::Poll(_) => Dialect::Poll,
|
||||
KernelKind::Future(_) => Dialect::Future,
|
||||
KernelKind::FutureInterrupt(_) => Dialect::FutureInterrupt,
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of classifying a node fn. A node with no supported lowering
|
||||
/// (an async node with lazy inputs, malformed record io, or a signature no
|
||||
/// class accepts) yields `None` and generates a struct and metadata but no
|
||||
/// `Node` impl.
|
||||
pub(crate) struct NodeModel {
|
||||
pub(crate) class: Class,
|
||||
pub(crate) dialect: Dialect,
|
||||
}
|
||||
|
||||
pub(crate) fn analyze(parsed: &ParsedNodeFn) -> Option<NodeModel> {
|
||||
if parsed.is_async && parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) {
|
||||
return None;
|
||||
}
|
||||
let class = if let Some(shape) = record_shape(parsed) {
|
||||
Class::RecordIo(shape)
|
||||
} else if has_record_io(parsed) {
|
||||
return None;
|
||||
} else if let Some(routing) = routing_io(parsed) {
|
||||
Class::Routing(routing)
|
||||
} else if record_flip(parsed) {
|
||||
Class::Flip { carrier: flip_carrier(parsed) }
|
||||
} else if record_opaque(parsed) {
|
||||
Class::Opaque
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
Some(NodeModel { class, dialect: dialect(parsed) })
|
||||
}
|
||||
|
||||
/// The per-field binding role, resolved once per regular field from the node
|
||||
/// class and field shape. Drives the eval bindings and the lazy-edge input
|
||||
/// types. The `lend` and `reads` axes stay field properties the value arms of
|
||||
/// `kernel_params`/`call_args`/`value_args` consult, since they cross roles.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum InputRole {
|
||||
RecordCarrier,
|
||||
FlipCarrier,
|
||||
ReadingSecondary,
|
||||
LendBorrow,
|
||||
RecordValue,
|
||||
PlainValue,
|
||||
DeriveRoutingSource,
|
||||
OpaqueRecordEdge,
|
||||
FlipRawLazyEdge,
|
||||
FlipLazy,
|
||||
RawLazy,
|
||||
Lazy,
|
||||
}
|
||||
|
||||
impl InputRole {
|
||||
/// A role that copies an element out of a record edge, so its record frame
|
||||
/// must be reclaimed after the read. The step lowering wraps such a bind in
|
||||
/// `mark`/`rewind`, making the stack discipline structural rather than
|
||||
/// hand-threaded through each read-out arm.
|
||||
pub(crate) fn reads_out(self) -> bool {
|
||||
matches!(self, InputRole::ReadingSecondary | InputRole::RecordValue)
|
||||
}
|
||||
}
|
||||
|
||||
/// The tail form of a node's eval, selected from its class and dialect: forward
|
||||
/// the kernel's own record, assemble a record (io or flip carrier), or spawn a
|
||||
/// source and lift its completion.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum Tail {
|
||||
Forward,
|
||||
Record,
|
||||
Flip,
|
||||
SpawnAsyncFn,
|
||||
SpawnFuture,
|
||||
}
|
||||
|
||||
/// One statement group of a node's `eval` body, lowered in order: the input
|
||||
/// binds first (one per input), then the numeric clamps, then the tail that
|
||||
/// assembles the output record and closes the dialect.
|
||||
pub(crate) enum EvalStep<'a> {
|
||||
Bind(usize, &'a ParsedField),
|
||||
Clamp(&'a ParsedField),
|
||||
Tail(Tail),
|
||||
}
|
||||
|
||||
/// Whether the signature declares record-tier attribute io: value-input reads
|
||||
/// or return-tuple writes. Reads on lazy inputs belong to the record lowering
|
||||
/// of the flip class instead.
|
||||
pub(crate) fn has_record_io(parsed: &ParsedNodeFn) -> bool {
|
||||
let value_reads = parsed
|
||||
.fields
|
||||
.iter()
|
||||
.any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Regular(_)));
|
||||
value_reads || record_writes(&slot_value_type(&parsed.output_type)).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn has_lazy_reads(parsed: &ParsedNodeFn) -> bool {
|
||||
parsed
|
||||
.fields
|
||||
.iter()
|
||||
.any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Node(_)))
|
||||
}
|
||||
|
||||
/// The value inputs of a routing node (every regular field that is not a
|
||||
/// routing source), with their indices into the regular fields.
|
||||
pub(crate) fn routing_value_indices(regular_fields: &[&ParsedField], routing: &RoutingIo) -> Vec<usize> {
|
||||
regular_fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, field)| match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => !matches!(ty, Type::Path(path) if path.path.get_ident() == Some(&routing.generic)),
|
||||
ParsedFieldType::Node(_) => false,
|
||||
})
|
||||
.map(|(index, _)| index)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The lazy inputs declaring attribute reads, with their indices into the
|
||||
/// unit-skipped regular fields.
|
||||
pub(crate) fn lazy_read_fields<'a>(regular_fields: &[&'a ParsedField]) -> Vec<(usize, &'a ParsedField)> {
|
||||
regular_fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, field)| matches!(field.ty, ParsedFieldType::Node(_)) && !field.attribute_reads.is_empty())
|
||||
.map(|(index, field)| (index, *field))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The indices (into the unit-skipped regular fields) of value inputs whose
|
||||
/// reads resolve against their own wire rather than the carrier's.
|
||||
pub(crate) fn reading_secondary_indices(regular_fields: &[&ParsedField], shape: &RecordShape) -> Vec<usize> {
|
||||
regular_fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(index, field)| !field.attribute_reads.is_empty() && (shape.skips_carrier() || *index != 0))
|
||||
.map(|(index, _)| index)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Every attribute read in field order with the owning field's index, flat so
|
||||
/// read slots are numbered across inputs.
|
||||
pub(crate) fn field_reads<'a>(regular_fields: &[&'a ParsedField]) -> Vec<(usize, &'a AttributeRead)> {
|
||||
regular_fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(index, field)| field.attribute_reads.iter().map(move |read| (index, read)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Substitutes bare generic idents with their row-assigned types.
|
||||
pub(crate) fn substitute_ident_types(ty: &Type, assignments: &[(Ident, Type)]) -> Type {
|
||||
struct Subst<'a> {
|
||||
assignments: &'a [(Ident, Type)],
|
||||
}
|
||||
|
||||
impl VisitMut for Subst<'_> {
|
||||
fn visit_type_mut(&mut self, ty: &mut Type) {
|
||||
if let Type::Path(path) = ty
|
||||
&& path.qself.is_none()
|
||||
&& let Some(ident) = path.path.get_ident()
|
||||
&& let Some((_, replacement)) = self.assignments.iter().find(|(generic, _)| generic == ident)
|
||||
{
|
||||
*ty = replacement.clone();
|
||||
return;
|
||||
}
|
||||
syn::visit_mut::visit_type_mut(self, ty);
|
||||
}
|
||||
}
|
||||
|
||||
let mut ty = ty.clone();
|
||||
Subst { assignments }.visit_type_mut(&mut ty);
|
||||
ty
|
||||
}
|
||||
|
||||
/// Replaces the routing generic in a derive-routing kernel's return type with
|
||||
/// the routing record value, since the kernel's edges rebind to '__record.
|
||||
pub(crate) fn substitute_routing_record(output: &Type, generic: &Ident, core_types: &TokenStream2) -> Type {
|
||||
struct Subst<'a> {
|
||||
generic: &'a Ident,
|
||||
replacement: Type,
|
||||
}
|
||||
|
||||
impl VisitMut for Subst<'_> {
|
||||
fn visit_type_mut(&mut self, ty: &mut Type) {
|
||||
if let Type::Path(path) = ty
|
||||
&& path.qself.is_none()
|
||||
&& path.path.get_ident() == Some(self.generic)
|
||||
{
|
||||
*ty = self.replacement.clone();
|
||||
return;
|
||||
}
|
||||
syn::visit_mut::visit_type_mut(self, ty);
|
||||
}
|
||||
}
|
||||
|
||||
let mut ty = output.clone();
|
||||
let mut subst = Subst {
|
||||
generic,
|
||||
replacement: syn::parse_quote!(#core_types::record::RecordValue<'__record>),
|
||||
};
|
||||
subst.visit_type_mut(&mut ty);
|
||||
ty
|
||||
}
|
||||
|
||||
pub(crate) fn inject_attr_lifetimes(output: &Type) -> Option<Type> {
|
||||
struct Injector {
|
||||
changed: bool,
|
||||
}
|
||||
|
||||
impl VisitMut for Injector {
|
||||
fn visit_path_segment_mut(&mut self, segment: &mut syn::PathSegment) {
|
||||
if segment.ident == "Attr"
|
||||
&& let PathArguments::AngleBracketed(args) = &mut segment.arguments
|
||||
&& !args.args.iter().any(|arg| matches!(arg, GenericArgument::Lifetime(_)))
|
||||
{
|
||||
args.args.insert(0, GenericArgument::Lifetime(Lifetime::new("'__attr", proc_macro2::Span::call_site())));
|
||||
self.changed = true;
|
||||
}
|
||||
syn::visit_mut::visit_path_segment_mut(self, segment);
|
||||
}
|
||||
}
|
||||
|
||||
let mut ty = output.clone();
|
||||
let mut injector = Injector { changed: false };
|
||||
injector.visit_type_mut(&mut ty);
|
||||
injector.changed.then_some(ty)
|
||||
}
|
||||
|
||||
pub(crate) fn contains_open_generic(parsed: &ParsedNodeFn, ty: &Type) -> bool {
|
||||
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
|
||||
parsed
|
||||
.fn_generics
|
||||
.iter()
|
||||
.any(|param| matches!(param, GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() && type_contains_ident(ty, &type_param.ident)))
|
||||
}
|
||||
|
||||
pub(crate) fn unbounded_generic(parsed: &ParsedNodeFn, ty: &Type) -> Option<Ident> {
|
||||
let ident = bare_ident(ty)?.clone();
|
||||
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
|
||||
parsed
|
||||
.fn_generics
|
||||
.iter()
|
||||
.find(|param| matches!(param, GenericParam::Type(type_param) if type_param.ident == ident && type_param.bounds.is_empty() && Some(&type_param.ident) != ctx_ident.as_ref()))?;
|
||||
if let Some(where_clause) = &parsed.where_clause
|
||||
&& tokens_contain_ident(where_clause.to_token_stream(), &ident)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(ident)
|
||||
}
|
||||
|
||||
pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option<RecordShape> {
|
||||
let value = match kernel_kind(&parsed.output_type) {
|
||||
KernelKind::Plain => parsed.output_type.clone(),
|
||||
KernelKind::Interrupt(inner) => inner,
|
||||
_ => return None,
|
||||
};
|
||||
let writes = record_writes(&value);
|
||||
let has_reads = parsed
|
||||
.fields
|
||||
.iter()
|
||||
.any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Regular(_)));
|
||||
if !has_reads && writes.is_none() {
|
||||
return None;
|
||||
}
|
||||
if parsed.is_async || parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) {
|
||||
return None;
|
||||
}
|
||||
let reads_well_placed = parsed.fields.iter().all(|field| {
|
||||
field.attribute_reads.is_empty() || (!field.is_data_field && matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: None, .. })))
|
||||
});
|
||||
if !reads_well_placed {
|
||||
return None;
|
||||
}
|
||||
let carrier_field = parsed.fields.first()?;
|
||||
if carrier_field.is_data_field {
|
||||
return None;
|
||||
}
|
||||
let ParsedFieldType::Regular(RegularParsedField { ty, lend: None, implementations, .. }) = &carrier_field.ty else {
|
||||
return None;
|
||||
};
|
||||
let carrier = match ty {
|
||||
Type::Tuple(tuple) if tuple.elems.is_empty() => RecordCarrier::None,
|
||||
ty => match implementations.is_empty().then(|| unbounded_generic(parsed, ty)).flatten() {
|
||||
Some(token) => RecordCarrier::Token(token),
|
||||
None => {
|
||||
if contains_open_generic(parsed, ty) {
|
||||
return None;
|
||||
}
|
||||
RecordCarrier::Read(ty.clone())
|
||||
}
|
||||
},
|
||||
};
|
||||
let (element, write_markers, removes) = match writes {
|
||||
Some(RecordWrites { element, markers, removes }) => (element, markers, removes),
|
||||
None => (value, Vec::new(), Vec::new()),
|
||||
};
|
||||
let element_write = match &carrier {
|
||||
RecordCarrier::Token(token) => match bare_ident(&element) {
|
||||
Some(ident) if ident == token => None,
|
||||
_ => return None,
|
||||
},
|
||||
_ => {
|
||||
if contains_open_generic(parsed, &element) {
|
||||
return None;
|
||||
}
|
||||
Some(element)
|
||||
}
|
||||
};
|
||||
if matches!(carrier, RecordCarrier::None) && !removes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(RecordShape {
|
||||
carrier,
|
||||
element_write,
|
||||
write_markers,
|
||||
removes,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn is_poll_kernel(output: &Type) -> bool {
|
||||
matches!(kernel_kind(output), KernelKind::Poll(_))
|
||||
}
|
||||
|
||||
/// A routing family: an unbounded generic shared by lazy inputs (and
|
||||
/// optionally the first parameter) and returned whole, instantiated at
|
||||
/// `RecordValue` so opaque records flow through the kernel. Detected only
|
||||
/// when the family's fields carry no implementations lists, so the existing
|
||||
/// per-type row spelling keeps its meaning.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RoutingIo {
|
||||
pub(crate) generic: Ident,
|
||||
}
|
||||
|
||||
/// Whether a flipped node's primary input is a carrier: the first parameter
|
||||
/// after the context, when it is an owned or lent value input. A carrier's
|
||||
/// fields pass through to the output; every production layout is element-only
|
||||
/// until attribute adoption, so the copy plan is empty and behavior is
|
||||
/// unchanged. Async kernels carry fields per eval around the slot (only the
|
||||
/// element crosses the future boundary), so their carrier must be owned: the
|
||||
/// future captures the element by value.
|
||||
pub(crate) fn flip_carrier(parsed: &ParsedNodeFn) -> bool {
|
||||
if !record_flip(parsed) {
|
||||
return false;
|
||||
}
|
||||
let Some(first) = parsed.fields.first() else { return false };
|
||||
if first.is_data_field {
|
||||
return false;
|
||||
}
|
||||
let ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) = &first.ty else {
|
||||
return false;
|
||||
};
|
||||
if matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty()) {
|
||||
return false;
|
||||
}
|
||||
let async_kernel = parsed.is_async || matches!(kernel_kind(&parsed.output_type), KernelKind::Future(_) | KernelKind::FutureInterrupt(_));
|
||||
!(async_kernel && lend.is_some())
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
// Shader nodes flip like any value node: the kernel doubles as the
|
||||
// shader body on the spirv target, but the struct and Node impl are
|
||||
// std-gated, so the record machinery never reaches the shader build.
|
||||
if parsed.attributes.batch.is_some() || parsed.attributes.plain {
|
||||
return false;
|
||||
}
|
||||
if type_disqualifies(&slot_value_type(&parsed.output_type)) {
|
||||
return false;
|
||||
}
|
||||
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
|
||||
for param in &parsed.fn_generics {
|
||||
match param {
|
||||
GenericParam::Type(type_param) if Some(&type_param.ident) == ctx_ident.as_ref() => {}
|
||||
// Registry rows assign a generic by unifying a field's type with
|
||||
// the row's, so a generic without an extractable position keeps
|
||||
// the plain lowering. A `skip_impl` node's rows are hand-written
|
||||
// with explicit types, so no extractable position is needed.
|
||||
GenericParam::Type(type_param) => {
|
||||
let extractable = parsed.fields.iter().filter(|field| !field.is_data_field).any(|field| {
|
||||
let ty = match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty,
|
||||
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type,
|
||||
};
|
||||
generic_extractable(ty, &type_param.ident)
|
||||
});
|
||||
if !extractable && !parsed.attributes.skip_impl {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
GenericParam::Lifetime(_) | GenericParam::Const(_) => return false,
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Whether unifying a value of `field_ty`'s shape can bind `generic`: the
|
||||
/// generic sits bare or under path type arguments, the shapes
|
||||
/// [`generic_assignment`] walks.
|
||||
pub(crate) fn generic_extractable(field_ty: &Type, generic: &Ident) -> bool {
|
||||
match field_ty {
|
||||
Type::Path(path) if path.qself.is_none() && path.path.get_ident() == Some(generic) => true,
|
||||
Type::Path(path) => path.path.segments.iter().any(|segment| match &segment.arguments {
|
||||
PathArguments::AngleBracketed(args) => args.args.iter().any(|argument| match argument {
|
||||
GenericArgument::Type(inner) => generic_extractable(inner, generic),
|
||||
_ => false,
|
||||
}),
|
||||
_ => false,
|
||||
}),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Binds `generic` by unifying `field_ty` against `row_ty`: where the field
|
||||
/// names the generic, the row's corresponding subtree is the assignment.
|
||||
pub(crate) fn generic_assignment(field_ty: &Type, row_ty: &Type, generic: &Ident) -> Option<Type> {
|
||||
if matches!(field_ty, Type::Path(path) if path.qself.is_none() && path.path.get_ident() == Some(generic)) {
|
||||
return Some(row_ty.clone());
|
||||
}
|
||||
let (Type::Path(field_path), Type::Path(row_path)) = (field_ty, row_ty) else {
|
||||
return None;
|
||||
};
|
||||
let field_segment = field_path.path.segments.last()?;
|
||||
let row_segment = row_path.path.segments.last()?;
|
||||
let (PathArguments::AngleBracketed(field_args), PathArguments::AngleBracketed(row_args)) = (&field_segment.arguments, &row_segment.arguments) else {
|
||||
return None;
|
||||
};
|
||||
field_args.args.iter().zip(row_args.args.iter()).find_map(|(field_arg, row_arg)| match (field_arg, row_arg) {
|
||||
(GenericArgument::Type(field_inner), GenericArgument::Type(row_inner)) => generic_assignment(field_inner, row_inner, generic),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn is_record_value(ty: &Type) -> bool {
|
||||
matches!(ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "RecordValue"))
|
||||
}
|
||||
|
||||
/// Whether a kernel operates on whole records: it names `RecordValue` in its
|
||||
/// output, receives raw record edges paired with the node's layout, and
|
||||
/// takes on the record APIs' unsafe contracts itself.
|
||||
pub(crate) fn record_opaque(parsed: &ParsedNodeFn) -> bool {
|
||||
is_record_value(&slot_value_type(&parsed.output_type))
|
||||
}
|
||||
|
||||
pub(crate) fn routing_io(parsed: &ParsedNodeFn) -> Option<RoutingIo> {
|
||||
if has_record_io(parsed) || parsed.is_async {
|
||||
return None;
|
||||
}
|
||||
if !matches!(kernel_kind(&parsed.output_type), KernelKind::Plain | KernelKind::Interrupt(_)) {
|
||||
return None;
|
||||
}
|
||||
let value = slot_value_type(&parsed.output_type);
|
||||
let Type::Path(path) = &value else { return None };
|
||||
let ident = path.path.get_ident()?.clone();
|
||||
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
|
||||
parsed
|
||||
.fn_generics
|
||||
.iter()
|
||||
.find(|param| matches!(param, GenericParam::Type(type_param) if type_param.ident == ident && type_param.bounds.is_empty() && Some(&type_param.ident) != ctx_ident.as_ref()))?;
|
||||
if let Some(where_clause) = &parsed.where_clause
|
||||
&& tokens_contain_ident(where_clause.to_token_stream(), &ident)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut sources = 0;
|
||||
for (index, field) in parsed.fields.iter().enumerate() {
|
||||
match &field.ty {
|
||||
ParsedFieldType::Node(NodeParsedField {
|
||||
output_type,
|
||||
input_type,
|
||||
implementations,
|
||||
}) => {
|
||||
if bare_ident(output_type) == Some(&ident) {
|
||||
// A source forwards its whole record opaquely; declared
|
||||
// reads contradict that and are rejected by validation.
|
||||
if !implementations.is_empty() || type_contains_ident(input_type, &ident) || !field.attribute_reads.is_empty() {
|
||||
return None;
|
||||
}
|
||||
sources += 1;
|
||||
} else if type_contains_ident(output_type, &ident) || type_contains_ident(input_type, &ident) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, implementations, lend, .. }) => {
|
||||
if bare_ident(ty) == Some(&ident) {
|
||||
if index != 0 || field.is_data_field || !implementations.is_empty() || lend.is_some() {
|
||||
return None;
|
||||
}
|
||||
sources += 1;
|
||||
} else if type_contains_ident(ty, &ident) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(sources > 0).then(|| RoutingIo { generic: ident })
|
||||
}
|
||||
|
||||
pub(crate) fn bare_ident(ty: &Type) -> Option<&Ident> {
|
||||
let Type::Path(path) = ty else { return None };
|
||||
path.path.get_ident()
|
||||
}
|
||||
|
||||
pub(crate) fn tokens_contain_ident(tokens: TokenStream2, ident: &Ident) -> bool {
|
||||
tokens.into_iter().any(|token| match token {
|
||||
proc_macro2::TokenTree::Ident(candidate) => &candidate == ident,
|
||||
proc_macro2::TokenTree::Group(group) => tokens_contain_ident(group.stream(), ident),
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn slot_value_type(output: &Type) -> Type {
|
||||
match kernel_kind(output) {
|
||||
KernelKind::Plain => output.clone(),
|
||||
KernelKind::Poll(inner) | KernelKind::Interrupt(inner) => inner,
|
||||
KernelKind::Future(payload) | KernelKind::FutureInterrupt(payload) => match kernel_kind(&payload) {
|
||||
KernelKind::Poll(inner) | KernelKind::Interrupt(inner) => inner,
|
||||
_ => payload,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_source_kernel(output: &Type) -> bool {
|
||||
matches!(kernel_kind(output), KernelKind::Future(_) | KernelKind::FutureInterrupt(_))
|
||||
}
|
||||
|
||||
pub(crate) enum KernelKind {
|
||||
Plain,
|
||||
Interrupt(Type),
|
||||
Poll(Type),
|
||||
Future(Type),
|
||||
FutureInterrupt(Type),
|
||||
}
|
||||
|
||||
pub(crate) fn source_future_payload(segment: &syn::PathSegment) -> Type {
|
||||
let PathArguments::AngleBracketed(args) = &segment.arguments else {
|
||||
return syn::parse_quote!(());
|
||||
};
|
||||
args.args
|
||||
.iter()
|
||||
.find_map(|argument| match argument {
|
||||
GenericArgument::Type(ty) => Some(ty.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_else(|| syn::parse_quote!(()))
|
||||
}
|
||||
|
||||
pub(crate) fn kernel_kind(output: &Type) -> KernelKind {
|
||||
let plain = || KernelKind::Plain;
|
||||
let Type::Path(path) = output else { return plain() };
|
||||
let Some(segment) = path.path.segments.last() else { return plain() };
|
||||
match segment.ident.to_string().as_str() {
|
||||
"GPoll" => {
|
||||
let PathArguments::AngleBracketed(args) = &segment.arguments else { return plain() };
|
||||
let inner = args.args.iter().find_map(|argument| match argument {
|
||||
GenericArgument::Type(ty) => Some(ty.clone()),
|
||||
_ => None,
|
||||
});
|
||||
inner.map(KernelKind::Poll).unwrap_or_else(plain)
|
||||
}
|
||||
"SourceFuture" => KernelKind::Future(source_future_payload(segment)),
|
||||
"Result" => {
|
||||
let PathArguments::AngleBracketed(args) = &segment.arguments else { return plain() };
|
||||
let mut types = args.args.iter().filter_map(|argument| match argument {
|
||||
GenericArgument::Type(ty) => Some(ty),
|
||||
_ => None,
|
||||
});
|
||||
let (Some(inner), Some(Type::Path(error_path))) = (types.next(), types.next()) else {
|
||||
return plain();
|
||||
};
|
||||
if error_path.path.segments.last().is_none_or(|segment| segment.ident != "Interrupt") {
|
||||
return plain();
|
||||
}
|
||||
if let Type::Path(inner_path) = inner
|
||||
&& let Some(inner_segment) = inner_path.path.segments.last()
|
||||
&& inner_segment.ident == "SourceFuture"
|
||||
{
|
||||
return KernelKind::FutureInterrupt(source_future_payload(inner_segment));
|
||||
}
|
||||
KernelKind::Interrupt(inner.clone())
|
||||
}
|
||||
_ => plain(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn context_param(parsed: &ParsedNodeFn) -> Option<&TypeParam> {
|
||||
let Type::Path(path) = &parsed.input.ty else {
|
||||
return None;
|
||||
};
|
||||
let ident = path.path.get_ident()?;
|
||||
parsed.fn_generics.iter().find_map(|param| match param {
|
||||
GenericParam::Type(type_param) if &type_param.ident == ident => Some(type_param),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn type_disqualifies(ty: &Type) -> bool {
|
||||
struct Disqualifier {
|
||||
found: bool,
|
||||
}
|
||||
|
||||
impl<'ast> Visit<'ast> for Disqualifier {
|
||||
fn visit_type_reference(&mut self, _: &'ast syn::TypeReference) {
|
||||
self.found = true;
|
||||
}
|
||||
|
||||
fn visit_type_impl_trait(&mut self, _: &'ast syn::TypeImplTrait) {
|
||||
self.found = true;
|
||||
}
|
||||
|
||||
fn visit_lifetime(&mut self, _: &'ast Lifetime) {
|
||||
self.found = true;
|
||||
}
|
||||
}
|
||||
|
||||
let mut visitor = Disqualifier { found: false };
|
||||
visitor.visit_type(ty);
|
||||
visitor.found
|
||||
}
|
||||
|
||||
pub(crate) fn desugar_extract_lifetime(bound: &TypeParamBound, core_types: &TokenStream2) -> TokenStream2 {
|
||||
let TypeParamBound::Trait(trait_bound) = bound else {
|
||||
return quote!(#bound);
|
||||
};
|
||||
let Some(segment) = trait_bound.path.segments.last() else {
|
||||
return quote!(#bound);
|
||||
};
|
||||
if segment.ident != "ExtractArena" {
|
||||
return quote!(#bound);
|
||||
}
|
||||
let PathArguments::AngleBracketed(args) = &segment.arguments else {
|
||||
return quote!(#bound);
|
||||
};
|
||||
if args.args.len() != 1 {
|
||||
return quote!(#bound);
|
||||
}
|
||||
let Some(GenericArgument::Lifetime(lifetime)) = args.args.first() else {
|
||||
return quote!(#bound);
|
||||
};
|
||||
quote!(#core_types::context::ExtractArena<ArenaRef = &#lifetime #core_types::arena::Arena>)
|
||||
}
|
||||
519
node-graph/node-macro/src/codegen/entries.rs
Normal file
519
node-graph/node-macro/src/codegen/entries.rs
Normal file
@@ -0,0 +1,519 @@
|
||||
use super::*;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{GenericParam, Ident, Type};
|
||||
|
||||
pub(crate) fn entries_tokens(parsed: &ParsedNodeFn, class: &Class, struct_name: &Ident, data_field_generic_idents: &[Ident], regular_fields: &[&ParsedField]) -> TokenStream2 {
|
||||
if !data_field_generic_idents.is_empty() {
|
||||
return quote!();
|
||||
}
|
||||
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::Opaque => record_opaque_entries_tokens(parsed, struct_name, regular_fields),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
let field_type = |field: &ParsedField| match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty.clone(),
|
||||
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type.clone(),
|
||||
};
|
||||
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
|
||||
let generic_positions: Option<Vec<(Ident, usize)>> = parsed
|
||||
.fn_generics
|
||||
.iter()
|
||||
.filter_map(|param| match param {
|
||||
GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() => Some(&type_param.ident),
|
||||
_ => None,
|
||||
})
|
||||
.map(|generic| {
|
||||
regular_fields
|
||||
.iter()
|
||||
.position(|field| generic_extractable(&field_type(field), generic))
|
||||
.map(|index| (generic.clone(), index))
|
||||
})
|
||||
.collect();
|
||||
let Some(generic_positions) = generic_positions else {
|
||||
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 node_underscores: Vec<TokenStream2> = regular_fields.iter().map(|_| quote!(_)).collect();
|
||||
|
||||
// Shorthand associated types in the output only resolve against the
|
||||
// generics' bounds, so rows name the output through a bounded alias. Only
|
||||
// output-reaching generics (directly or through a kept bound) may appear:
|
||||
// an unused alias parameter is an error.
|
||||
let candidate_params: Vec<&GenericParam> = parsed
|
||||
.fn_generics
|
||||
.iter()
|
||||
.filter(|param| matches!(param, GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref()))
|
||||
.collect();
|
||||
let param_ident = |param: &&GenericParam| match param {
|
||||
GenericParam::Type(type_param) => type_param.ident.clone(),
|
||||
_ => unreachable!("candidates are type parameters"),
|
||||
};
|
||||
let mut kept: Vec<bool> = candidate_params.iter().map(|param| type_contains_ident(&output, ¶m_ident(param))).collect();
|
||||
loop {
|
||||
let mut grew = false;
|
||||
for index in 0..candidate_params.len() {
|
||||
if kept[index] {
|
||||
continue;
|
||||
}
|
||||
let ident = param_ident(&candidate_params[index]);
|
||||
let mentioned = candidate_params.iter().zip(&kept).any(|(param, kept)| {
|
||||
*kept
|
||||
&& match param {
|
||||
GenericParam::Type(type_param) => type_param.bounds.iter().any(|bound| {
|
||||
let bound: Type = syn::parse_quote!(dyn #bound);
|
||||
type_contains_ident(&bound, &ident)
|
||||
}),
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
if mentioned {
|
||||
kept[index] = true;
|
||||
grew = true;
|
||||
}
|
||||
}
|
||||
if !grew {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let alias_params: Vec<&GenericParam> = candidate_params.iter().zip(&kept).filter(|(_, kept)| **kept).map(|(param, _)| *param).collect();
|
||||
let alias_param_idents: Vec<Ident> = alias_params.iter().map(|param| param_ident(param)).collect();
|
||||
let alias_param_tokens: Vec<TokenStream2> = alias_params.iter().map(|param| quote!(#param)).collect();
|
||||
let output_alias = format_ident!("__{}_output", fn_name);
|
||||
let alias_def = match alias_param_tokens.is_empty() {
|
||||
true => quote!(#[allow(non_camel_case_types)] type #output_alias = #output;),
|
||||
false => quote!(#[allow(non_camel_case_types, type_alias_bounds)] type #output_alias<#(#alias_param_tokens,)*> = #output;),
|
||||
};
|
||||
|
||||
let entries = rows.iter().filter_map(|row| {
|
||||
let assignments: Vec<(Ident, Type)> = generic_positions
|
||||
.iter()
|
||||
.map(|(generic, index)| generic_assignment(&field_type(regular_fields[*index]), &row[*index], generic).map(|assigned| (generic.clone(), assigned)))
|
||||
.collect::<Option<_>>()?;
|
||||
if type_disqualifies(&substitute_ident_types(&output, &assignments)) {
|
||||
return None;
|
||||
}
|
||||
let assignment_types: Vec<TokenStream2> = assignments.iter().map(|(_, ty)| quote!(#ty)).collect();
|
||||
let alias_arguments: Vec<TokenStream2> = assignments
|
||||
.iter()
|
||||
.filter(|(generic, _)| alias_param_idents.contains(generic))
|
||||
.map(|(_, ty)| quote!(#ty))
|
||||
.collect();
|
||||
let row_output = match alias_arguments.is_empty() {
|
||||
true => quote!(#output_alias),
|
||||
false => quote!(#output_alias<#(#alias_arguments),*>),
|
||||
};
|
||||
let assignment_types = assignment_types.iter();
|
||||
let turbofish = quote!(::<#(#node_underscores,)* #(#assignment_types,)*>);
|
||||
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,)
|
||||
});
|
||||
Some(quote! {
|
||||
gcore::registry::RegistryEntry {
|
||||
io: gcore::registry::NodeIOTypes::new(
|
||||
gcore::concrete!(gcore::context::ContextImpl<'static>),
|
||||
gcore::registry::record_type::<#row_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 #turbofish::new(#(#names,)* #(#layout_args)*);
|
||||
Ok(gcore::registry::EdgeHandle::new_record::<#row_output>(::std::sync::Arc::new(__node) as ::std::sync::Arc<gcore::registry::ErasedRecordNode>))
|
||||
},
|
||||
}
|
||||
})
|
||||
});
|
||||
let entries: Vec<TokenStream2> = entries.collect();
|
||||
if entries.is_empty() {
|
||||
return quote!();
|
||||
}
|
||||
|
||||
quote! {
|
||||
pub fn #entries_name() -> ::std::vec::Vec<gcore::registry::RegistryEntry> {
|
||||
#alias_def
|
||||
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.
|
||||
fn routing_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 {
|
||||
let Some(routing) = routing_io(parsed) else {
|
||||
return quote!();
|
||||
};
|
||||
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))
|
||||
};
|
||||
let values_concrete = regular_fields.iter().filter(|field| !is_source(field)).all(|field| {
|
||||
let (ty, lend) = match &field.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 {
|
||||
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 token_name = routing.generic.to_string();
|
||||
|
||||
let input_types = regular_fields.iter().map(|field| {
|
||||
if is_source(field) {
|
||||
return quote!(gcore::registry::generic_record_edge_type(#token_name));
|
||||
}
|
||||
match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(gcore::registry::record_edge_type::<#ty>()),
|
||||
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(gcore::registry::edge_type::<#output_type>()),
|
||||
}
|
||||
});
|
||||
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>()?;
|
||||
}
|
||||
}
|
||||
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#output_type>()?;),
|
||||
}
|
||||
});
|
||||
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 {
|
||||
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();
|
||||
#(#downcasts)*
|
||||
let __union = gcore::record::Layout::union(&[#(&#source_layouts),*]);
|
||||
#(#source_wraps)*
|
||||
let __node = #struct_name::new(#(#names,)* &__union, #(#value_layout_args)*);
|
||||
Ok(gcore::registry::EdgeHandle::new_erased(
|
||||
::std::sync::Arc::new(__node) as ::std::sync::Arc<gcore::registry::ErasedRecordNode>,
|
||||
#first_source_ty,
|
||||
))
|
||||
},
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn record_opaque_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 {
|
||||
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().filter(|field| !is_record(field)).all(|field| {
|
||||
let (ty, lend) = match &field.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 {
|
||||
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 input_types = regular_fields.iter().map(|field| {
|
||||
if is_record(field) {
|
||||
return quote!(gcore::registry::generic_record_edge_type("T"));
|
||||
}
|
||||
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;
|
||||
if is_record(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, lend: Some(_), .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;),
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;),
|
||||
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#output_type>()?;),
|
||||
}
|
||||
});
|
||||
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! {
|
||||
pub fn #entries_name() -> ::std::vec::Vec<gcore::registry::RegistryEntry> {
|
||||
vec![gcore::registry::RegistryEntry {
|
||||
io: gcore::registry::NodeIOTypes::new(
|
||||
gcore::concrete!(gcore::context::ContextImpl<'static>),
|
||||
gcore::Type::Record(Box::new(gcore::Type::Generic(::std::borrow::Cow::Borrowed("T")))),
|
||||
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,)* &#record_layout);
|
||||
Ok(gcore::registry::EdgeHandle::new_erased(
|
||||
::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 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 reading_secondaries = reading_secondary_indices(regular_fields, &shape);
|
||||
|
||||
let input_types = regular_fields.iter().enumerate().map(|(index, field)| {
|
||||
let ParsedFieldType::Regular(RegularParsedField { ty, .. }) = &field.ty else {
|
||||
unreachable!("record nodes take no lazy inputs")
|
||||
};
|
||||
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>()),
|
||||
RecordCarrier::None => unreachable!(),
|
||||
};
|
||||
}
|
||||
match field.attribute_reads.is_empty() {
|
||||
true => quote!(gcore::registry::edge_type::<#ty>()),
|
||||
false => quote!(gcore::registry::record_edge_type::<#ty>()),
|
||||
}
|
||||
});
|
||||
let downcasts = regular_fields.iter().enumerate().map(|(index, field)| {
|
||||
let name = &field.pat_ident.ident;
|
||||
let ParsedFieldType::Regular(RegularParsedField { ty, .. }) = &field.ty else {
|
||||
unreachable!("record nodes take no lazy inputs")
|
||||
};
|
||||
if carrier_in_fields && index == 0 {
|
||||
return quote! {
|
||||
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!(Ok(gcore::registry::EdgeHandle::new_erased(
|
||||
::std::sync::Arc::new(__node) as ::std::sync::Arc<gcore::registry::ErasedRecordNode>,
|
||||
__carrier_ty,
|
||||
))),
|
||||
)
|
||||
}
|
||||
(_, Some(element)) => (
|
||||
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"),
|
||||
};
|
||||
|
||||
quote! {
|
||||
pub fn #entries_name() -> ::std::vec::Vec<gcore::registry::RegistryEntry> {
|
||||
vec![gcore::registry::RegistryEntry {
|
||||
io: gcore::registry::NodeIOTypes::new(
|
||||
gcore::concrete!(gcore::context::ContextImpl<'static>),
|
||||
#io_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,)* #(#wire_layout_arg)* #(#input_layout_args)*);
|
||||
#construct_output
|
||||
},
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn implementation_rows(parsed: &ParsedNodeFn, regular_fields: &[&ParsedField]) -> Option<Vec<Vec<Type>>> {
|
||||
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
|
||||
let open_generics: Vec<&Ident> = parsed
|
||||
.fn_generics
|
||||
.iter()
|
||||
.filter_map(|param| match param {
|
||||
GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() => Some(&type_param.ident),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let candidates: Vec<Vec<Type>> = regular_fields
|
||||
.iter()
|
||||
.map(|field| match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, implementations, .. }) => match implementations.is_empty() {
|
||||
false => Some(implementations.iter().cloned().collect()),
|
||||
true => open_generics.iter().all(|generic| !crate::codegen::type_contains_ident(ty, generic)).then(|| vec![ty.clone()]),
|
||||
},
|
||||
ParsedFieldType::Node(NodeParsedField { output_type, implementations, .. }) => match implementations.is_empty() {
|
||||
false => Some(implementations.iter().map(|implementation| implementation.output.clone()).collect()),
|
||||
true => open_generics
|
||||
.iter()
|
||||
.all(|generic| !crate::codegen::type_contains_ident(output_type, generic))
|
||||
.then(|| vec![output_type.clone()]),
|
||||
},
|
||||
})
|
||||
.collect::<Option<_>>()?;
|
||||
|
||||
let row_count = candidates.iter().map(|types| types.len()).max().unwrap_or(1).max(1);
|
||||
Some((0..row_count).map(|row| candidates.iter().map(|types| types[row.min(types.len() - 1)].clone()).collect()).collect())
|
||||
}
|
||||
173
node-graph/node-macro/src/codegen/metadata.rs
Normal file
173
node-graph/node-macro/src/codegen/metadata.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
use super::*;
|
||||
|
||||
/// Generates strongly typed utilites to access inputs
|
||||
pub(crate) fn generate_node_input_references(
|
||||
parsed: &ParsedNodeFn,
|
||||
fn_generics: &[crate::GenericParam],
|
||||
field_idents: &[&PatIdent],
|
||||
core_types: &TokenStream2,
|
||||
identifier: &Ident,
|
||||
cfg: &TokenStream2,
|
||||
) -> TokenStream2 {
|
||||
let inputs_module_name = format_ident!("{}", parsed.struct_name.to_string().to_case(Case::Snake));
|
||||
|
||||
let mut generated_input_accessor = Vec::new();
|
||||
if !parsed.attributes.skip_impl {
|
||||
let (mut modified, mut generic_collector) = FilterUsedGenerics::new(fn_generics);
|
||||
|
||||
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();
|
||||
|
||||
// We only want the necessary generics.
|
||||
let used = generic_collector.filter_unnecessary_generics(&mut modified, &mut ty);
|
||||
// TODO: figure out a better name that doesn't conflict with so many types
|
||||
let struct_name = format_ident!("{}Input", input_ident.ident.to_string().to_case(Case::Pascal));
|
||||
let (fn_generic_params, phantom_data_declerations) = generate_phantom_data(used.iter());
|
||||
|
||||
// Only create structs with phantom data where necessary.
|
||||
generated_input_accessor.push(if phantom_data_declerations.is_empty() {
|
||||
quote! {
|
||||
pub struct #struct_name;
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
pub struct #struct_name <#(#used),*>{
|
||||
#(#phantom_data_declerations,)*
|
||||
}
|
||||
}
|
||||
});
|
||||
generated_input_accessor.push(quote! {
|
||||
impl <#(#used),*> #core_types::NodeInputDecleration for #struct_name <#(#fn_generic_params),*> {
|
||||
const INDEX: usize = #input_index;
|
||||
fn identifier() -> #core_types::ProtoNodeIdentifier {
|
||||
#inputs_module_name::IDENTIFIER.clone()
|
||||
}
|
||||
type Result = #ty;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
quote! {
|
||||
#cfg
|
||||
pub mod #inputs_module_name {
|
||||
use super::*;
|
||||
|
||||
/// The `ProtoNodeIdentifier` of this node without any generics attached to it
|
||||
pub const IDENTIFIER: #core_types::ProtoNodeIdentifier = #identifier();
|
||||
#(#generated_input_accessor)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// It is necessary to generate PhantomData for each fn generic to avoid compiler errors.
|
||||
pub(crate) fn generate_phantom_data<'a>(fn_generics: impl Iterator<Item = &'a crate::GenericParam>) -> (Vec<TokenStream2>, Vec<TokenStream2>) {
|
||||
let mut phantom_data_declerations = Vec::new();
|
||||
let mut fn_generic_params = Vec::new();
|
||||
|
||||
for fn_generic_param in fn_generics {
|
||||
let field_name = format_ident!("phantom_{}", phantom_data_declerations.len());
|
||||
|
||||
match fn_generic_param {
|
||||
crate::GenericParam::Lifetime(lifetime_param) => {
|
||||
let lifetime = &lifetime_param.lifetime;
|
||||
|
||||
fn_generic_params.push(quote! {#lifetime});
|
||||
phantom_data_declerations.push(quote! {#field_name: core::marker::PhantomData<&#lifetime ()>})
|
||||
}
|
||||
crate::GenericParam::Type(type_param) => {
|
||||
let generic_name = &type_param.ident;
|
||||
|
||||
fn_generic_params.push(quote! {#generic_name});
|
||||
phantom_data_declerations.push(quote! {#field_name: core::marker::PhantomData<#generic_name>});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(fn_generic_params, phantom_data_declerations)
|
||||
}
|
||||
|
||||
|
||||
/// Get only the necessary generics.
|
||||
struct FilterUsedGenerics {
|
||||
all: Vec<crate::GenericParam>,
|
||||
used: Vec<bool>,
|
||||
}
|
||||
|
||||
impl VisitMut for FilterUsedGenerics {
|
||||
fn visit_lifetime_mut(&mut self, used_lifetime: &mut Lifetime) {
|
||||
for (generic, used) in self.all.iter().zip(self.used.iter_mut()) {
|
||||
let crate::GenericParam::Lifetime(lifetime_param) = generic else { continue };
|
||||
if used_lifetime == &lifetime_param.lifetime {
|
||||
*used = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_path_mut(&mut self, path: &mut syn::Path) {
|
||||
for (index, (generic, used)) in self.all.iter().zip(self.used.iter_mut()).enumerate() {
|
||||
let crate::GenericParam::Type(type_param) = generic else { continue };
|
||||
if path.leading_colon.is_none() && !path.segments.is_empty() && path.segments[0].arguments.is_none() && path.segments[0].ident == type_param.ident {
|
||||
*used = true;
|
||||
// Sometimes the generics conflict with the type name so we rename the generics.
|
||||
path.segments[0].ident = format_ident!("G{index}");
|
||||
}
|
||||
}
|
||||
for mut el in Punctuated::pairs_mut(&mut path.segments) {
|
||||
self.visit_path_segment_mut(el.value_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FilterUsedGenerics {
|
||||
fn new(fn_generics: &[crate::GenericParam]) -> (Vec<crate::GenericParam>, Self) {
|
||||
let mut all_possible_generics = fn_generics.to_vec();
|
||||
// The 'n lifetime may also be needed; we must add it in
|
||||
all_possible_generics.insert(0, syn::GenericParam::Lifetime(syn::LifetimeParam::new(Lifetime::new("'n", proc_macro2::Span::call_site()))));
|
||||
|
||||
let modified = all_possible_generics
|
||||
.iter()
|
||||
.cloned()
|
||||
.enumerate()
|
||||
.map(|(index, mut generic)| {
|
||||
let crate::GenericParam::Type(type_param) = &mut generic else { return generic };
|
||||
// Sometimes the generics conflict with the type name so we rename the generics.
|
||||
type_param.ident = format_ident!("G{index}");
|
||||
generic
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let generic_collector = Self {
|
||||
used: vec![false; all_possible_generics.len()],
|
||||
all: all_possible_generics,
|
||||
};
|
||||
|
||||
(modified, generic_collector)
|
||||
}
|
||||
|
||||
fn used<'a>(&'a self, modified: &'a [crate::GenericParam]) -> impl Iterator<Item = &'a crate::GenericParam> {
|
||||
modified.iter().zip(&self.used).filter(|(_, used)| **used).map(move |(value, _)| value)
|
||||
}
|
||||
|
||||
fn filter_unnecessary_generics(&mut self, modified: &mut Vec<syn::GenericParam>, ty: &mut Type) -> Vec<syn::GenericParam> {
|
||||
self.used.fill(false);
|
||||
|
||||
// Find out which generics are necessary to support the node input
|
||||
self.visit_type_mut(ty);
|
||||
|
||||
// Sometimes generics may reference other generics. This is a non-optimal way of dealing with that.
|
||||
for _ in 0..=self.all.len() {
|
||||
for (index, item) in modified.iter_mut().enumerate() {
|
||||
if self.used[index] {
|
||||
self.visit_generic_param_mut(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.used(&*modified).cloned().collect()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user