Skip to main content

oxilean_parse/roundtrip/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::*;
6use crate::prettyprint::{print_decl, print_expr};
7use crate::{Lexer, Parser};
8
9/// Represents a corpus entry with metadata.
10#[allow(dead_code)]
11#[allow(missing_docs)]
12#[derive(Debug, Clone)]
13pub struct CorpusEntry {
14    /// Unique ID
15    pub id: usize,
16    /// Source text
17    pub source: String,
18    /// Nesting depth
19    pub depth: usize,
20    /// Token count (approximate)
21    pub token_count: usize,
22    /// Tags for categorisation
23    pub tags: Vec<String>,
24}
25impl CorpusEntry {
26    /// Create a new corpus entry.
27    #[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    /// Add a tag.
40    #[allow(dead_code)]
41    pub fn tag(mut self, t: &str) -> Self {
42        self.tags.push(t.to_string());
43        self
44    }
45}
46/// A round-trip test: parse then print, check idempotency.
47#[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/// Configuration for a round-trip property check.
84#[allow(dead_code)]
85#[allow(missing_docs)]
86#[derive(Debug, Clone)]
87pub struct RoundTripConfigExt {
88    /// Whether to normalise whitespace before comparison
89    pub normalise_whitespace: bool,
90    /// Whether to strip comments before comparison
91    pub strip_comments: bool,
92    /// Whether to ignore case differences
93    pub case_insensitive: bool,
94    /// Whether to allow extra trailing newlines
95    pub allow_trailing_newlines: bool,
96    /// Maximum allowed edit distance (0 means exact match required)
97    pub max_edit_distance: usize,
98}
99impl RoundTripConfigExt {
100    /// Create a strict config that requires exact match.
101    #[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    /// Create a lenient config that allows whitespace differences.
112    #[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/// Result of a round-trip test.
124#[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/// A simple text coverage tracker for round-trip tests.
178#[allow(dead_code)]
179#[allow(missing_docs)]
180#[derive(Debug, Default)]
181pub struct CoverageTracker {
182    /// Set of tested source strings
183    pub tested: std::collections::HashSet<String>,
184}
185impl CoverageTracker {
186    /// Create a new coverage tracker.
187    #[allow(dead_code)]
188    pub fn new() -> Self {
189        CoverageTracker {
190            tested: std::collections::HashSet::new(),
191        }
192    }
193    /// Mark a source as tested.
194    #[allow(dead_code)]
195    pub fn mark(&mut self, src: &str) {
196        self.tested.insert(src.to_string());
197    }
198    /// Returns the number of distinct sources tested.
199    #[allow(dead_code)]
200    pub fn count(&self) -> usize {
201        self.tested.len()
202    }
203}
204/// A snapshot of a round-trip run for regression testing.
205#[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}
237/// A collection of [`GoldenFile`] tests.
238pub struct GoldenTestSuite {
239    /// All test cases registered in this suite.
240    pub tests: Vec<GoldenFile>,
241}
242impl GoldenTestSuite {
243    /// Create an empty suite.
244    pub fn new() -> Self {
245        Self { tests: Vec::new() }
246    }
247    /// Add a test case.
248    pub fn add_test(&mut self, name: &str, source: &str, expected: &str) {
249        self.tests.push(GoldenFile::new(name, source, expected));
250    }
251    /// Run all tests.  Returns `(passed, failed)`.
252    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    /// References to tests that failed (i.e. `actual_ast != expected_ast`).
265    pub fn failing_tests(&self) -> Vec<&GoldenFile> {
266        self.tests.iter().filter(|t| t.diff().is_some()).collect()
267    }
268    /// Human-readable report for the whole suite.
269    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/// Statistics about a round-trip batch run.
283#[allow(dead_code)]
284#[allow(missing_docs)]
285#[derive(Debug, Clone, Default)]
286pub struct BatchRoundTripStats {
287    /// Total inputs tested
288    pub total: usize,
289    /// Number that passed round-trip
290    pub passed: usize,
291    /// Number that failed round-trip
292    pub failed: usize,
293    /// Number that panicked during parse
294    pub panicked: usize,
295    /// Sum of edit distances across all pairs
296    pub total_edit_distance: usize,
297}
298impl BatchRoundTripStats {
299    /// Create a new empty stats.
300    #[allow(dead_code)]
301    pub fn new() -> Self {
302        Self::default()
303    }
304    /// Record a pass.
305    #[allow(dead_code)]
306    pub fn record_pass(&mut self) {
307        self.total += 1;
308        self.passed += 1;
309    }
310    /// Record a failure with an edit distance.
311    #[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    /// Record a panic.
318    #[allow(dead_code)]
319    pub fn record_panic(&mut self) {
320        self.total += 1;
321        self.panicked += 1;
322    }
323    /// Returns the pass rate as a percentage.
324    #[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    /// Returns average edit distance for failures.
332    #[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/// A collection of round-trip tests.
341#[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/// A differencer for source texts, reporting character-level differences.
410#[allow(dead_code)]
411#[allow(missing_docs)]
412pub struct TextDiff<'a> {
413    /// Left text
414    pub left: &'a str,
415    /// Right text
416    pub right: &'a str,
417}
418impl<'a> TextDiff<'a> {
419    /// Create a new text diff.
420    #[allow(dead_code)]
421    pub fn new(left: &'a str, right: &'a str) -> Self {
422        TextDiff { left, right }
423    }
424    /// Returns true if left and right are identical.
425    #[allow(dead_code)]
426    pub fn is_identical(&self) -> bool {
427        self.left == self.right
428    }
429    /// Returns the number of differing characters.
430    #[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    /// Format a unified diff (simplified).
444    #[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/// A catalog of round-trip snapshots for golden testing.
466#[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/// A comparator that tracks the best matching prefix between two strings.
504#[allow(dead_code)]
505#[allow(missing_docs)]
506pub struct PrefixComparator<'a> {
507    /// Left string
508    left: &'a str,
509    /// Right string
510    right: &'a str,
511}
512impl<'a> PrefixComparator<'a> {
513    /// Create a new PrefixComparator.
514    #[allow(dead_code)]
515    pub fn new(left: &'a str, right: &'a str) -> Self {
516        PrefixComparator { left, right }
517    }
518    /// Returns the length of the longest common prefix.
519    #[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    /// Returns the common prefix as a string slice of `left`.
528    #[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/// A simple text-based tokeniser for round-trip validation.
542#[allow(dead_code)]
543#[allow(missing_docs)]
544#[derive(Debug, Clone)]
545pub struct TextToken {
546    /// Token kind label
547    pub kind: String,
548    /// Raw text of the token
549    pub text: String,
550    /// Byte offset in the source
551    pub offset: usize,
552}
553/// A generator for lambda expression test cases.
554#[allow(dead_code)]
555#[allow(missing_docs)]
556pub struct LambdaCorpusGenerator {
557    /// Number of variable names to cycle through
558    pub var_count: usize,
559}
560impl LambdaCorpusGenerator {
561    /// Create a new generator.
562    #[allow(dead_code)]
563    pub fn new(var_count: usize) -> Self {
564        LambdaCorpusGenerator { var_count }
565    }
566    /// Generate lambda expressions up to a given depth.
567    #[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/// A single golden-file test: parse `source` and compare the printed AST
586/// against `expected_ast`.
587///
588/// The `expected_ast` should match the output of [`print_decl`] exactly
589/// (modulo normalised whitespace).
590#[derive(Debug, Clone)]
591pub struct GoldenFile {
592    /// Descriptive test name.
593    pub name: String,
594    /// OxiLean source snippet to parse.
595    pub source: String,
596    /// Expected pretty-printed AST string.
597    pub expected_ast: String,
598    /// Populated by [`check`](Self::check) with the actual printed AST.
599    pub actual_ast: Option<String>,
600}
601impl GoldenFile {
602    /// Create a new golden-file test.
603    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    /// Parse `self.source`, store the result in `actual_ast`, and return
612    /// `true` iff the normalised printed form matches `expected_ast`.
613    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    /// Returns `None` when `actual_ast` matches `expected_ast`, otherwise a
630    /// short diff description.
631    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/// A round-trip batch processor.
653#[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/// Configuration for round-trip checking.
693#[derive(Clone, Debug)]
694pub struct RoundTripConfig {
695    /// Whether to collapse multiple whitespace characters into one before
696    /// comparing printed forms.
697    pub normalize_whitespace: bool,
698    /// Maximum number of differing characters before the result is reported
699    /// as a structure difference (0 = unlimited).
700    pub max_diff_chars: usize,
701    /// If `true`, span information is ignored when comparing AST nodes.
702    pub ignore_spans: bool,
703}
704impl RoundTripConfig {
705    /// Set the `normalize_whitespace` option.
706    pub fn with_normalize_whitespace(mut self, v: bool) -> Self {
707        self.normalize_whitespace = v;
708        self
709    }
710    /// Set the `max_diff_chars` option.
711    pub fn with_max_diff_chars(mut self, n: usize) -> Self {
712        self.max_diff_chars = n;
713        self
714    }
715    /// Set the `ignore_spans` option.
716    pub fn with_ignore_spans(mut self, v: bool) -> Self {
717        self.ignore_spans = v;
718        self
719    }
720    /// Normalise a string according to the current config.
721    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/// Represents a single editable region in the source for mutation testing.
730#[allow(dead_code)]
731#[allow(missing_docs)]
732#[derive(Debug, Clone)]
733pub struct EditRegion {
734    /// Byte offset start
735    pub start: usize,
736    /// Byte offset end
737    pub end: usize,
738    /// The kind of region
739    pub kind: EditRegionKind,
740}
741/// A registry of known good round-trip examples.
742#[allow(dead_code)]
743#[allow(missing_docs)]
744pub struct GoldenSet {
745    /// All golden examples
746    pub examples: Vec<GoldenExample>,
747}
748impl GoldenSet {
749    /// Create a new empty golden set.
750    #[allow(dead_code)]
751    pub fn new() -> Self {
752        GoldenSet {
753            examples: Vec::new(),
754        }
755    }
756    /// Add a golden example.
757    #[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    /// Returns the number of examples.
766    #[allow(dead_code)]
767    pub fn len(&self) -> usize {
768        self.examples.len()
769    }
770    /// Returns true if the set is empty.
771    #[allow(dead_code)]
772    pub fn is_empty(&self) -> bool {
773        self.examples.is_empty()
774    }
775}
776/// Stores a corpus of round-trip test inputs.
777#[allow(dead_code)]
778#[allow(missing_docs)]
779pub struct CorpusStore {
780    /// All entries in the corpus
781    pub entries: Vec<CorpusEntry>,
782    /// Next ID to assign
783    next_id: usize,
784}
785impl CorpusStore {
786    /// Create an empty corpus store.
787    #[allow(dead_code)]
788    pub fn new() -> Self {
789        CorpusStore {
790            entries: Vec::new(),
791            next_id: 0,
792        }
793    }
794    /// Add an entry from a source string.
795    #[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    /// Find entries by tag.
803    #[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    /// Returns the total number of entries.
811    #[allow(dead_code)]
812    pub fn len(&self) -> usize {
813        self.entries.len()
814    }
815    /// Returns true if the store is empty.
816    #[allow(dead_code)]
817    pub fn is_empty(&self) -> bool {
818        self.entries.is_empty()
819    }
820}
821/// A generator for forall expression test cases.
822#[allow(dead_code)]
823#[allow(missing_docs)]
824pub struct ForallCorpusGenerator {
825    /// Base predicates to use
826    pub predicates: Vec<String>,
827}
828impl ForallCorpusGenerator {
829    /// Create a new generator.
830    #[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    /// Generate forall expressions.
837    #[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/// Kind of editable region.
848#[allow(dead_code)]
849#[allow(missing_docs)]
850#[derive(Debug, Clone, PartialEq, Eq)]
851pub enum EditRegionKind {
852    /// A whitespace region
853    Whitespace,
854    /// An identifier region
855    Identifier,
856    /// A numeric literal
857    Number,
858    /// A string literal
859    StringLit,
860    /// An operator
861    Operator,
862    /// A keyword
863    Keyword,
864    /// A comment
865    Comment,
866    /// A parenthesised sub-expression
867    Parens,
868    /// A bracket group
869    Brackets,
870    /// A brace group
871    Braces,
872}
873/// A single golden round-trip example.
874#[allow(dead_code)]
875#[allow(missing_docs)]
876#[derive(Debug, Clone)]
877pub struct GoldenExample {
878    /// Input source
879    pub input: String,
880    /// Expected output after one round-trip
881    pub expected: String,
882    /// Human-readable name
883    pub name: String,
884}
885/// Stateful checker that accumulates success/failure counts.
886pub struct RoundTripChecker {
887    /// Configuration used for all checks performed by this checker.
888    pub config: RoundTripConfig,
889    /// Number of successful checks recorded via [`record_result`](Self::record_result).
890    pub success_count: u32,
891    /// Number of failed checks recorded via [`record_result`](Self::record_result).
892    pub failure_count: u32,
893}
894impl RoundTripChecker {
895    /// Create a new checker with the given configuration.
896    pub fn new(config: RoundTripConfig) -> Self {
897        Self {
898            config,
899            success_count: 0,
900            failure_count: 0,
901        }
902    }
903    /// Perform a round-trip check on a single expression source string.
904    ///
905    /// The check verifies that `print(parse(print(parse(src))))` equals
906    /// `print(parse(src))` — i.e. the printed form is idempotent.
907    /// This is a static method; use [`record_result`](Self::record_result)
908    /// to update the checker's counters.
909    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    /// Perform a round-trip check on a single declaration source string.
937    ///
938    /// Same idempotency check as [`check_expr`](Self::check_expr) but for
939    /// top-level declarations.
940    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    /// Record a result, incrementing the appropriate counter.
968    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    /// Success rate in the range `[0.0, 1.0]`.
976    ///
977    /// Returns `1.0` when no checks have been recorded.
978    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    /// Human-readable summary report.
987    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/// A simple normalization table for character replacements.
998#[allow(dead_code)]
999#[allow(missing_docs)]
1000pub struct NormTable {
1001    /// Entries mapping from to char
1002    pub entries: Vec<(char, char)>,
1003}
1004impl NormTable {
1005    /// Create a new empty normalization table.
1006    #[allow(dead_code)]
1007    pub fn new() -> Self {
1008        NormTable {
1009            entries: Vec::new(),
1010        }
1011    }
1012    /// Add a mapping.
1013    #[allow(dead_code)]
1014    pub fn add(&mut self, from: char, to: char) {
1015        self.entries.push((from, to));
1016    }
1017    /// Apply the table to a string.
1018    #[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/// A mutation applied to source text for round-trip testing.
1032#[allow(dead_code)]
1033#[allow(missing_docs)]
1034#[derive(Debug, Clone)]
1035pub struct SourceMutation {
1036    /// The original text before the mutation
1037    pub original: String,
1038    /// The mutated text
1039    pub mutated: String,
1040    /// Description of the mutation
1041    pub description: String,
1042}
1043/// A fuzzer for round-trip testing: generates random expression strings.
1044#[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/// Tracks round-trip statistics across a session.
1108#[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/// Outcome of a single round-trip check.
1156#[derive(Debug, Clone)]
1157pub enum RoundTripResult {
1158    /// `parse → print → parse → print` produced identical printed strings.
1159    Success,
1160    /// The pretty-printer could not produce a string for the first parse.
1161    PrettyPrintFailed(String),
1162    /// The re-parse of the pretty-printed string failed.
1163    ReparseError(String),
1164    /// Both parses succeeded but produced different printed strings.
1165    StructureDiffers {
1166        /// Printed form of the original parse.
1167        original: String,
1168        /// Printed form of the re-parse.
1169        reparsed: String,
1170    },
1171}
1172impl RoundTripResult {
1173    /// Returns `true` iff the result is [`Success`](Self::Success).
1174    pub fn is_success(&self) -> bool {
1175        matches!(self, Self::Success)
1176    }
1177    /// Human-readable description of the result.
1178    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/// A simple fuzzer that generates arithmetic expressions.
1190#[allow(dead_code)]
1191#[allow(missing_docs)]
1192pub struct ArithFuzzer {
1193    /// Depth limit for generated expressions
1194    pub max_depth: usize,
1195    /// Seed value for pseudo-randomness
1196    seed: u64,
1197}
1198impl ArithFuzzer {
1199    /// Create a new ArithFuzzer.
1200    #[allow(dead_code)]
1201    pub fn new(max_depth: usize) -> Self {
1202        ArithFuzzer {
1203            max_depth,
1204            seed: 42,
1205        }
1206    }
1207    /// Advance the seed.
1208    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    /// Generate a random integer in [0, n).
1215    fn rand_usize(&mut self, n: usize) -> usize {
1216        (self.next_u64() as usize) % n
1217    }
1218    /// Generate a random arithmetic expression.
1219    #[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    /// Generate a batch of expressions.
1232    #[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/// A property-based test harness for round-trip properties.
1238#[allow(dead_code)]
1239#[allow(missing_docs)]
1240pub struct PropertyTest {
1241    /// Name of this property test
1242    pub name: String,
1243    /// The number of iterations to run
1244    pub iterations: usize,
1245    /// Results collected
1246    pub results: Vec<(String, bool)>,
1247}
1248impl PropertyTest {
1249    /// Create a new property test with default iterations.
1250    #[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    /// Set the number of iterations.
1259    #[allow(dead_code)]
1260    pub fn with_iterations(mut self, n: usize) -> Self {
1261        self.iterations = n;
1262        self
1263    }
1264    /// Record a result.
1265    #[allow(dead_code)]
1266    pub fn record(&mut self, input: String, passed: bool) {
1267        self.results.push((input, passed));
1268    }
1269    /// Returns whether all recorded results passed.
1270    #[allow(dead_code)]
1271    pub fn all_passed(&self) -> bool {
1272        self.results.iter().all(|(_, ok)| *ok)
1273    }
1274    /// Returns a summary string.
1275    #[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/// A summary line for the round-trip report.
1283#[allow(dead_code)]
1284#[allow(missing_docs)]
1285#[derive(Debug, Clone)]
1286pub struct RoundTripSummaryLine {
1287    /// The input (possibly truncated)
1288    pub input_preview: String,
1289    /// Whether it passed
1290    pub passed: bool,
1291    /// The edit distance if it failed
1292    pub edit_distance: Option<usize>,
1293}