@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.
@@ -0,0 +1,191 @@
1
+ import type {
2
+ BotDecl,
3
+ Expression,
4
+ Program,
5
+ Statement,
6
+ TimerDecl,
7
+ TopLevelNode
8
+ } from "./ast.js";
9
+ import { makeCatalogError, NewtError } from "./errors.js";
10
+
11
+ const builtIns = new Set([
12
+ "user.name",
13
+ "user.id",
14
+ "user.mention",
15
+ "message.content",
16
+ "channel.name",
17
+ "server.name",
18
+ "server.members",
19
+ "args",
20
+ "target"
21
+ ]);
22
+
23
+ export function validate(program: Program, source = ""): NewtError[] {
24
+ const validator = new Validator(source);
25
+ return validator.validate(program);
26
+ }
27
+
28
+ class Validator {
29
+ private readonly errors: NewtError[] = [];
30
+ private hasBotName = false;
31
+ private hasBotToken = false;
32
+
33
+ constructor(private readonly source: string) {}
34
+
35
+ validate(program: Program): NewtError[] {
36
+ for (const node of program.body) {
37
+ this.visitTopLevel(node);
38
+ }
39
+
40
+ if (!this.hasBotName) {
41
+ this.errors.push(makeCatalogError("NEWT_E008", 1, 1, this.line(1)));
42
+ }
43
+ if (!this.hasBotToken) {
44
+ this.errors.push(makeCatalogError("NEWT_E009", 1, 1, this.line(1)));
45
+ }
46
+
47
+ return this.errors;
48
+ }
49
+
50
+ private visitTopLevel(node: TopLevelNode): void {
51
+ if (node.type === "BotDecl") {
52
+ this.visitBotDecl(node);
53
+ return;
54
+ }
55
+
56
+ if (node.type === "EveryTimerDecl" || node.type === "DailyTimerDecl") {
57
+ this.visitTimer(node);
58
+ return;
59
+ }
60
+
61
+ this.visitStatements(node.body, new Set());
62
+ }
63
+
64
+ private visitBotDecl(node: BotDecl): void {
65
+ if (node.kind === "name") {
66
+ this.hasBotName = true;
67
+ }
68
+ if (node.kind === "token") {
69
+ this.hasBotToken = true;
70
+ if (!node.fromEnv) {
71
+ this.errors.push(makeCatalogError("NEWT_E005", node.loc.line, node.loc.column, this.line(node.loc.line)));
72
+ }
73
+ }
74
+ }
75
+
76
+ private visitTimer(node: TimerDecl): void {
77
+ if (node.type === "EveryTimerDecl" && node.amount.value <= 0) {
78
+ this.errors.push(makeCatalogError("NEWT_E014", node.loc.line, node.loc.column, this.line(node.loc.line)));
79
+ }
80
+ this.visitStatements(node.body, new Set());
81
+ }
82
+
83
+ private visitStatements(statements: Statement[], scope: Set<string>, inTry = false): void {
84
+ for (const statement of statements) {
85
+ switch (statement.type) {
86
+ case "LetDecl":
87
+ this.visitExpression(statement.value, scope, inTry);
88
+ scope.add(statement.name);
89
+ break;
90
+ case "ReplyStatement":
91
+ case "SayStatement":
92
+ case "ExpressionStatement":
93
+ this.visitExpression("message" in statement ? statement.message : statement.expression, scope, inTry);
94
+ break;
95
+ case "SayEmbedStatement":
96
+ if (statement.embed.title) this.visitExpression(statement.embed.title, scope, inTry);
97
+ if (statement.embed.description) this.visitExpression(statement.embed.description, scope, inTry);
98
+ for (const field of statement.embed.fields) {
99
+ this.visitExpression(field.name, scope, inTry);
100
+ this.visitExpression(field.value, scope, inTry);
101
+ }
102
+ break;
103
+ case "StoreStatement":
104
+ this.visitExpression(statement.namespace, scope, inTry);
105
+ this.visitExpression(statement.value, scope, inTry);
106
+ break;
107
+ case "IfStatement":
108
+ this.visitExpression(statement.condition, scope, inTry);
109
+ this.visitStatements(statement.consequent, new Set(scope), inTry);
110
+ this.visitStatements(statement.alternate, new Set(scope), inTry);
111
+ break;
112
+ case "ForEachStatement":
113
+ this.visitExpression(statement.iterable, scope, inTry);
114
+ this.visitStatements(statement.body, new Set([...scope, statement.itemName]), inTry);
115
+ break;
116
+ case "RequireRoleStatement":
117
+ case "GiveRoleStatement":
118
+ case "RemoveRoleStatement":
119
+ if ("role" in statement && statement.role.value.length === 0) {
120
+ this.errors.push(makeCatalogError("NEWT_E015", statement.loc.line, statement.loc.column, this.line(statement.loc.line)));
121
+ }
122
+ if ("subject" in statement) this.visitExpression(statement.subject, scope, inTry);
123
+ break;
124
+ case "MuteStatement":
125
+ case "KickStatement":
126
+ case "BanStatement":
127
+ this.visitExpression(statement.subject, scope, inTry);
128
+ break;
129
+ case "TryCatchStatement":
130
+ this.visitStatements(statement.body, new Set(scope), true);
131
+ this.visitStatements(statement.errorHandler, new Set(scope), true);
132
+ break;
133
+ case "WaitStatement":
134
+ if (statement.duration.amount.value <= 0) {
135
+ this.errors.push(makeCatalogError("NEWT_E014", statement.loc.line, statement.loc.column, this.line(statement.loc.line)));
136
+ }
137
+ break;
138
+ default:
139
+ break;
140
+ }
141
+ }
142
+ }
143
+
144
+ private visitExpression(expression: Expression, scope: Set<string>, inTry: boolean): void {
145
+ switch (expression.type) {
146
+ case "IdentifierExpr":
147
+ if (!scope.has(expression.name) && !["user", "message", "channel", "server", "args", "target", "member", "current"].includes(expression.name)) {
148
+ this.errors.push(makeCatalogError("NEWT_E007", expression.loc.line, expression.loc.column, this.line(expression.loc.line), {
149
+ length: expression.name.length
150
+ }));
151
+ }
152
+ break;
153
+ case "MemberExpr": {
154
+ const path = expression.path.join(".");
155
+ const isKnown = builtIns.has(path) || scope.has(expression.path[0] ?? "") || expression.path[0] === "member";
156
+ if (!isKnown) {
157
+ this.errors.push(makeCatalogError("NEWT_E013", expression.loc.line, expression.loc.column, this.line(expression.loc.line), {
158
+ length: path.length
159
+ }));
160
+ }
161
+ break;
162
+ }
163
+ case "LoadExpr":
164
+ this.visitExpression(expression.namespace, scope, inTry);
165
+ if (expression.fallback) this.visitExpression(expression.fallback, scope, inTry);
166
+ break;
167
+ case "FetchExpr":
168
+ if (!inTry) {
169
+ this.errors.push(makeCatalogError("NEWT_E011", expression.loc.line, expression.loc.column, this.line(expression.loc.line)));
170
+ }
171
+ this.visitExpression(expression.url, scope, inTry);
172
+ break;
173
+ case "BinaryExpr":
174
+ this.visitExpression(expression.left, scope, inTry);
175
+ this.visitExpression(expression.right, scope, inTry);
176
+ break;
177
+ case "UnaryExpr":
178
+ this.visitExpression(expression.argument, scope, inTry);
179
+ break;
180
+ case "CallExpr":
181
+ for (const arg of expression.args) this.visitExpression(arg, scope, inTry);
182
+ break;
183
+ default:
184
+ break;
185
+ }
186
+ }
187
+
188
+ private line(line: number): string {
189
+ return this.source.split(/\r?\n/)[line - 1] ?? "";
190
+ }
191
+ }
@@ -0,0 +1,42 @@
1
+ import assert from "node:assert/strict";
2
+ import { readdirSync, readFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { test } from "node:test";
5
+ import { compile, tokenize } from "../src/index.js";
6
+
7
+ const examplesDir = resolve("../../examples");
8
+
9
+ test("all example programs compile", () => {
10
+ const files = readdirSync(examplesDir).filter((file) => file.endsWith(".newt"));
11
+ assert.deepEqual(files.sort(), [
12
+ "hello-world.newt",
13
+ "moderation-bot.newt",
14
+ "points-bot.newt",
15
+ "welcome-bot.newt"
16
+ ]);
17
+
18
+ for (const file of files) {
19
+ const source = readFileSync(join(examplesDir, file), "utf8");
20
+ const result = compile(source, file);
21
+ assert.equal(result.success, true, `${file} should compile`);
22
+ if (result.success) {
23
+ assert.match(result.botJs, /new Client/);
24
+ assert.match(result.packageJson, /discord\.js/);
25
+ }
26
+ }
27
+ });
28
+
29
+ test("lexer emits indentation and interpolated string tokens", () => {
30
+ const tokens = tokenize(`on command "hello":\n reply "Hi {user.name}!"\n`);
31
+ assert.ok(tokens.some((token) => token.type === "INDENT"));
32
+ assert.ok(tokens.some((token) => token.type === "DEDENT"));
33
+ assert.ok(tokens.some((token) => token.type === "STRING" && token.interpolated));
34
+ });
35
+
36
+ test("compiler reports missing bot token", () => {
37
+ const result = compile(`bot name "NoToken"\n\non ready:\n say "Ready"\n`, "broken.newt");
38
+ assert.equal(result.success, false);
39
+ if (!result.success) {
40
+ assert.ok(result.errors.some((error) => error.code === "NEWT_E009"));
41
+ }
42
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "declaration": true,
7
+ "outDir": "dist",
8
+ "rootDir": ".",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "skipLibCheck": true
13
+ },
14
+ "include": [
15
+ "src/**/*.ts",
16
+ "test/**/*.ts"
17
+ ]
18
+ }