@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 +21 -0
- package/README.md +12 -0
- package/package.json +40 -0
- package/src/_shared/helpers.js +351 -0
- package/src/_shared/read-dat.js +71 -0
- package/src/_shared/subscript.js +373 -0
- package/src/generate/code-gen.js +356 -0
- package/src/generate/equation-gen.js +1188 -0
- package/src/generate/loop-index-vars.js +27 -0
- package/src/generate/model-lhs-reader.js +88 -0
- package/src/index.js +8 -0
- package/src/model/equation-reader.js +708 -0
- package/src/model/expr-reader.js +202 -0
- package/src/model/model.js +1045 -0
- package/src/model/subscript-range-reader.js +123 -0
- package/src/model/toposort.js +100 -0
- package/src/model/var-name-reader.js +40 -0
- package/src/model/variable-reader.js +172 -0
- package/src/model/variable.js +111 -0
- package/src/parse/model-reader.js +141 -0
- package/src/parse/parser.js +32 -0
- package/src/parse-and-generate.js +114 -0
- package/src/preprocess/preprocessor.js +247 -0
|
@@ -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
|
+
}
|