Row a reading input's element generic instead of erasing it

This commit is contained in:
Dennis Kobert
2026-09-14 12:53:11 +02:00
parent 93d9b595ef
commit 53608ae7bf
4 changed files with 58 additions and 14 deletions
+15 -7
View File
@@ -134,7 +134,10 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let ctx_ident_for_flip = context_param(parsed).map(|ctx| ctx.ident.clone()); let ctx_ident_for_flip = context_param(parsed).map(|ctx| ctx.ident.clone());
let carries_generic = |ident: &Ident| { let carries_generic = |ident: &Ident| {
regular_fields.iter().enumerate().any(|(index, field)| match &field.ty { regular_fields.iter().enumerate().any(|(index, field)| match &field.ty {
ParsedFieldType::Regular(RegularParsedField { ty, list_levels, .. }) => (*list_levels > 0 || (record_io && index > 0)) && type_contains_ident(ty, ident), // A reading carrier spelling its rows out reaches the kernel concrete, so the struct carries its element generic like a ranked or secondary one.
ParsedFieldType::Regular(RegularParsedField { ty, list_levels, implementations, .. }) => {
(*list_levels > 0 || (record_io && (index > 0 || !implementations.is_empty()))) && type_contains_ident(ty, ident)
}
_ => false, _ => false,
}) })
}; };
@@ -804,10 +807,14 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
// A gather carrier copies the returned lane's frame, so it needs the plan // A gather carrier copies the returned lane's frame, so it needs the plan
// without a carrier layout of its own. // without a carrier layout of its own.
let gather_carrier = record_io && node.output.gathers; let gather_carrier = record_io && node.output.gathers;
// A carrier spelling its rows out is concrete in each row and the struct carries its element
// generic, so it reads like a concrete element; a passthrough carrier keeps its opaque token.
let carrier_rows = matches!(parsed.fields.first().map(|field| &field.ty), Some(ParsedFieldType::Regular(RegularParsedField { implementations, .. })) if !implementations.is_empty());
// A gathered element is carried by the copy plan, not as a lazy token, so // A gathered element is carried by the copy plan, not as a lazy token, so
// its generic stays a struct parameter. // its generic stays a struct parameter.
let record_token = match (kind, &node.output.shape.element) { let record_token = match (kind, &node.output.shape.element) {
(crate::codegen::ir::NodeKind::RecordIo, crate::codegen::ir::Element::Generic(ident)) if !gather_carrier => Some(ident.clone()), // A carrier spelling its rows out monomorphizes the element per row, so it stays a real struct parameter rather than an erased token.
(crate::codegen::ir::NodeKind::RecordIo, crate::codegen::ir::Element::Generic(ident)) if !gather_carrier && !carrier_rows => Some(ident.clone()),
// An opaque reading input's element is byte-carried the same way, even // An opaque reading input's element is byte-carried the same way, even
// though the output replaces it rather than carrying it through. // though the output replaces it rather than carrying it through.
_ => crate::codegen::classify::opaque_reading_carrier(parsed), _ => crate::codegen::classify::opaque_reading_carrier(parsed),
@@ -820,8 +827,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
crate::codegen::ir::Element::Concrete(ty) if !gather_carrier => Some(ty), crate::codegen::ir::Element::Concrete(ty) if !gather_carrier => Some(ty),
_ => None, _ => None,
}; };
let carrier_read_ty: Option<&Type> = node.inputs.first().filter(|input| input.subject).and_then(|input| match &input.shape.element { let carrier_read_ty: Option<Type> = node.inputs.first().filter(|input| input.subject).and_then(|input| match &input.shape.element {
crate::codegen::ir::Element::Concrete(ty) => Some(ty), crate::codegen::ir::Element::Concrete(ty) => Some(ty.clone()),
crate::codegen::ir::Element::Generic(ident) if carrier_rows => Some(syn::parse_quote!(#ident)),
_ => None, _ => None,
}); });
let subject_depth = node.inputs.iter().find(|input| input.subject).map_or(0, |input| input.shape.depth); let subject_depth = node.inputs.iter().find(|input| input.subject).map_or(0, |input| input.shape.depth);
@@ -1890,7 +1898,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
(record_io && async_source && !skips_carrier).then(|| { (record_io && async_source && !skips_carrier).then(|| {
let field = regular_fields[0]; let field = regular_fields[0];
let name = &field.pat_ident.ident; let name = &field.pat_ident.ident;
let ty = carrier_read_ty.expect("a carrying record source reads a concrete element"); let ty = carrier_read_ty.as_ref().expect("a carrying record source reads a concrete element");
quote! { quote! {
let __src = match __cell.eval_input(0, &self.#name, __input, __frame.frames()) { let __src = match __cell.eval_input(0, &self.#name, __input, __frame.frames()) {
Ok(value) => value, Ok(value) => value,
@@ -2033,7 +2041,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
// The kernel drives the derived carrier itself through its handle. // The kernel drives the derived carrier itself through its handle.
let name = &regular_fields[0].pat_ident.ident; let name = &regular_fields[0].pat_ident.ident;
Some(quote!(#name)) Some(quote!(#name))
} else if let Some(ty) = carrier_read_ty { } else if let Some(ty) = carrier_read_ty.as_ref() {
Some(tuple_arg(regular_fields[0], quote!(unsafe { #core_types::record::read_element::<#ty>(__src_rec) }))) Some(tuple_arg(regular_fields[0], quote!(unsafe { #core_types::record::read_element::<#ty>(__src_rec) })))
} else { } else {
Some(tuple_arg(regular_fields[0], quote!(#core_types::record::ElToken))) Some(tuple_arg(regular_fields[0], quote!(#core_types::record::ElToken)))
@@ -2464,7 +2472,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
_ => None, _ => None,
}), }),
); );
if let Some(ty) = carrier_read_ty { if let Some(ty) = carrier_read_ty.as_ref() {
bounds.push({ bounds.push({
let ty = &crate::codegen::classify::substitute_lifetimes(ty, "'static"); let ty = &crate::codegen::classify::substitute_lifetimes(ty, "'static");
quote!(#ty: ::core::clone::Clone) quote!(#ty: ::core::clone::Clone)
@@ -370,12 +370,14 @@ pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option<RecordShape> {
}; };
// A gathered subject is never read as an element, so its generic stays open. // A gathered subject is never read as an element, so its generic stays open.
let gathers = crate::codegen::ir::gathers_lane(parsed); let gathers = crate::codegen::ir::gathers_lane(parsed);
// A carrier spelling its rows out is concrete in each of them, so its element generic monomorphizes with the row rather than staying open.
let carrier_rows = !implementations.is_empty();
let token = match ty { let token = match ty {
Type::Tuple(tuple) if tuple.elems.is_empty() => None, Type::Tuple(tuple) if tuple.elems.is_empty() => None,
ty => match implementations.is_empty().then(|| unbounded_generic(parsed, ty)).flatten() { ty => match implementations.is_empty().then(|| unbounded_generic(parsed, ty)).flatten() {
Some(token) => Some(token), Some(token) => Some(token),
None => { None => {
if !gathers && contains_open_generic(parsed, ty) { if !gathers && !carrier_rows && contains_open_generic(parsed, ty) {
return None; return None;
} }
None None
@@ -405,7 +407,7 @@ pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option<RecordShape> {
} }
} }
None => { None => {
if !gathers && contains_open_generic(parsed, &element) { if !gathers && !carrier_rows && contains_open_generic(parsed, &element) {
return None; return None;
} }
} }
+32 -2
View File
@@ -277,6 +277,9 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields
// A record-io node's plain secondary reaches the constructor concrete, so its generic monomorphizes the row like a ranked element does. // A record-io node's plain secondary reaches the constructor concrete, so its generic monomorphizes the row like a ranked element does.
let record_secondary = let record_secondary =
|index: usize| matches!(ir::node_kind(&node), ir::NodeKind::RecordIo) && index > 0 && !node.inputs[index].subject && matches!(&regular_fields[index].ty, ParsedFieldType::Regular(_)); |index: usize| matches!(ir::node_kind(&node), ir::NodeKind::RecordIo) && index > 0 && !node.inputs[index].subject && matches!(&regular_fields[index].ty, ParsedFieldType::Regular(_));
// A reading carrier that spells its rows out is concrete in each of them, so its element generic monomorphizes the row
// (and with it the row's reads) rather than erasing. A passthrough carrier names no implementations and stays erased.
let record_carrier = |index: usize| matches!(ir::node_kind(&node), ir::NodeKind::RecordIo) && node.inputs[index].subject && matches!(&regular_fields[index].ty, ParsedFieldType::Regular(_));
let names_generic = |index: usize, generic: &Ident| match &regular_fields[index].ty { let names_generic = |index: usize, generic: &Ident| match &regular_fields[index].ty {
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => crate::codegen::type_contains_ident(ty, generic), ParsedFieldType::Regular(RegularParsedField { ty, .. }) => crate::codegen::type_contains_ident(ty, generic),
_ => false, _ => false,
@@ -292,13 +295,14 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields
GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() => Some(type_param.ident.clone()), GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() => Some(type_param.ident.clone()),
_ => None, _ => None,
}) })
.filter(|ident| (0..regular_fields.len()).any(|index| (ranked(index) || record_secondary(index)) && names_generic(index, ident))) .filter(|ident| (0..regular_fields.len()).any(|index| ((ranked(index) || record_secondary(index)) && names_generic(index, ident)) || (record_carrier(index) && solves_generic(index, ident))))
.collect(); .collect();
// Ranked sources come first, so a generic a ranked input already carries keeps sourcing its rows from that input. // Ranked sources come first, so a generic a ranked input already carries keeps sourcing its rows from that input.
let carried_source = |generic: &Ident| { let carried_source = |generic: &Ident| {
(0..regular_fields.len()) (0..regular_fields.len())
.find(|&index| ranked(index) && solves_generic(index, generic)) .find(|&index| ranked(index) && solves_generic(index, generic))
.or_else(|| (0..regular_fields.len()).find(|&index| record_secondary(index) && solves_generic(index, generic))) .or_else(|| (0..regular_fields.len()).find(|&index| record_secondary(index) && solves_generic(index, generic)))
.or_else(|| (0..regular_fields.len()).find(|&index| record_carrier(index) && solves_generic(index, generic)))
}; };
let carried: Option<Vec<(Ident, usize)>> = carried_generic_idents.iter().map(|ident| carried_source(ident).map(|index| (ident.clone(), index))).collect(); let carried: Option<Vec<(Ident, usize)>> = carried_generic_idents.iter().map(|ident| carried_source(ident).map(|index| (ident.clone(), index))).collect();
let Some(carried) = carried else { let Some(carried) = carried else {
@@ -340,7 +344,11 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields
let slots: Vec<SlotKind> = slots let slots: Vec<SlotKind> = slots
.iter() .iter()
.map(|slot| match slot { .map(|slot| match slot {
SlotKind::BaseGeneric(name) => SlotKind::BaseGeneric(name.clone()), // A generic subject the row assigns is that row's concrete carrier.
SlotKind::BaseGeneric(name) => match assignments.iter().find(|(generic, _)| generic == name) {
Some((_, ty)) => SlotKind::BaseConcrete(substitute_lifetimes(ty, "'static")),
None => SlotKind::BaseGeneric(name.clone()),
},
SlotKind::BaseConcrete(ty) => SlotKind::BaseConcrete(substitute_lifetimes(&substitute_ident_types(ty, assignments), "'static")), SlotKind::BaseConcrete(ty) => SlotKind::BaseConcrete(substitute_lifetimes(&substitute_ident_types(ty, assignments), "'static")),
SlotKind::Value(ty) => SlotKind::Value(substitute_lifetimes(&substitute_ident_types(ty, assignments), "'static")), SlotKind::Value(ty) => SlotKind::Value(substitute_lifetimes(&substitute_ident_types(ty, assignments), "'static")),
SlotKind::Extracted(ty) => SlotKind::Extracted(substitute_lifetimes(&substitute_ident_types(ty, assignments), "'static")), SlotKind::Extracted(ty) => SlotKind::Extracted(substitute_lifetimes(&substitute_ident_types(ty, assignments), "'static")),
@@ -665,4 +673,26 @@ mod tests {
assert!(generated.contains(&row), "the row carries the leveled element {element}: {generated}"); assert!(generated.contains(&row), "the row carries the leveled element {element}: {generated}");
} }
} }
/// A reading carrier that spells its rows out monomorphizes per row, so its
/// element generic reaches the registry concrete instead of erasing to a
/// token. Without this the node compiles but registers nothing.
#[test]
fn a_reading_carrier_rows_its_element_generic() {
let entries = entries_of(
quote!(category("")),
quote!(
fn round<V: Clone + Send + Sync>(_: impl Ctx, #[implementations(Graphic, Vector)] (content, transform): (V, Attr<Transform>), radius: f64) -> (V, Attr<Transform>) {
todo!()
}
),
);
assert!(entries.contains("fn round_entries"), "a reading carrier with implementations must emit its entries fn: {entries}");
assert_eq!(entries.matches("constructor :").count(), 2, "one row per implementation: {entries}");
for element in ["Graphic", "Vector"] {
let row = format!("record_source_type :: < {element} > ()");
assert!(entries.contains(&row), "the implementations row {element} is missing: {entries}");
}
assert!(!entries.contains("< V >"), "every row instantiates the carried element generic: {entries}");
}
} }
+7 -3
View File
@@ -80,7 +80,8 @@ fn validate_record_io(parsed: &ParsedNodeFn) {
emit_error!(field.pat_ident.span(), "attribute-only inputs are not supported yet; the value component cannot be `()`"); emit_error!(field.pat_ident.span(), "attribute-only inputs are not supported yet; the value component cannot be `()`");
} }
let is_token_carrier = index == 0 && implementations.is_empty() && crate::codegen::unbounded_generic(parsed, ty).is_some(); let is_token_carrier = index == 0 && implementations.is_empty() && crate::codegen::unbounded_generic(parsed, ty).is_some();
if !is_token_carrier && crate::codegen::contains_open_generic(parsed, ty) { // An input spelling its rows out is concrete in each of them, so its reads monomorphize with the row.
if !is_token_carrier && implementations.is_empty() && crate::codegen::contains_open_generic(parsed, ty) {
emit_error!( emit_error!(
field.pat_ident.span(), field.pat_ident.span(),
"a reading input's value is monomorphic for now; use a concrete type or an unbounded passthrough generic in the primary input" "a reading input's value is monomorphic for now; use a concrete type or an unbounded passthrough generic in the primary input"
@@ -110,6 +111,9 @@ fn validate_record_io(parsed: &ParsedNodeFn) {
); );
return; return;
}; };
// A carrier spelling its rows out is concrete in each of them, so its element generic is not an open one.
let carrier_rows = matches!(&carrier.ty, ParsedFieldType::Regular(RegularParsedField { implementations, .. }) if !implementations.is_empty());
let node = crate::codegen::ir::build(parsed); let node = crate::codegen::ir::build(parsed);
if lazy_carrier && !node.derives { if lazy_carrier && !node.derives {
emit_error!(parsed.input.pat_ident.span(), "a lazy record carrier evaluates at derived contexts; spell `impl Ctx + DeriveCtx`"); emit_error!(parsed.input.pat_ident.span(), "a lazy record carrier evaluates at derived contexts; spell `impl Ctx + DeriveCtx`");
@@ -149,12 +153,12 @@ fn validate_record_io(parsed: &ParsedNodeFn) {
None => { None => {
if let Some(ident) = crate::codegen::unbounded_generic(parsed, element) { 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); emit_error!(parsed.output_type.span(), "the returned generic element `{}` has no matching input", ident);
} else if !no_carrier && !node.output.gathers && crate::codegen::contains_open_generic(parsed, carrier_ty) { } else if !no_carrier && !node.output.gathers && !carrier_rows && crate::codegen::contains_open_generic(parsed, carrier_ty) {
emit_error!( emit_error!(
carrier.pat_ident.span(), carrier.pat_ident.span(),
"record element reads are monomorphic for now; use a concrete element type or an unbounded passthrough generic" "record element reads are monomorphic for now; use a concrete element type or an unbounded passthrough generic"
); );
} else if !node.output.gathers && crate::codegen::contains_open_generic(parsed, element) { } else if !node.output.gathers && !carrier_rows && crate::codegen::contains_open_generic(parsed, element) {
emit_error!(parsed.output_type.span(), "a written element must be a concrete type"); emit_error!(parsed.output_type.span(), "a written element must be a concrete type");
} }
} }