@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,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
+ }
@@ -191,31 +191,36 @@ export default class EquationReader extends ModelReader {
191
191
  startCell: args[2]
192
192
  }
193
193
  } else if (fn === '_IF_THEN_ELSE') {
194
- // Evaluate the condition expression of the `IF THEN ELSE`. If it resolves
195
- // to a compile-time constant, we only need to visit one branch, which means
196
- // that no references will be recorded for the other branch, therefore allowing
197
- // it to be skipped in the unused reference elimination phase and during the
198
- // final code generation phase.
199
- const condText = ctx.expr(0).getText()
200
- const exprReader = new ExprReader()
201
- const condExpr = exprReader.read(condText)
202
- if (condExpr.constantValue !== undefined) {
203
- // Record the conditional expression and its constant value so that
204
- // it can be accessed later by EquationGen. We need to record it
205
- // this way because any variables referenced by the expression may
206
- // be removed during the unused reference elimination phase.
207
- Model.addConstantExpr(condText, condExpr.constantValue)
208
- if (condExpr.constantValue !== 0) {
209
- // Only visit the "if true" branch
210
- this.setArgIndex(1)
211
- 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
+ }
212
218
  } else {
213
- // Only visit the "if false" branch
214
- this.setArgIndex(2)
215
- ctx.expr(2).accept(this)
219
+ // Visit the condition and both branches
220
+ super.visitExprList(ctx)
216
221
  }
217
222
  } else {
218
- // Visit the condition and both branches
223
+ // Optimization is disabled, visit the condition and both branches
219
224
  super.visitExprList(ctx)
220
225
  }
221
226
  } else {
@@ -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'
@@ -53,7 +57,8 @@ function resetModelState() {
53
57
  * Note that this function currently does not return anything and instead stores the parsed subscript
54
58
  * definitions in the `subscript` module and the parsed/analyzed variables in this module.
55
59
  *
56
- * @param {import('../parse/parser.js').VensimModelParseTree} parseTree The Vensim parse tree.
60
+ * TODO: FIX TYPE
61
+ * @param {*} parsedModel The parsed model structure.
57
62
  * @param {*} spec The parsed `spec.json` object.
58
63
  * @param {Map<string, any>} extData The map of datasets from external `.dat` files.
59
64
  * @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT DATA`
@@ -62,19 +67,38 @@ function resetModelState() {
62
67
  * files for `GET DIRECT SUBSCRIPT`).
63
68
  * @param {*} opts An optional object used by tests to stop the read process after a specific phase.
64
69
  */
65
- function read(parseTree, spec, extData, directData, modelDirname, opts) {
70
+ function read(parsedModel, spec, extData, directData, modelDirname, opts) {
66
71
  // Some arrays need to be separated into variables with individual indices to
67
72
  // prevent eval cycles. They are manually added to the spec file.
68
73
  let specialSeparationDims = spec.specialSeparationDims
69
74
 
70
- // Subscript ranges must be defined before reading variables that use them.
71
- readSubscriptRanges(parseTree, modelDirname)
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
+ }
72
81
  if (opts?.stopAfterReadSubscripts) return
73
- resolveSubscriptRanges(spec.dimensionFamilies)
82
+ resolveDimensions(spec.dimensionFamilies)
74
83
  if (opts?.stopAfterResolveSubscripts) return
75
84
 
76
85
  // Read variables from the model parse tree.
77
- 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
+ }
78
102
  if (opts?.stopAfterReadVariables) return
79
103
 
80
104
  if (spec) {
@@ -94,7 +118,7 @@ function read(parseTree, spec, extData, directData, modelDirname, opts) {
94
118
  }
95
119
 
96
120
  // Analyze model equations to fill in more details about variables.
97
- analyze()
121
+ analyze(parsedModel.kind, spec?.inputVars, opts)
98
122
  if (opts?.stopAfterAnalyze) return
99
123
 
100
124
  // Check that all input and output vars in the spec actually exist in the model.
@@ -133,10 +157,10 @@ function readSubscriptRanges(parseTree, modelDirname) {
133
157
  * @param {Object.<string, string>} dimensionFamilies The optional mapping of dimension name to family name
134
158
  * as provided in a `spec.json` file.
135
159
  */
136
- function resolveSubscriptRanges(dimensionFamilies) {
160
+ function resolveDimensions(dimensionFamilies) {
137
161
  let allDims = allDimensions()
138
162
 
139
- // Expand dimensions that appeared in subscript range definitions into indices.
163
+ // Expand dimensions that appeared in dimension definitions into subscripts/indices.
140
164
  // Repeat until there are only indices in dimension values.
141
165
  let dimFoundInValue
142
166
  do {
@@ -155,7 +179,7 @@ function resolveSubscriptRanges(dimensionFamilies) {
155
179
  }
156
180
  } while (dimFoundInValue)
157
181
 
158
- // Fill in subscript aliases from their model families.
182
+ // Fill in dimension aliases from their model families.
159
183
  for (let dim of allAliases()) {
160
184
  if (dim.value === '') {
161
185
  let refDim = sub(dim.family)
@@ -292,27 +316,44 @@ function readVariables(tree, specialSeparationDims, directData) {
292
316
  v.varName = '_time'
293
317
  addVariable(v)
294
318
  }
295
-
296
- function analyze() {
319
+ function analyze(parsedModelKind, inputVars, opts) {
297
320
  // Analyze the RHS of each equation in stages after all the variables are read.
298
321
  // Find non-apply-to-all vars that are defined with more than one equation.
299
322
  findNonAtoAVars()
323
+
300
324
  // Set the refId for each variable. Only non-apply-to-all vars include subscripts in the refId.
301
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
+
302
336
  // Read the RHS to list the refIds of vars that are referenced and set the var type.
303
- readEquations()
337
+ if (parsedModelKind === 'vensim-legacy') {
338
+ readEquations()
339
+ } else {
340
+ variables.forEach(readEquation)
341
+ }
304
342
  }
305
343
 
306
344
  function checkSpecVars(spec, extData) {
307
- // 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.
308
346
 
309
347
  function check(varNames, specType) {
310
348
  if (isIterable(varNames)) {
311
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)
312
353
  if (!R.contains('[', varName)) {
313
354
  if (!varWithRefId(varName)) {
314
355
  // Look for a variable in external data.
315
- if (extData.has(varName)) {
356
+ if (extData?.has(varName)) {
316
357
  // console.error(`found ${specType} ${varName} in extData`)
317
358
  // Copy data from an external file to an equation that does a lookup.
318
359
  let lookup = R.reduce(
@@ -323,7 +364,9 @@ function checkSpecVars(spec, extData) {
323
364
  let modelEquation = `${decanonicalize(varName)} = WITH LOOKUP(Time, (${lookup}))`
324
365
  addEquation(modelEquation)
325
366
  } else {
326
- 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
+ )
327
370
  }
328
371
  }
329
372
  }
@@ -662,7 +705,7 @@ function varWithRefId(refId) {
662
705
  }
663
706
  }
664
707
  if (!refVar) {
665
- vlog('ERROR: no var found for refId', refId)
708
+ // vlog('ERROR: no var found for refId', refId)
666
709
  }
667
710
  }
668
711
  return refVar
@@ -752,7 +795,29 @@ function vensimName(cVarName) {
752
795
  function cName(vensimVarName) {
753
796
  // Convert a Vensim variable name to a C name.
754
797
  // This function requires model analysis to be completed first when the variable has subscripts.
755
- 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
756
821
  }
757
822
  function isInputVar(varName) {
758
823
  // Return true if the given variable (in canonical form) is included in the list of