logica
Version:
a compile-to-javascript predicate logic language
166 lines (150 loc) • 4.43 kB
JavaScript
// UMD voodo:
(function (root, factory) {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define(factory);
}
}(this, function () {
function merge(objA, objB) {
var newObj = {}
for (var key in objA) {
newObj[key] = objA[key];
}
for (var key in objB) {
newObj[key] = objB[key];
}
return newObj;
}
var toArray = function (a) {
return Array.prototype.slice.call(a);
}
var tail = function (a) {
return Array.prototype.slice.call(a, 1);
}
var I = function (x) { return x; }
var TYPE = {
'any': 0,
'0': 'any',
'nullable': 1,
'1': 'nullable',
'null': 2,
'2': 'null',
'boolean': 4,
'4': 'boolean',
'number': 8,
'8': 'number',
'string': 16,
'16': 'string'
}
var defaultOperators = {
'EQ': function (operand1) {
var operands = toArray(arguments);
var type = typeof operand1;
return operands.every(function (op) {
if (typeof op !== type) {
throw new TypeError('Operand type mismatch');
}
return op === operand1;
})
},
'GT': function (op1, op2) {
if (arguments.length !== 2) {
throw new Error('GT must be called with exactly two operands')
}
if (typeof op1 !== typeof op2 || typeof op1 !== 'number') {
throw new Error('GT must be called with Number operands')
}
return op1 > op2;
},
'GTE': function (op1, op2) {
if (arguments.length !== 2) {
throw new Error('GTE must be called with exactly two operands')
}
if (typeof op1 !== typeof op2 || typeof op1 !== 'number') {
throw new Error('GTE must be called with Number operands')
}
return op1 >= op2;
},
'LT': function (op1, op2) {
if (arguments.length !== 2) {
throw new Error('LT must be called with exactly two operands')
}
if (typeof op1 !== typeof op2 || typeof op1 !== 'number') {
throw new Error('LT must be called with Number operands')
}
return op1 < op2;
},
'LTE': function (op1, op2) {
if (arguments.length !== 2) {
throw new Error('LTE must be called with exactly two operands')
}
if (typeof op1 !== typeof op2 || typeof op1 !== 'number') {
throw new Error('LTE must be called with Number operands')
}
return op1 <= op2;
},
'IN': function (head) {
var rest = [];
tail(arguments)
.forEach(function (op) {
if (typeof op === 'string') {
rest.push.apply(rest,
op.split(/[, ]/)
.map(function (str) { return str.trim() }))
}
rest.push(op)
})
if (typeof head === 'string') {
return head.split(/[, ]/).every(function (headOp) {
return rest.some(function (op) { return op === headOp })
})
} else {
return rest.some(function (op) {
return op === head
})
}
},
// check for required typed references in state object
'$': function(state, refs) {
for(var ref in refs) {
if (!(ref in state)) {
throw new ReferenceError('Symbol `' + ref + '` used in expression but not defined')
}
var expectedType = TYPE[refs[ref]];
if (expectedType === 'any') return;
var type = typeof state[ref];
if (type !== expectedType) {
throw new TypeError('Symbol `' + ref + '` must be a ' + expectedType + ', but is ' + type)
}
}
}
}
var functionBodyRegex = /function\s*?\(.*?\)\s*?{(.*)}/;
function hydrate(src, customOperators) {
var operators = !customOperators ? defaultOperators
: merge(defaultOperators, customOperators);
var fn;
try{
if (src.substr(0,4) === '/**/') {
// use compact source representation
src = src.split('/**/')
var symbols = src[1], expr = src[2];
src = "$.$(v," + symbols + ");return " + expr + ";";
fn = new Function('$','v', src);
fn.symbols = JSON.parse(symbols);
} else {
// try hydrating from full function src
var body = src.match(functionBodyRegex)[1];
fn = new Function('$','v', body)
}
} catch (e) {
console.log(e.stack)
throw new Error('Invalid function source', e);
}
return function (state) {
return fn.call(null, operators, state);
}
}
return hydrate;
}));