@starpak/lang 0.0.0-stage → 0.1.3
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/LICENSE +22 -0
- package/lib/ast.d.ts +341 -0
- package/lib/ast.d.ts.map +1 -0
- package/lib/ast.js +9 -0
- package/lib/ast.js.map +1 -0
- package/lib/checker.d.ts +17 -0
- package/lib/checker.d.ts.map +1 -0
- package/lib/checker.js +525 -0
- package/lib/checker.js.map +1 -0
- package/lib/diagnostics.d.ts +46 -0
- package/lib/diagnostics.d.ts.map +1 -0
- package/lib/diagnostics.js +77 -0
- package/lib/diagnostics.js.map +1 -0
- package/lib/emit/lua.d.ts +19 -0
- package/lib/emit/lua.d.ts.map +1 -0
- package/lib/emit/lua.js +451 -0
- package/lib/emit/lua.js.map +1 -0
- package/lib/emit/ts.d.ts +42 -0
- package/lib/emit/ts.d.ts.map +1 -0
- package/lib/emit/ts.js +950 -0
- package/lib/emit/ts.js.map +1 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +54 -0
- package/lib/index.js.map +1 -0
- package/lib/lexer.d.ts +10 -0
- package/lib/lexer.d.ts.map +1 -0
- package/lib/lexer.js +282 -0
- package/lib/lexer.js.map +1 -0
- package/lib/parser.d.ts +11 -0
- package/lib/parser.d.ts.map +1 -0
- package/lib/parser.js +1138 -0
- package/lib/parser.js.map +1 -0
- package/lib/token.d.ts +32 -0
- package/lib/token.d.ts.map +1 -0
- package/lib/token.js +18 -0
- package/lib/token.js.map +1 -0
- package/lib/types.d.ts +63 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +177 -0
- package/lib/types.js.map +1 -0
- package/lib/version.d.ts +25 -0
- package/lib/version.d.ts.map +1 -0
- package/lib/version.js +47 -0
- package/lib/version.js.map +1 -0
- package/package.json +10 -3
- package/src/ast.ts +467 -0
- package/src/checker.ts +554 -0
- package/src/diagnostics.ts +100 -0
- package/src/emit/lua.ts +484 -0
- package/src/emit/ts.ts +1024 -0
- package/src/index.ts +24 -0
- package/src/lexer.ts +301 -0
- package/src/parser.ts +1257 -0
- package/src/token.ts +34 -0
- package/src/types.ts +218 -0
- package/src/version.ts +44 -0
- package/README.md +0 -3
package/src/parser.ts
ADDED
|
@@ -0,0 +1,1257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spak Lang - Recursive descent parser
|
|
3
|
+
*
|
|
4
|
+
* Produces an AST (`Program`) from Spak source. The grammar follows
|
|
5
|
+
* docs/syntax/spak-lang.md.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ArrayPattern,
|
|
10
|
+
Assign,
|
|
11
|
+
AwaitExpr,
|
|
12
|
+
Binary,
|
|
13
|
+
Block,
|
|
14
|
+
BreakStmt,
|
|
15
|
+
Call,
|
|
16
|
+
ClassDecl,
|
|
17
|
+
ContinueStmt,
|
|
18
|
+
Decorator,
|
|
19
|
+
ElifBranch,
|
|
20
|
+
Expr,
|
|
21
|
+
ExportDecl,
|
|
22
|
+
ExportDefault,
|
|
23
|
+
FnDecl,
|
|
24
|
+
ForIn,
|
|
25
|
+
ForNumeric,
|
|
26
|
+
FunctionExpr,
|
|
27
|
+
Ident,
|
|
28
|
+
IfStmt,
|
|
29
|
+
ImportClause,
|
|
30
|
+
ImportDecl,
|
|
31
|
+
ImportForm,
|
|
32
|
+
Index,
|
|
33
|
+
MatchArm,
|
|
34
|
+
MatchStmt,
|
|
35
|
+
Member,
|
|
36
|
+
Param,
|
|
37
|
+
Pattern,
|
|
38
|
+
Position,
|
|
39
|
+
Program,
|
|
40
|
+
Range,
|
|
41
|
+
RepeatStmt,
|
|
42
|
+
ReturnStmt,
|
|
43
|
+
SpawnStmt,
|
|
44
|
+
Spread,
|
|
45
|
+
Statement,
|
|
46
|
+
StringLit,
|
|
47
|
+
Table,
|
|
48
|
+
TableField,
|
|
49
|
+
TablePattern,
|
|
50
|
+
TablePatternEntry,
|
|
51
|
+
ThrowStmt,
|
|
52
|
+
TryStmt,
|
|
53
|
+
TypeAliasDecl,
|
|
54
|
+
TypeAnn,
|
|
55
|
+
UnlessStmt,
|
|
56
|
+
VarDecl,
|
|
57
|
+
VarName,
|
|
58
|
+
WhileStmt,
|
|
59
|
+
} from './ast'
|
|
60
|
+
import { tokenize } from './lexer'
|
|
61
|
+
import { Token, TokenType } from './token'
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Binary operator precedence, low to high. `^` is right-associative.
|
|
65
|
+
*/
|
|
66
|
+
const BINARY_PRECEDENCE: Record<string, number> = {
|
|
67
|
+
'or': 1,
|
|
68
|
+
'and': 2,
|
|
69
|
+
'==': 3,
|
|
70
|
+
'~=': 3,
|
|
71
|
+
'<': 3,
|
|
72
|
+
'<=': 3,
|
|
73
|
+
'>': 3,
|
|
74
|
+
'>=': 3,
|
|
75
|
+
'|>': 4,
|
|
76
|
+
'??': 5,
|
|
77
|
+
'..': 6,
|
|
78
|
+
'+': 7,
|
|
79
|
+
'-': 7,
|
|
80
|
+
'*': 8,
|
|
81
|
+
'/': 8,
|
|
82
|
+
'//': 8,
|
|
83
|
+
'%': 8,
|
|
84
|
+
'^': 10,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const UNARY_PRECEDENCE = 9
|
|
88
|
+
|
|
89
|
+
const BLOCK_END_KEYWORDS = [
|
|
90
|
+
'end', 'else', 'elseif', 'when', 'until', 'catch', 'finally',
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
const PRIMITIVE_TYPE_NAMES = new Set([
|
|
94
|
+
'any', 'nil', 'number', 'string', 'boolean', 'table', 'function',
|
|
95
|
+
])
|
|
96
|
+
|
|
97
|
+
/** Statement kinds that may carry a `---` doc comment. */
|
|
98
|
+
const DOC_STATEMENTS = new Set([
|
|
99
|
+
'FnDecl', 'ClassDecl', 'VarDecl', 'TypeAliasDecl', 'ExportDecl', 'ImportDecl',
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
function isNameToken(token: Token): boolean {
|
|
103
|
+
return token.type === TokenType.Identifier || token.type === TokenType.Keyword
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class Parser {
|
|
107
|
+
private tokens: Token[]
|
|
108
|
+
private pos = 0
|
|
109
|
+
|
|
110
|
+
constructor(source: string) {
|
|
111
|
+
this.tokens = tokenize(source)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* ---------------------------------------------------------------- */
|
|
115
|
+
/* Token helpers */
|
|
116
|
+
/* ---------------------------------------------------------------- */
|
|
117
|
+
|
|
118
|
+
private peek(offset = 0): Token {
|
|
119
|
+
const index = this.pos + offset
|
|
120
|
+
return this.tokens[index < this.tokens.length ? index : this.tokens.length - 1]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private next(): Token {
|
|
124
|
+
const token = this.peek()
|
|
125
|
+
if (this.pos < this.tokens.length - 1) this.pos++
|
|
126
|
+
return token
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private at(type: TokenType, value?: string): boolean {
|
|
130
|
+
const token = this.peek()
|
|
131
|
+
return token.type === type && (value === undefined || token.value === value)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private atKeyword(value: string): boolean {
|
|
135
|
+
return this.at(TokenType.Keyword, value)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private atOperator(value: string): boolean {
|
|
139
|
+
return this.at(TokenType.Operator, value)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private match(type: TokenType, value?: string): boolean {
|
|
143
|
+
if (!this.at(type, value)) return false
|
|
144
|
+
this.next()
|
|
145
|
+
return true
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private matchKeyword(value: string): boolean {
|
|
149
|
+
return this.match(TokenType.Keyword, value)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private matchOperator(value: string): boolean {
|
|
153
|
+
return this.match(TokenType.Operator, value)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private expect(type: TokenType, value?: string, message?: string): Token {
|
|
157
|
+
if (this.at(type, value)) return this.next()
|
|
158
|
+
const token = this.peek()
|
|
159
|
+
throw this.error(message ?? `Expected ${value ?? type} but got "${token.value}"`, token)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private expectKeyword(value: string): Token {
|
|
163
|
+
return this.expect(TokenType.Keyword, value, `Expected "${value}"`)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private expectName(message: string): Token {
|
|
167
|
+
if (isNameToken(this.peek())) return this.next()
|
|
168
|
+
throw this.error(message)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private error(message: string, token: Token = this.peek()): SyntaxError {
|
|
172
|
+
return new SyntaxError(`${message} at ${token.line}:${token.column}`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private loc(token: Token): Position {
|
|
176
|
+
return { line: token.line, column: token.column }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private skipNewlines(): void {
|
|
180
|
+
while (this.at(TokenType.Newline)) this.next()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private skipSeparators(): void {
|
|
184
|
+
while (this.at(TokenType.Newline) || this.at(TokenType.Semicolon)) this.next()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private isBlockEnd(): boolean {
|
|
188
|
+
const token = this.peek()
|
|
189
|
+
return token.type === TokenType.Keyword && BLOCK_END_KEYWORDS.includes(token.value)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private isStatementEnd(): boolean {
|
|
193
|
+
const token = this.peek()
|
|
194
|
+
return token.type === TokenType.EOF
|
|
195
|
+
|| token.type === TokenType.Newline
|
|
196
|
+
|| token.type === TokenType.Semicolon
|
|
197
|
+
|| this.isBlockEnd()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* ---------------------------------------------------------------- */
|
|
201
|
+
/* Program & statements */
|
|
202
|
+
/* ---------------------------------------------------------------- */
|
|
203
|
+
|
|
204
|
+
parseProgram(): Program {
|
|
205
|
+
const start = this.peek()
|
|
206
|
+
const body: Statement[] = []
|
|
207
|
+
this.skipSeparators()
|
|
208
|
+
while (!this.at(TokenType.EOF)) {
|
|
209
|
+
const before = this.pos
|
|
210
|
+
body.push(this.parseStatement())
|
|
211
|
+
if (this.pos === before) throw this.error('Unexpected token')
|
|
212
|
+
this.skipSeparators()
|
|
213
|
+
}
|
|
214
|
+
return { kind: 'Program', body, loc: this.loc(start) }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private parseStatement(): Statement {
|
|
218
|
+
const doc = this.peek().doc
|
|
219
|
+
const statement = this.parseStatementCore()
|
|
220
|
+
if (doc && DOC_STATEMENTS.has(statement.kind)) {
|
|
221
|
+
;(statement as FnDecl | ClassDecl | VarDecl | TypeAliasDecl | ExportDecl | ImportDecl).doc = doc
|
|
222
|
+
}
|
|
223
|
+
return statement
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private parseStatementCore(): Statement {
|
|
227
|
+
const token = this.peek()
|
|
228
|
+
|
|
229
|
+
if (token.type === TokenType.Operator && token.value === '@') {
|
|
230
|
+
return this.parseDecoratedDeclaration()
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (token.type === TokenType.Keyword) {
|
|
234
|
+
switch (token.value) {
|
|
235
|
+
case 'global':
|
|
236
|
+
return this.parseVarDeclaration('global')
|
|
237
|
+
case 'const':
|
|
238
|
+
return this.parseVarDeclaration('const')
|
|
239
|
+
case 'import':
|
|
240
|
+
return this.parseImport()
|
|
241
|
+
case 'export':
|
|
242
|
+
return this.parseExport()
|
|
243
|
+
case 'type':
|
|
244
|
+
return this.parseTypeAlias()
|
|
245
|
+
case 'fn':
|
|
246
|
+
return this.parseFnDecl([], false, false)
|
|
247
|
+
case 'class':
|
|
248
|
+
return this.parseClassDecl([])
|
|
249
|
+
case 'async':
|
|
250
|
+
if (this.peek(1).type === TokenType.Keyword && this.peek(1).value === 'fn') {
|
|
251
|
+
this.next()
|
|
252
|
+
return this.parseFnDecl([], true, false)
|
|
253
|
+
}
|
|
254
|
+
break
|
|
255
|
+
case 'throw':
|
|
256
|
+
return this.parseThrow()
|
|
257
|
+
case 'spawn':
|
|
258
|
+
return this.parseSpawn()
|
|
259
|
+
case 'if':
|
|
260
|
+
return this.parseIf()
|
|
261
|
+
case 'unless':
|
|
262
|
+
return this.parseUnless()
|
|
263
|
+
case 'while':
|
|
264
|
+
return this.parseWhile()
|
|
265
|
+
case 'repeat':
|
|
266
|
+
return this.parseRepeat()
|
|
267
|
+
case 'for':
|
|
268
|
+
return this.parseFor()
|
|
269
|
+
case 'return':
|
|
270
|
+
return this.parseReturn()
|
|
271
|
+
case 'break': {
|
|
272
|
+
const keyword = this.next()
|
|
273
|
+
return { kind: 'BreakStmt', loc: this.loc(keyword) } as BreakStmt
|
|
274
|
+
}
|
|
275
|
+
case 'continue': {
|
|
276
|
+
const keyword = this.next()
|
|
277
|
+
return { kind: 'ContinueStmt', loc: this.loc(keyword) } as ContinueStmt
|
|
278
|
+
}
|
|
279
|
+
case 'match':
|
|
280
|
+
return this.parseMatch()
|
|
281
|
+
case 'try':
|
|
282
|
+
return this.parseTry()
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return this.parseSimpleStatement()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private parseBlock(terminators: string[]): Block {
|
|
290
|
+
const start = this.peek()
|
|
291
|
+
const body: Statement[] = []
|
|
292
|
+
this.skipSeparators()
|
|
293
|
+
while (!this.at(TokenType.EOF)) {
|
|
294
|
+
const token = this.peek()
|
|
295
|
+
if (token.type === TokenType.Keyword && terminators.includes(token.value)) break
|
|
296
|
+
const before = this.pos
|
|
297
|
+
body.push(this.parseStatement())
|
|
298
|
+
if (this.pos === before) throw this.error('Unexpected token')
|
|
299
|
+
this.skipSeparators()
|
|
300
|
+
}
|
|
301
|
+
return { kind: 'Block', body, loc: this.loc(start) }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private parseVarDeclaration(scope: 'local' | 'global' | 'const'): VarDecl {
|
|
305
|
+
const keyword = this.next()
|
|
306
|
+
const names: VarName[] = []
|
|
307
|
+
do {
|
|
308
|
+
const name = this.expect(TokenType.Identifier, undefined, 'Expected variable name')
|
|
309
|
+
const entry: VarName = { name: name.value }
|
|
310
|
+
if (this.matchOperator(':')) entry.type = this.parseType()
|
|
311
|
+
names.push(entry)
|
|
312
|
+
} while (this.matchOperator(','))
|
|
313
|
+
|
|
314
|
+
const values = this.matchOperator('=') ? this.parseExprList() : []
|
|
315
|
+
return { kind: 'VarDecl', names, values, scope, loc: this.loc(keyword) }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private parseSimpleStatement(): Statement {
|
|
319
|
+
const start = this.peek()
|
|
320
|
+
|
|
321
|
+
// Destructuring assignment: `{ x, y } = p` / `[a, b] = list`.
|
|
322
|
+
if (this.atOperator('{') || this.atOperator('[')) {
|
|
323
|
+
const saved = this.pos
|
|
324
|
+
try {
|
|
325
|
+
const target = this.parsePattern()
|
|
326
|
+
if (this.matchOperator('=')) {
|
|
327
|
+
const values = this.parseExprList()
|
|
328
|
+
const node: Assign = { kind: 'Assign', targets: [target], values, loc: this.loc(start) }
|
|
329
|
+
return node
|
|
330
|
+
}
|
|
331
|
+
} catch {
|
|
332
|
+
// Not a pattern; rewind and parse it as an expression instead.
|
|
333
|
+
}
|
|
334
|
+
this.pos = saved
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const first = this.parseExpression()
|
|
338
|
+
|
|
339
|
+
// Typed declaration without a keyword: x: number = 1
|
|
340
|
+
if (first.kind === 'Ident' && this.atOperator(':')) {
|
|
341
|
+
this.next()
|
|
342
|
+
const type = this.parseType()
|
|
343
|
+
const name: VarName = { name: first.name, type }
|
|
344
|
+
const values = this.matchOperator('=') ? this.parseExprList() : []
|
|
345
|
+
return { kind: 'VarDecl', names: [name], values, scope: 'local', loc: first.loc }
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const targets: Array<Expr | Pattern> = [first]
|
|
349
|
+
while (this.matchOperator(',')) targets.push(this.parseExpression())
|
|
350
|
+
|
|
351
|
+
if (this.matchOperator('=')) {
|
|
352
|
+
const values = this.parseExprList()
|
|
353
|
+
// Assignment is declaration: plain identifiers create local bindings.
|
|
354
|
+
if (targets.every((target) => target.kind === 'Ident')) {
|
|
355
|
+
const names: VarName[] = targets.map((target) => ({ name: (target as Ident).name }))
|
|
356
|
+
return { kind: 'VarDecl', names, values, scope: 'local', loc: this.loc(start) }
|
|
357
|
+
}
|
|
358
|
+
return { kind: 'Assign', targets, values, loc: this.loc(start) } as Assign
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (targets.length > 1) throw this.error('Expected "=" in assignment')
|
|
362
|
+
return { kind: 'ExprStmt', expr: first, loc: this.loc(start) }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private parsePattern(): Pattern {
|
|
366
|
+
if (this.atOperator('{')) return this.parseTablePattern()
|
|
367
|
+
if (this.atOperator('[')) return this.parseArrayPattern()
|
|
368
|
+
throw this.error('Expected destructuring pattern')
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private parseTablePattern(): TablePattern {
|
|
372
|
+
const start = this.expect(TokenType.Operator, '{')
|
|
373
|
+
const entries: TablePatternEntry[] = []
|
|
374
|
+
this.skipNewlines()
|
|
375
|
+
|
|
376
|
+
while (!this.atOperator('}')) {
|
|
377
|
+
const key = this.expectName('Expected field name in table pattern')
|
|
378
|
+
const entry: TablePatternEntry = { key: key.value }
|
|
379
|
+
if (this.matchOperator('=')) {
|
|
380
|
+
const value = this.parseExpression()
|
|
381
|
+
// `{ a = first }` renames, any other value is a default.
|
|
382
|
+
if (value.kind === 'Ident') entry.target = (value as Ident).name
|
|
383
|
+
else entry.default = value
|
|
384
|
+
}
|
|
385
|
+
entries.push(entry)
|
|
386
|
+
this.skipNewlines()
|
|
387
|
+
if (!this.matchOperator(',')) break
|
|
388
|
+
this.skipNewlines()
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
this.expect(TokenType.Operator, '}', 'Expected "}"')
|
|
392
|
+
return { kind: 'TablePattern', entries, loc: this.loc(start) }
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private parseArrayPattern(): ArrayPattern {
|
|
396
|
+
const start = this.expect(TokenType.Operator, '[')
|
|
397
|
+
const elements: string[] = []
|
|
398
|
+
this.skipNewlines()
|
|
399
|
+
|
|
400
|
+
while (!this.atOperator(']')) {
|
|
401
|
+
const name = this.expectName('Expected name in array pattern')
|
|
402
|
+
elements.push(name.value)
|
|
403
|
+
this.skipNewlines()
|
|
404
|
+
if (!this.matchOperator(',')) break
|
|
405
|
+
this.skipNewlines()
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
this.expect(TokenType.Operator, ']', 'Expected "]"')
|
|
409
|
+
return { kind: 'ArrayPattern', elements, loc: this.loc(start) }
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private parseFnDecl(decorators: Decorator[], isAsync: boolean, isMethod: boolean): FnDecl {
|
|
413
|
+
const start = this.expectKeyword('fn')
|
|
414
|
+
let name = this.expect(TokenType.Identifier, undefined, 'Expected function name').value
|
|
415
|
+
let receiver: string | undefined
|
|
416
|
+
if (this.matchOperator(':')) {
|
|
417
|
+
receiver = name
|
|
418
|
+
name = this.expect(TokenType.Identifier, undefined, 'Expected method name').value
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const params = this.parseParams()
|
|
422
|
+
const returnType = this.matchOperator(':') ? this.parseType() : undefined
|
|
423
|
+
|
|
424
|
+
let body: Block | undefined
|
|
425
|
+
let exprBody: Expr | undefined
|
|
426
|
+
if (this.matchOperator('=')) {
|
|
427
|
+
exprBody = this.parseExpression()
|
|
428
|
+
} else {
|
|
429
|
+
body = this.parseBlock(['end'])
|
|
430
|
+
this.expectKeyword('end')
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const decl: FnDecl = {
|
|
434
|
+
kind: 'FnDecl',
|
|
435
|
+
name,
|
|
436
|
+
params,
|
|
437
|
+
returnType,
|
|
438
|
+
body,
|
|
439
|
+
exprBody,
|
|
440
|
+
isMethod: isMethod || !!receiver,
|
|
441
|
+
decorators,
|
|
442
|
+
loc: this.loc(start),
|
|
443
|
+
}
|
|
444
|
+
if (receiver) decl.receiver = receiver
|
|
445
|
+
if (isAsync) decl.async = true
|
|
446
|
+
return decl
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private parseParams(): Param[] {
|
|
450
|
+
this.expect(TokenType.Operator, '(', 'Expected "("')
|
|
451
|
+
const params: Param[] = []
|
|
452
|
+
this.skipNewlines()
|
|
453
|
+
|
|
454
|
+
while (!this.atOperator(')')) {
|
|
455
|
+
if (this.matchOperator('...')) {
|
|
456
|
+
params.push({ name: '...', rest: true })
|
|
457
|
+
this.skipNewlines()
|
|
458
|
+
break
|
|
459
|
+
}
|
|
460
|
+
const name = this.expect(TokenType.Identifier, undefined, 'Expected parameter name')
|
|
461
|
+
const param: Param = { name: name.value }
|
|
462
|
+
if (this.matchOperator(':')) param.type = this.parseType()
|
|
463
|
+
if (this.matchOperator('=')) param.default = this.parseExpression()
|
|
464
|
+
params.push(param)
|
|
465
|
+
this.skipNewlines()
|
|
466
|
+
if (!this.matchOperator(',')) break
|
|
467
|
+
this.skipNewlines()
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
this.expect(TokenType.Operator, ')', 'Expected ")"')
|
|
471
|
+
return params
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private parseDecorators(): Decorator[] {
|
|
475
|
+
const decorators: Decorator[] = []
|
|
476
|
+
while (this.atOperator('@')) {
|
|
477
|
+
const at = this.next()
|
|
478
|
+
const name = this.expect(TokenType.Identifier, undefined, 'Expected decorator name').value
|
|
479
|
+
const args = this.atOperator('(') ? this.parseArguments() : []
|
|
480
|
+
decorators.push({ kind: 'Decorator', name, args, loc: this.loc(at) })
|
|
481
|
+
this.skipNewlines()
|
|
482
|
+
}
|
|
483
|
+
return decorators
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private parseDecoratedDeclaration(): Statement {
|
|
487
|
+
const decorators = this.parseDecorators()
|
|
488
|
+
if (this.atKeyword('class')) return this.parseClassDecl(decorators)
|
|
489
|
+
if (this.atKeyword('async') && this.peek(1).value === 'fn') {
|
|
490
|
+
this.next()
|
|
491
|
+
return this.parseFnDecl(decorators, true, false)
|
|
492
|
+
}
|
|
493
|
+
if (this.atKeyword('fn')) return this.parseFnDecl(decorators, false, false)
|
|
494
|
+
throw this.error('Expected "fn" or "class" after decorator')
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private parseClassDecl(decorators: Decorator[]): ClassDecl {
|
|
498
|
+
const start = this.expectKeyword('class')
|
|
499
|
+
const name = this.expect(TokenType.Identifier, undefined, 'Expected class name').value
|
|
500
|
+
const superClass = this.matchKeyword('extends')
|
|
501
|
+
? this.expect(TokenType.Identifier, undefined, 'Expected super class name').value
|
|
502
|
+
: undefined
|
|
503
|
+
|
|
504
|
+
const members: Array<FnDecl | VarDecl> = []
|
|
505
|
+
this.skipSeparators()
|
|
506
|
+
while (!this.atKeyword('end') && !this.at(TokenType.EOF)) {
|
|
507
|
+
const doc = this.peek().doc
|
|
508
|
+
let member: FnDecl | VarDecl
|
|
509
|
+
if (this.atOperator('@')) {
|
|
510
|
+
member = this.parseClassMember(this.parseDecorators())
|
|
511
|
+
} else {
|
|
512
|
+
member = this.parseClassMember([])
|
|
513
|
+
}
|
|
514
|
+
if (doc) member.doc = doc
|
|
515
|
+
members.push(member)
|
|
516
|
+
this.skipSeparators()
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
this.expectKeyword('end')
|
|
520
|
+
return { kind: 'ClassDecl', name, superClass, members, decorators, loc: this.loc(start) }
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Parses one class body member, with decorators already consumed. */
|
|
524
|
+
private parseClassMember(decorators: Decorator[]): FnDecl | VarDecl {
|
|
525
|
+
if (this.atKeyword('async') && this.peek(1).value === 'fn') {
|
|
526
|
+
this.next()
|
|
527
|
+
return this.parseFnDecl(decorators, true, true)
|
|
528
|
+
}
|
|
529
|
+
if (this.atKeyword('fn')) return this.parseFnDecl(decorators, false, true)
|
|
530
|
+
return this.parseField(decorators)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private parseField(decorators: Decorator[] = []): VarDecl {
|
|
534
|
+
const name = this.expectName('Expected field name')
|
|
535
|
+
const entry: VarName = { name: name.value }
|
|
536
|
+
if (this.matchOperator(':')) entry.type = this.parseType()
|
|
537
|
+
const values = this.matchOperator('=') ? [this.parseExpression()] : []
|
|
538
|
+
const decl: VarDecl = { kind: 'VarDecl', names: [entry], values, scope: 'local', loc: this.loc(name) }
|
|
539
|
+
if (decorators.length) decl.decorators = decorators
|
|
540
|
+
return decl
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private parseIf(): IfStmt {
|
|
544
|
+
const start = this.expectKeyword('if')
|
|
545
|
+
const cond = this.parseExpression()
|
|
546
|
+
this.expectKeyword('then')
|
|
547
|
+
const then = this.parseBlock(['elseif', 'else', 'end'])
|
|
548
|
+
|
|
549
|
+
const elifs: ElifBranch[] = []
|
|
550
|
+
while (this.matchKeyword('elseif')) {
|
|
551
|
+
const elifCond = this.parseExpression()
|
|
552
|
+
this.expectKeyword('then')
|
|
553
|
+
const body = this.parseBlock(['elseif', 'else', 'end'])
|
|
554
|
+
elifs.push({ cond: elifCond, body })
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const elseBody = this.matchKeyword('else') ? this.parseBlock(['end']) : undefined
|
|
558
|
+
this.expectKeyword('end')
|
|
559
|
+
return { kind: 'IfStmt', cond, then, elifs, else: elseBody, loc: this.loc(start) }
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private parseUnless(): UnlessStmt {
|
|
563
|
+
const start = this.expectKeyword('unless')
|
|
564
|
+
const cond = this.parseExpression()
|
|
565
|
+
this.expectKeyword('do')
|
|
566
|
+
const body = this.parseBlock(['end'])
|
|
567
|
+
this.expectKeyword('end')
|
|
568
|
+
return { kind: 'UnlessStmt', cond, body, loc: this.loc(start) }
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private parseWhile(): WhileStmt {
|
|
572
|
+
const start = this.expectKeyword('while')
|
|
573
|
+
const cond = this.parseExpression()
|
|
574
|
+
this.expectKeyword('do')
|
|
575
|
+
const body = this.parseBlock(['end'])
|
|
576
|
+
this.expectKeyword('end')
|
|
577
|
+
return { kind: 'WhileStmt', cond, body, loc: this.loc(start) }
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private parseRepeat(): RepeatStmt {
|
|
581
|
+
const start = this.expectKeyword('repeat')
|
|
582
|
+
const body = this.parseBlock(['until'])
|
|
583
|
+
this.expectKeyword('until')
|
|
584
|
+
const cond = this.parseExpression()
|
|
585
|
+
return { kind: 'RepeatStmt', body, cond, loc: this.loc(start) }
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private parseFor(): Statement {
|
|
589
|
+
const start = this.expectKeyword('for')
|
|
590
|
+
const first = this.expect(TokenType.Identifier, undefined, 'Expected loop variable')
|
|
591
|
+
const vars = [first.value]
|
|
592
|
+
while (this.atOperator(',')) {
|
|
593
|
+
this.next()
|
|
594
|
+
vars.push(this.expect(TokenType.Identifier, undefined, 'Expected loop variable').value)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (this.matchOperator('=')) {
|
|
598
|
+
if (vars.length > 1) throw this.error('Numeric for accepts a single variable')
|
|
599
|
+
const from = this.parseExpression()
|
|
600
|
+
this.expect(TokenType.Operator, ',', 'Expected "," in numeric for')
|
|
601
|
+
const to = this.parseExpression()
|
|
602
|
+
const step = this.matchOperator(',') ? this.parseExpression() : undefined
|
|
603
|
+
this.expectKeyword('do')
|
|
604
|
+
const body = this.parseBlock(['end'])
|
|
605
|
+
this.expectKeyword('end')
|
|
606
|
+
const node: ForNumeric = {
|
|
607
|
+
kind: 'ForNumeric',
|
|
608
|
+
varName: first.value,
|
|
609
|
+
from,
|
|
610
|
+
to,
|
|
611
|
+
step,
|
|
612
|
+
inclusive: true,
|
|
613
|
+
body,
|
|
614
|
+
loc: this.loc(start),
|
|
615
|
+
}
|
|
616
|
+
return node
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
this.expectKeyword('in')
|
|
620
|
+
const iter = this.parseRangeExpr()
|
|
621
|
+
this.expectKeyword('do')
|
|
622
|
+
const body = this.parseBlock(['end'])
|
|
623
|
+
this.expectKeyword('end')
|
|
624
|
+
const node: ForIn = { kind: 'ForIn', vars, iter, body, loc: this.loc(start) }
|
|
625
|
+
return node
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private parseReturn(): ReturnStmt {
|
|
629
|
+
const start = this.expectKeyword('return')
|
|
630
|
+
const values = this.isStatementEnd() ? [] : this.parseExprList()
|
|
631
|
+
return { kind: 'ReturnStmt', values, loc: this.loc(start) }
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private parseThrow(): ThrowStmt {
|
|
635
|
+
const start = this.expectKeyword('throw')
|
|
636
|
+
if (this.isStatementEnd()) throw this.error('Expected expression after "throw"')
|
|
637
|
+
return { kind: 'ThrowStmt', value: this.parseExpression(), loc: this.loc(start) }
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
private parseSpawn(): SpawnStmt {
|
|
641
|
+
const start = this.expectKeyword('spawn')
|
|
642
|
+
if (this.isStatementEnd()) throw this.error('Expected expression after "spawn"')
|
|
643
|
+
return { kind: 'SpawnStmt', value: this.parseExpression(), loc: this.loc(start) }
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private parseMatch(): MatchStmt {
|
|
647
|
+
const start = this.expectKeyword('match')
|
|
648
|
+
const subject = this.parseExpression()
|
|
649
|
+
this.skipSeparators()
|
|
650
|
+
|
|
651
|
+
const arms: MatchArm[] = []
|
|
652
|
+
while (this.atKeyword('when')) {
|
|
653
|
+
const when = this.next()
|
|
654
|
+
const patterns = this.parseExprList()
|
|
655
|
+
const guard = this.matchKeyword('if') ? this.parseExpression() : undefined
|
|
656
|
+
this.expectKeyword('then')
|
|
657
|
+
const body = this.parseBlock(['when', 'else', 'end'])
|
|
658
|
+
arms.push({ patterns, guard, body, loc: this.loc(when) })
|
|
659
|
+
this.skipSeparators()
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const elseBody = this.matchKeyword('else') ? this.parseBlock(['end']) : undefined
|
|
663
|
+
this.expectKeyword('end')
|
|
664
|
+
return { kind: 'MatchStmt', subject, arms, elseBody, loc: this.loc(start) }
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
private parseTry(): TryStmt {
|
|
668
|
+
const start = this.expectKeyword('try')
|
|
669
|
+
const body = this.parseBlock(['catch', 'finally', 'end'])
|
|
670
|
+
|
|
671
|
+
let catchVar: string | undefined
|
|
672
|
+
let catchType: TypeAnn | undefined
|
|
673
|
+
let catchBody: Block | undefined
|
|
674
|
+
if (this.matchKeyword('catch')) {
|
|
675
|
+
if (this.at(TokenType.Identifier)) {
|
|
676
|
+
catchVar = this.next().value
|
|
677
|
+
if (this.matchOperator(':')) catchType = this.parseType()
|
|
678
|
+
}
|
|
679
|
+
catchBody = this.parseBlock(['finally', 'end'])
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const finallyBody = this.matchKeyword('finally') ? this.parseBlock(['end']) : undefined
|
|
683
|
+
this.expectKeyword('end')
|
|
684
|
+
return { kind: 'TryStmt', body, catchVar, catchType, catchBody, finallyBody, loc: this.loc(start) }
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
private parseImport(): ImportDecl {
|
|
688
|
+
const start = this.expectKeyword('import')
|
|
689
|
+
const clauses: ImportClause[] = []
|
|
690
|
+
let source: string
|
|
691
|
+
let form: ImportForm
|
|
692
|
+
|
|
693
|
+
if (this.at(TokenType.String)) {
|
|
694
|
+
source = this.next().value
|
|
695
|
+
form = 'side-effect'
|
|
696
|
+
} else if (this.matchOperator('{')) {
|
|
697
|
+
this.skipNewlines()
|
|
698
|
+
while (!this.atOperator('}')) {
|
|
699
|
+
const name = this.expect(TokenType.Identifier, undefined, 'Expected import name')
|
|
700
|
+
const clause: ImportClause = { name: name.value }
|
|
701
|
+
if (this.matchKeyword('as')) {
|
|
702
|
+
clause.alias = this.expect(TokenType.Identifier, undefined, 'Expected alias').value
|
|
703
|
+
}
|
|
704
|
+
clauses.push(clause)
|
|
705
|
+
this.skipNewlines()
|
|
706
|
+
if (!this.matchOperator(',')) break
|
|
707
|
+
this.skipNewlines()
|
|
708
|
+
}
|
|
709
|
+
this.expect(TokenType.Operator, '}', 'Expected "}"')
|
|
710
|
+
this.expectKeyword('from')
|
|
711
|
+
source = this.expect(TokenType.String, undefined, 'Expected module path').value
|
|
712
|
+
form = 'named'
|
|
713
|
+
} else if (this.matchOperator('*')) {
|
|
714
|
+
this.expectKeyword('as')
|
|
715
|
+
const alias = this.expect(TokenType.Identifier, undefined, 'Expected namespace alias')
|
|
716
|
+
clauses.push({ name: '*', alias: alias.value })
|
|
717
|
+
this.expectKeyword('from')
|
|
718
|
+
source = this.expect(TokenType.String, undefined, 'Expected module path').value
|
|
719
|
+
form = 'namespace'
|
|
720
|
+
} else {
|
|
721
|
+
const name = this.expect(TokenType.Identifier, undefined, 'Expected import name')
|
|
722
|
+
const clause: ImportClause = { name: name.value }
|
|
723
|
+
if (this.matchKeyword('as')) {
|
|
724
|
+
clause.alias = this.expect(TokenType.Identifier, undefined, 'Expected alias').value
|
|
725
|
+
}
|
|
726
|
+
clauses.push(clause)
|
|
727
|
+
this.expectKeyword('from')
|
|
728
|
+
source = this.expect(TokenType.String, undefined, 'Expected module path').value
|
|
729
|
+
form = 'default'
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return { kind: 'ImportDecl', clauses, source, form, loc: this.loc(start) }
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
private parseExport(): Statement {
|
|
736
|
+
const start = this.expectKeyword('export')
|
|
737
|
+
if (this.matchKeyword('default')) {
|
|
738
|
+
const expr = this.parseExpression()
|
|
739
|
+
const node: ExportDefault = { kind: 'ExportDefault', expr, loc: this.loc(start) }
|
|
740
|
+
return node
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
let declaration: Statement
|
|
744
|
+
if (this.atKeyword('const')) declaration = this.parseVarDeclaration('const')
|
|
745
|
+
else if (this.atKeyword('global')) declaration = this.parseVarDeclaration('global')
|
|
746
|
+
else if (this.atKeyword('fn')) declaration = this.parseFnDecl([], false, false)
|
|
747
|
+
else if (this.atKeyword('class')) declaration = this.parseClassDecl([])
|
|
748
|
+
else if (this.atKeyword('type')) declaration = this.parseTypeAlias()
|
|
749
|
+
else if (this.atKeyword('async') && this.peek(1).value === 'fn') {
|
|
750
|
+
this.next()
|
|
751
|
+
declaration = this.parseFnDecl([], true, false)
|
|
752
|
+
} else {
|
|
753
|
+
throw this.error('Expected declaration after "export"')
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (declaration.kind === 'VarDecl') declaration.export = true
|
|
757
|
+
const node: ExportDecl = { kind: 'ExportDecl', declaration, loc: this.loc(start) }
|
|
758
|
+
return node
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private parseTypeAlias(): TypeAliasDecl {
|
|
762
|
+
const start = this.expectKeyword('type')
|
|
763
|
+
const name = this.expect(TokenType.Identifier, undefined, 'Expected type name').value
|
|
764
|
+
this.expect(TokenType.Operator, '=', 'Expected "=" in type alias')
|
|
765
|
+
const type = this.parseType()
|
|
766
|
+
return { kind: 'TypeAliasDecl', name, type, loc: this.loc(start) }
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/* ---------------------------------------------------------------- */
|
|
770
|
+
/* Types */
|
|
771
|
+
/* ---------------------------------------------------------------- */
|
|
772
|
+
|
|
773
|
+
private parseType(): TypeAnn {
|
|
774
|
+
const start = this.peek()
|
|
775
|
+
const types = [this.parseTypePostfix()]
|
|
776
|
+
while (this.matchOperator('|')) types.push(this.parseTypePostfix())
|
|
777
|
+
if (types.length === 1) return types[0]
|
|
778
|
+
return { kind: 'union', types, loc: this.loc(start) }
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
private parseTypePostfix(): TypeAnn {
|
|
782
|
+
let type = this.parseTypePrimary()
|
|
783
|
+
while (this.matchOperator('?')) {
|
|
784
|
+
type = { kind: 'optional', type, inner: type, loc: type.loc }
|
|
785
|
+
}
|
|
786
|
+
return type
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
private parseTypePrimary(): TypeAnn {
|
|
790
|
+
const token = this.peek()
|
|
791
|
+
|
|
792
|
+
if (this.matchOperator('(')) {
|
|
793
|
+
const params: TypeAnn[] = []
|
|
794
|
+
this.skipNewlines()
|
|
795
|
+
while (!this.atOperator(')')) {
|
|
796
|
+
params.push(this.parseType())
|
|
797
|
+
this.skipNewlines()
|
|
798
|
+
if (!this.matchOperator(',')) break
|
|
799
|
+
this.skipNewlines()
|
|
800
|
+
}
|
|
801
|
+
this.expect(TokenType.Operator, ')', 'Expected ")"')
|
|
802
|
+
this.expect(TokenType.Operator, '->', 'Expected "->" in function type')
|
|
803
|
+
const returns = this.parseType()
|
|
804
|
+
return { kind: 'fn', params, returns, loc: this.loc(token) }
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (this.matchOperator('[')) {
|
|
808
|
+
const element = this.parseType()
|
|
809
|
+
this.expect(TokenType.Operator, ']', 'Expected "]"')
|
|
810
|
+
return { kind: 'array', element, loc: this.loc(token) }
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (this.matchOperator('{')) {
|
|
814
|
+
this.skipNewlines()
|
|
815
|
+
const entries: Array<{ name: string; type: TypeAnn }> = []
|
|
816
|
+
while (!this.atOperator('}')) {
|
|
817
|
+
const member = this.expect(TokenType.Identifier, undefined, 'Expected type member name')
|
|
818
|
+
this.expect(TokenType.Operator, ':', 'Expected ":" in type')
|
|
819
|
+
const type = this.parseType()
|
|
820
|
+
entries.push({ name: member.value, type })
|
|
821
|
+
this.skipNewlines()
|
|
822
|
+
if (!this.matchOperator(',')) break
|
|
823
|
+
this.skipNewlines()
|
|
824
|
+
}
|
|
825
|
+
this.expect(TokenType.Operator, '}', 'Expected "}"')
|
|
826
|
+
|
|
827
|
+
if (entries.length === 1 && PRIMITIVE_TYPE_NAMES.has(entries[0].name)) {
|
|
828
|
+
return {
|
|
829
|
+
kind: 'dict',
|
|
830
|
+
key: { kind: 'named', name: entries[0].name, loc: this.loc(token) },
|
|
831
|
+
value: entries[0].type,
|
|
832
|
+
loc: this.loc(token),
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return { kind: 'record', fields: entries, loc: this.loc(token) }
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (token.type === TokenType.Identifier || this.atKeyword('nil')) {
|
|
839
|
+
this.next()
|
|
840
|
+
return { kind: 'named', name: token.value, loc: this.loc(token) }
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
throw this.error('Expected type annotation')
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/* ---------------------------------------------------------------- */
|
|
847
|
+
/* Expressions */
|
|
848
|
+
/* ---------------------------------------------------------------- */
|
|
849
|
+
|
|
850
|
+
private parseExprList(): Expr[] {
|
|
851
|
+
const list = [this.parseExpression()]
|
|
852
|
+
while (this.matchOperator(',')) list.push(this.parseExpression())
|
|
853
|
+
return list
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
private parseExpression(minPrecedence = 1): Expr {
|
|
857
|
+
let left = this.parseUnary()
|
|
858
|
+
|
|
859
|
+
while (true) {
|
|
860
|
+
const token = this.peek()
|
|
861
|
+
const op = token.value
|
|
862
|
+
const precedence = BINARY_PRECEDENCE[op]
|
|
863
|
+
if (precedence === undefined || precedence < minPrecedence) break
|
|
864
|
+
|
|
865
|
+
const isOperator = token.type === TokenType.Operator
|
|
866
|
+
|| (token.type === TokenType.Keyword && (op === 'and' || op === 'or'))
|
|
867
|
+
if (!isOperator) break
|
|
868
|
+
|
|
869
|
+
this.next()
|
|
870
|
+
const right = this.parseExpression(op === '^' ? precedence : precedence + 1)
|
|
871
|
+
const binary: Binary = { kind: 'Binary', op, left, right, loc: left.loc }
|
|
872
|
+
left = binary
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return left
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
private parseUnary(): Expr {
|
|
879
|
+
const token = this.peek()
|
|
880
|
+
if (token.type === TokenType.Keyword && token.value === 'not') {
|
|
881
|
+
this.next()
|
|
882
|
+
return {
|
|
883
|
+
kind: 'Unary',
|
|
884
|
+
op: 'not',
|
|
885
|
+
argument: this.parseExpression(UNARY_PRECEDENCE),
|
|
886
|
+
loc: this.loc(token),
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (token.type === TokenType.Keyword && token.value === 'await') {
|
|
890
|
+
this.next()
|
|
891
|
+
const node: AwaitExpr = {
|
|
892
|
+
kind: 'AwaitExpr',
|
|
893
|
+
argument: this.parseExpression(UNARY_PRECEDENCE),
|
|
894
|
+
loc: this.loc(token),
|
|
895
|
+
}
|
|
896
|
+
return node
|
|
897
|
+
}
|
|
898
|
+
if (token.type === TokenType.Operator && (token.value === '-' || token.value === '#')) {
|
|
899
|
+
this.next()
|
|
900
|
+
return {
|
|
901
|
+
kind: 'Unary',
|
|
902
|
+
op: token.value,
|
|
903
|
+
argument: this.parseExpression(UNARY_PRECEDENCE),
|
|
904
|
+
loc: this.loc(token),
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return this.parsePostfix()
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
private parsePostfix(): Expr {
|
|
911
|
+
let expr = this.parsePrimary()
|
|
912
|
+
|
|
913
|
+
while (true) {
|
|
914
|
+
const token = this.peek()
|
|
915
|
+
|
|
916
|
+
if (token.type === TokenType.Operator && token.value === '(') {
|
|
917
|
+
const args = this.parseArguments()
|
|
918
|
+
const call: Call = { kind: 'Call', callee: expr, args, loc: expr.loc }
|
|
919
|
+
expr = call
|
|
920
|
+
continue
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (token.type === TokenType.Operator && (token.value === '.' || token.value === '?.')) {
|
|
924
|
+
this.next()
|
|
925
|
+
const property = this.expectName('Expected property name')
|
|
926
|
+
const member: Member = {
|
|
927
|
+
kind: 'Member',
|
|
928
|
+
object: expr,
|
|
929
|
+
property: property.value,
|
|
930
|
+
loc: expr.loc,
|
|
931
|
+
}
|
|
932
|
+
if (token.value === '?.') member.optional = true
|
|
933
|
+
expr = member
|
|
934
|
+
continue
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (token.type === TokenType.Operator && token.value === '[') {
|
|
938
|
+
this.next()
|
|
939
|
+
this.skipNewlines()
|
|
940
|
+
const index = this.parseRangeExpr()
|
|
941
|
+
this.skipNewlines()
|
|
942
|
+
this.expect(TokenType.Operator, ']', 'Expected "]"')
|
|
943
|
+
const node: Index = { kind: 'Index', object: expr, index, loc: expr.loc }
|
|
944
|
+
if (index.kind === 'Range') node.isRange = true
|
|
945
|
+
expr = node
|
|
946
|
+
continue
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Method call: a:b(args). A bare `:` is a type annotation instead.
|
|
950
|
+
if (
|
|
951
|
+
token.type === TokenType.Operator && token.value === ':'
|
|
952
|
+
&& isNameToken(this.peek(1))
|
|
953
|
+
&& this.peek(2).type === TokenType.Operator && this.peek(2).value === '('
|
|
954
|
+
) {
|
|
955
|
+
this.next()
|
|
956
|
+
const property = this.next()
|
|
957
|
+
const member: Member = {
|
|
958
|
+
kind: 'Member',
|
|
959
|
+
object: expr,
|
|
960
|
+
property: property.value,
|
|
961
|
+
loc: expr.loc,
|
|
962
|
+
}
|
|
963
|
+
const args = this.parseArguments()
|
|
964
|
+
const call: Call = { kind: 'Call', callee: member, args, isMethod: true, loc: expr.loc }
|
|
965
|
+
expr = call
|
|
966
|
+
continue
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
break
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
return expr
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
private parseArguments(): Expr[] {
|
|
976
|
+
this.expect(TokenType.Operator, '(', 'Expected "("')
|
|
977
|
+
const args: Expr[] = []
|
|
978
|
+
this.skipNewlines()
|
|
979
|
+
while (!this.atOperator(')')) {
|
|
980
|
+
if (this.atOperator('...')) {
|
|
981
|
+
const spreadToken = this.next()
|
|
982
|
+
const spread: Spread = { kind: 'Spread', loc: this.loc(spreadToken) }
|
|
983
|
+
if (!this.atOperator(')') && !this.atOperator(',')) spread.argument = this.parseExpression()
|
|
984
|
+
args.push(spread)
|
|
985
|
+
} else {
|
|
986
|
+
args.push(this.parseExpression())
|
|
987
|
+
}
|
|
988
|
+
this.skipNewlines()
|
|
989
|
+
if (!this.matchOperator(',')) break
|
|
990
|
+
this.skipNewlines()
|
|
991
|
+
}
|
|
992
|
+
this.expect(TokenType.Operator, ')', 'Expected ")"')
|
|
993
|
+
return args
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
private parsePrimary(): Expr {
|
|
997
|
+
const token = this.peek()
|
|
998
|
+
|
|
999
|
+
switch (token.type) {
|
|
1000
|
+
case TokenType.Number: {
|
|
1001
|
+
this.next()
|
|
1002
|
+
return {
|
|
1003
|
+
kind: 'NumberLit',
|
|
1004
|
+
value: Number(token.value.replace(/_/g, '')),
|
|
1005
|
+
loc: this.loc(token),
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
case TokenType.String: {
|
|
1009
|
+
this.next()
|
|
1010
|
+
const literal: StringLit = { kind: 'StringLit', value: token.value, loc: this.loc(token) }
|
|
1011
|
+
if (token.value.includes('\n')) literal.multiline = true
|
|
1012
|
+
return literal
|
|
1013
|
+
}
|
|
1014
|
+
case TokenType.FString: {
|
|
1015
|
+
this.next()
|
|
1016
|
+
const literal: StringLit = {
|
|
1017
|
+
kind: 'StringLit',
|
|
1018
|
+
value: token.value,
|
|
1019
|
+
fstring: true,
|
|
1020
|
+
parts: parseFStringParts(token),
|
|
1021
|
+
loc: this.loc(token),
|
|
1022
|
+
}
|
|
1023
|
+
if (token.value.includes('\n')) literal.multiline = true
|
|
1024
|
+
return literal
|
|
1025
|
+
}
|
|
1026
|
+
case TokenType.Identifier: {
|
|
1027
|
+
this.next()
|
|
1028
|
+
const ident: Ident = { kind: 'Ident', name: token.value, loc: this.loc(token) }
|
|
1029
|
+
return ident
|
|
1030
|
+
}
|
|
1031
|
+
case TokenType.Keyword:
|
|
1032
|
+
return this.parseKeywordPrimary(token)
|
|
1033
|
+
case TokenType.Operator:
|
|
1034
|
+
if (token.value === '{') return this.parseTable()
|
|
1035
|
+
if (token.value === '(') {
|
|
1036
|
+
this.next()
|
|
1037
|
+
const expr = this.parseExpression()
|
|
1038
|
+
this.expect(TokenType.Operator, ')', 'Expected ")"')
|
|
1039
|
+
return expr
|
|
1040
|
+
}
|
|
1041
|
+
break
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
throw this.error(`Unexpected token "${token.value}"`)
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
private parseKeywordPrimary(token: Token): Expr {
|
|
1048
|
+
switch (token.value) {
|
|
1049
|
+
case 'true':
|
|
1050
|
+
case 'false':
|
|
1051
|
+
this.next()
|
|
1052
|
+
return { kind: 'BoolLit', value: token.value === 'true', loc: this.loc(token) }
|
|
1053
|
+
case 'nil':
|
|
1054
|
+
this.next()
|
|
1055
|
+
return { kind: 'NilLit', loc: this.loc(token) }
|
|
1056
|
+
case 'self':
|
|
1057
|
+
this.next()
|
|
1058
|
+
return { kind: 'SelfExpr', loc: this.loc(token) }
|
|
1059
|
+
case 'fn':
|
|
1060
|
+
return this.parseFunctionExpr(false)
|
|
1061
|
+
case 'async':
|
|
1062
|
+
if (this.peek(1).type === TokenType.Keyword && this.peek(1).value === 'fn') {
|
|
1063
|
+
this.next()
|
|
1064
|
+
return this.parseFunctionExpr(true)
|
|
1065
|
+
}
|
|
1066
|
+
break
|
|
1067
|
+
}
|
|
1068
|
+
throw this.error(`Unexpected keyword "${token.value}"`)
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
private parseFunctionExpr(isAsync: boolean): FunctionExpr {
|
|
1072
|
+
const start = this.expectKeyword('fn')
|
|
1073
|
+
const params = this.parseParams()
|
|
1074
|
+
const returnType = this.matchOperator(':') ? this.parseType() : undefined
|
|
1075
|
+
|
|
1076
|
+
let body: Block | undefined
|
|
1077
|
+
let exprBody: Expr | undefined
|
|
1078
|
+
if (this.matchOperator('->')) {
|
|
1079
|
+
exprBody = this.parseExpression()
|
|
1080
|
+
} else {
|
|
1081
|
+
body = this.parseBlock(['end'])
|
|
1082
|
+
this.expectKeyword('end')
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const node: FunctionExpr = {
|
|
1086
|
+
kind: 'FunctionExpr',
|
|
1087
|
+
params,
|
|
1088
|
+
returnType,
|
|
1089
|
+
body,
|
|
1090
|
+
exprBody,
|
|
1091
|
+
loc: this.loc(start),
|
|
1092
|
+
}
|
|
1093
|
+
if (isAsync) node.async = true
|
|
1094
|
+
return node
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
private parseTable(): Table {
|
|
1098
|
+
const start = this.expect(TokenType.Operator, '{')
|
|
1099
|
+
const fields: TableField[] = []
|
|
1100
|
+
|
|
1101
|
+
while (true) {
|
|
1102
|
+
this.skipNewlines()
|
|
1103
|
+
if (this.atOperator('}')) break
|
|
1104
|
+
|
|
1105
|
+
if (this.matchOperator('[')) {
|
|
1106
|
+
const key = this.parseExpression()
|
|
1107
|
+
this.expect(TokenType.Operator, ']', 'Expected "]"')
|
|
1108
|
+
this.expect(TokenType.Operator, '=', 'Expected "="')
|
|
1109
|
+
const value = this.parseExpression()
|
|
1110
|
+
fields.push({ key, value })
|
|
1111
|
+
} else if (
|
|
1112
|
+
isNameToken(this.peek())
|
|
1113
|
+
&& this.peek(1).type === TokenType.Operator
|
|
1114
|
+
&& this.peek(1).value === '='
|
|
1115
|
+
) {
|
|
1116
|
+
const key = this.next()
|
|
1117
|
+
this.next()
|
|
1118
|
+
const value = this.parseExpression()
|
|
1119
|
+
const keyNode: Ident = { kind: 'Ident', name: key.value, loc: this.loc(key) }
|
|
1120
|
+
fields.push({ key: keyNode, value })
|
|
1121
|
+
} else if (this.peek().type === TokenType.Identifier && this.isTableFieldEnd(this.peek(1))) {
|
|
1122
|
+
const shorthand = this.next()
|
|
1123
|
+
const ident: Ident = { kind: 'Ident', name: shorthand.value, loc: this.loc(shorthand) }
|
|
1124
|
+
fields.push({ key: ident, value: ident })
|
|
1125
|
+
} else {
|
|
1126
|
+
fields.push({ value: this.parseExpression() })
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
this.skipNewlines()
|
|
1130
|
+
if (this.matchOperator(',') || this.match(TokenType.Semicolon)) continue
|
|
1131
|
+
if (this.atOperator('}')) break
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
this.expect(TokenType.Operator, '}', 'Expected "}"')
|
|
1135
|
+
return { kind: 'Table', fields, loc: this.loc(start) }
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
private isTableFieldEnd(token: Token): boolean {
|
|
1139
|
+
return token.type === TokenType.Newline
|
|
1140
|
+
|| token.type === TokenType.Semicolon
|
|
1141
|
+
|| token.type === TokenType.EOF
|
|
1142
|
+
|| (token.type === TokenType.Operator && (token.value === ',' || token.value === '}'))
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Range is only meaningful in `for ... in` heads and slices, where `..`
|
|
1147
|
+
* must not be treated as string concatenation.
|
|
1148
|
+
*/
|
|
1149
|
+
private parseRangeExpr(): Expr {
|
|
1150
|
+
const start = this.peek()
|
|
1151
|
+
const expr = this.parseExpression()
|
|
1152
|
+
|
|
1153
|
+
if (expr.kind === 'Binary' && expr.op === '..') {
|
|
1154
|
+
const range: Range = {
|
|
1155
|
+
kind: 'Range',
|
|
1156
|
+
from: expr.left,
|
|
1157
|
+
to: expr.right,
|
|
1158
|
+
inclusive: true,
|
|
1159
|
+
loc: expr.loc,
|
|
1160
|
+
}
|
|
1161
|
+
return range
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
if (this.atOperator('..') || this.atOperator('..<')) {
|
|
1165
|
+
const inclusive = this.next().value === '..'
|
|
1166
|
+
const to = this.parseExpression()
|
|
1167
|
+
const range: Range = { kind: 'Range', from: expr, to, inclusive, loc: this.loc(start) }
|
|
1168
|
+
return range
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
return expr
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/** Parses a complete standalone expression (used by f-string interpolation). */
|
|
1175
|
+
parseExpressionOnly(): Expr {
|
|
1176
|
+
const expr = this.parseExpression()
|
|
1177
|
+
if (!this.at(TokenType.EOF)) throw this.error('Unexpected token in expression')
|
|
1178
|
+
return expr
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/** Splits an f-string body into literal text and parsed interpolation expressions. */
|
|
1183
|
+
function parseFStringParts(token: Token): Array<string | Expr> {
|
|
1184
|
+
const raw = token.value
|
|
1185
|
+
const parts: Array<string | Expr> = []
|
|
1186
|
+
let text = ''
|
|
1187
|
+
let index = 0
|
|
1188
|
+
|
|
1189
|
+
while (index < raw.length) {
|
|
1190
|
+
const ch = raw[index]
|
|
1191
|
+
if (ch === '{' && raw[index + 1] === '{') {
|
|
1192
|
+
text += '{'
|
|
1193
|
+
index += 2
|
|
1194
|
+
continue
|
|
1195
|
+
}
|
|
1196
|
+
if (ch === '}' && raw[index + 1] === '}') {
|
|
1197
|
+
text += '}'
|
|
1198
|
+
index += 2
|
|
1199
|
+
continue
|
|
1200
|
+
}
|
|
1201
|
+
if (ch === '{') {
|
|
1202
|
+
const close = findInterpolationEnd(raw, index + 1)
|
|
1203
|
+
if (close < 0) {
|
|
1204
|
+
throw new SyntaxError(
|
|
1205
|
+
`Unterminated f-string interpolation (line ${token.line}, column ${token.column})`,
|
|
1206
|
+
)
|
|
1207
|
+
}
|
|
1208
|
+
if (text) {
|
|
1209
|
+
parts.push(text)
|
|
1210
|
+
text = ''
|
|
1211
|
+
}
|
|
1212
|
+
parts.push(new Parser(raw.slice(index + 1, close)).parseExpressionOnly())
|
|
1213
|
+
index = close + 1
|
|
1214
|
+
continue
|
|
1215
|
+
}
|
|
1216
|
+
text += ch
|
|
1217
|
+
index++
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
if (text) parts.push(text)
|
|
1221
|
+
return parts
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/** Finds the `}` closing an interpolation, skipping nested braces and strings. */
|
|
1225
|
+
function findInterpolationEnd(raw: string, from: number): number {
|
|
1226
|
+
let depth = 1
|
|
1227
|
+
let quote = ''
|
|
1228
|
+
|
|
1229
|
+
for (let i = from; i < raw.length; i++) {
|
|
1230
|
+
const ch = raw[i]
|
|
1231
|
+
if (quote) {
|
|
1232
|
+
if (ch === '\\') i++
|
|
1233
|
+
else if (ch === quote) quote = ''
|
|
1234
|
+
continue
|
|
1235
|
+
}
|
|
1236
|
+
if (ch === '"' || ch === "'") {
|
|
1237
|
+
quote = ch
|
|
1238
|
+
continue
|
|
1239
|
+
}
|
|
1240
|
+
if (ch === '{') depth++
|
|
1241
|
+
else if (ch === '}') {
|
|
1242
|
+
depth--
|
|
1243
|
+
if (depth === 0) return i
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
return -1
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
export function parse(source: string): Program {
|
|
1251
|
+
return new Parser(source).parseProgram()
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/** Parses a single expression; mainly useful for tooling and f-string chunks. */
|
|
1255
|
+
export function parseExpression(source: string): Expr {
|
|
1256
|
+
return new Parser(source).parseExpressionOnly()
|
|
1257
|
+
}
|