Blocking operations in the WASM version (JavaScript expertise needed)

As some have discovered, I picked up the in-browser version of SWI-Prolog. See https://swi-prolog.discourse.group/t/wiki-discussion-swi-prolog-in-the-browser-using-wasm.

The problem is how to deal with blocking operations like Prolog’s read/1 and anything that reads. The JavaScript/browser model is event driven. So, JavaScript calling some predicate on Prolog to process an event is fine. Prolog having to wait for user input or some other event is not. Instead, control has to go back to JavaScript and JavaScript shall make the Prolog engine resume when the waited for data is available.

Ciao solved that by running Prolog in a separate web worker and using PostMessage to make the worker chat with main JavaScript. See https://oa.upm.es/71073/1/TFG_GUILLERMO_GARCIA_PRADALES.pdf

We can do the same (I guess), but we also have two alternatives. One was created after discussion with @matthijs: yield from a foreign predicate, i.e., a foreign predicate can ask the VM to return control to its caller, allowing the VM to be resumed later. The other is delimited continuations that do something similar, but I think they won’t work for this because control remains in the Prolog VM.

The idea would be that a blocking foreign predicate performs a yield, describing to JavaScript what it is waiting for, for example input. The JavaScript keeps dealing with the documents and its events and at some point realizes it may again resume Prolog. For example, we wait for a key-press (get_single_char/1). The predicate yields, on a KeyPress event, JavaScript stores the pressed key somewhere and resumes Prolog. Prolog picks up the key and continues.

The C side looks like this:

   qid_t q = PL_open_query(....);    // returns immediately
   if ( (rc = PL_next_solution(q)) == PL_S_YIELD )
      // pick up what we are waiting for and return to JavaScript
   else
      // Goal completed (handle results, close query and return to JavaScript)

Now my question is what this should look like from JavaScript? As is, we have a JavaScript function calling a Prolog goal from a string. From the above we get the same, but it may answer that this it is suspending, waiting for X.

If “mygoal” is at some point waiting for network data I imagine it could look like below (forgive my poor JavaScript). But what if we wait for the user to fill out some form and submit it? Ideally we’d have a reusable JavaScript function that can deal with all the things Prolog may be waiting for.

    let rc = prolog.call("mygoal"))
    if ( (url = is_fetch_request(rc)) )
        fetch(url)
           .then( (response) => response.json())
           .then( (data) => prolog.resume(data));
    // handle other things we might be waiting for

Note that on the long run we can possibly maintain various threads of control in Prolog waiting for different events using this as first level and delimited continuations as secondary level :slight_smile:

Anyone?

1 Like

Is this for reading from a separate “console” window? Or for getting input from a server? Or something else?

For getting input from a server (the client is JavaScript), I have a function that does a fetch from the server, and behaves as if it’s synchronous. No need for a separate web worker.

1 Like

| jan
August 4 |

  • | - |

As some have discovered, I picked up the in-browser version of SWI-Prolog. See https://swi-prolog.discourse.group/t/wiki-discussion-swi-prolog-in-the-browser-using-wasm.

The problem is how to deal with blocking operations like Prolog’s read/1 and anything that reads. The JavaScript/browser model is event driven. So, JavaScript calling some predicate on Prolog to process an event is fine. Prolog having to wait for user input or some other event is not. Instead, control has to go back to JavaScript and JavaScript shall make the Prolog engine resume when the waited for data is available.

Ciao solved that by running Prolog in a separate web worker and using PostMessage to make the worker chat with main JavaScript. See https://oa.upm.es/71073/1/TFG_GUILLERMO_GARCIA_PRADALES.pdf

Please note that this PDF is a student project which does not necessarily describe the latest or the best solution. Indeed some paragraphs are a bit inaccurate. Our aim was to describe this properly in a research paper (but it takes time). There is no need to do “clean-room reverse engineering” with Ciao. Just ask us (we recently opened Github discussions for Ciao but we’d really prefer a common discourse/whatever forum for all prologs…).

We can do the same (I guess), but we also have two alternatives. One was created after discussion with @matthijs: yield from a foreign predicate, i.e., a foreign predicate can ask the VM to return control to its caller, allowing the VM to be resumed later. The other is delimited continuations that do something similar, but I think they won’t work for this because control remains in the Prolog VM.

A “yield”-like primitive is exactly the solution that we showed in the GDE workshop video to do JS interactions. The current toplevel is still not using it (but the next one will do for some interactions).

Thanks. So you use the same approach I’m working at (at least roughly). I tried to attend the GDE workshop remotely. Unfortunately most talks were impossible to follow due to bad or even absent sound :frowning:

Seems the yield is implemented specifically for the toplevel, no? E.g. in the playground we get this without any user interaction:

?- read(X).
|: 
X = end_of_file ? 

yes

My plan is to do something similar as in SWISH: redefine most I/O predicates and make them act differently when reading/writing to the user_* streams.

I still have a rather unclear view of all the new JavaScript stuff. Last time I did a lot of JavaScript was using ES5 when I worked on SWISH. I have little clue what asyncyfying means in the context of WASM.

My rough plan to deal with interaction while Prolog is crunching away is to yield every N inferences. The performance impact should be small if we set N large enough to yield not more often than every couple of milliseconds.

Just some notes regarding Guillermo’s project: it is an undergrad student report, not a research paper nor the system documentation. They need to finish in a very short time and they do their best effort. The purpose is to make students work in a realistic environment and it is produced by their own, sometimes with no prior knowledge. Students are advised and then graded, but we have no way to prevent inaccuracies or even wrong paragraphs being written. Most of Guillermo’s work was in the JS interface code and not in the architecture. CiaoWASM was started around 6 years ago (when it was a different thing called ASM.js, which was first released 9 years ago). Only recently when WASM was more mature and gained more traction we decided to push this project and release it.

Producing a good description of the Ciao+WASM architecture, our roadmap, and something that can be useful for the Prolog community is something that we’d definitely want to do. Having said that, good documentation takes time to write and we preferred to make the system available as soon as possible. Also, it is a honor to see SWI incorporating the best ideas from other systems, but something to consider is how this community could contribute back to the other systems. Specially in the academic environment it is hard to publish results once they become “mainstream”, which forces people to work in stealth mode and not share implementations. This is a lose-lose situation.

Regarding workers: we encapsulated engines in a WebWorker so that multiple engines can run in parallel and aborted with some confidence of not interfering with the main thread. But this forces us using message passing, which is slower for some applications. Sometimes it is easier to start with general case. Some cool (and useless) toy: https://twitter.com/CiaoProlog/status/1555478052288843778 . We’ll allow engines in the main thread at a later stage. They are definitely needed.

Nobody is blaming Guillermo (at least I’m not). As I see it, the history is roughly this:

  • CiaoWASM was apparently started 6 years ago.
  • @rla started a WASM port of SWI-Prolog in 2018. I assume he had no knowledge about CiaoWASM. The project stalled for various reasons, immaturity of Emscripten surely being one of them. Still, it ran and @rla produced a little demo that is the basis of the current shell at https://dev.swi-prolorg.org/wasm.
  • @dmchurch got interested in this a couple of years ago, considering to use it as a replacement for the server side for SWISH. That work improved the build process, but again stalled for similar reasons.
  • @josderoo started using the WASM version for EYE. As he started complaining each time the build broke the build process has become a bit more mature. Still, it allows running SWI-Prolog on node.js which is not very useful. There is no good interface to call Prolog from JavaScript, none at all to call the other way and no browser integration.

That was up to some days ago. Inspired by the Ciao Playground I took a second look at the status to check how hard it would be to turn it into something good enough to show its potential. I integrated @rla’s initial Prolog shell code with the current (modularized) WASM build and extended it minimally to the current demo.

If you want to get anywhere you need to handle asynchronous behavior, so I used Google Scolar to find the thesis by Guillermo which claims using web workers. That looks like an option, but so is the ability to yield from the VM. Yield got to SWI-Prolog through @ptarau’s engine design while I worked with @ptarau at Kyndi. @matthijs asked for a foreign language API for this to be used with Rust, so all the building blocks are there. I’m investigating the low-level plumbing required have a proper JavaScript interface to be used (primarily) in the browser.

I’m happy to have a discussion on exchanging code and ideas between our projects. I think a telco is a better medium for that than here. Just drop me a personal message. I don’t think that should be too hard to resolve as our aims are quite complementary (I think).

3 Likes

The code is still ugly, but SWI-Prolog WebAssembly build test now uses yield to read goals and ask whether or not to try and produce more answers. If anyone is interested:

Please shoot at the JavaScript. Notably I think the async stuff can be written way more elegantly.

The code is in the main swipl-devel.git, in the branch yield_wasm.

3 Likes

This is amazing! I have been following development of SWI WebAssembly version and just saw this post. I think the JavaScript part is mostly good.

Yield in toplevel is an excellent idea to get a working interactive console! I also tried out Ciao Playground from above but I don’t see how to enter queries manually. The Guillermo thesis seems to be more about WebWorkers to have multiple engines running.

Regarding SWI Prolog/JS interop there are still lots of open decisions to make. Using FLI is way too low level indeed. Maybe the term representation could be ported from JPL interface to JS using similar classes.

Some time ago I started working on TypeScript support for FLI (prolog.js converted to TypeScript) and packaging up the SWI WebAssembly version to NPM. Typing support would make FLI a bit more tolerable for JS developers and make it easier to build top-level interface on top of it. NPM would make distribution and integration with Node and frontend (Webpack etc.) much easier.

1 Like

Sounds good. For now my emphasis is on the low level. I definitely need support to get the high level right.

I have had discussions with @ericzinda on a new general JSON representation for Prolog terms. The idea is to create something where the terms you normally like to transfer (lists, strings, numbers, objects) come pretty natural, but the representation has escapes that allow for faithful round trip of arbitrary Prolog terms.

One thing at a time :slight_smile: Auto yield is in the planning and probably fairly easy. Roughly means doing the same as calling js_yield/2 automatically each Nth increment of the inference counter. A more serious problem is that yielding from any position in the VM is probably not that easy and that is what would be required to yield for debugger interactions. Another complication is that in the current design there is a significant number of places where Prolog is called through C, e.g., C → Prolog → C → Prolog → … Yielding from there would require a general mechanism to yield in C. Does that exist? It might be possible using stack copying? You need to think outside the C standard, so even if it can work for C, can it work for WASM?

As I see it now, auto-yielding will surely be added. Hopefully yield for the debugger is possible. If not we have two alternatives: a debugger as meta-interpreter or a debugger based on recording the actions and replaying them. Possibly some of the C → Prolog → C → Prolog calls can be avoided or we can extend the C in the middle to save/restore its state such that we can yield through the callback.

FYI

This (SWI-Prolog Discourse) site supports highlight.js (ref)

TypeScript and WebAssembly have been enabled recently.

While WebAssembly is known to Hightlight.js it seems that Discourse has not been updated. Will leave the option set and hopefully in the future it will work. :slightly_smiling_face:

@j4n_bur53, the audio sequencer example is nice! (I’ll try to show some others that we did for Ciao Playground). It shows that integration with JS API opens many opportunities with relatively low effort. I grown in the times when you need poking memory at the 0xA000 segment to write graphics, and reverse engineering video card drivers due to lack of libraries. Any kind of interaction with the computer was painful and portability horrible. WASM and browsers makes me feel a bit better, it may not be as efficient as it could be but it works literally everywhere and machines are really fast.

Just in case it is useful, we published this paper back in 2012 about translation of Prolog to JS. We had some libraries to interact with the DOM and JS and some kind of JS foreign interface:

[1210.2864] Lightweight compilation of (C)LP to JavaScript

For WASM we need to revisit it but we’ll probably reuse part of the libraries. The compiler was a proof of concept and never released.

It may be good to have some common place to discuss these kind of proposals besides SWI forum and messages here and there. Theresa Swift (which probably is not reading this) mentioned that something like PEP (Python Enhancement Petition) would be good. SWI created a good user community but IMHO Prolog would be better if more people could share their expertise. In the past days we had the CICLOPS workshop and conferences – despite being slow – were a nice place to learn implementation magic.

Another interesting thing is that since WASM loading is 100% transparent to the user, it is perfectly possible to use different Prologs or different versions of the same Prolog. I remember that ‘chrome experiments’ was really cool at the beginning. It may be cool to setup a showcase of cool experiments or demos using (any of our) Prolog(s). Perhaps some github repo with a gallery and links to demos would be easy to manage.

This would produce some codebase useful for standardization, or at least, sharing the best ideas of each system.