@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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { generate } from "./codegen.js";
|
|
2
|
+
import { NewtError } from "./errors.js";
|
|
3
|
+
import { tokenize } from "./lexer.js";
|
|
4
|
+
import { parse } from "./parser.js";
|
|
5
|
+
import { validate } from "./validator.js";
|
|
6
|
+
export function compile(source, filename = "input.newt") {
|
|
7
|
+
try {
|
|
8
|
+
const tokens = tokenize(source);
|
|
9
|
+
const program = parse(tokens);
|
|
10
|
+
const errors = validate(program, source).map((error) => {
|
|
11
|
+
if (!error.filename) {
|
|
12
|
+
return new NewtError({ ...error, filename });
|
|
13
|
+
}
|
|
14
|
+
return error;
|
|
15
|
+
});
|
|
16
|
+
const fatalErrors = errors.filter((error) => error.severity === "error");
|
|
17
|
+
if (fatalErrors.length > 0) {
|
|
18
|
+
return { success: false, errors };
|
|
19
|
+
}
|
|
20
|
+
return { success: true, ...generate(program) };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error instanceof NewtError) {
|
|
24
|
+
return { success: false, errors: [new NewtError({ ...error, filename })] };
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export { generate } from "./codegen.js";
|
|
30
|
+
export { formatError, NewtError } from "./errors.js";
|
|
31
|
+
export { tokenize } from "./lexer.js";
|
|
32
|
+
export { parse } from "./parser.js";
|
|
33
|
+
export { validate } from "./validator.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type TokenType = "KEYWORD" | "STRING" | "NUMBER" | "IDENTIFIER" | "INDENT" | "DEDENT" | "NEWLINE" | "COLON" | "EQUALS" | "LPAREN" | "RPAREN" | "LBRACKET" | "RBRACKET" | "COMMA" | "DOT" | "OPERATOR" | "HASH_COLOR" | "COMMENT" | "EOF";
|
|
2
|
+
export interface Token {
|
|
3
|
+
type: TokenType;
|
|
4
|
+
value: string;
|
|
5
|
+
line: number;
|
|
6
|
+
column: number;
|
|
7
|
+
interpolated?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function tokenize(source: string): Token[];
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { makeCatalogError, NewtError } from "./errors.js";
|
|
2
|
+
const keywords = new Set([
|
|
3
|
+
"bot",
|
|
4
|
+
"name",
|
|
5
|
+
"prefix",
|
|
6
|
+
"token",
|
|
7
|
+
"on",
|
|
8
|
+
"ready",
|
|
9
|
+
"command",
|
|
10
|
+
"join",
|
|
11
|
+
"leave",
|
|
12
|
+
"message",
|
|
13
|
+
"reaction",
|
|
14
|
+
"add",
|
|
15
|
+
"let",
|
|
16
|
+
"if",
|
|
17
|
+
"else",
|
|
18
|
+
"for",
|
|
19
|
+
"every",
|
|
20
|
+
"at",
|
|
21
|
+
"reply",
|
|
22
|
+
"say",
|
|
23
|
+
"give",
|
|
24
|
+
"remove",
|
|
25
|
+
"require",
|
|
26
|
+
"role",
|
|
27
|
+
"store",
|
|
28
|
+
"load",
|
|
29
|
+
"fetch",
|
|
30
|
+
"try",
|
|
31
|
+
"error",
|
|
32
|
+
"in",
|
|
33
|
+
"from",
|
|
34
|
+
"env",
|
|
35
|
+
"each",
|
|
36
|
+
"contains",
|
|
37
|
+
"has",
|
|
38
|
+
"or",
|
|
39
|
+
"and",
|
|
40
|
+
"not",
|
|
41
|
+
"embed",
|
|
42
|
+
"title",
|
|
43
|
+
"description",
|
|
44
|
+
"color",
|
|
45
|
+
"field",
|
|
46
|
+
"daily",
|
|
47
|
+
"mute",
|
|
48
|
+
"kick",
|
|
49
|
+
"ban",
|
|
50
|
+
"pin",
|
|
51
|
+
"delete",
|
|
52
|
+
"wait",
|
|
53
|
+
"target",
|
|
54
|
+
"channel",
|
|
55
|
+
"server",
|
|
56
|
+
"user",
|
|
57
|
+
"member",
|
|
58
|
+
"members",
|
|
59
|
+
"minutes",
|
|
60
|
+
"minute",
|
|
61
|
+
"hours",
|
|
62
|
+
"hour",
|
|
63
|
+
"seconds",
|
|
64
|
+
"second",
|
|
65
|
+
"days",
|
|
66
|
+
"day",
|
|
67
|
+
"args",
|
|
68
|
+
"random",
|
|
69
|
+
"between",
|
|
70
|
+
"round",
|
|
71
|
+
"floor",
|
|
72
|
+
"ceil",
|
|
73
|
+
"uppercase",
|
|
74
|
+
"lowercase",
|
|
75
|
+
"replace",
|
|
76
|
+
"split"
|
|
77
|
+
]);
|
|
78
|
+
export function tokenize(source) {
|
|
79
|
+
const lexer = new Lexer(source);
|
|
80
|
+
return lexer.tokenize();
|
|
81
|
+
}
|
|
82
|
+
class Lexer {
|
|
83
|
+
source;
|
|
84
|
+
tokens = [];
|
|
85
|
+
indentStack = [0];
|
|
86
|
+
indentUnit;
|
|
87
|
+
lines;
|
|
88
|
+
constructor(source) {
|
|
89
|
+
this.source = source;
|
|
90
|
+
this.lines = source.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
91
|
+
}
|
|
92
|
+
tokenize() {
|
|
93
|
+
for (let index = 0; index < this.lines.length; index += 1) {
|
|
94
|
+
this.tokenizeLine(this.lines[index] ?? "", index + 1);
|
|
95
|
+
}
|
|
96
|
+
const lastLine = Math.max(1, this.lines.length);
|
|
97
|
+
while (this.indentStack.length > 1) {
|
|
98
|
+
this.indentStack.pop();
|
|
99
|
+
this.push("DEDENT", "", lastLine, 1);
|
|
100
|
+
}
|
|
101
|
+
this.push("EOF", "", lastLine, (this.lines[this.lines.length - 1]?.length ?? 0) + 1);
|
|
102
|
+
return this.tokens;
|
|
103
|
+
}
|
|
104
|
+
tokenizeLine(rawLine, lineNumber) {
|
|
105
|
+
const lineWithoutTrailing = rawLine.replace(/\s+$/u, "");
|
|
106
|
+
const contentStart = this.countIndent(lineWithoutTrailing);
|
|
107
|
+
const content = lineWithoutTrailing.slice(contentStart);
|
|
108
|
+
if (content.length === 0 || content.startsWith("#")) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.applyIndent(contentStart, lineNumber, rawLine);
|
|
112
|
+
let index = contentStart;
|
|
113
|
+
while (index < lineWithoutTrailing.length) {
|
|
114
|
+
const char = lineWithoutTrailing[index] ?? "";
|
|
115
|
+
const column = index + 1;
|
|
116
|
+
if (char === " " || char === "\t") {
|
|
117
|
+
index += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (char === "#") {
|
|
121
|
+
const maybeColor = lineWithoutTrailing.slice(index, index + 7);
|
|
122
|
+
if (/^#[0-9a-fA-F]{6}$/u.test(maybeColor)) {
|
|
123
|
+
this.push("HASH_COLOR", maybeColor, lineNumber, column);
|
|
124
|
+
index += 7;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
this.push("COMMENT", lineWithoutTrailing.slice(index), lineNumber, column);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
if (char === "\"") {
|
|
131
|
+
index = this.readString(lineWithoutTrailing, index, lineNumber);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (/[0-9]/u.test(char)) {
|
|
135
|
+
index = this.readNumber(lineWithoutTrailing, index, lineNumber);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (/[A-Za-z_]/u.test(char)) {
|
|
139
|
+
index = this.readWord(lineWithoutTrailing, index, lineNumber);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const singleToken = this.singleCharToken(char);
|
|
143
|
+
if (singleToken) {
|
|
144
|
+
this.push(singleToken, char, lineNumber, column);
|
|
145
|
+
index += 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if ("+-*/<>!".includes(char)) {
|
|
149
|
+
const next = lineWithoutTrailing[index + 1];
|
|
150
|
+
const value = next === "=" ? `${char}=` : char;
|
|
151
|
+
this.push("OPERATOR", value, lineNumber, column);
|
|
152
|
+
index += value.length;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
throw new NewtError({
|
|
156
|
+
code: "NEWT_E001",
|
|
157
|
+
message: `I do not know what to do with "${char}" here.`,
|
|
158
|
+
suggestion: "Check for a missing quote or an extra symbol.",
|
|
159
|
+
line: lineNumber,
|
|
160
|
+
column,
|
|
161
|
+
sourceLine: rawLine
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
this.push("NEWLINE", "", lineNumber, lineWithoutTrailing.length + 1);
|
|
165
|
+
}
|
|
166
|
+
applyIndent(indent, lineNumber, sourceLine) {
|
|
167
|
+
const current = this.indentStack[this.indentStack.length - 1] ?? 0;
|
|
168
|
+
if (indent === current) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (indent > current) {
|
|
172
|
+
const diff = indent - current;
|
|
173
|
+
if (!this.indentUnit) {
|
|
174
|
+
if (diff !== 2 && diff !== 4) {
|
|
175
|
+
throw makeCatalogError("NEWT_E006", lineNumber, 1, sourceLine);
|
|
176
|
+
}
|
|
177
|
+
this.indentUnit = diff;
|
|
178
|
+
}
|
|
179
|
+
else if (diff % this.indentUnit !== 0) {
|
|
180
|
+
throw makeCatalogError("NEWT_E006", lineNumber, 1, sourceLine);
|
|
181
|
+
}
|
|
182
|
+
this.indentStack.push(indent);
|
|
183
|
+
this.push("INDENT", "", lineNumber, 1);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
while (this.indentStack.length > 1 && indent < (this.indentStack[this.indentStack.length - 1] ?? 0)) {
|
|
187
|
+
this.indentStack.pop();
|
|
188
|
+
this.push("DEDENT", "", lineNumber, 1);
|
|
189
|
+
}
|
|
190
|
+
if (indent !== (this.indentStack[this.indentStack.length - 1] ?? 0)) {
|
|
191
|
+
throw makeCatalogError("NEWT_E006", lineNumber, 1, sourceLine);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
countIndent(line) {
|
|
195
|
+
let count = 0;
|
|
196
|
+
for (const char of line) {
|
|
197
|
+
if (char === " ") {
|
|
198
|
+
count += 1;
|
|
199
|
+
}
|
|
200
|
+
else if (char === "\t") {
|
|
201
|
+
count += 4;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return count;
|
|
208
|
+
}
|
|
209
|
+
readString(line, start, lineNumber) {
|
|
210
|
+
let index = start + 1;
|
|
211
|
+
let value = "";
|
|
212
|
+
let escaped = false;
|
|
213
|
+
while (index < line.length) {
|
|
214
|
+
const char = line[index] ?? "";
|
|
215
|
+
if (escaped) {
|
|
216
|
+
value += char;
|
|
217
|
+
escaped = false;
|
|
218
|
+
index += 1;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (char === "\\") {
|
|
222
|
+
escaped = true;
|
|
223
|
+
index += 1;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (char === "\"") {
|
|
227
|
+
this.push("STRING", value, lineNumber, start + 1, /\{[^}]+\}/u.test(value));
|
|
228
|
+
return index + 1;
|
|
229
|
+
}
|
|
230
|
+
value += char;
|
|
231
|
+
index += 1;
|
|
232
|
+
}
|
|
233
|
+
throw new NewtError({
|
|
234
|
+
code: "NEWT_E001",
|
|
235
|
+
message: "This string starts with a quote but never closes.",
|
|
236
|
+
suggestion: "Add a closing double quote at the end of the text.",
|
|
237
|
+
line: lineNumber,
|
|
238
|
+
column: start + 1,
|
|
239
|
+
sourceLine: line,
|
|
240
|
+
length: Math.max(1, line.length - start)
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
readNumber(line, start, lineNumber) {
|
|
244
|
+
let index = start;
|
|
245
|
+
while (/[0-9.]/u.test(line[index] ?? "")) {
|
|
246
|
+
index += 1;
|
|
247
|
+
}
|
|
248
|
+
this.push("NUMBER", line.slice(start, index), lineNumber, start + 1);
|
|
249
|
+
return index;
|
|
250
|
+
}
|
|
251
|
+
readWord(line, start, lineNumber) {
|
|
252
|
+
let index = start;
|
|
253
|
+
while (/[A-Za-z0-9_]/u.test(line[index] ?? "")) {
|
|
254
|
+
index += 1;
|
|
255
|
+
}
|
|
256
|
+
const value = line.slice(start, index);
|
|
257
|
+
this.push(keywords.has(value) ? "KEYWORD" : "IDENTIFIER", value, lineNumber, start + 1);
|
|
258
|
+
return index;
|
|
259
|
+
}
|
|
260
|
+
singleCharToken(char) {
|
|
261
|
+
switch (char) {
|
|
262
|
+
case ":":
|
|
263
|
+
return "COLON";
|
|
264
|
+
case "=":
|
|
265
|
+
return "EQUALS";
|
|
266
|
+
case "(":
|
|
267
|
+
return "LPAREN";
|
|
268
|
+
case ")":
|
|
269
|
+
return "RPAREN";
|
|
270
|
+
case "[":
|
|
271
|
+
return "LBRACKET";
|
|
272
|
+
case "]":
|
|
273
|
+
return "RBRACKET";
|
|
274
|
+
case ",":
|
|
275
|
+
return "COMMA";
|
|
276
|
+
case ".":
|
|
277
|
+
return "DOT";
|
|
278
|
+
default:
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
push(type, value, line, column, interpolated) {
|
|
283
|
+
this.tokens.push({ type, value, line, column, interpolated });
|
|
284
|
+
}
|
|
285
|
+
}
|