diff --git a/dune b/dune index a29ce3f422f3715c9df5320baa6691f5598391ab..2bc8efeaadff54275c498c99f041fd8c70f8d0ba 100644 --- a/dune +++ b/dune @@ -9,4 +9,4 @@ (public_name caisar) (modules_without_implementation property_syntax) (libraries yojson cmdliner logs logs.cli logs.fmt fmt.tty base unix str ppx_deriving_yojson.runtime) - (preprocess (pps ppx_deriving_yojson ppx_deriving.show ppx_deriving.ord))) + (preprocess (pps ppx_deriving_yojson ppx_deriving.show ppx_deriving.ord ppx_deriving.eq))) diff --git a/main.ml b/main.ml index cf118bd9873db493f21b2da5d0089733775b58bf..1384273d645244c11c3434927b72a9843e18a4a1 100644 --- a/main.ml +++ b/main.ml @@ -45,30 +45,29 @@ let setup_logs = (* Commands. *) -let config cmd detect filename () = - let filename = Option.value filename ~default:Solver.default_config_filename in +let config cmd detect file_opt () = + let file = Option.value file_opt ~default:Solver.default_config_file in Logs.debug - (fun m -> m "Command `%s' with configuration file `%s'." - cmd filename); + (fun m -> m "Command `%s' with configuration file `%s'." cmd file); if detect then begin - match Solver.detect filename with - | Ok () -> Logs.app (fun m -> m "Configuration saved in `%s'." filename) + match Solver.detect ~file with + | Ok () -> Logs.app (fun m -> m "Configuration saved in `%s'." file) | Error e -> Logs.err (fun m -> m "%s" e) end -let verify cmd raw_options config model property solver () = +let verify cmd raw_options config_file model property solver () = Logs.debug (fun m -> m "Command `%s' with@ model `%s',@ property `%s' and@ solver `%s' \ (using configuration file `%s')." - cmd model property solver config); + cmd model property (Solver.show_solver solver) config_file); let do_verify () = let open Result in Model.build ~filename:model >>= fun model -> Property.build ~filename:property >>= fun property -> - Solver.get_config_solver ~solver config >>= fun config_solver -> - Solver.exec ?raw_options config_solver model property + Solver.get_config solver ~file:config_file >>= fun config -> + Solver.exec ?raw_options config model property in match do_verify () with | Ok () -> Logs.app (fun m -> m "Done.") @@ -79,8 +78,16 @@ let verify cmd raw_options config model property solver () = let config_cmd = let cmdname = "config" in + let dirvar = "DIR" in + let envs = + [ Term.env_info + ~doc:"Path to directory where to search for the executable of a solver." + dirvar ] + in let detect = - let doc = Format.sprintf "Detect (supported) solvers in \\$PATH." in + let doc = + Format.sprintf "Detect solvers in \\$PATH (or \\$%s, if specified)." dirvar + in Arg.(value & flag & info ["d"; "detect"] ~doc) in let filename = @@ -97,12 +104,6 @@ let config_cmd = `S Manpage.s_description; `P (Format.sprintf "Handle the configuration of %s." caisar); ] in - let envs = - List.map - ~f:(Term.env_info - ~doc:"Use $(env) to specify the respective executable directory.") - Solver.env_vars - in Term.(ret (const (fun cmdname detect filename _ -> if not detect && Option.is_none filename @@ -117,9 +118,18 @@ let verify_cmd = let cmdname = "verify" in let docv_solver = "SOLVER" in let solver = - let doc = "The solver to use for verification." in + let doc = + "The solver to use for verification. \ + $(docv) must be either `pyrat' or `marabou'." + in + let solvers = + Arg.enum + (List.map + ~f:(fun s -> String.lowercase (Solver.show_solver s), s) + Solver.defaults) + in Arg.(required - & pos 2 (some string) None + & pos 2 (some solvers) (Some Solver.Pyrat) & info [] ~docv:docv_solver ~doc) in let docv_model = "MODEL" in @@ -138,7 +148,7 @@ let verify_cmd = in let config_filename = let doc = "$(b,FILE) to read the configuration from." in - Arg.(value & opt file Solver.default_config_filename + Arg.(value & opt file Solver.default_config_file & info ["c"; "config"] ~doc) in let doc = "Property verification of neural networks." in diff --git a/solver.ml b/solver.ml index d31c2df48dfbf7090d953f51106b5152a217e861..5078ddbe4d9a78703a22cc2b3729403bd43a667f 100644 --- a/solver.ml +++ b/solver.ml @@ -12,7 +12,7 @@ module Sys = Caml.Sys type solver = | Pyrat | Marabou - [@@deriving show { with_path = false }, yojson] + [@@deriving show { with_path = false }, ord, eq, yojson] (* Utilities. *) @@ -51,9 +51,6 @@ let pp_property solver property = | Marabou -> Ok (pp_property_marabou property) -let env_var_of_solver solver = String.uppercase (show_solver solver ^ "_dir") -let env_vars = List.map ~f:env_var_of_solver defaults - let default_exe_name_of_solver = function | Pyrat -> "pyrat.py" | Marabou -> "Marabou" @@ -64,7 +61,7 @@ let default_option_of_solver = function let exe_path_of_solver solver = let exe = default_exe_name_of_solver solver in - match Sys.getenv_opt (env_var_of_solver solver) with + match Sys.getenv_opt "DIR" with | None -> begin (* We want the complete path of [exe] in $PATH: we use `command -v [exe]' @@ -91,7 +88,17 @@ let exe_path_of_solver solver = (* Configuration. *) type version = - [`Unknown_version | `Version of string] [@@deriving yojson, show] + | Unknown + | Version of string +[@@deriving show { with_path = false }, yojson] + +type config = { + solver: solver; + exe: string; + version: version; +} [@@deriving yojson, show] + +type full_config = config list [@@deriving yojson, show] let version_of_solver solver s = let regexp_string = @@ -104,66 +111,62 @@ let version_of_solver solver s = let regexp_version = Str.regexp regexp_string in try ignore (Str.search_forward regexp_version s 0); - `Version (Str.matched_string s) + Version (Str.matched_string s) with Stdlib.Not_found -> Logs.debug (fun m -> m "No recognizable version found."); - `Unknown_version - - -type config_solver = { - name: string; - solver: solver; - exe: string; - version: version; -} [@@deriving yojson] + Unknown -type config = config_solver list [@@deriving yojson] - -let default_config_filename = +let default_config_file = let filename = ".caisar.conf" in match Sys.getenv_opt "HOME" with | None -> filename | Some p -> p ^ "/.caisar.conf" -let get_config filename = - try config_of_yojson (Yojson.Safe.from_file filename) - with Yojson.Json_error e -> Error e +let get_full_config filename = + let open Result in + match full_config_of_yojson (Yojson.Safe.from_file filename) with + | Error _ | exception _ -> + [] + | Ok full_config -> + begin + Logs.debug + (fun m -> m "Configuration in file `%s':@ %s" + filename (show_full_config full_config)); + full_config + end -let get_config_solver ~solver filename = +let get_config ~file solver = let open Result in Logs.info - (fun m -> m "Reading configuration file `%s' for solver `%s'." - filename solver); - if not (Sys.file_exists filename) - then Error (Format.sprintf "Configuration file `%s' not exist." filename) + (fun m -> m "Reading configuration file `%s' for solver `%a'." + file + pp_solver solver); + if not (Sys.file_exists file) + then Error (Format.sprintf "Configuration file `%s' not exist." file) else begin (* Read all solver configs present in the given configuration file. *) - get_config filename >>= fun config -> - (* Search for a [config_solver] with a name [solver]. *) - match List.find ~f:(fun cs -> String.equal cs.name solver) config with + let full_config = get_full_config file in + (* Search for a [config] with [solver]. *) + match List.find ~f:(fun cs -> equal_solver cs.solver solver) full_config with | None -> Error - (Format.sprintf - "No solver with name `%s' found in configuration file `%s'." - solver filename) - | Some config_solver -> + (Format.asprintf + "No solver `%a' found in configuration file `%s'." + pp_solver solver + file) + | Some config -> begin Logs.info - (fun m -> m "Found `%s' `%s'." - (show_solver config_solver.solver) - (show_version config_solver.version)); - Ok config_solver + (fun m -> m "Found `%a' `%s'." + pp_solver config.solver + (show_version config.version)); + Ok config end end -let create_config_file filename = - Logs.debug (fun m -> m "Creating configuration file `%s'." filename); - let config = - if Sys.file_exists filename - then Some (get_config filename) - else None - in - Stdlib.open_out filename, config +let create_config_file file = + Logs.debug (fun m -> m "Creating new configuration file `%s'." file); + Stdlib.open_out file let detect_default_solvers () = let module Filename = Caml.Filename in @@ -182,30 +185,29 @@ let detect_default_solvers () = exe [default_option_of_solver solver] in + Logs.debug + (fun m -> m "Executing command `%s' to detect `%a'." + cmd pp_solver solver); let retcode = Sys.command cmd in let in_channel = Stdlib.open_in tmp in let firstline = Stdlib.input_line in_channel in Stdlib.close_in in_channel; Sys.remove tmp; - let solver_name = show_solver solver in if retcode <> 0 then begin - Logs.info (fun m -> m "No solver `%s' found." solver_name); + Logs.info (fun m -> m "No solver `%a' found." pp_solver solver); None end else begin - (* If available, we save build a [config_solver] for solver. *) + (* If available, we save a [config] for solver. *) let version = version_of_solver solver firstline in - let config_solver = - { name = String.lowercase solver_name; - solver; - exe; - version; } - in + let config = { solver; exe; version; } in Logs.app - (fun m -> m "Found solver `%s' (%s): %s." - solver_name exe (show_version version)); - Some config_solver + (fun m -> m "Found solver `%a' `%a' (%s)." + pp_solver solver + pp_version version + exe); + Some config end) defaults in @@ -213,54 +215,50 @@ let detect_default_solvers () = with _ -> Error "Unexpected failure." - -let detect filename = +let detect ~file = let open Result in - Logs.info (fun m -> - m "Detecting solvers and writing configuration file `%s'." - filename); - (* Create new configuration file and retrieve the `config' from the existing - one, if any. *) - let out_channel, config_opt = create_config_file filename in - (* Detect default supported solvers in $PATH. *) - detect_default_solvers () >>= fun default_config -> - (* Build [config] by first appending [default_config] and the existing one - [config_opt], and then deduping the result. *) - begin - match config_opt with - | None -> - Ok default_config - | Some or_config -> - or_config >>= fun current_config -> - (* We consider two solvers equal as soon as their `name' is. *) - (* TODO: This comparison is not optimal but needed as long as we register - solver names as lazily as done in [detect_default_solvers]. *) - let compare cs1 cs2 = String.compare cs1.name cs2.name in - Ok - (List.dedup_and_sort - ~compare - (List.append current_config default_config)) - end >>= fun config -> + (* Retrieve the current configuration from [file]. *) + let current_full_config = get_full_config file in + (* Create new configuration file. *) + let out_channel = create_config_file file in + (* Detect default supported solvers in $PATH or $DIR. *) + detect_default_solvers () >>= fun default_full_config -> + (* Build [full_config] by first appending [default_full_config] to + [current_full_config], and then deduping the result. *) + let full_config = + (* TODO: This comparison is not optimal but sufficient as long as we register + solver as simply as done in [detect_default_solvers]. *) + let compare cs1 cs2 = compare_solver cs1.solver cs2.solver in + List.dedup_and_sort + ~compare + (List.append current_full_config default_full_config) + in (* We write all solver configs in the configuration file, as JSON data. *) - let config_json = config_to_yojson config in - Yojson.Safe.pretty_to_channel out_channel config_json; + let full_config_json = full_config_to_yojson full_config in + Yojson.Safe.pretty_to_channel out_channel full_config_json; Ok () + (* Verification. *) -let check_availability config_solver = +let check_availability config = let module Filename = Caml.Filename in - let solver_name = config_solver.name in - Logs.info (fun m -> m "Checking actual availability of `%s'." solver_name); + Logs.debug + (fun m -> m "Checking actual availability of `%a'." + pp_solver config.solver); try (* Execute command `solver --version' to check actual availability. *) let tmp = Filename.temp_file "caisar" "avail" in let cmd = Filename.quote_command ~stdout:tmp ~stderr:tmp - config_solver.exe - [default_option_of_solver config_solver.solver] + config.exe + [default_option_of_solver config.solver] in + Logs.debug + (fun m -> m "Executing command `%s' to check `%a' availability." + cmd + pp_solver config.solver); let retcode = Sys.command cmd in let in_channel = Stdlib.open_in tmp in Stdlib.close_in in_channel; @@ -276,14 +274,14 @@ let check_availability config_solver = with Stdlib.Not_found | End_of_file | Failure _ | Sys_error _ -> Error "Unexpected failure." -let check_compatibility config_solver model = - let solver = config_solver.solver in +let check_compatibility config model = + let solver = config.solver in match solver, model.Model.format with | (Pyrat | Marabou), (Model.Onnx as f) -> Error - (Format.sprintf - "No support yet for `%s' and model format `%s'." - (show_solver solver) + (Format.asprintf + "No support yet for `%a' and model format `%s'." + pp_solver solver (Model.show_format f)) | _ -> Ok () @@ -341,19 +339,19 @@ let build_command ?raw_options confg_solver property model = with Failure e -> Error (Format.sprintf "Unexpected failure@ `%s'." e) -let exec ?raw_options config_solver model property = +let exec ?raw_options config model property = let open Result in (* Check solver availability. *) - check_availability config_solver >>= fun () -> + check_availability config >>= fun () -> (* Check solver and model compatibility. *) - check_compatibility config_solver model >>= fun () -> + check_compatibility config model >>= fun () -> (* Build the required command-line. *) - build_command ?raw_options config_solver property model >>= fun (tmp, cmd) -> + build_command ?raw_options config property model >>= fun (tmp, cmd) -> (* Execute the command. *) begin try Logs.app - (fun m -> m "Launching verification with `%s'." config_solver.name); + (fun m -> m "Launching verification with `%a'." pp_solver config.solver); Logs.debug (fun m -> m "Executing command `%s'." cmd); let retcode = Sys.command cmd in Sys.remove tmp; diff --git a/solver.mli b/solver.mli index cb85fc70aad008b77af113f7e320e2ba111cd157..cc4aaae405d51f29f7d30e36e161cecaa241af22 100644 --- a/solver.mli +++ b/solver.mli @@ -10,25 +10,22 @@ type solver = Pyrat | Marabou [@@deriving show] (** The list of supported solvers by default. *) val defaults: solver list -val env_vars: string list +val default_config_file: string -val default_config_filename: string - -(** [detect file] searches for solvers in $PATH, or in the paths provided - via [env_vars], and save the configurations in [file]. *) -val detect: string -> (unit, string) Result.t +(** [detect ~file] searches for solvers in $PATH, or in the path provided in env + variable $DIR, and save the configurations in [file]. *) +val detect: file:string -> (unit, string) Result.t (** Solver configuration. *) -type config_solver +type config -(** [config_solver ~solver file] retrieves the [solver] configuration, if any, - from the given [file]. *) -val get_config_solver: - solver:string -> string -> (config_solver, string) Result.t +(** [get_config ~file solver] retrieves the [solver] configuration, if any, from + the given [file]. *) +val get_config: file:string -> solver -> (config, string) Result.t -(** [exec ?raw_options config_solver model property] runs [solver] wrt the given - [config_solver] on [property] for [model] with the provided [raw_options], if - any. *) +(** [exec ?raw_options config model property] runs a solver wrt the given + [config] on [property] for [model] with the provided [raw_options], if any. +*) val exec: ?raw_options:string -> - config_solver -> Model.t -> Property.t -> (unit, string) Result.t + config -> Model.t -> Property.t -> (unit, string) Result.t