@sdeverywhere/compile 0.7.9 → 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 CHANGED
@@ -1,12 +1,8 @@
1
1
  {
2
2
  "name": "@sdeverywhere/compile",
3
- "version": "0.7.9",
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
8
  "antlr4": "4.12.0",
@@ -35,6 +31,10 @@
35
31
  "prettier:check": "prettier --check .",
36
32
  "prettier:fix": "prettier --write .",
37
33
  "precommit": "../../scripts/precommit",
38
- "ci:build": "run-s lint prettier:check"
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
  }
@@ -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) {
@@ -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
@@ -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') {
@@ -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 = []
@@ -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
- function read(parseTree, spec, extData, directData, modelDirname) {
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, spec.dimensionFamilies, spec.indexFamilies, modelDirname)
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
- function readSubscriptRanges(tree, dimensionFamilies, indexFamilies, modelDirname) {
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(tree)
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,
@@ -115,9 +115,12 @@ export default class SubscriptRangeReader extends ModelReader {
115
115
  let lastCell = args[3]
116
116
  // let prefix = args[4]
117
117
  // If lastCell is a column letter, scan the column, else scan the row.
118
- let dataAddress = XLSX.utils.decode_cell(firstCell)
118
+ let dataAddress = XLSX.utils.decode_cell(firstCell.toUpperCase())
119
119
  let col = dataAddress.c
120
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
+ }
121
124
  let nextCell
122
125
  if (isNaN(parseInt(lastCell))) {
123
126
  nextCell = () => row++
@@ -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)