@sdeverywhere/compile 0.7.10 → 0.7.11
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 +2 -1
- package/src/generate/code-gen.js +38 -42
- package/src/generate/direct-data-helpers.js +86 -0
- package/src/generate/equation-gen.js +1 -1
- package/src/generate/expand-var-names.js +115 -0
- package/src/generate/gen-const-list.js +89 -0
- package/src/generate/gen-direct-const.js +91 -0
- package/src/generate/gen-equation.js +273 -0
- package/src/generate/gen-expr.js +851 -0
- package/src/generate/gen-lookup-from-direct.js +105 -0
- package/src/generate/gen-lookup-from-external.js +115 -0
- package/src/generate/gen-lookup-from-points.js +85 -0
- package/src/model/equation-reader.js +27 -22
- package/src/model/model.js +84 -19
- package/src/model/read-equation-fn-delay.js +312 -0
- package/src/model/read-equation-fn-npv.js +67 -0
- package/src/model/read-equation-fn-smooth.js +133 -0
- package/src/model/read-equation-fn-trend.js +44 -0
- package/src/model/read-equation-fn-with-lookup.js +37 -0
- package/src/model/read-equations.js +795 -0
- package/src/model/read-subscripts.js +77 -0
- package/src/model/read-variables.js +264 -0
- package/src/model/reduce-variables.js +166 -0
- package/src/model/variable.js +7 -2
- package/src/parse-and-generate.js +65 -12
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
import * as R from 'ramda'
|
|
2
|
+
|
|
3
|
+
import { parseVensimModel } from '@sdeverywhere/parse'
|
|
4
|
+
|
|
5
|
+
import { canonicalName, cartesianProductOf, newDepreciationVarName, newFixedDelayVarName } from '../_shared/helpers.js'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
extractMarkedDims,
|
|
9
|
+
indexNamesForSubscript,
|
|
10
|
+
isDimension,
|
|
11
|
+
isIndex,
|
|
12
|
+
normalizeSubscripts,
|
|
13
|
+
separatedVariableIndex,
|
|
14
|
+
sub
|
|
15
|
+
} from '../_shared/subscript.js'
|
|
16
|
+
|
|
17
|
+
import Model from './model.js'
|
|
18
|
+
import { generateDelayVariables } from './read-equation-fn-delay.js'
|
|
19
|
+
import { generateNpvVariables } from './read-equation-fn-npv.js'
|
|
20
|
+
import { generateSmoothVariables } from './read-equation-fn-smooth.js'
|
|
21
|
+
import { generateTrendVariables } from './read-equation-fn-trend.js'
|
|
22
|
+
import { generateLookup } from './read-equation-fn-with-lookup.js'
|
|
23
|
+
import { readVariables } from './read-variables.js'
|
|
24
|
+
|
|
25
|
+
class Context {
|
|
26
|
+
constructor(eqnLhs, refId) {
|
|
27
|
+
// The LHS of the equation being processed
|
|
28
|
+
this.eqnLhs = eqnLhs
|
|
29
|
+
|
|
30
|
+
// The refId of the variable being processed
|
|
31
|
+
this.refId = refId
|
|
32
|
+
|
|
33
|
+
// The array of refIds for variables referenced by this variable (needed at `init` time)
|
|
34
|
+
this.referencedInitVars = []
|
|
35
|
+
|
|
36
|
+
// The array of refIds for variables referenced by this variable (needed at `eval` time)
|
|
37
|
+
this.referencedEvalVars = []
|
|
38
|
+
|
|
39
|
+
// The call stack "frames" that are pushed when traversing into function call nodes
|
|
40
|
+
this.callStack = []
|
|
41
|
+
|
|
42
|
+
// Whether the RHS has something other than a constant
|
|
43
|
+
this.rhsNonConst = false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
addVarReference(varRefId) {
|
|
47
|
+
// Determine whether this is an "init" or "eval" reference
|
|
48
|
+
let mode
|
|
49
|
+
if (this.callStack.length > 0) {
|
|
50
|
+
// We are in a function call. The top-level function (the one at the bottom of the call
|
|
51
|
+
// stack) determines the mode.
|
|
52
|
+
mode = this.callStack[0].argMode
|
|
53
|
+
} else {
|
|
54
|
+
// We are not in a function call, so use the normal "eval" mode
|
|
55
|
+
mode = 'eval'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// In Vensim a variable can refer to its current value in the state.
|
|
59
|
+
// Do not add self-references to the lists of references.
|
|
60
|
+
// Do not duplicate references.
|
|
61
|
+
const vars = mode === 'init' ? this.referencedInitVars : this.referencedEvalVars
|
|
62
|
+
if (varRefId !== this.refId && !vars.includes(varRefId)) {
|
|
63
|
+
vars.push(varRefId)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
enterFunctionCall(fnId) {
|
|
68
|
+
const callFrame = {
|
|
69
|
+
fnId: fnId
|
|
70
|
+
}
|
|
71
|
+
this.callStack.push(callFrame)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
exitFunctionCall() {
|
|
75
|
+
this.callStack.pop()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Return the function ID for the parent call. For example, for the following:
|
|
80
|
+
* SMOOTH(x, MAX(y, z))
|
|
81
|
+
* If this is called while evaluating `x`, then this function will return `_SMOOTH`.
|
|
82
|
+
* If this is called while evaluating `y`, then this function will return `_MAX`.
|
|
83
|
+
*/
|
|
84
|
+
getParentFnId() {
|
|
85
|
+
if (this.callStack.length > 1) {
|
|
86
|
+
return this.callStack[this.callStack.length - 2].fnId
|
|
87
|
+
} else {
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Set the index of the arg being evaluated in this call stack frame.
|
|
94
|
+
*
|
|
95
|
+
* @param {number} index The zero-based arg index.
|
|
96
|
+
* @param {'init' | 'eval'} mode Whether this is a normal ('eval') arg position,
|
|
97
|
+
* or an 'init' position (like the second "initial" argument in an INTEG call).
|
|
98
|
+
*/
|
|
99
|
+
setArgIndex(index, mode = 'eval') {
|
|
100
|
+
const frame = this.callStack[this.callStack.length - 1]
|
|
101
|
+
frame.argIndex = index
|
|
102
|
+
frame.argMode = mode
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Define a new variable with the given equation. This will add the variable to the `Model`
|
|
107
|
+
* and then perform the same `readEquation` step that is applied to all other regular variables.
|
|
108
|
+
*
|
|
109
|
+
* @param {*} eqnText The equation in Vensim format.
|
|
110
|
+
*/
|
|
111
|
+
defineVariable(eqnText) {
|
|
112
|
+
// Parse the equation text
|
|
113
|
+
const parsedModel = { kind: 'vensim', root: parseVensimModel(eqnText) }
|
|
114
|
+
|
|
115
|
+
// Create one or more `Variable` instances from the equation
|
|
116
|
+
const vars = readVariables(parsedModel)
|
|
117
|
+
|
|
118
|
+
// Add the variables to the `Model`
|
|
119
|
+
vars.forEach(v => Model.addVariable(v))
|
|
120
|
+
|
|
121
|
+
vars.forEach(v => {
|
|
122
|
+
// Define the refId for the variable
|
|
123
|
+
v.refId = Model.refIdForVar(v)
|
|
124
|
+
|
|
125
|
+
// Process each variable using the same process as above
|
|
126
|
+
readEquation(v)
|
|
127
|
+
|
|
128
|
+
// Inhibit output for generated variables
|
|
129
|
+
v.includeInOutput = false
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extract the subscripts from one or more variable names and check if they "agree".
|
|
135
|
+
*/
|
|
136
|
+
extractSubscriptsFromVarNames(...varNames) {
|
|
137
|
+
// XXX: This is largely copied from the legacy `equation-reader.js`, consider revisiting
|
|
138
|
+
|
|
139
|
+
let result = new Set()
|
|
140
|
+
const re = /\[[^\]]+\]/g
|
|
141
|
+
for (let varName of varNames) {
|
|
142
|
+
let subs = varName.match(re)
|
|
143
|
+
if (subs) {
|
|
144
|
+
for (let sub of subs) {
|
|
145
|
+
result.add(sub.trim())
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (result.size > 1) {
|
|
151
|
+
console.error(`ERROR: Subscripts do not agree in extractSubscriptsFromVarNames: ${[...varNames]}`)
|
|
152
|
+
}
|
|
153
|
+
return [...result][0] || ''
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* TODO: Docs and types
|
|
159
|
+
*
|
|
160
|
+
* @param v {*} The `Variable` instance to process.
|
|
161
|
+
*/
|
|
162
|
+
export function readEquation(v) {
|
|
163
|
+
const eqn = v.parsedEqn
|
|
164
|
+
const context = new Context(eqn?.lhs, v.refId)
|
|
165
|
+
|
|
166
|
+
// Visit the RHS of the equation. If the equation is undefined, it is a synthesized
|
|
167
|
+
// variable (e.g., `Time`), in which case we skip this step.
|
|
168
|
+
if (eqn) {
|
|
169
|
+
const rhs = eqn.rhs
|
|
170
|
+
switch (rhs.kind) {
|
|
171
|
+
case 'expr':
|
|
172
|
+
visitExpr(v, rhs.expr, context)
|
|
173
|
+
break
|
|
174
|
+
case 'lookup':
|
|
175
|
+
visitLookupDef(v, rhs.lookupDef)
|
|
176
|
+
break
|
|
177
|
+
case 'const-list':
|
|
178
|
+
// Nothing to do here currently
|
|
179
|
+
break
|
|
180
|
+
case 'data':
|
|
181
|
+
// TODO: For reasons of compatibility with the legacy reader, the new `readVariables`
|
|
182
|
+
// will set `varType='data'` only when the variable is not expanded. Once we remove
|
|
183
|
+
// the legacy reader, we can fix `readVariables` to unconditionally set `varType='data'`
|
|
184
|
+
// for all variables with 'data' on the RHS. In the meantime, set it here.
|
|
185
|
+
v.varType = 'data'
|
|
186
|
+
break
|
|
187
|
+
default:
|
|
188
|
+
throw new Error(`Unhandled equation kind '${rhs.kind}' for '${v.modelLHS}'`)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Update the variable state
|
|
193
|
+
if (context.referencedInitVars.length > 0) {
|
|
194
|
+
v.initReferences = context.referencedInitVars
|
|
195
|
+
}
|
|
196
|
+
if (context.referencedEvalVars.length > 0) {
|
|
197
|
+
v.references = context.referencedEvalVars
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Refine the variable type based on the contents of the equation
|
|
201
|
+
if (v.points.length > 0) {
|
|
202
|
+
v.varType = 'lookup'
|
|
203
|
+
} else if (v.isAux() && !context.rhsNonConst) {
|
|
204
|
+
v.varType = 'const'
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* TODO: Docs
|
|
210
|
+
*
|
|
211
|
+
* @param {*} v
|
|
212
|
+
* @param {*} expr
|
|
213
|
+
* @param {*} context
|
|
214
|
+
*/
|
|
215
|
+
function visitExpr(v, expr, context) {
|
|
216
|
+
switch (expr.kind) {
|
|
217
|
+
case 'number':
|
|
218
|
+
case 'string':
|
|
219
|
+
case 'keyword':
|
|
220
|
+
break
|
|
221
|
+
|
|
222
|
+
case 'variable-ref':
|
|
223
|
+
visitVariableRef(v, expr, context)
|
|
224
|
+
break
|
|
225
|
+
|
|
226
|
+
case 'unary-op':
|
|
227
|
+
visitExpr(v, expr.expr, context)
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
case 'binary-op':
|
|
231
|
+
visitExpr(v, expr.lhs, context)
|
|
232
|
+
visitExpr(v, expr.rhs, context)
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
case 'parens':
|
|
236
|
+
visitExpr(v, expr.expr, context)
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
case 'lookup-def':
|
|
240
|
+
// TODO: Lookup defs in expression position should only occur in the case of `WITH LOOKUP`
|
|
241
|
+
// function calls, and those are transformed into a generated lookup variable, so there's
|
|
242
|
+
// nothing else we need to do here, but it would be good to add a check that the current
|
|
243
|
+
// function is `WITH LOOKUP` (if it is, ignore, but if it is not, throw an error).
|
|
244
|
+
break
|
|
245
|
+
|
|
246
|
+
case 'lookup-call':
|
|
247
|
+
visitLookupCall(v, expr, context)
|
|
248
|
+
break
|
|
249
|
+
|
|
250
|
+
case 'function-call':
|
|
251
|
+
visitFunctionCall(v, expr, context)
|
|
252
|
+
break
|
|
253
|
+
|
|
254
|
+
default:
|
|
255
|
+
throw new Error(`Unhandled expression kind '${expr.kind}' when reading '${v.modelLHS}'`)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* TODO: Docs
|
|
261
|
+
*
|
|
262
|
+
* @param {*} v
|
|
263
|
+
* @param {*} varRefExpr
|
|
264
|
+
* @param {*} context
|
|
265
|
+
*/
|
|
266
|
+
function visitVariableRef(v, varRefExpr, context) {
|
|
267
|
+
// Mark the RHS as non-constant, since it has a variable reference
|
|
268
|
+
context.rhsNonConst = true
|
|
269
|
+
|
|
270
|
+
if (isDimension(varRefExpr.varId) || isIndex(varRefExpr.varId)) {
|
|
271
|
+
// It is possible for a dimension or subscript/index name to be used where a variable
|
|
272
|
+
// would normally be. Here is an example taken from the "extdata" sample model:
|
|
273
|
+
// Chosen C = 1 ~~|
|
|
274
|
+
// C Selection[DimC] = IF THEN ELSE ( DimC = Chosen C , 1 , 0 ) ~~|
|
|
275
|
+
// If we detect a dimension or subscript/index, don't add it as a normal variable reference.
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Determine whether to add references to specific refIds (in the case of separated
|
|
280
|
+
// non-apply-to-all variables) or just a single base refId (in the case of non-subscripted
|
|
281
|
+
// or apply-to-all variables)
|
|
282
|
+
const baseRefId = varRefExpr.varId
|
|
283
|
+
const subIds = varRefExpr.subscriptRefs?.map(subRef => subRef.subId) || []
|
|
284
|
+
const expandedRefIds = expandedRefIdsForVar(v, baseRefId, subIds)
|
|
285
|
+
if (expandedRefIds.length > 0) {
|
|
286
|
+
// Add a reference to each instance of the non-apply-to-all variable
|
|
287
|
+
expandedRefIds.forEach(refId => context.addVarReference(refId))
|
|
288
|
+
} else {
|
|
289
|
+
// Add the single variable refId to the list of referenced variables
|
|
290
|
+
context.addVarReference(baseRefId)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* TODO: Docs
|
|
296
|
+
*
|
|
297
|
+
* @param {*} v
|
|
298
|
+
* @param {*} def
|
|
299
|
+
* @param {*} context
|
|
300
|
+
*/
|
|
301
|
+
function visitLookupDef(v, def) {
|
|
302
|
+
// Save the lookup range and points to the variable
|
|
303
|
+
if (def.range) {
|
|
304
|
+
v.range = [def.range.min, def.range.max]
|
|
305
|
+
}
|
|
306
|
+
v.points = def.points
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* TODO: Docs
|
|
311
|
+
*
|
|
312
|
+
* @param {*} v
|
|
313
|
+
* @param {*} callExpr
|
|
314
|
+
* @param {*} context
|
|
315
|
+
*/
|
|
316
|
+
function visitLookupCall(v, callExpr, context) {
|
|
317
|
+
// Mark the RHS as non-constant, since it has a lookup call
|
|
318
|
+
context.rhsNonConst = true
|
|
319
|
+
|
|
320
|
+
// Add a reference to the lookup variable
|
|
321
|
+
const lookupVarName = callExpr.varRef.varId
|
|
322
|
+
if (v.referencedLookupVarNames) {
|
|
323
|
+
v.referencedLookupVarNames.push(lookupVarName)
|
|
324
|
+
} else {
|
|
325
|
+
v.referencedLookupVarNames = [lookupVarName]
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Visit the single argument
|
|
329
|
+
visitExpr(v, callExpr.arg, context)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* TODO: Docs
|
|
334
|
+
*
|
|
335
|
+
* @param {*} v
|
|
336
|
+
* @param {*} callExpr
|
|
337
|
+
* @param {*} context
|
|
338
|
+
*/
|
|
339
|
+
function visitFunctionCall(v, callExpr, context) {
|
|
340
|
+
// Mark the RHS as non-constant, since it has a function call
|
|
341
|
+
context.rhsNonConst = true
|
|
342
|
+
|
|
343
|
+
// Enter this function call
|
|
344
|
+
context.enterFunctionCall(callExpr.fnId)
|
|
345
|
+
|
|
346
|
+
// By default, we will add this function to the list of functions that are referenced
|
|
347
|
+
// by the LHS variable, but we will skip this step for function calls like `DELAY` that
|
|
348
|
+
// are reimplemented in terms of other equations
|
|
349
|
+
let addFnReference = true
|
|
350
|
+
|
|
351
|
+
// By default, all arguments are assumed to be used at eval time, but certain functions
|
|
352
|
+
// will override this and mark specific argument positions as being used at init time
|
|
353
|
+
let argModes = Array(callExpr.args.length).fill('eval')
|
|
354
|
+
|
|
355
|
+
// By default, we will visit all arguments, but for certain functions like `DELAY` that
|
|
356
|
+
// are reimplemented in terms of other equations, we will skip visiting the arguments
|
|
357
|
+
// (they will be visited when processing the replacement equations)
|
|
358
|
+
let visitArgs = true
|
|
359
|
+
|
|
360
|
+
switch (callExpr.fnId) {
|
|
361
|
+
//
|
|
362
|
+
//
|
|
363
|
+
// 1-argument functions...
|
|
364
|
+
//
|
|
365
|
+
//
|
|
366
|
+
|
|
367
|
+
case '_ABS':
|
|
368
|
+
case '_ARCCOS':
|
|
369
|
+
case '_ARCSIN':
|
|
370
|
+
case '_ARCTAN':
|
|
371
|
+
case '_COS':
|
|
372
|
+
case '_ELMCOUNT':
|
|
373
|
+
case '_EXP':
|
|
374
|
+
case '_GAMMA_LN':
|
|
375
|
+
case '_INTEGER':
|
|
376
|
+
case '_LN':
|
|
377
|
+
case '_SIN':
|
|
378
|
+
case '_SQRT':
|
|
379
|
+
case '_SUM':
|
|
380
|
+
case '_TAN':
|
|
381
|
+
case '_VMAX':
|
|
382
|
+
case '_VMIN':
|
|
383
|
+
validateCallArgs(callExpr, 1)
|
|
384
|
+
break
|
|
385
|
+
|
|
386
|
+
// TODO: We do not currently have full support for the GAME function, so report a warning for now
|
|
387
|
+
case '_GAME':
|
|
388
|
+
if (process.env.SDE_REPORT_UNSUPPORTED_FUNCTIONS !== '0') {
|
|
389
|
+
console.warn(
|
|
390
|
+
`WARNING: The GAME function (used in the definition of '${v.modelLHS}') is currently implemented as a no-op (it returns the input value).`
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
validateCallArgs(callExpr, 1)
|
|
394
|
+
break
|
|
395
|
+
|
|
396
|
+
//
|
|
397
|
+
//
|
|
398
|
+
// 2-argument functions...
|
|
399
|
+
//
|
|
400
|
+
//
|
|
401
|
+
|
|
402
|
+
case '_LOOKUP_BACKWARD':
|
|
403
|
+
case '_LOOKUP_FORWARD':
|
|
404
|
+
case '_LOOKUP_INVERT':
|
|
405
|
+
case '_MAX':
|
|
406
|
+
case '_MIN':
|
|
407
|
+
case '_MODULO':
|
|
408
|
+
case '_POW':
|
|
409
|
+
case '_POWER':
|
|
410
|
+
case '_PULSE':
|
|
411
|
+
case '_QUANTUM':
|
|
412
|
+
case '_STEP':
|
|
413
|
+
case '_VECTOR_ELM_MAP':
|
|
414
|
+
case '_VECTOR_SORT_ORDER':
|
|
415
|
+
case '_ZIDZ':
|
|
416
|
+
validateCallArgs(callExpr, 2)
|
|
417
|
+
break
|
|
418
|
+
|
|
419
|
+
//
|
|
420
|
+
//
|
|
421
|
+
// 3-plus-argument functions...
|
|
422
|
+
//
|
|
423
|
+
//
|
|
424
|
+
|
|
425
|
+
case '_GET_DATA_BETWEEN_TIMES':
|
|
426
|
+
case '_RAMP':
|
|
427
|
+
case '_XIDZ':
|
|
428
|
+
validateCallArgs(callExpr, 3)
|
|
429
|
+
break
|
|
430
|
+
|
|
431
|
+
case '_PULSE_TRAIN':
|
|
432
|
+
validateCallArgs(callExpr, 4)
|
|
433
|
+
break
|
|
434
|
+
|
|
435
|
+
case '_VECTOR_SELECT':
|
|
436
|
+
validateCallArgs(callExpr, 5)
|
|
437
|
+
break
|
|
438
|
+
|
|
439
|
+
//
|
|
440
|
+
//
|
|
441
|
+
// Complex functions...
|
|
442
|
+
//
|
|
443
|
+
//
|
|
444
|
+
|
|
445
|
+
case '_ACTIVE_INITIAL':
|
|
446
|
+
validateCallDepth(callExpr, context)
|
|
447
|
+
validateCallArgs(callExpr, 2)
|
|
448
|
+
v.hasInitValue = true
|
|
449
|
+
// The 2nd argument is used at init time
|
|
450
|
+
argModes[1] = 'init'
|
|
451
|
+
break
|
|
452
|
+
|
|
453
|
+
case '_ALLOCATE_AVAILABLE':
|
|
454
|
+
validateCallDepth(callExpr, context)
|
|
455
|
+
validateCallArgs(callExpr, 3)
|
|
456
|
+
break
|
|
457
|
+
|
|
458
|
+
case '_DELAY1':
|
|
459
|
+
case '_DELAY1I':
|
|
460
|
+
case '_DELAY3':
|
|
461
|
+
case '_DELAY3I':
|
|
462
|
+
validateCallArgs(callExpr, callExpr.fnId.endsWith('I') ? 3 : 2)
|
|
463
|
+
addFnReference = false
|
|
464
|
+
visitArgs = false
|
|
465
|
+
generateDelayVariables(v, callExpr, context)
|
|
466
|
+
break
|
|
467
|
+
|
|
468
|
+
case '_DELAY_FIXED':
|
|
469
|
+
validateCallDepth(callExpr, context)
|
|
470
|
+
validateCallArgs(callExpr, 3)
|
|
471
|
+
v.varType = 'level'
|
|
472
|
+
v.varSubtype = 'fixedDelay'
|
|
473
|
+
v.hasInitValue = true
|
|
474
|
+
v.fixedDelayVarName = canonicalName(newFixedDelayVarName())
|
|
475
|
+
// The 2nd and 3rd arguments are used at init time
|
|
476
|
+
argModes[1] = 'init'
|
|
477
|
+
argModes[2] = 'init'
|
|
478
|
+
break
|
|
479
|
+
|
|
480
|
+
case '_DEPRECIATE_STRAIGHTLINE':
|
|
481
|
+
validateCallDepth(callExpr, context)
|
|
482
|
+
validateCallArgs(callExpr, 4)
|
|
483
|
+
v.varSubtype = 'depreciation'
|
|
484
|
+
v.hasInitValue = true
|
|
485
|
+
v.depreciationVarName = canonicalName(newDepreciationVarName())
|
|
486
|
+
// The 2nd and 3rd arguments are used at init time
|
|
487
|
+
// TODO: The 3rd (fisc) argument is not currently supported
|
|
488
|
+
// TODO: Shouldn't the last (init) argument be marked as 'init' here? (It's
|
|
489
|
+
// not treated as 'init' in the legacy reader.)
|
|
490
|
+
argModes[1] = 'init'
|
|
491
|
+
argModes[2] = 'init'
|
|
492
|
+
break
|
|
493
|
+
|
|
494
|
+
case '_GET_DIRECT_CONSTANTS': {
|
|
495
|
+
validateCallDepth(callExpr, context)
|
|
496
|
+
validateCallArgs(callExpr, 3)
|
|
497
|
+
validateCallArgType(callExpr, 0, 'string')
|
|
498
|
+
validateCallArgType(callExpr, 1, 'string')
|
|
499
|
+
validateCallArgType(callExpr, 2, 'string')
|
|
500
|
+
addFnReference = false
|
|
501
|
+
v.varType = 'const'
|
|
502
|
+
v.directConstArgs = {
|
|
503
|
+
file: callExpr.args[0].text,
|
|
504
|
+
tab: callExpr.args[1].text,
|
|
505
|
+
startCell: callExpr.args[2].text
|
|
506
|
+
}
|
|
507
|
+
break
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
case '_GET_DIRECT_DATA':
|
|
511
|
+
case '_GET_DIRECT_LOOKUPS':
|
|
512
|
+
validateCallDepth(callExpr, context)
|
|
513
|
+
validateCallArgs(callExpr, 4)
|
|
514
|
+
validateCallArgType(callExpr, 0, 'string')
|
|
515
|
+
validateCallArgType(callExpr, 1, 'string')
|
|
516
|
+
validateCallArgType(callExpr, 2, 'string')
|
|
517
|
+
validateCallArgType(callExpr, 3, 'string')
|
|
518
|
+
addFnReference = false
|
|
519
|
+
v.varType = 'data'
|
|
520
|
+
v.directDataArgs = {
|
|
521
|
+
file: callExpr.args[0].text,
|
|
522
|
+
tab: callExpr.args[1].text,
|
|
523
|
+
timeRowOrCol: callExpr.args[2].text,
|
|
524
|
+
startCell: callExpr.args[3].text
|
|
525
|
+
}
|
|
526
|
+
break
|
|
527
|
+
|
|
528
|
+
case '_IF_THEN_ELSE':
|
|
529
|
+
validateCallArgs(callExpr, 3)
|
|
530
|
+
addFnReference = false
|
|
531
|
+
break
|
|
532
|
+
|
|
533
|
+
case '_INITIAL':
|
|
534
|
+
validateCallDepth(callExpr, context)
|
|
535
|
+
validateCallArgs(callExpr, 1)
|
|
536
|
+
v.varType = 'initial'
|
|
537
|
+
v.hasInitValue = true
|
|
538
|
+
// The single argument is used at init time
|
|
539
|
+
argModes[0] = 'init'
|
|
540
|
+
break
|
|
541
|
+
|
|
542
|
+
case '_INTEG':
|
|
543
|
+
validateCallDepth(callExpr, context)
|
|
544
|
+
validateCallArgs(callExpr, 2)
|
|
545
|
+
v.varType = 'level'
|
|
546
|
+
v.hasInitValue = true
|
|
547
|
+
// The 2nd argument is used at init time
|
|
548
|
+
argModes[1] = 'init'
|
|
549
|
+
break
|
|
550
|
+
|
|
551
|
+
case '_NPV':
|
|
552
|
+
validateCallArgs(callExpr, 4)
|
|
553
|
+
addFnReference = false
|
|
554
|
+
visitArgs = false
|
|
555
|
+
generateNpvVariables(v, callExpr, context)
|
|
556
|
+
break
|
|
557
|
+
|
|
558
|
+
case '_SAMPLE_IF_TRUE':
|
|
559
|
+
validateCallDepth(callExpr, context)
|
|
560
|
+
validateCallArgs(callExpr, 3)
|
|
561
|
+
v.hasInitValue = true
|
|
562
|
+
// The 3rd argument is used at init time
|
|
563
|
+
argModes[2] = 'init'
|
|
564
|
+
break
|
|
565
|
+
|
|
566
|
+
case '_SMOOTH':
|
|
567
|
+
case '_SMOOTHI':
|
|
568
|
+
case '_SMOOTH3':
|
|
569
|
+
case '_SMOOTH3I':
|
|
570
|
+
validateCallArgs(callExpr, callExpr.fnId.endsWith('I') ? 3 : 2)
|
|
571
|
+
addFnReference = false
|
|
572
|
+
visitArgs = false
|
|
573
|
+
generateSmoothVariables(v, callExpr, context)
|
|
574
|
+
break
|
|
575
|
+
|
|
576
|
+
case '_TREND':
|
|
577
|
+
validateCallArgs(callExpr, 3)
|
|
578
|
+
addFnReference = false
|
|
579
|
+
visitArgs = false
|
|
580
|
+
generateTrendVariables(v, callExpr, context)
|
|
581
|
+
break
|
|
582
|
+
|
|
583
|
+
case '_WITH_LOOKUP':
|
|
584
|
+
validateCallDepth(callExpr, context)
|
|
585
|
+
validateCallArgs(callExpr, 2)
|
|
586
|
+
generateLookup(v, callExpr, context)
|
|
587
|
+
break
|
|
588
|
+
|
|
589
|
+
default: {
|
|
590
|
+
// See if the function name is actually the name of a lookup variable. For Vensim
|
|
591
|
+
// models, the antlr4-vensim grammar has separate definitions for lookup calls and
|
|
592
|
+
// function calls, but in practice they can only be differentiated in the case
|
|
593
|
+
// where the lookup has subscripts; when there are no subscripts, they get treated
|
|
594
|
+
// like normal function calls, and in that case we will end up here. If we find
|
|
595
|
+
// a variable with the given name, then we will assume it's a lookup call, otherwise
|
|
596
|
+
// we treat it as a call of an unimplemented function.
|
|
597
|
+
const varId = callExpr.fnId.toLowerCase()
|
|
598
|
+
const referencedVar = Model.varWithName(varId)
|
|
599
|
+
if (referencedVar === undefined || referencedVar.parsedEqn.rhs.kind !== 'lookup') {
|
|
600
|
+
// Throw an error if the function is not yet implemented in SDE
|
|
601
|
+
// TODO: This will report false positives in the case of user-defined macros. For now
|
|
602
|
+
// we provide the ability to turn off this check via an environment variable, but we
|
|
603
|
+
// should consider providing a way for the user to declare the names of any user-defined
|
|
604
|
+
// macros so that we can skip this check when those macros are detected.
|
|
605
|
+
if (process.env.SDE_REPORT_UNSUPPORTED_FUNCTIONS !== '0') {
|
|
606
|
+
const msg = `Unhandled function '${callExpr.fnId}' in readEquations for '${v.modelLHS}'`
|
|
607
|
+
if (process.env.SDE_REPORT_UNSUPPORTED_FUNCTIONS === 'warn') {
|
|
608
|
+
console.warn(`WARNING: ${msg}`)
|
|
609
|
+
} else {
|
|
610
|
+
throw new Error(msg)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
break
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (addFnReference) {
|
|
619
|
+
// Keep track of all function names referenced in this equation. Note that lookup
|
|
620
|
+
// variables are sometimes function-like, so they will be included here. This will be
|
|
621
|
+
// used later to decide whether a lookup variable needs to be included in generated code.
|
|
622
|
+
// TODO: The legacy `EquationReader` used `canonicalName` redundantly, which caused an
|
|
623
|
+
// extra leading underscore. We will do the same here for compatibility but this should
|
|
624
|
+
// be fixed after the legacy reader is removed.
|
|
625
|
+
const fnId = `_${callExpr.fnId.toLowerCase()}`
|
|
626
|
+
if (v.referencedFunctionNames) {
|
|
627
|
+
if (!v.referencedFunctionNames.includes(fnId)) {
|
|
628
|
+
v.referencedFunctionNames.push(fnId)
|
|
629
|
+
}
|
|
630
|
+
} else {
|
|
631
|
+
v.referencedFunctionNames = [fnId]
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (visitArgs) {
|
|
636
|
+
// Visit each argument
|
|
637
|
+
for (const [index, argExpr] of callExpr.args.entries()) {
|
|
638
|
+
if (callExpr.fnId === '_WITH_LOOKUP' && index > 1) {
|
|
639
|
+
// XXX: For `WITH LOOKUP` calls, only process the first argument; need to generalize this
|
|
640
|
+
break
|
|
641
|
+
} else if (callExpr.fnId === '_ALLOCATE_AVAILABLE' && index === 1) {
|
|
642
|
+
// XXX: Handle `ALLOCATE AVAILABLE` calls specially for now. This logic is copied from the
|
|
643
|
+
// legacy reader, but we may want to revisit later.
|
|
644
|
+
// Reference the second and third elements of the priority profile argument instead of the
|
|
645
|
+
// first one that Vensim requires for ALLOCATE AVAILABLE. This is required to pick up
|
|
646
|
+
// correct dependencies.
|
|
647
|
+
if (argExpr.kind !== 'variable-ref') {
|
|
648
|
+
throw new Error(`ALLOCATE AVAILABLE argument 'pp' must be a variable reference`)
|
|
649
|
+
}
|
|
650
|
+
const baseRefId = argExpr.varId
|
|
651
|
+
const subIds = argExpr.subscriptRefs?.map(subRef => subRef.subId) || []
|
|
652
|
+
const expandedRefIds = expandedRefIdsForVar(v, baseRefId, subIds)
|
|
653
|
+
const ptypeRefId = expandedRefIds[0]
|
|
654
|
+
const { subscripts } = Model.splitRefId(ptypeRefId)
|
|
655
|
+
const ptypeIndexName = subscripts[1]
|
|
656
|
+
const profileElementsDimName = sub(ptypeIndexName).family
|
|
657
|
+
const profileElementsDim = sub(profileElementsDimName)
|
|
658
|
+
const priorityRefId = ptypeRefId.replace(ptypeIndexName, profileElementsDim.value[1])
|
|
659
|
+
const widthRefId = ptypeRefId.replace(ptypeIndexName, profileElementsDim.value[2])
|
|
660
|
+
context.addVarReference(priorityRefId)
|
|
661
|
+
context.addVarReference(widthRefId)
|
|
662
|
+
continue
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
context.setArgIndex(index, argModes[index])
|
|
666
|
+
visitExpr(v, argExpr, context)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Exit this function call
|
|
671
|
+
context.exitFunctionCall()
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Throw an error if the given function call does not appear at the top level of an equation RHS
|
|
676
|
+
* (i.e., right after the equals sign).
|
|
677
|
+
*/
|
|
678
|
+
function validateCallDepth(callExpr, context) {
|
|
679
|
+
if (context.callStack.length !== 1) {
|
|
680
|
+
throw new Error(
|
|
681
|
+
`Function '${callExpr.fnName}' cannot be used inside other function calls (it must appear directly after the '=' sign in an equation)`
|
|
682
|
+
)
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Throw an error if the given function call does not have the expected number of arguments.
|
|
688
|
+
*/
|
|
689
|
+
function validateCallArgs(callExpr, expectedArgCount) {
|
|
690
|
+
if (callExpr.args.length !== expectedArgCount) {
|
|
691
|
+
throw new Error(
|
|
692
|
+
`Expected '${callExpr.fnName}' function call to have ${expectedArgCount} arguments but got ${callExpr.args.length} `
|
|
693
|
+
)
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Throw an error if the function call argument at the given index does not have the expected type.
|
|
699
|
+
*/
|
|
700
|
+
function validateCallArgType(callExpr, index, expectedKind) {
|
|
701
|
+
const argKind = callExpr.args[index].kind
|
|
702
|
+
if (argKind !== expectedKind) {
|
|
703
|
+
throw new Error(
|
|
704
|
+
`Expected '${callExpr.fnName}' function call argument at index ${index} to be of type ${expectedKind} arguments but got ${argKind} `
|
|
705
|
+
)
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* When an equation references a non-apply-to-all array, add its subscripts to the array
|
|
711
|
+
* var's refId.
|
|
712
|
+
*
|
|
713
|
+
* XXX: This is largely copied from the legacy `equation-reader.js` and modified to work
|
|
714
|
+
* with the AST instead of directly depending on antlr4-vensim constructs. This is pretty
|
|
715
|
+
* complex so we should try to refactor or at least add some more fine-grained unit tests
|
|
716
|
+
* for it.
|
|
717
|
+
*/
|
|
718
|
+
function expandedRefIdsForVar(lhsVariable, baseRefId, subscripts) {
|
|
719
|
+
// Remove dimension subscripts marked with ! and save them for later.
|
|
720
|
+
let markedDims = extractMarkedDims(subscripts)
|
|
721
|
+
subscripts = normalizeSubscripts(subscripts)
|
|
722
|
+
// console.error(`${this.var.refId} → ${this.refId} [ ${subscripts} ]`);
|
|
723
|
+
|
|
724
|
+
if (subscripts.length === 0) {
|
|
725
|
+
return []
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// See if this variable is non-apply-to-all. At this point, the refId is just the var name.
|
|
729
|
+
// References to apply-to-all variables do not need subscripts since they refer to the whole array.
|
|
730
|
+
let expansionFlags = Model.expansionFlags(baseRefId)
|
|
731
|
+
if (!expansionFlags) {
|
|
732
|
+
// The reference is to a non-subscripted or apply-to-all variable
|
|
733
|
+
return []
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// The reference is to a non-apply-to-all variable.
|
|
737
|
+
// Find the refIds of the vars that include the indices in the reference.
|
|
738
|
+
// Get the vars with the var name of the reference. We will choose from these vars.
|
|
739
|
+
let varsWithRefName = Model.varsWithName(baseRefId)
|
|
740
|
+
|
|
741
|
+
// The refIds of actual vars containing the indices will accumulate with possible duplicates.
|
|
742
|
+
let expandedRefIds = []
|
|
743
|
+
let iSub
|
|
744
|
+
|
|
745
|
+
// Accumulate an array of lists of the separated index names at each position.
|
|
746
|
+
let indexNames = []
|
|
747
|
+
for (iSub = 0; iSub < expansionFlags.length; iSub++) {
|
|
748
|
+
if (expansionFlags[iSub]) {
|
|
749
|
+
// For each index name at the subscript position, find refIds for vars that include the index.
|
|
750
|
+
// This process ensures that we generate references to vars that are in the var table.
|
|
751
|
+
let indexNamesAtPos
|
|
752
|
+
// Use the single index name for a separated variable if it exists.
|
|
753
|
+
// But don't do this if the subscript is a marked dimension in a vector function.
|
|
754
|
+
let separatedIndexName = separatedVariableIndex(subscripts[iSub], lhsVariable, subscripts)
|
|
755
|
+
if (!markedDims.includes(subscripts[iSub]) && separatedIndexName) {
|
|
756
|
+
indexNamesAtPos = [separatedIndexName]
|
|
757
|
+
} else {
|
|
758
|
+
// Generate references to all the indices for the subscript.
|
|
759
|
+
indexNamesAtPos = indexNamesForSubscript(subscripts[iSub])
|
|
760
|
+
}
|
|
761
|
+
indexNames.push(indexNamesAtPos)
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Flatten the arrays of index names at each position into an array of index name combinations.
|
|
766
|
+
let separatedIndices = cartesianProductOf(indexNames)
|
|
767
|
+
// Find a separated variable for each combination of indices.
|
|
768
|
+
for (let separatedIndex of separatedIndices) {
|
|
769
|
+
// Consider each var with the same name as the reference in the equation.
|
|
770
|
+
for (let refVar of varsWithRefName) {
|
|
771
|
+
let iSeparatedIndex = 0
|
|
772
|
+
for (iSub = 0; iSub < expansionFlags.length; iSub++) {
|
|
773
|
+
if (expansionFlags[iSub]) {
|
|
774
|
+
let refVarIndexNames = indexNamesForSubscript(refVar.subscripts[iSub])
|
|
775
|
+
if (refVarIndexNames.length === 0) {
|
|
776
|
+
console.error(
|
|
777
|
+
`ERROR: no subscript at subscript position ${iSub} for var ${refVar.refId} with subscripts ${refVar.subscripts}`
|
|
778
|
+
)
|
|
779
|
+
}
|
|
780
|
+
if (!refVarIndexNames.includes(separatedIndex[iSeparatedIndex++])) {
|
|
781
|
+
break
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (iSub >= expansionFlags.length) {
|
|
786
|
+
// All separated index names matched index names in the var, so add it as a reference.
|
|
787
|
+
expandedRefIds.push(refVar.refId)
|
|
788
|
+
break
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Sort the expandedRefIds and eliminate duplicates.
|
|
794
|
+
return R.uniq(expandedRefIds.sort())
|
|
795
|
+
}
|