@sdeverywhere/parse 0.1.5 → 0.1.7

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/dist/index.js CHANGED
@@ -1,2118 +1,1900 @@
1
- // src/_shared/canonical-id.js
2
- var reTrailingMark = new RegExp("\\s+!$", "g");
3
- var reWhitespace = new RegExp("(\\s|_)+", "g");
4
- var reSpecialChars = /[^\p{L}\p{N}_!]/gu;
1
+ import { assertNever } from "assert-never";
2
+ import { ModelLexer, ModelParser, ModelVisitor } from "antlr4-vensim";
3
+ import antlr4 from "antlr4";
4
+ import split from "split-string";
5
+ import { XmlNode, parseXml } from "@rgrove/parse-xml";
6
+ //#region src/_shared/canonical-id.ts
7
+ const reTrailingMark = /* @__PURE__ */ new RegExp("\\s+!$", "g");
8
+ const reWhitespace$1 = /* @__PURE__ */ new RegExp("(\\s|_)+", "g");
9
+ const reSpecialChars = /[^\p{L}\p{N}_!]/gu;
10
+ /**
11
+ * Format a model variable or subscript/dimension name into a valid C identifier (with
12
+ * special characters converted to underscore).
13
+ *
14
+ * Note that this should only be called with an individual variable base name (e.g.,
15
+ * 'Variable name') or a subscript/dimension name (e.g., 'DimA'). In the case where
16
+ * you have a full variable name that includes subscripts/dimensions (e.g.,
17
+ * 'Variable name[DimA,B2]'), use `canonicalVarId` to convert the base variable name
18
+ * and subscript/dimension parts to canonical form indepdendently.
19
+ *
20
+ * @param name The name of the variable in the source model, e.g., "Variable name".
21
+ * @returns The C identifier for the given name, e.g., "_variable_name".
22
+ */
5
23
  function canonicalId(name) {
6
- return "_" + name.trim().replace(reTrailingMark, "!").replace(reWhitespace, "_").replace(reSpecialChars, "_").toLowerCase();
24
+ return "_" + name.trim().replace(reTrailingMark, "!").replace(reWhitespace$1, "_").replace(reSpecialChars, "_").toLowerCase();
7
25
  }
26
+ /**
27
+ * Format a (subscripted or non-subscripted) model variable name into a canonical identifier,
28
+ * (with special characters converted to underscore, and subscript/dimension parts separated
29
+ * by commas).
30
+ *
31
+ * @param name The name of the variable in the source model, e.g., "Variable name[DimA, B2]".
32
+ * @returns The canonical identifier for the given name, e.g., "_variable_name[_dima,_b2]".
33
+ */
8
34
  function canonicalVarId(name) {
9
- const m = name.match(/([^[]+)(?:\[([^\]]+)\])?/);
10
- if (!m) {
11
- throw new Error(`Invalid variable name: ${name}`);
12
- }
13
- let id = canonicalId(m[1]);
14
- if (m[2]) {
15
- const subscripts = m[2].split(",").map((x) => canonicalId(x));
16
- id += `[${subscripts.join(",")}]`;
17
- }
18
- return id;
35
+ const m = name.match(/([^[]+)(?:\[([^\]]+)\])?/);
36
+ if (!m) throw new Error(`Invalid variable name: ${name}`);
37
+ let id = canonicalId(m[1]);
38
+ if (m[2]) {
39
+ const subscripts = m[2].split(",").map((x) => canonicalId(x));
40
+ id += `[${subscripts.join(",")}]`;
41
+ }
42
+ return id;
19
43
  }
44
+ /**
45
+ * Format a model function name into a valid C identifier (with special characters
46
+ * converted to underscore, and the ID converted to uppercase).
47
+ *
48
+ * @param name The name of the variable in the source model, e.g., "FUNCTION name".
49
+ * @returns The C identifier for the given name, e.g., "_FUNCTION_NAME".
50
+ */
20
51
  function canonicalFunctionId(name) {
21
- return canonicalId(name).toUpperCase();
52
+ return canonicalId(name).toUpperCase();
22
53
  }
23
-
24
- // src/ast/print-expr.ts
25
- import { assertNever } from "assert-never";
54
+ //#endregion
55
+ //#region src/ast/print-expr.ts
56
+ /**
57
+ * @hidden This is not yet part of the public API.
58
+ */
26
59
  function debugPrintExpr(expr, indent = 0) {
27
- const spaces = " ".repeat(indent * 2);
28
- const log = (s) => {
29
- console.log(`${spaces}${s}`);
30
- };
31
- switch (expr.kind) {
32
- case "number":
33
- log(`const: ${expr.text}`);
34
- break;
35
- case "string":
36
- log(`string: ${expr.text}`);
37
- break;
38
- case "keyword":
39
- log(`keyword: ${expr.text}`);
40
- break;
41
- case "variable-ref":
42
- log(`ref: ${fullIdForVarRef(expr)}`);
43
- break;
44
- case "unary-op":
45
- log(`unary-op: ${expr.op}`);
46
- debugPrintExpr(expr.expr, indent + 1);
47
- break;
48
- case "binary-op":
49
- log(`binary-op: ${expr.op}`);
50
- debugPrintExpr(expr.lhs, indent + 1);
51
- debugPrintExpr(expr.rhs, indent + 1);
52
- break;
53
- case "parens":
54
- log("parens");
55
- debugPrintExpr(expr.expr, indent + 1);
56
- break;
57
- case "lookup-def":
58
- log(`lookup-def`);
59
- break;
60
- case "lookup-call":
61
- log(`lookup-call: ${debugPrintExpr(expr.varRef)}`);
62
- debugPrintExpr(expr.arg, indent + 1);
63
- break;
64
- case "function-call":
65
- log(`function-call: ${expr.fnId}`);
66
- expr.args.forEach((arg) => debugPrintExpr(arg, indent + 1));
67
- break;
68
- default:
69
- assertNever(expr);
70
- }
60
+ const spaces = " ".repeat(indent * 2);
61
+ const log = (s) => {
62
+ console.log(`${spaces}${s}`);
63
+ };
64
+ switch (expr.kind) {
65
+ case "number":
66
+ log(`const: ${expr.text}`);
67
+ break;
68
+ case "string":
69
+ log(`string: ${expr.text}`);
70
+ break;
71
+ case "keyword":
72
+ log(`keyword: ${expr.text}`);
73
+ break;
74
+ case "variable-ref":
75
+ log(`ref: ${fullIdForVarRef(expr)}`);
76
+ break;
77
+ case "unary-op":
78
+ log(`unary-op: ${expr.op}`);
79
+ debugPrintExpr(expr.expr, indent + 1);
80
+ break;
81
+ case "binary-op":
82
+ log(`binary-op: ${expr.op}`);
83
+ debugPrintExpr(expr.lhs, indent + 1);
84
+ debugPrintExpr(expr.rhs, indent + 1);
85
+ break;
86
+ case "parens":
87
+ log("parens");
88
+ debugPrintExpr(expr.expr, indent + 1);
89
+ break;
90
+ case "lookup-def":
91
+ log(`lookup-def`);
92
+ break;
93
+ case "lookup-call":
94
+ log(`lookup-call: ${debugPrintExpr(expr.varRef)}`);
95
+ debugPrintExpr(expr.arg, indent + 1);
96
+ break;
97
+ case "function-call":
98
+ log(`function-call: ${expr.fnId}`);
99
+ expr.args.forEach((arg) => debugPrintExpr(arg, indent + 1));
100
+ break;
101
+ default: assertNever(expr);
102
+ }
71
103
  }
104
+ /**
105
+ * @hidden This is not yet part of the public API.
106
+ */
72
107
  function toPrettyString(expr, opts) {
73
- let lparen, rparen, spaceSep, commaSep;
74
- if (opts?.compact === true) {
75
- lparen = "(";
76
- rparen = ")";
77
- spaceSep = "";
78
- commaSep = ",";
79
- } else {
80
- lparen = "( ";
81
- rparen = " )";
82
- spaceSep = " ";
83
- commaSep = ", ";
84
- }
85
- switch (expr.kind) {
86
- case "number":
87
- return expr.text;
88
- case "string":
89
- return `'${expr.text}'`;
90
- case "keyword":
91
- return expr.text;
92
- case "variable-ref":
93
- if (opts?.formatVariableRef) {
94
- return opts.formatVariableRef(expr);
95
- } else {
96
- if (expr.subscriptRefs?.length > 0) {
97
- return `${expr.varName}[${expr.subscriptRefs.map((ref) => ref.subName).join(commaSep)}]`;
98
- } else {
99
- return expr.varName;
100
- }
101
- }
102
- case "unary-op":
103
- if (expr.op === ":NOT:") {
104
- return `${expr.op} ${toPrettyString(expr.expr, opts)}`;
105
- } else {
106
- return `${expr.op}${toPrettyString(expr.expr, opts)}`;
107
- }
108
- case "binary-op": {
109
- let op;
110
- if (opts?.html === true) {
111
- switch (expr.op) {
112
- case "<":
113
- op = "&lt;";
114
- break;
115
- case "<=":
116
- op = "&lt;=";
117
- break;
118
- case ">":
119
- op = "&gt;";
120
- break;
121
- case ">=":
122
- op = "&gt;=";
123
- break;
124
- default:
125
- op = expr.op;
126
- break;
127
- }
128
- } else {
129
- op = expr.op;
130
- }
131
- const lhs = toPrettyString(expr.lhs, opts);
132
- const rhs = toPrettyString(expr.rhs, opts);
133
- return `${lhs}${spaceSep}${op}${spaceSep}${rhs}`;
134
- }
135
- case "parens":
136
- return `${lparen}${toPrettyString(expr.expr, opts)}${rparen}`;
137
- case "lookup-def": {
138
- const pointString = (p) => {
139
- return `(${p[0]},${p[1]})`;
140
- };
141
- const points = expr.points.map(pointString).join(commaSep);
142
- if (expr.range) {
143
- const min = pointString(expr.range.min);
144
- const max = pointString(expr.range.max);
145
- return `${lparen}[${min}-${max}]${commaSep}${points}${rparen}`;
146
- } else {
147
- return `${lparen}${points}${rparen}`;
148
- }
149
- }
150
- case "lookup-call": {
151
- const varRef = toPrettyString(expr.varRef, opts);
152
- const arg = toPrettyString(expr.arg, opts);
153
- return `${varRef}${lparen}${arg}${rparen}`;
154
- }
155
- case "function-call": {
156
- const args = expr.args.map((arg) => toPrettyString(arg, opts));
157
- return `${expr.fnName}${lparen}${args.join(commaSep)}${rparen}`;
158
- }
159
- default:
160
- assertNever(expr);
161
- }
108
+ let lparen, rparen, spaceSep, commaSep;
109
+ if (opts?.compact === true) {
110
+ lparen = "(";
111
+ rparen = ")";
112
+ spaceSep = "";
113
+ commaSep = ",";
114
+ } else {
115
+ lparen = "( ";
116
+ rparen = " )";
117
+ spaceSep = " ";
118
+ commaSep = ", ";
119
+ }
120
+ switch (expr.kind) {
121
+ case "number": return expr.text;
122
+ case "string": return `'${expr.text}'`;
123
+ case "keyword": return expr.text;
124
+ case "variable-ref": if (opts?.formatVariableRef) return opts.formatVariableRef(expr);
125
+ else if (expr.subscriptRefs?.length > 0) return `${expr.varName}[${expr.subscriptRefs.map((ref) => ref.subName).join(commaSep)}]`;
126
+ else return expr.varName;
127
+ case "unary-op": if (expr.op === ":NOT:") return `${expr.op} ${toPrettyString(expr.expr, opts)}`;
128
+ else return `${expr.op}${toPrettyString(expr.expr, opts)}`;
129
+ case "binary-op": {
130
+ let op;
131
+ if (opts?.html === true) switch (expr.op) {
132
+ case "<":
133
+ op = "&lt;";
134
+ break;
135
+ case "<=":
136
+ op = "&lt;=";
137
+ break;
138
+ case ">":
139
+ op = "&gt;";
140
+ break;
141
+ case ">=":
142
+ op = "&gt;=";
143
+ break;
144
+ default: op = expr.op;
145
+ }
146
+ else op = expr.op;
147
+ const lhs = toPrettyString(expr.lhs, opts);
148
+ const rhs = toPrettyString(expr.rhs, opts);
149
+ return `${lhs}${spaceSep}${op}${spaceSep}${rhs}`;
150
+ }
151
+ case "parens": return `${lparen}${toPrettyString(expr.expr, opts)}${rparen}`;
152
+ case "lookup-def": {
153
+ const pointString = (p) => {
154
+ return `(${p[0]},${p[1]})`;
155
+ };
156
+ const points = expr.points.map(pointString).join(commaSep);
157
+ if (expr.range) {
158
+ const min = pointString(expr.range.min);
159
+ const max = pointString(expr.range.max);
160
+ return `${lparen}[${min}-${max}]${commaSep}${points}${rparen}`;
161
+ } else return `${lparen}${points}${rparen}`;
162
+ }
163
+ case "lookup-call": {
164
+ const varRef = toPrettyString(expr.varRef, opts);
165
+ const arg = toPrettyString(expr.arg, opts);
166
+ return `${varRef}${lparen}${arg}${rparen}`;
167
+ }
168
+ case "function-call": {
169
+ const args = expr.args.map((arg) => toPrettyString(arg, opts));
170
+ return `${expr.fnName}${lparen}${args.join(commaSep)}${rparen}`;
171
+ }
172
+ default: assertNever(expr);
173
+ }
162
174
  }
175
+ /**
176
+ * @hidden This is not yet part of the public API.
177
+ */
163
178
  function prettyPrintExpr(expr, indent = 0) {
164
- const spaces = " ".repeat(indent * 2);
165
- const log = (s) => {
166
- console.log(`${spaces}${s}`);
167
- };
168
- log(toPrettyString(expr));
179
+ const spaces = " ".repeat(indent * 2);
180
+ const log = (s) => {
181
+ console.log(`${spaces}${s}`);
182
+ };
183
+ log(toPrettyString(expr));
169
184
  }
170
185
  var Stats = class {
171
- constructor() {
172
- this.constCount = 0;
173
- this.varRefCount = 0;
174
- this.unaryOpCounts = /* @__PURE__ */ new Map();
175
- this.binaryOpCounts = /* @__PURE__ */ new Map();
176
- this.fnCallCounts = /* @__PURE__ */ new Map();
177
- this.luCallCounts = /* @__PURE__ */ new Map();
178
- }
186
+ constructor() {
187
+ this.constCount = 0;
188
+ this.varRefCount = 0;
189
+ this.unaryOpCounts = /* @__PURE__ */ new Map();
190
+ this.binaryOpCounts = /* @__PURE__ */ new Map();
191
+ this.fnCallCounts = /* @__PURE__ */ new Map();
192
+ this.luCallCounts = /* @__PURE__ */ new Map();
193
+ }
179
194
  };
180
195
  function increment(map, key) {
181
- const count = map.get(key) || 0;
182
- map.set(key, count + 1);
196
+ const count = map.get(key) || 0;
197
+ map.set(key, count + 1);
183
198
  }
184
199
  function getExprStats(expr, stats) {
185
- switch (expr.kind) {
186
- case "number":
187
- stats.constCount++;
188
- break;
189
- case "string":
190
- break;
191
- case "keyword":
192
- break;
193
- case "variable-ref":
194
- stats.varRefCount++;
195
- break;
196
- case "unary-op":
197
- getExprStats(expr.expr, stats);
198
- increment(stats.unaryOpCounts, expr.op);
199
- break;
200
- case "binary-op":
201
- getExprStats(expr.lhs, stats);
202
- getExprStats(expr.rhs, stats);
203
- increment(stats.binaryOpCounts, expr.op);
204
- break;
205
- case "parens":
206
- getExprStats(expr.expr, stats);
207
- break;
208
- case "lookup-def":
209
- break;
210
- case "lookup-call":
211
- increment(stats.luCallCounts, fullIdForVarRef(expr.varRef));
212
- getExprStats(expr.arg, stats);
213
- break;
214
- case "function-call":
215
- increment(stats.fnCallCounts, expr.fnId);
216
- expr.args.forEach((arg) => getExprStats(arg, stats));
217
- break;
218
- default:
219
- assertNever(expr);
220
- }
200
+ switch (expr.kind) {
201
+ case "number":
202
+ stats.constCount++;
203
+ break;
204
+ case "string": break;
205
+ case "keyword": break;
206
+ case "variable-ref":
207
+ stats.varRefCount++;
208
+ break;
209
+ case "unary-op":
210
+ getExprStats(expr.expr, stats);
211
+ increment(stats.unaryOpCounts, expr.op);
212
+ break;
213
+ case "binary-op":
214
+ getExprStats(expr.lhs, stats);
215
+ getExprStats(expr.rhs, stats);
216
+ increment(stats.binaryOpCounts, expr.op);
217
+ break;
218
+ case "parens":
219
+ getExprStats(expr.expr, stats);
220
+ break;
221
+ case "lookup-def": break;
222
+ case "lookup-call":
223
+ increment(stats.luCallCounts, fullIdForVarRef(expr.varRef));
224
+ getExprStats(expr.arg, stats);
225
+ break;
226
+ case "function-call":
227
+ increment(stats.fnCallCounts, expr.fnId);
228
+ expr.args.forEach((arg) => getExprStats(arg, stats));
229
+ break;
230
+ default: assertNever(expr);
231
+ }
221
232
  }
233
+ /**
234
+ * @hidden This is not yet part of the public API.
235
+ */
222
236
  function printExprStats(exprs) {
223
- const stats = new Stats();
224
- for (const expr of exprs) {
225
- getExprStats(expr, stats);
226
- }
227
- function printCount(count, label) {
228
- console.log(`${count.toString().padStart(6)} ${label}`);
229
- }
230
- function printCounts(map) {
231
- const entries = [...map.entries()].sort((a, b) => a[0].localeCompare(b[0]));
232
- let total = 0;
233
- for (const entry of entries) {
234
- printCount(entry[1], entry[0]);
235
- total += entry[1];
236
- }
237
- printCount(total, "total");
238
- return total;
239
- }
240
- let nodeCount = 0;
241
- printCount(stats.constCount, "consts");
242
- nodeCount += stats.constCount;
243
- printCount(stats.varRefCount, "var refs");
244
- nodeCount += stats.varRefCount;
245
- console.log();
246
- console.log("UNARY OPS");
247
- nodeCount += printCounts(stats.unaryOpCounts);
248
- console.log();
249
- console.log("BINARY OPS");
250
- nodeCount += printCounts(stats.binaryOpCounts);
251
- console.log();
252
- console.log("FUNCTION CALLS");
253
- nodeCount += printCounts(stats.fnCallCounts);
254
- console.log();
255
- console.log("LOOKUP CALLS");
256
- nodeCount += printCounts(stats.luCallCounts);
257
- console.log();
258
- console.log("TOTAL");
259
- printCount(nodeCount, "nodes");
237
+ const stats = new Stats();
238
+ for (const expr of exprs) getExprStats(expr, stats);
239
+ function printCount(count, label) {
240
+ console.log(`${count.toString().padStart(6)} ${label}`);
241
+ }
242
+ function printCounts(map) {
243
+ const entries = [...map.entries()].sort((a, b) => a[0].localeCompare(b[0]));
244
+ let total = 0;
245
+ for (const entry of entries) {
246
+ printCount(entry[1], entry[0]);
247
+ total += entry[1];
248
+ }
249
+ printCount(total, "total");
250
+ return total;
251
+ }
252
+ let nodeCount = 0;
253
+ printCount(stats.constCount, "consts");
254
+ nodeCount += stats.constCount;
255
+ printCount(stats.varRefCount, "var refs");
256
+ nodeCount += stats.varRefCount;
257
+ console.log();
258
+ console.log("UNARY OPS");
259
+ nodeCount += printCounts(stats.unaryOpCounts);
260
+ console.log();
261
+ console.log("BINARY OPS");
262
+ nodeCount += printCounts(stats.binaryOpCounts);
263
+ console.log();
264
+ console.log("FUNCTION CALLS");
265
+ nodeCount += printCounts(stats.fnCallCounts);
266
+ console.log();
267
+ console.log("LOOKUP CALLS");
268
+ nodeCount += printCounts(stats.luCallCounts);
269
+ console.log();
270
+ console.log("TOTAL");
271
+ printCount(nodeCount, "nodes");
260
272
  }
261
273
  function fullIdForVarRef(varRef) {
262
- if (varRef.subscriptRefs?.length > 0) {
263
- return `${varRef.varId}[${varRef.subscriptRefs.map((ref) => ref.subId).join(",")}]`;
264
- } else {
265
- return varRef.varId;
266
- }
274
+ if (varRef.subscriptRefs?.length > 0) return `${varRef.varId}[${varRef.subscriptRefs.map((ref) => ref.subId).join(",")}]`;
275
+ else return varRef.varId;
267
276
  }
268
-
269
- // src/ast/reduce-expr.ts
270
- import { assertNever as assertNever2 } from "assert-never";
271
-
272
- // src/ast/ast-builders.ts
277
+ //#endregion
278
+ //#region src/ast/ast-builders.ts
273
279
  function subRef(dimOrSubName) {
274
- return {
275
- subName: dimOrSubName,
276
- subId: canonicalId(dimOrSubName)
277
- };
280
+ return {
281
+ subName: dimOrSubName,
282
+ subId: canonicalId(dimOrSubName)
283
+ };
278
284
  }
279
285
  function num(value, text) {
280
- return {
281
- kind: "number",
282
- value,
283
- text: text || value.toString()
284
- };
286
+ return {
287
+ kind: "number",
288
+ value,
289
+ text: text || value.toString()
290
+ };
285
291
  }
286
292
  function unaryOp(op, expr) {
287
- return {
288
- kind: "unary-op",
289
- op,
290
- expr
291
- };
293
+ return {
294
+ kind: "unary-op",
295
+ op,
296
+ expr
297
+ };
292
298
  }
293
299
  function binaryOp(lhs, op, rhs) {
294
- return {
295
- kind: "binary-op",
296
- lhs,
297
- op,
298
- rhs
299
- };
300
+ return {
301
+ kind: "binary-op",
302
+ lhs,
303
+ op,
304
+ rhs
305
+ };
300
306
  }
301
307
  function parens(expr) {
302
- return {
303
- kind: "parens",
304
- expr
305
- };
308
+ return {
309
+ kind: "parens",
310
+ expr
311
+ };
306
312
  }
307
313
  function lookupDef(points, range) {
308
- return {
309
- kind: "lookup-def",
310
- range,
311
- points
312
- };
314
+ return {
315
+ kind: "lookup-def",
316
+ range,
317
+ points
318
+ };
313
319
  }
314
320
  function lookupCall(varRef, arg) {
315
- return {
316
- kind: "lookup-call",
317
- varRef,
318
- arg
319
- };
321
+ return {
322
+ kind: "lookup-call",
323
+ varRef,
324
+ arg
325
+ };
320
326
  }
321
327
  function call(fnName, ...args) {
322
- return {
323
- kind: "function-call",
324
- fnName,
325
- fnId: canonicalFunctionId(fnName),
326
- args
327
- };
328
+ return {
329
+ kind: "function-call",
330
+ fnName,
331
+ fnId: canonicalFunctionId(fnName),
332
+ args
333
+ };
328
334
  }
329
-
330
- // src/ast/reduce-expr.ts
335
+ //#endregion
336
+ //#region src/ast/reduce-expr.ts
337
+ /**
338
+ * @hidden This is not yet part of the public API.
339
+ */
331
340
  function reduceExpr(expr, opts) {
332
- switch (expr.kind) {
333
- case "number":
334
- case "string":
335
- case "keyword":
336
- return expr;
337
- case "variable-ref":
338
- if (opts?.resolveVarRef !== void 0) {
339
- const resolvedExpr = opts.resolveVarRef(expr);
340
- if (resolvedExpr) {
341
- return resolvedExpr;
342
- }
343
- }
344
- return expr;
345
- case "unary-op": {
346
- const child = reduceExpr(expr.expr, opts);
347
- switch (expr.op) {
348
- case "+":
349
- return child;
350
- case "-":
351
- if (child.kind === "number") {
352
- return num(-child.value);
353
- } else {
354
- return unaryOp("-", child);
355
- }
356
- case ":NOT:":
357
- if (child.kind === "number") {
358
- return num(child.value === 0 ? 1 : 0);
359
- } else {
360
- return unaryOp(":NOT:", child);
361
- }
362
- default:
363
- assertNever2(expr);
364
- }
365
- break;
366
- }
367
- case "binary-op": {
368
- const lhs = reduceExpr(expr.lhs, opts);
369
- const rhs = reduceExpr(expr.rhs, opts);
370
- if (lhs.kind === "number" && rhs.kind === "number") {
371
- switch (expr.op) {
372
- case "+":
373
- return num(lhs.value + rhs.value);
374
- case "-":
375
- return num(lhs.value - rhs.value);
376
- case "*":
377
- return num(lhs.value * rhs.value);
378
- case "/":
379
- return num(lhs.value / rhs.value);
380
- case "^":
381
- return num(Math.pow(lhs.value, rhs.value));
382
- case "=":
383
- return num(lhs.value === rhs.value ? 1 : 0);
384
- case "<>":
385
- return num(lhs.value !== rhs.value ? 1 : 0);
386
- case "<":
387
- return num(lhs.value < rhs.value ? 1 : 0);
388
- case ">":
389
- return num(lhs.value > rhs.value ? 1 : 0);
390
- case "<=":
391
- return num(lhs.value <= rhs.value ? 1 : 0);
392
- case ">=":
393
- return num(lhs.value >= rhs.value ? 1 : 0);
394
- case ":AND:":
395
- return num(lhs.value !== 0 && rhs.value !== 0 ? 1 : 0);
396
- case ":OR:":
397
- return num(lhs.value !== 0 || rhs.value !== 0 ? 1 : 0);
398
- default:
399
- assertNever2(expr);
400
- }
401
- } else if (lhs.kind === "number" || rhs.kind === "number") {
402
- const lhsNum = lhs.kind === "number" ? lhs.value : void 0;
403
- const rhsNum = rhs.kind === "number" ? rhs.value : void 0;
404
- const numValue = lhsNum !== void 0 ? lhsNum : rhsNum;
405
- const otherSide = lhsNum !== void 0 ? rhs : lhs;
406
- switch (expr.op) {
407
- case "+": {
408
- if (numValue === 0) {
409
- return otherSide;
410
- } else if (otherSide.kind === "binary-op" && otherSide.op === "+" && (otherSide.lhs.kind === "number" || otherSide.rhs.kind === "number")) {
411
- const otherSideLhsNum = otherSide.lhs.kind === "number" ? otherSide.lhs.value : void 0;
412
- const otherSideRhsNum = otherSide.rhs.kind === "number" ? otherSide.rhs.value : void 0;
413
- const otherSideConstValue = otherSideLhsNum !== void 0 ? otherSideLhsNum : otherSideRhsNum;
414
- const otherSideOtherPart = otherSideLhsNum !== void 0 ? otherSide.rhs : otherSide.lhs;
415
- return binaryOp(num(numValue + otherSideConstValue), "+", otherSideOtherPart);
416
- }
417
- break;
418
- }
419
- case "-": {
420
- if (rhsNum === 0) {
421
- return lhs;
422
- } else if (lhsNum === 0) {
423
- return unaryOp("-", rhs);
424
- }
425
- break;
426
- }
427
- case "*": {
428
- if (numValue === 0) {
429
- return num(0);
430
- } else if (numValue === 1) {
431
- return otherSide;
432
- } else if (otherSide.kind === "binary-op" && otherSide.op === "*" && (otherSide.lhs.kind === "number" || otherSide.rhs.kind === "number")) {
433
- const otherSideLhsNum = otherSide.lhs.kind === "number" ? otherSide.lhs.value : void 0;
434
- const otherSideRhsNum = otherSide.rhs.kind === "number" ? otherSide.rhs.value : void 0;
435
- const otherSideConstValue = otherSideLhsNum !== void 0 ? otherSideLhsNum : otherSideRhsNum;
436
- const otherSideOtherPart = otherSideLhsNum !== void 0 ? otherSide.rhs : otherSide.lhs;
437
- return binaryOp(num(numValue * otherSideConstValue), "*", otherSideOtherPart);
438
- }
439
- break;
440
- }
441
- case "/": {
442
- if (rhsNum === 1) {
443
- return lhs;
444
- }
445
- break;
446
- }
447
- case "^": {
448
- if (rhsNum === 0) {
449
- return num(1);
450
- } else if (rhsNum === 1) {
451
- return lhs;
452
- }
453
- break;
454
- }
455
- case ":AND:":
456
- return numValue === 0 ? num(0) : otherSide;
457
- case ":OR:":
458
- return numValue !== 0 ? num(1) : otherSide;
459
- default:
460
- break;
461
- }
462
- }
463
- return {
464
- kind: "binary-op",
465
- lhs,
466
- op: expr.op,
467
- rhs
468
- };
469
- }
470
- case "parens": {
471
- const child = reduceExpr(expr.expr, opts);
472
- return applyParens(child);
473
- }
474
- case "lookup-def":
475
- return expr;
476
- case "lookup-call":
477
- return expr;
478
- case "function-call": {
479
- if (expr.fnId === "_IF_THEN_ELSE") {
480
- const conditionExpr = reduceExpr(expr.args[0], opts);
481
- if (conditionExpr.kind === "number") {
482
- const branchExpr = conditionExpr.value !== 0 ? reduceExpr(expr.args[1], opts) : reduceExpr(expr.args[2], opts);
483
- return applyParens(branchExpr);
484
- }
485
- }
486
- const reducedArgs = expr.args.map((arg) => reduceExpr(arg, opts));
487
- const allConst = reducedArgs.every((arg) => arg.kind === "number");
488
- if (allConst) {
489
- const constArg = (index) => {
490
- const num2 = reducedArgs[index];
491
- return num2.value;
492
- };
493
- switch (expr.fnId) {
494
- case "_ABS":
495
- return num(Math.abs(constArg(0)));
496
- case "_COS":
497
- return num(Math.cos(constArg(0)));
498
- case "_EXP":
499
- return num(Math.exp(constArg(0)));
500
- case "_INITIAL":
501
- return reducedArgs[0];
502
- case "_INTEGER":
503
- return num(Math.trunc(constArg(0)));
504
- case "_LN":
505
- return num(Math.log(constArg(0)));
506
- case "_MAX":
507
- return num(Math.max(constArg(0), constArg(1)));
508
- case "_MIN":
509
- return num(Math.min(constArg(0), constArg(1)));
510
- case "_MODULO":
511
- return num(constArg(0) % constArg(1));
512
- case "_POWER":
513
- return num(Math.pow(constArg(0), constArg(1)));
514
- case "_SIN":
515
- return num(Math.sin(constArg(0)));
516
- case "_SQRT":
517
- return num(Math.sqrt(constArg(0)));
518
- default:
519
- break;
520
- }
521
- }
522
- return {
523
- kind: "function-call",
524
- fnName: expr.fnName,
525
- fnId: expr.fnId,
526
- args: reducedArgs
527
- };
528
- }
529
- default:
530
- assertNever2(expr);
531
- }
341
+ switch (expr.kind) {
342
+ case "number":
343
+ case "string":
344
+ case "keyword": return expr;
345
+ case "variable-ref":
346
+ if (opts?.resolveVarRef !== void 0) {
347
+ const resolvedExpr = opts.resolveVarRef(expr);
348
+ if (resolvedExpr) return resolvedExpr;
349
+ }
350
+ return expr;
351
+ case "unary-op": {
352
+ const child = reduceExpr(expr.expr, opts);
353
+ switch (expr.op) {
354
+ case "+": return child;
355
+ case "-": if (child.kind === "number") return num(-child.value);
356
+ else return unaryOp("-", child);
357
+ case ":NOT:": if (child.kind === "number") return num(child.value === 0 ? 1 : 0);
358
+ else return unaryOp(":NOT:", child);
359
+ default: assertNever(expr);
360
+ }
361
+ break;
362
+ }
363
+ case "binary-op": {
364
+ const lhs = reduceExpr(expr.lhs, opts);
365
+ const rhs = reduceExpr(expr.rhs, opts);
366
+ if (lhs.kind === "number" && rhs.kind === "number") switch (expr.op) {
367
+ case "+": return num(lhs.value + rhs.value);
368
+ case "-": return num(lhs.value - rhs.value);
369
+ case "*": return num(lhs.value * rhs.value);
370
+ case "/": return num(lhs.value / rhs.value);
371
+ case "^": return num(Math.pow(lhs.value, rhs.value));
372
+ case "=": return num(lhs.value === rhs.value ? 1 : 0);
373
+ case "<>": return num(lhs.value !== rhs.value ? 1 : 0);
374
+ case "<": return num(lhs.value < rhs.value ? 1 : 0);
375
+ case ">": return num(lhs.value > rhs.value ? 1 : 0);
376
+ case "<=": return num(lhs.value <= rhs.value ? 1 : 0);
377
+ case ">=": return num(lhs.value >= rhs.value ? 1 : 0);
378
+ case ":AND:": return num(lhs.value !== 0 && rhs.value !== 0 ? 1 : 0);
379
+ case ":OR:": return num(lhs.value !== 0 || rhs.value !== 0 ? 1 : 0);
380
+ default: assertNever(expr);
381
+ }
382
+ else if (lhs.kind === "number" || rhs.kind === "number") {
383
+ const lhsNum = lhs.kind === "number" ? lhs.value : void 0;
384
+ const rhsNum = rhs.kind === "number" ? rhs.value : void 0;
385
+ const numValue = lhsNum !== void 0 ? lhsNum : rhsNum;
386
+ const otherSide = lhsNum !== void 0 ? rhs : lhs;
387
+ switch (expr.op) {
388
+ case "+":
389
+ if (numValue === 0) return otherSide;
390
+ else if (otherSide.kind === "binary-op" && otherSide.op === "+" && (otherSide.lhs.kind === "number" || otherSide.rhs.kind === "number")) {
391
+ const otherSideLhsNum = otherSide.lhs.kind === "number" ? otherSide.lhs.value : void 0;
392
+ const otherSideRhsNum = otherSide.rhs.kind === "number" ? otherSide.rhs.value : void 0;
393
+ const otherSideConstValue = otherSideLhsNum !== void 0 ? otherSideLhsNum : otherSideRhsNum;
394
+ const otherSideOtherPart = otherSideLhsNum !== void 0 ? otherSide.rhs : otherSide.lhs;
395
+ return binaryOp(num(numValue + otherSideConstValue), "+", otherSideOtherPart);
396
+ }
397
+ break;
398
+ case "-":
399
+ if (rhsNum === 0) return lhs;
400
+ else if (lhsNum === 0) return unaryOp("-", rhs);
401
+ break;
402
+ case "*":
403
+ if (numValue === 0) return num(0);
404
+ else if (numValue === 1) return otherSide;
405
+ else if (otherSide.kind === "binary-op" && otherSide.op === "*" && (otherSide.lhs.kind === "number" || otherSide.rhs.kind === "number")) {
406
+ const otherSideLhsNum = otherSide.lhs.kind === "number" ? otherSide.lhs.value : void 0;
407
+ const otherSideRhsNum = otherSide.rhs.kind === "number" ? otherSide.rhs.value : void 0;
408
+ const otherSideConstValue = otherSideLhsNum !== void 0 ? otherSideLhsNum : otherSideRhsNum;
409
+ const otherSideOtherPart = otherSideLhsNum !== void 0 ? otherSide.rhs : otherSide.lhs;
410
+ return binaryOp(num(numValue * otherSideConstValue), "*", otherSideOtherPart);
411
+ }
412
+ break;
413
+ case "/":
414
+ if (rhsNum === 1) return lhs;
415
+ break;
416
+ case "^":
417
+ if (rhsNum === 0) return num(1);
418
+ else if (rhsNum === 1) return lhs;
419
+ break;
420
+ case ":AND:": return numValue === 0 ? num(0) : otherSide;
421
+ case ":OR:": return numValue !== 0 ? num(1) : otherSide;
422
+ }
423
+ }
424
+ return {
425
+ kind: "binary-op",
426
+ lhs,
427
+ op: expr.op,
428
+ rhs
429
+ };
430
+ }
431
+ case "parens": return applyParens(reduceExpr(expr.expr, opts));
432
+ case "lookup-def": return expr;
433
+ case "lookup-call": return expr;
434
+ case "function-call": {
435
+ if (expr.fnId === "_IF_THEN_ELSE") {
436
+ const conditionExpr = reduceExpr(expr.args[0], opts);
437
+ if (conditionExpr.kind === "number") return applyParens(conditionExpr.value !== 0 ? reduceExpr(expr.args[1], opts) : reduceExpr(expr.args[2], opts));
438
+ }
439
+ const reducedArgs = expr.args.map((arg) => reduceExpr(arg, opts));
440
+ if (reducedArgs.every((arg) => arg.kind === "number")) {
441
+ const constArg = (index) => {
442
+ return reducedArgs[index].value;
443
+ };
444
+ switch (expr.fnId) {
445
+ case "_ABS": return num(Math.abs(constArg(0)));
446
+ case "_COS": return num(Math.cos(constArg(0)));
447
+ case "_EXP": return num(Math.exp(constArg(0)));
448
+ case "_INITIAL": return reducedArgs[0];
449
+ case "_INTEGER": return num(Math.trunc(constArg(0)));
450
+ case "_LN": return num(Math.log(constArg(0)));
451
+ case "_MAX": return num(Math.max(constArg(0), constArg(1)));
452
+ case "_MIN": return num(Math.min(constArg(0), constArg(1)));
453
+ case "_MODULO": return num(constArg(0) % constArg(1));
454
+ case "_POWER": return num(Math.pow(constArg(0), constArg(1)));
455
+ case "_SIN": return num(Math.sin(constArg(0)));
456
+ case "_SQRT": return num(Math.sqrt(constArg(0)));
457
+ }
458
+ }
459
+ return {
460
+ kind: "function-call",
461
+ fnName: expr.fnName,
462
+ fnId: expr.fnId,
463
+ args: reducedArgs
464
+ };
465
+ }
466
+ default: assertNever(expr);
467
+ }
532
468
  }
469
+ /**
470
+ * A variant of `reduceExpr` that does not aggressively reduce the expression, but only
471
+ * tries to eliminate the unused branch if a conditional (`IF THEN ELSE`) has a condition
472
+ * that resolves to a constant.
473
+ *
474
+ * @hidden This is not yet part of the public API.
475
+ *
476
+ * @param expr The expression to reduce.
477
+ * @param opts The reduce options.
478
+ * @returns A possibly reduced expression.
479
+ */
533
480
  function reduceConditionals(expr, opts) {
534
- switch (expr.kind) {
535
- case "number":
536
- case "string":
537
- case "keyword":
538
- return expr;
539
- case "variable-ref":
540
- return expr;
541
- case "unary-op": {
542
- const child = reduceConditionals(expr.expr, opts);
543
- return unaryOp(expr.op, child);
544
- }
545
- case "binary-op": {
546
- const lhs = reduceConditionals(expr.lhs, opts);
547
- const rhs = reduceConditionals(expr.rhs, opts);
548
- return binaryOp(lhs, expr.op, rhs);
549
- }
550
- case "parens": {
551
- const child = reduceConditionals(expr.expr, opts);
552
- return applyParens(child);
553
- }
554
- case "lookup-def":
555
- return expr;
556
- case "lookup-call": {
557
- const arg = reduceConditionals(expr.arg, opts);
558
- return lookupCall(expr.varRef, arg);
559
- }
560
- case "function-call": {
561
- if (expr.fnId === "_IF_THEN_ELSE") {
562
- const conditionExpr = reduceExpr(expr.args[0], opts);
563
- if (conditionExpr.kind === "number") {
564
- const branchExpr = conditionExpr.value !== 0 ? reduceConditionals(expr.args[1], opts) : reduceConditionals(expr.args[2], opts);
565
- return applyParens(branchExpr);
566
- }
567
- }
568
- const reducedArgs = expr.args.map((arg) => reduceConditionals(arg, opts));
569
- return {
570
- kind: "function-call",
571
- fnName: expr.fnName,
572
- fnId: expr.fnId,
573
- args: reducedArgs
574
- };
575
- }
576
- default:
577
- assertNever2(expr);
578
- }
481
+ switch (expr.kind) {
482
+ case "number":
483
+ case "string":
484
+ case "keyword": return expr;
485
+ case "variable-ref": return expr;
486
+ case "unary-op": {
487
+ const child = reduceConditionals(expr.expr, opts);
488
+ return unaryOp(expr.op, child);
489
+ }
490
+ case "binary-op": {
491
+ const lhs = reduceConditionals(expr.lhs, opts);
492
+ const rhs = reduceConditionals(expr.rhs, opts);
493
+ return binaryOp(lhs, expr.op, rhs);
494
+ }
495
+ case "parens": return applyParens(reduceConditionals(expr.expr, opts));
496
+ case "lookup-def": return expr;
497
+ case "lookup-call": {
498
+ const arg = reduceConditionals(expr.arg, opts);
499
+ return lookupCall(expr.varRef, arg);
500
+ }
501
+ case "function-call": {
502
+ if (expr.fnId === "_IF_THEN_ELSE") {
503
+ const conditionExpr = reduceExpr(expr.args[0], opts);
504
+ if (conditionExpr.kind === "number") return applyParens(conditionExpr.value !== 0 ? reduceConditionals(expr.args[1], opts) : reduceConditionals(expr.args[2], opts));
505
+ }
506
+ const reducedArgs = expr.args.map((arg) => reduceConditionals(arg, opts));
507
+ return {
508
+ kind: "function-call",
509
+ fnName: expr.fnName,
510
+ fnId: expr.fnId,
511
+ args: reducedArgs
512
+ };
513
+ }
514
+ default: assertNever(expr);
515
+ }
579
516
  }
517
+ /**
518
+ * Reduce the given expression that may need to be wrapped in parentheses. If the parentheses
519
+ * are not needed, the given `child` expression will be returned as is, otherwise it will be
520
+ * wrapped in a "parens" node.
521
+ *
522
+ * @param child The child of a parens expression to reduce.
523
+ * @returns A possibly reduced expression.
524
+ */
580
525
  function applyParens(child) {
581
- switch (child.kind) {
582
- case "number":
583
- case "string":
584
- case "keyword":
585
- case "variable-ref":
586
- return child;
587
- default:
588
- return parens(child);
589
- }
526
+ switch (child.kind) {
527
+ case "number":
528
+ case "string":
529
+ case "keyword":
530
+ case "variable-ref": return child;
531
+ default: return parens(child);
532
+ }
590
533
  }
591
-
592
- // src/vensim/impl/subscript-range-reader.js
593
- import { ModelParser as ModelParser2, ModelVisitor } from "antlr4-vensim";
594
-
595
- // src/vensim/impl/antlr-parser.js
596
- import antlr4 from "antlr4";
597
- import { ModelLexer, ModelParser } from "antlr4-vensim";
534
+ //#endregion
535
+ //#region src/vensim/impl/antlr-parser.js
536
+ /**
537
+ * Create a `ModelParser` for the given model text, which can be the
538
+ * contents of an entire `mdl` file, or a portion of one (e.g., an
539
+ * expression or definition).
540
+ *
541
+ * @param {string} input The string containing the model text.
542
+ * @return {ModelParser} A `ModelParser` from which a parse tree can be obtained.
543
+ */
598
544
  function createAntlrParser(input) {
599
- const errorListener = new CustomErrorListener(input);
600
- let chars = new antlr4.InputStream(input);
601
- let lexer = new ModelLexer(chars);
602
- lexer.removeErrorListeners();
603
- lexer.addErrorListener(errorListener);
604
- let tokens = new antlr4.CommonTokenStream(lexer);
605
- let parser = new ModelParser(tokens);
606
- parser.buildParseTrees = true;
607
- parser.removeErrorListeners();
608
- parser.addErrorListener(errorListener);
609
- return parser;
545
+ const errorListener = new CustomErrorListener(input);
546
+ let chars = new antlr4.InputStream(input);
547
+ let lexer = new ModelLexer(chars);
548
+ lexer.removeErrorListeners();
549
+ lexer.addErrorListener(errorListener);
550
+ let tokens = new antlr4.CommonTokenStream(lexer);
551
+ let parser = new ModelParser(tokens);
552
+ parser.buildParseTrees = true;
553
+ parser.removeErrorListeners();
554
+ parser.addErrorListener(errorListener);
555
+ return parser;
610
556
  }
611
557
  var CustomErrorListener = class extends antlr4.error.ErrorListener {
612
- constructor(input) {
613
- super();
614
- this.input = input;
615
- }
616
- syntaxError(_recognizer, _offendingSymbol, line, column, msg) {
617
- throw new Error(msg, {
618
- cause: {
619
- code: "VensimParseError",
620
- line,
621
- column
622
- }
623
- });
624
- }
558
+ constructor(input) {
559
+ super();
560
+ this.input = input;
561
+ }
562
+ syntaxError(_recognizer, _offendingSymbol, line, column, msg) {
563
+ throw new Error(msg, { cause: {
564
+ code: "VensimParseError",
565
+ line,
566
+ column
567
+ } });
568
+ }
625
569
  };
626
-
627
- // src/vensim/impl/subscript-range-reader.js
570
+ //#endregion
571
+ //#region src/vensim/impl/subscript-range-reader.js
628
572
  var SubscriptRangeReader = class extends ModelVisitor {
629
- /**
630
- * @public
631
- * @param {import('../vensim-parse-context').VensimParseContext} parseContext An object
632
- * that provides access to file system resources (such as external data files) that are
633
- * referenced during the parse phase.
634
- */
635
- constructor(parseContext) {
636
- super();
637
- this.parseContext = parseContext;
638
- }
639
- /**
640
- * Parse the given Vensim subscript range definition and return a `DimensionDef` AST node.
641
- *
642
- * @public
643
- * @param {string} subscriptRangeText A string containing the Vensim subscript range definition.
644
- * @returns {import('../../ast/ast-types').DimensionDef} A `DimensionDef` AST node.
645
- */
646
- /*public*/
647
- parse(subscriptRangeText) {
648
- const parser = createAntlrParser(subscriptRangeText);
649
- const subscriptRangeCtx = parser.subscriptRange();
650
- return this.visitSubscriptRange(subscriptRangeCtx);
651
- }
652
- /**
653
- * Process the given ANTLR `SubscriptRangeContext` from an already parsed Vensim
654
- * subscript range definition.
655
- *
656
- * @public
657
- * @param {import('antlr4-vensim').SubscriptRangeContext} ctx The ANTLR `SubscriptRangeContext`.
658
- * @returns {import('../../ast/ast-types').Expr} A `SubscriptRange` AST node.
659
- */
660
- /*public*/
661
- visitSubscriptRange(ctx) {
662
- this.subscriptNames = [];
663
- this.subscriptMappings = [];
664
- const comment = "";
665
- const ids = ctx.Id();
666
- if (ids.length === 1) {
667
- const dimName = ids[0].getText();
668
- const dimId = canonicalId(dimName);
669
- super.visitSubscriptRange(ctx);
670
- return {
671
- dimName,
672
- dimId,
673
- familyName: dimName,
674
- familyId: dimId,
675
- subscriptRefs: this.subscriptNames.map((subName) => {
676
- return {
677
- subName,
678
- subId: canonicalId(subName)
679
- };
680
- }),
681
- subscriptMappings: this.subscriptMappings,
682
- comment
683
- };
684
- } else if (ids.length === 2) {
685
- const dimName = ids[0].getText();
686
- const dimId = canonicalId(dimName);
687
- const familyName = ids[1].getText();
688
- const familyId = canonicalId(familyName);
689
- return {
690
- dimName,
691
- dimId,
692
- familyName,
693
- familyId,
694
- subscriptRefs: [],
695
- subscriptMappings: [],
696
- comment
697
- };
698
- }
699
- }
700
- visitSubscriptDefList(ctx) {
701
- for (const subscriptDef of ctx.children) {
702
- if (subscriptDef.symbol?.type === ModelParser2.Id) {
703
- this.subscriptNames.push(subscriptDef.getText());
704
- } else if (subscriptDef.ruleIndex === ModelParser2.RULE_subscriptSequence) {
705
- this.visitSubscriptSequence(subscriptDef);
706
- }
707
- }
708
- }
709
- visitSubscriptSequence(ctx) {
710
- const re = /^(.*?)(\d+)$/;
711
- const ids = ctx.Id().map((id) => id.getText());
712
- const matches = ids.map((id) => re.exec(id));
713
- if (matches[0][1] === matches[1][1]) {
714
- const prefix = matches[0][1];
715
- const start = parseInt(matches[0][2]);
716
- const end = parseInt(matches[1][2]);
717
- for (let i = start; i <= end; i++) {
718
- this.subscriptNames.push(prefix + i);
719
- }
720
- }
721
- }
722
- visitSubscriptMapping(ctx) {
723
- const toDimName = ctx.Id().getText();
724
- this.mappedSubscriptNames = [];
725
- super.visitSubscriptMapping(ctx);
726
- this.subscriptMappings.push({
727
- toDimName,
728
- toDimId: canonicalId(toDimName),
729
- subscriptRefs: this.mappedSubscriptNames.map((subName) => {
730
- return {
731
- subName,
732
- subId: canonicalId(subName)
733
- };
734
- })
735
- });
736
- }
737
- visitSubscriptList(ctx) {
738
- this.mappedSubscriptNames = ctx.Id().map((id) => id.getText());
739
- }
740
- visitCall(ctx) {
741
- const fnName = ctx.Id().getText();
742
- const fnId = canonicalFunctionId(fnName);
743
- if (fnId === "_GET_DIRECT_SUBSCRIPT") {
744
- super.visitCall(ctx);
745
- } else {
746
- throw new Error(
747
- `Only 'GET DIRECT SUBSCRIPT' calls are supported in subscript range definitions, but saw '${fnName}'`
748
- );
749
- }
750
- }
751
- visitExprList(ctx) {
752
- const args = ctx.expr().map((expr) => {
753
- const exprText = expr.getText();
754
- return exprText.replaceAll("'", "");
755
- });
756
- const fileName = args[0];
757
- const tabOrDelimiter = args[1];
758
- const firstCell = args[2];
759
- const lastCell = args[3];
760
- const prefix = args[4];
761
- this.subscriptNames = this.parseContext?.getDirectSubscripts(fileName, tabOrDelimiter, firstCell, lastCell, prefix) || [];
762
- }
573
+ /**
574
+ * @public
575
+ * @param {import('../vensim-parse-context').VensimParseContext} parseContext An object
576
+ * that provides access to file system resources (such as external data files) that are
577
+ * referenced during the parse phase.
578
+ */
579
+ constructor(parseContext) {
580
+ super();
581
+ this.parseContext = parseContext;
582
+ }
583
+ /**
584
+ * Parse the given Vensim subscript range definition and return a `DimensionDef` AST node.
585
+ *
586
+ * @public
587
+ * @param {string} subscriptRangeText A string containing the Vensim subscript range definition.
588
+ * @returns {import('../../ast/ast-types').DimensionDef} A `DimensionDef` AST node.
589
+ */
590
+ parse(subscriptRangeText) {
591
+ const subscriptRangeCtx = createAntlrParser(subscriptRangeText).subscriptRange();
592
+ return this.visitSubscriptRange(subscriptRangeCtx);
593
+ }
594
+ /**
595
+ * Process the given ANTLR `SubscriptRangeContext` from an already parsed Vensim
596
+ * subscript range definition.
597
+ *
598
+ * @public
599
+ * @param {import('antlr4-vensim').SubscriptRangeContext} ctx The ANTLR `SubscriptRangeContext`.
600
+ * @returns {import('../../ast/ast-types').Expr} A `SubscriptRange` AST node.
601
+ */
602
+ visitSubscriptRange(ctx) {
603
+ this.subscriptNames = [];
604
+ this.subscriptMappings = [];
605
+ const comment = "";
606
+ const ids = ctx.Id();
607
+ if (ids.length === 1) {
608
+ const dimName = ids[0].getText();
609
+ const dimId = canonicalId(dimName);
610
+ super.visitSubscriptRange(ctx);
611
+ return {
612
+ dimName,
613
+ dimId,
614
+ familyName: dimName,
615
+ familyId: dimId,
616
+ subscriptRefs: this.subscriptNames.map((subName) => {
617
+ return {
618
+ subName,
619
+ subId: canonicalId(subName)
620
+ };
621
+ }),
622
+ subscriptMappings: this.subscriptMappings,
623
+ comment
624
+ };
625
+ } else if (ids.length === 2) {
626
+ const dimName = ids[0].getText();
627
+ const dimId = canonicalId(dimName);
628
+ const familyName = ids[1].getText();
629
+ return {
630
+ dimName,
631
+ dimId,
632
+ familyName,
633
+ familyId: canonicalId(familyName),
634
+ subscriptRefs: [],
635
+ subscriptMappings: [],
636
+ comment
637
+ };
638
+ }
639
+ }
640
+ visitSubscriptDefList(ctx) {
641
+ for (const subscriptDef of ctx.children) if (subscriptDef.symbol?.type === ModelParser.Id) this.subscriptNames.push(subscriptDef.getText());
642
+ else if (subscriptDef.ruleIndex === ModelParser.RULE_subscriptSequence) this.visitSubscriptSequence(subscriptDef);
643
+ }
644
+ visitSubscriptSequence(ctx) {
645
+ const re = /^(.*?)(\d+)$/;
646
+ const matches = ctx.Id().map((id) => id.getText()).map((id) => re.exec(id));
647
+ if (matches[0][1] === matches[1][1]) {
648
+ const prefix = matches[0][1];
649
+ const start = parseInt(matches[0][2]);
650
+ const end = parseInt(matches[1][2]);
651
+ for (let i = start; i <= end; i++) this.subscriptNames.push(prefix + i);
652
+ }
653
+ }
654
+ visitSubscriptMapping(ctx) {
655
+ const toDimName = ctx.Id().getText();
656
+ this.mappedSubscriptNames = [];
657
+ super.visitSubscriptMapping(ctx);
658
+ this.subscriptMappings.push({
659
+ toDimName,
660
+ toDimId: canonicalId(toDimName),
661
+ subscriptRefs: this.mappedSubscriptNames.map((subName) => {
662
+ return {
663
+ subName,
664
+ subId: canonicalId(subName)
665
+ };
666
+ })
667
+ });
668
+ }
669
+ visitSubscriptList(ctx) {
670
+ this.mappedSubscriptNames = ctx.Id().map((id) => id.getText());
671
+ }
672
+ visitCall(ctx) {
673
+ const fnName = ctx.Id().getText();
674
+ if (canonicalFunctionId(fnName) === "_GET_DIRECT_SUBSCRIPT") super.visitCall(ctx);
675
+ else throw new Error(`Only 'GET DIRECT SUBSCRIPT' calls are supported in subscript range definitions, but saw '${fnName}'`);
676
+ }
677
+ visitExprList(ctx) {
678
+ const args = ctx.expr().map((expr) => {
679
+ return expr.getText().replaceAll("'", "");
680
+ });
681
+ const fileName = args[0];
682
+ const tabOrDelimiter = args[1];
683
+ const firstCell = args[2];
684
+ const lastCell = args[3];
685
+ const prefix = args[4];
686
+ this.subscriptNames = this.parseContext?.getDirectSubscripts(fileName, tabOrDelimiter, firstCell, lastCell, prefix) || [];
687
+ }
763
688
  };
764
-
765
- // src/vensim/parse-vensim-subscript-range.ts
689
+ //#endregion
690
+ //#region src/vensim/parse-vensim-subscript-range.ts
691
+ /**
692
+ * Parse the given Vensim subscript range definition and return a `DimensionDef` AST node.
693
+ *
694
+ * @param input A string containing the Vensim subscript range definition.
695
+ * @param context An object that provides access to file system resources (such as
696
+ * external data files) that are referenced during the parse phase.
697
+ * @returns A `DimensionDef` AST node.
698
+ */
766
699
  function parseVensimSubscriptRange(input, context) {
767
- const subscriptReader = new SubscriptRangeReader(context);
768
- return subscriptReader.parse(input);
700
+ return new SubscriptRangeReader(context).parse(input);
769
701
  }
770
-
771
- // src/vensim/impl/expr-reader.js
772
- import { ModelLexer as ModelLexer2, ModelVisitor as ModelVisitor2 } from "antlr4-vensim";
773
- var ExprReader = class extends ModelVisitor2 {
774
- constructor() {
775
- super();
776
- this.callStack = [];
777
- }
778
- /**
779
- * Parse the given Vensim expression definition and return an `Expr` AST node.
780
- *
781
- * @public
782
- * @param {string} exprText A string containing the Vensim expression.
783
- * @returns {import('../../ast/ast-types').Expr} An `Expr` AST node.
784
- */
785
- /*public*/
786
- parse(exprText) {
787
- const parser = createAntlrParser(exprText);
788
- const exprCtx = parser.expr();
789
- return this.visitExpr(exprCtx);
790
- }
791
- /**
792
- * Process the given ANTLR `ExprContext` from an already parsed Vensim
793
- * expression definition.
794
- *
795
- * @public
796
- * @param {import('antlr4-vensim').ExprContext} ctx The ANTLR `ExprContext`.
797
- * @returns {import('../../ast/ast-types').Expr} An `Expr` AST node.
798
- */
799
- /*public*/
800
- visitExpr(ctx) {
801
- ctx.accept(this);
802
- return this.expr;
803
- }
804
- //
805
- // Constants
806
- //
807
- visitConst(ctx) {
808
- const text = ctx.Const().getText();
809
- if (text.startsWith("'") && text.endsWith("'")) {
810
- this.expr = {
811
- kind: "string",
812
- text: text.substr(1, text.length - 2)
813
- };
814
- } else {
815
- const value = parseFloat(text);
816
- this.expr = {
817
- kind: "number",
818
- value,
819
- text
820
- };
821
- }
822
- }
823
- //
824
- // Keywords
825
- //
826
- visitKeyword(ctx) {
827
- const text = ctx.Keyword().getText();
828
- this.expr = {
829
- kind: "keyword",
830
- text
831
- };
832
- }
833
- //
834
- // Function calls and variables
835
- //
836
- visitCall(ctx) {
837
- const vensimFnName = ctx.Id().getText();
838
- const fnId = canonicalFunctionId(vensimFnName);
839
- this.callStack.push({ fn: fnId, args: [] });
840
- super.visitCall(ctx);
841
- const callInfo = this.callStack.pop();
842
- this.expr = {
843
- kind: "function-call",
844
- fnName: vensimFnName,
845
- fnId,
846
- args: callInfo.args
847
- };
848
- }
849
- visitExprList(ctx) {
850
- const exprs = ctx.expr();
851
- for (let i = 0; i < exprs.length; i++) {
852
- exprs[i].accept(this);
853
- const n = this.callStack.length;
854
- if (n > 0) {
855
- this.callStack[n - 1].args.push(this.expr);
856
- }
857
- }
858
- }
859
- visitVar(ctx) {
860
- const vensimVarName = ctx.Id().getText().trim();
861
- const varId = canonicalId(vensimVarName);
862
- this.subscripts = void 0;
863
- super.visitVar(ctx);
864
- const subscriptNames = this.subscripts;
865
- const subscriptRefs = subscriptNames?.map((name) => {
866
- return {
867
- subName: name,
868
- subId: canonicalId(name)
869
- };
870
- });
871
- this.subscripts = void 0;
872
- this.expr = {
873
- kind: "variable-ref",
874
- varName: vensimVarName,
875
- varId,
876
- subscriptRefs
877
- };
878
- }
879
- visitSubscriptList(ctx) {
880
- this.subscripts = ctx.Id().map((id) => id.getText());
881
- }
882
- //
883
- // Lookups
884
- //
885
- getPoint(lookupPoint) {
886
- const exprs = lookupPoint.expr();
887
- if (exprs.length >= 2) {
888
- return [parseFloat(exprs[0].getText()), parseFloat(exprs[1].getText())];
889
- }
890
- }
891
- visitLookupRange(ctx) {
892
- this.lookupRange = ctx.lookupPoint().map((p) => this.getPoint(p));
893
- super.visitLookupRange(ctx);
894
- }
895
- visitLookupPointList(ctx) {
896
- this.lookupPoints = ctx.lookupPoint().map((p) => this.getPoint(p));
897
- super.visitLookupPointList(ctx);
898
- }
899
- visitLookupArg(ctx) {
900
- super.visitLookupArg(ctx);
901
- let range;
902
- if (this.lookupRange && this.lookupRange.length === 2) {
903
- range = {
904
- min: this.lookupRange[0],
905
- max: this.lookupRange[1]
906
- };
907
- }
908
- this.expr = {
909
- kind: "lookup-def",
910
- range,
911
- points: this.lookupPoints
912
- };
913
- this.lookupRange = void 0;
914
- this.lookupPoints = void 0;
915
- }
916
- visitLookupCall(ctx) {
917
- const lookupVarName = ctx.Id().getText();
918
- const lookupVarId = canonicalId(lookupVarName);
919
- if (ctx.subscriptList()) {
920
- ctx.subscriptList().accept(this);
921
- }
922
- const subscriptNames = this.subscripts;
923
- const subscriptRefs = subscriptNames?.map((name) => {
924
- return {
925
- subName: name,
926
- subId: canonicalId(name)
927
- };
928
- });
929
- this.subscripts = void 0;
930
- const lookupVarRef = {
931
- kind: "variable-ref",
932
- varName: lookupVarName,
933
- varId: lookupVarId,
934
- subscriptRefs
935
- };
936
- ctx.expr().accept(this);
937
- const lookupArg = this.expr;
938
- this.expr = {
939
- kind: "lookup-call",
940
- varRef: lookupVarRef,
941
- arg: lookupArg
942
- };
943
- }
944
- //
945
- // Unary operators
946
- //
947
- completeUnary(op) {
948
- const child = this.expr;
949
- this.expr = {
950
- kind: "unary-op",
951
- op,
952
- expr: child
953
- };
954
- }
955
- visitNegative(ctx) {
956
- super.visitNegative(ctx);
957
- this.completeUnary("-");
958
- }
959
- visitPositive(ctx) {
960
- super.visitPositive(ctx);
961
- this.completeUnary("+");
962
- }
963
- visitNot(ctx) {
964
- super.visitNot(ctx);
965
- this.completeUnary(":NOT:");
966
- }
967
- //
968
- // Binary operators
969
- //
970
- visitBinaryArgs(ctx, op) {
971
- ctx.expr(0).accept(this);
972
- const lhs = this.expr;
973
- ctx.expr(1).accept(this);
974
- const rhs = this.expr;
975
- this.expr = {
976
- kind: "binary-op",
977
- lhs,
978
- op,
979
- rhs
980
- };
981
- }
982
- visitPower(ctx) {
983
- this.visitBinaryArgs(ctx, "^");
984
- }
985
- visitMulDiv(ctx) {
986
- this.visitBinaryArgs(ctx, ctx.op.type === ModelLexer2.Star ? "*" : "/");
987
- }
988
- visitAddSub(ctx) {
989
- this.visitBinaryArgs(ctx, ctx.op.type === ModelLexer2.Plus ? "+" : "-");
990
- }
991
- visitRelational(ctx) {
992
- let op;
993
- switch (ctx.op.type) {
994
- case ModelLexer2.Less:
995
- op = "<";
996
- break;
997
- case ModelLexer2.Greater:
998
- op = ">";
999
- break;
1000
- case ModelLexer2.LessEqual:
1001
- op = "<=";
1002
- break;
1003
- case ModelLexer2.GreaterEqual:
1004
- op = ">=";
1005
- break;
1006
- default:
1007
- throw new Error(`Unexpected relational operator '${op}'`);
1008
- }
1009
- this.visitBinaryArgs(ctx, op);
1010
- }
1011
- visitEquality(ctx) {
1012
- this.visitBinaryArgs(ctx, ctx.op.type === ModelLexer2.Equal ? "=" : "<>");
1013
- }
1014
- visitAnd(ctx) {
1015
- this.visitBinaryArgs(ctx, ":AND:");
1016
- }
1017
- visitOr(ctx) {
1018
- this.visitBinaryArgs(ctx, ":OR:");
1019
- }
1020
- //
1021
- // Tokens
1022
- //
1023
- visitParens(ctx) {
1024
- super.visitParens(ctx);
1025
- const child = this.expr;
1026
- this.expr = {
1027
- kind: "parens",
1028
- expr: child
1029
- };
1030
- }
702
+ //#endregion
703
+ //#region src/vensim/impl/expr-reader.js
704
+ var ExprReader = class extends ModelVisitor {
705
+ constructor() {
706
+ super();
707
+ this.callStack = [];
708
+ }
709
+ /**
710
+ * Parse the given Vensim expression definition and return an `Expr` AST node.
711
+ *
712
+ * @public
713
+ * @param {string} exprText A string containing the Vensim expression.
714
+ * @returns {import('../../ast/ast-types').Expr} An `Expr` AST node.
715
+ */
716
+ parse(exprText) {
717
+ const exprCtx = createAntlrParser(exprText).expr();
718
+ return this.visitExpr(exprCtx);
719
+ }
720
+ /**
721
+ * Process the given ANTLR `ExprContext` from an already parsed Vensim
722
+ * expression definition.
723
+ *
724
+ * @public
725
+ * @param {import('antlr4-vensim').ExprContext} ctx The ANTLR `ExprContext`.
726
+ * @returns {import('../../ast/ast-types').Expr} An `Expr` AST node.
727
+ */
728
+ visitExpr(ctx) {
729
+ ctx.accept(this);
730
+ return this.expr;
731
+ }
732
+ visitConst(ctx) {
733
+ const text = ctx.Const().getText();
734
+ if (text.startsWith("'") && text.endsWith("'")) this.expr = {
735
+ kind: "string",
736
+ text: text.substr(1, text.length - 2)
737
+ };
738
+ else {
739
+ const value = parseFloat(text);
740
+ this.expr = {
741
+ kind: "number",
742
+ value,
743
+ text
744
+ };
745
+ }
746
+ }
747
+ visitKeyword(ctx) {
748
+ const text = ctx.Keyword().getText();
749
+ this.expr = {
750
+ kind: "keyword",
751
+ text
752
+ };
753
+ }
754
+ visitCall(ctx) {
755
+ const vensimFnName = ctx.Id().getText();
756
+ const fnId = canonicalFunctionId(vensimFnName);
757
+ this.callStack.push({
758
+ fn: fnId,
759
+ args: []
760
+ });
761
+ super.visitCall(ctx);
762
+ const callInfo = this.callStack.pop();
763
+ this.expr = {
764
+ kind: "function-call",
765
+ fnName: vensimFnName,
766
+ fnId,
767
+ args: callInfo.args
768
+ };
769
+ }
770
+ visitExprList(ctx) {
771
+ const exprs = ctx.expr();
772
+ for (let i = 0; i < exprs.length; i++) {
773
+ exprs[i].accept(this);
774
+ const n = this.callStack.length;
775
+ if (n > 0) this.callStack[n - 1].args.push(this.expr);
776
+ }
777
+ }
778
+ visitVar(ctx) {
779
+ const vensimVarName = ctx.Id().getText().trim();
780
+ const varId = canonicalId(vensimVarName);
781
+ this.subscripts = void 0;
782
+ super.visitVar(ctx);
783
+ const subscriptRefs = this.subscripts?.map((name) => {
784
+ return {
785
+ subName: name,
786
+ subId: canonicalId(name)
787
+ };
788
+ });
789
+ this.subscripts = void 0;
790
+ this.expr = {
791
+ kind: "variable-ref",
792
+ varName: vensimVarName,
793
+ varId,
794
+ subscriptRefs
795
+ };
796
+ }
797
+ visitSubscriptList(ctx) {
798
+ this.subscripts = ctx.Id().map((id) => id.getText());
799
+ }
800
+ getPoint(lookupPoint) {
801
+ const exprs = lookupPoint.expr();
802
+ if (exprs.length >= 2) return [parseFloat(exprs[0].getText()), parseFloat(exprs[1].getText())];
803
+ }
804
+ visitLookupRange(ctx) {
805
+ this.lookupRange = ctx.lookupPoint().map((p) => this.getPoint(p));
806
+ super.visitLookupRange(ctx);
807
+ }
808
+ visitLookupPointList(ctx) {
809
+ this.lookupPoints = ctx.lookupPoint().map((p) => this.getPoint(p));
810
+ super.visitLookupPointList(ctx);
811
+ }
812
+ visitLookupArg(ctx) {
813
+ super.visitLookupArg(ctx);
814
+ let range;
815
+ if (this.lookupRange && this.lookupRange.length === 2) range = {
816
+ min: this.lookupRange[0],
817
+ max: this.lookupRange[1]
818
+ };
819
+ this.expr = {
820
+ kind: "lookup-def",
821
+ range,
822
+ points: this.lookupPoints
823
+ };
824
+ this.lookupRange = void 0;
825
+ this.lookupPoints = void 0;
826
+ }
827
+ visitLookupCall(ctx) {
828
+ const lookupVarName = ctx.Id().getText();
829
+ const lookupVarId = canonicalId(lookupVarName);
830
+ if (ctx.subscriptList()) ctx.subscriptList().accept(this);
831
+ const subscriptRefs = this.subscripts?.map((name) => {
832
+ return {
833
+ subName: name,
834
+ subId: canonicalId(name)
835
+ };
836
+ });
837
+ this.subscripts = void 0;
838
+ const lookupVarRef = {
839
+ kind: "variable-ref",
840
+ varName: lookupVarName,
841
+ varId: lookupVarId,
842
+ subscriptRefs
843
+ };
844
+ ctx.expr().accept(this);
845
+ const lookupArg = this.expr;
846
+ this.expr = {
847
+ kind: "lookup-call",
848
+ varRef: lookupVarRef,
849
+ arg: lookupArg
850
+ };
851
+ }
852
+ completeUnary(op) {
853
+ const child = this.expr;
854
+ this.expr = {
855
+ kind: "unary-op",
856
+ op,
857
+ expr: child
858
+ };
859
+ }
860
+ visitNegative(ctx) {
861
+ super.visitNegative(ctx);
862
+ this.completeUnary("-");
863
+ }
864
+ visitPositive(ctx) {
865
+ super.visitPositive(ctx);
866
+ this.completeUnary("+");
867
+ }
868
+ visitNot(ctx) {
869
+ super.visitNot(ctx);
870
+ this.completeUnary(":NOT:");
871
+ }
872
+ visitBinaryArgs(ctx, op) {
873
+ ctx.expr(0).accept(this);
874
+ const lhs = this.expr;
875
+ ctx.expr(1).accept(this);
876
+ const rhs = this.expr;
877
+ this.expr = {
878
+ kind: "binary-op",
879
+ lhs,
880
+ op,
881
+ rhs
882
+ };
883
+ }
884
+ visitPower(ctx) {
885
+ this.visitBinaryArgs(ctx, "^");
886
+ }
887
+ visitMulDiv(ctx) {
888
+ this.visitBinaryArgs(ctx, ctx.op.type === ModelLexer.Star ? "*" : "/");
889
+ }
890
+ visitAddSub(ctx) {
891
+ this.visitBinaryArgs(ctx, ctx.op.type === ModelLexer.Plus ? "+" : "-");
892
+ }
893
+ visitRelational(ctx) {
894
+ let op;
895
+ switch (ctx.op.type) {
896
+ case ModelLexer.Less:
897
+ op = "<";
898
+ break;
899
+ case ModelLexer.Greater:
900
+ op = ">";
901
+ break;
902
+ case ModelLexer.LessEqual:
903
+ op = "<=";
904
+ break;
905
+ case ModelLexer.GreaterEqual:
906
+ op = ">=";
907
+ break;
908
+ default: throw new Error(`Unexpected relational operator '${op}'`);
909
+ }
910
+ this.visitBinaryArgs(ctx, op);
911
+ }
912
+ visitEquality(ctx) {
913
+ this.visitBinaryArgs(ctx, ctx.op.type === ModelLexer.Equal ? "=" : "<>");
914
+ }
915
+ visitAnd(ctx) {
916
+ this.visitBinaryArgs(ctx, ":AND:");
917
+ }
918
+ visitOr(ctx) {
919
+ this.visitBinaryArgs(ctx, ":OR:");
920
+ }
921
+ visitParens(ctx) {
922
+ super.visitParens(ctx);
923
+ const child = this.expr;
924
+ this.expr = {
925
+ kind: "parens",
926
+ expr: child
927
+ };
928
+ }
1031
929
  };
1032
-
1033
- // src/vensim/parse-vensim-expr.ts
930
+ //#endregion
931
+ //#region src/vensim/parse-vensim-expr.ts
932
+ /**
933
+ * Parse the given Vensim expression definition and return an `Expr` AST node.
934
+ *
935
+ * @param input A string containing the Vensim expression.
936
+ * @returns An `Expr` AST node.
937
+ */
1034
938
  function parseVensimExpr(input) {
1035
- const exprReader = new ExprReader();
1036
- return exprReader.parse(input);
939
+ return new ExprReader().parse(input);
1037
940
  }
1038
-
1039
- // src/vensim/impl/equation-reader.js
1040
- import { ModelVisitor as ModelVisitor3 } from "antlr4-vensim";
1041
- var EquationReader = class extends ModelVisitor3 {
1042
- constructor() {
1043
- super();
1044
- }
1045
- /**
1046
- * Parse the given Vensim equation definition and return an `Equation` AST node.
1047
- *
1048
- * @public
1049
- * @param {string} equationText A string containing the Vensim equation definition.
1050
- * @return {import('../../ast/ast-types').Equation} An `Equation` AST node.
1051
- */
1052
- /*public*/
1053
- parse(equationText) {
1054
- const parser = createAntlrParser(equationText);
1055
- const equationCtx = parser.equation();
1056
- return this.visitEquation(equationCtx);
1057
- }
1058
- /**
1059
- * Process the given ANTLR `EquationContext` from an already parsed Vensim
1060
- * equation definition.
1061
- *
1062
- * @public
1063
- * @param {import('antlr4-vensim').EquationContext} ctx The ANTLR `EquationContext`.
1064
- * @returns {import('../../ast/ast-types').Equation} An `Equation` AST node.
1065
- */
1066
- /*public*/
1067
- visitEquation(ctx) {
1068
- this.equationLhs = void 0;
1069
- this.lookupDef = void 0;
1070
- ctx.lhs().accept(this);
1071
- let equationRhs;
1072
- const exprCtx = ctx.expr();
1073
- if (exprCtx) {
1074
- const exprReader = new ExprReader();
1075
- const expr = exprReader.visitExpr(exprCtx);
1076
- equationRhs = {
1077
- kind: "expr",
1078
- expr
1079
- };
1080
- } else if (ctx.constList()) {
1081
- ctx.constList().accept(this);
1082
- equationRhs = {
1083
- kind: "const-list",
1084
- constants: this.constants,
1085
- text: this.constListText
1086
- };
1087
- } else if (ctx.lookup()) {
1088
- ctx.lookup().accept(this);
1089
- equationRhs = {
1090
- kind: "lookup",
1091
- lookupDef: this.lookupDef
1092
- };
1093
- } else {
1094
- equationRhs = {
1095
- kind: "data"
1096
- };
1097
- }
1098
- if (this.equationLhs) {
1099
- this.equation = {
1100
- lhs: this.equationLhs,
1101
- rhs: equationRhs,
1102
- // TODO: For now, fill in an empty string for these two; this is mainly
1103
- // for compatibility with unit tests that expect empty string instead of
1104
- // undefined, but this should be revisited
1105
- units: "",
1106
- comment: ""
1107
- };
1108
- }
1109
- return this.equation;
1110
- }
1111
- visitSubscriptList(ctx) {
1112
- if (this.subscripts === void 0) {
1113
- this.subscripts = ctx.Id().map((id) => id.getText());
1114
- } else {
1115
- if (this.exceptSubscriptSets === void 0) {
1116
- this.exceptSubscriptSets = [];
1117
- }
1118
- this.exceptSubscriptSets.push(ctx.Id().map((id) => id.getText()));
1119
- }
1120
- }
1121
- visitLhs(ctx) {
1122
- const lhsVarName = ctx.Id().getText();
1123
- const lhsVarId = canonicalId(lhsVarName);
1124
- super.visitLhs(ctx);
1125
- const subscriptNames = this.subscripts;
1126
- const subscriptRefs = subscriptNames?.map((name) => {
1127
- return {
1128
- subName: name,
1129
- subId: canonicalId(name)
1130
- };
1131
- });
1132
- const exceptSubscriptSets = this.exceptSubscriptSets;
1133
- const exceptSubscriptRefSets = exceptSubscriptSets?.map((subscriptSet) => {
1134
- return subscriptSet.map((name) => {
1135
- return {
1136
- subName: name,
1137
- subId: canonicalId(name)
1138
- };
1139
- });
1140
- });
1141
- this.subscripts = void 0;
1142
- this.exceptSubscripts = void 0;
1143
- this.equationLhs = {
1144
- varDef: {
1145
- kind: "variable-def",
1146
- varName: lhsVarName,
1147
- varId: lhsVarId,
1148
- subscriptRefs,
1149
- exceptSubscriptRefSets
1150
- }
1151
- };
1152
- }
1153
- //
1154
- // CONST LISTS
1155
- //
1156
- visitConstList(ctx) {
1157
- this.constants = ctx.expr().map((expr) => {
1158
- const text = expr.getText();
1159
- const value = parseFloat(text);
1160
- return {
1161
- kind: "number",
1162
- value,
1163
- text
1164
- };
1165
- });
1166
- this.constListText = ctx.getText();
1167
- }
1168
- //
1169
- // LOOKUPS
1170
- //
1171
- getPoint(lookupPoint) {
1172
- const exprs = lookupPoint.expr();
1173
- if (exprs.length >= 2) {
1174
- return [parseFloat(exprs[0].getText()), parseFloat(exprs[1].getText())];
1175
- }
1176
- }
1177
- visitLookup(ctx) {
1178
- this.lookupRange = void 0;
1179
- this.lookupPoints = void 0;
1180
- if (ctx.lookupRange()) {
1181
- ctx.lookupRange().accept(this);
1182
- }
1183
- if (ctx.lookupPointList()) {
1184
- ctx.lookupPointList().accept(this);
1185
- }
1186
- let range;
1187
- if (this.lookupRange && this.lookupRange.length === 2) {
1188
- range = {
1189
- min: this.lookupRange[0],
1190
- max: this.lookupRange[1]
1191
- };
1192
- }
1193
- this.lookupDef = {
1194
- kind: "lookup-def",
1195
- range,
1196
- points: this.lookupPoints
1197
- };
1198
- }
1199
- visitLookupRange(ctx) {
1200
- this.lookupRange = ctx.lookupPoint().map((p) => this.getPoint(p));
1201
- super.visitLookupRange(ctx);
1202
- }
1203
- visitLookupPointList(ctx) {
1204
- this.lookupPoints = ctx.lookupPoint().map((p) => this.getPoint(p));
1205
- super.visitLookupPointList(ctx);
1206
- }
941
+ //#endregion
942
+ //#region src/vensim/impl/equation-reader.js
943
+ var EquationReader = class extends ModelVisitor {
944
+ constructor() {
945
+ super();
946
+ }
947
+ /**
948
+ * Parse the given Vensim equation definition and return an `Equation` AST node.
949
+ *
950
+ * @public
951
+ * @param {string} equationText A string containing the Vensim equation definition.
952
+ * @return {import('../../ast/ast-types').Equation} An `Equation` AST node.
953
+ */
954
+ parse(equationText) {
955
+ const equationCtx = createAntlrParser(equationText).equation();
956
+ return this.visitEquation(equationCtx);
957
+ }
958
+ /**
959
+ * Process the given ANTLR `EquationContext` from an already parsed Vensim
960
+ * equation definition.
961
+ *
962
+ * @public
963
+ * @param {import('antlr4-vensim').EquationContext} ctx The ANTLR `EquationContext`.
964
+ * @returns {import('../../ast/ast-types').Equation} An `Equation` AST node.
965
+ */
966
+ visitEquation(ctx) {
967
+ this.equationLhs = void 0;
968
+ this.lookupDef = void 0;
969
+ ctx.lhs().accept(this);
970
+ let equationRhs;
971
+ const exprCtx = ctx.expr();
972
+ if (exprCtx) equationRhs = {
973
+ kind: "expr",
974
+ expr: new ExprReader().visitExpr(exprCtx)
975
+ };
976
+ else if (ctx.constList()) {
977
+ ctx.constList().accept(this);
978
+ equationRhs = {
979
+ kind: "const-list",
980
+ constants: this.constants,
981
+ text: this.constListText
982
+ };
983
+ } else if (ctx.lookup()) {
984
+ ctx.lookup().accept(this);
985
+ equationRhs = {
986
+ kind: "lookup",
987
+ lookupDef: this.lookupDef
988
+ };
989
+ } else equationRhs = { kind: "data" };
990
+ if (this.equationLhs) this.equation = {
991
+ lhs: this.equationLhs,
992
+ rhs: equationRhs,
993
+ units: "",
994
+ comment: ""
995
+ };
996
+ return this.equation;
997
+ }
998
+ visitSubscriptList(ctx) {
999
+ if (this.subscripts === void 0) this.subscripts = ctx.Id().map((id) => id.getText());
1000
+ else {
1001
+ if (this.exceptSubscriptSets === void 0) this.exceptSubscriptSets = [];
1002
+ this.exceptSubscriptSets.push(ctx.Id().map((id) => id.getText()));
1003
+ }
1004
+ }
1005
+ visitLhs(ctx) {
1006
+ const lhsVarName = ctx.Id().getText();
1007
+ const lhsVarId = canonicalId(lhsVarName);
1008
+ super.visitLhs(ctx);
1009
+ const subscriptRefs = this.subscripts?.map((name) => {
1010
+ return {
1011
+ subName: name,
1012
+ subId: canonicalId(name)
1013
+ };
1014
+ });
1015
+ const exceptSubscriptRefSets = this.exceptSubscriptSets?.map((subscriptSet) => {
1016
+ return subscriptSet.map((name) => {
1017
+ return {
1018
+ subName: name,
1019
+ subId: canonicalId(name)
1020
+ };
1021
+ });
1022
+ });
1023
+ this.subscripts = void 0;
1024
+ this.exceptSubscripts = void 0;
1025
+ this.equationLhs = { varDef: {
1026
+ kind: "variable-def",
1027
+ varName: lhsVarName,
1028
+ varId: lhsVarId,
1029
+ subscriptRefs,
1030
+ exceptSubscriptRefSets
1031
+ } };
1032
+ }
1033
+ visitConstList(ctx) {
1034
+ this.constants = ctx.expr().map((expr) => {
1035
+ const text = expr.getText();
1036
+ return {
1037
+ kind: "number",
1038
+ value: parseFloat(text),
1039
+ text
1040
+ };
1041
+ });
1042
+ this.constListText = ctx.getText();
1043
+ }
1044
+ getPoint(lookupPoint) {
1045
+ const exprs = lookupPoint.expr();
1046
+ if (exprs.length >= 2) return [parseFloat(exprs[0].getText()), parseFloat(exprs[1].getText())];
1047
+ }
1048
+ visitLookup(ctx) {
1049
+ this.lookupRange = void 0;
1050
+ this.lookupPoints = void 0;
1051
+ if (ctx.lookupRange()) ctx.lookupRange().accept(this);
1052
+ if (ctx.lookupPointList()) ctx.lookupPointList().accept(this);
1053
+ let range;
1054
+ if (this.lookupRange && this.lookupRange.length === 2) range = {
1055
+ min: this.lookupRange[0],
1056
+ max: this.lookupRange[1]
1057
+ };
1058
+ this.lookupDef = {
1059
+ kind: "lookup-def",
1060
+ range,
1061
+ points: this.lookupPoints
1062
+ };
1063
+ }
1064
+ visitLookupRange(ctx) {
1065
+ this.lookupRange = ctx.lookupPoint().map((p) => this.getPoint(p));
1066
+ super.visitLookupRange(ctx);
1067
+ }
1068
+ visitLookupPointList(ctx) {
1069
+ this.lookupPoints = ctx.lookupPoint().map((p) => this.getPoint(p));
1070
+ super.visitLookupPointList(ctx);
1071
+ }
1207
1072
  };
1208
-
1209
- // src/vensim/parse-vensim-equation.ts
1073
+ //#endregion
1074
+ //#region src/vensim/parse-vensim-equation.ts
1075
+ /**
1076
+ * Parse the given Vensim equation definition and return an `Equation` AST node.
1077
+ *
1078
+ * @param input A string containing the Vensim equation definition.
1079
+ * @returns An `Equation` AST node.
1080
+ */
1210
1081
  function parseVensimEquation(input) {
1211
- const equationReader = new EquationReader();
1212
- return equationReader.parse(input);
1082
+ return new EquationReader().parse(input);
1213
1083
  }
1214
-
1215
- // src/vensim/preprocess-vensim.ts
1216
- import split from "split-string";
1084
+ //#endregion
1085
+ //#region src/vensim/preprocess-vensim.ts
1086
+ /**
1087
+ * Process the given Vensim model content so that it can be parsed
1088
+ * by `antlr4-vensim`. This will:
1089
+ * - strip out group markers
1090
+ * - remove macro definitions, which are currently unsupported
1091
+ * - remove equations that reference certain unsupported functions
1092
+ * (e.g., `TABBED ARRAY`)
1093
+ * - remove everything in the private Vensim sketch section
1094
+ * - join lines that are separated by a continuation (backslash)
1095
+ * - split the input into distinct definitions (equations and
1096
+ * subscript ranges)
1097
+ *
1098
+ * The definitions are further processed to preserve the units and
1099
+ * comment text in separate properties, but strips them from the
1100
+ * equation string (replaced with `~~`) to make it easier for
1101
+ * `antlr4-vensim` to process.
1102
+ *
1103
+ * @param input The original Vensim mdl file content.
1104
+ * @param options The options that control preprocessing.
1105
+ * @return A `PreprocessedVensimModel` instance containing the preprocessed
1106
+ * Vensim definitions.
1107
+ */
1217
1108
  function preprocessVensimModel(input, options) {
1218
- const removalKeys = options?.removalKeys;
1219
- function shouldRemove(text) {
1220
- if (text.includes("TABBED ARRAY")) {
1221
- return true;
1222
- }
1223
- if (removalKeys) {
1224
- for (const key of removalKeys) {
1225
- if (text.includes(key)) {
1226
- return true;
1227
- }
1228
- }
1229
- }
1230
- return false;
1231
- }
1232
- const macrosResult = removeMacros(input);
1233
- input = macrosResult.processed;
1234
- const rawDefs = splitDefs(input);
1235
- const vensimDefs = [];
1236
- const removedBlocks = [];
1237
- for (const rawDef of rawDefs) {
1238
- if (shouldRemove(rawDef.text)) {
1239
- removedBlocks.push(rawDef.text.trim() + "|");
1240
- continue;
1241
- }
1242
- const vensimDef = processDef(rawDef);
1243
- if (vensimDef) {
1244
- vensimDefs.push(vensimDef);
1245
- }
1246
- }
1247
- return {
1248
- defs: vensimDefs,
1249
- removedMacros: macrosResult.removed,
1250
- removedBlocks
1251
- };
1109
+ const removalKeys = options?.removalKeys;
1110
+ function shouldRemove(text) {
1111
+ if (text.includes("TABBED ARRAY")) return true;
1112
+ if (removalKeys) {
1113
+ for (const key of removalKeys) if (text.includes(key)) return true;
1114
+ }
1115
+ return false;
1116
+ }
1117
+ const macrosResult = removeMacros(input);
1118
+ input = macrosResult.processed;
1119
+ const rawDefs = splitDefs(input);
1120
+ const vensimDefs = [];
1121
+ const removedBlocks = [];
1122
+ for (const rawDef of rawDefs) {
1123
+ if (shouldRemove(rawDef.text)) {
1124
+ removedBlocks.push(rawDef.text.trim() + "|");
1125
+ continue;
1126
+ }
1127
+ const vensimDef = processDef(rawDef);
1128
+ if (vensimDef) vensimDefs.push(vensimDef);
1129
+ }
1130
+ return {
1131
+ defs: vensimDefs,
1132
+ removedMacros: macrosResult.removed,
1133
+ removedBlocks
1134
+ };
1252
1135
  }
1136
+ /**
1137
+ * Split a Vensim model string into an array of definitions (including
1138
+ * equations, subscript ranges, and groups), without the "|" terminator.
1139
+ * This will allow "|" to occur in quoted variable names across line breaks.
1140
+ * Backslash characters will be retained.
1141
+ */
1253
1142
  function splitDefs(input) {
1254
- const defTexts = split(input, { separator: "|", quotes: ['"'], keep: () => true });
1255
- const rawDefs = [];
1256
- let lineNum = 1;
1257
- let currentGroup;
1258
- for (let defText of defTexts) {
1259
- if (lineNum === 1) {
1260
- defText = defText.replace("{UTF-8}", "");
1261
- }
1262
- if (defText.includes("\\---/// Sketch")) {
1263
- break;
1264
- }
1265
- const parts = defText.match(/(\s*)(.*)/ms);
1266
- const leadingLineBreaks = parts[1]?.match(/\r\n|\n|\r/gm);
1267
- lineNum += leadingLineBreaks?.length || 0;
1268
- if (defText.includes("********************************************************")) {
1269
- const groupLines = splitLines(defText).filter((s) => s.trim().length > 0);
1270
- currentGroup = void 0;
1271
- if (groupLines.length > 1) {
1272
- const groupNameLine = groupLines[1];
1273
- const groupNameParts = groupNameLine.match(/^\s*\.(.*)$/);
1274
- if (groupNameParts) {
1275
- currentGroup = groupNameParts[1];
1276
- }
1277
- }
1278
- } else {
1279
- rawDefs.push({
1280
- text: defText,
1281
- line: lineNum,
1282
- group: currentGroup
1283
- });
1284
- }
1285
- const contentLineBreaks = parts[2]?.match(/\r\n|\n|\r/gm);
1286
- lineNum += contentLineBreaks?.length || 0;
1287
- }
1288
- return rawDefs;
1143
+ const defTexts = split(input, {
1144
+ separator: "|",
1145
+ quotes: ["\""],
1146
+ keep: () => true
1147
+ });
1148
+ const rawDefs = [];
1149
+ let lineNum = 1;
1150
+ let currentGroup;
1151
+ for (let defText of defTexts) {
1152
+ if (lineNum === 1) defText = defText.replace("{UTF-8}", "");
1153
+ if (defText.includes("\\---/// Sketch")) break;
1154
+ const parts = defText.match(/(\s*)(.*)/ms);
1155
+ const leadingLineBreaks = parts[1]?.match(/\r\n|\n|\r/gm);
1156
+ lineNum += leadingLineBreaks?.length || 0;
1157
+ if (defText.includes("********************************************************")) {
1158
+ const groupLines = splitLines(defText).filter((s) => s.trim().length > 0);
1159
+ currentGroup = void 0;
1160
+ if (groupLines.length > 1) {
1161
+ const groupNameParts = groupLines[1].match(/^\s*\.(.*)$/);
1162
+ if (groupNameParts) currentGroup = groupNameParts[1];
1163
+ }
1164
+ } else rawDefs.push({
1165
+ text: defText,
1166
+ line: lineNum,
1167
+ group: currentGroup
1168
+ });
1169
+ const contentLineBreaks = parts[2]?.match(/\r\n|\n|\r/gm);
1170
+ lineNum += contentLineBreaks?.length || 0;
1171
+ }
1172
+ return rawDefs;
1289
1173
  }
1174
+ /**
1175
+ * Split a string into separate lines.
1176
+ */
1290
1177
  function splitLines(input) {
1291
- return input.split(/\r\n|\n|\r/);
1178
+ return input.split(/\r\n|\n|\r/);
1292
1179
  }
1180
+ /**
1181
+ * Split a string on the given separator character. This will not split
1182
+ * if the character appears inside a quoted variable name. For example,
1183
+ * this will not split on '(' when it appears in the variable name, but
1184
+ * will split the '(' in the lookup expression
1185
+ * "quoted variable (name)" ((0,0),(1,1)) ~~|
1186
+ */
1293
1187
  function splitExceptInQuoted(input, sep) {
1294
- return split(input, { separator: sep, quotes: ['"'] });
1188
+ return split(input, {
1189
+ separator: sep,
1190
+ quotes: ["\""]
1191
+ });
1295
1192
  }
1193
+ /**
1194
+ * Join lines that are separated by a continuation (backslash) character.
1195
+ */
1296
1196
  function processBackslashes(input) {
1297
- const inputLines = splitLines(input);
1298
- let output = "";
1299
- let prevLine = "";
1300
- for (let line of inputLines) {
1301
- if (prevLine !== "") {
1302
- line = prevLine + line.trim();
1303
- prevLine = "";
1304
- }
1305
- const continuation = line.match(/\\\s*$/);
1306
- if (continuation) {
1307
- prevLine = line.substr(0, continuation.index).replace(/\s+$/, " ");
1308
- } else {
1309
- output += line + "\n";
1310
- }
1311
- }
1312
- return output;
1197
+ const inputLines = splitLines(input);
1198
+ let output = "";
1199
+ let prevLine = "";
1200
+ for (let line of inputLines) {
1201
+ if (prevLine !== "") {
1202
+ line = prevLine + line.trim();
1203
+ prevLine = "";
1204
+ }
1205
+ const continuation = line.match(/\\\s*$/);
1206
+ if (continuation) prevLine = line.substr(0, continuation.index).replace(/\s+$/, " ");
1207
+ else output += line + "\n";
1208
+ }
1209
+ return output;
1313
1210
  }
1211
+ /**
1212
+ * Match delimiters recursively. Replace delimited strings globally.
1213
+ *
1214
+ * @param str The string to operate on.
1215
+ * @param open The opening delimiter characters.
1216
+ * @param close The closing delimiter characters.
1217
+ * @param newStr The string to replace delimited substrings with.
1218
+ */
1314
1219
  function replaceDelimitedStrings(str, open, close, newStr) {
1315
- let result = "";
1316
- let start = 0;
1317
- let depth = 0;
1318
- const n = str.length;
1319
- for (let i = 0; i < n; i++) {
1320
- if (str.charAt(i) === open) {
1321
- if (depth === 0) {
1322
- result += str.substring(start, i);
1323
- }
1324
- depth++;
1325
- } else if (str.charAt(i) === close && depth > 0) {
1326
- depth--;
1327
- if (depth === 0) {
1328
- result += newStr;
1329
- start = i + 1;
1330
- }
1331
- }
1332
- }
1333
- if (start < n) {
1334
- result += str.substring(start);
1335
- }
1336
- return result;
1220
+ let result = "";
1221
+ let start = 0;
1222
+ let depth = 0;
1223
+ const n = str.length;
1224
+ for (let i = 0; i < n; i++) if (str.charAt(i) === open) {
1225
+ if (depth === 0) result += str.substring(start, i);
1226
+ depth++;
1227
+ } else if (str.charAt(i) === close && depth > 0) {
1228
+ depth--;
1229
+ if (depth === 0) {
1230
+ result += newStr;
1231
+ start = i + 1;
1232
+ }
1233
+ }
1234
+ if (start < n) result += str.substring(start);
1235
+ return result;
1337
1236
  }
1237
+ /**
1238
+ * Replace all newlines and redundant whitespace into single spaces so
1239
+ * that everything is on a single line.
1240
+ */
1338
1241
  function reduceWhitespace(input) {
1339
- return input.replace(/\s\s+/g, " ").trim();
1242
+ return input.replace(/\s\s+/g, " ").trim();
1340
1243
  }
1341
- var reWhitespace2 = new RegExp("(\\s|_)+", "g");
1244
+ const reWhitespace = /* @__PURE__ */ new RegExp("(\\s|_)+", "g");
1245
+ /**
1246
+ * Create a key from the given definition's LHS that can be used during
1247
+ * flattening and/or sorting, and include the kind.
1248
+ */
1342
1249
  function keyForDef(def) {
1343
- let key = def;
1344
- key = key.replace(/:INTERPOLATE:/g, "");
1345
- let kind;
1346
- if (key.includes("=")) {
1347
- kind = "eqn";
1348
- key = key.split("=")[0].trim();
1349
- } else if (key.includes(":")) {
1350
- kind = "dim";
1351
- key = key.split(":")[0].trim();
1352
- } else {
1353
- kind = "decl";
1354
- }
1355
- key = splitExceptInQuoted(key, "(")[0];
1356
- key = key.replace(/"/g, "");
1357
- key = key.trim();
1358
- key = key.replace(/(?<=\[).*?(?=\])/g, (match) => match.replace(/\s/g, ""));
1359
- key = key.replace(reWhitespace2, "_");
1360
- key = key.toLowerCase();
1361
- return { key, kind };
1250
+ let key = def;
1251
+ key = key.replace(/:INTERPOLATE:/g, "");
1252
+ let kind;
1253
+ if (key.includes("=")) {
1254
+ kind = "eqn";
1255
+ key = key.split("=")[0].trim();
1256
+ } else if (key.includes(":")) {
1257
+ kind = "dim";
1258
+ key = key.split(":")[0].trim();
1259
+ } else kind = "decl";
1260
+ key = splitExceptInQuoted(key, "(")[0];
1261
+ key = key.replace(/"/g, "");
1262
+ key = key.trim();
1263
+ key = key.replace(/(?<=\[).*?(?=\])/g, (match) => match.replace(/\s/g, ""));
1264
+ key = key.replace(reWhitespace, "_");
1265
+ key = key.toLowerCase();
1266
+ return {
1267
+ key,
1268
+ kind
1269
+ };
1362
1270
  }
1271
+ /**
1272
+ * Strip out unnecessary parts of the given raw definition string and
1273
+ * return a `VensimDef` containing the processed definition along with
1274
+ * the units and comment strings.
1275
+ */
1363
1276
  function processDef(rawDef) {
1364
- let input = rawDef.text;
1365
- input = input.replace(/:RAW:/g, "");
1366
- input = replaceDelimitedStrings(input, "{", "}", "");
1367
- input = input.trim();
1368
- if (input.length === 0) {
1369
- return void 0;
1370
- }
1371
- input = processBackslashes(input);
1372
- const parts = input.split("~");
1373
- if (parts.length < 3) {
1374
- throw new Error(`Found invalid model definition during preprocessing (missing comment delimiters?):
1375
-
1376
- ${input}`);
1377
- }
1378
- const rawDefText = reduceWhitespace(parts[0]);
1379
- const { key, kind } = keyForDef(rawDefText);
1380
- const def = `${rawDefText} ~~|`;
1381
- const units = reduceWhitespace(parts[1]);
1382
- const comment = reduceWhitespace(parts[2]);
1383
- const group = rawDef.group;
1384
- return {
1385
- key,
1386
- def,
1387
- kind,
1388
- line: rawDef.line,
1389
- units,
1390
- comment,
1391
- ...group ? { group } : {}
1392
- };
1277
+ let input = rawDef.text;
1278
+ input = input.replace(/:RAW:/g, "");
1279
+ input = replaceDelimitedStrings(input, "{", "}", "");
1280
+ input = input.trim();
1281
+ if (input.length === 0) return;
1282
+ input = processBackslashes(input);
1283
+ const parts = input.split("~");
1284
+ if (parts.length < 3) throw new Error(`Found invalid model definition during preprocessing (missing comment delimiters?):\n\n${input}`);
1285
+ const rawDefText = reduceWhitespace(parts[0]);
1286
+ const { key, kind } = keyForDef(rawDefText);
1287
+ const def = `${rawDefText} ~~|`;
1288
+ const units = reduceWhitespace(parts[1]);
1289
+ const comment = reduceWhitespace(parts[2]);
1290
+ const group = rawDef.group;
1291
+ return {
1292
+ key,
1293
+ def,
1294
+ kind,
1295
+ line: rawDef.line,
1296
+ units,
1297
+ comment,
1298
+ ...group ? { group } : {}
1299
+ };
1393
1300
  }
1301
+ /**
1302
+ * Remove macro definitions from the input string. We replace the text in the input
1303
+ * string with blank lines where the macro definition appeared so that line numbers
1304
+ * for definitions that follow are unaffected.
1305
+ *
1306
+ * @param input The original Vensim mdl file content.
1307
+ * @return The processed string and the text blocks that were removed.
1308
+ */
1394
1309
  function removeMacros(input) {
1395
- const removed = [];
1396
- const processed = input.replace(/:MACRO:.*:END OF MACRO:/gms, (match) => {
1397
- removed.push(match);
1398
- const numBreaks = match.split(/\r\n|\n|\r/gms).length - 1;
1399
- return numBreaks > 0 ? "\n".repeat(numBreaks) : "";
1400
- });
1401
- return {
1402
- processed,
1403
- removed
1404
- };
1310
+ const removed = [];
1311
+ return {
1312
+ processed: input.replace(/:MACRO:.*:END OF MACRO:/gms, (match) => {
1313
+ removed.push(match);
1314
+ const numBreaks = match.split(/\r\n|\n|\r/gms).length - 1;
1315
+ return numBreaks > 0 ? "\n".repeat(numBreaks) : "";
1316
+ }),
1317
+ removed
1318
+ };
1405
1319
  }
1406
-
1407
- // src/vensim/impl/model-reader.js
1408
- import { ModelVisitor as ModelVisitor4 } from "antlr4-vensim";
1409
- var ModelReader = class extends ModelVisitor4 {
1410
- /**
1411
- * @public
1412
- * @param {import('../vensim-parse-context').VensimParseContext} parseContext An object
1413
- * that provides access to file system resources (such as external data files) that are
1414
- * referenced during the parse phase.
1415
- */
1416
- constructor(parseContext) {
1417
- super();
1418
- this.parseContext = parseContext;
1419
- this.dimensions = [];
1420
- this.equations = [];
1421
- }
1422
- /**
1423
- * Parse the given Vensim model definition and return a `Model` AST node.
1424
- *
1425
- * @public
1426
- * @param {string} modelText A string containing the Vensim model.
1427
- * @returns {import('../../ast/ast-types').Model} A `Model` AST node.
1428
- */
1429
- /*public*/
1430
- parse(modelText) {
1431
- const parser = createAntlrParser(modelText);
1432
- const modelCtx = parser.model();
1433
- modelCtx.accept(this);
1434
- return this.model;
1435
- }
1436
- visitModel(ctx) {
1437
- const subscriptRangesCtx = ctx.subscriptRange();
1438
- if (subscriptRangesCtx) {
1439
- const subscriptReader = new SubscriptRangeReader(this.parseContext);
1440
- for (const subscriptRangeCtx of subscriptRangesCtx) {
1441
- const dimensionDef = subscriptReader.visitSubscriptRange(subscriptRangeCtx);
1442
- this.dimensions.push(dimensionDef);
1443
- }
1444
- }
1445
- const equationsCtx = ctx.equation();
1446
- if (equationsCtx) {
1447
- const equationReader = new EquationReader();
1448
- for (const equationCtx of equationsCtx) {
1449
- const equation = equationReader.visitEquation(equationCtx);
1450
- this.equations.push(equation);
1451
- }
1452
- }
1453
- this.model = {
1454
- dimensions: this.dimensions,
1455
- equations: this.equations
1456
- };
1457
- }
1320
+ //#endregion
1321
+ //#region src/vensim/impl/model-reader.js
1322
+ var ModelReader = class extends ModelVisitor {
1323
+ /**
1324
+ * @public
1325
+ * @param {import('../vensim-parse-context').VensimParseContext} parseContext An object
1326
+ * that provides access to file system resources (such as external data files) that are
1327
+ * referenced during the parse phase.
1328
+ */
1329
+ constructor(parseContext) {
1330
+ super();
1331
+ this.parseContext = parseContext;
1332
+ this.dimensions = [];
1333
+ this.equations = [];
1334
+ }
1335
+ /**
1336
+ * Parse the given Vensim model definition and return a `Model` AST node.
1337
+ *
1338
+ * @public
1339
+ * @param {string} modelText A string containing the Vensim model.
1340
+ * @returns {import('../../ast/ast-types').Model} A `Model` AST node.
1341
+ */
1342
+ parse(modelText) {
1343
+ createAntlrParser(modelText).model().accept(this);
1344
+ return this.model;
1345
+ }
1346
+ visitModel(ctx) {
1347
+ const subscriptRangesCtx = ctx.subscriptRange();
1348
+ if (subscriptRangesCtx) {
1349
+ const subscriptReader = new SubscriptRangeReader(this.parseContext);
1350
+ for (const subscriptRangeCtx of subscriptRangesCtx) {
1351
+ const dimensionDef = subscriptReader.visitSubscriptRange(subscriptRangeCtx);
1352
+ this.dimensions.push(dimensionDef);
1353
+ }
1354
+ }
1355
+ const equationsCtx = ctx.equation();
1356
+ if (equationsCtx) {
1357
+ const equationReader = new EquationReader();
1358
+ for (const equationCtx of equationsCtx) {
1359
+ const equation = equationReader.visitEquation(equationCtx);
1360
+ this.equations.push(equation);
1361
+ }
1362
+ }
1363
+ this.model = {
1364
+ dimensions: this.dimensions,
1365
+ equations: this.equations
1366
+ };
1367
+ }
1458
1368
  };
1459
-
1460
- // src/vensim/parse-vensim-model.ts
1369
+ //#endregion
1370
+ //#region src/vensim/parse-vensim-model.ts
1371
+ /**
1372
+ * Parse the given Vensim model definition and return a `Model` AST node.
1373
+ *
1374
+ * @param input A string containing the Vensim model.
1375
+ * @param context An object that provides access to file system resources (such as
1376
+ * external data files) that are referenced during the parse phase.
1377
+ * @param sort Whether to sort definitions alphabetically during the preprocessing phase.
1378
+ * @returns A `Model` AST node.
1379
+ */
1461
1380
  function parseVensimModel(input, context, sort = false) {
1462
- const dimensions = [];
1463
- const equations = [];
1464
- const { defs } = preprocessVensimModel(input);
1465
- if (sort) {
1466
- defs.sort((a, b) => {
1467
- return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
1468
- });
1469
- }
1470
- for (const def of defs) {
1471
- let parsedModel;
1472
- try {
1473
- const modelReader = new ModelReader(context);
1474
- parsedModel = modelReader.parse(def.def);
1475
- } catch (e) {
1476
- let linePart = "";
1477
- if (e.cause?.code === "VensimParseError") {
1478
- if (e.cause.line) {
1479
- linePart += ` at line ${e.cause.line - 1 + def.line}`;
1480
- if (e.cause.column) {
1481
- linePart += `, col ${e.cause.column}`;
1482
- }
1483
- }
1484
- }
1485
- const msg = `Failed to parse Vensim model definition${linePart}:
1486
- ${def.def}
1487
-
1488
- Detail:
1489
- ${e.message}`;
1490
- throw new Error(msg);
1491
- }
1492
- for (const dimensionDef of parsedModel.dimensions) {
1493
- const group = def.group;
1494
- dimensions.push({
1495
- ...dimensionDef,
1496
- comment: def.comment,
1497
- ...group ? { group } : {}
1498
- });
1499
- }
1500
- for (const equation of parsedModel.equations) {
1501
- const group = def.group;
1502
- equations.push({
1503
- ...equation,
1504
- units: def.units,
1505
- comment: def.comment,
1506
- ...group ? { group } : {}
1507
- });
1508
- }
1509
- }
1510
- return {
1511
- dimensions,
1512
- equations
1513
- };
1381
+ const dimensions = [];
1382
+ const equations = [];
1383
+ const { defs } = preprocessVensimModel(input);
1384
+ if (sort) defs.sort((a, b) => {
1385
+ return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
1386
+ });
1387
+ for (const def of defs) {
1388
+ let parsedModel;
1389
+ try {
1390
+ parsedModel = new ModelReader(context).parse(def.def);
1391
+ } catch (e) {
1392
+ let linePart = "";
1393
+ if (e.cause?.code === "VensimParseError") {
1394
+ if (e.cause.line) {
1395
+ linePart += ` at line ${e.cause.line - 1 + def.line}`;
1396
+ if (e.cause.column) linePart += `, col ${e.cause.column}`;
1397
+ }
1398
+ }
1399
+ const msg = `Failed to parse Vensim model definition${linePart}:\n${def.def}\n\nDetail:\n ${e.message}`;
1400
+ throw new Error(msg, { cause: e });
1401
+ }
1402
+ for (const dimensionDef of parsedModel.dimensions) {
1403
+ const group = def.group;
1404
+ dimensions.push({
1405
+ ...dimensionDef,
1406
+ comment: def.comment,
1407
+ ...group ? { group } : {}
1408
+ });
1409
+ }
1410
+ for (const equation of parsedModel.equations) {
1411
+ const group = def.group;
1412
+ equations.push({
1413
+ ...equation,
1414
+ units: def.units,
1415
+ comment: def.comment,
1416
+ ...group ? { group } : {}
1417
+ });
1418
+ }
1419
+ }
1420
+ return {
1421
+ dimensions,
1422
+ equations
1423
+ };
1514
1424
  }
1515
-
1516
- // src/xmile/xml.ts
1517
- import { XmlNode } from "@rgrove/parse-xml";
1425
+ //#endregion
1426
+ //#region src/xmile/xml.ts
1518
1427
  function firstElemOf(parent, tagName) {
1519
- return parent?.children.find((n) => {
1520
- if (n.type === XmlNode.TYPE_ELEMENT) {
1521
- const e = n;
1522
- return e.name === tagName;
1523
- } else {
1524
- return void 0;
1525
- }
1526
- });
1428
+ return parent?.children.find((n) => {
1429
+ if (n.type === XmlNode.TYPE_ELEMENT) return n.name === tagName;
1430
+ else return;
1431
+ });
1527
1432
  }
1528
1433
  function firstTextOf(parent) {
1529
- return parent?.children.find((n) => {
1530
- return n.type === XmlNode.TYPE_TEXT;
1531
- });
1434
+ return parent?.children.find((n) => {
1435
+ return n.type === XmlNode.TYPE_TEXT;
1436
+ });
1532
1437
  }
1533
1438
  function elemsOf(parent, tagNames) {
1534
- if (parent === void 0) {
1535
- return [];
1536
- }
1537
- const elems = [];
1538
- for (const n of parent.children) {
1539
- if (n.type === XmlNode.TYPE_ELEMENT) {
1540
- const e = n;
1541
- if (tagNames.includes(e.name)) {
1542
- elems.push(e);
1543
- }
1544
- }
1545
- }
1546
- return elems;
1439
+ if (parent === void 0) return [];
1440
+ const elems = [];
1441
+ for (const n of parent.children) if (n.type === XmlNode.TYPE_ELEMENT) {
1442
+ const e = n;
1443
+ if (tagNames.includes(e.name)) elems.push(e);
1444
+ }
1445
+ return elems;
1547
1446
  }
1548
1447
  function xmlError(elem, msg) {
1549
- return `${msg}: ${JSON.stringify(elem.toJSON(), null, 2)}`;
1448
+ return `${msg}: ${JSON.stringify(elem.toJSON(), null, 2)}`;
1550
1449
  }
1551
-
1552
- // src/xmile/parse-xmile-dimension-def.ts
1450
+ //#endregion
1451
+ //#region src/xmile/parse-xmile-dimension-def.ts
1452
+ /**
1453
+ * Parse the given XMILE dimension (`<dim>`) definition and return a `DimensionDef` AST node.
1454
+ *
1455
+ * @param dimElem An `XmlElement` containing the XMILE `<dim>` definition.
1456
+ * @returns A `DimensionDef` AST node.
1457
+ */
1553
1458
  function parseXmileDimensionDef(dimElem) {
1554
- const dimName = dimElem.attributes?.name;
1555
- if (dimName === void 0) {
1556
- throw new Error(xmlError(dimElem, "<dim> name attribute is required for dimension definition"));
1557
- }
1558
- const elemElems = elemsOf(dimElem, ["elem"]);
1559
- if (elemElems.length === 0) {
1560
- throw new Error(xmlError(dimElem, "<dim> must contain one or more <elem> elements"));
1561
- }
1562
- const subscriptRefs = [];
1563
- for (const elem of elemElems) {
1564
- const subName = elem.attributes?.name;
1565
- if (subName === void 0) {
1566
- throw new Error(xmlError(dimElem, "<elem> name attribute is required for dimension element definition"));
1567
- }
1568
- const subId = canonicalId(subName);
1569
- subscriptRefs.push({
1570
- subId,
1571
- subName
1572
- });
1573
- }
1574
- const comment = firstElemOf(dimElem, "doc")?.text || "";
1575
- const dimId = canonicalId(dimName);
1576
- return {
1577
- dimName,
1578
- dimId,
1579
- // TODO: For Vensim `DimA <-> DimB` aliases, the family name would be `DimB`
1580
- familyName: dimName,
1581
- familyId: dimId,
1582
- subscriptRefs,
1583
- // TODO: Does XMILE support mappings?
1584
- subscriptMappings: [],
1585
- comment
1586
- };
1459
+ const dimName = dimElem.attributes?.name;
1460
+ if (dimName === void 0) throw new Error(xmlError(dimElem, "<dim> name attribute is required for dimension definition"));
1461
+ const elemElems = elemsOf(dimElem, ["elem"]);
1462
+ if (elemElems.length === 0) throw new Error(xmlError(dimElem, "<dim> must contain one or more <elem> elements"));
1463
+ const subscriptRefs = [];
1464
+ for (const elem of elemElems) {
1465
+ const subName = elem.attributes?.name;
1466
+ if (subName === void 0) throw new Error(xmlError(dimElem, "<elem> name attribute is required for dimension element definition"));
1467
+ const subId = canonicalId(subName);
1468
+ subscriptRefs.push({
1469
+ subId,
1470
+ subName
1471
+ });
1472
+ }
1473
+ const comment = firstElemOf(dimElem, "doc")?.text || "";
1474
+ const dimId = canonicalId(dimName);
1475
+ return {
1476
+ dimName,
1477
+ dimId,
1478
+ familyName: dimName,
1479
+ familyId: dimId,
1480
+ subscriptRefs,
1481
+ subscriptMappings: [],
1482
+ comment
1483
+ };
1587
1484
  }
1588
-
1589
- // src/xmile/parse-xmile-model.ts
1590
- import { parseXml } from "@rgrove/parse-xml";
1591
-
1592
- // src/xmile/parse-xmile-variable-def.ts
1485
+ //#endregion
1486
+ //#region src/xmile/parse-xmile-variable-def.ts
1487
+ /**
1488
+ * Parse the given XMILE variable definition and return an array of `Equation` AST nodes
1489
+ * corresponding to the variable definition (or definitions, in the case of a
1490
+ * non-apply-to-all variable that is defined with an `<element>` for each subscript).
1491
+ *
1492
+ * @param varElem An `XmlElement` containing the XMILE variable definition.
1493
+ * @returns An `Equation` AST node.
1494
+ */
1593
1495
  function parseXmileVariableDef(varElem) {
1594
- let varName = parseRequiredAttr(varElem, varElem, "name");
1595
- varName = varName.replace(/\\n/g, " ");
1596
- const varId = canonicalId(varName);
1597
- const units = firstElemOf(varElem, "units")?.text || "";
1598
- const comment = firstElemOf(varElem, "doc")?.text || "";
1599
- function exprEquation(subscriptRefs, expr) {
1600
- return {
1601
- lhs: {
1602
- varDef: {
1603
- kind: "variable-def",
1604
- varName,
1605
- varId,
1606
- subscriptRefs
1607
- }
1608
- },
1609
- rhs: {
1610
- kind: "expr",
1611
- expr
1612
- },
1613
- units,
1614
- comment
1615
- };
1616
- }
1617
- function lookupEquation(subscriptRefs, lookup) {
1618
- return {
1619
- lhs: {
1620
- varDef: {
1621
- kind: "variable-def",
1622
- varName,
1623
- varId,
1624
- subscriptRefs
1625
- }
1626
- },
1627
- rhs: {
1628
- kind: "lookup",
1629
- lookupDef: lookup
1630
- },
1631
- units,
1632
- comment
1633
- };
1634
- }
1635
- if (varElem.name === "gf") {
1636
- const lookup = parseGfElem(varElem, varElem);
1637
- return [lookupEquation(void 0, lookup)];
1638
- }
1639
- const dimensionsElem = firstElemOf(varElem, "dimensions");
1640
- const equationDefs = [];
1641
- if (dimensionsElem === void 0) {
1642
- const gfElem = firstElemOf(varElem, "gf");
1643
- if (gfElem) {
1644
- if (varElem.name !== "flow" && varElem.name !== "aux") {
1645
- throw new Error(xmlError(varElem, "<gf> is only allowed for <flow> and <aux> variables"));
1646
- }
1647
- const lookup = parseGfElem(varElem, gfElem);
1648
- equationDefs.push(lookupEquation(void 0, lookup));
1649
- } else {
1650
- const expr = parseEqnElem(varElem, varElem);
1651
- if (expr) {
1652
- equationDefs.push(exprEquation(void 0, expr));
1653
- }
1654
- }
1655
- } else {
1656
- const dimElems = elemsOf(dimensionsElem, ["dim"]);
1657
- const dimNames = [];
1658
- for (const dimElem of dimElems) {
1659
- const dimName = dimElem.attributes?.name;
1660
- if (dimName === void 0) {
1661
- throw new Error(xmlError(varElem, "<dim> name attribute is required in <dimensions> for variable definition"));
1662
- }
1663
- dimNames.push(dimName);
1664
- }
1665
- const elementElems = elemsOf(varElem, ["element"]);
1666
- if (elementElems.length === 0) {
1667
- const dimRefs = dimNames.map(subRef);
1668
- const expr = parseEqnElem(varElem, varElem);
1669
- if (expr) {
1670
- equationDefs.push(exprEquation(dimRefs, expr));
1671
- }
1672
- } else {
1673
- for (const elementElem of elementElems) {
1674
- const subscriptAttr = elementElem.attributes?.subscript;
1675
- if (subscriptAttr === void 0) {
1676
- throw new Error(xmlError(varElem, "<element> subscript attribute is required in variable definition"));
1677
- }
1678
- const subscriptNames = subscriptAttr.split(",").map((s) => s.trim());
1679
- const subRefs = [];
1680
- for (const subscriptName of subscriptNames) {
1681
- if (!isNaN(parseInt(subscriptAttr))) {
1682
- throw new Error(xmlError(varElem, "Numeric subscript indices are not currently supported"));
1683
- }
1684
- subRefs.push(subRef(subscriptName));
1685
- }
1686
- const expr = parseEqnElem(varElem, elementElem);
1687
- if (expr) {
1688
- equationDefs.push(exprEquation(subRefs, expr));
1689
- }
1690
- }
1691
- }
1692
- }
1693
- return equationDefs;
1496
+ let varName = parseRequiredAttr(varElem, varElem, "name");
1497
+ varName = varName.replace(/\\n/g, " ");
1498
+ const varId = canonicalId(varName);
1499
+ const units = firstElemOf(varElem, "units")?.text || "";
1500
+ const comment = firstElemOf(varElem, "doc")?.text || "";
1501
+ function exprEquation(subscriptRefs, expr) {
1502
+ return {
1503
+ lhs: { varDef: {
1504
+ kind: "variable-def",
1505
+ varName,
1506
+ varId,
1507
+ subscriptRefs
1508
+ } },
1509
+ rhs: {
1510
+ kind: "expr",
1511
+ expr
1512
+ },
1513
+ units,
1514
+ comment
1515
+ };
1516
+ }
1517
+ function lookupEquation(subscriptRefs, lookup) {
1518
+ return {
1519
+ lhs: { varDef: {
1520
+ kind: "variable-def",
1521
+ varName,
1522
+ varId,
1523
+ subscriptRefs
1524
+ } },
1525
+ rhs: {
1526
+ kind: "lookup",
1527
+ lookupDef: lookup
1528
+ },
1529
+ units,
1530
+ comment
1531
+ };
1532
+ }
1533
+ if (varElem.name === "gf") return [lookupEquation(void 0, parseGfElem(varElem, varElem))];
1534
+ const dimensionsElem = firstElemOf(varElem, "dimensions");
1535
+ const equationDefs = [];
1536
+ if (dimensionsElem === void 0) {
1537
+ const gfElem = firstElemOf(varElem, "gf");
1538
+ if (gfElem) {
1539
+ if (varElem.name !== "flow" && varElem.name !== "aux") throw new Error(xmlError(varElem, "<gf> is only allowed for <flow> and <aux> variables"));
1540
+ const lookup = parseGfElem(varElem, gfElem);
1541
+ equationDefs.push(lookupEquation(void 0, lookup));
1542
+ } else {
1543
+ const expr = parseEqnElem(varElem, varElem);
1544
+ if (expr) equationDefs.push(exprEquation(void 0, expr));
1545
+ }
1546
+ } else {
1547
+ const dimElems = elemsOf(dimensionsElem, ["dim"]);
1548
+ const dimNames = [];
1549
+ for (const dimElem of dimElems) {
1550
+ const dimName = dimElem.attributes?.name;
1551
+ if (dimName === void 0) throw new Error(xmlError(varElem, "<dim> name attribute is required in <dimensions> for variable definition"));
1552
+ dimNames.push(dimName);
1553
+ }
1554
+ const elementElems = elemsOf(varElem, ["element"]);
1555
+ if (elementElems.length === 0) {
1556
+ const dimRefs = dimNames.map(subRef);
1557
+ const expr = parseEqnElem(varElem, varElem);
1558
+ if (expr) equationDefs.push(exprEquation(dimRefs, expr));
1559
+ } else for (const elementElem of elementElems) {
1560
+ const subscriptAttr = elementElem.attributes?.subscript;
1561
+ if (subscriptAttr === void 0) throw new Error(xmlError(varElem, "<element> subscript attribute is required in variable definition"));
1562
+ const subscriptNames = subscriptAttr.split(",").map((s) => s.trim());
1563
+ const subRefs = [];
1564
+ for (const subscriptName of subscriptNames) {
1565
+ if (!isNaN(parseInt(subscriptAttr))) throw new Error(xmlError(varElem, "Numeric subscript indices are not currently supported"));
1566
+ subRefs.push(subRef(subscriptName));
1567
+ }
1568
+ const expr = parseEqnElem(varElem, elementElem);
1569
+ if (expr) equationDefs.push(exprEquation(subRefs, expr));
1570
+ }
1571
+ }
1572
+ return equationDefs;
1694
1573
  }
1695
1574
  function parseEqnElem(varElem, parentElem) {
1696
- const varTagName = varElem.name;
1697
- const eqnElem = firstElemOf(parentElem, "eqn");
1698
- const eqnText = eqnElem ? firstTextOf(eqnElem) : void 0;
1699
- switch (varTagName) {
1700
- case "aux": {
1701
- if (eqnText === void 0) {
1702
- return void 0;
1703
- }
1704
- const initEqnElem = firstElemOf(parentElem, "init_eqn");
1705
- const initEqnText = initEqnElem ? firstTextOf(initEqnElem) : void 0;
1706
- if (initEqnText !== void 0) {
1707
- const eqnExpr = parseExpr(eqnText.text);
1708
- const initEqnExpr = parseExpr(initEqnText.text);
1709
- return call("ACTIVE INITIAL", eqnExpr, initEqnExpr);
1710
- }
1711
- return parseExpr(eqnText.text);
1712
- }
1713
- case "stock": {
1714
- if (eqnText === void 0) {
1715
- throw new Error(xmlError(varElem, "An <eqn> is required for a <stock> variable"));
1716
- }
1717
- const inflowElems = elemsOf(parentElem, ["inflow"]);
1718
- const outflowElems = elemsOf(parentElem, ["outflow"]);
1719
- const inflowTexts = inflowElems.map((inflowElem) => {
1720
- const inflowText = firstTextOf(inflowElem);
1721
- if (inflowText === void 0) {
1722
- throw new Error(xmlError(varElem, "An <inflow> must be non-empty for a <stock> variable"));
1723
- }
1724
- return inflowText.text;
1725
- });
1726
- const outflowTexts = outflowElems.map((outflowElem) => {
1727
- const outflowText = firstTextOf(outflowElem);
1728
- if (outflowText === void 0) {
1729
- throw new Error(xmlError(varElem, "An <outflow> must be non-empty for a <stock> variable"));
1730
- }
1731
- return outflowText.text;
1732
- });
1733
- if (firstElemOf(parentElem, "conveyor")) {
1734
- throw new Error(xmlError(varElem, "Currently <conveyor> is not supported for a <stock> variable"));
1735
- }
1736
- if (firstElemOf(parentElem, "queue")) {
1737
- throw new Error(xmlError(varElem, "Currently <queue> is not supported for a <stock> variable"));
1738
- }
1739
- const inflowParts = inflowTexts.join(" + ");
1740
- let outflowParts = outflowTexts.join(" - ");
1741
- if (outflowTexts.length > 0) {
1742
- if (inflowParts.length > 0) {
1743
- outflowParts = `- ${outflowParts}`;
1744
- } else {
1745
- outflowParts = `-${outflowParts}`;
1746
- }
1747
- }
1748
- const flowsExpr = parseExpr(`${inflowParts} ${outflowParts}`);
1749
- const initExpr = parseExpr(eqnText.text);
1750
- return call("INTEG", flowsExpr, initExpr);
1751
- }
1752
- case "flow":
1753
- if (eqnText === void 0) {
1754
- throw new Error(xmlError(varElem, "Currently <eqn> or <gf> is required for a <flow> variable"));
1755
- }
1756
- if (firstElemOf(parentElem, "multiplier")) {
1757
- throw new Error(xmlError(varElem, "Currently <multiplier> is not supported for a <flow> variable"));
1758
- }
1759
- if (firstElemOf(parentElem, "overflow")) {
1760
- throw new Error(xmlError(varElem, "Currently <overflow> is not supported for a <flow> variable"));
1761
- }
1762
- if (firstElemOf(parentElem, "leak")) {
1763
- throw new Error(xmlError(varElem, "Currently <leak> is not supported for a <flow> variable"));
1764
- }
1765
- return parseExpr(eqnText.text);
1766
- default:
1767
- throw new Error(xmlError(varElem, `Unhandled variable type '${varTagName}'`));
1768
- }
1575
+ const varTagName = varElem.name;
1576
+ const eqnElem = firstElemOf(parentElem, "eqn");
1577
+ const eqnText = eqnElem ? firstTextOf(eqnElem) : void 0;
1578
+ switch (varTagName) {
1579
+ case "aux": {
1580
+ if (eqnText === void 0) return;
1581
+ const initEqnElem = firstElemOf(parentElem, "init_eqn");
1582
+ const initEqnText = initEqnElem ? firstTextOf(initEqnElem) : void 0;
1583
+ if (initEqnText !== void 0) return call("ACTIVE INITIAL", parseExpr(eqnText.text), parseExpr(initEqnText.text));
1584
+ return parseExpr(eqnText.text);
1585
+ }
1586
+ case "stock": {
1587
+ if (eqnText === void 0) throw new Error(xmlError(varElem, "An <eqn> is required for a <stock> variable"));
1588
+ const inflowElems = elemsOf(parentElem, ["inflow"]);
1589
+ const outflowElems = elemsOf(parentElem, ["outflow"]);
1590
+ const inflowTexts = inflowElems.map((inflowElem) => {
1591
+ const inflowText = firstTextOf(inflowElem);
1592
+ if (inflowText === void 0) throw new Error(xmlError(varElem, "An <inflow> must be non-empty for a <stock> variable"));
1593
+ return inflowText.text;
1594
+ });
1595
+ const outflowTexts = outflowElems.map((outflowElem) => {
1596
+ const outflowText = firstTextOf(outflowElem);
1597
+ if (outflowText === void 0) throw new Error(xmlError(varElem, "An <outflow> must be non-empty for a <stock> variable"));
1598
+ return outflowText.text;
1599
+ });
1600
+ if (firstElemOf(parentElem, "conveyor")) throw new Error(xmlError(varElem, "Currently <conveyor> is not supported for a <stock> variable"));
1601
+ if (firstElemOf(parentElem, "queue")) throw new Error(xmlError(varElem, "Currently <queue> is not supported for a <stock> variable"));
1602
+ const inflowParts = inflowTexts.join(" + ");
1603
+ let outflowParts = outflowTexts.join(" - ");
1604
+ if (outflowTexts.length > 0) {
1605
+ if (inflowParts.length > 0) outflowParts = `- ${outflowParts}`;
1606
+ else outflowParts = `-${outflowParts}`;
1607
+ }
1608
+ return call("INTEG", parseExpr(`${inflowParts} ${outflowParts}`), parseExpr(eqnText.text));
1609
+ }
1610
+ case "flow":
1611
+ if (eqnText === void 0) throw new Error(xmlError(varElem, "Currently <eqn> or <gf> is required for a <flow> variable"));
1612
+ if (firstElemOf(parentElem, "multiplier")) throw new Error(xmlError(varElem, "Currently <multiplier> is not supported for a <flow> variable"));
1613
+ if (firstElemOf(parentElem, "overflow")) throw new Error(xmlError(varElem, "Currently <overflow> is not supported for a <flow> variable"));
1614
+ if (firstElemOf(parentElem, "leak")) throw new Error(xmlError(varElem, "Currently <leak> is not supported for a <flow> variable"));
1615
+ return parseExpr(eqnText.text);
1616
+ default: throw new Error(xmlError(varElem, `Unhandled variable type '${varTagName}'`));
1617
+ }
1769
1618
  }
1770
1619
  function parseExpr(exprText) {
1771
- exprText = convertConditionalExpressions(exprText);
1772
- exprText = exprText.replace(/\[([^\]]*)\*([^\]]*)\]/g, "[$1_SDE_WILDCARD_!$2]");
1773
- return parseVensimExpr(exprText);
1620
+ exprText = convertConditionalExpressions(exprText);
1621
+ exprText = exprText.replace(/\[([^\]]*)\*([^\]]*)\]/g, "[$1_SDE_WILDCARD_!$2]");
1622
+ return parseVensimExpr(exprText);
1774
1623
  }
1775
1624
  function parseGfElem(varElem, gfElem) {
1776
- const typeAttr = parseOptionalAttr(gfElem, "type");
1777
- if (typeAttr && typeAttr !== "continuous") {
1778
- throw new Error(xmlError(varElem, 'Currently "continuous" is the only type supported for <gf>'));
1779
- }
1780
- const yptsElem = firstElemOf(gfElem, "ypts");
1781
- if (yptsElem === void 0) {
1782
- throw new Error(xmlError(varElem, "<ypts> must be defined for a <gf>"));
1783
- }
1784
- const ypts = parseGfPts(varElem, yptsElem);
1785
- if (ypts.length === 0) {
1786
- throw new Error(xmlError(varElem, "<ypts> must have at least one element"));
1787
- }
1788
- const xptsElem = firstElemOf(gfElem, "xpts");
1789
- const xscaleElem = firstElemOf(gfElem, "xscale");
1790
- if (xptsElem && xscaleElem) {
1791
- throw new Error(xmlError(varElem, "<gf> must contain <xpts> or <xscale> but not both"));
1792
- } else if (xptsElem === void 0 && xscaleElem === void 0) {
1793
- throw new Error(xmlError(varElem, "<gf> must contain either <xpts> or <xscale>"));
1794
- }
1795
- let xpts;
1796
- if (xptsElem) {
1797
- xpts = parseGfPts(varElem, xptsElem);
1798
- if (xpts.length === 0) {
1799
- throw new Error(xmlError(varElem, "<xpts> must have at least one element"));
1800
- }
1801
- } else {
1802
- const xMin = parseFloatAttr(varElem, xscaleElem, "min");
1803
- const xMax = parseFloatAttr(varElem, xscaleElem, "max");
1804
- if (xMin > xMax) {
1805
- throw new Error(xmlError(varElem, "<xscale> max attribute must be > min attribute"));
1806
- }
1807
- xpts = Array(ypts.length);
1808
- const xRange = xMax - xMin;
1809
- if (ypts.length === 1) {
1810
- xpts[0] = 0;
1811
- } else {
1812
- for (let i = 0; i < ypts.length; i++) {
1813
- const frac = i / (ypts.length - 1);
1814
- xpts[i] = xMin + xRange * frac;
1815
- }
1816
- }
1817
- }
1818
- if (xpts.length !== ypts.length) {
1819
- throw new Error(xmlError(varElem, "<xpts> and <ypts> must have the same number of elements"));
1820
- }
1821
- const points = [];
1822
- for (let i = 0; i < xpts.length; i++) {
1823
- points.push([xpts[i], ypts[i]]);
1824
- }
1825
- return lookupDef(points);
1625
+ const typeAttr = parseOptionalAttr(gfElem, "type");
1626
+ if (typeAttr && typeAttr !== "continuous") throw new Error(xmlError(varElem, "Currently \"continuous\" is the only type supported for <gf>"));
1627
+ const yptsElem = firstElemOf(gfElem, "ypts");
1628
+ if (yptsElem === void 0) throw new Error(xmlError(varElem, "<ypts> must be defined for a <gf>"));
1629
+ const ypts = parseGfPts(varElem, yptsElem);
1630
+ if (ypts.length === 0) throw new Error(xmlError(varElem, "<ypts> must have at least one element"));
1631
+ const xptsElem = firstElemOf(gfElem, "xpts");
1632
+ const xscaleElem = firstElemOf(gfElem, "xscale");
1633
+ if (xptsElem && xscaleElem) throw new Error(xmlError(varElem, "<gf> must contain <xpts> or <xscale> but not both"));
1634
+ else if (xptsElem === void 0 && xscaleElem === void 0) throw new Error(xmlError(varElem, "<gf> must contain either <xpts> or <xscale>"));
1635
+ let xpts;
1636
+ if (xptsElem) {
1637
+ xpts = parseGfPts(varElem, xptsElem);
1638
+ if (xpts.length === 0) throw new Error(xmlError(varElem, "<xpts> must have at least one element"));
1639
+ } else {
1640
+ const xMin = parseFloatAttr(varElem, xscaleElem, "min");
1641
+ const xMax = parseFloatAttr(varElem, xscaleElem, "max");
1642
+ if (xMin > xMax) throw new Error(xmlError(varElem, "<xscale> max attribute must be > min attribute"));
1643
+ xpts = Array(ypts.length);
1644
+ const xRange = xMax - xMin;
1645
+ if (ypts.length === 1) xpts[0] = 0;
1646
+ else for (let i = 0; i < ypts.length; i++) {
1647
+ const frac = i / (ypts.length - 1);
1648
+ xpts[i] = xMin + xRange * frac;
1649
+ }
1650
+ }
1651
+ if (xpts.length !== ypts.length) throw new Error(xmlError(varElem, "<xpts> and <ypts> must have the same number of elements"));
1652
+ const points = [];
1653
+ for (let i = 0; i < xpts.length; i++) points.push([xpts[i], ypts[i]]);
1654
+ return lookupDef(points);
1826
1655
  }
1827
1656
  function parseGfPts(varElem, ptsElem) {
1828
- const ptsText = firstTextOf(ptsElem)?.text;
1829
- if (ptsText === void 0) {
1830
- return [];
1831
- }
1832
- const sep = ptsElem.attributes?.sep || ",";
1833
- const elems = ptsText.split(sep);
1834
- const nums = [];
1835
- for (const elem of elems) {
1836
- const numText = elem.trim();
1837
- const num2 = parseFloat(numText);
1838
- if (isNaN(num2)) {
1839
- console.log(JSON.stringify(ptsElem));
1840
- throw new Error(xmlError(varElem, `Invalid number value '${numText}' in <${ptsElem.name}>'`));
1841
- }
1842
- nums.push(num2);
1843
- }
1844
- return nums;
1657
+ const ptsText = firstTextOf(ptsElem)?.text;
1658
+ if (ptsText === void 0) return [];
1659
+ const sep = ptsElem.attributes?.sep || ",";
1660
+ const elems = ptsText.split(sep);
1661
+ const nums = [];
1662
+ for (const elem of elems) {
1663
+ const numText = elem.trim();
1664
+ const num = parseFloat(numText);
1665
+ if (isNaN(num)) {
1666
+ console.log(JSON.stringify(ptsElem));
1667
+ throw new Error(xmlError(varElem, `Invalid number value '${numText}' in <${ptsElem.name}>'`));
1668
+ }
1669
+ nums.push(num);
1670
+ }
1671
+ return nums;
1845
1672
  }
1846
1673
  function parseRequiredAttr(varElem, elem, attrName) {
1847
- let s = elem.attributes && elem.attributes[attrName];
1848
- s = s?.trim();
1849
- if (s === void 0 || s.length === 0) {
1850
- throw new Error(xmlError(varElem, `<${elem.name}> ${attrName} attribute is required`));
1851
- }
1852
- return s;
1674
+ let s = elem.attributes && elem.attributes[attrName];
1675
+ s = s?.trim();
1676
+ if (s === void 0 || s.length === 0) throw new Error(xmlError(varElem, `<${elem.name}> ${attrName} attribute is required`));
1677
+ return s;
1853
1678
  }
1854
1679
  function parseOptionalAttr(elem, attrName) {
1855
- const s = elem.attributes && elem.attributes[attrName];
1856
- return s?.trim();
1680
+ return (elem.attributes && elem.attributes[attrName])?.trim();
1857
1681
  }
1858
1682
  function parseFloatAttr(varElem, elem, attrName) {
1859
- const s = parseRequiredAttr(varElem, elem, attrName);
1860
- const num2 = parseFloat(s);
1861
- if (isNaN(num2)) {
1862
- throw new Error(xmlError(varElem, `Invalid number value '${s}' for <${elem.name}> ${attrName} attribute'`));
1863
- }
1864
- return num2;
1683
+ const s = parseRequiredAttr(varElem, elem, attrName);
1684
+ const num = parseFloat(s);
1685
+ if (isNaN(num)) throw new Error(xmlError(varElem, `Invalid number value '${s}' for <${elem.name}> ${attrName} attribute'`));
1686
+ return num;
1865
1687
  }
1688
+ /**
1689
+ * Parse XMILE conditional expressions recursively to handle nested IF-THEN-ELSE statements.
1690
+ *
1691
+ * This transforms XMILE syntax:
1692
+ * IF condition THEN trueExpr ELSE falseExpr
1693
+ * to Vensim syntax:
1694
+ * IF THEN ELSE(condition, trueExpr, falseExpr)
1695
+ *
1696
+ * Examples of supported nested structures:
1697
+ * - Simple: IF x > 0 THEN 1 ELSE 0
1698
+ * - Nested: IF x > 0 THEN IF y > 0 THEN 2 ELSE 1 ELSE 0
1699
+ * - Complex: IF a > 0 THEN IF b > 0 THEN IF c > 0 THEN 3 ELSE 2 ELSE 1 ELSE 0
1700
+ * - In function call: ABS(IF x > 0 THEN 1 ELSE 0) + 1
1701
+ */
1866
1702
  function convertConditionalExpressions(exprText) {
1867
- const normalizedText = exprText.trim().replace(/\s+/g, " ");
1868
- const ifMatch = normalizedText.match(/\bIF\s+(.+)$/i);
1869
- if (!ifMatch) {
1870
- return exprText;
1871
- }
1872
- const ifIndex = normalizedText.search(/\bIF\s+/i);
1873
- const beforeIf = normalizedText.substring(0, ifIndex);
1874
- const afterIf = normalizedText.substring(ifIndex + 3).trim();
1875
- const thenMatch = afterIf.match(/^(.+?)\s+THEN\s+(.+)$/i);
1876
- if (!thenMatch) {
1877
- return exprText;
1878
- }
1879
- const condition = thenMatch[1].trim();
1880
- const afterThen = thenMatch[2];
1881
- let elseIndex = -1;
1882
- let parenCount = 0;
1883
- let inQuotes = false;
1884
- let quoteChar = "";
1885
- for (let i = 0; i < afterThen.length; i++) {
1886
- const char = afterThen[i];
1887
- if ((char === '"' || char === "'") && (i === 0 || afterThen[i - 1] !== "\\")) {
1888
- if (!inQuotes) {
1889
- inQuotes = true;
1890
- quoteChar = char;
1891
- } else if (char === quoteChar) {
1892
- inQuotes = false;
1893
- quoteChar = "";
1894
- }
1895
- continue;
1896
- }
1897
- if (inQuotes) {
1898
- continue;
1899
- }
1900
- if (char === "(") {
1901
- parenCount++;
1902
- } else if (char === ")") {
1903
- parenCount--;
1904
- }
1905
- if (parenCount === 0 && !inQuotes) {
1906
- const elseMatch = afterThen.substring(i).match(/^ELSE\s+(.+)$/i);
1907
- if (elseMatch) {
1908
- elseIndex = i;
1909
- break;
1910
- }
1911
- }
1912
- }
1913
- if (elseIndex === -1) {
1914
- return exprText;
1915
- }
1916
- const trueExpr = afterThen.substring(0, elseIndex).trim();
1917
- let falseExpr = afterThen.substring(elseIndex + 5).trim();
1918
- let endIndex = -1;
1919
- parenCount = 0;
1920
- inQuotes = false;
1921
- quoteChar = "";
1922
- for (let i = 0; i < falseExpr.length; i++) {
1923
- const char = falseExpr[i];
1924
- if ((char === '"' || char === "'") && (i === 0 || falseExpr[i - 1] !== "\\")) {
1925
- if (!inQuotes) {
1926
- inQuotes = true;
1927
- quoteChar = char;
1928
- } else if (char === quoteChar) {
1929
- inQuotes = false;
1930
- quoteChar = "";
1931
- }
1932
- continue;
1933
- }
1934
- if (inQuotes) {
1935
- continue;
1936
- }
1937
- if (char === "(") {
1938
- parenCount++;
1939
- } else if (char === ")") {
1940
- if (parenCount === 0) {
1941
- endIndex = i;
1942
- break;
1943
- }
1944
- parenCount--;
1945
- }
1946
- }
1947
- if (endIndex !== -1) {
1948
- falseExpr = falseExpr.substring(0, endIndex).trim();
1949
- }
1950
- const convertedTrueExpr = convertConditionalExpressions(trueExpr);
1951
- const convertedFalseExpr = convertConditionalExpressions(falseExpr);
1952
- const convertedCondition = condition.replace(/(?<!".*?)\b AND \b(?!.*?")/gi, " :AND: ").replace(/(?<!".*?)\b OR \b(?!.*?")/gi, " :OR: ").replace(/(?<!".*?)\b\s?NOT \b(?!.*?")/gi, " :NOT: ").replace(/^\((.+)\)$/, "$1");
1953
- const elseStartInAfterIf = afterIf.indexOf(" ELSE ") + 6;
1954
- const falseExprStartInAfterIf = elseStartInAfterIf;
1955
- const falseExprEndInAfterIf = falseExprStartInAfterIf + falseExpr.length;
1956
- const conditionalEndInNormalizedText = ifIndex + 3 + falseExprEndInAfterIf;
1957
- const afterConditional = normalizedText.substring(conditionalEndInNormalizedText).trim();
1958
- return `${beforeIf}IF THEN ELSE(${convertedCondition}, ${convertedTrueExpr}, ${convertedFalseExpr})${afterConditional}`;
1703
+ const normalizedText = exprText.trim().replace(/\s+/g, " ");
1704
+ if (!normalizedText.match(/\bIF\s+(.+)$/i)) return exprText;
1705
+ const ifIndex = normalizedText.search(/\bIF\s+/i);
1706
+ const beforeIf = normalizedText.substring(0, ifIndex);
1707
+ const afterIf = normalizedText.substring(ifIndex + 3).trim();
1708
+ const thenMatch = afterIf.match(/^(.+?)\s+THEN\s+(.+)$/i);
1709
+ if (!thenMatch) return exprText;
1710
+ const condition = thenMatch[1].trim();
1711
+ const afterThen = thenMatch[2];
1712
+ let elseIndex = -1;
1713
+ let parenCount = 0;
1714
+ let inQuotes = false;
1715
+ let quoteChar = "";
1716
+ for (let i = 0; i < afterThen.length; i++) {
1717
+ const char = afterThen[i];
1718
+ if ((char === "\"" || char === "'") && (i === 0 || afterThen[i - 1] !== "\\")) {
1719
+ if (!inQuotes) {
1720
+ inQuotes = true;
1721
+ quoteChar = char;
1722
+ } else if (char === quoteChar) {
1723
+ inQuotes = false;
1724
+ quoteChar = "";
1725
+ }
1726
+ continue;
1727
+ }
1728
+ if (inQuotes) continue;
1729
+ if (char === "(") parenCount++;
1730
+ else if (char === ")") parenCount--;
1731
+ if (parenCount === 0 && !inQuotes) {
1732
+ if (afterThen.substring(i).match(/^ELSE\s+(.+)$/i)) {
1733
+ elseIndex = i;
1734
+ break;
1735
+ }
1736
+ }
1737
+ }
1738
+ if (elseIndex === -1) return exprText;
1739
+ const trueExpr = afterThen.substring(0, elseIndex).trim();
1740
+ let falseExpr = afterThen.substring(elseIndex + 5).trim();
1741
+ let endIndex = -1;
1742
+ parenCount = 0;
1743
+ inQuotes = false;
1744
+ quoteChar = "";
1745
+ for (let i = 0; i < falseExpr.length; i++) {
1746
+ const char = falseExpr[i];
1747
+ if ((char === "\"" || char === "'") && (i === 0 || falseExpr[i - 1] !== "\\")) {
1748
+ if (!inQuotes) {
1749
+ inQuotes = true;
1750
+ quoteChar = char;
1751
+ } else if (char === quoteChar) {
1752
+ inQuotes = false;
1753
+ quoteChar = "";
1754
+ }
1755
+ continue;
1756
+ }
1757
+ if (inQuotes) continue;
1758
+ if (char === "(") parenCount++;
1759
+ else if (char === ")") {
1760
+ if (parenCount === 0) {
1761
+ endIndex = i;
1762
+ break;
1763
+ }
1764
+ parenCount--;
1765
+ }
1766
+ }
1767
+ if (endIndex !== -1) falseExpr = falseExpr.substring(0, endIndex).trim();
1768
+ const convertedTrueExpr = convertConditionalExpressions(trueExpr);
1769
+ const convertedFalseExpr = convertConditionalExpressions(falseExpr);
1770
+ const convertedCondition = condition.replace(/(?<!".*?)\b AND \b(?!.*?")/gi, " :AND: ").replace(/(?<!".*?)\b OR \b(?!.*?")/gi, " :OR: ").replace(/(?<!".*?)\b\s?NOT \b(?!.*?")/gi, " :NOT: ").replace(/^\((.+)\)$/, "$1");
1771
+ const falseExprEndInAfterIf = afterIf.indexOf(" ELSE ") + 6 + falseExpr.length;
1772
+ const conditionalEndInNormalizedText = ifIndex + 3 + falseExprEndInAfterIf;
1773
+ return `${beforeIf}IF THEN ELSE(${convertedCondition}, ${convertedTrueExpr}, ${convertedFalseExpr})${normalizedText.substring(conditionalEndInNormalizedText).trim()}`;
1959
1774
  }
1960
-
1961
- // src/xmile/parse-xmile-model.ts
1775
+ //#endregion
1776
+ //#region src/xmile/parse-xmile-model.ts
1777
+ /**
1778
+ * Parse the given XMILE model definition and return a `Model` AST node.
1779
+ *
1780
+ * @param input A string containing the XMILE model.
1781
+ * @returns A `Model` AST node.
1782
+ */
1962
1783
  function parseXmileModel(input) {
1963
- let xml;
1964
- try {
1965
- xml = parseXml(input, { includeOffsets: true });
1966
- } catch (e) {
1967
- const msg = `Failed to parse XMILE model definition:
1968
-
1969
- ${e.message}`;
1970
- throw new Error(msg);
1971
- }
1972
- const simulationSpec = parseSimSpecs(xml.root, input);
1973
- const dimensions = parseDimensionDefs(xml.root, input);
1974
- const equations = parseVariableDefs(xml.root, input);
1975
- return {
1976
- simulationSpec,
1977
- dimensions,
1978
- equations
1979
- };
1784
+ let xml;
1785
+ try {
1786
+ xml = parseXml(input, { includeOffsets: true });
1787
+ } catch (e) {
1788
+ const msg = `Failed to parse XMILE model definition:\n\n${e.message}`;
1789
+ throw new Error(msg, { cause: e });
1790
+ }
1791
+ return {
1792
+ simulationSpec: parseSimSpecs(xml.root, input),
1793
+ dimensions: parseDimensionDefs(xml.root, input),
1794
+ equations: parseVariableDefs(xml.root, input)
1795
+ };
1980
1796
  }
1981
1797
  function parseSimSpecs(rootElem, originalXml) {
1982
- const simSpecsElem = firstElemOf(rootElem, "sim_specs");
1983
- if (simSpecsElem === void 0) {
1984
- throw new Error(xmlError(rootElem, "<sim_specs> element is required for XMILE model definition"));
1985
- }
1986
- function getSimSpecValue(name, required) {
1987
- const elem = firstElemOf(simSpecsElem, name);
1988
- if (required && elem === void 0) {
1989
- const error = new Error(xmlError(simSpecsElem, `<${name}> element is required in XMILE sim specs`));
1990
- throwXmileParseError(error, originalXml, simSpecsElem, "model");
1991
- }
1992
- if (elem === void 0) {
1993
- return void 0;
1994
- }
1995
- const value = Number(elem.text);
1996
- if (!isNaN(value)) {
1997
- return value;
1998
- } else {
1999
- const error = new Error(xmlError(elem, `Invalid numeric value for <${name}> element: ${elem.text}`));
2000
- throwXmileParseError(error, originalXml, simSpecsElem, "model");
2001
- }
2002
- }
2003
- const startTime = getSimSpecValue("start", true);
2004
- const endTime = getSimSpecValue("stop", true);
2005
- let timeStep = getSimSpecValue("dt", false);
2006
- if (timeStep === void 0) {
2007
- timeStep = 1;
2008
- }
2009
- return {
2010
- startTime,
2011
- endTime,
2012
- timeStep
2013
- };
1798
+ const simSpecsElem = firstElemOf(rootElem, "sim_specs");
1799
+ if (simSpecsElem === void 0) throw new Error(xmlError(rootElem, "<sim_specs> element is required for XMILE model definition"));
1800
+ function getSimSpecValue(name, required) {
1801
+ const elem = firstElemOf(simSpecsElem, name);
1802
+ if (required && elem === void 0) throwXmileParseError(new Error(xmlError(simSpecsElem, `<${name}> element is required in XMILE sim specs`)), originalXml, simSpecsElem, "model");
1803
+ if (elem === void 0) return;
1804
+ const value = Number(elem.text);
1805
+ if (!isNaN(value)) return value;
1806
+ else throwXmileParseError(new Error(xmlError(elem, `Invalid numeric value for <${name}> element: ${elem.text}`)), originalXml, simSpecsElem, "model");
1807
+ }
1808
+ const startTime = getSimSpecValue("start", true);
1809
+ const endTime = getSimSpecValue("stop", true);
1810
+ let timeStep = getSimSpecValue("dt", false);
1811
+ if (timeStep === void 0) timeStep = 1;
1812
+ return {
1813
+ startTime,
1814
+ endTime,
1815
+ timeStep
1816
+ };
2014
1817
  }
2015
1818
  function parseDimensionDefs(rootElem, originalXml) {
2016
- const dimensionDefs = [];
2017
- const dimensionsElem = firstElemOf(rootElem, "dimensions");
2018
- if (dimensionsElem) {
2019
- const dimElems = elemsOf(dimensionsElem, ["dim"]);
2020
- for (const dimElem of dimElems) {
2021
- try {
2022
- dimensionDefs.push(parseXmileDimensionDef(dimElem));
2023
- } catch (e) {
2024
- throwXmileParseError(e, originalXml, dimElem, "dimension");
2025
- }
2026
- }
2027
- }
2028
- return dimensionDefs;
1819
+ const dimensionDefs = [];
1820
+ const dimensionsElem = firstElemOf(rootElem, "dimensions");
1821
+ if (dimensionsElem) {
1822
+ const dimElems = elemsOf(dimensionsElem, ["dim"]);
1823
+ for (const dimElem of dimElems) try {
1824
+ dimensionDefs.push(parseXmileDimensionDef(dimElem));
1825
+ } catch (e) {
1826
+ throwXmileParseError(e, originalXml, dimElem, "dimension");
1827
+ }
1828
+ }
1829
+ return dimensionDefs;
2029
1830
  }
2030
1831
  function parseVariableDefs(rootElem, originalXml) {
2031
- const modelElem = firstElemOf(rootElem, "model");
2032
- if (modelElem === void 0) {
2033
- return [];
2034
- }
2035
- const equations = [];
2036
- const variablesElem = firstElemOf(modelElem, "variables");
2037
- if (variablesElem) {
2038
- const varElems = elemsOf(variablesElem, ["aux", "stock", "flow", "gf"]);
2039
- for (const varElem of varElems) {
2040
- try {
2041
- const eqns = parseXmileVariableDef(varElem);
2042
- if (eqns) {
2043
- equations.push(...eqns);
2044
- }
2045
- } catch (e) {
2046
- throwXmileParseError(e, originalXml, varElem, "variable");
2047
- }
2048
- }
2049
- }
2050
- return equations;
1832
+ const modelElem = firstElemOf(rootElem, "model");
1833
+ if (modelElem === void 0) return [];
1834
+ const equations = [];
1835
+ const variablesElem = firstElemOf(modelElem, "variables");
1836
+ if (variablesElem) {
1837
+ const varElems = elemsOf(variablesElem, [
1838
+ "aux",
1839
+ "stock",
1840
+ "flow",
1841
+ "gf"
1842
+ ]);
1843
+ for (const varElem of varElems) try {
1844
+ const eqns = parseXmileVariableDef(varElem);
1845
+ if (eqns) equations.push(...eqns);
1846
+ } catch (e) {
1847
+ throwXmileParseError(e, originalXml, varElem, "variable");
1848
+ }
1849
+ }
1850
+ return equations;
2051
1851
  }
2052
1852
  function throwXmileParseError(originalError, originalXml, elem, elemKind) {
2053
- let linePart = "";
2054
- const lineNumInOriginalXml = getLineNumber(originalXml, elem.start);
2055
- if (lineNumInOriginalXml !== -1) {
2056
- const cause = originalError.cause;
2057
- if (cause?.code === "VensimParseError") {
2058
- if (cause.line) {
2059
- const lineNum = cause.line - 1 + lineNumInOriginalXml;
2060
- linePart += ` at line ${lineNum}`;
2061
- if (cause.column) {
2062
- linePart += `, col ${cause.column}`;
2063
- }
2064
- }
2065
- } else {
2066
- linePart += ` at line ${lineNumInOriginalXml}`;
2067
- }
2068
- }
2069
- const elemString = extractXmlLines(originalXml, elem.start, elem.end);
2070
- const msg = `Failed to parse XMILE ${elemKind} definition${linePart}:
2071
- ${elemString}
2072
-
2073
- Detail:
2074
- ${originalError.message}`;
2075
- throw new Error(msg);
1853
+ let linePart = "";
1854
+ const lineNumInOriginalXml = getLineNumber(originalXml, elem.start);
1855
+ if (lineNumInOriginalXml !== -1) {
1856
+ const cause = originalError.cause;
1857
+ if (cause?.code === "VensimParseError") {
1858
+ if (cause.line) {
1859
+ const lineNum = cause.line - 1 + lineNumInOriginalXml;
1860
+ linePart += ` at line ${lineNum}`;
1861
+ if (cause.column) linePart += `, col ${cause.column}`;
1862
+ }
1863
+ } else linePart += ` at line ${lineNumInOriginalXml}`;
1864
+ }
1865
+ const elemString = extractXmlLines(originalXml, elem.start, elem.end);
1866
+ const msg = `Failed to parse XMILE ${elemKind} definition${linePart}:\n${elemString}\n\nDetail:\n ${originalError.message}`;
1867
+ throw new Error(msg);
2076
1868
  }
1869
+ /**
1870
+ * Calculate the line number from a byte offset in the original XML string.
1871
+ *
1872
+ * @param xmlString The original XML string
1873
+ * @param byteOffset The byte offset from the XmlElement
1874
+ * @returns The line number (1-indexed) or -1 if offset is invalid
1875
+ */
2077
1876
  function getLineNumber(xmlString, byteOffset) {
2078
- if (byteOffset === -1 || byteOffset >= xmlString.length) {
2079
- return -1;
2080
- }
2081
- const substring = xmlString.substring(0, byteOffset);
2082
- return substring.split("\n").length;
1877
+ if (byteOffset === -1 || byteOffset >= xmlString.length) return -1;
1878
+ return xmlString.substring(0, byteOffset).split("\n").length;
2083
1879
  }
1880
+ /**
1881
+ * Extract relevant lines from the original XML string using start/end byte offsets.
1882
+ * Includes full lines for context, even if start/end are not at line boundaries.
1883
+ *
1884
+ * @param originalXml The original XML string
1885
+ * @param startOffset The starting byte offset
1886
+ * @param endOffset The ending byte offset
1887
+ * @returns A string containing the relevant lines with line numbers
1888
+ */
2084
1889
  function extractXmlLines(originalXml, startOffset, endOffset) {
2085
- if (startOffset === -1 || endOffset === -1 || startOffset >= originalXml.length || endOffset > originalXml.length) {
2086
- return "[Unable to extract XML lines - invalid offsets]";
2087
- }
2088
- let lineStart = startOffset;
2089
- while (lineStart > 0 && originalXml[lineStart - 1] !== "\n") {
2090
- lineStart--;
2091
- }
2092
- let lineEnd = endOffset;
2093
- while (lineEnd < originalXml.length && originalXml[lineEnd] !== "\n") {
2094
- lineEnd++;
2095
- }
2096
- const relevantXml = originalXml.substring(lineStart, lineEnd);
2097
- return relevantXml;
1890
+ if (startOffset === -1 || endOffset === -1 || startOffset >= originalXml.length || endOffset > originalXml.length) return "[Unable to extract XML lines - invalid offsets]";
1891
+ let lineStart = startOffset;
1892
+ while (lineStart > 0 && originalXml[lineStart - 1] !== "\n") lineStart--;
1893
+ let lineEnd = endOffset;
1894
+ while (lineEnd < originalXml.length && originalXml[lineEnd] !== "\n") lineEnd++;
1895
+ return originalXml.substring(lineStart, lineEnd);
2098
1896
  }
2099
- export {
2100
- canonicalFunctionId,
2101
- canonicalId,
2102
- canonicalVarId,
2103
- debugPrintExpr,
2104
- parseVensimEquation,
2105
- parseVensimExpr,
2106
- parseVensimModel,
2107
- parseVensimSubscriptRange,
2108
- parseXmileDimensionDef,
2109
- parseXmileModel,
2110
- parseXmileVariableDef,
2111
- preprocessVensimModel,
2112
- prettyPrintExpr,
2113
- printExprStats,
2114
- reduceConditionals,
2115
- reduceExpr,
2116
- toPrettyString
2117
- };
1897
+ //#endregion
1898
+ export { canonicalFunctionId, canonicalId, canonicalVarId, debugPrintExpr, parseVensimEquation, parseVensimExpr, parseVensimModel, parseVensimSubscriptRange, parseXmileDimensionDef, parseXmileModel, parseXmileVariableDef, preprocessVensimModel, prettyPrintExpr, printExprStats, reduceConditionals, reduceExpr, toPrettyString };
1899
+
2118
1900
  //# sourceMappingURL=index.js.map