@sdeverywhere/compile 0.7.31 → 0.7.33
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/README.md +2 -2
- package/package.json +6 -3
- package/src/_shared/helpers.js +11 -4
- package/src/_shared/model-spec.js +174 -0
- package/src/_shared/normalize-model-spec.js +32 -0
- package/src/_shared/read-dat.js +10 -3
- package/src/_shared/xlsx.js +60 -45
- package/src/generate/gen-code.js +10 -12
- package/src/generate/gen-equation.js +5 -1
- package/src/generate/gen-expr.js +171 -17
- package/src/index.js +20 -5
- package/src/model/analyze-cycles.js +208 -0
- package/src/model/model.js +73 -1
- package/src/model/read-equations.js +27 -8
- package/src/model/read-variables.js +19 -14
- package/src/model/toposort.js +83 -1
- package/src/parse-and-generate.js +50 -19
|
@@ -508,6 +508,10 @@ function visitFunctionCall(v, callExpr, context) {
|
|
|
508
508
|
argModes[2] = 'init'
|
|
509
509
|
break
|
|
510
510
|
|
|
511
|
+
case '_DEMAND_AT_PRICE':
|
|
512
|
+
validateCallArgs(callExpr, 3)
|
|
513
|
+
break
|
|
514
|
+
|
|
511
515
|
case '_DEPRECIATE_STRAIGHTLINE':
|
|
512
516
|
validateCallDepth(callExpr, context)
|
|
513
517
|
validateCallArgs(callExpr, 4)
|
|
@@ -522,6 +526,10 @@ function visitFunctionCall(v, callExpr, context) {
|
|
|
522
526
|
argModes[2] = 'init'
|
|
523
527
|
break
|
|
524
528
|
|
|
529
|
+
case '_FIND_MARKET_PRICE':
|
|
530
|
+
validateCallArgs(callExpr, 4)
|
|
531
|
+
break
|
|
532
|
+
|
|
525
533
|
case '_GAME':
|
|
526
534
|
validateCallDepth(callExpr, context)
|
|
527
535
|
validateCallArgs(callExpr, 1)
|
|
@@ -587,6 +595,11 @@ function visitFunctionCall(v, callExpr, context) {
|
|
|
587
595
|
argModes[1] = 'init'
|
|
588
596
|
break
|
|
589
597
|
|
|
598
|
+
case '_INVERT_MATRIX':
|
|
599
|
+
validateCallDepth(callExpr, context)
|
|
600
|
+
validateCallArgs(callExpr, 2)
|
|
601
|
+
break
|
|
602
|
+
|
|
590
603
|
case '_NPV':
|
|
591
604
|
validateCallArgs(callExpr, 4)
|
|
592
605
|
addFnReference = false
|
|
@@ -612,6 +625,10 @@ function visitFunctionCall(v, callExpr, context) {
|
|
|
612
625
|
generateSmoothVariables(v, callExpr, context)
|
|
613
626
|
break
|
|
614
627
|
|
|
628
|
+
case '_SUPPLY_AT_PRICE':
|
|
629
|
+
validateCallArgs(callExpr, 3)
|
|
630
|
+
break
|
|
631
|
+
|
|
615
632
|
case '_TREND':
|
|
616
633
|
validateCallArgs(callExpr, 3)
|
|
617
634
|
addFnReference = false
|
|
@@ -847,25 +864,27 @@ function visitFunctionCall(v, callExpr, context) {
|
|
|
847
864
|
if (callExpr.fnId === '_WITH_LOOKUP' && index > 1) {
|
|
848
865
|
// XXX: For `WITH LOOKUP` calls, only process the first argument; need to generalize this
|
|
849
866
|
break
|
|
850
|
-
} else if (
|
|
851
|
-
|
|
867
|
+
} else if (
|
|
868
|
+
(callExpr.fnId === '_ALLOCATE_AVAILABLE' && index === 1) ||
|
|
869
|
+
(callExpr.fnId === '_DEMAND_AT_PRICE' && index === 1) ||
|
|
870
|
+
(callExpr.fnId === '_SUPPLY_AT_PRICE' && index === 1) ||
|
|
871
|
+
(callExpr.fnId === '_FIND_MARKET_PRICE' && (index === 1 || index === 3))
|
|
872
|
+
) {
|
|
873
|
+
// Handle the second (`pp` or priority profile) argument of allocation function calls
|
|
852
874
|
// specially. An example call with a 2D `pp` looks like this:
|
|
853
875
|
// shipments[branch] = ALLOCATE AVAILABLE(demand[branch], priority[branch,ptype], avail) ~~|
|
|
854
876
|
// Or a 3D `pp` with a dimension:
|
|
855
877
|
// shipments[item,branch] = ALLOCATE AVAILABLE(demand[branch], priority[item,branch,ptype], avail) ~~|
|
|
856
878
|
// Or a 3D `pp` with a specific subscript:
|
|
857
879
|
// shipments[branch] = ALLOCATE AVAILABLE(demand[branch], priority[item1,branch,ptype], avail) ~~|
|
|
858
|
-
// Vensim requires passing a reference with `ptype` as the last subscript
|
|
859
|
-
// implementation uses the `ppriority` and `pwidth` values (the `ptype` is currently assumed
|
|
860
|
-
// to be 3). Therefore we need to add references to all variants of the variable, not just
|
|
861
|
-
// the ones for `ptype`.
|
|
880
|
+
// Vensim requires passing a reference with `ptype` as the last subscript.
|
|
862
881
|
if (argExpr.kind !== 'variable-ref') {
|
|
863
|
-
throw new Error(
|
|
882
|
+
throw new Error(`${callExpr.fnName} argument 'pp' must be a variable reference`)
|
|
864
883
|
}
|
|
865
884
|
// TODO: Throw an error if the last dimension of arg0 does not match last dimension of LHS
|
|
866
885
|
// TODO: Throw an error if the second-to-last dimension of arg1 does not match last dimension of LHS
|
|
867
886
|
// TODO: Throw an error if the last subscript of arg1 does not have the "shape" of a `ppriority` dimension
|
|
868
|
-
// TODO: Throw an error if the `ptype` value is
|
|
887
|
+
// TODO: Throw an error if the `ptype` value is unsupported
|
|
869
888
|
// Get the RHS subscript/dimension IDs
|
|
870
889
|
const rhsVarBaseRefId = argExpr.varId
|
|
871
890
|
const rhsVarSubIds = argExpr.subscriptRefs?.map(subRef => subRef.subId) || []
|
|
@@ -120,22 +120,27 @@ function variablesForEquation(eqn, specialSeparationDims, separateAllVarsWithDim
|
|
|
120
120
|
let separationDims = specialSeparationDims[baseVarId] || []
|
|
121
121
|
if (!Array.isArray(separationDims)) {
|
|
122
122
|
separationDims = [separationDims]
|
|
123
|
+
} else {
|
|
124
|
+
// Copy the array so that the spec object is not mutated below
|
|
125
|
+
separationDims = [...separationDims]
|
|
123
126
|
}
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
separationDims
|
|
137
|
-
|
|
127
|
+
// Additionally, separate on dims from `separateAllVarsWithDims` if the var
|
|
128
|
+
// matches one of the dim lists.
|
|
129
|
+
for (let dimList of separateAllVarsWithDims) {
|
|
130
|
+
// Technically we allow each entry in the spec array to be either a single
|
|
131
|
+
// dim ID string or an array of dim IDs, so convert to array if needed
|
|
132
|
+
if (!Array.isArray(dimList)) {
|
|
133
|
+
dimList = [dimList]
|
|
134
|
+
}
|
|
135
|
+
// The list entry from the spec is only considered a match if every dimension
|
|
136
|
+
// in the spec list appears on the LHS
|
|
137
|
+
if (dimList.every(dim => subIds.includes(dim))) {
|
|
138
|
+
for (const dim of dimList) {
|
|
139
|
+
if (!separationDims.includes(dim)) {
|
|
140
|
+
separationDims.push(dim)
|
|
141
|
+
}
|
|
138
142
|
}
|
|
143
|
+
break
|
|
139
144
|
}
|
|
140
145
|
}
|
|
141
146
|
positionsToExpand = subscriptPositionsToExpand(subIds, exceptSubIdSets, separationDims, variable.modelFormula)
|
package/src/model/toposort.js
CHANGED
|
@@ -42,7 +42,19 @@ function toposort(nodes, edges) {
|
|
|
42
42
|
} catch (_) {
|
|
43
43
|
nodeRep = ''
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
var chain = [...predecessors]
|
|
46
|
+
var error = new Error('Found cyclic dependency during toposort:\n' + chain.join(' →\n') + ' →' + nodeRep)
|
|
47
|
+
// Attach the cycle itself (the portion of the dependency chain from the first
|
|
48
|
+
// occurrence of the repeated node) so that callers can analyze it.
|
|
49
|
+
error.cycle = chain.slice(chain.indexOf(node))
|
|
50
|
+
// Also attach all cycle clusters in the graph (the strongly connected components
|
|
51
|
+
// with more than one node, plus any single node that depends on itself) along
|
|
52
|
+
// with the graph edges so that callers can analyze every cycle at once.
|
|
53
|
+
error.cycles = stronglyConnectedComponents(nodes, outgoingEdges).filter(
|
|
54
|
+
scc => scc.length > 1 || (outgoingEdges.get(scc[0]) || new Set()).has(scc[0])
|
|
55
|
+
)
|
|
56
|
+
error.outgoingEdges = outgoingEdges
|
|
57
|
+
throw error
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
if (!nodesHash.has(node)) {
|
|
@@ -70,6 +82,76 @@ function toposort(nodes, edges) {
|
|
|
70
82
|
}
|
|
71
83
|
}
|
|
72
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Find the strongly connected components of the graph using an iterative form of
|
|
87
|
+
* Tarjan's algorithm (iterative to avoid stack overflow on the deep dependency
|
|
88
|
+
* chains found in large models).
|
|
89
|
+
*
|
|
90
|
+
* @param {Array} nodes The nodes in the graph.
|
|
91
|
+
* @param {Map} outgoingEdges A map of each node to the set of nodes that it points to.
|
|
92
|
+
* @returns {Array} An array of strongly connected components, where each component is
|
|
93
|
+
* an array of the nodes that it contains.
|
|
94
|
+
*/
|
|
95
|
+
function stronglyConnectedComponents(nodes, outgoingEdges) {
|
|
96
|
+
var index = 0
|
|
97
|
+
var nodeIndex = new Map()
|
|
98
|
+
var lowlink = new Map()
|
|
99
|
+
var onStack = new Set()
|
|
100
|
+
var stack = []
|
|
101
|
+
var sccs = []
|
|
102
|
+
for (var start of nodes) {
|
|
103
|
+
if (nodeIndex.has(start)) {
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
var frames = [{ node: start, edges: null, i: 0, child: undefined }]
|
|
107
|
+
while (frames.length > 0) {
|
|
108
|
+
var frame = frames[frames.length - 1]
|
|
109
|
+
var node = frame.node
|
|
110
|
+
if (frame.edges === null) {
|
|
111
|
+
// First visit to this node
|
|
112
|
+
nodeIndex.set(node, index)
|
|
113
|
+
lowlink.set(node, index)
|
|
114
|
+
index++
|
|
115
|
+
stack.push(node)
|
|
116
|
+
onStack.add(node)
|
|
117
|
+
frame.edges = Array.from(outgoingEdges.get(node) || [])
|
|
118
|
+
} else if (frame.child !== undefined) {
|
|
119
|
+
// Returning from a child visit
|
|
120
|
+
lowlink.set(node, Math.min(lowlink.get(node), lowlink.get(frame.child)))
|
|
121
|
+
frame.child = undefined
|
|
122
|
+
}
|
|
123
|
+
var descended = false
|
|
124
|
+
while (frame.i < frame.edges.length) {
|
|
125
|
+
var w = frame.edges[frame.i++]
|
|
126
|
+
if (!nodeIndex.has(w)) {
|
|
127
|
+
frame.child = w
|
|
128
|
+
frames.push({ node: w, edges: null, i: 0, child: undefined })
|
|
129
|
+
descended = true
|
|
130
|
+
break
|
|
131
|
+
} else if (onStack.has(w)) {
|
|
132
|
+
lowlink.set(node, Math.min(lowlink.get(node), nodeIndex.get(w)))
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (descended) {
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
// All edges have been visited, so the node is complete
|
|
139
|
+
if (lowlink.get(node) === nodeIndex.get(node)) {
|
|
140
|
+
var scc = []
|
|
141
|
+
var member
|
|
142
|
+
do {
|
|
143
|
+
member = stack.pop()
|
|
144
|
+
onStack.delete(member)
|
|
145
|
+
scc.push(member)
|
|
146
|
+
} while (member !== node)
|
|
147
|
+
sccs.push(scc)
|
|
148
|
+
}
|
|
149
|
+
frames.pop()
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return sccs
|
|
153
|
+
}
|
|
154
|
+
|
|
73
155
|
function uniqueNodes(arr) {
|
|
74
156
|
var res = new Set()
|
|
75
157
|
for (var i = 0, len = arr.length; i < len; i++) {
|
|
@@ -6,6 +6,7 @@ import { parseVensimModel, parseXmileModel } from '@sdeverywhere/parse'
|
|
|
6
6
|
|
|
7
7
|
import B from './_shared/bufx.js'
|
|
8
8
|
import { readXlsx } from './_shared/helpers.js'
|
|
9
|
+
import { normalizeModelSpec } from './_shared/normalize-model-spec.js'
|
|
9
10
|
import { readDat } from './_shared/read-dat.js'
|
|
10
11
|
import { printSubscripts } from './_shared/subscript.js'
|
|
11
12
|
import { cName } from './_shared/var-names.js'
|
|
@@ -13,6 +14,34 @@ import Model from './model/model.js'
|
|
|
13
14
|
import { getDirectSubscripts } from './model/read-subscripts.js'
|
|
14
15
|
import { generateCode } from './generate/gen-code.js'
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* The kind of model that is being parsed.
|
|
19
|
+
*
|
|
20
|
+
* @typedef {'vensim' | 'xmile'} ModelKind
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A parsed tree representation of a model, along with the kind of model that was parsed.
|
|
25
|
+
*
|
|
26
|
+
* @typedef {Object} ParsedModel
|
|
27
|
+
* @property {ModelKind} kind The kind of model that was parsed.
|
|
28
|
+
* @property {import('@sdeverywhere/parse').Model} root The root of the parsed model AST.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* An operation that can be performed by `parseAndGenerate`.
|
|
33
|
+
*
|
|
34
|
+
* - `generateC` writes the generated C code to the build directory.
|
|
35
|
+
* - `generateJS` writes the generated JS code to the build directory.
|
|
36
|
+
* - `printVarList` writes variables and subscripts to txt and json files under the build
|
|
37
|
+
* directory.
|
|
38
|
+
* - `printRefIdTest` prints reference identifiers to the console.
|
|
39
|
+
* - `printRefGraph` prints the variable dependency graph to the console.
|
|
40
|
+
* - `convertNames` generates no output, but makes the results of model analysis available.
|
|
41
|
+
*
|
|
42
|
+
* @typedef {'generateC' | 'generateJS' | 'printVarList' | 'printRefIdTest' | 'printRefGraph' | 'convertNames'} GenerateOperation
|
|
43
|
+
*/
|
|
44
|
+
|
|
16
45
|
/**
|
|
17
46
|
* Parse a Vensim or XMILE model and generate C code.
|
|
18
47
|
*
|
|
@@ -27,27 +56,30 @@ import { generateCode } from './generate/gen-code.js'
|
|
|
27
56
|
* analysis will be available.
|
|
28
57
|
*
|
|
29
58
|
* @param {string} input The preprocessed Vensim or XMILE model text.
|
|
30
|
-
* @param {
|
|
31
|
-
* @param {
|
|
32
|
-
* @param {
|
|
33
|
-
*
|
|
34
|
-
* read but no operation will be performed.
|
|
59
|
+
* @param {ModelKind} modelKind The kind of model to parse.
|
|
60
|
+
* @param {import('./_shared/model-spec.js').ModelSpec} spec The model spec (from the JSON file).
|
|
61
|
+
* @param {GenerateOperation[]} operations The set of operations to perform. If the array is
|
|
62
|
+
* empty, the model will be read but no operation will be performed.
|
|
35
63
|
* @param {string} modelDirname The absolute path to the directory containing data (dat, xlsx, csv)
|
|
36
64
|
* files that are referenced by the model. These files will be resolved relative to this directory.
|
|
37
65
|
* @param {string} modelName The model name (without the mdl extension).
|
|
38
66
|
* @param {string} buildDir The output directory where the C or list files will be written.
|
|
39
67
|
* @param {string} [varname] The variable name passed to the 'sde causes' command.
|
|
40
|
-
* @return A
|
|
68
|
+
* @return {Promise<string>} A promise that resolves with the generated C or JS code.
|
|
41
69
|
*/
|
|
42
70
|
export async function parseAndGenerate(input, modelKind, spec, operations, modelDirname, modelName, buildDir, varname) {
|
|
71
|
+
// Resolve any deprecated property names in the spec so that we only need to
|
|
72
|
+
// consult the preferred names below
|
|
73
|
+
normalizeModelSpec(spec)
|
|
74
|
+
|
|
43
75
|
// Read time series from external DAT files into a single object.
|
|
44
|
-
//
|
|
45
|
-
//
|
|
76
|
+
// `datFiles` is an array of either filenames or objects giving a variable
|
|
77
|
+
// name prefix as the key and a filename as the value.
|
|
46
78
|
let extData = new Map()
|
|
47
|
-
if (spec.
|
|
48
|
-
for (let datfile of spec.
|
|
79
|
+
if (spec.datFiles) {
|
|
80
|
+
for (let datfile of spec.datFiles) {
|
|
49
81
|
let prefix = ''
|
|
50
|
-
let filename
|
|
82
|
+
let filename
|
|
51
83
|
if (typeof datfile === 'object') {
|
|
52
84
|
prefix = Object.keys(datfile)[0]
|
|
53
85
|
filename = datfile[prefix]
|
|
@@ -110,8 +142,9 @@ export async function parseAndGenerate(input, modelKind, spec, operations, model
|
|
|
110
142
|
*
|
|
111
143
|
* This is used only to implement the `sde names` command.
|
|
112
144
|
*
|
|
113
|
-
* @param namesPathname The path to the file containing variables names.
|
|
114
|
-
* @param
|
|
145
|
+
* @param {string} namesPathname The path to the file containing variables names.
|
|
146
|
+
* @param {'to-c' | 'to-vensim'} operation The conversion to perform.
|
|
147
|
+
* @return {void}
|
|
115
148
|
*/
|
|
116
149
|
export function printNames(namesPathname, operation) {
|
|
117
150
|
let lines = B.lines(B.read(namesPathname))
|
|
@@ -130,15 +163,13 @@ export function printNames(namesPathname, operation) {
|
|
|
130
163
|
/**
|
|
131
164
|
* Read and parse the given model text and return the parsed model structure.
|
|
132
165
|
*
|
|
133
|
-
* TODO: Fix return type
|
|
134
|
-
*
|
|
135
166
|
* @param {string} input The string containing the model text.
|
|
136
|
-
* @param {
|
|
137
|
-
* @param {string} modelDir The absolute path to the directory containing data (dat, xlsx, csv)
|
|
167
|
+
* @param {ModelKind} modelKind The kind of model to parse.
|
|
168
|
+
* @param {string} [modelDir] The absolute path to the directory containing data (dat, xlsx, csv)
|
|
138
169
|
* files that are referenced by the model. These files will be resolved relative to this directory.
|
|
139
170
|
* @param {Object} [options] The options that control parsing.
|
|
140
|
-
* @param {boolean} options.sort Whether to sort definitions alphabetically in the preprocess step.
|
|
141
|
-
* @return {
|
|
171
|
+
* @param {boolean} [options.sort] Whether to sort definitions alphabetically in the preprocess step.
|
|
172
|
+
* @return {ParsedModel} A parsed tree representation of the model.
|
|
142
173
|
*/
|
|
143
174
|
export function parseModel(input, modelKind, modelDir, options) {
|
|
144
175
|
if (modelKind === 'vensim') {
|