├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src └── logic.js └── test ├── mocha.opts └── simple.js /.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": ["es2015"] } 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | tmp-* 4 | index.js 5 | *.orig 6 | *.log 7 | *~ 8 | *.map 9 | log 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Vitaliy Akimov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Logic programming in JavaScript 2 | 3 | The library is based on 4 | [Backtracking, Interleaving, and Terminating Monad Transformers](http://okmij.org/ftp/papers/LogicT.pdf) 5 | 6 | Using it with [@mfjs/compiler](https://github.com/awto/mfjs-compiler) turns 7 | JavaScript into a logical programming language. 8 | 9 | For example, here is almost literal translation of classical Prolog 10 | bi-directional list append function to JavaScript (defined in separate 11 | [samples project](https://github.com/awto/mfjs-samples/tree/master/unify)): 12 | 13 | ```javascript 14 | function append(a,b,r) { 15 | const [h, t, rt] = newRefs() 16 | unify(a, cons(h, t)) 17 | unify(r, cons(h, rt)) 18 | append(t, b, rt) 19 | M.answer() 20 | unify(a, nil()) 21 | unify(r, b) 22 | } 23 | ``` 24 | 25 | with usages: 26 | 27 | ```javascript 28 | let l1 = List.from([1,2,3]) 29 | let l2 = List.from(['a','b','c']) 30 | // free variables: 31 | let [l3,l4,l5] = newRefs() 32 | 33 | append(l1, l2, l3) 34 | console.log('append:', List.toArray(l3)) 35 | // ==> append: [ 1, 2, 3, 'a', 'b', 'c' ] 36 | 37 | append(l1, l4, l3) 38 | console.log('suffix', List.toArray(l4)) 39 | // ==> suffix [ 'a', 'b', 'c' ] 40 | 41 | append(l5, l2, l3) 42 | console.log('prefix', List.toArray(l5)) 43 | // ==> prefix [ 1, 2, 3 ] 44 | ``` 45 | 46 | or non-determenistic with only result defined: 47 | 48 | ```javascript 49 | let [x,y] = newRefs() 50 | let z = List.from([1,2,3,4]) 51 | // only result is instantied 52 | append(x,y,z) 53 | console.log('x:', List.toArray(x)) 54 | console.log('y:', List.toArray(y)) 55 | console.log('z:', List.toArray(z)) 56 | ``` 57 | 58 | outputs all 5 possible answers: 59 | 60 | ``` 61 | x: [ 1, 2, 3, 4 ] 62 | y: [] 63 | z: [ 1, 2, 3, 4 ] 64 | 65 | x: [ 1, 2, 3 ] 66 | y: [ 4 ] 67 | z: [ 1, 2, 3, 4 ] 68 | 69 | x: [ 1, 2 ] 70 | y: [ 3, 4 ] 71 | z: [ 1, 2, 3, 4 ] 72 | 73 | x: [ 1 ] 74 | y: [ 2, 3, 4 ] 75 | z: [ 1, 2, 3, 4 ] 76 | 77 | x: [] 78 | y: [ 1, 2, 3, 4 ] 79 | z: [ 1, 2, 3, 4 ] 80 | ``` 81 | 82 | ## API 83 | 84 | In standard Prolog language there is only depth first search options for 85 | possible answer lookup. The library provides means to add arbitrary search 86 | strategies. By default it provides depth-first and breadth-first searches. 87 | 88 | Result object returned from `L.run` is ES iterable. Its default iterator 89 | traverses answers in depth-first order. The result object also has `bfs` 90 | function returning iterator for breadth first order. 91 | 92 | There are a few additional functions in logic monad definition: 93 | 94 | * once - takes logical computation and returns only its first answer if any 95 | * level - for logical computation returns object with either field `value` 96 | or `alts`, the first is final result of the computation and the second 97 | is a list of other logical computations if original one was non-deterministic 98 | 99 | ## Usage 100 | 101 | ``` 102 | $ npm install --save-dev @mfjs/compiler 103 | $ npm install --save @mfjs/core @mfjs/logic 104 | $ mfjsc input.js --output=out 105 | # or for browser 106 | $ browserify -t @mfjsc/compiler/monadify input.js -o index.js 107 | ``` 108 | 109 | ## 110 | 111 | ## License 112 | 113 | Copyright © 2016 Vitaliy Akimov 114 | 115 | Distributed under the terms of the [The MIT License (MIT)](LICENSE). 116 | 117 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mfjs/logic", 3 | "version": "1.1.0", 4 | "description": "mfjs interface for logic programming", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "build": "babel src/logic.js --out-file index.js", 9 | "prepublish": "npm run build" 10 | }, 11 | "keywords": [ 12 | "logic", 13 | "LP", 14 | "mfjs" 15 | ], 16 | "author": "Vitaliy Akimov ", 17 | "license": "MIT", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/awto/mfjs-logic.git" 21 | }, 22 | "dependencies": { 23 | "@mfjs/cc": "~1.1.0" 24 | }, 25 | "peerDependencies": { 26 | "@mfjs/core": "~1.1.0" 27 | }, 28 | "devDependencies": { 29 | "@mfjs/compiler": "~1.0.0", 30 | "babel-cli": "^6.8.0", 31 | "babel-preset-es2015": "^6.6.0", 32 | "babel-register": "^6.8.0", 33 | "expect.js": "^0.3.1", 34 | "mocha": "^2.4.5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/logic.js: -------------------------------------------------------------------------------- 1 | 'strict mode'; 2 | import M from '@mfjs/core' 3 | import CC from '@mfjs/cc' 4 | 5 | class Result { 6 | constructor(tree) { 7 | this.tree = tree 8 | } 9 | dfs() { 10 | return new MetaIterator(new DfsIterator(this.tree)); 11 | } 12 | bfs() { 13 | return new MetaIterator(new BfsIterator(this.tree)); 14 | } 15 | } 16 | 17 | Result.prototype[Symbol.iterator] = Result.prototype.dfs; 18 | 19 | class MetaIterator { 20 | constructor(inner) { 21 | this.inner = inner 22 | } 23 | next() { 24 | return CC.exec.call(L,this.inner.next()) 25 | } 26 | } 27 | 28 | MetaIterator.prototype[Symbol.iterator] = function() { return this; } 29 | 30 | class Iterator { 31 | constructor(tree) { 32 | this.stack = [tree] 33 | } 34 | next() { 35 | if (!this.stack.length) 36 | return L.pure({done: true}) 37 | return L.bind(this.stack.shift(), r => { 38 | if (r.alts) { 39 | this.advance(r.alts) 40 | return this.next() 41 | } 42 | return L.pure({value:r.value}) 43 | }) 44 | } 45 | } 46 | 47 | class BfsIterator extends Iterator { 48 | constructor(tree) { 49 | super(tree) 50 | } 51 | advance(alts) { 52 | this.stack.push(...alts) 53 | } 54 | } 55 | 56 | class DfsIterator extends Iterator { 57 | constructor(tree) { 58 | super(tree) 59 | } 60 | advance(alts) { 61 | this.stack.unshift(...alts) 62 | } 63 | } 64 | 65 | const btPrompt = CC.newPrompt("bt") 66 | 67 | class LogicDefs extends CC.Defs { 68 | constructor() { 69 | super() 70 | } 71 | level(m) { 72 | return this.pushPrompt(btPrompt, 73 | this.apply( 74 | m, 75 | v => { return {value:v} })) 76 | } 77 | alt(...args) { 78 | return this.join(this.shift(btPrompt, sk => { 79 | const len = args.length, alts = [] 80 | for(let i = 0; i < len; ++i) 81 | alts.push(sk(args[i])) 82 | return this.pure({alts}) 83 | })) 84 | } 85 | once(m) { 86 | return this.apply( 87 | (new DfsIterator(this.level(m))).next(), 88 | r => r.done ? this.empty() : this.pure(r.value)) 89 | } 90 | exec(m) { 91 | return new Result(this.level(m)); 92 | } 93 | run(m) { 94 | return this.exec(M.liftContext(this,m)()) 95 | } 96 | } 97 | 98 | const L = new LogicDefs() 99 | 100 | M.completePrototype(L,CC.ctor.prototype,true) 101 | 102 | export default L 103 | 104 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-register,mjs:@mfjs/compiler/nastyRegister -r @mfjs/core/test/kit/inject 2 | -------------------------------------------------------------------------------- /test/simple.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const M = require('@mfjs/core') 3 | import L from '../src/logic' 4 | 5 | //M.setContext(L) 6 | 7 | M.option({ 8 | test: { 9 | CallExpression:{ 10 | match:{ 11 | name:{"run":true} 12 | }, 13 | select:'matchCallName', 14 | cases:{true:{sub:'defaultFull'}} 15 | }, 16 | full: { 17 | CallExpression:{ 18 | match:{ 19 | name:{expect:true,equal:true,fail:true,check:true} 20 | } 21 | } 22 | }, 23 | compile:true 24 | } 25 | }) 26 | M.profile('test') 27 | 28 | describe('running logical monad', () => { 29 | context('with no logical effects', () => { 30 | it('should return single value', () => { 31 | const k = L.run(() => { 32 | return 2 33 | }) 34 | expect(Array.from(k)).to.eql([2]) 35 | }) 36 | }) 37 | context('with alt', () => { 38 | it('should convert choice alternative into a single value', () => { 39 | const k = L.run(() => { 40 | const v = M.alt(1,2,3) 41 | return v * 10 42 | }) 43 | expect(Array.from(k)).to.eql([10,20,30]) 44 | }) 45 | }) 46 | context('with yield', () => { 47 | it('should answer yield argument', () => { 48 | const k = L.run(() => { 49 | M.yield(1) 50 | M.yield(2) 51 | M.yield(3) 52 | return 4 53 | }) 54 | expect(Array.from(k)).to.eql([1,2,3,4]) 55 | }) 56 | it('should suspend execution after `yield`', () => { 57 | const k = L.run(() => { 58 | M.yield(1) 59 | expect().fail() 60 | }).dfs() 61 | expect(k.next().value).to.eql(1) 62 | }) 63 | it('should suspend execution until `next`', () => { 64 | const k = L.run(() => { 65 | expect().fail() 66 | M.yield(1) 67 | }).dfs() 68 | expect(typeof k).to.equal('object') 69 | }) 70 | it('should revert local variables values on backtracking', () => { 71 | const k = L.run(() => { 72 | let i = 1 73 | M.yield(i) 74 | i++ 75 | expect(i).to.equal(2) 76 | M.empty() 77 | expect().fail() 78 | M.yield(i) 79 | expect(i).to.equal(1) 80 | M.yield(i) 81 | i++ 82 | M.yield(i) 83 | M.empty() 84 | }) 85 | expect(Array.from(k)).to.eql([1,1,2]) 86 | }) 87 | context('in loop body', () => { 88 | it('should return an answer for each iteration', () => { 89 | const k = L.run(() => { 90 | for (let i = 1; i <= 4; ++i) 91 | M.yield(i) 92 | M.empty(); 93 | }) 94 | expect(Array.from(k)).to.eql([1,2,3,4]) 95 | }) 96 | }) 97 | }) 98 | }) 99 | 100 | describe('empty', () => { 101 | it('should return no answers', () => { 102 | const k = L.run(() => { 103 | M.empty() 104 | }) 105 | expect(Array.from(k)).to.eql([]) 106 | }) 107 | }) 108 | 109 | describe('yield', () => { 110 | it('should discharge empty', () => { 111 | const k = L.run(() => { 112 | M.empty() 113 | expect().fail() 114 | M.yield(1) 115 | return 2 116 | }) 117 | expect(Array.from(k)).to.eql([2]) 118 | }) 119 | }) 120 | 121 | describe('control flow', () => { 122 | const state = [] 123 | const rec = v => state.push(v) 124 | const check = (...args) => 125 | expect(state).to.eql(args) 126 | context('with labeld break', () => { 127 | it('should respect js control flow', () => { 128 | const k = L.run(() => { 129 | for(var i = 0; i < 5; i++) { 130 | rec(`i1:${i}`) 131 | if (i === 3) 132 | break 133 | rec(`i2:${i}`) 134 | M.yield(i) 135 | rec(`i3:${i}`) 136 | M.yield(`i:${i}`) 137 | rec(`i4:${i}`) 138 | } 139 | rec(`i5:${i}`) 140 | if (i === 3) 141 | M.empty() 142 | rec(`i6:${i}`) 143 | return 'fin' 144 | }) 145 | expect(Array.from(k)).to.eql([0,'i:0',1,'i:1',2,'i:2','i:3',4, 146 | 'i:4','fin']) 147 | check('i1:0','i2:0','i3:0','i4:0','i1:1','i2:1','i3:1','i4:1', 148 | 'i1:2','i2:2','i3:2','i4:2','i1:3','i5:3','i3:3','i4:3', 149 | 'i1:4','i2:4','i3:4','i4:4','i5:5','i6:5') 150 | }) 151 | }) 152 | }) 153 | 154 | describe('breadth first order', () => { 155 | it('should return elements in breadth first order', () => { 156 | const k = L.run(() => { 157 | { 158 | M.yield(1) 159 | M.yield(2) 160 | { 161 | M.yield(3) 162 | M.yield(4) 163 | } 164 | M.yield(5) 165 | } 166 | M.yield(6) 167 | M.yield(7) 168 | return 8 169 | }).bfs() 170 | // TODO: maybe associativity of yield should be changed 171 | expect(Array.from(k)).to.eql([8,7,6,1,2,5,3,4]) 172 | }) 173 | }) 174 | 175 | describe('once function', () => { 176 | it('should discard all but one answers', function() { 177 | const k = L.run(() => L.once(M.reify(() => { 178 | M.yield(1) 179 | expect().fail() 180 | M.yield(2) 181 | M.yield(3) 182 | }))) 183 | expect(Array.from(k)).to.eql([1]) 184 | }) 185 | it('should return no answers if its argument returns none', function() { 186 | const k = L.run(() => L.once(M.empty())) 187 | expect(Array.from(k)).to.eql([]) 188 | }) 189 | }) 190 | --------------------------------------------------------------------------------