Skip to content
Snippets Groups Projects
main.ml 10.07 KiB
(**************************************************************************)
(*                                                                        *)
(*  This file is part of CAISAR.                                          *)
(*                                                                        *)
(*  Copyright (C) 2022                                                    *)
(*    CEA (Commissariat à l'énergie atomique et aux énergies              *)
(*         alternatives)                                                  *)
(*                                                                        *)
(*  You can redistribute it and/or modify it under the terms of the GNU   *)
(*  Lesser General Public License as published by the Free Software       *)
(*  Foundation, version 2.1.                                              *)
(*                                                                        *)
(*  It is distributed in the hope that it will be useful,                 *)
(*  but WITHOUT ANY WARRANTY; without even the implied warranty of        *)
(*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the          *)
(*  GNU Lesser General Public License for more details.                   *)
(*                                                                        *)
(*  See the GNU Lesser General Public License version 2.1                 *)
(*  for more details (enclosed in the file licenses/LGPLv2.1).            *)
(*                                                                        *)
(**************************************************************************)

open Base
open Cmdliner

let caisar = "caisar"

let () =
  Nn2smt.init ();
  Vars_on_lhs.init ()

let () =
  Pyrat.init ();
  Marabou.init ();
  Vnnlib.init ()

(* -- Logs. *)

let pp_header =
  let x =
    match Array.length Caml.Sys.argv with
    | 0 -> Caml.(Filename.basename Sys.executable_name)
    | _ -> Caml.(Filename.basename Sys.argv.(0))
  in
  let pp_h ppf style h = Fmt.pf ppf "[%s][%a] " x Fmt.(styled style string) h in
  fun ppf (l, h) ->
    let open Logs_fmt in
    match l with
    | Logs.App -> Fmt.pf ppf "[%a] " Fmt.(styled app_style string) x
    | Logs.Error ->
      pp_h ppf err_style (match h with None -> "ERROR" | Some h -> h)
    | Logs.Warning ->
      pp_h ppf warn_style (match h with None -> "WARNING" | Some h -> h)
    | Logs.Info ->
      pp_h ppf info_style (match h with None -> "INFO" | Some h -> h)
    | Logs.Debug ->
      pp_h ppf debug_style (match h with None -> "DEBUG" | Some h -> h)

let setup_logs =
  let setup_log level =
    Fmt_tty.setup_std_outputs ~style_renderer:`Ansi_tty ();
    Logs.set_level level;
    Logs.set_reporter (Logs_fmt.reporter ~pp_header ())
  in
  Term.(const setup_log $ Logs_cli.level ())

let log_level_is_debug () =
  match Logs.level () with Some Debug -> true | _ -> false

(* -- Commands. *)

let config detect () =
  if detect
  then (
    Logs.debug (fun m -> m "Automatic detection");
    let config =
      let debug = log_level_is_debug () in
      Autodetect.autodetection ~debug ()
    in
    let open Why3 in
    let provers = Whyconf.get_provers config in
    if not (Whyconf.Mprover.is_empty provers)
    then
      Logs.app (fun m ->
        m "@[<v>%a@]"
          (Pp.print_iter2 Whyconf.Mprover.iter Pp.newline Pp.nothing
             Whyconf.print_prover Pp.nothing)
          provers))

let memlimit_of_string s =
  let s = String.strip s in
  let value =
    let re = Re__Pcre.regexp "[0-9]+" in
    Re__Core.matches re s
  in
  let unit =
    let re = Re__Pcre.regexp "MB?|GB?|TB?" in
    Re__Core.matches re s
  in
  match (value, unit) with
  | [ v ], ([] | [ "M" ] | [ "MB" ]) -> Int.of_string v
  | [ v ], ([ "G" ] | [ "GB" ]) -> Int.of_string v * 1000
  | [ v ], ([ "T" ] | [ "TB" ]) -> Int.of_string v * 1000000
  | _ -> invalid_arg "Unrecognized memory limit"

let timelimit_of_string s =
  let s = String.strip s in
  let value =
    let re = Re__Pcre.regexp "[0-9]+" in
    Re__Core.matches re s
  in
  let unit =
    let re = Re__Pcre.regexp "[a-z]" in
    Re__Core.matches re s
  in
  match (value, unit) with
  | [ v ], ([] | [ "s" ]) -> Int.of_string v
  | [ v ], [ "m" ] -> Int.of_string v * 60
  | [ v ], [ "h" ] -> Int.of_string v * 3600
  | _ -> invalid_arg "Unrecognized time limit"

let verify ?format ~loadpath ?memlimit ?timelimit prover ?dataset_csv files =
  let debug = log_level_is_debug () in
  let memlimit = Option.map memlimit ~f:memlimit_of_string in
  let timelimit = Option.map timelimit ~f:timelimit_of_string in
  List.iter
    ~f:
      (Verification.verify ~debug ?format ~loadpath ?memlimit ?timelimit prover
         ?dataset_csv)
    files

let verify_json ?memlimit ?timelimit file =
  let jc = Result.ok_exn (Verification.JSON.of_string file) in
  let prover = jc.Verification.JSON.prover in
  let dataset_csv = jc.Verification.JSON.property.dataset in
  let file = Result.ok_or_failwith (Verification.File.of_json_config jc) in
  verify ~loadpath:[] ?memlimit ?timelimit prover ~dataset_csv [ file ]

let exec_cmd cmdname cmd =
  Logs.debug (fun m -> m "Command `%s'" cmdname);
  cmd ()

(* -- Command line. *)

let config_cmd =
  let cmdname = "config" in
  let info =
    let doc = Fmt.str "%s configuration." caisar in
    let exits = Cmd.Exit.defaults in
    let man =
      [
        `S Manpage.s_description;
        `P (Fmt.str "Handle the configuration of %s." caisar);
      ]
    in
    Cmd.info cmdname ~sdocs:Manpage.s_common_options ~exits ~doc ~man
  in
  let term =
    let detect =
      let doc = "Detect solvers in \\$PATH." in
      Arg.(value & flag & info [ "d"; "detect" ] ~doc)
    in
    Term.(
      ret
        (const (fun detect _ ->
           if not detect
           then `Help (`Pager, Some cmdname)
           else
             (* TODO: Do not only check for [detect], and enable it by default,
                as soon as other options are available. *)
             `Ok (exec_cmd cmdname (fun () -> config true ())))
        $ detect $ setup_logs))
  in
  Cmd.v info term

let memlimit =
  let doc =
    "Memory limit. $(docv) is intended in megabytes (MB) by default, but \
     gigabytes (GB) and terabytes (TB) may also be specified instead."
  in
  Arg.(
    value & opt (some string) None & info [ "m"; "memlimit" ] ~doc ~docv:"MEM")

let timelimit =
  let doc =
    "Time limit. $(docv) is intended in seconds (s) by default, but minutes \
     (m) and hours (h) may also be specified instead."
  in
  Arg.(
    value & opt (some string) None & info [ "t"; "timelimit" ] ~doc ~docv:"TIME")

let verify_cmd =
  let cmdname = "verify" in
  let info =
    let doc = "Property verification using external provers." in
    Cmd.info cmdname ~sdocs:Manpage.s_common_options ~exits:Cmd.Exit.defaults
      ~doc
      ~man:[ `S Manpage.s_description; `P doc ]
  in
  let loadpath =
    let doc = "Additional loadpath." in
    Arg.(value & opt_all string [ "." ] & info [ "L"; "loadpath" ] ~doc)
  in
  let format =
    let doc = "File format." in
    Arg.(value & opt (some string) None & info [ "format" ] ~doc)
  in
  let term =
    let files =
      let doc = "File to verify." in
      let file_or_stdin = Arg.conv' Verification.File.(of_string, pretty) in
      Arg.(non_empty & pos_all file_or_stdin [] & info [] ~doc ~docv:"FILE")
    in
    let prover =
      let all_provers = Prover.list_available () in
      let doc =
        Fmt.str
          "Prover to use. Support is provided for the following provers: %a."
          (Fmt.list ~sep:Fmt.comma Fmt.string)
          (List.map ~f:Prover.to_string all_provers)
      in
      let provers =
        Arg.enum (List.map ~f:(fun p -> (Prover.to_string p, p)) all_provers)
      in
      Arg.(required & opt (some provers) None & info [ "p"; "prover" ] ~doc)
    in
    let dataset_csv =
      let doc = "Dataset in CSV format." in
      Arg.(value & opt (some file) None & info [ "dataset-csv" ] ~doc)
    in
    Term.(
      const
        (fun format loadpath memlimit timelimit prover dataset_csv files _ ->
        exec_cmd cmdname (fun () ->
          verify ?format ~loadpath ?memlimit ?timelimit prover ?dataset_csv
            files))
      $ format $ loadpath $ memlimit $ timelimit $ prover $ dataset_csv $ files
      $ setup_logs)
  in
  Cmd.v info term

let verify_json_cmd =
  let cmdname = "verify-json" in
  let info =
    let doc =
      "Property verification using external provers via json configuration \
       file."
    in
    Cmd.info cmdname ~sdocs:Manpage.s_common_options ~exits:Cmd.Exit.defaults
      ~doc
      ~man:[ `S Manpage.s_description; `P doc ]
  in
  let term =
    let json =
      let doc = "JSON file." in
      Arg.(
        required & pos ~rev:true 0 (some file) None & info [] ~doc ~docv:"FILE")
    in
    Term.(
      const (fun memlimit timelimit json _ ->
        exec_cmd cmdname (fun () -> verify_json ?memlimit ?timelimit json))
      $ memlimit $ timelimit $ json $ setup_logs)
  in
  Cmd.v info term

let default_info =
  let doc =
    "A platform for characterizing the safety and robustness of artificial \
     intelligence based software."
  in
  let sdocs = Manpage.s_common_options in
  let man =
    [
      `S Manpage.s_common_options;
      `P "These options are common to all commands.";
      `S "MORE HELP";
      `P "Use `$(mname) $(i,COMMAND) --help' for help on a single command.";
      `S Manpage.s_bugs;
      `P "Email bug reports to <...>";
    ]
  in
  let version = "0.1" in
  let exits = Cmd.Exit.defaults in
  Cmd.info caisar ~version ~doc ~sdocs ~exits ~man

let default_cmd = Term.(ret (const (fun _ -> `Help (`Pager, None)) $ const ()))

let () =
  (* We register custom printers for a selection of exceptions otherwise Why3
     will print the related messages as "anomaly: <exception with message>". *)
  Why3.Exn_printer.register (fun fmt exn ->
    match exn with
    | Invalid_argument msg -> Fmt.pf fmt "Invalid argument: %s" msg
    | Failure msg -> Fmt.pf fmt "Failure: %s" msg
    | _ -> raise exn)

let () =
  try
    Cmd.group ~default:default_cmd default_info
      [ config_cmd; verify_cmd; verify_json_cmd ]
    |> Cmd.eval ~catch:false |> Caml.exit
  with exn when not (log_level_is_debug ()) ->
    Logs.err (fun m -> m "@[%a@]" Why3.Exn_printer.exn_printer exn)