Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/guregu/trealla-js

Trealla Prolog for the web
https://github.com/guregu/trealla-js

javascript logic-programming prolog webassembly

Last synced: 9 days ago
JSON representation

Trealla Prolog for the web

Awesome Lists containing this project

README

        

# trealla-js

~~Javascript~~ TypeScript bindings for [Trealla Prolog](https://github.com/trealla-prolog/trealla).

Trealla is a quick and lean ISO Prolog interpreter.

Trealla is built targeting [WASI](https://wasi.dev/) and should be useful for both browsers and serverless runtimes.

**Demo**: https://php.energy/trealla.html

**Status**: beta!

## Get

trealla-js embeds the Trealla WASM binary. Simply import the module, load it, and you're good to go.

### JS Modules

You can import Trealla directly from a CDN that supports ECMAScript Modules.

For now, it's best to pin a version as in: `https://esm.sh/[email protected]`.

```js
import { load, Prolog } from 'https://esm.sh/trealla';
import { load, Prolog } from 'https://esm.run/trealla';
import { load, Prolog } from 'https://unpkg.com/trealla';
import { load, Prolog } from 'https://cdn.skypack.dev/trealla';
```

### NPM

This package is [available on NPM](https://www.npmjs.com/package/trealla) as `trealla`.

```bash
npm install trealla
```

```js
import { load, Prolog } from 'trealla';
```

## Example

### Javascript to Prolog

```html

import { Prolog, load, atom } from 'https://esm.sh/trealla';

// Load the runtime.
// This is requred before construction of any interpreters.
await load();

// Create a new Prolog interpreter
// Each interpreter is independent and persistent
const pl = new Prolog();

// Queries are async generators.
// You can run multiple queries against the same interpreter simultaneously.
const query = pl.query('between(2, 10, X), Y is X^2, format("(~w,~w)~n", [X, Y]).');
for await (const answer of query) {
console.log(answer);
}

// Use the bind option to easily bind variables.
// You can bind strings as-is.
// Atoms can be quickly constructed with the atom template tag.
// See: Term type.
const greeting = await pl.queryOnce('format("hello ~a", [X])', {bind: {X: atom`world`}});
console.log(greeting.stdout); // "hello world"
console.log(greeting.answer.X); // Atom { functor: "world" }

```

```javascript
{
"status": "success",
"answer": {"X": 2, "Y": 4},
"stdout": "(2,4)\n"
}
// ...
```

### Prolog to Javascript

Experimental. With great power comes great responsibility 🀠

#### Writing a Prolog predicate in Javascript πŸ†•

You can implement Prolog predicates using Javascript.

This is useful for taking advantage of browser functionality, or utilizing JS's async runtime.

```typescript
// Native predicates are fully type-safe :-)
export type PredicateFunction =
(pl: Prolog, subq: Ptr, goal: G, ctrl: Ctrl) =>
Continuation | Promise> | AsyncIterable>;
export type Continuation = G | boolean;
export type Goal = Atom | Compound;
```

Create a new Predicate with `new Predicate(...)` and register it with `pl.register(...)`.

The return value of all predicates is a "continuation" that is either:
- A goal that will be unified with the call
- Boolean `true` to succeed unconditionally
- Boolean `false` to fail unconditionally

Throwing a Prolog term will cause `throw/1` to be called by the guest.
Throwing a non-Term will become `throw(error(system_error(js_exception, "details..."), foo/N))`.

```typescript
// Example of between/3 implemented in JS
export const betwixt_3 = new Predicate>(
"betwixt", 3,
async function*(_pl, _subquery, goal) {
const [min, max, n] = goal.args;
if (!isNumber(min))
throw type_error("number", min, goal.pi);
if (!isNumber(max))
throw type_error("number", max, goal.pi);

for (let i = isNumber(n) ? n : min; i <= max; i++) {
goal.args[2] = i;
if (i == max)
return goal;
yield goal;
}
});

await pl.register(betwixt_3, /* optional module name */);
```

The fanciest predicate function is an async generator, in which you can use `yield` to create choice points, and `return` as a kind of internal cut.

You can also use regular async functions (i.e. functions that return a `Promise`) or plain functions.

The Prolog interpreter will automatically yield to the host when calling a native predicate backed by an async function or generator.

#### Evaluating JS code from Prolog

**NOTE**: work in progress, see `examples/{hostcall,yield}.mjs`

The JS host will evaluate the expression you give it ~~and marshal it to JSON~~.
You can use `js_eval/2` to grab the result.

```prolog
greet :-
js_eval("return prompt('Name?');", Name),
format("Greetings, ~s.", [Name]).

here(URL) :-
js_eval("return new trealla.Atom(location.href);", URL).
% URL = 'https://php.energy/trealla.html'
```

If your evaluated code returns a promise, Prolog will yield to the host to evaluate the promise.
Hopefully this should be transparent to the user.

```prolog
?- js_eval("return fetch('http://example.com').then(x => x.text());", Src).
Src = "Example page..."
```

Function signature of eval:
```typescript
function eval(pl: Prolog, subq: Ptr, goal: Goal, trealla: {...LIBRARY_BINDINGS}) {
/* your code here */
// return someTerm;
}
```

The `trealla` argument provides bindings to the library's constructors for terms.

### Caveats

Multiple queries can be run concurrently. If you'd like to kill a query early, use the `return()` method on the generator returned from `query()`.
This is not necessary if you iterate through until it is finished.

### Output format

You can change the output format with the `format` option in queries.

The format is `"json"` by default which goes through `library(js)` and returns JSON-friendly Javascript objects (see: type `Term`).

### `"prolog"` format

You can get pure text output with the `"prolog"` format.
The output is the same as Trealla's regular toplevel, but full terms (with a dot) are printed.

```javascript
for await (const answer of pl.query(`dif(A, B) ; dif(C, D).`, {format: "prolog"})) {
console.log(answer);
};
// "dif(A,B)."
// "dif(C,D)."
```

### Automatic yielding

By default, the interpreter will yield every 20ms to let the UI thread catch up.
This prevents long-running queries from freezing the browser, but incurs a small (~20%) overhead.
You can disable this behavior by setting the query option `autoyield` to `0`.

### Virtual Filesystem

Each Prolog interpreter instance has its own virtual filesystem you can read and write to.
For details, check out the [wasmer-js docs for `MemFS`](https://github.com/wasmerio/wasmer-js/tree/1184d7acbb77d424003b701278d2580107c50918?tab=readme-ov-file#typescript-api).
Although we don't use wasmer-js anymore, the same API is still provided.

```js
const pl = new Prolog();
// create a file in the virtual filesystem
pl.fs.open("/greeting.pl", { write: true, create: true }).writeString(`
:- module(greeting, [hello/1]).
hello(world).
hello(δΈ–η•Œ).
`);

// consult file
await pl.consult("/greeting.pl");

// use the file we added
const query = pl.query("use_module(greeting), hello(X)");
for await (const answer of query) {
console.log(answer); // X = world, X = δΈ–η•Œ
}
```

## Javascript API
Approaching stability.

```typescript
declare module 'trealla' {
/** Call this first to load the runtime.
Must be called before any interpreters are constructed. */
function load(): Promise;

/** Prolog interpreter.
Each interpreter is independent, having its own knowledgebase and virtual filesystem.
Multiple queries can be run against one interpreter simultaneously. */
class Prolog {
constructor(options?: PrologOptions);

/** Run a query. This is an asynchronous generator function.
Use a `for await` loop to easily iterate through results.
Exiting the loop will automatically destroy the query and reclaim memory.
If manually iterating with `next()`, call the `return()` method of the generator to kill it early.
Runtimes that support finalizers will make a best effort attempt to kill live but garbage-collected queries. */
public query(goal: string, options?: QueryOptions): AsyncGenerator;
/** Runs a query and returns a single solution, ignoring others. */
public queryOnce(goal: string, options?: QueryOptions): Promise;

/** Consult (load) a Prolog file with the given filename. */
public consult(filename: string): Promise;
/** Consult (load) a Prolog file with the given text content. */
public consultText(text: string | Uint8Array): Promise;

/** Use fs to manipulate the virtual filesystem. */
public readonly fs: FS;
}

interface PrologOptions {
/** Library files path (default: "/library")
This is to set the search path for use_module(library(...)). */
library?: string;
/** Environment variables.
Accessible with the predicate getenv/2. */
env?: Record;
/** Quiet mode. Disables warnings printed to stderr if true. */
quiet?: boolean;
/** Manually specify module instead of the default. */
module?: WebAssembly.Module;
}

interface QueryOptions {
/** Mapping of variables to bind in the query. */
bind?: Substitution;
/** Prolog program text to evaluate before the query. */
program?: string | Uint8Array;
/** Answer format. This changes the return type of the query generator.
`"json"` (default) returns Javascript objects.
`"prolog"` returns the standard Prolog toplevel output as strings.
You can add custom formats to the global `FORMATS` object.
You can also pass in a `Toplevel` object directly. */
format?: keyof typeof FORMATS | Toplevel;
/** Encoding options for "json" or custom formats. */
encode?: EncodingOptions;
/** Automatic yield interval in milliseconds. Default is 20ms. */
autoyield?: number;
}

type EncodingOptions = JSONEncodingOptions | PrologEncodingOptions | Record;

interface JSONEncodingOptions {
/** Encoding for Prolog atoms. Default is "object". */
atoms?: "string" | "object";
/** Encoding for Prolog strings. Default is "string". */
strings?: "string" | "list";

/** Functor for compounds of arity 1 to be converted to booleans.
For example, `"{}"` to turn the Prolog term `{true}` into true ala Tau,
or `"@"` for SWI-ish behavior that uses `@(true)`. */
booleans?: string;
/** Functor for compounds of arity 1 to be converted to null.
For example, `"{}"` to turn the Prolog term `{null}` into null`. */
nulls?: string;
/** Functor for compounds of arity 1 to be converted to undefined.
For example, `"{}"` to turn the Prolog term `{undefined}` into undefined`. */
undefineds?: string;
}

interface PrologEncodingOptions {
/** Include the fullstop "." in results. */
/** True by default. */
dot?: boolean;
}

/** Answer for the "json" format. */
interface Answer {
status: "success" | "failure" | "error";
answer?: Substitution;
error?: Term;
/** Standard output text (`user_output` stream in Prolog) */
stdout?: string;
/** Standard error text (`user_error` stream in Prolog) */
stderr?: string;
}

/** Mapping of variable name β†’ Term substitutions. */
type Substitution = Record;

/** Prolog term.
Default encoding (in order of priority):
string(X) β†’ string
is_list(X) β†’ List
atom(X) β†’ Atom
compound(X) β†’ Compound
integer(X) β†’ BigInt if necessary
number(X) β†’ number
var(X) β†’ Variable
*/
type Term = Atom | Compound | Variable | List | string | number | BigInt;

type List = Term[];

class Atom {
constructor(functor: string);
functor: string;
/** Predicate indicator (example: `"foo/0"`) */
readonly pi: string;
toProlog(): string;
}

/** String template literal for making atoms: atom`foo` = 'foo'. */
function atom([functor]): Atom;

class Compound {
constructor(functor: string, args: List);
functor: string;
args: List;
/** Predicate indicator (in `"foo/N"` format) */
readonly pi: string;
toProlog(): string;
}

class Variable {
constructor(name: string, attr: List);
/** Variable name. */
var: string;
/** Residual goals. */
attr?: List;
toProlog(): string;
}

/** Convert Term objects to their Prolog text representation. */
function toProlog(object: Term): string;

/** Parse JSON representations of terms. */
function fromJSON(json: string, options?: JSONEncodingOptions): Term;

/** Convert Term objects to JSON text. */
function toJSON(term: Term, indent?: string): string;

const FORMATS: {
json: Toplevel,
prolog: Toplevel,
// add your own!
// [name: string]: Toplevel
};

interface Toplevel {
/** Prepare query string, returns goal to execute. */
query(pl: Prolog, goal: string, bind?: Substitution, options?: Options): string;
/** Parse stdout and return an answer. */
parse(pl: Prolog, status: boolean, stdout: Uint8Array, stderr: Uint8Array, options?: Options): T;
/** Yield simple truth value, when output is blank.
For queries such as `true.` and `1=2.`.
Return null to bail early and yield no values. */
truth(pl: Prolog, status: boolean, stderr: Uint8Array, options?: Options): T | null;
}
}
```

# Predicate reference

trealla-js includes [all libraries bundled with Trealla](https://github.com/guregu/trealla/tree/main/library).
Import a library module with the `use_module(library(Name))` directive or predicate.

The predicates described below are imported by default.

## Specialized built-ins

These predicates are Trealla built-ins specialized for a Javascript execution environment.

### crypto_data_hash/3

Hashes the given string and options. Calls into the global [`crypto`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest) object.

```prolog
%! crypto_data_hash(+Data, -Hash, +Options) is det.
% Unifies Hash with a hashed hex string representation of Data, which is a string.
% Options is a list of options:
% - algorithm(Algorithm): Algorithm is an atom representing the hash algorithm to use.
% One of: sha256 (default), sha386, sha512, sha1 (insecure).
crypto_data_hash(Data, Hash, Options).
```

This will only work in secure contexts (i.e. over HTTPS) in browsers. Node users may need to set the global crypto object.

```js
import crypto from "node:crypto";
globalThis.crypto = crypto;
```

### sleep/1

Sleeps for the given amount of seconds. This yields to the host, unblocking the main thread for the duration.

```prolog
%! sleep(+N) is det.
% Sleep for N seconds. N is an integer.
sleep(Seconds).
```

## library(wasm_js)

Module `library(wasm_js)` provides predicates for calling into the host.

### http_consult/1

Load Prolog code from URL.

```prolog
%! http_consult(+URL) is det.
% Downloads Prolog code from URL, which must be a string, and consults it.
http_consult(URL).
```

### http_fetch/3

Fetch content from a URL.

```prolog
%! http_fetch(+URL, +Options, -Content) is det.
% Fetch URL (string) and unify the result with Content.
% This is a friendly wrapper around Javascript's fetch API.
% Options is a list of options:
% - as(string): Content will be unified with the text of the result as a string
% - as(json): Content will be parsed as JSON and unified with a JSON term
% - headers(["key"-"value", ...]): HTTP headers to send
% - body(Cs): body to send (Cs is string)
http_fetch(URL, Options, Content).
```

### js_eval_json/2

Evaluate a string of Javascript code. Code is evaluated using [`Function`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function) and only has access to the global envrionment.

```prolog
%! js_eval_json(+Code, -JSON) is det.
% Evaluate Code, which must be a string of valid Javascript code.
% Returning a promise will cause the query to yield to the host. The host will await the promise and resume the query.
% Return values are encoded to JSON and returned as a JSON term (see pseudojson:json_value/2).
js_eval_json(Code, JSON).
```

### js_eval/2

Low-level predicate for evaluating JS code.

```prolog
%! js_eval(+Code, -Cs) is det.
% Low-level predicate that functions the same as js_eval_json/2 but without the JSON decoding.
% Returning a Uint8Array in your JS code will bypass the host's default JSON encoding.
% Combined with this, you can customize the host->guest API.
js_eval(Code, Cs).
```

## library(pseudojson)

Module `library(pseudojson)` is preloaded. It provides very fast predicates for encoding and decoding JSON.
Its One Crazy Trick is using regular Prolog terms such as `{"foo":"bar"}` for reading/writing.
This means that it accepts invalid JSON that is a valid Prolog term.

The predicate `json_value/2` converts between the same representation of JSON values as `library(json)`, to ensure future compatibility.
You are free to use `library(json)` which provides a JSON DCG that properly validates (but is slow for certain inputs).

### json_chars/2

Encoding and decoding of JSON strings.

```prolog
%! json_chars(?JSON, ?Cs) is det.
% JSON is a Prolog term representing the JSON.
% Cs is a JSON string.
json_chars(JSON, Cs).
```

### json_value/2

Relates JSON terms and friendlier Value terms that are compatible with `library(json)`.

- strings: `string("abc")`
- numbers: `number(123)`
- booleans: `boolean(true)`
- objects: `pairs([string("key")-Value, ...])`
- arrays: `list([...])`

```prolog
%! json_value(?JSON, ?Value) is det.
% Unifies JSON and Value with their library(pseudojson) and library(json) counterparts.
% Can be used to convert between JSON terms and friendlier Value terms.
json_value(JSON, Value).
```

## Implementation Details

Currently uses the WASM build from [guregu/trealla](https://github.com/guregu/trealla).
JSON output goes through the [`wasm`](https://github.com/guregu/trealla/blob/main/library/wasm.pl) module.

### Development

Make sure you can build Trealla.

```bash
# install deps
npm install
# build wasm
npm run compile
# build js
npm run build
# (build and) run tests
npm run test
```

## See Also

- [trealla-prolog/go](https://github.com/trealla-prolog/go) is Trealla for Go.
- [Tau Prolog](http://www.tau-prolog.org/) is a pure Javascript Prolog.
- [SWI Prolog](https://swi-prolog.discourse.group/t/swi-prolog-in-the-browser-using-wasm/5650) has a WASM implementation using Emscripten.
- [Ciao](https://github.com/ciao-lang/ciaowasm) has a WASM implementation using Emscripten.