@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.
@@ -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 (callExpr.fnId === '_ALLOCATE_AVAILABLE' && index === 1) {
851
- // Handle the second (`pp` or priority profile) argument of `ALLOCATE AVAILABLE` calls
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, but the function
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(`ALLOCATE AVAILABLE argument 'pp' must be a variable reference`)
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 not 3
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 = XLSX.utils.decode_cell(firstCell.toUpperCase())
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
- // 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
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)
@@ -42,7 +42,19 @@ function toposort(nodes, edges) {
42
42
  } catch (_) {
43
43
  nodeRep = ''
44
44
  }
45
- throw new Error('Found cyclic dependency during toposort:\n' + [...predecessors].join(' →\n') + ' →' + nodeRep)
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++) {