chr
Version:
Interpreter for Constraint Handling Rules (CHR) in JavaScript
417 lines (353 loc) • 10.5 kB
JavaScript
module.exports = Compiler
const util = require('./util')
const fakeScope = require('./fake-scope')
const indent = util.indent
const indentBy = util.indentBy
const destructuring = util.destructuring
const escape = util.escape
function Compiler (rule, opts) {
opts = opts || {}
this.rule = rule
this.replacements = opts.replacements || {}
this.scope = opts.scope || {}
this.opts = {
this: opts.this || 'this',
helper: opts.helper || 'self.Helper',
defaultCallbackNames: opts.defaultCallbackNames || ['cb', 'callback']
}
}
Compiler.prototype.headNo = function compileHeadNo (headNo) {
const self = this
headNo = headNo || 0
const rule = this.rule
const opts = this.opts
if (!rule.head[headNo]) {
throw new Error('No constraint with number ' + headNo + ' in this rule head')
}
const constraint = rule.head[headNo]
if (constraint.type !== 'Constraint') {
throw new Error('No constraint at number ' + headNo)
}
let parts = []
let level = 0
parts.push(
indent(level) + 'var self = ' + opts.this,
indent(level) + ''
)
if (constraint.arity > 0) {
parts = parts.concat(destructuring(constraint, 'constraint.args').map(indentBy(level)))
parts.push(
indent(level)
)
}
// start:def_constraintIds
parts.push(
indent(level) + 'var constraintIds = ['
)
rule.head.forEach(function (head, headIndex) {
let line = headIndex === 0 ? indent(1) : ', '
if (headIndex === headNo) {
line += '[ constraint.id ]'
} else {
line += 'self.Store.lookup("' + head.name + '", ' + head.arity + ')'
}
parts.push(
indent(level) + line
)
})
parts.push(
indent(level) + ']',
indent(level) + ''
)
// end:def_constraintIds
// start:return_promise
parts.push(
indent(level) + 'return new Promise(function (resolve, reject) {'
)
level += 1
// start:def_iterator
parts.push(
indent(level) + this.opts.helper + '.forEach(constraintIds, function iterateConstraint (ids, callback) {'
)
level += 1
// start:test_allAlive
parts.push(
indent(level) + 'if (!self.Store.allAlive(ids))',
indent(level) + ' return callback()',
indent(level)
)
// end:test_allAlive
// start:test_ruleFired
parts.push(
indent(level) + 'if (self.History.has("' + rule.name + '", ids))',
indent(level) + ' return callback()',
indent(level)
)
// end:test_ruleFired
// start:destructuring_other_constraints
rule.head.forEach(function (head, headIndex) {
if (headIndex === headNo) {
return
}
if (head.arity > 0) {
parts = parts.concat(destructuring(head, 'self.Store.args(ids[' + headIndex + '])', 'return callback()').map(indentBy(level)))
parts.push(
indent(level)
)
}
})
// end:destructuring_other_constraints
// start:guards_promises
// generates something like:
// var guards = []
if (rule.guard && rule.guard.length > 0) {
parts = parts.concat(
this.generateGuardPromisesArray().map(indentBy(level))
)
parts.push(
indent(level) + 'Promise.all(guards)',
indent(level) + '.then(function () {'
)
level += 1
}
parts.push(
indent(level) + 'self.History.add("' + rule.name + '", ids)'
)
for (let k = rule.r + 1; k <= rule.head.length; k++) {
// remove constraints
parts.push(
indent(level) + 'self.Store.kill(ids[' + (k - 1) + '])'
)
}
// start:tells
if (rule.body.length > 0) {
parts.push(
indent(level)
)
parts = parts.concat(self.generateTellPromises().map(indentBy(level)))
} else {
parts.push(
indent(level) + 'callback()'
)
}
// end: tells
if (rule.guard && rule.guard.length > 0) {
level -= 1
parts.push(
indent(level) + '})',
indent(level) + '.catch(function () {',
indent(level + 1) + ' callback()',
indent(level) + '})'
)
}
// end:guards_promises
level -= 1
parts.push(
indent(level) + '}, resolve)'
)
// end:def_iterator
level -= 1
parts.push('})')
// end:return_promise
return parts
}
Compiler.prototype.generateGuardPromisesArray = function generateGuardPromisesArray () {
const self = this
let parts = []
parts.push(
'var guards = ['
)
this.rule.guard.forEach(function (guard, guardIndex) {
const expr = guardIndex === 0 ? indent(1) : ', '
if (guard.type === 'Replacement' && typeof guard.num !== 'undefined') {
// get parameters via dependency injection
const params = util.getFunctionParameters(self.replacements[guard.num])
const lastParamName = util.getLastParamName(params)
parts.push(
expr + 'new Promise(function (s, j) {',
indent(2) + 'var ' + lastParamName + ' = function (e, r) { (e || !r) ? j() : s() }',
indent(2) + 'replacements["' + guard.num + '"].apply(self, [' + params + '])',
indent(1) + '})'
)
return
}
if (guard.type === 'Replacement' && typeof guard.expr !== 'undefined') {
parts = parts.concat(fakeScope(self.scope, guard.expr.original, { isGuard: true }).map(function (row, rowId) {
if (rowId === 0) {
return expr + row
}
return indent(1) + row
}))
return
}
parts.push(
expr + 'new Promise(function (s, j) { (' + self.generateGuard(guard) + ') ? s() : j() })'
)
})
parts.push(
']',
''
)
return parts
}
Compiler.prototype.generateGuards = function generateGuards () {
const self = this
const rule = this.rule
let expr = 'if ('
const boolExprs = []
rule.guard.forEach(function (guard) {
if (guard.type !== 'Replacement') {
boolExprs.push(self.generateGuard(guard))
}
})
expr += boolExprs.join(' && ')
expr += ') {'
return expr
}
Compiler.prototype.generateGuard = function generateGuard (guard) {
if (guard.type === 'BinaryExpression') {
return this.generateBinaryExpression(guard)
}
return 'false'
}
Compiler.prototype.generateTellPromises = function generateTellPromises () {
const self = this
let parts = []
parts.push('Promise.resolve()')
this.rule.body.forEach(function (body, bodyIndex) {
if (body.type === 'Constraint' && body.name === 'true' && body.arity === 0) {
return
}
parts.push('.then(function () {')
if (body.type === 'Constraint') {
let expr = indent(1) + 'return self.' + body.name + '('
expr += body.parameters.map(function (parameter) {
return self.generateExpression(parameter)
}).join(', ')
expr += ')'
parts.push(expr)
parts.push('})')
return
}
if (body.type === 'Replacement' && typeof body.expr !== 'undefined') {
parts = parts.concat(fakeScope(self.scope, body.expr.original).map(function (row, rowId) {
if (rowId === 0) {
return 'return ' + row
}
return indent(1) + row
}))
parts.push('})')
return
}
let params
let lastParamName
if (body.type === 'Replacement' && typeof body.num !== 'undefined') {
// get parameters via dependency injection
params = util.getFunctionParameters(self.replacements[body.num])
lastParamName = util.getLastParamName(params)
if (lastParamName && self.opts.defaultCallbackNames.indexOf(lastParamName) > -1) {
// async
parts.push(
indent(1) + 'return new Promise(function (s) {',
indent(2) + 'var ' + lastParamName + ' = s',
indent(2) + 'replacements["' + body.num + '"].apply(self, [' + params + '])',
indent(1) + '})',
'})'
)
} else {
// sync
parts.push(
indent(1) + 'return new Promise(function (s) {',
indent(2) + 'replacements["' + body.num + '"].apply(self, [' + params + '])',
indent(2) + 's()',
indent(1) + '})',
'})'
)
}
return
}
if (body.type === 'Replacement' && typeof body.func !== 'undefined') {
var func = eval(body.func) // eslint-disable-line
params = util.getFunctionParameters(func)
lastParamName = util.getLastParamName(params, true)
if (lastParamName && self.opts.defaultCallbackNames.indexOf(lastParamName) > -1) {
// async
parts.push(
indent(1) + 'return new Promise(function (s) {',
indent(2) + '(' + body.func + ').apply(self, [' + util.replaceLastParam(params, 's') + '])',
indent(1) + '})',
'})'
)
} else {
// sync
parts.push(
indent(1) + 'return new Promise(function (s) {',
indent(2) + '(' + body.func + ').apply(self, [' + params + '])',
indent(2) + 's()',
indent(1) + '})',
'})'
)
}
}
})
parts.push(
'.then(function () {',
indent(1) + 'callback()',
'})',
'.catch(function() {',
indent(1) + 'reject()',
'})'
)
return parts
}
Compiler.prototype.generateTell = function generateTell (body) {
const self = this
let expr = ''
if (body.type === 'Constraint') {
expr += 'self.' + body.name + '('
expr += body.parameters.map(function (parameter) {
return self.generateExpression(parameter)
}).join(', ')
expr += ')'
return [expr]
}
if (body.type === 'Replacement') {
if (typeof body.expr !== 'undefined') {
return fakeScope(self.scope, body.expr.original)
}
return
}
expr += [
'if (!(' + self.generateBinaryExpression(body) + ')) {',
indent(1) + 'self.Store.invalidate()',
indent(1) + 'return',
'}'
].join('\n')
return [expr]
}
Compiler.prototype.generateBinaryExpression = function generateBinaryExpression (expr) {
const self = this
return ['left', 'right'].map(function (part) {
if (expr[part].type === 'Identifier') {
return expr[part].name
}
if (expr[part].type === 'Literal') {
return escape(expr[part].value)
}
if (expr[part].type === 'BinaryExpression') {
return '(' + self.generateBinaryExpression(expr[part]) + ')'
}
return ''
}).join(' ' + expr.operator + ' ')
}
Compiler.prototype.generateExpression = function generateExpression (parameter) {
if (parameter.type === 'Identifier') {
return parameter.name
}
if (parameter.type === 'BinaryExpression') {
return this.generateBinaryExpression(parameter)
}
if (parameter.type === 'Literal') {
return escape(parameter.value)
}
}