├── README ├── examples ├── example.pl └── simple.js ├── main.js ├── package.json └── src ├── eventwaiter.js ├── loader.js ├── parser.js ├── prover.js ├── query.js ├── tokenizer.js └── types ├── atom.js ├── body.js ├── rule.js ├── term.js └── variable.js /README: -------------------------------------------------------------------------------- 1 | Prolog interpreter for node.js 2 | 3 | This is based on the jsprolog project which you can find from the original author's website: http://ioctl.org/logic/prolog-latest, but it is almost entirely rewritten. 4 | -------------------------------------------------------------------------------- /examples/example.pl: -------------------------------------------------------------------------------- 1 | bird(pigeon). 2 | lives(pigeon,_). 3 | 4 | bird(penguin). 5 | lives(penguin,antarctica). 6 | lives(penguin,argentina). 7 | lives(penguin,australia). 8 | lives(penguin,chile). 9 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | var prologjs = require( '../main' ); 2 | 3 | prologjs.load( 'example.pl', function( dialog ) { 4 | dialog.prove( 'lives(penguin,X).', function( data ) { 5 | console.log( "Penguins live in: " + data.X.join( ' ' ) ); 6 | } ); 7 | } ); 8 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | var load = require( './src/loader' ).load; 2 | 3 | exports.load = load; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prologjs", 3 | "description": "Prolog interpreter for node", 4 | "version": "0.1.0", 5 | "author": "Aleksis Brezas ", 6 | "contributors": [ 7 | { "name": "Aleksis Brezas", "email": "abresas@kamibu.com" } 8 | ], 9 | "licenses": [ { 10 | "type": "MIT" 11 | } ], 12 | "engine": [ "node >=0.4.0" ], 13 | "main": "./main" 14 | } 15 | -------------------------------------------------------------------------------- /src/eventwaiter.js: -------------------------------------------------------------------------------- 1 | var inherits = require( 'util' ).inherits, 2 | EventEmitter = require( 'events' ).EventEmitter; 3 | 4 | var EventWaiter = function() { 5 | this.waiting = 0; 6 | this.enabled = true; 7 | 8 | EventEmitter.call( this ); 9 | }; 10 | 11 | inherits( EventWaiter, EventEmitter ); 12 | 13 | exports = module.exports = EventWaiter; 14 | 15 | EventWaiter.prototype.enable = function() { 16 | if ( !this.waiting ) { 17 | this.emit( 'complete' ); 18 | } 19 | }; 20 | 21 | EventWaiter.prototype.disable = function() { 22 | this.enabled = false; 23 | }; 24 | 25 | EventWaiter.prototype.createCallback = function() { 26 | ++this.waiting; 27 | 28 | var self = this; 29 | return function() { 30 | if ( !self.waiting ) { 31 | console.warn( 'prologjs: callback called when not waiting' ); 32 | return; 33 | } 34 | --self.waiting; 35 | var args = [ 'one' ]; 36 | for ( var i in arguments ) { 37 | args.push( arguments[ i ] ); 38 | } 39 | self.emit.apply( self, args ); 40 | if ( self.enabled && !self.waiting ) { 41 | self.emit( 'complete' ); 42 | } 43 | }; 44 | }; 45 | 46 | exports.EventWaiter = EventWaiter; 47 | -------------------------------------------------------------------------------- /src/loader.js: -------------------------------------------------------------------------------- 1 | var Prover = require( './prover' ), 2 | parser = require( './parser' ), 3 | fs = require( 'fs' ); 4 | 5 | function load( path, callback ) { 6 | fs.readFile( path, 'utf-8', function( err, data ) { 7 | var db = data.split( '\n' ); 8 | var rules = []; 9 | for ( var i in db ) { 10 | var prologRule = db[ i ]; 11 | if ( !prologRule || prologRule[ 0 ] == '%' ) { 12 | continue; 13 | } 14 | 15 | var parsedRule = parser.getRule( prologRule ); 16 | 17 | rules.push( parsedRule ); 18 | } 19 | 20 | callback( new Prover( rules ) ); 21 | } ); 22 | } 23 | 24 | exports.load = load; 25 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | var Body = require( './types/body' ), 2 | Rule = require( './types/rule' ), 3 | Term = require( './types/term' ), 4 | Atom = require( './types/atom' ), 5 | Variable = require( './types/variable' ), 6 | Tokenizer = require( './tokenizer' ); 7 | 8 | exports = module.exports = parser = {}; 9 | 10 | parser.getBody = function( tokenizer ) { 11 | if ( typeof tokenizer == 'string' ) { 12 | tokenizer = new Tokenizer( tokenizer ); // got body as a string 13 | } 14 | 15 | var term, list = [], string = tokenizer.remainder; 16 | 17 | while ( ( term = parser.getTerm( tokenizer ) ) !== null ) { 18 | list.push( term ); 19 | if ( tokenizer.current != "," ) { 20 | break; 21 | } 22 | tokenizer.consume(); 23 | } 24 | 25 | if ( !list.length ) { 26 | throw 'Failed to parse body: "' + string + '".'; 27 | } 28 | 29 | return new Body( list ); 30 | }; 31 | 32 | parser.getRule = function( ruleString ) { 33 | var tokenizer = new Tokenizer( ruleString ), 34 | head = parser.getTerm( tokenizer ), 35 | body; 36 | 37 | if ( tokenizer.current == ':-' ) { 38 | tokenizer.consume(); 39 | body = this.getBody( tokenizer ); 40 | 41 | if ( tokenizer.current != '.' ) { 42 | throw 'error parsing rule "' + ruleString + '". expected "." after body, got: "' + tokenizer.current + '"'; 43 | } 44 | } 45 | else if ( tokenizer.current == '.' ) { 46 | // simple rule 47 | body = null; 48 | } 49 | else { 50 | throw 'error parsing rule "' + ruleString + '". expected "." or ":-", got: "' + tokenizer.current + '"'; 51 | } 52 | 53 | return new Rule( head, body ); 54 | }; 55 | 56 | // parse term of the form id( param1, param2, ... ) 57 | parser.getTerm = function( tokenizer ) { 58 | if ( tokenizer.type == "punc" && tokenizer.current == "!" ) { 59 | tokenizer.consume(); 60 | return new Term( "cut", [] ); 61 | } 62 | 63 | var negative = false; 64 | if ( tokenizer.type == 'punc' && tokenizer.current == '\\+' ) { 65 | tokenizer.consume(); 66 | negative = true; 67 | } 68 | 69 | var notthis = false; 70 | if ( tokenizer.current == "NOTTHIS" ) { 71 | notthis = true; 72 | tokenizer.consume(); 73 | } 74 | 75 | if ( tokenizer.type != "id" ) { 76 | return null; 77 | } 78 | 79 | var name = tokenizer.current; 80 | tokenizer.consume(); 81 | 82 | if ( tokenizer.current != "(" ) { 83 | // fail shorthand for fail(), ie, fail/0 84 | if (name == "fail") { 85 | return new Term(name, []); 86 | } 87 | return null; 88 | } 89 | 90 | tokenizer.consume(); 91 | var parts = []; 92 | while ( tokenizer.current != ")" ) { 93 | if ( tokenizer.type == "eof" ) { 94 | return null; 95 | } 96 | 97 | var part = parser.getParameters( tokenizer ); 98 | if ( !part ) { 99 | return null; 100 | } 101 | 102 | if ( tokenizer.current == "," ) { 103 | tokenizer.consume(); 104 | } 105 | else if ( tokenizer.current != ")" ) { 106 | return null; 107 | } 108 | 109 | parts.push( part ); 110 | } 111 | 112 | tokenizer.consume(); 113 | 114 | var t = new Term( name, parts, notthis ); 115 | if ( negative ) { 116 | t.negative = true; 117 | } 118 | return t; 119 | }; 120 | 121 | parser.getVariable = function( tokenizer ) { 122 | var name = tokenizer.current; 123 | tokenizer.consume(); 124 | return new Variable( name ); 125 | }; 126 | 127 | parser.getList = function( tokenizer ) { 128 | if ( tokenizer.current != "[" ) { 129 | return null; 130 | } 131 | 132 | tokenizer.consume(); 133 | 134 | // special case, [] = new atom( nil ) 135 | 136 | if ( tokenizer.type == "punc" && tokenizer.current == "]" ) { 137 | tokenizer.consume(); 138 | return new Atom( "nil" ); 139 | } 140 | 141 | var l = [], i = 0; 142 | 143 | while ( true ) { 144 | var t = parser.getParameters( tokenizer ); 145 | if ( !t ) { 146 | return null; 147 | } 148 | 149 | l[ i++ ] = t; 150 | if ( tokenizer.current != "," ) { 151 | break; 152 | } 153 | tokenizer.consume(); 154 | } 155 | 156 | var append; 157 | if ( tokenizer.current == "|" ) { 158 | tokenizer.consume(); 159 | if ( tokenizer.type != "var" ) { 160 | return null; 161 | } 162 | append = new Variable( tokenizer.current ); 163 | tokenizer.consume(); 164 | } 165 | else { 166 | append = new Atom( "nil" ); 167 | } 168 | if ( tokenizer.current != "]" ) { 169 | return null; 170 | } 171 | tokenizer.consume(); 172 | 173 | for ( --i; i >=0; i--) { 174 | append = new Term( "cons", [ l[i], append ] ); 175 | } 176 | 177 | return append; 178 | }; 179 | 180 | parser.getParameters = function( tokenizer ) { 181 | // Part -> var | id | id(optParamList) 182 | // Part -> [ listBit ] ::-> cons(...) 183 | 184 | switch ( tokenizer.type ) { 185 | case "var": 186 | return parser.getVariable( tokenizer ); 187 | case "id": 188 | var name = tokenizer.current; 189 | tokenizer.consume(); 190 | 191 | if ( tokenizer.current != "(" ) { 192 | return new Atom( name ); 193 | } 194 | tokenizer.consume(); 195 | 196 | var p = []; 197 | var i = 0; 198 | while ( tokenizer.current != ")" ) { 199 | if ( tokenizer.type == "eof" ) { 200 | return null; 201 | } 202 | 203 | var part = parser.getParameters( tokenizer ); 204 | if ( !part ) { 205 | return null; 206 | } 207 | 208 | if ( tokenizer.current == "," ) { 209 | tokenizer.consume(); 210 | } 211 | else if ( tokenizer.current != ")" ) { 212 | return null; 213 | } 214 | 215 | p[ i++ ] = part; 216 | } 217 | tokenizer.consume(); 218 | 219 | return new Term(name, p); 220 | case "punc": 221 | if ( tokenizer.current == "[" ) { 222 | return parser.getList( tokenizer ); 223 | } 224 | // else fall 225 | default: 226 | throw "unexpected token in parameters " + tokenizer.type + " " + tokenizer.current; 227 | } 228 | }; 229 | -------------------------------------------------------------------------------- /src/prover.js: -------------------------------------------------------------------------------- 1 | var Query = require( './query' ); 2 | 3 | var Prover = function( rules ) { 4 | this.userFunctions = {}; 5 | this.userData = []; 6 | this.rules = rules.slice(); 7 | }; 8 | 9 | exports = module.exports = Prover; 10 | 11 | Prover.prototype.addRule = function( ruleString ) { 12 | this.rules.push( parser.getRule( ruleString ) ); 13 | }; 14 | 15 | Prover.prototype.getRules = function() { 16 | return this.rules; 17 | }; 18 | 19 | Prover.prototype.setData = function( data ) { 20 | this.userData = data.slice(); 21 | }; 22 | 23 | Prover.prototype.clearData = function() { 24 | this.userData = []; 25 | }; 26 | 27 | Prover.prototype.addFunction = function( name, func ) { 28 | this.userFunctions[ name ] = func; 29 | }; 30 | 31 | Prover.prototype.removeFunction = function( name ) { 32 | delete this.userFunctions[ name ]; 33 | }; 34 | 35 | Prover.prototype._unify = function( rule, target ) { 36 | var head = rule.head; 37 | 38 | // basic checks 39 | if ( head.name != target.name || head.partlist.length != target.partlist.length ) { 40 | return false; 41 | } 42 | 43 | var variables = {}; 44 | // check if arguments match 45 | for ( var i = 0; i < head.partlist.length; ++i ) { 46 | var t_arg = target.partlist[ i ]; 47 | var r_arg = head.partlist[ i ]; 48 | if ( t_arg.type != r_arg.type && t_arg.type != 'Variable' ) { 49 | return false; 50 | } 51 | if ( t_arg.type == 'Atom' ) { 52 | if ( t_arg.name == r_arg.name ) { 53 | continue; 54 | } 55 | // else 56 | return false; 57 | } 58 | if ( t_arg.type == 'Variable' && r_arg.type != 'Variable' ) { 59 | if ( t_arg.name == '_' ) { 60 | continue; 61 | } 62 | if ( variables[ t_arg.name ] ) { 63 | variables[ t_arg.name ].push( r_arg.toString() ); 64 | continue; 65 | } 66 | // else 67 | variables[ t_arg.name ] = [ r_arg.toString() ]; 68 | } 69 | // should check for (Atom/Term,Variable) and (Variable,Variable) too but not needed for now 70 | } 71 | 72 | return variables; 73 | }; 74 | 75 | Prover.prototype.proveBody = function( query, variables, rule ) { 76 | // will check one condition for now, and will assume it is not a built-in 77 | var rule = rule.body.list[ 0 ], 78 | func = this.userFunctions[ rule.name ], 79 | args = [], 80 | rCallback; 81 | 82 | if ( !func ) { 83 | console.warn( 'prologjs: unknown condition: ' + rule.name ); 84 | return; 85 | } 86 | 87 | for ( i = 0; i < rule.partlist.length; ++i ) { 88 | args.push( rule.partlist[ i ].name ); 89 | } 90 | 91 | args.push( this.userData.slice() ); 92 | 93 | rCallback = query.createResultCallback(); 94 | 95 | args.push( function( result ) { 96 | /* 97 | console.log( 'prologjs: got callback result' ); 98 | console.log( result ); 99 | console.log( 'variables are' ); 100 | console.log( variables ); 101 | */ 102 | if ( rule.negative ) { 103 | result = !result; 104 | } 105 | if ( result ) { 106 | return rCallback( variables ); 107 | } 108 | // else do nothing 109 | rCallback( false ); 110 | } ); 111 | 112 | func.apply( {}, args ); 113 | }; 114 | 115 | Prover.prototype.proveRule = function( query, rule ) { 116 | var variables = this._unify( rule, query.currentRule ); 117 | 118 | if ( !variables ) { 119 | return; 120 | } 121 | // so far so good with the head, now check body 122 | 123 | if ( !rule.body ) { 124 | // easy one, no conditions 125 | query.extendResults( variables ); 126 | return; 127 | } 128 | 129 | this.proveBody( query, variables, rule ); 130 | }; 131 | 132 | Prover.prototype.prove = function( queryString, cb ) { 133 | var query = new Query( queryString, cb ); 134 | 135 | query.currentRule = parser.getBody( query.string ).list[ 0 ]; 136 | for ( var i in this.rules ) { 137 | this.proveRule( query, this.rules[ i ] ); 138 | } 139 | 140 | setTimeout( function() { 141 | // in case no callback was needed and complete wasn't fired 142 | // this could be better if query api was always async 143 | // but that would have its own quirks too 144 | if ( !query.waiting ) { 145 | query.emit( 'complete' ); 146 | } 147 | }, 0 ); 148 | }; 149 | -------------------------------------------------------------------------------- /src/query.js: -------------------------------------------------------------------------------- 1 | var EventWaiter = require( './eventwaiter' ), 2 | inherits = require( 'util' ).inherits; 3 | 4 | var Query = function( queryString, callback ) { 5 | EventWaiter.call( this ); 6 | 7 | this.string = queryString; 8 | this.results = {}; 9 | this.callback = callback; 10 | this.completed = false; 11 | 12 | var self = this; 13 | 14 | this.on( 'one', this.extendResults.bind( this ) ); 15 | this.on( 'complete', this.onComplete.bind( this ) ); 16 | }; 17 | 18 | exports = module.exports = Query; 19 | 20 | inherits( Query, EventWaiter ); 21 | 22 | Query.prototype.extendResults = function( variables ) { 23 | if ( !variables ) { 24 | return; 25 | } 26 | for ( var name in variables ) { 27 | // console.log( 'var ' + name + ' = ' + variables[ name ] ); 28 | if ( this.results[ name ] ) { 29 | Array.prototype.push.apply( this.results[ name ], variables[ name ] ); 30 | continue; 31 | } 32 | // else 33 | this.results[ name ] = variables[ name ].slice(); 34 | } 35 | }; 36 | 37 | Query.prototype.onComplete = function() { 38 | if ( this.completed ) { 39 | return console.warn( 'prologjs: query has been completed again warning' ); 40 | } 41 | this.completed = true; 42 | this.callback( this.results ); 43 | }; 44 | 45 | Query.prototype.createResultCallback = function() { 46 | return this.createCallback(); 47 | }; 48 | -------------------------------------------------------------------------------- /src/tokenizer.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = Tokenizer; 2 | 3 | function Tokenizer( string ) { 4 | this.remainder = string; 5 | this.current = null; 6 | this.type = null; // "eof", "id", "var", "punc" etc. 7 | this.consume(); // Load up the first token. 8 | } 9 | 10 | Tokenizer.prototype.punctuation = [ '(', ')', '.', ',', '[', ']', '!', ':', ':-', '\\+', '\\' ]; 11 | 12 | Tokenizer.prototype.consumeEmpty = function() { 13 | if ( this.remainder ) { 14 | return false; 15 | } 16 | 17 | this.current = null; 18 | this.type = "eof"; 19 | return true; 20 | }; 21 | 22 | Tokenizer.prototype.consumePunctuation = function() { 23 | var length = 0; 24 | var l = this.remainder.length; 25 | while ( length < l && this.punctuation.indexOf( this.remainder.substring( 0, length + 1 ) ) != -1 ) { 26 | ++length; 27 | } 28 | 29 | if ( !length ) { 30 | return false; 31 | } 32 | 33 | this.current = this.remainder.substring( 0, length ); 34 | this.remainder = this.remainder.substring( length ); 35 | this.type = "punc"; 36 | 37 | return true; 38 | }; 39 | 40 | Tokenizer.prototype.consumeVariable = function() { 41 | // check for uppercase letter 42 | if ( !isUppercase( this.remainder[ 0 ] ) && this.remainder[ 0 ] != '_' ) { 43 | return false; 44 | } 45 | var end = 0; 46 | var l = this.remainder.length; 47 | while ( end < l - 1 && isAlphanum( this.remainder[ end + 1 ] ) ) { 48 | ++end; 49 | } 50 | 51 | this.current = this.remainder.substring( 0, end + 1 ); 52 | this.remainder = this.remainder.substring( end + 1 ); 53 | this.type = "var"; 54 | 55 | return true; 56 | }; 57 | 58 | Tokenizer.prototype.consumeString = function() { 59 | var type = this.remainder[ 0 ], 60 | start = 1, 61 | closeIndex; 62 | 63 | if ( type != '"' && type != "'" ) { 64 | return false; 65 | } 66 | 67 | while ( true ) { 68 | closeIndex = this.remainder.indexOf( type, start ); 69 | if ( closeIndex == -1 ) { 70 | return false; 71 | } 72 | if ( this.remainder[ closeIndex - 1 ] == "\\" ) { 73 | start = closeIndex + 1; 74 | } 75 | else { 76 | break; 77 | } 78 | } 79 | 80 | this.current = this.remainder.substring( 0, closeIndex + 1 ); 81 | this.remainder = this.remainder.substring( closeIndex + 1 ); 82 | this.type = "id"; 83 | 84 | return true; 85 | }; 86 | 87 | Tokenizer.prototype.consumeAlphanumeric = function() { 88 | var end = -1; 89 | var l = this.remainder.length; 90 | while ( end < l - 1 && ( isAlphanum( this.remainder[ end + 1 ] ) || this.remainder[ end + 1 ] == '.' ) ) { 91 | ++end; 92 | } 93 | if ( end == -1 ) { 94 | return false; 95 | } 96 | this.current = this.remainder.substring( 0, end + 1 ); 97 | this.remainder = this.remainder.substring( end + 1 ); 98 | this.type = "id"; 99 | return true; 100 | }; 101 | 102 | // lexical analysis 103 | Tokenizer.prototype.consume = function() { 104 | if ( this.type == "eof" ) { 105 | return; 106 | } 107 | 108 | this.remainder = this.remainder.trim(); 109 | var checkTypes = [ 'empty', 'punctuation', 'variable', 'string', 'alphanumeric' ]; 110 | var foundType = false; 111 | 112 | for ( var i = 0; i < checkTypes.length; ++i ) { 113 | var type = checkTypes[ i ]; 114 | var method = 'consume' + type[ 0 ].toUpperCase() + type.substring( 1 ); 115 | if ( this[ method ]() ) { 116 | foundType = true; 117 | break; 118 | } 119 | } 120 | 121 | if ( !foundType ) { 122 | this.current = null; 123 | this.type = "eof"; 124 | } 125 | }; 126 | 127 | function isUppercase( c ) { 128 | var code = c.charCodeAt( 0 ); 129 | return 65 <= code && code <= 90; 130 | } 131 | 132 | function isAlphanum( c ) { 133 | var code = c.charCodeAt( 0 ); 134 | return ( 48 <= code && code <= 57 ) || // number 135 | ( 65 <= code && code <= 90 ) || // uppercase 136 | ( 97 <= code && code <= 122 ); // lowercase 137 | } 138 | 139 | -------------------------------------------------------------------------------- /src/types/atom.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = Atom; 2 | 3 | function Atom( name ) { 4 | this.name = name; 5 | this.type = "Atom"; 6 | } 7 | 8 | Atom.prototype.toString = function () { 9 | return "" + this.name; 10 | }; 11 | -------------------------------------------------------------------------------- /src/types/body.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = Body; 2 | 3 | function Body( list ) { 4 | this.list = list; 5 | } 6 | -------------------------------------------------------------------------------- /src/types/rule.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = Rule; 2 | 3 | function Rule( head, body ) { 4 | this.head = head; 5 | this.body = body; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/term.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = Term; 2 | 3 | function Term( head, list, exclude ) { 4 | this.name = head; 5 | this.type = "Term"; 6 | this.exclude = exclude; 7 | 8 | this.partlist = list.slice(); 9 | this.partlist.stringify = function() { 10 | var i, str = ""; 11 | for ( i = 0; i < this.length; i++ ) { 12 | str += this[ i ]; 13 | if ( i < this.length - 1 ) { 14 | str += ", "; 15 | } 16 | } 17 | 18 | return str; 19 | }; 20 | } 21 | 22 | Term.prototype.toString = function() { 23 | var str = ""; 24 | if ( this.name == "cons" ) { 25 | var x = this; 26 | while ( x.type == "Term" && x.name == "cons" && x.partlist.length == 2 ) { 27 | x = x.partlist[ 1 ]; 28 | } 29 | if ( ( x.type == "Atom" && x.name == "nil" ) || x.type == "Variable" ) { 30 | x = this; 31 | str += "["; 32 | var com = false; 33 | while ( x.type == "Term" && x.name == "cons" && x.partlist.length == 2 ) { 34 | if ( com ) { 35 | str += ", "; 36 | } 37 | str += x.partlist[ 0 ]; 38 | x = x.partlist[ 1 ]; 39 | } 40 | if ( x.type == "Variable" ) { 41 | str += " | " + x; 42 | } 43 | return str + "]"; 44 | } 45 | } 46 | 47 | return this.name + "(" + this.partlist.stringify() + ")"; 48 | }; 49 | -------------------------------------------------------------------------------- /src/types/variable.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = Variable; 2 | 3 | function Variable( name ) { 4 | this.name = name; 5 | this.type = "Variable"; 6 | } 7 | --------------------------------------------------------------------------------