expexp
Version:
The express model io and express model and data representation.
1,514 lines (1,412 loc) • 55 kB
JavaScript
//import stringify from 'json-stable-stringify'
import {VariableScope} from './variableScope.js'
import {Evaluator} from './evaluator.js'
import {SlctEval} from './slcteval.js'
// For selector query parsing
import nearley from 'nearley'
import grammar from './slctgram.js'
import {traverse, isArrSem} from './tree.js'
import jssStringify from 'json-stable-stringify'
// The data store provides for each type and entity of the model the following
// create function: <ref> = inst.<name>(<args>)
// A token looks like this: {t:'entity|type', id:'e|t[0..9]*'}
// Each entity has a lazy evaluated variable scope instance with some or all
// attr, derive and inverse already evaluated.
// The variable scopes of enities contain the values with resolved type tokens,
// where as the attribute value array holds raw (unresolved) data.
// TODO May be the USEDIN impl could also help to invalidate that
// inverse attribute caching within other VariableScope objects determined.
export function Data(model) {
const me = {}
const T_TYPE = 'type'
const T_NTT = 'entity'
const LBL_SEP = ' '
const INDET = undefined
const UNKNOWN = null
const m = model // just a shortcut
const typeById = {} // Type instances by their tok.id .
const typeTokens = []
const nttById = {} // Entity instances by their tok.id .
const nttTokens = []
// Additional caches
const usedInById = {} // Entity tok.id -> {entity type -> {attrId -> [entity token]}}
// TODO values of types are immutable while for entities there should be a way
// how their attributes can be changed after creation. Most probably for that
// purpose there should be kind of a start and end editing operation and
// constraints are only checked when editing ends, for a valid entity can be
// set up in several programming steps that consist of illegal constraint states.
// Get the value of one of the token's entities attribute, derive or inverse.
// The query says which attribute, derive or inverse is ment.
// Types are resolved. Entities are not resolved.
me.getValue = function(tok, query, doNotResolveLeafType=false) {
let res // undefined
if (tok.t == T_TYPE) {
res = getData(tok).value
} else { // Is entity token.
res = getNttValue(tok, query)
if (res.result) {
res = res.value
} else {
throw new ReferenceError(res.message)
}
}
if (doNotResolveLeafType == false) {
return resolveTypes(res)
} else {
return res
}
}
me.setValue = function(tok, query, newValue) {
if (tok.t == T_NTT) {
const res = setNttValue(tok, query, newValue)
if (res.result) {
return res.value // Old value.
} else {
throw new ReferenceError(res.message)
}
} else { // Is type token.
throw new TypeError(`The ${tok} is not an entity.`)
}
}
me.evalFunc = function(funcIc, actParams) {
const res = evalFunc(funcId, resolveTypes(actParams))
if (res.result) {
return res.value
} else {
// TODO do we have to combine messages?
throw new Error(res.message)
}
}
// The actual params must be resolved (type references removed).
me.evalFuncSave = function(funcId, actParams) {
return evalFunc(funcId, actParams)
}
me.info = function(tok) {
return jssStringify(getData(tok), INFO_JSS_OPTS)
}
me.rawData = function(tok) {
return getRawData(tok)
}
me.isToken = function(tok) {
return isToken(tok)
}
me.isType = function(tok) {
return isToken(tok) && tok.t == T_TYPE
}
me.isEntity = function(tok, query, noJsStyle=false) {
const isNtt = isToken(tok) && tok.t == T_NTT
if (isNtt == false || query === undefined) {
return isNtt
} else { // Entity with query.
const toks = filterInstances([tok], query, noJsStyle)
return toks.length == 1
}
}
// Get all type tokens.
me.types = function() {
return typeTokens.slice()
}
// Get a list of instances (entity tokens).
me.instances = function(query, noJsStyle=false) {
if (!query) return nttTokens.slice()
return filterInstances(nttTokens, query, noJsStyle)
}
// For a token get an array of referenced entity tokens.
me.entityRefs = function(tok) {
const refs = {}
if (isToken(tok)) {
const instInfo = getData(tok) // resolved
if (tok.t == T_NTT) {
collectEntityRefs(refs, instInfo.attrValues)
} else { // tok.t == T_TYPE
collectEntityRefs(refs, instInfo.value)
}
} else {
collectEntityRefs(refs, tok)
}
return Object.values(refs)
}
me.isDeletePossible = function(tok) {
const deps = fullUsedIn(tok, undefined, undefined, true)
return deps.length == 0
}
// Throws an exception if there are still dependencies of that ref.
me.delete = function(tok) {
// TODO there is no usedIn for type tokens yet.
if ((isToken(tok) && tok.t == T_NTT) == false) {
throw new TypeError('Only entity instances can be deleted.')
}
const deps = fullUsedIn(tok, undefined, undefined, true)
if (deps.length == 0) {
const idx = nttTokens.indexOf(tok)
nttTokens.splice(idx, 1)
delete nttById[tok.id]
} else {
// We cannot use deps.length as it is not correct.
throw new ReferenceError(`Delete of ${tok.id} is not possible due to existing references in other instances. Use data.usedIn(token) to see which ones.)`)
}
}
me.checkNcreateNtt = function(nttId, args) {
return createOrFailReportNttInst(nttId, args)
}
me.validateNtt = function(tok) {
return validateNtt(tok)
}
me.validateType = function(tok) {
return validateType(tok)
}
// The role name like 'SCHEMA_NAME.ENTITY_NAME.ATTR_NAME'.
me.usedIn = function(tok, roleName) {
if (roleName != '') {
const {nttId, attrId} = m.resolveRoleName(roleName)
if (!nttId) {
// TODO throw error ?
throw new TypeError(`Invalid usedIn role name ${roleName}.`)
}
return fullUsedIn(tok, nttId, attrId)
} else { // Return all.
return fullUsedIn(tok)
}
}
me.typesOf = function(val) {
return typesOf(val)
}
me.leafTypesOf = function(val) {
return leafTypesOf(val)
}
me.rootTypesOf = function(val) {
return rootTypesOf(val)
}
const setup = function() {
// nothing to do
}
// Data helpers
const getRawData = function(tok) {
if (tok.t == T_TYPE) {
return typeById[tok.id]
} else { // must be T_NTT
// The return value is explicitly not an object, with the attribute names
// as keys and their values as values, because then we lose the order of
// the attributes.
return nttById[tok.id]
}
}
const getData = function(tok) {
const raw = getRawData(tok)
return resolveTypes(raw)
}
const resolveTypes = function(v) {
// It is similar to what happens in data.validateNtt. Reuse that
// code here. Not the same: checkValue resolves the tokens to handle
// the actual value(s), but it does not keep them in data.
// Do tokens of entities remain as is. TODO Is this always good?
let rv // undefined
if (Array.isArray(v)) {
rv = v.map(resolveTypes)
} else {
rv = isToken(v) && v.t == T_TYPE?getData(v).value:v
}
return rv
}
// Types are already resolved in v.
const collectEntityRefs = function(refs, v) {
if (Array.isArray(v)) {
v.forEach(function(vv){collectEntityRefs(refs, vv)})
} else if (typeof v == 'object') {
if (isToken(v) && v.t == T_NTT) {
refs[v.id] = v
} else if (isArrSem(v)) {
v.s.forEach(function(vv){collectEntityRefs(refs, vv)})
}
} // else ignore
}
// Token helpers
let nextId = 1000
const isToken = function(value) {
const res =
value !== INDET && value !== UNKNOWN &&
typeof value === 'object' &&
't' in value && (value.t == T_NTT || value.t == T_TYPE) &&
'id' in value && typeof value.id === 'string' && 0 < value.id.length
return res
}
const genTok = function(type) {
const id = nextId++
return {t:type, id:`${type[0]}-${id}`}
}
const genNttTok = function() {
return genTok(T_NTT)
}
const genTypeTok = function() {
return genTok(T_TYPE)
}
const simpleTypeToCheckFn = function(simpleType) {
// This defines the JS representation of all simple types.
switch(simpleType.t) {
case 't_int':
return Number.isInteger
case 't_rea':
case 't_nbr':
return n => (typeof n == 'number' && !isNaN(n) && isFinite(n))
case 't_lox':
// The logical istead of boolean has unknown besides true/false.
// We use null as an unknown boolean, because null in boolean arithmetic
// in JS automatically propagates as desired: null == (null && false)
// Unfortunately false == (false && null), but somehow that is expected.
// In a json representation a null is not pruned (in contrast to NaN or undefined).
return l => (l == UNKNOWN || typeof l == 'boolean' || l == 'TRUE' || l == 'FALSE' || l == 'UNKNOWN')
case 't_bol':
return b => (typeof b == 'boolean' || b == 'TRUE' || b == 'FALSE')
case 't_bin':
// In JS binary is 0b110 - a number or Uint8Array for large binary.
return function(b) {
if (b instanceof Uint8Array) {
// Only need to check the length.
const roundUpLen = b.length * 8
const roundDownLen = 0<roundUpLen?roundUpLen-8:0
// We cannot check it exactly but at least obligingly.
return isValFromTo(simpleType, roundUpLen, roundDownLen)
} else {
return typeof b == 'number' && !isNaN(b) && Number.isSafeInteger(b) && -1 < b
}
}
case 't_str':
return function(s) {
if (typeof s != 'string') {
return false
}
return isValFromTo(simpleType, s.length, s.length)
}
case 'enm':
case 'enm_xtdd':
return e => (!!simpleType.vals.s.find(elt => elt.id == e))
}
}
const idxStr = function(idxPos) {
if (idxPos == 0) {
return '1st'
} else if (idxPos == 1) {
return '2nd'
} else if (idxPos == 2) {
return '3rd'
} else {
return `${idxPos+1}th`
}
}
const mrqStr = function(value) {
if (typeof value != 'string') {
return value
} else {
return '"'+value+'"'
}
}
const toStringRep = function(elt) {
// The elt cannot be an object.
if (Array.isArray(elt)) {
const subStrs = []
for (let subElt of elt) {
subStrs.push(toStringRep(subElt))
}
return subStrs.join(',')
} else if (isToken(elt)) { // even typeof == 'object' would be enough here
if (elt.t == T_TYPE) {
const instInfo = typeById[elt.id]
return toStringRep(instInfo.value)
} else { // elt.t == T_NTT
const instInfo = nttById[elt.id]
return toStringRep(instInfo.attrValues)
}
} else if (typeof elt === 'string') {
return '"'+elt+'"'
} else if (elt === UNKNOWN) {
return 'null'
} else if (elt === INDET) {
return 'undefined'
} else {
return elt.toString()
}
}
const firstUniqueViolation = function(arr) {
const dic = {}
for (let i=0;i<arr.length;i++) {
const elt = arr[i]
const strRep = toStringRep(elt)
if (strRep in dic) {
return {idx0:dic[strRep], idx1:i}
}
dic[strRep] = i
}
}
const aggrTypeToOpt = function(tt) {
if (tt.t == 'arr') {
return tt.opt // The list, set and bag do not have that.
} else {
return false
}
}
const isConstBound = function(bound) {
// {"t":"a_sxp_trms", "s":[{"t":"a_trm_ftrs", "s":[{ "t":"ftr", "arg0":{"t":"int", "val":8}, "qals0":null, "arg1":null, "qals1":null }]}]}
return bound != null &&
bound.t == 'a_sxp_trms' && bound.s.length == 1 &&
bound.s[0].t == 'a_trm_ftrs' && bound.s[0].s.length == 1 &&
bound.s[0].s[0].t == 'ftr' && bound.s[0].s[0].arg1 == null && // ignore qualifiers
bound.s[0].s[0].arg0 && bound.s[0].s[0].arg0.t == 'int'
}
const getConstBoundInt = function(bound) {
return bound.s[0].s[0].arg0.val
}
const isValFromTo = function(simpleType, fromVal, toVal) {
if (simpleType.bds != null && simpleType.bds.t == 'bds') { // exclude not_set
if (isConstBound(simpleType.bds.fr)) {
const defBd = getConstBoundInt(simpleType.bds.fr)
if ( fromVal < defBd ) {
return false
}
}
if (isConstBound(simpleType.bds.to)) {
const defBd = getConstBoundInt(simpleType.bds.to)
if ( defBd < toVal ) {
return false
}
}
}
return true
}
const isValFromToFailing = function(type, labelPath, lowBnd, upBnd=-1) {
// Notice that return false is the opposite of return {result:false, ..} .
if (type.bds != null) return false
if (upBnd == -1) {
upBnd = lowBnd
}
if (type.t != 'arr') {
if (isConstBound(type.bds.fr)) {
const defBd = getConstBoundInt(type.bds.fr)
if (upBnd < defBd) {
return {result:false, message:`The ${labelPath.join(LBL_SEP)}\'s length must be at least ${defBd} but actually is only ${upBnd}.`}
}
} else {
// TODO evaluate none-const-int value
}
if (isConstBound(type.bds.to)) {
const defBd = getConstBoundInt(type.bds.to)
if (defBd < lowBnd) {
return {result:false, message:`The ${labelPath.join(LBL_SEP)}\'s length must not be more than ${defBd} but actually is ${lowBnd}.`}
}
} else {
// TODO evaluate none-const-int value
}
} else {
// TODO Is it allowed to declare an array without upper bound constraint?
if (isConstBound(type.bds.to)) {
let defFromIdx = 0
if (isConstBound(type.bds.fr)) {
defFromIdx = getConstBoundInt(type.bds.fr)
}
const defToIdx = getConstBoundInt(type.bds.to)
const expectedArrayLength = defToIdx - defFromIdx + 1
if (upBnd < expectedArrayLength) {
return {result:false, message:`The ${labelPath.join(LBL_SEP)}\'s length must be at least ${expectedArrayLength} but actually is only ${upBnd}.`}
}
if (expectedArrayLength < lowBnd) {
return {result:false, message:`The ${labelPath.join(LBL_SEP)}\'s length must not be more than ${expectedArrayLength} but actually is ${lowBnd}.`}
}
} else {
// TODO evaluate none-const-int value
// else - The array is as long as the data used to initialise.
}
}
return false
}
// Returns on success the standardised/unified value:
// {result:true, value:..}
// and on failure a message describing why:
// {result:false, message:'...'}
const checkValue = function(value, type, opt, omitted, labelPath, forbidSelect = false) {
// The value of an omitted attribute needs not be checked. It is always set null.
if (omitted) {
return {result: true, value: INDET}
}
///////////////////////////////////////////////
// 1. From type derive what is expected as value.
let possibleTypes // undefined
let chkFn // undefined
let isAggr = false
let isSelect = false
let refuseSelect = false
switch (type.t) {
case 'any_ref':
case 'typ_ref':
case 'ntt_ref': // The model says value must be a token.
// This is only the case when one type is of another type or an
// aggregation element must be of a certain type.
// According to the model a type_ref can be an entity or a type.
// This has nothing to do with entity inheritence.
// But first we have to check for forbidden selects.
if (m.hasType(id2name(type.id))) {
const chnIds = m.typeIdChainById(id2name(type.id))
const rootTypeId = chnIds[0] // select the root
const rootType = m.typeById(id2name(rootTypeId))
if (rootType.spc.t == 'slc' || rootType.spc.t == 'slc_xtdd') {
isSelect = true
refuseSelect = forbidSelect
if (forbidSelect) {
break
}
}
}
possibleTypes = m.possibleTypesOfRef(type) // Get all select (recursively) or inherited ones.
break
case 'lst':
case 'set':
case 'arr':
case 'bag':
isAggr = true
break
case 't_int':
case 't_rea':
case 't_nbr':
case 't_bol':
case 't_lox':
case 't_bin':
case 'enm':
case 'enm_xtdd':
case 't_str':
chkFn = simpleTypeToCheckFn(type)
break
case 'slc':
case 'slc_xtdd':
isSelect = true
refuseSelect = forbidSelect
// if (forbidSelect == false) {
// possibleTypes = m.possibleTypesOfSelect(type.id)
// }
break
default:
return {result:false, message:`No valid type "${type.t}" for ${labelPath.join(LBL_SEP)}.`}
}
if (refuseSelect) {
return {result:false, message:`The ${type.id} instance cannot be created, since select types cannot be instanciated.`}
}
//////////////////////////////////////////////
// 2. Check if value matches the expectations.
// 2.a Get the actual value by resolving tokens.
let actualValue // undefined
let chnIds // undefined
if (isToken(value)) {
const tok = value
if (tok.t == T_TYPE) {
// Check if an id like 'TpLabel' or any of the types it derives from
// is within possibleTypes.
const instInfo = typeById[tok.id]
chnIds = m.typeIdChainById(id2name(instInfo.typeId))
actualValue = instInfo.value
} else { // tok.t == T_NTT
// Check if an id like 'NttSoftware' or any of the entities it
// derives from is within possibleTypes.
const instInfo = nttById[tok.id]
chnIds = m.entityIdChainById(id2name(instInfo.nttId))
// For an entity is the actual value the token, not instInfo.attrValues.
actualValue = tok
// Later in possibleTypes && chnIds, there is the exact type checking.
}
} else {
actualValue = value
}
// From here
// - The actualValue is always set.
// - The chnIds might be set if value is a token (for type also if value is resolved).
// 2.b Compare the actual value to what it should be.
// Remember: This interface is just towards JS and not specific to any
// external fileformat.
if (possibleTypes) {
// A type reference (or select) to either a type or an entity is expected.
// Th possibleTypes like: {'NttPerson':'entity', 'NttOrg':'entity', 'TpLabel':'type', ..}
// TODO How does it look like and is it possible, that a select consists of simple types?
if (chnIds) {
// For types and entities (value was a token)
// If already the type does not fit, it is needless to compare the
// actual value and we can also exclude just by type. Or even better:
// The typing grants that the values will just be fine without checking.
// Most probably the order of checking does not matter, since if it
// matches, anyway just value is returned.
for (let pId of chnIds) {
if (pId in possibleTypes) {
return {result:true, value:value} // Return no info on how or why it matches.
}
}
const possibleStr = Object.keys(possibleTypes).join(', ')
const actualStr = chnIds.join(', ')
if (isSelect) {
// This message is similar to the one for simple values.
return {result:false, message:`The ${labelPath.join(LBL_SEP)}\'s actually possible types ${mrqStr(actualStr)} do not fit anything in the selection of ${possibleStr}.`}
} else {
return {result:false, message:`The ${labelPath.join(LBL_SEP)} accepts the following types ${possibleStr}, but the actual value is only of ${actualStr}.`}
}
} else {
// Use actualValue (which cannot be a token here - otherwise there would have been a chnIds).
// E.g. it should be possible to pass a string for TpCompass.
// A possible type in the list can also be an aggregation.
let lastChk // undefined
for (let pId in possibleTypes) {
if (possibleTypes[pId] == 'type') {
// Since simple types as well as the definitions of an aggregation
// reside in the very basis, we especially just look for the root.
const chnIds = m.typeIdChainById(id2name(pId))
const rootTypeId = chnIds[0] // select the root
const rootType = m.typeById(id2name(rootTypeId))
const subPath = labelPath.slice()
subPath.push(pId) // TODO correct like that?
const chk = checkValue(actualValue, rootType.spc, opt, omitted, subPath, forbidSelect)
lastChk = chk
if (chk.result) {
// Most probably it makes no sense to create a token of type pId with actualValue.
return chk
}
}
}
if (isSelect) {
if (opt) {
return {result: true, value: INDET}
} else {
const possibleStr = Object.keys(possibleTypes).join(', ')
// This message is similar to the one with a token passed.
return {result:false, message:`The ${labelPath.join(LBL_SEP)}\'s actual value ${mrqStr(actualValue)} does not fit anything in the selection of ${possibleStr}.`}
}
} else if (lastChk) {
// Return the negative result of the only check.
return lastChk
} else {
if (opt) {
return {result: true, value: INDET}
} else {
return {result:false, message:`The ${labelPath.join(LBL_SEP)} must be a token of an entity but the actual value ${mrqStr(actualValue)} does not fit.`}
}
}
}
} else if (isAggr) {
// If we have a chnIds here, this can only happen if a token is passed
// as value for an in-place aggregation declaration (not a named type
// reference of an aggregation type).
// In that case the type cannot be veryfied by comparing chnIds to
// possibleTypes (that we just do not have), and we rely on actualValue instead.
if (Array.isArray(actualValue)) {
const arrValue = actualValue
// TODO isValFromToFailing for type.t == 'array' case different
// Check all concerning just the array (not its values) here
// The arrValue's length must be within type.bounds even if
// value was a type ref pointing to sth. with more flexible bounds.
const valFail = isValFromToFailing(type, labelPath, arrValue.length)
if (valFail) {
return valFail
}
// Check empty array indices
if ((type.t == 'arr' && type.opt) == false) {
for (let i=0;i<arrValue.length;i++) {
const aVal = arrValue[i]
if (aVal === INDET) {
return {result:false, message:`The ${labelPath.join(LBL_SEP)} has an empty value at the ${idxStr(i)} position.`}
}
}
}
// Check uniqueness
if (type.t == 'set' || type.unq) {
const unqVio = firstUniqueViolation(arrValue)
if (unqVio) {
return {result:false, message:`The ${labelPath.join(LBL_SEP)} has at least a uniqueness violation at the ${idxStr(unqVio.idx0)} and ${idxStr(unqVio.idx1)} position.`}
}
}
// And check each value of the array (arrValue)
const usedValues = []
for (let i=0;i<arrValue.length;i++) {
const aVal = arrValue[i]
// This checkValue actually may lead to a potential recursion.
const subPath = labelPath.slice()
subPath.push(idxStr(i)) // TODO correct like that?
const chkRes = checkValue(aVal, type.spc, aggrTypeToOpt(type), false, subPath)
if (chkRes.result) {
// Use the standardised/unified value instead of the original one.
usedValues.push(chkRes.value)
} else {
// This is reached, when there is a wrong entity type within an aggregation.
return chkRes // We cannot chain that.
}
}
return {result:true, value:usedValues}
} else if (actualValue === UNKNOWN || actualValue === INDET) {
if (opt) {
return {result:true, value:value}
} else {
}
} else {
return {result:false, message:`The ${labelPath.join(LBL_SEP)} must be an aggregation (or token of an aggregation type) but the actual value ${mrqStr(actualValue)} does not fit.`}
}
} else if (chkFn) {
// We need a simple type here.
// For simple Types the one argument must fit the type.
// E.g. LOGICAL <- true/false/null
// What is passed for an ENUM, for an aggregation type or for a SELECT?
// - For an ENUM a string is passed, containing the one of the enums
// concrete value. That must be checked.
// - For all aggregation types an array is passed.
// Length constraints must be checked and the type for each entry.
// An array of type/entity contains just refs (created before hand).
// - For a SELECT a ref is passed. It is resolved and its type checked.
if (actualValue !== UNKNOWN && actualValue !== INDET) {
const chkRes = chkFn(actualValue)
if (chkRes) {
switch (type.t) {
case 't_bol':
if (actualValue == 'TRUE') {
return {result:true, value:true}
} else if (actualValue == 'FALSE') {
return {result:true, value:false}
} else {
return {result:true, value:value} // not actualValue
}
break
case 't_lox':
if (actualValue == 'TRUE') {
return {result:true, value:true}
} else if (actualValue == 'FALSE') {
return {result:true, value:false}
} else if (actualValue == 'UNKNOWN') {
return {result:true, value:UNKNOWN}
} else {
return {result:true, value:value} // not actualValue
}
break
case 't_bin':
if (typeof actualValue == 'number') { // no need to repeat the whole check
// actualValue is granted to be a positive save integer in JS
// The number is round up to the next number of bytes and
// then converted into an Uint8Array.
let n = actualValue
let bArr = []
const N_2_E_32 = 4294967296 // 2 ^ 32
while (0 < n) {
const b = n & 0b1111111
bArr.unshift(b)
if (n < N_2_E_32) { // 2 ^ 32
n = n >> 8
} else { // for the ones up to arround 2 ^ 52 (save integers)
n = Math.floor(n / 0b11111111)
}
}
return {result:true, value:new Uint8Array(bArr)}
} else { // plain Uint8Array or reference to Uint8Array
return {result:true, value:value} // not actualValue
}
break
default:
return {result:true, value:value} // not actualValue
}
} else {
return {result:false, message:`The ${labelPath.join(LBL_SEP)} must be a ${type.t}, but the actual value ${mrqStr(value)} does not fit.`}
}
} else if (actualValue !== INDET ) {
if (opt || type.t == 't_lox') {
return {result:true, value:UNKNOWN}
} else {
return {result:false, message:`The ${labelPath.join(LBL_SEP)} is mandatory but an actual value is missing.`}
}
} else {
if (opt) {
return {result:true, value:INDET}
} else {
return {result:false, message:`The ${labelPath.join(LBL_SEP)} is mandatory but an actual value is missing.`}
}
}
}
}
const createTypeInst = function(typeId, args) {
const type = m.typeById(id2name(typeId))
// The value of a type is not optional.
if (args.length != 1) {
throw new TypeError(`Creating instance of type ${typeId} failed, because the number of arguments for its value is ${args.length} instead of exactly one.`)
}
const arg = args[0]
const labelPath = [typeId, 'value']
const chkRes = checkValue(arg, type.spc, false, false, labelPath, true)
if (chkRes.result == false) {
throw new TypeError(`For ${type.id} with ${arg} the error is: ${chkRes.message}`)
}
// Use the standardised/unified value instead of the original one.
const val = chkRes.value
const instInfo = {typeId:typeId, value:val}
const tok = genTypeTok()
typeById[tok.id] = instInfo
typeTokens.push(tok)
return tok
}
const createNttInst = function(nttId, args) {
const res = createOrFailReportNttInst(nttId, args)
if (res.result) {
return res.value
} else {
if (res.messages.length == 1) {
throw new TypeError(res.messages[0])
} else { // Must be 1 < res.messages.length
const allMessages = res.messages.join(' ')
throw new TypeError(`Several problems: ${allMessages}`)
}
}
}
const createOrFailReportNttInst = function(nttId, args) {
const ntt = m.entityById(id2name(nttId))
if (ntt.abs) {
return {result: false, messages:[`Creating instance of entity ${nttId} failed, since abstract entities cannot be instanciated.`]}
}
const attrs = m.fullAttrListById(id2name(nttId))
let minNofArgs = 0
for (let i=attrs.length-1;-1<i;i--) {
if (attrs[i].opt == false && attrs[i].omit == false) {
minNofArgs = i+1
break
}
}
if (args.length < minNofArgs) {
return {result: false, messages:[`Creating instance of entity ${nttId} failed due to too few arguments. Only ${args.length} of ${minNofArgs} provided.`]}
}
const attrVals = []
const errorMessages = []
for (let i=0;i<attrs.length;i++) {
const attr = attrs[i]
if (i<args.length) {
const arg = args[i]
const labelPath = [nttId, idxStr(i), 'attribute', 'value']
const chkRes = checkValue(arg, attr.type, attr.opt, attr.omit, labelPath)
if (chkRes.result) {
// Use the standardised/unified value instead of the original one.
attrVals.push(chkRes.value)
} else {
errorMessages.push(chkRes.message)
}
} else {
attrVals.push(INDET)
}
}
if (0 < errorMessages.length) {
return {result: false, messages:errorMessages}
}
// Do not store redundant model info in the instance info, just the entity id.
const tok = genNttTok()
const instInfo = {nttId:nttId, attrValues:attrVals}
nttById[tok.id] = instInfo
nttTokens.push(tok)
// Register all entity references within the attributes of this new instance.
for (let i=0;i<attrs.length;i++) {
const attr = attrs[i]
if (i<args.length) {
const arg = args[i]
traverseToRegUsedIn(arg, tok, attr.id)
}
}
return {result:true, value:tok}
}
const combineWhereResult = function(res, eres, prefix) {
if (eres.result) {
// eres.value can be true, false or INDETERMINATE for where
if (res.value !== INDET) {
if (eres.value !== INDET) {
res.value = res.value && eres.value
} else {
res.value = eres.value // INDETERMINATE
}
}
} else {
res.result = false
if (!res.messages) {
res.messages = []
}
res.messages.push.apply(res.messages, eres.messages.map(function(m){
return prefix + m
}))
}
}
// The where rules of the type are evaluated.
const validateType = function(tok) {
const instInfo = typeById[tok.id]
const type = m.typeById(id2name(instInfo.typeId))
const scp = VariableScope()
let res = {result:true, value:true}
for (const elt of type.wheres) {
const ev = Evaluator(m, me, scp)
ev.selfValue = getData(tok).value
const eres = ev.evalExpr(elt.expr)
combineWhereResult(res, eres, `For ${instInfo.typeId} type ${tok.id} the where condition ${elt.id} has issue: `)
console.log(elt.id, '=', res)
}
return res
}
// Unique is validated before where constraints.
const validateNtt = function(tok) {
// TODO Validate these:
// [ ] Expressions within aggregation bounds of attribute and derive declarations
// [ ] Remaining attribute values (not yet checked on creation) based on the previous dynamicaly calculated aggregation bounds.
// [ ] Derived attribute values
// [ ] Unique constraints
// [x] Where constraints
const instInfo = nttById[tok.id]
const evalOrder = m.nttEvalOrder(id2name(instInfo.nttId))
if (evalOrder.result) {
// evalOrder.elements.forEach(function(elt){console.log(elt)})
const eltModels = m.fullAttrListById(id2name(instInfo.nttId)) // These are per id.
// The eltModels has the same indexing as instInfo.attrValues .
const idsSorted = eltModels.map(elt => elt.id)
if (instInfo.scope === undefined) {
instInfo.scope = VariableScope()
}
const scp = instInfo.scope
const res = {result:true, value:true}
const addittionalMessagesIfFail = []
for (const elt of evalOrder.elements) {
// console.log('elt', elt)
// TODO Handle the bounds?
// TODO Handle the derive values.
if (elt.t == 'attr') {
elt.ids.forEach(function(ie){
const attrId = ie.id
if (scp.has(attrId) == false) {
const i = idsSorted.indexOf(attrId)
const am = eltModels[i]
const v = i<instInfo.attrValues.length?instInfo.attrValues[i]:INDET
// In our case v ist often a type token that must be resolved to a value before being usable as value.
let rv = resolveTypes(v) // undefined
// console.log(i, am.type, v, rv)
scp.regVal(am.id, rv)
}
})
} else if (elt.t == 'derive') {
const attrId = elt.id.id
if (scp.has(attrId) == false) {
const ev = Evaluator(m, me, scp)
ev.selfValue = tok
const eres = ev.evalExpr(elt.expr) // types are already resolved
if (eres.result) {
scp.regVal(attrId, eres.value)
} else {
addittionalMessagesIfFail.push(eres.message)
scp.regVal(attrId, INDET)
}
}
} else if (elt.t == 'where') {
const ev = Evaluator(m, me, scp)
ev.selfValue = tok // unresolved (lazzy)
const eres = ev.evalExpr(elt.expr)
combineWhereResult(res, eres, `For ${instInfo.nttId} entity ${tok.id} the where condition ${elt.id} has issue: `)
if (0 < addittionalMessagesIfFail.length) {
if (res.result == false) {
res.messages.push.apply(res.messages, addittionalMessagesIfFail)
}
}
console.log(elt.id, '=', res)
} else if (elt.t == 'inverse') {
const attrId = elt.id.id
if (scp.has(attrId) == false) {
const toks = fullUsedIn(tok, elt.nttRef.id, elt.invRef.id)
scp.regVal(attrId, toks)
}
}
}
return res
} else {
return {result:false, messages:[`For ${instInfo.nttId} entity ${tok.id} when getting the evaluation order there is the issue: ` + evalOrder.message]}
}
}
// The parentScope is only passed, if this function is a sub-function of
// a procedure or function. Entity scopes are not passed here.
// The actual parameters must be resolved (type references removed).
const evalFunc = function(funcId, actParams, parentScope) {
const evalOrder = m.funcEvalOrder(id2name(funcId))
if (evalOrder.result) {
const scp = VariableScope(parentScope)
// evalOrder.elements.forEach(function(elt){console.log(elt)})
const eltModels = m.funcParamListById(id2name(funcId)) // These are per id.
// The eltModels has the same indexing as actParams.
const idsSorted = eltModels.map(elt => elt.id)
const res = {result:true, value:INDET}
for (const elt of evalOrder.elements) {
// console.log('elt', elt)
// TODO Handle the bounds?
if (elt.t == 'param') {
elt.ids.forEach(function(ie){
const eltId = ie.id
const i = idsSorted.indexOf(eltId)
const am = eltModels[i]
const v = i<actParams.length?actParams[i]:INDET
// The v cannot be a type token, because the actual parameters are resolved.
// console.log(i, am.type, v, rv)
scp.regVal(eltId, v) // For proc this is registered by ref if VAR.
})
} else if (elt.t == 'local_var') {
// Eval once, but set several times (for each id).
const ev = Evaluator(m, me, scp)
// There is no ev.selfValue set.
const eres = ev.evalExpr(elt.expr)
elt.ids.forEach(function(ie){
const eltId = ie.id
if (eres.result) {
scp.regVal(eltId, eres.value)
// TODO Remove this hack:
// As a hack we return always the last calculated local value.
res.value = eres.value // TODO Remove later
} else {
// TODO add to any messages or alike?
scp.regVal(eltId, INDET)
}
})
}
}
return res
} else {
return {result:false, messages:[`For function ${funcId} when gettin the evaluation order there is the issue: ` + evalOrder.message]}
}
}
let slctParser // undefined
let slctParserState // undefined
let jsReplacements // undefined
const grantSlctParser = function() {
if (slctParser == undefined) {
jsReplacements = [
['&&',' AND '],
['\\|\\|',' OR '],
['\\^',' XOR '],
['==',' = '],
['!=','<>'],
['~~','*='],
// ['{','{{'],
// ['}','}}'],
// ['\\[!','{'],
// ['!\\]','}'],
['!',' NOT '],
['%',' MOD ']
].map(frTo => ([new RegExp(frTo[0],'g'), frTo[1]]))
slctParser = new nearley.Parser(nearley.Grammar.fromCompiled(grammar))
slctParserState = slctParser.save()
}
slctParser.restore(slctParserState)
}
const filterInstances = function(nttToks, query, noJsStyle=false) {
if (query.includes('{')) {
grantSlctParser()
if (noJsStyle == false) {
jsReplacements.forEach(frTo => {
query = query.replace(frTo[0], frTo[1])
})
}
slctParser.feed(query)
// TODO check results.length == 0 or <1
const json = slctParser.results[0]
traverse(json, function(json) { delete json.p })
const ev = SlctEval(m, nttById, getRawData, getData, getNttValue, resOneStage)
throw new Error('Selection query with scope is not yet supported.')
// console.log(ev.eval(nttToks[0], json))
// return [nttToks[0]]
// return nttToks.filter(tok => {
// return ev.eval(tok, json)
// })
}
// TODO Replace this simple impl by the full query expression eval above.
const regStr = function(cmp, str) {
if (str.startsWith('"') && str.endsWith('"')) {
str = str.substring(1, str.length-1)
} else if (str.startsWith('/') && str.endsWith('/')) {
// Also later cmp is ignored
str = str.substring(1, str.length-1)
return new RegExp(str)
}
return str
}
const valCmp = function(iss, cmp, shd) {
if (iss == undefined) return false
if (isToken(iss) && iss.t == T_TYPE) {
iss = getData(iss).value
}
if (shd instanceof RegExp) {
if (cmp != '!=') {
// Ignore all other cmp.
return shd.test(iss)
} else {
return shd.test(iss) == false
}
}
switch (cmp) {
case '==':
return iss == shd
case '!=':
return iss != shd
case '~~':
return iss.includes(shd)
case '<=':
return iss <= shd
case '<':
return iss < shd
case '>=':
return iss >= shd
case '>':
return iss > shd
case undefined:
return !!iss
default:
throw new Error('Illegal compare operator ' + cmp)
}
}
const nttCmps = []
const nttStrs = []
const chn2cmp = {}
const chn2val = {}
query.split('&&').forEach(term => {
term = term.trim()
if (term[0] != '(' || term[term.length-1] != ')')
throw new Error(`Condition "${term}" must be surrounded by parenthese.`)
term = term.substring(1, term.length-1).trim()
if (term[0] == 'e' && '=!<>~'.includes(term[1])) {
const eqlIdx = term.lastIndexOf('=')
let nttName // undefined
let nttCmp // undefined
if (0 < eqlIdx) {
nttCmp = term.substring(1, eqlIdx+1).trim()
nttName = term.substring(eqlIdx+1).trim() // 3
} else { // simply < or > or ~~
const smlIdx = term.lastIndexOf('~')
if (0 < smlIdx) {
nttCmp = term.substring(1, smlIdx+1).trim()
nttName = term.substring(smlIdx+1).trim() // 3
} else { // < or >
nttCmp = term.substring(1, 2).trim()
nttName = term.substring(2).trim()
}
}
nttCmps.push(nttCmp)
nttStrs.push(regStr(nttCmp, nttName))
} else { // attribute chain
const eqlIdx = term.lastIndexOf('=')
let chn // undefined
let cmp // undefined
let val // undefined
if (0 < eqlIdx) {
chn = term.substring(0, eqlIdx-1).trim()
cmp = term.substring(eqlIdx-1, eqlIdx+1).trim()
val = term.substring(eqlIdx+1).trim()
} else { // < or > or ~~
let lgIdx = term.indexOf('<')
if (lgIdx == -1) lgIdx = term.indexOf('>')
if (-1 < lgIdx) {
chn = term.substring(0, lgIdx).trim()
cmp = term.substring(lgIdx, lgIdx+1).trim()
val = term.substring(lgIdx+1).trim()
} else {
const smlIdx = term.lastIndexOf('~')
if (0 < smlIdx) {
chn = term.substring(0, smlIdx-1).trim()
cmp = term.substring(smlIdx-1, smlIdx+1).trim()
val = term.substring(smlIdx+1).trim()
} else {
chn = term
}
}
}
chn2cmp[chn] = cmp
chn2val[chn] = regStr(cmp, val)
}
})
return nttToks.filter(tok => {
let d = getRawData(tok)
// First compare the entity type (that is more excluding)
const nttOk = nttCmps.find((cmp, i) => {
const str = nttStrs[i]
const isOk = valCmp(d.nttId, cmp, str)
if (isOk) return true
// Check whole entity inheritence (more expensive)
const ihts = m.entityIdChainById(id2name(d.nttId))
if (0 < ihts.length) {
ihts.pop()
ihts.reverse()
const ihtOk = ihts.find(nttId => {
return valCmp(nttId, cmp, str)
})
return !!ihtOk
} else {
return false
}
})
if (0 < nttCmps.length && !nttOk) return false
// This time getNttValue will use getData with the resolved data.
for (const [chn, cmp] of Object.entries(chn2cmp)) {
const shd = chn2val[chn]
const isRes = getNttValue(tok, chn)
if (isRes.result == false) continue // ignore this chain
const iss = isRes.value // iss can also be a token
if (valCmp(iss, cmp, shd) == false) return false
}
return true
})
}
// The new value is set and the old one returned.
const setOneStage = function(tok, name, newVal) {
const instInfo = nttById[tok.id]
const explAttrDefs = m.fullAttrListById(id2name(instInfo.nttId))
let attrIdx // undefined
const attrDef = explAttrDefs.find((a, i) => {
const res = a.id == name
if (res) attrIdx = i
return res
})
if (attrDef) { // It is an explicit attribute.
const d = getData(tok) // type values are resolved
const oldVal = d.attrValues[attrIdx]
d.attrValues[attrIdx] = newVal
if (isToken(newVal) && newVal.t == T_NTT) {
registerUsedIn(newVal, tok, name)
}
if (isToken(oldVal) && oldVal.t == T_NTT) {
unregisterUsedIn(oldVal, tok, name)
}
if (Array.isArray(oldVal)) {
return oldVal
} else {
return [oldVal]
}
} else {
return `There is no explicit attribute ${instInfo.nttId}.${name} to be set.`
}
}
const resOneStage = function(tok, name) {
if (Array.isArray(tok)) {
tok = tok[0]
}
const instInfo = nttById[tok.id]
const explAttrDefs = m.fullAttrListById(id2name(instInfo.nttId))
let attrIdx // undefined
const attrDef = explAttrDefs.find((a, i) => {
const res = a.id == name
if (res) attrIdx = i
return res
})
if (attrDef) { // It is an explicit attribute.
let d = getData(tok) // type values are resolved
const val = d.attrValues[attrIdx]
return {result:true, value:val}
} else {
const evalOrder = m.nttEvalOrder(id2name(instInfo.nttId))
if (evalOrder.result) {
const elt = evalOrder.elements.find(
e => e.t == 'inv' && e.id.id == name
)
if (elt) {
const toks = fullUsedIn(tok, elt.nttRef.id, elt.invRef.id)
return {result:true, value:toks}
}
}
return {result:false, message:`Cannot find ${instInfo.nttId}.${name} for ${tok.id}`}
}
}
const setNttValue = function(tok, query, newVal) {
const names = query.split('.')
let val = [tok]
for (let i=0;i<names.length-1;i++) {
const name = names[i]
const nextVal = []
for (const v of val) {
const stg = resOneStage(v, name)
if (stg.result) {
if (0 < i && Array.isArray(stg.value)) {
nextVal.push.apply(nextVal, stg.value)
} else {
nextVal.push(stg.value)
}
} else {
return {result:false, message:`Cannot resolve ${v} on level ${i} with: ${stg.message}`}
}
}
val = nextVal
}
const name = names[names.length-1]
const v = val[0]
const stg = setOneStage(v, name, newVal)
if (Array.isArray(stg)) {
// Return old value.
return {result:true, value:stg[0]}
} else {
return {result:false, message:`Cannot resolve ${v} on level ${i} to set new value with: ${stg}`}
}
}
// Returns the cached value from the entities scope, if available.
// Fills entities cache scope with all intermediate values needed to evaluate and
// store (e.g. dynamic aggregation bounds) the value asked for by name.
const getNttValue = function(tok, query) {
const names = query.split('.')
let val = [tok]
for (let i=0;i<names.length;i++) {
const name = names[i]
const nextVal = []
for (const v of val) {
const stg = resOneStage(v, name)
if (stg.result) {
if (0 < i && Array.isArray(stg.value)) {
nextVal.push.apply(nextVal, stg.value)
} else {
nextVal.push(stg.value)
}
} else {
return {result:false, message:`Cannot resolve ${v} on level ${i} with: ${stg.message}`}
}
}
val = nextVal
}
return {result:true, value:val[0]}
//////////////////////////////////////
// const name = query
// const val = resOneStage(tok, name)
// if (Array.isArray(val)) {
// return {result:true, value:val[0]}
// } else {
// return {result:false, message:val}
// }
////////////////////////////////////////
// const instInfo = nttById[tok.id]
// if (instInfo.scope && instInfo.scope.has(name)) {
// // That was easy.
// return {result:true, value:instInfo.scope.get(name)}
// }
//
// // TODO this code is similar to validateNtt ..
//
// const evalOrder = m.nttEvalOrder(instInfo.nttId)
// if (evalOrder.result) {
// // In evalOrder.elements an elt.t=='attr' has an elt.ids array but others elt.id .
// // evalOrder.elements.forEach(function(elt){console.log(elt)})
//
// // Determine the target attribute (asked for by name) and its dependencies.
// let attr // undefined - .t in derive, inverse or attr
// let deps // undefined
// // We presume evalOrder.result == true
// for (let i=0;i<evalOrder.elements.length;i++) {
// const elt = evalOrder.elements[i]
// const eDeps = evalOrder.dependencies[i]
// // console.log('elt', elt)
// // Both derive and inverse have one single id per element (unlike 'attr').
// if ((elt.t == 'derive' || elt.t == 'inverse') && elt.id.id == name) {
// attr = elt
// deps = eDeps
// break
// } else if( elt.t == 'attr') { // Use elt.ids .
// const found = elt.ids.find(ie=>ie.id==name)
// if (found) {
// attr = elt // not variable 'found'
// deps = eDeps
// break
// }
// }
// }
//
// if (attr) { // Then deps is also available.
// const dPath = deps.concat([attr])
// // Prepare the attribute models (for elt.t=='attr' only).
// const attrModels = m.fullAttrListById(instInfo.nttId) // These are per id.
// // The attrModels has the same indexing as instInfo.attrValues .
// const attrIdsSorted = attrModels.map(elt => elt.id)
// // Use variable for scope (So we even could impl turn off caching.).
// if (instInfo.scope === undefined) {
// instInfo.scope = VariableScope()
// }
// const scp = instInfo.scope
// for (let a=0;a<dPath.length;a++) {
// const elt = dPath[a]
// if (elt.t == 'attr') {
// elt.ids.forEach(function(ie){
// const attrId = ie.id
// if (scp.has(attrId) == false) {
// const i = attrIdsSorted.indexOf(attrId)
// const v = i<instInfo.attrValues.length?instInfo.attrValues[i]:INDET
// // In our case v ist often a type token that must be resolved to a value before being usable as value.
// let rv = resolveTypes(v) // undefined
// // const am = attrModels[i]
// // console.log(i, am.type, v, rv)
// scp.regVal(attrId, rv) // once attrId was am.id here
// }
// })
// } else if (elt.t == 'derive') {
// const attrId = elt.id.id
// if (scp.has(attrId) == false) {
// const ev = Evaluator(m, me, scp)
// ev.selfValue = tok
// const eres = ev.evalExpr(elt.expr) // types are already resolved
// if (eres.result) {
// scp.regVal(attrId, eres.value)
// } else {
// // TODO scp.regVal(attrId, INDET) // Avoid error again by caching INDETERMINATE
// return eres // return single error immediately
// }
// }
// } else if (elt.t == 'inverse') {
// const attrId = elt.id.id
// if (scp.has(attrId) == false) {
// const toks = fullUsedIn(tok, elt.nttRef.id, elt.invRef.id)
// scp.regVal(attrId, toks)
// }
// } // no default (ignore where)
// }
// return {result:true, value:scp.get(name)}
// } else {
// return {result:false, message:`There is no attribute ${name} in ${instInfo.nttId}.`}
// }
// } else {
// return {result:false, messages:[`For ${instInfo.nttId} entity ${tok.id} when getting the evaluation order there is the issue: ` + evalOrder.message]}
// }
}
const traverseToRegUsedIn = function(v, usedInNttTok, usedInAttrId) {
// It is similar to what happens in data.validateNtt.
if (Array.isArray(v)) {
v.map(function(x){traverseToRegUsedIn(x, usedInNttTok, usedInAttrId)})
} else {
if (isToken(v) && v.t == T_NTT) {
registerUsedIn(v, usedInNttTok, usedInAttrId)
}
}
}
// Registers a token of an entity as being used within anoter entity referenced
// as token and also saves the attribute where it is used.
const registerUsedIn = function(tok, usedInNttTok, attrId) {
// usedInById = {Entity tok.id -> {entity type -> {attrId -> [entity token]}} }
const instId = usedInNttTok.id // usedInNttTok.t == T_NTT
const instInfo = nttById[instId]
const nttId = instInfo.nttId
if ((tok.id in usedInById) == false) {
usedInById[tok.id] = {}
}
const nttId2attrs = usedInById[tok.id]
if ((nttId in nttId2attrs) == false) {
nttId2attrs[nttId] = {}
}
const attrId2Toks = nttId2attrs[nttId]
if ((attrId in attrId2Toks) == false) {
attrId2Toks[attrId] = []
}
const toks = attrId2Toks[attrId]
if (toks.includes(usedInNttTok) == false) {
toks.push(usedInNttTok)
}
}
// Unregisters a token of an entity as being used within anoter entity referenced
// as token and also removes the info about the attribute where it is used.
const unregisterUsedIn = function(tok, usedInNttTok, attrId) {
// usedInById = {Entity tok.id -> {entity type -> {attrId -> [entity token]}} }
const instId = usedInNttTok.id // usedInNttTok.t == T_NTT
const instInfo = nttById[instId]
const nttId = instInfo.nttId
// Locate it.
const nttId2attrs = usedInById[tok.id]
const attrId2Toks = nttId2attrs[nttId]
const toks = attrId2Toks[attrId]
// Unregister and clean up empty containers.
const nttIdx = toks.indexOf(usedInNttTok)
toks.splice(nttIdx, 1)
if (toks.length == 0) {
delete attrId2Toks[attrId]
}
if (Object.keys(attrId2Toks).length == 0) {
delete usedInById[tok.id]
}
}
// Full featured internal