@sdeverywhere/compile 0.7.9 → 0.7.11

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,89 @@
1
+ import { cartesianProductOf, cdbl } from '../_shared/helpers.js'
2
+ import { isDimension, normalizeSubscripts, sub } from '../_shared/subscript.js'
3
+
4
+ /**
5
+ * Generate code for a single element in a const list definition.
6
+ *
7
+ * @param {*} variable The `Variable` instance to process.
8
+ * @param {*} parsedEqn The parsed equation.
9
+ * @return {string[]} An array of strings containing the generated C code for the variable,
10
+ * one string per line of code.
11
+ */
12
+ export function generateConstListElement(variable, parsedEqn) {
13
+ // In the "read variables" phase, const lists are expanded into separated variable
14
+ // definitions, so `variable` here will have `subscripts` that represent specific
15
+ // subscript indices in normalized order (alphabetized by parent dimension/family
16
+ // name). However, we need to consult the LHS subscripts/dimensions, which will
17
+ // be in the original order from the model equation.
18
+ //
19
+ // In the following example,
20
+ // we have a 2D variable whose original dimensions are not in normal order:
21
+ // DimA: A1, A2 ~~|
22
+ // DimB: B1, B2, B3 ~~|
23
+ // x[DimB, DimA] = 1, 2; 3, 4; 5, 6; ~~|
24
+ //
25
+ // The variable `x` will have been separated into:
26
+ // x[B1,A1]
27
+ // x[B1,A2]
28
+ // x[B2,A1]
29
+ // ...
30
+ //
31
+ // Each one will refer to a single element from the original const list. To determine
32
+ // which element in the const list goes with which variable instance, we build an array
33
+ // of all subscript combinations and then find the index of the one that matches the
34
+ // combination used for the separated variable instance.
35
+ const lhsSubRefs = variable.parsedEqn.lhs.varDef.subscriptRefs
36
+ const lhsSubIds = lhsSubRefs.map(subRef => subRef.subId)
37
+ const subIdArrays = lhsSubIds.map(subOrDimId => {
38
+ if (isDimension(subOrDimId)) {
39
+ // Use the full array of subscripts (indexes) for the dimension at this position
40
+ return sub(subOrDimId).value
41
+ } else {
42
+ // This is a single subscript (index), so use an array with a single element
43
+ return [subOrDimId]
44
+ }
45
+ })
46
+
47
+ // Continuing with the above example, at this point we will have a 2D array:
48
+ // [
49
+ // [_b1,_b2,_b3],
50
+ // [_a1,_a2]
51
+ // ]
52
+ // We expand these into the set of all combinations of subscripts in the original
53
+ // order of the dimensions from the equation LHS.
54
+ const origCombos = cartesianProductOf(subIdArrays)
55
+
56
+ // Now we have the combinations in original order:
57
+ // [_b1,_a1]
58
+ // [_b1,_a2]
59
+ // [_b2,_a1]
60
+ // ...
61
+ // But we need to put them into normalized order so that we can find the index of
62
+ // `variable.subscripts` (which is already in normalized order).
63
+ const normalizedCombos = origCombos.map(normalizeSubscripts)
64
+
65
+ // Convert to strings to make matching easier. Now we have the strings in normalized order:
66
+ // [_a1,_b1]
67
+ // [_a2,_b1]
68
+ // [_a1,_b2]
69
+ // ...
70
+ const comboStrings = normalizedCombos.map(combo => combo.map(subId => `[${subId}]`).join(''))
71
+
72
+ // Convert `variable.subscripts` into the same format so that we can do an array lookup,
73
+ // for example if this separated variable instance is x[_a2,_b1], this will be:
74
+ // [_a2,_b1]
75
+ const lhsComboString = variable.subscripts.map(subId => `[${subId}]`).join('')
76
+
77
+ // Find the index of the combination that matches `variable.subscripts`
78
+ const constIndex = comboStrings.indexOf(lhsComboString)
79
+ if (constIndex < 0) {
80
+ throw new Error(`Failed to determine index of const list element for ${variable.refId}`)
81
+ }
82
+
83
+ // Determine the LHS and RHS of the const assignment
84
+ const lhsVarId = variable.varName
85
+ const lhsIndicesString = variable.subscripts.map(subId => `[${sub(subId).value}]`).join('')
86
+ const lhsRef = `${lhsVarId}${lhsIndicesString}`
87
+ const rhsConstValue = cdbl(parsedEqn.rhs.constants[constIndex].value)
88
+ return ` ${lhsRef} = ${rhsConstValue};`
89
+ }
@@ -0,0 +1,91 @@
1
+ import XLSX from 'xlsx'
2
+
3
+ import { cartesianProductOf } from '../_shared/helpers.js'
4
+ import { indexInSepDim, isDimension, sub } from '../_shared/subscript.js'
5
+
6
+ import { handleExcelOrCsvFile } from './direct-data-helpers.js'
7
+
8
+ /**
9
+ * Generate code for a variable that uses `GET DIRECT CONSTANTS` to source constant values from an external
10
+ * file (in CSV or Excel format).
11
+ *
12
+ * @param {*} variable The `Variable` instance to process.
13
+ * @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT CONSTANTS` call (e.g.,
14
+ * `?data`) to the tabular data contained in the loaded data file.
15
+ * @param {string} modelDir The path to the directory containing the model (used for resolving data files).
16
+ * @return {string[]} An array of strings containing the generated C code for the variable,
17
+ * one string per line of code.
18
+ */
19
+ export function generateDirectConstInit(variable, directData, modelDir) {
20
+ // Create a function that reads the CSV or XLS[X] content
21
+ let { file, tab, startCell } = variable.directConstArgs
22
+ let getCellValue = handleExcelOrCsvFile(file, tab, 'constants', directData, modelDir)
23
+
24
+ // Get C subscripts in text form for the LHS in normal order.
25
+ let lhsSubIds = variable.parsedEqn.lhs.varDef.subscriptRefs?.map(s => s.subId) || []
26
+ let modelDimNames = lhsSubIds.filter(s => isDimension(s))
27
+
28
+ // Generate offsets from the start cell in the table corresponding to LHS indices.
29
+ let cellOffsets = []
30
+ let cSubscripts = variable.subscripts.map(s => (isDimension(s) ? sub(s).value : [s]))
31
+ let lhsIndexSubscripts = cartesianProductOf(cSubscripts)
32
+
33
+ // Find the table cell offset for each LHS index tuple.
34
+ let lines = []
35
+ for (let indexSubscripts of lhsIndexSubscripts) {
36
+ let entry = [null, null]
37
+ for (let i = 0; i < variable.subscripts.length; i++) {
38
+ // LHS dimensions or indices in a separated dimension map to table cells.
39
+ let lhsSubscript = variable.subscripts[i]
40
+ if (isDimension(lhsSubscript) || indexInSepDim(lhsSubscript, variable)) {
41
+ // Consider the LHS index subscript at this position.
42
+ let indexSubscript = indexSubscripts[i]
43
+ let ind = sub(indexSubscript)
44
+ // Find the model subscript position corresponding to the LHS index subscript.
45
+ for (let iModelDim = 0; iModelDim < modelDimNames.length; iModelDim++) {
46
+ // Only fill an entry position once.
47
+ if (entry[iModelDim] === null) {
48
+ let modelDim = sub(modelDimNames[iModelDim])
49
+ if (modelDim.family === ind.family) {
50
+ // Set the numeric index for the model dimension in the cell offset entry.
51
+ // Use the position within the dimension to map subdimensions onto cell offsets.
52
+ let pos = modelDim.value.indexOf(indexSubscript)
53
+ // Vectors use a 2D cell offset that maps to columns in the first row.
54
+ // Tables use a 2D cell offset with the row or column matching the model dimension.
55
+ let entryRowOrCol = modelDimNames.length > 1 ? iModelDim : 1
56
+ entry[entryRowOrCol] = pos
57
+ break
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ // Replace unfilled entry positions with zero.
64
+ entry = entry.map(x => (x === null ? 0 : x))
65
+ // Read values by column first when the start cell ends with an asterisk.
66
+ // Ref: https://www.vensim.com/documentation/fn_get_direct_constants.html
67
+ if (startCell.endsWith('*')) {
68
+ entry.reverse()
69
+ }
70
+ cellOffsets.push(entry)
71
+ }
72
+
73
+ // Read tabular data into an indexed variable for each cell.
74
+ let numericSubscripts = lhsIndexSubscripts.map(idx => idx.map(s => sub(s).value))
75
+ let lhsSubscripts = numericSubscripts.map(s => s.reduce((a, v) => a.concat(`[${v}]`), ''))
76
+ let dataAddress = XLSX.utils.decode_cell(startCell.toUpperCase())
77
+ let startCol = dataAddress.c
78
+ let startRow = dataAddress.r
79
+ if (startCol < 0 || startRow < 0) {
80
+ throw new Error(`Failed to parse 'cell' argument for GET DIRECT CONSTANTS call for ${variable.refId}: ${startCell}`)
81
+ }
82
+ for (let i = 0; i < cellOffsets.length; i++) {
83
+ let rowOffset = cellOffsets[i][0] ? cellOffsets[i][0] : 0
84
+ let colOffset = cellOffsets[i][1] ? cellOffsets[i][1] : 0
85
+ let dataValue = getCellValue(startCol + colOffset, startRow + rowOffset)
86
+ let lhs = `${variable.varName}${lhsSubscripts[i] || ''}`
87
+ lines.push(` ${lhs} = ${dataValue};`)
88
+ }
89
+
90
+ return lines
91
+ }
@@ -0,0 +1,273 @@
1
+ import {
2
+ dimensionNames,
3
+ hasMapping,
4
+ isDimension,
5
+ isIndex,
6
+ isTrivialDimension,
7
+ normalizeSubscripts,
8
+ separatedVariableIndex,
9
+ sub
10
+ } from '../_shared/subscript.js'
11
+ import { generateConstListElement } from './gen-const-list.js'
12
+
13
+ import { generateDirectConstInit } from './gen-direct-const.js'
14
+ import { generateExpr } from './gen-expr.js'
15
+ import { generateLookupsFromDirectData } from './gen-lookup-from-direct.js'
16
+ import { generateLookupsFromExternalData } from './gen-lookup-from-external.js'
17
+ import { generateLookupFromPoints } from './gen-lookup-from-points.js'
18
+
19
+ import LoopIndexVars from './loop-index-vars.js'
20
+
21
+ /**
22
+ * Generate C code for the given model equation.
23
+ *
24
+ * TODO: Variable type
25
+ * TODO: Define type for mode values
26
+ *
27
+ * @param {*} variable The `Variable` instance to process.
28
+ * @param {'decl' | 'init-constants' | 'init-lookups' | 'init-levels' | 'eval'} mode The code generation mode.
29
+ * @param {Map<string, any>} extData The map of datasets from external `.dat` files.
30
+ * @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT DATA` call (e.g.,
31
+ * `?data`) to the tabular data contained in the loaded data file.
32
+ * @param {string} modelDir The path to the directory containing the model (used for resolving data files).
33
+ * @return {string[]} An array of strings containing the generated C code for the variable,
34
+ * one string per line of code.
35
+ */
36
+ export function generateEquation(variable, mode, extData, directData, modelDir) {
37
+ // Maps of LHS subscript families to loop index vars for lookup on the RHS
38
+ const loopIndexVars = new LoopIndexVars(['i', 'j', 'k', 'l', 'm'])
39
+ const arrayIndexVars = new LoopIndexVars(['u', 'v', 'w', 's', 't', 'f', 'g', 'h', 'o', 'p', 'q', 'r'])
40
+
41
+ // Generate the LHS variable reference code
42
+ const parsedEqn = variable.parsedEqn
43
+ const cLhs = cVarRefWithLhsSubscripts(variable, parsedEqn.lhs.varDef.varId, loopIndexVars)
44
+
45
+ // Include the original model equation in a comment that comes before the generated code
46
+ // for that equation
47
+ const modelFormula = variable.origModelFormula || variable.modelFormula
48
+ const comment = ` // ${variable.modelLHS} = ${modelFormula.replace(/\n/g, '')}`
49
+
50
+ // Apply special handling for const lists
51
+ if (parsedEqn.rhs.kind === 'const-list') {
52
+ if (mode !== 'init-constants' && mode !== 'eval') {
53
+ throw new Error(`Invalid code gen mode '${mode}' for const list variable ${variable.modelLHS}`)
54
+ }
55
+ // XXX: The legacy code gen emitted a comment before each init statement, so we will
56
+ // do the same for now to maintain compatibility
57
+ return [comment, generateConstListElement(variable, parsedEqn)]
58
+ }
59
+
60
+ // Emit direct constants individually without separating them first
61
+ if (variable.directConstArgs) {
62
+ const initCode = generateDirectConstInit(variable, directData, modelDir)
63
+ return [comment, ...initCode]
64
+ }
65
+
66
+ // Get the dimension IDs for the LHS variable
67
+ const dimIds = dimensionNames(variable.subscripts)
68
+
69
+ // Turn each dimension ID into a loop with a loop index variable.
70
+ // If the variable has no subscripts, nothing will be emitted here.
71
+ const openLoops = []
72
+ const closeLoops = []
73
+ for (const dimId of dimIds) {
74
+ const indexName = loopIndexVars.index(dimId)
75
+ const dimLength = sub(dimId).size
76
+ openLoops.push(` for (size_t ${indexName} = 0; ${indexName} < ${dimLength}; ${indexName}++) {`)
77
+ closeLoops.push(' }')
78
+ }
79
+
80
+ // Apply special handling for data variables. The data can be defined in one of three ways:
81
+ // - as a set of explicit data points (stored in the `Variable` instance), or
82
+ // - from an external file via a `GET DIRECT DATA` call, or
83
+ // - from an external data file (i.e., a "normal" data variable)
84
+ if (variable.isData()) {
85
+ if (variable.points.length > 0) {
86
+ // The variable already has data points defined, so generate a new lookup using that data.
87
+ // Note that unlike the other lookup cases, this one needs to include loop open/close code
88
+ // if the variable is subscripted.
89
+ const lookupDef = generateLookupFromPoints(variable, mode, /*copy=*/ true, cLhs, loopIndexVars)
90
+ if (lookupDef.length > 0) {
91
+ return [comment, ...openLoops, ...lookupDef, ...closeLoops]
92
+ } else {
93
+ return []
94
+ }
95
+ } else if (variable.directDataArgs) {
96
+ // The data is referenced using a `GET DIRECT DATA` call; generate one or more lookups
97
+ // using the data defined in external files
98
+ return generateLookupsFromDirectData(variable, mode, directData, modelDir, cLhs)
99
+ } else {
100
+ // This is a "normal" data variable; generate one or more lookups using the data defined
101
+ // in external files
102
+ return generateLookupsFromExternalData(variable, mode, extData, cLhs)
103
+ }
104
+ }
105
+
106
+ // Apply special handling for lookup variables. The data for lookup variables is already
107
+ // defined as a set of explicit data points (stored in the `Variable` instance).
108
+ if (variable.isLookup()) {
109
+ return generateLookupFromPoints(variable, mode, /*copy=*/ false, cLhs, loopIndexVars)
110
+ }
111
+
112
+ // Keep a buffer of code that will be included before the innermost loop
113
+ const preInnerLoopLines = []
114
+
115
+ // Keep a buffer of code that will be included before the generated primary formula
116
+ const preFormulaLines = []
117
+
118
+ // Keep a buffer of code that will be included after the generated primary formula
119
+ const postFormulaLines = []
120
+
121
+ // Keep track of marked dimensions
122
+ const markedDimIds = new Set()
123
+
124
+ // Generate code for an equation with an expression on the RHS
125
+ const genExprCtx = {
126
+ variable,
127
+ mode,
128
+ cLhs,
129
+ loopIndexVars,
130
+ arrayIndexVars,
131
+ resetMarkedDims: () => markedDimIds.clear(),
132
+ addMarkedDim: dimId => markedDimIds.add(dimId),
133
+ emitPreInnerLoop: s => preInnerLoopLines.push(s),
134
+ emitPreFormula: s => preFormulaLines.push(s),
135
+ emitPostFormula: s => postFormulaLines.push(s),
136
+ cVarRef: varRef => cVarRef(variable, varRef, markedDimIds, loopIndexVars, arrayIndexVars),
137
+ cVarRefWithLhsSubscripts: baseVarId => cVarRefWithLhsSubscripts(variable, baseVarId, loopIndexVars),
138
+ cVarIndex: subOrDimId => cVarIndex(variable, [subOrDimId], subOrDimId, markedDimIds, loopIndexVars, arrayIndexVars)
139
+ }
140
+ const cRhs = generateExpr(parsedEqn.rhs.expr, genExprCtx)
141
+ const formula = ` ${cLhs} = ${cRhs};`
142
+
143
+ // Insert the pre-inner loop code, if needed
144
+ if (preInnerLoopLines.length > 0) {
145
+ openLoops.splice(variable.subscripts.length - 1, 0, ...preInnerLoopLines)
146
+ }
147
+
148
+ // Combine all lines of comments and code into a single array
149
+ return [comment, ...openLoops, ...preFormulaLines, formula, ...postFormulaLines, ...closeLoops]
150
+ }
151
+
152
+ /**
153
+ * Return the C code for a subscripted reference to the given variable using the LHS subscripts
154
+ * of the given equation.
155
+ *
156
+ * @param {*} lhsVariable The LHS `Variable` instance.
157
+ * @param {*} baseVarId The base variable ID to which the subscript parts will be appended.
158
+ * @param {LoopIndexVars} loopIndexVars The loop index state.
159
+ * @return {string} The C variable reference.
160
+ */
161
+ function cVarRefWithLhsSubscripts(lhsVariable, baseVarId, loopIndexVars) {
162
+ const lhsSubIds = lhsVariable.subscripts
163
+ const cSubParts = lhsSubIds.map(subId => {
164
+ if (isDimension(subId)) {
165
+ const i = loopIndexVars.index(subId)
166
+ if (isTrivialDimension(subId)) {
167
+ // When the dimension is trivial, we can simply emit e.g. `[i]` instead of `[_dim[i]]`
168
+ return `[${i}]`
169
+ } else {
170
+ return `[${subId}][${i}]`
171
+ }
172
+ } else {
173
+ return `[${sub(subId).value}]`
174
+ }
175
+ })
176
+ return `${baseVarId}${cSubParts.join('')}`
177
+ }
178
+
179
+ /**
180
+ * Return the C code for a RHS (possibly subscripted) variable reference.
181
+ *
182
+ * @param {*} lhsVariable The LHS `Variable` instance.
183
+ * @param {*} rhsVarRef The `VariableRef` used in a RHS expression.
184
+ * @param {Set<string>} markedDimIds The set of dimension IDs that are marked for use
185
+ * in an array function, for example `SUM(x[DimA!])`.
186
+ * @param {LoopIndexVars} loopIndexVars The loop index state.
187
+ * @param {LoopIndexVars} arrayIndexVars The loop index state used for array functions
188
+ * (that use marked dimensions).
189
+ * @returns {string} The C variable reference.
190
+ */
191
+ function cVarRef(lhsVariable, rhsVarRef, markedDimIds, loopIndexVars, arrayIndexVars) {
192
+ if (rhsVarRef.subscriptRefs === undefined) {
193
+ // No subscripts, so return the base variable ID
194
+ return rhsVarRef.varId
195
+ }
196
+
197
+ // Normalize the RHS subscripts
198
+ let rhsSubIds
199
+ try {
200
+ // XXX: For now, strip the mark here (need to revisit this)
201
+ rhsSubIds = normalizeSubscripts(rhsVarRef.subscriptRefs.map(subRef => subRef.subId.replace('!', '')))
202
+ } catch (e) {
203
+ throw new Error(`normalizeSubscripts failed in rhsVarRef: refId=${lhsVariable.refId} error=${e}`)
204
+ }
205
+
206
+ // Determine the subscript code (array lookup) for each dimension. For example, if
207
+ // the RHS variable reference in the model looks like `x[DimA]`, this will convert the
208
+ // `[DimA]` part to `[_dima[i]]` (or simply `[i]` if it is a "trivial" dimension).
209
+ const cSubParts = rhsSubIds.map(rhsSubId => {
210
+ return cVarIndex(lhsVariable, rhsSubIds, rhsSubId, markedDimIds, loopIndexVars, arrayIndexVars)
211
+ })
212
+
213
+ return `${rhsVarRef.varId}${cSubParts.map(part => `[${part}]`).join('')}`
214
+ }
215
+
216
+ /**
217
+ * Return the C code for indexing into a subscripted variable.
218
+ *
219
+ * @param {*} lhsVariable The LHS `Variable` instance.
220
+ * @param {string[]} rhsSubIds The set of all subscript or dimension IDs used on the RHS.
221
+ * @param {string} rhsSubId The specific subscript or dimension ID being evaluated.
222
+ * @param {Set<string>} markedDimIds The set of dimension IDs that are marked for use
223
+ * in an array function, for example `SUM(x[DimA!])`.
224
+ * @param {LoopIndexVars} loopIndexVars The loop index state.
225
+ * @param {LoopIndexVars} arrayIndexVars The loop index state used for array functions
226
+ * (that use marked dimensions).
227
+ * @returns {string} The C variable reference.
228
+ */
229
+ function cVarIndex(lhsVariable, rhsSubIds, rhsSubId, markedDimIds, loopIndexVars, arrayIndexVars) {
230
+ if (isIndex(rhsSubId)) {
231
+ // This is a specific subscript (i.e., an index); dereference the array using the index
232
+ // number of the subscript
233
+ return `${sub(rhsSubId).value}`
234
+ }
235
+
236
+ // Otherwise, this is a dimension. Get the corresponding loop index variable used
237
+ // in the "for" loop.
238
+ let indexName
239
+ if (markedDimIds.has(rhsSubId)) {
240
+ // This is a marked dimension as used in an array function (e.g., `SUM`), so use
241
+ // the name of the array loop index variable
242
+ indexName = arrayIndexVars.index(rhsSubId)
243
+ } else {
244
+ // Use the single index name for a separated variable if it exists
245
+ const separatedIndexName = separatedVariableIndex(rhsSubId, lhsVariable, rhsSubIds)
246
+ if (separatedIndexName) {
247
+ return `${sub(separatedIndexName).value}`
248
+ }
249
+
250
+ // See if we need to apply a mapping because the RHS dim is not found on the LHS
251
+ const found = lhsVariable.subscripts.findIndex(lhsSubId => sub(lhsSubId).family === sub(rhsSubId).family)
252
+ if (found < 0) {
253
+ // Find the mapping from the RHS subscript to a LHS subscript
254
+ for (const lhsSubId of lhsVariable.subscripts) {
255
+ if (hasMapping(rhsSubId, lhsSubId)) {
256
+ indexName = loopIndexVars.index(lhsSubId)
257
+ return `__map${rhsSubId}${lhsSubId}[${indexName}]`
258
+ }
259
+ }
260
+ }
261
+
262
+ // There is no mapping, so use the loop index for this dim family on the LHS
263
+ indexName = loopIndexVars.index(rhsSubId)
264
+ }
265
+
266
+ // Dereference the array using the corresponding loop index variable
267
+ if (isTrivialDimension(rhsSubId)) {
268
+ // When the dimension is trivial, we can emit e.g. `[i]` instead of `[_dim[i]]`
269
+ return `${indexName}`
270
+ } else {
271
+ return `${rhsSubId}[${indexName}]`
272
+ }
273
+ }