@sdeverywhere/compile 0.7.27 → 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.27",
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",
@@ -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
  *
@@ -15,10 +15,12 @@ import Model from './model.js'
15
15
  /**
16
16
  * Generate level and aux variables that implement one of the following `SMOOTH` function
17
17
  * call variants:
18
- * - SMOOTH
19
- * - SMOOTHI
20
- * - SMOOTH3
21
- * - SMOOTH3I
18
+ * - SMOOTH (Vensim)
19
+ * - SMOOTHI (Vensim)
20
+ * - SMOOTH3 (Vensim)
21
+ * - SMOOTH3I (Vensim)
22
+ * - SMTH1 (Stella)
23
+ * - SMTH3 (Stella)
22
24
  *
23
25
  * TODO: Docs
24
26
  *
@@ -43,7 +45,7 @@ export function generateSmoothVariables(v, callExpr, context) {
43
45
  }
44
46
 
45
47
  const fnId = callExpr.fnId
46
- if (fnId === '_SMOOTH' || fnId === '_SMOOTHI') {
48
+ if (fnId === '_SMOOTH' || fnId === '_SMOOTHI' || fnId === '_SMTH1') {
47
49
  // Generate 1 level variable that will replace the `SMOOTH[I]` function call
48
50
  const level = generateSmoothLevel(v, context, argInput, argDelay, argInit, 1)
49
51
  // For `SMOOTH[I]`, the smoothVarRefId is the level var's refId
@@ -3,7 +3,7 @@ import { toPrettyString } from '@sdeverywhere/parse'
3
3
  import { canonicalName, newAuxVarName, newLevelVarName } from '../_shared/helpers.js'
4
4
 
5
5
  /**
6
- * Generate two level variables and one aux that implement an `NPV` function call.
6
+ * Generate two level variables and one aux that implement an `TREND` function call.
7
7
  *
8
8
  * TODO: Docs
9
9
  *