@sdeverywhere/compile 0.7.33 → 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/package.json +1 -1
- package/src/generate/gen-code-c.js +153 -9
- package/src/model/reduce-variables.js +14 -6
package/package.json
CHANGED
|
@@ -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.
|
|
@@ -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
|