@sdeverywhere/compile 0.7.16 → 0.7.18

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.
@@ -10,11 +10,12 @@ import { pointsString } from './gen-lookup-from-points.js'
10
10
  * @param {*} variable The `Variable` instance to process.
11
11
  * @param {'decl' | 'init-lookups'} mode The code generation mode.
12
12
  * @param {Map<string, any>} extData The map of datasets from external `.dat` files.
13
- * @param {string} varLhs The C code for the LHS variable reference.
14
- * @return {string[]} An array of strings containing the generated C code for the variable,
13
+ * @param {string} varLhs The C/JS code for the LHS variable reference.
14
+ * @param {'c' | 'js'} outFormat The output format.
15
+ * @return {string[]} An array of strings containing the generated C/JS code for the variable,
15
16
  * one string per line of code.
16
17
  */
17
- export function generateLookupsFromExternalData(variable, mode, extData, varLhs) {
18
+ export function generateLookupsFromExternalData(variable, mode, extData, varLhs, outFormat) {
18
19
  if (mode !== 'decl' && mode !== 'init-lookups') {
19
20
  throw new Error(`Invalid code gen mode '${mode}' for data variable ${variable.modelLHS}`)
20
21
  }
@@ -37,10 +38,24 @@ export function generateLookupsFromExternalData(variable, mode, extData, varLhs)
37
38
  // In decl mode, declare a static data array that will be used to create the associated `Lookup`
38
39
  // at init time
39
40
  const points = pointsString(Array.from(data.entries()))
40
- return `double ${dataName}[${data.size * 2}] = { ${points} };`
41
+ switch (outFormat) {
42
+ case 'c':
43
+ return `double ${dataName}[${data.size * 2}] = { ${points} };`
44
+ case 'js':
45
+ return `const ${dataName} = [${points}];`
46
+ default:
47
+ throw new Error(`Unhandled output format '${outFormat}'`)
48
+ }
41
49
  } else if (mode === 'init-lookups') {
42
50
  // In init mode, create the `Lookup`, passing in a pointer to the static data array declared in decl mode.
43
- return ` ${lhs} = __new_lookup(${data.size}, /*copy=*/false, ${dataName});`
51
+ switch (outFormat) {
52
+ case 'c':
53
+ return ` ${lhs} = __new_lookup(${data.size}, /*copy=*/false, ${dataName});`
54
+ case 'js':
55
+ return ` ${lhs} = fns.createLookup(${data.size}, ${dataName});`
56
+ default:
57
+ throw new Error(`Unhandled output format '${outFormat}'`)
58
+ }
44
59
  } else {
45
60
  return []
46
61
  }
@@ -8,12 +8,13 @@ import { isDimension, isTrivialDimension, sub } from '../_shared/subscript.js'
8
8
  * @param {'decl' | 'init-lookups'} mode The code generation mode.
9
9
  * @param {boolean} copy If false, a static data array will be used (good for larger data sets).
10
10
  * If true, the data will inlined and copied when initializing the lookup (good for smaller data sets).
11
- * @param {string} varLhs The C code for the LHS variable reference.
11
+ * @param {string} varLhs The C/JS code for the LHS variable reference.
12
12
  * @param {LoopIndexVars} loopIndexVars The loop index state.
13
- * @return {string[]} An array of strings containing the generated C code for the variable,
13
+ * @param {'c' | 'js'} outFormat The output format.
14
+ * @return {string[]} An array of strings containing the generated C/JS code for the variable,
14
15
  * one string per line of code.
15
16
  */
16
- export function generateLookupFromPoints(variable, mode, copy, varLhs, loopIndexVars) {
17
+ export function generateLookupFromPoints(variable, mode, copy, varLhs, loopIndexVars, outFormat) {
17
18
  if (variable.points.length === 0) {
18
19
  throw new Error(`Empty lookup data array for ${variable.modelLHS}`)
19
20
  }
@@ -26,7 +27,14 @@ export function generateLookupFromPoints(variable, mode, copy, varLhs, loopIndex
26
27
  } else if (mode === 'init-lookups') {
27
28
  // In init mode, generate a new lookup using the data points from the variable
28
29
  const points = pointsString(variable.points)
29
- return [` ${varLhs} = __new_lookup(${variable.points.length}, /*copy=*/true, (double[]){ ${points} });`]
30
+ switch (outFormat) {
31
+ case 'c':
32
+ return [` ${varLhs} = __new_lookup(${variable.points.length}, /*copy=*/true, (double[]){ ${points} });`]
33
+ case 'js':
34
+ return [` ${varLhs} = fns.createLookup(${variable.points.length}, [${points}]);`]
35
+ default:
36
+ throw new Error(`Unhandled output format '${outFormat}'`)
37
+ }
30
38
  }
31
39
  } else {
32
40
  // Construct the name of the data array, which is based on the associated lookup var name,
@@ -38,11 +46,25 @@ export function generateLookupFromPoints(variable, mode, copy, varLhs, loopIndex
38
46
  // the data in memory, and seems to perform much better when compiled to wasm when compared to the
39
47
  // previous approach that used varargs + copying, especially on constrained (e.g. iOS) devices.
40
48
  const points = pointsString(variable.points)
41
- return [`double ${dataName}[${variable.points.length * 2}] = { ${points} };`]
49
+ switch (outFormat) {
50
+ case 'c':
51
+ return [`double ${dataName}[${variable.points.length * 2}] = { ${points} };`]
52
+ case 'js':
53
+ return [`const ${dataName} = [${points}];`]
54
+ default:
55
+ throw new Error(`Unhandled output format '${outFormat}'`)
56
+ }
42
57
  } else if (mode === 'init-lookups') {
43
58
  // In init mode, create the `Lookup`, passing in a pointer to the static data array declared earlier.
44
59
  // TODO: Make use of the lookup range
45
- return [` ${varLhs} = __new_lookup(${variable.points.length}, /*copy=*/false, ${dataName});`]
60
+ switch (outFormat) {
61
+ case 'c':
62
+ return [` ${varLhs} = __new_lookup(${variable.points.length}, /*copy=*/false, ${dataName});`]
63
+ case 'js':
64
+ return [` ${varLhs} = fns.createLookup(${variable.points.length}, ${dataName});`]
65
+ default:
66
+ throw new Error(`Unhandled output format '${outFormat}'`)
67
+ }
46
68
  }
47
69
  }
48
70
 
package/src/index.js CHANGED
@@ -3,6 +3,41 @@
3
3
  export { canonicalName } from './_shared/helpers.js'
4
4
  export { readDat } from './_shared/read-dat.js'
5
5
  export { preprocessModel } from './preprocess/preprocessor.js'
6
- export { parseModel } from './parse/parser.js'
7
- export { generateCode } from './generate/code-gen.js'
8
- export { parseAndGenerate, printNames } from './parse-and-generate.js'
6
+ export { generateCode } from './generate/gen-code.js'
7
+ export { parseAndGenerate, parseModel, printNames } from './parse-and-generate.js'
8
+
9
+ import { resetHelperState } from './_shared/helpers.js'
10
+ import { resetSubscriptsAndDimensions } from './_shared/subscript.js'
11
+ import Model from './model/model.js'
12
+ import { parseModel } from './parse-and-generate.js'
13
+
14
+ /**
15
+ * @hidden This is not yet part of the public API; it is exposed only for use
16
+ * in the experimental playground app.
17
+ */
18
+ export function resetState() {
19
+ // XXX: These steps are needed due to subs/dims and variables being in module-level storage
20
+ resetHelperState()
21
+ resetSubscriptsAndDimensions()
22
+ Model.resetModelState()
23
+ }
24
+
25
+ /**
26
+ * @hidden This is not yet part of the public API; it is exposed only for use
27
+ * in the experimental playground app.
28
+ */
29
+ export function parseInlineVensimModel(mdlContent /*: string*/, modelDir /*?: string*/) /*: ParsedModel*/ {
30
+ // For tests that parse inline model text, in the case of the legacy parser, don't run
31
+ // the preprocess step, and in the case of the new parser (which implicitly runs the
32
+ // preprocess step), don't sort the definitions. This makes it easier to do apples
33
+ // to apples comparisons on the outputs from the two parser implementations.
34
+ return parseModel(mdlContent, modelDir, { sort: false })
35
+ }
36
+
37
+ /**
38
+ * @hidden This is not yet part of the public API; it is exposed only for use
39
+ * in the experimental playground app.
40
+ */
41
+ export function getModelListing() /*: string*/ {
42
+ return Model.jsonList()
43
+ }
@@ -2,7 +2,7 @@ import B from 'bufx'
2
2
  import yaml from 'js-yaml'
3
3
  import * as R from 'ramda'
4
4
 
5
- import { canonicalName, decanonicalize, isIterable, listConcat, strlist, vlog, vsort } from '../_shared/helpers.js'
5
+ import { canonicalName, decanonicalize, isIterable, /*listConcat,*/ strlist, vlog, vsort } from '../_shared/helpers.js'
6
6
  import {
7
7
  addIndex,
8
8
  allAliases,
@@ -14,22 +14,19 @@ import {
14
14
  sub,
15
15
  subscriptFamilies
16
16
  } from '../_shared/subscript.js'
17
- import { createParser } from '../parse/parser.js'
18
17
 
19
- import EquationReader from './equation-reader.js'
20
18
  import { readEquation } from './read-equations.js'
21
19
  import { readDimensionDefs } from './read-subscripts.js'
22
- import { readVariables as readVariables2 } from './read-variables.js'
20
+ import { readVariables } from './read-variables.js'
23
21
  import { reduceVariables } from './reduce-variables.js'
24
- import SubscriptRangeReader from './subscript-range-reader.js'
25
22
  import toposort from './toposort.js'
26
- import VarNameReader from './var-name-reader.js'
27
23
  import Variable from './variable.js'
28
- import VariableReader from './variable-reader.js'
29
24
 
30
25
  let variables = []
31
26
  let inputVars = []
32
27
  let constantExprs = new Map()
28
+ let cachedVarIndexInfo
29
+ let cachedJsonList
33
30
 
34
31
  // Also keep variables in a map (with `varName` as key) for faster lookup
35
32
  const variablesByName = new Map()
@@ -49,6 +46,8 @@ function resetModelState() {
49
46
  variablesByName.clear()
50
47
  constantExprs.clear()
51
48
  nonAtoANames = Object.create(null)
49
+ cachedVarIndexInfo = undefined
50
+ cachedJsonList = undefined
52
51
  }
53
52
 
54
53
  /**
@@ -73,38 +72,27 @@ function read(parsedModel, spec, extData, directData, modelDirname, opts) {
73
72
  let specialSeparationDims = spec.specialSeparationDims
74
73
 
75
74
  // Dimensions must be defined before reading variables that use them.
76
- if (parsedModel.kind === 'vensim-legacy') {
77
- readSubscriptRanges(parsedModel.parseTree, modelDirname)
78
- } else {
79
- readDimensionDefs(parsedModel, modelDirname)
80
- }
75
+ readDimensionDefs(parsedModel, modelDirname)
81
76
  if (opts?.stopAfterReadSubscripts) return
82
77
  resolveDimensions(spec.dimensionFamilies)
83
78
  if (opts?.stopAfterResolveSubscripts) return
84
79
 
85
80
  // Read variables from the model parse tree.
86
- if (parsedModel.kind === 'vensim-legacy') {
87
- // TODO: directData is actually unused in VariableReader
88
- readVariables(parsedModel.parseTree, specialSeparationDims, directData)
89
- } else {
90
- // Read the variables
91
- const vars = readVariables2(parsedModel, specialSeparationDims)
81
+ const vars = readVariables(parsedModel, specialSeparationDims)
92
82
 
93
- // Include a placeholder variable for the exogenous `Time` variable
94
- const timeVar = new Variable(null)
95
- timeVar.modelLHS = 'Time'
96
- timeVar.varName = '_time'
97
- vars.push(timeVar)
83
+ // Include a placeholder variable for the exogenous `Time` variable
84
+ const timeVar = new Variable()
85
+ timeVar.modelLHS = 'Time'
86
+ timeVar.varName = '_time'
87
+ vars.push(timeVar)
98
88
 
99
- // Add the variables to the `Model`
100
- vars.forEach(addVariable)
101
- }
89
+ // Add the variables to the `Model`
90
+ vars.forEach(addVariable)
102
91
  if (opts?.stopAfterReadVariables) return
103
92
 
104
93
  if (spec) {
105
- // If the spec file contains `input/outputVarNames` (with full Vensim variable names)
106
- // convert those to C names first. Otherwise, use `input/outputNames` which are already
107
- // assumed to be valid C names.
94
+ // If the spec file contains `input/outputVarNames`, convert the full Vensim variable
95
+ // names to C names first so that later phases only need to work with canonical names
108
96
  if (spec.inputVarNames) {
109
97
  spec.inputVars = R.map(cName, spec.inputVarNames)
110
98
  }
@@ -122,7 +110,7 @@ function read(parsedModel, spec, extData, directData, modelDirname, opts) {
122
110
  if (opts?.stopAfterAnalyze) return
123
111
 
124
112
  // Check that all input and output vars in the spec actually exist in the model.
125
- checkSpecVars(spec, extData)
113
+ checkSpecVars(spec)
126
114
 
127
115
  // Remove variables that are not referenced by an input or output variable.
128
116
  removeUnusedVariables(spec)
@@ -131,22 +119,6 @@ function read(parsedModel, spec, extData, directData, modelDirname, opts) {
131
119
  resolveDuplicateDeclarations()
132
120
  }
133
121
 
134
- /**
135
- * Read subscript ranges from the given model.
136
- *
137
- * Note that this function currently does not return anything and instead stores the parsed subscript
138
- * range definitions in the `subscript` module.
139
- *
140
- * @param {import('../parse/parser.js').VensimModelParseTree} parseTree The Vensim parse tree.
141
- * @param {string} modelDirname The path to the directory containing the model (used for resolving data
142
- * files for `GET DIRECT SUBSCRIPT`).
143
- */
144
- function readSubscriptRanges(parseTree, modelDirname) {
145
- // Read subscript ranges from the model.
146
- let subscriptRangeReader = new SubscriptRangeReader(modelDirname)
147
- subscriptRangeReader.visitModel(parseTree)
148
- }
149
-
150
122
  /**
151
123
  * Process the previously read subscript/dimension definitions (stored in the `subscript` module) to
152
124
  * resolve aliases, families, and indices.
@@ -290,32 +262,6 @@ function resolveDimensions(dimensionFamilies) {
290
262
  }
291
263
  }
292
264
 
293
- /**
294
- * Read equations from the given model and generate `Variable` instances for all variables that
295
- * are encountered while parsing.
296
- *
297
- * Note that this function currently does not return anything and instead stores the parsed
298
- * variable definitions in the `model` module.
299
- *
300
- * @param {import('../parse/parser.js').VensimModelParseTree} tree The Vensim parse tree.
301
- * @param {Object.<string, string>} specialSeparationDims The variable names that need to be
302
- * separated because of circular references. A mapping from "C" variable name to "C" dimension
303
- * name to separate on.
304
- * @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT DATA`
305
- * call (e.g., `?data`) to the tabular data contained in the loaded data file.
306
- */
307
- function readVariables(tree, specialSeparationDims, directData) {
308
- // Read all variables in the model parse tree.
309
- // This populates the variables table with basic information for each variable
310
- // such as the var name and subscripts.
311
- let variableReader = new VariableReader(specialSeparationDims, directData)
312
- variableReader.visitModel(tree)
313
- // Add a placeholder variable for the exogenous variable Time.
314
- let v = new Variable(null)
315
- v.modelLHS = 'Time'
316
- v.varName = '_time'
317
- addVariable(v)
318
- }
319
265
  function analyze(parsedModelKind, inputVars, opts) {
320
266
  // Analyze the RHS of each equation in stages after all the variables are read.
321
267
  // Find non-apply-to-all vars that are defined with more than one equation.
@@ -325,23 +271,17 @@ function analyze(parsedModelKind, inputVars, opts) {
325
271
  setRefIds()
326
272
 
327
273
  // If enabled, reduce expressions used in variable definitions.
328
- if (parsedModelKind !== 'vensim-legacy') {
329
- if (opts?.reduceVariables !== false && process.env.SDE_NONPUBLIC_REDUCE_VARIABLES !== '0') {
330
- let reduceMode = opts?.reduceVariables || process.env.SDE_NONPUBLIC_REDUCE_VARIABLES || 'default'
331
- reduceVariables(variables, inputVars || [], reduceMode)
332
- }
274
+ if (opts?.reduceVariables !== false && process.env.SDE_NONPUBLIC_REDUCE_VARIABLES !== '0') {
275
+ let reduceMode = opts?.reduceVariables || process.env.SDE_NONPUBLIC_REDUCE_VARIABLES || 'default'
276
+ reduceVariables(variables, inputVars || [], reduceMode)
333
277
  }
334
278
  if (opts?.stopAfterReduceVariables === true) return
335
279
 
336
280
  // Read the RHS to list the refIds of vars that are referenced and set the var type.
337
- if (parsedModelKind === 'vensim-legacy') {
338
- readEquations()
339
- } else {
340
- variables.forEach(readEquation)
341
- }
281
+ variables.forEach(readEquation)
342
282
  }
343
283
 
344
- function checkSpecVars(spec, extData) {
284
+ function checkSpecVars(spec) {
345
285
  // Look up each var in the spec and issue and throw error if it does not exist.
346
286
 
347
287
  function check(varNames, specType) {
@@ -352,22 +292,9 @@ function checkSpecVars(spec, extData) {
352
292
  // out of the valid range)
353
293
  if (!R.contains('[', varName)) {
354
294
  if (!varWithRefId(varName)) {
355
- // Look for a variable in external data.
356
- if (extData?.has(varName)) {
357
- // console.error(`found ${specType} ${varName} in extData`)
358
- // Copy data from an external file to an equation that does a lookup.
359
- let lookup = R.reduce(
360
- (a, p) => listConcat(a, `(${p[0]}, ${p[1]})`, true),
361
- '',
362
- Array.from(extData.get(varName))
363
- )
364
- let modelEquation = `${decanonicalize(varName)} = WITH LOOKUP(Time, (${lookup}))`
365
- addEquation(modelEquation)
366
- } else {
367
- throw new Error(
368
- `The ${specType} variable ${varName} was declared in spec.json, but no matching variable was found in the model or external data sources`
369
- )
370
- }
295
+ throw new Error(
296
+ `The ${specType} variable ${varName} was declared in spec.json, but no matching variable was found in the model or external data sources`
297
+ )
371
298
  }
372
299
  }
373
300
  }
@@ -569,28 +496,6 @@ function setRefIds() {
569
496
  v.refId = refIdForVar(v)
570
497
  }, variables)
571
498
  }
572
- function readEquations() {
573
- // Augment variables with information from their equations.
574
- // This requires a refId for each var so that actual refIds can be resolved for the reference list.
575
- R.forEach(v => {
576
- let equationReader = new EquationReader(v)
577
- equationReader.read()
578
- }, variables)
579
- }
580
- function addEquation(modelEquation) {
581
- // Add an equation in Vensim model format.
582
- let parser = createParser(modelEquation)
583
- let tree = parser.equation()
584
- // Read the var and add it to the Model var table.
585
- let variableReader = new VariableReader()
586
- variableReader.visitEquation(tree)
587
- let v = variableReader.var
588
- // Fill in the refId.
589
- v.refId = refIdForVar(v)
590
- // Finish the variable by parsing the RHS.
591
- let equationReader = new EquationReader(v)
592
- equationReader.read()
593
- }
594
499
  //
595
500
  // Model API
596
501
  //
@@ -800,11 +705,7 @@ function vensimName(cVarName) {
800
705
  function cName(vensimVarName) {
801
706
  // Convert a Vensim variable name to a C name.
802
707
  // This function requires model analysis to be completed first when the variable has subscripts.
803
- if (process.env.SDE_NONPUBLIC_USE_NEW_PARSE === '0') {
804
- // TODO: For now we use the legacy VarNameReader when the old parser is active; this
805
- // code will be removed once the old parser is removed
806
- return new VarNameReader().read(vensimVarName)
807
- }
708
+
808
709
  // Split the variable name from the subscripts
809
710
  let matches = vensimVarName.match(/([^[]+)(?:\[([^\]]+)\])?/)
810
711
  if (!matches) {
@@ -1165,13 +1066,12 @@ function allListedVars() {
1165
1066
  if (timeVar) {
1166
1067
  vars.push(timeVar)
1167
1068
  }
1168
- vars.push(...initVars())
1169
1069
  vars.push(...auxVars())
1170
- // TODO: Also levelVars not covered by initVars?
1070
+ vars.push(...levelVars())
1171
1071
 
1172
- // Filter out data/lookup variables and variables that are generated/used internally
1072
+ // Filter out variables that are generated/used internally
1173
1073
  const isInternal = v => {
1174
- return v.refId.startsWith('__level') || v.refId.startsWith('__aux')
1074
+ return v.includeInOutput === false
1175
1075
  }
1176
1076
 
1177
1077
  return R.filter(v => !isInternal(v), vars)
@@ -1186,6 +1086,7 @@ function filteredListedVars() {
1186
1086
  function varIndexInfoMap() {
1187
1087
  // Return a map containing information for each listed variable:
1188
1088
  // varName
1089
+ // varType
1189
1090
  // varIndex
1190
1091
  // subscriptCount
1191
1092
 
@@ -1193,21 +1094,17 @@ function varIndexInfoMap() {
1193
1094
  // generated model
1194
1095
  const sortedVars = filteredListedVars()
1195
1096
 
1196
- // Get the set of unique variable names, and assign a 1-based index
1197
- // to each; this matches the index number used in `storeOutput()`
1198
- // in the generated C code
1097
+ // Get the set of unique variable names, and assign a 1-based index to each.
1098
+ // This matches the index number used in `storeOutput` and `setLookup` in the
1099
+ // generated C/JS code
1199
1100
  const infoMap = new Map()
1200
1101
  let varIndex = 1
1201
1102
  for (const v of sortedVars) {
1202
- if (v.varType === 'data' || v.varType === 'lookup') {
1203
- // Omit the index for data and lookup variables; at this time, the data for these
1204
- // cannot be output like for other types of variables
1205
- continue
1206
- }
1207
1103
  const varName = v.varName
1208
1104
  if (!infoMap.get(varName)) {
1209
1105
  infoMap.set(varName, {
1210
1106
  varName,
1107
+ varType: v.varType,
1211
1108
  varIndex,
1212
1109
  subscriptCount: v.families ? v.families.length : 0
1213
1110
  })
@@ -1222,43 +1119,89 @@ function varIndexInfo() {
1222
1119
  // Return an array, sorted by `varName`, containing information for each
1223
1120
  // listed variable:
1224
1121
  // varName
1122
+ // varType
1225
1123
  // varIndex
1226
1124
  // subscriptCount
1227
- return Array.from(varIndexInfoMap().values())
1125
+ if (cachedVarIndexInfo) {
1126
+ return cachedVarIndexInfo
1127
+ }
1128
+ cachedVarIndexInfo = Array.from(varIndexInfoMap().values())
1129
+ return cachedVarIndexInfo
1228
1130
  }
1229
1131
 
1230
1132
  function jsonList() {
1231
- // Return a stringified JSON object containing variable and subscript information
1232
- // for the model.
1133
+ // Return an object containing variable and subscript information for the model
1134
+ // that will be used to write the JSON model listing files.
1135
+ if (cachedJsonList) {
1136
+ return cachedJsonList
1137
+ }
1233
1138
 
1234
1139
  // Get the set of available subscripts
1235
1140
  const allDims = [...allDimensions()]
1236
- const sortedDims = allDims.sort((a, b) => a.name.localeCompare(b.name))
1141
+ const sortedFullDims = allDims.sort((a, b) => a.name.localeCompare(b.name))
1237
1142
 
1238
1143
  // Extract a subset of the available info for each variable and put them in eval order
1239
- const sortedVars = filteredListedVars()
1144
+ const sortedFullVars = filteredListedVars()
1240
1145
 
1241
1146
  // Assign a 1-based index for each variable that has data that can be accessed.
1242
- // This matches the index number used in `storeOutput()` in the generated C code.
1147
+ // This matches the index number used in `storeOutput` and `setLookup` in the
1148
+ // generated C/JS code
1243
1149
  const infoMap = varIndexInfoMap()
1244
- for (const v of sortedVars) {
1150
+ for (const v of sortedFullVars) {
1245
1151
  const varInfo = infoMap.get(v.varName)
1246
1152
  if (varInfo) {
1247
1153
  v.varIndex = varInfo.varIndex
1248
1154
  }
1249
1155
  }
1250
1156
 
1251
- // Convert to JSON
1252
- const obj = {
1253
- dimensions: sortedDims,
1254
- variables: sortedVars
1157
+ // Derive minimal versions of the full arrays; these only contain the minimal
1158
+ // subset of fields that are needed by the `ModelListing` class from the
1159
+ // runtime package. The property names in the minimal objects are slightly
1160
+ // different than the full ones to better match the latest naming used in the
1161
+ // compile and runtime packages.
1162
+ const sortedMinimalDims = sortedFullDims.map(d => {
1163
+ return {
1164
+ id: d.name,
1165
+ subIds: d.value
1166
+ }
1167
+ })
1168
+
1169
+ // Note that `sortedFullVars` may contain duplicates in the case of separated
1170
+ // variables, but for the minimal listing we only want to have one entry per
1171
+ // index (i.e., one entry for each base variable ID), so we filter out the
1172
+ // duplicates here.
1173
+ const baseIds = new Set()
1174
+ const sortedMinimalVars = []
1175
+ for (const v of sortedFullVars) {
1176
+ const baseId = v.varName
1177
+ if (!baseIds.has(baseId)) {
1178
+ baseIds.add(baseId)
1179
+
1180
+ const varInfo = {}
1181
+ varInfo.id = baseId
1182
+ if (v.families) {
1183
+ varInfo.dimIds = v.families
1184
+ }
1185
+ varInfo.index = v.varIndex
1186
+ sortedMinimalVars.push(varInfo)
1187
+ }
1188
+ }
1189
+
1190
+ cachedJsonList = {
1191
+ full: {
1192
+ dimensions: sortedFullDims,
1193
+ variables: sortedFullVars
1194
+ },
1195
+ minimal: {
1196
+ dimensions: sortedMinimalDims,
1197
+ variables: sortedMinimalVars
1198
+ }
1255
1199
  }
1256
- return JSON.stringify(obj, null, 2)
1200
+ return cachedJsonList
1257
1201
  }
1258
1202
 
1259
1203
  export default {
1260
1204
  addConstantExpr,
1261
- addEquation,
1262
1205
  addNonAtoAVar,
1263
1206
  addVariable,
1264
1207
  allVars,
@@ -0,0 +1,50 @@
1
+ import { canonicalName } from '../_shared/helpers.js'
2
+
3
+ /**
4
+ * Generate a lookup variable that can be used to provide inputs to a the `GAME`
5
+ * function at runtime.
6
+ *
7
+ * @param {*} v
8
+ * @param {*} callExpr
9
+ * @param {*} context
10
+ */
11
+ export function generateGameVariables(v, callExpr, context) {
12
+ // If the LHS includes subscripts, use those same subscripts when generating
13
+ // the new lookup variable
14
+ let subs
15
+ if (context.eqnLhs.varDef.subscriptRefs) {
16
+ const subNames = context.eqnLhs.varDef.subscriptRefs.map(subRef => subRef.subName)
17
+ subs = `[${subNames.join(',')}]`
18
+ } else {
19
+ subs = ''
20
+ }
21
+
22
+ // Synthesize a lookup variable name that is the same as the LHS variable
23
+ // name with ' game inputs' appended to it
24
+ const gameLookupVarName = context.eqnLhs.varDef.varName + ' game inputs'
25
+
26
+ // Add a reference to the synthesized game inputs lookup
27
+ const gameLookupVarId = canonicalName(gameLookupVarName)
28
+ v.gameLookupVarName = gameLookupVarId
29
+ if (v.referencedLookupVarNames) {
30
+ v.referencedLookupVarNames.push(gameLookupVarId)
31
+ } else {
32
+ v.referencedLookupVarNames = [gameLookupVarId]
33
+ }
34
+
35
+ // Define a variable for the synthesized game inputs lookup
36
+ const gameLookupVars = context.defineVariable(`${gameLookupVarName}${subs} ~~|`)
37
+
38
+ // Normally `defineVariable` sets `includeInOutput` to false for generated
39
+ // variables, but we want the generated lookup variable to appear in the
40
+ // model listing so that the user can reference it, so set `includeInOutput`
41
+ // to true. Also change the `varType` to 'lookup' instead of 'data'. We
42
+ // will declare a `Lookup` variable in the generated code, but unlike a
43
+ // normal lookup, we won't initialize it with data by default (it can only
44
+ // be updated at runtime).
45
+ gameLookupVars.forEach(v => {
46
+ v.includeInOutput = true
47
+ v.varType = 'lookup'
48
+ v.varSubtype = 'gameInputs'
49
+ })
50
+ }
@@ -16,6 +16,7 @@ import {
16
16
 
17
17
  import Model from './model.js'
18
18
  import { generateDelayVariables } from './read-equation-fn-delay.js'
19
+ import { generateGameVariables } from './read-equation-fn-game.js'
19
20
  import { generateNpvVariables } from './read-equation-fn-npv.js'
20
21
  import { generateSmoothVariables } from './read-equation-fn-smooth.js'
21
22
  import { generateTrendVariables } from './read-equation-fn-trend.js'
@@ -128,6 +129,8 @@ class Context {
128
129
  // Inhibit output for generated variables
129
130
  v.includeInOutput = false
130
131
  })
132
+
133
+ return vars
131
134
  }
132
135
 
133
136
  /**
@@ -383,16 +386,6 @@ function visitFunctionCall(v, callExpr, context) {
383
386
  validateCallArgs(callExpr, 1)
384
387
  break
385
388
 
386
- // TODO: We do not currently have full support for the GAME function, so report a warning for now
387
- case '_GAME':
388
- if (process.env.SDE_REPORT_UNSUPPORTED_FUNCTIONS !== '0') {
389
- console.warn(
390
- `WARNING: The GAME function (used in the definition of '${v.modelLHS}') is currently implemented as a no-op (it returns the input value).`
391
- )
392
- }
393
- validateCallArgs(callExpr, 1)
394
- break
395
-
396
389
  //
397
390
  //
398
391
  // 2-argument functions...
@@ -491,6 +484,12 @@ function visitFunctionCall(v, callExpr, context) {
491
484
  argModes[2] = 'init'
492
485
  break
493
486
 
487
+ case '_GAME':
488
+ validateCallDepth(callExpr, context)
489
+ validateCallArgs(callExpr, 1)
490
+ generateGameVariables(v, callExpr, context)
491
+ break
492
+
494
493
  case '_GET_DIRECT_CONSTANTS': {
495
494
  validateCallDepth(callExpr, context)
496
495
  validateCallArgs(callExpr, 3)
@@ -49,7 +49,7 @@ export function readVariables(parsedModel, specialSeparationDims) {
49
49
  */
50
50
  function variablesForEquation(eqn, specialSeparationDims) {
51
51
  // Start a new variable defined by this equation
52
- const variable = new Variable(null)
52
+ const variable = new Variable()
53
53
 
54
54
  // Fill in the LHS details
55
55
  const lhs = eqn.lhs.varDef
@@ -137,7 +137,7 @@ function variablesForEquation(eqn, specialSeparationDims) {
137
137
  // Generate variables expanded over subscripts to the model
138
138
  const variables = []
139
139
  for (const expansion of expansions) {
140
- const v = new Variable(null)
140
+ const v = new Variable()
141
141
  v.varName = baseVarId
142
142
  v.modelLHS = lhsText
143
143
  v.modelFormula = rhsText