@sdeverywhere/compile 0.7.26 → 0.7.28
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 +2 -2
- package/src/generate/gen-code-c.js +86 -36
- package/src/generate/gen-code-js.js +59 -0
- package/src/generate/gen-expr.js +87 -37
- package/src/index.js +9 -1
- package/src/model/model.js +79 -5
- package/src/model/read-equation-fn-delay.js +6 -4
- package/src/model/read-equation-fn-smooth.js +7 -5
- package/src/model/read-equation-fn-trend.js +1 -1
- package/src/model/read-equations.js +541 -234
- package/src/parse-and-generate.js +35 -23
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdeverywhere/compile",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.28",
|
|
4
4
|
"description": "The core Vensim to C compiler for the SDEverywhere tool suite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@sdeverywhere/parse": "^0.1.
|
|
8
|
+
"@sdeverywhere/parse": "^0.1.3",
|
|
9
9
|
"byline": "^5.0.0",
|
|
10
10
|
"csv-parse": "^5.3.3",
|
|
11
11
|
"ramda": "^0.27.0",
|
|
@@ -146,6 +146,24 @@ ${chunkedFunctions('evalLevels', Model.levelVars(), ' // Evaluate levels.')}`
|
|
|
146
146
|
// Input/output section
|
|
147
147
|
//
|
|
148
148
|
function emitIOCode() {
|
|
149
|
+
// Configure the body of the `setConstant` function depending on the value
|
|
150
|
+
// of the `customConstants` property in the spec file
|
|
151
|
+
let setConstantBody
|
|
152
|
+
if (spec.customConstants === true || Array.isArray(spec.customConstants)) {
|
|
153
|
+
setConstantBody = `\
|
|
154
|
+
switch (varIndex) {
|
|
155
|
+
${setConstantImpl(Model.varIndexInfo(), spec.customConstants)}
|
|
156
|
+
default:
|
|
157
|
+
fprintf(stderr, "No constant found for var index %zu in setConstant\\n", varIndex);
|
|
158
|
+
break;
|
|
159
|
+
}`
|
|
160
|
+
} else {
|
|
161
|
+
let msg = 'The setConstant function was not enabled for the generated model. '
|
|
162
|
+
msg += 'Set the customConstants property in the spec/config file to allow for overriding constants at runtime.'
|
|
163
|
+
setConstantBody = `\
|
|
164
|
+
fprintf(stderr, "${msg}\\n");`
|
|
165
|
+
}
|
|
166
|
+
|
|
149
167
|
// Configure the body of the `setLookup` function depending on the value
|
|
150
168
|
// of the `customLookups` property in the spec file
|
|
151
169
|
// TODO: The fprintf calls should be replaced with a mechanism that throws
|
|
@@ -200,12 +218,12 @@ ${customOutputSection(Model.varIndexInfo(), spec.customOutputs)}
|
|
|
200
218
|
|
|
201
219
|
mode = 'io'
|
|
202
220
|
return `\
|
|
203
|
-
void setInputs(
|
|
204
|
-
${
|
|
221
|
+
void setInputs(double* inputValues, int32_t* inputIndices) {
|
|
222
|
+
${setInputsImpl()}
|
|
205
223
|
}
|
|
206
224
|
|
|
207
|
-
void
|
|
208
|
-
${
|
|
225
|
+
void setConstant(size_t varIndex, size_t* subIndices, double value) {
|
|
226
|
+
${setConstantBody}
|
|
209
227
|
}
|
|
210
228
|
|
|
211
229
|
void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPoints) {
|
|
@@ -322,13 +340,19 @@ ${section(chunk)}
|
|
|
322
340
|
}
|
|
323
341
|
function internalVarsSection() {
|
|
324
342
|
// Declare internal variables to run the model.
|
|
325
|
-
let
|
|
343
|
+
let numInputsDecl
|
|
344
|
+
if (spec.inputVars && spec.inputVars.length > 0) {
|
|
345
|
+
numInputsDecl = `const int numInputs = ${spec.inputVars.length};`
|
|
346
|
+
} else {
|
|
347
|
+
numInputsDecl = `const int numInputs = 0;`
|
|
348
|
+
}
|
|
349
|
+
let numOutputsDecl
|
|
326
350
|
if (outputAllVars) {
|
|
327
|
-
|
|
351
|
+
numOutputsDecl = `const int numOutputs = ${expandedVarNames().length};`
|
|
328
352
|
} else {
|
|
329
|
-
|
|
353
|
+
numOutputsDecl = `const int numOutputs = ${spec.outputVars.length};`
|
|
330
354
|
}
|
|
331
|
-
return
|
|
355
|
+
return `${numInputsDecl}\n${numOutputsDecl}`
|
|
332
356
|
}
|
|
333
357
|
function arrayDimensionsSection() {
|
|
334
358
|
// Emit a declaration for each array dimension's index numbers.
|
|
@@ -402,38 +426,64 @@ ${section(chunk)}
|
|
|
402
426
|
const section = R.pipe(outputVars, code, lines)
|
|
403
427
|
return section(varIndexInfo)
|
|
404
428
|
}
|
|
405
|
-
function
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
let inputVars = ''
|
|
409
|
-
if (spec.inputVars && spec.inputVars.length > 0) {
|
|
410
|
-
let inputVarPtrs = R.reduce((a, inputVar) => R.concat(a, ` &${inputVar},\n`), '', spec.inputVars)
|
|
411
|
-
inputVars = `\
|
|
412
|
-
static double* inputVarPtrs[] = {\n${inputVarPtrs} };
|
|
413
|
-
char* inputs = (char*)inputData;
|
|
414
|
-
char* token = strtok(inputs, " ");
|
|
415
|
-
while (token) {
|
|
416
|
-
char* p = strchr(token, ':');
|
|
417
|
-
if (p) {
|
|
418
|
-
*p = '\\0';
|
|
419
|
-
int modelVarIndex = atoi(token);
|
|
420
|
-
double value = atof(p+1);
|
|
421
|
-
*inputVarPtrs[modelVarIndex] = value;
|
|
429
|
+
function setInputsImpl() {
|
|
430
|
+
if (!spec.inputVars || spec.inputVars.length === 0) {
|
|
431
|
+
return ''
|
|
422
432
|
}
|
|
423
|
-
|
|
424
|
-
|
|
433
|
+
// Build the pointer table for input variables
|
|
434
|
+
let inputVarPtrs = R.reduce((a, inputVar) => R.concat(a, ` &${inputVar},\n`), '', spec.inputVars)
|
|
435
|
+
return `\
|
|
436
|
+
static double* inputVarPtrs[] = {
|
|
437
|
+
${inputVarPtrs} };
|
|
438
|
+
if (inputIndices == NULL) {
|
|
439
|
+
// When inputIndices is NULL, assume that inputValues contains all input values
|
|
440
|
+
// in the same order that the variables are defined in the model spec
|
|
441
|
+
for (size_t i = 0; i < numInputs; i++) {
|
|
442
|
+
*inputVarPtrs[i] = inputValues[i];
|
|
425
443
|
}
|
|
426
|
-
|
|
444
|
+
} else {
|
|
445
|
+
// When inputIndices is non-NULL, set the input values according to the indices
|
|
446
|
+
// in the inputIndices array, where each index corresponds to the index of the
|
|
447
|
+
// variable in the model spec
|
|
448
|
+
size_t numInputsToSet = (size_t)inputIndices[0];
|
|
449
|
+
for (size_t i = 0; i < numInputsToSet; i++) {
|
|
450
|
+
size_t inputVarIndex = (size_t)inputIndices[i + 1];
|
|
451
|
+
*inputVarPtrs[inputVarIndex] = inputValues[i];
|
|
452
|
+
}
|
|
453
|
+
}`
|
|
427
454
|
}
|
|
428
|
-
function
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
455
|
+
function setConstantImpl(varIndexInfo, customConstants) {
|
|
456
|
+
// Emit case statements for all const variables that can be overridden at runtime
|
|
457
|
+
let includeCase
|
|
458
|
+
if (Array.isArray(customConstants)) {
|
|
459
|
+
// Only include a case statement if the variable was explicitly included
|
|
460
|
+
// in the `customConstants` array in the spec file
|
|
461
|
+
const customConstantVarNames = customConstants.map(varName => {
|
|
462
|
+
// The developer might specify a variable name that includes subscripts,
|
|
463
|
+
// but we will ignore the subscript part and only match on the base name
|
|
464
|
+
return canonicalVensimName(varName.split('[')[0])
|
|
465
|
+
})
|
|
466
|
+
includeCase = varName => customConstantVarNames.includes(varName)
|
|
467
|
+
} else {
|
|
468
|
+
// Include a case statement for all constant variables
|
|
469
|
+
includeCase = () => true
|
|
435
470
|
}
|
|
436
|
-
|
|
471
|
+
const constVars = R.filter(info => {
|
|
472
|
+
return info.varType === 'const' && includeCase(info.varName)
|
|
473
|
+
})
|
|
474
|
+
const code = R.map(info => {
|
|
475
|
+
let constVar = info.varName
|
|
476
|
+
for (let i = 0; i < info.subscriptCount; i++) {
|
|
477
|
+
constVar += `[subIndices[${i}]]`
|
|
478
|
+
}
|
|
479
|
+
let c = ''
|
|
480
|
+
c += ` case ${info.varIndex}:\n`
|
|
481
|
+
c += ` ${constVar} = value;\n`
|
|
482
|
+
c += ` break;`
|
|
483
|
+
return c
|
|
484
|
+
})
|
|
485
|
+
const section = R.pipe(constVars, code, lines)
|
|
486
|
+
return section(varIndexInfo)
|
|
437
487
|
}
|
|
438
488
|
function setLookupImpl(varIndexInfo, customLookups) {
|
|
439
489
|
// Emit case statements for all lookups and data variables that can be overridden
|
|
@@ -241,6 +241,27 @@ ${chunkedFunctions('evalLevels', true, Model.levelVars(), ' // Evaluate levels'
|
|
|
241
241
|
function emitIOCode() {
|
|
242
242
|
mode = 'io'
|
|
243
243
|
|
|
244
|
+
// Configure the body of the `setConstant` function depending on the value
|
|
245
|
+
// of the `customConstants` property in the spec file
|
|
246
|
+
let setConstantBody
|
|
247
|
+
if (spec.customConstants === true || Array.isArray(spec.customConstants)) {
|
|
248
|
+
setConstantBody = `\
|
|
249
|
+
if (!varSpec) {
|
|
250
|
+
throw new Error('Got undefined varSpec in setConstant');
|
|
251
|
+
}
|
|
252
|
+
const varIndex = varSpec.varIndex;
|
|
253
|
+
const subs = varSpec.subscriptIndices;
|
|
254
|
+
switch (varIndex) {
|
|
255
|
+
${setConstantImpl(Model.varIndexInfo(), spec.customConstants)}
|
|
256
|
+
default:
|
|
257
|
+
throw new Error(\`No constant found for var index \${varIndex} in setConstant\`);
|
|
258
|
+
}`
|
|
259
|
+
} else {
|
|
260
|
+
let msg = 'The setConstant function was not enabled for the generated model. '
|
|
261
|
+
msg += 'Set the customConstants property in the spec/config file to allow for overriding constants at runtime.'
|
|
262
|
+
setConstantBody = ` throw new Error('${msg}');`
|
|
263
|
+
}
|
|
264
|
+
|
|
244
265
|
// Configure the body of the `setLookup` function depending on the value
|
|
245
266
|
// of the `customLookups` property in the spec file
|
|
246
267
|
let setLookupBody
|
|
@@ -312,6 +333,10 @@ ${customOutputSection(Model.varIndexInfo(), spec.customOutputs)}
|
|
|
312
333
|
return `\
|
|
313
334
|
/*export*/ function setInputs(valueAtIndex /*: (index: number) => number*/) {${inputsFromBufferImpl()}}
|
|
314
335
|
|
|
336
|
+
/*export*/ function setConstant(varSpec /*: VarSpec*/, value /*: number*/) {
|
|
337
|
+
${setConstantBody}
|
|
338
|
+
}
|
|
339
|
+
|
|
315
340
|
/*export*/ function setLookup(varSpec /*: VarSpec*/, points /*: Float64Array | undefined*/) {
|
|
316
341
|
${setLookupBody}
|
|
317
342
|
}
|
|
@@ -521,6 +546,39 @@ ${section(chunk)}
|
|
|
521
546
|
}
|
|
522
547
|
return inputVars
|
|
523
548
|
}
|
|
549
|
+
function setConstantImpl(varIndexInfo, customConstants) {
|
|
550
|
+
// Emit case statements for all const variables that can be overridden at runtime
|
|
551
|
+
let overrideAllowed
|
|
552
|
+
if (Array.isArray(customConstants)) {
|
|
553
|
+
// Only include a case statement if the variable was explicitly included
|
|
554
|
+
// in the `customConstants` array in the spec file
|
|
555
|
+
const customConstantVarNames = customConstants.map(varName => {
|
|
556
|
+
// The developer might specify a variable name that includes subscripts,
|
|
557
|
+
// but we will ignore the subscript part and only match on the base name
|
|
558
|
+
return canonicalVensimName(varName.split('[')[0])
|
|
559
|
+
})
|
|
560
|
+
overrideAllowed = varName => customConstantVarNames.includes(varName)
|
|
561
|
+
} else {
|
|
562
|
+
// Include a case statement for all constant variables
|
|
563
|
+
overrideAllowed = () => true
|
|
564
|
+
}
|
|
565
|
+
const constVars = R.filter(info => {
|
|
566
|
+
return info.varType === 'const' && overrideAllowed(info.varName)
|
|
567
|
+
})
|
|
568
|
+
const code = R.map(info => {
|
|
569
|
+
let constVar = info.varName
|
|
570
|
+
for (let i = 0; i < info.subscriptCount; i++) {
|
|
571
|
+
constVar += `[subs[${i}]]`
|
|
572
|
+
}
|
|
573
|
+
let c = ''
|
|
574
|
+
c += ` case ${info.varIndex}:\n`
|
|
575
|
+
c += ` ${constVar} = value;\n`
|
|
576
|
+
c += ` break;`
|
|
577
|
+
return c
|
|
578
|
+
})
|
|
579
|
+
const section = R.pipe(constVars, code, lines)
|
|
580
|
+
return section(varIndexInfo)
|
|
581
|
+
}
|
|
524
582
|
function setLookupImpl(varIndexInfo, customLookups) {
|
|
525
583
|
// Emit case statements for all lookups and data variables that can be overridden
|
|
526
584
|
// at runtime
|
|
@@ -605,6 +663,7 @@ export default async function () {
|
|
|
605
663
|
|
|
606
664
|
setTime,
|
|
607
665
|
setInputs,
|
|
666
|
+
setConstant,
|
|
608
667
|
setLookup,
|
|
609
668
|
|
|
610
669
|
storeOutputs,
|
package/src/generate/gen-expr.js
CHANGED
|
@@ -162,8 +162,28 @@ export function generateExpr(expr, ctx) {
|
|
|
162
162
|
* @return {string} The generated C/JS code.
|
|
163
163
|
*/
|
|
164
164
|
function generateFunctionCall(callExpr, ctx) {
|
|
165
|
-
|
|
165
|
+
function generateSimpleFunctionCall(fnId) {
|
|
166
|
+
const args = callExpr.args.map(argExpr => generateExpr(argExpr, ctx))
|
|
167
|
+
if (ctx.outFormat === 'js' && fnId === '_IF_THEN_ELSE') {
|
|
168
|
+
// When generating conditional expressions for JS target, since we can't rely on macros like we do for C,
|
|
169
|
+
// it is better to translate it into a ternary instead of relying on a built-in function (since the latter
|
|
170
|
+
// would require always evaluating both branches, while the former can be more optimized by the interpreter)
|
|
171
|
+
return `((${args[0]}) ? (${args[1]}) : (${args[2]}))`
|
|
172
|
+
} else {
|
|
173
|
+
// For simple functions, emit a C/JS function call with a generated C/JS expression for each argument
|
|
174
|
+
return `${fnRef(fnId, ctx)}(${args.join(', ')})`
|
|
175
|
+
}
|
|
176
|
+
}
|
|
166
177
|
|
|
178
|
+
function generateLookupFunctionCall(fnId) {
|
|
179
|
+
// For LOOKUP* functions, the first argument must be a reference to the lookup variable. Emit
|
|
180
|
+
// a C/JS function call with a generated C/JS expression for each remaining argument.
|
|
181
|
+
const cVarRef = ctx.cVarRef(callExpr.args[0])
|
|
182
|
+
const cArgs = callExpr.args.slice(1).map(arg => generateExpr(arg, ctx))
|
|
183
|
+
return `${fnRef(fnId, ctx)}(${cVarRef}, ${cArgs.join(', ')})`
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const fnId = callExpr.fnId
|
|
167
187
|
switch (fnId) {
|
|
168
188
|
//
|
|
169
189
|
//
|
|
@@ -174,46 +194,54 @@ function generateFunctionCall(callExpr, ctx) {
|
|
|
174
194
|
//
|
|
175
195
|
//
|
|
176
196
|
|
|
197
|
+
// Simple functions that are common to Vensim and XMILE/Stella
|
|
177
198
|
case '_ABS':
|
|
178
199
|
case '_ARCCOS':
|
|
179
200
|
case '_ARCSIN':
|
|
180
201
|
case '_ARCTAN':
|
|
181
202
|
case '_COS':
|
|
182
203
|
case '_EXP':
|
|
183
|
-
case '_GAMMA_LN':
|
|
184
204
|
case '_IF_THEN_ELSE':
|
|
185
|
-
case '_INTEGER':
|
|
186
205
|
case '_LN':
|
|
187
206
|
case '_MAX':
|
|
188
207
|
case '_MIN':
|
|
189
|
-
case '_MODULO':
|
|
190
|
-
case '_POW':
|
|
191
|
-
case '_POWER':
|
|
192
|
-
case '_PULSE':
|
|
193
|
-
case '_PULSE_TRAIN':
|
|
194
|
-
case '_QUANTUM':
|
|
195
208
|
case '_RAMP':
|
|
196
209
|
case '_SIN':
|
|
197
210
|
case '_SQRT':
|
|
198
211
|
case '_STEP':
|
|
199
212
|
case '_TAN':
|
|
213
|
+
return generateSimpleFunctionCall(fnId)
|
|
214
|
+
|
|
215
|
+
// Simple functions supported by Vensim only
|
|
216
|
+
case '_GAMMA_LN':
|
|
217
|
+
case '_INTEGER':
|
|
218
|
+
case '_MODULO':
|
|
219
|
+
case '_POW':
|
|
220
|
+
case '_POWER':
|
|
221
|
+
case '_PULSE_TRAIN':
|
|
222
|
+
case '_PULSE':
|
|
223
|
+
case '_QUANTUM':
|
|
200
224
|
case '_WITH_LOOKUP':
|
|
201
225
|
case '_XIDZ':
|
|
202
|
-
case '_ZIDZ':
|
|
203
|
-
const args = callExpr.args.map(argExpr => generateExpr(argExpr, ctx))
|
|
226
|
+
case '_ZIDZ':
|
|
204
227
|
if (ctx.outFormat === 'js' && fnId === '_GAMMA_LN') {
|
|
205
228
|
throw new Error(`${callExpr.fnName} function not yet implemented for JS code gen`)
|
|
206
229
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
230
|
+
return generateSimpleFunctionCall(fnId)
|
|
231
|
+
|
|
232
|
+
// Simple functions supported by XMILE/Stella only
|
|
233
|
+
case '_INT':
|
|
234
|
+
// XMILE/Stella uses `INT`, but it is the same as the Vensim `INTEGER` function,
|
|
235
|
+
// which is the name used in the runtime function implementation
|
|
236
|
+
return generateSimpleFunctionCall('_INTEGER')
|
|
237
|
+
case '_MOD':
|
|
238
|
+
// XMILE/Stella uses `MOD`, but it is the same as the Vensim `MODULO` function,
|
|
239
|
+
// which is the name used in the runtime function implementation
|
|
240
|
+
return generateSimpleFunctionCall('_MODULO')
|
|
241
|
+
case '_SAFEDIV':
|
|
242
|
+
// XMILE/Stella uses `SAFEDIV`, but it is the same as the Vensim `ZIDZ` function,
|
|
243
|
+
// which is the name used in the runtime function implementation
|
|
244
|
+
return generateSimpleFunctionCall('_ZIDZ')
|
|
217
245
|
|
|
218
246
|
//
|
|
219
247
|
//
|
|
@@ -225,17 +253,12 @@ function generateFunctionCall(callExpr, ctx) {
|
|
|
225
253
|
//
|
|
226
254
|
//
|
|
227
255
|
|
|
256
|
+
// Lookup functions supported by Vensim only
|
|
228
257
|
case '_GET_DATA_BETWEEN_TIMES':
|
|
229
258
|
case '_LOOKUP_BACKWARD':
|
|
230
259
|
case '_LOOKUP_FORWARD':
|
|
231
|
-
case '_LOOKUP_INVERT':
|
|
232
|
-
|
|
233
|
-
// a C/JS function call with a generated C/JS expression for each remaining argument.
|
|
234
|
-
const cVarRef = ctx.cVarRef(callExpr.args[0])
|
|
235
|
-
const cArgs = callExpr.args.slice(1).map(arg => generateExpr(arg, ctx))
|
|
236
|
-
return `${fnRef(fnId, ctx)}(${cVarRef}, ${cArgs.join(', ')})`
|
|
237
|
-
}
|
|
238
|
-
|
|
260
|
+
case '_LOOKUP_INVERT':
|
|
261
|
+
return generateLookupFunctionCall(fnId)
|
|
239
262
|
case '_GAME': {
|
|
240
263
|
// For the GAME function, emit a C/JS function call that has the synthesized game inputs lookup
|
|
241
264
|
// as the first argument, followed by the default value argument from the function call
|
|
@@ -244,6 +267,16 @@ function generateFunctionCall(callExpr, ctx) {
|
|
|
244
267
|
return `${fnRef(fnId, ctx)}(${cLookupArg}, ${cDefaultArg})`
|
|
245
268
|
}
|
|
246
269
|
|
|
270
|
+
// Lookup functions supported by XMILE/Stella only
|
|
271
|
+
case '_LOOKUP':
|
|
272
|
+
// XMILE/Stella has an explicit `LOOKUP` function while Vensim uses `x(y)` syntax, but
|
|
273
|
+
// underneath both are implemented at runtime by the `LOOKUP` function
|
|
274
|
+
return generateLookupFunctionCall('_LOOKUP')
|
|
275
|
+
case '_LOOKUPINV':
|
|
276
|
+
// XMILE/Stella uses `LOOKUPINV`, but it is the same as the Vensim `LOOKUP INVERT` function,
|
|
277
|
+
// which is the name used in the runtime function implementation
|
|
278
|
+
return generateLookupFunctionCall('_LOOKUP_INVERT')
|
|
279
|
+
|
|
247
280
|
//
|
|
248
281
|
//
|
|
249
282
|
// Level functions
|
|
@@ -251,12 +284,16 @@ function generateFunctionCall(callExpr, ctx) {
|
|
|
251
284
|
//
|
|
252
285
|
|
|
253
286
|
case '_ACTIVE_INITIAL':
|
|
287
|
+
case '_DELAY':
|
|
254
288
|
case '_DELAY_FIXED':
|
|
255
289
|
case '_DEPRECIATE_STRAIGHTLINE':
|
|
256
290
|
case '_SAMPLE_IF_TRUE':
|
|
257
291
|
case '_INTEG':
|
|
258
292
|
// Split level functions into init and eval expressions
|
|
259
|
-
if (
|
|
293
|
+
if (
|
|
294
|
+
ctx.outFormat === 'js' &&
|
|
295
|
+
(fnId === '_DELAY' || fnId === '_DELAY_FIXED' || fnId === '_DEPRECIATE_STRAIGHTLINE')
|
|
296
|
+
) {
|
|
260
297
|
throw new Error(`${callExpr.fnName} function not yet implemented for JS code gen`)
|
|
261
298
|
}
|
|
262
299
|
if (ctx.mode.startsWith('init')) {
|
|
@@ -311,7 +348,12 @@ function generateFunctionCall(callExpr, ctx) {
|
|
|
311
348
|
case '_SMOOTH':
|
|
312
349
|
case '_SMOOTHI':
|
|
313
350
|
case '_SMOOTH3':
|
|
314
|
-
case '_SMOOTH3I':
|
|
351
|
+
case '_SMOOTH3I':
|
|
352
|
+
case '_SMTH1':
|
|
353
|
+
case '_SMTH3': {
|
|
354
|
+
// Note that Vensim uses `SMOOTH[I]` and `SMOOTH3[I]` while XMILE uses `SMTH1` and
|
|
355
|
+
// `SMTH3`, but otherwise they have been translated the same way during the read
|
|
356
|
+
// equations phase
|
|
315
357
|
const smoothVar = Model.varWithRefId(ctx.variable.smoothVarRefId)
|
|
316
358
|
return ctx.cVarRef(smoothVar.parsedEqn.lhs.varDef)
|
|
317
359
|
}
|
|
@@ -345,11 +387,13 @@ function generateFunctionCall(callExpr, ctx) {
|
|
|
345
387
|
}
|
|
346
388
|
return generateAllocateAvailableCall(callExpr, ctx)
|
|
347
389
|
|
|
348
|
-
case '_ELMCOUNT':
|
|
349
|
-
|
|
390
|
+
case '_ELMCOUNT':
|
|
391
|
+
case '_SIZE': {
|
|
392
|
+
// Emit the size of the dimension in place of the dimension name. Note that Vensim uses
|
|
393
|
+
// `ELMCOUNT` while XMILE uses `SIZE`, but otherwise they are the same.
|
|
350
394
|
const dimArg = callExpr.args[0]
|
|
351
395
|
if (dimArg.kind !== 'variable-ref') {
|
|
352
|
-
throw new Error(
|
|
396
|
+
throw new Error(`Argument for ${callExpr.fnName} must be a dimension name`)
|
|
353
397
|
}
|
|
354
398
|
const dimId = dimArg.varId
|
|
355
399
|
return `${sub(dimId).size}`
|
|
@@ -362,7 +406,9 @@ function generateFunctionCall(callExpr, ctx) {
|
|
|
362
406
|
throw new Error(`Unexpected function '${fnId}' in code gen for '${ctx.variable.modelLHS}'`)
|
|
363
407
|
|
|
364
408
|
case '_INITIAL':
|
|
365
|
-
|
|
409
|
+
case '_INIT':
|
|
410
|
+
// Note that Vensim uses `INITIAL` while XMILE uses `INIT`, but otherwise they are the same.
|
|
411
|
+
// In init mode, only emit the initial expression without the INITIAL function call.
|
|
366
412
|
if (ctx.mode.startsWith('init')) {
|
|
367
413
|
return generateExpr(callExpr.args[0], ctx)
|
|
368
414
|
} else {
|
|
@@ -423,6 +469,7 @@ function generateLevelInit(callExpr, ctx) {
|
|
|
423
469
|
case '_INTEG':
|
|
424
470
|
initialArgIndex = 1
|
|
425
471
|
break
|
|
472
|
+
case '_DELAY':
|
|
426
473
|
case '_DELAY_FIXED': {
|
|
427
474
|
// Emit the code that initializes the `FixedDelay` support struct
|
|
428
475
|
const fixedDelay = ctx.cVarRefWithLhsSubscripts(ctx.variable.fixedDelayVarName)
|
|
@@ -474,12 +521,15 @@ function generateLevelEval(callExpr, ctx) {
|
|
|
474
521
|
// For ACTIVE INITIAL, emit the first arg without a function call
|
|
475
522
|
return generateExpr(callExpr.args[0], ctx)
|
|
476
523
|
|
|
524
|
+
case '_DELAY':
|
|
477
525
|
case '_DELAY_FIXED': {
|
|
478
|
-
//
|
|
526
|
+
// Stella's DELAY function is behaviorally equivalent to Vensim's DELAY FIXED function, so
|
|
527
|
+
// they use the same `_DELAY_FIXED` runtime function. For these, emit the first arg
|
|
528
|
+
// followed by the FixedDelay support var.
|
|
479
529
|
const args = []
|
|
480
530
|
args.push(generateExpr(callExpr.args[0], ctx))
|
|
481
531
|
args.push(ctx.cVarRefWithLhsSubscripts(ctx.variable.fixedDelayVarName))
|
|
482
|
-
return
|
|
532
|
+
return `${fnRef('_DELAY_FIXED', ctx)}(${args.join(', ')})`
|
|
483
533
|
}
|
|
484
534
|
|
|
485
535
|
case '_DEPRECIATE_STRAIGHTLINE': {
|
package/src/index.js
CHANGED
|
@@ -35,7 +35,15 @@ export function parseInlineVensimModel(mdlContent /*: string*/, modelDir /*?: st
|
|
|
35
35
|
// the preprocess step, and in the case of the new parser (which implicitly runs the
|
|
36
36
|
// preprocess step), don't sort the definitions. This makes it easier to do apples
|
|
37
37
|
// to apples comparisons on the outputs from the two parser implementations.
|
|
38
|
-
return parseModel(mdlContent, modelDir, { sort: false })
|
|
38
|
+
return parseModel(mdlContent, 'vensim', modelDir, { sort: false })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @hidden This is not yet part of the public API; it is exposed only for use
|
|
43
|
+
* in the experimental playground app.
|
|
44
|
+
*/
|
|
45
|
+
export function parseInlineXmileModel(mdlContent /*: string*/, modelDir /*?: string*/) /*: ParsedModel*/ {
|
|
46
|
+
return parseModel(mdlContent, 'xmile', modelDir)
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
/**
|
package/src/model/model.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as R from 'ramda'
|
|
2
2
|
|
|
3
|
+
import { canonicalVarId, toPrettyString } from '@sdeverywhere/parse'
|
|
4
|
+
|
|
3
5
|
import B from '../_shared/bufx.js'
|
|
4
|
-
import {
|
|
6
|
+
import { decanonicalize, isIterable, strlist, vlog, vsort } from '../_shared/helpers.js'
|
|
5
7
|
import {
|
|
6
8
|
addIndex,
|
|
7
9
|
allAliases,
|
|
@@ -15,7 +17,7 @@ import {
|
|
|
15
17
|
import { cName } from '../_shared/var-names.js'
|
|
16
18
|
|
|
17
19
|
import { expandVar } from './expand-var-instances.js'
|
|
18
|
-
import { readEquation } from './read-equations.js'
|
|
20
|
+
import { readEquation, resolveXmileDimensionWildcards } from './read-equations.js'
|
|
19
21
|
import { readDimensionDefs } from './read-subscripts.js'
|
|
20
22
|
import { readVariables } from './read-variables.js'
|
|
21
23
|
import { reduceVariables } from './reduce-variables.js'
|
|
@@ -92,10 +94,80 @@ function read(parsedModel, spec, extData, directData, modelDirname, opts) {
|
|
|
92
94
|
timeVar.varName = '_time'
|
|
93
95
|
vars.push(timeVar)
|
|
94
96
|
|
|
97
|
+
// Helper function to define a control variable for XMILE models
|
|
98
|
+
function defineXmileControlVar(varName, varId, rhsValue) {
|
|
99
|
+
let rhsExpr
|
|
100
|
+
if (typeof rhsValue === 'number') {
|
|
101
|
+
rhsExpr = {
|
|
102
|
+
kind: 'number',
|
|
103
|
+
value: rhsValue,
|
|
104
|
+
text: rhsValue.toString()
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
rhsExpr = {
|
|
108
|
+
kind: 'variable-ref',
|
|
109
|
+
varName: rhsValue,
|
|
110
|
+
varId: canonicalVarId(rhsValue)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const v = new Variable()
|
|
114
|
+
v.modelLHS = varName
|
|
115
|
+
v.varName = varId
|
|
116
|
+
v.parsedEqn = {
|
|
117
|
+
lhs: {
|
|
118
|
+
varDef: {
|
|
119
|
+
varName,
|
|
120
|
+
varId
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
rhs: {
|
|
124
|
+
kind: 'expr',
|
|
125
|
+
expr: rhsExpr
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
v.modelFormula = toPrettyString(rhsExpr, { compact: true })
|
|
129
|
+
v.includeInOutput = false
|
|
130
|
+
vars.push(v)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (parsedModel.kind === 'xmile') {
|
|
134
|
+
// XXX: Unlike Vensim models, XMILE models do not include the control parameters as
|
|
135
|
+
// normal model equations; instead, they are defined in the `<sim_specs>` element.
|
|
136
|
+
// In addition, XMILE allows these values to be accessed in equations (e.g., `<start>`
|
|
137
|
+
// can be accessed as `STARTTIME`, `<stop>` as `STOPTIME`, and `<dt>` as `DT`).
|
|
138
|
+
// For compatibility with the existing runtime code (which expects these variables
|
|
139
|
+
// to be defined using the Vensim names), we will synthesize variables using the
|
|
140
|
+
// Vensim names (e.g., `INITIAL TIME`) and also synthesize variables that derive
|
|
141
|
+
// from these using the XMILE names (e.g., `STARTTIME`).
|
|
142
|
+
defineXmileControlVar('INITIAL TIME', '_initial_time', parsedModel.root.simulationSpec.startTime)
|
|
143
|
+
defineXmileControlVar('FINAL TIME', '_final_time', parsedModel.root.simulationSpec.endTime)
|
|
144
|
+
defineXmileControlVar('TIME STEP', '_time_step', parsedModel.root.simulationSpec.timeStep)
|
|
145
|
+
defineXmileControlVar('STARTTIME', '_starttime', 'INITIAL TIME')
|
|
146
|
+
defineXmileControlVar('STOPTIME', '_stoptime', 'FINAL TIME')
|
|
147
|
+
defineXmileControlVar('DT', '_dt', 'TIME STEP')
|
|
148
|
+
// XXX: For now, also include a `SAVEPER` variable that is the same as `TIME STEP` (is there
|
|
149
|
+
// an equivalent of this in XMILE?)
|
|
150
|
+
defineXmileControlVar('SAVEPER', '_saveper', 'TIME STEP')
|
|
151
|
+
}
|
|
152
|
+
|
|
95
153
|
// Add the variables to the `Model`
|
|
96
154
|
vars.forEach(addVariable)
|
|
97
155
|
if (opts?.stopAfterReadVariables) return
|
|
98
156
|
|
|
157
|
+
if (parsedModel.kind === 'xmile') {
|
|
158
|
+
// XXX: In the case of XMILE, we need to resolve any wildcards used in dimension
|
|
159
|
+
// position in the RHS of the equation
|
|
160
|
+
for (const variable of vars) {
|
|
161
|
+
if (variable.parsedEqn?.rhs?.kind === 'expr') {
|
|
162
|
+
const updatedEqn = resolveXmileDimensionWildcards(variable)
|
|
163
|
+
if (updatedEqn) {
|
|
164
|
+
variable.parsedEqn = updatedEqn
|
|
165
|
+
variable.modelFormula = toPrettyString(updatedEqn.rhs.expr, { compact: true })
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
99
171
|
if (spec) {
|
|
100
172
|
// If the spec file contains `input/outputVarNames`, convert the full Vensim variable
|
|
101
173
|
// names to C names first so that later phases only need to work with canonical names
|
|
@@ -268,7 +340,7 @@ function resolveDimensions(dimensionFamilies) {
|
|
|
268
340
|
}
|
|
269
341
|
}
|
|
270
342
|
|
|
271
|
-
function analyze(
|
|
343
|
+
function analyze(modelKind, inputVars, opts) {
|
|
272
344
|
// Analyze the RHS of each equation in stages after all the variables are read.
|
|
273
345
|
// Find non-apply-to-all vars that are defined with more than one equation.
|
|
274
346
|
findNonAtoAVars()
|
|
@@ -284,7 +356,9 @@ function analyze(parsedModelKind, inputVars, opts) {
|
|
|
284
356
|
if (opts?.stopAfterReduceVariables === true) return
|
|
285
357
|
|
|
286
358
|
// Read the RHS to list the refIds of vars that are referenced and set the var type.
|
|
287
|
-
variables.forEach(
|
|
359
|
+
variables.forEach(v => {
|
|
360
|
+
readEquation(v, modelKind)
|
|
361
|
+
})
|
|
288
362
|
}
|
|
289
363
|
|
|
290
364
|
function checkSpecVars(spec) {
|
|
@@ -1221,7 +1295,7 @@ function jsonList() {
|
|
|
1221
1295
|
|
|
1222
1296
|
const varInstances = expandVar(v)
|
|
1223
1297
|
for (const { varName, subscriptIndices } of varInstances) {
|
|
1224
|
-
const varId =
|
|
1298
|
+
const varId = canonicalVarId(varName)
|
|
1225
1299
|
const varItem = {
|
|
1226
1300
|
varId,
|
|
1227
1301
|
varName,
|
|
@@ -16,10 +16,12 @@ import Model from './model.js'
|
|
|
16
16
|
/**
|
|
17
17
|
* Generate level and aux variables that implement one of the following `DELAY` function
|
|
18
18
|
* call variants:
|
|
19
|
-
* - DELAY1
|
|
20
|
-
* - DELAY1I
|
|
21
|
-
* - DELAY3
|
|
22
|
-
* - DELAY3I
|
|
19
|
+
* - DELAY1 (Vensim)
|
|
20
|
+
* - DELAY1I (Vensim)
|
|
21
|
+
* - DELAY3 (Vensim)
|
|
22
|
+
* - DELAY3I (Vensim)
|
|
23
|
+
* - DELAY1 (Stella)
|
|
24
|
+
* - DELAY3 (Stella)
|
|
23
25
|
*
|
|
24
26
|
* TODO: Docs
|
|
25
27
|
*
|