@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.
@@ -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
+ }