1use crate::diff_report::{
9 AddRemove, BodyPatch, DiffReport, EffectChanges, Modified, Renamed,
10};
11use lex_ast::{stage_canonical_hash_hex, CExpr, Effect, EffectArg, FnDecl, Stage, TypeDecl, TypeExpr};
12use std::collections::{BTreeMap, BTreeSet, HashMap};
13
14pub fn compute_diff(
25 a: &BTreeMap<String, FnDecl>,
26 b: &BTreeMap<String, FnDecl>,
27 body_patches: bool,
28) -> DiffReport {
29 compute_diff_with_types(a, b, &BTreeMap::new(), &BTreeMap::new(), body_patches)
30}
31
32pub fn compute_diff_with_types(
41 a: &BTreeMap<String, FnDecl>,
42 b: &BTreeMap<String, FnDecl>,
43 a_types: &BTreeMap<String, TypeDecl>,
44 b_types: &BTreeMap<String, TypeDecl>,
45 body_patches: bool,
46) -> DiffReport {
47 let mut report = DiffReport::default();
48 let names_a: BTreeSet<&String> = a.keys().collect();
49 let names_b: BTreeSet<&String> = b.keys().collect();
50
51 let only_a: Vec<&String> = names_a.difference(&names_b).copied().collect();
52 let only_b: Vec<&String> = names_b.difference(&names_a).copied().collect();
53
54 let mut hash_to_bs: HashMap<String, Vec<&String>> = HashMap::new();
71 for &bn in &only_b {
72 hash_to_bs.entry(body_hash(&b[bn])).or_default().push(bn);
73 }
74 let mut renamed_pairs: Vec<(String, String)> = Vec::new();
75 let mut consumed_a: BTreeSet<String> = BTreeSet::new();
76 let mut consumed_b: BTreeSet<String> = BTreeSet::new();
77 for &an in &only_a {
78 let fa = &a[an];
79 let fa_norm_id = body_hash(fa);
80 let Some(candidates) = hash_to_bs.get(&fa_norm_id) else { continue };
81 let Some(&bn) = candidates.iter().find(|bn| !consumed_b.contains(**bn)) else { continue };
82 renamed_pairs.push((an.clone(), bn.clone()));
83 consumed_a.insert(an.clone());
84 consumed_b.insert(bn.clone());
85 }
86
87 for &n in &only_a {
88 if consumed_a.contains(n) { continue; }
89 let fd = &a[n];
90 report.removed.push(AddRemove {
91 name: n.clone(),
92 signature: render_signature(fd),
93 old_sig_id: lex_ast::sig_id(&Stage::FnDecl(fd.clone())),
94 });
95 }
96 for &n in &only_b {
97 if consumed_b.contains(n) { continue; }
98 let fd = &b[n];
99 report.added.push(AddRemove {
100 name: n.clone(),
101 signature: render_signature(fd),
102 old_sig_id: None,
103 });
104 }
105 for (an, bn) in &renamed_pairs {
106 let fa = &a[an];
107 let fd = &b[bn];
108 report.renamed.push(Renamed {
109 from: an.clone(),
110 to: bn.clone(),
111 signature: render_signature(fd),
112 old_sig_id: lex_ast::sig_id(&Stage::FnDecl(fa.clone())).unwrap_or_default(),
113 });
114 }
115
116 for n in names_a.intersection(&names_b) {
118 let fa = &a[*n];
119 let fb = &b[*n];
120 let sig_a = render_signature(fa);
121 let sig_b = render_signature(fb);
122 if body_hash(fa) == body_hash(fb) && sig_a == sig_b { continue; }
123
124 let patches = if body_patches {
125 let mut patches = Vec::new();
126 diff_expr(&fa.body, &fb.body, "body", &mut patches, 4);
127 patches
128 } else { Vec::new() };
129
130 let effect_changes = effect_diff(&fa.effects, &fb.effects);
131 report.modified.push(Modified {
132 name: (*n).clone(),
133 signature_before: sig_a.clone(),
134 signature_after: sig_b.clone(),
135 signature_changed: sig_a != sig_b,
136 effect_changes,
137 body_patches: patches,
138 old_sig_id: lex_ast::sig_id(&Stage::FnDecl(fa.clone())).unwrap_or_default(),
139 });
140 }
141
142 let tnames_a: BTreeSet<&String> = a_types.keys().collect();
149 let tnames_b: BTreeSet<&String> = b_types.keys().collect();
150 for n in tnames_a.difference(&tnames_b) {
151 let td = &a_types[*n];
152 report.removed.push(AddRemove {
153 name: (*n).clone(),
154 signature: render_type_signature(td),
155 old_sig_id: lex_ast::sig_id(&Stage::TypeDecl(td.clone())),
156 });
157 }
158 for n in tnames_b.difference(&tnames_a) {
159 let td = &b_types[*n];
160 report.added.push(AddRemove {
161 name: (*n).clone(),
162 signature: render_type_signature(td),
163 old_sig_id: None,
164 });
165 }
166 for n in tnames_a.intersection(&tnames_b) {
167 let ta = &a_types[*n];
168 let tb = &b_types[*n];
169 let hash_a = stage_canonical_hash_hex(&Stage::TypeDecl(ta.clone()));
170 let hash_b = stage_canonical_hash_hex(&Stage::TypeDecl(tb.clone()));
171 if hash_a == hash_b { continue; }
172 let sig_a = lex_ast::sig_id(&Stage::TypeDecl(ta.clone()));
173 let sig_b = lex_ast::sig_id(&Stage::TypeDecl(tb.clone()));
174 if sig_a != sig_b {
175 report.removed.push(AddRemove {
178 name: (*n).clone(),
179 signature: render_type_signature(ta),
180 old_sig_id: sig_a,
181 });
182 report.added.push(AddRemove {
183 name: (*n).clone(),
184 signature: render_type_signature(tb),
185 old_sig_id: None,
186 });
187 } else {
188 report.modified.push(Modified {
189 name: (*n).clone(),
190 signature_before: render_type_signature(ta),
191 signature_after: render_type_signature(tb),
192 signature_changed: true,
193 effect_changes: EffectChanges::default(),
194 body_patches: Vec::new(),
195 old_sig_id: sig_a.unwrap_or_default(),
196 });
197 }
198 }
199 report
200}
201
202fn body_hash(fd: &FnDecl) -> String {
207 let mut anon = fd.clone();
208 anon.name = String::new();
209 let stage = Stage::FnDecl(anon);
210 stage_canonical_hash_hex(&stage)
211}
212
213fn diff_expr(a: &CExpr, b: &CExpr, path: &str, out: &mut Vec<BodyPatch>, depth: u32) {
217 if depth == 0 { return; }
218 let kind_a = node_kind(a);
219 let kind_b = node_kind(b);
220 if kind_a != kind_b {
221 out.push(BodyPatch {
222 op: "Replace".into(), node_path: path.into(),
223 from_kind: kind_a.into(), to_kind: kind_b.into(),
224 });
225 return;
226 }
227 match (a, b) {
229 (CExpr::Literal { value: la }, CExpr::Literal { value: lb }) => {
230 if la != lb {
231 out.push(BodyPatch {
232 op: "Replace".into(), node_path: path.into(),
233 from_kind: "Literal".into(), to_kind: "Literal".into(),
234 });
235 }
236 }
237 (CExpr::Var { name: na }, CExpr::Var { name: nb }) => {
238 if na != nb {
239 out.push(BodyPatch {
240 op: "Replace".into(), node_path: path.into(),
241 from_kind: format!("Var({na})"), to_kind: format!("Var({nb})"),
242 });
243 }
244 }
245 (CExpr::Call { callee: ca, args: aa },
246 CExpr::Call { callee: cb, args: ab }) => {
247 diff_expr(ca, cb, &format!("{path}.callee"), out, depth - 1);
248 diff_args(aa, ab, &format!("{path}.args"), out, depth);
249 }
250 (CExpr::Let { name: na, value: va, body: ba, .. },
251 CExpr::Let { name: nb, value: vb, body: bb, .. }) => {
252 if na != nb {
253 out.push(BodyPatch {
254 op: "Replace".into(),
255 node_path: format!("{path}.name"),
256 from_kind: format!("Let({na})"),
257 to_kind: format!("Let({nb})"),
258 });
259 }
260 diff_expr(va, vb, &format!("{path}.value"), out, depth - 1);
261 diff_expr(ba, bb, &format!("{path}.body"), out, depth - 1);
262 }
263 (CExpr::Match { scrutinee: sa, arms: ams },
264 CExpr::Match { scrutinee: sb, arms: bms }) => {
265 diff_expr(sa, sb, &format!("{path}.scrutinee"), out, depth - 1);
266 let n = ams.len().max(bms.len());
267 for i in 0..n {
268 let p = format!("{path}.arms[{i}]");
269 match (ams.get(i), bms.get(i)) {
270 (Some(a), Some(b)) =>
271 diff_expr(&a.body, &b.body, &p, out, depth - 1),
272 (Some(_), None) => out.push(BodyPatch {
273 op: "Deleted".into(), node_path: p,
274 from_kind: "MatchArm".into(), to_kind: "(removed)".into(),
275 }),
276 (None, Some(_)) => out.push(BodyPatch {
277 op: "Inserted".into(), node_path: p,
278 from_kind: "(none)".into(), to_kind: "MatchArm".into(),
279 }),
280 (None, None) => break,
281 }
282 }
283 }
284 (CExpr::Block { statements: sa, result: ra },
285 CExpr::Block { statements: sb, result: rb }) => {
286 diff_args(sa, sb, &format!("{path}.statements"), out, depth);
287 diff_expr(ra, rb, &format!("{path}.result"), out, depth - 1);
288 }
289 (CExpr::FieldAccess { value: va, field: fa },
290 CExpr::FieldAccess { value: vb, field: fb }) => {
291 diff_expr(va, vb, &format!("{path}.value"), out, depth - 1);
292 if fa != fb {
293 out.push(BodyPatch {
294 op: "Replace".into(), node_path: format!("{path}.field"),
295 from_kind: format!("Field({fa})"), to_kind: format!("Field({fb})"),
296 });
297 }
298 }
299 (CExpr::Lambda { body: ba, .. }, CExpr::Lambda { body: bb, .. }) => {
300 diff_expr(ba, bb, &format!("{path}.body"), out, depth - 1);
301 }
302 _ => {
306 out.push(BodyPatch {
307 op: "Replace".into(), node_path: path.into(),
308 from_kind: kind_a.into(), to_kind: kind_b.into(),
309 });
310 }
311 }
312}
313
314fn diff_args(a: &[CExpr], b: &[CExpr], path: &str, out: &mut Vec<BodyPatch>, depth: u32) {
315 let n = a.len().max(b.len());
316 for i in 0..n {
317 let p = format!("{path}[{i}]");
318 match (a.get(i), b.get(i)) {
319 (Some(x), Some(y)) => diff_expr(x, y, &p, out, depth - 1),
320 (Some(x), None) => out.push(BodyPatch {
321 op: "Deleted".into(), node_path: p,
322 from_kind: node_kind(x).into(), to_kind: "(removed)".into(),
323 }),
324 (None, Some(y)) => out.push(BodyPatch {
325 op: "Inserted".into(), node_path: p,
326 from_kind: "(none)".into(), to_kind: node_kind(y).into(),
327 }),
328 (None, None) => break,
329 }
330 }
331}
332
333fn node_kind(e: &CExpr) -> &'static str {
334 match e {
335 CExpr::Literal { .. } => "Literal",
336 CExpr::Var { .. } => "Var",
337 CExpr::Call { .. } => "Call",
338 CExpr::Let { .. } => "Let",
339 CExpr::Match { .. } => "Match",
340 CExpr::Block { .. } => "Block",
341 CExpr::Constructor { .. } => "Constructor",
342 CExpr::RecordLit { .. } => "RecordLit",
343 CExpr::TupleLit { .. } => "TupleLit",
344 CExpr::ListLit { .. } => "ListLit",
345 CExpr::FieldAccess { .. } => "FieldAccess",
346 CExpr::Lambda { .. } => "Lambda",
347 CExpr::BinOp { .. } => "BinOp",
348 CExpr::UnaryOp { .. } => "UnaryOp",
349 CExpr::Return { .. } => "Return",
350 }
351}
352
353pub fn render_signature(fd: &FnDecl) -> String {
354 let params: Vec<String> = fd.params.iter()
355 .map(|p| format!("{} :: {}", p.name, render_type(&p.ty))).collect();
356 let eff = if fd.effects.is_empty() { String::new() } else {
357 let labels: Vec<String> = fd.effects.iter().map(effect_label).collect();
358 format!("[{}] ", labels.join(", "))
359 };
360 format!("fn {}({}) -> {}{}", fd.name, params.join(", "),
361 eff, render_type(&fd.return_type))
362}
363
364pub fn render_type_signature(td: &TypeDecl) -> String {
368 let params = if td.params.is_empty() {
369 String::new()
370 } else {
371 format!("[{}]", td.params.join(", "))
372 };
373 format!("type {}{} = {}", td.name, params, render_type(&td.definition))
374}
375
376pub fn effect_label(e: &Effect) -> String {
381 match &e.arg {
382 Some(EffectArg::Str { value }) => format!("{}({:?})", e.name, value),
383 Some(EffectArg::Int { value }) => format!("{}({})", e.name, value),
384 Some(EffectArg::Ident { value }) => format!("{}({})", e.name, value),
385 None => e.name.clone(),
386 }
387}
388
389fn effect_diff(a: &[Effect], b: &[Effect]) -> EffectChanges {
393 let labels_a: BTreeSet<String> = a.iter().map(effect_label).collect();
394 let labels_b: BTreeSet<String> = b.iter().map(effect_label).collect();
395 let added: Vec<String> = labels_b.difference(&labels_a).cloned().collect();
396 let removed: Vec<String> = labels_a.difference(&labels_b).cloned().collect();
397 EffectChanges {
398 before: labels_a.into_iter().collect(),
399 after: labels_b.into_iter().collect(),
400 added,
401 removed,
402 }
403}
404
405fn render_type(t: &TypeExpr) -> String {
406 match t {
407 TypeExpr::Named { name, args } => {
408 if args.is_empty() { name.clone() }
409 else {
410 let parts: Vec<String> = args.iter().map(render_type).collect();
411 format!("{name}[{}]", parts.join(", "))
412 }
413 }
414 TypeExpr::Tuple { items } => {
415 let parts: Vec<String> = items.iter().map(render_type).collect();
416 format!("({})", parts.join(", "))
417 }
418 TypeExpr::Record { fields } => {
419 let parts: Vec<String> = fields.iter()
420 .map(|f| format!("{} :: {}", f.name, render_type(&f.ty))).collect();
421 format!("{{ {} }}", parts.join(", "))
422 }
423 TypeExpr::RecordWithSpreads { spreads, fields } => {
424 let mut parts: Vec<String> = spreads.iter().map(|s| format!("...{}", s)).collect();
425 parts.extend(fields.iter().map(|f| format!("{} :: {}", f.name, render_type(&f.ty))));
426 format!("{{ {} }}", parts.join(", "))
427 }
428 TypeExpr::Function { params, effects, effect_row_var, ret } => {
429 let parts: Vec<String> = params.iter().map(render_type).collect();
430 let eff = if effects.is_empty() && effect_row_var.is_none() { String::new() } else {
431 let mut names: Vec<String> = effects.iter().map(|e| e.name.clone()).collect();
432 if let Some(v) = effect_row_var { names.push(format!("| {}", v)); }
433 format!("[{}] ", names.join(", "))
434 };
435 format!("({}) -> {}{}", parts.join(", "), eff, render_type(ret))
436 }
437 TypeExpr::Union { variants } => variants.iter().map(|v| match &v.payload {
438 Some(p) => format!("{}({})", v.name, render_type(p)),
439 None => v.name.clone(),
440 }).collect::<Vec<_>>().join(" | "),
441 TypeExpr::Refined { base, binding, .. } => {
442 format!("{}{{{} | …}}", render_type(base), binding)
447 }
448 }
449}