use std::fmt::Write as _;
use std::path::Path;
use std::process::ExitCode;
use rucc_base::Interner;
use rucc_target::{TargetInfo, Triple};
use rucc_tuple::{TARGETS, TargetTuple};
use rucc_types::{
ArrayLen, FieldDecl, FloatKind, IntKind, RecordKind, RecordOptions, TypeId, Types, declare,
layout_record,
};
const DIR: &str = "tests/abi-corpus";
const SEED: u64 = 0x5243_4300_4d36_2e35;
const GENERATED: usize = 48;
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Mode {
Write,
Check,
}
pub(crate) fn one(target: TargetTuple) -> ExitCode {
let Some(triple) = Triple::from_tuple(target) else {
eprintln!("error: {} has no TargetInfo, so no record can be laid out for it", target);
eprintln!(" `rucc-abi` describes its scalars and the layout engine takes a three field");
eprintln!(" triple, which is the gap `abi-corpus` reports rather than approximates");
return ExitCode::FAILURE;
};
print!("{}", render(target, &TargetInfo::new(triple)));
ExitCode::SUCCESS
}
pub(crate) fn run(root: &Path, mode: Mode) -> ExitCode {
let dir = root.join(DIR);
let mut written = 0;
let mut stale = Vec::new();
let mut skipped = Vec::new();
for entry in TARGETS {
let Ok(target) = entry.tuple.parse::<TargetTuple>() else {
eprintln!("error: the target table holds `{}`, which does not parse", entry.tuple);
return ExitCode::FAILURE;
};
let Some(triple) = Triple::from_tuple(target) else {
skipped.push(entry.tuple);
continue;
};
let path = dir.join(format!("{}.c", target.to_canonical_string()));
let wanted = render(target, &TargetInfo::new(triple));
if mode == Mode::Check {
if std::fs::read_to_string(&path).unwrap_or_default() != wanted {
stale.push(entry.tuple);
}
written += 1;
continue;
}
if let Err(error) = std::fs::create_dir_all(&dir) {
eprintln!("error: {error}");
return ExitCode::FAILURE;
}
if let Err(error) = std::fs::write(&path, wanted) {
eprintln!("error: {}: {error}", path.display());
return ExitCode::FAILURE;
}
written += 1;
}
if !skipped.is_empty() {
eprintln!(
"abi-corpus: {} of {} rows have no TargetInfo and were skipped",
skipped.len(),
TARGETS.len()
);
eprintln!(" {}", skipped.join(" "));
}
if mode == Mode::Check {
if stale.is_empty() {
println!("abi-corpus: {written} files are up to date");
return ExitCode::SUCCESS;
}
println!("abi-corpus: {} files are out of date, run `cargo xtask abi-corpus`", stale.len());
for tuple in stale {
println!(" {tuple}");
}
return ExitCode::FAILURE;
}
println!("abi-corpus: wrote {written} files to {DIR}");
ExitCode::SUCCESS
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Leaf {
Int(IntKind),
Float(FloatKind),
Pointer,
}
const LEAVES: &[Leaf] = &[
Leaf::Int(IntKind::Char),
Leaf::Int(IntKind::SChar),
Leaf::Int(IntKind::UChar),
Leaf::Int(IntKind::Short),
Leaf::Int(IntKind::UShort),
Leaf::Int(IntKind::Int),
Leaf::Int(IntKind::UInt),
Leaf::Int(IntKind::Long),
Leaf::Int(IntKind::ULong),
Leaf::Int(IntKind::LongLong),
Leaf::Int(IntKind::Int128),
Leaf::Float(FloatKind::Float),
Leaf::Float(FloatKind::Double),
Leaf::Float(FloatKind::LongDouble),
Leaf::Pointer,
];
const BIT_BASES: &[IntKind] =
&[IntKind::Char, IntKind::UChar, IntKind::Short, IntKind::UShort, IntKind::Int, IntKind::UInt];
#[derive(Clone, Copy)]
struct Member {
ty: MemberType,
elements: u64,
bits: Option<u32>,
align: Option<u64>,
}
#[derive(Clone, Copy)]
enum MemberType {
Leaf(Leaf),
Record(usize),
}
struct Shape {
name: String,
why: &'static str,
kind: RecordKind,
options: RecordOptions,
members: Vec<Member>,
flexible: bool,
}
impl Member {
fn leaf(leaf: Leaf) -> Member {
Member { ty: MemberType::Leaf(leaf), elements: 1, bits: None, align: None }
}
fn bit_field(base: IntKind, bits: u32) -> Member {
Member { ty: MemberType::Leaf(Leaf::Int(base)), elements: 1, bits: Some(bits), align: None }
}
}
fn shapes() -> Vec<Shape> {
let mut shapes = Vec::new();
shapes.push(Shape {
name: "nest_small".to_string(),
why: "A small struct with an alignment of its own, nested by the shapes below.",
kind: RecordKind::Struct,
options: RecordOptions::default(),
members: vec![
Member::leaf(Leaf::Int(IntKind::Int)),
Member::leaf(Leaf::Int(IntKind::Char)),
],
flexible: false,
});
shapes.push(Shape {
name: "nest_union".to_string(),
why: "A union nested by the shapes below, because a union member's alignment is the \
largest of its members and not the first one's.",
kind: RecordKind::Union,
options: RecordOptions::default(),
members: vec![
Member::leaf(Leaf::Int(IntKind::Char)),
Member::leaf(Leaf::Float(FloatKind::Double)),
],
flexible: false,
});
shapes.push(Shape {
name: "empty".to_string(),
why: "The empty struct, which C does not have and both references do. It is a GNU \
extension with a size of zero, and C++ gives it a size of one, so this is the one \
case where being right means disagreeing with the other language.",
kind: RecordKind::Struct,
options: RecordOptions::default(),
members: Vec::new(),
flexible: false,
});
for base in BIT_BASES {
let capacity = bit_capacity(*base);
let mut members = Vec::new();
for width in 1..=capacity {
members.push(Member::bit_field(*base, width));
}
shapes.push(Shape {
name: format!("bits_ladder_{}", base_name(*base)),
why: "One bit-field of every width the type has, in order. The size of the whole is \
the sum rounded up, and a target that allocates into the wrong unit gets a \
different answer at the first width that does not fit.",
kind: RecordKind::Struct,
options: RecordOptions::default(),
members,
flexible: false,
});
}
for base in BIT_BASES {
let capacity = bit_capacity(*base);
shapes.push(Shape {
name: format!("bits_straddle_{}", base_name(*base)),
why: "A width that fills all but one bit of a unit, followed by one that cannot fit \
in the bit that is left. Whether the second one starts a new unit or is split \
across the boundary is the difference this measures.",
kind: RecordKind::Struct,
options: RecordOptions::default(),
members: vec![
Member::bit_field(*base, capacity - 1),
Member::bit_field(*base, capacity),
Member::leaf(Leaf::Int(IntKind::Char)),
],
flexible: false,
});
}
shapes.push(Shape {
name: "bits_zero_width".to_string(),
why: "The zero width bit-field. It holds nothing and occupies no bits, and the member \
after it starts at the next boundary of its type, so the offset of the char at the \
end is the whole of what this asserts.",
kind: RecordKind::Struct,
options: RecordOptions::default(),
members: vec![
Member::bit_field(IntKind::UInt, 3),
Member::bit_field(IntKind::UInt, 0),
Member::bit_field(IntKind::UInt, 5),
Member::leaf(Leaf::Int(IntKind::Char)),
],
flexible: false,
});
shapes.push(Shape {
name: "bits_zero_width_only".to_string(),
why: "A struct whose only member is a zero width bit-field. It has a size of zero on both \
references and an alignment that is the base type's, which is the one place a zero \
width member changes something other than an offset.",
kind: RecordKind::Struct,
options: RecordOptions::default(),
members: vec![Member::bit_field(IntKind::UInt, 0)],
flexible: false,
});
shapes.push(Shape {
name: "bits_then_member".to_string(),
why: "A bit-field that does not fill its unit followed by an ordinary member. The \
ordinary one starts at its own alignment and the bits before it are padding.",
kind: RecordKind::Struct,
options: RecordOptions::default(),
members: vec![
Member::bit_field(IntKind::UInt, 3),
Member::leaf(Leaf::Int(IntKind::Int)),
Member::bit_field(IntKind::UInt, 3),
Member::leaf(Leaf::Int(IntKind::Char)),
],
flexible: false,
});
for align in [1u64, 2, 4, 8, 16, 32] {
shapes.push(Shape {
name: format!("alignas_{align}"),
why: "`_Alignas` on a char member. It raises the member's alignment and the record's \
with it, so the one byte case is a char that stays where it was and the thirty \
two byte case moves everything after it.",
kind: RecordKind::Struct,
options: RecordOptions::default(),
members: vec![
Member::leaf(Leaf::Int(IntKind::Char)),
Member {
ty: MemberType::Leaf(Leaf::Int(IntKind::Char)),
elements: 1,
bits: None,
align: Some(align),
},
Member::leaf(Leaf::Int(IntKind::Char)),
],
flexible: false,
});
}
for (name, leaf) in [
("alignas_over_int", Leaf::Int(IntKind::Int)),
("alignas_over_long_double", Leaf::Float(FloatKind::LongDouble)),
] {
shapes.push(Shape {
name: name.to_string(),
why: "`_Alignas(32)` on a member that already has an alignment of its own. Thirty two \
is above every scalar alignment on every target, so this is the same request \
everywhere and the answer still differs where the member's size does.",
kind: RecordKind::Struct,
options: RecordOptions::default(),
members: vec![
Member::leaf(Leaf::Int(IntKind::Char)),
Member { ty: MemberType::Leaf(leaf), elements: 1, bits: None, align: Some(32) },
Member::leaf(Leaf::Int(IntKind::Char)),
],
flexible: false,
});
}
shapes.push(Shape {
name: "long_double_pair".to_string(),
why: "A char in front of a `long double`, which is the shortest program that tells the \
four `long double` answers apart.",
kind: RecordKind::Struct,
options: RecordOptions::default(),
members: vec![
Member::leaf(Leaf::Int(IntKind::Char)),
Member::leaf(Leaf::Float(FloatKind::LongDouble)),
],
flexible: false,
});
shapes.push(Shape {
name: "int128_pair".to_string(),
why: "A char in front of an `__int128`, which is sixteen bytes aligned to sixteen on \
every target here. s390x caps it at eight and s390x is one of the rows this corpus \
cannot reach yet.",
kind: RecordKind::Struct,
options: RecordOptions::default(),
members: vec![
Member::leaf(Leaf::Int(IntKind::Char)),
Member::leaf(Leaf::Int(IntKind::Int128)),
],
flexible: false,
});
shapes.push(Shape {
name: "flexible".to_string(),
why: "A flexible array member. It sits where it would have sat and adds nothing to the \
size, and the tail padding in front of it is what makes the idiom allocate enough.",
kind: RecordKind::Struct,
options: RecordOptions::default(),
members: vec![
Member::leaf(Leaf::Int(IntKind::Int)),
Member::leaf(Leaf::Int(IntKind::Char)),
Member {
ty: MemberType::Leaf(Leaf::Int(IntKind::Int)),
elements: 0,
bits: None,
align: None,
},
],
flexible: true,
});
shapes.push(Shape {
name: "union_of_the_widest".to_string(),
why: "A union of the three widest scalars. Its size is the largest member rounded up to \
the alignment, and both of those move between targets.",
kind: RecordKind::Union,
options: RecordOptions::default(),
members: vec![
Member::leaf(Leaf::Int(IntKind::Int128)),
Member::leaf(Leaf::Float(FloatKind::LongDouble)),
Member::leaf(Leaf::Pointer),
],
flexible: false,
});
let nestable = shapes.len();
let mut rng = Rng::new(SEED);
for index in 0..GENERATED {
let kind = if rng.below(4) == 0 { RecordKind::Union } else { RecordKind::Struct };
let count = 1 + rng.below(6) as usize;
let mut members = Vec::with_capacity(count);
for _ in 0..count {
members.push(random_member(&mut rng, nestable));
}
if kind == RecordKind::Union {
for member in &mut members {
member.bits = None;
}
}
shapes.push(Shape {
name: format!("gen_{index:02}"),
why: "",
kind,
options: RecordOptions::default(),
members,
flexible: false,
});
}
shapes
}
fn random_member(rng: &mut Rng, nestable: usize) -> Member {
let ty = if rng.below(7) == 0 {
MemberType::Record(rng.below(nestable as u64) as usize)
} else {
MemberType::Leaf(LEAVES[rng.below(LEAVES.len() as u64) as usize])
};
let elements = if rng.below(5) == 0 { 2 + rng.below(2) } else { 1 };
let mut member = Member { ty, elements, bits: None, align: None };
if elements == 1 && rng.below(4) == 0 {
if let MemberType::Leaf(Leaf::Int(base)) = ty {
if BIT_BASES.contains(&base) {
member.bits = Some(1 + rng.below(u64::from(bit_capacity(base))) as u32);
return member;
}
}
}
if rng.below(9) == 0 {
member.align = Some(if rng.below(2) == 0 { 16 } else { 32 });
}
member
}
fn bit_capacity(base: IntKind) -> u32 {
match base {
IntKind::Char | IntKind::SChar | IntKind::UChar => 8,
IntKind::Short | IntKind::UShort => 16,
_ => 32,
}
}
fn base_name(base: IntKind) -> &'static str {
match base {
IntKind::Char => "char",
IntKind::SChar => "schar",
IntKind::UChar => "uchar",
IntKind::Short => "short",
IntKind::UShort => "ushort",
IntKind::UInt => "uint",
_ => "int",
}
}
fn render(target: TargetTuple, info: &TargetInfo) -> String {
let shapes = shapes();
let mut types = Types::new();
let mut names = Interner::new();
let mut built: Vec<TypeId> = Vec::with_capacity(shapes.len());
let mut out = String::new();
header(&mut out, target);
for shape in &shapes {
let tag = names.intern(&shape.name);
let record = types.declare_record(shape.kind, Some(tag));
let id = types.record(record);
let mut decls = Vec::with_capacity(shape.members.len());
for (index, member) in shape.members.iter().enumerate() {
let last = index + 1 == shape.members.len();
let base = match member.ty {
MemberType::Leaf(Leaf::Int(kind)) => types.int(kind),
MemberType::Leaf(Leaf::Float(kind)) => types.float(kind),
MemberType::Leaf(Leaf::Pointer) => {
let void = types.void();
types.pointer(void)
}
MemberType::Record(at) => built[at],
};
let ty = if shape.flexible && last {
types.array(base, ArrayLen::Unknown)
} else if member.elements == 1 {
base
} else {
types.array(base, ArrayLen::Fixed(member.elements))
};
let name = match member.bits {
Some(0) => None,
_ => Some(names.intern(&format!("m{index}"))),
};
decls.push(FieldDecl {
name,
ty,
bits: member.bits,
align: member.align,
packed: false,
});
}
let laid_out = match layout_record(&types, shape.kind, &decls, &shape.options, info) {
Ok(laid_out) => laid_out,
Err(error) => {
panic!(
"{}: {} does not lay out: {error}",
target.to_canonical_string(),
shape.name
);
}
};
declaration(&mut out, &types, &names, shape, &decls, laid_out.fields.as_slice());
assertions(&mut out, &names, shape, &decls, &laid_out);
types.complete_record(record, laid_out);
built.push(id);
}
out
}
fn header(out: &mut String, target: TargetTuple) {
let canonical = target.to_canonical_string();
let _ = writeln!(out, "/* Record layout for {canonical}, as rucc computes it.");
for line in [
"",
"Generated by `cargo run -q -p rucc-targets -- abi-corpus --write`. Do not edit this",
"file, edit the grammar in `build-tools/rucc-targets/src/corpus.rs`.",
"",
"Every number below comes from `rucc_types::layout_record`, which is the function the",
"compiler calls when it parses a struct. So this file failing to compile under gcc or",
"clang for this target is a disagreement about layout between rucc and the reference,",
"and the diagnostic names the record and the member it is about.",
"",
"It compiles with `-c -std=c17 -Wall -Wextra -Werror` and no libc, which is what lets",
"the freestanding rows be checked the same way the hosted ones are.",
"",
"The shapes are the same in every target's file and only the numbers differ, so a diff",
"between two of these is the list of layout decisions the two targets make differently.",
] {
if line.is_empty() {
out.push_str(" *\n");
} else {
let _ = writeln!(out, " * {line}");
}
}
out.push_str(" */\n\n");
}
fn declaration(
out: &mut String,
types: &Types,
names: &Interner,
shape: &Shape,
decls: &[FieldDecl],
fields: &[rucc_types::Field],
) {
if !shape.why.is_empty() {
out.push_str("/* ");
let mut column = 3;
for word in shape.why.split_whitespace() {
if column + word.len() > 96 {
out.push_str("\n * ");
column = 3;
} else if column > 3 {
out.push(' ');
column += 1;
}
out.push_str(word);
column += word.len();
}
out.push_str(" */\n");
}
let keyword = match shape.kind {
RecordKind::Struct => "struct",
RecordKind::Union => "union",
};
let _ = writeln!(out, "{keyword} {} {{", shape.name);
for (decl, field) in decls.iter().zip(fields) {
out.push('\t');
if let Some(align) = decl.align {
let _ = write!(out, "_Alignas({align}) ");
}
match (decl.name, decl.bits) {
(None, Some(bits)) => {
let _ = write!(out, "{} : {bits}", rucc_types::spell(types, names, decl.ty));
}
(Some(name), Some(bits)) => {
let _ = write!(out, "{} : {bits}", declare(types, names, decl.ty, name));
}
(Some(name), None) => out.push_str(&declare(types, names, decl.ty, name)),
(None, None) => unreachable!("only a zero width bit-field is unnamed here"),
}
let _ = writeln!(out, ";{}", offset_note(field));
}
out.push_str("};\n");
}
fn offset_note(field: &rucc_types::Field) -> String {
match field.bits {
Some(_) => format!("\t/* bit {} */", field.bit_offset()),
None => format!("\t/* +{} */", field.offset),
}
}
fn assertions(
out: &mut String,
names: &Interner,
shape: &Shape,
decls: &[FieldDecl],
laid_out: &rucc_types::RecordLayout,
) {
let keyword = match shape.kind {
RecordKind::Struct => "struct",
RecordKind::Union => "union",
};
let name = &shape.name;
let _ = writeln!(
out,
"_Static_assert(sizeof({keyword} {name}) == {}, \"sizeof {keyword} {name}\");",
laid_out.layout.size
);
let _ = writeln!(
out,
"_Static_assert(_Alignof({keyword} {name}) == {}, \"_Alignof {keyword} {name}\");",
laid_out.layout.align
);
for (decl, field) in decls.iter().zip(&laid_out.fields) {
if field.bits.is_some() {
continue;
}
let Some(symbol) = decl.name else { continue };
let member = names.resolve(symbol);
let _ = writeln!(
out,
"_Static_assert(__builtin_offsetof({keyword} {name}, {member}) == {}, \"offsetof {keyword} {name}.{member}\");",
field.offset
);
}
out.push('\n');
}
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Rng {
Rng(seed)
}
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
z ^ (z >> 31)
}
fn below(&mut self, limit: u64) -> u64 {
self.next() % limit
}
}