Wiki Discussion: SWI-Prolog in the browser using WASM

I think ideally it should be both and on Node.js you should be able to select either of them. The actual WebAssembly binary should be exactly the same. The difference is in the wrapping JavaScript provided by Emscripten. I’m working slowly on the package. My main goal so far was actually TypeScript support and automated build through docker to make current code easier to use by others. Improving API, term representation etc. was secondary.

Besides Javascript/Typescript, there’s also Dart (used to implement Flutter, I think), which runs in the browser (Chrome, Edge, Firefox, Safari). Some years ago, I attended a talk that claimed Dart had more parallelism than Javascript, but I don’t remember the details; and Javascript might have adopted some or all of Dart’s parallelism features in the interim.

Anyway, I found a few articles, in case they’re useful:

That was the starting point. I have been discussing a new JSON format with @ericzinda at Consider adding an option to use a different JSON Format · Issue #4 · SWI-Prolog/packages-mqi · GitHub

It is not very hard of course. The main issue is what you hint at: a lot of stuff will move and that has implications for the build process, users and documentation. We can minimize some of that by adding a library at the old location that loads and reexports the new library (and prints a deprecated warning). That is also what happened to the DCG support that started in the HTTP package as well. I’m still in doubt. The other option would be to simply minimize the HTTP package for the WASM version. That is far easier but doesn’t solve the long term issue.

One of the next steps is to be able to add the foreign libraries in the WASM version.

I have now added support for @ericzinda’s proposal for JSON/Object representation to the prolog.js module as Prolog.toJSON() and Prolog.toProlog() that is connected to the WASM version. I’m also working towards a better high level interface to call Prolog from JavaScript. You can see it at work at SWI-Prolog test playground. The source is in the src/wasm directory as test.html and test.pl. Roughly, this adds:

  • A Prolog.consult(url1, url2, ...).then(function) that downloads the urls from the server, puts them in /tmp and loads them into Prolog.
  • Prolog.query(...) which has several signatures and returns an iterable object that represents the query. So, we can do call a goal from a string and get the bindings as an object that holds the JavaScript translation of the results, using the variables as keys. Variables starting with an _ are not included in this object.
  Prolog.with_frame(() =>
  { let n = 0;

    for(const r of Prolog.query("p(X)"))
    { println("stdout", r.X);
    }
  });

Or, provide input and do a once goal. The code below computes the sum of all numbers in a list in Prolog. It takes an object with input bindings that is translated to Prolog data. The output object does not include the input variables.

function sum_list(list)
{ return Prolog.with_frame(() =>
  { return Prolog.query("sum_list(List, Sum)", {List:list}).once().Sum;
  });
}

There are still several issues with this stuff.

  • I’d rather get rid of the Prolog.with_frame() that scopes the Prolog terms references.
  • Breaking out of the query due to an exception (not yet done) or a break from the for…of construct does not close it. AFAIK JavaScript has no destructor like C++ when an object gets out of scope.
  • Errors are practically nowhere handled. I’m still doubting between return codes and JavaScript exceptions. The lack of destructors might make cleanup complicated :frowning:
  • It all looks pretty ugly and is not documented.

Still, feedback on the overall direction is welcome :slight_smile:

3 Likes

In my opinion it looks pretty good. It is much more usable than it was a week ago. We could just require query to be closed and throw an error if a new query is opened without closing the previous one (another option is to queue them).

Your JavaScript is very good and pretty much self-documenting. Only the indentation is a bit odd but it matches the C code style in SWI :slight_smile:

I would like to provide Typescript annotations. For JavaScript programmers they would be very helpful since modern IDE-s will display and autocomplete code by them. Things are moving very fast but I hope to find time to put into this.

2 Likes

I have pushed an update to #wasm and updated the wiki page. There are two big developments:

  • Call between Prolog and JavaScript and JavaScript and Prolog are getting close to their final version.
  • Calls from Prolog to JavaScript can represent DOM elements as Prolog blobs. The DOM elements known to Prolog are subject to Prolog garbage collection :slight_smile:

For example, we can how define a Prolog predicate to add a paragraph to the shell output window:

add_par(Text) :-
  Out  := document.getElementById("output"),
  More := document.getElementById("more"),
  Par  := document.createElement("p"),
  Par.textContent := Text,
  _ := Out.insertBefore(Par, More).

After which we can call

?- add_par("Hello world!").

We can also call the other way around nicely, for example writing all the results of p/1:

for(const r of Prolog.query("p(X)")) {
  console.log(r.X);
}
3 Likes

It is indeed inspired by @nicos work on R/real. I don’t know whether this approach has a name. This can’t deal with functions and other complex syntax. We notably need a way to add event listeners.

There is no need to register anything. Just define the function and call _ := add_par("Hello world!") Of course you can define a wrapper predicate or we can add something that creates these wrappers.

(Nothing of Importance here)

Aside from the above, in EYE we have to do

:- catch(use_module(library(sha)), _, true).
:- catch(use_module(library(pcre)), _, true).
:- catch(use_module(library(http/http_open)), _, true).
:- catch(use_module(library(semweb/rdf_turtle)), _, true).

because those libraries are not yet available in WASM.
This is not a burning issue, I just wondered if there is a plan to support those libraries in WASM?

Yes. What is wrong with that? You can do whatever JavaScript can do except for stuff that requires more complex syntax such as functions. In other words you can access methods, setters and getters and chain these using a.b.c… In fact, internally they are chained as a[b][c]… because . is a function in SWI-Prolog. goal_expansion on a.b.c… translates this to a[b][c]… to avoid the function evaluation. Next step :=/2 translates this into a Prolog term that expresses the actions that need to be done as a list of primitive actions. This is passed through ‘$js_call’/2 which is defined using EM_ASM_INT(), passing the raw Prolog terms to JavaScript. JavaScript calls Prolog.toJSON() (should that be Prolog.toObject()? as it is not really JSON). JavaScript executes the (now) array of actions resulting in a return value. This is handed to Prolog.toProlog() and made available as the result. If the arguments are sufficiently instantiated we could do most of the translation at compile time and simply pass the shared JavaScript object.

Most of them depend on foreign components. I have not yet looked into how easy or hard it is to support these. It seems emscripten has a notion of shared libraries, so it should be possible. Opening a URL as a stream as done by library(http/http_open) may be a real problem. A quick search suggests that emscripten has sockets, but they are a wrapper around websockets, not raw TCP/IP sockets. The obvious solution is to download in JavaScript, similar to Prolog.consult().then(), which downloads a file asynchronously using fetch(), compiles the file and runs the then(). This would not allow streaming parsing of e.g. Turtle input.

1 Like

SWISH is just a server side sandbox. And yes, doing eval() on strings is dangerous if you do not control where the string comes from. Otherwise I don’t see the difference with <script> elements.

I pushed a starting point to deal with this. After reading all the warning on emscripten against using dynamic linking I decided for another route I had in mind to resolve this issue: allow adding the foreign extensions to the core system. This is now a new option -DSTATIC_EXTENSIONS to cmake which is enabled by default for Emscripten. This allows building fully static SWI-Prolog executables with extensions, which surely has value on its own :slight_smile:

Currently includes some of the clib libraries, the sgml library and some of the http libraries.

An overall worry is the size of the whole thing :frowning:

Pushed to #wasm_demo

3 Likes

now works fine, thanks @jan

After seeing

commit 7d8f1e2eba12b0d245ac92ca7a5547f1c6f0589a
Author: Jan Wielemaker <J.Wielemaker@vu.nl>
Date:   Sat Aug 20 10:09:42 2022 +0200

    Included turtle and ntriple libraries in WASM build.

we tested the latest version but get

ERROR:    source_sink `library(semweb/rdf_turtle)' does not exist

I didn’t include this deprecated library. It just loads `library(semweb/turtle)'. Note that the writing part of this library does not yet work as it depends on semweb/rdf_db and the C part of the RDF store heavily depends on threading.

1 Like

Aha, that is nice and I will use library(semweb/turtle) from now on. Thanks!

Can I also use, like in JavaScript?

Par["textContent"] := Text

It seems to me from the term_expansion/2 I find in wasm.pl,
and the operator definition op(50, yf, []).

But I am not 100% sure, whether term_expansion/2 has another
purpose, like avoiding the dot expansion for dicts?

Par[textContent] := Text is the output of the expansion. That should work.

@jan

The instructions note

wget https://gmplib.org/download/gmp/gmp-6.1.2.tar.lz
tar xf gmp-6.1.2.tar.lz
cd gmp-6.1.2

In checking the GMP latest version it is 6.2.1, was that a typo?

I copied that from somewhere … Guess I should have checked (and tested) the latest version.

In the meanwhile #wasm_demo is updated to include GMP, (6.1.2) providing unbounded integers and rational numbers. Note that the GMP library is untested. It might be wise not to trust the results blindly.