@sdeverywhere/compile 0.7.32 → 0.7.34
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/README.md +2 -2
- package/package.json +6 -3
- package/src/_shared/helpers.js +11 -4
- package/src/_shared/model-spec.js +174 -0
- package/src/_shared/normalize-model-spec.js +32 -0
- package/src/_shared/read-dat.js +10 -3
- package/src/generate/gen-code-c.js +153 -9
- package/src/generate/gen-code.js +10 -12
- package/src/generate/gen-expr.js +1 -1
- package/src/index.js +20 -5
- package/src/model/reduce-variables.js +14 -6
- package/src/parse-and-generate.js +50 -19
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @sdeverywhere/compile
|
|
2
2
|
|
|
3
|
-
This package contains the core [SDEverywhere](https://github.com/climateinteractive/SDEverywhere) compiler that takes a Vensim model as input and generates C code as output.
|
|
3
|
+
This package contains the core [SDEverywhere](https://github.com/climateinteractive/SDEverywhere) compiler that takes a Vensim or Stella model as input and generates JavaScript or C code as output.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
@@ -32,7 +32,7 @@ More usage details will be included here at a later time when the interfaces sta
|
|
|
32
32
|
## Documentation
|
|
33
33
|
|
|
34
34
|
The `compile` package is currently treated as an implementation detail of the `cli` package.
|
|
35
|
-
As such, there is no
|
|
35
|
+
As such, there is no generated API documentation at this time, but we hope to expose a public API once the interfaces stabilize.
|
|
36
36
|
|
|
37
37
|
## License
|
|
38
38
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdeverywhere/compile",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.34",
|
|
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
|
+
"types": "./dist/index.d.ts",
|
|
7
8
|
"dependencies": {
|
|
8
|
-
"@sdeverywhere/parse": "^0.1.
|
|
9
|
+
"@sdeverywhere/parse": "^0.1.6",
|
|
9
10
|
"byline": "^5.0.0",
|
|
10
11
|
"csv-parse": "^5.3.3",
|
|
11
12
|
"fflate": "^0.8.3",
|
|
@@ -24,6 +25,8 @@
|
|
|
24
25
|
"url": "https://github.com/climateinteractive/SDEverywhere/issues"
|
|
25
26
|
},
|
|
26
27
|
"scripts": {
|
|
28
|
+
"clean": "rm -rf dist",
|
|
29
|
+
"build": "tsup",
|
|
27
30
|
"lint": "eslint . --max-warnings 0",
|
|
28
31
|
"prettier:check": "prettier --check .",
|
|
29
32
|
"prettier:fix": "prettier --write .",
|
|
@@ -32,6 +35,6 @@
|
|
|
32
35
|
"test": "vitest run",
|
|
33
36
|
"test:watch": "vitest --hideSkippedTests",
|
|
34
37
|
"test:ci": "vitest run",
|
|
35
|
-
"ci:build": "run-s lint prettier:check type-check test:ci"
|
|
38
|
+
"ci:build": "run-s clean lint prettier:check type-check build test:ci"
|
|
36
39
|
}
|
|
37
40
|
}
|
package/src/_shared/helpers.js
CHANGED
|
@@ -37,11 +37,18 @@ export function resetHelperState() {
|
|
|
37
37
|
resetXlsxCache()
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Format a model variable or subscript/dimension name into a valid C identifier.
|
|
42
|
+
*
|
|
43
|
+
* In the case where you have a full variable name that includes subscripts/dimensions
|
|
44
|
+
* (e.g., 'Variable name[DimA,B2]'), use `canonicalVensimName` to convert the base
|
|
45
|
+
* variable name and subscript/dimension parts to canonical form indepdendently.
|
|
46
|
+
*
|
|
47
|
+
* @param {import('./model-spec.js').VarName} name The variable name as used in the
|
|
48
|
+
* modeling tool.
|
|
49
|
+
* @return {import('./model-spec.js').VarId} The canonical variable identifier.
|
|
50
|
+
*/
|
|
40
51
|
export let canonicalName = name => {
|
|
41
|
-
// Format a model variable or subscript/dimension name into a valid C identifier.
|
|
42
|
-
// In the case where you have a full variable name that includes subscripts/dimensions
|
|
43
|
-
// (e.g., 'Variable name[DimA,B2]'), use `canonicalVensimName` to convert the
|
|
44
|
-
// base variable name and subscript/dimension parts to canonical form indepdendently.
|
|
45
52
|
return canonicalId(name)
|
|
46
53
|
}
|
|
47
54
|
export let decanonicalize = name => {
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Copyright (c) 2026 Climate Interactive / New Venture Fund
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A variable name as used in the modeling tool, for example `Some Var[DimA]` as used in
|
|
5
|
+
* a Vensim model.
|
|
6
|
+
*
|
|
7
|
+
* @typedef {string} VarName
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A variable identifier in the canonical format used internally by SDEverywhere, for
|
|
12
|
+
* example `_some_var`. These are derived from a `VarName` by lowercasing the name and
|
|
13
|
+
* replacing special characters with underscores.
|
|
14
|
+
*
|
|
15
|
+
* @typedef {string} VarId
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A dimension (subscript range) identifier in the canonical format used internally by
|
|
20
|
+
* SDEverywhere, for example `_dima`.
|
|
21
|
+
*
|
|
22
|
+
* @typedef {string} DimId
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Describes a `dat` file that provides data for exogenous data variables in the model.
|
|
27
|
+
*
|
|
28
|
+
* This can either be:
|
|
29
|
+
* - a plain file name (relative to the model directory), for example `data.dat`, or
|
|
30
|
+
* - an object with a single key/value pair, where the key is a prefix that is prepended
|
|
31
|
+
* to each variable name read from the file, and the value is the file name, for
|
|
32
|
+
* example `{ "prefix ": "data.dat" }`.
|
|
33
|
+
*
|
|
34
|
+
* @typedef {string | { [varNamePrefix: string]: string }} DatFileSpec
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Describes a model (e.g., a Vensim mdl file) and the input/output variables that should
|
|
39
|
+
* be included in the model generated by SDEverywhere.
|
|
40
|
+
*
|
|
41
|
+
* This is the type of the object that is parsed from a `spec.json` file (as passed to the
|
|
42
|
+
* `sde generate` command using the `--spec` argument) and that is accepted by the
|
|
43
|
+
* `parseAndGenerate` function.
|
|
44
|
+
*
|
|
45
|
+
* All properties are optional. If neither `inputVarNames` nor `outputVarNames` is
|
|
46
|
+
* provided, the generated model will include all variables from the model and will not
|
|
47
|
+
* allow any inputs to be set at runtime.
|
|
48
|
+
*
|
|
49
|
+
* @typedef {Object} ModelSpec
|
|
50
|
+
*
|
|
51
|
+
* @property {VarName[]} [inputVarNames] The input variables for the model, using the
|
|
52
|
+
* variable names as they appear in the modeling tool.
|
|
53
|
+
*
|
|
54
|
+
* When this is provided, only the listed variables can be set at runtime, and any
|
|
55
|
+
* variables that are not needed to compute the configured `outputVarNames` will be pruned
|
|
56
|
+
* from the generated model.
|
|
57
|
+
*
|
|
58
|
+
* @property {VarName[]} [outputVarNames] The output variables for the model, using the
|
|
59
|
+
* variable names as they appear in the modeling tool.
|
|
60
|
+
*
|
|
61
|
+
* It is customary to include `Time` as the first output variable.
|
|
62
|
+
*
|
|
63
|
+
* When this is provided, only the listed variables (plus the variables needed to compute
|
|
64
|
+
* them) will be included in the generated model.
|
|
65
|
+
*
|
|
66
|
+
* @property {DatFileSpec[]} [datFiles] The `dat` files that provide the data for exogenous
|
|
67
|
+
* data variables in the model.
|
|
68
|
+
*
|
|
69
|
+
* Each entry is resolved relative to the model directory (i.e., the directory that is
|
|
70
|
+
* passed using the `--datadir` argument, which defaults to the directory that contains the
|
|
71
|
+
* model file).
|
|
72
|
+
*
|
|
73
|
+
* @property {{ [dataTag: string]: string }} [directData] The mapping of data tag to
|
|
74
|
+
* Excel workbook file name, used to resolve the data for `GET DIRECT DATA`,
|
|
75
|
+
* `GET DIRECT CONSTANTS`, and `GET DIRECT LOOKUPS` calls in the model.
|
|
76
|
+
*
|
|
77
|
+
* Each key is the tag that appears in the model equation (for example, `?data`), and each
|
|
78
|
+
* value is the name of an `xlsx` file that is resolved relative to the model directory.
|
|
79
|
+
*
|
|
80
|
+
* @property {{ [dimName: string]: string }} [dimensionFamilies] The mapping of dimension
|
|
81
|
+
* name to family name, used when SDEverywhere cannot infer the family for a dimension
|
|
82
|
+
* from the model alone.
|
|
83
|
+
*
|
|
84
|
+
* Both the keys and the values use the dimension names as they appear in the modeling tool
|
|
85
|
+
* (they are converted to canonical form when the spec file is read).
|
|
86
|
+
*
|
|
87
|
+
* @property {{ [varId: VarId]: DimId | DimId[] }} [specialSeparationDims] The mapping of
|
|
88
|
+
* variable identifier to the dimension(s) on which that variable should be separated
|
|
89
|
+
* into one variable instance per subscript.
|
|
90
|
+
*
|
|
91
|
+
* Separating a variable is sometimes necessary to break a dependency cycle that would
|
|
92
|
+
* otherwise prevent the model from being evaluated. Each value can be either a single
|
|
93
|
+
* dimension identifier or an array of dimension identifiers.
|
|
94
|
+
*
|
|
95
|
+
* @property {(DimId | DimId[])[]} [separateAllVarsWithDims] The dimensions for which all
|
|
96
|
+
* variables should be separated into one variable instance per subscript.
|
|
97
|
+
*
|
|
98
|
+
* This is a convenience alternative to `specialSeparationDims` that avoids the need to
|
|
99
|
+
* list each affected variable. Each entry can be either a single dimension identifier or
|
|
100
|
+
* an array of dimension identifiers; a variable is separated only if every dimension in
|
|
101
|
+
* the entry appears on the left-hand side of its equation.
|
|
102
|
+
*
|
|
103
|
+
* @property {boolean} [bundleListing] Whether to bundle a model listing with the generated
|
|
104
|
+
* model.
|
|
105
|
+
*
|
|
106
|
+
* If undefined, defaults to false.
|
|
107
|
+
*
|
|
108
|
+
* When this is true, a model listing will be bundled with the generated model to allow the
|
|
109
|
+
* `runtime` package to resolve variables that are referenced by name or identifier. This
|
|
110
|
+
* listing will increase the size of the generated model, so it is recommended to set this
|
|
111
|
+
* to true only if it is needed.
|
|
112
|
+
*
|
|
113
|
+
* @property {boolean | VarName[]} [customConstants] Whether to allow constants to be
|
|
114
|
+
* overridden at runtime using `setConstant`.
|
|
115
|
+
*
|
|
116
|
+
* If undefined or false, the generated model will implement `setConstant` as a no-op,
|
|
117
|
+
* meaning that constants cannot be overridden at runtime.
|
|
118
|
+
*
|
|
119
|
+
* If true, all constants in the generated model will be available to be overridden.
|
|
120
|
+
*
|
|
121
|
+
* If an array is provided, only those variable names in the array will be available to be
|
|
122
|
+
* overridden.
|
|
123
|
+
*
|
|
124
|
+
* @property {boolean | VarName[]} [customLookups] Whether to allow lookups to be
|
|
125
|
+
* overridden at runtime using `setLookup`.
|
|
126
|
+
*
|
|
127
|
+
* If undefined or false, the generated model will implement `setLookup` as a no-op,
|
|
128
|
+
* meaning that lookups cannot be overridden at runtime.
|
|
129
|
+
*
|
|
130
|
+
* If true, all lookups in the generated model will be available to be overridden.
|
|
131
|
+
*
|
|
132
|
+
* If an array is provided, only those variable names in the array will be available to be
|
|
133
|
+
* overridden.
|
|
134
|
+
*
|
|
135
|
+
* @property {boolean | VarName[]} [customOutputs] Whether to allow for capturing the data
|
|
136
|
+
* for arbitrary variables at runtime (including variables that are not configured in the
|
|
137
|
+
* `outputVarNames` array).
|
|
138
|
+
*
|
|
139
|
+
* If undefined or false, the generated model will implement `storeOutput` as a no-op,
|
|
140
|
+
* meaning that the data for arbitrary variables cannot be captured at runtime.
|
|
141
|
+
*
|
|
142
|
+
* If true, all variables in the generated model will be available to be captured at
|
|
143
|
+
* runtime.
|
|
144
|
+
*
|
|
145
|
+
* If an array is provided, only those variable names in the array will be available to be
|
|
146
|
+
* captured at runtime.
|
|
147
|
+
*
|
|
148
|
+
* @property {DatFileSpec[]} [externalDatfiles] The `dat` files that provide the data for
|
|
149
|
+
* exogenous data variables in the model.
|
|
150
|
+
*
|
|
151
|
+
* DEPRECATED: Use `datFiles` instead. This property is still honored (but is ignored if
|
|
152
|
+
* `datFiles` is also provided) and will be removed in a future release.
|
|
153
|
+
*
|
|
154
|
+
* @property {string} [name] An optional descriptive name for the model.
|
|
155
|
+
*
|
|
156
|
+
* This is not currently used by SDEverywhere, but is allowed (and is included in many
|
|
157
|
+
* existing `spec.json` files) as a way to document what the model is.
|
|
158
|
+
*
|
|
159
|
+
* @property {VarId[]} [inputVars] The input variable identifiers for the model, in
|
|
160
|
+
* canonical form.
|
|
161
|
+
*
|
|
162
|
+
* This is derived from `inputVarNames` while the model is being read, and is not intended
|
|
163
|
+
* to be set in a `spec.json` file.
|
|
164
|
+
*
|
|
165
|
+
* @property {VarId[]} [outputVars] The output variable identifiers for the model, in
|
|
166
|
+
* canonical form.
|
|
167
|
+
*
|
|
168
|
+
* This is derived from `outputVarNames` while the model is being read, and is not intended
|
|
169
|
+
* to be set in a `spec.json` file.
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
// Note that this module only declares types (as JSDoc typedefs), so this empty export
|
|
173
|
+
// is needed to make it a module (otherwise the typedefs would be declared globally).
|
|
174
|
+
export {}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Copyright (c) 2026 Climate Interactive / New Venture Fund
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize the given model spec so that the rest of the compile package only needs
|
|
5
|
+
* to work with the preferred property names.
|
|
6
|
+
*
|
|
7
|
+
* Some `spec.json` properties have been renamed over time. For each renamed property,
|
|
8
|
+
* this copies the value from the deprecated property to the preferred one, unless the
|
|
9
|
+
* preferred property is already defined (in which case the preferred one wins). The
|
|
10
|
+
* deprecated properties are left in place so that the spec object is unchanged from
|
|
11
|
+
* the caller's point of view.
|
|
12
|
+
*
|
|
13
|
+
* Note that the given spec object is modified in place (and returned for convenience),
|
|
14
|
+
* which is consistent with how the spec object is treated elsewhere in this package.
|
|
15
|
+
* This function is idempotent, so it is safe to call it more than once on the same spec.
|
|
16
|
+
*
|
|
17
|
+
* @template {import('./model-spec.js').ModelSpec | undefined} T
|
|
18
|
+
* @param {T} spec The model spec to normalize, or undefined.
|
|
19
|
+
* @return {T} The same spec object that was provided.
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeModelSpec(spec) {
|
|
22
|
+
if (spec === undefined) {
|
|
23
|
+
return spec
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The `externalDatfiles` property was renamed to `datFiles`
|
|
27
|
+
if (spec.datFiles === undefined && spec.externalDatfiles !== undefined) {
|
|
28
|
+
spec.datFiles = spec.externalDatfiles
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return spec
|
|
32
|
+
}
|
package/src/_shared/read-dat.js
CHANGED
|
@@ -5,14 +5,21 @@ import * as R from 'ramda'
|
|
|
5
5
|
import B from './bufx.js'
|
|
6
6
|
import { canonicalVensimName } from './helpers.js'
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* The datasets read from external `dat` files, keyed by variable identifier. Each
|
|
10
|
+
* dataset is a map of time to value.
|
|
11
|
+
*
|
|
12
|
+
* @typedef {Map<import('./model-spec.js').VarId, Map<number, number>>} ExtData
|
|
13
|
+
*/
|
|
14
|
+
|
|
8
15
|
/**
|
|
9
16
|
* Read a Vensim `dat` file with static data and return a Map.
|
|
10
17
|
* Each dataset consists of a key (the variable name in the canonical
|
|
11
18
|
* format used by SDE) and a map of time/value pairs.
|
|
12
19
|
*
|
|
13
|
-
* @param pathname The absolute path to the dat file.
|
|
14
|
-
* @param prefix An optional prefix string prepended to var names.
|
|
15
|
-
* @return A Map containing the datasets.
|
|
20
|
+
* @param {string} pathname The absolute path to the dat file.
|
|
21
|
+
* @param {string} [prefix] An optional prefix string prepended to var names.
|
|
22
|
+
* @return {Promise<ExtData>} A promise that resolves with a Map containing the datasets.
|
|
16
23
|
*/
|
|
17
24
|
export async function readDat(pathname, prefix = '') {
|
|
18
25
|
let log = new Map()
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import * as R from 'ramda'
|
|
2
2
|
|
|
3
|
-
import { asort, canonicalVensimName, lines, strlist, mapIndexed } from '../_shared/helpers.js'
|
|
3
|
+
import { asort, canonicalVensimName, cdbl, lines, strlist, mapIndexed } from '../_shared/helpers.js'
|
|
4
4
|
import { sub, allDimensions, allMappings, subscriptFamilies } from '../_shared/subscript.js'
|
|
5
5
|
import Model from '../model/model.js'
|
|
6
6
|
|
|
7
7
|
import { generateEquation } from './gen-equation.js'
|
|
8
8
|
import { expandVarNames } from './expand-var-names.js'
|
|
9
9
|
|
|
10
|
+
// The control variables are declared in `sde.h` and read by the support code in `model.c`,
|
|
11
|
+
// so they always have to be emitted as mutable globals.
|
|
12
|
+
const controlVarNames = new Set(['_final_time', '_initial_time', '_saveper', '_time_step'])
|
|
13
|
+
|
|
10
14
|
export function generateC(parsedModel, opts) {
|
|
11
15
|
return codeGenerator(parsedModel, opts).generate()
|
|
12
16
|
}
|
|
@@ -17,6 +21,9 @@ let codeGenerator = (parsedModel, opts) => {
|
|
|
17
21
|
let mode = ''
|
|
18
22
|
// Set to true to output all variables when there is no model run spec.
|
|
19
23
|
let outputAllVars = spec.outputVarNames === undefined || spec.outputVarNames.length === 0
|
|
24
|
+
// The constant variables that are emitted as C literals, keyed by variable name; see
|
|
25
|
+
// `resolveLiteralConstVars` below.
|
|
26
|
+
let literalConstVars = new Map()
|
|
20
27
|
// Function to generate a section of the code
|
|
21
28
|
let generateSection = R.map(v => {
|
|
22
29
|
return generateEquation(v, mode, extData, directData, modelDirname, 'c')
|
|
@@ -37,6 +44,9 @@ let codeGenerator = (parsedModel, opts) => {
|
|
|
37
44
|
// Do not generate output, but leave the results of model analysis.
|
|
38
45
|
}
|
|
39
46
|
if (operations.includes('generateC')) {
|
|
47
|
+
// Decide which constants can be emitted as C literals; this must happen before any
|
|
48
|
+
// code is generated, since it affects both the declaration and the init sections.
|
|
49
|
+
resolveLiteralConstVars()
|
|
40
50
|
// Generate code for each variable in the proper order.
|
|
41
51
|
let code = emitDeclCode()
|
|
42
52
|
code += emitInitLookupsCode()
|
|
@@ -48,6 +58,130 @@ let codeGenerator = (parsedModel, opts) => {
|
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
|
|
61
|
+
//
|
|
62
|
+
// Constant folding
|
|
63
|
+
//
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Determine which constant variables can be emitted as C literals (`static const double
|
|
67
|
+
* _x = 2.0;`) instead of as mutable globals that are assigned in `initConstants`.
|
|
68
|
+
*
|
|
69
|
+
* The point is to let the C compiler see the values. When a constant is a mutable global,
|
|
70
|
+
* every use of it has to be compiled as a load of an unknown quantity; when it is a literal,
|
|
71
|
+
* the compiler can fold it into the expressions that use it. The largest effect by far is
|
|
72
|
+
* that `pow(x, e)` calls where `e` is a named constant with a value like 2, 0.5, or 1 get
|
|
73
|
+
* strength-reduced into multiplies and `sqrt`.
|
|
74
|
+
*
|
|
75
|
+
* That strength reduction is also why this is opt-in: `x*x` and `sqrt(x)` are correctly
|
|
76
|
+
* rounded while `pow` is not, so results can change in the last few digits. For En-ROADS
|
|
77
|
+
* the largest observed relative difference is ~1e-13 (and the new value is usually the more
|
|
78
|
+
* accurate one), but it is enough to change bit-exact regression baselines. Set
|
|
79
|
+
* `SDE_NONPUBLIC_EMIT_CONST_LITERALS=1` to enable.
|
|
80
|
+
*
|
|
81
|
+
* Only unsubscripted constants with a plain numeric value qualify. Input variables are
|
|
82
|
+
* excluded (they are assigned by `setInputs` on each run), as are constants that can be
|
|
83
|
+
* overridden with `setConstant`, the control variables (which are declared in `sde.h`), and
|
|
84
|
+
* constants that come from a `GET DIRECT CONSTANTS` call.
|
|
85
|
+
*/
|
|
86
|
+
function resolveLiteralConstVars() {
|
|
87
|
+
literalConstVars = new Map()
|
|
88
|
+
|
|
89
|
+
if (process.env.SDE_NONPUBLIC_EMIT_CONST_LITERALS !== '1') {
|
|
90
|
+
// Skip this optimization if not explicitly enabled
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (spec.customConstants === true) {
|
|
95
|
+
// Any constant can be overridden at runtime, so none of them can be emitted as literals
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let customConstantVarNames = []
|
|
100
|
+
if (Array.isArray(spec.customConstants)) {
|
|
101
|
+
// The developer might specify a variable name that includes subscripts, but we will
|
|
102
|
+
// ignore the subscript part and only match on the base name
|
|
103
|
+
customConstantVarNames = spec.customConstants.map(varName => canonicalVensimName(varName.split('[')[0]))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const v of Model.constVars()) {
|
|
107
|
+
if (v.subscripts.length > 0) {
|
|
108
|
+
// Skip subscripted constants. Some Vensim functions (`ALLOCATE AVAILABLE`,
|
|
109
|
+
// `VECTOR SORT ORDER`, `INVERT MATRIX`, etc) take array arguments as `double*`, and a
|
|
110
|
+
// `static const double[]` cannot be passed to those. Emitting a constant array as a
|
|
111
|
+
// literal would require tracking which arrays are passed by address, so for now we only
|
|
112
|
+
// handle the scalar case.
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
if (controlVarNames.has(v.varName)) {
|
|
116
|
+
// Skip the control variables (`INITIAL TIME`, `FINAL TIME`, `TIME STEP`, and `SAVEPER`).
|
|
117
|
+
// These are declared as `extern` in `sde.h` and read by `model.c`, so they must remain
|
|
118
|
+
// mutable globals with external linkage.
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
if (Model.isInputVar(v.varName)) {
|
|
122
|
+
// Skip input variables. These are assigned by `setInputs` on every run, so their value
|
|
123
|
+
// is not fixed at compile time.
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
if (customConstantVarNames.includes(v.varName)) {
|
|
127
|
+
// Skip constants that the developer declared as overridable with `setConstant`; like
|
|
128
|
+
// inputs, these can be assigned at runtime.
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
if (v.directConstArgs) {
|
|
132
|
+
// Skip constants that get their value from a `GET DIRECT CONSTANTS` call. Those values
|
|
133
|
+
// are read from an external data file at init time, so they are not known here.
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
const rhs = v.parsedEqn?.rhs
|
|
137
|
+
if (rhs?.kind !== 'expr') {
|
|
138
|
+
// Skip constants that don't have a simple expression on the right-hand side
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
const value = constNumberValue(rhs.expr)
|
|
142
|
+
if (value === undefined) {
|
|
143
|
+
// Skip constants whose right-hand side doesn't resolve to a number. An arithmetic
|
|
144
|
+
// expression (even one over numbers only, like `2*3`) is emitted as generated code in
|
|
145
|
+
// `initConstants` rather than as a value we can write out here.
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
literalConstVars.set(v.varName, cdbl(value))
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Return the numeric value of the given expression, or undefined if it is not a number.
|
|
154
|
+
*
|
|
155
|
+
* This looks through parentheses and unary plus/minus operators, so an equation like
|
|
156
|
+
* `x = -(1.5)` resolves to -1.5. These are the only expressions that are reduced here;
|
|
157
|
+
* folding arithmetic (`2*3` and the like) would mean computing the value in JavaScript
|
|
158
|
+
* instead of letting the C compiler do it, which we avoid.
|
|
159
|
+
*
|
|
160
|
+
* @param {*} expr The expression to evaluate.
|
|
161
|
+
* @returns {number | undefined} The numeric value of the expression, or undefined if the
|
|
162
|
+
* expression is not a (possibly negated) number.
|
|
163
|
+
*/
|
|
164
|
+
function constNumberValue(expr) {
|
|
165
|
+
switch (expr?.kind) {
|
|
166
|
+
case 'number':
|
|
167
|
+
return expr.value
|
|
168
|
+
case 'parens':
|
|
169
|
+
return constNumberValue(expr.expr)
|
|
170
|
+
case 'unary-op': {
|
|
171
|
+
if (expr.op !== '-' && expr.op !== '+') {
|
|
172
|
+
return undefined
|
|
173
|
+
}
|
|
174
|
+
const childValue = constNumberValue(expr.expr)
|
|
175
|
+
if (childValue === undefined) {
|
|
176
|
+
return undefined
|
|
177
|
+
}
|
|
178
|
+
return expr.op === '-' ? -childValue : childValue
|
|
179
|
+
}
|
|
180
|
+
default:
|
|
181
|
+
return undefined
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
51
185
|
// Each code section follows in an outline of the generated model code.
|
|
52
186
|
|
|
53
187
|
//
|
|
@@ -56,7 +190,7 @@ let codeGenerator = (parsedModel, opts) => {
|
|
|
56
190
|
function emitDeclCode() {
|
|
57
191
|
mode = 'decl'
|
|
58
192
|
return `#include "sde.h"
|
|
59
|
-
|
|
193
|
+
${literalConstSection()}
|
|
60
194
|
// Model variables
|
|
61
195
|
${declSection()}
|
|
62
196
|
|
|
@@ -112,12 +246,9 @@ bool data_initialized = false;
|
|
|
112
246
|
|
|
113
247
|
function emitInitConstantsCode() {
|
|
114
248
|
mode = 'init-constants'
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
' // Initialize constants.',
|
|
119
|
-
' initLookups();\n initData();'
|
|
120
|
-
)
|
|
249
|
+
// Skip the constants that are emitted as literals in the declaration section
|
|
250
|
+
const constVars = R.reject(v => literalConstVars.has(v.varName), Model.constVars())
|
|
251
|
+
return chunkedFunctions('initConstants', constVars, ' // Initialize constants.', ' initLookups();\n initData();')
|
|
121
252
|
}
|
|
122
253
|
|
|
123
254
|
function emitInitLevelsCode() {
|
|
@@ -336,7 +467,20 @@ ${section(chunk)}
|
|
|
336
467
|
asort,
|
|
337
468
|
lines
|
|
338
469
|
)
|
|
339
|
-
|
|
470
|
+
// Skip the constants that are emitted as literals in `literalConstSection`
|
|
471
|
+
const vars = R.reject(v => literalConstVars.has(v.varName), Model.allVars())
|
|
472
|
+
return decls(vars) + fixedDelayDecls + depreciationDecls
|
|
473
|
+
}
|
|
474
|
+
function literalConstSection() {
|
|
475
|
+
// Emit a definition for each constant that is emitted as a C literal (see
|
|
476
|
+
// `resolveLiteralConstVars`). Note that this includes the section heading and a
|
|
477
|
+
// leading blank line so that the whole section disappears when there are no such
|
|
478
|
+
// constants.
|
|
479
|
+
if (literalConstVars.size === 0) {
|
|
480
|
+
return ''
|
|
481
|
+
}
|
|
482
|
+
const defs = [...literalConstVars].map(([varName, value]) => `static const double ${varName} = ${value};`)
|
|
483
|
+
return `\n// Constants\n${lines(asort(defs))}\n`
|
|
340
484
|
}
|
|
341
485
|
function internalVarsSection() {
|
|
342
486
|
// Declare internal variables to run the model.
|
package/src/generate/gen-code.js
CHANGED
|
@@ -4,23 +4,21 @@ import { generateJS } from './gen-code-js.js'
|
|
|
4
4
|
/**
|
|
5
5
|
* Generate code from the given parsed model.
|
|
6
6
|
*
|
|
7
|
-
* @param {
|
|
7
|
+
* @param {import('../parse-and-generate.js').ParsedModel} parsedModel The parsed model structure.
|
|
8
8
|
* @param {Object} opts The options that control code generation.
|
|
9
|
-
* @param {
|
|
10
|
-
* @param {
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* analysis will be available.
|
|
16
|
-
* @param {Map<string, any>} opts.extData The map of datasets from external `.dat` files.
|
|
17
|
-
* @param {Map<string, any>} opts.directData The mapping of dataset name used in a
|
|
9
|
+
* @param {import('../_shared/model-spec.js').ModelSpec} opts.spec The parsed `spec.json` object.
|
|
10
|
+
* @param {import('../parse-and-generate.js').GenerateOperation[]} opts.operations The array
|
|
11
|
+
* of operations to perform.
|
|
12
|
+
* @param {import('../_shared/read-dat.js').ExtData} [opts.extData] The map of datasets from
|
|
13
|
+
* external `.dat` files.
|
|
14
|
+
* @param {Map<string, any>} [opts.directData] The mapping of dataset name used in a
|
|
18
15
|
* `GET DIRECT DATA` call (e.g., `?data`) to the tabular data contained in the loaded
|
|
19
16
|
* data file.
|
|
20
|
-
* @param {string} opts.modelDirname The absolute path to the directory containing data
|
|
17
|
+
* @param {string} [opts.modelDirname] The absolute path to the directory containing data
|
|
21
18
|
* (dat, xlsx, csv) files that are referenced by the model. This path is used for
|
|
22
19
|
* resolving data files for `GET DIRECT SUBSCRIPT` calls.
|
|
23
|
-
* @
|
|
20
|
+
* @param {string} [opts.varname] The variable name passed to the `sde causes` command.
|
|
21
|
+
* @returns {string} A string containing the generated code.
|
|
24
22
|
*/
|
|
25
23
|
export function generateCode(parsedModel, opts) {
|
|
26
24
|
// Note that the two `generate` functions perform the same steps (other than the
|
package/src/generate/gen-expr.js
CHANGED
|
@@ -483,7 +483,7 @@ function generateLevelInit(callExpr, ctx) {
|
|
|
483
483
|
const fnId = callExpr.fnId
|
|
484
484
|
|
|
485
485
|
// Get the index of the argument holding the initial value expression
|
|
486
|
-
let initialArgIndex
|
|
486
|
+
let initialArgIndex
|
|
487
487
|
switch (fnId) {
|
|
488
488
|
case '_ACTIVE_INITIAL':
|
|
489
489
|
case '_INTEG':
|
package/src/index.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
// Copyright (c) 2022 Climate Interactive / New Venture Fund
|
|
2
2
|
|
|
3
|
+
//
|
|
4
|
+
// Note that the types below are declared (as JSDoc typedefs) alongside the code that
|
|
5
|
+
// defines and uses them. We use `export *` here rather than re-declaring each typedef
|
|
6
|
+
// so that the generated declarations refer to a single definition of each type.
|
|
7
|
+
//
|
|
8
|
+
export * from './_shared/model-spec.js'
|
|
9
|
+
|
|
3
10
|
// XXX: For now we re-export the preprocess function from the parse package
|
|
4
11
|
// mainly for use by the cli package (so that we don't need to have the cli
|
|
5
12
|
// package directly depend on the parse package)
|
|
6
13
|
export { preprocessVensimModel } from '@sdeverywhere/parse'
|
|
7
14
|
|
|
8
15
|
export { canonicalName } from './_shared/helpers.js'
|
|
9
|
-
export
|
|
16
|
+
export * from './_shared/read-dat.js'
|
|
10
17
|
export { generateCode } from './generate/gen-code.js'
|
|
11
|
-
export
|
|
18
|
+
export * from './parse-and-generate.js'
|
|
12
19
|
|
|
13
20
|
import { resetHelperState } from './_shared/helpers.js'
|
|
14
21
|
import { resetSubscriptsAndDimensions } from './_shared/subscript.js'
|
|
@@ -29,8 +36,12 @@ export function resetState() {
|
|
|
29
36
|
/**
|
|
30
37
|
* @hidden This is not yet part of the public API; it is exposed only for use
|
|
31
38
|
* in the experimental playground app.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} mdlContent The string containing the Vensim model text.
|
|
41
|
+
* @param {string} [modelDir] The absolute path to the directory containing data files.
|
|
42
|
+
* @return {import('./parse-and-generate.js').ParsedModel} A parsed tree representation of the model.
|
|
32
43
|
*/
|
|
33
|
-
export function parseInlineVensimModel(mdlContent
|
|
44
|
+
export function parseInlineVensimModel(mdlContent, modelDir) {
|
|
34
45
|
// For tests that parse inline model text, in the case of the legacy parser, don't run
|
|
35
46
|
// the preprocess step, and in the case of the new parser (which implicitly runs the
|
|
36
47
|
// preprocess step), don't sort the definitions. This makes it easier to do apples
|
|
@@ -41,8 +52,12 @@ export function parseInlineVensimModel(mdlContent /*: string*/, modelDir /*?: st
|
|
|
41
52
|
/**
|
|
42
53
|
* @hidden This is not yet part of the public API; it is exposed only for use
|
|
43
54
|
* in the experimental playground app.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} mdlContent The string containing the XMILE model text.
|
|
57
|
+
* @param {string} [modelDir] The absolute path to the directory containing data files.
|
|
58
|
+
* @return {import('./parse-and-generate.js').ParsedModel} A parsed tree representation of the model.
|
|
44
59
|
*/
|
|
45
|
-
export function parseInlineXmileModel(mdlContent
|
|
60
|
+
export function parseInlineXmileModel(mdlContent, modelDir) {
|
|
46
61
|
return parseModel(mdlContent, 'xmile', modelDir)
|
|
47
62
|
}
|
|
48
63
|
|
|
@@ -50,6 +65,6 @@ export function parseInlineXmileModel(mdlContent /*: string*/, modelDir /*?: str
|
|
|
50
65
|
* @hidden This is not yet part of the public API; it is exposed only for use
|
|
51
66
|
* in the experimental playground app.
|
|
52
67
|
*/
|
|
53
|
-
export function getModelListing()
|
|
68
|
+
export function getModelListing() {
|
|
54
69
|
return Model.jsonList()
|
|
55
70
|
}
|
|
@@ -49,14 +49,22 @@ export function reduceVariables(variables, inputVarIds, mode) {
|
|
|
49
49
|
return
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
52
|
+
// Stop if this variable is already being reduced further up the call stack, which means
|
|
53
|
+
// it takes part in a dependency cycle. This is normal and expected: every stock and flow
|
|
54
|
+
// feedback loop is a cycle (a level's rate refers to a variable that reads the level), and
|
|
55
|
+
// a variable that holds its own value from the previous time step (`SAMPLE IF TRUE`)
|
|
56
|
+
// refers to itself. Leaving the variable unreduced here is safe. The caller
|
|
57
|
+
// (`resolveVarRef`) only substitutes a referenced variable when its reduced RHS is a
|
|
58
|
+
// single number, and a variable that takes part in a cycle refers to at least one other
|
|
59
|
+
// variable, so its RHS can never be a single number. The cycle simply stops the reduction
|
|
60
|
+
// from propagating any further along that path. Note that a cycle that is a genuine error
|
|
61
|
+
// in the model (a simultaneous equation between two aux variables, say) is still reported:
|
|
62
|
+
// `sortVarsOfType` detects it during the dependency sort and reports the whole chain.
|
|
56
63
|
if (activelyReducingRefIds.has(v.refId)) {
|
|
57
|
-
|
|
64
|
+
return
|
|
58
65
|
}
|
|
59
|
-
|
|
66
|
+
|
|
67
|
+
// Add this variable to the set of active ones
|
|
60
68
|
activelyReducingRefIds.add(v.refId)
|
|
61
69
|
|
|
62
70
|
// We currently have two options for reducing variables. The less aggressive
|
|
@@ -6,6 +6,7 @@ import { parseVensimModel, parseXmileModel } from '@sdeverywhere/parse'
|
|
|
6
6
|
|
|
7
7
|
import B from './_shared/bufx.js'
|
|
8
8
|
import { readXlsx } from './_shared/helpers.js'
|
|
9
|
+
import { normalizeModelSpec } from './_shared/normalize-model-spec.js'
|
|
9
10
|
import { readDat } from './_shared/read-dat.js'
|
|
10
11
|
import { printSubscripts } from './_shared/subscript.js'
|
|
11
12
|
import { cName } from './_shared/var-names.js'
|
|
@@ -13,6 +14,34 @@ import Model from './model/model.js'
|
|
|
13
14
|
import { getDirectSubscripts } from './model/read-subscripts.js'
|
|
14
15
|
import { generateCode } from './generate/gen-code.js'
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* The kind of model that is being parsed.
|
|
19
|
+
*
|
|
20
|
+
* @typedef {'vensim' | 'xmile'} ModelKind
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A parsed tree representation of a model, along with the kind of model that was parsed.
|
|
25
|
+
*
|
|
26
|
+
* @typedef {Object} ParsedModel
|
|
27
|
+
* @property {ModelKind} kind The kind of model that was parsed.
|
|
28
|
+
* @property {import('@sdeverywhere/parse').Model} root The root of the parsed model AST.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* An operation that can be performed by `parseAndGenerate`.
|
|
33
|
+
*
|
|
34
|
+
* - `generateC` writes the generated C code to the build directory.
|
|
35
|
+
* - `generateJS` writes the generated JS code to the build directory.
|
|
36
|
+
* - `printVarList` writes variables and subscripts to txt and json files under the build
|
|
37
|
+
* directory.
|
|
38
|
+
* - `printRefIdTest` prints reference identifiers to the console.
|
|
39
|
+
* - `printRefGraph` prints the variable dependency graph to the console.
|
|
40
|
+
* - `convertNames` generates no output, but makes the results of model analysis available.
|
|
41
|
+
*
|
|
42
|
+
* @typedef {'generateC' | 'generateJS' | 'printVarList' | 'printRefIdTest' | 'printRefGraph' | 'convertNames'} GenerateOperation
|
|
43
|
+
*/
|
|
44
|
+
|
|
16
45
|
/**
|
|
17
46
|
* Parse a Vensim or XMILE model and generate C code.
|
|
18
47
|
*
|
|
@@ -27,27 +56,30 @@ import { generateCode } from './generate/gen-code.js'
|
|
|
27
56
|
* analysis will be available.
|
|
28
57
|
*
|
|
29
58
|
* @param {string} input The preprocessed Vensim or XMILE model text.
|
|
30
|
-
* @param {
|
|
31
|
-
* @param {
|
|
32
|
-
* @param {
|
|
33
|
-
*
|
|
34
|
-
* read but no operation will be performed.
|
|
59
|
+
* @param {ModelKind} modelKind The kind of model to parse.
|
|
60
|
+
* @param {import('./_shared/model-spec.js').ModelSpec} spec The model spec (from the JSON file).
|
|
61
|
+
* @param {GenerateOperation[]} operations The set of operations to perform. If the array is
|
|
62
|
+
* empty, the model will be read but no operation will be performed.
|
|
35
63
|
* @param {string} modelDirname The absolute path to the directory containing data (dat, xlsx, csv)
|
|
36
64
|
* files that are referenced by the model. These files will be resolved relative to this directory.
|
|
37
65
|
* @param {string} modelName The model name (without the mdl extension).
|
|
38
66
|
* @param {string} buildDir The output directory where the C or list files will be written.
|
|
39
67
|
* @param {string} [varname] The variable name passed to the 'sde causes' command.
|
|
40
|
-
* @return A
|
|
68
|
+
* @return {Promise<string>} A promise that resolves with the generated C or JS code.
|
|
41
69
|
*/
|
|
42
70
|
export async function parseAndGenerate(input, modelKind, spec, operations, modelDirname, modelName, buildDir, varname) {
|
|
71
|
+
// Resolve any deprecated property names in the spec so that we only need to
|
|
72
|
+
// consult the preferred names below
|
|
73
|
+
normalizeModelSpec(spec)
|
|
74
|
+
|
|
43
75
|
// Read time series from external DAT files into a single object.
|
|
44
|
-
//
|
|
45
|
-
//
|
|
76
|
+
// `datFiles` is an array of either filenames or objects giving a variable
|
|
77
|
+
// name prefix as the key and a filename as the value.
|
|
46
78
|
let extData = new Map()
|
|
47
|
-
if (spec.
|
|
48
|
-
for (let datfile of spec.
|
|
79
|
+
if (spec.datFiles) {
|
|
80
|
+
for (let datfile of spec.datFiles) {
|
|
49
81
|
let prefix = ''
|
|
50
|
-
let filename
|
|
82
|
+
let filename
|
|
51
83
|
if (typeof datfile === 'object') {
|
|
52
84
|
prefix = Object.keys(datfile)[0]
|
|
53
85
|
filename = datfile[prefix]
|
|
@@ -110,8 +142,9 @@ export async function parseAndGenerate(input, modelKind, spec, operations, model
|
|
|
110
142
|
*
|
|
111
143
|
* This is used only to implement the `sde names` command.
|
|
112
144
|
*
|
|
113
|
-
* @param namesPathname The path to the file containing variables names.
|
|
114
|
-
* @param
|
|
145
|
+
* @param {string} namesPathname The path to the file containing variables names.
|
|
146
|
+
* @param {'to-c' | 'to-vensim'} operation The conversion to perform.
|
|
147
|
+
* @return {void}
|
|
115
148
|
*/
|
|
116
149
|
export function printNames(namesPathname, operation) {
|
|
117
150
|
let lines = B.lines(B.read(namesPathname))
|
|
@@ -130,15 +163,13 @@ export function printNames(namesPathname, operation) {
|
|
|
130
163
|
/**
|
|
131
164
|
* Read and parse the given model text and return the parsed model structure.
|
|
132
165
|
*
|
|
133
|
-
* TODO: Fix return type
|
|
134
|
-
*
|
|
135
166
|
* @param {string} input The string containing the model text.
|
|
136
|
-
* @param {
|
|
137
|
-
* @param {string} modelDir The absolute path to the directory containing data (dat, xlsx, csv)
|
|
167
|
+
* @param {ModelKind} modelKind The kind of model to parse.
|
|
168
|
+
* @param {string} [modelDir] The absolute path to the directory containing data (dat, xlsx, csv)
|
|
138
169
|
* files that are referenced by the model. These files will be resolved relative to this directory.
|
|
139
170
|
* @param {Object} [options] The options that control parsing.
|
|
140
|
-
* @param {boolean} options.sort Whether to sort definitions alphabetically in the preprocess step.
|
|
141
|
-
* @return {
|
|
171
|
+
* @param {boolean} [options.sort] Whether to sort definitions alphabetically in the preprocess step.
|
|
172
|
+
* @return {ParsedModel} A parsed tree representation of the model.
|
|
142
173
|
*/
|
|
143
174
|
export function parseModel(input, modelKind, modelDir, options) {
|
|
144
175
|
if (modelKind === 'vensim') {
|