1#![deny(missing_docs)]
27
28pub mod apply;
29pub mod impact;
30pub mod inverse;
31pub mod ops;
32pub mod traverse;
33pub mod validate;
34
35use thiserror::Error;
36
37pub use apply::{apply_patch, PatchResult};
39pub use impact::Impact;
40pub use inverse::invert_patch;
41pub use ops::{Patch, PatchOp};
42pub use validate::validate_patch;
43
44#[derive(Debug, Error)]
46pub enum PatchError {
47 #[error("node not found: {node_id}")]
49 NodeNotFound {
50 node_id: String,
52 },
53
54 #[error("function not found: {func_id}")]
56 FunctionNotFound {
57 func_id: String,
59 },
60
61 #[error("duplicate function name: {name}")]
63 DuplicateFunction {
64 name: String,
66 },
67
68 #[error("validation failed at operation {op_index}: {message}")]
70 ValidationFailed {
71 op_index: usize,
73 message: String,
75 },
76
77 #[error("type check failed after patch: {message}")]
79 TypeCheckFailed {
80 message: String,
82 },
83
84 #[error("version mismatch: expected {expected}, got {actual}")]
86 VersionMismatch {
87 expected: String,
89 actual: String,
91 },
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use airl_ir::effects::Effect;
98 use airl_ir::ids::{FuncId, NodeId};
99 use airl_ir::module::{FuncDef, Import, Module};
100 use airl_ir::node::{LiteralValue, Node};
101 use airl_ir::types::Type;
102
103 fn load_module(json: &str) -> Module {
104 serde_json::from_str(json).unwrap()
105 }
106
107 fn hello_module() -> Module {
108 load_module(
109 r#"{
110 "format_version": "0.1.0",
111 "module": {
112 "id": "mod_main", "name": "main",
113 "metadata": {"version": "1.0.0", "description": "", "author": "", "created_at": ""},
114 "imports": [{"module": "std::io", "items": ["println"]}],
115 "exports": [],
116 "types": [],
117 "functions": [{
118 "id": "f_main", "name": "main", "params": [], "returns": "Unit",
119 "effects": ["IO"],
120 "body": {"id": "n_1", "kind": "Call", "type": "Unit",
121 "target": "std::io::println",
122 "args": [{"id": "n_2", "kind": "Literal", "type": "String", "value": "hello world"}]
123 }
124 }]
125 }
126 }"#,
127 )
128 }
129
130 fn make_patch(ops: Vec<PatchOp>) -> Patch {
131 Patch {
132 id: "test_patch".to_string(),
133 parent_version: String::new(),
134 operations: ops,
135 rationale: "test".to_string(),
136 author: "test-agent".to_string(),
137 }
138 }
139
140 #[test]
143 fn test_replace_leaf_node() {
144 let module = hello_module();
145 let patch = make_patch(vec![PatchOp::ReplaceNode {
146 target: NodeId::new("n_2"),
147 replacement: Node::Literal {
148 id: NodeId::new("n_2"),
149 node_type: Type::String,
150 value: LiteralValue::Str("goodbye world".to_string()),
151 },
152 }]);
153
154 let result = apply_patch(&module, &patch).unwrap();
155
156 let func = result.new_module.find_function("main").unwrap();
158 match &func.body {
159 Node::Call { args, .. } => match &args[0] {
160 Node::Literal { value, .. } => {
161 assert_eq!(*value, LiteralValue::Str("goodbye world".to_string()));
162 }
163 other => panic!("Expected Literal, got: {other:?}"),
164 },
165 other => panic!("Expected Call, got: {other:?}"),
166 }
167 }
168
169 #[test]
170 fn test_replace_node_runs_correctly() {
171 let module = hello_module();
172 let patch = make_patch(vec![PatchOp::ReplaceNode {
173 target: NodeId::new("n_2"),
174 replacement: Node::Literal {
175 id: NodeId::new("n_2"),
176 node_type: Type::String,
177 value: LiteralValue::Str("patched!".to_string()),
178 },
179 }]);
180
181 let result = apply_patch(&module, &patch).unwrap();
182
183 let output = airl_interp::interpret(&result.new_module).unwrap();
185 assert_eq!(output.stdout, "patched!\n");
186 }
187
188 #[test]
189 fn test_replace_nonexistent_node_fails() {
190 let module = hello_module();
191 let patch = make_patch(vec![PatchOp::ReplaceNode {
192 target: NodeId::new("n_999"),
193 replacement: Node::Literal {
194 id: NodeId::new("n_999"),
195 node_type: Type::Unit,
196 value: LiteralValue::Unit,
197 },
198 }]);
199
200 let result = apply_patch(&module, &patch);
201 assert!(result.is_err());
202 }
203
204 #[test]
207 fn test_add_function() {
208 let module = hello_module();
209 let new_func = FuncDef {
210 id: FuncId::new("f_greet"),
211 name: "greet".to_string(),
212 params: vec![],
213 returns: Type::String,
214 effects: vec![Effect::Pure],
215 body: Node::Literal {
216 id: NodeId::new("g_1"),
217 node_type: Type::String,
218 value: LiteralValue::Str("hi".to_string()),
219 },
220 };
221
222 let patch = make_patch(vec![PatchOp::AddFunction { func: new_func }]);
223 let result = apply_patch(&module, &patch).unwrap();
224
225 assert_eq!(result.new_module.functions().len(), 2);
226 assert!(result.new_module.find_function("greet").is_some());
227 }
228
229 #[test]
230 fn test_remove_function() {
231 let module = hello_module();
232 let patch = make_patch(vec![PatchOp::RemoveFunction {
233 func_id: FuncId::new("f_main"),
234 }]);
235
236 let result = apply_patch(&module, &patch).unwrap();
237 assert_eq!(result.new_module.functions().len(), 0);
238 }
239
240 #[test]
241 fn test_add_duplicate_function_fails() {
242 let module = hello_module();
243 let dup_func = FuncDef {
244 id: FuncId::new("f_main2"),
245 name: "main".to_string(), params: vec![],
247 returns: Type::Unit,
248 effects: vec![],
249 body: Node::Literal {
250 id: NodeId::new("d_1"),
251 node_type: Type::Unit,
252 value: LiteralValue::Unit,
253 },
254 };
255
256 let patch = make_patch(vec![PatchOp::AddFunction { func: dup_func }]);
257 let result = apply_patch(&module, &patch);
258 assert!(result.is_err());
259 }
260
261 #[test]
264 fn test_rename_call_target() {
265 let module = hello_module();
266 let patch = make_patch(vec![PatchOp::RenameSymbol {
267 old_name: "std::io::println".to_string(),
268 new_name: "std::io::print".to_string(),
269 scope: None,
270 }]);
271
272 let result = apply_patch(&module, &patch).unwrap();
273 let func = result.new_module.find_function("main").unwrap();
274 match &func.body {
275 Node::Call { target, .. } => {
276 assert_eq!(target, "std::io::print");
277 }
278 other => panic!("Expected Call, got: {other:?}"),
279 }
280 }
281
282 #[test]
285 fn test_add_effect() {
286 let module = hello_module();
287 let patch = make_patch(vec![PatchOp::AddEffect {
288 func_id: FuncId::new("f_main"),
289 effect: Effect::Fail {
290 error_type: "IOError".to_string(),
291 },
292 }]);
293
294 let result = apply_patch(&module, &patch).unwrap();
295 let func = result.new_module.find_function("main").unwrap();
296 assert!(func.effects.contains(&Effect::Fail {
297 error_type: "IOError".to_string()
298 }));
299 assert!(func.effects.contains(&Effect::IO));
300 }
301
302 #[test]
303 fn test_remove_effect() {
304 let module = hello_module();
305 let patch = make_patch(vec![PatchOp::RemoveEffect {
306 func_id: FuncId::new("f_main"),
307 effect: Effect::IO,
308 }]);
309
310 let result = apply_patch(&module, &patch).unwrap();
311 let func = result.new_module.find_function("main").unwrap();
312 assert!(!func.effects.contains(&Effect::IO));
313 }
314
315 #[test]
318 fn test_add_import() {
319 let module = hello_module();
320 let patch = make_patch(vec![PatchOp::AddImport {
321 import: Import {
322 module: "std::math".to_string(),
323 items: vec!["abs".to_string()],
324 },
325 }]);
326
327 let result = apply_patch(&module, &patch).unwrap();
328 assert_eq!(result.new_module.module.imports.len(), 2);
329 }
330
331 #[test]
332 fn test_remove_import() {
333 let module = hello_module();
334 let patch = make_patch(vec![PatchOp::RemoveImport {
335 import: Import {
336 module: "std::io".to_string(),
337 items: vec!["println".to_string()],
338 },
339 }]);
340
341 let result = apply_patch(&module, &patch).unwrap();
342 assert_eq!(result.new_module.module.imports.len(), 0);
343 }
344
345 #[test]
348 fn test_inversion_replace_node() {
349 let module = hello_module();
350 let patch = make_patch(vec![PatchOp::ReplaceNode {
351 target: NodeId::new("n_2"),
352 replacement: Node::Literal {
353 id: NodeId::new("n_2"),
354 node_type: Type::String,
355 value: LiteralValue::Str("changed".to_string()),
356 },
357 }]);
358
359 let inverse = invert_patch(&module, &patch).unwrap();
361
362 let patched = apply_patch(&module, &patch).unwrap();
364
365 let restored = apply_patch(&patched.new_module, &inverse).unwrap();
367
368 assert_eq!(
370 serde_json::to_string(&module).unwrap(),
371 serde_json::to_string(&restored.new_module).unwrap(),
372 );
373 }
374
375 #[test]
376 fn test_inversion_add_remove_function() {
377 let module = hello_module();
378 let new_func = FuncDef {
379 id: FuncId::new("f_helper"),
380 name: "helper".to_string(),
381 params: vec![],
382 returns: Type::Unit,
383 effects: vec![Effect::Pure],
384 body: Node::Literal {
385 id: NodeId::new("h_1"),
386 node_type: Type::Unit,
387 value: LiteralValue::Unit,
388 },
389 };
390
391 let patch = make_patch(vec![PatchOp::AddFunction {
392 func: new_func.clone(),
393 }]);
394
395 let inverse = invert_patch(&module, &patch).unwrap();
396 let patched = apply_patch(&module, &patch).unwrap();
397 assert_eq!(patched.new_module.functions().len(), 2);
398
399 let restored = apply_patch(&patched.new_module, &inverse).unwrap();
400 assert_eq!(restored.new_module.functions().len(), 1);
401 }
402
403 #[test]
404 fn test_inversion_rename_symbol() {
405 let module = hello_module();
406 let patch = make_patch(vec![PatchOp::RenameSymbol {
407 old_name: "std::io::println".to_string(),
408 new_name: "std::io::print".to_string(),
409 scope: None,
410 }]);
411
412 let inverse = invert_patch(&module, &patch).unwrap();
413 let patched = apply_patch(&module, &patch).unwrap();
414 let restored = apply_patch(&patched.new_module, &inverse).unwrap();
415
416 assert_eq!(
417 serde_json::to_string(&module).unwrap(),
418 serde_json::to_string(&restored.new_module).unwrap(),
419 );
420 }
421
422 #[test]
425 fn test_impact_replace_node() {
426 let module = hello_module();
427 let patch = make_patch(vec![PatchOp::ReplaceNode {
428 target: NodeId::new("n_2"),
429 replacement: Node::Literal {
430 id: NodeId::new("n_2"),
431 node_type: Type::String,
432 value: LiteralValue::Str("x".to_string()),
433 },
434 }]);
435
436 let result = apply_patch(&module, &patch).unwrap();
437 assert!(result
438 .impact
439 .affected_functions
440 .contains(&FuncId::new("f_main")));
441 }
442
443 #[test]
446 fn test_version_changes_after_patch() {
447 let module = hello_module();
448 let v1 = airl_ir::version::VersionId::compute(&module).to_hex();
449
450 let patch = make_patch(vec![PatchOp::ReplaceNode {
451 target: NodeId::new("n_2"),
452 replacement: Node::Literal {
453 id: NodeId::new("n_2"),
454 node_type: Type::String,
455 value: LiteralValue::Str("different".to_string()),
456 },
457 }]);
458
459 let result = apply_patch(&module, &patch).unwrap();
460 assert_ne!(v1, result.new_version);
461 }
462
463 #[test]
466 fn test_multiple_ops_in_one_patch() {
467 let module = hello_module();
468 let patch = make_patch(vec![
469 PatchOp::ReplaceNode {
471 target: NodeId::new("n_2"),
472 replacement: Node::Literal {
473 id: NodeId::new("n_2"),
474 node_type: Type::String,
475 value: LiteralValue::Str("multi-patched".to_string()),
476 },
477 },
478 PatchOp::AddEffect {
480 func_id: FuncId::new("f_main"),
481 effect: Effect::Fail {
482 error_type: "E".to_string(),
483 },
484 },
485 ]);
486
487 let result = apply_patch(&module, &patch).unwrap();
488 let func = result.new_module.find_function("main").unwrap();
489 assert!(func.has_effect(&Effect::Fail {
490 error_type: "E".to_string()
491 }));
492
493 let output = airl_interp::interpret(&result.new_module).unwrap();
494 assert_eq!(output.stdout, "multi-patched\n");
495 }
496
497 #[test]
500 fn test_collect_node_ids() {
501 let module = hello_module();
502 let func = module.find_function("main").unwrap();
503 let ids = traverse::collect_node_ids(&func.body);
504 assert!(ids.contains(&NodeId::new("n_1")));
505 assert!(ids.contains(&NodeId::new("n_2")));
506 assert_eq!(ids.len(), 2);
507 }
508
509 #[test]
510 fn test_find_node() {
511 let module = hello_module();
512 let func = module.find_function("main").unwrap();
513 let node = traverse::find_node(&func.body, &NodeId::new("n_2"));
514 assert!(node.is_some());
515 match node.unwrap() {
516 Node::Literal { value, .. } => {
517 assert_eq!(*value, LiteralValue::Str("hello world".to_string()));
518 }
519 other => panic!("Expected Literal, got: {other:?}"),
520 }
521 }
522}