@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.
@@ -0,0 +1,105 @@
1
+ import * as R from 'ramda'
2
+ import XLSX from 'xlsx'
3
+
4
+ import { listConcat } from '../_shared/helpers.js'
5
+ import { sub } from '../_shared/subscript.js'
6
+
7
+ import { handleExcelOrCsvFile } from './direct-data-helpers.js'
8
+
9
+ /**
10
+ * Generate code for a variable that uses `GET DIRECT DATA` to source data from an external file
11
+ * (in CSV or Excel format).
12
+ *
13
+ * @param {*} variable The `Variable` instance to process.
14
+ * @param {'decl' | 'init-lookups'} mode The code generation mode.
15
+ * @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT DATA` call (e.g.,
16
+ * `?data`) to the tabular data contained in the loaded data file.
17
+ * @param {string} modelDir The path to the directory containing the model (used for resolving data files).
18
+ * @param {string} varLhs The C code for the LHS variable reference.
19
+ * @return {string[]} An array of strings containing the generated C code for the variable,
20
+ * one string per line of code.
21
+ */
22
+ export function generateLookupsFromDirectData(variable, mode, directData, modelDir, varLhs) {
23
+ if (mode === 'decl') {
24
+ // Nothing to emit in decl mode
25
+ return []
26
+ } else if (mode !== 'init-lookups') {
27
+ throw new Error(`Invalid code gen mode '${mode}' for data variable ${variable.modelLHS}`)
28
+ }
29
+
30
+ // Create a function that reads the CSV or XLS[X] content
31
+ const { file, tab, timeRowOrCol, startCell } = variable.directDataArgs
32
+ const getCellValue = handleExcelOrCsvFile(file, tab, 'data', directData, modelDir)
33
+
34
+ // If direct data exists for this variable, copy it from the workbook into one or more lookups
35
+ let indexNum = 0
36
+ if (!R.isEmpty(variable.separationDims)) {
37
+ // Generate a lookup for a separated index in the variable's dimension.
38
+ if (variable.separationDims.length > 1) {
39
+ console.error(`WARNING: direct data variable ${variable.varName} separated on more than one dimension`)
40
+ }
41
+ let dimName = variable.separationDims[0]
42
+ for (let subscript of variable.subscripts) {
43
+ if (sub(subscript).family === dimName) {
44
+ // Use the index value in the subscript family when that is the separation dimension.
45
+ indexNum = sub(subscript).value
46
+ break
47
+ }
48
+ if (sub(dimName).value.includes(subscript)) {
49
+ // Look up the index when the separation dimension is a subdimension.
50
+ indexNum = sub(dimName).value.indexOf(subscript)
51
+ break
52
+ }
53
+ }
54
+ }
55
+ return [generateDirectDataLookup(varLhs, getCellValue, timeRowOrCol, startCell, indexNum)]
56
+ }
57
+
58
+ function generateDirectDataLookup(varLhs, getCellValue, timeRowOrCol, startCell, indexNum) {
59
+ // Read a row or column of data as (time, value) pairs from the worksheet.
60
+ // The cell(c,r) function wraps data access by column and row.
61
+ let lookupData = ''
62
+ let lookupSize = 0
63
+ let dataAddress = XLSX.utils.decode_cell(startCell.toUpperCase())
64
+ let dataCol = dataAddress.c
65
+ let dataRow = dataAddress.r
66
+ if (dataCol < 0 || dataRow < 0) {
67
+ throw new Error(`Failed to parse 'cell' argument for GET DIRECT {DATA,LOOKUPS} call for ${varLhs}: ${startCell}`)
68
+ }
69
+
70
+ let timeCol, timeRow, nextCell
71
+ if (isNaN(parseInt(timeRowOrCol))) {
72
+ // Time values are in a column.
73
+ timeCol = XLSX.utils.decode_col(timeRowOrCol.toUpperCase())
74
+ timeRow = dataRow
75
+ dataCol += indexNum
76
+ nextCell = () => {
77
+ dataRow++
78
+ timeRow++
79
+ }
80
+ } else {
81
+ // Time values are in a row.
82
+ timeCol = dataCol
83
+ timeRow = XLSX.utils.decode_row(timeRowOrCol)
84
+ dataRow += indexNum
85
+ nextCell = () => {
86
+ dataCol++
87
+ timeCol++
88
+ }
89
+ }
90
+
91
+ let timeValue = getCellValue(timeCol, timeRow)
92
+ let dataValue = getCellValue(dataCol, dataRow)
93
+ while (timeValue != null && dataValue != null) {
94
+ lookupData = listConcat(lookupData, `${timeValue}, ${dataValue}`, true)
95
+ lookupSize++
96
+ nextCell()
97
+ dataValue = getCellValue(dataCol, dataRow)
98
+ timeValue = getCellValue(timeCol, timeRow)
99
+ }
100
+ if (lookupSize === 0) {
101
+ throw new Error(`Empty lookup data array for ${varLhs}`)
102
+ }
103
+
104
+ return ` ${varLhs} = __new_lookup(${lookupSize}, /*copy=*/true, (double[]){ ${lookupData} });`
105
+ }
@@ -0,0 +1,115 @@
1
+ import * as R from 'ramda'
2
+
3
+ import { cartesianProductOf, permutationsOf } from '../_shared/helpers.js'
4
+ import { isDimension, sub } from '../_shared/subscript.js'
5
+ import { pointsString } from './gen-lookup-from-points.js'
6
+
7
+ /**
8
+ * Generate code for a data variable whose data is sourced from an external file (in DAT format).
9
+ *
10
+ * @param {*} variable The `Variable` instance to process.
11
+ * @param {'decl' | 'init-lookups'} mode The code generation mode.
12
+ * @param {Map<string, any>} extData The map of datasets from external `.dat` files.
13
+ * @param {string} varLhs The C code for the LHS variable reference.
14
+ * @return {string[]} An array of strings containing the generated C code for the variable,
15
+ * one string per line of code.
16
+ */
17
+ export function generateLookupsFromExternalData(variable, mode, extData, varLhs) {
18
+ if (mode !== 'decl' && mode !== 'init-lookups') {
19
+ throw new Error(`Invalid code gen mode '${mode}' for data variable ${variable.modelLHS}`)
20
+ }
21
+
22
+ // If there is external data for this variable, copy it from an external file to a lookup.
23
+ // Just like in `generateLookupFromPoints`, we declare static arrays to hold the data points in
24
+ // the first pass ("decl" mode), then initialize each `Lookup` using that data in the second pass
25
+ // ("init" mode).
26
+ const newLookup = (name, lhs, data, subscriptIndexes) => {
27
+ if (!data) {
28
+ throw new Error(`Data for ${name} not found in external data sources`)
29
+ }
30
+
31
+ if (data.size === 0) {
32
+ throw new Error(`Empty lookup data array for ${lhs}`)
33
+ }
34
+
35
+ const dataName = variable.varName + '_data_' + R.map(i => `_${i}_`, subscriptIndexes).join('')
36
+ if (mode === 'decl') {
37
+ // In decl mode, declare a static data array that will be used to create the associated `Lookup`
38
+ // at init time
39
+ const points = pointsString(Array.from(data.entries()))
40
+ return `double ${dataName}[${data.size * 2}] = { ${points} };`
41
+ } else if (mode === 'init-lookups') {
42
+ // In init mode, create the `Lookup`, passing in a pointer to the static data array declared in decl mode.
43
+ return ` ${lhs} = __new_lookup(${data.size}, /*copy=*/false, ${dataName});`
44
+ } else {
45
+ return []
46
+ }
47
+ }
48
+
49
+ // There are three common cases that we handle:
50
+ // - variable has no subscripts (C variable _thing = _thing from dat file)
51
+ // - variable has subscript(s) (C variable with index _thing[0] = _thing[_subscript] from dat file)
52
+ // - variable has dimension(s) (C variable in for loop, _thing[i] = _thing[_subscript_i] from dat file)
53
+
54
+ if (!variable.subscripts || variable.subscripts.length === 0) {
55
+ // No subscripts
56
+ const data = extData.get(variable.varName)
57
+ return [newLookup(variable.varName, varLhs, data, [])]
58
+ }
59
+
60
+ if (variable.subscripts.length === 1 && !isDimension(variable.subscripts[0])) {
61
+ // There is exactly one subscript
62
+ const subscript = variable.subscripts[0]
63
+ const nameInDat = `${variable.varName}[${subscript}]`
64
+ const data = extData.get(nameInDat)
65
+ const subIndex = sub(subscript).value
66
+ return [newLookup(nameInDat, varLhs, data, [subIndex])]
67
+ }
68
+
69
+ if (!R.all(s => isDimension(s), variable.subscripts)) {
70
+ // We don't yet handle the case where there are more than one subscript or a mix of
71
+ // subscripts and dimensions
72
+ // TODO: Remove this restriction
73
+ throw new Error(`Data variable ${variable.varName} has >= 2 subscripts; not yet handled`)
74
+ }
75
+
76
+ // At this point, we know that we have one or more dimensions; compute all combinations
77
+ // of the dimensions that we will iterate over
78
+ const lines = []
79
+ const allDims = R.map(s => sub(s).value, variable.subscripts)
80
+ const dimTuples = cartesianProductOf(allDims)
81
+ for (const dims of dimTuples) {
82
+ // Note: It appears that the dat file can have the subscripts in a different order
83
+ // than what SDE uses when declaring the C array. If we don't find data for one
84
+ // order, we try the other possible permutations.
85
+ const dimNamePermutations = permutationsOf(dims)
86
+ let nameInDat, data
87
+ for (const dimNames of dimNamePermutations) {
88
+ nameInDat = `${variable.varName}[${dimNames.join(',')}]`
89
+ data = extData.get(nameInDat)
90
+ if (data) {
91
+ break
92
+ }
93
+ }
94
+ if (!data) {
95
+ // We currently treat this as a warning, not an error, since there can sometimes be
96
+ // datasets that are a sparse matrix, i.e., data is not defined for certain dimensions.
97
+ // For these cases, the lookup will not be initialized (the Lookup pointer will remain
98
+ // NULL, and any calls to `LOOKUP` will return `:NA:`.
99
+ if (mode === 'decl') {
100
+ console.error(`WARNING: Data for ${nameInDat} not found in external data sources`)
101
+ }
102
+ continue
103
+ }
104
+
105
+ const subscriptIndexes = R.map(dim => sub(dim).value, dims)
106
+ const varSubscripts = R.map(index => `[${index}]`, subscriptIndexes).join('')
107
+ const lhs = `${variable.varName}${varSubscripts}`
108
+ const lookup = newLookup(nameInDat, lhs, data, subscriptIndexes)
109
+ if (lookup) {
110
+ lines.push(lookup)
111
+ }
112
+ }
113
+
114
+ return lines
115
+ }
@@ -0,0 +1,85 @@
1
+ import { cdbl } from '../_shared/helpers.js'
2
+ import { isDimension, isTrivialDimension, sub } from '../_shared/subscript.js'
3
+
4
+ /**
5
+ * Generate code for a data or lookup variable that contains an explicit set of points.
6
+ *
7
+ * @param {*} variable The `Variable` instance to process.
8
+ * @param {'decl' | 'init-lookups'} mode The code generation mode.
9
+ * @param {boolean} copy If false, a static data array will be used (good for larger data sets).
10
+ * If true, the data will inlined and copied when initializing the lookup (good for smaller data sets).
11
+ * @param {string} varLhs The C code for the LHS variable reference.
12
+ * @param {LoopIndexVars} loopIndexVars The loop index state.
13
+ * @return {string[]} An array of strings containing the generated C code for the variable,
14
+ * one string per line of code.
15
+ */
16
+ export function generateLookupFromPoints(variable, mode, copy, varLhs, loopIndexVars) {
17
+ if (variable.points.length === 0) {
18
+ throw new Error(`Empty lookup data array for ${variable.modelLHS}`)
19
+ }
20
+
21
+ if (copy) {
22
+ // Inline the data points and copy them when initializing the lookup
23
+ if (mode === 'decl') {
24
+ // Nothing to emit in decl mode
25
+ return []
26
+ } else if (mode === 'init-lookups') {
27
+ // In init mode, generate a new lookup using the data points from the variable
28
+ const points = pointsString(variable.points)
29
+ return [` ${varLhs} = __new_lookup(${variable.points.length}, /*copy=*/true, (double[]){ ${points} });`]
30
+ }
31
+ } else {
32
+ // Construct the name of the data array, which is based on the associated lookup var name,
33
+ // with any subscripts tacked on the end.
34
+ const dataName = variable.varName + '_data_' + generateLookupDataName(variable.subscripts, loopIndexVars)
35
+ if (mode === 'decl') {
36
+ // In decl mode, declare a static data array that will be used to create the associated `Lookup`
37
+ // at init time. Using static arrays is better for code size, helps us avoid creating a copy of
38
+ // the data in memory, and seems to perform much better when compiled to wasm when compared to the
39
+ // previous approach that used varargs + copying, especially on constrained (e.g. iOS) devices.
40
+ const points = pointsString(variable.points)
41
+ return [`double ${dataName}[${variable.points.length * 2}] = { ${points} };`]
42
+ } else if (mode === 'init-lookups') {
43
+ // In init mode, create the `Lookup`, passing in a pointer to the static data array declared earlier.
44
+ // TODO: Make use of the lookup range
45
+ return [` ${varLhs} = __new_lookup(${variable.points.length}, /*copy=*/false, ${dataName});`]
46
+ }
47
+ }
48
+
49
+ throw new Error(`Invalid code gen mode '${mode}' for lookup ${variable.modelLHS}`)
50
+ }
51
+
52
+ /**
53
+ * Return a string containing a comma separated list of [x,y] pairs from the given array of points.
54
+ *
55
+ * @param {number[][]} points The array of [x,y] tuples.
56
+ * @return {string} The string containing the comma separated point values.
57
+ */
58
+ export function pointsString(points) {
59
+ return points.map(p => `${cdbl(p[0])}, ${cdbl(p[1])}`).join(', ')
60
+ }
61
+
62
+ /**
63
+ * Return a C name for the static data array associated with a lookup variable.
64
+ *
65
+ * @param {string[]} subIds The array of subscript IDs.
66
+ * @param {LoopIndexVars} loopIndexVars The loop index state.
67
+ * @return {string} The C array name.
68
+ */
69
+ function generateLookupDataName(subIds, loopIndexVars) {
70
+ return subIds
71
+ .map(subId => {
72
+ if (isDimension(subId)) {
73
+ const i = loopIndexVars.index(subId)
74
+ if (isTrivialDimension(subId)) {
75
+ // When the dimension is trivial, we can simply emit e.g. `[i]` instead of `[_dim[i]]`
76
+ return `_${i}_`
77
+ } else {
78
+ return `_${subId}_${i}_`
79
+ }
80
+ } else {
81
+ return `_${sub(subId).value}_`
82
+ }
83
+ })
84
+ .join('')
85
+ }
@@ -22,10 +22,11 @@ import {
22
22
  import {
23
23
  extractMarkedDims,
24
24
  indexNamesForSubscript,
25
+ isDimension,
26
+ isIndex,
25
27
  normalizeSubscripts,
26
28
  separatedVariableIndex,
27
- sub,
28
- isDimension
29
+ sub
29
30
  } from '../_shared/subscript.js'
30
31
  import ModelReader from '../parse/model-reader.js'
31
32
  import { createParser } from '../parse/parser.js'
@@ -190,31 +191,36 @@ export default class EquationReader extends ModelReader {
190
191
  startCell: args[2]
191
192
  }
192
193
  } else if (fn === '_IF_THEN_ELSE') {
193
- // Evaluate the condition expression of the `IF THEN ELSE`. If it resolves
194
- // to a compile-time constant, we only need to visit one branch, which means
195
- // that no references will be recorded for the other branch, therefore allowing
196
- // it to be skipped in the unused reference elimination phase and during the
197
- // final code generation phase.
198
- const condText = ctx.expr(0).getText()
199
- const exprReader = new ExprReader()
200
- const condExpr = exprReader.read(condText)
201
- if (condExpr.constantValue !== undefined) {
202
- // Record the conditional expression and its constant value so that
203
- // it can be accessed later by EquationGen. We need to record it
204
- // this way because any variables referenced by the expression may
205
- // be removed during the unused reference elimination phase.
206
- Model.addConstantExpr(condText, condExpr.constantValue)
207
- if (condExpr.constantValue !== 0) {
208
- // Only visit the "if true" branch
209
- this.setArgIndex(1)
210
- ctx.expr(1).accept(this)
194
+ if (process.env.SDE_NONPUBLIC_REDUCE_VARIABLES !== '0') {
195
+ // Evaluate the condition expression of the `IF THEN ELSE`. If it resolves
196
+ // to a compile-time constant, we only need to visit one branch, which means
197
+ // that no references will be recorded for the other branch, therefore allowing
198
+ // it to be skipped in the unused reference elimination phase and during the
199
+ // final code generation phase.
200
+ const condText = ctx.expr(0).getText()
201
+ const exprReader = new ExprReader()
202
+ const condExpr = exprReader.read(condText)
203
+ if (condExpr.constantValue !== undefined) {
204
+ // Record the conditional expression and its constant value so that
205
+ // it can be accessed later by EquationGen. We need to record it
206
+ // this way because any variables referenced by the expression may
207
+ // be removed during the unused reference elimination phase.
208
+ Model.addConstantExpr(condText, condExpr.constantValue)
209
+ if (condExpr.constantValue !== 0) {
210
+ // Only visit the "if true" branch
211
+ this.setArgIndex(1)
212
+ ctx.expr(1).accept(this)
213
+ } else {
214
+ // Only visit the "if false" branch
215
+ this.setArgIndex(2)
216
+ ctx.expr(2).accept(this)
217
+ }
211
218
  } else {
212
- // Only visit the "if false" branch
213
- this.setArgIndex(2)
214
- ctx.expr(2).accept(this)
219
+ // Visit the condition and both branches
220
+ super.visitExprList(ctx)
215
221
  }
216
222
  } else {
217
- // Visit the condition and both branches
223
+ // Optimization is disabled, visit the condition and both branches
218
224
  super.visitExprList(ctx)
219
225
  }
220
226
  } else {
@@ -238,8 +244,8 @@ export default class EquationReader extends ModelReader {
238
244
  // Get the var name of a variable in a call and save it as a reference.
239
245
  let id = ctx.Id().getText()
240
246
  let varName = canonicalName(id)
241
- // Do not add a dimension name as a reference.
242
- if (!isDimension(varName)) {
247
+ // Do not add a dimension or index name as a reference.
248
+ if (!isDimension(varName) && !isIndex(varName)) {
243
249
  let fn = this.currentFunctionName()
244
250
  this.refId = varName
245
251
  this.expandedRefIds = []
@@ -2,7 +2,7 @@ import B from 'bufx'
2
2
  import yaml from 'js-yaml'
3
3
  import * as R from 'ramda'
4
4
 
5
- import { decanonicalize, isIterable, listConcat, strlist, vlog, vsort } from '../_shared/helpers.js'
5
+ import { canonicalName, decanonicalize, isIterable, listConcat, strlist, vlog, vsort } from '../_shared/helpers.js'
6
6
  import {
7
7
  addIndex,
8
8
  allAliases,
@@ -17,6 +17,10 @@ import {
17
17
  import { createParser } from '../parse/parser.js'
18
18
 
19
19
  import EquationReader from './equation-reader.js'
20
+ import { readEquation } from './read-equations.js'
21
+ import { readDimensionDefs } from './read-subscripts.js'
22
+ import { readVariables as readVariables2 } from './read-variables.js'
23
+ import { reduceVariables } from './reduce-variables.js'
20
24
  import SubscriptRangeReader from './subscript-range-reader.js'
21
25
  import toposort from './toposort.js'
22
26
  import VarNameReader from './var-name-reader.js'
@@ -38,14 +42,65 @@ const PRINT_INIT_GRAPH = false
38
42
  const PRINT_AUX_GRAPH = false
39
43
  const PRINT_LEVEL_GRAPH = false
40
44
 
41
- function read(parseTree, spec, extData, directData, modelDirname) {
45
+ // XXX: This is needed for tests due to variables being in module-level storage
46
+ function resetModelState() {
47
+ variables.length = 0
48
+ inputVars.length = 0
49
+ variablesByName.clear()
50
+ constantExprs.clear()
51
+ nonAtoANames = Object.create(null)
52
+ }
53
+
54
+ /**
55
+ * Read the given model parse tree and resolve all subscript and variable/equation definitions.
56
+ *
57
+ * Note that this function currently does not return anything and instead stores the parsed subscript
58
+ * definitions in the `subscript` module and the parsed/analyzed variables in this module.
59
+ *
60
+ * TODO: FIX TYPE
61
+ * @param {*} parsedModel The parsed model structure.
62
+ * @param {*} spec The parsed `spec.json` object.
63
+ * @param {Map<string, any>} extData The map of datasets from external `.dat` files.
64
+ * @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT DATA`
65
+ * call (e.g., `?data`) to the tabular data contained in the loaded data file.
66
+ * @param {string} modelDirname The path to the directory containing the model (used for resolving data
67
+ * files for `GET DIRECT SUBSCRIPT`).
68
+ * @param {*} opts An optional object used by tests to stop the read process after a specific phase.
69
+ */
70
+ function read(parsedModel, spec, extData, directData, modelDirname, opts) {
42
71
  // Some arrays need to be separated into variables with individual indices to
43
72
  // prevent eval cycles. They are manually added to the spec file.
44
73
  let specialSeparationDims = spec.specialSeparationDims
45
- // Subscript ranges must be defined before reading variables that use them.
46
- readSubscriptRanges(parseTree, spec.dimensionFamilies, spec.indexFamilies, modelDirname)
74
+
75
+ // Dimensions must be defined before reading variables that use them.
76
+ if (parsedModel.kind === 'vensim-legacy') {
77
+ readSubscriptRanges(parsedModel.parseTree, modelDirname)
78
+ } else {
79
+ readDimensionDefs(parsedModel, modelDirname)
80
+ }
81
+ if (opts?.stopAfterReadSubscripts) return
82
+ resolveDimensions(spec.dimensionFamilies)
83
+ if (opts?.stopAfterResolveSubscripts) return
84
+
47
85
  // Read variables from the model parse tree.
48
- readVariables(parseTree, specialSeparationDims, directData)
86
+ if (parsedModel.kind === 'vensim-legacy') {
87
+ // TODO: directData is actually unused in VariableReader
88
+ readVariables(parsedModel.parseTree, specialSeparationDims, directData)
89
+ } else {
90
+ // Read the variables
91
+ const vars = readVariables2(parsedModel, specialSeparationDims)
92
+
93
+ // Include a placeholder variable for the exogenous `Time` variable
94
+ const timeVar = new Variable(null)
95
+ timeVar.modelLHS = 'Time'
96
+ timeVar.varName = '_time'
97
+ vars.push(timeVar)
98
+
99
+ // Add the variables to the `Model`
100
+ vars.forEach(addVariable)
101
+ }
102
+ if (opts?.stopAfterReadVariables) return
103
+
49
104
  if (spec) {
50
105
  // If the spec file contains `input/outputVarNames` (with full Vensim variable names)
51
106
  // convert those to C names first. Otherwise, use `input/outputNames` which are already
@@ -61,21 +116,51 @@ function read(parseTree, spec, extData, directData, modelDirname) {
61
116
  inputVars = spec.inputVars
62
117
  }
63
118
  }
119
+
64
120
  // Analyze model equations to fill in more details about variables.
65
- analyze()
121
+ analyze(parsedModel.kind, spec?.inputVars, opts)
122
+ if (opts?.stopAfterAnalyze) return
123
+
66
124
  // Check that all input and output vars in the spec actually exist in the model.
67
125
  checkSpecVars(spec, extData)
126
+
68
127
  // Remove variables that are not referenced by an input or output variable.
69
128
  removeUnusedVariables(spec)
129
+
70
130
  // Resolve duplicate declarations by converting to one variable type.
71
131
  resolveDuplicateDeclarations()
72
132
  }
73
- function readSubscriptRanges(tree, dimensionFamilies, indexFamilies, modelDirname) {
133
+
134
+ /**
135
+ * Read subscript ranges from the given model.
136
+ *
137
+ * Note that this function currently does not return anything and instead stores the parsed subscript
138
+ * range definitions in the `subscript` module.
139
+ *
140
+ * @param {import('../parse/parser.js').VensimModelParseTree} parseTree The Vensim parse tree.
141
+ * @param {string} modelDirname The path to the directory containing the model (used for resolving data
142
+ * files for `GET DIRECT SUBSCRIPT`).
143
+ */
144
+ function readSubscriptRanges(parseTree, modelDirname) {
74
145
  // Read subscript ranges from the model.
75
146
  let subscriptRangeReader = new SubscriptRangeReader(modelDirname)
76
- subscriptRangeReader.visitModel(tree)
147
+ subscriptRangeReader.visitModel(parseTree)
148
+ }
149
+
150
+ /**
151
+ * Process the previously read subscript/dimension definitions (stored in the `subscript` module) to
152
+ * resolve aliases, families, and indices.
153
+ *
154
+ * Note that this function currently does not return anything and only updates the set of dimension
155
+ * and subscript definitions in the `subscript` module.
156
+ *
157
+ * @param {Object.<string, string>} dimensionFamilies The optional mapping of dimension name to family name
158
+ * as provided in a `spec.json` file.
159
+ */
160
+ function resolveDimensions(dimensionFamilies) {
77
161
  let allDims = allDimensions()
78
- // Expand dimensions that appeared in subscript range definitions into indices.
162
+
163
+ // Expand dimensions that appeared in dimension definitions into subscripts/indices.
79
164
  // Repeat until there are only indices in dimension values.
80
165
  let dimFoundInValue
81
166
  do {
@@ -94,7 +179,7 @@ function readSubscriptRanges(tree, dimensionFamilies, indexFamilies, modelDirnam
94
179
  }
95
180
  } while (dimFoundInValue)
96
181
 
97
- // Fill in subscript aliases from their model families.
182
+ // Fill in dimension aliases from their model families.
98
183
  for (let dim of allAliases()) {
99
184
  if (dim.value === '') {
100
185
  let refDim = sub(dim.family)
@@ -204,6 +289,21 @@ function readSubscriptRanges(tree, dimensionFamilies, indexFamilies, modelDirnam
204
289
  }
205
290
  }
206
291
  }
292
+
293
+ /**
294
+ * Read equations from the given model and generate `Variable` instances for all variables that
295
+ * are encountered while parsing.
296
+ *
297
+ * Note that this function currently does not return anything and instead stores the parsed
298
+ * variable definitions in the `model` module.
299
+ *
300
+ * @param {import('../parse/parser.js').VensimModelParseTree} tree The Vensim parse tree.
301
+ * @param {Object.<string, string>} specialSeparationDims The variable names that need to be
302
+ * separated because of circular references. A mapping from "C" variable name to "C" dimension
303
+ * name to separate on.
304
+ * @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT DATA`
305
+ * call (e.g., `?data`) to the tabular data contained in the loaded data file.
306
+ */
207
307
  function readVariables(tree, specialSeparationDims, directData) {
208
308
  // Read all variables in the model parse tree.
209
309
  // This populates the variables table with basic information for each variable
@@ -216,26 +316,44 @@ function readVariables(tree, specialSeparationDims, directData) {
216
316
  v.varName = '_time'
217
317
  addVariable(v)
218
318
  }
219
- function analyze() {
319
+ function analyze(parsedModelKind, inputVars, opts) {
220
320
  // Analyze the RHS of each equation in stages after all the variables are read.
221
321
  // Find non-apply-to-all vars that are defined with more than one equation.
222
322
  findNonAtoAVars()
323
+
223
324
  // Set the refId for each variable. Only non-apply-to-all vars include subscripts in the refId.
224
325
  setRefIds()
326
+
327
+ // If enabled, reduce expressions used in variable definitions.
328
+ if (parsedModelKind !== 'vensim-legacy') {
329
+ if (opts?.reduceVariables !== false && process.env.SDE_NONPUBLIC_REDUCE_VARIABLES !== '0') {
330
+ let reduceMode = opts?.reduceVariables || process.env.SDE_NONPUBLIC_REDUCE_VARIABLES || 'default'
331
+ reduceVariables(variables, inputVars || [], reduceMode)
332
+ }
333
+ }
334
+ if (opts?.stopAfterReduceVariables === true) return
335
+
225
336
  // Read the RHS to list the refIds of vars that are referenced and set the var type.
226
- readEquations()
337
+ if (parsedModelKind === 'vensim-legacy') {
338
+ readEquations()
339
+ } else {
340
+ variables.forEach(readEquation)
341
+ }
227
342
  }
228
343
 
229
344
  function checkSpecVars(spec, extData) {
230
- // Look up each var in the spec and issue and error message if it does not exist.
345
+ // Look up each var in the spec and issue and throw error if it does not exist.
231
346
 
232
347
  function check(varNames, specType) {
233
348
  if (isIterable(varNames)) {
234
349
  for (let varName of varNames) {
350
+ // TODO: This code as written does not check variables that include subscripts, but
351
+ // we should check those as well (and make sure that subscripts or indices are not
352
+ // out of the valid range)
235
353
  if (!R.contains('[', varName)) {
236
354
  if (!varWithRefId(varName)) {
237
355
  // Look for a variable in external data.
238
- if (extData.has(varName)) {
356
+ if (extData?.has(varName)) {
239
357
  // console.error(`found ${specType} ${varName} in extData`)
240
358
  // Copy data from an external file to an equation that does a lookup.
241
359
  let lookup = R.reduce(
@@ -246,7 +364,9 @@ function checkSpecVars(spec, extData) {
246
364
  let modelEquation = `${decanonicalize(varName)} = WITH LOOKUP(Time, (${lookup}))`
247
365
  addEquation(modelEquation)
248
366
  } else {
249
- console.error(`${specType} variable ${varName} not found in the model or external data sources`)
367
+ throw new Error(
368
+ `The ${specType} variable ${varName} was declared in spec.json, but no matching variable was found in the model or external data sources`
369
+ )
250
370
  }
251
371
  }
252
372
  }
@@ -585,7 +705,7 @@ function varWithRefId(refId) {
585
705
  }
586
706
  }
587
707
  if (!refVar) {
588
- vlog('ERROR: no var found for refId', refId)
708
+ // vlog('ERROR: no var found for refId', refId)
589
709
  }
590
710
  }
591
711
  return refVar
@@ -675,7 +795,29 @@ function vensimName(cVarName) {
675
795
  function cName(vensimVarName) {
676
796
  // Convert a Vensim variable name to a C name.
677
797
  // This function requires model analysis to be completed first when the variable has subscripts.
678
- return new VarNameReader().read(vensimVarName)
798
+ if (process.env.SDE_NONPUBLIC_USE_NEW_PARSE !== '1') {
799
+ // TODO: For now we use the legacy VarNameReader when the old parser is active; this
800
+ // code will be removed once the old parser is removed
801
+ return new VarNameReader().read(vensimVarName)
802
+ }
803
+ // Split the variable name from the subscripts
804
+ let matches = vensimVarName.match(/([^[]+)(?:\[([^\]]+)\])?/)
805
+ if (!matches) {
806
+ throw new Error(`Invalid variable name '${vensimVarName}' found when converting to C representation`)
807
+ }
808
+ let cVarName = canonicalName(matches[1])
809
+ if (matches[2]) {
810
+ // The variable name includes subscripts, so split them into individual IDs
811
+ let cSubIds = matches[2].split(',').map(x => canonicalName(x))
812
+ cSubIds = normalizeSubscripts(cSubIds)
813
+ // If a subscript is an index, convert it to an index number to match Vensim data exports
814
+ let cSubIdParts = cSubIds.map(cSubId => {
815
+ return isIndex(cSubId) ? `[${sub(cSubId).value}]` : `[${cSubId}]`
816
+ })
817
+ // Append the subscript parts to the base variable name to create the full reference
818
+ cVarName += cSubIdParts.join('')
819
+ }
820
+ return cVarName
679
821
  }
680
822
  function isInputVar(varName) {
681
823
  // Return true if the given variable (in canonical form) is included in the list of
@@ -1146,6 +1288,7 @@ export default {
1146
1288
  read,
1147
1289
  refIdForVar,
1148
1290
  refIdsWithName,
1291
+ resetModelState,
1149
1292
  splitRefId,
1150
1293
  variables,
1151
1294
  varIndexInfo,