Flip the Node trait from eval onto the frame claim's serve

This commit is contained in:
Dennis Kobert
2026-08-29 12:26:46 +00:00
parent 277641d27e
commit 3af6834d3c
22 changed files with 1226 additions and 1156 deletions

View File

@@ -828,14 +828,30 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
]);
}
// A kernel-declared `ExtractArena<'e>` bound already carries the arena at
// its own lifetime; a second equality bound would contradict it.
let ctx_extracts_arena = ctx_param.is_some_and(|ctx_param| {
ctx_param
.bounds
.iter()
.any(|bound| matches!(bound, TypeParamBound::Trait(trait_bound) if trait_bound.path.segments.last().is_some_and(|segment| segment.ident == "ExtractArena")))
});
// The serving lifetime is quantified by each serving method, so the impl
// never binds the context's arena; the kernel keeps its own bound.
let extracts_arena = |bound: &TypeParamBound| matches!(bound, TypeParamBound::Trait(trait_bound) if trait_bound.path.segments.last().is_some_and(|segment| segment.ident == "ExtractArena"));
let mut impl_ctx_bounds: Vec<TokenStream2> = match ctx_param {
Some(ctx_param) => ctx_param.bounds.iter().filter(|bound| !matches!(bound, TypeParamBound::Lifetime(_)) && !extracts_arena(bound)).map(|bound| quote!(#bound)).collect(),
None => Vec::new(),
};
if ctx_param.is_none() {
impl_ctx_bounds.push(quote!(#core_types::Ctx));
}
if async_source && !snapshot_ctx {
impl_ctx_bounds.push(quote!(#core_types::context::DeriveCtx));
}
if snapshot_ctx {
impl_ctx_bounds.extend([
quote!(#core_types::context::DeriveCtx),
quote!(#core_types::context::ExtractFootprint),
quote!(#core_types::context::ExtractRealTime),
quote!(#core_types::context::ExtractAnimationTime),
quote!(#core_types::context::ExtractPointerPosition),
quote!(#core_types::context::ExtractIndex),
quote!(#core_types::context::ExtractPosition),
]);
}
let derives = ctx_param.is_some_and(|ctx_param| {
ctx_param.bounds.iter().any(|bound| match bound {
TypeParamBound::Trait(trait_bound) => trait_bound.path.segments.last().is_some_and(|segment| segment.ident == "DeriveCtx"),
@@ -843,20 +859,40 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
})
});
let derive_routing = derives && routing_generic.is_some();
// A kernel that holds records (a forwarded routing wire, or a lazy edge it
// serves itself) names the record lifetime; unless it declared a serving
// lifetime of its own, the context binds the arena at that lifetime.
let kernel_lazy = parsed.fields.iter().any(|field| !field.is_data_field && matches!(field.ty, ParsedFieldType::Node(_)));
let wants_record_lifetime = routing_generic.is_some() || ((record_io || flip) && kernel_lazy);
let ctx_declares_arena = ctx_param.is_some_and(|ctx_param| ctx_param.bounds.iter().any(extracts_arena));
let bind_record_arena = wants_record_lifetime && !ctx_declares_arena;
if bind_record_arena {
ctx_bounds.push(quote!(#core_types::context::ExtractArena<ArenaRef = &'__record #core_types::arena::Arena>));
}
let ctx_generic = match ctx_bounds.is_empty() {
true => quote!(#ctx_ident),
false => quote!(#ctx_ident: #(#ctx_bounds)+*),
};
let impl_ctx_generic = match impl_ctx_bounds.is_empty() {
true => quote!(#ctx_ident),
false => quote!(#ctx_ident: #(#impl_ctx_bounds)+*),
};
let generic_tokens = |param: &GenericParam| match param {
GenericParam::Type(type_param) if Some(&type_param.ident) == ctx_param.map(|ctx_param| &ctx_param.ident) => ctx_generic.clone(),
param => quote!(#param),
};
let impl_generic_tokens = |param: &GenericParam| match param {
GenericParam::Type(type_param) if Some(&type_param.ident) == ctx_param.map(|ctx_param| &ctx_param.ident) => impl_ctx_generic.clone(),
param => quote!(#param),
};
let mut generics: Vec<TokenStream2> = parsed
.fn_generics
.iter()
.filter(|param| match param {
GenericParam::Type(type_param) => !derive_routing || Some(&type_param.ident) != routing_generic.as_ref(),
// A routing generic is the record itself, so the kernel names the
// record value rather than carrying the parameter.
GenericParam::Type(type_param) => Some(&type_param.ident) != routing_generic.as_ref(),
_ => true,
})
.map(|param| match param {
@@ -886,52 +922,24 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
.iter()
.filter(|param| match param {
GenericParam::Type(type_param) => Some(&type_param.ident) != routing_generic.as_ref() && Some(&type_param.ident) != record_token.as_ref(),
// A serving lifetime stays only while the ctx bound constrains it
// (`ExtractArena<'e>`); wire types substitute its erased
// projection, which would leave it unconstrained. A flipped
// kernel's serving lifetime rebinds to the record lifetime, so
// the impl drops it entirely.
GenericParam::Lifetime(lifetime_param) => !flip && ctx_param.is_some_and(|ctx| quote!(#ctx).to_string().contains(&lifetime_param.lifetime.to_string())),
// A serving lifetime is the serve method's own, so it never rides
// the impl: an impl-level binding of the arena would contradict
// the one the method quantifies over.
GenericParam::Lifetime(_) => false,
_ => true,
})
.map(&generic_tokens)
.map(&impl_generic_tokens)
.collect();
if ctx_param.is_none() {
generics.push(ctx_generic.clone());
impl_generics.push(ctx_generic);
}
if routing_generic.is_some() || record_io || flip {
impl_generics.insert(0, quote!('__record));
}
// A flipped kernel's serving lifetime is the record lifetime at the impl:
// the ctx bound rebinds under the impl's own name.
if flip {
let serving_names: Vec<String> = parsed
.fn_generics
.iter()
.filter_map(|param| match param {
GenericParam::Lifetime(lifetime_param) => Some(lifetime_param.lifetime.ident.to_string()),
_ => None,
})
.collect();
if !serving_names.is_empty() {
impl_generics = impl_generics.into_iter().map(|tokens| crate::codegen::classify::rename_lifetimes_to_record(tokens, &serving_names)).collect();
}
impl_generics.push(impl_ctx_generic.clone());
}
let lazy_carrier = record_io && carrier_present && matches!(parsed.fields.iter().find(|field| !field.is_data_field).map(|field| &field.ty), Some(ParsedFieldType::Node(_)));
if derive_routing || (lazy_carrier && derives) {
generics.insert(0, quote!('__record));
}
let fn_name = &parsed.fn_name;
let mod_name = format_ident!("_{}_mod", parsed.mod_name);
let struct_name = format_ident!("{}Node", parsed.struct_name);
let output_type = &parsed.output_type;
let trait_output = match (record_io, &routing_generic) {
(true, _) | (false, Some(_)) => syn::parse_quote!(#core_types::record::RecordValue<'__record>),
(false, None) if flip => syn::parse_quote!(#core_types::record::RecordValue<'__record>),
(false, None) => slot_value_type(&parsed.output_type),
};
let raw_lazy = matches!(*model, Dialect::Poll);
let injected_name = |ident: &Ident| async_source && (ident == "_runtime" || ident == "_source");
let where_predicates: Vec<TokenStream2> = parsed.where_clause.iter().flat_map(|clause| clause.predicates.iter()).map(|predicate| quote!(#predicate)).collect();
@@ -965,49 +973,44 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
});
}
if flip {
let mut kernel_lazy = false;
for (index, field) in regular_fields.iter().enumerate() {
if matches!(&field.ty, ParsedFieldType::Node(_)) {
kernel_lazy = true;
let source_generic = format_ident!("__Source{index}");
let derived_extra = derives
.then(|| quote!(+ for<'__derived> #core_types::record::RecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>))
.then(|| quote!(+ for<'__derived> #core_types::record::DerivedRecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>))
.into_iter();
generics.push(quote! {
#source_generic: #core_types::node::Node<#ctx_ident, Output = #core_types::record::RecordValue<'__record>> #(#derived_extra)*
#source_generic: #core_types::node::Node<#ctx_ident> #(#derived_extra)*
});
}
}
if kernel_lazy {
generics.insert(0, quote!('__record));
}
}
if record_io {
let mut kernel_lazy = false;
for (index, field) in regular_fields.iter().enumerate() {
if matches!(&field.ty, ParsedFieldType::Node(_)) && matches!(crate::codegen::ir::lazy_binding(&node, index), LazyBinding::Element) {
kernel_lazy = true;
let source_generic = format_ident!("__Source{index}");
let derived_extra = derives
.then(|| quote!(+ for<'__derived> #core_types::record::RecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>))
.then(|| quote!(+ for<'__derived> #core_types::record::DerivedRecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>))
.into_iter();
generics.push(quote! {
#source_generic: #core_types::node::Node<#ctx_ident, Output = #core_types::record::RecordValue<'__record>> #(#derived_extra)*
#source_generic: #core_types::node::Node<#ctx_ident> #(#derived_extra)*
});
}
}
if kernel_lazy && !(derive_routing || (lazy_carrier && derives)) {
generics.insert(0, quote!('__record));
}
}
if opaque {
for (index, field) in regular_fields.iter().enumerate() {
if let ParsedFieldType::Node(NodeParsedField { output_type, .. }) = &field.ty {
if matches!(&field.ty, ParsedFieldType::Node(_)) {
let source_generic = format_ident!("__Source{index}");
generics.push(quote!(#source_generic: #core_types::node::Node<#ctx_ident, Output = #output_type>));
generics.push(quote!(#source_generic: #core_types::node::Node<#ctx_ident>));
}
}
}
// The record lifetime the kernel's wire types and arena bound name; the
// impl infers it from the serving lifetime at every call.
if wants_record_lifetime {
generics.insert(0, quote!('__record));
}
let data_names: Vec<&Ident> = data_fields.iter().map(|field| &field.pat_ident.ident).collect();
let data_params = data_fields.iter().map(|field| {
@@ -1018,9 +1021,13 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
quote!(#pat: &#ty)
});
let lazy_bound = |output_type: &Type| match derives {
true => quote!(for<'__derived> #core_types::node::Node<#core_types::context::Derived<'__derived, #ctx_ident>, Output = #output_type>),
false => quote!(#core_types::node::Node<#ctx_ident, Output = #output_type>),
let derived_edge = quote!(for<'__derived> #core_types::record::DerivedRecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>);
let lazy_bound = || match derives {
true => {
let derived_edge = derived_edge.clone();
quote!(#core_types::node::Node<#ctx_ident> + #derived_edge)
}
false => quote!(#core_types::node::Node<#ctx_ident>),
};
let routing_source = |ty: &Type| routing_generic.as_ref().is_some_and(|generic| crate::codegen::classify::routing_source_output(ty, generic));
@@ -1074,6 +1081,8 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
(None, false) => quote!(#pat: #core_types::node::List<'_, #ty>),
}
}
// A routing source is the forwarded record itself, not an element.
ParsedFieldType::Regular(RegularParsedField { ty, .. }) if routing_source(ty) => quote!(#pat: #core_types::record::RecordValue<'__record>),
ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => quote!(#pat: &#ty),
ParsedFieldType::Regular(RegularParsedField { ty, .. }) if !field.attribute_reads.is_empty() => read_tuple_param(field, quote!(#pat), quote!(#ty)),
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#pat: #ty),
@@ -1094,12 +1103,12 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let out = lazy_read_out(field, output_type);
quote!(#pat: #core_types::record::ElementLazyInput<'_, #out, #source_generic>)
}
(LazyBinding::Plain, true) => {
let bound = lazy_bound(output_type);
(LazyBinding::Generic, true) => {
let bound = lazy_bound();
quote!(#pat: &impl #bound)
}
(LazyBinding::Plain, false) => {
let bound = lazy_bound(output_type);
(LazyBinding::Generic, false) => {
let bound = lazy_bound();
quote!(#pat: #core_types::node::LazyInput<'_, impl #bound>)
}
}
@@ -1107,63 +1116,48 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
}
});
let record_value_ty: Type = syn::parse_quote!(#core_types::record::RecordValue<'__record>);
let node_bounds = regular_fields.iter().enumerate().zip(&node_generics).map(|((index, field), node_generic)| match &field.ty {
// A ranked input rides a record edge whatever the node kind; the
// materialized batch reads its lanes.
ParsedFieldType::Regular(_) if ir::materialized_levels(&node, index) > 0 => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>),
ParsedFieldType::Regular(_) if flip => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>),
ParsedFieldType::Node(_) if flip => match derives {
true => quote! {
#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>,
#node_generic: for<'__derived> #core_types::record::RecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>
let node_bounds = regular_fields.iter().enumerate().zip(&node_generics).map(|((index, field), node_generic)| {
let plain = quote!(#node_generic: #core_types::node::Node<#ctx_ident>);
// A lazy edge the kernel evaluates at derived contexts needs the
// derived form: the derived context's arena binding is unnameable
// under a higher rank.
let derived = quote!(#node_generic: #derived_edge);
let derived_plus = quote! {
#node_generic: #core_types::node::Node<#ctx_ident>,
#node_generic: #derived_edge
};
match &field.ty {
ParsedFieldType::Node(_) if flip => match derives {
true => derived_plus,
false => plain,
},
false => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>),
},
ParsedFieldType::Node(_) if record_io && !skips_carrier && index == 0 => match derives {
true => quote!(#node_generic: for<'__derived> #core_types::record::DerivedRecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>),
false => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>),
},
ParsedFieldType::Regular(_) if record_io && !skips_carrier && index == 0 => {
quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>)
}
ParsedFieldType::Regular(_) if record_io && !field.attribute_reads.is_empty() => {
quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>)
}
// An element-consuming lazy secondary rides a record edge, derivable
// when the kernel evaluates it at derived contexts.
ParsedFieldType::Node(_) if record_io && matches!(ir::lazy_binding(&node, index), LazyBinding::Element) => match derives {
true => quote! {
#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>,
#node_generic: for<'__derived> #core_types::record::RecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>
ParsedFieldType::Node(_) if record_io && !skips_carrier && index == 0 => match derives {
true => derived,
false => plain,
},
false => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>),
},
ParsedFieldType::Regular(RegularParsedField { ty, .. }) if routing_source(ty) => {
quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>)
}
ParsedFieldType::Regular(_) if routing_generic.is_some() => {
quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>)
}
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #ty>),
ParsedFieldType::Node(NodeParsedField { output_type, .. }) if routing_source(output_type) => match derives {
true => quote!(#node_generic: for<'__derived> #core_types::record::DerivedRecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>),
false => {
let bound = lazy_bound(&record_value_ty);
// An element-consuming lazy secondary rides a record edge, derivable
// when the kernel evaluates it at derived contexts.
ParsedFieldType::Node(_) if record_io && matches!(ir::lazy_binding(&node, index), LazyBinding::Element) => match derives {
true => derived_plus,
false => plain,
},
ParsedFieldType::Node(NodeParsedField { output_type, .. }) if routing_source(output_type) => match derives {
true => derived,
false => plain,
},
ParsedFieldType::Node(_) if opaque => plain,
ParsedFieldType::Node(_) => {
let bound = lazy_bound();
quote!(#node_generic: #bound)
}
},
ParsedFieldType::Node(NodeParsedField { output_type, .. }) if opaque && is_record_value(output_type) => {
quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #output_type>)
}
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => {
let bound = lazy_bound(output_type);
quote!(#node_generic: #bound)
// Every wire is a record edge; a value input's element copies out of
// the record its edge serves.
ParsedFieldType::Regular(_) => plain,
}
});
let mut lend_outlives: Vec<TokenStream2> = Vec::new();
if let Type::Reference(reference) = &trait_output
if let Type::Reference(reference) = &slot_value_type(&parsed.output_type)
&& let Some(lifetime) = &reference.lifetime
{
let inner = &reference.elem;
@@ -1224,29 +1218,17 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
.collect::<Vec<(usize, &AttributeRead)>>()
};
// A serving node's interrupt exits close through the frame claim's drop;
// a forwarding node still closes by hand, since its kernel serves the
// record and a top-level claim would double the frame.
let claims_frame = flip || record_io;
let interrupt_close = match claims_frame {
true => quote!(),
false => quote! {
unsafe { #core_types::record::interrupt_frame(_entry_sp, <Self as #core_types::node::Node<#ctx_ident>>::layout(self)) };
},
// Every exit closes through the frame claim's drop, so no exit path
// reclaims by hand.
let interrupt_close = quote!();
let frame_entry = quote! {
#[allow(unused_mut, unused_variables)]
let mut __frame = __slot;
};
let frame_entry = match claims_frame {
true => quote! {
#[allow(unused_mut, unused_variables)]
let mut __frame = #core_types::record::FrameClaim::enter(<Self as #core_types::node::Node<#ctx_ident>>::layout(self));
},
false => quote!(let _entry_sp = #core_types::record::stack::sp();),
};
let lane_frame_entry = match claims_frame {
true => quote! {
#[allow(unused_mut, unused_variables)]
let mut __frame = #core_types::record::FrameClaim::enter(__node_layout);
},
false => quote!(let _entry_sp = #core_types::record::stack::sp();),
// The batch loop is not a serve, so each lane claims its own frame.
let lane_frame_entry = quote! {
#[allow(unused_mut, unused_variables)]
let mut __frame = #core_types::record::FrameClaim::enter(__node_layout);
};
let bind_body = |index: usize, field: &ParsedField, batch_mode: bool| {
let name = &field.pat_ident.ident;
@@ -1400,16 +1382,29 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let #name: #ty = unsafe { #core_types::record::read_element(self.#slot.rec(&#name)) };
}
}
ValueBinding::Plain => quote! {
let #name = match __cell.eval_input(#index, &self.#name, __input) {
Ok(value) => value,
Err(interrupt) => #interrupt_return,
};
},
// A plain value rides a record edge like every other input; the
// element copies out against the edge's own layout, except for a
// routing source, whose record is what the kernel forwards.
ValueBinding::Plain => {
let read = (!routing_source(ty)).then(|| {
quote! {
let #name: #ty = unsafe {
#core_types::record::read_element(#core_types::node::Node::<#ctx_ident>::layout(&self.#name).rec(&#name))
};
}
});
quote! {
let #name = match __cell.eval_input(#index, &self.#name, __input) {
Ok(value) => value,
Err(interrupt) => #interrupt_return,
};
#read
}
}
},
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => match (ir::lazy_binding(&node, index), raw_lazy) {
// A raw poll edge is threaded straight through, so it does not bind here.
(LazyBinding::Plain, true) => quote!(),
(LazyBinding::Generic, true) => quote!(),
(LazyBinding::DeriveRouting, _) => quote! {
let #name = #core_types::record::RecordLazyInput::new(&self.#name, &__cell, #index, self.__layout.depth.saturating_sub(#pushed_levels));
},
@@ -1462,13 +1457,20 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
(LazyBinding::OpaqueRecord, _) => quote! {
let #name = #core_types::record::RecordEdgeInput::new(&self.#name, &self.__layout);
},
(LazyBinding::Plain, false) => quote! {
(LazyBinding::Generic, false) => quote! {
let #name = #core_types::node::LazyInput::new(&self.#name, &__cell, #index);
},
},
}
};
// A bind whose element copies out reclaims the edge's frame; a forwarded
// record must outlive the bind, so its frame stays.
let reads_out_at = |index: usize| match &regular_fields[index].ty {
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => !routing_source(ty) && ir::value_binding(&node, index).reads_out(),
ParsedFieldType::Node(_) => false,
};
let clamp_tokens = |field: &ParsedField| {
let ParsedFieldType::Regular(RegularParsedField { number_hard_min, number_hard_max, .. }) = &field.ty else {
return None;
@@ -1493,7 +1495,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
ParsedFieldType::Regular(_) => quote!(#name),
ParsedFieldType::Node(_) => match (ir::lazy_binding(&node, index), raw_lazy) {
(LazyBinding::Element, true) | (LazyBinding::OpaqueRecord, _) => quote!(&#name),
(LazyBinding::Plain, true) => quote!(&self.#name),
(LazyBinding::Generic, true) => quote!(&self.#name),
_ => quote!(#name),
},
}
@@ -1559,23 +1561,28 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let #arg = #core_types::extent::ListIn::new(&#query, &__total);
}
}
ValueBinding::RecordElement | ValueBinding::ReadingSecondary => {
let slot = format_ident!("__in_{index}");
// A routing source forwards its record whole; its extents are
// the queryable quantity.
_ if routing_source(ty) => extent_edge(&query, &arg),
ValueBinding::RecordElement | ValueBinding::ReadingSecondary | ValueBinding::Plain => {
let layout = match ir::value_binding(&node, index) {
ValueBinding::Plain => quote!(#core_types::node::Node::<#ctx_ident>::layout(&self.#name)),
_ => {
let slot = format_ident!("__in_{index}");
quote!(self.#slot)
}
};
quote! {
let #query = || {
// SAFETY: the element copies out by value; extent
// queries leave the record stack untouched.
let __scope = unsafe { #core_types::record::stack::ScopeGuard::enter() };
#core_types::node::Node::eval(&self.#name, __input)
.map(|__value| unsafe { #core_types::record::read_element::<#ty>(self.#slot.rec(&__value)) })
#core_types::record::serve_edge(&self.#name, __input)
.map(|__value| unsafe { #core_types::record::read_element::<#ty>(#layout.rec(&__value)) })
};
let #arg = #core_types::extent::ValueIn::new(&#query);
}
}
ValueBinding::Plain => quote! {
let #query = || #core_types::node::Node::eval(&self.#name, __input);
let #arg = #core_types::extent::ValueIn::new(&#query);
},
// A carrier, lent, or materialized ranked input is a record
// edge; its extents are the queryable quantity.
_ => extent_edge(&query, &arg),
@@ -1585,7 +1592,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
arg_names.push(arg);
}
quote! {
fn extent_at(&self, __input: &#ctx_ident, __level: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> {
fn extent_at<'__serve>(&self, __input: &#ctx_ident, __level: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent>
where
#ctx_ident: #core_types::context::ExtractArena<ArenaRef = &'__serve #core_types::arena::Arena>,
{
#(#arg_decls)*
let __level_in = #core_types::extent::LevelIn::new(__level, <Self as #core_types::node::Node<#ctx_ident>>::layout(self).depth);
#path(#(#arg_names,)* __level_in)
@@ -1593,7 +1603,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
}
} else if let Some(path) = &parsed.attributes.extent_raw {
quote! {
fn extent_at(&self, __input: &#ctx_ident, __level: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> {
fn extent_at<'__serve>(&self, __input: &#ctx_ident, __level: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent>
where
#ctx_ident: #core_types::context::ExtractArena<ArenaRef = &'__serve #core_types::arena::Arena>,
{
#path(self, __input, __level)
}
}
@@ -1620,7 +1633,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
},
};
quote! {
fn extent_at(&self, __input: &#ctx_ident, __level: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> {
fn extent_at<'__serve>(&self, __input: &#ctx_ident, __level: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent>
where
#ctx_ident: #core_types::context::ExtractArena<ArenaRef = &'__serve #core_types::arena::Arena>,
{
#query
let __arg = #core_types::extent::ExtentIn::new(&__query);
let __level_in = #core_types::extent::LevelIn::new(__level, <Self as #core_types::node::Node<#ctx_ident>>::layout(self).depth);
@@ -1631,7 +1647,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
// A leveled output without an extent fn reports a lower bound;
// consumers size it by draining to the past-end signal.
quote! {
fn extent_at(&self, _: &#ctx_ident, _: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> {
fn extent_at<'__serve>(&self, _: &#ctx_ident, _: u8) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent>
where
#ctx_ident: #core_types::context::ExtractArena<ArenaRef = &'__serve #core_types::arena::Arena>,
{
#core_types::gpoll::GPoll::Final(#core_types::gpoll::Extent::AtLeast(0))
}
}
@@ -1652,14 +1671,14 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
};
let batch_signature = quote! {
fn eval_batch<'__batch>(
fn eval_batch<'__batch, '__serve>(
&'__batch self,
__input: &'__batch #ctx_ident,
__range: ::std::ops::Range<u64>,
__scratch: Option<&'__batch mut [::std::mem::MaybeUninit<u64>]>,
) -> #core_types::node::BatchStatus<'__batch>
where
#ctx_ident: #core_types::context::InjectIndex + Copy,
#ctx_ident: #core_types::context::InjectIndex + Copy + #core_types::context::ExtractArena<ArenaRef = &'__serve #core_types::arena::Arena>,
};
let produces_records = record_io || routing_generic.is_some() || flip;
@@ -1678,18 +1697,20 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
.flatten();
let lane_lifetime = lane_injected.is_some().then(|| quote!('__lane,));
let kernel_output = lane_injected.or(attr_injected);
let kernel_output = match derive_routing {
let kernel_output = match routing_generic.is_some() {
true => {
let generic = routing_generic.as_ref().expect("derive routing implies routing");
let generic = routing_generic.as_ref().expect("guarded by the arm");
let ty = substitute_routing_record(&parsed.output_type, generic, core_types);
quote!(#ty)
}
false => kernel_output.map(|ty| quote!(#ty)).unwrap_or_else(|| quote!(#output_type)),
};
let claim_param = parsed.claim.iter().map(|claim| quote!(, #claim));
let claim_arg = parsed.claim.iter().map(|_| quote!(, __frame));
let kernel = match async_fn {
false => quote! {
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
#vis fn #fn_name<#attr_lifetime #lane_lifetime #(#generics,)*>(#ctx_pat: &#ctx_ident #(, #data_params)* #(, #kernel_params)*) -> #kernel_output #fn_where #body
#vis fn #fn_name<#attr_lifetime #lane_lifetime #(#generics,)*>(#ctx_pat: &#ctx_ident #(, #data_params)* #(, #kernel_params)* #(#claim_param)*) -> #kernel_output #fn_where #body
},
true => {
let kernel_generics = parsed.fn_generics.iter().filter(|param| match param {
@@ -1722,16 +1743,34 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
true => quote!(#core_types::node::StatusCell::no_partial()),
false => quote!(#core_types::node::StatusCell::new()),
};
let kernel_call = quote!(self::#fn_name(__input #(, &self.#data_names)* #(, #call_args)*));
let kernel_call = quote!(self::#fn_name(__input #(, &self.#data_names)* #(, #call_args)* #(#claim_arg)*));
// A record-opaque kernel serves through the claim it was handed; every
// other forwarding kernel returns a record of this node's layout, which
// fills the claim.
let forwarded = |value: TokenStream2| match opaque {
true => value,
// SAFETY: the kernel's record is of this node's layout.
false => quote!(unsafe { __frame.forward(&#value) }),
};
let lift = match *model {
Dialect::Interrupt => quote! {
match #kernel_call {
Ok(value) => __cell.finish(value),
Err(interrupt) => { #interrupt_close interrupt.into() }
Dialect::Interrupt => {
let served = forwarded(quote!(value));
quote! {
match #kernel_call {
Ok(value) => __cell.finish(#served),
Err(interrupt) => { #interrupt_close interrupt.into() }
}
}
}
Dialect::Poll => match opaque {
true => quote!(__cell.merge(#kernel_call)),
// SAFETY: the kernel's record is of this node's layout.
false => quote!(__cell.merge(#kernel_call).map(|value| unsafe { __frame.forward(&value) })),
},
Dialect::Poll => quote!(__cell.merge(#kernel_call)),
_ => quote!(__cell.finish(#kernel_call)),
_ => {
let served = forwarded(quote!(#kernel_call));
quote!(__cell.finish(#served))
}
};
let placeholder_value_names: Vec<&Ident> = kernel_fields
@@ -1764,19 +1803,13 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
#clamp
}
});
// Async slots persist plain values across evaluations; a flipped source
// lifts the slot value onto its record wire at every merge point, into
// the carried frame when the node has a carrier.
let merge_lifted = |poll: TokenStream2| match flip {
true => quote!(__cell.merge(__frame.lift(#poll, #core_types::context::ExtractArena::arena(__input)))),
false => quote!(__cell.merge(#poll)),
};
let pending_return = match flip && carrier_flip {
true => quote! {
__frame.lift::<#slot_ty>(#core_types::gpoll::GPoll::Pending, #core_types::context::ExtractArena::arena(__input))
},
false => quote!({ #interrupt_close #core_types::gpoll::GPoll::Pending }),
};
// Async slots persist plain values across evaluations; the source lifts
// the slot value onto its record wire at every merge point, into the
// carried frame when the node has a carrier.
let merge_lifted = |poll: TokenStream2| quote!(__cell.merge(__frame.lift_served(#poll, #core_types::context::ExtractArena::arena(__input))));
// The claim drops with the frame still claimed, so a valueless exit needs
// no closing of its own.
let pending_return = quote!(#core_types::gpoll::GPoll::Pending);
let inflight = match &parsed.attributes.placeholder {
Some(path) => merge_lifted(quote!(#core_types::gpoll::GPoll::Partial(#path(#(&#placeholder_value_names),*)))),
None => pending_return.clone(),
@@ -1951,7 +1984,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
#element_store
#(#attr_stores)*
// SAFETY: the carry and the writes above complete the record.
let __value = unsafe { __frame.finish() };
let __value = unsafe { __frame.finish_served() };
}
});
let record_tail = record_tail_core.clone().map(|core| {
@@ -1965,7 +1998,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let prelude = carried_prelude.clone().unwrap_or_default();
return quote! {
#prelude
__cell.merge(__frame.lift(#kernel_call, #core_types::context::ExtractArena::arena(__input)))
__cell.merge(__frame.lift_served(#kernel_call, #core_types::context::ExtractArena::arena(__input)))
};
}
let kernel_value = match *model {
@@ -1981,7 +2014,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
quote! {
#prelude
let __kernel_value = #kernel_value;
__cell.merge(__frame.lift(#core_types::gpoll::GPoll::Final(__kernel_value), #core_types::context::ExtractArena::arena(__input)))
__cell.merge(__frame.lift_served(#core_types::gpoll::GPoll::Final(__kernel_value), #core_types::context::ExtractArena::arena(__input)))
}
});
let tail_form = if async_fn {
@@ -2055,14 +2088,16 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
// base lane, so the per-lane loop runs only the kernel and the carrier;
// eager inputs are batch-invariant by contract (per-lane variance rides
// lazy carriers).
// The fill loop copies each lane's frame out, so it needs the record, not
// the serving proof.
let hoisted_lane_poll = match tail_form {
Tail::Record => record_tail_core.clone().map(|core| {
quote! {
#core
let __poll = __cell.finish(__value);
let __poll = __cell.finish(__value).map(#core_types::record::Served::value);
}
}),
Tail::Forward if routing_generic.is_some() => Some(quote!(let __poll = #lift;)),
Tail::Forward if routing_generic.is_some() => Some(quote!(let __poll = #lift.map(#core_types::record::Served::value);)),
_ => None,
};
// A serving-lifetime element rides the per-lane fill loop: the hoisted
@@ -2091,7 +2126,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
.filter(|(index, field)| matches!(field.ty, ParsedFieldType::Regular(_)) && hoists(*index))
.map(|(index, field)| {
let body = bind_body(index, field, true);
match ir::value_binding(&node, index).reads_out() {
match reads_out_at(index) {
false => body,
true => {
let mark = format_ident!("__scope_{index}");
@@ -2269,12 +2304,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
};
let record_bounds: Vec<TokenStream2> = {
let arena_bound = (record_io && !ctx_extracts_arena && (skips_carrier || lazy_carrier || element_write.is_some())) || (!record_io && (derive_routing || flip));
let mut bounds = if arena_bound {
vec![quote!(#ctx_ident: #core_types::context::ExtractArena<ArenaRef = &'__record #core_types::arena::Arena>)]
} else {
Vec::new()
};
// The serving lifetime is the serve method's, so the arena binding
// rides there rather than on the impl.
let mut bounds: Vec<TokenStream2> = Vec::new();
// A reading secondary input's element copies out of its record, as
// does a concrete carrier read.
if record_io {
@@ -2566,7 +2598,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let eval_body = eval_steps.iter().map(|step| match step {
EvalStep::Bind(index, field) => {
let body = bind_body(*index, field, false);
let reads_out = matches!(&field.ty, ParsedFieldType::Regular(_)) && ir::value_binding(&node, *index).reads_out();
let reads_out = reads_out_at(*index);
match reads_out {
false => body,
true => {
@@ -2597,9 +2629,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
#(#flip_bounds,)*
#(#where_predicates,)*
{
type Output = #trait_output;
fn eval(&self, __input: &#ctx_ident) -> #core_types::gpoll::GPoll<Self::Output> {
fn serve<'__serve, '__slot>(&self, __input: &#ctx_ident, __slot: #core_types::record::FrameClaim<'__slot>) -> #core_types::gpoll::GPoll<#core_types::record::Served<'__serve>>
where
#ctx_ident: #core_types::context::ExtractArena<ArenaRef = &'__serve #core_types::arena::Arena>,
{
// The exit trace rides a guard so early returns report too, which
// is what pins a frame leak to its node.
#[cfg(debug_assertions)]