Resolve a named read's offset when the graph compiles

The fold already holds the name and the read input's finished layout, so
the offset falls out there rather than at construction: `RecordLayout`
carries the resolved numbers and `set_layout` copies them into the read
slots. Constructors are untouched, and census-marker reads keep their
current installation.

A read meets the value type the name was written at, so a disagreement
between a read here and a write upstream is the same graph error as two
writes disagreeing; the one-name-one-type check now spans reads and
writes together. An absent attribute stays absent and the read serves
the forced default rather than reporting it.

`read_attribute` is the catalog's get half, typed and never `Option` at
the kernel boundary, with the name declared exactly as the write side
declares it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dennis Kobert
2026-09-09 10:50:08 +00:00
parent fc94487d0b
commit 5eb80721dd
23 changed files with 349 additions and 69 deletions

View File

@@ -984,8 +984,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
// more: the name is spent resolving the layout when the graph compiles, so
// it reaches neither the kernel's parameters nor its call. The wire input
// stays, since the fold reads the constant off it.
let kernel_omits =
|field: &ParsedField| injected_name(&field.pat_ident.ident) || matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { name_source: Some(_), .. }));
let kernel_omits = |field: &ParsedField| injected_name(&field.pat_ident.ident) || matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { name_source: Some(_), .. }));
let where_predicates: Vec<TokenStream2> = parsed.where_clause.iter().flat_map(|clause| clause.predicates.iter()).map(|predicate| quote!(#predicate)).collect();
let NodeFields {
@@ -1731,7 +1730,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
// kernel gets a fresh one; reference-valued writes name their real
// lifetime explicitly and pass through untouched. An async source's value
// outlives the evaluation, so its writes are `'static` instead.
let attr_injected = record_io.then(|| inject_attr_lifetimes(&parsed.output_type, if async_source { "'static" } else { "'__attr" })).flatten();
let attr_injected = record_io
.then(|| inject_attr_lifetimes(&parsed.output_type, if async_source { "'static" } else { "'__attr" }))
.flatten();
let attr_lifetime = (attr_injected.is_some() && !async_source).then(|| quote!('__attr,));
let lane_injected = gather_carrier
.then(|| crate::codegen::classify::inject_lane_lifetime(attr_injected.as_ref().unwrap_or(&parsed.output_type)))
@@ -2545,9 +2546,24 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
}
})
.collect();
// A name-generic read's offset was resolved against the input it reads,
// which only the compiler sees, so installing it is a copy.
let mut folded_read = 0usize;
let read_installs: Vec<TokenStream2> = flat_reads
.iter()
.enumerate()
.filter(|(_, (_, read))| crate::parsing::named_marker(&read.marker).is_some())
.map(|(slot, _)| {
let slot = format_ident!("__read_{slot}");
let position = folded_read;
folded_read += 1;
quote!(self.#slot = __resolved.named_reads[#position];)
})
.collect();
let plan = (!skips_carrier || gather_carrier).then(|| quote!(self.__plan = __resolved.plan;));
Some(quote! {
#(#write_installs)*
#(#read_installs)*
self.__frame_bytes = __resolved.frame_bytes;
self.__lane_invariant = __resolved.lane_invariant;
#plan
@@ -2671,6 +2687,12 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let read_inits = flat_reads.iter().enumerate().map(|(slot, (owner, read))| {
let marker = &read.marker;
let slot = format_ident!("__read_{slot}");
// A name-generic read has no marker name to look up here; the
// compiler resolved its offset against the input's own layout, so
// `set_layout` installs the number.
if crate::parsing::named_marker(marker).is_some() {
return quote!(let #slot = ::core::option::Option::None;);
}
let source = match !skips_carrier && *owner == 0 {
true => quote!(__carrier_layout),
false => format_ident!("__in_{owner}").to_token_stream(),