@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,77 @@
1
+ import XLSX from 'xlsx'
2
+
3
+ import { readCsv } from '../_shared/helpers.js'
4
+ import { Subscript } from '../_shared/subscript.js'
5
+
6
+ /**
7
+ * Read the dimension definitions from the given model.
8
+ *
9
+ * @param {*} parsedModel TODO: Use ParsedVensimModel type here
10
+ */
11
+ export function readDimensionDefs(parsedModel) {
12
+ // Read and process all dimension definitions from the parsed model
13
+ for (const dimensionDef of parsedModel.root.dimensions) {
14
+ const dimName = dimensionDef.dimName
15
+ const familyName = dimensionDef.familyName
16
+ if (dimensionDef.subscriptRefs.length > 0) {
17
+ // This is a normal dimension definition
18
+ const subNames = dimensionDef.subscriptRefs.map(ref => ref.subName)
19
+ const mappings = dimensionDef.subscriptMappings.map(mapping => {
20
+ // Convert from the AST representation (`SubscriptMapping`) to the structure used
21
+ // by the compiler
22
+ return {
23
+ toDim: mapping.toDimName,
24
+ value: mapping.subscriptRefs.map(ref => ref.subName)
25
+ }
26
+ })
27
+ Subscript(dimName, subNames, familyName, mappings)
28
+ } else {
29
+ // This is an alias (for example, `DimA <-> DimB`)
30
+ // XXX: The legacy Vensim parser set `modelValue` to an empty string (instead of an
31
+ // empty array) in the case of a `<->` alias, so we will do the same here for now.
32
+ // Once we remove the legacy parsing code we could fix the tests to expect an empty
33
+ // array instead of empty string.
34
+ Subscript(dimName, '', familyName, [])
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Read the subscripts for a `GET DIRECT SUBSCRIPT` call in a dimension definition.
41
+ *
42
+ * @param {string} filePath The absolute
43
+ * @param {string} tabOrDelimiter
44
+ * @param {string} firstCell The name of the first cell.
45
+ * @param {string} lastCell The name of the last cell.
46
+ */
47
+ export function getDirectSubscripts(fileName, tabOrDelimiter, firstCell, lastCell) {
48
+ // If lastCell is a column letter, scan the column, else scan the row
49
+ const dataAddress = XLSX.utils.decode_cell(firstCell.toUpperCase())
50
+ let col = dataAddress.c
51
+ let row = dataAddress.r
52
+ if (col < 0 || row < 0) {
53
+ throw new Error(`Failed to parse 'firstcell' argument for GET DIRECT SUBSCRIPT call: ${firstCell}`)
54
+ }
55
+ let nextCell
56
+ if (isNaN(parseInt(lastCell))) {
57
+ nextCell = () => row++
58
+ } else {
59
+ nextCell = () => col++
60
+ }
61
+
62
+ // Read subscript names from the CSV file at the given position
63
+ // TODO: We currently only support reading from CSV files, but Vensim also allows for
64
+ // Excel files for `GET DIRECT SUBSCRIPT`, so we should add support for those here
65
+ const data = readCsv(fileName, tabOrDelimiter)
66
+ const subNames = []
67
+ if (data) {
68
+ let subName = data[row][col]
69
+ while (subName != null) {
70
+ subNames.push(subName)
71
+ nextCell()
72
+ subName = data[row] != null ? data[row][col] : null
73
+ }
74
+ }
75
+
76
+ return subNames
77
+ }
@@ -0,0 +1,264 @@
1
+ import * as R from 'ramda'
2
+
3
+ import { toPrettyString } from '@sdeverywhere/parse'
4
+
5
+ import { cartesianProductOf } from '../_shared/helpers.js'
6
+ import {
7
+ isDimension,
8
+ isIndex,
9
+ isSubdimension,
10
+ normalizeSubscripts,
11
+ sub,
12
+ subscriptsMatch
13
+ } from '../_shared/subscript.js'
14
+
15
+ import Variable from './variable.js'
16
+
17
+ /**
18
+ * TODO: Docs
19
+ *
20
+ * @param {*} parsedModel TODO: Use ParsedVensimModel type here
21
+ * @param {Object.<string, string>} [specialSeparationDims] The variable names that need to be
22
+ * separated because of circular references. A mapping from "C" variable name to "C" dimension
23
+ * name to separate on.
24
+ * @returns {*} An array containing all `Variable` instances that were generated from
25
+ * the model equations.
26
+ */
27
+ export function readVariables(parsedModel, specialSeparationDims) {
28
+ const variables = []
29
+
30
+ // Add one or more `Variable` definitions for each parsed equation
31
+ for (const eqn of parsedModel.root.equations) {
32
+ variables.push(...variablesForEquation(eqn, specialSeparationDims || {}))
33
+ }
34
+
35
+ return variables
36
+ }
37
+
38
+ /**
39
+ * Process a single parsed equation and return one or more `Variable` definitions
40
+ * that were derived from the given equation.
41
+ *
42
+ * TODO: Types
43
+ *
44
+ * @param {*} eqn The parsed equation.
45
+ * @param {Object.<string, string>} specialSeparationDims The variable names that need to be
46
+ * separated because of circular references.
47
+ * @returns {*} An array containing all `Variable` instances that were generated from
48
+ * the given equation.
49
+ */
50
+ function variablesForEquation(eqn, specialSeparationDims) {
51
+ // Start a new variable defined by this equation
52
+ const variable = new Variable(null)
53
+
54
+ // Fill in the LHS details
55
+ const lhs = eqn.lhs.varDef
56
+ const baseVarId = lhs.varId
57
+ let lhsText
58
+ if (lhs.subscriptRefs?.length > 0) {
59
+ // Note that we use the original order of subscripts here, not the "normalized"
60
+ // order as below. (This is how the legacy parser worked, so we will preserve
61
+ // that behavior for now.)
62
+ const subNames = lhs.subscriptRefs.map(sub => sub.subName)
63
+ let exceptPart
64
+ if (lhs.exceptSubscriptRefSets) {
65
+ const exceptSets = lhs.exceptSubscriptRefSets.map(exceptSubRefs => {
66
+ const exceptSubNames = exceptSubRefs.map(sub => sub.subName)
67
+ return `[${exceptSubNames.join(',')}]`
68
+ })
69
+ exceptPart = `:EXCEPT:${exceptSets.join(',')}`
70
+ } else {
71
+ exceptPart = ''
72
+ }
73
+ lhsText = `${lhs.varName}[${subNames.join(',')}]${exceptPart}`
74
+ } else {
75
+ lhsText = lhs.varName
76
+ }
77
+ variable.modelLHS = lhsText
78
+ variable.varName = baseVarId
79
+
80
+ // Fill in the RHS details
81
+ let rhsText
82
+ if (eqn.rhs.kind === 'expr') {
83
+ rhsText = toPrettyString(eqn.rhs.expr, { compact: true })
84
+ } else if (eqn.rhs.kind === 'const-list') {
85
+ rhsText = eqn.rhs.text
86
+ } else {
87
+ rhsText = ''
88
+ if (eqn.rhs.kind === 'data') {
89
+ // The legacy parser sets the variable's varType to 'data' at this stage,
90
+ // so we will do the same
91
+ variable.varType = 'data'
92
+ }
93
+ }
94
+ variable.modelFormula = rhsText
95
+ variable.parsedEqn = eqn
96
+
97
+ // If the variable is subscripted, expand on the LHS subscripts
98
+ let expansions = []
99
+ if (lhs.subscriptRefs?.length > 0) {
100
+ // XXX: We use `normalizeSubscripts` here so that we are compatible with
101
+ // the legacy parser. It normalizes by putting the subscripts in alphabetical
102
+ // order by family. This approach to ordering may lead to issues in cases where
103
+ // there are multiple dimensions used that resolve to the same family, so we
104
+ // should revisit this.
105
+ const subIds = normalizeSubscripts(lhs.subscriptRefs.map(ref => ref.subId))
106
+ const exceptSubIdSets = []
107
+ if (lhs.exceptSubscriptRefSets?.length > 0) {
108
+ for (const exceptSubRefs of lhs.exceptSubscriptRefSets) {
109
+ exceptSubIdSets.push(normalizeSubscripts(exceptSubRefs.map(ref => ref.subId)))
110
+ }
111
+ }
112
+
113
+ // Determine which positions we will expand
114
+ let positionsToExpand
115
+ if (eqn.rhs.kind === 'const-list') {
116
+ // For const lists, we expand on all dimensions (unconditionally)
117
+ positionsToExpand = subIds.map(subId => isDimension(subId))
118
+ } else {
119
+ // For other equations, look at the different subscripts and the RHS to determine
120
+ // which positions to expand. Note that `specialSeparationDims` in the spec file
121
+ // can be a single string or an array of strings.
122
+ let separationDims = specialSeparationDims[baseVarId] || []
123
+ if (!Array.isArray(separationDims)) {
124
+ separationDims = [separationDims]
125
+ }
126
+ positionsToExpand = subscriptPositionsToExpand(subIds, exceptSubIdSets, separationDims, variable.modelFormula)
127
+ }
128
+
129
+ // Expand on LHS subscripts
130
+ expansions = computeExpansions(baseVarId, subIds, exceptSubIdSets, positionsToExpand)
131
+ }
132
+
133
+ if (expansions.length === 0) {
134
+ // Generate a single variable defined by the equation
135
+ return [variable]
136
+ } else {
137
+ // Generate variables expanded over subscripts to the model
138
+ const variables = []
139
+ for (const expansion of expansions) {
140
+ const v = new Variable(null)
141
+ v.varName = baseVarId
142
+ v.modelLHS = lhsText
143
+ v.modelFormula = rhsText
144
+ v.parsedEqn = eqn
145
+ v.subscripts = expansion.subIds
146
+ v.separationDims = expansion.separationDimIds
147
+ variables.push(v)
148
+ }
149
+ return variables
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Return an array of boolean flags that indicate whether the variable should be
155
+ * expanded over the subscripts for that position.
156
+ *
157
+ * TODO: Use correct types here
158
+ *
159
+ * @param {*} subIds The list of subscripts appearing on the LHS in normalized order.
160
+ * @param {*} exceptSubIdSets An array of subscript lists from the :EXCEPT: clause.
161
+ * @param {string[]} separationDims The variable names that need to be separated for this
162
+ * variable because of circular references.
163
+ * @param {string} rhsText The text of the RHS of the variable definition.
164
+ * @returns {boolean[]} An array of boolean flags, one for each subscript position.
165
+ */
166
+ function subscriptPositionsToExpand(subIds, exceptSubIdSets, separationDims, rhsText) {
167
+ // Decide whether we need to expand each subscript on the LHS.
168
+ // Construct an array of booleans in each subscript position.
169
+ const expandFlags = new Array(subIds.length).fill(false)
170
+
171
+ for (let i = 0; i < subIds.length; i++) {
172
+ const subId = subIds[i]
173
+ let expand = false
174
+
175
+ // Expand a subdimension and special separation dims in the LHS
176
+ if (isDimension(subId)) {
177
+ expand = isSubdimension(subId)
178
+ if (!expand) {
179
+ expand = separationDims.includes(subId)
180
+ }
181
+ }
182
+
183
+ if (!expand) {
184
+ // Direct data vars with subscripts are separated because we generate a lookup for each index
185
+ if (isDimension(subId) && (rhsText.includes('GET DIRECT DATA') || rhsText.includes('GET DIRECT LOOKUPS'))) {
186
+ expand = true
187
+ }
188
+ }
189
+
190
+ if (!expand) {
191
+ // Also expand on exception subscripts that are indices or subdimensions
192
+ for (const exceptSubIds of exceptSubIdSets) {
193
+ expand = isIndex(exceptSubIds[i]) || isSubdimension(exceptSubIds[i])
194
+ if (expand) {
195
+ break
196
+ }
197
+ }
198
+ }
199
+
200
+ expandFlags[i] = expand
201
+ }
202
+
203
+ return expandFlags
204
+ }
205
+
206
+ /**
207
+ * Return an array describing the set of variable expansions.
208
+ *
209
+ * TODO: Use correct types here
210
+ *
211
+ * @param {string} baseVarId The canonical base name of the variable.
212
+ * @param {*} subIds The list of subscripts appearing on the LHS in normalized order.
213
+ * @param {*} exceptSubIdSets An array of subscript lists from the :EXCEPT: clause.
214
+ * @param {*} positionsToExpand An array of boolean flags, one for each subscript position.
215
+ * @returns {*} An array of objects containing the `subIds` and `separationDims` for each expansion.
216
+ */
217
+ function computeExpansions(baseVarId, subIds, exceptSubIdSets, positionsToExpand) {
218
+ // Construct an array with an array at each subscript position. If the subscript is expanded at that position,
219
+ // it will become an array of indices. Otherwise, it remains an index or dimension as a single-valued array.
220
+ const expandedSubIdsPerPosition = []
221
+ const separationDimIds = []
222
+ for (let i = 0; i < subIds.length; i++) {
223
+ const subId = subIds[i]
224
+ let subIdsInExpansion
225
+ if (positionsToExpand[i]) {
226
+ separationDimIds.push(subId)
227
+ if (isDimension(subId)) {
228
+ subIdsInExpansion = sub(subId).value
229
+ }
230
+ }
231
+ expandedSubIdsPerPosition.push(subIdsInExpansion || [subId])
232
+ }
233
+
234
+ // Generate an array of fully expanded subscripts, which may be indices or dimensions
235
+ const expandedSubIdSets = cartesianProductOf(expandedSubIdsPerPosition)
236
+ const skipExpansion = expandedSubIds => {
237
+ // Check the subscripts against each set of except subscripts. Skip expansion if one of them matches.
238
+ const subsRange = R.range(0, expandedSubIds.length)
239
+ for (const exceptSubIds of exceptSubIdSets) {
240
+ if (expandedSubIds.length === exceptSubIds.length) {
241
+ if (R.all(i => subscriptsMatch(expandedSubIds[i], exceptSubIds[i]), subsRange)) {
242
+ return true
243
+ }
244
+ } else {
245
+ console.error(`WARNING: expandedSubIds length ≠ exceptSubIds length in ${baseVarId}`)
246
+ }
247
+ }
248
+ return false
249
+ }
250
+
251
+ const expansions = []
252
+ for (const expandedSubIds of expandedSubIdSets) {
253
+ // Skip expansions that match exception subscripts
254
+ if (!skipExpansion(expandedSubIds)) {
255
+ // Add a new expansion
256
+ expansions.push({
257
+ subIds: expandedSubIds,
258
+ separationDimIds
259
+ })
260
+ }
261
+ }
262
+
263
+ return expansions
264
+ }
@@ -0,0 +1,166 @@
1
+ import { reduceConditionals, reduceExpr, toPrettyString } from '@sdeverywhere/parse'
2
+
3
+ import Model from './model.js'
4
+
5
+ /**
6
+ * Process the parsed `Equation` instances associated with all variables in the model and attempt to
7
+ * reduce expressions through constant folding and simplifying trivial operations and function calls.
8
+ *
9
+ * @param {*} variables The array of `Variable` instances.
10
+ * @param {string[]} inputVarIds The array of IDs for the configured input variables, which need to be
11
+ * preserved (will not be reduced/eliminated).
12
+ * @param {'default' | 'aggressive'} mode The reduction method to use.
13
+ */
14
+ export function reduceVariables(variables, inputVarIds, mode) {
15
+ const baseVarIdForRefId = refId => {
16
+ return refId.split('[')[0]
17
+ }
18
+
19
+ const refIdForVarRef = varRef => {
20
+ if (varRef.subscriptRefs?.length > 0) {
21
+ const subIds = varRef.subscriptRefs.map(subRef => subRef.subId)
22
+ return `${varRef.varId}[${subIds.join(',')}]`
23
+ } else {
24
+ return varRef.varId
25
+ }
26
+ }
27
+
28
+ // TODO: For now, we will look up input variables by base varId (ignoring any subscripts)
29
+ // to avoid any confusion
30
+ const inputBaseVarIdsSet = new Set()
31
+ for (const inputVarId of inputVarIds) {
32
+ inputBaseVarIdsSet.add(baseVarIdForRefId(inputVarId))
33
+ }
34
+
35
+ // Keep track of which variables are currently being reduced in order to detect cycles
36
+ // let currentLhsBaseVarId
37
+ const activelyReducingRefIds = new Set()
38
+
39
+ const reduceVariable = v => {
40
+ if (v.reduced) {
41
+ // The variable has already been visited/reduced
42
+ return
43
+ }
44
+
45
+ // For now, only attempt to reduce variables that have an "expr" equation
46
+ // TODO: Handle const lists too
47
+ if (v.parsedEqn?.rhs?.kind !== 'expr') {
48
+ v.reduced = true
49
+ return
50
+ }
51
+
52
+ // Add this variable to the set of active ones
53
+ // TODO: Allow cycle if the current LHS is referenced on the RHS
54
+ // const baseVarId = v.parsedEqn.lhs.varId
55
+ // if (baseVarId !== currentLhsBaseVarId) {
56
+ if (activelyReducingRefIds.has(v.refId)) {
57
+ throw new Error(`Cycle detected when reducing variables: ${v.refId}`)
58
+ }
59
+ // }
60
+ activelyReducingRefIds.add(v.refId)
61
+
62
+ // We currently have two options for reducing variables. The less aggressive
63
+ // one only reduces conditionals (the default for now, for compatibility with
64
+ // the legacy reader), and the more aggressive one (enabled via environment
65
+ // variable) performs constant folding and simplifies trivial expressions and
66
+ // function calls.
67
+ let reduce
68
+ if (mode === 'aggressive') {
69
+ reduce = reduceExpr
70
+ } else {
71
+ reduce = reduceConditionals
72
+ }
73
+
74
+ // Reduce the expression
75
+ const parsedRhsExpr = v.parsedEqn.rhs.expr
76
+ const reducedRhsExpr = reduce(parsedRhsExpr, {
77
+ resolveVarRef
78
+ })
79
+
80
+ // Save reduced RHS as a new `parsedEqn`
81
+ const newEqn = {
82
+ lhs: v.parsedEqn.lhs,
83
+ rhs: {
84
+ kind: 'expr',
85
+ expr: reducedRhsExpr
86
+ }
87
+ }
88
+ v.parsedEqn = newEqn
89
+
90
+ // TODO: Ideally we would leave the `modelFormula` untouched so that when we generate the
91
+ // comment above the generated code, it would show the original equation instead of the
92
+ // reduced one. But the tests currently rely on `modelFormula` being updated after we
93
+ // reduce the RHS, so we save the original formula to `origModelFormula` and use that when
94
+ // generating the code comment, and save the reduced formula to `modelFormula`.
95
+ const reducedRhsText = toPrettyString(reducedRhsExpr, { compact: true })
96
+ v.origModelFormula = v.modelFormula
97
+ v.modelFormula = reducedRhsText
98
+
99
+ // Set a flag to indicate that the variable has been visited and reduced
100
+ v.reduced = true
101
+
102
+ // Remove this variable from the set of active ones
103
+ activelyReducingRefIds.delete(v.refId)
104
+ }
105
+
106
+ const resolveVarRef = varRef => {
107
+ // If we look up by refId, it will fail if it's a separated var. If it's a normal
108
+ // unsubscripted or apply-to-all var, then refId lookup should return one var.
109
+ // TODO: This approach needs to be revisited
110
+ const refId = refIdForVarRef(varRef)
111
+ const refVar = Model.varWithRefId(refId)
112
+ if (refVar) {
113
+ // XXX: For now, don't try to reduce variables like this:
114
+ // x[DimA] = 1
115
+ // This is because `generateSmoothVariables` doesn't work well in cases
116
+ // where some arguments have subscripts and some don't. So for example,
117
+ // if the "init" arg is subscripted but is defined as a constant, we would
118
+ // reduce it to a constant, but `generateSmoothVariables` doesn't know how
119
+ // to mix.
120
+ if (refVar.subscripts !== undefined && refVar.subscripts.length > 0) {
121
+ return undefined
122
+ }
123
+
124
+ // In "aggressive" mode, attempt to reduce the referenced variable, otherwise
125
+ // use the parsed RHS expression as is
126
+ if (mode === 'aggressive') {
127
+ reduceVariable(refVar)
128
+ }
129
+
130
+ // If the attempt was successful, the reduced expression will be defined here
131
+ const reducedExpr = refVar.parsedEqn?.rhs?.expr
132
+ if (reducedExpr) {
133
+ // Only substitute if the reduced expression resolves to a simple constant and
134
+ // the variable is not configured as an input (that can override the constant
135
+ // value at runtime). Otherwise we will keep the variable reference intact.
136
+ // TODO: Are there other cases where we should do a substitution? Like when
137
+ // the expression reduces to a single 'variable-ref'?
138
+ const refBaseVarId = baseVarIdForRefId(refId)
139
+ if (reducedExpr.kind === 'number' && !inputBaseVarIdsSet.has(refBaseVarId)) {
140
+ return reducedExpr
141
+ } else {
142
+ return undefined
143
+ }
144
+ } else {
145
+ return undefined
146
+ }
147
+ } else {
148
+ // We couldn't find the variable by refId, so find all variables with that base varId
149
+ // TODO: Revisit this
150
+ // const refVars = Model.varsWithName(varRef.varId)
151
+ // if (refVars.length === 1) {
152
+ // // When there's a single variable, we can use that
153
+ // reduceVariable(refVars[0])
154
+ // return refVars[0].parsedEqn?.rhs?.expr
155
+ // } else {
156
+ // return undefined
157
+ // }
158
+ return undefined
159
+ }
160
+ }
161
+
162
+ // Visit each variable and attempt to reduce it
163
+ for (const v of variables) {
164
+ reduceVariable(v)
165
+ }
166
+ }
@@ -115,9 +115,12 @@ export default class SubscriptRangeReader extends ModelReader {
115
115
  let lastCell = args[3]
116
116
  // let prefix = args[4]
117
117
  // If lastCell is a column letter, scan the column, else scan the row.
118
- let dataAddress = XLSX.utils.decode_cell(firstCell)
118
+ let dataAddress = XLSX.utils.decode_cell(firstCell.toUpperCase())
119
119
  let col = dataAddress.c
120
120
  let row = dataAddress.r
121
+ if (col < 0 || row < 0) {
122
+ throw new Error(`Failed to parse 'firstcell' argument for GET DIRECT SUBSCRIPT call: ${firstCell}`)
123
+ }
121
124
  let nextCell
122
125
  if (isNaN(parseInt(lastCell))) {
123
126
  nextCell = () => row++
@@ -3,8 +3,13 @@ export default class Variable {
3
3
  // The equation rule context allows us to generate code by visiting the parse tree.
4
4
  this.eqnCtx = eqnCtx
5
5
  // Save both sides of the equation text in the model for documentation purposes.
6
- this.modelLHS = eqnCtx ? eqnCtx.lhs().getText() : ''
7
- this.modelFormula = this.formula(eqnCtx)
6
+ if (eqnCtx) {
7
+ this.modelLHS = eqnCtx.lhs().getText()
8
+ this.modelFormula = this.formula(eqnCtx)
9
+ } else {
10
+ this.modelLHS = ''
11
+ this.modelFormula = ''
12
+ }
8
13
  // An equation defines a variable with a var name, saved in canonical form here.
9
14
  this.varName = ''
10
15
  // Subscripts are canonical dimension or index names on the LHS in normal order.
@@ -3,13 +3,17 @@
3
3
  import antlr4 from 'antlr4'
4
4
  import { ModelLexer, ModelParser } from 'antlr4-vensim'
5
5
 
6
+ /**
7
+ * @typedef {object} VensimModelParseTree
8
+ */
9
+
6
10
  /**
7
11
  * Create a `ModelParser` for the given model text, which can be the
8
12
  * contents of an entire `mdl` file, or a portion of one (e.g., an
9
13
  * expression or definition).
10
14
  *
11
- * @param input The string containing the model text.
12
- * @return A `ModelParser` from which a parse tree can be obtained.
15
+ * @param {string} input The string containing the model text.
16
+ * @return {ModelParser} A `ModelParser` from which a parse tree can be obtained.
13
17
  */
14
18
  export function createParser(input) {
15
19
  let chars = new antlr4.InputStream(input)
@@ -23,8 +27,8 @@ export function createParser(input) {
23
27
  /**
24
28
  * Read the given model text and return a parse tree.
25
29
  *
26
- * @param input The string containing the model text.
27
- * @return A parse tree representation of the model.
30
+ * @param {string} input The string containing the model text.
31
+ * @return {VensimModelParseTree} A parse tree representation of the model.
28
32
  */
29
33
  export function parseModel(input) {
30
34
  let parser = createParser(input)