@sdeverywhere/compile 0.7.17 → 0.7.19
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.
- package/package.json +1 -3
- package/src/generate/expand-var-names.js +0 -10
- package/src/generate/{code-gen.js → gen-code-c.js} +188 -89
- package/src/generate/gen-code-js.js +616 -0
- package/src/generate/gen-code.js +36 -0
- package/src/generate/gen-equation.js +16 -6
- package/src/generate/gen-expr.js +205 -48
- package/src/generate/gen-lookup-from-direct.js +14 -6
- package/src/generate/gen-lookup-from-external.js +20 -5
- package/src/generate/gen-lookup-from-points.js +28 -6
- package/src/index.js +38 -3
- package/src/model/model.js +169 -170
- package/src/model/read-equation-fn-game.js +50 -0
- package/src/model/read-equations.js +9 -10
- package/src/model/read-variables.js +2 -2
- package/src/model/variable.js +6 -21
- package/src/parse-and-generate.js +22 -19
- package/src/preprocess/preprocessor.js +2 -2
- package/src/generate/equation-gen.js +0 -1268
- package/src/generate/model-lhs-reader.js +0 -88
- package/src/model/equation-reader.js +0 -723
- package/src/model/expr-reader.js +0 -202
- package/src/model/subscript-range-reader.js +0 -143
- package/src/model/var-name-reader.js +0 -40
- package/src/model/variable-reader.js +0 -172
- package/src/parse/model-reader.js +0 -141
- package/src/parse/parser.js +0 -36
|
@@ -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
|
-
}
|