trealla-js
Javascript TypeScript bindings for Trealla Prolog via wasmer-js.
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
<script type="module">
import { Prolog, load, atom } from 'https://esm.sh/trealla';
await load();
const pl = new Prolog();
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);
}
const greeting = await pl.queryOnce('format("hello ~a", [X])', {bind: {X: atom`world`}});
console.log(greeting.stdout);
console.log(greeting.answer.X);
</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.
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))
.
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, );
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}) {
}
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);
};
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.
const pl = new Prolog();
pl.fs.open("/greeting.pl", { write: true, create: true }).writeString(`
:- module(greeting, [hello/1]).
hello(world).
hello(世界).
`);
await pl.consult("/greeting.pl");
const query = pl.query("use_module(greeting), hello(X)");
for await (const answer of query) {
console.log(answer);
}
Javascript API
Approaching stability.
declare module 'trealla' {
function load(): Promise<void>;
class Prolog {
constructor(options?: PrologOptions);
public query<T = Answer>(goal: string, options?: QueryOptions): AsyncGenerator<T, void, void>;
public queryOnce<T = Answer>(goal: string, options?: QueryOptions): Promise<T>;
public consult(filename: string): Promise<void>;
public consultText(text: string | Uint8Array): Promise<void>;
public readonly fs: any;
}
interface PrologOptions {
library?: string;
env?: Record<string, string>;
quiet?: boolean;
module?: WebAssembly.Module;
}
interface QueryOptions {
bind?: Substitution;
program?: string | Uint8Array;
format?: keyof typeof FORMATS | Toplevel<any, any>;
encode?: EncodingOptions;
autoyield?: number;
}
type EncodingOptions = JSONEncodingOptions | PrologEncodingOptions | Record<string, unknown>;
interface JSONEncodingOptions {
atoms?: "string" | "object";
strings?: "string" | "list";
booleans?: string;
nulls?: string;
undefineds?: string;
}
interface PrologEncodingOptions {
dot?: boolean;
}
interface Answer {
status: "success" | "failure" | "error";
answer?: Substitution;
error?: Term;
stdout?: string;
stderr?: string;
}
type Substitution = Record<string, Term>;
type Term = Atom | Compound | Variable | List | string | number | BigInt;
type List = Term[];
class Atom {
constructor(functor: string);
functor: string;
readonly pi: string;
toProlog(): string;
}
function atom([functor]): Atom;
class Compound {
constructor(functor: string, args: List);
functor: string;
args: List;
readonly pi: string;
toProlog(): string;
}
class Variable {
constructor(name: string, attr: List);
var: string;
attr?: List;
toProlog(): string;
}
function toProlog(object: Term): string;
function fromJSON(json: string, options?: JSONEncodingOptions): Term;
function toJSON(term: Term, indent?: string): string;
const FORMATS: {
json: Toplevel<Answer, JSONEncodingOptions>,
prolog: Toplevel<string, PrologEncodingOptions>,
};
interface Toplevel<T, Options> {
query(pl: Prolog, goal: string, bind?: Substitution, options?: Options): string;
parse(pl: Prolog, status: boolean, stdout: Uint8Array, stderr: Uint8Array, options?: Options): T;
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)
is autoloaded. It 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.
npm install
npm run compile
npm run build
node examples/node.mjs
See Also