@sdeverywhere/compile 0.7.24 → 0.7.26
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 +2 -3
- package/src/_shared/bufx.js +66 -0
- package/src/_shared/helpers.js +1 -1
- package/src/_shared/read-dat.js +1 -1
- package/src/_shared/subscript.js +3 -19
- package/src/_shared/var-names.js +31 -0
- package/src/generate/expand-var-names.js +10 -77
- package/src/model/expand-var-instances.js +139 -0
- package/src/model/model.js +100 -49
- package/src/model/toposort.js +1 -1
- package/src/parse-and-generate.js +5 -8
package/package.json
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdeverywhere/compile",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.26",
|
|
4
4
|
"description": "The core Vensim to C compiler for the SDEverywhere tool suite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@sdeverywhere/parse": "^0.1.2",
|
|
9
|
-
"bufx": "^1.0.5",
|
|
10
9
|
"byline": "^5.0.0",
|
|
11
10
|
"csv-parse": "^5.3.3",
|
|
12
|
-
"js-yaml": "^3.13.1",
|
|
13
11
|
"ramda": "^0.27.0",
|
|
12
|
+
"strip-bom": "^5.0.0",
|
|
14
13
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"
|
|
15
14
|
},
|
|
16
15
|
"author": "Climate Interactive",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import stripBom from 'strip-bom'
|
|
3
|
+
|
|
4
|
+
// Numeric value of a string or number
|
|
5
|
+
let num = x => (typeof x === 'number' ? x : Number.parseFloat(x))
|
|
6
|
+
// Split a string into lines that may have Windows, Unix, or old Mac line endings.
|
|
7
|
+
let lines = s => s.split(/\r\n|\n|\r/)
|
|
8
|
+
// Print a string to the console
|
|
9
|
+
let print = s => {
|
|
10
|
+
console.log(s)
|
|
11
|
+
}
|
|
12
|
+
// Read a UTF-8 file into a string. Strip the BOM if present.
|
|
13
|
+
let read = pathname => stripBom(fs.readFileSync(pathname, 'utf8'))
|
|
14
|
+
// Write a string to a UTF-8 file
|
|
15
|
+
let write = (s, pathname) => {
|
|
16
|
+
fs.writeFileSync(pathname, s, { encoding: 'utf8' })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Output buffer
|
|
20
|
+
let bufs = { _: '' }
|
|
21
|
+
// Open a buffer for writing
|
|
22
|
+
let open = channel => (bufs[channel] = '')
|
|
23
|
+
// Emit a string to a buffer
|
|
24
|
+
let emit = (a, channel = null) => {
|
|
25
|
+
channel = channel || '_'
|
|
26
|
+
bufs[channel] += a
|
|
27
|
+
}
|
|
28
|
+
// Emit a string to a buffer terminated by a newline
|
|
29
|
+
let emitLine = (a, channel = null) => {
|
|
30
|
+
channel = channel || '_'
|
|
31
|
+
bufs[channel] += a + '\n'
|
|
32
|
+
}
|
|
33
|
+
// Print a buffer to the console
|
|
34
|
+
let printBuf = (channel = null) => {
|
|
35
|
+
channel = channel || '_'
|
|
36
|
+
print(bufs[channel])
|
|
37
|
+
}
|
|
38
|
+
// Write a buffer to a file
|
|
39
|
+
let writeBuf = (pathname, channel = null) => {
|
|
40
|
+
channel = channel || '_'
|
|
41
|
+
write(bufs[channel], pathname)
|
|
42
|
+
}
|
|
43
|
+
// Get buffer contents as a string
|
|
44
|
+
let getBuf = (channel = null) => {
|
|
45
|
+
channel = channel || '_'
|
|
46
|
+
return bufs[channel]
|
|
47
|
+
}
|
|
48
|
+
// Clear a buffer
|
|
49
|
+
let clearBuf = (channel = null) => {
|
|
50
|
+
channel = channel || '_'
|
|
51
|
+
bufs[channel] = ''
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default {
|
|
55
|
+
clearBuf,
|
|
56
|
+
emit,
|
|
57
|
+
emitLine,
|
|
58
|
+
getBuf,
|
|
59
|
+
lines,
|
|
60
|
+
num,
|
|
61
|
+
open,
|
|
62
|
+
printBuf,
|
|
63
|
+
read,
|
|
64
|
+
write,
|
|
65
|
+
writeBuf
|
|
66
|
+
}
|
package/src/_shared/helpers.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as fs from 'node:fs'
|
|
2
2
|
import util from 'util'
|
|
3
|
-
import B from 'bufx'
|
|
4
3
|
import { parse as parseCsv } from 'csv-parse/sync'
|
|
5
4
|
import * as R from 'ramda'
|
|
6
5
|
import XLSX from 'xlsx'
|
|
6
|
+
import B from './bufx.js'
|
|
7
7
|
|
|
8
8
|
import { canonicalId, canonicalVarId } from '@sdeverywhere/parse'
|
|
9
9
|
|
package/src/_shared/read-dat.js
CHANGED
package/src/_shared/subscript.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import util from 'util'
|
|
2
|
-
import B from 'bufx'
|
|
3
|
-
import yaml from 'js-yaml'
|
|
4
2
|
import * as R from 'ramda'
|
|
3
|
+
import B from './bufx.js'
|
|
5
4
|
import { canonicalName, vlog } from './helpers.js'
|
|
6
5
|
|
|
7
6
|
// A subscript is a dimension or an index.
|
|
@@ -107,7 +106,7 @@ export function sub(name) {
|
|
|
107
106
|
let result
|
|
108
107
|
try {
|
|
109
108
|
result = subscripts.get(name)
|
|
110
|
-
} catch (
|
|
109
|
+
} catch (_) {
|
|
111
110
|
console.error(`sub name ${name} not found`)
|
|
112
111
|
}
|
|
113
112
|
return result
|
|
@@ -218,21 +217,6 @@ export function printSubscripts() {
|
|
|
218
217
|
}
|
|
219
218
|
return B.getBuf()
|
|
220
219
|
}
|
|
221
|
-
export function yamlSubsList() {
|
|
222
|
-
let subs = {}
|
|
223
|
-
for (let [k, v] of subscripts) {
|
|
224
|
-
subs[k] = v
|
|
225
|
-
}
|
|
226
|
-
return yaml.safeDump(subs)
|
|
227
|
-
}
|
|
228
|
-
export function loadSubscriptsFromYaml(yamlSubs) {
|
|
229
|
-
// Load the subscripts map from subscripts serialized to a YAML file by yamlSubsList.
|
|
230
|
-
// This function should be called instead of adding subscripts through the constructor.
|
|
231
|
-
let subs = yaml.safeLoad(yamlSubs)
|
|
232
|
-
for (const k in subs) {
|
|
233
|
-
subscripts.set(k, subs[k])
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
220
|
export function extractMarkedDims(subscripts) {
|
|
237
221
|
// Extract all marked dimensions and update subscripts.
|
|
238
222
|
let dims = []
|
|
@@ -249,7 +233,7 @@ export function subscriptFamilies(subscripts) {
|
|
|
249
233
|
// Return a list of the subscript families for each subscript.
|
|
250
234
|
try {
|
|
251
235
|
return R.map(subscriptName => sub(subscriptName).family, subscripts)
|
|
252
|
-
} catch (
|
|
236
|
+
} catch (_) {
|
|
253
237
|
console.error(`ERROR: subscript not found in "${subscripts.join(',')}" in subscriptFamilies`)
|
|
254
238
|
}
|
|
255
239
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { canonicalName } from './helpers.js'
|
|
2
|
+
import { isIndex, sub } from './subscript.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Convert a Vensim variable name to a C name.
|
|
6
|
+
*
|
|
7
|
+
* WARNING: This function requires model analysis to be completed first when the variable
|
|
8
|
+
* has subscripts.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} vensimVarName The full Vensim variable name (can contain subscripts).
|
|
11
|
+
* @returns {string} A canonical C representation of the variable name (e.g., '_variable_name').
|
|
12
|
+
*/
|
|
13
|
+
export function cName(vensimVarName) {
|
|
14
|
+
// Split the variable name from the subscripts
|
|
15
|
+
let matches = vensimVarName.match(/([^[]+)(?:\[([^\]]+)\])?/)
|
|
16
|
+
if (!matches) {
|
|
17
|
+
throw new Error(`Invalid variable name '${vensimVarName}' found when converting to C representation`)
|
|
18
|
+
}
|
|
19
|
+
let cVarName = canonicalName(matches[1])
|
|
20
|
+
if (matches[2]) {
|
|
21
|
+
// The variable name includes subscripts, so split them into individual IDs
|
|
22
|
+
let cSubIds = matches[2].split(',').map(x => canonicalName(x))
|
|
23
|
+
// If a subscript is an index, convert it to an index number to match Vensim data exports
|
|
24
|
+
let cSubIdParts = cSubIds.map(cSubId => {
|
|
25
|
+
return isIndex(cSubId) ? `[${sub(cSubId).value}]` : `[${cSubId}]`
|
|
26
|
+
})
|
|
27
|
+
// Append the subscript parts to the base variable name to create the full reference
|
|
28
|
+
cVarName += cSubIdParts.join('')
|
|
29
|
+
}
|
|
30
|
+
return cVarName
|
|
31
|
+
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import * as R from 'ramda'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { sub, isDimension } from '../_shared/subscript.js'
|
|
3
|
+
import { cName } from '../_shared/var-names.js'
|
|
5
4
|
|
|
5
|
+
import { expandVar } from '../model/expand-var-instances.js'
|
|
6
6
|
import Model from '../model/model.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Return an array of names for all
|
|
9
|
+
* Return an array of names for all accessible variables, sorted alphabetically and expanded to
|
|
10
10
|
* include the full set of subscripted variants for variables that include subscripts.
|
|
11
11
|
*
|
|
12
|
-
* @param
|
|
13
|
-
*
|
|
12
|
+
* @param {*} variables The `Variable` objects to process.
|
|
13
|
+
* @param {boolean} canonical If true, convert names to canonical representation (variable identifiers),
|
|
14
|
+
* otherwise return the original name of each variable as it appears in the model.
|
|
14
15
|
* @returns {string[]} An array of variable names or identifiers.
|
|
15
16
|
*/
|
|
16
17
|
export function expandVarNames(canonical) {
|
|
@@ -19,10 +20,12 @@ export function expandVarNames(canonical) {
|
|
|
19
20
|
R.reduce(
|
|
20
21
|
(a, v) => {
|
|
21
22
|
if (v.varType !== 'lookup' && v.varType !== 'data' && v.includeInOutput) {
|
|
23
|
+
const varInstances = expandVar(v)
|
|
24
|
+
const varNames = varInstances.map(instance => instance.varName)
|
|
22
25
|
if (canonical) {
|
|
23
|
-
return R.concat(a, R.map(
|
|
26
|
+
return R.concat(a, R.map(cName, varNames))
|
|
24
27
|
} else {
|
|
25
|
-
return R.concat(a,
|
|
28
|
+
return R.concat(a, varNames)
|
|
26
29
|
}
|
|
27
30
|
} else {
|
|
28
31
|
return a
|
|
@@ -33,73 +36,3 @@ export function expandVarNames(canonical) {
|
|
|
33
36
|
)
|
|
34
37
|
)
|
|
35
38
|
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Return an array of names for the given variable including all subscript variants.
|
|
39
|
-
*
|
|
40
|
-
* @param {*} v A `Variable` instance.
|
|
41
|
-
* @returns {string[]} An array of expanded names for the given variable.
|
|
42
|
-
*/
|
|
43
|
-
function namesForVar(v) {
|
|
44
|
-
if (v.parsedEqn === undefined) {
|
|
45
|
-
// XXX: The special `Time` variable does not have a `parsedEqn`, so use the raw LHS
|
|
46
|
-
return [v.modelLHS]
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Expand each variable to get the names of all subscripted variants
|
|
50
|
-
const lhsVarDef = v.parsedEqn.lhs.varDef
|
|
51
|
-
const lhsSubRefs = lhsVarDef.subscriptRefs
|
|
52
|
-
if (lhsSubRefs?.length > 0) {
|
|
53
|
-
// At each position, expand any dimensions or use a subscript (index) directly
|
|
54
|
-
const subOrDimNames = lhsSubRefs.map(subRef => subRef.subName)
|
|
55
|
-
return expandDims(lhsVarDef.varName, subOrDimNames)
|
|
56
|
-
} else {
|
|
57
|
-
// No subscripts, so include a single variable name
|
|
58
|
-
return [lhsVarDef.varName]
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Return an array of all expanded subscript combinations.
|
|
64
|
-
*
|
|
65
|
-
* @param {string} baseVarName The base name of the variable.
|
|
66
|
-
* @param {string[]} subOrDimNames The array of subscript or dimension names.
|
|
67
|
-
* @returns {string[]} An array of string representations of subscripted references,
|
|
68
|
-
* e.g., `'x[A1,B1]' ,'x[A1,B2]', ...]`.
|
|
69
|
-
*/
|
|
70
|
-
function expandDims(baseVarName, subOrDimNames) {
|
|
71
|
-
// Expand the dimension for each position
|
|
72
|
-
const expanded = subOrDimNames.map(name => expandDim(name).flat(Infinity))
|
|
73
|
-
|
|
74
|
-
// Expand these into the set of all combinations of subscripts for the variable
|
|
75
|
-
const origCombos = cartesianProductOf(expanded)
|
|
76
|
-
return origCombos.map(combo => {
|
|
77
|
-
const subs = combo.join(',')
|
|
78
|
-
return `${baseVarName}[${subs}]`
|
|
79
|
-
})
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Return an array containing all subscript (index) names in the given dimension. If
|
|
84
|
-
* the given name is a subscript, it will return a single-element array with that
|
|
85
|
-
* subscript name.
|
|
86
|
-
*
|
|
87
|
-
* @param {string} subOrDimName A subscript or dimension name.
|
|
88
|
-
* @returns {string[]} A (possibly nested) array of subscript names.
|
|
89
|
-
*/
|
|
90
|
-
function expandDim(subOrDimName) {
|
|
91
|
-
// Convert the name to an ID
|
|
92
|
-
const subOrDimId = canonicalName(subOrDimName)
|
|
93
|
-
|
|
94
|
-
if (isDimension(subOrDimId)) {
|
|
95
|
-
// Get the object for the dimension
|
|
96
|
-
const dimObj = sub(subOrDimId)
|
|
97
|
-
|
|
98
|
-
// The dimension may contain a mix of individual subscripts (indices) and/or subdimensions,
|
|
99
|
-
// so recursively expand them
|
|
100
|
-
return dimObj.modelValue.map(expandDim)
|
|
101
|
-
} else {
|
|
102
|
-
// This is an individual subscript (index), so return it directly
|
|
103
|
-
return [subOrDimName]
|
|
104
|
-
}
|
|
105
|
-
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { cartesianProductOf, canonicalName } from '../_shared/helpers.js'
|
|
2
|
+
import { isDimension, sub } from '../_shared/subscript.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A single instance of a variable.
|
|
6
|
+
* @typedef {Object} VarInstance
|
|
7
|
+
* @property {string} varName The full name of the variable instance, e.g., "Variable Name[SubA, SubB]".
|
|
8
|
+
* @property {number[]} [subscriptIndices] The array of subscript indices; only defined if this variable
|
|
9
|
+
* has subscripts.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Return an array of names and subscript indices for the given variable, expanded to
|
|
14
|
+
* include all subscript variants.
|
|
15
|
+
*
|
|
16
|
+
* @param {*} v A `Variable` object.
|
|
17
|
+
* @returns {VarInstance[]} An array of `VarInstance` objects corresponding to the expanded variable.
|
|
18
|
+
*/
|
|
19
|
+
export function expandVar(v) {
|
|
20
|
+
if (v.parsedEqn === undefined) {
|
|
21
|
+
// XXX: The special `Time` variable does not have a `parsedEqn`, so use the raw LHS
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
varName: v.modelLHS
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Expand each subscript position to get the names of all subscripted variants
|
|
30
|
+
const lhsVarDef = v.parsedEqn.lhs.varDef
|
|
31
|
+
const lhsSubOrDimIds = v.subscripts
|
|
32
|
+
if (lhsSubOrDimIds === undefined || lhsSubOrDimIds.length === 0) {
|
|
33
|
+
// No subscripts, so include a single variable name
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
varName: lhsVarDef.varName
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// At each position, expand any dimensions or use a subscript (index) directly
|
|
42
|
+
return expandDims(lhsVarDef.varName, lhsSubOrDimIds)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Return an array of all expanded subscript combinations.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} baseVarName The base name of the variable.
|
|
49
|
+
* @param {string[]} subOrDimIds The array of subscript or dimension IDs.
|
|
50
|
+
* @returns {VarInstance[]} An array of `VarInstance` objects with string representations of
|
|
51
|
+
* subscripted references, e.g., `'x[A1,B1]', 'x[A1,B2]', ...`.
|
|
52
|
+
*/
|
|
53
|
+
function expandDims(baseVarName, subOrDimIds) {
|
|
54
|
+
// Expand the dimension for each position
|
|
55
|
+
const expanded = subOrDimIds.map(id => expandSubSpecs(id).flat(Infinity))
|
|
56
|
+
|
|
57
|
+
// Expand these into the set of all combinations of subscripts for the variable
|
|
58
|
+
const origCombos = cartesianProductOf(expanded)
|
|
59
|
+
return origCombos.map(combo => {
|
|
60
|
+
const subNames = combo.map(spec => spec.name).join(',')
|
|
61
|
+
const subIndices = combo.map(spec => spec.index)
|
|
62
|
+
return {
|
|
63
|
+
varName: `${baseVarName}[${subNames}]`,
|
|
64
|
+
subscriptIndices: subIndices
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Pairs a subscript name with its index.
|
|
71
|
+
* @typedef {Object} SubSpec
|
|
72
|
+
* @property {string} name The name of the subscript, e.g., "A1".
|
|
73
|
+
* @property {number} index The subscript index relative to its parent dimension.
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Return an array containing all subscript specs in the given dimension. If
|
|
78
|
+
* the given ID is a subscript, it will return a single-element array with that
|
|
79
|
+
* subscript name and index.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} subOrDimId A subscript or dimension ID (e.g., '_a1', '_dima').
|
|
82
|
+
* @returns {SubSpec[]} A (possibly nested) array of `SubSpec` objects.
|
|
83
|
+
*/
|
|
84
|
+
function expandSubSpecs(subOrDimId) {
|
|
85
|
+
if (isDimension(subOrDimId)) {
|
|
86
|
+
// Get the object for the dimension
|
|
87
|
+
const dimObj = sub(subOrDimId)
|
|
88
|
+
|
|
89
|
+
// The dimension may contain a mix of individual subscripts (indices) and/or subdimensions,
|
|
90
|
+
// so recursively expand them
|
|
91
|
+
return dimObj.value.map(expandSubSpecs)
|
|
92
|
+
} else {
|
|
93
|
+
// This is an individual subscript (index), so return its name and index value
|
|
94
|
+
// XXX: Currently, the object returned by `sub` will not include the `modelName`
|
|
95
|
+
// for subscripts (it's only defined for dimensions?), so if we don't have the
|
|
96
|
+
// `modelName`, find it using the parent dimension object. This could be avoided
|
|
97
|
+
// if subscript objects maintained their original model name.
|
|
98
|
+
const subObj = sub(subOrDimId)
|
|
99
|
+
const dimSubNames = expandSubNamesForDim(subObj.family).flat(Infinity)
|
|
100
|
+
const subName = dimSubNames[subObj.value]
|
|
101
|
+
if (subName === undefined) {
|
|
102
|
+
throw new Error(`Failed to resolve name of subscript ${subOrDimId} in dimension ${subObj.family}`)
|
|
103
|
+
}
|
|
104
|
+
return [
|
|
105
|
+
{
|
|
106
|
+
name: subName,
|
|
107
|
+
index: subObj.value
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Return an array containing all subscript (index) names in the given dimension,
|
|
115
|
+
* expanding subdimensions as needed. If the given ID is a subscript, it will return
|
|
116
|
+
* single-element array with that subscript name.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} dimId A dimension ID (e.g., '_dima').
|
|
119
|
+
* @returns {string[]} A (possibly nested) array of subscript names (e.g., 'A1').
|
|
120
|
+
*/
|
|
121
|
+
function expandSubNamesForDim(dimId) {
|
|
122
|
+
if (!isDimension(dimId)) {
|
|
123
|
+
throw new Error('expandSubNames should only be called with a dimension ID')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Get the object for the dimension
|
|
127
|
+
const dimObj = sub(dimId)
|
|
128
|
+
|
|
129
|
+
// The dimension may contain a mix of individual subscripts (indices) and/or
|
|
130
|
+
// subdimensions, so recursively expand them
|
|
131
|
+
return dimObj.modelValue.map(subOrDimName => {
|
|
132
|
+
const subOrDimId = canonicalName(subOrDimName)
|
|
133
|
+
if (isDimension(subOrDimId)) {
|
|
134
|
+
return expandSubNamesForDim(subOrDimId)
|
|
135
|
+
} else {
|
|
136
|
+
return [subOrDimName]
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
}
|
package/src/model/model.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import B from 'bufx'
|
|
2
|
-
import yaml from 'js-yaml'
|
|
3
1
|
import * as R from 'ramda'
|
|
4
2
|
|
|
5
|
-
import
|
|
3
|
+
import B from '../_shared/bufx.js'
|
|
4
|
+
import { canonicalVensimName, decanonicalize, isIterable, strlist, vlog, vsort } from '../_shared/helpers.js'
|
|
6
5
|
import {
|
|
7
6
|
addIndex,
|
|
8
7
|
allAliases,
|
|
@@ -13,7 +12,9 @@ import {
|
|
|
13
12
|
sub,
|
|
14
13
|
subscriptFamilies
|
|
15
14
|
} from '../_shared/subscript.js'
|
|
15
|
+
import { cName } from '../_shared/var-names.js'
|
|
16
16
|
|
|
17
|
+
import { expandVar } from './expand-var-instances.js'
|
|
17
18
|
import { readEquation } from './read-equations.js'
|
|
18
19
|
import { readDimensionDefs } from './read-subscripts.js'
|
|
19
20
|
import { readVariables } from './read-variables.js'
|
|
@@ -60,12 +61,12 @@ function resetModelState() {
|
|
|
60
61
|
* TODO: FIX TYPE
|
|
61
62
|
* @param {*} parsedModel The parsed model structure.
|
|
62
63
|
* @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`
|
|
64
|
+
* @param {Map<string, any>} [extData] The map of datasets from external `.dat` files.
|
|
65
|
+
* @param {Map<string, any>} [directData] The mapping of dataset name used in a `GET DIRECT DATA`
|
|
65
66
|
* 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
|
+
* @param {string} [modelDirname] The path to the directory containing the model (used for resolving data
|
|
67
68
|
* files for `GET DIRECT SUBSCRIPT`).
|
|
68
|
-
* @param {*} opts An optional object used by tests to stop the read process after a specific phase.
|
|
69
|
+
* @param {*} [opts] An optional object used by tests to stop the read process after a specific phase.
|
|
69
70
|
*/
|
|
70
71
|
function read(parsedModel, spec, extData, directData, modelDirname, opts) {
|
|
71
72
|
// Some arrays need to be separated into variables with individual indices to
|
|
@@ -425,7 +426,13 @@ function removeUnusedVariables(spec) {
|
|
|
425
426
|
}
|
|
426
427
|
|
|
427
428
|
// Filter out unneeded variables so we're left with the minimal set of variables to emit
|
|
428
|
-
|
|
429
|
+
const filteredVariables = R.filter(v => referencedVarNames.has(v.varName), variables)
|
|
430
|
+
// TODO: Note that we reuse the same `variables` array instance here instead of reassigning
|
|
431
|
+
// to it because some code (like in `code-gen/expand-var-names.js` and in some tests) uses
|
|
432
|
+
// the `variables` array (from module-level storage) directly. We need to fix those uses
|
|
433
|
+
// to use accessors to avoid these subtle issues.
|
|
434
|
+
variables.length = 0
|
|
435
|
+
variables.push(...filteredVariables)
|
|
429
436
|
|
|
430
437
|
// Rebuild the variables-by-name map
|
|
431
438
|
variablesByName.clear()
|
|
@@ -732,28 +739,6 @@ function vensimName(cVarName) {
|
|
|
732
739
|
}
|
|
733
740
|
return result
|
|
734
741
|
}
|
|
735
|
-
function cName(vensimVarName) {
|
|
736
|
-
// Convert a Vensim variable name to a C name.
|
|
737
|
-
// This function requires model analysis to be completed first when the variable has subscripts.
|
|
738
|
-
|
|
739
|
-
// Split the variable name from the subscripts
|
|
740
|
-
let matches = vensimVarName.match(/([^[]+)(?:\[([^\]]+)\])?/)
|
|
741
|
-
if (!matches) {
|
|
742
|
-
throw new Error(`Invalid variable name '${vensimVarName}' found when converting to C representation`)
|
|
743
|
-
}
|
|
744
|
-
let cVarName = canonicalName(matches[1])
|
|
745
|
-
if (matches[2]) {
|
|
746
|
-
// The variable name includes subscripts, so split them into individual IDs
|
|
747
|
-
let cSubIds = matches[2].split(',').map(x => canonicalName(x))
|
|
748
|
-
// If a subscript is an index, convert it to an index number to match Vensim data exports
|
|
749
|
-
let cSubIdParts = cSubIds.map(cSubId => {
|
|
750
|
-
return isIndex(cSubId) ? `[${sub(cSubId).value}]` : `[${cSubId}]`
|
|
751
|
-
})
|
|
752
|
-
// Append the subscript parts to the base variable name to create the full reference
|
|
753
|
-
cVarName += cSubIdParts.join('')
|
|
754
|
-
}
|
|
755
|
-
return cVarName
|
|
756
|
-
}
|
|
757
742
|
function isInputVar(varName) {
|
|
758
743
|
// Return true if the given variable (in canonical form) is included in the list of
|
|
759
744
|
// input variables in the spec file.
|
|
@@ -960,14 +945,6 @@ function printVarList() {
|
|
|
960
945
|
}
|
|
961
946
|
return B.getBuf()
|
|
962
947
|
}
|
|
963
|
-
function yamlVarList() {
|
|
964
|
-
// Print selected properties of all variable objects to a YAML string.
|
|
965
|
-
let vars = R.sortBy(
|
|
966
|
-
R.prop('refId'),
|
|
967
|
-
R.map(v => filterVar(v), variables)
|
|
968
|
-
)
|
|
969
|
-
return yaml.safeDump(vars)
|
|
970
|
-
}
|
|
971
948
|
function printVar(v) {
|
|
972
949
|
let nonAtoA = isNonAtoAName(v.varName) ? ' (non-apply-to-all)' : ''
|
|
973
950
|
B.emitLine(`${v.modelLHS}: ${v.varType}${nonAtoA}`)
|
|
@@ -1105,12 +1082,13 @@ function allListedVars() {
|
|
|
1105
1082
|
}
|
|
1106
1083
|
|
|
1107
1084
|
// The order of execution/evaluation in the generated model is:
|
|
1108
|
-
// initConstants (vars of type `const` only)
|
|
1109
|
-
// initLookups (vars of type `lookup` only)
|
|
1110
|
-
// initData (vars of type `data` only)
|
|
1111
|
-
// initLevels (vars returned by `initVars`, a mix of initial, aux, and level vars
|
|
1112
|
-
//
|
|
1113
|
-
//
|
|
1085
|
+
// initConstants (vars of type `const` only; called for t=0 only)
|
|
1086
|
+
// initLookups (vars of type `lookup` only; called for t=0 only)
|
|
1087
|
+
// initData (vars of type `data` only; called for t=0 only)
|
|
1088
|
+
// initLevels (vars returned by `initVars`, a mix of initial, aux, and level vars;
|
|
1089
|
+
// called for t=0 only)
|
|
1090
|
+
// evalAux (vars of type `aux` only; called for t>=0)
|
|
1091
|
+
// evalLevels (vars of type `level` only; called before `evalAux` for t>0)
|
|
1114
1092
|
// So to make the ordering in the listing better match the order of evaluation,
|
|
1115
1093
|
// we emit variables in the above order, but filter to avoid having duplicates.
|
|
1116
1094
|
addUnique(constVars())
|
|
@@ -1151,7 +1129,7 @@ function varIndexInfoMap() {
|
|
|
1151
1129
|
|
|
1152
1130
|
// Get the set of unique variable names, and assign a 1-based index to each.
|
|
1153
1131
|
// This matches the index number used in `storeOutput` and `setLookup` in the
|
|
1154
|
-
// generated C/JS code
|
|
1132
|
+
// generated C/JS code.
|
|
1155
1133
|
const infoMap = new Map()
|
|
1156
1134
|
let varIndex = 1
|
|
1157
1135
|
for (const v of sortedVars) {
|
|
@@ -1209,6 +1187,73 @@ function jsonList() {
|
|
|
1209
1187
|
}
|
|
1210
1188
|
}
|
|
1211
1189
|
|
|
1190
|
+
//
|
|
1191
|
+
// We include a `varInstances` object in the generated JSON listing that
|
|
1192
|
+
// includes an array of expanded variable items (one item for every "instance"
|
|
1193
|
+
// of a variable, including subscripted variables) in the same order that they
|
|
1194
|
+
// are evaluated (assigned) in the generated model.
|
|
1195
|
+
//
|
|
1196
|
+
// Each object contains the following minimal set of fields that are needed
|
|
1197
|
+
// for accessing the data for a single variable instance at runtime:
|
|
1198
|
+
// varId (e.g., '_variable_name')
|
|
1199
|
+
// varName (e.g., 'Variable Name')
|
|
1200
|
+
// varType ('const', 'data', 'lookup', 'initial', 'level', 'aux')
|
|
1201
|
+
// varIndex
|
|
1202
|
+
// subscriptIndices
|
|
1203
|
+
//
|
|
1204
|
+
// The order of execution/evaluation in the generated model is:
|
|
1205
|
+
// initConstants (vars of type `const` only, called for t=0 only)
|
|
1206
|
+
// initLookups (vars of type `lookup` only, called for t=0 only)
|
|
1207
|
+
// initData (vars of type `data` only, called for t=0 only)
|
|
1208
|
+
// initLevels (vars returned by `initVars`, a mix of initial, aux, and level vars,
|
|
1209
|
+
// called for t=0 only)
|
|
1210
|
+
// evalAux (vars of type `aux` only; called for t>=0)
|
|
1211
|
+
// evalLevels (vars of type `level` only; called before `evalAux` for t>0)
|
|
1212
|
+
//
|
|
1213
|
+
function expandedVarItems(vars) {
|
|
1214
|
+
const expandedVars = []
|
|
1215
|
+
|
|
1216
|
+
for (const v of vars) {
|
|
1217
|
+
// Filter out variables that are generated/used internally
|
|
1218
|
+
if (v.includeInOutput === false) {
|
|
1219
|
+
continue
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const varInstances = expandVar(v)
|
|
1223
|
+
for (const { varName, subscriptIndices } of varInstances) {
|
|
1224
|
+
const varId = canonicalVensimName(varName)
|
|
1225
|
+
const varItem = {
|
|
1226
|
+
varId,
|
|
1227
|
+
varName,
|
|
1228
|
+
varType: v.varType
|
|
1229
|
+
}
|
|
1230
|
+
const varInfo = infoMap.get(v.varName)
|
|
1231
|
+
if (varInfo) {
|
|
1232
|
+
varItem.varIndex = varInfo.varIndex
|
|
1233
|
+
if (subscriptIndices?.length > 0) {
|
|
1234
|
+
varItem.subscriptIndices = subscriptIndices
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
expandedVars.push(varItem)
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
return expandedVars
|
|
1242
|
+
}
|
|
1243
|
+
const expandedConstants = expandedVarItems(constVars())
|
|
1244
|
+
const expandedLookupVars = expandedVarItems(lookupVars())
|
|
1245
|
+
const expandedDataVars = expandedVarItems(dataVars())
|
|
1246
|
+
// The special exogenous `Time` variable may have already been removed by
|
|
1247
|
+
// `removeUnusedVariables` if it is not referenced explicitly in the model,
|
|
1248
|
+
// so we will only include it in the listing if it is defined here. Note
|
|
1249
|
+
// that `_time` is set to `_initial_time` as the first step in the
|
|
1250
|
+
// `initLevels` function, which is why it is included in the "init" group.
|
|
1251
|
+
const timeVar = varWithName('_time')
|
|
1252
|
+
const specialInitVars = timeVar ? [timeVar] : []
|
|
1253
|
+
const expandedInitVars = expandedVarItems([...specialInitVars, ...initVars()])
|
|
1254
|
+
const expandedLevelVars = expandedVarItems(levelVars())
|
|
1255
|
+
const expandedAuxVars = expandedVarItems(auxVars())
|
|
1256
|
+
|
|
1212
1257
|
// Derive minimal versions of the full arrays; these only contain the minimal
|
|
1213
1258
|
// subset of fields that are needed by the `ModelListing` class from the
|
|
1214
1259
|
// runtime package. The property names in the minimal objects are slightly
|
|
@@ -1245,7 +1290,15 @@ function jsonList() {
|
|
|
1245
1290
|
cachedJsonList = {
|
|
1246
1291
|
full: {
|
|
1247
1292
|
dimensions: sortedFullDims,
|
|
1248
|
-
variables: sortedFullVars
|
|
1293
|
+
variables: sortedFullVars,
|
|
1294
|
+
varInstances: {
|
|
1295
|
+
constants: expandedConstants,
|
|
1296
|
+
lookupVars: expandedLookupVars,
|
|
1297
|
+
dataVars: expandedDataVars,
|
|
1298
|
+
initVars: expandedInitVars,
|
|
1299
|
+
levelVars: expandedLevelVars,
|
|
1300
|
+
auxVars: expandedAuxVars
|
|
1301
|
+
}
|
|
1249
1302
|
},
|
|
1250
1303
|
minimal: {
|
|
1251
1304
|
dimensions: sortedMinimalDims,
|
|
@@ -1261,7 +1314,6 @@ export default {
|
|
|
1261
1314
|
addVariable,
|
|
1262
1315
|
allVars,
|
|
1263
1316
|
auxVars,
|
|
1264
|
-
cName,
|
|
1265
1317
|
constVars,
|
|
1266
1318
|
dataVars,
|
|
1267
1319
|
expansionFlags,
|
|
@@ -1287,6 +1339,5 @@ export default {
|
|
|
1287
1339
|
varsWithName,
|
|
1288
1340
|
varWithName,
|
|
1289
1341
|
varWithRefId,
|
|
1290
|
-
vensimName
|
|
1291
|
-
yamlVarList
|
|
1342
|
+
vensimName
|
|
1292
1343
|
}
|
package/src/model/toposort.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// Copyright (c) 2022 Climate Interactive / New Venture Fund
|
|
2
2
|
|
|
3
3
|
import path from 'path'
|
|
4
|
-
import B from 'bufx'
|
|
5
4
|
|
|
6
5
|
import { parseVensimModel } from '@sdeverywhere/parse'
|
|
7
6
|
|
|
7
|
+
import B from './_shared/bufx.js'
|
|
8
8
|
import { readXlsx } from './_shared/helpers.js'
|
|
9
9
|
import { readDat } from './_shared/read-dat.js'
|
|
10
|
-
import { printSubscripts
|
|
10
|
+
import { printSubscripts } from './_shared/subscript.js'
|
|
11
|
+
import { cName } from './_shared/var-names.js'
|
|
11
12
|
import Model from './model/model.js'
|
|
12
13
|
import { getDirectSubscripts } from './model/read-subscripts.js'
|
|
13
14
|
import { generateCode } from './generate/gen-code.js'
|
|
@@ -20,7 +21,7 @@ import { generateCode } from './generate/gen-code.js'
|
|
|
20
21
|
* - If `operations` has 'generateC', the generated C code will be written to `buildDir`.
|
|
21
22
|
* - If `operations` has 'generateJS', the generated JS code will be written to `buildDir`.
|
|
22
23
|
* - If `operations` has 'printVarList', variables and subscripts will be written to
|
|
23
|
-
* txt
|
|
24
|
+
* txt and json files under `buildDir`.
|
|
24
25
|
* - If `operations` has 'printRefIdTest', reference identifiers will be printed to the console.
|
|
25
26
|
* - If `operations` has 'convertNames', no output will be generated, but the results of model
|
|
26
27
|
* analysis will be available.
|
|
@@ -91,10 +92,6 @@ export async function parseAndGenerate(input, spec, operations, modelDirname, mo
|
|
|
91
92
|
writeOutput(`${modelName}_vars.txt`, Model.printVarList())
|
|
92
93
|
// Write subscripts to a text file.
|
|
93
94
|
writeOutput(`${modelName}_subs.txt`, printSubscripts())
|
|
94
|
-
// Write variables to a YAML file.
|
|
95
|
-
writeOutput(`${modelName}_vars.yaml`, Model.yamlVarList())
|
|
96
|
-
// Write subscripts to a YAML file.
|
|
97
|
-
writeOutput(`${modelName}_subs.yaml`, yamlSubsList())
|
|
98
95
|
// Write variables and subscripts to a JSON file.
|
|
99
96
|
const jsonList = Model.jsonList()
|
|
100
97
|
writeOutput(`${modelName}.json`, JSON.stringify(jsonList.full, null, 2))
|
|
@@ -120,7 +117,7 @@ export function printNames(namesPathname, operation) {
|
|
|
120
117
|
for (let line of lines) {
|
|
121
118
|
if (line.length > 0) {
|
|
122
119
|
if (operation === 'to-c') {
|
|
123
|
-
B.emitLine(
|
|
120
|
+
B.emitLine(cName(line))
|
|
124
121
|
} else {
|
|
125
122
|
B.emitLine(Model.vensimName(line))
|
|
126
123
|
}
|