@sdeverywhere/compile 0.7.10 → 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.
@@ -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
+ }
@@ -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
- this.modelLHS = eqnCtx ? eqnCtx.lhs().getText() : ''
7
- this.modelFormula = this.formula(eqnCtx)
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.
@@ -3,11 +3,14 @@
3
3
  import path from 'path'
4
4
  import B from 'bufx'
5
5
 
6
+ import { parseVensimModel } from '@sdeverywhere/parse'
7
+
6
8
  import { readXlsx } from './_shared/helpers.js'
7
9
  import { readDat } from './_shared/read-dat.js'
8
10
  import { printSubscripts, yamlSubsList } from './_shared/subscript.js'
9
- import { parseModel } from './parse/parser.js'
11
+ import { parseModel as legacyParseVensimModel } from './parse/parser.js'
10
12
  import Model from './model/model.js'
13
+ import { getDirectSubscripts } from './model/read-subscripts.js'
11
14
  import { generateCode } from './generate/code-gen.js'
12
15
 
13
16
  /**
@@ -15,24 +18,25 @@ import { generateCode } from './generate/code-gen.js'
15
18
  *
16
19
  * This is the primary entrypoint for the `sde generate` command.
17
20
  *
18
- * - If `operation` is 'generateC', the generated C code will be written to `buildDir`.
19
- * - If `operation` is 'printVarList', variables and subscripts will be written to
21
+ * - If `operations` has 'generateC', the generated C code will be written to `buildDir`.
22
+ * - If `operations` has 'printVarList', variables and subscripts will be written to
20
23
  * txt, yaml, and json files under `buildDir`.
21
- * - If `operation` is 'printRefIdTest', reference identifiers will be printed to the console.
22
- * - If `operation` is 'convertNames', no output will be generated, but the results of model
24
+ * - If `operation` has 'printRefIdTest', reference identifiers will be printed to the console.
25
+ * - If `operation` has 'convertNames', no output will be generated, but the results of model
23
26
  * analysis will be available.
24
27
  *
25
28
  * @param input The preprocessed Vensim model text.
26
29
  * @param spec The model spec (from the JSON file).
27
- * @param operation Either 'generateC', 'printVarList', 'printRefIdTest', 'convertNames',
28
- * or empty string.
30
+ * @param operations The set of operations to perform; can include 'generateC', 'printVarList',
31
+ * 'printRefIdTest', 'convertNames'. If the array is empty, the model will be read but no
32
+ * operation will be performed.
29
33
  * @param modelDirname The absolute path to the directory containing the mdl file.
30
34
  * The dat and xlsx files referenced by the spec will be relative to this directory.
31
35
  * @param modelName The model name (without the mdl extension).
32
36
  * @param buildDir The output directory where the C or list files will be written.
33
37
  * @return A string containing the generated C code.
34
38
  */
35
- export async function parseAndGenerate(input, spec, operation, modelDirname, modelName, buildDir) {
39
+ export async function parseAndGenerate(input, spec, operations, modelDirname, modelName, buildDir) {
36
40
  // Read time series from external DAT files into a single object.
37
41
  // externalDatfiles is an array of either filenames or objects
38
42
  // giving a variable name prefix as the key and a filename as the value.
@@ -63,20 +67,20 @@ export async function parseAndGenerate(input, spec, operation, modelDirname, mod
63
67
  }
64
68
 
65
69
  // Parse the model and generate code.
66
- let parseTree = parseModel(input)
67
- let code = generateCode(parseTree, { spec, operation, extData, directData, modelDirname })
70
+ let parsedModel = parseModel(input, modelDirname)
71
+ let code = generateCode(parsedModel, { spec, operations, extData, directData, modelDirname })
68
72
 
69
73
  function writeOutput(filename, text) {
70
74
  let outputPathname = path.join(buildDir, filename)
71
75
  B.write(text, outputPathname)
72
76
  }
73
77
 
74
- if (operation === 'generateC') {
78
+ if (operations.includes('generateC')) {
75
79
  // Write the generated C to a file
76
80
  writeOutput(`${modelName}.c`, code)
77
81
  }
78
82
 
79
- if (operation === 'printVarList') {
83
+ if (operations.includes('printVarList')) {
80
84
  // Write variables to a text file.
81
85
  writeOutput(`${modelName}_vars.txt`, Model.printVarList())
82
86
  // Write subscripts to a text file.
@@ -114,3 +118,52 @@ export function printNames(namesPathname, operation) {
114
118
  }
115
119
  B.printBuf()
116
120
  }
121
+
122
+ /**
123
+ * Read and parse the given model text and return the parsed model structure.
124
+ *
125
+ * TODO: Fix return type
126
+ *
127
+ * @param {string} input The string containing the model text.
128
+ * @param {string} modelDir The absolute path to the directory containing the mdl file.
129
+ * The dat, xlsx, and csv files referenced by the model will be relative to this directory.
130
+ * @param {boolean} sort Whether to sort definitions alphabetically in the preprocess step.
131
+ * @return {*} A parsed tree representation of the model.
132
+ */
133
+ export function parseModel(input, modelDir, sort = false) {
134
+ if (process.env.SDE_NONPUBLIC_USE_NEW_PARSE !== '1') {
135
+ // Use the legacy parser
136
+ return {
137
+ kind: 'vensim-legacy',
138
+ parseTree: legacyParseVensimModel(input)
139
+ }
140
+ }
141
+
142
+ // Prepare the parse context that provides access to external data files
143
+ let parseContext /*: VensimParseContext*/
144
+ if (modelDir) {
145
+ parseContext = {
146
+ getDirectSubscripts(fileName, tabOrDelimiter, firstCell, lastCell /*, prefix*/) {
147
+ // Resolve the CSV file relative the model directory
148
+ const csvPath = path.resolve(modelDir, fileName)
149
+
150
+ // Read the subscripts from the CSV file
151
+ return getDirectSubscripts(csvPath, tabOrDelimiter, firstCell, lastCell)
152
+ }
153
+ }
154
+ }
155
+
156
+ // Parse the model
157
+ // TODO: The `parseVensimModel` function currently implicitly runs the preprocess
158
+ // step on the input text. We should make this configurable (because `parseModel`
159
+ // is currently called after the legacy preprocessor has already been run).
160
+ // TODO: We currently sort the preprocessed definitions alphabetically for
161
+ // compatibility with the legacy preprocessor. Once we drop the legacy code
162
+ // we could remove this step and update the tests to use the original order.
163
+ const root = parseVensimModel(input, parseContext, sort)
164
+
165
+ return {
166
+ kind: 'vensim',
167
+ root
168
+ }
169
+ }