├── .gitignore ├── .vscode └── settings.json ├── README.md ├── package.json ├── src ├── jsprolog.ts ├── prologAST.ts ├── prologParser.ts ├── prologParserSpec.ts ├── prologSolver.ts └── prologSolverSpec.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.js.map 2 | src/**/*.js 3 | specs/**/*.js 4 | node_modules 5 | dist 6 | 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "**/.git": true, 5 | "**/.DS_Store": true, 6 | "**/*.map": true, 7 | "**/*.js": { 8 | "when": "$(basename).ts" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsprolog 2 | 3 | ### What is it? 4 | 5 | It's a simple Prolog interpreter written with TypeScript. 6 | I've adapted it from [Jan's javascript Prolog interpreter](http://ioctl.org/logic/prolog-latest) because I needed a convenient automated theorem proving engine in JavaScript. 7 | 8 | ### What it can do 9 | It can solve simple stuff, for instance N-queen problem. It supports tail recursion. 10 | 11 | ### Limitations 12 | - Parser doesn't support operators. Use function call notation: `=(X, Y), is(Z, +(1, X))`. 13 | - It has a **very** limited set of built-in predicates. 14 | - Current implementation is slow. Finding all solutions of N-queen problem takes about 25 seconds on Intel i5-3570. 15 | 16 | ### Preliminary string support 17 | Jsprolog features rudimental support for strings: 18 | 19 | - Double quoted text is parsed as a list of character codes. 20 | - Use `\"` to mask double quote (NB: remember to escape `\` as well in javascript strings: `"looks \\\"pretty\\\""` ) 21 | - No built-in predicates to work with strings. 22 | - Will be replaced with string type in future. 23 | 24 | ### How to use 25 | ``` 26 | npm i jsprolog 27 | ``` 28 | 29 | *Please note that the project is far from stabilization and the API will surely change at some point in the future.* 30 | 31 | ```javascript 32 | var Prolog = require('jsprolog'); 33 | var db = Prolog.Parser.parse("member(X,[X|R]). member(X, [Y | R]) :- member(X, R)."), 34 | query = Prolog.Parser.parseQuery("member(X,[a,b,c]),member(Y,[1,2,3])."), 35 | iter = Prolog.Solver.query(db, query); 36 | while(iter.next()){ 37 | console.log("X = ", iter.current.X, ", Y = ", iter.current.Y); 38 | } 39 | ``` 40 | 41 | Also refer to specs/prologSolverSpec.js for usage examples. 42 | 43 | ### Supported built-in predicates 44 | 45 | Predicate | Notes 46 | ----------| ------------------------------------------------- 47 | =/2 | Doesn't support cyclic terms. 48 | !/0 | 49 | fail/0 | 50 | call/1 | 51 | findall/3 | 52 | is/2 | Supports only +,-,/,*. Silently fails on error. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsprolog", 3 | "version": "0.2.2", 4 | "description": "A simple Prolog interpreter written with javascript (ECMAScript5).", 5 | "keywords": [ 6 | "prolog" 7 | ], 8 | "homepage": "https://github.com/sleepyowl/jsprolog", 9 | "bugs": "https://github.com/sleepyowl/jsprolog/issues", 10 | "license": "MIT", 11 | "files": [ 12 | "dist" 13 | ], 14 | "main": "dist/jsprolog.js", 15 | "typings": "dist/jsprolog.d.ts", 16 | "author": { 17 | "name": "Dmitry Soloviev", 18 | "email": "dmitry.soloviev@outlook.com" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/sleepyowl/jsprolog.git" 23 | }, 24 | "dependencies": {}, 25 | "devDependencies": { 26 | "@types/chai": "^4.0.0", 27 | "@types/mocha": "^2.2.41", 28 | "chai": "^3.5.0", 29 | "mocha": "^2.4.5", 30 | "tslint": "^3.6.0", 31 | "typescript": "^2.0.0" 32 | }, 33 | "scripts": { 34 | "prepublish": "./node_modules/.bin/tsc", 35 | "test": "./node_modules/.bin/tsc && mocha ./dist/*Spec.js" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/jsprolog.ts: -------------------------------------------------------------------------------- 1 | import * as AST from './prologAST'; 2 | import * as Parser from './prologParser'; 3 | import * as Solver from './prologSolver'; 4 | 5 | export default {AST,Parser,Solver}; 6 | 7 | -------------------------------------------------------------------------------- /src/prologAST.ts: -------------------------------------------------------------------------------- 1 | export enum PartType { 2 | Variable, 3 | Atom, 4 | Term 5 | } 6 | 7 | export abstract class Part { 8 | /** 9 | * @class Part 10 | * @classdesc Part := Variable(name) | Atom(name) | Term(name, partlist) 11 | * @param {string} name Name of the variable/atom/term 12 | */ 13 | constructor(public name: string) { } 14 | /** 15 | * Type of the 16 | * @member Part.type 17 | */ 18 | type: PartType; 19 | toString() { 20 | return this.name; 21 | } 22 | } 23 | 24 | export class Variable extends Part { 25 | constructor(name: string) { 26 | super(name); 27 | } 28 | } 29 | Variable.prototype.type = PartType.Variable; // TODO: verify if it's faster than instanceof checks 30 | 31 | export class Atom extends Part { 32 | constructor(head: string) { 33 | super(head); 34 | } 35 | static Nil = new Atom(null); 36 | } 37 | Atom.prototype.type = PartType.Atom; // TODO: verify if it's faster than instanceof checks 38 | 39 | /** 40 | * Term(name, list) 41 | */ 42 | export class Term extends Part { 43 | partlist: Partlist 44 | constructor(head: string, list: Part[]) { 45 | super(head); 46 | this.partlist = new Partlist(list); 47 | } 48 | toString() { 49 | var result = ""; 50 | if (this.name == "cons") { 51 | var x: (Atom | Term | Variable) = this; 52 | 53 | while (x instanceof Term && x.name == "cons" && x.partlist.list.length == 2) { 54 | x = (x).partlist.list[1]; 55 | } 56 | 57 | if ((x === Atom.Nil) || x instanceof Variable) { 58 | x = this; 59 | result += "["; 60 | var com = false; 61 | while (x.type == PartType.Term && x.name == "cons" && (x).partlist.list.length == 2) { 62 | if (com) { 63 | result += ", "; 64 | } 65 | result += (x).partlist.list[0].toString(); 66 | com = true; 67 | x = (x).partlist.list[1]; 68 | } 69 | if (x.type == PartType.Variable) { 70 | result += " | "; 71 | } 72 | result += "]"; 73 | return result; 74 | } else { 75 | result += `ERROR: unexpected atom: ${x.toString()}`; 76 | } 77 | } 78 | result += `${this.name}(${this.partlist.toString()})`; 79 | return result; 80 | }; 81 | } 82 | Term.prototype.type = PartType.Term; // TODO: verify if it's faster than instanceof checks 83 | 84 | 85 | export class Partlist { 86 | constructor(public list: Part[]) { } 87 | toString() { 88 | return this.list.map(function(e) { return e.toString(); }).join(", "); 89 | } 90 | } 91 | 92 | /** 93 | * Rule(head, bodylist): Part(head), [:- Body(bodylist)]. 94 | */ 95 | export class Rule { 96 | body: Partlist 97 | get isFact() { 98 | return !this.body; 99 | } 100 | constructor(public head: Part, bodylist?: Part[]) { 101 | this.body = bodylist && new Partlist(bodylist); 102 | } 103 | toString() { 104 | return this.head.toString() + (this.body ? " :- " + this.body.toString() + "." : "."); 105 | } 106 | } 107 | 108 | export function listOfArray(array, cdr?: Part) { 109 | cdr = cdr || Atom.Nil; 110 | for (var i = array.length, car; car = array[--i];) { 111 | cdr = new Term("cons", [car, cdr]); 112 | } 113 | return cdr; 114 | } 115 | -------------------------------------------------------------------------------- /src/prologParser.ts: -------------------------------------------------------------------------------- 1 | import { Part, Variable, Atom, Term, Partlist, Rule, listOfArray } from './prologAST'; 2 | 3 | /** 4 | * Parses the DB 5 | */ 6 | export function parse(string) { 7 | var tk = new Tokeniser(string), 8 | rules = []; 9 | 10 | while (tk.current != null) { 11 | rules.push(parseRule(tk)); 12 | } 13 | 14 | return rules; 15 | } 16 | 17 | export function parseQuery(string) { 18 | var tk = new Tokeniser(string); 19 | return new Partlist(parseBody(tk)); 20 | } 21 | 22 | export default { parse, parseQuery }; 23 | 24 | ////////////////////////////////////////////////////////////////////// 25 | // TODO: lexer error handling 26 | 27 | declare const enum TokenType { 28 | Punc, 29 | Var, 30 | Id, 31 | String, 32 | EOF 33 | } 34 | 35 | function trimQuotes(x: string) { return x.substr(1, x.length - 2); } 36 | 37 | var tokenizerRules = [ 38 | [/^([\(\)\.,\[\]\|]|\:\-)/, TokenType.Punc], 39 | [/^([A-Z_][a-zA-Z0-9_]*)/, TokenType.Var], 40 | [/^('[^']*?')/, TokenType.Id, trimQuotes], 41 | [/^("(?:[^"\\]|\\.)*?")/, TokenType.String, trimQuotes], 42 | [/^([a-z][a-zA-Z0-9_]*)/, TokenType.Id], 43 | [/^(-?\d+(\.\d+)?)/, TokenType.Id, function (x) { return +x; }], 44 | [/^(\+|\-|\*|\/|\=|\!)/, TokenType.Id] 45 | ]; 46 | 47 | class Tokeniser { 48 | remainder: string 49 | current: string 50 | accepted: string 51 | type: TokenType 52 | 53 | constructor(source: string) { 54 | this.remainder = source; 55 | this.current = null; 56 | this.type = null; // "eof", TokenType.Id, TokenType.Var, TokenType.Punc etc. 57 | this.consume(); // Load up the first token. 58 | } 59 | consume() { 60 | if (this.type == TokenType.EOF) return; 61 | 62 | // Eat any leading WS and %-style comments 63 | var r = this.remainder.match(/^(\s+|([%].*)[\n\r]+)*/); 64 | if (r) { 65 | this.remainder = this.remainder.substring(r[0].length); 66 | } 67 | 68 | if (!this.remainder.length) { 69 | this.current = null; 70 | this.type = TokenType.EOF; 71 | return; 72 | } 73 | 74 | for (var i = 0, rule; rule = tokenizerRules[i++];) { 75 | if (r = this.remainder.match(rule[0])) { 76 | this.remainder = this.remainder.substring(r[0].length); 77 | this.type = rule[1]; 78 | this.current = typeof (rule[2]) === "function" ? rule[2](r[1]) : r[1]; 79 | return; 80 | } 81 | } 82 | 83 | throw "Unexpected tokenizer input"; 84 | } 85 | accept(type: TokenType, symbol?: string) { 86 | if (this.type === type && (typeof (symbol) === "undefined" || this.current === symbol)) { 87 | this.accepted = this.current; 88 | this.consume(); 89 | return true; 90 | } 91 | return false; 92 | } 93 | 94 | expect(type: TokenType, symbol?: string) { 95 | if (!this.accept(type, symbol)) { 96 | throw this.type === TokenType.EOF ? "Syntax error: unexpected end of file" : `Syntax error: unexpected token ${this.current}`; 97 | } 98 | return true; // TODO: no need for boolean? 99 | } 100 | } 101 | 102 | ////////////////////////////////////////////////////////////////////// 103 | 104 | function parseRule(tk: Tokeniser) { 105 | // Rule := Term . | Term :- PartList . 106 | 107 | var h = parseTerm(tk); 108 | 109 | if (tk.accept(TokenType.Punc, ".")) { 110 | return new Rule(h); 111 | } 112 | 113 | tk.expect(TokenType.Punc, ":-"); 114 | var b = parseBody(tk); 115 | 116 | return new Rule(h, b); 117 | } 118 | 119 | function parseTerm(tk: Tokeniser) {// Term -> id ( optParamList ) 120 | tk.expect(TokenType.Id); 121 | var name = tk.accepted; 122 | 123 | // accept fail and ! w/o () 124 | if (tk.current != "(" && (name == "fail" || name === "!")) { 125 | return new Term(name, []); 126 | } 127 | 128 | tk.expect(TokenType.Punc, "("); 129 | 130 | var p = []; 131 | while (tk.current !== "eof") { 132 | p.push(parsePart(tk)); 133 | 134 | if (tk.accept(TokenType.Punc, ")")) { 135 | break; 136 | } 137 | 138 | tk.expect(TokenType.Punc, ","); 139 | } 140 | 141 | return new Term(name, p); 142 | } 143 | 144 | function parsePart(tk: Tokeniser) { 145 | // Part -> var | id | id(optParamList) 146 | // Part -> [ listBit ] ::-> cons(...) 147 | if (tk.accept(TokenType.Var)) { 148 | return new Variable(tk.accepted); 149 | } 150 | 151 | // Parse a list (syntactic sugar goes here) 152 | if (tk.accept(TokenType.Punc, "[")) { 153 | return parseList(tk); 154 | } 155 | 156 | // Parse a string 157 | if (tk.accept(TokenType.String)) { 158 | return parseString(tk); 159 | } 160 | 161 | tk.expect(TokenType.Id); 162 | var name = tk.accepted; 163 | 164 | if (!tk.accept(TokenType.Punc, "(")) { 165 | return new Atom(name); 166 | } 167 | 168 | var p = []; 169 | while (tk.type !== TokenType.EOF) { 170 | p.push(parsePart(tk)); 171 | 172 | if (tk.accept(TokenType.Punc, ")")) { 173 | break; 174 | } 175 | 176 | tk.expect(TokenType.Punc, ","); 177 | } 178 | 179 | return new Term(name, p); 180 | } 181 | 182 | const escapes = { 183 | 'b': "\b".charCodeAt(0), 184 | 'r': "\r".charCodeAt(0), 185 | 'n': "\n".charCodeAt(0), 186 | 'f': "\f".charCodeAt(0), 187 | 't': "\t".charCodeAt(0), 188 | 'v': "\v".charCodeAt(0), 189 | "'": "'".charCodeAt(0), 190 | '"': '"'.charCodeAt(0), 191 | "\\": "\\".charCodeAt(0) 192 | }; 193 | 194 | function parseString(tk: Tokeniser) { 195 | const str = tk.accepted; 196 | const arr = []; 197 | 198 | for (let i = 0; i < str.length; ++i) { 199 | let code = str.charCodeAt(i); 200 | 201 | if(code === 92) { 202 | // we expect string to be valid 203 | // no checks here 204 | code = escapes[str[++i]]; 205 | if(typeof code === "undefined") { 206 | throw `Unexpected backslash sequence \\${str[i]}`; 207 | } 208 | } 209 | 210 | arr.push(new Atom(code.toString())); 211 | } 212 | return listOfArray(arr, Atom.Nil); 213 | } 214 | 215 | function parseList(tk: Tokeniser) { 216 | // empty list 217 | if (tk.accept(TokenType.Punc, "]")) { 218 | return Atom.Nil; 219 | } 220 | 221 | // Get a list of parts into l 222 | var l = []; 223 | 224 | while (tk.current !== "eof") { 225 | l.push(parsePart(tk)); 226 | if (!tk.accept(TokenType.Punc, ",")) { 227 | break; 228 | } 229 | } 230 | 231 | // Find the end of the list ... "| Var ]" or "]". 232 | var append; 233 | if (tk.accept(TokenType.Punc, "|")) { 234 | tk.expect(TokenType.Var); 235 | append = new Variable(tk.accepted); 236 | } else { 237 | append = Atom.Nil; 238 | } 239 | tk.expect(TokenType.Punc, "]"); 240 | 241 | //// Construct list 242 | //for (var i = l.length; i--;) { 243 | // append = new Term("cons", [l[i], append]); 244 | //} 245 | 246 | return listOfArray(l, append); 247 | } 248 | 249 | function parseBody(tk: Tokeniser) {// Body -> Term {, Term...} 250 | var terms = []; 251 | 252 | while (tk.current !== "eof") { 253 | terms.push(parseTerm(tk)); 254 | if (tk.accept(TokenType.Punc, ".")) { 255 | break; 256 | } else { 257 | tk.expect(TokenType.Punc, ","); 258 | } 259 | } 260 | 261 | return terms; 262 | } 263 | 264 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /src/prologParserSpec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from "chai"; 2 | 3 | import * as prologAST from './prologAST'; 4 | import * as prologParser from './prologParser'; 5 | 6 | describe("prolog parser", function () { 7 | it("throws on syntax errors", function () { 8 | expect(function () { prologParser.parse("rule(X) :- :- check(X)."); }).to.throw("Syntax error: unexpected token :-"); 9 | expect(function () { prologParser.parse("x."); }).to.throw("Syntax error: unexpected token ."); 10 | expect(function () { prologParser.parse("fact([a,b,c"); }).to.throw("Syntax error: unexpected end of file"); 11 | }); 12 | 13 | it("can parse simple term", function () { 14 | var db = "adjacent(x,y)."; 15 | var rules = prologParser.parse(db); 16 | expect(rules).to.be.ok; 17 | expect(rules.length).to.eq(1); 18 | expect(rules[0] instanceof prologAST.Rule).to.be.ok; 19 | expect(rules[0].head instanceof prologAST.Term).to.be.ok; 20 | expect(rules[0].head.name).to.eq("adjacent"); 21 | expect(rules[0].head.partlist instanceof prologAST.Partlist).to.be.ok; 22 | expect(rules[0].head.partlist.list instanceof Array).to.be.ok; 23 | expect(rules[0].head.partlist.list.length).to.eq(2); 24 | expect(rules[0].head.partlist.list[0] instanceof prologAST.Atom).to.be.ok; 25 | expect(rules[0].head.partlist.list[1] instanceof prologAST.Atom).to.be.ok; 26 | expect(rules[0].head.partlist.list[0].name).to.eq("x"); 27 | expect(rules[0].head.partlist.list[1].name).to.eq("y"); 28 | }); 29 | 30 | it("parses integer numbers", function () { 31 | var db = "val(10)."; 32 | var rules = prologParser.parse(db); 33 | expect(rules).to.be.ok; 34 | expect(rules.length).to.eq(1); 35 | expect(rules[0] instanceof prologAST.Rule).to.be.ok; 36 | expect(rules[0].head instanceof prologAST.Term).to.be.ok; 37 | expect(rules[0].head.name).to.eq("val"); 38 | expect(rules[0].head.partlist instanceof prologAST.Partlist).to.be.ok; 39 | expect(rules[0].head.partlist.list instanceof Array).to.be.ok; 40 | expect(rules[0].head.partlist.list.length).to.eq(1); 41 | expect(rules[0].head.partlist.list[0] instanceof prologAST.Atom).to.be.ok; 42 | expect(rules[0].head.partlist.list[0].name).to.eq(10); 43 | }); 44 | 45 | it("parses decimal numbers with fractional part", function () { 46 | var db = "val(3.14)."; 47 | var rules = prologParser.parse(db); 48 | expect(rules).to.be.ok; 49 | expect(rules.length).to.eq(1); 50 | expect(rules[0] instanceof prologAST.Rule).to.be.ok; 51 | expect(rules[0].head instanceof prologAST.Term).to.be.ok; 52 | expect(rules[0].head.name).to.eq("val"); 53 | expect(rules[0].head.partlist instanceof prologAST.Partlist).to.be.ok; 54 | expect(rules[0].head.partlist.list instanceof Array).to.be.ok; 55 | expect(rules[0].head.partlist.list.length).to.eq(1); 56 | expect(rules[0].head.partlist.list[0] instanceof prologAST.Atom).to.be.ok; 57 | expect(rules[0].head.partlist.list[0].name).to.eq(3.14); 58 | }); 59 | 60 | it("can parse simple rule", function () { 61 | var db = "parent(X,Y):-child(Y,X),organism(X),organism(Y)."; 62 | var rules = prologParser.parse(db); 63 | 64 | expect(rules.length).to.eq(1); 65 | expect(rules[0] instanceof prologAST.Rule).to.be.ok; 66 | 67 | expect(rules[0].head instanceof prologAST.Term).to.be.ok; 68 | expect(rules[0].head.name).to.eq("parent"); 69 | expect(rules[0].head.partlist instanceof prologAST.Partlist).to.be.ok; 70 | expect(rules[0].head.partlist.list instanceof Array).to.be.ok; 71 | expect(rules[0].head.partlist.list.length).to.eq(2); 72 | expect(rules[0].head.partlist.list[0] instanceof prologAST.Variable).to.be.ok; 73 | expect(rules[0].head.partlist.list[1] instanceof prologAST.Variable).to.be.ok; 74 | expect(rules[0].head.partlist.list[0].name).to.eq("X"); 75 | expect(rules[0].head.partlist.list[1].name).to.eq("Y"); 76 | 77 | expect(rules[0].body instanceof prologAST.Partlist).to.be.ok; 78 | expect(rules[0].body.list.length).to.eq(3); 79 | expect(rules[0].body.list[0] instanceof prologAST.Term).to.be.ok; 80 | expect(rules[0].body.list[1] instanceof prologAST.Term).to.be.ok; 81 | expect(rules[0].body.list[2] instanceof prologAST.Term).to.be.ok; 82 | 83 | expect(rules[0].body.list[0].name).to.eq("child"); 84 | expect(rules[0].body.list[0].partlist instanceof prologAST.Partlist).to.be.ok; 85 | expect(rules[0].body.list[0].partlist.list.length).to.eq(2); 86 | expect(rules[0].body.list[0].partlist.list[0] instanceof prologAST.Variable).to.be.ok; 87 | expect(rules[0].body.list[0].partlist.list[1] instanceof prologAST.Variable).to.be.ok; 88 | expect(rules[0].body.list[0].partlist.list[0].name).to.eq("Y"); 89 | expect(rules[0].body.list[0].partlist.list[1].name).to.eq("X"); 90 | 91 | expect(rules[0].body.list[1].name).to.eq("organism"); 92 | expect(rules[0].body.list[1].partlist.list[0].name).to.eq("X"); 93 | 94 | expect(rules[0].body.list[2].name).to.eq("organism"); 95 | expect(rules[0].body.list[2].partlist.list[0].name).to.eq("Y"); 96 | 97 | }); 98 | 99 | it("parses several rules", function () { 100 | var db = "parent(X,Y):-child(Y,X). sibling(X,Y):-child(X,Z),child(Y,Z)."; 101 | var rules = prologParser.parse(db); 102 | expect(rules.length).to.eq(2); 103 | 104 | expect(rules[0] instanceof prologAST.Rule).to.be.ok; 105 | expect(rules[0].head instanceof prologAST.Term).to.be.ok; 106 | expect(rules[0].head.name).to.eq("parent"); 107 | 108 | expect(rules[1] instanceof prologAST.Rule).to.be.ok; 109 | expect(rules[1].head instanceof prologAST.Term).to.be.ok; 110 | expect(rules[1].head.name).to.eq("sibling"); 111 | }); 112 | 113 | it("parses ! and fail", function () { 114 | var db = "not(Term):-call(Term),!,fail. not(Term)."; 115 | var rules = prologParser.parse(db); 116 | expect(rules.length).to.eq(2); 117 | expect(rules[0].body.list[1].name).to.eq("!"); 118 | expect(rules[0].body.list[1].partlist.list.length).to.eq(0); 119 | expect(rules[0].body.list[2].name).to.eq("fail"); 120 | expect(rules[0].body.list[2].partlist.list.length).to.eq(0); 121 | }); 122 | 123 | it("handles new lines and tabs properly", function () { 124 | var db = "parent(X,Y):-\nchild(Y,X).\nsibling(X,Y)\n:-\n\tchild(X,Z),child(Y,Z)."; 125 | var rules = prologParser.parse(db); 126 | expect(rules.length).to.eq(2); 127 | 128 | expect(rules[0] instanceof prologAST.Rule).to.be.ok; 129 | expect(rules[0].head instanceof prologAST.Term).to.be.ok; 130 | expect(rules[0].head.name).to.eq("parent"); 131 | 132 | expect(rules[1] instanceof prologAST.Rule).to.be.ok; 133 | expect(rules[1].head instanceof prologAST.Term).to.be.ok; 134 | expect(rules[1].head.name).to.eq("sibling"); 135 | }); 136 | 137 | it("handles line comments properly", function () { 138 | var db = "parent(X,Y):-\nchild(Y,X).\n % a comment\n% on the next line too\n\n% and skipping a line comment\nsibling(X,Y)\n:-\n\n\tchild(X,Z),child(Y,Z)."; 139 | var rules = prologParser.parse(db); 140 | expect(rules.length).to.eq(2); 141 | 142 | expect(rules[0] instanceof prologAST.Rule).to.be.ok; 143 | expect(rules[0].head instanceof prologAST.Term).to.be.ok; 144 | expect(rules[0].head.name).to.eq("parent"); 145 | 146 | expect(rules[1] instanceof prologAST.Rule).to.be.ok; 147 | expect(rules[1].head instanceof prologAST.Term).to.be.ok; 148 | expect(rules[1].head.name).to.eq("sibling"); 149 | }); 150 | 151 | it("parses lists in format [a,b,c] correctly", function () { 152 | var db = "fact([a,b,c])."; 153 | var rules = prologParser.parse(db); 154 | expect(rules.length).to.eq(1); 155 | expect(rules[0] instanceof prologAST.Rule).to.be.ok; 156 | expect(rules[0].head instanceof prologAST.Term).to.be.ok; 157 | expect(rules[0].head.name).to.eq("fact"); 158 | 159 | var cons = rules[0].head.partlist.list[0]; 160 | expect(cons.name).to.eq("cons"); 161 | expect(cons.partlist.list[0].name).to.eq("a"); 162 | 163 | cons = cons.partlist.list[1]; 164 | expect(cons.name).to.eq("cons"); 165 | expect(cons.partlist.list[0].name).to.eq("b"); 166 | 167 | cons = cons.partlist.list[1]; 168 | expect(cons.name).to.eq("cons"); 169 | expect(cons.partlist.list[0].name).to.eq("c"); 170 | 171 | cons = cons.partlist.list[1]; 172 | expect(cons).to.eq(prologAST.Atom.Nil); 173 | }); 174 | 175 | it("parses lists in format [a,b|X] correctly", function () { 176 | var db = "fact([a,b|X])."; 177 | var rules = prologParser.parse(db); 178 | expect(rules.length).to.eq(1); 179 | expect(rules[0] instanceof prologAST.Rule).to.be.ok; 180 | expect(rules[0].head instanceof prologAST.Term).to.be.ok; 181 | expect(rules[0].head.name).to.eq("fact"); 182 | 183 | var cons = rules[0].head.partlist.list[0]; 184 | expect(cons.name).to.eq("cons"); 185 | expect(cons.partlist.list[0].name).to.eq("a"); 186 | 187 | cons = cons.partlist.list[1]; 188 | expect(cons.name).to.eq("cons"); 189 | expect(cons.partlist.list[0].name).to.eq("b"); 190 | 191 | cons = cons.partlist.list[1]; 192 | expect(cons instanceof prologAST.Variable).to.be.ok; 193 | expect(cons.name).to.eq("X"); 194 | }); 195 | 196 | it("parses several double quoted strings correctly", function () { 197 | var db = `fact("sådan er det \\"bare\\"", 10, "second string \\'\\\\\\r\\t\\n\\v\\b").`; 198 | var rules = prologParser.parse(db); 199 | expect(rules.length).to.eq(1); 200 | expect(rules[0] instanceof prologAST.Rule).to.be.ok; 201 | expect(rules[0].head instanceof prologAST.Term).to.be.ok; 202 | expect(rules[0].head.name).to.eq("fact"); 203 | 204 | const s2a = (s:string) => `[${s.split("").map(x => x.charCodeAt(0)).join(", ")}]`; 205 | const str = rules[0].head.partlist.list[0]; 206 | const str2 = rules[0].head.partlist.list[2]; 207 | expect(str.name).to.eq("cons"); 208 | expect(str.toString()).to.eq(s2a("sådan er det \"bare\"")); 209 | 210 | expect(str2.name).to.eq("cons"); 211 | expect(str2.toString()).to.eq(s2a("second string \'\\\r\t\n\v\b")); 212 | }); 213 | }); -------------------------------------------------------------------------------- /src/prologSolver.ts: -------------------------------------------------------------------------------- 1 | import {Part, Variable, Atom, Term, Partlist, Rule, listOfArray} from './prologAST'; 2 | 3 | 4 | export var options = { 5 | maxIterations: null, 6 | experimental: { 7 | tailRecursion: false 8 | } 9 | }; 10 | 11 | /** 12 | * executes a query agains the database 13 | * @param db compiled rule database 14 | * @param query compiled query 15 | * @returns iterator to iterate through results 16 | */ 17 | export function query(rulesDB, query) { 18 | var vars = varNames(query.list), 19 | cdb = {}; 20 | 21 | // maybe move to parser level, idk 22 | for (var i = 0, name, rule; i < rulesDB.length; i++) { 23 | rule = rulesDB[i]; 24 | name = rule.head.name; 25 | if (name in cdb) { 26 | cdb[name].push(rule); 27 | } else { 28 | cdb[name] = [rule]; 29 | } 30 | } 31 | 32 | var iterator = new Iterator(); 33 | 34 | var cont = getdtreeiterator(query.list, cdb, function (bindingContext) { 35 | var result = {}; 36 | for (var i = 0, v; v = vars[i++];) { 37 | result[v.name] = termToJsValue(bindingContext.value(v)); 38 | } 39 | iterator.current = result; 40 | }); 41 | 42 | 43 | Iterator.prototype.next = function () { 44 | var i = 0; 45 | this.current = null; 46 | while (cont != null && !this.current) { 47 | cont = cont(); 48 | if (typeof (options.maxIterations) === "number" && options.maxIterations <= ++i) { 49 | throw "iteration limit reached"; 50 | } 51 | } 52 | 53 | return !!this.current; 54 | }; 55 | 56 | return iterator; 57 | function Iterator() { } 58 | }; 59 | 60 | 61 | /** 62 | * Get a list of all variables mentioned in a list of Terms. 63 | */ 64 | function varNames(list) { 65 | var out = [], vars = {}, t, n; 66 | list = list.slice(0); // clone 67 | while (list.length) { 68 | t = list.pop(); 69 | if (t instanceof Variable) { 70 | n = t.name; 71 | // ignore special variable _ 72 | // push only new names 73 | if (n !== "_" && out.indexOf(n) === -1) { 74 | out.push(n); 75 | vars[n] = t; 76 | } 77 | } else if (t instanceof Term) { 78 | // we don't care about tree walk order 79 | Array.prototype.push.apply(list, t.partlist.list); 80 | } 81 | } 82 | 83 | return out.map(function (name) { return vars[name]; }); 84 | } 85 | 86 | var builtinPredicates = { 87 | "!/0": function (loop, goals, idx, bindingContext, fbacktrack) { 88 | var nextgoals = goals.slice(1); // cut always succeeds 89 | return loop(nextgoals, 0, new BindingContext(bindingContext), function () { 90 | return fbacktrack && fbacktrack(true, goals[0].parent); 91 | }); 92 | }, 93 | "fail/0": function (loop, goals, idx, bindingContext, fbacktrack) { 94 | return fbacktrack; // FAIL 95 | }, 96 | "call/1": function (loop, goals, idx, bindingContext, fbacktrack) { 97 | var first = bindingContext.value(goals[0].partlist.list[0]); 98 | if (!(first instanceof Term)) { 99 | return fbacktrack; // FAIL 100 | } 101 | 102 | var ng = goals.slice(0); 103 | ng[0] = first; 104 | (first as any).parent = goals[0]; 105 | 106 | return loop(ng, 0, bindingContext, fbacktrack); 107 | }, 108 | "=/2": function (loop, goals, idx, bindingContext, fbacktrack) { 109 | var ctx = new BindingContext(bindingContext); 110 | if (ctx.unify(goals[0].partlist.list[0], goals[0].partlist.list[1])) { 111 | return loop(goals.slice(1), 0, ctx, fbacktrack); 112 | } else { 113 | return fbacktrack; // FAIL 114 | } 115 | }, 116 | "findall/3": function (loop, goals, idx, bindingContext, fbacktrack, db) { // TODO: refactor rule db passing 117 | var args = goals[0].partlist.list, 118 | results = []; 119 | 120 | return getdtreeiterator([args[1]], db, collect, bindingContext, report); 121 | function collect(ctx) { 122 | results.push(ctx.value(args[0])); 123 | } 124 | function report() { 125 | var result = listOfArray(results); 126 | if (bindingContext.unify(args[2], result)) { 127 | return loop(goals.slice(1), 0, bindingContext, fbacktrack); 128 | } else { 129 | return fbacktrack; 130 | } 131 | } 132 | }, 133 | "is/2": function (loop, goals, idx, bindingContext, fbacktrack) { 134 | var args = goals[0].partlist.list, 135 | expression = bindingContext.value(args[1]), 136 | ctx = new BindingContext(bindingContext); 137 | 138 | if (varNames([expression]).length) { 139 | return fbacktrack; // TODO: prolog exception "ERROR: is/2: Arguments are not sufficiently instantiated" 140 | } 141 | 142 | // build evaluation queue: 143 | var queue = [expression], acc = [], c, i, x, l; 144 | 145 | while (queue.length) { 146 | x = queue.pop(); 147 | acc.push(x); 148 | if (x instanceof Term) { 149 | Array.prototype.push.apply(queue, x.partlist.list); 150 | } 151 | } 152 | 153 | // evaluate 154 | queue = acc; 155 | acc = []; 156 | i = queue.length; 157 | while (i--) { 158 | x = queue[i]; 159 | if (x instanceof Term) { 160 | c = x.partlist.list.length; 161 | l = acc.splice(-c, c); 162 | 163 | switch (x.name) { 164 | case "+": 165 | acc.push(l[0] + l[1]); 166 | break; 167 | case "-": 168 | acc.push(l[0] - l[1]); 169 | break; 170 | case "*": 171 | acc.push(l[0] * l[1]); 172 | break; 173 | case "/": 174 | acc.push(l[0] / l[1]); 175 | break; 176 | default: 177 | return fbacktrack;// TODO: prolog exception "ERROR: is/2: Arithmetic: `{x.name}' is not a function" 178 | } 179 | } else { 180 | if (typeof (x.name) === "number") { 181 | acc.push(x.name); 182 | } else { 183 | // TODO: handle functions like pi e etc 184 | return fbacktrack; 185 | } 186 | } 187 | } 188 | 189 | if (ctx.unify(args[0], new Atom(acc[0]))) { 190 | return loop(goals.slice(1), 0, ctx, fbacktrack); 191 | } else { 192 | return fbacktrack; 193 | } 194 | 195 | } 196 | }; 197 | 198 | /** 199 | * The main proving engine 200 | * @param originalGoals original goals to prove 201 | * @param rulesDB prolog database to consult with 202 | * @param fsuccess success callback 203 | * @returns a function to perform next step 204 | */ 205 | function getdtreeiterator(originalGoals, rulesDB, fsuccess, rootBindingContext?, rootBacktrack?) { 206 | "use strict"; 207 | var tailEnabled = options.experimental.tailRecursion; 208 | return function () { return loop(originalGoals, 0, rootBindingContext || null, rootBacktrack || null); }; 209 | 210 | // main loop continuation 211 | function loop(goals, idx, parentBindingContext, fbacktrack) { 212 | 213 | if (!goals.length) { 214 | fsuccess(parentBindingContext); 215 | return fbacktrack; 216 | } 217 | 218 | var currentGoal = goals[0], 219 | currentBindingContext = new BindingContext(parentBindingContext), 220 | currentGoalVarNames, rule, varMap, renamedHead, nextGoalsVarNames, existing; 221 | 222 | // TODO: add support for builtins with variable arity (like call/2+) 223 | var builtin = builtinPredicates[currentGoal.name + "/" + currentGoal.partlist.list.length]; 224 | if (typeof (builtin) === "function") { 225 | return builtin(loop, goals, idx, currentBindingContext, fbacktrack, rulesDB); 226 | } 227 | 228 | // searching for next matching rule 229 | for (var i = idx, db = rulesDB[currentGoal.name], dblen = db && db.length; i < dblen; i++) { 230 | rule = db[i]; 231 | varMap = {}; 232 | renamedHead = new Term(rule.head.name, currentBindingContext.renameVariables(rule.head.partlist.list, currentGoal, varMap)); 233 | renamedHead.parent = currentGoal.parent; 234 | if (!currentBindingContext.unify(currentGoal, renamedHead)) { 235 | continue; 236 | } 237 | 238 | var nextGoals = goals.slice(1); // current head succeeded 239 | 240 | if (rule.body != null) { 241 | nextGoals = currentBindingContext.renameVariables(rule.body.list, renamedHead, varMap).concat(nextGoals); 242 | } 243 | 244 | // TODO: remove 'free' variables (need to check values as well) 245 | 246 | if (rule.body != null && nextGoals.length === 1) { 247 | // call in a tail position: reusing parent variables 248 | // prevents context groth in some recursive scenarios 249 | if (tailEnabled) { 250 | currentGoalVarNames = varNames([currentGoal]); 251 | nextGoalsVarNames = varNames(nextGoals); 252 | existing = nextGoalsVarNames.concat(currentGoalVarNames).map(function (e) { return e.name; }); 253 | 254 | if (currentGoalVarNames.length === nextGoalsVarNames.length) { 255 | for (var vn in varMap) { 256 | for (var cv, cn, nn, k = currentGoalVarNames.length; k--;) { 257 | cn = currentGoalVarNames[k]; 258 | nn = nextGoalsVarNames[k]; 259 | cv = currentBindingContext.value(cn); 260 | 261 | if (cn.name != nn.name && varMap[vn] === nn) { 262 | // do not short-cut if cn's value references nn 263 | // TODO: probably need to check other variables 264 | if (cv && varNames([cv]).indexOf(nn) !== -1) { 265 | continue; 266 | } 267 | varMap[vn] = cn; 268 | currentBindingContext.ctx[cn.name] = currentBindingContext.ctx[nn.name]; 269 | currentBindingContext.unbind(nn.name); 270 | } 271 | } 272 | } 273 | 274 | // re-rename vars in next goals (can be optimised) 275 | nextGoals = currentBindingContext.renameVariables(rule.body.list, renamedHead, varMap); 276 | } 277 | } 278 | 279 | return function levelDownTail() { 280 | // skipping backtracking to the same level because it's the last goal 281 | // TODO: removing extra stuff from binding context 282 | return loop(nextGoals, 0, currentBindingContext, fbacktrack); 283 | }; 284 | } else { 285 | /// CURRENT BACKTRACK CONTINUATION /// 286 | /// WHEN INVOKED BACKTRACKS TO THE /// 287 | /// NEXT RULE IN THE PREVIOUS LEVEL /// 288 | var fCurrentBT = function (cut, parent) { 289 | if (cut) { 290 | return fbacktrack && fbacktrack(parent.parent !== goals[0].parent, parent); 291 | } else { 292 | return loop(goals, i + 1, parentBindingContext, fbacktrack); 293 | } 294 | }; 295 | return function levelDown() { 296 | return loop(nextGoals, 0, currentBindingContext, fCurrentBT); 297 | }; 298 | } 299 | 300 | } 301 | return fbacktrack; 302 | } 303 | }; 304 | 305 | /** 306 | * helper function to convert terms to result values returned by query function 307 | */ 308 | function termToJsValue(v) { 309 | if (v instanceof Atom) { 310 | return v === Atom.Nil ? [] : v.name; 311 | } 312 | 313 | if (v instanceof Term && v.name === "cons") { 314 | var t = []; 315 | while (v.partlist && v.name !== "nil") { // we're not expecting malformed lists... 316 | t.push(termToJsValue(v.partlist.list[0])); 317 | v = v.partlist.list[1]; 318 | } 319 | return t; 320 | } 321 | 322 | return v.toString(); 323 | } 324 | 325 | 326 | /** 327 | * creates binding context for variables 328 | */ 329 | function BindingContext(parent) { 330 | this.ctx = Object.create(parent && parent.ctx || {}); 331 | } 332 | 333 | /** 334 | * fine-print the context (for debugging purposes) 335 | * ! SLOW because of for-in 336 | */ 337 | BindingContext.prototype.toString = function toString() { 338 | var r = [], p = []; 339 | for (var key in this.ctx) { 340 | Array.prototype.push.call( 341 | Object.prototype.hasOwnProperty.call(this.ctx, key) ? r : p, 342 | key + " = " + this.ctx[key]); 343 | } 344 | return r.join(", ") + " || " + p.join(", "); 345 | }; 346 | 347 | var globalGoalCounter = 0; 348 | 349 | /** 350 | * renames variables to make sure names are unique 351 | * @param list list of terms to rename 352 | * @param parent parent term (parent is used in cut) 353 | * @param varMap (out) map of variable mappings, used to make sure that both head and body have same names 354 | * @returns new term with renamed variables 355 | */ 356 | BindingContext.prototype.renameVariables = function renameVariables(list, parent, varMap) { 357 | var out = [], 358 | queue = [], 359 | stack = [list], 360 | clen, 361 | tmp, 362 | v; 363 | 364 | // prepare depth-first queue 365 | while (stack.length) { 366 | list = stack.pop(); 367 | queue.push(list); 368 | if (list instanceof Array) { 369 | list.length && Array.prototype.push.apply(stack, list); 370 | } else if (list instanceof Term) { 371 | list.partlist.list.length && Array.prototype.push.apply(stack, list.partlist.list); 372 | } 373 | } 374 | 375 | // process depth-first queue 376 | var vars = varMap || {}, _ = new Variable("_"); 377 | for (var i = queue.length - 1; i >= 0; i--) { 378 | list = queue[i]; 379 | if (list instanceof Atom) { 380 | out.push(list); 381 | } else if (list instanceof Variable) { 382 | if (list.name === "_") { 383 | v = _; 384 | } else { 385 | v = vars[list.name] || (vars[list.name] = new Variable("_G" + (globalGoalCounter++))); 386 | } 387 | out.push(v); 388 | } else if (list instanceof Term) { 389 | clen = list.partlist.list.length; 390 | tmp = new Term(list.name, out.splice(-clen, clen)); 391 | for (var pl = tmp.partlist.list, k = pl.length; k--;) { 392 | if (pl[k] instanceof Term) { 393 | pl[k].parent = tmp; 394 | } 395 | } 396 | tmp.parent = parent; 397 | out.push(tmp); 398 | } else { 399 | clen = list.length; 400 | clen && Array.prototype.push.apply(out, out.splice(-clen, clen)); 401 | } 402 | } 403 | 404 | return out; 405 | }; 406 | 407 | /** 408 | * Binds variable to a value in the context 409 | * @param name name of the variable to bind 410 | * @param value value to bind to the variable 411 | */ 412 | BindingContext.prototype.bind = function (name, value) { 413 | this.ctx[name] = value; 414 | }; 415 | 416 | /** 417 | * Unbinds variable in the CURRENT context 418 | * Variable remains bound in parent contexts 419 | * and might be resolved though proto chain 420 | * @param name variable name to unbind 421 | */ 422 | BindingContext.prototype.unbind = function (name) { 423 | delete this.ctx[name]; 424 | }; 425 | 426 | /** 427 | * Gets the value of the term, recursively replacing variables with bound values 428 | * @param x term to calculate value for 429 | * @returns value of term x 430 | */ 431 | BindingContext.prototype.value = function value(x) { 432 | var queue = [x], acc = [], c, i; 433 | 434 | while (queue.length) { 435 | x = queue.pop(); 436 | acc.push(x); 437 | if (x instanceof Term) { 438 | Array.prototype.push.apply(queue, x.partlist.list); 439 | } else if (x instanceof Variable) { 440 | c = this.ctx[x.name]; 441 | 442 | if (c) { 443 | acc.pop(); 444 | queue.push(c); 445 | } 446 | } 447 | } 448 | 449 | queue = acc; 450 | acc = []; 451 | i = queue.length; 452 | while (i--) { 453 | x = queue[i]; 454 | if (x instanceof Term) { 455 | c = x.partlist.list.length; 456 | acc.push(new Term(x.name, acc.splice(-c, c))); 457 | } else acc.push(x); 458 | } 459 | 460 | return acc[0]; 461 | }; 462 | 463 | /** 464 | * Unifies terms x and y, renaming and binding variables in process 465 | * !! mutates variable names (altering x, y and varMap in main loop) 466 | * @returns true if terms unify, false otherwise 467 | */ 468 | BindingContext.prototype.unify = function unify(x, y) { 469 | var toSetNames = [], 470 | toSet = {}, 471 | acc = [], 472 | queue = [this.value(x), this.value(y)], 473 | xpl, 474 | ypl, 475 | i, 476 | len; 477 | 478 | while (queue.length) { 479 | x = queue.pop(); 480 | y = queue.pop(); 481 | 482 | if (x instanceof Term && y instanceof Term) { // no need to expand if we are not unifying two terms 483 | xpl = x.partlist.list; 484 | ypl = y.partlist.list; 485 | if (x.name == y.name && xpl.length == ypl.length) { 486 | for (i = 0, len = xpl.length; i < len; i++) { 487 | queue.push(xpl[i], ypl[i]); 488 | } 489 | } else { 490 | return false; 491 | } 492 | } else { 493 | if ((x instanceof Atom || y instanceof Atom) && !(x instanceof Variable || y instanceof Variable)) { 494 | if (!(x instanceof Atom && y instanceof Atom && x.name == y.name)) { 495 | return false; 496 | } 497 | } 498 | acc.push(x, y); 499 | } 500 | } 501 | 502 | i = acc.length; 503 | while (i) { 504 | y = acc[--i]; 505 | x = acc[--i]; 506 | 507 | if (x instanceof Variable) { 508 | if (x.name === "_") { continue; } 509 | if (toSetNames.indexOf(x.name) === -1) { 510 | toSetNames.push(x.name); 511 | } else if (toSet[x.name].name !== y.name) { 512 | return false; 513 | } 514 | toSet[x.name] = y; 515 | 516 | } else if (y instanceof Variable) { 517 | if (y.name === "_") { continue; } 518 | if (toSetNames.indexOf(y.name) === -1) { 519 | toSetNames.push(y.name); 520 | } else if (toSet[y.name].name !== x.name) { 521 | return false; 522 | } 523 | toSet[y.name] = x; 524 | } 525 | } 526 | 527 | // renaming unified variables 528 | // it's guaranteed that variable with the same name is the same instance within rule, see renameVariables() 529 | var varmap = {}, key; 530 | for (i = 0; key = toSetNames[i++];) { 531 | if (toSet[key] instanceof Variable) { 532 | varmap[toSet[key].name] = key; 533 | toSet[key].name = key; 534 | } 535 | } 536 | 537 | // bind values to variables (minding renames) 538 | for (i = 0; key = toSetNames[i++];) { 539 | if (!(toSet[key] instanceof Variable)) { 540 | this.bind(varmap[key] || key, toSet[key]); 541 | } 542 | } 543 | 544 | return true; 545 | }; 546 | -------------------------------------------------------------------------------- /src/prologSolverSpec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from "chai"; 2 | 3 | import * as AST from './prologAST'; 4 | import {query as solver_query, options as solver_options} from './prologSolver'; 5 | import * as Parser from './prologParser'; 6 | 7 | describe("prolog solver", function () { 8 | 9 | var maxIterations = solver_options.maxIterations, 10 | tailRecursion = solver_options.experimental.tailRecursion; 11 | 12 | before(function () { 13 | solver_options.maxIterations = 500000; 14 | solver_options.experimental.tailRecursion = true; 15 | }); 16 | 17 | after(function () { 18 | solver_options.maxIterations = maxIterations; 19 | solver_options.experimental.tailRecursion = tailRecursion; 20 | }); 21 | 22 | it("solves simple fact", function () { 23 | var db = Parser.parse("male(bob)."); 24 | var query = Parser.parseQuery("male(bob)."); 25 | var result = solver_query(db, query); 26 | expect(result.next()).to.be.true; 27 | }); 28 | 29 | it("doesn't throw on missing rule", function () { 30 | var db = Parser.parse("male(bob)."); 31 | var query = Parser.parseQuery("female(bob)."); 32 | var result = solver_query(db, query); 33 | expect(result.next()).to.be.false; 34 | }); 35 | 36 | it("solves simple fact and returns values", function () { 37 | var db = Parser.parse("male(bob). male(jacob)."); 38 | var query = Parser.parseQuery("male(X)."); 39 | var result = solver_query(db, query); 40 | expect(result.next()).to.be.true; 41 | expect(result.current.X).to.eq("bob"); 42 | expect(result.next()).to.be.true; 43 | expect(result.current.X).to.eq("jacob"); 44 | expect(result.next()).to.be.false; 45 | }); 46 | 47 | it("doesn't unify _ with itself", function () { 48 | var db = Parser.parse("fact(x,x,1). fact(x,y,2)."), 49 | query = Parser.parseQuery("fact(_,_,X)."), 50 | result = solver_query(db, query); 51 | 52 | expect(result.next()).to.be.true; 53 | expect(result.current).to.not.haveOwnProperty("_"); 54 | expect(result.current.X).to.eq(1); 55 | expect(result.next()).to.be.true; 56 | expect(result.current.X).to.eq(2); 57 | expect(result.next()).to.be.false; 58 | }); 59 | 60 | it("unifies normal variable with itself", function () { 61 | var db = Parser.parse("u(X,X). r(X,Y):-u(X,Y). "), 62 | query = Parser.parseQuery("r(a,a)."), 63 | result = solver_query(db, query); 64 | 65 | expect(result.next()).to.be.true; 66 | }); 67 | 68 | it("can produce cartesian product", function () { 69 | var db = Parser.parse("fact(a). fact(b). decart(X,Y):-fact(X),fact(Y)."); 70 | var query = Parser.parseQuery("decart(Fact1,Fact2)."); 71 | var result = solver_query(db, query); 72 | expect(result.next()).to.be.true; 73 | expect(result.current.Fact1).to.eq("a"); expect(result.current.Fact2).to.eq("a"); 74 | expect(result.next()).to.be.true; 75 | expect(result.current.Fact1).to.eq("a"); expect(result.current.Fact2).to.eq("b"); 76 | expect(result.next()).to.be.true; 77 | expect(result.current.Fact1).to.eq("b"); expect(result.current.Fact2).to.eq("a"); 78 | expect(result.next()).to.be.true; 79 | expect(result.current.Fact1).to.eq("b"); expect(result.current.Fact2).to.eq("b"); 80 | expect(result.next()).to.be.false; 81 | }); 82 | 83 | it("correctly works with lists", function () { 84 | var db = Parser.parse("member(X,[X|R]). member(X, [Y | R]) :- member(X, R)."), 85 | query, 86 | 87 | result, 88 | list = AST.Atom.Nil, 89 | depth = 200; 90 | 91 | // member(x,[l0 ... ln]). 92 | for (var i = depth; i > 0; i--) { 93 | list = new AST.Term("cons", [new AST.Atom("l" + i), list]); 94 | } 95 | query = new AST.Partlist([new AST.Term("member", [new AST.Atom("l" + depth), list])]); 96 | 97 | result = solver_query(db, query); 98 | expect(result.next()).to.be.true; 99 | expect(result.next()).to.be.false; 100 | }); 101 | 102 | it("produces list cartesian", function () { 103 | var db = Parser.parse("member(X,[X|R]). member(X, [Y | R]) :- member(X, R)."), 104 | query = Parser.parseQuery("member(X,[a,b,c]),member(Y,[1,2,3])."), 105 | result = solver_query(db, query), 106 | X = [], 107 | Y = []; 108 | 109 | while(result.next()) { 110 | X.push(result.current.X); 111 | Y.push(result.current.Y); 112 | } 113 | 114 | expect(X).to.eql(["a", "a", "a", "b", "b", "b", "c", "c", "c"]); 115 | expect(Y).to.eql([1 , 2 , 3 , 1 , 2 , 3 , 1 , 2 , 3]); 116 | }); 117 | 118 | it("can append lists", function () { 119 | var db = Parser.parse('append([], List, List). append([Head | Tail], List2, [Head | Result]):-append(Tail, List2, Result).'), 120 | query = Parser.parseQuery('append([b,c,d],[one,two,three,four],L).'), 121 | result = solver_query(db, query); 122 | expect(result.next()).to.be.ok; 123 | expect(result.current.L).to.eql(['b', 'c', 'd', 'one', 'two', 'three', 'four']); 124 | expect(result.next()).to.be.false; 125 | }); 126 | 127 | it("can copy lists", function () { 128 | var db = Parser.parse("cop([],[]). cop([X|T1],[X|T2]):-cop(T1,T2)."), 129 | query = Parser.parseQuery('cop([1,2,3],X).'), 130 | result = solver_query(db, query); 131 | 132 | expect(result.next()).to.be.true; 133 | expect(result.current.X).to.eql([1, 2, 3]); 134 | expect(result.next()).to.be.false; 135 | }); 136 | 137 | it("can filter lists", function () { 138 | var db = Parser.parse("fil(_, [],[]). fil(X, [X|T1], T2):-fil(X, T1,T2). fil(X, [Y|T1], [Y|T2]):-fil(X,T1,T2)."); 139 | var query = Parser.parseQuery('fil(8, [1,2,8,3],X).'); 140 | var result = solver_query(db, query); 141 | expect(result.next()).to.be.true; 142 | expect(result.current.X).to.eql([1, 2, 3]); 143 | expect(result.next()).to.be.false; 144 | }); 145 | 146 | it("returns correct number of results (cut issue)", function () { 147 | var db = Parser.parse( 148 | 'fnd(Name, [Name|T1], Name).' + 149 | 'fnd(Name, [R | T1], T2) :- fnd(Name, T1, T2).' + 150 | 'limit([],_,[]). ' + 151 | 'limit([H|T],GEnv,[X | Env]):-fnd(H, GEnv, X), !, limit(T,GEnv,Env).' + 152 | 'limit([H|T],GEnv,Env):-limit(T,GEnv,Env).'); 153 | var query = Parser.parseQuery("limit(['i', 'document'], ['i', 'document', 'q'], R)."); 154 | var result = solver_query(db, query); 155 | expect(result.next()).to.be.true; 156 | expect(result.current.R).to.eql(['i', 'document']); 157 | expect(result.next()).to.be.false; 158 | }); 159 | 160 | 161 | describe("solves examples", function () { 162 | // warning: 5s to run 163 | it("eight queens problem (first solution only)", function () { 164 | var db = Parser.parse( 165 | "solution(Ylist):- sol(Ylist, [1, 2, 3, 4, 5, 6, 7, 8], [1, 2, 3, 4, 5, 6, 7, 8], [-7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7], [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])." + 166 | "sol([], [], [], _, _)." + 167 | "sol([Y | Ylist], [X | Dx1], Dy, Du, Dv):- del(Y, Dy, Dy1), is(U, -(X, Y)), del(U, Du, Du1), is(V, +(X, Y)), del(V, Dv, Dv1), sol(Ylist, Dx1, Dy1, Du1, Dv1)." + 168 | "del(Item, [Item | List], List)." + 169 | "del(Item, [First | List], [First | List1]):- del(Item, List, List1)."), 170 | query = Parser.parseQuery("solution(X)."), 171 | result = solver_query(db, query); 172 | 173 | expect(result.next()).to.be.true; 174 | expect(result.current.X).to.eql([1, 5, 8, 6, 3, 7, 2, 4]); 175 | // 2nd and 3rd (commented because slow) 176 | //expect(result.next()).to.be.ok; 177 | //expect(result.current.X).to.eql([1, 6, 8, 3, 7, 4, 2, 5]); 178 | //expect(result.next()).to.be.ok; 179 | //expect(result.current.X).to.eql([1, 7, 4, 6, 8, 2, 5, 3]); 180 | }); 181 | 182 | it("color map example from prolog tutorial", function () { 183 | var db = Parser.parse( 184 | "adjacent(1, 2).adjacent(2, 1). adjacent(1, 3).adjacent(3, 1)." + 185 | "adjacent(1, 4).adjacent(4, 1). adjacent(1, 5).adjacent(5, 1)." + 186 | "adjacent(2, 3).adjacent(3, 2). adjacent(2, 4).adjacent(4, 2)." + 187 | "adjacent(3, 4).adjacent(4, 3). adjacent(4, 5).adjacent(5, 4). " + 188 | "color(1, red, a).color(1, red, b). color(2, blue, a).color(2, blue, b)." + 189 | "color(3, green, a).color(3, green, b). color(4, yellow, a).color(4, blue, b)." + 190 | "color(5, blue, a).color(5, green, b). " + 191 | "conflict(Coloring):- " + 192 | "adjacent(X, Y), color(X, Color, Coloring), color(Y, Color, Coloring)." + 193 | "conflict(R1, R2, Coloring) :-" + 194 | "adjacent(R1, R2)," + 195 | "color(R1,Color,Coloring)," + 196 | "color(R2,Color,Coloring)."), 197 | query = Parser.parseQuery("conflict(R1,R2,b),color(R1,C,b)."), 198 | result; 199 | 200 | result = solver_query(db, query); 201 | 202 | expect(result.next()).to.be.true; 203 | expect(result.current.R1).to.eq(2); 204 | expect(result.current.R2).to.eq(4); 205 | expect(result.current.C).to.eq("blue"); 206 | expect(result.next()).to.be.true; 207 | expect(result.current.R1).to.eq(4); 208 | expect(result.current.R2).to.eq(2); 209 | expect(result.current.C).to.eq("blue"); 210 | expect(result.next()).to.be.false; 211 | }); 212 | }); 213 | 214 | describe("builtin", function () { 215 | describe("!/0", function () { 216 | it("correctly cuts", function () { 217 | var db = Parser.parse("fact(a).fact(b).firstFact(X):-fact(X),!."); 218 | var query = Parser.parseQuery("firstFact(Fact)."); 219 | var result = solver_query(db, query); 220 | expect(result.next()).to.be.true; 221 | expect(result.current.Fact).to.eq("a"); 222 | expect(result.next()).to.be.false; 223 | }); 224 | 225 | it("works with classic not implementation", function () { 226 | var db = Parser.parse("not(Term):-call(Term),!,fail. not(Term). fact(a). fact(b). secret(b). fact(c). open(X):-fact(X),not(secret(X))."); 227 | var query = Parser.parseQuery("open(X)."); 228 | var result = solver_query(db, query); 229 | expect(result.next()).to.be.true; 230 | expect(result.current.X).to.eq("a"); 231 | expect(result.next()).to.be.true; 232 | expect(result.current.X).to.eq("c"); 233 | expect(result.next()).to.be.false; 234 | }); 235 | 236 | it("works with not unify", function () { 237 | var db = Parser.parse("not(Term):-call(Term),!,fail. not(Term). r(X,Y):-not(=(X,Y)). "), 238 | query = Parser.parseQuery("r(a,b)."), 239 | result = solver_query(db, query); 240 | 241 | expect(result.next()).to.be.ok; 242 | }); 243 | }); 244 | 245 | describe("=/2", function () { 246 | it("=/2 unifies atoms", function () { 247 | var db = [], 248 | query = Parser.parseQuery("=(5,5),=(a,a)."), 249 | result = solver_query(db, query); 250 | 251 | expect(result.next()).to.be.ok; 252 | }); 253 | 254 | it("=/2 unifies structures", function () { 255 | var db = [], 256 | query = Parser.parseQuery("=(tax(income,13.0), tax(income,13.0))."), 257 | result = solver_query(db, query); 258 | 259 | expect(result.next()).to.be.ok; 260 | }); 261 | 262 | it("=/2 unifies atom with variable", function () { 263 | var db = [], 264 | 265 | query = Parser.parseQuery("=(X,5)."), 266 | result = solver_query(db, query); 267 | 268 | expect(result.next()).to.be.ok; 269 | expect(result.current.X).to.eq(5); 270 | }); 271 | }); 272 | 273 | describe("findall/3", function () { 274 | it("returns all results", function () { 275 | var db = Parser.parse("city(moscow,russia).city(vladivostok,russia).city(boston,usa)."), 276 | query = Parser.parseQuery("findall(C,city(C,_),Cities)."), 277 | result = solver_query(db, query); 278 | 279 | expect(result.next()).to.be.true; 280 | expect(result.current.Cities).to.eql(["moscow", "vladivostok", "boston"]); 281 | expect(result.next()).to.be.false; 282 | }); 283 | 284 | it("sees parent context", function () { 285 | var db = Parser.parse("city(moscow,russia).city(vladivostok,russia).city(boston,usa)."), 286 | query = Parser.parseQuery("=(R,usa),findall(C,city(C,R),Cities)."), 287 | result = solver_query(db, query); 288 | 289 | expect(result.next()).to.be.true; 290 | expect(result.current.Cities).to.eql(["boston"]); 291 | expect(result.next()).to.be.false; 292 | }); 293 | 294 | it("works if first argument is not a variable and grounded", function () { 295 | var db = Parser.parse("city(moscow,russia).city(vladivostok,russia).city(boston,usa)."), 296 | query = Parser.parseQuery("findall(10,city(C,R),Cities)."), 297 | result = solver_query(db, query); 298 | 299 | expect(result.next()).to.be.true; 300 | expect(result.current.Cities).to.eql([10, 10, 10]); 301 | expect(result.next()).to.be.false; 302 | }); 303 | 304 | it("works if first argument is a partial term", function () { 305 | var db = Parser.parse("city(moscow,russia).city(vladivostok,russia).city(boston,usa)."), 306 | query = Parser.parseQuery("findall(term(C),city(C,R),Cities)."), 307 | result = solver_query(db, query); 308 | 309 | expect(result.next()).to.be.true; 310 | expect(result.current.Cities).to.eql(['term(moscow)', 'term(vladivostok)', 'term(boston)']); 311 | expect(result.next()).to.be.false; 312 | }); 313 | 314 | it("works if last argument is not a variable and should unify", function () { 315 | var db = Parser.parse("city(moscow,russia).city(vladivostok,russia).city(boston,usa)."), 316 | query = Parser.parseQuery("findall(C,city(C,R),[moscow, vladivostok, boston])."), 317 | result = solver_query(db, query); 318 | 319 | expect(result.next()).to.be.true; 320 | }); 321 | 322 | it("works if last argument is not a variable and shouldn't unify", function () { 323 | var db = Parser.parse("city(moscow,russia).city(vladivostok,russia).city(boston,usa)."), 324 | query = Parser.parseQuery("findall(C,city(C,R),[brussels])."), 325 | result = solver_query(db, query); 326 | 327 | expect(result.next()).to.be.false; 328 | }); 329 | 330 | it("returns empty list if goal fails", function () { 331 | var db = Parser.parse("city(moscow,russia).city(vladivostok,russia).city(boston,usa)."), 332 | 333 | query = Parser.parseQuery("findall(C,city(C,canada),Cities)."), 334 | result = solver_query(db, query); 335 | 336 | expect(result.next()).to.be.true; 337 | expect(result.current.Cities).to.eql([]); 338 | expect(result.next()).to.be.false; 339 | }); 340 | }); 341 | 342 | describe("is/2", function () { 343 | it("handles four arithmetic operations", function () { 344 | var db = [], 345 | 346 | query = Parser.parseQuery("is(X, /(*(+(3,-(8,3)),2),4))."), 347 | result = solver_query(db, query); 348 | 349 | expect(result.next()).to.be.true; 350 | expect(result.current.X).to.eq(4); 351 | expect(result.next()).to.be.false; 352 | }); 353 | }); 354 | }); 355 | }); 356 | 357 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "declaration": true, 6 | "outDir": "dist", 7 | "rootDir": "src" 8 | }, 9 | "include": [ 10 | "src" 11 | ] 12 | } --------------------------------------------------------------------------------