@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,220 @@
|
|
|
1
|
+
export interface SourceLocation {
|
|
2
|
+
line: number;
|
|
3
|
+
column: number;
|
|
4
|
+
}
|
|
5
|
+
export interface BaseNode {
|
|
6
|
+
type: string;
|
|
7
|
+
loc: SourceLocation;
|
|
8
|
+
}
|
|
9
|
+
export interface Program extends BaseNode {
|
|
10
|
+
type: "Program";
|
|
11
|
+
body: TopLevelNode[];
|
|
12
|
+
}
|
|
13
|
+
export type TopLevelNode = BotDecl | Handler | TimerDecl;
|
|
14
|
+
export type BotDeclKind = "name" | "prefix" | "token";
|
|
15
|
+
export interface BotDecl extends BaseNode {
|
|
16
|
+
type: "BotDecl";
|
|
17
|
+
kind: BotDeclKind;
|
|
18
|
+
value: StringLiteral;
|
|
19
|
+
fromEnv: boolean;
|
|
20
|
+
}
|
|
21
|
+
export type Handler = ReadyHandler | CommandHandler | MessageContainsHandler | JoinHandler | LeaveHandler | ReactionAddHandler;
|
|
22
|
+
export interface ReadyHandler extends BaseNode {
|
|
23
|
+
type: "ReadyHandler";
|
|
24
|
+
body: Statement[];
|
|
25
|
+
}
|
|
26
|
+
export interface CommandHandler extends BaseNode {
|
|
27
|
+
type: "CommandHandler";
|
|
28
|
+
command: string;
|
|
29
|
+
body: Statement[];
|
|
30
|
+
}
|
|
31
|
+
export interface MessageContainsHandler extends BaseNode {
|
|
32
|
+
type: "MessageContainsHandler";
|
|
33
|
+
needle: StringLiteral;
|
|
34
|
+
body: Statement[];
|
|
35
|
+
}
|
|
36
|
+
export interface JoinHandler extends BaseNode {
|
|
37
|
+
type: "JoinHandler";
|
|
38
|
+
body: Statement[];
|
|
39
|
+
}
|
|
40
|
+
export interface LeaveHandler extends BaseNode {
|
|
41
|
+
type: "LeaveHandler";
|
|
42
|
+
body: Statement[];
|
|
43
|
+
}
|
|
44
|
+
export interface ReactionAddHandler extends BaseNode {
|
|
45
|
+
type: "ReactionAddHandler";
|
|
46
|
+
emoji: StringLiteral;
|
|
47
|
+
body: Statement[];
|
|
48
|
+
}
|
|
49
|
+
export type TimerDecl = EveryTimerDecl | DailyTimerDecl;
|
|
50
|
+
export interface EveryTimerDecl extends BaseNode {
|
|
51
|
+
type: "EveryTimerDecl";
|
|
52
|
+
amount: NumberLiteral;
|
|
53
|
+
unit: TimeUnit;
|
|
54
|
+
body: Statement[];
|
|
55
|
+
}
|
|
56
|
+
export interface DailyTimerDecl extends BaseNode {
|
|
57
|
+
type: "DailyTimerDecl";
|
|
58
|
+
time: StringLiteral;
|
|
59
|
+
body: Statement[];
|
|
60
|
+
}
|
|
61
|
+
export type TimeUnit = "second" | "seconds" | "minute" | "minutes" | "hour" | "hours" | "day" | "days";
|
|
62
|
+
export type Statement = ReplyStatement | SayStatement | SayEmbedStatement | LetDecl | StoreStatement | IfStatement | ForEachStatement | RequireRoleStatement | GiveRoleStatement | RemoveRoleStatement | MuteStatement | KickStatement | BanStatement | PinStatement | DeleteKeyStatement | WaitStatement | TryCatchStatement | ExpressionStatement;
|
|
63
|
+
export interface ReplyStatement extends BaseNode {
|
|
64
|
+
type: "ReplyStatement";
|
|
65
|
+
message: Expression;
|
|
66
|
+
}
|
|
67
|
+
export interface SayStatement extends BaseNode {
|
|
68
|
+
type: "SayStatement";
|
|
69
|
+
message: Expression;
|
|
70
|
+
channel?: StringLiteral;
|
|
71
|
+
}
|
|
72
|
+
export interface SayEmbedStatement extends BaseNode {
|
|
73
|
+
type: "SayEmbedStatement";
|
|
74
|
+
embed: EmbedBlock;
|
|
75
|
+
}
|
|
76
|
+
export interface EmbedBlock extends BaseNode {
|
|
77
|
+
type: "EmbedBlock";
|
|
78
|
+
title?: StringLiteral;
|
|
79
|
+
description?: StringLiteral;
|
|
80
|
+
color?: ColorLiteral;
|
|
81
|
+
fields: EmbedField[];
|
|
82
|
+
}
|
|
83
|
+
export interface EmbedField extends BaseNode {
|
|
84
|
+
type: "EmbedField";
|
|
85
|
+
name: StringLiteral;
|
|
86
|
+
value: StringLiteral;
|
|
87
|
+
}
|
|
88
|
+
export interface LetDecl extends BaseNode {
|
|
89
|
+
type: "LetDecl";
|
|
90
|
+
name: string;
|
|
91
|
+
value: Expression;
|
|
92
|
+
}
|
|
93
|
+
export interface StoreStatement extends BaseNode {
|
|
94
|
+
type: "StoreStatement";
|
|
95
|
+
namespace: Expression;
|
|
96
|
+
key: string;
|
|
97
|
+
value: Expression;
|
|
98
|
+
}
|
|
99
|
+
export interface IfStatement extends BaseNode {
|
|
100
|
+
type: "IfStatement";
|
|
101
|
+
condition: Expression;
|
|
102
|
+
consequent: Statement[];
|
|
103
|
+
alternate: Statement[];
|
|
104
|
+
}
|
|
105
|
+
export interface ForEachStatement extends BaseNode {
|
|
106
|
+
type: "ForEachStatement";
|
|
107
|
+
itemName: string;
|
|
108
|
+
iterable: Expression;
|
|
109
|
+
body: Statement[];
|
|
110
|
+
}
|
|
111
|
+
export interface RequireRoleStatement extends BaseNode {
|
|
112
|
+
type: "RequireRoleStatement";
|
|
113
|
+
role: StringLiteral;
|
|
114
|
+
}
|
|
115
|
+
export interface GiveRoleStatement extends BaseNode {
|
|
116
|
+
type: "GiveRoleStatement";
|
|
117
|
+
subject: Expression;
|
|
118
|
+
role: StringLiteral;
|
|
119
|
+
}
|
|
120
|
+
export interface RemoveRoleStatement extends BaseNode {
|
|
121
|
+
type: "RemoveRoleStatement";
|
|
122
|
+
subject: Expression;
|
|
123
|
+
role: StringLiteral;
|
|
124
|
+
}
|
|
125
|
+
export interface MuteStatement extends BaseNode {
|
|
126
|
+
type: "MuteStatement";
|
|
127
|
+
subject: Expression;
|
|
128
|
+
duration?: DurationLiteral;
|
|
129
|
+
}
|
|
130
|
+
export interface KickStatement extends BaseNode {
|
|
131
|
+
type: "KickStatement";
|
|
132
|
+
subject: Expression;
|
|
133
|
+
}
|
|
134
|
+
export interface BanStatement extends BaseNode {
|
|
135
|
+
type: "BanStatement";
|
|
136
|
+
subject: Expression;
|
|
137
|
+
}
|
|
138
|
+
export interface PinStatement extends BaseNode {
|
|
139
|
+
type: "PinStatement";
|
|
140
|
+
target: Expression;
|
|
141
|
+
}
|
|
142
|
+
export interface DeleteKeyStatement extends BaseNode {
|
|
143
|
+
type: "DeleteKeyStatement";
|
|
144
|
+
namespace: Expression;
|
|
145
|
+
key: string;
|
|
146
|
+
}
|
|
147
|
+
export interface WaitStatement extends BaseNode {
|
|
148
|
+
type: "WaitStatement";
|
|
149
|
+
duration: DurationLiteral;
|
|
150
|
+
}
|
|
151
|
+
export interface TryCatchStatement extends BaseNode {
|
|
152
|
+
type: "TryCatchStatement";
|
|
153
|
+
body: Statement[];
|
|
154
|
+
errorHandler: Statement[];
|
|
155
|
+
}
|
|
156
|
+
export interface ExpressionStatement extends BaseNode {
|
|
157
|
+
type: "ExpressionStatement";
|
|
158
|
+
expression: Expression;
|
|
159
|
+
}
|
|
160
|
+
export type Expression = StringLiteral | NumberLiteral | BooleanLiteral | ColorLiteral | IdentifierExpr | MemberExpr | ArgsIndexExpr | LoadExpr | FetchExpr | BinaryExpr | UnaryExpr | CallExpr;
|
|
161
|
+
export interface StringLiteral extends BaseNode {
|
|
162
|
+
type: "StringLiteral";
|
|
163
|
+
value: string;
|
|
164
|
+
interpolated: boolean;
|
|
165
|
+
}
|
|
166
|
+
export interface NumberLiteral extends BaseNode {
|
|
167
|
+
type: "NumberLiteral";
|
|
168
|
+
value: number;
|
|
169
|
+
}
|
|
170
|
+
export interface BooleanLiteral extends BaseNode {
|
|
171
|
+
type: "BooleanLiteral";
|
|
172
|
+
value: boolean;
|
|
173
|
+
}
|
|
174
|
+
export interface ColorLiteral extends BaseNode {
|
|
175
|
+
type: "ColorLiteral";
|
|
176
|
+
value: string;
|
|
177
|
+
}
|
|
178
|
+
export interface IdentifierExpr extends BaseNode {
|
|
179
|
+
type: "IdentifierExpr";
|
|
180
|
+
name: string;
|
|
181
|
+
}
|
|
182
|
+
export interface MemberExpr extends BaseNode {
|
|
183
|
+
type: "MemberExpr";
|
|
184
|
+
path: string[];
|
|
185
|
+
}
|
|
186
|
+
export interface ArgsIndexExpr extends BaseNode {
|
|
187
|
+
type: "ArgsIndexExpr";
|
|
188
|
+
index: number;
|
|
189
|
+
}
|
|
190
|
+
export interface LoadExpr extends BaseNode {
|
|
191
|
+
type: "LoadExpr";
|
|
192
|
+
namespace: Expression;
|
|
193
|
+
key: string;
|
|
194
|
+
fallback?: Expression;
|
|
195
|
+
}
|
|
196
|
+
export interface FetchExpr extends BaseNode {
|
|
197
|
+
type: "FetchExpr";
|
|
198
|
+
url: Expression;
|
|
199
|
+
}
|
|
200
|
+
export interface BinaryExpr extends BaseNode {
|
|
201
|
+
type: "BinaryExpr";
|
|
202
|
+
operator: string;
|
|
203
|
+
left: Expression;
|
|
204
|
+
right: Expression;
|
|
205
|
+
}
|
|
206
|
+
export interface UnaryExpr extends BaseNode {
|
|
207
|
+
type: "UnaryExpr";
|
|
208
|
+
operator: string;
|
|
209
|
+
argument: Expression;
|
|
210
|
+
}
|
|
211
|
+
export interface CallExpr extends BaseNode {
|
|
212
|
+
type: "CallExpr";
|
|
213
|
+
callee: string;
|
|
214
|
+
args: Expression[];
|
|
215
|
+
}
|
|
216
|
+
export interface DurationLiteral extends BaseNode {
|
|
217
|
+
type: "DurationLiteral";
|
|
218
|
+
amount: NumberLiteral;
|
|
219
|
+
unit: TimeUnit;
|
|
220
|
+
}
|
package/dist/src/ast.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
export function generate(program) {
|
|
2
|
+
const botName = getBotValue(program, "name") ?? "NewtBot";
|
|
3
|
+
const prefix = getBotValue(program, "prefix") ?? "!";
|
|
4
|
+
const tokenDecl = program.body.find((node) => node.type === "BotDecl" && node.kind === "token");
|
|
5
|
+
const tokenExpr = tokenDecl?.fromEnv ? `process.env.${tokenDecl.value.value}` : JSON.stringify(tokenDecl?.value.value ?? "");
|
|
6
|
+
const handlers = program.body
|
|
7
|
+
.filter((node) => node.type.endsWith("Handler") || node.type.endsWith("TimerDecl"))
|
|
8
|
+
.map((node) => emitTopLevel(node, prefix))
|
|
9
|
+
.join("\n\n");
|
|
10
|
+
const botJs = `import { Client, EmbedBuilder, GatewayIntentBits, Partials } from "discord.js";
|
|
11
|
+
import Database from "better-sqlite3";
|
|
12
|
+
import axios from "axios";
|
|
13
|
+
|
|
14
|
+
const client = new Client({
|
|
15
|
+
intents: [
|
|
16
|
+
GatewayIntentBits.Guilds,
|
|
17
|
+
GatewayIntentBits.GuildMessages,
|
|
18
|
+
GatewayIntentBits.GuildMembers,
|
|
19
|
+
GatewayIntentBits.MessageContent,
|
|
20
|
+
GatewayIntentBits.GuildMessageReactions
|
|
21
|
+
],
|
|
22
|
+
partials: [Partials.Message, Partials.Channel, Partials.Reaction]
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const db = new Database("newt-store.sqlite");
|
|
26
|
+
db.exec("CREATE TABLE IF NOT EXISTS store (namespace TEXT NOT NULL, key TEXT NOT NULL, value TEXT, PRIMARY KEY(namespace, key))");
|
|
27
|
+
const botName = ${JSON.stringify(botName)};
|
|
28
|
+
const prefix = ${JSON.stringify(prefix)};
|
|
29
|
+
|
|
30
|
+
function saveValue(namespace, key, value) {
|
|
31
|
+
db.prepare("INSERT OR REPLACE INTO store(namespace, key, value) VALUES (?, ?, ?)").run(String(namespace), String(key), JSON.stringify(value));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadValue(namespace, key, fallback = undefined) {
|
|
35
|
+
const row = db.prepare("SELECT value FROM store WHERE namespace = ? AND key = ?").get(String(namespace), String(key));
|
|
36
|
+
return row ? JSON.parse(row.value) : fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function findChannel(guild, name) {
|
|
40
|
+
return guild?.channels?.cache?.find((channel) => channel.name === name);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function findRole(guild, name) {
|
|
44
|
+
return guild?.roles?.cache?.find((role) => role.name === name);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
${handlers}
|
|
48
|
+
|
|
49
|
+
client.login(${tokenExpr});
|
|
50
|
+
`;
|
|
51
|
+
return {
|
|
52
|
+
botJs,
|
|
53
|
+
packageJson: JSON.stringify({
|
|
54
|
+
type: "module",
|
|
55
|
+
scripts: { start: "node bot.js" },
|
|
56
|
+
dependencies: {
|
|
57
|
+
"axios": "^1.7.0",
|
|
58
|
+
"better-sqlite3": "^11.0.0",
|
|
59
|
+
"discord.js": "^14.15.0"
|
|
60
|
+
}
|
|
61
|
+
}, null, 2)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function getBotValue(program, kind) {
|
|
65
|
+
return program.body.find((node) => node.type === "BotDecl" && node.kind === kind)?.value.value;
|
|
66
|
+
}
|
|
67
|
+
function emitTopLevel(node, prefix) {
|
|
68
|
+
switch (node.type) {
|
|
69
|
+
case "ReadyHandler":
|
|
70
|
+
return `client.once("ready", async () => {
|
|
71
|
+
for (const guild of client.guilds.cache.values()) {
|
|
72
|
+
const server = guild;
|
|
73
|
+
${emitStatements(node.body, " ", "guild")}
|
|
74
|
+
}
|
|
75
|
+
});`;
|
|
76
|
+
case "CommandHandler":
|
|
77
|
+
return `client.on("messageCreate", async (message) => {
|
|
78
|
+
if (message.author.bot) return;
|
|
79
|
+
if (!message.content.startsWith(prefix + ${JSON.stringify(node.command)})) return;
|
|
80
|
+
const args = message.content.slice((prefix + ${JSON.stringify(node.command)}).length).trim().split(/\\s+/).filter(Boolean);
|
|
81
|
+
const user = message.author;
|
|
82
|
+
const channel = message.channel;
|
|
83
|
+
const server = message.guild;
|
|
84
|
+
const target = message.mentions.members.first();
|
|
85
|
+
${emitStatements(node.body, " ", "message")}
|
|
86
|
+
});`;
|
|
87
|
+
case "MessageContainsHandler":
|
|
88
|
+
return `client.on("messageCreate", async (message) => {
|
|
89
|
+
if (message.author.bot) return;
|
|
90
|
+
if (!message.content.includes(${emitExpression(node.needle)})) return;
|
|
91
|
+
const user = message.author;
|
|
92
|
+
const channel = message.channel;
|
|
93
|
+
const server = message.guild;
|
|
94
|
+
${emitStatements(node.body, " ", "message")}
|
|
95
|
+
});`;
|
|
96
|
+
case "JoinHandler":
|
|
97
|
+
return `client.on("guildMemberAdd", async (member) => {
|
|
98
|
+
const user = member.user;
|
|
99
|
+
const server = member.guild;
|
|
100
|
+
const channel = findChannel(server, "general");
|
|
101
|
+
${emitStatements(node.body, " ", "member")}
|
|
102
|
+
});`;
|
|
103
|
+
case "LeaveHandler":
|
|
104
|
+
return `client.on("guildMemberRemove", async (member) => {
|
|
105
|
+
const user = member.user;
|
|
106
|
+
const server = member.guild;
|
|
107
|
+
${emitStatements(node.body, " ", "member")}
|
|
108
|
+
});`;
|
|
109
|
+
case "ReactionAddHandler":
|
|
110
|
+
return `client.on("messageReactionAdd", async (reaction, user) => {
|
|
111
|
+
if (reaction.emoji.name !== ${emitExpression(node.emoji)}) return;
|
|
112
|
+
const message = reaction.message;
|
|
113
|
+
const channel = message.channel;
|
|
114
|
+
const server = message.guild;
|
|
115
|
+
${emitStatements(node.body, " ", "message")}
|
|
116
|
+
});`;
|
|
117
|
+
case "EveryTimerDecl":
|
|
118
|
+
return `setInterval(async () => {\n${emitStatements(node.body, " ", "message")}\n}, ${durationMs(node.amount.value, node.unit)});`;
|
|
119
|
+
case "DailyTimerDecl":
|
|
120
|
+
return `setInterval(async () => {
|
|
121
|
+
const now = new Date();
|
|
122
|
+
const hhmm = now.toTimeString().slice(0, 5);
|
|
123
|
+
if (hhmm !== ${emitExpression(node.time)}) return;
|
|
124
|
+
${emitStatements(node.body, " ", "message")}
|
|
125
|
+
}, 60000);`;
|
|
126
|
+
default:
|
|
127
|
+
return "";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function emitStatements(statements, indent, triggerName) {
|
|
131
|
+
return statements.map((statement) => emitStatement(statement, indent, triggerName)).join("\n");
|
|
132
|
+
}
|
|
133
|
+
function emitStatement(statement, indent, triggerName) {
|
|
134
|
+
switch (statement.type) {
|
|
135
|
+
case "ReplyStatement":
|
|
136
|
+
return `${indent}await ${triggerName}.reply(${emitExpression(statement.message)});`;
|
|
137
|
+
case "SayStatement": {
|
|
138
|
+
if (statement.channel) {
|
|
139
|
+
return `${indent}await findChannel(${triggerName === "guild" ? "server" : `server ?? ${triggerName}.guild`}, ${emitExpression(statement.channel)})?.send(${emitExpression(statement.message)});`;
|
|
140
|
+
}
|
|
141
|
+
return `${indent}await (${triggerName}.channel ?? channel)?.send(${emitExpression(statement.message)});`;
|
|
142
|
+
}
|
|
143
|
+
case "SayEmbedStatement":
|
|
144
|
+
if (triggerName === "member") {
|
|
145
|
+
return `${indent}await findChannel(server, "general")?.send({ embeds: [${emitEmbed(statement.embed)}] });`;
|
|
146
|
+
}
|
|
147
|
+
return `${indent}await (${triggerName}.channel ?? channel)?.send({ embeds: [${emitEmbed(statement.embed)}] });`;
|
|
148
|
+
case "LetDecl":
|
|
149
|
+
return `${indent}const ${statement.name} = ${emitExpression(statement.value)};`;
|
|
150
|
+
case "StoreStatement":
|
|
151
|
+
return `${indent}saveValue(${emitExpression(statement.namespace)}, ${JSON.stringify(statement.key)}, ${emitExpression(statement.value)});`;
|
|
152
|
+
case "IfStatement":
|
|
153
|
+
return `${indent}if (${emitExpression(statement.condition)}) {\n${emitStatements(statement.consequent, `${indent} `, triggerName)}\n${indent}}${statement.alternate.length ? ` else {\n${emitStatements(statement.alternate, `${indent} `, triggerName)}\n${indent}}` : ""}`;
|
|
154
|
+
case "ForEachStatement":
|
|
155
|
+
return `${indent}for (const ${statement.itemName} of (server?.members?.cache?.values?.() ?? [])) {\n${emitStatements(statement.body, `${indent} `, triggerName)}\n${indent}}`;
|
|
156
|
+
case "RequireRoleStatement":
|
|
157
|
+
return `${indent}if (!${triggerName}.member?.roles?.cache?.some((role) => role.name === ${emitExpression(statement.role)})) return;`;
|
|
158
|
+
case "GiveRoleStatement":
|
|
159
|
+
if (triggerName === "member") {
|
|
160
|
+
return `${indent}await ${triggerName}.roles?.add(findRole(server, ${emitExpression(statement.role)}));`;
|
|
161
|
+
}
|
|
162
|
+
return `${indent}await (${emitExpression(statement.subject)}?.roles ?? ${triggerName}.member?.roles)?.add(findRole(server ?? ${triggerName}.guild, ${emitExpression(statement.role)}));`;
|
|
163
|
+
case "RemoveRoleStatement":
|
|
164
|
+
return `${indent}await (${emitExpression(statement.subject)}?.roles ?? ${triggerName}.member?.roles)?.remove(findRole(server ?? ${triggerName}.guild, ${emitExpression(statement.role)}));`;
|
|
165
|
+
case "MuteStatement":
|
|
166
|
+
return `${indent}await ${emitExpression(statement.subject)}?.timeout?.(${statement.duration ? durationMs(statement.duration.amount.value, statement.duration.unit) : 600000});`;
|
|
167
|
+
case "KickStatement":
|
|
168
|
+
return `${indent}await ${emitExpression(statement.subject)}?.kick?.();`;
|
|
169
|
+
case "BanStatement":
|
|
170
|
+
return `${indent}await ${emitExpression(statement.subject)}?.ban?.();`;
|
|
171
|
+
case "TryCatchStatement":
|
|
172
|
+
return `${indent}try {\n${emitStatements(statement.body, `${indent} `, triggerName)}\n${indent}} catch (error) {\n${emitStatements(statement.errorHandler, `${indent} `, triggerName)}\n${indent}}`;
|
|
173
|
+
case "WaitStatement":
|
|
174
|
+
return `${indent}await new Promise((resolve) => setTimeout(resolve, ${durationMs(statement.duration.amount.value, statement.duration.unit)}));`;
|
|
175
|
+
case "ExpressionStatement":
|
|
176
|
+
return `${indent}${emitExpression(statement.expression)};`;
|
|
177
|
+
default:
|
|
178
|
+
return `${indent}// TODO: ${statement.type}`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function emitEmbed(embed) {
|
|
182
|
+
const lines = ["new EmbedBuilder()"];
|
|
183
|
+
if (embed.title)
|
|
184
|
+
lines.push(`.setTitle(${emitExpression(embed.title)})`);
|
|
185
|
+
if (embed.description)
|
|
186
|
+
lines.push(`.setDescription(${emitExpression(embed.description)})`);
|
|
187
|
+
if (embed.color)
|
|
188
|
+
lines.push(`.setColor(${JSON.stringify(embed.color.value)})`);
|
|
189
|
+
for (const field of embed.fields) {
|
|
190
|
+
lines.push(`.addFields({ name: ${emitExpression(field.name)}, value: ${emitExpression(field.value)} })`);
|
|
191
|
+
}
|
|
192
|
+
return lines.join("");
|
|
193
|
+
}
|
|
194
|
+
function emitExpression(expression) {
|
|
195
|
+
switch (expression.type) {
|
|
196
|
+
case "StringLiteral":
|
|
197
|
+
if (expression.interpolated) {
|
|
198
|
+
// Replace {expr} placeholders with properly emitted expressions
|
|
199
|
+
let result = expression.value.replace(/`/g, "\\`");
|
|
200
|
+
// Find all {expr} patterns and replace them
|
|
201
|
+
result = result.replace(/\{([^}]+)\}/g, (match, exprContent) => {
|
|
202
|
+
// Map user.name to user.username for Discord.js v14 compatibility
|
|
203
|
+
if (exprContent === "user.name") {
|
|
204
|
+
return "${user.username}";
|
|
205
|
+
}
|
|
206
|
+
if (exprContent === "member.name") {
|
|
207
|
+
return "${member.user.username}";
|
|
208
|
+
}
|
|
209
|
+
return "${" + exprContent + "}";
|
|
210
|
+
});
|
|
211
|
+
return "`" + result + "`";
|
|
212
|
+
}
|
|
213
|
+
return JSON.stringify(expression.value);
|
|
214
|
+
case "NumberLiteral":
|
|
215
|
+
return String(expression.value);
|
|
216
|
+
case "BooleanLiteral":
|
|
217
|
+
return String(expression.value);
|
|
218
|
+
case "ColorLiteral":
|
|
219
|
+
return JSON.stringify(expression.value);
|
|
220
|
+
case "IdentifierExpr":
|
|
221
|
+
return expression.name;
|
|
222
|
+
case "MemberExpr":
|
|
223
|
+
// Map user.name to user.username for Discord.js v14 compatibility
|
|
224
|
+
if (expression.path.join(".") === "user.name") {
|
|
225
|
+
return "user.username";
|
|
226
|
+
}
|
|
227
|
+
// Map member.name to member.user.username for Discord.js v14 compatibility
|
|
228
|
+
if (expression.path.join(".") === "member.name") {
|
|
229
|
+
return "member.user.username";
|
|
230
|
+
}
|
|
231
|
+
return expression.path.join(".");
|
|
232
|
+
case "ArgsIndexExpr":
|
|
233
|
+
return `args[${expression.index}]`;
|
|
234
|
+
case "LoadExpr":
|
|
235
|
+
return `loadValue(${emitExpression(expression.namespace)}, ${JSON.stringify(expression.key)}${expression.fallback ? `, ${emitExpression(expression.fallback)}` : ""})`;
|
|
236
|
+
case "FetchExpr":
|
|
237
|
+
return `(await axios.get(${emitExpression(expression.url)})).data`;
|
|
238
|
+
case "BinaryExpr":
|
|
239
|
+
if (expression.operator === "or") {
|
|
240
|
+
return `(${emitExpression(expression.left)} ?? ${emitExpression(expression.right)})`;
|
|
241
|
+
}
|
|
242
|
+
if (expression.operator === "has") {
|
|
243
|
+
return `(message.member?.roles?.cache?.some((role) => role.name === ${emitExpression(expression.right)}) ?? false)`;
|
|
244
|
+
}
|
|
245
|
+
return `(${emitExpression(expression.left)} ${expression.operator} ${emitExpression(expression.right)})`;
|
|
246
|
+
case "UnaryExpr":
|
|
247
|
+
return `(${expression.operator}${emitExpression(expression.argument)})`;
|
|
248
|
+
case "CallExpr":
|
|
249
|
+
return `${expression.callee}(${expression.args.map(emitExpression).join(", ")})`;
|
|
250
|
+
default:
|
|
251
|
+
return "undefined";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function durationMs(amount, unit) {
|
|
255
|
+
const multipliers = {
|
|
256
|
+
second: 1000,
|
|
257
|
+
seconds: 1000,
|
|
258
|
+
minute: 60000,
|
|
259
|
+
minutes: 60000,
|
|
260
|
+
hour: 3600000,
|
|
261
|
+
hours: 3600000,
|
|
262
|
+
day: 86400000,
|
|
263
|
+
days: 86400000
|
|
264
|
+
};
|
|
265
|
+
return amount * (multipliers[unit] ?? 1000);
|
|
266
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type NewtErrorSeverity = "error" | "warning";
|
|
2
|
+
export type NewtErrorCode = "NEWT_E001" | "NEWT_E002" | "NEWT_E003" | "NEWT_E004" | "NEWT_E005" | "NEWT_E006" | "NEWT_E007" | "NEWT_E008" | "NEWT_E009" | "NEWT_E010" | "NEWT_E011" | "NEWT_E012" | "NEWT_E013" | "NEWT_E014" | "NEWT_E015";
|
|
3
|
+
export interface NewtErrorOptions {
|
|
4
|
+
code: NewtErrorCode;
|
|
5
|
+
message: string;
|
|
6
|
+
line: number;
|
|
7
|
+
column: number;
|
|
8
|
+
sourceLine?: string;
|
|
9
|
+
suggestion?: string;
|
|
10
|
+
severity?: NewtErrorSeverity;
|
|
11
|
+
filename?: string;
|
|
12
|
+
length?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class NewtError extends Error {
|
|
15
|
+
readonly code: NewtErrorCode;
|
|
16
|
+
readonly line: number;
|
|
17
|
+
readonly column: number;
|
|
18
|
+
readonly sourceLine?: string;
|
|
19
|
+
readonly suggestion?: string;
|
|
20
|
+
readonly severity: NewtErrorSeverity;
|
|
21
|
+
readonly filename?: string;
|
|
22
|
+
readonly length: number;
|
|
23
|
+
constructor(options: NewtErrorOptions);
|
|
24
|
+
}
|
|
25
|
+
export declare const errorCatalog: Record<NewtErrorCode, {
|
|
26
|
+
message: string;
|
|
27
|
+
suggestion: string;
|
|
28
|
+
severity?: NewtErrorSeverity;
|
|
29
|
+
}>;
|
|
30
|
+
export declare function makeCatalogError(code: NewtErrorCode, line: number, column: number, sourceLine?: string, overrides?: Partial<NewtErrorOptions>): NewtError;
|
|
31
|
+
export declare function formatError(error: NewtError, sourceOrFilename?: string): string;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export class NewtError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
line;
|
|
4
|
+
column;
|
|
5
|
+
sourceLine;
|
|
6
|
+
suggestion;
|
|
7
|
+
severity;
|
|
8
|
+
filename;
|
|
9
|
+
length;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super(options.message);
|
|
12
|
+
this.name = "NewtError";
|
|
13
|
+
this.code = options.code;
|
|
14
|
+
this.line = options.line;
|
|
15
|
+
this.column = options.column;
|
|
16
|
+
this.sourceLine = options.sourceLine;
|
|
17
|
+
this.suggestion = options.suggestion;
|
|
18
|
+
this.severity = options.severity ?? "error";
|
|
19
|
+
this.filename = options.filename;
|
|
20
|
+
this.length = options.length ?? 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export const errorCatalog = {
|
|
24
|
+
NEWT_E001: {
|
|
25
|
+
message: "Strings must be in quotes.",
|
|
26
|
+
suggestion: "Wrap the text in double quotes, like: reply \"Hello!\""
|
|
27
|
+
},
|
|
28
|
+
NEWT_E002: {
|
|
29
|
+
message: "Event handlers need a colon at the end.",
|
|
30
|
+
suggestion: "Try ending the line with a colon, like: on command \"hello\":"
|
|
31
|
+
},
|
|
32
|
+
NEWT_E003: {
|
|
33
|
+
message: "That is not a known event name.",
|
|
34
|
+
suggestion: "Try one of: on ready, on command, on join, on leave, on message contains, or on reaction add."
|
|
35
|
+
},
|
|
36
|
+
NEWT_E004: {
|
|
37
|
+
message: "This line needs to be inside a handler.",
|
|
38
|
+
suggestion: "Put this action under a line like: on command \"hello\":"
|
|
39
|
+
},
|
|
40
|
+
NEWT_E005: {
|
|
41
|
+
message: "[SECURITY WARNING] Never put your bot token directly in the file.",
|
|
42
|
+
suggestion: "Use: bot token from env \"DISCORD_TOKEN\"",
|
|
43
|
+
severity: "warning"
|
|
44
|
+
},
|
|
45
|
+
NEWT_E006: {
|
|
46
|
+
message: "Indentation looks off on this line.",
|
|
47
|
+
suggestion: "Use 2 or 4 spaces consistently within the file."
|
|
48
|
+
},
|
|
49
|
+
NEWT_E007: {
|
|
50
|
+
message: "This variable has not been defined yet.",
|
|
51
|
+
suggestion: "Add a let line before using it, like: let name = \"Newt\""
|
|
52
|
+
},
|
|
53
|
+
NEWT_E008: {
|
|
54
|
+
message: "Your bot needs a name.",
|
|
55
|
+
suggestion: "Add a line near the top: bot name \"MyBot\""
|
|
56
|
+
},
|
|
57
|
+
NEWT_E009: {
|
|
58
|
+
message: "Your bot needs a token source.",
|
|
59
|
+
suggestion: "Add: bot token from env \"DISCORD_TOKEN\""
|
|
60
|
+
},
|
|
61
|
+
NEWT_E010: {
|
|
62
|
+
message: "Embed colors must be hex colors.",
|
|
63
|
+
suggestion: "Use a six-digit color like: color #5865F2"
|
|
64
|
+
},
|
|
65
|
+
NEWT_E011: {
|
|
66
|
+
message: "Network requests should have an error fallback.",
|
|
67
|
+
suggestion: "Put fetch inside try: and add an on error: block."
|
|
68
|
+
},
|
|
69
|
+
NEWT_E012: {
|
|
70
|
+
message: "target only works when a command message mentions someone.",
|
|
71
|
+
suggestion: "Make sure this command is used like: !mute @Someone"
|
|
72
|
+
},
|
|
73
|
+
NEWT_E013: {
|
|
74
|
+
message: "That built-in variable does not exist.",
|
|
75
|
+
suggestion: "Use a built-in like user.name, message.content, channel.name, server.name, args, or target."
|
|
76
|
+
},
|
|
77
|
+
NEWT_E014: {
|
|
78
|
+
message: "Timer intervals must be greater than zero.",
|
|
79
|
+
suggestion: "Use a positive interval, like: every 1 hour:"
|
|
80
|
+
},
|
|
81
|
+
NEWT_E015: {
|
|
82
|
+
message: "Role names cannot be empty.",
|
|
83
|
+
suggestion: "Use a real role name, like: require role \"Moderator\""
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
export function makeCatalogError(code, line, column, sourceLine, overrides = {}) {
|
|
87
|
+
const entry = errorCatalog[code];
|
|
88
|
+
return new NewtError({
|
|
89
|
+
code,
|
|
90
|
+
line,
|
|
91
|
+
column,
|
|
92
|
+
sourceLine,
|
|
93
|
+
message: entry.message,
|
|
94
|
+
suggestion: entry.suggestion,
|
|
95
|
+
severity: entry.severity,
|
|
96
|
+
...overrides
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export function formatError(error, sourceOrFilename = "") {
|
|
100
|
+
const filename = error.filename ?? (sourceOrFilename.includes("\n") ? "input.newt" : sourceOrFilename || "input.newt");
|
|
101
|
+
const sourceLine = error.sourceLine ?? getSourceLine(sourceOrFilename, error.line);
|
|
102
|
+
const label = error.severity === "warning" ? "Warning" : "Error";
|
|
103
|
+
const caretOffset = Math.max(0, error.column - 1);
|
|
104
|
+
const caretLength = Math.max(1, error.length);
|
|
105
|
+
const caretLine = `${" ".repeat(caretOffset)}${"^".repeat(caretLength)}`;
|
|
106
|
+
const suggestion = error.suggestion ? ` ${error.suggestion}` : "";
|
|
107
|
+
return [
|
|
108
|
+
`${label} [${error.code}] on line ${error.line} in ${filename}:`,
|
|
109
|
+
"",
|
|
110
|
+
` ${sourceLine}`,
|
|
111
|
+
` ${caretLine}`,
|
|
112
|
+
`${error.message}${suggestion}`
|
|
113
|
+
].join("\n");
|
|
114
|
+
}
|
|
115
|
+
function getSourceLine(source, line) {
|
|
116
|
+
if (!source.includes("\n")) {
|
|
117
|
+
return "";
|
|
118
|
+
}
|
|
119
|
+
return source.split(/\r?\n/)[line - 1] ?? "";
|
|
120
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type GeneratedProject } from "./codegen.js";
|
|
2
|
+
import { NewtError } from "./errors.js";
|
|
3
|
+
export interface CompileSuccess extends GeneratedProject {
|
|
4
|
+
success: true;
|
|
5
|
+
}
|
|
6
|
+
export interface CompileFailure {
|
|
7
|
+
success: false;
|
|
8
|
+
errors: NewtError[];
|
|
9
|
+
}
|
|
10
|
+
export type CompileResult = CompileSuccess | CompileFailure;
|
|
11
|
+
export declare function compile(source: string, filename?: string): CompileResult;
|
|
12
|
+
export { generate } from "./codegen.js";
|
|
13
|
+
export { formatError, NewtError } from "./errors.js";
|
|
14
|
+
export { tokenize, type Token, type TokenType } from "./lexer.js";
|
|
15
|
+
export { parse } from "./parser.js";
|
|
16
|
+
export { validate } from "./validator.js";
|
|
17
|
+
export type * from "./ast.js";
|