@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016-2022 Todd Fincannon and Climate Interactive / New Venture Fund
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # @sdeverywhere/compile
2
+
3
+ This package contains the core SDEverywhere compiler that takes a Vensim model
4
+ as input and generates C code as output.
5
+
6
+ ## Documentation
7
+
8
+ TODO
9
+
10
+ ## License
11
+
12
+ SDEverywhere is distributed under the MIT license. See `LICENSE` for more details.
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@sdeverywhere/compile",
3
+ "version": "0.7.0",
4
+ "description": "The core Vensim to C compiler for the SDEverywhere tool suite.",
5
+ "type": "module",
6
+ "files": [
7
+ "src/**",
8
+ "!.DS_Store"
9
+ ],
10
+ "main": "./src/index.js",
11
+ "dependencies": {
12
+ "antlr4": "4.9.2",
13
+ "antlr4-vensim": "0.6.0",
14
+ "bufx": "^1.0.5",
15
+ "byline": "^5.0.0",
16
+ "csv-parse": "^4.15.4",
17
+ "js-yaml": "^3.13.1",
18
+ "ramda": "^0.27.0",
19
+ "split-string": "^6.0.0",
20
+ "xlsx": "^0.17.0"
21
+ },
22
+ "author": "Climate Interactive",
23
+ "license": "MIT",
24
+ "homepage": "https://sdeverywhere.org",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/climateinteractive/SDEverywhere.git",
28
+ "directory": "packages/compile"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/climateinteractive/SDEverywhere/issues"
32
+ },
33
+ "scripts": {
34
+ "lint": "eslint . --max-warnings 0",
35
+ "prettier:check": "prettier --check .",
36
+ "prettier:fix": "prettier --write .",
37
+ "precommit": "../../scripts/precommit",
38
+ "ci:build": "run-s lint prettier:check"
39
+ }
40
+ }
@@ -0,0 +1,351 @@
1
+ import util from 'util'
2
+ import B from 'bufx'
3
+ import parseCsv from 'csv-parse/lib/sync.js'
4
+ import R from 'ramda'
5
+ import split from 'split-string'
6
+ import XLSX from 'xlsx'
7
+
8
+ // Set true to print a stack trace in vlog
9
+ export const PRINT_VLOG_TRACE = false
10
+
11
+ // next sequence number for generated temporary variable names
12
+ let nextTmpVarSeq = 1
13
+ // next sequence number for generated lookup variable names
14
+ let nextLookupVarSeq = 1
15
+ // next sequence number for generated fixed delay variable names
16
+ let nextFixedDelayVarSeq = 1
17
+ // next sequence number for generated level variable names
18
+ let nextLevelVarSeq = 1
19
+ // next sequence number for generated aux variable names
20
+ let nextAuxVarSeq = 1
21
+ // parsed csv data cache
22
+ let csvData = new Map()
23
+ // string table for web apps
24
+ export let strings = []
25
+
26
+ export let canonicalName = name => {
27
+ // Format a model variable name into a valid C identifier.
28
+ return (
29
+ '_' +
30
+ name
31
+ .trim()
32
+ .replace(/"/g, '_')
33
+ .replace(/\s+!$/g, '!')
34
+ .replace(/\s/g, '_')
35
+ .replace(/,/g, '_')
36
+ .replace(/-/g, '_')
37
+ .replace(/\./g, '_')
38
+ .replace(/\$/g, '_')
39
+ .replace(/'/g, '_')
40
+ .replace(/&/g, '_')
41
+ .replace(/%/g, '_')
42
+ .replace(/\//g, '_')
43
+ .replace(/\|/g, '_')
44
+ .toLowerCase()
45
+ )
46
+ }
47
+ export let decanonicalize = name => {
48
+ // Decanonicalize the var name.
49
+ name = name.replace(/^_/, '').replace(/_/g, ' ')
50
+ // Vensim variable names need to be surrounded by quotes if they:
51
+ // do not start with a letter
52
+ // do not contain only letters, spaces, numbers, single quotes, and dollar signs.
53
+ if (!name.match(/^[A-Za-z]/) || name.match(/[^A-Za-z0-9\s'$]/)) {
54
+ name = `"${name}"`
55
+ }
56
+ return name
57
+ }
58
+ export let cFunctionName = name => {
59
+ return canonicalName(name).toUpperCase()
60
+ }
61
+ export let isSeparatedVar = v => {
62
+ return v.separationDims.length > 0
63
+ }
64
+ export let newTmpVarName = () => {
65
+ // Return a unique temporary variable name
66
+ return `__t${nextTmpVarSeq++}`
67
+ }
68
+ export let newLookupVarName = () => {
69
+ // Return a unique lookup arg variable name
70
+ return `_lookup${nextLookupVarSeq++}`
71
+ }
72
+ export let newFixedDelayVarName = () => {
73
+ // Return a unique fixed delay variable name
74
+ return `_fixed_delay${nextFixedDelayVarSeq++}`
75
+ }
76
+ export let newLevelVarName = (basename = null, levelNumber = 0) => {
77
+ // Return a unique level variable name.
78
+ let levelName = basename || nextLevelVarSeq++
79
+ if (levelNumber) {
80
+ levelName += `_${levelNumber}`
81
+ }
82
+ return `_level${levelName}`
83
+ }
84
+ export let newAuxVarName = (basename = null, auxNumber = 0) => {
85
+ // Return a unique aux variable name.
86
+ let auxName = basename || nextAuxVarSeq++
87
+ if (auxNumber) {
88
+ auxName += `_${auxNumber}`
89
+ }
90
+ return `_aux${auxName}`
91
+ }
92
+ export let isSmoothFunction = fn => {
93
+ // Return true if fn is a Vensim smooth function.
94
+ return fn === '_SMOOTH' || fn === '_SMOOTHI' || fn === '_SMOOTH3' || fn === '_SMOOTH3I'
95
+ }
96
+ export let isTrendFunction = fn => {
97
+ // Return true if fn is a Vensim trend function.
98
+ return fn === '_TREND'
99
+ }
100
+ export let isNpvFunction = fn => {
101
+ // Return true if fn is a Vensim NPV function.
102
+ return fn === '_NPV'
103
+ }
104
+ export let isDelayFunction = fn => {
105
+ // Return true if fn is a Vensim delay function.
106
+ return fn === '_DELAY1' || fn === '_DELAY1I' || fn === '_DELAY3' || fn === '_DELAY3I'
107
+ }
108
+ export let isArrayFunction = fn => {
109
+ // Return true if fn is a Vensim array function.
110
+ return fn === '_SUM' || fn === '_VECTOR_SELECT' || fn === '_VMAX' || fn === '_VMIN'
111
+ }
112
+ export let listConcat = (a, x, addSpaces = false) => {
113
+ // Append a string x to string a with comma delimiters
114
+ let s = addSpaces ? ' ' : ''
115
+ if (R.isEmpty(x)) {
116
+ return a
117
+ } else {
118
+ return a + (R.isEmpty(a) ? '' : `,${s}`) + x
119
+ }
120
+ }
121
+ export let cdbl = x => {
122
+ // Convert a number into a C double constant.
123
+ let s = x.toString()
124
+ if (!s.includes('.') && !s.toLowerCase().includes('e')) {
125
+ s += '.0'
126
+ }
127
+ return s
128
+ }
129
+ export let strToConst = c => {
130
+ let str = matchRegex(c, /'(.*)'/)
131
+ if (str) {
132
+ // Convert a Vensim string constant into a C string literal.
133
+ return `"${str}"`
134
+ } else {
135
+ // Parse the string into a float.
136
+ let d = parseFloat(c)
137
+ return cdbl(d)
138
+ }
139
+ }
140
+ export let first = a => R.head(a)
141
+ export let rest = a => R.tail(a)
142
+ export let extractMatch = (fn, list) => {
143
+ // Return the first element of a list that matches the predicate and remove it from the list,
144
+ // or return undefined if no element matches.
145
+ let i = R.findIndex(fn, list)
146
+ if (i >= 0) {
147
+ return list.splice(i, 1)[0]
148
+ } else {
149
+ return undefined
150
+ }
151
+ }
152
+ export let replaceInArray = (oldStr, newStr, a) => {
153
+ // Replace the first occurrence of oldStr with newStr in an array of strings a.
154
+ // A new array is constructed. The original array remains unchanged.
155
+ let i = R.indexOf(oldStr, a)
156
+ if (i >= 0) {
157
+ let b = a.slice(0)
158
+ b.splice(i, 1, newStr)
159
+ return b
160
+ } else {
161
+ return a
162
+ }
163
+ }
164
+ export let mapObjProps = (f, obj) => {
165
+ // Map the key and value for each of the object's properties through function f.
166
+ let result = {}
167
+ R.forEach(k => (result[f(k)] = f(obj[k])), Object.keys(obj))
168
+ return result
169
+ }
170
+ export let isIterable = obj => {
171
+ // Return true of the object is iterable.
172
+ if (obj == null) {
173
+ return false
174
+ }
175
+ return typeof obj[Symbol.iterator] === 'function'
176
+ }
177
+ export let stringToId = str => {
178
+ // Look up a string id. Create the id from the string if it is not found.
179
+ let stringIndex = R.indexOf(str, strings)
180
+ if (stringIndex < 0) {
181
+ stringIndex = strings.length
182
+ strings.push(str)
183
+ }
184
+ return `id${stringIndex}`
185
+ }
186
+ // Command helpers
187
+ export let readXlsx = pathname => {
188
+ return XLSX.readFile(pathname, { cellDates: true })
189
+ }
190
+ export let readCsv = (pathname, delimiter = ',') => {
191
+ // Read the CSV file at the pathname and parse it with the given delimiter.
192
+ // Return an array of rows that are each an array of columns.
193
+ // If there is a header row, it is returned as the first row.
194
+ // Cache parsed files to support multiple reads from different equations.
195
+ let csv = csvData.get(pathname)
196
+ if (csv == null) {
197
+ const CSV_PARSE_OPTS = {
198
+ delimiter,
199
+ columns: false,
200
+ trim: true,
201
+ skip_empty_lines: true,
202
+ skip_lines_with_empty_values: true
203
+ }
204
+ let data = B.read(pathname)
205
+ csv = parseCsv(data, CSV_PARSE_OPTS)
206
+ csvData.set(pathname, csv)
207
+ }
208
+ return csv
209
+ }
210
+ // Convert the var name and subscript names to canonical form separately.
211
+ export let canonicalVensimName = vname => {
212
+ let result = vname
213
+ let m = vname.match(/([^[]+)(?:\[([^\]]+)\])?/)
214
+ if (m) {
215
+ result = canonicalName(m[1])
216
+ if (m[2]) {
217
+ let subscripts = m[2].split(',').map(x => canonicalName(x))
218
+ result += `[${subscripts.join(',')}]`
219
+ }
220
+ }
221
+ return result
222
+ }
223
+ // Split a model string into an array of equations without the "|" terminator.
224
+ // Allow "|" to occur in quoted variable names across line breaks.
225
+ // Retain the backslash character.
226
+ export let splitEquations = mdl => {
227
+ return split(mdl, { separator: '|', quotes: ['"'], keep: () => true })
228
+ }
229
+ // Function to map over lists's value and index
230
+ export let mapIndexed = R.addIndex(R.map)
231
+ // Function to sort an array of strings
232
+ export let asort = R.sort((a, b) => (a > b ? 1 : a < b ? -1 : 0))
233
+ // Function to alpha sort an array of variables on the model LHS
234
+ export let vsort = R.sort((a, b) => (a.modelLHS > b.modelLHS ? 1 : a.modelLHS < b.modelLHS ? -1 : 0))
235
+ // Function to list an array to stderr
236
+ export let printArray = R.forEach(x => console.error(x))
237
+ // Function to expand an array of strings into a comma-delimited list of strings
238
+ export let strlist = a => {
239
+ return a.join(', ')
240
+ }
241
+ // Function to join an array with newlines
242
+ export let lines = R.join('\n')
243
+ // Match a string against a regular expression and return the first match.
244
+ // If a capturing group was present, return the first group, otherwise
245
+ // return the entire match. If the string did not match, return the empty string.
246
+ export let matchRegex = (str, regex) => {
247
+ let m = str.match(regex)
248
+ if (!m) {
249
+ return ''
250
+ } else if (m.length > 1) {
251
+ return m[1]
252
+ } else if (m.length > 0) {
253
+ return m[0]
254
+ }
255
+ }
256
+ // Match a string against a regular expression with capture groups.
257
+ // Return an array of matches for each capture group.
258
+ // If the string did not match, return the empty string.
259
+ export let matchRegexCaptures = (str, regex) => {
260
+ let m = str.match(regex)
261
+ if (m && m.length > 0) {
262
+ return m.splice(1)
263
+ } else {
264
+ return []
265
+ }
266
+ }
267
+ // Match delimiters recursively. Replace delimited strings globally.
268
+ export let replaceDelimitedStrings = (str, open, close, newStr) => {
269
+ // str is the string to operate on.
270
+ // open and close are the opening and closing delimiter characters.
271
+ // newStr is the string to replace delimited substrings with.
272
+ let result = ''
273
+ let start = 0
274
+ let depth = 0
275
+ let n = str.length
276
+ for (let i = 0; i < n; i++) {
277
+ if (str.charAt(i) === open) {
278
+ if (depth === 0) {
279
+ result += str.substring(start, i)
280
+ }
281
+ depth++
282
+ } else if (str.charAt(i) === close && depth > 0) {
283
+ depth--
284
+ if (depth === 0) {
285
+ result += newStr
286
+ start = i + 1
287
+ }
288
+ }
289
+ }
290
+ if (start < n) {
291
+ result += str.substring(start)
292
+ }
293
+ return result
294
+ }
295
+
296
+ /**
297
+ * Return the cartesian product of the given array of arrays.
298
+ *
299
+ * For example, if we have an array that lists out two dimensions:
300
+ * [ ['a1','a2'], ['b1','b2','b3'] ]
301
+ * this function will return all the combinations, e.g.:
302
+ * [ ['a1', 'b1'], ['a1', 'b2'], ['a1', 'b3'], ['a2', 'b1'], ... ]
303
+ *
304
+ * This can be used in place of nested for loops and has the benefit of working
305
+ * for multi-dimensional inputs.
306
+ */
307
+ export const cartesianProductOf = arr => {
308
+ // Implementation based on: https://stackoverflow.com/a/36234242
309
+ return arr.reduce(
310
+ (a, b) => {
311
+ return a.map(x => b.map(y => x.concat([y]))).reduce((v, w) => v.concat(w), [])
312
+ },
313
+ [[]]
314
+ )
315
+ }
316
+
317
+ /**
318
+ * Return all possible permutations of the given array elements.
319
+ *
320
+ * For example, if we have an array of numbers:
321
+ * [1,2,3]
322
+ * this function will return all the permutations, e.g.:
323
+ * [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
324
+ */
325
+ export const permutationsOf = (elems, subperms = [[]]) => {
326
+ // Implementation based on: https://gist.github.com/CrossEye/f7c2f77f7db7a94af209
327
+ return R.isEmpty(elems)
328
+ ? subperms
329
+ : R.addIndex(R.chain)(
330
+ (elem, idx) => permutationsOf(R.remove(idx, 1, elems), R.map(R.append(elem), subperms)),
331
+ elems
332
+ )
333
+ }
334
+
335
+ //
336
+ // Debugging helpers
337
+ //
338
+ export let vlog = (title, value, depth = 1) => {
339
+ if (value) {
340
+ console.error(title, ':', util.inspect(value, { depth: depth, colors: false }))
341
+ } else {
342
+ console.error(title)
343
+ }
344
+ if (PRINT_VLOG_TRACE) {
345
+ console.trace()
346
+ }
347
+ }
348
+ export let abend = error => {
349
+ console.error(error)
350
+ process.exit(1)
351
+ }
@@ -0,0 +1,71 @@
1
+ import fs from 'fs'
2
+ import B from 'bufx'
3
+ import byline from 'byline'
4
+ import R from 'ramda'
5
+
6
+ import { canonicalVensimName } from './helpers.js'
7
+
8
+ /**
9
+ * Read a Vensim `dat` file with static data and return a Map.
10
+ * Each dataset consists of a key (the variable name in the canonical
11
+ * format used by SDE) and a map of time/value pairs.
12
+ *
13
+ * @param pathname The absolute path to the dat file.
14
+ * @param prefix An optional prefix string prepended to var names.
15
+ * @return A Map containing the datasets.
16
+ */
17
+ export async function readDat(pathname, prefix = '') {
18
+ let log = new Map()
19
+ let varName = ''
20
+ let varValues = new Map()
21
+ let lineNum = 1
22
+
23
+ let splitDatLine = line => {
24
+ const f = line.split('\t').map(s => s.trim())
25
+ if (f.length < 2 || !R.isEmpty(f[1])) {
26
+ return f
27
+ } else {
28
+ return [f[0]]
29
+ }
30
+ }
31
+
32
+ let addValues = () => {
33
+ if (varName !== '' && varValues.size > 0) {
34
+ log.set(prefix + varName, varValues)
35
+ }
36
+ }
37
+
38
+ return new Promise(resolve => {
39
+ let stream = byline(fs.createReadStream(pathname, 'utf8'))
40
+ stream.on('data', line => {
41
+ let values = splitDatLine(line)
42
+ if (values.length === 1) {
43
+ // Lines with a single value are variable names that start a data section.
44
+ // Save the values for the current var if we are not on the first one.
45
+ addValues()
46
+ // Start a new map for this var.
47
+ // Convert the var name to canonical form so it is the same in both logs.
48
+ varName = canonicalVensimName(values[0])
49
+ varValues = new Map()
50
+ } else if (values.length > 1) {
51
+ // Data lines in Vensim DAT format have {time}\t{value} format with optional comments afterward.
52
+ let t = B.num(values[0])
53
+ let value = B.num(values[1])
54
+ // Save the value at time t in the varValues map.
55
+ if (Number.isNaN(t)) {
56
+ console.error(`DAT file ${pathname}:${lineNum} time value is NaN`)
57
+ } else if (Number.isNaN(value)) {
58
+ console.error(`DAT file ${pathname}:${lineNum} var "${varName}" value is NaN at time=${t}`)
59
+ } else {
60
+ varValues.set(t, value)
61
+ }
62
+ }
63
+ lineNum++
64
+ // if (lineNum % 1e5 === 0) console.log(num(lineNum).format('0,0'))
65
+ })
66
+ stream.on('end', () => {
67
+ addValues()
68
+ resolve(log)
69
+ })
70
+ })
71
+ }