@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.
@@ -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
- 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
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
- // 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
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
- // 3-plus-argument functions...
438
- //
439
- //
444
+ //
445
+ //
446
+ // 3-plus-argument functions...
447
+ //
448
+ //
440
449
 
441
- case '_GET_DATA_BETWEEN_TIMES':
442
- case '_RAMP':
443
- case '_XIDZ':
444
- validateCallArgs(callExpr, 3)
445
- break
450
+ case '_GET_DATA_BETWEEN_TIMES':
451
+ case '_RAMP':
452
+ case '_XIDZ':
453
+ validateCallArgs(callExpr, 3)
454
+ break
446
455
 
447
- case '_PULSE_TRAIN':
448
- validateCallArgs(callExpr, 4)
449
- break
456
+ case '_PULSE_TRAIN':
457
+ validateCallArgs(callExpr, 4)
458
+ break
450
459
 
451
- case '_VECTOR_SELECT':
452
- validateCallArgs(callExpr, 5)
453
- break
460
+ case '_VECTOR_SELECT':
461
+ validateCallArgs(callExpr, 5)
462
+ break
454
463
 
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
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
- case '_ALLOCATE_AVAILABLE':
470
- validateCallDepth(callExpr, context)
471
- validateCallArgs(callExpr, 3)
472
- break
478
+ case '_ALLOCATE_AVAILABLE':
479
+ validateCallDepth(callExpr, context)
480
+ validateCallArgs(callExpr, 3)
481
+ break
473
482
 
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
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
- 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
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
- 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
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
- case '_GAME':
511
- validateCallDepth(callExpr, context)
512
- validateCallArgs(callExpr, 1)
513
- generateGameVariables(v, callExpr, context)
514
- break
519
+ case '_GAME':
520
+ validateCallDepth(callExpr, context)
521
+ validateCallArgs(callExpr, 1)
522
+ generateGameVariables(v, callExpr, context)
523
+ break
515
524
 
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
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
- break
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
- 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
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
- case '_IF_THEN_ELSE':
551
- validateCallArgs(callExpr, 3)
552
- addFnReference = false
553
- break
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
- 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
670
+ //
671
+ //
672
+ // 3-plus-argument functions...
673
+ //
674
+ //
563
675
 
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
676
+ case '_RAMP':
677
+ validateCallArgs(callExpr, 3)
678
+ break
572
679
 
573
- case '_NPV':
574
- validateCallArgs(callExpr, 4)
575
- addFnReference = false
576
- visitArgs = false
577
- generateNpvVariables(v, callExpr, context)
578
- break
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
- 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
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
- 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
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
- case '_TREND':
599
- validateCallArgs(callExpr, 3)
600
- addFnReference = false
601
- visitArgs = false
602
- generateTrendVariables(v, callExpr, context)
603
- break
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
- case '_WITH_LOOKUP':
606
- validateCallDepth(callExpr, context)
607
- validateCallArgs(callExpr, 2)
608
- generateLookup(v, callExpr, context)
609
- break
733
+ case '_IF_THEN_ELSE':
734
+ validateCallArgs(callExpr, 3)
735
+ addFnReference = false
736
+ break
610
737
 
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
- }
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 (callExpr.args.length !== expectedArgCount) {
729
- throw new Error(
730
- `Expected '${callExpr.fnName}' function call to have ${expectedArgCount} arguments but got ${callExpr.args.length} `
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
+ }