1use airl_ir::module::{FuncDef, Module};
10use serde::{Deserialize, Serialize};
11use std::collections::{HashMap, HashSet};
12
13#[derive(Clone, Debug, Serialize, Deserialize)]
18pub struct ModuleDiff {
19 pub added_functions: Vec<String>,
21 pub removed_functions: Vec<String>,
23 pub modified_functions: Vec<FunctionDiff>,
25 pub added_imports: Vec<String>,
27 pub removed_imports: Vec<String>,
29}
30
31#[derive(Clone, Debug, Serialize, Deserialize)]
33pub struct FunctionDiff {
34 pub name: String,
36 pub signature_changed: bool,
38 pub old_signature: String,
40 pub new_signature: String,
42 pub effects_changed: bool,
44 pub old_effects: Vec<String>,
46 pub new_effects: Vec<String>,
48 pub body_node_count_delta: i64,
50 pub old_node_count: u32,
52 pub new_node_count: u32,
54}
55
56impl ModuleDiff {
57 pub fn is_empty(&self) -> bool {
59 self.added_functions.is_empty()
60 && self.removed_functions.is_empty()
61 && self.modified_functions.is_empty()
62 && self.added_imports.is_empty()
63 && self.removed_imports.is_empty()
64 }
65
66 pub fn summary(&self) -> String {
68 let mut parts = Vec::new();
69 if !self.added_functions.is_empty() {
70 parts.push(format!("+{} fn", self.added_functions.len()));
71 }
72 if !self.removed_functions.is_empty() {
73 parts.push(format!("-{} fn", self.removed_functions.len()));
74 }
75 if !self.modified_functions.is_empty() {
76 parts.push(format!("~{} fn", self.modified_functions.len()));
77 }
78 if !self.added_imports.is_empty() {
79 parts.push(format!("+{} import", self.added_imports.len()));
80 }
81 if !self.removed_imports.is_empty() {
82 parts.push(format!("-{} import", self.removed_imports.len()));
83 }
84 if parts.is_empty() {
85 "no changes".to_string()
86 } else {
87 parts.join(", ")
88 }
89 }
90}
91
92pub fn diff(old: &Module, new: &Module) -> ModuleDiff {
94 let old_funcs: HashMap<&str, &FuncDef> = old
95 .functions()
96 .iter()
97 .map(|f| (f.name.as_str(), f))
98 .collect();
99 let new_funcs: HashMap<&str, &FuncDef> = new
100 .functions()
101 .iter()
102 .map(|f| (f.name.as_str(), f))
103 .collect();
104
105 let old_names: HashSet<&str> = old_funcs.keys().copied().collect();
106 let new_names: HashSet<&str> = new_funcs.keys().copied().collect();
107
108 let added_functions: Vec<String> = new_names
109 .difference(&old_names)
110 .map(|s| s.to_string())
111 .collect();
112 let removed_functions: Vec<String> = old_names
113 .difference(&new_names)
114 .map(|s| s.to_string())
115 .collect();
116
117 let mut modified_functions = Vec::new();
118 for name in old_names.intersection(&new_names) {
119 let old_f = old_funcs[name];
120 let new_f = new_funcs[name];
121 if let Some(fd) = diff_function(old_f, new_f) {
122 modified_functions.push(fd);
123 }
124 }
125
126 let old_imports: HashSet<String> = old
128 .module
129 .imports
130 .iter()
131 .map(|i| format!("{}::{}", i.module, i.items.join(",")))
132 .collect();
133 let new_imports: HashSet<String> = new
134 .module
135 .imports
136 .iter()
137 .map(|i| format!("{}::{}", i.module, i.items.join(",")))
138 .collect();
139
140 let added_imports: Vec<String> = new_imports.difference(&old_imports).cloned().collect();
141 let removed_imports: Vec<String> = old_imports.difference(&new_imports).cloned().collect();
142
143 let mut added_functions = added_functions;
145 added_functions.sort();
146 let mut removed_functions = removed_functions;
147 removed_functions.sort();
148 modified_functions.sort_by(|a, b| a.name.cmp(&b.name));
149 let mut added_imports = added_imports;
150 added_imports.sort();
151 let mut removed_imports = removed_imports;
152 removed_imports.sort();
153
154 ModuleDiff {
155 added_functions,
156 removed_functions,
157 modified_functions,
158 added_imports,
159 removed_imports,
160 }
161}
162
163fn diff_function(old: &FuncDef, new: &FuncDef) -> Option<FunctionDiff> {
164 let old_sig = function_signature(old);
165 let new_sig = function_signature(new);
166 let signature_changed = old_sig != new_sig;
167
168 let old_effects: Vec<String> = old.effects.iter().map(|e| e.to_effect_str()).collect();
169 let new_effects: Vec<String> = new.effects.iter().map(|e| e.to_effect_str()).collect();
170 let effects_changed = old_effects != new_effects;
171
172 let old_nodes = count_nodes(&old.body);
173 let new_nodes = count_nodes(&new.body);
174 let body_node_count_delta = new_nodes as i64 - old_nodes as i64;
175
176 let body_structurally_same = old.body == new.body;
177
178 if !signature_changed && !effects_changed && body_structurally_same {
179 return None;
180 }
181
182 Some(FunctionDiff {
183 name: old.name.clone(),
184 signature_changed,
185 old_signature: old_sig,
186 new_signature: new_sig,
187 effects_changed,
188 old_effects,
189 new_effects,
190 body_node_count_delta,
191 old_node_count: old_nodes,
192 new_node_count: new_nodes,
193 })
194}
195
196fn function_signature(func: &FuncDef) -> String {
197 let params: Vec<String> = func
198 .params
199 .iter()
200 .map(|p| format!("{}: {}", p.name, p.param_type.to_type_str()))
201 .collect();
202 format!(
203 "fn {}({}) -> {}",
204 func.name,
205 params.join(", "),
206 func.returns.to_type_str()
207 )
208}
209
210fn count_nodes(node: &airl_ir::node::Node) -> u32 {
211 use airl_ir::node::Node;
212 1 + match node {
213 Node::Let { value, body, .. } => count_nodes(value) + count_nodes(body),
214 Node::If {
215 cond,
216 then_branch,
217 else_branch,
218 ..
219 } => count_nodes(cond) + count_nodes(then_branch) + count_nodes(else_branch),
220 Node::Call { args, .. } => args.iter().map(count_nodes).sum(),
221 Node::Return { value, .. } => count_nodes(value),
222 Node::BinOp { lhs, rhs, .. } => count_nodes(lhs) + count_nodes(rhs),
223 Node::UnaryOp { operand, .. } => count_nodes(operand),
224 Node::Block {
225 statements, result, ..
226 } => statements.iter().map(count_nodes).sum::<u32>() + count_nodes(result),
227 Node::Loop { body, .. } => count_nodes(body),
228 Node::Match {
229 scrutinee, arms, ..
230 } => count_nodes(scrutinee) + arms.iter().map(|a| count_nodes(&a.body)).sum::<u32>(),
231 Node::ArrayLiteral { elements, .. } => elements.iter().map(count_nodes).sum(),
232 Node::IndexAccess { array, index, .. } => count_nodes(array) + count_nodes(index),
233 Node::StructLiteral { fields, .. } => fields.iter().map(|(_, n)| count_nodes(n)).sum(),
234 Node::FieldAccess { object, .. } => count_nodes(object),
235 _ => 0,
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 fn load(json: &str) -> Module {
244 serde_json::from_str(json).unwrap()
245 }
246
247 fn hello_v1() -> Module {
248 load(
249 r#"{
250 "format_version":"0.1.0",
251 "module":{"id":"m","name":"main",
252 "metadata":{"version":"1","description":"","author":"","created_at":""},
253 "imports":[],"exports":[],"types":[],
254 "functions":[{
255 "id":"f","name":"main","params":[],"returns":"Unit","effects":["IO"],
256 "body":{"id":"n1","kind":"Call","type":"Unit","target":"std::io::println",
257 "args":[{"id":"n2","kind":"Literal","type":"String","value":"hello"}]}
258 }]
259 }
260 }"#,
261 )
262 }
263
264 fn hello_v2_changed_body() -> Module {
265 load(
266 r#"{
267 "format_version":"0.1.0",
268 "module":{"id":"m","name":"main",
269 "metadata":{"version":"1","description":"","author":"","created_at":""},
270 "imports":[],"exports":[],"types":[],
271 "functions":[{
272 "id":"f","name":"main","params":[],"returns":"Unit","effects":["IO"],
273 "body":{"id":"n1","kind":"Call","type":"Unit","target":"std::io::println",
274 "args":[{"id":"n2","kind":"Literal","type":"String","value":"world"}]}
275 }]
276 }
277 }"#,
278 )
279 }
280
281 #[test]
282 fn test_diff_empty_when_identical() {
283 let m = hello_v1();
284 let d = diff(&m, &m);
285 assert!(d.is_empty());
286 assert_eq!(d.summary(), "no changes");
287 }
288
289 #[test]
290 fn test_diff_modified_body() {
291 let v1 = hello_v1();
292 let v2 = hello_v2_changed_body();
293 let d = diff(&v1, &v2);
294 assert!(!d.is_empty());
295 assert_eq!(d.modified_functions.len(), 1);
296 assert_eq!(d.modified_functions[0].name, "main");
297 assert!(!d.modified_functions[0].signature_changed);
299 }
300
301 #[test]
302 fn test_diff_added_function() {
303 let v1 = hello_v1();
304 let v2 = load(
305 r#"{
306 "format_version":"0.1.0",
307 "module":{"id":"m","name":"main",
308 "metadata":{"version":"1","description":"","author":"","created_at":""},
309 "imports":[],"exports":[],"types":[],
310 "functions":[
311 {"id":"f","name":"main","params":[],"returns":"Unit","effects":["IO"],
312 "body":{"id":"n1","kind":"Literal","type":"Unit","value":null}},
313 {"id":"g","name":"helper","params":[],"returns":"I64","effects":["Pure"],
314 "body":{"id":"n2","kind":"Literal","type":"I64","value":42}}
315 ]
316 }
317 }"#,
318 );
319 let d = diff(&v1, &v2);
320 assert_eq!(d.added_functions, vec!["helper".to_string()]);
321 assert!(d.removed_functions.is_empty());
322 }
323
324 #[test]
325 fn test_diff_summary() {
326 let v1 = hello_v1();
327 let v2 = hello_v2_changed_body();
328 let d = diff(&v1, &v2);
329 assert_eq!(d.summary(), "~1 fn");
330 }
331}