@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,32 @@
1
+ // Copyright (c) 2022 Climate Interactive / New Venture Fund
2
+
3
+ import antlr4 from 'antlr4'
4
+ import { ModelLexer, ModelParser } from 'antlr4-vensim'
5
+
6
+ /**
7
+ * Create a `ModelParser` for the given model text, which can be the
8
+ * contents of an entire `mdl` file, or a portion of one (e.g., an
9
+ * expression or definition).
10
+ *
11
+ * @param input The string containing the model text.
12
+ * @return A `ModelParser` from which a parse tree can be obtained.
13
+ */
14
+ export function createParser(input) {
15
+ let chars = new antlr4.InputStream(input)
16
+ let lexer = new ModelLexer(chars)
17
+ let tokens = new antlr4.CommonTokenStream(lexer)
18
+ let parser = new ModelParser(tokens)
19
+ parser.buildParseTrees = true
20
+ return parser
21
+ }
22
+
23
+ /**
24
+ * Read the given model text and return a parse tree.
25
+ *
26
+ * @param input The string containing the model text.
27
+ * @return A parse tree representation of the model.
28
+ */
29
+ export function parseModel(input) {
30
+ let parser = createParser(input)
31
+ return parser.model()
32
+ }
@@ -0,0 +1,114 @@
1
+ // Copyright (c) 2022 Climate Interactive / New Venture Fund
2
+
3
+ import path from 'path'
4
+ import B from 'bufx'
5
+
6
+ import { readXlsx } from './_shared/helpers.js'
7
+ import { readDat } from './_shared/read-dat.js'
8
+ import { printSubscripts, yamlSubsList } from './_shared/subscript.js'
9
+ import { parseModel } from './parse/parser.js'
10
+ import Model from './model/model.js'
11
+ import { generateCode } from './generate/code-gen.js'
12
+
13
+ /**
14
+ * Parse a Vensim model and generate C code.
15
+ *
16
+ * This is the primary entrypoint for the `sde generate` command.
17
+ *
18
+ * - If `operation` is 'generateC', the generated C code will be written to `buildDir`.
19
+ * - If `operation` is 'printVarList', variables and subscripts will be written to
20
+ * txt and yaml files under `buildDir`.
21
+ * - If `operation` is 'printRefIdTest', reference identifiers will be printed to the console.
22
+ * - If `operation` is 'convertNames', no output will be generated, but the results of model
23
+ * analysis will be available.
24
+ *
25
+ * @param input The preprocessed Vensim model text.
26
+ * @param spec The model spec (from the JSON file).
27
+ * @param operation Either 'generateC', 'printVarList', 'printRefIdTest', 'convertNames',
28
+ * or empty string.
29
+ * @param modelDirname The absolute path to the directory containing the mdl file.
30
+ * The dat and xlsx files referenced by the spec will be relative to this directory.
31
+ * @param modelName The model name (without the mdl extension).
32
+ * @param buildDir The output directory where the C or list files will be written.
33
+ * @return A string containing the generated C code.
34
+ */
35
+ export async function parseAndGenerate(input, spec, operation, modelDirname, modelName, buildDir) {
36
+ // Read time series from external DAT files into a single object.
37
+ // externalDatfiles is an array of either filenames or objects
38
+ // giving a variable name prefix as the key and a filename as the value.
39
+ let extData = new Map()
40
+ if (spec.externalDatfiles) {
41
+ for (let datfile of spec.externalDatfiles) {
42
+ let prefix = ''
43
+ let filename = ''
44
+ if (typeof datfile === 'object') {
45
+ prefix = Object.keys(datfile)[0]
46
+ filename = datfile[prefix]
47
+ } else {
48
+ filename = datfile
49
+ }
50
+ let pathname = path.join(modelDirname, filename)
51
+ let data = await readDat(pathname, prefix)
52
+ extData = new Map([...extData, ...data])
53
+ }
54
+ }
55
+
56
+ // Attach Excel workbook data to directData entries by file name.
57
+ let directData = new Map()
58
+ if (spec.directData) {
59
+ for (let [file, xlsxFilename] of Object.entries(spec.directData)) {
60
+ let pathname = path.join(modelDirname, xlsxFilename)
61
+ directData.set(file, readXlsx(pathname))
62
+ }
63
+ }
64
+
65
+ // Parse the model and generate code.
66
+ let parseTree = parseModel(input)
67
+ let code = generateCode(parseTree, { spec, operation, extData, directData, modelDirname })
68
+
69
+ function writeOutput(filename, text) {
70
+ let outputPathname = path.join(buildDir, filename)
71
+ B.write(text, outputPathname)
72
+ }
73
+
74
+ if (operation === 'generateC') {
75
+ // Write the generated C to a file
76
+ writeOutput(`${modelName}.c`, code)
77
+ }
78
+
79
+ if (operation === 'printVarList') {
80
+ // Write variables to a text file.
81
+ writeOutput(`${modelName}_vars.txt`, Model.printVarList())
82
+ // Write subscripts to a text file.
83
+ writeOutput(`${modelName}_subs.txt`, printSubscripts())
84
+ // Write variables to a YAML file.
85
+ writeOutput(`${modelName}_vars.yaml`, Model.yamlVarList())
86
+ // Write subscripts to a YAML file.
87
+ writeOutput(`${modelName}_subs.yaml`, yamlSubsList())
88
+ }
89
+
90
+ return code
91
+ }
92
+
93
+ /**
94
+ * Read the variable names from the given file, convert them to their
95
+ * C or Vensim representation, and print the results to the console.
96
+ *
97
+ * This is used only to implement the `sde names` command.
98
+ *
99
+ * @param namesPathname The path to the file containing variables names.
100
+ * @param operation Either 'to-c' or 'to-vensim'.
101
+ */
102
+ export function printNames(namesPathname, operation) {
103
+ let lines = B.lines(B.read(namesPathname))
104
+ for (let line of lines) {
105
+ if (line.length > 0) {
106
+ if (operation === 'to-c') {
107
+ B.emitLine(Model.cName(line))
108
+ } else {
109
+ B.emitLine(Model.vensimName(line))
110
+ }
111
+ }
112
+ }
113
+ B.printBuf()
114
+ }
@@ -0,0 +1,247 @@
1
+ import path from 'path'
2
+ import B from 'bufx'
3
+ import R from 'ramda'
4
+ import { splitEquations, replaceDelimitedStrings } from '../_shared/helpers.js'
5
+
6
+ export let preprocessModel = (mdlFilename, spec, profile = 'genc', writeFiles = false, outDecls = []) => {
7
+ const MACROS_FILENAME = 'macros.txt'
8
+ const REMOVALS_FILENAME = 'removals.txt'
9
+ const INSERTIONS_FILENAME = 'mdl-edits.txt'
10
+ const ENCODING = '{UTF-8}'
11
+ let profiles = {
12
+ // simplified but still runnable model
13
+ genc: {
14
+ emitEncoding: true,
15
+ emitCommentMarkers: true,
16
+ joinFormulaLines: false
17
+ },
18
+ // even simpler model that does not run
19
+ analysis: {
20
+ emitEncoding: false,
21
+ emitCommentMarkers: false,
22
+ joinFormulaLines: true
23
+ }
24
+ }
25
+ let opts = profiles[profile]
26
+ let mdl, eqns
27
+ // Equations that contain a string in the removalKeys list in the spec file will be removed.
28
+ let removalKeys = (spec && spec.removalKeys) || []
29
+ // Optional insertions can be used to add expanded macros back into the model.
30
+ let insertions = ''
31
+ let getMdlFromPPBuf = () => {
32
+ // Reset the mdl string from the preprocessor buffer.
33
+ mdl = B.getBuf('pp')
34
+ B.clearBuf('pp')
35
+ }
36
+ let emitPP = str => {
37
+ if (str) {
38
+ B.emit(str, 'pp')
39
+ }
40
+ }
41
+ // Open output channels.
42
+ B.open('rm')
43
+ B.open('macros')
44
+ B.open('pp')
45
+ // Read the optional insertions file into the model unless we are doing a pass that writes removals.
46
+ try {
47
+ if (!writeFiles) {
48
+ let insPathname = path.join(path.dirname(mdlFilename), INSERTIONS_FILENAME)
49
+ insertions = B.read(insPathname)
50
+ }
51
+ } catch (error) {
52
+ // TODO: Handle error
53
+ }
54
+ // Read the model file.
55
+ try {
56
+ mdl = B.read(mdlFilename)
57
+ } catch (error) {
58
+ console.error(error.message)
59
+ return
60
+ }
61
+ // Remove the macro section.
62
+ let inMacroSection = false
63
+ for (let line of B.lines(mdl)) {
64
+ if (!inMacroSection && R.contains(':MACRO:', line)) {
65
+ B.emitLine(line, 'macros')
66
+ inMacroSection = true
67
+ } else if (inMacroSection) {
68
+ B.emitLine(line, 'macros')
69
+ if (R.contains(':END OF MACRO:', line)) {
70
+ B.emit('\n', 'macros')
71
+ inMacroSection = false
72
+ }
73
+ } else {
74
+ B.emitLine(line, 'pp')
75
+ }
76
+ }
77
+ getMdlFromPPBuf()
78
+
79
+ // Split the model into an array of equations and groups.
80
+ eqns = splitEquations(mdl)
81
+ // Remove some equations into the removals channel.
82
+ for (let eqn of eqns) {
83
+ if (R.contains('\\---/// Sketch', eqn)) {
84
+ // Skip everything starting with the first sketch section.
85
+ break
86
+ } else if (R.contains('********************************************************', eqn)) {
87
+ // Skip groups
88
+ } else if (R.contains('TABBED ARRAY', eqn) || R.any(x => R.contains(x, eqn), removalKeys)) {
89
+ // Remove tabbed arrays and equations containing removal key strings from the spec.
90
+ B.emit(eqn, 'rm')
91
+ B.emit('|', 'rm')
92
+ } else if (!R.isEmpty(eqn)) {
93
+ // Emit the equation.
94
+ emitPP(eqn)
95
+ emitPP('|')
96
+ }
97
+ }
98
+ getMdlFromPPBuf()
99
+
100
+ // Join lines continued with trailing backslash characters.
101
+ let prevLine = ''
102
+ for (let line of B.lines(mdl)) {
103
+ // Join a previous line with a backslash ending to the current line.
104
+ if (!R.isEmpty(prevLine)) {
105
+ line = prevLine + line.trim()
106
+ prevLine = ''
107
+ }
108
+ let continuation = line.match(/\\\s*$/)
109
+ if (continuation) {
110
+ // If there is a backslash ending on this line, save it without the backslash.
111
+ prevLine = line.substr(0, continuation.index).replace(/\s+$/, ' ')
112
+ } else {
113
+ // With no continuation on this line, go ahead and emit it.
114
+ B.emitLine(line, 'pp')
115
+ }
116
+ }
117
+ getMdlFromPPBuf()
118
+
119
+ // Emit the encoding line and optional insertions.
120
+ if (opts.emitEncoding) {
121
+ B.emitLine(ENCODING, 'pp')
122
+ B.emitLine('', 'pp')
123
+ }
124
+ if (insertions) {
125
+ B.emitLine(insertions, 'pp')
126
+ }
127
+
128
+ // Split into separate equations
129
+ eqns = splitEquations(mdl)
130
+
131
+ // Extract the LHS variable name for each equation, which we will use to sort
132
+ // the equations alphabetically
133
+ const unsorted = outDecls
134
+ for (let eqn of eqns) {
135
+ // Ignore the encoding
136
+ eqn = eqn.replace('{UTF-8}', '')
137
+ // Remove ":RAW:" flag; it is not needed by SDE and causes problems if left in
138
+ eqn = eqn.replace(/:RAW:/g, '')
139
+ // Remove inline comments
140
+ eqn = replaceDelimitedStrings(eqn, '{', '}', '')
141
+ // Remove whitespace
142
+ eqn = eqn.trim()
143
+ if (eqn.length > 0) {
144
+ // Remove newlines so that we look at the full equation as a single line
145
+ let line = eqn.replace(/\n/g, ' ').trim()
146
+ let kind
147
+ let key = line
148
+ // Remove everything after the comment delimiters
149
+ key = key.split('~')[0]
150
+ // Strip the ":INTERPOLATE:"; it should not be included in the key
151
+ key = key.replace(/:INTERPOLATE:/g, '')
152
+ if (key.includes('=')) {
153
+ // The line contains an '='; treat this as an equation
154
+ kind = 'eqn'
155
+ key = key.split('=')[0].trim()
156
+ } else if (key.includes(':')) {
157
+ // The line contains a ':'; treat this as an subscript declaration
158
+ kind = 'sub'
159
+ key = key.split(':')[0].trim()
160
+ } else {
161
+ // Treat this as a general declaration
162
+ kind = 'decl'
163
+ }
164
+ // Ignore double quotes
165
+ key = key.replace(/"/g, '')
166
+ // Ignore the lookup data if it starts on the first line
167
+ key = key.split('(')[0]
168
+ // Ignore any whitespace that remains
169
+ key = key.trim()
170
+ // Remove whitespace on the inside of the brackets
171
+ key = key.replace(/\[\s*/g, '[')
172
+ key = key.replace(/\s*\]/g, ']')
173
+ // Ignore case
174
+ key = key.toLowerCase()
175
+ unsorted.push({
176
+ key,
177
+ kind,
178
+ originalDecl: eqn
179
+ })
180
+ }
181
+ }
182
+
183
+ // Sort the equations alphabetically by LHS variable name
184
+ const sorted = unsorted.sort((a, b) => {
185
+ return a.key < b.key ? -1 : a.key > b.key ? 1 : 0
186
+ })
187
+
188
+ // Emit formula lines without comment contents.
189
+ for (const elem of sorted) {
190
+ const eqn = elem.originalDecl
191
+ let processedDecl = eqn
192
+ let iComment = eqn.indexOf('~')
193
+ if (iComment >= 0) {
194
+ processedDecl = ''
195
+ let formula = B.lines(eqn.substr(0, iComment))
196
+ for (let i = 0; i < formula.length; i++) {
197
+ let line = formula[i]
198
+ // Remove trailing whitespace
199
+ line = line.replace(/\s+$/, '')
200
+ if (i === 0) {
201
+ if (line !== ENCODING) {
202
+ emitPP(line)
203
+ processedDecl += line
204
+ }
205
+ } else {
206
+ if (opts.joinFormulaLines) {
207
+ // Remove any leading tabs
208
+ const lineWithoutLeadingTabs = line.replace(/^\t+/, '')
209
+ emitPP(lineWithoutLeadingTabs)
210
+ processedDecl += lineWithoutLeadingTabs
211
+ } else {
212
+ // Only emit the line if it has non-whitespace characters
213
+ if (line.length > 0) {
214
+ emitPP(`\n${line}`)
215
+ processedDecl += `\n${line}`
216
+ }
217
+ }
218
+ }
219
+ }
220
+ // Emit the last line
221
+ if (opts.emitCommentMarkers) {
222
+ const declEnd = '\n\t~~|'
223
+ B.emitLine(`${declEnd}\n`, 'pp')
224
+ processedDecl += declEnd
225
+ } else {
226
+ B.emitLine('', 'pp')
227
+ }
228
+ }
229
+ elem.processedDecl = processedDecl
230
+ }
231
+ getMdlFromPPBuf()
232
+
233
+ // Write removals to a file in the model directory.
234
+ if (writeFiles) {
235
+ if (B.getBuf('macros')) {
236
+ let macrosPathname = path.join(path.dirname(mdlFilename), MACROS_FILENAME)
237
+ B.writeBuf(macrosPathname, 'macros')
238
+ }
239
+ if (B.getBuf('rm')) {
240
+ let rmPathname = path.join(path.dirname(mdlFilename), REMOVALS_FILENAME)
241
+ B.writeBuf(rmPathname, 'rm')
242
+ }
243
+ }
244
+
245
+ // Return the preprocessed model as a string.
246
+ return mdl
247
+ }