@katnip-org/compiler 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 (70) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +5 -0
  3. package/README.md +42 -0
  4. package/build/cli.d.ts +2 -0
  5. package/build/cli.js +141 -0
  6. package/build/cli.js.map +1 -0
  7. package/build/codegen/SB3Generator.d.ts +1 -0
  8. package/build/codegen/SB3Generator.js +2 -0
  9. package/build/codegen/SB3Generator.js.map +1 -0
  10. package/build/index.d.ts +3 -0
  11. package/build/index.js +26 -0
  12. package/build/index.js.map +1 -0
  13. package/build/ir/IRGenerator.d.ts +1 -0
  14. package/build/ir/IRGenerator.js +2 -0
  15. package/build/ir/IRGenerator.js.map +1 -0
  16. package/build/ir/IRNode.d.ts +1 -0
  17. package/build/ir/IRNode.js +2 -0
  18. package/build/ir/IRNode.js.map +1 -0
  19. package/build/lexer/Lexer.d.ts +69 -0
  20. package/build/lexer/Lexer.js +441 -0
  21. package/build/lexer/Lexer.js.map +1 -0
  22. package/build/lexer/LexerState.d.ts +14 -0
  23. package/build/lexer/LexerState.js +16 -0
  24. package/build/lexer/LexerState.js.map +1 -0
  25. package/build/lexer/Token.d.ts +105 -0
  26. package/build/lexer/Token.js +114 -0
  27. package/build/lexer/Token.js.map +1 -0
  28. package/build/parser/AST-nodes.d.ts +230 -0
  29. package/build/parser/AST-nodes.js +10 -0
  30. package/build/parser/AST-nodes.js.map +1 -0
  31. package/build/parser/BindingPowerTable.d.ts +8 -0
  32. package/build/parser/BindingPowerTable.js +37 -0
  33. package/build/parser/BindingPowerTable.js.map +1 -0
  34. package/build/parser/Parser.d.ts +208 -0
  35. package/build/parser/Parser.js +1215 -0
  36. package/build/parser/Parser.js.map +1 -0
  37. package/build/semantic/InternalTypes.d.ts +43 -0
  38. package/build/semantic/InternalTypes.js +95 -0
  39. package/build/semantic/InternalTypes.js.map +1 -0
  40. package/build/semantic/SemanticAnalyzer.d.ts +109 -0
  41. package/build/semantic/SemanticAnalyzer.js +998 -0
  42. package/build/semantic/SemanticAnalyzer.js.map +1 -0
  43. package/build/semantic/StdlibLoader.d.ts +18 -0
  44. package/build/semantic/StdlibLoader.js +42 -0
  45. package/build/semantic/StdlibLoader.js.map +1 -0
  46. package/build/semantic/SymbolTable.d.ts +64 -0
  47. package/build/semantic/SymbolTable.js +28 -0
  48. package/build/semantic/SymbolTable.js.map +1 -0
  49. package/build/utils/ErrorReporter.d.ts +29 -0
  50. package/build/utils/ErrorReporter.js +84 -0
  51. package/build/utils/ErrorReporter.js.map +1 -0
  52. package/build/utils/Logger.d.ts +32 -0
  53. package/build/utils/Logger.js +62 -0
  54. package/build/utils/Logger.js.map +1 -0
  55. package/build/utils/colors.d.ts +8 -0
  56. package/build/utils/colors.js +14 -0
  57. package/build/utils/colors.js.map +1 -0
  58. package/package.json +58 -0
  59. package/stdlib/clone.knip +5 -0
  60. package/stdlib/console.knip +6 -0
  61. package/stdlib/control.knip +3 -0
  62. package/stdlib/dict.knip +7 -0
  63. package/stdlib/events.knip +3 -0
  64. package/stdlib/list.knip +10 -0
  65. package/stdlib/looks.knip +4 -0
  66. package/stdlib/math.knip +7 -0
  67. package/stdlib/motion.knip +25 -0
  68. package/stdlib/pen.knip +14 -0
  69. package/stdlib/prelude.knip +25 -0
  70. package/stdlib/str.knip +3 -0
@@ -0,0 +1,1215 @@
1
+ /**
2
+ * @fileoverview Contains the main parser class for the Katnip compiler.
3
+ */
4
+ import { isValuedTokenType, UnitTokenType } from "../lexer/Token.js";
5
+ import { ErrorReporter, KatnipError } from "../utils/ErrorReporter.js";
6
+ import { KatnipLog, KatnipLogType, Logger } from "../utils/Logger.js";
7
+ import { VariableDeclarationType } from "./AST-nodes.js";
8
+ import { bindingPowerTable, getBindingPower } from "./BindingPowerTable.js";
9
+ export class Parser {
10
+ reporter;
11
+ logger;
12
+ tokens = [];
13
+ position = 0;
14
+ constructor(reporter, logger = new Logger()) {
15
+ this.reporter = reporter;
16
+ this.logger = logger;
17
+ }
18
+ /**
19
+ * Parses a list of tokens into an Abstract Syntax Tree (AST).
20
+ * @param tokens - The list of tokens to parse.
21
+ * @returns The resulting AST or void if parsing fails.
22
+ */
23
+ parse(tokens) {
24
+ this.logger.print(new KatnipLog(KatnipLogType.Info, `--- Parsing started with ${tokens.length} tokens ---`));
25
+ this.tokens = tokens;
26
+ this.position = 0;
27
+ const statements = [];
28
+ while (!this.isAtEnd()) {
29
+ const before = this.position;
30
+ this.logger.log(new KatnipLog(KatnipLogType.Debug, `Parsing statement at token: ${this.peek()?.token.type}`));
31
+ const stmt = this.parseStatement();
32
+ if (stmt)
33
+ statements.push(stmt);
34
+ if (this.position === before) {
35
+ this.reporter.add(new KatnipError("Parser", `Parser got stuck on token '${this.peek()?.token.type}'`, this.peek()?.start ?? { line: -1, column: -1 }));
36
+ this.advance();
37
+ }
38
+ }
39
+ return { type: "Program", body: statements };
40
+ }
41
+ // -- Helper functions --
42
+ /**
43
+ * Retrieves the current token without advancing the position.
44
+ * @param lookAhead - Number of tokens to look ahead.
45
+ * @returns The current token or null if at the end.
46
+ */
47
+ peek(lookAhead = 0) {
48
+ const index = this.position + lookAhead;
49
+ return index < this.tokens.length ? this.tokens[index] : null;
50
+ }
51
+ /**
52
+ * Retrieves the previous token.
53
+ * @returns The previous token or null if at the beginning.
54
+ */
55
+ previous() {
56
+ return this.position > 0 ? this.tokens[this.position - 1] : null;
57
+ }
58
+ /**
59
+ * Advances the position and retrieves the previous token.
60
+ * @returns The previous token or null if at the beginning.
61
+ */
62
+ advance() {
63
+ if (!this.isAtEnd()) {
64
+ this.position++;
65
+ }
66
+ return this.previous();
67
+ }
68
+ /**
69
+ * Checks if the parser has reached the end of the token list.
70
+ * @returns True if at the end, false otherwise.
71
+ */
72
+ isAtEnd() {
73
+ return this.peek()?.token.type === "<EOF>";
74
+ }
75
+ /**
76
+ * Checks if the current token matches the specified type or value.
77
+ * @param kind - The kind of check ("type" or "value").
78
+ * @param patterns - The patterns to match against.
79
+ * @returns True if the token matches, false otherwise.
80
+ */
81
+ checkToken(kind, patterns) {
82
+ if (this.isAtEnd())
83
+ return false;
84
+ const token = this.peek();
85
+ if (!token)
86
+ return false;
87
+ if (kind === "type") {
88
+ return patterns.includes(token.token.type);
89
+ }
90
+ else {
91
+ if (!isValuedTokenType(token.token.type))
92
+ return false;
93
+ return patterns.includes(token.token.value);
94
+ }
95
+ }
96
+ /**
97
+ * Attempts to consume the current token if it matches the specified type or value.
98
+ * @param kind - The kind of check ("type" or "value").
99
+ * @param patterns - The patterns to match against.
100
+ * @returns True if the token was consumed, false otherwise.
101
+ */
102
+ tryConsume(kind, patterns) {
103
+ if (this.checkToken(kind, patterns)) {
104
+ this.advance();
105
+ return true;
106
+ }
107
+ return false;
108
+ }
109
+ /**
110
+ * Consumes the current token if it matches the specified type or value.
111
+ * Reports an error if the token does not match.
112
+ * @param options - The options specifying the type and/or value to match.
113
+ * @param message - The error message to report if the token does not match.
114
+ * @returns The consumed token information or an error token.
115
+ */
116
+ consume(options, message, sync) {
117
+ const types = options.type ? (Array.isArray(options.type) ? options.type : [options.type]) : [];
118
+ const values = options.value ? (Array.isArray(options.value) ? options.value : [options.value]) : [];
119
+ if (types.length && this.tryConsume("type", types)) {
120
+ return this.previous();
121
+ }
122
+ if (values.length && this.tryConsume("value", values)) {
123
+ return this.previous();
124
+ }
125
+ if (!types.length && !values.length)
126
+ throw new Error("consume() must be called with either type or value");
127
+ const previousToken = this.previous();
128
+ const currentToken = this.peek();
129
+ if (previousToken) {
130
+ this.reporter.add(new KatnipError("Parser", message, {
131
+ line: previousToken.end.line,
132
+ column: previousToken.end.column
133
+ }));
134
+ }
135
+ else if (currentToken) {
136
+ this.reporter.add(new KatnipError("Parser", message, currentToken.start));
137
+ }
138
+ const before = this.position;
139
+ if (sync) {
140
+ this.synchronize(sync, "Failed to synchronize after consume error");
141
+ }
142
+ if (this.position === before && !this.isAtEnd()) {
143
+ this.advance();
144
+ }
145
+ return {
146
+ token: { type: "ErrorToken", value: "" },
147
+ start: { line: -1, column: -1 },
148
+ end: { line: -1, column: -1 }
149
+ };
150
+ }
151
+ /**
152
+ * Checks whether the current token matches without consuming it.
153
+ * Reports an error if it does not match.
154
+ * @param options - Token type/value options to check.
155
+ * @param message - Error message to report on failure.
156
+ * @returns True when the token matches, false otherwise.
157
+ */
158
+ expect(options, message) {
159
+ const types = options.type
160
+ ? (Array.isArray(options.type) ? options.type : [options.type])
161
+ : [];
162
+ const values = options.value
163
+ ? (Array.isArray(options.value) ? options.value : [options.value])
164
+ : [];
165
+ if ((types.length && this.checkToken("type", types)) ||
166
+ (values.length && this.checkToken("value", values))) {
167
+ return true;
168
+ }
169
+ const prev = this.previous();
170
+ if (prev) {
171
+ this.reporter.add(new KatnipError("Parser", message, {
172
+ line: prev.end.line,
173
+ column: prev.end.column
174
+ }));
175
+ }
176
+ else if (this.peek()) {
177
+ this.reporter.add(new KatnipError("Parser", message, this.peek().start));
178
+ }
179
+ return false;
180
+ }
181
+ /**
182
+ * Advances tokens until a matching type/value is found or EOF is reached.
183
+ * Reports an error if the end is reached without finding a match.
184
+ * @param options - Token type/value options to synchronize on.
185
+ * @param message - Error message to report on failure.
186
+ */
187
+ synchronize(options, message) {
188
+ const beginning = this.peek().start;
189
+ const types = options.type
190
+ ? (Array.isArray(options.type) ? options.type : [options.type])
191
+ : [];
192
+ const values = options.value
193
+ ? (Array.isArray(options.value) ? options.value : [options.value])
194
+ : [];
195
+ const before = this.position;
196
+ while (this.peek() != null && !this.checkToken("type", types) && !this.checkToken("value", values)) {
197
+ this.advance();
198
+ }
199
+ if (this.position === before && this.peek() != null) {
200
+ this.advance();
201
+ }
202
+ if (this.peek() == null) {
203
+ this.reporter.add(new KatnipError("Parser", message, beginning));
204
+ }
205
+ }
206
+ /**
207
+ * Consumes a statement-terminating ';'.
208
+ * If it is missing, reports the errorbut does NOT advance.
209
+ * The current token starts the next statement.
210
+ */
211
+ expectSemicolon(message) {
212
+ if (this.tryConsume("type", [";"]))
213
+ return;
214
+ const prev = this.previous();
215
+ this.reporter.add(new KatnipError("Parser", message, prev
216
+ ? { line: prev.end.line, column: prev.end.column }
217
+ : this.peek()?.start ?? { line: -1, column: -1 }));
218
+ }
219
+ // -- Statement parsing --
220
+ /**
221
+ * Parses through and returns a type annotation.
222
+ * @returns The parsed type annotation
223
+ */
224
+ parseTypeAnnotation() {
225
+ return this.parseUnionType();
226
+ }
227
+ /**
228
+ * Parses a union type expression (e.g., A | B).
229
+ * @returns The parsed type node.
230
+ */
231
+ parseUnionType() {
232
+ let left = this.parsePrimaryType();
233
+ while (this.tryConsume("type", ["|"])) {
234
+ const right = this.parsePrimaryType();
235
+ left = {
236
+ type: "UnionType",
237
+ left,
238
+ right,
239
+ loc: { start: left.loc.start, end: right.loc.end }
240
+ };
241
+ }
242
+ return left;
243
+ }
244
+ /**
245
+ * Parses a primary type, including optional generic parameters.
246
+ * @returns The parsed type node.
247
+ */
248
+ parsePrimaryType() {
249
+ if (this.checkToken("type", ["("])) {
250
+ return this.parseTupleType();
251
+ }
252
+ const typeNameToken = this.consume({ type: "Identifier" }, "Expected type name");
253
+ const typeName = typeNameToken.token.value;
254
+ if (this.tryConsume("type", ["<"])) {
255
+ const typeParams = [];
256
+ while (!this.checkToken("type", [">"])) {
257
+ typeParams.push(this.parseTypeAnnotation());
258
+ if (!this.tryConsume("type", [","]) && !this.checkToken("type", [">"])) {
259
+ this.reporter.add(new KatnipError("Parser", "Expected ',' or '>'", this.peek()?.start ?? { line: -1, column: -1 }));
260
+ }
261
+ }
262
+ this.consume({ type: ">" }, "Expected closing '>'");
263
+ return {
264
+ type: "Type",
265
+ typeName,
266
+ typeParams,
267
+ loc: { start: typeNameToken.start, end: this.previous().end }
268
+ };
269
+ }
270
+ return {
271
+ type: "Type",
272
+ typeName,
273
+ loc: { start: typeNameToken.start, end: typeNameToken.end }
274
+ };
275
+ }
276
+ /**
277
+ * Parses a tuple type, e.g. (str, num). Used for fixed-shape sequences
278
+ * like the element type of list<(str, num)>.
279
+ * @returns The parsed tuple type node.
280
+ */
281
+ parseTupleType() {
282
+ const open = this.consume({ type: "(" }, "Expected '(' for tuple type");
283
+ const elements = [];
284
+ if (!this.checkToken("type", [")"])) {
285
+ do {
286
+ elements.push(this.parseTypeAnnotation());
287
+ } while (this.tryConsume("type", [","]));
288
+ }
289
+ const close = this.consume({ type: ")" }, "Expected ')' to close tuple type");
290
+ return {
291
+ type: "TupleType",
292
+ elements,
293
+ loc: { start: open.start, end: close.end }
294
+ };
295
+ }
296
+ /**
297
+ * Parses a single statement from the token list.
298
+ * @returns The parsed statement or an error token.
299
+ */
300
+ parseStatement() {
301
+ this.logger.log(new KatnipLog(KatnipLogType.Debug, `parsing statement starting with token: ${this.peek()?.token.type} | value: ${isValuedTokenType(this.peek()?.token.type || "<EOF>") ? (this.peek()?.token).value : "N/A"}`));
302
+ if (this.checkToken("type", ["Comment_SingleExpanded", "Comment_SingleCollapsed", "Comment_MultilineExpanded", "Comment_MultilineCollapsed"])) {
303
+ const comment = (this.peek()?.token).value;
304
+ this.advance();
305
+ return null;
306
+ // return comment;
307
+ }
308
+ if (this.checkToken("type", ["Identifier"])) {
309
+ if (this.checkToken("value", ["proc"]) && !(this.peek(1)?.token.type === "."))
310
+ return this.parseProcedureDefinition();
311
+ if (this.checkToken("value", ["enum"]))
312
+ return this.parseEnumDefinition();
313
+ if (this.checkToken("value", ["private", "temp", "public"])) {
314
+ const next = this.peek(1)?.token;
315
+ const nextVal = next && isValuedTokenType(next.type) ? next.value : undefined;
316
+ if (nextVal === "proc")
317
+ return this.parseProcedureDefinition();
318
+ if (nextVal === "enum")
319
+ return this.parseEnumDefinition();
320
+ return this.parseVariableDeclaration();
321
+ }
322
+ if (this.checkToken("value", ["sprite"]))
323
+ return this.parseSpriteDefinition();
324
+ if (this.checkToken("value", ["if"]))
325
+ return this.parseIfStatement();
326
+ if (this.checkToken("value", ["for"]))
327
+ return this.parseForStatement();
328
+ if (this.checkToken("value", ["while"]))
329
+ return this.parseWhileStatement();
330
+ if (this.checkToken("value", ["do"]))
331
+ return this.parseDoWhileStatement();
332
+ if (this.checkToken("value", ["return"]))
333
+ return this.parseReturnStatement();
334
+ if (this.checkToken("value", ["switch"]))
335
+ return this.parseSwitchStatement();
336
+ if (this.checkToken("value", ["case"]))
337
+ return this.parseCaseStatement();
338
+ if (this.checkToken("value", ["default"]))
339
+ return this.parseDefaultCaseStatement();
340
+ // Parse an expression statement
341
+ const expression = this.parseExpression();
342
+ const assignmentExpression = this.parseAssignmentExpression(expression);
343
+ if (assignmentExpression) {
344
+ return assignmentExpression;
345
+ }
346
+ const handlerDeclaration = this.parseHandlerDeclaration(expression);
347
+ if (handlerDeclaration) {
348
+ return handlerDeclaration;
349
+ }
350
+ this.expectSemicolon("Expected semicolon at the end of an expression statement");
351
+ return {
352
+ type: "ExpressionStatement",
353
+ expression: expression,
354
+ loc: { start: expression.loc.start, end: expression.loc.end }
355
+ };
356
+ }
357
+ this.reporter.add(new KatnipError("Parser", `Unexpected token: Type '${this.peek().token.type}'`, this.peek()?.start || { line: -1, column: -1 }));
358
+ this.advance();
359
+ return { type: "ErrorStatement", message: "Failed to parse statement", loc: { start: { line: -1, column: -1 }, end: { line: -1, column: -1 } } };
360
+ }
361
+ /**
362
+ * Parses a return statement: `return;` or `return <expression>;`.
363
+ * @returns The parsed return statement node.
364
+ */
365
+ parseReturnStatement() {
366
+ const keyword = this.consume({ type: "Identifier", value: "return" }, "Expected 'return' keyword");
367
+ let argument = null;
368
+ if (!this.checkToken("type", [";"])) {
369
+ argument = this.parseExpression();
370
+ }
371
+ this.expectSemicolon("Expected ';' at the end of return statement");
372
+ return {
373
+ type: "ReturnStatement",
374
+ argument,
375
+ loc: { start: keyword.start, end: this.previous()?.end || keyword.end },
376
+ };
377
+ }
378
+ /**
379
+ * Parses an optional leading access modifier (`public`/`private`/`temp`).
380
+ * Defaults to `private`. `start` is the modifier's position (or null) to anchor at loc.
381
+ */
382
+ parseAccessModifier() {
383
+ if (this.checkToken("value", ["private", "public", "temp"])) {
384
+ const tok = this.consume({ type: "Identifier", value: Object.values(VariableDeclarationType) }, "Expected access modifier");
385
+ return { access: tok.token.value, start: tok.start };
386
+ }
387
+ return { access: VariableDeclarationType.private, start: null };
388
+ }
389
+ /**
390
+ * Parses a procedure definition from the token list.
391
+ * @returns The parsed procedure declaration node.
392
+ */
393
+ parseProcedureDefinition() {
394
+ const { access, start: accessStart } = this.parseAccessModifier();
395
+ this.consume({ type: "Identifier", value: "proc" }, "Expected 'proc' keyword");
396
+ const nameToken = this.consume({ type: "Identifier" }, "Expected procedure name");
397
+ const name = nameToken.token.value;
398
+ let parsingDecorators = true;
399
+ const decorators = [];
400
+ const parameters = [];
401
+ if (this.tryConsume("type", ["("])) {
402
+ while (!this.checkToken("type", [")"])) {
403
+ this.logger.log(new KatnipLog(KatnipLogType.Debug, `parsing proc param/decorator, next token: ${this.peek()?.token.type}, ${this.peek() && isValuedTokenType(this.peek().token.type) ? this.peek().token.value : "N/A"}`));
404
+ if (this.checkToken("type", ["@"])) {
405
+ this.advance();
406
+ }
407
+ else if (this.checkToken("type", ["Identifier"])) {
408
+ parsingDecorators = false;
409
+ }
410
+ else {
411
+ this.reporter.add(new KatnipError("Parser", "Expected decorator or parameter", this.peek()?.start || { line: -1, column: -1 }));
412
+ }
413
+ if (parsingDecorators) {
414
+ this.logger.log(new KatnipLog(KatnipLogType.Debug, "parsing decorators"));
415
+ const decoratorNameToken = this.consume({ type: "Identifier" }, "Expected decorator name");
416
+ const decoratorName = decoratorNameToken.token.value;
417
+ let decoratorValue;
418
+ if (!this.expect({ type: ["=", ",", ")"] }, "Expected '=', ',' or ')' after decorator name")) {
419
+ this.synchronize({ type: [",", "=", ")"] }, "Failed to parse decorator value");
420
+ }
421
+ if (this.tryConsume("type", ["="])) {
422
+ decoratorValue = this.parseExpression();
423
+ }
424
+ else {
425
+ decoratorValue = {
426
+ type: "Literal",
427
+ value: "true",
428
+ valueType: "String",
429
+ loc: {
430
+ start: decoratorNameToken.start,
431
+ end: decoratorNameToken.end
432
+ }
433
+ };
434
+ }
435
+ decorators.push({
436
+ type: "Decorator",
437
+ name: decoratorName,
438
+ value: decoratorValue,
439
+ loc: {
440
+ start: decoratorValue.loc.start,
441
+ end: decoratorValue.loc.end
442
+ }
443
+ });
444
+ }
445
+ else {
446
+ const parameterNameToken = this.consume({ type: "Identifier" }, "Expected parameter name");
447
+ const parameterName = parameterNameToken.token.value;
448
+ this.consume({ type: ":" }, "Expected ':' after parameter name for type annotation");
449
+ const parameterTypeToken = this.parseTypeAnnotation();
450
+ this.logger.log(new KatnipLog(KatnipLogType.Debug, `Parsed parameter type: ${JSON.stringify(parameterTypeToken)}`));
451
+ let defaultValueToken;
452
+ if (this.tryConsume("type", ["="])) {
453
+ defaultValueToken = this.parseExpression();
454
+ }
455
+ parameters.push({
456
+ type: "Parameter",
457
+ name: parameterName,
458
+ paramType: parameterTypeToken,
459
+ default: defaultValueToken,
460
+ loc: {
461
+ start: parameterNameToken.start,
462
+ end: defaultValueToken?.loc.end || parameterTypeToken.loc.end
463
+ }
464
+ });
465
+ }
466
+ if (!this.checkToken("type", [")"])) {
467
+ this.consume({ type: "," }, "Expected ',' or ')'");
468
+ }
469
+ }
470
+ this.consume({ type: ")" }, "Expected closing parenthesis for parameters");
471
+ }
472
+ this.logger.log(new KatnipLog(KatnipLogType.Debug, "❌ - Finished parsing parameters/decorators"));
473
+ this.consume({ type: "->" }, "Expected arrow return symbol '->'");
474
+ const returnType = this.parseTypeAnnotation();
475
+ this.consume({ type: "{" }, "Expected open curly brace '{' for procedure body");
476
+ const stmts = [];
477
+ while (!this.isAtEnd() && !this.checkToken("type", ["}"])) {
478
+ const stmt = this.parseStatement();
479
+ if (stmt)
480
+ stmts.push(stmt);
481
+ }
482
+ this.consume({ type: "}" }, "Expected closing curly brace '}' for procedure body");
483
+ const body = {
484
+ type: "Block",
485
+ body: stmts,
486
+ loc: { start: stmts[0]?.loc.start || nameToken.start, end: stmts[stmts.length - 1]?.loc.end || nameToken.start }
487
+ };
488
+ return {
489
+ type: "ProcedureDeclaration",
490
+ access,
491
+ name,
492
+ decorators,
493
+ parameters,
494
+ returnType,
495
+ body,
496
+ loc: {
497
+ start: accessStart ?? nameToken.start,
498
+ end: this.previous()?.end || { line: -1, column: -1 }
499
+ }
500
+ };
501
+ }
502
+ /**
503
+ * Parses an enum definition from the token list.
504
+ * @returns The parsed enum declaration node.
505
+ */
506
+ parseEnumDefinition() {
507
+ const { access, start: accessStart } = this.parseAccessModifier();
508
+ this.consume({ type: "Identifier", value: "enum" }, "Expected 'enum' keyword");
509
+ const nameToken = this.consume({ type: "Identifier" }, "Expected enum name");
510
+ const name = nameToken.token.value;
511
+ this.consume({ type: "{" }, "Expected opening brace for enum members");
512
+ const members = [];
513
+ while (!this.checkToken("type", ["}"])) {
514
+ const memberToken = this.consume({ type: "Identifier" }, "Expected enum member name");
515
+ members.push(memberToken.token.value);
516
+ if (!this.tryConsume("type", [","]) && !this.checkToken("type", ["}"])) {
517
+ this.reporter.add(new KatnipError("Parser", "Expected ',' or '}'", this.peek()?.start || { line: -1, column: -1 }));
518
+ }
519
+ }
520
+ this.consume({ type: "}" }, "Expected closing brace for enum members");
521
+ return {
522
+ type: "EnumDeclaration",
523
+ access,
524
+ name: name,
525
+ members: members,
526
+ loc: {
527
+ start: accessStart ?? nameToken.start,
528
+ end: this.previous()?.end || { line: -1, column: -1 }
529
+ }
530
+ };
531
+ }
532
+ /**
533
+ * Parses a variable declaration from the token list.
534
+ * @returns The parsed variable declaration node.
535
+ */
536
+ parseVariableDeclaration() {
537
+ this.logger.log(new KatnipLog(KatnipLogType.Debug, `parsing variable declaration starting with token: ${this.peek()?.token.type}`));
538
+ const access = this.consume({ type: "Identifier", value: Object.values(VariableDeclarationType) }, "Expected 'private', 'temp', or 'public' keyword");
539
+ const variableName = this.consume({ type: "Identifier" }, "Expected variable name");
540
+ // Valid fomations: no type + yes init, yes type + yes init, yes type + no init
541
+ // INvalid formations: no type + no init
542
+ let typeAnnotation = null;
543
+ let initializer = null;
544
+ if (this.tryConsume("type", [":"])) {
545
+ typeAnnotation = this.parseTypeAnnotation();
546
+ if (this.tryConsume("type", ["="])) {
547
+ initializer = this.parseExpression();
548
+ }
549
+ }
550
+ else {
551
+ this.consume({ type: "=" }, "Expected '=' for variable value declaration");
552
+ initializer = this.parseExpression();
553
+ }
554
+ this.expectSemicolon("Expected ';' at the end of variable declaration");
555
+ return {
556
+ type: "VariableDeclaration",
557
+ access: access.token.value,
558
+ name: variableName.token.value,
559
+ varType: typeAnnotation,
560
+ initializer: initializer,
561
+ loc: {
562
+ start: access.start,
563
+ end: this.previous()?.end || { line: -1, column: -1 }
564
+ }
565
+ };
566
+ }
567
+ parseSpriteDefinition() {
568
+ this.consume({ type: "Identifier", value: "sprite" }, "Expected 'sprite' keyword");
569
+ const nameToken = this.consume({ type: "Identifier" }, "Expected sprite name");
570
+ const name = nameToken.token.value;
571
+ const stmts = this.parseBlockExpression();
572
+ const body = {
573
+ type: "Block",
574
+ body: stmts,
575
+ loc: { start: stmts[0]?.loc.start || nameToken.start, end: stmts[stmts.length - 1]?.loc.end || nameToken.start }
576
+ };
577
+ return {
578
+ type: "SpriteDeclaration",
579
+ name,
580
+ body,
581
+ loc: { start: nameToken.start, end: this.previous()?.end || nameToken.start }
582
+ };
583
+ }
584
+ /**
585
+ * Parses an assignment expression using a previously parsed left-hand side.
586
+ * @param left - The expression to assign into.
587
+ * @returns The assignment node or null if no assignment operator follows.
588
+ */
589
+ parseAssignmentExpression(left) {
590
+ if (!this.checkToken("type", ["=", "+=", "-=", "*=", "/=", "%=", "**="])) {
591
+ return null;
592
+ }
593
+ const assginmentOperator = this.consume({ type: [
594
+ UnitTokenType.Equals,
595
+ UnitTokenType.PlusEquals,
596
+ UnitTokenType.MinusEquals,
597
+ UnitTokenType.AsteriskEquals,
598
+ UnitTokenType.FwdSlashEquals,
599
+ UnitTokenType.PercentEquals,
600
+ UnitTokenType.PowerEquals
601
+ ] }, "Expected assignment operator").token.type;
602
+ const right = this.parseExpression();
603
+ this.expectSemicolon("Expected semicolon at the end of an expression statement");
604
+ return {
605
+ type: "VariableAssignment",
606
+ operator: assginmentOperator,
607
+ left: left,
608
+ right: right,
609
+ loc: { start: left.loc.start, end: right.loc.end }
610
+ };
611
+ }
612
+ /**
613
+ * Parses a handler declaration following a call expression.
614
+ * @param left - The call expression that starts the handler declaration.
615
+ * @returns The handler declaration node or null if not applicable.
616
+ */
617
+ parseHandlerDeclaration(left) {
618
+ if (left.type == "CallExpression" && this.checkToken("type", ["{"])) {
619
+ const body = this.parseBlockExpression();
620
+ return {
621
+ type: "HandlerStatement",
622
+ call: left,
623
+ body: {
624
+ type: "Block",
625
+ body: body,
626
+ loc: {
627
+ start: body[0]?.loc.start || left.loc.start,
628
+ end: body[body.length - 1]?.loc.end || left.loc.end,
629
+ },
630
+ },
631
+ loc: {
632
+ start: left.loc.start,
633
+ end: body[body.length - 1]?.loc.end || left.loc.end,
634
+ },
635
+ };
636
+ }
637
+ return null;
638
+ }
639
+ /**
640
+ * Parses a for statement from the token list.
641
+ * @returns The parsed for statement node.
642
+ */
643
+ parseForStatement() {
644
+ this.consume({ type: "Identifier", value: "for" }, "Expected 'for' keyword");
645
+ this.consume({ type: "(" }, "Expected '(' after 'for'");
646
+ const rawPattern = this.parseExpression();
647
+ let pattern;
648
+ if (rawPattern.type === "Identifier" || rawPattern.type === "TupleExpression") {
649
+ pattern = rawPattern;
650
+ }
651
+ else {
652
+ this.reporter.add(new KatnipError("Parser", "Expected identifier or tuple pattern in for loop", rawPattern.loc.start));
653
+ pattern = {
654
+ type: "Identifier",
655
+ name: "_",
656
+ loc: rawPattern.loc
657
+ };
658
+ }
659
+ this.consume({ type: "," }, "Expected ',' after for loop pattern");
660
+ const iterable = this.parseExpression();
661
+ this.consume({ type: ")" }, "Expected ')' after for loop iterable");
662
+ const bodyExpression = this.parseBlockExpression();
663
+ const body = {
664
+ type: "Block",
665
+ body: bodyExpression,
666
+ loc: {
667
+ start: bodyExpression[0]?.loc.start || iterable.loc.end,
668
+ end: bodyExpression[bodyExpression.length - 1]?.loc.end || iterable.loc.end
669
+ }
670
+ };
671
+ return {
672
+ type: "ForStatement",
673
+ pattern: pattern,
674
+ iterable,
675
+ body,
676
+ loc: {
677
+ start: pattern.loc.start,
678
+ end: body.loc.end
679
+ }
680
+ };
681
+ }
682
+ /**
683
+ * Parses a while statement from the token list.
684
+ * @returns The parsed while statement node.
685
+ */
686
+ parseWhileStatement() {
687
+ this.consume({ type: "Identifier", value: "while" }, "Expected 'while' keyword");
688
+ this.consume({ type: "(" }, "Expected '(' after 'while'");
689
+ const condition = this.parseExpression();
690
+ this.consume({ type: ")" }, "Expected ')' after while condition");
691
+ const bodyExpression = this.parseBlockExpression();
692
+ const body = {
693
+ type: "Block",
694
+ body: bodyExpression,
695
+ loc: {
696
+ start: bodyExpression[0]?.loc.start || condition.loc.end,
697
+ end: bodyExpression[bodyExpression.length - 1]?.loc.end || condition.loc.end
698
+ }
699
+ };
700
+ return {
701
+ type: "WhileStatement",
702
+ condition,
703
+ body,
704
+ loc: {
705
+ start: condition.loc.start,
706
+ end: body.loc.end
707
+ }
708
+ };
709
+ }
710
+ /**
711
+ * Parses a do-while statement from the token list.
712
+ * @returns The parsed do-while statement node.
713
+ */
714
+ parseDoWhileStatement() {
715
+ this.consume({ type: "Identifier", value: "do" }, "Expected 'do' keyword");
716
+ const bodyExpression = this.parseBlockExpression();
717
+ const body = {
718
+ type: "Block",
719
+ body: bodyExpression,
720
+ loc: {
721
+ start: bodyExpression[0]?.loc.start || this.previous()?.start || { line: -1, column: -1 },
722
+ end: bodyExpression[bodyExpression.length - 1]?.loc.end || this.previous()?.end || { line: -1, column: -1 }
723
+ }
724
+ };
725
+ this.consume({ type: "Identifier", value: "while" }, "Expected 'while' after do block");
726
+ this.consume({ type: "(" }, "Expected '(' after 'while'");
727
+ const condition = this.parseExpression();
728
+ this.consume({ type: ")" }, "Expected ')' after do-while condition");
729
+ this.tryConsume("type", [";"]);
730
+ return {
731
+ type: "DoWhileStatement",
732
+ condition,
733
+ body,
734
+ loc: {
735
+ start: body.loc.start,
736
+ end: this.previous()?.end || condition.loc.end
737
+ }
738
+ };
739
+ }
740
+ parseSwitchStatement() {
741
+ const start = this.peek().start;
742
+ this.consume({ type: "Identifier", value: "switch" }, "Expected 'switch' keyword");
743
+ this.consume({ type: "(" }, "Expected '(' after 'switch'");
744
+ const value = this.parseExpression();
745
+ this.consume({ type: ")" }, "Expected ')' after the 'switch'-value");
746
+ this.consume({ type: "{" }, "Expected '{' after switch expression");
747
+ const body = [];
748
+ while (!this.isAtEnd() && !this.checkToken("type", ["}"])) {
749
+ const stmt = this.parseStatement();
750
+ if (stmt?.type === "CaseDeclaration" || stmt?.type === "DefaultCaseDeclaration") {
751
+ body.push(stmt);
752
+ }
753
+ }
754
+ this.consume({ type: "}" }, "Expected '}' to close switch block");
755
+ return {
756
+ type: "SwitchDeclaration",
757
+ value,
758
+ body,
759
+ loc: { start, end: this.previous()?.end || value.loc.end }
760
+ };
761
+ }
762
+ parseCaseStatement() {
763
+ const start = this.peek().start;
764
+ this.consume({ type: "Identifier", value: "case" }, "Expected 'case' keyword");
765
+ this.consume({ type: "(" }, "Expected '(' after 'case'");
766
+ const values = [this.parseExpression()];
767
+ while (this.tryConsume("type", [","])) {
768
+ values.push(this.parseExpression());
769
+ }
770
+ this.consume({ type: ")" }, "Expected ')' after case values");
771
+ const stmts = this.parseBlockExpression();
772
+ const body = {
773
+ type: "Block",
774
+ body: stmts,
775
+ loc: { start: stmts[0]?.loc.start || start, end: stmts[stmts.length - 1]?.loc.end || start }
776
+ };
777
+ return { type: "CaseDeclaration", values, body, loc: { start, end: body.loc.end } };
778
+ }
779
+ parseDefaultCaseStatement() {
780
+ const start = this.peek().start;
781
+ this.consume({ type: "Identifier", value: "default" }, "Expected 'default' keyword");
782
+ const stmts = this.parseBlockExpression();
783
+ const body = {
784
+ type: "Block",
785
+ body: stmts,
786
+ loc: { start: stmts[0]?.loc.start || start, end: stmts[stmts.length - 1]?.loc.end || start }
787
+ };
788
+ return { type: "DefaultCaseDeclaration", body, loc: { start, end: body.loc.end } };
789
+ }
790
+ /**
791
+ * Parses an if/elif/else statement from the token list.
792
+ * @returns The parsed if statement node.
793
+ */
794
+ parseIfStatement() {
795
+ this.consume({ type: "Identifier", value: "if" }, "Expected 'if' keyword");
796
+ this.consume({ type: "(" }, "Expected '(' after 'if'");
797
+ const condition = this.parseExpression();
798
+ this.consume({ type: ")" }, "Expected ')' after the 'if'-condition");
799
+ const thenBlockExpression = this.parseBlockExpression();
800
+ const thenBlock = {
801
+ type: "Block",
802
+ body: thenBlockExpression,
803
+ loc: {
804
+ start: thenBlockExpression[0]?.loc.start || condition.loc.end,
805
+ end: thenBlockExpression[thenBlockExpression.length - 1]?.loc.end || condition.loc.end
806
+ }
807
+ };
808
+ const elifs = [];
809
+ while (this.tryConsume("value", ["elif"])) {
810
+ this.consume({ type: "(" }, "Expected '(' after 'elif'");
811
+ const elifCondition = this.parseExpression();
812
+ this.consume({ type: ")" }, "Expected ')' after elif condition");
813
+ const elifBlock = this.parseBlockExpression();
814
+ elifs.push({
815
+ type: "ElifClause",
816
+ condition: elifCondition,
817
+ block: {
818
+ type: "Block",
819
+ body: elifBlock,
820
+ loc: {
821
+ start: elifBlock[0]?.loc.start || elifCondition.loc.start,
822
+ end: elifBlock[elifBlock.length - 1]?.loc.end || elifCondition.loc.end
823
+ }
824
+ },
825
+ loc: {
826
+ start: elifCondition.loc.start,
827
+ end: elifBlock[elifBlock.length - 1]?.loc.end || elifCondition.loc.end
828
+ }
829
+ });
830
+ }
831
+ let elseBlock = null;
832
+ if (this.tryConsume("value", ["else"])) {
833
+ const elseBlockExpression = this.parseBlockExpression();
834
+ elseBlock = {
835
+ type: "Block",
836
+ body: elseBlockExpression,
837
+ loc: {
838
+ start: elseBlockExpression[0]?.loc.start || this.peek()?.start || condition.loc.end,
839
+ end: elseBlockExpression[elseBlockExpression.length - 1]?.loc.end || this.peek()?.end || condition.loc.end
840
+ }
841
+ };
842
+ }
843
+ return {
844
+ type: "IfStatement",
845
+ condition,
846
+ thenBlock,
847
+ elifs,
848
+ elseBlock,
849
+ loc: {
850
+ start: condition.loc.start,
851
+ end: elseBlock ? elseBlock.loc.end : thenBlock.loc.end
852
+ }
853
+ };
854
+ }
855
+ /**
856
+ * Parses an expression from the token list.
857
+ * @returns The parsed expression node.
858
+ */
859
+ parseExpression(minBP = 0) {
860
+ this.logger.log(new KatnipLog(KatnipLogType.Debug, `PREfix operator: ${this.peek()?.token.type}`));
861
+ let left = this.parsePrefix();
862
+ while (true) {
863
+ const token = this.peek();
864
+ if (!token)
865
+ break;
866
+ const bp = getBindingPower(token);
867
+ if (!bp || bp.lbp <= minBP)
868
+ break;
869
+ //this.advance();
870
+ this.logger.log(new KatnipLog(KatnipLogType.Debug, `Infix operator: ${this.peek()?.token.type}`));
871
+ left = this.parseInfix(left, bp);
872
+ }
873
+ this.logger.log(new KatnipLog(KatnipLogType.Debug, `QUIT on: ${this.peek()?.token.type}`));
874
+ return left;
875
+ }
876
+ /**
877
+ * Checks whether a token can begin an expression.
878
+ * @param token - The token to evaluate.
879
+ * @returns True when the token can start an expression, otherwise false.
880
+ */
881
+ canStartExpression(token) {
882
+ if (!token)
883
+ return false;
884
+ const type = token.token.type;
885
+ return (type === "Identifier" ||
886
+ type === "Number" ||
887
+ type === "String" ||
888
+ type === "InterpolatedString" ||
889
+ type === "(" ||
890
+ type === "[" ||
891
+ type === "{" ||
892
+ type === "!" ||
893
+ type === "-");
894
+ }
895
+ /**
896
+ * Parses an interpolated string expression.
897
+ * @returns The parsed interpolated string expression node.
898
+ */
899
+ parseInterpolatedString() {
900
+ const startToken = this.consume({ type: "InterpolatedString" }, "Expected interpolated string literal");
901
+ const parts = [];
902
+ parts.push(startToken.token.value);
903
+ let end = startToken.end;
904
+ while (this.canStartExpression(this.peek())) {
905
+ const expr = this.parseExpression();
906
+ parts.push(expr);
907
+ end = expr.loc.end;
908
+ if (!this.checkToken("type", ["InterpolatedStringEnd"])) {
909
+ this.reporter.add(new KatnipError("Parser", "Expected end of interpolated expression '}'", this.peek()?.start ?? expr.loc.end));
910
+ break;
911
+ }
912
+ const endToken = this.consume({ type: "InterpolatedStringEnd" }, "Expected end of interpolated expression '}'");
913
+ end = endToken.end;
914
+ if (!this.checkToken("type", ["InterpolatedString"])) {
915
+ break;
916
+ }
917
+ const nextChunk = this.consume({ type: "InterpolatedString" }, "Expected interpolated string chunk");
918
+ parts.push(nextChunk.token.value);
919
+ end = nextChunk.end;
920
+ }
921
+ return {
922
+ type: "InterpolatedString",
923
+ parts,
924
+ loc: { start: startToken.start, end }
925
+ };
926
+ }
927
+ /**
928
+ * Parses the prefix part of an expression.
929
+ * @returns The parsed expression node.
930
+ */
931
+ parsePrefix() {
932
+ const token = this.peek();
933
+ if (!token)
934
+ return { type: "ErrorToken", value: "", loc: { start: { line: -1, column: -1 }, end: { line: -1, column: -1 } } };
935
+ const value = token.token.type;
936
+ // Unary operators
937
+ if (value === "!" || value === "-") {
938
+ this.advance();
939
+ const opKey = value === "-" ? "UnaryMinus" : value;
940
+ const { rbp } = bindingPowerTable[opKey];
941
+ const right = this.parseExpression(rbp);
942
+ return {
943
+ type: "UnaryExpression",
944
+ operator: value,
945
+ argument: right,
946
+ loc: { start: token.start, end: right.loc?.end || token.end },
947
+ };
948
+ }
949
+ // Parenthesized expression
950
+ if (this.tryConsume("type", ["("])) {
951
+ if (this.checkToken("type", [")"])) {
952
+ this.advance();
953
+ return { type: "EmptyExpression", loc: { start: token.start, end: this.previous().end } };
954
+ }
955
+ const expr = this.parseExpression();
956
+ if (this.checkToken("type", [","])) {
957
+ let tupleElements = [expr];
958
+ while (this.tryConsume("type", [","])) {
959
+ tupleElements.push(this.parseExpression());
960
+ }
961
+ this.consume({ type: ")" }, "Expected ')' after parenthesized expression");
962
+ return {
963
+ type: "TupleExpression",
964
+ elements: tupleElements,
965
+ loc: { start: token.start, end: this.previous().end }
966
+ };
967
+ }
968
+ this.consume({ type: ")" }, "Expected ')' after parenthesized expression");
969
+ return expr;
970
+ }
971
+ // Bracket expression
972
+ if (this.tryConsume("type", ["["])) {
973
+ const listContents = [];
974
+ do {
975
+ listContents.push(this.parseExpression());
976
+ this.tryConsume("type", [","]);
977
+ } while (!this.checkToken("type", ["]"]));
978
+ this.consume({ type: "]" }, "Expected ']' after list expression");
979
+ return {
980
+ type: "ListExpression",
981
+ elements: listContents,
982
+ loc: { start: token.start, end: this.previous().end }
983
+ };
984
+ }
985
+ // Dictionary expression
986
+ if (this.tryConsume("type", ["{"])) {
987
+ const curlyArgs = [];
988
+ if (!this.checkToken("type", ["}"])) {
989
+ do {
990
+ const keyToken = this.consume({ type: ["String", "Number", "Identifier"] }, "Expected string or number literal or identifier as dictionary key").token;
991
+ this.consume({ type: ":" }, "Expected ':' after dictionary key");
992
+ const value = this.parseExpression();
993
+ let type = "Null";
994
+ if (keyToken.type === "String" ||
995
+ keyToken.type === "Identifier") {
996
+ type = "String";
997
+ }
998
+ else if (keyToken.type === "Number") {
999
+ type = "Number";
1000
+ }
1001
+ curlyArgs.push({ key: { type: "Literal", value: keyToken.value, valueType: type, loc: { start: token.start, end: token.end } }, value });
1002
+ } while (this.tryConsume("type", [","]));
1003
+ }
1004
+ this.consume({ type: "}" }, "Expected '}' after parameters");
1005
+ return {
1006
+ type: "DictExpression",
1007
+ entries: curlyArgs,
1008
+ loc: {
1009
+ start: token.start,
1010
+ end: this.previous().end,
1011
+ },
1012
+ };
1013
+ }
1014
+ // Identifiers
1015
+ if (this.checkToken("type", ["Identifier"])) {
1016
+ const id = this.consume({ type: "Identifier" }, "Expected identifier");
1017
+ if (id.token.value === "true" || id.token.value === "false") {
1018
+ return {
1019
+ type: "Literal",
1020
+ value: id.token.value === "true",
1021
+ valueType: "Boolean",
1022
+ loc: { start: id.start, end: id.end },
1023
+ };
1024
+ }
1025
+ return {
1026
+ type: "Identifier",
1027
+ name: id.token.value,
1028
+ loc: { start: id.start, end: id.end },
1029
+ };
1030
+ }
1031
+ // String literal
1032
+ if (this.checkToken("type", ["String"])) {
1033
+ const lit = this.consume({ type: "String" }, "Expected string literal");
1034
+ return {
1035
+ type: "Literal",
1036
+ value: lit.token.value,
1037
+ valueType: "String",
1038
+ loc: { start: lit.start, end: lit.end },
1039
+ };
1040
+ }
1041
+ if (this.checkToken("type", ["InterpolatedString"])) {
1042
+ return this.parseInterpolatedString();
1043
+ }
1044
+ // Number literal
1045
+ if (this.checkToken("type", ["Number"])) {
1046
+ const lit = this.consume({ type: "Number" }, "Expected number literal");
1047
+ return {
1048
+ type: "Literal",
1049
+ value: Number(lit.token.value),
1050
+ valueType: "Number",
1051
+ loc: { start: lit.start, end: lit.end },
1052
+ };
1053
+ }
1054
+ // EOF
1055
+ if (this.checkToken("type", ["<EOF>"])) {
1056
+ return { type: "ErrorToken", value: "", loc: { start: token.start, end: token.end } };
1057
+ }
1058
+ // Fallback error
1059
+ const unexpectedToken = token.token.type;
1060
+ const unexpectedValue = isValuedTokenType(unexpectedToken) ? token.token.value : null;
1061
+ this.reporter.add(new KatnipError("Parser", `Unexpected token in expression: Type '${unexpectedToken}'${unexpectedValue ? `, Value '${unexpectedValue}'` : ''}`, token.start));
1062
+ this.advance();
1063
+ return { type: "ErrorToken", value: "", loc: { start: token.start, end: token.end } };
1064
+ }
1065
+ /**
1066
+ * Parses a single call argument: either a positional expression or a named argument of the form `name = value`.
1067
+ * @returns The parsed argument node.
1068
+ */
1069
+ parseArgument() {
1070
+ if (this.checkToken("type", ["Identifier"]) &&
1071
+ this.peek(1)?.token.type === "=") {
1072
+ const nameToken = this.consume({ type: "Identifier" }, "Expected argument name");
1073
+ this.consume({ type: "=" }, "Expected '=' after argument name");
1074
+ const value = this.parseExpression();
1075
+ return {
1076
+ type: "NamedArgument",
1077
+ name: nameToken.token.value,
1078
+ value,
1079
+ loc: { start: nameToken.start, end: value.loc.end }
1080
+ };
1081
+ }
1082
+ return this.parseExpression();
1083
+ }
1084
+ /**
1085
+ * Parses the infix part of an expression.
1086
+ * @returns The parsed expression node.
1087
+ */
1088
+ parseInfix(left, bp) {
1089
+ const token = this.peek();
1090
+ if (!token) {
1091
+ this.reporter.add(new KatnipError("Parser", "Unexpected end of input in infix expression", left.loc?.end || { line: -1, column: -1 }));
1092
+ return { type: "ErrorToken", value: "", loc: left.loc || { start: { line: -1, column: -1 }, end: { line: -1, column: -1 } } };
1093
+ }
1094
+ if (this.checkToken("type", ["("])) {
1095
+ this.advance();
1096
+ const parenArgs = [];
1097
+ if (!this.checkToken("type", [")"])) {
1098
+ do {
1099
+ parenArgs.push(this.parseArgument());
1100
+ } while (this.tryConsume("type", [","]));
1101
+ }
1102
+ this.consume({ type: ")" }, "Expected ')' after arguments");
1103
+ return {
1104
+ type: "CallExpression",
1105
+ object: left,
1106
+ arguments: parenArgs,
1107
+ loc: {
1108
+ start: left.loc.start,
1109
+ end: this.previous()?.end || token.end,
1110
+ },
1111
+ };
1112
+ }
1113
+ if (this.checkToken("type", ["["])) {
1114
+ this.advance();
1115
+ // Optional start: absent when the bracket opens straight onto ':' or ']'.
1116
+ let sliceStart = null;
1117
+ if (!this.checkToken("type", [":", "]"])) {
1118
+ sliceStart = this.parseExpression();
1119
+ }
1120
+ // No ':' => plain index access (arr[expr]), which needs an index.
1121
+ if (!this.checkToken("type", [":"])) {
1122
+ if (!sliceStart) {
1123
+ this.reporter.add(new KatnipError("Parser", "Expected an index or slice inside '[]'", this.peek()?.start || { line: -1, column: -1 }));
1124
+ }
1125
+ this.consume({ type: "]" }, "Expected ']' after index");
1126
+ return {
1127
+ type: "IndexerAccess",
1128
+ object: left,
1129
+ index: sliceStart ?? { type: "ErrorToken", value: "", loc: { start: left.loc.start, end: left.loc.end } },
1130
+ loc: {
1131
+ start: left.loc.start,
1132
+ end: this.previous()?.end || token.end,
1133
+ },
1134
+ };
1135
+ }
1136
+ // Slice: consume the first ':', then an optional end and optional ':' + step.
1137
+ this.advance(); // consume ':'
1138
+ let sliceEnd = null;
1139
+ if (!this.checkToken("type", [":", "]"])) {
1140
+ sliceEnd = this.parseExpression();
1141
+ }
1142
+ let sliceStep = null;
1143
+ if (this.tryConsume("type", [":"])) {
1144
+ if (!this.checkToken("type", ["]"])) {
1145
+ sliceStep = this.parseExpression();
1146
+ }
1147
+ }
1148
+ this.consume({ type: "]" }, "Expected ']' after slice");
1149
+ return {
1150
+ type: "SliceAccess",
1151
+ object: left,
1152
+ start: sliceStart,
1153
+ end: sliceEnd,
1154
+ step: sliceStep,
1155
+ loc: {
1156
+ start: left.loc.start,
1157
+ end: this.previous()?.end || token.end,
1158
+ },
1159
+ };
1160
+ }
1161
+ if (this.checkToken("type", ["."])) {
1162
+ // Member access
1163
+ if (this.checkToken("type", ["."])) {
1164
+ this.advance();
1165
+ const id = this.consume({ type: "Identifier" }, "Expected property name after '.'");
1166
+ return {
1167
+ type: "MemberExpression",
1168
+ object: left,
1169
+ property: {
1170
+ type: "Identifier",
1171
+ name: id.token.value,
1172
+ loc: { start: id.start, end: id.end },
1173
+ },
1174
+ loc: { start: left.loc.start, end: id.end },
1175
+ };
1176
+ }
1177
+ }
1178
+ // Binary operator (fallback). Use binding power to parse right-hand side.
1179
+ const tokenValue = isValuedTokenType(token.token.type)
1180
+ ? token.token.value
1181
+ : token.token.type;
1182
+ this.advance();
1183
+ const right = this.parseExpression(bp.rbp);
1184
+ return {
1185
+ type: "BinaryExpression",
1186
+ operator: tokenValue,
1187
+ left,
1188
+ right,
1189
+ loc: { start: left.loc.start, end: right.loc.end },
1190
+ };
1191
+ }
1192
+ /**
1193
+ * Parses a block expression following a method call.
1194
+ * @param allowedStatements - A list of allowed statement types inside this body.
1195
+ * @returns The parsed block expression node.
1196
+ */
1197
+ parseBlockExpression(allowedStatements) {
1198
+ if (!this.checkToken("type", ["{"])) {
1199
+ this.reporter.add(new KatnipError("Parser", "Expected '{' to open block", this.peek()?.start ?? this.previous()?.end ?? { line: -1, column: -1 }));
1200
+ while (!this.isAtEnd() && !this.checkToken("type", ["{", "}"]))
1201
+ this.advance();
1202
+ }
1203
+ this.tryConsume("type", ["{"]);
1204
+ const body = [];
1205
+ while (!this.isAtEnd() && !this.checkToken("type", ["}"])) {
1206
+ const stmt = this.parseStatement();
1207
+ if (stmt && (!allowedStatements || allowedStatements.includes(stmt))) {
1208
+ body.push(stmt);
1209
+ }
1210
+ }
1211
+ this.consume({ type: "}" }, "Expected '}' to close block");
1212
+ return body;
1213
+ }
1214
+ }
1215
+ //# sourceMappingURL=Parser.js.map