@flux-lang/core 0.1.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/dist/args.d.ts +18 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +60 -0
- package/dist/args.js.map +1 -0
- package/dist/ast.d.ts +194 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +3 -0
- package/dist/ast.js.map +1 -0
- package/dist/bin/flux.d.ts +2 -0
- package/dist/bin/flux.d.ts.map +1 -0
- package/dist/bin/flux.js +157 -0
- package/dist/bin/flux.js.map +1 -0
- package/dist/checks.d.ts +12 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +98 -0
- package/dist/checks.js.map +1 -0
- package/dist/commands/check.d.ts +2 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +118 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/repl.d.ts +2 -0
- package/dist/commands/repl.d.ts.map +1 -0
- package/dist/commands/repl.js +37 -0
- package/dist/commands/repl.js.map +1 -0
- package/dist/fs-utils.d.ts +4 -0
- package/dist/fs-utils.d.ts.map +1 -0
- package/dist/fs-utils.js +34 -0
- package/dist/fs-utils.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/parse.d.ts +2 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +107 -0
- package/dist/parse.js.map +1 -0
- package/dist/parser.d.ts +3 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +1452 -0
- package/dist/parser.js.map +1 -0
- package/dist/runtime/kernel.d.ts +20 -0
- package/dist/runtime/kernel.d.ts.map +1 -0
- package/dist/runtime/kernel.js +356 -0
- package/dist/runtime/kernel.js.map +1 -0
- package/dist/runtime/model.d.ts +146 -0
- package/dist/runtime/model.d.ts.map +1 -0
- package/dist/runtime/model.js +3 -0
- package/dist/runtime/model.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +33 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,1452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token types for the Flux lexer.
|
|
3
|
+
* We keep keywords mostly as identifiers, except for a few special literals.
|
|
4
|
+
*/
|
|
5
|
+
var TokenType;
|
|
6
|
+
(function (TokenType) {
|
|
7
|
+
// Structural
|
|
8
|
+
TokenType[TokenType["LBrace"] = 0] = "LBrace";
|
|
9
|
+
TokenType[TokenType["RBrace"] = 1] = "RBrace";
|
|
10
|
+
TokenType[TokenType["LBracket"] = 2] = "LBracket";
|
|
11
|
+
TokenType[TokenType["RBracket"] = 3] = "RBracket";
|
|
12
|
+
TokenType[TokenType["LParen"] = 4] = "LParen";
|
|
13
|
+
TokenType[TokenType["RParen"] = 5] = "RParen";
|
|
14
|
+
TokenType[TokenType["Comma"] = 6] = "Comma";
|
|
15
|
+
TokenType[TokenType["Semicolon"] = 7] = "Semicolon";
|
|
16
|
+
TokenType[TokenType["Colon"] = 8] = "Colon";
|
|
17
|
+
TokenType[TokenType["Equals"] = 9] = "Equals";
|
|
18
|
+
TokenType[TokenType["At"] = 10] = "At";
|
|
19
|
+
// Single-char operators / punctuation
|
|
20
|
+
TokenType[TokenType["Dot"] = 11] = "Dot";
|
|
21
|
+
TokenType[TokenType["Greater"] = 12] = "Greater";
|
|
22
|
+
TokenType[TokenType["Less"] = 13] = "Less";
|
|
23
|
+
TokenType[TokenType["Bang"] = 14] = "Bang";
|
|
24
|
+
TokenType[TokenType["Plus"] = 15] = "Plus";
|
|
25
|
+
TokenType[TokenType["Minus"] = 16] = "Minus";
|
|
26
|
+
TokenType[TokenType["Star"] = 17] = "Star";
|
|
27
|
+
TokenType[TokenType["Slash"] = 18] = "Slash";
|
|
28
|
+
TokenType[TokenType["Percent"] = 19] = "Percent";
|
|
29
|
+
// Multi-char operators
|
|
30
|
+
TokenType[TokenType["AndAnd"] = 20] = "AndAnd";
|
|
31
|
+
TokenType[TokenType["OrOr"] = 21] = "OrOr";
|
|
32
|
+
TokenType[TokenType["EqualEqual"] = 22] = "EqualEqual";
|
|
33
|
+
TokenType[TokenType["EqualEqualEqual"] = 23] = "EqualEqualEqual";
|
|
34
|
+
TokenType[TokenType["BangEqual"] = 24] = "BangEqual";
|
|
35
|
+
TokenType[TokenType["BangEqualEqual"] = 25] = "BangEqualEqual";
|
|
36
|
+
TokenType[TokenType["LessEqual"] = 26] = "LessEqual";
|
|
37
|
+
TokenType[TokenType["GreaterEqual"] = 27] = "GreaterEqual";
|
|
38
|
+
// Literals
|
|
39
|
+
TokenType[TokenType["Int"] = 28] = "Int";
|
|
40
|
+
TokenType[TokenType["Float"] = 29] = "Float";
|
|
41
|
+
TokenType[TokenType["String"] = 30] = "String";
|
|
42
|
+
TokenType[TokenType["Bool"] = 31] = "Bool";
|
|
43
|
+
TokenType[TokenType["Inf"] = 32] = "Inf";
|
|
44
|
+
TokenType[TokenType["Identifier"] = 33] = "Identifier";
|
|
45
|
+
TokenType[TokenType["EOF"] = 34] = "EOF";
|
|
46
|
+
})(TokenType || (TokenType = {}));
|
|
47
|
+
/**
|
|
48
|
+
* Simple lexer for Flux.
|
|
49
|
+
* - Distinguishes Int vs Float.
|
|
50
|
+
* - Handles "inf" as a dedicated keyword token.
|
|
51
|
+
* - Handles true/false as Bool tokens.
|
|
52
|
+
* - Supports both line (//) and block comments.
|
|
53
|
+
*/
|
|
54
|
+
class Lexer {
|
|
55
|
+
src;
|
|
56
|
+
pos = 0;
|
|
57
|
+
line = 1;
|
|
58
|
+
col = 1;
|
|
59
|
+
constructor(source) {
|
|
60
|
+
this.src = source;
|
|
61
|
+
}
|
|
62
|
+
tokenize() {
|
|
63
|
+
const tokens = [];
|
|
64
|
+
while (!this.isAtEnd()) {
|
|
65
|
+
const ch = this.peekChar();
|
|
66
|
+
if (this.isWhitespace(ch)) {
|
|
67
|
+
this.skipWhitespace();
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (ch === "/" && this.peekChar(1) === "/") {
|
|
71
|
+
this.skipLineComment();
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (ch === "/" && this.peekChar(1) === "*") {
|
|
75
|
+
this.skipBlockComment();
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const startLine = this.line;
|
|
79
|
+
const startCol = this.col;
|
|
80
|
+
if (this.isAlpha(ch) || ch === "_") {
|
|
81
|
+
const ident = this.readIdentifier();
|
|
82
|
+
const lower = ident.toLowerCase();
|
|
83
|
+
if (lower === "true" || lower === "false") {
|
|
84
|
+
tokens.push({
|
|
85
|
+
type: TokenType.Bool,
|
|
86
|
+
lexeme: ident,
|
|
87
|
+
value: lower === "true",
|
|
88
|
+
line: startLine,
|
|
89
|
+
column: startCol,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
else if (lower === "inf") {
|
|
93
|
+
tokens.push({
|
|
94
|
+
type: TokenType.Inf,
|
|
95
|
+
lexeme: ident,
|
|
96
|
+
value: "inf",
|
|
97
|
+
line: startLine,
|
|
98
|
+
column: startCol,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
tokens.push({
|
|
103
|
+
type: TokenType.Identifier,
|
|
104
|
+
lexeme: ident,
|
|
105
|
+
value: ident,
|
|
106
|
+
line: startLine,
|
|
107
|
+
column: startCol,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (this.isDigit(ch) || (ch === "-" && this.isDigit(this.peekChar(1)))) {
|
|
113
|
+
tokens.push(this.readNumberToken());
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (ch === '"' || ch === "'") {
|
|
117
|
+
tokens.push(this.readStringToken());
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
// Single-character tokens
|
|
121
|
+
switch (ch) {
|
|
122
|
+
case "{":
|
|
123
|
+
this.advanceChar();
|
|
124
|
+
tokens.push({ type: TokenType.LBrace, lexeme: "{", line: startLine, column: startCol });
|
|
125
|
+
break;
|
|
126
|
+
case "}":
|
|
127
|
+
this.advanceChar();
|
|
128
|
+
tokens.push({ type: TokenType.RBrace, lexeme: "}", line: startLine, column: startCol });
|
|
129
|
+
break;
|
|
130
|
+
case "[":
|
|
131
|
+
this.advanceChar();
|
|
132
|
+
tokens.push({ type: TokenType.LBracket, lexeme: "[", line: startLine, column: startCol });
|
|
133
|
+
break;
|
|
134
|
+
case "]":
|
|
135
|
+
this.advanceChar();
|
|
136
|
+
tokens.push({ type: TokenType.RBracket, lexeme: "]", line: startLine, column: startCol });
|
|
137
|
+
break;
|
|
138
|
+
case "(":
|
|
139
|
+
this.advanceChar();
|
|
140
|
+
tokens.push({ type: TokenType.LParen, lexeme: "(", line: startLine, column: startCol });
|
|
141
|
+
break;
|
|
142
|
+
case ")":
|
|
143
|
+
this.advanceChar();
|
|
144
|
+
tokens.push({ type: TokenType.RParen, lexeme: ")", line: startLine, column: startCol });
|
|
145
|
+
break;
|
|
146
|
+
case ",":
|
|
147
|
+
this.advanceChar();
|
|
148
|
+
tokens.push({ type: TokenType.Comma, lexeme: ",", line: startLine, column: startCol });
|
|
149
|
+
break;
|
|
150
|
+
case ";":
|
|
151
|
+
this.advanceChar();
|
|
152
|
+
tokens.push({ type: TokenType.Semicolon, lexeme: ";", line: startLine, column: startCol });
|
|
153
|
+
break;
|
|
154
|
+
case ":":
|
|
155
|
+
this.advanceChar();
|
|
156
|
+
tokens.push({ type: TokenType.Colon, lexeme: ":", line: startLine, column: startCol });
|
|
157
|
+
break;
|
|
158
|
+
case "=": {
|
|
159
|
+
this.advanceChar();
|
|
160
|
+
if (this.peekChar() === "=") {
|
|
161
|
+
this.advanceChar(); // second '='
|
|
162
|
+
if (this.peekChar() === "=") {
|
|
163
|
+
this.advanceChar(); // third '='
|
|
164
|
+
tokens.push({
|
|
165
|
+
type: TokenType.EqualEqualEqual,
|
|
166
|
+
lexeme: "===",
|
|
167
|
+
line: startLine,
|
|
168
|
+
column: startCol,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
tokens.push({
|
|
173
|
+
type: TokenType.EqualEqual,
|
|
174
|
+
lexeme: "==",
|
|
175
|
+
line: startLine,
|
|
176
|
+
column: startCol,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
tokens.push({
|
|
182
|
+
type: TokenType.Equals,
|
|
183
|
+
lexeme: "=",
|
|
184
|
+
line: startLine,
|
|
185
|
+
column: startCol,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case "@":
|
|
191
|
+
this.advanceChar();
|
|
192
|
+
tokens.push({ type: TokenType.At, lexeme: "@", line: startLine, column: startCol });
|
|
193
|
+
break;
|
|
194
|
+
case ".":
|
|
195
|
+
this.advanceChar();
|
|
196
|
+
tokens.push({
|
|
197
|
+
type: TokenType.Dot,
|
|
198
|
+
lexeme: ".",
|
|
199
|
+
line: startLine,
|
|
200
|
+
column: startCol,
|
|
201
|
+
});
|
|
202
|
+
break;
|
|
203
|
+
case ">":
|
|
204
|
+
this.advanceChar();
|
|
205
|
+
if (this.peekChar() === "=") {
|
|
206
|
+
this.advanceChar();
|
|
207
|
+
tokens.push({
|
|
208
|
+
type: TokenType.GreaterEqual,
|
|
209
|
+
lexeme: ">=",
|
|
210
|
+
line: startLine,
|
|
211
|
+
column: startCol,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
tokens.push({
|
|
216
|
+
type: TokenType.Greater,
|
|
217
|
+
lexeme: ">",
|
|
218
|
+
line: startLine,
|
|
219
|
+
column: startCol,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
case "<":
|
|
224
|
+
this.advanceChar();
|
|
225
|
+
if (this.peekChar() === "=") {
|
|
226
|
+
this.advanceChar();
|
|
227
|
+
tokens.push({
|
|
228
|
+
type: TokenType.LessEqual,
|
|
229
|
+
lexeme: "<=",
|
|
230
|
+
line: startLine,
|
|
231
|
+
column: startCol,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
tokens.push({
|
|
236
|
+
type: TokenType.Less,
|
|
237
|
+
lexeme: "<",
|
|
238
|
+
line: startLine,
|
|
239
|
+
column: startCol,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
case "!":
|
|
244
|
+
this.advanceChar();
|
|
245
|
+
if (this.peekChar() === "=") {
|
|
246
|
+
this.advanceChar();
|
|
247
|
+
if (this.peekChar() === "=") {
|
|
248
|
+
this.advanceChar();
|
|
249
|
+
tokens.push({
|
|
250
|
+
type: TokenType.BangEqualEqual,
|
|
251
|
+
lexeme: "!==",
|
|
252
|
+
line: startLine,
|
|
253
|
+
column: startCol,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
tokens.push({
|
|
258
|
+
type: TokenType.BangEqual,
|
|
259
|
+
lexeme: "!=",
|
|
260
|
+
line: startLine,
|
|
261
|
+
column: startCol,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
tokens.push({
|
|
267
|
+
type: TokenType.Bang,
|
|
268
|
+
lexeme: "!",
|
|
269
|
+
line: startLine,
|
|
270
|
+
column: startCol,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
case "+":
|
|
275
|
+
this.advanceChar();
|
|
276
|
+
tokens.push({
|
|
277
|
+
type: TokenType.Plus,
|
|
278
|
+
lexeme: "+",
|
|
279
|
+
line: startLine,
|
|
280
|
+
column: startCol,
|
|
281
|
+
});
|
|
282
|
+
break;
|
|
283
|
+
case "-":
|
|
284
|
+
// Note: "-<digit>" is handled by readNumberToken() earlier.
|
|
285
|
+
this.advanceChar();
|
|
286
|
+
tokens.push({
|
|
287
|
+
type: TokenType.Minus,
|
|
288
|
+
lexeme: "-",
|
|
289
|
+
line: startLine,
|
|
290
|
+
column: startCol,
|
|
291
|
+
});
|
|
292
|
+
break;
|
|
293
|
+
case "*":
|
|
294
|
+
this.advanceChar();
|
|
295
|
+
tokens.push({
|
|
296
|
+
type: TokenType.Star,
|
|
297
|
+
lexeme: "*",
|
|
298
|
+
line: startLine,
|
|
299
|
+
column: startCol,
|
|
300
|
+
});
|
|
301
|
+
break;
|
|
302
|
+
case "/":
|
|
303
|
+
// bare '/' (comments handled above)
|
|
304
|
+
this.advanceChar();
|
|
305
|
+
tokens.push({
|
|
306
|
+
type: TokenType.Slash,
|
|
307
|
+
lexeme: "/",
|
|
308
|
+
line: startLine,
|
|
309
|
+
column: startCol,
|
|
310
|
+
});
|
|
311
|
+
break;
|
|
312
|
+
case "%":
|
|
313
|
+
this.advanceChar();
|
|
314
|
+
tokens.push({
|
|
315
|
+
type: TokenType.Percent,
|
|
316
|
+
lexeme: "%",
|
|
317
|
+
line: startLine,
|
|
318
|
+
column: startCol,
|
|
319
|
+
});
|
|
320
|
+
break;
|
|
321
|
+
case "&":
|
|
322
|
+
if (this.peekChar(1) === "&") {
|
|
323
|
+
this.advanceChar(); // first '&'
|
|
324
|
+
this.advanceChar(); // second '&'
|
|
325
|
+
tokens.push({
|
|
326
|
+
type: TokenType.AndAnd,
|
|
327
|
+
lexeme: "&&",
|
|
328
|
+
line: startLine,
|
|
329
|
+
column: startCol,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
throw this.error("Unexpected '&' (did you mean '&&'?)", startLine, startCol);
|
|
334
|
+
}
|
|
335
|
+
break;
|
|
336
|
+
case "|":
|
|
337
|
+
if (this.peekChar(1) === "|") {
|
|
338
|
+
this.advanceChar(); // first '|'
|
|
339
|
+
this.advanceChar(); // second '|'
|
|
340
|
+
tokens.push({
|
|
341
|
+
type: TokenType.OrOr,
|
|
342
|
+
lexeme: "||",
|
|
343
|
+
line: startLine,
|
|
344
|
+
column: startCol,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
throw this.error("Unexpected '|' (did you mean '||'?)", startLine, startCol);
|
|
349
|
+
}
|
|
350
|
+
break;
|
|
351
|
+
default:
|
|
352
|
+
throw this.error(`Unexpected character '${ch}'`, startLine, startCol);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
tokens.push({
|
|
356
|
+
type: TokenType.EOF,
|
|
357
|
+
lexeme: "",
|
|
358
|
+
line: this.line,
|
|
359
|
+
column: this.col,
|
|
360
|
+
});
|
|
361
|
+
return tokens;
|
|
362
|
+
}
|
|
363
|
+
isAtEnd() {
|
|
364
|
+
return this.pos >= this.src.length;
|
|
365
|
+
}
|
|
366
|
+
peekChar(offset = 0) {
|
|
367
|
+
const idx = this.pos + offset;
|
|
368
|
+
if (idx >= this.src.length)
|
|
369
|
+
return "\0";
|
|
370
|
+
return this.src[idx];
|
|
371
|
+
}
|
|
372
|
+
advanceChar() {
|
|
373
|
+
const ch = this.src[this.pos++] ?? "\0";
|
|
374
|
+
if (ch === "\n") {
|
|
375
|
+
this.line += 1;
|
|
376
|
+
this.col = 1;
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
this.col += 1;
|
|
380
|
+
}
|
|
381
|
+
return ch;
|
|
382
|
+
}
|
|
383
|
+
isWhitespace(ch) {
|
|
384
|
+
return ch === " " || ch === "\t" || ch === "\r" || ch === "\n";
|
|
385
|
+
}
|
|
386
|
+
skipWhitespace() {
|
|
387
|
+
while (!this.isAtEnd() && this.isWhitespace(this.peekChar())) {
|
|
388
|
+
this.advanceChar();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
skipLineComment() {
|
|
392
|
+
// assume starting at first '/'
|
|
393
|
+
this.advanceChar(); // '/'
|
|
394
|
+
this.advanceChar(); // second '/'
|
|
395
|
+
while (!this.isAtEnd() && this.peekChar() !== "\n") {
|
|
396
|
+
this.advanceChar();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
skipBlockComment() {
|
|
400
|
+
this.advanceChar(); // '/'
|
|
401
|
+
this.advanceChar(); // '*'
|
|
402
|
+
while (!this.isAtEnd()) {
|
|
403
|
+
if (this.peekChar() === "*" && this.peekChar(1) === "/") {
|
|
404
|
+
this.advanceChar(); // '*'
|
|
405
|
+
this.advanceChar(); // '/'
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
this.advanceChar();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
isAlpha(ch) {
|
|
412
|
+
return (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z");
|
|
413
|
+
}
|
|
414
|
+
isDigit(ch) {
|
|
415
|
+
return ch >= "0" && ch <= "9";
|
|
416
|
+
}
|
|
417
|
+
readIdentifier() {
|
|
418
|
+
let result = "";
|
|
419
|
+
while (!this.isAtEnd()) {
|
|
420
|
+
const ch = this.peekChar();
|
|
421
|
+
if (this.isAlpha(ch) || this.isDigit(ch) || ch === "_") {
|
|
422
|
+
result += this.advanceChar();
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return result;
|
|
429
|
+
}
|
|
430
|
+
readNumberToken() {
|
|
431
|
+
const startLine = this.line;
|
|
432
|
+
const startCol = this.col;
|
|
433
|
+
let text = "";
|
|
434
|
+
let hasDot = false;
|
|
435
|
+
// optional leading '-'
|
|
436
|
+
if (this.peekChar() === "-") {
|
|
437
|
+
text += this.advanceChar();
|
|
438
|
+
}
|
|
439
|
+
while (!this.isAtEnd() && this.isDigit(this.peekChar())) {
|
|
440
|
+
text += this.advanceChar();
|
|
441
|
+
}
|
|
442
|
+
if (this.peekChar() === "." && this.isDigit(this.peekChar(1))) {
|
|
443
|
+
hasDot = true;
|
|
444
|
+
text += this.advanceChar(); // '.'
|
|
445
|
+
while (!this.isAtEnd() && this.isDigit(this.peekChar())) {
|
|
446
|
+
text += this.advanceChar();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const num = Number(text);
|
|
450
|
+
if (Number.isNaN(num)) {
|
|
451
|
+
throw this.error(`Invalid numeric literal '${text}'`, startLine, startCol);
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
type: hasDot ? TokenType.Float : TokenType.Int,
|
|
455
|
+
lexeme: text,
|
|
456
|
+
value: num,
|
|
457
|
+
line: startLine,
|
|
458
|
+
column: startCol,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
readStringToken() {
|
|
462
|
+
const quote = this.advanceChar(); // consume opening quote
|
|
463
|
+
const startLine = this.line;
|
|
464
|
+
const startCol = this.col;
|
|
465
|
+
let text = "";
|
|
466
|
+
while (!this.isAtEnd()) {
|
|
467
|
+
const ch = this.peekChar();
|
|
468
|
+
if (ch === quote) {
|
|
469
|
+
this.advanceChar(); // closing quote
|
|
470
|
+
return {
|
|
471
|
+
type: TokenType.String,
|
|
472
|
+
lexeme: text,
|
|
473
|
+
value: text,
|
|
474
|
+
line: startLine,
|
|
475
|
+
column: startCol,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
if (ch === "\n") {
|
|
479
|
+
// allow multiline? for now, yes
|
|
480
|
+
text += this.advanceChar();
|
|
481
|
+
}
|
|
482
|
+
else if (ch === "\\") {
|
|
483
|
+
// simple escape handling: \" and \\ only
|
|
484
|
+
this.advanceChar(); // '\'
|
|
485
|
+
const next = this.peekChar();
|
|
486
|
+
if (next === quote || next === "\\") {
|
|
487
|
+
text += this.advanceChar();
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
text += "\\" + this.advanceChar();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
text += this.advanceChar();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
throw this.error("Unterminated string literal", startLine, startCol);
|
|
498
|
+
}
|
|
499
|
+
error(message, line, column) {
|
|
500
|
+
return new Error(`Lexer error at ${line}:${column} - ${message}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Parser
|
|
505
|
+
*/
|
|
506
|
+
class Parser {
|
|
507
|
+
tokens;
|
|
508
|
+
current = 0;
|
|
509
|
+
constructor(tokens) {
|
|
510
|
+
this.tokens = tokens;
|
|
511
|
+
}
|
|
512
|
+
parseDocument() {
|
|
513
|
+
// document { ... }
|
|
514
|
+
this.expectIdentifier("document", "Expected 'document' at start of file");
|
|
515
|
+
this.consume(TokenType.LBrace, "Expected '{' after 'document'");
|
|
516
|
+
const meta = { version: "0.1.0" };
|
|
517
|
+
const state = { params: [] };
|
|
518
|
+
let pageConfig;
|
|
519
|
+
const grids = [];
|
|
520
|
+
const rules = [];
|
|
521
|
+
let runtime;
|
|
522
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
523
|
+
if (this.checkIdentifier("meta")) {
|
|
524
|
+
const blockMeta = this.parseMetaBlock();
|
|
525
|
+
Object.assign(meta, blockMeta);
|
|
526
|
+
}
|
|
527
|
+
else if (this.checkIdentifier("state")) {
|
|
528
|
+
const st = this.parseStateBlock();
|
|
529
|
+
state.params.push(...st.params);
|
|
530
|
+
}
|
|
531
|
+
else if (this.checkIdentifier("pageConfig")) {
|
|
532
|
+
pageConfig = this.parsePageConfigBlock();
|
|
533
|
+
}
|
|
534
|
+
else if (this.checkIdentifier("grid")) {
|
|
535
|
+
grids.push(this.parseGridBlock());
|
|
536
|
+
}
|
|
537
|
+
else if (this.checkIdentifier("rule")) {
|
|
538
|
+
rules.push(this.parseRuleDecl());
|
|
539
|
+
}
|
|
540
|
+
else if (this.checkIdentifier("runtime")) {
|
|
541
|
+
runtime = this.parseRuntimeBlock();
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
const tok = this.peek();
|
|
545
|
+
throw this.errorAtToken(tok, `Unexpected top-level construct '${tok.lexeme}'`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
this.consume(TokenType.RBrace, "Expected '}' at end of document");
|
|
549
|
+
const doc = {
|
|
550
|
+
meta,
|
|
551
|
+
state,
|
|
552
|
+
pageConfig,
|
|
553
|
+
grids,
|
|
554
|
+
rules,
|
|
555
|
+
runtime,
|
|
556
|
+
};
|
|
557
|
+
return doc;
|
|
558
|
+
}
|
|
559
|
+
// --- Meta ---
|
|
560
|
+
parseMetaBlock() {
|
|
561
|
+
this.expectIdentifier("meta", "Expected 'meta'");
|
|
562
|
+
this.consume(TokenType.LBrace, "Expected '{' after 'meta'");
|
|
563
|
+
const meta = { version: "0.1.0" };
|
|
564
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
565
|
+
const keyTok = this.consume(TokenType.Identifier, "Expected meta field name");
|
|
566
|
+
const key = String(keyTok.value);
|
|
567
|
+
this.consume(TokenType.Equals, "Expected '=' after meta field name");
|
|
568
|
+
const valueTok = this.consume(TokenType.String, "Expected string value for meta field");
|
|
569
|
+
meta[key] = String(valueTok.value);
|
|
570
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
571
|
+
}
|
|
572
|
+
this.consume(TokenType.RBrace, "Expected '}' after meta block");
|
|
573
|
+
return meta;
|
|
574
|
+
}
|
|
575
|
+
// --- State ---
|
|
576
|
+
parseStateBlock() {
|
|
577
|
+
this.expectIdentifier("state", "Expected 'state'");
|
|
578
|
+
this.consume(TokenType.LBrace, "Expected '{' after 'state'");
|
|
579
|
+
const params = [];
|
|
580
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
581
|
+
if (this.checkIdentifier("param")) {
|
|
582
|
+
params.push(this.parseParamDecl());
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
// Tolerant skip of unknown statements inside state
|
|
586
|
+
this.skipStatement();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
this.consume(TokenType.RBrace, "Expected '}' after state block");
|
|
590
|
+
return { params };
|
|
591
|
+
}
|
|
592
|
+
parseParamDecl() {
|
|
593
|
+
this.expectIdentifier("param", "Expected 'param'");
|
|
594
|
+
const nameTok = this.consume(TokenType.Identifier, "Expected parameter name");
|
|
595
|
+
const name = String(nameTok.value);
|
|
596
|
+
this.consume(TokenType.Colon, "Expected ':' after parameter name");
|
|
597
|
+
const typeTok = this.consume(TokenType.Identifier, "Expected parameter type");
|
|
598
|
+
const typeName = String(typeTok.value);
|
|
599
|
+
const validTypes = ["int", "float", "bool", "string", "enum"];
|
|
600
|
+
if (!validTypes.includes(typeName)) {
|
|
601
|
+
throw this.errorAtToken(typeTok, `Unknown parameter type '${typeName}'`);
|
|
602
|
+
}
|
|
603
|
+
let min;
|
|
604
|
+
let max;
|
|
605
|
+
// Optional range
|
|
606
|
+
if (this.match(TokenType.LBracket)) {
|
|
607
|
+
const minLit = this.parseLiteral();
|
|
608
|
+
min = minLit;
|
|
609
|
+
this.consume(TokenType.Comma, "Expected ',' in range");
|
|
610
|
+
if (this.match(TokenType.Inf)) {
|
|
611
|
+
max = "inf";
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
const maxLit = this.parseLiteral();
|
|
615
|
+
max = maxLit;
|
|
616
|
+
}
|
|
617
|
+
this.consume(TokenType.RBracket, "Expected ']' to close range");
|
|
618
|
+
}
|
|
619
|
+
this.consume(TokenType.At, "Expected '@' before initial value");
|
|
620
|
+
const initLit = this.parseLiteral();
|
|
621
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
622
|
+
const param = {
|
|
623
|
+
name,
|
|
624
|
+
type: typeName,
|
|
625
|
+
min,
|
|
626
|
+
max,
|
|
627
|
+
initial: initLit,
|
|
628
|
+
};
|
|
629
|
+
return param;
|
|
630
|
+
}
|
|
631
|
+
// --- PageConfig ---
|
|
632
|
+
parsePageConfigBlock() {
|
|
633
|
+
this.expectIdentifier("pageConfig", "Expected 'pageConfig'");
|
|
634
|
+
this.consume(TokenType.LBrace, "Expected '{' after 'pageConfig'");
|
|
635
|
+
let size;
|
|
636
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
637
|
+
if (this.checkIdentifier("size")) {
|
|
638
|
+
size = this.parsePageSizeBlock();
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
this.skipStatement();
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
this.consume(TokenType.RBrace, "Expected '}' after pageConfig block");
|
|
645
|
+
if (!size) {
|
|
646
|
+
throw this.errorAtToken(this.peek(), "pageConfig must contain a size block");
|
|
647
|
+
}
|
|
648
|
+
return { size };
|
|
649
|
+
}
|
|
650
|
+
parsePageSizeBlock() {
|
|
651
|
+
this.expectIdentifier("size", "Expected 'size'");
|
|
652
|
+
this.consume(TokenType.LBrace, "Expected '{' after 'size'");
|
|
653
|
+
let width;
|
|
654
|
+
let height;
|
|
655
|
+
let units;
|
|
656
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
657
|
+
if (this.checkIdentifier("width")) {
|
|
658
|
+
this.advance(); // width
|
|
659
|
+
this.consume(TokenType.Equals, "Expected '=' after 'width'");
|
|
660
|
+
const valTok = this.consumeNumber("Expected numeric width");
|
|
661
|
+
width = Number(valTok.value);
|
|
662
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
663
|
+
}
|
|
664
|
+
else if (this.checkIdentifier("height")) {
|
|
665
|
+
this.advance(); // height
|
|
666
|
+
this.consume(TokenType.Equals, "Expected '=' after 'height'");
|
|
667
|
+
const valTok = this.consumeNumber("Expected numeric height");
|
|
668
|
+
height = Number(valTok.value);
|
|
669
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
670
|
+
}
|
|
671
|
+
else if (this.checkIdentifier("units")) {
|
|
672
|
+
this.advance(); // units
|
|
673
|
+
this.consume(TokenType.Equals, "Expected '=' after 'units'");
|
|
674
|
+
const valTok = this.consume(TokenType.String, "Expected string for units");
|
|
675
|
+
units = String(valTok.value);
|
|
676
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
this.skipStatement();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
this.consume(TokenType.RBrace, "Expected '}' after size block");
|
|
683
|
+
if (width === undefined || height === undefined || units === undefined) {
|
|
684
|
+
throw this.errorAtToken(this.peek(), "Incomplete page size (width/height/units required)");
|
|
685
|
+
}
|
|
686
|
+
return { width, height, units };
|
|
687
|
+
}
|
|
688
|
+
// --- Grid & Cell ---
|
|
689
|
+
parseGridBlock() {
|
|
690
|
+
this.expectIdentifier("grid", "Expected 'grid'");
|
|
691
|
+
const nameTok = this.consume(TokenType.Identifier, "Expected grid name");
|
|
692
|
+
const name = String(nameTok.value);
|
|
693
|
+
this.consume(TokenType.LBrace, "Expected '{' after grid name");
|
|
694
|
+
let topology;
|
|
695
|
+
let page;
|
|
696
|
+
let rows;
|
|
697
|
+
let cols;
|
|
698
|
+
const cells = [];
|
|
699
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
700
|
+
if (this.checkIdentifier("topology")) {
|
|
701
|
+
this.advance(); // topology
|
|
702
|
+
this.consume(TokenType.Equals, "Expected '=' after 'topology'");
|
|
703
|
+
const topTok = this.consume(TokenType.Identifier, "Expected topology kind");
|
|
704
|
+
topology = String(topTok.value);
|
|
705
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
706
|
+
}
|
|
707
|
+
else if (this.checkIdentifier("page")) {
|
|
708
|
+
this.advance(); // page
|
|
709
|
+
this.consume(TokenType.Equals, "Expected '=' after 'page'");
|
|
710
|
+
const numTok = this.consumeNumber("Expected page number");
|
|
711
|
+
page = Number(numTok.value);
|
|
712
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
713
|
+
}
|
|
714
|
+
else if (this.checkIdentifier("size")) {
|
|
715
|
+
const size = this.parseGridSizeBlock();
|
|
716
|
+
rows = size.rows;
|
|
717
|
+
cols = size.cols;
|
|
718
|
+
}
|
|
719
|
+
else if (this.checkIdentifier("cell")) {
|
|
720
|
+
cells.push(this.parseCellBlock());
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
this.skipStatement();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
this.consume(TokenType.RBrace, "Expected '}' after grid block");
|
|
727
|
+
if (!topology) {
|
|
728
|
+
throw this.errorAtToken(this.peek(), "Grid must declare a topology");
|
|
729
|
+
}
|
|
730
|
+
const grid = {
|
|
731
|
+
name,
|
|
732
|
+
topology: topology,
|
|
733
|
+
page,
|
|
734
|
+
size: { rows, cols },
|
|
735
|
+
cells,
|
|
736
|
+
};
|
|
737
|
+
return grid;
|
|
738
|
+
}
|
|
739
|
+
parseGridSizeBlock() {
|
|
740
|
+
this.expectIdentifier("size", "Expected 'size'");
|
|
741
|
+
this.consume(TokenType.LBrace, "Expected '{' after 'size'");
|
|
742
|
+
let rows;
|
|
743
|
+
let cols;
|
|
744
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
745
|
+
if (this.checkIdentifier("rows")) {
|
|
746
|
+
this.advance(); // rows
|
|
747
|
+
this.consume(TokenType.Equals, "Expected '=' after 'rows'");
|
|
748
|
+
const numTok = this.consumeNumber("Expected integer for rows");
|
|
749
|
+
rows = Number(numTok.value);
|
|
750
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
751
|
+
}
|
|
752
|
+
else if (this.checkIdentifier("cols")) {
|
|
753
|
+
this.advance(); // cols
|
|
754
|
+
this.consume(TokenType.Equals, "Expected '=' after 'cols'");
|
|
755
|
+
const numTok = this.consumeNumber("Expected integer for cols");
|
|
756
|
+
cols = Number(numTok.value);
|
|
757
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
this.skipStatement();
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
this.consume(TokenType.RBrace, "Expected '}' after size block");
|
|
764
|
+
return { rows, cols };
|
|
765
|
+
}
|
|
766
|
+
parseCellBlock() {
|
|
767
|
+
this.expectIdentifier("cell", "Expected 'cell'");
|
|
768
|
+
const idTok = this.consume(TokenType.Identifier, "Expected cell id");
|
|
769
|
+
const id = String(idTok.value);
|
|
770
|
+
this.consume(TokenType.LBrace, "Expected '{' after cell id");
|
|
771
|
+
const cell = {
|
|
772
|
+
id,
|
|
773
|
+
tags: [],
|
|
774
|
+
};
|
|
775
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
776
|
+
if (this.checkIdentifier("tags")) {
|
|
777
|
+
this.advance(); // tags
|
|
778
|
+
this.consume(TokenType.Equals, "Expected '=' after 'tags'");
|
|
779
|
+
this.consume(TokenType.LBracket, "Expected '[' after 'tags ='");
|
|
780
|
+
const tags = [];
|
|
781
|
+
if (!this.check(TokenType.RBracket)) {
|
|
782
|
+
// at least one tag
|
|
783
|
+
const first = this.consume(TokenType.Identifier, "Expected tag identifier");
|
|
784
|
+
tags.push(String(first.value));
|
|
785
|
+
while (this.match(TokenType.Comma)) {
|
|
786
|
+
const t = this.consume(TokenType.Identifier, "Expected tag identifier");
|
|
787
|
+
tags.push(String(t.value));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
this.consume(TokenType.RBracket, "Expected ']' after tag list");
|
|
791
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
792
|
+
cell.tags = tags;
|
|
793
|
+
}
|
|
794
|
+
else if (this.checkIdentifier("content")) {
|
|
795
|
+
this.advance(); // content
|
|
796
|
+
this.consume(TokenType.Equals, "Expected '=' after 'content'");
|
|
797
|
+
const strTok = this.consume(TokenType.String, "Expected string for content");
|
|
798
|
+
cell.content = String(strTok.value);
|
|
799
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
800
|
+
}
|
|
801
|
+
else if (this.checkIdentifier("dynamic")) {
|
|
802
|
+
this.advance(); // dynamic
|
|
803
|
+
this.consume(TokenType.Equals, "Expected '=' after 'dynamic'");
|
|
804
|
+
const numTok = this.consumeNumber("Expected numeric value for dynamic");
|
|
805
|
+
cell.dynamic = Number(numTok.value);
|
|
806
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
// Tolerant skip of unknown fields inside cell
|
|
810
|
+
this.skipStatement();
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
this.consume(TokenType.RBrace, "Expected '}' after cell block");
|
|
814
|
+
return cell;
|
|
815
|
+
}
|
|
816
|
+
// --- Expressions ---
|
|
817
|
+
parseExpr() {
|
|
818
|
+
return this.parseOr();
|
|
819
|
+
}
|
|
820
|
+
parseOr() {
|
|
821
|
+
let expr = this.parseAnd();
|
|
822
|
+
while (true) {
|
|
823
|
+
if (this.matchKeyword("or") || this.match(TokenType.OrOr)) {
|
|
824
|
+
const right = this.parseAnd();
|
|
825
|
+
expr = this.makeBinary(expr, "or", right);
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return expr;
|
|
832
|
+
}
|
|
833
|
+
parseAnd() {
|
|
834
|
+
let expr = this.parseEquality();
|
|
835
|
+
while (true) {
|
|
836
|
+
if (this.matchKeyword("and") || this.match(TokenType.AndAnd)) {
|
|
837
|
+
const right = this.parseEquality();
|
|
838
|
+
expr = this.makeBinary(expr, "and", right);
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return expr;
|
|
845
|
+
}
|
|
846
|
+
parseEquality() {
|
|
847
|
+
let expr = this.parseComparison();
|
|
848
|
+
while (true) {
|
|
849
|
+
if (this.match(TokenType.EqualEqual)) {
|
|
850
|
+
const right = this.parseComparison();
|
|
851
|
+
expr = this.makeBinary(expr, "==", right);
|
|
852
|
+
}
|
|
853
|
+
else if (this.match(TokenType.BangEqual)) {
|
|
854
|
+
const right = this.parseComparison();
|
|
855
|
+
expr = this.makeBinary(expr, "!=", right);
|
|
856
|
+
}
|
|
857
|
+
else if (this.match(TokenType.EqualEqualEqual)) {
|
|
858
|
+
const right = this.parseComparison();
|
|
859
|
+
expr = this.makeBinary(expr, "===", right);
|
|
860
|
+
}
|
|
861
|
+
else if (this.match(TokenType.BangEqualEqual)) {
|
|
862
|
+
const right = this.parseComparison();
|
|
863
|
+
expr = this.makeBinary(expr, "!==", right);
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return expr;
|
|
870
|
+
}
|
|
871
|
+
parseComparison() {
|
|
872
|
+
let expr = this.parseTerm();
|
|
873
|
+
while (true) {
|
|
874
|
+
if (this.match(TokenType.Less)) {
|
|
875
|
+
const right = this.parseTerm();
|
|
876
|
+
expr = this.makeBinary(expr, "<", right);
|
|
877
|
+
}
|
|
878
|
+
else if (this.match(TokenType.LessEqual)) {
|
|
879
|
+
const right = this.parseTerm();
|
|
880
|
+
expr = this.makeBinary(expr, "<=", right);
|
|
881
|
+
}
|
|
882
|
+
else if (this.match(TokenType.Greater)) {
|
|
883
|
+
const right = this.parseTerm();
|
|
884
|
+
expr = this.makeBinary(expr, ">", right);
|
|
885
|
+
}
|
|
886
|
+
else if (this.match(TokenType.GreaterEqual)) {
|
|
887
|
+
const right = this.parseTerm();
|
|
888
|
+
expr = this.makeBinary(expr, ">=", right);
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return expr;
|
|
895
|
+
}
|
|
896
|
+
parseTerm() {
|
|
897
|
+
let expr = this.parseFactor();
|
|
898
|
+
while (true) {
|
|
899
|
+
if (this.match(TokenType.Plus)) {
|
|
900
|
+
const right = this.parseFactor();
|
|
901
|
+
expr = this.makeBinary(expr, "+", right);
|
|
902
|
+
}
|
|
903
|
+
else if (this.match(TokenType.Minus)) {
|
|
904
|
+
const right = this.parseFactor();
|
|
905
|
+
expr = this.makeBinary(expr, "-", right);
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return expr;
|
|
912
|
+
}
|
|
913
|
+
parseFactor() {
|
|
914
|
+
let expr = this.parseUnary();
|
|
915
|
+
while (true) {
|
|
916
|
+
if (this.match(TokenType.Star)) {
|
|
917
|
+
const right = this.parseUnary();
|
|
918
|
+
expr = this.makeBinary(expr, "*", right);
|
|
919
|
+
}
|
|
920
|
+
else if (this.match(TokenType.Slash)) {
|
|
921
|
+
const right = this.parseUnary();
|
|
922
|
+
expr = this.makeBinary(expr, "/", right);
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return expr;
|
|
929
|
+
}
|
|
930
|
+
parseUnary() {
|
|
931
|
+
if (this.matchKeyword("not") || this.match(TokenType.Bang)) {
|
|
932
|
+
const argument = this.parseUnary();
|
|
933
|
+
const op = "not";
|
|
934
|
+
return {
|
|
935
|
+
kind: "UnaryExpression",
|
|
936
|
+
op,
|
|
937
|
+
argument,
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
if (this.match(TokenType.Minus)) {
|
|
941
|
+
const argument = this.parseUnary();
|
|
942
|
+
const op = "-";
|
|
943
|
+
return {
|
|
944
|
+
kind: "UnaryExpression",
|
|
945
|
+
op,
|
|
946
|
+
argument,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
return this.parsePostfix();
|
|
950
|
+
}
|
|
951
|
+
parsePostfix() {
|
|
952
|
+
let expr = this.parsePrimary();
|
|
953
|
+
while (true) {
|
|
954
|
+
if (this.match(TokenType.Dot)) {
|
|
955
|
+
const nameTok = this.consume(TokenType.Identifier, "Expected property name after '.'");
|
|
956
|
+
const property = String(nameTok.value);
|
|
957
|
+
expr = {
|
|
958
|
+
kind: "MemberExpression",
|
|
959
|
+
object: expr,
|
|
960
|
+
property,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
else if (this.match(TokenType.LParen)) {
|
|
964
|
+
const args = this.parseArgumentList();
|
|
965
|
+
expr = this.maybeNeighborsCall(expr, args);
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return expr;
|
|
972
|
+
}
|
|
973
|
+
parsePrimary() {
|
|
974
|
+
const tok = this.peek();
|
|
975
|
+
switch (tok.type) {
|
|
976
|
+
case TokenType.Int:
|
|
977
|
+
case TokenType.Float: {
|
|
978
|
+
this.advance();
|
|
979
|
+
return {
|
|
980
|
+
kind: "Literal",
|
|
981
|
+
value: tok.value,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
case TokenType.String: {
|
|
985
|
+
this.advance();
|
|
986
|
+
return {
|
|
987
|
+
kind: "Literal",
|
|
988
|
+
value: tok.value,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
case TokenType.Bool: {
|
|
992
|
+
this.advance();
|
|
993
|
+
return {
|
|
994
|
+
kind: "Literal",
|
|
995
|
+
value: tok.value,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
case TokenType.Identifier: {
|
|
999
|
+
this.advance();
|
|
1000
|
+
return {
|
|
1001
|
+
kind: "Identifier",
|
|
1002
|
+
name: tok.lexeme,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
case TokenType.LBrace: {
|
|
1006
|
+
// Allow curly-braced grouping in expression position, e.g.:
|
|
1007
|
+
// when { neighbors.all().dynamic > 0.5 } then { ... }
|
|
1008
|
+
this.advance(); // consume '{'
|
|
1009
|
+
const expr = this.parseExpr();
|
|
1010
|
+
this.consume(TokenType.RBrace, "Expected '}' after expression group");
|
|
1011
|
+
return expr;
|
|
1012
|
+
}
|
|
1013
|
+
default:
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
1016
|
+
if (this.match(TokenType.LParen)) {
|
|
1017
|
+
const expr = this.parseExpr();
|
|
1018
|
+
this.consume(TokenType.RParen, "Expected ')' after expression");
|
|
1019
|
+
return expr;
|
|
1020
|
+
}
|
|
1021
|
+
throw this.errorAtToken(tok, "Expected expression");
|
|
1022
|
+
}
|
|
1023
|
+
parseArgumentList() {
|
|
1024
|
+
const args = [];
|
|
1025
|
+
if (this.check(TokenType.RParen)) {
|
|
1026
|
+
this.consume(TokenType.RParen, "Expected ')' after argument list");
|
|
1027
|
+
return args;
|
|
1028
|
+
}
|
|
1029
|
+
args.push(this.parseExpr());
|
|
1030
|
+
while (this.match(TokenType.Comma)) {
|
|
1031
|
+
args.push(this.parseExpr());
|
|
1032
|
+
}
|
|
1033
|
+
this.consume(TokenType.RParen, "Expected ')' after argument list");
|
|
1034
|
+
return args;
|
|
1035
|
+
}
|
|
1036
|
+
maybeNeighborsCall(callee, args) {
|
|
1037
|
+
if (callee.kind === "MemberExpression" &&
|
|
1038
|
+
callee.object.kind === "Identifier" &&
|
|
1039
|
+
callee.object.name === "neighbors") {
|
|
1040
|
+
return {
|
|
1041
|
+
kind: "NeighborsCallExpression",
|
|
1042
|
+
namespace: "neighbors",
|
|
1043
|
+
method: callee.property,
|
|
1044
|
+
args,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
return {
|
|
1048
|
+
kind: "CallExpression",
|
|
1049
|
+
callee,
|
|
1050
|
+
args,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
makeBinary(left, op, right) {
|
|
1054
|
+
return {
|
|
1055
|
+
kind: "BinaryExpression",
|
|
1056
|
+
op,
|
|
1057
|
+
left,
|
|
1058
|
+
right,
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
// --- Rule & Runtime (placeholders for now) ---
|
|
1062
|
+
// --- Rules ---
|
|
1063
|
+
parseRuleDecl() {
|
|
1064
|
+
this.expectIdentifier("rule", "Expected 'rule'");
|
|
1065
|
+
const nameTok = this.consume(TokenType.Identifier, "Expected rule name");
|
|
1066
|
+
const name = String(nameTok.value);
|
|
1067
|
+
const { mode, scope, onEventType } = this.parseRuleHeader();
|
|
1068
|
+
// Enforce event header constraints
|
|
1069
|
+
if (mode === "event" && !onEventType) {
|
|
1070
|
+
throw this.errorAtToken(this.peek(), "Event rules must specify an 'on=\"...\"' event type");
|
|
1071
|
+
}
|
|
1072
|
+
this.consume(TokenType.LBrace, "Expected '{' to start rule body");
|
|
1073
|
+
// First branch: when ... then { ... }
|
|
1074
|
+
this.expectIdentifier("when", "Expected 'when' in rule body");
|
|
1075
|
+
const firstCondition = this.parseExpr();
|
|
1076
|
+
this.expectIdentifier("then", "Expected 'then' after rule condition");
|
|
1077
|
+
const firstThen = this.parseStatementBlock();
|
|
1078
|
+
const branches = [
|
|
1079
|
+
{ condition: firstCondition, thenBranch: firstThen },
|
|
1080
|
+
];
|
|
1081
|
+
let elseBranch;
|
|
1082
|
+
// Optional: else when ... { ... } chains and final else { ... }
|
|
1083
|
+
while (this.checkIdentifier("else")) {
|
|
1084
|
+
this.advance(); // 'else'
|
|
1085
|
+
if (this.checkIdentifier("when")) {
|
|
1086
|
+
// else when ...
|
|
1087
|
+
this.advance(); // 'when'
|
|
1088
|
+
const cond = this.parseExpr();
|
|
1089
|
+
this.expectIdentifier("then", "Expected 'then' after 'else when' condition");
|
|
1090
|
+
const thenBlock = this.parseStatementBlock();
|
|
1091
|
+
branches.push({ condition: cond, thenBranch: thenBlock });
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
// plain else { ... }
|
|
1095
|
+
elseBranch = this.parseStatementBlock();
|
|
1096
|
+
break;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
this.consume(TokenType.RBrace, "Expected '}' after rule body");
|
|
1100
|
+
return {
|
|
1101
|
+
name,
|
|
1102
|
+
mode,
|
|
1103
|
+
scope,
|
|
1104
|
+
onEventType,
|
|
1105
|
+
branches,
|
|
1106
|
+
// convenience mirrors of the first branch
|
|
1107
|
+
condition: branches[0].condition,
|
|
1108
|
+
thenBranch: branches[0].thenBranch,
|
|
1109
|
+
elseBranch,
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
parseRuleHeader() {
|
|
1113
|
+
let mode = "docstep";
|
|
1114
|
+
let scope;
|
|
1115
|
+
let onEventType;
|
|
1116
|
+
if (this.match(TokenType.LParen)) {
|
|
1117
|
+
if (!this.check(TokenType.RParen)) {
|
|
1118
|
+
while (true) {
|
|
1119
|
+
const keyTok = this.consume(TokenType.Identifier, "Expected header key");
|
|
1120
|
+
const key = String(keyTok.value);
|
|
1121
|
+
this.consume(TokenType.Equals, "Expected '=' after header key");
|
|
1122
|
+
if (key === "mode") {
|
|
1123
|
+
const valTok = this.consume(TokenType.Identifier, "Expected mode value");
|
|
1124
|
+
const val = String(valTok.value);
|
|
1125
|
+
if (val !== "docstep" && val !== "event" && val !== "timer") {
|
|
1126
|
+
throw this.errorAtToken(valTok, `Invalid rule mode '${val}'`);
|
|
1127
|
+
}
|
|
1128
|
+
mode = val;
|
|
1129
|
+
}
|
|
1130
|
+
else if (key === "grid") {
|
|
1131
|
+
const valTok = this.consume(TokenType.Identifier, "Expected grid name");
|
|
1132
|
+
const gridName = String(valTok.value);
|
|
1133
|
+
scope = { grid: gridName };
|
|
1134
|
+
}
|
|
1135
|
+
else if (key === "on") {
|
|
1136
|
+
const valTok = this.consume(TokenType.String, "Expected string for 'on'");
|
|
1137
|
+
onEventType = String(valTok.value);
|
|
1138
|
+
}
|
|
1139
|
+
else {
|
|
1140
|
+
throw this.errorAtToken(keyTok, `Unknown rule header key '${key}'`);
|
|
1141
|
+
}
|
|
1142
|
+
if (!this.match(TokenType.Comma))
|
|
1143
|
+
break;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
this.consume(TokenType.RParen, "Expected ')' after rule header");
|
|
1147
|
+
}
|
|
1148
|
+
return { mode, scope, onEventType };
|
|
1149
|
+
}
|
|
1150
|
+
parseStatementBlock() {
|
|
1151
|
+
this.consume(TokenType.LBrace, "Expected '{' to start block");
|
|
1152
|
+
const statements = [];
|
|
1153
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
1154
|
+
statements.push(this.parseStatement());
|
|
1155
|
+
}
|
|
1156
|
+
this.consume(TokenType.RBrace, "Expected '}' to close block");
|
|
1157
|
+
return statements;
|
|
1158
|
+
}
|
|
1159
|
+
parseStatement() {
|
|
1160
|
+
if (this.checkIdentifier("let")) {
|
|
1161
|
+
return this.parseLetStatement();
|
|
1162
|
+
}
|
|
1163
|
+
if (this.checkIdentifier("advanceDocstep")) {
|
|
1164
|
+
return this.parseAdvanceDocstepStatement();
|
|
1165
|
+
}
|
|
1166
|
+
// For v0.1: only assignments beyond 'let' and 'advanceDocstep'
|
|
1167
|
+
const lhs = this.parseExpr();
|
|
1168
|
+
if (!this.match(TokenType.Equals)) {
|
|
1169
|
+
throw this.errorAtToken(this.peek(), "Only assignment, 'let', and 'advanceDocstep()' statements are allowed in rule bodies in v0.1");
|
|
1170
|
+
}
|
|
1171
|
+
const value = this.parseExpr();
|
|
1172
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1173
|
+
if (lhs.kind !== "Identifier" && lhs.kind !== "MemberExpression") {
|
|
1174
|
+
throw this.errorAtToken(this.peek(), "Invalid assignment target");
|
|
1175
|
+
}
|
|
1176
|
+
const stmt = {
|
|
1177
|
+
kind: "AssignmentStatement",
|
|
1178
|
+
target: lhs,
|
|
1179
|
+
value,
|
|
1180
|
+
};
|
|
1181
|
+
return stmt;
|
|
1182
|
+
}
|
|
1183
|
+
parseLetStatement() {
|
|
1184
|
+
this.expectIdentifier("let", "Expected 'let'");
|
|
1185
|
+
const nameTok = this.consume(TokenType.Identifier, "Expected identifier after 'let'");
|
|
1186
|
+
const name = String(nameTok.value);
|
|
1187
|
+
this.consume(TokenType.Equals, "Expected '=' after let name");
|
|
1188
|
+
const value = this.parseExpr();
|
|
1189
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1190
|
+
return {
|
|
1191
|
+
kind: "LetStatement",
|
|
1192
|
+
name,
|
|
1193
|
+
value,
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
parseAdvanceDocstepStatement() {
|
|
1197
|
+
this.expectIdentifier("advanceDocstep", "Expected 'advanceDocstep'");
|
|
1198
|
+
this.consume(TokenType.LParen, "Expected '(' after 'advanceDocstep'");
|
|
1199
|
+
this.consume(TokenType.RParen, "Expected ')' after 'advanceDocstep('");
|
|
1200
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1201
|
+
return {
|
|
1202
|
+
kind: "AdvanceDocstepStatement",
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
// --- Runtime ---
|
|
1206
|
+
parseRuntimeBlock() {
|
|
1207
|
+
this.expectIdentifier("runtime", "Expected 'runtime'");
|
|
1208
|
+
this.consume(TokenType.LBrace, "Expected '{' after 'runtime'");
|
|
1209
|
+
const config = {};
|
|
1210
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
1211
|
+
if (this.checkIdentifier("eventsApply")) {
|
|
1212
|
+
this.advance(); // eventsApply
|
|
1213
|
+
this.consume(TokenType.Equals, "Expected '=' after 'eventsApply'");
|
|
1214
|
+
const valTok = this.consume(TokenType.String, "Expected string value for eventsApply");
|
|
1215
|
+
const raw = String(valTok.value);
|
|
1216
|
+
const value = raw;
|
|
1217
|
+
if (value !== "immediate" &&
|
|
1218
|
+
value !== "deferred" &&
|
|
1219
|
+
value !== "cellImmediateParamsDeferred") {
|
|
1220
|
+
throw this.errorAtToken(valTok, `Invalid eventsApply policy '${value}'`);
|
|
1221
|
+
}
|
|
1222
|
+
config.eventsApply = value;
|
|
1223
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1224
|
+
}
|
|
1225
|
+
else if (this.checkIdentifier("docstepAdvance")) {
|
|
1226
|
+
this.advance(); // docstepAdvance
|
|
1227
|
+
this.consume(TokenType.Equals, "Expected '=' after 'docstepAdvance'");
|
|
1228
|
+
this.consume(TokenType.LBracket, "Expected '[' after 'docstepAdvance ='");
|
|
1229
|
+
const specs = [];
|
|
1230
|
+
if (!this.check(TokenType.RBracket)) {
|
|
1231
|
+
specs.push(this.parseDocstepAdvanceSpec());
|
|
1232
|
+
while (this.match(TokenType.Comma)) {
|
|
1233
|
+
specs.push(this.parseDocstepAdvanceSpec());
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
this.consume(TokenType.RBracket, "Expected ']' after docstepAdvance list");
|
|
1237
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1238
|
+
config.docstepAdvance = specs;
|
|
1239
|
+
}
|
|
1240
|
+
else {
|
|
1241
|
+
const tok = this.peek();
|
|
1242
|
+
throw this.errorAtToken(tok, `Unknown field '${tok.lexeme}' in runtime block`);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
this.consume(TokenType.RBrace, "Expected '}' after runtime block");
|
|
1246
|
+
return config;
|
|
1247
|
+
}
|
|
1248
|
+
parseDocstepAdvanceSpec() {
|
|
1249
|
+
// v0.1: only timer(...) supported
|
|
1250
|
+
this.expectIdentifier("timer", "Expected 'timer' in docstepAdvance spec");
|
|
1251
|
+
this.consume(TokenType.LParen, "Expected '(' after 'timer'");
|
|
1252
|
+
const { amount, unit } = this.parseDurationSpec();
|
|
1253
|
+
this.consume(TokenType.RParen, "Expected ')' after timer(...)");
|
|
1254
|
+
const spec = {
|
|
1255
|
+
kind: "timer",
|
|
1256
|
+
amount,
|
|
1257
|
+
unit,
|
|
1258
|
+
};
|
|
1259
|
+
return spec;
|
|
1260
|
+
}
|
|
1261
|
+
parseDurationSpec() {
|
|
1262
|
+
const numTok = this.consumeNumber("Expected numeric duration");
|
|
1263
|
+
const amount = Number(numTok.value);
|
|
1264
|
+
// Default unit if omitted: seconds
|
|
1265
|
+
let unit = "s";
|
|
1266
|
+
if (this.check(TokenType.Identifier)) {
|
|
1267
|
+
const unitTok = this.advance();
|
|
1268
|
+
const raw = String(unitTok.value);
|
|
1269
|
+
const lowered = raw.toLowerCase();
|
|
1270
|
+
// seconds
|
|
1271
|
+
if (lowered === "s" ||
|
|
1272
|
+
lowered === "sec" ||
|
|
1273
|
+
lowered === "secs" ||
|
|
1274
|
+
lowered === "second" ||
|
|
1275
|
+
lowered === "seconds") {
|
|
1276
|
+
unit = "s";
|
|
1277
|
+
}
|
|
1278
|
+
// milliseconds
|
|
1279
|
+
else if (lowered === "ms" ||
|
|
1280
|
+
lowered === "millisecond" ||
|
|
1281
|
+
lowered === "milliseconds") {
|
|
1282
|
+
unit = "ms";
|
|
1283
|
+
}
|
|
1284
|
+
// beats (musical)
|
|
1285
|
+
else if (lowered === "beat" || lowered === "beats") {
|
|
1286
|
+
unit = "beats";
|
|
1287
|
+
}
|
|
1288
|
+
else {
|
|
1289
|
+
throw this.errorAtToken(unitTok, `Unknown duration unit '${unitTok.lexeme}'`);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return { amount, unit };
|
|
1293
|
+
}
|
|
1294
|
+
skipRuleBlock() {
|
|
1295
|
+
// rule <name> ( ... ) { ... }
|
|
1296
|
+
this.expectIdentifier("rule", "Expected 'rule'");
|
|
1297
|
+
// consume name
|
|
1298
|
+
this.consume(TokenType.Identifier, "Expected rule name");
|
|
1299
|
+
// optional header args: (...)
|
|
1300
|
+
if (this.match(TokenType.LParen)) {
|
|
1301
|
+
this.skipUntilMatchingParen();
|
|
1302
|
+
}
|
|
1303
|
+
// body block
|
|
1304
|
+
this.consume(TokenType.LBrace, "Expected '{' to start rule body");
|
|
1305
|
+
this.skipBlock();
|
|
1306
|
+
}
|
|
1307
|
+
skipRuntimeBlock() {
|
|
1308
|
+
this.expectIdentifier("runtime", "Expected 'runtime'");
|
|
1309
|
+
this.consume(TokenType.LBrace, "Expected '{' after 'runtime'");
|
|
1310
|
+
this.skipBlock();
|
|
1311
|
+
return undefined; // runtime config to be implemented later
|
|
1312
|
+
}
|
|
1313
|
+
// --- Helpers: skipping ---
|
|
1314
|
+
skipBlock() {
|
|
1315
|
+
let depth = 1; // assume we've just consumed '{'
|
|
1316
|
+
while (!this.isAtEnd() && depth > 0) {
|
|
1317
|
+
const tok = this.advance();
|
|
1318
|
+
if (tok.type === TokenType.LBrace)
|
|
1319
|
+
depth++;
|
|
1320
|
+
else if (tok.type === TokenType.RBrace)
|
|
1321
|
+
depth--;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
skipUntilMatchingParen() {
|
|
1325
|
+
let depth = 1; // starting after '('
|
|
1326
|
+
while (!this.isAtEnd() && depth > 0) {
|
|
1327
|
+
const tok = this.advance();
|
|
1328
|
+
if (tok.type === TokenType.LParen)
|
|
1329
|
+
depth++;
|
|
1330
|
+
else if (tok.type === TokenType.RParen)
|
|
1331
|
+
depth--;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Skip a "field" or statement until we hit a semicolon or closing brace.
|
|
1336
|
+
* Used for tolerant skipping of unknown fields inside known blocks.
|
|
1337
|
+
*/
|
|
1338
|
+
skipStatement() {
|
|
1339
|
+
while (!this.isAtEnd()) {
|
|
1340
|
+
if (this.check(TokenType.Semicolon)) {
|
|
1341
|
+
this.advance();
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
if (this.check(TokenType.RBrace)) {
|
|
1345
|
+
// caller is responsible for consuming the '}' if needed
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
this.advance();
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
// --- Literal & utility parsing ---
|
|
1352
|
+
parseLiteral() {
|
|
1353
|
+
const tok = this.peek();
|
|
1354
|
+
switch (tok.type) {
|
|
1355
|
+
case TokenType.Int:
|
|
1356
|
+
case TokenType.Float:
|
|
1357
|
+
this.advance();
|
|
1358
|
+
return tok.value;
|
|
1359
|
+
case TokenType.String:
|
|
1360
|
+
this.advance();
|
|
1361
|
+
return tok.value;
|
|
1362
|
+
case TokenType.Bool:
|
|
1363
|
+
this.advance();
|
|
1364
|
+
return tok.value;
|
|
1365
|
+
case TokenType.Identifier:
|
|
1366
|
+
// enum literal or bare identifier; treat as string
|
|
1367
|
+
this.advance();
|
|
1368
|
+
return tok.lexeme;
|
|
1369
|
+
default:
|
|
1370
|
+
throw this.errorAtToken(tok, "Expected literal");
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
consumeNumber(message) {
|
|
1374
|
+
const tok = this.peek();
|
|
1375
|
+
if (tok.type === TokenType.Int || tok.type === TokenType.Float) {
|
|
1376
|
+
return this.advance();
|
|
1377
|
+
}
|
|
1378
|
+
throw this.errorAtToken(tok, message);
|
|
1379
|
+
}
|
|
1380
|
+
// --- Token navigation ---
|
|
1381
|
+
peek(offset = 0) {
|
|
1382
|
+
const idx = this.current + offset;
|
|
1383
|
+
if (idx >= this.tokens.length) {
|
|
1384
|
+
return this.tokens[this.tokens.length - 1];
|
|
1385
|
+
}
|
|
1386
|
+
return this.tokens[idx];
|
|
1387
|
+
}
|
|
1388
|
+
isAtEnd() {
|
|
1389
|
+
return this.peek().type === TokenType.EOF;
|
|
1390
|
+
}
|
|
1391
|
+
advance() {
|
|
1392
|
+
if (!this.isAtEnd())
|
|
1393
|
+
this.current++;
|
|
1394
|
+
return this.tokens[this.current - 1];
|
|
1395
|
+
}
|
|
1396
|
+
check(type) {
|
|
1397
|
+
if (this.isAtEnd())
|
|
1398
|
+
return false;
|
|
1399
|
+
return this.peek().type === type;
|
|
1400
|
+
}
|
|
1401
|
+
match(...types) {
|
|
1402
|
+
for (const t of types) {
|
|
1403
|
+
if (this.check(t)) {
|
|
1404
|
+
this.advance();
|
|
1405
|
+
return true;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
return false;
|
|
1409
|
+
}
|
|
1410
|
+
consume(type, message) {
|
|
1411
|
+
if (this.check(type))
|
|
1412
|
+
return this.advance();
|
|
1413
|
+
throw this.errorAtToken(this.peek(), message);
|
|
1414
|
+
}
|
|
1415
|
+
consumeOptional(type) {
|
|
1416
|
+
if (this.check(type)) {
|
|
1417
|
+
this.advance();
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
// --- Identifier/keyword helpers ---
|
|
1421
|
+
matchKeyword(value) {
|
|
1422
|
+
if (this.checkIdentifier(value)) {
|
|
1423
|
+
this.advance();
|
|
1424
|
+
return true;
|
|
1425
|
+
}
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|
|
1428
|
+
checkIdentifier(value) {
|
|
1429
|
+
const tok = this.peek();
|
|
1430
|
+
return tok.type === TokenType.Identifier && tok.lexeme === value;
|
|
1431
|
+
}
|
|
1432
|
+
expectIdentifier(value, message) {
|
|
1433
|
+
const tok = this.peek();
|
|
1434
|
+
if (tok.type === TokenType.Identifier && tok.lexeme === value) {
|
|
1435
|
+
this.advance();
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
throw this.errorAtToken(tok, message);
|
|
1439
|
+
}
|
|
1440
|
+
// --- Error helper ---
|
|
1441
|
+
errorAtToken(token, message) {
|
|
1442
|
+
return new Error(`Parse error at ${token.line}:${token.column} near '${token.lexeme}': ${message}`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
// Public API
|
|
1446
|
+
export function parseDocument(source) {
|
|
1447
|
+
const lexer = new Lexer(source);
|
|
1448
|
+
const tokens = lexer.tokenize();
|
|
1449
|
+
const parser = new Parser(tokens);
|
|
1450
|
+
return parser.parseDocument();
|
|
1451
|
+
}
|
|
1452
|
+
//# sourceMappingURL=parser.js.map
|