use std::fmt;
use std::hash::Hash;
use siphasher::sip128::{Hasher128, SipHasher13};
use crate::types::{Type, TypeId, TypeShape, TypeTable};
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum AggregateKind {
Struct,
Union,
Enum,
}
impl AggregateKind {
#[inline]
#[must_use]
pub const fn keyword(self) -> &'static str {
match self {
AggregateKind::Struct => "struct",
AggregateKind::Union => "union",
AggregateKind::Enum => "enum",
}
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct CanonicalMember {
pub name: String,
pub bit_offset: Option<u64>,
pub bitfield_width: Option<u32>,
pub ty: CanonicalType,
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub enum CanonicalType {
Void,
Bool,
Int {
bytes: Option<u8>,
signed: bool,
},
Float {
bytes: Option<u8>,
},
Ptr {
pointee: Box<CanonicalType>,
width: Option<u8>,
},
Array {
elem: Box<CanonicalType>,
len: u64,
},
Named {
tag: String,
kind: AggregateKind,
},
Aggregate {
tag: Option<String>,
kind: AggregateKind,
members: Vec<CanonicalMember>,
size: Option<u64>,
},
Enum {
tag: Option<String>,
underlying: Box<CanonicalType>,
members: Vec<(String, u64)>,
size: Option<u64>,
},
Function {
ret: Box<CanonicalType>,
params: Vec<CanonicalType>,
varargs: bool,
},
Typedef {
name: String,
underlying: Box<CanonicalType>,
},
Opaque(String),
BackRef(usize),
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct TypeKey(pub u128);
impl fmt::Display for TypeKey {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:032x}", self.0)
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub enum TypeIdentity {
Tagged {
tag: String,
kind: AggregateKind,
},
Alias {
name: String,
},
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct CanonicalOptions {
pub include_sizes: bool,
}
impl CanonicalOptions {
#[inline]
#[must_use]
pub const fn strict() -> Self {
Self {
include_sizes: true,
}
}
#[inline]
#[must_use]
pub const fn logical() -> Self {
Self {
include_sizes: false,
}
}
#[inline]
fn width(self, bytes: u8) -> Option<u8> {
self.include_sizes.then_some(bytes)
}
#[inline]
fn ptr_width(self, size: Option<u64>) -> Option<u8> {
self.include_sizes
.then(|| size.and_then(|s| u8::try_from(s).ok()))
.flatten()
}
#[inline]
fn offset(self, bits: u64) -> Option<u64> {
self.include_sizes.then_some(bits)
}
#[inline]
fn size(self, size: Option<u64>) -> Option<u64> {
self.include_sizes.then_some(size).flatten()
}
}
impl Default for CanonicalOptions {
#[inline]
fn default() -> Self {
Self::strict()
}
}
impl CanonicalType {
#[must_use]
pub fn key(&self) -> TypeKey {
let mut hasher = SipHasher13::new();
self.hash(&mut hasher);
TypeKey(hasher.finish128().as_u128())
}
#[must_use]
pub fn identity(&self) -> Option<TypeIdentity> {
match self {
CanonicalType::Named { tag, kind } => Some(TypeIdentity::Tagged {
tag: tag.clone(),
kind: *kind,
}),
CanonicalType::Aggregate {
tag: Some(tag),
kind,
..
} => Some(TypeIdentity::Tagged {
tag: tag.clone(),
kind: *kind,
}),
CanonicalType::Enum { tag: Some(tag), .. } => Some(TypeIdentity::Tagged {
tag: tag.clone(),
kind: AggregateKind::Enum,
}),
CanonicalType::Typedef { name, .. } => Some(TypeIdentity::Alias { name: name.clone() }),
_ => None,
}
}
}
fn is_synthetic(tag: &str) -> bool {
tag.is_empty() || tag.starts_with('$')
}
#[must_use]
pub fn canonicalize(table: &TypeTable, root: TypeId, opts: CanonicalOptions) -> CanonicalType {
let mut stack = Vec::new();
canon(table, root, opts, &mut stack, true)
}
fn canon(
table: &TypeTable,
id: TypeId,
opts: CanonicalOptions,
stack: &mut Vec<TypeId>,
spell_named: bool,
) -> CanonicalType {
let value = table.get(id);
match &value.shape {
TypeShape::Void => CanonicalType::Void,
TypeShape::Bool => CanonicalType::Bool,
TypeShape::Int { bytes, signed } => CanonicalType::Int {
bytes: opts.width(*bytes),
signed: *signed,
},
TypeShape::Float { bytes } => CanonicalType::Float {
bytes: opts.width(*bytes),
},
TypeShape::Ptr(inner) => CanonicalType::Ptr {
pointee: Box::new(canon(table, *inner, opts, stack, false)),
width: opts.ptr_width(value.size),
},
TypeShape::Array { elem, len } => CanonicalType::Array {
elem: Box::new(canon(table, *elem, opts, stack, false)),
len: *len,
},
TypeShape::Struct { name, members } => aggregate(
table,
id,
AggregateKind::Struct,
name.as_deref(),
members,
value.size,
opts,
stack,
spell_named,
),
TypeShape::Union { name, members } => aggregate(
table,
id,
AggregateKind::Union,
name.as_deref(),
members,
value.size,
opts,
stack,
spell_named,
),
TypeShape::Enum {
name,
underlying,
members,
} => {
if let Some(cut) = nominal_cut(name.as_deref(), AggregateKind::Enum, spell_named) {
return cut;
}
let mut constants: Vec<(String, u64)> =
members.iter().map(|m| (m.name.clone(), m.value)).collect();
constants.sort_by(|a, b| a.0.cmp(&b.0));
CanonicalType::Enum {
tag: real_tag(name.as_deref()),
underlying: Box::new(canon(table, *underlying, opts, stack, false)),
members: constants,
size: opts.size(value.size),
}
}
TypeShape::Function {
ret,
params,
varargs,
} => CanonicalType::Function {
ret: Box::new(canon(table, *ret, opts, stack, false)),
params: params
.iter()
.map(|p| canon(table, *p, opts, stack, false))
.collect(),
varargs: *varargs,
},
TypeShape::Typedef { name, underlying } => CanonicalType::Typedef {
name: name.clone(),
underlying: Box::new(canon(table, *underlying, opts, stack, false)),
},
TypeShape::Opaque(name) => CanonicalType::Opaque(name.clone()),
TypeShape::Unknown => CanonicalType::Opaque("<unfilled>".to_owned()),
}
}
fn real_tag(name: Option<&str>) -> Option<String> {
name.filter(|n| !is_synthetic(n)).map(str::to_owned)
}
fn nominal_cut(
name: Option<&str>,
kind: AggregateKind,
spell_named: bool,
) -> Option<CanonicalType> {
match real_tag(name) {
Some(tag) if !spell_named => Some(CanonicalType::Named { tag, kind }),
_ => None,
}
}
#[allow(clippy::too_many_arguments)] fn aggregate(
table: &TypeTable,
id: TypeId,
kind: AggregateKind,
name: Option<&str>,
members: &[crate::types::TypeMember],
size: Option<u64>,
opts: CanonicalOptions,
stack: &mut Vec<TypeId>,
spell_named: bool,
) -> CanonicalType {
if let Some(cut) = nominal_cut(name, kind, spell_named) {
return cut;
}
if let Some(pos) = stack.iter().rposition(|x| *x == id) {
return CanonicalType::BackRef(stack.len() - pos);
}
stack.push(id);
let mut members: Vec<CanonicalMember> = members
.iter()
.map(|m| CanonicalMember {
name: m.name.clone(),
bit_offset: opts.offset(m.bit_offset),
bitfield_width: m.bitfield_width,
ty: canon(table, m.ty, opts, stack, false),
})
.collect();
stack.pop();
if kind == AggregateKind::Union {
members.sort_by(|a, b| a.name.cmp(&b.name));
}
CanonicalType::Aggregate {
tag: real_tag(name),
kind,
members,
size: opts.size(size),
}
}
impl Type {
#[must_use]
pub fn canonical(&self) -> CanonicalType {
canonicalize(self.types(), self.root(), CanonicalOptions::strict())
}
#[must_use]
pub fn canonical_with(&self, opts: CanonicalOptions) -> CanonicalType {
canonicalize(self.types(), self.root(), opts)
}
#[must_use]
pub fn identity(&self) -> Option<TypeIdentity> {
self.canonical().identity()
}
#[must_use]
pub fn diff(&self, other: &Type) -> TypeDiff {
self.canonical().diff(&other.canonical())
}
}
impl fmt::Display for CanonicalType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
self.write_compact(f)
} else {
self.write_canonical(f)
}
}
}
impl CanonicalType {
fn write_scalar(&self, f: &mut fmt::Formatter<'_>) -> Option<fmt::Result> {
Some(match self {
CanonicalType::Void => f.write_str("void"),
CanonicalType::Bool => f.write_str("bool"),
CanonicalType::Int { bytes, signed } => {
let sign = if *signed { "i" } else { "u" };
match bytes {
Some(b) => write!(f, "{sign}{}", u32::from(*b) * 8),
None => write!(f, "{sign}int"),
}
}
CanonicalType::Float { bytes } => match bytes {
Some(b) => write!(f, "f{}", u32::from(*b) * 8),
None => f.write_str("float"),
},
CanonicalType::BackRef(n) => write!(f, "#{n}"),
_ => return None,
})
}
fn write_canonical(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(scalar) = self.write_scalar(f) {
return scalar;
}
match self {
CanonicalType::Ptr { pointee, width } => {
f.write_str("ptr")?;
if let Some(w) = width {
write!(f, ":{w}")?;
}
write!(f, "({pointee})")
}
CanonicalType::Array { elem, len } => write!(f, "[{elem};{len}]"),
CanonicalType::Named { tag, kind } => write!(f, "{}:{tag}", kind.keyword()),
CanonicalType::Aggregate {
tag,
kind,
members,
size,
} => {
f.write_str(kind.keyword())?;
if let Some(t) = tag {
write!(f, ":{t}")?;
}
f.write_str("{")?;
for (i, m) in members.iter().enumerate() {
if i > 0 {
f.write_str(",")?;
}
f.write_str(&m.name)?;
if let Some(o) = m.bit_offset {
write!(f, "@{o}")?;
}
if let Some(w) = m.bitfield_width {
write!(f, ":{w}")?;
}
write!(f, "={}", m.ty)?;
}
f.write_str("}")?;
if let Some(s) = size {
write!(f, "={s}")?;
}
Ok(())
}
CanonicalType::Enum {
tag,
underlying,
members,
size,
} => {
f.write_str("enum")?;
if let Some(t) = tag {
write!(f, ":{t}")?;
}
write!(f, "({underlying}){{")?;
for (i, (n, v)) in members.iter().enumerate() {
if i > 0 {
f.write_str(",")?;
}
write!(f, "{n}={v}")?;
}
f.write_str("}")?;
if let Some(s) = size {
write!(f, "={s}")?;
}
Ok(())
}
CanonicalType::Function {
ret,
params,
varargs,
} => {
f.write_str("fn(")?;
for (i, p) in params.iter().enumerate() {
if i > 0 {
f.write_str(",")?;
}
write!(f, "{p}")?;
}
if *varargs {
if !params.is_empty() {
f.write_str(",")?;
}
f.write_str("...")?;
}
write!(f, ")->{ret}")
}
CanonicalType::Typedef { name, underlying } => write!(f, "typedef {name}={underlying}"),
CanonicalType::Opaque(name) => write!(f, "opaque:{name}"),
CanonicalType::Void
| CanonicalType::Bool
| CanonicalType::Int { .. }
| CanonicalType::Float { .. }
| CanonicalType::BackRef(_) => unreachable!("scalars written above"),
}
}
fn write_compact(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(scalar) = self.write_scalar(f) {
return scalar;
}
match self {
CanonicalType::Ptr { pointee, .. } => write!(f, "{pointee:#}*"),
CanonicalType::Array { elem, len } => write!(f, "{elem:#}[{len}]"),
CanonicalType::Named { tag, .. } => f.write_str(tag),
CanonicalType::Aggregate { tag, kind, .. } => {
f.write_str(kind.keyword())?;
match tag {
Some(t) => write!(f, " {t}"),
None => f.write_str(" {...}"),
}
}
CanonicalType::Enum { tag, .. } => match tag {
Some(t) => write!(f, "enum {t}"),
None => f.write_str("enum {...}"),
},
CanonicalType::Function {
ret,
params,
varargs,
} => {
write!(f, "{ret:#}(")?;
for (i, p) in params.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
write!(f, "{p:#}")?;
}
if *varargs {
if !params.is_empty() {
f.write_str(", ")?;
}
f.write_str("...")?;
}
f.write_str(")")
}
CanonicalType::Typedef { name, .. } => f.write_str(name),
CanonicalType::Opaque(name) => f.write_str(name),
CanonicalType::Void
| CanonicalType::Bool
| CanonicalType::Int { .. }
| CanonicalType::Float { .. }
| CanonicalType::BackRef(_) => unreachable!("scalars written above"),
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct TypeDiff {
changes: Vec<Change>,
}
impl TypeDiff {
#[inline]
#[must_use]
pub fn changes(&self) -> &[Change] {
&self.changes
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.changes.is_empty()
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.changes.len()
}
#[must_use]
pub fn added(&self) -> usize {
self.changes
.iter()
.filter(|c| matches!(c.kind, ChangeKind::Added(_) | ChangeKind::ConstantAdded(_)))
.count()
}
#[must_use]
pub fn removed(&self) -> usize {
self.changes
.iter()
.filter(|c| {
matches!(
c.kind,
ChangeKind::Removed(_) | ChangeKind::ConstantRemoved(_)
)
})
.count()
}
#[must_use]
pub fn changed(&self) -> usize {
self.changes
.iter()
.filter(|c| {
matches!(
c.kind,
ChangeKind::Retyped { .. }
| ChangeKind::Renamed(_)
| ChangeKind::Moved { .. }
| ChangeKind::BitfieldChanged { .. }
| ChangeKind::ConstantChanged { .. }
)
})
.count()
}
#[must_use]
pub fn size_change(&self) -> Option<(Option<u64>, Option<u64>)> {
self.changes.iter().find_map(|c| match c.kind {
ChangeKind::SizeChanged { left, right } if c.path.is_empty() => Some((left, right)),
_ => None,
})
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Change {
pub path: String,
pub kind: ChangeKind,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum ChangeKind {
Added(CanonicalType),
Removed(CanonicalType),
Retyped {
left: CanonicalType,
right: CanonicalType,
},
Renamed(String),
Moved {
from: u64,
to: u64,
},
BitfieldChanged {
from: Option<u32>,
to: Option<u32>,
},
SizeChanged {
left: Option<u64>,
right: Option<u64>,
},
ConstantAdded(u64),
ConstantRemoved(u64),
ConstantChanged {
left: u64,
right: u64,
},
}
impl CanonicalType {
#[must_use]
pub fn diff(&self, other: &CanonicalType) -> TypeDiff {
let mut changes = Vec::new();
self.diff_into(String::new(), other, &mut changes);
TypeDiff { changes }
}
fn diff_into(&self, path: String, other: &CanonicalType, out: &mut Vec<Change>) {
if self == other {
return;
}
match (self, other) {
(
CanonicalType::Aggregate {
tag: lt,
kind: lk,
members: lm,
size: ls,
},
CanonicalType::Aggregate {
tag: rt,
kind: rk,
members: rm,
size: rs,
},
) if lk == rk && lt == rt => {
if ls != rs {
out.push(Change {
path: path.clone(),
kind: ChangeKind::SizeChanged {
left: *ls,
right: *rs,
},
});
}
diff_members(&path, lm, rm, out);
}
(
CanonicalType::Enum {
tag: lt,
underlying: lu,
members: lm,
size: ls,
},
CanonicalType::Enum {
tag: rt,
underlying: ru,
members: rm,
size: rs,
},
) if lt == rt => {
lu.diff_into(join(&path, "underlying"), ru, out);
if ls != rs {
out.push(Change {
path: path.clone(),
kind: ChangeKind::SizeChanged {
left: *ls,
right: *rs,
},
});
}
diff_constants(&path, lm, rm, out);
}
(
CanonicalType::Ptr {
pointee: lp,
width: lw,
},
CanonicalType::Ptr {
pointee: rp,
width: rw,
},
) if lw == rw => lp.diff_into(path, rp, out),
(
CanonicalType::Array { elem: le, len: ll },
CanonicalType::Array { elem: re, len: rl },
) if ll == rl => le.diff_into(path, re, out),
(
CanonicalType::Function {
ret: lret,
params: lp,
varargs: lv,
},
CanonicalType::Function {
ret: rret,
params: rp,
varargs: rv,
},
) if lv == rv && lp.len() == rp.len() => {
lret.diff_into(join(&path, "return"), rret, out);
for (i, (a, b)) in lp.iter().zip(rp).enumerate() {
a.diff_into(join(&path, &format!("arg{i}")), b, out);
}
}
(
CanonicalType::Void
| CanonicalType::Bool
| CanonicalType::Int { .. }
| CanonicalType::Float { .. }
| CanonicalType::Ptr { .. }
| CanonicalType::Array { .. }
| CanonicalType::Named { .. }
| CanonicalType::Aggregate { .. }
| CanonicalType::Enum { .. }
| CanonicalType::Function { .. }
| CanonicalType::Typedef { .. }
| CanonicalType::Opaque(_)
| CanonicalType::BackRef(_),
_,
) => out.push(Change {
path,
kind: ChangeKind::Retyped {
left: self.clone(),
right: other.clone(),
},
}),
}
}
}
impl CanonicalMember {
fn path_in(&self, parent: &str) -> String {
if self.name.is_empty() {
parent.to_owned()
} else {
join(parent, &self.name)
}
}
fn diff_against(
&self,
other: &CanonicalMember,
parent: &str,
report_move: bool,
out: &mut Vec<Change>,
) {
let at = other.path_in(parent);
self.ty.diff_into(at.clone(), &other.ty, out);
if self.bitfield_width != other.bitfield_width {
out.push(Change {
path: at.clone(),
kind: ChangeKind::BitfieldChanged {
from: self.bitfield_width,
to: other.bitfield_width,
},
});
}
if report_move
&& let (Some(from), Some(to)) = (self.bit_offset, other.bit_offset)
&& from != to
{
out.push(Change {
path: at,
kind: ChangeKind::Moved { from, to },
});
}
}
}
fn diff_members(path: &str, lm: &[CanonicalMember], rm: &[CanonicalMember], out: &mut Vec<Change>) {
let mut l_used = vec![false; lm.len()];
let mut r_used = vec![false; rm.len()];
let mut pairs: Vec<(usize, usize, bool)> = Vec::new();
for (li, l) in lm.iter().enumerate() {
if l.name.is_empty() {
continue;
}
if let Some(ri) = (0..rm.len()).find(|&ri| !r_used[ri] && rm[ri].name == l.name) {
l_used[li] = true;
r_used[ri] = true;
pairs.push((li, ri, false));
}
}
for li in 0..lm.len() {
if l_used[li] {
continue;
}
let Some(off) = lm[li].bit_offset else {
continue;
};
let mut at_off = (0..rm.len()).filter(|&ri| !r_used[ri] && rm[ri].bit_offset == Some(off));
if let (Some(ri), None) = (at_off.next(), at_off.next()) {
l_used[li] = true;
r_used[ri] = true;
pairs.push((li, ri, true));
}
}
let structural = l_used.iter().any(|u| !u) || r_used.iter().any(|u| !u);
for (li, ri, renamed) in pairs {
let (l, r) = (&lm[li], &rm[ri]);
if renamed && l.name != r.name {
out.push(Change {
path: r.path_in(path),
kind: ChangeKind::Renamed(l.name.clone()),
});
}
l.diff_against(r, path, !structural, out);
}
for l in lm
.iter()
.enumerate()
.filter(|&(li, _)| !l_used[li])
.map(|(_, l)| l)
{
out.push(Change {
path: l.path_in(path),
kind: ChangeKind::Removed(l.ty.clone()),
});
}
for r in rm
.iter()
.enumerate()
.filter(|&(ri, _)| !r_used[ri])
.map(|(_, r)| r)
{
out.push(Change {
path: r.path_in(path),
kind: ChangeKind::Added(r.ty.clone()),
});
}
}
fn diff_constants(path: &str, lm: &[(String, u64)], rm: &[(String, u64)], out: &mut Vec<Change>) {
for (name, value) in lm {
match rm.iter().find(|(rn, _)| rn == name) {
Some((_, rv)) if rv != value => out.push(Change {
path: join(path, name),
kind: ChangeKind::ConstantChanged {
left: *value,
right: *rv,
},
}),
Some(_) => {}
None => out.push(Change {
path: join(path, name),
kind: ChangeKind::ConstantRemoved(*value),
}),
}
}
for (name, value) in rm {
if !lm.iter().any(|(ln, _)| ln == name) {
out.push(Change {
path: join(path, name),
kind: ChangeKind::ConstantAdded(*value),
});
}
}
}
fn join(path: &str, name: &str) -> String {
if path.is_empty() {
name.to_owned()
} else {
format!("{path}.{name}")
}
}
fn size_str(size: Option<u64>) -> String {
size.map_or_else(|| "?".to_owned(), |v| format!("{v:#x}"))
}
fn off_str(bits: u64) -> String {
match bits % 8 {
0 => format!("{:#x}", bits / 8),
rem => format!("{:#x}+{rem}b", bits / 8),
}
}
fn bits_str(width: Option<u32>) -> String {
width.map_or_else(|| "none".to_owned(), |w| w.to_string())
}
fn split_parent(path: &str) -> (&str, &str) {
match path.rfind('.') {
Some(i) => (&path[..i], &path[i + 1..]),
None => ("", path),
}
}
struct DiffRow {
verb: &'static str,
path: String,
before: String,
after: Option<String>,
}
impl Change {
fn row(&self) -> DiffRow {
let path = self.path.clone();
match &self.kind {
ChangeKind::Added(t) => DiffRow::single("Add", path, format!("{t:#}")),
ChangeKind::Removed(t) => DiffRow::single("Remove", path, format!("{t:#}")),
ChangeKind::ConstantAdded(v) => DiffRow::single("Add", path, format!("{v:#x}")),
ChangeKind::ConstantRemoved(v) => DiffRow::single("Remove", path, format!("{v:#x}")),
ChangeKind::Retyped { left, right } => DiffRow::pair(
"Change type",
path,
format!("{left:#}"),
format!("{right:#}"),
),
ChangeKind::BitfieldChanged { from, to } => {
DiffRow::pair("Change width", path, bits_str(*from), bits_str(*to))
}
ChangeKind::ConstantChanged { left, right } => DiffRow::pair(
"Change value",
path,
format!("{left:#x}"),
format!("{right:#x}"),
),
ChangeKind::SizeChanged { left, right } => {
DiffRow::pair("Resize", path, size_str(*left), size_str(*right))
}
ChangeKind::Moved { from, to } => {
DiffRow::pair("Move", path, off_str(*from), off_str(*to))
}
ChangeKind::Renamed(from) => {
let (parent, leaf) = split_parent(&self.path);
DiffRow::pair("Rename", parent.to_owned(), from.clone(), leaf.to_owned())
}
}
}
}
impl DiffRow {
fn single(verb: &'static str, path: String, before: String) -> Self {
Self {
verb,
path,
before,
after: None,
}
}
fn pair(verb: &'static str, path: String, before: String, after: String) -> Self {
Self {
verb,
path,
before,
after: Some(after),
}
}
fn value(&self) -> String {
match &self.after {
Some(after) => format!("{} → {}", self.before, after),
None => self.before.clone(),
}
}
fn write(
&self,
f: &mut fmt::Formatter<'_>,
verb_w: usize,
has_path: bool,
path_w: usize,
budget: Option<usize>,
) -> fmt::Result {
let head = if has_path {
format!("{:verb_w$} {:path_w$}", self.verb, self.path)
} else {
format!("{:verb_w$}", self.verb)
};
let line = format!("{head} {}", self.value());
if budget.is_none_or(|b| line.chars().count() <= b) {
return f.write_str(line.trim_end());
}
if has_path && !self.path.is_empty() {
write!(f, "{:verb_w$} {}", self.verb, self.path)?;
} else {
f.write_str(self.verb)?;
}
match &self.after {
Some(after) => write!(f, "\n {}\n → {}", self.before, after),
None => write!(f, "\n {}", self.before),
}
}
}
impl fmt::Display for TypeDiff {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.changes.is_empty() {
return f.write_str("(identical)");
}
let rows: Vec<DiffRow> = self.changes.iter().map(Change::row).collect();
let verb_w = rows.iter().map(|r| r.verb.len()).max().unwrap_or(0);
let has_path = rows.iter().any(|r| !r.path.is_empty());
let path_w = rows
.iter()
.map(|r| r.path.chars().count())
.max()
.unwrap_or(0);
let budget = f.width();
for (i, r) in rows.iter().enumerate() {
if i > 0 {
writeln!(f)?;
}
r.write(f, verb_w, has_path, path_w, budget)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use assert2::assert;
use super::*;
use crate::types::{EnumMember, TypeMember, TypeValue};
fn scalar(table: &mut TypeTable, bytes: u8, signed: bool) -> TypeId {
table.intern(TypeValue {
shape: TypeShape::Int { bytes, signed },
size: Some(u64::from(bytes)),
})
}
fn ptr(table: &mut TypeTable, to: TypeId, width: u64) -> TypeId {
table.intern(TypeValue {
shape: TypeShape::Ptr(to),
size: Some(width),
})
}
fn member(name: &str, bit_offset: u64, ty: TypeId) -> TypeMember {
TypeMember {
name: name.to_owned(),
bit_offset,
ty,
bitfield_width: None,
}
}
fn point(table: &mut TypeTable) -> TypeId {
let int = scalar(table, 4, true);
table.intern(TypeValue {
shape: TypeShape::Struct {
name: Some("point".to_owned()),
members: vec![member("x", 0, int), member("y", 32, int)],
},
size: Some(8),
})
}
#[test]
fn scalar_projects_to_all_forms() {
let mut table = TypeTable::new();
let id = scalar(&mut table, 4, true);
let c = canonicalize(&table, id, CanonicalOptions::strict());
assert!(
c == CanonicalType::Int {
bytes: Some(4),
signed: true
}
);
assert!(c.to_string() == "i32");
assert!(c.identity().is_none());
assert!(c.key() == c.key());
}
#[test]
fn identical_types_in_separate_tables_share_every_projection() {
let mut a = TypeTable::new();
let root_a = point(&mut a);
let ca = canonicalize(&a, root_a, CanonicalOptions::strict());
let mut b = TypeTable::new();
let root_b = point(&mut b);
let cb = canonicalize(&b, root_b, CanonicalOptions::strict());
assert!(ca == cb);
assert!(ca.key() == cb.key());
assert!(ca.to_string() == cb.to_string());
assert!(ca.identity() == cb.identity());
}
#[test]
fn a_changed_member_keeps_identity_but_changes_the_fingerprint() {
let mut a = TypeTable::new();
let root_a = point(&mut a);
let ca = canonicalize(&a, root_a, CanonicalOptions::strict());
let mut b = TypeTable::new();
let int = scalar(&mut b, 4, true);
let root_b = b.intern(TypeValue {
shape: TypeShape::Struct {
name: Some("point".to_owned()),
members: vec![
member("x", 0, int),
member("y", 32, int),
member("z", 64, int),
],
},
size: Some(12),
});
let cb = canonicalize(&b, root_b, CanonicalOptions::strict());
assert!(ca.identity() == cb.identity());
assert!(
ca.identity()
== Some(TypeIdentity::Tagged {
tag: "point".to_owned(),
kind: AggregateKind::Struct,
})
);
assert!(ca != cb);
assert!(ca.key() != cb.key());
}
#[test]
fn a_named_aggregate_is_spelled_at_root_and_cut_when_referenced() {
let mut table = TypeTable::new();
let node = table.alloc_placeholder();
let node_ptr = ptr(&mut table, node, 8);
table.fill(
node,
TypeValue {
shape: TypeShape::Struct {
name: Some("node".to_owned()),
members: vec![member("next", 0, node_ptr)],
},
size: Some(8),
},
);
let c = canonicalize(&table, node, CanonicalOptions::strict());
let CanonicalType::Aggregate { members, .. } = &c else {
panic!("root spells its body");
};
assert!(
members[0].ty
== CanonicalType::Ptr {
pointee: Box::new(CanonicalType::Named {
tag: "node".to_owned(),
kind: AggregateKind::Struct,
}),
width: Some(8),
}
);
}
#[test]
fn a_synthetic_named_cycle_closes_with_a_backref() {
let mut table = TypeTable::new();
let anon = table.alloc_placeholder();
let anon_ptr = ptr(&mut table, anon, 8);
table.fill(
anon,
TypeValue {
shape: TypeShape::Struct {
name: Some("$A".to_owned()),
members: vec![member("self", 0, anon_ptr)],
},
size: Some(8),
},
);
let c = canonicalize(&table, anon, CanonicalOptions::strict());
let CanonicalType::Aggregate { tag, members, .. } = &c else {
panic!("a synthetic tag is spelled structurally");
};
assert!(tag.is_none()); assert!(
members[0].ty
== CanonicalType::Ptr {
pointee: Box::new(CanonicalType::BackRef(1)),
width: Some(8),
}
);
}
#[test]
fn the_size_knob_separates_abi_from_logical() {
let mut a = TypeTable::new();
let i32t = scalar(&mut a, 4, true);
let root_a = a.intern(TypeValue {
shape: TypeShape::Struct {
name: None,
members: vec![member("v", 0, i32t)],
},
size: Some(4),
});
let mut b = TypeTable::new();
let i64t = scalar(&mut b, 8, true);
let root_b = b.intern(TypeValue {
shape: TypeShape::Struct {
name: None,
members: vec![member("v", 0, i64t)],
},
size: Some(8),
});
let strict_a = canonicalize(&a, root_a, CanonicalOptions::strict());
let strict_b = canonicalize(&b, root_b, CanonicalOptions::strict());
assert!(strict_a.key() != strict_b.key());
let logical_a = canonicalize(&a, root_a, CanonicalOptions::logical());
let logical_b = canonicalize(&b, root_b, CanonicalOptions::logical());
assert!(logical_a == logical_b);
assert!(logical_a.key() == logical_b.key());
assert!(logical_a.to_string() == "struct{v=iint}");
}
#[test]
fn bitfields_ride_along_in_the_member() {
let mut table = TypeTable::new();
let u32t = scalar(&mut table, 4, false);
let root = table.intern(TypeValue {
shape: TypeShape::Struct {
name: Some("flags".to_owned()),
members: vec![TypeMember {
name: "lo".to_owned(),
bit_offset: 0,
ty: u32t,
bitfield_width: Some(3),
}],
},
size: Some(4),
});
let c = canonicalize(&table, root, CanonicalOptions::strict());
let CanonicalType::Aggregate { members, .. } = &c else {
panic!("struct");
};
assert!(members[0].bitfield_width == Some(3));
assert!(c.to_string() == "struct:flags{lo@0:3=u32}=4");
}
#[test]
fn an_enum_spells_at_root_and_cuts_when_referenced() {
let mut table = TypeTable::new();
let int = scalar(&mut table, 4, false);
let color = table.intern(TypeValue {
shape: TypeShape::Enum {
name: Some("color".to_owned()),
underlying: int,
members: vec![
EnumMember {
name: "red".to_owned(),
value: 0,
},
EnumMember {
name: "green".to_owned(),
value: 1,
},
],
},
size: Some(4),
});
let color_ptr = ptr(&mut table, color, 8);
let referenced = canonicalize(&table, color_ptr, CanonicalOptions::strict());
assert!(
referenced
== CanonicalType::Ptr {
pointee: Box::new(CanonicalType::Named {
tag: "color".to_owned(),
kind: AggregateKind::Enum,
}),
width: Some(8),
}
);
let defined = canonicalize(&table, color, CanonicalOptions::strict());
assert!(
defined.identity()
== Some(TypeIdentity::Tagged {
tag: "color".to_owned(),
kind: AggregateKind::Enum,
})
);
assert!(defined.to_string() == "enum:color(u32){green=1,red=0}=4");
}
#[test]
fn a_function_prototype_canonicalizes() {
let mut table = TypeTable::new();
let int = scalar(&mut table, 4, true);
let cstr = {
let ch = table.intern(TypeValue {
shape: TypeShape::Int {
bytes: 1,
signed: true,
},
size: Some(1),
});
ptr(&mut table, ch, 8)
};
let proto = table.intern(TypeValue {
shape: TypeShape::Function {
ret: int,
params: vec![cstr],
varargs: true,
},
size: None,
});
let c = canonicalize(&table, proto, CanonicalOptions::strict());
assert!(c.to_string() == "fn(ptr:8(i8),...)->i32");
assert!(c.identity().is_none());
}
#[test]
fn a_typedef_keeps_its_alias_as_identity() {
let mut table = TypeTable::new();
let int = scalar(&mut table, 4, false);
let alias = table.intern(TypeValue {
shape: TypeShape::Typedef {
name: "u32".to_owned(),
underlying: int,
},
size: Some(4),
});
let c = canonicalize(&table, alias, CanonicalOptions::strict());
assert!(
c.identity()
== Some(TypeIdentity::Alias {
name: "u32".to_owned(),
})
);
assert!(c.to_string() == "typedef u32=u32");
}
#[test]
fn identical_types_diff_to_nothing() {
let mut t = TypeTable::new();
let p = point(&mut t);
let c = canonicalize(&t, p, CanonicalOptions::strict());
let d = c.diff(&c);
assert!(d.is_empty());
assert!(d.to_string() == "(identical)");
}
#[test]
fn a_scalar_retype_is_a_root_change() {
let a = CanonicalType::Int {
bytes: Some(4),
signed: true,
};
let b = CanonicalType::Int {
bytes: Some(8),
signed: true,
};
let d = a.diff(&b);
assert!(d.changes().len() == 1);
assert!(d.to_string() == "Change type i32 → i64");
}
#[test]
fn drift_reports_retype_add_and_size() {
let mut a = TypeTable::new();
let p = point(&mut a);
let ca = canonicalize(&a, p, CanonicalOptions::strict());
let mut b = TypeTable::new();
let i32t = scalar(&mut b, 4, true);
let i64t = scalar(&mut b, 8, true);
let root = b.intern(TypeValue {
shape: TypeShape::Struct {
name: Some("point".to_owned()),
members: vec![
member("x", 0, i32t),
member("y", 32, i64t),
member("z", 96, i32t),
],
},
size: Some(16),
});
let cb = canonicalize(&b, root, CanonicalOptions::strict());
let d = ca.diff(&cb);
let s = d.to_string();
assert!(!d.is_empty());
assert!(s.contains("Resize"));
assert!(s.contains("0x8 → 0x10"));
assert!(s.contains("Change type"));
assert!(s.contains("i32 → i64"));
assert!(s.contains("Add"));
assert!(d.len() == 3);
assert!(d.added() == 1);
assert!(d.removed() == 0);
assert!(d.changed() == 1);
assert!(d.size_change() == Some((Some(8), Some(16))));
}
#[test]
fn a_nested_member_change_uses_a_dotted_path() {
let build = |vbytes: u8| {
let mut t = TypeTable::new();
let v = scalar(&mut t, vbytes, true);
let inner = t.intern(TypeValue {
shape: TypeShape::Struct {
name: None,
members: vec![member("v", 0, v)],
},
size: Some(u64::from(vbytes)),
});
let outer = t.intern(TypeValue {
shape: TypeShape::Struct {
name: Some("outer".to_owned()),
members: vec![member("in", 0, inner)],
},
size: Some(u64::from(vbytes)),
});
canonicalize(&t, outer, CanonicalOptions::strict())
};
let d = build(4).diff(&build(8));
let s = d.to_string();
assert!(s.contains("in.v"));
assert!(s.contains("i32 → i64"));
}
fn cint(bytes: u8, signed: bool) -> CanonicalType {
CanonicalType::Int {
bytes: Some(bytes),
signed,
}
}
fn cfield(name: &str, off: u64, ty: CanonicalType) -> CanonicalMember {
CanonicalMember {
name: name.to_owned(),
bit_offset: Some(off),
bitfield_width: None,
ty,
}
}
fn cstruct(tag: &str, members: Vec<CanonicalMember>, size: u64) -> CanonicalType {
CanonicalType::Aggregate {
tag: Some(tag.to_owned()),
kind: AggregateKind::Struct,
members,
size: Some(size),
}
}
fn enum_of(table: &mut TypeTable, tag: &str, consts: &[(&str, u64)]) -> TypeId {
let underlying = scalar(table, 4, false);
table.intern(TypeValue {
shape: TypeShape::Enum {
name: Some(tag.to_owned()),
underlying,
members: consts
.iter()
.map(|(n, v)| EnumMember {
name: (*n).to_owned(),
value: *v,
})
.collect(),
},
size: Some(4),
})
}
#[test]
fn an_enum_reordered_is_the_same_type() {
let mut a = TypeTable::new();
let ea = enum_of(&mut a, "E", &[("A", 1), ("B", 2), ("C", 3)]);
let ca = canonicalize(&a, ea, CanonicalOptions::strict());
let mut b = TypeTable::new();
let eb = enum_of(&mut b, "E", &[("C", 3), ("A", 1), ("B", 2)]);
let cb = canonicalize(&b, eb, CanonicalOptions::strict());
assert!(ca == cb);
assert!(ca.key() == cb.key());
assert!(ca.diff(&cb).is_empty());
}
#[test]
fn a_union_reordered_is_the_same_type() {
let build = |order: &[&str]| {
let mut t = TypeTable::new();
let i = scalar(&mut t, 4, true);
let f = scalar(&mut t, 8, false);
let by_name = |n: &str| TypeMember {
name: n.to_owned(),
bit_offset: 0,
ty: if n == "i" { i } else { f },
bitfield_width: None,
};
let root = t.intern(TypeValue {
shape: TypeShape::Union {
name: Some("U".to_owned()),
members: order.iter().map(|n| by_name(n)).collect(),
},
size: Some(8),
});
canonicalize(&t, root, CanonicalOptions::strict())
};
let ca = build(&["i", "f"]);
let cb = build(&["f", "i"]);
assert!(ca == cb);
assert!(ca.diff(&cb).is_empty());
}
#[test]
fn a_renamed_field_is_detected_not_add_remove() {
let a = cstruct("S", vec![cfield("old", 0, cint(4, true))], 4);
let b = cstruct("S", vec![cfield("new", 0, cint(4, true))], 4);
let d = a.diff(&b);
assert!(d.changes().len() == 1);
assert!(d.to_string() == "Rename old → new");
}
#[test]
fn a_repacked_field_moves() {
let a = cstruct(
"S",
vec![
cfield("x", 0, cint(4, true)),
cfield("y", 64, cint(4, true)),
],
16,
);
let b = cstruct(
"S",
vec![
cfield("x", 0, cint(4, true)),
cfield("y", 96, cint(4, true)),
],
16,
);
let d = a.diff(&b);
assert!(d.to_string() == "Move y 0x8 → 0xc");
}
#[test]
fn an_inserted_field_does_not_report_the_offset_cascade() {
let a = cstruct(
"S",
vec![
cfield("x", 0, cint(4, true)),
cfield("y", 32, cint(4, true)),
],
8,
);
let b = cstruct(
"S",
vec![
cfield("x", 0, cint(4, true)),
cfield("mid", 32, cint(4, true)),
cfield("y", 64, cint(4, true)),
],
12,
);
let d = a.diff(&b);
let s = d.to_string();
assert!(s.contains("Add"));
assert!(s.contains("mid"));
assert!(s.contains("Resize"));
assert!(s.contains("0x8 → 0xc"));
assert!(!s.contains("Move")); }
#[test]
fn a_bitfield_width_change_is_flagged() {
let bf = |w: u32| CanonicalType::Aggregate {
tag: Some("F".to_owned()),
kind: AggregateKind::Struct,
members: vec![CanonicalMember {
name: "b".to_owned(),
bit_offset: Some(0),
bitfield_width: Some(w),
ty: cint(4, false),
}],
size: Some(4),
};
let d = bf(3).diff(&bf(5));
assert!(d.to_string() == "Change width b 3 → 5");
}
#[test]
fn a_different_tag_is_a_whole_retype() {
let a = cstruct("A", vec![cfield("x", 0, cint(4, true))], 4);
let b = cstruct("B", vec![cfield("x", 0, cint(4, true))], 4);
let d = a.diff(&b);
assert!(d.changes().len() == 1);
assert!(let ChangeKind::Retyped { .. } = &d.changes()[0].kind);
}
#[test]
fn a_struct_and_union_of_one_body_still_differ() {
let s = cstruct("X", vec![cfield("a", 0, cint(4, true))], 4);
let u = CanonicalType::Aggregate {
tag: Some("X".to_owned()),
kind: AggregateKind::Union,
members: vec![cfield("a", 0, cint(4, true))],
size: Some(4),
};
let d = s.diff(&u);
assert!(let ChangeKind::Retyped { .. } = &d.changes()[0].kind);
}
#[test]
fn enum_constants_add_remove_and_change() {
let en = |consts: &[(&str, u64)]| CanonicalType::Enum {
tag: Some("E".to_owned()),
underlying: Box::new(cint(4, false)),
members: consts.iter().map(|(n, v)| ((*n).to_owned(), *v)).collect(),
size: Some(4),
};
let a = en(&[("A", 1), ("B", 2)]);
let b = en(&[("A", 9), ("C", 3)]); let d = a.diff(&b);
let s = d.to_string();
assert!(s.contains("Change value"));
assert!(s.contains("0x1 → 0x9"));
assert!(s.contains("Remove"));
assert!(s.contains("0x2"));
assert!(s.contains("Add"));
assert!(s.contains("0x3"));
assert!(d.added() == 1);
assert!(d.removed() == 1);
assert!(d.changed() == 1);
assert!(d.size_change().is_none());
}
}
#[cfg(test)]
mod property {
use std::collections::HashSet;
use proptest::prelude::*;
use super::*;
fn field_name() -> impl Strategy<Value = String> {
prop_oneof![Just("a"), Just("b"), Just("c"), Just("d")].prop_map(str::to_owned)
}
fn const_name() -> impl Strategy<Value = String> {
prop_oneof![Just("K0"), Just("K1"), Just("K2")].prop_map(str::to_owned)
}
fn tag_name() -> impl Strategy<Value = String> {
prop_oneof![Just("T"), Just("U")].prop_map(str::to_owned)
}
fn opt_tag() -> impl Strategy<Value = Option<String>> {
prop_oneof![Just(None), tag_name().prop_map(Some)]
}
fn opt_size() -> impl Strategy<Value = Option<u64>> {
prop_oneof![Just(None), (0u64..64).prop_map(Some)]
}
fn opt_bitfield() -> impl Strategy<Value = Option<u32>> {
prop_oneof![Just(None), (1u32..8).prop_map(Some)]
}
fn agg_kind() -> impl Strategy<Value = AggregateKind> {
prop_oneof![
Just(AggregateKind::Struct),
Just(AggregateKind::Union),
Just(AggregateKind::Enum),
]
}
fn build_members(
kind: AggregateKind,
raw: Vec<(String, Option<u32>, CanonicalType)>,
) -> Vec<CanonicalMember> {
let mut seen = HashSet::new();
let mut out: Vec<CanonicalMember> = Vec::new();
for (name, bitfield_width, ty) in raw {
if seen.insert(name.clone()) {
let bit_offset = Some(if kind == AggregateKind::Union {
0
} else {
out.len() as u64 * 64
});
out.push(CanonicalMember {
name,
bit_offset,
bitfield_width,
ty,
});
}
}
if kind == AggregateKind::Union {
out.sort_by(|a, b| a.name.cmp(&b.name));
}
out
}
fn constants() -> impl Strategy<Value = Vec<(String, u64)>> {
prop::collection::vec((const_name(), any::<u64>()), 0..4).prop_map(|raw| {
let mut seen = HashSet::new();
let mut out: Vec<(String, u64)> = Vec::new();
for (name, value) in raw {
if seen.insert(name.clone()) {
out.push((name, value));
}
}
out.sort_by(|a, b| a.0.cmp(&b.0));
out
})
}
fn canonical() -> impl Strategy<Value = CanonicalType> {
let leaf = prop_oneof![
Just(CanonicalType::Void),
Just(CanonicalType::Bool),
(
prop_oneof![Just(1u8), Just(2u8), Just(4u8), Just(8u8)],
any::<bool>()
)
.prop_map(|(bytes, signed)| CanonicalType::Int {
bytes: Some(bytes),
signed
}),
prop_oneof![Just(4u8), Just(8u8)].prop_map(|b| CanonicalType::Float { bytes: Some(b) }),
(tag_name(), agg_kind()).prop_map(|(tag, kind)| CanonicalType::Named { tag, kind }),
tag_name().prop_map(CanonicalType::Opaque),
(0usize..3).prop_map(CanonicalType::BackRef),
];
leaf.prop_recursive(4, 40, 4, |inner| {
let members = move |kind: AggregateKind, inner: BoxedStrategy<CanonicalType>| {
prop::collection::vec((field_name(), opt_bitfield(), inner), 0..4)
.prop_map(move |raw| build_members(kind, raw))
};
let inner = inner.boxed();
prop_oneof![
(
inner.clone(),
prop_oneof![Just(None), Just(Some(4u8)), Just(Some(8u8))]
)
.prop_map(|(p, width)| CanonicalType::Ptr {
pointee: Box::new(p),
width
}),
(inner.clone(), 0u64..4).prop_map(|(e, len)| CanonicalType::Array {
elem: Box::new(e),
len
}),
(
opt_tag(),
members(AggregateKind::Struct, inner.clone()),
opt_size()
)
.prop_map(|(tag, members, size)| CanonicalType::Aggregate {
tag,
kind: AggregateKind::Struct,
members,
size
}),
(
opt_tag(),
members(AggregateKind::Union, inner.clone()),
opt_size()
)
.prop_map(|(tag, members, size)| CanonicalType::Aggregate {
tag,
kind: AggregateKind::Union,
members,
size
}),
(opt_tag(), inner.clone(), constants(), opt_size()).prop_map(
|(tag, underlying, members, size)| CanonicalType::Enum {
tag,
underlying: Box::new(underlying),
members,
size
}
),
(tag_name(), inner.clone()).prop_map(|(name, u)| CanonicalType::Typedef {
name,
underlying: Box::new(u)
}),
(
inner.clone(),
prop::collection::vec(inner, 0..3),
any::<bool>()
)
.prop_map(|(ret, params, varargs)| CanonicalType::Function {
ret: Box::new(ret),
params,
varargs
}),
]
})
}
proptest! {
#[test]
fn diff_is_empty_iff_equal(a in canonical(), b in canonical()) {
prop_assert_eq!(a.diff(&b).is_empty(), a == b);
}
#[test]
fn self_diff_is_empty(a in canonical()) {
prop_assert!(a.diff(&a).is_empty());
}
}
}