Scripting webpages with Prolog, WebAssembly and Virtual DOM
Some time ago I experimented with a Prolog Virtual DOM implementation to create and manipulate the webpage DOM inside a web browser. I wanted to see how suitable would Prolog terms be for representing the page elements. My second goal was to see if SWI-Prolog can be easily ported to run on WebAssembly. SWI-Prolog is the most popular Open Source Prolog implementation.
Prolog for browser
WebAssembly is a relatively new browser technology that makes it possible to run standardised bytecode on browsers. Its design started in 2015 and the core spec was released in early 2018. WebAssembly is comparable to Java applets, an ancient technology once popular 20 years ago. However, compared to Java applets, WebAssembly bytecode is much more lower-level, suitable for efficient compilation from C and C++. WebAssembly is not tied to any particular high-level language. The sandbox is much stricter and a lot more secure than Java applets ever were. WebAssembly (a core version) is supported by all major browsers today and the implementation is not tied to a single company as it was with Java applets.
It would be nice to have a Prolog to WebAssembly compiler but instead of building another Prolog implementation (someone else can do it), I decided to port SWI-Prolog to WebAssembly. SWI-Prolog itself is written in the C language. Once we can run SWI-Prolog on WebAssembly, we have a high-quality feature-complete Prolog in browsers.
SWI-Prolog on WebAssembly
WebAssembly is a bare sandbox, a raw blob of memory. It has no concept of OS. Lots of C code, including SWI-Prolog, depends on the C standard library to read files and interact with out-of-process resources. Emscripten is a compiler that provides all this. It emulates the common OS interfaces and provides some glue code for things like the virtual file system.
The biggest roadblock in porting SWI-Prolog to WebAssembly turned out to be a file system issue. SWI-Prolog compiles a set of predicates into a so-called boot file. The file needs to be created during the build process. We had to modify the build process to run the WebAssembly build of SWI through Node.js (another WebAssembly implementation besides modern browsers) to generate the boot file. The porting process is more-less described here.
Boot file issue aside, the porting process went quite smooth. Not many C language related code changes had to be done. Jan Wielemaker, the main SWI-Prolog author, helped me out with those changes.
We can fake the DOM tree, manipulate with this fake tree and then somehow magically transfer the manipulation effects to the actual DOM tree. This approach called Virtual DOM (VDOM). With VDOM, you construct a tree that corresponds to the actual tree of DOM elements, except that the VDOM tree does not contain real DOM elements. It only needs to contain enough information to construct the real DOM elements.
In the case of Prolog, the VDOM tree would simply be a specifically shaped Prolog term. Here is an example of a
div element containing
an unordered list:
div(, [ ul([className=my_list], [ li(, ['Item1']) li(, ['Item2']) ]) ])
Example of a component:
:- vdom_component_register(hello, render_hello). render_hello(Name, _, VDom):- VDom = div(, [ 'Hello ', Name ]).
and its usage in elsewhere of the application:
div(, [ hello([data='World'], ) ])
document.body element. Whenever an event is caught, a Prolog-side predicate is called. The target element's
id attribute is passed to the predicate. The predicate will modify the application state, render a new VDOM tree and calculate the
Example of a click event handler:
event_click(Id, Diff, true):- atom_concat('task-', _, Id), !, update_state(toggle_task_done(Id)), diff(Diff).
I created a small ToDo application. The app can be viewed from here. The difference terms can be seen using by opening the browser's developer console. The demo should work with the latest versions of all major browsers (Chrome, Firefox, Edge and Safari).
Prolog terms are a good fit for representing DOM elements. This representation was borrowed from the SWI-Prolog server-side HTML support package. The bigger issue is embedding loop-like constructs or general expressions. Some time ago I built a Prolog server-side templating language and I had to come up with my own expression language in there.
The biggest issue with WebAssembly is complexity. Here we have a 4-level language interface:
Virtual DOM itself is relatively opaque to debugging. The diffing algorithm does 3 things in a single pass:
- Calculate diff;
- Produce full VDOM tree (as components are either rendered or reused);
- Render components.
There is also a practical issue of having to download a Prolog runtime. The compiled core is 1.3MB. The virtual file system file (VFS) containing the boot file and the standard library is 2MB. For a simple page this is quite a lot. Runtime and the VFS file can be effectively cached so that subsequent page visits are fast. Prepared WebAssembly bytecode can be cached by the browser (although Emscripten does not support it at the moment), making repeated visits faster again.