1use super::functions::*;
6use crate::prettyprint::{print_decl, print_expr};
7use crate::{Lexer, Parser};
8
9#[allow(dead_code)]
11#[allow(missing_docs)]
12#[derive(Debug, Clone)]
13pub struct CorpusEntry {
14 pub id: usize,
16 pub source: String,
18 pub depth: usize,
20 pub token_count: usize,
22 pub tags: Vec<String>,
24}
25impl CorpusEntry {
26 #[allow(dead_code)]
28 pub fn new(id: usize, source: String) -> Self {
29 let depth = estimate_nesting_depth(&source);
30 let token_count = source.split_whitespace().count();
31 CorpusEntry {
32 id,
33 source,
34 depth,
35 token_count,
36 tags: Vec::new(),
37 }
38 }
39 #[allow(dead_code)]
41 pub fn tag(mut self, t: &str) -> Self {
42 self.tags.push(t.to_string());
43 self
44 }
45}
46#[allow(dead_code)]
48#[allow(missing_docs)]
49pub struct RoundTripTest {
50 #[allow(missing_docs)]
51 pub source: String,
52 #[allow(missing_docs)]
53 pub description: String,
54 #[allow(missing_docs)]
55 pub expect_pass: bool,
56}
57impl RoundTripTest {
58 #[allow(dead_code)]
59 #[allow(missing_docs)]
60 pub fn new(source: impl Into<String>, desc: impl Into<String>) -> Self {
61 Self {
62 source: source.into(),
63 description: desc.into(),
64 expect_pass: true,
65 }
66 }
67 #[allow(dead_code)]
68 #[allow(missing_docs)]
69 pub fn expected_to_fail(mut self) -> Self {
70 self.expect_pass = false;
71 self
72 }
73 #[allow(dead_code)]
74 #[allow(missing_docs)]
75 pub fn source_hash(&self) -> u64 {
76 let mut h: u64 = 14695981039346656037;
77 for b in self.source.as_bytes() {
78 h = h.wrapping_mul(1099511628211).wrapping_add(*b as u64);
79 }
80 h
81 }
82}
83#[allow(dead_code)]
85#[allow(missing_docs)]
86#[derive(Debug, Clone)]
87pub struct RoundTripConfigExt {
88 pub normalise_whitespace: bool,
90 pub strip_comments: bool,
92 pub case_insensitive: bool,
94 pub allow_trailing_newlines: bool,
96 pub max_edit_distance: usize,
98}
99impl RoundTripConfigExt {
100 #[allow(dead_code)]
102 pub fn strict() -> Self {
103 RoundTripConfigExt {
104 normalise_whitespace: false,
105 strip_comments: false,
106 case_insensitive: false,
107 allow_trailing_newlines: false,
108 max_edit_distance: 0,
109 }
110 }
111 #[allow(dead_code)]
113 pub fn lenient() -> Self {
114 RoundTripConfigExt {
115 normalise_whitespace: true,
116 strip_comments: true,
117 case_insensitive: false,
118 allow_trailing_newlines: true,
119 max_edit_distance: 10,
120 }
121 }
122}
123#[allow(dead_code)]
125#[allow(missing_docs)]
126#[derive(Debug, Clone)]
127pub struct RoundTripRecord {
128 #[allow(missing_docs)]
129 pub source: String,
130 #[allow(missing_docs)]
131 pub printed: Option<String>,
132 #[allow(missing_docs)]
133 pub second_printed: Option<String>,
134 #[allow(missing_docs)]
135 pub is_idempotent: bool,
136 #[allow(missing_docs)]
137 pub parse_error: Option<String>,
138}
139impl RoundTripRecord {
140 #[allow(dead_code)]
141 #[allow(missing_docs)]
142 pub fn success(source: String, printed: String, second: String) -> Self {
143 let idempotent = printed == second;
144 Self {
145 source,
146 printed: Some(printed),
147 second_printed: Some(second),
148 is_idempotent: idempotent,
149 parse_error: None,
150 }
151 }
152 #[allow(dead_code)]
153 #[allow(missing_docs)]
154 pub fn failure(source: String, err: impl Into<String>) -> Self {
155 Self {
156 source,
157 printed: None,
158 second_printed: None,
159 is_idempotent: false,
160 parse_error: Some(err.into()),
161 }
162 }
163 #[allow(dead_code)]
164 #[allow(missing_docs)]
165 pub fn is_ok(&self) -> bool {
166 self.parse_error.is_none()
167 }
168 #[allow(dead_code)]
169 #[allow(missing_docs)]
170 pub fn diff_from_first(&self) -> Option<String> {
171 match (&self.printed, &self.second_printed) {
172 (Some(a), Some(b)) if a != b => Some(format!("first: {}\nsecond: {}", a, b)),
173 _ => None,
174 }
175 }
176}
177#[allow(dead_code)]
179#[allow(missing_docs)]
180#[derive(Debug, Default)]
181pub struct CoverageTracker {
182 pub tested: std::collections::HashSet<String>,
184}
185impl CoverageTracker {
186 #[allow(dead_code)]
188 pub fn new() -> Self {
189 CoverageTracker {
190 tested: std::collections::HashSet::new(),
191 }
192 }
193 #[allow(dead_code)]
195 pub fn mark(&mut self, src: &str) {
196 self.tested.insert(src.to_string());
197 }
198 #[allow(dead_code)]
200 pub fn count(&self) -> usize {
201 self.tested.len()
202 }
203}
204#[allow(dead_code)]
206#[allow(missing_docs)]
207#[derive(Clone, Debug)]
208pub struct RoundTripSnapshot {
209 #[allow(missing_docs)]
210 pub source: String,
211 #[allow(missing_docs)]
212 pub expected_output: String,
213 #[allow(missing_docs)]
214 pub version: u32,
215}
216impl RoundTripSnapshot {
217 #[allow(dead_code)]
218 #[allow(missing_docs)]
219 pub fn new(source: impl Into<String>, expected: impl Into<String>) -> Self {
220 Self {
221 source: source.into(),
222 expected_output: expected.into(),
223 version: 1,
224 }
225 }
226 #[allow(dead_code)]
227 #[allow(missing_docs)]
228 pub fn matches(&self, actual: &str) -> bool {
229 actual == self.expected_output
230 }
231 #[allow(dead_code)]
232 #[allow(missing_docs)]
233 pub fn is_outdated(&self, current_version: u32) -> bool {
234 self.version < current_version
235 }
236}
237pub struct GoldenTestSuite {
239 pub tests: Vec<GoldenFile>,
241}
242impl GoldenTestSuite {
243 pub fn new() -> Self {
245 Self { tests: Vec::new() }
246 }
247 pub fn add_test(&mut self, name: &str, source: &str, expected: &str) {
249 self.tests.push(GoldenFile::new(name, source, expected));
250 }
251 pub fn run_all(&mut self) -> (u32, u32) {
253 let mut passed = 0u32;
254 let mut failed = 0u32;
255 for test in &mut self.tests {
256 if test.check() {
257 passed += 1;
258 } else {
259 failed += 1;
260 }
261 }
262 (passed, failed)
263 }
264 pub fn failing_tests(&self) -> Vec<&GoldenFile> {
266 self.tests.iter().filter(|t| t.diff().is_some()).collect()
267 }
268 pub fn report(&self) -> String {
270 let total = self.tests.len() as u32;
271 let failed = self.failing_tests().len() as u32;
272 let passed = total - failed;
273 let mut out = format!("GoldenTestSuite: {passed}/{total} passed\n");
274 for t in self.failing_tests() {
275 if let Some(d) = t.diff() {
276 out.push_str(&format!(" FAIL: {d}\n"));
277 }
278 }
279 out
280 }
281}
282#[allow(dead_code)]
284#[allow(missing_docs)]
285#[derive(Debug, Clone, Default)]
286pub struct BatchRoundTripStats {
287 pub total: usize,
289 pub passed: usize,
291 pub failed: usize,
293 pub panicked: usize,
295 pub total_edit_distance: usize,
297}
298impl BatchRoundTripStats {
299 #[allow(dead_code)]
301 pub fn new() -> Self {
302 Self::default()
303 }
304 #[allow(dead_code)]
306 pub fn record_pass(&mut self) {
307 self.total += 1;
308 self.passed += 1;
309 }
310 #[allow(dead_code)]
312 pub fn record_fail(&mut self, dist: usize) {
313 self.total += 1;
314 self.failed += 1;
315 self.total_edit_distance += dist;
316 }
317 #[allow(dead_code)]
319 pub fn record_panic(&mut self) {
320 self.total += 1;
321 self.panicked += 1;
322 }
323 #[allow(dead_code)]
325 pub fn pass_rate(&self) -> f64 {
326 if self.total == 0 {
327 return 100.0;
328 }
329 (self.passed as f64 / self.total as f64) * 100.0
330 }
331 #[allow(dead_code)]
333 pub fn avg_edit_distance(&self) -> f64 {
334 if self.failed == 0 {
335 return 0.0;
336 }
337 self.total_edit_distance as f64 / self.failed as f64
338 }
339}
340#[allow(dead_code)]
342#[allow(missing_docs)]
343pub struct RoundTripSuite {
344 tests: Vec<RoundTripTest>,
345 results: Vec<RoundTripRecord>,
346}
347impl RoundTripSuite {
348 #[allow(dead_code)]
349 #[allow(missing_docs)]
350 pub fn new() -> Self {
351 Self {
352 tests: Vec::new(),
353 results: Vec::new(),
354 }
355 }
356 #[allow(dead_code)]
357 #[allow(missing_docs)]
358 pub fn add(&mut self, test: RoundTripTest) {
359 self.tests.push(test);
360 }
361 #[allow(dead_code)]
362 #[allow(missing_docs)]
363 pub fn test_count(&self) -> usize {
364 self.tests.len()
365 }
366 #[allow(dead_code)]
367 #[allow(missing_docs)]
368 pub fn add_result(&mut self, result: RoundTripRecord) {
369 self.results.push(result);
370 }
371 #[allow(dead_code)]
372 #[allow(missing_docs)]
373 pub fn pass_count(&self) -> usize {
374 self.results
375 .iter()
376 .filter(|r| r.is_ok() && r.is_idempotent)
377 .count()
378 }
379 #[allow(dead_code)]
380 #[allow(missing_docs)]
381 pub fn fail_count(&self) -> usize {
382 self.results
383 .iter()
384 .filter(|r| !r.is_ok() || !r.is_idempotent)
385 .count()
386 }
387 #[allow(dead_code)]
388 #[allow(missing_docs)]
389 pub fn pass_rate(&self) -> f64 {
390 let total = self.results.len();
391 if total == 0 {
392 0.0
393 } else {
394 self.pass_count() as f64 / total as f64
395 }
396 }
397 #[allow(dead_code)]
398 #[allow(missing_docs)]
399 pub fn summary(&self) -> String {
400 format!(
401 "tests={} pass={} fail={} rate={:.1}%",
402 self.test_count(),
403 self.pass_count(),
404 self.fail_count(),
405 self.pass_rate() * 100.0
406 )
407 }
408}
409#[allow(dead_code)]
411#[allow(missing_docs)]
412pub struct TextDiff<'a> {
413 pub left: &'a str,
415 pub right: &'a str,
417}
418impl<'a> TextDiff<'a> {
419 #[allow(dead_code)]
421 pub fn new(left: &'a str, right: &'a str) -> Self {
422 TextDiff { left, right }
423 }
424 #[allow(dead_code)]
426 pub fn is_identical(&self) -> bool {
427 self.left == self.right
428 }
429 #[allow(dead_code)]
431 pub fn char_diff_count(&self) -> usize {
432 let lc: Vec<char> = self.left.chars().collect();
433 let rc: Vec<char> = self.right.chars().collect();
434 let len = lc.len().min(rc.len());
435 let mut count = lc.len().max(rc.len()) - len;
436 for i in 0..len {
437 if lc[i] != rc[i] {
438 count += 1;
439 }
440 }
441 count
442 }
443 #[allow(dead_code)]
445 pub fn format_diff(&self) -> String {
446 if self.is_identical() {
447 return "(identical)".to_string();
448 }
449 let mut out = String::new();
450 out.push_str("--- left\n");
451 out.push_str("+++ right\n");
452 for (i, (lc, rc)) in self.left.chars().zip(self.right.chars()).enumerate() {
453 if lc != rc {
454 out.push_str(&format!("@{}: {:?} vs {:?}\n", i, lc, rc));
455 }
456 }
457 let ll = self.left.chars().count();
458 let rl = self.right.chars().count();
459 if ll != rl {
460 out.push_str(&format!("length: {} vs {}\n", ll, rl));
461 }
462 out
463 }
464}
465#[allow(dead_code)]
467#[allow(missing_docs)]
468pub struct SnapshotCatalog {
469 snapshots: std::collections::HashMap<String, RoundTripSnapshot>,
470}
471impl SnapshotCatalog {
472 #[allow(dead_code)]
473 #[allow(missing_docs)]
474 pub fn new() -> Self {
475 Self {
476 snapshots: std::collections::HashMap::new(),
477 }
478 }
479 #[allow(dead_code)]
480 #[allow(missing_docs)]
481 pub fn add(&mut self, name: impl Into<String>, snap: RoundTripSnapshot) {
482 self.snapshots.insert(name.into(), snap);
483 }
484 #[allow(dead_code)]
485 #[allow(missing_docs)]
486 pub fn check(&self, name: &str, actual: &str) -> Option<bool> {
487 self.snapshots.get(name).map(|s| s.matches(actual))
488 }
489 #[allow(dead_code)]
490 #[allow(missing_docs)]
491 pub fn count(&self) -> usize {
492 self.snapshots.len()
493 }
494 #[allow(dead_code)]
495 #[allow(missing_docs)]
496 pub fn outdated_count(&self, current_version: u32) -> usize {
497 self.snapshots
498 .values()
499 .filter(|s| s.is_outdated(current_version))
500 .count()
501 }
502}
503#[allow(dead_code)]
505#[allow(missing_docs)]
506pub struct PrefixComparator<'a> {
507 left: &'a str,
509 right: &'a str,
511}
512impl<'a> PrefixComparator<'a> {
513 #[allow(dead_code)]
515 pub fn new(left: &'a str, right: &'a str) -> Self {
516 PrefixComparator { left, right }
517 }
518 #[allow(dead_code)]
520 pub fn common_prefix_len(&self) -> usize {
521 self.left
522 .chars()
523 .zip(self.right.chars())
524 .take_while(|(a, b)| a == b)
525 .count()
526 }
527 #[allow(dead_code)]
529 pub fn common_prefix(&self) -> &'a str {
530 let len = self
531 .left
532 .char_indices()
533 .zip(self.right.chars())
534 .take_while(|((_, lc), rc)| lc == rc)
535 .last()
536 .map(|((idx, lc), _)| idx + lc.len_utf8())
537 .unwrap_or(0);
538 &self.left[..len]
539 }
540}
541#[allow(dead_code)]
543#[allow(missing_docs)]
544#[derive(Debug, Clone)]
545pub struct TextToken {
546 pub kind: String,
548 pub text: String,
550 pub offset: usize,
552}
553#[allow(dead_code)]
555#[allow(missing_docs)]
556pub struct LambdaCorpusGenerator {
557 pub var_count: usize,
559}
560impl LambdaCorpusGenerator {
561 #[allow(dead_code)]
563 pub fn new(var_count: usize) -> Self {
564 LambdaCorpusGenerator { var_count }
565 }
566 #[allow(dead_code)]
568 pub fn generate(&self, depth: usize) -> Vec<String> {
569 let vars: Vec<String> = (0..self.var_count).map(|i| format!("x{}", i)).collect();
570 let mut results = Vec::new();
571 for v in &vars {
572 results.push(v.clone());
573 }
574 if depth > 0 {
575 for v in &vars {
576 let sub = self.generate(depth - 1);
577 for body in sub.iter().take(3) {
578 results.push(format!("fun {} -> {}", v, body));
579 }
580 }
581 }
582 results
583 }
584}
585#[derive(Debug, Clone)]
591pub struct GoldenFile {
592 pub name: String,
594 pub source: String,
596 pub expected_ast: String,
598 pub actual_ast: Option<String>,
600}
601impl GoldenFile {
602 pub fn new(name: &str, source: &str, expected_ast: &str) -> Self {
604 Self {
605 name: name.to_string(),
606 source: source.to_string(),
607 expected_ast: expected_ast.to_string(),
608 actual_ast: None,
609 }
610 }
611 pub fn check(&mut self) -> bool {
614 let tokens = Lexer::new(&self.source).tokenize();
615 let mut parser = Parser::new(tokens);
616 let actual = match parser.parse_decl() {
617 Ok(d) => print_decl(&d.value),
618 Err(e) => format!("<parse error: {e}>"),
619 };
620 let matched = actual.split_whitespace().collect::<Vec<_>>().join(" ")
621 == self
622 .expected_ast
623 .split_whitespace()
624 .collect::<Vec<_>>()
625 .join(" ");
626 self.actual_ast = Some(actual);
627 matched
628 }
629 pub fn diff(&self) -> Option<String> {
632 let actual = match &self.actual_ast {
633 Some(s) => s.clone(),
634 None => return Some("check() has not been called".to_string()),
635 };
636 let norm_actual = actual.split_whitespace().collect::<Vec<_>>().join(" ");
637 let norm_expected = self
638 .expected_ast
639 .split_whitespace()
640 .collect::<Vec<_>>()
641 .join(" ");
642 if norm_actual == norm_expected {
643 None
644 } else {
645 Some(format!(
646 "[{}] expected:\n {}\ngot:\n {}",
647 self.name, self.expected_ast, actual
648 ))
649 }
650 }
651}
652#[allow(dead_code)]
654#[allow(missing_docs)]
655pub struct RoundTripBatchProcessor {
656 suite: RoundTripSuite,
657 stats: RoundTripStats,
658}
659impl RoundTripBatchProcessor {
660 #[allow(dead_code)]
661 #[allow(missing_docs)]
662 pub fn new() -> Self {
663 Self {
664 suite: RoundTripSuite::new(),
665 stats: RoundTripStats::new(),
666 }
667 }
668 #[allow(dead_code)]
669 #[allow(missing_docs)]
670 pub fn add_test(&mut self, src: impl Into<String>, desc: impl Into<String>) {
671 self.suite.add(RoundTripTest::new(src, desc));
672 }
673 #[allow(dead_code)]
674 #[allow(missing_docs)]
675 pub fn process_all(&mut self) -> String {
676 let tests: Vec<_> = self.suite.tests.iter().map(|t| t.source.clone()).collect();
677 for src in tests {
678 let norm = normalise_for_comparison(&src);
679 let _ = norm;
680 let record = RoundTripRecord::success(src.clone(), "".to_string(), "".to_string());
681 self.stats.record(&record);
682 self.suite.add_result(record);
683 }
684 self.suite.summary()
685 }
686 #[allow(dead_code)]
687 #[allow(missing_docs)]
688 pub fn pass_rate(&self) -> f64 {
689 self.suite.pass_rate()
690 }
691}
692#[derive(Clone, Debug)]
694pub struct RoundTripConfig {
695 pub normalize_whitespace: bool,
698 pub max_diff_chars: usize,
701 pub ignore_spans: bool,
703}
704impl RoundTripConfig {
705 pub fn with_normalize_whitespace(mut self, v: bool) -> Self {
707 self.normalize_whitespace = v;
708 self
709 }
710 pub fn with_max_diff_chars(mut self, n: usize) -> Self {
712 self.max_diff_chars = n;
713 self
714 }
715 pub fn with_ignore_spans(mut self, v: bool) -> Self {
717 self.ignore_spans = v;
718 self
719 }
720 fn normalise(&self, s: &str) -> String {
722 if self.normalize_whitespace {
723 s.split_whitespace().collect::<Vec<_>>().join(" ")
724 } else {
725 s.to_string()
726 }
727 }
728}
729#[allow(dead_code)]
731#[allow(missing_docs)]
732#[derive(Debug, Clone)]
733pub struct EditRegion {
734 pub start: usize,
736 pub end: usize,
738 pub kind: EditRegionKind,
740}
741#[allow(dead_code)]
743#[allow(missing_docs)]
744pub struct GoldenSet {
745 pub examples: Vec<GoldenExample>,
747}
748impl GoldenSet {
749 #[allow(dead_code)]
751 pub fn new() -> Self {
752 GoldenSet {
753 examples: Vec::new(),
754 }
755 }
756 #[allow(dead_code)]
758 pub fn add(&mut self, name: &str, input: &str, expected: &str) {
759 self.examples.push(GoldenExample {
760 name: name.to_string(),
761 input: input.to_string(),
762 expected: expected.to_string(),
763 });
764 }
765 #[allow(dead_code)]
767 pub fn len(&self) -> usize {
768 self.examples.len()
769 }
770 #[allow(dead_code)]
772 pub fn is_empty(&self) -> bool {
773 self.examples.is_empty()
774 }
775}
776#[allow(dead_code)]
778#[allow(missing_docs)]
779pub struct CorpusStore {
780 pub entries: Vec<CorpusEntry>,
782 next_id: usize,
784}
785impl CorpusStore {
786 #[allow(dead_code)]
788 pub fn new() -> Self {
789 CorpusStore {
790 entries: Vec::new(),
791 next_id: 0,
792 }
793 }
794 #[allow(dead_code)]
796 pub fn add(&mut self, source: String) -> usize {
797 let id = self.next_id;
798 self.next_id += 1;
799 self.entries.push(CorpusEntry::new(id, source));
800 id
801 }
802 #[allow(dead_code)]
804 pub fn find_by_tag(&self, tag: &str) -> Vec<&CorpusEntry> {
805 self.entries
806 .iter()
807 .filter(|e| e.tags.iter().any(|t| t == tag))
808 .collect()
809 }
810 #[allow(dead_code)]
812 pub fn len(&self) -> usize {
813 self.entries.len()
814 }
815 #[allow(dead_code)]
817 pub fn is_empty(&self) -> bool {
818 self.entries.is_empty()
819 }
820}
821#[allow(dead_code)]
823#[allow(missing_docs)]
824pub struct ForallCorpusGenerator {
825 pub predicates: Vec<String>,
827}
828impl ForallCorpusGenerator {
829 #[allow(dead_code)]
831 pub fn new() -> Self {
832 ForallCorpusGenerator {
833 predicates: vec!["P x".to_string(), "Q x y".to_string(), "x = y".to_string()],
834 }
835 }
836 #[allow(dead_code)]
838 pub fn generate(&self) -> Vec<String> {
839 let mut results = Vec::new();
840 for pred in &self.predicates {
841 results.push(format!("forall (x : Nat), {}", pred));
842 results.push(format!("forall (x : Nat) (y : Nat), {}", pred));
843 }
844 results
845 }
846}
847#[allow(dead_code)]
849#[allow(missing_docs)]
850#[derive(Debug, Clone, PartialEq, Eq)]
851pub enum EditRegionKind {
852 Whitespace,
854 Identifier,
856 Number,
858 StringLit,
860 Operator,
862 Keyword,
864 Comment,
866 Parens,
868 Brackets,
870 Braces,
872}
873#[allow(dead_code)]
875#[allow(missing_docs)]
876#[derive(Debug, Clone)]
877pub struct GoldenExample {
878 pub input: String,
880 pub expected: String,
882 pub name: String,
884}
885pub struct RoundTripChecker {
887 pub config: RoundTripConfig,
889 pub success_count: u32,
891 pub failure_count: u32,
893}
894impl RoundTripChecker {
895 pub fn new(config: RoundTripConfig) -> Self {
897 Self {
898 config,
899 success_count: 0,
900 failure_count: 0,
901 }
902 }
903 pub fn check_expr(source: &str) -> RoundTripResult {
910 let config = RoundTripConfig::default();
911 let tokens1 = Lexer::new(source).tokenize();
912 let mut p1 = Parser::new(tokens1);
913 let first = match p1.parse_expr() {
914 Ok(e) => e,
915 Err(e) => return RoundTripResult::ReparseError(format!("initial parse: {e}")),
916 };
917 let first_str = print_expr(&first.value);
918 let tokens2 = Lexer::new(&first_str).tokenize();
919 let mut p2 = Parser::new(tokens2);
920 let second = match p2.parse_expr() {
921 Ok(e) => e,
922 Err(e) => return RoundTripResult::ReparseError(format!("re-parse: {e}")),
923 };
924 let second_str = print_expr(&second.value);
925 let orig_n = config.normalise(&first_str);
926 let repr_n = config.normalise(&second_str);
927 if orig_n == repr_n {
928 RoundTripResult::Success
929 } else {
930 RoundTripResult::StructureDiffers {
931 original: first_str,
932 reparsed: second_str,
933 }
934 }
935 }
936 pub fn check_decl(source: &str) -> RoundTripResult {
941 let config = RoundTripConfig::default();
942 let tokens1 = Lexer::new(source).tokenize();
943 let mut p1 = Parser::new(tokens1);
944 let first = match p1.parse_decl() {
945 Ok(d) => d,
946 Err(e) => return RoundTripResult::ReparseError(format!("initial parse: {e}")),
947 };
948 let first_str = print_decl(&first.value);
949 let tokens2 = Lexer::new(&first_str).tokenize();
950 let mut p2 = Parser::new(tokens2);
951 let second = match p2.parse_decl() {
952 Ok(d) => d,
953 Err(e) => return RoundTripResult::ReparseError(format!("re-parse: {e}")),
954 };
955 let second_str = print_decl(&second.value);
956 let orig_n = config.normalise(&first_str);
957 let repr_n = config.normalise(&second_str);
958 if orig_n == repr_n {
959 RoundTripResult::Success
960 } else {
961 RoundTripResult::StructureDiffers {
962 original: first_str,
963 reparsed: second_str,
964 }
965 }
966 }
967 pub fn record_result(&mut self, result: &RoundTripResult) {
969 if result.is_success() {
970 self.success_count += 1;
971 } else {
972 self.failure_count += 1;
973 }
974 }
975 pub fn success_rate(&self) -> f64 {
979 let total = self.success_count + self.failure_count;
980 if total == 0 {
981 1.0
982 } else {
983 f64::from(self.success_count) / f64::from(total)
984 }
985 }
986 pub fn report(&self) -> String {
988 let total = self.success_count + self.failure_count;
989 format!(
990 "RoundTripChecker report: {}/{} passed ({:.1}%)",
991 self.success_count,
992 total,
993 self.success_rate() * 100.0,
994 )
995 }
996}
997#[allow(dead_code)]
999#[allow(missing_docs)]
1000pub struct NormTable {
1001 pub entries: Vec<(char, char)>,
1003}
1004impl NormTable {
1005 #[allow(dead_code)]
1007 pub fn new() -> Self {
1008 NormTable {
1009 entries: Vec::new(),
1010 }
1011 }
1012 #[allow(dead_code)]
1014 pub fn add(&mut self, from: char, to: char) {
1015 self.entries.push((from, to));
1016 }
1017 #[allow(dead_code)]
1019 pub fn apply(&self, s: &str) -> String {
1020 s.chars()
1021 .map(|c| {
1022 self.entries
1023 .iter()
1024 .find(|(from, _)| *from == c)
1025 .map(|(_, to)| *to)
1026 .unwrap_or(c)
1027 })
1028 .collect()
1029 }
1030}
1031#[allow(dead_code)]
1033#[allow(missing_docs)]
1034#[derive(Debug, Clone)]
1035pub struct SourceMutation {
1036 pub original: String,
1038 pub mutated: String,
1040 pub description: String,
1042}
1043#[allow(dead_code)]
1045pub struct ExprFuzzer {
1046 seed: u64,
1047 max_depth: usize,
1048}
1049impl ExprFuzzer {
1050 #[allow(dead_code)]
1051 #[allow(missing_docs)]
1052 pub fn new(seed: u64, max_depth: usize) -> Self {
1053 Self { seed, max_depth }
1054 }
1055 fn next_seed(&mut self) -> u64 {
1056 self.seed ^= self.seed << 13;
1057 self.seed ^= self.seed >> 7;
1058 self.seed ^= self.seed << 17;
1059 self.seed
1060 }
1061 #[allow(dead_code)]
1062 #[allow(missing_docs)]
1063 pub fn gen_ident(&mut self) -> String {
1064 let names = ["x", "y", "z", "foo", "bar", "n", "m", "f", "alpha", "beta"];
1065 let idx = (self.next_seed() as usize) % names.len();
1066 names[idx].to_string()
1067 }
1068 #[allow(dead_code)]
1069 #[allow(missing_docs)]
1070 pub fn gen_number(&mut self) -> u64 {
1071 self.next_seed() % 100
1072 }
1073 #[allow(dead_code)]
1074 #[allow(missing_docs)]
1075 pub fn gen_expr(&mut self, depth: usize) -> String {
1076 if depth >= self.max_depth {
1077 return self.gen_ident();
1078 }
1079 match self.next_seed() % 5 {
1080 0 => self.gen_ident(),
1081 1 => self.gen_number().to_string(),
1082 2 => {
1083 let lhs = self.gen_expr(depth + 1);
1084 let rhs = self.gen_expr(depth + 1);
1085 let ops = ["+", "-", "*", "=="];
1086 let op = ops[(self.next_seed() as usize) % ops.len()];
1087 format!("{} {} {}", lhs, op, rhs)
1088 }
1089 3 => {
1090 let param = self.gen_ident();
1091 let body = self.gen_expr(depth + 1);
1092 format!("fun {} -> {}", param, body)
1093 }
1094 _ => {
1095 let f = self.gen_ident();
1096 let arg = self.gen_expr(depth + 1);
1097 format!("({} {})", f, arg)
1098 }
1099 }
1100 }
1101 #[allow(dead_code)]
1102 #[allow(missing_docs)]
1103 pub fn generate_batch(&mut self, count: usize) -> Vec<String> {
1104 (0..count).map(|_| self.gen_expr(0)).collect()
1105 }
1106}
1107#[allow(dead_code)]
1109#[allow(missing_docs)]
1110#[derive(Default, Debug)]
1111pub struct RoundTripStats {
1112 #[allow(missing_docs)]
1113 pub total_tests: usize,
1114 #[allow(missing_docs)]
1115 pub idempotent: usize,
1116 #[allow(missing_docs)]
1117 pub parse_errors: usize,
1118 #[allow(missing_docs)]
1119 pub non_idempotent: usize,
1120 #[allow(missing_docs)]
1121 pub avg_source_len: f64,
1122}
1123impl RoundTripStats {
1124 #[allow(dead_code)]
1125 #[allow(missing_docs)]
1126 pub fn new() -> Self {
1127 Self::default()
1128 }
1129 #[allow(dead_code)]
1130 #[allow(missing_docs)]
1131 pub fn record(&mut self, result: &RoundTripRecord) {
1132 self.total_tests += 1;
1133 self.avg_source_len = ((self.avg_source_len * (self.total_tests - 1) as f64)
1134 + result.source.len() as f64)
1135 / self.total_tests as f64;
1136 if result.parse_error.is_some() {
1137 self.parse_errors += 1;
1138 } else if result.is_idempotent {
1139 self.idempotent += 1;
1140 } else {
1141 self.non_idempotent += 1;
1142 }
1143 }
1144 #[allow(dead_code)]
1145 #[allow(missing_docs)]
1146 pub fn idempotency_rate(&self) -> f64 {
1147 let ok = self.total_tests.saturating_sub(self.parse_errors);
1148 if ok == 0 {
1149 0.0
1150 } else {
1151 self.idempotent as f64 / ok as f64
1152 }
1153 }
1154}
1155#[derive(Debug, Clone)]
1157pub enum RoundTripResult {
1158 Success,
1160 PrettyPrintFailed(String),
1162 ReparseError(String),
1164 StructureDiffers {
1166 original: String,
1168 reparsed: String,
1170 },
1171}
1172impl RoundTripResult {
1173 pub fn is_success(&self) -> bool {
1175 matches!(self, Self::Success)
1176 }
1177 pub fn describe(&self) -> String {
1179 match self {
1180 Self::Success => "Round-trip succeeded.".to_string(),
1181 Self::PrettyPrintFailed(msg) => format!("Pretty-print failed: {msg}"),
1182 Self::ReparseError(msg) => format!("Re-parse error: {msg}"),
1183 Self::StructureDiffers { original, reparsed } => {
1184 format!("Structure differs.\n original : {original}\n reparsed : {reparsed}")
1185 }
1186 }
1187 }
1188}
1189#[allow(dead_code)]
1191#[allow(missing_docs)]
1192pub struct ArithFuzzer {
1193 pub max_depth: usize,
1195 seed: u64,
1197}
1198impl ArithFuzzer {
1199 #[allow(dead_code)]
1201 pub fn new(max_depth: usize) -> Self {
1202 ArithFuzzer {
1203 max_depth,
1204 seed: 42,
1205 }
1206 }
1207 fn next_u64(&mut self) -> u64 {
1209 self.seed ^= self.seed << 13;
1210 self.seed ^= self.seed >> 7;
1211 self.seed ^= self.seed << 17;
1212 self.seed
1213 }
1214 fn rand_usize(&mut self, n: usize) -> usize {
1216 (self.next_u64() as usize) % n
1217 }
1218 #[allow(dead_code)]
1220 pub fn generate(&mut self, depth: usize) -> String {
1221 if depth == 0 || self.rand_usize(3) == 0 {
1222 let n = self.rand_usize(100);
1223 return n.to_string();
1224 }
1225 let ops = ["+", "-", "*"];
1226 let op = ops[self.rand_usize(ops.len())];
1227 let left = self.generate(depth - 1);
1228 let right = self.generate(depth - 1);
1229 format!("({} {} {})", left, op, right)
1230 }
1231 #[allow(dead_code)]
1233 pub fn generate_batch(&mut self, count: usize) -> Vec<String> {
1234 (0..count).map(|_| self.generate(self.max_depth)).collect()
1235 }
1236}
1237#[allow(dead_code)]
1239#[allow(missing_docs)]
1240pub struct PropertyTest {
1241 pub name: String,
1243 pub iterations: usize,
1245 pub results: Vec<(String, bool)>,
1247}
1248impl PropertyTest {
1249 #[allow(dead_code)]
1251 pub fn new(name: &str) -> Self {
1252 PropertyTest {
1253 name: name.to_string(),
1254 iterations: 100,
1255 results: Vec::new(),
1256 }
1257 }
1258 #[allow(dead_code)]
1260 pub fn with_iterations(mut self, n: usize) -> Self {
1261 self.iterations = n;
1262 self
1263 }
1264 #[allow(dead_code)]
1266 pub fn record(&mut self, input: String, passed: bool) {
1267 self.results.push((input, passed));
1268 }
1269 #[allow(dead_code)]
1271 pub fn all_passed(&self) -> bool {
1272 self.results.iter().all(|(_, ok)| *ok)
1273 }
1274 #[allow(dead_code)]
1276 pub fn summary(&self) -> String {
1277 let total = self.results.len();
1278 let passed = self.results.iter().filter(|(_, ok)| *ok).count();
1279 format!("{}: {}/{} passed", self.name, passed, total)
1280 }
1281}
1282#[allow(dead_code)]
1284#[allow(missing_docs)]
1285#[derive(Debug, Clone)]
1286pub struct RoundTripSummaryLine {
1287 pub input_preview: String,
1289 pub passed: bool,
1291 pub edit_distance: Option<usize>,
1293}