Serve a declared name's census default on an absent read

An absent read of a name the census declares now serves that name's own
default rather than its value type's, which is the rule for a declared
name. The census stages it exactly as it fills any absent field, as the
declared default's bytes, so the read reuses the census's own mechanism
instead of a second one; the compiler takes them when the name folds and
`set_layout` installs them beside the offset.

A name the census does not declare keeps the value type's default, which
is that case's own rule. One name carries one value type, checked when
the name folds, so the row's value type and width agree with the read's.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dennis Kobert
2026-09-09 11:13:41 +00:00
parent fffcaa6555
commit 6e5ab01b76
11 changed files with 104 additions and 3 deletions

View File

@@ -392,6 +392,7 @@ impl ProtoNetwork {
lane_invariant,
named_writes: Vec::new(),
named_reads: Vec::new(),
named_read_defaults: Vec::new(),
layout,
}),
ConstructionArgs::Nodes(inputs) => node.resolved.layout_meta.as_ref().and_then(|meta| {

View File

@@ -841,6 +841,13 @@ mod test {
assert_eq!(read_back(read_attribute_network("opacity", "opacity", TaggedValue::F64(0.5))), 0.5);
}
#[test]
fn an_absent_census_named_read_serves_the_census_default() {
// `opacity` is declared `f64` defaulting to 1, so its absence reads as
// the census default rather than the value type's.
assert_eq!(read_back(read_attribute_network("novel:count", "opacity", TaggedValue::F64(2.5))), 1.);
}
#[test]
fn a_named_read_disagreeing_with_its_write_is_refused() {
// The name is written at a path upstream and read at `f64` here, which

View File

@@ -114,6 +114,29 @@ pub unsafe fn read_at<'e, A: attribute::Attribute>(rec: Rec<'_>, offset: Option<
})
}
/// [`read_at`] for a name-generic read, whose marker carries the value type
/// but not the name and so cannot know the name's own default. `absent` is
/// that default's bytes, which the compiler takes from the census once the
/// name is folded; without one the value type's default applies, which is
/// exactly the rule for a name the census does not declare.
///
/// # Safety
/// As [`read_at`]. `absent`, where present, must hold the object
/// representation of `A::Value`, which the compiler takes from the census row
/// for the folded name after checking that row's value type against `A`'s. A
/// reference-valued default addresses `'static` data, so the value it yields
/// outlives any evaluation.
pub unsafe fn read_at_defaulting<'e, A: attribute::Attribute>(rec: Rec<'_>, offset: Option<usize>, absent: Option<&[u8]>) -> attribute::Attr<'e, A> {
attribute::Attr(match (offset, absent) {
// SAFETY: the caller's contract.
(Some(offset), _) => unsafe { rec.read::<A::Value<'e>>(offset) },
// SAFETY: the caller's contract; the bytes are unaligned storage, so
// the value is read out rather than referenced in place.
(None, Some(bytes)) => unsafe { bytes.as_ptr().cast::<A::Value<'e>>().read_unaligned() },
(None, None) => A::default(),
})
}
/// The read-less [`DerivedLazyInput`] glue: the token alone.
///
/// # Safety

View File

@@ -377,6 +377,11 @@ pub struct RecordLayout {
/// name and the read input's finished layout sit together, so `set_layout`
/// only copies the numbers into the read slots.
pub named_reads: Vec<Option<usize>>,
/// The census default's bytes for each absent name-from-input read whose
/// name the census declares, in the same order. A name the census does not
/// declare has `None` and reads as its value type's default, which is that
/// case's own rule; a read that resolved to an offset needs no default.
pub named_read_defaults: Vec<Option<Box<[u8]>>>,
}
/// A write whose name comes from the graph rather than from a marker: the
@@ -552,12 +557,31 @@ impl LayoutMeta {
// A named read resolves against the input it reads, whose layout is
// finished by the time this node folds. An absent attribute stays
// `None`, which the read serves as the name's forced default.
let named_reads = self
let named_reads: Vec<Option<usize>> = self
.named_reads
.iter()
.zip(&self.folded_read_names)
.map(|(read, name)| inputs.get(read.input as usize).copied().flatten().and_then(|layout| layout.offset_of(name, read.template.level)))
.collect();
// An absent read serves the name's own default, which for a declared
// name is the census's rather than the value type's. The census stages
// it the same way it fills any absent field: as the declared default's
// bytes. One name carries one value type, checked when the name folds,
// so the size agreement below is a guard rather than a branch.
let named_read_defaults = self
.named_reads
.iter()
.zip(&self.folded_read_names)
.zip(&named_reads)
.map(|((read, name), offset)| {
let row = offset.is_none().then(|| attribute::info(name))??;
(row.value_type == read.template.type_id && row.size == read.template.size).then(|| {
let mut bytes = vec![0u8; row.size];
(row.write_default_bytes)(&mut bytes);
bytes.into_boxed_slice()
})
})
.collect();
RecordLayout {
layout,
frame_bytes,
@@ -565,6 +589,7 @@ impl LayoutMeta {
lane_invariant: 0,
named_writes: self.folded_names.clone(),
named_reads,
named_read_defaults,
}
}

View File

@@ -21,7 +21,7 @@ mod serve;
mod test_support;
mod testkit;
pub use access::{Rec, RecordValue, apply_plan, borrow_element, erase_static, read_at, read_element, token_only, write_element, write_element_sized, write_field};
pub use access::{Rec, RecordValue, apply_plan, borrow_element, erase_static, read_at, read_at_defaulting, read_element, token_only, write_element, write_element_sized, write_field};
pub use frames::{FrameArena, FrameScope, Frames};
pub use input::{DerivedLazyInput, DerivedRecordInput, ElementInput, ElementLazyInput, LevelStatus, RecordExtract, RecordInput, RecordLazyInput, fill_frames, materialize_batch, materialize_level};
pub use layout::{

View File

@@ -280,6 +280,7 @@ mod tests {
lane_invariant: u32::MAX,
named_writes: Vec::new(),
named_reads: Vec::new(),
named_read_defaults: Vec::new(),
});
RecordExtract::new(graph, &layout)
}

View File

@@ -240,6 +240,18 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let slot = format_ident!("__read_{index}");
quote!(pub(super) #slot: Option<usize>)
}));
// A name-generic read carries the folded name's own default beside its
// offset, since its marker names the value type but not the name.
state.extend(
field_reads(&struct_regular_fields)
.iter()
.enumerate()
.filter(|(_, (_, read))| crate::parsing::named_marker(&read.marker).is_some())
.map(|(index, _)| {
let slot = format_ident!("__read_default_{index}");
quote!(pub(super) #slot: Option<::std::boxed::Box<[u8]>>)
}),
);
state.extend((0..node.output.shape.attrs.len()).map(|index| {
let slot = format_ident!("__write_{index}");
quote!(pub(super) #slot: usize)
@@ -1259,7 +1271,15 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let read_binding = |slot: usize, read: &AttributeRead, rec: TokenStream2| {
let pat = &read.pat_ident;
let marker = &read.marker;
let default_slot = format_ident!("__read_default_{slot}");
let slot = format_ident!("__read_{slot}");
// A name-generic marker carries the value type but not the name, so it
// cannot know the name's own default; the compiler supplies it.
if crate::parsing::named_marker(marker).is_some() {
return quote! {
let #pat = unsafe { #core_types::record::read_at_defaulting::<#marker>(#rec, self.#slot, self.#default_slot.as_deref()) };
};
}
quote! {
let #pat = unsafe { #core_types::record::read_at::<#marker>(#rec, self.#slot) };
}
@@ -2540,10 +2560,14 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
.enumerate()
.filter(|(_, (_, read))| crate::parsing::named_marker(&read.marker).is_some())
.map(|(slot, _)| {
let default_slot = format_ident!("__read_default_{slot}");
let slot = format_ident!("__read_{slot}");
let position = folded_read;
folded_read += 1;
quote!(self.#slot = __resolved.named_reads[#position];)
quote! {
self.#slot = __resolved.named_reads[#position];
self.#default_slot = __resolved.named_read_defaults[#position].clone();
}
})
.collect();
let plan = (!skips_carrier || gather_carrier).then(|| quote!(self.__plan = __resolved.plan;));
@@ -2697,6 +2721,16 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
});
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,));
// The folded name's default arrives with the layout, so the constructor
// leaves the slot empty and `set_layout` fills it.
let read_default_inits = flat_reads
.iter()
.enumerate()
.filter(|(_, (_, read))| crate::parsing::named_marker(&read.marker).is_some())
.map(|(index, _)| {
let slot = format_ident!("__read_default_{index}");
quote!(#slot: ::core::option::Option::None,)
});
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}");
@@ -2743,6 +2777,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
__frame_bytes: 0,
__lane_invariant: 0,
#(#read_names)*
#(#read_default_inits)*
#(#write_defaults)*
#(#mat_cache_defaults)*
#(#slot_default)*

View File

@@ -664,6 +664,7 @@ mod tests {
let resolved = core_types::record::RecordLayout {
named_writes: Vec::new(),
named_reads: Vec::new(),
named_read_defaults: Vec::new(),
lane_invariant: u32::MAX,
..meta.resolve(inputs)
};
@@ -675,6 +676,7 @@ mod tests {
let bundle = core_types::record::RecordLayout {
named_writes: Vec::new(),
named_reads: Vec::new(),
named_read_defaults: Vec::new(),
frame_bytes: layout.frame_bytes(),
plan: Vec::new(),
layout: layout.clone(),
@@ -1581,6 +1583,7 @@ mod tests {
let resolved = core_types::record::RecordLayout {
named_writes: Vec::new(),
named_reads: Vec::new(),
named_read_defaults: Vec::new(),
lane_invariant: 0,
..mirror_layout_meta().resolve(&[Some(&layout)])
};

View File

@@ -287,6 +287,7 @@ mod tests {
let resolved = record::RecordLayout {
named_writes: Vec::new(),
named_reads: Vec::new(),
named_read_defaults: Vec::new(),
lane_invariant: u32::MAX,
..meta.resolve(inputs)
};
@@ -298,6 +299,7 @@ mod tests {
let bundle = record::RecordLayout {
named_writes: Vec::new(),
named_reads: Vec::new(),
named_read_defaults: Vec::new(),
frame_bytes: layout.frame_bytes(),
plan: Vec::new(),
layout: layout.clone(),

View File

@@ -274,6 +274,7 @@ mod tests {
lane_invariant: u32::MAX,
named_writes: Vec::new(),
named_reads: Vec::new(),
named_read_defaults: Vec::new(),
},
);
let GPoll::Final(result) = core_types::record::serve_input(&graph, &ctx, &frames) else {

View File

@@ -1603,6 +1603,7 @@ mod graphene_test {
node.set_layout(core_types::record::RecordLayout {
named_writes: Vec::new(),
named_reads: Vec::new(),
named_read_defaults: Vec::new(),
frame_bytes: layout.frame_bytes(),
plan: Vec::new(),
layout: layout.clone(),
@@ -1666,6 +1667,7 @@ mod graphene_test {
wired.set_layout(core_types::record::RecordLayout {
named_writes: Vec::new(),
named_reads: Vec::new(),
named_read_defaults: Vec::new(),
frame_bytes: layout.frame_bytes(),
plan: Vec::new(),
layout: layout.clone(),
@@ -1714,6 +1716,7 @@ mod graphene_test {
wired.set_layout(core_types::record::RecordLayout {
named_writes: Vec::new(),
named_reads: Vec::new(),
named_read_defaults: Vec::new(),
frame_bytes: layout.frame_bytes(),
plan: Vec::new(),
layout: layout.clone(),