@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.
Files changed (53) hide show
  1. package/dist/args.d.ts +18 -0
  2. package/dist/args.d.ts.map +1 -0
  3. package/dist/args.js +60 -0
  4. package/dist/args.js.map +1 -0
  5. package/dist/ast.d.ts +194 -0
  6. package/dist/ast.d.ts.map +1 -0
  7. package/dist/ast.js +3 -0
  8. package/dist/ast.js.map +1 -0
  9. package/dist/bin/flux.d.ts +2 -0
  10. package/dist/bin/flux.d.ts.map +1 -0
  11. package/dist/bin/flux.js +157 -0
  12. package/dist/bin/flux.js.map +1 -0
  13. package/dist/checks.d.ts +12 -0
  14. package/dist/checks.d.ts.map +1 -0
  15. package/dist/checks.js +98 -0
  16. package/dist/checks.js.map +1 -0
  17. package/dist/commands/check.d.ts +2 -0
  18. package/dist/commands/check.d.ts.map +1 -0
  19. package/dist/commands/check.js +118 -0
  20. package/dist/commands/check.js.map +1 -0
  21. package/dist/commands/repl.d.ts +2 -0
  22. package/dist/commands/repl.d.ts.map +1 -0
  23. package/dist/commands/repl.js +37 -0
  24. package/dist/commands/repl.js.map +1 -0
  25. package/dist/fs-utils.d.ts +4 -0
  26. package/dist/fs-utils.d.ts.map +1 -0
  27. package/dist/fs-utils.js +34 -0
  28. package/dist/fs-utils.js.map +1 -0
  29. package/dist/index.d.ts +6 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +11 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/parse.d.ts +2 -0
  34. package/dist/parse.d.ts.map +1 -0
  35. package/dist/parse.js +107 -0
  36. package/dist/parse.js.map +1 -0
  37. package/dist/parser.d.ts +3 -0
  38. package/dist/parser.d.ts.map +1 -0
  39. package/dist/parser.js +1452 -0
  40. package/dist/parser.js.map +1 -0
  41. package/dist/runtime/kernel.d.ts +20 -0
  42. package/dist/runtime/kernel.d.ts.map +1 -0
  43. package/dist/runtime/kernel.js +356 -0
  44. package/dist/runtime/kernel.js.map +1 -0
  45. package/dist/runtime/model.d.ts +146 -0
  46. package/dist/runtime/model.d.ts.map +1 -0
  47. package/dist/runtime/model.js +3 -0
  48. package/dist/runtime/model.js.map +1 -0
  49. package/dist/version.d.ts +2 -0
  50. package/dist/version.d.ts.map +1 -0
  51. package/dist/version.js +3 -0
  52. package/dist/version.js.map +1 -0
  53. 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