@sdeverywhere/compile 0.7.17 → 0.7.18
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 -3
- package/src/generate/expand-var-names.js +0 -10
- package/src/generate/{code-gen.js → gen-code-c.js} +188 -89
- package/src/generate/gen-code-js.js +616 -0
- package/src/generate/gen-code.js +36 -0
- package/src/generate/gen-equation.js +16 -6
- package/src/generate/gen-expr.js +205 -48
- package/src/generate/gen-lookup-from-direct.js +14 -6
- package/src/generate/gen-lookup-from-external.js +20 -5
- package/src/generate/gen-lookup-from-points.js +28 -6
- package/src/index.js +38 -3
- package/src/model/model.js +92 -148
- package/src/model/read-equation-fn-game.js +50 -0
- package/src/model/read-equations.js +9 -10
- package/src/model/read-variables.js +2 -2
- package/src/model/variable.js +6 -21
- package/src/parse-and-generate.js +22 -19
- package/src/preprocess/preprocessor.js +2 -2
- package/src/generate/equation-gen.js +0 -1268
- package/src/generate/model-lhs-reader.js +0 -88
- package/src/model/equation-reader.js +0 -723
- package/src/model/expr-reader.js +0 -202
- package/src/model/subscript-range-reader.js +0 -143
- package/src/model/var-name-reader.js +0 -40
- package/src/model/variable-reader.js +0 -172
- package/src/parse/model-reader.js +0 -141
- package/src/parse/parser.js +0 -36
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import * as R from 'ramda'
|
|
2
|
+
|
|
3
|
+
import { asort, canonicalVensimName, lines, strlist, 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 { generateEquation } from './gen-equation.js'
|
|
8
|
+
import { expandVarNames } from './expand-var-names.js'
|
|
9
|
+
|
|
10
|
+
export function generateJS(parsedModel, opts) {
|
|
11
|
+
return codeGenerator(parsedModel, opts).generate()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let codeGenerator = (parsedModel, opts) => {
|
|
15
|
+
const { spec, operations, 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 = spec.outputVarNames === undefined || spec.outputVarNames.length === 0
|
|
20
|
+
// Function to generate a section of the code
|
|
21
|
+
let generateSection = R.map(v => {
|
|
22
|
+
return generateEquation(v, mode, extData, directData, modelDirname, 'js')
|
|
23
|
+
})
|
|
24
|
+
let section = R.pipe(generateSection, R.flatten, lines)
|
|
25
|
+
function generate() {
|
|
26
|
+
// Read variables and subscript ranges from the model parse tree.
|
|
27
|
+
// This is the main entry point for code generation and is called just once.
|
|
28
|
+
Model.read(parsedModel, spec, extData, directData, modelDirname)
|
|
29
|
+
// In list mode, print variables to the console instead of generating code.
|
|
30
|
+
if (operations.includes('printRefIdTest')) {
|
|
31
|
+
Model.printRefIdTest()
|
|
32
|
+
}
|
|
33
|
+
if (operations.includes('printRefGraph')) {
|
|
34
|
+
Model.printRefGraph(opts.varname)
|
|
35
|
+
}
|
|
36
|
+
if (operations.includes('convertNames')) {
|
|
37
|
+
// Do not generate output, but leave the results of model analysis.
|
|
38
|
+
}
|
|
39
|
+
if (operations.includes('generateJS')) {
|
|
40
|
+
// Generate code for each variable in the proper order.
|
|
41
|
+
let code = emitDeclCode()
|
|
42
|
+
code += emitInitLookupsCode()
|
|
43
|
+
code += emitInitConstantsCode()
|
|
44
|
+
code += emitInitLevelsCode()
|
|
45
|
+
code += emitEvalCode()
|
|
46
|
+
code += emitIOCode()
|
|
47
|
+
code += emitModelListing(spec.bundleListing)
|
|
48
|
+
code += emitDefaultFunction()
|
|
49
|
+
return code
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Each code section follows in an outline of the generated model code.
|
|
54
|
+
|
|
55
|
+
//
|
|
56
|
+
// Declaration section
|
|
57
|
+
//
|
|
58
|
+
function emitDeclCode() {
|
|
59
|
+
mode = 'decl'
|
|
60
|
+
return `\
|
|
61
|
+
// Model variables
|
|
62
|
+
${declSection()}
|
|
63
|
+
|
|
64
|
+
// Array dimensions
|
|
65
|
+
${arrayDimensionsSection()}
|
|
66
|
+
|
|
67
|
+
// Dimension mappings
|
|
68
|
+
${dimensionMappingsSection()}
|
|
69
|
+
|
|
70
|
+
// Lookup data arrays
|
|
71
|
+
${section(Model.lookupVars())}
|
|
72
|
+
${section(Model.dataVars())}
|
|
73
|
+
|
|
74
|
+
// Time variable
|
|
75
|
+
let _time;
|
|
76
|
+
/*export*/ function setTime(time) {
|
|
77
|
+
_time = time;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Control variables
|
|
81
|
+
let controlParamsInitialized = false;
|
|
82
|
+
function initControlParamsIfNeeded() {
|
|
83
|
+
if (controlParamsInitialized) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (fns === undefined) {
|
|
88
|
+
throw new Error('Must call setModelFunctions() before running the model');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// We currently require INITIAL TIME and TIME STEP to be defined
|
|
92
|
+
// as constant values. Some models may define SAVEPER in terms of
|
|
93
|
+
// TIME STEP (or FINAL TIME in terms of INITIAL TIME), which means
|
|
94
|
+
// that the compiler may treat them as an aux, not as a constant.
|
|
95
|
+
// We call initConstants() to ensure that we have initial values
|
|
96
|
+
// for these control parameters.
|
|
97
|
+
initConstants();
|
|
98
|
+
if (_initial_time === undefined) {
|
|
99
|
+
throw new Error('INITIAL TIME must be defined as a constant value');
|
|
100
|
+
}
|
|
101
|
+
if (_time_step === undefined) {
|
|
102
|
+
throw new Error('TIME STEP must be defined as a constant value');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (_final_time === undefined || _saveper === undefined) {
|
|
106
|
+
// If _final_time or _saveper is undefined after calling initConstants(),
|
|
107
|
+
// it means one or both is defined as an aux, in which case we perform
|
|
108
|
+
// an initial step of the run loop in order to initialize the value(s).
|
|
109
|
+
// First, set the time and initial function context.
|
|
110
|
+
setTime(_initial_time);
|
|
111
|
+
fns.setContext({
|
|
112
|
+
timeStep: _time_step,
|
|
113
|
+
currentTime: _time
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Perform initial step to initialize _final_time and/or _saveper
|
|
117
|
+
initLevels();
|
|
118
|
+
evalAux();
|
|
119
|
+
if (_final_time === undefined) {
|
|
120
|
+
throw new Error('FINAL TIME must be defined');
|
|
121
|
+
}
|
|
122
|
+
if (_saveper === undefined) {
|
|
123
|
+
throw new Error('SAVEPER must be defined');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
controlParamsInitialized = true;
|
|
128
|
+
}
|
|
129
|
+
/*export*/ function getInitialTime() {
|
|
130
|
+
initControlParamsIfNeeded();
|
|
131
|
+
return _initial_time;
|
|
132
|
+
}
|
|
133
|
+
/*export*/ function getFinalTime() {
|
|
134
|
+
initControlParamsIfNeeded();
|
|
135
|
+
return _final_time;
|
|
136
|
+
}
|
|
137
|
+
/*export*/ function getTimeStep() {
|
|
138
|
+
initControlParamsIfNeeded();
|
|
139
|
+
return _time_step;
|
|
140
|
+
}
|
|
141
|
+
/*export*/ function getSaveFreq() {
|
|
142
|
+
initControlParamsIfNeeded();
|
|
143
|
+
return _saveper;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Model functions
|
|
147
|
+
let fns;
|
|
148
|
+
/*export*/ function getModelFunctions() {
|
|
149
|
+
return fns;
|
|
150
|
+
}
|
|
151
|
+
/*export*/ function setModelFunctions(functions /*: JsModelFunctions*/) {
|
|
152
|
+
fns = functions;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Internal helper functions
|
|
156
|
+
function multiDimArray(dimLengths) {
|
|
157
|
+
if (dimLengths.length > 0) {
|
|
158
|
+
const len = dimLengths[0]
|
|
159
|
+
const arr = new Array(len)
|
|
160
|
+
for (let i = 0; i < len; i++) {
|
|
161
|
+
arr[i] = multiDimArray(dimLengths.slice(1))
|
|
162
|
+
}
|
|
163
|
+
return arr
|
|
164
|
+
} else {
|
|
165
|
+
return 0
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Internal constants
|
|
170
|
+
const _NA_ = -Number.MAX_VALUE;
|
|
171
|
+
|
|
172
|
+
`
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
//
|
|
176
|
+
// Initialization section
|
|
177
|
+
//
|
|
178
|
+
function emitInitLookupsCode() {
|
|
179
|
+
mode = 'init-lookups'
|
|
180
|
+
let code = `// Internal state
|
|
181
|
+
let lookups_initialized = false;
|
|
182
|
+
let data_initialized = false;
|
|
183
|
+
|
|
184
|
+
`
|
|
185
|
+
code += chunkedFunctions(
|
|
186
|
+
'initLookups',
|
|
187
|
+
false,
|
|
188
|
+
Model.lookupVars(),
|
|
189
|
+
' // Initialize lookups\n if (!lookups_initialized) {',
|
|
190
|
+
' lookups_initialized = true;\n }'
|
|
191
|
+
)
|
|
192
|
+
code += '\n'
|
|
193
|
+
code += chunkedFunctions(
|
|
194
|
+
'initData',
|
|
195
|
+
false,
|
|
196
|
+
Model.dataVars(),
|
|
197
|
+
' // Initialize data\n if (!data_initialized) {',
|
|
198
|
+
' data_initialized = true;\n }'
|
|
199
|
+
)
|
|
200
|
+
return code
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function emitInitConstantsCode() {
|
|
204
|
+
mode = 'init-constants'
|
|
205
|
+
return `
|
|
206
|
+
${chunkedFunctions(
|
|
207
|
+
'initConstants',
|
|
208
|
+
true,
|
|
209
|
+
Model.constVars(),
|
|
210
|
+
' // Initialize constants',
|
|
211
|
+
' initLookups();\n initData();'
|
|
212
|
+
)}`
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function emitInitLevelsCode() {
|
|
216
|
+
mode = 'init-levels'
|
|
217
|
+
return `
|
|
218
|
+
${chunkedFunctions(
|
|
219
|
+
'initLevels',
|
|
220
|
+
true,
|
|
221
|
+
Model.initVars(),
|
|
222
|
+
' // Initialize variables with initialization values, such as levels, and the variables they depend on'
|
|
223
|
+
)}`
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
//
|
|
227
|
+
// Evaluation section
|
|
228
|
+
//
|
|
229
|
+
function emitEvalCode() {
|
|
230
|
+
mode = 'eval'
|
|
231
|
+
|
|
232
|
+
return `
|
|
233
|
+
${chunkedFunctions('evalAux', true, Model.auxVars(), ' // Evaluate auxiliaries in order from the bottom up')}
|
|
234
|
+
${chunkedFunctions('evalLevels', true, Model.levelVars(), ' // Evaluate levels')}
|
|
235
|
+
`
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
//
|
|
239
|
+
// Input/output section
|
|
240
|
+
//
|
|
241
|
+
function emitIOCode() {
|
|
242
|
+
mode = 'io'
|
|
243
|
+
|
|
244
|
+
// Configure the body of the `setLookup` function depending on the value
|
|
245
|
+
// of the `customLookups` property in the spec file
|
|
246
|
+
let setLookupBody
|
|
247
|
+
if (spec.customLookups === true || Array.isArray(spec.customLookups)) {
|
|
248
|
+
setLookupBody = `\
|
|
249
|
+
if (!varSpec) {
|
|
250
|
+
throw new Error('Got undefined varSpec in setLookup');
|
|
251
|
+
}
|
|
252
|
+
const varIndex = varSpec.varIndex;
|
|
253
|
+
const subs = varSpec.subscriptIndices;
|
|
254
|
+
switch (varIndex) {
|
|
255
|
+
${setLookupImpl(Model.varIndexInfo(), spec.customLookups)}
|
|
256
|
+
default:
|
|
257
|
+
throw new Error(\`No lookup found for var index \${varIndex} in setLookup\`);
|
|
258
|
+
}`
|
|
259
|
+
} else {
|
|
260
|
+
let msg = 'The setLookup function was not enabled for the generated model. '
|
|
261
|
+
msg += 'Set the customLookups property in the spec/config file to allow for overriding lookups at runtime.'
|
|
262
|
+
setLookupBody = ` throw new Error('${msg}');`
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// This is the list of original output variable names (as supplied by the user in
|
|
266
|
+
// the `spec.json` file), for example, `a[A2,B1]`. These are exported mainly for
|
|
267
|
+
// use in the implementation of the `sde exec` command, which generates a TSV file
|
|
268
|
+
// with a header line that includes the original variable names for all outputs.
|
|
269
|
+
const outputVarNames = outputAllVars ? expandedVarNames(true) : spec.outputVarNames
|
|
270
|
+
const outputVarNameElems = outputVarNames
|
|
271
|
+
.map(name => `'${Model.vensimName(name).replace(/'/g, `\\'`)}'`)
|
|
272
|
+
.join(',\n ')
|
|
273
|
+
|
|
274
|
+
// This is the list of output variable identifiers (in canonical format), for
|
|
275
|
+
// example, `_a[_a2,_b2]`. These are exported for use in the runtime package
|
|
276
|
+
// for having a canonical identifier associated with the data for each output.
|
|
277
|
+
const outputVarIds = outputVarNames.map(canonicalVensimName)
|
|
278
|
+
const outputVarIdElems = outputVarIds.map(id => `'${id}'`).join(',\n ')
|
|
279
|
+
|
|
280
|
+
// This is the list of output variable access declarations, which are in valid
|
|
281
|
+
// C code format, with subscripts mapped to C index form, for example,
|
|
282
|
+
// `_a[1][0]`. These are used in the implementation of `storeOutputs`.
|
|
283
|
+
const outputVarAccesses = outputAllVars ? expandedVarNames() : spec.outputVars
|
|
284
|
+
|
|
285
|
+
// Configure the body of the `storeOutput` function depending on the value
|
|
286
|
+
// of the `customOutputs` property in the spec file
|
|
287
|
+
let storeOutputBody
|
|
288
|
+
if (spec.customOutputs === true || Array.isArray(spec.customOutputs)) {
|
|
289
|
+
storeOutputBody = `\
|
|
290
|
+
if (!varSpec) {
|
|
291
|
+
throw new Error('Got undefined varSpec in storeOutput');
|
|
292
|
+
}
|
|
293
|
+
const varIndex = varSpec.varIndex;
|
|
294
|
+
const subs = varSpec.subscriptIndices;
|
|
295
|
+
switch (varIndex) {
|
|
296
|
+
${customOutputSection(Model.varIndexInfo(), spec.customOutputs)}
|
|
297
|
+
default:
|
|
298
|
+
throw new Error(\`No variable found for var index \${varIndex} in storeOutput\`);
|
|
299
|
+
}`
|
|
300
|
+
} else {
|
|
301
|
+
let msg = 'The storeOutput function was not enabled for the generated model. '
|
|
302
|
+
msg +=
|
|
303
|
+
'Set the customOutputs property in the spec/config file to allow for capturing arbitrary variables at runtime.'
|
|
304
|
+
storeOutputBody = ` throw new Error('${msg}');`
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return `\
|
|
308
|
+
/*export*/ function setInputs(valueAtIndex /*: (index: number) => number*/) {${inputsFromBufferImpl()}}
|
|
309
|
+
|
|
310
|
+
/*export*/ function setLookup(varSpec /*: VarSpec*/, points /*: Float64Array*/) {
|
|
311
|
+
${setLookupBody}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/*export*/ const outputVarIds = [
|
|
315
|
+
${outputVarIdElems}
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
/*export*/ const outputVarNames = [
|
|
319
|
+
${outputVarNameElems}
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
/*export*/ function storeOutputs(storeValue /*: (value: number) => void*/) {
|
|
323
|
+
${specOutputSection(outputVarAccesses)}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/*export*/ function storeOutput(varSpec /*: VarSpec*/, storeValue /*: (value: number) => void*/) {
|
|
327
|
+
${storeOutputBody}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
`
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
//
|
|
334
|
+
// Chunked function helper
|
|
335
|
+
//
|
|
336
|
+
function chunkedFunctions(name, exported, vars, preStep, postStep) {
|
|
337
|
+
// Emit one function for each chunk
|
|
338
|
+
let func = (chunk, idx) => {
|
|
339
|
+
return `\
|
|
340
|
+
function ${name}${idx}() {
|
|
341
|
+
${section(chunk)}
|
|
342
|
+
}
|
|
343
|
+
`
|
|
344
|
+
}
|
|
345
|
+
let funcs = R.pipe(mapIndexed(func), lines)
|
|
346
|
+
|
|
347
|
+
// Emit one roll-up function that calls the other chunk functions
|
|
348
|
+
const indent = name === 'initLookups' || name === 'initData' ? 4 : 2
|
|
349
|
+
let funcCall = (chunk, idx) => {
|
|
350
|
+
return `${' '.repeat(indent)}${name}${idx}();`
|
|
351
|
+
}
|
|
352
|
+
let funcCalls = R.pipe(mapIndexed(funcCall), lines)
|
|
353
|
+
|
|
354
|
+
// Break the vars into chunks. The default value of 30 was empirically
|
|
355
|
+
// determined by looking at runtime performance and memory usage of the
|
|
356
|
+
// En-ROADS model on various devices.
|
|
357
|
+
let chunkSize
|
|
358
|
+
if (process.env.SDE_CODE_GEN_CHUNK_SIZE) {
|
|
359
|
+
chunkSize = parseInt(process.env.SDE_CODE_GEN_CHUNK_SIZE)
|
|
360
|
+
} else {
|
|
361
|
+
chunkSize = 30
|
|
362
|
+
}
|
|
363
|
+
let chunks
|
|
364
|
+
if (chunkSize > 0) {
|
|
365
|
+
chunks = R.splitEvery(chunkSize, vars)
|
|
366
|
+
} else {
|
|
367
|
+
chunks = [vars]
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const chunkedFuncs = funcs(chunks)
|
|
371
|
+
const chunkedCalls = funcCalls(chunks)
|
|
372
|
+
|
|
373
|
+
let code = ''
|
|
374
|
+
if (chunkedFuncs.length > 0) {
|
|
375
|
+
code += `${chunkedFuncs}\n`
|
|
376
|
+
}
|
|
377
|
+
code += `${exported ? '/*export*/ ' : ''}function ${name}() {\n`
|
|
378
|
+
if (preStep?.length > 0) {
|
|
379
|
+
code += `${preStep}\n`
|
|
380
|
+
}
|
|
381
|
+
if (chunkedCalls.length > 0) {
|
|
382
|
+
code += `${chunkedCalls}\n`
|
|
383
|
+
}
|
|
384
|
+
if (postStep?.length > 0) {
|
|
385
|
+
code += `${postStep}\n`
|
|
386
|
+
}
|
|
387
|
+
code += '}\n'
|
|
388
|
+
return code
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
//
|
|
392
|
+
// Declaration section helpers
|
|
393
|
+
//
|
|
394
|
+
function declSection() {
|
|
395
|
+
// Emit a declaration for each variable in the model.
|
|
396
|
+
let fixedDelayDecls = ''
|
|
397
|
+
let depreciationDecls = ''
|
|
398
|
+
let decl = v => {
|
|
399
|
+
// Build a C array declaration for the variable v.
|
|
400
|
+
// This uses the subscript family for each dimension, which may overallocate
|
|
401
|
+
// if the subscript is a subdimension.
|
|
402
|
+
let families = subscriptFamilies(v.subscripts)
|
|
403
|
+
if (v.isFixedDelay()) {
|
|
404
|
+
// TODO
|
|
405
|
+
// Add the associated FixedDelay var decl.
|
|
406
|
+
fixedDelayDecls += `\nFixedDelay* ${v.fixedDelayVarName}${R.map(
|
|
407
|
+
family => `[${sub(family).size}]`,
|
|
408
|
+
families
|
|
409
|
+
).join('')};`
|
|
410
|
+
} else if (v.isDepreciation()) {
|
|
411
|
+
// TODO
|
|
412
|
+
// Add the associated Depreciation var decl.
|
|
413
|
+
depreciationDecls += `\nDepreciation* ${v.depreciationVarName}${R.map(
|
|
414
|
+
family => `[${sub(family).size}]`,
|
|
415
|
+
families
|
|
416
|
+
).join('')};`
|
|
417
|
+
}
|
|
418
|
+
if (families.length > 0) {
|
|
419
|
+
const dimLengths = families.map(family => `${sub(family).size}`).join(', ')
|
|
420
|
+
return `let ${v.varName} = multiDimArray([${dimLengths}]);`
|
|
421
|
+
} else {
|
|
422
|
+
return `let ${v.varName};`
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Non-apply-to-all variables are declared multiple times, but coalesce using uniq.
|
|
426
|
+
let decls = R.pipe(
|
|
427
|
+
R.map(v => `${decl(v)}`),
|
|
428
|
+
R.uniq,
|
|
429
|
+
asort,
|
|
430
|
+
lines
|
|
431
|
+
)
|
|
432
|
+
return decls(Model.allVars()) + fixedDelayDecls + depreciationDecls
|
|
433
|
+
}
|
|
434
|
+
function arrayDimensionsSection() {
|
|
435
|
+
// Emit a declaration for each array dimension's index numbers.
|
|
436
|
+
// These index number arrays will be used to indirectly reference array elements.
|
|
437
|
+
// The indirection is required to support subdimensions that are a non-contiguous subset of the array elements.
|
|
438
|
+
let a = R.map(dim => `const ${dim.name} = [${indexNumberList(sub(dim.name).value)}];`)
|
|
439
|
+
let arrayDims = R.pipe(a, asort, lines)
|
|
440
|
+
return arrayDims(allDimensions())
|
|
441
|
+
}
|
|
442
|
+
function dimensionMappingsSection() {
|
|
443
|
+
// Emit a mapping array for each dimension mapping.
|
|
444
|
+
let a = R.map(m => {
|
|
445
|
+
return `const __map${m.mapFrom}${m.mapTo} = [${indexNumberList(m.value)}];`
|
|
446
|
+
})
|
|
447
|
+
let mappingArrays = R.pipe(a, asort, lines)
|
|
448
|
+
return mappingArrays(allMappings())
|
|
449
|
+
}
|
|
450
|
+
function indexNumberList(indices) {
|
|
451
|
+
// Make a comma-delimited list of index numbers in the dimension working from the index names.
|
|
452
|
+
let a = R.map(indexName => sub(indexName).value, indices)
|
|
453
|
+
return strlist(a)
|
|
454
|
+
}
|
|
455
|
+
function expandedVarNames(vensimNames = false) {
|
|
456
|
+
// Return a list of var names for all variables except lookups and data variables.
|
|
457
|
+
// The names are in Vensim format if vensimNames is true, otherwise they are in C format.
|
|
458
|
+
// Expand subscripted vars into separate var names with each index.
|
|
459
|
+
const canonicalNames = !vensimNames
|
|
460
|
+
return expandVarNames(canonicalNames)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
//
|
|
464
|
+
// Input/output section helpers
|
|
465
|
+
//
|
|
466
|
+
function specOutputSection(varNames) {
|
|
467
|
+
// Emit `storeValue` calls for all variables listed in the `outputVarNames`
|
|
468
|
+
// array in the spec file using varNames in C/JS format.
|
|
469
|
+
let code = R.map(varName => ` storeValue(${varName});`)
|
|
470
|
+
let section = R.pipe(code, lines)
|
|
471
|
+
return section(varNames)
|
|
472
|
+
}
|
|
473
|
+
function customOutputSection(varIndexInfo, customOutputs) {
|
|
474
|
+
// Emit `storeValue` calls for all variables that can be accessed as an output.
|
|
475
|
+
// This excludes data and lookup variables; at this time, the data for these
|
|
476
|
+
// cannot be output like for other types of variables.
|
|
477
|
+
let includeCase
|
|
478
|
+
if (Array.isArray(customOutputs)) {
|
|
479
|
+
// Only include a case statement if the variable was explicitly included
|
|
480
|
+
// in the `customOutputs` array in the spec file
|
|
481
|
+
const customOutputVarNames = customOutputs.map(varName => {
|
|
482
|
+
// The developer might specify a variable name that includes subscripts,
|
|
483
|
+
// but we will ignore the subscript part and only match on the base name
|
|
484
|
+
return canonicalVensimName(varName.split('[')[0])
|
|
485
|
+
})
|
|
486
|
+
includeCase = varName => customOutputVarNames.includes(varName)
|
|
487
|
+
} else {
|
|
488
|
+
// Include a case statement for all accessible variables
|
|
489
|
+
includeCase = () => true
|
|
490
|
+
}
|
|
491
|
+
const outputVars = R.filter(info => {
|
|
492
|
+
return info.varType !== 'lookup' && info.varType !== 'data' && includeCase(info.varName)
|
|
493
|
+
})
|
|
494
|
+
const code = R.map(info => {
|
|
495
|
+
let varAccess = info.varName
|
|
496
|
+
for (let i = 0; i < info.subscriptCount; i++) {
|
|
497
|
+
varAccess += `[subs[${i}]]`
|
|
498
|
+
}
|
|
499
|
+
let c = ''
|
|
500
|
+
c += ` case ${info.varIndex}:\n`
|
|
501
|
+
c += ` storeValue(${varAccess});\n`
|
|
502
|
+
c += ` break;`
|
|
503
|
+
return c
|
|
504
|
+
})
|
|
505
|
+
const section = R.pipe(outputVars, code, lines)
|
|
506
|
+
return section(varIndexInfo)
|
|
507
|
+
}
|
|
508
|
+
function inputsFromBufferImpl() {
|
|
509
|
+
let inputVars = ''
|
|
510
|
+
if (spec.inputVars && spec.inputVars.length > 0) {
|
|
511
|
+
inputVars += '\n'
|
|
512
|
+
for (let i = 0; i < spec.inputVars.length; i++) {
|
|
513
|
+
const inputVar = spec.inputVars[i]
|
|
514
|
+
inputVars += ` ${inputVar} = valueAtIndex(${i});\n`
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return inputVars
|
|
518
|
+
}
|
|
519
|
+
function setLookupImpl(varIndexInfo, customLookups) {
|
|
520
|
+
// Emit `createLookup` calls for all lookups and data variables that can be overridden
|
|
521
|
+
// at runtime
|
|
522
|
+
let overrideAllowed
|
|
523
|
+
if (Array.isArray(customLookups)) {
|
|
524
|
+
// Only include a case statement if the variable was explicitly included
|
|
525
|
+
// in the `customLookups` array in the spec file
|
|
526
|
+
const customLookupVarNames = customLookups.map(varName => {
|
|
527
|
+
// The developer might specify a variable name that includes subscripts,
|
|
528
|
+
// but we will ignore the subscript part and only match on the base name
|
|
529
|
+
return canonicalVensimName(varName.split('[')[0])
|
|
530
|
+
})
|
|
531
|
+
overrideAllowed = varName => customLookupVarNames.includes(varName)
|
|
532
|
+
} else {
|
|
533
|
+
// Include a case statement for all lookup and data variables
|
|
534
|
+
overrideAllowed = () => true
|
|
535
|
+
}
|
|
536
|
+
const lookupAndDataVars = R.filter(info => {
|
|
537
|
+
return (info.varType === 'lookup' || info.varType === 'data') && overrideAllowed(info.varName)
|
|
538
|
+
})
|
|
539
|
+
const code = R.map(info => {
|
|
540
|
+
let lookupVar = info.varName
|
|
541
|
+
for (let i = 0; i < info.subscriptCount; i++) {
|
|
542
|
+
lookupVar += `[subs[${i}]]`
|
|
543
|
+
}
|
|
544
|
+
let c = ''
|
|
545
|
+
c += ` case ${info.varIndex}:\n`
|
|
546
|
+
c += ` ${lookupVar} = fns.createLookup(points.length / 2, points);\n`
|
|
547
|
+
c += ` break;`
|
|
548
|
+
return c
|
|
549
|
+
})
|
|
550
|
+
const section = R.pipe(lookupAndDataVars, code, lines)
|
|
551
|
+
return section(varIndexInfo)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
generate: generate
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
//
|
|
559
|
+
// Module exports
|
|
560
|
+
//
|
|
561
|
+
function emitModelListing(bundleListing) {
|
|
562
|
+
let minimalListingJs
|
|
563
|
+
if (bundleListing !== false) {
|
|
564
|
+
const minimalListingJson = JSON.stringify(Model.jsonList().minimal, null, 2)
|
|
565
|
+
minimalListingJs = minimalListingJson.replace(/"(\w+)"\s*:/g, '$1:').replaceAll('"', "'")
|
|
566
|
+
} else {
|
|
567
|
+
minimalListingJs = 'undefined;'
|
|
568
|
+
}
|
|
569
|
+
return `\
|
|
570
|
+
/*export*/ const modelListing = ${minimalListingJs}
|
|
571
|
+
|
|
572
|
+
`
|
|
573
|
+
}
|
|
574
|
+
function emitDefaultFunction() {
|
|
575
|
+
// TODO: For now, the default function returns an object that has the shape of the
|
|
576
|
+
// `JsModel` interface. It is an async function for future proofing and so that it
|
|
577
|
+
// has the same signature as the default function exported in a generated `WasmModule`.
|
|
578
|
+
// One issue with the current implementation is that the generated code uses
|
|
579
|
+
// module-level storage for variables, so if one were to call this default function
|
|
580
|
+
// more than once, the returned objects would share the same underlying variables
|
|
581
|
+
// (they are not distinct instances). We can fix this by changing the code generator
|
|
582
|
+
// to output a class, or some other approach that allows for creating distinct
|
|
583
|
+
// instances. This is unlikely to be a problem in practice though, so it isn't
|
|
584
|
+
// high priority.
|
|
585
|
+
return `\
|
|
586
|
+
export default async function () {
|
|
587
|
+
return {
|
|
588
|
+
kind: 'js',
|
|
589
|
+
outputVarIds,
|
|
590
|
+
outputVarNames,
|
|
591
|
+
modelListing,
|
|
592
|
+
|
|
593
|
+
getInitialTime,
|
|
594
|
+
getFinalTime,
|
|
595
|
+
getTimeStep,
|
|
596
|
+
getSaveFreq,
|
|
597
|
+
|
|
598
|
+
getModelFunctions,
|
|
599
|
+
setModelFunctions,
|
|
600
|
+
|
|
601
|
+
setTime,
|
|
602
|
+
setInputs,
|
|
603
|
+
setLookup,
|
|
604
|
+
|
|
605
|
+
storeOutputs,
|
|
606
|
+
storeOutput,
|
|
607
|
+
|
|
608
|
+
initConstants,
|
|
609
|
+
initLevels,
|
|
610
|
+
evalAux,
|
|
611
|
+
evalLevels
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
`
|
|
615
|
+
}
|
|
616
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { generateC } from './gen-code-c.js'
|
|
2
|
+
import { generateJS } from './gen-code-js.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate code from the given parsed model.
|
|
6
|
+
*
|
|
7
|
+
* @param {*} parsedModel The parsed model structure.
|
|
8
|
+
* @param {Object} opts The options that control code generation.
|
|
9
|
+
* @param {Object} opts.spec The parsed `spec.json` object.
|
|
10
|
+
* @param {string[]} opts.operations The array of operations to perform.
|
|
11
|
+
* - If it has 'generateC', the generated C code will be written to `buildDir`.
|
|
12
|
+
* - If it has 'generateJS', the generated JS code will be written to `buildDir`.
|
|
13
|
+
* - If it has 'printRefIdTest', reference identifiers will be printed to the console.
|
|
14
|
+
* - If it has 'convertNames', no output will be generated, but the results of model
|
|
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
|
|
18
|
+
* `GET DIRECT DATA` call (e.g., `?data`) to the tabular data contained in the loaded
|
|
19
|
+
* data file.
|
|
20
|
+
* @param {string} opts.modelDirname The path to the directory containing the model
|
|
21
|
+
* (used for resolving data files for `GET DIRECT SUBSCRIPT`).
|
|
22
|
+
* @returns A string containing the generated code.
|
|
23
|
+
*/
|
|
24
|
+
export function generateCode(parsedModel, opts) {
|
|
25
|
+
// Note that the two `generate` functions perform the same steps (other than the
|
|
26
|
+
// difference in output format), so we will use `generateJS` if JS is requested
|
|
27
|
+
// as the output format, otherwise we will use `generateC`.
|
|
28
|
+
// TODO: For now we only allow for either generateJS or generateC, but not both at
|
|
29
|
+
// the same time. Maybe we should make it possible to generate both with a single
|
|
30
|
+
// call.
|
|
31
|
+
if (opts.operations.includes('generateJS')) {
|
|
32
|
+
return generateJS(parsedModel, opts)
|
|
33
|
+
} else {
|
|
34
|
+
return generateC(parsedModel, opts)
|
|
35
|
+
}
|
|
36
|
+
}
|