expexp
Version:
The express model io and express model and data representation.
1,036 lines (963 loc) • 32.7 kB
JavaScript
import {traverseSimpleExpression, traverseExpression, traverseSimpleFactor}
from './mniTree.js'
import {VariableScope} from './variableScope.js'
//
// TODO distinguish string from enum value (create a malicious test first)
// TODO how to know for operations with two aggregate operands what is the
// type of result set,bag,list,array? e.g. in the handleInOp
// The HIINDEX, LOINDEX, HIBOUND, LOBOUND depend on that.
export function Evaluator(model, inst, scope) {
const me = {}
const vs = [[]] // stack of current value(s)
// The first dim grows with argument lists, parenthesis.
// The second dim grows and shrinks while evaluating term or simple_expression.
const ss = [scope] // scope stack (only grows with function calls or query expressions)
const errors = [] // List of error messages
let ignore = false // If set, this references the json, that ends the ignore state.
me.selfValue = null
const setup = function() {
// Do nothing.
}
// Pushes / starts an empty new array on the value stack.
const pushNewArr = function() {
vs.push([])
}
// Most of the time this should not be used directly.
// It is more internal for the others stack operations.
// But there are certain sensible exceptions: When a infrastructure array
// becomes a data array may be, and others.
const curArr = function() {
return vs[vs.length-1]
}
const curVal = function(off = 0) {
const curAr = curArr()
return curAr[curAr.length-1+off] // most of the time curArr[0]
}
const pushVal = function(value) {
curArr().push(value)
}
const chgVal = function(value) {
const curAr = curArr()
if (0 < curAr.length) {
curAr[curAr.length-1] = value
} else {
curAr.push(value)
}
}
const popVal = function() {
return curArr().pop() // Is undefined if array is empty.
}
const popArr = function() {
return vs.pop()
}
const popTransfer = function() {
const actVal = curVal()
vs.pop()
pushVal(actVal)
}
const is = function(path) {
return new Array(path.length * 2 + 1).join(' ')
}
const nice = function(val) { // Similar to instance.toStringRep
if (typeof val == 'string') {
val = '"'+val+'"'
} else if (val instanceof Uint8Array) {
val = `Uint8Array(${val.length})[..]`
}
return val
}
const path2str = function(path, json, idx) {
const strs = path.concat([json]).map(function(elt, i, arr){
let parent // undefined
if (0 < i) {
parent = arr[i-1]
}
let eltStr // undefined
if (Array.isArray(elt)) {
eltStr = ':'
} else { // object
eltStr = `{${elt.t}}`
}
if (Array.isArray(parent)) {
return `[${parent.indexOf(elt)}]${eltStr}`
} else if (parent) { // parent is object
const attr = Object.entries(parent).find(kv => (kv[1] === elt))
if (attr) {
return `.${attr[0]}${eltStr}`
} else {
return `??${eltStr}`
}
} else { // first elt
return eltStr
}
})
// TODO also add json and idx
return strs.join('')
}
const beforeFct = function(json, path, idx) {
if (ignore) return
// const parent = path[path.length-1]
// We rarely have to something before (rather after).
// TODO also those with arg0 and arg1 need a current value stack push.
switch (json.t) {
case 'expr':
case 'bifct':
case 'func_call':
pushNewArr()
console.log(is(path),'bf', json.t, 'PUSH')
break
case 'factor':
handleBeforeFactor(json, path, idx)
break
case 'query':
ignore = json.cond // ignore everything until after condition
// Also json.src is not visited / handled.
console.log(is(path),'bf', json.t, 'IGNORE START (until af expr)')
break
case 'aggr': // aggregate initializer
pushNewArr() // This sturcture array will become a data array in afterFct.
console.log(is(path),'bf', json.t, 'PUSH')
break
default:
console.log(is(path),'bf', json.t)
}
}
const afterFct = function(json, path, idx) {
if (ignore === json) {
ignore = false
console.log(is(path),'af', json.t, 'IGNORE END')
return
} else if (ignore) {
return
}
// const parent = path[path.length-1]
switch (json.t) {
case 'integer':
case 'real':
case 'string':
console.log(is(path),'af', json.t, json.value)
pushVal(json.value)
break
case 'var_ref':
handleVarRef(json, path, idx)
break
// case 'type_ref': This comes before enum_value and is ingored here.
case 'func_call':
handleFuncCall(json, path, idx)
break
case 'bifct':
handleBiFct(json, path, idx)
break
case 'biconst':
handleBiConst(json, path, idx)
break
case 'expr':
handleExpr(json, path, idx)
break
case 'factor':
handleAfterFactor(json, path, idx)
break
case 'query':
handleAfterQuery(json, path, idx)
break
case 'aggr':
handleAfterAggr(json, path, idx)
break
case 'relopext':
case 'addlikeop':
case 'multlikeop':
console.log(is(path),'af', json.t, json.id)
break
case 'attr_ref': // Old term attr_qual.
handleAfterAttrRef(json, path, idx)
break
case 'entity_ref':
handleAfterEntityRef(json, path, idx)
break
case 'idx_qual':
handleAfterIdxQual(json, path, idx)
break
case 'enum_value':
handleAfterEnumValue(json, path, idx)
break
default:
console.log(is(path),'af', json.t)
}
}
const beforeArrFct = function(json, path, idx) {
if (ignore) return
if (0<json.length) {
const parent = 0<path.length?path[path.length-1]:null
if (parent) {
if (Array.isArray(parent)) { // is term
handleBeforeTerm(json, path, idx)
} else if (parent.t == 'expr') { // is simple expression
console.log(is(path),'bA', json.length, 'simple_expression PUSH')
pushNewArr()
} else {
console.log(is(path),'bA', json.length)
}
} else {
console.log(is(path),'bA', json.length)
}
} // ignore empty arrays
}
const afterArrFct = function(json, path, idx) {
if (ignore === json) {
ignore = false
console.log(is(path),'aA', json.length, 'IGNORE END')
return
} else if (ignore) {
return
}
if (0<json.length) {
const parent = 0<path.length?path[path.length-1]:null
if (parent) {
if (Array.isArray(parent)) { // is term
handleAfterTerm(json, path, idx)
} else if (parent.t == 'expr') { // is simple expression
console.log(is(path),'aA', json.length, 'simple_expression POP')
popTransfer()
} else if (parent.t == 'bifct' || parent.t == 'func_call') {
console.log(is(path),'aA', json.length, 'args')
} else {
console.log(is(path),'aA', json.length)
}
} else {
console.log(is(path),'bA', json.length)
}
} // ignore empty arrays
}
const handleVarRef = function(json, path, idx) {
const actVal = ss[ss.length-1].get(json.id)
console.log(is(path),'af', json.t, json.id, actVal)
pushVal(actVal)
}
const handleFuncCall = function(json, path, idx) {
const actParams = curArr()
popArr()
console.log(is(path),'af', json.t, json.id, actParams.length, 'POP')
//const funMdl = model.functionById(json.id) // .params, .locals, .result
// TODO handle visibility by checking:
// funMdl.parent == null || me.selfFunc ?===? funMdl.parent || or the funMdl.parent's parent ...
const res = inst.evalFuncSave(json.id, actParams)
if (res.result) {
pushVal(res.value)
} else {
// TODO Is it one or several messages?
pushVal(INDETERMINATE)
errors.push.apply(errors, res.messages)
}
}
const handleBiFct = function(json, path, idx) {
console.log(is(path),'af', json.t, json.id, json.values.length, 'POP')
let actVal // undefined
switch (json.id) {
case 'exists':
actVal = evalExists(curVal(), json, path, idx)
break
case 'sizeof':
actVal = evalSizeOf(curVal(), json, path, idx)
break
case 'length':
actVal = evalLength(curVal(), json, path, idx)
break
case 'blength':
actVal = evalBlength(curVal(), json, path, idx)
break
case 'typeof':
actVal = evalTypeof(curVal(), json, path, idx)
break
case 'usedin':
handleAfterUsedIn(json, path, idx) // chgVal with result array
actVal = curVal()
break
default:
throw new Error('Unknown built in function ' + json.id)
}
popArr()
pushVal(actVal)
}
// This one behaves like an operation function: It chgVal just the result array.
const handleAfterUsedIn = function(json, path, idx) {
handleTwoOperands(
'usedIn',
(a)=>(inst.isEntity(a)?'entity token':false),
(b)=>((b == '' || model.resolveRoleName(b))?'valid role name or empty string':false),
(a,b)=>(inst.usedIn(a, b)),
json, path, idx
// Take result array of tokens from usedIn unchanged.
// Defaults to INDETERMINATE.
)
}
const evalTypeof = function(val, json, path, idx) {
if (isIndeterminate(val)) {
return INDETERMINATE
}
if (inst.isToken(val)) {
const scId = model.schemaId()
const dta = inst.rawData(val) // We only need this to distinguish and the model ids.
let chn // undefined
if (dta.nttId) {
chn = model.entityIdChainById(dta.nttId)
} else { // dta.typeId
chn = model.typeIdChainById(dta.typeId)
// TODO base type or aggregation might be missing
// TODO If it is REAL or INTEGER it is also NUMBER.
}
const res = chn.map(eltId => scId.toUpperCase() + '.' + eltId.toUpperCase())
return res
} else {
errors.push(`The ${json.t} ${json.id} cannot determine the type strings of invalid value ${nice(val)} for ${path2str(path, json, idx)}.`)
return INDETERMINATE
}
}
const evalExists = function(val, json, path, idx) {
if (isIndeterminate(val)) {
return false // Not INDETERMINATE .
}
if (isUnknown(val) == false) {
return true
} else {
// UNKNOWN is a valid value, that exists.
return true
}
}
const evalSizeOf = function(val, json, path, idx) {
if (isIndeterminate(val)) {
return INDETERMINATE
}
if (Array.isArray(val)) {
return val.length
} else {
errors.push(`The ${json.t} ${json.id} cannot take size of value ${nice(val)} for ${path2str(path, json, idx)}.`)
return INDETERMINATE
}
}
const evalLength = function(val, json, path, idx) {
if (isIndeterminate(val)) {
return INDETERMINATE
}
if (typeof val == 'string') { // TODO distinguish string from enum value
return val.length
} else {
errors.push(`The ${json.t} ${json.id} cannot take length of value ${nice(val)} for ${path2str(path, json, idx)}.`)
return INDETERMINATE
}
}
const evalBlength = function(val, json, path, idx) {
if (isIndeterminate(val)) {
return INDETERMINATE
}
if (val instanceof Uint8Array) {
return val.length * 8 // TODO that is not exact by the last bits - what to do?
} else {
errors.push(`The ${json.t} ${json.id} cannot take length of binary value ${nice(val)} for ${path2str(path, json, idx)}.`)
return INDETERMINATE
}
}
const handleLogicalOp = function(caseStr, dcnMx, json, path, idx) {
if (grantTwoSameOperands(caseStr, toLogicalTypeStr, json, path, idx) == false) {
chgVal(INDETERMINATE) // Change or push. (More errors might follow.)
return
}
let left = curVal(-1)
let right = curVal()
if (isIndeterminate(left)) left = UNKNOWN
if (isIndeterminate(right)) right = UNKNOWN
chgVal(dcnMx[left][right])
}
const handleInOp = function(json, path, idx) {
handleTwoOperands(
'membership (in)',
function(value){ // Is only called, if two operands are granted.
const right = curVal()
// But the right value is not yet granted its type.
if (toArrayTypeStr(right)) {
// TODO check value is of base type of aggregation (array)
return 'ok' // TODO return false if the check fails
} else {
// The whole op will be invalid and result in INDETERMINATE because
// the richt value is not valid. So we do not matter about the left.
return 'ok'
}
},
toArrayTypeStr,
(a,b)=>(b.includes(a)),
json, path, idx,
x=>x, UNKNOWN
)
}
const handleCompareOp = function(caseStr, cmpFct, json, path, idx, doTestAggrAndEntity = false) {
if (grantTwoOperands(caseStr, json, path, idx) == false) {
chgVal(INDETERMINATE) // Change or push. (More errors might follow.)
return
}
const left = curVal(-1)
const right = curVal()
const cmp = isIndeterminate(left)?right:left
switch (typeof cmp) {
case 'number':
chgVal(cmpFct(left, right))
break
case 'string': // TODO how distinguish from enum value?
chgVal(cmpFct(left, right)) // compare lexicograhically is ok
break
case 'null': // UNKNOWN
case 'boolean':
chgVal(cmpFct(left, right)) // TODO check that is correct?
break
default:
if (doTestAggrAndEntity && Array.isArray(cmp)) {
// TODO aggregation compare
} else if (cmp instanceof Uint8Array) {
// TODO binary compare
} else if (doTestAggrAndEntity && false) { // TODO use isToken(cmp) instead of false
// TODO entity compare
} else if (isIndeterminate(left) || isIndeterminate(right)) {
chgVal(UNKNOWN) // not an error case and not INDETERMINATE
} else {
const tLeft = typeof left
const tRight = typeof right
errors.push(`From neither type of operand (neither left ${tLeft} nor right ${tRight}) the type of ${caseStr} operation can be detremined at ${path2str(path, json, idx)}.`)
chgVal(INDETERMINATE)
}
}
}
const handlePlusOp = function(json, path, idx) {
if (grantTwoOperands('plus', json, path, idx) == false) {
chgVal(INDETERMINATE) // Change or push. (More errors might follow.)
return
}
const left = curVal(-1)
const right = curVal()
const cmp = isIndeterminate(left)?right:left
switch (typeof cmp) {
case 'number':
handleTwoSameOperands('addition', toNumberTypeStr, (a,b)=>(a+b), json, path, idx) // no need to fix result
break
case 'string': // TODO how distinguish from enum value?
handleTwoSameOperands('string concat', toStringTypeStr, (a,b)=>(a+b), json, path, idx) // no need to fix result
break
default:
if (Array.isArray(cmp)) {
// TODO aggregation union
} else if (cmp instanceof Uint8Array) {
// TODO concat binary
} else if (isIndeterminate(left) || isIndeterminate(right)) {
chgVal(INDETERMINATE) // not an error case
} else {
const tLeft = typeof left
const tRight = typeof right
errors.push(`From neither type of operand (neither left ${tLeft} nor right ${tRight}) the type of plus operation can be detremined at ${path2str(path, json, idx)}.`)
chgVal(INDETERMINATE)
}
}
}
const handleMinusOp = function(json, path, idx) {
handleMinusOrAsteriskOp(
'minus',
'subtraction', toNumberTypeStr, (a,b)=>(a-b),
'difference', TODO=>TODO, TODO=>TODO,
json, path, idx
)
}
const handleAsteriskOp = function(json, path, idx) {
handleMinusOrAsteriskOp(
'asterisk',
'multiplication', toNumberTypeStr, (a,b)=>(a*b),
'intersect', TODO=>TODO, TODO=>TODO,
json, path, idx
)
}
const handleMinusOrAsteriskOp = function(
caseStr,
numCaseStr, numTypeFct, numFct,
aggrCaseStr, aggrTypeFct, aggrFct, // TODO hope that is all needed
json, path, idx
){
if (grantTwoOperands(caseStr, json, path, idx) == false) {
chgVal(INDETERMINATE) // Change or push. (More errors might follow.)
return
}
const left = curVal(-1)
const right = curVal()
const cmp = isIndeterminate(left)?right:left
if (typeof cmp == 'number') {
handleTwoSameOperands(numCaseStr, numTypeFct, numFct, json, path, idx) // no need to fix result
} else if (Array.isArray(cmp)) {
// TODO aggregation difference resp. intersect (use aggrCaseStr, aggrTypeFct, aggrFct somehow)
} else if (isIndeterminate(left) || isIndeterminate(right)) {
chgVal(INDETERMINATE) // not an error case
} else {
const tLeft = typeof left
const tRight = typeof right
errors.push(`From neither type of operand (neither left ${tLeft} nor right ${tRight}) the type of ${caseStr} operation can be detremined at ${path2str(path, json, idx)}.`)
chgVal(INDETERMINATE)
}
}
const toNumberTypeStr = function(value) {
if (typeof value == 'number') {
return Number.isInteger(value)?'int':'real'
}
return false
}
const toStringTypeStr = function(value) {
const res = typeof value
return (res == 'string')?res:false
}
const toArrayTypeStr = function(value) {
const res = Array.isArray(value)
return res?'array':false
}
const toLogicalTypeStr = function(value) {
const t = typeof value
switch (t) {
case 'boolean':
case 'null': // UNKNOWN
case 'undefined': // INDETERMINATE (is also valid, for it is treated as UNKNOWN)
return 'logical'
break
default:
return false
}
}
const handleTwoOperands = function(caseStr, leftTypeFct, rightTypeFct, fct, json, path, idx, fixResultFct = x=>x, defaultValue = INDETERMINATE) {
if (grantTwoOperandsOfTypes(caseStr, leftTypeFct, rightTypeFct, json, path, idx) == false) {
chgVal(INDETERMINATE) // Change or push. (More errors might follow.)
return
}
handleTwoGrantedOpsOrDefault(fct, fixResultFct, defaultValue)
}
const handleTwoSameOperands = function(caseStr, typeFct, fct, json, path, idx, fixResultFct = x=>x, defaultValue = INDETERMINATE) {
if (grantTwoSameOperands(caseStr, typeFct, json, path, idx) == false) {
chgVal(INDETERMINATE) // Change or push. (More errors might follow.)
return
}
handleTwoGrantedOpsOrDefault(fct, fixResultFct, defaultValue)
}
// Both operands on the stack must already be granted to be valid (This
// comprises potential INDETERMINATE value).
const handleTwoGrantedOpsOrDefault = function(fct, fixResultFct = x=>x, defaultValue = INDETERMINATE) {
const left = curVal(-1)
const right = curVal()
if (isIndeterminate(left) == false && isIndeterminate(right) == false) {
chgVal(fixResultFct(fct(left, right)))
} else {
chgVal(defaultValue)
}
}
const grantTwoSameOperands = function(caseStr, typeFct, json, path, idx) {
return grantTwoOperandsOfTypes(caseStr, typeFct, undefined, json, path, idx)
}
// A *TypeFct takes a value as its sole argument and returns the type as
// string or false if the value is of invalid type. When talking of type, that
// does not mean JS types (returned by typeof). (JS typeof is only used for
// the error messages to provide a better hint of what is wrong.)
// If rightTypeFct is undefined, leftTypeFct is applied to both and
// additionally their result granted to be equal.
// Any operand INDETERMINATE is ok and results in true (even if the other
// one is invalid).
const grantTwoOperandsOfTypes = function(caseStr, leftTypeFct, rightTypeFct, json, path, idx) {
if (grantTwoOperands(caseStr, json, path, idx) == false) {
chgVal(INDETERMINATE) // Change or push. (More errors might follow.)
return
}
const left = curVal(-1)
const right = curVal()
// If any is INDETERMINATE, that is just ok.
// Even if the left is INDETERMINATE and a possible *TypeFct(right) == false.
if (isIndeterminate(left) || isIndeterminate(right)) {
return true
}
const tLeft = leftTypeFct(left)
const tRight = rightTypeFct?rightTypeFct(right):leftTypeFct(right)
if (tLeft && tRight) {
if (rightTypeFct == null) { // Just check if that param is passed.
// Grant that the types are the same.
if (tLeft != tRight) {
errors.push(`The first/left argument/operand is of type ${tLeft} and the second/right one of type ${tRight}, but they should be of the same type. This is invalid for ${caseStr} at ${path2str(path, json, idx)}.`)
return false
}
}
return true
} else { // One or both operands are invalid.
const errFct = function(side, tWrong) {
errors.push(`The ${side} argument/operand of type ${tWrong} is invalid for ${caseStr} at ${path2str(path, json, idx)}.`)
}
if (tLeft == false) {
errFct('first/left', typeof left)
}
if (tRight == false) {
errFct('second/right', typeof right)
}
return false
}
}
const grantTwoOperands = function(caseStr, json, path, idx) {
const curAr = curArr()
const res = 1 < curAr.length
if (res == false) {
errors.push(`There is only ${curAr.length} operand for ${caseStr} at ${path2str(path, json, idx)}.`)
}
return res
}
const handleBiConst = function(json, path, idx) {
console.log(is(path),'af', json.t, json.id)
let actVal // undefined
switch (json.id) {
case 'self':
actVal = me.selfValue
break
case 'true':
actVal = true
break
case 'false':
actVal = false
break
case 'unknown':
actVal = null // The third logical value.
break
case 'const_e':
actVal = Math.E
break
case 'pi':
actVal = Math.PI
break
case 'question_mark':
actVal = INDETERMINATE
break
default:
throw new Error('Unknown built in constant ' + json.id)
}
pushVal(actVal)
}
const handleExpr = function(json, path, idx) {
if (json.op) {
console.log(is(path),'af', json.t, json.op.id, 'POP')
switch(json.op.id) {
case 'eq':
handleCompareOp('equal', (a,b)=>(a==b), json, path, idx, true)
break
case 'ne':
handleCompareOp('not-equal', (a,b)=>(a!=b), json, path, idx, true)
break
case 'lt':
handleCompareOp('less-than', (a,b)=>(a<b), json, path, idx)
break
case 'le':
handleCompareOp('less-than-or-equal', (a,b)=>(a<=b), json, path, idx)
break
case 'gt':
handleCompareOp('greater-than', (a,b)=>(a>b), json, path, idx)
break
case 'ge':
handleCompareOp('greater-than-or-equal', (a,b)=>(a>=b), json, path, idx)
break
case 'seq': // TODO what is with entity tokens?
chgVal(curVal(-1) === curVal())
break
case 'sne': // TODO what is with entity tokens?
chgVal(curVal(-1) !== curVal())
break
case 'in':
handleInOp(json, path, idx)
break
// case 'like':
// // TODO
// break
default:
throw new Error('Unknown rel_op_extended ' + json.op.id)
}
} else {
console.log(is(path),'af', json.t, 'nop POP')
}
// console.log(curArr(), vs.length)
popTransfer()
}
const handleBeforeFactor = function(json, path, idx) {
const parent = path[path.length-1]
// Check if it is a series of AND and the result is already false.
if (0 < idx && idx % 2 == 0) {
// console.log('check false AND .. :', curVal(), parent[idx-2], parent[idx-1], json)
// It is actually very special to decide according to the current value
// whether to continue evaluation or not.
const multLikeOp = parent[idx-1]
if ((multLikeOp.id == 'and' && curVal() == false) == false) {
pushNewArr()
console.log(is(path),'bf', json.t, 'PUSH')
} else {
// Do not push and start skipping all until handleAfterFactor.
pushVal(false) // Just repeat the false value.
ignore = json
console.log(is(path),'bf', json.t, 'IGNORE START')
}
} else {
pushNewArr()
console.log(is(path),'bf', json.t, 'PUSH')
}
}
const handleAfterFactor = function(json, path, idx) {
const parent = path[path.length-1]
console.log(is(path),'af', json.t, idx, 'POP')
// TODO not only arg0 but also arg1 and quals0 and quals1
// TODO somehow use this:
// handleTwoSameOperands(
// 'exponentiation', toNumberTypeStr, (a,b)=>(a ** b),
// json, path, idx,
// x=>(Number.isFinite(x)?x:INDETERMINATE) // better fix the result
// )
popTransfer()
// Do the term calculation already here (especially for 'and'):
if (0 < idx && idx % 2 == 0) {
// console.log('calc:', parent[idx-2], parent[idx-1], json)
const multLikeOp = parent[idx-1]
switch (multLikeOp.id) {
case 'and':
handleLogicalOp('and', AND_OP_MX, json, path, idx)
break
case 'asterisk':
handleAsteriskOp(json, path, idx)
break
case 'divis':
handleTwoSameOperands(
'division', toNumberTypeStr, (a,b)=>(a/b),
json, path, idx,
x=>(Number.isFinite(x)?x:INDETERMINATE) // fix division by zero
)
break
case 'div':
// TODO more sophisticated
chgVal(Math.floor(curVal(-1) / curVal()))
break
case 'mod':
// TODO more sophisticated
chgVal(curVal(-1) % curVal())
break
// TODO || (entity construction)
default:
throw new Error('Unknown multiplication_like_op ' + multLikeOp.id)
}
}
}
const handleBeforeTerm = function(json, path, idx) {
const parent = 0<path.length?path[path.length-1]:null
// Check if it is a series of OR and the result is already true.
if (0 < idx && idx % 2 == 0) {
// console.log('check true OR .. :', curVal(), parent[idx-2], parent[idx-1], json)
// It is actually very special to decide according to the current value
// whether to continue evaluation or not.
const addLikeOp = parent[idx-1]
if ((addLikeOp.id == 'or' && curVal() == true) == false) {
console.log(is(path),'bA', json.length, 'term PUSH')
pushNewArr()
} else {
// Do not push and start skipping all until handleAfterTerm.
pushVal(true) // Just repeat the true value.
ignore = json
console.log(is(path),'bA', json.length, 'term IGNORE START')
}
} else {
console.log(is(path),'bA', json.length, 'term PUSH')
pushNewArr()
}
}
const handleAfterTerm = function(json, path, idx) {
const parent = 0<path.length?path[path.length-1]:null
console.log(is(path),'aA', json.length, idx, 'term POP')
popTransfer()
// Do the simple_expression calculation already here (especially for 'or'):
if (0 < idx && idx % 2 == 0) {
// console.log('calc:', parent[idx-2], parent[idx-1], json)
const addLikeOp = parent[idx-1]
switch (addLikeOp.id) {
case 'or':
handleLogicalOp('or', OR_OP_MX, json, path, idx)
break
case 'xor':
handleLogicalOp('xor', XOR_OP_MX, json, path, idx)
break
case 'plus':
handlePlusOp(json, path, idx) // arithmetic, string, binary or aggregation
break
case 'minus':
handleMinusOp(json, path, idx) // arithmetic or aggregation
break
default:
throw new Error('Unknown add_like_op ' + addLikeOp.id)
}
}
}
const handleAfterQuery = function(json, path, idx) {
const parent = path[path.length-1]
console.log(is(path),'af', json.t, idx)
// Catch up the ignored json.src traversal.
traverseSimpleExpression(json.src, afterFct, beforeFct, afterArrFct, beforeArrFct, path)
const arr = curVal() // curVal(-1) before the popVal()
// Start / continue evaluation for every p of arr.
// Start a traverseExpression(json.cond, ..) for every element of arr.
if (isIndeterminate(arr) == false) {
if (0 < arr.length) {
const res = []
for (const value of arr) {
const scp = VariableScope(ss[ss.length-1])
ss.push(scp)
scp.regVal(json.var, value)
// The way scope works, the value of json.var could be changed, but
// the way the query impl is this is not possible.
traverseExpression(json.cond, afterFct, beforeFct, afterArrFct, beforeArrFct, path)
const cond = popVal() // recently added by traverseExpression
if (cond == true) { // INDETERMINATE or UNKNOWN is just ignored (like false).
res.push(value)
}
ss.pop()
}
chgVal(res) // swap original full array with filtered one
} // Just keep null or empty array
} else {
chgVal(INDETERMINATE)
}
}
// Aggregate initializer
// The type of the result ist aggregate of generic.
const handleAfterAggr = function(json, path, idx) {
// Make internal structure array become a data array.
const arr = popArr() // It is very exceptional to use that return value.
pushVal(arr) // The whole array is the new value.
console.log(is(path),'af', json.t, 'POP')
}
const handleAfterAttrRef = function(json, path, idx) {
// In case of self the entity_ref is already replaced by the token.
if (curVal() === me.selfValue) {
// Same behaviour as var_ref.
handleVarRef(json, path, idx)
} else {
const nttTok = curVal()
if (isIndeterminate(nttTok) == false) {
// For evaluating that value on the other entity, we do not need to pass
// the current scope there as parent.
// This could be an explicit attribute, derived or inverse
const attrVal = inst.getNttValue(nttTok, json.id)
chgVal(attrVal)
console.log(is(path),'af', json.t, nttTok, '.' + json.id, '=', attrVal)
// That entity instance is not validated itself. Validation does not recur.
} else {
// Just let indeterminate on the stack.
console.log(is(path),'af', json.t, json.id, ' = INDET')
}
}
}
const handleAfterIdxQual = function(json, path, idx) {
// console.log(curArr())
// The latest stage contains an array followed by an integer index.
const arr = curVal(-1)
const idxVal = popVal()
if (Array.isArray(arr) && Number.isInteger(idxVal)) {
// TODO handle the indexing according to the actual type of the aggregation
// The indexing of an ARRAY may even be specified to start with a negative index.
const value = arr[idxVal-1] // Meaning of index is 1 = first element.
console.log(is(path),'af', json.t, idxVal, value)
chgVal(value)
} else {
console.log(is(path),'af', json.t, 'INDET')
chgVal(INDETERMINATE)
}
}
const handleAfterEntityRef = function(json, path, idx) {
console.log(is(path),'af', json.t, '! TODO !')
// console.log(curArr())
// The latest stage most probably contains an object followed by an entity_ref.
// TODO
// Outside of expressions entity_ref may also occur in other circumstances.
// TODO Check if inside a rule there is cofusion between entity_ref and
// that arrays of all objects of an entity.
}
const handleAfterEnumValue = function(json, path, idx) {
// console.log(curArr())
// The latest stage does not provide any value to operate on.
// The value pushed here only stems from the meta data enum type definition.
const factor = path[path.length-2] // Parent is a qualifier array within a 'factor'.
// Question is: Does this enum_value appear in quals0 or quals1 of the 'factor'.
let typeRef // undefined
if (!factor.quals1 || factor.quals0[0] === json) {
typeRef = factor.arg0 // Presumed to be a type_ref.
} else { // factor.quals1
typeRef = factor.arg1 // Presumed to be a type_ref.
}
const enumTypeId = typeRef.id
const enumValue = json.id
// Model already checked that model.hasType(typeId) - an enum - and
// that enumValue belongs to it.
const enumType = model.typeById(enumTypeId)
// console.log(type.spec.t, type.spec.values.map(elt => elt.id).join(','))
pushVal(enumValue, enumType)
console.log(is(path),'af', json.t, enumTypeId, enumValue)
}
const evalToCurrentOrErrors = function() {
if (errors.length == 0) {
return {result:true, value:curVal()}
} else {
return {result:false, messages:errors}
}
}
me.evalSimpleExpr = function(json) {
traverseSimpleExpression(json, afterFct, beforeFct, afterArrFct, beforeArrFct)
return evalToCurrentOrErrors()
}
me.evalExpr = function(json) {
traverseExpression(json, afterFct, beforeFct, afterArrFct, beforeArrFct)
return evalToCurrentOrErrors()
}
setup()
return me
}
// Logical
export const TRUE = true
export const FALSE = false
export const UNKNOWN = null
// Setup logical OP matrices.
const AND_OP_MX = {}
const OR_OP_MX = {}
const XOR_OP_MX = {}
for (const mx of [AND_OP_MX, OR_OP_MX, XOR_OP_MX]) {
mx[true] = {} // true converted to 'true'
mx[false] = {} // false converted to 'false'
mx[UNKNOWN] = {} // null converted to 'null'
}
// and (false is strongest, then UNKNOWN, else true)
AND_OP_MX[false][false] = false
AND_OP_MX[true][false] = false
AND_OP_MX[false][true] = false
AND_OP_MX[UNKNOWN][false] = false
AND_OP_MX[false][UNKNOWN] = false
AND_OP_MX[UNKNOWN][UNKNOWN] = UNKNOWN
AND_OP_MX[true][UNKNOWN] = UNKNOWN
AND_OP_MX[UNKNOWN][true] = UNKNOWN
AND_OP_MX[true][true] = true
// or (true is strongest, then UNKNOWN, else false)
OR_OP_MX[true][true] = true
OR_OP_MX[true][false] = true
OR_OP_MX[false][true] = true
OR_OP_MX[true][UNKNOWN] = true
OR_OP_MX[UNKNOWN][true] = true
OR_OP_MX[UNKNOWN][UNKNOWN] = UNKNOWN
OR_OP_MX[UNKNOWN][false] = UNKNOWN
OR_OP_MX[false][UNKNOWN] = UNKNOWN
OR_OP_MX[false][false] = false
// xor (any UNKNOWN is so, without UNKNOWN its just xor)
XOR_OP_MX[true][false] = true
XOR_OP_MX[false][true] = true
XOR_OP_MX[true][true] = false
XOR_OP_MX[false][false] = false
XOR_OP_MX[UNKNOWN][UNKNOWN] = UNKNOWN
XOR_OP_MX[true][UNKNOWN] = UNKNOWN
XOR_OP_MX[UNKNOWN][true] = UNKNOWN
XOR_OP_MX[UNKNOWN][false] = UNKNOWN
XOR_OP_MX[false][UNKNOWN] = UNKNOWN
export const INDETERMINATE = undefined
export function isIndeterminate(val) {
return val === undefined
}
export function isUnknown(val) {
return val == null
}