Wiki Discussion: SWI-Prolog in the browser using WASM

@jan, @swi sorry for a slow reaction to SWI-Prolog releases. The NPM package https://www.npmjs.com/package/swipl-wasm is still very unstable but it bundles all necessary files including swipl.js, swipl.data and swipl.wasm. They are in dist/swipl/*. The package README contains how to use these files.

I just built and published new version based on 8.5.17. Right now building has to be done manually but I hope we find a solution for that.

Regarding 8.5.17, it does not run on node anymore. The issue is in src/wasm/prolog.js which contains:

HTMLCollection.prototype.toList = function()
{ const ar = [];

  for(let i=0; i<this.length; i++)
    ar.push(this.item(i));

  return ar;
}

This triggers TypeError in nodejs since HTMLCollection does not exist there.

1 Like

Great, this should make it user-friendly to generate qlf files to be deployed on the web without worrying about conditional compilation.

This seems to be a utility/helper method, could it not just be left out of the nodejs version by not compiling it in if HTMLcollection does not exist?

You are doing well :slight_smile: Thanks! Pushed a fix for the HTMLCollection. I still have trouble understanding the browser vs node issue. As I understand it, we have

  • swipl-web.js + swipl-web.wasm + swipl-web.data. These work in the browser. Notably swipl-web.data provides the content for the Emscripten file system that appears as /swipl in the browser.
  • swipl.js and swipl.wasm. These work with node. I think swipl-web.wasm and swipl.wasm are pretty much the same, but I’m not sure. However, there is no swipl.data and instead the files are on the host physical file system.

If this picture is correct, the node version should be shipped with a directory structure that provides the Prolog home. The build system creates swipl.js and swipl.wasm as these components are needed to run the Prolog steps for the build using node. In this setup the Prolog resources are in the home directory of the build tree.

Given the (large) Prolog resources that need to be distributed completely differently, it seems the browser and node versions are pretty much disjoint and the best we can do is to distribute them as different npm packages. Right?

P.s. node src/swipl.js works fine despite the HTMLCollection issue. Why is that?

the new qcompile/2 is quite nice, but it doesn’t seem to handle use_module/2 or reexport/2 properly.

m1.pl:

:- use_module(m2).

m1 :-
   m2.

m2.pl:

:- module(m2, [
   m2/0
]).

:- reexport(m3,[m3/0]).

m2 :-
   format("Hello from m2"),
   m3.

m3.pl:

:- module(m3, [
   m3/0
]).


m3 :-
   format(", and from m3 via re_export").

Test 1 without qcompile/2:

1 ?- m1.
Hello from m2, and from m3 via re_export
true.

Test 2 with qcompile:

1 ?- qcompile('m1.pl',[include(user)]).
true.
$ swipl -l m1.qlf -g m1
ERROR: -g m1: catch/3: Unknown procedure: m2:m2/0

Handling reexport/1,2 was the problem. The code for dealing with .qlf files created from multiple Prolog files is a bit old … Fixed with 7e685450f6117d2d20c2e45d123f635faaa13484.

1 Like

I will take a look. It used to be that .wasm and .data files were common for both nodejs and browser variants but seems like they are not anymore. It used to be:

  • Browser: swipl-web.js + swipl-web.wasm + swipl-web.data
  • Nodejs: swipl.js + swipl-web.wasm + swipl-web.data
2 Likes

I cloned the GitHub - rla/npm-swipl-wasm: SWI-Prolog WebAssembly build as a NPM package, built the project and I can confirm that it runs on both the browser and node. For those who want to test, after building

  • for the browser, start the node http-server in the root of the cloned repo using http-server ., open your browser at the indicated location and navigate to examples/browser.html. That should print a to the browser window. Why not “hello world”? :slight_smile:

  • For node, simply run node examples/run-on-node.js, which again prints a.

I was confused about the development environment, where after the build you can run node src/swipl.js to get a normal interactive SWI-Prolog instance that “sees” the physical file system. Looks like this does work in my local built, so something may be wrong with the way it is build in docker. I’ll sync both.

1 Like

I don’t get it. In my local build as well as when looking into @rla’s build container I can run node swipl.js and when all is in the right place, it runs the normal Prolog toplevel. Without everything in the right place I end up with an error message, either not being able to load swipl.wasm or from Prolog not being able to find its startup resources.

When I go to the dist directory though, node swipl.js exits immediately without any message. Using strace to trace its system call suggests it doesn’t even try to find the .wasm file :frowning: Maybe someone more familiar with the node and JavaScript eco system can solve this?

I debugged these issues a bit. I don’t seem to be able to run toplevel and I think that’s expected behavior since PL_initialise does not run toplevel. https://www.swi-prolog.org/pldoc/doc_for?object=c(‘PL_initialise’) PL_initialise will print the banner and then exit as nothing keeps the nodejs process around.

The MODULARIZE option (which we also use) for Emscripten turns generated SWI-Prolog module code into a factory function that needs to be imported and then executed. This will locate (at least it will try) .wasm and .data files. That’s why the example in the package also uses:

const SWIPL = require("./swipl/swipl"); // imports the module
// ...
const swipl = await SWIPL({ options }); // runs the factory function and places the instance into swipl variable

FYI

Qt 6.4 Released

With Qt 6.4 we are taking the support for WebAssembly out of technology preview. With Qt for WebAssembly, Qt developers can use their existing skills, and often existing code, to target the web. Applications targeting the web assembly platform can run in most modern web browsers, and are easily distributed like any other web content. Thanks to near-native performance, and the rich UI and 3D features of Qt Quick and Qt Quick 3D, solutions requiring heavy data processing and demanding visualization can now easily be built for the web.

I have made two enhancements to #wasm_demo

  • Replace the <textarea> with CodeMirror and use the Prolog mode that is part of SWISH. Doesn’t use SWISH’s enhanced Prolog mode using Prolog support (which should be a lot simpler using the WASM version than server interaction).

  • Allow yield while loading local files. This does keep the browser responsive while loading large files and it allows loading from URLs from a local file. So, we can copy paste this: and use it:

:- use_module('https://raw.githubusercontent.com/SWI-Prolog/sCASP/master/prolog/scasp.pl').

p :- not q.
q :- not p.

And now

?- ? p.
% s(CASP) model
{ p,      not q
} ; 
false.
6 Likes

More updated to #wasm_demo

  • Provide server-side example programs. These are added to the file dropdown and when selected copied to your local store, so you can load and edit them. Currently there are only two, one loading s(CASP) from Github and one manipulating the shell itself through DOM manipulations.
    If you have nice examples, please share them.
  • Changed the console to use a more structured DOM. The console now has a sequence of div elements that each reflect a query and its answers. This allows collapsing and removing queries and in theory a lot more. As experiment it now collapses all old queries. That gives much more overview of the history and allows selectively opening them. Please share whether you like it.
  • support the edit/1 interface.

A mayor problem when you do fancy stuff is that it is easy to get Prolog involved in multiple asynchronous queries. That is fine, as long as they are strictly nested, i.e., we can start a new asynchronous query while yielded from an older one. We must however complete the new one before we can continue the old one. That cannot be solved without engines. All we can hope for on the short term is to detect this (Prolog now crashes badly).

1 Like

Hello, I wrote a little online prolog editor along the lines of SWISH but using this WASM build. I mainly needed it to draw the trace as a graphical tree, but it might be useful to someone.

P.S. I implemented “asynchronous” queries by running them in separate workers.
Live Version

3 Likes

You are correct, I did mean to write parallel queries. Some times the two terms are used interchangeably, but it is better to be precise. More in detail, Javascript is run as a single thread in a page (for the most part), so blocking queries such as sleep would freeze the interface entirely, which was not great. As far as I know workers are the most straightforward way to perform parallel tasks in JS, so I created a pool of workers which then execute a single query each before reinstantiating. They are entirely separated from each other and the main thread (besides messages of course).
The WASM implementation offers asynchronous queries (using forEach), however very long to execute steps (Such as sleep, or Example 4 in the editor) will slow down or freeze the page if they are in the main thread. Again, using workers they do no longer affect the editor itself.

Testing the library on Node, calling JS from Prolog does not seem to work (:=/2 or js_script/2). The error is “window object undefined”, so it’s still expecting to be on a browser.
I tried to create a dummy global window object and assign names to it, but it didn’t work either.

Then I found that in the repository example js_run_script/1 is used rather than js_script/2 and this seems to work, provided one assigns functions and variables to the global scope first.
Functions/variables must be assigned globally when using CommonJS modules or to a global object when using ES modules (I found this through experimenting).

Example with CommonJS modules:

const SWIPL = require("swipl-wasm");

const data = [];

// Just add some data
add = function () {
  data.push(1);
};

(async function () {
  const swipl = await SWIPL({ arguments: ["-q"] });
  swipl.prolog.call(`js_run_script("add()")`); // js_script/1 throws, js_script/2 requires 'window'

  console.log(data); // Correctly outputs [1]
})();

Example with ES modules:

import SWIPL from "swipl-wasm";

const data = [];

// Just add some data
global.add = function () {
  data.push(1);
};

const swipl = await SWIPL({ arguments: ["-q"] });
swipl.prolog.call(`js_run_script("add()")`); // js_script/1 throws, js_script/2 requires 'window'

console.log(data); // Correctly outputs [1]

So, I don’t know whether js_run_script/1 (which is undocumented) is the right predicate for calling JS from Prolog rather than js_script/2. Also, the example provided in the documentation seems to have the wrong arity (js_script/1 is used).

Hello. I’m trying to build as described in #wasm_wiki and after:

./configure
ninja

I get a repeated #error “Unsupported int64_t alignment conversion” always involving pl-incl.h:2867 - Any hint as to what is happening here?

@jacintodavila Don’t have insight on your exact error - but wanted to note that we have everything nicely pre-built for you over in GitHub - SWI-Prolog/npm-swipl-wasm: SWI-Prolog WebAssembly build as a NPM package.

And if you do need to make customisations to your build then the following Dockerfile is a good starting point npm-swipl-wasm/Dockerfile at 4bb2278f6be13fe0ade1133a9f134d284129f078 ¡ SWI-Prolog/npm-swipl-wasm ¡ GitHub (noting that there is this open PR updating the dockerfile slightly if you want to be at the bleeding edge)

2 Likes

Thank you Jesse @jeswr . I did try your bundle. Very useful. It went well cloning, npm install, npm run build. Then I started testing the examples. This runs fine npm run test:serve-http. But the node’s example does not:

$node --unhandled-rejections=strict examples/run-on-node.js
---/npm-swipl-wasm/examples/run-on-node.js:5
  console.log(swipl.prolog.query("member(X, [a, b, c]).").once().X);
                           ^

TypeError: Cannot read property 'query' of undefined
    at ---/npm-swipl-wasm/examples/run-on-node.js:5:28

The code in that js file is not the same that in the wiki:

wiki:

const SWIPL = require("swipl-wasm/dist/swipl");

in run-on-node.js

const SWIPL = require("../dist/swipl-node");

Can you confirm which is the correct module to require?

Thank you.

Hi @jacintodavila - I have created this quickly getting started guide that should get you up and running.

Note that if you clone the repo (which you should not do unless you are contributing to the project) then you can just use const SWIPL = require("../dist") for node and browser as we export an isomorphic bundle there.

If you are importing swipl-wasm in your own project like discussed in the getting started guide above; you get the same files by doing const SWIPL = require("swipl-wasm") or import SWIPL from "swipl-wasm"

Thank you for the getting started guide @jeswr - I carefully followed the instructions and got the same error. But then I noticed I was using an old version of node. I updated to v20.1.0 and it worked as expected.

But, how can I test the second example if I don´t have the scripts settings npm run test:serve-http nor the examples dir in there? (because I am not cloning the repo).

Btw, what I want is to bundle in my own prolog code, which is not a single file but a whole pack with prolog modules. Is that possible?

Thank you.