stone-lang 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 (68) hide show
  1. package/README.md +52 -0
  2. package/StoneEngine.js +879 -0
  3. package/StoneEngineService.js +1727 -0
  4. package/adapters/FileSystemAdapter.js +230 -0
  5. package/adapters/OutputAdapter.js +208 -0
  6. package/adapters/index.js +6 -0
  7. package/cli/CLIOutputAdapter.js +196 -0
  8. package/cli/DaemonClient.js +349 -0
  9. package/cli/JSONOutputAdapter.js +135 -0
  10. package/cli/ReplSession.js +567 -0
  11. package/cli/ViewerServer.js +590 -0
  12. package/cli/commands/check.js +84 -0
  13. package/cli/commands/daemon.js +189 -0
  14. package/cli/commands/kill.js +66 -0
  15. package/cli/commands/package.js +713 -0
  16. package/cli/commands/ps.js +65 -0
  17. package/cli/commands/run.js +537 -0
  18. package/cli/entry.js +169 -0
  19. package/cli/index.js +14 -0
  20. package/cli/stonec.js +358 -0
  21. package/cli/test-compiler.js +181 -0
  22. package/cli/viewer/index.html +495 -0
  23. package/daemon/IPCServer.js +455 -0
  24. package/daemon/ProcessManager.js +327 -0
  25. package/daemon/ProcessRunner.js +307 -0
  26. package/daemon/daemon.js +398 -0
  27. package/daemon/index.js +16 -0
  28. package/frontend/analysis/index.js +5 -0
  29. package/frontend/analysis/livenessAnalyzer.js +568 -0
  30. package/frontend/analysis/treeShaker.js +265 -0
  31. package/frontend/index.js +20 -0
  32. package/frontend/parsing/astBuilder.js +2196 -0
  33. package/frontend/parsing/index.js +7 -0
  34. package/frontend/parsing/sonParser.js +592 -0
  35. package/frontend/parsing/stoneAstTypes.js +703 -0
  36. package/frontend/parsing/terminal-registry.js +435 -0
  37. package/frontend/parsing/tokenizer.js +692 -0
  38. package/frontend/type-checker/OverloadedFunctionType.js +43 -0
  39. package/frontend/type-checker/TypeEnvironment.js +165 -0
  40. package/frontend/type-checker/bidirectionalInference.js +149 -0
  41. package/frontend/type-checker/index.js +10 -0
  42. package/frontend/type-checker/moduleAnalysis.js +248 -0
  43. package/frontend/type-checker/operatorMappings.js +35 -0
  44. package/frontend/type-checker/overloadResolution.js +605 -0
  45. package/frontend/type-checker/typeChecker.js +452 -0
  46. package/frontend/type-checker/typeCompatibility.js +389 -0
  47. package/frontend/type-checker/visitors/controlFlow.js +483 -0
  48. package/frontend/type-checker/visitors/functions.js +604 -0
  49. package/frontend/type-checker/visitors/index.js +38 -0
  50. package/frontend/type-checker/visitors/literals.js +341 -0
  51. package/frontend/type-checker/visitors/modules.js +159 -0
  52. package/frontend/type-checker/visitors/operators.js +109 -0
  53. package/frontend/type-checker/visitors/statements.js +768 -0
  54. package/frontend/types/index.js +5 -0
  55. package/frontend/types/operatorMap.js +134 -0
  56. package/frontend/types/types.js +2046 -0
  57. package/frontend/utils/errorCollector.js +244 -0
  58. package/frontend/utils/index.js +5 -0
  59. package/frontend/utils/moduleResolver.js +479 -0
  60. package/package.json +50 -0
  61. package/packages/browserCache.js +359 -0
  62. package/packages/fetcher.js +236 -0
  63. package/packages/index.js +130 -0
  64. package/packages/lockfile.js +271 -0
  65. package/packages/manifest.js +291 -0
  66. package/packages/packageResolver.js +356 -0
  67. package/packages/resolver.js +310 -0
  68. package/packages/semver.js +635 -0
@@ -0,0 +1,2196 @@
1
+ /**
2
+ * Stone Language Parser
3
+ *
4
+ * Converts token stream into Abstract Syntax Tree (AST)
5
+ */
6
+
7
+ import { TokenType, Lexer } from "./tokenizer.js";
8
+ import {
9
+ Program,
10
+ Assignment,
11
+ DestructuringAssignment,
12
+ IndexedAssignment,
13
+ TypeAlias,
14
+ Literal,
15
+ Identifier,
16
+ BinaryOp,
17
+ UnaryOp,
18
+ FunctionCall,
19
+ BranchExpression,
20
+ BranchPath,
21
+ LoopExpression,
22
+ LoopVariable,
23
+ ForDriver,
24
+ WhileDriver,
25
+ VariableUpdate,
26
+ ReturnStatement,
27
+ BreakStatement,
28
+ ContinueStatement,
29
+ Range,
30
+ ArrayLiteral,
31
+ ObjectLiteral,
32
+ BlockExpression,
33
+ MemberAccess,
34
+ StringInterpolation,
35
+ FunctionDefinition,
36
+ Parameter,
37
+ TypeAnnotation,
38
+ createLocation,
39
+ // Unified braces
40
+ BindingStructure,
41
+ Binding,
42
+ // Module types
43
+ ImportStatement,
44
+ NamespaceImportStatement,
45
+ ImportSpecifier,
46
+ ExportStatement,
47
+ SelectiveReExportStatement,
48
+ NamespaceReExportStatement,
49
+ } from "./stoneAstTypes.js";
50
+
51
+ export class Parser {
52
+ constructor(tokens, filename = "<stdin>") {
53
+ this.tokens = tokens;
54
+ this.filename = filename;
55
+ this.pos = 0;
56
+ }
57
+
58
+ /**
59
+ * Create a parse error with stage information
60
+ * @param {string} message - Error message
61
+ * @param {Object} token - Token for location info (optional)
62
+ * @returns {Error} Error with stage and location info
63
+ */
64
+ _parseError(message, token = null) {
65
+ const loc = token ? { line: token.line, column: token.column } : null;
66
+ const error = new Error(message);
67
+ error.stage = 'parse';
68
+ error.type = 'syntax';
69
+ error.location = loc;
70
+ return error;
71
+ }
72
+
73
+ /**
74
+ * Parse the entire program
75
+ */
76
+ parse() {
77
+ const statements = [];
78
+ const imports = [];
79
+ const exports = [];
80
+
81
+ while (!this.isAtEnd()) {
82
+ const stmt = this.parseStatement();
83
+ if (stmt) {
84
+ statements.push(stmt);
85
+ // Track imports and exports separately for module resolution
86
+ if (stmt.type === "ImportStatement" || stmt.type === "NamespaceImportStatement") {
87
+ imports.push(stmt);
88
+ }
89
+ if (stmt.type === "ExportStatement" || stmt.type === "SelectiveReExportStatement" || stmt.type === "NamespaceReExportStatement") {
90
+ exports.push(stmt);
91
+ }
92
+ }
93
+ }
94
+
95
+ const program = new Program(statements, this.createLocation());
96
+ program.imports = imports;
97
+ program.exports = exports;
98
+ return program;
99
+ }
100
+
101
+ /**
102
+ * Parse a single statement
103
+ */
104
+ parseStatement() {
105
+ while (this.match(TokenType.NEWLINE)) {
106
+ // Skip
107
+ }
108
+
109
+ if (this.isAtEnd()) return null;
110
+
111
+ // Import statement: import ... from ... OR import path/to/module
112
+ if (this.match(TokenType.IMPORT)) {
113
+ return this.parseImportStatement();
114
+ }
115
+
116
+ // Export statement: export fn ... OR export name = ...
117
+ if (this.match(TokenType.EXPORT)) {
118
+ return this.parseExportStatement();
119
+ }
120
+
121
+ // Type alias: type Name = TypeAnnotation
122
+ if (this.match(TokenType.TYPE)) {
123
+ return this.parseTypeAlias();
124
+ }
125
+
126
+ // Extension function: ext name(self: type, ...) = expr
127
+ if (this.match(TokenType.EXTENSION)) {
128
+ return this.parseFunctionDefinition(true);
129
+ }
130
+
131
+ // Function definition: fn name(params) = expr OR fn name(params) { ... }
132
+ if (this.match(TokenType.FN)) {
133
+ return this.parseFunctionDefinition(false);
134
+ }
135
+
136
+ // Return statement
137
+ if (this.match(TokenType.RETURN)) {
138
+ return this.parseReturn();
139
+ }
140
+
141
+ // Break statement
142
+ if (this.match(TokenType.BREAK)) {
143
+ return new BreakStatement(this.createLocation());
144
+ }
145
+
146
+ // Continue statement
147
+ if (this.match(TokenType.CONTINUE)) {
148
+ return new ContinueStatement(this.createLocation());
149
+ }
150
+
151
+ // Bare for/while loop without state block: for (i in range(n)) { body }
152
+ // This is syntactic sugar for { } for (i in range(n)) { body } with empty state
153
+ if (this.check(TokenType.FOR) || this.check(TokenType.WHILE)) {
154
+ const startLoc = this.createLocation();
155
+ // Create empty state block (no loop variables)
156
+ const emptyState = new BindingStructure([], null, startLoc);
157
+ return this.parseLoopWithState(emptyState, startLoc);
158
+ }
159
+
160
+ // Loop variable update with compound operators (+=, -=, *=, /=)
161
+ if (this.check(TokenType.IDENTIFIER)) {
162
+ const nextToken = this.tokens[this.pos + 1];
163
+ if (
164
+ nextToken &&
165
+ (nextToken.type === TokenType.PLUS_ASSIGN ||
166
+ nextToken.type === TokenType.MINUS_ASSIGN ||
167
+ nextToken.type === TokenType.STAR_ASSIGN ||
168
+ nextToken.type === TokenType.SLASH_ASSIGN)
169
+ ) {
170
+ const startLoc = this.createLocation();
171
+ const variableName = this.advance().value;
172
+ const operator = this.advance().value;
173
+ const value = this.parseExpression();
174
+ return new VariableUpdate(variableName, operator, value, startLoc);
175
+ }
176
+
177
+ // Increment/decrement operators (++, --)
178
+ if (
179
+ nextToken &&
180
+ (nextToken.type === TokenType.INCREMENT ||
181
+ nextToken.type === TokenType.DECREMENT)
182
+ ) {
183
+ const startLoc = this.createLocation();
184
+ const variableName = this.advance().value;
185
+ const operator = this.advance().value;
186
+ return new VariableUpdate(variableName, operator, null, startLoc);
187
+ }
188
+ }
189
+
190
+ // Try to parse as assignment or expression
191
+ return this.parseAssignmentOrExpression();
192
+ }
193
+
194
+ /**
195
+ * Parse import statement
196
+ * Supports forms:
197
+ * 1. Selective import: import name1, name2 as alias from path/to/module
198
+ * 2. Namespace import: import path/to/module [as Alias]
199
+ * 3. Quoted namespace import: import "path with spaces" [as Alias]
200
+ * 4. Quoted selective import: import name from "path with spaces"
201
+ * 5. Directory + inferred filename: import stats from "dir" -> resolves to "dir/stats.stn"
202
+ */
203
+ parseImportStatement() {
204
+ const startLoc = this.createLocation();
205
+
206
+ const firstToken = this.peek();
207
+
208
+ // Check for quoted path at the start (namespace import with quoted path)
209
+ if (firstToken.type === TokenType.STRING) {
210
+ const { path, isQuoted } = this.parseModulePath();
211
+ let alias = null;
212
+
213
+ if (this.matchContextualKeyword("as")) {
214
+ alias = this.consume(TokenType.IDENTIFIER, "Expected alias name after 'as'").value;
215
+ }
216
+
217
+ return new NamespaceImportStatement(path, alias, startLoc, isQuoted);
218
+ }
219
+
220
+ if (firstToken.type === TokenType.IDENTIFIER) {
221
+ // Could be selective or namespace import - need to look ahead
222
+ // Save position for backtracking
223
+ const savedPos = this.pos;
224
+
225
+ // Try parsing as selective import specifiers
226
+ const specifiers = [];
227
+
228
+ // Parse first identifier
229
+ let nameToken = this.advance();
230
+ let name = nameToken.value;
231
+ let specLoc = { line: nameToken.line, column: nameToken.column };
232
+ let alias = null;
233
+
234
+ if (this.matchContextualKeyword("as")) {
235
+ alias = this.consume(TokenType.IDENTIFIER, "Expected alias name after 'as'").value;
236
+ }
237
+
238
+ specifiers.push(new ImportSpecifier(name, alias, specLoc));
239
+
240
+ // Check for comma (more specifiers) or FROM (selective import)
241
+ while (this.match(TokenType.COMMA)) {
242
+ if (!this.check(TokenType.IDENTIFIER)) {
243
+ break;
244
+ }
245
+ nameToken = this.advance();
246
+ name = nameToken.value;
247
+ specLoc = { line: nameToken.line, column: nameToken.column };
248
+ alias = null;
249
+ if (this.matchContextualKeyword("as")) {
250
+ alias = this.consume(TokenType.IDENTIFIER, "Expected alias name after 'as'").value;
251
+ }
252
+ specifiers.push(new ImportSpecifier(name, alias, specLoc));
253
+ }
254
+
255
+ // If we see FROM, this is a selective import
256
+ if (this.match(TokenType.FROM)) {
257
+ const { path, isQuoted } = this.parseModulePath();
258
+
259
+ // For quoted paths with exactly ONE specifier, set inferredFilename
260
+ // This enables: import stats from "core logic/math" -> "core logic/math/stats.stn"
261
+ let inferredFilename = null;
262
+ if (isQuoted && specifiers.length === 1) {
263
+ inferredFilename = specifiers[0].importedName;
264
+ }
265
+
266
+ return new ImportStatement(specifiers, path, startLoc, isQuoted, inferredFilename);
267
+ }
268
+
269
+ // Otherwise, backtrack and parse as namespace import
270
+ this.pos = savedPos;
271
+ }
272
+
273
+ // Parse as namespace import: import path/to/module [as Alias]
274
+ const { path, isQuoted } = this.parseModulePath();
275
+ let alias = null;
276
+
277
+ if (this.matchContextualKeyword("as")) {
278
+ alias = this.consume(TokenType.IDENTIFIER, "Expected alias name after 'as'").value;
279
+ }
280
+
281
+ return new NamespaceImportStatement(path, alias, startLoc, isQuoted);
282
+ }
283
+
284
+ /**
285
+ * Parse a module path
286
+ * Handles:
287
+ * - Identifier paths: path/to/module, ./relative/path, ../parent/path
288
+ * - Quoted string paths: "path with spaces/to/module"
289
+ * Module paths use / as separator
290
+ * @returns {{ path: string, isQuoted: boolean }}
291
+ */
292
+ parseModulePath() {
293
+ // Check for quoted string path (ModuleLocator)
294
+ if (this.check(TokenType.STRING)) {
295
+ const stringToken = this.advance();
296
+ const path = stringToken.value;
297
+
298
+ // Validate: no empty path
299
+ if (!path || path.trim() === '') {
300
+ throw this._parseError(`Module path cannot be empty`, stringToken);
301
+ }
302
+
303
+ return { path, isQuoted: true };
304
+ }
305
+
306
+ // Existing identifier-based path parsing (ModulePath)
307
+ let path = "";
308
+
309
+ // Handle relative paths: ./ or ../
310
+ // Note: ".." is tokenized as RANGE, not two separate DOTs
311
+ if (this.check(TokenType.RANGE)) {
312
+ // Parent directory: ../
313
+ this.advance(); // consume the ".." (RANGE token)
314
+ path += "..";
315
+
316
+ // Expect slash after ..
317
+ if (this.match(TokenType.SLASH)) {
318
+ path += "/";
319
+ }
320
+ } else if (this.check(TokenType.DOT)) {
321
+ this.advance(); // consume single dot
322
+ path += ".";
323
+
324
+ // Expect slash after .
325
+ if (this.match(TokenType.SLASH)) {
326
+ path += "/";
327
+ }
328
+ }
329
+
330
+ // Helper to parse a path segment that may contain hyphens
331
+ // e.g., "test-stn-scripts" is parsed as one segment
332
+ const parsePathSegment = () => {
333
+ if (!this.check(TokenType.IDENTIFIER)) {
334
+ return null;
335
+ }
336
+ let segment = this.advance().value;
337
+
338
+ // Allow hyphens within path segments: identifier-identifier-...
339
+ // Check that the hyphen immediately follows (same line, no space implied by token positions)
340
+ while (this.check(TokenType.MINUS)) {
341
+ const minusToken = this.peek();
342
+ const prevToken = this.tokens[this.pos - 1];
343
+ // Only treat as hyphenated if on same line and appears contiguous
344
+ if (minusToken.line === prevToken.line) {
345
+ this.advance(); // consume the minus
346
+ if (this.check(TokenType.IDENTIFIER)) {
347
+ const nextToken = this.peek();
348
+ if (nextToken.line === minusToken.line) {
349
+ segment += "-" + this.advance().value;
350
+ } else {
351
+ // Minus followed by identifier on different line - put minus back conceptually
352
+ // Actually we can't put it back, so this is an error case
353
+ throw this._parseError(`Unexpected line break in hyphenated module path`, nextToken);
354
+ }
355
+ } else {
356
+ throw this._parseError(`Expected identifier after '-' in module path`, this.peek());
357
+ }
358
+ } else {
359
+ break;
360
+ }
361
+ }
362
+ return segment;
363
+ };
364
+
365
+ // Parse path segments separated by /
366
+ const firstSegment = parsePathSegment();
367
+ if (firstSegment) {
368
+ path += firstSegment;
369
+
370
+ while (this.match(TokenType.SLASH)) {
371
+ path += "/";
372
+ const segment = parsePathSegment();
373
+ if (segment) {
374
+ path += segment;
375
+ } else {
376
+ throw this._parseError(`Expected identifier after '/' in module path`, this.peek());
377
+ }
378
+ }
379
+ }
380
+
381
+ if (!path) {
382
+ throw this._parseError(`Expected module path`, this.peek());
383
+ }
384
+
385
+ return { path, isQuoted: false };
386
+ }
387
+
388
+ /**
389
+ * Parse export statement
390
+ * export fn name(...) = ... - function export
391
+ * export name = value - local binding export
392
+ * export ./path [as alias] - namespace re-export
393
+ * export name1, name2 from ./path - selective re-export
394
+ */
395
+ parseExportStatement() {
396
+ const startLoc = this.createLocation();
397
+
398
+ // Extension function export: export ext name(self: type, ...) = ...
399
+ if (this.match(TokenType.EXTENSION)) {
400
+ const funcDef = this.parseFunctionDefinition(true);
401
+ return new ExportStatement(funcDef, startLoc);
402
+ }
403
+
404
+ // Function export: export fn name(...) { ... }
405
+ if (this.match(TokenType.FN)) {
406
+ const funcDef = this.parseFunctionDefinition(false);
407
+ return new ExportStatement(funcDef, startLoc);
408
+ }
409
+
410
+ // Namespace re-export starting with relative path: export ./path [as alias]
411
+ if (this.check(TokenType.DOT)) {
412
+ const { path: modulePath } = this.parseModulePath();
413
+ let alias = null;
414
+ if (this.matchContextualKeyword("as")) {
415
+ alias = this.consume(TokenType.IDENTIFIER, "Expected alias name after 'as'").value;
416
+ }
417
+ return new NamespaceReExportStatement(modulePath, alias, startLoc);
418
+ }
419
+
420
+ // Check for identifier - could be selective re-export or local binding export
421
+ if (this.check(TokenType.IDENTIFIER)) {
422
+ const savedPos = this.pos;
423
+
424
+ // Try parsing as selective re-export specifiers
425
+ const specifiers = [];
426
+ let name = this.advance().value;
427
+ let alias = null;
428
+
429
+ if (this.matchContextualKeyword("as")) {
430
+ alias = this.consume(TokenType.IDENTIFIER, "Expected alias name after 'as'").value;
431
+ }
432
+ specifiers.push({ name, alias });
433
+
434
+ // Check for more specifiers (comma-separated)
435
+ while (this.match(TokenType.COMMA)) {
436
+ if (!this.check(TokenType.IDENTIFIER)) break;
437
+ name = this.advance().value;
438
+ alias = null;
439
+ if (this.matchContextualKeyword("as")) {
440
+ alias = this.consume(TokenType.IDENTIFIER, "Expected alias name after 'as'").value;
441
+ }
442
+ specifiers.push({ name, alias });
443
+ }
444
+
445
+ // If followed by FROM, it's selective re-export
446
+ if (this.match(TokenType.FROM)) {
447
+ const { path: modulePath } = this.parseModulePath();
448
+
449
+ // Check for trailing `as alias` after module path (for single specifier only)
450
+ // Syntax: export name from ./path as alias
451
+ if (specifiers.length === 1 && specifiers[0].alias === null && this.matchContextualKeyword("as")) {
452
+ specifiers[0].alias = this.consume(TokenType.IDENTIFIER, "Expected alias name after 'as'").value;
453
+ }
454
+
455
+ return new SelectiveReExportStatement(specifiers, modulePath, startLoc);
456
+ }
457
+
458
+ // Backtrack - not a selective re-export
459
+ this.pos = savedPos;
460
+
461
+ // Try as local binding export: export name = value
462
+ const assignment = this.parseAssignmentOrExpression();
463
+ return new ExportStatement(assignment, startLoc);
464
+ }
465
+
466
+ throw this._parseError(`Expected function definition, assignment, or re-export after 'export'`, this.peek());
467
+ }
468
+
469
+ /**
470
+ * Parse type alias
471
+ * type Name = TypeAnnotation
472
+ */
473
+ parseTypeAlias() {
474
+ const startLoc = this.createLocation();
475
+
476
+ const name = this.consume(
477
+ TokenType.IDENTIFIER,
478
+ "Expected type alias name"
479
+ ).value;
480
+
481
+ this.consume(TokenType.ASSIGN, "Expected '=' after type alias name");
482
+
483
+ const typeAnnotation = this.parseTypeAnnotation();
484
+
485
+ return new TypeAlias(name, typeAnnotation, startLoc);
486
+ }
487
+
488
+ /**
489
+ * Parse function definition
490
+ * fn name(params) = expr
491
+ * fn name(params) { ... }
492
+ * extend fn name(self: type, ...) = expr (when isExtension=true)
493
+ */
494
+ parseFunctionDefinition(isExtension = false) {
495
+ const startLoc = this.createLocation();
496
+
497
+ const name = this.consume(
498
+ TokenType.IDENTIFIER,
499
+ "Expected function name"
500
+ ).value;
501
+
502
+ // Parse parameters
503
+ this.consume(TokenType.LPAREN, "Expected '(' after function name");
504
+ const parameters = [];
505
+
506
+ if (!this.check(TokenType.RPAREN)) {
507
+ do {
508
+ const paramName = this.consume(
509
+ TokenType.IDENTIFIER,
510
+ "Expected parameter name"
511
+ ).value;
512
+ let typeAnnotation = null;
513
+ let defaultValue = null;
514
+
515
+ // Optional type annotation: param: Type
516
+ if (this.match(TokenType.COLON)) {
517
+ typeAnnotation = this.parseTypeAnnotation();
518
+ }
519
+
520
+ // Optional default value: param = value
521
+ if (this.match(TokenType.ASSIGN)) {
522
+ defaultValue = this.parseExpression();
523
+ }
524
+
525
+ parameters.push(new Parameter(paramName, typeAnnotation, defaultValue));
526
+ } while (this.match(TokenType.COMMA));
527
+ }
528
+
529
+ this.consume(TokenType.RPAREN, "Expected ')' after parameters");
530
+
531
+ // Validate extension function requirements
532
+ let selfType = null;
533
+ if (isExtension) {
534
+ if (parameters.length === 0) {
535
+ throw this._parseError(
536
+ "Extension function must have at least one parameter named 'self'",
537
+ this.peek()
538
+ );
539
+ }
540
+ const firstParam = parameters[0];
541
+ if (firstParam.name !== "self") {
542
+ throw this._parseError(
543
+ `Extension function's first parameter must be named 'self', got '${firstParam.name}'`,
544
+ this.peek()
545
+ );
546
+ }
547
+ if (!firstParam.typeAnnotation) {
548
+ throw this._parseError(
549
+ "Extension function's 'self' parameter must have a type annotation",
550
+ this.peek()
551
+ );
552
+ }
553
+ selfType = firstParam.typeAnnotation;
554
+ }
555
+
556
+ // Optional return type annotation
557
+ let returnType = null;
558
+ if (this.match(TokenType.ARROW)) {
559
+ returnType = this.parseTypeAnnotation();
560
+ }
561
+
562
+ // Expression form: fn name(params) = expr
563
+ if (this.match(TokenType.ASSIGN)) {
564
+ const body = this.parseExpression();
565
+ const funcDef = new FunctionDefinition(
566
+ name,
567
+ parameters,
568
+ returnType,
569
+ body,
570
+ true,
571
+ startLoc
572
+ );
573
+ funcDef.isExtension = isExtension;
574
+ funcDef.selfType = selfType;
575
+ return funcDef;
576
+ }
577
+
578
+ // Block form: fn name(params) { ... }
579
+ this.consume(
580
+ TokenType.LBRACE,
581
+ "Expected '=' or '{' after function signature"
582
+ );
583
+ const body = this.parseBlock();
584
+ this.consume(TokenType.RBRACE, "Expected '}' to end function body");
585
+
586
+ const funcDef = new FunctionDefinition(
587
+ name,
588
+ parameters,
589
+ returnType,
590
+ body,
591
+ false,
592
+ startLoc
593
+ );
594
+ funcDef.isExtension = isExtension;
595
+ funcDef.selfType = selfType;
596
+ return funcDef;
597
+ }
598
+
599
+ /**
600
+ * Parse assignment or expression statement
601
+ */
602
+ parseAssignmentOrExpression() {
603
+ const startLoc = this.createLocation();
604
+
605
+ // Check for indexed assignment: name[i] = ... or name[i][j] = ...
606
+ if (this.check(TokenType.IDENTIFIER)) {
607
+ const savedPos = this.pos;
608
+
609
+ // Try to parse as indexed assignment
610
+ const indexedResult = this.tryParseIndexedAssignment(startLoc);
611
+ if (indexedResult) {
612
+ return indexedResult;
613
+ }
614
+
615
+ // Restore position if not indexed assignment
616
+ this.pos = savedPos;
617
+ }
618
+
619
+ // Try to parse as regular assignment (possibly with destructuring)
620
+ if (this.check(TokenType.IDENTIFIER)) {
621
+ const savedPos = this.pos;
622
+
623
+ // Parse destructuring list: a, b, c OR a as x, b, c as z
624
+ const destructuring = this.parseDestructuringList();
625
+
626
+ // Check for type annotation BEFORE = (syntax: x: Int = 42, x: {name: string} = ...)
627
+ let typeAnnotation = null;
628
+ if (destructuring.length === 1 && this.check(TokenType.COLON)) {
629
+ // Check if next is a type start: identifier, { (record), or ( (function)
630
+ const aheadPos = this.pos + 1;
631
+ if (aheadPos < this.tokens.length) {
632
+ const nextType = this.tokens[aheadPos].type;
633
+ if (
634
+ nextType === TokenType.IDENTIFIER ||
635
+ nextType === TokenType.LBRACE ||
636
+ nextType === TokenType.LPAREN
637
+ ) {
638
+ this.advance(); // consume :
639
+ typeAnnotation = this.parseTypeAnnotation();
640
+ }
641
+ }
642
+ }
643
+
644
+ // Check if this is actually an assignment (= or :=)
645
+ const isDestructuringAssignment = this.match(TokenType.COLON_ASSIGN);
646
+ const isRegularAssignment =
647
+ !isDestructuringAssignment && this.match(TokenType.ASSIGN);
648
+
649
+ if (isDestructuringAssignment || isRegularAssignment) {
650
+ // Type annotation with := is not allowed
651
+ if (isDestructuringAssignment && typeAnnotation) {
652
+ throw this._parseError(
653
+ `Type annotations are not allowed with := operator`,
654
+ { line: startLoc.line, column: startLoc.column }
655
+ );
656
+ }
657
+
658
+ // Parse the right-hand side
659
+ const value = this.parseExpression();
660
+
661
+ // Handle destructuring with := operator
662
+ if (isDestructuringAssignment) {
663
+ // Special handling for LoopExpression - attach destructuring to the loop
664
+ if (value.type === "LoopExpression") {
665
+ value.destructuring = destructuring;
666
+ value.isDestructured = true;
667
+ // Only validate destructuring if there are multiple targets
668
+ // Single name binding (e.g., result := loop(...)) binds the whole loop result object
669
+ if (destructuring.length > 1) {
670
+ this.validateLoopDestructuring(value);
671
+ }
672
+ return new Assignment(destructuring[0].name, value, null, startLoc);
673
+ }
674
+
675
+ // For any other expression (including ObjectLiteral, Identifier, etc.)
676
+ // create a DestructuringAssignment node
677
+ return new DestructuringAssignment(destructuring, value, startLoc);
678
+ }
679
+
680
+ // Handle multi-variable destructuring with = (also requires loop/record)
681
+ if (isRegularAssignment && destructuring.length > 1) {
682
+ if (
683
+ value.type !== "LoopExpression" &&
684
+ value.type !== "ObjectLiteral"
685
+ ) {
686
+ throw this._parseError(
687
+ `Multi-variable assignment requires a loop or record expression`,
688
+ { line: startLoc.line, column: startLoc.column }
689
+ );
690
+ }
691
+
692
+ if (value.type === "LoopExpression") {
693
+ value.destructuring = destructuring;
694
+ value.isDestructured = false;
695
+ this.validateLoopDestructuring(value);
696
+ }
697
+
698
+ return new Assignment(destructuring[0].name, value, null, startLoc);
699
+ }
700
+
701
+ // Single variable assignment (standard case)
702
+ if (destructuring.length === 1) {
703
+ if (value.type === "LoopExpression") {
704
+ value.isDestructured = false;
705
+ }
706
+ // Promote number literals to complex when type annotation is :complex
707
+ let assignValue = value;
708
+ if (typeAnnotation?.kind === "simple" && typeAnnotation.details?.name === "complex") {
709
+ if (value?.type === "Literal" && value.literalType === "number") {
710
+ assignValue = new Literal(new Complex(value.value, 0), "complex", value.location);
711
+ } else if (value?.type === "UnaryOp" && value.operator === "-" &&
712
+ value.operand?.type === "Literal" && value.operand.literalType === "number") {
713
+ // Handle negative numbers: -3 is parsed as UnaryOp("-", Literal(3))
714
+ assignValue = new Literal(new Complex(-value.operand.value, 0), "complex", value.location);
715
+ }
716
+ }
717
+ return new Assignment(
718
+ destructuring[0].name,
719
+ assignValue,
720
+ typeAnnotation,
721
+ startLoc
722
+ );
723
+ }
724
+ }
725
+
726
+ // Not an assignment - restore position and parse as expression
727
+ this.pos = savedPos;
728
+ }
729
+
730
+ // Parse as expression
731
+ const expr = this.parseExpression();
732
+ return expr;
733
+ }
734
+
735
+ /**
736
+ * Try to parse indexed assignment: name[i] = expr or name[i][j] = expr
737
+ * Returns null if not an indexed assignment
738
+ */
739
+ tryParseIndexedAssignment(startLoc) {
740
+ const targetName = this.advance().value;
741
+ const indices = [];
742
+
743
+ // Must have at least one [
744
+ if (!this.check(TokenType.LBRACKET)) {
745
+ return null;
746
+ }
747
+
748
+ // Collect all indices
749
+ while (this.match(TokenType.LBRACKET)) {
750
+ // Index must be a simple identifier for mapping
751
+ if (!this.check(TokenType.IDENTIFIER)) {
752
+ return null; // Not a mapping pattern, might be computed access
753
+ }
754
+
755
+ const indexName = this.advance().value;
756
+
757
+ if (!this.match(TokenType.RBRACKET)) {
758
+ return null;
759
+ }
760
+
761
+ indices.push(indexName);
762
+ }
763
+
764
+ // Must be followed by = for indexed assignment
765
+ if (!this.match(TokenType.ASSIGN)) {
766
+ return null;
767
+ }
768
+
769
+ // Parse the value expression
770
+ const value = this.parseExpression();
771
+
772
+ return new IndexedAssignment(targetName, indices, value, startLoc);
773
+ }
774
+
775
+ /**
776
+ * Parse destructuring list: a, b, c OR a as x, b, c as z
777
+ */
778
+ parseDestructuringList() {
779
+ const destructuring = [];
780
+
781
+ if (!this.check(TokenType.IDENTIFIER)) {
782
+ return destructuring;
783
+ }
784
+
785
+ let name = this.advance().value;
786
+ let alias = null;
787
+
788
+ if (this.matchContextualKeyword("as")) {
789
+ alias = this.consume(
790
+ TokenType.IDENTIFIER,
791
+ "Expected alias name after 'as'"
792
+ ).value;
793
+ }
794
+
795
+ destructuring.push({ name, alias });
796
+
797
+ while (this.match(TokenType.COMMA)) {
798
+ if (!this.check(TokenType.IDENTIFIER)) {
799
+ break;
800
+ }
801
+
802
+ name = this.advance().value;
803
+ alias = null;
804
+
805
+ if (this.matchContextualKeyword("as")) {
806
+ alias = this.consume(
807
+ TokenType.IDENTIFIER,
808
+ "Expected alias name after 'as'"
809
+ ).value;
810
+ }
811
+
812
+ destructuring.push({ name, alias });
813
+ }
814
+
815
+ return destructuring;
816
+ }
817
+
818
+ /**
819
+ * Validate that destructuring matches loop variables
820
+ */
821
+ validateLoopDestructuring(loopExpr) {
822
+ if (!loopExpr.destructuring) return;
823
+
824
+ const variableNames = new Set(loopExpr.variables.map((v) => v.name));
825
+
826
+ for (const { name } of loopExpr.destructuring) {
827
+ if (!variableNames.has(name)) {
828
+ throw this._parseError(
829
+ `Variable '${name}' does not match any loop variable. ` +
830
+ `Available variables: ${Array.from(variableNames).join(", ")}`,
831
+ { line: loopExpr.location.line, column: loopExpr.location.column }
832
+ );
833
+ }
834
+ }
835
+ }
836
+
837
+ /**
838
+ * Parse return statement
839
+ */
840
+ parseReturn() {
841
+ const startLoc = this.createLocation();
842
+ const value = this.parseExpression();
843
+ return new ReturnStatement(value, startLoc);
844
+ }
845
+
846
+ /**
847
+ * Parse an expression (entry point - lowest precedence)
848
+ */
849
+ parseExpression() {
850
+ return this.parseOr();
851
+ }
852
+
853
+ /**
854
+ * Parse logical OR: ||
855
+ */
856
+ parseOr() {
857
+ let expr = this.parseAnd();
858
+
859
+ while (this.match(TokenType.OR)) {
860
+ const opToken = this.previous();
861
+ const opLoc = { line: opToken.line, column: opToken.column };
862
+ const right = this.parseAnd();
863
+ expr = new BinaryOp("||", expr, right, opLoc);
864
+ }
865
+
866
+ return expr;
867
+ }
868
+
869
+ /**
870
+ * Parse logical AND: &&
871
+ */
872
+ parseAnd() {
873
+ let expr = this.parseComparison();
874
+
875
+ while (this.match(TokenType.AND)) {
876
+ const opToken = this.previous();
877
+ const opLoc = { line: opToken.line, column: opToken.column };
878
+ const right = this.parseComparison();
879
+ expr = new BinaryOp("&&", expr, right, opLoc);
880
+ }
881
+
882
+ return expr;
883
+ }
884
+
885
+ /**
886
+ * Parse comparison operations: <, >, <=, >=, ==, !=
887
+ */
888
+ parseComparison() {
889
+ let expr = this.parseAddition();
890
+
891
+ while (
892
+ this.match(
893
+ TokenType.LT,
894
+ TokenType.GT,
895
+ TokenType.LTE,
896
+ TokenType.GTE,
897
+ TokenType.EQ,
898
+ TokenType.NEQ
899
+ )
900
+ ) {
901
+ const opToken = this.previous();
902
+ const opLoc = { line: opToken.line, column: opToken.column };
903
+ const operator = opToken.value;
904
+ const right = this.parseAddition();
905
+ expr = new BinaryOp(operator, expr, right, opLoc);
906
+ }
907
+
908
+ return expr;
909
+ }
910
+
911
+ /**
912
+ * Parse addition and subtraction: +, -, .+, .-
913
+ */
914
+ parseAddition() {
915
+ let expr = this.parseMatrixMult();
916
+
917
+ while (
918
+ this.match(
919
+ TokenType.PLUS,
920
+ TokenType.MINUS,
921
+ TokenType.DOT_PLUS,
922
+ TokenType.DOT_MINUS
923
+ )
924
+ ) {
925
+ const opToken = this.previous();
926
+ const opLoc = { line: opToken.line, column: opToken.column };
927
+ const operator = opToken.value;
928
+ const right = this.parseMatrixMult();
929
+ expr = new BinaryOp(operator, expr, right, opLoc);
930
+ }
931
+
932
+ return expr;
933
+ }
934
+
935
+ /**
936
+ * Parse matrix multiplication: @
937
+ */
938
+ parseMatrixMult() {
939
+ let expr = this.parseMultiplication();
940
+
941
+ while (this.match(TokenType.AT)) {
942
+ const opToken = this.previous();
943
+ const opLoc = { line: opToken.line, column: opToken.column };
944
+ const right = this.parseMultiplication();
945
+ expr = new BinaryOp("@", expr, right, opLoc);
946
+ }
947
+
948
+ return expr;
949
+ }
950
+
951
+ /**
952
+ * Parse multiplication, division, modulo: *, /, %, .*, ./
953
+ */
954
+ parseMultiplication() {
955
+ let expr = this.parsePower();
956
+
957
+ while (
958
+ this.match(
959
+ TokenType.STAR,
960
+ TokenType.SLASH,
961
+ TokenType.PERCENT,
962
+ TokenType.DOT_STAR,
963
+ TokenType.DOT_SLASH
964
+ )
965
+ ) {
966
+ const opToken = this.previous();
967
+ const opLoc = { line: opToken.line, column: opToken.column };
968
+ const operator = opToken.value;
969
+ const right = this.parsePower();
970
+ expr = new BinaryOp(operator, expr, right, opLoc);
971
+ }
972
+
973
+ return expr;
974
+ }
975
+
976
+ /**
977
+ * Parse power: ^, .^ (right-associative)
978
+ */
979
+ parsePower() {
980
+ let expr = this.parseUnary();
981
+
982
+ // Right-associative: a ^ b ^ c = a ^ (b ^ c)
983
+ if (this.match(TokenType.CARET, TokenType.DOT_CARET)) {
984
+ const opToken = this.previous();
985
+ const opLoc = { line: opToken.line, column: opToken.column };
986
+ const operator = opToken.value;
987
+ const right = this.parsePower(); // Recursively parse right side
988
+ expr = new BinaryOp(operator, expr, right, opLoc);
989
+ }
990
+
991
+ return expr;
992
+ }
993
+
994
+ /**
995
+ * Parse unary operations: -, !
996
+ */
997
+ parseUnary() {
998
+ if (this.match(TokenType.MINUS)) {
999
+ const opToken = this.previous();
1000
+ const opLoc = { line: opToken.line, column: opToken.column };
1001
+ const operand = this.parseUnary();
1002
+ return new UnaryOp("-", operand, opLoc);
1003
+ }
1004
+
1005
+ if (this.match(TokenType.NOT)) {
1006
+ const opToken = this.previous();
1007
+ const opLoc = { line: opToken.line, column: opToken.column };
1008
+ const operand = this.parseUnary();
1009
+ return new UnaryOp("!", operand, opLoc);
1010
+ }
1011
+
1012
+ return this.parsePostfix();
1013
+ }
1014
+
1015
+ /**
1016
+ * Parse postfix operations: member access, function calls, array access, transpose
1017
+ */
1018
+ parsePostfix() {
1019
+ let expr = this.parsePrimary();
1020
+
1021
+ while (true) {
1022
+ // Member access: obj.field or transpose: A.T
1023
+ if (this.match(TokenType.DOT)) {
1024
+ const dotToken = this.previous();
1025
+ const dotLoc = { line: dotToken.line, column: dotToken.column };
1026
+
1027
+ // Accept IDENTIFIER or TYPE keyword as property name
1028
+ // (TYPE is contextual - allowed as property name like obj.type)
1029
+ let property;
1030
+ if (this.check(TokenType.TYPE)) {
1031
+ property = this.advance();
1032
+ } else {
1033
+ property = this.consume(TokenType.IDENTIFIER, "Expected property name");
1034
+ }
1035
+
1036
+ expr = new MemberAccess(
1037
+ expr,
1038
+ new Identifier(property.value),
1039
+ false,
1040
+ dotLoc
1041
+ );
1042
+ }
1043
+ // Array/computed access: obj[index]
1044
+ else if (this.match(TokenType.LBRACKET)) {
1045
+ const bracketToken = this.previous();
1046
+ const bracketLoc = { line: bracketToken.line, column: bracketToken.column };
1047
+ const index = this.parseExpression();
1048
+ this.consume(TokenType.RBRACKET, "Expected ']'");
1049
+ expr = new MemberAccess(expr, index, true, bracketLoc);
1050
+ }
1051
+ // Function call: func(args) or obj.method(args)
1052
+ else if (this.check(TokenType.LPAREN) && (expr instanceof Identifier || expr instanceof MemberAccess)) {
1053
+ this.advance();
1054
+ const parenToken = this.previous();
1055
+ const parenLoc = { line: parenToken.line, column: parenToken.column };
1056
+ const args = this.parseArguments();
1057
+ this.consume(TokenType.RPAREN, "Expected ')'");
1058
+ if (expr instanceof Identifier) {
1059
+ expr = new FunctionCall(expr.name, args, parenLoc);
1060
+ } else {
1061
+ // Method call: obj.method(args) becomes FunctionCall with callee expression
1062
+ expr = new FunctionCall(null, args, parenLoc, expr);
1063
+ }
1064
+ } else {
1065
+ break;
1066
+ }
1067
+ }
1068
+
1069
+ return expr;
1070
+ }
1071
+
1072
+ /**
1073
+ * Parse primary expressions (literals, identifiers, loops, branches, etc.)
1074
+ */
1075
+ parsePrimary() {
1076
+ const startLoc = this.createLocation();
1077
+
1078
+ // Literals
1079
+ if (this.match(TokenType.NUMBER)) {
1080
+ const value = this.previous().value;
1081
+
1082
+ // Check for range operator
1083
+ if (this.match(TokenType.RANGE)) {
1084
+ // Default range (inclusive like ..=)
1085
+ const end = this.parseExpression();
1086
+ let step = null;
1087
+ if (this.match(TokenType.BY)) {
1088
+ step = this.parseExpression();
1089
+ }
1090
+ return new Range(
1091
+ new Literal(value, "number", startLoc),
1092
+ end,
1093
+ true,
1094
+ step,
1095
+ startLoc
1096
+ );
1097
+ } else if (this.match(TokenType.RANGE_EXCLUSIVE)) {
1098
+ const end = this.parseExpression();
1099
+ let step = null;
1100
+ if (this.match(TokenType.BY)) {
1101
+ step = this.parseExpression();
1102
+ }
1103
+ return new Range(
1104
+ new Literal(value, "number", startLoc),
1105
+ end,
1106
+ false,
1107
+ step,
1108
+ startLoc
1109
+ );
1110
+ } else if (this.match(TokenType.RANGE_INCLUSIVE)) {
1111
+ const end = this.parseExpression();
1112
+ let step = null;
1113
+ if (this.match(TokenType.BY)) {
1114
+ step = this.parseExpression();
1115
+ }
1116
+ return new Range(
1117
+ new Literal(value, "number", startLoc),
1118
+ end,
1119
+ true,
1120
+ step,
1121
+ startLoc
1122
+ );
1123
+ }
1124
+
1125
+ return new Literal(value, "number", startLoc);
1126
+ }
1127
+
1128
+ // Imaginary literal: 4i or 4j → Complex(0, 4)
1129
+ if (this.match(TokenType.IMAGINARY)) {
1130
+ const imagValue = this.previous().value;
1131
+ return new Literal(new Complex(0, imagValue), "complex", startLoc);
1132
+ }
1133
+
1134
+ if (this.match(TokenType.STRING)) {
1135
+ return new Literal(this.previous().value, "string", startLoc);
1136
+ }
1137
+
1138
+ if (this.match(TokenType.TRUE)) {
1139
+ return new Literal(true, "bool", startLoc);
1140
+ }
1141
+
1142
+ if (this.match(TokenType.FALSE)) {
1143
+ return new Literal(false, "bool", startLoc);
1144
+ }
1145
+
1146
+ if (this.match(TokenType.NONE)) {
1147
+ return new Literal(null, "none", startLoc);
1148
+ }
1149
+
1150
+ // F-string (string interpolation)
1151
+ if (this.match(TokenType.F_STRING_START)) {
1152
+ const fstringToken = this.previous();
1153
+ const fstringLoc = { line: fstringToken.line, column: fstringToken.column };
1154
+ return this.parseFString(fstringToken.value, fstringLoc);
1155
+ }
1156
+
1157
+ // Array literal: [1, 2, 3]
1158
+ if (this.match(TokenType.LBRACKET)) {
1159
+ return this.parseArrayLiteral();
1160
+ }
1161
+
1162
+ // Unified braces: binding structure or string-keyed object literal
1163
+ // Syntax: { name = value, ... } for bindings (objects)
1164
+ // { "key": value, ... } for string-keyed objects (JSON-like)
1165
+ // { key: value, ... } for identifier-keyed objects (also JSON-like)
1166
+ if (this.match(TokenType.LBRACE)) {
1167
+ // String-keyed object literal: {"name": value} - for JSON compatibility
1168
+ if (
1169
+ this.check(TokenType.STRING) &&
1170
+ this.checkAhead(TokenType.COLON, 1)
1171
+ ) {
1172
+ return this.parseObjectLiteral();
1173
+ }
1174
+ // Identifier-keyed object literal: {name: value} - for JSON compatibility
1175
+ // Distinguished from binding with type (name: Type = value) by checking
1176
+ // if the token after : is NOT a valid type start (IDENTIFIER, LBRACE, LPAREN)
1177
+ if (
1178
+ this.check(TokenType.IDENTIFIER) &&
1179
+ this.checkAhead(TokenType.COLON, 1)
1180
+ ) {
1181
+ // Look at token after the colon (position + 2)
1182
+ const afterColonType = this.tokens[this.pos + 2]?.type;
1183
+ // If it's NOT a type start, it's an object literal property
1184
+ if (
1185
+ afterColonType !== TokenType.IDENTIFIER &&
1186
+ afterColonType !== TokenType.LBRACE &&
1187
+ afterColonType !== TokenType.LPAREN
1188
+ ) {
1189
+ return this.parseObjectLiteral();
1190
+ }
1191
+ }
1192
+ if (this.check(TokenType.RBRACE)) {
1193
+ // Empty object
1194
+ this.advance();
1195
+ return new ObjectLiteral([], startLoc);
1196
+ }
1197
+ // Binding structure: { name = value, ... } or { statements... }
1198
+ // Could be standalone or followed by for/while driver (making it a loop)
1199
+ const structure = this.parseBindingStructure(startLoc);
1200
+
1201
+ // Check if followed by driver → it's a loop expression
1202
+ if (this.check(TokenType.FOR) || this.check(TokenType.WHILE)) {
1203
+ return this.parseLoopWithState(structure, startLoc);
1204
+ }
1205
+
1206
+ // Driverless loop: { state } { body } - infinite loop until break
1207
+ if (this.check(TokenType.LBRACE)) {
1208
+ return this.parseDriverlessLoop(structure, startLoc);
1209
+ }
1210
+
1211
+ return structure;
1212
+ }
1213
+
1214
+ // Branch expression (if/elif/else)
1215
+ if (this.match(TokenType.IF)) {
1216
+ const ifToken = this.previous();
1217
+ const ifLoc = { line: ifToken.line, column: ifToken.column };
1218
+ return this.parseBranchExpression(ifLoc);
1219
+ }
1220
+
1221
+ // Identifier
1222
+ if (this.match(TokenType.IDENTIFIER)) {
1223
+ const name = this.previous().value;
1224
+ return new Identifier(name, startLoc);
1225
+ }
1226
+
1227
+ // Parenthesized expression
1228
+ if (this.match(TokenType.LPAREN)) {
1229
+ const expr = this.parseExpression();
1230
+ this.consume(TokenType.RPAREN, "Expected ')' after expression");
1231
+ return expr;
1232
+ }
1233
+
1234
+ throw this._parseError(`Unexpected token: ${this.peek().type}`, this.peek());
1235
+ }
1236
+
1237
+ /**
1238
+ * Parse array literal: [1, 2, 3]
1239
+ */
1240
+ parseArrayLiteral() {
1241
+ // Get location of [ which was just consumed before this function was called
1242
+ const bracketToken = this.previous();
1243
+ const startLoc = { line: bracketToken.line, column: bracketToken.column };
1244
+ const elements = [];
1245
+
1246
+ if (!this.check(TokenType.RBRACKET)) {
1247
+ do {
1248
+ elements.push(this.parseExpression());
1249
+ } while (this.match(TokenType.COMMA));
1250
+ }
1251
+
1252
+ this.consume(TokenType.RBRACKET, "Expected ']'");
1253
+ return new ArrayLiteral(elements, startLoc);
1254
+ }
1255
+
1256
+ /**
1257
+ * Parse object literal: {name: "Alice", age: 30} or {"name": "Alice"}
1258
+ */
1259
+ parseObjectLiteral() {
1260
+ // Get location of { which was just consumed before this function was called
1261
+ const braceToken = this.previous();
1262
+ const startLoc = { line: braceToken.line, column: braceToken.column };
1263
+ const properties = [];
1264
+
1265
+ if (!this.check(TokenType.RBRACE)) {
1266
+ do {
1267
+ // Key can be identifier, string, or 'type' keyword (contextual)
1268
+ let key;
1269
+ if (this.check(TokenType.IDENTIFIER)) {
1270
+ key = this.advance().value;
1271
+ } else if (this.check(TokenType.TYPE)) {
1272
+ // Allow 'type' as field name (it's a contextual keyword)
1273
+ key = this.advance().value;
1274
+ } else if (this.check(TokenType.STRING)) {
1275
+ key = this.advance().value;
1276
+ } else {
1277
+ throw this._parseError(`Expected property name`, this.peek());
1278
+ }
1279
+ this.consume(TokenType.COLON, "Expected ':' after property name");
1280
+ const value = this.parseExpression();
1281
+ properties.push({ key, value });
1282
+ } while (this.match(TokenType.COMMA));
1283
+ }
1284
+
1285
+ this.consume(TokenType.RBRACE, "Expected '}'");
1286
+ return new ObjectLiteral(properties, startLoc);
1287
+ }
1288
+
1289
+ /**
1290
+ * Parse block expression: { statements... return value }
1291
+ * A block expression is a sequence of statements ending with a return.
1292
+ * @example { x = 5; y = x * 2; return y }
1293
+ */
1294
+ parseBlockExpression(startLoc) {
1295
+ const body = this.parseBlock();
1296
+ this.consume(TokenType.RBRACE, "Expected '}' after block expression");
1297
+ return new BlockExpression(body, startLoc);
1298
+ }
1299
+
1300
+ /**
1301
+ * Parse f-string (string interpolation)
1302
+ * @param {Array} parts - The f-string parts from tokenizer
1303
+ * @param {Object} startLoc - Location of the f-string token
1304
+ */
1305
+ parseFString(parts, startLoc) {
1306
+ const literals = [];
1307
+ const expressions = [];
1308
+
1309
+ for (const part of parts) {
1310
+ if (part.type === "literal") {
1311
+ literals.push(part.value);
1312
+ } else if (part.type === "interpolation") {
1313
+ const lexer = new Lexer(part.value);
1314
+ const tokens = lexer.tokenize();
1315
+ const parser = new Parser(tokens);
1316
+ const expr = parser.parseExpression();
1317
+ expressions.push(expr);
1318
+ }
1319
+ }
1320
+
1321
+ return new StringInterpolation(
1322
+ literals,
1323
+ expressions,
1324
+ startLoc
1325
+ );
1326
+ }
1327
+
1328
+ /**
1329
+ * Parse binding structure: { name = value, name: type = value, ..., trailingExpr? }
1330
+ *
1331
+ * A binding structure is a unified construct for:
1332
+ * - Object-like data: { x = 10, y = 20 } → { x: 10, y: 20 }
1333
+ * - Blocks with computation: { x = 10, x + 1 } → 11
1334
+ * - Loop state: { sum = 0 } for (x in xs) { sum' = sum + x }
1335
+ *
1336
+ * @param startLoc - Location where the { was found
1337
+ */
1338
+ parseBindingStructure(startLoc) {
1339
+ const bindings = [];
1340
+ let trailingExpr = null;
1341
+
1342
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
1343
+ // Check if this looks like a binding: IDENTIFIER (: type)? =
1344
+ if (this.isBindingStart()) {
1345
+ const binding = this.parseBinding();
1346
+ bindings.push(binding);
1347
+ } else {
1348
+ // Not a binding - it's either an effect, return statement, or trailing expression
1349
+
1350
+ // Handle return statements inside binding structures
1351
+ if (this.match(TokenType.RETURN)) {
1352
+ const returnExpr = this.parseExpression();
1353
+ const returnStmt = new ReturnStatement(returnExpr, this.createLocation());
1354
+
1355
+ // If followed by }, this return is the trailing expression
1356
+ if (this.check(TokenType.RBRACE)) {
1357
+ trailingExpr = returnStmt;
1358
+ } else {
1359
+ // Return as an effect (e.g., early return in a conditional)
1360
+ bindings.push(new Binding(null, returnStmt, null));
1361
+ }
1362
+ } else {
1363
+ // Parse as expression
1364
+ const expr = this.parseExpression();
1365
+
1366
+ // Check what comes next to determine if this is trailing or effect
1367
+ // If followed by comma/newline and more content, it's an effect
1368
+ // If followed by }, it's the trailing expression
1369
+ if (this.check(TokenType.RBRACE)) {
1370
+ trailingExpr = expr;
1371
+ } else {
1372
+ // It's an effect - wrap as a binding with no name (or store separately)
1373
+ // For now, we treat effects as bindings with generated names (or we can just
1374
+ // store them in a separate effects array - but let's keep it simple)
1375
+ // Actually, looking at the spec, effects don't need to be bound.
1376
+ // Let's store them as special "effect" bindings with null name.
1377
+ bindings.push(new Binding(null, expr, null));
1378
+ }
1379
+ }
1380
+ }
1381
+
1382
+ // Skip comma if present (commas and newlines are interchangeable separators)
1383
+ this.match(TokenType.COMMA);
1384
+ }
1385
+
1386
+ this.consume(TokenType.RBRACE, "Expected '}' after binding structure");
1387
+ return new BindingStructure(bindings, trailingExpr, startLoc);
1388
+ }
1389
+
1390
+ /**
1391
+ * Check if current position looks like a binding start: IDENTIFIER (: type)? (= expr)?
1392
+ * Uses lookahead to distinguish bindings from expressions
1393
+ *
1394
+ * Now also detects type descriptor bindings: name: type (without = value)
1395
+ */
1396
+ isBindingStart() {
1397
+ // Allow both IDENTIFIER and TYPE keyword as binding name
1398
+ // (TYPE is contextual - allowed as field name in objects)
1399
+ if (!this.check(TokenType.IDENTIFIER) && !this.check(TokenType.TYPE)) {
1400
+ return false;
1401
+ }
1402
+
1403
+ // Save position for lookahead
1404
+ const savedPos = this.pos;
1405
+ this.advance(); // consume identifier/type
1406
+
1407
+ let isBinding = false;
1408
+
1409
+ // Check for optional type annotation
1410
+ if (this.check(TokenType.COLON)) {
1411
+ // Could be:
1412
+ // - binding with type and value: name: Type = value
1413
+ // - type descriptor: name: Type (no value, ends at comma/rbrace)
1414
+ // - object literal property: key: value (NOT a binding)
1415
+ this.advance(); // consume :
1416
+
1417
+ // Check if the first token after : is a valid type start
1418
+ // Valid type starts: IDENTIFIER (simple type), LBRACE (record/dict type), LPAREN (function type)
1419
+ // If it's something else (NUMBER, STRING literal, etc.), it's an object literal property
1420
+ const nextType = this.peek().type;
1421
+ if (
1422
+ nextType !== TokenType.IDENTIFIER &&
1423
+ nextType !== TokenType.LBRACE &&
1424
+ nextType !== TokenType.LPAREN
1425
+ ) {
1426
+ // Not a type annotation - this is an object literal property like {key: 5}
1427
+ this.pos = savedPos;
1428
+ return false;
1429
+ }
1430
+
1431
+ // Skip type annotation tokens
1432
+ // Type annotations can contain: identifiers, <, >, commas (in generics), |, ?, [], {}
1433
+ let parenDepth = 0;
1434
+ let braceDepth = 0;
1435
+ let bracketDepth = 0;
1436
+ let angleDepth = 0;
1437
+
1438
+ while (!this.isAtEnd()) {
1439
+ // Track nesting
1440
+ if (this.check(TokenType.LPAREN)) parenDepth++;
1441
+ else if (this.check(TokenType.RPAREN)) parenDepth--;
1442
+ else if (this.check(TokenType.LBRACE)) braceDepth++;
1443
+ else if (this.check(TokenType.RBRACE)) {
1444
+ if (braceDepth === 0) {
1445
+ // End of containing structure - this is a type descriptor without value
1446
+ isBinding = true;
1447
+ break;
1448
+ }
1449
+ braceDepth--;
1450
+ }
1451
+ else if (this.check(TokenType.LBRACKET)) bracketDepth++;
1452
+ else if (this.check(TokenType.RBRACKET)) bracketDepth--;
1453
+ else if (this.check(TokenType.LESS)) angleDepth++;
1454
+ else if (this.check(TokenType.GREATER)) angleDepth--;
1455
+
1456
+ // At top level (not nested in generics/braces), check for terminators
1457
+ if (parenDepth === 0 && braceDepth === 0 && bracketDepth === 0 && angleDepth === 0) {
1458
+ if (this.check(TokenType.ASSIGN)) {
1459
+ isBinding = true; // name: Type = value
1460
+ break;
1461
+ }
1462
+ if (this.check(TokenType.COMMA)) {
1463
+ // Type descriptor without value: name: Type,
1464
+ isBinding = true;
1465
+ break;
1466
+ }
1467
+ }
1468
+
1469
+ this.advance();
1470
+ }
1471
+ } else if (this.check(TokenType.ASSIGN)) {
1472
+ // Simple binding: name =
1473
+ isBinding = true;
1474
+ }
1475
+
1476
+ // Restore position
1477
+ this.pos = savedPos;
1478
+ return isBinding;
1479
+ }
1480
+
1481
+ /**
1482
+ * Parse a single binding: name (: type)? (= expr)?
1483
+ *
1484
+ * Supports:
1485
+ * - Regular binding: name = expr
1486
+ * - Typed binding: name: type = expr
1487
+ * - Type descriptor (no value): name: type
1488
+ * - Type descriptor with default: name: type = default
1489
+ */
1490
+ parseBinding() {
1491
+ // Accept both IDENTIFIER and TYPE keyword as binding name
1492
+ // (TYPE is contextual - allowed as field name in objects like { type = "line" })
1493
+ let name;
1494
+ if (this.check(TokenType.TYPE)) {
1495
+ name = this.advance().value;
1496
+ } else {
1497
+ name = this.consume(TokenType.IDENTIFIER, "Expected binding name").value;
1498
+ }
1499
+
1500
+ let typeAnnotation = null;
1501
+ let value = null;
1502
+ let isTypeDescriptor = false;
1503
+
1504
+ if (this.match(TokenType.COLON)) {
1505
+ typeAnnotation = this.parseTypeAnnotation();
1506
+ isTypeDescriptor = true; // Has type annotation = type descriptor
1507
+
1508
+ // Check for optional default value
1509
+ if (this.match(TokenType.ASSIGN)) {
1510
+ value = this.parseExpression();
1511
+ }
1512
+ // If no = follows, this is a type descriptor without default (required field)
1513
+ } else {
1514
+ // No type annotation - regular binding requires a value
1515
+ this.consume(TokenType.ASSIGN, "Expected '=' in binding");
1516
+ value = this.parseExpression();
1517
+ }
1518
+
1519
+ // Promote number literals to complex when type annotation is :complex
1520
+ // This ensures `a:complex = -3` creates a Complex(-3, 0) at parse time
1521
+ if (typeAnnotation?.kind === "simple" && typeAnnotation.details?.name === "complex") {
1522
+ if (value?.type === "Literal" && value.literalType === "number") {
1523
+ value = new Literal(new Complex(value.value, 0), "complex", value.location);
1524
+ } else if (value?.type === "UnaryOp" && value.operator === "-" &&
1525
+ value.operand?.type === "Literal" && value.operand.literalType === "number") {
1526
+ // Handle negative numbers: -3 is parsed as UnaryOp("-", Literal(3))
1527
+ value = new Literal(new Complex(-value.operand.value, 0), "complex", value.location);
1528
+ }
1529
+ }
1530
+
1531
+ return new Binding(name, value, typeAnnotation, isTypeDescriptor);
1532
+ }
1533
+
1534
+ /**
1535
+ * Parse loop with state block: { state } driver { body }
1536
+ * Called when we've already parsed the state block and see a driver keyword
1537
+ *
1538
+ * @param stateBlock - Already parsed BindingStructure for initial state
1539
+ * @param startLoc - Location of the opening {
1540
+ */
1541
+ parseLoopWithState(stateBlock, startLoc) {
1542
+ // Parse driver chain
1543
+ const drivers = this.parseDriverChain();
1544
+
1545
+ // Parse body using parseBlock (supports full statement syntax including destructuring)
1546
+ this.consume(TokenType.LBRACE, "Expected '{' to start loop body");
1547
+ const bodyStatements = this.parseBlock();
1548
+ this.consume(TokenType.RBRACE, "Expected '}' to end loop body");
1549
+
1550
+ // Convert BindingStructure bindings to LoopVariables for compatibility
1551
+ const variables = stateBlock.bindings.map(b =>
1552
+ new LoopVariable(b.name, b.value, b.typeAnnotation)
1553
+ );
1554
+
1555
+ let loopExpr = new LoopExpression(variables, drivers, bodyStatements, null, startLoc);
1556
+ loopExpr = this.validateAndNormalizeLoopDrivers(loopExpr);
1557
+
1558
+ return loopExpr;
1559
+ }
1560
+
1561
+ /**
1562
+ * Parse driverless (infinite) loop: { state } { body }
1563
+ * This is an infinite loop that continues until break
1564
+ *
1565
+ * @param stateBlock - Already parsed BindingStructure for initial state
1566
+ * @param startLoc - Location of the opening {
1567
+ */
1568
+ parseDriverlessLoop(stateBlock, startLoc) {
1569
+ // Parse body using parseBlock
1570
+ this.consume(TokenType.LBRACE, "Expected '{' to start loop body");
1571
+ const bodyStatements = this.parseBlock();
1572
+ this.consume(TokenType.RBRACE, "Expected '}' to end loop body");
1573
+
1574
+ // Convert BindingStructure bindings to LoopVariables
1575
+ const variables = stateBlock.bindings.map(b =>
1576
+ new LoopVariable(b.name, b.value, b.typeAnnotation)
1577
+ );
1578
+
1579
+ // Create LoopExpression with empty drivers array (infinite loop)
1580
+ return new LoopExpression(variables, [], bodyStatements, null, startLoc);
1581
+ }
1582
+
1583
+ /**
1584
+ * Convert BindingStructure to array of statements for backward compatibility
1585
+ * This is a transitional helper until the executor is updated
1586
+ */
1587
+ bindingStructureToStatements(structure) {
1588
+ const statements = [];
1589
+
1590
+ for (const binding of structure.bindings) {
1591
+ if (binding.name === null) {
1592
+ // Effect - just add the expression as a statement
1593
+ if (binding.value) {
1594
+ statements.push(binding.value);
1595
+ }
1596
+ } else if (binding.value) {
1597
+ // Convert to Assignment - prime notation (sum') is handled by the executor context
1598
+ // Assignment(target, value, typeAnnotation, location)
1599
+ statements.push(new Assignment(
1600
+ binding.name, // Keep full name including prime suffix (sum')
1601
+ binding.value,
1602
+ binding.typeAnnotation,
1603
+ binding.value.location
1604
+ ));
1605
+ }
1606
+ // Skip type descriptor bindings without values (name: type) in block context
1607
+ // They don't produce statements - they're just type declarations
1608
+ }
1609
+
1610
+ // Add trailing expression as return if present
1611
+ if (structure.trailingExpr) {
1612
+ statements.push(new ReturnStatement(structure.trailingExpr, structure.trailingExpr.location));
1613
+ }
1614
+
1615
+ return statements;
1616
+ }
1617
+
1618
+ /**
1619
+ * Parse driver chain: for(...) while(...) for(...)
1620
+ */
1621
+ parseDriverChain() {
1622
+ const drivers = [];
1623
+
1624
+ while (this.check(TokenType.FOR) || this.check(TokenType.WHILE)) {
1625
+ const driver = this.parseDriver();
1626
+ if (driver) {
1627
+ drivers.push(driver);
1628
+ }
1629
+ }
1630
+
1631
+ return drivers;
1632
+ }
1633
+
1634
+ /**
1635
+ * Parse a single driver: for(...) or while(...)
1636
+ */
1637
+ parseDriver() {
1638
+ const startLoc = this.createLocation();
1639
+
1640
+ // for(pattern in iterable)
1641
+ if (this.match(TokenType.FOR)) {
1642
+ this.consume(TokenType.LPAREN, "Expected '(' after 'for'");
1643
+
1644
+ const pattern = this.consume(
1645
+ TokenType.IDENTIFIER,
1646
+ "Expected variable name in for driver"
1647
+ ).value;
1648
+
1649
+ this.consume(TokenType.IN, "Expected 'in' after variable");
1650
+
1651
+ const iterable = this.parseExpression();
1652
+
1653
+ let step = null;
1654
+ if (this.match(TokenType.BY)) {
1655
+ step = this.parseExpression();
1656
+ }
1657
+
1658
+ this.consume(TokenType.RPAREN, "Expected ')' after for driver");
1659
+
1660
+ return new ForDriver(pattern, iterable, step, startLoc);
1661
+ }
1662
+
1663
+ // while(condition)
1664
+ if (this.match(TokenType.WHILE)) {
1665
+ this.consume(TokenType.LPAREN, "Expected '(' after 'while'");
1666
+
1667
+ const condition = this.parseExpression();
1668
+
1669
+ this.consume(TokenType.RPAREN, "Expected ')' after while condition");
1670
+
1671
+ return new WhileDriver(condition, startLoc);
1672
+ }
1673
+
1674
+ return null;
1675
+ }
1676
+
1677
+ /**
1678
+ * Parse branch expression (if/elif/else)
1679
+ * @param {Object} startLoc - Location of the 'if' keyword (passed from caller)
1680
+ */
1681
+ parseBranchExpression(startLoc) {
1682
+ const paths = [];
1683
+
1684
+ // Parse 'if' branch
1685
+ this.consume(TokenType.LPAREN, "Expected '(' after 'if'");
1686
+ const ifCondition = this.parseExpression();
1687
+ this.consume(TokenType.RPAREN, "Expected ')' after if condition");
1688
+ this.consume(TokenType.LBRACE, "Expected '{' after if condition");
1689
+ const ifBody = this.parseBlock();
1690
+ this.consume(TokenType.RBRACE, "Expected '}' to end if body");
1691
+
1692
+ paths.push(new BranchPath("if", ifCondition, ifBody));
1693
+
1694
+ // Parse 'elif' branches
1695
+ while (this.match(TokenType.ELIF)) {
1696
+ this.consume(TokenType.LPAREN, "Expected '(' after 'elif'");
1697
+ const elifCondition = this.parseExpression();
1698
+ this.consume(TokenType.RPAREN, "Expected ')' after elif condition");
1699
+ this.consume(TokenType.LBRACE, "Expected '{' after elif condition");
1700
+ const elifBody = this.parseBlock();
1701
+ this.consume(TokenType.RBRACE, "Expected '}' to end elif body");
1702
+
1703
+ paths.push(new BranchPath("elif", elifCondition, elifBody));
1704
+ }
1705
+
1706
+ // Parse optional 'else' branch
1707
+ // NOTE: else is required for value-producing conditionals but optional for side-effect only
1708
+ if (this.match(TokenType.ELSE)) {
1709
+ this.consume(TokenType.LBRACE, "Expected '{' after else");
1710
+ const elseBody = this.parseBlock();
1711
+ this.consume(TokenType.RBRACE, "Expected '}' to end else body");
1712
+
1713
+ paths.push(new BranchPath("else", null, elseBody));
1714
+ }
1715
+
1716
+ return new BranchExpression(paths, startLoc);
1717
+ }
1718
+
1719
+ /**
1720
+ * Parse a block of statements
1721
+ */
1722
+ parseBlock() {
1723
+ const statements = [];
1724
+
1725
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
1726
+ const stmt = this.parseStatement();
1727
+ if (stmt) {
1728
+ statements.push(stmt);
1729
+ }
1730
+ }
1731
+
1732
+ return statements;
1733
+ }
1734
+
1735
+ /**
1736
+ * Parse function arguments
1737
+ */
1738
+ parseArguments() {
1739
+ const args = [];
1740
+
1741
+ if (!this.check(TokenType.RPAREN)) {
1742
+ do {
1743
+ args.push(this.parseExpression());
1744
+ } while (this.match(TokenType.COMMA));
1745
+ }
1746
+
1747
+ return args;
1748
+ }
1749
+
1750
+ /**
1751
+ * Parse type annotation
1752
+ *
1753
+ * Supports:
1754
+ * - Simple types: Int, Float, Bool, String, Unit
1755
+ * - Generic types: Array<Float, 2>, List<Int>
1756
+ * - Function types: (Int, Int) -> Float
1757
+ * - Record types: { x: Int, y: Float }, { x: Int, .. }
1758
+ * - Optional types: Int?, Array<Float, 2>?
1759
+ */
1760
+ parseTypeAnnotation() {
1761
+ const location = this.createLocation();
1762
+
1763
+ // Check for literal type: "on" or 42 or "on" | "off"
1764
+ if (this.check(TokenType.STRING) || this.check(TokenType.NUMBER) ||
1765
+ (this.check(TokenType.MINUS) && this.checkAhead(TokenType.NUMBER, 1))) {
1766
+ return this.parseLiteralTypeOrUnion(location);
1767
+ }
1768
+
1769
+ // Check for function type: (T1, T2) -> R
1770
+ if (this.check(TokenType.LPAREN)) {
1771
+ return this.parseFunctionType(location);
1772
+ }
1773
+
1774
+ // Check for record type: { x: Int, y: Float }
1775
+ if (this.check(TokenType.LBRACE)) {
1776
+ return this.parseRecordType(location);
1777
+ }
1778
+
1779
+ // Simple or generic type: Int, Array<Float, 2>, List<Int>
1780
+ const typeName = this.consume(
1781
+ TokenType.IDENTIFIER,
1782
+ "Expected type name"
1783
+ ).value;
1784
+
1785
+ let typeAnnotation;
1786
+
1787
+ // Check for generic parameters: <T, R>
1788
+ if (this.match(TokenType.LT)) {
1789
+ const params = this.parseGenericParams();
1790
+ this.consume(TokenType.GT, "Expected '>' after generic parameters");
1791
+ typeAnnotation = new TypeAnnotation(
1792
+ "generic",
1793
+ { name: typeName, params },
1794
+ location
1795
+ );
1796
+ } else {
1797
+ typeAnnotation = new TypeAnnotation(
1798
+ "simple",
1799
+ { name: typeName },
1800
+ location
1801
+ );
1802
+ }
1803
+
1804
+ // Check for optional marker: ?
1805
+ if (this.match(TokenType.QUESTION)) {
1806
+ typeAnnotation.isOptional = true;
1807
+ }
1808
+
1809
+ // Check for promotion syntax: Type | converter(param) | converter2(param) | ...
1810
+ if (this.match(TokenType.PIPE)) {
1811
+ return this.parsePromotionChain(typeAnnotation, location);
1812
+ }
1813
+
1814
+ return typeAnnotation;
1815
+ }
1816
+
1817
+ /**
1818
+ * Parse a literal type or literal union type.
1819
+ * Examples: "on", 42, -5, "on" | "off", 0 | 1 | 2
1820
+ */
1821
+ parseLiteralTypeOrUnion(location) {
1822
+ const firstLiteral = this.parseLiteralValue();
1823
+ const primitiveKind = typeof firstLiteral === 'string' ? 'string' : 'num';
1824
+ const values = [firstLiteral];
1825
+
1826
+ // Check for more | literal patterns
1827
+ while (this.check(TokenType.PIPE)) {
1828
+ // Peek ahead to see if this is a literal or a converter (identifier + lparen)
1829
+ // Literal union: "on" | "off"
1830
+ // Converter would be: Type | converter(param) - but we already parsed the first literal
1831
+ const nextIsLiteral = this.checkAhead(TokenType.STRING, 1) ||
1832
+ this.checkAhead(TokenType.NUMBER, 1) ||
1833
+ (this.checkAhead(TokenType.MINUS, 1) && this.checkAhead(TokenType.NUMBER, 2));
1834
+
1835
+ if (!nextIsLiteral) {
1836
+ break; // Not a literal, stop here
1837
+ }
1838
+
1839
+ this.advance(); // consume the PIPE
1840
+ const nextLiteral = this.parseLiteralValue();
1841
+ const nextKind = typeof nextLiteral === 'string' ? 'string' : 'num';
1842
+
1843
+ if (nextKind !== primitiveKind) {
1844
+ throw this._parseError(
1845
+ `Literal union values must all be the same type (got ${primitiveKind} and ${nextKind})`
1846
+ );
1847
+ }
1848
+ values.push(nextLiteral);
1849
+ }
1850
+
1851
+ if (values.length === 1) {
1852
+ return new TypeAnnotation("literal", {
1853
+ value: firstLiteral,
1854
+ primitiveKind: primitiveKind
1855
+ }, location);
1856
+ }
1857
+
1858
+ return new TypeAnnotation("literalUnion", {
1859
+ values: values,
1860
+ primitiveKind: primitiveKind
1861
+ }, location);
1862
+ }
1863
+
1864
+ /**
1865
+ * Parse a single literal value (string or number, possibly negative).
1866
+ */
1867
+ parseLiteralValue() {
1868
+ // Handle negative numbers
1869
+ if (this.match(TokenType.MINUS)) {
1870
+ const num = this.consume(TokenType.NUMBER, "Expected number after '-'").value;
1871
+ return -num;
1872
+ }
1873
+
1874
+ if (this.check(TokenType.STRING)) {
1875
+ return this.advance().value;
1876
+ }
1877
+
1878
+ if (this.check(TokenType.NUMBER)) {
1879
+ return this.advance().value;
1880
+ }
1881
+
1882
+ throw this._parseError("Expected string or number literal in type position");
1883
+ }
1884
+
1885
+ /**
1886
+ * Parse one or more converters after Type |
1887
+ * Examples: Point | from_num(p), Point | from_num(p) | from_arr(p)
1888
+ */
1889
+ parsePromotionChain(targetType, location) {
1890
+ const converters = [];
1891
+ let firstParamRef = null;
1892
+
1893
+ do {
1894
+ const converterName = this.consume(
1895
+ TokenType.IDENTIFIER,
1896
+ "Expected converter function name after '|'"
1897
+ ).value;
1898
+ this.consume(TokenType.LPAREN, "Expected '(' after converter function name");
1899
+ const paramRef = this.consume(
1900
+ TokenType.IDENTIFIER,
1901
+ "Expected parameter reference in converter call"
1902
+ ).value;
1903
+ this.consume(TokenType.RPAREN, "Expected ')' after parameter reference");
1904
+
1905
+ // Validate all converters reference the same parameter
1906
+ if (firstParamRef === null) {
1907
+ firstParamRef = paramRef;
1908
+ } else if (paramRef !== firstParamRef) {
1909
+ throw this._parseError(
1910
+ `All converters must reference the same parameter '${firstParamRef}', got '${paramRef}'`
1911
+ );
1912
+ }
1913
+
1914
+ converters.push({ funcName: converterName, paramRef: paramRef });
1915
+ } while (this.match(TokenType.PIPE));
1916
+
1917
+ // Use the new multi-converter format
1918
+ return new TypeAnnotation(
1919
+ "promoted",
1920
+ {
1921
+ targetType: targetType,
1922
+ converters: converters,
1923
+ paramRef: firstParamRef
1924
+ },
1925
+ location
1926
+ );
1927
+ }
1928
+
1929
+ /**
1930
+ * Check if the token at offset ahead matches the given type.
1931
+ */
1932
+ checkAhead(tokenType, offset) {
1933
+ const index = this.current + offset;
1934
+ if (index >= this.tokens.length) return false;
1935
+ return this.tokens[index].type === tokenType;
1936
+ }
1937
+
1938
+ /**
1939
+ * Parse generic type parameters: <T, 2>, <Int>, etc.
1940
+ * Handles both type parameters and numeric literals (for Array rank)
1941
+ */
1942
+ parseGenericParams() {
1943
+ const params = [];
1944
+
1945
+ do {
1946
+ // Check for number (like rank in Array<Float, 2>)
1947
+ if (this.check(TokenType.NUMBER)) {
1948
+ params.push(this.advance().value);
1949
+ } else {
1950
+ // It's a type
1951
+ params.push(this.parseTypeAnnotation());
1952
+ }
1953
+ } while (this.match(TokenType.COMMA));
1954
+
1955
+ return params;
1956
+ }
1957
+
1958
+ /**
1959
+ * Parse function type: (Int, Int) -> Float
1960
+ */
1961
+ parseFunctionType(location) {
1962
+ this.consume(TokenType.LPAREN, "Expected '(' for function type");
1963
+
1964
+ const paramTypes = [];
1965
+ if (!this.check(TokenType.RPAREN)) {
1966
+ do {
1967
+ paramTypes.push(this.parseTypeAnnotation());
1968
+ } while (this.match(TokenType.COMMA));
1969
+ }
1970
+
1971
+ this.consume(TokenType.RPAREN, "Expected ')' after function parameters");
1972
+ this.consume(TokenType.ARROW, "Expected '->' in function type");
1973
+
1974
+ const returnType = this.parseTypeAnnotation();
1975
+
1976
+ const typeAnnotation = new TypeAnnotation(
1977
+ "function",
1978
+ { paramTypes, returnType },
1979
+ location
1980
+ );
1981
+
1982
+ // Check for optional marker: ?
1983
+ if (this.match(TokenType.QUESTION)) {
1984
+ typeAnnotation.isOptional = true;
1985
+ }
1986
+
1987
+ return typeAnnotation;
1988
+ }
1989
+
1990
+ /**
1991
+ * Parse record type: { x: Int, y: Float }
1992
+ * Or dictionary type: {[string]: num}
1993
+ * Note: Open records ({ x: Int, .. }) are no longer supported
1994
+ */
1995
+ parseRecordType(location) {
1996
+ this.consume(TokenType.LBRACE, "Expected '{' for record type");
1997
+
1998
+ // Check for dictionary type: {[K]: V}
1999
+ if (this.match(TokenType.LBRACKET)) {
2000
+ const keyType = this.parseTypeAnnotation();
2001
+ this.consume(TokenType.RBRACKET, "Expected ']' after key type");
2002
+ this.consume(TokenType.COLON, "Expected ':' after key type");
2003
+ const valueType = this.parseTypeAnnotation();
2004
+ this.consume(TokenType.RBRACE, "Expected '}' after dictionary type");
2005
+
2006
+ const typeAnnotation = new TypeAnnotation(
2007
+ "dictionary",
2008
+ { keyType, valueType },
2009
+ location
2010
+ );
2011
+
2012
+ // Check for optional marker: ?
2013
+ if (this.match(TokenType.QUESTION)) {
2014
+ typeAnnotation.isOptional = true;
2015
+ }
2016
+
2017
+ return typeAnnotation;
2018
+ }
2019
+
2020
+ const fields = [];
2021
+
2022
+ if (!this.check(TokenType.RBRACE)) {
2023
+ do {
2024
+ // Check for deprecated open record marker: ..
2025
+ if (this.check(TokenType.RANGE)) {
2026
+ throw this._parseError(
2027
+ "Open records (.. syntax) are no longer supported. All records must have exact fields.",
2028
+ this.peek()
2029
+ );
2030
+ }
2031
+
2032
+ const fieldName = this.consume(
2033
+ TokenType.IDENTIFIER,
2034
+ "Expected field name"
2035
+ ).value;
2036
+ this.consume(TokenType.COLON, "Expected ':' after field name");
2037
+ const fieldType = this.parseTypeAnnotation();
2038
+
2039
+ fields.push({ name: fieldName, type: fieldType });
2040
+ } while (this.match(TokenType.COMMA));
2041
+ }
2042
+
2043
+ this.consume(TokenType.RBRACE, "Expected '}' after record type");
2044
+
2045
+ const typeAnnotation = new TypeAnnotation(
2046
+ "record",
2047
+ { fields, open: false },
2048
+ location
2049
+ );
2050
+
2051
+ // Check for optional marker: ?
2052
+ if (this.match(TokenType.QUESTION)) {
2053
+ typeAnnotation.isOptional = true;
2054
+ }
2055
+
2056
+ return typeAnnotation;
2057
+ }
2058
+
2059
+ /**
2060
+ * Validate and normalize loop drivers
2061
+ */
2062
+ validateAndNormalizeLoopDrivers(loopExpr) {
2063
+ if (loopExpr.drivers) {
2064
+ const forDrivers = loopExpr.drivers.filter((d) => d instanceof ForDriver);
2065
+ const patterns = forDrivers.map((d) => d.pattern);
2066
+ const seen = new Set();
2067
+
2068
+ for (const pattern of patterns) {
2069
+ if (seen.has(pattern)) {
2070
+ throw this._parseError(
2071
+ `Duplicate iterator variable name '${pattern}'`,
2072
+ { line: loopExpr.location.line, column: loopExpr.location.column }
2073
+ );
2074
+ }
2075
+ seen.add(pattern);
2076
+ }
2077
+
2078
+ if (loopExpr.variables) {
2079
+ const stateVarNames = new Set(loopExpr.variables.map((v) => v.name));
2080
+
2081
+ for (const driver of forDrivers) {
2082
+ if (stateVarNames.has(driver.pattern)) {
2083
+ throw this._parseError(
2084
+ `Iterator variable '${driver.pattern}' conflicts with loop state variable`,
2085
+ { line: loopExpr.location.line, column: loopExpr.location.column }
2086
+ );
2087
+ }
2088
+ }
2089
+ }
2090
+
2091
+ for (const driver of loopExpr.drivers) {
2092
+ if (driver instanceof ForDriver) {
2093
+ if (!driver.iterable) {
2094
+ throw this._parseError(
2095
+ `For driver must have an iterable expression`,
2096
+ { line: driver.location.line, column: driver.location.column }
2097
+ );
2098
+ }
2099
+ } else if (driver instanceof WhileDriver) {
2100
+ if (!driver.condition) {
2101
+ throw this._parseError(
2102
+ `While driver must have a condition expression`,
2103
+ { line: driver.location.line, column: driver.location.column }
2104
+ );
2105
+ }
2106
+ }
2107
+ }
2108
+ }
2109
+
2110
+ return loopExpr;
2111
+ }
2112
+
2113
+ // Helper methods
2114
+
2115
+ match(...types) {
2116
+ for (const type of types) {
2117
+ if (this.check(type)) {
2118
+ this.advance();
2119
+ return true;
2120
+ }
2121
+ }
2122
+ return false;
2123
+ }
2124
+
2125
+ check(type) {
2126
+ if (this.isAtEnd()) return false;
2127
+ return this.peek().type === type;
2128
+ }
2129
+
2130
+ /**
2131
+ * Match a contextual keyword (an identifier with a specific value).
2132
+ * Used for 'as' which is not a reserved keyword but acts as one in alias positions.
2133
+ */
2134
+ matchContextualKeyword(value) {
2135
+ if (this.check(TokenType.IDENTIFIER) && this.peek().value === value) {
2136
+ this.advance();
2137
+ return true;
2138
+ }
2139
+ return false;
2140
+ }
2141
+
2142
+ checkAhead(type, offset) {
2143
+ const pos = this.pos + offset;
2144
+ if (pos >= this.tokens.length) return false;
2145
+ return this.tokens[pos].type === type;
2146
+ }
2147
+
2148
+ consume(type, message) {
2149
+ if (this.check(type)) return this.advance();
2150
+
2151
+ const token = this.peek();
2152
+ throw this._parseError(`${message}, got ${token.type}`, token);
2153
+ }
2154
+
2155
+ advance() {
2156
+ if (!this.isAtEnd()) this.pos++;
2157
+ return this.previous();
2158
+ }
2159
+
2160
+ isAtEnd() {
2161
+ return this.peek().type === TokenType.EOF;
2162
+ }
2163
+
2164
+ peek() {
2165
+ return this.tokens[this.pos];
2166
+ }
2167
+
2168
+ previous() {
2169
+ return this.tokens[this.pos - 1];
2170
+ }
2171
+
2172
+ createLocation() {
2173
+ const token = this.peek();
2174
+ return createLocation(token.line, token.column, this.filename);
2175
+ }
2176
+ }
2177
+
2178
+ /**
2179
+ * Build AST from source code
2180
+ */
2181
+ export function buildASTFromSource(source, filename = "<stdin>") {
2182
+ const lexer = new Lexer(source, filename);
2183
+ const tokens = lexer.tokenize();
2184
+ const parser = new Parser(tokens, filename);
2185
+ return parser.parse();
2186
+ }
2187
+
2188
+ /**
2189
+ * Build AST from tokens
2190
+ */
2191
+ export function buildASTFromTokens(tokens, filename = "<stdin>") {
2192
+ const parser = new Parser(tokens, filename);
2193
+ return parser.parse();
2194
+ }
2195
+
2196
+ export const parse = buildASTFromSource;