From 1525ffdde00bc696f0e5de2d50adf9b8d0fa3ad1 Mon Sep 17 00:00:00 2001 From: Michele Alberti <michele.alberti@cea.fr> Date: Wed, 14 Apr 2021 15:51:57 +0200 Subject: [PATCH] Rework property language and restructure code. --- config.ml | 216 ++++++++++++ config.mli | 28 ++ dune | 7 +- engine.ml | 259 +++++++++++++++ engine.mli | 11 + main.ml | 28 +- marabou.ml | 114 +++++++ property_syntax.mli => marabou.mli | 15 +- model.ml | 1 + property.ml | 249 ++++++++++---- property.mli | 40 ++- property_lexer.mll | 66 ++-- property_parser.mly | 132 +++++--- property_types.mli | 35 ++ pyrat.ml | 55 ++++ pyrat.mli | 7 + solver.ml | 508 ++--------------------------- solver.mli | 70 ++-- 18 files changed, 1185 insertions(+), 656 deletions(-) create mode 100644 config.ml create mode 100644 config.mli create mode 100644 engine.ml create mode 100644 engine.mli create mode 100644 marabou.ml rename property_syntax.mli => marabou.mli (62%) create mode 100644 property_types.mli create mode 100644 pyrat.ml create mode 100644 pyrat.mli diff --git a/config.ml b/config.ml new file mode 100644 index 0000000..cad1681 --- /dev/null +++ b/config.ml @@ -0,0 +1,216 @@ +(**************************************************************************) +(* *) +(* This file is part of Caisar. *) +(* *) +(**************************************************************************) + +open Base + +module Format = Caml.Format +module Sys = Caml.Sys +module Filename = Caml.Filename + +type version = + | Unknown + | Version of string +[@@deriving show { with_path = false }, yojson] + +type t = { + name: string; + exe: string; + version: version; +} +[@@deriving yojson, show] + +type full_config = t list [@@deriving yojson, show] + +let exe_path_of_solver solver = + let module S = (val solver: Solver.S) in + (* We want the complete path of [exe] in $PATH: we use `command -v [exe]' + and retrieve the result, if any. *) + let tmp = Filename.temp_file "caisar" "command" in + let _retcode = + Sys.command + (Filename.quote_command + ~stdout:tmp ~stderr:tmp + "command" ["-v"; S.exe_name]) + in + let in_channel = Stdlib.open_in tmp in + let found, exe = + try true, Stdlib.input_line in_channel + with End_of_file -> false, S.exe_name + in + Stdlib.close_in in_channel; + Sys.remove tmp; + if found + then Ok exe + else begin + match Sys.getenv_opt "DIR" with + | None -> + Error (Format.asprintf "Cannot find the executable of `%s'." S.name) + | Some dir -> + (* The env variable should already provide the path to the executable: we + first check that the path exists, refers to a directory and it is + absolute, then we concatenate the [exe] name to it. *) + if not (Sys.file_exists dir && Sys.is_directory dir) + then + Error (Format.sprintf "`%s' not exist or is not a directory." dir) + else + if Filename.is_relative dir + then + Error + (Format.sprintf + "`%s' is a relative path. An absolute path is required for DIR." + dir) + else + let exe = + if Filename.check_suffix dir Filename.dir_sep + then dir ^ exe + else Format.sprintf "%s%s%s" dir Filename.dir_sep exe + in + Ok exe + end + +let version_of_solver solver s = + let module S = (val solver: Solver.S) in + try + (* We suppose that the first occurrence that matches [re_version] is indeed + the version of the concercend [solver]. *) + ignore (Str.search_forward S.re_version s 0); + Version (Str.matched_string s) + with Stdlib.Not_found -> + Logs.debug (fun m -> m "No recognizable version found."); + Unknown + +let default_config_file = + let filename = ".caisar.conf" in + match Sys.getenv_opt "HOME" with + | None -> filename + | Some p -> p ^ "/.caisar.conf" + +let retrieve_full_config file = + let open Result in + match full_config_of_yojson (Yojson.Safe.from_file file) with + | Error _ | exception _ -> + [] + | Ok full_config -> + begin + Logs.debug (fun m -> + m "Configuration in file `%s':@ %a" + file + pp_full_config full_config); + full_config + end + +let retrieve ~file solver = + let open Result in + let module S = (val solver: Solver.S) in + Logs.info (fun m -> + m "Reading configuration file `%s' for solver `%s'." file S.name); + 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. *) + let full_config = retrieve_full_config file in + (* Search for a [config] with [solver]. *) + match List.find ~f:(fun cs -> String.equal cs.name S.name) full_config with + | None -> + Error + (Format.asprintf + "No solver `%s' found in configuration file `%s'." + S.name file) + | Some ({ name; version; _ } as config) -> + begin + Logs.info (fun m -> m "Found `%s' `%s'." name (show_version version)); + Ok config + end + end + +(* All solver instances. *) +let default_solvers = + [ (module Pyrat: Solver.S); (module Marabou: Solver.S) ] + +let detect_default_solvers () = + try + let config = + List.filter_map + ~f:(fun solver -> + match exe_path_of_solver solver with + | Error msg -> + Logs.info (fun m -> m "%s" msg); + None + | Ok exe -> + (* We detect whether solver is available in $PATH, or in the + provided path via env variable, by executing `solver + --version' command. *) + let module S = (val solver: Solver.S) in + let tmp = Filename.temp_file "caisar" "detect" in + let cmd = + Filename.quote_command + ~stdout:tmp ~stderr:tmp + exe + [S.exe_default_option] + in + Logs.debug (fun m -> + m "Executing command `%s' to detect `%s'." + cmd S.name); + let retcode = Sys.command cmd in + let cmd_output = + let in_channel = Stdlib.open_in tmp in + let buf = Buffer.create 512 in + try + while true do + Stdlib.Buffer.add_channel buf in_channel 512; + done; + assert false + with End_of_file -> + Stdlib.close_in in_channel; + Buffer.contents buf + in + Sys.remove tmp; + if retcode <> 0 + then begin + Logs.info (fun m -> m "No solver `%s' found." S.name); + None + end + else begin + (* If available, we save a [config] for solver. *) + let version = version_of_solver solver cmd_output in + let config = { name = S.name; exe; version; } in + Logs.app (fun m -> + m "Found solver `%s' `%a' (%s)." + S.name + pp_version version + exe); + Some config + end) + default_solvers + in + Ok config + with _ -> + Error "Unexpected failure." + +let detect ~file = + let open Result in + (* Detect default supported solvers in $PATH or $DIR. *) + detect_default_solvers () >>= fun default_full_config -> + (* Retrieve the current configuration from [file]. *) + let current_full_config = retrieve_full_config file in + (* Create new configuration file. *) + Logs.debug (fun m -> m "Creating new configuration file `%s'." file); + let out_channel = Stdlib.open_out file in + (* 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 = String.compare cs1.name cs2.name 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 full_config_json = full_config_to_yojson full_config in + Yojson.Safe.pretty_to_channel out_channel full_config_json; + Stdlib.close_out out_channel; + Ok () diff --git a/config.mli b/config.mli new file mode 100644 index 0000000..cfb1caa --- /dev/null +++ b/config.mli @@ -0,0 +1,28 @@ +(**************************************************************************) +(* *) +(* This file is part of Caisar. *) +(* *) +(**************************************************************************) + +type version = + | Unknown + | Version of string + +type t = private { + name: string; + exe: string; + version: version; +} + +(** Path to default configuration file. *) +val default_config_file: string + +(** The list of supported solvers by default. *) +val default_solvers: Solver.instance list + +(** [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 + +(** @return the [solver] configuration, if any, from the given [file]. *) +val retrieve: file:string -> Solver.instance -> (t, string) Result.t diff --git a/dune b/dune index 2bc8efe..4ec3915 100644 --- a/dune +++ b/dune @@ -2,11 +2,12 @@ (modules property_lexer)) (menhir - (modules property_parser)) + (modules property_parser) + (flags --table)) (executable (name main) (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) + (modules_without_implementation property_types) + (libraries menhirLib 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 ppx_deriving.eq))) diff --git a/engine.ml b/engine.ml new file mode 100644 index 0000000..c66f486 --- /dev/null +++ b/engine.ml @@ -0,0 +1,259 @@ +(**************************************************************************) +(* *) +(* This file is part of Caisar. *) +(* *) +(**************************************************************************) + +open Base +open Property_types + +module Format = Caml.Format +module Sys = Caml.Sys +module Filename = Caml.Filename + +(* Verification task submitted to a solver. *) +type task = { + (* Identifier. *) + id: string; + (* Original goal name. *) + goalname: string; + (* Subgoal identifier. + Typically a goal is translated to multiple subgoals by a solver. *) + subgoal: int; + (* The formula to verify. *) + formula: string; + (* Verification result. + [None] means no verification yet. *) + result: (Solver.Result.t, string) Result.t option; +} + +let check_availability solver config = + let module S = (val solver: Solver.S) in + Logs.debug (fun m -> m "Checking actual availability of `%s'." S.name); + 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.Config.exe + [S.exe_default_option] + in + Logs.debug (fun m -> + m "Executing command `%s' to check `%s' availability." cmd S.name); + let retcode = Sys.command cmd in + let in_channel = Stdlib.open_in tmp in + Stdlib.close_in in_channel; + Sys.remove tmp; + if retcode = 0 + then + Ok () + else + Error + (Format.sprintf + "Command `%s' failed. \ + Try to launch the configuration process again." + cmd) + with Stdlib.Not_found | End_of_file | Failure _ | Sys_error _ -> + Error "Unexpected failure." + +let check_compatibility solver model = + let module S = (val solver: Solver.S) in + if S.is_model_format_supported model.Model.format + then Ok () + else + Error + (Format.asprintf + "No support yet for `%s' and model format `%s'." + S.name + (Model.show_format model.format)) + +let tasks_of_property solver { hypothesis; goals } = + let module S = (val solver: Solver.S) in + let table = Hashtbl.create (module String) in + List.iter + goals + ~f:(fun { name; formula; } -> + let elts = S.(repr (translate ~hypothesis ~goal:formula)) in + let tasks = + List.mapi + elts + ~f:(fun idx elt -> + { id = Format.sprintf "%s.%d" name idx; + goalname = name; + subgoal = idx; + formula = Format.asprintf "%a" S.pretty elt; + result = None; }) + in + Hashtbl.set table ~key:name ~data:tasks); + table + +let build_command ~raw_options solver config model task = + let open Result in + let module S = (val solver: Solver.S) in + (* Write property to temporary file. *) + begin + try + let tmp = Filename.temp_file "prop" S.name in + let out_channel = Stdlib.open_out tmp in + let fmt = Format.formatter_of_out_channel out_channel in + Format.fprintf fmt "%s@?" task.formula; + Stdlib.close_out out_channel; + Ok tmp + with Sys_error e -> + Error (Format.sprintf "Unexpected failure:@ %s." e) + end >>= fun property -> + (* Build actual command-line. *) + try + let log = Filename.temp_file "log" S.name in + let output = Filename.temp_file "output" S.name in + let options = S.options ~model:model.Model.filename ~property ~output in + let command = + let args = options @ raw_options in + Filename.quote_command ~stdout:log ~stderr:log config.Config.exe args + in + Ok (command, property, log, output) + with Failure e -> + Error (Format.sprintf "Unexpected failure:@ %s." e) + +let extract_result solver ~log ~output = + let module S = (val solver: Solver.S) in + Logs.info (fun m -> m "Extracting result of verification."); + let output = + let content file = + let in_channel = Stdlib.open_in file in + let buf = Buffer.create 512 in + try + while true do + Stdlib.Buffer.add_channel buf in_channel 512; + done; + assert false + with End_of_file -> + Stdlib.close_in in_channel; + buf + in + let output = content output in + if Buffer.length output > 0 + then output + else content log + in + let output = Buffer.contents output in + let results = [ Solver.Result.Valid; Invalid; Timeout; Unknown ] in + List.fold + results + ~init:Solver.Result.Failure + ~f:(fun acc result -> + let regexp = S.re_of_result result in + let found = + try Str.search_forward regexp output 0 >= 0 + with Stdlib.Not_found -> false + in + if found + then result + else acc) + +let exec_tasks ~raw_options solver config model tasks_htbl = + let module S = (val solver: Solver.S) in + let open Result in + Logs.app (fun m -> m "Launching verification with `%s'." S.name); + (* Run one task at a time and modify its status. *) + Hashtbl.map_inplace + tasks_htbl + ~f:(fun tasks -> + List.map + tasks + ~f:(fun task -> + Logs.app (fun m -> + m "Verifying goal `%s', subgoal %d (%s)." + task.goalname task.subgoal task.id); + let res_exec_task = + (* Build the required command-line. *) + build_command ~raw_options solver config model task + >>= fun (cmd, prop, log, output) -> + (* Execute the command. *) + Logs.info (fun m -> m "Executing command `%s'." cmd); + try + let retcode = Sys.command cmd in + Sys.remove prop; + if retcode <> 0 + then Error (Format.sprintf "Command `%s' has failed." cmd) + else begin + (* Extract and pretty-print verification result. *) + let result = extract_result solver ~log ~output in + Logs.app (fun m -> m "Result %s: %a." + task.id Solver.Result.pretty result); + Sys.remove output; + if Solver.Result.equal result Solver.Result.Failure + then + Logs.app (fun m -> + m "See error messages in `%s' for more information." + log) + else + Sys.remove log; + Ok result + end + with _ -> + Error "Unexpected failure." + in + { task with result = Some res_exec_task; })); + tasks_htbl + +let consolidate_task_results solver tasks_htbl = + let module S = (val solver: Solver.S) in + Hashtbl.map + tasks_htbl + ~f:(fun tasks -> + let consolidated_result = + List.fold tasks ~init:None + ~f:(fun accum { result; _ } -> + let result = + match result with + | None -> None + | Some Ok result -> Some result + | Some Error _ -> Some Failure + in + match accum, result with + | _, None -> accum + | None, result -> result + | Some r1, Some r2 -> Some (S.consolidate r1 r2)) + in + consolidated_result, tasks) + +let pretty_summary tasks_htbl = + let pretty_task_status fmt { id; result; _ } = + let result = Option.value_exn result in + let result_string = + match result with + | Ok r -> Format.asprintf "%a" Solver.Result.pretty r + | Error msg -> msg + in + Format.fprintf fmt "@[-- %s: @[<hov>%s@]@]" id result_string + in + let pretty_goal_tasks fmt (goalname, (consolidated_result, tasks)) = + Format.fprintf fmt "@[- %s: @[<v>%a@ %a@]@]" + goalname + Solver.Result.pretty (Option.value_exn consolidated_result) + (Format.pp_print_list ~pp_sep:Format.pp_print_space pretty_task_status) + tasks + in + Logs.app (fun m -> + m "@[<v 2>Summary:@ %a@]" + (Format.pp_print_list ~pp_sep:Format.pp_print_space pretty_goal_tasks) + (Hashtbl.to_alist tasks_htbl)) + +let exec ~raw_options solver config model property = + let open Result in + let module S = (val solver: Solver.S) in + (* Check solver availability. *) + check_availability solver config >>= fun () -> + (* Check solver and model compatibility. *) + check_compatibility solver model >>= fun () -> + (* Translate property into tasks. *) + tasks_of_property solver property |> + (* Run verification process. *) + exec_tasks ~raw_options solver config model |> + (* Consolidate task results for every goal. *) + consolidate_task_results solver |> + (* Pretty print a summary on results. *) + pretty_summary |> fun () -> + Ok () diff --git a/engine.mli b/engine.mli new file mode 100644 index 0000000..ad36710 --- /dev/null +++ b/engine.mli @@ -0,0 +1,11 @@ +(**************************************************************************) +(* *) +(* This file is part of Caisar. *) +(* *) +(**************************************************************************) + +(** [exec ~raw_options config model property] runs a solver wrt the given + [config] on [property] for [model] with the provided [raw_options]. *) +val exec: + raw_options:string list -> + Solver.instance -> Config.t -> Model.t -> Property.t -> (unit, string) Result.t diff --git a/main.ml b/main.ml index 6cdafd5..425cc9d 100644 --- a/main.ml +++ b/main.ml @@ -6,6 +6,7 @@ open Base open Cmdliner + module Format = Caml.Format let caisar = "caisar" @@ -50,23 +51,24 @@ let config cmd detect file () = (fun m -> m "Command `%s' with configuration file `%s'." cmd file); if detect then begin - match Solver.detect ~file with + match Config.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_file model property solver () = + let module S = (val solver: Solver.S) in Logs.debug (fun m -> m "Command `%s' with@ model `%s',@ property `%s' and@ solver `%s' \ (using configuration file `%s')." - cmd model property (Solver.show_solver solver) config_file); + cmd model property S.name 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 ~file:config_file >>= fun config -> - Solver.exec ~raw_options config model property + Config.retrieve ~file:config_file solver >>= fun config -> + Engine.exec ~raw_options solver config model property in match do_verify () with | Ok () -> Logs.app (fun m -> m "Done.") @@ -95,7 +97,7 @@ let config_cmd = let doc = Format.sprintf "$(b,FILE) to write the %s configuration to." caisar in - Arg.(value & pos 0 string Solver.default_config_file + Arg.(value & pos 0 string Config.default_config_file & info [] ~docv:docv_filename ~doc) in let doc = Format.sprintf "%s configuration." caisar in @@ -120,7 +122,11 @@ let verify_cmd = let docv_solver = "SOLVER" in let solver = let solver_names = - List.map ~f:(fun s -> String.lowercase (Solver.show s)) Solver.defaults + List.map + Config.default_solvers + ~f:(fun solver -> + let module S = (val solver: Solver.S) in + String.lowercase S.name) in let doc = Format.asprintf @@ -134,11 +140,13 @@ let verify_cmd = let solvers = Arg.enum (List.map - ~f:(fun s -> String.lowercase (Solver.show_solver s), s) - Solver.defaults) + Config.default_solvers + ~f:(fun solver -> + let module S = (val solver: Solver.S) in + String.lowercase S.name, solver)) in Arg.(required - & pos 2 (some solvers) (Some Solver.Pyrat) + & pos 2 (some solvers) (Some (module Pyrat: Solver.S)) & info [] ~docv:docv_solver ~doc) in let docv_model = "MODEL" in @@ -158,7 +166,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_file + Arg.(value & opt file Config.default_config_file & info ["c"; "config"] ~doc) in let doc = "Property verification of neural networks." in diff --git a/marabou.ml b/marabou.ml new file mode 100644 index 0000000..1fbe98f --- /dev/null +++ b/marabou.ml @@ -0,0 +1,114 @@ +(**************************************************************************) +(* *) +(* This file is part of Caisar. *) +(* *) +(**************************************************************************) + +open Property_types + +type t = Property.disjunct Property.nform + +let name = "Marabou" +let exe_name = "Marabou" +let exe_default_option = "--version" +let re_version = Str.regexp "[0-9]\\(\\.[0-9]\\)*\\(\\.?[A-Za-z-+]\\)*" + +let re_of_result result = + Str.regexp + (match result with + | Solver.Result.Valid -> "\\(s\\|S\\)at\\|SAT" + | Invalid -> "\\(u\\|U\\)nsat\\|UNSAT" + | Timeout -> "\\(t\\|T\\)imeout\\|TIMEOUT" + | Unknown -> "\\(u\\|U\\)nknown\\|UNKNOWN" + | Failure -> assert false) + +let is_model_format_supported = function + | Model.Nnet _ -> true + | Onnx -> false + +let options ~model ~property ~output = + let verbosity_level = + match Logs.level () with + | None | Some (App | Error | Warning) -> 0 + | Some Info -> 1 + | Some Debug -> 2 + in + [ "--property"; property; + "--input"; model; + "--verbosity"; Int.to_string verbosity_level; + "--summary-file"; output ] + +let consolidate r1 r2= + match r1, r2 with + | _, Solver.Result.Valid | Solver.Result.Valid, _ -> Solver.Result.Valid + | Invalid, result | result, Invalid -> result + | Failure, _ | _, Failure -> Failure + | Timeout, _ | _, Timeout -> Timeout + | Unknown, Unknown -> Unknown + +(* Transform [k_y_0 + ky_1 + ... + ky_n cop ky_n+1] into [ky_n+1 + k_y_0 + + ky_1 + ... + ky_n cop 0], taking care of the sign. *) +let rec constant_rhs formula = + match formula with + | Constraint (terms, cop, General (sign_opt, cst_opt, var)) -> + let terms = + match sign_opt, cst_opt with + | _, Some Int i when i = 0 -> terms + | _, Some Float f when Base.Float.(=) f 0. -> terms + | (None | Some Plus), None -> + General (Some Minus, None, var) :: terms + | (None | Some Plus), Some cst -> + let negcst = + match cst with + | Int i -> Int (~- i) + | Float f -> Float (~-. f) + in + General (Some Plus, Some negcst, var) :: terms + | Some Minus, _ -> + General (Some Plus, cst_opt, var) :: terms + in + Constraint (terms, cop, Constant (Int 0)) + | Constraint (_, _, Constant _) -> + formula + | And (p1, p2) -> + And (constant_rhs p1, constant_rhs p2) + | Or (p1, p2) -> + Or (constant_rhs p1, constant_rhs p2) + +let translate ~hypothesis ~goal = + let goal = constant_rhs goal in + Property.dnf_of_formula (And (hypothesis, goal)) + +let repr = Property.get_disjuncts + +let pretty fmt t = + let pp_terms fmt terms = + match terms with + | [] -> + assert false (* Terms are non-empty lists by construction. *) + | [ t ] -> + Property.pretty_term fmt t + | t :: tt -> + let pp_term = Property.pretty_term ~pp_sign:true in + Format.fprintf fmt "@[%a %a@]" + pp_term t + (Format.pp_print_list + ~pp_sep:(fun fmt () -> Format.pp_print_char fmt ' ') + pp_term) tt + in + let rec pp_formula fmt formula = + match formula with + | Constraint (terms, cop, term) -> + Format.fprintf fmt "%a %a %a" + pp_terms terms + Property.pretty_cop cop + (Property.pretty_term ~pp_sign:true) term + | And (p1, p2) -> + Format.fprintf fmt "%a@\n%a" pp_formula p1 pp_formula p2 + | Or _ -> + (* Marabou does not handle Or connectives directly. Anyway, it should + not happen as in [translate] we convert the original goal into a + dnf-like form where we retrieve only the final conjuncts. *) + assert false + in + Format.fprintf fmt "@[%a@]" pp_formula t diff --git a/property_syntax.mli b/marabou.mli similarity index 62% rename from property_syntax.mli rename to marabou.mli index c81a3ea..8cf7a73 100644 --- a/property_syntax.mli +++ b/marabou.mli @@ -4,17 +4,4 @@ (* *) (**************************************************************************) -type var = - | Input of string - | Output of string - -type cop = Eq | Ge | Le | Gt | Lt - -type cst = - | Int of int - | Float of float - -type t = - | Atomic_cst of var * cop * cst - | Atomic_var of var * cop * var - | And of t * t +include Solver.S diff --git a/model.ml b/model.ml index a9d8bdc..528ec20 100644 --- a/model.ml +++ b/model.ml @@ -5,6 +5,7 @@ (**************************************************************************) open Base + module Format = Caml.Format module Sys = Caml.Sys module Filename = Caml.Filename diff --git a/property.ml b/property.ml index f677587..c749d47 100644 --- a/property.ml +++ b/property.ml @@ -5,66 +5,64 @@ (**************************************************************************) open Base +open Property_types + module Format = Caml.Format module Sys = Caml.Sys +module Lexer = Property_lexer +module Parser = Property_parser +module MI = Parser.MenhirInterpreter +module ER = MenhirLib.ErrorReports +module LU = MenhirLib.LexerUtil -type t = Property_syntax.t - -let do_build ~property line = - let linebuf = Lexing.from_string line in - try - let p = Property_parser.main Property_lexer.token linebuf in - let property = - Option.value_map - property - ~default:p - ~f:(fun p' -> Property_syntax.And (p', p)) - in - Ok property +type t = Property_types.t +type 'a printer = Caml.Format.formatter -> 'a -> unit + +(* [show text (pos1, pos2)] displays a range of the input text [text] + delimited by the positions [pos1] and [pos2]. *) +let show text positions = + ER.extract text positions + |> ER.sanitize + |> ER.compress + |> ER.shorten 20 (* max width 43 *) + +(* The parser has succeeded and produced a property. *) +let succeed (property: t) = + Ok property + +(* The parser has encountered a syntax error. *) +let fail text buffer (_ : t MI.checkpoint) = + (* Indicate where in the input file the error occurred. *) + let location = LU.range (ER.last buffer) in + (* Show the tokens just before and just after the error. *) + let indication = + Format.sprintf "Syntax error %s." (ER.show (show text) buffer) + in + (* Show these three components. *) + Error (Format.sprintf "%s%s" location indication) + +let build ~filename = + (* Read the file; allocate and initialize a lexing buffer. *) + let text, lexbuf = LU.read filename in + (* Wrap the lexer and lexbuf together into a supplier, that is, a + function of type [unit -> token * position * position]. *) + let supplier = MI.lexer_lexbuf_to_supplier Lexer.token lexbuf in + (* Equip the supplier with a two-place buffer that records the positions + of the last two tokens. This is useful when a syntax error occurs, as + these are the token just before and just after the error. *) + let buffer, supplier = ER.wrap_supplier supplier in + (* Fetch the parser's initial checkpoint. *) + let checkpoint = Parser.Incremental.main lexbuf.lex_curr_p in + (* Run the parser. *) + try MI.loop_handle succeed (fail text buffer) supplier checkpoint with | Property_lexer.Error msg -> Error (Format.sprintf "%s" msg) - | Property_parser.Error -> - Error - (Format.sprintf - "Property syntax error at offset %d." - (Lexing.lexeme_start linebuf)) -let build ~filename = - if Sys.file_exists filename - then - let in_channel = Stdlib.open_in filename in - let rec read_one_line ~property channel = - let line_opt, continue = Property_lexer.line channel in - if not continue - then begin - Stdlib.close_in in_channel; - Result.of_option - property - ~error:(Format.sprintf "No property found in `%s'." filename) - end - else begin - let property_or_error = - (* Since [continue] is true, there is a line by lexer construction. *) - let line = Option.value_exn line_opt in - do_build ~property line - in - Result.join - (Result.map - property_or_error - ~f:(fun p -> read_one_line ~property:(Some p) channel)) - end - in - read_one_line ~property:None (Lexing.from_channel in_channel) - else - Error (Format.sprintf "No such file `%s'." filename) - -let rec repr t = - let open Property_syntax in - let string_of_var = function - | Input s -> Format.sprintf "Input %s" s - | Output s -> Format.sprintf "Output %s" s - in + +(* Printing. *) + +let pretty_cop fmt cop = let string_of_cop = function | Eq -> "=" | Ge -> ">=" @@ -72,23 +70,134 @@ let rec repr t = | Gt -> ">" | Lt -> "<" in - let string_of_cst = function - | Int i -> Format.sprintf "int %d" i - | Float f -> Format.sprintf "float %f" f - in - match t with - | Atomic_cst (var, cop, cst) -> - let var_s = string_of_var var in - let cop_s = string_of_cop cop in - let cst_s = string_of_cst cst in - Format.sprintf "%s %s %s" var_s cop_s cst_s - | Atomic_var (var1, cop, var2) -> - let var1_s = string_of_var var1 in - let cop_s = string_of_cop cop in - let var2_s = string_of_var var2 in - Format.sprintf "%s %s %s" var1_s cop_s var2_s + Format.pp_print_string fmt (string_of_cop cop) + +let pretty_var fmt = function + | Input s | Output s -> Format.pp_print_string fmt s + +let pretty_cst fmt = function + | Int i -> Format.fprintf fmt "%d" i + | Float f -> Format.fprintf fmt "%f" f + +let pretty_term ?(pp_sign=false) fmt = function + | Constant cst -> + pretty_cst fmt cst + | General (sign_opt, cst_opt, var) -> + begin match sign_opt, cst_opt with + | _, Some Int i when i = 0 -> () + | _, Some Float f when Float.(=) f 0. -> () + | (None | Some Plus), None -> + Format.fprintf fmt "@[%s%a@]" + (if not pp_sign then "" else "+") + pretty_var var + | (None | Some Plus), Some cst -> + let is_cst_negative = + match cst with + | Int i -> Int.is_negative i + | Float f -> Float.is_negative f + in + Format.fprintf fmt "@[%s%a%a@]" + (if not pp_sign || is_cst_negative then "" else "+") + pretty_cst cst + pretty_var var + | Some Minus, None -> + Format.fprintf fmt "@[-%a@]" pretty_var var + | Some Minus, Some cst -> + let negcst, is_negcst_negative = + match cst with + | Int i -> Int (~- i), i > 0 + | Float f -> Float (~-. f), Float.(>) f 0. + in + Format.fprintf fmt "@[%s%a%a@]" + (if not pp_sign || is_negcst_negative then "" else "+") + pretty_cst negcst + pretty_var var + end + +let pretty_terms pp_term fmt terms = + match terms with + | [] -> + assert false (* Terms are non-empty lists by construction. *) + | [ t ] -> + pp_term fmt t + | t :: tt -> + Format.fprintf fmt "@[%a %a@]" + pp_term t + (Format.pp_print_list + ~pp_sep:(fun fmt () -> Format.pp_print_char fmt ' ') + pp_term) tt + +let rec pretty_formula fmt formula = + match formula with + | Constraint ([], _, _) -> + (* Non-empty list by construction. *) + assert false + | Constraint (terms, cop, term) -> + Format.fprintf fmt "@[%a %a %a@]" + (pretty_terms pretty_term) terms + pretty_cop cop + (pretty_term ~pp_sign:false) term | And (p1, p2) -> - Format.sprintf "And (%s, %s)" (repr p1) (repr p2) + Format.fprintf fmt "@[%a@ &&@ %a@]" pretty_formula p1 pretty_formula p2 + | Or (p1, p2) -> + Format.fprintf fmt "@[%a@ ||@ %a@]" pretty_formula p1 pretty_formula p2 + +let pretty_goal fmt goal = + Format.fprintf fmt "@[<v 2>goal %s:@ @[<hov>%a@]@]" + goal.name + pretty_formula goal.formula let pretty fmt t = - ignore (fmt, t) + Format.fprintf fmt "@[%a@]@\n@[<v>%a@]" + pretty_formula t.hypothesis + (Format.pp_print_list pretty_goal) t.goals + + +(* Normal forms. *) + +type conjunct = formula +type disjunct = formula +type 'a nform = formula list + +let combine ~f f1 f2 = + if List.is_empty f1 || List.is_empty f2 + then assert false + else begin + let rec loop (l1: formula list) l2 acc = + match l1 with + | [] -> + acc + | t :: tt -> + loop tt l2 List.(rev_append (map ~f:(fun x -> f t x) l2) acc) + in + List.rev (loop f1 f2 []) + end + +let rec convert_to_nf ~and_ ~or_ formula = + match formula with + | Constraint _ -> + [ formula ] + | And (p1, p2) -> + and_ (convert_to_nf ~and_ ~or_ p1) (convert_to_nf ~and_ ~or_ p2) + | Or (p1, p2) -> + or_ (convert_to_nf ~and_ ~or_ p1) (convert_to_nf ~and_ ~or_ p2) + +let cnf_of_formula (formula: formula) = + let and_ = function + | [] -> assert false + | [ _ ] as f -> f + | t :: tt -> [ List.fold tt ~init:t ~f:(fun acc f -> And (acc, f)) ] + in + convert_to_nf + ~and_:(fun l1 l2 -> and_ (List.append l1 l2)) + ~or_:(combine ~f:(fun x y -> Or (x, y))) + formula + +let dnf_of_formula (formula: formula) = + convert_to_nf + ~and_:(combine ~f:(fun x y -> And (x, y))) + ~or_:List.append + formula + +let get_conjuncts = Fn.id +let get_disjuncts = Fn.id diff --git a/property.mli b/property.mli index 34a1ee6..56dc81f 100644 --- a/property.mli +++ b/property.mli @@ -5,15 +5,47 @@ (**************************************************************************) open Base +open Property_types -(** A property. *) -type t = Property_syntax.t +type t = Property_types.t (** Builds a property out of the content of the given [filename], if possible. *) val build: filename:string -> (t, string) Result.t -(** Provides the raw representation of the given property. *) -val repr: t -> string +(** {2 Pretty printing.} *) + +type 'a printer = Caml.Format.formatter -> 'a -> unit + +(** Comparison operator pretty printer. *) +val pretty_cop: cop printer + +(** Variable pretty printer. *) +val pretty_var: var printer + +(** Constant pretty printer. *) +val pretty_cst: constant printer + +(** Term pretty printer. + @param pp_sign determines whether the printer should print the sign also + for monomial terms. +*) +val pretty_term: ?pp_sign:bool -> term printer + +(** Term list pretty printer given one for a term. *) +val pretty_terms: term printer -> term list printer (** [pretty property] pretty prints [property]. *) val pretty: Caml.Format.formatter -> t -> unit + + +(** {2 Normal forms.} *) + +type conjunct +type disjunct +type 'a nform + +val cnf_of_formula: formula -> conjunct nform +val dnf_of_formula: formula -> disjunct nform + +val get_conjuncts: conjunct nform -> formula list +val get_disjuncts: disjunct nform -> formula list diff --git a/property_lexer.mll b/property_lexer.mll index a08e5fb..e066004 100644 --- a/property_lexer.mll +++ b/property_lexer.mll @@ -11,35 +11,49 @@ let input = 'x'['0'-'9']+ let output = 'y'['0'-'9']+ -(* This rule looks for a single line, terminated with '\n' or eof. It returns a - pair of an optional string, the line that was found, and a Boolean flag, - false if eof was reached. *) - -rule line = parse -| ([^'\n']* '\n') as line - (* Normal case: one line, no eof. *) - { Some line, true } -| eof - (* Normal case: no data, eof. *) - { None, false } -| ([^'\n']+ as line) eof - (* Special case: some data but missing '\n', then eof. - Consider this as the last line, and add the missing '\n'. *) - { Some (line ^ "\n"), false } +(* Keywords. *) + +let goal = "goal" + +(* Macros. *) + +let space = [' ' '\t' '\r'] + +let dec = ['0'-'9'] +let dec_sep = ['0'-'9' '_'] + +let lalpha = ['a'-'z'] +let ualpha = ['A'-'Z'] +let alpha = (lalpha | ualpha) -(* This rule analyzes a single line and turns it into a stream of tokens. *) +let suffix = (alpha | dec_sep)* +let ident = alpha suffix -and token = parse + +(* This rule analyzes multiple lines until eof and turns them into a stream of + tokens. *) + +rule token = parse | [' ' '\t' '\r'] { token lexbuf } +| '\n' + { MenhirLib.LexerUtil.newline lexbuf; token lexbuf } | input as i { INPUT i } | output as o { OUTPUT o } -| '-'?['0'-'9']+'.'['0'-'9']+ as f +| goal + { GOAL } +| (ident as gid) space* ":" + { GOALID gid } +| ('-'|'+')?['0'-'9']+'.'['0'-'9']+ as f { FLOAT (float_of_string f) } -| '-'?['0'-'9']+ as i +| ('-'|'+')?['0'-'9']+ as i { INT (int_of_string i) } +| "+" + { PLUS } +| "-" + { MINUS } | "=" { EQ } | ">" @@ -50,7 +64,15 @@ and token = parse { GE } | "<=" { LE } -| '\n' - { EOL } +| "&&" + { AND } +| "||" + { OR } +| '(' + { LPAREN } +| ')' + { RPAREN } +| eof + { EOF } | _ - { raise (Error (Printf.sprintf "Property lexer at offset %d: unexpected character.\n" (Lexing.lexeme_start lexbuf))) } + { raise (Error (Format.sprintf "Property lexer at offset %d: unexpected character." (Lexing.lexeme_start lexbuf))) } diff --git a/property_parser.mly b/property_parser.mly index 6cf5926..6bdb619 100644 --- a/property_parser.mly +++ b/property_parser.mly @@ -8,49 +8,103 @@ %token <int> INT %token <float> FLOAT -%token <string> INPUT OUTPUT +%token <string> INPUT OUTPUT GOALID %token EQ GT LT GE LE -%token EOL +%token PLUS MINUS +%token AND OR +%token LPAREN RPAREN +%token GOAL +%token EOF (* Starting rule. *) -%start <Property_syntax.t> main -%{ open Property_syntax %} +%start <Property_types.t> main +%{ open Property_types %} %% -(* Rules. *) - -main: -| p = property EOL - { p } - -property: -| var cop cst - { Atomic_cst ($1, $2, $3) } -| var cop var - { Atomic_var ($1, $2, $3) } - -var: -| i = INPUT - { Input i } -| o = OUTPUT - { Output o } - -cst: -| i = INT - { Int i } -| f = FLOAT - { Float f } - -cop: -| EQ - { Eq } -| GE - { Ge } -| LE - { Le } -| GT - { Gt } -| LT - { Lt } +(* -------------------------------------------------------------------------- *) + +(* We parse a property given by + a hypothesis + a non-empty list of goals + followed by an end-of-file. *) + +let main := + ~ = hypothesis; goals = nonempty_list(goal); EOF; { { hypothesis; goals; } } + +(* A hypothesis is a conjunctive formula over constraints. *) + +let hypothesis == + and_formula(constraint_) + +(* A goal has a name and a formula. *) + +let goal := + GOAL; name = GOALID; ~ = formula; { { name; formula; } } + +(* A formula is given by disjunctions of conjuctions of (delimited) formulae. *) + +let formula == + or_formula(and_formula(delimited_formula)) + +(* A disjunctive formula is a left-associative list of formulae, separated by OR + (i.e. ||) connective. *) + +let or_formula(elem) := + | elem + | ors = or_formula(elem); OR; ~ = elem; { Or (ors, elem) } + +(* A conjunctive formula is a left-associative list of formulae, separated by + AND (i.e. &&) connective. *) + +let and_formula(elem) := + | elem + | ands = and_formula(elem); AND; ~ = elem; { And (ands, elem) } + +(* A delimited formula is + either a constraint + or a parenthesized formula. *) + +let delimited_formula := + | constraint_ + | delimited(LPAREN, formula, RPAREN) + +(* A constraint is the application of a comparison operator to a non-empty list + of terms and a constant or a term. *) + +let constraint_ := + | terms = nonempty_list(term); ~ = cop; ~ = constant; { Constraint (terms, cop, Constant constant) } + | terms = nonempty_list(term); ~ = cop; ~ = term; { Constraint (terms, cop, term) } + +(* A term is a constant coupled with a variable, possibly with a sign. *) + +let term := + s = ioption(sign); c = ioption(constant); ~ = variable; { General (s, c, variable) } + +(* A constant is an integer or float value. *) + +let constant := + | ~ = INT; <Int> + | ~ = FLOAT; <Float> + +(* A variable is either input or output. *) + +let variable := + | ~ = INPUT; <Input> + | ~ = OUTPUT; <Output> + +(* Signs. *) + +let sign == + | PLUS; { Plus } + | MINUS; { Minus } + +(* Comparison operators. *) + +let cop == + | EQ; { Eq } + | GE; { Ge } + | LE; { Le } + | GT; { Gt } + | LT; { Lt } diff --git a/property_types.mli b/property_types.mli new file mode 100644 index 0000000..dd315b2 --- /dev/null +++ b/property_types.mli @@ -0,0 +1,35 @@ +(**************************************************************************) +(* *) +(* This file is part of Caisar. *) +(* *) +(**************************************************************************) + +type cop = Eq | Ge | Le | Gt | Lt +type sign = Plus | Minus + +type constant = + | Int of int + | Float of float + +type var = + | Input of string + | Output of string + +type term = + | Constant of constant + | General of sign option * constant option * var + +type formula = + | Constraint of term list * cop * term + | And of formula * formula + | Or of formula * formula + +type goal = { + name: string; + formula: formula; +} + +type t = { + hypothesis: formula; + goals: goal list; +} diff --git a/pyrat.ml b/pyrat.ml new file mode 100644 index 0000000..7ad8404 --- /dev/null +++ b/pyrat.ml @@ -0,0 +1,55 @@ +(**************************************************************************) +(* *) +(* This file is part of Caisar. *) +(* *) +(**************************************************************************) + +open Property_types + +type t = Property.conjunct Property.nform + +let name = "PyRAT" +let exe_name = "pyrat.py" +let exe_default_option = "-v" +let re_version = Str.regexp "[0-9]\\(\\.[0-9]\\)*\\(\\.?[A-Za-z-+]\\)*" + +let re_of_result result = + Str.regexp + (match result with + | Solver.Result.Valid -> "True" + | Invalid -> "False" + | Timeout -> "Timeout" + | Unknown -> "Unknown" + | Failure -> assert false) + +let is_model_format_supported = function + | Model.Nnet _ -> true + | Onnx -> false + +let options ~model ~property ~output:_ = + model :: property :: + match Logs.level () with + | None | Some (App | Error | Warning) -> [] + | Some (Info | Debug) -> [ "--verbose" ] + +let consolidate _r1 _r2 = assert false + +let translate ~hypothesis ~goal = + Property.cnf_of_formula (And (hypothesis, goal)) + +let repr = Property.get_conjuncts + +let pretty fmt t = + let rec pp_formula fmt formula = + match formula with + | Constraint (terms, cop, term) -> + Format.fprintf fmt "%a %a %a" + Property.(pretty_terms (pretty_term ~pp_sign:false)) terms + Property.pretty_cop cop + (Property.pretty_term ~pp_sign:false) term + | And (p1, p2) -> + Format.fprintf fmt "%a@\n%a" pp_formula p1 pp_formula p2 + | Or (p1, p2) -> + Format.fprintf fmt "%a or %a" pp_formula p1 pp_formula p2 + in + Format.fprintf fmt "@[%a@]" pp_formula t diff --git a/pyrat.mli b/pyrat.mli new file mode 100644 index 0000000..8cf7a73 --- /dev/null +++ b/pyrat.mli @@ -0,0 +1,7 @@ +(**************************************************************************) +(* *) +(* This file is part of Caisar. *) +(* *) +(**************************************************************************) + +include Solver.S diff --git a/solver.ml b/solver.ml index 858deba..6b7dc37 100644 --- a/solver.ml +++ b/solver.ml @@ -5,480 +5,38 @@ (**************************************************************************) open Base -module Format = Caml.Format -module Sys = Caml.Sys -module Filename = Caml.Filename - -(* Supported solvers. *) -type solver = - | Pyrat - | Marabou - [@@deriving show { with_path = false }, ord, eq, yojson] - - -(* Utilities. *) - -let defaults = [ Pyrat; Marabou ] - -let rec pp_property_marabou property = - let open Property_syntax in - let string_of_cop = function - | Eq -> "=" - | Ge -> ">=" - | Le -> "<=" - | Gt -> ">" - | Lt -> "<" - in - let string_of_cst = function - | Int i -> Stdlib.string_of_int i - | Float f -> Stdlib.string_of_float f - in - match property with - | Atomic_cst ((Input v | Output v), cop, cst) -> - Format.sprintf "%s %s %s" v (string_of_cop cop) (string_of_cst cst) - | Atomic_var ((Input v1 | Output v1), cop, (Input v2 | Output v2)) -> - Format.sprintf "%s %s %s" v1 (string_of_cop cop) v2 - | And (p1, p2) -> - let s1 = pp_property_marabou p1 in - let s2 = pp_property_marabou p2 in - Format.sprintf "%s\n%s" s1 s2 - -let pp_property_pyrat = pp_property_marabou - -let pp_property solver property = - match solver with - | Pyrat -> - Ok (pp_property_pyrat property) - | Marabou -> - Ok (pp_property_marabou property) - -let default_exe_name_of_solver = function - | Pyrat -> "pyrat.py" - | Marabou -> "Marabou" - -let default_option_of_solver = function - | Pyrat -> "-v" - | Marabou -> "--version" - -let exe_path_of_solver solver = - let exe = default_exe_name_of_solver solver in - (* We want the complete path of [exe] in $PATH: we use `command -v [exe]' - and retrieve the result, if any. *) - let tmp = Filename.temp_file "caisar" "command" in - let _retcode = - Sys.command - (Filename.quote_command - ~stdout:tmp ~stderr:tmp - "command" ["-v"; exe]) - in - let in_channel = Stdlib.open_in tmp in - let found, exe = - try true, Stdlib.input_line in_channel - with End_of_file -> false, exe - in - Stdlib.close_in in_channel; - Sys.remove tmp; - if found - then Ok exe - else begin - match Sys.getenv_opt "DIR" with - | None -> - Error - (Format.asprintf "Cannot find the executable of `%a'." pp_solver solver) - | Some dir -> - (* The env variable should already provide the path to the executable: we - first check that the path exists, refers to a directory and it is - absolute, then we concatenate the [exe] name to it. *) - if not (Sys.file_exists dir && Sys.is_directory dir) - then - Error (Format.sprintf "`%s' not exist or is not a directory." dir) - else - if Filename.is_relative dir - then - Error - (Format.sprintf - "`%s' is a relative path. An absolute path is required for DIR." - dir) - else - let exe = - if Filename.check_suffix dir Filename.dir_sep - then dir ^ exe - else Format.sprintf "%s%s%s" dir Filename.dir_sep exe - in - Ok exe - end - -let string_valid_of_solver = function - | Pyrat -> "True" - | Marabou -> "\\(s\\|S\\)at\\|SAT" - -let string_invalid_of_solver = function - | Pyrat -> "False" - | Marabou -> "\\(u\\|U\\)nsat\\|UNSAT" - -let string_timeout_of_solver = function - | Pyrat -> "Timeout" - | Marabou -> "\\(t\\|T\\)imeout\\|TIMEOUT" - -let string_unknown_of_solver = function - | Pyrat -> "Unknown" - | Marabou -> "\\(u\\|U\\)nknown\\|UNKNOWN" - - -(* Configuration. *) - -type version = - | Unknown - | Version of string -[@@deriving show { with_path = false }, yojson] +open Property_types -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 = - (* We use same pattern to extract solver version numbers for the moment. *) - match solver with - | Pyrat | Marabou -> - (* We want to match for version string of the form '0.0.1alpha+'. *) - "[0-9]\\(\\.[0-9]\\)*\\(\\.?[A-Za-z-+]\\)*" - in - let regexp_version = Str.regexp regexp_string in - try - (* We suppose that the first occurrence that matches [regexp_string] is - indeed the version of the concercend [solver]. *) - ignore (Str.search_forward regexp_version s 0); - Version (Str.matched_string s) - with Stdlib.Not_found -> - Logs.debug (fun m -> m "No recognizable version found."); - Unknown - -let default_config_file = - let filename = ".caisar.conf" in - match Sys.getenv_opt "HOME" with - | None -> filename - | Some p -> p ^ "/.caisar.conf" - -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 ~file solver = - let open Result in - Logs.info - (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. *) - 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.asprintf - "No solver `%a' found in configuration file `%s'." - pp_solver solver - file) - | Some config -> - begin - Logs.info - (fun m -> m "Found `%a' `%s'." - pp_solver config.solver - (show_version config.version)); - Ok config - end - end - -let create_config_file file = - Logs.debug (fun m -> m "Creating new configuration file `%s'." file); - Stdlib.open_out file - -let detect_default_solvers () = - try - let config = - List.filter_map - ~f:(fun solver -> - match exe_path_of_solver solver with - | Error msg -> - Logs.info (fun m -> m "%s" msg); - None - | Ok exe -> - (* We detect whether solver is available in $PATH, or in the - provided path via env variable, by executing `solver - --version' command. *) - let tmp = Filename.temp_file "caisar" "detect" in - let cmd = - Filename.quote_command - ~stdout:tmp ~stderr:tmp - 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 cmd_output = - let in_channel = Stdlib.open_in tmp in - let buf = Buffer.create 512 in - try - while true do - Stdlib.Buffer.add_channel buf in_channel 512; - done; - assert false - with End_of_file -> - Stdlib.close_in in_channel; - Buffer.contents buf - in - Sys.remove tmp; - if retcode <> 0 - then begin - Logs.info (fun m -> m "No solver `%a' found." pp_solver solver); - None - end - else begin - (* If available, we save a [config] for solver. *) - let version = version_of_solver solver cmd_output in - let config = { solver; exe; version; } in - Logs.app - (fun m -> m "Found solver `%a' `%a' (%s)." - pp_solver solver - pp_version version - exe); - Some config - end) - defaults - in - Ok config - with _ -> - Error "Unexpected failure." - -let detect ~file = - let open Result in - (* Detect default supported solvers in $PATH or $DIR. *) - detect_default_solvers () >>= fun default_full_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 - (* 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 full_config_json = full_config_to_yojson full_config in - Yojson.Safe.pretty_to_channel out_channel full_config_json; - Stdlib.close_out out_channel; - Ok () - - -(* Verification. *) - -(* Verification result. *) -type result = - | Valid - | Invalid - | Timeout - | Unknown - | Failure -[@@deriving show { with_path = false }, eq] - -let check_availability config = - 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.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; - Sys.remove tmp; - if retcode = 0 - then - Ok () - else - Error - (Format.sprintf - "Command `%s' failed. Try to launch the configuration process again." - cmd) - with Stdlib.Not_found | End_of_file | Failure _ | Sys_error _ -> - Error "Unexpected failure." - -let check_compatibility config model = - let solver = config.solver in - match solver, model.Model.format with - | (Pyrat | Marabou), (Model.Onnx as f) -> - Error - (Format.asprintf - "No support yet for `%a' and model format `%s'." - pp_solver solver - (Model.show_format f)) - | _ -> - Ok () - -let build_command ~raw_options confg_solver property model = - let open Result in - let solver = confg_solver.solver in - (* Format property wrt solver. *) - pp_property solver property >>= fun s -> - (* Write property to temporary file. *) - begin - try - let tmp = Filename.temp_file "prop" (show_solver solver) in - let out_channel = Stdlib.open_out tmp in - let fmt = Format.formatter_of_out_channel out_channel in - Format.fprintf fmt "%s@?" s; - Stdlib.close_out out_channel; - Ok tmp - with Sys_error e -> - Error (Format.sprintf "Unexpected failure:@ %s." e) - end >>= fun prop -> - (* Build actual command-line. *) - try - let stdout = Filename.temp_file "output" (show_solver solver) in - let summary = - match solver with - | Pyrat -> None - | Marabou -> Some (Filename.temp_file "summary" (show_solver solver)) - in - let cmd = - let exe = confg_solver.exe in - let args = - match solver with - | Pyrat -> - let verbose = - match Logs.level () with - | None | Some (App | Error | Warning) -> false - | Some (Info | Debug) -> true - in - model.Model.filename :: - prop :: - if verbose then "--verbose" :: raw_options else raw_options - | Marabou -> - let verbosity_level = - match Logs.level () with - | None | Some (App | Error | Warning) -> 0 - | Some Info -> 1 - | Some Debug -> 2 - in - "--property" :: prop :: - "--input" :: model.Model.filename :: - "--verbosity" :: Int.to_string verbosity_level :: - "--summary-file" :: Option.value_exn summary :: - raw_options - in - Filename.quote_command ~stdout ~stderr:stdout exe args - in - Ok (prop, cmd, (stdout, summary)) - with Failure e -> - Error (Format.sprintf "Unexpected failure:@ %s." e) - -let extract_result config stdout summary = - Logs.info (fun m -> m "Extracting result of verification."); - let solver = config.solver in - let output = - let file = - match solver with - | Pyrat -> stdout - | Marabou -> Option.value_exn summary - in - let in_channel = Stdlib.open_in file in - let buf = Buffer.create 512 in - try - while true do - Stdlib.Buffer.add_channel buf in_channel 512; - done; - assert false - with End_of_file -> - Stdlib.close_in in_channel; - Buffer.contents buf - in - let regexps_result = - [ (string_valid_of_solver, Valid); - (string_invalid_of_solver, Invalid); - (string_timeout_of_solver, Timeout); - (string_unknown_of_solver, Unknown); ] - in - List.fold - regexps_result - ~init:Failure - ~f:(fun acc (f, result) -> - let regexp = Str.regexp (f solver) in - let found = - try Str.search_forward regexp output 0 >= 0 - with Stdlib.Not_found -> false - in - if found - then result - else acc) - -let pretty_result fmt result = - Format.fprintf fmt "%s" (String.uppercase (show_result result)) +module Format = Caml.Format -let exec ~raw_options config model property = - let open Result in - (* Check solver availability. *) - check_availability config >>= fun () -> - (* Check solver and model compatibility. *) - check_compatibility config model >>= fun () -> - (* Build the required command-line. *) - build_command ~raw_options config property model - >>= fun (prop, cmd, (stdout, summary)) -> - (* Execute the command. *) - begin - Logs.app (fun m -> - m "Launching verification with `%a'." pp_solver config.solver); - try - Logs.info (fun m -> m "Executing command `%s'." cmd); - let retcode = Sys.command cmd in - Sys.remove prop; - if retcode <> 0 - then - Error (Format.sprintf "Command `%s' failed." cmd) - else begin - (* Extract and pretty-print verification result. *) - let result = extract_result config stdout summary in - Logs.app (fun m -> m "Result: %a." pretty_result result); - if equal_result result Failure - then Logs.app (fun m -> - m "See error messages in `%s' for more information." stdout) - else Sys.remove stdout; - Option.value_map summary ~default:() ~f:Sys.remove; - Ok () - end - with _ -> - Error "Unexpected failure." - end +module Result = struct + type t = + | Valid + | Invalid + | Timeout + | Unknown + | Failure + [@@deriving show { with_path = false }, eq] + + let pretty fmt t = + Format.fprintf fmt "%s" (String.uppercase (show t)) +end + +(* Solver instances. *) + +module type S = sig + type t + val name: string + val exe_name: string + val exe_default_option: string + val re_version: Str.regexp + val re_of_result: Result.t -> Str.regexp + val is_model_format_supported: Model.format -> bool + val options: model:string -> property:string -> output:string -> string list + val consolidate: Result.t -> Result.t -> Result.t + val translate: hypothesis:formula -> goal:formula -> t + val repr: t -> formula list + val pretty: Caml.Format.formatter -> formula -> unit +end + +type instance = (module S) diff --git a/solver.mli b/solver.mli index 6a70c6a..6934a70 100644 --- a/solver.mli +++ b/solver.mli @@ -4,28 +4,60 @@ (* *) (**************************************************************************) -(** Supported solvers. *) -type solver = Pyrat | Marabou [@@deriving show] +open Property_types -(** The list of supported solvers by default. *) -val defaults: solver list +(** Solver possible results. *) +module Result: sig + type t = + | Valid + | Invalid + | Timeout + | Unknown + | Failure + [@@deriving show, eq] -val default_config_file: string + val pretty: Caml.Format.formatter -> t -> unit +end -(** [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 +(** Solvers common interface. *) +module type S = sig + type t -(** Solver configuration. *) -type config + (** The name of the solver. *) + val name: string -(** [get_config ~file solver] retrieves the [solver] configuration, if any, from - the given [file]. *) -val get_config: file:string -> solver -> (config, string) Result.t + (** The executable name. *) + val exe_name: string -(** [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 list -> - config -> Model.t -> Property.t -> (unit, string) Result.t + (** The default option of the executable. Typically, `--version'. *) + val exe_default_option: string + + (** Regexp for extracting the solver version when running the solver on the + default option. *) + val re_version: Str.regexp + + (** @return regexp for extracting the given result from the solver output. *) + val re_of_result: Result.t -> Str.regexp + + (** @return whether the given model format is supported by the solver. *) + val is_model_format_supported: Model.format -> bool + + (** @return solver options to execute the solver on the given [model] and + [property] files, while using [output] as file to store the result. *) + val options: model:string -> property:string -> output:string -> string list + + (** @return consolidated result of the given ones. *) + val consolidate: Result.t -> Result.t -> Result.t + + (** Encodes the given formulae into the solver internal representation. *) + val translate: hypothesis:formula -> goal:formula -> t + + (** Decodes the internal representation into formulae. *) + val repr: t -> formula list + + (** Solver pretty-printing of formulae. *) + val pretty: Caml.Format.formatter -> formula -> unit +end + +(** An actual instance of solver interface [S]. *) +type instance = (module S) -- GitLab