1use crate::diff_report::{
9 AddRemove, BodyPatch, DiffReport, EffectChanges, Modified, Renamed,
10};
11use lex_ast::{stage_canonical_hash_hex, CExpr, Effect, EffectArg, FnDecl, Stage, TypeExpr};
12use std::collections::{BTreeMap, BTreeSet, HashMap};
13
14pub fn compute_diff(
20 a: &BTreeMap<String, FnDecl>,
21 b: &BTreeMap<String, FnDecl>,
22 body_patches: bool,
23) -> DiffReport {
24 let mut report = DiffReport::default();
25 let names_a: BTreeSet<&String> = a.keys().collect();
26 let names_b: BTreeSet<&String> = b.keys().collect();
27
28 let only_a: Vec<&String> = names_a.difference(&names_b).copied().collect();
29 let only_b: Vec<&String> = names_b.difference(&names_a).copied().collect();
30
31 let mut hash_to_bs: HashMap<String, Vec<&String>> = HashMap::new();
48 for &bn in &only_b {
49 hash_to_bs.entry(body_hash(&b[bn])).or_default().push(bn);
50 }
51 let mut renamed_pairs: Vec<(String, String)> = Vec::new();
52 let mut consumed_a: BTreeSet<String> = BTreeSet::new();
53 let mut consumed_b: BTreeSet<String> = BTreeSet::new();
54 for &an in &only_a {
55 let fa = &a[an];
56 let fa_norm_id = body_hash(fa);
57 let Some(candidates) = hash_to_bs.get(&fa_norm_id) else { continue };
58 let Some(&bn) = candidates.iter().find(|bn| !consumed_b.contains(**bn)) else { continue };
59 renamed_pairs.push((an.clone(), bn.clone()));
60 consumed_a.insert(an.clone());
61 consumed_b.insert(bn.clone());
62 }
63
64 for &n in &only_a {
65 if consumed_a.contains(n) { continue; }
66 let fd = &a[n];
67 report.removed.push(AddRemove {
68 name: n.clone(),
69 signature: render_signature(fd),
70 old_sig_id: lex_ast::sig_id(&Stage::FnDecl(fd.clone())),
71 });
72 }
73 for &n in &only_b {
74 if consumed_b.contains(n) { continue; }
75 let fd = &b[n];
76 report.added.push(AddRemove {
77 name: n.clone(),
78 signature: render_signature(fd),
79 old_sig_id: None,
80 });
81 }
82 for (an, bn) in &renamed_pairs {
83 let fa = &a[an];
84 let fd = &b[bn];
85 report.renamed.push(Renamed {
86 from: an.clone(),
87 to: bn.clone(),
88 signature: render_signature(fd),
89 old_sig_id: lex_ast::sig_id(&Stage::FnDecl(fa.clone())).unwrap_or_default(),
90 });
91 }
92
93 for n in names_a.intersection(&names_b) {
95 let fa = &a[*n];
96 let fb = &b[*n];
97 let sig_a = render_signature(fa);
98 let sig_b = render_signature(fb);
99 if body_hash(fa) == body_hash(fb) && sig_a == sig_b { continue; }
100
101 let patches = if body_patches {
102 let mut patches = Vec::new();
103 diff_expr(&fa.body, &fb.body, "body", &mut patches, 4);
104 patches
105 } else { Vec::new() };
106
107 let effect_changes = effect_diff(&fa.effects, &fb.effects);
108 report.modified.push(Modified {
109 name: (*n).clone(),
110 signature_before: sig_a.clone(),
111 signature_after: sig_b.clone(),
112 signature_changed: sig_a != sig_b,
113 effect_changes,
114 body_patches: patches,
115 old_sig_id: lex_ast::sig_id(&Stage::FnDecl(fa.clone())).unwrap_or_default(),
116 });
117 }
118 report
119}
120
121fn body_hash(fd: &FnDecl) -> String {
126 let mut anon = fd.clone();
127 anon.name = String::new();
128 let stage = Stage::FnDecl(anon);
129 stage_canonical_hash_hex(&stage)
130}
131
132fn diff_expr(a: &CExpr, b: &CExpr, path: &str, out: &mut Vec<BodyPatch>, depth: u32) {
136 if depth == 0 { return; }
137 let kind_a = node_kind(a);
138 let kind_b = node_kind(b);
139 if kind_a != kind_b {
140 out.push(BodyPatch {
141 op: "Replace".into(), node_path: path.into(),
142 from_kind: kind_a.into(), to_kind: kind_b.into(),
143 });
144 return;
145 }
146 match (a, b) {
148 (CExpr::Literal { value: la }, CExpr::Literal { value: lb }) => {
149 if la != lb {
150 out.push(BodyPatch {
151 op: "Replace".into(), node_path: path.into(),
152 from_kind: "Literal".into(), to_kind: "Literal".into(),
153 });
154 }
155 }
156 (CExpr::Var { name: na }, CExpr::Var { name: nb }) => {
157 if na != nb {
158 out.push(BodyPatch {
159 op: "Replace".into(), node_path: path.into(),
160 from_kind: format!("Var({na})"), to_kind: format!("Var({nb})"),
161 });
162 }
163 }
164 (CExpr::Call { callee: ca, args: aa },
165 CExpr::Call { callee: cb, args: ab }) => {
166 diff_expr(ca, cb, &format!("{path}.callee"), out, depth - 1);
167 diff_args(aa, ab, &format!("{path}.args"), out, depth);
168 }
169 (CExpr::Let { name: na, value: va, body: ba, .. },
170 CExpr::Let { name: nb, value: vb, body: bb, .. }) => {
171 if na != nb {
172 out.push(BodyPatch {
173 op: "Replace".into(),
174 node_path: format!("{path}.name"),
175 from_kind: format!("Let({na})"),
176 to_kind: format!("Let({nb})"),
177 });
178 }
179 diff_expr(va, vb, &format!("{path}.value"), out, depth - 1);
180 diff_expr(ba, bb, &format!("{path}.body"), out, depth - 1);
181 }
182 (CExpr::Match { scrutinee: sa, arms: ams },
183 CExpr::Match { scrutinee: sb, arms: bms }) => {
184 diff_expr(sa, sb, &format!("{path}.scrutinee"), out, depth - 1);
185 let n = ams.len().max(bms.len());
186 for i in 0..n {
187 let p = format!("{path}.arms[{i}]");
188 match (ams.get(i), bms.get(i)) {
189 (Some(a), Some(b)) =>
190 diff_expr(&a.body, &b.body, &p, out, depth - 1),
191 (Some(_), None) => out.push(BodyPatch {
192 op: "Deleted".into(), node_path: p,
193 from_kind: "MatchArm".into(), to_kind: "(removed)".into(),
194 }),
195 (None, Some(_)) => out.push(BodyPatch {
196 op: "Inserted".into(), node_path: p,
197 from_kind: "(none)".into(), to_kind: "MatchArm".into(),
198 }),
199 (None, None) => break,
200 }
201 }
202 }
203 (CExpr::Block { statements: sa, result: ra },
204 CExpr::Block { statements: sb, result: rb }) => {
205 diff_args(sa, sb, &format!("{path}.statements"), out, depth);
206 diff_expr(ra, rb, &format!("{path}.result"), out, depth - 1);
207 }
208 (CExpr::FieldAccess { value: va, field: fa },
209 CExpr::FieldAccess { value: vb, field: fb }) => {
210 diff_expr(va, vb, &format!("{path}.value"), out, depth - 1);
211 if fa != fb {
212 out.push(BodyPatch {
213 op: "Replace".into(), node_path: format!("{path}.field"),
214 from_kind: format!("Field({fa})"), to_kind: format!("Field({fb})"),
215 });
216 }
217 }
218 (CExpr::Lambda { body: ba, .. }, CExpr::Lambda { body: bb, .. }) => {
219 diff_expr(ba, bb, &format!("{path}.body"), out, depth - 1);
220 }
221 _ => {
225 out.push(BodyPatch {
226 op: "Replace".into(), node_path: path.into(),
227 from_kind: kind_a.into(), to_kind: kind_b.into(),
228 });
229 }
230 }
231}
232
233fn diff_args(a: &[CExpr], b: &[CExpr], path: &str, out: &mut Vec<BodyPatch>, depth: u32) {
234 let n = a.len().max(b.len());
235 for i in 0..n {
236 let p = format!("{path}[{i}]");
237 match (a.get(i), b.get(i)) {
238 (Some(x), Some(y)) => diff_expr(x, y, &p, out, depth - 1),
239 (Some(x), None) => out.push(BodyPatch {
240 op: "Deleted".into(), node_path: p,
241 from_kind: node_kind(x).into(), to_kind: "(removed)".into(),
242 }),
243 (None, Some(y)) => out.push(BodyPatch {
244 op: "Inserted".into(), node_path: p,
245 from_kind: "(none)".into(), to_kind: node_kind(y).into(),
246 }),
247 (None, None) => break,
248 }
249 }
250}
251
252fn node_kind(e: &CExpr) -> &'static str {
253 match e {
254 CExpr::Literal { .. } => "Literal",
255 CExpr::Var { .. } => "Var",
256 CExpr::Call { .. } => "Call",
257 CExpr::Let { .. } => "Let",
258 CExpr::Match { .. } => "Match",
259 CExpr::Block { .. } => "Block",
260 CExpr::Constructor { .. } => "Constructor",
261 CExpr::RecordLit { .. } => "RecordLit",
262 CExpr::TupleLit { .. } => "TupleLit",
263 CExpr::ListLit { .. } => "ListLit",
264 CExpr::FieldAccess { .. } => "FieldAccess",
265 CExpr::Lambda { .. } => "Lambda",
266 CExpr::BinOp { .. } => "BinOp",
267 CExpr::UnaryOp { .. } => "UnaryOp",
268 CExpr::Return { .. } => "Return",
269 }
270}
271
272pub fn render_signature(fd: &FnDecl) -> String {
273 let params: Vec<String> = fd.params.iter()
274 .map(|p| format!("{} :: {}", p.name, render_type(&p.ty))).collect();
275 let eff = if fd.effects.is_empty() { String::new() } else {
276 let labels: Vec<String> = fd.effects.iter().map(effect_label).collect();
277 format!("[{}] ", labels.join(", "))
278 };
279 format!("fn {}({}) -> {}{}", fd.name, params.join(", "),
280 eff, render_type(&fd.return_type))
281}
282
283pub fn effect_label(e: &Effect) -> String {
288 match &e.arg {
289 Some(EffectArg::Str { value }) => format!("{}({:?})", e.name, value),
290 Some(EffectArg::Int { value }) => format!("{}({})", e.name, value),
291 Some(EffectArg::Ident { value }) => format!("{}({})", e.name, value),
292 None => e.name.clone(),
293 }
294}
295
296fn effect_diff(a: &[Effect], b: &[Effect]) -> EffectChanges {
300 let labels_a: BTreeSet<String> = a.iter().map(effect_label).collect();
301 let labels_b: BTreeSet<String> = b.iter().map(effect_label).collect();
302 let added: Vec<String> = labels_b.difference(&labels_a).cloned().collect();
303 let removed: Vec<String> = labels_a.difference(&labels_b).cloned().collect();
304 EffectChanges {
305 before: labels_a.into_iter().collect(),
306 after: labels_b.into_iter().collect(),
307 added,
308 removed,
309 }
310}
311
312fn render_type(t: &TypeExpr) -> String {
313 match t {
314 TypeExpr::Named { name, args } => {
315 if args.is_empty() { name.clone() }
316 else {
317 let parts: Vec<String> = args.iter().map(render_type).collect();
318 format!("{name}[{}]", parts.join(", "))
319 }
320 }
321 TypeExpr::Tuple { items } => {
322 let parts: Vec<String> = items.iter().map(render_type).collect();
323 format!("({})", parts.join(", "))
324 }
325 TypeExpr::Record { fields } => {
326 let parts: Vec<String> = fields.iter()
327 .map(|f| format!("{} :: {}", f.name, render_type(&f.ty))).collect();
328 format!("{{ {} }}", parts.join(", "))
329 }
330 TypeExpr::RecordWithSpreads { spreads, fields } => {
331 let mut parts: Vec<String> = spreads.iter().map(|s| format!("...{}", s)).collect();
332 parts.extend(fields.iter().map(|f| format!("{} :: {}", f.name, render_type(&f.ty))));
333 format!("{{ {} }}", parts.join(", "))
334 }
335 TypeExpr::Function { params, effects, effect_row_var, ret } => {
336 let parts: Vec<String> = params.iter().map(render_type).collect();
337 let eff = if effects.is_empty() && effect_row_var.is_none() { String::new() } else {
338 let mut names: Vec<String> = effects.iter().map(|e| e.name.clone()).collect();
339 if let Some(v) = effect_row_var { names.push(format!("| {}", v)); }
340 format!("[{}] ", names.join(", "))
341 };
342 format!("({}) -> {}{}", parts.join(", "), eff, render_type(ret))
343 }
344 TypeExpr::Union { variants } => variants.iter().map(|v| match &v.payload {
345 Some(p) => format!("{}({})", v.name, render_type(p)),
346 None => v.name.clone(),
347 }).collect::<Vec<_>>().join(" | "),
348 TypeExpr::Refined { base, binding, .. } => {
349 format!("{}{{{} | …}}", render_type(base), binding)
354 }
355 }
356}