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

@@ -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,
};