Skip to main content

airl_patch/
inverse.rs

1//! Patch inversion: generate an undo patch for any applied patch.
2//!
3//! Key property: apply(inverse(p), apply(p, module)) == module
4
5use airl_ir::module::Module;
6
7use crate::ops::{Patch, PatchOp};
8use crate::traverse;
9use crate::PatchError;
10
11/// Generate an inverse patch that undoes the given patch.
12///
13/// Must be called BEFORE the patch is applied, so we can capture the
14/// original state of modified nodes/functions.
15pub fn invert_patch(module: &Module, patch: &Patch) -> Result<Patch, PatchError> {
16    let mut inverse_ops = Vec::new();
17
18    for op in &patch.operations {
19        let inv = invert_op(module, op)?;
20        inverse_ops.push(inv);
21    }
22
23    // Reverse the order: if patch applies [A, B, C], inverse applies [C⁻¹, B⁻¹, A⁻¹]
24    inverse_ops.reverse();
25
26    Ok(Patch {
27        id: format!("inv_{}", patch.id),
28        parent_version: String::new(), // Will be the version after applying the original patch
29        operations: inverse_ops,
30        rationale: format!("Inverse of: {}", patch.rationale),
31        author: patch.author.clone(),
32    })
33}
34
35fn invert_op(module: &Module, op: &PatchOp) -> Result<PatchOp, PatchError> {
36    match op {
37        PatchOp::ReplaceNode { target, .. } => {
38            // Capture the original node before replacement
39            let func = traverse::find_containing_function(module, target).ok_or(
40                PatchError::NodeNotFound {
41                    node_id: target.to_string(),
42                },
43            )?;
44            let original_node =
45                traverse::find_node(&func.body, target).ok_or(PatchError::NodeNotFound {
46                    node_id: target.to_string(),
47                })?;
48
49            Ok(PatchOp::ReplaceNode {
50                // The inverse replaces the NEW node (which will have replacement's root ID)
51                // back with the original node. We use the original target ID since after
52                // applying the forward patch, the replacement's root sits where target was.
53                target: target.clone(),
54                replacement: original_node.clone(),
55            })
56        }
57
58        PatchOp::AddFunction { func } => Ok(PatchOp::RemoveFunction {
59            func_id: func.id.clone(),
60        }),
61
62        PatchOp::RemoveFunction { func_id } => {
63            // Capture the function being removed
64            let func = module
65                .find_function_by_id(func_id)
66                .ok_or(PatchError::FunctionNotFound {
67                    func_id: func_id.to_string(),
68                })?;
69            Ok(PatchOp::AddFunction { func: func.clone() })
70        }
71
72        PatchOp::AddImport { import } => Ok(PatchOp::RemoveImport {
73            import: import.clone(),
74        }),
75
76        PatchOp::RemoveImport { import } => Ok(PatchOp::AddImport {
77            import: import.clone(),
78        }),
79
80        PatchOp::RenameSymbol {
81            old_name,
82            new_name,
83            scope,
84        } => Ok(PatchOp::RenameSymbol {
85            old_name: new_name.clone(),
86            new_name: old_name.clone(),
87            scope: scope.clone(),
88        }),
89
90        PatchOp::AddEffect { func_id, effect } => Ok(PatchOp::RemoveEffect {
91            func_id: func_id.clone(),
92            effect: effect.clone(),
93        }),
94
95        PatchOp::RemoveEffect { func_id, effect } => Ok(PatchOp::AddEffect {
96            func_id: func_id.clone(),
97            effect: effect.clone(),
98        }),
99    }
100}