Can I "reset" my REPL?

I would like to “reset” my REPL, in a similar fashion to ghci’s :reload command.

$ ghci
GHCi, version 8.10.7: https://www.haskell.org/ghc/  :? for help
Loaded package environment from /home/benjamin/.ghc/x86_64-linux-8.10.7/environments/default
Prelude> :reload
Ok, no modules loaded

Does anybody know a trick or two to make that work? That’d make a REPL driven development workflow doable.

I’d be happy to loose any state and reload an .ocamlinit file within my current folder, as if anew. But I’d want to stay within the current REPL process (killing the underlying ocaml process is too harsh and not practical)

2 Likes

Uh, how about Unix.exec to exec over a fresh ocaml ? Or is that too close to kill/restart ?

Another alternative: start your ocaml process from a bash script that runs ocaml over and over in a loop ? Then you can exit the current ocaml process, and a new one will be started right in its place, instantly.

I am curioius why kill/restart is “too harsh and not practical”. On Unix, that should definitely not be the case.

Thanks @Chet_Murthy, you put me on the right track!

I got initially confused after a bit of fiddling and, mainly, I could not see a way to kill the ocaml REPL without relying on its name. That would be too coarse as I would also kill other processes, such as the one launched by: vim->merlin->ocaml.

Here’s what I got so far:

Step 1) Prepare a local .ocamlinit file for customization on REPL start.

Down looks appropriate for a minimal setup with readline functionality

$ cat ./.ocamlinit 
#use "down.top";;
#load "./hello.cmo";;

Step 2) Launch watchers with: foreman start -f Procfile.dev

Foreman is just a handy tool, launching those watchers via other terminal panes would also work.

$ cat ./Procfile.dev 
compile: find *.ml | entr -p ocamlc /_
kill_repl: find *.cmo | entr -p bash -c 'kill $(cat ./.ocaml.pid)'

Step 3a) Use a wrapper script to track the REPL PID

$ cat ./ocaml.exec 
#!/bin/bash

echo $$ >./.ocaml.pid
exec ocaml

Step 3b) Launch the wrapper in a loop, within the current shell

Here, I sleep in order to be able to ctrl-d, ctrl-c out of this loop.

while true;do ./ocaml.exec;sleep 0.1;done

It’s ok…ish

I still have a couple of problems:

  1. I get garbled output if I try to launch step 3b via a script (via another wrapper), not sure why.
  2. down won’t save history on receiving a kill signal

Regarding that last point, I’m not sure where at_exit is defined

I tried to play with sending different signals, but history did not save so I presume down relies on receiving ctrl+d to trigger at_exit. Is this correct if you don’t mind me asking @dbuenzli?

Once I got that sorted, the next step is to simulate pressing up then enter, after REPL reload (triggered itself by a file change). Tmux is handy for that, I could use something like this:

tmux send-keys -t "PROJ_NAME:PANE_NUM_WITH_REPL" Up
tmux send-keys -t "PROJ_NAME:PANE_NUM_WITH_REPL" Enter

You can use a combination of fork and exec to implement a save reset-like functionality:

#require "lwt.unix";;

let save () =
  match (Lwt_unix.fork ()) with
  | 0 -> Lwt.return ()
  | pid ->
    Lwt.map (fun _ -> ()) (Lwt_unix.waitpid [] pid)

let reset () = exit (-1)

Now:

# let x = 10;;
val x : int = 10
# save ();;
- : unit = ()
# let x = true;;
val x : bool = true
# x;;
- : bool = true
# reset ();;
- : unit = ()
# x;;
- : int = 10
5 Likes

Very nice piece of code @Gopiandcode! A bit of a mind twister for me if I’m honest :slight_smile:

The only trouble with your solution is that the REPL won’t pickup newly compiled code. I wasn’t clear enough about that at the start though.

If I understand things correctly, here’s what’s going on:

  • open REPL with save/reset functionality, load cmo
  • save () to jump into a child process
  • change code, recompile the cmo
  • reset () to refresh but I’m back into my original process so no new code is loaded :sob:

I understood I first need to write this file, then start the REPL so please correct me if I’m wrong (note: this config only works with utop at the moment, I get loading issues with the ocaml binary)

$ cat .ocamlinit
#require "lwt.unix";;
#load "hello.cmo";;

let save () =
  match (Lwt_unix.fork ()) with
  | 0 -> Lwt.return ()
  | pid ->
    Lwt.map (fun _ -> ()) (Lwt_unix.waitpid [] pid)

let reset () = 
  exit (-1)

Ah, I see. If you’re going for that kind of workflow then things get a little bit more challenging.

A little less simple, but you can use pipes to send loaded files back to the parent process:

#require "lwt.unix";;

(* keep track of loaded files *)
let files_to_load = ref []
(* pipe to send new loaded files to parent *)
let pipe_to_parent = ref None


let save () =
  let open Lwt.Syntax in
  let (pout, pin) = Lwt_unix.pipe () in
  match (Lwt_unix.fork ()) with
  | 0 ->
    (* pipe management *)
    pipe_to_parent := Some (Lwt_io.of_fd ~mode:Lwt_io.output pin);
    let* _ = Lwt_unix.close pout in
    (* load files *)
    List.iter (fun file ->
      ignore (Topdirs.load_file Format.std_formatter file)
    ) !files_to_load;
    (* good to go *)
    Lwt.return ()
  | pid ->
    (* pipe management *)
    let* _ = Lwt_unix.close pin in
    (* process any new files loaded *)
    let* _ =
      Lwt_io.read_lines
        (Lwt_io.of_fd ~mode:Lwt_io.input pout)
      |> Lwt_stream.iter (fun s -> files_to_load := s :: !files_to_load) in
    (* wait for child *)
    let* _ = (Lwt_unix.waitpid [] pid) in
    let* _ = Lwt_unix.close pout in
    (* good to go *)
    Lwt.return ()

let reset () =
  exit (-1)

let load_file name =
  Option.iter (fun pipe_to_parent ->
    Lwt.async (fun () ->
      Lwt_io.write_line pipe_to_parent name
    )
  ) !pipe_to_parent;  
  ignore (Topdirs.load_file Format.std_formatter name)

let _ = Toploop.add_directive "load" (Toploop.Directive_string (load_file))
    {
      section = "Loading code";
      doc = "Load in memory a bytecode object, produced by ocamlc.";
    }

With this, you can get the following interface:

(* hacking on example.ml *) 
utop # Out_channel.with_open_bin "./example.ml" (fun oc -> output_string oc "let f (x) = x + 30");;
- : unit = ()

utop # Sys.command "ocamlc ./example.ml";;
- : int = 0

utop # save ();;
- : unit = ()
(* load example.cmo into your process *)
utop # #load "./example.cmo";;

utop # Example.f 1;;
- : int = 31
(* more hacking on example.cmo *)
utop # Out_channel.with_open_bin "./example.ml" (fun oc -> output_string oc "let f (x) = x + 3");;
- : unit = ()

utop # Sys.command "ocamlc ./example.ml";;
- : int = 0

(* reset and reload *)
utop # reset ();;
- : unit = ()
utop # save ();;
- : unit = ()

(* example.f has updated *)
utop # Example.f 1;;
- : int = 4

It’s a little annoying to have to call reset and save to make things work - the main problem is that once you’ve loaded a cmo into your process, attempting to reload the same cmo might cause issues if their interfaces disagree.

You could polish up the interface some more by making save recursive dependent on whether reset was called in the child, and avoid some of this, but with this level of complexity it might just be simpler to use the more typical OCaml workflow of just re-loading your utop after recompiling your project (if you use dune, then this is just a matter of dune utop to load your project + all it’s libraries preloaded)

4 Likes

Thanks very much @Gopiandcode!!

You went far beyond my expectations, you’ve got some seriously cool ideas there, I’ll be sure to play with them! :smiley: