Handling `stdin` when converting a REPL to an interactive web page

Hi,

I have a REPL written in OCaml that I would like to embed in a web page through Js_of_ocaml. The REPL needs some initial data to get started. Then it enters a loop where it awaits commands on stdin and outputs results on stdout. I would like to keep the REPL untouched and write a wrapper that will handle interaction with the web page.

The output to stdout can easily be captured by Sys_js.set_channel_flusher stdout append where append adds the new output to the web page.
However, I don’t see how to handle the input: I would like to wait from the user to type some input and the enter key before passing this to the REPL (with itself called read_line). Waiting on the user can be done with onkeydown and a Dom_html.handler. However, I don’t see how make this handler interact with the Sys_js.set_channel_filler for stdin? Maybe using Lwt?

NB: I plan to use some web workers to separate the DOM handling and the REPL, in order to avoid freezes during long computations on the REPL side. I don’t think it changes this specific issue though.

I think you are making your life much more complicated than needed by wanting to use the main loop of your REPL directly. Just make your REPL’s initialization function public, as well as the core processing function from the main loop. That way, you won’t have to worry about control inversion. This might require to modify a bit the REPL, but the changes could be as innocuous as putting a few function declarations in .mli files.

In case what I am saying is a bit too abstract, let’s make it clearer. Assume the main code of your REPL is

let () =
  initialize ();
  while true do
    let s = read_string () in
    process_input s
  done

then your embedded code becomes

let () =
  initialize ();
  set_on_message (fun s -> process_input s)
1 Like

Thank you @silene for your answer! Unfortunately the application is quite complex and extracting the processing function would take a significant time. That’s why I am looking for a way to invert the control, if possible.

I think that the toplevel I have in brzo does what you want, the code is here. Basically you are looking for JsooTop.execute.

1 Like

Ah and now that I think of it you may also want to have a look at brr’s browser console which also has input and toplevel execution in different contexts similar to what you want to do with webworkers (here the browser dev tool extension handles input and your webpage execution). These are the execution bits.

Thank you @dbuenzli for your answer. Just to be sure, is this a way to obtain an Ocaml repl/top-level?

To clarify, I’m trying to expose a REPL I wrote by myself, which targets another language (implemented through an interpreter in OCaml).

Indeed, sorry I read your message too quickly. Forget about what I said it’s OT :–) I’d say @silene puts you on the right track here.

That being said you could try to invert the control using an OCaml 5 effect. Basically you raise an effect from the function you give to Sys_js.set_channel_filler and invoke the continuation whenever some data you get from the handler.

1 Like

Thank you @dbuenzli, I’ll try to play with effects and report back when this is done.

I finally had time to play a bit with effects and the control inversion, I added a sample here: Raphaël Monat / jsoo-inversion · GitLab

The web part sets up Sys_js.set_channel_filler on stdin to perform the inversion. When this inversion is called, a blocking function js_handle_stdin is called. This function loops until an input is provided by the user in the web page.

In its current shape, the project does not work: the effect is not caught (Stdlib.Effect.Unhandled on Dune__exe__Toplevel.Input_inversion; I have enabled effects in jsoo). Would anyone have an idea of workaround?

I have no idea why effects do not work for you. But I can at least tell you that the above is a very bad idea. Indeed, there is no such thing as concurrent execution in javascript. (The closest thing to it is the use of workers, which is some kind of event-driven programming, but it will not solve your issue here.) When some code starts, no other code is executed until it exits. In other words, the following snippet is effectively an infinite loop:

let content = ref None
let rec js_handle_stdin () =
  match !content with
  | None -> js_handle_stdin ()

Consequently, if you were to call the function, your browser would soon tell you that some javascript code has been running for too long on the web page and it would ask you whether you want to kill it. (Either that or you would get a stack overflow.)

Thank you for your answer! Yes, this is indeed blocking. My rationale was to first have a way to handle the control inversion before having a clean, non-blocking version of js_handle_stdin.