@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdeverywhere/compile",
3
- "version": "0.7.33",
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",
@@ -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
- return chunkedFunctions(
116
- 'initConstants',
117
- Model.constVars(),
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
- return decls(Model.allVars()) + fixedDelayDecls + depreciationDecls
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
- // Add this variable to the set of active ones
53
- // TODO: Allow cycle if the current LHS is referenced on the RHS
54
- // const baseVarId = v.parsedEqn.lhs.varId
55
- // if (baseVarId !== currentLhsBaseVarId) {
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
- throw new Error(`Cycle detected when reducing variables: ${v.refId}`)
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