expexp
Version:
The express model io and express model and data representation.
679 lines (631 loc) • 22.8 kB
JavaScript
import {traverse} from './tree.js'
export function Model(rawJson, trust = false) {
const me = {}
// Operations on the model would go here.
const name2Id = {}
const tpId2Tp = {}
const nttId2Ntt = {}
const funcId2Func = {}
const procId2Proc = {}
const rlId2Rl = {}
me.schemaId = function() {
return rawJson.id
}
me.ALL_LEVELS = -999999
me.entities = function(lvl=me.ALL_LEVELS, noAbs = false) {
if (lvl == me.ALL_LEVELS && noAbs == false) {
return Object.values(nttId2Ntt)
}
// TODO for 0 get all root (in hierarchy), for 1 only childs of first level, etc.
// TODO for -1 get all children without descendents, for -2 the ones that have no grandchildren, etc.
}
me.types = function(lvl=me.ALL_LEVELS) {
if (lvl == me.ALL_LEVELS) {
return Object.values(tpId2Tp)
}
// if (lvl == me.ALL_LEVELS) {
// // TODO return array of all
// }
// TODO for 0 get all root (in hierarchy), for 1 only childs of first level, etc.
// TODO for -1 get all children without descendents, for -2 the ones that have no grandchildren, etc.
}
me.hasType = function(name) {
const id = name2Id[name]
return id in tpId2Tp
}
me.hasEntity = function(name) {
const id = name2Id[name]
return id in nttId2Ntt
}
me.hasFunction = function(name) {
const id = name2Id[name]
return id in funcId2Func
}
me.typeById = function(name) {
const id = name2Id[name]
return tpId2Tp[id]
}
me.entityById = function(name) {
const id = name2Id[name]
return nttId2Ntt[id]
}
me.functionById = function(name) {
const id = name2Id[name]
return funcId2Func[id]
}
me.procById = function(name) {
const id = name2Id[name]
return procId2Proc[id]
}
me.ruleById = function(name) {
const id = name2Id[name]
return rlId2Rl[id]
}
me.typeIdChainById = function(name) {
const id = name2Id[name]
return typeSpecChainById(id).map(tp => tp.id)
}
me.entityIdChainById = function(name) {
const id = name2Id[name]
return nttSuperChainById(id).map(ntt => ntt.id)
}
// It returns:
// [ {ctxId:'', id:'', type:{t:'',..}, opt:true/false}, .. ]
// By the way: At least one of the returned params has opt:true .
me.procParamListById = function(name) {
const id = name2Id[name]
// TODO
}
// It returns:
// [ {ctxId:'', id:'', type:{t:'',..}, var:true/false}, .. ]
me.funcParamListById = function(name) {
const id = name2Id[name]
const func = funcId2Func[id]
const res = []
if (func.prms == null) return
for (const rawElt of func.prms.s) {
for (const eltIdObj of rawElt.ids.s) {
res.push({
ctxId: id, // function id
id: eltIdObj.id,
type: rawElt.typ,
var: false // TODO later for proc this can also be true
// There is no opt nor omit attribute.
})
}
}
return res
}
// The result of this function is a flattened list of all attributes with
// the additional info about what entity in the hierarchy it belongs to.
// It returns:
// [ {ctxId:'', id:'', type:{t:'',..}, opt:true/false}, .. ]
me.fullAttrListById = function(name) {
const id = name2Id[name]
return fullAttrListById(id)
}
// Resolve all possible types or entities for a type reference.
// Select types are resolved (recursively if necessary).
// The ref of:
// {t:'type_ref', id:'..'}
// Returns for example:
// {'NttPerson':'entity', 'NttOrg':'entity', 'TpLabel':'type', ..}
me.possibleTypesOfRef = function(ref) {
const res = {}
resolveType(res, ref)
return res
}
me.nttEvalOrder = function(name) {
const id = name2Id[name]
return nttEvalOrder(id) // TODO cache result?
}
me.funcEvalOrder = function(name) {
const id = name2Id[name]
return funcEvalOrder(id) // TODO cache result?
}
me.procEvalOrder = function(name) {
const id = name2Id[name]
return procEvalOrder(id) // TODO cache result?
}
// Resolve all possible types or entities for a select type.
// Select types are resolved (recursively if necessary).
// Inheritence of entities and types is ignored. Also of types because their
// super types normally have less constraints.
// Finally possibilities looks like that for example:
// {'NttPerson':'entity', 'NttOrg':'entity', 'TpLabel':'type', ..}
// me.possibleTypesOfSelect = function(selectId) {
// if (selectId in tpId2Tp) {
// const type = tpId2Tp[selectId]
// if (type.spec.t == 'select'){
// const res = {}
// for (let subRef of type.spec.refs) {
// resolveType(res, subRef) // recur
// }
// return res
// }
// }
// throw new Error(`The id "${selectId}" is not of a select type.`)
// }
// The role name like 'SCHEMA_NAME.ENTITY_NAME.ATTR_NAME'.
// Returns {scmId: .., nttId: .., attrId: ..} or undefined.
me.resolveRoleName = function(roleName) {
if (typeof roleName == 'string') {
const prts = roleName.split('.')
const scIdUp = rawJson.id.toUpperCase()
if (prts.length == 3 && prts[0] == scIdUp) {
// console.log('prts', prts)
const eltName = scIdUp + '.' + prts[1]
const eltId = name2Id[eltName]
if (eltId && nttId2Ntt.hasOwnProperty(eltId)) {
// console.log('eltId', eltId)
const attrName = prts[2]
const attrs = fullAttrListById(eltId) // Is an entity id.
const res = attrs.find(elt => (elt.id.toUpperCase() == attrName))
// console.log(attrName, attrs.length, res)
if (res) {
// console.log(rawJson.id, eltId, res.id)
return {scmId: rawJson.id, nttId: eltId, attrId: res.id}
}
}
}
}
}
const setup = function() {
if (trust == false) {
// TODO validate the rawJson (from the parser).
// But not as long as we only use validated input files.
// - uniqueness of various ids
// - referenced ids exist
// - ranges are sensible (only static ones can be checked)
// - inverse redundances
// - circular declarations of select / subtype
// - default values of locals are valid if enum type declared
// - In expressions enum values are valid according to scope.
// -
}
if (rawJson.scts == null) return
for (let e of rawJson.scts.s) {
switch(e.t) {
case 'typ':
registerName(tpId2Tp, e)
break
case 'ntt':
registerName(nttId2Ntt, e)
break
case 'sct_dcl':
registerName({}, e) // TODO not {}
break
case 'fun':
registerName(funcId2Func, e)
break
case 'prc':
registerName(procId2Proc, e)
break
case 'rul':
registerName(rlId2Rl, e)
break
default:
throw new Error(`Unknown model element of type ${e.t}.`)
}
}
}
const registerName = function(idToElt, elt) {
const scIdUp = rawJson.id.toUpperCase()
const name = scIdUp + '.' + elt.id.toUpperCase()
if (name2Id.hasOwnProperty(name) == false) {
name2Id[name] = elt.id
idToElt[elt.id] = elt
} else {
throw new Error(`The element ${elt.id} maps to name ${name}, which is already registered.`)
}
}
// Resolve all possible types or entities for a type reference.
// Select types are resolved (recursively if necessary).
// Inheritence of entities and types is ignored. Also of types because their
// super types normally have less constraints.
// The ref of:
// {t:'type_ref', id:'..'}
// Finally possibilities looks like that for example:
// {'NttPerson':'entity', 'NttOrg':'entity', 'TpLabel':'type', ..}
const resolveType = function(possibilities, ref) {
if (ref.id in nttId2Ntt) {
possibilities[ref.id] = 'entity'
} else if (ref.id in tpId2Tp) {
const type = tpId2Tp[ref.id]
if (type.spc.t != 'slc') { // a select
// final candidate
possibilities[ref.id] = 'type'
// if (type.spec.t != 'type_ref') {
// // final candidate (E.g: type TpInt of INTEGER, or ENUM, or aggregation)
// possibilities[ref.id] = 'type'
// } else { // type.spec.t == 'type_ref' (E.g. TpCompass in the test data)
// resolveType(possibilities, type.spec)
// }
} else { // type.spc.t == 'slc'
for (let subRef of type.spc.refs.s) {
resolveType(possibilities, subRef) // recur
}
}
} else {
throw new Error(`Type reference to ${ref.id} cannot be resolved.`)
}
}
// The result of this function is a flattened list of all attributes with
// the additional info about what entity in the hierarchy it belongs to.
// It returns:
// [ {ctxId:'', id:'', type:{t:'',..}, opt:true/false}, .. ]
const fullAttrListById = function(id) {
// The raw model has per attribute several ids possible and there is no
// hint per attribute what enitity in the hierarchy it belongs to.
const inheritence = nttSuperChainById(id)
const attrs = []
for (let ntt of inheritence) {
const nttId = ntt.id
if (!!ntt.atrs) {
for (let rawAttr of ntt.atrs.s) {
for (let attrIdObj of rawAttr.ids.s) {
attrs.push({
ctxId: nttId,
id: attrIdObj.id,
type: rawAttr.typ,
omit: false,
opt: rawAttr.opt
})
}
}
}
// Use the qual_attr within the ntt.derives to determine the omitted boolean on attr to be true.
if (ntt.drvs) {
for (let drv of ntt.drvs.s) {
if (drv.id.t == 'qal_atr') {
// Search within attrs and set omit true.
for (let attr of attrs) {
if (attr.ctxId == drv.id.nttRef.id && attr.id == drv.id.id) {
attr.omit = true
}
}
}
}
}
//attrs.push(...ntt.attrs) is too simple
}
return attrs
}
const nttSuperChainById = function(id) {
let cur = nttId2Ntt[id]
const res = [cur]
while (!!cur.sprs && cur.sprs.s[0].t == 'ntt_ref') { // super
const spr = cur.sprs.s[0]
if (spr.id in nttId2Ntt) {
cur = nttId2Ntt[spr.id]
res.push(cur)
} else {
throw new Error(`When resolving super chain for entity ${id} on level ${cur.id}, the type ref ${spr.id} does not exist.`)
}
}
res.reverse()
return res
}
const typeSpecChainById = function(id) {
let cur = tpId2Tp[id]
const res = [cur]
while (cur.spc.t == 'any_ref') {
if (cur.spc.id in tpId2Tp) {
cur = tpId2Tp[cur.spc.id]
res.push(cur)
} else {
throw new Error(`When resolving spec chain for type ${id} on level ${cur.id}, the type ref ${cur.spc.id} does not exist.`)
}
}
res.reverse()
return res
}
// This function provides the order (at least one of several possible) in
// which the elements of a function can be evaluated savely, that means avoiding
// the situation that part of the expression is not yet defined (but could
// be so later).
// The order respects these different kind of elements from the function:
// - param definitions not consisting of aggregation or of aggregations with only static bounds
// - local definitions not consisting of aggregation or of aggregations with only static bounds
// - param definitions consisting of aggregations with dynamic bounds
// - local definitions consisting of aggregations with dynamic bounds
// - param value expressions
// - local value expressions
// The functions return value initialisation (bound spec with var_ref) is
// excluded from that order, and presumed to be the very last to be
// evaluated and calculated only at the very moment of return.
// The basis for the evaluation order is the values given by parameters that
// are not aggregations or aggregations with constant/static values for the
// bounds.
const funcEvalOrder = function(id) {
const func = funcId2Func[id]
const nameSpace = []
if (func.prms != null) {
nameSpace.push.apply(nameSpace, func.prms.s)
}
if (func.lcls != null) {
nameSpace.push.apply(nameSpace, func.lcls.s)
}
const circleMessageFct = function(eltsWithCircles, idsWithCirclesStr){
return `The ${func.id}'s parameters and locals most probably form at least one circular dependency including ${idsWithCirclesStr}.'`
}
// Pass func as additional element to evaluate also func.result's dependencies.
return evalOrderOfNameSpace(nameSpace, circleMessageFct, [func])
}
// This function provides the order (at least one of several possible) in
// which the elements of a procedure can be evaluated savely, that means avoiding
// the situation that part of the expression is not yet defined (but could
// be so later).
// The order respects these different kind of elements from the procedure:
// - param definitions not consisting of aggregation or of aggregations with only static bounds
// - local definitions not consisting of aggregation or of aggregations with only static bounds
// - param definitions consisting of aggregations with dynamic bounds
// - local definitions consisting of aggregations with dynamic bounds
// - param value expressions
// - local value expressions
// The basis for the evaluation order is the values given by parameters that
// are not aggregations or aggregations with constant/static values for the
// bounds.
const procEvalOrder = function(id) {
const proc = procId2Proc[id]
const nameSpace = []
if (proc.prms != null) {
nameSpace.push.apply(nameSpace, proc.prms.s)
}
if (proc.lcls != null) {
nameSpace.push.apply(nameSpace, proc.lcls.s)
}
const circleMessageFct = function(eltsWithCircles, idsWithCirclesStr){
return `The ${proc.id}'s parameters and locals most probably form at least one circular dependency including ${idsWithCirclesStr}.'`
}
return evalOrderOfNameSpace(nameSpace, circleMessageFct) // no additional elements
}
// This function provides the order (at least one of several possible) in
// which the elements of an entity can be evaluated savely, that means avoiding
// the situation that part of the expression is not yet defined (but could
// be so later).
// The order respects these different kind of elements from the entity:
// - attribute definitions not consisting of aggregation or of aggregations with only static bounds
// - derived definitions not consisting of aggregation or of aggregations with only static bounds
// - attribute definitions consisting of aggregations with dynamic bounds
// - derived definitions consisting of aggregations with dynamic bounds
// - derived value expressions
// - inverse value expressions
// - expressions within where constraints
// The basis for the evaluation order is the values given by attributes that
// are not aggregations or aggregations with constant/static values for the
// bounds.
const nttEvalOrder = function(id) {
const inheritence = nttSuperChainById(id)
// The inheritence is starting with the root entity first.
// The use of me.fullAttrListById here would not be appropriate, since
// derives and the others would be missing.
const nameSpace = []
const wheres = []
for (const ntt of inheritence) {
if (ntt.atrs != null) {
nameSpace.push.apply(nameSpace, ntt.atrs.s)
}
if (ntt.drvs != null) {
nameSpace.push.apply(nameSpace, ntt.drvs.s)
}
if (ntt.invs != null) {
nameSpace.push.apply(nameSpace, ntt.invs.s)
}
if (ntt.whrs != null) {
wheres.push.apply(wheres, ntt.whrs.s)
}
}
const circleMessageFct = function(eltsWithCircles, idsWithCirclesStr){
let ntt = nttId2Ntt[id]
return `The ${ntt.id}'s attributes, derives and inverse attributes most probably form at least one circular dependency including ${idsWithCirclesStr}. (Inheritence might veil the reason.)'`
}
return evalOrderOfNameSpace(nameSpace, circleMessageFct, wheres)
}
const evalOrderOfNameSpace = function(nameSpace, circleMessageFct, additionalElts = []) {
const idToElt = setupId2Elt(nameSpace) // Eventual renaming and shadowing is handled.
const elts = [] // actual elements
const eltSet = new WeakMap() // Just a temp variable for uniqueness.
for (const elt of Object.values(idToElt)) {
if (eltSet.has(elt) == false) {
elts.push(elt)
eltSet.set(elt, true)
}
}
elts.push.apply(elts, additionalElts)
// Sorting is not necessary, but having a deterministic result helps write tests.
elts.sort((a, b)=>idsOfElt(a)[0].localeCompare(idsOfElt(b)[0]))
// We cannot use WeakMap bacause later we need to know the number of elements.
// const deps = elts.map(ns => depsOfElt(ns, idToElt).reduce(
// (acc, elt) => {return acc.set(elt, true)}, new WeakMap()
// ))
const deps = elts.map(ns => depsOfElt(ns, idToElt))
// Keep the refernce to the dependencies for the end to also return them.
// Since each change to the elements of this array by filter later creates
// a new array this reference to the full dependencies remains valid.
const fullDeps = []
fullDeps.push.apply(fullDeps, deps)
// for (let i=0;i<elts.length;i++) {
// const elt = elts[i]
// const dep = deps[i]
// console.log(elt.id, elt.ids)
// console.log('depends on:')
// dep.forEach(function(delt){console.log(delt)})
// console.log('')
// }
// Resolve on elts and deps (the ones first that have no dependencies).
const res = []
const resIdxs = []
const out = new WeakMap()
let outCount = 0
while (outCount < elts.length) {
const wave = []
const waveIdxs = []
// Setup current wave.
for (let i=0;i<elts.length;i++) {
const elt = elts[i]
if (out.has(elt) == false) { // Ignore the ones already treated.
const dep = deps[i] // an array of elements
if (dep.length == 0) { // element without dependency
wave.push(elt)
waveIdxs.push(i)
}
}
}
// In all dependencies remove the ones, that have no dependencies left.
for (let i=0;i<elts.length;i++) {
const elt = elts[i]
if (out.has(elt) == false) { // Ignore the ones already treated.
deps[i] = deps[i].filter(welt=>wave.indexOf(welt)==-1)
}
}
// Mark wave as out (and count its elements).
for (const elt of wave) {
out.set(elt, true) // object is key
outCount++
}
// Handle pathological case of empty wave.
if (wave.length == 0) {
// The remaining elements may have circular dependencies.
const eltsWithCircles = elts.filter(elt=>out.has(elt)==false)
const idsStrFct = function(elt) {
return idsOfElt(elt).join(', ')
}
const idsWithCirclesStr = eltsWithCircles.map(idsStrFct).join(', ')
return {result: false, message:circleMessageFct(eltsWithCircles, idsWithCirclesStr)}
}
res.push.apply(res, wave)
resIdxs.push.apply(resIdxs, waveIdxs)
}
const directDeps = resIdxs.map(idx => fullDeps[idx])
const orderedDeps = directDeps.map(dep => collectDeps(elts, fullDeps, res, dep))
return {result:true, elements:res, dependencies:orderedDeps}
}
const collectDeps = function(elts, deps, order, base) {
const already = new WeakMap()
base.forEach(function(elt){already.set(elt, true)})
let next = base
while (0 < next.length) {
const cur = []
for (const elt of next) {
const i = elts.indexOf(elt)
const dep = deps[i]
for (const dd of dep) {
if (already.has(dd) == false) {
cur.push(dd)
already.set(dd, true)
}
}
}
next = cur
}
return order.filter(elt => already.has(elt))
}
const idsOfElt = function(elt) {
const ids = []
withIdsOfElt(elt, function(id){ids.push(id)})
return ids
}
const withIdsOfElt = function(elt, visitFct) {
if (!!elt.ids) { // attributes, locals and parameters
elt.ids.s.forEach(function(idObj){
visitFct(idObj.id, elt, idObj)
})
} else { // derives, inverse attributes and entities
if (typeof elt.id === 'object') {
// elt.id.t is attr_id or qual_attr (for both derives and inverse)
visitFct(elt.id.id, elt, elt.id)
} else { // typeof ns.id === 'string' and elt is entity
visitFct(elt.id, elt, null)
}
}
}
// Setup nameSpace lookup.
// The nameSpace is an array of attributes, derives, inverse attributes,
// parameters, locals or even entities (in case of rules.for).
// The order of the elements in nameSpace matters as soon as there is the
// same id twice or multiple time in the array. This may happen due to
// inheritence of entities with redeclared_attribute or a derive shadowing
// a super's attribute or derive.
const setupId2Elt = function(nameSpace) {
// A single id is either an attr_id or qual_attr (both having id as the
// field to pick). But for entity id contains directly the id string.
const res = {}
for (const elt of nameSpace) {
withIdsOfElt(elt, function(id, base){
res[id] = base
})
}
return res
}
// Get all dependencies of either a types where conditions,
// an entities attribute, derived attribute, inverse attribute or where condition
// or of a functions or procedures parameter or local variable declaration,
// or of a rules where condition.
// To get a functions return value's dependencies, pass the function as element.
// Most important this function returns also dependencies arising from any
// simple_expression used within aggregation bounds (of any level) within
// the type declaration.
const depsOfElt = function(elt, idToElt) {
const depIdToElt = {}
switch (elt.t) {
case 'atr_dcl':
depsOfType(elt.typ, idToElt, depIdToElt)
// Here only deps are possible if elt.typ is aggregation
// and in the bounds there are references. This is very rare.
break
case 'drv':
depsOfType(elt.typ, idToElt, depIdToElt)
depsOfExpr(elt.xpr, idToElt, depIdToElt)
break
case 'inv':
// We actually never really have complicated expressions in there, but
// just literal numbers or not set.
depsOfBounds(elt.agrBds, idToElt, depIdToElt)
break
case 'whr':
depsOfExpr(elt.xpr, idToElt, depIdToElt)
break
case 'prm_dcl':
depsOfType(elt.typ, idToElt, depIdToElt)
break
case 'lcl_dcl':
depsOfType(elt.typ, idToElt, depIdToElt)
depsOfExpr(elt.xpr, idToElt, depIdToElt)
break
case 'fun': // stands for its return value
depsOfType(elt.res, idToElt, depIdToElt)
break
default:
throw new Error(`Unknown type ${elt.t} to extract dependencies from.`)
}
return Object.values(depIdToElt)
}
const depsOfBounds = function(bounds, idToElt, depIdToElt) {
traverse(bounds, function(json, path, ii) {
if (json.t == 'any_ref' && idToElt[json.id]) {
depIdToElt[json.id] = idToElt[json.id]
}
})
}
// The only way dependencies arise from a type declaration is because of
// possible one or many bound_spec in there.
const depsOfType = function(type, idToElt, depIdToElt) {
traverse(type, function(json, path, ii) {
if (json.t == 'any_ref' && idToElt[json.id]) {
depIdToElt[json.id] = idToElt[json.id]
}
})
}
// Dependencies must be determined either for an expression or most often
// for a simple_expression. Thus expr can be either or.
const depsOfExpr = function(expr, idToElt, depIdToElt) {
traverse(expr, function(json, path, ii) {
if (json.t == 'any_ref' && idToElt[json.id]) {
depIdToElt[json.id] = idToElt[json.id]
}
})
}
setup()
return me
}