@sdeverywhere/compile 0.7.0

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.
@@ -0,0 +1,373 @@
1
+ import util from 'util'
2
+ import B from 'bufx'
3
+ import yaml from 'js-yaml'
4
+ import R from 'ramda'
5
+ import { canonicalName, asort, vlog } from './helpers.js'
6
+
7
+ // A subscript is a dimension or an index.
8
+ // Both have the same properties: model name, canonical name, family, values.
9
+
10
+ // |Property |Description |
11
+ // |-------------|------------------------------------------------------------------|
12
+ // |modelName |subscript name in Vensim format |
13
+ // |modelValue |subscript value in Vensim format |
14
+ // |modelMappings|mappings from a dimension to a mapping value in Vensim format |
15
+ // |name |subscript name in canonical C format |
16
+ // |value |subscript value in canonical C format |
17
+ // |mappings |mappings from a dimension to a mapping value in canonical C format|
18
+ // |size |number of indices in a dimension or 1 for an index |
19
+ // |family |subscript family on canonical C format (finalized later) |
20
+
21
+ // A family is a dimension that is a reference to the maximal dimension containing an index or subdimension.
22
+ // The family of the maximal dimension is itself.
23
+ // Values are a single number for indices and an array of subscripts for dimensions.
24
+ // A Variable includes a list of subscripts in normal order.
25
+ // Mappings give a list of indices in the mapping value that map in order to to-dim indices.
26
+
27
+ // Subscript API
28
+ //
29
+ // Subscript(modelName, modelValue, modelFamily, modelMappings)
30
+ // with modelMappings of the form [ { toDim: dimension, value: indexArray } ]
31
+ // sets a subscript dimension that maps the index elements in indexArray
32
+ // to the index elements in another dimension
33
+ // Subscript(modelName)
34
+ // gets the subscript with the model name
35
+ // Subscript(modelName, modelValue) with modelValue as array
36
+ // sets a subscript dimension and its indices
37
+ // Subscript(modelName, modelValue, modelFamily) with modelValue as number
38
+ // sets a subscript element and its index value in the subscript family
39
+ // Subscript(modelName, '', modelFamily, [])
40
+ // sets modelName as an alias for the dimension named as modelFamily
41
+ //
42
+ // Call Subscript with an array of subscript indices to establish a dimension.
43
+ // If there is a mapping to another dimension, give modelMappings in the call.
44
+ // Then call Subscript with each individual subscript index name and its numeric index value.
45
+ // All dimension and index names are in model form, not canonical form.
46
+ // They must be converted to canonical form later after all subscripts are read.
47
+
48
+ // The Subscript module maintains a subscript map with the canonical
49
+ // subscript name as the key and a subscript object as the value.
50
+ let subscripts = new Map()
51
+
52
+ export function Subscript(modelName, modelValue = null, modelFamily = null, modelMappings = null) {
53
+ let name = canonicalName(modelName)
54
+ if (modelValue === null) {
55
+ // Look up a subscript by its model name.
56
+ return sub(name)
57
+ }
58
+ let value, size
59
+ if (Array.isArray(modelValue)) {
60
+ // Map the subscript value array into canonical form.
61
+ value = R.map(x => canonicalName(x), modelValue)
62
+ size = value.length
63
+ } else if (typeof modelValue === 'number') {
64
+ // The value is an index.
65
+ value = modelValue
66
+ size = 1
67
+ } else if (modelValue === '') {
68
+ // An empty value string indicates a subscript alias given by the modelFamily.
69
+ value = ''
70
+ size = 0
71
+ }
72
+ // Convert the model family into canonical form.
73
+ if (modelFamily === null) {
74
+ // The default family is the subscript itself.
75
+ modelFamily = modelName
76
+ }
77
+ let family = canonicalName(modelFamily)
78
+ // Convert the subscript mappings into canonical form.
79
+ let mappings = {}
80
+ if (modelMappings && !R.isEmpty(modelMappings)) {
81
+ for (let m of modelMappings) {
82
+ mappings[canonicalName(m.toDim)] = R.map(subName => canonicalName(subName), m.value)
83
+ }
84
+ }
85
+ // Save the subscript as an object in the subscripts store.
86
+ let subscript = {
87
+ modelName,
88
+ modelValue,
89
+ modelMappings,
90
+ name,
91
+ value,
92
+ size,
93
+ family,
94
+ mappings
95
+ }
96
+ subscripts.set(name, subscript)
97
+ return subscript
98
+ }
99
+ export function sub(name) {
100
+ // Look up a subscript by its canonical name.
101
+ // Return undefined if the name is not a subscript name.
102
+ let result
103
+ try {
104
+ result = subscripts.get(name)
105
+ } catch (e) {
106
+ console.error(`sub name ${name} not found`)
107
+ }
108
+ return result
109
+ }
110
+ export function isIndex(name) {
111
+ let s = sub(name)
112
+ return s && typeof s.value === 'number'
113
+ }
114
+ export function isDimension(name) {
115
+ let s = sub(name)
116
+ return s && Array.isArray(s.value)
117
+ }
118
+ export function isSubdimension(name) {
119
+ let result = false
120
+ let s = sub(name)
121
+ if (s && Array.isArray(s.value)) {
122
+ result = s.size < sub(s.family).size
123
+ }
124
+ return result
125
+ }
126
+ export function isTrivialDimension(name) {
127
+ // Return true if the dimension values are trivial, i.e., {0, 1, 2, ..., n-1}
128
+ let s = sub(name)
129
+ if (!s || !Array.isArray(s.value)) {
130
+ return false
131
+ }
132
+ // The following evaluates to true when all sub-dimensions match their position in the array
133
+ return R.addIndex(R.all)((subdim, idx) => sub(subdim).value === idx, s.value)
134
+ }
135
+ export function indexInSepDim(ind, v) {
136
+ // Find the separation dim in the variable that includes the index, or return null.
137
+ let result = null
138
+ for (let sepDim of v.separationDims) {
139
+ if (sub(sepDim).value.includes(ind)) {
140
+ result = sepDim
141
+ break
142
+ }
143
+ }
144
+ return result
145
+ }
146
+ export function subscriptsMatch(s1, s2) {
147
+ // Return true when subscript s1 matches subscript s2.
148
+ let matches = false
149
+ if (isIndex(s1) && isIndex(s2)) {
150
+ matches = s1 === s2
151
+ } else if (isDimension(s1) && isDimension(s2)) {
152
+ matches = s1 === s2
153
+ if (!matches) {
154
+ // Also match when s2 is a subdimension of s1.
155
+ matches = sub(s2).family === sub(s1).family && sub(s2).value.length < sub(s1).value.length
156
+ }
157
+ } else if (isDimension(s1) && isIndex(s2)) {
158
+ matches = sub(s1).value.includes(s2)
159
+ } else if (isIndex(s1) && isDimension(s2)) {
160
+ matches = sub(s2).value.includes(s1)
161
+ }
162
+ return matches
163
+ }
164
+ export function addIndex(name, value, family) {
165
+ // Add an index with arguments in canonical form.
166
+ let subscript = {
167
+ name: name,
168
+ value: value,
169
+ size: 1,
170
+ family: family,
171
+ mappings: {}
172
+ }
173
+ subscripts.set(name, subscript)
174
+ }
175
+ export function hasMapping(fromSubscript, toSubscript) {
176
+ let subFrom = sub(fromSubscript)
177
+ let subTo = sub(toSubscript)
178
+ if (subFrom === undefined) {
179
+ vlog('ERROR: undefined hasMapping fromSubscript', fromSubscript)
180
+ }
181
+ if (subTo === undefined) {
182
+ vlog('ERROR: undefined hasMapping toSubscript', toSubscript)
183
+ }
184
+ if (subFrom.mappings[toSubscript]) {
185
+ return true
186
+ }
187
+ return false
188
+ }
189
+ export function mapIndex(fromSubName, fromIndexName, toSubName) {
190
+ // Return the index names that the fromSubName dimension maps from fromIndexName
191
+ // to the toSubName dimension. Return an empty array if there is no such mapping.
192
+ let toIndexNames = []
193
+ let fromSub = sub(fromSubName)
194
+ let toSub = sub(toSubName)
195
+ if (fromSub && toSub && isDimension(fromSubName) && isDimension(toSubName)) {
196
+ let mapping = fromSub.mappings[toSubName]
197
+ if (mapping) {
198
+ // Find the positions of fromIndexName in the mapping.
199
+ for (let pos = 0; pos < mapping.length; pos++) {
200
+ if (mapping[pos] === fromIndexName) {
201
+ // Return the index name at the same position in the toSub dimension.
202
+ toIndexNames.push(toSub.value[pos])
203
+ }
204
+ }
205
+ }
206
+ }
207
+ return toIndexNames
208
+ }
209
+ export function printSubscripts() {
210
+ B.clearBuf()
211
+ for (let [k, v] of subscripts) {
212
+ B.emitLine(`${k}:\n${util.inspect(v, { depth: null })}\n`)
213
+ }
214
+ return B.getBuf()
215
+ }
216
+ export function yamlSubsList() {
217
+ let subs = {}
218
+ for (let [k, v] of subscripts) {
219
+ subs[k] = v
220
+ }
221
+ return yaml.safeDump(subs)
222
+ }
223
+ export function loadSubscriptsFromYaml(yamlSubs) {
224
+ // Load the subscripts map from subscripts serialized to a YAML file by yamlSubsList.
225
+ // This function should be called instead of adding subscripts through the constructor.
226
+ let subs = yaml.safeLoad(yamlSubs)
227
+ for (const k in subs) {
228
+ subscripts.set(k, subs[k])
229
+ }
230
+ }
231
+ export function normalizeSubscripts(subscripts) {
232
+ // Sort a list of subscript names already in canonical form according to the subscript family.
233
+ let subs = R.map(name => sub(name), subscripts)
234
+ subs = R.sortBy(R.prop('family'), subs)
235
+ let normalizedSubs
236
+ try {
237
+ normalizedSubs = R.map(R.prop('name'), subs)
238
+ } catch (e) {
239
+ console.error(`normalizeSubscripts fails for ${subscripts}`)
240
+ }
241
+ return normalizedSubs
242
+ }
243
+ export function extractMarkedDims(subscripts) {
244
+ // Extract all marked dimensions and update subscripts.
245
+ let dims = []
246
+ for (let i = 0; i < subscripts.length; i++) {
247
+ if (subscripts[i].includes('!')) {
248
+ // Remove the "!" from the subscript name and save it as a marked dimension.
249
+ subscripts[i] = subscripts[i].replace('!', '')
250
+ dims.push(subscripts[i])
251
+ }
252
+ }
253
+ return dims
254
+ }
255
+ export function subscriptFamilies(subscripts) {
256
+ // Return a list of the subscript families for each subscript.
257
+ try {
258
+ return R.map(subscriptName => sub(subscriptName).family, subscripts)
259
+ } catch (e) {
260
+ console.error(`ERROR: subscript not found in "${subscripts.join(',')}" in subscriptFamilies`)
261
+ }
262
+ }
263
+ export function subscriptFamily(subscriptName) {
264
+ // Return the subscript family object for the subscript name.
265
+ let family = sub(subscriptName).family
266
+ return sub(family)
267
+ }
268
+ export function allSubscripts() {
269
+ // Return an array of all subscript objects.
270
+ return [...subscripts.values()]
271
+ }
272
+ export function allDimensions() {
273
+ // Return an array of all dimension subscript objects.
274
+ return R.filter(subscript => Array.isArray(subscript.value), allSubscripts())
275
+ }
276
+ export function allAliases() {
277
+ // Return an array of all subscript aliases.
278
+ return R.filter(subscript => subscript.value === '', allSubscripts())
279
+ }
280
+ export function allMappings() {
281
+ // Return an array of all subscript mappings as objects.
282
+ let mappings = []
283
+ R.forEach(subscript => {
284
+ R.forEach(mapTo => {
285
+ mappings.push({
286
+ mapFrom: subscript.name,
287
+ mapTo: mapTo,
288
+ value: subscript.mappings[mapTo]
289
+ })
290
+ }, Object.keys(subscript.mappings))
291
+ }, allSubscripts())
292
+ return mappings
293
+ }
294
+ export function indexNamesForSubscript(subscript) {
295
+ // Return a list of index names for a subscript in canonical form.
296
+ if (isIndex(subscript)) {
297
+ // The subscript is an index, so just return it.
298
+ return [subscript]
299
+ } else {
300
+ // Return a list of index names for the dimension.
301
+ let dim = sub(subscript)
302
+ if (!dim) {
303
+ vlog('ERROR: no indexNamesForSubscript', subscript)
304
+ console.trace()
305
+ return []
306
+ }
307
+ return dim.value
308
+ }
309
+ }
310
+ export function separatedVariableIndex(rhsSub, variable, rhsSubscripts) {
311
+ // Given an RHS subscript, find an LHS index in the separation dimension that matches it.
312
+ // The LHS and RHS subscripts need not be in normal order or have the same number of subscripts in a var.
313
+ // The search proceeds through three stages:
314
+ // 1. Find a sepDim that matches rhsSub.
315
+ // 2. Then find an lhsSub in the same family as the sepDim.
316
+ // 3. Further qualify the lhsSub.
317
+
318
+ // If rhsSub is found on the LHS, don't convert it into an index.
319
+ if (!variable.subscripts.includes(rhsSub)) {
320
+ // (1)
321
+ for (let sepDim of variable.separationDims) {
322
+ if (rhsSub === sepDim || hasMapping(rhsSub, sepDim)) {
323
+ // (2)
324
+ for (let lhsSub of variable.subscripts) {
325
+ if (sub(lhsSub).family === sub(sepDim).family) {
326
+ if (!isIndex(lhsSub)) {
327
+ console.error(`ERROR: ${variable.refId} subscript in separation dimension ${sepDim} is not an index`)
328
+ } else {
329
+ // (3)
330
+ if (rhsSub === sepDim) {
331
+ // There may be more than one lhsSub in the same family as rhsSub.
332
+ // Pick the one that belongs to the rhsSub.
333
+ // If there are two LHS subs both in the same family, choose by position instead.
334
+ if (
335
+ rhsSubscripts &&
336
+ variable.subscripts.length === 2 &&
337
+ rhsSubscripts.length === 2 &&
338
+ sub(variable.subscripts[0]).family === sub(variable.subscripts[1]).family
339
+ ) {
340
+ let pos = rhsSubscripts.indexOf(rhsSub)
341
+ return variable.subscripts[pos]
342
+ } else {
343
+ if (indexNamesForSubscript(rhsSub).includes(lhsSub)) {
344
+ return lhsSub
345
+ }
346
+ }
347
+ } else {
348
+ // Find the index that maps from the subscript dimension to the separated var index.
349
+ for (let fromIndexName of sub(rhsSub).value) {
350
+ let mappedIndices = mapIndex(rhsSub, fromIndexName, sepDim)
351
+ if (mappedIndices.includes(lhsSub)) {
352
+ return fromIndexName
353
+ }
354
+ }
355
+ }
356
+ }
357
+ }
358
+ }
359
+ }
360
+ }
361
+ }
362
+ return null
363
+ }
364
+ // Function to filter canonical dimension names from a list of names
365
+ export let dimensionNames = R.pipe(
366
+ R.filter(subscript => isDimension(subscript)),
367
+ asort
368
+ )
369
+ // Function to filter canonical index names from a list of names
370
+ export let indexNames = R.pipe(
371
+ R.filter(subscript => isIndex(subscript)),
372
+ asort
373
+ )