UNPKG

trealla-multibundle

Version:

Trealla Prolog bindings for JS

593 lines (453 loc) β€’ 20.2 kB
# 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/trealla@X.Y.Z`. ```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 <!-- 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> ``` ```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<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))`. ```typescript // 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. ```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 = "<html><head><title>Example page..." ``` Function signature of eval: ```typescript 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. ```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 = δΈ–η•Œ } ``` ### Template strings The `prolog` string template literal is an easy way to escape terms. Each `${value}` will be interpreted as a Prolog term. ```typescript import { prolog, Variable } from "trealla"; const result = await pl.queryOnce( prolog`atom_chars(${new Variable("X")}, ${"Hello!"}).`, ); console.log(result.answer); // { X: Atom('Hello!') } ``` ## 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<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"; /** Encoding for Prolog integers. Default is "fit", which uses bigints if outside of the safe integer range. */ integers?: "fit" | "bigint" | "number"; /** 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 rational(X) β†’ Rational number(X) β†’ number var(X) β†’ Variable */ type Term = Atom | Compound | Variable | List | string | number | bigint | Rational; 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; } type Numeric = number | bigint; class Rational { constructor(numerator: Numeric, denominator: Numeric); numerator: Numeric; denominator: Numeric; 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](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.