@sdeverywhere/compile 0.7.20 → 0.7.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -4
- package/src/_shared/helpers.js +8 -39
- package/src/_shared/subscript.js +1 -23
- package/src/generate/gen-code-c.js +10 -15
- package/src/generate/gen-code-js.js +8 -3
- package/src/generate/gen-const-list.js +9 -19
- package/src/generate/gen-equation.js +114 -79
- package/src/generate/gen-expr.js +67 -24
- package/src/generate/gen-lookup-from-points.js +17 -4
- package/src/generate/loop-index-vars.js +12 -0
- package/src/index.js +5 -1
- package/src/model/model.js +2 -6
- package/src/model/read-equation-fn-delay.js +55 -29
- package/src/model/read-equation-fn-game.js +2 -2
- package/src/model/read-equation-fn-npv.js +10 -3
- package/src/model/read-equation-fn-smooth.js +15 -8
- package/src/model/read-equation-fn-trend.js +6 -2
- package/src/model/read-equation-fn-with-lookup.js +1 -1
- package/src/model/read-equations.js +273 -119
- package/src/model/read-variables.js +13 -21
- package/src/model/toposort.js +1 -1
- package/src/parse-and-generate.js +10 -15
- package/src/preprocess/preprocessor.js +0 -247
|
@@ -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
|
|
108
|
-
*
|
|
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 {
|
|
109
|
+
* @param {string[]} eqnStrings An array of individual equation strings in Vensim format.
|
|
111
110
|
*/
|
|
112
|
-
|
|
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
|
|
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
|
-
*
|
|
267
|
+
* Visit a RHS variable reference and add the referenced variable instance(s) to the
|
|
268
|
+
* set of references.
|
|
264
269
|
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
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
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
//
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
//
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
const
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
*
|
|
710
|
-
*
|
|
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
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
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,
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
-
//
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
//
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
//
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
//
|
|
765
|
-
|
|
766
|
-
//
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
101
|
-
|
|
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(
|
|
98
|
+
exceptSubIdSets.push(exceptSubRefs.map(ref => ref.subId))
|
|
110
99
|
}
|
|
111
100
|
}
|
|
112
101
|
|
|
113
|
-
//
|
|
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
|
|
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
|
|
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.
|
package/src/model/toposort.js
CHANGED
|
@@ -42,7 +42,7 @@ function toposort(nodes, edges) {
|
|
|
42
42
|
} catch (e) {
|
|
43
43
|
nodeRep = ''
|
|
44
44
|
}
|
|
45
|
-
throw new Error('Found cyclic dependency during toposort:\n' + [...predecessors].join(' →\n') + nodeRep)
|
|
45
|
+
throw new Error('Found cyclic dependency during toposort:\n' + [...predecessors].join(' →\n') + ' →' + nodeRep)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
if (!nodesHash.has(node)) {
|
|
@@ -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
|
|