use crate::schema_canonical::{Schema, TypeRepr};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldKind {
Inline {
size: usize,
align: usize,
},
PointerIndirect {
tail_alignment: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListElementKind {
InlineFixed {
elem_size: usize,
elem_align: usize,
},
PointerArray {
elem_alignment: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldOffset {
pub name: String,
pub offset: usize,
pub size: usize,
pub align: usize,
pub kind: FieldKind,
pub list_element: Option<ListElementKind>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OffsetTable {
pub fields: Vec<FieldOffset>,
pub root_size: usize,
pub root_align: usize,
}
impl OffsetTable {
pub fn fixed_area_size(&self) -> usize {
self.root_size
}
pub fn requires_tail_area(&self) -> bool {
self.fields
.iter()
.any(|f| matches!(f.kind, FieldKind::PointerIndirect { .. }))
}
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum LayoutError {
#[error("layout v1 does not yet support type `{kind}` in field `{field}`")]
UnsupportedTypeInLayoutV1 {
field: String,
kind: &'static str,
},
#[error("layout v1 does not yet support list element `{inner}` in field `{field}`")]
UnsupportedListElement {
field: String,
inner: &'static str,
},
#[error("layout overflow while placing field `{field}` at offset {offset}")]
Overflow {
field: String,
offset: usize,
},
}
struct FieldLayoutDecision {
kind: FieldKind,
list_element: Option<ListElementKind>,
}
fn field_layout_decision_for(
field_name: &str,
ty: &TypeRepr,
) -> Result<FieldLayoutDecision, LayoutError> {
match ty {
TypeRepr::Unit => Ok(FieldLayoutDecision {
kind: FieldKind::Inline { size: 1, align: 1 },
list_element: None,
}),
TypeRepr::Bool => Ok(FieldLayoutDecision {
kind: FieldKind::Inline { size: 1, align: 1 },
list_element: None,
}),
TypeRepr::Int => Ok(FieldLayoutDecision {
kind: FieldKind::Inline { size: 8, align: 8 },
list_element: None,
}),
TypeRepr::Float => Ok(FieldLayoutDecision {
kind: FieldKind::Inline { size: 8, align: 8 },
list_element: None,
}),
TypeRepr::String => Ok(FieldLayoutDecision {
kind: FieldKind::PointerIndirect { tail_alignment: 1 },
list_element: None,
}),
TypeRepr::List { element } => list_layout_decision(field_name, element.as_ref()),
TypeRepr::Schema { schema } => {
let sub = SchemaLayout::offsets_for(schema)?;
Ok(FieldLayoutDecision {
kind: FieldKind::PointerIndirect {
tail_alignment: sub.root_align,
},
list_element: None,
})
}
TypeRepr::Option { .. } | TypeRepr::Result { .. } | TypeRepr::Enum { .. } => {
Ok(FieldLayoutDecision {
kind: FieldKind::PointerIndirect {
tail_alignment: variant_record_alignment(field_name, ty)?,
},
list_element: None,
})
}
TypeRepr::Closure { .. } => Err(LayoutError::UnsupportedTypeInLayoutV1 {
field: field_name.to_string(),
kind: "Closure",
}),
}
}
fn list_layout_decision(
field_name: &str,
element: &TypeRepr,
) -> Result<FieldLayoutDecision, LayoutError> {
match element {
TypeRepr::Int | TypeRepr::Float => Ok(FieldLayoutDecision {
kind: FieldKind::PointerIndirect { tail_alignment: 8 },
list_element: Some(ListElementKind::InlineFixed {
elem_size: 8,
elem_align: 8,
}),
}),
TypeRepr::Bool => Ok(FieldLayoutDecision {
kind: FieldKind::PointerIndirect { tail_alignment: 4 },
list_element: Some(ListElementKind::InlineFixed {
elem_size: 1,
elem_align: 1,
}),
}),
TypeRepr::String => Ok(FieldLayoutDecision {
kind: FieldKind::PointerIndirect { tail_alignment: 4 },
list_element: Some(ListElementKind::PointerArray { elem_alignment: 4 }),
}),
TypeRepr::Schema { schema } => {
let sub = SchemaLayout::offsets_for(schema)?;
Ok(FieldLayoutDecision {
kind: FieldKind::PointerIndirect { tail_alignment: 4 },
list_element: Some(ListElementKind::PointerArray {
elem_alignment: sub.root_align,
}),
})
}
TypeRepr::List { element: inner } => {
let inner_align = inner_list_record_alignment(field_name, inner)?;
Ok(FieldLayoutDecision {
kind: FieldKind::PointerIndirect { tail_alignment: 4 },
list_element: Some(ListElementKind::PointerArray {
elem_alignment: inner_align,
}),
})
}
TypeRepr::Option { .. } | TypeRepr::Result { .. } | TypeRepr::Enum { .. } => {
Ok(FieldLayoutDecision {
kind: FieldKind::PointerIndirect { tail_alignment: 4 },
list_element: Some(ListElementKind::PointerArray {
elem_alignment: variant_record_alignment(field_name, element)?,
}),
})
}
TypeRepr::Unit => Err(LayoutError::UnsupportedListElement {
field: field_name.to_string(),
inner: "Unit",
}),
TypeRepr::Closure { .. } => Err(LayoutError::UnsupportedListElement {
field: field_name.to_string(),
inner: "Closure",
}),
}
}
fn inner_list_record_alignment(field_name: &str, inner: &TypeRepr) -> Result<usize, LayoutError> {
match inner {
TypeRepr::Int | TypeRepr::Float => Ok(8),
TypeRepr::Bool => Ok(4),
TypeRepr::String => Ok(4),
TypeRepr::Schema { schema } => {
SchemaLayout::offsets_for(schema)?;
Ok(4)
}
TypeRepr::List { element: deeper } => {
inner_list_record_alignment(field_name, deeper)?;
Ok(4)
}
TypeRepr::Option { .. } | TypeRepr::Result { .. } | TypeRepr::Enum { .. } => {
variant_record_alignment(field_name, inner)?;
Ok(4)
}
other => Err(LayoutError::UnsupportedListElement {
field: field_name.to_string(),
inner: match other {
TypeRepr::Unit => "Unit",
TypeRepr::Closure { .. } => "Closure",
_ => "List",
},
}),
}
}
fn variant_payload_slot(field_name: &str, ty: &TypeRepr) -> Result<(usize, usize), LayoutError> {
match ty {
TypeRepr::Unit | TypeRepr::Bool => Ok((1, 1)),
TypeRepr::Int | TypeRepr::Float => Ok((8, 8)),
TypeRepr::String
| TypeRepr::List { .. }
| TypeRepr::Schema { .. }
| TypeRepr::Option { .. }
| TypeRepr::Result { .. }
| TypeRepr::Enum { .. } => {
ensure_type_layoutable(field_name, ty)?;
Ok((4, 4))
}
TypeRepr::Closure { .. } => Err(LayoutError::UnsupportedTypeInLayoutV1 {
field: field_name.to_string(),
kind: "Closure",
}),
}
}
fn type_graph_alignment(field_name: &str, ty: &TypeRepr) -> Result<usize, LayoutError> {
match ty {
TypeRepr::Unit | TypeRepr::Bool | TypeRepr::String => Ok(1),
TypeRepr::Int | TypeRepr::Float => Ok(8),
TypeRepr::List { element } => match element.as_ref() {
TypeRepr::Int | TypeRepr::Float => Ok(8),
TypeRepr::Bool => Ok(1),
other => type_graph_alignment(field_name, other),
},
TypeRepr::Schema { schema } => {
let sub = SchemaLayout::offsets_for(schema)?;
let tail = schema
.fields
.iter()
.map(|f| type_graph_alignment(&f.name, &f.ty))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.max()
.unwrap_or(1);
Ok(sub.root_align.max(tail))
}
TypeRepr::Option { .. } | TypeRepr::Result { .. } | TypeRepr::Enum { .. } => {
variant_record_alignment(field_name, ty)
}
TypeRepr::Closure { .. } => Err(LayoutError::UnsupportedTypeInLayoutV1 {
field: field_name.to_string(),
kind: "Closure",
}),
}
}
fn ensure_type_layoutable(field_name: &str, ty: &TypeRepr) -> Result<(), LayoutError> {
match ty {
TypeRepr::List { element } => {
list_layout_decision(field_name, element)?;
Ok(())
}
TypeRepr::Schema { schema } => {
SchemaLayout::offsets_for(schema)?;
Ok(())
}
TypeRepr::Option { .. } | TypeRepr::Result { .. } | TypeRepr::Enum { .. } => {
variant_record_alignment(field_name, ty)?;
Ok(())
}
TypeRepr::Closure { .. } => Err(LayoutError::UnsupportedTypeInLayoutV1 {
field: field_name.to_string(),
kind: "Closure",
}),
_ => Ok(()),
}
}
fn variant_record_alignment(field_name: &str, ty: &TypeRepr) -> Result<usize, LayoutError> {
let payloads: Vec<TypeRepr> = match ty {
TypeRepr::Option { inner } => vec![inner.as_ref().clone()],
TypeRepr::Result { ok, err } => vec![ok.as_ref().clone(), err.as_ref().clone()],
TypeRepr::Enum { name, variants } => variants
.iter()
.filter_map(|variant| {
variant.payload_schema(name).map(|schema| TypeRepr::Schema {
schema: Box::new(schema),
})
})
.collect(),
other => {
return Err(LayoutError::UnsupportedTypeInLayoutV1 {
field: field_name.to_string(),
kind: match other {
TypeRepr::Closure { .. } => "Closure",
_ => "Variant",
},
})
}
};
let mut align = 4usize;
for payload in &payloads {
let (_, slot_align) = variant_payload_slot(field_name, payload)?;
let graph_align = type_graph_alignment(field_name, payload)?;
align = align.max(slot_align).max(graph_align).max(1);
}
Ok(align)
}
fn align_up(value: usize, align: usize) -> Option<usize> {
debug_assert!(align != 0, "alignment must be non-zero");
value.checked_next_multiple_of(align)
}
pub struct SchemaLayout;
impl SchemaLayout {
pub fn offsets_for(schema: &Schema) -> Result<OffsetTable, LayoutError> {
let mut fields: Vec<FieldOffset> = Vec::with_capacity(schema.fields.len());
let mut cursor: usize = 0;
let mut max_align: usize = 1;
for field in &schema.fields {
let decision = field_layout_decision_for(&field.name, &field.ty)?;
let kind = decision.kind;
let (size, align) = match kind {
FieldKind::Inline { size, align } => (size, align),
FieldKind::PointerIndirect { .. } => (4, 4),
};
let offset = align_up(cursor, align).ok_or_else(|| LayoutError::Overflow {
field: field.name.clone(),
offset: cursor,
})?;
let next = offset
.checked_add(size)
.ok_or_else(|| LayoutError::Overflow {
field: field.name.clone(),
offset,
})?;
cursor = next;
if align > max_align {
max_align = align;
}
fields.push(FieldOffset {
name: field.name.clone(),
offset,
size,
align,
kind,
list_element: decision.list_element,
});
}
let root_size = if schema.fields.is_empty() {
0
} else {
align_up(cursor, max_align).ok_or_else(|| LayoutError::Overflow {
field: "<root padding>".to_string(),
offset: cursor,
})?
};
Ok(OffsetTable {
fields,
root_size,
root_align: max_align,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema_canonical::{Field, Schema};
fn field(name: &str, ty: TypeRepr) -> Field {
Field {
name: name.into(),
ty,
default: None,
}
}
#[test]
fn pure_int_fields_pack_at_eight_byte_stride() {
let schema = Schema {
name: "Trio".into(),
generics: vec![],
is_tuple: false,
fields: vec![
field("a", TypeRepr::Int),
field("b", TypeRepr::Int),
field("c", TypeRepr::Int),
],
};
let table = SchemaLayout::offsets_for(&schema).expect("layout");
assert_eq!(table.fields[0].offset, 0);
assert_eq!(table.fields[1].offset, 8);
assert_eq!(table.fields[2].offset, 16);
assert_eq!(table.root_size, 24);
assert_eq!(table.root_align, 8);
}
#[test]
fn int_then_bool_pads_after_int() {
let schema = Schema {
name: "Pair".into(),
generics: vec![],
is_tuple: false,
fields: vec![field("score", TypeRepr::Int), field("flag", TypeRepr::Bool)],
};
let table = SchemaLayout::offsets_for(&schema).expect("layout");
assert_eq!(table.fields[0].offset, 0);
assert_eq!(table.fields[0].size, 8);
assert_eq!(table.fields[1].offset, 8);
assert_eq!(table.fields[1].size, 1);
assert_eq!(table.root_size, 16);
assert_eq!(table.root_align, 8);
}
#[test]
fn bool_then_int_pads_between_fields() {
let schema = Schema {
name: "Pair".into(),
generics: vec![],
is_tuple: false,
fields: vec![field("flag", TypeRepr::Bool), field("score", TypeRepr::Int)],
};
let table = SchemaLayout::offsets_for(&schema).expect("layout");
assert_eq!(table.fields[0].offset, 0);
assert_eq!(table.fields[1].offset, 8);
assert_eq!(table.root_size, 16);
assert_eq!(table.root_align, 8);
}
#[test]
fn pure_bool_fields_pack_tightly() {
let schema = Schema {
name: "Flags".into(),
generics: vec![],
is_tuple: false,
fields: vec![
field("a", TypeRepr::Bool),
field("b", TypeRepr::Bool),
field("c", TypeRepr::Bool),
],
};
let table = SchemaLayout::offsets_for(&schema).expect("layout");
assert_eq!(table.fields[0].offset, 0);
assert_eq!(table.fields[1].offset, 1);
assert_eq!(table.fields[2].offset, 2);
assert_eq!(table.root_size, 3);
assert_eq!(table.root_align, 1);
}
#[test]
fn empty_schema_has_zero_size_and_align_one() {
let schema = Schema {
name: "Unit".into(),
generics: vec![],
is_tuple: false,
fields: vec![],
};
let table = SchemaLayout::offsets_for(&schema).expect("layout");
assert!(table.fields.is_empty());
assert_eq!(table.root_size, 0);
assert_eq!(table.root_align, 1);
}
#[test]
fn string_field_takes_one_pointer_slot() {
let schema = Schema {
name: "Greet".into(),
generics: vec![],
is_tuple: false,
fields: vec![field("name", TypeRepr::String)],
};
let table = SchemaLayout::offsets_for(&schema).expect("layout");
assert_eq!(table.fields[0].offset, 0);
assert_eq!(table.fields[0].size, 4);
assert_eq!(table.fields[0].align, 4);
assert!(matches!(
table.fields[0].kind,
FieldKind::PointerIndirect { tail_alignment: 1 }
));
assert_eq!(table.root_size, 4);
assert_eq!(table.root_align, 4);
assert!(table.requires_tail_area());
assert_eq!(table.fixed_area_size(), 4);
}
#[test]
fn list_of_int_field_takes_one_pointer_slot() {
let schema = Schema {
name: "Nums".into(),
generics: vec![],
is_tuple: false,
fields: vec![field(
"nums",
TypeRepr::List {
element: Box::new(TypeRepr::Int),
},
)],
};
let table = SchemaLayout::offsets_for(&schema).expect("layout");
assert_eq!(table.fields[0].offset, 0);
assert_eq!(table.fields[0].size, 4);
assert_eq!(table.fields[0].align, 4);
assert!(matches!(
table.fields[0].kind,
FieldKind::PointerIndirect { tail_alignment: 8 }
));
}
#[test]
fn list_of_string_takes_pointer_array_layout() {
let schema = Schema {
name: "Names".into(),
generics: vec![],
is_tuple: false,
fields: vec![field(
"names",
TypeRepr::List {
element: Box::new(TypeRepr::String),
},
)],
};
let table = SchemaLayout::offsets_for(&schema).expect("layout");
assert_eq!(table.fields[0].offset, 0);
assert_eq!(table.fields[0].size, 4);
assert_eq!(table.fields[0].align, 4);
assert!(matches!(
table.fields[0].kind,
FieldKind::PointerIndirect { tail_alignment: 4 }
));
assert!(matches!(
table.fields[0].list_element,
Some(ListElementKind::PointerArray { elem_alignment: 4 })
));
}
#[test]
fn list_of_float_takes_inline_eight_byte_elements() {
let schema = Schema {
name: "Speeds".into(),
generics: vec![],
is_tuple: false,
fields: vec![field(
"xs",
TypeRepr::List {
element: Box::new(TypeRepr::Float),
},
)],
};
let table = SchemaLayout::offsets_for(&schema).expect("layout");
assert!(matches!(
table.fields[0].kind,
FieldKind::PointerIndirect { tail_alignment: 8 }
));
assert!(matches!(
table.fields[0].list_element,
Some(ListElementKind::InlineFixed {
elem_size: 8,
elem_align: 8
})
));
}
#[test]
fn list_of_bool_packs_one_byte_per_element() {
let schema = Schema {
name: "Flags".into(),
generics: vec![],
is_tuple: false,
fields: vec![field(
"xs",
TypeRepr::List {
element: Box::new(TypeRepr::Bool),
},
)],
};
let table = SchemaLayout::offsets_for(&schema).expect("layout");
assert!(matches!(
table.fields[0].kind,
FieldKind::PointerIndirect { tail_alignment: 4 }
));
assert!(matches!(
table.fields[0].list_element,
Some(ListElementKind::InlineFixed {
elem_size: 1,
elem_align: 1
})
));
}
#[test]
fn list_of_nested_scalar_list_is_pointer_array() {
let schema = Schema {
name: "Nested".into(),
generics: vec![],
is_tuple: false,
fields: vec![field(
"xs",
TypeRepr::List {
element: Box::new(TypeRepr::List {
element: Box::new(TypeRepr::Int),
}),
},
)],
};
let table = SchemaLayout::offsets_for(&schema).expect("nested scalar list accepted");
assert_eq!(
table.fields[0].list_element,
Some(ListElementKind::PointerArray { elem_alignment: 8 })
);
}
#[test]
fn list_of_inner_pointer_array_list_is_accepted() {
let schema = Schema {
name: "Nested".into(),
generics: vec![],
is_tuple: false,
fields: vec![field(
"xs",
TypeRepr::List {
element: Box::new(TypeRepr::List {
element: Box::new(TypeRepr::String),
}),
},
)],
};
let table = SchemaLayout::offsets_for(&schema).expect("List<List<String>> accepted");
assert_eq!(
table.fields[0].list_element,
Some(ListElementKind::PointerArray { elem_alignment: 4 })
);
}
#[test]
fn list_of_inner_list_with_option_leaf_is_accepted() {
let schema = Schema {
name: "Nested".into(),
generics: vec![],
is_tuple: false,
fields: vec![field(
"xs",
TypeRepr::List {
element: Box::new(TypeRepr::List {
element: Box::new(TypeRepr::Option {
inner: Box::new(TypeRepr::Int),
}),
}),
},
)],
};
let table = SchemaLayout::offsets_for(&schema).expect("nested Option list must layout");
assert_eq!(
table.fields[0].list_element,
Some(ListElementKind::PointerArray { elem_alignment: 4 })
);
}
#[test]
fn list_of_schema_picks_subrecord_alignment() {
let inner = Schema {
name: "Inner".into(),
generics: vec![],
is_tuple: false,
fields: vec![field("v", TypeRepr::Int)],
};
let schema = Schema {
name: "Outer".into(),
generics: vec![],
is_tuple: false,
fields: vec![field(
"xs",
TypeRepr::List {
element: Box::new(TypeRepr::Schema {
schema: Box::new(inner),
}),
},
)],
};
let table = SchemaLayout::offsets_for(&schema).expect("layout");
assert!(matches!(
table.fields[0].list_element,
Some(ListElementKind::PointerArray { elem_alignment: 8 })
));
}
#[test]
fn string_then_int_packs_pointer_then_padding_then_int() {
let schema = Schema {
name: "Mixed".into(),
generics: vec![],
is_tuple: false,
fields: vec![field("name", TypeRepr::String), field("age", TypeRepr::Int)],
};
let table = SchemaLayout::offsets_for(&schema).expect("layout");
assert_eq!(table.fields[0].offset, 0);
assert_eq!(table.fields[0].size, 4);
assert_eq!(table.fields[1].offset, 8);
assert_eq!(table.fields[1].size, 8);
assert_eq!(table.root_size, 16);
assert_eq!(table.root_align, 8);
assert!(table.requires_tail_area());
}
#[test]
fn null_field_is_one_byte_one_aligned() {
let schema = Schema {
name: "Sentinel".into(),
generics: vec![],
is_tuple: false,
fields: vec![field("nil", TypeRepr::Unit)],
};
let table = SchemaLayout::offsets_for(&schema).expect("layout");
assert_eq!(table.fields[0].offset, 0);
assert_eq!(table.fields[0].size, 1);
assert_eq!(table.fields[0].align, 1);
assert_eq!(table.root_size, 1);
assert_eq!(table.root_align, 1);
}
#[test]
fn float_field_is_eight_byte_aligned() {
let schema = Schema {
name: "Phys".into(),
generics: vec![],
is_tuple: false,
fields: vec![
field("flag", TypeRepr::Bool),
field("mass", TypeRepr::Float),
],
};
let table = SchemaLayout::offsets_for(&schema).expect("layout");
assert_eq!(table.fields[1].offset, 8);
assert_eq!(table.fields[1].size, 8);
assert_eq!(table.fields[1].align, 8);
assert_eq!(table.root_size, 16);
}
}