@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.
@@ -1,8 +1,8 @@
1
1
  import { parseVensimModel } from '@sdeverywhere/parse'
2
2
 
3
- import { canonicalName, cartesianProductOf, newDepreciationVarName, newFixedDelayVarName } from '../_shared/helpers.js'
3
+ import { canonicalName, newDepreciationVarName, newFixedDelayVarName } from '../_shared/helpers.js'
4
4
 
5
- import { hasMapping, indexNamesForSubscript, isDimension, isIndex, sub } from '../_shared/subscript.js'
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
- switch (callExpr.fnId) {
387
- //
388
- //
389
- // 1-argument functions...
390
- //
391
- //
392
-
393
- case '_ABS':
394
- case '_ARCCOS':
395
- case '_ARCSIN':
396
- case '_ARCTAN':
397
- case '_COS':
398
- case '_ELMCOUNT':
399
- case '_EXP':
400
- case '_GAMMA_LN':
401
- case '_INTEGER':
402
- case '_LN':
403
- case '_SIN':
404
- case '_SQRT':
405
- case '_SUM':
406
- case '_TAN':
407
- case '_VMAX':
408
- case '_VMIN':
409
- validateCallArgs(callExpr, 1)
410
- break
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
- // 2-argument functions...
415
- //
416
- //
417
-
418
- case '_LOOKUP_BACKWARD':
419
- case '_LOOKUP_FORWARD':
420
- case '_LOOKUP_INVERT':
421
- case '_MAX':
422
- case '_MIN':
423
- case '_MODULO':
424
- case '_POW':
425
- case '_POWER':
426
- case '_PULSE':
427
- case '_QUANTUM':
428
- case '_STEP':
429
- case '_VECTOR_ELM_MAP':
430
- case '_VECTOR_SORT_ORDER':
431
- case '_ZIDZ':
432
- validateCallArgs(callExpr, 2)
433
- break
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
- // 3-plus-argument functions...
438
- //
439
- //
445
+ //
446
+ //
447
+ // 3-plus-argument functions...
448
+ //
449
+ //
440
450
 
441
- case '_GET_DATA_BETWEEN_TIMES':
442
- case '_RAMP':
443
- case '_XIDZ':
444
- validateCallArgs(callExpr, 3)
445
- break
451
+ case '_GET_DATA_BETWEEN_TIMES':
452
+ case '_RAMP':
453
+ case '_XIDZ':
454
+ validateCallArgs(callExpr, 3)
455
+ break
446
456
 
447
- case '_PULSE_TRAIN':
448
- validateCallArgs(callExpr, 4)
449
- break
457
+ case '_PULSE_TRAIN':
458
+ validateCallArgs(callExpr, 4)
459
+ break
450
460
 
451
- case '_VECTOR_SELECT':
452
- validateCallArgs(callExpr, 5)
453
- break
461
+ case '_VECTOR_SELECT':
462
+ validateCallArgs(callExpr, 5)
463
+ break
454
464
 
455
- //
456
- //
457
- // Complex functions...
458
- //
459
- //
460
-
461
- case '_ACTIVE_INITIAL':
462
- validateCallDepth(callExpr, context)
463
- validateCallArgs(callExpr, 2)
464
- v.hasInitValue = true
465
- // The 2nd argument is used at init time
466
- argModes[1] = 'init'
467
- break
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
- case '_ALLOCATE_AVAILABLE':
470
- validateCallDepth(callExpr, context)
471
- validateCallArgs(callExpr, 3)
472
- break
479
+ case '_ALLOCATE_AVAILABLE':
480
+ validateCallDepth(callExpr, context)
481
+ validateCallArgs(callExpr, 3)
482
+ break
473
483
 
474
- case '_DELAY1':
475
- case '_DELAY1I':
476
- case '_DELAY3':
477
- case '_DELAY3I':
478
- validateCallArgs(callExpr, callExpr.fnId.endsWith('I') ? 3 : 2)
479
- addFnReference = false
480
- visitArgs = false
481
- generateDelayVariables(v, callExpr, context)
482
- break
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
- case '_DELAY_FIXED':
485
- validateCallDepth(callExpr, context)
486
- validateCallArgs(callExpr, 3)
487
- v.varType = 'level'
488
- v.varSubtype = 'fixedDelay'
489
- v.hasInitValue = true
490
- v.fixedDelayVarName = canonicalName(newFixedDelayVarName())
491
- // The 2nd and 3rd arguments are used at init time
492
- argModes[1] = 'init'
493
- argModes[2] = 'init'
494
- break
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
- case '_DEPRECIATE_STRAIGHTLINE':
497
- validateCallDepth(callExpr, context)
498
- validateCallArgs(callExpr, 4)
499
- v.varSubtype = 'depreciation'
500
- v.hasInitValue = true
501
- v.depreciationVarName = canonicalName(newDepreciationVarName())
502
- // The 2nd and 3rd arguments are used at init time
503
- // TODO: The 3rd (fisc) argument is not currently supported
504
- // TODO: Shouldn't the last (init) argument be marked as 'init' here? (It's
505
- // not treated as 'init' in the legacy reader.)
506
- argModes[1] = 'init'
507
- argModes[2] = 'init'
508
- break
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
- case '_GAME':
511
- validateCallDepth(callExpr, context)
512
- validateCallArgs(callExpr, 1)
513
- generateGameVariables(v, callExpr, context)
514
- break
520
+ case '_GAME':
521
+ validateCallDepth(callExpr, context)
522
+ validateCallArgs(callExpr, 1)
523
+ generateGameVariables(v, callExpr, context)
524
+ break
515
525
 
516
- case '_GET_DIRECT_CONSTANTS': {
517
- validateCallDepth(callExpr, context)
518
- validateCallArgs(callExpr, 3)
519
- validateCallArgType(callExpr, 0, 'string')
520
- validateCallArgType(callExpr, 1, 'string')
521
- validateCallArgType(callExpr, 2, 'string')
522
- addFnReference = false
523
- v.varType = 'const'
524
- v.directConstArgs = {
525
- file: callExpr.args[0].text,
526
- tab: callExpr.args[1].text,
527
- startCell: callExpr.args[2].text
528
- }
529
- break
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
- case '_GET_DIRECT_DATA':
533
- case '_GET_DIRECT_LOOKUPS':
534
- validateCallDepth(callExpr, context)
535
- validateCallArgs(callExpr, 4)
536
- validateCallArgType(callExpr, 0, 'string')
537
- validateCallArgType(callExpr, 1, 'string')
538
- validateCallArgType(callExpr, 2, 'string')
539
- validateCallArgType(callExpr, 3, 'string')
540
- addFnReference = false
541
- v.varType = 'data'
542
- v.directDataArgs = {
543
- file: callExpr.args[0].text,
544
- tab: callExpr.args[1].text,
545
- timeRowOrCol: callExpr.args[2].text,
546
- startCell: callExpr.args[3].text
547
- }
548
- break
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
- case '_IF_THEN_ELSE':
551
- validateCallArgs(callExpr, 3)
552
- addFnReference = false
553
- break
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
- case '_INITIAL':
556
- validateCallDepth(callExpr, context)
557
- validateCallArgs(callExpr, 1)
558
- v.varType = 'initial'
559
- v.hasInitValue = true
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
- case '_INTEG':
565
- validateCallDepth(callExpr, context)
566
- validateCallArgs(callExpr, 2)
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
- case '_NPV':
574
- validateCallArgs(callExpr, 4)
575
- addFnReference = false
576
- visitArgs = false
577
- generateNpvVariables(v, callExpr, context)
578
- break
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
- case '_SAMPLE_IF_TRUE':
581
- validateCallDepth(callExpr, context)
582
- validateCallArgs(callExpr, 3)
583
- v.hasInitValue = true
584
- // The 3rd argument is used at init time
585
- argModes[2] = 'init'
586
- break
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
- case '_SMOOTH':
589
- case '_SMOOTHI':
590
- case '_SMOOTH3':
591
- case '_SMOOTH3I':
592
- validateCallArgs(callExpr, callExpr.fnId.endsWith('I') ? 3 : 2)
593
- addFnReference = false
594
- visitArgs = false
595
- generateSmoothVariables(v, callExpr, context)
596
- break
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
- case '_TREND':
599
- validateCallArgs(callExpr, 3)
600
- addFnReference = false
601
- visitArgs = false
602
- generateTrendVariables(v, callExpr, context)
603
- break
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
- case '_WITH_LOOKUP':
606
- validateCallDepth(callExpr, context)
607
- validateCallArgs(callExpr, 2)
608
- generateLookup(v, callExpr, context)
609
- break
736
+ case '_IF_THEN_ELSE':
737
+ validateCallArgs(callExpr, 3)
738
+ addFnReference = false
739
+ break
610
740
 
611
- default: {
612
- // See if the function name is actually the name of a lookup variable. For Vensim
613
- // models, the antlr4-vensim grammar has separate definitions for lookup calls and
614
- // function calls, but in practice they can only be differentiated in the case
615
- // where the lookup has subscripts; when there are no subscripts, they get treated
616
- // like normal function calls, and in that case we will end up here. If we find
617
- // a variable with the given name, then we will assume it's a lookup call, otherwise
618
- // we treat it as a call of an unimplemented function.
619
- const varId = callExpr.fnId.toLowerCase()
620
- const referencedVar = Model.varWithName(varId)
621
- if (referencedVar === undefined || referencedVar.parsedEqn.rhs.kind !== 'lookup') {
622
- // Throw an error if the function is not yet implemented in SDE
623
- // TODO: This will report false positives in the case of user-defined macros. For now
624
- // we provide the ability to turn off this check via an environment variable, but we
625
- // should consider providing a way for the user to declare the names of any user-defined
626
- // macros so that we can skip this check when those macros are detected.
627
- if (process.env.SDE_REPORT_UNSUPPORTED_FUNCTIONS !== '0') {
628
- const msg = `Unhandled function '${callExpr.fnId}' in readEquations for '${v.modelLHS}'`
629
- if (process.env.SDE_REPORT_UNSUPPORTED_FUNCTIONS === 'warn') {
630
- console.warn(`WARNING: ${msg}`)
631
- } else {
632
- throw new Error(msg)
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 (callExpr.args.length !== expectedArgCount) {
729
- throw new Error(
730
- `Expected '${callExpr.fnName}' function call to have ${expectedArgCount} arguments but got ${callExpr.args.length} `
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, get all combinations of the LHS subscripts that map to the subscripts/dimensions
782
- // in the RHS variable reference. For example:
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 an array of strings representing the possible subset of combinations, like this:
786
- // _a1,_c2,_b1
787
- // _a1,_c2,_b2
788
- // _a2,_c2,_b1
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
- // - get all combinations of RHS subscripts that can be accepted by that RHS instance
793
- // (build an array of strings, e.g., ['_a1,_c1,_b1', '_a1,_c1,_b1', ...])
794
- // - see if any of the LHS subscript combos match any of the RHS subscript combos; if
795
- // so, then add the RHS `refId` to the array of variables referenced by the LHS
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: Get all combinations of the LHS subscripts that map to the subscripts/dimensions
827
- // in the RHS variable reference. Here `rhsSubIds` is the array of parsed subscript/dimension
828
- // IDs that appear in the RHS variable reference. We figure out which LHS subscripts/dimensions
829
- // are relevant for the RHS subscripts/dimensions given the context of the LHS variable (which
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: Build an array of mapped LHS subscript combos (one string of comma-separated
836
- // subscript IDs for each combo)
837
- const mappedLhsSubIdsPerPosition = mappedLhsSubIds.map(indexNamesForSubscript)
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
+ }