@sdeverywhere/compile 0.7.27 → 0.7.29
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 -2
- package/src/_shared/helpers.js +2 -2
- package/src/generate/direct-data-helpers.js +18 -6
- package/src/generate/gen-code.js +3 -2
- package/src/generate/gen-direct-const.js +5 -0
- package/src/generate/gen-equation.js +6 -0
- package/src/generate/gen-expr.js +91 -37
- package/src/generate/gen-lookup-from-direct.js +14 -7
- package/src/index.js +9 -1
- package/src/model/model.js +79 -5
- package/src/model/read-equation-fn-delay.js +6 -4
- package/src/model/read-equation-fn-smooth.js +7 -5
- package/src/model/read-equation-fn-trend.js +1 -1
- package/src/model/read-equations-expand.js +72 -0
- package/src/model/read-equations.js +572 -282
- package/src/model/read-variables.js +7 -1
- package/src/parse-and-generate.js +39 -27
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { parseVensimModel } from '@sdeverywhere/parse'
|
|
2
2
|
|
|
3
|
-
import { canonicalName,
|
|
3
|
+
import { canonicalName, newDepreciationVarName, newFixedDelayVarName } from '../_shared/helpers.js'
|
|
4
4
|
|
|
5
|
-
import { hasMapping,
|
|
5
|
+
import { hasMapping, isDimension, isIndex, sub } from '../_shared/subscript.js'
|
|
6
6
|
|
|
7
7
|
import Model from './model.js'
|
|
8
8
|
import { generateDelayVariables } from './read-equation-fn-delay.js'
|
|
@@ -11,10 +11,14 @@ import { generateNpvVariables } from './read-equation-fn-npv.js'
|
|
|
11
11
|
import { generateSmoothVariables } from './read-equation-fn-smooth.js'
|
|
12
12
|
import { generateTrendVariables } from './read-equation-fn-trend.js'
|
|
13
13
|
import { generateLookup } from './read-equation-fn-with-lookup.js'
|
|
14
|
+
import { matchingRhsRefIds } from './read-equations-expand.js'
|
|
14
15
|
import { readVariables } from './read-variables.js'
|
|
15
16
|
|
|
16
17
|
class Context {
|
|
17
|
-
constructor(eqnLhs, refId) {
|
|
18
|
+
constructor(modelKind, eqnLhs, refId) {
|
|
19
|
+
// The kind of model being read, either 'vensim' or 'xmile'
|
|
20
|
+
this.modelKind = modelKind
|
|
21
|
+
|
|
18
22
|
// The LHS of the equation being processed
|
|
19
23
|
this.eqnLhs = eqnLhs
|
|
20
24
|
|
|
@@ -109,7 +113,7 @@ class Context {
|
|
|
109
113
|
* @param {string[]} eqnStrings An array of individual equation strings in Vensim format.
|
|
110
114
|
*/
|
|
111
115
|
defineVariables(eqnStrings) {
|
|
112
|
-
// Parse the equation text
|
|
116
|
+
// Parse the equation text, which is assumed to be in Vensim format
|
|
113
117
|
const eqnText = eqnStrings.join('\n')
|
|
114
118
|
const parsedModel = { kind: 'vensim', root: parseVensimModel(eqnText) }
|
|
115
119
|
|
|
@@ -131,7 +135,7 @@ class Context {
|
|
|
131
135
|
|
|
132
136
|
vars.forEach(v => {
|
|
133
137
|
// Process each variable using the same process as above
|
|
134
|
-
readEquation(v)
|
|
138
|
+
readEquation(v, 'vensim')
|
|
135
139
|
|
|
136
140
|
// Inhibit output for generated variables
|
|
137
141
|
v.includeInOutput = false
|
|
@@ -168,10 +172,11 @@ class Context {
|
|
|
168
172
|
* TODO: Docs and types
|
|
169
173
|
*
|
|
170
174
|
* @param v {*} The `Variable` instance to process.
|
|
175
|
+
* @param {string} modelKind The kind of model being read, either 'vensim' or 'xmile'.
|
|
171
176
|
*/
|
|
172
|
-
export function readEquation(v) {
|
|
177
|
+
export function readEquation(v, modelKind) {
|
|
173
178
|
const eqn = v.parsedEqn
|
|
174
|
-
const context = new Context(eqn?.lhs, v.refId)
|
|
179
|
+
const context = new Context(modelKind, eqn?.lhs, v.refId)
|
|
175
180
|
|
|
176
181
|
// Visit the RHS of the equation. If the equation is undefined, it is a synthesized
|
|
177
182
|
// variable (e.g., `Time`), in which case we skip this step.
|
|
@@ -383,257 +388,434 @@ function visitFunctionCall(v, callExpr, context) {
|
|
|
383
388
|
// (they will be visited when processing the replacement equations)
|
|
384
389
|
let visitArgs = true
|
|
385
390
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
391
|
+
// If the `unhandled` flag is set, it means we did not match a known function
|
|
392
|
+
let unhandled = false
|
|
393
|
+
|
|
394
|
+
// Helper function that validates a function call for a Vensim model
|
|
395
|
+
function validateVensimFunctionCall() {
|
|
396
|
+
switch (callExpr.fnId) {
|
|
397
|
+
//
|
|
398
|
+
//
|
|
399
|
+
// 1-argument functions...
|
|
400
|
+
//
|
|
401
|
+
//
|
|
402
|
+
|
|
403
|
+
case '_ABS':
|
|
404
|
+
case '_ARCCOS':
|
|
405
|
+
case '_ARCSIN':
|
|
406
|
+
case '_ARCTAN':
|
|
407
|
+
case '_COS':
|
|
408
|
+
case '_ELMCOUNT':
|
|
409
|
+
case '_EXP':
|
|
410
|
+
case '_GAMMA_LN':
|
|
411
|
+
case '_INTEGER':
|
|
412
|
+
case '_LN':
|
|
413
|
+
case '_SIN':
|
|
414
|
+
case '_SQRT':
|
|
415
|
+
case '_SUM':
|
|
416
|
+
case '_TAN':
|
|
417
|
+
case '_VMAX':
|
|
418
|
+
case '_VMIN':
|
|
419
|
+
validateCallArgs(callExpr, 1)
|
|
420
|
+
break
|
|
411
421
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
422
|
+
//
|
|
423
|
+
//
|
|
424
|
+
// 2-argument functions...
|
|
425
|
+
//
|
|
426
|
+
//
|
|
427
|
+
|
|
428
|
+
case '_LOOKUP_BACKWARD':
|
|
429
|
+
case '_LOOKUP_FORWARD':
|
|
430
|
+
case '_LOOKUP_INVERT':
|
|
431
|
+
case '_MAX':
|
|
432
|
+
case '_MIN':
|
|
433
|
+
case '_MODULO':
|
|
434
|
+
case '_POW':
|
|
435
|
+
case '_POWER':
|
|
436
|
+
case '_PULSE':
|
|
437
|
+
case '_QUANTUM':
|
|
438
|
+
case '_STEP':
|
|
439
|
+
case '_VECTOR_ELM_MAP':
|
|
440
|
+
case '_VECTOR_SORT_ORDER':
|
|
441
|
+
case '_ZIDZ':
|
|
442
|
+
validateCallArgs(callExpr, 2)
|
|
443
|
+
break
|
|
434
444
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
445
|
+
//
|
|
446
|
+
//
|
|
447
|
+
// 3-plus-argument functions...
|
|
448
|
+
//
|
|
449
|
+
//
|
|
440
450
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
451
|
+
case '_GET_DATA_BETWEEN_TIMES':
|
|
452
|
+
case '_RAMP':
|
|
453
|
+
case '_XIDZ':
|
|
454
|
+
validateCallArgs(callExpr, 3)
|
|
455
|
+
break
|
|
446
456
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
457
|
+
case '_PULSE_TRAIN':
|
|
458
|
+
validateCallArgs(callExpr, 4)
|
|
459
|
+
break
|
|
450
460
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
461
|
+
case '_VECTOR_SELECT':
|
|
462
|
+
validateCallArgs(callExpr, 5)
|
|
463
|
+
break
|
|
454
464
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
465
|
+
//
|
|
466
|
+
//
|
|
467
|
+
// Complex functions...
|
|
468
|
+
//
|
|
469
|
+
//
|
|
470
|
+
|
|
471
|
+
case '_ACTIVE_INITIAL':
|
|
472
|
+
validateCallDepth(callExpr, context)
|
|
473
|
+
validateCallArgs(callExpr, 2)
|
|
474
|
+
v.hasInitValue = true
|
|
475
|
+
// The 2nd argument is used at init time
|
|
476
|
+
argModes[1] = 'init'
|
|
477
|
+
break
|
|
468
478
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
479
|
+
case '_ALLOCATE_AVAILABLE':
|
|
480
|
+
validateCallDepth(callExpr, context)
|
|
481
|
+
validateCallArgs(callExpr, 3)
|
|
482
|
+
break
|
|
473
483
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
484
|
+
case '_DELAY1':
|
|
485
|
+
case '_DELAY1I':
|
|
486
|
+
case '_DELAY3':
|
|
487
|
+
case '_DELAY3I':
|
|
488
|
+
validateCallArgs(callExpr, callExpr.fnId.endsWith('I') ? 3 : 2)
|
|
489
|
+
addFnReference = false
|
|
490
|
+
visitArgs = false
|
|
491
|
+
generateDelayVariables(v, callExpr, context)
|
|
492
|
+
break
|
|
483
493
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
494
|
+
case '_DELAY_FIXED':
|
|
495
|
+
validateCallDepth(callExpr, context)
|
|
496
|
+
validateCallArgs(callExpr, 3)
|
|
497
|
+
v.varType = 'level'
|
|
498
|
+
v.varSubtype = 'fixedDelay'
|
|
499
|
+
v.hasInitValue = true
|
|
500
|
+
v.fixedDelayVarName = canonicalName(newFixedDelayVarName())
|
|
501
|
+
// The 2nd and 3rd arguments are used at init time
|
|
502
|
+
argModes[1] = 'init'
|
|
503
|
+
argModes[2] = 'init'
|
|
504
|
+
break
|
|
495
505
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
506
|
+
case '_DEPRECIATE_STRAIGHTLINE':
|
|
507
|
+
validateCallDepth(callExpr, context)
|
|
508
|
+
validateCallArgs(callExpr, 4)
|
|
509
|
+
v.varSubtype = 'depreciation'
|
|
510
|
+
v.hasInitValue = true
|
|
511
|
+
v.depreciationVarName = canonicalName(newDepreciationVarName())
|
|
512
|
+
// The 2nd and 3rd arguments are used at init time
|
|
513
|
+
// TODO: The 3rd (fisc) argument is not currently supported
|
|
514
|
+
// TODO: Shouldn't the last (init) argument be marked as 'init' here? (It's
|
|
515
|
+
// not treated as 'init' in the legacy reader.)
|
|
516
|
+
argModes[1] = 'init'
|
|
517
|
+
argModes[2] = 'init'
|
|
518
|
+
break
|
|
509
519
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
520
|
+
case '_GAME':
|
|
521
|
+
validateCallDepth(callExpr, context)
|
|
522
|
+
validateCallArgs(callExpr, 1)
|
|
523
|
+
generateGameVariables(v, callExpr, context)
|
|
524
|
+
break
|
|
515
525
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
526
|
+
case '_GET_DIRECT_CONSTANTS':
|
|
527
|
+
case '_GET_XLS_CONSTANTS':
|
|
528
|
+
validateCallDepth(callExpr, context)
|
|
529
|
+
validateCallArgs(callExpr, 3)
|
|
530
|
+
validateCallArgType(callExpr, 0, 'string')
|
|
531
|
+
validateCallArgType(callExpr, 1, 'string')
|
|
532
|
+
validateCallArgType(callExpr, 2, 'string')
|
|
533
|
+
addFnReference = false
|
|
534
|
+
v.varType = 'const'
|
|
535
|
+
v.directConstArgs = {
|
|
536
|
+
file: callExpr.args[0].text,
|
|
537
|
+
tab: callExpr.args[1].text,
|
|
538
|
+
startCell: callExpr.args[2].text
|
|
539
|
+
}
|
|
540
|
+
break
|
|
541
|
+
|
|
542
|
+
case '_GET_DIRECT_DATA':
|
|
543
|
+
case '_GET_XLS_DATA':
|
|
544
|
+
case '_GET_DIRECT_LOOKUPS':
|
|
545
|
+
case '_GET_XLS_LOOKUPS':
|
|
546
|
+
validateCallDepth(callExpr, context)
|
|
547
|
+
validateCallArgs(callExpr, 4)
|
|
548
|
+
validateCallArgType(callExpr, 0, 'string')
|
|
549
|
+
validateCallArgType(callExpr, 1, 'string')
|
|
550
|
+
validateCallArgType(callExpr, 2, 'string')
|
|
551
|
+
validateCallArgType(callExpr, 3, 'string')
|
|
552
|
+
addFnReference = false
|
|
553
|
+
v.varType = 'data'
|
|
554
|
+
v.directDataArgs = {
|
|
555
|
+
file: callExpr.args[0].text,
|
|
556
|
+
tab: callExpr.args[1].text,
|
|
557
|
+
timeRowOrCol: callExpr.args[2].text,
|
|
558
|
+
startCell: callExpr.args[3].text
|
|
559
|
+
}
|
|
560
|
+
break
|
|
561
|
+
|
|
562
|
+
case '_IF_THEN_ELSE':
|
|
563
|
+
validateCallArgs(callExpr, 3)
|
|
564
|
+
addFnReference = false
|
|
565
|
+
break
|
|
566
|
+
|
|
567
|
+
case '_INITIAL':
|
|
568
|
+
validateCallDepth(callExpr, context)
|
|
569
|
+
validateCallArgs(callExpr, 1)
|
|
570
|
+
v.varType = 'initial'
|
|
571
|
+
v.hasInitValue = true
|
|
572
|
+
// The single argument is used at init time
|
|
573
|
+
argModes[0] = 'init'
|
|
574
|
+
break
|
|
575
|
+
|
|
576
|
+
case '_INTEG':
|
|
577
|
+
validateCallDepth(callExpr, context)
|
|
578
|
+
validateCallArgs(callExpr, 2)
|
|
579
|
+
v.varType = 'level'
|
|
580
|
+
v.hasInitValue = true
|
|
581
|
+
// The 2nd argument is used at init time
|
|
582
|
+
argModes[1] = 'init'
|
|
583
|
+
break
|
|
584
|
+
|
|
585
|
+
case '_NPV':
|
|
586
|
+
validateCallArgs(callExpr, 4)
|
|
587
|
+
addFnReference = false
|
|
588
|
+
visitArgs = false
|
|
589
|
+
generateNpvVariables(v, callExpr, context)
|
|
590
|
+
break
|
|
591
|
+
|
|
592
|
+
case '_SAMPLE_IF_TRUE':
|
|
593
|
+
validateCallDepth(callExpr, context)
|
|
594
|
+
validateCallArgs(callExpr, 3)
|
|
595
|
+
v.hasInitValue = true
|
|
596
|
+
// The 3rd argument is used at init time
|
|
597
|
+
argModes[2] = 'init'
|
|
598
|
+
break
|
|
599
|
+
|
|
600
|
+
case '_SMOOTH':
|
|
601
|
+
case '_SMOOTHI':
|
|
602
|
+
case '_SMOOTH3':
|
|
603
|
+
case '_SMOOTH3I':
|
|
604
|
+
validateCallArgs(callExpr, callExpr.fnId.endsWith('I') ? 3 : 2)
|
|
605
|
+
addFnReference = false
|
|
606
|
+
visitArgs = false
|
|
607
|
+
generateSmoothVariables(v, callExpr, context)
|
|
608
|
+
break
|
|
609
|
+
|
|
610
|
+
case '_TREND':
|
|
611
|
+
validateCallArgs(callExpr, 3)
|
|
612
|
+
addFnReference = false
|
|
613
|
+
visitArgs = false
|
|
614
|
+
generateTrendVariables(v, callExpr, context)
|
|
615
|
+
break
|
|
616
|
+
|
|
617
|
+
case '_WITH_LOOKUP':
|
|
618
|
+
validateCallDepth(callExpr, context)
|
|
619
|
+
validateCallArgs(callExpr, 2)
|
|
620
|
+
generateLookup(v, callExpr, context)
|
|
621
|
+
break
|
|
622
|
+
|
|
623
|
+
default:
|
|
624
|
+
unhandled = true
|
|
625
|
+
break
|
|
530
626
|
}
|
|
627
|
+
}
|
|
531
628
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
629
|
+
// Helper function that validates a function call for a Stella model
|
|
630
|
+
// XXX: Currently we conflate "XMILE model" with "XMILE model as generated by Stella",
|
|
631
|
+
// so this function only handles the subset of Stella functions that are supported in
|
|
632
|
+
// SDEverywhere's runtime library
|
|
633
|
+
function validateStellaFunctionCall() {
|
|
634
|
+
switch (callExpr.fnId) {
|
|
635
|
+
//
|
|
636
|
+
//
|
|
637
|
+
// 1-argument functions...
|
|
638
|
+
//
|
|
639
|
+
//
|
|
640
|
+
|
|
641
|
+
case '_ABS':
|
|
642
|
+
case '_ARCCOS':
|
|
643
|
+
case '_ARCSIN':
|
|
644
|
+
case '_ARCTAN':
|
|
645
|
+
case '_COS':
|
|
646
|
+
case '_EXP':
|
|
647
|
+
case '_GAMMALN':
|
|
648
|
+
case '_INT':
|
|
649
|
+
case '_LN':
|
|
650
|
+
case '_SIN':
|
|
651
|
+
case '_SIZE':
|
|
652
|
+
case '_SQRT':
|
|
653
|
+
case '_SUM':
|
|
654
|
+
case '_TAN':
|
|
655
|
+
break
|
|
549
656
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
657
|
+
//
|
|
658
|
+
//
|
|
659
|
+
// 2-argument functions...
|
|
660
|
+
//
|
|
661
|
+
//
|
|
662
|
+
|
|
663
|
+
case '_LOOKUP':
|
|
664
|
+
case '_LOOKUPINV':
|
|
665
|
+
case '_MAX':
|
|
666
|
+
case '_MIN':
|
|
667
|
+
case '_MOD':
|
|
668
|
+
case '_SAFEDIV':
|
|
669
|
+
case '_STEP':
|
|
670
|
+
validateCallArgs(callExpr, 2)
|
|
671
|
+
break
|
|
554
672
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
// The single argument is used at init time
|
|
561
|
-
argModes[0] = 'init'
|
|
562
|
-
break
|
|
673
|
+
//
|
|
674
|
+
//
|
|
675
|
+
// 3-plus-argument functions...
|
|
676
|
+
//
|
|
677
|
+
//
|
|
563
678
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
v.varType = 'level'
|
|
568
|
-
v.hasInitValue = true
|
|
569
|
-
// The 2nd argument is used at init time
|
|
570
|
-
argModes[1] = 'init'
|
|
571
|
-
break
|
|
679
|
+
case '_RAMP':
|
|
680
|
+
validateCallArgs(callExpr, 3)
|
|
681
|
+
break
|
|
572
682
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
683
|
+
//
|
|
684
|
+
//
|
|
685
|
+
// Complex functions...
|
|
686
|
+
//
|
|
687
|
+
//
|
|
688
|
+
|
|
689
|
+
case '_ACTIVE_INITIAL':
|
|
690
|
+
// NOTE: Stella doesn't have a built-in `ACTIVE INITIAL` function, but our XMILE parser
|
|
691
|
+
// synthesizes an `ACTIVE INITIAL` function call for `<aux>` variable definitions that
|
|
692
|
+
// have both `<eqn>` and `<init_eqn>` elements. This is equivalent to Vensim's
|
|
693
|
+
// `ACTIVE INITIAL` function.
|
|
694
|
+
validateCallDepth(callExpr, context)
|
|
695
|
+
validateCallArgs(callExpr, 2)
|
|
696
|
+
v.hasInitValue = true
|
|
697
|
+
// The 2nd argument is used at init time
|
|
698
|
+
argModes[1] = 'init'
|
|
699
|
+
break
|
|
579
700
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
701
|
+
case '_DELAY':
|
|
702
|
+
// Stella's DELAY function is equivalent to Vensim's DELAY FIXED function
|
|
703
|
+
validateCallDepth(callExpr, context)
|
|
704
|
+
validateCallArgs(callExpr, 3)
|
|
705
|
+
v.varType = 'level'
|
|
706
|
+
v.varSubtype = 'fixedDelay'
|
|
707
|
+
v.hasInitValue = true
|
|
708
|
+
v.fixedDelayVarName = canonicalName(newFixedDelayVarName())
|
|
709
|
+
// The 2nd and 3rd arguments are used at init time
|
|
710
|
+
argModes[1] = 'init'
|
|
711
|
+
argModes[2] = 'init'
|
|
712
|
+
break
|
|
587
713
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
714
|
+
case '_DEPRECIATE_STRAIGHTLINE':
|
|
715
|
+
// Stella's DEPRECIATE_STRAIGHTLINE function has the same signature as Vensim's
|
|
716
|
+
validateCallDepth(callExpr, context)
|
|
717
|
+
validateCallArgs(callExpr, 4)
|
|
718
|
+
v.varSubtype = 'depreciation'
|
|
719
|
+
v.hasInitValue = true
|
|
720
|
+
v.depreciationVarName = canonicalName(newDepreciationVarName())
|
|
721
|
+
// The 2nd and 3rd arguments are used at init time
|
|
722
|
+
argModes[1] = 'init'
|
|
723
|
+
argModes[2] = 'init'
|
|
724
|
+
break
|
|
597
725
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
726
|
+
case '_DELAY1':
|
|
727
|
+
case '_DELAY3':
|
|
728
|
+
// Stella's DELAY1 and DELAY3 functions can take a third "initial" argument (in which case
|
|
729
|
+
// they behave like Vensim's DELAY1I and DELAY3I functions)
|
|
730
|
+
validateCallArgs(callExpr, [2, 3])
|
|
731
|
+
addFnReference = false
|
|
732
|
+
visitArgs = false
|
|
733
|
+
generateDelayVariables(v, callExpr, context)
|
|
734
|
+
break
|
|
604
735
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
break
|
|
736
|
+
case '_IF_THEN_ELSE':
|
|
737
|
+
validateCallArgs(callExpr, 3)
|
|
738
|
+
addFnReference = false
|
|
739
|
+
break
|
|
610
740
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
741
|
+
case '_INIT':
|
|
742
|
+
validateCallDepth(callExpr, context)
|
|
743
|
+
validateCallArgs(callExpr, 1)
|
|
744
|
+
v.varType = 'initial'
|
|
745
|
+
v.hasInitValue = true
|
|
746
|
+
// The single argument is used at init time
|
|
747
|
+
argModes[0] = 'init'
|
|
748
|
+
break
|
|
749
|
+
|
|
750
|
+
case '_INTEG':
|
|
751
|
+
// NOTE: Stella doesn't have a built-in `INTEG` function, but our XMILE parser synthesizes
|
|
752
|
+
// an `INTEG` function call for `<stock>` variable definitions using the `<inflow>` and
|
|
753
|
+
// `<outflow>` elements as the `rate` argument for the Vensim-style `INTEG` function call
|
|
754
|
+
validateCallDepth(callExpr, context)
|
|
755
|
+
validateCallArgs(callExpr, 2)
|
|
756
|
+
v.varType = 'level'
|
|
757
|
+
v.hasInitValue = true
|
|
758
|
+
// The 2nd argument is used at init time
|
|
759
|
+
argModes[1] = 'init'
|
|
760
|
+
break
|
|
761
|
+
|
|
762
|
+
case '_SMTH1':
|
|
763
|
+
case '_SMTH3':
|
|
764
|
+
// Stella's SMTH1 and SMTH3 functions can take a third "initial" argument (in which case
|
|
765
|
+
// they behave like Vensim's SMOOTHI and SMOOTH3I functions)
|
|
766
|
+
validateCallArgs(callExpr, [2, 3])
|
|
767
|
+
addFnReference = false
|
|
768
|
+
visitArgs = false
|
|
769
|
+
generateSmoothVariables(v, callExpr, context)
|
|
770
|
+
break
|
|
771
|
+
|
|
772
|
+
case '_TREND':
|
|
773
|
+
validateCallArgs(callExpr, 3)
|
|
774
|
+
addFnReference = false
|
|
775
|
+
visitArgs = false
|
|
776
|
+
generateTrendVariables(v, callExpr, context)
|
|
777
|
+
break
|
|
778
|
+
|
|
779
|
+
default:
|
|
780
|
+
unhandled = true
|
|
781
|
+
break
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Validate the function call based on the model kind
|
|
786
|
+
if (context.modelKind === 'vensim') {
|
|
787
|
+
validateVensimFunctionCall()
|
|
788
|
+
} else if (context.modelKind === 'xmile') {
|
|
789
|
+
validateStellaFunctionCall()
|
|
790
|
+
} else {
|
|
791
|
+
throw new Error(`Unknown model kind: ${context.modelKind}`)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (unhandled) {
|
|
795
|
+
// We did not match a known function, so we need to check if this is a lookup call.
|
|
796
|
+
// See if the function name is actually the name of a lookup variable. For Vensim
|
|
797
|
+
// models, the antlr4-vensim grammar has separate definitions for lookup calls and
|
|
798
|
+
// function calls, but in practice they can only be differentiated in the case
|
|
799
|
+
// where the lookup has subscripts; when there are no subscripts, they get treated
|
|
800
|
+
// like normal function calls, and in that case we will end up here. If we find
|
|
801
|
+
// a variable with the given name, then we will assume it's a lookup call, otherwise
|
|
802
|
+
// we treat it as a call of an unimplemented function.
|
|
803
|
+
const varId = callExpr.fnId.toLowerCase()
|
|
804
|
+
const referencedVar = Model.varWithName(varId)
|
|
805
|
+
if (referencedVar === undefined || referencedVar.parsedEqn.rhs.kind !== 'lookup') {
|
|
806
|
+
// Throw an error if the function is not yet implemented in SDE
|
|
807
|
+
// TODO: This will report false positives in the case of user-defined macros. For now
|
|
808
|
+
// we provide the ability to turn off this check via an environment variable, but we
|
|
809
|
+
// should consider providing a way for the user to declare the names of any user-defined
|
|
810
|
+
// macros so that we can skip this check when those macros are detected.
|
|
811
|
+
if (process.env.SDE_REPORT_UNSUPPORTED_FUNCTIONS !== '0') {
|
|
812
|
+
const msg = `Unhandled function '${callExpr.fnId}' in readEquations for '${v.modelLHS}'`
|
|
813
|
+
if (process.env.SDE_REPORT_UNSUPPORTED_FUNCTIONS === 'warn') {
|
|
814
|
+
console.warn(`WARNING: ${msg}`)
|
|
815
|
+
} else {
|
|
816
|
+
throw new Error(msg)
|
|
634
817
|
}
|
|
635
818
|
}
|
|
636
|
-
break
|
|
637
819
|
}
|
|
638
820
|
}
|
|
639
821
|
|
|
@@ -725,10 +907,20 @@ function validateCallDepth(callExpr, context) {
|
|
|
725
907
|
* Throw an error if the given function call does not have the expected number of arguments.
|
|
726
908
|
*/
|
|
727
909
|
function validateCallArgs(callExpr, expectedArgCount) {
|
|
728
|
-
if (
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
910
|
+
if (Array.isArray(expectedArgCount)) {
|
|
911
|
+
if (!expectedArgCount.includes(callExpr.args.length)) {
|
|
912
|
+
throw new Error(
|
|
913
|
+
`Expected '${callExpr.fnName}' function call to have ${expectedArgCount.join('|')} arguments but got ${
|
|
914
|
+
callExpr.args.length
|
|
915
|
+
}`
|
|
916
|
+
)
|
|
917
|
+
}
|
|
918
|
+
} else {
|
|
919
|
+
if (callExpr.args.length !== expectedArgCount) {
|
|
920
|
+
throw new Error(
|
|
921
|
+
`Expected '${callExpr.fnName}' function call to have ${expectedArgCount} arguments but got ${callExpr.args.length}`
|
|
922
|
+
)
|
|
923
|
+
}
|
|
732
924
|
}
|
|
733
925
|
}
|
|
734
926
|
|
|
@@ -778,21 +970,26 @@ function expandedRefIdsForVar(lhsVariable, rhsBaseRefId, rhsSubIds) {
|
|
|
778
970
|
// it must be non-apply-to-all. The goal now is to determine which instances (refIds) are
|
|
779
971
|
// relevant for the given `lhsVariable` context.
|
|
780
972
|
//
|
|
781
|
-
// First,
|
|
782
|
-
//
|
|
973
|
+
// First, determine the set of LHS subscript indices accessed at each position of the RHS
|
|
974
|
+
// variable reference. For example:
|
|
783
975
|
// y[DimA,DimB,DimC] :EXCEPT: [DimA,DimB,C1] = x[DimA,DimC,DimB]
|
|
784
976
|
// In this case the `DimC` on the RHS is only "accessed" by `C2` from the LHS, so we would
|
|
785
|
-
// build
|
|
786
|
-
// _a1,
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
// _a2,_c2,_b2
|
|
977
|
+
// build a per-position set of accessed indices, like this:
|
|
978
|
+
// position 0 (DimA on RHS): { _a1, _a2 }
|
|
979
|
+
// position 1 (DimC on RHS): { _c2 }
|
|
980
|
+
// position 2 (DimB on RHS): { _b1, _b2 }
|
|
790
981
|
//
|
|
791
|
-
// Then, for each RHS variable instance
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
//
|
|
982
|
+
// Then, for each RHS variable instance, check whether every subscript position has at
|
|
983
|
+
// least one index in common between the LHS set and the indices that the RHS instance
|
|
984
|
+
// accepts at that position. If so, add the RHS `refId` to the array of variables
|
|
985
|
+
// referenced by the LHS.
|
|
986
|
+
//
|
|
987
|
+
// Conceptually this is equivalent to checking whether any combination in the LHS
|
|
988
|
+
// cartesian product matches any combination in the RHS cartesian product, but we can
|
|
989
|
+
// avoid computing the cartesian products explicitly because positions in a cartesian
|
|
990
|
+
// product are independent: if every position has at least one element in common, then
|
|
991
|
+
// there exists a full combination that matches. This reduces the complexity from
|
|
992
|
+
// O(product of dimension sizes) to O(sum of dimension sizes).
|
|
796
993
|
//
|
|
797
994
|
// In the following examples, suppose the referenced RHS variable is non-apply-to-all and
|
|
798
995
|
// has two instances:
|
|
@@ -823,43 +1020,18 @@ function expandedRefIdsForVar(lhsVariable, rhsBaseRefId, rhsSubIds) {
|
|
|
823
1020
|
// _x[_dima,_c2,_dimb]
|
|
824
1021
|
//
|
|
825
1022
|
|
|
826
|
-
// Step 1:
|
|
827
|
-
//
|
|
828
|
-
//
|
|
829
|
-
// are relevant for the RHS subscripts/dimensions given the context of the LHS variable
|
|
830
|
-
// may have been separated/expanded).
|
|
1023
|
+
// Step 1: Resolve the LHS subscript/dimension at each position of the RHS variable
|
|
1024
|
+
// reference. Here `rhsSubIds` is the array of parsed subscript/dimension IDs that
|
|
1025
|
+
// appear in the RHS variable reference. We figure out which LHS subscripts/dimensions
|
|
1026
|
+
// are relevant for the RHS subscripts/dimensions given the context of the LHS variable
|
|
1027
|
+
// (which may have been separated/expanded).
|
|
831
1028
|
const lhsSubRefs = lhsVariable.parsedEqn.lhs.varDef.subscriptRefs
|
|
832
1029
|
const lhsSubIds = lhsSubRefs?.map(subRef => subRef.subId) || []
|
|
833
1030
|
const mappedLhsSubIds = rhsSubIds.map(rhsSubId => resolveRhsSubOrDim(lhsVariable, lhsSubIds, rhsSubId))
|
|
834
1031
|
|
|
835
|
-
// Step 2:
|
|
836
|
-
//
|
|
837
|
-
|
|
838
|
-
const mappedLhsCombos = cartesianProductOf(mappedLhsSubIdsPerPosition).map(combo => combo.join(','))
|
|
839
|
-
|
|
840
|
-
// Step 3: For each RHS variable instance, get all combinations of RHS subscripts that can
|
|
841
|
-
// be accepted by that particular RHS instance
|
|
842
|
-
const rhsRefIds = []
|
|
843
|
-
for (const rhsVarInstance of rhsVarInstances) {
|
|
844
|
-
// Build RHS subscript combos (one string of comma-separated subscript IDs for each combo)
|
|
845
|
-
const rhsVarInstanceSubIdsPerPosition = rhsVarInstance.subscripts.map(indexNamesForSubscript)
|
|
846
|
-
const rhsCombos = cartesianProductOf(rhsVarInstanceSubIdsPerPosition).map(combo => combo.join(','))
|
|
847
|
-
|
|
848
|
-
// See if any of the LHS subscript combos match any of the RHS subscript combos
|
|
849
|
-
for (const lhsCombo of mappedLhsCombos) {
|
|
850
|
-
if (rhsCombos.includes(lhsCombo)) {
|
|
851
|
-
// There was a match; add the refId and break out of the inner loop
|
|
852
|
-
rhsRefIds.push(rhsVarInstance.refId)
|
|
853
|
-
break
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// Return the sorted array of relevant refIds
|
|
859
|
-
// TODO: Sorting is not essential here, but the legacy reader sorted so we will keep that
|
|
860
|
-
// behavior now to avoid invalidating tests. Later we should remove this `sort` call and
|
|
861
|
-
// update the tests accordingly.
|
|
862
|
-
return rhsRefIds.sort()
|
|
1032
|
+
// Step 2: Find the RHS variable instances whose subscripts overlap with the LHS
|
|
1033
|
+
// subscripts at every position
|
|
1034
|
+
return matchingRhsRefIds(mappedLhsSubIds, rhsVarInstances)
|
|
863
1035
|
}
|
|
864
1036
|
|
|
865
1037
|
/**
|
|
@@ -949,3 +1121,121 @@ function resolveRhsSubOrDim(lhsVariable, lhsSubIds, rhsSubId) {
|
|
|
949
1121
|
throw new Error(`Failed to find LHS dimension for RHS dimension ${rhsSubId} in lhs=${lhsVariable.refId}`)
|
|
950
1122
|
}
|
|
951
1123
|
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Resolve any XMILE dimension wildcards in the given equation and return a new equation
|
|
1127
|
+
* that has the `_SDE_WILDCARD_` placeholder replaced with the actual dimension name.
|
|
1128
|
+
*
|
|
1129
|
+
* @param {*} variable The `Variable` instance to process.
|
|
1130
|
+
* @returns {*} The parsed equation with the `_SDE_WILDCARD_` placeholder replaced with the
|
|
1131
|
+
* actual dimension name, or `undefined` if the equation does not contain any wildcards.
|
|
1132
|
+
*/
|
|
1133
|
+
export function resolveXmileDimensionWildcards(variable) {
|
|
1134
|
+
const eqn = variable.parsedEqn
|
|
1135
|
+
if (!eqn.rhs || eqn.rhs.kind !== 'expr') {
|
|
1136
|
+
return undefined
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Create a deep copy of the equation and resolve wildcards
|
|
1140
|
+
let hasWildcards = false
|
|
1141
|
+
function resolveWildcardsInExpr(expr) {
|
|
1142
|
+
switch (expr.kind) {
|
|
1143
|
+
case 'variable-ref': {
|
|
1144
|
+
if (!expr.subscriptRefs) {
|
|
1145
|
+
return expr
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// Check if this variable reference has wildcards
|
|
1149
|
+
let varRefHasWildcard = false
|
|
1150
|
+
const newSubscriptRefs = expr.subscriptRefs.map((subRef, subIndex) => {
|
|
1151
|
+
if (subRef.subId.startsWith('__sde_wildcard_')) {
|
|
1152
|
+
varRefHasWildcard = true
|
|
1153
|
+
hasWildcards = true
|
|
1154
|
+
|
|
1155
|
+
// Look up the referenced variable to get its dimensions
|
|
1156
|
+
const referencedVars = Model.varsWithName(expr.varId)
|
|
1157
|
+
if (referencedVars && referencedVars.length > 0) {
|
|
1158
|
+
// Get the dimension ID at this index from the referenced variable
|
|
1159
|
+
const referencedDimOrSubId = referencedVars[0].subscripts[subIndex]
|
|
1160
|
+
|
|
1161
|
+
// Get the dimension name for the ID
|
|
1162
|
+
const referencedDimOrSub = sub(referencedDimOrSubId)
|
|
1163
|
+
let referencedDimName
|
|
1164
|
+
let referencedDimId
|
|
1165
|
+
if (isIndex(referencedDimOrSubId)) {
|
|
1166
|
+
// This is a subscript, so get the parent dimension name and ID
|
|
1167
|
+
const parentDim = sub(referencedDimOrSub.family)
|
|
1168
|
+
referencedDimName = parentDim.modelName
|
|
1169
|
+
referencedDimId = parentDim.name
|
|
1170
|
+
} else {
|
|
1171
|
+
// This is a dimension, so take its name and ID directly
|
|
1172
|
+
referencedDimName = referencedDimOrSub.modelName
|
|
1173
|
+
referencedDimId = referencedDimOrSub.name
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Preserve any trailing characters (like '!') from the wildcard
|
|
1177
|
+
const trailingChars = subRef.subId.substring('__sde_wildcard_'.length)
|
|
1178
|
+
return {
|
|
1179
|
+
subName: referencedDimName + trailingChars,
|
|
1180
|
+
subId: referencedDimId + trailingChars
|
|
1181
|
+
}
|
|
1182
|
+
} else {
|
|
1183
|
+
// If we can't find the referenced variable or it has no dimensions, keep the wildcard
|
|
1184
|
+
return subRef
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return subRef
|
|
1188
|
+
})
|
|
1189
|
+
|
|
1190
|
+
if (varRefHasWildcard) {
|
|
1191
|
+
return { ...expr, subscriptRefs: newSubscriptRefs }
|
|
1192
|
+
}
|
|
1193
|
+
return expr
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
case 'binary-op':
|
|
1197
|
+
return {
|
|
1198
|
+
...expr,
|
|
1199
|
+
lhs: resolveWildcardsInExpr(expr.lhs),
|
|
1200
|
+
rhs: resolveWildcardsInExpr(expr.rhs)
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
case 'parens':
|
|
1204
|
+
case 'unary-op':
|
|
1205
|
+
return { ...expr, expr: resolveWildcardsInExpr(expr.expr) }
|
|
1206
|
+
|
|
1207
|
+
case 'function-call': {
|
|
1208
|
+
const newArgs = expr.args.map(arg => resolveWildcardsInExpr(arg))
|
|
1209
|
+
return { ...expr, args: newArgs }
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
case 'lookup-call':
|
|
1213
|
+
return { ...expr, arg: resolveWildcardsInExpr(expr.arg) }
|
|
1214
|
+
|
|
1215
|
+
case 'number':
|
|
1216
|
+
case 'string':
|
|
1217
|
+
case 'keyword':
|
|
1218
|
+
case 'lookup-def':
|
|
1219
|
+
return expr
|
|
1220
|
+
|
|
1221
|
+
default:
|
|
1222
|
+
throw new Error(`Unhandled expression kind '${expr.kind}' when reading '${variable.modelLHS}'`)
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const resolvedRhs = resolveWildcardsInExpr(eqn.rhs.expr)
|
|
1227
|
+
if (!hasWildcards) {
|
|
1228
|
+
// No wildcards were found, so return the original equation
|
|
1229
|
+
return undefined
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Wildcards were found, so return a new equation with the wildcards replaced with the
|
|
1233
|
+
// actual dimension names
|
|
1234
|
+
return {
|
|
1235
|
+
...eqn,
|
|
1236
|
+
rhs: {
|
|
1237
|
+
...eqn.rhs,
|
|
1238
|
+
expr: resolvedRhs
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|