├── README.md ├── LICENCE ├── styles.css ├── glue.js ├── index.html ├── parser.js └── interpreter.js /README.md: -------------------------------------------------------------------------------- 1 | Prolog interpreter 2 | --------------- 3 | 4 | Simple prolog interpreter using ES6 generators. 5 | 6 | For more details see https://curiosity-driven.org/prolog-interpreter 7 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Curiosity driven 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Curiosity driven 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | textarea { width: 90%; height: 400px; } 18 | input[type='text'] { width: 70%; } -------------------------------------------------------------------------------- /glue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Curiosity driven 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | document.getElementById('submit').addEventListener('click', function() { 18 | var rules = parser(lexer(document.getElementById('rules').value)).parseRules(); 19 | 20 | var db = new Database(rules); 21 | 22 | var goalText = document.getElementById('query').value; 23 | 24 | var goal = parser(lexer(goalText)).parseTerm(); 25 | 26 | var list = document.getElementById('answers'); 27 | list.innerHTML = ''; 28 | 29 | for (var item of db.query(goal)) { 30 | var li = document.createElement('LI'); 31 | li.textContent = item; 32 | list.appendChild(li); 33 | } 34 | 35 | if (list.innerHTML === '') { 36 | list.innerHTML = 'No solutions'; 37 | } 38 | }); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | Prolog interpreter 20 | 21 | 22 | 23 |

Prolog interpreter

24 |

Requires modern browser that supports ES6 generators. See the article for implementation details.

25 |

Rules

26 | 72 |

Query

73 | 74 | 75 |

Answers

76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Curiosity driven 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | function *lexer(text) { 18 | var tokenRegexp = /[A-Za-z_]+|:\-|[()\.,]/g; 19 | var match; 20 | while ((match = tokenRegexp.exec(text)) !== null) { 21 | yield match[0]; 22 | } 23 | } 24 | 25 | function parser(tokens) { 26 | var current, done, scope; 27 | function next() { 28 | var next = tokens.next(); 29 | current = next.value; 30 | done = next.done; 31 | } 32 | function parseAtom() { 33 | var name = current; 34 | if (!/^[A-Za-z_]+$/.test(name)) { 35 | throw new SyntaxError('Bad atom name: ' + name); 36 | } 37 | next(); 38 | return name; 39 | } 40 | function parseTerm() { 41 | if (current === '(') { 42 | next(); // eat ( 43 | var args = []; 44 | while (current !== ')') { 45 | args.push(parseTerm()); 46 | if (current !== ',' && current !== ')') { 47 | throw new SyntaxError('Expected , or ) in term but got ' + current); 48 | } 49 | if (current === ',') { 50 | next(); // eat , 51 | } 52 | } 53 | next(); // eat ) 54 | return new Conjunction(args); 55 | } 56 | var functor = parseAtom(); 57 | if (/^[A-Z_][A-Za-z_]*$/.test(functor)) { 58 | if (functor === '_') { 59 | return new Variable('_'); 60 | } 61 | // variable X in the same scope should point to the same object 62 | var variable = scope[functor]; 63 | if (!variable) { 64 | variable = scope[functor] = new Variable(functor); 65 | } 66 | return variable; 67 | } 68 | if (current !== '(') { 69 | return new Term(functor); 70 | } 71 | next(); // eat ( 72 | var args = []; 73 | while (current !== ')') { 74 | args.push(parseTerm()); 75 | if (current !== ',' && current !== ')') { 76 | throw new SyntaxError('Expected , or ) in term but got ' + current); 77 | } 78 | if (current === ',') { 79 | next(); // eat , 80 | } 81 | } 82 | next(); // eat ) 83 | return new Term(functor, args); 84 | } 85 | function parseRule() { 86 | var head = parseTerm(); 87 | if (current === '.') { 88 | next(); // eat . 89 | return new Rule(head, Term.TRUE); 90 | } 91 | if (current !== ':-') { 92 | throw new SyntaxError('Expected :- in rule but got ' + current); 93 | } 94 | next(); // eat :- 95 | var args = []; 96 | while (current !== '.') { 97 | args.push(parseTerm()); 98 | if (current !== ',' && current !== '.') { 99 | throw new SyntaxError('Expected , or ) in term but got ' + current); 100 | } 101 | if (current === ',') { 102 | next(); // eat , 103 | } 104 | } 105 | next(); // eat . 106 | var body; 107 | if (args.length === 1) { 108 | // body is a regular Term 109 | body = args[0]; 110 | } else { 111 | // body is a conjunction of all terms 112 | body = new Conjunction(args); 113 | } 114 | return new Rule(head, body); 115 | } 116 | next(); // start the tokens iterator 117 | return { 118 | parseRules: function() { 119 | var rules = []; 120 | while (!done) { 121 | // each rule gets its own scope for variables 122 | scope = { }; 123 | rules.push(parseRule()); 124 | } 125 | return rules; 126 | }, 127 | parseTerm: function() { 128 | scope = { }; 129 | return parseTerm(); 130 | } 131 | }; 132 | } -------------------------------------------------------------------------------- /interpreter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Curiosity driven 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | function mergeBindings(bindings1, bindings2) { 18 | if (!bindings1 || !bindings2) { 19 | return null; 20 | } 21 | var conflict = false; 22 | var bindings = new Map; 23 | bindings1.forEach(function(value, variable) { 24 | bindings.set(variable, value); 25 | }); 26 | bindings2.forEach(function(value, variable) { 27 | var other = bindings.get(variable); 28 | if (other) { 29 | var sub = other.match(value); 30 | if (!sub) { 31 | conflict = true; 32 | } else { 33 | sub.forEach(function(value, variable) { 34 | bindings.set(variable, value); 35 | }); 36 | } 37 | } else { 38 | bindings.set(variable, value); 39 | } 40 | }); 41 | if (conflict) { 42 | return null; 43 | } 44 | return bindings; 45 | }; 46 | 47 | function Variable(name) { 48 | this.name = name; 49 | } 50 | 51 | Variable.prototype.match = function(other) { 52 | var bindings = new Map; 53 | if (this !== other) { 54 | bindings.set(this, other); 55 | } 56 | return bindings; 57 | }; 58 | 59 | Variable.prototype.substitute = function(bindings) { 60 | var value = bindings.get(this); 61 | if (value) { 62 | // if value is a compound term then substitute 63 | // variables inside it too 64 | return value.substitute(bindings); 65 | } 66 | return this; 67 | }; 68 | 69 | function Term(functor, args) { 70 | this.functor = functor; 71 | this.args = args || []; 72 | } 73 | 74 | function zip(arrays) { 75 | return arrays[0].map(function(element, index) { 76 | return arrays.map(function(array) { 77 | return array[index]; 78 | }); 79 | }); 80 | } 81 | 82 | Term.prototype.match = function(other) { 83 | if (other instanceof Term) { 84 | if (this.functor !== other.functor) { 85 | return null; 86 | } 87 | if (this.args.length !== other.args.length) { 88 | return null; 89 | } 90 | return zip([this.args, other.args]).map(function(args) { 91 | return args[0].match(args[1]); 92 | }).reduce(mergeBindings, new Map); 93 | } 94 | return other.match(this); 95 | }; 96 | 97 | Term.prototype.substitute = function(bindings) { 98 | return new Term(this.functor, this.args.map(function(arg) { 99 | return arg.substitute(bindings); 100 | })); 101 | }; 102 | 103 | Term.prototype.query = function*(database) { 104 | yield* database.query(this); 105 | }; 106 | 107 | Term.TRUE = new Term('true'); 108 | 109 | Term.TRUE.substitute = function() { 110 | return this; 111 | }; 112 | 113 | Term.TRUE.query = function*() { 114 | yield this; 115 | }; 116 | 117 | function Rule(head, body) { 118 | this.head = head; 119 | this.body = body; 120 | } 121 | 122 | function Conjunction(args) { 123 | this.args = args; 124 | } 125 | 126 | Conjunction.prototype = Object.create(Term.prototype); 127 | 128 | Conjunction.prototype.query = function*(database) { 129 | var self = this; 130 | function* solutions(index, bindings) { 131 | var arg = self.args[index]; 132 | if (!arg) { 133 | yield self.substitute(bindings); 134 | } else { 135 | for (var item of database.query(arg.substitute(bindings))) { 136 | var unified = mergeBindings(arg.match(item), bindings); 137 | if (unified) { 138 | yield* solutions(index + 1, unified); 139 | } 140 | } 141 | } 142 | } 143 | yield* solutions(0, new Map); 144 | }; 145 | 146 | Conjunction.prototype.substitute = function(bindings) { 147 | return new Conjunction(this.args.map(function(arg) { 148 | return arg.substitute(bindings); 149 | })); 150 | }; 151 | 152 | function Database(rules) { 153 | this.rules = rules; 154 | } 155 | 156 | Database.prototype.query = function*(goal) { 157 | for (var i = 0, rule; rule = this.rules[i]; i++) { 158 | var match = rule.head.match(goal); 159 | if (match) { 160 | var head = rule.head.substitute(match); 161 | var body = rule.body.substitute(match); 162 | for (var item of body.query(this)) { 163 | yield head.substitute(body.match(item)); 164 | } 165 | } 166 | } 167 | }; 168 | 169 | Variable.prototype.toString = function() { 170 | return this.name; 171 | }; 172 | 173 | Term.prototype.toString = function() { 174 | if (this.args.length === 0) { 175 | return this.functor; 176 | } 177 | return this.functor + '(' + this.args.join(', ') + ')'; 178 | }; 179 | 180 | Rule.prototype.toString = function() { 181 | return this.head + ' :- ' + this.body; 182 | }; 183 | 184 | Conjunction.prototype.toString = function() { 185 | return this.args.join(', '); 186 | }; 187 | 188 | Database.prototype.toString = function() { 189 | return this.rules.join('.\n') + '.'; 190 | }; 191 | --------------------------------------------------------------------------------