1use std::collections::HashMap;
4
5use super::types::{
6 DeclKind, IrDeserializeResult, IrFormat, IrSerializeConfig, SerialDecl, SerializedIr,
7};
8
9pub const CURRENT_VERSION: u32 = 1;
13
14pub fn serialize_ir(module: &SerializedIr, cfg: &IrSerializeConfig) -> String {
18 match cfg.format {
19 IrFormat::Text => ir_to_text(module),
20 IrFormat::Json => ir_to_json(module),
21 IrFormat::Binary => ir_to_binary_text(module),
22 }
23}
24
25pub fn deserialize_ir(data: &str, format: IrFormat) -> IrDeserializeResult {
30 match format {
31 IrFormat::Text => match ir_from_text(data) {
32 Ok(ir) => {
33 if ir.version != CURRENT_VERSION {
34 IrDeserializeResult::VersionMismatch {
35 expected: CURRENT_VERSION,
36 found: ir.version,
37 }
38 } else {
39 IrDeserializeResult::Ok(ir)
40 }
41 }
42 Err(e) => IrDeserializeResult::ParseError(e),
43 },
44 IrFormat::Json => match ir_from_json(data) {
45 Ok(ir) => {
46 if ir.version != CURRENT_VERSION {
47 IrDeserializeResult::VersionMismatch {
48 expected: CURRENT_VERSION,
49 found: ir.version,
50 }
51 } else {
52 IrDeserializeResult::Ok(ir)
53 }
54 }
55 Err(e) => IrDeserializeResult::ParseError(e),
56 },
57 IrFormat::Binary => {
58 IrDeserializeResult::Unsupported("binary format is write-only in this version".into())
59 }
60 }
61}
62
63pub fn serialize_decl(decl: &SerialDecl, cfg: &IrSerializeConfig) -> String {
65 let mut out = String::new();
66 out.push_str(&format!("decl {} {}", decl.kind, decl.name));
67 if cfg.include_types {
68 out.push_str(&format!(" : {}", decl.type_));
69 }
70 if !decl.params.is_empty() {
71 out.push_str(&format!(" params=[{}]", decl.params.join(", ")));
72 }
73 if cfg.include_proofs {
74 if let Some(body) = &decl.body {
75 if cfg.pretty {
76 out.push_str(&format!("\n body: {}", body));
77 } else {
78 out.push_str(&format!(" body={}", body));
79 }
80 }
81 }
82 out
83}
84
85pub fn ir_to_json(module: &SerializedIr) -> String {
89 let mut out = String::new();
90 out.push_str("{\n");
91 out.push_str(&format!(" \"version\": {},\n", module.version));
92 out.push_str(&format!(
93 " \"module_name\": \"{}\",\n",
94 escape_json(&module.module_name)
95 ));
96 out.push_str(" \"metadata\": {\n");
98 let meta_entries: Vec<String> = module
99 .metadata
100 .iter()
101 .map(|(k, v)| format!(" \"{}\": \"{}\"", escape_json(k), escape_json(v)))
102 .collect();
103 out.push_str(&meta_entries.join(",\n"));
104 if !meta_entries.is_empty() {
105 out.push('\n');
106 }
107 out.push_str(" },\n");
108 out.push_str(" \"declarations\": [\n");
110 let decl_strs: Vec<String> = module
111 .declarations
112 .iter()
113 .map(|d| serialize_decl_json(d))
114 .collect();
115 out.push_str(&decl_strs.join(",\n"));
116 if !decl_strs.is_empty() {
117 out.push('\n');
118 }
119 out.push_str(" ]\n");
120 out.push('}');
121 out
122}
123
124fn serialize_decl_json(d: &SerialDecl) -> String {
125 let body_str = match &d.body {
126 Some(b) => format!("\"{}\"", escape_json(b)),
127 None => "null".to_string(),
128 };
129 let params_str: Vec<String> = d
130 .params
131 .iter()
132 .map(|p| format!("\"{}\"", escape_json(p)))
133 .collect();
134 format!(
135 " {{\"name\": \"{}\", \"kind\": \"{}\", \"type\": \"{}\", \"body\": {}, \"params\": [{}]}}",
136 escape_json(&d.name),
137 d.kind,
138 escape_json(&d.type_),
139 body_str,
140 params_str.join(", ")
141 )
142}
143
144pub fn ir_from_json(s: &str) -> Result<SerializedIr, String> {
146 let s = s.trim();
148 if !s.starts_with('{') {
149 return Err("expected JSON object".into());
150 }
151
152 let version = parse_json_u32(s, "version")
153 .ok_or_else(|| "missing or invalid 'version' field".to_string())?;
154 let module_name = parse_json_string(s, "module_name")
155 .ok_or_else(|| "missing 'module_name' field".to_string())?;
156 let metadata = parse_json_metadata(s);
157 let declarations = parse_json_declarations(s)?;
158
159 Ok(SerializedIr {
160 version,
161 module_name,
162 declarations,
163 metadata,
164 })
165}
166
167pub fn ir_to_text(module: &SerializedIr) -> String {
171 let mut out = String::new();
172 out.push_str(&format!("-- OxiLean IR version {}\n", module.version));
173 out.push_str(&format!("module {}\n", module.module_name));
174 for (k, v) in &module.metadata {
175 out.push_str(&format!("-- meta {} = {}\n", k, v));
176 }
177 out.push('\n');
178 for decl in &module.declarations {
179 out.push_str(&format!("{} {} : {}", decl.kind, decl.name, decl.type_));
180 if !decl.params.is_empty() {
181 out.push_str(&format!(" -- params: {}", decl.params.join(", ")));
182 }
183 out.push('\n');
184 if let Some(body) = &decl.body {
185 out.push_str(&format!(" := {}\n", body));
186 }
187 }
188 out
189}
190
191pub fn ir_from_text(s: &str) -> Result<SerializedIr, String> {
193 let mut version: Option<u32> = None;
194 let mut module_name: Option<String> = None;
195 let mut metadata: HashMap<String, String> = HashMap::new();
196 let mut declarations: Vec<SerialDecl> = Vec::new();
197
198 let mut pending_decl: Option<SerialDecl> = None;
199
200 for line in s.lines() {
201 let line = line.trim();
202 if line.is_empty() {
203 continue;
204 }
205 if let Some(rest) = line.strip_prefix("-- OxiLean IR version ") {
206 let v: u32 = rest
207 .trim()
208 .parse()
209 .map_err(|_| format!("invalid version '{}'", rest.trim()))?;
210 version = Some(v);
211 } else if let Some(rest) = line.strip_prefix("-- meta ") {
212 if let Some((k, v)) = rest.split_once(" = ") {
213 metadata.insert(k.trim().to_string(), v.trim().to_string());
214 }
215 } else if let Some(rest) = line.strip_prefix("module ") {
216 module_name = Some(rest.trim().to_string());
217 } else if let Some(rest) = line.strip_prefix(":= ") {
218 if let Some(ref mut d) = pending_decl {
219 let body = if let Some((b, _)) = rest.split_once(" --") {
221 b.trim().to_string()
222 } else {
223 rest.trim().to_string()
224 };
225 d.body = Some(body);
226 }
227 } else if line.starts_with("--") {
228 } else {
230 if let Some(d) = parse_text_decl_line(line) {
232 if let Some(prev) = pending_decl.take() {
233 declarations.push(prev);
234 }
235 pending_decl = Some(d);
236 }
237 }
238 }
239 if let Some(d) = pending_decl {
240 declarations.push(d);
241 }
242
243 Ok(SerializedIr {
244 version: version.unwrap_or(CURRENT_VERSION),
245 module_name: module_name.unwrap_or_default(),
246 declarations,
247 metadata,
248 })
249}
250
251pub fn merge_modules(mut a: SerializedIr, b: SerializedIr) -> Result<SerializedIr, String> {
256 let a_names: std::collections::HashSet<&str> =
257 a.declarations.iter().map(|d| d.name.as_str()).collect();
258 for d in &b.declarations {
259 if a_names.contains(d.name.as_str()) {
260 return Err(format!(
261 "name collision: '{}' exists in both modules",
262 d.name
263 ));
264 }
265 }
266 a.declarations.extend(b.declarations);
267 for (k, v) in b.metadata {
268 a.metadata.entry(k).or_insert(v);
269 }
270 Ok(a)
271}
272
273pub fn diff_modules(
279 old: &SerializedIr,
280 new: &SerializedIr,
281) -> (Vec<String>, Vec<String>, Vec<String>) {
282 let old_map: HashMap<&str, &SerialDecl> = old
283 .declarations
284 .iter()
285 .map(|d| (d.name.as_str(), d))
286 .collect();
287 let new_map: HashMap<&str, &SerialDecl> = new
288 .declarations
289 .iter()
290 .map(|d| (d.name.as_str(), d))
291 .collect();
292
293 let mut added: Vec<String> = Vec::new();
294 let mut removed: Vec<String> = Vec::new();
295 let mut modified: Vec<String> = Vec::new();
296
297 for (name, new_decl) in &new_map {
298 match old_map.get(name) {
299 None => added.push(name.to_string()),
300 Some(old_decl) => {
301 if old_decl != new_decl {
302 modified.push(name.to_string());
303 }
304 }
305 }
306 }
307 for name in old_map.keys() {
308 if !new_map.contains_key(name) {
309 removed.push(name.to_string());
310 }
311 }
312
313 added.sort();
314 removed.sort();
315 modified.sort();
316 (added, removed, modified)
317}
318
319fn escape_json(s: &str) -> String {
323 let mut out = String::with_capacity(s.len());
324 for c in s.chars() {
325 match c {
326 '"' => out.push_str("\\\""),
327 '\\' => out.push_str("\\\\"),
328 '\n' => out.push_str("\\n"),
329 '\r' => out.push_str("\\r"),
330 '\t' => out.push_str("\\t"),
331 other => out.push(other),
332 }
333 }
334 out
335}
336
337fn ir_to_binary_text(module: &SerializedIr) -> String {
341 let inner = ir_to_text(module);
344 let encoded: String = inner
345 .bytes()
346 .map(|b| format!("{:02x}", b))
347 .collect::<Vec<_>>()
348 .join("");
349 format!("OXI_BIN_V{}:{}", module.version, encoded)
350}
351
352fn parse_json_u32(json: &str, key: &str) -> Option<u32> {
354 let needle = format!("\"{}\":", key);
355 let start = json.find(&needle)? + needle.len();
356 let rest = json[start..].trim_start();
357 let end = rest
358 .find(|c: char| !c.is_ascii_digit())
359 .unwrap_or(rest.len());
360 rest[..end].parse().ok()
361}
362
363fn parse_json_string(json: &str, key: &str) -> Option<String> {
365 let needle = format!("\"{}\":", key);
366 let start = json.find(&needle)? + needle.len();
367 let rest = json[start..].trim_start();
368 if !rest.starts_with('"') {
369 return None;
370 }
371 Some(unescape_json_string(&rest[1..])?.0)
372}
373
374fn unescape_json_string(s: &str) -> Option<(String, usize)> {
377 let mut out = String::new();
378 let mut chars = s.char_indices();
379 loop {
380 let (i, c) = chars.next()?;
381 match c {
382 '"' => return Some((out, i + 1)),
383 '\\' => {
384 let (_, esc) = chars.next()?;
385 match esc {
386 '"' => out.push('"'),
387 '\\' => out.push('\\'),
388 'n' => out.push('\n'),
389 'r' => out.push('\r'),
390 't' => out.push('\t'),
391 other => {
392 out.push('\\');
393 out.push(other);
394 }
395 }
396 }
397 other => out.push(other),
398 }
399 }
400}
401
402fn parse_json_metadata(json: &str) -> HashMap<String, String> {
404 let mut map = HashMap::new();
405 let needle = "\"metadata\":";
406 let start = match json.find(needle) {
407 Some(p) => p + needle.len(),
408 None => return map,
409 };
410 let rest = json[start..].trim_start();
411 if !rest.starts_with('{') {
412 return map;
413 }
414 let inner = &rest[1..];
415 let mut pos = 0;
417 loop {
418 let chunk = inner[pos..].trim_start();
419 if chunk.starts_with('}') || chunk.is_empty() {
420 break;
421 }
422 if chunk.starts_with(',') {
423 pos += inner[pos..].find(',').map(|x| x + 1).unwrap_or(1);
424 continue;
425 }
426 if let Some(chunk_after_quote) = chunk.strip_prefix('"') {
427 match unescape_json_string(chunk_after_quote) {
429 Some((key, k_consumed)) => {
430 let after_key = chunk[1 + k_consumed..].trim_start();
431 if !after_key.starts_with(':') {
432 break;
433 }
434 let after_colon = after_key[1..].trim_start();
435 if let Some(after_colon_stripped) = after_colon.strip_prefix('"') {
436 match unescape_json_string(after_colon_stripped) {
437 Some((val, _)) => {
438 map.insert(key, val);
439 }
440 None => break,
441 }
442 }
443 match inner[pos..].find([',', '}']) {
445 Some(adv) => pos += adv + 1,
446 None => break,
447 }
448 }
449 None => break,
450 }
451 } else {
452 pos += 1;
453 }
454 }
455 map
456}
457
458fn parse_json_declarations(json: &str) -> Result<Vec<SerialDecl>, String> {
460 let mut decls = Vec::new();
461 let needle = "\"declarations\":";
462 let start = match json.find(needle) {
463 Some(p) => p + needle.len(),
464 None => return Ok(decls),
465 };
466 let rest = json[start..].trim_start();
467 if !rest.starts_with('[') {
468 return Ok(decls);
469 }
470 let mut depth = 0i32;
472 let mut obj_start: Option<usize> = None;
473 let chars: Vec<(usize, char)> = rest.char_indices().collect();
474 for &(i, c) in &chars {
475 match c {
476 '{' => {
477 depth += 1;
478 if depth == 1 {
479 obj_start = Some(i);
480 }
481 }
482 '}' => {
483 depth -= 1;
484 if depth == 0 {
485 if let Some(s) = obj_start.take() {
486 let obj = &rest[s..=i];
487 match parse_decl_json_obj(obj) {
488 Ok(d) => decls.push(d),
489 Err(e) => return Err(e),
490 }
491 }
492 }
493 }
494 ']' if depth == 0 => break,
495 _ => {}
496 }
497 }
498 Ok(decls)
499}
500
501fn parse_decl_json_obj(obj: &str) -> Result<SerialDecl, String> {
502 let name =
503 parse_json_string(obj, "name").ok_or_else(|| "declaration missing 'name'".to_string())?;
504 let kind_str =
505 parse_json_string(obj, "kind").ok_or_else(|| "declaration missing 'kind'".to_string())?;
506 let type_ = parse_json_string(obj, "type").unwrap_or_default();
507 let body = parse_json_nullable_string(obj, "body");
508 let params = parse_json_string_array(obj, "params");
509 let kind = parse_decl_kind(&kind_str)?;
510 Ok(SerialDecl {
511 name,
512 kind,
513 type_,
514 body,
515 params,
516 })
517}
518
519fn parse_json_nullable_string(json: &str, key: &str) -> Option<String> {
521 let needle = format!("\"{}\":", key);
522 let start = json.find(&needle)? + needle.len();
523 let rest = json[start..].trim_start();
524 if rest.starts_with("null") {
525 return None;
526 }
527 if let Some(rest_stripped) = rest.strip_prefix('"') {
528 return unescape_json_string(rest_stripped).map(|(s, _)| s);
529 }
530 None
531}
532
533fn parse_json_string_array(json: &str, key: &str) -> Vec<String> {
535 let needle = format!("\"{}\":", key);
536 let start = match json.find(&needle) {
537 Some(p) => p + needle.len(),
538 None => return Vec::new(),
539 };
540 let rest = json[start..].trim_start();
541 if !rest.starts_with('[') {
542 return Vec::new();
543 }
544 let inner = &rest[1..];
545 let mut result = Vec::new();
546 let mut pos = 0;
547 loop {
548 let chunk = inner[pos..].trim_start();
549 if chunk.starts_with(']') || chunk.is_empty() {
550 break;
551 }
552 if chunk.starts_with(',') {
553 pos += 1;
554 continue;
555 }
556 if let Some(chunk_after_quote) = chunk.strip_prefix('"') {
557 match unescape_json_string(chunk_after_quote) {
558 Some((s, consumed)) => {
559 result.push(s);
560 let skip = inner[pos..].trim_start().len();
562 let leading = inner[pos..].len() - skip;
563 pos += leading + 1 + consumed;
564 }
565 None => break,
566 }
567 } else {
568 pos += 1;
569 }
570 }
571 result
572}
573
574fn parse_text_decl_line(line: &str) -> Option<SerialDecl> {
576 let line = if let Some((main, _comment)) = line.split_once(" --") {
580 main.trim()
581 } else {
582 line
583 };
584
585 let (params_raw, rest) = if let Some((r, p)) = line.split_once(" -- params: ") {
586 (
587 p.split(',')
588 .map(|s| s.trim().to_string())
589 .collect::<Vec<_>>(),
590 r,
591 )
592 } else {
593 (Vec::new(), line)
594 };
595
596 let (kind_token, after_kind) = rest.split_once(' ')?;
598 let kind = parse_decl_kind(kind_token).ok()?;
599 let (name, type_) = after_kind.split_once(" : ")?;
601 Some(SerialDecl {
602 name: name.trim().to_string(),
603 kind,
604 type_: type_.trim().to_string(),
605 body: None,
606 params: params_raw,
607 })
608}
609
610fn parse_decl_kind(s: &str) -> Result<DeclKind, String> {
612 match s {
613 "def" => Ok(DeclKind::Def),
614 "theorem" => Ok(DeclKind::Theorem),
615 "axiom" => Ok(DeclKind::Axiom),
616 "inductive" => Ok(DeclKind::Inductive),
617 "constructor" => Ok(DeclKind::Constructor),
618 "recursor" => Ok(DeclKind::Recursor),
619 other => Err(format!("unknown DeclKind '{}'", other)),
620 }
621}
622
623#[cfg(test)]
626mod tests {
627 use super::super::types::{DeclKind, IrFormat, IrSerializeConfig, SerialDecl, SerializedIr};
628 use super::*;
629 use std::collections::HashMap;
630
631 fn sample_ir() -> SerializedIr {
632 let mut meta = HashMap::new();
633 meta.insert("compiler".to_string(), "oxilean-0.1.2".to_string());
634 SerializedIr {
635 version: CURRENT_VERSION,
636 module_name: "TestModule".to_string(),
637 declarations: vec![
638 SerialDecl {
639 name: "myDef".to_string(),
640 kind: DeclKind::Def,
641 type_: "Nat".to_string(),
642 body: Some("42".to_string()),
643 params: vec![],
644 },
645 SerialDecl {
646 name: "myTheorem".to_string(),
647 kind: DeclKind::Theorem,
648 type_: "a = a".to_string(),
649 body: Some("rfl".to_string()),
650 params: vec!["a".to_string()],
651 },
652 ],
653 metadata: meta,
654 }
655 }
656
657 #[test]
660 fn test_serialize_ir_text_roundtrip() {
661 let ir = sample_ir();
662 let cfg = IrSerializeConfig {
663 format: IrFormat::Text,
664 ..Default::default()
665 };
666 let text = serialize_ir(&ir, &cfg);
667 let result = deserialize_ir(&text, IrFormat::Text);
668 match result {
669 IrDeserializeResult::Ok(deserialized) => {
670 assert_eq!(deserialized.module_name, ir.module_name);
671 assert_eq!(deserialized.declarations.len(), ir.declarations.len());
672 }
673 other => panic!("expected Ok, got {:?}", other),
674 }
675 }
676
677 #[test]
678 fn test_serialize_ir_json_roundtrip() {
679 let ir = sample_ir();
680 let cfg = IrSerializeConfig {
681 format: IrFormat::Json,
682 ..Default::default()
683 };
684 let json = serialize_ir(&ir, &cfg);
685 let result = deserialize_ir(&json, IrFormat::Json);
686 match result {
687 IrDeserializeResult::Ok(deserialized) => {
688 assert_eq!(deserialized.module_name, ir.module_name);
689 assert_eq!(deserialized.version, CURRENT_VERSION);
690 }
691 other => panic!("expected Ok, got {:?}", other),
692 }
693 }
694
695 #[test]
696 fn test_deserialize_binary_returns_unsupported() {
697 let result = deserialize_ir("some data", IrFormat::Binary);
698 assert!(matches!(result, IrDeserializeResult::Unsupported(_)));
699 }
700
701 #[test]
702 fn test_deserialize_invalid_text_returns_parse_error() {
703 let result = deserialize_ir("-- OxiLean IR version abc\nmodule Foo", IrFormat::Text);
706 assert!(matches!(result, IrDeserializeResult::ParseError(_)));
707 }
708
709 #[test]
712 fn test_ir_to_json_contains_module_name() {
713 let ir = sample_ir();
714 let json = ir_to_json(&ir);
715 assert!(json.contains("TestModule"));
716 }
717
718 #[test]
719 fn test_ir_to_json_contains_version() {
720 let ir = sample_ir();
721 let json = ir_to_json(&ir);
722 assert!(json.contains(&CURRENT_VERSION.to_string()));
723 }
724
725 #[test]
726 fn test_ir_from_json_parses_version() {
727 let ir = sample_ir();
728 let json = ir_to_json(&ir);
729 let parsed = ir_from_json(&json).expect("parse failed");
730 assert_eq!(parsed.version, CURRENT_VERSION);
731 }
732
733 #[test]
734 fn test_ir_from_json_parses_declarations() {
735 let ir = sample_ir();
736 let json = ir_to_json(&ir);
737 let parsed = ir_from_json(&json).expect("parse failed");
738 assert_eq!(parsed.declarations.len(), 2);
739 }
740
741 #[test]
742 fn test_ir_from_json_decl_names() {
743 let ir = sample_ir();
744 let json = ir_to_json(&ir);
745 let parsed = ir_from_json(&json).expect("parse failed");
746 let names: Vec<_> = parsed
747 .declarations
748 .iter()
749 .map(|d| d.name.as_str())
750 .collect();
751 assert!(names.contains(&"myDef"));
752 assert!(names.contains(&"myTheorem"));
753 }
754
755 #[test]
756 fn test_ir_from_json_decl_kind() {
757 let ir = sample_ir();
758 let json = ir_to_json(&ir);
759 let parsed = ir_from_json(&json).expect("parse failed");
760 let def_decl = parsed
761 .declarations
762 .iter()
763 .find(|d| d.name == "myDef")
764 .expect("myDef not found");
765 assert_eq!(def_decl.kind, DeclKind::Def);
766 }
767
768 #[test]
769 fn test_ir_from_json_error_on_empty() {
770 assert!(ir_from_json("").is_err());
771 }
772
773 #[test]
776 fn test_ir_to_text_contains_module() {
777 let ir = sample_ir();
778 let text = ir_to_text(&ir);
779 assert!(text.contains("module TestModule"));
780 }
781
782 #[test]
783 fn test_ir_to_text_contains_def() {
784 let ir = sample_ir();
785 let text = ir_to_text(&ir);
786 assert!(text.contains("def myDef"));
787 }
788
789 #[test]
790 fn test_ir_from_text_roundtrip_module_name() {
791 let ir = sample_ir();
792 let text = ir_to_text(&ir);
793 let parsed = ir_from_text(&text).expect("parse failed");
794 assert_eq!(parsed.module_name, "TestModule");
795 }
796
797 #[test]
798 fn test_ir_from_text_roundtrip_decl_count() {
799 let ir = sample_ir();
800 let text = ir_to_text(&ir);
801 let parsed = ir_from_text(&text).expect("parse failed");
802 assert_eq!(parsed.declarations.len(), 2);
803 }
804
805 #[test]
806 fn test_ir_from_text_roundtrip_body() {
807 let ir = sample_ir();
808 let text = ir_to_text(&ir);
809 let parsed = ir_from_text(&text).expect("parse failed");
810 let d = parsed
811 .declarations
812 .iter()
813 .find(|d| d.name == "myDef")
814 .expect("myDef not found");
815 assert_eq!(d.body, Some("42".to_string()));
816 }
817
818 #[test]
821 fn test_serialize_decl_with_type() {
822 let d = SerialDecl {
823 name: "foo".to_string(),
824 kind: DeclKind::Axiom,
825 type_: "Prop".to_string(),
826 body: None,
827 params: vec![],
828 };
829 let cfg = IrSerializeConfig::default();
830 let s = serialize_decl(&d, &cfg);
831 assert!(s.contains("axiom foo"));
832 assert!(s.contains("Prop"));
833 }
834
835 #[test]
836 fn test_serialize_decl_no_type() {
837 let d = SerialDecl {
838 name: "bar".to_string(),
839 kind: DeclKind::Def,
840 type_: "Int".to_string(),
841 body: Some("0".to_string()),
842 params: vec![],
843 };
844 let cfg = IrSerializeConfig {
845 include_types: false,
846 ..Default::default()
847 };
848 let s = serialize_decl(&d, &cfg);
849 assert!(!s.contains("Int"));
850 }
851
852 #[test]
853 fn test_serialize_decl_no_proof() {
854 let d = SerialDecl {
855 name: "thm".to_string(),
856 kind: DeclKind::Theorem,
857 type_: "True".to_string(),
858 body: Some("trivial".to_string()),
859 params: vec![],
860 };
861 let cfg = IrSerializeConfig {
862 include_proofs: false,
863 ..Default::default()
864 };
865 let s = serialize_decl(&d, &cfg);
866 assert!(!s.contains("trivial"));
867 }
868
869 #[test]
872 fn test_merge_modules_success() {
873 let a = SerializedIr {
874 version: CURRENT_VERSION,
875 module_name: "A".to_string(),
876 declarations: vec![SerialDecl {
877 name: "x".to_string(),
878 kind: DeclKind::Def,
879 type_: "Nat".to_string(),
880 body: None,
881 params: vec![],
882 }],
883 metadata: HashMap::new(),
884 };
885 let b = SerializedIr {
886 version: CURRENT_VERSION,
887 module_name: "B".to_string(),
888 declarations: vec![SerialDecl {
889 name: "y".to_string(),
890 kind: DeclKind::Def,
891 type_: "Int".to_string(),
892 body: None,
893 params: vec![],
894 }],
895 metadata: HashMap::new(),
896 };
897 let merged = merge_modules(a, b).expect("merge failed");
898 assert_eq!(merged.declarations.len(), 2);
899 }
900
901 #[test]
902 fn test_merge_modules_collision() {
903 let decl = SerialDecl {
904 name: "collision".to_string(),
905 kind: DeclKind::Def,
906 type_: "Nat".to_string(),
907 body: None,
908 params: vec![],
909 };
910 let a = SerializedIr {
911 version: CURRENT_VERSION,
912 module_name: "A".to_string(),
913 declarations: vec![decl.clone()],
914 metadata: HashMap::new(),
915 };
916 let b = SerializedIr {
917 version: CURRENT_VERSION,
918 module_name: "B".to_string(),
919 declarations: vec![decl],
920 metadata: HashMap::new(),
921 };
922 assert!(merge_modules(a, b).is_err());
923 }
924
925 #[test]
928 fn test_diff_added() {
929 let old = SerializedIr {
930 version: CURRENT_VERSION,
931 module_name: "M".to_string(),
932 declarations: vec![],
933 metadata: HashMap::new(),
934 };
935 let new = SerializedIr {
936 version: CURRENT_VERSION,
937 module_name: "M".to_string(),
938 declarations: vec![SerialDecl {
939 name: "newDecl".to_string(),
940 kind: DeclKind::Def,
941 type_: "Nat".to_string(),
942 body: None,
943 params: vec![],
944 }],
945 metadata: HashMap::new(),
946 };
947 let (added, removed, modified) = diff_modules(&old, &new);
948 assert_eq!(added, vec!["newDecl"]);
949 assert!(removed.is_empty());
950 assert!(modified.is_empty());
951 }
952
953 #[test]
954 fn test_diff_removed() {
955 let decl = SerialDecl {
956 name: "gone".to_string(),
957 kind: DeclKind::Axiom,
958 type_: "False".to_string(),
959 body: None,
960 params: vec![],
961 };
962 let old = SerializedIr {
963 version: CURRENT_VERSION,
964 module_name: "M".to_string(),
965 declarations: vec![decl],
966 metadata: HashMap::new(),
967 };
968 let new = SerializedIr {
969 version: CURRENT_VERSION,
970 module_name: "M".to_string(),
971 declarations: vec![],
972 metadata: HashMap::new(),
973 };
974 let (added, removed, modified) = diff_modules(&old, &new);
975 assert!(added.is_empty());
976 assert_eq!(removed, vec!["gone"]);
977 assert!(modified.is_empty());
978 }
979
980 #[test]
981 fn test_diff_modified() {
982 let d_old = SerialDecl {
983 name: "f".to_string(),
984 kind: DeclKind::Def,
985 type_: "Nat".to_string(),
986 body: Some("1".to_string()),
987 params: vec![],
988 };
989 let d_new = SerialDecl {
990 name: "f".to_string(),
991 kind: DeclKind::Def,
992 type_: "Nat".to_string(),
993 body: Some("2".to_string()),
994 params: vec![],
995 };
996 let old = SerializedIr {
997 version: CURRENT_VERSION,
998 module_name: "M".to_string(),
999 declarations: vec![d_old],
1000 metadata: HashMap::new(),
1001 };
1002 let new = SerializedIr {
1003 version: CURRENT_VERSION,
1004 module_name: "M".to_string(),
1005 declarations: vec![d_new],
1006 metadata: HashMap::new(),
1007 };
1008 let (added, removed, modified) = diff_modules(&old, &new);
1009 assert!(added.is_empty());
1010 assert!(removed.is_empty());
1011 assert_eq!(modified, vec!["f"]);
1012 }
1013
1014 #[test]
1015 fn test_diff_no_change() {
1016 let ir = sample_ir();
1017 let (added, removed, modified) = diff_modules(&ir, &ir);
1018 assert!(added.is_empty());
1019 assert!(removed.is_empty());
1020 assert!(modified.is_empty());
1021 }
1022
1023 #[test]
1026 fn test_version_mismatch_in_text() {
1027 let text = "-- OxiLean IR version 99\nmodule Foo\n";
1028 let result = deserialize_ir(text, IrFormat::Text);
1029 assert!(matches!(
1030 result,
1031 IrDeserializeResult::VersionMismatch {
1032 expected: _,
1033 found: 99
1034 }
1035 ));
1036 }
1037
1038 #[test]
1039 fn test_version_mismatch_in_json() {
1040 let json =
1041 "{\"version\": 99, \"module_name\": \"Foo\", \"metadata\": {}, \"declarations\": []}";
1042 let result = deserialize_ir(json, IrFormat::Json);
1043 assert!(matches!(
1044 result,
1045 IrDeserializeResult::VersionMismatch {
1046 expected: _,
1047 found: 99
1048 }
1049 ));
1050 }
1051}