|
| 1 | +(* SPDX-License-Identifier: MPL-2.0 *) |
| 2 | +(* Owner: Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> *) |
| 3 | +(* |
| 4 | + * test_roundtrip.ml — TG-4 pretty-print/parse round-trip property test. |
| 5 | + * |
| 6 | + * Obligation (PROOF-NARRATIVE.md §3 TG-4): |
| 7 | + * |
| 8 | + * For every closed value e in the core Tangle language, |
| 9 | + * parse(pretty(e)) = e |
| 10 | + * |
| 11 | + * Strategy: |
| 12 | + * 1. Build a fixed corpus of source strings exercising each AST node. |
| 13 | + * 2. Parse each source into a program AST. |
| 14 | + * 3. Pretty-print the AST back to a string. |
| 15 | + * 4. Re-parse the pretty-printed string. |
| 16 | + * 5. Compare the two ASTs for structural equality. |
| 17 | + * |
| 18 | + * Discharges: |
| 19 | + * - PROOF-NARRATIVE.md §3 TG-4 (round-trip). |
| 20 | + * - ASSUMPTIONS.md A-TG-4.1 (pretty printer is unambiguous w.r.t. parser). |
| 21 | + * - ASSUMPTIONS.md A-TG-4.2 (lexer doesn't strip required information). |
| 22 | + *) |
| 23 | + |
| 24 | +(* --------------------------------------------------------------------- *) |
| 25 | +(* Test harness — mirrors test_parser.ml/test_property.ml style *) |
| 26 | +(* --------------------------------------------------------------------- *) |
| 27 | + |
| 28 | +let passed = ref 0 |
| 29 | +let failed = ref 0 |
| 30 | +let total = ref 0 |
| 31 | + |
| 32 | +let test (name : string) (f : unit -> unit) : unit = |
| 33 | + incr total; |
| 34 | + try |
| 35 | + f (); |
| 36 | + incr passed; |
| 37 | + Printf.printf " PASS: %s\n" name |
| 38 | + with exn -> |
| 39 | + incr failed; |
| 40 | + Printf.printf " FAIL: %s (%s)\n" name (Printexc.to_string exn) |
| 41 | + |
| 42 | +(* --------------------------------------------------------------------- *) |
| 43 | +(* Parser + pretty wrappers *) |
| 44 | +(* --------------------------------------------------------------------- *) |
| 45 | + |
| 46 | +let parse (source : string) : Tangle.Ast.program option = |
| 47 | + let lexbuf = Lexing.from_string source in |
| 48 | + try Some (Tangle.Parser.program Tangle.Lexer.token lexbuf) |
| 49 | + with |
| 50 | + | _ -> None |
| 51 | + |
| 52 | +let parse_ok (source : string) : Tangle.Ast.program = |
| 53 | + match parse source with |
| 54 | + | Some p -> p |
| 55 | + | None -> failwith ("parse failed for: " ^ source) |
| 56 | + |
| 57 | +(* --------------------------------------------------------------------- *) |
| 58 | +(* AST equality — uses OCaml structural equality, but documented *) |
| 59 | +(* --------------------------------------------------------------------- *) |
| 60 | + |
| 61 | +(* The Ast.program type is a list of statements built from algebraic |
| 62 | + * datatypes. OCaml's structural equality (=) correctly compares them |
| 63 | + * field-by-field. |
| 64 | + * |
| 65 | + * Known caveats: |
| 66 | + * - Float literals: OCaml = compares floats by IEEE 754, which means |
| 67 | + * NaN != NaN. The current corpus has no NaN/Inf literals so this |
| 68 | + * is moot. If float literals are added, switch to a tolerant eq. |
| 69 | + * - Position info: Ast.ml does not carry source positions, so they |
| 70 | + * can't contaminate equality. |
| 71 | + *) |
| 72 | +let program_equal (p1 : Tangle.Ast.program) (p2 : Tangle.Ast.program) : bool = |
| 73 | + p1 = p2 |
| 74 | + |
| 75 | +(* --------------------------------------------------------------------- *) |
| 76 | +(* Corpus: one source line per AST constructor (or as close as *) |
| 77 | +(* practical given the v0.1.0 grammar). *) |
| 78 | +(* --------------------------------------------------------------------- *) |
| 79 | + |
| 80 | +let basic_corpus : (string * string) list = [ |
| 81 | + (* Literals + identity *) |
| 82 | + ("integer literal", "def x = 42"); |
| 83 | + ("string literal", {|def x = "hello"|}); |
| 84 | + ("bool true", "def x = true"); |
| 85 | + ("bool false", "def x = false"); |
| 86 | + ("identity", "def x = identity"); |
| 87 | + (* Braid generators *) |
| 88 | + ("empty braid", "def x = braid[]"); |
| 89 | + ("single sigma", "def x = braid[s1]"); |
| 90 | + ("trefoil-shaped", "def x = braid[s1, s1, s1]"); |
| 91 | + ("inverse generator", "def x = braid[s2^-1]"); |
| 92 | + ("mixed-sign braid", "def x = braid[s1, s2^-1, s1]"); |
| 93 | + (* Algebraic operations *) |
| 94 | + ("addition", "def x = a + b"); |
| 95 | + ("composition", "def x = braid[s1] . braid[s2]"); |
| 96 | + ("tensor product", "def x = braid[s1] | braid[s2]"); |
| 97 | + ("close call", "def x = close(braid[s1])"); |
| 98 | + ("equality", "def x = a == b"); |
| 99 | + (* Function definitions *) |
| 100 | + ("nullary def", "def x = identity"); |
| 101 | + ("unary def", "def f(a) = a"); |
| 102 | + ("binary def", "def f(a, b) = a + b"); |
| 103 | +] |
| 104 | + |
| 105 | +(* --------------------------------------------------------------------- *) |
| 106 | +(* Test bodies *) |
| 107 | +(* --------------------------------------------------------------------- *) |
| 108 | + |
| 109 | +let test_roundtrip_basic () = |
| 110 | + Printf.printf "\n--- TG-4: Basic round-trip on per-constructor corpus ---\n"; |
| 111 | + List.iter (fun (label, source) -> |
| 112 | + test ("roundtrip: " ^ label) (fun () -> |
| 113 | + let prog1 = parse_ok source in |
| 114 | + let pp_text = Tangle.Pretty.program_to_string prog1 in |
| 115 | + match parse pp_text with |
| 116 | + | None -> |
| 117 | + Printf.eprintf " re-parse failed for pretty output:\n %s\n" pp_text; |
| 118 | + failwith ("re-parse failed: " ^ label) |
| 119 | + | Some prog2 -> |
| 120 | + if not (program_equal prog1 prog2) then begin |
| 121 | + Printf.eprintf " AST mismatch on %s\n" label; |
| 122 | + Printf.eprintf " source: %s\n" source; |
| 123 | + Printf.eprintf " pretty: %s\n" pp_text; |
| 124 | + failwith ("ast mismatch: " ^ label) |
| 125 | + end) |
| 126 | + ) basic_corpus |
| 127 | + |
| 128 | +let test_roundtrip_idempotent () = |
| 129 | + Printf.printf "\n--- TG-4: pretty(parse(pretty(parse(s)))) = pretty(parse(s)) ---\n"; |
| 130 | + (* If parse and pretty are inverse on the AST level, then they're also |
| 131 | + * inverse on the pretty-form level after one normalisation pass. |
| 132 | + * This catches cases where the pretty printer is right-inverse but |
| 133 | + * not left-inverse (e.g. drops sugar). *) |
| 134 | + List.iter (fun (label, source) -> |
| 135 | + test ("idempotent: " ^ label) (fun () -> |
| 136 | + let prog1 = parse_ok source in |
| 137 | + let pp1 = Tangle.Pretty.program_to_string prog1 in |
| 138 | + let prog2 = parse_ok pp1 in |
| 139 | + let pp2 = Tangle.Pretty.program_to_string prog2 in |
| 140 | + if pp1 <> pp2 then begin |
| 141 | + Printf.eprintf " Pretty-form drift on %s\n" label; |
| 142 | + Printf.eprintf " first pretty: %s\n" pp1; |
| 143 | + Printf.eprintf " second pretty: %s\n" pp2; |
| 144 | + failwith ("pretty drift: " ^ label) |
| 145 | + end) |
| 146 | + ) basic_corpus |
| 147 | + |
| 148 | +(* --------------------------------------------------------------------- *) |
| 149 | +(* Entry point *) |
| 150 | +(* --------------------------------------------------------------------- *) |
| 151 | + |
| 152 | +let () = |
| 153 | + Printf.printf "=== TG-4 round-trip property tests ===\n"; |
| 154 | + Printf.printf "Obligation: parse(pretty(e)) = e for closed e in core Tangle.\n"; |
| 155 | + test_roundtrip_basic (); |
| 156 | + test_roundtrip_idempotent (); |
| 157 | + Printf.printf "\n=== Results: %d/%d passed" !passed !total; |
| 158 | + if !failed > 0 then |
| 159 | + Printf.printf ", %d FAILED" !failed; |
| 160 | + Printf.printf " ===\n"; |
| 161 | + if !failed > 0 then exit 1 |
0 commit comments