@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 +7 -6
- package/src/_shared/helpers.js +11 -11
- package/src/_shared/subscript.js +5 -0
- package/src/generate/code-gen.js +38 -42
- package/src/generate/direct-data-helpers.js +86 -0
- package/src/generate/equation-gen.js +79 -52
- package/src/generate/expand-var-names.js +115 -0
- package/src/generate/gen-const-list.js +89 -0
- package/src/generate/gen-direct-const.js +91 -0
- package/src/generate/gen-equation.js +273 -0
- package/src/generate/gen-expr.js +851 -0
- package/src/generate/gen-lookup-from-direct.js +105 -0
- package/src/generate/gen-lookup-from-external.js +115 -0
- package/src/generate/gen-lookup-from-points.js +85 -0
- package/src/model/equation-reader.js +32 -26
- package/src/model/model.js +160 -17
- package/src/model/read-equation-fn-delay.js +312 -0
- package/src/model/read-equation-fn-npv.js +67 -0
- package/src/model/read-equation-fn-smooth.js +133 -0
- package/src/model/read-equation-fn-trend.js +44 -0
- package/src/model/read-equation-fn-with-lookup.js +37 -0
- package/src/model/read-equations.js +795 -0
- package/src/model/read-subscripts.js +77 -0
- package/src/model/read-variables.js +264 -0
- package/src/model/reduce-variables.js +166 -0
- package/src/model/subscript-range-reader.js +4 -1
- package/src/model/variable.js +7 -2
- package/src/parse/parser.js +8 -4
- package/src/parse-and-generate.js +65 -12
package/package.json
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdeverywhere/compile",
|
|
3
|
-
"version": "0.7.
|
|
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
|
-
"
|
|
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
|
}
|
package/src/_shared/helpers.js
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
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 })
|
package/src/_shared/subscript.js
CHANGED
|
@@ -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) {
|
package/src/generate/code-gen.js
CHANGED
|
@@ -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
|
|
9
|
+
import { expandVarNames } from './expand-var-names.js'
|
|
9
10
|
|
|
10
|
-
export function generateCode(
|
|
11
|
-
return codeGenerator(
|
|
11
|
+
export function generateCode(parsedModel, opts) {
|
|
12
|
+
return codeGenerator(parsedModel, opts).generate()
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
let codeGenerator = (
|
|
15
|
-
const { spec,
|
|
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 =>
|
|
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(
|
|
41
|
+
Model.read(parsedModel, spec, extData, directData, modelDirname)
|
|
35
42
|
// In list mode, print variables to the console instead of generating code.
|
|
36
|
-
if (
|
|
43
|
+
if (operations.includes('printRefIdTest')) {
|
|
37
44
|
Model.printRefIdTest()
|
|
38
|
-
}
|
|
45
|
+
}
|
|
46
|
+
if (operations.includes('printRefGraph')) {
|
|
39
47
|
Model.printRefGraph(opts.varname)
|
|
40
|
-
}
|
|
48
|
+
}
|
|
49
|
+
if (operations.includes('convertNames')) {
|
|
41
50
|
// Do not generate output, but leave the results of model analysis.
|
|
42
|
-
}
|
|
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
|
|
202
|
-
// determined by looking at runtime performance and memory usage
|
|
203
|
-
//
|
|
204
|
-
let
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|