UNPKG

expexp

Version:

The express model io and express model and data representation.

1,514 lines (1,412 loc) 55 kB
//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