@ciao-lang/ts-ciao-interface
Version:
Simple Ciao interface for node.
1,445 lines (1,312 loc) • 46.9 kB
JavaScript
/*
* ciao-prolog.js
*
* Main JavaScript interface to Ciao Prolog compiled to WebAssembly.
*
* Copyright (C) 2017-2023 Jose F. Morales
*/
/**
The JS interface for Ciao is implemented in several layers. See
the documentation of the following classes/objects for more
information:
- `ToplevelProc`
- `CiaoWorker`
- `LLCiao`
- `CIAOENGINE`
*/
/* =========================================================================== */
/**
* `CiaoPromiseProxy`: promise object used to store and communicate
* results from `CiaoWorker` when using a Web Worker.
*/
class CiaoPromiseProxy {
/* TODO: similar to (:- block) implementation */
/* TODO: 'reject' not supported */
/* TODO: Make sure that this passes the Promises tests suite: https://github.com/promises-aplus/promises-tests */
constructor() {
this.hasValue = false; /* has a value */
this.value = null; /* value */
this.handler = null; /* 'then' handler */
this.next = null; /* temporary promise, when the handler is not yet there */
}
setValue(value) {
if (typeof this.handler === 'function') { /* we have both handler and value, call */
var ret = this.handler(value);
if (typeof ret === 'undefined') {
this.next.setValue(undefined); /* no value */
} else {
if (typeof ret.then === 'function') { /* assume some Thenable (a promise) */
var this_next = this.next;
ret.then(function (r) {
this_next.setValue(r); /* propagate value */
});
} else {
this.next.setValue(ret); /* just value (equivalent to a promise coercion) */
}
}
} else { /* otherwise save the value */
this.value = value;
this.hasValue = true;
}
}
/* Note: assume that handler returns undefined or a promise */
then(handler) {
if (this.hasValue === true) { /* have both! call handler */
var ret = handler(this.value);
if (typeof ret.then === 'function') { /* assume some Thenable (a promise) */
return ret;
} else { /* promise coercion */
return CiaoPromiseProxy.resolve(ret);
}
} else { /* otherwise save the handler */
var next = new CiaoPromiseProxy(); /* create temporary promise */
this.next = next;
this.handler = handler;
return next;
}
}
/* A promise with resolved value `v` */
static resolve(v) {
var p = new CiaoPromiseProxy();
p.setValue(v);
return p;
}
}
/* =========================================================================== */
/**
* `LLCiao`: Low level interface for CIAOENGINE. It implements
* environment setup, loading of bundle data.
*
* This function should not have any dependency with the parent
* code. It can be executed directly or captured as a string to be
* loaded as a Web Worker.
*/
function new_LLCiao() {
var ENVIRONMENT_IS_WORKER = typeof importScripts === 'function';
var ENVIRONMENT_IS_NODE = typeof process == 'object' && typeof process.versions == 'object' && typeof process.versions.node == 'string';
if (ENVIRONMENT_IS_NODE) {
const fs = require('fs/promises');
const vm = require('vm');
var tryImportScript = async function (src) {
global.require = require; // TODO: why?
global.__dirname = __dirname; // TODO: why?
global.tryImportScript = tryImportScript; // TODO: why?
// TODO: better way?
var data = await fs.readFile(src);
// Note: emcc options "-s NODEJS_CATCH_EXIT=0 -s
// NODEJS_CATCH_REJECTION=0" are required to prevent nodejs from
// printing whole code on exceptions
vm.runInThisContext(data, { displayErrors: false, filename: src });
};
var tryFetchJSON = async function (src) {
var data = await fs.readFile(src);
return JSON.parse(data);
};
} else {
if (ENVIRONMENT_IS_WORKER) {
var tryImportScript = async function (src) {
importScripts(src); // async interface to importScripts from Web Worker
};
} else {
var tryImportScript = globalThis.tryImportScript;
}
var tryFetchJSON = async function (src) {
const response = await fetch(src);
return await response.json();
};
}
var EMCiao = {}; // Emscripten module for CIAOENGINE
var stdout = "";
var stderr = "";
var LLCiao = {};
LLCiao.emciao_initialized = false;
LLCiao.stats = {}; // Statistics
LLCiao.bundle = {}; // Bundle map
LLCiao.depends = []; // Bundle dependencies
LLCiao.root_URL = null; // URL for CIAOROOT (null when not initialized yet)
/* --------------------------------------------------------------------------- */
LLCiao.get_stats = function () { return LLCiao.stats; };
/* --------------------------------------------------------------------------- */
LLCiao.get_ciao_root = function () { return LLCiao.bundle['core'].wksp; };
function mkdir_noerr(path) {
let FS = LLCiao.getFS();
try { FS.mkdir(path); } catch (e) { /* ignore errors */ };
}
// Create directory path
function mkpath(path) {
var spath = path.split('/');
var len = spath.length;
var dir = "";
if (len > 0) {
dir += spath[0];
mkdir_noerr(dir);
for (var i = 1; i < len; i++) {
dir += "/" + spath[i];
mkdir_noerr(dir);
}
}
}
LLCiao.preload_file = function (dir, relpath) {
if (LLCiao.root_URL === null) throw new Error('null root_URL');
var srcurl = LLCiao.root_URL + relpath;
var srcdir = dir + '/' + relpath;
/* Split in dir and base */
var dirsplit = srcdir.split('/');
var base = dirsplit.pop();
var dir = dirsplit.join('/');
/* Create directory path for dir */
mkpath(dir);
/* Create preloaded file */
let FS = LLCiao.getFS();
//FS.createPreloadedFile(dir, base, srcurl, true, false);
FS.createPreloadedFile(dir, base, srcurl, true, true); // rw access
// TODO: only works in web workers, does not work in lpdoc example
// FS.createLazyFile(dir, base, srcurl, true, true); // rw access
};
async function maybeImportData(b_data, key) {
if (b_data.hasOwnProperty(key)) {
await tryImportScript(EMCiao.locateFile(b_data[key], ''));
}
}
LLCiao.preload_bundle = async function (b) {
// this loads *.mods.js
globalThis.__emciao = EMCiao;
const b_data = LLCiao.bundle[b];
if (b_data.hasOwnProperty('preload_files')) {
for (const rel_path of b_data.preload_files) {
LLCiao.preload_file(b_data.wksp, rel_path);
}
}
await Promise.all([maybeImportData(b_data, 'src_data'),
maybeImportData(b_data, 'mods_data')]);
};
LLCiao.collect_wksps = function () {
let wksps = [];
let seen = {};
for (const j of LLCiao.depends) {
var wksp = LLCiao.bundle[j].wksp;
if (!seen[wksp]) {
wksps.unshift(wksp); /* right order? */
seen[wksp] = true;
}
}
return wksps;
};
// Update timestamps of build/cache files
// TODO: needs around 8ms
// TODO: allow frozen workspaces in c_itf, inhibits recompilation!
// TODO: update needed for dynamic use_bundle?
LLCiao.update_timestamps = function (wksps) {
let FS = LLCiao.getFS();
var tsnow = Date.now();
for (const wksp of wksps) {
const dir = wksp + "/build/cache";
for (const f of FS.readdir(dir)) {
if (f.endsWith('.po') || f.endsWith('.itf')) {
const path = dir + "/" + f;
// const timestamp = FS.lookupPath(path).node.timestamp;
FS.utime(path, tsnow, tsnow);
}
}
}
};
// Mount a directory (only for NodeJS)
LLCiao.mount_dir = function (srcdir, dstdir) {
mkpath(dstdir);
var FS = LLCiao.getFS();
FS.mount(FS.filesystems.NODEFS, { root: srcdir }, dstdir);
};
// Pending loads
var pending_load = false;
var pending_load_resolve = undefined;
/* Suspend until all use_bundle has been completed */
LLCiao.wait_no_deps = function () {
return new Promise((resolve, reject) => { // TODO: 'reject' ignored
if (pending_load) {
if (pending_load_resolve !== undefined) { throw new Error('unresolved pending load'); };
pending_load_resolve = (function () { resolve(true); }); // called from monitor_deps when done
} else { /* there were no pending loads */
resolve(true);
}
});
};
function monitor_deps(numdeps) {
if (numdeps == 0) { // no more pending loads, notify if needed
if (pending_load_resolve !== undefined) {
pending_load_resolve();
pending_load_resolve = undefined;
}
pending_load = false; // no pending loads
}
}
LLCiao.use_bundle = async function (bundle) {
if (!ENVIRONMENT_IS_NODE) console.log(`{loading bundle '${bundle}'}`);
// Read and store .bundle.json metadata (see grade_wasm.pl)
const b_data = await tryFetchJSON(LLCiao.root_URL + "build/dist/" + bundle + ".bundle.json");
if (!LLCiao.depends) LLCiao.depends = [];
LLCiao.depends.push(b_data.name);
LLCiao.bundle[b_data.name] = b_data;
// Initiate load if possible
if (LLCiao.emciao_initialized) { /* (preRun has already been called, preload here) */
pending_load = true; // at least one pending load
await LLCiao.preload_bundle(bundle);
}
return true;
}
/* --------------------------------------------------------------------------- */
/* Initialization */
/**
* `CIAOENGINE` is the Emscripten/WASM compiled engine (very low
* level). It is loaded dynamically from `eng` (e.g.,
* `ciaoengwasm`).
*/
LLCiao.eng_load = async function (url, eng) {
LLCiao.root_URL = url;
/* Load CIAOENGINE (generated from emcc) */
if (!ENVIRONMENT_IS_NODE) console.log(`{loading engine '${eng}'}`);
await tryImportScript(LLCiao.root_URL + "build/bin/" + eng + ".js");
return true;
};
LLCiao.init_emciao = function () {
return new Promise((resolve, reject) => { // TODO: 'reject' ignored
LLCiao.init_emciao_(resolve);
});
};
LLCiao.init_emciao_ = function (resolve) {
/* Start the engine with hooks for initialization */
EMCiao['locateFile'] = function (path, prefix) {
// custom dirs
let root_URL = LLCiao.root_URL;
if (root_URL === null) throw new Error('null root_URL');
if (path.endsWith(".mem")) return root_URL + "build/bin/" + path;
if (path.endsWith(".wasm")) return root_URL + "build/bin/" + path;
if (path.endsWith(".src.data")) return root_URL + "build/dist/" + path;
if (path.endsWith(".src.js")) return root_URL + "build/dist/" + path;
if (path.endsWith(".mods.data")) return root_URL + "build/dist/" + path;
if (path.endsWith(".mods.js")) return root_URL + "build/dist/" + path;
// otherwise, use the default, the prefix (JS file's dir) + the path
return prefix + path;
};
// Collect workspaces from dependencies
let wksps = LLCiao.collect_wksps();
// Capture stdout and stderr
EMCiao.print = function (out) {
stdout += out + "\n";
};
EMCiao.printErr = function (err) {
stderr += err + "\n";
};
// continue using code after run()
EMCiao.noExitRuntime = true;
// preRun (setup environment before run())
EMCiao.preRun = [];
EMCiao.preRun.push(function () {
// ('pre-js.js' intializes EMCiao['FS'] and EMCiao['getENV'])
// Monitor run dependencies
EMCiao['monitorRunDependencies'] = monitor_deps;
// Preload bundle files (if needed)
for (const b of LLCiao.depends) {
// annotate asynchronous preload_bundle termination as run dependencies for emscripten
var dep = "preload bundle " + b;
EMCiao['addRunDependency'](dep);
LLCiao.preload_bundle(b).then((result) => {
EMCiao['removeRunDependency'](dep);
});
}
// Preload bootfile so that it is accessible from the FS
LLCiao.preload_file(LLCiao.get_ciao_root(), "build/bin/" + LLCiao.bootfile); /* TODO: customize */
/* Set CIAOPATH from bundles */
(EMCiao.getENV())['CIAOPATH'] = wksps.join(":");
});
//
EMCiao.onRuntimeInitialized = function () {
LLCiao.update_timestamps(wksps);
//
var bootfile = LLCiao.get_ciao_root() + "/build/bin/" + LLCiao.bootfile;
let bootfile_ptr = EMCiao.stringToNewUTF8(bootfile);
EMCiao._ciaowasm_init(bootfile_ptr);
EMCiao._free(bootfile_ptr);
/* Boot the engine, which will execute main/0 and exit with a live runtime */
EMCiao._ciaowasm_boot();
/* (Continue) */
LLCiao.emciao_initialized = true;
resolve(null);
};
/* Begin execution of Emscripten (wasm) compiled engine */
if (!ENVIRONMENT_IS_NODE) console.log(`{booting engine}`);
globalThis.CIAOENGINE.run(EMCiao);
};
/* --------------------------------------------------------------------------- */
LLCiao.bootfile = 'ciaowasm';
/* Begin new query. See ciaowasm:query_one_fs/0 */
LLCiao.query_one_begin = function (goal) {
var query;
query = 'q((' + goal + ')).';
let FS = LLCiao.getFS();
FS.writeFile('/.q-i', query, { encoding: 'utf8' });
let q_ptr = EMCiao.stringToNewUTF8("ciaowasm:query_one_fs");
EMCiao._ciaowasm_query_begin(q_ptr);
EMCiao._free(q_ptr);
return query_result();
};
/* Obtain next solution */
LLCiao.query_one_next = function () {
EMCiao._ciaowasm_query_next();
return query_result();
};
/* Resume query */
LLCiao.query_one_resume = function () {
EMCiao._ciaowasm_query_resume();
return query_result();
};
/* End query */
LLCiao.query_end = function () {
EMCiao._ciaowasm_query_end();
}
function recv_jscmd() {
let str;
let cmd;
let FS = LLCiao.getFS();
try {
str = FS.readFile('/.j-c', { encoding: 'utf8' });
FS.unlink('/.j-c');
} catch (err) {
return null;
}
return JSON.parse(str);
}
function query_result() {
let is_ok = EMCiao._ciaowasm_query_ok();
if (is_ok) {
let is_suspended = EMCiao._ciaowasm_query_suspended();
if (is_suspended) {
let cmd = recv_jscmd(); // null if none
// special case for debugger
if (cmd !== null && cmd.cmd === 'acall' && cmd.name === '$dbgtrace_get_line') {
return { cont: 'dbgtrace', arg: null };
}
return { cont: 'suspended', arg: cmd };
} else {
let FS = LLCiao.getFS();
let cont = FS.readFile('/.q-c', { encoding: 'utf8' });
let arg = FS.readFile('/.q-a', { encoding: 'utf8' });
return { cont: cont, arg: arg };
}
} else {
return { cont: 'failed', arg: '' };
}
}
/* --------------------------------------------------------------------------- */
/* Get Emscripten FS */
LLCiao.getFS = function () { return EMCiao['FS']; };
LLCiao.read_stdout = function () {
var out = stdout.replaceAll("\n$$$fake_flush$$$\n", ""); /* TODO: see ciaowasm.pl for details about this horrible workaround */
stdout = "";
return out;
};
LLCiao.read_stderr = function () {
var err = stderr.replaceAll("\n$$$fake_flush$$$\n", ""); /* TODO: see ciaowasm.pl for details about this horrible workaround */
stderr = "";
return err;
};
LLCiao.writeFile = function (a, b) {
try {
return LLCiao.getFS().writeFile(a, b, { encoding: 'utf8' });
} catch (err) {
return null;
}
};
LLCiao.readFile = function (a) {
try {
return LLCiao.getFS().readFile(a, { encoding: 'utf8' });
} catch (err) {
return null;
}
};
/* --------------------------------------------------------------------------- */
LLCiao.send_jsret = function (x) {
if (x === undefined) return true;
try {
let str = JSON.stringify(x);
return LLCiao.getFS().writeFile('/.j-o', str, { encoding: 'utf8' });
} catch (err) {
return null;
}
}
// sync
var f = {};
f['get_stats'] = true;
f['get_ciao_root'] = true;
f['writeFile'] = true;
f['readFile'] = true;
f['read_stdout'] = true;
f['read_stderr'] = true;
f['send_jsret'] = true;
f['query_one_begin'] = true;
f['query_one_resume'] = true;
f['query_one_next'] = true;
f['query_end'] = true;
// async
var af = {};
af['eng_load'] = true;
af['init_emciao'] = true;
af['wait_no_deps'] = true;
af['use_bundle'] = true;
LLCiao.run_cmd = async function (cmd, args) {
if (af.hasOwnProperty(cmd)) { // async
return LLCiao[cmd].apply(undefined, args);
} else if (f.hasOwnProperty(cmd)) { // sync
return new Promise((resolve, reject) => { // TODO: 'reject' ignored
resolve(LLCiao[cmd].apply(undefined, args));
});
} else {
throw new Error("unknown cmd " + cmd);
}
};
/* --------------------------------------------------------------------------- */
return LLCiao;
}
// TODO: make Web Worker optional
function ciao_worker_fun() {
var __ciao = new_LLCiao();
this.onmessage = function (event) {
var resolve = (function (x) { postMessage({ id: event.data.id, ret: x }); });
__ciao.run_cmd(event.data.cmd, event.data.args).then(resolve);
};
/* connect with browser console */
console = self.console; /* TODO: keep only in debug? */
}
// Note: use data-URI as an alternative "data:application/x-javascript;base64,"+...
function ciao_worker_url() {
let code = new_LLCiao.toString();
let worker_code = ciao_worker_fun.toString();
worker_code = worker_code.slice(worker_code.indexOf("{") + 1, worker_code.lastIndexOf("}"));
code += "\n" + worker_code; // get body (vars need to be global)
const blob = new Blob([code], { type: 'text/javascript' });
return URL.createObjectURL(blob);
}
/* =========================================================================== */
//var use_webworker = false;
var use_webworker = true;
/**
* `CiaoWorker`: High level interface, schedules JS<->Prolog
* interaction, has (optional) Web Worker support (CiaoPromiseProxy).
*/
class CiaoWorker {
constructor(root_URL) {
this.eng_loaded = false;
this.eng_booted = false;
this.root_URL = root_URL;
//
if (use_webworker) {
var listeners = [];
this.listeners = listeners;
this.w = new Worker(ciao_worker_url());
this.w.onmessage = function (event) {
listeners[event.data.id].setValue(event.data.ret);
delete listeners[event.data.id]; /* undefine this array element */
};
} else {
this.llciao = new_LLCiao();
}
}
/**
* Perform a request to `this.w` (the worker running the Ciao
* engine). It creates and returns a CiaoPromiseProxy, whose value
* is set on completion.
*/
#async_(cmd, args) {
if (use_webworker) {
var proxy = new CiaoPromiseProxy();
/* set in listeners (get next free id or push) */
var id = 0;
for (var id = 0; id < this.listeners.length; id++) {
if (this.listeners[id] === undefined) break;
}
if (id === this.listeners.length) {
this.listeners.push(proxy);
} else {
this.listeners[id] = proxy;
}
this.w.postMessage({
id: id, /* id of message in listener array */
cmd: cmd,
args: args
});
return proxy;
} else {
return this.llciao.run_cmd(cmd, args);
}
}
/**
* Ensure that Ciao is initialized. Level can be:
* 1: engine loaded
* 2: engine loaded and booted
*/
async ensure_init(level) {
if (this.pending_level >= level) return;
this.pending_level = level;
if (level >= 1 && !this.eng_loaded) {
this.eng_loaded = true;
var url = this.root_URL;
// (hack to get absolute url)
if (use_webworker) {
let a = document.createElement('a');
a.href = url;
url = a.href; // TODO: needs to be absolute due to importScripts from Web Worker
}
await this.#async_('eng_load', [url, "ciaoengwasm"]);
}
if (level >= 2 && !this.eng_booted) {
this.eng_booted = true;
await this.#async_('init_emciao', []);
}
}
/**
* Use the bundle passed as a parameter, loading the engine if needed.
* @param {string} name - Name of the bundle.
*/
async use_bundle(name) {
await this.ensure_init(1);
return await this.#async_('use_bundle', [name]);
};
async wait_no_deps() {
await this.ensure_init(1);
return await this.#async_('wait_no_deps', []);
}
/**
* Get the stats of the latest call.
*/
async get_stats() {
await this.ensure_init(1);
return await this.#async_('get_stats', []);
};
/**
* Get the Ciao root path.
*/
async get_ciao_root() {
await this.ensure_init(1);
return await this.#async_('get_ciao_root', []);
};
/**
* Begins the query passed as parameter and obtains one solution. The
* decision tree stays awake and waits for user's input.
* @param {string} goal - Query to be launched.
*/
async query_one_begin(goal) {
console.log('Query sent: ', goal);
await this.ensure_init(2);
let q_out = await this.#async_('query_one_begin', [goal]);
return await this.#query_complete(q_out);
}
/**
* Obtain the next solution for the query previously launched.
*/
async query_one_next() {
let q_out = await this.#async_('query_one_next', []);
return await this.#query_complete(q_out);
}
/** Resume query until completed (not suspended). Process jscmd if needed
*/
async #query_complete(q_out) {
while (q_out.cont === 'suspended') {
let cmd = q_out.arg;
if (cmd !== null) { /* run cmd if needed */
let ret = await jscmd_run(this, cmd);
await this.#async_('send_jsret', [ret]);
}
q_out = await this.#async_('query_one_resume', []);
}
return q_out;
}
/** Resume a query suspended in dbgtrace
*/
async query_resume_dbgtrace(dbgcmd) {
/* pre: previous q_out.cont === 'dbgtrace' */
await this.#async_('send_jsret', [dbgcmd]);
let q_out = await this.#async_('query_one_resume', []);
return await this.#query_complete(q_out);
}
/**
* End the query.
*/
async query_end() { return await this.#async_('query_end', []); }
/**
* Read file specified in path parameter.
* @param {string} path - Path of the file to read.
*/
async readFile(path) {
await this.ensure_init(2);
return await this.#async_('readFile', [path]);
};
/**
* Write a string in a file.
* @param {string} path - Path of the file to write.
* @param {string} contents - String of the contents to write in the file.
*/
async writeFile(path, contents) {
await this.ensure_init(2);
return await this.#async_('writeFile', [path, contents]);
};
/**
* Capture the stdout of the most recent query.
*/
async read_stdout() { return await this.#async_('read_stdout', []); };
/**
* Capture the stderr of the most recent query.
*/
async read_stderr() { return await this.#async_('read_stderr', []); };
/**
* Terminate the worker `w`
*/
terminate() {
if (use_webworker) {
return this.w.terminate();
} else {
throw new Error('terminate needs Web Worker');
}
}
}
/* =========================================================================== */
/* JS reference table */
// This table is used to assign unique indices to JS objects passed to
// a CiaoWorker.
//
// TODO:
// - indices are reused but the heap table is not compacted
// - use 'externref' [1] to simplify gluecode? (does it work from
// workers?)
// - foreign GC needed to do jsref_free automatically
//
// [1] https://github.com/WebAssembly/reference-types/blob/master/proposals/reference-types/Overview.md
/* The index->obj table (global and shared) */
var jsref_heap = []; // index->obj table
var jsref_freeidx = []; // free indices in the heap table (holes)
// Allocate a slot for `obj` and return its index
function jsref_alloc(obj) {
if (jsref_freeidx.length != 0) { // reuse a free idx
let idx = jsref_freeidx.pop();
jsref_heap[idx] = obj;
return idx;
} else { // alloc a new idx
return jsref_heap.push(obj) - 1; // index
}
}
// Free the slot `idx`
function jsref_free(idx) {
jsref_freeidx.push(idx); // add to the free pool
jsref_heap[idx] = null; // remove
}
function jsref_obj(idx) {
return jsref_heap[idx];
}
/* =========================================================================== */
/* JS command buffer */
// JS command buffer is a JSON encoded list of commands.
//
// Instructions are objects such that:
//
// i.cmd=="def": assigns code i.code for function i.name at jscmd_f
// i.cmd=="call": call function i.name with arguments i.args
// i.cmd=="acall": await call function i.name with arguments i.args
// TODO: simplify this part using reference types
// https://github.com/WebAssembly/reference-types/blob/master/proposals/reference-types/Overview.md
/* JS function table (global and shared) */
var jscmd_f = {};
/* Execute jscmd */
async function jscmd_run(w, jscmd) {
let ret;
switch (jscmd.cmd) {
case 'def': /* define function */
jscmd_f[jscmd.name] = Function('"use strict";return (' + jscmd.code + ')')();
return null; /* TODO: provide id? */
case 'call':
jscmd.args.unshift(w); // worker as 1st argument
return jscmd_f[jscmd.name].apply(undefined, jscmd.args);
case 'acall':
jscmd.args.unshift(w); // worker as 1st argument
return await jscmd_f[jscmd.name].apply(undefined, jscmd.args);
default:
console.log('error: unknown jscmd: ' + jscmd);
}
}
/* =========================================================================== */
/**
* `ToplevelProc`: toplevel (REPL) process
*/
// TODO: move more code to Prolog (this reimplements part of
// toplevel.pl due to problems with blocking IO in
// WASM/JS). Suspendable IO on wasm level would allow sharing more
// code here.
const toplevelCfg_defaults = {
// Show statistics (and some logging info) per query (in the JS console)
statistics: true,
// Query timeout (seconds) (0 to disable)
query_timeout: 200,
// Special queries // TODO: missing arity
special_query: {
"use_module": { read_code: true, mark_errs: true },
"run_tests_in_module": {
read_code: true,
mark_errs: true,
depends: ['ciaodbg'],
on_init: ["use_module(library(unittest))"]
},
// "clean_mods": {
// on_init: ['use_module(ciaobld(ciaoc_batch_call), [clean_mods/1])']
// },
"doc_cmd": {
read_code: true,
mark_errs: true,
depends: ['lpdoc'],
on_init: ['use_module(lpdoc(docmaker))']
},
//
"set_menu_flag": { // arity {3}
read_code: false,
mark_errs: false,
depends: ['ciaopp', 'typeslib'],
on_init: ["use_module(ciaopp(ciaopp))"]
},
"module": {
read_code: true,
mark_errs: true,
depends: ['ciaopp', 'typeslib'],
on_init: ["use_module(ciaopp(ciaopp))"]
},
"auto_analyze": { // arity {1,2}
read_code: true,
mark_errs: true,
depends: ['ciaopp', 'typeslib'],
on_init: ["use_module(ciaopp(ciaopp))"]
},
"auto_optimize": { // arity {1,2}
read_code: true,
mark_errs: true,
depends: ['ciaopp', 'typeslib'],
on_init: ["use_module(ciaopp(ciaopp))"]
},
"auto_check_assert": { // arity {1,2}
read_code: true,
mark_errs: true,
depends: ['ciaopp', 'typeslib'],
on_init: ["use_module(ciaopp(ciaopp))"]
},
"filter_analyze": { // arity {1,2}
read_code: true,
mark_errs: true,
depends: ['ciaopp', 'typeslib', 'exfilter'],
on_init: ["use_module(exfilter(exfilter))"]
},
"filter_analyze_exercise_mode": { // arity {1,2}
read_code: true,
mark_errs: true,
depends: ['ciaopp', 'typeslib', 'exfilter'],
on_init: ["use_module(exfilter(exfilter))"]
}
},
// Default bundles and initialization queries
init_bundles: [
'ciaowasm', // (for foreign-js)
'core',
'builder'
],
init_queries: [
'use_module(engine(internals), [reload_bundleregs/0])',
'use_module(library(classic/classic_predicates))'
],
// Query for loading code
// toplevelCfg.custom_load_query = ((m) => ...);
// Transformation for user queries
// toplevelCfg.custom_run_query = ((q) => ... );
// Post-print code (before update_inner_layout())
// toplevelCfg.custom_postprint_sol = (async (pg) => ...);
};
if (typeof toplevelCfg === 'undefined') { var toplevelCfg = {}; }
toplevelCfg = Object.assign({ ...toplevelCfg_defaults }, toplevelCfg);
var QueryState = {
READY: 0, // ready for a query
RUNNING: 1, // query running
VALIDATING: 2, // prompt waiting for validating solution
DBGTRACE: 3 // prompt in a debugging state
};
class ToplevelProc {
constructor(root_URL) {
this.root_URL = root_URL;
this.w = null;
this.comint = null; // associated comint ('null' to ignore)
this.muted = false; // temporarily ignore comint // TODO: change comint instead?
this.state = null;
this.q_opts = {}; // running/validating query opts
this.timer = undefined;
}
/* ---------------------------------------------------------------------- */
/* Is the worker started? */
is_started() {
return (this.w !== null);
}
/* Start the worker (and load defaults, show prompt, load program) */
async start() {
if (!this.muted) this.comint.set_log('Loading bundles and booting');
this.w = new CiaoWorker(this.root_URL); // create a Ciao worker
await this.load_ciao_defaults(); // TODO: check result?
if (!this.muted) this.comint.set_log('');
//
this.update_state(QueryState.READY);
this.q_opts = {};
if (!this.muted) this.comint.display_status_new_prompt('silent');
await this.comint.pg.on_cproc_start();
}
/* Restart the worker */
async restart() {
this.shutdown();
this.update_state(QueryState.READY);
this.q_opts = {};
const pmuted = this.set_muted(true); // TODO: mute on restart, make it optional?
await this.start();
this.muted = pmuted;
}
/* Terminate worker */
shutdown() {
if (!this.is_started()) return;
this.w.terminate();
this.w = null;
}
/* Make sure that worker is started */
async ensure_started(comint) {
if (this.is_started()) return;
this.comint = comint; // attach to this comint
await this.start();
}
// set muted and return previous value
set_muted(v) {
const prev = this.muted;
this.muted = v;
return prev;
}
/* ---------------------------------------------------------------------- */
// Load the default bundles and modules
async load_ciao_defaults() {
// Use default bundles and show boot info (this starts the engine)
for (const b of toplevelCfg.init_bundles) {
await this.w.use_bundle(b);
}
// // TODO: parallel load does not speed up load
// await Promise.all(toplevelCfg.init_bundles.map(async (b) => { await this.w.use_bundle(b); }));
// Boot and show system info
{
this.w.curr_cproc = this; // TODO: simplify
await this.w.query_one_begin("'$:'('internals:$bootversion')"); // TODO: check errors!
let out = await this.w.read_stdout();
let err = await this.w.read_stderr();
await this.w.query_end();
this.comint.pg.show_version(out + err);
}
// Initialization queries on the toplevel
for (const q of toplevelCfg.init_queries) {
await this.muted_query_dumpout(q);
}
return true;
}
// Add (and execute) a new initialization query
async push_on_init(qs) {
const started = this.is_started();
for (const q of qs) {
if (!toplevelCfg.init_queries.includes(q)) {
toplevelCfg.init_queries.push(q);
if (started) await this.muted_query_dumpout(q);
}
}
}
// Add (and load) a new bundle dependency
async push_depends(bs) {
let updated = false;
const started = this.is_started();
for (const b of bs) {
if (!toplevelCfg.init_bundles.includes(b)) {
toplevelCfg.init_bundles.push(b);
if (started) await this.w.use_bundle(b); // load if already started
updated = true;
}
}
// if (started && updated) await this.restart(); // TODO: not needed now!
if (started && updated) {
await this.w.wait_no_deps(); /* wait until there are no pending loading deps */
await this.muted_query_dumpout('reload_bundleregs');
}
}
/* ---------------------------------------------------------------------- */
// Do a query, only one solution, dump stdout/stderr,
async muted_query_dumpout(q) {
if (toplevelCfg.statistics) console.log(`{implicit: ${q}}`);
this.w.curr_cproc = this; // TODO: simplify
await this.w.query_one_begin(q);
await this.dumpout(); // TODO: check errors!
await this.w.query_end();
}
// Dump last query stdout/stderr (ignore or show in console)
async dumpout() {
let out = await this.w.read_stdout();
let err = await this.w.read_stderr();
if (toplevelCfg.statistics) console.log(out + err);
}
/* ---------------------------------------------------------------------- */
set_query_timeout() {
if (toplevelCfg.query_timeout == 0) return; /* no timeout */
this.timer = setTimeout((async () => {
if (!this.muted) this.comint.print_msg('\n{ABORTED: Time limit exceeded.}\n');
await this.restart();
if (!this.muted) this.comint.display_status_new_prompt('silent'); /* amend prompt if needed */
}), toplevelCfg.query_timeout * 1000); /* set a timeout */
}
cancel_query_timeout() {
if (this.timer !== undefined) {
clearTimeout(this.timer);
this.timer = undefined;
}
}
/* ---------------------------------------------------------------------- */
// TODO: only works for single goal queries; this needs to be done
// at Prolog level with Prolog->JS communication
async trans_query(query) {
let treat_outerr = null;
// apply transformation if needed
if (toplevelCfg.custom_run_query !== undefined) {
query = toplevelCfg.custom_run_query(query);
}
// perform special query actions
let f_match = query.match(/([a-z][_a-zA-Z0-9]*)(?:\(|$)/); // functor name // TODO: arity is missing, do from Prolog
if (f_match != null && f_match.length == 2) {
const special_query = toplevelCfg.special_query[f_match[1]];
if (special_query !== undefined) {
if (special_query.depends !== undefined) { // new (bundle) dependencies
await this.push_depends(special_query.depends);
}
if (special_query.on_init !== undefined) { // new initialization queries
await this.push_on_init(special_query.on_init);
}
if (special_query.read_code === true) { // the query may read the code, upload to worker
await this.comint.pg.upload_code_to_worker();
}
if (special_query.action !== undefined) { // replace auto_action
this.comint.pg.set_auto_action(special_query.action);
}
if (special_query.mark_errs === true) { // the query may show messages on the code, treat outerr
treat_outerr = async (out, err) => {
this.comint.pg.mark_errs(out, err); // TODO: missing matching file?
};
}
}
}
//
return { q: query, treat_outerr: treat_outerr };
}
/* ---------------------------------------------------------------------- */
update_state(state) {
this.state = state;
this.comint.update_inner_layout(); // (query state changed)
}
/* Alert if we are still running */
check_not_running() {
if (this.state === QueryState.RUNNING) {
alert('Already running a query');
return false;
}
return true;
}
/* Alert if we are locked validating/debugging in another comint */
check_not_locked(comint) {
if (this.comint !== comint) {
alert('Already validating/debugging a query in other comint');
return false;
}
return true;
}
/* Waiting for a line (validating query or debugging state) */
is_waiting_for_line() {
return (this.state === QueryState.VALIDATING || this.state === QueryState.DBGTRACE);
}
/**
* Execute a new query on the toplevel (Pre: this.state === QueryState.READY)
* @param {string} query - Query to be executed.
*/
async run_query(comint, query, opts) {
if (this.state !== QueryState.READY) {
console.log('bug: already running or validating a query'); // TODO: treat_enter too fast?
return; // TODO: query is lost!
}
this.comint = comint; // attach to this comint
// ----
let tr = await this.trans_query(query);
query = tr.q;
// TODO: almost duplicated
this.update_state(QueryState.RUNNING);
this.q_opts = opts;
// begin a new query
if (!this.muted && opts.msg !== undefined) this.comint.set_log(opts.msg);
this.set_query_timeout();
this.w.curr_cproc = this; // TODO: simplify
let q_out = await this.w.query_one_begin(query);
this.cancel_query_timeout();
if (!this.muted && opts.msg !== undefined) this.comint.set_log('');
//
await this.treat_sol(q_out, tr.treat_outerr); // treat query result
}
/**
* Send a line command (`action`). This is used to validate
* solutions (if this.state === QueryState.VALIDATING) or send a
* debugger command (if this.state === QueryState.DBGTRACE)
*/
async send_line(comint, action) {
this.comint = comint; // attach to this comint
if (this.state === QueryState.VALIDATING) {
if (action === '') { // accept solution, end query
this.w.curr_cproc = this; // TODO: simplify
await this.w.query_end();
this.update_state(QueryState.READY);
this.q_opts = {};
/*if (!this.muted)*/ this.comint.display_status_new_prompt('yes');
} else { // ask for the next solution
// TODO: almost duplicated
this.update_state(QueryState.RUNNING);
// next query solution
this.set_query_timeout();
this.w.curr_cproc = this; // TODO: simplify
let q_out = await this.w.query_one_next();
this.cancel_query_timeout();
//
await this.treat_sol(q_out, null); // treat query result
}
} else if (this.state === QueryState.DBGTRACE) {
this.update_state(QueryState.RUNNING);
// continue execution
this.set_query_timeout();
this.w.curr_cproc = this; // TODO: simplify
let q_out = await this.w.query_resume_dbgtrace(action);
this.cancel_query_timeout();
//
await this.treat_sol(q_out, null); // treat query result
} else {
console.log('bug: not in a validating/debugging solution state'); // TODO: treat_enter too fast?
}
}
/**
* If current query has a solution, print it and asks for more if
* there are more solutions available. If it has no solutions, finish
* the query.
* @param {Object} q_out - Object containing an array with the solution of a query.
*/
async treat_sol(q_out, treat_outerr) {
let out = await this.w.read_stdout();
let err = await this.w.read_stderr();
/* print stdout and stderr output */
if (!this.muted) this.comint.print_out(out + err);
/* special case for debugger */
if (q_out.cont === 'dbgtrace') { // TODO: better way?
this.update_state(QueryState.DBGTRACE);
return;
}
/* print solution */
let solstatus;
if (q_out.cont === 'failed') { // no more solutions
solstatus = 'no';
} else {
// TODO: fixme, see toplevel.pl
/* Pretty print query results (solutions or errors) */
// (see ciaowasm.pl for possible cases)
if (q_out.cont === 'success') {
if (this.comint.with_prompt) { /* only if itr, otherwise ignore bindings and cut */
let prettysol = q_out.arg;
if (prettysol === '') { // (no bindings, cut)
solstatus = 'yes';
} else {
solstatus = '?'; // TODO: not always! detect when there are no choicepoints
if (!this.muted) this.comint.print_sol(prettysol); // print solution
}
}
} else if (q_out.cont === 'exception') { // TODO: horrible hack
let ball = q_out.arg;
if (!this.muted) this.comint.print_msg(`{ERROR: No handle found for thrown exception ${ball}}\n`);
solstatus = 'aborted';
} else if (q_out.cont === 'malformed') {
solstatus = 'silent';
if (!this.muted) this.comint.print_msg('{SYNTAX ERROR: Malformed query}\n');
} else {
solstatus = 'silent';
console.log(`bug: unrecognized query result cont: ${q_out.cont} ${q_out.arg}`);
}
}
const no_treat_outerr = this.q_opts.no_treat_outerr;
if (solstatus === '?') {
this.update_state(QueryState.VALIDATING);
if (!this.muted) this.comint.print_promptval();
} else {
await this.w.query_end();
this.update_state(QueryState.READY);
this.q_opts = {};
if (!this.muted) this.comint.display_status_new_prompt(solstatus);
}
if (treat_outerr !== null) {
if (no_treat_outerr !== true) {
await treat_outerr(out, err);
}
} else if (!this.muted) {
if (toplevelCfg.custom_postprint_sol !== undefined) {
// custom postprint if needed
await toplevelCfg.custom_postprint_sol(this.comint.pg);
}
}
}
/* ---------------------------------------------------------------------- */
/**
* Abort the execution of the current query (if inside of `run_query(query)`).
*/
async abort() {
if (this.state === QueryState.RUNNING) {
this.cancel_query_timeout();
// print message
// if (!this.muted) this.comint.print_msg('\n{ Execution aborted }\n'); // (same text as Ciao)
if (!this.muted) this.comint.print_msg('\n{ Execution aborted (resetting dynamic database) }\n'); // TODO: remove note when preserving the database is working
// restart worker and update variables
// TODO: just abort query, not the worker
await this.restart();
if (!this.muted) this.comint.display_status_new_prompt('silent'); /* amend prompt if needed */
}
}
}
/* =========================================================================== */
/**
* Toplevel for execution under NodeJS
*/
// TODO: with_prompt is not configurable yet
// TODO: exit code is not returned
var ENVIRONMENT_IS_NODE = typeof process == 'object' && typeof process.versions == 'object' && typeof process.versions.node == 'string';
if (ENVIRONMENT_IS_NODE) {
const path = require('path');
const process = require('process');
const printout = function (x) { process.stdout.write(x); };
// Locate site path relative to the script dir
var site_path = path.join(__dirname, '..');
// Simpler PGCell for RLComint
class RLPGCell {
constructor(cproc) {
this.cproc = cproc;
}
async setup(rl) {
// readline-based comint
this.comint = new RLComint(rl, this, {});
// start
await this.cproc.ensure_started(this.comint);
}
async on_cproc_start() {
}
show_version(str) {
this.cproc.comint.display(str);
}
async upload_code_to_worker() {
}
mark_errs(out, err) {
}
/* mark source debug info */
mark_srcdbg_info(info) {
this.comint.display(` In ${info.src} (${info.ln0}-${info.ln1}) ${info.pred}-${info.num}\n`);
}
}
class RLComint {
constructor(rl, pg, opts) {
this.prompt = '?- ';
this.promptval = ' ? ';
this.with_prompt = (opts.noprompt === true ? false : true); // interactive
this.pg = pg; // associated pgcell
this.rl = rl; // readline
this.next_prompt = ''; // prompt to be displayed (may contain solution)
}
update_inner_layout() { }
display(text) { process.stdout.write(text); }
print_out(str) { this.display(str); }
print_sol(str) { this.next_prompt = "\n" + str; }
print_msg(str) { this.display(str); }
print_prompt() {
if (!this.with_prompt) return; /* (skip in non-interactive) */
this.next_prompt = this.prompt;
}
print_promptval() {
if (!this.with_prompt) return; /* (skip in non-interactive) */
this.next_prompt += this.promptval; // (assume that print_sol has been called)
}
display_status(str) { // toplevel:display_status/1
if (!this.with_prompt) return; /* (skip in non-interactive) */
if (str === 'silent') return; /* skip this status */
this.display('\n');
this.display(str);
this.display('\n');
}
display_status_new_prompt(str) { /* show status and a new prompt */
this.display_status(str);
this.print_prompt();
}
set_log(text) { }
// TODO: refactor with ciao_playground.js
async #treat_enter_(text) {
const cproc = this.pg.cproc;
if (cproc.is_waiting_for_line()) {
if (!cproc.check_not_locked(this)) return; // TODO: not possible?
await cproc.send_line(this, text);
} else {
// Perform query
if (text === '') {
this.print_prompt(); // show prompt again
} else {
if (text.slice(-1) !== '.') { // query is malformed
// TODO: accept multiline inputs?
this.display(`\
{SYNTAX ERROR: Malformed query. It must end with a period.}
`);
this.display_status_new_prompt('silent');
} else {
let q = text.slice(0, -1);
await cproc.run_query(this, q, {});
}
}
}
}
/* Interaction loop */
async loop() {
const cproc = this.pg.cproc;
let q_prompt = this.prompt;
while (true) {
let text;
try {
text = await this.rl.question(q_prompt);
} catch (err) {
console.log('Err: ' + Err);
process.exit(1);
}
// TODO: do not capture 'halt.' here, set status from Prolog (so that exit code is properly notified)
if (text.slice(-1) === '.' && text.slice(0, -1) === "halt") {
break;
}
await this.#treat_enter_(text);
q_prompt = this.next_prompt;
this.next_prompt = "";
}
}
}
toplevelCfg.statistics = false;
use_webworker = false;
(async () => {
// Start a new toplevel and connect to a RLPGCell for interaction
const cproc = new ToplevelProc(site_path + '/ciao/');
var pg = new RLPGCell(cproc);
await pg.setup(rl);
// Mount current directory as /local
// TODO: customize
var currdir = process.cwd();
var dstdir = '/local';
cproc.w.llciao.mount_dir(currdir, dstdir);
// Change directory
// FS.chdir(dstdir); // TODO: FS.chdir does not seem to work, change from Prolog
await cproc.muted_query_dumpout(`working_directory(_,'${dstdir}')`);
});
}
exports.CiaoWorker = CiaoWorker;