@sdeverywhere/compile 0.7.21 → 0.7.23

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdeverywhere/compile",
3
- "version": "0.7.21",
3
+ "version": "0.7.23",
4
4
  "description": "The core Vensim to C compiler for the SDEverywhere tool suite.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -153,11 +153,19 @@ ${chunkedFunctions('evalLevels', Model.levelVars(), ' // Evaluate levels.')}`
153
153
  let setLookupBody
154
154
  if (spec.customLookups === true || Array.isArray(spec.customLookups)) {
155
155
  setLookupBody = `\
156
+ Lookup** pLookup = NULL;
156
157
  switch (varIndex) {
157
158
  ${setLookupImpl(Model.varIndexInfo(), spec.customLookups)}
158
159
  default:
159
160
  fprintf(stderr, "No lookup found for var index %zu in setLookup\\n", varIndex);
160
161
  break;
162
+ }
163
+ if (pLookup != NULL) {
164
+ if (*pLookup == NULL) {
165
+ *pLookup = __new_lookup(numPoints, /*copy=*/true, points);
166
+ } else {
167
+ __set_lookup(*pLookup, numPoints, points);
168
+ }
161
169
  }`
162
170
  } else {
163
171
  let msg = 'The setLookup function was not enabled for the generated model. '
@@ -200,19 +208,6 @@ void setInputsFromBuffer(double* inputData) {
200
208
  ${inputsFromBufferImpl()}
201
209
  }
202
210
 
203
- void replaceLookup(Lookup** lookup, double* points, size_t numPoints) {
204
- if (lookup == NULL) {
205
- return;
206
- }
207
- if (*lookup != NULL) {
208
- __delete_lookup(*lookup);
209
- *lookup = NULL;
210
- }
211
- if (points != NULL) {
212
- *lookup = __new_lookup(numPoints, /*copy=*/true, points);
213
- }
214
- }
215
-
216
211
  void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPoints) {
217
212
  ${setLookupBody}
218
213
  }
@@ -441,7 +436,7 @@ ${section(chunk)}
441
436
  return inputVars.join('\n')
442
437
  }
443
438
  function setLookupImpl(varIndexInfo, customLookups) {
444
- // Emit `replaceLookup` calls for all lookups and data variables that can be overridden
439
+ // Emit case statements for all lookups and data variables that can be overridden
445
440
  // at runtime
446
441
  let includeCase
447
442
  if (Array.isArray(customLookups)) {
@@ -467,7 +462,7 @@ ${section(chunk)}
467
462
  }
468
463
  let c = ''
469
464
  c += ` case ${info.varIndex}:\n`
470
- c += ` replaceLookup(&${lookupVar}, points, numPoints);\n`
465
+ c += ` pLookup = &${lookupVar};\n`
471
466
  c += ` break;`
472
467
  return c
473
468
  })
@@ -251,10 +251,15 @@ ${chunkedFunctions('evalLevels', true, Model.levelVars(), ' // Evaluate levels'
251
251
  }
252
252
  const varIndex = varSpec.varIndex;
253
253
  const subs = varSpec.subscriptIndices;
254
+ let lookup;
254
255
  switch (varIndex) {
255
256
  ${setLookupImpl(Model.varIndexInfo(), spec.customLookups)}
256
257
  default:
257
258
  throw new Error(\`No lookup found for var index \${varIndex} in setLookup\`);
259
+ }
260
+ if (lookup) {
261
+ const size = points ? points.length / 2 : 0;
262
+ lookup.setData(size, points);
258
263
  }`
259
264
  } else {
260
265
  let msg = 'The setLookup function was not enabled for the generated model. '
@@ -307,7 +312,7 @@ ${customOutputSection(Model.varIndexInfo(), spec.customOutputs)}
307
312
  return `\
308
313
  /*export*/ function setInputs(valueAtIndex /*: (index: number) => number*/) {${inputsFromBufferImpl()}}
309
314
 
310
- /*export*/ function setLookup(varSpec /*: VarSpec*/, points /*: Float64Array*/) {
315
+ /*export*/ function setLookup(varSpec /*: VarSpec*/, points /*: Float64Array | undefined*/) {
311
316
  ${setLookupBody}
312
317
  }
313
318
 
@@ -517,7 +522,7 @@ ${section(chunk)}
517
522
  return inputVars
518
523
  }
519
524
  function setLookupImpl(varIndexInfo, customLookups) {
520
- // Emit `createLookup` calls for all lookups and data variables that can be overridden
525
+ // Emit case statements for all lookups and data variables that can be overridden
521
526
  // at runtime
522
527
  let overrideAllowed
523
528
  if (Array.isArray(customLookups)) {
@@ -543,7 +548,7 @@ ${section(chunk)}
543
548
  }
544
549
  let c = ''
545
550
  c += ` case ${info.varIndex}:\n`
546
- c += ` ${lookupVar} = fns.createLookup(points.length / 2, points);\n`
551
+ c += ` lookup = ${lookupVar};\n`
547
552
  c += ` break;`
548
553
  return c
549
554
  })
@@ -113,13 +113,12 @@ export function generateEquation(variable, mode, extData, directData, modelDir,
113
113
  // Apply special handling for lookup variables. The data for lookup variables is already
114
114
  // defined as a set of explicit data points (stored in the `Variable` instance).
115
115
  if (variable.isLookup()) {
116
- if (variable.varSubtype === 'gameInputs') {
117
- // For a synthesized game inputs lookup, there is no data array (the data is expected
118
- // to be supplied at runtime), so don't emit decl or init code for these
119
- return []
116
+ // Emit decl/init code for the lookup
117
+ const lookupDef = generateLookupFromPoints(variable, mode, /*copy=*/ false, cLhs, loopIndexVars, outFormat)
118
+ if (lookupDef.length > 0) {
119
+ return [...openLoops, ...lookupDef, ...closeLoops]
120
120
  } else {
121
- // For all other lookups, emit decl/init code for the lookup
122
- return generateLookupFromPoints(variable, mode, /*copy=*/ false, cLhs, loopIndexVars, outFormat)
121
+ return []
123
122
  }
124
123
  }
125
124
 
@@ -16,10 +16,23 @@ import { isDimension, isTrivialDimension, sub } from '../_shared/subscript.js'
16
16
  */
17
17
  export function generateLookupFromPoints(variable, mode, copy, varLhs, loopIndexVars, outFormat) {
18
18
  if (variable.points.length === 0) {
19
- throw new Error(`Empty lookup data array for ${variable.modelLHS}`)
20
- }
21
-
22
- if (copy) {
19
+ // In the case where the lookup data array is empty (typically this only occurs in the
20
+ // case of game inputs), generate a new empty lookup
21
+ if (mode === 'decl') {
22
+ // Nothing to emit in decl mode
23
+ return []
24
+ } else if (mode === 'init-lookups') {
25
+ // In init mode, generate a new empty lookup
26
+ switch (outFormat) {
27
+ case 'c':
28
+ return [` ${varLhs} = __new_lookup(0, /*copy=*/false, NULL);`]
29
+ case 'js':
30
+ return [` ${varLhs} = fns.createLookup(0, undefined);`]
31
+ default:
32
+ throw new Error(`Unhandled output format '${outFormat}'`)
33
+ }
34
+ }
35
+ } else if (copy) {
23
36
  // Inline the data points and copy them when initializing the lookup
24
37
  if (mode === 'decl') {
25
38
  // Nothing to emit in decl mode
@@ -71,6 +71,10 @@ function read(parsedModel, spec, extData, directData, modelDirname, opts) {
71
71
  // Some arrays need to be separated into variables with individual indices to
72
72
  // prevent eval cycles. They are manually added to the spec file.
73
73
  let specialSeparationDims = spec.specialSeparationDims
74
+ // All arrays that have one of the specified dimension ID lists are
75
+ // separated on those dimensions. This allows variables to be separated
76
+ // without listing each one.
77
+ let separateAllVarsWithDims = spec.separateAllVarsWithDims
74
78
 
75
79
  // Dimensions must be defined before reading variables that use them.
76
80
  readDimensionDefs(parsedModel, modelDirname)
@@ -79,7 +83,7 @@ function read(parsedModel, spec, extData, directData, modelDirname, opts) {
79
83
  if (opts?.stopAfterResolveSubscripts) return
80
84
 
81
85
  // Read variables from the model parse tree.
82
- const vars = readVariables(parsedModel, specialSeparationDims)
86
+ const vars = readVariables(parsedModel, specialSeparationDims, separateAllVarsWithDims)
83
87
 
84
88
  // Include a placeholder variable for the exogenous `Time` variable
85
89
  const timeVar = new Variable()
@@ -114,6 +114,9 @@ class Context {
114
114
  const parsedModel = { kind: 'vensim', root: parseVensimModel(eqnText) }
115
115
 
116
116
  // Create one or more `Variable` instances from the equations
117
+ // TODO: Technically we should pass `spec.separateAllVarsWithDims` here so that any
118
+ // generated apply-to-all variables will be separated according to the spec like other
119
+ // normal variables
117
120
  const vars = readVariables(parsedModel)
118
121
 
119
122
  // Add the variables to the `Model`
@@ -12,17 +12,21 @@ import Variable from './variable.js'
12
12
  *
13
13
  * @param {*} parsedModel TODO: Use ParsedVensimModel type here
14
14
  * @param {Object.<string, string>} [specialSeparationDims] The variable names that need to be
15
- * separated because of circular references. A mapping from "C" variable name to "C" dimension
16
- * name to separate on.
15
+ * separated because of circular references. This is an optional mapping from "C" variable name
16
+ * to "C" dimension name to separate on.
17
+ * @param {string[][]} [separateAllVarsWithDims] An optional array containing arrays of dimension
18
+ * identifiers for which variables will be separated. For example, if the LHS is `x[DimA,DimB,DimC]`
19
+ * and this array contains a list `['_dima', '_dimb']`, then the LHS will be separated on both
20
+ * DimA and DimB.
17
21
  * @returns {*} An array containing all `Variable` instances that were generated from
18
22
  * the model equations.
19
23
  */
20
- export function readVariables(parsedModel, specialSeparationDims) {
24
+ export function readVariables(parsedModel, specialSeparationDims, separateAllVarsWithDims) {
21
25
  const variables = []
22
26
 
23
27
  // Add one or more `Variable` definitions for each parsed equation
24
28
  for (const eqn of parsedModel.root.equations) {
25
- variables.push(...variablesForEquation(eqn, specialSeparationDims || {}))
29
+ variables.push(...variablesForEquation(eqn, specialSeparationDims || {}, separateAllVarsWithDims || []))
26
30
  }
27
31
 
28
32
  return variables
@@ -37,10 +41,12 @@ export function readVariables(parsedModel, specialSeparationDims) {
37
41
  * @param {*} eqn The parsed equation.
38
42
  * @param {Object.<string, string>} specialSeparationDims The variable names that need to be
39
43
  * separated because of circular references.
44
+ * @param {string[][]} separateAllVarsWithDims An array containing arrays of dimension
45
+ * identifiers for which variables will be separated.
40
46
  * @returns {*} An array containing all `Variable` instances that were generated from
41
47
  * the given equation.
42
48
  */
43
- function variablesForEquation(eqn, specialSeparationDims) {
49
+ function variablesForEquation(eqn, specialSeparationDims, separateAllVarsWithDims) {
44
50
  // Start a new variable defined by this equation
45
51
  const variable = new Variable()
46
52
 
@@ -115,6 +121,23 @@ function variablesForEquation(eqn, specialSeparationDims) {
115
121
  if (!Array.isArray(separationDims)) {
116
122
  separationDims = [separationDims]
117
123
  }
124
+ // Alternatively, if the variable was not in `specialSeparationDims`, separate
125
+ // on dims from `separateAllVarsWithDims` if the var matches one of the dim lists.
126
+ if (separationDims.length === 0) {
127
+ for (let dimList of separateAllVarsWithDims) {
128
+ // Technically we allow each entry in the spec array to be either a single
129
+ // dim ID string or an array of dim IDs, so convert to array if needed
130
+ if (!Array.isArray(dimList)) {
131
+ dimList = [dimList]
132
+ }
133
+ // The list entry from the spec is only considered a match if every dimension
134
+ // in the spec list appears on the LHS
135
+ if (dimList.every(dim => subIds.includes(dim))) {
136
+ separationDims = dimList
137
+ break
138
+ }
139
+ }
140
+ }
118
141
  positionsToExpand = subscriptPositionsToExpand(subIds, exceptSubIdSets, separationDims, variable.modelFormula)
119
142
  }
120
143
 
@@ -42,7 +42,7 @@ function toposort(nodes, edges) {
42
42
  } catch (e) {
43
43
  nodeRep = ''
44
44
  }
45
- throw new Error('Found cyclic dependency during toposort:\n' + [...predecessors].join(' →\n') + nodeRep)
45
+ throw new Error('Found cyclic dependency during toposort:\n' + [...predecessors].join(' →\n') + ' →' + nodeRep)
46
46
  }
47
47
 
48
48
  if (!nodesHash.has(node)) {