@sdeverywhere/parse 0.1.6 → 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.d.ts +366 -354
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1776 -1994
- package/dist/index.js.map +1 -1
- package/package.json +4 -6
- package/dist/index.cjs +0 -2170
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -605
package/dist/index.js
CHANGED
|
@@ -1,2118 +1,1900 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
52
|
+
return canonicalId(name).toUpperCase();
|
|
22
53
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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 = "<";
|
|
134
|
+
break;
|
|
135
|
+
case "<=":
|
|
136
|
+
op = "<=";
|
|
137
|
+
break;
|
|
138
|
+
case ">":
|
|
139
|
+
op = ">";
|
|
140
|
+
break;
|
|
141
|
+
case ">=":
|
|
142
|
+
op = ">=";
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
182
|
-
|
|
196
|
+
const count = map.get(key) || 0;
|
|
197
|
+
map.set(key, count + 1);
|
|
183
198
|
}
|
|
184
199
|
function getExprStats(expr, stats) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
280
|
+
return {
|
|
281
|
+
subName: dimOrSubName,
|
|
282
|
+
subId: canonicalId(dimOrSubName)
|
|
283
|
+
};
|
|
278
284
|
}
|
|
279
285
|
function num(value, text) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
286
|
+
return {
|
|
287
|
+
kind: "number",
|
|
288
|
+
value,
|
|
289
|
+
text: text || value.toString()
|
|
290
|
+
};
|
|
285
291
|
}
|
|
286
292
|
function unaryOp(op, expr) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
293
|
+
return {
|
|
294
|
+
kind: "unary-op",
|
|
295
|
+
op,
|
|
296
|
+
expr
|
|
297
|
+
};
|
|
292
298
|
}
|
|
293
299
|
function binaryOp(lhs, op, rhs) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
+
return {
|
|
301
|
+
kind: "binary-op",
|
|
302
|
+
lhs,
|
|
303
|
+
op,
|
|
304
|
+
rhs
|
|
305
|
+
};
|
|
300
306
|
}
|
|
301
307
|
function parens(expr) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
308
|
+
return {
|
|
309
|
+
kind: "parens",
|
|
310
|
+
expr
|
|
311
|
+
};
|
|
306
312
|
}
|
|
307
313
|
function lookupDef(points, range) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
314
|
+
return {
|
|
315
|
+
kind: "lookup-def",
|
|
316
|
+
range,
|
|
317
|
+
points
|
|
318
|
+
};
|
|
313
319
|
}
|
|
314
320
|
function lookupCall(varRef, arg) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
321
|
+
return {
|
|
322
|
+
kind: "lookup-call",
|
|
323
|
+
varRef,
|
|
324
|
+
arg
|
|
325
|
+
};
|
|
320
326
|
}
|
|
321
327
|
function call(fnName, ...args) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
+
return {
|
|
329
|
+
kind: "function-call",
|
|
330
|
+
fnName,
|
|
331
|
+
fnId: canonicalFunctionId(fnName),
|
|
332
|
+
args
|
|
333
|
+
};
|
|
328
334
|
}
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region src/vensim/impl/subscript-range-reader.js
|
|
628
572
|
var SubscriptRangeReader = class extends ModelVisitor {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
|
|
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
|
-
|
|
768
|
-
return subscriptReader.parse(input);
|
|
700
|
+
return new SubscriptRangeReader(context).parse(input);
|
|
769
701
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1036
|
-
return exprReader.parse(input);
|
|
939
|
+
return new ExprReader().parse(input);
|
|
1037
940
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1212
|
-
return equationReader.parse(input);
|
|
1082
|
+
return new EquationReader().parse(input);
|
|
1213
1083
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
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
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
-
|
|
1242
|
+
return input.replace(/\s\s+/g, " ").trim();
|
|
1340
1243
|
}
|
|
1341
|
-
|
|
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
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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
|
-
|
|
1517
|
-
import { XmlNode } from "@rgrove/parse-xml";
|
|
1425
|
+
//#endregion
|
|
1426
|
+
//#region src/xmile/xml.ts
|
|
1518
1427
|
function firstElemOf(parent, tagName) {
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
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
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1434
|
+
return parent?.children.find((n) => {
|
|
1435
|
+
return n.type === XmlNode.TYPE_TEXT;
|
|
1436
|
+
});
|
|
1532
1437
|
}
|
|
1533
1438
|
function elemsOf(parent, tagNames) {
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
-
|
|
1448
|
+
return `${msg}: ${JSON.stringify(elem.toJSON(), null, 2)}`;
|
|
1550
1449
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
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
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
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
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
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
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
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
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
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
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
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
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
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
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
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
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
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
|
-
|
|
1856
|
-
return s?.trim();
|
|
1680
|
+
return (elem.attributes && elem.attributes[attrName])?.trim();
|
|
1857
1681
|
}
|
|
1858
1682
|
function parseFloatAttr(varElem, elem, attrName) {
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
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
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
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
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
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
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
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
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
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
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
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
|
-
|
|
2079
|
-
|
|
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
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
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
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
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
|