@sdeverywhere/compile 0.7.9 → 0.7.11
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 +7 -6
- package/src/_shared/helpers.js +11 -11
- package/src/_shared/subscript.js +5 -0
- package/src/generate/code-gen.js +38 -42
- package/src/generate/direct-data-helpers.js +86 -0
- package/src/generate/equation-gen.js +79 -52
- package/src/generate/expand-var-names.js +115 -0
- package/src/generate/gen-const-list.js +89 -0
- package/src/generate/gen-direct-const.js +91 -0
- package/src/generate/gen-equation.js +273 -0
- package/src/generate/gen-expr.js +851 -0
- package/src/generate/gen-lookup-from-direct.js +105 -0
- package/src/generate/gen-lookup-from-external.js +115 -0
- package/src/generate/gen-lookup-from-points.js +85 -0
- package/src/model/equation-reader.js +32 -26
- package/src/model/model.js +160 -17
- package/src/model/read-equation-fn-delay.js +312 -0
- package/src/model/read-equation-fn-npv.js +67 -0
- package/src/model/read-equation-fn-smooth.js +133 -0
- package/src/model/read-equation-fn-trend.js +44 -0
- package/src/model/read-equation-fn-with-lookup.js +37 -0
- package/src/model/read-equations.js +795 -0
- package/src/model/read-subscripts.js +77 -0
- package/src/model/read-variables.js +264 -0
- package/src/model/reduce-variables.js +166 -0
- package/src/model/subscript-range-reader.js +4 -1
- package/src/model/variable.js +7 -2
- package/src/parse/parser.js +8 -4
- package/src/parse-and-generate.js +65 -12
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import XLSX from 'xlsx'
|
|
2
|
+
|
|
3
|
+
import { readCsv } from '../_shared/helpers.js'
|
|
4
|
+
import { Subscript } from '../_shared/subscript.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read the dimension definitions from the given model.
|
|
8
|
+
*
|
|
9
|
+
* @param {*} parsedModel TODO: Use ParsedVensimModel type here
|
|
10
|
+
*/
|
|
11
|
+
export function readDimensionDefs(parsedModel) {
|
|
12
|
+
// Read and process all dimension definitions from the parsed model
|
|
13
|
+
for (const dimensionDef of parsedModel.root.dimensions) {
|
|
14
|
+
const dimName = dimensionDef.dimName
|
|
15
|
+
const familyName = dimensionDef.familyName
|
|
16
|
+
if (dimensionDef.subscriptRefs.length > 0) {
|
|
17
|
+
// This is a normal dimension definition
|
|
18
|
+
const subNames = dimensionDef.subscriptRefs.map(ref => ref.subName)
|
|
19
|
+
const mappings = dimensionDef.subscriptMappings.map(mapping => {
|
|
20
|
+
// Convert from the AST representation (`SubscriptMapping`) to the structure used
|
|
21
|
+
// by the compiler
|
|
22
|
+
return {
|
|
23
|
+
toDim: mapping.toDimName,
|
|
24
|
+
value: mapping.subscriptRefs.map(ref => ref.subName)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
Subscript(dimName, subNames, familyName, mappings)
|
|
28
|
+
} else {
|
|
29
|
+
// This is an alias (for example, `DimA <-> DimB`)
|
|
30
|
+
// XXX: The legacy Vensim parser set `modelValue` to an empty string (instead of an
|
|
31
|
+
// empty array) in the case of a `<->` alias, so we will do the same here for now.
|
|
32
|
+
// Once we remove the legacy parsing code we could fix the tests to expect an empty
|
|
33
|
+
// array instead of empty string.
|
|
34
|
+
Subscript(dimName, '', familyName, [])
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Read the subscripts for a `GET DIRECT SUBSCRIPT` call in a dimension definition.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} filePath The absolute
|
|
43
|
+
* @param {string} tabOrDelimiter
|
|
44
|
+
* @param {string} firstCell The name of the first cell.
|
|
45
|
+
* @param {string} lastCell The name of the last cell.
|
|
46
|
+
*/
|
|
47
|
+
export function getDirectSubscripts(fileName, tabOrDelimiter, firstCell, lastCell) {
|
|
48
|
+
// If lastCell is a column letter, scan the column, else scan the row
|
|
49
|
+
const dataAddress = XLSX.utils.decode_cell(firstCell.toUpperCase())
|
|
50
|
+
let col = dataAddress.c
|
|
51
|
+
let row = dataAddress.r
|
|
52
|
+
if (col < 0 || row < 0) {
|
|
53
|
+
throw new Error(`Failed to parse 'firstcell' argument for GET DIRECT SUBSCRIPT call: ${firstCell}`)
|
|
54
|
+
}
|
|
55
|
+
let nextCell
|
|
56
|
+
if (isNaN(parseInt(lastCell))) {
|
|
57
|
+
nextCell = () => row++
|
|
58
|
+
} else {
|
|
59
|
+
nextCell = () => col++
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Read subscript names from the CSV file at the given position
|
|
63
|
+
// TODO: We currently only support reading from CSV files, but Vensim also allows for
|
|
64
|
+
// Excel files for `GET DIRECT SUBSCRIPT`, so we should add support for those here
|
|
65
|
+
const data = readCsv(fileName, tabOrDelimiter)
|
|
66
|
+
const subNames = []
|
|
67
|
+
if (data) {
|
|
68
|
+
let subName = data[row][col]
|
|
69
|
+
while (subName != null) {
|
|
70
|
+
subNames.push(subName)
|
|
71
|
+
nextCell()
|
|
72
|
+
subName = data[row] != null ? data[row][col] : null
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return subNames
|
|
77
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import * as R from 'ramda'
|
|
2
|
+
|
|
3
|
+
import { toPrettyString } from '@sdeverywhere/parse'
|
|
4
|
+
|
|
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'
|
|
14
|
+
|
|
15
|
+
import Variable from './variable.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* TODO: Docs
|
|
19
|
+
*
|
|
20
|
+
* @param {*} parsedModel TODO: Use ParsedVensimModel type here
|
|
21
|
+
* @param {Object.<string, string>} [specialSeparationDims] The variable names that need to be
|
|
22
|
+
* separated because of circular references. A mapping from "C" variable name to "C" dimension
|
|
23
|
+
* name to separate on.
|
|
24
|
+
* @returns {*} An array containing all `Variable` instances that were generated from
|
|
25
|
+
* the model equations.
|
|
26
|
+
*/
|
|
27
|
+
export function readVariables(parsedModel, specialSeparationDims) {
|
|
28
|
+
const variables = []
|
|
29
|
+
|
|
30
|
+
// Add one or more `Variable` definitions for each parsed equation
|
|
31
|
+
for (const eqn of parsedModel.root.equations) {
|
|
32
|
+
variables.push(...variablesForEquation(eqn, specialSeparationDims || {}))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return variables
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Process a single parsed equation and return one or more `Variable` definitions
|
|
40
|
+
* that were derived from the given equation.
|
|
41
|
+
*
|
|
42
|
+
* TODO: Types
|
|
43
|
+
*
|
|
44
|
+
* @param {*} eqn The parsed equation.
|
|
45
|
+
* @param {Object.<string, string>} specialSeparationDims The variable names that need to be
|
|
46
|
+
* separated because of circular references.
|
|
47
|
+
* @returns {*} An array containing all `Variable` instances that were generated from
|
|
48
|
+
* the given equation.
|
|
49
|
+
*/
|
|
50
|
+
function variablesForEquation(eqn, specialSeparationDims) {
|
|
51
|
+
// Start a new variable defined by this equation
|
|
52
|
+
const variable = new Variable(null)
|
|
53
|
+
|
|
54
|
+
// Fill in the LHS details
|
|
55
|
+
const lhs = eqn.lhs.varDef
|
|
56
|
+
const baseVarId = lhs.varId
|
|
57
|
+
let lhsText
|
|
58
|
+
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.)
|
|
62
|
+
const subNames = lhs.subscriptRefs.map(sub => sub.subName)
|
|
63
|
+
let exceptPart
|
|
64
|
+
if (lhs.exceptSubscriptRefSets) {
|
|
65
|
+
const exceptSets = lhs.exceptSubscriptRefSets.map(exceptSubRefs => {
|
|
66
|
+
const exceptSubNames = exceptSubRefs.map(sub => sub.subName)
|
|
67
|
+
return `[${exceptSubNames.join(',')}]`
|
|
68
|
+
})
|
|
69
|
+
exceptPart = `:EXCEPT:${exceptSets.join(',')}`
|
|
70
|
+
} else {
|
|
71
|
+
exceptPart = ''
|
|
72
|
+
}
|
|
73
|
+
lhsText = `${lhs.varName}[${subNames.join(',')}]${exceptPart}`
|
|
74
|
+
} else {
|
|
75
|
+
lhsText = lhs.varName
|
|
76
|
+
}
|
|
77
|
+
variable.modelLHS = lhsText
|
|
78
|
+
variable.varName = baseVarId
|
|
79
|
+
|
|
80
|
+
// Fill in the RHS details
|
|
81
|
+
let rhsText
|
|
82
|
+
if (eqn.rhs.kind === 'expr') {
|
|
83
|
+
rhsText = toPrettyString(eqn.rhs.expr, { compact: true })
|
|
84
|
+
} else if (eqn.rhs.kind === 'const-list') {
|
|
85
|
+
rhsText = eqn.rhs.text
|
|
86
|
+
} else {
|
|
87
|
+
rhsText = ''
|
|
88
|
+
if (eqn.rhs.kind === 'data') {
|
|
89
|
+
// The legacy parser sets the variable's varType to 'data' at this stage,
|
|
90
|
+
// so we will do the same
|
|
91
|
+
variable.varType = 'data'
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
variable.modelFormula = rhsText
|
|
95
|
+
variable.parsedEqn = eqn
|
|
96
|
+
|
|
97
|
+
// If the variable is subscripted, expand on the LHS subscripts
|
|
98
|
+
let expansions = []
|
|
99
|
+
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))
|
|
106
|
+
const exceptSubIdSets = []
|
|
107
|
+
if (lhs.exceptSubscriptRefSets?.length > 0) {
|
|
108
|
+
for (const exceptSubRefs of lhs.exceptSubscriptRefSets) {
|
|
109
|
+
exceptSubIdSets.push(normalizeSubscripts(exceptSubRefs.map(ref => ref.subId)))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Determine which positions we will expand
|
|
114
|
+
let positionsToExpand
|
|
115
|
+
if (eqn.rhs.kind === 'const-list') {
|
|
116
|
+
// For const lists, we expand on all dimensions (unconditionally)
|
|
117
|
+
positionsToExpand = subIds.map(subId => isDimension(subId))
|
|
118
|
+
} else {
|
|
119
|
+
// For other equations, look at the different subscripts and the RHS to determine
|
|
120
|
+
// which positions to expand. Note that `specialSeparationDims` in the spec file
|
|
121
|
+
// can be a single string or an array of strings.
|
|
122
|
+
let separationDims = specialSeparationDims[baseVarId] || []
|
|
123
|
+
if (!Array.isArray(separationDims)) {
|
|
124
|
+
separationDims = [separationDims]
|
|
125
|
+
}
|
|
126
|
+
positionsToExpand = subscriptPositionsToExpand(subIds, exceptSubIdSets, separationDims, variable.modelFormula)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Expand on LHS subscripts
|
|
130
|
+
expansions = computeExpansions(baseVarId, subIds, exceptSubIdSets, positionsToExpand)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (expansions.length === 0) {
|
|
134
|
+
// Generate a single variable defined by the equation
|
|
135
|
+
return [variable]
|
|
136
|
+
} else {
|
|
137
|
+
// Generate variables expanded over subscripts to the model
|
|
138
|
+
const variables = []
|
|
139
|
+
for (const expansion of expansions) {
|
|
140
|
+
const v = new Variable(null)
|
|
141
|
+
v.varName = baseVarId
|
|
142
|
+
v.modelLHS = lhsText
|
|
143
|
+
v.modelFormula = rhsText
|
|
144
|
+
v.parsedEqn = eqn
|
|
145
|
+
v.subscripts = expansion.subIds
|
|
146
|
+
v.separationDims = expansion.separationDimIds
|
|
147
|
+
variables.push(v)
|
|
148
|
+
}
|
|
149
|
+
return variables
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Return an array of boolean flags that indicate whether the variable should be
|
|
155
|
+
* expanded over the subscripts for that position.
|
|
156
|
+
*
|
|
157
|
+
* TODO: Use correct types here
|
|
158
|
+
*
|
|
159
|
+
* @param {*} subIds The list of subscripts appearing on the LHS in normalized order.
|
|
160
|
+
* @param {*} exceptSubIdSets An array of subscript lists from the :EXCEPT: clause.
|
|
161
|
+
* @param {string[]} separationDims The variable names that need to be separated for this
|
|
162
|
+
* variable because of circular references.
|
|
163
|
+
* @param {string} rhsText The text of the RHS of the variable definition.
|
|
164
|
+
* @returns {boolean[]} An array of boolean flags, one for each subscript position.
|
|
165
|
+
*/
|
|
166
|
+
function subscriptPositionsToExpand(subIds, exceptSubIdSets, separationDims, rhsText) {
|
|
167
|
+
// Decide whether we need to expand each subscript on the LHS.
|
|
168
|
+
// Construct an array of booleans in each subscript position.
|
|
169
|
+
const expandFlags = new Array(subIds.length).fill(false)
|
|
170
|
+
|
|
171
|
+
for (let i = 0; i < subIds.length; i++) {
|
|
172
|
+
const subId = subIds[i]
|
|
173
|
+
let expand = false
|
|
174
|
+
|
|
175
|
+
// Expand a subdimension and special separation dims in the LHS
|
|
176
|
+
if (isDimension(subId)) {
|
|
177
|
+
expand = isSubdimension(subId)
|
|
178
|
+
if (!expand) {
|
|
179
|
+
expand = separationDims.includes(subId)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!expand) {
|
|
184
|
+
// Direct data vars with subscripts are separated because we generate a lookup for each index
|
|
185
|
+
if (isDimension(subId) && (rhsText.includes('GET DIRECT DATA') || rhsText.includes('GET DIRECT LOOKUPS'))) {
|
|
186
|
+
expand = true
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!expand) {
|
|
191
|
+
// Also expand on exception subscripts that are indices or subdimensions
|
|
192
|
+
for (const exceptSubIds of exceptSubIdSets) {
|
|
193
|
+
expand = isIndex(exceptSubIds[i]) || isSubdimension(exceptSubIds[i])
|
|
194
|
+
if (expand) {
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
expandFlags[i] = expand
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return expandFlags
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Return an array describing the set of variable expansions.
|
|
208
|
+
*
|
|
209
|
+
* TODO: Use correct types here
|
|
210
|
+
*
|
|
211
|
+
* @param {string} baseVarId The canonical base name of the variable.
|
|
212
|
+
* @param {*} subIds The list of subscripts appearing on the LHS in normalized order.
|
|
213
|
+
* @param {*} exceptSubIdSets An array of subscript lists from the :EXCEPT: clause.
|
|
214
|
+
* @param {*} positionsToExpand An array of boolean flags, one for each subscript position.
|
|
215
|
+
* @returns {*} An array of objects containing the `subIds` and `separationDims` for each expansion.
|
|
216
|
+
*/
|
|
217
|
+
function computeExpansions(baseVarId, subIds, exceptSubIdSets, positionsToExpand) {
|
|
218
|
+
// Construct an array with an array at each subscript position. If the subscript is expanded at that position,
|
|
219
|
+
// it will become an array of indices. Otherwise, it remains an index or dimension as a single-valued array.
|
|
220
|
+
const expandedSubIdsPerPosition = []
|
|
221
|
+
const separationDimIds = []
|
|
222
|
+
for (let i = 0; i < subIds.length; i++) {
|
|
223
|
+
const subId = subIds[i]
|
|
224
|
+
let subIdsInExpansion
|
|
225
|
+
if (positionsToExpand[i]) {
|
|
226
|
+
separationDimIds.push(subId)
|
|
227
|
+
if (isDimension(subId)) {
|
|
228
|
+
subIdsInExpansion = sub(subId).value
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
expandedSubIdsPerPosition.push(subIdsInExpansion || [subId])
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Generate an array of fully expanded subscripts, which may be indices or dimensions
|
|
235
|
+
const expandedSubIdSets = cartesianProductOf(expandedSubIdsPerPosition)
|
|
236
|
+
const skipExpansion = expandedSubIds => {
|
|
237
|
+
// Check the subscripts against each set of except subscripts. Skip expansion if one of them matches.
|
|
238
|
+
const subsRange = R.range(0, expandedSubIds.length)
|
|
239
|
+
for (const exceptSubIds of exceptSubIdSets) {
|
|
240
|
+
if (expandedSubIds.length === exceptSubIds.length) {
|
|
241
|
+
if (R.all(i => subscriptsMatch(expandedSubIds[i], exceptSubIds[i]), subsRange)) {
|
|
242
|
+
return true
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
console.error(`WARNING: expandedSubIds length ≠ exceptSubIds length in ${baseVarId}`)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return false
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const expansions = []
|
|
252
|
+
for (const expandedSubIds of expandedSubIdSets) {
|
|
253
|
+
// Skip expansions that match exception subscripts
|
|
254
|
+
if (!skipExpansion(expandedSubIds)) {
|
|
255
|
+
// Add a new expansion
|
|
256
|
+
expansions.push({
|
|
257
|
+
subIds: expandedSubIds,
|
|
258
|
+
separationDimIds
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return expansions
|
|
264
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { reduceConditionals, reduceExpr, toPrettyString } from '@sdeverywhere/parse'
|
|
2
|
+
|
|
3
|
+
import Model from './model.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Process the parsed `Equation` instances associated with all variables in the model and attempt to
|
|
7
|
+
* reduce expressions through constant folding and simplifying trivial operations and function calls.
|
|
8
|
+
*
|
|
9
|
+
* @param {*} variables The array of `Variable` instances.
|
|
10
|
+
* @param {string[]} inputVarIds The array of IDs for the configured input variables, which need to be
|
|
11
|
+
* preserved (will not be reduced/eliminated).
|
|
12
|
+
* @param {'default' | 'aggressive'} mode The reduction method to use.
|
|
13
|
+
*/
|
|
14
|
+
export function reduceVariables(variables, inputVarIds, mode) {
|
|
15
|
+
const baseVarIdForRefId = refId => {
|
|
16
|
+
return refId.split('[')[0]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const refIdForVarRef = varRef => {
|
|
20
|
+
if (varRef.subscriptRefs?.length > 0) {
|
|
21
|
+
const subIds = varRef.subscriptRefs.map(subRef => subRef.subId)
|
|
22
|
+
return `${varRef.varId}[${subIds.join(',')}]`
|
|
23
|
+
} else {
|
|
24
|
+
return varRef.varId
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// TODO: For now, we will look up input variables by base varId (ignoring any subscripts)
|
|
29
|
+
// to avoid any confusion
|
|
30
|
+
const inputBaseVarIdsSet = new Set()
|
|
31
|
+
for (const inputVarId of inputVarIds) {
|
|
32
|
+
inputBaseVarIdsSet.add(baseVarIdForRefId(inputVarId))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Keep track of which variables are currently being reduced in order to detect cycles
|
|
36
|
+
// let currentLhsBaseVarId
|
|
37
|
+
const activelyReducingRefIds = new Set()
|
|
38
|
+
|
|
39
|
+
const reduceVariable = v => {
|
|
40
|
+
if (v.reduced) {
|
|
41
|
+
// The variable has already been visited/reduced
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// For now, only attempt to reduce variables that have an "expr" equation
|
|
46
|
+
// TODO: Handle const lists too
|
|
47
|
+
if (v.parsedEqn?.rhs?.kind !== 'expr') {
|
|
48
|
+
v.reduced = true
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Add this variable to the set of active ones
|
|
53
|
+
// TODO: Allow cycle if the current LHS is referenced on the RHS
|
|
54
|
+
// const baseVarId = v.parsedEqn.lhs.varId
|
|
55
|
+
// if (baseVarId !== currentLhsBaseVarId) {
|
|
56
|
+
if (activelyReducingRefIds.has(v.refId)) {
|
|
57
|
+
throw new Error(`Cycle detected when reducing variables: ${v.refId}`)
|
|
58
|
+
}
|
|
59
|
+
// }
|
|
60
|
+
activelyReducingRefIds.add(v.refId)
|
|
61
|
+
|
|
62
|
+
// We currently have two options for reducing variables. The less aggressive
|
|
63
|
+
// one only reduces conditionals (the default for now, for compatibility with
|
|
64
|
+
// the legacy reader), and the more aggressive one (enabled via environment
|
|
65
|
+
// variable) performs constant folding and simplifies trivial expressions and
|
|
66
|
+
// function calls.
|
|
67
|
+
let reduce
|
|
68
|
+
if (mode === 'aggressive') {
|
|
69
|
+
reduce = reduceExpr
|
|
70
|
+
} else {
|
|
71
|
+
reduce = reduceConditionals
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Reduce the expression
|
|
75
|
+
const parsedRhsExpr = v.parsedEqn.rhs.expr
|
|
76
|
+
const reducedRhsExpr = reduce(parsedRhsExpr, {
|
|
77
|
+
resolveVarRef
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Save reduced RHS as a new `parsedEqn`
|
|
81
|
+
const newEqn = {
|
|
82
|
+
lhs: v.parsedEqn.lhs,
|
|
83
|
+
rhs: {
|
|
84
|
+
kind: 'expr',
|
|
85
|
+
expr: reducedRhsExpr
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
v.parsedEqn = newEqn
|
|
89
|
+
|
|
90
|
+
// TODO: Ideally we would leave the `modelFormula` untouched so that when we generate the
|
|
91
|
+
// comment above the generated code, it would show the original equation instead of the
|
|
92
|
+
// reduced one. But the tests currently rely on `modelFormula` being updated after we
|
|
93
|
+
// reduce the RHS, so we save the original formula to `origModelFormula` and use that when
|
|
94
|
+
// generating the code comment, and save the reduced formula to `modelFormula`.
|
|
95
|
+
const reducedRhsText = toPrettyString(reducedRhsExpr, { compact: true })
|
|
96
|
+
v.origModelFormula = v.modelFormula
|
|
97
|
+
v.modelFormula = reducedRhsText
|
|
98
|
+
|
|
99
|
+
// Set a flag to indicate that the variable has been visited and reduced
|
|
100
|
+
v.reduced = true
|
|
101
|
+
|
|
102
|
+
// Remove this variable from the set of active ones
|
|
103
|
+
activelyReducingRefIds.delete(v.refId)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const resolveVarRef = varRef => {
|
|
107
|
+
// If we look up by refId, it will fail if it's a separated var. If it's a normal
|
|
108
|
+
// unsubscripted or apply-to-all var, then refId lookup should return one var.
|
|
109
|
+
// TODO: This approach needs to be revisited
|
|
110
|
+
const refId = refIdForVarRef(varRef)
|
|
111
|
+
const refVar = Model.varWithRefId(refId)
|
|
112
|
+
if (refVar) {
|
|
113
|
+
// XXX: For now, don't try to reduce variables like this:
|
|
114
|
+
// x[DimA] = 1
|
|
115
|
+
// This is because `generateSmoothVariables` doesn't work well in cases
|
|
116
|
+
// where some arguments have subscripts and some don't. So for example,
|
|
117
|
+
// if the "init" arg is subscripted but is defined as a constant, we would
|
|
118
|
+
// reduce it to a constant, but `generateSmoothVariables` doesn't know how
|
|
119
|
+
// to mix.
|
|
120
|
+
if (refVar.subscripts !== undefined && refVar.subscripts.length > 0) {
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// In "aggressive" mode, attempt to reduce the referenced variable, otherwise
|
|
125
|
+
// use the parsed RHS expression as is
|
|
126
|
+
if (mode === 'aggressive') {
|
|
127
|
+
reduceVariable(refVar)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// If the attempt was successful, the reduced expression will be defined here
|
|
131
|
+
const reducedExpr = refVar.parsedEqn?.rhs?.expr
|
|
132
|
+
if (reducedExpr) {
|
|
133
|
+
// Only substitute if the reduced expression resolves to a simple constant and
|
|
134
|
+
// the variable is not configured as an input (that can override the constant
|
|
135
|
+
// value at runtime). Otherwise we will keep the variable reference intact.
|
|
136
|
+
// TODO: Are there other cases where we should do a substitution? Like when
|
|
137
|
+
// the expression reduces to a single 'variable-ref'?
|
|
138
|
+
const refBaseVarId = baseVarIdForRefId(refId)
|
|
139
|
+
if (reducedExpr.kind === 'number' && !inputBaseVarIdsSet.has(refBaseVarId)) {
|
|
140
|
+
return reducedExpr
|
|
141
|
+
} else {
|
|
142
|
+
return undefined
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
return undefined
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
// We couldn't find the variable by refId, so find all variables with that base varId
|
|
149
|
+
// TODO: Revisit this
|
|
150
|
+
// const refVars = Model.varsWithName(varRef.varId)
|
|
151
|
+
// if (refVars.length === 1) {
|
|
152
|
+
// // When there's a single variable, we can use that
|
|
153
|
+
// reduceVariable(refVars[0])
|
|
154
|
+
// return refVars[0].parsedEqn?.rhs?.expr
|
|
155
|
+
// } else {
|
|
156
|
+
// return undefined
|
|
157
|
+
// }
|
|
158
|
+
return undefined
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Visit each variable and attempt to reduce it
|
|
163
|
+
for (const v of variables) {
|
|
164
|
+
reduceVariable(v)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -115,9 +115,12 @@ export default class SubscriptRangeReader extends ModelReader {
|
|
|
115
115
|
let lastCell = args[3]
|
|
116
116
|
// let prefix = args[4]
|
|
117
117
|
// If lastCell is a column letter, scan the column, else scan the row.
|
|
118
|
-
let dataAddress = XLSX.utils.decode_cell(firstCell)
|
|
118
|
+
let dataAddress = XLSX.utils.decode_cell(firstCell.toUpperCase())
|
|
119
119
|
let col = dataAddress.c
|
|
120
120
|
let row = dataAddress.r
|
|
121
|
+
if (col < 0 || row < 0) {
|
|
122
|
+
throw new Error(`Failed to parse 'firstcell' argument for GET DIRECT SUBSCRIPT call: ${firstCell}`)
|
|
123
|
+
}
|
|
121
124
|
let nextCell
|
|
122
125
|
if (isNaN(parseInt(lastCell))) {
|
|
123
126
|
nextCell = () => row++
|
package/src/model/variable.js
CHANGED
|
@@ -3,8 +3,13 @@ export default class Variable {
|
|
|
3
3
|
// The equation rule context allows us to generate code by visiting the parse tree.
|
|
4
4
|
this.eqnCtx = eqnCtx
|
|
5
5
|
// Save both sides of the equation text in the model for documentation purposes.
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
if (eqnCtx) {
|
|
7
|
+
this.modelLHS = eqnCtx.lhs().getText()
|
|
8
|
+
this.modelFormula = this.formula(eqnCtx)
|
|
9
|
+
} else {
|
|
10
|
+
this.modelLHS = ''
|
|
11
|
+
this.modelFormula = ''
|
|
12
|
+
}
|
|
8
13
|
// An equation defines a variable with a var name, saved in canonical form here.
|
|
9
14
|
this.varName = ''
|
|
10
15
|
// Subscripts are canonical dimension or index names on the LHS in normal order.
|
package/src/parse/parser.js
CHANGED
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
import antlr4 from 'antlr4'
|
|
4
4
|
import { ModelLexer, ModelParser } from 'antlr4-vensim'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} VensimModelParseTree
|
|
8
|
+
*/
|
|
9
|
+
|
|
6
10
|
/**
|
|
7
11
|
* Create a `ModelParser` for the given model text, which can be the
|
|
8
12
|
* contents of an entire `mdl` file, or a portion of one (e.g., an
|
|
9
13
|
* expression or definition).
|
|
10
14
|
*
|
|
11
|
-
* @param input The string containing the model text.
|
|
12
|
-
* @return A `ModelParser` from which a parse tree can be obtained.
|
|
15
|
+
* @param {string} input The string containing the model text.
|
|
16
|
+
* @return {ModelParser} A `ModelParser` from which a parse tree can be obtained.
|
|
13
17
|
*/
|
|
14
18
|
export function createParser(input) {
|
|
15
19
|
let chars = new antlr4.InputStream(input)
|
|
@@ -23,8 +27,8 @@ export function createParser(input) {
|
|
|
23
27
|
/**
|
|
24
28
|
* Read the given model text and return a parse tree.
|
|
25
29
|
*
|
|
26
|
-
* @param input The string containing the model text.
|
|
27
|
-
* @return A parse tree representation of the model.
|
|
30
|
+
* @param {string} input The string containing the model text.
|
|
31
|
+
* @return {VensimModelParseTree} A parse tree representation of the model.
|
|
28
32
|
*/
|
|
29
33
|
export function parseModel(input) {
|
|
30
34
|
let parser = createParser(input)
|