UNPKG

@ciao-lang/ts-ciao-interface

Version:

Simple Ciao interface for node.

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