@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.
- package/package.json +1 -3
- package/src/generate/expand-var-names.js +0 -10
- package/src/generate/{code-gen.js → gen-code-c.js} +192 -103
- package/src/generate/gen-code-js.js +616 -0
- package/src/generate/gen-code.js +36 -0
- package/src/generate/gen-equation.js +16 -6
- package/src/generate/gen-expr.js +205 -48
- package/src/generate/gen-lookup-from-direct.js +14 -6
- package/src/generate/gen-lookup-from-external.js +20 -5
- package/src/generate/gen-lookup-from-points.js +28 -6
- package/src/index.js +38 -3
- package/src/model/model.js +94 -151
- package/src/model/read-equation-fn-game.js +50 -0
- package/src/model/read-equations.js +9 -10
- package/src/model/read-variables.js +2 -2
- package/src/model/variable.js +6 -21
- package/src/parse-and-generate.js +22 -19
- package/src/preprocess/preprocessor.js +2 -2
- package/src/generate/equation-gen.js +0 -1268
- package/src/generate/model-lhs-reader.js +0 -88
- package/src/model/equation-reader.js +0 -723
- package/src/model/expr-reader.js +0 -202
- package/src/model/subscript-range-reader.js +0 -143
- package/src/model/var-name-reader.js +0 -40
- package/src/model/variable-reader.js +0 -172
- package/src/parse/model-reader.js +0 -141
- package/src/parse/parser.js +0 -36
|
@@ -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
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
7
|
-
export {
|
|
8
|
-
|
|
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
|
+
}
|
package/src/model/model.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
|
106
|
-
//
|
|
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
|
|
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 (
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
338
|
-
readEquations()
|
|
339
|
-
} else {
|
|
340
|
-
variables.forEach(readEquation)
|
|
341
|
-
}
|
|
281
|
+
variables.forEach(readEquation)
|
|
342
282
|
}
|
|
343
283
|
|
|
344
|
-
function checkSpecVars(spec
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1070
|
+
vars.push(...levelVars())
|
|
1171
1071
|
|
|
1172
|
-
// Filter out
|
|
1072
|
+
// Filter out variables that are generated/used internally
|
|
1173
1073
|
const isInternal = v => {
|
|
1174
|
-
return v.
|
|
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
|
-
//
|
|
1198
|
-
//
|
|
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
|
-
|
|
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
|
|
1232
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
|
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(
|
|
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(
|
|
140
|
+
const v = new Variable()
|
|
141
141
|
v.varName = baseVarId
|
|
142
142
|
v.modelLHS = lhsText
|
|
143
143
|
v.modelFormula = rhsText
|