UNPKG

logica

Version:

a compile-to-javascript predicate logic language

401 lines (353 loc) 12.8 kB
var logica = require('../index') var compile = logica.compile var hydrate = logica.hydrate var heredoc = require('heredoc') require('chai').should() var expect = require('chai').expect function exec(src, state) { return hydrate(compile(src))(state); } describe('logica', function () { it('is a predicate logic language', function () { //example var src = heredoc(function(){/* (AND, (=, PrintJobPaperSize, 'letter'), (OR, (=, LetterPaperInTray, true), (=, PCLoadLetter, true) ) ) */}) // or, less verbose var src2 = heredoc(function(){/* (AND, (=, PrintJobPaperSize, 'letter'), (OR, LetterPaperInTray, PCLoadLetter ) ) */}) var compiled = compile(src) // returns javascript source compiled.should.be.a('string'); var fn = hydrate(compiled); fn.should.be.a('function'); var state = { PrintJobPaperSize: 'letter', LetterPaperInTray: false, PCLoadLetter: true } fn(state).should.equal(true); }) it('whitespace is more or less arbitrary as long as there is at least one space between things', function () { var src = heredoc(function() {/* (AND stuff moreStuff otherThingsA (OR yetMoreThings yaKnow) ) */}) var compiled = compile(src) }) it('supports rest-of-line comments', function () { var src = heredoc(function() {/* # this is a comment (AND (GT, 3, 1) (= 1, 1) # so is this ) */}) logica.exec(src).should.equal(true) }) it('is ok to have tailing whitspace in an Operation', function () { var True = "(AND, true, true, true )" exec(True).should.equal(true) var True = "(AND true true true )" exec(True).should.equal(true) var True = "(AND (AND true true true \t \r\n ) true)" exec(True).should.equal(true) var True = "(AND true true true\n)" exec(True).should.equal(true) }) describe('runtime', function () { it('throws a ReferenceError if the Operation references an undefined SYMBOL', function () { var Throws = "(foo = 'baz')" expect(function () { exec(Throws, {}) }).to.throw(ReferenceError) }) }) describe('built in operators', function () { describe('AND', function () { it('returns false if any of the operands is false', function () { var False = "(AND, true, true, false)" exec(False).should.equal(false); }) it('returns true if all of the operands are true', function () { var True = "(AND, true, true, true, true)" exec(True).should.equal(true); }) }) describe('OR', function () { it('returns true if any of the operands is true', function () { var True = "(OR, false, false, false, true, false)" exec(True).should.equal(true); }) it('returns false if all of the operands are false', function () { var False = "(OR, false, false, false)" exec(False).should.equal(false); }) }) describe('NOT', function () { it('throws parse error if called with more than one operand', function () { var Throws = "(NOT, true, true)" expect(function () { exec(Throws) }).to.throw(/Parse error/) }) it('returns true if its operand is false', function () { var True = "(NOT, (AND, true, false))" exec(True).should.equal(true) }) it('returns false if its operand is true', function () { var False = "(NOT, true)" exec(False).should.equal(false) }) it('throws Parse error if called with a non-boolean operand', function () { var Throws = "(NOT, 't')" expect(function () { exec(Throws) }).to.throw(/Parse error/) }) it('throws if called with a Symbol operand which is not a boolean datatype', function () { var state = {foo: 'bar'} var Throws = "(NOT foo)" expect(function () { exec(Throws, state) }).to.throw(/boolean/) }) }) describe('=', function () { it('throws if operands are of different types', function () { var Throw = "(=, 1, '1')" expect(function () { exec(Throw) }).to.throw(/type mismatch/i) }) it('compares all of the operands for equality', function () { var False = "(=, 1, 2, 2, 1)" var True = "(=, 1, 1, 1)" exec(False).should.equal(false); exec(True).should.equal(true); }) }) describe('> (GT)', function () { // it('throws if not all operands are Numbers', function () { // var Throws = "(GT, 1, '3')" // expect(function () { // exec(Throws) // }).to.throw(/Number/) // }) it('throws if there are more than two operands', function () { var Throws = "(GT, 1, 2, 3)" expect(function () { exec(Throws) }).to.throw(/operands/) }) it('returns true if the first operand is greater than the second', function () { var True = "(GT 32234234, 8)" exec(True).should.equal(true); }) it('returns false if the first operand is not greater than the second', function () { var FalseEqual = "(GT, 1,1)" exec(FalseEqual).should.equal(false) var FalseLess = "(GT, 1, 2)" exec(FalseLess).should.equal(false) }) }) describe('>= (GTE)', function () { // it('throws if not all operands are Numbers', function () { // var Throws = "(GTE, 1, '3')" // expect(function () { // exec(Throws) // }).to.throw(/Number/) // }) it('throws if there are more than two operands', function () { var Throws = "(GTE, 1, 2, 3)" expect(function () { exec(Throws) }).to.throw(/operands/) }) it('returns true if the first operand is greater than or equal to the second', function () { var TrueGreater = "(GTE, 9000, 23)" exec(TrueGreater).should.equal(true); var TrueEq = "(GTE, 100, 100)" exec(TrueEq).should.equal(true); }) it('returns false if the first operand is not greater than or equal the second', function () { var FalseLess = "(GTE, 8, 12)" exec(FalseLess).should.equal(false); }) }) describe('< (LT)', function () { // it('throws if not all operands are Numbers', function () { // var Throws = "(LT, 1, '3')" // expect(function () { // exec(Throws) // }).to.throw(/Number/) // }) it('throws if there are more than two operands', function () { var Throws = "(LT, 1, 2, 3)" expect(function () { exec(Throws) }).to.throw(/operands/) }) it('returns true if the first operand is less than the second', function () { var True = "(LT, 1, 9999)" exec(True).should.equal(true); }) it('returns false if the first operand is not less than the second', function () { var False = "(LT, 23873030, 2)" exec(False).should.equal(false); }) }) describe('<= (LTE)', function () { // it('throws if not all operands are Numbers', function () { // var Throws = "(LTE, 1, '3')" // expect(function () { // exec(Throws) // }).to.throw(/Number/) // }) it('throws if there are more than two operands', function () { var Throws = "(LTE, 1, 2, 3)" expect(function () { exec(Throws) }).to.throw(/operands/) }) it('returns true if the first operand is less than or equal to the second', function () { var TrueEq = "(lte 123, 123)" exec(TrueEq).should.equal(true); var TrueLess = "(LTE 23, 235434)" exec(TrueLess).should.equal(true); }) it('returns false if the first operand is not less than or equal to the second', function () { var False = "(LTE 14, 0)" exec(False).should.equal(false) }) }) describe('IN', function () { it('returns true if the head operand is equal to any of the tail operands', function () { var True = "(42 IN 40, 41, 42, 43, 44, 45)" exec(True).should.equal(true) }) it('returns false if the head operand is not equal to any of the tail operands', function () { var False = "(42 IN 40, 41, 43, 44, 45)" exec(False).should.equal(false) }) it('returns true if the head operand is contained in a list in tailOperands', function () { var True = "('foo' in 'foo, bar, baz')" exec(True).should.equal(true) }) it('returns false if the head operand is contained in a list in tailOperands', function () { var False = "('fo' in 'foo, bar, baz')" exec(False).should.equal(false) }) it('lists are strings, delimited by either a comma or whitespace (like operands)', function () { var True = "('foo' in 'foo, bar, baz')" exec(True).should.equal(true) var True = "('foo' in 'foo bar baz')" exec(True).should.equal(true) var True = "('foo' in 'foo,bar baz') # don't actually do this, it's confusing (but valid)" exec(True).should.equal(true) }) it('returns true if the head operand is a list which is a subset of one of the tail operands', function () { var True = "('foo, bar' in 'foo, bar, baz')" exec(True).should.equal(true) var True = "('foo, bar' in 'foo, baz' 'bar')" exec(True).should.equal(true) }) }) }) describe('syntax errors', function () { it('throws if complement is called with a non-boolean literal', function () { expect(function() { exec('(not null)') }).to.throw(/Parse/) expect(function() { exec('(not "foo")') }).to.throw(/Parse/) expect(function() { exec('(not 12)') }).to.throw(/Parse/) }) it('throws if combination is called with a non-boolean literal', function () { expect(function() { exec('(and null true)') }).to.throw(/Parse/) expect(function() { exec('(and 12 true)') }).to.throw(/Parse/) expect(function() { exec('(and "sdf" true)') }).to.throw(/Parse/) expect(function() { exec('(or null true)') }).to.throw(/Parse/) expect(function() { exec('(or 12 true)') }).to.throw(/Parse/) expect(function() { exec('(or "sdf" true)') }).to.throw(/Parse/) }) it('throws if there are mixed type literals in an operation', function () { expect(function() { exec('(1 = null)') }).to.throw(/operand type mismatch/) expect(function() { exec('(1 = 1 null)') }).to.throw(/operand type mismatch/) expect(function() { exec('("a" = 12)') }).to.throw(/operand type mismatch/) expect(function() { exec('(a = true 2)') }).to.throw(/operand type mismatch/) expect(function() { exec('(a = b c "12" 12)') }).to.throw(/operand type mismatch/) }) }) describe('runtime typechecking', function () { it('throws if complement is called with a non-boolean symbol', function () { expect(function() { exec('(not foo)', {foo: 'baz'}) }).to.throw(/boolean/) }) it('throws if combination is called with a non-boolean symbol', function () { expect(function() { exec('(and bar)', {bar: 12}) }).to.throw(/boolean/) expect(function() { exec('(and bar)', {bar: undefined}) }).to.throw(/boolean/) expect(function() { exec('(and bar)', {bar: null}) }).to.throw(/boolean/) expect(function() { exec('(and bar)', {bar: 'blarg'}) }).to.throw(/boolean/) expect(function() { exec('(or bar)', {bar: 12}) }).to.throw(/boolean/) expect(function() { exec('(or bar)', {bar: undefined}) }).to.throw(/boolean/) expect(function() { exec('(or bar)', {bar: null}) }).to.throw(/boolean/) expect(function() { exec('(or bar)', {bar: 'blarg'}) }).to.throw(/boolean/) }) }) })