@sdeverywhere/compile 0.7.10 → 0.7.12

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,10 +1,11 @@
1
1
  {
2
2
  "name": "@sdeverywhere/compile",
3
- "version": "0.7.10",
3
+ "version": "0.7.12",
4
4
  "description": "The core Vensim to C compiler for the SDEverywhere tool suite.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "dependencies": {
8
+ "@sdeverywhere/parse": "^0.1.0",
8
9
  "antlr4": "4.12.0",
9
10
  "antlr4-vensim": "0.6.2",
10
11
  "bufx": "^1.0.5",
@@ -375,7 +375,3 @@ export let vlog = (title, value, depth = 1) => {
375
375
  console.trace()
376
376
  }
377
377
  }
378
- export let abend = error => {
379
- console.error(error)
380
- process.exit(1)
381
- }
@@ -35,8 +35,16 @@ export async function readDat(pathname, prefix = '') {
35
35
  }
36
36
  }
37
37
 
38
- return new Promise(resolve => {
39
- let stream = byline(fs.createReadStream(pathname, 'utf8'))
38
+ return new Promise((resolve, reject) => {
39
+ // Errors from the read stream aren't propagated by the byline package
40
+ // so we attach the error handler to `readStream` rather than to `stream`
41
+ let readStream = fs.createReadStream(pathname, 'utf8')
42
+ let stream = byline(readStream)
43
+ readStream.on('error', e => {
44
+ stream.destroy()
45
+ reject(new Error(`Failed to read dat file: ${e.message}`))
46
+ })
47
+
40
48
  stream.on('data', line => {
41
49
  let values = splitDatLine(line)
42
50
  if (values.length === 1) {
@@ -63,6 +71,7 @@ export async function readDat(pathname, prefix = '') {
63
71
  lineNum++
64
72
  // if (lineNum % 1e5 === 0) console.log(num(lineNum).format('0,0'))
65
73
  })
74
+
66
75
  stream.on('end', () => {
67
76
  addValues()
68
77
  resolve(log)
@@ -1,18 +1,19 @@
1
1
  import * as R from 'ramda'
2
2
 
3
- import { asort, lines, strlist, abend, mapIndexed } from '../_shared/helpers.js'
3
+ import { asort, lines, strlist, 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,32 +26,37 @@ 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
- try {
34
- Model.read(parseTree, spec, extData, directData, modelDirname)
35
- // In list mode, print variables to the console instead of generating code.
36
- if (operation === 'printRefIdTest') {
37
- Model.printRefIdTest()
38
- } else if (operation === 'printRefGraph') {
39
- Model.printRefGraph(opts.varname)
40
- } else if (operation === 'convertNames') {
41
- // Do not generate output, but leave the results of model analysis.
42
- } else if (operation === 'generateC') {
43
- // Generate code for each variable in the proper order.
44
- let code = emitDeclCode()
45
- code += emitInitLookupsCode()
46
- code += emitInitConstantsCode()
47
- code += emitInitLevelsCode()
48
- code += emitEvalCode()
49
- code += emitIOCode()
50
- return code
51
- }
52
- } catch (e) {
53
- abend(e)
40
+ Model.read(parsedModel, spec, extData, directData, modelDirname)
41
+ // In list mode, print variables to the console instead of generating code.
42
+ if (operations.includes('printRefIdTest')) {
43
+ Model.printRefIdTest()
44
+ }
45
+ if (operations.includes('printRefGraph')) {
46
+ Model.printRefGraph(opts.varname)
47
+ }
48
+ if (operations.includes('convertNames')) {
49
+ // Do not generate output, but leave the results of model analysis.
50
+ }
51
+ if (operations.includes('generateC')) {
52
+ // Generate code for each variable in the proper order.
53
+ let code = emitDeclCode()
54
+ code += emitInitLookupsCode()
55
+ code += emitInitConstantsCode()
56
+ code += emitInitLevelsCode()
57
+ code += emitEvalCode()
58
+ code += emitIOCode()
59
+ return code
54
60
  }
55
61
  }
56
62
 
@@ -198,10 +204,21 @@ void ${name}${idx}() {
198
204
  }
199
205
  let funcCalls = R.pipe(mapIndexed(funcCall), lines)
200
206
 
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)
207
+ // Break the vars into chunks. The default value of 30 was empirically
208
+ // determined by looking at runtime performance and memory usage of the
209
+ // En-ROADS model on various devices.
210
+ let chunkSize
211
+ if (process.env.SDE_CODE_GEN_CHUNK_SIZE) {
212
+ chunkSize = parseInt(process.env.SDE_CODE_GEN_CHUNK_SIZE)
213
+ } else {
214
+ chunkSize = 30
215
+ }
216
+ let chunks
217
+ if (chunkSize > 0) {
218
+ chunks = R.splitEvery(chunkSize, vars)
219
+ } else {
220
+ chunks = [vars]
221
+ }
205
222
 
206
223
  if (!preStep) {
207
224
  preStep = ''
@@ -296,33 +313,8 @@ ${postStep}
296
313
  // Return a list of var names for all variables except lookups and data variables.
297
314
  // The names are in Vensim format if vensimNames is true, otherwise they are in C format.
298
315
  // 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
- )
316
+ const canonicalNames = !vensimNames
317
+ return expandVarNames(canonicalNames)
326
318
  }
327
319
  //
328
320
  // 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
+ }
@@ -673,7 +673,7 @@ export default class EquationGen extends ModelReader {
673
673
  throw new Error(`ERROR: lookup size = ${this.var.points.length} in ${this.var.refId}`)
674
674
  }
675
675
  let lookupData = R.reduce((a, p) => listConcat(a, `${cdbl(p[0])}, ${cdbl(p[1])}`, true), '', this.var.points)
676
- 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} })`)
677
677
  }
678
678
  } else {
679
679
  super.visitEquation(ctx)
@@ -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
+ }
@@ -0,0 +1,89 @@
1
+ import { cartesianProductOf, cdbl } from '../_shared/helpers.js'
2
+ import { isDimension, normalizeSubscripts, sub } from '../_shared/subscript.js'
3
+
4
+ /**
5
+ * Generate code for a single element in a const list definition.
6
+ *
7
+ * @param {*} variable The `Variable` instance to process.
8
+ * @param {*} parsedEqn The parsed equation.
9
+ * @return {string[]} An array of strings containing the generated C code for the variable,
10
+ * one string per line of code.
11
+ */
12
+ export function generateConstListElement(variable, parsedEqn) {
13
+ // In the "read variables" phase, const lists are expanded into separated variable
14
+ // definitions, so `variable` here will have `subscripts` that represent specific
15
+ // subscript indices in normalized order (alphabetized by parent dimension/family
16
+ // name). However, we need to consult the LHS subscripts/dimensions, which will
17
+ // be in the original order from the model equation.
18
+ //
19
+ // In the following example,
20
+ // we have a 2D variable whose original dimensions are not in normal order:
21
+ // DimA: A1, A2 ~~|
22
+ // DimB: B1, B2, B3 ~~|
23
+ // x[DimB, DimA] = 1, 2; 3, 4; 5, 6; ~~|
24
+ //
25
+ // The variable `x` will have been separated into:
26
+ // x[B1,A1]
27
+ // x[B1,A2]
28
+ // x[B2,A1]
29
+ // ...
30
+ //
31
+ // Each one will refer to a single element from the original const list. To determine
32
+ // which element in the const list goes with which variable instance, we build an array
33
+ // of all subscript combinations and then find the index of the one that matches the
34
+ // combination used for the separated variable instance.
35
+ const lhsSubRefs = variable.parsedEqn.lhs.varDef.subscriptRefs
36
+ const lhsSubIds = lhsSubRefs.map(subRef => subRef.subId)
37
+ const subIdArrays = lhsSubIds.map(subOrDimId => {
38
+ if (isDimension(subOrDimId)) {
39
+ // Use the full array of subscripts (indexes) for the dimension at this position
40
+ return sub(subOrDimId).value
41
+ } else {
42
+ // This is a single subscript (index), so use an array with a single element
43
+ return [subOrDimId]
44
+ }
45
+ })
46
+
47
+ // Continuing with the above example, at this point we will have a 2D array:
48
+ // [
49
+ // [_b1,_b2,_b3],
50
+ // [_a1,_a2]
51
+ // ]
52
+ // We expand these into the set of all combinations of subscripts in the original
53
+ // order of the dimensions from the equation LHS.
54
+ const origCombos = cartesianProductOf(subIdArrays)
55
+
56
+ // Now we have the combinations in original order:
57
+ // [_b1,_a1]
58
+ // [_b1,_a2]
59
+ // [_b2,_a1]
60
+ // ...
61
+ // But we need to put them into normalized order so that we can find the index of
62
+ // `variable.subscripts` (which is already in normalized order).
63
+ const normalizedCombos = origCombos.map(normalizeSubscripts)
64
+
65
+ // Convert to strings to make matching easier. Now we have the strings in normalized order:
66
+ // [_a1,_b1]
67
+ // [_a2,_b1]
68
+ // [_a1,_b2]
69
+ // ...
70
+ const comboStrings = normalizedCombos.map(combo => combo.map(subId => `[${subId}]`).join(''))
71
+
72
+ // Convert `variable.subscripts` into the same format so that we can do an array lookup,
73
+ // for example if this separated variable instance is x[_a2,_b1], this will be:
74
+ // [_a2,_b1]
75
+ const lhsComboString = variable.subscripts.map(subId => `[${subId}]`).join('')
76
+
77
+ // Find the index of the combination that matches `variable.subscripts`
78
+ const constIndex = comboStrings.indexOf(lhsComboString)
79
+ if (constIndex < 0) {
80
+ throw new Error(`Failed to determine index of const list element for ${variable.refId}`)
81
+ }
82
+
83
+ // Determine the LHS and RHS of the const assignment
84
+ const lhsVarId = variable.varName
85
+ const lhsIndicesString = variable.subscripts.map(subId => `[${sub(subId).value}]`).join('')
86
+ const lhsRef = `${lhsVarId}${lhsIndicesString}`
87
+ const rhsConstValue = cdbl(parsedEqn.rhs.constants[constIndex].value)
88
+ return ` ${lhsRef} = ${rhsConstValue};`
89
+ }
@@ -0,0 +1,91 @@
1
+ import XLSX from 'xlsx'
2
+
3
+ import { cartesianProductOf } from '../_shared/helpers.js'
4
+ import { indexInSepDim, isDimension, sub } from '../_shared/subscript.js'
5
+
6
+ import { handleExcelOrCsvFile } from './direct-data-helpers.js'
7
+
8
+ /**
9
+ * Generate code for a variable that uses `GET DIRECT CONSTANTS` to source constant values from an external
10
+ * file (in CSV or Excel format).
11
+ *
12
+ * @param {*} variable The `Variable` instance to process.
13
+ * @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT CONSTANTS` 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
+ * @return {string[]} An array of strings containing the generated C code for the variable,
17
+ * one string per line of code.
18
+ */
19
+ export function generateDirectConstInit(variable, directData, modelDir) {
20
+ // Create a function that reads the CSV or XLS[X] content
21
+ let { file, tab, startCell } = variable.directConstArgs
22
+ let getCellValue = handleExcelOrCsvFile(file, tab, 'constants', directData, modelDir)
23
+
24
+ // Get C subscripts in text form for the LHS in normal order.
25
+ let lhsSubIds = variable.parsedEqn.lhs.varDef.subscriptRefs?.map(s => s.subId) || []
26
+ let modelDimNames = lhsSubIds.filter(s => isDimension(s))
27
+
28
+ // Generate offsets from the start cell in the table corresponding to LHS indices.
29
+ let cellOffsets = []
30
+ let cSubscripts = variable.subscripts.map(s => (isDimension(s) ? sub(s).value : [s]))
31
+ let lhsIndexSubscripts = cartesianProductOf(cSubscripts)
32
+
33
+ // Find the table cell offset for each LHS index tuple.
34
+ let lines = []
35
+ for (let indexSubscripts of lhsIndexSubscripts) {
36
+ let entry = [null, null]
37
+ for (let i = 0; i < variable.subscripts.length; i++) {
38
+ // LHS dimensions or indices in a separated dimension map to table cells.
39
+ let lhsSubscript = variable.subscripts[i]
40
+ if (isDimension(lhsSubscript) || indexInSepDim(lhsSubscript, variable)) {
41
+ // Consider the LHS index subscript at this position.
42
+ let indexSubscript = indexSubscripts[i]
43
+ let ind = sub(indexSubscript)
44
+ // Find the model subscript position corresponding to the LHS index subscript.
45
+ for (let iModelDim = 0; iModelDim < modelDimNames.length; iModelDim++) {
46
+ // Only fill an entry position once.
47
+ if (entry[iModelDim] === null) {
48
+ let modelDim = sub(modelDimNames[iModelDim])
49
+ if (modelDim.family === ind.family) {
50
+ // Set the numeric index for the model dimension in the cell offset entry.
51
+ // Use the position within the dimension to map subdimensions onto cell offsets.
52
+ let pos = modelDim.value.indexOf(indexSubscript)
53
+ // Vectors use a 2D cell offset that maps to columns in the first row.
54
+ // Tables use a 2D cell offset with the row or column matching the model dimension.
55
+ let entryRowOrCol = modelDimNames.length > 1 ? iModelDim : 1
56
+ entry[entryRowOrCol] = pos
57
+ break
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ // Replace unfilled entry positions with zero.
64
+ entry = entry.map(x => (x === null ? 0 : x))
65
+ // Read values by column first when the start cell ends with an asterisk.
66
+ // Ref: https://www.vensim.com/documentation/fn_get_direct_constants.html
67
+ if (startCell.endsWith('*')) {
68
+ entry.reverse()
69
+ }
70
+ cellOffsets.push(entry)
71
+ }
72
+
73
+ // Read tabular data into an indexed variable for each cell.
74
+ let numericSubscripts = lhsIndexSubscripts.map(idx => idx.map(s => sub(s).value))
75
+ let lhsSubscripts = numericSubscripts.map(s => s.reduce((a, v) => a.concat(`[${v}]`), ''))
76
+ let dataAddress = XLSX.utils.decode_cell(startCell.toUpperCase())
77
+ let startCol = dataAddress.c
78
+ let startRow = dataAddress.r
79
+ if (startCol < 0 || startRow < 0) {
80
+ throw new Error(`Failed to parse 'cell' argument for GET DIRECT CONSTANTS call for ${variable.refId}: ${startCell}`)
81
+ }
82
+ for (let i = 0; i < cellOffsets.length; i++) {
83
+ let rowOffset = cellOffsets[i][0] ? cellOffsets[i][0] : 0
84
+ let colOffset = cellOffsets[i][1] ? cellOffsets[i][1] : 0
85
+ let dataValue = getCellValue(startCol + colOffset, startRow + rowOffset)
86
+ let lhs = `${variable.varName}${lhsSubscripts[i] || ''}`
87
+ lines.push(` ${lhs} = ${dataValue};`)
88
+ }
89
+
90
+ return lines
91
+ }