1use crate::diff_report::DiffReport;
16use crate::operation::{EffectSet, ModuleRef, OperationKind, SigId, StageId};
17use lex_ast::{sig_id, stage_id, Effect, Stage};
18use std::collections::{BTreeMap, BTreeSet};
19
20pub type ImportMap = BTreeMap<String, BTreeSet<ModuleRef>>;
21
22#[derive(Debug, thiserror::Error)]
23pub enum DiffMappingError {
24 #[error("diff mentions removed/modified name `{0}` but old_name_to_sig has no entry")]
25 MissingOldSigForName(String),
26 #[error("diff mentions added/renamed name `{0}` but new_stages has no matching stage")]
27 MissingNewStageForName(String),
28 #[error("sig `{0}` is in old_name_to_sig but not in old_head")]
29 MissingOldHeadForSig(SigId),
30 #[error("stage for `{0}` produces no sig_id (likely an Import that slipped through)")]
31 NoSigIdForStage(String),
32 #[error("stage for `{0}` produces no stage_id (likely an Import that slipped through)")]
33 NoStageIdForStage(String),
34}
35
36#[derive(Debug)]
37pub struct DiffInputs<'a> {
38 pub old_head: &'a BTreeMap<SigId, StageId>,
40 pub old_name_to_sig: &'a BTreeMap<String, SigId>,
43 pub old_effects: &'a BTreeMap<SigId, EffectSet>,
45 pub old_imports: &'a ImportMap,
47 pub new_stages: &'a [Stage],
49 pub new_imports: &'a ImportMap,
51 pub diff: &'a DiffReport,
53}
54
55pub fn diff_to_ops(inputs: DiffInputs<'_>) -> Result<Vec<OperationKind>, DiffMappingError> {
56 let mut out = Vec::new();
57 let new_by_name: BTreeMap<&str, &Stage> = inputs.new_stages.iter()
58 .filter_map(|s| {
59 let n = match s {
60 Stage::FnDecl(fd) => fd.name.as_str(),
61 Stage::TypeDecl(td) => td.name.as_str(),
62 Stage::Import(_) => return None,
63 };
64 Some((n, s))
65 })
66 .collect();
67
68 for (file, modules) in inputs.new_imports {
71 let old = inputs.old_imports.get(file).cloned().unwrap_or_default();
72 for m in modules.difference(&old) {
73 out.push(OperationKind::AddImport {
74 in_file: file.clone(),
75 module: m.clone(),
76 });
77 }
78 for m in old.difference(modules) {
79 out.push(OperationKind::RemoveImport {
80 in_file: file.clone(),
81 module: m.clone(),
82 });
83 }
84 }
85 for (file, old) in inputs.old_imports {
86 if !inputs.new_imports.contains_key(file) {
87 for m in old {
88 out.push(OperationKind::RemoveImport {
89 in_file: file.clone(),
90 module: m.clone(),
91 });
92 }
93 }
94 }
95
96 for r in &inputs.diff.removed {
98 let Some(sig) = inputs.old_name_to_sig.get(&r.name) else {
99 return Err(DiffMappingError::MissingOldSigForName(r.name.clone()));
100 };
101 let Some(last) = inputs.old_head.get(sig) else {
102 return Err(DiffMappingError::MissingOldHeadForSig(sig.clone()));
103 };
104 if r.signature.starts_with("type ") {
107 out.push(OperationKind::RemoveType {
108 sig_id: sig.clone(),
109 last_stage_id: last.clone(),
110 });
111 } else {
112 out.push(OperationKind::RemoveFunction {
113 sig_id: sig.clone(),
114 last_stage_id: last.clone(),
115 });
116 }
117 }
118
119 for a in &inputs.diff.added {
121 let Some(stage) = new_by_name.get(a.name.as_str()) else {
122 return Err(DiffMappingError::MissingNewStageForName(a.name.clone()));
123 };
124 let Some(sig) = sig_id(stage) else {
125 return Err(DiffMappingError::NoSigIdForStage(a.name.clone()));
126 };
127 let Some(stg) = stage_id(stage) else {
128 return Err(DiffMappingError::NoStageIdForStage(a.name.clone()));
129 };
130 match stage {
131 Stage::FnDecl(fd) => {
132 let effects = effect_set(&fd.effects);
133 out.push(OperationKind::AddFunction {
134 sig_id: sig, stage_id: stg, effects,
135 });
136 }
137 Stage::TypeDecl(_) => {
138 out.push(OperationKind::AddType { sig_id: sig, stage_id: stg });
139 }
140 Stage::Import(_) => unreachable!(),
141 }
142 }
143
144 for r in &inputs.diff.renamed {
146 let Some(from_sig) = inputs.old_name_to_sig.get(&r.from) else {
147 return Err(DiffMappingError::MissingOldSigForName(r.from.clone()));
148 };
149 let Some(stage) = new_by_name.get(r.to.as_str()) else {
150 return Err(DiffMappingError::MissingNewStageForName(r.to.clone()));
151 };
152 let Some(to_sig) = sig_id(stage) else {
153 return Err(DiffMappingError::NoSigIdForStage(r.to.clone()));
154 };
155 let Some(body_id) = stage_id(stage) else {
156 return Err(DiffMappingError::NoStageIdForStage(r.to.clone()));
157 };
158 out.push(OperationKind::RenameSymbol {
159 from: from_sig.clone(),
160 to: to_sig,
161 body_stage_id: body_id,
162 });
163 }
164
165 for m in &inputs.diff.modified {
167 let Some(sig) = inputs.old_name_to_sig.get(&m.name) else {
168 return Err(DiffMappingError::MissingOldSigForName(m.name.clone()));
169 };
170 let Some(from_id) = inputs.old_head.get(sig) else {
171 return Err(DiffMappingError::MissingOldHeadForSig(sig.clone()));
172 };
173 let Some(stage) = new_by_name.get(m.name.as_str()) else {
174 return Err(DiffMappingError::MissingNewStageForName(m.name.clone()));
175 };
176 let Some(to_id) = stage_id(stage) else {
177 return Err(DiffMappingError::NoStageIdForStage(m.name.clone()));
178 };
179 let effects_changed =
180 !m.effect_changes.added.is_empty() || !m.effect_changes.removed.is_empty();
181 match stage {
182 Stage::FnDecl(fd) if effects_changed => {
183 let from_effects = inputs.old_effects.get(sig).cloned().unwrap_or_default();
184 let to_effects = effect_set(&fd.effects);
185 out.push(OperationKind::ChangeEffectSig {
186 sig_id: sig.clone(),
187 from_stage_id: from_id.clone(),
188 to_stage_id: to_id,
189 from_effects,
190 to_effects,
191 });
192 }
193 Stage::FnDecl(_) => {
194 out.push(OperationKind::ModifyBody {
195 sig_id: sig.clone(),
196 from_stage_id: from_id.clone(),
197 to_stage_id: to_id,
198 });
199 }
200 Stage::TypeDecl(_) => {
201 out.push(OperationKind::ModifyType {
202 sig_id: sig.clone(),
203 from_stage_id: from_id.clone(),
204 to_stage_id: to_id,
205 });
206 }
207 Stage::Import(_) => unreachable!(),
208 }
209 }
210
211 Ok(out)
212}
213
214fn effect_set(effs: &[Effect]) -> EffectSet {
225 effs.iter().map(|e| e.name.clone()).collect()
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::diff_report::{DiffReport, EffectChanges, Modified, Renamed};
232
233 fn dr() -> DiffReport { DiffReport::default() }
234
235 #[test]
236 fn empty_diff_yields_no_ops() {
237 let head: BTreeMap<SigId, StageId> = BTreeMap::new();
238 let n2s: BTreeMap<String, SigId> = BTreeMap::new();
239 let eff: BTreeMap<SigId, EffectSet> = BTreeMap::new();
240 let oi: ImportMap = ImportMap::new();
241 let ni: ImportMap = ImportMap::new();
242 let stages: Vec<Stage> = Vec::new();
243 let d = dr();
244 let ops = diff_to_ops(DiffInputs {
245 old_head: &head,
246 old_name_to_sig: &n2s,
247 old_effects: &eff,
248 old_imports: &oi,
249 new_stages: &stages,
250 new_imports: &ni,
251 diff: &d,
252 }).expect("ok");
253 assert!(ops.is_empty());
254 }
255
256 #[test]
257 fn rename_emits_a_single_rename_op() {
258 let src = "fn parse_int(s :: Str) -> Int { 0 }";
260 let prog = lex_syntax::load_program_from_str(src).unwrap();
261 let stages = lex_ast::canonicalize_program(&prog);
262 let parse_int = stages.iter()
263 .find(|s| matches!(s, Stage::FnDecl(fd) if fd.name == "parse_int"))
264 .cloned().unwrap();
265 let to_sig = sig_id(&parse_int).unwrap();
266 let to_stage = stage_id(&parse_int).unwrap();
267
268 let mut head = BTreeMap::new();
269 head.insert("parse-old-sig".to_string(), to_stage.clone());
270 let mut n2s = BTreeMap::new();
271 n2s.insert("parse".to_string(), "parse-old-sig".to_string());
272
273 let mut diff = dr();
274 diff.renamed.push(Renamed {
275 from: "parse".into(),
276 to: "parse_int".into(),
277 signature: "fn parse_int(s :: Str) -> Int".into(),
278 });
279
280 let eff = BTreeMap::new();
281 let oi = ImportMap::new();
282 let ni = ImportMap::new();
283 let ops = diff_to_ops(DiffInputs {
284 old_head: &head,
285 old_name_to_sig: &n2s,
286 old_effects: &eff,
287 old_imports: &oi,
288 new_stages: &[parse_int],
289 new_imports: &ni,
290 diff: &diff,
291 }).expect("ok");
292 assert_eq!(ops.len(), 1);
293 match &ops[0] {
294 OperationKind::RenameSymbol { from, to, body_stage_id } => {
295 assert_eq!(from, "parse-old-sig");
296 assert_eq!(to, &to_sig);
297 assert_eq!(body_stage_id, &to_stage);
298 }
299 other => panic!("expected RenameSymbol, got {other:?}"),
300 }
301 }
302
303 #[test]
304 fn body_only_modify_emits_modify_body() {
305 let src = "fn fac(n :: Int) -> Int { 1 }";
306 let prog = lex_syntax::load_program_from_str(src).unwrap();
307 let stages = lex_ast::canonicalize_program(&prog);
308 let fac = stages.iter().find(|s| matches!(s, Stage::FnDecl(fd) if fd.name == "fac"))
309 .cloned().unwrap();
310 let sig = sig_id(&fac).unwrap();
311 let new_stg = stage_id(&fac).unwrap();
312
313 let mut head = BTreeMap::new();
314 head.insert(sig.clone(), "old-stage-id".to_string());
315 let mut n2s = BTreeMap::new();
316 n2s.insert("fac".to_string(), sig.clone());
317
318 let mut diff = dr();
319 diff.modified.push(Modified {
320 name: "fac".into(),
321 signature_before: "fn fac(n :: Int) -> Int".into(),
322 signature_after: "fn fac(n :: Int) -> Int".into(),
323 signature_changed: false,
324 effect_changes: EffectChanges::default(),
325 body_patches: Vec::new(),
326 });
327
328 let eff = BTreeMap::new();
329 let oi = ImportMap::new();
330 let ni = ImportMap::new();
331 let ops = diff_to_ops(DiffInputs {
332 old_head: &head, old_name_to_sig: &n2s, old_effects: &eff,
333 old_imports: &oi, new_stages: &[fac], new_imports: &ni, diff: &diff,
334 }).expect("ok");
335 assert_eq!(ops.len(), 1);
336 match &ops[0] {
337 OperationKind::ModifyBody { sig_id: s, from_stage_id, to_stage_id } => {
338 assert_eq!(s, &sig);
339 assert_eq!(from_stage_id, "old-stage-id");
340 assert_eq!(to_stage_id, &new_stg);
341 }
342 other => panic!("expected ModifyBody, got {other:?}"),
343 }
344 }
345
346 #[test]
347 fn import_added_emits_add_import() {
348 let mut new_imports = ImportMap::new();
349 new_imports.insert("main.lex".into(),
350 std::iter::once("std.io".to_string()).collect());
351 let head = BTreeMap::new();
352 let n2s = BTreeMap::new();
353 let eff = BTreeMap::new();
354 let oi = ImportMap::new();
355 let stages: Vec<Stage> = Vec::new();
356 let diff = dr();
357 let ops = diff_to_ops(DiffInputs {
358 old_head: &head, old_name_to_sig: &n2s, old_effects: &eff,
359 old_imports: &oi, new_stages: &stages, new_imports: &new_imports, diff: &diff,
360 }).expect("ok");
361 assert_eq!(ops.len(), 1);
362 match &ops[0] {
363 OperationKind::AddImport { in_file, module } => {
364 assert_eq!(in_file, "main.lex");
365 assert_eq!(module, "std.io");
366 }
367 other => panic!("expected AddImport, got {other:?}"),
368 }
369 }
370
371 #[test]
372 fn missing_old_sig_for_removed_name_errors() {
373 let head: BTreeMap<SigId, StageId> = BTreeMap::new();
374 let n2s: BTreeMap<String, SigId> = BTreeMap::new(); let eff: BTreeMap<SigId, EffectSet> = BTreeMap::new();
376 let oi = ImportMap::new();
377 let ni = ImportMap::new();
378 let stages: Vec<Stage> = Vec::new();
379 let mut diff = dr();
380 diff.removed.push(crate::diff_report::AddRemove {
381 name: "ghost".into(),
382 signature: "fn ghost() -> Int".into(),
383 });
384 let err = diff_to_ops(DiffInputs {
385 old_head: &head, old_name_to_sig: &n2s, old_effects: &eff,
386 old_imports: &oi, new_stages: &stages, new_imports: &ni, diff: &diff,
387 }).unwrap_err();
388 match err {
389 DiffMappingError::MissingOldSigForName(n) => assert_eq!(n, "ghost"),
390 other => panic!("expected MissingOldSigForName, got {other:?}"),
391 }
392 }
393}