use super::*; /// How a record node's primary input lowers: `None` writes a fresh record, /// `Token` carries the element bytes through as `ElToken`, `Read` reads a /// concrete element at offset 0, and `LazyToken` is a derive-routing carrier /// the kernel evaluates itself, returning the row token it received. The /// element and write set fold from the IR. #[derive(Clone)] pub(crate) enum RecordCarrier { None, Token, Read, LazyToken, } /// A well-formed record-io node: only the carrier form is retained, so /// [`skips_carrier`] can gate the fresh-record path. Malformed record io yields /// `None` from [`record_shape`] and generates no node impl. #[derive(Clone)] pub(crate) struct RecordShape { pub(crate) carrier: RecordCarrier, } impl RecordShape { pub(crate) fn skips_carrier(&self) -> bool { matches!(self.carrier, RecordCarrier::None) } } /// The effect/return axis of a node's kernel, resolved once from the signature. /// It selects the eval tail (finish / merge / spawn) and the kernel signature /// wrapping across every node kind. #[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 dialect of a node fn that lowers to a `Node` impl, or `None` when no /// lowering supports the signature (an async node with lazy inputs, malformed /// record io, or a shape no kind accepts). The kind itself is derived from the /// intent IR ([`crate::codegen::ir::node_kind`]); this only gates support. pub(crate) fn analyze(parsed: &ParsedNodeFn) -> Option { if parsed.is_async && parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) { return None; } let supported = if record_shape(parsed).is_some() { true } else if has_record_io(parsed) { return None; } else { routing_io(parsed).is_some() || record_flip(parsed) || record_opaque(parsed) || has_materialized_input(parsed) }; supported.then(|| dialect(parsed)) } /// 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 neither a /// routing source nor a ranked whole-list input), with their indices into the /// regular fields. pub(crate) fn routing_value_indices(regular_fields: &[&ParsedField], generic: &Ident) -> Vec { regular_fields .iter() .enumerate() .filter(|(_, field)| match &field.ty { ParsedFieldType::Regular(RegularParsedField { ty, list_levels, .. }) => *list_levels == 0 && !matches!(ty, Type::Path(path) if path.path.get_ident() == Some(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 input rather than the carrier's. pub(crate) fn reading_secondary_indices(regular_fields: &[&ParsedField], skips_carrier: bool) -> Vec { regular_fields .iter() .enumerate() .filter(|(index, field)| !field.attribute_reads.is_empty() && (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 inputs 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, lifetime: &str) -> Option { struct Injector<'a> { changed: bool, lifetime: &'a str, } 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(self.lifetime, 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, lifetime }; injector.visit_type_mut(&mut ty); injector.changed.then_some(ty) } /// Binds a `Lane` output to the kernel's subject lifetime, replacing whatever /// the author spelled: the lane borrows the materialized subject, not the arena. pub(crate) fn inject_lane_lifetime(output: &Type) -> Option { struct Injector { changed: bool, } impl VisitMut for Injector { fn visit_path_segment_mut(&mut self, segment: &mut syn::PathSegment) { if segment.ident == "Lane" && let PathArguments::AngleBracketed(args) = &mut segment.arguments { let kept: Vec = args.args.iter().filter(|arg| !matches!(arg, GenericArgument::Lifetime(_))).cloned().collect(); args.args = kept.into_iter().collect(); args.args.insert(0, GenericArgument::Lifetime(Lifetime::new("'__lane", 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 { 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 { let source = is_async_source(parsed); let value = match kernel_kind(&parsed.output_type) { KernelKind::Plain => parsed.output_type.clone(), KernelKind::Interrupt(inner) => inner, _ if source => slot_value_type(&parsed.output_type), _ => 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; } // An async source's slot stores the kernel's plain tuple; the per-eval lift // writes it through the claim, and the reads have no input to bind against. if source && (has_reads || writes.is_none()) { return None; } let carrier_field = parsed.fields.first()?; if carrier_field.is_data_field { return None; } // A first-field lazy carrier: the kernel evaluates the derived content // itself and returns its opaque row token beside the write set. let lazy_carrier = matches!(&carrier_field.ty, ParsedFieldType::Node(_)); // Lazy secondaries are consumed as plain elements; raw record inputs and // ranked outputs have no element binding here. let unsupported_lazy_secondary = |field: &ParsedField| match &field.ty { ParsedFieldType::Node(NodeParsedField { output_type, .. }) => is_served(output_type) || crate::codegen::ir::strip_ilist(output_type).1 > 0 || !field.attribute_reads.is_empty(), ParsedFieldType::Regular(_) => false, }; if parsed.fields.iter().skip(lazy_carrier as usize).any(|field| unsupported_lazy_secondary(field)) { return None; } let reads_well_placed = parsed.fields.iter().enumerate().all(|(index, field)| { field.attribute_reads.is_empty() || (lazy_carrier && index == 0) || (!field.is_data_field && matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: None, .. }))) }); if !reads_well_placed { return None; } if lazy_carrier { let ParsedFieldType::Node(NodeParsedField { output_type, .. }) = &carrier_field.ty else { unreachable!("guarded by the lazy_carrier match"); }; let token = unbounded_generic(parsed, output_type)?; let element = match writes { Some(RecordWrites { element, .. }) => element, None => value, }; if !matches!(bare_ident(&element), Some(ident) if ident == &token) { return None; } return Some(RecordShape { carrier: RecordCarrier::LazyToken }); } let ParsedFieldType::Regular(RegularParsedField { ty, lend: None, implementations, .. }) = &carrier_field.ty else { return None; }; // A gathered subject is never read as an element, so its generic stays open. let gathers = crate::codegen::ir::gathers_lane(parsed); let token = match ty { Type::Tuple(tuple) if tuple.elems.is_empty() => None, ty => match implementations.is_empty().then(|| unbounded_generic(parsed, ty)).flatten() { Some(token) => Some(token), None => { if !gathers && contains_open_generic(parsed, ty) { return None; } None } }, }; let carrier = match ty { Type::Tuple(tuple) if tuple.elems.is_empty() => RecordCarrier::None, _ if token.is_some() => RecordCarrier::Token, _ => RecordCarrier::Read, }; let (element, _, removes) = match writes { Some(RecordWrites { element, markers, removes }) => (element, markers, removes), None => (value, Vec::new(), Vec::new()), }; match &token { Some(token) => { if !matches!(bare_ident(&element), Some(ident) if ident == token) { return None; } } None => { if !gathers && contains_open_generic(parsed, &element) { return None; } } } if matches!(carrier, RecordCarrier::None) && !removes.is_empty() { return None; } // The byte-carried token never becomes a value, so it cannot cross a // future boundary. if source && matches!(carrier, RecordCarrier::Token) { return None; } Some(RecordShape { carrier }) } 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 any value input declares `IList` nesting, so the node materializes a ranked input. pub(crate) fn has_materialized_input(parsed: &ParsedNodeFn) -> bool { parsed .fields .iter() .any(|field| matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { list_levels, .. }) if *list_levels > 0)) } /// Whether a plain node's lowering flips onto record inputs. pub(crate) fn record_flip(parsed: &ParsedNodeFn) -> bool { if record_shape(parsed).is_some() || has_record_io(parsed) || routing_io(parsed).is_some() || record_opaque(parsed) { 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; } } // A named lifetime is the serving lifetime: input types substitute // its erased projection in registry and layout contexts. 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 { 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, }) } /// A whole-record position: the served proof a record-opaque kernel hands /// back, or the subject it receives without naming an element. pub(crate) fn is_served(ty: &Type) -> bool { matches!(ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "Served")) } /// Whether a kernel operates on whole records: it serves through the claim it /// was handed, receives raw record inputs paired with the node's layout, and /// takes on the record APIs' unsafe contracts itself. pub(crate) fn record_opaque(parsed: &ParsedNodeFn) -> bool { is_served(&slot_value_type(&parsed.output_type)) } pub(crate) fn routing_io(parsed: &ParsedNodeFn) -> Option { 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 routing_source_output(output_type, &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() } /// Whether a lazy input's declared output is the routing generic, at any /// `IList` nesting: the nesting is rank depth, not a distinct row type. pub(crate) fn routing_source_output(output_type: &Type, generic: &Ident) -> bool { let (stripped, _) = crate::codegen::ir::strip_ilist(output_type); bare_ident(&stripped) == Some(generic) } 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(_)) } /// A kernel whose value completes off the evaluation: an `async fn` or a /// `SourceFuture` return. Its slot persists a plain value across evaluations, /// so nothing it returns may borrow the arena. pub(crate) fn is_async_source(parsed: &ParsedNodeFn) -> bool { parsed.is_async || is_source_kernel(&parsed.output_type) } /// The type an async source's slot persists. A writing source's value outlives /// the evaluation, so every lifetime it names, including a bare `Attr`'s /// elided one, is `'static`. pub(crate) fn slot_static_type(output: &Type) -> Type { let value = slot_value_type(output); match record_writes(&value).is_some() { true => substitute_lifetimes(&inject_attr_lifetimes(&value, "'static").unwrap_or_else(|| value.clone()), "'static"), false => value, } } 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; } } let mut visitor = Disqualifier { found: false }; visitor.visit_type(ty); visitor.found } /// The input type with every named serving lifetime replaced: `'static` for /// registry, layout, and declaration contexts (the erased projection shares /// its type id and layout), `'_` for eval bindings, where inference recovers /// the serving lifetime. pub(crate) fn substitute_lifetimes(ty: &Type, replacement: &str) -> Type { struct Subst { replacement: &'static str, } impl VisitMut for Subst { fn visit_lifetime_mut(&mut self, lifetime: &mut Lifetime) { if lifetime.ident != "static" { *lifetime = Lifetime::new(self.replacement, lifetime.span()); } } } let replacement = match replacement { "'static" => "'static", _ => "'_", }; let mut ty = ty.clone(); Subst { replacement }.visit_type_mut(&mut ty); ty } /// The serving lifetime an input type names, so a materialized binding can tie /// the list view to the element's own region. pub(crate) fn named_serving_lifetime(ty: &Type) -> Option { struct Find { found: Option, } impl<'ast> Visit<'ast> for Find { fn visit_lifetime(&mut self, lifetime: &'ast Lifetime) { if self.found.is_none() && lifetime.ident != "static" { self.found = Some(lifetime.clone()); } } } let mut visitor = Find { found: None }; visitor.visit_type(ty); visitor.found } /// Rewrites a kernel-declared `ExtractArena<'e>` bound into the equality the /// trait names. 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) } #[cfg(test)] mod lifetime_subst_tests { use super::*; #[test] fn named_lifetimes_erase_to_static() { let ty: Type = syn::parse_quote!(Graphic<'e>); let erased = substitute_lifetimes(&ty, "'static"); assert_eq!(quote::quote!(#erased).to_string(), "Graphic < 'static >"); } }