(* An illustration of how the heap to stack optimisation is broken. * This example is adapted from de Vilhena and Pottier (2021). * file: heap2stack.ml * compile: ocamlopt -I $(opam var lib)/multicont multicont.cmxa heap2stack.ml * run: ./a.out *)
(* We first require a little bit of setup. The following declares an operation `Twice' which we use to implement multiple returns. *) type _ Effect.t += Twice : unitEffect.t
(* The handler `htwice' interprets `Twice' by simply invoking its continuation twice. *) let htwice : (unit, unit) Effect.Deep.handler = { retc = (fun x -> x) ; exnc = (fun e -> raise e) ; effc = (fun (type a) (eff : a Effect.t) -> letopenEffect.Deepin match eff with | Twice -> Some (fun (k : (a, _) continuation) -> continue (Multicont.Deep.clone_continuation k) (); continue k ()) | _ -> None) }
(* Now for the interesting stuff. In the code below, the compiler will perform an escape analysis on the reference `i' and deduce that it does not escape the local scope, because it is unaware of the semantics of `perform Twice', hence the optimiser will transform `i' into an immediate on the stack to save a heap allocation. As a consequence, the assertion `(!i = 1)' will succeed twice, whereas it should fail after the second return of `perform Twice'. *) let heap2stack () = Effect.Deep.match_with (fun() -> let i = ref0in Effect.perform Twice; i := !i + 1; Printf.printf "i = %d\n%!" !i; assert (!i = 1)) () htwice
(* The following does not trigger an assertion failure. *) let _ = heap2stack ()
(* To fix this issue, we can wrap reference allocations in an instance of `Sys.opaque_identity'. However, this is not really a viable fix in general, as we may not have access to the client code that allocates the reference! *) let heap2stack' () = Effect.Deep.match_with (fun() -> let i = Sys.opaque_identity (ref0) in Effect.perform Twice; i := !i + 1; Printf.printf "i = %d\n%!" !i; assert (!i = 1)) () htwice
(* The following triggers an assertion failure. *) let _ = heap2stack' ()