1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
(** Copyright 2021-2025, Kakadu and contributors *)

(** SPDX-License-Identifier: LGPL-3.0-or-later *)

open Lambda_lib
open Ast
open Parser
open Printer
open QCheck

let gen_name =
  let names = [ "x"; "y"; "z"; "n"; "m"; "f"; "g"; "h"; "a"; "b"; "c" ] in
  Gen.oneofl names
;;

let gen_const =
  Gen.frequency [ 4, Gen.map (fun n -> Int n) Gen.small_int; 1, Gen.return Unit ]
;;

let gen_op = Gen.oneofl [ OpAdd; OpSub; OpMul; OpDiv; OpEq; OpGt; OpLt; OpGte; OpLte ]

let rec gen_expr depth =
  let leaf =
    [ Gen.map (fun c -> Ast.Const c) gen_const; Gen.map (fun n -> Ast.Var n) gen_name ]
  in
  if depth <= 0
  then Gen.oneof leaf
  else (
    let sub = gen_expr (depth / 2) in
    let simpler = gen_expr (depth - 1) in
    let let_gen =
      let open Gen in
      oneofl [ NonRec; Rec ]
      >>= fun kind ->
      gen_name
      >>= fun name ->
      (match kind with
       | Rec -> map2 (fun arg body -> Ast.Fun (arg, body)) gen_name simpler
       | NonRec -> simpler)
      >>= fun rhs ->
      option sub
      >>= fun body_opt ->
      let scope =
        match body_opt with
        | None -> GlobalVar
        | Some _ -> LocalVar
      in
      return (Ast.Let (scope, kind, name, rhs, body_opt))
    in
    Gen.oneof
      (leaf
       @ [ Gen.map2 (fun n body -> Ast.Fun (n, body)) gen_name simpler
         ; Gen.map2 (fun fn arg -> Ast.App (fn, arg)) simpler simpler
         ; Gen.map3 (fun l op r -> Ast.BinOp (op, l, r)) simpler gen_op simpler
         ; Gen.map3 (fun c t e_opt -> Ast.If (c, t, e_opt)) sub simpler (Gen.option sub)
         ; let_gen
         ]))
;;

let arb_expr =
  make ~print:string_of_expr (Gen.sized (fun sz -> gen_expr (1 + (sz mod 6))))
;;

let invalid_programs =
  [ "let = x"
  ; "fun -> x"
  ; "if x then"
  ; "x + + 2"
  ; "let rec f ="
  ; "f ( ) )"
  ; "let 5 = 3"
  ; "fun x y -> -> x"
  ; "if (1 <) then 2"
  ]
;;

let prop_roundtrip =
  Test.make ~name:"parser/pretty-printer roundtrip" ~count:500 arb_expr (fun e ->
    let printed = string_of_expr e in
    match parse printed with
    | Ok e' -> e' = e
    | Error _ -> false)
;;

let prop_stable_string =
  Test.make ~name:"pretty-print is idempotent after parse" ~count:500 arb_expr (fun e ->
    let first = string_of_expr e in
    match parse first with
    | Error _ -> false
    | Ok e' ->
      let second = string_of_expr e' in
      let reparse = parse second in
      (match reparse with
       | Ok e'' -> second = string_of_expr e'' && e' = e''
       | Error _ -> false))
;;

let prop_invalid_fails =
  let gen_bad = Gen.oneofl invalid_programs in
  Test.make ~name:"invalid snippets are rejected" ~count:200 (make gen_bad) (fun src ->
    match parse src with
    | Ok _ -> false
    | Error _ -> true)
;;

let () =
  (* Keep progress lines but suppress frequent interim updates *)
  QCheck_base_runner.set_time_between_msg 1e9
;;

let () =
  QCheck_runner.run_tests_main
    ~argv:[| Sys.argv.(0); "-v"; "--colors" |]
    [ prop_roundtrip; prop_stable_string; prop_invalid_fails ]
;;