@sdeverywhere/compile 0.7.27 → 0.7.28
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/generate/gen-expr.js +87 -37
- 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.js +541 -234
- package/src/parse-and-generate.js +35 -23
|
@@ -14,7 +14,10 @@ import { generateLookup } from './read-equation-fn-with-lookup.js'
|
|
|
14
14
|
import { readVariables } from './read-variables.js'
|
|
15
15
|
|
|
16
16
|
class Context {
|
|
17
|
-
constructor(eqnLhs, refId) {
|
|
17
|
+
constructor(modelKind, eqnLhs, refId) {
|
|
18
|
+
// The kind of model being read, either 'vensim' or 'xmile'
|
|
19
|
+
this.modelKind = modelKind
|
|
20
|
+
|
|
18
21
|
// The LHS of the equation being processed
|
|
19
22
|
this.eqnLhs = eqnLhs
|
|
20
23
|
|
|
@@ -109,7 +112,7 @@ class Context {
|
|
|
109
112
|
* @param {string[]} eqnStrings An array of individual equation strings in Vensim format.
|
|
110
113
|
*/
|
|
111
114
|
defineVariables(eqnStrings) {
|
|
112
|
-
// Parse the equation text
|
|
115
|
+
// Parse the equation text, which is assumed to be in Vensim format
|
|
113
116
|
const eqnText = eqnStrings.join('\n')
|
|
114
117
|
const parsedModel = { kind: 'vensim', root: parseVensimModel(eqnText) }
|
|
115
118
|
|
|
@@ -131,7 +134,7 @@ class Context {
|
|
|
131
134
|
|
|
132
135
|
vars.forEach(v => {
|
|
133
136
|
// Process each variable using the same process as above
|
|
134
|
-
readEquation(v)
|
|
137
|
+
readEquation(v, 'vensim')
|
|
135
138
|
|
|
136
139
|
// Inhibit output for generated variables
|
|
137
140
|
v.includeInOutput = false
|
|
@@ -168,10 +171,11 @@ class Context {
|
|
|
168
171
|
* TODO: Docs and types
|
|
169
172
|
*
|
|
170
173
|
* @param v {*} The `Variable` instance to process.
|
|
174
|
+
* @param {string} modelKind The kind of model being read, either 'vensim' or 'xmile'.
|
|
171
175
|
*/
|
|
172
|
-
export function readEquation(v) {
|
|
176
|
+
export function readEquation(v, modelKind) {
|
|
173
177
|
const eqn = v.parsedEqn
|
|
174
|
-
const context = new Context(eqn?.lhs, v.refId)
|
|
178
|
+
const context = new Context(modelKind, eqn?.lhs, v.refId)
|
|
175
179
|
|
|
176
180
|
// Visit the RHS of the equation. If the equation is undefined, it is a synthesized
|
|
177
181
|
// variable (e.g., `Time`), in which case we skip this step.
|
|
@@ -383,257 +387,432 @@ function visitFunctionCall(v, callExpr, context) {
|
|
|
383
387
|
// (they will be visited when processing the replacement equations)
|
|
384
388
|
let visitArgs = true
|
|
385
389
|
|
|
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
|
-
|
|
390
|
+
// If the `unhandled` flag is set, it means we did not match a known function
|
|
391
|
+
let unhandled = false
|
|
392
|
+
|
|
393
|
+
// Helper function that validates a function call for a Vensim model
|
|
394
|
+
function validateVensimFunctionCall() {
|
|
395
|
+
switch (callExpr.fnId) {
|
|
396
|
+
//
|
|
397
|
+
//
|
|
398
|
+
// 1-argument functions...
|
|
399
|
+
//
|
|
400
|
+
//
|
|
401
|
+
|
|
402
|
+
case '_ABS':
|
|
403
|
+
case '_ARCCOS':
|
|
404
|
+
case '_ARCSIN':
|
|
405
|
+
case '_ARCTAN':
|
|
406
|
+
case '_COS':
|
|
407
|
+
case '_ELMCOUNT':
|
|
408
|
+
case '_EXP':
|
|
409
|
+
case '_GAMMA_LN':
|
|
410
|
+
case '_INTEGER':
|
|
411
|
+
case '_LN':
|
|
412
|
+
case '_SIN':
|
|
413
|
+
case '_SQRT':
|
|
414
|
+
case '_SUM':
|
|
415
|
+
case '_TAN':
|
|
416
|
+
case '_VMAX':
|
|
417
|
+
case '_VMIN':
|
|
418
|
+
validateCallArgs(callExpr, 1)
|
|
419
|
+
break
|
|
411
420
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
421
|
+
//
|
|
422
|
+
//
|
|
423
|
+
// 2-argument functions...
|
|
424
|
+
//
|
|
425
|
+
//
|
|
426
|
+
|
|
427
|
+
case '_LOOKUP_BACKWARD':
|
|
428
|
+
case '_LOOKUP_FORWARD':
|
|
429
|
+
case '_LOOKUP_INVERT':
|
|
430
|
+
case '_MAX':
|
|
431
|
+
case '_MIN':
|
|
432
|
+
case '_MODULO':
|
|
433
|
+
case '_POW':
|
|
434
|
+
case '_POWER':
|
|
435
|
+
case '_PULSE':
|
|
436
|
+
case '_QUANTUM':
|
|
437
|
+
case '_STEP':
|
|
438
|
+
case '_VECTOR_ELM_MAP':
|
|
439
|
+
case '_VECTOR_SORT_ORDER':
|
|
440
|
+
case '_ZIDZ':
|
|
441
|
+
validateCallArgs(callExpr, 2)
|
|
442
|
+
break
|
|
434
443
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
444
|
+
//
|
|
445
|
+
//
|
|
446
|
+
// 3-plus-argument functions...
|
|
447
|
+
//
|
|
448
|
+
//
|
|
440
449
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
450
|
+
case '_GET_DATA_BETWEEN_TIMES':
|
|
451
|
+
case '_RAMP':
|
|
452
|
+
case '_XIDZ':
|
|
453
|
+
validateCallArgs(callExpr, 3)
|
|
454
|
+
break
|
|
446
455
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
456
|
+
case '_PULSE_TRAIN':
|
|
457
|
+
validateCallArgs(callExpr, 4)
|
|
458
|
+
break
|
|
450
459
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
460
|
+
case '_VECTOR_SELECT':
|
|
461
|
+
validateCallArgs(callExpr, 5)
|
|
462
|
+
break
|
|
454
463
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
464
|
+
//
|
|
465
|
+
//
|
|
466
|
+
// Complex functions...
|
|
467
|
+
//
|
|
468
|
+
//
|
|
469
|
+
|
|
470
|
+
case '_ACTIVE_INITIAL':
|
|
471
|
+
validateCallDepth(callExpr, context)
|
|
472
|
+
validateCallArgs(callExpr, 2)
|
|
473
|
+
v.hasInitValue = true
|
|
474
|
+
// The 2nd argument is used at init time
|
|
475
|
+
argModes[1] = 'init'
|
|
476
|
+
break
|
|
468
477
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
478
|
+
case '_ALLOCATE_AVAILABLE':
|
|
479
|
+
validateCallDepth(callExpr, context)
|
|
480
|
+
validateCallArgs(callExpr, 3)
|
|
481
|
+
break
|
|
473
482
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
+
case '_DELAY1':
|
|
484
|
+
case '_DELAY1I':
|
|
485
|
+
case '_DELAY3':
|
|
486
|
+
case '_DELAY3I':
|
|
487
|
+
validateCallArgs(callExpr, callExpr.fnId.endsWith('I') ? 3 : 2)
|
|
488
|
+
addFnReference = false
|
|
489
|
+
visitArgs = false
|
|
490
|
+
generateDelayVariables(v, callExpr, context)
|
|
491
|
+
break
|
|
483
492
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
493
|
+
case '_DELAY_FIXED':
|
|
494
|
+
validateCallDepth(callExpr, context)
|
|
495
|
+
validateCallArgs(callExpr, 3)
|
|
496
|
+
v.varType = 'level'
|
|
497
|
+
v.varSubtype = 'fixedDelay'
|
|
498
|
+
v.hasInitValue = true
|
|
499
|
+
v.fixedDelayVarName = canonicalName(newFixedDelayVarName())
|
|
500
|
+
// The 2nd and 3rd arguments are used at init time
|
|
501
|
+
argModes[1] = 'init'
|
|
502
|
+
argModes[2] = 'init'
|
|
503
|
+
break
|
|
495
504
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
505
|
+
case '_DEPRECIATE_STRAIGHTLINE':
|
|
506
|
+
validateCallDepth(callExpr, context)
|
|
507
|
+
validateCallArgs(callExpr, 4)
|
|
508
|
+
v.varSubtype = 'depreciation'
|
|
509
|
+
v.hasInitValue = true
|
|
510
|
+
v.depreciationVarName = canonicalName(newDepreciationVarName())
|
|
511
|
+
// The 2nd and 3rd arguments are used at init time
|
|
512
|
+
// TODO: The 3rd (fisc) argument is not currently supported
|
|
513
|
+
// TODO: Shouldn't the last (init) argument be marked as 'init' here? (It's
|
|
514
|
+
// not treated as 'init' in the legacy reader.)
|
|
515
|
+
argModes[1] = 'init'
|
|
516
|
+
argModes[2] = 'init'
|
|
517
|
+
break
|
|
509
518
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
519
|
+
case '_GAME':
|
|
520
|
+
validateCallDepth(callExpr, context)
|
|
521
|
+
validateCallArgs(callExpr, 1)
|
|
522
|
+
generateGameVariables(v, callExpr, context)
|
|
523
|
+
break
|
|
515
524
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
525
|
+
case '_GET_DIRECT_CONSTANTS': {
|
|
526
|
+
validateCallDepth(callExpr, context)
|
|
527
|
+
validateCallArgs(callExpr, 3)
|
|
528
|
+
validateCallArgType(callExpr, 0, 'string')
|
|
529
|
+
validateCallArgType(callExpr, 1, 'string')
|
|
530
|
+
validateCallArgType(callExpr, 2, 'string')
|
|
531
|
+
addFnReference = false
|
|
532
|
+
v.varType = 'const'
|
|
533
|
+
v.directConstArgs = {
|
|
534
|
+
file: callExpr.args[0].text,
|
|
535
|
+
tab: callExpr.args[1].text,
|
|
536
|
+
startCell: callExpr.args[2].text
|
|
537
|
+
}
|
|
538
|
+
break
|
|
528
539
|
}
|
|
529
|
-
|
|
540
|
+
|
|
541
|
+
case '_GET_DIRECT_DATA':
|
|
542
|
+
case '_GET_DIRECT_LOOKUPS':
|
|
543
|
+
validateCallDepth(callExpr, context)
|
|
544
|
+
validateCallArgs(callExpr, 4)
|
|
545
|
+
validateCallArgType(callExpr, 0, 'string')
|
|
546
|
+
validateCallArgType(callExpr, 1, 'string')
|
|
547
|
+
validateCallArgType(callExpr, 2, 'string')
|
|
548
|
+
validateCallArgType(callExpr, 3, 'string')
|
|
549
|
+
addFnReference = false
|
|
550
|
+
v.varType = 'data'
|
|
551
|
+
v.directDataArgs = {
|
|
552
|
+
file: callExpr.args[0].text,
|
|
553
|
+
tab: callExpr.args[1].text,
|
|
554
|
+
timeRowOrCol: callExpr.args[2].text,
|
|
555
|
+
startCell: callExpr.args[3].text
|
|
556
|
+
}
|
|
557
|
+
break
|
|
558
|
+
|
|
559
|
+
case '_IF_THEN_ELSE':
|
|
560
|
+
validateCallArgs(callExpr, 3)
|
|
561
|
+
addFnReference = false
|
|
562
|
+
break
|
|
563
|
+
|
|
564
|
+
case '_INITIAL':
|
|
565
|
+
validateCallDepth(callExpr, context)
|
|
566
|
+
validateCallArgs(callExpr, 1)
|
|
567
|
+
v.varType = 'initial'
|
|
568
|
+
v.hasInitValue = true
|
|
569
|
+
// The single argument is used at init time
|
|
570
|
+
argModes[0] = 'init'
|
|
571
|
+
break
|
|
572
|
+
|
|
573
|
+
case '_INTEG':
|
|
574
|
+
validateCallDepth(callExpr, context)
|
|
575
|
+
validateCallArgs(callExpr, 2)
|
|
576
|
+
v.varType = 'level'
|
|
577
|
+
v.hasInitValue = true
|
|
578
|
+
// The 2nd argument is used at init time
|
|
579
|
+
argModes[1] = 'init'
|
|
580
|
+
break
|
|
581
|
+
|
|
582
|
+
case '_NPV':
|
|
583
|
+
validateCallArgs(callExpr, 4)
|
|
584
|
+
addFnReference = false
|
|
585
|
+
visitArgs = false
|
|
586
|
+
generateNpvVariables(v, callExpr, context)
|
|
587
|
+
break
|
|
588
|
+
|
|
589
|
+
case '_SAMPLE_IF_TRUE':
|
|
590
|
+
validateCallDepth(callExpr, context)
|
|
591
|
+
validateCallArgs(callExpr, 3)
|
|
592
|
+
v.hasInitValue = true
|
|
593
|
+
// The 3rd argument is used at init time
|
|
594
|
+
argModes[2] = 'init'
|
|
595
|
+
break
|
|
596
|
+
|
|
597
|
+
case '_SMOOTH':
|
|
598
|
+
case '_SMOOTHI':
|
|
599
|
+
case '_SMOOTH3':
|
|
600
|
+
case '_SMOOTH3I':
|
|
601
|
+
validateCallArgs(callExpr, callExpr.fnId.endsWith('I') ? 3 : 2)
|
|
602
|
+
addFnReference = false
|
|
603
|
+
visitArgs = false
|
|
604
|
+
generateSmoothVariables(v, callExpr, context)
|
|
605
|
+
break
|
|
606
|
+
|
|
607
|
+
case '_TREND':
|
|
608
|
+
validateCallArgs(callExpr, 3)
|
|
609
|
+
addFnReference = false
|
|
610
|
+
visitArgs = false
|
|
611
|
+
generateTrendVariables(v, callExpr, context)
|
|
612
|
+
break
|
|
613
|
+
|
|
614
|
+
case '_WITH_LOOKUP':
|
|
615
|
+
validateCallDepth(callExpr, context)
|
|
616
|
+
validateCallArgs(callExpr, 2)
|
|
617
|
+
generateLookup(v, callExpr, context)
|
|
618
|
+
break
|
|
619
|
+
|
|
620
|
+
default:
|
|
621
|
+
unhandled = true
|
|
622
|
+
break
|
|
530
623
|
}
|
|
624
|
+
}
|
|
531
625
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
626
|
+
// Helper function that validates a function call for a Stella model
|
|
627
|
+
// XXX: Currently we conflate "XMILE model" with "XMILE model as generated by Stella",
|
|
628
|
+
// so this function only handles the subset of Stella functions that are supported in
|
|
629
|
+
// SDEverywhere's runtime library
|
|
630
|
+
function validateStellaFunctionCall() {
|
|
631
|
+
switch (callExpr.fnId) {
|
|
632
|
+
//
|
|
633
|
+
//
|
|
634
|
+
// 1-argument functions...
|
|
635
|
+
//
|
|
636
|
+
//
|
|
637
|
+
|
|
638
|
+
case '_ABS':
|
|
639
|
+
case '_ARCCOS':
|
|
640
|
+
case '_ARCSIN':
|
|
641
|
+
case '_ARCTAN':
|
|
642
|
+
case '_COS':
|
|
643
|
+
case '_EXP':
|
|
644
|
+
case '_GAMMALN':
|
|
645
|
+
case '_INT':
|
|
646
|
+
case '_LN':
|
|
647
|
+
case '_SIN':
|
|
648
|
+
case '_SIZE':
|
|
649
|
+
case '_SQRT':
|
|
650
|
+
case '_SUM':
|
|
651
|
+
case '_TAN':
|
|
652
|
+
break
|
|
549
653
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
654
|
+
//
|
|
655
|
+
//
|
|
656
|
+
// 2-argument functions...
|
|
657
|
+
//
|
|
658
|
+
//
|
|
659
|
+
|
|
660
|
+
case '_LOOKUP':
|
|
661
|
+
case '_LOOKUPINV':
|
|
662
|
+
case '_MAX':
|
|
663
|
+
case '_MIN':
|
|
664
|
+
case '_MOD':
|
|
665
|
+
case '_SAFEDIV':
|
|
666
|
+
case '_STEP':
|
|
667
|
+
validateCallArgs(callExpr, 2)
|
|
668
|
+
break
|
|
554
669
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
// The single argument is used at init time
|
|
561
|
-
argModes[0] = 'init'
|
|
562
|
-
break
|
|
670
|
+
//
|
|
671
|
+
//
|
|
672
|
+
// 3-plus-argument functions...
|
|
673
|
+
//
|
|
674
|
+
//
|
|
563
675
|
|
|
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
|
|
676
|
+
case '_RAMP':
|
|
677
|
+
validateCallArgs(callExpr, 3)
|
|
678
|
+
break
|
|
572
679
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
680
|
+
//
|
|
681
|
+
//
|
|
682
|
+
// Complex functions...
|
|
683
|
+
//
|
|
684
|
+
//
|
|
685
|
+
|
|
686
|
+
case '_ACTIVE_INITIAL':
|
|
687
|
+
// NOTE: Stella doesn't have a built-in `ACTIVE INITIAL` function, but our XMILE parser
|
|
688
|
+
// synthesizes an `ACTIVE INITIAL` function call for `<aux>` variable definitions that
|
|
689
|
+
// have both `<eqn>` and `<init_eqn>` elements. This is equivalent to Vensim's
|
|
690
|
+
// `ACTIVE INITIAL` function.
|
|
691
|
+
validateCallDepth(callExpr, context)
|
|
692
|
+
validateCallArgs(callExpr, 2)
|
|
693
|
+
v.hasInitValue = true
|
|
694
|
+
// The 2nd argument is used at init time
|
|
695
|
+
argModes[1] = 'init'
|
|
696
|
+
break
|
|
579
697
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
698
|
+
case '_DELAY':
|
|
699
|
+
// Stella's DELAY function is equivalent to Vensim's DELAY FIXED function
|
|
700
|
+
validateCallDepth(callExpr, context)
|
|
701
|
+
validateCallArgs(callExpr, 3)
|
|
702
|
+
v.varType = 'level'
|
|
703
|
+
v.varSubtype = 'fixedDelay'
|
|
704
|
+
v.hasInitValue = true
|
|
705
|
+
v.fixedDelayVarName = canonicalName(newFixedDelayVarName())
|
|
706
|
+
// The 2nd and 3rd arguments are used at init time
|
|
707
|
+
argModes[1] = 'init'
|
|
708
|
+
argModes[2] = 'init'
|
|
709
|
+
break
|
|
587
710
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
711
|
+
case '_DEPRECIATE_STRAIGHTLINE':
|
|
712
|
+
// Stella's DEPRECIATE_STRAIGHTLINE function has the same signature as Vensim's
|
|
713
|
+
validateCallDepth(callExpr, context)
|
|
714
|
+
validateCallArgs(callExpr, 4)
|
|
715
|
+
v.varSubtype = 'depreciation'
|
|
716
|
+
v.hasInitValue = true
|
|
717
|
+
v.depreciationVarName = canonicalName(newDepreciationVarName())
|
|
718
|
+
// The 2nd and 3rd arguments are used at init time
|
|
719
|
+
argModes[1] = 'init'
|
|
720
|
+
argModes[2] = 'init'
|
|
721
|
+
break
|
|
597
722
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
723
|
+
case '_DELAY1':
|
|
724
|
+
case '_DELAY3':
|
|
725
|
+
// Stella's DELAY1 and DELAY3 functions can take a third "initial" argument (in which case
|
|
726
|
+
// they behave like Vensim's DELAY1I and DELAY3I functions)
|
|
727
|
+
validateCallArgs(callExpr, [2, 3])
|
|
728
|
+
addFnReference = false
|
|
729
|
+
visitArgs = false
|
|
730
|
+
generateDelayVariables(v, callExpr, context)
|
|
731
|
+
break
|
|
604
732
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
break
|
|
733
|
+
case '_IF_THEN_ELSE':
|
|
734
|
+
validateCallArgs(callExpr, 3)
|
|
735
|
+
addFnReference = false
|
|
736
|
+
break
|
|
610
737
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
738
|
+
case '_INIT':
|
|
739
|
+
validateCallDepth(callExpr, context)
|
|
740
|
+
validateCallArgs(callExpr, 1)
|
|
741
|
+
v.varType = 'initial'
|
|
742
|
+
v.hasInitValue = true
|
|
743
|
+
// The single argument is used at init time
|
|
744
|
+
argModes[0] = 'init'
|
|
745
|
+
break
|
|
746
|
+
|
|
747
|
+
case '_INTEG':
|
|
748
|
+
// NOTE: Stella doesn't have a built-in `INTEG` function, but our XMILE parser synthesizes
|
|
749
|
+
// an `INTEG` function call for `<stock>` variable definitions using the `<inflow>` and
|
|
750
|
+
// `<outflow>` elements as the `rate` argument for the Vensim-style `INTEG` function call
|
|
751
|
+
validateCallDepth(callExpr, context)
|
|
752
|
+
validateCallArgs(callExpr, 2)
|
|
753
|
+
v.varType = 'level'
|
|
754
|
+
v.hasInitValue = true
|
|
755
|
+
// The 2nd argument is used at init time
|
|
756
|
+
argModes[1] = 'init'
|
|
757
|
+
break
|
|
758
|
+
|
|
759
|
+
case '_SMTH1':
|
|
760
|
+
case '_SMTH3':
|
|
761
|
+
// Stella's SMTH1 and SMTH3 functions can take a third "initial" argument (in which case
|
|
762
|
+
// they behave like Vensim's SMOOTHI and SMOOTH3I functions)
|
|
763
|
+
validateCallArgs(callExpr, [2, 3])
|
|
764
|
+
addFnReference = false
|
|
765
|
+
visitArgs = false
|
|
766
|
+
generateSmoothVariables(v, callExpr, context)
|
|
767
|
+
break
|
|
768
|
+
|
|
769
|
+
case '_TREND':
|
|
770
|
+
validateCallArgs(callExpr, 3)
|
|
771
|
+
addFnReference = false
|
|
772
|
+
visitArgs = false
|
|
773
|
+
generateTrendVariables(v, callExpr, context)
|
|
774
|
+
break
|
|
775
|
+
|
|
776
|
+
default:
|
|
777
|
+
unhandled = true
|
|
778
|
+
break
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Validate the function call based on the model kind
|
|
783
|
+
if (context.modelKind === 'vensim') {
|
|
784
|
+
validateVensimFunctionCall()
|
|
785
|
+
} else if (context.modelKind === 'xmile') {
|
|
786
|
+
validateStellaFunctionCall()
|
|
787
|
+
} else {
|
|
788
|
+
throw new Error(`Unknown model kind: ${context.modelKind}`)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (unhandled) {
|
|
792
|
+
// We did not match a known function, so we need to check if this is a lookup call.
|
|
793
|
+
// See if the function name is actually the name of a lookup variable. For Vensim
|
|
794
|
+
// models, the antlr4-vensim grammar has separate definitions for lookup calls and
|
|
795
|
+
// function calls, but in practice they can only be differentiated in the case
|
|
796
|
+
// where the lookup has subscripts; when there are no subscripts, they get treated
|
|
797
|
+
// like normal function calls, and in that case we will end up here. If we find
|
|
798
|
+
// a variable with the given name, then we will assume it's a lookup call, otherwise
|
|
799
|
+
// we treat it as a call of an unimplemented function.
|
|
800
|
+
const varId = callExpr.fnId.toLowerCase()
|
|
801
|
+
const referencedVar = Model.varWithName(varId)
|
|
802
|
+
if (referencedVar === undefined || referencedVar.parsedEqn.rhs.kind !== 'lookup') {
|
|
803
|
+
// Throw an error if the function is not yet implemented in SDE
|
|
804
|
+
// TODO: This will report false positives in the case of user-defined macros. For now
|
|
805
|
+
// we provide the ability to turn off this check via an environment variable, but we
|
|
806
|
+
// should consider providing a way for the user to declare the names of any user-defined
|
|
807
|
+
// macros so that we can skip this check when those macros are detected.
|
|
808
|
+
if (process.env.SDE_REPORT_UNSUPPORTED_FUNCTIONS !== '0') {
|
|
809
|
+
const msg = `Unhandled function '${callExpr.fnId}' in readEquations for '${v.modelLHS}'`
|
|
810
|
+
if (process.env.SDE_REPORT_UNSUPPORTED_FUNCTIONS === 'warn') {
|
|
811
|
+
console.warn(`WARNING: ${msg}`)
|
|
812
|
+
} else {
|
|
813
|
+
throw new Error(msg)
|
|
634
814
|
}
|
|
635
815
|
}
|
|
636
|
-
break
|
|
637
816
|
}
|
|
638
817
|
}
|
|
639
818
|
|
|
@@ -725,10 +904,20 @@ function validateCallDepth(callExpr, context) {
|
|
|
725
904
|
* Throw an error if the given function call does not have the expected number of arguments.
|
|
726
905
|
*/
|
|
727
906
|
function validateCallArgs(callExpr, expectedArgCount) {
|
|
728
|
-
if (
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
907
|
+
if (Array.isArray(expectedArgCount)) {
|
|
908
|
+
if (!expectedArgCount.includes(callExpr.args.length)) {
|
|
909
|
+
throw new Error(
|
|
910
|
+
`Expected '${callExpr.fnName}' function call to have ${expectedArgCount.join('|')} arguments but got ${
|
|
911
|
+
callExpr.args.length
|
|
912
|
+
}`
|
|
913
|
+
)
|
|
914
|
+
}
|
|
915
|
+
} else {
|
|
916
|
+
if (callExpr.args.length !== expectedArgCount) {
|
|
917
|
+
throw new Error(
|
|
918
|
+
`Expected '${callExpr.fnName}' function call to have ${expectedArgCount} arguments but got ${callExpr.args.length}`
|
|
919
|
+
)
|
|
920
|
+
}
|
|
732
921
|
}
|
|
733
922
|
}
|
|
734
923
|
|
|
@@ -949,3 +1138,121 @@ function resolveRhsSubOrDim(lhsVariable, lhsSubIds, rhsSubId) {
|
|
|
949
1138
|
throw new Error(`Failed to find LHS dimension for RHS dimension ${rhsSubId} in lhs=${lhsVariable.refId}`)
|
|
950
1139
|
}
|
|
951
1140
|
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Resolve any XMILE dimension wildcards in the given equation and return a new equation
|
|
1144
|
+
* that has the `_SDE_WILDCARD_` placeholder replaced with the actual dimension name.
|
|
1145
|
+
*
|
|
1146
|
+
* @param {*} variable The `Variable` instance to process.
|
|
1147
|
+
* @returns {*} The parsed equation with the `_SDE_WILDCARD_` placeholder replaced with the
|
|
1148
|
+
* actual dimension name, or `undefined` if the equation does not contain any wildcards.
|
|
1149
|
+
*/
|
|
1150
|
+
export function resolveXmileDimensionWildcards(variable) {
|
|
1151
|
+
const eqn = variable.parsedEqn
|
|
1152
|
+
if (!eqn.rhs || eqn.rhs.kind !== 'expr') {
|
|
1153
|
+
return undefined
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Create a deep copy of the equation and resolve wildcards
|
|
1157
|
+
let hasWildcards = false
|
|
1158
|
+
function resolveWildcardsInExpr(expr) {
|
|
1159
|
+
switch (expr.kind) {
|
|
1160
|
+
case 'variable-ref': {
|
|
1161
|
+
if (!expr.subscriptRefs) {
|
|
1162
|
+
return expr
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Check if this variable reference has wildcards
|
|
1166
|
+
let varRefHasWildcard = false
|
|
1167
|
+
const newSubscriptRefs = expr.subscriptRefs.map((subRef, subIndex) => {
|
|
1168
|
+
if (subRef.subId.startsWith('__sde_wildcard_')) {
|
|
1169
|
+
varRefHasWildcard = true
|
|
1170
|
+
hasWildcards = true
|
|
1171
|
+
|
|
1172
|
+
// Look up the referenced variable to get its dimensions
|
|
1173
|
+
const referencedVars = Model.varsWithName(expr.varId)
|
|
1174
|
+
if (referencedVars && referencedVars.length > 0) {
|
|
1175
|
+
// Get the dimension ID at this index from the referenced variable
|
|
1176
|
+
const referencedDimOrSubId = referencedVars[0].subscripts[subIndex]
|
|
1177
|
+
|
|
1178
|
+
// Get the dimension name for the ID
|
|
1179
|
+
const referencedDimOrSub = sub(referencedDimOrSubId)
|
|
1180
|
+
let referencedDimName
|
|
1181
|
+
let referencedDimId
|
|
1182
|
+
if (isIndex(referencedDimOrSubId)) {
|
|
1183
|
+
// This is a subscript, so get the parent dimension name and ID
|
|
1184
|
+
const parentDim = sub(referencedDimOrSub.family)
|
|
1185
|
+
referencedDimName = parentDim.modelName
|
|
1186
|
+
referencedDimId = parentDim.name
|
|
1187
|
+
} else {
|
|
1188
|
+
// This is a dimension, so take its name and ID directly
|
|
1189
|
+
referencedDimName = referencedDimOrSub.modelName
|
|
1190
|
+
referencedDimId = referencedDimOrSub.name
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// Preserve any trailing characters (like '!') from the wildcard
|
|
1194
|
+
const trailingChars = subRef.subId.substring('__sde_wildcard_'.length)
|
|
1195
|
+
return {
|
|
1196
|
+
subName: referencedDimName + trailingChars,
|
|
1197
|
+
subId: referencedDimId + trailingChars
|
|
1198
|
+
}
|
|
1199
|
+
} else {
|
|
1200
|
+
// If we can't find the referenced variable or it has no dimensions, keep the wildcard
|
|
1201
|
+
return subRef
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return subRef
|
|
1205
|
+
})
|
|
1206
|
+
|
|
1207
|
+
if (varRefHasWildcard) {
|
|
1208
|
+
return { ...expr, subscriptRefs: newSubscriptRefs }
|
|
1209
|
+
}
|
|
1210
|
+
return expr
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
case 'binary-op':
|
|
1214
|
+
return {
|
|
1215
|
+
...expr,
|
|
1216
|
+
lhs: resolveWildcardsInExpr(expr.lhs),
|
|
1217
|
+
rhs: resolveWildcardsInExpr(expr.rhs)
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
case 'parens':
|
|
1221
|
+
case 'unary-op':
|
|
1222
|
+
return { ...expr, expr: resolveWildcardsInExpr(expr.expr) }
|
|
1223
|
+
|
|
1224
|
+
case 'function-call': {
|
|
1225
|
+
const newArgs = expr.args.map(arg => resolveWildcardsInExpr(arg))
|
|
1226
|
+
return { ...expr, args: newArgs }
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
case 'lookup-call':
|
|
1230
|
+
return { ...expr, arg: resolveWildcardsInExpr(expr.arg) }
|
|
1231
|
+
|
|
1232
|
+
case 'number':
|
|
1233
|
+
case 'string':
|
|
1234
|
+
case 'keyword':
|
|
1235
|
+
case 'lookup-def':
|
|
1236
|
+
return expr
|
|
1237
|
+
|
|
1238
|
+
default:
|
|
1239
|
+
throw new Error(`Unhandled expression kind '${expr.kind}' when reading '${variable.modelLHS}'`)
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const resolvedRhs = resolveWildcardsInExpr(eqn.rhs.expr)
|
|
1244
|
+
if (!hasWildcards) {
|
|
1245
|
+
// No wildcards were found, so return the original equation
|
|
1246
|
+
return undefined
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// Wildcards were found, so return a new equation with the wildcards replaced with the
|
|
1250
|
+
// actual dimension names
|
|
1251
|
+
return {
|
|
1252
|
+
...eqn,
|
|
1253
|
+
rhs: {
|
|
1254
|
+
...eqn.rhs,
|
|
1255
|
+
expr: resolvedRhs
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|