@sdeverywhere/compile 0.7.20 → 0.7.21

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,18 +1,8 @@
1
- import * as R from 'ramda'
2
-
3
1
  import { parseVensimModel } from '@sdeverywhere/parse'
4
2
 
5
3
  import { canonicalName, cartesianProductOf, newDepreciationVarName, newFixedDelayVarName } from '../_shared/helpers.js'
6
4
 
7
- import {
8
- extractMarkedDims,
9
- indexNamesForSubscript,
10
- isDimension,
11
- isIndex,
12
- normalizeSubscripts,
13
- separatedVariableIndex,
14
- sub
15
- } from '../_shared/subscript.js'
5
+ import { hasMapping, indexNamesForSubscript, isDimension, isIndex, sub } from '../_shared/subscript.js'
16
6
 
17
7
  import Model from './model.js'
18
8
  import { generateDelayVariables } from './read-equation-fn-delay.js'
@@ -104,25 +94,39 @@ class Context {
104
94
  }
105
95
 
106
96
  /**
107
- * Define a new variable with the given equation. This will add the variable to the `Model`
108
- * and then perform the same `readEquation` step that is applied to all other regular variables.
97
+ * Define new variables for the given equations that are generated at compile time. This will
98
+ * add the variables for the given equations to the `Model` first, and after they are added,
99
+ * it will perform the* same `readEquation` step on each that is applied to all other regular
100
+ * variables.
101
+ *
102
+ * NOTE: In the case where multiple equations are generated at compile time for the purposes
103
+ * of implementing a complex function (e.g., `DELAY3`), this should be called once only after
104
+ * the equation text for those generated variables is known. This ensures that all variables
105
+ * are defined in the model before `readEquation` performs further processing, similar to the
106
+ * process we use when reading the original model (we first call `readVariables` on all
107
+ * model variable definitions before calling `readEquation` on each).
109
108
  *
110
- * @param {*} eqnText The equation in Vensim format.
109
+ * @param {string[]} eqnStrings An array of individual equation strings in Vensim format.
111
110
  */
112
- defineVariable(eqnText) {
111
+ defineVariables(eqnStrings) {
113
112
  // Parse the equation text
113
+ const eqnText = eqnStrings.join('\n')
114
114
  const parsedModel = { kind: 'vensim', root: parseVensimModel(eqnText) }
115
115
 
116
- // Create one or more `Variable` instances from the equation
116
+ // Create one or more `Variable` instances from the equations
117
117
  const vars = readVariables(parsedModel)
118
118
 
119
119
  // Add the variables to the `Model`
120
120
  vars.forEach(v => Model.addVariable(v))
121
121
 
122
+ // Define the refId for each variable. Note that the refIds for all added variables
123
+ // need to be defined before we proceed to the next step that calls `readEquation`
124
+ // for each variable.
122
125
  vars.forEach(v => {
123
- // Define the refId for the variable
124
126
  v.refId = Model.refIdForVar(v)
127
+ })
125
128
 
129
+ vars.forEach(v => {
126
130
  // Process each variable using the same process as above
127
131
  readEquation(v)
128
132
 
@@ -260,11 +264,29 @@ function visitExpr(v, expr, context) {
260
264
  }
261
265
 
262
266
  /**
263
- * TODO: Docs
267
+ * Visit a RHS variable reference and add the referenced variable instance(s) to the
268
+ * set of references.
264
269
  *
265
- * @param {*} v
266
- * @param {*} varRefExpr
267
- * @param {*} context
270
+ * If the referenced variable does not have subscripts, or is an apply-to-all variable,
271
+ * this will add a single reference:
272
+ *
273
+ * Example 1: RHS variable has no subscripts (`_y` has a single reference to `_x`)
274
+ * y = x ~~|
275
+ *
276
+ * Example 2: RHS variable is apply-to-all (`_y` has a single reference to `_x`)
277
+ * x[DimA] = 1 ~~|
278
+ * y[DimA] = x[DimA] ~~|
279
+ *
280
+ * If the referenced variable is a non-apply-to-all (separated) variable, this will
281
+ * add one reference for each variable instance:
282
+ *
283
+ * Example 3: RHS variable is non-apply-to-all (`_y` has references to `_x[_a1]`, `_x[_a2]`, etc)
284
+ * x[DimA] = 1, 2 ~~|
285
+ * y[DimA] = x[DimA] ~~|
286
+ *
287
+ * @param {*} v The parsed variable.
288
+ * @param {*} varRefExpr The variable reference that appears on the RHS of the `v` equation.
289
+ * @param {*} context The read context.
268
290
  */
269
291
  function visitVariableRef(v, varRefExpr, context) {
270
292
  // Mark the RHS as non-constant, since it has a variable reference
@@ -282,15 +304,13 @@ function visitVariableRef(v, varRefExpr, context) {
282
304
  // Determine whether to add references to specific refIds (in the case of separated
283
305
  // non-apply-to-all variables) or just a single base refId (in the case of non-subscripted
284
306
  // or apply-to-all variables)
285
- const baseRefId = varRefExpr.varId
286
- const subIds = varRefExpr.subscriptRefs?.map(subRef => subRef.subId) || []
287
- const expandedRefIds = expandedRefIdsForVar(v, baseRefId, subIds)
288
- if (expandedRefIds.length > 0) {
289
- // Add a reference to each instance of the non-apply-to-all variable
290
- expandedRefIds.forEach(refId => context.addVarReference(refId))
291
- } else {
292
- // Add the single variable refId to the list of referenced variables
293
- context.addVarReference(baseRefId)
307
+ const rhsBaseRefId = varRefExpr.varId
308
+ const rhsSubIds = varRefExpr.subscriptRefs?.map(subRef => subRef.subId) || []
309
+
310
+ // Record each instance of the referenced variable
311
+ const expandedRefIds = expandedRefIdsForVar(v, rhsBaseRefId, rhsSubIds)
312
+ for (const refId of expandedRefIds) {
313
+ context.addVarReference(refId)
294
314
  }
295
315
  }
296
316
 
@@ -638,26 +658,42 @@ function visitFunctionCall(v, callExpr, context) {
638
658
  // XXX: For `WITH LOOKUP` calls, only process the first argument; need to generalize this
639
659
  break
640
660
  } else if (callExpr.fnId === '_ALLOCATE_AVAILABLE' && index === 1) {
641
- // XXX: Handle `ALLOCATE AVAILABLE` calls specially for now. This logic is copied from the
642
- // legacy reader, but we may want to revisit later.
643
- // Reference the second and third elements of the priority profile argument instead of the
644
- // first one that Vensim requires for ALLOCATE AVAILABLE. This is required to pick up
645
- // correct dependencies.
661
+ // Handle the second (`pp` or priority profile) argument of `ALLOCATE AVAILABLE` calls
662
+ // specially. An example call with a 2D `pp` looks like this:
663
+ // shipments[branch] = ALLOCATE AVAILABLE(demand[branch], priority[branch,ptype], avail) ~~|
664
+ // Or a 3D `pp` with a dimension:
665
+ // shipments[item,branch] = ALLOCATE AVAILABLE(demand[branch], priority[item,branch,ptype], avail) ~~|
666
+ // Or a 3D `pp` with a specific subscript:
667
+ // shipments[branch] = ALLOCATE AVAILABLE(demand[branch], priority[item1,branch,ptype], avail) ~~|
668
+ // Vensim requires passing a reference with `ptype` as the last subscript, but the function
669
+ // implementation uses the `ppriority` and `pwidth` values (the `ptype` is currently assumed
670
+ // to be 3). Therefore we need to add references to all variants of the variable, not just
671
+ // the ones for `ptype`.
646
672
  if (argExpr.kind !== 'variable-ref') {
647
673
  throw new Error(`ALLOCATE AVAILABLE argument 'pp' must be a variable reference`)
648
674
  }
649
- const baseRefId = argExpr.varId
650
- const subIds = argExpr.subscriptRefs?.map(subRef => subRef.subId) || []
651
- const expandedRefIds = expandedRefIdsForVar(v, baseRefId, subIds)
652
- const ptypeRefId = expandedRefIds[0]
653
- const { subscripts } = Model.splitRefId(ptypeRefId)
654
- const ptypeIndexName = subscripts[1]
655
- const profileElementsDimName = sub(ptypeIndexName).family
656
- const profileElementsDim = sub(profileElementsDimName)
657
- const priorityRefId = ptypeRefId.replace(ptypeIndexName, profileElementsDim.value[1])
658
- const widthRefId = ptypeRefId.replace(ptypeIndexName, profileElementsDim.value[2])
659
- context.addVarReference(priorityRefId)
660
- context.addVarReference(widthRefId)
675
+ // TODO: Throw an error if the last dimension of arg0 does not match last dimension of LHS
676
+ // TODO: Throw an error if the second-to-last dimension of arg1 does not match last dimension of LHS
677
+ // TODO: Throw an error if the last subscript of arg1 does not have the "shape" of a `ppriority` dimension
678
+ // TODO: Throw an error if the `ptype` value is not 3
679
+ // Get the RHS subscript/dimension IDs
680
+ const rhsVarBaseRefId = argExpr.varId
681
+ const rhsVarSubIds = argExpr.subscriptRefs?.map(subRef => subRef.subId) || []
682
+ // Extract the `ptype` subscript and get its parent dimension/family ID
683
+ const ptypeSubId = rhsVarSubIds[rhsVarSubIds.length - 1]
684
+ const profileDimId = sub(ptypeSubId).family
685
+ const profileDim = sub(profileDimId)
686
+ // Get all refIds for the referenced variable for each of the `profile` subscripts
687
+ for (const profileSubId of profileDim.value) {
688
+ // Replace the last `ptype` subscript with the ID of the parent dimension so that
689
+ // `expandedRefIdsForVar` will return refIds for that last subscript
690
+ rhsVarSubIds[rhsVarSubIds.length - 1] = profileSubId
691
+ const expandedRefIds = expandedRefIdsForVar(v, rhsVarBaseRefId, rhsVarSubIds)
692
+ // Record each instance of the referenced variable
693
+ for (const refId of expandedRefIds) {
694
+ context.addVarReference(refId)
695
+ }
696
+ }
661
697
  continue
662
698
  }
663
699
 
@@ -706,89 +742,207 @@ function validateCallArgType(callExpr, index, expectedKind) {
706
742
  }
707
743
 
708
744
  /**
709
- * When an equation references a non-apply-to-all array, add its subscripts to the array
710
- * var's refId.
745
+ * Return an array of `refId`s for a variable that is referenced on the RHS of the
746
+ * equation for `lhsVariable`.
747
+ *
748
+ * If the referenced variable is non-subscripted or is apply-to-all, this will return
749
+ * an array with a single `refId`.
750
+ *
751
+ * If the referenced variable is non-apply-to-all (i.e., it has more than one instance),
752
+ * this will return an array of `refId`s that are relevant for the given `lhsVariable`.
711
753
  *
712
- * XXX: This is largely copied from the legacy `equation-reader.js` and modified to work
713
- * with the AST instead of directly depending on antlr4-vensim constructs. This is pretty
714
- * complex so we should try to refactor or at least add some more fine-grained unit tests
715
- * for it.
754
+ * @param {*} lhsVariable The LHS variable instance that has an equation that references
755
+ * the given `rhsBaseVarRefId` variable on the RHS.
756
+ * @param {string} rhsBaseRefId The base variable ID of the variable referenced on the RHS.
757
+ * @param {string[]} rhsSubIds The array of parsed subscript/dimension IDs that are included
758
+ * in the RHS variable reference.
716
759
  */
717
- function expandedRefIdsForVar(lhsVariable, baseRefId, subscripts) {
718
- // Remove dimension subscripts marked with ! and save them for later.
719
- let markedDims = extractMarkedDims(subscripts)
720
- subscripts = normalizeSubscripts(subscripts)
721
- // console.error(`${this.var.refId} → ${this.refId} [ ${subscripts} ]`);
722
-
723
- if (subscripts.length === 0) {
724
- return []
760
+ function expandedRefIdsForVar(lhsVariable, rhsBaseRefId, rhsSubIds) {
761
+ if (rhsSubIds.length === 0) {
762
+ // The RHS reference is to non-subscripted variable, so return a single `refId`
763
+ return [rhsBaseRefId]
725
764
  }
726
765
 
727
- // See if this variable is non-apply-to-all. At this point, the refId is just the var name.
728
- // References to apply-to-all variables do not need subscripts since they refer to the whole array.
729
- let expansionFlags = Model.expansionFlags(baseRefId)
730
- if (!expansionFlags) {
731
- // The reference is to a non-subscripted or apply-to-all variable
732
- return []
766
+ // Get all variable instances for the referenced RHS variable
767
+ const rhsVarInstances = Model.varsWithName(rhsBaseRefId)
768
+ if (rhsVarInstances.length === 0) {
769
+ // The referenced variable is unknown; throw an error
770
+ throw new Error(`No variable found for ${rhsBaseRefId}, which was referenced by ${lhsVariable.refId}`)
733
771
  }
734
772
 
735
- // The reference is to a non-apply-to-all variable.
736
- // Find the refIds of the vars that include the indices in the reference.
737
- // Get the vars with the var name of the reference. We will choose from these vars.
738
- let varsWithRefName = Model.varsWithName(baseRefId)
739
-
740
- // The refIds of actual vars containing the indices will accumulate with possible duplicates.
741
- let expandedRefIds = []
742
- let iSub
743
-
744
- // Accumulate an array of lists of the separated index names at each position.
745
- let indexNames = []
746
- for (iSub = 0; iSub < expansionFlags.length; iSub++) {
747
- if (expansionFlags[iSub]) {
748
- // For each index name at the subscript position, find refIds for vars that include the index.
749
- // This process ensures that we generate references to vars that are in the var table.
750
- let indexNamesAtPos
751
- // Use the single index name for a separated variable if it exists.
752
- // But don't do this if the subscript is a marked dimension in a vector function.
753
- let separatedIndexName = separatedVariableIndex(subscripts[iSub], lhsVariable, subscripts)
754
- if (!markedDims.includes(subscripts[iSub]) && separatedIndexName) {
755
- indexNamesAtPos = [separatedIndexName]
756
- } else {
757
- // Generate references to all the indices for the subscript.
758
- indexNamesAtPos = indexNamesForSubscript(subscripts[iSub])
773
+ //
774
+ // At this point we know that there are multiple instances of the referenced variable, so
775
+ // it must be non-apply-to-all. The goal now is to determine which instances (refIds) are
776
+ // relevant for the given `lhsVariable` context.
777
+ //
778
+ // First, get all combinations of the LHS subscripts that map to the subscripts/dimensions
779
+ // in the RHS variable reference. For example:
780
+ // y[DimA,DimB,DimC] :EXCEPT: [DimA,DimB,C1] = x[DimA,DimC,DimB]
781
+ // In this case the `DimC` on the RHS is only "accessed" by `C2` from the LHS, so we would
782
+ // build an array of strings representing the possible subset of combinations, like this:
783
+ // _a1,_c2,_b1
784
+ // _a1,_c2,_b2
785
+ // _a2,_c2,_b1
786
+ // _a2,_c2,_b2
787
+ //
788
+ // Then, for each RHS variable instance:
789
+ // - get all combinations of RHS subscripts that can be accepted by that RHS instance
790
+ // (build an array of strings, e.g., ['_a1,_c1,_b1', '_a1,_c1,_b1', ...])
791
+ // - see if any of the LHS subscript combos match any of the RHS subscript combos; if
792
+ // so, then add the RHS `refId` to the array of variables referenced by the LHS
793
+ //
794
+ // In the following examples, suppose the referenced RHS variable is non-apply-to-all and
795
+ // has two instances:
796
+ // _x[_dima,_c1,_dimb]
797
+ // _x[_dima,_c2,_dimb]
798
+ //
799
+ // Example 1: Suppose we have the following equation, where the LHS variable is apply-to-all:
800
+ // y[DimA,DimB,DimC] = x[DimA,DimC,DimB]
801
+ // In this case, the LHS variable will reference all instances of `x`, so this function will
802
+ // return two refIds:
803
+ // _x[_dima,_c1,_dimb]
804
+ // _x[_dima,_c2,_dimb]
805
+ //
806
+ // Example 2: Suppose we have the following equation, where the LHS variable is apply-to-all,
807
+ // but the RHS reference is for a specific instance:
808
+ // y[DimA,DimB,DimC] = x[DimA,C1,DimB]
809
+ // In this case, the LHS variable will reference only one instance of `x`, so this function
810
+ // will return one refId:
811
+ // _x[_dima,_c1,_dimb]
812
+ //
813
+ // Example 3: Suppose we have the following equation, where the LHS variable is NON-apply-to-all:
814
+ // y[DimA,DimB,DimC] :EXCEPT: [DimA,DimB,C1] = x[DimA,DimC,DimB]
815
+ // The LHS variable will already have been separated into multiple instances (due to the "except"
816
+ // clause), and the specific instance of the LHS variable in this case will be:
817
+ // _y[_dima,_dimb,_c2]
818
+ // In this case, this instance of the LHS variable will reference only one instance of `x`,
819
+ // so this function will return one refId:
820
+ // _x[_dima,_c2,_dimb]
821
+ //
822
+
823
+ // Step 1: Get all combinations of the LHS subscripts that map to the subscripts/dimensions
824
+ // in the RHS variable reference. Here `rhsSubIds` is the array of parsed subscript/dimension
825
+ // IDs that appear in the RHS variable reference. We figure out which LHS subscripts/dimensions
826
+ // are relevant for the RHS subscripts/dimensions given the context of the LHS variable (which
827
+ // may have been separated/expanded).
828
+ const lhsSubRefs = lhsVariable.parsedEqn.lhs.varDef.subscriptRefs
829
+ const lhsSubIds = lhsSubRefs?.map(subRef => subRef.subId) || []
830
+ const mappedLhsSubIds = rhsSubIds.map(rhsSubId => resolveRhsSubOrDim(lhsVariable, lhsSubIds, rhsSubId))
831
+
832
+ // Step 2: Build an array of mapped LHS subscript combos (one string of comma-separated
833
+ // subscript IDs for each combo)
834
+ const mappedLhsSubIdsPerPosition = mappedLhsSubIds.map(indexNamesForSubscript)
835
+ const mappedLhsCombos = cartesianProductOf(mappedLhsSubIdsPerPosition).map(combo => combo.join(','))
836
+
837
+ // Step 3: For each RHS variable instance, get all combinations of RHS subscripts that can
838
+ // be accepted by that particular RHS instance
839
+ const rhsRefIds = []
840
+ for (const rhsVarInstance of rhsVarInstances) {
841
+ // Build RHS subscript combos (one string of comma-separated subscript IDs for each combo)
842
+ const rhsVarInstanceSubIdsPerPosition = rhsVarInstance.subscripts.map(indexNamesForSubscript)
843
+ const rhsCombos = cartesianProductOf(rhsVarInstanceSubIdsPerPosition).map(combo => combo.join(','))
844
+
845
+ // See if any of the LHS subscript combos match any of the RHS subscript combos
846
+ for (const lhsCombo of mappedLhsCombos) {
847
+ if (rhsCombos.includes(lhsCombo)) {
848
+ // There was a match; add the refId and break out of the inner loop
849
+ rhsRefIds.push(rhsVarInstance.refId)
850
+ break
759
851
  }
760
- indexNames.push(indexNamesAtPos)
761
852
  }
762
853
  }
763
854
 
764
- // Flatten the arrays of index names at each position into an array of index name combinations.
765
- let separatedIndices = cartesianProductOf(indexNames)
766
- // Find a separated variable for each combination of indices.
767
- for (let separatedIndex of separatedIndices) {
768
- // Consider each var with the same name as the reference in the equation.
769
- for (let refVar of varsWithRefName) {
770
- let iSeparatedIndex = 0
771
- for (iSub = 0; iSub < expansionFlags.length; iSub++) {
772
- if (expansionFlags[iSub]) {
773
- let refVarIndexNames = indexNamesForSubscript(refVar.subscripts[iSub])
774
- if (refVarIndexNames.length === 0) {
775
- console.error(
776
- `ERROR: no subscript at subscript position ${iSub} for var ${refVar.refId} with subscripts ${refVar.subscripts}`
777
- )
778
- }
779
- if (!refVarIndexNames.includes(separatedIndex[iSeparatedIndex++])) {
780
- break
781
- }
782
- }
783
- }
784
- if (iSub >= expansionFlags.length) {
785
- // All separated index names matched index names in the var, so add it as a reference.
786
- expandedRefIds.push(refVar.refId)
787
- break
855
+ // Return the sorted array of relevant refIds
856
+ // TODO: Sorting is not essential here, but the legacy reader sorted so we will keep that
857
+ // behavior now to avoid invalidating tests. Later we should remove this `sort` call and
858
+ // update the tests accordingly.
859
+ return rhsRefIds.sort()
860
+ }
861
+
862
+ /**
863
+ * Return the LHS dimension or subscript that is associated with the given RHS subscript
864
+ * or dimension ID appearing on the RHS of an equation.
865
+ *
866
+ * @param {*} lhsVariable The LHS variable instance that has an equation that references
867
+ * a variable on the RHS.
868
+ * @param {string[]} lhsSubIds The array of original (parsed) subscript or dimension IDs
869
+ * for the LHS variable definition.
870
+ * @param {string} rhsSubId The dimension or subscript ID appearing in a variable reference
871
+ * on the RHS of an equation.
872
+ * @return A single dimension or subscript ID.
873
+ */
874
+ function resolveRhsSubOrDim(lhsVariable, lhsSubIds, rhsSubId) {
875
+ if (rhsSubId.includes('!')) {
876
+ // The dimension ID at this position is "marked", indicating that the vector function
877
+ // (e.g., `SUM`) should operate over the elements in this dimension. This implies
878
+ // that the LHS depends on all instances of the RHS variable; we will use that RHS
879
+ // dimension so that all subscripts within that dimension are expanded in Step 2.
880
+ return rhsSubId.replace('!', '')
881
+ }
882
+
883
+ if (isIndex(rhsSubId)) {
884
+ // The ID at this position is for a subscript/index, so we will use that directly
885
+ return rhsSubId
886
+ }
887
+
888
+ // The ID at this position is for a dimension; figure out which LHS subscript/dimension
889
+ // is a match. First see if there is an exact match.
890
+ const lhsDimIndex = lhsSubIds.findIndex(lhsSubId => lhsSubId === rhsSubId)
891
+ if (lhsDimIndex >= 0) {
892
+ // There is a match. If the LHS variable is separated, use the separated subscript
893
+ // ID at this position (i.e., the value from the `subscripts` array), otherwise we
894
+ // use the dimension ID at this position.
895
+ return lhsVariable.subscripts[lhsDimIndex]
896
+ }
897
+
898
+ // There isn't an exact match; in this case, find the position of the LHS dimension that
899
+ // has a mapping to the RHS dimension
900
+ const mappedLhsDimIndex = lhsSubIds.findIndex(lhsSubId => hasMapping(rhsSubId, lhsSubId))
901
+ if (mappedLhsDimIndex >= 0) {
902
+ // There is a match. If the LHS variable is separated, use the _mapped_ separated
903
+ // subscript ID at this position (i.e., the value from the `subscripts` array),
904
+ // otherwise we use the _mapped_ dimension ID at this position.
905
+ const mappedLhsSubOrDimId = lhsVariable.subscripts[mappedLhsDimIndex]
906
+ if (isIndex(mappedLhsSubOrDimId)) {
907
+ // Determine the mapped subscript. For example, suppose we have:
908
+ // Dim: (t1-t3) ~~|
909
+ // SubA: (t2-t3) -> SubB ~~|
910
+ // SubB: (t1-t2) -> SubA ~~|
911
+ // y[SubA] = x[SubB] ~~|
912
+ // Note that `y` will be separated. Suppose we are evaluating the first instance
913
+ // of `y`, i.e., `_y[_t2]`. We get the object/metadata for each dimension, which
914
+ // will look like the following (unrelated properties are omitted):
915
+ // lhsDim == {
916
+ // name: '_suba',
917
+ // value: [ '_t2', '_t3' ],
918
+ // family: '_dim',
919
+ // mappings: { _subb: [ '_t2', '_t3' ] }
920
+ // }
921
+ // rhsDim == {
922
+ // name: '_subb',
923
+ // value: [ '_t1', '_t2' ],
924
+ // family: '_dim',
925
+ // mappings: { _suba: [ '_t1', '_t2' ] }
926
+ // }
927
+ // The separated LHS subscript in this case is `_t2`, so we need to figure out
928
+ // the corresponding subscript in the mapped RHS dimension, which is `_t1`.
929
+ const mappedLhsSubId = mappedLhsSubOrDimId
930
+ const mappedLhsDimId = lhsSubIds[mappedLhsDimIndex]
931
+ const lhsDim = sub(mappedLhsDimId)
932
+ const rhsDim = sub(rhsSubId)
933
+ const lhsSubIndex = lhsDim.value.indexOf(mappedLhsSubId)
934
+ if (lhsSubIndex >= 0) {
935
+ return rhsDim.mappings[mappedLhsDimId][lhsSubIndex]
936
+ } else {
937
+ throw new Error(
938
+ `Failed to find mapped LHS subscript ${mappedLhsSubId} for RHS dimension ${rhsSubId} in lhs=${lhsVariable.refId}`
939
+ )
788
940
  }
941
+ } else {
942
+ // TODO: Need to explain this case better
943
+ return rhsSubId
789
944
  }
945
+ } else {
946
+ throw new Error(`Failed to find LHS dimension for RHS dimension ${rhsSubId} in lhs=${lhsVariable.refId}`)
790
947
  }
791
-
792
- // Sort the expandedRefIds and eliminate duplicates.
793
- return R.uniq(expandedRefIds.sort())
794
948
  }
@@ -3,14 +3,7 @@ import * as R from 'ramda'
3
3
  import { toPrettyString } from '@sdeverywhere/parse'
4
4
 
5
5
  import { cartesianProductOf } from '../_shared/helpers.js'
6
- import {
7
- isDimension,
8
- isIndex,
9
- isSubdimension,
10
- normalizeSubscripts,
11
- sub,
12
- subscriptsMatch
13
- } from '../_shared/subscript.js'
6
+ import { isDimension, isIndex, isSubdimension, sub, subscriptsMatch } from '../_shared/subscript.js'
14
7
 
15
8
  import Variable from './variable.js'
16
9
 
@@ -56,12 +49,11 @@ function variablesForEquation(eqn, specialSeparationDims) {
56
49
  const baseVarId = lhs.varId
57
50
  let lhsText
58
51
  if (lhs.subscriptRefs?.length > 0) {
59
- // Note that we use the original order of subscripts here, not the "normalized"
60
- // order as below. (This is how the legacy parser worked, so we will preserve
61
- // that behavior for now.)
52
+ // Get the LHS subscript/dimension names
62
53
  const subNames = lhs.subscriptRefs.map(sub => sub.subName)
63
54
  let exceptPart
64
55
  if (lhs.exceptSubscriptRefSets) {
56
+ // Get the LHS "except" subscript/dimension names
65
57
  const exceptSets = lhs.exceptSubscriptRefSets.map(exceptSubRefs => {
66
58
  const exceptSubNames = exceptSubRefs.map(sub => sub.subName)
67
59
  return `[${exceptSubNames.join(',')}]`
@@ -97,20 +89,20 @@ function variablesForEquation(eqn, specialSeparationDims) {
97
89
  // If the variable is subscripted, expand on the LHS subscripts
98
90
  let expansions = []
99
91
  if (lhs.subscriptRefs?.length > 0) {
100
- // XXX: We use `normalizeSubscripts` here so that we are compatible with
101
- // the legacy parser. It normalizes by putting the subscripts in alphabetical
102
- // order by family. This approach to ordering may lead to issues in cases where
103
- // there are multiple dimensions used that resolve to the same family, so we
104
- // should revisit this.
105
- const subIds = normalizeSubscripts(lhs.subscriptRefs.map(ref => ref.subId))
92
+ // Get the LHS subscript/dimension IDs
93
+ const subIds = lhs.subscriptRefs.map(ref => ref.subId)
106
94
  const exceptSubIdSets = []
107
95
  if (lhs.exceptSubscriptRefSets?.length > 0) {
96
+ // Get the LHS "except" subscript/dimension IDs
108
97
  for (const exceptSubRefs of lhs.exceptSubscriptRefSets) {
109
- exceptSubIdSets.push(normalizeSubscripts(exceptSubRefs.map(ref => ref.subId)))
98
+ exceptSubIdSets.push(exceptSubRefs.map(ref => ref.subId))
110
99
  }
111
100
  }
112
101
 
113
- // Determine which positions we will expand
102
+ // At this point, we need to decide how to deal with each subscript/dimension position,
103
+ // i.e., whether to expand to multiple non-apply-to-all variable instances (that are
104
+ // evaluated independently at runtime), or to have one apply-to-all variable instance
105
+ // (that can be evaluated using loops at runtime)
114
106
  let positionsToExpand
115
107
  if (eqn.rhs.kind === 'const-list') {
116
108
  // For const lists, we expand on all dimensions (unconditionally)
@@ -156,7 +148,7 @@ function variablesForEquation(eqn, specialSeparationDims) {
156
148
  *
157
149
  * TODO: Use correct types here
158
150
  *
159
- * @param {*} subIds The list of subscripts appearing on the LHS in normalized order.
151
+ * @param {*} subIds The list of subscripts appearing on the LHS in the original order.
160
152
  * @param {*} exceptSubIdSets An array of subscript lists from the :EXCEPT: clause.
161
153
  * @param {string[]} separationDims The variable names that need to be separated for this
162
154
  * variable because of circular references.
@@ -209,7 +201,7 @@ function subscriptPositionsToExpand(subIds, exceptSubIdSets, separationDims, rhs
209
201
  * TODO: Use correct types here
210
202
  *
211
203
  * @param {string} baseVarId The canonical base name of the variable.
212
- * @param {*} subIds The list of subscripts appearing on the LHS in normalized order.
204
+ * @param {*} subIds The list of subscripts appearing on the LHS in the original order.
213
205
  * @param {*} exceptSubIdSets An array of subscript lists from the :EXCEPT: clause.
214
206
  * @param {*} positionsToExpand An array of boolean flags, one for each subscript position.
215
207
  * @returns {*} An array of objects containing the `subIds` and `separationDims` for each expansion.
@@ -25,18 +25,19 @@ import { generateCode } from './generate/gen-code.js'
25
25
  * - If `operations` has 'convertNames', no output will be generated, but the results of model
26
26
  * analysis will be available.
27
27
  *
28
- * @param input The preprocessed Vensim model text.
29
- * @param spec The model spec (from the JSON file).
30
- * @param operations The set of operations to perform; can include 'generateC', 'generateJS',
28
+ * @param {string} input The preprocessed Vensim model text.
29
+ * @param {*} spec The model spec (from the JSON file).
30
+ * @param {string[]} operations The set of operations to perform; can include 'generateC', 'generateJS',
31
31
  * 'printVarList', 'printRefIdTest', 'convertNames'. If the array is empty, the model will be
32
32
  * read but no operation will be performed.
33
- * @param modelDirname The absolute path to the directory containing the mdl file.
33
+ * @param {string} modelDirname The absolute path to the directory containing the mdl file.
34
34
  * The dat and xlsx files referenced by the spec will be relative to this directory.
35
- * @param modelName The model name (without the mdl extension).
36
- * @param buildDir The output directory where the C or list files will be written.
35
+ * @param {string} modelName The model name (without the mdl extension).
36
+ * @param {string} buildDir The output directory where the C or list files will be written.
37
+ * @param {string} [varname] The variable name passed to the 'sde causes' command.
37
38
  * @return A string containing the generated C code.
38
39
  */
39
- export async function parseAndGenerate(input, spec, operations, modelDirname, modelName, buildDir) {
40
+ export async function parseAndGenerate(input, spec, operations, modelDirname, modelName, buildDir, varname) {
40
41
  // Read time series from external DAT files into a single object.
41
42
  // externalDatfiles is an array of either filenames or objects
42
43
  // giving a variable name prefix as the key and a filename as the value.
@@ -68,7 +69,7 @@ export async function parseAndGenerate(input, spec, operations, modelDirname, mo
68
69
 
69
70
  // Parse the model and generate code
70
71
  let parsedModel = parseModel(input, modelDirname)
71
- let code = generateCode(parsedModel, { spec, operations, extData, directData, modelDirname })
72
+ let code = generateCode(parsedModel, { spec, operations, extData, directData, modelDirname, varname })
72
73
 
73
74
  function writeOutput(filename, text) {
74
75
  let outputPathname = path.join(buildDir, filename)
@@ -136,7 +137,7 @@ export function printNames(namesPathname, operation) {
136
137
  * @param {string} input The string containing the model text.
137
138
  * @param {string} modelDir The absolute path to the directory containing the mdl file.
138
139
  * The dat, xlsx, and csv files referenced by the model will be relative to this directory.
139
- * @param {Object} options The options that control parsing.
140
+ * @param {Object} [options] The options that control parsing.
140
141
  * @param {boolean} options.sort Whether to sort definitions alphabetically in the preprocess step.
141
142
  * @return {*} A parsed tree representation of the model.
142
143
  */
@@ -156,12 +157,6 @@ export function parseModel(input, modelDir, options) {
156
157
  }
157
158
 
158
159
  // Parse the model
159
- // TODO: The `parseVensimModel` function currently implicitly runs the preprocess
160
- // step on the input text. We should make this configurable (because `parseModel`
161
- // is currently called after the legacy preprocessor has already been run).
162
- // TODO: We currently sort the preprocessed definitions alphabetically for
163
- // compatibility with the legacy preprocessor. Once we drop the legacy code
164
- // we could remove this step and update the tests to use the original order.
165
160
  const sort = options?.sort === true
166
161
  const root = parseVensimModel(input, parseContext, sort)
167
162