/* Part of SWI-Prolog Author: Jan Wielemaker and Raivo Laanemets E-mail: J.Wielemaker@vu.nl WWW: http://www.swi-prolog.org Copyright (c) 2022, SWI-Prolog Solutions b.v. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Notes: - Emscripten embedding does not yet support private methods using #, so we use __ as prefix instead. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ const class_var = (class PrologVar { constructor(id) { this.$t = "v"; if ( id !== undefined ) this.v = id; } }); const class_string = (class PrologString { constructor(string) { this.$t = "s"; this.v = string; } toString() { return this.v; } toJSON() { return this.v; } }); const class_rational = (class PrologRational { constructor(n, d) { this.$t = "r"; this.n = n; this.d = d; } toNumber() { return Number(this.d)/Number(this.n); } toString() { return this.d + "r" + this.n; } toJSON() { return this.toString(); } }); const class_compound = (class PrologCompound { constructor(name, args) { this.$t = "t"; this.functor = name; this[name] = args; } arguments() { return this[this.functor]; } arg(n) { return this.arguments[n]; } arity() { return this.arguments.length; } toJSON() { const obj = {$t:"t"} obj[this.functor] = this.arguments(); return obj; } }); const class_list = (class PrologList { constructor(array, tail) { this.$t = "l"; this.v = array; if ( tail !== undefined ) this.t = tail; } }); const class_blob = (class PrologBlob { constructor() { this.$t = "b"; } }); const class_abortable_promise = (class AbortablePromise extends Promise { constructor(executer) { super(executer); this.executer = executer; } abort() { if ( this.executer.abort ) return this.executer.abort(); else console.log("Cannot abort promise"); return false; } }); /******************************* * CLASS Prolog * *******************************/ /** * Class Prolog provides a high level interface to interact with Prolog. * * @param {Object} module Emscripten module in which Prolog was created * @param {Array} [args] are the Prolog initialization arguments, * normally the command line flags. */ class Prolog { constructor(module, args) { this.module = module; this.args = args; this.lastyieldat = 0; this.functor_arg_names_ = {}; this.objects = {}; // id --> Object this.object_ids = new WeakMap(); // objec --> id this.next_object_id = 0; this.open_queries = []; // Stack with open queries this.__set_foreign_constants(); this.__bind_foreign_functions(); this.__export_classes(); this.__initialize(); } __initialize() { let argv0 = this.args || []; argv0.unshift("swipl"); let argv = argv0.map(function(arg) { return this.module.allocate( this.module.intArrayFromString(arg), 'i8', this.module.ALLOC_NORMAL); }, this); var ptr = _malloc(argv.length * 4); argv.forEach(function(arg, i) { this.module.setValue(ptr + i * 4, arg, '*'); }, this); if (!this.bindings.PL_initialise(argv.length, ptr)) { throw new Error('SWI-Prolog initialisation failed.'); } this.MODULE_user = this.new_module("user"); this.call("set_prolog_flag(color_term, false)."); this.call("set_prolog_flag(debug_on_error, false)"); this.call("use_module(library(wasm))"); } __export_classes() { this.Var = class_var; this.String = class_string; this.Rational = class_rational; this.Compound = class_compound; this.List = class_list; this.Blob = class_blob; this.Promise = class_abortable_promise; } __set_foreign_constants() { this.PL_VARIABLE = (1); this.PL_ATOM = (2); this.PL_INTEGER = (3); this.PL_RATIONAL = (4); this.PL_FLOAT = (5); this.PL_STRING = (6); this.PL_TERM = (7); this.PL_NIL = (8); this.PL_BLOB = (9); this.PL_LIST_PAIR = (10); this.PL_FUNCTOR = (11); this.PL_LIST = (12); this.PL_CHARS = (13); this.PL_POINTER = (14); this.PL_CODE_LIST = (15); this.PL_CHAR_LIST = (16); this.PL_BOOL = (17); this.PL_FUNCTOR_CHARS = (18); this._PL_PREDICATE_INDICATOR = (19); this.PL_SHORT = (20); this.PL_INT = (21); this.PL_LONG = (22); this.PL_DOUBLE = (23); this.PL_NCHARS = (24); this.PL_UTF8_CHARS = (25); this.PL_UTF8_STRING = (26); this.PL_INT64 = (27); this.PL_NUTF8_CHARS = (28); this.PL_NUTF8_CODES = (29); this.PL_NUTF8_STRING = (30); this.PL_NWCHARS = (31); this.PL_NWCODES = (32); this.PL_NWSTRING = (33); this.PL_MBCHARS = (34); this.PL_MBCODES = (35); this.PL_MBSTRING = (36); this.PL_INTPTR = (37); this.PL_CHAR = (38); this.PL_CODE = (39); this.PL_BYTE = (40); this.PL_PARTIAL_LIST = (41); this.PL_CYCLIC_TERM = (42); this.PL_NOT_A_LIST = (43); this.PL_DICT = (44); this.REP_ISO_LATIN_1 = 0x00000000; this.REP_UTF8 = 0x00100000; this.REP_MB = 0x00200000; this.REP_FN = this.REP_UTF8; this.CVT_ATOM = 0x00000001; this.CVT_STRING = 0x00000002; this.CVT_LIST = 0x00000004; this.CVT_INTEGER = 0x00000008; this.CVT_RATIONAL = 0x00000010; this.CVT_FLOAT = 0x00000020; this.CVT_VARIABLE = 0x00000040; this.CVT_NUMBER = (this.CVT_INTEGER|this.CVT_RATIONAL| this.CVT_FLOAT); this.CVT_ATOMIC = (this.CVT_NUMBER|this.CVT_ATOM| this.CVT_STRING); this.CVT_WRITE = 0x00000080; this.CVT_WRITE_CANONICAL = 0x00000100; this.CVT_WRITEQ = 0x00000200; this.CVT_ALL = (this.CVT_ATOMIC|this.CVT_LIST); this.CVT_MASK = 0x00000fff; this.CVT_EXCEPTION = 0x00001000; this.CVT_VARNOFAIL = 0x00002000; this.BUF_DISCARDABLE = 0x00000000; this.BUF_STACK = 0x00010000; this.BUF_MALLOC = 0x00020000; this.BUF_ALLOW_STACK = 0x00040000; this.PL_Q_NORMAL = 0x0002; this.PL_Q_NODEBUG = 0x0004; this.PL_Q_CATCH_EXCEPTION = 0x0008; this.PL_Q_PASS_EXCEPTION = 0x0010; this.PL_Q_ALLOW_YIELD = 0x0020; this.PL_Q_EXT_STATUS = 0x0040; this.PL_S_EXCEPTION = -1; this.PL_S_FALSE = 0; this.PL_S_TRUE = 1; this.PL_S_LAST = 2; this.PL_S_YIELD = 255; this.PL_WRT_QUOTED = 0x0001; this.PL_WRT_NEWLINE = 0x2000; } __bind_foreign_functions() { this.bindings = { _PL_streams: this.module.cwrap( '_PL_streams', 'number', []), PL_functor_arity: this.module.cwrap( 'PL_functor_arity', 'number', ['number']), PL_functor_name: this.module.cwrap( 'PL_functor_name', 'number', ['number']), PL_get_functor: this.module.cwrap( 'PL_get_functor', 'number', ['number', 'number']), PL_get_chars: this.module.cwrap( 'PL_get_chars', 'number', ['number', 'number', 'number']), PL_get_arg: this.module.cwrap( 'PL_get_arg', 'number', ['number', 'number', 'number']), PL_get_int64: this.module.cwrap( 'PL_get_int64', 'number', ['number', 'number']), PL_get_float: this.module.cwrap( 'PL_get_float', 'number', ['number', 'number']), PL_put_chars: this.module.cwrap( 'PL_put_chars', 'number', ['number', 'number', 'number', 'number']), put_bytes: this.module.cwrap( 'PL_put_chars', 'number', ['number', 'number', 'number', 'array']), PL_put_atom: this.module.cwrap( 'PL_put_atom', 'number', ['number']), PL_put_variable: this.module.cwrap( 'PL_put_variable', 'number', ['number']), PL_unify: this.module.cwrap( 'PL_unify', 'number', ['number', 'number']), PL_is_string: this.module.cwrap( 'PL_is_string', 'number', ['number']), PL_is_variable: this.module.cwrap( 'PL_is_variable', 'number', ['number']), PL_term_type: this.module.cwrap( 'PL_term_type', 'number', ['number']), PL_get_list: this.module.cwrap( 'PL_get_list', 'number', ['number', 'number', 'number']), PL_get_nil: this.module.cwrap( 'PL_get_nil', 'number', ['number']), PL_initialise: this.module.cwrap( 'PL_initialise', 'number', ['number', 'number']), PL_new_atom: this.module.cwrap( 'PL_new_atom', 'number', ['string']), PL_register_atom: this.module.cwrap( 'PL_register_atom', null, ['number']), PL_unregister_atom: this.module.cwrap( 'PL_unregister_atom', null, ['number']), PL_new_module: this.module.cwrap( 'PL_new_module', 'number', ['number']), PL_new_functor: this.module.cwrap( 'PL_new_functor', 'number', ['number', 'number']), PL_new_term_ref: this.module.cwrap( 'PL_new_term_ref', 'number', []), PL_new_term_refs: this.module.cwrap( 'PL_new_term_refs', 'number', ['number']), PL_copy_term_ref: this.module.cwrap( 'PL_copy_term_ref', 'number', ['number']), PL_reset_term_refs: this.module.cwrap( 'PL_reset_term_refs', null, ['number']), PL_put_functor: this.module.cwrap( 'PL_put_functor', 'number', ['number', 'number']), PL_put_integer: this.module.cwrap( 'PL_put_integer', 'number', ['number', 'number']), PL_put_float: this.module.cwrap( 'PL_put_float', 'number', ['number', 'number']), PL_put_nil: this.module.cwrap( 'PL_put_nil', 'number', []), PL_cons_functor_v: this.module.cwrap( 'PL_cons_functor_v', 'number', ['number', 'number', 'number']), PL_cons_list: this.module.cwrap( 'PL_cons_list', 'number', ['number', 'number', 'number']), PL_put_dict: this.module.cwrap( 'PL_put_dict', 'number', ['number','number','number','number','number']), PL_put_term_from_chars: this.module.cwrap( 'PL_put_term_from_chars', 'number',['number','number','number','string']), PL_put_term: this.module.cwrap( 'PL_put_term', 'number', ['number', 'number']), PL_write_term: this.module.cwrap( 'PL_write_term', 'number', ['number', 'number', 'number', 'number']), PL_call: this.module.cwrap( 'PL_call', 'number', ['number', 'number']), PL_open_foreign_frame: this.module.cwrap( 'PL_open_foreign_frame', 'number', []), PL_close_foreign_frame: this.module.cwrap( 'PL_close_foreign_frame', 'number', ['number']), PL_discard_foreign_frame: this.module.cwrap( 'PL_close_foreign_frame', 'number', ['number']), PL_predicate: this.module.cwrap( 'PL_predicate', 'number', ['number', 'number', 'number']), PL_open_query: this.module.cwrap( 'PL_open_query', 'number', ['number', 'number', 'number', 'number']), PL_next_solution: this.module.cwrap( 'PL_next_solution', 'number', ['number']), PL_close_query: this.module.cwrap( 'PL_close_query', 'number', ['number']), PL_cut_query: this.module.cwrap( 'PL_cut_query', 'number', ['number']), PL_exception: this.module.cwrap( 'PL_exception', 'number', ['number']), PL_raise_exception: this.module.cwrap( 'PL_raise_exception', 'number', ['number']), WASM_ttymode: this.module.cwrap( 'WASM_ttymode', 'number', []), WASM_yield_request: this.module.cwrap( 'WASM_yield_request', 'number', []), WASM_set_yield_result: this.module.cwrap( 'WASM_set_yield_result', 'number', ['number']), WASM_variable_id: this.module.cwrap( 'WASM_variable_id', 'number', ['number']), js_unify_obj: this.module.cwrap( 'js_unify_obj', 'number', ['number', 'number']), js_get_obj: this.module.cwrap( 'js_get_obj', 'number', ['number']) }; } /** * Call a Prolog goal. This function deals with many variations to * call Prolog. * * @param {String} goal Goal to run * @param {String} [opts.module] Module in which to call Goal * @param {Boolean} [opts.async] Call as yieldable */ call(goal, opts) { opts = opts||{}; if ( typeof(goal) === "string" ) { if ( opts.async ) { return this.__call_yieldable(goal, opts); } else { return this.with_frame(function() { const term = this.new_term_ref(); if ( !this.chars_to_term(goal, term) ) throw new Error('Query has a syntax error: ' + query); const module = opts.module ? this.new_module(opts.module) : this.MODULE_user; return !!this.bindings.PL_call(term, module); }); } } } /** * Call code while reclaiming possibly allocated term_t references. * @param {Function} f function to be called * @param {Boolean} [persist] if `false`, discard all binding created * within the scope of the frame; */ with_frame(f, persist) { const fid = this.bindings.PL_open_foreign_frame(); if ( fid ) { const rc = f.call(this); if ( persist === false ) this.bindings.PL_discard_foreign_frame(fid); else this.bindings.PL_close_foreign_frame(fid); return rc; } return false; /* Throw? */ } __string_to_c(string) { const len = this.module.lengthBytesUTF8(string); const ptr = _malloc(len+1); this.module.stringToUTF8(string, ptr, len+1); return { ptr: ptr, length: len } } /** * Get a reference to a predicate * * @param name {String} Name of the predicate. If this is the only * argument it encodes module, name and arity * as `[module:]name/arity` * @param arity {Integer} Arity of the predicate * @param module {String} Module to resolve the predicate */ predicate(name, arity, module) { if ( arity === undefined ) { let ar = /^([^:]+):(.*)\/([0-9]+)$/.exec(name); if ( ar ) { module = ar[1]; name = ar[2]; arity = parseInt(ar[3]); } else { ar = /(.*)\/([0-9]+)$/.exec(name); if ( ar) { name = ar[1]; arity = parseInt(ar[2]); } } if ( arity === undefined ) throw(`Prolog.predicate: illegal specification: ${name}`); } const c_name = stringToNewUTF8(name); const c_module = stringToNewUTF8(module||"user"); const pred = this.bindings.PL_predicate(c_name, arity, c_module); _free(c_name); _free(c_module); return pred; } /** * Lookup a Prolog module by name * * @param {String} name Name of the module * @return {module_t} Handle to the Prolog module */ new_module(name) { const c_atom = this.new_atom(name); const module = this.bindings.PL_new_module(c_atom); this.unregister_atom(c_atom); return module; } /** * Consult a list of files. Files are loaded ordered and may * be a mixture of local Prolog files and URLs. Both `.pl` and * `.qlf` files are supported. */ consult(...args) { return this.forEach("load_files(user:Files)", {Files:args}); } load_string(s, id) { if ( !id ) { this.__script_id = (this.__script_id+1)||1; id = "/string/"+this.__script_id; } return this.forEach("setup_call_cleanup("+ "open_string(S, _In),"+ "load_files(user:Id, [stream(_In)]),"+ "close(_In))", {S:new this.String(s), Id:id}); } async load_scripts() { const prolog = this; const scripts = document.querySelectorAll("script[type='text/prolog']"); const loaded = []; for(let i = 0; i { prolog.query(goal, {Event__:ev}).once(); }); } fetch(url, opts, type) { return fetch(url, opts).then((response) => response[type]()); } url_properties(url) { return fetch(url, {method: 'HEAD'}).then((r) => { if ( r.status == 200 ) { let size = parseInt(r.headers.get("content-length")); const mods = r.headers.get("last-modified"); const time = Date.parse(mods) || 0; if ( ! size instanceof Number ) size = -1; return { url: r.url, status: r.status, size: size, last_modified: time/1000 }; } else { return { url: url, status: r.status }; } }); } /** * Convert a Prolog message term into a string. Notably used to * translate Prolog exceptions to meaningful messages in the JavaScript * side. * * @return {String} string representation of the message */ message_to_string(term) { return this.with_frame(() => { const av = this.new_term_ref(2); this.bindings.PL_put_term(av+0, term); const flags = this.PL_Q_NORMAL; const pred = this.predicate("message_to_string/2"); const qid = this.bindings.PL_open_query(0, flags, pred, av); let msg; if ( this.bindings.PL_next_solution(qid) ) msg = this.get_chars(av+1); else msg = "Unknown Prolog exception"; this.bindings.PL_close_query(qid); return msg; }, false); } /** * When using `Module.on_output`, flush the output streams. */ flush_output(stream) { if ( stream == undefined ) { flush("stderr"); flush("stdout"); } else { flush(stream); } } log(...args) { log_output("stdout", args); } /** * Signature: * - query(module, flags, pred, argv, [map], [fid]) * - query(goal[, input]) */ query(module, flags, pred, argv, map, fid) { if ( typeof(argv) === "number" ) /* term_t array */ { return new Query(this, module, flags, pred, argv, map); } else if ( typeof(module) === "string" && pred === undefined ) { const goal = module; const fid = this.bindings.PL_open_foreign_frame(); const av = this.new_term_ref(3); const input = flags||{}; this.frame = fid; this.put_chars(av+0, goal); this.toProlog(input, av+1); const q = new Query(this, 0, this.PL_Q_CATCH_EXCEPTION, "wasm_call_string/3", av, (a) => this.toJSON(a+2)); q.from_text = true; return q; } } /** * Run a possibly long running goal and process its answers. * Signature: * - foreach(goal, [input], [callback]) * @return {Promise} that is resolved on completion and rejected on * a Prolog exception. */ forEach(goal, ...args) { const prolog = this; const fid = this.bindings.PL_open_foreign_frame(); const av = this.new_term_ref(3); let callback; let input; if ( typeof(args[0]) === "object" ) { input = args[0]; callback = args[1]; } else { input = {}; callback = args[0]; } if ( callback !== undefined && typeof(callback) !== "function" ) throw TypeError("callback must be a function"); this.frame = fid; this.put_chars(av+0, goal); this.toProlog(input, av+1); const q = new Query(this, this.MODULE_user, this.PL_Q_ALLOW_YIELD|this.PL_Q_CATCH_EXCEPTION, "wasm_call_string_with_heartbeat/3", av, (a) => this.toJSON(a+2)); return new Promise(function(resolve, reject) { let answers = callback ? 0 : []; function next_foreach(rc) { while(true) { if ( rc.yield !== undefined ) { switch(rc.yield) { case "beat": return setTimeout(() => next_foreach(rc.resume("true")), 0); case "builtin": return rc.resume((rc) => next_foreach(rc)); default: // unsupported yield throw(rc); } } else if ( rc.value ) { if ( callback ) { answers++; callback.call(prolog, rc.value); } else { answers.push(rc.value); } if ( rc.done == false ) { rc = q.next_yieldable(); continue; } } q.close(); if ( rc.error ) return reject(rc.message); if ( rc.done ) return resolve(answers); } } return next_foreach(q.next_yieldable()); }); } abort() { this.abort_request = true; } /** * Create an abortable promise that represents sleeping * @param {Number} time to sleep in seconds. * @return {Promise} This promise represents the sleep and provides a * method abort() that aborts the sleep. */ promise_sleep(time) { const f = function(resolve, reject) { f.reject = reject; f.timer = setTimeout(() => { f.timer = undefined; resolve(true); }, time*1000); }; f.abort = function() { if ( f.timer ) { clearTimeout(f.timer); f.timer = undefined; f.reject("abort"); } } return new this.Promise(f); } /** * @return {IOSTREAM*} as a number (pointer) */ stream(name) { const iob = this.bindings._PL_streams(); /* IOSTREAM** */ let offset = undefined; switch(name) { case "user_input": offset = 0; break; case "user_output": offset = 1; break; case "user_error": offset = 2; break; default: throw(`Unknown stream ${name}`); } return this.module.getValue(iob+offset*4, 'i32'); } write(term, opts) { opts = opts||{}; const precedence = opts.precedence||1200; const flags = opts.flags == undefined ? this.PL_WRT_QUOTED|this.PL_WRT_NEWLINE : flags; let s = undefined; if ( opts.stream ) { if ( typeof(stream) === "string" ) s = this.stream(opts.stream); } else { s = this.stream("user_output"); } return this.bindings.PL_write_term(s, term, precedence, flags); } // Return the arity of the given functor. functor_arity(functor) { return this.bindings.PL_functor_arity(functor); } // Return an atom representing the name of the given functor. functor_name(functor) { return this.bindings.PL_functor_name(functor); } // Returns functor of the given term. // Returns null when the term is not a compound. get_functor(term) { const ptr = _malloc(4); let result; if ( this.bindings.PL_get_functor(term, ptr) ) result = this.module.getValue(ptr, 'i32'); else result = null; _free(ptr); return result; } // Returns integer number for the given term. // Returns null when the term is not an integer. get_integer(term) { const ptr = _malloc(8); let rc; if ( this.bindings.PL_get_int64(term, ptr) ) { rc = this.module.getValue(ptr, 'i64'); if ( rc >= Number.MIN_SAFE_INTEGER && rc <= Number.MAX_SAFE_INTEGER ) rc = Number(rc); } else { const s = this.get_chars(term, this.CVT_INTEGER); rc = BigInt(s); } _free(ptr); return rc; } get_float(term) { const ptr = _malloc(8); let rc; if (this.bindings.PL_get_float(term, ptr)) { rc = this.module.getValue(ptr, 'double'); } else { rc = null; } _free(ptr); return rc; } /** * Make a JavaScript string available to Prolog. By default the * Prolog is represented as a Prolog string. * * @return {Boolean} */ put_chars(term, string, flags) { flags = flags||this.PL_STRING; flags |= this.REP_UTF8; const c = this.__string_to_c(string); const ret = !!this.bindings.PL_put_chars(term, flags, c.length, c.ptr); _free(c.ptr); return ret; } // See https://groups.google.com/g/emscripten-discuss/c/nQlUHq-Nk68 put_bytes(term, array_buffer) { const content = new Uint8Array(array_buffer); return !!this.bindings.put_bytes(term, this.PL_STRING|this.REP_ISO_LATIN_1, content.length, content); } put_bigint(term, value) { const s = value.toString(); return this.bindings.PL_put_term_from_chars(term, this.REP_UTF8, -1, s); } // Unifies the terms. Returns false if the terms // do not unify. unify(term1, term2) { return !!this.bindings.PL_unify(term1, term2); } // Returns whether the term is a string. is_string(term) { return !!this.bindings.PL_is_string(term); } is_variable(term) { return !!this.bindings.PL_is_variable(term); } // Return a C-string for the text represented by the given atom. atom_chars(atom) { const t = this.new_term_ref(); this.bindings.PL_put_atom(t, atom); const str = this.get_chars(t, this.CVT_ATOM); this.bindings.PL_reset_term_refs(t); return str; } // Get the TTY mode as one of "notty", "raw" or "cooked" ttymode() { return this.module.UTF8ToString(this.bindings.WASM_ttymode()); } yield_request() { const tref = this.bindings.WASM_yield_request(); return this.toJSON(tref); } set_yield_result(obj) { this.with_frame(() => { const term = this.toProlog(obj, undefined, {string:"string"}); if ( !term ) { console.log("Could not convert", obj); throw("Could not convert JavaScript data to Prolog"); } this.bindings.WASM_set_yield_result(term); }, true); } /** * Call a goal that may yield. When no yield happens this returns the * result of Query.next(). If the predicate called await/2 an * the returned object contains a key `yield` that either * holds a string or a JSON object representing the request. The key * `resume` is a function that should be called to resume Prolog with * a value that appears in the second argument of js_call/2. * * The `yield` key can be `builtin`, in which case an object is returned * that contains a `resume` key that executes the built-in and continues * using the passed function. The returned object may provide an `abort` * key to abort the query immediately. * * @param {String_t} goal Prolog goal to be called * @param {String} [module] Module in which to call the goal. * @return Either the result of Query.next() or a _yield_ request as * described above. */ __call_yieldable(goal, module) { var pred_call1; const flags = this.PL_Q_NORMAL|this.PL_Q_ALLOW_YIELD; if ( !pred_call1 ) pred_call1 = this.predicate("call", 1, "system"); const fid = this.bindings.PL_open_foreign_frame(); const term = this.new_term_ref(); if ( !this.chars_to_term(goal, term) ) throw new Error('Query has a syntax error: ' + query); const q = this.query(module, flags, pred_call1, term, fid); return q.next_yieldable(); } /******************************* * CONVERSION * *******************************/ /** * Define arguments name for a functor. For example * * Prolog.functor_arg_names("point", ["x", "y"]); */ set_arg_names(name, args) { if ( !this.functor_arg_names_[name] ) this.functor_arg_names_[name] = {}; this.functor_arg_names_[name][args.length] = args; } arg_names(name, arity) { if ( this.functor_arg_names_[name] ) return this.functor_arg_names_[name][arity]; } /** * Convert a Prolog term into a JavaScript object. This follows * https://github.com/SWI-Prolog/packages-mqi/issues/4 */ toJSON(term, options) { options = options||{}; function toJSON(prolog, term, options) { switch ( prolog.bindings.PL_term_type(term) ) { case prolog.PL_VARIABLE: return new prolog.Var(prolog.bindings.WASM_variable_id(term)); case prolog.PL_STRING: if ( options.string !== "string" ) return new prolog.String(prolog.get_chars(term)); /*FALLTHROUGH*/ case prolog.PL_ATOM: return prolog.get_chars(term); case prolog.PL_NIL: return []; case prolog.PL_BLOB: { const id = prolog.bindings.js_get_obj(term); if ( id != -1 ) return prolog.objects[id]; return new prolog.Blob(); } case prolog.PL_INTEGER: return prolog.get_integer(term); case prolog.PL_RATIONAL: { let s = prolog.get_chars(term, prolog.CVT_RATIONAL); let a = s.split("r"); function toInt(s) { const bi = BigInt(s); if ( bi >= Number.MIN_SAFE_INTEGER && bi <= Number.MAX_SAFE_INTEGER ) return Number(bi); return bi; } return new prolog.Rational(toInt(a[0]), toInt(a[1])); } case prolog.PL_FLOAT: return prolog.get_float(term); case prolog.PL_TERM: { const f = prolog.get_functor(term); const name = prolog.atom_chars(prolog.functor_name(f)); const arity = prolog.functor_arity(f); const map = prolog.arg_names(name, arity); const a = prolog.new_term_ref(); if ( map ) { let result = { $tag: name }; for(var i=0; i= 0 && rc; i--) { rc = ( toProlog(prolog, data[i], h, ctx) && prolog.bindings.PL_cons_list(term, h, term) ); } return rc; } switch(typeof(data)) { case "number": if ( Number.isInteger(data) ) rc = prolog.bindings.PL_put_integer(term, data); else rc = prolog.bindings.PL_put_float(term, data); break; case "bigint": rc = prolog.put_bigint(term, data); break; case "string": { const flags = ctx.string === "string" ? prolog.PL_STRING : prolog.PL_ATOM; rc = prolog.put_chars(term, data, flags); break; } case "boolean": rc = prolog.put_chars(term, data ? "true" : "false", prolog.PL_ATOM); break; case "undefined": rc = prolog.put_chars(term, "undefined", prolog.PL_ATOM); break; case "object": if ( data === null ) { rc = prolog.put_chars(term, "null", prolog.PL_ATOM); } else if ( Array.isArray(data) ) { rc = toList(term, data); } else if ( data.$t ) { switch( data.$t ) { case "s": rc = prolog.put_chars(term, data.v, prolog.PL_STRING); break; case "r": { const s = data.n+"r"+data.d; rc = prolog.bindings.PL_put_term_from_chars(term, prolog.REP_UTF8, -1, s); break; } case "t": { const keys = Object.keys(data); let args; let name; for(var i=0; i { prolog.set_yield_result(value); return this.next(); } }; } } } next_yieldable() { function next(query) { const prolog = query.prolog; while(true) { let rc = query.next(); if ( rc.yield !== undefined ) { let request = rc.yield; if ( prolog.abort_request ) { prolog.abort_request = undefined; prolog.set_yield_result("abort"); continue; } if ( request === "beat" ) { const now = Date.now(); const passed = now - prolog.lastyieldat; if ( passed < 20 ) { prolog.set_yield_result("true"); continue; } prolog.lastyieldat = now; } else if ( request instanceof Promise ) { let result = { yield: "builtin", request: request, query: query, resume: (cont) => { if ( typeof(cont) === "string" ) { prolog.set_yield_result(cont); return next(query); } else { result.cont = cont; request .then((value) => { prolog.set_yield_result(value); cont.call(prolog, next(query)); }) .catch((error) => { prolog.set_yield_result({$error: error}); cont.call(prolog, next(query)); }) } }, abort: () => { if ( !(request.abort && request.abort()) ) { console.log("Cannot abort", request); prolog.abort_request = true; } } }; return result; } // Get back here instead of Query.next() rc.resume = (value) => { prolog.set_yield_result(value); return next(query); }; } else if ( rc.done === false ) { rc.resume = () => next(query); } return rc; } } return next(this); } /** * Run a query once. * @return To simplify distinguishing success from failure the * returned object has a field `success` if the Prolog query * completed without an error. */ once() { const rc = this.next(); this.close(); if ( this.from_text ) { delete rc.done; if ( rc.value ) { rc.value.success = true; return rc.value; } else { if ( !rc.error ) rc.success = false; return rc; } } else { return rc.value ? rc.value : rc; } } close() { if ( this.open ) { const prolog = this.prolog; if ( this != prolog.open_queries.at(-1) ) console.log("Attempt for Query.close() on not innermost query"); prolog.open_queries.pop(); this.prolog.bindings.PL_cut_query(this.qid); if ( this.frame ) this.prolog.bindings.PL_discard_foreign_frame(this.frame); this.open = false; } } } /******************************* * BIND PROLOG TO THE MODULE * *******************************/ Module.onRuntimeInitialized = function() { Module.prolog = new Prolog(Module, Module.arguments); }; /******************************* * CALL JAVASCRIPT * *******************************/ /** * Call a chain of functions and selectors. This function works on * the data structures created by `:=/2` */ function prolog_js_call(request, result) { const prolog = Module.prolog; function eval_chain(ar, obj) { obj = obj||window; function eval_one(obj, fname, args) { if ( args.length == 0 ) { switch(fname) { case "instanceof": return obj.constructor.name; } } else if ( args.length == 1 ) { switch(fname) { case "-": return -args[0]; case "!": return !args[0]; case "instanceof": return obj instanceof args[0]; } } else if ( args.length == 2 ) { switch(fname) { case "+": return args[0] + args[1]; case "-": return args[0] - args[1]; case "*": return args[0] * args[1]; case "/": return args[0] / args[1]; case "&": return args[0] & args[1]; case "|": return args[0] | args[1]; case "&&": return args[0] && args[1]; case "||": return args[0] || args[1]; } } const func = obj[fname]; if ( typeof(func) === "function" ) return func.apply(obj, args); else console.log("ERROR: Function", fname, "is not defined on", obj); } for(let i=0; i eval_chain(v)); obj = eval_one(obj, next.f, args); } } return obj; } try { return prolog.with_frame(() => { const ar = prolog.toJSON(request, { string: "string" }); let obj; if ( ar.setter ) { const target = eval_chain(ar.target); const value = eval_chain(ar.value); target[ar.setter] = value; obj = true; } else { obj = eval_chain(ar); } return prolog.unify(result, prolog.toProlog(obj)); }, false); } catch (e) { return prolog.bindings.PL_raise_exception( prolog.toProlog(new prolog.Compound("error", [ new prolog.Compound("js_error", [e.toString()]), new prolog.Var() ]))); } } function prolog_js_obj_class_name(id) { const prolog = Module.prolog; const obj = prolog.objects[id]; return obj.constructor.name; } /** * Release an object held by Prolog */ function release_registered_object(id) { const prolog = Module.prolog; const obj = prolog.objects[id]; prolog.object_ids.delete(obj); delete prolog.objects[id]; } if ( globalThis.BigInt.prototype.toJSON === undefined ) { globalThis.BigInt.prototype.toJSON = function () { return this.toString(); } } if ( globalThis.HTMLCollection && globalThis.HTMLCollection.prototype && !globalThis.HTMLCollection.prototype.toList ) { globalThis.HTMLCollection.prototype.toList = function() { const ar = []; for(let i=0; i