@newt-dev/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/dist/src/ast.d.ts +220 -0
- package/dist/src/ast.js +1 -0
- package/dist/src/codegen.d.ts +6 -0
- package/dist/src/codegen.js +266 -0
- package/dist/src/errors.d.ts +31 -0
- package/dist/src/errors.js +120 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +33 -0
- package/dist/src/lexer.d.ts +9 -0
- package/dist/src/lexer.js +285 -0
- package/dist/src/parser.d.ts +3 -0
- package/dist/src/parser.js +459 -0
- package/dist/src/validator.d.ts +3 -0
- package/dist/src/validator.js +176 -0
- package/dist/test/integration.test.d.ts +1 -0
- package/dist/test/integration.test.js +37 -0
- package/package.json +37 -0
- package/src/ast.ts +307 -0
- package/src/codegen.ts +286 -0
- package/src/errors.ts +164 -0
- package/src/index.ts +48 -0
- package/src/lexer.ts +349 -0
- package/src/parser.ts +534 -0
- package/src/validator.ts +191 -0
- package/test/integration.test.ts +42 -0
- package/tsconfig.json +18 -0
package/src/parser.ts
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type BinaryExpr,
|
|
3
|
+
type BotDecl,
|
|
4
|
+
type CommandHandler,
|
|
5
|
+
type DailyTimerDecl,
|
|
6
|
+
type DurationLiteral,
|
|
7
|
+
type EmbedBlock,
|
|
8
|
+
type EveryTimerDecl,
|
|
9
|
+
type Expression,
|
|
10
|
+
type Handler,
|
|
11
|
+
type MessageContainsHandler,
|
|
12
|
+
type Program,
|
|
13
|
+
type Statement,
|
|
14
|
+
type StringLiteral,
|
|
15
|
+
type TimeUnit,
|
|
16
|
+
type TopLevelNode
|
|
17
|
+
} from "./ast.js";
|
|
18
|
+
import { makeCatalogError, NewtError } from "./errors.js";
|
|
19
|
+
import type { Token, TokenType } from "./lexer.js";
|
|
20
|
+
|
|
21
|
+
export function parse(tokens: Token[]): Program {
|
|
22
|
+
return new Parser(tokens).parseProgram();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class Parser {
|
|
26
|
+
private current = 0;
|
|
27
|
+
|
|
28
|
+
constructor(private readonly tokens: Token[]) {}
|
|
29
|
+
|
|
30
|
+
parseProgram(): Program {
|
|
31
|
+
const body: TopLevelNode[] = [];
|
|
32
|
+
this.skipNewlines();
|
|
33
|
+
|
|
34
|
+
while (!this.isAtEnd()) {
|
|
35
|
+
if (this.checkKeyword("bot")) {
|
|
36
|
+
body.push(this.parseBotDecl());
|
|
37
|
+
} else if (this.checkKeyword("on")) {
|
|
38
|
+
body.push(this.parseHandler());
|
|
39
|
+
} else if (this.checkKeyword("every") || this.checkKeyword("at")) {
|
|
40
|
+
body.push(this.parseTimer());
|
|
41
|
+
} else {
|
|
42
|
+
const token = this.peek();
|
|
43
|
+
throw makeCatalogError("NEWT_E004", token.line, token.column, this.sourceLineHint(token));
|
|
44
|
+
}
|
|
45
|
+
this.skipNewlines();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { type: "Program", loc: { line: 1, column: 1 }, body };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private parseBotDecl(): BotDecl {
|
|
52
|
+
const start = this.consumeKeyword("bot");
|
|
53
|
+
const kindToken = this.consumeType("KEYWORD", "Bot declarations need a setting like name, prefix, or token.");
|
|
54
|
+
if (!["name", "prefix", "token"].includes(kindToken.value)) {
|
|
55
|
+
throw this.error(kindToken, "Bot declarations support name, prefix, and token.");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let fromEnv = false;
|
|
59
|
+
if (kindToken.value === "token" && this.matchKeyword("from")) {
|
|
60
|
+
this.consumeKeyword("env");
|
|
61
|
+
fromEnv = true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const value = this.parseStringLiteral();
|
|
65
|
+
this.consumeLineEnd();
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
type: "BotDecl",
|
|
69
|
+
loc: this.loc(start),
|
|
70
|
+
kind: kindToken.value as BotDecl["kind"],
|
|
71
|
+
value,
|
|
72
|
+
fromEnv
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private parseHandler(): Handler {
|
|
77
|
+
const start = this.consumeKeyword("on");
|
|
78
|
+
const event = this.consumeType("KEYWORD", "Handlers need an event name after on.");
|
|
79
|
+
|
|
80
|
+
let handler: Handler;
|
|
81
|
+
if (event.value === "ready") {
|
|
82
|
+
handler = { type: "ReadyHandler", loc: this.loc(start), body: [] };
|
|
83
|
+
} else if (event.value === "command") {
|
|
84
|
+
const command = this.parseStringLiteral();
|
|
85
|
+
handler = { type: "CommandHandler", loc: this.loc(start), command: command.value, body: [] };
|
|
86
|
+
} else if (event.value === "message") {
|
|
87
|
+
this.consumeKeyword("contains");
|
|
88
|
+
const needle = this.parseStringLiteral();
|
|
89
|
+
handler = { type: "MessageContainsHandler", loc: this.loc(start), needle, body: [] };
|
|
90
|
+
} else if (event.value === "join") {
|
|
91
|
+
handler = { type: "JoinHandler", loc: this.loc(start), body: [] };
|
|
92
|
+
} else if (event.value === "leave") {
|
|
93
|
+
handler = { type: "LeaveHandler", loc: this.loc(start), body: [] };
|
|
94
|
+
} else if (event.value === "reaction") {
|
|
95
|
+
this.consumeKeyword("add");
|
|
96
|
+
const emoji = this.parseStringLiteral();
|
|
97
|
+
handler = { type: "ReactionAddHandler", loc: this.loc(start), emoji, body: [] };
|
|
98
|
+
} else {
|
|
99
|
+
throw makeCatalogError("NEWT_E003", event.line, event.column, this.sourceLineHint(event));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.consumeBlockStart();
|
|
103
|
+
handler.body = this.parseBlock();
|
|
104
|
+
return handler;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private parseTimer(): EveryTimerDecl | DailyTimerDecl {
|
|
108
|
+
if (this.checkKeyword("every")) {
|
|
109
|
+
const start = this.advance();
|
|
110
|
+
const amount = this.parseNumberLiteral();
|
|
111
|
+
const unit = this.consumeType("KEYWORD", "Timers need a unit like seconds, minutes, or hours.");
|
|
112
|
+
this.consumeBlockStart();
|
|
113
|
+
return {
|
|
114
|
+
type: "EveryTimerDecl",
|
|
115
|
+
loc: this.loc(start),
|
|
116
|
+
amount,
|
|
117
|
+
unit: unit.value as TimeUnit,
|
|
118
|
+
body: this.parseBlock()
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const start = this.consumeKeyword("at");
|
|
123
|
+
const time = this.parseStringLiteral();
|
|
124
|
+
this.consumeKeyword("daily");
|
|
125
|
+
this.consumeBlockStart();
|
|
126
|
+
return { type: "DailyTimerDecl", loc: this.loc(start), time, body: this.parseBlock() };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private parseBlock(): Statement[] {
|
|
130
|
+
this.skipNewlines();
|
|
131
|
+
this.consumeType("INDENT", "Indented lines should come after a block header.");
|
|
132
|
+
const body: Statement[] = [];
|
|
133
|
+
this.skipNewlines();
|
|
134
|
+
|
|
135
|
+
while (!this.isAtEnd() && !this.check("DEDENT")) {
|
|
136
|
+
body.push(this.parseStatement());
|
|
137
|
+
this.skipNewlines();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.consumeType("DEDENT", "Blocks need to return to the previous indentation level.");
|
|
141
|
+
return body;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private parseStatement(): Statement {
|
|
145
|
+
if (this.checkKeyword("reply")) {
|
|
146
|
+
const start = this.advance();
|
|
147
|
+
const message = this.parseExpressionUntilLineEnd();
|
|
148
|
+
this.consumeLineEnd();
|
|
149
|
+
return { type: "ReplyStatement", loc: this.loc(start), message };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (this.checkKeyword("say")) {
|
|
153
|
+
return this.parseSayStatement();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (this.checkKeyword("let")) {
|
|
157
|
+
const start = this.advance();
|
|
158
|
+
const name = this.consumeType("IDENTIFIER", "let needs a variable name.").value;
|
|
159
|
+
this.consumeType("EQUALS", "let needs an equals sign before the value.");
|
|
160
|
+
const value = this.parseExpressionUntilLineEnd();
|
|
161
|
+
this.consumeLineEnd();
|
|
162
|
+
return { type: "LetDecl", loc: this.loc(start), name, value };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (this.checkKeyword("store")) {
|
|
166
|
+
const start = this.advance();
|
|
167
|
+
const namespace = this.parseAtom();
|
|
168
|
+
const key = this.consumeWord("store needs a key name after the namespace.").value;
|
|
169
|
+
this.consumeType("EQUALS", "store needs an equals sign before the value.");
|
|
170
|
+
const value = this.parseExpressionUntilLineEnd();
|
|
171
|
+
this.consumeLineEnd();
|
|
172
|
+
return { type: "StoreStatement", loc: this.loc(start), namespace, key, value };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (this.checkKeyword("if")) {
|
|
176
|
+
return this.parseIfStatement();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (this.checkKeyword("for")) {
|
|
180
|
+
return this.parseForEachStatement();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (this.checkKeyword("require")) {
|
|
184
|
+
const start = this.advance();
|
|
185
|
+
this.consumeKeyword("role");
|
|
186
|
+
const role = this.parseStringLiteral();
|
|
187
|
+
this.consumeLineEnd();
|
|
188
|
+
return { type: "RequireRoleStatement", loc: this.loc(start), role };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (this.checkKeyword("give") || this.checkKeyword("remove")) {
|
|
192
|
+
return this.parseRoleMutation();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (this.checkKeyword("mute")) {
|
|
196
|
+
const start = this.advance();
|
|
197
|
+
const subject = this.parseAtom();
|
|
198
|
+
let duration: DurationLiteral | undefined;
|
|
199
|
+
if (this.matchKeyword("for")) {
|
|
200
|
+
duration = this.parseDuration();
|
|
201
|
+
}
|
|
202
|
+
this.consumeLineEnd();
|
|
203
|
+
return { type: "MuteStatement", loc: this.loc(start), subject, duration };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (this.checkKeyword("kick") || this.checkKeyword("ban")) {
|
|
207
|
+
const start = this.advance();
|
|
208
|
+
const subject = this.parseAtom();
|
|
209
|
+
this.consumeLineEnd();
|
|
210
|
+
return { type: start.value === "kick" ? "KickStatement" : "BanStatement", loc: this.loc(start), subject };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (this.checkKeyword("try")) {
|
|
214
|
+
return this.parseTryCatch();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (this.checkKeyword("wait")) {
|
|
218
|
+
const start = this.advance();
|
|
219
|
+
this.consumeKeyword("for");
|
|
220
|
+
const duration = this.parseDuration();
|
|
221
|
+
this.consumeLineEnd();
|
|
222
|
+
return { type: "WaitStatement", loc: this.loc(start), duration };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const start = this.peek();
|
|
226
|
+
const expression = this.parseExpressionUntilLineEnd();
|
|
227
|
+
this.consumeLineEnd();
|
|
228
|
+
return { type: "ExpressionStatement", loc: this.loc(start), expression };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private parseSayStatement(): Statement {
|
|
232
|
+
const start = this.consumeKeyword("say");
|
|
233
|
+
if (this.matchKeyword("embed")) {
|
|
234
|
+
this.consumeBlockStart();
|
|
235
|
+
const embed = this.parseEmbedBlock(start);
|
|
236
|
+
return { type: "SayEmbedStatement", loc: this.loc(start), embed };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const message = this.parseExpressionUntil(["NEWLINE", "EOF"], ["in"]);
|
|
240
|
+
let channel: StringLiteral | undefined;
|
|
241
|
+
if (this.matchKeyword("in")) {
|
|
242
|
+
this.consumeKeyword("channel");
|
|
243
|
+
channel = this.parseStringLiteral();
|
|
244
|
+
}
|
|
245
|
+
this.consumeLineEnd();
|
|
246
|
+
return { type: "SayStatement", loc: this.loc(start), message, channel };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private parseEmbedBlock(start: Token): EmbedBlock {
|
|
250
|
+
this.skipNewlines();
|
|
251
|
+
this.consumeType("INDENT", "Embed details need to be indented.");
|
|
252
|
+
const embed: EmbedBlock = { type: "EmbedBlock", loc: this.loc(start), fields: [] };
|
|
253
|
+
this.skipNewlines();
|
|
254
|
+
|
|
255
|
+
while (!this.isAtEnd() && !this.check("DEDENT")) {
|
|
256
|
+
if (this.matchKeyword("title")) {
|
|
257
|
+
embed.title = this.parseStringLiteral();
|
|
258
|
+
} else if (this.matchKeyword("description")) {
|
|
259
|
+
embed.description = this.parseStringLiteral();
|
|
260
|
+
} else if (this.matchKeyword("color")) {
|
|
261
|
+
const color = this.consumeType("HASH_COLOR", "Embed colors look like #5865F2.");
|
|
262
|
+
embed.color = { type: "ColorLiteral", loc: this.loc(color), value: color.value };
|
|
263
|
+
} else if (this.matchKeyword("field")) {
|
|
264
|
+
const name = this.parseStringLiteral();
|
|
265
|
+
const value = this.parseStringLiteral();
|
|
266
|
+
embed.fields.push({ type: "EmbedField", loc: name.loc, name, value });
|
|
267
|
+
} else {
|
|
268
|
+
throw this.error(this.peek(), "Embeds support title, description, color, and field lines.");
|
|
269
|
+
}
|
|
270
|
+
this.consumeLineEnd();
|
|
271
|
+
this.skipNewlines();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this.consumeType("DEDENT", "Embed blocks need to return to the previous indentation level.");
|
|
275
|
+
return embed;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private parseIfStatement(): Statement {
|
|
279
|
+
const start = this.consumeKeyword("if");
|
|
280
|
+
const condition = this.parseExpressionUntil(["COLON"]);
|
|
281
|
+
this.consumeBlockStart();
|
|
282
|
+
const consequent = this.parseBlock();
|
|
283
|
+
let alternate: Statement[] = [];
|
|
284
|
+
if (this.matchKeyword("else")) {
|
|
285
|
+
this.consumeBlockStart();
|
|
286
|
+
alternate = this.parseBlock();
|
|
287
|
+
}
|
|
288
|
+
return { type: "IfStatement", loc: this.loc(start), condition, consequent, alternate };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private parseForEachStatement(): Statement {
|
|
292
|
+
const start = this.consumeKeyword("for");
|
|
293
|
+
this.consumeKeyword("each");
|
|
294
|
+
const itemName = this.consumeWord("for each needs an item name.").value;
|
|
295
|
+
this.consumeKeyword("in");
|
|
296
|
+
const iterable = this.parseExpressionUntil(["COLON"]);
|
|
297
|
+
this.consumeBlockStart();
|
|
298
|
+
return { type: "ForEachStatement", loc: this.loc(start), itemName, iterable, body: this.parseBlock() };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private parseRoleMutation(): Statement {
|
|
302
|
+
const start = this.advance();
|
|
303
|
+
const subject = this.parseAtom();
|
|
304
|
+
this.consumeKeyword("role");
|
|
305
|
+
const role = this.parseStringLiteral();
|
|
306
|
+
this.consumeLineEnd();
|
|
307
|
+
return start.value === "give"
|
|
308
|
+
? { type: "GiveRoleStatement", loc: this.loc(start), subject, role }
|
|
309
|
+
: { type: "RemoveRoleStatement", loc: this.loc(start), subject, role };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private parseTryCatch(): Statement {
|
|
313
|
+
const start = this.consumeKeyword("try");
|
|
314
|
+
this.consumeBlockStart();
|
|
315
|
+
const body = this.parseBlock();
|
|
316
|
+
this.consumeKeyword("on");
|
|
317
|
+
this.consumeKeyword("error");
|
|
318
|
+
this.consumeBlockStart();
|
|
319
|
+
const errorHandler = this.parseBlock();
|
|
320
|
+
return { type: "TryCatchStatement", loc: this.loc(start), body, errorHandler };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private parseDuration(): DurationLiteral {
|
|
324
|
+
const amount = this.parseNumberLiteral();
|
|
325
|
+
const unit = this.consumeType("KEYWORD", "Durations need a unit like seconds, minutes, or hours.");
|
|
326
|
+
return { type: "DurationLiteral", loc: amount.loc, amount, unit: unit.value as TimeUnit };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private parseExpressionUntilLineEnd(): Expression {
|
|
330
|
+
return this.parseExpressionUntil(["NEWLINE", "EOF"]);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private parseExpressionUntil(endTypes: TokenType[], endKeywords: string[] = []): Expression {
|
|
334
|
+
const parts: Expression[] = [];
|
|
335
|
+
const operators: Token[] = [];
|
|
336
|
+
|
|
337
|
+
while (!this.isAtEnd() && !endTypes.includes(this.peek().type) && !this.isEndKeyword(endKeywords)) {
|
|
338
|
+
if (this.check("OPERATOR") || this.checkKeyword("or") || this.checkKeyword("and") || this.checkKeyword("has")) {
|
|
339
|
+
const operator = this.advance();
|
|
340
|
+
operators.push(operator);
|
|
341
|
+
if (operator.value === "has" && this.checkKeyword("role")) {
|
|
342
|
+
this.advance();
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
parts.push(this.parseAtom());
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (parts.length === 0) {
|
|
350
|
+
throw this.error(this.peek(), "This line needs a value here.");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let expression = parts[0]!;
|
|
354
|
+
for (let index = 1; index < parts.length; index += 1) {
|
|
355
|
+
const operator = operators[index - 1]?.value ?? "+";
|
|
356
|
+
expression = {
|
|
357
|
+
type: "BinaryExpr",
|
|
358
|
+
loc: expression.loc,
|
|
359
|
+
operator,
|
|
360
|
+
left: expression,
|
|
361
|
+
right: parts[index]!
|
|
362
|
+
} satisfies BinaryExpr;
|
|
363
|
+
}
|
|
364
|
+
return expression;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private parseAtom(): Expression {
|
|
368
|
+
if (this.check("STRING")) {
|
|
369
|
+
return this.parseStringLiteral();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (this.check("NUMBER")) {
|
|
373
|
+
return this.parseNumberLiteral();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (this.matchKeyword("load")) {
|
|
377
|
+
const start = this.previous();
|
|
378
|
+
const namespace = this.parseAtom();
|
|
379
|
+
const key = this.consumeWord("load needs a key name after the namespace.").value;
|
|
380
|
+
let fallback: Expression | undefined;
|
|
381
|
+
if (this.matchKeyword("or")) {
|
|
382
|
+
fallback = this.parseAtom();
|
|
383
|
+
}
|
|
384
|
+
return { type: "LoadExpr", loc: this.loc(start), namespace, key, fallback };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (this.matchKeyword("fetch")) {
|
|
388
|
+
const start = this.previous();
|
|
389
|
+
return { type: "FetchExpr", loc: this.loc(start), url: this.parseAtom() };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (this.match("LPAREN")) {
|
|
393
|
+
const expression = this.parseExpressionUntil(["RPAREN"]);
|
|
394
|
+
this.consumeType("RPAREN", "Close this expression with ).");
|
|
395
|
+
return expression;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const token = this.consumeWord("Expected a value.");
|
|
399
|
+
const path = [token.value];
|
|
400
|
+
while (this.match("DOT")) {
|
|
401
|
+
path.push(this.consumeWord("Expected a name after the dot.").value);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (path[0] === "args" && this.match("LBRACKET")) {
|
|
405
|
+
const index = this.parseNumberLiteral();
|
|
406
|
+
this.consumeType("RBRACKET", "Close args[index] with ].");
|
|
407
|
+
return { type: "ArgsIndexExpr", loc: this.loc(token), index: index.value };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return path.length === 1
|
|
411
|
+
? { type: "IdentifierExpr", loc: this.loc(token), name: path[0]! }
|
|
412
|
+
: { type: "MemberExpr", loc: this.loc(token), path };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private parseStringLiteral(): StringLiteral {
|
|
416
|
+
const token = this.consumeType("STRING", "Text values need double quotes.");
|
|
417
|
+
return { type: "StringLiteral", loc: this.loc(token), value: token.value, interpolated: Boolean(token.interpolated) };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private parseNumberLiteral() {
|
|
421
|
+
const token = this.consumeType("NUMBER", "Expected a number here.");
|
|
422
|
+
return { type: "NumberLiteral" as const, loc: this.loc(token), value: Number(token.value) };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private consumeBlockStart(): void {
|
|
426
|
+
if (!this.match("COLON")) {
|
|
427
|
+
const token = this.peek();
|
|
428
|
+
throw makeCatalogError("NEWT_E002", token.line, token.column, this.sourceLineHint(token));
|
|
429
|
+
}
|
|
430
|
+
this.consumeLineEnd();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private consumeLineEnd(): void {
|
|
434
|
+
if (this.match("NEWLINE") || this.check("EOF")) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
throw this.error(this.peek(), "I expected this line to end here.");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private skipNewlines(): void {
|
|
441
|
+
while (this.match("NEWLINE")) {
|
|
442
|
+
// Keep moving.
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private isEndKeyword(keywords: string[]): boolean {
|
|
447
|
+
return this.check("KEYWORD") && keywords.includes(this.peek().value);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private consumeKeyword(value: string): Token {
|
|
451
|
+
if (this.checkKeyword(value)) {
|
|
452
|
+
return this.advance();
|
|
453
|
+
}
|
|
454
|
+
throw this.error(this.peek(), `Expected "${value}" here.`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private matchKeyword(value: string): boolean {
|
|
458
|
+
if (!this.checkKeyword(value)) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
this.advance();
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private checkKeyword(value: string): boolean {
|
|
466
|
+
return this.check("KEYWORD") && this.peek().value === value;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private consumeWord(message: string): Token {
|
|
470
|
+
if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
471
|
+
return this.advance();
|
|
472
|
+
}
|
|
473
|
+
throw this.error(this.peek(), message);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private consumeType(type: TokenType, message: string): Token {
|
|
477
|
+
if (this.check(type)) {
|
|
478
|
+
return this.advance();
|
|
479
|
+
}
|
|
480
|
+
throw this.error(this.peek(), message);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private match(type: TokenType): boolean {
|
|
484
|
+
if (!this.check(type)) {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
this.advance();
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private check(type: TokenType): boolean {
|
|
492
|
+
if (this.isAtEnd()) {
|
|
493
|
+
return type === "EOF";
|
|
494
|
+
}
|
|
495
|
+
return this.peek().type === type;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private advance(): Token {
|
|
499
|
+
if (!this.isAtEnd()) {
|
|
500
|
+
this.current += 1;
|
|
501
|
+
}
|
|
502
|
+
return this.previous();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private isAtEnd(): boolean {
|
|
506
|
+
return this.peek().type === "EOF";
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private peek(): Token {
|
|
510
|
+
return this.tokens[this.current] ?? this.tokens[this.tokens.length - 1]!;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private previous(): Token {
|
|
514
|
+
return this.tokens[this.current - 1] ?? this.tokens[0]!;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private loc(token: Token) {
|
|
518
|
+
return { line: token.line, column: token.column };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private sourceLineHint(_token: Token): string | undefined {
|
|
522
|
+
return undefined;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private error(token: Token, message: string): NewtError {
|
|
526
|
+
return new NewtError({
|
|
527
|
+
code: "NEWT_E001",
|
|
528
|
+
message,
|
|
529
|
+
suggestion: "Check the Newt syntax for this line and try again.",
|
|
530
|
+
line: token.line,
|
|
531
|
+
column: token.column
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|