@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.
@@ -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) || []
@@ -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++) {
@@ -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 {string} modelKind The kind of model to parse, either 'vensim' or 'xmile'.
31
- * @param {*} spec The model spec (from the JSON file).
32
- * @param {string[]} operations The set of operations to perform; can include 'generateC', 'generateJS',
33
- * 'printVarList', 'printRefIdTest', 'convertNames'. If the array is empty, the model will be
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 string containing the generated C code.
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
- // externalDatfiles is an array of either filenames or objects
45
- // giving a variable name prefix as the key and a filename as the value.
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.externalDatfiles) {
48
- for (let datfile of spec.externalDatfiles) {
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 operation Either 'to-c' or 'to-vensim'.
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 {string} modelKind The kind of model to parse, either 'vensim' or 'xmile'.
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 {*} A parsed tree representation of the model.
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') {