@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 CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@sdeverywhere/compile",
3
- "version": "0.7.27",
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.2",
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",
@@ -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 exception.
130
+ // A string that cannot be converted throws an error.
131
131
  export let cdbl = x => {
132
132
  function throwError() {
133
- throw new Error(`ERROR: cannot convert "${x}" to a number`)
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
- return cell != null ? cdbl(cell.v) : null
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
- let value = '0.0'
84
+ if (data[r] == null || data[r][c] == null || data[r][c] === '') {
85
+ return null
86
+ }
76
87
  try {
77
- value = data[r] != null && data[r][c] != null ? cdbl(data[r][c]) : null
78
- } catch (error) {
79
- console.error(`${error.message} in ${dataPathname}`)
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`)
@@ -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 the model
21
- * (used for resolving data files for `GET DIRECT SUBSCRIPT`).
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 []
@@ -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,24 +387,31 @@ 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}`
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
- // In init mode, only emit the initial expression without the INITIAL function call
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
- // For DELAY FIXED, emit the first arg followed by the FixedDelay support var
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 generateCall(args)
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
- let timeValue = getCellValue(timeCol, timeRow)
93
- let dataValue = getCellValue(dataCol, dataRow)
94
- while (timeValue != null && dataValue != null) {
95
- lookupData = listConcat(lookupData, `${timeValue}, ${dataValue}`, true)
96
- lookupSize++
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
  /**
@@ -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
  *
@@ -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
+ }