Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple ts in Lin and Lin_api signatures #112

Open
wants to merge 60 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
e902ec1
initial Lin attempt to support multiple ts
jmid Aug 12, 2022
742118b
array test example updated
jmid Aug 12, 2022
6864a6d
port Hashtbl test example
jmid Aug 12, 2022
86979cb
add optional init_cmd printer
jmid Aug 15, 2022
a9dfab4
add show function and use init_cmd printer
jmid Aug 15, 2022
0457c84
initial support for multiple ts in Lin_api + some documentation
jmid Aug 15, 2022
403d914
update Array tests to multiple ts
jmid Aug 15, 2022
9ecbb37
update Hashtbl tests with multiple ts support
jmid Aug 15, 2022
c51da9c
update atomic tests to multiple ts
jmid Aug 16, 2022
f3c7786
update Bytes tests to multiple ts
jmid Aug 16, 2022
5c699a1
update internal tests to multiple ts
jmid Aug 16, 2022
3b63ae4
update kcas tests to multiple ts
jmid Aug 16, 2022
af1de5a
update Lazy tests to multiple ts
jmid Aug 16, 2022
96f4adc
update src/neg_tests to multiple ts
jmid Aug 16, 2022
1e1cdf5
update Queue tests to multiple ts
jmid Aug 16, 2022
f01d599
update Stack tests to multiple ts
jmid Aug 16, 2022
af70d49
omit flush in Var.pp
jmid Aug 16, 2022
5754e04
rm opam dependency on ppx_deriving_qcheck
jmid Aug 16, 2022
406e653
print init return value in annotated trace for consistency
jmid Aug 16, 2022
d765e64
fix Var.t printing w/pp and show
jmid Aug 16, 2022
0f3b8cb
make Bytes test a negative test
jmid Aug 18, 2022
b80dd93
first shrink var, then recurse
jmid Aug 19, 2022
4c6dff8
reduce Bytes Thread test count
jmid Aug 19, 2022
9f61c20
comment out Bytes.escaped causing segfault
jmid Aug 19, 2022
3e6fa03
increase frequency of Bytes.{fill,blit_string}
jmid Aug 19, 2022
e409fda
add Bytes.make
jmid Aug 19, 2022
cbae0ea
update documented Util interface with optional init_cmd
jmid Sep 22, 2022
bf7784d
Factorise some common code
shym Oct 4, 2022
1190f41
Factorise some common code
shym Oct 4, 2022
a317f9c
Complete the Bigarray.Array1 tests with multiple `t`s
shym Oct 4, 2022
f3d1591
Lin: Fix commands during shrinking to use existing states
shym Oct 6, 2022
ca731fa
Shorten random arrays to avoid slow tests
shym Oct 6, 2022
32c4b9c
Generalise the iterator on valid states for shrinking
shym Oct 7, 2022
13ee694
Add a bounded_array function
shym Oct 7, 2022
c655491
Use bounded_array in bigarray tests
shym Oct 7, 2022
d4fc1eb
Define fix_cmd for all existing Lin tests
shym Oct 7, 2022
d05a7e6
rename valid_states, omit drop_invalids
jmid Oct 13, 2022
c1c1220
eliminate a few let opens
jmid Oct 13, 2022
aedc426
Update out-of-date `run` comment in lib/lin.ml
jmid Oct 13, 2022
0591d44
forgot to rename in internal/cleanup.ml
jmid Oct 13, 2022
18cda2e
remove duplicate Bytes.make
jmid Oct 14, 2022
6670815
restore init size
jmid Oct 14, 2022
6425129
reenable Bytes.escaped which has been patched
jmid Oct 14, 2022
1289119
faster valid_t_vars shrinker/iterator, using clever array-indexing fr…
jmid Oct 14, 2022
5a6ad69
update print signature
jmid Oct 20, 2022
7104612
update returning_ and returning_or_exc_ signatures
jmid Oct 20, 2022
5e460ad
clarify returning_ combinator semantics
jmid Oct 28, 2022
cf7312a
attempt to speed-up tvar-index shrinking
jmid Oct 31, 2022
7dbb92a
simplifed approach: rename only invalid t_vars to 0 - as they represent
jmid Nov 1, 2022
d2ca78d
shrinker improvements
jmid Nov 8, 2022
affe328
update lin_test.mls to interface
jmid Nov 8, 2022
d35745e
update src/internal too
jmid Nov 8, 2022
94649b9
Add comment to clarify eta expansion
jmid Nov 11, 2022
0317176
remove commented-out extract_envs
jmid Nov 11, 2022
31dff51
Merge pull request #177 from jmid/experiment-mv-tvar-shrinking-last
jmid Nov 18, 2022
cad83ac
Merge branch 'main' into multiple-ts
jmid Feb 2, 2023
43d5f8a
fix test/cleanup.ml
jmid Feb 2, 2023
a1dc261
Introduce Lin.Internal.cmd_triple and simplify signatures
jmid Feb 27, 2023
f3aa8a8
Merge branch 'main' into multiple-ts
jmid Mar 7, 2023
457a493
increase chance of Bigarray.Array1.get to observe diffs
jmid Mar 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
356 changes: 256 additions & 100 deletions lib/lin.ml

Large diffs are not rendered by default.

59 changes: 48 additions & 11 deletions lib/lin.mli
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@
at any time.
*)
module Internal : sig
module Var : sig
type t = int
val next : unit -> t
val reset : unit -> unit
val pp : Format.formatter -> t -> unit
val show : t -> string
end

module Env : sig
type t = Var.t list
val gen_t_var : t -> Var.t QCheck.Gen.t
val valid_t_vars : t -> Var.t -> Var.t QCheck.Iter.t
end

module type CmdSpec = sig
type t
(** The type of the system under test *)
Expand All @@ -17,12 +31,18 @@ module Internal : sig
val show_cmd : cmd -> string
(** [show_cmd c] returns a string representing the command [c]. *)

val gen_cmd : cmd QCheck.Gen.t
(** A command generator. *)
val gen_cmd : Var.t QCheck.Gen.t -> (Var.t option * cmd) QCheck.Gen.t
(** A command generator.
It accepts a variable generator and generates a pair [(opt,cmd)] with the option indicating
an storage index to store the [cmd]'s result. *)

val shrink_cmd : cmd QCheck.Shrink.t
val shrink_cmd : Env.t -> cmd QCheck.Shrink.t
(** A command shrinker.
To a first approximation you can use {!QCheck.Shrink.nil}. *)
It accepts the current environment as its argument.
To a first approximation you can use [fun _env -> Shrink.nil]. *)

val cmd_uses_var : Var.t -> cmd -> bool
(** [cmd_uses_var v cmd] should return [true] iff the command [cmd] refers to the variable [v]. *)

type res
(** The command result type *)
Expand All @@ -40,16 +60,27 @@ module Internal : sig
(** Utility function to clean up [t] after each test instance,
e.g., for closing sockets, files, or resetting global parameters *)

val run : cmd -> t -> res
(** [run c t] should interpret the command [c] over the system under test [t] (typically side-effecting). *)
val run : (Var.t option * cmd) -> t array -> res
(** [run (opt,c) t] should interpret the command [c] over the various instances of the system under test [t array] (typically side-effecting).
[opt] indicates the index to store the result. *)
end

type 'cmd cmd_triple =
{ env_size : int;
seq_prefix : (Var.t option * 'cmd) list;
tail_left : (Var.t option * 'cmd) list;
tail_right : (Var.t option * 'cmd) list; }

module Make(Spec : CmdSpec) : sig
val arb_cmds_triple : int -> int -> (Spec.cmd list * Spec.cmd list * Spec.cmd list) QCheck.arbitrary
val check_seq_cons : (Spec.cmd * Spec.res) list -> (Spec.cmd * Spec.res) list -> (Spec.cmd * Spec.res) list -> Spec.t -> Spec.cmd list -> bool
val interp_plain : Spec.t -> Spec.cmd list -> (Spec.cmd * Spec.res) list
val lin_test : rep_count:int -> retries:int -> count:int -> name:string -> lin_prop:(Spec.cmd list * Spec.cmd list * Spec.cmd list -> bool) -> QCheck.Test.t
val neg_lin_test : rep_count:int -> retries:int -> count:int -> name:string -> lin_prop:(Spec.cmd list * Spec.cmd list * Spec.cmd list -> bool) -> QCheck.Test.t
val init_sut : int -> Spec.t array
val cleanup : Spec.t array -> (Var.t option * Spec.cmd) list -> (Var.t option * Spec.cmd) list -> (Var.t option * Spec.cmd) list -> unit
val show_cmd : Var.t option * Spec.cmd -> string
val init_cmd_ret : string
val arb_cmds_triple : int -> int -> Spec.cmd cmd_triple QCheck.arbitrary
val check_seq_cons : int -> ((Var.t option * Spec.cmd) * Spec.res) list -> ((Var.t option * Spec.cmd) * Spec.res) list -> ((Var.t option * Spec.cmd) * Spec.res) list -> Spec.t array -> (Var.t option * Spec.cmd) list -> bool
val interp_plain : Spec.t array -> (Var.t option * Spec.cmd) list -> ((Var.t option * Spec.cmd) * Spec.res) list
val lin_test : rep_count:int -> retries:int -> count:int -> name:string -> lin_prop:(Spec.cmd cmd_triple -> bool) -> QCheck.Test.t
val neg_lin_test : rep_count:int -> retries:int -> count:int -> name:string -> lin_prop:(Spec.cmd cmd_triple -> bool) -> QCheck.Test.t
end

val pp_exn : Format.formatter -> exn -> unit
Expand Down Expand Up @@ -200,6 +231,12 @@ val array : ('a, 'c, 's, combinable) ty -> ('a array, 'c, 's, combinable) ty
and have their elements generated by the [t] combinator.
It is based on {!QCheck.array}. *)

val bounded_array : int -> ('a, 'c, 's, combinable) ty -> ('a array, 'c, 's, combinable) ty
(** The [bounded_array] combinator represents the {{!Stdlib.Array.t}[array]} type.
The generated arrays from [bounded_array len t] have a length between [0] and [len]
(inclusive) and have their elements generated by the [t] combinator.
It is based on {!QCheck.array_of_size}. *)

val seq : ('a, 'c, 's, combinable) ty -> ('a Seq.t, 'c, 's, combinable) ty
(** The [seq] combinator represents the {!Stdlib.Seq.t} type.
The generated sequences from [seq t] have a length resulting from {!QCheck.Gen.nat}
Expand Down
16 changes: 8 additions & 8 deletions lib/lin_domain.ml
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,23 @@ module Make_internal (Spec : Internal.CmdSpec [@alert "-internal"]) = struct
List.combine cs (Array.to_list res_arr)

(* Linearization property based on [Domain] and an Atomic flag *)
let lin_prop (seq_pref,cmds1,cmds2) =
let sut = Spec.init () in
let lin_prop { Internal.env_size=array_size; seq_prefix=seq_pref; tail_left=cmds1; tail_right=cmds2 } =
let sut = init_sut array_size in
let pref_obs = interp sut seq_pref in
let wait = Atomic.make true in
let dom1 = Domain.spawn (fun () -> while Atomic.get wait do Domain.cpu_relax() done; try Ok (interp sut cmds1) with exn -> Error exn) in
let dom2 = Domain.spawn (fun () -> Atomic.set wait false; try Ok (interp sut cmds2) with exn -> Error exn) in
let obs1 = Domain.join dom1 in
let obs2 = Domain.join dom2 in
Spec.cleanup sut ;
cleanup sut seq_pref cmds1 cmds2;
let obs1 = match obs1 with Ok v -> v | Error exn -> raise exn in
let obs2 = match obs2 with Ok v -> v | Error exn -> raise exn in
let seq_sut = Spec.init () in
check_seq_cons pref_obs obs1 obs2 seq_sut []
let seq_sut = init_sut array_size in
check_seq_cons array_size pref_obs obs1 obs2 seq_sut []
|| QCheck.Test.fail_reportf " Results incompatible with sequential execution\n\n%s"
@@ Util.print_triple_vertical ~fig_indent:5 ~res_width:35
(fun (c,r) -> Printf.sprintf "%s : %s" (Spec.show_cmd c) (Spec.show_res r))
(pref_obs,obs1,obs2)
@@ Util.print_triple_vertical ~fig_indent:5 ~res_width:35 ~init_cmd:init_cmd_ret
(fun (c,r) -> Printf.sprintf "%s : %s" (show_cmd c) (Spec.show_res r))
(pref_obs,obs1,obs2)[@@alert "-internal"]

let lin_test ~count ~name =
lin_test ~rep_count:50 ~count ~retries:3 ~name ~lin_prop:lin_prop
Expand Down
5 changes: 3 additions & 2 deletions lib/lin_domain.mli
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ open Lin

(** functor to build an internal module representing parallel tests *)
module Make_internal (Spec : Internal.CmdSpec [@alert "-internal"]) : sig
val arb_cmds_triple : int -> int -> (Spec.cmd list * Spec.cmd list * Spec.cmd list) QCheck.arbitrary
val lin_prop : (Spec.cmd list * Spec.cmd list * Spec.cmd list) -> bool
[@@@alert "-internal"]
val arb_cmds_triple : int -> int -> Spec.cmd Internal.cmd_triple QCheck.arbitrary
val lin_prop : Spec.cmd Internal.cmd_triple -> bool
val lin_test : count:int -> name:string -> QCheck.Test.t
val neg_lin_test : count:int -> name:string -> QCheck.Test.t
end
Expand Down
40 changes: 22 additions & 18 deletions lib/lin_effect.ml
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,18 @@ module Make_internal (Spec : Internal.CmdSpec [@alert "-internal"]) = struct
| SchedYield -> "<SchedYield>"
| UserCmd c -> Spec.show_cmd c

let gen_cmd =
let gen_cmd env =
(Gen.frequency
[(3,Gen.return SchedYield);
(5,Gen.map (fun c -> UserCmd c) Spec.gen_cmd)])
[(3,Gen.return (None,SchedYield));
(5,Gen.map (fun (opt,c) -> (opt,UserCmd c)) (Spec.gen_cmd env))])

let shrink_cmd c = match c with
let shrink_cmd env c = match c with
| SchedYield -> Iter.empty
| UserCmd c -> Iter.map (fun c' -> UserCmd c') (Spec.shrink_cmd c)
| UserCmd c -> Iter.map (fun c' -> UserCmd c') (Spec.shrink_cmd env c)

let cmd_uses_var var c = match c with
| SchedYield -> false
| UserCmd cmd -> Spec.cmd_uses_var var cmd

type res = SchedYieldRes | UserRes of Spec.res

Expand All @@ -72,18 +76,18 @@ module Make_internal (Spec : Internal.CmdSpec [@alert "-internal"]) = struct
| _, _ -> false

let run c sut = match c with
| SchedYield ->
| _, SchedYield ->
(yield (); SchedYieldRes)
| UserCmd uc ->
let res = Spec.run uc sut in
| opt, UserCmd uc ->
let res = Spec.run (opt,uc) sut in
UserRes res
end

module EffTest = Internal.Make(EffSpec) [@alert "-internal"]

let arb_cmds_triple = EffTest.arb_cmds_triple

let filter_res rs = List.filter (fun (c,_) -> c <> EffSpec.SchedYield) rs
let filter_res rs = List.filter (fun ((_,c),_) -> c <> EffSpec.SchedYield) rs

let rec interp sut cs = match cs with
| [] -> []
Expand All @@ -92,25 +96,25 @@ module Make_internal (Spec : Internal.CmdSpec [@alert "-internal"]) = struct
(c,res)::interp sut cs

(* Concurrent agreement property based on effect-handler scheduler *)
let lin_prop (seq_pref,cmds1,cmds2) =
let sut = Spec.init () in
let lin_prop { Internal.env_size=array_size; seq_prefix=seq_pref; tail_left=cmds1; tail_right=cmds2 } =
let sut = EffTest.init_sut array_size in
let pref_obs = EffTest.interp_plain sut (List.filter (fun (_,c) -> c <> EffSpec.SchedYield) seq_pref) in
(* exclude [Yield]s from sequential prefix *)
let pref_obs = EffTest.interp_plain sut (List.filter (fun c -> c <> EffSpec.SchedYield) seq_pref) in
let obs1,obs2 = ref (Ok []), ref (Ok []) in
let main () =
fork (fun () -> let tmp1 = try Ok (interp sut cmds1) with exn -> Error exn in obs1 := tmp1);
fork (fun () -> let tmp2 = try Ok (interp sut cmds2) with exn -> Error exn in obs2 := tmp2); in
let () = start_sched main in
let () = Spec.cleanup sut in
let () = EffTest.cleanup sut seq_pref cmds1 cmds2 in
let obs1 = match !obs1 with Ok v -> ref v | Error exn -> raise exn in
let obs2 = match !obs2 with Ok v -> ref v | Error exn -> raise exn in
let seq_sut = Spec.init () in
let seq_sut = EffTest.init_sut array_size in
(* exclude [Yield]s from sequential executions when searching for an interleaving *)
EffTest.check_seq_cons (filter_res pref_obs) (filter_res !obs1) (filter_res !obs2) seq_sut []
EffTest.check_seq_cons array_size (filter_res pref_obs) (filter_res !obs1) (filter_res !obs2) seq_sut []
|| QCheck.Test.fail_reportf " Results incompatible with linearized model\n\n%s"
@@ Util.print_triple_vertical ~fig_indent:5 ~res_width:35
(fun (c,r) -> Printf.sprintf "%s : %s" (EffSpec.show_cmd c) (EffSpec.show_res r))
(pref_obs,!obs1,!obs2)
@@ Util.print_triple_vertical ~fig_indent:5 ~res_width:35 ~init_cmd:EffTest.init_cmd_ret
(fun (c,r) -> Printf.sprintf "%s : %s" (EffTest.show_cmd c) (EffSpec.show_res r))
(pref_obs,!obs1,!obs2)[@@alert "-internal"]

let lin_test ~count ~name =
let arb_cmd_triple = EffTest.arb_cmds_triple 20 12 in
Expand Down
5 changes: 3 additions & 2 deletions lib/lin_effect.mli
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ module Make_internal (Spec : Internal.CmdSpec [@alert "-internal"]) : sig
module EffSpec : sig
type cmd
end
val arb_cmds_triple : int -> int -> (EffSpec.cmd list * EffSpec.cmd list * EffSpec.cmd list) QCheck.arbitrary
val lin_prop : (EffSpec.cmd list * EffSpec.cmd list * EffSpec.cmd list) -> bool
[@@@alert "-internal"]
val arb_cmds_triple : int -> int -> EffSpec.cmd Internal.cmd_triple QCheck.arbitrary
val lin_prop : EffSpec.cmd Internal.cmd_triple -> bool
val lin_test : count:int -> name:string -> QCheck.Test.t
val neg_lin_test : count:int -> name:string -> QCheck.Test.t
end
Expand Down
16 changes: 8 additions & 8 deletions lib/lin_thread.ml
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,25 @@ module Make_internal (Spec : Internal.CmdSpec [@alert "-internal"]) = struct
let arb_cmds_triple = arb_cmds_triple

(* Linearization property based on [Thread] *)
let lin_prop (seq_pref, cmds1, cmds2) =
let sut = Spec.init () in
let lin_prop { Internal.env_size=array_size; seq_prefix=seq_pref; tail_left=cmds1; tail_right=cmds2 } =
let sut = init_sut array_size in
let obs1, obs2 = ref (Ok []), ref (Ok []) in
let pref_obs = interp_plain sut seq_pref in
let wait = ref true in
let th1 = Thread.create (fun () -> while !wait do Thread.yield () done; obs1 := try Ok (interp_thread sut cmds1) with exn -> Error exn) () in
let th2 = Thread.create (fun () -> wait := false; obs2 := try Ok (interp_thread sut cmds2) with exn -> Error exn) () in
Thread.join th1;
Thread.join th2;
Spec.cleanup sut;
cleanup sut seq_pref cmds1 cmds2;
let obs1 = match !obs1 with Ok v -> ref v | Error exn -> raise exn in
let obs2 = match !obs2 with Ok v -> ref v | Error exn -> raise exn in
let seq_sut = Spec.init () in
let seq_sut = init_sut array_size in
(* we reuse [check_seq_cons] to linearize and interpret sequentially *)
check_seq_cons pref_obs !obs1 !obs2 seq_sut []
check_seq_cons array_size pref_obs !obs1 !obs2 seq_sut []
|| QCheck.Test.fail_reportf " Results incompatible with sequential execution\n\n%s"
@@ Util.print_triple_vertical ~fig_indent:5 ~res_width:35
(fun (c,r) -> Printf.sprintf "%s : %s" (Spec.show_cmd c) (Spec.show_res r))
(pref_obs,!obs1,!obs2)
@@ Util.print_triple_vertical ~fig_indent:5 ~res_width:35 ~init_cmd:init_cmd_ret
(fun (c,r) -> Printf.sprintf "%s : %s" (show_cmd c) (Spec.show_res r))
(pref_obs,!obs1,!obs2)[@@alert "-internal"]

let lin_test ~count ~name =
lin_test ~rep_count:100 ~count ~retries:5 ~name ~lin_prop:lin_prop
Expand Down
5 changes: 3 additions & 2 deletions lib/lin_thread.mli
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ open Lin

(** functor to build an internal module representing concurrent tests *)
module Make_internal (Spec : Internal.CmdSpec [@alert "-internal"]) : sig
val arb_cmds_triple : int -> int -> (Spec.cmd list * Spec.cmd list * Spec.cmd list) QCheck.arbitrary
val lin_prop : (Spec.cmd list * Spec.cmd list * Spec.cmd list) -> bool
[@@@alert "-internal"]
val arb_cmds_triple : int -> int -> Spec.cmd Internal.cmd_triple QCheck.arbitrary
val lin_prop : Spec.cmd Internal.cmd_triple -> bool
val lin_test : count:int -> name:string -> QCheck.Test.t
val neg_lin_test : count:int -> name:string -> QCheck.Test.t
end
Expand Down
3 changes: 2 additions & 1 deletion lib/util.ml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ let print_vertical ?(fig_indent=3) show cmds =
let () = List.iter (fun c -> indent (); print_seq_col c) cmds in
Buffer.contents buf

let print_triple_vertical ?(fig_indent=10) ?(res_width=20) ?(center_prefix=true) show (seq,cmds1,cmds2) =
let print_triple_vertical ?(fig_indent=10) ?(res_width=20) ?(center_prefix=true) ?init_cmd show (seq,cmds1,cmds2) =
let seq,cmds1,cmds2 = List.(map show seq, map show cmds1, map show cmds2) in
let seq = match init_cmd with None -> seq | Some init_cmd -> init_cmd::seq in
let max_width ss = List.fold_left max 0 (List.map String.length ss) in
let width = List.fold_left max 0 [max_width seq; max_width cmds1; max_width cmds2] in
let res_width = max width res_width in
Expand Down
2 changes: 2 additions & 0 deletions lib/util.mli
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ val print_triple_vertical :
?fig_indent:int ->
?res_width:int ->
?center_prefix:bool ->
?init_cmd:string ->
('a -> string) -> 'a list * 'a list * 'a list -> string
(** [print_triple_vertical pr (xs,ys,zs)] returns a string representing a
parallel trace, with [xs] printed first, and then [ys] and [zs] printed
in parallel.
Optional [fig_indent] indicates how many spaces it should be indented (default: 10 spaces).
Optional [res_width] specifies the reserved width for printing each list entry (default: 20 chars).
Optional [center_prefix] centers the sequential prefix if [true] (the default) and otherwise left-adjust it.
Optional [init_cmd] indicates a string-rendered, initial command.
*)

val protect : ('a -> 'b) -> 'a -> ('b, exn) result
Expand Down
Loading