open Stdune open Memo.O module Caml_lazy = Lazy open Dune_tests_common (* CR-someday amokhov: We should split this humongous test file into smaller pieces. *) module Scheduler = struct let t = Test_scheduler.create () let yield () = Test_scheduler.yield t let run f = Test_scheduler.run t f end let () = init () let printf = Printf.printf let () = Memo.Debug.check_invariants := true let print_metrics () = Memo.Metrics.assert_invariants (); printf "%s\n" (Memo.Metrics.report ~reset_after_reporting:true) ;; let string_fn_create name = Memo.create name ~input:(module String) ~cutoff:String.equal let int_fn_create name ~cutoff = Memo.create name ~input:(module Int) ~cutoff (* to run a computation *) let run m = Scheduler.run (Memo.run m) let run_memo f v = try run (Memo.exec f v) with | Memo.Error.E err -> raise (Memo.Error.get err) ;; let run_and_log_errors m = match Scheduler.run (Fiber.collect_errors (fun () -> Memo.run m)) with | Ok res -> res | Error exns -> List.iter exns ~f:(fun exn -> Format.printf "Error: %a@." Pp.to_fmt (Dyn.pp (Exn_with_backtrace.to_dyn exn))) ;; (* the trivial dependencies are simply the identity function *) let compdep x = Memo.return (x ^ x) (* our two dependencies are called some and another *) let mcompdep1 = string_fn_create "some" compdep let mcompdep2 = string_fn_create "another" compdep (* compute the dependencies once so they are present in the global hash table *) let () = ignore (run_memo mcompdep1 "a" : string); ignore (run_memo mcompdep2 "a" : string) ;; (* define a counter so we can track how often our computation has been run *) let counter = ref 0 (* our computation increases the counter, adds the two dependencies, "some" and "another" and works by multiplying the input by two *) let comp x = let+ a = Memo.return x >>= Memo.exec mcompdep1 >>= Memo.exec mcompdep2 in counter := !counter + 1; String.sub a ~pos:0 ~len:(String.length a |> min 3) ;; let mcomp = string_fn_create "test" comp (* running it the first time should increase the counter, running it again should not, but should still return the same result *) let%expect_test _ = Format.printf "%d@." !counter; print_endline (run_memo mcomp "a"); Format.printf "%d@." !counter; print_endline (run_memo mcomp "a"); Format.printf "%d@." !counter; [%expect {| 0 aaa 1 aaa 1 |}] ;; let print_deps memo input = let open Dyn in Memo.For_tests.get_deps memo input |> option (list (pair (option string) Fun.id)) |> print_dyn ;; let%expect_test _ = print_deps mcomp "a"; [%expect {| Some [ (Some "some", "a"); (Some "another", "aa") ] |}] ;; let%expect_test _ = (* running it on a new input should cause it to recompute the first time it is run *) print_endline (run_memo mcomp "hello"); Format.printf "%d@." !counter; print_endline (run_memo mcomp "hello"); Format.printf "%d@." !counter; [%expect {| hel 2 hel 2 |}] ;; let%expect_test _ = (* updating the first dependency should require recomputation of mcomp 7 *) print_endline (run_memo mcompdep1 "testtest"); print_endline (run_memo mcomp "hello"); Format.printf "%d@." !counter; print_endline (run_memo mcomp "hello"); Format.printf "%d@." !counter; [%expect {| testtesttesttest hel 2 hel 2 |}] ;; let stack = ref [] let dump_stack v = let* s = Memo.get_call_stack () in stack := s; Memo.return v ;; let mcompcycle = let mcompcycle = Fdecl.create Dyn.opaque in let compcycle x = let* x = Memo.return x >>= dump_stack in counter := !counter + 1; if !counter < 20 then (x + 1) mod 3 |> Memo.exec (Fdecl.get mcompcycle) else failwith "cycle" in let fn = int_fn_create "cycle" compcycle ~cutoff:Int.equal in Fdecl.set mcompcycle fn; fn ;; let%expect_test _ = counter := 0; try ignore (run_memo mcompcycle 5 : int) with | Memo.Cycle_error.E err -> let cycle = Memo.Cycle_error.get err |> List.filter_map ~f:(Memo.Stack_frame.as_instance_of ~of_:mcompcycle) in print (Pp.enumerate cycle ~f:(Pp.textf "%d")); print (Pp.textf "%d" !counter); !stack |> List.map ~f:(fun st -> let open Dyn in pair (option string) Fun.id (Memo.Stack_frame.name st, Memo.Stack_frame.input st)) |> Dyn.list Fun.id |> print_dyn; [%expect {| - 2 - 1 - 0 4 [ (Some "cycle", 2) ; (Some "cycle", 1) ; (Some "cycle", 0) ; (Some "cycle", 5) ] |}] ;; let mfib = let mfib = Fdecl.create Dyn.opaque in let compfib x = let mfib = Memo.exec (Fdecl.get mfib) in counter := !counter + 1; if x <= 1 then Memo.return x else let* r1 = mfib (x - 1) in let+ r2 = mfib (x - 2) in r1 + r2 in let fn = int_fn_create "fib" compfib ~cutoff:Int.equal in Fdecl.set mfib fn; fn ;; let%expect_test _ = Memo.Metrics.reset (); counter := 0; Format.printf "%d@." (run_memo mfib 2000); Format.printf "%d@." !counter; Format.printf "%d@." (run_memo mfib 1800); Format.printf "%d@." !counter; [%expect {| 2406280077793834213 2001 3080005411477819488 2001 |}]; print_metrics (); [%expect {| Memo graph: 0/0/0 nodes/edges/blocked (restore), 2001/3998/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}] ;; let make_f name = Memo.create name ~cutoff:String.equal let id = let f = make_f "id" ~input:(module String) Memo.return in Memo.exec f ;; module Test_lazy (Lazy : sig type 'a t val create : (unit -> 'a Memo.t) -> 'a t val force : 'a t -> 'a Memo.t end) = struct let lazy_memo = let f = Memo.create "lazy_memo" ~input:(module String) (fun s -> Memo.return (Lazy.create (fun () -> id ("lazy: " ^ s)))) in Memo.exec f ;; let f1_def, f1 = let f = make_f "f1" ~input:(module String) (fun s -> let+ s = lazy_memo s >>= Lazy.force in "f1: " ^ s) in f, Memo.exec f ;; let f2_def, f2 = let f = make_f "f2" ~input:(module String) (fun s -> let+ s = lazy_memo s >>= Lazy.force in "f2: " ^ s) in f, Memo.exec f ;; let run () = run (let* x = f1 "foo" in let* y = f2 "foo" in Memo.return (x, y)) ;; let deps () = let open Dyn in let conv = option (list (pair (option string) Fun.id)) in pair conv conv (Memo.For_tests.get_deps f1_def "foo", Memo.For_tests.get_deps f2_def "foo") ;; end module Builtin_lazy = Test_lazy (struct type 'a t = 'a Memo.t Stdlib.Lazy.t let create = Stdlib.Lazy.from_fun let force = Stdlib.Lazy.force end) let%expect_test _ = Builtin_lazy.run () |> Dyn.(pair string string) |> print_dyn; [%expect {| ("f1: lazy: foo", "f2: lazy: foo") |}] ;; let%expect_test _ = (* This test used to demonstrate a bug due to a bad interaction between [lazy] and synchronous memoized functions. The dependency on [lazy] was only registered by one of the dependents below, which meant we couldn't safely use [lazy] together with [Memo]. Now that [Memo] doesn't support memoization of synchronous functions anymore, we can freely mix [lazy] and [Memo]. *) Builtin_lazy.deps () |> print_dyn; [%expect {| (Some [ (Some "lazy_memo", "foo"); (Some "id", "lazy: foo") ], Some [ (Some "lazy_memo", "foo"); (Some "id", "lazy: foo") ]) |}] ;; module Memo_lazy = Test_lazy (struct include Memo.Lazy (* Here we hide the optional argument [cutoff] of [Memo.Lazy.create]. *) let create f = create f end) let%expect_test _ = Memo_lazy.run () |> Dyn.(pair string string) |> print_dyn; [%expect {| ("f1: lazy: foo", "f2: lazy: foo") |}] ;; let%expect_test _ = Memo_lazy.deps () |> print_dyn; [%expect {| (Some [ (Some "lazy_memo", "foo"); (None, ()) ], Some [ (Some "lazy_memo", "foo"); (None, ()) ]) |}] ;; (* Tests for depending on the current run *) let depends_on_run = Memo.create "foobar" ~input:(module Unit) ~cutoff:Unit.equal (fun () -> let+ (_ : Memo.Run.t) = Memo.current_run () in print_endline "running foobar") ;; let%expect_test _ = run (Memo.exec depends_on_run ()); run (Memo.exec depends_on_run ()); print_endline "resetting memo"; Memo.reset Memo.Invalidation.empty; run (Memo.exec depends_on_run ()); [%expect {| running foobar resetting memo running foobar |}] ;; (* Tests for Memo.Cell *) let%expect_test _ = let f x = Memo.return ("*" ^ x) in let memo = Memo.create "for-cell" ~input:(module String) ~cutoff:String.equal f in let cell = Memo.cell memo "foobar" in print_endline (run (Memo.Cell.read cell)); print_endline (run (Memo.Cell.read cell)); [%expect {| *foobar *foobar |}] ;; let%expect_test "fib linked list" = Memo.Metrics.reset (); let module Element = struct type t = { prev_cell : (int, t) Memo.Cell.t ; value : int ; next_cell : (int, t) Memo.Cell.t } end in let force cell : Element.t Memo.t = Memo.Cell.read cell in let memo_fdecl = Fdecl.create Dyn.opaque in let compute_element x = let memo = Fdecl.get memo_fdecl in printf "computing %d\n" x; let prev_cell = Memo.cell memo (x - 1) in let+ value = if x < 1 then Memo.return 0 else if x = 1 then Memo.return 1 else let* x = force prev_cell and* y = force prev_cell in let+ z = force y.prev_cell in x.value + z.value in { Element.next_cell = Memo.cell memo (x + 1); prev_cell; value } in let memo = Memo.create "fib" ~input:(module Int) compute_element in Fdecl.set memo_fdecl memo; let fourth = run (Memo.exec memo 4) in printf "4th: %d\n" fourth.value; printf "next: %d\n" (run (force fourth.next_cell)).value; let seventh = run (Memo.exec memo 7) in printf "7th: %d\n" seventh.value; printf "prev: %d\n" (run (force seventh.prev_cell)).value; printf "prev: %d\n" (run (let* x = force seventh.prev_cell in force x.prev_cell)) .value; [%expect {| computing 4 computing 3 computing 2 computing 1 computing 0 4th: 3 computing 5 next: 5 computing 7 computing 6 7th: 13 prev: 8 prev: 5 |}]; (* Note that duplicate dependencies are not filtered out. *) print_deps memo 5; [%expect {| Some [ (Some "fib", 4); (Some "fib", 4); (Some "fib", 3) ] |}]; print_metrics (); [%expect {| Memo graph: 0/0/0 nodes/edges/blocked (restore), 8/18/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}] ;; module Function = struct type 'a input = | I : int Type_eq.Id.t * int -> int input | S : string Type_eq.Id.t * string -> string input type 'a output = 'a list let name = "memo-poly" let id (type a) (x : a input) : a Type_eq.Id.t = match x with | I (id, _) -> id | S (id, _) -> id ;; let to_dyn _ = Dyn.Opaque let eval (type a) (x : a input) : a output Memo.t = match x with | I (_, i) -> let* () = Memo.return () in printf "Evaluating %d\n" i; Memo.return (List.init i ~f:(fun i -> i + 1)) | S (_, s) -> let* () = Memo.return () in printf "Evaluating %S\n" s; Memo.return [ s ] ;; let get (type a) (x : a input) : a = match x with | I (_, x) -> x | S (_, x) -> x ;; end let%expect_test "Memo.Poly" = let module M = Memo.Poly (Function) in let (i1 : int Function.input) = I (Type_eq.Id.create (), 1) in let (i2 : int Function.input) = I (Type_eq.Id.create (), 2) in let (s1 : string Function.input) = S (Type_eq.Id.create (), "hi") in let (s2 : string Function.input) = S (Type_eq.Id.create (), "hi again") in let run_int i = let res = run (M.eval i) in Dyn.to_string (Dyn.list Dyn.int res) in let run_string s = let res = run (M.eval s) in Dyn.to_string (Dyn.list Dyn.string res) in printf "----- First-time calls -----\n"; printf "%d -> %s\n" (Function.get i1) (run_int i1); printf "%S -> %s\n" (Function.get s1) (run_string s1); printf "%d -> %s\n" (Function.get i2) (run_int i2); printf "%S -> %s\n" (Function.get s2) (run_string s2); printf "----- Repeated calls (memoized) -----\n"; printf "%d -> %s\n" (Function.get i1) (run_int i1); printf "%S -> %s\n" (Function.get s1) (run_string s1); printf "%d -> %s\n" (Function.get i2) (run_int i2); printf "%S -> %s\n" (Function.get s2) (run_string s2); [%expect {| ----- First-time calls ----- Evaluating 1 1 -> [ 1 ] Evaluating "hi" "hi" -> [ "hi" ] Evaluating 2 2 -> [ 1; 2 ] Evaluating "hi again" "hi again" -> [ "hi again" ] ----- Repeated calls (memoized) ----- 1 -> [ 1 ] "hi" -> [ "hi" ] 2 -> [ 1; 2 ] "hi again" -> [ "hi again" ] |}] ;; let print_cycle_error cycle_error = let frames = Memo.Cycle_error.get cycle_error in printf "Dependency cycle detected:\n"; List.iteri frames ~f:(fun i frame -> let called_by = match i with | 0 -> "" | _ -> "called by " in printf "- %s%s\n" called_by (Dyn.to_string (Memo.Stack_frame.to_dyn frame))) ;; let print_result arg res = let res = Result.map_error res ~f: (List.map ~f: (Exn_with_backtrace.map ~f:(fun exn -> match exn with | Memo.Cycle_error.E error -> print_cycle_error error; exn | _ -> exn))) in let open Dyn in Format.printf "f %d = %a@." arg Pp.to_fmt (Dyn.pp (Result.to_dyn int (list Exn_with_backtrace.to_dyn) res)) ;; let run_collect_errors f = let open Fiber.O in Fiber.collect_errors (fun () -> Memo.run (f ())) >>| function | Ok _ as res -> res | Error errs -> Error (List.map errs ~f:(fun (e : Exn_with_backtrace.t) -> match e.exn with | Memo.Error.E err -> { e with exn = Memo.Error.get err } | _ -> e)) ;; let evaluate_and_print f x = let res = try Fiber.run ~iter:(fun () -> raise Exit) (run_collect_errors (fun () -> Memo.exec f x)) with | exn -> Error [ Exn_with_backtrace.capture exn ] in print_result x res ;; let%expect_test "error handling and memo" = let f = int_fn_create "f" ~cutoff:Int.equal (fun x -> printf "Calling f %d\n" x; if x = 42 then failwith "42" else if x = 84 then Memo.fork_and_join_unit (fun () -> failwith "left") (fun () -> failwith "right") else Memo.return x) in let test x = evaluate_and_print f x in test 20; test 20; test 42; test 42; test 84; test 84; [%expect {| Calling f 20 f 20 = Ok 20 f 20 = Ok 20 Calling f 42 f 42 = Error [ { exn = "Failure(\"42\")"; backtrace = "" } ] f 42 = Error [ { exn = "Failure(\"42\")"; backtrace = "" } ] Calling f 84 f 84 = Error [ { exn = "Failure(\"left\")"; backtrace = "" } ; { exn = "Failure(\"right\")"; backtrace = "" } ] f 84 = Error [ { exn = "Failure(\"left\")"; backtrace = "" } ; { exn = "Failure(\"right\")"; backtrace = "" } ] |}] ;; (* A test function counting runs. *) let count_runs name = let counter = ref 0 in fun () -> printf "Started evaluating %s\n" name; incr counter; let result = !counter in let+ (_ : Memo.Run.t) = Memo.current_run () in printf "Evaluated %s: %d\n" name result; result ;; (* A test function incrementing a given memo. *) let increment which which_memo () = printf "Started evaluating %s\n" which; let+ input = Memo.exec which_memo () in let result = input + 1 in printf "Evaluated %s: %d\n" which result; result ;; (* Create a memoization node with or without cutoff. *) let create ~with_cutoff name f = let cutoff = Option.some_if with_cutoff Int.equal in Memo.create name ~input:(module Unit) ?cutoff f ;; let%expect_test "diamond with non-uniform cutoff structure" = let base = create ~with_cutoff:true "base" (count_runs "base") in let length_of_base which () = printf "Started evaluating %s\n" which; let+ base = Memo.exec base () in let result = String.length (Int.to_string base) in printf "Evaluated %s: %d\n" which result; result in let no_cutoff = create ~with_cutoff:false "no_cutoff" (length_of_base "no_cutoff") in let yes_cutoff = create ~with_cutoff:true "yes_cutoff" (length_of_base "yes_cutoff") in let after_no_cutoff = create ~with_cutoff:true "after_no_cutoff" (increment "after_no_cutoff" no_cutoff) in let after_yes_cutoff = create ~with_cutoff:true "after_yes_cutoff" (increment "after_yes_cutoff" yes_cutoff) in let summit offset = printf "Started evaluating summit with offset %d\n" offset; let+ after_no_cutoff, after_yes_cutoff = let* x = Memo.exec after_no_cutoff () in let+ y = Memo.exec after_yes_cutoff () in x, y in let result = after_no_cutoff + after_yes_cutoff + offset in printf "Evaluated summit with offset %d: %d\n" offset result; result in let summit = Memo.create "summit" ~input:(module Int) summit in Memo.Metrics.reset (); evaluate_and_print summit 0; print_metrics (); [%expect {| Started evaluating summit with offset 0 Started evaluating after_no_cutoff Started evaluating no_cutoff Started evaluating base Evaluated base: 1 Evaluated no_cutoff: 1 Evaluated after_no_cutoff: 2 Started evaluating after_yes_cutoff Started evaluating yes_cutoff Evaluated yes_cutoff: 1 Evaluated after_yes_cutoff: 2 Evaluated summit with offset 0: 4 f 0 = Ok 4 Memo graph: 0/0/0 nodes/edges/blocked (restore), 6/7/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.Metrics.reset (); evaluate_and_print summit 1; print_metrics (); [%expect {| Started evaluating summit with offset 1 Evaluated summit with offset 1: 5 f 1 = Ok 5 Memo graph: 0/0/0 nodes/edges/blocked (restore), 1/2/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.reset Memo.Invalidation.empty; evaluate_and_print summit 0; print_metrics (); [%expect {| Started evaluating base Evaluated base: 2 Started evaluating after_no_cutoff Started evaluating no_cutoff Evaluated no_cutoff: 1 Evaluated after_no_cutoff: 2 Started evaluating yes_cutoff Evaluated yes_cutoff: 1 f 0 = Ok 4 Memo graph: 6/7/0 nodes/edges/blocked (restore), 5/4/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.Metrics.reset (); evaluate_and_print summit 1; print_metrics (); [%expect {| f 1 = Ok 5 Memo graph: 1/2/0 nodes/edges/blocked (restore), 0/0/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.Metrics.reset (); evaluate_and_print summit 2; print_metrics (); [%expect {| Started evaluating summit with offset 2 Evaluated summit with offset 2: 6 f 2 = Ok 6 Memo graph: 0/0/0 nodes/edges/blocked (restore), 1/2/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}] ;; (* The test below sets up the following situation: - In the initial run, there are no dependency cycles. - In the second run, [base_or_summit] gets an additional dynamic dependency and eventually cycles back to itself. - In all subsequent runs, we are back to having no dependency cycles. The dependency chains in the new test have alternating cutoff/no-cutoff structure, to make sure that cycle detection can handle such cases. *) let%expect_test "dynamic cycles with non-uniform cutoff structure" = let base = create ~with_cutoff:true "base" (count_runs "base") in let first_base_then_summit which ~summit_fdecl () = printf "Started evaluating %s\n" which; let* base = Memo.exec base () in match base with | 2 as input -> let summit = Fdecl.get summit_fdecl in printf "Cycling to summit from %s...\n" which; let+ result = Memo.exec summit input in printf "Miraculously evaluated %s: %d\n" which result; result | input -> printf "Evaluated %s: %d\n" which input; Memo.return input in let rec incrementing_chain ~end_with_cutoff ~from n = match n with | 0 -> from | _ -> let from = incrementing_chain ~end_with_cutoff:(not end_with_cutoff) ~from (n - 1) in let cutoff = match end_with_cutoff with | false -> "_no_cutoff" | true -> "_yes_cutoff" in let name = "incrementing_chain_" ^ Int.to_string n ^ cutoff in create ~with_cutoff:end_with_cutoff name (increment name from) in let incrementing_chain_plus_input ~end_with_cutoff ~from = let chain = incrementing_chain ~end_with_cutoff:(not end_with_cutoff) ~from 4 in let plus_input input = printf "Started evaluating the summit with input %d\n" input; let+ result = Memo.exec chain () in let result = result + input in printf "Evaluated the summit with input %d: %d\n" input result; result in let cutoff = Option.some_if end_with_cutoff Int.equal in Memo.create "incrementing_chain_plus_input" ~input:(module Int) ?cutoff plus_input in let summit_fdecl = Fdecl.create (fun _ -> Dyn.Opaque) in let cycle_creator_no_cutoff = create ~with_cutoff:false "cycle_creator_no_cutoff" (first_base_then_summit "cycle_creator_no_cutoff" ~summit_fdecl) in let summit_no_cutoff = incrementing_chain_plus_input ~end_with_cutoff:false ~from:cycle_creator_no_cutoff in Fdecl.set summit_fdecl summit_no_cutoff; let summit_fdecl = Fdecl.create (fun _ -> Dyn.Opaque) in let cycle_creator_yes_cutoff = create ~with_cutoff:true "cycle_creator_yes_cutoff" (first_base_then_summit "cycle_creator_yes_cutoff" ~summit_fdecl) in let summit_yes_cutoff = incrementing_chain_plus_input ~end_with_cutoff:true ~from:cycle_creator_yes_cutoff in Fdecl.set summit_fdecl summit_yes_cutoff; (* Calling [Memo.exec] and then not running the resulting [Fiber.t] used to bring the memoization framework into an inconsistent internal state, due to the eager execution of some internal side effects. That further manifested in deadlocks and reappearance of zombie computations. The problem has now been fixed and so the line below is just a no-op. *) Memo.Metrics.reset (); let (_ : int Memo.t) = Memo.exec cycle_creator_no_cutoff () in print_metrics (); [%expect {| Memo graph: 0/0/0 nodes/edges/blocked (restore), 0/0/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.Metrics.reset (); evaluate_and_print summit_no_cutoff 0; print_metrics (); [%expect {| Started evaluating the summit with input 0 Started evaluating incrementing_chain_4_yes_cutoff Started evaluating incrementing_chain_3_no_cutoff Started evaluating incrementing_chain_2_yes_cutoff Started evaluating incrementing_chain_1_no_cutoff Started evaluating cycle_creator_no_cutoff Started evaluating base Evaluated base: 1 Evaluated cycle_creator_no_cutoff: 1 Evaluated incrementing_chain_1_no_cutoff: 2 Evaluated incrementing_chain_2_yes_cutoff: 3 Evaluated incrementing_chain_3_no_cutoff: 4 Evaluated incrementing_chain_4_yes_cutoff: 5 Evaluated the summit with input 0: 5 f 0 = Ok 5 Memo graph: 0/0/0 nodes/edges/blocked (restore), 7/7/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.Metrics.reset (); evaluate_and_print summit_yes_cutoff 0; print_metrics (); [%expect {| Started evaluating the summit with input 0 Started evaluating incrementing_chain_4_no_cutoff Started evaluating incrementing_chain_3_yes_cutoff Started evaluating incrementing_chain_2_no_cutoff Started evaluating incrementing_chain_1_yes_cutoff Started evaluating cycle_creator_yes_cutoff Evaluated cycle_creator_yes_cutoff: 1 Evaluated incrementing_chain_1_yes_cutoff: 2 Evaluated incrementing_chain_2_no_cutoff: 3 Evaluated incrementing_chain_3_yes_cutoff: 4 Evaluated incrementing_chain_4_no_cutoff: 5 Evaluated the summit with input 0: 5 f 0 = Ok 5 Memo graph: 0/0/0 nodes/edges/blocked (restore), 6/6/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.Metrics.reset (); evaluate_and_print summit_no_cutoff 2; print_metrics (); [%expect {| Started evaluating the summit with input 2 Evaluated the summit with input 2: 7 f 2 = Ok 7 Memo graph: 0/0/0 nodes/edges/blocked (restore), 1/1/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.Metrics.reset (); evaluate_and_print summit_yes_cutoff 2; print_metrics (); [%expect {| Started evaluating the summit with input 2 Evaluated the summit with input 2: 7 f 2 = Ok 7 Memo graph: 0/0/0 nodes/edges/blocked (restore), 1/1/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.reset Memo.Invalidation.empty; evaluate_and_print summit_no_cutoff 0; print_metrics (); [%expect {| Started evaluating base Evaluated base: 2 Started evaluating incrementing_chain_2_yes_cutoff Started evaluating incrementing_chain_1_no_cutoff Started evaluating cycle_creator_no_cutoff Cycling to summit from cycle_creator_no_cutoff... Started evaluating incrementing_chain_4_yes_cutoff Started evaluating incrementing_chain_3_no_cutoff Started evaluating the summit with input 0 Dependency cycle detected: - ("incrementing_chain_plus_input", 2) - called by ("cycle_creator_no_cutoff", ()) - called by ("incrementing_chain_1_no_cutoff", ()) - called by ("incrementing_chain_2_yes_cutoff", ()) - called by ("incrementing_chain_3_no_cutoff", ()) - called by ("incrementing_chain_4_yes_cutoff", ()) f 0 = Error [ { exn = "Cycle_error.E\n\ \ [ (\"incrementing_chain_plus_input\", 2)\n\ \ ; (\"cycle_creator_no_cutoff\", ())\n\ \ ; (\"incrementing_chain_1_no_cutoff\", ())\n\ \ ; (\"incrementing_chain_2_yes_cutoff\", ())\n\ \ ; (\"incrementing_chain_3_no_cutoff\", ())\n\ \ ; (\"incrementing_chain_4_yes_cutoff\", ())\n\ \ ]" ; backtrace = "" } ] Memo graph: 8/8/1 nodes/edges/blocked (restore), 8/7/0 nodes/edges/blocked (compute) Memo cycle detection graph: 6/5/1 nodes/edges/paths |}]; Memo.Metrics.reset (); evaluate_and_print summit_yes_cutoff 0; print_metrics (); [%expect {| Started evaluating cycle_creator_yes_cutoff Cycling to summit from cycle_creator_yes_cutoff... Started evaluating incrementing_chain_1_yes_cutoff Started evaluating incrementing_chain_3_yes_cutoff Started evaluating incrementing_chain_2_no_cutoff Started evaluating the summit with input 0 Started evaluating incrementing_chain_4_no_cutoff Dependency cycle detected: - ("incrementing_chain_plus_input", 2) - called by ("cycle_creator_yes_cutoff", ()) - called by ("incrementing_chain_1_yes_cutoff", ()) - called by ("incrementing_chain_2_no_cutoff", ()) - called by ("incrementing_chain_3_yes_cutoff", ()) - called by ("incrementing_chain_4_no_cutoff", ()) f 0 = Error [ { exn = "Cycle_error.E\n\ \ [ (\"incrementing_chain_plus_input\", 2)\n\ \ ; (\"cycle_creator_yes_cutoff\", ())\n\ \ ; (\"incrementing_chain_1_yes_cutoff\", ())\n\ \ ; (\"incrementing_chain_2_no_cutoff\", ())\n\ \ ; (\"incrementing_chain_3_yes_cutoff\", ())\n\ \ ; (\"incrementing_chain_4_no_cutoff\", ())\n\ \ ]" ; backtrace = "" } ] Memo graph: 7/7/1 nodes/edges/blocked (restore), 6/6/0 nodes/edges/blocked (compute) Memo cycle detection graph: 6/5/1 nodes/edges/paths |}]; Memo.Metrics.reset (); evaluate_and_print summit_no_cutoff 2; print_metrics (); [%expect {| Dependency cycle detected: - ("incrementing_chain_4_yes_cutoff", ()) - called by ("incrementing_chain_plus_input", 2) - called by ("cycle_creator_no_cutoff", ()) - called by ("incrementing_chain_1_no_cutoff", ()) - called by ("incrementing_chain_2_yes_cutoff", ()) - called by ("incrementing_chain_3_no_cutoff", ()) f 2 = Error [ { exn = "Cycle_error.E\n\ \ [ (\"incrementing_chain_4_yes_cutoff\", ())\n\ \ ; (\"incrementing_chain_plus_input\", 2)\n\ \ ; (\"cycle_creator_no_cutoff\", ())\n\ \ ; (\"incrementing_chain_1_no_cutoff\", ())\n\ \ ; (\"incrementing_chain_2_yes_cutoff\", ())\n\ \ ; (\"incrementing_chain_3_no_cutoff\", ())\n\ \ ]" ; backtrace = "" } ] Memo graph: 0/0/0 nodes/edges/blocked (restore), 0/0/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.Metrics.reset (); evaluate_and_print summit_yes_cutoff 2; print_metrics (); [%expect {| Dependency cycle detected: - ("incrementing_chain_4_no_cutoff", ()) - called by ("incrementing_chain_plus_input", 2) - called by ("cycle_creator_yes_cutoff", ()) - called by ("incrementing_chain_1_yes_cutoff", ()) - called by ("incrementing_chain_2_no_cutoff", ()) - called by ("incrementing_chain_3_yes_cutoff", ()) f 2 = Error [ { exn = "Cycle_error.E\n\ \ [ (\"incrementing_chain_4_no_cutoff\", ())\n\ \ ; (\"incrementing_chain_plus_input\", 2)\n\ \ ; (\"cycle_creator_yes_cutoff\", ())\n\ \ ; (\"incrementing_chain_1_yes_cutoff\", ())\n\ \ ; (\"incrementing_chain_2_no_cutoff\", ())\n\ \ ; (\"incrementing_chain_3_yes_cutoff\", ())\n\ \ ]" ; backtrace = "" } ] Memo graph: 0/0/0 nodes/edges/blocked (restore), 0/0/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.reset Memo.Invalidation.empty; evaluate_and_print summit_no_cutoff 0; print_metrics (); [%expect {| Started evaluating base Evaluated base: 3 Started evaluating incrementing_chain_2_yes_cutoff Started evaluating incrementing_chain_1_no_cutoff Started evaluating cycle_creator_no_cutoff Evaluated cycle_creator_no_cutoff: 3 Evaluated incrementing_chain_1_no_cutoff: 4 Evaluated incrementing_chain_2_yes_cutoff: 5 Started evaluating incrementing_chain_4_yes_cutoff Started evaluating incrementing_chain_3_no_cutoff Evaluated incrementing_chain_3_no_cutoff: 6 Evaluated incrementing_chain_4_yes_cutoff: 7 Started evaluating the summit with input 0 Evaluated the summit with input 0: 7 f 0 = Ok 7 Memo graph: 7/7/0 nodes/edges/blocked (restore), 8/7/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.Metrics.reset (); evaluate_and_print summit_yes_cutoff 0; print_metrics (); [%expect {| Started evaluating cycle_creator_yes_cutoff Evaluated cycle_creator_yes_cutoff: 3 Started evaluating incrementing_chain_1_yes_cutoff Evaluated incrementing_chain_1_yes_cutoff: 4 Started evaluating incrementing_chain_3_yes_cutoff Started evaluating incrementing_chain_2_no_cutoff Evaluated incrementing_chain_2_no_cutoff: 5 Evaluated incrementing_chain_3_yes_cutoff: 6 Started evaluating the summit with input 0 Started evaluating incrementing_chain_4_no_cutoff Evaluated incrementing_chain_4_no_cutoff: 7 Evaluated the summit with input 0: 7 f 0 = Ok 7 Memo graph: 6/6/0 nodes/edges/blocked (restore), 6/6/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.Metrics.reset (); evaluate_and_print summit_no_cutoff 2; print_metrics (); [%expect {| Started evaluating the summit with input 2 Evaluated the summit with input 2: 9 f 2 = Ok 9 Memo graph: 1/0/0 nodes/edges/blocked (restore), 1/1/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.Metrics.reset (); evaluate_and_print summit_yes_cutoff 2; print_metrics (); [%expect {| Started evaluating the summit with input 2 Evaluated the summit with input 2: 9 f 2 = Ok 9 Memo graph: 1/0/0 nodes/edges/blocked (restore), 1/1/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}] ;; (* This test used to demonstrate possible deadlocks in Memo because it led to creating the same cycle twice in the cycle detection graph, which is not supported by our incremental cycle detection library. Since then we made some changes to Memo that make it impossible to create the same cycle twice, because the first cycle creation is cached. *) (* CR-someday amokhov: Note that it may still be possible to trigger deadlocks in Memo by creating two different cycles in the same build run. It would be nice to add a test demonstrating this scenario. *) let%expect_test "No deadlocks when creating the same cycle twice" = let fdecl_base = Fdecl.create (fun _ -> Dyn.Opaque) in let cycle_creator = create ~with_cutoff:true "cycle_creator" (fun () -> printf "Started evaluating cycle_creator\n"; let base = Fdecl.get fdecl_base in let+ result = let+ bases = Memo.of_reproducible_fiber (Fiber.parallel_map [ (); () ] ~f:(fun () -> Memo.run (Memo.exec base ()))) in match bases with | [ base1; base2 ] -> base1 + base2 | _ -> assert false in printf "Miraculously evaluated cycle_creator: %d\n" result; result) in let base = create ~with_cutoff:true "base" (fun () -> printf "Started evaluating base\n"; let+ result = Memo.exec cycle_creator () in printf "Miraculously evaluated base: %d\n" result; result) in Fdecl.set fdecl_base base; let middle = create ~with_cutoff:true "middle" (fun () -> printf "Started evaluating middle\n"; let+ result = Memo.exec base () in printf "Miraculously evaluated middle: %d\n" result; result) in let summit = Memo.create "summit" ~input:(module Int) (fun offset -> printf "Started evaluating summit\n"; let+ middle = Memo.exec middle () in let result = middle + offset in printf "Miraculously evaluated summit: %d\n" result; result) in evaluate_and_print summit 0; evaluate_and_print summit 1; [%expect {| Started evaluating summit Started evaluating middle Started evaluating base Started evaluating cycle_creator Dependency cycle detected: - ("cycle_creator", ()) - called by ("base", ()) f 0 = Error [ { exn = "Cycle_error.E [ (\"cycle_creator\", ()); (\"base\", ()) ]" ; backtrace = "" } ] Started evaluating summit Dependency cycle detected: - ("cycle_creator", ()) - called by ("base", ()) f 1 = Error [ { exn = "Cycle_error.E [ (\"cycle_creator\", ()); (\"base\", ()) ]" ; backtrace = "" } ] |}]; Memo.reset Memo.Invalidation.empty; evaluate_and_print summit 0; evaluate_and_print summit 2; [%expect {| Dependency cycle detected: - ("base", ()) - called by ("cycle_creator", ()) f 0 = Error [ { exn = "Cycle_error.E [ (\"base\", ()); (\"cycle_creator\", ()) ]" ; backtrace = "" } ] Started evaluating summit Dependency cycle detected: - ("base", ()) - called by ("cycle_creator", ()) f 2 = Error [ { exn = "Cycle_error.E [ (\"base\", ()); (\"cycle_creator\", ()) ]" ; backtrace = "" } ] |}] ;; let lazy_rec ~name f = let fdecl = Fdecl.create (fun _ -> Dyn.Opaque) in let node = Memo.Lazy.create ~name (fun () -> f (Fdecl.get fdecl)) in Fdecl.set fdecl node; node ;; let%expect_test "two similar, but not physically-equal, cycle errors" = let cycle1 = lazy_rec ~name:"cycle" (fun node -> Memo.Lazy.force node) in let cycle2 = lazy_rec ~name:"cycle" (fun node -> Memo.Lazy.force node) in let both = Memo.Lazy.create ~name:"both" (fun () -> Memo.fork_and_join_unit (fun () -> Memo.Lazy.force cycle1) (fun () -> Memo.Lazy.force cycle2)) in run_and_log_errors (Memo.Lazy.force both); (* Even though these errors look similar, they are actually talking about two different cycles which can be distinguished by the internal node ids, so they are not deduplicated. *) [%expect {| Error: { exn = "Memo.Error.E\n\ \ { exn = \"Cycle_error.E [ (\\\"cycle\\\", ()) ]\"; stack = [ (\"both\", ()) ] }" ; backtrace = "" } Error: { exn = "Memo.Error.E\n\ \ { exn = \"Cycle_error.E [ (\\\"cycle\\\", ()) ]\"; stack = [ (\"both\", ()) ] }" ; backtrace = "" } |}] ;; let%expect_test "Nested nodes with cutoff are recomputed optimally" = let counter = create ~with_cutoff:false "counter" (count_runs "counter") in let summit = Memo.create "summit" ~input:(module Int) (fun offset -> printf "Started evaluating summit\n"; let middle = create ~with_cutoff:false "middle" (fun () -> printf "Started evaluating middle\n"; let base = create ~with_cutoff:false "base" (fun () -> printf "Started evaluating base\n"; let+ result = Memo.exec counter () in printf "Evaluated middle: %d\n" result; result) in let+ result = Memo.exec base () in printf "Evaluated middle: %d\n" result; result) in let+ middle = Memo.exec middle () in let result = middle + offset in printf "Evaluated summit: %d\n" result; result) in Memo.Metrics.reset (); evaluate_and_print summit 0; evaluate_and_print summit 1; print_metrics (); (* In the first run, everything is OK. *) [%expect {| Started evaluating summit Started evaluating middle Started evaluating base Started evaluating counter Evaluated counter: 1 Evaluated middle: 1 Evaluated middle: 1 Evaluated summit: 1 f 0 = Ok 1 Started evaluating summit Started evaluating middle Started evaluating base Evaluated middle: 1 Evaluated middle: 1 Evaluated summit: 2 f 1 = Ok 2 Memo graph: 0/0/0 nodes/edges/blocked (restore), 8/7/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.reset Memo.Invalidation.empty; evaluate_and_print summit 0; evaluate_and_print summit 2; print_metrics (); (* In the second run, we don't recompute [base] three times as we did before. *) [%expect {| Started evaluating summit Started evaluating middle Started evaluating base Started evaluating counter Evaluated counter: 2 Evaluated middle: 2 Evaluated middle: 2 Evaluated summit: 2 f 0 = Ok 2 Started evaluating summit Started evaluating middle Started evaluating base Evaluated middle: 2 Evaluated middle: 2 Evaluated summit: 4 f 2 = Ok 4 Memo graph: 4/4/0 nodes/edges/blocked (restore), 8/7/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}] ;; (* In addition to its direct purpose, this test also: (i) demonstrates what happens in the presence of non-determinism; and (ii) tests cell invalidation. *) let%expect_test "Test that there are no phantom dependencies" = let counter = ref 0 in let const_8 = create ~with_cutoff:false "base" (fun () -> let result = 8 in printf "base = %d\n" result; Memo.return result) in let cell = Memo.cell const_8 () in let summit = Memo.create "summit" ~input:(module Int) (fun offset -> printf "Started evaluating summit\n"; let middle = create ~with_cutoff:false "middle" (fun () -> incr counter; match !counter with | 1 -> printf "*** middle depends on base ***\n"; Memo.Cell.read cell | _ -> printf "*** middle does not depend on base ***\n"; Memo.return 0) in let+ middle = Memo.exec middle () in let result = middle + offset in printf "Evaluated summit: %d\n" result; result) in evaluate_and_print summit 0; print_metrics (); [%expect {| Started evaluating summit *** middle depends on base *** base = 8 Evaluated summit: 8 f 0 = Ok 8 Memo graph: 0/0/0 nodes/edges/blocked (restore), 3/2/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.reset Memo.Invalidation.empty; evaluate_and_print summit 0; print_metrics (); (* No recomputation is needed since the [cell] is up to date. *) [%expect {| f 0 = Ok 8 Memo graph: 3/2/0 nodes/edges/blocked (restore), 0/0/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.reset (Memo.Cell.invalidate ~reason:Test cell); evaluate_and_print summit 0; (* Note that we no longer depend on the [cell]. *) [%expect {| Started evaluating summit *** middle does not depend on base *** Evaluated summit: 0 f 0 = Ok 0 |}]; Memo.reset (Memo.Cell.invalidate ~reason:Test cell); evaluate_and_print summit 0; print_metrics (); (* [middle] is not recomputed, since it no longer depends on the [cell]. In the past, the [cell] remained as a "phantom dependency" causing unnecessary recomputations. *) [%expect {| f 0 = Ok 0 Memo graph: 4/3/0 nodes/edges/blocked (restore), 2/1/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}] ;; let%expect_test "Abandoned node with no cutoff is recomputed" = let count_runs = count_runs "base" in let which_base = ref 0 in let base () = incr which_base; printf "Created base #%d\n" !which_base; create ~with_cutoff:false "base" count_runs in let last_created_base = ref None in let captured_base = ref None in let middle = Memo.create "middle" ~input:(module Unit) (fun () -> printf "Started evaluating middle\n"; let base = base () in last_created_base := Some base; let+ result = Memo.exec base () in printf "Evaluated middle: %d\n" result; result) in let summit = Memo.create "summit" ~input:(module Int) (fun input -> printf "Started evaluating summit\n"; let* middle = Memo.exec middle () in let+ result = match middle with | 1 -> printf "*** Captured last base ***\n"; captured_base := !last_created_base; Memo.exec (Option.value_exn !captured_base) () | 2 -> printf "*** Abandoned captured base ***\n"; Memo.return input | _ -> printf "*** Recalled captured base ***\n"; Memo.exec (Option.value_exn !captured_base) () in printf "Evaluated summit: %d\n" result; result) in Memo.reset Memo.Invalidation.empty; evaluate_and_print summit 0; print_metrics (); [%expect {| Started evaluating summit Started evaluating middle Created base #1 Started evaluating base Evaluated base: 1 Evaluated middle: 1 *** Captured last base *** Evaluated summit: 1 f 0 = Ok 1 Memo graph: 0/0/0 nodes/edges/blocked (restore), 4/4/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.reset Memo.Invalidation.empty; evaluate_and_print summit 0; print_metrics (); [%expect {| Started evaluating summit Started evaluating middle Created base #2 Started evaluating base Evaluated base: 2 Evaluated middle: 2 *** Abandoned captured base *** Evaluated summit: 0 f 0 = Ok 0 Memo graph: 3/3/0 nodes/edges/blocked (restore), 4/3/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; (* At this point, [captured_base] is a stale computation: [restore_from_cache] failed but [compute] never started. *) Memo.reset Memo.Invalidation.empty; evaluate_and_print summit 0; print_metrics (); (* We will now attempt to force [compute] of a stale computation but this is handled correctly by restarting the computation. Note that this causes an additional increment of the counter, thus leading to an inconsistent value of [base] observed by the [middle] (3) and [summit] (4) nodes. *) [%expect {| Started evaluating summit Started evaluating middle Created base #3 Started evaluating base Evaluated base: 3 Evaluated middle: 3 *** Recalled captured base *** Started evaluating base Evaluated base: 4 Evaluated summit: 4 f 0 = Ok 4 Memo graph: 3/3/0 nodes/edges/blocked (restore), 5/5/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}] ;; let print_exns f = let res = match Fiber.run ~iter:(fun () -> raise Exit) (run_collect_errors f) with | Ok _ -> assert false | Error exns -> Error (List.map exns ~f:(fun (e : Exn_with_backtrace.t) -> e.exn)) | exception exn -> Error [ exn ] in let open Dyn in Format.printf "%a@." Pp.to_fmt (Dyn.pp (Result.to_dyn unit (list Exn.to_dyn) res)) ;; let%expect_test "error handling with diamonds" = Printexc.record_backtrace true; let f_impl = Fdecl.create Dyn.opaque in let f = int_fn_create "error-diamond: f" ~cutoff:Unit.equal (fun x -> Fdecl.get f_impl x) in Fdecl.set f_impl (fun x -> printf "Calling f %d\n" x; if x = 0 then failwith "reached 0" else Memo.fork_and_join_unit (fun () -> Memo.exec f (x - 1)) (fun () -> Memo.exec f (x - 1))); let test x = print_exns (fun () -> Memo.exec f x) in test 0; [%expect {| Calling f 0 Error [ "Failure(\"reached 0\")" ] |}]; test 1; [%expect {| Calling f 1 Error [ "Failure(\"reached 0\")" ] |}]; test 2; [%expect {| Calling f 2 Error [ "Failure(\"reached 0\")" ] |}] ;; let%expect_test "error handling and duplicate exceptions" = Printexc.record_backtrace true; let f_impl = Fdecl.create Dyn.opaque in let f = int_fn_create "test8: duplicate-exception: f" ~cutoff:Unit.equal (fun x -> Fdecl.get f_impl x) in let fail = int_fn_create "test8: fail" ~cutoff:Unit.equal (fun _x -> failwith "42") in let forward_fail = int_fn_create "test8: forward fail" ~cutoff:Unit.equal (fun x -> Memo.exec fail x) in let forward_fail2 = int_fn_create "test8: forward fail2" ~cutoff:Unit.equal (fun x -> Memo.exec fail x) in Fdecl.set f_impl (fun x -> printf "Calling f %d\n" x; match x with | 0 -> Memo.exec forward_fail x | 1 -> Memo.exec forward_fail2 x | _ -> Memo.fork_and_join_unit (fun () -> Memo.exec f (x - 1)) (fun () -> Memo.exec f (x - 2))); let test x = print_exns (fun () -> Memo.exec f x) in test 2; [%expect {| Calling f 2 Calling f 1 Calling f 0 Error [ "Failure(\"42\")" ] |}] ;; let%expect_test "reproducible errors are cached" = Printexc.record_backtrace false; let f = Memo.create "area of a square" ~input:(module Int) (fun x -> printf "Started evaluating %d\n" x; if x < 0 then failwith (sprintf "Negative input %d" x); if x = 0 then raise (Memo.Non_reproducible (Failure "Zero input")); let res = x * x in printf "Evaluated %d: %d\n" x res; Memo.return res) in Memo.Metrics.reset (); evaluate_and_print f 5; evaluate_and_print f (-5); evaluate_and_print f 0; (* Note that the [Non_reproducible] wrapper has been removed. *) print_metrics (); [%expect {| Started evaluating 5 Evaluated 5: 25 f 5 = Ok 25 Started evaluating -5 f -5 = Error [ { exn = "Failure(\"Negative input -5\")"; backtrace = "" } ] Started evaluating 0 f 0 = Error [ { exn = "Failure(\"Zero input\")"; backtrace = "" } ] Memo graph: 0/0/0 nodes/edges/blocked (restore), 3/0/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.Metrics.reset (); evaluate_and_print f 5; evaluate_and_print f (-5); evaluate_and_print f 0; print_metrics (); (* Note that we do not see any "Started evaluating" messages because both [Ok] and [Error] results have been cached. *) [%expect {| f 5 = Ok 25 f -5 = Error [ { exn = "Failure(\"Negative input -5\")"; backtrace = "" } ] f 0 = Error [ { exn = "Failure(\"Zero input\")"; backtrace = "" } ] Memo graph: 0/0/0 nodes/edges/blocked (restore), 0/0/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.reset Memo.Invalidation.empty; evaluate_and_print f 5; evaluate_and_print f (-5); evaluate_and_print f 0; print_metrics (); (* Here we re-execute only one computation: the one that corresponds to the non-reproducible error. *) [%expect {| f 5 = Ok 25 f -5 = Error [ { exn = "Failure(\"Negative input -5\")"; backtrace = "" } ] Started evaluating 0 f 0 = Error [ { exn = "Failure(\"Zero input\")"; backtrace = "" } ] Memo graph: 3/0/0 nodes/edges/blocked (restore), 1/0/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}] ;; let%expect_test "errors work with early cutoff" = let divide = let exception Input_too_large of Memo.Run.t in let first_run = Memo.Run.For_tests.current () in Printexc.register_printer (fun exn -> match exn with | Input_too_large run -> Some (sprintf "Input_too_large <%s run>" (if Memo.Run.For_tests.compare first_run run = Eq then "first" else "second")) | _ -> None); Memo.create "divide 100 by input" ~input:(module Int) ~cutoff:Int.equal (fun x -> let+ run = Memo.current_run () in printf "[divide] Started evaluating %d\n" x; if x > 100 then (* This exception will be different in each run. *) raise (Input_too_large run); let res = 100 / x in printf "[divide] Evaluated %d: %d\n" x res; res) in let f = Memo.create "Negate" ~input:(module Int) (fun x -> printf "[negate] Started evaluating %d\n" x; let+ res = Memo.exec divide x >>| Stdlib.Int.neg in printf "[negate] Evaluated %d: %d\n" x res; res) in Memo.Metrics.reset (); evaluate_and_print f 0; evaluate_and_print f 20; evaluate_and_print f 200; print_metrics (); [%expect {| [negate] Started evaluating 0 [divide] Started evaluating 0 f 0 = Error [ { exn = "Division_by_zero"; backtrace = "" } ] [negate] Started evaluating 20 [divide] Started evaluating 20 [divide] Evaluated 20: 5 [negate] Evaluated 20: -5 f 20 = Ok -5 [negate] Started evaluating 200 [divide] Started evaluating 200 f 200 = Error [ { exn = "Input_too_large "; backtrace = "" } ] Memo graph: 0/0/0 nodes/edges/blocked (restore), 7/6/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.reset Memo.Invalidation.empty; evaluate_and_print f 0; evaluate_and_print f 20; evaluate_and_print f 200; print_metrics (); (* Here we reevaluate all calls to [divide] because they depend on the current run. Due to the early cutoff, we skip recomputing the outer [negate] for the inputs 0 (error) and 20 (success), because the results remain the same. However, we do attempt to re-evaluate [negate] for the input 200 because the result of [divide] does change: we get a fresh exception. *) [%expect {| [divide] Started evaluating 0 f 0 = Error [ { exn = "Division_by_zero"; backtrace = "" } ] [divide] Started evaluating 20 [divide] Evaluated 20: 5 f 20 = Ok -5 [divide] Started evaluating 200 [negate] Started evaluating 200 f 200 = Error [ { exn = "Input_too_large "; backtrace = "" } ] Memo graph: 6/6/0 nodes/edges/blocked (restore), 5/4/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}] ;; (* This test uses non-deterministic tasks to show that adding old dependency edges to the cycle detection graph can lead to spurious cycle errors, where a cycle is formed by a combination of old and new edges. In the first build run, A depends on B. In all later runs, B depends on A. *) let%expect_test "Test that there are no spurious cycles" = let task_b_fdecl = Fdecl.create (fun _ -> Dyn.Opaque) in let task_a = let memory_a = ref 0 in Memo.create "A" ~input:(module Int) (fun _input -> printf "Started evaluating A\n"; let+ result = match !memory_a with | 0 -> let+ b = Memo.exec (Fdecl.get task_b_fdecl) 0 in b + 1 | _ -> Memo.return 0 in incr memory_a; printf "A = %d\n" result; printf "Evaluated A\n"; result) in let task_b = let memory_b = ref 0 in Memo.create "B" ~input:(module Int) ~cutoff:Int.equal (fun _input -> printf "Started evaluating B\n"; let+ result = match !memory_b with | 0 -> Memo.return 0 | _ -> let+ a = Memo.exec task_a 0 in a + 1 in incr memory_b; printf "B = %d\n" result; printf "Evaluated B\n"; result) in Fdecl.set task_b_fdecl task_b; Memo.Metrics.reset (); evaluate_and_print task_a 0; [%expect {| Started evaluating A Started evaluating B B = 0 Evaluated B A = 1 Evaluated A f 0 = Ok 1 |}]; evaluate_and_print task_b 0; [%expect {| f 0 = Ok 0 |}]; print_metrics (); [%expect {| Memo graph: 0/0/0 nodes/edges/blocked (restore), 2/1/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; Memo.reset (Memo.Cell.invalidate ~reason:Test (Memo.cell task_b 0)); evaluate_and_print task_a 0; (* Note that here task B blows up with a cycle error when trying to restore its result from the cache. A doesn't need it and terminates correctly. *) [%expect {| Started evaluating B Started evaluating A A = 0 Evaluated A f 0 = Ok 0 |}]; evaluate_and_print task_b 0; (* Now we get to see the spurious cycle. *) [%expect {| Dependency cycle detected: - ("A", 0) - called by ("B", 0) f 0 = Error [ { exn = "Cycle_error.E [ (\"A\", 0); (\"B\", 0) ]" ; backtrace = "" } ] |}] ;; let%expect_test "Test Memo.clear_cache" = let add_one = Memo.create "Add 1" ~input:(module Int) (fun input -> let result = input + 1 in printf "Evaluated add_one(%d)\n" input; Memo.return result) in let add_two = Memo.create "Add 2" ~input:(module Int) (fun input -> let+ result = Memo.exec add_one input in printf "Evaluated add_two(%d)\n" input; result + 1) in Memo.Metrics.reset (); evaluate_and_print add_one 1; evaluate_and_print add_one 2; [%expect {| Evaluated add_one(1) f 1 = Ok 2 Evaluated add_one(2) f 2 = Ok 3 |}]; evaluate_and_print add_two 1; evaluate_and_print add_two 2; [%expect {| Evaluated add_two(1) f 1 = Ok 3 Evaluated add_two(2) f 2 = Ok 4 |}]; print_metrics (); [%expect {| Memo graph: 0/0/0 nodes/edges/blocked (restore), 4/2/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; let invalidation = Memo.Invalidation.invalidate_cache ~reason:Test add_one in Memo.reset invalidation; evaluate_and_print add_one 1; evaluate_and_print add_one 2; (* We recompute all [add_one] calls. *) [%expect {| Evaluated add_one(1) f 1 = Ok 2 Evaluated add_one(2) f 2 = Ok 3 |}]; evaluate_and_print add_two 1; evaluate_and_print add_two 2; (* We recompute [add_two] calls because they depend on [add_one] calls. *) [%expect {| Evaluated add_two(1) f 1 = Ok 3 Evaluated add_two(2) f 2 = Ok 4 |}]; print_metrics (); [%expect {| Memo graph: 2/2/0 nodes/edges/blocked (restore), 4/2/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}] ;; (* In the first run, the dependency structure is: A -> B -> C -> current run. In the second run, it's: A -> X -> B -> X, i.e. there is a dependency cycle. We force the computation of A and C in parallel, and make C yield, so that B gets blocked waiting for C to eventually complete. As a result, during the restore_from_cache phase in the second run, the cycle detection algorithm will add the path A -> B -> C to the DAG. Then, in the compute phase, B will get blocked on X, adding the path A -> X -> B -> X to the DAG, thus detecting the cycle. If the two phases are not cleanly separated, the second path might get cut down to just B -> X, missing the cycle and leading to a deadlock. *) let%expect_test "restore_from_cache and compute phases are well-separated" = let task_c = Memo.create "C" ~input:(module Int) (fun input -> printf "Started evaluating C\n"; let* () = Memo.of_reproducible_fiber (Fiber.of_thunk Scheduler.yield) in let+ (_ : Memo.Run.t) = Memo.current_run () in printf "Evaluated C\n"; input + 1) in let task_x_fdecl = Fdecl.create (fun _ -> Dyn.Opaque) in let task_b = let memory_b = ref 0 in Memo.create "B" ~input:(module Int) (fun input -> printf "Started evaluating B\n"; let+ result = match !memory_b with | 0 -> Memo.exec task_c input | _ -> Memo.exec (Fdecl.get task_x_fdecl) input in incr memory_b; printf "B = %d\n" result; printf "Evaluated B\n"; result) in let task_a = let memory_a = ref 0 in Memo.create "A" ~input:(module Int) (fun input -> printf "Started evaluating A\n"; let+ result = match !memory_a with | 0 -> Memo.exec task_b input | _ -> Memo.exec (Fdecl.get task_x_fdecl) input in incr memory_a; printf "A = %d\n" result; printf "Evaluated A\n"; result) in let task_x = Memo.create "X" ~input:(module Int) (fun input -> printf "Started evaluating X\n"; let+ result = Memo.exec task_b input in printf "Evaluated X\n"; result) in Fdecl.set task_x_fdecl task_x; Memo.Metrics.reset (); let (_results : int * int) = Scheduler.run (Fiber.fork_and_join (fun () -> Memo.run (Memo.exec task_c 0)) (fun () -> Memo.run (Memo.exec task_a 0))) in [%expect {| Started evaluating C Started evaluating A Started evaluating B Evaluated C B = 1 Evaluated B A = 1 Evaluated A |}]; print_metrics (); [%expect {| Memo graph: 0/0/0 nodes/edges/blocked (restore), 4/3/1 nodes/edges/blocked (compute) Memo cycle detection graph: 3/2/1 nodes/edges/paths |}]; Memo.reset Memo.Invalidation.empty; (match Scheduler.run (Fiber.fork_and_join (fun () -> Memo.run (Memo.exec task_c 0)) (fun () -> Memo.run (Memo.exec task_a 0))) with | (_result : int * int) -> () | exception Test_scheduler.Never -> print_endline "Deadlock!" | exception (Memo.Error.E error as exn) -> (match Memo.Error.get error with | Memo.Cycle_error.E error -> print_cycle_error error | _ -> raise exn)); [%expect {| Started evaluating C Started evaluating A Started evaluating X Started evaluating B Dependency cycle detected: - ("B", 0) - called by ("X", 0) |}] ;; let%expect_test "Simple computation chain with a cutoff" = let f = Fdecl.create (fun _ -> Dyn.Opaque) in let f_impl = Memo.create "integers" ~cutoff:Int.equal ~input:(module Int) (fun x -> printf "Started evaluating f(%d)\n" x; let+ res = match x with | 0 -> Memo.return 0 | n -> let+ prev = Memo.exec (Fdecl.get f) (n - 1) in prev + 1 in printf "Evaluated f(%d) = %d\n" x res; res) in Fdecl.set f f_impl; let f = Fdecl.get f in Memo.Metrics.reset (); evaluate_and_print f 3; print_metrics (); [%expect {| Started evaluating f(3) Started evaluating f(2) Started evaluating f(1) Started evaluating f(0) Evaluated f(0) = 0 Evaluated f(1) = 1 Evaluated f(2) = 2 Evaluated f(3) = 3 f 3 = Ok 3 Memo graph: 0/0/0 nodes/edges/blocked (restore), 4/3/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}]; let cell = Memo.cell f 1 in Memo.reset (Memo.Cell.invalidate ~reason:Test cell); evaluate_and_print f 3; print_metrics (); (* CR-someday amokhov: f(1) is recomputed because we've just invalidated it. Further recomputations are avoided thanks to the early cutoff. *) [%expect {| Started evaluating f(1) Evaluated f(1) = 1 f 3 = Ok 3 Memo graph: 3/2/0 nodes/edges/blocked (restore), 1/1/0 nodes/edges/blocked (compute) Memo cycle detection graph: 0/0/0 nodes/edges/paths |}] ;; let%expect_test "loss of concurrency" = let cell name = Memo.lazy_cell ~cutoff:Unit.equal (fun () -> (* this hackery needed to observe concurrency *) Memo.of_reproducible_fiber @@ Fiber.of_thunk (fun () -> printfn "start %s" name; let open Fiber.O in let+ () = Scheduler.yield () in printfn "finish %s" name)) in let a = cell "a" in let b = cell "b" in let c = Memo.lazy_cell (fun () -> Memo.fork_and_join (fun () -> Memo.Cell.read a) (fun () -> Memo.Cell.read b)) in let read x = run @@ Memo.map ~f:ignore @@ Memo.Cell.read x in (* First we evaluate everything. Note that [a] and [b] are evalauted concurrently *) read c; [%expect {| start a start b finish a finish b |}]; (* Now we invalidate [a] and [b]. As a consequence [c] should be recomputed. *) Memo.reset (Memo.Invalidation.combine (Memo.Cell.invalidate a ~reason:Test) (Memo.Cell.invalidate b ~reason:Test)); (* Now we recompute [c]. Notice that [a] and [b] are no longer computed concurrently *) read c; [%expect {| start a finish a start b finish b |}] ;; let%expect_test "variables - print new" = let var = Memo.Var.create ~name:"foo" 200 in run (Memo.Var.read var) |> printfn "var: %d"; [%expect {| var: 200 |}] ;; let%expect_test "variables - invalidation" = let var = Memo.Var.create ~name:"foo" 200 in let run () = run (Memo.Var.read var) |> printfn "var: %d" in run (); [%expect {| var: 200 |}]; let invalidation = Memo.Var.set var 400 in Memo.Invalidation.details_hum invalidation |> String.concat ~sep:"\n" |> print_endline; Memo.reset invalidation; run (); [%expect {| Variable foo changed var: 400 |}] ;; let%expect_test "variables - cutoff" = let var = Memo.Var.create ~name:"foo" ~cutoff:(fun x y -> x mod 2 = y mod 2) 200 in let node = Memo.Var.read var |> Memo.map ~f:Fun.id in let set_invalidate_print_run value = let invalidation = Memo.Var.set var value in Memo.reset invalidation; node |> run |> printfn "var: %d" in set_invalidate_print_run 202; [%expect {| var: 200 |}]; set_invalidate_print_run 203; [%expect {| var: 203 |}]; set_invalidate_print_run 202; [%expect {| var: 202 |}] ;; let%expect_test "Ensure that implicit output storage cell is not reused between runs" = let var = Memo.Var.create ~name:"var" () in let output = Memo.Implicit_output.add (module struct type t = string list let name = "foo" let union x y = x @ y end) in let test = Memo.Implicit_output.collect output (fun () -> let* () = Memo.Var.read var in Memo.Implicit_output.produce output [ "x" ]) in let set_invalidate_print_run () = let invalidation = Memo.Var.set var () in Memo.reset invalidation; (let+ (), collected = test in (Dyn.option @@ Dyn.list Dyn.string) collected |> Dyn.to_string |> print_endline) |> run in set_invalidate_print_run (); [%expect {| Some [ "x" ] |}]; set_invalidate_print_run (); [%expect {| Some [ "x" ] |}] ;;