novac 1.0.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.
@@ -0,0 +1,762 @@
1
+ /**
2
+ * Nova Parser (CommonJS)
3
+ * -----------------------
4
+ * Generates Nova-style AST with full expression support.
5
+ */
6
+ const { Lexer } = require("./lexer");
7
+ const { CustomError } = require("./error");
8
+
9
+ // Operator precedence map
10
+ const PRECEDENCE = {
11
+ "||": 1,
12
+ "&&": 2,
13
+ "==": 3,
14
+ "!=": 3,
15
+ "<": 4,
16
+ "<=": 4,
17
+ ">": 4,
18
+ ">=": 4,
19
+ "+": 5,
20
+ "-": 5,
21
+ "*": 6,
22
+ "/": 6,
23
+ "%": 6,
24
+ };
25
+
26
+ class Parser {
27
+ constructor(source) {
28
+ this.tokens = new Lexer(source).tokenize();
29
+ this.rawsrc = source;
30
+ this.current = 0;
31
+ this.symbols = new Map();
32
+ }
33
+
34
+ parse() {
35
+ const nodes = [];
36
+ while (!this.isAtEnd()) {
37
+ const node = this.statement();
38
+ if (node) nodes.push(node);
39
+ }
40
+ return { kind: "program", nodes };
41
+ }
42
+
43
+ nd(obj) {
44
+ let token = this.peek();
45
+ return {
46
+ ...obj,
47
+ line: token.line ?? 0,
48
+ column: token.column ?? 0,
49
+ };
50
+ }
51
+
52
+ // ─────────────── Statements ───────────────
53
+ statement() {
54
+ if (this.match("KEYWORD", "class")) return this.classDeclaration();
55
+ if (this.match("KEYWORD", "const")) return this.varDeclaration(true);
56
+ if (this.match("KEYWORD", "var") || this.match("KEYWORD", "let"))
57
+ return this.varDeclaration();
58
+ if (this.match("KEYWORD", "func")) return this.funcDeclaration(true);
59
+ if (this.match("KEYWORD", "if")) return this.branchBody("if");
60
+ if (this.match("KEYWORD", "while")) return this.branchBody("while");
61
+ if (this.match("KEYWORD", "repeat")) return this.branchBody("repeat");
62
+ if (this.match("KEYWORD", "do")) return this.branchBody("do");
63
+ if (this.match("KEYWORD", "until")) return this.branchBody("until");
64
+ if (this.match("KEYWORD", "unless")) return this.branchBody("unless");
65
+ if (this.match("KEYWORD", "for")) return this.branchBody("for", ";");
66
+ if (this.match("KEYWORD", "function")) return this.funcDeclaration();
67
+ if (this.match("KEYWORD", "return")) return this.returnStatement(false);
68
+ if (this.match("KEYWORD", "give")) return this.returnStatement(true);
69
+ if (this.match("KEYWORD", "throw")) return this.throwStatement();
70
+ if (this.match("KEYWORD", "try")) return this.tryStatement();
71
+ if (this.match("PUNCTUATION", "{")) return this.blockBody(true);
72
+ return this.expressionStatement();
73
+ }
74
+
75
+ templateLiteral() {
76
+ this.consume("TEMPLATE_START", "Expected start of template literal");
77
+
78
+ const parts = [];
79
+
80
+ while (!this.check("TEMPLATE_END") && !this.isAtEnd()) {
81
+ // Raw text part
82
+ if (this.match("STRING_PART")) {
83
+ parts.push({
84
+ type: "Literal",
85
+ value: this.previous().value,
86
+ });
87
+ continue;
88
+ }
89
+
90
+ // Interpolation section
91
+ if (this.match("INTERPOLATION_START")) {
92
+ const expr = this.expression();
93
+
94
+ // Expect matching INTERPOLATION_END now (old parser expected raw '}')
95
+ this.consume(
96
+ "INTERPOLATION_END",
97
+ "Expected '}' after expression in template literal"
98
+ );
99
+
100
+ parts.push(expr);
101
+ continue;
102
+ }
103
+
104
+ // Safety net for unexpected stuff
105
+ this.error(
106
+ `Unexpected token '${this.peek().value}' in template literal`
107
+ );
108
+ }
109
+
110
+ // Closing backtick
111
+ this.consume("TEMPLATE_END", "Unterminated template literal");
112
+
113
+ return {
114
+ type: "TemplateLiteral",
115
+ parts,
116
+ };
117
+ }
118
+
119
+ throwStatement() {
120
+ const value = this.expression();
121
+ this.match("PUNCTUATION", ";");
122
+ return this.nd({ kind: "throw", value });
123
+ }
124
+
125
+ tryStatement() {
126
+ const tryBody = this.blockBody();
127
+ let catchBody = null;
128
+ let catchName = null;
129
+ let finallyBody = null;
130
+
131
+ if (this.match("KEYWORD", "catch")) {
132
+ this.consume("PUNCTUATION", "(", "Expected '(' for catch block");
133
+ catchName = this.consume(
134
+ "IDENTIFIER",
135
+ "Expected catch variable name",
136
+ ).value;
137
+ this.consume("PUNCTUATION", ")", "Expected ')' for catch block");
138
+ catchBody = this.blockBody();
139
+ }
140
+
141
+ if (this.match("KEYWORD", "finally")) {
142
+ finallyBody = this.blockBody();
143
+ }
144
+
145
+ if (!catchBody && !finallyBody) {
146
+ this.error("Try block must be followed by catch or finally");
147
+ }
148
+
149
+ return this.nd({
150
+ kind: "try",
151
+ tryBody,
152
+ catchBody,
153
+ catchName,
154
+ finallyBody,
155
+ });
156
+ }
157
+
158
+ varDeclaration(isConst) {
159
+ let nameNode = null;
160
+ let destructure = null;
161
+ let isPointer = false;
162
+ if (this.match("OPERATOR", "*")) {
163
+ // 🔥 NEW/MODIFIED CHECK
164
+ isPointer = true;
165
+ }
166
+
167
+ // check destructuring pattern
168
+ if (this.check("PUNCTUATION", "{")) {
169
+ destructure = this.objectPattern();
170
+ } else if (this.check("PUNCTUATION", "[")) {
171
+ destructure = this.arrayPattern();
172
+ } else {
173
+ nameNode = this.consume("IDENTIFIER", "Expected variable name");
174
+
175
+ // 🔥 NEW: Check for explicit type annotation (e.g. let x: int)
176
+ let explicitType = null;
177
+ if (this.match("PUNCTUATION", ":")) {
178
+ explicitType = this.consume("IDENTIFIER", "Expected type name after ':'").value;
179
+ }
180
+
181
+ // 🔥 Attach to nameNode
182
+ nameNode = { ...nameNode, typeAnnotation: explicitType };
183
+ }
184
+
185
+ let value = null;
186
+ if (this.match("OPERATOR", "=")) {
187
+ value = this.expression();
188
+ }
189
+
190
+ this.match("PUNCTUATION", ";");
191
+
192
+ if (destructure) {
193
+ return this.nd({
194
+ kind: "declare",
195
+ destructure,
196
+ value,
197
+ isConst,
198
+ isPointer,
199
+ });
200
+ }
201
+
202
+ // 🧩 Store declared variable type in the symbol table
203
+ if (nameNode.value) {
204
+ this.symbols.set(nameNode.value, {
205
+ type: nameNode.typeAnnotation || null,
206
+ isConst,
207
+ });
208
+ }
209
+
210
+ // ✅ Parse-time type validation for explicit types
211
+ if (nameNode.typeAnnotation && value?.kind === "value") {
212
+ const val = value.value;
213
+ let inferredType;
214
+
215
+ if (typeof val === "number") {
216
+ inferredType = Number.isInteger(val) ? "int" : "float";
217
+ } else if (typeof val === "string") {
218
+ inferredType = "string";
219
+ } else if (typeof val === "boolean") {
220
+ inferredType = "bool";
221
+ } else if (typeof val === "object") {
222
+ inferredType = "obj";
223
+ } else {
224
+ inferredType = typeof val;
225
+ }
226
+
227
+ if (inferredType !== nameNode.typeAnnotation) {
228
+ this.error(
229
+ `[Type mismatch] '${nameNode.value}' expected '${nameNode.typeAnnotation}', got '${inferredType}'`,
230
+ this.peek()
231
+ );
232
+ }
233
+ }
234
+
235
+ return this.nd({
236
+ kind: "declare",
237
+ name: nameNode.value,
238
+ value,
239
+ isConst,
240
+ isPointer,
241
+ explicitType: nameNode.typeAnnotation || null, // ← new line
242
+ });
243
+ }
244
+
245
+ objectPattern() {
246
+ this.consume("PUNCTUATION", "{", "Expected '{' in destructuring");
247
+ const props = [];
248
+ if (!this.check("PUNCTUATION", "}")) {
249
+ do {
250
+ const key = this.consume("IDENTIFIER", "Expected property name").value;
251
+ let alias = key;
252
+ if (this.match("PUNCTUATION", ":")) {
253
+ alias = this.consume("IDENTIFIER", "Expected alias name").value;
254
+ }
255
+ props.push({ key, alias });
256
+ } while (this.match("PUNCTUATION", ","));
257
+ }
258
+ this.consume("PUNCTUATION", "}", "Expected '}'");
259
+ return { kind: "objpattern", props };
260
+ }
261
+
262
+ arrayPattern(Consumed) {
263
+ if (!Consumed)
264
+ this.consume("PUNCTUATION", "[", "Expected '[' in destructuring");
265
+ const elements = [];
266
+ if (!this.check("PUNCTUATION", "]")) {
267
+ do {
268
+ const name = this.consume("IDENTIFIER", "Expected element name").value;
269
+ elements.push(name);
270
+ } while (this.match("PUNCTUATION", ","));
271
+ }
272
+ this.consume("PUNCTUATION", "]", "Expected ']'");
273
+ return this.nd({ kind: "arrpattern", elements });
274
+ }
275
+
276
+ returnStatement(isGive) {
277
+ let value = null;
278
+ if (!this.check("PUNCTUATION", ";") && !this.check("EOF")) {
279
+ value = this.expression();
280
+ }
281
+ this.match("PUNCTUATION", ";");
282
+ return { kind: "return", value, terminate: isGive };
283
+ }
284
+
285
+ funcDeclaration(isArrow) {
286
+ let isAsync = false;
287
+ if (this.match("KEYWORD", "async")) isAsync = true;
288
+ const name = this.consume("IDENTIFIER", "Expected function name");
289
+ const { args, body } = this.parseFuncBody(isArrow);
290
+ return { kind: "function", name: name.value, args, body, isAsync };
291
+ }
292
+
293
+ classDeclaration() {
294
+ // Consume 'class' keyword (already matched in statement)
295
+ const name = this.consume("IDENTIFIER", "Expected class name");
296
+
297
+ // 🔥 NEW: Parse optional 'extends' clause
298
+ let superClass = null;
299
+ if (this.match("KEYWORD", "extends")) {
300
+ const superName = this.consume("IDENTIFIER", "Expected superclass name");
301
+ superClass = this.nd({ kind: "ref", name: superName.value }); // Store as a ref node
302
+ }
303
+
304
+ // Delegate parsing of the class body to objectDeclaration
305
+ const bodyObj = this.objectDeclaration(); // parses everything inside { ... }
306
+
307
+ // Convert objectDeclaration props into class members
308
+ const members = [];
309
+ for (const [k, v] of Object.entries(bodyObj.props)) {
310
+ members.push({
311
+ kind: v.kind === "function" ? "function" : "declare",
312
+ name: k,
313
+ value: v.kind !== "function" ? v : undefined,
314
+ args: v.kind === "function" ? v.args : undefined,
315
+ body: v.kind === "function" ? v.body : undefined,
316
+ });
317
+ }
318
+
319
+ // 🔥 Return the AST node with the new superClass property
320
+ return { kind: "class", name: name.value, superClass, members };
321
+ }
322
+
323
+ expressionStatement() {
324
+ let expr = this.expression();
325
+
326
+ // Handle assignment: a = b
327
+ if (expr.kind === "ref" && this.match("OPERATOR", "=")) {
328
+ const value = this.expression();
329
+ expr = { kind: "assign", name: expr.name, value };
330
+ }
331
+
332
+ this.match("PUNCTUATION", ";");
333
+ return { kind: "exec", expr };
334
+ }
335
+ constExpression(minPrec) {
336
+ return this.expression(minPrec, true);
337
+ }
338
+ // ─────────────── Expressions ───────────────
339
+ expression(minPrec = 0, isConst = false) {
340
+ if (this.isAtEnd()) return { kind: "EOF" };
341
+ let tok = this.peek();
342
+ let left;
343
+
344
+ // ───── Unary / Primary (Handling 'await') ─────
345
+ if (this.check("KEYWORD", "await")) {
346
+ const awaitToken = this.advance(); // consume 'await'
347
+ const operand = this.expression(9); // 9 for very high precedence
348
+ left = this.nd({ kind: "await", operand });
349
+ } else if (tok.type === "KEYWORD" && tok.value === "const") {
350
+ this.advance();
351
+ const operand = this.constExpression(minPrec);
352
+ left = this.nd({ kind: "value", value: operand });
353
+ } else if (tok.type === "OPERATOR" && tok.isUnary) {
354
+ const op = this.advance().value;
355
+ const operand = this.expression(7);
356
+ if (op === "*") left = this.nd({ kind: "deref", operand });
357
+ else left = this.nd({ kind: "unary", operator: op, operand });
358
+ } else if (tok.type === "PUNCTUATION" && tok.value === "(") {
359
+ // might be grouped expression OR arrow func params
360
+ const startPos = this.current;
361
+ this.advance(); // consume '('
362
+
363
+ const args = [];
364
+ let isArrow = false;
365
+
366
+ if (!this.check("PUNCTUATION", ")")) {
367
+ do {
368
+ args.push(this.expression());
369
+ } while (this.match("PUNCTUATION", ","));
370
+ }
371
+ this.consume("PUNCTUATION", ")", "Expected ')' after parameters");
372
+ if (this.match("OPERATOR", "=>")) isArrow = true;
373
+
374
+ if (isArrow) {
375
+ let body;
376
+ const values = args;
377
+ if (this.check("PUNCTUATION", "{")) {
378
+ body = this.blockBody();
379
+ } else {
380
+ body = [
381
+ this.nd({
382
+ kind: "return",
383
+ value: this.expression(),
384
+ terminate: true,
385
+ }),
386
+ ];
387
+ }
388
+ return this.nd({
389
+ kind: "arrowfunc",
390
+ args: values.map((a) => a?.name),
391
+ body,
392
+ });
393
+ } else {
394
+ // normal grouped expr
395
+ left = this.nd(args[0]);
396
+ }
397
+ } else if (this.match("PUNCTUATION", "[")) {
398
+ const elements = [];
399
+ if (!this.check("PUNCTUATION", "]")) {
400
+ do {
401
+ elements.push(this.expression());
402
+ } while (this.match("PUNCTUATION", ","));
403
+ }
404
+ this.consume("PUNCTUATION", "]", "Expected ']' after array literal");
405
+ left = this.nd({ kind: "array", elements });
406
+ } else if (tok.type === "PUNCTUATION" && tok.value === "{") {
407
+ left = this.objectDeclaration();
408
+ } else if (
409
+ tok.type === "NUMBER" ||
410
+ tok.type === "STRING" ||
411
+ tok.type === "LITERAL"
412
+ ) {
413
+ left = this.nd({ kind: "value", value: this.advance().value });
414
+ }
415
+
416
+ else if (tok.type === "TEMPLATE_START") {
417
+ left = this.templateLiteral();
418
+ } else if (tok.type === 'INTERPOLATION_END') {
419
+ return;
420
+ } else if (tok.type === "IDENTIFIER") {
421
+ // may be single-param arrow
422
+ const name = this.advance().value;
423
+ if (this.match("OPERATOR", "=>")) {
424
+ let body;
425
+ if (this.check("PUNCTUATION", "{")) {
426
+ body = this.blockBody();
427
+ } else {
428
+ body = [
429
+ this.nd({
430
+ kind: "return",
431
+ value: this.expression(),
432
+ terminate: true,
433
+ }),
434
+ ];
435
+ }
436
+ return this.nd({ kind: "arrowfunc", args: [name], body });
437
+ }
438
+ const sym = this.symbols.get(name);
439
+
440
+ left = this.nd({
441
+ kind: "ref",
442
+ name,
443
+ explicitType: sym ? sym.type : null, // 🪄 carry type info
444
+ })
445
+ } else if (tok.type === "EOF") {
446
+ return this.nd({ kind: "EOF" });
447
+ } else {
448
+ this.error(`Unexpected token '${tok.value}'`);
449
+ }
450
+
451
+ // ───── Postfix / Calls / Property ─────
452
+ while (!this.isAtEnd()) {
453
+ tok = this.peek();
454
+ if (
455
+ tok.type === "PUNCTUATION" &&
456
+ (tok.value === ";" || tok.value === "}")
457
+ )
458
+ break;
459
+
460
+ if (tok.type === "OPERATOR" && tok.isPostfix) {
461
+ const op = this.advance().value;
462
+ left = this.nd({ kind: "postfix", operator: op, operand: left });
463
+ continue;
464
+ }
465
+
466
+ if (tok.type === "IDENTIFIER" || tok.type === "NUMBER") {
467
+ const op = this.advance().value;
468
+ left = this.nd({ kind: "currency", name: op, operand: left });
469
+ continue;
470
+ }
471
+
472
+ if (tok.type === "PUNCTUATION" && tok.value === "(") {
473
+ this.advance();
474
+ const args = [];
475
+ if (!this.check("PUNCTUATION", ")")) {
476
+ do {
477
+ args.push(this.expression());
478
+ } while (this.match("PUNCTUATION", ","));
479
+ }
480
+ this.consume("PUNCTUATION", ")", "Expected ')' after arguments");
481
+ if (left.kind === "ref") {
482
+ switch (left.name) {
483
+ case 'PRS_goto':
484
+ let exec = new (require("./executor.js").Executor)();
485
+ let value = exec.evaluate(args[0]);
486
+ this.current = value;
487
+ left = this.nd({ kind: "value", value });
488
+ case 'PRS_generate':
489
+ left = this.nd({ kind: "value", value: (new Parser((new Lexer(args[0]?.value)).tokenize())).parse() });
490
+ }
491
+ }
492
+ else {
493
+ left = this.nd({
494
+ kind: "call",
495
+ name: left,
496
+ args,
497
+ });
498
+ }
499
+ continue;
500
+ }
501
+
502
+ if (tok.type === "PUNCTUATION" && tok.value === "{") {
503
+ this.advance();
504
+ let args = [];
505
+ if (!this.check("PUNCTUATION", "}")) {
506
+ do {
507
+ args.push(this.expression());
508
+ } while (this.match("PUNCTUATION", ","));
509
+ }
510
+ this.consume("PUNCTUATION", "}", "Expected '}' after construct opening brace");
511
+ left = this.nd({
512
+ kind: "construct",
513
+ left,
514
+ args,
515
+ });
516
+ continue;
517
+ }
518
+
519
+ if (tok.type === "PUNCTUATION" && tok.value === ".") {
520
+ this.advance();
521
+ const prop = this.consume("IDENTIFIER", "Expected property name");
522
+ left = this.nd({ kind: "prop", object: left, name: prop.value });
523
+ continue;
524
+ } else if (this.match("PUNCTUATION", "[")) {
525
+ const indexExpr = this.expression();
526
+ this.consume("PUNCTUATION", "]", "Expected ']' after subscript");
527
+ left = this.nd({ kind: "subscript", object: left, index: indexExpr });
528
+ continue;
529
+ }
530
+
531
+ break;
532
+ }
533
+
534
+ // ───── Binary operators ─────
535
+ while (!this.isAtEnd()) {
536
+ tok = this.peek();
537
+ if (tok.type !== "OPERATOR" || tok.value === ";" || tok.value === "}")
538
+ break;
539
+
540
+ const prec = tok.precedence || PRECEDENCE[tok.value] || 0;
541
+ if (prec < minPrec) break;
542
+
543
+ const op = this.advance().value;
544
+ const right = this.expression(prec + 1);
545
+
546
+ if (op === "=") {
547
+ // 🔥 Parse-time type validation for assignments
548
+ if (left && left.explicitType && right?.kind === "value") {
549
+ const val = right.value;
550
+ let inferredType;
551
+
552
+ if (typeof val === "number") {
553
+ inferredType = Number.isInteger(val) ? "int" : "float";
554
+ } else if (typeof val === "string") {
555
+ inferredType = "string";
556
+ } else if (typeof val === "boolean") {
557
+ inferredType = "bool";
558
+ } else if (typeof val === "object") {
559
+ inferredType = "obj";
560
+ } else {
561
+ inferredType = typeof val;
562
+ }
563
+
564
+ const expected = left.explicitType;
565
+ const compatible =
566
+ inferredType === expected ||
567
+ (expected === "float" && inferredType === "int"); // implicit promotion allowed
568
+
569
+ if (!compatible) {
570
+ this.error(
571
+ `[Type mismatch] '${left.name || left.value}' expected '${expected}', got '${inferredType}'`,
572
+ this.peek()
573
+ );
574
+ }
575
+ }
576
+
577
+ left = this.nd({
578
+ kind: "assign",
579
+ name: left,
580
+ value: right,
581
+ });
582
+ } else {
583
+ left = this.nd({ kind: "binary", operator: op, left, right });
584
+ }
585
+ }
586
+
587
+ if (isConst) {
588
+ let exec = new (require("./executor.js").Executor)();
589
+ return exec.evaluate(left);
590
+ }
591
+
592
+ return left;
593
+ }
594
+
595
+ // ─────────────── Helpers ───────────────
596
+ parseFuncBody(isArrow) {
597
+ this.consume("PUNCTUATION", "(", "Expected '('");
598
+ const args = [];
599
+ if (!this.check("PUNCTUATION", ")")) {
600
+ do {
601
+ args.push(this.consume("IDENTIFIER", "Expected argument name").value);
602
+ } while (this.match("PUNCTUATION", ","));
603
+ }
604
+ this.consume("PUNCTUATION", ")", "Expected ')'");
605
+ if (isArrow) this.consume("OPERATOR", "=>", "Expected '=>");
606
+ this.consume("PUNCTUATION", "{", "Expected '{'");
607
+ const body = [];
608
+ while (!this.check("PUNCTUATION", "}") && !this.isAtEnd())
609
+ body.push(this.statement());
610
+ this.consume("PUNCTUATION", "}", "Expected '}'");
611
+ return { args, body };
612
+ }
613
+
614
+ parseParenBody(branchName, SEP = ",") {
615
+ // Parse the arguments / condition
616
+ this.consume("PUNCTUATION", "(", "Expected '('");
617
+ const args = [];
618
+ if (branchName === "for") {
619
+ args.push(this.statement());
620
+ this.match("PUNCTUATION", ";");
621
+ args.push(this.expression());
622
+ this.match("PUNCTUATION", ";");
623
+ args.push(this.statement());
624
+ } else {
625
+ if (!this.check("PUNCTUATION", ")")) {
626
+ do {
627
+ args.push(this.expression());
628
+ } while (this.match("PUNCTUATION", SEP));
629
+ }
630
+ }
631
+ this.consume("PUNCTUATION", ")", "Expected ')'");
632
+
633
+ // Automatically handle block or single statement for body
634
+ let body;
635
+ if (this.check("PUNCTUATION", "{")) {
636
+ body = this.blockBody(); // grabs all statements inside { … }
637
+ } else {
638
+ body = [this.statement()]; // single statement
639
+ }
640
+ this.match("PUNCTUATION", ";");
641
+ // For `if`, only take the first argument as condition
642
+ return { args: branchName === "if" ? args[0] : args, body };
643
+ }
644
+
645
+ branchBody(name = "if", SEP = ",") {
646
+ const { args, body } = this.parseParenBody(name, SEP);
647
+ const branchNode = this.nd({ kind: "branch", type: name, args, body });
648
+
649
+ // check if the next statement is 'else' and attach it as branchNode.next
650
+ if (this.match("KEYWORD", "else")) {
651
+ let elseNode;
652
+ // plain else
653
+ const elseBody = [this.statement()];
654
+ elseNode = this.nd({
655
+ kind: "branch",
656
+ type: "else",
657
+ args: null,
658
+ body: elseBody,
659
+ });
660
+ branchNode.next = elseNode; // attach chain
661
+ }
662
+ this.match("PUNCTUATION", ";");
663
+ return branchNode;
664
+ }
665
+
666
+ blockBody(consumed) {
667
+ if (!consumed) this.consume("PUNCTUATION", "{", "Expected '{'");
668
+ const body = [];
669
+ while (!this.check("PUNCTUATION", "}") && !this.isAtEnd()) {
670
+ body.push(this.statement());
671
+ }
672
+ this.consume("PUNCTUATION", "}", "Expected '}'");
673
+ return body;
674
+ }
675
+
676
+ objectDeclaration() {
677
+ const props = {};
678
+ this.consume("PUNCTUATION", "{", "Expected '{' to start object literal");
679
+
680
+ while (!this.check("PUNCTUATION", "}") && !this.isAtEnd()) {
681
+ const tok = this.peek();
682
+
683
+ // Nested object literal
684
+ if (tok.type === "PUNCTUATION" && tok.value === "{") {
685
+ const nested = this.objectDeclaration();
686
+ Object.assign(props, nested.props);
687
+ continue;
688
+ }
689
+
690
+ // Property key
691
+ const nameTok = this.consume("IDENTIFIER", "Expected property name");
692
+ const propName = nameTok.value;
693
+ let value;
694
+
695
+ // Function value
696
+ if (this.check("PUNCTUATION", "(")) {
697
+ value = this.parseFuncBody();
698
+ } else {
699
+ this.consume("OPERATOR", ":", "Expected ':' after property name");
700
+ value = this.expression();
701
+ }
702
+
703
+ this.match("PUNCTUATION", ","); // optional
704
+ props[propName] = value;
705
+ }
706
+
707
+ this.consume("PUNCTUATION", "}", "Expected '}' to close object literal");
708
+ return this.nd({ kind: "object", props });
709
+ }
710
+
711
+ // ─────────────── Token helpers ───────────────
712
+ peek() {
713
+ return this.tokens[this.current];
714
+ }
715
+ previous() {
716
+ return this.tokens[this.current - 1];
717
+ }
718
+ next() {
719
+ return this.tokens[this.current + 1];
720
+ }
721
+ isAtEnd() {
722
+ return this.peek().type === "EOF";
723
+ }
724
+ advance() {
725
+ if (!this.isAtEnd()) this.current++;
726
+ return this.previous();
727
+ }
728
+ check(type, value = null) {
729
+ if (this.isAtEnd()) return false;
730
+ const t = this.peek();
731
+ if (t.type !== type) return false;
732
+ if (value !== null && t.value !== value) return false;
733
+ return true;
734
+ }
735
+ match(type, value = null) {
736
+ if (this.check(type, value)) {
737
+ this.advance();
738
+ return true;
739
+ }
740
+ return false;
741
+ }
742
+ consume(type, valueOrMsg, msgIfVal) {
743
+ const hasValue = typeof valueOrMsg === "string" && msgIfVal;
744
+ const expectedValue = hasValue ? valueOrMsg : null;
745
+ const msg = hasValue ? msgIfVal : valueOrMsg;
746
+ if (this.check(type, expectedValue)) return this.advance();
747
+ this.error(msg);
748
+ }
749
+
750
+ error(msg) {
751
+ const t = this.peek();
752
+ const lineText = this.rawsrc.split("\n")[t.line - 1] ?? "";
753
+ throw new new CustomError("ParseError")(
754
+ `[NovaParser ${t.line}:${t.column}] ${msg}\n` +
755
+ ` error at:\n` +
756
+ ` ${lineText}\n` +
757
+ ` ${" ".repeat(Math.max(t.column - 1, 0))}^^`,
758
+ );
759
+ }
760
+ }
761
+
762
+ module.exports = { Parser };