@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 CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@sdeverywhere/compile",
3
- "version": "0.7.26",
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.2",
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(const char* inputData) {
204
- ${inputsFromStringImpl()}
221
+ void setInputs(double* inputValues, int32_t* inputIndices) {
222
+ ${setInputsImpl()}
205
223
  }
206
224
 
207
- void setInputsFromBuffer(double* inputData) {
208
- ${inputsFromBufferImpl()}
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 decls
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
- decls = `const int numOutputs = ${expandedVarNames().length};`
351
+ numOutputsDecl = `const int numOutputs = ${expandedVarNames().length};`
328
352
  } else {
329
- decls = `const int numOutputs = ${spec.outputVars.length};`
353
+ numOutputsDecl = `const int numOutputs = ${spec.outputVars.length};`
330
354
  }
331
- return decls
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 inputsFromStringImpl() {
406
- // If there was an I/O spec file, then emit code to parse input variables.
407
- // The user can replace this with a parser for a different serialization format.
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
- token = strtok(NULL, " ");
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
- return inputVars
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 inputsFromBufferImpl() {
429
- let inputVars = []
430
- if (spec.inputVars && spec.inputVars.length > 0) {
431
- for (let i = 0; i < spec.inputVars.length; i++) {
432
- const inputVar = spec.inputVars[i]
433
- inputVars.push(` ${inputVar} = inputData[${i}];`)
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
- return inputVars.join('\n')
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,
@@ -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
- const fnId = callExpr.fnId
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
- if (ctx.outFormat === 'js' && fnId === '_IF_THEN_ELSE') {
208
- // When generating conditional expressions for JS target, since we can't rely on macros like we do for C,
209
- // it is better to translate it into a ternary instead of relying on a built-in function (since the latter
210
- // would require always evaluating both branches, while the former can be more optimized by the interpreter)
211
- return `((${args[0]}) ? (${args[1]}) : (${args[2]}))`
212
- } else {
213
- // For simple functions, emit a C/JS function call with a generated C/JS expression for each argument
214
- return `${fnRef(fnId, ctx)}(${args.join(', ')})`
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
- // For LOOKUP* functions, the first argument must be a reference to the lookup variable. Emit
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 (ctx.outFormat === 'js' && (fnId === '_DELAY_FIXED' || fnId === '_DEPRECIATE_STRAIGHTLINE')) {
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
- // Emit the size of the dimension in place of the dimension name
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('Argument for ELMCOUNT must be a dimension name')
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
- // In init mode, only emit the initial expression without the INITIAL function call
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
- // For DELAY FIXED, emit the first arg followed by the FixedDelay support var
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 generateCall(args)
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
  /**
@@ -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 { canonicalVensimName, decanonicalize, isIterable, strlist, vlog, vsort } from '../_shared/helpers.js'
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(parsedModelKind, inputVars, opts) {
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(readEquation)
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 = canonicalVensimName(varName)
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
  *