@starpak/lang 0.0.0-stage → 0.1.2

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