/trealla-js

Trealla Prolog for the web

Primary LanguageTypeScriptMIT LicenseMIT

trealla-js

Javascript TypeScript bindings for Trealla Prolog.

Trealla is a quick and lean ISO Prolog interpreter.

Trealla is built targeting WASI 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/trealla@X.Y.Z.

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 as trealla.

npm install trealla
import { load, Prolog } from 'trealla';

Example

Javascript to Prolog

<!-- Make sure to use type="module" for inline scripts. -->
<script type="module">

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" }

</script>
{
  "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.

// Native predicates are fully type-safe :-)
export type PredicateFunction<G extends Goal> =
	(pl: Prolog, subq: Ptr<subquery_t>, goal: G, ctrl: Ctrl) =>
		Continuation<G> | Promise<Continuation<G>> | AsyncIterable<Continuation<G>>;
export type Continuation<G extends Goal> = G | boolean;
export type Goal = Atom | Compound<string, [Term, ...Term[]]>;

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)).

// Example of between/3 implemented in JS
export const betwixt_3 = new Predicate<Compound<"betwixt", [number, number, number | Variable]>>(
    "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.

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.

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

Function signature of eval:

function eval(pl: Prolog, subq: Ptr<subquery_t>, 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.

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. Although we don't use wasmer-js anymore, the same API is still provided.

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.

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

  /** 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<T = Answer>(goal: string, options?: QueryOptions): AsyncGenerator<T, void, void>;
    /** Runs a query and returns a single solution, ignoring others. */
    public queryOnce<T = Answer>(goal: string, options?: QueryOptions): Promise<T>;

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

    /** 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<string, string>;
    /** 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<any, any>;
    /** Encoding options for "json" or custom formats. */
    encode?: EncodingOptions;
    /** Automatic yield interval in milliseconds. Default is 20ms. */
    autoyield?: number;
  }

  type EncodingOptions = JSONEncodingOptions | PrologEncodingOptions | Record<string, unknown>;

  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<string, Term>;

  /** 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<Answer, JSONEncodingOptions>,
    prolog: Toplevel<string, PrologEncodingOptions>,
    // add your own!
    // [name: string]: Toplevel<any, any>
  };

  interface Toplevel<T, Options> {
    /** 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. 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 object.

%! 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.

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.

%! 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.

%! 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.

%! 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 and only has access to the global envrionment.

%! 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.

%! 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.

%! 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([...])
%! 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. JSON output goes through the wasm module.

Development

Make sure you can build Trealla.

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

See Also