@8bitscript/compiler 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/index.mjs +91 -0
- package/package.json +29 -0
- package/src/ast/index.mjs +90 -0
- package/src/checker/index.mjs +294 -0
- package/src/diagnostics/index.mjs +179 -0
- package/src/fold/facts.mjs +209 -0
- package/src/fold/index.mjs +412 -0
- package/src/intellisense/index.mjs +558 -0
- package/src/ir/index.mjs +1422 -0
- package/src/lexer/index.mjs +383 -0
- package/src/linker/hazards.mjs +121 -0
- package/src/linker/index.mjs +1075 -0
- package/src/parser/index.mjs +795 -0
- package/src/resolver/index.mjs +534 -0
- package/src/templates/index.mjs +278 -0
- package/src/types/index.mjs +116 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
// Recursive-descent parser: tokens in, AST out.
|
|
2
|
+
//
|
|
3
|
+
// Two rules shape the whole design.
|
|
4
|
+
//
|
|
5
|
+
// First, it never throws. An editor parses on every keystroke, so half-typed
|
|
6
|
+
// source is the normal input, not an error case. On a syntax error the parser
|
|
7
|
+
// records a diagnostic, synchronises to the next statement boundary, and keeps
|
|
8
|
+
// going — a file with ten mistakes yields ten diagnostics and a partial tree,
|
|
9
|
+
// not one diagnostic and nothing.
|
|
10
|
+
//
|
|
11
|
+
// Second, it parses only what the language has actually specified. `switch` and
|
|
12
|
+
// `case` are lexed as keywords but have no parse rule, so writing one is an
|
|
13
|
+
// honest syntax error rather than a silently accepted guess at syntax nobody
|
|
14
|
+
// has decided on.
|
|
15
|
+
import { Codes, diagnostic } from '../diagnostics/index.mjs';
|
|
16
|
+
import { TokenKind, tokenize } from '../lexer/index.mjs';
|
|
17
|
+
import { NodeType, node } from '../ast/index.mjs';
|
|
18
|
+
|
|
19
|
+
/** Binary operator precedence, loosest first. Mirrors the C/TypeScript table. */
|
|
20
|
+
const BINARY_PRECEDENCE = {
|
|
21
|
+
'||': 1,
|
|
22
|
+
'&&': 2,
|
|
23
|
+
'|': 3,
|
|
24
|
+
'^': 4,
|
|
25
|
+
'&': 5,
|
|
26
|
+
'==': 6, '!=': 6,
|
|
27
|
+
'<': 7, '>': 7, '<=': 7, '>=': 7,
|
|
28
|
+
'<<': 8, '>>': 8,
|
|
29
|
+
'+': 9, '-': 9,
|
|
30
|
+
'*': 10, '/': 10, '%': 10,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const ASSIGNMENT_OPERATORS = new Set([
|
|
34
|
+
'=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/** Keywords that can begin a statement — used to resynchronise after an error. */
|
|
38
|
+
const STATEMENT_START = new Set([
|
|
39
|
+
'let', 'const', 'function', 'export', 'import', 'return',
|
|
40
|
+
'if', 'while', 'for', 'break', 'continue', 'asm6502', 'namespace',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
class Parser {
|
|
44
|
+
constructor(tokens, text, file) {
|
|
45
|
+
// Comments carry no syntax. Dropping them here keeps every rule below free
|
|
46
|
+
// of "skip trivia" noise.
|
|
47
|
+
this.tokens = tokens.filter((t) => t.kind !== TokenKind.Comment);
|
|
48
|
+
this.text = text;
|
|
49
|
+
this.file = file;
|
|
50
|
+
this.pos = 0;
|
|
51
|
+
this.diagnostics = [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---- token access -------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
peek(offset = 0) {
|
|
57
|
+
return this.tokens[this.pos + offset] ?? null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get atEnd() {
|
|
61
|
+
return this.pos >= this.tokens.length;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Offset just past the last token, for spans that run to end of file. */
|
|
65
|
+
get endOffset() {
|
|
66
|
+
const last = this.tokens[this.tokens.length - 1];
|
|
67
|
+
return last ? last.start + last.length : 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
next() {
|
|
71
|
+
return this.tokens[this.pos++] ?? null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
at(text) {
|
|
75
|
+
return this.peek()?.text === text;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
atKeyword(word) {
|
|
79
|
+
const t = this.peek();
|
|
80
|
+
return t?.kind === TokenKind.Keyword && t.text === word;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
eat(text) {
|
|
84
|
+
if (this.at(text)) {
|
|
85
|
+
this.pos += 1;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe(token) {
|
|
92
|
+
if (!token) return 'end of file';
|
|
93
|
+
return `'${token.text}'`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
error(message, token = this.peek()) {
|
|
97
|
+
const start = token ? token.start : this.endOffset;
|
|
98
|
+
const length = token ? token.length : 0;
|
|
99
|
+
this.diagnostics.push(diagnostic(Codes.SYNTAX_ERROR, message, this.file, start, length));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Consume `text` or record what was found instead. */
|
|
103
|
+
expect(text) {
|
|
104
|
+
const token = this.peek();
|
|
105
|
+
if (token?.text === text) {
|
|
106
|
+
this.pos += 1;
|
|
107
|
+
return token;
|
|
108
|
+
}
|
|
109
|
+
this.error(`expected '${text}', found ${this.describe(token)}`, token);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
expectIdentifier(what = 'an identifier') {
|
|
114
|
+
const token = this.peek();
|
|
115
|
+
if (token?.kind === TokenKind.Identifier) {
|
|
116
|
+
this.pos += 1;
|
|
117
|
+
return node(NodeType.Identifier, token.start, token.start + token.length, {
|
|
118
|
+
name: token.text,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
this.error(`expected ${what}, found ${this.describe(token)}`, token);
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Skip forward until something that can plausibly start a new statement.
|
|
127
|
+
*
|
|
128
|
+
* This is what turns one mistake into one diagnostic instead of a cascade.
|
|
129
|
+
*/
|
|
130
|
+
synchronize() {
|
|
131
|
+
while (!this.atEnd) {
|
|
132
|
+
const token = this.next();
|
|
133
|
+
if (token.text === ';') return;
|
|
134
|
+
if (token.text === '}') return;
|
|
135
|
+
const ahead = this.peek();
|
|
136
|
+
if (ahead?.kind === TokenKind.Keyword && STATEMENT_START.has(ahead.text)) return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---- program ------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
parseProgram() {
|
|
143
|
+
const body = [];
|
|
144
|
+
while (!this.atEnd) {
|
|
145
|
+
const before = this.pos;
|
|
146
|
+
const statement = this.parseStatement();
|
|
147
|
+
if (statement) body.push(statement);
|
|
148
|
+
// Guarantee forward progress: a rule that consumed nothing would spin.
|
|
149
|
+
if (this.pos === before) {
|
|
150
|
+
this.error(`unexpected ${this.describe(this.peek())}`);
|
|
151
|
+
this.next();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return node(NodeType.Program, 0, this.text.length, { body });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---- statements ---------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
parseStatement() {
|
|
160
|
+
const token = this.peek();
|
|
161
|
+
if (!token) return null;
|
|
162
|
+
|
|
163
|
+
if (token.kind === TokenKind.Decorator) return this.parseDecorated();
|
|
164
|
+
if (token.kind === TokenKind.AsmBlock) return this.parseAsmBlock(token.start);
|
|
165
|
+
|
|
166
|
+
if (token.kind === TokenKind.Keyword) {
|
|
167
|
+
switch (token.text) {
|
|
168
|
+
case 'import': return this.parseImport();
|
|
169
|
+
case 'export': return this.parseExport();
|
|
170
|
+
case 'let':
|
|
171
|
+
case 'const': return this.parseVariableDeclaration();
|
|
172
|
+
case 'function': return this.parseFunctionDeclaration(token.start, false);
|
|
173
|
+
case 'namespace': return this.parseNamespace(token.start, false);
|
|
174
|
+
case 'if': return this.parseIf();
|
|
175
|
+
case 'while': return this.parseWhile();
|
|
176
|
+
case 'for': return this.parseFor();
|
|
177
|
+
case 'return': return this.parseReturn();
|
|
178
|
+
case 'break':
|
|
179
|
+
case 'continue': return this.parseBreakOrContinue();
|
|
180
|
+
case 'asm6502': {
|
|
181
|
+
const start = this.next().start;
|
|
182
|
+
if (this.peek()?.kind === TokenKind.AsmBlock) return this.parseAsmBlock(start);
|
|
183
|
+
// The lexer only emits a block token when a `{` follows.
|
|
184
|
+
this.error("expected '{' after 'asm6502'", this.peek());
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
default:
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (token.text === '{') return this.parseBlock();
|
|
193
|
+
if (token.text === ';') {
|
|
194
|
+
this.next();
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return this.parseExpressionStatement();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The block body is opaque: 6502 assembly, held verbatim for the backend. */
|
|
202
|
+
parseAsmBlock(start) {
|
|
203
|
+
const token = this.next();
|
|
204
|
+
return node(NodeType.AsmBlock, start, token.start + token.length, { body: token.text });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
parseDecorated() {
|
|
208
|
+
const decorators = [];
|
|
209
|
+
while (this.peek()?.kind === TokenKind.Decorator) {
|
|
210
|
+
const token = this.next();
|
|
211
|
+
let end = token.start + token.length;
|
|
212
|
+
const args = [];
|
|
213
|
+
if (this.at('(')) {
|
|
214
|
+
this.next();
|
|
215
|
+
while (!this.atEnd && !this.at(')')) {
|
|
216
|
+
const argument = this.parseExpression();
|
|
217
|
+
if (!argument) break;
|
|
218
|
+
args.push(argument);
|
|
219
|
+
if (!this.eat(',')) break;
|
|
220
|
+
}
|
|
221
|
+
const close = this.expect(')');
|
|
222
|
+
if (close) end = close.start + close.length;
|
|
223
|
+
}
|
|
224
|
+
decorators.push(
|
|
225
|
+
node(NodeType.Decorator, token.start, end, { name: token.text.slice(1), args }),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const target = this.parseStatement();
|
|
230
|
+
if (target) target.decorators = decorators;
|
|
231
|
+
return target;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
parseImport() {
|
|
235
|
+
const start = this.next().start;
|
|
236
|
+
const specifiers = [];
|
|
237
|
+
|
|
238
|
+
if (this.at('{')) {
|
|
239
|
+
this.next();
|
|
240
|
+
while (!this.atEnd && !this.at('}')) {
|
|
241
|
+
const name = this.expectIdentifier('an imported name');
|
|
242
|
+
if (!name) break;
|
|
243
|
+
let local = null;
|
|
244
|
+
if (this.atKeyword('as')) {
|
|
245
|
+
this.next();
|
|
246
|
+
local = this.expectIdentifier('a local name');
|
|
247
|
+
}
|
|
248
|
+
specifiers.push(local ? { ...name, imported: name.name, name: local.name } : name);
|
|
249
|
+
if (!this.eat(',')) break;
|
|
250
|
+
}
|
|
251
|
+
this.expect('}');
|
|
252
|
+
if (!this.atKeyword('from')) this.error("expected 'from'", this.peek());
|
|
253
|
+
else this.next();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const sourceToken = this.peek();
|
|
257
|
+
let source = null;
|
|
258
|
+
if (sourceToken?.kind === TokenKind.String) {
|
|
259
|
+
this.next();
|
|
260
|
+
source = node(NodeType.StringLiteral, sourceToken.start, sourceToken.start + sourceToken.length, {
|
|
261
|
+
value: sourceToken.text.slice(1, -1),
|
|
262
|
+
});
|
|
263
|
+
} else {
|
|
264
|
+
this.error(`expected a module specifier, found ${this.describe(sourceToken)}`, sourceToken);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const end = this.eat(';') ? this.tokens[this.pos - 1].start + 1 : (source?.start ?? start);
|
|
268
|
+
return node(NodeType.ImportDeclaration, start, end, { specifiers, source });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
parseExport() {
|
|
272
|
+
const start = this.next().start;
|
|
273
|
+
if (this.atKeyword('function')) return this.parseFunctionDeclaration(start, true);
|
|
274
|
+
if (this.atKeyword('namespace')) return this.parseNamespace(start, true);
|
|
275
|
+
if (this.atKeyword('let') || this.atKeyword('const')) {
|
|
276
|
+
const declaration = this.parseVariableDeclaration(start);
|
|
277
|
+
if (declaration) declaration.exported = true;
|
|
278
|
+
return declaration;
|
|
279
|
+
}
|
|
280
|
+
this.error(`expected a declaration after 'export', found ${this.describe(this.peek())}`);
|
|
281
|
+
this.synchronize();
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* `namespace screen { function setBorderColor(...): void { ... } }`.
|
|
287
|
+
*
|
|
288
|
+
* A namespace is compile-time-only qualification, not a struct or a value:
|
|
289
|
+
* its members compile straight to ordinary functions and inlined
|
|
290
|
+
* constants, and calling `screen.setBorderColor(...)` is exactly as cheap
|
|
291
|
+
* as calling a plain function with that name would be. Members are written
|
|
292
|
+
* without their own `export` — the namespace itself is the unit that is or
|
|
293
|
+
* isn't visible to other modules.
|
|
294
|
+
*/
|
|
295
|
+
parseNamespace(start, exported) {
|
|
296
|
+
this.next(); // 'namespace'
|
|
297
|
+
const name = this.expectIdentifier('a namespace name');
|
|
298
|
+
const members = [];
|
|
299
|
+
if (this.expect('{')) {
|
|
300
|
+
while (!this.atEnd && !this.at('}')) {
|
|
301
|
+
const before = this.pos;
|
|
302
|
+
if (this.atKeyword('function')) {
|
|
303
|
+
const token = this.peek();
|
|
304
|
+
members.push(this.parseFunctionDeclaration(token.start, false));
|
|
305
|
+
} else if (this.atKeyword('let') || this.atKeyword('const')) {
|
|
306
|
+
members.push(this.parseVariableDeclaration());
|
|
307
|
+
} else {
|
|
308
|
+
this.error(`expected a function or const inside a namespace, found ${this.describe(this.peek())}`);
|
|
309
|
+
}
|
|
310
|
+
if (this.pos === before) this.next();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const close = this.expect('}');
|
|
314
|
+
const end = close ? close.start + 1 : this.endOffset;
|
|
315
|
+
return node(NodeType.NamespaceDeclaration, start, end, { name, members, exported });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
parseVariableDeclaration(startOverride = null) {
|
|
319
|
+
const keyword = this.next();
|
|
320
|
+
const start = startOverride ?? keyword.start;
|
|
321
|
+
const name = this.expectIdentifier('a variable name');
|
|
322
|
+
if (!name) {
|
|
323
|
+
this.synchronize();
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let typeAnnotation = null;
|
|
328
|
+
if (this.eat(':')) typeAnnotation = this.parseType();
|
|
329
|
+
|
|
330
|
+
let initializer = null;
|
|
331
|
+
if (this.eat('=')) initializer = this.parseExpression();
|
|
332
|
+
|
|
333
|
+
const end = this.eat(';')
|
|
334
|
+
? this.tokens[this.pos - 1].start + 1
|
|
335
|
+
: (initializer ?? typeAnnotation ?? name).start
|
|
336
|
+
+ (initializer ?? typeAnnotation ?? name).length;
|
|
337
|
+
|
|
338
|
+
return node(NodeType.VariableDeclaration, start, end, {
|
|
339
|
+
kind: keyword.text,
|
|
340
|
+
name,
|
|
341
|
+
typeAnnotation,
|
|
342
|
+
initializer,
|
|
343
|
+
exported: false,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
parseFunctionDeclaration(start, exported) {
|
|
348
|
+
this.next(); // 'function'
|
|
349
|
+
const name = this.expectIdentifier('a function name');
|
|
350
|
+
const params = [];
|
|
351
|
+
|
|
352
|
+
if (this.expect('(')) {
|
|
353
|
+
while (!this.atEnd && !this.at(')')) {
|
|
354
|
+
const paramName = this.expectIdentifier('a parameter name');
|
|
355
|
+
if (!paramName) break;
|
|
356
|
+
let paramType = null;
|
|
357
|
+
if (this.eat(':')) paramType = this.parseType();
|
|
358
|
+
// `border: utinyint = BorderColor.BLACK`: a default, a compile-time
|
|
359
|
+
// value the call site gets when the argument is left off.
|
|
360
|
+
let defaultValue = null;
|
|
361
|
+
if (this.eat('=')) defaultValue = this.parseExpression();
|
|
362
|
+
const last = defaultValue ?? paramType ?? paramName;
|
|
363
|
+
const pEnd = last.start + last.length;
|
|
364
|
+
params.push(node(NodeType.Parameter, paramName.start, pEnd, {
|
|
365
|
+
name: paramName, typeAnnotation: paramType, defaultValue,
|
|
366
|
+
}));
|
|
367
|
+
if (!this.eat(',')) break;
|
|
368
|
+
}
|
|
369
|
+
this.expect(')');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
let returnType = null;
|
|
373
|
+
if (this.eat(':')) returnType = this.parseType();
|
|
374
|
+
|
|
375
|
+
const body = this.at('{') ? this.parseBlock() : null;
|
|
376
|
+
if (!body) this.error("expected a function body", this.peek());
|
|
377
|
+
|
|
378
|
+
const end = body ? body.start + body.length : this.endOffset;
|
|
379
|
+
return node(NodeType.FunctionDeclaration, start, end, {
|
|
380
|
+
name, params, returnType, body, exported,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
parseBlock() {
|
|
385
|
+
const open = this.expect('{');
|
|
386
|
+
const start = open ? open.start : this.peek()?.start ?? this.endOffset;
|
|
387
|
+
const body = [];
|
|
388
|
+
while (!this.atEnd && !this.at('}')) {
|
|
389
|
+
const before = this.pos;
|
|
390
|
+
const statement = this.parseStatement();
|
|
391
|
+
if (statement) body.push(statement);
|
|
392
|
+
if (this.pos === before) {
|
|
393
|
+
this.error(`unexpected ${this.describe(this.peek())}`);
|
|
394
|
+
this.next();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const close = this.expect('}');
|
|
398
|
+
const end = close ? close.start + 1 : this.endOffset;
|
|
399
|
+
return node(NodeType.BlockStatement, start, end, { body });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
parseIf() {
|
|
403
|
+
const start = this.next().start;
|
|
404
|
+
this.expect('(');
|
|
405
|
+
const test = this.parseExpression();
|
|
406
|
+
this.expect(')');
|
|
407
|
+
const consequent = this.parseStatement();
|
|
408
|
+
let alternate = null;
|
|
409
|
+
if (this.atKeyword('else')) {
|
|
410
|
+
this.next();
|
|
411
|
+
alternate = this.parseStatement();
|
|
412
|
+
}
|
|
413
|
+
const last = alternate ?? consequent;
|
|
414
|
+
const end = last ? last.start + last.length : this.endOffset;
|
|
415
|
+
return node(NodeType.IfStatement, start, end, { test, consequent, alternate });
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
parseWhile() {
|
|
419
|
+
const start = this.next().start;
|
|
420
|
+
this.expect('(');
|
|
421
|
+
const test = this.parseExpression();
|
|
422
|
+
this.expect(')');
|
|
423
|
+
const body = this.parseStatement();
|
|
424
|
+
const end = body ? body.start + body.length : this.endOffset;
|
|
425
|
+
return node(NodeType.WhileStatement, start, end, { test, body });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
parseFor() {
|
|
429
|
+
const start = this.next().start;
|
|
430
|
+
this.expect('(');
|
|
431
|
+
const init = this.at(';') ? null : this.parseStatementLikeInit();
|
|
432
|
+
this.eat(';');
|
|
433
|
+
const test = this.at(';') ? null : this.parseExpression();
|
|
434
|
+
this.expect(';');
|
|
435
|
+
const update = this.at(')') ? null : this.parseExpression();
|
|
436
|
+
this.expect(')');
|
|
437
|
+
const body = this.parseStatement();
|
|
438
|
+
const end = body ? body.start + body.length : this.endOffset;
|
|
439
|
+
return node(NodeType.ForStatement, start, end, { init, test, update, body });
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** A `for` initialiser is either a declaration or a bare expression. */
|
|
443
|
+
parseStatementLikeInit() {
|
|
444
|
+
if (this.atKeyword('let') || this.atKeyword('const')) {
|
|
445
|
+
const keyword = this.next();
|
|
446
|
+
const name = this.expectIdentifier('a variable name');
|
|
447
|
+
let typeAnnotation = null;
|
|
448
|
+
if (this.eat(':')) typeAnnotation = this.parseType();
|
|
449
|
+
let initializer = null;
|
|
450
|
+
if (this.eat('=')) initializer = this.parseExpression();
|
|
451
|
+
const last = initializer ?? typeAnnotation ?? name;
|
|
452
|
+
const end = last ? last.start + last.length : keyword.start + keyword.length;
|
|
453
|
+
return node(NodeType.VariableDeclaration, keyword.start, end, {
|
|
454
|
+
kind: keyword.text, name, typeAnnotation, initializer, exported: false,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return this.parseExpression();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
parseReturn() {
|
|
461
|
+
const keyword = this.next();
|
|
462
|
+
const argument = this.at(';') || this.at('}') ? null : this.parseExpression();
|
|
463
|
+
const end = this.eat(';')
|
|
464
|
+
? this.tokens[this.pos - 1].start + 1
|
|
465
|
+
: argument
|
|
466
|
+
? argument.start + argument.length
|
|
467
|
+
: keyword.start + keyword.length;
|
|
468
|
+
return node(NodeType.ReturnStatement, keyword.start, end, { argument });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
parseBreakOrContinue() {
|
|
472
|
+
const keyword = this.next();
|
|
473
|
+
const end = this.eat(';') ? this.tokens[this.pos - 1].start + 1 : keyword.start + keyword.length;
|
|
474
|
+
const type = keyword.text === 'break' ? NodeType.BreakStatement : NodeType.ContinueStatement;
|
|
475
|
+
return node(type, keyword.start, end, {});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
parseExpressionStatement() {
|
|
479
|
+
const expression = this.parseExpression();
|
|
480
|
+
if (!expression) {
|
|
481
|
+
this.synchronize();
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
const end = this.eat(';')
|
|
485
|
+
? this.tokens[this.pos - 1].start + 1
|
|
486
|
+
: expression.start + expression.length;
|
|
487
|
+
return node(NodeType.ExpressionStatement, expression.start, end, { expression });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ---- types --------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
/** `u8`, `ptr<u8>`, `array<u8, 16>`, `volatile<u8>`. */
|
|
493
|
+
parseType() {
|
|
494
|
+
const token = this.peek();
|
|
495
|
+
if (!token || (token.kind !== TokenKind.Type && token.kind !== TokenKind.Identifier)) {
|
|
496
|
+
this.error(`expected a type, found ${this.describe(token)}`, token);
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
this.next();
|
|
500
|
+
let end = token.start + token.length;
|
|
501
|
+
const typeArguments = [];
|
|
502
|
+
|
|
503
|
+
if (this.at('<')) {
|
|
504
|
+
this.next();
|
|
505
|
+
while (!this.atEnd && !this.at('>')) {
|
|
506
|
+
const argToken = this.peek();
|
|
507
|
+
// An array length is a value, not a type: `array<u8, 16>`. A decimal
|
|
508
|
+
// literal (`array<u8, 0.5>`) is never a valid length — fall through
|
|
509
|
+
// to parseType() below, which reports a plain "expected a type"
|
|
510
|
+
// diagnostic instead of building a bogus IntegerLiteral from a
|
|
511
|
+
// fractional token's integer-only fields.
|
|
512
|
+
if (argToken?.kind === TokenKind.Number && !argToken.isDecimal) {
|
|
513
|
+
this.next();
|
|
514
|
+
typeArguments.push(
|
|
515
|
+
node(NodeType.IntegerLiteral, argToken.start, argToken.start + argToken.length, {
|
|
516
|
+
value: argToken.value, raw: argToken.text, radix: argToken.radix,
|
|
517
|
+
}),
|
|
518
|
+
);
|
|
519
|
+
} else {
|
|
520
|
+
const inner = this.parseType();
|
|
521
|
+
if (!inner) break;
|
|
522
|
+
typeArguments.push(inner);
|
|
523
|
+
}
|
|
524
|
+
if (!this.eat(',')) break;
|
|
525
|
+
}
|
|
526
|
+
const close = this.expect('>');
|
|
527
|
+
if (close) end = close.start + close.length;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return node(NodeType.TypeReference, token.start, end, { name: token.text, typeArguments });
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ---- expressions --------------------------------------------------------
|
|
534
|
+
|
|
535
|
+
parseExpression() {
|
|
536
|
+
return this.parseAssignment();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
parseAssignment() {
|
|
540
|
+
const left = this.parseBinary(0);
|
|
541
|
+
if (!left) return null;
|
|
542
|
+
const token = this.peek();
|
|
543
|
+
if (token && ASSIGNMENT_OPERATORS.has(token.text)) {
|
|
544
|
+
this.next();
|
|
545
|
+
// Right-associative: `a = b = c` is `a = (b = c)`.
|
|
546
|
+
const right = this.parseAssignment();
|
|
547
|
+
const end = right ? right.start + right.length : token.start + token.length;
|
|
548
|
+
return node(NodeType.AssignmentExpression, left.start, end, {
|
|
549
|
+
operator: token.text, left, right,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
return left;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Precedence climbing over the table above. */
|
|
556
|
+
parseBinary(minPrecedence) {
|
|
557
|
+
let left = this.parseUnary();
|
|
558
|
+
if (!left) return null;
|
|
559
|
+
|
|
560
|
+
for (;;) {
|
|
561
|
+
const token = this.peek();
|
|
562
|
+
const precedence = token ? BINARY_PRECEDENCE[token.text] : undefined;
|
|
563
|
+
if (precedence === undefined || precedence < minPrecedence) return left;
|
|
564
|
+
this.next();
|
|
565
|
+
const right = this.parseBinary(precedence + 1);
|
|
566
|
+
if (!right) return left;
|
|
567
|
+
left = node(NodeType.BinaryExpression, left.start, right.start + right.length, {
|
|
568
|
+
operator: token.text, left, right,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
parseUnary() {
|
|
574
|
+
const token = this.peek();
|
|
575
|
+
if (token && ['!', '~', '-', '+'].includes(token.text)) {
|
|
576
|
+
this.next();
|
|
577
|
+
const argument = this.parseUnary();
|
|
578
|
+
if (!argument) return null;
|
|
579
|
+
return node(NodeType.UnaryExpression, token.start, argument.start + argument.length, {
|
|
580
|
+
operator: token.text, argument,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
if (token && ['++', '--'].includes(token.text)) {
|
|
584
|
+
this.next();
|
|
585
|
+
const argument = this.parseUnary();
|
|
586
|
+
if (!argument) return null;
|
|
587
|
+
return node(NodeType.UpdateExpression, token.start, argument.start + argument.length, {
|
|
588
|
+
operator: token.text, argument, prefix: true,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
return this.parsePostfix();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
parsePostfix() {
|
|
595
|
+
let expression = this.parsePrimary();
|
|
596
|
+
if (!expression) return null;
|
|
597
|
+
|
|
598
|
+
for (;;) {
|
|
599
|
+
const token = this.peek();
|
|
600
|
+
if (!token) return expression;
|
|
601
|
+
|
|
602
|
+
if (token.text === '.') {
|
|
603
|
+
this.next();
|
|
604
|
+
const property = this.expectIdentifier('a property name');
|
|
605
|
+
if (!property) return expression;
|
|
606
|
+
expression = node(NodeType.MemberExpression, expression.start,
|
|
607
|
+
property.start + property.length, { object: expression, property });
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (token.text === '[') {
|
|
612
|
+
this.next();
|
|
613
|
+
const index = this.parseExpression();
|
|
614
|
+
const close = this.expect(']');
|
|
615
|
+
const end = close ? close.start + 1 : expression.start + expression.length;
|
|
616
|
+
expression = node(NodeType.IndexExpression, expression.start, end, {
|
|
617
|
+
object: expression, index,
|
|
618
|
+
});
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (token.text === '(') {
|
|
623
|
+
this.next();
|
|
624
|
+
const args = [];
|
|
625
|
+
while (!this.atEnd && !this.at(')')) {
|
|
626
|
+
const argument = this.parseExpression();
|
|
627
|
+
if (!argument) break;
|
|
628
|
+
args.push(argument);
|
|
629
|
+
if (!this.eat(',')) break;
|
|
630
|
+
}
|
|
631
|
+
const close = this.expect(')');
|
|
632
|
+
const end = close ? close.start + 1 : expression.start + expression.length;
|
|
633
|
+
expression = node(NodeType.CallExpression, expression.start, end, {
|
|
634
|
+
callee: expression, args,
|
|
635
|
+
});
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (token.text === '++' || token.text === '--') {
|
|
640
|
+
this.next();
|
|
641
|
+
expression = node(NodeType.UpdateExpression, expression.start,
|
|
642
|
+
token.start + token.length, { operator: token.text, argument: expression, prefix: false });
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return expression;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* `\`TICK ${ticks % 10:1} OPTION ${option}\``: the lexer handed over one
|
|
652
|
+
* token with the spans of its text runs and `${...}` fields; each field's
|
|
653
|
+
* source is re-lexed here (its offsets shifted back into the file, so a
|
|
654
|
+
* diagnostic inside a field lands on the right characters) and parsed as
|
|
655
|
+
* an ordinary expression, followed by an optional `:width` — an integer
|
|
656
|
+
* literal saying how many cells the field takes. `:` is not a binary
|
|
657
|
+
* operator in this grammar, so the expression parser stops at it by
|
|
658
|
+
* itself.
|
|
659
|
+
*/
|
|
660
|
+
parseTemplate(token) {
|
|
661
|
+
const parts = [];
|
|
662
|
+
for (const part of token.parts) {
|
|
663
|
+
if (part.kind === 'text') {
|
|
664
|
+
parts.push(node(NodeType.TemplateText, part.start, part.end, {
|
|
665
|
+
value: this.text.slice(part.start, part.end),
|
|
666
|
+
}));
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
const source = this.text.slice(part.sourceStart, part.sourceEnd);
|
|
670
|
+
const { tokens, diagnostics } = tokenize(source, this.file);
|
|
671
|
+
for (const t of tokens) t.start += part.sourceStart;
|
|
672
|
+
for (const d of diagnostics) d.start += part.sourceStart;
|
|
673
|
+
this.diagnostics.push(...diagnostics);
|
|
674
|
+
const inner = new Parser(tokens, this.text, this.file);
|
|
675
|
+
inner.pos = 0;
|
|
676
|
+
const expression = inner.parseExpression();
|
|
677
|
+
let width = null;
|
|
678
|
+
if (expression && inner.eat(':')) {
|
|
679
|
+
const widthToken = inner.peek();
|
|
680
|
+
if (widthToken?.kind === TokenKind.Number && !widthToken.isDecimal) {
|
|
681
|
+
inner.next();
|
|
682
|
+
width = node(NodeType.IntegerLiteral, widthToken.start, widthToken.start + widthToken.length, {
|
|
683
|
+
value: widthToken.value, raw: widthToken.text, radix: widthToken.radix,
|
|
684
|
+
});
|
|
685
|
+
} else {
|
|
686
|
+
inner.error(`expected a field width after ':', found ${inner.describe(widthToken)}`, widthToken);
|
|
687
|
+
inner.pos = inner.tokens.length; // reported once; don't also flag the rest as junk
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (expression && !inner.atEnd) {
|
|
691
|
+
inner.error(`unexpected ${inner.describe(inner.peek())} in template field`);
|
|
692
|
+
}
|
|
693
|
+
this.diagnostics.push(...inner.diagnostics);
|
|
694
|
+
if (!expression) continue;
|
|
695
|
+
parts.push(node(NodeType.TemplateField, part.start, part.end, { expression, width }));
|
|
696
|
+
}
|
|
697
|
+
return node(NodeType.TemplateLiteral, token.start, token.start + token.length, { parts });
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
parsePrimary() {
|
|
701
|
+
const token = this.peek();
|
|
702
|
+
if (!token) {
|
|
703
|
+
this.error('expected an expression, found end of file');
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
const end = token.start + token.length;
|
|
707
|
+
|
|
708
|
+
if (token.kind === TokenKind.Number) {
|
|
709
|
+
this.next();
|
|
710
|
+
if (token.isDecimal) {
|
|
711
|
+
return node(NodeType.DecimalLiteral, token.start, end, {
|
|
712
|
+
numerator: token.numerator, denominator: token.denominator, raw: token.text,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
return node(NodeType.IntegerLiteral, token.start, end, {
|
|
716
|
+
value: token.value, raw: token.text, radix: token.radix,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (token.kind === TokenKind.String) {
|
|
721
|
+
this.next();
|
|
722
|
+
return node(NodeType.StringLiteral, token.start, end, {
|
|
723
|
+
value: token.text.slice(1, -1),
|
|
724
|
+
// Already reported by the lexer; the checker leaves its text alone.
|
|
725
|
+
...(token.unterminated ? { unterminated: true } : {}),
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (token.kind === TokenKind.Template) {
|
|
730
|
+
this.next();
|
|
731
|
+
return this.parseTemplate(token);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (token.kind === TokenKind.Keyword && (token.text === 'true' || token.text === 'false')) {
|
|
735
|
+
this.next();
|
|
736
|
+
return node(NodeType.BooleanLiteral, token.start, end, { value: token.text === 'true' });
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// A type name in expression position is a plain identifier, e.g. a call.
|
|
740
|
+
if (token.kind === TokenKind.Identifier || token.kind === TokenKind.Type) {
|
|
741
|
+
this.next();
|
|
742
|
+
return node(NodeType.Identifier, token.start, end, { name: token.text });
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// `#frames`: an identifier the compiler evaluates (the fold pass consumes
|
|
746
|
+
// it as a call; one left over anywhere else is that pass's diagnostic).
|
|
747
|
+
if (token.kind === TokenKind.CompileTime) {
|
|
748
|
+
this.next();
|
|
749
|
+
return node(NodeType.Identifier, token.start, end, { name: token.text.slice(1), compileTime: true });
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (token.text === '(') {
|
|
753
|
+
this.next();
|
|
754
|
+
const inner = this.parseExpression();
|
|
755
|
+
this.expect(')');
|
|
756
|
+
return inner;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// `[1, 2, 3]`: an array initialiser. A trailing comma is allowed, as in
|
|
760
|
+
// TypeScript, so a table one value per line can end every line alike.
|
|
761
|
+
if (token.text === '[') {
|
|
762
|
+
this.next();
|
|
763
|
+
const elements = [];
|
|
764
|
+
while (!this.atEnd && !this.at(']')) {
|
|
765
|
+
const element = this.parseExpression();
|
|
766
|
+
if (!element) break;
|
|
767
|
+
elements.push(element);
|
|
768
|
+
if (!this.eat(',')) break;
|
|
769
|
+
}
|
|
770
|
+
const close = this.expect(']');
|
|
771
|
+
const last = elements[elements.length - 1];
|
|
772
|
+
const literalEnd = close ? close.start + 1 : (last ? last.start + last.length : end);
|
|
773
|
+
return node(NodeType.ArrayLiteral, token.start, literalEnd, { elements });
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
this.error(`expected an expression, found ${this.describe(token)}`, token);
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Parse a token stream.
|
|
783
|
+
*
|
|
784
|
+
* Always returns both an AST and diagnostics; never throws.
|
|
785
|
+
*
|
|
786
|
+
* @param {object[]} tokens
|
|
787
|
+
* @param {string} text
|
|
788
|
+
* @param {string} file
|
|
789
|
+
* @returns {{ ast: object, diagnostics: object[] }}
|
|
790
|
+
*/
|
|
791
|
+
export function parse(tokens, text, file = '<unknown>') {
|
|
792
|
+
const parser = new Parser(tokens, text, file);
|
|
793
|
+
const ast = parser.parseProgram();
|
|
794
|
+
return { ast, diagnostics: parser.diagnostics };
|
|
795
|
+
}
|