@sdeverywhere/compile 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1045 @@
1
+ import B from 'bufx'
2
+ import yaml from 'js-yaml'
3
+ import R from 'ramda'
4
+
5
+ import { decanonicalize, isIterable, listConcat, strlist, vlog, vsort } from '../_shared/helpers.js'
6
+ import {
7
+ addIndex,
8
+ allAliases,
9
+ allDimensions,
10
+ indexNamesForSubscript,
11
+ isDimension,
12
+ isIndex,
13
+ normalizeSubscripts,
14
+ sub,
15
+ subscriptFamilies
16
+ } from '../_shared/subscript.js'
17
+ import { createParser } from '../parse/parser.js'
18
+
19
+ import EquationReader from './equation-reader.js'
20
+ import SubscriptRangeReader from './subscript-range-reader.js'
21
+ import toposort from './toposort.js'
22
+ import VarNameReader from './var-name-reader.js'
23
+ import Variable from './variable.js'
24
+ import VariableReader from './variable-reader.js'
25
+
26
+ let variables = []
27
+ let inputVars = []
28
+ let constantExprs = new Map()
29
+
30
+ // Also keep variables in a map (with `varName` as key) for faster lookup
31
+ const variablesByName = new Map()
32
+
33
+ let nonAtoANames = Object.create(null)
34
+ // Set true for diagnostic printing of init, aux, and level vars in sorted order.
35
+ const PRINT_SORTED_VARS = false
36
+ // Set true to print dependency graphs before they are sorted.
37
+ const PRINT_INIT_GRAPH = false
38
+ const PRINT_AUX_GRAPH = false
39
+ const PRINT_LEVEL_GRAPH = false
40
+
41
+ function read(parseTree, spec, extData, directData, modelDirname) {
42
+ // Some arrays need to be separated into variables with individual indices to
43
+ // prevent eval cycles. They are manually added to the spec file.
44
+ let specialSeparationDims = spec.specialSeparationDims
45
+ // Subscript ranges must be defined before reading variables that use them.
46
+ readSubscriptRanges(parseTree, spec.dimensionFamilies, spec.indexFamilies, modelDirname)
47
+ // Read variables from the model parse tree.
48
+ readVariables(parseTree, specialSeparationDims, directData)
49
+ if (spec) {
50
+ // If the spec file contains `input/outputVarNames` (with full Vensim variable names)
51
+ // convert those to C names first. Otherwise, use `input/outputNames` which are already
52
+ // assumed to be valid C names.
53
+ if (spec.inputVarNames) {
54
+ spec.inputVars = R.map(cName, spec.inputVarNames)
55
+ }
56
+ if (spec.outputVarNames) {
57
+ spec.outputVars = R.map(cName, spec.outputVarNames)
58
+ }
59
+ // Save the input vars locally so that they can be referenced by `isInputVar`.
60
+ if (spec.inputVars) {
61
+ inputVars = spec.inputVars
62
+ }
63
+ }
64
+ // Analyze model equations to fill in more details about variables.
65
+ analyze()
66
+ // Check that all input and output vars in the spec actually exist in the model.
67
+ checkSpecVars(spec, extData)
68
+ // Remove variables that are not referenced by an input or output variable.
69
+ removeUnusedVariables(spec)
70
+ // Resolve duplicate declarations by converting to one variable type.
71
+ resolveDuplicateDeclarations()
72
+ }
73
+ function readSubscriptRanges(tree, dimensionFamilies, indexFamilies, modelDirname) {
74
+ // Read subscript ranges from the model.
75
+ let subscriptRangeReader = new SubscriptRangeReader(modelDirname)
76
+ subscriptRangeReader.visitModel(tree)
77
+ let allDims = allDimensions()
78
+ // Expand dimensions that appeared in subscript range definitions into indices.
79
+ // Repeat until there are only indices in dimension values.
80
+ let dimFoundInValue
81
+ do {
82
+ dimFoundInValue = false
83
+ for (let dim of allDims) {
84
+ if (dim.value !== '') {
85
+ let value = R.flatten(
86
+ R.map(subscript => (isDimension(subscript) ? sub(subscript).value : subscript), dim.value)
87
+ )
88
+ if (!R.equals(value, dim.value)) {
89
+ dimFoundInValue = true
90
+ dim.value = value
91
+ dim.size = value.length
92
+ }
93
+ }
94
+ }
95
+ } while (dimFoundInValue)
96
+
97
+ // Fill in subscript aliases from their model families.
98
+ for (let dim of allAliases()) {
99
+ if (dim.value === '') {
100
+ let refDim = sub(dim.family)
101
+ dim.value = refDim.value
102
+ dim.size = refDim.size
103
+ dim.modelValue = refDim.modelValue
104
+ allDims.push(dim)
105
+ }
106
+ }
107
+
108
+ // Update the families of dimensions. At this point, all dimensions have their family
109
+ // provisionally set to their own dimension name.
110
+ let dimComparator = (dim1, dim2) => {
111
+ // Sort dimensions by size ascending, by name descending.
112
+ if (dim1.size < dim2.size) {
113
+ return -1
114
+ } else if (dim1.size > dim2.size) {
115
+ return 1
116
+ } else if (dim1.name > dim2.name) {
117
+ return -1
118
+ } else if (dim1.name < dim2.name) {
119
+ return 1
120
+ } else {
121
+ return 0
122
+ }
123
+ }
124
+ for (let dim of allDims) {
125
+ // Try looking up the family in the spec file dimension families if they exist.
126
+ if (dimensionFamilies && dimensionFamilies[dim.name]) {
127
+ dim.family = dimensionFamilies[dim.name]
128
+ } else {
129
+ // Find the dimension in this family with the largest number of values.
130
+ // This is the "maximal" dimension that serves as the subscript family.
131
+ // If two dimensions have the same maximal size, choose the one that comes
132
+ // first in alpha sort order, by convention.
133
+ // Take the first index in the dimension.
134
+ let index = dim.value[0]
135
+ let familyDims = R.sort(
136
+ dimComparator,
137
+ R.filter(thisDim => R.contains(index, thisDim.value), allDims)
138
+ )
139
+ if (familyDims.length > 0) {
140
+ dim.family = R.last(familyDims).name
141
+ } else {
142
+ console.error(`No family found for dimension ${dim.name}`)
143
+ }
144
+ }
145
+ }
146
+
147
+ // Define indices in order from the maximal (family) dimension.
148
+ // Until now, only dimensions have been defined. We wait until dimension families have been
149
+ // determined to define indices, so that they will belong to exactly one dimension (the family).
150
+ for (let dim of allDims) {
151
+ if (dim.family === dim.name) {
152
+ for (let i = 0; i < dim.value.length; i++) {
153
+ addIndex(dim.value[i], i, dim.family)
154
+ }
155
+ }
156
+ }
157
+
158
+ // When there is a subscript mapping, the mapping value pulled from the subscript range
159
+ // in the model is either a map-to dimension with the same cardinality as the map-from
160
+ // dimension, or a list of subscripts in the map-to dimension with the same cardinality
161
+ // as the map-from dimension. The mapping value must be transformed into a list of
162
+ // map-from indices in one-to-one correspondence with the map-to indices.
163
+ for (let fromDim of allDims) {
164
+ for (let toDimName in fromDim.mappings) {
165
+ let toDim = sub(toDimName)
166
+ let mappingValue = fromDim.mappings[toDimName]
167
+ let invertedMappingValue = []
168
+ if (R.isEmpty(mappingValue)) {
169
+ // When there is no list of map-to subscripts, list fromDim indices.
170
+ invertedMappingValue = fromDim.value
171
+ } else {
172
+ // The mapping value is a list of map-to subscripts.
173
+ // List fromDim indices in the order in which they map onto toDim indices.
174
+ // Indices are filled in the mapping value by map-to index number as they
175
+ // occur in the map-from dimension.
176
+ let setMappingValue = (toSubName, toIndNumber, fromIndName) => {
177
+ if (Number.isInteger(toIndNumber) && toIndNumber >= 0 && toIndNumber < toDim.size) {
178
+ invertedMappingValue[toIndNumber] = fromIndName
179
+ } else {
180
+ console.error(
181
+ `ERROR: map-to index "${toSubName}" not found when mapping from dimension "${fromDim.name}" index "${fromIndName}"`
182
+ )
183
+ }
184
+ }
185
+ for (let i = 0; i < fromDim.value.length; i++) {
186
+ let fromIndName = fromDim.value[i]
187
+ let toSubName = mappingValue[i]
188
+ let toSub = sub(toSubName)
189
+ if (isDimension(toSubName)) {
190
+ // Fill in indices from a dimension in the mapping value.
191
+ for (let toIndName of toSub.value) {
192
+ let toIndNumber = toDim.value.indexOf(toIndName)
193
+ setMappingValue(toSubName, toIndNumber, fromIndName)
194
+ }
195
+ } else {
196
+ // Fill in a single index from an index in the mapping value.
197
+ let toIndNumber = toDim.value.indexOf(toSub.name)
198
+ setMappingValue(toSubName, toIndNumber, fromIndName)
199
+ }
200
+ }
201
+ }
202
+ // Replace toDim subscripts in the mapping value with fromDim subscripts that map to them.
203
+ fromDim.mappings[toDimName] = invertedMappingValue
204
+ }
205
+ }
206
+ }
207
+ function readVariables(tree, specialSeparationDims, directData) {
208
+ // Read all variables in the model parse tree.
209
+ // This populates the variables table with basic information for each variable
210
+ // such as the var name and subscripts.
211
+ let variableReader = new VariableReader(specialSeparationDims, directData)
212
+ variableReader.visitModel(tree)
213
+ // Add a placeholder variable for the exogenous variable Time.
214
+ let v = new Variable(null)
215
+ v.modelLHS = 'Time'
216
+ v.varName = '_time'
217
+ addVariable(v)
218
+ }
219
+ function analyze() {
220
+ // Analyze the RHS of each equation in stages after all the variables are read.
221
+ // Find non-apply-to-all vars that are defined with more than one equation.
222
+ findNonAtoAVars()
223
+ // Set the refId for each variable. Only non-apply-to-all vars include subscripts in the refId.
224
+ setRefIds()
225
+ // Read the RHS to list the refIds of vars that are referenced and set the var type.
226
+ readEquations()
227
+ }
228
+
229
+ function checkSpecVars(spec, extData) {
230
+ // Look up each var in the spec and issue and error message if it does not exist.
231
+
232
+ function check(varNames, specType) {
233
+ if (isIterable(varNames)) {
234
+ for (let varName of varNames) {
235
+ if (!R.contains('[', varName)) {
236
+ if (!varWithRefId(varName)) {
237
+ // Look for a variable in external data.
238
+ if (extData.has(varName)) {
239
+ // console.error(`found ${specType} ${varName} in extData`)
240
+ // Copy data from an external file to an equation that does a lookup.
241
+ let lookup = R.reduce(
242
+ (a, p) => listConcat(a, `(${p[0]}, ${p[1]})`, true),
243
+ '',
244
+ Array.from(extData.get(varName))
245
+ )
246
+ let modelEquation = `${decanonicalize(varName)} = WITH LOOKUP(Time, (${lookup}))`
247
+ addEquation(modelEquation)
248
+ } else {
249
+ console.error(`${specType} variable ${varName} not found in the model or external data sources`)
250
+ }
251
+ }
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ if (spec) {
258
+ check(spec.inputVars, 'input')
259
+ check(spec.outputVars, 'output')
260
+ }
261
+ }
262
+
263
+ function removeUnusedVariables(spec) {
264
+ // Remove any variables that are not referenced by an input or output variable.
265
+ // This ensures that only computations that are relevant to the outputs are performed.
266
+
267
+ // Only remove dead code if we have an explicit set of inputs and outputs
268
+ if (!spec.outputVars || spec.outputVars.length === 0 || !spec.inputVars || spec.inputVars.length === 0) {
269
+ return
270
+ }
271
+
272
+ // Keep track of all variable names that are referenced somewhere. Note that we
273
+ // don't attempt to track specific "ref ids" (e.g. `_some_variable[_subscript]`)
274
+ // but instead just track generic variable names (e.g. `_some_variable`). This
275
+ // ensures that we include all subscripts for a variable, which might mean we
276
+ // include some subscripts that aren't needed, but it is safer than trying to
277
+ // eliminate those and possibly omit something that is needed.
278
+ const referencedVarNames = []
279
+
280
+ // Add the given variable name to the list of referenced variables, if it's not
281
+ // already there.
282
+ const recordUsedVarName = varName => {
283
+ if (!referencedVarNames.includes(varName)) {
284
+ referencedVarNames.push(varName)
285
+ }
286
+ }
287
+
288
+ // Add the given variable to the list of referenced variables, and do the same for
289
+ // some special things (i.e., lookups) that it might reference.
290
+ const recordUsedVariable = v => {
291
+ // Add the variable to the list of referenced variables
292
+ recordUsedVarName(v.varName)
293
+
294
+ // Include any lookup variables that are referenced by this variable
295
+ if (v.referencedLookupVarNames) {
296
+ for (const lookupVarName of v.referencedLookupVarNames) {
297
+ recordUsedVarName(lookupVarName)
298
+ }
299
+ }
300
+
301
+ // Look through the list of function names that are referenced by this
302
+ // variable and see if any of them are lookups (which should be included in
303
+ // our list of referenced variables)
304
+ if (v.referencedFunctionNames) {
305
+ for (const fnName of v.referencedFunctionNames) {
306
+ // Convert the function name (e.g. `__damage_lookup`) to a lookup var name (chop off
307
+ // the leading underscore)
308
+ const lookupName = fnName.slice(1)
309
+ const varForFn = varWithName(lookupName)
310
+ if (varForFn && varForFn.isLookup()) {
311
+ recordUsedVarName(varForFn.varName)
312
+ }
313
+ }
314
+ }
315
+ }
316
+
317
+ // Walk the reference tree rooted at the given var and record it (and anything
318
+ // that it references) as being "used".
319
+ const referencedRefIds = new Set()
320
+ const recordRefsOfVariable = v => {
321
+ // If this variable is subscripted, we need to record all subscript variants;
322
+ // `refIdsWithName` will return those. We also need to record all variables
323
+ // that are referenced by this variable, either directly (`v.references`) or
324
+ // in an "INITIAL" expression (`v.initReferences`). It's OK if we end up with
325
+ // duplicates in this list, because we will examine each reference only once.
326
+ let refIds = refIdsWithName(v.varName)
327
+ refIds = refIds.concat(v.references)
328
+ refIds = refIds.concat(v.initReferences)
329
+ for (const refId of refIds) {
330
+ if (!referencedRefIds.has(refId)) {
331
+ referencedRefIds.add(refId)
332
+ const refVar = varWithRefId(refId)
333
+ recordUsedVariable(refVar)
334
+ recordRefsOfVariable(refVar)
335
+ }
336
+ }
337
+ }
338
+
339
+ // Always keep special vars used by SDE
340
+ recordUsedVarName('_initial_time')
341
+ recordUsedVarName('_final_time')
342
+ recordUsedVarName('_saveper')
343
+ recordUsedVarName('_time_step')
344
+
345
+ // Keep all input variables
346
+ for (const inputVarName of spec.inputVars) {
347
+ for (const v of varsWithName(inputVarName)) {
348
+ recordUsedVariable(v)
349
+ }
350
+ }
351
+
352
+ // Keep all output variables and the variables they depend on
353
+ for (const outputVarName of spec.outputVars) {
354
+ // The outputVars can include a raw index, e.g. `_output_var[0]`,
355
+ // which isn't an actual "ref id", so we'll just derive the
356
+ // var name by chopping off the index part.
357
+ const outputVarBaseName = outputVarName.split('[')[0]
358
+ for (const v of varsWithName(outputVarBaseName)) {
359
+ recordUsedVariable(v)
360
+ recordRefsOfVariable(v)
361
+ }
362
+ }
363
+
364
+ // Filter out unneeded variables so we're left with the minimal set of variables to emit
365
+ variables = R.filter(v => referencedVarNames.includes(v.varName), variables)
366
+
367
+ // Rebuild the variables-by-name map
368
+ variablesByName.clear()
369
+ for (const v of variables) {
370
+ let varsForName = variablesByName.get(v.varName)
371
+ if (!varsForName) {
372
+ varsForName = []
373
+ variablesByName.set(v.varName, varsForName)
374
+ }
375
+ varsForName.push(v)
376
+ }
377
+ }
378
+ function resolveDuplicateDeclarations() {
379
+ // Find subscripted const vars where some subscripts are data vars.
380
+ // TODO consider doing the same for lookup vars
381
+ // Least and greatest safe double values in C rounded to convenient consts
382
+ const MIN_SAFE_DBL = -1e308
383
+ const MAX_SAFE_DBL = 1e308
384
+ let data = dataVars()
385
+ for (let constVar of constVars()) {
386
+ if (data.find(d => d.varName === constVar.varName)) {
387
+ // Change the var type from const to data and add lookup data points.
388
+ // For a constant, the equivalent lookup has the same value over the entire x axis.
389
+ let value = parseFloat(constVar.modelFormula)
390
+ if (isNaN(value)) {
391
+ console.error(`The value for const var ${constVar.refId} converted to a lookup is NaN.`)
392
+ }
393
+ constVar.varType = 'data'
394
+ constVar.points = [
395
+ [MIN_SAFE_DBL, value],
396
+ [MAX_SAFE_DBL, value]
397
+ ]
398
+ }
399
+ }
400
+ }
401
+ //
402
+ // Analysis helpers
403
+ //
404
+ function findNonAtoAVars() {
405
+ // Find variables with multiple instances with the same var name, which makes them
406
+ // elements in a non-apply-to-all array. This function constructs the nonAtoANames list.
407
+ function areSubsEqual(vars, i) {
408
+ // Scan the subscripts for each var at position i in normal order.
409
+ // Return true if the subscript is the same for all vars with that name.
410
+ let subscript = vars[0].subscripts[i]
411
+ for (let v of vars) {
412
+ if (v.subscripts[i] !== subscript) {
413
+ return false
414
+ }
415
+ }
416
+ return true
417
+ }
418
+ R.forEach(name => {
419
+ let vars = varsWithName(name)
420
+ if (vars.length > 1) {
421
+ // This is a non-apply-to-all array. Construct the exansion dims array for it.
422
+ // The expansion dim is true at each dim position where the subscript varies.
423
+ let numDims = vars[0].subscripts.length
424
+ let expansionDims = []
425
+ for (let i = 0; i < numDims; i++) {
426
+ expansionDims[i] = !areSubsEqual(vars, i)
427
+ }
428
+ nonAtoANames[name] = expansionDims
429
+ }
430
+ }, varNames())
431
+ }
432
+ function addNonAtoAVar(varName, expansionDims) {
433
+ nonAtoANames[varName] = expansionDims
434
+ }
435
+ function setRefIds() {
436
+ // Set the refId for each var. This requires knowing which vars are non-apply-to-all.
437
+ R.forEach(v => {
438
+ v.refId = refIdForVar(v)
439
+ }, variables)
440
+ }
441
+ function readEquations() {
442
+ // Augment variables with information from their equations.
443
+ // This requires a refId for each var so that actual refIds can be resolved for the reference list.
444
+ R.forEach(v => {
445
+ let equationReader = new EquationReader(v)
446
+ equationReader.read()
447
+ }, variables)
448
+ }
449
+ function addEquation(modelEquation) {
450
+ // Add an equation in Vensim model format.
451
+ let parser = createParser(modelEquation)
452
+ let tree = parser.equation()
453
+ // Read the var and add it to the Model var table.
454
+ let variableReader = new VariableReader()
455
+ variableReader.visitEquation(tree)
456
+ let v = variableReader.var
457
+ // Fill in the refId.
458
+ v.refId = refIdForVar(v)
459
+ // Finish the variable by parsing the RHS.
460
+ let equationReader = new EquationReader(v)
461
+ equationReader.read()
462
+ }
463
+ //
464
+ // Model API
465
+ //
466
+ function addVariable(v) {
467
+ // Add the variable to the variables list.
468
+ variables.push(v)
469
+
470
+ // Add to the map of variables by name
471
+ let varsForName = variablesByName.get(v.varName)
472
+ if (!varsForName) {
473
+ varsForName = []
474
+ variablesByName.set(v.varName, varsForName)
475
+ }
476
+ varsForName.push(v)
477
+ }
478
+ function isNonAtoAName(varName) {
479
+ return R.has(varName, nonAtoANames)
480
+ }
481
+ function expansionFlags(varName) {
482
+ return nonAtoANames[varName]
483
+ }
484
+ function allVars() {
485
+ // Return all vars except placeholders.
486
+ function isNotPlaceholderVar(v) {
487
+ return v.varName !== '_time'
488
+ }
489
+ return R.filter(isNotPlaceholderVar, variables)
490
+ }
491
+ function constVars() {
492
+ return vsort(varsOfType('const'))
493
+ }
494
+ function lookupVars() {
495
+ return vsort(varsOfType('lookup'))
496
+ }
497
+ function dataVars() {
498
+ return vsort(varsOfType('data'))
499
+ }
500
+ function auxVars() {
501
+ // console.error('AUX VARS');
502
+ return sortVarsOfType('aux')
503
+ }
504
+ function levelVars() {
505
+ // console.error('LEVEL VARS');
506
+ return sortVarsOfType('level')
507
+ }
508
+ function initVars() {
509
+ // console.error('INIT VARS');
510
+ return sortInitVars()
511
+ }
512
+ function varWithRefId(refId) {
513
+ const findVarWithRefId = rid => {
514
+ // First see if we have a map key where ref id matches the var name
515
+ let varsForName = variablesByName.get(rid)
516
+ if (varsForName) {
517
+ const v = R.find(R.propEq('refId', rid), varsForName)
518
+ if (v) {
519
+ return v
520
+ }
521
+ }
522
+
523
+ // Failing that, chop off the subscript part of the ref id and
524
+ // find the variables that share that name
525
+ const varNamePart = rid.split('[')[0]
526
+ varsForName = variablesByName.get(varNamePart)
527
+ if (varsForName) {
528
+ const v = R.find(R.propEq('refId', rid), varsForName)
529
+ if (v) {
530
+ return v
531
+ }
532
+ }
533
+
534
+ return undefined
535
+ }
536
+
537
+ // Find a variable from a reference id.
538
+ // A direct reference will find scalar vars, apply-to-all arrays, and non-apply-to-all array
539
+ // elements defined by individual index.
540
+ let refVar = findVarWithRefId(refId)
541
+ if (!refVar) {
542
+ // Look at variables with the reference's varName to find one with matching subscripts.
543
+ let refIdParts = splitRefId(refId)
544
+ let refVarName = refIdParts.varName
545
+ let refSubscripts = refIdParts.subscripts
546
+ let varRefIds = refIdsWithName(refVarName)
547
+ for (const varRefId of varRefIds) {
548
+ let { subscripts } = splitRefId(varRefId)
549
+ // Compare subscripts at each position in normal order. If the var name does not have subscripts,
550
+ // the match will succeed, since the var is an apply-to-all array that includes the refId.
551
+ let matches = true
552
+ for (let pos = 0; pos < subscripts.length; pos++) {
553
+ // If both subscripts are an index or dimension, they must match.
554
+ if (
555
+ (isIndex(subscripts[pos]) && isIndex(refSubscripts[pos])) ||
556
+ (isDimension(subscripts[pos]) && isDimension(refSubscripts[pos]))
557
+ ) {
558
+ if (subscripts[pos] !== refSubscripts[pos]) {
559
+ matches = false
560
+ break
561
+ }
562
+ } else if (isDimension(subscripts[pos]) && isIndex(refSubscripts[pos])) {
563
+ // If the ref subscript is an index and the var subscript is a dimension,
564
+ // they match if the dimension includes the index.
565
+ if (!sub(subscripts[pos]).value.includes(refSubscripts[pos])) {
566
+ matches = false
567
+ break
568
+ }
569
+ } else {
570
+ // We should not encounter a case where the ref subscript is a dimension
571
+ // and the var subscript is an index.
572
+ matches = false
573
+ break
574
+ }
575
+ }
576
+ if (matches) {
577
+ refVar = findVarWithRefId(varRefId)
578
+ break
579
+ }
580
+ }
581
+ if (!refVar) {
582
+ vlog('ERROR: no var found for refId', refId)
583
+ }
584
+ }
585
+ return refVar
586
+ }
587
+ function splitRefId(refId) {
588
+ // Split a refId into component parts with a regular expression matching var name and subscripts.
589
+ let re = /\w+|\[/g
590
+ let inSubs = false
591
+ let varName = ''
592
+ let subscripts = []
593
+ let m
594
+ while ((m = re.exec(refId))) {
595
+ if (m[0] === '[') {
596
+ inSubs = true
597
+ } else if (inSubs) {
598
+ subscripts.push(m[0])
599
+ } else {
600
+ varName = m[0]
601
+ }
602
+ }
603
+ // Put subscripts in normal order.
604
+ subscripts = normalizeSubscripts(subscripts)
605
+ return { varName, subscripts }
606
+ }
607
+ function varWithName(varName) {
608
+ // Find a variable with the given name in canonical form.
609
+ // The function returns the first instance of a non-apply-to-all variable with the name.
610
+ const varsForName = variablesByName.get(varName)
611
+ if (varsForName && varsForName.length > 0) {
612
+ return varsForName[0]
613
+ } else {
614
+ return undefined
615
+ }
616
+ }
617
+ function varsWithName(varName) {
618
+ // Find all variables with the given name in canonical form.
619
+ return variablesByName.get(varName) || []
620
+ }
621
+ function refIdsWithName(varName) {
622
+ // Find refIds of all variables with the given name in canonical form.
623
+ return varsWithName(varName).map(v => v.refId)
624
+ }
625
+ function varNames() {
626
+ // Return a sorted list of var names.
627
+ return R.uniq(Array.from(variablesByName.keys())).sort()
628
+ }
629
+ function vensimName(cVarName) {
630
+ // Convert a C variable name to a Vensim name.
631
+ let result = cVarName
632
+ // Get the variable name and subscripts with regexes.
633
+ let m = cVarName.match(/(_[A-Za-z0-9_]+)((\[\d+\])*)/)
634
+ if (m) {
635
+ let varName = m[1]
636
+ let indexNumbers = []
637
+ for (let x of m[2].matchAll(/\[(\d+)\]/g)) {
638
+ indexNumbers.push(x[1])
639
+ }
640
+ // Get the subscript families and look up the subscript names.
641
+ let subscripts = []
642
+ let v = varWithName(varName)
643
+ if (v) {
644
+ // Ensure that the C var name is subscripted when the var has subscripts.
645
+ if (R.isEmpty(v.subscripts) || !R.isEmpty(indexNumbers)) {
646
+ m = v.modelLHS.match(/[^[]+/)
647
+ if (m) {
648
+ result = m[0]
649
+ }
650
+ let families = subscriptFamilies(v.subscripts)
651
+ for (let i = 0; i < families.length; i++) {
652
+ let indexNames = indexNamesForSubscript(families[i])
653
+ let indexNumber = Number.parseInt(indexNumbers[i])
654
+ let indexModelName = decanonicalize(indexNames[indexNumber])
655
+ subscripts.push(indexModelName)
656
+ }
657
+ if (!R.isEmpty(subscripts)) {
658
+ result += `[${subscripts.join(',')}]`
659
+ }
660
+ } else {
661
+ console.error(`${cVarName} has no subscripts in vensimName`)
662
+ }
663
+ } else {
664
+ console.error(`no var with name ${varName} in vensimName`)
665
+ }
666
+ }
667
+ return result
668
+ }
669
+ function cName(vensimVarName) {
670
+ // Convert a Vensim variable name to a C name.
671
+ // This function requires model analysis to be completed first when the variable has subscripts.
672
+ return new VarNameReader().read(vensimVarName)
673
+ }
674
+ function isInputVar(varName) {
675
+ // Return true if the given variable (in canonical form) is included in the list of
676
+ // input variables in the spec file.
677
+ return inputVars.includes(varName)
678
+ }
679
+ function addConstantExpr(exprText, constantValue) {
680
+ // Record the constant value for the given expression in a map for later lookup.
681
+ constantExprs.set(exprText, constantValue)
682
+ }
683
+ function getConstantExprValue(exprText) {
684
+ // Return the constant value for the given expression if one was recorded.
685
+ return constantExprs.get(exprText)
686
+ }
687
+ //
688
+ // Helpers for getting lists of vars
689
+ //
690
+ function varsOfType(varType, vars = null) {
691
+ // Extract vars of the given var type.
692
+ if (!vars) {
693
+ vars = variables
694
+ }
695
+ function pass(v) {
696
+ return v.varType === varType && v.varName !== '_time'
697
+ }
698
+ return R.filter(pass, vars)
699
+ }
700
+ function sortVarsOfType(varType) {
701
+ if (PRINT_SORTED_VARS) {
702
+ console.error(varType.toUpperCase())
703
+ }
704
+
705
+ // Get vars with varType 'aux' or 'level' sorted in dependency order at eval time.
706
+ // Start with vars of the given varType.
707
+ let vars = varsOfType(varType)
708
+
709
+ // Accumulate a list of variable dependencies as var pairs.
710
+ let graph = R.unnest(R.map(v => refs(v), vars))
711
+ function refs(v) {
712
+ // Return a list of dependency pairs for all vars referenced by v at eval time.
713
+ let refs = R.map(refId => varWithRefId(refId), v.references)
714
+ // Only consider references having the correct var type.
715
+ // Remove duplicate references.
716
+ refs = R.uniq(R.filter(R.propEq('varType', varType), refs))
717
+ // Return the list of dependencies as refId pairs.
718
+ return R.map(ref => {
719
+ if (v.varType === 'level' && ref.varType === 'level') {
720
+ // Reverse the order of level-to-level references so that level evaluation refers
721
+ // to the value in the previous time step rather than the currently evaluated one.
722
+ return [ref.refId, v.refId]
723
+ } else {
724
+ return [v.refId, ref.refId]
725
+ }
726
+ }, refs)
727
+ }
728
+
729
+ // Sort into an lhs dependency list.
730
+ if (PRINT_AUX_GRAPH) printDepsGraph(graph, 'AUX')
731
+ if (PRINT_LEVEL_GRAPH) printDepsGraph(graph, 'LEVEL')
732
+ let deps
733
+ try {
734
+ deps = toposort(graph).reverse()
735
+ } catch (e) {
736
+ console.error(e.message)
737
+ process.exit(1)
738
+ }
739
+
740
+ // Turn the dependency-sorted var name list into a var list.
741
+ let sortedVars = varsOfType(
742
+ varType,
743
+ R.map(refId => varWithRefId(refId), deps)
744
+ )
745
+
746
+ // Add the ref ids to a set for faster lookup in the next step
747
+ const sortedVarRefIds = new Set()
748
+ for (const v of sortedVars) {
749
+ sortedVarRefIds.add(v.refId)
750
+ }
751
+
752
+ // Find vars of the given varType with no dependencies, and add them to the list.
753
+ const nodepVars = R.filter(v => !sortedVarRefIds.has(v.refId), vars)
754
+ const sortedNodepVars = vsort(nodepVars)
755
+ sortedVars = R.concat(sortedNodepVars, sortedVars)
756
+
757
+ if (PRINT_SORTED_VARS) {
758
+ sortedVars.forEach(v => console.error(`${v.refId}`))
759
+ }
760
+ return sortedVars
761
+ }
762
+ function sortInitVars() {
763
+ if (PRINT_SORTED_VARS) {
764
+ console.error('INIT')
765
+ }
766
+
767
+ // Get dependencies at init time for vars with init values, such as levels.
768
+ // This will be a subgraph of all dependencies rooted in vars with init values.
769
+ // Therefore, we have to recurse into dependencies starting with those vars.
770
+ let initVars = R.filter(R.propEq('hasInitValue', true), variables)
771
+ // vlog('initVars.length', initVars.length);
772
+
773
+ // Copy the list so we can mutate it and have the original list later.
774
+ // This starts a queue of vars to examine. Referenced var will be added to the queue.
775
+ let vars = R.map(v => v.copy(), initVars)
776
+ // printVars(vars);
777
+ // R.forEach(v => { console.error(v.refId); console.error(v.references); }, vars);
778
+
779
+ // Keep track of which var ref ids are currently in the queue for faster lookup
780
+ const queueRefIds = new Set()
781
+ for (const v of vars) {
782
+ queueRefIds.add(v.refId)
783
+ }
784
+
785
+ // Build a map of dependencies indexed by the lhs of each var.
786
+ const depsMap = new Map()
787
+ while (vars.length > 0) {
788
+ let v = vars.pop()
789
+ queueRefIds.delete(v.refId)
790
+ // console.error(`- ${v.refId} (${vars.length})`);
791
+ addDepsToMap(v)
792
+ }
793
+
794
+ function addDepsToMap(v) {
795
+ // Add dependencies of var v to the map when they are not already present.
796
+ // Use init references for vars such as levels that have an initial value.
797
+ let refIds = v.hasInitValue ? v.initReferences : v.references
798
+ // console.error(`${v.refId} ${refIds.length}`);
799
+ if (refIds.length > 0) {
800
+ // console.error(`${v.refId}`);
801
+ // Add dependencies for each referenced var.
802
+ depsMap.set(v.refId, refIds)
803
+ // console.error(`→ ${v.refId}`);
804
+ R.forEach(refId => {
805
+ // Add each dependency onto the queue if it has not already been analyzed.
806
+ if (!depsMap.get(refId)) {
807
+ // console.error(refId);
808
+ let refVar = varWithRefId(refId)
809
+ if (refVar) {
810
+ if (refVar.varType !== 'const' && !queueRefIds.has(refVar.refId)) {
811
+ vars.push(refVar)
812
+ queueRefIds.add(refVar.refId)
813
+ // console.error(`+ ${refVar.refId}`);
814
+ }
815
+ } else {
816
+ console.error(`no var with refId for ${refId}, referenced by ${v.refId}`)
817
+ }
818
+ }
819
+ }, refIds)
820
+ }
821
+ }
822
+
823
+ // Construct a dependency graph in the form of [var name, dependency var name] pairs.
824
+ // We use refIds instead of vars here because the deps are stated in refIds.
825
+ let graph = []
826
+ // vlog('depsMap', depsMap);
827
+ for (let refId of depsMap.keys()) {
828
+ R.forEach(dep => graph.push([refId, dep]), depsMap.get(refId))
829
+ }
830
+ if (PRINT_INIT_GRAPH) printDepsGraph(graph, 'INIT')
831
+
832
+ // Sort into a reference id dependency list.
833
+ let deps
834
+ try {
835
+ deps = toposort(graph).reverse()
836
+ } catch (e) {
837
+ console.error(e.message)
838
+ process.exit(1)
839
+ }
840
+
841
+ // Turn the reference id list into a var list.
842
+ let sortedVars = R.map(refId => varWithRefId(refId), deps)
843
+
844
+ // Filter out vars with constant values.
845
+ sortedVars = R.reject(
846
+ R.propSatisfies(varType => varType === 'const' || varType === 'lookup' || varType === 'data', 'varType'),
847
+ sortedVars
848
+ )
849
+
850
+ // Add the ref ids to a set for faster lookup in the next step
851
+ const sortedVarRefIds = new Set()
852
+ for (const v of sortedVars) {
853
+ sortedVarRefIds.add(v.refId)
854
+ }
855
+
856
+ // Find vars with init values but no dependencies, and add them to the list.
857
+ const nodepVars = R.filter(v => !sortedVarRefIds.has(v.refId), initVars)
858
+ const sortedNodepVars = vsort(nodepVars)
859
+ sortedVars = R.concat(sortedNodepVars, sortedVars)
860
+
861
+ if (PRINT_SORTED_VARS) {
862
+ sortedVars.forEach(v => console.error(`${v.refId}`))
863
+ }
864
+ return sortedVars
865
+ }
866
+ //
867
+ // Helpers for refIds
868
+ //
869
+ function refIdForVar(v) {
870
+ // Start a reference id using the variable name.
871
+ let refId = v.varName
872
+ // References to apply-to-all arrays reference the entire array, so no subscripts
873
+ // are required in the refId.
874
+ if (v.hasSubscripts() && isNonAtoAName(v.varName)) {
875
+ // Add subscripts already sorted in normal form for references to non-apply-to-all arrays.
876
+ refId += `[${v.subscripts.join(',')}]`
877
+ }
878
+ return refId
879
+ }
880
+ //
881
+ // Helpers for model analysis
882
+ //
883
+ function printVarList() {
884
+ // Print full information on each var.
885
+ B.clearBuf()
886
+ let vars = R.sortBy(R.prop('refId'), variables)
887
+ for (const v of vars) {
888
+ printVar(v)
889
+ }
890
+ return B.getBuf()
891
+ }
892
+ function yamlVarList() {
893
+ // Print selected properties of all variable objects to a YAML string.
894
+ let vars = R.sortBy(
895
+ R.prop('refId'),
896
+ R.map(v => filterVar(v), variables)
897
+ )
898
+ return yaml.safeDump(vars)
899
+ }
900
+ function printVar(v) {
901
+ let nonAtoA = isNonAtoAName(v.varName) ? ' (non-apply-to-all)' : ''
902
+ B.emitLine(`${v.modelLHS}: ${v.varType}${nonAtoA}`)
903
+ if (!v.hasPoints()) {
904
+ B.emitLine(`= ${v.modelFormula}`)
905
+ }
906
+ B.emitLine(`refId(${v.refId})`)
907
+ if (v.hasSubscripts()) {
908
+ B.emitLine(`families(${strlist(subscriptFamilies(v.subscripts))})`)
909
+ B.emitLine(`subscripts(${strlist(v.subscripts)})`)
910
+ }
911
+ if (v.separationDims.length > 0) {
912
+ B.emitLine(`separationDims(${strlist(v.separationDims)})`)
913
+ }
914
+ B.emitLine(`hasInitValue(${v.hasInitValue})`)
915
+ if (v.references.length > 0) {
916
+ B.emitLine(`refs(${strlist(v.references)})`)
917
+ }
918
+ if (v.initReferences.length > 0) {
919
+ B.emitLine(`initRefs(${strlist(v.initReferences)})`)
920
+ }
921
+ // if (v.hasPoints()) {
922
+ // B.emitLine(R.map(p => `(${p[0]}, ${p[1]})`, v.points));
923
+ // }
924
+ B.emitLine('')
925
+ }
926
+ function filterVar(v) {
927
+ let varObj = {}
928
+ varObj.refId = v.refId
929
+ varObj.varName = v.varName
930
+ if (v.hasSubscripts()) {
931
+ varObj.subscripts = v.subscripts
932
+ varObj.families = subscriptFamilies(v.subscripts)
933
+ }
934
+ if (v.references.length > 0) {
935
+ varObj.references = v.references
936
+ }
937
+ varObj.hasInitValue = v.hasInitValue
938
+ if (v.initReferences.length > 0) {
939
+ varObj.initReferences = v.initReferences
940
+ }
941
+ varObj.varType = v.varType
942
+ if (v.separationDims.length > 0) {
943
+ varObj.separationDims = v.separationDims
944
+ }
945
+ varObj.modelLHS = v.modelLHS
946
+ varObj.modelFormula = v.modelFormula
947
+ return varObj
948
+ }
949
+ function printRefIdTest() {
950
+ // Verify that each variable has the correct number of instances of the var name.
951
+ R.forEach(v => {
952
+ let varName = v.varName
953
+ let vars = varsWithName(varName)
954
+ if (v.hasSubscripts()) {
955
+ if (isNonAtoAName(varName)) {
956
+ // A non-apply-to-all array has more than one instance of the var name in practice.
957
+ if (vars.length < 2) {
958
+ vlog('ERROR: only one instance of non-apply-to-all array', varName)
959
+ }
960
+ } else {
961
+ // An apply-to-all array should have only one instance of the var name.
962
+ if (vars.length > 1) {
963
+ vlog('ERROR: more than one instance of apply-to-all array', varName)
964
+ // printVars(vars)
965
+ }
966
+ }
967
+ } else {
968
+ // The var is a scalar and should only have one instance of the var name.
969
+ if (vars.length > 1) {
970
+ vlog('ERROR: more than one instance of scalar var', varName)
971
+ // printVars(vars)
972
+ }
973
+ }
974
+ }, variables)
975
+ // Verify that each refId in references exists as the refId of a concrete variable.
976
+ R.forEach(v => {
977
+ R.forEach(refId => checkRefVar(refId), v.references)
978
+ R.forEach(refId => checkRefVar(refId), v.initReferences)
979
+ }, variables)
980
+ function checkRefVar(refId) {
981
+ let refVar = R.find(R.propEq('refId', refId), variables)
982
+ if (!refVar) {
983
+ vlog('ERROR: no var for refId', refId)
984
+ }
985
+ }
986
+ }
987
+ function printRefGraph(varName) {
988
+ // Walk the reference tree rooted at varName and print it out in indented form.
989
+ let printRefs = (v, indent, stack) => {
990
+ for (let refId of v.references) {
991
+ // Exclude a variable here to limit the depth of the search.
992
+ // if (!refId.startsWith('_policy_levels')) {
993
+ if (!stack.includes(refId)) {
994
+ console.log(`${' '.repeat(indent)}${refId}`)
995
+ let refVar = R.find(R.propEq('refId', refId), variables)
996
+ printRefs(refVar, indent + 1, R.append(refId, stack))
997
+ }
998
+ // }
999
+ }
1000
+ }
1001
+ for (let v of varsWithName(varName)) {
1002
+ console.log(v.varName)
1003
+ printRefs(v, 1, [])
1004
+ }
1005
+ }
1006
+ function printDepsGraph(graph, varType) {
1007
+ // The dependency graph is an array of pairs.
1008
+ console.error(`${varType} GRAPH`)
1009
+ for (const dep of graph) {
1010
+ console.error(`${dep[0]} → ${dep[1]}`)
1011
+ }
1012
+ }
1013
+ export default {
1014
+ addConstantExpr,
1015
+ addEquation,
1016
+ addNonAtoAVar,
1017
+ addVariable,
1018
+ allVars,
1019
+ auxVars,
1020
+ cName,
1021
+ constVars,
1022
+ dataVars,
1023
+ expansionFlags,
1024
+ filterVar,
1025
+ getConstantExprValue,
1026
+ initVars,
1027
+ isInputVar,
1028
+ isNonAtoAName,
1029
+ levelVars,
1030
+ lookupVars,
1031
+ printRefGraph,
1032
+ printRefIdTest,
1033
+ printVarList,
1034
+ read,
1035
+ refIdForVar,
1036
+ refIdsWithName,
1037
+ splitRefId,
1038
+ variables,
1039
+ varNames,
1040
+ varsWithName,
1041
+ varWithName,
1042
+ varWithRefId,
1043
+ vensimName,
1044
+ yamlVarList
1045
+ }