@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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as R from 'ramda'
|
|
2
|
+
import XLSX from 'xlsx'
|
|
3
|
+
|
|
4
|
+
import { listConcat } from '../_shared/helpers.js'
|
|
5
|
+
import { sub } from '../_shared/subscript.js'
|
|
6
|
+
|
|
7
|
+
import { handleExcelOrCsvFile } from './direct-data-helpers.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate code for a variable that uses `GET DIRECT DATA` to source data from an external file
|
|
11
|
+
* (in CSV or Excel format).
|
|
12
|
+
*
|
|
13
|
+
* @param {*} variable The `Variable` instance to process.
|
|
14
|
+
* @param {'decl' | 'init-lookups'} mode The code generation mode.
|
|
15
|
+
* @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT DATA` call (e.g.,
|
|
16
|
+
* `?data`) to the tabular data contained in the loaded data file.
|
|
17
|
+
* @param {string} modelDir The path to the directory containing the model (used for resolving data files).
|
|
18
|
+
* @param {string} varLhs The C code for the LHS variable reference.
|
|
19
|
+
* @return {string[]} An array of strings containing the generated C code for the variable,
|
|
20
|
+
* one string per line of code.
|
|
21
|
+
*/
|
|
22
|
+
export function generateLookupsFromDirectData(variable, mode, directData, modelDir, varLhs) {
|
|
23
|
+
if (mode === 'decl') {
|
|
24
|
+
// Nothing to emit in decl mode
|
|
25
|
+
return []
|
|
26
|
+
} else if (mode !== 'init-lookups') {
|
|
27
|
+
throw new Error(`Invalid code gen mode '${mode}' for data variable ${variable.modelLHS}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Create a function that reads the CSV or XLS[X] content
|
|
31
|
+
const { file, tab, timeRowOrCol, startCell } = variable.directDataArgs
|
|
32
|
+
const getCellValue = handleExcelOrCsvFile(file, tab, 'data', directData, modelDir)
|
|
33
|
+
|
|
34
|
+
// If direct data exists for this variable, copy it from the workbook into one or more lookups
|
|
35
|
+
let indexNum = 0
|
|
36
|
+
if (!R.isEmpty(variable.separationDims)) {
|
|
37
|
+
// Generate a lookup for a separated index in the variable's dimension.
|
|
38
|
+
if (variable.separationDims.length > 1) {
|
|
39
|
+
console.error(`WARNING: direct data variable ${variable.varName} separated on more than one dimension`)
|
|
40
|
+
}
|
|
41
|
+
let dimName = variable.separationDims[0]
|
|
42
|
+
for (let subscript of variable.subscripts) {
|
|
43
|
+
if (sub(subscript).family === dimName) {
|
|
44
|
+
// Use the index value in the subscript family when that is the separation dimension.
|
|
45
|
+
indexNum = sub(subscript).value
|
|
46
|
+
break
|
|
47
|
+
}
|
|
48
|
+
if (sub(dimName).value.includes(subscript)) {
|
|
49
|
+
// Look up the index when the separation dimension is a subdimension.
|
|
50
|
+
indexNum = sub(dimName).value.indexOf(subscript)
|
|
51
|
+
break
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return [generateDirectDataLookup(varLhs, getCellValue, timeRowOrCol, startCell, indexNum)]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function generateDirectDataLookup(varLhs, getCellValue, timeRowOrCol, startCell, indexNum) {
|
|
59
|
+
// Read a row or column of data as (time, value) pairs from the worksheet.
|
|
60
|
+
// The cell(c,r) function wraps data access by column and row.
|
|
61
|
+
let lookupData = ''
|
|
62
|
+
let lookupSize = 0
|
|
63
|
+
let dataAddress = XLSX.utils.decode_cell(startCell.toUpperCase())
|
|
64
|
+
let dataCol = dataAddress.c
|
|
65
|
+
let dataRow = dataAddress.r
|
|
66
|
+
if (dataCol < 0 || dataRow < 0) {
|
|
67
|
+
throw new Error(`Failed to parse 'cell' argument for GET DIRECT {DATA,LOOKUPS} call for ${varLhs}: ${startCell}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let timeCol, timeRow, nextCell
|
|
71
|
+
if (isNaN(parseInt(timeRowOrCol))) {
|
|
72
|
+
// Time values are in a column.
|
|
73
|
+
timeCol = XLSX.utils.decode_col(timeRowOrCol.toUpperCase())
|
|
74
|
+
timeRow = dataRow
|
|
75
|
+
dataCol += indexNum
|
|
76
|
+
nextCell = () => {
|
|
77
|
+
dataRow++
|
|
78
|
+
timeRow++
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
// Time values are in a row.
|
|
82
|
+
timeCol = dataCol
|
|
83
|
+
timeRow = XLSX.utils.decode_row(timeRowOrCol)
|
|
84
|
+
dataRow += indexNum
|
|
85
|
+
nextCell = () => {
|
|
86
|
+
dataCol++
|
|
87
|
+
timeCol++
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let timeValue = getCellValue(timeCol, timeRow)
|
|
92
|
+
let dataValue = getCellValue(dataCol, dataRow)
|
|
93
|
+
while (timeValue != null && dataValue != null) {
|
|
94
|
+
lookupData = listConcat(lookupData, `${timeValue}, ${dataValue}`, true)
|
|
95
|
+
lookupSize++
|
|
96
|
+
nextCell()
|
|
97
|
+
dataValue = getCellValue(dataCol, dataRow)
|
|
98
|
+
timeValue = getCellValue(timeCol, timeRow)
|
|
99
|
+
}
|
|
100
|
+
if (lookupSize === 0) {
|
|
101
|
+
throw new Error(`Empty lookup data array for ${varLhs}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return ` ${varLhs} = __new_lookup(${lookupSize}, /*copy=*/true, (double[]){ ${lookupData} });`
|
|
105
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as R from 'ramda'
|
|
2
|
+
|
|
3
|
+
import { cartesianProductOf, permutationsOf } from '../_shared/helpers.js'
|
|
4
|
+
import { isDimension, sub } from '../_shared/subscript.js'
|
|
5
|
+
import { pointsString } from './gen-lookup-from-points.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate code for a data variable whose data is sourced from an external file (in DAT format).
|
|
9
|
+
*
|
|
10
|
+
* @param {*} variable The `Variable` instance to process.
|
|
11
|
+
* @param {'decl' | 'init-lookups'} mode The code generation mode.
|
|
12
|
+
* @param {Map<string, any>} extData The map of datasets from external `.dat` files.
|
|
13
|
+
* @param {string} varLhs The C code for the LHS variable reference.
|
|
14
|
+
* @return {string[]} An array of strings containing the generated C code for the variable,
|
|
15
|
+
* one string per line of code.
|
|
16
|
+
*/
|
|
17
|
+
export function generateLookupsFromExternalData(variable, mode, extData, varLhs) {
|
|
18
|
+
if (mode !== 'decl' && mode !== 'init-lookups') {
|
|
19
|
+
throw new Error(`Invalid code gen mode '${mode}' for data variable ${variable.modelLHS}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// If there is external data for this variable, copy it from an external file to a lookup.
|
|
23
|
+
// Just like in `generateLookupFromPoints`, we declare static arrays to hold the data points in
|
|
24
|
+
// the first pass ("decl" mode), then initialize each `Lookup` using that data in the second pass
|
|
25
|
+
// ("init" mode).
|
|
26
|
+
const newLookup = (name, lhs, data, subscriptIndexes) => {
|
|
27
|
+
if (!data) {
|
|
28
|
+
throw new Error(`Data for ${name} not found in external data sources`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (data.size === 0) {
|
|
32
|
+
throw new Error(`Empty lookup data array for ${lhs}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const dataName = variable.varName + '_data_' + R.map(i => `_${i}_`, subscriptIndexes).join('')
|
|
36
|
+
if (mode === 'decl') {
|
|
37
|
+
// In decl mode, declare a static data array that will be used to create the associated `Lookup`
|
|
38
|
+
// at init time
|
|
39
|
+
const points = pointsString(Array.from(data.entries()))
|
|
40
|
+
return `double ${dataName}[${data.size * 2}] = { ${points} };`
|
|
41
|
+
} else if (mode === 'init-lookups') {
|
|
42
|
+
// In init mode, create the `Lookup`, passing in a pointer to the static data array declared in decl mode.
|
|
43
|
+
return ` ${lhs} = __new_lookup(${data.size}, /*copy=*/false, ${dataName});`
|
|
44
|
+
} else {
|
|
45
|
+
return []
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// There are three common cases that we handle:
|
|
50
|
+
// - variable has no subscripts (C variable _thing = _thing from dat file)
|
|
51
|
+
// - variable has subscript(s) (C variable with index _thing[0] = _thing[_subscript] from dat file)
|
|
52
|
+
// - variable has dimension(s) (C variable in for loop, _thing[i] = _thing[_subscript_i] from dat file)
|
|
53
|
+
|
|
54
|
+
if (!variable.subscripts || variable.subscripts.length === 0) {
|
|
55
|
+
// No subscripts
|
|
56
|
+
const data = extData.get(variable.varName)
|
|
57
|
+
return [newLookup(variable.varName, varLhs, data, [])]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (variable.subscripts.length === 1 && !isDimension(variable.subscripts[0])) {
|
|
61
|
+
// There is exactly one subscript
|
|
62
|
+
const subscript = variable.subscripts[0]
|
|
63
|
+
const nameInDat = `${variable.varName}[${subscript}]`
|
|
64
|
+
const data = extData.get(nameInDat)
|
|
65
|
+
const subIndex = sub(subscript).value
|
|
66
|
+
return [newLookup(nameInDat, varLhs, data, [subIndex])]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!R.all(s => isDimension(s), variable.subscripts)) {
|
|
70
|
+
// We don't yet handle the case where there are more than one subscript or a mix of
|
|
71
|
+
// subscripts and dimensions
|
|
72
|
+
// TODO: Remove this restriction
|
|
73
|
+
throw new Error(`Data variable ${variable.varName} has >= 2 subscripts; not yet handled`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// At this point, we know that we have one or more dimensions; compute all combinations
|
|
77
|
+
// of the dimensions that we will iterate over
|
|
78
|
+
const lines = []
|
|
79
|
+
const allDims = R.map(s => sub(s).value, variable.subscripts)
|
|
80
|
+
const dimTuples = cartesianProductOf(allDims)
|
|
81
|
+
for (const dims of dimTuples) {
|
|
82
|
+
// Note: It appears that the dat file can have the subscripts in a different order
|
|
83
|
+
// than what SDE uses when declaring the C array. If we don't find data for one
|
|
84
|
+
// order, we try the other possible permutations.
|
|
85
|
+
const dimNamePermutations = permutationsOf(dims)
|
|
86
|
+
let nameInDat, data
|
|
87
|
+
for (const dimNames of dimNamePermutations) {
|
|
88
|
+
nameInDat = `${variable.varName}[${dimNames.join(',')}]`
|
|
89
|
+
data = extData.get(nameInDat)
|
|
90
|
+
if (data) {
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!data) {
|
|
95
|
+
// We currently treat this as a warning, not an error, since there can sometimes be
|
|
96
|
+
// datasets that are a sparse matrix, i.e., data is not defined for certain dimensions.
|
|
97
|
+
// For these cases, the lookup will not be initialized (the Lookup pointer will remain
|
|
98
|
+
// NULL, and any calls to `LOOKUP` will return `:NA:`.
|
|
99
|
+
if (mode === 'decl') {
|
|
100
|
+
console.error(`WARNING: Data for ${nameInDat} not found in external data sources`)
|
|
101
|
+
}
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const subscriptIndexes = R.map(dim => sub(dim).value, dims)
|
|
106
|
+
const varSubscripts = R.map(index => `[${index}]`, subscriptIndexes).join('')
|
|
107
|
+
const lhs = `${variable.varName}${varSubscripts}`
|
|
108
|
+
const lookup = newLookup(nameInDat, lhs, data, subscriptIndexes)
|
|
109
|
+
if (lookup) {
|
|
110
|
+
lines.push(lookup)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return lines
|
|
115
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { cdbl } from '../_shared/helpers.js'
|
|
2
|
+
import { isDimension, isTrivialDimension, sub } from '../_shared/subscript.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate code for a data or lookup variable that contains an explicit set of points.
|
|
6
|
+
*
|
|
7
|
+
* @param {*} variable The `Variable` instance to process.
|
|
8
|
+
* @param {'decl' | 'init-lookups'} mode The code generation mode.
|
|
9
|
+
* @param {boolean} copy If false, a static data array will be used (good for larger data sets).
|
|
10
|
+
* If true, the data will inlined and copied when initializing the lookup (good for smaller data sets).
|
|
11
|
+
* @param {string} varLhs The C code for the LHS variable reference.
|
|
12
|
+
* @param {LoopIndexVars} loopIndexVars The loop index state.
|
|
13
|
+
* @return {string[]} An array of strings containing the generated C code for the variable,
|
|
14
|
+
* one string per line of code.
|
|
15
|
+
*/
|
|
16
|
+
export function generateLookupFromPoints(variable, mode, copy, varLhs, loopIndexVars) {
|
|
17
|
+
if (variable.points.length === 0) {
|
|
18
|
+
throw new Error(`Empty lookup data array for ${variable.modelLHS}`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (copy) {
|
|
22
|
+
// Inline the data points and copy them when initializing the lookup
|
|
23
|
+
if (mode === 'decl') {
|
|
24
|
+
// Nothing to emit in decl mode
|
|
25
|
+
return []
|
|
26
|
+
} else if (mode === 'init-lookups') {
|
|
27
|
+
// In init mode, generate a new lookup using the data points from the variable
|
|
28
|
+
const points = pointsString(variable.points)
|
|
29
|
+
return [` ${varLhs} = __new_lookup(${variable.points.length}, /*copy=*/true, (double[]){ ${points} });`]
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
// Construct the name of the data array, which is based on the associated lookup var name,
|
|
33
|
+
// with any subscripts tacked on the end.
|
|
34
|
+
const dataName = variable.varName + '_data_' + generateLookupDataName(variable.subscripts, loopIndexVars)
|
|
35
|
+
if (mode === 'decl') {
|
|
36
|
+
// In decl mode, declare a static data array that will be used to create the associated `Lookup`
|
|
37
|
+
// at init time. Using static arrays is better for code size, helps us avoid creating a copy of
|
|
38
|
+
// the data in memory, and seems to perform much better when compiled to wasm when compared to the
|
|
39
|
+
// previous approach that used varargs + copying, especially on constrained (e.g. iOS) devices.
|
|
40
|
+
const points = pointsString(variable.points)
|
|
41
|
+
return [`double ${dataName}[${variable.points.length * 2}] = { ${points} };`]
|
|
42
|
+
} else if (mode === 'init-lookups') {
|
|
43
|
+
// In init mode, create the `Lookup`, passing in a pointer to the static data array declared earlier.
|
|
44
|
+
// TODO: Make use of the lookup range
|
|
45
|
+
return [` ${varLhs} = __new_lookup(${variable.points.length}, /*copy=*/false, ${dataName});`]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw new Error(`Invalid code gen mode '${mode}' for lookup ${variable.modelLHS}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Return a string containing a comma separated list of [x,y] pairs from the given array of points.
|
|
54
|
+
*
|
|
55
|
+
* @param {number[][]} points The array of [x,y] tuples.
|
|
56
|
+
* @return {string} The string containing the comma separated point values.
|
|
57
|
+
*/
|
|
58
|
+
export function pointsString(points) {
|
|
59
|
+
return points.map(p => `${cdbl(p[0])}, ${cdbl(p[1])}`).join(', ')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Return a C name for the static data array associated with a lookup variable.
|
|
64
|
+
*
|
|
65
|
+
* @param {string[]} subIds The array of subscript IDs.
|
|
66
|
+
* @param {LoopIndexVars} loopIndexVars The loop index state.
|
|
67
|
+
* @return {string} The C array name.
|
|
68
|
+
*/
|
|
69
|
+
function generateLookupDataName(subIds, loopIndexVars) {
|
|
70
|
+
return subIds
|
|
71
|
+
.map(subId => {
|
|
72
|
+
if (isDimension(subId)) {
|
|
73
|
+
const i = loopIndexVars.index(subId)
|
|
74
|
+
if (isTrivialDimension(subId)) {
|
|
75
|
+
// When the dimension is trivial, we can simply emit e.g. `[i]` instead of `[_dim[i]]`
|
|
76
|
+
return `_${i}_`
|
|
77
|
+
} else {
|
|
78
|
+
return `_${subId}_${i}_`
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
return `_${sub(subId).value}_`
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
.join('')
|
|
85
|
+
}
|
|
@@ -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'
|
|
@@ -190,31 +191,36 @@ export default class EquationReader extends ModelReader {
|
|
|
190
191
|
startCell: args[2]
|
|
191
192
|
}
|
|
192
193
|
} else if (fn === '_IF_THEN_ELSE') {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
194
|
+
if (process.env.SDE_NONPUBLIC_REDUCE_VARIABLES !== '0') {
|
|
195
|
+
// Evaluate the condition expression of the `IF THEN ELSE`. If it resolves
|
|
196
|
+
// to a compile-time constant, we only need to visit one branch, which means
|
|
197
|
+
// that no references will be recorded for the other branch, therefore allowing
|
|
198
|
+
// it to be skipped in the unused reference elimination phase and during the
|
|
199
|
+
// final code generation phase.
|
|
200
|
+
const condText = ctx.expr(0).getText()
|
|
201
|
+
const exprReader = new ExprReader()
|
|
202
|
+
const condExpr = exprReader.read(condText)
|
|
203
|
+
if (condExpr.constantValue !== undefined) {
|
|
204
|
+
// Record the conditional expression and its constant value so that
|
|
205
|
+
// it can be accessed later by EquationGen. We need to record it
|
|
206
|
+
// this way because any variables referenced by the expression may
|
|
207
|
+
// be removed during the unused reference elimination phase.
|
|
208
|
+
Model.addConstantExpr(condText, condExpr.constantValue)
|
|
209
|
+
if (condExpr.constantValue !== 0) {
|
|
210
|
+
// Only visit the "if true" branch
|
|
211
|
+
this.setArgIndex(1)
|
|
212
|
+
ctx.expr(1).accept(this)
|
|
213
|
+
} else {
|
|
214
|
+
// Only visit the "if false" branch
|
|
215
|
+
this.setArgIndex(2)
|
|
216
|
+
ctx.expr(2).accept(this)
|
|
217
|
+
}
|
|
211
218
|
} else {
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
ctx.expr(2).accept(this)
|
|
219
|
+
// Visit the condition and both branches
|
|
220
|
+
super.visitExprList(ctx)
|
|
215
221
|
}
|
|
216
222
|
} else {
|
|
217
|
-
//
|
|
223
|
+
// Optimization is disabled, visit the condition and both branches
|
|
218
224
|
super.visitExprList(ctx)
|
|
219
225
|
}
|
|
220
226
|
} else {
|
|
@@ -238,8 +244,8 @@ export default class EquationReader extends ModelReader {
|
|
|
238
244
|
// Get the var name of a variable in a call and save it as a reference.
|
|
239
245
|
let id = ctx.Id().getText()
|
|
240
246
|
let varName = canonicalName(id)
|
|
241
|
-
// Do not add a dimension name as a reference.
|
|
242
|
-
if (!isDimension(varName)) {
|
|
247
|
+
// Do not add a dimension or index name as a reference.
|
|
248
|
+
if (!isDimension(varName) && !isIndex(varName)) {
|
|
243
249
|
let fn = this.currentFunctionName()
|
|
244
250
|
this.refId = varName
|
|
245
251
|
this.expandedRefIds = []
|
package/src/model/model.js
CHANGED
|
@@ -2,7 +2,7 @@ import B from 'bufx'
|
|
|
2
2
|
import yaml from 'js-yaml'
|
|
3
3
|
import * as R from 'ramda'
|
|
4
4
|
|
|
5
|
-
import { decanonicalize, isIterable, listConcat, strlist, vlog, vsort } from '../_shared/helpers.js'
|
|
5
|
+
import { canonicalName, decanonicalize, isIterable, listConcat, strlist, vlog, vsort } from '../_shared/helpers.js'
|
|
6
6
|
import {
|
|
7
7
|
addIndex,
|
|
8
8
|
allAliases,
|
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
import { createParser } from '../parse/parser.js'
|
|
18
18
|
|
|
19
19
|
import EquationReader from './equation-reader.js'
|
|
20
|
+
import { readEquation } from './read-equations.js'
|
|
21
|
+
import { readDimensionDefs } from './read-subscripts.js'
|
|
22
|
+
import { readVariables as readVariables2 } from './read-variables.js'
|
|
23
|
+
import { reduceVariables } from './reduce-variables.js'
|
|
20
24
|
import SubscriptRangeReader from './subscript-range-reader.js'
|
|
21
25
|
import toposort from './toposort.js'
|
|
22
26
|
import VarNameReader from './var-name-reader.js'
|
|
@@ -38,14 +42,65 @@ const PRINT_INIT_GRAPH = false
|
|
|
38
42
|
const PRINT_AUX_GRAPH = false
|
|
39
43
|
const PRINT_LEVEL_GRAPH = false
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
// XXX: This is needed for tests due to variables being in module-level storage
|
|
46
|
+
function resetModelState() {
|
|
47
|
+
variables.length = 0
|
|
48
|
+
inputVars.length = 0
|
|
49
|
+
variablesByName.clear()
|
|
50
|
+
constantExprs.clear()
|
|
51
|
+
nonAtoANames = Object.create(null)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Read the given model parse tree and resolve all subscript and variable/equation definitions.
|
|
56
|
+
*
|
|
57
|
+
* Note that this function currently does not return anything and instead stores the parsed subscript
|
|
58
|
+
* definitions in the `subscript` module and the parsed/analyzed variables in this module.
|
|
59
|
+
*
|
|
60
|
+
* TODO: FIX TYPE
|
|
61
|
+
* @param {*} parsedModel The parsed model structure.
|
|
62
|
+
* @param {*} spec The parsed `spec.json` object.
|
|
63
|
+
* @param {Map<string, any>} extData The map of datasets from external `.dat` files.
|
|
64
|
+
* @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT DATA`
|
|
65
|
+
* call (e.g., `?data`) to the tabular data contained in the loaded data file.
|
|
66
|
+
* @param {string} modelDirname The path to the directory containing the model (used for resolving data
|
|
67
|
+
* files for `GET DIRECT SUBSCRIPT`).
|
|
68
|
+
* @param {*} opts An optional object used by tests to stop the read process after a specific phase.
|
|
69
|
+
*/
|
|
70
|
+
function read(parsedModel, spec, extData, directData, modelDirname, opts) {
|
|
42
71
|
// Some arrays need to be separated into variables with individual indices to
|
|
43
72
|
// prevent eval cycles. They are manually added to the spec file.
|
|
44
73
|
let specialSeparationDims = spec.specialSeparationDims
|
|
45
|
-
|
|
46
|
-
|
|
74
|
+
|
|
75
|
+
// Dimensions must be defined before reading variables that use them.
|
|
76
|
+
if (parsedModel.kind === 'vensim-legacy') {
|
|
77
|
+
readSubscriptRanges(parsedModel.parseTree, modelDirname)
|
|
78
|
+
} else {
|
|
79
|
+
readDimensionDefs(parsedModel, modelDirname)
|
|
80
|
+
}
|
|
81
|
+
if (opts?.stopAfterReadSubscripts) return
|
|
82
|
+
resolveDimensions(spec.dimensionFamilies)
|
|
83
|
+
if (opts?.stopAfterResolveSubscripts) return
|
|
84
|
+
|
|
47
85
|
// Read variables from the model parse tree.
|
|
48
|
-
|
|
86
|
+
if (parsedModel.kind === 'vensim-legacy') {
|
|
87
|
+
// TODO: directData is actually unused in VariableReader
|
|
88
|
+
readVariables(parsedModel.parseTree, specialSeparationDims, directData)
|
|
89
|
+
} else {
|
|
90
|
+
// Read the variables
|
|
91
|
+
const vars = readVariables2(parsedModel, specialSeparationDims)
|
|
92
|
+
|
|
93
|
+
// Include a placeholder variable for the exogenous `Time` variable
|
|
94
|
+
const timeVar = new Variable(null)
|
|
95
|
+
timeVar.modelLHS = 'Time'
|
|
96
|
+
timeVar.varName = '_time'
|
|
97
|
+
vars.push(timeVar)
|
|
98
|
+
|
|
99
|
+
// Add the variables to the `Model`
|
|
100
|
+
vars.forEach(addVariable)
|
|
101
|
+
}
|
|
102
|
+
if (opts?.stopAfterReadVariables) return
|
|
103
|
+
|
|
49
104
|
if (spec) {
|
|
50
105
|
// If the spec file contains `input/outputVarNames` (with full Vensim variable names)
|
|
51
106
|
// convert those to C names first. Otherwise, use `input/outputNames` which are already
|
|
@@ -61,21 +116,51 @@ function read(parseTree, spec, extData, directData, modelDirname) {
|
|
|
61
116
|
inputVars = spec.inputVars
|
|
62
117
|
}
|
|
63
118
|
}
|
|
119
|
+
|
|
64
120
|
// Analyze model equations to fill in more details about variables.
|
|
65
|
-
analyze()
|
|
121
|
+
analyze(parsedModel.kind, spec?.inputVars, opts)
|
|
122
|
+
if (opts?.stopAfterAnalyze) return
|
|
123
|
+
|
|
66
124
|
// Check that all input and output vars in the spec actually exist in the model.
|
|
67
125
|
checkSpecVars(spec, extData)
|
|
126
|
+
|
|
68
127
|
// Remove variables that are not referenced by an input or output variable.
|
|
69
128
|
removeUnusedVariables(spec)
|
|
129
|
+
|
|
70
130
|
// Resolve duplicate declarations by converting to one variable type.
|
|
71
131
|
resolveDuplicateDeclarations()
|
|
72
132
|
}
|
|
73
|
-
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Read subscript ranges from the given model.
|
|
136
|
+
*
|
|
137
|
+
* Note that this function currently does not return anything and instead stores the parsed subscript
|
|
138
|
+
* range definitions in the `subscript` module.
|
|
139
|
+
*
|
|
140
|
+
* @param {import('../parse/parser.js').VensimModelParseTree} parseTree The Vensim parse tree.
|
|
141
|
+
* @param {string} modelDirname The path to the directory containing the model (used for resolving data
|
|
142
|
+
* files for `GET DIRECT SUBSCRIPT`).
|
|
143
|
+
*/
|
|
144
|
+
function readSubscriptRanges(parseTree, modelDirname) {
|
|
74
145
|
// Read subscript ranges from the model.
|
|
75
146
|
let subscriptRangeReader = new SubscriptRangeReader(modelDirname)
|
|
76
|
-
subscriptRangeReader.visitModel(
|
|
147
|
+
subscriptRangeReader.visitModel(parseTree)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Process the previously read subscript/dimension definitions (stored in the `subscript` module) to
|
|
152
|
+
* resolve aliases, families, and indices.
|
|
153
|
+
*
|
|
154
|
+
* Note that this function currently does not return anything and only updates the set of dimension
|
|
155
|
+
* and subscript definitions in the `subscript` module.
|
|
156
|
+
*
|
|
157
|
+
* @param {Object.<string, string>} dimensionFamilies The optional mapping of dimension name to family name
|
|
158
|
+
* as provided in a `spec.json` file.
|
|
159
|
+
*/
|
|
160
|
+
function resolveDimensions(dimensionFamilies) {
|
|
77
161
|
let allDims = allDimensions()
|
|
78
|
-
|
|
162
|
+
|
|
163
|
+
// Expand dimensions that appeared in dimension definitions into subscripts/indices.
|
|
79
164
|
// Repeat until there are only indices in dimension values.
|
|
80
165
|
let dimFoundInValue
|
|
81
166
|
do {
|
|
@@ -94,7 +179,7 @@ function readSubscriptRanges(tree, dimensionFamilies, indexFamilies, modelDirnam
|
|
|
94
179
|
}
|
|
95
180
|
} while (dimFoundInValue)
|
|
96
181
|
|
|
97
|
-
// Fill in
|
|
182
|
+
// Fill in dimension aliases from their model families.
|
|
98
183
|
for (let dim of allAliases()) {
|
|
99
184
|
if (dim.value === '') {
|
|
100
185
|
let refDim = sub(dim.family)
|
|
@@ -204,6 +289,21 @@ function readSubscriptRanges(tree, dimensionFamilies, indexFamilies, modelDirnam
|
|
|
204
289
|
}
|
|
205
290
|
}
|
|
206
291
|
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Read equations from the given model and generate `Variable` instances for all variables that
|
|
295
|
+
* are encountered while parsing.
|
|
296
|
+
*
|
|
297
|
+
* Note that this function currently does not return anything and instead stores the parsed
|
|
298
|
+
* variable definitions in the `model` module.
|
|
299
|
+
*
|
|
300
|
+
* @param {import('../parse/parser.js').VensimModelParseTree} tree The Vensim parse tree.
|
|
301
|
+
* @param {Object.<string, string>} specialSeparationDims The variable names that need to be
|
|
302
|
+
* separated because of circular references. A mapping from "C" variable name to "C" dimension
|
|
303
|
+
* name to separate on.
|
|
304
|
+
* @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT DATA`
|
|
305
|
+
* call (e.g., `?data`) to the tabular data contained in the loaded data file.
|
|
306
|
+
*/
|
|
207
307
|
function readVariables(tree, specialSeparationDims, directData) {
|
|
208
308
|
// Read all variables in the model parse tree.
|
|
209
309
|
// This populates the variables table with basic information for each variable
|
|
@@ -216,26 +316,44 @@ function readVariables(tree, specialSeparationDims, directData) {
|
|
|
216
316
|
v.varName = '_time'
|
|
217
317
|
addVariable(v)
|
|
218
318
|
}
|
|
219
|
-
function analyze() {
|
|
319
|
+
function analyze(parsedModelKind, inputVars, opts) {
|
|
220
320
|
// Analyze the RHS of each equation in stages after all the variables are read.
|
|
221
321
|
// Find non-apply-to-all vars that are defined with more than one equation.
|
|
222
322
|
findNonAtoAVars()
|
|
323
|
+
|
|
223
324
|
// Set the refId for each variable. Only non-apply-to-all vars include subscripts in the refId.
|
|
224
325
|
setRefIds()
|
|
326
|
+
|
|
327
|
+
// If enabled, reduce expressions used in variable definitions.
|
|
328
|
+
if (parsedModelKind !== 'vensim-legacy') {
|
|
329
|
+
if (opts?.reduceVariables !== false && process.env.SDE_NONPUBLIC_REDUCE_VARIABLES !== '0') {
|
|
330
|
+
let reduceMode = opts?.reduceVariables || process.env.SDE_NONPUBLIC_REDUCE_VARIABLES || 'default'
|
|
331
|
+
reduceVariables(variables, inputVars || [], reduceMode)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (opts?.stopAfterReduceVariables === true) return
|
|
335
|
+
|
|
225
336
|
// Read the RHS to list the refIds of vars that are referenced and set the var type.
|
|
226
|
-
|
|
337
|
+
if (parsedModelKind === 'vensim-legacy') {
|
|
338
|
+
readEquations()
|
|
339
|
+
} else {
|
|
340
|
+
variables.forEach(readEquation)
|
|
341
|
+
}
|
|
227
342
|
}
|
|
228
343
|
|
|
229
344
|
function checkSpecVars(spec, extData) {
|
|
230
|
-
// Look up each var in the spec and issue and error
|
|
345
|
+
// Look up each var in the spec and issue and throw error if it does not exist.
|
|
231
346
|
|
|
232
347
|
function check(varNames, specType) {
|
|
233
348
|
if (isIterable(varNames)) {
|
|
234
349
|
for (let varName of varNames) {
|
|
350
|
+
// TODO: This code as written does not check variables that include subscripts, but
|
|
351
|
+
// we should check those as well (and make sure that subscripts or indices are not
|
|
352
|
+
// out of the valid range)
|
|
235
353
|
if (!R.contains('[', varName)) {
|
|
236
354
|
if (!varWithRefId(varName)) {
|
|
237
355
|
// Look for a variable in external data.
|
|
238
|
-
if (extData
|
|
356
|
+
if (extData?.has(varName)) {
|
|
239
357
|
// console.error(`found ${specType} ${varName} in extData`)
|
|
240
358
|
// Copy data from an external file to an equation that does a lookup.
|
|
241
359
|
let lookup = R.reduce(
|
|
@@ -246,7 +364,9 @@ function checkSpecVars(spec, extData) {
|
|
|
246
364
|
let modelEquation = `${decanonicalize(varName)} = WITH LOOKUP(Time, (${lookup}))`
|
|
247
365
|
addEquation(modelEquation)
|
|
248
366
|
} else {
|
|
249
|
-
|
|
367
|
+
throw new Error(
|
|
368
|
+
`The ${specType} variable ${varName} was declared in spec.json, but no matching variable was found in the model or external data sources`
|
|
369
|
+
)
|
|
250
370
|
}
|
|
251
371
|
}
|
|
252
372
|
}
|
|
@@ -585,7 +705,7 @@ function varWithRefId(refId) {
|
|
|
585
705
|
}
|
|
586
706
|
}
|
|
587
707
|
if (!refVar) {
|
|
588
|
-
vlog('ERROR: no var found for refId', refId)
|
|
708
|
+
// vlog('ERROR: no var found for refId', refId)
|
|
589
709
|
}
|
|
590
710
|
}
|
|
591
711
|
return refVar
|
|
@@ -675,7 +795,29 @@ function vensimName(cVarName) {
|
|
|
675
795
|
function cName(vensimVarName) {
|
|
676
796
|
// Convert a Vensim variable name to a C name.
|
|
677
797
|
// This function requires model analysis to be completed first when the variable has subscripts.
|
|
678
|
-
|
|
798
|
+
if (process.env.SDE_NONPUBLIC_USE_NEW_PARSE !== '1') {
|
|
799
|
+
// TODO: For now we use the legacy VarNameReader when the old parser is active; this
|
|
800
|
+
// code will be removed once the old parser is removed
|
|
801
|
+
return new VarNameReader().read(vensimVarName)
|
|
802
|
+
}
|
|
803
|
+
// Split the variable name from the subscripts
|
|
804
|
+
let matches = vensimVarName.match(/([^[]+)(?:\[([^\]]+)\])?/)
|
|
805
|
+
if (!matches) {
|
|
806
|
+
throw new Error(`Invalid variable name '${vensimVarName}' found when converting to C representation`)
|
|
807
|
+
}
|
|
808
|
+
let cVarName = canonicalName(matches[1])
|
|
809
|
+
if (matches[2]) {
|
|
810
|
+
// The variable name includes subscripts, so split them into individual IDs
|
|
811
|
+
let cSubIds = matches[2].split(',').map(x => canonicalName(x))
|
|
812
|
+
cSubIds = normalizeSubscripts(cSubIds)
|
|
813
|
+
// If a subscript is an index, convert it to an index number to match Vensim data exports
|
|
814
|
+
let cSubIdParts = cSubIds.map(cSubId => {
|
|
815
|
+
return isIndex(cSubId) ? `[${sub(cSubId).value}]` : `[${cSubId}]`
|
|
816
|
+
})
|
|
817
|
+
// Append the subscript parts to the base variable name to create the full reference
|
|
818
|
+
cVarName += cSubIdParts.join('')
|
|
819
|
+
}
|
|
820
|
+
return cVarName
|
|
679
821
|
}
|
|
680
822
|
function isInputVar(varName) {
|
|
681
823
|
// Return true if the given variable (in canonical form) is included in the list of
|
|
@@ -1146,6 +1288,7 @@ export default {
|
|
|
1146
1288
|
read,
|
|
1147
1289
|
refIdForVar,
|
|
1148
1290
|
refIdsWithName,
|
|
1291
|
+
resetModelState,
|
|
1149
1292
|
splitRefId,
|
|
1150
1293
|
variables,
|
|
1151
1294
|
varIndexInfo,
|