1use crate::op_log::OpLog;
8use crate::operation::{OpId, OperationKind, OperationRecord, SigId, StageId};
9use std::collections::{BTreeMap, BTreeSet};
10use std::io;
11
12#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
13#[serde(tag = "outcome", rename_all = "snake_case")]
14pub enum MergeOutcome {
15 Both { sig_id: SigId, stage_id: Option<StageId> },
17 Src { sig_id: SigId, stage_id: Option<StageId> },
19 Dst { sig_id: SigId, stage_id: Option<StageId> },
21 Conflict {
23 sig_id: SigId,
24 kind: ConflictKind,
25 base: Option<StageId>,
26 src: Option<StageId>,
27 dst: Option<StageId>,
28 },
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum ConflictKind {
34 ModifyModify,
35 ModifyDelete,
36 DeleteModify,
37 AddAdd,
38}
39
40#[derive(Debug)]
41pub struct MergeOutput {
42 pub lca: Option<OpId>,
43 pub outcomes: Vec<MergeOutcome>,
44}
45
46pub fn merge(
47 op_log: &OpLog,
48 src_head: Option<&OpId>,
49 dst_head: Option<&OpId>,
50) -> io::Result<MergeOutput> {
51 let lca = match (src_head, dst_head) {
52 (Some(s), Some(d)) => op_log.lca(s, d)?,
53 _ => None,
54 };
55 let src_ops = match src_head {
56 Some(h) => op_log.ops_since(h, lca.as_ref())?,
57 None => Vec::new(),
58 };
59 let dst_ops = match dst_head {
60 Some(h) => op_log.ops_since(h, lca.as_ref())?,
61 None => Vec::new(),
62 };
63
64 let src_by_sig = group_by_sig(&src_ops);
65 let dst_by_sig = group_by_sig(&dst_ops);
66
67 let lca_head: BTreeMap<SigId, StageId> = match lca.as_ref() {
68 Some(id) => head_at(op_log, id)?,
69 None => BTreeMap::new(),
70 };
71
72 let mut outcomes = Vec::new();
73 let sigs: BTreeSet<&SigId> = src_by_sig.keys().chain(dst_by_sig.keys()).collect();
74 for sig in sigs {
75 let s = src_by_sig.get(sig);
76 let d = dst_by_sig.get(sig);
77 let s_stage = s.map(|recs| latest_stage(sig, recs));
78 let d_stage = d.map(|recs| latest_stage(sig, recs));
79 match (s, d) {
80 (Some(s_recs), Some(d_recs)) => {
81 let s_last = s_recs.last().map(|r| r.op_id.as_str()).unwrap_or("");
82 let d_last = d_recs.last().map(|r| r.op_id.as_str()).unwrap_or("");
83 if s_last == d_last {
84 outcomes.push(MergeOutcome::Both {
85 sig_id: sig.clone(),
86 stage_id: s_stage.unwrap(),
87 });
88 } else {
89 let kind = classify(&s_stage.clone().unwrap(), &d_stage.clone().unwrap(), &lca_head, sig);
90 outcomes.push(MergeOutcome::Conflict {
91 sig_id: sig.clone(),
92 kind,
93 base: lca_head.get(sig).cloned(),
94 src: s_stage.unwrap(),
95 dst: d_stage.unwrap(),
96 });
97 }
98 }
99 (Some(_), None) => {
100 outcomes.push(MergeOutcome::Src {
101 sig_id: sig.clone(),
102 stage_id: s_stage.unwrap(),
103 });
104 }
105 (None, Some(_)) => {
106 outcomes.push(MergeOutcome::Dst {
107 sig_id: sig.clone(),
108 stage_id: d_stage.unwrap(),
109 });
110 }
111 (None, None) => unreachable!(),
112 }
113 }
114
115 Ok(MergeOutput { lca, outcomes })
116}
117
118fn group_by_sig(ops: &[OperationRecord]) -> BTreeMap<SigId, Vec<&OperationRecord>> {
119 let mut out: BTreeMap<SigId, Vec<&OperationRecord>> = BTreeMap::new();
120 for r in ops {
121 for sig in touched_sigs(&r.op.kind) {
122 out.entry(sig).or_default().push(r);
123 }
124 }
125 for v in out.values_mut() { v.reverse(); }
128 out
129}
130
131fn touched_sigs(k: &OperationKind) -> Vec<SigId> {
132 match k {
133 OperationKind::ModifyBody { sig_id, to_sig_id: Some(to), .. }
135 | OperationKind::ChangeEffectSig { sig_id, to_sig_id: Some(to), .. }
136 | OperationKind::ModifyType { sig_id, to_sig_id: Some(to), .. }
137 if to != sig_id =>
138 {
139 vec![sig_id.clone(), to.clone()]
140 }
141 OperationKind::AddFunction { sig_id, .. }
142 | OperationKind::RemoveFunction { sig_id, .. }
143 | OperationKind::ModifyBody { sig_id, .. }
144 | OperationKind::ChangeEffectSig { sig_id, .. }
145 | OperationKind::AddType { sig_id, .. }
146 | OperationKind::RemoveType { sig_id, .. }
147 | OperationKind::ModifyType { sig_id, .. }
148 | OperationKind::ReplaceMatchArm { sig_id, .. }
149 | OperationKind::RenameLocal { sig_id, .. }
150 | OperationKind::InlineLet { sig_id, .. }
151 | OperationKind::Promote { sig_id, .. } => vec![sig_id.clone()],
152 OperationKind::RenameSymbol { from, to, .. } => vec![from.clone(), to.clone()],
155 OperationKind::AddImport { .. }
156 | OperationKind::RemoveImport { .. }
157 | OperationKind::Merge { .. } => Vec::new(),
158 OperationKind::Candidate { .. } => Vec::new(),
163 }
164}
165
166fn latest_stage(sig: &SigId, recs: &[&OperationRecord]) -> Option<StageId> {
174 use crate::operation::{OperationKind as OK, StageTransition::*};
175 let mut current: Option<StageId> = None;
176 for r in recs {
177 if let OK::RenameSymbol { from, to, body_stage_id } = &r.op.kind {
179 if sig == from {
180 current = None;
182 } else if sig == to {
183 current = Some(body_stage_id.clone());
184 }
185 continue;
186 }
187 match &r.produces {
188 Create { stage_id, .. } => current = Some(stage_id.clone()),
189 Replace { to, .. } => current = Some(to.clone()),
190 Remove { .. } => current = None,
191 Rename { body_stage_id, .. } => current = Some(body_stage_id.clone()),
192 ImportOnly | Merge { .. } => {}
193 }
194 }
195 current
196}
197
198fn head_at(op_log: &OpLog, head: &OpId) -> io::Result<BTreeMap<SigId, StageId>> {
199 let mut map = BTreeMap::new();
200 for r in op_log.walk_forward(head, None)? {
201 use crate::operation::StageTransition::*;
202 match &r.produces {
203 Create { sig_id, stage_id } => { map.insert(sig_id.clone(), stage_id.clone()); }
204 Replace { sig_id, to, .. } => { map.insert(sig_id.clone(), to.clone()); }
205 Remove { sig_id, .. } => { map.remove(sig_id); }
206 Rename { from, to, body_stage_id } => {
207 map.remove(from);
208 map.insert(to.clone(), body_stage_id.clone());
209 }
210 ImportOnly => {}
211 Merge { entries } => {
212 for (sig, stage) in entries {
213 match stage {
214 Some(s) => { map.insert(sig.clone(), s.clone()); }
215 None => { map.remove(sig); }
216 }
217 }
218 }
219 }
220 }
221 Ok(map)
222}
223
224fn classify(
225 src: &Option<StageId>,
226 dst: &Option<StageId>,
227 base: &BTreeMap<SigId, StageId>,
228 sig: &SigId,
229) -> ConflictKind {
230 let in_base = base.contains_key(sig);
231 match (in_base, src.is_some(), dst.is_some()) {
232 (false, true, true) => ConflictKind::AddAdd,
233 (true, true, true) => ConflictKind::ModifyModify,
234 (true, true, false) => ConflictKind::ModifyDelete,
235 (true, false, true) => ConflictKind::DeleteModify,
236 other => {
242 debug_assert!(false, "classify: unreachable shape {other:?} for sig {sig}");
243 ConflictKind::ModifyModify
244 }
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use crate::apply::apply;
252 use crate::operation::{Operation, OperationKind, StageTransition};
253 use std::collections::BTreeSet;
254
255 fn fresh() -> (OpLog, tempfile::TempDir) {
256 let tmp = tempfile::tempdir().unwrap();
257 (OpLog::open(tmp.path()).unwrap(), tmp)
258 }
259
260 fn add_fn(log: &OpLog, parent: Option<&OpId>, sig: &str, stg: &str) -> OpId {
261 let op = Operation::new(
262 OperationKind::AddFunction {
263 sig_id: sig.into(),
264 stage_id: stg.into(),
265 effects: BTreeSet::new(),
266 budget_cost: None,
267 in_file: None,
268 },
269 parent.cloned().into_iter().collect::<Vec<_>>(),
270 );
271 let t = StageTransition::Create { sig_id: sig.into(), stage_id: stg.into() };
272 apply(log, parent, op, t).unwrap().op_id
273 }
274
275 fn modify_body(log: &OpLog, parent: &OpId, sig: &str, from: &str, to: &str) -> OpId {
276 let op = Operation::new(
277 OperationKind::ModifyBody {
278 sig_id: sig.into(),
279 from_stage_id: from.into(),
280 to_stage_id: to.into(),
281 from_budget: None,
282 to_budget: None,
283 to_sig_id: None,
284 },
285 [parent.clone()],
286 );
287 let t = StageTransition::Replace {
288 sig_id: sig.into(), from: from.into(), to: to.into(),
289 };
290 apply(log, Some(parent), op, t).unwrap().op_id
291 }
292
293 #[test]
294 fn disjoint_sigs_merge_cleanly() {
295 let (log, _tmp) = fresh();
296 let root = add_fn(&log, None, "shared", "s0");
297 let s_only = add_fn(&log, Some(&root), "src-only", "src1");
298 let d_only = add_fn(&log, Some(&root), "dst-only", "dst1");
299
300 let out = merge(&log, Some(&s_only), Some(&d_only)).unwrap();
301 assert_eq!(out.lca.as_ref(), Some(&root));
302 let kinds: Vec<&str> = out.outcomes.iter().map(|o| match o {
303 MergeOutcome::Src { .. } => "src",
304 MergeOutcome::Dst { .. } => "dst",
305 MergeOutcome::Both { .. } => "both",
306 MergeOutcome::Conflict { .. } => "conflict",
307 }).collect();
308 assert!(kinds.contains(&"src") && kinds.contains(&"dst"));
309 assert!(!kinds.contains(&"conflict"));
310 }
311
312 #[test]
313 fn same_sig_divergent_is_modify_modify_conflict() {
314 let (log, _tmp) = fresh();
315 let root = add_fn(&log, None, "fac", "s0");
316 let src = modify_body(&log, &root, "fac", "s0", "s-src");
317 let dst = modify_body(&log, &root, "fac", "s0", "s-dst");
318
319 let out = merge(&log, Some(&src), Some(&dst)).unwrap();
320 let conflict = out.outcomes.iter().find(|o| matches!(o, MergeOutcome::Conflict { .. }));
321 assert!(conflict.is_some());
322 if let Some(MergeOutcome::Conflict { kind, .. }) = conflict {
323 assert!(matches!(kind, ConflictKind::ModifyModify));
324 }
325 }
326
327 #[test]
328 fn independent_histories_no_lca() {
329 let (log, _tmp) = fresh();
330 let a = add_fn(&log, None, "a", "sa");
331 let b = add_fn(&log, None, "b", "sb");
332 let out = merge(&log, Some(&a), Some(&b)).unwrap();
333 assert!(out.lca.is_none());
334 }
335
336 #[test]
337 fn rename_on_src_with_concurrent_modify_on_dst_conflicts() {
338 let (log, _tmp) = fresh();
343 let root = add_fn(&log, None, "fac", "s0");
344
345 let rename_op = Operation::new(
347 OperationKind::RenameSymbol {
348 from: "fac".into(),
349 to: "fac2".into(),
350 body_stage_id: "s0".into(),
351 },
352 [root.clone()],
353 );
354 let rename_t = StageTransition::Rename {
355 from: "fac".into(), to: "fac2".into(),
356 body_stage_id: "s0".into(),
357 };
358 let src = apply(&log, Some(&root), rename_op, rename_t).unwrap().op_id;
359
360 let dst = modify_body(&log, &root, "fac", "s0", "s-dst");
362
363 let out = merge(&log, Some(&src), Some(&dst)).unwrap();
364
365 let fac_outcome = out.outcomes.iter().find(|o| match o {
368 MergeOutcome::Conflict { sig_id, .. }
369 | MergeOutcome::Src { sig_id, .. }
370 | MergeOutcome::Dst { sig_id, .. }
371 | MergeOutcome::Both { sig_id, .. } => sig_id == "fac",
372 });
373 assert!(matches!(fac_outcome, Some(MergeOutcome::Conflict { .. })),
374 "expected `fac` to be a conflict, got {fac_outcome:?}");
375 }
376}