logica
Version:
a compile-to-javascript predicate logic language
401 lines (353 loc) • 12.8 kB
JavaScript
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/)
})
})
})