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)
55 }
56}
57
58pub fn diff_layouts(old: &[StructLayout], new: &[StructLayout]) -> DiffResult {
59 let old_map: HashMap<&str, &StructLayout> = old.iter().map(|s| (s.name.as_str(), s)).collect();
60 let new_map: HashMap<&str, &StructLayout> = new.iter().map(|s| (s.name.as_str(), s)).collect();
61
62 let mut added = Vec::new();
63 let mut removed = Vec::new();
64 let mut changed = Vec::new();
65 let mut unchanged_count = 0;
66
67 for (name, old_struct) in &old_map {
68 if !new_map.contains_key(name) {
69 removed.push(StructSummary {
70 name: name.to_string(),
71 size: old_struct.size,
72 padding_bytes: old_struct.metrics.padding_bytes,
73 });
74 }
75 }
76
77 for (name, new_struct) in &new_map {
78 match old_map.get(name) {
79 None => {
80 added.push(StructSummary {
81 name: name.to_string(),
82 size: new_struct.size,
83 padding_bytes: new_struct.metrics.padding_bytes,
84 });
85 }
86 Some(old_struct) => {
87 if let Some(change) = diff_struct(old_struct, new_struct) {
88 changed.push(change);
89 } else {
90 unchanged_count += 1;
91 }
92 }
93 }
94 }
95
96 added.sort_by(|a, b| a.name.cmp(&b.name));
97 removed.sort_by(|a, b| a.name.cmp(&b.name));
98 changed.sort_by(|a, b| a.name.cmp(&b.name));
99
100 DiffResult { added, removed, changed, unchanged_count }
101}
102
103fn diff_struct(old: &StructLayout, new: &StructLayout) -> Option<StructChange> {
104 let size_delta = i64::try_from(new.size)
105 .unwrap_or(i64::MAX)
106 .saturating_sub(i64::try_from(old.size).unwrap_or(i64::MAX));
107 let padding_delta = i64::try_from(new.metrics.padding_bytes)
108 .unwrap_or(i64::MAX)
109 .saturating_sub(i64::try_from(old.metrics.padding_bytes).unwrap_or(i64::MAX));
110
111 let mut member_changes = Vec::new();
112
113 let old_members: HashMap<&str, _> = old.members.iter().map(|m| (m.name.as_str(), m)).collect();
114 let new_members: HashMap<&str, _> = new.members.iter().map(|m| (m.name.as_str(), m)).collect();
115
116 for (name, old_member) in &old_members {
117 if !new_members.contains_key(name) {
118 member_changes.push(MemberChange {
119 kind: MemberChangeKind::Removed,
120 name: name.to_string(),
121 details: format!("offset {:?}, size {:?}", old_member.offset, old_member.size),
122 });
123 }
124 }
125
126 for (name, new_member) in &new_members {
127 match old_members.get(name) {
128 None => {
129 member_changes.push(MemberChange {
130 kind: MemberChangeKind::Added,
131 name: name.to_string(),
132 details: format!("offset {:?}, size {:?}", new_member.offset, new_member.size),
133 });
134 }
135 Some(old_member) => {
136 if old_member.offset != new_member.offset {
137 member_changes.push(MemberChange {
138 kind: MemberChangeKind::OffsetChanged,
139 name: name.to_string(),
140 details: format!("{:?} -> {:?}", old_member.offset, new_member.offset),
141 });
142 }
143 if old_member.size != new_member.size {
144 member_changes.push(MemberChange {
145 kind: MemberChangeKind::SizeChanged,
146 name: name.to_string(),
147 details: format!("{:?} -> {:?}", old_member.size, new_member.size),
148 });
149 }
150 if old_member.type_name != new_member.type_name {
151 member_changes.push(MemberChange {
152 kind: MemberChangeKind::TypeChanged,
153 name: name.to_string(),
154 details: format!("{} -> {}", old_member.type_name, new_member.type_name),
155 });
156 }
157 }
158 }
159 }
160
161 if size_delta == 0 && padding_delta == 0 && member_changes.is_empty() {
162 return None;
163 }
164
165 Some(StructChange {
166 name: old.name.clone(),
167 old_size: old.size,
168 new_size: new.size,
169 size_delta,
170 old_padding: old.metrics.padding_bytes,
171 new_padding: new.metrics.padding_bytes,
172 padding_delta,
173 member_changes,
174 })
175}