@sdeverywhere/compile 0.7.8 → 0.7.10
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 +8 -8
- package/src/_shared/helpers.js +11 -11
- package/src/_shared/subscript.js +5 -0
- package/src/generate/equation-gen.js +102 -62
- package/src/model/equation-reader.js +5 -4
- package/src/model/model.js +82 -4
- package/src/model/subscript-range-reader.js +27 -7
- package/src/parse/parser.js +8 -4
package/package.json
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdeverywhere/compile",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.10",
|
|
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": {
|
|
12
|
-
"antlr4": "4.
|
|
13
|
-
"antlr4-vensim": "0.6.
|
|
8
|
+
"antlr4": "4.12.0",
|
|
9
|
+
"antlr4-vensim": "0.6.2",
|
|
14
10
|
"bufx": "^1.0.5",
|
|
15
11
|
"byline": "^5.0.0",
|
|
16
12
|
"csv-parse": "^5.3.3",
|
|
@@ -35,6 +31,10 @@
|
|
|
35
31
|
"prettier:check": "prettier --check .",
|
|
36
32
|
"prettier:fix": "prettier --write .",
|
|
37
33
|
"precommit": "../../scripts/precommit",
|
|
38
|
-
"
|
|
34
|
+
"type-check": "tsc --noEmit -p tsconfig-test.json",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:watch": "vitest",
|
|
37
|
+
"test:ci": "vitest run",
|
|
38
|
+
"ci:build": "run-s lint prettier:check type-check test:ci"
|
|
39
39
|
}
|
|
40
40
|
}
|
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) {
|
|
@@ -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
|
|
@@ -983,22 +1005,28 @@ export default class EquationGen extends ModelReader {
|
|
|
983
1005
|
// Push the var name on the stack and then emit it.
|
|
984
1006
|
let id = ctx.Id().getText()
|
|
985
1007
|
let varName = canonicalName(id)
|
|
1008
|
+
let functionName = this.currentFunctionName()
|
|
986
1009
|
if (isDimension(varName)) {
|
|
987
|
-
if (
|
|
1010
|
+
if (functionName === '_ELMCOUNT') {
|
|
988
1011
|
// Emit the size of the dimension in place of the dimension name.
|
|
989
1012
|
this.emit(`${sub(varName).size}`)
|
|
990
1013
|
} else {
|
|
991
|
-
// A
|
|
992
|
-
// (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).
|
|
993
1016
|
let s = this.rhsSubscriptGen([varName])
|
|
994
1017
|
// Remove the brackets around the C subscript expression.
|
|
995
1018
|
s = s.slice(1, s.length - 1)
|
|
996
1019
|
this.emit(`(${s} + 1)`)
|
|
997
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}`)
|
|
998
1026
|
} else {
|
|
999
1027
|
this.varNames.push(varName)
|
|
1000
|
-
if (
|
|
1001
|
-
let argIndex = this.argIndexForFunctionName(
|
|
1028
|
+
if (functionName === '_VECTOR_SELECT') {
|
|
1029
|
+
let argIndex = this.argIndexForFunctionName(functionName)
|
|
1002
1030
|
if (argIndex === 0) {
|
|
1003
1031
|
this.vsSelectionArray = this.currentVarName()
|
|
1004
1032
|
super.visitVar(ctx)
|
|
@@ -1007,30 +1035,42 @@ export default class EquationGen extends ModelReader {
|
|
|
1007
1035
|
} else {
|
|
1008
1036
|
super.visitVar(ctx)
|
|
1009
1037
|
}
|
|
1010
|
-
} else if (
|
|
1011
|
-
if (this.argIndexForFunctionName(
|
|
1038
|
+
} else if (functionName === '_VECTOR_ELM_MAP') {
|
|
1039
|
+
if (this.argIndexForFunctionName(functionName) === 1) {
|
|
1012
1040
|
this.vemOffset = this.currentVarName()
|
|
1013
1041
|
}
|
|
1014
1042
|
super.visitVar(ctx)
|
|
1015
|
-
} else if (
|
|
1016
|
-
if (this.argIndexForFunctionName(
|
|
1043
|
+
} else if (functionName === '_VECTOR_SORT_ORDER') {
|
|
1044
|
+
if (this.argIndexForFunctionName(functionName) === 0) {
|
|
1017
1045
|
this.vsoVarName = this.currentVarName()
|
|
1018
1046
|
this.vsoTmpName = newTmpVarName()
|
|
1019
1047
|
this.emit(this.vsoTmpName)
|
|
1020
1048
|
}
|
|
1021
1049
|
super.visitVar(ctx)
|
|
1022
|
-
} else if (
|
|
1023
|
-
if (this.argIndexForFunctionName(
|
|
1050
|
+
} else if (functionName === '_ALLOCATE_AVAILABLE') {
|
|
1051
|
+
if (this.argIndexForFunctionName(functionName) === 0) {
|
|
1024
1052
|
this.aaRequestArray = this.currentVarName()
|
|
1025
1053
|
this.aaTmpName = newTmpVarName()
|
|
1026
1054
|
this.emit(this.aaTmpName)
|
|
1027
|
-
} else if (this.argIndexForFunctionName(
|
|
1055
|
+
} else if (this.argIndexForFunctionName(functionName) === 1) {
|
|
1028
1056
|
this.aaPriorityArray = this.currentVarName()
|
|
1029
1057
|
}
|
|
1030
1058
|
super.visitVar(ctx)
|
|
1031
|
-
} else if (
|
|
1059
|
+
} else if (functionName === '_GET_DATA_BETWEEN_TIMES') {
|
|
1032
1060
|
this.emit(this.currentVarName())
|
|
1033
1061
|
super.visitVar(ctx)
|
|
1062
|
+
} else if (
|
|
1063
|
+
functionName === '_LOOKUP_FORWARD' ||
|
|
1064
|
+
functionName === '_LOOKUP_BACKWARD' ||
|
|
1065
|
+
functionName === '_LOOKUP_INVERT'
|
|
1066
|
+
) {
|
|
1067
|
+
let argIndex = this.argIndexForFunctionName(functionName)
|
|
1068
|
+
if (argIndex === 0) {
|
|
1069
|
+
this.emit(this.currentVarName())
|
|
1070
|
+
super.visitVar(ctx)
|
|
1071
|
+
} else {
|
|
1072
|
+
emitVar()
|
|
1073
|
+
}
|
|
1034
1074
|
} else {
|
|
1035
1075
|
emitVar()
|
|
1036
1076
|
}
|
|
@@ -22,10 +22,11 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
extractMarkedDims,
|
|
24
24
|
indexNamesForSubscript,
|
|
25
|
+
isDimension,
|
|
26
|
+
isIndex,
|
|
25
27
|
normalizeSubscripts,
|
|
26
28
|
separatedVariableIndex,
|
|
27
|
-
sub
|
|
28
|
-
isDimension
|
|
29
|
+
sub
|
|
29
30
|
} from '../_shared/subscript.js'
|
|
30
31
|
import ModelReader from '../parse/model-reader.js'
|
|
31
32
|
import { createParser } from '../parse/parser.js'
|
|
@@ -238,8 +239,8 @@ export default class EquationReader extends ModelReader {
|
|
|
238
239
|
// Get the var name of a variable in a call and save it as a reference.
|
|
239
240
|
let id = ctx.Id().getText()
|
|
240
241
|
let varName = canonicalName(id)
|
|
241
|
-
// Do not add a dimension name as a reference.
|
|
242
|
-
if (!isDimension(varName)) {
|
|
242
|
+
// Do not add a dimension or index name as a reference.
|
|
243
|
+
if (!isDimension(varName) && !isIndex(varName)) {
|
|
243
244
|
let fn = this.currentFunctionName()
|
|
244
245
|
this.refId = varName
|
|
245
246
|
this.expandedRefIds = []
|
package/src/model/model.js
CHANGED
|
@@ -38,14 +38,45 @@ const PRINT_INIT_GRAPH = false
|
|
|
38
38
|
const PRINT_AUX_GRAPH = false
|
|
39
39
|
const PRINT_LEVEL_GRAPH = false
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
// XXX: This is needed for tests due to variables being in module-level storage
|
|
42
|
+
function resetModelState() {
|
|
43
|
+
variables.length = 0
|
|
44
|
+
inputVars.length = 0
|
|
45
|
+
variablesByName.clear()
|
|
46
|
+
constantExprs.clear()
|
|
47
|
+
nonAtoANames = Object.create(null)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read the given model parse tree and resolve all subscript and variable/equation definitions.
|
|
52
|
+
*
|
|
53
|
+
* Note that this function currently does not return anything and instead stores the parsed subscript
|
|
54
|
+
* definitions in the `subscript` module and the parsed/analyzed variables in this module.
|
|
55
|
+
*
|
|
56
|
+
* @param {import('../parse/parser.js').VensimModelParseTree} parseTree The Vensim parse tree.
|
|
57
|
+
* @param {*} spec The parsed `spec.json` object.
|
|
58
|
+
* @param {Map<string, any>} extData The map of datasets from external `.dat` files.
|
|
59
|
+
* @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT DATA`
|
|
60
|
+
* call (e.g., `?data`) to the tabular data contained in the loaded data file.
|
|
61
|
+
* @param {string} modelDirname The path to the directory containing the model (used for resolving data
|
|
62
|
+
* files for `GET DIRECT SUBSCRIPT`).
|
|
63
|
+
* @param {*} opts An optional object used by tests to stop the read process after a specific phase.
|
|
64
|
+
*/
|
|
65
|
+
function read(parseTree, spec, extData, directData, modelDirname, opts) {
|
|
42
66
|
// Some arrays need to be separated into variables with individual indices to
|
|
43
67
|
// prevent eval cycles. They are manually added to the spec file.
|
|
44
68
|
let specialSeparationDims = spec.specialSeparationDims
|
|
69
|
+
|
|
45
70
|
// Subscript ranges must be defined before reading variables that use them.
|
|
46
|
-
readSubscriptRanges(parseTree,
|
|
71
|
+
readSubscriptRanges(parseTree, modelDirname)
|
|
72
|
+
if (opts?.stopAfterReadSubscripts) return
|
|
73
|
+
resolveSubscriptRanges(spec.dimensionFamilies)
|
|
74
|
+
if (opts?.stopAfterResolveSubscripts) return
|
|
75
|
+
|
|
47
76
|
// Read variables from the model parse tree.
|
|
48
77
|
readVariables(parseTree, specialSeparationDims, directData)
|
|
78
|
+
if (opts?.stopAfterReadVariables) return
|
|
79
|
+
|
|
49
80
|
if (spec) {
|
|
50
81
|
// If the spec file contains `input/outputVarNames` (with full Vensim variable names)
|
|
51
82
|
// convert those to C names first. Otherwise, use `input/outputNames` which are already
|
|
@@ -61,20 +92,50 @@ function read(parseTree, spec, extData, directData, modelDirname) {
|
|
|
61
92
|
inputVars = spec.inputVars
|
|
62
93
|
}
|
|
63
94
|
}
|
|
95
|
+
|
|
64
96
|
// Analyze model equations to fill in more details about variables.
|
|
65
97
|
analyze()
|
|
98
|
+
if (opts?.stopAfterAnalyze) return
|
|
99
|
+
|
|
66
100
|
// Check that all input and output vars in the spec actually exist in the model.
|
|
67
101
|
checkSpecVars(spec, extData)
|
|
102
|
+
|
|
68
103
|
// Remove variables that are not referenced by an input or output variable.
|
|
69
104
|
removeUnusedVariables(spec)
|
|
105
|
+
|
|
70
106
|
// Resolve duplicate declarations by converting to one variable type.
|
|
71
107
|
resolveDuplicateDeclarations()
|
|
72
108
|
}
|
|
73
|
-
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Read subscript ranges from the given model.
|
|
112
|
+
*
|
|
113
|
+
* Note that this function currently does not return anything and instead stores the parsed subscript
|
|
114
|
+
* range definitions in the `subscript` module.
|
|
115
|
+
*
|
|
116
|
+
* @param {import('../parse/parser.js').VensimModelParseTree} parseTree The Vensim parse tree.
|
|
117
|
+
* @param {string} modelDirname The path to the directory containing the model (used for resolving data
|
|
118
|
+
* files for `GET DIRECT SUBSCRIPT`).
|
|
119
|
+
*/
|
|
120
|
+
function readSubscriptRanges(parseTree, modelDirname) {
|
|
74
121
|
// Read subscript ranges from the model.
|
|
75
122
|
let subscriptRangeReader = new SubscriptRangeReader(modelDirname)
|
|
76
|
-
subscriptRangeReader.visitModel(
|
|
123
|
+
subscriptRangeReader.visitModel(parseTree)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Process the previously read subscript/dimension definitions (stored in the `subscript` module) to
|
|
128
|
+
* resolve aliases, families, and indices.
|
|
129
|
+
*
|
|
130
|
+
* Note that this function currently does not return anything and only updates the set of dimension
|
|
131
|
+
* and subscript definitions in the `subscript` module.
|
|
132
|
+
*
|
|
133
|
+
* @param {Object.<string, string>} dimensionFamilies The optional mapping of dimension name to family name
|
|
134
|
+
* as provided in a `spec.json` file.
|
|
135
|
+
*/
|
|
136
|
+
function resolveSubscriptRanges(dimensionFamilies) {
|
|
77
137
|
let allDims = allDimensions()
|
|
138
|
+
|
|
78
139
|
// Expand dimensions that appeared in subscript range definitions into indices.
|
|
79
140
|
// Repeat until there are only indices in dimension values.
|
|
80
141
|
let dimFoundInValue
|
|
@@ -204,6 +265,21 @@ function readSubscriptRanges(tree, dimensionFamilies, indexFamilies, modelDirnam
|
|
|
204
265
|
}
|
|
205
266
|
}
|
|
206
267
|
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Read equations from the given model and generate `Variable` instances for all variables that
|
|
271
|
+
* are encountered while parsing.
|
|
272
|
+
*
|
|
273
|
+
* Note that this function currently does not return anything and instead stores the parsed
|
|
274
|
+
* variable definitions in the `model` module.
|
|
275
|
+
*
|
|
276
|
+
* @param {import('../parse/parser.js').VensimModelParseTree} tree The Vensim parse tree.
|
|
277
|
+
* @param {Object.<string, string>} specialSeparationDims The variable names that need to be
|
|
278
|
+
* separated because of circular references. A mapping from "C" variable name to "C" dimension
|
|
279
|
+
* name to separate on.
|
|
280
|
+
* @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT DATA`
|
|
281
|
+
* call (e.g., `?data`) to the tabular data contained in the loaded data file.
|
|
282
|
+
*/
|
|
207
283
|
function readVariables(tree, specialSeparationDims, directData) {
|
|
208
284
|
// Read all variables in the model parse tree.
|
|
209
285
|
// This populates the variables table with basic information for each variable
|
|
@@ -216,6 +292,7 @@ function readVariables(tree, specialSeparationDims, directData) {
|
|
|
216
292
|
v.varName = '_time'
|
|
217
293
|
addVariable(v)
|
|
218
294
|
}
|
|
295
|
+
|
|
219
296
|
function analyze() {
|
|
220
297
|
// Analyze the RHS of each equation in stages after all the variables are read.
|
|
221
298
|
// Find non-apply-to-all vars that are defined with more than one equation.
|
|
@@ -1146,6 +1223,7 @@ export default {
|
|
|
1146
1223
|
read,
|
|
1147
1224
|
refIdForVar,
|
|
1148
1225
|
refIdsWithName,
|
|
1226
|
+
resetModelState,
|
|
1149
1227
|
splitRefId,
|
|
1150
1228
|
variables,
|
|
1151
1229
|
varIndexInfo,
|
|
@@ -48,13 +48,29 @@ export default class SubscriptRangeReader extends ModelReader {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
visitSubscriptList(ctx) {
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
// Get the subscripts from each subscript index in the list.
|
|
52
|
+
for (let child of ctx.children) {
|
|
53
|
+
if (child.symbol?.type === ModelParser.Id) {
|
|
54
|
+
this.addSubscriptIndex(ctx, child)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
visitSubscriptDefList(ctx) {
|
|
59
|
+
// Subscript range definitions can have indices and numeric subscript sequences.
|
|
60
|
+
for (let child of ctx.children) {
|
|
61
|
+
if (child.symbol?.type === ModelParser.Id) {
|
|
62
|
+
this.addSubscriptIndex(ctx, child)
|
|
63
|
+
} else if (child.ruleIndex === ModelParser.RULE_subscriptSequence) {
|
|
64
|
+
this.visitSubscriptSequence(child)
|
|
65
|
+
}
|
|
55
66
|
}
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
}
|
|
68
|
+
addSubscriptIndex(ctx, child) {
|
|
69
|
+
let subscript = child.getText()
|
|
70
|
+
if (ctx.parentCtx.ruleIndex === ModelParser.RULE_subscriptRange) {
|
|
71
|
+
this.indNames.push(subscript)
|
|
72
|
+
} else if (ctx.parentCtx.ruleIndex === ModelParser.RULE_subscriptMapping) {
|
|
73
|
+
this.mappingValue.push(subscript)
|
|
58
74
|
}
|
|
59
75
|
}
|
|
60
76
|
visitSubscriptMapping(ctx) {
|
|
@@ -74,6 +90,7 @@ export default class SubscriptRangeReader extends ModelReader {
|
|
|
74
90
|
let prefix = matches[0][1]
|
|
75
91
|
let start = parseInt(matches[0][2])
|
|
76
92
|
let end = parseInt(matches[1][2])
|
|
93
|
+
// TODO get this to work with subscript mappings too
|
|
77
94
|
for (let i = start; i <= end; i++) {
|
|
78
95
|
this.indNames.push(prefix + i)
|
|
79
96
|
}
|
|
@@ -98,9 +115,12 @@ export default class SubscriptRangeReader extends ModelReader {
|
|
|
98
115
|
let lastCell = args[3]
|
|
99
116
|
// let prefix = args[4]
|
|
100
117
|
// If lastCell is a column letter, scan the column, else scan the row.
|
|
101
|
-
let dataAddress = XLSX.utils.decode_cell(firstCell)
|
|
118
|
+
let dataAddress = XLSX.utils.decode_cell(firstCell.toUpperCase())
|
|
102
119
|
let col = dataAddress.c
|
|
103
120
|
let row = dataAddress.r
|
|
121
|
+
if (col < 0 || row < 0) {
|
|
122
|
+
throw new Error(`Failed to parse 'firstcell' argument for GET DIRECT SUBSCRIPT call: ${firstCell}`)
|
|
123
|
+
}
|
|
104
124
|
let nextCell
|
|
105
125
|
if (isNaN(parseInt(lastCell))) {
|
|
106
126
|
nextCell = () => row++
|
package/src/parse/parser.js
CHANGED
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
import antlr4 from 'antlr4'
|
|
4
4
|
import { ModelLexer, ModelParser } from 'antlr4-vensim'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} VensimModelParseTree
|
|
8
|
+
*/
|
|
9
|
+
|
|
6
10
|
/**
|
|
7
11
|
* Create a `ModelParser` for the given model text, which can be the
|
|
8
12
|
* contents of an entire `mdl` file, or a portion of one (e.g., an
|
|
9
13
|
* expression or definition).
|
|
10
14
|
*
|
|
11
|
-
* @param input The string containing the model text.
|
|
12
|
-
* @return A `ModelParser` from which a parse tree can be obtained.
|
|
15
|
+
* @param {string} input The string containing the model text.
|
|
16
|
+
* @return {ModelParser} A `ModelParser` from which a parse tree can be obtained.
|
|
13
17
|
*/
|
|
14
18
|
export function createParser(input) {
|
|
15
19
|
let chars = new antlr4.InputStream(input)
|
|
@@ -23,8 +27,8 @@ export function createParser(input) {
|
|
|
23
27
|
/**
|
|
24
28
|
* Read the given model text and return a parse tree.
|
|
25
29
|
*
|
|
26
|
-
* @param input The string containing the model text.
|
|
27
|
-
* @return A parse tree representation of the model.
|
|
30
|
+
* @param {string} input The string containing the model text.
|
|
31
|
+
* @return {VensimModelParseTree} A parse tree representation of the model.
|
|
28
32
|
*/
|
|
29
33
|
export function parseModel(input) {
|
|
30
34
|
let parser = createParser(input)
|