@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 CHANGED
@@ -1,14 +1,11 @@
1
1
  {
2
2
  "name": "@sdeverywhere/compile",
3
- "version": "0.7.9",
3
+ "version": "0.7.11",
4
4
  "description": "The core Vensim to C compiler for the SDEverywhere tool suite.",
5
5
  "type": "module",
6
- "files": [
7
- "src/**",
8
- "!.DS_Store"
9
- ],
10
6
  "main": "./src/index.js",
11
7
  "dependencies": {
8
+ "@sdeverywhere/parse": "^0.1.0",
12
9
  "antlr4": "4.12.0",
13
10
  "antlr4-vensim": "0.6.2",
14
11
  "bufx": "^1.0.5",
@@ -35,6 +32,10 @@
35
32
  "prettier:check": "prettier --check .",
36
33
  "prettier:fix": "prettier --write .",
37
34
  "precommit": "../../scripts/precommit",
38
- "ci:build": "run-s lint prettier:check"
35
+ "type-check": "tsc --noEmit -p tsconfig-test.json",
36
+ "test": "vitest run",
37
+ "test:watch": "vitest",
38
+ "test:ci": "vitest run",
39
+ "ci:build": "run-s lint prettier:check type-check test:ci"
39
40
  }
40
41
  }
@@ -22,8 +22,17 @@ let nextLevelVarSeq = 1
22
22
  let nextAuxVarSeq = 1
23
23
  // parsed csv data cache
24
24
  let csvData = new Map()
25
- // string table for web apps
26
- export let strings = []
25
+
26
+ // XXX: This is needed for tests due to sequence numbers being in module-level storage
27
+ export function resetHelperState() {
28
+ nextTmpVarSeq = 1
29
+ nextLookupVarSeq = 1
30
+ nextFixedDelayVarSeq = 1
31
+ nextDepreciationVarSeq = 1
32
+ nextLevelVarSeq = 1
33
+ nextAuxVarSeq = 1
34
+ csvData.clear()
35
+ }
27
36
 
28
37
  export let canonicalName = name => {
29
38
  // Format a model variable name into a valid C identifier.
@@ -200,15 +209,6 @@ export let isIterable = obj => {
200
209
  }
201
210
  return typeof obj[Symbol.iterator] === 'function'
202
211
  }
203
- export let stringToId = str => {
204
- // Look up a string id. Create the id from the string if it is not found.
205
- let stringIndex = R.indexOf(str, strings)
206
- if (stringIndex < 0) {
207
- stringIndex = strings.length
208
- strings.push(str)
209
- }
210
- return `id${stringIndex}`
211
- }
212
212
  // Command helpers
213
213
  export let readXlsx = pathname => {
214
214
  return XLSX.readFile(pathname, { cellDates: true })
@@ -49,6 +49,11 @@ import { canonicalName, asort, vlog } from './helpers.js'
49
49
  // subscript name as the key and a subscript object as the value.
50
50
  let subscripts = new Map()
51
51
 
52
+ // XXX: This is needed for tests due to subs/dims being in module-level storage
53
+ export function resetSubscriptsAndDimensions() {
54
+ subscripts.clear()
55
+ }
56
+
52
57
  export function Subscript(modelName, modelValue = null, modelFamily = null, modelMappings = null) {
53
58
  let name = canonicalName(modelName)
54
59
  if (modelValue === null) {
@@ -4,15 +4,16 @@ import { asort, lines, strlist, abend, mapIndexed } from '../_shared/helpers.js'
4
4
  import { sub, allDimensions, allMappings, subscriptFamilies } from '../_shared/subscript.js'
5
5
  import Model from '../model/model.js'
6
6
 
7
+ import { generateEquation } from './gen-equation.js'
7
8
  import EquationGen from './equation-gen.js'
8
- import ModelLHSReader from './model-lhs-reader.js'
9
+ import { expandVarNames } from './expand-var-names.js'
9
10
 
10
- export function generateCode(parseTree, opts) {
11
- return codeGenerator(parseTree, opts).generate()
11
+ export function generateCode(parsedModel, opts) {
12
+ return codeGenerator(parsedModel, opts).generate()
12
13
  }
13
14
 
14
- let codeGenerator = (parseTree, opts) => {
15
- const { spec, operation, extData, directData, modelDirname } = opts
15
+ let codeGenerator = (parsedModel, opts) => {
16
+ const { spec, operations, extData, directData, modelDirname } = opts
16
17
  // Set to 'decl', 'init-lookups', 'eval', etc depending on the section being generated.
17
18
  let mode = ''
18
19
  // Set to true to output all variables when there is no model run spec.
@@ -25,21 +26,30 @@ let codeGenerator = (parseTree, opts) => {
25
26
  outputAllVars = true
26
27
  }
27
28
  // Function to generate a section of the code
28
- let generateSection = R.map(v => new EquationGen(v, extData, directData, mode, modelDirname).generate())
29
+ let generateSection = R.map(v => {
30
+ if (parsedModel.kind === 'vensim-legacy') {
31
+ return new EquationGen(v, extData, directData, mode, modelDirname).generate()
32
+ } else {
33
+ return generateEquation(v, mode, extData, directData, modelDirname)
34
+ }
35
+ })
29
36
  let section = R.pipe(generateSection, R.flatten, lines)
30
37
  function generate() {
31
38
  // Read variables and subscript ranges from the model parse tree.
32
39
  // This is the main entry point for code generation and is called just once.
33
40
  try {
34
- Model.read(parseTree, spec, extData, directData, modelDirname)
41
+ Model.read(parsedModel, spec, extData, directData, modelDirname)
35
42
  // In list mode, print variables to the console instead of generating code.
36
- if (operation === 'printRefIdTest') {
43
+ if (operations.includes('printRefIdTest')) {
37
44
  Model.printRefIdTest()
38
- } else if (operation === 'printRefGraph') {
45
+ }
46
+ if (operations.includes('printRefGraph')) {
39
47
  Model.printRefGraph(opts.varname)
40
- } else if (operation === 'convertNames') {
48
+ }
49
+ if (operations.includes('convertNames')) {
41
50
  // Do not generate output, but leave the results of model analysis.
42
- } else if (operation === 'generateC') {
51
+ }
52
+ if (operations.includes('generateC')) {
43
53
  // Generate code for each variable in the proper order.
44
54
  let code = emitDeclCode()
45
55
  code += emitInitLookupsCode()
@@ -198,10 +208,21 @@ void ${name}${idx}() {
198
208
  }
199
209
  let funcCalls = R.pipe(mapIndexed(funcCall), lines)
200
210
 
201
- // Break the vars into chunks of 30; this number was empirically
202
- // determined by looking at runtime performance and memory usage
203
- // of the En-ROADS model on various devices
204
- let chunks = R.splitEvery(30, vars)
211
+ // Break the vars into chunks. The default value of 30 was empirically
212
+ // determined by looking at runtime performance and memory usage of the
213
+ // En-ROADS model on various devices.
214
+ let chunkSize
215
+ if (process.env.SDE_CODE_GEN_CHUNK_SIZE) {
216
+ chunkSize = parseInt(process.env.SDE_CODE_GEN_CHUNK_SIZE)
217
+ } else {
218
+ chunkSize = 30
219
+ }
220
+ let chunks
221
+ if (chunkSize > 0) {
222
+ chunks = R.splitEvery(chunkSize, vars)
223
+ } else {
224
+ chunks = [vars]
225
+ }
205
226
 
206
227
  if (!preStep) {
207
228
  preStep = ''
@@ -296,33 +317,8 @@ ${postStep}
296
317
  // Return a list of var names for all variables except lookups and data variables.
297
318
  // The names are in Vensim format if vensimNames is true, otherwise they are in C format.
298
319
  // Expand subscripted vars into separate var names with each index.
299
- function sortedVars() {
300
- // Return a list of all vars sorted by the model LHS var name (without subscripts), case insensitive.
301
- return R.sortBy(v => {
302
- let modelLHSReader = new ModelLHSReader()
303
- modelLHSReader.read(v.modelLHS)
304
- return modelLHSReader.varName.toUpperCase()
305
- }, Model.variables)
306
- }
307
- return R.uniq(
308
- R.reduce(
309
- (a, v) => {
310
- if (v.varType !== 'lookup' && v.varType !== 'data' && v.includeInOutput) {
311
- let modelLHSReader = new ModelLHSReader()
312
- modelLHSReader.read(v.modelLHS)
313
- if (vensimNames) {
314
- return R.concat(a, modelLHSReader.names())
315
- } else {
316
- return R.concat(a, R.map(Model.cName, modelLHSReader.names()))
317
- }
318
- } else {
319
- return a
320
- }
321
- },
322
- [],
323
- sortedVars()
324
- )
325
- )
320
+ const canonicalNames = !vensimNames
321
+ return expandVarNames(canonicalNames)
326
322
  }
327
323
  //
328
324
  // Input/output section helpers
@@ -0,0 +1,86 @@
1
+ import path from 'node:path'
2
+
3
+ import XLSX from 'xlsx'
4
+
5
+ import { cdbl, readCsv, readXlsx } from '../_shared/helpers.js'
6
+
7
+ /**
8
+ * Return a `getCellValue` function that reads the CSV or XLS[X] content.
9
+ *
10
+ * @param {string} fileOrTag The filename (e.g., 'data.xlsx') or tag name (e.g., '?data').
11
+ * @param {string} tabOrDelimiter
12
+ * @param {'data' | 'constants'} dataKind The kind of `GET DIRECT ...` being used.
13
+ * @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT DATA` call (e.g.,
14
+ * `?data`) to the tabular data contained in the loaded data file.
15
+ * @param {string} modelDir The path to the directory containing the model (used for resolving data files).
16
+ * @returns A `getCellValue` function.
17
+ */
18
+ export function handleExcelOrCsvFile(fileOrTag, tabOrDelimiter, dataKind, directData, modelDir) {
19
+ if (fileOrTag.startsWith('?')) {
20
+ // The file is a tag for an Excel file with data in the directData map.
21
+ const workbook = directData.get(fileOrTag)
22
+ return handleExcelWorkbook(fileOrTag, workbook, tabOrDelimiter, dataKind, 'tagged')
23
+ } else {
24
+ // The file is a CSV or XLS[X] pathname. Read it now.
25
+ const dataPathname = path.resolve(modelDir, fileOrTag)
26
+ if (dataPathname.toLowerCase().endsWith('csv')) {
27
+ return handleCsvFile(fileOrTag, dataPathname, tabOrDelimiter, dataKind)
28
+ } else {
29
+ const workbook = readXlsx(dataPathname)
30
+ return handleExcelWorkbook(fileOrTag, workbook, tabOrDelimiter, dataKind, 'file')
31
+ }
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Return a `getCellValue` function for the given Excel workbook parsed from an XLS[X] file.
37
+ *
38
+ * @param {string} fileOrTag The filename (e.g., 'data.xlsx') or tag name (e.g., '?data').
39
+ * @param {*} workbook The workbook data loaded from the file.
40
+ * @param {string} tab The name of the tab within the workbook.
41
+ * @param {'data' | 'constants'} dataKind The kind of `GET DIRECT ...` being used.
42
+ * @param {'file' | 'tagged'} dataSource The reference kind, either 'file' or 'tagged'.
43
+ * @returns A `getCellValue` function.
44
+ */
45
+ function handleExcelWorkbook(fileOrTag, workbook, tab, dataKind, dataSource) {
46
+ if (workbook) {
47
+ let sheet = workbook.Sheets[tab]
48
+ if (sheet) {
49
+ return (c, r) => {
50
+ let cell = sheet[XLSX.utils.encode_cell({ c, r })]
51
+ return cell != null ? cdbl(cell.v) : null
52
+ }
53
+ } else {
54
+ throw new Error(`Direct ${dataKind} worksheet ${tab} in ${dataSource} ${fileOrTag} not found`)
55
+ }
56
+ } else {
57
+ throw new Error(`Direct ${dataKind} workbook ${dataSource} ${fileOrTag} not found`)
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Return a `getCellValue` function for the given CSV file.
63
+ *
64
+ * @param {string} file The filename of the data file.
65
+ * @param {string} dataFilename The full path to the data file.
66
+ * @param {string} delimiter The delimiter for the tabular data.
67
+ * @param {'data' | 'constants'} dataKind The kind of `GET DIRECT ...` being used.
68
+ * @returns A `getCellValue` function.
69
+ */
70
+ function handleCsvFile(file, dataPathname, delimiter, dataKind) {
71
+ // Return a `getCellValue` function for the given CSV file.
72
+ let data = readCsv(dataPathname, delimiter)
73
+ if (data) {
74
+ return (c, r) => {
75
+ let value = '0.0'
76
+ try {
77
+ value = data[r] != null && data[r][c] != null ? cdbl(data[r][c]) : null
78
+ } catch (error) {
79
+ console.error(`${error.message} in ${dataPathname}`)
80
+ }
81
+ return value
82
+ }
83
+ } else {
84
+ throw new Error(`Direct ${dataKind} file ${file} could not be read`)
85
+ }
86
+ }
@@ -17,6 +17,7 @@ import {
17
17
  newTmpVarName,
18
18
  permutationsOf,
19
19
  readCsv,
20
+ readXlsx,
20
21
  strToConst,
21
22
  vlog
22
23
  } from '../_shared/helpers.js'
@@ -208,6 +209,56 @@ export default class EquationGen extends ModelReader {
208
209
  }
209
210
  return value
210
211
  }
212
+ handleExcelWorkbook(fileOrTag, workbook, tab, dataKind, dataSource) {
213
+ // Return a `getCellValue` function for the given Excel workbook parsed from an XLS[X] file.
214
+ if (workbook) {
215
+ let sheet = workbook.Sheets[tab]
216
+ if (sheet) {
217
+ return (c, r) => {
218
+ let cell = sheet[XLSX.utils.encode_cell({ c, r })]
219
+ return cell != null ? cdbl(cell.v) : null
220
+ }
221
+ } else {
222
+ throw new Error(`Direct ${dataKind} worksheet ${tab} in ${dataSource} ${fileOrTag} not found`)
223
+ }
224
+ } else {
225
+ throw new Error(`Direct ${dataKind} workbook ${dataSource} ${fileOrTag} not found`)
226
+ }
227
+ }
228
+ handleCsvFile(file, dataPathname, tab, dataKind) {
229
+ // Return a `getCellValue` function for the given CSV file.
230
+ let data = readCsv(dataPathname, tab)
231
+ if (data) {
232
+ return (c, r) => {
233
+ let value = '0.0'
234
+ try {
235
+ value = data[r] != null && data[r][c] != null ? cdbl(data[r][c]) : null
236
+ } catch (error) {
237
+ console.error(`${error.message} in ${dataPathname}`)
238
+ }
239
+ return value
240
+ }
241
+ } else {
242
+ throw new Error(`Direct ${dataKind} file ${file} could not be read`)
243
+ }
244
+ }
245
+ handleExcelOrCsvFile(fileOrTag, tab, dataKind) {
246
+ // Return a `getCellValue` function that reads the CSV or XLS[X] content.
247
+ if (fileOrTag.startsWith('?')) {
248
+ // The file is a tag for an Excel file with data in the directData map.
249
+ let workbook = this.directData.get(fileOrTag)
250
+ return this.handleExcelWorkbook(fileOrTag, workbook, tab, dataKind, 'tagged')
251
+ } else {
252
+ // The file is a CSV or XLS[X] pathname. Read it now.
253
+ let dataPathname = path.resolve(this.modelDirname, fileOrTag)
254
+ if (dataPathname.toLowerCase().endsWith('csv')) {
255
+ return this.handleCsvFile(fileOrTag, dataPathname, tab, dataKind)
256
+ } else {
257
+ let workbook = readXlsx(dataPathname)
258
+ return this.handleExcelWorkbook(fileOrTag, workbook, tab, dataKind, 'file')
259
+ }
260
+ }
261
+ }
211
262
  lookupDataNameGen(subscripts) {
212
263
  // Construct a name for the static data array associated with a lookup variable.
213
264
  return R.map(subscript => {
@@ -360,40 +411,11 @@ export default class EquationGen extends ModelReader {
360
411
  // If direct data exists for this variable, copy it from the workbook into one or more lookups.
361
412
  let result = []
362
413
  if (this.mode === 'init-lookups') {
363
- let getCellValue
364
414
  let { file, tab, timeRowOrCol, startCell } = this.var.directDataArgs
365
- if (file.startsWith('?')) {
366
- // The file is a tag for an Excel file with data in the directData map.
367
- let workbook = this.directData.get(file)
368
- if (workbook) {
369
- let sheet = workbook.Sheets[tab]
370
- if (sheet) {
371
- getCellValue = (c, r) => {
372
- let cell = sheet[XLSX.utils.encode_cell({ c, r })]
373
- return cell != null ? cdbl(cell.v) : null
374
- }
375
- } else {
376
- throw new Error(`ERROR: Direct data worksheet ${tab} tagged ${file} not found`)
377
- }
378
- } else {
379
- throw new Error(`ERROR: Direct data workbook tagged ${file} not found`)
380
- }
381
- } else {
382
- // The file is a CSV pathname. Read it now.
383
- let csvPathname = path.resolve(this.modelDirname, file)
384
- let data = readCsv(csvPathname, tab)
385
- if (data) {
386
- getCellValue = (c, r) => {
387
- let value = '0.0'
388
- try {
389
- value = data[r] != null && data[r][c] != null ? cdbl(data[r][c]) : null
390
- } catch (error) {
391
- console.error(`${error.message} in ${csvPathname}`)
392
- }
393
- return value
394
- }
395
- }
396
- }
415
+
416
+ // Create a function that reads the CSV or XLS[X] content
417
+ let getCellValue = this.handleExcelOrCsvFile(file, tab, 'data')
418
+
397
419
  // If the data was found, convert it to a lookup.
398
420
  if (getCellValue) {
399
421
  let indexNum = 0
@@ -427,12 +449,17 @@ export default class EquationGen extends ModelReader {
427
449
  let dataCol, dataRow, dataValue, timeCol, timeRow, timeValue, nextCell
428
450
  let lookupData = ''
429
451
  let lookupSize = 0
430
- let dataAddress = XLSX.utils.decode_cell(startCell)
452
+ let dataAddress = XLSX.utils.decode_cell(startCell.toUpperCase())
431
453
  dataCol = dataAddress.c
432
454
  dataRow = dataAddress.r
455
+ if (dataCol < 0 || dataRow < 0) {
456
+ throw new Error(
457
+ `Failed to parse 'cell' argument for GET DIRECT {DATA,LOOKUPS} call for ${this.lhs}: ${startCell}`
458
+ )
459
+ }
433
460
  if (isNaN(parseInt(timeRowOrCol))) {
434
461
  // Time values are in a column.
435
- timeCol = XLSX.utils.decode_col(timeRowOrCol)
462
+ timeCol = XLSX.utils.decode_col(timeRowOrCol.toUpperCase())
436
463
  timeRow = dataRow
437
464
  dataCol += indexNum
438
465
  nextCell = () => {
@@ -468,18 +495,10 @@ export default class EquationGen extends ModelReader {
468
495
  // The subscripts may be indices to pick out a subset of the data.
469
496
  let result = this.comments
470
497
  let { file, tab, startCell } = this.var.directConstArgs
471
- let csvPathname = path.resolve(this.modelDirname, file)
472
- let data = readCsv(csvPathname, tab)
473
- if (data) {
474
- let getCellValue = (c, r) => {
475
- let value = '0.0'
476
- try {
477
- value = data[r] != null && data[r][c] != null ? cdbl(data[r][c]) : null
478
- } catch (error) {
479
- console.error(`${error.message} in ${csvPathname}`)
480
- }
481
- return value
482
- }
498
+
499
+ // Create a function that reads the CSV or XLS[X] content
500
+ let getCellValue = this.handleExcelOrCsvFile(file, tab, 'constants')
501
+ if (getCellValue) {
483
502
  // Get C subscripts in text form for the LHS in normal order.
484
503
  let modelLHSReader = new ModelLHSReader()
485
504
  modelLHSReader.read(this.var.modelLHS)
@@ -526,12 +545,15 @@ export default class EquationGen extends ModelReader {
526
545
  }
527
546
  cellOffsets.push(entry)
528
547
  }
529
- // Read CSV data into an indexed variable for each cell.
548
+ // Read tabular data into an indexed variable for each cell.
530
549
  let numericSubscripts = lhsIndexSubscripts.map(idx => idx.map(s => sub(s).value))
531
550
  let lhsSubscripts = numericSubscripts.map(s => s.reduce((a, v) => a.concat(`[${v}]`), ''))
532
- let dataAddress = XLSX.utils.decode_cell(startCell)
551
+ let dataAddress = XLSX.utils.decode_cell(startCell.toUpperCase())
533
552
  let startCol = dataAddress.c
534
553
  let startRow = dataAddress.r
554
+ if (startCol < 0 || startRow < 0) {
555
+ throw new Error(`Failed to parse 'cell' argument for GET DIRECT CONSTANTS call for ${this.lhs}: ${startCell}`)
556
+ }
535
557
  for (let i = 0; i < cellOffsets.length; i++) {
536
558
  let rowOffset = cellOffsets[i][0] ? cellOffsets[i][0] : 0
537
559
  let colOffset = cellOffsets[i][1] ? cellOffsets[i][1] : 0
@@ -651,7 +673,7 @@ export default class EquationGen extends ModelReader {
651
673
  throw new Error(`ERROR: lookup size = ${this.var.points.length} in ${this.var.refId}`)
652
674
  }
653
675
  let lookupData = R.reduce((a, p) => listConcat(a, `${cdbl(p[0])}, ${cdbl(p[1])}`, true), '', this.var.points)
654
- this.emit(`__new_lookup(${this.var.points.length}, /*copy=*/true, (double[]){ ${lookupData} });`)
676
+ this.emit(`__new_lookup(${this.var.points.length}, /*copy=*/true, (double[]){ ${lookupData} })`)
655
677
  }
656
678
  } else {
657
679
  super.visitEquation(ctx)
@@ -989,13 +1011,18 @@ export default class EquationGen extends ModelReader {
989
1011
  // Emit the size of the dimension in place of the dimension name.
990
1012
  this.emit(`${sub(varName).size}`)
991
1013
  } else {
992
- // A subscript masquerading as a variable takes the value of the loop index var plus one
993
- // (since Vensim indices are one-based).
1014
+ // A dimension masquerading as a variable (i.e., in expression position) takes the
1015
+ // value of the loop index var plus one (since Vensim indices are one-based).
994
1016
  let s = this.rhsSubscriptGen([varName])
995
1017
  // Remove the brackets around the C subscript expression.
996
1018
  s = s.slice(1, s.length - 1)
997
1019
  this.emit(`(${s} + 1)`)
998
1020
  }
1021
+ } else if (isIndex(varName)) {
1022
+ // A subscript masquerading as a variable (i.e., in expression position) takes the
1023
+ // numeric index value plus one (since Vensim indices are one-based).
1024
+ const index = sub(varName).value
1025
+ this.emit(`${index + 1}`)
999
1026
  } else {
1000
1027
  this.varNames.push(varName)
1001
1028
  if (functionName === '_VECTOR_SELECT') {
@@ -0,0 +1,115 @@
1
+ import * as R from 'ramda'
2
+
3
+ import { cartesianProductOf, canonicalName } from '../_shared/helpers.js'
4
+ import { sub, isDimension } from '../_shared/subscript.js'
5
+
6
+ import Model from '../model/model.js'
7
+
8
+ import ModelLHSReader from './model-lhs-reader.js'
9
+
10
+ /**
11
+ * Return an array of names for all variable in the model, sorted alphabetically and expanded to
12
+ * include the full set of subscripted variants for variables that include subscripts.
13
+ *
14
+ * @param canonical If true, convert names to canonical representation (variable identifiers), otherwise
15
+ * return the original name of each variable as it appears in the model.
16
+ * @returns {string[]} An array of variable names or identifiers.
17
+ */
18
+ export function expandVarNames(canonical) {
19
+ const sortedVars = R.sortBy(v => v.varName, Model.variables)
20
+ return R.uniq(
21
+ R.reduce(
22
+ (a, v) => {
23
+ if (v.varType !== 'lookup' && v.varType !== 'data' && v.includeInOutput) {
24
+ if (canonical) {
25
+ return R.concat(a, R.map(Model.cName, namesForVar(v)))
26
+ } else {
27
+ return R.concat(a, namesForVar(v))
28
+ }
29
+ } else {
30
+ return a
31
+ }
32
+ },
33
+ [],
34
+ sortedVars
35
+ )
36
+ )
37
+ }
38
+
39
+ /**
40
+ * Return an array of names for the given variable including all subscript variants.
41
+ *
42
+ * @param {*} v A `Variable` instance.
43
+ * @returns {string[]} An array of expanded names for the given variable.
44
+ */
45
+ function namesForVar(v) {
46
+ if (process.env.SDE_NONPUBLIC_USE_NEW_PARSE !== '1') {
47
+ // TODO: When the old parsing code is active, use the old ModelLHSReader. This code path
48
+ // will be removed when the old parsing code is removed.
49
+ let modelLHSReader = new ModelLHSReader()
50
+ modelLHSReader.read(v.modelLHS)
51
+ return modelLHSReader.names()
52
+ }
53
+
54
+ if (v.parsedEqn === undefined) {
55
+ // XXX: The special `Time` variable does not have a `parsedEqn`, so use the raw LHS
56
+ return [v.modelLHS]
57
+ }
58
+
59
+ // Expand each variable to get the names of all subscripted variants
60
+ const lhsVarDef = v.parsedEqn.lhs.varDef
61
+ const lhsSubRefs = lhsVarDef.subscriptRefs
62
+ if (lhsSubRefs?.length > 0) {
63
+ // At each position, expand any dimensions or use a subscript (index) directly
64
+ const subOrDimNames = lhsSubRefs.map(subRef => subRef.subName)
65
+ return expandDims(lhsVarDef.varName, subOrDimNames)
66
+ } else {
67
+ // No subscripts, so include a single variable name
68
+ return [lhsVarDef.varName]
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Return an array of all expanded subscript combinations.
74
+ *
75
+ * @param {string} baseVarName The base name of the variable.
76
+ * @param {string[]} subOrDimNames The array of subscript or dimension names.
77
+ * @returns {string[]} An array of string representations of subscripted references,
78
+ * e.g., `'x[A1,B1]' ,'x[A1,B2]', ...]`.
79
+ */
80
+ function expandDims(baseVarName, subOrDimNames) {
81
+ // Expand the dimension for each position
82
+ const expanded = subOrDimNames.map(name => expandDim(name).flat(Infinity))
83
+
84
+ // Expand these into the set of all combinations of subscripts for the variable
85
+ const origCombos = cartesianProductOf(expanded)
86
+ return origCombos.map(combo => {
87
+ const subs = combo.join(',')
88
+ return `${baseVarName}[${subs}]`
89
+ })
90
+ }
91
+
92
+ /**
93
+ * Return an array containing all subscript (index) names in the given dimension. If
94
+ * the given name is a subscript, it will return a single-element array with that
95
+ * subscript name.
96
+ *
97
+ * @param {string} subOrDimName A subscript or dimension name.
98
+ * @returns {string[]} A (possibly nested) array of subscript names.
99
+ */
100
+ function expandDim(subOrDimName) {
101
+ // Convert the name to an ID
102
+ const subOrDimId = canonicalName(subOrDimName)
103
+
104
+ if (isDimension(subOrDimId)) {
105
+ // Get the object for the dimension
106
+ const dimObj = sub(subOrDimId)
107
+
108
+ // The dimension may contain a mix of individual subscripts (indices) and/or subdimensions,
109
+ // so recursively expand them
110
+ return dimObj.modelValue.map(expandDim)
111
+ } else {
112
+ // This is an individual subscript (index), so return it directly
113
+ return [subOrDimName]
114
+ }
115
+ }