Prolog: Writing a Server-side App in WebAssembly using Spin
Tags: webassembly, serverless, tutorial, prolog
Published: 2023-10-31 19:34:05

This post walks through creating a simple Prolog serverless function. Prolog is a logic-oriented language with several implementations. We’ll be using Trealla Prolog.

> I am new to Prolog, so this is an exploration for me as well. For those more seasoned in the language, feel free to leave corrections and so on in the comments below. I probably used incorrect terminology and so on.

Prerequisites:
* [Spin 1.5 or greater](https://developer.fermyon.com/spin/install)

## Configuring Spin for Prolog Support
To install Prolog support in Spin, you can add the template:

```console
$ spin templates install --git https://github.com/guregu/trealla-spin --update
```

This uses the _amazing_ [Prolog Spin template](https://github.com/guregu/trealla-spin) and the Traella Prolog interpreter. I am a huge fan of this template because, as a newcomer to Prolog, it got me started right away.

## Building an App
First, we’ll create a new app using the newly installed `http-prolog` template. We’ll name the project `hello-prolog`:

```console
$ spin new http-prolog hello-prolog --accept-defaults
```

The option `—accept-defaults` just tells Spin not to walk us through the creation wizard and instead use the default settings.

Now we have a directory named `hello-prolog` with just a few files:

```console
$ tree hello-prolog/
hello-prolog/
├── spin.toml
└── src
    └── init.pl

1 directory, 2 files
```

Since Prolog is an interpreted language, we don’t necessarily need to build our source into a Wasm binary. All we need is a copy of the interpreter that is itself compiled to WebAssembly. If we take a look at `spin.toml` (the Spin configuration file), we’ll see exactly that:

```toml
spin_version = "1"
authors = ["Matt Butcher <matt.butcher@fermyon.com>"]
description = ""
name = "hello-prolog"
trigger = { type = "http", base = "/" }
version = "0.1.0"

[[component]]
id = "hello-prolog"
files = [ { source = "./src", destination = "/" } ]
# for outgoing HTTP (see spin:http_fetch/3)
allowed_http_hosts = [] # "insecure:allow-all" is a special unsafe value to allow any host
# access to key-value stores (see spin:store_* predicates)
key_value_stores = ["default"]
[component.source]
url = "https://github.com/guregu/trealla/releases/download/v0.14.4/libtpl-spin.wasm"
digest = "sha256:6adb31903bc55e2b5ef3db1619727596f0b08bb789ff6c42df458d0209228677"
[component.trigger]
route = "/..."
```

Note that the `url` line tells Spin to download the `libtpl-spin.wasm` file.

While many of the basic Spin templates produce boilerplate code, the Prolog template produces a great example that generates HTML, uses Key Value Store, serializing to JSON, and illustrates some cool features of Prolog. The program is in the `src/init.pl`:

```prolog
:- use_module(library(spin)).

% See library/spin.pl for all the predicates built-in
% https://github.com/guregu/trealla/blob/main/library/spin.pl

%% http_handler(+Spec, +Headers, +Body, -Status)

http_handler(get("/", _QueryParams), _RequestHeaders, _RequestBody, 200) :-
    html_content,
    setup_call_cleanup(
        store_open(default, Store),
        (
            (  store_get(Store, counter, N0)
            -> true
            ;  N0 = 0
            ),
            succ(N0, N),
            store_set(Store, counter, N)
        ),
        store_close(Store)
    ),
    http_header_set("x-powered-by", "memes"),
    current_prolog_flag(dialect, Dialect),
    % stream alias http_body is the response body
    write(http_body, '<!doctype html><html>'),
    format(http_body, "<h1>Hello, ~a prolog!</h1>", [Dialect]),
    format(http_body, "Welcome, visitor #<b>~d!</b>", [N]),
    write(http_body, '</html>').

http_handler(get("/json", _), _, _, 200) :-
    wall_time(Time),
    % json_content({"time": Time}) works too
    json_content(pairs([string("time")-number(Time)])).


```

The sample above implements two HTTP routes:
- Requests to `/` will get an HTML page with a view counter
- Requests to `/json` will get the server’s current time serialized into a JSON document

### The JSON endpoint

Let’s look at the `/json` one first:

```prolog
http_handler(get("/json", _), _, _, 200) :-
    wall_time(Time),
    % json_content({"time": Time}) works too
    json_content(pairs([string("time")-number(Time)])).
```

This applies the `http_handler` rule. First it fetches the time (`wall_time()`), then it transforms the time the JSON object `{“time”: :1698343563}` (where that long number is the UNIX timestamp).

### The HTML endpoint

We already looked at the second  `http_handler()`  rule, which generates JSON. Let’s take a quick high-level look at the first.

This rule does two things:
* It manages a page view counter using Spin’s key value storage
* It generates some HTML to return to the user

The key value storage logic is this part:

```prolog
		store_open(default, Store),
        (
            (  store_get(Store, counter, N0)
            -> true
            ;  N0 = 0
            ),
            succ(N0, N),
            store_set(Store, counter, N)
        ),
        store_close(Store)
```

Essentially, with an open store, we check to see if the counter is already present. If not, it is initialized to 0. Then the counter is incremented and stored. At the end, the store is closed.

So each time this rule is invoked, the counter will be incremented.

The second part of this handler creates the HTTP response:

```prolog
	http_header_set("x-powered-by", "memes"),
    current_prolog_flag(dialect, Dialect),
    % stream alias http_body is the response body
    write(http_body, '<!doctype html><html>'),
    format(http_body, "<h1>Hello, ~a prolog!</h1>", [Dialect]),
    format(http_body, "Welcome, visitor #<b>~d!</b>", [N]),
    write(http_body, '</html>').
```

The HTTP header `X-Powered-By` is set to `memes` . This is just a fun illustration of how to set a header.

Afterward, the next four terms write out HTML. The two terms that use `format` are substituting terms (the Prolog `Dialect` and the counter `N` from key value storage) into the string.

When we access the endpoint, this is what we’ll see:

```console
$ curl localhost:3000/    
<!doctype html><html><h1>Hello, trealla prolog!</h1>Welcome, visitor #<b>1!</b></html>
```

And running it again, we’ll see the counter increment:

```console
curl localhost:3000/    
<!doctype html><html><h1>Hello, trealla prolog!</h1>Welcome, visitor #<b>2!</b></html>
```

## Deploying to Fermyon Cloud
The notes on the `spin-prolog` README suggest that the demo will not work on Fermyon Cloud. However, I tested it out, and it works great!

I did a `spin deploy` (after doing a `spin login` using GitHub integration):

```console
$ spin deploy
Uploading hello-prolog version 0.1.0-rb0b520ce to Fermyon Cloud...
Deploying...
Waiting for application to become ready....... ready
Available Routes:
  hello-prolog: https://hello-prolog-grl101su.fermyon.app (wildcard)
```

This produced a public endpoint (which you can test. I left it running).

Testing with `curl` a few times, I see:

```console
$ curl https://hello-prolog-grl101su.fermyon.app
<!doctype html><html><h1>Hello, trealla prolog!</h1>Welcome, visitor #<b>1!</b></html>
$ curl https://hello-prolog-grl101su.fermyon.app
<!doctype html><html><h1>Hello, trealla prolog!</h1>Welcome, visitor #<b>2!</b></html>
$ curl https://hello-prolog-grl101su.fermyon.app
<!doctype html><html><h1>Hello, trealla prolog!</h1>Welcome, visitor #<b>3!</b></html>
```

As the notes on the Spin Prolog page note, most of the Fermyon Cloud services are accessible through the SDK, though the serverless AI LLM inferencing is not yet available.

## Wrap-Up

Prolog is a new language for me. I have a background in formal logic, though, and it is such an enticing language from that perspective.

I'm super grateful to [Guregu](https://github.com/guregu) for making my first forays into the language so pleasant.