use formalang::ast::{PrimitiveType, Visibility};
use formalang::ir::{IrEnum, IrModule, IrSpan, IrStruct, IrTrait, ResolvedType};
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum LayoutError {
#[error("type {kind} is not yet supported by the struct-layout planner")]
NotYetSupported {
kind: String,
},
#[error("struct '{name}' size exceeds u32::MAX after alignment padding")]
SizeOverflow {
name: String,
},
#[error("enum '{name}' has more than u32::MAX variants")]
TooManyVariants {
name: String,
},
#[error("enum '{name}' is uninhabited (zero variants)")]
UninhabitedEnum {
name: String,
},
}
#[expect(
clippy::exhaustive_structs,
reason = "plain layout record consumed externally; intentionally constructible"
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FieldLayout {
pub offset: u32,
pub size: u32,
pub align: u32,
}
#[expect(
clippy::exhaustive_structs,
reason = "plain layout record consumed externally; intentionally constructible"
)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StructLayout {
pub size: u32,
pub align: u32,
pub fields: Vec<FieldLayout>,
}
pub fn plan_struct(s: &IrStruct, _module: &IrModule) -> Result<StructLayout, LayoutError> {
let mut offset: u32 = 0;
let mut max_align: u32 = 1;
let mut fields = Vec::with_capacity(s.fields.len());
for f in &s.fields {
let (size, align) = type_size_align(&f.ty)?;
let aligned = align_up(offset, align).ok_or_else(|| LayoutError::SizeOverflow {
name: s.name.clone(),
})?;
let next_offset = aligned
.checked_add(size)
.ok_or_else(|| LayoutError::SizeOverflow {
name: s.name.clone(),
})?;
fields.push(FieldLayout {
offset: aligned,
size,
align,
});
offset = next_offset;
if align > max_align {
max_align = align;
}
}
let total_size = align_up(offset, max_align).ok_or_else(|| LayoutError::SizeOverflow {
name: s.name.clone(),
})?;
Ok(StructLayout {
size: total_size,
align: max_align,
fields,
})
}
fn primitive_size_align(p: PrimitiveType) -> Result<(u32, u32), LayoutError> {
match p {
PrimitiveType::Boolean => Ok((1, 1)),
PrimitiveType::I32 | PrimitiveType::F32 => Ok((4, 4)),
PrimitiveType::I64 | PrimitiveType::F64 => Ok((8, 8)),
PrimitiveType::String | PrimitiveType::Path | PrimitiveType::Regex => {
Ok((POINTER_SIZE, POINTER_ALIGN))
}
PrimitiveType::Never | _ => Err(LayoutError::NotYetSupported {
kind: format!("{p:?}"),
}),
}
}
fn type_size_align(ty: &ResolvedType) -> Result<(u32, u32), LayoutError> {
match ty {
ResolvedType::Primitive(p) => primitive_size_align(*p),
ResolvedType::Struct(_)
| ResolvedType::Enum(_)
| ResolvedType::Tuple(_)
| ResolvedType::Generic { .. }
| ResolvedType::Closure { .. }
| ResolvedType::Trait(_) => Ok((POINTER_SIZE, POINTER_ALIGN)),
ResolvedType::TypeParam(name) => Err(LayoutError::NotYetSupported {
kind: format!("TypeParam({name})"),
}),
ResolvedType::External { name, .. } => Err(LayoutError::NotYetSupported {
kind: format!(
"External({name}) — should have been inlined by upstream MonomorphisePass; reaching the backend means an upstream invariant violation"
),
}),
ResolvedType::Error => Err(LayoutError::NotYetSupported {
kind: "Error".to_owned(),
}),
}
}
fn align_up(offset: u32, align: u32) -> Option<u32> {
if align <= 1 {
return Some(offset);
}
let mask = align.checked_sub(1)?;
let added = offset.checked_add(mask)?;
Some(added & !mask)
}
pub const ENUM_TAG_SIZE: u32 = 4;
pub const ENUM_TAG_ALIGN: u32 = 4;
#[expect(
clippy::exhaustive_structs,
reason = "plain layout record consumed externally; intentionally constructible"
)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VariantLayout {
pub name: String,
pub tag: u32,
pub fields: Vec<FieldLayout>,
}
#[expect(
clippy::exhaustive_structs,
reason = "plain layout record consumed externally; intentionally constructible"
)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumLayout {
pub size: u32,
pub align: u32,
pub tag_offset: u32,
pub payload_offset: u32,
pub variants: Vec<VariantLayout>,
}
pub fn plan_enum(e: &IrEnum, module: &IrModule) -> Result<EnumLayout, LayoutError> {
if e.variants.is_empty() {
return Err(LayoutError::UninhabitedEnum {
name: e.name.clone(),
});
}
let mut variant_payloads = Vec::with_capacity(e.variants.len());
let mut max_payload_align: u32 = 1;
let mut max_payload_size: u32 = 0;
for variant in &e.variants {
let placeholder = IrStruct {
name: format!("{}::{}", e.name, variant.name),
visibility: Visibility::Private,
traits: Vec::new(),
fields: variant.fields.clone(),
generic_params: Vec::new(),
doc: None,
span: IrSpan::default(),
};
let payload = plan_struct(&placeholder, module)?;
if payload.align > max_payload_align {
max_payload_align = payload.align;
}
if payload.size > max_payload_size {
max_payload_size = payload.size;
}
variant_payloads.push(payload);
}
let enum_align = if max_payload_align > ENUM_TAG_ALIGN {
max_payload_align
} else {
ENUM_TAG_ALIGN
};
let payload_offset =
align_up(ENUM_TAG_SIZE, max_payload_align).ok_or_else(|| LayoutError::SizeOverflow {
name: e.name.clone(),
})?;
let raw_size = payload_offset
.checked_add(max_payload_size)
.ok_or_else(|| LayoutError::SizeOverflow {
name: e.name.clone(),
})?;
let total_size = align_up(raw_size, enum_align).ok_or_else(|| LayoutError::SizeOverflow {
name: e.name.clone(),
})?;
let mut variants = Vec::with_capacity(e.variants.len());
for (i, (variant, payload)) in e
.variants
.iter()
.zip(variant_payloads.into_iter())
.enumerate()
{
let tag = u32::try_from(i).map_err(|_| LayoutError::TooManyVariants {
name: e.name.clone(),
})?;
let mut absolute_fields = Vec::with_capacity(payload.fields.len());
for field in &payload.fields {
let abs_offset = payload_offset.checked_add(field.offset).ok_or_else(|| {
LayoutError::SizeOverflow {
name: e.name.clone(),
}
})?;
absolute_fields.push(FieldLayout {
offset: abs_offset,
size: field.size,
align: field.align,
});
}
variants.push(VariantLayout {
name: variant.name.clone(),
tag,
fields: absolute_fields,
});
}
Ok(EnumLayout {
size: total_size,
align: enum_align,
tag_offset: 0,
payload_offset,
variants,
})
}
pub const ARRAY_HEADER_SIZE: u32 = 12;
pub const ARRAY_HEADER_ALIGN: u32 = 4;
pub const ARRAY_HEADER_PTR_OFFSET: u32 = 0;
pub const ARRAY_HEADER_LEN_OFFSET: u32 = 4;
pub const ARRAY_HEADER_CAP_OFFSET: u32 = 8;
pub(crate) const POINTER_SIZE: u32 = 4;
const POINTER_ALIGN: u32 = 4;
#[expect(
clippy::exhaustive_structs,
reason = "plain layout record consumed externally; intentionally constructible"
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ArrayLayout {
pub header_size: u32,
pub header_align: u32,
pub element_size: u32,
pub element_align: u32,
}
pub fn plan_array(elem: &ResolvedType, _module: &IrModule) -> Result<ArrayLayout, LayoutError> {
let (element_size, element_align) = array_element_size_align(elem)?;
Ok(ArrayLayout {
header_size: ARRAY_HEADER_SIZE,
header_align: ARRAY_HEADER_ALIGN,
element_size,
element_align,
})
}
#[expect(
clippy::exhaustive_structs,
reason = "plain layout record consumed externally; intentionally constructible"
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RangeLayout {
pub size: u32,
pub align: u32,
pub bound_size: u32,
pub bound_align: u32,
pub end_offset: u32,
}
pub fn plan_range(bound: &ResolvedType, _module: &IrModule) -> Result<RangeLayout, LayoutError> {
let (bound_size, bound_align) = match bound {
ResolvedType::Primitive(p) => primitive_size_align(*p)?,
ResolvedType::Struct(_)
| ResolvedType::Enum(_)
| ResolvedType::Tuple(_)
| ResolvedType::Closure { .. }
| ResolvedType::Trait(_)
| ResolvedType::Generic { .. }
| ResolvedType::TypeParam(_)
| ResolvedType::External { .. }
| ResolvedType::Error => {
return Err(LayoutError::NotYetSupported {
kind: format!("Range<{bound:?}>"),
});
}
};
let end_offset =
align_up(bound_size, bound_align).ok_or_else(|| LayoutError::SizeOverflow {
name: "<range>".to_owned(),
})?;
let raw_size = end_offset
.checked_add(bound_size)
.ok_or_else(|| LayoutError::SizeOverflow {
name: "<range>".to_owned(),
})?;
let size = align_up(raw_size, bound_align).ok_or_else(|| LayoutError::SizeOverflow {
name: "<range>".to_owned(),
})?;
Ok(RangeLayout {
size,
align: bound_align,
bound_size,
bound_align,
end_offset,
})
}
pub const DICTIONARY_HEADER_SIZE: u32 = ARRAY_HEADER_SIZE;
pub const DICTIONARY_HEADER_ALIGN: u32 = ARRAY_HEADER_ALIGN;
#[expect(
clippy::exhaustive_structs,
reason = "plain layout record consumed externally; intentionally constructible"
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DictionaryLayout {
pub header_size: u32,
pub header_align: u32,
pub entry_size: u32,
pub entry_align: u32,
}
#[must_use]
pub const fn plan_dictionary(
_key_ty: &ResolvedType,
_value_ty: &ResolvedType,
_module: &IrModule,
) -> DictionaryLayout {
DictionaryLayout {
header_size: DICTIONARY_HEADER_SIZE,
header_align: DICTIONARY_HEADER_ALIGN,
entry_size: POINTER_SIZE,
entry_align: POINTER_ALIGN,
}
}
pub const STRING_HEADER_SIZE: u32 = 8;
pub const STRING_HEADER_ALIGN: u32 = 4;
pub const STRING_PTR_OFFSET: u32 = 0;
pub const STRING_LEN_OFFSET: u32 = 4;
#[expect(
clippy::exhaustive_structs,
reason = "plain layout record consumed externally; intentionally constructible"
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StringLayout {
pub header_size: u32,
pub header_align: u32,
pub ptr_offset: u32,
pub len_offset: u32,
}
#[must_use]
pub const fn plan_string(_module: &IrModule) -> StringLayout {
StringLayout {
header_size: STRING_HEADER_SIZE,
header_align: STRING_HEADER_ALIGN,
ptr_offset: STRING_PTR_OFFSET,
len_offset: STRING_LEN_OFFSET,
}
}
pub const OPTIONAL_TAG_SIZE: u32 = 4;
pub const OPTIONAL_TAG_ALIGN: u32 = 4;
pub const OPTIONAL_TAG_NIL: u32 = 0;
pub const OPTIONAL_TAG_SOME: u32 = 1;
#[expect(
clippy::exhaustive_structs,
reason = "plain layout record consumed externally; intentionally constructible"
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OptionalLayout {
pub size: u32,
pub align: u32,
pub tag_offset: u32,
pub payload_offset: u32,
pub payload_size: u32,
pub payload_align: u32,
}
pub fn plan_optional(
inner: &ResolvedType,
_module: &IrModule,
) -> Result<OptionalLayout, LayoutError> {
if matches!(inner, ResolvedType::Primitive(PrimitiveType::Never)) {
return Ok(OptionalLayout {
size: OPTIONAL_TAG_SIZE,
align: OPTIONAL_TAG_ALIGN,
tag_offset: 0,
payload_offset: OPTIONAL_TAG_SIZE,
payload_size: 0,
payload_align: 1,
});
}
let (payload_size, payload_align) = optional_payload_size_align(inner)?;
let total_align = if payload_align > OPTIONAL_TAG_ALIGN {
payload_align
} else {
OPTIONAL_TAG_ALIGN
};
let payload_offset =
align_up(OPTIONAL_TAG_SIZE, payload_align).ok_or_else(|| LayoutError::SizeOverflow {
name: "<optional>".to_owned(),
})?;
let raw_size =
payload_offset
.checked_add(payload_size)
.ok_or_else(|| LayoutError::SizeOverflow {
name: "<optional>".to_owned(),
})?;
let size = align_up(raw_size, total_align).ok_or_else(|| LayoutError::SizeOverflow {
name: "<optional>".to_owned(),
})?;
Ok(OptionalLayout {
size,
align: total_align,
tag_offset: 0,
payload_offset,
payload_size,
payload_align,
})
}
fn optional_payload_size_align(ty: &ResolvedType) -> Result<(u32, u32), LayoutError> {
match ty {
ResolvedType::Primitive(p) => primitive_size_align(*p),
ResolvedType::Struct(_)
| ResolvedType::Enum(_)
| ResolvedType::Tuple(_)
| ResolvedType::Generic { .. }
| ResolvedType::Trait(_) => Ok((POINTER_SIZE, POINTER_ALIGN)),
ResolvedType::Closure { .. } => Err(LayoutError::NotYetSupported {
kind: "Closure".to_owned(),
}),
ResolvedType::TypeParam(name) => Err(LayoutError::NotYetSupported {
kind: format!("TypeParam({name})"),
}),
ResolvedType::External { name, .. } => Err(LayoutError::NotYetSupported {
kind: format!(
"External({name}) — should have been inlined by upstream MonomorphisePass; reaching the backend means an upstream invariant violation"
),
}),
ResolvedType::Error => Err(LayoutError::NotYetSupported {
kind: "Error".to_owned(),
}),
}
}
fn array_element_size_align(ty: &ResolvedType) -> Result<(u32, u32), LayoutError> {
match ty {
ResolvedType::Primitive(p) => primitive_size_align(*p),
ResolvedType::Struct(_)
| ResolvedType::Enum(_)
| ResolvedType::Tuple(_)
| ResolvedType::Generic { .. }
| ResolvedType::Trait(_) => Ok((POINTER_SIZE, POINTER_ALIGN)),
ResolvedType::Closure { .. } => Err(LayoutError::NotYetSupported {
kind: "Closure".to_owned(),
}),
ResolvedType::TypeParam(name) => Err(LayoutError::NotYetSupported {
kind: format!("TypeParam({name})"),
}),
ResolvedType::External { name, .. } => Err(LayoutError::NotYetSupported {
kind: format!(
"External({name}) — should have been inlined by upstream MonomorphisePass; reaching the backend means an upstream invariant violation"
),
}),
ResolvedType::Error => Err(LayoutError::NotYetSupported {
kind: "Error".to_owned(),
}),
}
}
pub const VTABLE_SLOT_SIZE: u32 = 4;
pub const VTABLE_SLOT_ALIGN: u32 = 4;
#[expect(
clippy::exhaustive_structs,
reason = "plain layout record consumed externally; intentionally constructible"
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VTableLayout {
pub size: u32,
pub align: u32,
pub method_count: u32,
pub slot_size: u32,
}
pub fn plan_vtable(trait_def: &IrTrait, _module: &IrModule) -> Result<VTableLayout, LayoutError> {
let method_count =
u32::try_from(trait_def.methods.len()).map_err(|_| LayoutError::SizeOverflow {
name: format!("vtable<{}>", trait_def.name),
})?;
let size =
method_count
.checked_mul(VTABLE_SLOT_SIZE)
.ok_or_else(|| LayoutError::SizeOverflow {
name: format!("vtable<{}>", trait_def.name),
})?;
Ok(VTableLayout {
size,
align: VTABLE_SLOT_ALIGN,
method_count,
slot_size: VTABLE_SLOT_SIZE,
})
}