Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
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
- Host: GitHub
- URL: https://github.com/guregu/trealla-js
- Owner: guregu
- License: mit
- Created: 2022-08-30T21:10:31.000Z (about 2 years ago)
- Default Branch: master
- Last Pushed: 2024-05-23T07:45:36.000Z (6 months ago)
- Last Synced: 2024-05-23T08:50:45.489Z (6 months ago)
- Topics: javascript, logic-programming, prolog, webassembly
- Language: TypeScript
- Homepage:
- Size: 284 KB
- Stars: 33
- Watchers: 5
- Forks: 0
- Open Issues: 7
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome-ccamel - guregu/trealla-js - Trealla Prolog for the web (TypeScript)
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 unconditionallyThrowing 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.