@gaussylean/trealla
TypeScript icon, indicating that this package has built-in type declarations

0.13.24 • Public • Published

trealla-js

Javascript 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

<!-- 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>
{
  "result": "success",
  "answer": {"X": 2, "Y": 4},
  "stdout": "(2,4)\n"
}
// ...

Prolog to Javascript

Experimental. With great power comes great responsibility 🤠

The JS host will evaluate the expression you give it and marshal it to JSON. You can use js_eval_json/2 to grab the result. Note that JSON does not handle all types such as undefined.

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

here(URL) :-
  js_eval_json("return 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_json("fetch('http://example.com').then(x => x.text());", Src).
   Src = "<html><head><title>Example page..."

js_eval/2 works the same but does not attempt to parse the JSON. If your JS expression returns a Uint8Array, it will be returned as-is instead of JSON-encoded.

?- js_eval("return new TextEncoder().encode('arbitrary text');", Result)
   Result = "arbitrary text".

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

"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:dif(A,B)."
// "dif:dif(C,D)."

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();
// 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: any; // wasmer-js filesystem
  }

  interface PrologOptions {
    // Library files path (default: "/library")
    // This is 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;
  }

  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/null/undefined.
    // e.g. "{}" to turn {true} into true ala Tau, "@" for SWI-ish behavior.
    booleans?: string;
    nulls?: string;
    undefineds?: string;
  }

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

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

  // Mapping of variable name → Term substitutions.
  type Substitution = Record<string, 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 {
    functor: string;
    readonly pi: string; // predicate indicator ("foo/0")
    toProlog(): string;
  }

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

  class Compound {
    functor: string;
    args: List;
    readonly pi: string; // predicate indicator ("foo/N")
    toProlog(): string;
  }

  class Variable {
    var: string; // variable name
    attr?: List; // residual goals
    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

library(js)

Module library(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).

js_fetch/3

Fetch content from a URL.

%! js_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)
js_fetch(URL, Options, Content).

js_eval_json/2

Evaluate a string of Javascript code.

%! 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. Output goes through the js module.

Development

Make sure you can build Trealla.

# install deps
npm install
# build wasm
npm run compile
# build js
npm run build
# "test"
node examples/node.mjs

See Also

Package Sidebar

Install

npm i @gaussylean/trealla

Weekly Downloads

3

Version

0.13.24

License

MIT

Unpacked Size

8.4 MB

Total Files

9

Last publish

Collaborators

  • gaussylean