@sdeverywhere/compile 0.7.17 → 0.7.19

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,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
+ }