open! Import

module type S = sig
  type t [@@deriving compare, sexp_of]

  val num_bits : int
  val min_value : t
  val minus_one : t
  val zero : t
  val one : t
  val max_value : t
  val to_int64 : t -> int64
  val shift_right : t -> int -> t
  val random : Random.State.t -> t -> t -> t
end

module I : S with type t = int = struct
  include Int

  let random = Random.State.int_incl
end

module Native : S with type t = nativeint = struct
  include Nativeint

  let random = Random.State.nativeint_incl
end

module I32 : S with type t = int32 = struct
  include Int32

  let random = Random.State.int32_incl
end

module I64 : S with type t = int64 = struct
  include Int64

  let random = Random.State.int64_incl
end

module I63 : S with type t = Int63.t = struct
  include Int63

  let random state lo hi = Int63.random_incl ~state lo hi
end

let iter (type a) (module M : S with type t = a) ~f =
  let state = Random.State.make [| 0; 1; 2; 3; 4; 5 |] in
  List.iter ~f [ M.min_value; M.minus_one; M.zero; M.one; M.max_value ];
  for _ = 1 to 10_000 do
    (* skew toward low numbers of bits so that, e.g., choosing a random int64 does
       frequently find a value that can be converted to int32. *)
    let strip_bits = Random.State.int_incl state 0 (M.num_bits - 1) in
    let lo = M.shift_right M.min_value strip_bits in
    let hi = M.shift_right M.max_value strip_bits in
    f (M.random state lo hi)
  done
;;

let try_with f x = Option.try_with (fun () -> f x)

(* Checks that a conversion from [A.t] to [B.t] is total using [of] and [to]. *)
let test_total
  (type a b)
  (module A : S with type t = a)
  (module B : S with type t = b)
  ~of_:b_of_a
  ~to_:a_to_b
  =
  iter
    (module A)
    ~f:(fun a ->
      require_compare_equal [%here] (module B) (b_of_a a) (a_to_b a);
      require_compare_equal [%here] (module Int64) (A.to_int64 a) (B.to_int64 (b_of_a a)))
;;

let truncate int64 ~num_bits =
  Int64.shift_right (Int64.shift_left int64 (64 - num_bits)) (64 - num_bits)
;;

(* Checks that a conversion from [A.t] to [B.t] is partial using [of] and [to], and the
   [_exn] equivalents. In the case where the conversion fails, ensure that the value,
   converted to an [Int64.t] is outside the representable range of [B.t] converted to an
   [Int64.t] as well. *)
let test_partial
  (type a b)
  (module A : S with type t = a)
  (module B : S with type t = b)
  ~of_:b_of_a
  ~of_exn:b_of_a_exn
  ~of_trunc:b_of_a_trunc
  ~to_:a_to_b
  ~to_exn:a_to_b_exn
  ~to_trunc:a_to_b_trunc
  =
  let module B_option = struct
    type t = B.t option [@@deriving compare, sexp_of]
  end
  in
  let convertible_count = ref 0 in
  iter
    (module A)
    ~f:(fun a ->
      require_compare_equal [%here] (module B_option) (b_of_a a) (a_to_b a);
      require_compare_equal [%here] (module B_option) (b_of_a a) (try_with b_of_a_exn a);
      require_compare_equal [%here] (module B_option) (a_to_b a) (try_with a_to_b_exn a);
      match b_of_a a with
      | Some b ->
        Int.incr convertible_count;
        require_compare_equal [%here] (module B) b (b_of_a_trunc a);
        require_compare_equal [%here] (module B) b (a_to_b_trunc a);
        require_compare_equal [%here] (module Int64) (A.to_int64 a) (B.to_int64 b)
      | None ->
        let trunc = truncate (A.to_int64 a) ~num_bits:B.num_bits in
        require_compare_equal [%here] (module Int64) trunc (B.to_int64 (b_of_a_trunc a));
        require_compare_equal [%here] (module Int64) trunc (B.to_int64 (a_to_b_trunc a));
        require
          [%here]
          (Int64.( > ) (A.to_int64 a) (B.to_int64 B.max_value)
           || Int64.( < ) (A.to_int64 a) (B.to_int64 B.min_value))
          ~if_false_then_print_s:(lazy [%message "failed to convert" ~_:(a : A.t)]));
  (* Make sure we stress the conversion a nontrivial number of times. This makes sure the
     random generation is useful and we aren't just testing the hard-coded examples. *)
  require
    [%here]
    (!convertible_count > 100)
    ~if_false_then_print_s:
      (lazy
        [%message
          "did not test successful conversion often enough" (convertible_count : int ref)])
;;

let%expect_test "int <-> nativeint" =
  test_total (module I) (module Native) ~of_:Nativeint.of_int ~to_:Int.to_nativeint;
  [%expect {| |}];
  test_partial
    (module Native)
    (module I)
    ~of_:Int.of_nativeint
    ~of_exn:Int.of_nativeint_exn
    ~of_trunc:Int.of_nativeint_trunc
    ~to_:Nativeint.to_int
    ~to_exn:Nativeint.to_int_exn
    ~to_trunc:Nativeint.to_int_trunc;
  [%expect {| |}]
;;

let%expect_test "int <-> int32" =
  test_partial
    (module I)
    (module I32)
    ~of_:Int32.of_int
    ~of_exn:Int32.of_int_exn
    ~of_trunc:Int32.of_int_trunc
    ~to_:Int.to_int32
    ~to_exn:Int.to_int32_exn
    ~to_trunc:Int.to_int32_trunc;
  [%expect {| |}];
  test_partial
    (module I32)
    (module I)
    ~of_:Int.of_int32
    ~of_exn:Int.of_int32_exn
    ~of_trunc:Int.of_int32_trunc
    ~to_:Int32.to_int
    ~to_exn:Int32.to_int_exn
    ~to_trunc:Int32.to_int_trunc;
  [%expect {| |}]
;;

let%expect_test "nativeint <-> int32" =
  test_partial
    (module Native)
    (module I32)
    ~of_:Int32.of_nativeint
    ~of_exn:Int32.of_nativeint_exn
    ~of_trunc:Int32.of_nativeint_trunc
    ~to_:Nativeint.to_int32
    ~to_exn:Nativeint.to_int32_exn
    ~to_trunc:Nativeint.to_int32_trunc;
  [%expect {| |}];
  test_total (module I32) (module Native) ~of_:Nativeint.of_int32 ~to_:Int32.to_nativeint;
  [%expect {| |}]
;;

let%expect_test "int <-> int64" =
  test_total (module I) (module I64) ~of_:Int64.of_int ~to_:Int.to_int64;
  [%expect {| |}];
  test_partial
    (module I64)
    (module I)
    ~of_:Int.of_int64
    ~of_exn:Int.of_int64_exn
    ~of_trunc:Int.of_int64_trunc
    ~to_:Int64.to_int
    ~to_exn:Int64.to_int_exn
    ~to_trunc:Int64.to_int_trunc;
  [%expect {| |}]
;;

let%expect_test "nativeint <-> int64" =
  test_total (module Native) (module I64) ~of_:Int64.of_nativeint ~to_:Nativeint.to_int64;
  [%expect {| |}];
  test_partial
    (module I64)
    (module Native)
    ~of_:Nativeint.of_int64
    ~of_exn:Nativeint.of_int64_exn
    ~of_trunc:Nativeint.of_int64_trunc
    ~to_:Int64.to_nativeint
    ~to_exn:Int64.to_nativeint_exn
    ~to_trunc:Int64.to_nativeint_trunc;
  [%expect {| |}]
;;

let%expect_test "int32 <-> int64" =
  test_total (module I32) (module I64) ~of_:Int64.of_int32 ~to_:Int32.to_int64;
  [%expect {| |}];
  test_partial
    (module I64)
    (module I32)
    ~of_:Int32.of_int64
    ~of_exn:Int32.of_int64_exn
    ~of_trunc:Int32.of_int64_trunc
    ~to_:Int64.to_int32
    ~to_exn:Int64.to_int32_exn
    ~to_trunc:Int64.to_int32_trunc;
  [%expect {| |}]
;;

let%expect_test "int <-> int63" =
  test_total (module I) (module I63) ~of_:Int63.of_int ~to_:Int63.of_int;
  [%expect {| |}];
  test_partial
    (module I63)
    (module I)
    ~of_:Int63.to_int
    ~of_exn:Int63.to_int_exn
    ~of_trunc:Int63.to_int_trunc
    ~to_:Int63.to_int
    ~to_exn:Int63.to_int_exn
    ~to_trunc:Int63.to_int_trunc;
  [%expect {| |}]
;;

let%expect_test "nativeint <-> int63" =
  test_partial
    (module Native)
    (module I63)
    ~of_:Int63.of_nativeint
    ~of_exn:Int63.of_nativeint_exn
    ~of_trunc:Int63.of_nativeint_trunc
    ~to_:Int63.of_nativeint
    ~to_exn:Int63.of_nativeint_exn
    ~to_trunc:Int63.of_nativeint_trunc;
  [%expect {| |}];
  test_partial
    (module I63)
    (module Native)
    ~of_:Int63.to_nativeint
    ~of_exn:Int63.to_nativeint_exn
    ~of_trunc:Int63.to_nativeint_trunc
    ~to_:Int63.to_nativeint
    ~to_exn:Int63.to_nativeint_exn
    ~to_trunc:Int63.to_nativeint_trunc;
  [%expect {| |}]
;;

let%expect_test "int32 <-> int63" =
  test_total (module I32) (module I63) ~of_:Int63.of_int32 ~to_:Int63.of_int32;
  [%expect {| |}];
  test_partial
    (module I63)
    (module I32)
    ~of_:Int63.to_int32
    ~of_exn:Int63.to_int32_exn
    ~of_trunc:Int63.to_int32_trunc
    ~to_:Int63.to_int32
    ~to_exn:Int63.to_int32_exn
    ~to_trunc:Int63.to_int32_trunc;
  [%expect {| |}]
;;

let%expect_test "int64 <-> int63" =
  test_partial
    (module I64)
    (module I63)
    ~of_:Int63.of_int64
    ~of_exn:Int63.of_int64_exn
    ~of_trunc:Int63.of_int64_trunc
    ~to_:Int63.of_int64
    ~to_exn:Int63.of_int64_exn
    ~to_trunc:Int63.of_int64_trunc;
  [%expect {| |}];
  test_total (module I63) (module I64) ~of_:Int63.to_int64 ~to_:Int63.to_int64;
  [%expect {| |}]
;;
