From 6e5ab01b76cb64cb0a1992128bdd8d462341d35c Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Wed, 9 Sep 2026 11:13:41 +0000 Subject: [PATCH] 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 --- node-graph/graph-craft/src/proto.rs | 1 + .../src/dynamic_executor.rs | 7 ++++ .../libraries/core-types/src/record/access.rs | 23 ++++++++++++ .../libraries/core-types/src/record/layout.rs | 27 +++++++++++++- .../libraries/core-types/src/record/mod.rs | 2 +- .../libraries/core-types/src/runtime.rs | 1 + node-graph/node-macro/src/codegen.rs | 37 ++++++++++++++++++- node-graph/nodes/gcore/src/record.rs | 3 ++ node-graph/nodes/graphic/src/record.rs | 2 + node-graph/nodes/gstd/src/render_node.rs | 1 + node-graph/nodes/math/src/lib.rs | 3 ++ 11 files changed, 104 insertions(+), 3 deletions(-) diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 0cff96760a..fddf7d9748 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -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| { diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index e8f4e35280..09200278a2 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -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 diff --git a/node-graph/libraries/core-types/src/record/access.rs b/node-graph/libraries/core-types/src/record/access.rs index c03f8565d1..f1872ef258 100644 --- a/node-graph/libraries/core-types/src/record/access.rs +++ b/node-graph/libraries/core-types/src/record/access.rs @@ -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, absent: Option<&[u8]>) -> attribute::Attr<'e, A> { + attribute::Attr(match (offset, absent) { + // SAFETY: the caller's contract. + (Some(offset), _) => unsafe { rec.read::>(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::>().read_unaligned() }, + (None, None) => A::default(), + }) +} + /// The read-less [`DerivedLazyInput`] glue: the token alone. /// /// # Safety diff --git a/node-graph/libraries/core-types/src/record/layout.rs b/node-graph/libraries/core-types/src/record/layout.rs index 1ddf6f3f02..d322398076 100644 --- a/node-graph/libraries/core-types/src/record/layout.rs +++ b/node-graph/libraries/core-types/src/record/layout.rs @@ -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>, + /// 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>>, } /// 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> = 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, } } diff --git a/node-graph/libraries/core-types/src/record/mod.rs b/node-graph/libraries/core-types/src/record/mod.rs index 4dddfc6861..33d6bb2ef2 100644 --- a/node-graph/libraries/core-types/src/record/mod.rs +++ b/node-graph/libraries/core-types/src/record/mod.rs @@ -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::{ diff --git a/node-graph/libraries/core-types/src/runtime.rs b/node-graph/libraries/core-types/src/runtime.rs index f4d4f70910..fe365d2850 100644 --- a/node-graph/libraries/core-types/src/runtime.rs +++ b/node-graph/libraries/core-types/src/runtime.rs @@ -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) } diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index a2ba32db08..5adfb9264d 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -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) })); + // 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(®ular_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)* diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index a9e3565243..92d688be54 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -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)]) }; diff --git a/node-graph/nodes/graphic/src/record.rs b/node-graph/nodes/graphic/src/record.rs index e71ded31f0..597c3866cc 100644 --- a/node-graph/nodes/graphic/src/record.rs +++ b/node-graph/nodes/graphic/src/record.rs @@ -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(), diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 6afb8e7acd..4d0e975ec7 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -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 { diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index a4969f8fe9..3b6c1ed421 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -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(),