1use crate::types::StructLayout;
2use serde::Serialize;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Serialize)]
6pub struct DiffResult {
7 pub added: Vec<StructSummary>,
8 pub removed: Vec<StructSummary>,
9 pub changed: Vec<StructChange>,
10 pub unchanged_count: usize,
11}
12
13#[derive(Debug, Clone, Serialize)]
14pub struct StructSummary {
15 pub name: String,
16 pub size: u64,
17 pub padding_bytes: u64,
18}
19
20#[derive(Debug, Clone, Serialize)]
21pub struct StructChange {
22 pub name: String,
23 pub old_size: u64,
24 pub new_size: u64,
25 pub size_delta: i64,
26 pub old_padding: u64,
27 pub new_padding: u64,
28 pub padding_delta: i64,
29 pub member_changes: Vec<MemberChange>,
30}
31
32#[derive(Debug, Clone, Serialize)]
33pub struct MemberChange {
34 pub kind: MemberChangeKind,
35 pub name: String,
36 pub details: String,
37}
38
39#[derive(Debug, Clone, Serialize, PartialEq)]
40pub enum MemberChangeKind {
41 Added,
42 Removed,
43 OffsetChanged,
44 SizeChanged,
45 TypeChanged,
46}
47
48impl DiffResult {
49 pub fn has_changes(&self) -> bool {
50 !self.added.is_empty() || !self.removed.is_empty() || !self.changed.is_empty()
51 }
52
53 pub fn has_regressions(&self) -> bool {
54 self.changed.iter().any(|c| c.size_delta > 0 || c.padding_delta > 0)
56 }
57}
58
59pub fn diff_layouts(old: &[StructLayout], new: &[StructLayout]) -> DiffResult {
60 let old_map: HashMap<&str, &StructLayout> = old.iter().map(|s| (s.name.as_str(), s)).collect();
61 let new_map: HashMap<&str, &StructLayout> = new.iter().map(|s| (s.name.as_str(), s)).collect();
62
63 let mut added = Vec::new();
64 let mut removed = Vec::new();
65 let mut changed = Vec::new();
66 let mut unchanged_count = 0;
67
68 for (name, old_struct) in &old_map {
70 if !new_map.contains_key(name) {
71 removed.push(StructSummary {
72 name: name.to_string(),
73 size: old_struct.size,
74 padding_bytes: old_struct.metrics.padding_bytes,
75 });
76 }
77 }
78
79 for (name, new_struct) in &new_map {
81 match old_map.get(name) {
82 None => {
83 added.push(StructSummary {
84 name: name.to_string(),
85 size: new_struct.size,
86 padding_bytes: new_struct.metrics.padding_bytes,
87 });
88 }
89 Some(old_struct) => {
90 if let Some(change) = diff_struct(old_struct, new_struct) {
91 changed.push(change);
92 } else {
93 unchanged_count += 1;
94 }
95 }
96 }
97 }
98
99 added.sort_by(|a, b| a.name.cmp(&b.name));
101 removed.sort_by(|a, b| a.name.cmp(&b.name));
102 changed.sort_by(|a, b| a.name.cmp(&b.name));
103
104 DiffResult { added, removed, changed, unchanged_count }
105}
106
107fn diff_struct(old: &StructLayout, new: &StructLayout) -> Option<StructChange> {
108 let size_delta = i64::try_from(new.size)
110 .unwrap_or(i64::MAX)
111 .saturating_sub(i64::try_from(old.size).unwrap_or(i64::MAX));
112 let padding_delta = i64::try_from(new.metrics.padding_bytes)
113 .unwrap_or(i64::MAX)
114 .saturating_sub(i64::try_from(old.metrics.padding_bytes).unwrap_or(i64::MAX));
115
116 let mut member_changes = Vec::new();
117
118 let old_members: HashMap<&str, _> = old.members.iter().map(|m| (m.name.as_str(), m)).collect();
119 let new_members: HashMap<&str, _> = new.members.iter().map(|m| (m.name.as_str(), m)).collect();
120
121 for (name, old_member) in &old_members {
123 if !new_members.contains_key(name) {
124 member_changes.push(MemberChange {
125 kind: MemberChangeKind::Removed,
126 name: name.to_string(),
127 details: format!("offset {:?}, size {:?}", old_member.offset, old_member.size),
128 });
129 }
130 }
131
132 for (name, new_member) in &new_members {
134 match old_members.get(name) {
135 None => {
136 member_changes.push(MemberChange {
137 kind: MemberChangeKind::Added,
138 name: name.to_string(),
139 details: format!("offset {:?}, size {:?}", new_member.offset, new_member.size),
140 });
141 }
142 Some(old_member) => {
143 if old_member.offset != new_member.offset {
144 member_changes.push(MemberChange {
145 kind: MemberChangeKind::OffsetChanged,
146 name: name.to_string(),
147 details: format!("{:?} -> {:?}", old_member.offset, new_member.offset),
148 });
149 }
150 if old_member.size != new_member.size {
151 member_changes.push(MemberChange {
152 kind: MemberChangeKind::SizeChanged,
153 name: name.to_string(),
154 details: format!("{:?} -> {:?}", old_member.size, new_member.size),
155 });
156 }
157 if old_member.type_name != new_member.type_name {
158 member_changes.push(MemberChange {
159 kind: MemberChangeKind::TypeChanged,
160 name: name.to_string(),
161 details: format!("{} -> {}", old_member.type_name, new_member.type_name),
162 });
163 }
164 }
165 }
166 }
167
168 if size_delta == 0 && padding_delta == 0 && member_changes.is_empty() {
169 return None;
170 }
171
172 Some(StructChange {
173 name: old.name.clone(),
174 old_size: old.size,
175 new_size: new.size,
176 size_delta,
177 old_padding: old.metrics.padding_bytes,
178 new_padding: new.metrics.padding_bytes,
179 padding_delta,
180 member_changes,
181 })
182}