Skip to content

Commit 833f960

Browse files
test(tg-4): pretty-print/parse round-trip property test (#31)
TG-4 from PROOF-NARRATIVE.md §3: parse(pretty(e)) = e for every closed value e in the core Tangle language. Free fuzz oracle and foundation of any IR-viewer tooling that re-parses pretty output. ## What's added `compiler/test/test_roundtrip.ml` (~165 LoC, agent-designed) — new test executable. Two testsets: 1. **roundtrip_basic** — Per-constructor corpus (18 inputs spanning integer/string/bool literals, identity, braid generators, addition, composition, tensor, close, equality, nullary/unary/ binary defs). Parses each source, pretty-prints, re-parses, asserts structural AST equality. 2. **roundtrip_idempotent** — Asserts `pretty(parse(pretty(parse(s)))) = pretty(parse(s))`. Catches the case where pretty is right-inverse but not left-inverse (e.g. drops surface sugar). ## Infrastructure Follows the existing `test_parser.ml` / `test_property.ml` harness style (manual passed/failed/total counters, no external test framework). Wired into `compiler/test/dune` `(tests names ...)` list, runs under `dune runtest` or `dune exec test/test_roundtrip.exe`. ## AST equality Uses OCaml structural equality (`=`) on `Ast.program`. The Ast types are pure algebraic datatypes with no custom equality logic — `=` works correctly. Known caveat: float literals (none in the current corpus; would need epsilon comparison if added). ## Cross-references - PROOF-NARRATIVE.md §3 TG-4 - ASSUMPTIONS.md A-TG-4.1 (pretty printer unambiguous) - ASSUMPTIONS.md A-TG-4.2 (lexer doesn't strip required information) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f937a0e commit 833f960

2 files changed

Lines changed: 162 additions & 1 deletion

File tree

compiler/test/dune

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
; SPDX-License-Identifier: MPL-2.0
22

33
(tests
4-
(names test_parser test_typecheck test_eval test_e2e test_property test_compositional)
4+
(names test_parser test_typecheck test_eval test_e2e test_property test_compositional test_roundtrip)
55
(libraries tangle))

compiler/test/test_roundtrip.ml

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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

Comments
 (0)