use rucc_target::TargetInfo;
use crate::kind::{ArrayLen, RecordId, RecordKind, TypeKind};
use crate::layout::layout;
use crate::record::Field;
use crate::types::{TypeId, Types};
pub const GRANULE: u64 = 8;
const SIZES: &[u64] = &[1, 4, 8, 16, 32, 64];
const LIMIT: u64 = 1 << 20;
const LAYOUTS: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Keying {
Exact,
PointersTogether,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Cell {
Pad,
One(TypeKind),
Mixed,
}
impl Cell {
fn with(self, kind: TypeKind) -> Cell {
match self {
Cell::Pad => Cell::One(kind),
Cell::One(had) if had == kind => self,
_ => Cell::Mixed,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Tally {
pub records: u64,
pub skipped: u64,
pub bytes: u64,
pub padding: u64,
pub uniform: u64,
pub mixed: u64,
pub blank: u64,
}
impl Tally {
#[must_use]
pub fn granules(&self) -> u64 {
self.uniform + self.mixed + self.blank
}
#[must_use]
pub fn disagreeing(&self) -> f64 {
let all = self.granules();
if all == 0 { 0.0 } else { self.mixed as f64 / all as f64 }
}
#[must_use]
pub fn ratio(&self, granule: u64) -> f64 {
4.0 / granule as f64 + 4.0 * self.disagreeing()
}
pub fn absorb(&mut self, other: Tally) {
self.records += other.records;
self.skipped += other.skipped;
self.bytes += other.bytes;
self.padding += other.padding;
self.uniform += other.uniform;
self.mixed += other.mixed;
self.blank += other.blank;
}
}
#[must_use]
pub fn measure(
types: &Types,
id: RecordId,
target: &TargetInfo,
keying: Keying,
granule: u64,
) -> Option<Tally> {
let info = types.record_info(id);
let size = info.layout?.size;
if size > LIMIT {
return Some(Tally { skipped: 1, ..Tally::default() });
}
let width = usize::try_from(granule).ok().filter(|width| *width > 0)?;
let mut layouts = vec![vec![Cell::Pad; usize::try_from(size).ok()?]];
paint_record(types, id, 0, target, keying, &mut layouts);
let mut tally = Tally { records: 1, bytes: size, ..Tally::default() };
let count = layouts[0].len().div_ceil(width);
for index in 0..count {
let from = index * width;
let to = (from + width).min(layouts[0].len());
let mut disagrees = false;
let mut typed = false;
for layout in &layouts {
let (mixed, seen) = verdict(&layout[from..to]);
disagrees |= mixed;
typed |= seen;
}
match (disagrees, typed) {
(true, _) => tally.mixed += 1,
(false, true) => tally.uniform += 1,
(false, false) => tally.blank += 1,
}
tally.padding += (from..to)
.filter(|byte| layouts.iter().all(|layout| layout[*byte] == Cell::Pad))
.count() as u64;
}
Some(tally)
}
fn verdict(granule: &[Cell]) -> (bool, bool) {
let mut seen: Option<TypeKind> = None;
for cell in granule {
match *cell {
Cell::Pad => {}
Cell::Mixed => return (true, true),
Cell::One(kind) => match seen {
None => seen = Some(kind),
Some(had) if had == kind => {}
Some(_) => return (true, true),
},
}
}
(false, seen.is_some())
}
#[must_use]
pub fn measure_all(types: &Types, target: &TargetInfo, keying: Keying, granule: u64) -> Tally {
let mut tally = Tally::default();
for (id, _) in types.records() {
if let Some(one) = measure(types, id, target, keying, granule) {
tally.absorb(one);
}
}
tally
}
#[must_use]
pub fn report(types: &Types, names: &rucc_base::Interner, target: &TargetInfo) -> String {
use std::fmt::Write as _;
let mut out = String::new();
writeln!(out, "per record, at a granule of {GRANULE} bytes\n").expect("a string takes writes");
writeln!(out, "{:>8} {:>8} {:>8} {:>8} record", "bytes", "uniform", "mixed", "blank")
.expect("a string takes writes");
for (id, info) in types.records() {
let Some(tally) = measure(types, id, target, Keying::Exact, GRANULE) else {
continue;
};
let kind = match info.kind {
RecordKind::Struct => "struct",
RecordKind::Union => "union",
};
let tag = match info.tag {
Some(tag) => names.resolve(tag).to_string(),
None => format!("<anonymous {}>", id.0),
};
writeln!(
out,
"{:>8} {:>8} {:>8} {:>8} {kind} {tag}",
tally.bytes, tally.uniform, tally.mixed, tally.blank
)
.expect("a string takes writes");
}
for keying in [Keying::Exact, Keying::PointersTogether] {
let label = match keying {
Keying::Exact => "every type distinct",
Keying::PointersTogether => "every pointer one type",
};
writeln!(out, "\n{label}").expect("a string takes writes");
writeln!(
out,
"{:>8} {:>8} {:>9} {:>9} {:>9} {:>9}",
"granule", "records", "bytes", "granules", "disagree", "plane"
)
.expect("a string takes writes");
for &size in SIZES {
let tally = measure_all(types, target, keying, size);
writeln!(
out,
"{:>8} {:>8} {:>9} {:>9} {:>9.4} {:>9.4}",
size,
tally.records,
tally.bytes,
tally.granules(),
tally.disagreeing(),
tally.ratio(size)
)
.expect("a string takes writes");
}
}
let whole = measure_all(types, target, Keying::Exact, GRANULE);
writeln!(out, "\npadding {} of {} bytes", whole.padding, whole.bytes)
.expect("a string takes writes");
writeln!(out, "skipped {} records too large to measure", whole.skipped)
.expect("a string takes writes");
writeln!(out, "budget 1.25 bytes of plane per byte of program, at Tier D")
.expect("a string takes writes");
out
}
fn paint(
types: &Types,
ty: TypeId,
base: u64,
target: &TargetInfo,
keying: Keying,
layouts: &mut Vec<Vec<Cell>>,
) {
let canonical = types.canonical(ty);
match types.kind(canonical) {
TypeKind::Record(id) => paint_record(types, id, base, target, keying, layouts),
TypeKind::Array { elem, len } => {
let ArrayLen::Fixed(count) = len else {
return;
};
let Ok(each) = layout(types, elem, target) else {
return;
};
for index in 0..count {
let Some(at) = each.size.checked_mul(index).and_then(|off| base.checked_add(off))
else {
return;
};
paint(types, elem, at, target, keying, layouts);
}
}
TypeKind::Atomic(inner) => paint(types, inner, base, target, keying, layouts),
_ => {
let Ok(whole) = layout(types, canonical, target) else {
return;
};
fill(types, canonical, base, whole.size, keying, layouts);
}
}
}
fn paint_record(
types: &Types,
id: RecordId,
base: u64,
target: &TargetInfo,
keying: Keying,
layouts: &mut Vec<Vec<Cell>>,
) {
let info = types.record_info(id);
let grown = match info.kind {
RecordKind::Union if info.fields.len() > 1 => {
layouts.len().checked_mul(info.fields.len()).filter(|grown| *grown <= LAYOUTS)
}
_ => None,
};
if let Some(grown) = grown {
let start = layouts.clone();
let mut out = Vec::with_capacity(grown);
for field in &info.fields {
let mut copy = start.clone();
place(types, field, base, target, keying, &mut copy);
out.append(&mut copy);
}
*layouts = out;
return;
}
for field in &info.fields {
let at = match info.kind {
RecordKind::Struct => base + field.offset,
RecordKind::Union => base,
};
place(types, field, at, target, keying, layouts);
}
}
fn place(
types: &Types,
field: &Field,
at: u64,
target: &TargetInfo,
keying: Keying,
layouts: &mut Vec<Vec<Cell>>,
) {
match field.bits {
Some(0) => {}
Some(width) => {
let bytes = u64::from(field.bit + width).div_ceil(8);
fill(types, field.ty, at, bytes, keying, layouts);
}
None => paint(types, field.ty, at, target, keying, layouts),
}
}
fn fill(
types: &Types,
ty: TypeId,
base: u64,
count: u64,
keying: Keying,
layouts: &mut [Vec<Cell>],
) {
let kind = key(types, ty, keying);
let Ok(from) = usize::try_from(base) else {
return;
};
for cells in layouts.iter_mut() {
let to = usize::try_from(base.saturating_add(count)).unwrap_or(usize::MAX).min(cells.len());
if from >= to {
continue;
}
for cell in &mut cells[from..to] {
*cell = cell.with(kind);
}
}
}
fn key(types: &Types, ty: TypeId, keying: Keying) -> TypeKind {
let kind = types.kind(types.canonical(ty));
match kind {
TypeKind::Enum(id) => match types.enum_info(id).underlying {
Some(underlying) => types.kind(types.canonical(underlying)),
None => kind,
},
TypeKind::Pointer(_) if keying == Keying::PointersTogether => {
TypeKind::Pointer(types.void())
}
_ => kind,
}
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_target::Triple;
use super::*;
use crate::kind::IntKind;
use crate::layout_record;
use crate::record::{FieldDecl, RecordOptions};
fn target() -> TargetInfo {
TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().expect("a triple"))
}
fn built(types: &mut Types, names: &mut Interner, members: &[(&str, TypeId)]) -> RecordId {
let fields: Vec<FieldDecl> = members
.iter()
.map(|(name, ty)| FieldDecl::new(Some(names.intern(name)), *ty))
.collect();
let id = types.declare_record(RecordKind::Struct, None);
let laid_out =
layout_record(types, RecordKind::Struct, &fields, &RecordOptions::default(), &target())
.expect("a record with a layout");
types.complete_record(id, laid_out);
id
}
#[test]
fn a_granule_of_one_type_agrees_with_itself() {
let mut types = Types::new();
let mut names = Interner::new();
let long = types.int(IntKind::Long);
let id = built(&mut types, &mut names, &[("a", long), ("b", long)]);
let tally = measure(&types, id, &target(), Keying::Exact, 16).expect("a complete record");
assert_eq!(tally.bytes, 16);
assert_eq!(tally.uniform, 1);
assert_eq!(tally.mixed, 0);
assert_eq!(tally.padding, 0);
}
#[test]
fn two_types_in_one_granule_do_not() {
let mut types = Types::new();
let mut names = Interner::new();
let long = types.int(IntKind::Long);
let double = types.float(crate::kind::FloatKind::Double);
let id = built(&mut types, &mut names, &[("a", long), ("b", double)]);
let tally = measure(&types, id, &target(), Keying::Exact, 16).expect("a complete record");
assert_eq!(tally.bytes, 16);
assert_eq!(tally.mixed, 1);
assert_eq!(tally.uniform, 0);
}
#[test]
fn padding_has_no_type_and_costs_nothing() {
let mut types = Types::new();
let mut names = Interner::new();
let ch = types.int(IntKind::Char);
let id = built(&mut types, &mut names, &[("a", ch)]);
let tally = measure(&types, id, &target(), Keying::Exact, 16).expect("a complete record");
assert_eq!(tally.bytes, 1);
assert_eq!(tally.padding, 0);
assert_eq!(tally.uniform, 1);
}
#[test]
fn padding_between_members_is_counted_and_does_not_make_a_granule_disagree() {
let mut types = Types::new();
let mut names = Interner::new();
let ch = types.int(IntKind::Char);
let long = types.int(IntKind::Long);
let inner = built(&mut types, &mut names, &[("c", ch)]);
let inner = types.record(inner);
let id = built(&mut types, &mut names, &[("a", ch), ("b", long), ("c", inner)]);
let tally = measure(&types, id, &target(), Keying::Exact, 16).expect("a complete record");
assert_eq!(tally.bytes, 24);
assert_eq!(tally.padding, 7 + 7);
assert_eq!(tally.mixed, 1);
assert_eq!(tally.uniform, 1);
}
#[test]
fn an_array_paints_every_element_and_stays_one_type() {
let mut types = Types::new();
let mut names = Interner::new();
let int = types.int(IntKind::Int);
let array = types.array(int, ArrayLen::Fixed(16));
let id = built(&mut types, &mut names, &[("a", array)]);
let tally = measure(&types, id, &target(), Keying::Exact, 16).expect("a complete record");
assert_eq!(tally.bytes, 64);
assert_eq!(tally.uniform, 4);
assert_eq!(tally.mixed, 0);
}
#[test]
fn a_union_is_a_choice_and_not_a_coexistence() {
let mut types = Types::new();
let mut names = Interner::new();
let long = types.int(IntKind::Long);
let double = types.float(crate::kind::FloatKind::Double);
let fields = [FieldDecl::new(Some(names.intern("i")), long), FieldDecl::new(None, double)];
let id = types.declare_record(RecordKind::Union, None);
let laid_out =
layout_record(&types, RecordKind::Union, &fields, &RecordOptions::default(), &target())
.expect("a union with a layout");
types.complete_record(id, laid_out);
let tally = measure(&types, id, &target(), Keying::Exact, 16).expect("a complete record");
assert_eq!(tally.bytes, 8);
assert_eq!(tally.mixed, 0);
assert_eq!(tally.uniform, 1);
}
#[test]
fn a_union_sharing_a_granule_with_a_member_of_another_type_does_disagree() {
let mut types = Types::new();
let mut names = Interner::new();
let long = types.int(IntKind::Long);
let double = types.float(crate::kind::FloatKind::Double);
let members = [FieldDecl::new(Some(names.intern("i")), long), FieldDecl::new(None, double)];
let inner = types.declare_record(RecordKind::Union, None);
let laid_out = layout_record(
&types,
RecordKind::Union,
&members,
&RecordOptions::default(),
&target(),
)
.expect("a union with a layout");
types.complete_record(inner, laid_out);
let inner = types.record(inner);
let int = types.int(IntKind::Int);
let id = built(&mut types, &mut names, &[("u", inner), ("n", int)]);
let tally = measure(&types, id, &target(), Keying::Exact, 16).expect("a complete record");
assert_eq!(tally.bytes, 16);
assert_eq!(tally.mixed, 1);
}
#[test]
fn two_pointers_to_different_things_agree_only_under_the_looser_keying() {
let mut types = Types::new();
let mut names = Interner::new();
let ch = types.int(IntKind::Char);
let int = types.int(IntKind::Int);
let to_char = types.pointer(ch);
let to_int = types.pointer(int);
let id = built(&mut types, &mut names, &[("a", to_char), ("b", to_int)]);
let exact = measure(&types, id, &target(), Keying::Exact, 16).expect("a complete record");
assert_eq!(exact.mixed, 1);
let loose = measure(&types, id, &target(), Keying::PointersTogether, 16)
.expect("a complete record");
assert_eq!(loose.mixed, 0);
assert_eq!(loose.uniform, 1);
}
#[test]
fn an_enumeration_agrees_with_the_integer_it_is_represented_in() {
let mut types = Types::new();
let mut names = Interner::new();
let int = types.int(IntKind::Int);
let enumeration = types.declare_enum(None);
types.complete_enum(enumeration, int, false);
let enumeration = types.enumeration(enumeration);
let id = built(&mut types, &mut names, &[("a", int), ("b", enumeration)]);
let tally = measure(&types, id, &target(), Keying::Exact, 16).expect("a complete record");
assert_eq!(tally.mixed, 0);
assert_eq!(tally.uniform, 1);
}
#[test]
fn a_flexible_array_member_paints_nothing_because_it_occupies_nothing() {
let mut types = Types::new();
let mut names = Interner::new();
let long = types.int(IntKind::Long);
let ch = types.int(IntKind::Char);
let flexible = types.array(ch, ArrayLen::Unknown);
let id = built(&mut types, &mut names, &[("a", long), ("rest", flexible)]);
let tally = measure(&types, id, &target(), Keying::Exact, 16).expect("a complete record");
assert_eq!(tally.bytes, 8);
assert_eq!(tally.uniform, 1);
assert_eq!(tally.mixed, 0);
}
#[test]
fn the_ratio_is_a_quarter_when_nothing_disagrees_and_four_when_everything_does() {
let none = Tally { uniform: 4, ..Tally::default() };
assert!((none.ratio(16) - 0.25).abs() < 1e-9);
let all = Tally { mixed: 4, ..Tally::default() };
assert!((all.ratio(16) - 4.25).abs() < 1e-9);
let budget = Tally { uniform: 3, mixed: 1, ..Tally::default() };
assert!((budget.ratio(16) - 1.25).abs() < 1e-9);
let budget = Tally { uniform: 13, mixed: 3, ..Tally::default() };
assert!((budget.ratio(8) - 1.25).abs() < 1e-9);
}
#[test]
fn the_default_granule_is_eight_because_a_pointer_and_two_ints_fit_in_sixteen() {
let mut types = Types::new();
let mut names = Interner::new();
let ch = types.int(IntKind::Char);
let int = types.int(IntKind::Int);
let to_char = types.pointer(ch);
let id = built(&mut types, &mut names, &[("p", to_char), ("a", int), ("b", int)]);
let wide = measure(&types, id, &target(), Keying::Exact, 16).expect("a complete record");
assert_eq!(wide.mixed, 1);
assert_eq!(wide.uniform, 0);
assert_eq!(GRANULE, 8);
let tally =
measure(&types, id, &target(), Keying::Exact, GRANULE).expect("a complete record");
assert_eq!(tally.mixed, 0);
assert_eq!(tally.uniform, 2);
}
}