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.
- package/README.md +52 -0
- package/StoneEngine.js +879 -0
- package/StoneEngineService.js +1727 -0
- package/adapters/FileSystemAdapter.js +230 -0
- package/adapters/OutputAdapter.js +208 -0
- package/adapters/index.js +6 -0
- package/cli/CLIOutputAdapter.js +196 -0
- package/cli/DaemonClient.js +349 -0
- package/cli/JSONOutputAdapter.js +135 -0
- package/cli/ReplSession.js +567 -0
- package/cli/ViewerServer.js +590 -0
- package/cli/commands/check.js +84 -0
- package/cli/commands/daemon.js +189 -0
- package/cli/commands/kill.js +66 -0
- package/cli/commands/package.js +713 -0
- package/cli/commands/ps.js +65 -0
- package/cli/commands/run.js +537 -0
- package/cli/entry.js +169 -0
- package/cli/index.js +14 -0
- package/cli/stonec.js +358 -0
- package/cli/test-compiler.js +181 -0
- package/cli/viewer/index.html +495 -0
- package/daemon/IPCServer.js +455 -0
- package/daemon/ProcessManager.js +327 -0
- package/daemon/ProcessRunner.js +307 -0
- package/daemon/daemon.js +398 -0
- package/daemon/index.js +16 -0
- package/frontend/analysis/index.js +5 -0
- package/frontend/analysis/livenessAnalyzer.js +568 -0
- package/frontend/analysis/treeShaker.js +265 -0
- package/frontend/index.js +20 -0
- package/frontend/parsing/astBuilder.js +2196 -0
- package/frontend/parsing/index.js +7 -0
- package/frontend/parsing/sonParser.js +592 -0
- package/frontend/parsing/stoneAstTypes.js +703 -0
- package/frontend/parsing/terminal-registry.js +435 -0
- package/frontend/parsing/tokenizer.js +692 -0
- package/frontend/type-checker/OverloadedFunctionType.js +43 -0
- package/frontend/type-checker/TypeEnvironment.js +165 -0
- package/frontend/type-checker/bidirectionalInference.js +149 -0
- package/frontend/type-checker/index.js +10 -0
- package/frontend/type-checker/moduleAnalysis.js +248 -0
- package/frontend/type-checker/operatorMappings.js +35 -0
- package/frontend/type-checker/overloadResolution.js +605 -0
- package/frontend/type-checker/typeChecker.js +452 -0
- package/frontend/type-checker/typeCompatibility.js +389 -0
- package/frontend/type-checker/visitors/controlFlow.js +483 -0
- package/frontend/type-checker/visitors/functions.js +604 -0
- package/frontend/type-checker/visitors/index.js +38 -0
- package/frontend/type-checker/visitors/literals.js +341 -0
- package/frontend/type-checker/visitors/modules.js +159 -0
- package/frontend/type-checker/visitors/operators.js +109 -0
- package/frontend/type-checker/visitors/statements.js +768 -0
- package/frontend/types/index.js +5 -0
- package/frontend/types/operatorMap.js +134 -0
- package/frontend/types/types.js +2046 -0
- package/frontend/utils/errorCollector.js +244 -0
- package/frontend/utils/index.js +5 -0
- package/frontend/utils/moduleResolver.js +479 -0
- package/package.json +50 -0
- package/packages/browserCache.js +359 -0
- package/packages/fetcher.js +236 -0
- package/packages/index.js +130 -0
- package/packages/lockfile.js +271 -0
- package/packages/manifest.js +291 -0
- package/packages/packageResolver.js +356 -0
- package/packages/resolver.js +310 -0
- 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;
|