@sdeverywhere/compile 0.7.27 → 0.7.29
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/_shared/helpers.js +2 -2
- package/src/generate/direct-data-helpers.js +18 -6
- package/src/generate/gen-code.js +3 -2
- package/src/generate/gen-direct-const.js +5 -0
- package/src/generate/gen-equation.js +6 -0
- package/src/generate/gen-expr.js +91 -37
- package/src/generate/gen-lookup-from-direct.js +14 -7
- 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-expand.js +72 -0
- package/src/model/read-equations.js +572 -282
- package/src/model/read-variables.js +7 -1
- package/src/parse-and-generate.js +39 -27
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdeverywhere/compile",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.29",
|
|
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.4",
|
|
9
9
|
"byline": "^5.0.0",
|
|
10
10
|
"csv-parse": "^5.3.3",
|
|
11
11
|
"ramda": "^0.27.0",
|
package/src/_shared/helpers.js
CHANGED
|
@@ -127,10 +127,10 @@ export let listConcat = (a, x, addSpaces = false) => {
|
|
|
127
127
|
}
|
|
128
128
|
// Convert a number or string into a C double constant string.
|
|
129
129
|
// A blank string is converted to zero, following Excel.
|
|
130
|
-
// A string that cannot be converted throws an
|
|
130
|
+
// A string that cannot be converted throws an error.
|
|
131
131
|
export let cdbl = x => {
|
|
132
132
|
function throwError() {
|
|
133
|
-
throw new Error(`
|
|
133
|
+
throw new Error(`Cannot convert "${x}" to a number`)
|
|
134
134
|
}
|
|
135
135
|
let s = '0.0'
|
|
136
136
|
if (typeof x === 'number') {
|
|
@@ -48,7 +48,16 @@ function handleExcelWorkbook(fileOrTag, workbook, tab, dataKind, dataSource) {
|
|
|
48
48
|
if (sheet) {
|
|
49
49
|
return (c, r) => {
|
|
50
50
|
let cell = sheet[XLSX.utils.encode_cell({ c, r })]
|
|
51
|
-
|
|
51
|
+
if (cell == null || cell.v === '') {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return cdbl(cell.v)
|
|
56
|
+
} catch (_error) {
|
|
57
|
+
// Return null when the cell value cannot be converted to a number;
|
|
58
|
+
// the caller will treat this as the end of data.
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
52
61
|
}
|
|
53
62
|
} else {
|
|
54
63
|
throw new Error(`Direct ${dataKind} worksheet ${tab} in ${dataSource} ${fileOrTag} not found`)
|
|
@@ -72,13 +81,16 @@ function handleCsvFile(file, dataPathname, delimiter, dataKind) {
|
|
|
72
81
|
let data = readCsv(dataPathname, delimiter)
|
|
73
82
|
if (data) {
|
|
74
83
|
return (c, r) => {
|
|
75
|
-
|
|
84
|
+
if (data[r] == null || data[r][c] == null || data[r][c] === '') {
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
76
87
|
try {
|
|
77
|
-
|
|
78
|
-
} catch (
|
|
79
|
-
|
|
88
|
+
return cdbl(data[r][c])
|
|
89
|
+
} catch (_error) {
|
|
90
|
+
// Return null when the cell value cannot be converted to a number;
|
|
91
|
+
// the caller will treat this as the end of data.
|
|
92
|
+
return null
|
|
80
93
|
}
|
|
81
|
-
return value
|
|
82
94
|
}
|
|
83
95
|
} else {
|
|
84
96
|
throw new Error(`Direct ${dataKind} file ${file} could not be read`)
|
package/src/generate/gen-code.js
CHANGED
|
@@ -17,8 +17,9 @@ import { generateJS } from './gen-code-js.js'
|
|
|
17
17
|
* @param {Map<string, any>} opts.directData The mapping of dataset name used in a
|
|
18
18
|
* `GET DIRECT DATA` call (e.g., `?data`) to the tabular data contained in the loaded
|
|
19
19
|
* data file.
|
|
20
|
-
* @param {string} opts.modelDirname The path to the directory containing
|
|
21
|
-
* (
|
|
20
|
+
* @param {string} opts.modelDirname The absolute path to the directory containing data
|
|
21
|
+
* (dat, xlsx, csv) files that are referenced by the model. This path is used for
|
|
22
|
+
* resolving data files for `GET DIRECT SUBSCRIPT` calls.
|
|
22
23
|
* @returns A string containing the generated code.
|
|
23
24
|
*/
|
|
24
25
|
export function generateCode(parsedModel, opts) {
|
|
@@ -82,7 +82,12 @@ export function generateDirectConstInit(variable, directData, modelDir) {
|
|
|
82
82
|
for (let i = 0; i < cellOffsets.length; i++) {
|
|
83
83
|
let rowOffset = cellOffsets[i][0] ? cellOffsets[i][0] : 0
|
|
84
84
|
let colOffset = cellOffsets[i][1] ? cellOffsets[i][1] : 0
|
|
85
|
+
// Use 0.0 as a fallback when the cell is missing, empty, or contains a non-numeric value.
|
|
86
|
+
// (Vensim raises an error in this case, but SDE has historically tolerated invalid cells.)
|
|
85
87
|
let dataValue = getCellValue(startCol + colOffset, startRow + rowOffset)
|
|
88
|
+
if (dataValue == null) {
|
|
89
|
+
dataValue = '0.0'
|
|
90
|
+
}
|
|
86
91
|
let lhs = `${variable.varName}${lhsSubscripts[i] || ''}`
|
|
87
92
|
lines.push(` ${lhs} = ${dataValue};`)
|
|
88
93
|
}
|
|
@@ -116,6 +116,12 @@ export function generateEquation(variable, mode, extData, directData, modelDir,
|
|
|
116
116
|
// Emit decl/init code for the lookup
|
|
117
117
|
const lookupDef = generateLookupFromPoints(variable, mode, /*copy=*/ false, cLhs, loopIndexVars, outFormat)
|
|
118
118
|
if (lookupDef.length > 0) {
|
|
119
|
+
if (mode === 'decl') {
|
|
120
|
+
// When declaring a lookup, even if the lookup variable includes dimensions (i.e., is
|
|
121
|
+
// partially apply-to-all), the data variable declarations should not be inside for loops,
|
|
122
|
+
// so we omit them in this case
|
|
123
|
+
return [...lookupDef]
|
|
124
|
+
}
|
|
119
125
|
return [...openLoops, ...lookupDef, ...closeLoops]
|
|
120
126
|
} else {
|
|
121
127
|
return []
|
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,24 +387,31 @@ 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}`
|
|
356
400
|
}
|
|
357
401
|
|
|
358
402
|
case '_GET_DIRECT_CONSTANTS':
|
|
403
|
+
case '_GET_XLS_CONSTANTS':
|
|
359
404
|
case '_GET_DIRECT_DATA':
|
|
405
|
+
case '_GET_XLS_DATA':
|
|
360
406
|
case '_GET_DIRECT_LOOKUPS':
|
|
407
|
+
case '_GET_XLS_LOOKUPS':
|
|
361
408
|
// These functions are handled at a higher level, so we should not get here
|
|
362
409
|
throw new Error(`Unexpected function '${fnId}' in code gen for '${ctx.variable.modelLHS}'`)
|
|
363
410
|
|
|
364
411
|
case '_INITIAL':
|
|
365
|
-
|
|
412
|
+
case '_INIT':
|
|
413
|
+
// Note that Vensim uses `INITIAL` while XMILE uses `INIT`, but otherwise they are the same.
|
|
414
|
+
// In init mode, only emit the initial expression without the INITIAL function call.
|
|
366
415
|
if (ctx.mode.startsWith('init')) {
|
|
367
416
|
return generateExpr(callExpr.args[0], ctx)
|
|
368
417
|
} else {
|
|
@@ -423,6 +472,7 @@ function generateLevelInit(callExpr, ctx) {
|
|
|
423
472
|
case '_INTEG':
|
|
424
473
|
initialArgIndex = 1
|
|
425
474
|
break
|
|
475
|
+
case '_DELAY':
|
|
426
476
|
case '_DELAY_FIXED': {
|
|
427
477
|
// Emit the code that initializes the `FixedDelay` support struct
|
|
428
478
|
const fixedDelay = ctx.cVarRefWithLhsSubscripts(ctx.variable.fixedDelayVarName)
|
|
@@ -474,12 +524,15 @@ function generateLevelEval(callExpr, ctx) {
|
|
|
474
524
|
// For ACTIVE INITIAL, emit the first arg without a function call
|
|
475
525
|
return generateExpr(callExpr.args[0], ctx)
|
|
476
526
|
|
|
527
|
+
case '_DELAY':
|
|
477
528
|
case '_DELAY_FIXED': {
|
|
478
|
-
//
|
|
529
|
+
// Stella's DELAY function is behaviorally equivalent to Vensim's DELAY FIXED function, so
|
|
530
|
+
// they use the same `_DELAY_FIXED` runtime function. For these, emit the first arg
|
|
531
|
+
// followed by the FixedDelay support var.
|
|
479
532
|
const args = []
|
|
480
533
|
args.push(generateExpr(callExpr.args[0], ctx))
|
|
481
534
|
args.push(ctx.cVarRefWithLhsSubscripts(ctx.variable.fixedDelayVarName))
|
|
482
|
-
return
|
|
535
|
+
return `${fnRef('_DELAY_FIXED', ctx)}(${args.join(', ')})`
|
|
483
536
|
}
|
|
484
537
|
|
|
485
538
|
case '_DEPRECIATE_STRAIGHTLINE': {
|
|
@@ -920,6 +973,7 @@ function visitVariableRefs(expr, onVarRef) {
|
|
|
920
973
|
break
|
|
921
974
|
|
|
922
975
|
case 'lookup-call':
|
|
976
|
+
visitVariableRefs(expr.varRef, onVarRef)
|
|
923
977
|
visitVariableRefs(expr.arg, onVarRef)
|
|
924
978
|
break
|
|
925
979
|
|
|
@@ -89,14 +89,21 @@ function generateDirectDataLookup(varLhs, getCellValue, timeRowOrCol, startCell,
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
// Read time/value pairs, matching Vensim's behavior:
|
|
93
|
+
// - Stop reading when the first non-numeric time value is encountered. This
|
|
94
|
+
// allows additional content (e.g., labels) to follow the data in the row or column.
|
|
95
|
+
// - Skip pairs with a non-numeric data value, but continue reading subsequent pairs.
|
|
96
|
+
while (true) {
|
|
97
|
+
const timeValue = getCellValue(timeCol, timeRow)
|
|
98
|
+
if (timeValue == null) {
|
|
99
|
+
break
|
|
100
|
+
}
|
|
101
|
+
const dataValue = getCellValue(dataCol, dataRow)
|
|
102
|
+
if (dataValue != null) {
|
|
103
|
+
lookupData = listConcat(lookupData, `${timeValue}, ${dataValue}`, true)
|
|
104
|
+
lookupSize++
|
|
105
|
+
}
|
|
97
106
|
nextCell()
|
|
98
|
-
dataValue = getCellValue(dataCol, dataRow)
|
|
99
|
-
timeValue = getCellValue(timeCol, timeRow)
|
|
100
107
|
}
|
|
101
108
|
if (lookupSize === 0) {
|
|
102
109
|
throw new Error(`Empty lookup data array for ${varLhs}`)
|
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
|
*
|
|
@@ -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 `
|
|
6
|
+
* Generate two level variables and one aux that implement an `TREND` function call.
|
|
7
7
|
*
|
|
8
8
|
* TODO: Docs
|
|
9
9
|
*
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { indexNamesForSubscript } from '../_shared/subscript.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Given the array of LHS subscript/dimension IDs (already mapped to correspond to the
|
|
5
|
+
* RHS positions) and a set of RHS variable instances, return the refIds of the RHS
|
|
6
|
+
* instances whose subscript combinations overlap with the LHS combinations at every
|
|
7
|
+
* position.
|
|
8
|
+
*
|
|
9
|
+
* Conceptually, this is equivalent to checking whether any combination in the LHS
|
|
10
|
+
* cartesian product matches any combination in a given RHS instance's cartesian
|
|
11
|
+
* product. But because positions in a cartesian product are independent, we only
|
|
12
|
+
* need to check that each position has at least one index in common between the
|
|
13
|
+
* LHS and RHS index sets. This reduces the complexity of the check from
|
|
14
|
+
* O(product of dimension sizes) to O(sum of dimension sizes).
|
|
15
|
+
*
|
|
16
|
+
* For example, suppose DimA={A1,A2} and DimB={B1,B2}, the LHS accesses `[DimA,DimB]`,
|
|
17
|
+
* and we want to check whether it overlaps with a RHS variable instance `_x[_dima,_b1]`.
|
|
18
|
+
* The full cartesian products look like this:
|
|
19
|
+
* LHS combos: { (A1,B1), (A1,B2), (A2,B1), (A2,B2) }
|
|
20
|
+
* RHS combos: { (A1,B1), (A2,B1) }
|
|
21
|
+
* The two sets share (A1,B1) and (A2,B1), so there is a match. But we don't need
|
|
22
|
+
* to enumerate either set — we can check each position independently:
|
|
23
|
+
* position 0: LHS {A1,A2} ∩ RHS {A1,A2} = {A1,A2} (non-empty)
|
|
24
|
+
* position 1: LHS {B1,B2} ∩ RHS {B1} = {B1} (non-empty)
|
|
25
|
+
* Every position has at least one index in common, so we know a full-combo match
|
|
26
|
+
* must exist (pick any shared index at each position, e.g., (A2,B1), and it is in
|
|
27
|
+
* both products). Conversely, if any position has an empty intersection, no full
|
|
28
|
+
* combo can match — for example, if instead the LHS accessed `[A1,DimB]` (a specific
|
|
29
|
+
* index at position 0) and the RHS instance were `_x[_a2,_dimb]`, position 0 would
|
|
30
|
+
* give LHS {A1} ∩ RHS {A2} = ∅ and we could stop immediately.
|
|
31
|
+
*
|
|
32
|
+
* @param {string[]} mappedLhsSubIds The array of LHS subscript/dimension IDs at each
|
|
33
|
+
* position, mapped to correspond to the RHS variable reference positions.
|
|
34
|
+
* @param {{ subscripts: string[], refId: string }[]} rhsVarInstances The array of RHS
|
|
35
|
+
* variable instances to filter.
|
|
36
|
+
* @returns {string[]} A sorted array of refIds for the RHS instances whose subscripts
|
|
37
|
+
* overlap with the LHS at every position.
|
|
38
|
+
*/
|
|
39
|
+
export function matchingRhsRefIds(mappedLhsSubIds, rhsVarInstances) {
|
|
40
|
+
// Build a Set of LHS index names for each position for quick lookup
|
|
41
|
+
const lhsIndexSets = mappedLhsSubIds.map(id => new Set(indexNamesForSubscript(id)))
|
|
42
|
+
|
|
43
|
+
// For each RHS variable instance, check if there is overlap at every subscript
|
|
44
|
+
// position between the LHS and RHS index sets
|
|
45
|
+
const rhsRefIds = []
|
|
46
|
+
for (const rhsVarInstance of rhsVarInstances) {
|
|
47
|
+
let matches = true
|
|
48
|
+
for (let i = 0; i < rhsVarInstance.subscripts.length; i++) {
|
|
49
|
+
const rhsIndices = indexNamesForSubscript(rhsVarInstance.subscripts[i])
|
|
50
|
+
let hasOverlap = false
|
|
51
|
+
for (const id of rhsIndices) {
|
|
52
|
+
if (lhsIndexSets[i].has(id)) {
|
|
53
|
+
hasOverlap = true
|
|
54
|
+
break
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!hasOverlap) {
|
|
58
|
+
matches = false
|
|
59
|
+
break
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (matches) {
|
|
63
|
+
rhsRefIds.push(rhsVarInstance.refId)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Return the sorted array of relevant refIds
|
|
68
|
+
// TODO: Sorting is not essential here, but the legacy reader sorted so we will keep
|
|
69
|
+
// that behavior now to avoid invalidating tests. Later we should remove this `sort`
|
|
70
|
+
// call and update the tests accordingly.
|
|
71
|
+
return rhsRefIds.sort()
|
|
72
|
+
}
|