@sdeverywhere/compile 0.7.17 → 0.7.18

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.
@@ -1,723 +0,0 @@
1
- import { ModelParser } from 'antlr4-vensim'
2
- import * as R from 'ramda'
3
-
4
- import {
5
- canonicalName,
6
- canonicalVensimName,
7
- cFunctionName,
8
- decanonicalize,
9
- isDelayFunction,
10
- isSeparatedVar,
11
- isSmoothFunction,
12
- isTrendFunction,
13
- isNpvFunction,
14
- matchRegex,
15
- newAuxVarName,
16
- newLevelVarName,
17
- newLookupVarName,
18
- newFixedDelayVarName,
19
- newDepreciationVarName,
20
- cartesianProductOf
21
- } from '../_shared/helpers.js'
22
- import {
23
- extractMarkedDims,
24
- indexNamesForSubscript,
25
- isDimension,
26
- isIndex,
27
- normalizeSubscripts,
28
- separatedVariableIndex,
29
- sub
30
- } from '../_shared/subscript.js'
31
- import ModelReader from '../parse/model-reader.js'
32
- import { createParser } from '../parse/parser.js'
33
-
34
- import ExprReader from './expr-reader.js'
35
- import Model from './model.js'
36
- import VariableReader from './variable-reader.js'
37
-
38
- // Set this true to get a list of functions used in the model. This may include lookups.
39
- const PRINT_FUNCTION_NAMES = false
40
-
41
- export default class EquationReader extends ModelReader {
42
- constructor(variable) {
43
- super()
44
- // variable that will be read
45
- this.var = variable
46
- // reference id constructed in parts
47
- this.refId = ''
48
- // list of reference ids filled when a dimension reference is expanded; overrides this.refId
49
- this.expandedRefIds = []
50
- // flag that indicates the RHS has something other than a constant
51
- this.rhsNonConst = false
52
- }
53
- read() {
54
- // Fill in more information about the variable by analyzing the equation parse tree.
55
- // Variables that were added programmatically do not have a parse tree context.
56
- if (this.var.eqnCtx) {
57
- this.visitEquation(this.var.eqnCtx)
58
- }
59
- // Refine the var type based on the contents of the equation.
60
- if (this.var.points.length > 0) {
61
- this.var.varType = 'lookup'
62
- } else if (this.var.isAux() && !this.rhsNonConst) {
63
- this.var.varType = 'const'
64
- }
65
- }
66
- //
67
- // Helpers
68
- //
69
- addReferencesToList(list) {
70
- // Add reference ids gathered while walking the RHS parse tree to the variable's reference list.
71
- let add = refId => {
72
- // In Vensim a variable can refer to its current value in the state.
73
- // Do not add self-references to the lists of references.
74
- // Do not duplicate references.
75
- if (refId !== this.var.refId && !list.includes(refId)) {
76
- list.push(refId)
77
- }
78
- }
79
- // Add expanded reference ids if they exist, otherwise, add the single reference id.
80
- if (R.isEmpty(this.expandedRefIds)) {
81
- add(this.refId)
82
- } else {
83
- this.expandedRefIds.forEach(refId => add(refId))
84
- }
85
- }
86
- //
87
- // Visitor callbacks
88
- //
89
- visitCall(ctx) {
90
- // Mark the RHS as non-constant, since it has a function call.
91
- this.rhsNonConst = true
92
- // Convert the function name from Vensim to C format.
93
- let fn = cFunctionName(ctx.Id().getText())
94
- this.callStack.push({ fn: fn })
95
- if (PRINT_FUNCTION_NAMES) {
96
- console.error(fn)
97
- }
98
- if (fn === '_INTEG' || fn === '_DELAY_FIXED') {
99
- this.var.varType = 'level'
100
- this.var.hasInitValue = true
101
- if (fn === '_DELAY_FIXED') {
102
- this.var.varSubtype = 'fixedDelay'
103
- this.var.fixedDelayVarName = canonicalName(newFixedDelayVarName())
104
- }
105
- } else if (fn === '_INITIAL') {
106
- this.var.varType = 'initial'
107
- this.var.hasInitValue = true
108
- } else if (fn === '_ACTIVE_INITIAL' || fn === '_SAMPLE_IF_TRUE') {
109
- this.var.hasInitValue = true
110
- } else if (fn === '_GET_DIRECT_DATA' || fn === '_GET_DIRECT_LOOKUPS') {
111
- this.var.varType = 'data'
112
- } else if (fn === '_GET_DIRECT_CONSTANTS') {
113
- this.var.varType = 'const'
114
- } else if (fn === '_DEPRECIATE_STRAIGHTLINE') {
115
- this.var.hasInitValue = true
116
- this.var.varSubtype = 'depreciation'
117
- this.var.depreciationVarName = canonicalName(newDepreciationVarName())
118
- }
119
- super.visitCall(ctx)
120
- this.callStack.pop()
121
- }
122
- visitExprList(ctx) {
123
- let fn = this.currentFunctionName()
124
- if (isSmoothFunction(fn)) {
125
- // Generate a level var to expand the SMOOTH* call.
126
- // TODO consider allowing more than one SMOOTH* call substitution
127
- // Get SMOOTH* arguments for the function expansion.
128
- let args = R.map(expr => expr.getText(), ctx.expr())
129
- this.expandSmoothFunction(fn, args)
130
- } else if (isTrendFunction(fn)) {
131
- // Generate a level var to expand the TREND call.
132
- // Get TREND arguments for the function expansion.
133
- let args = R.map(expr => expr.getText(), ctx.expr())
134
- let input = args[0]
135
- let avgTime = args[1]
136
- let init = args[2]
137
- let level = this.expandTrendFunction(fn, args)
138
- let genSubs = this.genSubs(input, avgTime, init)
139
- let aux = newAuxVarName()
140
- this.addVariable(
141
- `${aux}${genSubs} = ZIDZ(${input} - ${level}${genSubs}, ${avgTime} * ABS(${level}${genSubs})) ~~|`
142
- )
143
- this.var.trendVarName = canonicalName(aux)
144
- this.var.references.push(this.var.trendVarName)
145
- } else if (isNpvFunction(fn)) {
146
- // Generate level vars to expand the NPV call.
147
- // Get NPV arguments for the function expansion.
148
- let args = R.map(expr => expr.getText(), ctx.expr())
149
- let stream = args[0]
150
- let discountRate = args[1]
151
- let initVal = args[2]
152
- let factor = args[3]
153
- let level = this.generateNpvLevels(stream, discountRate, initVal, factor)
154
- let genSubs = this.genSubs(stream, discountRate, initVal, factor)
155
- let aux = newAuxVarName()
156
- // npv = (ncum + stream * TIME STEP * df) * factor
157
- this.addVariable(`${aux}${genSubs} = (${level.ncum} + ${stream} * TIME STEP * ${level.df}) * ${factor} ~~|`)
158
- this.var.npvVarName = canonicalName(aux)
159
- this.var.references.push(this.var.npvVarName)
160
- } else if (isDelayFunction(fn)) {
161
- // Generate a level var to expand the DELAY* call.
162
- let args = R.map(expr => expr.getText(), ctx.expr())
163
- this.expandDelayFunction(fn, args)
164
- } else if (fn === '_GET_DIRECT_DATA' || fn === '_GET_DIRECT_LOOKUPS') {
165
- // Extract string constant arguments into an object used in code generation.
166
- // For Excel files, the file argument names an indirect "?" file tag from the model settings.
167
- // For CSV files, it gives a relative pathname in the model directory.
168
- // For Excel files, the tab argument names an Excel worksheet.
169
- // For CSV files, it gives the delimiter character.
170
- let args = R.map(
171
- arg => matchRegex(arg, /'(.*)'/),
172
- R.map(expr => expr.getText(), ctx.expr())
173
- )
174
- this.var.directDataArgs = {
175
- file: args[0],
176
- tab: args[1],
177
- timeRowOrCol: args[2],
178
- startCell: args[3]
179
- }
180
- } else if (fn === '_GET_DIRECT_CONSTANTS') {
181
- // Extract string constant arguments into an object used in code generation.
182
- // The file argument gives a relative pathname in the model directory.
183
- // The tab argument gives the delimiter character.
184
- let args = R.map(
185
- arg => matchRegex(arg, /'(.*)'/),
186
- R.map(expr => expr.getText(), ctx.expr())
187
- )
188
- this.var.directConstArgs = {
189
- file: args[0],
190
- tab: args[1],
191
- startCell: args[2]
192
- }
193
- } else if (fn === '_IF_THEN_ELSE') {
194
- if (process.env.SDE_NONPUBLIC_REDUCE_VARIABLES !== '0') {
195
- // Evaluate the condition expression of the `IF THEN ELSE`. If it resolves
196
- // to a compile-time constant, we only need to visit one branch, which means
197
- // that no references will be recorded for the other branch, therefore allowing
198
- // it to be skipped in the unused reference elimination phase and during the
199
- // final code generation phase.
200
- const condText = ctx.expr(0).getText()
201
- const exprReader = new ExprReader()
202
- const condExpr = exprReader.read(condText)
203
- if (condExpr.constantValue !== undefined) {
204
- // Record the conditional expression and its constant value so that
205
- // it can be accessed later by EquationGen. We need to record it
206
- // this way because any variables referenced by the expression may
207
- // be removed during the unused reference elimination phase.
208
- Model.addConstantExpr(condText, condExpr.constantValue)
209
- if (condExpr.constantValue !== 0) {
210
- // Only visit the "if true" branch
211
- this.setArgIndex(1)
212
- ctx.expr(1).accept(this)
213
- } else {
214
- // Only visit the "if false" branch
215
- this.setArgIndex(2)
216
- ctx.expr(2).accept(this)
217
- }
218
- } else {
219
- // Visit the condition and both branches
220
- super.visitExprList(ctx)
221
- }
222
- } else {
223
- // Optimization is disabled, visit the condition and both branches
224
- super.visitExprList(ctx)
225
- }
226
- } else {
227
- // Keep track of all function names referenced in this expression. Note that lookup
228
- // variables are sometimes function-like, so they will be included here. This will be
229
- // used later to decide whether a lookup variable needs to be included in generated code.
230
- const canonicalFnName = canonicalName(fn)
231
- if (this.var.referencedFunctionNames) {
232
- if (!this.var.referencedFunctionNames.includes(canonicalFnName)) {
233
- this.var.referencedFunctionNames.push(canonicalFnName)
234
- }
235
- } else {
236
- this.var.referencedFunctionNames = [canonicalFnName]
237
- }
238
- super.visitExprList(ctx)
239
- }
240
- }
241
- visitVar(ctx) {
242
- // Mark the RHS as non-constant, since it has a variable reference.
243
- this.rhsNonConst = true
244
- // Get the var name of a variable in a call and save it as a reference.
245
- let id = ctx.Id().getText()
246
- let varName = canonicalName(id)
247
- // Do not add a dimension or index name as a reference.
248
- if (!isDimension(varName) && !isIndex(varName)) {
249
- let fn = this.currentFunctionName()
250
- this.refId = varName
251
- this.expandedRefIds = []
252
- super.visitVar(ctx)
253
- // Separate init references from eval references in level formulas.
254
- if (isSmoothFunction(fn) || isTrendFunction(fn) || isNpvFunction(fn) || isDelayFunction(fn)) {
255
- // Do not set references inside the call, since it will be replaced
256
- // with the generated level var.
257
- } else if (this.argIndexForFunctionName('_INTEG') === 1) {
258
- this.addReferencesToList(this.var.initReferences)
259
- } else if (this.argIndexForFunctionName('_DELAY_FIXED') === 1) {
260
- this.addReferencesToList(this.var.initReferences)
261
- } else if (this.argIndexForFunctionName('_DELAY_FIXED') === 2) {
262
- this.addReferencesToList(this.var.initReferences)
263
- } else if (this.argIndexForFunctionName('_DEPRECIATE_STRAIGHTLINE') === 1) {
264
- this.addReferencesToList(this.var.initReferences)
265
- } else if (this.argIndexForFunctionName('_DEPRECIATE_STRAIGHTLINE') === 2) {
266
- this.addReferencesToList(this.var.initReferences)
267
- } else if (this.argIndexForFunctionName('_ACTIVE_INITIAL') === 1) {
268
- this.addReferencesToList(this.var.initReferences)
269
- } else if (this.argIndexForFunctionName('_SAMPLE_IF_TRUE') === 2) {
270
- this.addReferencesToList(this.var.initReferences)
271
- } else if (this.argIndexForFunctionName('_ALLOCATE_AVAILABLE') === 1) {
272
- // Reference the second and third elements of the priority profile argument instead of the first one
273
- // that Vensim requires for ALLOCATE AVAILABLE. This is required to get correct dependencies.
274
- let ptypeRefId = this.expandedRefIds[0]
275
- let { subscripts } = Model.splitRefId(ptypeRefId)
276
- let ptypeIndexName = subscripts[1]
277
- let profileElementsDimName = sub(ptypeIndexName).family
278
- let profileElementsDim = sub(profileElementsDimName)
279
- let priorityRefId = ptypeRefId.replace(ptypeIndexName, profileElementsDim.value[1])
280
- let widthRefId = ptypeRefId.replace(ptypeIndexName, profileElementsDim.value[2])
281
- this.expandedRefIds = [priorityRefId, widthRefId]
282
- this.addReferencesToList(this.var.references)
283
- } else if (this.var.isInitial()) {
284
- this.addReferencesToList(this.var.initReferences)
285
- } else {
286
- this.addReferencesToList(this.var.references)
287
- }
288
- }
289
- }
290
- visitLookupCall(ctx) {
291
- // Mark the RHS as non-constant, since it has a lookup.
292
- this.rhsNonConst = true
293
- // Keep track of the lookup variable that is referenced on the RHS.
294
- const id = ctx.Id().getText()
295
- const lookupVarName = canonicalName(id)
296
- if (this.var.referencedLookupVarNames) {
297
- this.var.referencedLookupVarNames.push(lookupVarName)
298
- } else {
299
- this.var.referencedLookupVarNames = [lookupVarName]
300
- }
301
- // Complete the visit.
302
- ctx.expr().accept(this)
303
- super.visitLookupCall(ctx)
304
- }
305
- visitLookupArg(ctx) {
306
- // When a call argument is a lookup, generate a new lookup variable and save the variable name to emit later.
307
- // TODO consider expanding this to more than one lookup arg per equation
308
- const lookupArgVarName = this.generateLookupArg(ctx)
309
- this.var.lookupArgVarName = lookupArgVarName
310
- // Keep track of all lookup variables that are referenced. This will be used later to decide
311
- // whether a lookup variable needs to be included in generated code.
312
- if (this.var.referencedLookupVarNames) {
313
- this.var.referencedLookupVarNames.push(lookupArgVarName)
314
- } else {
315
- this.var.referencedLookupVarNames = [lookupArgVarName]
316
- }
317
- }
318
- visitSubscriptList(ctx) {
319
- // When an equation references a non-appy-to-all array, add its subscripts to the array var's refId.
320
- if (ctx.parentCtx.ruleIndex === ModelParser.RULE_expr) {
321
- // Get the referenced var's subscripts in canonical form.
322
- let subscripts = R.map(id => canonicalName(id.getText()), ctx.Id())
323
- // Remove dimension subscripts marked with ! and save them for later.
324
- let markedDims = extractMarkedDims(subscripts)
325
- subscripts = normalizeSubscripts(subscripts)
326
- // console.error(`${this.var.refId} → ${this.refId} [ ${subscripts} ]`);
327
- if (subscripts.length > 0) {
328
- // See if this variable is non-apply-to-all. At this point, the refId is just the var name.
329
- // References to apply-to-all variables do not need subscripts since they refer to the whole array.
330
- let expansionFlags = Model.expansionFlags(this.refId)
331
- if (expansionFlags) {
332
- // The reference is to a non-apply-to-all variable.
333
- // Find the refIds of the vars that include the indices in the reference.
334
- // Get the vars with the var name of the reference. We will choose from these vars.
335
- let varsWithRefName = Model.varsWithName(this.refId)
336
- // The refIds of actual vars containing the indices will accumulate with possible duplicates.
337
- let expandedRefIds = []
338
- let iSub
339
- // Accumulate an array of lists of the separated index names at each position.
340
- let indexNames = []
341
- for (iSub = 0; iSub < expansionFlags.length; iSub++) {
342
- if (expansionFlags[iSub]) {
343
- // For each index name at the subscript position, find refIds for vars that include the index.
344
- // This process ensures that we generate references to vars that are in the var table.
345
- let indexNamesAtPos
346
- // Use the single index name for a separated variable if it exists.
347
- // But don't do this if the subscript is a marked dimension in a vector function.
348
- let separatedIndexName = separatedVariableIndex(subscripts[iSub], this.var, subscripts)
349
- if (!markedDims.includes(subscripts[iSub]) && separatedIndexName) {
350
- indexNamesAtPos = [separatedIndexName]
351
- } else {
352
- // Generate references to all the indices for the subscript.
353
- indexNamesAtPos = indexNamesForSubscript(subscripts[iSub])
354
- }
355
- indexNames.push(indexNamesAtPos)
356
- }
357
- }
358
- // Flatten the arrays of index names at each position into an array of index name combinations.
359
- let separatedIndices = cartesianProductOf(indexNames)
360
- // Find a separated variable for each combination of indices.
361
- for (let separatedIndex of separatedIndices) {
362
- // Consider each var with the same name as the reference in the equation.
363
- for (let refVar of varsWithRefName) {
364
- let iSeparatedIndex = 0
365
- for (iSub = 0; iSub < expansionFlags.length; iSub++) {
366
- if (expansionFlags[iSub]) {
367
- let refVarIndexNames = indexNamesForSubscript(refVar.subscripts[iSub])
368
- if (refVarIndexNames.length === 0) {
369
- console.error(
370
- `ERROR: no subscript at subscript position ${iSub} for var ${refVar.refId} with subscripts ${refVar.subscripts}`
371
- )
372
- }
373
- if (!refVarIndexNames.includes(separatedIndex[iSeparatedIndex++])) {
374
- break
375
- }
376
- }
377
- }
378
- if (iSub >= expansionFlags.length) {
379
- // All separated index names matched index names in the var, so add it as a reference.
380
- expandedRefIds.push(refVar.refId)
381
- break
382
- }
383
- }
384
- }
385
- // Sort the expandedRefIds and eliminate duplicates.
386
- this.expandedRefIds = R.uniq(expandedRefIds.sort())
387
- }
388
- }
389
- }
390
- super.visitSubscriptList(ctx)
391
- }
392
- visitLookupRange(ctx) {
393
- this.var.range = R.map(p => this.getPoint(p), ctx.lookupPoint())
394
- super.visitLookupRange(ctx)
395
- }
396
- visitLookupPointList(ctx) {
397
- this.var.points = R.map(p => this.getPoint(p), ctx.lookupPoint())
398
- super.visitLookupPointList(ctx)
399
- }
400
- getPoint(lookupPoint) {
401
- let exprs = lookupPoint.expr()
402
- if (exprs.length >= 2) {
403
- return [parseFloat(exprs[0].getText()), parseFloat(exprs[1].getText())]
404
- }
405
- }
406
- generateLookupArg(lookupArgCtx) {
407
- // Generate a variable for a lookup argument found in the RHS.
408
- let varName = newLookupVarName()
409
- let eqn = `${varName}${lookupArgCtx.getText()} ~~|`
410
- this.addVariable(eqn)
411
- return canonicalName(varName)
412
- }
413
- expandSmoothFunction(fn, args) {
414
- // Generate variables for a SMOOTH* call found in the RHS.
415
- let input = args[0]
416
- let delay = args[1]
417
- let init = args[2] !== undefined ? args[2] : args[0]
418
- if (fn === '_SMOOTH' || fn === '_SMOOTHI') {
419
- this.generateSmoothLevel(input, delay, init, 1)
420
- } else if (fn === '_SMOOTH3' || fn === '_SMOOTH3I') {
421
- let delay3 = `(${delay} / 3)`
422
- let level1 = this.generateSmoothLevel(input, delay3, init, 1)
423
- let level2 = this.generateSmoothLevel(level1, delay3, init, 2)
424
- this.generateSmoothLevel(level2, delay3, init, 3)
425
- }
426
- }
427
- generateSmoothLevel(input, delay, init, levelNumber) {
428
- // Generate a level equation to implement SMOOTH.
429
- // The parameters are model names. Return the canonical name of the generated level var.
430
- let genSubs = this.genSubs(input, delay, init)
431
- // For SMOOTH3, the previous level is the input for level number 2 and 3. Add RHS subscripts.
432
- if (levelNumber > 1 && genSubs) {
433
- input += genSubs
434
- }
435
- let level, levelLHS, levelRefId
436
- if (isSeparatedVar(this.var)) {
437
- // Levels generated by separated vars are also separated. We have to compute the indices here instead
438
- // of using the dimension on the LHS and letting addVariable do it, so that the whole array of
439
- // separated variables are not added for each visit here by an already-separated index.
440
- // Start by getting a level var based on the var name, so it is the same for all separated levels.
441
- level = newLevelVarName(this.var.varName, levelNumber)
442
- // Replace the dimension in the generated variable subscript with the separated index from the LHS.
443
- // Find the index in the LHS that was expanded from the separation dimension.
444
- let index
445
- let sepDim
446
- let r = genSubs.match(/\[(.*)\]/)
447
- if (r) {
448
- let rhsSubs = r[1].split(',').map(x => canonicalName(x))
449
- for (let rhsSub of rhsSubs) {
450
- let separatedIndexName = separatedVariableIndex(rhsSub, this.var, rhsSubs)
451
- if (separatedIndexName) {
452
- index = decanonicalize(separatedIndexName)
453
- sepDim = decanonicalize(rhsSub)
454
- break
455
- }
456
- }
457
- }
458
- // Use the Vensim form of the index in the LHS and in all arguments.
459
- if (index) {
460
- let re = new RegExp(`\\[(.*?)${sepDim}(.*?)\\]`, 'gi')
461
- let replacement = `[$1${index}$2]`
462
- let newGenSubs = genSubs.replace(re, replacement)
463
- levelLHS = `${level}${newGenSubs}`
464
- levelRefId = canonicalVensimName(levelLHS)
465
- input = input.replace(re, replacement)
466
- delay = delay.replace(re, replacement)
467
- init = init.replace(re, replacement)
468
- }
469
- } else {
470
- // In the normal case, generate a unique variable name for the level var.
471
- level = newLevelVarName()
472
- levelLHS = level + genSubs
473
- // If it has subscripts, the refId is still just the var name, because it is an apply-to-all array.
474
- levelRefId = canonicalName(level)
475
- }
476
- let eqn = `${levelLHS} = INTEG((${input} - ${levelLHS}) / ${delay}, ${init}) ~~|`
477
- if (isSeparatedVar(this.var)) {
478
- Model.addNonAtoAVar(canonicalName(level), [true])
479
- }
480
- this.addVariable(eqn)
481
- // Add a reference to the new level var.
482
- // For SMOOTH3, the smoothVarRefId is the final level refId.
483
- this.var.smoothVarRefId = levelRefId
484
- this.refId = levelRefId
485
- this.expandedRefIds = []
486
- this.addReferencesToList(this.var.references)
487
- return level
488
- }
489
- expandTrendFunction(fn, args) {
490
- // Generate variables for a TREND call found in the RHS.
491
- let input = args[0]
492
- let avgTime = args[1]
493
- let init = args[2]
494
- let level = this.generateTrendLevel(input, avgTime, init)
495
- return level
496
- }
497
- generateTrendLevel(input, avgTime, init) {
498
- // Generate a level equation to implement TREND.
499
- // The parameters are model names. Return the canonical name of the generated level var.
500
- let genSubs = this.genSubs(input, avgTime, init)
501
- let level = newLevelVarName()
502
- let levelLHS = level + genSubs
503
- let eqn = `${levelLHS} = INTEG((${input} - ${levelLHS}) / ${avgTime}, ${input} / (1 + ${init} * ${avgTime})) ~~|`
504
- this.addVariable(eqn)
505
- // Add a reference to the new level var.
506
- // If it has subscripts, the refId is still just the var name, because it is an apply-to-all array.
507
- this.refId = canonicalName(level)
508
- this.expandedRefIds = []
509
- this.addReferencesToList(this.var.references)
510
- return level
511
- }
512
- generateNpvLevels(stream, discountRate, initVal, factor) {
513
- // Generate two level equations to implement NPV.
514
- // Return the canonical names of the generated level vars as object properties.
515
- let genSubs = this.genSubs(stream, discountRate, initVal, factor)
516
- // df = INTEG((-df * discount rate) / (1 + discount rate * TIME STEP), 1)
517
- let df = newLevelVarName()
518
- let dfLHS = df + genSubs
519
- let dfEqn = `${dfLHS} = INTEG((-${dfLHS} * ${discountRate}) / (1 + ${discountRate} * TIME STEP), 1) ~~|`
520
- this.addVariable(dfEqn)
521
- // ncum = INTEG(stream * df, init val)
522
- let ncum = newLevelVarName()
523
- let ncumLHS = ncum + genSubs
524
- let ncumEqn = `${ncumLHS} = INTEG(${stream} * ${dfLHS}, ${initVal}) ~~|`
525
- this.addVariable(ncumEqn)
526
- // Add references to the new level vars.
527
- // If they have subscripts, the refIds are still just the var name, because they are apply-to-all arrays.
528
- this.refId = ''
529
- this.expandedRefIds = [canonicalName(ncum), canonicalName(df)]
530
- this.addReferencesToList(this.var.references)
531
- return { ncum, df }
532
- }
533
- expandDelayFunction(fn, args) {
534
- // Generate variables for a DELAY* call found in the RHS.
535
- let input = args[0]
536
- let delay = args[1]
537
- let genSubs = this.genSubs(this.var.modelLHS)
538
-
539
- if (fn === '_DELAY1' || fn === '_DELAY1I') {
540
- let level, levelLHS, levelRefId
541
- let init = `${args[2] !== undefined ? args[2] : args[0]} * ${delay}`
542
- let varLHS = this.var.modelLHS
543
- if (isSeparatedVar(this.var)) {
544
- level = newLevelVarName(this.var.varName, 1)
545
- let index
546
- let sepDim
547
- let r = genSubs.match(/\[(.*)\]/)
548
- if (r) {
549
- let rhsSubs = r[1].split(',').map(x => canonicalName(x))
550
- for (let rhsSub of rhsSubs) {
551
- let separatedIndexName = separatedVariableIndex(rhsSub, this.var, rhsSubs)
552
- if (separatedIndexName) {
553
- index = decanonicalize(separatedIndexName)
554
- sepDim = decanonicalize(rhsSub)
555
- break
556
- }
557
- }
558
- }
559
- if (index) {
560
- let re = new RegExp(sepDim, 'gi')
561
- genSubs = genSubs.replace(re, index)
562
- levelLHS = `${level}${genSubs}`
563
- levelRefId = canonicalVensimName(levelLHS)
564
- input = input.replace(re, index)
565
- varLHS = varLHS.replace(re, index)
566
- delay = delay.replace(re, index)
567
- init = init.replace(re, index)
568
- }
569
- Model.addNonAtoAVar(canonicalName(level), [true])
570
- } else {
571
- level = newLevelVarName()
572
- levelLHS = level + genSubs
573
- levelRefId = canonicalName(level)
574
- }
575
- // Generate a level var that will replace the DELAY function call.
576
- this.var.delayVarRefId = this.generateDelayLevel(levelLHS, levelRefId, input, varLHS, init)
577
- // Generate an aux var to hold the delay time expression.
578
- let delayTimeVarName = newAuxVarName()
579
- this.var.delayTimeVarName = canonicalName(delayTimeVarName)
580
- if (isSeparatedVar(this.var)) {
581
- Model.addNonAtoAVar(this.var.delayTimeVarName, [true])
582
- }
583
- let delayTimeEqn = `${delayTimeVarName}${genSubs} = ${delay} ~~|`
584
- this.addVariable(delayTimeEqn)
585
- // Add a reference to the var, since it won't show up until code gen time.
586
- this.var.references.push(canonicalVensimName(`${delayTimeVarName}${genSubs}`))
587
- } else if (fn === '_DELAY3' || fn === '_DELAY3I') {
588
- let level1, level1LHS, level1RefId
589
- let level2, level2LHS, level2RefId
590
- let level3, level3LHS, level3RefId
591
- let delay3 = `((${delay}) / 3)`
592
- let init = `${args[2] !== undefined ? args[2] : args[0]} * ${delay3}`
593
- let aux1, aux1LHS
594
- let aux2, aux2LHS
595
- let aux3, aux3LHS
596
- let aux4, aux4LHS
597
- if (isSeparatedVar(this.var)) {
598
- level1 = newLevelVarName(this.var.varName, 1)
599
- level2 = newLevelVarName(this.var.varName, 2)
600
- level3 = newLevelVarName(this.var.varName, 3)
601
- aux1 = newAuxVarName(this.var.varName, 1)
602
- aux2 = newAuxVarName(this.var.varName, 2)
603
- aux3 = newAuxVarName(this.var.varName, 3)
604
- aux4 = newAuxVarName(this.var.varName, 4)
605
- let index
606
- let sepDim
607
- let r = genSubs.match(/\[(.*)\]/)
608
- if (r) {
609
- let rhsSubs = r[1].split(',').map(x => canonicalName(x))
610
- for (let rhsSub of rhsSubs) {
611
- let separatedIndexName = separatedVariableIndex(rhsSub, this.var, rhsSubs)
612
- if (separatedIndexName) {
613
- index = decanonicalize(separatedIndexName)
614
- sepDim = decanonicalize(rhsSub)
615
- break
616
- }
617
- }
618
- }
619
- if (index) {
620
- let re = new RegExp(sepDim, 'gi')
621
- genSubs = genSubs.replace(re, index)
622
- level1LHS = `${level1}${genSubs}`
623
- level2LHS = `${level2}${genSubs}`
624
- level3LHS = `${level3}${genSubs}`
625
- aux1LHS = `${aux1}${genSubs}`
626
- aux2LHS = `${aux2}${genSubs}`
627
- aux3LHS = `${aux3}${genSubs}`
628
- aux4LHS = `${aux4}${genSubs}`
629
- level1RefId = canonicalVensimName(level1LHS)
630
- level2RefId = canonicalVensimName(level2LHS)
631
- level3RefId = canonicalVensimName(level3LHS)
632
- input = input.replace(re, index)
633
- delay3 = delay3.replace(re, index)
634
- init = init.replace(re, index)
635
- }
636
- Model.addNonAtoAVar(canonicalName(level1), [true])
637
- Model.addNonAtoAVar(canonicalName(level2), [true])
638
- Model.addNonAtoAVar(canonicalName(level3), [true])
639
- } else {
640
- level1 = newLevelVarName()
641
- level2 = newLevelVarName()
642
- level3 = newLevelVarName()
643
- aux1 = newAuxVarName()
644
- aux2 = newAuxVarName()
645
- aux3 = newAuxVarName()
646
- aux4 = newAuxVarName()
647
- level1LHS = level1 + genSubs
648
- level2LHS = level2 + genSubs
649
- level3LHS = level3 + genSubs
650
- aux1LHS = aux1 + genSubs
651
- aux2LHS = aux2 + genSubs
652
- aux3LHS = aux3 + genSubs
653
- aux4LHS = aux4 + genSubs
654
- level1RefId = canonicalName(level1)
655
- level2RefId = canonicalName(level2)
656
- level3RefId = canonicalName(level3)
657
- }
658
- // Generate a level var that will replace the DELAY function call.
659
- this.var.delayVarRefId = this.generateDelayLevel(level3LHS, level3RefId, aux2LHS, aux3LHS, init)
660
- this.generateDelayLevel(level2LHS, level2RefId, aux1LHS, aux2LHS, init)
661
- this.generateDelayLevel(level1LHS, level1RefId, input, aux1LHS, init)
662
- // Generate equations for the aux vars using the subs in the generated level var.
663
- this.addVariable(`${aux1LHS} = ${level1LHS} / ${delay3} ~~|`)
664
- this.addVariable(`${aux2LHS} = ${level2LHS} / ${delay3} ~~|`)
665
- this.addVariable(`${aux3LHS} = ${level3LHS} / ${delay3} ~~|`)
666
- // Generate an aux var to hold the delay time expression.
667
- this.var.delayTimeVarName = canonicalName(aux4)
668
- if (isSeparatedVar(this.var)) {
669
- Model.addNonAtoAVar(this.var.delayTimeVarName, [true])
670
- }
671
- this.addVariable(`${aux4LHS} = ${delay3} ~~|`)
672
- // Add a reference to the var, since it won't show up until code gen time.
673
- this.var.references.push(canonicalVensimName(`${aux4}${genSubs}`))
674
- }
675
- }
676
- generateDelayLevel(levelLHS, levelRefId, input, aux, init) {
677
- // Generate a level equation to implement DELAY.
678
- // The parameters are model names. Return the refId of the generated level var.
679
- let eqn = `${levelLHS} = INTEG(${input} - ${aux}, ${init}) ~~|`
680
- this.addVariable(eqn)
681
- // Add a reference to the new level var.
682
- this.refId = levelRefId
683
- this.expandedRefIds = []
684
- this.addReferencesToList(this.var.references)
685
- return levelRefId
686
- }
687
- addVariable(modelEquation) {
688
- let parser = createParser(modelEquation)
689
- let tree = parser.equation()
690
- // Read the var and add it to the Model var table.
691
- let variableReader = new VariableReader()
692
- variableReader.visitEquation(tree)
693
- // Fill in the rest of the var, which may been expanded on a separation dim.
694
- let generatedVars = variableReader.expandedVars.length > 0 ? variableReader.expandedVars : [variableReader.var]
695
- R.forEach(v => {
696
- // Fill in the refId.
697
- v.refId = Model.refIdForVar(v)
698
- // Inhibit output for generated variables.
699
- v.includeInOutput = false
700
- // Finish the variable by parsing the RHS.
701
- let equationReader = new EquationReader(v)
702
- equationReader.read()
703
- }, generatedVars)
704
- }
705
- genSubs(...varNames) {
706
- // Get the subscripts from one or more varnames. Check if they agree.
707
- // This is used to get the subscripts for generated variables.
708
- let result = new Set()
709
- const re = /\[[^\]]+\]/g
710
- for (let varName of varNames) {
711
- let subs = varName.match(re)
712
- if (subs) {
713
- for (let sub of subs) {
714
- result.add(sub.trim())
715
- }
716
- }
717
- }
718
- if (result.size > 1) {
719
- console.error(`ERROR: genSubs subscripts do not agree: ${[...varNames]}`)
720
- }
721
- return [...result][0] || ''
722
- }
723
- }