Stamp value types on layout fields and include type identity in layout equality

This commit is contained in:
Dennis Kobert
2026-08-27 21:17:31 +00:00
parent e8aea0728a
commit 4cb711e3f8
+51 -61
View File
@@ -19,6 +19,7 @@ pub struct FieldWrite {
pub level: u8, pub level: u8,
pub size: usize, pub size: usize,
pub align: usize, pub align: usize,
pub type_id: std::any::TypeId,
pub read_erased: unsafe fn(*const u8) -> Box<dyn crate::list::AnyAttributeValue>, pub read_erased: unsafe fn(*const u8) -> Box<dyn crate::list::AnyAttributeValue>,
pub repark: Option<unsafe fn(&dyn crate::list::AnyAttributeValue, *mut u8, &crate::arena::Arena) -> Option<()>>, pub repark: Option<unsafe fn(&dyn crate::list::AnyAttributeValue, *mut u8, &crate::arena::Arena) -> Option<()>>,
/// Hashes the field's content. `None` means the stored bytes are the /// Hashes the field's content. `None` means the stored bytes are the
@@ -31,7 +32,7 @@ pub struct FieldWrite {
impl FieldWrite { impl FieldWrite {
pub fn of<A: crate::attribute::Attribute>(level: u8) -> Self pub fn of<A: crate::attribute::Attribute>(level: u8) -> Self
where where
A::Value<'static>: graphene_hash::CacheHash + PartialEq, A::Value<'static>: graphene_hash::CacheHash + PartialEq + 'static,
{ {
unsafe fn content_hash<V: graphene_hash::CacheHash>(ptr: *const u8, state: &mut dyn core::hash::Hasher) { unsafe fn content_hash<V: graphene_hash::CacheHash>(ptr: *const u8, state: &mut dyn core::hash::Hasher) {
let mut state = state; let mut state = state;
@@ -45,6 +46,7 @@ impl FieldWrite {
level, level,
size: size_of::<A::Value<'static>>(), size: size_of::<A::Value<'static>>(),
align: align_of::<A::Value<'static>>(), align: align_of::<A::Value<'static>>(),
type_id: std::any::TypeId::of::<A::Value<'static>>(),
read_erased: A::read_erased, read_erased: A::read_erased,
repark: A::REPARK, repark: A::REPARK,
content_hash: Some(content_hash::<A::Value<'static>>), content_hash: Some(content_hash::<A::Value<'static>>),
@@ -55,9 +57,10 @@ impl FieldWrite {
/// One field of a [`Layout`]: a (name, level) key resolved to an offset. /// One field of a [`Layout`]: a (name, level) key resolved to an offset.
/// Levels are numbered innermost-out; only level 0 exists at rank 0. /// Levels are numbered innermost-out; only level 0 exists at rank 0.
/// Equality is structural: the glue pointer is excluded, since fn-pointer /// Equality is structural over the field's identity, its value type
/// identity is not guaranteed across codegen units and layout equality /// included; only the glue pointers are excluded, since fn-pointer identity
/// drives identity forwarding. /// is not guaranteed across codegen units and layout equality drives
/// identity forwarding.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct FieldDesc { pub struct FieldDesc {
pub name: &'static str, pub name: &'static str,
@@ -65,6 +68,7 @@ pub struct FieldDesc {
pub offset: usize, pub offset: usize,
pub size: usize, pub size: usize,
pub align: usize, pub align: usize,
pub type_id: std::any::TypeId,
pub read_erased: unsafe fn(*const u8) -> Box<dyn crate::list::AnyAttributeValue>, pub read_erased: unsafe fn(*const u8) -> Box<dyn crate::list::AnyAttributeValue>,
pub repark: Option<unsafe fn(&dyn crate::list::AnyAttributeValue, *mut u8, &crate::arena::Arena) -> Option<()>>, pub repark: Option<unsafe fn(&dyn crate::list::AnyAttributeValue, *mut u8, &crate::arena::Arena) -> Option<()>>,
/// Hashes the field's content. `None` means the stored bytes are the /// Hashes the field's content. `None` means the stored bytes are the
@@ -76,7 +80,25 @@ pub struct FieldDesc {
impl PartialEq for FieldDesc { impl PartialEq for FieldDesc {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
(self.name, self.level, self.offset, self.size, self.align) == (other.name, other.level, other.offset, other.size, other.align) (self.name, self.level, self.offset, self.size, self.align, self.type_id) == (other.name, other.level, other.offset, other.size, other.align, other.type_id)
}
}
impl FieldDesc {
/// The write this field re-declares when a layout's fields fold into
/// another layout.
pub fn as_write(&self) -> FieldWrite {
FieldWrite {
name: self.name,
level: self.level,
size: self.size,
align: self.align,
type_id: self.type_id,
read_erased: self.read_erased,
repark: self.repark,
content_hash: self.content_hash,
content_eq: self.content_eq,
}
} }
} }
@@ -84,8 +106,9 @@ impl Eq for FieldDesc {}
/// The element slot of a layout: its dimensions plus erased glue bound where /// The element slot of a layout: its dimensions plus erased glue bound where
/// the element type is statically known, so generic consumers read or /// the element type is statically known, so generic consumers read or
/// deep-copy the element without it. Equality is structural: glue pointers /// deep-copy the element without it. Equality is structural over the
/// are excluded for the same reason as [`FieldDesc`]'s. /// element's identity, its type included; glue pointers are excluded for the
/// same reason as [`FieldDesc`]'s.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct ElementWrite { pub struct ElementWrite {
pub size: usize, pub size: usize,
@@ -103,7 +126,7 @@ pub struct ElementWrite {
impl PartialEq for ElementWrite { impl PartialEq for ElementWrite {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
(self.size, self.align, self.parked) == (other.size, other.align, other.parked) (self.size, self.align, self.parked, self.type_id) == (other.size, other.align, other.parked, other.type_id)
} }
} }
@@ -176,23 +199,10 @@ impl Layout {
/// size is a type conflict and panics; the census keeps declared names to /// size is a type conflict and panics; the census keeps declared names to
/// one type, so this only fires on wiring bugs. /// one type, so this only fires on wiring bugs.
pub fn with_writes(&self, depth: u8, element: ElementWrite, writes: &[FieldWrite]) -> Layout { pub fn with_writes(&self, depth: u8, element: ElementWrite, writes: &[FieldWrite]) -> Layout {
let mut merged: Vec<FieldWrite> = self let mut merged: Vec<FieldWrite> = self.fields.iter().map(FieldDesc::as_write).collect();
.fields
.iter()
.map(|field| FieldWrite {
name: field.name,
level: field.level,
size: field.size,
align: field.align,
read_erased: field.read_erased,
repark: field.repark,
content_hash: field.content_hash,
content_eq: field.content_eq,
})
.collect();
for &write in writes { for &write in writes {
match merged.iter().find(|field| field.name == write.name && field.level == write.level) { match merged.iter().find(|field| field.name == write.name && field.level == write.level) {
Some(existing) => assert_eq!(existing.size, write.size, "attribute `{}` written at two different sizes", write.name), Some(existing) => assert_eq!(existing.type_id, write.type_id, "attribute `{}` written at two different types", write.name),
None => merged.push(write), None => merged.push(write),
} }
} }
@@ -210,6 +220,7 @@ impl Layout {
offset, offset,
size: write.size, size: write.size,
align: write.align, align: write.align,
type_id: write.type_id,
read_erased: write.read_erased, read_erased: write.read_erased,
repark: write.repark, repark: write.repark,
content_hash: write.content_hash, content_hash: write.content_hash,
@@ -231,21 +242,7 @@ impl Layout {
/// This layout minus the named fields, offsets recomputed. Removing an /// This layout minus the named fields, offsets recomputed. Removing an
/// absent name is a no-op: downstream reads yield the default either way. /// absent name is a no-op: downstream reads yield the default either way.
pub fn without(&self, removes: &[(&str, u8)]) -> Layout { pub fn without(&self, removes: &[(&str, u8)]) -> Layout {
let retained: Vec<FieldWrite> = self let retained: Vec<FieldWrite> = self.fields.iter().filter(|field| !removes.contains(&(field.name, field.level))).map(FieldDesc::as_write).collect();
.fields
.iter()
.filter(|field| !removes.contains(&(field.name, field.level)))
.map(|field| FieldWrite {
name: field.name,
level: field.level,
size: field.size,
align: field.align,
read_erased: field.read_erased,
repark: field.repark,
content_hash: field.content_hash,
content_eq: field.content_eq,
})
.collect();
Layout::default().with_writes(self.depth, self.element, &retained) Layout::default().with_writes(self.depth, self.element, &retained)
} }
@@ -258,20 +255,7 @@ impl Layout {
let mut union = Layout::default().with_writes(depth, first.element, &[]); let mut union = Layout::default().with_writes(depth, first.element, &[]);
for layout in layouts { for layout in layouts {
assert_eq!(union.element, layout.element, "union layouts must share the element"); assert_eq!(union.element, layout.element, "union layouts must share the element");
let writes: Vec<FieldWrite> = layout let writes: Vec<FieldWrite> = layout.fields.iter().map(FieldDesc::as_write).collect();
.fields
.iter()
.map(|field| FieldWrite {
name: field.name,
level: field.level,
size: field.size,
align: field.align,
read_erased: field.read_erased,
repark: field.repark,
content_hash: field.content_hash,
content_eq: field.content_eq,
})
.collect();
union = union.with_writes(union.depth, union.element, &writes); union = union.with_writes(union.depth, union.element, &writes);
} }
union union
@@ -1799,13 +1783,16 @@ impl<'e> RunBuilder<'e> {
/// Writes the marker's value on an already pushed lane. The layout must /// Writes the marker's value on an already pushed lane. The layout must
/// carry the marker among its field writes. /// carry the marker among its field writes.
pub fn attr<A: crate::attribute::Attribute>(&mut self, lane: usize, value: A::Value<'e>) { pub fn attr<A: crate::attribute::Attribute>(&mut self, lane: usize, value: A::Value<'e>)
where
A::Value<'static>: 'static,
{
assert!(lane < self.pushed, "attributes write onto pushed lanes"); assert!(lane < self.pushed, "attributes write onto pushed lanes");
let offset = self.layout.offset_of(A::NAME, 0).expect("the layout carries the written marker"); let field = self.layout.fields.iter().find(|field| field.name == A::NAME && field.level == 0).expect("the layout carries the written marker");
let field = self.layout.fields.iter().find(|field| field.name == A::NAME && field.level == 0).expect("resolved above"); let offset = field.offset;
assert_eq!(field.size, size_of::<A::Value<'e>>(), "the field was declared at the marker's value type"); assert_eq!(field.type_id, std::any::TypeId::of::<A::Value<'static>>(), "the field was declared at the marker's value type");
// SAFETY: the offset comes from the builder's own layout and the size // SAFETY: the offset comes from the builder's own layout and the value
// matches the marker's value type. // type matches the field's declared type.
unsafe { self.frames.add(lane * self.layout.lane_stride() + offset).cast::<A::Value<'e>>().write(value) }; unsafe { self.frames.add(lane * self.layout.lane_stride() + offset).cast::<A::Value<'e>>().write(value) };
} }
@@ -2265,6 +2252,7 @@ mod tests {
level: 0, level: 0,
size, size,
align, align,
type_id: std::any::TypeId::of::<()>(),
read_erased: unread, read_erased: unread,
repark: None, repark: None,
content_hash: None, content_hash: None,
@@ -2339,10 +2327,12 @@ mod tests {
} }
#[test] #[test]
#[should_panic(expected = "two different sizes")] #[should_panic(expected = "two different types")]
fn size_conflicts_panic() { fn type_conflicts_panic() {
let mut conflicting = sized_field("opacity", 8, 8);
conflicting.type_id = std::any::TypeId::of::<u64>();
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("opacity")]); let layout = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("opacity")]);
layout.with_writes(0, element_write::<f64>(), &[sized_field("opacity", 4, 4)]); layout.with_writes(0, element_write::<f64>(), &[conflicting]);
} }
#[test] #[test]