diff --git a/src/dataset.ml b/src/dataset.ml index 6ce481846cd19043eea6f79a4265e965fe684d8c..32dbe5af9dc5adc50546268a5277033e9dd90e9b 100644 --- a/src/dataset.ml +++ b/src/dataset.ml @@ -23,9 +23,27 @@ open Why3 open Base +let real_constant_of_float value = + let neg = Float.is_negative value in + let value = Fmt.str "%.64f" (Float.abs value) in + (* Split into integer and fractional parts. *) + let int_frac = String.split value ~on:'.' in + let int = List.hd_exn int_frac in + let frac = + match List.tl_exn int_frac with + | [] -> "" + | [ s ] -> s + | _ -> assert false (* Since every float has one '.' at most. *) + in + Constant.ConstReal (Number.real_literal ~radix:10 ~neg ~int ~frac ~exp:None) + +let float_of_real_constant rc = + Float.of_string (Fmt.str "%a" Number.(print_real_constant full_support) rc) + type normalization = | MinMax of float * float (* min-max scaling *) | Znorm of float * float (* z-normalization (mean, standard deviation) *) +[@@deriving yojson, show] let require_clipping = function MinMax _ -> true | Znorm _ -> false @@ -45,9 +63,6 @@ let apply_normalization csv ds = class_ :: features) let interpret_normalization = - let float_of_real_constant rc = - Float.of_string (Fmt.str "%a" Number.(print_real_constant full_support) rc) - in Trans.fold_decl (fun decl acc -> match decl.d_node with @@ -83,12 +98,44 @@ let interpret_normalization = | _ -> acc) [] -type eps = Constant.constant +let term_of_normalization env normalization = + let th = Pmodule.read_module env [ "caisar" ] "DataSetClassification" in + let t_dataset = + let ls_dataset = Theory.ns_find_ls th.mod_theory.th_export [ "dataset" ] in + Term.t_app_infer ls_dataset [] + in + let ty = + let th = Env.read_theory env [ "ieee_float" ] "Float64" in + Ty.ty_app (Theory.ns_find_ts th.th_export [ "t" ]) [] + in + match normalization with + | MinMax (min, max) -> + let t_min = Term.t_const (real_constant_of_float min) ty in + let t_max = Term.t_const (real_constant_of_float max) ty in + let ls_min_max_scale = + Theory.ns_find_ls th.mod_theory.th_export [ "min_max_scale" ] + in + Term.t_app_infer ls_min_max_scale [ t_min; t_max; t_dataset ] + | Znorm (mean, std_dev) -> + let t_mean = Term.t_const (real_constant_of_float mean) ty in + let t_std_dev = Term.t_const (real_constant_of_float std_dev) ty in + let ls_z_norm = Theory.ns_find_ls th.mod_theory.th_export [ "z_norm" ] in + Term.t_app_infer ls_z_norm [ t_mean; t_std_dev; t_dataset ] + +type eps = float [@@deriving yojson, show] + +let string_of_eps eps = Float.to_string eps + +let term_of_eps env eps = + let th = Env.read_theory env [ "ieee_float" ] "Float64" in + let ty = Ty.ty_app (Theory.ns_find_ts th.th_export [ "t" ]) [] in + Term.t_const (real_constant_of_float eps) ty type property = | Correct | Robust of eps | CondRobust of eps +[@@deriving yojson, show] type ('a, 'b) predicate = { model : 'a; @@ -97,8 +144,6 @@ type ('a, 'b) predicate = { property : property; } -let string_of_eps eps = Fmt.str "%a" Constant.print_def eps - let string_of_predicate_kind = function | Correct -> "correct" | Robust _ -> "robust" @@ -131,13 +176,14 @@ let interpret_predicate env ~on_model ~on_dataset task = if Term.ls_equal ls correct_predicate then Correct else failwith_unsupported_ls ls - | [ { t_node = Tconst (Constant.ConstReal _ as e); _ } ] -> + | [ { t_node = Tconst (Constant.ConstReal e); _ } ] -> let robust_predicate = find_predicate_ls env "robust" in let cond_robust_predicate = find_predicate_ls env "cond_robust" in + let f = float_of_real_constant e in if Term.ls_equal ls robust_predicate - then Robust e + then Robust f else if Term.ls_equal ls cond_robust_predicate - then CondRobust e + then CondRobust f else failwith_unsupported_ls ls | _ -> failwith_unsupported_term term in @@ -157,19 +203,6 @@ let clip_value value = let value = if Float.is_negative value then Float.zero else value in if Float.(is_negative (one -. value)) then Float.one else value -let real_constant value = - (* Split into integer and fractional parts. *) - let int_frac = String.split Float.(to_string (abs value)) ~on:'.' in - let int = List.hd_exn int_frac in - let frac = - match List.tl_exn int_frac with - | [] -> "" - | [ s ] -> s - | _ -> assert false (* Since every float has one '.' at most. *) - in - Constant.ConstReal - (Number.real_literal ~radix:10 ~neg:false ~int ~frac ~exp:None) - let add_axioms ?eps ~clip th task input value = let eps = Float.(abs (of_string (Option.value eps ~default:"0"))) in let value = Float.of_string value in @@ -182,7 +215,7 @@ let add_axioms ?eps ~clip th task input value = let t_ge = let value = value -. eps in let value = if clip then clip_value value else value in - let cst = real_constant value in + let cst = real_constant_of_float value in Term.(ps_app le [ t_const cst Ty.ty_real; fs_app input [] Ty.ty_real ]) in let task = Task.add_prop_decl task Decl.Paxiom pr_ge t_ge in @@ -194,7 +227,7 @@ let add_axioms ?eps ~clip th task input value = let t_le = let value = value +. eps in let value = if clip then clip_value value else value in - let cst = real_constant value in + let cst = real_constant_of_float value in Term.(ps_app le [ fs_app input [] Ty.ty_real; t_const cst Ty.ty_real ]) in Task.add_prop_decl task Decl.Paxiom pr_le t_le @@ -214,16 +247,16 @@ let add_output_decl ~id predicate_kind ~outputs ~record th task = (* Add goal formula. *) let class_ = List.hd_exn record in let goal_fmla = - let gt = Theory.(ns_find_ls th.th_export [ Ident.op_infix ">" ]) in - let class_ls = List.nth_exn outputs (Int.of_string class_) in + let ls_gt = Theory.(ns_find_ls th.th_export [ Ident.op_infix ">" ]) in + let ls_class = List.nth_exn outputs (Int.of_string class_) in List.fold outputs ~init:Term.t_true ~f:(fun fmla output -> - if Term.ls_equal class_ls output + if Term.ls_equal ls_class output then fmla else let gt_fmla = Term.( - ps_app gt - [ fs_app class_ls [] Ty.ty_real; fs_app output [] Ty.ty_real ]) + ps_app ls_gt + [ fs_app ls_class [] Ty.ty_real; fs_app output [] Ty.ty_real ]) in Term.t_and fmla gt_fmla) in diff --git a/src/dataset.mli b/src/dataset.mli index 5efbdb9b017720a70e2c04d9aa9bacc988bac23c..f1f2e13d9964cc986ef609f3595de91fa8860d96 100644 --- a/src/dataset.mli +++ b/src/dataset.mli @@ -22,14 +22,20 @@ open Why3 -type eps +type eps [@@deriving yojson, show] + +val string_of_eps : eps -> string +val term_of_eps : Env.env -> eps -> Term.term type property = private | Correct | Robust of eps | CondRobust of eps +[@@deriving yojson, show] + +type normalization [@@deriving yojson, show] -type normalization +val term_of_normalization : Env.env -> normalization -> Term.term type ('model, 'dataset) predicate = private { model : 'model; @@ -38,8 +44,6 @@ type ('model, 'dataset) predicate = private { property : property; } -val string_of_eps : eps -> string - val interpret_predicate : Env.env -> on_model:(Term.lsymbol -> 'model) -> diff --git a/src/language.ml b/src/language.ml index b89e65b09a7d74e81d4e135aa0b16876e131d229..789d5bc6a2f101bd9e3267504dfd3b562424fa33 100644 --- a/src/language.ml +++ b/src/language.ml @@ -102,47 +102,56 @@ let register_svm_as_array env nb_inputs nb_classes filename mstr = in Wstdlib.Mstr.add name (Pmodule.close_module th_uc) mstr -let nnet_parser env _ filename _ = - let model = Nnet.parse ~permissive:true filename in - match model with - | Error s -> Loc.errorm "%s" s - | Ok { n_inputs; n_outputs; _ } -> - Wstdlib.Mstr.empty - |> register_nn_as_tuple env n_inputs n_outputs filename - |> register_nn_as_array env n_inputs n_outputs filename +let nnet_parser = + Env.Wenv.memoize 13 (fun env -> + let h = Hashtbl.create (module String) in + Hashtbl.findi_or_add h ~default:(fun filename -> + let model = Nnet.parse ~permissive:true filename in + match model with + | Error s -> Loc.errorm "%s" s + | Ok { n_inputs; n_outputs; _ } -> + Wstdlib.Mstr.empty + |> register_nn_as_tuple env n_inputs n_outputs filename + |> register_nn_as_array env n_inputs n_outputs filename)) -let onnx_parser env _ filename _ = - let model = Onnx.parse filename in - match model with - | Error s -> Loc.errorm "%s" s - | Ok { n_inputs; n_outputs; nier } -> - let nier = - match nier with - | Error msg -> - Logs.warn (fun m -> - m "Cannot build network intermediate representation:@ %s" msg); - None - | Ok nier -> Some nier - in - Wstdlib.Mstr.empty - |> register_nn_as_tuple env n_inputs n_outputs filename ?nier - |> register_nn_as_array env n_inputs n_outputs filename ?nier +let onnx_parser = + Env.Wenv.memoize 13 (fun env -> + let h = Hashtbl.create (module String) in + Hashtbl.findi_or_add h ~default:(fun filename -> + let model = Onnx.parse filename in + match model with + | Error s -> Loc.errorm "%s" s + | Ok { n_inputs; n_outputs; nier } -> + let nier = + match nier with + | Error msg -> + Logs.warn (fun m -> + m "Cannot build network intermediate representation:@ %s" msg); + None + | Ok nier -> Some nier + in + Wstdlib.Mstr.empty + |> register_nn_as_tuple env n_inputs n_outputs filename ?nier + |> register_nn_as_array env n_inputs n_outputs filename ?nier)) -let ovo_parser env _ filename _ = - let model = Ovo.parse filename in - match model with - | Error s -> Loc.errorm "%s" s - | Ok { n_inputs; n_outputs } -> - register_svm_as_array env n_inputs n_outputs filename Wstdlib.Mstr.empty +let ovo_parser = + Env.Wenv.memoize 13 (fun env -> + let h = Hashtbl.create (module String) in + Hashtbl.findi_or_add h ~default:(fun filename -> + let model = Ovo.parse filename in + match model with + | Error s -> Loc.errorm "%s" s + | Ok { n_inputs; n_outputs } -> + register_svm_as_array env n_inputs n_outputs filename Wstdlib.Mstr.empty)) let register_nnet_support () = Env.register_format ~desc:"NNet format (ReLU only)" Pmodule.mlw_language - "NNet" [ "nnet" ] nnet_parser + "NNet" [ "nnet" ] (fun env _ filename _ -> nnet_parser env filename) let register_onnx_support () = Env.register_format ~desc:"ONNX format" Pmodule.mlw_language "ONNX" [ "onnx" ] - onnx_parser + (fun env _ filename _ -> onnx_parser env filename) let register_ovo_support () = Env.register_format ~desc:"OVO format" Pmodule.mlw_language "OVO" [ "ovo" ] - ovo_parser + (fun env _ filename _ -> ovo_parser env filename) diff --git a/src/language.mli b/src/language.mli index 39453b44cd27434478bda373e22b03cc8955026e..68c71572b844dc0aff4a1578be3d999950cad000 100644 --- a/src/language.mli +++ b/src/language.mli @@ -50,3 +50,15 @@ val register_onnx_support : unit -> unit val register_ovo_support : unit -> unit (** Register OVO parser. *) + +val nnet_parser : Env.env -> string -> Pmodule.pmodule Wstdlib.Mstr.t +(* [nnet_parser env filename] parses and creates the theories corresponding to + the given nnet [filename]. The result is memoized. *) + +val onnx_parser : Env.env -> string -> Pmodule.pmodule Wstdlib.Mstr.t +(* [onnx_parser env filename] parses and creates the theories corresponding to + the given onnx [filename]. The result is memoized. *) + +val ovo_parser : Env.env -> string -> Pmodule.pmodule Wstdlib.Mstr.t +(* [nnet_parser env filename] parses and creates the theories corresponding to + the given ovo [filename]. The result is memoized. *) diff --git a/src/main.ml b/src/main.ml index 35fcd65082e735f31a4595e2ca3eee658b94bbd5..c59b9740df57988f8f391843a1640aab3441af4f 100644 --- a/src/main.ml +++ b/src/main.ml @@ -102,7 +102,7 @@ let memlimit_of_string s = | [ v ], ([] | [ "M" ] | [ "MB" ]) -> Int.of_string v | [ v ], ([ "G" ] | [ "GB" ]) -> Int.of_string v * 1000 | [ v ], ([ "T" ] | [ "TB" ]) -> Int.of_string v * 1000000 - | _ -> failwith "Unrecognized memory limit" + | _ -> invalid_arg "Unrecognized memory limit" let timelimit_of_string s = let s = String.strip s in @@ -118,18 +118,25 @@ let timelimit_of_string s = | [ v ], ([] | [ "s" ]) -> Int.of_string v | [ v ], [ "m" ] -> Int.of_string v * 60 | [ v ], [ "h" ] -> Int.of_string v * 3600 - | _ -> failwith "Unrecognized time limit" + | _ -> invalid_arg "Unrecognized time limit" -let verify format loadpath memlimit timelimit prover dataset_csv files = +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 + (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 () @@ -158,7 +165,7 @@ let config_cmd = ret (const (fun detect _ -> if not detect - then `Help (`Pager, Some "config") + 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. *) @@ -167,47 +174,43 @@ let config_cmd = 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 doc = "Property verification using external provers." 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 = "Files to verify." in - let file_or_stdin = Verification.File.(of_string, pretty) in - Arg.(value & pos_all file_or_stdin [] & info [] ~doc) - in - let format = - let doc = "File format." in - Arg.(value & opt (some string) None & info [ "format" ] ~doc) - in - let loadpath = - let doc = "Additional loadpath." in - Arg.(value & opt_all string [ "." ] & info [ "L"; "loadpath" ] ~doc) - in - 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") - in - 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 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 @@ -227,15 +230,37 @@ let verify_cmd = Arg.(value & opt (some file) None & info [ "dataset-csv" ] ~doc) in Term.( - ret - (const - (fun format loadpath memlimit timelimit prover dataset_csv files _ -> - `Ok - (exec_cmd cmdname (fun () -> - verify format loadpath memlimit timelimit prover dataset_csv - files))) - $ format $ loadpath $ memlimit $ timelimit $ prover $ dataset_csv - $ files $ setup_logs)) + 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 @@ -272,7 +297,8 @@ let () = let () = try - Cmd.group ~default:default_cmd default_info [ config_cmd; verify_cmd ] + 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) diff --git a/src/prover.ml b/src/prover.ml index 517d4f447504bdac64e80191001cff15679417b1..847cc3e20e0896f11deb8cf00cf636834185642a 100644 --- a/src/prover.ml +++ b/src/prover.ml @@ -25,6 +25,7 @@ type t = | Pyrat | Saver | CVC5 +[@@deriving yojson, show] let list_available () = [ Marabou; Pyrat; Saver; CVC5 ] diff --git a/src/prover.mli b/src/prover.mli index 36ddf1a72a47776dc3ec2322a3359fed09011af2..1ae811a671fa5d950f8c60f1f9baa0bfd4d9192d 100644 --- a/src/prover.mli +++ b/src/prover.mli @@ -25,6 +25,7 @@ type t = private | Pyrat | Saver | CVC5 +[@@deriving yojson, show] val list_available : unit -> t list val of_string : string -> t option diff --git a/src/verification.ml b/src/verification.ml index 71728ea3122517fc51a02e4db18b4e0c4ec62b63..fa183c52c83c0b68ba0352589c91cb6dd234692c 100644 --- a/src/verification.ml +++ b/src/verification.ml @@ -24,21 +24,142 @@ open Why3 open Base module Filename = Caml.Filename +module JSON = struct + type t = { + prover : Prover.t; + model : string; + property : property; + } + [@@deriving yojson, show] + + and property = { + dataset : string; + normalization : Dataset.normalization option; + kind : Dataset.property; + } + [@@deriving yojson, show] + + let of_string s = + match of_yojson (Yojson.Safe.from_file s) with + | Ok t -> Ok t + | Error msg -> + Error + (invalid_arg + (Fmt.str "Unrecognized JSON configuration in file '%s' (%s)" s msg)) + | exception Yojson.Json_error msg -> + Error + (failwith + (Fmt.str "Unexpected error while parsing JSON file '%s' (%s)" s msg)) + + let pretty fmt t = Fmt.fmt "%a" fmt pp t + + let mstr_theory_of_json env t = + let th_as_array = + let model_parser = + let extension = Filename.(extension (basename t.model)) in + match String.lowercase extension with + | ".nnet" -> Language.nnet_parser + | ".onnx" -> Language.onnx_parser + | ".ovo" -> Language.ovo_parser + | "" -> invalid_arg "Cannot find model file extension" + | _ -> + invalid_arg (Fmt.str "Unrecognized model extension '%s'" extension) + in + let pmod = model_parser env (Unix.realpath t.model) in + (Wstdlib.Mstr.find "AsArray" pmod).mod_theory + in + let th_float64 = Env.read_theory env [ "ieee_float" ] "Float64" in + let th_ds_classification = + Pmodule.read_module env [ "caisar" ] "DataSetClassification" + in + let th_ds_props = Pmodule.read_module env [ "caisar" ] "DataSetProps" in + let name = "JSON" in + let th_uc = Theory.create_theory (Ident.id_fresh name) in + let th_uc = Theory.use_export th_uc th_as_array in + let th_uc = Theory.use_export th_uc th_float64 in + let th_uc = Theory.use_export th_uc th_ds_classification.mod_theory in + let th_uc = Theory.use_export th_uc th_ds_props.mod_theory in + let t_dataset = + let ls_dataset = + Theory.ns_find_ls th_ds_classification.mod_theory.th_export + [ "dataset" ] + in + Term.t_app_infer ls_dataset [] + in + let t_model = + let ls_model = Theory.ns_find_ls th_as_array.th_export [ "model" ] in + Term.t_app_infer ls_model [] + in + (* Add dataset normalization. *) + let t_dataset, th_uc = + match t.property.normalization with + | None -> (t_dataset, th_uc) + | Some n -> + let t_normalization = Dataset.term_of_normalization env n in + let ls_dataset_norm = + let ty = + Ty.ty_app + (Theory.ns_find_ts th_ds_classification.mod_theory.th_export + [ "dataset" ]) + [] + in + Term.create_fsymbol (Ident.id_fresh "dataset'") [] ty + in + let ld_dataset_norm = + Decl.make_ls_defn ls_dataset_norm [] t_normalization + in + ( Term.t_app_infer ls_dataset_norm [], + Theory.add_logic_decl th_uc [ ld_dataset_norm ] ) + in + (* Create goal formula about specified property. *) + let th_uc = + let t_property = + match t.property.kind with + | Dataset.Correct -> + let ls_correct = + Theory.ns_find_ls th_ds_props.mod_theory.th_export [ "correct" ] + in + Term.t_app_infer ls_correct [ t_model; t_dataset ] + | Robust eps -> + let ls_robust = + Theory.ns_find_ls th_ds_props.mod_theory.th_export [ "robust" ] + in + let t_eps = Dataset.term_of_eps env eps in + Term.t_app_infer ls_robust [ t_model; t_dataset; t_eps ] + | CondRobust _ -> invalid_arg "Not yet supported property" + in + let prsymbol = Decl.create_prsymbol (Ident.id_fresh "property") in + Theory.add_prop_decl th_uc Decl.Pgoal prsymbol t_property + in + let th = Theory.close_theory th_uc in + Wstdlib.Mstr.singleton name th +end + module File = struct type t = | Stdin | File of string + | Json of JSON.t + + let of_json_config jc = + if Caml.Sys.file_exists jc.JSON.model + then + if Caml.Sys.file_exists jc.JSON.property.dataset + then Ok (Json jc) + else Error (Fmt.str "no '%s' file or directory" jc.property.dataset) + else Error (Fmt.str "no '%s' file or directory" jc.model) let of_string s = if String.equal s "-" - then `Ok Stdin + then Ok Stdin else if Caml.Sys.file_exists s - then `Ok (File s) - else `Error (Fmt.str "no '%s' file or directory" s) + then Ok (File s) + else Error (Fmt.str "no '%s' file or directory" s) let pretty fmt = function | Stdin -> Fmt.string fmt "-" | File f -> Fmt.string fmt f + | Json j -> JSON.pretty fmt j end let () = @@ -173,7 +294,7 @@ let call_prover ~limit config env prover config_prover driver dataset_csv task = Fmt.(option ~none:nop (any " " ++ string)) additional_info) -let verify ?(debug = false) format loadpath ?memlimit ?timelimit prover +let verify ?(debug = false) ?format ~loadpath ?memlimit ?timelimit prover ?dataset_csv file = if debug then Debug.set_flag (Debug.lookup_flag "call_prover"); let env, config = create_env ~debug loadpath in @@ -188,13 +309,14 @@ let verify ?(debug = false) format loadpath ?memlimit ?timelimit prover Call_provers.limit_mem = memlimit; } in - let _, mstr_theory = + let mstr_theory = match file with | File.Stdin -> - ("stdin", Env.(read_channel ?format base_language env "stdin" Caml.stdin)) + Env.(read_channel ?format base_language env "stdin" Caml.stdin) | File file -> let mlw_files, _ = Env.(read_file ?format base_language env file) in - (file, mlw_files) + mlw_files + | Json json -> JSON.mstr_theory_of_json env json in Wstdlib.Mstr.iter (fun _ theory -> diff --git a/src/verification.mli b/src/verification.mli index 6bfed09d04491523e4d732c8649f637d93a8ea1d..5572343a325c553d6cfcc4bfc52a63fc4cfeb585 100644 --- a/src/verification.mli +++ b/src/verification.mli @@ -20,17 +20,37 @@ (* *) (**************************************************************************) +module JSON : sig + type t = private { + prover : Prover.t; + model : string; + property : property; + } + [@@deriving yojson, show] + + and property = private { + dataset : string; + normalization : Dataset.normalization option; + kind : Dataset.property; + } + [@@deriving yojson, show] + + val of_string : string -> (t, exn) result + val pretty : Format.formatter -> t -> unit +end + module File : sig type t - val of_string : string -> [ `Ok of t | `Error of string ] + val of_json_config : JSON.t -> (t, string) result + val of_string : string -> (t, string) result val pretty : Format.formatter -> t -> unit end val verify : ?debug:bool -> - string option -> - string list -> + ?format:string -> + loadpath:string list -> ?memlimit:int -> ?timelimit:int -> Prover.t -> diff --git a/tests/pyrat_verify_json.t b/tests/pyrat_verify_json.t new file mode 100644 index 0000000000000000000000000000000000000000..aace62695510d7b4b8a40b3c1348795d35f2bd70 --- /dev/null +++ b/tests/pyrat_verify_json.t @@ -0,0 +1,18 @@ +Test verify-json + $ cat - > test_data.csv << EOF + > 1,0,255,200,5,198 + > EOF + + $ cat - > config.json << EOF + > {"prover":["Pyrat"],"model":"TestNetwork.nnet","property":{"dataset":"test_data.csv","normalization":["MinMax",0.0,255.0],"kind":["Robust",0.01]}} + > EOF + + $ chmod u+x bin/pyrat.py + + $ bin/pyrat.py --version + PyRAT 1.1 + + $ PATH=$(pwd)/bin:$PATH + + $ caisar verify-json config.json 2>&1 <<EOF | ./filter_tmpdir.sh + [caisar] Goal property: Unknown ()