@sdeverywhere/compile 0.7.30 → 0.7.32
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 +4 -4
- package/src/_shared/helpers.js +7 -18
- package/src/_shared/xlsx.js +517 -0
- package/src/generate/direct-data-helpers.js +2 -3
- package/src/generate/gen-direct-const.js +2 -3
- package/src/generate/gen-equation.js +5 -1
- package/src/generate/gen-expr.js +170 -16
- package/src/generate/gen-lookup-from-direct.js +4 -4
- 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-subscripts.js +2 -3
- package/src/model/read-variables.js +19 -14
- package/src/model/toposort.js +83 -1
|
@@ -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) || []
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import XLSX from 'xlsx'
|
|
2
|
-
|
|
3
1
|
import { readCsv } from '../_shared/helpers.js'
|
|
4
2
|
import { Subscript } from '../_shared/subscript.js'
|
|
3
|
+
import { decodeCell } from '../_shared/xlsx.js'
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Read the dimension definitions from the given model.
|
|
@@ -46,7 +45,7 @@ export function readDimensionDefs(parsedModel) {
|
|
|
46
45
|
*/
|
|
47
46
|
export function getDirectSubscripts(fileName, tabOrDelimiter, firstCell, lastCell) {
|
|
48
47
|
// If lastCell is a column letter, scan the column, else scan the row
|
|
49
|
-
const dataAddress =
|
|
48
|
+
const dataAddress = decodeCell(firstCell.toUpperCase())
|
|
50
49
|
let col = dataAddress.c
|
|
51
50
|
let row = dataAddress.r
|
|
52
51
|
if (col < 0 || row < 0) {
|
|
@@ -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++) {
|