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.
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.
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.
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.
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.