septima-lang 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +435 -0
- package/change-log.md +57 -0
- package/dist/tests/parser.spec.d.ts +1 -0
- package/dist/tests/parser.spec.js +75 -0
- package/dist/tests/septima-compile.spec.d.ts +1 -0
- package/dist/tests/septima-compile.spec.js +118 -0
- package/dist/tests/septima.spec.d.ts +1 -0
- package/dist/tests/septima.spec.js +1090 -0
- package/dist/tests/value.spec.d.ts +1 -0
- package/dist/tests/value.spec.js +263 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +1 -1
- package/src/a.js +66 -0
- package/src/ast-node.ts +340 -0
- package/src/extract-message.ts +5 -0
- package/src/fail-me.ts +7 -0
- package/src/find-array-method.ts +124 -0
- package/src/find-string-method.ts +84 -0
- package/src/index.ts +1 -0
- package/src/location.ts +13 -0
- package/src/parser.ts +698 -0
- package/src/result.ts +54 -0
- package/src/runtime.ts +462 -0
- package/src/scanner.ts +136 -0
- package/src/septima.ts +218 -0
- package/src/should-never-happen.ts +4 -0
- package/src/source-code.ts +101 -0
- package/src/stack.ts +18 -0
- package/src/switch-on.ts +4 -0
- package/src/symbol-table.ts +9 -0
- package/src/value.ts +823 -0
- package/tests/parser.spec.ts +81 -0
- package/tests/septima-compile.spec.ts +187 -0
- package/tests/septima.spec.ts +1169 -0
- package/tests/value.spec.ts +291 -0
package/src/parser.ts
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrayLiteralPart,
|
|
3
|
+
AstNode,
|
|
4
|
+
FormalArg,
|
|
5
|
+
Ident,
|
|
6
|
+
Import,
|
|
7
|
+
Let,
|
|
8
|
+
Literal,
|
|
9
|
+
ObjectLiteralPart,
|
|
10
|
+
span,
|
|
11
|
+
TemplatePart,
|
|
12
|
+
Unit,
|
|
13
|
+
} from './ast-node'
|
|
14
|
+
import { Scanner, Token } from './scanner'
|
|
15
|
+
import { switchOn } from './switch-on'
|
|
16
|
+
|
|
17
|
+
export class Parser {
|
|
18
|
+
constructor(private readonly scanner: Scanner) {}
|
|
19
|
+
|
|
20
|
+
private get unitId() {
|
|
21
|
+
return this.scanner.sourceCode.pathFromSourceRoot
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
parse(): Unit {
|
|
25
|
+
const ret = this.unit()
|
|
26
|
+
if (!this.scanner.eof()) {
|
|
27
|
+
throw new Error(`Loitering input ${this.scanner.sourceRef}`)
|
|
28
|
+
}
|
|
29
|
+
return ret
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
unit(): Unit {
|
|
33
|
+
const imports = this.imports()
|
|
34
|
+
const expression = this.expression('TOP_LEVEL')
|
|
35
|
+
return { tag: 'unit', imports, expression, unitId: this.scanner.sourceCode.pathFromSourceRoot }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
imports(): Import[] {
|
|
39
|
+
const ret: Import[] = []
|
|
40
|
+
while (true) {
|
|
41
|
+
const start = this.scanner.consumeIf('import')
|
|
42
|
+
if (!start) {
|
|
43
|
+
return ret
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.scanner.consume('*')
|
|
47
|
+
this.scanner.consume('as')
|
|
48
|
+
const ident = this.identifier()
|
|
49
|
+
this.scanner.consume('from')
|
|
50
|
+
const pathToImportFrom = this.maybePrimitiveLiteral()
|
|
51
|
+
if (pathToImportFrom === undefined) {
|
|
52
|
+
throw new Error(`Expected a literal ${this.scanner.sourceRef}`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const notString = () => {
|
|
56
|
+
throw new Error(`Expected a string literal ${this.scanner.sourceCode.sourceRef(span(pathToImportFrom))}`)
|
|
57
|
+
}
|
|
58
|
+
switchOn(pathToImportFrom.type, {
|
|
59
|
+
bool: notString,
|
|
60
|
+
num: notString,
|
|
61
|
+
undef: notString,
|
|
62
|
+
str: () => {},
|
|
63
|
+
})
|
|
64
|
+
ret.push({ start, ident, pathToImportFrom: pathToImportFrom.t, unitId: this.unitId })
|
|
65
|
+
|
|
66
|
+
this.scanner.consumeIf(';')
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
definitions(kind: 'TOP_LEVEL' | 'NESTED'): Let[] {
|
|
71
|
+
const ret: Let[] = []
|
|
72
|
+
while (true) {
|
|
73
|
+
if (this.scanner.consumeIf(';')) {
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
if (kind === 'NESTED') {
|
|
77
|
+
if (this.scanner.headMatches('export ')) {
|
|
78
|
+
throw new Error(`non-top-level definition cannot be exported ${this.scanner.sourceRef}`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
let start = this.scanner.consumeIf('let ') ?? this.scanner.consumeIf('const ')
|
|
82
|
+
let isExported = false
|
|
83
|
+
if (!start && kind === 'TOP_LEVEL') {
|
|
84
|
+
start = this.scanner.consumeIf('export let ') ?? this.scanner.consumeIf('export const ')
|
|
85
|
+
isExported = true
|
|
86
|
+
}
|
|
87
|
+
if (!start) {
|
|
88
|
+
return ret
|
|
89
|
+
}
|
|
90
|
+
const ident = this.identifier()
|
|
91
|
+
this.scanner.consume('=')
|
|
92
|
+
const value = this.lambda()
|
|
93
|
+
ret.push({ start, ident, value, isExported })
|
|
94
|
+
|
|
95
|
+
if (this.scanner.headMatches(';')) {
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
if (this.scanner.headMatches('let ') || this.scanner.headMatches('const ')) {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (this.scanner.headMatches('export ')) {
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
return ret
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
expression(kind: 'TOP_LEVEL' | 'NESTED' = 'NESTED'): AstNode {
|
|
110
|
+
const definitions = this.definitions(kind)
|
|
111
|
+
if (kind === 'TOP_LEVEL' && this.scanner.eof()) {
|
|
112
|
+
return { tag: 'topLevelExpression', definitions, unitId: this.unitId }
|
|
113
|
+
}
|
|
114
|
+
const throwToken = this.scanner.consumeIf('throw')
|
|
115
|
+
this.scanner.consumeIf('return')
|
|
116
|
+
const computation = this.lambda()
|
|
117
|
+
|
|
118
|
+
if (definitions.length === 0 && !throwToken) {
|
|
119
|
+
return computation
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { tag: 'topLevelExpression', definitions, throwToken, computation, unitId: this.unitId }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
formalArg(): FormalArg {
|
|
126
|
+
const arg = this.identifier()
|
|
127
|
+
let defaultValue: AstNode | undefined = undefined
|
|
128
|
+
|
|
129
|
+
if (!this.scanner.headMatches('=>') && this.scanner.consumeIf('=')) {
|
|
130
|
+
defaultValue = this.expression()
|
|
131
|
+
}
|
|
132
|
+
return { tag: 'formalArg', ident: arg, defaultValue, unitId: this.unitId }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
lambda(): AstNode {
|
|
136
|
+
const start = this.scanner.consumeIf('fun')
|
|
137
|
+
if (!start) {
|
|
138
|
+
return this.arrowFunction()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.scanner.consume('(')
|
|
142
|
+
const args: FormalArg[] = []
|
|
143
|
+
|
|
144
|
+
if (this.scanner.consumeIf(')')) {
|
|
145
|
+
// no formal args
|
|
146
|
+
} else {
|
|
147
|
+
while (true) {
|
|
148
|
+
const arg = this.formalArg()
|
|
149
|
+
args.push(arg)
|
|
150
|
+
if (this.scanner.consumeIf(')')) {
|
|
151
|
+
break
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.scanner.consume(',')
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const body = this.expression()
|
|
159
|
+
return { tag: 'lambda', start, formalArgs: args, body, unitId: this.unitId }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
arrowFunction(): AstNode {
|
|
163
|
+
const unitId = this.unitId
|
|
164
|
+
|
|
165
|
+
if (this.scanner.headMatches('(', ')', '=>')) {
|
|
166
|
+
const start = this.scanner.consume('(')
|
|
167
|
+
this.scanner.consume(')')
|
|
168
|
+
this.scanner.consume('=>')
|
|
169
|
+
const body = this.lambdaBody()
|
|
170
|
+
return { tag: 'lambda', start, formalArgs: [], body, unitId }
|
|
171
|
+
}
|
|
172
|
+
if (this.scanner.headMatches(IDENT_PATTERN, '=>')) {
|
|
173
|
+
const formal = this.formalArg()
|
|
174
|
+
this.scanner.consume('=>')
|
|
175
|
+
const body = this.lambdaBody()
|
|
176
|
+
return { tag: 'lambda', start: formal.ident.t, formalArgs: [formal], body, unitId }
|
|
177
|
+
}
|
|
178
|
+
if (this.scanner.headMatches('(', IDENT_PATTERN, ')', '=>')) {
|
|
179
|
+
const start = this.scanner.consume('(')
|
|
180
|
+
const formal = this.formalArg()
|
|
181
|
+
this.scanner.consume(')')
|
|
182
|
+
this.scanner.consume('=>')
|
|
183
|
+
const body = this.lambdaBody()
|
|
184
|
+
return { tag: 'lambda', start, formalArgs: [formal], body, unitId }
|
|
185
|
+
}
|
|
186
|
+
if (this.scanner.headMatches('(', IDENT_PATTERN, { either: [',', '='], noneOf: ['=='] })) {
|
|
187
|
+
const start = this.scanner.consume('(')
|
|
188
|
+
const formalArgs: FormalArg[] = []
|
|
189
|
+
let defaultSeen = false
|
|
190
|
+
while (true) {
|
|
191
|
+
const pos = this.scanner.sourceRef
|
|
192
|
+
const formal = this.formalArg()
|
|
193
|
+
if (defaultSeen && !formal.defaultValue) {
|
|
194
|
+
throw new Error(`A required parameter cannot follow an optional parameter: ${pos}`)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
defaultSeen = defaultSeen || Boolean(formal.defaultValue)
|
|
198
|
+
formalArgs.push(formal)
|
|
199
|
+
|
|
200
|
+
if (this.scanner.consumeIf(')')) {
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.scanner.consume(',')
|
|
205
|
+
if (this.scanner.consumeIf(')')) {
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
this.scanner.consume('=>')
|
|
211
|
+
const body = this.lambdaBody()
|
|
212
|
+
return { tag: 'lambda', start, formalArgs, body, unitId }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return this.ifExpression()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private lambdaBody() {
|
|
219
|
+
if (this.scanner.consumeIf('{')) {
|
|
220
|
+
const ret = this.expression()
|
|
221
|
+
this.scanner.consume('}')
|
|
222
|
+
return ret
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return this.expression()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
ifExpression(): AstNode {
|
|
229
|
+
if (!this.scanner.consumeIf('if')) {
|
|
230
|
+
return this.ternary()
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this.scanner.consume('(')
|
|
234
|
+
const condition = this.expression()
|
|
235
|
+
this.scanner.consume(')')
|
|
236
|
+
|
|
237
|
+
const positive = this.expression()
|
|
238
|
+
|
|
239
|
+
this.scanner.consume('else')
|
|
240
|
+
|
|
241
|
+
const negative = this.expression()
|
|
242
|
+
|
|
243
|
+
return { tag: 'if', condition, positive, negative, unitId: this.unitId }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
ternary(): AstNode {
|
|
247
|
+
const condition = this.undefinedCoallesing()
|
|
248
|
+
if (this.scanner.headMatches('??')) {
|
|
249
|
+
return condition
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!this.scanner.consumeIf('?')) {
|
|
253
|
+
return condition
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const positive = this.expression()
|
|
257
|
+
this.scanner.consume(':')
|
|
258
|
+
const negative = this.expression()
|
|
259
|
+
|
|
260
|
+
return { tag: 'ternary', condition, positive, negative, unitId: this.unitId }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
undefinedCoallesing(): AstNode {
|
|
264
|
+
const lhs = this.or()
|
|
265
|
+
if (this.scanner.consumeIf('??')) {
|
|
266
|
+
return { tag: 'binaryOperator', operator: '??', lhs, rhs: this.undefinedCoallesing(), unitId: this.unitId }
|
|
267
|
+
}
|
|
268
|
+
return lhs
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
or(): AstNode {
|
|
272
|
+
const lhs = this.and()
|
|
273
|
+
if (this.scanner.consumeIf('||')) {
|
|
274
|
+
return { tag: 'binaryOperator', operator: '||', lhs, rhs: this.or(), unitId: this.unitId }
|
|
275
|
+
}
|
|
276
|
+
return lhs
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
and(): AstNode {
|
|
280
|
+
const lhs = this.equality()
|
|
281
|
+
if (this.scanner.consumeIf('&&')) {
|
|
282
|
+
return { tag: 'binaryOperator', operator: '&&', lhs, rhs: this.and(), unitId: this.unitId }
|
|
283
|
+
}
|
|
284
|
+
return lhs
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
equality(): AstNode {
|
|
288
|
+
const lhs = this.comparison()
|
|
289
|
+
if (this.scanner.consumeIf('==')) {
|
|
290
|
+
return { tag: 'binaryOperator', operator: '==', lhs, rhs: this.equality(), unitId: this.unitId }
|
|
291
|
+
}
|
|
292
|
+
if (this.scanner.consumeIf('!=')) {
|
|
293
|
+
return { tag: 'binaryOperator', operator: '!=', lhs, rhs: this.equality(), unitId: this.unitId }
|
|
294
|
+
}
|
|
295
|
+
return lhs
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
comparison(): AstNode {
|
|
299
|
+
const lhs = this.addition()
|
|
300
|
+
if (this.scanner.consumeIf('>=')) {
|
|
301
|
+
return { tag: 'binaryOperator', operator: '>=', lhs, rhs: this.comparison(), unitId: this.unitId }
|
|
302
|
+
}
|
|
303
|
+
if (this.scanner.consumeIf('<=')) {
|
|
304
|
+
return { tag: 'binaryOperator', operator: '<=', lhs, rhs: this.comparison(), unitId: this.unitId }
|
|
305
|
+
}
|
|
306
|
+
if (this.scanner.consumeIf('>')) {
|
|
307
|
+
return { tag: 'binaryOperator', operator: '>', lhs, rhs: this.comparison(), unitId: this.unitId }
|
|
308
|
+
}
|
|
309
|
+
if (this.scanner.consumeIf('<')) {
|
|
310
|
+
return { tag: 'binaryOperator', operator: '<', lhs, rhs: this.comparison(), unitId: this.unitId }
|
|
311
|
+
}
|
|
312
|
+
return lhs
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
addition(): AstNode {
|
|
316
|
+
const lhs = this.multiplication()
|
|
317
|
+
if (this.scanner.consumeIf('+')) {
|
|
318
|
+
return { tag: 'binaryOperator', operator: '+', lhs, rhs: this.addition(), unitId: this.unitId }
|
|
319
|
+
}
|
|
320
|
+
if (this.scanner.consumeIf('-')) {
|
|
321
|
+
return { tag: 'binaryOperator', operator: '-', lhs, rhs: this.addition(), unitId: this.unitId }
|
|
322
|
+
}
|
|
323
|
+
return lhs
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
multiplication(): AstNode {
|
|
327
|
+
const lhs = this.power()
|
|
328
|
+
if (this.scanner.consumeIf('*')) {
|
|
329
|
+
return { tag: 'binaryOperator', operator: '*', lhs, rhs: this.multiplication(), unitId: this.unitId }
|
|
330
|
+
}
|
|
331
|
+
if (this.scanner.consumeIf('/')) {
|
|
332
|
+
return { tag: 'binaryOperator', operator: '/', lhs, rhs: this.multiplication(), unitId: this.unitId }
|
|
333
|
+
}
|
|
334
|
+
if (this.scanner.consumeIf('%')) {
|
|
335
|
+
return { tag: 'binaryOperator', operator: '%', lhs, rhs: this.multiplication(), unitId: this.unitId }
|
|
336
|
+
}
|
|
337
|
+
return lhs
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
power(): AstNode {
|
|
341
|
+
const lhs = this.unary()
|
|
342
|
+
if (this.scanner.consumeIf('**')) {
|
|
343
|
+
return { tag: 'binaryOperator', operator: '**', lhs, rhs: this.power(), unitId: this.unitId }
|
|
344
|
+
}
|
|
345
|
+
return lhs
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
unary(): AstNode {
|
|
349
|
+
let operatorToken = this.scanner.consumeIf('!')
|
|
350
|
+
if (operatorToken) {
|
|
351
|
+
return { tag: 'unaryOperator', operand: this.unary(), operator: '!', operatorToken, unitId: this.unitId }
|
|
352
|
+
}
|
|
353
|
+
operatorToken = this.scanner.consumeIf('+')
|
|
354
|
+
if (operatorToken) {
|
|
355
|
+
return { tag: 'unaryOperator', operand: this.unary(), operator: '+', operatorToken, unitId: this.unitId }
|
|
356
|
+
}
|
|
357
|
+
operatorToken = this.scanner.consumeIf('-')
|
|
358
|
+
if (operatorToken) {
|
|
359
|
+
return { tag: 'unaryOperator', operand: this.unary(), operator: '-', operatorToken, unitId: this.unitId }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return this.call()
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
call(): AstNode {
|
|
366
|
+
const callee = this.memberAccess()
|
|
367
|
+
|
|
368
|
+
if (!this.scanner.consumeIf('(')) {
|
|
369
|
+
return callee
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const { actualArgs, end } = this.actualArgList()
|
|
373
|
+
return { tag: 'functionCall', actualArgs, callee, end, unitId: this.unitId }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private actualArgList() {
|
|
377
|
+
const actualArgs: AstNode[] = []
|
|
378
|
+
const endEmpty = this.scanner.consumeIf(')')
|
|
379
|
+
if (endEmpty) {
|
|
380
|
+
// no actual args
|
|
381
|
+
return { actualArgs, end: endEmpty }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
while (true) {
|
|
385
|
+
const arg = this.expression()
|
|
386
|
+
actualArgs.push(arg)
|
|
387
|
+
let end = this.scanner.consumeIf(')')
|
|
388
|
+
if (end) {
|
|
389
|
+
return { actualArgs, end }
|
|
390
|
+
}
|
|
391
|
+
this.scanner.consume(',')
|
|
392
|
+
end = this.scanner.consumeIf(')')
|
|
393
|
+
if (end) {
|
|
394
|
+
return { actualArgs, end }
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
memberAccess(): AstNode {
|
|
400
|
+
let ret = this.parenthesized()
|
|
401
|
+
|
|
402
|
+
while (true) {
|
|
403
|
+
if (this.scanner.consumeIf('.')) {
|
|
404
|
+
ret = { tag: 'dot', receiver: ret, ident: this.identifier(), unitId: this.unitId }
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (this.scanner.consumeIf('[')) {
|
|
409
|
+
ret = { tag: 'indexAccess', receiver: ret, index: this.expression(), unitId: this.unitId }
|
|
410
|
+
this.scanner.consume(']')
|
|
411
|
+
continue
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (this.scanner.consumeIf('(')) {
|
|
415
|
+
const { actualArgs, end } = this.actualArgList()
|
|
416
|
+
ret = { tag: 'functionCall', actualArgs, callee: ret, end, unitId: this.unitId }
|
|
417
|
+
continue
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return ret
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
parenthesized(): AstNode {
|
|
425
|
+
if (this.scanner.consumeIf('(')) {
|
|
426
|
+
const ret = this.expression()
|
|
427
|
+
this.scanner.consume(')')
|
|
428
|
+
return ret
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return this.literalOrIdent()
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
literalOrIdent(): AstNode {
|
|
435
|
+
const ret = this.maybeLiteral() ?? this.maybeIdentifier()
|
|
436
|
+
if (!ret) {
|
|
437
|
+
throw new Error(`Unparsable input ${this.scanner.sourceRef}`)
|
|
438
|
+
}
|
|
439
|
+
return ret
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
maybeLiteral(): AstNode | undefined {
|
|
443
|
+
return this.maybePrimitiveLiteral() ?? this.maybeTemplateLiteral() ?? this.maybeCompositeLiteral()
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
maybePrimitiveLiteral(): Literal | undefined {
|
|
447
|
+
let t = this.scanner.consumeIf('undefined')
|
|
448
|
+
if (t) {
|
|
449
|
+
return { tag: 'literal', type: 'undef', t, unitId: this.unitId }
|
|
450
|
+
}
|
|
451
|
+
t = this.scanner.consumeIf('true')
|
|
452
|
+
if (t) {
|
|
453
|
+
return { tag: 'literal', type: 'bool', t, unitId: this.unitId }
|
|
454
|
+
}
|
|
455
|
+
t = this.scanner.consumeIf('false')
|
|
456
|
+
if (t) {
|
|
457
|
+
return { tag: 'literal', type: 'bool', t, unitId: this.unitId }
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
t = this.scanner.consumeIf(/([0-9]*[.])?[0-9]+/)
|
|
461
|
+
if (t) {
|
|
462
|
+
return { tag: 'literal', type: 'num', t, unitId: this.unitId }
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const stringLiteral = this.maybeStringLiteral()
|
|
466
|
+
if (stringLiteral) {
|
|
467
|
+
return stringLiteral
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return undefined
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
maybeStringLiteral(): Literal | undefined {
|
|
474
|
+
// double-quotes-enclosd string
|
|
475
|
+
if (this.scanner.consumeIf(`"`, false)) {
|
|
476
|
+
const t = this.scanner.consume(/[^"]*/)
|
|
477
|
+
this.scanner.consume(`"`)
|
|
478
|
+
return { tag: 'literal', type: 'str', t, unitId: this.unitId }
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// single-quotes-enclosd string
|
|
482
|
+
if (this.scanner.consumeIf(`'`, false)) {
|
|
483
|
+
const t = this.scanner.consume(/[^']*/)
|
|
484
|
+
this.scanner.consume(`'`)
|
|
485
|
+
return { tag: 'literal', type: 'str', t, unitId: this.unitId }
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return undefined
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
maybeTemplateLiteral(): AstNode | undefined {
|
|
492
|
+
const start = this.scanner.consumeIf('`', false)
|
|
493
|
+
if (!start) {
|
|
494
|
+
return undefined
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const parts: TemplatePart[] = []
|
|
498
|
+
let currentString = ''
|
|
499
|
+
|
|
500
|
+
while (true) {
|
|
501
|
+
if (this.scanner.eof()) {
|
|
502
|
+
throw new Error(`Unterminated template literal ${this.scanner.sourceRef}`)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Consume any characters that are not `, $, or \
|
|
506
|
+
const text = this.scanner.consumeIf(/[^`$\\]+/, false)
|
|
507
|
+
if (text) {
|
|
508
|
+
currentString += text.text
|
|
509
|
+
continue
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (this.scanner.consumeIf('\\', false)) {
|
|
513
|
+
// Escape sequence
|
|
514
|
+
if (this.scanner.consumeIf('$', false)) {
|
|
515
|
+
currentString += '$'
|
|
516
|
+
} else if (this.scanner.consumeIf('\\', false)) {
|
|
517
|
+
currentString += '\\'
|
|
518
|
+
} else if (this.scanner.consumeIf('`', false)) {
|
|
519
|
+
currentString += '`'
|
|
520
|
+
} else if (this.scanner.consumeIf('n', false)) {
|
|
521
|
+
currentString += '\n'
|
|
522
|
+
} else if (this.scanner.consumeIf('t', false)) {
|
|
523
|
+
currentString += '\t'
|
|
524
|
+
} else if (this.scanner.consumeIf('r', false)) {
|
|
525
|
+
currentString += '\r'
|
|
526
|
+
} else {
|
|
527
|
+
// Unknown escape - keep both backslash and next char
|
|
528
|
+
const nextChar = this.scanner.consumeIf(/./, false)
|
|
529
|
+
currentString += '\\' + (nextChar?.text ?? '')
|
|
530
|
+
}
|
|
531
|
+
continue
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (this.scanner.consumeIf('${', true)) {
|
|
535
|
+
// Start of interpolation
|
|
536
|
+
if (currentString.length > 0) {
|
|
537
|
+
parts.push({ tag: 'string', value: currentString })
|
|
538
|
+
currentString = ''
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Parse the expression inside ${} - eat whitespace before expression
|
|
542
|
+
const expr = this.expression()
|
|
543
|
+
// Don't eat whitespace after } to preserve it in the template
|
|
544
|
+
this.scanner.consume('}', false)
|
|
545
|
+
|
|
546
|
+
parts.push({ tag: 'expression', expr })
|
|
547
|
+
continue
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (this.scanner.consumeIf('$', false)) {
|
|
551
|
+
// Lone $ not followed by {
|
|
552
|
+
currentString += '$'
|
|
553
|
+
continue
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// End of template literal
|
|
557
|
+
const end = this.scanner.consume('`', true)
|
|
558
|
+
if (currentString.length > 0) {
|
|
559
|
+
parts.push({ tag: 'string', value: currentString })
|
|
560
|
+
}
|
|
561
|
+
return { tag: 'templateLiteral', parts, start, end, unitId: this.unitId }
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
maybeCompositeLiteral(): AstNode | undefined {
|
|
566
|
+
let t = this.scanner.consumeIf('[')
|
|
567
|
+
if (t) {
|
|
568
|
+
return this.arrayBody(t)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
t = this.scanner.consumeIf('{')
|
|
572
|
+
if (t) {
|
|
573
|
+
return this.objectBody(t)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return undefined
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* This method assumes that the caller consumed the opening '[' token. It consumes the array's elements
|
|
581
|
+
* (comma-separated list of expressions) as well as the closing ']' token.
|
|
582
|
+
*/
|
|
583
|
+
arrayBody(start: Token): AstNode {
|
|
584
|
+
const t = this.scanner.consumeIf(']')
|
|
585
|
+
if (t) {
|
|
586
|
+
// an empty array literal
|
|
587
|
+
return { tag: 'arrayLiteral', start, parts: [], end: t, unitId: this.unitId }
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const parts: ArrayLiteralPart[] = []
|
|
591
|
+
while (true) {
|
|
592
|
+
if (this.scanner.consumeIf(',')) {
|
|
593
|
+
const end = this.scanner.consumeIf(']')
|
|
594
|
+
if (end) {
|
|
595
|
+
return { tag: 'arrayLiteral', start, parts, end, unitId: this.unitId }
|
|
596
|
+
}
|
|
597
|
+
continue
|
|
598
|
+
}
|
|
599
|
+
if (this.scanner.consumeIf('...')) {
|
|
600
|
+
parts.push({ tag: 'spread', v: this.expression() })
|
|
601
|
+
} else {
|
|
602
|
+
const exp = this.expression()
|
|
603
|
+
parts.push({ tag: 'element', v: exp })
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
let end = this.scanner.consumeIf(']')
|
|
607
|
+
if (end) {
|
|
608
|
+
return { tag: 'arrayLiteral', start, parts, end, unitId: this.unitId }
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
this.scanner.consume(',')
|
|
612
|
+
end = this.scanner.consumeIf(']')
|
|
613
|
+
if (end) {
|
|
614
|
+
return { tag: 'arrayLiteral', start, parts, end, unitId: this.unitId }
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* This method assumes that the caller consumed the opening '{' token. It consumes the object's attributes
|
|
621
|
+
* (comma-separated list of key:value parirs) as well as the closing '}' token.
|
|
622
|
+
*/
|
|
623
|
+
objectBody(start: Token): AstNode {
|
|
624
|
+
const t = this.scanner.consumeIf('}')
|
|
625
|
+
if (t) {
|
|
626
|
+
// an empty array literal
|
|
627
|
+
return { tag: 'objectLiteral', start, parts: [], end: t, unitId: this.unitId }
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const parts: ObjectLiteralPart[] = []
|
|
631
|
+
|
|
632
|
+
const consumePart = () => {
|
|
633
|
+
if (this.scanner.consumeIf('...')) {
|
|
634
|
+
parts.push({ tag: 'spread', o: this.expression() })
|
|
635
|
+
return
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (this.scanner.consumeIf('[')) {
|
|
639
|
+
const k = this.expression()
|
|
640
|
+
this.scanner.consume(']')
|
|
641
|
+
this.scanner.consume(':')
|
|
642
|
+
const v = this.expression()
|
|
643
|
+
parts.push({ tag: 'computedName', k, v })
|
|
644
|
+
return
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const stringLiteral = this.maybeStringLiteral()
|
|
648
|
+
if (stringLiteral) {
|
|
649
|
+
this.scanner.consume(':')
|
|
650
|
+
const v = this.expression()
|
|
651
|
+
parts.push({ tag: 'quotedString', k: stringLiteral, v })
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const k = this.identifier()
|
|
656
|
+
if (this.scanner.consumeIf(':')) {
|
|
657
|
+
const v = this.expression()
|
|
658
|
+
parts.push({ tag: 'hardName', k, v })
|
|
659
|
+
} else {
|
|
660
|
+
parts.push({ tag: 'hardName', k, v: k })
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
while (true) {
|
|
665
|
+
consumePart()
|
|
666
|
+
let end = this.scanner.consumeIf('}')
|
|
667
|
+
if (end) {
|
|
668
|
+
return { tag: 'objectLiteral', start, parts, end, unitId: this.unitId }
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
this.scanner.consume(',')
|
|
672
|
+
end = this.scanner.consumeIf('}')
|
|
673
|
+
if (end) {
|
|
674
|
+
return { tag: 'objectLiteral', start, parts, end, unitId: this.unitId }
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
private identifier(): Ident {
|
|
680
|
+
const ret = this.maybeIdentifier()
|
|
681
|
+
if (!ret) {
|
|
682
|
+
throw new Error(`Expected an identifier ${this.scanner.sourceRef}`)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return ret
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private maybeIdentifier(): Ident | undefined {
|
|
689
|
+
const t = this.scanner.consumeIf(IDENT_PATTERN)
|
|
690
|
+
if (t) {
|
|
691
|
+
return { tag: 'ident', t, unitId: this.unitId }
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return undefined
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const IDENT_PATTERN = /[a-zA-Z][0-9A-Za-z_]*/
|