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)]

View File

@@ -304,7 +304,7 @@ pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option<RecordShape> {
// Lazy secondaries are consumed as plain elements; raw record edges and
// ranked outputs have no element binding here.
let unsupported_lazy_secondary = |field: &ParsedField| match &field.ty {
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => is_record_value(output_type) || crate::codegen::ir::strip_ilist(output_type).1 > 0 || !field.attribute_reads.is_empty(),
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)) {
@@ -503,15 +503,17 @@ pub(crate) fn generic_assignment(field_ty: &Type, row_ty: &Type, generic: &Ident
})
}
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"))
/// 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 names `RecordValue` in its
/// output, receives raw record edges paired with the node's layout, and
/// Whether a kernel operates on whole records: it serves through the claim it
/// was handed, 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))
is_served(&slot_value_type(&parsed.output_type))
}
pub(crate) fn routing_io(parsed: &ParsedNodeFn) -> Option<RoutingIo> {
@@ -739,45 +741,9 @@ pub(crate) fn named_serving_lifetime(ty: &Type) -> Option<Lifetime> {
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 {
desugar_extract_lifetime_at(bound, core_types, None)
}
/// Renames every occurrence of the named lifetimes to `'__record` in a token
/// stream: a flipped kernel's serving lifetime is the record lifetime at the
/// impl, under whichever name the author picked.
pub(crate) fn rename_lifetimes_to_record(stream: TokenStream2, names: &[String]) -> TokenStream2 {
use proc_macro2::{Group, TokenTree};
let mut out = Vec::new();
let mut tokens = stream.into_iter().peekable();
while let Some(token) = tokens.next() {
match token {
TokenTree::Group(group) => {
let renamed = rename_lifetimes_to_record(group.stream(), names);
let mut fresh = Group::new(group.delimiter(), renamed);
fresh.set_span(group.span());
out.push(TokenTree::Group(fresh));
}
TokenTree::Punct(punct) if punct.as_char() == '\'' => {
match tokens.peek() {
Some(TokenTree::Ident(ident)) if names.iter().any(|name| ident == name) => {
let span = ident.span();
tokens.next();
out.push(TokenTree::Punct(punct));
out.push(TokenTree::Ident(proc_macro2::Ident::new("__record", span)));
}
_ => out.push(TokenTree::Punct(punct)),
}
}
token => out.push(token),
}
}
out.into_iter().collect()
}
/// As [`desugar_extract_lifetime`], with the arena lifetime overridden: a
/// flipped kernel's serving lifetime is the record lifetime at the impl.
pub(crate) fn desugar_extract_lifetime_at(bound: &TypeParamBound, core_types: &TokenStream2, at: Option<TokenStream2>) -> TokenStream2 {
let TypeParamBound::Trait(trait_bound) = bound else {
return quote!(#bound);
};
@@ -796,7 +762,6 @@ pub(crate) fn desugar_extract_lifetime_at(bound: &TypeParamBound, core_types: &T
let Some(GenericArgument::Lifetime(lifetime)) = args.args.first() else {
return quote!(#bound);
};
let lifetime = at.unwrap_or_else(|| quote!(#lifetime));
quote!(#core_types::context::ExtractArena<ArenaRef = &#lifetime #core_types::arena::Arena>)
}

View File

@@ -359,12 +359,9 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields
let #layout = #handle.layout().clone();
let #name = #handle.downcast_record::<#value_ty>()?;
},
SlotKind::Extracted(value_ty) => quote! {
let #handle = inputs.next().unwrap();
let #layout = #handle.layout().clone();
let #name = gcore::record::RecordExtract::<#value_ty, _>::new(#handle.downcast_record::<#value_ty>()?, &#layout);
},
SlotKind::Ranked(value_ty) => quote! {
// The node reads the element off the edge's own layout, so
// neither slot rides a layout to the constructor.
SlotKind::Extracted(value_ty) | SlotKind::Ranked(value_ty) => quote! {
let #name = inputs.next().unwrap().downcast_record::<#value_ty>()?;
},
}

View File

@@ -2,7 +2,7 @@
#![allow(dead_code)]
use crate::codegen::classify::{
Dialect, RoutingIo, bare_ident, context_param, dialect, flip_carrier, generic_assignment, generic_extractable, is_record_value, record_shape, routing_io, slot_value_type,
Dialect, RoutingIo, bare_ident, context_param, dialect, flip_carrier, generic_assignment, generic_extractable, is_served, record_shape, routing_io, slot_value_type,
};
use crate::codegen::entries::implementation_rows;
use crate::parsing::{AttributeRead, NodeParsedField, ParsedField, ParsedFieldType, ParsedNodeFn, RecordWrites, RegularParsedField, record_writes};
@@ -77,7 +77,7 @@ fn inputs(parsed: &ParsedNodeFn, fields: &[&ParsedField], generics: &[Ident]) ->
fn subject(index: usize, field: &ParsedField, carrier_subject: bool, routing: Option<&RoutingIo>) -> bool {
match &field.ty {
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => {
is_record_value(output_type) || routing.is_some_and(|routing| crate::codegen::classify::routing_source_output(output_type, &routing.generic)) || (index == 0 && carrier_subject)
is_served(output_type) || routing.is_some_and(|routing| crate::codegen::classify::routing_source_output(output_type, &routing.generic)) || (index == 0 && carrier_subject)
}
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => routing.is_some_and(|routing| bare_ident(ty) == Some(&routing.generic)) || (index == 0 && carrier_subject),
}
@@ -166,7 +166,7 @@ fn item_shape(element: &Type, depth: u8, reads: &[AttributeRead], generics: &[Id
}
fn element_of(ty: &Type, generics: &[Ident]) -> Element {
if is_record_value(ty) {
if is_served(ty) {
return Element::Opaque;
}
match bare_ident(ty) {
@@ -371,10 +371,11 @@ pub(crate) enum ValueBinding {
}
/// How a lazy (`impl Node`) input binds in eval. The `Poll` effect further
/// selects the borrowed vs `__cell`-driven form within `Element`/`Plain`.
/// selects the borrowed vs `__cell`-driven form within `Element`/`Generic`.
pub(crate) enum LazyBinding {
Element,
Plain,
/// The kernel holds the whole record behind a bare generic element.
Generic,
DeriveRouting,
DeriveCarrier,
OpaqueRecord,
@@ -383,7 +384,7 @@ pub(crate) enum LazyBinding {
impl ValueBinding {
/// Copies an element out of a record edge, so the frame is reclaimed after.
pub(crate) fn reads_out(&self) -> bool {
matches!(self, ValueBinding::ReadingSecondary | ValueBinding::RecordElement)
matches!(self, ValueBinding::ReadingSecondary | ValueBinding::RecordElement | ValueBinding::Plain)
}
}
@@ -484,7 +485,7 @@ pub(crate) fn lazy_binding(node: &Node, index: usize) -> LazyBinding {
} else if matches!(input.shape.element, Element::Opaque) {
LazyBinding::OpaqueRecord
} else {
LazyBinding::Plain
LazyBinding::Generic
}
}
@@ -654,7 +655,7 @@ mod tests {
} else if kinds.opaque {
let record = fields
.iter()
.position(|field| matches!(&field.ty, ParsedFieldType::Node(NodeParsedField { output_type, .. }) if is_record_value(output_type)));
.position(|field| matches!(&field.ty, ParsedFieldType::Node(NodeParsedField { output_type, .. }) if is_served(output_type)));
Facts {
sources: record.into_iter().collect(),
carried: true,
@@ -770,7 +771,7 @@ mod tests {
assert_bridge(
quote!(category("")),
quote! {
fn memo<'e>(_: impl Ctx, #[data] cache: Store, content: impl Node<Context<'_>, Output = RecordValue<'e>>) -> GPoll<RecordValue<'e>> { content.eval(()) }
fn memo<'e, 'l>(_: impl Ctx, #[data] cache: Store, content: impl Node<Context<'_>>, slot: FrameClaim<'l>) -> GPoll<Served<'e>> { content.serve(&(), slot) }
},
);
}
@@ -819,7 +820,7 @@ mod tests {
"flip-raw"
} else if flip {
"flip-lazy"
} else if opaque && raw && is_record_value(output_type) {
} else if opaque && raw && is_served(output_type) {
"opaque-record"
} else if raw {
"raw-lazy"
@@ -846,8 +847,8 @@ mod tests {
(LazyBinding::OpaqueRecord, _) => "opaque-record",
(LazyBinding::Element, true) => "flip-raw",
(LazyBinding::Element, false) => "flip-lazy",
(LazyBinding::Plain, true) => "raw-lazy",
(LazyBinding::Plain, false) => "lazy",
(LazyBinding::Generic, true) => "raw-lazy",
(LazyBinding::Generic, false) => "lazy",
},
}
}
@@ -1012,8 +1013,8 @@ mod tests {
assert_bindings(
quote!(category("")),
quote!(
fn memo<'e>(_: impl Ctx, #[data] cache: Store, content: impl Node<Context<'_>, Output = RecordValue<'e>>) -> GPoll<RecordValue<'e>> {
content.eval(())
fn memo<'e, 'l>(_: impl Ctx, #[data] cache: Store, content: impl Node<Context<'_>>, slot: FrameClaim<'l>) -> GPoll<Served<'e>> {
content.serve(&(), slot)
}
),
);

View File

@@ -17,7 +17,7 @@ pub(crate) fn generate_node_input_references(
for (input_index, (parsed_input, input_ident)) in parsed.fields.iter().zip(field_idents).enumerate() {
// `IList` nesting is rank metadata, not part of the value type.
let mut ty = match &parsed_input.ty {
let ty = match &parsed_input.ty {
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty.clone(),
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => crate::codegen::ir::strip_ilist(output_type).0,
};

View File

@@ -36,6 +36,9 @@ pub(crate) struct ParsedNodeFn {
pub(crate) output_depth: u8,
pub(crate) is_async: bool,
pub(crate) fields: Vec<ParsedField>,
/// The caller's frame claim, declared by a record-opaque kernel that
/// serves through it; not a wired input.
pub(crate) claim: Option<PatType>,
pub(crate) body: TokenStream2,
pub(crate) description: String,
}
@@ -685,7 +688,7 @@ pub(crate) fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Resu
let fn_generics = input_fn.sig.generics.params.into_iter().collect();
let is_async = input_fn.sig.asyncness.is_some();
let (input, fields) = parse_inputs(&input_fn.sig.inputs)?;
let (input, fields, claim) = parse_inputs(&input_fn.sig.inputs)?;
let (output_type, output_depth) = crate::codegen::ir::strip_output_rank(&parse_output(&input_fn.sig.output)?);
let where_clause = input_fn.sig.generics.where_clause;
let body = input_fn.block.to_token_stream();
@@ -718,15 +721,17 @@ pub(crate) fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Resu
output_depth,
is_async,
fields,
claim,
where_clause,
body,
description,
})
}
fn parse_inputs(inputs: &Punctuated<FnArg, Comma>) -> syn::Result<(Input, Vec<ParsedField>)> {
fn parse_inputs(inputs: &Punctuated<FnArg, Comma>) -> syn::Result<(Input, Vec<ParsedField>, Option<PatType>)> {
let mut fields = Vec::new();
let mut input = None;
let mut claim = None;
for (index, arg) in inputs.iter().enumerate() {
if let FnArg::Typed(PatType { pat, ty, attrs, .. }) = arg {
@@ -765,6 +770,17 @@ fn parse_inputs(inputs: &Punctuated<FnArg, Comma>) -> syn::Result<(Input, Vec<Pa
if attr_marker(ty).is_some() {
return Err(Error::new_spanned(pat_ident, "an attribute read binds to an input: destructure it as `(value, Attr<..>)`"));
}
// The claim is the caller's, not an input: it reaches the kernel
// from the serve the node is lowered into.
if is_frame_claim(ty) {
claim = Some(PatType {
attrs: Vec::new(),
pat: pat.clone(),
colon_token: Default::default(),
ty: ty.clone(),
});
continue;
}
let field = parse_field(pat_ident.clone(), (**ty).clone(), attrs).map_err(|e| Error::new_spanned(pat_ident, format!("Failed to parse argument '{}': {}", pat_ident.ident, e)))?;
fields.push(field);
} else if let Pat::Tuple(pat_tuple) = &**pat {
@@ -789,7 +805,13 @@ fn parse_inputs(inputs: &Punctuated<FnArg, Comma>) -> syn::Result<(Input, Vec<Pa
}
let input = input.ok_or_else(|| Error::new_spanned(inputs, "Expected at least one input argument. The first argument should be the node input type."))?;
Ok((input, fields))
Ok((input, fields, claim))
}
/// Whether the parameter is the caller-provided frame claim a record-opaque
/// kernel serves through.
fn is_frame_claim(ty: &Type) -> bool {
matches!(ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "FrameClaim"))
}
/// Splits a lazy input's `Output = (T, Attr<..>..)` tuple into the element
@@ -1129,9 +1151,10 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
));
}
let (input_type, output_type) = node_input_type
.zip(node_output_type)
.ok_or_else(|| Error::new_spanned(&ty, "Invalid Node type. Expected `impl Node<Input, Output = OutputType>`"))?;
let input_type = node_input_type.ok_or_else(|| Error::new_spanned(&ty, "Invalid Node type. Expected `impl Node<Input>` or `impl Node<Input, Output = OutputType>`"))?;
// A subject named without an output is a whole-record wire: the kernel
// serves it through its own claim rather than reading an element.
let output_type = node_output_type.unwrap_or_else(|| syn::parse_quote!(Served<'_>));
if !matches!(&value_source, ParsedValueSource::None) {
return Err(Error::new_spanned(&ty, "No default values for `impl Node` allowed"));
}
@@ -1485,6 +1508,7 @@ mod tests {
output_type: parse_quote!(f64),
output_depth: 0,
is_async: false,
claim: None,
fields: vec![ParsedField {
pat_ident: pat_ident("b"),
name: None,
@@ -1565,6 +1589,7 @@ mod tests {
output_type: parse_quote!(T),
output_depth: 0,
is_async: false,
claim: None,
fields: vec![
ParsedField {
pat_ident: pat_ident("transform_target"),
@@ -1660,6 +1685,7 @@ mod tests {
output_type: parse_quote!(Vector),
output_depth: 0,
is_async: false,
claim: None,
fields: vec![ParsedField {
pat_ident: pat_ident("radius"),
name: None,
@@ -1736,6 +1762,7 @@ mod tests {
output_type: parse_quote!(List<Raster<P>>),
output_depth: 0,
is_async: false,
claim: None,
fields: vec![ParsedField {
pat_ident: pat_ident("shadows"),
name: None,
@@ -1824,6 +1851,7 @@ mod tests {
output_type: parse_quote!(f64),
output_depth: 0,
is_async: false,
claim: None,
fields: vec![ParsedField {
pat_ident: pat_ident("b"),
name: None,
@@ -1915,6 +1943,7 @@ mod tests {
output_type: parse_quote!(List<Raster<CPU>>),
output_depth: 0,
is_async: true,
claim: None,
fields: vec![ParsedField {
pat_ident: pat_ident("path"),
name: None,
@@ -1991,6 +2020,7 @@ mod tests {
output_type: parse_quote!(i32),
output_depth: 0,
is_async: false,
claim: None,
fields: vec![],
body: TokenStream2::new(),
description: String::new(),

View File

@@ -318,6 +318,7 @@ impl PerPixelAdjustCodegen<'_> {
},
output_type: raster_gpu,
output_depth: 0,
claim: None,
is_async: false,
fields,
body,

View File

@@ -54,7 +54,7 @@ fn validate_record_io(parsed: &ParsedNodeFn) {
for field in parsed.fields.iter().skip(1) {
if let ParsedFieldType::Node(NodeParsedField { output_type, .. }) = &field.ty {
// Lazy secondaries are consumed as plain elements through the wire.
if crate::codegen::classify::is_record_value(output_type) || crate::codegen::ir::strip_ilist(output_type).1 > 0 {
if crate::codegen::classify::is_served(output_type) || crate::codegen::ir::strip_ilist(output_type).1 > 0 {
emit_error!(field.pat_ident.span(), "a record node's lazy inputs consume plain elements, not record or ranked wires");
}
if !field.attribute_reads.is_empty() {