@sdeverywhere/compile 0.7.0

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.
@@ -0,0 +1,356 @@
1
+ import R from 'ramda'
2
+
3
+ import { asort, lines, strlist, abend, mapIndexed } from '../_shared/helpers.js'
4
+ import { sub, allDimensions, allMappings, subscriptFamilies } from '../_shared/subscript.js'
5
+ import Model from '../model/model.js'
6
+
7
+ import EquationGen from './equation-gen.js'
8
+ import ModelLHSReader from './model-lhs-reader.js'
9
+
10
+ export function generateCode(parseTree, opts) {
11
+ return codeGenerator(parseTree, opts).generate()
12
+ }
13
+
14
+ let codeGenerator = (parseTree, opts) => {
15
+ const { spec, operation, extData, directData, modelDirname } = opts
16
+ // Set to 'decl', 'init-lookups', 'eval', etc depending on the section being generated.
17
+ let mode = ''
18
+ // Set to true to output all variables when there is no model run spec.
19
+ let outputAllVars
20
+ if (spec.outputVars && spec.outputVars.length > 0) {
21
+ outputAllVars = false
22
+ } else if (spec.outputVarNames && spec.outputVarNames.length > 0) {
23
+ outputAllVars = false
24
+ } else {
25
+ outputAllVars = true
26
+ }
27
+ // Function to generate a section of the code
28
+ let generateSection = R.map(v => new EquationGen(v, extData, directData, mode, modelDirname).generate())
29
+ let section = R.pipe(generateSection, R.flatten, lines)
30
+ function generate() {
31
+ // Read variables and subscript ranges from the model parse tree.
32
+ // This is the main entry point for code generation and is called just once.
33
+ try {
34
+ Model.read(parseTree, spec, extData, directData, modelDirname)
35
+ // In list mode, print variables to the console instead of generating code.
36
+ if (operation === 'printRefIdTest') {
37
+ Model.printRefIdTest()
38
+ } else if (operation === 'printRefGraph') {
39
+ Model.printRefGraph(opts.varname)
40
+ } else if (operation === 'convertNames') {
41
+ // Do not generate output, but leave the results of model analysis.
42
+ } else if (operation === 'generateC') {
43
+ // Generate code for each variable in the proper order.
44
+ let code = emitDeclCode()
45
+ code += emitInitLookupsCode()
46
+ code += emitInitConstantsCode()
47
+ code += emitInitLevelsCode()
48
+ code += emitEvalCode()
49
+ code += emitIOCode()
50
+ return code
51
+ }
52
+ } catch (e) {
53
+ abend(e)
54
+ }
55
+ }
56
+
57
+ // Each code section follows in an outline of the generated model code.
58
+
59
+ //
60
+ // Declaration section
61
+ //
62
+ function emitDeclCode() {
63
+ mode = 'decl'
64
+ return `#include "sde.h"
65
+
66
+ // Model variables
67
+ ${declSection()}
68
+
69
+ // Internal variables
70
+ ${internalVarsSection()}
71
+
72
+ // Array dimensions
73
+ ${arrayDimensionsSection()}
74
+
75
+ // Dimension mappings
76
+ ${dimensionMappingsSection()}
77
+
78
+ // Lookup data arrays
79
+ ${section(Model.lookupVars())}
80
+ ${section(Model.dataVars())}
81
+
82
+ `
83
+ }
84
+
85
+ //
86
+ // Initialization section
87
+ //
88
+ function emitInitLookupsCode() {
89
+ mode = 'init-lookups'
90
+ let code = `// Internal state
91
+ bool lookups_initialized = false;
92
+ bool data_initialized = false;
93
+ `
94
+ code += chunkedFunctions(
95
+ 'initLookups',
96
+ Model.lookupVars(),
97
+ ` // Initialize lookups.
98
+ if (!lookups_initialized) {
99
+ `,
100
+ ` lookups_initialized = true;
101
+ }
102
+ `
103
+ )
104
+ code += chunkedFunctions(
105
+ 'initData',
106
+ Model.dataVars(),
107
+ ` // Initialize data.
108
+ if (!data_initialized) {
109
+ `,
110
+ ` data_initialized = true;
111
+ }
112
+ `
113
+ )
114
+ return code
115
+ }
116
+
117
+ function emitInitConstantsCode() {
118
+ mode = 'init-constants'
119
+ return `
120
+ ${chunkedFunctions('initConstants', Model.constVars(), ' // Initialize constants.', ' initLookups();\n initData();')}
121
+ `
122
+ }
123
+
124
+ function emitInitLevelsCode() {
125
+ mode = 'init-levels'
126
+ return `
127
+ ${chunkedFunctions(
128
+ 'initLevels',
129
+ Model.initVars(),
130
+ `
131
+ // Initialize variables with initialization values, such as levels, and the variables they depend on.
132
+ _time = _initial_time;`
133
+ )}
134
+ `
135
+ }
136
+
137
+ //
138
+ // Evaluation section
139
+ //
140
+ function emitEvalCode() {
141
+ mode = 'eval'
142
+
143
+ return `
144
+ ${chunkedFunctions('evalAux', Model.auxVars(), ' // Evaluate auxiliaries in order from the bottom up.')}
145
+
146
+ ${chunkedFunctions('evalLevels', Model.levelVars(), ' // Evaluate levels.')}
147
+ `
148
+ }
149
+
150
+ //
151
+ // Input/output section
152
+ //
153
+ function emitIOCode() {
154
+ let headerVars = outputAllVars ? expandedVarNames(true) : spec.outputVars
155
+ let outputVars = outputAllVars ? expandedVarNames() : spec.outputVars
156
+ mode = 'io'
157
+ return `void setInputs(const char* inputData) {${inputsFromStringImpl()}}
158
+
159
+ void setInputsFromBuffer(double* inputData) {${inputsFromBufferImpl()}}
160
+
161
+ const char* getHeader() {
162
+ return "${R.map(varName => headerTitle(varName), headerVars).join('\\t')}";
163
+ }
164
+
165
+ void storeOutputData() {
166
+ ${outputSection(outputVars)}
167
+ }
168
+ `
169
+ }
170
+
171
+ //
172
+ // Chunked function helper
173
+ //
174
+ function chunkedFunctions(name, vars, preStep, postStep) {
175
+ // Emit one function for each chunk
176
+ let func = (chunk, idx) => {
177
+ return `
178
+ void ${name}${idx}() {
179
+ ${section(chunk)}
180
+ }
181
+ `
182
+ }
183
+ let funcs = R.pipe(mapIndexed(func), lines)
184
+
185
+ // Emit one roll-up function that calls the other chunk functions
186
+ let funcCall = (chunk, idx) => {
187
+ return ` ${name}${idx}();`
188
+ }
189
+ let funcCalls = R.pipe(mapIndexed(funcCall), lines)
190
+
191
+ // Break the vars into chunks of 30; this number was empirically
192
+ // determined by looking at runtime performance and memory usage
193
+ // of the En-ROADS model on various devices
194
+ let chunks = R.splitEvery(30, vars)
195
+
196
+ if (!preStep) {
197
+ preStep = ''
198
+ }
199
+ if (!postStep) {
200
+ postStep = ''
201
+ }
202
+
203
+ return `
204
+ ${funcs(chunks)}
205
+
206
+ void ${name}() {
207
+ ${preStep}
208
+ ${funcCalls(chunks)}
209
+ ${postStep}
210
+ }
211
+ `
212
+ }
213
+
214
+ //
215
+ // Declaration section helpers
216
+ //
217
+ function declSection() {
218
+ // Emit a declaration for each variable in the model.
219
+ let fixedDelayDecls = ''
220
+ let decl = v => {
221
+ // Build a C array declaration for the variable v.
222
+ // This uses the subscript family for each dimension, which may overallocate
223
+ // if the subscript is a subdimension.
224
+ let varType = v.isLookup() || v.isData() ? 'Lookup* ' : 'double '
225
+ let families = subscriptFamilies(v.subscripts)
226
+ if (v.isFixedDelay()) {
227
+ // Add the associated FixedDelay var decl.
228
+ fixedDelayDecls += `\nFixedDelay* ${v.fixedDelayVarName}${R.map(
229
+ family => `[${sub(family).size}]`,
230
+ families
231
+ ).join('')};`
232
+ }
233
+ return varType + v.varName + R.map(family => `[${sub(family).size}]`, families).join('')
234
+ }
235
+ // Non-apply-to-all variables are declared multiple times, but coalesce using uniq.
236
+ let decls = R.pipe(
237
+ R.map(v => `${decl(v)};`),
238
+ R.uniq,
239
+ asort,
240
+ lines
241
+ )
242
+ return decls(Model.allVars()) + fixedDelayDecls
243
+ }
244
+ function internalVarsSection() {
245
+ // Declare internal variables to run the model.
246
+ if (outputAllVars) {
247
+ return `const int numOutputs = ${expandedVarNames().length};`
248
+ } else {
249
+ return `const int numOutputs = ${spec.outputVars.length};`
250
+ }
251
+ }
252
+ function arrayDimensionsSection() {
253
+ // Emit a declaration for each array dimension's index numbers.
254
+ // These index number arrays will be used to indirectly reference array elements.
255
+ // The indirection is required to support subdimensions that are a non-contiguous subset of the array elements.
256
+ let a = R.map(dim => `const size_t ${dim.name}[${dim.size}] = { ${indexNumberList(sub(dim.name).value)} };`)
257
+ let arrayDims = R.pipe(a, asort, lines)
258
+ return arrayDims(allDimensions())
259
+ }
260
+ function dimensionMappingsSection() {
261
+ // Emit a mapping array for each dimension mapping.
262
+ let a = R.map(m => {
263
+ return `const size_t __map${m.mapFrom}${m.mapTo}[${sub(m.mapTo).size}] = { ${indexNumberList(m.value)} };`
264
+ })
265
+ let mappingArrays = R.pipe(a, asort, lines)
266
+ return mappingArrays(allMappings())
267
+ }
268
+ function indexNumberList(indices) {
269
+ // Make a comma-delimited list of index numbers in the dimension working from the index names.
270
+ let a = R.map(indexName => sub(indexName).value, indices)
271
+ return strlist(a)
272
+ }
273
+ function expandedVarNames(vensimNames = false) {
274
+ // Return a list of var names for all variables except lookups and data variables.
275
+ // The names are in Vensim format if vensimNames is true, otherwise they are in C format.
276
+ // Expand subscripted vars into separate var names with each index.
277
+ function sortedVars() {
278
+ // Return a list of all vars sorted by the model LHS var name (without subscripts), case insensitive.
279
+ return R.sortBy(v => {
280
+ let modelLHSReader = new ModelLHSReader()
281
+ modelLHSReader.read(v.modelLHS)
282
+ return modelLHSReader.varName.toUpperCase()
283
+ }, Model.variables)
284
+ }
285
+ return R.uniq(
286
+ R.reduce(
287
+ (a, v) => {
288
+ if (v.varType !== 'lookup' && v.varType !== 'data' && v.includeInOutput) {
289
+ let modelLHSReader = new ModelLHSReader()
290
+ modelLHSReader.read(v.modelLHS)
291
+ if (vensimNames) {
292
+ return R.concat(a, modelLHSReader.names())
293
+ } else {
294
+ return R.concat(a, R.map(Model.cName, modelLHSReader.names()))
295
+ }
296
+ } else {
297
+ return a
298
+ }
299
+ },
300
+ [],
301
+ sortedVars()
302
+ )
303
+ )
304
+ }
305
+ //
306
+ // Input/output section helpers
307
+ //
308
+ function outputSection(varNames) {
309
+ // Emit output calls using varNames in C format.
310
+ let code = R.map(varName => ` outputVar(${varName});`)
311
+ let section = R.pipe(code, lines)
312
+ return section(varNames)
313
+ }
314
+ function inputsFromStringImpl() {
315
+ // If there was an I/O spec file, then emit code to parse input variables.
316
+ // The user can replace this with a parser for a different serialization format.
317
+ let inputVars = ''
318
+ if (spec.inputVars && spec.inputVars.length > 0) {
319
+ let inputVarPtrs = R.reduce((a, inputVar) => R.concat(a, ` &${inputVar},\n`), '', spec.inputVars)
320
+ inputVars = `
321
+ static double* inputVarPtrs[] = {\n${inputVarPtrs} };
322
+ char* inputs = (char*)inputData;
323
+ char* token = strtok(inputs, " ");
324
+ while (token) {
325
+ char* p = strchr(token, ':');
326
+ if (p) {
327
+ *p = '\\0';
328
+ int modelVarIndex = atoi(token);
329
+ double value = atof(p+1);
330
+ *inputVarPtrs[modelVarIndex] = value;
331
+ }
332
+ token = strtok(NULL, " ");
333
+ }
334
+ `
335
+ }
336
+ return inputVars
337
+ }
338
+ function inputsFromBufferImpl() {
339
+ let inputVars = ''
340
+ if (spec.inputVars && spec.inputVars.length > 0) {
341
+ inputVars += '\n'
342
+ for (let i = 0; i < spec.inputVars.length; i++) {
343
+ const inputVar = spec.inputVars[i]
344
+ inputVars += ` ${inputVar} = inputData[${i}];\n`
345
+ }
346
+ }
347
+ return inputVars
348
+ }
349
+ function headerTitle(varName) {
350
+ return Model.vensimName(varName).replace(/"/g, '\\"')
351
+ }
352
+
353
+ return {
354
+ generate: generate
355
+ }
356
+ }