Gather a chosen subject lane through a Lane output and convert pack_strips

This commit is contained in:
Dennis Kobert
2026-08-24 08:22:25 +00:00
parent aba941e81d
commit d38c767312
7 changed files with 342 additions and 121 deletions

View File

@@ -77,6 +77,9 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
// A `_: ()` primary keeps its slot: dropping it would shift every
// per-index classification against the IR and the document's arity.
let record_skips_carrier = record_io && !carrier_present;
// A gather carrier copies the returned lane's frame, so it needs the plan
// without a carrier layout of its own.
let gather_carrier = record_io && node.output.gathers;
let struct_regular_fields: Vec<_> = regular_fields.to_vec();
let struct_regular_field_names: Vec<_> = struct_regular_fields.iter().map(|f| &f.pat_ident.ident).collect();
@@ -193,6 +196,8 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let mut state = vec![quote!(pub(super) __layout: gcore::record::Layout)];
if !record_skips_carrier {
state.push(quote!(pub(super) __carrier: gcore::record::Layout));
}
if !record_skips_carrier || gather_carrier {
state.push(quote!(pub(super) __plan: ::std::vec::Vec<(usize, usize, usize)>));
}
state.push(quote!(pub(super) __frame_bytes: usize));
@@ -752,16 +757,22 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
(crate::codegen::ir::NodeKind::Routing, crate::codegen::ir::Element::Generic(ident)) => Some(ident.clone()),
_ => None,
};
let skips_carrier = record_io && !carrier_present;
// A gather carrier copies the returned lane's frame, so it needs the plan
// without a carrier layout of its own.
let gather_carrier = record_io && node.output.gathers;
// A gathered element is carried by the copy plan, not as a lazy token, so
// its generic stays a struct parameter.
let record_token = match (kind, &node.output.shape.element) {
(crate::codegen::ir::NodeKind::RecordIo, crate::codegen::ir::Element::Generic(ident)) => Some(ident.clone()),
(crate::codegen::ir::NodeKind::RecordIo, crate::codegen::ir::Element::Generic(ident)) if !gather_carrier => Some(ident.clone()),
_ => None,
};
let skips_carrier = record_io && !carrier_present;
// The record-io write set, resolved from the output item and carrier input.
let write_markers: Vec<&Type> = node.output.shape.attrs.iter().map(|attr| &attr.marker).collect();
let removes: Vec<&Type> = node.output.removes.iter().map(|attr| &attr.marker).collect();
// A gathered element rides the copy plan, never a write.
let element_write: Option<&Type> = match &node.output.shape.element {
crate::codegen::ir::Element::Concrete(ty) => Some(ty),
crate::codegen::ir::Element::Concrete(ty) if !gather_carrier => Some(ty),
_ => None,
};
let carrier_read_ty: Option<&Type> = node.inputs.first().filter(|input| input.subject).and_then(|input| match &input.shape.element {
@@ -998,7 +1009,12 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let pat = &field.pat_ident;
match &field.ty {
ParsedFieldType::Regular(RegularParsedField { ty, .. }) if ir::materialized_levels(&node, index) > 0 => {
quote!(#pat: #core_types::node::List<'_, #ty>)
// The gathered lane borrows this list, so both take the kernel's
// own subject lifetime.
match ir::gathered_subject(&node) == Some(index) {
true => quote!(#pat: #core_types::node::List<'__lane, #ty>),
false => quote!(#pat: #core_types::node::List<'_, #ty>),
}
}
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)),
@@ -1578,8 +1594,13 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
// A bare `Attr<M>` in the return type cannot elide its lifetime, so the
// kernel gets a fresh one; reference-valued writes name their real
// lifetime explicitly and pass through untouched.
let kernel_output = record_io.then(|| inject_attr_lifetimes(&parsed.output_type)).flatten();
let attr_lifetime = kernel_output.is_some().then(|| quote!('__attr,));
let attr_injected = record_io.then(|| inject_attr_lifetimes(&parsed.output_type)).flatten();
let attr_lifetime = attr_injected.is_some().then(|| quote!('__attr,));
let lane_injected = gather_carrier
.then(|| crate::codegen::classify::inject_lane_lifetime(attr_injected.as_ref().unwrap_or(&parsed.output_type)))
.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 {
true => {
let generic = routing_generic.as_ref().expect("derive routing implies routing");
@@ -1591,7 +1612,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let kernel = match async_fn {
false => quote! {
#[allow(clippy::too_many_arguments)]
#vis fn #fn_name<#attr_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)*) -> #kernel_output #fn_where #body
},
true => {
let kernel_generics = parsed.fn_generics.iter().filter(|param| match param {
@@ -1784,6 +1805,15 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
}
})
.unwrap_or_default();
// A gathered lane owns its record, so the plan reads straight off it.
let gather_carry = gather_carrier
.then(|| {
quote! {
let __src_rec = __element.rec();
unsafe { #core_types::record::apply_plan(__src_rec, __dst, &self.__plan) };
}
})
.unwrap_or_default();
let carrier_read_bindings: Vec<TokenStream2> = match skips_carrier || lazy_carrier {
true => Vec::new(),
false => reads_of(0).into_iter().map(|(slot, read)| read_binding(slot, read, quote!(__src_rec))).collect(),
@@ -1798,9 +1828,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
_ => quote!(#record_kernel_call),
};
let attr_binders: Vec<Ident> = (0..write_markers.len()).map(|index| format_ident!("__attr_{index}")).collect();
let element_binder = match (element_write, lazy_carrier) {
(Some(_), _) | (None, true) => quote!(__element),
(None, false) => quote!(_),
let element_binder = match (element_write.is_some(), lazy_carrier || gather_carrier) {
(true, _) | (_, true) => quote!(__element),
(false, false) => quote!(_),
};
// Slot binders in the return tuple's own order: an `Attr` binds the
// next write binder, a `RemoveAttr` binds nothing.
@@ -1854,6 +1884,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let __kernel_value = #kernel_value;
#destructure
#lazy_carry
#gather_carry
#element_store
#(#attr_stores)*
if self.__frame_bytes != 0 {
@@ -2226,7 +2257,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
self.#slot = __resolved.layout.offset_of(<#marker as #core_types::attribute::Attribute>::NAME, 0).expect("a written attribute is always part of the wired layout");
}
});
let plan = (!skips_carrier).then(|| quote!(self.__plan = __resolved.plan;));
let plan = (!skips_carrier || gather_carrier).then(|| quote!(self.__plan = __resolved.plan;));
Some(quote! {
#(#write_installs)*
self.__frame_bytes = __resolved.frame_bytes;
@@ -2292,7 +2323,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
Some(ty) => quote!({ use #core_types::record::{ElementWritePickHashed as _, ElementWritePickPlain as _}; (&#core_types::record::ElementWritePick::<#ty>(::core::marker::PhantomData)).element_write() }),
None => quote!(__carrier.element),
};
let layout_def = match skips_carrier {
// A gather carrier's base is the gathered subject's layout, so its free
// layout fn takes that layout even though the subject materializes.
let layout_def = match skips_carrier && !gather_carrier {
true => quote! {
#vis fn #layout_fn() -> #core_types::record::Layout {
#core_types::record::Layout::default().with_writes(0, #element, &[#(#write_descs),*])
@@ -2352,19 +2385,39 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let slot = format_ident!("__in_{index}");
quote!(#slot: #slot.clone(),)
});
let plan_default = (!skips_carrier).then(|| quote!(__plan: ::std::vec::Vec::new(),)).into_iter();
let plan_default = (!skips_carrier || gather_carrier).then(|| quote!(__plan: ::std::vec::Vec::new(),)).into_iter();
let read_names = (0..flat_reads.len()).map(|index| format_ident!("__read_{index}")).map(|slot| quote!(#slot,));
let write_defaults = (0..write_markers.len()).map(|index| format_ident!("__write_{index}")).map(|slot| quote!(#slot: 0,));
let mat_cache_defaults = materialized_indices(&regular_fields, &node).into_iter().map(|index| {
let slot = format_ident!("__mat_cache_{index}");
quote!(#slot: ::core::default::Default::default(),)
});
// A ranked input's element generic rides the struct as a phantom
// parameter, so the constructor declares and initializes it too.
let carried_type_params: Vec<&Ident> = struct_type_params
.iter()
.filter(|ident| !data_field_generic_idents.contains(ident) && !node_generics.contains(ident))
.collect();
let carried_generic_params: Vec<TokenStream2> = carried_type_params
.iter()
.map(|ident| {
parsed
.fn_generics
.iter()
.find_map(|param| match param {
GenericParam::Type(type_param) if &&type_param.ident == ident => Some(quote!(#type_param)),
_ => None,
})
.unwrap_or_else(|| quote!(#ident))
})
.collect();
let marker_init = (!carried_type_params.is_empty()).then(|| quote!(__marker: ::core::marker::PhantomData,)).into_iter();
quote! {
#layout_def
#layout_meta_def
#[automatically_derived]
impl<#(#data_field_generic_idents,)* #(#node_generics,)*> #mod_name::#struct_name<#(#struct_type_params,)*> {
impl<#(#data_field_generic_idents,)* #(#node_generics,)* #(#carried_generic_params,)*> #mod_name::#struct_name<#(#struct_type_params,)*> {
#[allow(clippy::too_many_arguments)]
#vis fn new(#(#edge_args,)* #(#carrier_layout_param)* #(#input_layout_params)*) -> Self {
#(#read_inits)*
@@ -2375,6 +2428,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
#(#input_layout_inits)*
__layout: ::core::default::Default::default(),
#(#plan_default)*
#(#marker_init)*
__frame_bytes: 0,
#(#read_names)*
#(#write_defaults)*

View File

@@ -230,6 +230,33 @@ pub(crate) fn inject_attr_lifetimes(output: &Type) -> Option<Type> {
injector.changed.then_some(ty)
}
/// Binds a `Lane` output to the kernel's subject lifetime, replacing whatever
/// the author spelled: the lane borrows the materialized subject, not the arena.
pub(crate) fn inject_lane_lifetime(output: &Type) -> Option<Type> {
struct Injector {
changed: bool,
}
impl VisitMut for Injector {
fn visit_path_segment_mut(&mut self, segment: &mut syn::PathSegment) {
if segment.ident == "Lane"
&& let PathArguments::AngleBracketed(args) = &mut segment.arguments
{
let kept: Vec<GenericArgument> = args.args.iter().filter(|arg| !matches!(arg, GenericArgument::Lifetime(_))).cloned().collect();
args.args = kept.into_iter().collect();
args.args.insert(0, GenericArgument::Lifetime(Lifetime::new("'__lane", proc_macro2::Span::call_site())));
self.changed = true;
}
syn::visit_mut::visit_path_segment_mut(self, segment);
}
}
let mut ty = output.clone();
let mut injector = Injector { changed: false };
injector.visit_type_mut(&mut ty);
injector.changed.then_some(ty)
}
pub(crate) fn contains_open_generic(parsed: &ParsedNodeFn, ty: &Type) -> bool {
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
parsed
@@ -306,12 +333,14 @@ pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option<RecordShape> {
let ParsedFieldType::Regular(RegularParsedField { ty, lend: None, implementations, .. }) = &carrier_field.ty else {
return None;
};
// A gathered subject is never read as an element, so its generic stays open.
let gathers = crate::codegen::ir::gathers_lane(parsed);
let token = match ty {
Type::Tuple(tuple) if tuple.elems.is_empty() => None,
ty => match implementations.is_empty().then(|| unbounded_generic(parsed, ty)).flatten() {
Some(token) => Some(token),
None => {
if contains_open_generic(parsed, ty) {
if !gathers && contains_open_generic(parsed, ty) {
return None;
}
None
@@ -334,7 +363,7 @@ pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option<RecordShape> {
}
}
None => {
if contains_open_generic(parsed, &element) {
if !gathers && contains_open_generic(parsed, &element) {
return None;
}
}

View File

@@ -90,6 +90,10 @@ fn output(parsed: &ParsedNodeFn, generics: &[Ident]) -> Output {
Some(RecordWrites { element, markers, removes }) => (element, markers, removes),
None => (row, Vec::new(), Vec::new()),
};
let (element, gathers) = match lane_inner(&element) {
Some(inner) => (inner, true),
None => (element, false),
};
Output {
shape: ItemShape {
element: element_of(&element, generics),
@@ -97,6 +101,7 @@ fn output(parsed: &ParsedNodeFn, generics: &[Ident]) -> Output {
attrs: writes.into_iter().map(|marker| LevelAttr { marker, level: 0 }).collect(),
},
removes: removes.into_iter().map(|marker| LevelAttr { marker, level: 0 }).collect(),
gathers,
}
}
@@ -210,6 +215,31 @@ fn replace_first_type_arg(ty: &Type, replacement: Type) -> Type {
ty
}
/// Whether the output's element position spells `Lane<T>`.
pub(crate) fn gathers_lane(parsed: &ParsedNodeFn) -> bool {
gathered_element(parsed).is_some()
}
fn gathered_element(parsed: &ParsedNodeFn) -> Option<Type> {
let row = slot_value_type(&parsed.output_type);
let element = record_writes(&row).map_or(row, |writes| writes.element);
lane_inner(&element).is_some().then_some(element)
}
/// The element type inside a `Lane<T>` position, lifetime argument skipped.
fn lane_inner(ty: &Type) -> Option<Type> {
let Type::Path(path) = ty else { return None };
let segment = path.path.segments.last()?;
if segment.ident != "Lane" {
return None;
}
let PathArguments::AngleBracketed(args) = &segment.arguments else { return None };
args.args.iter().find_map(|arg| match arg {
GenericArgument::Type(inner) => Some(inner.clone()),
_ => None,
})
}
fn ilist_inner(ty: &Type) -> Option<Type> {
let Type::Path(path) = ty else { return None };
let segment = path.path.segments.last()?;
@@ -226,13 +256,7 @@ fn ilist_inner(ty: &Type) -> Option<Type> {
/// Emits the `LayoutMeta` literal from the IR. `element_spec` is supplied by the
/// caller since it is the one row-dependent facet; the rest folds from the node.
pub(crate) fn layout_meta_tokens(node: &Node, element_spec: TokenStream2, core_types: &TokenStream2) -> TokenStream2 {
// A materialized subject is folded, not carried: it contributes no layout.
let sources = node
.inputs
.iter()
.enumerate()
.filter(|(index, input)| input.subject && materialized_levels(node, *index) == 0)
.map(|(index, _)| index as u8);
let sources = layout_sources(node).into_iter().map(|index| index as u8);
let reads = node.inputs.iter().enumerate().filter_map(|(index, input)| {
(matches!(input.evaluation, Evaluation::Eager) && !input.shape.attrs.is_empty()).then(|| {
let descs = field_writes(&input.shape.attrs, core_types);
@@ -264,6 +288,29 @@ pub(crate) fn layout_meta_tokens(node: &Node, element_spec: TokenStream2, core_t
}
}
/// The subjects whose layouts union into the output's base: un-materialized
/// ones, plus a gathered subject.
pub(crate) fn layout_sources(node: &Node) -> Vec<usize> {
node.inputs
.iter()
.enumerate()
.filter(|(index, input)| input.subject && (materialized_levels(node, *index) == 0 || gathered_subject(node) == Some(*index)))
.map(|(index, _)| index)
.collect()
}
/// The materialized subject a gather-carrier copies its output frames from.
pub(crate) fn gathered_subject(node: &Node) -> Option<usize> {
if !node.output.gathers {
return None;
}
node.inputs
.iter()
.enumerate()
.find(|(index, input)| input.subject && materialized_levels(node, *index) > 0)
.map(|(index, _)| index)
}
/// The single carried subject a level-preserving node forwards its extents
/// to: exactly one un-materialized subject, no level shift, and no fold.
pub(crate) fn forwarded_subject(node: &Node) -> Option<usize> {
@@ -282,8 +329,12 @@ pub(crate) fn forwarded_subject(node: &Node) -> Option<usize> {
}
}
/// The materialized subject a node folds, as `(input, levels)`.
/// The materialized subject a node folds, as `(input, levels)`. A gathered
/// subject is count-preserving, not folded.
pub(crate) fn folded_subject(node: &Node) -> Option<(u8, u8)> {
if node.output.gathers {
return None;
}
node.inputs
.iter()
.enumerate()
@@ -303,14 +354,9 @@ fn field_writes(attrs: &[LevelAttr], core_types: &TokenStream2) -> Vec<TokenStre
}
fn level_delta(node: &Node) -> i8 {
// A materialized subject contributes no base layout, so the delta is
// relative to the fresh (empty) base.
let base_depth = node
.inputs
.iter()
.enumerate()
.find(|(index, input)| input.subject && materialized_levels(node, *index) == 0)
.map_or(0, |(_, input)| input.shape.depth as i8);
// A folded subject contributes no base layout, so the delta is relative to
// the fresh (empty) base.
let base_depth = layout_sources(node).first().map_or(0, |&index| node.inputs[index].shape.depth as i8);
node.output.shape.depth as i8 - base_depth
}
@@ -389,7 +435,11 @@ fn is_routing(node: &Node) -> bool {
fn has_attr_io(node: &Node) -> bool {
// Reads on lazy inputs ride the flip; only eager reads make a record-io node.
node.inputs.iter().any(|input| matches!(input.evaluation, Evaluation::Eager) && !input.shape.attrs.is_empty()) || !node.output.shape.attrs.is_empty() || !node.output.removes.is_empty()
// A gathered output takes the record tail regardless of its write set.
node.output.gathers
|| node.inputs.iter().any(|input| matches!(input.evaluation, Evaluation::Eager) && !input.shape.attrs.is_empty())
|| !node.output.shape.attrs.is_empty()
|| !node.output.removes.is_empty()
}
/// Levels of `input[index]` the output does not carry; `> 0` folds the input
@@ -484,6 +534,9 @@ pub(crate) enum Evaluation {
pub(crate) struct Output {
pub(crate) shape: ItemShape,
pub(crate) removes: Vec<LevelAttr>,
/// The element position spells `Lane<T>`, so the output frame is a copy of a
/// chosen subject lane.
pub(crate) gathers: bool,
}
/// An item's ranked layout; `attrs` are reads on an input, writes on the output.
@@ -545,13 +598,7 @@ mod tests {
};
let subject_depth = node.inputs.iter().find(|input| input.subject).map_or(0, |input| input.shape.depth as i8);
Facts {
sources: node
.inputs
.iter()
.enumerate()
.filter(|(index, input)| input.subject && materialized_levels(node, *index) == 0)
.map(|(index, _)| index)
.collect(),
sources: layout_sources(node),
carried,
writes: markers(node.output.shape.attrs.iter().map(|attr| &attr.marker)),
removes: markers(node.output.removes.iter().map(|attr| &attr.marker)),

View File

@@ -110,10 +110,15 @@ fn validate_record_io(parsed: &ParsedNodeFn) {
);
return;
};
if lazy_carrier && !crate::codegen::ir::build(parsed).derives {
let node = crate::codegen::ir::build(parsed);
if lazy_carrier && !node.derives {
emit_error!(parsed.input.pat_ident.span(), "a lazy record carrier evaluates at derived contexts; spell `impl Ctx + DeriveCtx`");
return;
}
if node.output.gathers && crate::codegen::ir::gathered_subject(&node).is_none() {
emit_error!(parsed.output_type.span(), "a `Lane` output gathers a materialized subject; give the primary input an `IList` type");
return;
}
let no_carrier = matches!(carrier_ty, Type::Tuple(tuple) if tuple.elems.is_empty());
let token = match (no_carrier, &carrier.ty) {
@@ -131,12 +136,12 @@ fn validate_record_io(parsed: &ParsedNodeFn) {
None => {
if let Some(ident) = crate::codegen::unbounded_generic(parsed, element) {
emit_error!(parsed.output_type.span(), "the returned generic element `{}` has no matching input", ident);
} else if !no_carrier && crate::codegen::contains_open_generic(parsed, carrier_ty) {
} else if !no_carrier && !node.output.gathers && crate::codegen::contains_open_generic(parsed, carrier_ty) {
emit_error!(
carrier.pat_ident.span(),
"record element reads are monomorphic for now; use a concrete element type or an unbounded passthrough generic"
);
} else if crate::codegen::contains_open_generic(parsed, element) {
} else if !node.output.gathers && crate::codegen::contains_open_generic(parsed, element) {
emit_error!(parsed.output_type.span(), "a written element must be a concrete type");
}
}