UNPKG

node-swipl-io

Version:

A Node.js interface to the SWI-Prolog.

497 lines (430 loc) 14.8 kB
const path = require('path'); const assert = require('assert'); const EventEmitter = require('events'); const spawn = require('child_process').spawn; const split = require('split'); const term = require('./lib/term'); const debug = require('debug')('swipl'); // Helper class to track the engine state and // detect error conditions. class EngineState extends EventEmitter { constructor() { super(); this.setState(EngineState.ACCEPTING); } isClosed() { return this.state === EngineState.CLOSED; } isAccepting() { return this.state === EngineState.ACCEPTING; } isQuery() { return this.state === EngineState.QUERY; } setAccepting() { this.setState(EngineState.ACCEPTING); } setClosed() { this.setState(EngineState.CLOSED); } setQuery() { this.setState(EngineState.QUERY); } setWaiting() { this.setState(EngineState.WAITING); } setState(state) { this.state = state; debug(`Engine state set to ${state}.`); this.emit('change', state); } } EngineState.ACCEPTING = 'accepting'; // Accepting new queries. EngineState.QUERY = 'query'; // Has a fresh query instance. EngineState.WAITING = 'waiting'; // Waiting output from Prolog. EngineState.CLOSED = 'closed'; // Engine is closed. // Query waiting for another query to finish. class QueuedQuery { constructor(string, deferred) { this.string = string; this.deferred = deferred; } } // Prolog engine. Representing one external // SWI-Prolog process. class Engine { constructor() { const top = path.join(__dirname, 'top.pl'); this.swipl = spawn('swipl', [ '-f', top, '--no-tty', '-q', '-t', 'loop', '--nodebug', '-O' ]); this.state = new EngineState(); this.status = 0; this.query = null; this.queue = []; this.swipl.on('close', (code) => { this.status = code; this.state.setClosed(); }); this.state.on('change', () => { if (this.state.isAccepting()) { // If there was queued query then // start working on it. const queued = this.queue.shift(); if (queued) { debug('Working on a queued query.'); this.query = new Query(this, queued.string); this.state.setQuery(); queued.deferred.resolve(this.query); } } }); this.swipl.stdout.pipe(split()).on('data', (line) => { line = line.trim(); if (line.length === 0) { // Last line, empty, do nothing. return; } debug(`Received from Prolog: ${line}.`); if (this.state.isClosed()) { // Engine is closed. Do nothing. return; } try { const obj = JSON.parse(line); if (this.query) { // Pass the response to the query instance. // It will apply changes to the engine state // as well. this.query._response(obj); } } catch (err) { // Received invalid output from Prolog. this.state.setClosed(); if (this.query && this.query.deferred) { this.query.deferred.reject(err); } // Reject all queued queries as well. for (const queued of this.queue) { queued.reject(new Error('The engine was closed: ' + err)); } } }); // Stderr of SWI-Prolog is just redirected // to the main stderr. this.swipl.stderr.on('data', (data) => { process.stderr.write(data); }); } // Creates a new Query instance on this engine. async createQuery(string) { assert.strictEqual(typeof string, 'string', 'Query must be a string.'); assert.ok(!this.state.isClosed(), 'Engine must not be closed.'); if (this.state.isAccepting()) { // Query can be executed right now. this.query = new Query(this, string); this.state.setQuery(); return this.query; } else { debug('Engine busy, putting query into a queue.'); // Query cannot be executed now. It is put into // a queue waiting until the query can be worked on. const deferred = new Deferred(); this.queue.push(new QueuedQuery(string, deferred)); return deferred.promise; } } // Helper around createQuery to extract single // solution. async call(string) { const query = await this.createQuery(string); try { // Wait until respone comes in // before closing the query. return await query.next(); } finally { await query.close(); } } async consult(file) { const queryString = `consult("${file}").` const query = await this.createQuery(queryString); try { // Wait until respone comes in // before closing the query. return await query.next(); } finally { await query.close(); } } // Closes the engine. Stops the Prolog process. close() { this.swipl.kill(); this.state.setClosed(); } _sendQuery(string) { assert.ok(this.state.isQuery(), 'Engine has query.'); this._sendObject({ query: string }); } _sendNext() { debug('Requesting next solution.'); assert.ok(this.state.isQuery(), 'Engine has query.'); this._sendObject({ action: 'next' }); } _sendClose() { assert.ok(this.state.isQuery(), 'Engine has query.'); this._sendObject({ action: 'close' }); } _sendObject(obj) { const json = JSON.stringify(obj); debug(`Sending to Prolog: ${json}.`); this.swipl.stdin.write(`${json}\n`); this.state.setWaiting(); } } class EngineService { engine = new Engine() constructor(...files) { // TODO Add files is an array of strings path if (!this.engine) { this.engine = new Engine(); } files.map(e => this.engine.consult(e)) } call = async (params) => { const query = await this.engine.createQuery(params) let result try { result = await query.next() } finally { await query.close() } return result } createQuery = async (params) => { return this.engine.createQuery(params) } consult = async (file) => { await this.engine.consult(file) } close = () => { this.engine.close() } composeQuery = (name, ...args) => { var queryStr = `${args[0]}` args.shift() for (let val of args) queryStr = `${queryStr}, ${val}` const query = await this.engine.createQuery(`${name}(${queryStr}).`); let result = [] let result1 do { result1 = await query.next() if (result1!=false) { for (var key in result1) { if (result1.hasOwnProperty(key)) { if (result1[key].hasOwnProperty('head')) { result1[key] = await this.toArray(result1[key]) } } } result.push(result1) } } while(result1!=false) await query.close() // console.log(JSON.stringify(result)) if(result.length==1) return result[0] if(result.length==0) return false return result } addToTailProlog = async (list, elem) => { if(typeof list === "object") { let a = await this.toArray(list); list = toStrList(a); } const query = await this.engine.createQuery(`insertAtEnd(${elem},${list}, Res)`); const result = await query.next(); await query.close(); return result.Res } fromStruct = async (struct) => { if(typeof struct === "string"){ const query = await this.engine.createQuery(`toListFromStr(${struct}, Res)`); try { const result = await query.next(); struct = result.Res } finally { await query.close(); } } var {args, name} = struct return {name, args} } toArray = async (list) => { if(typeof list === "string"){ const query = await this.engine.createQuery(`toListFromStr(${list}, Res).`); try { const result = await query.next(); list = result.Res } finally { await query.close(); } } if(list.hasOwnProperty('head')) { var arr = [] var {head, tail} = list arr.push(head) while (tail != "[]") { var {head, tail} = tail arr.push(head) } return arr } return list } addToTailJS = async (list, elem) => { let ress = (await this.toArray(list)) ress.push(elem) return ress } } // Helper class to track the query state and // detect error conditions. class QueryState { constructor() { this.setState(QueryState.FRESH); } setWaiting() { this.setState(QueryState.WAITING); } setOpen() { this.setState(QueryState.OPEN); } setClosed() { this.setState(QueryState.CLOSED); } isFresh() { return this.state === QueryState.FRESH; } isOpen() { return this.state === QueryState.OPEN; } isWaiting() { return this.state === QueryState.WAITING; } isClosed() { return this.state === QueryState.CLOSED; } setState(state) { this.state = state; debug(`Query state set to ${state}.`); } } QueryState.FRESH = 'fresh'; // No query sent to Prolog yet. QueryState.OPEN = 'open'; // Query sent to Prolog. Not waiting. QueryState.WAITING = 'waiting'; // Waiting for input from Prolog. QueryState.CLOSED = 'closed'; // No more answers/error. class Query { constructor(engine, string) { this.query = string; this.engine = engine; this.deferred = null; this.state = new QueryState(); } // Finds next solution. Returns a promise that // resolves to the bindings object or false. // In case of an error, the promise is rejected. async next() { if (this.state.isFresh()) { this.engine._sendQuery(this.query); this.deferred = new Deferred(); this.state.setWaiting(); return this.deferred.promise; } else if (this.state.isOpen()) { this.engine._sendNext(); this.deferred = new Deferred(); this.state.setWaiting(); return this.deferred.promise; } else { throw new Error(`Invalid query state ${this.state.state}.`); } } // Closes the query. async close() { if (this.state.isClosed()) { return Promise.resolve(); } else if (this.state.isOpen()) { this.deferred = new Deferred(); this.engine._sendClose(); this.state.setWaiting(); return this.deferred.promise; } else if (this.state.isFresh()) { this.state.setClosed(); this.engine.state.setAccepting(); return Promise.resolve(); } else { throw new Error(`Invalid query state ${this.state.state}.`); } } // Receives response from Prolog. _response(obj) { assert(this.state.isWaiting(), 'Query is waiting for response.'); if (obj.status === 'success') { this.state.setOpen(); this.deferred.resolve(obj.bindings); this.engine.state.setQuery(); } else if (obj.status === 'fail') { this.state.setClosed(); this.deferred.resolve(false); this.engine.query = null; this.engine.state.setAccepting(); } else if (obj.status === 'error') { this.state.setClosed(); this.deferred.reject(new Error(obj.error)); this.engine.query = null; this.engine.state.setAccepting(); } } } // Helper class to create a deferred promise. class Deferred { constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } } function appendToList(elem, list){ if(typeof list === "string"){ return `[${elem}|${list}]` } return {head: elem, tail: list} } async function toJSONList(...args) { let list = "[]" for (let val of args) if(Array.isArray(val)) for (let innerval of val) list = {head: innerval, tail: list} else list = {head: val, tail: list} return list } function toStrList(...args) { var list = '[]' for (let val of args) if(Array.isArray(val)) for (let innerval of val) list = `[${innerval}|${list}]` else list = `[${val}|${list}]` return list } function toStrStruct(name, ...args) { var struct = '' // args.map().join(',') for (let val of args) struct = `${val},${struct}` return `${name}(${struct.slice(0,-1)})` // return `${name}(${args.map().join(',')})` } function toJSONStruct(name, ...args) { return {args, name} } module.exports = { EngineService, term, toJSONList, toJSONStruct, toStrStruct, toStrList, appendToList };