vba-runner 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.ja.md +94 -0
- package/README.md +91 -0
- package/dist/bin/vba-analyzer.cjs +5093 -0
- package/dist/bin/vba-formatter.cjs +1012 -0
- package/dist/bin/vba-parse-check.cjs +2499 -0
- package/dist/bin/vba-run.cjs +9414 -0
- package/dist/lib.cjs +10650 -0
- package/package.json +33 -0
|
@@ -0,0 +1,2499 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
// ../../src/engine/lexer.ts
|
|
26
|
+
var LexError = class extends Error {
|
|
27
|
+
constructor(message, line, column) {
|
|
28
|
+
super(`${message} (line ${line})`);
|
|
29
|
+
this.line = line;
|
|
30
|
+
this.column = column;
|
|
31
|
+
this.name = "LexError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var Lexer = class _Lexer {
|
|
35
|
+
input = "";
|
|
36
|
+
pos = 0;
|
|
37
|
+
line = 1;
|
|
38
|
+
column = 1;
|
|
39
|
+
diagnostics = [];
|
|
40
|
+
// MS-VBAL §3.3.5: name-start-character = Unicode-Letter (Lu,Ll,Lt,Lm,Lo,Nl) | "_"
|
|
41
|
+
static reUnicodeNameStart = /^[\p{L}\p{Nl}]$/u;
|
|
42
|
+
// name-continue-character adds Unicode-Combining-Character (Mn,Mc), Digit (Nd), Connector-Punct (Pc)
|
|
43
|
+
static reUnicodeNameContinue = /^[\p{Mn}\p{Mc}\p{Nd}\p{Pc}]$/u;
|
|
44
|
+
constructor(input) {
|
|
45
|
+
this.input = input;
|
|
46
|
+
}
|
|
47
|
+
peek() {
|
|
48
|
+
if (this.pos >= this.input.length) return "\0";
|
|
49
|
+
return this.input[this.pos];
|
|
50
|
+
}
|
|
51
|
+
advance() {
|
|
52
|
+
if (this.pos >= this.input.length) return "\0";
|
|
53
|
+
const char = this.input[this.pos++];
|
|
54
|
+
if (char === "\n") {
|
|
55
|
+
this.line++;
|
|
56
|
+
this.column = 1;
|
|
57
|
+
} else {
|
|
58
|
+
this.column++;
|
|
59
|
+
}
|
|
60
|
+
return char;
|
|
61
|
+
}
|
|
62
|
+
isWhitespace(char) {
|
|
63
|
+
return char === " " || char === " " || char === "\r";
|
|
64
|
+
}
|
|
65
|
+
isAlpha(char) {
|
|
66
|
+
if (char >= "a" && char <= "z" || char >= "A" && char <= "Z" || char === "_") return true;
|
|
67
|
+
const code = char.codePointAt(0) ?? 0;
|
|
68
|
+
return code > 127 && _Lexer.reUnicodeNameStart.test(char);
|
|
69
|
+
}
|
|
70
|
+
isAlphaNumeric(char) {
|
|
71
|
+
if (char >= "a" && char <= "z" || char >= "A" && char <= "Z" || char === "_") return true;
|
|
72
|
+
if (char >= "0" && char <= "9") return true;
|
|
73
|
+
const code = char.codePointAt(0) ?? 0;
|
|
74
|
+
return code > 127 && (_Lexer.reUnicodeNameStart.test(char) || _Lexer.reUnicodeNameContinue.test(char));
|
|
75
|
+
}
|
|
76
|
+
isDigit(char) {
|
|
77
|
+
return char >= "0" && char <= "9";
|
|
78
|
+
}
|
|
79
|
+
skipWhitespace() {
|
|
80
|
+
while (this.isWhitespace(this.peek())) {
|
|
81
|
+
this.advance();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
getNextToken() {
|
|
85
|
+
while (true) {
|
|
86
|
+
this.skipWhitespace();
|
|
87
|
+
const startLine = this.line;
|
|
88
|
+
const startColumn = this.column;
|
|
89
|
+
if (this.pos >= this.input.length) {
|
|
90
|
+
return { type: 136 /* EOF */, value: "", line: startLine, column: startColumn };
|
|
91
|
+
}
|
|
92
|
+
const char = this.peek();
|
|
93
|
+
if (char === "#") {
|
|
94
|
+
let foundClosingHash = false;
|
|
95
|
+
for (let j = this.pos + 1; j < this.input.length; j++) {
|
|
96
|
+
if (this.input[j] === "#") {
|
|
97
|
+
foundClosingHash = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
if (this.input[j] === "\n") break;
|
|
101
|
+
}
|
|
102
|
+
if (foundClosingHash) {
|
|
103
|
+
let potentialDate = "";
|
|
104
|
+
let k = this.pos + 1;
|
|
105
|
+
while (k < this.input.length && this.input[k] !== "#" && this.input[k] !== "\n") {
|
|
106
|
+
potentialDate += this.input[k];
|
|
107
|
+
k++;
|
|
108
|
+
}
|
|
109
|
+
const dateRegex = /^[0-9\/\-\s:apm,]+$/i;
|
|
110
|
+
const monthsRegex = /jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec/i;
|
|
111
|
+
if (dateRegex.test(potentialDate) || monthsRegex.test(potentialDate)) {
|
|
112
|
+
this.advance();
|
|
113
|
+
let dateValue = "";
|
|
114
|
+
while (this.peek() !== "#" && this.peek() !== "\n" && this.peek() !== "\0") {
|
|
115
|
+
dateValue += this.advance();
|
|
116
|
+
}
|
|
117
|
+
if (this.peek() === "#") {
|
|
118
|
+
this.advance();
|
|
119
|
+
return { type: 134 /* Date */, value: dateValue, line: startLine, column: startColumn };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
this.advance();
|
|
124
|
+
return { type: 111 /* OperatorHash */, value: "#", line: startLine, column: startColumn };
|
|
125
|
+
}
|
|
126
|
+
if (char === "_") {
|
|
127
|
+
const next = this.pos + 1 < this.input.length ? this.input[this.pos + 1] : "\0";
|
|
128
|
+
const afterCr = next === "\r" && this.pos + 2 < this.input.length ? this.input[this.pos + 2] : "\0";
|
|
129
|
+
if (next === "\n" || next === "\r" && afterCr === "\n") {
|
|
130
|
+
this.advance();
|
|
131
|
+
if (this.peek() === "\r") this.advance();
|
|
132
|
+
if (this.peek() === "\n") this.advance();
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
let lookahead = this.pos + 1;
|
|
136
|
+
while (lookahead < this.input.length && this.input[lookahead] === " ") lookahead++;
|
|
137
|
+
const afterSpaces = lookahead < this.input.length ? this.input[lookahead] : "\0";
|
|
138
|
+
if (afterSpaces === "\n" || afterSpaces === "\r" || afterSpaces === "'") {
|
|
139
|
+
const msg = afterSpaces === "'" ? "\u884C\u7D99\u7D9A\u6587\u5B57 '_' \u306E\u5F8C\u306B\u30B3\u30E1\u30F3\u30C8\u306F\u8A18\u8FF0\u3067\u304D\u307E\u305B\u3093" : "\u884C\u7D99\u7D9A\u6587\u5B57 '_' \u306E\u76F4\u5F8C\u306B\u7A7A\u767D\u3092\u7F6E\u304F\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093";
|
|
140
|
+
this.advance();
|
|
141
|
+
while (this.peek() !== "\n" && this.peek() !== "\0") this.advance();
|
|
142
|
+
if (this.peek() === "\n") this.advance();
|
|
143
|
+
throw new LexError(msg, startLine, startColumn);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (char === "'") {
|
|
147
|
+
let commentText = "";
|
|
148
|
+
while (this.peek() !== "\n" && this.peek() !== "\0") {
|
|
149
|
+
commentText += this.advance();
|
|
150
|
+
}
|
|
151
|
+
if (/[ \t]*_[ \t]*$/.test(commentText)) {
|
|
152
|
+
this.diagnostics.push({
|
|
153
|
+
message: "\u30B3\u30E1\u30F3\u30C8\u672B\u5C3E\u306E '_' \u306F\u884C\u7D99\u7D9A\u3068\u3057\u3066\u6A5F\u80FD\u3057\u307E\u305B\u3093",
|
|
154
|
+
line: startLine,
|
|
155
|
+
column: startColumn + 1 + commentText.lastIndexOf("_")
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (char === "\n") {
|
|
161
|
+
this.advance();
|
|
162
|
+
return { type: 135 /* Newline */, value: "\n", line: startLine, column: startColumn };
|
|
163
|
+
}
|
|
164
|
+
if (char === '"') {
|
|
165
|
+
this.advance();
|
|
166
|
+
let strValue = "";
|
|
167
|
+
while (this.peek() !== "\0") {
|
|
168
|
+
if (this.peek() === '"') {
|
|
169
|
+
this.advance();
|
|
170
|
+
if (this.peek() === '"') {
|
|
171
|
+
strValue += '"';
|
|
172
|
+
this.advance();
|
|
173
|
+
} else {
|
|
174
|
+
return { type: 2 /* String */, value: strValue, line: startLine, column: startColumn };
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
strValue += this.advance();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { type: 2 /* String */, value: strValue, line: startLine, column: startColumn };
|
|
181
|
+
}
|
|
182
|
+
if (char === "=") {
|
|
183
|
+
this.advance();
|
|
184
|
+
return { type: 120 /* OperatorEquals */, value: "=", line: startLine, column: startColumn };
|
|
185
|
+
}
|
|
186
|
+
if (char === "<") {
|
|
187
|
+
this.advance();
|
|
188
|
+
if (this.peek() === ">") {
|
|
189
|
+
this.advance();
|
|
190
|
+
return { type: 121 /* OperatorNotEquals */, value: "<>", line: startLine, column: startColumn };
|
|
191
|
+
}
|
|
192
|
+
if (this.peek() === "=") {
|
|
193
|
+
this.advance();
|
|
194
|
+
return { type: 124 /* OperatorLessThanOrEqual */, value: "<=", line: startLine, column: startColumn };
|
|
195
|
+
}
|
|
196
|
+
return { type: 122 /* OperatorLessThan */, value: "<", line: startLine, column: startColumn };
|
|
197
|
+
}
|
|
198
|
+
if (char === ">") {
|
|
199
|
+
this.advance();
|
|
200
|
+
if (this.peek() === "=") {
|
|
201
|
+
this.advance();
|
|
202
|
+
return { type: 125 /* OperatorGreaterThanOrEqual */, value: ">=", line: startLine, column: startColumn };
|
|
203
|
+
}
|
|
204
|
+
return { type: 123 /* OperatorGreaterThan */, value: ">", line: startLine, column: startColumn };
|
|
205
|
+
}
|
|
206
|
+
if (char === "+") {
|
|
207
|
+
this.advance();
|
|
208
|
+
return { type: 112 /* OperatorPlus */, value: "+", line: startLine, column: startColumn };
|
|
209
|
+
}
|
|
210
|
+
if (char === "-") {
|
|
211
|
+
this.advance();
|
|
212
|
+
return { type: 113 /* OperatorMinus */, value: "-", line: startLine, column: startColumn };
|
|
213
|
+
}
|
|
214
|
+
if (char === "!") {
|
|
215
|
+
this.advance();
|
|
216
|
+
return { type: 132 /* OperatorExclamation */, value: "!", line: startLine, column: startColumn };
|
|
217
|
+
}
|
|
218
|
+
if (char === "&") {
|
|
219
|
+
this.advance();
|
|
220
|
+
const next = this.peek().toLowerCase();
|
|
221
|
+
if (next === "h") {
|
|
222
|
+
this.advance();
|
|
223
|
+
let hexStr = "";
|
|
224
|
+
while (/[0-9a-f]/i.test(this.peek())) {
|
|
225
|
+
hexStr += this.advance();
|
|
226
|
+
}
|
|
227
|
+
return { type: 1 /* Number */, value: "0x" + hexStr, line: startLine, column: startColumn };
|
|
228
|
+
} else if (next === "o" || this.isDigit(next)) {
|
|
229
|
+
if (next === "o") this.advance();
|
|
230
|
+
let octStr = "";
|
|
231
|
+
while (/[0-7]/.test(this.peek())) {
|
|
232
|
+
octStr += this.advance();
|
|
233
|
+
}
|
|
234
|
+
return { type: 1 /* Number */, value: "0o" + octStr, line: startLine, column: startColumn };
|
|
235
|
+
}
|
|
236
|
+
return { type: 117 /* OperatorAmpersand */, value: "&", line: startLine, column: startColumn };
|
|
237
|
+
}
|
|
238
|
+
if (char === ",") {
|
|
239
|
+
this.advance();
|
|
240
|
+
return { type: 126 /* OperatorComma */, value: ",", line: startLine, column: startColumn };
|
|
241
|
+
}
|
|
242
|
+
if (char === "#") {
|
|
243
|
+
this.advance();
|
|
244
|
+
return { type: 111 /* OperatorHash */, value: "#", line: startLine, column: startColumn };
|
|
245
|
+
}
|
|
246
|
+
if (char === "(") {
|
|
247
|
+
this.advance();
|
|
248
|
+
return { type: 127 /* OperatorLParen */, value: "(", line: startLine, column: startColumn };
|
|
249
|
+
}
|
|
250
|
+
if (char === ")") {
|
|
251
|
+
this.advance();
|
|
252
|
+
return { type: 128 /* OperatorRParen */, value: ")", line: startLine, column: startColumn };
|
|
253
|
+
}
|
|
254
|
+
if (char === "[") {
|
|
255
|
+
this.advance();
|
|
256
|
+
let foreignStr = "";
|
|
257
|
+
while (this.peek() !== "]" && this.peek() !== "\n" && this.peek() !== "\r" && this.peek() !== "\0") {
|
|
258
|
+
foreignStr += this.advance();
|
|
259
|
+
}
|
|
260
|
+
if (this.peek() === "]") this.advance();
|
|
261
|
+
return { type: 138 /* ForeignName */, value: foreignStr, line: startLine, column: startColumn };
|
|
262
|
+
}
|
|
263
|
+
if (char === ".") {
|
|
264
|
+
this.advance();
|
|
265
|
+
return { type: 129 /* OperatorDot */, value: ".", line: startLine, column: startColumn };
|
|
266
|
+
}
|
|
267
|
+
if (char === ":") {
|
|
268
|
+
this.advance();
|
|
269
|
+
if (this.peek() === "=") {
|
|
270
|
+
this.advance();
|
|
271
|
+
return { type: 131 /* OperatorColonEquals */, value: ":=", line: startLine, column: startColumn };
|
|
272
|
+
}
|
|
273
|
+
return { type: 130 /* OperatorColon */, value: ":", line: startLine, column: startColumn };
|
|
274
|
+
}
|
|
275
|
+
if (char === ";") {
|
|
276
|
+
this.advance();
|
|
277
|
+
return { type: 133 /* OperatorSemicolon */, value: ";", line: startLine, column: startColumn };
|
|
278
|
+
}
|
|
279
|
+
if (this.isDigit(char)) {
|
|
280
|
+
let numStr = "";
|
|
281
|
+
while (this.isDigit(this.peek())) {
|
|
282
|
+
numStr += this.advance();
|
|
283
|
+
}
|
|
284
|
+
if (this.peek() === ".") {
|
|
285
|
+
numStr += this.advance();
|
|
286
|
+
while (this.isDigit(this.peek())) {
|
|
287
|
+
numStr += this.advance();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const nextChar = this.peek().toLowerCase();
|
|
291
|
+
if (nextChar === "e") {
|
|
292
|
+
numStr += this.advance();
|
|
293
|
+
if (this.peek() === "+" || this.peek() === "-") {
|
|
294
|
+
numStr += this.advance();
|
|
295
|
+
}
|
|
296
|
+
while (this.isDigit(this.peek())) {
|
|
297
|
+
numStr += this.advance();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const peekChar = this.peek();
|
|
301
|
+
if (["%", "&", "@", "!", "#", "^"].indexOf(peekChar) !== -1) {
|
|
302
|
+
numStr += this.advance();
|
|
303
|
+
}
|
|
304
|
+
return { type: 1 /* Number */, value: numStr, line: startLine, column: startColumn };
|
|
305
|
+
}
|
|
306
|
+
if (this.isAlpha(char)) {
|
|
307
|
+
let idStr = "";
|
|
308
|
+
while (this.isAlphaNumeric(this.peek())) {
|
|
309
|
+
idStr += this.advance();
|
|
310
|
+
}
|
|
311
|
+
const typeHints = ["$", "%", "&", "#", "@"];
|
|
312
|
+
if (typeHints.includes(this.peek())) {
|
|
313
|
+
idStr += this.advance();
|
|
314
|
+
}
|
|
315
|
+
const lowerId = idStr.toLowerCase();
|
|
316
|
+
const lowerBase = lowerId.replace(/[$%&#@]$/, "");
|
|
317
|
+
if (lowerBase === "rem") {
|
|
318
|
+
while (this.peek() !== "\n" && this.peek() !== "\0") {
|
|
319
|
+
this.advance();
|
|
320
|
+
}
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (lowerBase === "for") return { type: 3 /* KeywordFor */, value: idStr, line: startLine, column: startColumn };
|
|
324
|
+
if (lowerBase === "to") return { type: 4 /* KeywordTo */, value: idStr, line: startLine, column: startColumn };
|
|
325
|
+
if (lowerBase === "next") return { type: 5 /* KeywordNext */, value: idStr, line: startLine, column: startColumn };
|
|
326
|
+
if (lowerBase === "if") return { type: 6 /* KeywordIf */, value: idStr, line: startLine, column: startColumn };
|
|
327
|
+
if (lowerBase === "then") return { type: 7 /* KeywordThen */, value: idStr, line: startLine, column: startColumn };
|
|
328
|
+
if (lowerBase === "elseif") return { type: 8 /* KeywordElseIf */, value: idStr, line: startLine, column: startColumn };
|
|
329
|
+
if (lowerBase === "else") return { type: 9 /* KeywordElse */, value: idStr, line: startLine, column: startColumn };
|
|
330
|
+
if (lowerBase === "end") return { type: 10 /* KeywordEnd */, value: idStr, line: startLine, column: startColumn };
|
|
331
|
+
if (lowerBase === "do") return { type: 11 /* KeywordDo */, value: idStr, line: startLine, column: startColumn };
|
|
332
|
+
if (lowerBase === "while") return { type: 12 /* KeywordWhile */, value: idStr, line: startLine, column: startColumn };
|
|
333
|
+
if (lowerBase === "wend") return { type: 13 /* KeywordWend */, value: idStr, line: startLine, column: startColumn };
|
|
334
|
+
if (lowerBase === "loop") return { type: 14 /* KeywordLoop */, value: idStr, line: startLine, column: startColumn };
|
|
335
|
+
if (lowerBase === "until") return { type: 15 /* KeywordUntil */, value: idStr, line: startLine, column: startColumn };
|
|
336
|
+
if (lowerBase === "gosub") return { type: 37 /* KeywordGoSub */, value: idStr, line: startLine, column: startColumn };
|
|
337
|
+
if (lowerBase === "return") return { type: 38 /* KeywordReturn */, value: idStr, line: startLine, column: startColumn };
|
|
338
|
+
if (lowerBase === "lset") return { type: 39 /* KeywordLSet */, value: idStr, line: startLine, column: startColumn };
|
|
339
|
+
if (lowerBase === "rset") return { type: 40 /* KeywordRSet */, value: idStr, line: startLine, column: startColumn };
|
|
340
|
+
if (lowerBase === "stop") return { type: 16 /* KeywordStop */, value: idStr, line: startLine, column: startColumn };
|
|
341
|
+
if (lowerBase === "sub") return { type: 17 /* KeywordSub */, value: idStr, line: startLine, column: startColumn };
|
|
342
|
+
if (lowerBase === "function") return { type: 18 /* KeywordFunction */, value: idStr, line: startLine, column: startColumn };
|
|
343
|
+
if (lowerBase === "property") return { type: 19 /* KeywordProperty */, value: idStr, line: startLine, column: startColumn };
|
|
344
|
+
if (lowerBase === "get") return { type: 20 /* KeywordGet */, value: idStr, line: startLine, column: startColumn };
|
|
345
|
+
if (lowerBase === "let") return { type: 21 /* KeywordLet */, value: idStr, line: startLine, column: startColumn };
|
|
346
|
+
if (lowerBase === "dim") return { type: 22 /* KeywordDim */, value: idStr, line: startLine, column: startColumn };
|
|
347
|
+
if (lowerBase === "as") return { type: 23 /* KeywordAs */, value: idStr, line: startLine, column: startColumn };
|
|
348
|
+
if (lowerBase === "new") return { type: 24 /* KeywordNew */, value: idStr, line: startLine, column: startColumn };
|
|
349
|
+
if (lowerBase === "collection") return { type: 25 /* KeywordCollection */, value: idStr, line: startLine, column: startColumn };
|
|
350
|
+
if (lowerBase === "like") return { type: 63 /* KeywordLike */, value: idStr, line: startLine, column: startColumn };
|
|
351
|
+
if (lowerBase === "and") return { type: 26 /* KeywordAnd */, value: idStr, line: startLine, column: startColumn };
|
|
352
|
+
if (lowerBase === "or") return { type: 27 /* KeywordOr */, value: idStr, line: startLine, column: startColumn };
|
|
353
|
+
if (lowerBase === "xor") return { type: 64 /* KeywordXor */, value: idStr, line: startLine, column: startColumn };
|
|
354
|
+
if (lowerBase === "eqv") return { type: 65 /* KeywordEqv */, value: idStr, line: startLine, column: startColumn };
|
|
355
|
+
if (lowerBase === "imp") return { type: 66 /* KeywordImp */, value: idStr, line: startLine, column: startColumn };
|
|
356
|
+
if (lowerBase === "not") return { type: 28 /* KeywordNot */, value: idStr, line: startLine, column: startColumn };
|
|
357
|
+
if (lowerBase === "option") return { type: 29 /* KeywordOption */, value: idStr, line: startLine, column: startColumn };
|
|
358
|
+
if (lowerBase === "explicit") return { type: 30 /* KeywordExplicit */, value: idStr, line: startLine, column: startColumn };
|
|
359
|
+
if (lowerBase === "const") return { type: 31 /* KeywordConst */, value: idStr, line: startLine, column: startColumn };
|
|
360
|
+
if (lowerBase === "set") return { type: 32 /* KeywordSet */, value: idStr, line: startLine, column: startColumn };
|
|
361
|
+
if (lowerBase === "call") return { type: 33 /* KeywordCall */, value: idStr, line: startLine, column: startColumn };
|
|
362
|
+
if (lowerBase === "on") return { type: 34 /* KeywordOn */, value: idStr, line: startLine, column: startColumn };
|
|
363
|
+
if (lowerBase === "error") return { type: 35 /* KeywordError */, value: idStr, line: startLine, column: startColumn };
|
|
364
|
+
if (lowerBase === "goto") return { type: 36 /* KeywordGoTo */, value: idStr, line: startLine, column: startColumn };
|
|
365
|
+
if (lowerBase === "erase") return { type: 41 /* KeywordErase */, value: idStr, line: startLine, column: startColumn };
|
|
366
|
+
if (lowerBase === "redim") return { type: 42 /* KeywordReDim */, value: idStr, line: startLine, column: startColumn };
|
|
367
|
+
if (lowerBase === "step") return { type: 43 /* KeywordStep */, value: idStr, line: startLine, column: startColumn };
|
|
368
|
+
if (lowerBase === "empty") return { type: 44 /* KeywordEmpty */, value: idStr, line: startLine, column: startColumn };
|
|
369
|
+
if (lowerBase === "exit") return { type: 45 /* KeywordExit */, value: idStr, line: startLine, column: startColumn };
|
|
370
|
+
if (lowerBase === "byref") return { type: 46 /* KeywordByRef */, value: idStr, line: startLine, column: startColumn };
|
|
371
|
+
if (lowerBase === "byval") return { type: 47 /* KeywordByVal */, value: idStr, line: startLine, column: startColumn };
|
|
372
|
+
if (lowerBase === "mod") return { type: 118 /* KeywordMod */, value: idStr, line: startLine, column: startColumn };
|
|
373
|
+
if (lowerBase === "type") return { type: 48 /* KeywordType */, value: idStr, line: startLine, column: startColumn };
|
|
374
|
+
if (lowerBase === "nothing") return { type: 49 /* KeywordNothing */, value: idStr, line: startLine, column: startColumn };
|
|
375
|
+
if (lowerBase === "optional") return { type: 50 /* KeywordOptional */, value: idStr, line: startLine, column: startColumn };
|
|
376
|
+
if (lowerBase === "is") return { type: 51 /* KeywordIs */, value: idStr, line: startLine, column: startColumn };
|
|
377
|
+
if (lowerBase === "resume") return { type: 52 /* KeywordResume */, value: idStr, line: startLine, column: startColumn };
|
|
378
|
+
if (lowerBase === "select") return { type: 53 /* KeywordSelect */, value: idStr, line: startLine, column: startColumn };
|
|
379
|
+
if (lowerBase === "case") return { type: 54 /* KeywordCase */, value: idStr, line: startLine, column: startColumn };
|
|
380
|
+
if (lowerBase === "each") return { type: 55 /* KeywordEach */, value: idStr, line: startLine, column: startColumn };
|
|
381
|
+
if (lowerBase === "in") return { type: 56 /* KeywordIn */, value: idStr, line: startLine, column: startColumn };
|
|
382
|
+
if (lowerBase === "public") return { type: 57 /* KeywordPublic */, value: idStr, line: startLine, column: startColumn };
|
|
383
|
+
if (lowerBase === "private") return { type: 58 /* KeywordPrivate */, value: idStr, line: startLine, column: startColumn };
|
|
384
|
+
if (lowerBase === "enum") return { type: 59 /* KeywordEnum */, value: idStr, line: startLine, column: startColumn };
|
|
385
|
+
if (lowerBase === "typeof") return { type: 62 /* KeywordTypeOf */, value: idStr, line: startLine, column: startColumn };
|
|
386
|
+
if (lowerBase === "friend") return { type: 60 /* KeywordFriend */, value: idStr, line: startLine, column: startColumn };
|
|
387
|
+
if (lowerBase === "with") return { type: 61 /* KeywordWith */, value: idStr, line: startLine, column: startColumn };
|
|
388
|
+
if (lowerBase === "null") return { type: 67 /* KeywordNull */, value: idStr, line: startLine, column: startColumn };
|
|
389
|
+
if (lowerBase === "static") return { type: 68 /* KeywordStatic */, value: idStr, line: startLine, column: startColumn };
|
|
390
|
+
if (lowerBase === "class") return { type: 69 /* KeywordClass */, value: idStr, line: startLine, column: startColumn };
|
|
391
|
+
if (lowerBase === "me") return { type: 70 /* KeywordMe */, value: idStr, line: startLine, column: startColumn };
|
|
392
|
+
if (lowerBase === "compare") return { type: 71 /* KeywordCompare */, value: idStr, line: startLine, column: startColumn };
|
|
393
|
+
if (lowerBase === "binary") return { type: 72 /* KeywordBinary */, value: idStr, line: startLine, column: startColumn };
|
|
394
|
+
if (lowerBase === "text") return { type: 73 /* KeywordText */, value: idStr, line: startLine, column: startColumn };
|
|
395
|
+
if (lowerBase === "attribute") return { type: 74 /* KeywordAttribute */, value: idStr, line: startLine, column: startColumn };
|
|
396
|
+
if (lowerBase === "declare") return { type: 75 /* KeywordDeclare */, value: idStr, line: startLine, column: startColumn };
|
|
397
|
+
if (lowerBase === "lib") return { type: 76 /* KeywordLib */, value: idStr, line: startLine, column: startColumn };
|
|
398
|
+
if (lowerBase === "alias") return { type: 77 /* KeywordAlias */, value: idStr, line: startLine, column: startColumn };
|
|
399
|
+
if (lowerBase === "ptrsafe") return { type: 78 /* KeywordPtrSafe */, value: idStr, line: startLine, column: startColumn };
|
|
400
|
+
if (lowerBase === "open") return { type: 81 /* KeywordOpen */, value: idStr, line: startLine, column: startColumn };
|
|
401
|
+
if (lowerBase === "close") return { type: 82 /* KeywordClose */, value: idStr, line: startLine, column: startColumn };
|
|
402
|
+
if (lowerBase === "line") return { type: 83 /* KeywordLine */, value: idStr, line: startLine, column: startColumn };
|
|
403
|
+
if (lowerBase === "input") return { type: 84 /* KeywordInput */, value: idStr, line: startLine, column: startColumn };
|
|
404
|
+
if (lowerBase === "print") return { type: 85 /* KeywordPrint */, value: idStr, line: startLine, column: startColumn };
|
|
405
|
+
if (lowerBase === "put") return { type: 86 /* KeywordPut */, value: idStr, line: startLine, column: startColumn };
|
|
406
|
+
if (lowerBase === "output") return { type: 87 /* KeywordOutput */, value: idStr, line: startLine, column: startColumn };
|
|
407
|
+
if (lowerBase === "append") return { type: 88 /* KeywordAppend */, value: idStr, line: startLine, column: startColumn };
|
|
408
|
+
if (lowerBase === "random") return { type: 89 /* KeywordRandom */, value: idStr, line: startLine, column: startColumn };
|
|
409
|
+
if (lowerBase === "access") return { type: 90 /* KeywordAccess */, value: idStr, line: startLine, column: startColumn };
|
|
410
|
+
if (lowerBase === "read") return { type: 91 /* KeywordRead */, value: idStr, line: startLine, column: startColumn };
|
|
411
|
+
if (lowerBase === "write") return { type: 92 /* KeywordWrite */, value: idStr, line: startLine, column: startColumn };
|
|
412
|
+
if (lowerBase === "lock") return { type: 93 /* KeywordLock */, value: idStr, line: startLine, column: startColumn };
|
|
413
|
+
if (lowerBase === "shared") return { type: 94 /* KeywordShared */, value: idStr, line: startLine, column: startColumn };
|
|
414
|
+
if (lowerBase === "spc") return { type: 95 /* KeywordSpc */, value: idStr, line: startLine, column: startColumn };
|
|
415
|
+
if (lowerBase === "tab") return { type: 96 /* KeywordTab */, value: idStr, line: startLine, column: startColumn };
|
|
416
|
+
if (lowerBase === "kill") return { type: 97 /* KeywordKill */, value: idStr, line: startLine, column: startColumn };
|
|
417
|
+
if (lowerBase === "get") return { type: 20 /* KeywordGet */, value: idStr, line: startLine, column: startColumn };
|
|
418
|
+
if (lowerBase === "seek") return { type: 98 /* KeywordSeek */, value: idStr, line: startLine, column: startColumn };
|
|
419
|
+
if (lowerBase === "reset") return { type: 99 /* KeywordReset */, value: idStr, line: startLine, column: startColumn };
|
|
420
|
+
if (lowerBase === "unlock") return { type: 100 /* KeywordUnlock */, value: idStr, line: startLine, column: startColumn };
|
|
421
|
+
if (lowerBase === "paramarray") return { type: 101 /* KeywordParamArray */, value: idStr, line: startLine, column: startColumn };
|
|
422
|
+
if (lowerBase === "module") return { type: 80 /* KeywordModule */, value: idStr, line: startLine, column: startColumn };
|
|
423
|
+
if (lowerBase === "event") return { type: 102 /* KeywordEvent */, value: idStr, line: startLine, column: startColumn };
|
|
424
|
+
if (lowerBase === "raiseevent") return { type: 103 /* KeywordRaiseEvent */, value: idStr, line: startLine, column: startColumn };
|
|
425
|
+
if (lowerBase === "withevents") return { type: 104 /* KeywordWithEvents */, value: idStr, line: startLine, column: startColumn };
|
|
426
|
+
if (lowerBase === "implements") return { type: 105 /* KeywordImplements */, value: idStr, line: startLine, column: startColumn };
|
|
427
|
+
if (lowerBase === "appactivate") return { type: 106 /* KeywordAppActivate */, value: idStr, line: startLine, column: startColumn };
|
|
428
|
+
if (lowerBase === "sendkeys") return { type: 107 /* KeywordSendKeys */, value: idStr, line: startLine, column: startColumn };
|
|
429
|
+
if (lowerBase === "mid") return { type: 108 /* KeywordMid */, value: idStr, line: startLine, column: startColumn };
|
|
430
|
+
if (lowerBase === "midb") return { type: 108 /* KeywordMid */, value: idStr, line: startLine, column: startColumn };
|
|
431
|
+
if (lowerBase === "width") return { type: 109 /* KeywordWidth */, value: idStr, line: startLine, column: startColumn };
|
|
432
|
+
if (lowerBase === "addressof") return { type: 110 /* KeywordAddressOf */, value: idStr, line: startLine, column: startColumn };
|
|
433
|
+
return { type: 0 /* Identifier */, value: idStr, line: startLine, column: startColumn };
|
|
434
|
+
}
|
|
435
|
+
if (char === "*") {
|
|
436
|
+
this.advance();
|
|
437
|
+
return { type: 114 /* OperatorMultiply */, value: "*", line: startLine, column: startColumn };
|
|
438
|
+
}
|
|
439
|
+
if (char === "/") {
|
|
440
|
+
this.advance();
|
|
441
|
+
return { type: 115 /* OperatorDivide */, value: "/", line: startLine, column: startColumn };
|
|
442
|
+
}
|
|
443
|
+
if (char === "\\") {
|
|
444
|
+
this.advance();
|
|
445
|
+
return { type: 116 /* OperatorIntDivide */, value: "\\", line: startLine, column: startColumn };
|
|
446
|
+
}
|
|
447
|
+
if (char === "^") {
|
|
448
|
+
this.advance();
|
|
449
|
+
return { type: 119 /* OperatorPower */, value: "^", line: startLine, column: startColumn };
|
|
450
|
+
}
|
|
451
|
+
const unknownChar = this.advance();
|
|
452
|
+
return { type: 137 /* Unknown */, value: unknownChar, line: startLine, column: startColumn };
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
tokenize() {
|
|
456
|
+
const tokens = [];
|
|
457
|
+
let token;
|
|
458
|
+
do {
|
|
459
|
+
token = this.getNextToken();
|
|
460
|
+
tokens.push(token);
|
|
461
|
+
} while (token.type !== 136 /* EOF */);
|
|
462
|
+
return tokens;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// ../../src/engine/parser.ts
|
|
467
|
+
var ParseError = class extends Error {
|
|
468
|
+
constructor(message, line, column) {
|
|
469
|
+
super(message);
|
|
470
|
+
this.line = line;
|
|
471
|
+
this.column = column;
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
var Parser = class _Parser {
|
|
475
|
+
tokens;
|
|
476
|
+
pos = 0;
|
|
477
|
+
parseAsClass;
|
|
478
|
+
_diagnostics = [];
|
|
479
|
+
// ---------------------------------------------------------------
|
|
480
|
+
// §3.3.5.2 contextual keyword Sets
|
|
481
|
+
//
|
|
482
|
+
// IDENTIFIER = <any lex-identifier that is not a reserved-identifier>
|
|
483
|
+
// The following keywords are NOT listed in any <reserved-identifier>
|
|
484
|
+
// category and are therefore valid IDENTIFIERs outside the specific
|
|
485
|
+
// syntactic positions where they carry dedicated meaning.
|
|
486
|
+
// ---------------------------------------------------------------
|
|
487
|
+
/** file-mode specifiers: Open...For <mode> (§5.4.5.1 open-stmt) */
|
|
488
|
+
static CONTEXTUAL_KW_FILE_MODE = /* @__PURE__ */ new Set([
|
|
489
|
+
87 /* KeywordOutput */,
|
|
490
|
+
88 /* KeywordAppend */,
|
|
491
|
+
89 /* KeywordRandom */,
|
|
492
|
+
72 /* KeywordBinary */
|
|
493
|
+
// also: Option Compare Binary
|
|
494
|
+
]);
|
|
495
|
+
/** file-access specifiers: Open...Access <access> (§5.4.5.1 open-stmt) */
|
|
496
|
+
static CONTEXTUAL_KW_FILE_ACCESS = /* @__PURE__ */ new Set([
|
|
497
|
+
90 /* KeywordAccess */,
|
|
498
|
+
91 /* KeywordRead */
|
|
499
|
+
]);
|
|
500
|
+
/** option-statement modifiers: Option Compare/Base/Explicit/Module (§5.2.2) */
|
|
501
|
+
static CONTEXTUAL_KW_OPTION = /* @__PURE__ */ new Set([
|
|
502
|
+
73 /* KeywordText */,
|
|
503
|
+
// Option Compare Text
|
|
504
|
+
71 /* KeywordCompare */,
|
|
505
|
+
// Option Compare ...
|
|
506
|
+
30 /* KeywordExplicit */,
|
|
507
|
+
// Option Explicit
|
|
508
|
+
79 /* KeywordBase */,
|
|
509
|
+
// Option Base 0|1
|
|
510
|
+
80 /* KeywordModule */
|
|
511
|
+
// Option Private Module
|
|
512
|
+
]);
|
|
513
|
+
/** declare-statement modifiers: Declare [PtrSafe]...Lib...Alias (§5.2.3.1) */
|
|
514
|
+
static CONTEXTUAL_KW_DECLARE = /* @__PURE__ */ new Set([
|
|
515
|
+
76 /* KeywordLib */,
|
|
516
|
+
77 /* KeywordAlias */,
|
|
517
|
+
78 /* KeywordPtrSafe */
|
|
518
|
+
]);
|
|
519
|
+
/** standalone statements absent from <statement-keyword> (§3.3.5.2) */
|
|
520
|
+
static CONTEXTUAL_KW_STMT_ABSENT = /* @__PURE__ */ new Set([
|
|
521
|
+
43 /* KeywordStep */,
|
|
522
|
+
// For...Step (not in <marker-keyword>)
|
|
523
|
+
108 /* KeywordMid */,
|
|
524
|
+
// Mid-stmt mode-specifier (§5.4.3.8); also Mid/Mid$/MidB function
|
|
525
|
+
97 /* KeywordKill */,
|
|
526
|
+
// Kill <pathname>
|
|
527
|
+
109 /* KeywordWidth */,
|
|
528
|
+
// Width #<filenumber>, <width>
|
|
529
|
+
83 /* KeywordLine */,
|
|
530
|
+
// Line Input #<filenumber>, <variable>
|
|
531
|
+
99 /* KeywordReset */,
|
|
532
|
+
// Reset
|
|
533
|
+
106 /* KeywordAppActivate */,
|
|
534
|
+
// AppActivate <title>
|
|
535
|
+
107 /* KeywordSendKeys */
|
|
536
|
+
// SendKeys <keys>
|
|
537
|
+
]);
|
|
538
|
+
/** Structural keywords that appear in dedicated lexer token types but are NOT
|
|
539
|
+
* listed in any §3.3.5.2 reserved-identifier category.
|
|
540
|
+
* They are valid IDENTIFIERs in declaration and expression contexts.
|
|
541
|
+
* Note: statement-level dispatch (e.g. Class → parseClassDeclaration) still has
|
|
542
|
+
* priority; assignment `Class = x` is disambiguated by a `!= OperatorEquals` guard. */
|
|
543
|
+
static CONTEXTUAL_KW_STRUCTURAL = /* @__PURE__ */ new Set([
|
|
544
|
+
69 /* KeywordClass */,
|
|
545
|
+
// Class declaration keyword (not in spec's statement-keyword list)
|
|
546
|
+
25 /* KeywordCollection */,
|
|
547
|
+
// Built-in object type name (not in reserved-identifier list)
|
|
548
|
+
35 /* KeywordError */,
|
|
549
|
+
// On Error construct element (not standalone reserved)
|
|
550
|
+
19 /* KeywordProperty */
|
|
551
|
+
// Property Get/Set/Let keyword (not in spec's statement-keyword list)
|
|
552
|
+
]);
|
|
553
|
+
/** Union of all contextual keyword groups above.
|
|
554
|
+
* Tokens in this Set are valid IDENTIFIERs in Dim declarations,
|
|
555
|
+
* expression context, and assignment statements. */
|
|
556
|
+
static CONTEXTUAL_KW = /* @__PURE__ */ new Set([
|
|
557
|
+
..._Parser.CONTEXTUAL_KW_FILE_MODE,
|
|
558
|
+
..._Parser.CONTEXTUAL_KW_FILE_ACCESS,
|
|
559
|
+
..._Parser.CONTEXTUAL_KW_OPTION,
|
|
560
|
+
..._Parser.CONTEXTUAL_KW_DECLARE,
|
|
561
|
+
..._Parser.CONTEXTUAL_KW_STMT_ABSENT,
|
|
562
|
+
..._Parser.CONTEXTUAL_KW_STRUCTURAL
|
|
563
|
+
]);
|
|
564
|
+
/** <statement-keyword> tokens additionally permitted as IDENTIFIERs in
|
|
565
|
+
* expression context (parsePrimary) for practical VBA compatibility —
|
|
566
|
+
* e.g. as method names (obj.Print, ws.Get) or file I/O targets.
|
|
567
|
+
* These remain <reserved-identifier> per §3.3.5.2. */
|
|
568
|
+
static COMPAT_KW_EXPR = /* @__PURE__ */ new Set([
|
|
569
|
+
98 /* KeywordSeek */,
|
|
570
|
+
// <statement-keyword>
|
|
571
|
+
84 /* KeywordInput */,
|
|
572
|
+
// <statement-keyword> / <special-form>
|
|
573
|
+
85 /* KeywordPrint */,
|
|
574
|
+
// <statement-keyword>
|
|
575
|
+
86 /* KeywordPut */,
|
|
576
|
+
// <statement-keyword>
|
|
577
|
+
20 /* KeywordGet */,
|
|
578
|
+
// <statement-keyword>
|
|
579
|
+
93 /* KeywordLock */,
|
|
580
|
+
// <statement-keyword>
|
|
581
|
+
100 /* KeywordUnlock */
|
|
582
|
+
// <statement-keyword>
|
|
583
|
+
]);
|
|
584
|
+
errorRecovery;
|
|
585
|
+
/** Returns true if token is a valid IDENTIFIER per §3.3.5.2:
|
|
586
|
+
* either a plain lex-identifier or a contextual keyword that is not reserved. */
|
|
587
|
+
isIdentifier(token) {
|
|
588
|
+
return token.type === 0 /* Identifier */ || token.type === 138 /* ForeignName */ || _Parser.CONTEXTUAL_KW.has(token.type);
|
|
589
|
+
}
|
|
590
|
+
constructor(tokens, options = {}) {
|
|
591
|
+
this.tokens = tokens;
|
|
592
|
+
this.parseAsClass = options.parseAsClass;
|
|
593
|
+
this.errorRecovery = options.errorRecovery ?? false;
|
|
594
|
+
}
|
|
595
|
+
// Keywords can appear as property/class names in VBA (e.g. obj.Property, New Collection)
|
|
596
|
+
isNameToken(token) {
|
|
597
|
+
return token.type === 0 /* Identifier */ || token.type === 138 /* ForeignName */ || token.type >= 3 /* KeywordFor */ && token.type <= 110 /* KeywordAddressOf */;
|
|
598
|
+
}
|
|
599
|
+
recordError(message, token) {
|
|
600
|
+
const pos = { line: token.line, column: token.column };
|
|
601
|
+
this._diagnostics.push({ message, loc: { start: pos, end: pos }, severity: "error" });
|
|
602
|
+
}
|
|
603
|
+
throwMissingRParen() {
|
|
604
|
+
const peek = this.peek();
|
|
605
|
+
if (peek.type === 135 /* Newline */) {
|
|
606
|
+
const prevToken = this.tokens[Math.max(0, this.pos - 1)];
|
|
607
|
+
if (prevToken && this.isContinuationEndToken(prevToken.type)) {
|
|
608
|
+
this.throwError(
|
|
609
|
+
`\u884C\u7D99\u7D9A\u6587\u5B57 '_' \u304C\u5FC5\u8981\u3067\u3059\uFF08'${prevToken.value}' \u306E\u5F8C\u3067\u6539\u884C\u3055\u308C\u3066\u3044\u307E\u3059\uFF09`,
|
|
610
|
+
peek
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
this.throwError(`Parse error: Expected ')' at line ${peek.line} `);
|
|
615
|
+
}
|
|
616
|
+
isContinuationEndToken(type) {
|
|
617
|
+
return type === 112 /* OperatorPlus */ || type === 113 /* OperatorMinus */ || type === 114 /* OperatorMultiply */ || type === 115 /* OperatorDivide */ || type === 116 /* OperatorIntDivide */ || type === 119 /* OperatorPower */ || type === 117 /* OperatorAmpersand */ || type === 120 /* OperatorEquals */ || type === 121 /* OperatorNotEquals */ || type === 122 /* OperatorLessThan */ || type === 123 /* OperatorGreaterThan */ || type === 124 /* OperatorLessThanOrEqual */ || type === 125 /* OperatorGreaterThanOrEqual */ || type === 126 /* OperatorComma */ || type === 127 /* OperatorLParen */ || type === 26 /* KeywordAnd */ || type === 27 /* KeywordOr */ || type === 64 /* KeywordXor */ || type === 65 /* KeywordEqv */ || type === 66 /* KeywordImp */ || type === 118 /* KeywordMod */ || type === 63 /* KeywordLike */ || type === 51 /* KeywordIs */ || type === 28 /* KeywordNot */;
|
|
618
|
+
}
|
|
619
|
+
tokenDisplay(value) {
|
|
620
|
+
return value.replace(/\n/g, "<\u6539\u884C>").replace(/\r/g, "<CR>").replace(/\t/g, "<\u30BF\u30D6>");
|
|
621
|
+
}
|
|
622
|
+
throwError(message, token) {
|
|
623
|
+
const peek = this.peek();
|
|
624
|
+
const t = token ?? (peek.type !== 136 /* EOF */ ? peek : this.tokens[Math.max(0, this.pos - 1)]);
|
|
625
|
+
throw new ParseError(message, t.line, t.column);
|
|
626
|
+
}
|
|
627
|
+
syncToNextTopLevelStatement() {
|
|
628
|
+
while (this.peek().type !== 136 /* EOF */ && this.peek().type !== 135 /* Newline */) {
|
|
629
|
+
this.advance();
|
|
630
|
+
}
|
|
631
|
+
while (this.isAtEndTerminator()) {
|
|
632
|
+
this.advance();
|
|
633
|
+
this.advance();
|
|
634
|
+
while (this.peek().type !== 136 /* EOF */ && this.peek().type !== 135 /* Newline */) {
|
|
635
|
+
this.advance();
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
peek(offset = 0) {
|
|
640
|
+
if (this.pos + offset >= this.tokens.length) {
|
|
641
|
+
return this.tokens[this.tokens.length - 1];
|
|
642
|
+
}
|
|
643
|
+
return this.tokens[this.pos + offset];
|
|
644
|
+
}
|
|
645
|
+
advance() {
|
|
646
|
+
const token = this.peek();
|
|
647
|
+
this.pos++;
|
|
648
|
+
return token;
|
|
649
|
+
}
|
|
650
|
+
parseOptionCompareStatement() {
|
|
651
|
+
const modeToken = this.advance();
|
|
652
|
+
let mode;
|
|
653
|
+
if (modeToken.type === 72 /* KeywordBinary */) {
|
|
654
|
+
mode = "Binary";
|
|
655
|
+
} else if (modeToken.type === 73 /* KeywordText */) {
|
|
656
|
+
mode = "Text";
|
|
657
|
+
} else {
|
|
658
|
+
this.throwError(`Parse error: Expected 'Binary' or 'Text' after 'Option Compare' at line ${modeToken.line}`);
|
|
659
|
+
}
|
|
660
|
+
return { type: "OptionCompareStatement", mode };
|
|
661
|
+
}
|
|
662
|
+
parseParameter() {
|
|
663
|
+
let isByVal = false;
|
|
664
|
+
let isOptional = false;
|
|
665
|
+
if (this.match(50 /* KeywordOptional */)) {
|
|
666
|
+
isOptional = true;
|
|
667
|
+
}
|
|
668
|
+
let isParamArray = false;
|
|
669
|
+
if (this.match(101 /* KeywordParamArray */)) {
|
|
670
|
+
isParamArray = true;
|
|
671
|
+
}
|
|
672
|
+
let hasPassingModifier = false;
|
|
673
|
+
if (this.peek().type === 47 /* KeywordByVal */ || this.peek().type === 46 /* KeywordByRef */) {
|
|
674
|
+
hasPassingModifier = true;
|
|
675
|
+
isByVal = this.advance().type === 47 /* KeywordByVal */;
|
|
676
|
+
}
|
|
677
|
+
const token = this.peek();
|
|
678
|
+
if (token.type !== 0 /* Identifier */ && (token.type < 79 /* KeywordBase */ || token.type > 110 /* KeywordAddressOf */)) {
|
|
679
|
+
this.throwError(`Parse error at line ${token.line}: Expected parameter name (Found ${this.tokenDisplay(token.value)})`);
|
|
680
|
+
}
|
|
681
|
+
const nameToken = this.advance();
|
|
682
|
+
let isArray = false;
|
|
683
|
+
if (this.match(127 /* OperatorLParen */)) {
|
|
684
|
+
this.consume(128 /* OperatorRParen */, "Expected ')' after parameter name");
|
|
685
|
+
isArray = true;
|
|
686
|
+
}
|
|
687
|
+
let paramType;
|
|
688
|
+
if (this.match(23 /* KeywordAs */)) {
|
|
689
|
+
paramType = this.advance().value;
|
|
690
|
+
if (this.peek().type === 129 /* OperatorDot */) {
|
|
691
|
+
this.advance();
|
|
692
|
+
paramType += "." + this.advance().value;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
let defaultValue;
|
|
696
|
+
if (this.match(120 /* OperatorEquals */)) {
|
|
697
|
+
defaultValue = this.parseExpression();
|
|
698
|
+
}
|
|
699
|
+
return {
|
|
700
|
+
type: "Parameter",
|
|
701
|
+
name: nameToken.value,
|
|
702
|
+
isByVal,
|
|
703
|
+
hasPassingModifier,
|
|
704
|
+
isOptional,
|
|
705
|
+
isParamArray,
|
|
706
|
+
isArray,
|
|
707
|
+
paramType,
|
|
708
|
+
defaultValue,
|
|
709
|
+
loc: { start: { line: nameToken.line, column: nameToken.column }, end: { line: nameToken.line, column: nameToken.column + nameToken.value.length } }
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
parseAttributeStatement() {
|
|
713
|
+
this.advance();
|
|
714
|
+
const name = this.advance().value;
|
|
715
|
+
this.consume(120 /* OperatorEquals */, "Expected '=' after Attribute name");
|
|
716
|
+
const value = this.parseExpression();
|
|
717
|
+
return { type: "AttributeStatement", name, value };
|
|
718
|
+
}
|
|
719
|
+
parseOpenStatement() {
|
|
720
|
+
this.advance();
|
|
721
|
+
const path2 = this.parseExpression();
|
|
722
|
+
this.consume(3 /* KeywordFor */, "Expected 'For' in Open statement");
|
|
723
|
+
let mode = "Random";
|
|
724
|
+
const modeToken = this.advance();
|
|
725
|
+
switch (modeToken.type) {
|
|
726
|
+
case 84 /* KeywordInput */:
|
|
727
|
+
mode = "Input";
|
|
728
|
+
break;
|
|
729
|
+
case 87 /* KeywordOutput */:
|
|
730
|
+
mode = "Output";
|
|
731
|
+
break;
|
|
732
|
+
case 88 /* KeywordAppend */:
|
|
733
|
+
mode = "Append";
|
|
734
|
+
break;
|
|
735
|
+
case 89 /* KeywordRandom */:
|
|
736
|
+
mode = "Random";
|
|
737
|
+
break;
|
|
738
|
+
case 72 /* KeywordBinary */:
|
|
739
|
+
mode = "Binary";
|
|
740
|
+
break;
|
|
741
|
+
default:
|
|
742
|
+
this.throwError(`Parse error: Invalid Open mode '${modeToken.value}' at line ${modeToken.line}`);
|
|
743
|
+
}
|
|
744
|
+
let access = void 0;
|
|
745
|
+
if (this.match(90 /* KeywordAccess */)) {
|
|
746
|
+
const first = this.advance();
|
|
747
|
+
if (first.type === 91 /* KeywordRead */) {
|
|
748
|
+
if (this.match(92 /* KeywordWrite */)) {
|
|
749
|
+
access = "Read Write";
|
|
750
|
+
} else {
|
|
751
|
+
access = "Read";
|
|
752
|
+
}
|
|
753
|
+
} else if (first.type === 92 /* KeywordWrite */) {
|
|
754
|
+
access = "Write";
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
let lock = void 0;
|
|
758
|
+
if (this.match(93 /* KeywordLock */)) {
|
|
759
|
+
const first = this.advance();
|
|
760
|
+
if (first.type === 94 /* KeywordShared */) {
|
|
761
|
+
lock = "Shared";
|
|
762
|
+
} else if (first.type === 91 /* KeywordRead */) {
|
|
763
|
+
if (this.match(92 /* KeywordWrite */)) {
|
|
764
|
+
lock = "Lock Read Write";
|
|
765
|
+
} else {
|
|
766
|
+
lock = "Lock Read";
|
|
767
|
+
}
|
|
768
|
+
} else if (first.type === 92 /* KeywordWrite */) {
|
|
769
|
+
lock = "Lock Write";
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
this.consume(23 /* KeywordAs */, "Expected 'As' in Open statement");
|
|
773
|
+
this.match(111 /* OperatorHash */);
|
|
774
|
+
const fileNumber = this.parseExpression();
|
|
775
|
+
return { type: "OpenStatement", path: path2, mode, access, lock, fileNumber };
|
|
776
|
+
}
|
|
777
|
+
parseCloseStatement() {
|
|
778
|
+
this.advance();
|
|
779
|
+
const fileNumbers = [];
|
|
780
|
+
while (!this.isAtTerminator()) {
|
|
781
|
+
this.match(111 /* OperatorHash */);
|
|
782
|
+
fileNumbers.push(this.parseExpression());
|
|
783
|
+
if (!this.match(126 /* OperatorComma */)) break;
|
|
784
|
+
}
|
|
785
|
+
return { type: "CloseStatement", fileNumbers };
|
|
786
|
+
}
|
|
787
|
+
parsePrintStatement() {
|
|
788
|
+
this.advance();
|
|
789
|
+
this.consume(111 /* OperatorHash */, "Expected '#' in Print statement");
|
|
790
|
+
const fileNumber = this.parseExpression();
|
|
791
|
+
this.consume(126 /* OperatorComma */, "Expected ',' after file number in Print statement");
|
|
792
|
+
const expressions = [];
|
|
793
|
+
while (!this.isAtTerminator()) {
|
|
794
|
+
if (this.match(95 /* KeywordSpc */)) {
|
|
795
|
+
this.consume(127 /* OperatorLParen */, "Expected '(' after Spc");
|
|
796
|
+
expressions.push({ type: "Spc", val: this.parseExpression() });
|
|
797
|
+
this.consume(128 /* OperatorRParen */, "Expected ')' after Spc");
|
|
798
|
+
} else if (this.match(96 /* KeywordTab */)) {
|
|
799
|
+
if (this.match(127 /* OperatorLParen */)) {
|
|
800
|
+
expressions.push({ type: "Tab", val: this.parseExpression() });
|
|
801
|
+
this.consume(128 /* OperatorRParen */, "Expected ')' after Tab");
|
|
802
|
+
} else {
|
|
803
|
+
expressions.push("Tab");
|
|
804
|
+
}
|
|
805
|
+
} else if (this.peek().type === 126 /* OperatorComma */) {
|
|
806
|
+
this.advance();
|
|
807
|
+
expressions.push("Comma");
|
|
808
|
+
} else if (this.peek().type === 133 /* OperatorSemicolon */) {
|
|
809
|
+
this.advance();
|
|
810
|
+
expressions.push("Semicolon");
|
|
811
|
+
} else {
|
|
812
|
+
expressions.push(this.parseExpression());
|
|
813
|
+
}
|
|
814
|
+
if (this.isAtTerminator()) break;
|
|
815
|
+
}
|
|
816
|
+
return { type: "PrintStatement", fileNumber, expressions };
|
|
817
|
+
}
|
|
818
|
+
parseLineInputStatement() {
|
|
819
|
+
this.advance();
|
|
820
|
+
this.consume(84 /* KeywordInput */, "Expected 'Input' after 'Line'");
|
|
821
|
+
this.consume(111 /* OperatorHash */, "Expected '#' in Line Input statement");
|
|
822
|
+
const fileNumber = this.parseExpression();
|
|
823
|
+
this.consume(126 /* OperatorComma */, "Expected ',' after file number");
|
|
824
|
+
const variable = this.parsePrimary();
|
|
825
|
+
if (variable.type !== "Identifier") {
|
|
826
|
+
this.throwError(`Parse error: Expected variable name in Line Input at line ${this.peek().line}`);
|
|
827
|
+
}
|
|
828
|
+
return { type: "LineInputStatement", fileNumber, variable };
|
|
829
|
+
}
|
|
830
|
+
parsePutStatement() {
|
|
831
|
+
this.advance();
|
|
832
|
+
this.consume(111 /* OperatorHash */, "Expected '#' in Put statement");
|
|
833
|
+
const fileNumber = this.parseExpression();
|
|
834
|
+
this.consume(126 /* OperatorComma */, "Expected ',' after file number");
|
|
835
|
+
let recordNumber = void 0;
|
|
836
|
+
if (this.peek().type !== 126 /* OperatorComma */) {
|
|
837
|
+
recordNumber = this.parseExpression();
|
|
838
|
+
}
|
|
839
|
+
this.consume(126 /* OperatorComma */, "Expected second ',' in Put statement");
|
|
840
|
+
const data = this.parseExpression();
|
|
841
|
+
return { type: "PutStatement", fileNumber, recordNumber, data };
|
|
842
|
+
}
|
|
843
|
+
parseKillStatement() {
|
|
844
|
+
this.advance();
|
|
845
|
+
const path2 = this.parseExpression();
|
|
846
|
+
return { type: "KillStatement", path: path2 };
|
|
847
|
+
}
|
|
848
|
+
parseWriteStatement() {
|
|
849
|
+
this.advance();
|
|
850
|
+
this.consume(111 /* OperatorHash */, "Expected '#' in Write statement");
|
|
851
|
+
const fileNumber = this.parseExpression();
|
|
852
|
+
const items = [];
|
|
853
|
+
if (this.match(126 /* OperatorComma */)) {
|
|
854
|
+
while (!this.isAtTerminator()) {
|
|
855
|
+
items.push(this.parseExpression());
|
|
856
|
+
if (!this.match(126 /* OperatorComma */)) break;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return { type: "WriteStatement", fileNumber, items };
|
|
860
|
+
}
|
|
861
|
+
parseInputStatement() {
|
|
862
|
+
this.advance();
|
|
863
|
+
this.consume(111 /* OperatorHash */, "Expected '#' in Input statement");
|
|
864
|
+
const fileNumber = this.parseExpression();
|
|
865
|
+
this.consume(126 /* OperatorComma */, "Expected ',' in Input statement");
|
|
866
|
+
const variables = [];
|
|
867
|
+
while (!this.isAtTerminator()) {
|
|
868
|
+
variables.push(this.parseExpression());
|
|
869
|
+
if (!this.match(126 /* OperatorComma */)) break;
|
|
870
|
+
}
|
|
871
|
+
return { type: "InputStatement", fileNumber, variables };
|
|
872
|
+
}
|
|
873
|
+
parseGetStatement() {
|
|
874
|
+
this.advance();
|
|
875
|
+
this.consume(111 /* OperatorHash */, "Expected '#' in Get statement");
|
|
876
|
+
const fileNumber = this.parseExpression();
|
|
877
|
+
this.consume(126 /* OperatorComma */, "Expected ',' in Get statement");
|
|
878
|
+
let recordNumber = void 0;
|
|
879
|
+
if (this.peek().type !== 126 /* OperatorComma */) {
|
|
880
|
+
recordNumber = this.parseExpression();
|
|
881
|
+
}
|
|
882
|
+
this.consume(126 /* OperatorComma */, "Expected ',' in Get statement");
|
|
883
|
+
const variable = this.parseExpression();
|
|
884
|
+
return { type: "GetStatement", fileNumber, recordNumber, variable };
|
|
885
|
+
}
|
|
886
|
+
parseSeekStatement() {
|
|
887
|
+
this.advance();
|
|
888
|
+
this.consume(111 /* OperatorHash */, "Expected '#' in Seek statement");
|
|
889
|
+
const fileNumber = this.parseExpression();
|
|
890
|
+
this.consume(126 /* OperatorComma */, "Expected ',' in Seek statement");
|
|
891
|
+
const position = this.parseExpression();
|
|
892
|
+
return { type: "SeekStatement", fileNumber, position };
|
|
893
|
+
}
|
|
894
|
+
parseResetStatement() {
|
|
895
|
+
this.advance();
|
|
896
|
+
return { type: "ResetStatement" };
|
|
897
|
+
}
|
|
898
|
+
parseDeclareStatement() {
|
|
899
|
+
this.advance();
|
|
900
|
+
let isPtrSafe = false;
|
|
901
|
+
if (this.peek().type === 78 /* KeywordPtrSafe */) {
|
|
902
|
+
this.advance();
|
|
903
|
+
isPtrSafe = true;
|
|
904
|
+
}
|
|
905
|
+
let isSub = false;
|
|
906
|
+
if (this.peek().type === 17 /* KeywordSub */) {
|
|
907
|
+
this.advance();
|
|
908
|
+
isSub = true;
|
|
909
|
+
} else if (this.peek().type === 18 /* KeywordFunction */) {
|
|
910
|
+
this.advance();
|
|
911
|
+
isSub = false;
|
|
912
|
+
} else {
|
|
913
|
+
this.throwError(`Parser error: Expected Sub or Function after Declare at line ${this.peek().line}`);
|
|
914
|
+
}
|
|
915
|
+
const name = this.advance().value;
|
|
916
|
+
if (this.peek().type !== 76 /* KeywordLib */) {
|
|
917
|
+
this.throwError(`Parser error: Expected Lib after Declare name at line ${this.peek().line}`);
|
|
918
|
+
}
|
|
919
|
+
this.advance();
|
|
920
|
+
const libName = this.advance().value.replace(/^"|"$/g, "");
|
|
921
|
+
let aliasName;
|
|
922
|
+
if (this.peek().type === 77 /* KeywordAlias */) {
|
|
923
|
+
this.advance();
|
|
924
|
+
aliasName = this.advance().value.replace(/^"|"$/g, "");
|
|
925
|
+
}
|
|
926
|
+
let parameters = [];
|
|
927
|
+
if (this.peek().type === 127 /* OperatorLParen */) {
|
|
928
|
+
this.advance();
|
|
929
|
+
while (this.peek().type !== 128 /* OperatorRParen */ && this.peek().type !== 136 /* EOF */) {
|
|
930
|
+
parameters.push(this.parseParameter());
|
|
931
|
+
if (this.peek().type === 126 /* OperatorComma */) {
|
|
932
|
+
this.advance();
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
this.consume(128 /* OperatorRParen */, "Expected ')' after declare parameters");
|
|
936
|
+
}
|
|
937
|
+
let returnType;
|
|
938
|
+
if (!isSub) {
|
|
939
|
+
if (this.peek().type === 23 /* KeywordAs */) {
|
|
940
|
+
this.advance();
|
|
941
|
+
returnType = this.advance().value;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
type: "DeclareStatement",
|
|
946
|
+
isPtrSafe,
|
|
947
|
+
isSub,
|
|
948
|
+
name,
|
|
949
|
+
libName,
|
|
950
|
+
aliasName,
|
|
951
|
+
parameters,
|
|
952
|
+
returnType
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
isAtEndTerminator() {
|
|
956
|
+
const token = this.peek();
|
|
957
|
+
if (token.type !== 10 /* KeywordEnd */) return false;
|
|
958
|
+
const next = this.peek(1);
|
|
959
|
+
return next.type === 17 /* KeywordSub */ || next.type === 18 /* KeywordFunction */ || next.type === 6 /* KeywordIf */ || next.type === 53 /* KeywordSelect */ || next.type === 61 /* KeywordWith */ || next.type === 48 /* KeywordType */ || next.type === 59 /* KeywordEnum */ || next.type === 19 /* KeywordProperty */ || next.type === 69 /* KeywordClass */;
|
|
960
|
+
}
|
|
961
|
+
match(expectedType) {
|
|
962
|
+
if (this.peek().type === expectedType) {
|
|
963
|
+
this.advance();
|
|
964
|
+
return true;
|
|
965
|
+
}
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
// Returns true for operators that can ONLY appear in a binary position (never as unary or
|
|
969
|
+
// argument starters). When one of these follows a greedily-parsed CallExpression in a
|
|
970
|
+
// statement context, it signals that the `(args)` was actually part of the argument expression.
|
|
971
|
+
isBinaryOnlyOperator(type) {
|
|
972
|
+
return type === 114 /* OperatorMultiply */ || type === 115 /* OperatorDivide */ || type === 116 /* OperatorIntDivide */ || type === 119 /* OperatorPower */ || type === 112 /* OperatorPlus */ || type === 113 /* OperatorMinus */ || type === 117 /* OperatorAmpersand */ || type === 121 /* OperatorNotEquals */ || type === 122 /* OperatorLessThan */ || type === 123 /* OperatorGreaterThan */ || type === 124 /* OperatorLessThanOrEqual */ || type === 125 /* OperatorGreaterThanOrEqual */ || type === 118 /* KeywordMod */;
|
|
973
|
+
}
|
|
974
|
+
// Returns true if there is whitespace between the previous token and the current token (same line).
|
|
975
|
+
// Used to distinguish `Foo(arg)` (no space = function-call postfix) from
|
|
976
|
+
// `Foo (arg)*x` (space = argument expression; `(arg)*x` must be parsed as one expression).
|
|
977
|
+
hasSpaceBeforeCurrentToken() {
|
|
978
|
+
if (this.pos <= 0) return false;
|
|
979
|
+
const cur = this.tokens[this.pos];
|
|
980
|
+
const prev = this.tokens[this.pos - 1];
|
|
981
|
+
if (prev.line !== cur.line) return false;
|
|
982
|
+
return cur.column > prev.column + prev.value.length;
|
|
983
|
+
}
|
|
984
|
+
isAtTerminator() {
|
|
985
|
+
const type = this.peek().type;
|
|
986
|
+
return type === 135 /* Newline */ || type === 136 /* EOF */ || type === 130 /* OperatorColon */;
|
|
987
|
+
}
|
|
988
|
+
consume(expectedType, message) {
|
|
989
|
+
if (this.peek().type === expectedType) {
|
|
990
|
+
return this.advance();
|
|
991
|
+
}
|
|
992
|
+
this.throwError(`Parse error at line ${this.peek().line}: ${message}`);
|
|
993
|
+
}
|
|
994
|
+
skipNewlines() {
|
|
995
|
+
while (this.match(135 /* Newline */)) {
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
// Build a SourceLocation spanning from start token to end token (inclusive).
|
|
999
|
+
exprLoc(start, end) {
|
|
1000
|
+
return {
|
|
1001
|
+
start: { line: start.line, column: start.column },
|
|
1002
|
+
end: { line: end.line, column: end.column + end.value.length }
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
// Returns true if the token can serve as a name (identifier or any keyword).
|
|
1006
|
+
// VBA §5.2.3.3 allows reserved words as UDT/Enum member names (reserved-name-member-dcl).
|
|
1007
|
+
// MS-VBAL §3.3.5: name-start は Unicode-Letter (Lo を含む) も許容するため Unicode フラグを使う。
|
|
1008
|
+
isWordToken(t) {
|
|
1009
|
+
return /^[\p{L}\p{Nl}_][\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}_]*$/u.test(t.value);
|
|
1010
|
+
}
|
|
1011
|
+
// Create an Identifier node with location from the given token.
|
|
1012
|
+
makeIdentifier(token) {
|
|
1013
|
+
return {
|
|
1014
|
+
type: "Identifier",
|
|
1015
|
+
name: token.value,
|
|
1016
|
+
loc: {
|
|
1017
|
+
start: { line: token.line, column: token.column },
|
|
1018
|
+
end: { line: token.line, column: token.column + token.value.length }
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
parse() {
|
|
1023
|
+
if (this.parseAsClass) {
|
|
1024
|
+
const classDecl = this.parseClassBody(this.parseAsClass, false);
|
|
1025
|
+
return { type: "Program", body: [classDecl], diagnostics: this._diagnostics };
|
|
1026
|
+
}
|
|
1027
|
+
const program = {
|
|
1028
|
+
type: "Program",
|
|
1029
|
+
body: [],
|
|
1030
|
+
diagnostics: this._diagnostics
|
|
1031
|
+
};
|
|
1032
|
+
this.skipNewlines();
|
|
1033
|
+
while (this.peek().type !== 136 /* EOF */) {
|
|
1034
|
+
const startPos = this.pos;
|
|
1035
|
+
const startToken = this.peek();
|
|
1036
|
+
try {
|
|
1037
|
+
const stmt = this.parseStatement();
|
|
1038
|
+
if (stmt) {
|
|
1039
|
+
program.body.push(stmt);
|
|
1040
|
+
}
|
|
1041
|
+
} catch (e) {
|
|
1042
|
+
if (!this.errorRecovery) throw e;
|
|
1043
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1044
|
+
if (e instanceof ParseError) {
|
|
1045
|
+
const pos = { line: e.line, column: e.column };
|
|
1046
|
+
this._diagnostics.push({ message: msg, loc: { start: pos, end: pos }, severity: "error" });
|
|
1047
|
+
} else {
|
|
1048
|
+
this.recordError(msg, startToken);
|
|
1049
|
+
}
|
|
1050
|
+
this.syncToNextTopLevelStatement();
|
|
1051
|
+
}
|
|
1052
|
+
if (this.pos === startPos && this.peek().type !== 136 /* EOF */) {
|
|
1053
|
+
if (this.isAtEndTerminator()) {
|
|
1054
|
+
this.advance();
|
|
1055
|
+
this.advance();
|
|
1056
|
+
} else {
|
|
1057
|
+
this.advance();
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
this.skipNewlines();
|
|
1061
|
+
}
|
|
1062
|
+
return program;
|
|
1063
|
+
}
|
|
1064
|
+
parseExpressionPublic() {
|
|
1065
|
+
this.skipNewlines();
|
|
1066
|
+
return this.parseExpression();
|
|
1067
|
+
}
|
|
1068
|
+
// §5.4.1: block-statement = statement EOS
|
|
1069
|
+
// checkEOS=false is used only for inline-If bodies where Else/EOF terminate instead of EOS.
|
|
1070
|
+
parseStatement(checkEOS = true) {
|
|
1071
|
+
this.skipNewlines();
|
|
1072
|
+
const startToken = this.peek();
|
|
1073
|
+
const stmt = this.parseStatementInner();
|
|
1074
|
+
if (stmt !== null) {
|
|
1075
|
+
const isLabel = stmt.type === "LabelStatement";
|
|
1076
|
+
if (checkEOS && !isLabel && !this.isAtTerminator()) {
|
|
1077
|
+
this.throwError(`Parse error: unexpected token '${this.peek().value}' after statement at line ${this.peek().line}`);
|
|
1078
|
+
}
|
|
1079
|
+
const endToken = this.tokens[this.pos - 1];
|
|
1080
|
+
if (startToken.line !== void 0) {
|
|
1081
|
+
stmt.line = startToken.line;
|
|
1082
|
+
const startPos = { line: startToken.line, column: startToken.column };
|
|
1083
|
+
const endPos = endToken && endToken.line !== void 0 ? { line: endToken.line, column: endToken.column + endToken.value.length } : startPos;
|
|
1084
|
+
stmt.loc = { start: startPos, end: endPos };
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return stmt;
|
|
1088
|
+
}
|
|
1089
|
+
parseStatementInner() {
|
|
1090
|
+
const token = this.peek();
|
|
1091
|
+
if (token.type === 57 /* KeywordPublic */ || token.type === 58 /* KeywordPrivate */ || token.type === 60 /* KeywordFriend */) {
|
|
1092
|
+
const scope = this.advance().value.toLowerCase();
|
|
1093
|
+
const next = this.peek();
|
|
1094
|
+
if (next.type === 17 /* KeywordSub */ || next.type === 18 /* KeywordFunction */ || next.type === 19 /* KeywordProperty */) {
|
|
1095
|
+
return this.parseProcedureDeclaration(scope, false);
|
|
1096
|
+
}
|
|
1097
|
+
if (next.type === 31 /* KeywordConst */) {
|
|
1098
|
+
const stmt2 = this.parseConstDeclaration();
|
|
1099
|
+
stmt2.scope = scope;
|
|
1100
|
+
return stmt2;
|
|
1101
|
+
}
|
|
1102
|
+
if (next.type === 48 /* KeywordType */) {
|
|
1103
|
+
const stmt2 = this.parseTypeDeclaration();
|
|
1104
|
+
stmt2.scope = scope;
|
|
1105
|
+
return stmt2;
|
|
1106
|
+
}
|
|
1107
|
+
if (next.type === 59 /* KeywordEnum */) {
|
|
1108
|
+
const stmt2 = this.parseEnumDeclaration();
|
|
1109
|
+
stmt2.scope = scope;
|
|
1110
|
+
return stmt2;
|
|
1111
|
+
}
|
|
1112
|
+
if (next.type === 75 /* KeywordDeclare */) {
|
|
1113
|
+
const stmt2 = this.parseDeclareStatement();
|
|
1114
|
+
stmt2.scope = scope;
|
|
1115
|
+
return stmt2;
|
|
1116
|
+
}
|
|
1117
|
+
const stmt = this.parseDimStatement(false, true);
|
|
1118
|
+
if (stmt) {
|
|
1119
|
+
stmt.scope = scope;
|
|
1120
|
+
}
|
|
1121
|
+
return stmt;
|
|
1122
|
+
}
|
|
1123
|
+
if (token.type === 3 /* KeywordFor */) {
|
|
1124
|
+
return this.parseForStatement();
|
|
1125
|
+
} else if (token.type === 6 /* KeywordIf */) {
|
|
1126
|
+
return this.parseIfStatement();
|
|
1127
|
+
} else if (token.type === 11 /* KeywordDo */) {
|
|
1128
|
+
return this.parseDoWhileStatement();
|
|
1129
|
+
} else if (token.type === 12 /* KeywordWhile */) {
|
|
1130
|
+
return this.parseWhileStatement();
|
|
1131
|
+
} else if (token.type === 17 /* KeywordSub */ || token.type === 18 /* KeywordFunction */ || token.type === 19 /* KeywordProperty */ && this.peek(1).type !== 120 /* OperatorEquals */) {
|
|
1132
|
+
return this.parseProcedureDeclaration();
|
|
1133
|
+
} else if (token.type === 68 /* KeywordStatic */) {
|
|
1134
|
+
this.advance();
|
|
1135
|
+
const next = this.peek();
|
|
1136
|
+
if (next.type === 17 /* KeywordSub */ || next.type === 18 /* KeywordFunction */ || next.type === 19 /* KeywordProperty */) {
|
|
1137
|
+
return this.parseProcedureDeclaration(void 0, true);
|
|
1138
|
+
}
|
|
1139
|
+
return this.parseDimStatement(true, true);
|
|
1140
|
+
} else if (token.type === 22 /* KeywordDim */) {
|
|
1141
|
+
return this.parseDimStatement();
|
|
1142
|
+
} else if (token.type === 31 /* KeywordConst */) {
|
|
1143
|
+
return this.parseConstDeclaration();
|
|
1144
|
+
} else if (token.type === 16 /* KeywordStop */) {
|
|
1145
|
+
this.advance();
|
|
1146
|
+
if (!this.isAtTerminator()) {
|
|
1147
|
+
this.throwError(`Parse error: unexpected token after 'Stop'`);
|
|
1148
|
+
}
|
|
1149
|
+
return { type: "StopStatement" };
|
|
1150
|
+
} else if (token.type === 10 /* KeywordEnd */) {
|
|
1151
|
+
if (this.isAtEndTerminator()) {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
this.advance();
|
|
1155
|
+
if (!this.isAtTerminator()) {
|
|
1156
|
+
this.throwError(`Parse error: unexpected token after 'End'`);
|
|
1157
|
+
}
|
|
1158
|
+
return { type: "EndStatement" };
|
|
1159
|
+
} else if (token.type === 36 /* KeywordGoTo */) {
|
|
1160
|
+
return this.parseGoToStatement();
|
|
1161
|
+
} else if (token.type === 32 /* KeywordSet */) {
|
|
1162
|
+
return this.parseSetStatement();
|
|
1163
|
+
} else if (token.type === 34 /* KeywordOn */) {
|
|
1164
|
+
const next = this.peek(1);
|
|
1165
|
+
if (next.type === 35 /* KeywordError */) {
|
|
1166
|
+
return this.parseOnErrorStatement();
|
|
1167
|
+
} else {
|
|
1168
|
+
return this.parseOnGoToSubStatement();
|
|
1169
|
+
}
|
|
1170
|
+
} else if (token.type === 35 /* KeywordError */ && this.peek(1).type !== 120 /* OperatorEquals */) {
|
|
1171
|
+
return this.parseErrorStatement();
|
|
1172
|
+
} else if (token.type === 37 /* KeywordGoSub */) {
|
|
1173
|
+
return this.parseGoSubStatement();
|
|
1174
|
+
} else if (token.type === 38 /* KeywordReturn */) {
|
|
1175
|
+
this.advance();
|
|
1176
|
+
return { type: "ReturnStatement" };
|
|
1177
|
+
} else if (token.type === 39 /* KeywordLSet */) {
|
|
1178
|
+
return this.parseLSetStatement();
|
|
1179
|
+
} else if (token.type === 40 /* KeywordRSet */) {
|
|
1180
|
+
return this.parseRSetStatement();
|
|
1181
|
+
} else if (token.type === 45 /* KeywordExit */) {
|
|
1182
|
+
return this.parseExitStatement();
|
|
1183
|
+
} else if (token.type === 41 /* KeywordErase */) {
|
|
1184
|
+
return this.parseEraseStatement();
|
|
1185
|
+
} else if (token.type === 42 /* KeywordReDim */) {
|
|
1186
|
+
return this.parseReDimStatement();
|
|
1187
|
+
} else if (token.type === 52 /* KeywordResume */) {
|
|
1188
|
+
return this.parseResumeStatement();
|
|
1189
|
+
} else if (token.type === 105 /* KeywordImplements */) {
|
|
1190
|
+
return this.parseImplementsDirective();
|
|
1191
|
+
} else if (token.type === 106 /* KeywordAppActivate */ && this.peek(1).type !== 120 /* OperatorEquals */) {
|
|
1192
|
+
return this.parseAppActivateStatement();
|
|
1193
|
+
} else if (token.type === 107 /* KeywordSendKeys */ && this.peek(1).type !== 120 /* OperatorEquals */) {
|
|
1194
|
+
return this.parseSendKeysStatement();
|
|
1195
|
+
} else if (token.type === 29 /* KeywordOption */) {
|
|
1196
|
+
this.advance();
|
|
1197
|
+
if (this.match(71 /* KeywordCompare */)) {
|
|
1198
|
+
return this.parseOptionCompareStatement();
|
|
1199
|
+
}
|
|
1200
|
+
if (this.match(30 /* KeywordExplicit */)) {
|
|
1201
|
+
return { type: "OptionExplicitStatement" };
|
|
1202
|
+
}
|
|
1203
|
+
if (this.peek().type === 0 /* Identifier */ && this.peek().value.toLowerCase() === "base") {
|
|
1204
|
+
this.advance();
|
|
1205
|
+
const baseToken = this.advance();
|
|
1206
|
+
if (baseToken.value === "0" || baseToken.value === "1") {
|
|
1207
|
+
return { type: "OptionBaseStatement", base: parseInt(baseToken.value) };
|
|
1208
|
+
}
|
|
1209
|
+
this.throwError(`Parse error: Option Base must be 0 or 1 at line ${baseToken.line}`);
|
|
1210
|
+
}
|
|
1211
|
+
if (this.match(58 /* KeywordPrivate */)) {
|
|
1212
|
+
this.consume(80 /* KeywordModule */, "Expected 'Module' after 'Option Private'");
|
|
1213
|
+
return { type: "OptionPrivateModuleStatement" };
|
|
1214
|
+
}
|
|
1215
|
+
return null;
|
|
1216
|
+
} else if (token.type === 74 /* KeywordAttribute */) {
|
|
1217
|
+
return this.parseAttributeStatement();
|
|
1218
|
+
} else if (token.type === 75 /* KeywordDeclare */) {
|
|
1219
|
+
return this.parseDeclareStatement();
|
|
1220
|
+
} else if (token.type === 53 /* KeywordSelect */) {
|
|
1221
|
+
return this.parseSelectCaseStatement();
|
|
1222
|
+
} else if (token.type === 61 /* KeywordWith */) {
|
|
1223
|
+
return this.parseWithStatement();
|
|
1224
|
+
} else if (token.type === 48 /* KeywordType */) {
|
|
1225
|
+
return this.parseTypeDeclaration();
|
|
1226
|
+
} else if (token.type === 59 /* KeywordEnum */) {
|
|
1227
|
+
return this.parseEnumDeclaration();
|
|
1228
|
+
} else if (token.type === 81 /* KeywordOpen */) {
|
|
1229
|
+
return this.parseOpenStatement();
|
|
1230
|
+
} else if (token.type === 82 /* KeywordClose */) {
|
|
1231
|
+
return this.parseCloseStatement();
|
|
1232
|
+
} else if (token.type === 83 /* KeywordLine */ && this.peek(1).type !== 120 /* OperatorEquals */) {
|
|
1233
|
+
return this.parseLineInputStatement();
|
|
1234
|
+
} else if (token.type === 85 /* KeywordPrint */) {
|
|
1235
|
+
return this.parsePrintStatement();
|
|
1236
|
+
} else if (token.type === 86 /* KeywordPut */) {
|
|
1237
|
+
return this.parsePutStatement();
|
|
1238
|
+
} else if (token.type === 20 /* KeywordGet */) {
|
|
1239
|
+
return this.parseGetStatement();
|
|
1240
|
+
} else if (token.type === 84 /* KeywordInput */) {
|
|
1241
|
+
return this.parseInputStatement();
|
|
1242
|
+
} else if (token.type === 92 /* KeywordWrite */) {
|
|
1243
|
+
return this.parseWriteStatement();
|
|
1244
|
+
} else if (token.type === 98 /* KeywordSeek */) {
|
|
1245
|
+
return this.parseSeekStatement();
|
|
1246
|
+
} else if (token.type === 99 /* KeywordReset */ && this.peek(1).type !== 120 /* OperatorEquals */) {
|
|
1247
|
+
return this.parseResetStatement();
|
|
1248
|
+
} else if (token.type === 97 /* KeywordKill */ && this.peek(1).type !== 120 /* OperatorEquals */) {
|
|
1249
|
+
return this.parseKillStatement();
|
|
1250
|
+
} else if (token.type === 102 /* KeywordEvent */) {
|
|
1251
|
+
return this.parseEventDeclaration();
|
|
1252
|
+
} else if (token.type === 103 /* KeywordRaiseEvent */) {
|
|
1253
|
+
return this.parseRaiseEventStatement();
|
|
1254
|
+
} else if (token.type === 93 /* KeywordLock */) {
|
|
1255
|
+
return this.parseLockStatement();
|
|
1256
|
+
} else if (token.type === 100 /* KeywordUnlock */) {
|
|
1257
|
+
return this.parseUnlockStatement();
|
|
1258
|
+
} else if (token.type === 109 /* KeywordWidth */ && this.peek(1).type !== 120 /* OperatorEquals */) {
|
|
1259
|
+
return this.parseWidthStatement();
|
|
1260
|
+
} else if (token.type === 69 /* KeywordClass */ && this.peek(1).type !== 120 /* OperatorEquals */) {
|
|
1261
|
+
return this.parseClassDeclaration();
|
|
1262
|
+
} else if (token.type === 33 /* KeywordCall */) {
|
|
1263
|
+
this.advance();
|
|
1264
|
+
const expr = this.parsePrimary();
|
|
1265
|
+
if (expr.type === "CallExpression") {
|
|
1266
|
+
return { type: "CallStatement", expression: expr };
|
|
1267
|
+
} else if (expr.type === "Identifier" || expr.type === "MemberExpression") {
|
|
1268
|
+
return { type: "CallStatement", expression: { type: "CallExpression", callee: expr, args: [] } };
|
|
1269
|
+
}
|
|
1270
|
+
this.throwError(`Parse error: Expected procedure call after 'Call'`);
|
|
1271
|
+
} else if (token.type === 0 /* Identifier */ || token.type === 138 /* ForeignName */ || token.type === 129 /* OperatorDot */ || token.type === 1 /* Number */ || _Parser.CONTEXTUAL_KW.has(token.type)) {
|
|
1272
|
+
if (token.type === 0 /* Identifier */ && this.pos + 1 < this.tokens.length && this.tokens[this.pos + 1].type === 130 /* OperatorColon */) {
|
|
1273
|
+
const labelName = token.value;
|
|
1274
|
+
this.advance();
|
|
1275
|
+
this.advance();
|
|
1276
|
+
return { type: "LabelStatement", label: labelName };
|
|
1277
|
+
} else if (token.type === 1 /* Number */) {
|
|
1278
|
+
const labelName = token.value;
|
|
1279
|
+
this.advance();
|
|
1280
|
+
this.match(130 /* OperatorColon */);
|
|
1281
|
+
return { type: "LabelStatement", label: labelName };
|
|
1282
|
+
}
|
|
1283
|
+
const savedPos = this.pos;
|
|
1284
|
+
const expr = this.parsePrimary();
|
|
1285
|
+
if (this.match(120 /* OperatorEquals */)) {
|
|
1286
|
+
return {
|
|
1287
|
+
type: "AssignmentStatement",
|
|
1288
|
+
left: expr,
|
|
1289
|
+
right: this.parseExpression()
|
|
1290
|
+
};
|
|
1291
|
+
} else {
|
|
1292
|
+
if (expr.type === "CallExpression" && this.isBinaryOnlyOperator(this.peek().type)) {
|
|
1293
|
+
this.pos = savedPos;
|
|
1294
|
+
const callee = this.parsePrimary(
|
|
1295
|
+
/* stopBeforeSpacedLParen= */
|
|
1296
|
+
true
|
|
1297
|
+
);
|
|
1298
|
+
const args3 = [this.parseCallArgument()];
|
|
1299
|
+
while (this.match(126 /* OperatorComma */)) {
|
|
1300
|
+
args3.push(this.parseCallArgument());
|
|
1301
|
+
}
|
|
1302
|
+
return { type: "CallStatement", expression: { type: "CallExpression", callee, args: args3 } };
|
|
1303
|
+
}
|
|
1304
|
+
const args2 = [];
|
|
1305
|
+
if (this.peek().type !== 135 /* Newline */ && this.peek().type !== 136 /* EOF */ && this.peek().type !== 9 /* KeywordElse */ && this.peek().type !== 8 /* KeywordElseIf */ && this.peek().type !== 10 /* KeywordEnd */ && this.peek().type !== 5 /* KeywordNext */ && this.peek().type !== 14 /* KeywordLoop */) {
|
|
1306
|
+
args2.push(this.parseCallArgument());
|
|
1307
|
+
while (this.match(126 /* OperatorComma */)) {
|
|
1308
|
+
args2.push(this.parseCallArgument());
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
if (args2.length > 0) {
|
|
1312
|
+
return { type: "CallStatement", expression: { type: "CallExpression", callee: expr, args: args2 } };
|
|
1313
|
+
} else if (expr.type === "CallExpression") {
|
|
1314
|
+
const callExpr = expr;
|
|
1315
|
+
if (callExpr.args.length === 0 && callExpr.callee.type === "Identifier" && !callExpr.callee.foreign) {
|
|
1316
|
+
const line = callExpr.loc?.start.line ?? this.peek().line;
|
|
1317
|
+
this.throwError(`Parse error: syntax error at line ${line}`);
|
|
1318
|
+
}
|
|
1319
|
+
return { type: "CallStatement", expression: callExpr };
|
|
1320
|
+
} else {
|
|
1321
|
+
return { type: "CallStatement", expression: { type: "CallExpression", callee: expr, args: [] } };
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
} else if (token.type === 137 /* Unknown */) {
|
|
1325
|
+
this.throwError(`Parse error: Unknown token '${this.tokenDisplay(token.value)}' at line ${token.line}`);
|
|
1326
|
+
} else {
|
|
1327
|
+
this.advance();
|
|
1328
|
+
}
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
parseProcedureDeclaration(scope, isStatic) {
|
|
1332
|
+
const isFunction = this.peek().type === 18 /* KeywordFunction */;
|
|
1333
|
+
const isProperty = this.peek().type === 19 /* KeywordProperty */;
|
|
1334
|
+
this.advance();
|
|
1335
|
+
let propertyType;
|
|
1336
|
+
if (isProperty) {
|
|
1337
|
+
const typeToken = this.advance();
|
|
1338
|
+
if (typeToken.type === 20 /* KeywordGet */) propertyType = "get";
|
|
1339
|
+
else if (typeToken.type === 21 /* KeywordLet */) propertyType = "let";
|
|
1340
|
+
else if (typeToken.type === 32 /* KeywordSet */) propertyType = "set";
|
|
1341
|
+
else {
|
|
1342
|
+
this.throwError(`Parse error: Expected 'Get', 'Let', or 'Set' after 'Property' at line ${typeToken.line}`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
const idToken = this.advance();
|
|
1346
|
+
if (!this.isIdentifier(idToken) && (idToken.type < 79 /* KeywordBase */ || idToken.type > 110 /* KeywordAddressOf */)) {
|
|
1347
|
+
this.throwError(`Parse error at line ${idToken.line}: Expected procedure name (Found ${this.tokenDisplay(idToken.value)})`);
|
|
1348
|
+
}
|
|
1349
|
+
const name = this.makeIdentifier(idToken);
|
|
1350
|
+
const parameters = [];
|
|
1351
|
+
let paramsEndColumn;
|
|
1352
|
+
if (this.match(127 /* OperatorLParen */)) {
|
|
1353
|
+
if (this.peek().type !== 128 /* OperatorRParen */) {
|
|
1354
|
+
parameters.push(this.parseParameter());
|
|
1355
|
+
while (this.match(126 /* OperatorComma */)) {
|
|
1356
|
+
parameters.push(this.parseParameter());
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
const rParen = this.consume(128 /* OperatorRParen */, "Expected ')' after procedure parameters");
|
|
1360
|
+
paramsEndColumn = rParen.column + 1;
|
|
1361
|
+
}
|
|
1362
|
+
let returnType;
|
|
1363
|
+
if (this.match(23 /* KeywordAs */)) {
|
|
1364
|
+
returnType = this.advance().value;
|
|
1365
|
+
if (this.peek().type === 129 /* OperatorDot */) {
|
|
1366
|
+
this.advance();
|
|
1367
|
+
returnType += "." + this.advance().value;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
if (this.peek().type === 68 /* KeywordStatic */) {
|
|
1371
|
+
this.advance();
|
|
1372
|
+
isStatic = true;
|
|
1373
|
+
}
|
|
1374
|
+
this.skipNewlines();
|
|
1375
|
+
const body = [];
|
|
1376
|
+
const expectedEndStr = isFunction ? "Function" : isProperty ? "Property" : "Sub";
|
|
1377
|
+
while (!this.isAtEndTerminator() && this.peek().type !== 136 /* EOF */) {
|
|
1378
|
+
const stmt = this.parseStatement();
|
|
1379
|
+
if (stmt) body.push(stmt);
|
|
1380
|
+
this.skipNewlines();
|
|
1381
|
+
}
|
|
1382
|
+
if (this.peek().type === 10 /* KeywordEnd */) {
|
|
1383
|
+
this.advance();
|
|
1384
|
+
const endToken = this.advance();
|
|
1385
|
+
if (endToken.value.toLowerCase() !== expectedEndStr.toLowerCase()) {
|
|
1386
|
+
this.throwError(`Parse error: Expected '${expectedEndStr}' after 'End' at line ${endToken.line}`);
|
|
1387
|
+
}
|
|
1388
|
+
} else if (this.peek().type === 136 /* EOF */) {
|
|
1389
|
+
this.throwError(`Parse error: Expected 'End ${expectedEndStr}'`);
|
|
1390
|
+
}
|
|
1391
|
+
return { type: "ProcedureDeclaration", isFunction, isProperty, propertyType, name, parameters, returnType, body, scope: scope || "public", isStatic, paramsEndColumn };
|
|
1392
|
+
}
|
|
1393
|
+
parseDimStatement(isStatic = false, keywordConsumed = false) {
|
|
1394
|
+
if (!keywordConsumed && !isStatic) this.advance();
|
|
1395
|
+
const declarations = [];
|
|
1396
|
+
while (true) {
|
|
1397
|
+
let isWithEvents = false;
|
|
1398
|
+
if (this.match(104 /* KeywordWithEvents */)) {
|
|
1399
|
+
isWithEvents = true;
|
|
1400
|
+
}
|
|
1401
|
+
const idToken = this.advance();
|
|
1402
|
+
if (!this.isIdentifier(idToken) && (idToken.type < 79 /* KeywordBase */ || idToken.type > 110 /* KeywordAddressOf */)) {
|
|
1403
|
+
this.throwError(`Parse error at line ${idToken.line}: Expected variable name (Found ${this.tokenDisplay(idToken.value)})`);
|
|
1404
|
+
}
|
|
1405
|
+
const name = this.makeIdentifier(idToken);
|
|
1406
|
+
let isArray = false;
|
|
1407
|
+
let arrayBounds;
|
|
1408
|
+
let isNew = false;
|
|
1409
|
+
let objectType;
|
|
1410
|
+
let arrayEndColumn;
|
|
1411
|
+
if (this.match(127 /* OperatorLParen */)) {
|
|
1412
|
+
isArray = true;
|
|
1413
|
+
if (this.peek().type !== 128 /* OperatorRParen */) {
|
|
1414
|
+
arrayBounds = [];
|
|
1415
|
+
while (true) {
|
|
1416
|
+
let lower;
|
|
1417
|
+
let upper = this.parseExpression();
|
|
1418
|
+
if (this.match(4 /* KeywordTo */)) {
|
|
1419
|
+
lower = upper;
|
|
1420
|
+
upper = this.parseExpression();
|
|
1421
|
+
}
|
|
1422
|
+
arrayBounds.push({ lower, upper });
|
|
1423
|
+
if (!this.match(126 /* OperatorComma */)) {
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
const rParenTok = this.peek();
|
|
1429
|
+
if (this.match(128 /* OperatorRParen */)) {
|
|
1430
|
+
arrayEndColumn = rParenTok.column + 1;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
if (this.match(23 /* KeywordAs */)) {
|
|
1434
|
+
if (this.match(24 /* KeywordNew */)) {
|
|
1435
|
+
isNew = true;
|
|
1436
|
+
}
|
|
1437
|
+
const typeToken = this.peek();
|
|
1438
|
+
if (this.isNameToken(typeToken)) {
|
|
1439
|
+
objectType = this.advance().value;
|
|
1440
|
+
if (this.peek().type === 129 /* OperatorDot */) {
|
|
1441
|
+
this.advance();
|
|
1442
|
+
objectType += "." + this.advance().value;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
declarations.push({ name, isArray, arrayBounds, isNew, isWithEvents, objectType, arrayEndColumn });
|
|
1447
|
+
if (this.match(126 /* OperatorComma */)) {
|
|
1448
|
+
continue;
|
|
1449
|
+
} else {
|
|
1450
|
+
break;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return { type: "VariableDeclaration", declarations, isStatic };
|
|
1454
|
+
}
|
|
1455
|
+
parseConstDeclaration() {
|
|
1456
|
+
this.advance();
|
|
1457
|
+
const idToken = this.advance();
|
|
1458
|
+
if (!this.isIdentifier(idToken)) this.throwError(`Parse error: Expected identifier after Const at line ${idToken.line}`);
|
|
1459
|
+
const name = this.makeIdentifier(idToken);
|
|
1460
|
+
if (this.match(23 /* KeywordAs */)) {
|
|
1461
|
+
this.advance();
|
|
1462
|
+
}
|
|
1463
|
+
if (!this.match(120 /* OperatorEquals */)) this.throwError(`Parse error: Expected '=' in Const at line ${this.peek().line}`);
|
|
1464
|
+
const value = this.parseExpression();
|
|
1465
|
+
return { type: "ConstDeclaration", name, value };
|
|
1466
|
+
}
|
|
1467
|
+
parseSetStatement() {
|
|
1468
|
+
this.advance();
|
|
1469
|
+
const left = this.parsePrimary();
|
|
1470
|
+
if (!this.match(120 /* OperatorEquals */)) this.throwError(`Parse error: Expected '=' in Set statement at line ${this.peek().line}`);
|
|
1471
|
+
const right = this.parseExpression();
|
|
1472
|
+
return { type: "SetStatement", left, right };
|
|
1473
|
+
}
|
|
1474
|
+
parseOnErrorStatement() {
|
|
1475
|
+
this.advance();
|
|
1476
|
+
if (!this.match(35 /* KeywordError */)) this.throwError(`Parse error: Expected 'Error' after 'On' at line ${this.peek().line}`);
|
|
1477
|
+
let label = "";
|
|
1478
|
+
if (this.match(36 /* KeywordGoTo */)) {
|
|
1479
|
+
const labelToken = this.advance();
|
|
1480
|
+
label = labelToken.value;
|
|
1481
|
+
} else {
|
|
1482
|
+
while (this.peek().type !== 135 /* Newline */ && this.peek().type !== 136 /* EOF */) {
|
|
1483
|
+
label += this.advance().value + " ";
|
|
1484
|
+
}
|
|
1485
|
+
label = label.trim();
|
|
1486
|
+
}
|
|
1487
|
+
return { type: "OnErrorStatement", label };
|
|
1488
|
+
}
|
|
1489
|
+
parseExitStatement() {
|
|
1490
|
+
this.advance();
|
|
1491
|
+
const typeToken = this.advance();
|
|
1492
|
+
let exitType;
|
|
1493
|
+
if (typeToken.type === 3 /* KeywordFor */) {
|
|
1494
|
+
exitType = "For";
|
|
1495
|
+
} else if (typeToken.type === 11 /* KeywordDo */) {
|
|
1496
|
+
exitType = "Do";
|
|
1497
|
+
} else if (typeToken.type === 17 /* KeywordSub */) {
|
|
1498
|
+
exitType = "Sub";
|
|
1499
|
+
} else if (typeToken.type === 18 /* KeywordFunction */) {
|
|
1500
|
+
exitType = "Function";
|
|
1501
|
+
} else if (typeToken.type === 19 /* KeywordProperty */) {
|
|
1502
|
+
exitType = "Property";
|
|
1503
|
+
} else {
|
|
1504
|
+
this.throwError(`Parse error: Expected 'For', 'Do', 'Sub', 'Function', or 'Property' after 'Exit' at line ${typeToken.line}`);
|
|
1505
|
+
}
|
|
1506
|
+
return { type: "ExitStatement", exitType };
|
|
1507
|
+
}
|
|
1508
|
+
parseGoToStatement() {
|
|
1509
|
+
this.advance();
|
|
1510
|
+
const labelToken = this.advance();
|
|
1511
|
+
if (labelToken.type !== 0 /* Identifier */ && labelToken.type !== 1 /* Number */) {
|
|
1512
|
+
this.throwError(`Parse error: Expected identifier or number after 'GoTo' at line ${labelToken.line}`);
|
|
1513
|
+
}
|
|
1514
|
+
return { type: "GoToStatement", label: labelToken.value };
|
|
1515
|
+
}
|
|
1516
|
+
parseResumeStatement() {
|
|
1517
|
+
this.advance();
|
|
1518
|
+
let target = "";
|
|
1519
|
+
while (this.peek().type !== 135 /* Newline */ && this.peek().type !== 136 /* EOF */) {
|
|
1520
|
+
target += this.advance().value;
|
|
1521
|
+
}
|
|
1522
|
+
return { type: "ResumeStatement", target: target.trim() };
|
|
1523
|
+
}
|
|
1524
|
+
parseEraseStatement() {
|
|
1525
|
+
this.advance();
|
|
1526
|
+
const idToken = this.advance();
|
|
1527
|
+
const name = this.makeIdentifier(idToken);
|
|
1528
|
+
return { type: "EraseStatement", name };
|
|
1529
|
+
}
|
|
1530
|
+
parseReDimStatement() {
|
|
1531
|
+
this.advance();
|
|
1532
|
+
let isPreserve = false;
|
|
1533
|
+
if (this.peek().type === 0 /* Identifier */ && this.peek().value.toLowerCase() === "preserve") {
|
|
1534
|
+
isPreserve = true;
|
|
1535
|
+
this.advance();
|
|
1536
|
+
}
|
|
1537
|
+
const idToken = this.advance();
|
|
1538
|
+
const name = this.makeIdentifier(idToken);
|
|
1539
|
+
const bounds = [];
|
|
1540
|
+
if (this.match(127 /* OperatorLParen */)) {
|
|
1541
|
+
if (this.peek().type !== 128 /* OperatorRParen */) {
|
|
1542
|
+
while (true) {
|
|
1543
|
+
let lower;
|
|
1544
|
+
let upper = this.parseExpression();
|
|
1545
|
+
if (this.match(4 /* KeywordTo */)) {
|
|
1546
|
+
lower = upper;
|
|
1547
|
+
upper = this.parseExpression();
|
|
1548
|
+
}
|
|
1549
|
+
bounds.push({ lower, upper });
|
|
1550
|
+
if (!this.match(126 /* OperatorComma */)) {
|
|
1551
|
+
break;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
this.match(128 /* OperatorRParen */);
|
|
1556
|
+
}
|
|
1557
|
+
let objectType;
|
|
1558
|
+
if (this.match(23 /* KeywordAs */)) {
|
|
1559
|
+
objectType = this.advance().value;
|
|
1560
|
+
}
|
|
1561
|
+
return { type: "ReDimStatement", name, bounds, isPreserve, objectType };
|
|
1562
|
+
}
|
|
1563
|
+
parseTypeDeclaration() {
|
|
1564
|
+
this.advance();
|
|
1565
|
+
const nameToken = this.advance();
|
|
1566
|
+
if (!this.isIdentifier(nameToken)) {
|
|
1567
|
+
this.throwError(`Parse error: Expected identifier after 'Type' at line ${nameToken.line}`);
|
|
1568
|
+
}
|
|
1569
|
+
const typeName = nameToken.value;
|
|
1570
|
+
const members = [];
|
|
1571
|
+
this.skipNewlines();
|
|
1572
|
+
while (this.peek().type !== 10 /* KeywordEnd */ && this.peek().type !== 136 /* EOF */) {
|
|
1573
|
+
const memberNameToken = this.advance();
|
|
1574
|
+
if (!this.isWordToken(memberNameToken)) {
|
|
1575
|
+
this.throwError(`Parse error: Expected member name in Type at line ${memberNameToken.line}`);
|
|
1576
|
+
}
|
|
1577
|
+
if (this.peek().type === 127 /* OperatorLParen */) {
|
|
1578
|
+
this.advance();
|
|
1579
|
+
let depth = 1;
|
|
1580
|
+
while (depth > 0 && this.peek().type !== 136 /* EOF */ && this.peek().type !== 135 /* Newline */) {
|
|
1581
|
+
const t = this.advance();
|
|
1582
|
+
if (t.type === 127 /* OperatorLParen */) depth++;
|
|
1583
|
+
else if (t.type === 128 /* OperatorRParen */) depth--;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
if (!this.match(23 /* KeywordAs */)) {
|
|
1587
|
+
this.throwError(`Parse error: Expected 'As' in Type member declaration at line ${this.peek().line}`);
|
|
1588
|
+
}
|
|
1589
|
+
const memberTypeToken = this.advance();
|
|
1590
|
+
members.push({ name: memberNameToken.value, memberType: memberTypeToken.value });
|
|
1591
|
+
this.skipNewlines();
|
|
1592
|
+
}
|
|
1593
|
+
if (this.peek().type === 10 /* KeywordEnd */) {
|
|
1594
|
+
this.advance();
|
|
1595
|
+
if (!this.match(48 /* KeywordType */)) {
|
|
1596
|
+
this.throwError(`Parse error: Expected 'Type' after 'End' at line ${this.peek().line}`);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
return { type: "TypeDeclaration", name: typeName, members };
|
|
1600
|
+
}
|
|
1601
|
+
parseEnumDeclaration() {
|
|
1602
|
+
this.advance();
|
|
1603
|
+
const nameToken = this.advance();
|
|
1604
|
+
if (!this.isIdentifier(nameToken)) {
|
|
1605
|
+
this.throwError(`Parse error: Expected identifier after 'Enum' at line ${nameToken.line}`);
|
|
1606
|
+
}
|
|
1607
|
+
const name = this.makeIdentifier(nameToken);
|
|
1608
|
+
const members = [];
|
|
1609
|
+
this.skipNewlines();
|
|
1610
|
+
while (this.peek().type !== 10 /* KeywordEnd */ && this.peek().type !== 136 /* EOF */) {
|
|
1611
|
+
const memberNameToken = this.advance();
|
|
1612
|
+
if (!this.isWordToken(memberNameToken)) {
|
|
1613
|
+
this.throwError(`Parse error: Expected member name in Enum at line ${memberNameToken.line}`);
|
|
1614
|
+
}
|
|
1615
|
+
const memberName = this.makeIdentifier(memberNameToken);
|
|
1616
|
+
let value;
|
|
1617
|
+
if (this.match(120 /* OperatorEquals */)) {
|
|
1618
|
+
value = this.parseExpression();
|
|
1619
|
+
}
|
|
1620
|
+
members.push({ name: memberName, value });
|
|
1621
|
+
this.skipNewlines();
|
|
1622
|
+
}
|
|
1623
|
+
if (this.peek().type === 10 /* KeywordEnd */) {
|
|
1624
|
+
this.advance();
|
|
1625
|
+
if (!this.match(59 /* KeywordEnum */)) {
|
|
1626
|
+
this.throwError(`Parse error: Expected 'Enum' after 'End' at line ${this.peek().line}`);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
return { type: "EnumDeclaration", name, members };
|
|
1630
|
+
}
|
|
1631
|
+
parseClassDeclaration() {
|
|
1632
|
+
this.advance();
|
|
1633
|
+
const nameToken = this.advance();
|
|
1634
|
+
if (nameToken.type !== 0 /* Identifier */) {
|
|
1635
|
+
this.throwError(`Parse error: Expected class name after 'Class' at line ${nameToken.line}`);
|
|
1636
|
+
}
|
|
1637
|
+
return this.parseClassBody(nameToken.value, true);
|
|
1638
|
+
}
|
|
1639
|
+
parseClassBody(className, untilEndClass) {
|
|
1640
|
+
const fields = [];
|
|
1641
|
+
const procedures = [];
|
|
1642
|
+
const body = [];
|
|
1643
|
+
this.skipNewlines();
|
|
1644
|
+
while (this.peek().type !== 136 /* EOF */) {
|
|
1645
|
+
if (untilEndClass && this.peek().type === 10 /* KeywordEnd */ && this.peek(1).type === 69 /* KeywordClass */) {
|
|
1646
|
+
break;
|
|
1647
|
+
}
|
|
1648
|
+
const tok = this.peek();
|
|
1649
|
+
if (tok.type === 135 /* Newline */) {
|
|
1650
|
+
this.skipNewlines();
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
let scope;
|
|
1654
|
+
if (tok.type === 57 /* KeywordPublic */ || tok.type === 58 /* KeywordPrivate */ || tok.type === 60 /* KeywordFriend */) {
|
|
1655
|
+
scope = tok.value.toLowerCase();
|
|
1656
|
+
this.advance();
|
|
1657
|
+
}
|
|
1658
|
+
const inner = this.peek();
|
|
1659
|
+
if (inner.type === 17 /* KeywordSub */ || inner.type === 18 /* KeywordFunction */ || inner.type === 19 /* KeywordProperty */) {
|
|
1660
|
+
const proc = this.parseProcedureDeclaration(scope);
|
|
1661
|
+
proc.moduleName = className;
|
|
1662
|
+
procedures.push(proc);
|
|
1663
|
+
body.push(proc);
|
|
1664
|
+
} else if (inner.type === 22 /* KeywordDim */) {
|
|
1665
|
+
this.advance();
|
|
1666
|
+
const field = this.parseDimStatement(false, true);
|
|
1667
|
+
field.scope = scope ?? "public";
|
|
1668
|
+
fields.push(field);
|
|
1669
|
+
body.push(field);
|
|
1670
|
+
} else if (inner.type === 105 /* KeywordImplements */) {
|
|
1671
|
+
const impl = this.parseImplementsDirective();
|
|
1672
|
+
body.push(impl);
|
|
1673
|
+
} else if (inner.type === 102 /* KeywordEvent */) {
|
|
1674
|
+
const event = this.parseEventDeclaration(scope);
|
|
1675
|
+
body.push(event);
|
|
1676
|
+
} else if (inner.type === 68 /* KeywordStatic */) {
|
|
1677
|
+
this.advance();
|
|
1678
|
+
const field = this.parseDimStatement(true, true);
|
|
1679
|
+
field.scope = scope ?? "public";
|
|
1680
|
+
fields.push(field);
|
|
1681
|
+
body.push(field);
|
|
1682
|
+
} else if (scope !== void 0 && inner.type === 0 /* Identifier */) {
|
|
1683
|
+
const field = this.parseDimStatement(false, true);
|
|
1684
|
+
field.scope = scope;
|
|
1685
|
+
fields.push(field);
|
|
1686
|
+
body.push(field);
|
|
1687
|
+
} else {
|
|
1688
|
+
this.advance();
|
|
1689
|
+
}
|
|
1690
|
+
this.skipNewlines();
|
|
1691
|
+
}
|
|
1692
|
+
if (untilEndClass && this.peek().type === 10 /* KeywordEnd */) {
|
|
1693
|
+
this.advance();
|
|
1694
|
+
if (!this.match(69 /* KeywordClass */)) {
|
|
1695
|
+
this.throwError(`Parse error: Expected 'Class' after 'End' at line ${this.peek().line}`);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
return { type: "ClassDeclaration", name: className, fields, procedures, body };
|
|
1699
|
+
}
|
|
1700
|
+
parseForStatement() {
|
|
1701
|
+
this.advance();
|
|
1702
|
+
if (this.peek().type === 55 /* KeywordEach */) {
|
|
1703
|
+
return this.parseForEachStatementBody();
|
|
1704
|
+
}
|
|
1705
|
+
const idToken = this.advance();
|
|
1706
|
+
if (!this.isIdentifier(idToken)) {
|
|
1707
|
+
this.throwError(`Parse error: Expected identifier after 'For' at line ${idToken.line} `);
|
|
1708
|
+
}
|
|
1709
|
+
const identifier = this.makeIdentifier(idToken);
|
|
1710
|
+
if (!this.match(120 /* OperatorEquals */)) {
|
|
1711
|
+
this.throwError(`Parse error: Expected '=' in For statement at line ${this.peek().line} `);
|
|
1712
|
+
}
|
|
1713
|
+
const startExpr = this.parseExpression();
|
|
1714
|
+
if (!this.match(4 /* KeywordTo */)) {
|
|
1715
|
+
this.throwError(`Parse error: Expected 'To' in For statement at line ${this.peek().line} `);
|
|
1716
|
+
}
|
|
1717
|
+
const endExpr = this.parseExpression();
|
|
1718
|
+
let stepExpr;
|
|
1719
|
+
if (this.match(43 /* KeywordStep */)) {
|
|
1720
|
+
stepExpr = this.parseExpression();
|
|
1721
|
+
}
|
|
1722
|
+
this.skipNewlines();
|
|
1723
|
+
const body = [];
|
|
1724
|
+
while (this.peek().type !== 5 /* KeywordNext */ && this.peek().type !== 136 /* EOF */ && !this.isAtEndTerminator()) {
|
|
1725
|
+
const stmt = this.parseStatement();
|
|
1726
|
+
if (stmt) body.push(stmt);
|
|
1727
|
+
this.skipNewlines();
|
|
1728
|
+
}
|
|
1729
|
+
if (!this.match(5 /* KeywordNext */)) {
|
|
1730
|
+
this.throwError(`Parse error: Expected 'Next' at line ${this.peek().line} `);
|
|
1731
|
+
}
|
|
1732
|
+
let nextIdentifier;
|
|
1733
|
+
if (this.isIdentifier(this.peek())) {
|
|
1734
|
+
const nextIdToken = this.advance();
|
|
1735
|
+
nextIdentifier = this.makeIdentifier(nextIdToken);
|
|
1736
|
+
if (nextIdentifier.name.toLowerCase() !== identifier.name.toLowerCase()) {
|
|
1737
|
+
this.throwError(
|
|
1738
|
+
`Compile error: Variable reference not valid in 'Next' (expected '${identifier.name}', got '${nextIdentifier.name}')`,
|
|
1739
|
+
nextIdToken
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
return {
|
|
1744
|
+
type: "ForStatement",
|
|
1745
|
+
identifier,
|
|
1746
|
+
start: startExpr,
|
|
1747
|
+
end: endExpr,
|
|
1748
|
+
step: stepExpr,
|
|
1749
|
+
body,
|
|
1750
|
+
nextIdentifier
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
parseForEachStatementBody() {
|
|
1754
|
+
this.advance();
|
|
1755
|
+
const varToken = this.advance();
|
|
1756
|
+
if (!this.isIdentifier(varToken)) {
|
|
1757
|
+
this.throwError(`Parse error: Expected identifier after 'For Each' at line ${varToken.line}`);
|
|
1758
|
+
}
|
|
1759
|
+
const variable = this.makeIdentifier(varToken);
|
|
1760
|
+
if (!this.match(56 /* KeywordIn */)) {
|
|
1761
|
+
this.throwError(`Parse error: Expected 'In' in For Each statement at line ${this.peek().line}`);
|
|
1762
|
+
}
|
|
1763
|
+
const collection = this.parseExpression();
|
|
1764
|
+
this.skipNewlines();
|
|
1765
|
+
const body = [];
|
|
1766
|
+
while (this.peek().type !== 5 /* KeywordNext */ && this.peek().type !== 136 /* EOF */ && !this.isAtEndTerminator()) {
|
|
1767
|
+
const stmt = this.parseStatement();
|
|
1768
|
+
if (stmt) body.push(stmt);
|
|
1769
|
+
this.skipNewlines();
|
|
1770
|
+
}
|
|
1771
|
+
if (!this.match(5 /* KeywordNext */)) {
|
|
1772
|
+
this.throwError(`Parse error: Expected 'Next' at line ${this.peek().line}`);
|
|
1773
|
+
}
|
|
1774
|
+
let nextIdentifier;
|
|
1775
|
+
if (this.isIdentifier(this.peek())) {
|
|
1776
|
+
const nextIdToken = this.advance();
|
|
1777
|
+
nextIdentifier = this.makeIdentifier(nextIdToken);
|
|
1778
|
+
if (nextIdentifier.name.toLowerCase() !== variable.name.toLowerCase()) {
|
|
1779
|
+
this.throwError(
|
|
1780
|
+
`Compile error: Variable reference not valid in 'Next' (expected '${variable.name}', got '${nextIdentifier.name}')`,
|
|
1781
|
+
nextIdToken
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
return {
|
|
1786
|
+
type: "ForEachStatement",
|
|
1787
|
+
variable,
|
|
1788
|
+
collection,
|
|
1789
|
+
body,
|
|
1790
|
+
nextIdentifier
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
parseIfStatement() {
|
|
1794
|
+
this.advance();
|
|
1795
|
+
const condition = this.parseExpression();
|
|
1796
|
+
if (!this.match(7 /* KeywordThen */)) {
|
|
1797
|
+
this.throwError(`Parse error: Expected 'Then' after condition at line ${this.peek().line}`);
|
|
1798
|
+
}
|
|
1799
|
+
const isMultiLine = this.peek().type === 135 /* Newline */;
|
|
1800
|
+
const consequent = [];
|
|
1801
|
+
let alternate = null;
|
|
1802
|
+
if (!isMultiLine) {
|
|
1803
|
+
while (this.peek().type !== 135 /* Newline */ && this.peek().type !== 136 /* EOF */ && this.peek().type !== 9 /* KeywordElse */) {
|
|
1804
|
+
const stmt = this.parseStatement(false);
|
|
1805
|
+
if (stmt) consequent.push(stmt);
|
|
1806
|
+
}
|
|
1807
|
+
if (this.peek().type === 9 /* KeywordElse */) {
|
|
1808
|
+
this.advance();
|
|
1809
|
+
alternate = [];
|
|
1810
|
+
while (this.peek().type !== 135 /* Newline */ && this.peek().type !== 136 /* EOF */) {
|
|
1811
|
+
const stmt = this.parseStatement(false);
|
|
1812
|
+
if (stmt) alternate.push(stmt);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
return {
|
|
1816
|
+
type: "IfStatement",
|
|
1817
|
+
condition,
|
|
1818
|
+
consequent,
|
|
1819
|
+
alternate
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
this.skipNewlines();
|
|
1823
|
+
while (!this.isAtEndTerminator() && this.peek().type !== 9 /* KeywordElse */ && this.peek().type !== 8 /* KeywordElseIf */ && this.peek().type !== 136 /* EOF */) {
|
|
1824
|
+
const stmt = this.parseStatement();
|
|
1825
|
+
if (stmt) consequent.push(stmt);
|
|
1826
|
+
this.skipNewlines();
|
|
1827
|
+
}
|
|
1828
|
+
if (this.peek().type === 8 /* KeywordElseIf */) {
|
|
1829
|
+
alternate = this.parseIfStatement();
|
|
1830
|
+
} else if (this.match(9 /* KeywordElse */)) {
|
|
1831
|
+
this.skipNewlines();
|
|
1832
|
+
alternate = [];
|
|
1833
|
+
while (!this.isAtEndTerminator() && this.peek().type !== 136 /* EOF */) {
|
|
1834
|
+
const stmt = this.parseStatement();
|
|
1835
|
+
if (stmt) alternate.push(stmt);
|
|
1836
|
+
this.skipNewlines();
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
if (this.peek().type === 10 /* KeywordEnd */) {
|
|
1840
|
+
this.advance();
|
|
1841
|
+
if (!this.match(6 /* KeywordIf */)) {
|
|
1842
|
+
this.throwError(`Parse error: Expected 'If' after 'End' at line ${this.peek().line}`);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
return {
|
|
1846
|
+
type: "IfStatement",
|
|
1847
|
+
condition,
|
|
1848
|
+
consequent,
|
|
1849
|
+
alternate
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
parseDoWhileStatement() {
|
|
1853
|
+
this.advance();
|
|
1854
|
+
let conditionType;
|
|
1855
|
+
let conditionPosition;
|
|
1856
|
+
let condition;
|
|
1857
|
+
if (this.peek().type === 12 /* KeywordWhile */) {
|
|
1858
|
+
this.advance();
|
|
1859
|
+
conditionType = "while";
|
|
1860
|
+
conditionPosition = "pre";
|
|
1861
|
+
condition = this.parseExpression();
|
|
1862
|
+
} else if (this.peek().type === 15 /* KeywordUntil */) {
|
|
1863
|
+
this.advance();
|
|
1864
|
+
conditionType = "until";
|
|
1865
|
+
conditionPosition = "pre";
|
|
1866
|
+
condition = this.parseExpression();
|
|
1867
|
+
}
|
|
1868
|
+
this.skipNewlines();
|
|
1869
|
+
const body = [];
|
|
1870
|
+
while (this.peek().type !== 14 /* KeywordLoop */ && this.peek().type !== 136 /* EOF */ && !this.isAtEndTerminator()) {
|
|
1871
|
+
const stmt = this.parseStatement();
|
|
1872
|
+
if (stmt) body.push(stmt);
|
|
1873
|
+
this.skipNewlines();
|
|
1874
|
+
}
|
|
1875
|
+
if (!this.match(14 /* KeywordLoop */)) {
|
|
1876
|
+
this.throwError(`Parse error: Expected 'Loop' at line ${this.peek().line} `);
|
|
1877
|
+
}
|
|
1878
|
+
if (this.peek().type === 12 /* KeywordWhile */) {
|
|
1879
|
+
this.advance();
|
|
1880
|
+
conditionType = "while";
|
|
1881
|
+
conditionPosition = "post";
|
|
1882
|
+
condition = this.parseExpression();
|
|
1883
|
+
} else if (this.peek().type === 15 /* KeywordUntil */) {
|
|
1884
|
+
this.advance();
|
|
1885
|
+
conditionType = "until";
|
|
1886
|
+
conditionPosition = "post";
|
|
1887
|
+
condition = this.parseExpression();
|
|
1888
|
+
}
|
|
1889
|
+
return {
|
|
1890
|
+
type: "DoWhileStatement",
|
|
1891
|
+
conditionType,
|
|
1892
|
+
conditionPosition,
|
|
1893
|
+
condition,
|
|
1894
|
+
body
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
parseWhileStatement() {
|
|
1898
|
+
this.advance();
|
|
1899
|
+
const condition = this.parseExpression();
|
|
1900
|
+
this.skipNewlines();
|
|
1901
|
+
const body = [];
|
|
1902
|
+
while (this.peek().type !== 13 /* KeywordWend */ && this.peek().type !== 136 /* EOF */ && !this.isAtEndTerminator()) {
|
|
1903
|
+
const stmt = this.parseStatement();
|
|
1904
|
+
if (stmt) body.push(stmt);
|
|
1905
|
+
this.skipNewlines();
|
|
1906
|
+
}
|
|
1907
|
+
if (!this.match(13 /* KeywordWend */)) {
|
|
1908
|
+
this.throwError(`Parse error: Expected 'Wend' at line ${this.peek().line}`);
|
|
1909
|
+
}
|
|
1910
|
+
return { type: "WhileStatement", condition, body };
|
|
1911
|
+
}
|
|
1912
|
+
parseSelectCaseStatement() {
|
|
1913
|
+
this.advance();
|
|
1914
|
+
if (!this.match(54 /* KeywordCase */)) {
|
|
1915
|
+
this.throwError(`Parse error: Expected 'Case' after 'Select' at line ${this.peek().line}`);
|
|
1916
|
+
}
|
|
1917
|
+
const expression = this.parseExpression();
|
|
1918
|
+
this.skipNewlines();
|
|
1919
|
+
const cases = [];
|
|
1920
|
+
let elseBody = null;
|
|
1921
|
+
while (!this.isAtEndTerminator() && this.peek().type !== 136 /* EOF */) {
|
|
1922
|
+
if (this.peek().type !== 54 /* KeywordCase */) {
|
|
1923
|
+
this.throwError(`Parse error: Expected 'Case' in Select Case at line ${this.peek().line}`);
|
|
1924
|
+
}
|
|
1925
|
+
this.advance();
|
|
1926
|
+
if (this.peek().type === 9 /* KeywordElse */) {
|
|
1927
|
+
this.advance();
|
|
1928
|
+
this.skipNewlines();
|
|
1929
|
+
elseBody = [];
|
|
1930
|
+
while (!this.isAtEndTerminator() && this.peek().type !== 54 /* KeywordCase */ && this.peek().type !== 136 /* EOF */) {
|
|
1931
|
+
const stmt = this.parseStatement();
|
|
1932
|
+
if (stmt) elseBody.push(stmt);
|
|
1933
|
+
this.skipNewlines();
|
|
1934
|
+
}
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1937
|
+
const ranges = [];
|
|
1938
|
+
ranges.push(this.parseRangeClause());
|
|
1939
|
+
while (this.match(126 /* OperatorComma */)) {
|
|
1940
|
+
ranges.push(this.parseRangeClause());
|
|
1941
|
+
}
|
|
1942
|
+
this.skipNewlines();
|
|
1943
|
+
const body = [];
|
|
1944
|
+
while (!this.isAtEndTerminator() && this.peek().type !== 54 /* KeywordCase */ && this.peek().type !== 136 /* EOF */) {
|
|
1945
|
+
const stmt = this.parseStatement();
|
|
1946
|
+
if (stmt) body.push(stmt);
|
|
1947
|
+
this.skipNewlines();
|
|
1948
|
+
}
|
|
1949
|
+
cases.push({ ranges, body });
|
|
1950
|
+
}
|
|
1951
|
+
if (this.peek().type === 10 /* KeywordEnd */) {
|
|
1952
|
+
this.advance();
|
|
1953
|
+
if (!this.match(53 /* KeywordSelect */)) {
|
|
1954
|
+
this.throwError(`Parse error: Expected 'Select' after 'End' at line ${this.peek().line}`);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
return { type: "SelectCaseStatement", expression, cases, elseBody };
|
|
1958
|
+
}
|
|
1959
|
+
parseWithStatement() {
|
|
1960
|
+
this.advance();
|
|
1961
|
+
const expression = this.parseExpression();
|
|
1962
|
+
this.skipNewlines();
|
|
1963
|
+
const body = [];
|
|
1964
|
+
while (!this.isAtEndTerminator() && this.peek().type !== 136 /* EOF */) {
|
|
1965
|
+
const stmt = this.parseStatement();
|
|
1966
|
+
if (stmt) body.push(stmt);
|
|
1967
|
+
this.skipNewlines();
|
|
1968
|
+
}
|
|
1969
|
+
if (this.peek().type === 10 /* KeywordEnd */) {
|
|
1970
|
+
this.advance();
|
|
1971
|
+
if (!this.match(61 /* KeywordWith */)) {
|
|
1972
|
+
this.throwError(`Parse error: Expected 'With' after 'End' at line ${this.peek().line}`);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
return { type: "WithStatement", expression, body };
|
|
1976
|
+
}
|
|
1977
|
+
parseRangeClause() {
|
|
1978
|
+
const isKeyword = this.peek().type === 51 /* KeywordIs */;
|
|
1979
|
+
if (isKeyword) this.advance();
|
|
1980
|
+
const compOp = this.peek();
|
|
1981
|
+
if (compOp.type === 120 /* OperatorEquals */ || compOp.type === 121 /* OperatorNotEquals */ || compOp.type === 122 /* OperatorLessThan */ || compOp.type === 123 /* OperatorGreaterThan */ || compOp.type === 124 /* OperatorLessThanOrEqual */ || compOp.type === 125 /* OperatorGreaterThanOrEqual */) {
|
|
1982
|
+
this.advance();
|
|
1983
|
+
const value = this.parseExpression();
|
|
1984
|
+
return { kind: "comparison", operator: compOp.value, value };
|
|
1985
|
+
}
|
|
1986
|
+
if (isKeyword) {
|
|
1987
|
+
this.throwError(`Parse error: Expected comparison operator after 'Is' at line ${this.peek().line}`);
|
|
1988
|
+
}
|
|
1989
|
+
const startExpr = this.parseExpression();
|
|
1990
|
+
if (this.match(4 /* KeywordTo */)) {
|
|
1991
|
+
const endExpr = this.parseExpression();
|
|
1992
|
+
return { kind: "to", start: startExpr, end: endExpr };
|
|
1993
|
+
}
|
|
1994
|
+
return { kind: "expression", value: startExpr };
|
|
1995
|
+
}
|
|
1996
|
+
parseExpression() {
|
|
1997
|
+
return this.parseLogicalImp();
|
|
1998
|
+
}
|
|
1999
|
+
parseLogicalImp() {
|
|
2000
|
+
let left = this.parseLogicalEqv();
|
|
2001
|
+
while (this.peek().type === 66 /* KeywordImp */) {
|
|
2002
|
+
const operator = this.advance().value;
|
|
2003
|
+
const right = this.parseLogicalEqv();
|
|
2004
|
+
left = { type: "BinaryExpression", operator, left, right, loc: this.makeBinaryLoc(left, right) };
|
|
2005
|
+
}
|
|
2006
|
+
return left;
|
|
2007
|
+
}
|
|
2008
|
+
parseLogicalEqv() {
|
|
2009
|
+
let left = this.parseLogicalXor();
|
|
2010
|
+
while (this.peek().type === 65 /* KeywordEqv */) {
|
|
2011
|
+
const operator = this.advance().value;
|
|
2012
|
+
const right = this.parseLogicalXor();
|
|
2013
|
+
left = { type: "BinaryExpression", operator, left, right, loc: this.makeBinaryLoc(left, right) };
|
|
2014
|
+
}
|
|
2015
|
+
return left;
|
|
2016
|
+
}
|
|
2017
|
+
parseLogicalXor() {
|
|
2018
|
+
let left = this.parseLogicalOr();
|
|
2019
|
+
while (this.peek().type === 64 /* KeywordXor */) {
|
|
2020
|
+
const operator = this.advance().value;
|
|
2021
|
+
const right = this.parseLogicalOr();
|
|
2022
|
+
left = { type: "BinaryExpression", operator, left, right, loc: this.makeBinaryLoc(left, right) };
|
|
2023
|
+
}
|
|
2024
|
+
return left;
|
|
2025
|
+
}
|
|
2026
|
+
parseLogicalOr() {
|
|
2027
|
+
let left = this.parseLogicalAnd();
|
|
2028
|
+
while (this.peek().type === 27 /* KeywordOr */) {
|
|
2029
|
+
const operator = this.advance().value;
|
|
2030
|
+
const right = this.parseLogicalAnd();
|
|
2031
|
+
left = { type: "BinaryExpression", operator, left, right, loc: this.makeBinaryLoc(left, right) };
|
|
2032
|
+
}
|
|
2033
|
+
return left;
|
|
2034
|
+
}
|
|
2035
|
+
parseLogicalAnd() {
|
|
2036
|
+
let left = this.parseLogicalNot();
|
|
2037
|
+
while (this.peek().type === 26 /* KeywordAnd */) {
|
|
2038
|
+
const operator = this.advance().value;
|
|
2039
|
+
const right = this.parseLogicalNot();
|
|
2040
|
+
left = { type: "BinaryExpression", operator, left, right, loc: this.makeBinaryLoc(left, right) };
|
|
2041
|
+
}
|
|
2042
|
+
return left;
|
|
2043
|
+
}
|
|
2044
|
+
parseLogicalNot() {
|
|
2045
|
+
if (this.peek().type === 28 /* KeywordNot */) {
|
|
2046
|
+
const notTok = this.tokens[this.pos];
|
|
2047
|
+
this.advance();
|
|
2048
|
+
const argument = this.parseEquality();
|
|
2049
|
+
const node = { type: "UnaryExpression", operator: "Not", argument };
|
|
2050
|
+
node.loc = { start: { line: notTok.line, column: notTok.column }, end: argument.loc?.end ?? { line: notTok.line, column: notTok.column } };
|
|
2051
|
+
return node;
|
|
2052
|
+
}
|
|
2053
|
+
return this.parseEquality();
|
|
2054
|
+
}
|
|
2055
|
+
makeBinaryLoc(left, right) {
|
|
2056
|
+
if (!left.loc || !right.loc) return void 0;
|
|
2057
|
+
return { start: left.loc.start, end: right.loc.end };
|
|
2058
|
+
}
|
|
2059
|
+
parseEquality() {
|
|
2060
|
+
let left = this.parseRelational();
|
|
2061
|
+
while (this.peek().type === 120 /* OperatorEquals */ || this.peek().type === 121 /* OperatorNotEquals */ || this.peek().type === 51 /* KeywordIs */ || this.peek().type === 63 /* KeywordLike */) {
|
|
2062
|
+
const operator = this.advance().value;
|
|
2063
|
+
const right = this.parseRelational();
|
|
2064
|
+
left = { type: "BinaryExpression", operator, left, right, loc: this.makeBinaryLoc(left, right) };
|
|
2065
|
+
}
|
|
2066
|
+
return left;
|
|
2067
|
+
}
|
|
2068
|
+
parseRelational() {
|
|
2069
|
+
let left = this.parseConcatenation();
|
|
2070
|
+
while (this.peek().type === 122 /* OperatorLessThan */ || this.peek().type === 123 /* OperatorGreaterThan */ || this.peek().type === 124 /* OperatorLessThanOrEqual */ || this.peek().type === 125 /* OperatorGreaterThanOrEqual */) {
|
|
2071
|
+
const operator = this.advance().value;
|
|
2072
|
+
const right = this.parseConcatenation();
|
|
2073
|
+
left = { type: "BinaryExpression", operator, left, right, loc: this.makeBinaryLoc(left, right) };
|
|
2074
|
+
}
|
|
2075
|
+
return left;
|
|
2076
|
+
}
|
|
2077
|
+
parseConcatenation() {
|
|
2078
|
+
let left = this.parseAdditive();
|
|
2079
|
+
while (this.peek().type === 117 /* OperatorAmpersand */) {
|
|
2080
|
+
const operator = this.advance().value;
|
|
2081
|
+
const right = this.parseAdditive();
|
|
2082
|
+
left = { type: "BinaryExpression", operator, left, right, loc: this.makeBinaryLoc(left, right) };
|
|
2083
|
+
}
|
|
2084
|
+
return left;
|
|
2085
|
+
}
|
|
2086
|
+
parseAdditive() {
|
|
2087
|
+
let left = this.parseModulo();
|
|
2088
|
+
while (this.peek().type === 112 /* OperatorPlus */ || this.peek().type === 113 /* OperatorMinus */) {
|
|
2089
|
+
const operator = this.advance().value;
|
|
2090
|
+
const right = this.parseModulo();
|
|
2091
|
+
left = { type: "BinaryExpression", operator, left, right, loc: this.makeBinaryLoc(left, right) };
|
|
2092
|
+
}
|
|
2093
|
+
return left;
|
|
2094
|
+
}
|
|
2095
|
+
parseModulo() {
|
|
2096
|
+
let left = this.parseIntDivision();
|
|
2097
|
+
while (this.peek().type === 118 /* KeywordMod */) {
|
|
2098
|
+
const operator = this.advance().value;
|
|
2099
|
+
const right = this.parseIntDivision();
|
|
2100
|
+
left = { type: "BinaryExpression", operator, left, right, loc: this.makeBinaryLoc(left, right) };
|
|
2101
|
+
}
|
|
2102
|
+
return left;
|
|
2103
|
+
}
|
|
2104
|
+
parseIntDivision() {
|
|
2105
|
+
let left = this.parseMultiplicative();
|
|
2106
|
+
while (this.peek().type === 116 /* OperatorIntDivide */) {
|
|
2107
|
+
const operator = this.advance().value;
|
|
2108
|
+
const right = this.parseMultiplicative();
|
|
2109
|
+
left = { type: "BinaryExpression", operator, left, right, loc: this.makeBinaryLoc(left, right) };
|
|
2110
|
+
}
|
|
2111
|
+
return left;
|
|
2112
|
+
}
|
|
2113
|
+
parseMultiplicative() {
|
|
2114
|
+
let left = this.parseUnary();
|
|
2115
|
+
while (this.peek().type === 114 /* OperatorMultiply */ || this.peek().type === 115 /* OperatorDivide */) {
|
|
2116
|
+
const operator = this.advance().value;
|
|
2117
|
+
const right = this.parseUnary();
|
|
2118
|
+
left = { type: "BinaryExpression", operator, left, right, loc: this.makeBinaryLoc(left, right) };
|
|
2119
|
+
}
|
|
2120
|
+
return left;
|
|
2121
|
+
}
|
|
2122
|
+
parseUnary() {
|
|
2123
|
+
if (this.peek().type === 113 /* OperatorMinus */ || this.peek().type === 112 /* OperatorPlus */) {
|
|
2124
|
+
const opTok = this.tokens[this.pos];
|
|
2125
|
+
const operator = this.advance().value;
|
|
2126
|
+
const argument = this.parseUnary();
|
|
2127
|
+
const node = { type: "UnaryExpression", operator, argument };
|
|
2128
|
+
node.loc = { start: { line: opTok.line, column: opTok.column }, end: argument.loc?.end ?? { line: opTok.line, column: opTok.column } };
|
|
2129
|
+
return node;
|
|
2130
|
+
}
|
|
2131
|
+
return this.parseExponentiation();
|
|
2132
|
+
}
|
|
2133
|
+
parseExponentiation() {
|
|
2134
|
+
let left = this.parsePrimary();
|
|
2135
|
+
while (this.peek().type === 119 /* OperatorPower */) {
|
|
2136
|
+
const operator = this.advance().value;
|
|
2137
|
+
const right = this.parsePrimary();
|
|
2138
|
+
left = { type: "BinaryExpression", operator, left, right, loc: this.makeBinaryLoc(left, right) };
|
|
2139
|
+
}
|
|
2140
|
+
return left;
|
|
2141
|
+
}
|
|
2142
|
+
parsePrimary(stopBeforeSpacedLParen = false) {
|
|
2143
|
+
const startTok = this.tokens[this.pos];
|
|
2144
|
+
const token = this.advance();
|
|
2145
|
+
let expr;
|
|
2146
|
+
if (token.type === 1 /* Number */) {
|
|
2147
|
+
const m = token.value.match(/[%&@!#^]$/);
|
|
2148
|
+
const typeSuffix = m ? m[0] : void 0;
|
|
2149
|
+
const cleanVal = token.value.replace(/[%&@!#^]$/, "");
|
|
2150
|
+
const isFloat = !cleanVal.startsWith("0x") && !cleanVal.startsWith("0o") && /[.eE]/.test(cleanVal) ? true : void 0;
|
|
2151
|
+
expr = { type: "NumberLiteral", value: Number(cleanVal), typeSuffix, isFloat };
|
|
2152
|
+
} else if (token.type === 2 /* String */) {
|
|
2153
|
+
expr = { type: "StringLiteral", value: token.value };
|
|
2154
|
+
} else if (token.type === 134 /* Date */) {
|
|
2155
|
+
expr = { type: "DateLiteral", value: token.value };
|
|
2156
|
+
} else if (token.type === 138 /* ForeignName */) {
|
|
2157
|
+
expr = { type: "Identifier", name: token.value, foreign: true };
|
|
2158
|
+
} else if (token.type === 0 /* Identifier */ || _Parser.CONTEXTUAL_KW.has(token.type) || _Parser.COMPAT_KW_EXPR.has(token.type)) {
|
|
2159
|
+
expr = { type: "Identifier", name: token.value };
|
|
2160
|
+
} else if (token.type === 110 /* KeywordAddressOf */) {
|
|
2161
|
+
const procName = this.consume(0 /* Identifier */, "Expected procedure name after 'AddressOf'");
|
|
2162
|
+
expr = { type: "AddressOfExpression", procedureName: { type: "Identifier", name: procName.value } };
|
|
2163
|
+
} else if (token.type === 44 /* KeywordEmpty */) {
|
|
2164
|
+
expr = { type: "Identifier", name: token.value };
|
|
2165
|
+
} else if (token.type === 49 /* KeywordNothing */) {
|
|
2166
|
+
expr = { type: "Identifier", name: "Nothing" };
|
|
2167
|
+
} else if (token.type === 67 /* KeywordNull */) {
|
|
2168
|
+
expr = { type: "Identifier", name: "Null" };
|
|
2169
|
+
} else if (token.type === 70 /* KeywordMe */) {
|
|
2170
|
+
expr = { type: "Identifier", name: "Me" };
|
|
2171
|
+
} else if (token.type === 35 /* KeywordError */) {
|
|
2172
|
+
expr = { type: "Identifier", name: "Error" };
|
|
2173
|
+
} else if (token.type === 24 /* KeywordNew */) {
|
|
2174
|
+
const classNameToken = this.advance();
|
|
2175
|
+
if (!this.isNameToken(classNameToken)) {
|
|
2176
|
+
this.throwError(`Parse error: Expected class name after 'New' at line ${classNameToken.line}`);
|
|
2177
|
+
}
|
|
2178
|
+
let className = classNameToken.value;
|
|
2179
|
+
if (this.peek().type === 129 /* OperatorDot */) {
|
|
2180
|
+
this.advance();
|
|
2181
|
+
className += "." + this.advance().value;
|
|
2182
|
+
}
|
|
2183
|
+
expr = { type: "NewExpression", className };
|
|
2184
|
+
} else if (token.type === 127 /* OperatorLParen */) {
|
|
2185
|
+
const innerExpr = this.parseExpression();
|
|
2186
|
+
if (!this.match(128 /* OperatorRParen */)) {
|
|
2187
|
+
this.throwMissingRParen();
|
|
2188
|
+
}
|
|
2189
|
+
expr = { type: "ParenthesizedExpression", expression: innerExpr };
|
|
2190
|
+
} else if (token.type === 62 /* KeywordTypeOf */) {
|
|
2191
|
+
expr = this.parseRelational();
|
|
2192
|
+
if (!this.match(51 /* KeywordIs */)) {
|
|
2193
|
+
this.throwError(`Parse error: Expected 'Is' after 'TypeOf' at line ${this.peek().line}`);
|
|
2194
|
+
}
|
|
2195
|
+
const typeToken = this.advance();
|
|
2196
|
+
if (typeToken.type !== 0 /* Identifier */ && typeToken.type !== 25 /* KeywordCollection */) {
|
|
2197
|
+
this.throwError(`Parse error: Expected type name after 'Is' at line ${typeToken.line}`);
|
|
2198
|
+
}
|
|
2199
|
+
expr = { type: "TypeOfIsExpression", expression: expr, typeName: typeToken.value };
|
|
2200
|
+
} else if (token.type === 129 /* OperatorDot */) {
|
|
2201
|
+
const propToken = this.advance();
|
|
2202
|
+
if (!this.isNameToken(propToken)) {
|
|
2203
|
+
this.throwError(`Parse error: Expected identifier after '.' at line ${propToken.line}`);
|
|
2204
|
+
}
|
|
2205
|
+
const property = { type: "Identifier", name: propToken.value };
|
|
2206
|
+
expr = { type: "ImplicitWithObjectExpression", property };
|
|
2207
|
+
} else if (token.type === 135 /* Newline */) {
|
|
2208
|
+
const prevToken = this.tokens[Math.max(0, this.pos - 2)];
|
|
2209
|
+
if (prevToken && this.isContinuationEndToken(prevToken.type)) {
|
|
2210
|
+
this.throwError(
|
|
2211
|
+
`\u884C\u7D99\u7D9A\u6587\u5B57 '_' \u304C\u5FC5\u8981\u3067\u3059\uFF08'${prevToken.value}' \u306E\u5F8C\u3067\u6539\u884C\u3055\u308C\u3066\u3044\u307E\u3059\uFF09`,
|
|
2212
|
+
token
|
|
2213
|
+
);
|
|
2214
|
+
} else {
|
|
2215
|
+
this.throwError(`Parse error: Unexpected token in expression '${this.tokenDisplay(token.value)}' at line ${token.line} `, token);
|
|
2216
|
+
}
|
|
2217
|
+
} else {
|
|
2218
|
+
this.throwError(`Parse error: Unexpected token in expression '${this.tokenDisplay(token.value)}' at line ${token.line} `, token);
|
|
2219
|
+
}
|
|
2220
|
+
expr.loc = this.exprLoc(startTok, this.tokens[this.pos - 1]);
|
|
2221
|
+
while (true) {
|
|
2222
|
+
if (this.match(129 /* OperatorDot */)) {
|
|
2223
|
+
if (this.peek().type === 136 /* EOF */) this.throwError("Expected property name after '.'");
|
|
2224
|
+
const propToken = this.advance();
|
|
2225
|
+
const property = { type: "Identifier", name: propToken.value };
|
|
2226
|
+
expr = { type: "MemberExpression", object: expr, property };
|
|
2227
|
+
expr.loc = this.exprLoc(startTok, this.tokens[this.pos - 1]);
|
|
2228
|
+
} else if (this.match(132 /* OperatorExclamation */)) {
|
|
2229
|
+
if (this.peek().type === 136 /* EOF */) this.throwError("Expected identifier after '!'");
|
|
2230
|
+
const propToken = this.advance();
|
|
2231
|
+
const property = { type: "Identifier", name: propToken.value };
|
|
2232
|
+
expr = { type: "DictionaryAccessExpression", object: expr, property };
|
|
2233
|
+
expr.loc = this.exprLoc(startTok, this.tokens[this.pos - 1]);
|
|
2234
|
+
} else if (this.peek().type === 127 /* OperatorLParen */) {
|
|
2235
|
+
if (stopBeforeSpacedLParen && this.hasSpaceBeforeCurrentToken()) {
|
|
2236
|
+
break;
|
|
2237
|
+
}
|
|
2238
|
+
this.advance();
|
|
2239
|
+
const args2 = [];
|
|
2240
|
+
if (this.peek().type !== 128 /* OperatorRParen */) {
|
|
2241
|
+
args2.push(this.parseCallArgument());
|
|
2242
|
+
while (this.match(126 /* OperatorComma */)) {
|
|
2243
|
+
args2.push(this.parseCallArgument());
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
if (!this.match(128 /* OperatorRParen */)) {
|
|
2247
|
+
this.throwMissingRParen();
|
|
2248
|
+
}
|
|
2249
|
+
expr = { type: "CallExpression", callee: expr, args: args2 };
|
|
2250
|
+
expr.loc = this.exprLoc(startTok, this.tokens[this.pos - 1]);
|
|
2251
|
+
} else {
|
|
2252
|
+
break;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
return expr;
|
|
2256
|
+
}
|
|
2257
|
+
// Parse a call argument, handling named arguments (e.g., shift:=xlUp)
|
|
2258
|
+
parseCallArgument() {
|
|
2259
|
+
const next = this.peek().type;
|
|
2260
|
+
if (next === 126 /* OperatorComma */ || next === 128 /* OperatorRParen */ || this.isAtTerminator()) {
|
|
2261
|
+
return { type: "MissingArgument" };
|
|
2262
|
+
}
|
|
2263
|
+
if (this.pos + 1 < this.tokens.length && this.tokens[this.pos + 1].type === 131 /* OperatorColonEquals */ && typeof this.peek().value === "string" && /^[A-Za-z_]\w*$/.test(this.peek().value)) {
|
|
2264
|
+
const nameToken = this.advance();
|
|
2265
|
+
this.advance();
|
|
2266
|
+
const value = this.parseExpression();
|
|
2267
|
+
return { type: "NamedArgument", name: nameToken.value, value };
|
|
2268
|
+
}
|
|
2269
|
+
return this.parseExpression();
|
|
2270
|
+
}
|
|
2271
|
+
parseOnGoToSubStatement() {
|
|
2272
|
+
this.advance();
|
|
2273
|
+
const expression = this.parseExpression();
|
|
2274
|
+
let isGoSub = false;
|
|
2275
|
+
if (this.match(36 /* KeywordGoTo */)) {
|
|
2276
|
+
isGoSub = false;
|
|
2277
|
+
} else if (this.match(37 /* KeywordGoSub */)) {
|
|
2278
|
+
isGoSub = true;
|
|
2279
|
+
} else {
|
|
2280
|
+
this.throwError(`Parse error: Expected 'GoTo' or 'GoSub' after 'On' expression at line ${this.peek().line}`);
|
|
2281
|
+
}
|
|
2282
|
+
const labels = [];
|
|
2283
|
+
while (true) {
|
|
2284
|
+
const labelToken = this.advance();
|
|
2285
|
+
if (labelToken.type !== 0 /* Identifier */ && labelToken.type !== 1 /* Number */) {
|
|
2286
|
+
this.throwError(`Parse error: Expected label (identifier or number) in On...GoTo/GoSub at line ${labelToken.line}`);
|
|
2287
|
+
}
|
|
2288
|
+
labels.push(labelToken.value);
|
|
2289
|
+
if (!this.match(126 /* OperatorComma */)) {
|
|
2290
|
+
break;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
return { type: "OnGoToSubStatement", expression, isGoSub, labels };
|
|
2294
|
+
}
|
|
2295
|
+
parseGoSubStatement() {
|
|
2296
|
+
this.advance();
|
|
2297
|
+
const labelToken = this.advance();
|
|
2298
|
+
if (labelToken.type !== 0 /* Identifier */ && labelToken.type !== 1 /* Number */) {
|
|
2299
|
+
this.throwError(`Parse error: Expected label after GoSub at line ${labelToken.line}`);
|
|
2300
|
+
}
|
|
2301
|
+
return { type: "GoSubStatement", label: labelToken.value };
|
|
2302
|
+
}
|
|
2303
|
+
parseLSetStatement() {
|
|
2304
|
+
this.advance();
|
|
2305
|
+
const left = this.parsePrimary();
|
|
2306
|
+
if (!this.match(120 /* OperatorEquals */)) {
|
|
2307
|
+
this.throwError(`Parse error: Expected '=' in LSet statement at line ${this.peek().line}`);
|
|
2308
|
+
}
|
|
2309
|
+
const right = this.parseExpression();
|
|
2310
|
+
return { type: "LSetStatement", left, right };
|
|
2311
|
+
}
|
|
2312
|
+
parseRSetStatement() {
|
|
2313
|
+
this.advance();
|
|
2314
|
+
const left = this.parsePrimary();
|
|
2315
|
+
if (!this.match(120 /* OperatorEquals */)) {
|
|
2316
|
+
this.throwError(`Parse error: Expected '=' in RSet statement at line ${this.peek().line}`);
|
|
2317
|
+
}
|
|
2318
|
+
const right = this.parseExpression();
|
|
2319
|
+
return { type: "RSetStatement", left, right };
|
|
2320
|
+
}
|
|
2321
|
+
parseErrorStatement() {
|
|
2322
|
+
this.advance();
|
|
2323
|
+
const errorNumber = this.parseExpression();
|
|
2324
|
+
return { type: "ErrorStatement", errorNumber };
|
|
2325
|
+
}
|
|
2326
|
+
parseEventDeclaration(scope) {
|
|
2327
|
+
this.advance();
|
|
2328
|
+
const idToken = this.consume(0 /* Identifier */, "Expected identifier after 'Event'");
|
|
2329
|
+
const name = { type: "Identifier", name: idToken.value };
|
|
2330
|
+
const parameters = [];
|
|
2331
|
+
if (this.match(127 /* OperatorLParen */)) {
|
|
2332
|
+
if (this.peek().type !== 128 /* OperatorRParen */) {
|
|
2333
|
+
parameters.push(this.parseParameter());
|
|
2334
|
+
while (this.match(126 /* OperatorComma */)) {
|
|
2335
|
+
parameters.push(this.parseParameter());
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
this.consume(128 /* OperatorRParen */, "Expected ')' after event parameters");
|
|
2339
|
+
}
|
|
2340
|
+
return { type: "EventDeclaration", name, parameters, scope };
|
|
2341
|
+
}
|
|
2342
|
+
parseRaiseEventStatement() {
|
|
2343
|
+
this.advance();
|
|
2344
|
+
const idToken = this.consume(0 /* Identifier */, "Expected identifier after 'RaiseEvent'");
|
|
2345
|
+
const eventName = { type: "Identifier", name: idToken.value };
|
|
2346
|
+
const args2 = [];
|
|
2347
|
+
if (this.match(127 /* OperatorLParen */)) {
|
|
2348
|
+
if (this.peek().type !== 128 /* OperatorRParen */) {
|
|
2349
|
+
args2.push(this.parseExpression());
|
|
2350
|
+
while (this.match(126 /* OperatorComma */)) {
|
|
2351
|
+
args2.push(this.parseExpression());
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
this.consume(128 /* OperatorRParen */, "Expected ')' after RaiseEvent arguments");
|
|
2355
|
+
}
|
|
2356
|
+
return { type: "RaiseEventStatement", eventName, args: args2 };
|
|
2357
|
+
}
|
|
2358
|
+
parseImplementsDirective() {
|
|
2359
|
+
this.advance();
|
|
2360
|
+
const idToken = this.consume(0 /* Identifier */, "Expected interface name after 'Implements'");
|
|
2361
|
+
return { type: "ImplementsDirective", interfaceName: idToken.value };
|
|
2362
|
+
}
|
|
2363
|
+
parseAppActivateStatement() {
|
|
2364
|
+
this.advance();
|
|
2365
|
+
const title = this.parseExpression();
|
|
2366
|
+
let wait;
|
|
2367
|
+
if (this.match(126 /* OperatorComma */)) {
|
|
2368
|
+
wait = this.parseExpression();
|
|
2369
|
+
}
|
|
2370
|
+
return { type: "AppActivateStatement", title, wait };
|
|
2371
|
+
}
|
|
2372
|
+
parseSendKeysStatement() {
|
|
2373
|
+
this.advance();
|
|
2374
|
+
const keys = this.parseExpression();
|
|
2375
|
+
let wait;
|
|
2376
|
+
if (this.match(126 /* OperatorComma */)) {
|
|
2377
|
+
wait = this.parseExpression();
|
|
2378
|
+
}
|
|
2379
|
+
return { type: "SendKeysStatement", keys, wait };
|
|
2380
|
+
}
|
|
2381
|
+
parseLockStatement() {
|
|
2382
|
+
this.advance();
|
|
2383
|
+
this.match(111 /* OperatorHash */);
|
|
2384
|
+
const fileNumber = this.parseExpression();
|
|
2385
|
+
let recordRange;
|
|
2386
|
+
if (this.match(126 /* OperatorComma */)) {
|
|
2387
|
+
const start = this.parseExpression();
|
|
2388
|
+
let end;
|
|
2389
|
+
if (this.match(4 /* KeywordTo */)) {
|
|
2390
|
+
end = this.parseExpression();
|
|
2391
|
+
}
|
|
2392
|
+
recordRange = { start, end };
|
|
2393
|
+
}
|
|
2394
|
+
return { type: "LockStatement", fileNumber, recordRange };
|
|
2395
|
+
}
|
|
2396
|
+
parseUnlockStatement() {
|
|
2397
|
+
this.advance();
|
|
2398
|
+
this.match(111 /* OperatorHash */);
|
|
2399
|
+
const fileNumber = this.parseExpression();
|
|
2400
|
+
let recordRange;
|
|
2401
|
+
if (this.match(126 /* OperatorComma */)) {
|
|
2402
|
+
const start = this.parseExpression();
|
|
2403
|
+
let end;
|
|
2404
|
+
if (this.match(4 /* KeywordTo */)) {
|
|
2405
|
+
end = this.parseExpression();
|
|
2406
|
+
}
|
|
2407
|
+
recordRange = { start, end };
|
|
2408
|
+
}
|
|
2409
|
+
return { type: "UnlockStatement", fileNumber, recordRange };
|
|
2410
|
+
}
|
|
2411
|
+
parseWidthStatement() {
|
|
2412
|
+
this.advance();
|
|
2413
|
+
this.consume(111 /* OperatorHash */, "Expected '#' after Width");
|
|
2414
|
+
const fileNumber = this.parseExpression();
|
|
2415
|
+
this.consume(126 /* OperatorComma */, "Expected ',' after file number");
|
|
2416
|
+
const width = this.parseExpression();
|
|
2417
|
+
return { type: "WidthStatement", fileNumber, width };
|
|
2418
|
+
}
|
|
2419
|
+
};
|
|
2420
|
+
|
|
2421
|
+
// ../../test-libs/vba-parse-check.ts
|
|
2422
|
+
var fs = __toESM(require("fs"), 1);
|
|
2423
|
+
var path = __toESM(require("path"), 1);
|
|
2424
|
+
function checkFile(filePath) {
|
|
2425
|
+
const src = fs.readFileSync(filePath, "utf8");
|
|
2426
|
+
const diags = [];
|
|
2427
|
+
const lexer = new Lexer(src);
|
|
2428
|
+
let tokens;
|
|
2429
|
+
try {
|
|
2430
|
+
tokens = lexer.tokenize();
|
|
2431
|
+
} catch (e) {
|
|
2432
|
+
if (e instanceof LexError) {
|
|
2433
|
+
return [{
|
|
2434
|
+
file: filePath,
|
|
2435
|
+
line: e.line,
|
|
2436
|
+
column: e.column,
|
|
2437
|
+
severity: "error",
|
|
2438
|
+
source: "lexer",
|
|
2439
|
+
message: e.message.replace(/ \(line \d+\)$/, "")
|
|
2440
|
+
}];
|
|
2441
|
+
}
|
|
2442
|
+
throw e;
|
|
2443
|
+
}
|
|
2444
|
+
for (const d of lexer.diagnostics) {
|
|
2445
|
+
diags.push({
|
|
2446
|
+
file: filePath,
|
|
2447
|
+
line: d.line,
|
|
2448
|
+
column: d.column,
|
|
2449
|
+
severity: "error",
|
|
2450
|
+
source: "lexer",
|
|
2451
|
+
message: d.message
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
const ast = new Parser(tokens, { errorRecovery: true }).parse();
|
|
2455
|
+
for (const d of ast.diagnostics) {
|
|
2456
|
+
diags.push({
|
|
2457
|
+
file: filePath,
|
|
2458
|
+
line: d.loc.start.line,
|
|
2459
|
+
column: d.loc.start.column,
|
|
2460
|
+
severity: d.severity === "error" ? "error" : "warning",
|
|
2461
|
+
source: "parser",
|
|
2462
|
+
message: d.message
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
return diags;
|
|
2466
|
+
}
|
|
2467
|
+
function collectFiles(target) {
|
|
2468
|
+
const stat = fs.statSync(target);
|
|
2469
|
+
if (stat.isFile()) return [target];
|
|
2470
|
+
return fs.readdirSync(target).filter((f) => /\.(bas|cls|frm)$/i.test(f)).map((f) => path.join(target, f));
|
|
2471
|
+
}
|
|
2472
|
+
var args = process.argv.slice(2);
|
|
2473
|
+
var jsonMode = args.includes("--json");
|
|
2474
|
+
var targets = args.filter((a) => !a.startsWith("--"));
|
|
2475
|
+
if (targets.length === 0) {
|
|
2476
|
+
process.stderr.write("Usage: vba-parse-check <file|dir> [--json]\n");
|
|
2477
|
+
process.exit(1);
|
|
2478
|
+
}
|
|
2479
|
+
var allDiags = [];
|
|
2480
|
+
for (const target of targets) {
|
|
2481
|
+
for (const file of collectFiles(target)) {
|
|
2482
|
+
allDiags.push(...checkFile(file));
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
if (jsonMode) {
|
|
2486
|
+
process.stdout.write(JSON.stringify(allDiags, null, 2) + "\n");
|
|
2487
|
+
} else {
|
|
2488
|
+
if (allDiags.length === 0) {
|
|
2489
|
+
process.stdout.write("No errors found.\n");
|
|
2490
|
+
} else {
|
|
2491
|
+
for (const d of allDiags) {
|
|
2492
|
+
process.stdout.write(
|
|
2493
|
+
`${d.file}:${d.line}:${d.column}: ${d.severity} [${d.source}] ${d.message}
|
|
2494
|
+
`
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
2497
|
+
process.exit(1);
|
|
2498
|
+
}
|
|
2499
|
+
}
|