botql 1.0.2 → 1.2.2
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/Database.js +1 -146
- package/LICENSE +30 -10
- package/Parser.js +2 -759
- package/RAG.js +1 -572
- package/README.md +183 -12
- package/Terminal.js +96 -0
- package/botql.browser.js +686 -1550
- package/botql.js +4 -786
- package/createBot.js +88 -0
- package/package.json +3 -2
- package/Bootstrap.js +0 -74
package/Parser.js
CHANGED
|
@@ -1,762 +1,5 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
2
|
+
*Parser.min.js
|
|
5
3
|
*
|
|
6
|
-
* Converte código-fonte .sql (sintaxe BotQL) numa AST de statements
|
|
7
|
-
* que o botql.js (interpreter) consegue percorrer e executar.
|
|
8
|
-
*
|
|
9
|
-
* Uso:
|
|
10
|
-
* const { Parser } = require('./Parser.js');
|
|
11
|
-
* const ast = new Parser(sourceCode).parseProgram();
|
|
12
4
|
*/
|
|
13
|
-
|
|
14
|
-
// ===== Tokenizer =====
|
|
15
|
-
|
|
16
|
-
const TokenType = {
|
|
17
|
-
KEYWORD: 'KEYWORD',
|
|
18
|
-
STRING: 'STRING',
|
|
19
|
-
NUMBER: 'NUMBER',
|
|
20
|
-
IDENT: 'IDENT',
|
|
21
|
-
SYMBOL: 'SYMBOL',
|
|
22
|
-
EOF: 'EOF'
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// Palavras reservadas (case-insensitive na fonte, normalizadas para maiúsculas)
|
|
26
|
-
const KEYWORDS = new Set([
|
|
27
|
-
'CREATE', 'BOT', 'PLATFORM', 'CONNECT', 'TABLE', 'PREVENT', 'DEFAULT',
|
|
28
|
-
'ON', 'START', 'MESSAGE', 'FROM',
|
|
29
|
-
'WHEN', 'CONTAINS', 'OR', 'OTHERWISE', 'KEYWORDS', 'THINK', 'WAITING',
|
|
30
|
-
'REPLY', 'TO', 'FORWARD', 'PARSE', 'SEND',
|
|
31
|
-
'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET', 'WHERE',
|
|
32
|
-
'RUN', 'IMPORT', 'AS', 'RESPONSE'
|
|
33
|
-
]);
|
|
34
|
-
|
|
35
|
-
class Token {
|
|
36
|
-
constructor(type, value, line) {
|
|
37
|
-
this.type = type;
|
|
38
|
-
this.value = value;
|
|
39
|
-
this.line = line;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
class Tokenizer {
|
|
44
|
-
constructor(source) {
|
|
45
|
-
this.source = source;
|
|
46
|
-
this.pos = 0;
|
|
47
|
-
this.line = 1;
|
|
48
|
-
this.tokens = [];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
error(msg) {
|
|
52
|
-
throw new Error(`syntax error: ${msg}`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
peekChar(offset = 0) {
|
|
56
|
-
return this.source[this.pos + offset];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
tokenize() {
|
|
60
|
-
while (this.pos < this.source.length) {
|
|
61
|
-
const c = this.peekChar();
|
|
62
|
-
|
|
63
|
-
// Nova linha
|
|
64
|
-
if (c === '\n') {
|
|
65
|
-
this.line++;
|
|
66
|
-
this.pos++;
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Espaços
|
|
71
|
-
if (/\s/.test(c)) {
|
|
72
|
-
this.pos++;
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Comentário de linha: -- ...
|
|
77
|
-
if (c === '-' && this.peekChar(1) === '-') {
|
|
78
|
-
while (this.pos < this.source.length && this.peekChar() !== '\n') this.pos++;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// String literal
|
|
83
|
-
if (c === '"' || c === "'") {
|
|
84
|
-
this.tokens.push(this.readString(c));
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Número
|
|
89
|
-
if (/[0-9]/.test(c)) {
|
|
90
|
-
this.tokens.push(this.readNumber());
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Símbolos de um caractere
|
|
95
|
-
if ('{}(),.=+*;'.includes(c)) {
|
|
96
|
-
this.tokens.push(new Token(TokenType.SYMBOL, c, this.line));
|
|
97
|
-
this.pos++;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Identificador / palavra-chave
|
|
102
|
-
if (/[A-Za-z_]/.test(c)) {
|
|
103
|
-
const word = this.readWord();
|
|
104
|
-
this.tokens.push(word);
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
this.error(`unexpected character: "${c}"`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
this.tokens.push(new Token(TokenType.EOF, null, this.line));
|
|
112
|
-
return this.tokens;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
readString(quote) {
|
|
116
|
-
const startLine = this.line;
|
|
117
|
-
this.pos++; // consome a aspa de abertura
|
|
118
|
-
let value = '';
|
|
119
|
-
while (this.pos < this.source.length && this.peekChar() !== quote) {
|
|
120
|
-
if (this.peekChar() === '\\') {
|
|
121
|
-
this.pos++;
|
|
122
|
-
value += this.peekChar();
|
|
123
|
-
this.pos++;
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (this.peekChar() === '\n') this.line++;
|
|
127
|
-
value += this.peekChar();
|
|
128
|
-
this.pos++;
|
|
129
|
-
}
|
|
130
|
-
if (this.peekChar() !== quote) {
|
|
131
|
-
this.error('unterminated string');
|
|
132
|
-
}
|
|
133
|
-
this.pos++; // consome a aspa de fecho
|
|
134
|
-
return new Token(TokenType.STRING, value, startLine);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
readNumber() {
|
|
138
|
-
const startLine = this.line;
|
|
139
|
-
let value = '';
|
|
140
|
-
while (this.pos < this.source.length && /[0-9.]/.test(this.peekChar())) {
|
|
141
|
-
value += this.peekChar();
|
|
142
|
-
this.pos++;
|
|
143
|
-
}
|
|
144
|
-
return new Token(TokenType.NUMBER, Number(value), startLine);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
readWord() {
|
|
148
|
-
const startLine = this.line;
|
|
149
|
-
let value = '';
|
|
150
|
-
while (this.pos < this.source.length && /[A-Za-z0-9_]/.test(this.peekChar())) {
|
|
151
|
-
value += this.peekChar();
|
|
152
|
-
this.pos++;
|
|
153
|
-
}
|
|
154
|
-
const upper = value.toUpperCase();
|
|
155
|
-
if (KEYWORDS.has(upper) && value === upper) {
|
|
156
|
-
return new Token(TokenType.KEYWORD, upper, startLine);
|
|
157
|
-
}
|
|
158
|
-
return new Token(TokenType.IDENT, value, startLine);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ===== Parser (recursive descent) =====
|
|
163
|
-
|
|
164
|
-
class Parser {
|
|
165
|
-
constructor(source) {
|
|
166
|
-
this.tokens = new Tokenizer(source).tokenize();
|
|
167
|
-
this.pos = 0;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
error(msg) {
|
|
171
|
-
const tok = this.current();
|
|
172
|
-
throw new Error(`syntax error: ${msg}, near "${tok ? tok.value : 'EOF'}"`);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
current() {
|
|
176
|
-
return this.tokens[this.pos];
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
at(type, value) {
|
|
180
|
-
const tok = this.current();
|
|
181
|
-
if (tok.type !== type) return false;
|
|
182
|
-
if (value !== undefined && tok.value !== value) return false;
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
atKeyword(...values) {
|
|
187
|
-
return this.at(TokenType.KEYWORD) && values.includes(this.current().value);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Casa por valor, independente do token ser KEYWORD ou IDENT.
|
|
191
|
-
// Necessário para palavras como "SIGNAL", que tanto introduz um
|
|
192
|
-
// evento (ON SIGNAL FROM) como é usada como identificador (SIGNAL.PAIR).
|
|
193
|
-
atWord(...values) {
|
|
194
|
-
const tok = this.current();
|
|
195
|
-
return (tok.type === TokenType.KEYWORD || tok.type === TokenType.IDENT) && values.includes(tok.value);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
advance() {
|
|
199
|
-
const tok = this.current();
|
|
200
|
-
if (tok.type !== TokenType.EOF) this.pos++;
|
|
201
|
-
return tok;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
expect(type, value) {
|
|
205
|
-
if (!this.at(type, value)) {
|
|
206
|
-
this.error(`expected ${value || type}`);
|
|
207
|
-
}
|
|
208
|
-
return this.advance();
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
expectKeyword(value) {
|
|
212
|
-
return this.expect(TokenType.KEYWORD, value);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
isAtEnd() {
|
|
216
|
-
return this.at(TokenType.EOF);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// ---- Programa ----
|
|
220
|
-
|
|
221
|
-
parseProgram() {
|
|
222
|
-
const statements = [];
|
|
223
|
-
while (!this.isAtEnd()) {
|
|
224
|
-
statements.push(this.parseStatement());
|
|
225
|
-
}
|
|
226
|
-
return { type: 'Program', body: statements };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ---- Bloco { ... } ou statement único ----
|
|
230
|
-
// Regra da linguagem: bloco com mais de uma ação usa chaves,
|
|
231
|
-
// uma única ação não precisa.
|
|
232
|
-
|
|
233
|
-
parseBody() {
|
|
234
|
-
if (this.at(TokenType.SYMBOL, '{')) {
|
|
235
|
-
this.advance(); // {
|
|
236
|
-
const statements = [];
|
|
237
|
-
while (!this.at(TokenType.SYMBOL, '}')) {
|
|
238
|
-
if (this.isAtEnd()) this.error('unclosed block "{"');
|
|
239
|
-
statements.push(this.parseStatement());
|
|
240
|
-
}
|
|
241
|
-
this.advance(); // }
|
|
242
|
-
return statements;
|
|
243
|
-
}
|
|
244
|
-
// Ação única, sem chaves
|
|
245
|
-
return [this.parseStatement()];
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// ---- Dispatch de statement ----
|
|
249
|
-
|
|
250
|
-
parseStatement() {
|
|
251
|
-
if (this.atKeyword('CREATE')) return this.parseCreate();
|
|
252
|
-
if (this.atKeyword('PLATFORM')) return this.parsePlatform();
|
|
253
|
-
if (this.atKeyword('CONNECT')) return this.parseConnect();
|
|
254
|
-
if (this.atKeyword('ON')) return this.parseOn();
|
|
255
|
-
if (this.atKeyword('WHEN')) return this.parseWhen();
|
|
256
|
-
if (this.atKeyword('OTHERWISE')) return this.parseOtherwise();
|
|
257
|
-
if (this.atKeyword('REPLY')) return this.parseReply();
|
|
258
|
-
if (this.atKeyword('THINK')) return this.parseThink();
|
|
259
|
-
if (this.atKeyword('FORWARD')) return this.parseForward();
|
|
260
|
-
if (this.atKeyword('PARSE')) return this.parseParseSignal();
|
|
261
|
-
if (this.atKeyword('SEND')) return this.parseSend();
|
|
262
|
-
if (this.atKeyword('INSERT')) return this.parseInsert();
|
|
263
|
-
if (this.atKeyword('UPDATE')) return this.parseUpdate();
|
|
264
|
-
if (this.atKeyword('RUN')) return this.parseRunBot();
|
|
265
|
-
if (this.atKeyword('IMPORT')) return this.parseImport();
|
|
266
|
-
|
|
267
|
-
this.error('unexpected statement');
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// ---- CREATE BOT / CREATE TABLE ----
|
|
271
|
-
|
|
272
|
-
parseCreate() {
|
|
273
|
-
this.expectKeyword('CREATE');
|
|
274
|
-
if (this.atKeyword('BOT')) {
|
|
275
|
-
this.advance();
|
|
276
|
-
const name = this.expect(TokenType.STRING).value;
|
|
277
|
-
return { type: 'CreateBot', name };
|
|
278
|
-
}
|
|
279
|
-
if (this.atKeyword('TABLE')) {
|
|
280
|
-
this.advance();
|
|
281
|
-
const name = this.expect(TokenType.IDENT).value;
|
|
282
|
-
const columns = this.parseColumnList();
|
|
283
|
-
const preventDefault = this.tryParsePreventDefault();
|
|
284
|
-
const defaultMessage = this.tryParseDefaultMessage();
|
|
285
|
-
return { type: 'CreateTable', name, columns, preventDefault, defaultMessage };
|
|
286
|
-
}
|
|
287
|
-
this.error('expected BOT or TABLE after CREATE');
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
parseColumnList() {
|
|
291
|
-
this.expect(TokenType.SYMBOL, '(');
|
|
292
|
-
const columns = [];
|
|
293
|
-
while (!this.at(TokenType.SYMBOL, ')')) {
|
|
294
|
-
const colName = this.expect(TokenType.IDENT).value;
|
|
295
|
-
const constraints = [];
|
|
296
|
-
let colType = null;
|
|
297
|
-
// Tipo e constraints são identificadores livres (INT, TEXT, PRIMARY, KEY, ...)
|
|
298
|
-
// até à vírgula ou ao fecho do parêntese.
|
|
299
|
-
while (this.at(TokenType.IDENT) && !this.at(TokenType.SYMBOL, ',') && !this.at(TokenType.SYMBOL, ')')) {
|
|
300
|
-
const word = this.advance().value;
|
|
301
|
-
if (!colType) colType = word;
|
|
302
|
-
else constraints.push(word);
|
|
303
|
-
}
|
|
304
|
-
columns.push({ name: colName, columnType: colType, constraints });
|
|
305
|
-
if (this.at(TokenType.SYMBOL, ',')) this.advance();
|
|
306
|
-
}
|
|
307
|
-
this.expect(TokenType.SYMBOL, ')');
|
|
308
|
-
return columns;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
tryParsePreventDefault() {
|
|
312
|
-
if (this.atKeyword('PREVENT')) {
|
|
313
|
-
this.advance();
|
|
314
|
-
this.expectKeyword('DEFAULT');
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
return false;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// ---- DEFAULT MESSAGE "texto" ----
|
|
321
|
-
// ---- DEFAULT MESSAGE (ficheiro.txt, N) ----
|
|
322
|
-
//
|
|
323
|
-
// Modificador do CREATE TABLE, ao lado de PREVENT DEFAULT: saudação
|
|
324
|
-
// automática enviada só na primeira mensagem de cada client/sender
|
|
325
|
-
// (ver botql.js, receiveMessage). Aceita texto direto ou a mesma forma
|
|
326
|
-
// de ficheiro indexado do REPLY — por isso reaproveita isReplyFileForm
|
|
327
|
-
// para o lookahead. "DEFAULT" aqui não é o mesmo "DEFAULT" de "PREVENT
|
|
328
|
-
// DEFAULT" — só é reconhecido como início de DEFAULT MESSAGE quando NÃO
|
|
329
|
-
// vem logo a seguir a PREVENT (tryParsePreventDefault já consumiu esse
|
|
330
|
-
// caso antes de chegarmos aqui).
|
|
331
|
-
tryParseDefaultMessage() {
|
|
332
|
-
if (!this.atKeyword('DEFAULT')) return null;
|
|
333
|
-
const next = this.tokens[this.pos + 1];
|
|
334
|
-
if (!next || next.type !== TokenType.KEYWORD || next.value !== 'MESSAGE') return null;
|
|
335
|
-
|
|
336
|
-
this.advance(); // DEFAULT
|
|
337
|
-
this.advance(); // MESSAGE
|
|
338
|
-
|
|
339
|
-
if (this.isReplyFileForm()) {
|
|
340
|
-
this.expect(TokenType.SYMBOL, '(');
|
|
341
|
-
const file = this.parseFileName();
|
|
342
|
-
this.expect(TokenType.SYMBOL, ',');
|
|
343
|
-
const index = this.parseExpression();
|
|
344
|
-
this.expect(TokenType.SYMBOL, ')');
|
|
345
|
-
return { file, index };
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const value = this.parseExpression();
|
|
349
|
-
return { value };
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// ---- PLATFORM ----
|
|
353
|
-
|
|
354
|
-
parsePlatform() {
|
|
355
|
-
this.expectKeyword('PLATFORM');
|
|
356
|
-
const value = this.advance().value; // ex: WHATSAPP (IDENT)
|
|
357
|
-
return { type: 'Platform', value };
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// ---- CONNECT SERVIÇO TIPO "credencial" ----
|
|
361
|
-
// ---- CONNECT RESPONSE alias ----
|
|
362
|
-
//
|
|
363
|
-
// A segunda forma liga o bot a uma IA usando um valor já importado (o
|
|
364
|
-
// alias de um IMPORT {..., N} AS alias) — só estabelece a ligação,
|
|
365
|
-
// não dispara nada sozinho (quem dispara é Response()/Response(alias),
|
|
366
|
-
// dentro de uma ação). RESPONSE é keyword fixa aqui, por isso da para
|
|
367
|
-
// distinguir logo no primeiro token, sem lookahead: a forma normal
|
|
368
|
-
// nunca começa por essa palavra.
|
|
369
|
-
|
|
370
|
-
parseConnect() {
|
|
371
|
-
this.expectKeyword('CONNECT');
|
|
372
|
-
|
|
373
|
-
if (this.atKeyword('RESPONSE')) {
|
|
374
|
-
this.advance();
|
|
375
|
-
const alias = this.expect(TokenType.IDENT).value;
|
|
376
|
-
return { type: 'ConnectResponse', alias };
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const service = this.advance().value;
|
|
380
|
-
const kind = this.advance().value;
|
|
381
|
-
const credential = this.expect(TokenType.STRING).value;
|
|
382
|
-
return { type: 'Connect', service, kind, credential };
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// ---- ON START | ON MESSAGE | ON SIGNAL FROM "..." ----
|
|
386
|
-
|
|
387
|
-
parseOn() {
|
|
388
|
-
this.expectKeyword('ON');
|
|
389
|
-
if (this.atKeyword('START')) {
|
|
390
|
-
this.advance();
|
|
391
|
-
const body = this.parseBody();
|
|
392
|
-
return { type: 'On', event: 'START', body };
|
|
393
|
-
}
|
|
394
|
-
if (this.atKeyword('MESSAGE')) {
|
|
395
|
-
this.advance();
|
|
396
|
-
const body = this.parseBody();
|
|
397
|
-
return { type: 'On', event: 'MESSAGE', body };
|
|
398
|
-
}
|
|
399
|
-
if (this.atWord('SIGNAL')) {
|
|
400
|
-
this.advance();
|
|
401
|
-
this.expectKeyword('FROM');
|
|
402
|
-
const source = this.expect(TokenType.STRING).value;
|
|
403
|
-
const body = this.parseBody();
|
|
404
|
-
return { type: 'On', event: 'SIGNAL', source, body };
|
|
405
|
-
}
|
|
406
|
-
this.error('unexpected event after ON');
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// ---- WHEN CONTAINS "..." [OR CONTAINS "..."]* ----
|
|
410
|
-
|
|
411
|
-
parseWhen() {
|
|
412
|
-
this.expectKeyword('WHEN');
|
|
413
|
-
const conditions = [this.parseCondition()];
|
|
414
|
-
while (this.atKeyword('OR')) {
|
|
415
|
-
this.advance();
|
|
416
|
-
conditions.push(this.parseCondition());
|
|
417
|
-
}
|
|
418
|
-
const body = this.parseBody();
|
|
419
|
-
return { type: 'When', conditions, body };
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
parseCondition() {
|
|
423
|
-
this.expectKeyword('CONTAINS');
|
|
424
|
-
|
|
425
|
-
// CONTAINS ("oi", "ola", "boa tarde", ...) — lista inline, evita
|
|
426
|
-
// repetir "OR CONTAINS" para cada palavra-chave.
|
|
427
|
-
if (this.at(TokenType.SYMBOL, '(')) {
|
|
428
|
-
const values = this.parseStringList();
|
|
429
|
-
return { op: 'CONTAINS_ANY', values };
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// CONTAINS KEYWORDS(ficheiro.txt) — lista carregada de um ficheiro
|
|
433
|
-
// à parte (uma palavra-chave por linha), para listas grandes
|
|
434
|
-
// (100+ palavras) sem poluir o .sql. Sem aspas de propósito: não é
|
|
435
|
-
// uma string de busca, é um nome de ficheiro (fica visualmente
|
|
436
|
-
// diferente de CONTAINS "texto").
|
|
437
|
-
if (this.atKeyword('KEYWORDS')) {
|
|
438
|
-
this.advance();
|
|
439
|
-
this.expect(TokenType.SYMBOL, '(');
|
|
440
|
-
const path = this.parseFileName();
|
|
441
|
-
this.expect(TokenType.SYMBOL, ')');
|
|
442
|
-
return { op: 'CONTAINS_KEYWORDS_FILE', path };
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// CONTAINS "texto" — forma original, continua a funcionar.
|
|
446
|
-
const value = this.expect(TokenType.STRING).value;
|
|
447
|
-
return { op: 'CONTAINS', value };
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Lista de strings entre parênteses: ("a", "b", "c")
|
|
451
|
-
parseStringList() {
|
|
452
|
-
this.expect(TokenType.SYMBOL, '(');
|
|
453
|
-
const values = [];
|
|
454
|
-
while (!this.at(TokenType.SYMBOL, ')')) {
|
|
455
|
-
values.push(this.expect(TokenType.STRING).value);
|
|
456
|
-
if (this.at(TokenType.SYMBOL, ',')) this.advance();
|
|
457
|
-
}
|
|
458
|
-
this.expect(TokenType.SYMBOL, ')');
|
|
459
|
-
return values;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Nome de ficheiro sem aspas, ex: saudacoes.txt ou lista_bot.sql.
|
|
463
|
-
// O tokenizer separa "saudacoes" (IDENT) e ".txt" em IDENT + SYMBOL('.')
|
|
464
|
-
// + IDENT, por isso remontamos aqui em vez de pedir um STRING.
|
|
465
|
-
parseFileName() {
|
|
466
|
-
let name = this.expect(TokenType.IDENT).value;
|
|
467
|
-
while (this.at(TokenType.SYMBOL, '.')) {
|
|
468
|
-
this.advance();
|
|
469
|
-
name += '.' + this.expect(TokenType.IDENT).value;
|
|
470
|
-
}
|
|
471
|
-
return name;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// ---- OTHERWISE ----
|
|
475
|
-
|
|
476
|
-
parseOtherwise() {
|
|
477
|
-
this.expectKeyword('OTHERWISE');
|
|
478
|
-
const body = this.parseBody();
|
|
479
|
-
return { type: 'Otherwise', body };
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// ---- REPLY [TO alvo] expressão ----
|
|
483
|
-
// ---- REPLY [TO alvo] (ficheiro.txt, N) ----
|
|
484
|
-
//
|
|
485
|
-
// A segunda forma referencia uma entrada numerada de um ficheiro de
|
|
486
|
-
// respostas (uma linha "N- texto" por entrada). Distingue-se da
|
|
487
|
-
// expressão comum porque começa por "(" seguido de um nome de
|
|
488
|
-
// ficheiro (IDENT + "." + IDENT), e não por uma STRING/NUMBER/IDENT
|
|
489
|
-
// isolado — daí o lookahead antes de decidir qual ramo seguir.
|
|
490
|
-
|
|
491
|
-
// ---- THINK(ficheiro.txt) ----
|
|
492
|
-
// ---- THINK(ficheiro.txt) OR REPLY (fallback.txt, N) ----
|
|
493
|
-
// ---- THINK(ficheiro.txt) OR REPLY "texto fixo" ----
|
|
494
|
-
//
|
|
495
|
-
// Diferente de REPLY: THINK não responde com um texto já mapeado, faz
|
|
496
|
-
// retrieval local sobre um ficheiro de conhecimento (ver RAG.js) e só
|
|
497
|
-
// responde se achar um bloco com confiança suficiente. Antes de
|
|
498
|
-
// procurar, sinaliza onThinking (para a plataforma poder mostrar
|
|
499
|
-
// "Pensando..." enquanto isso, já que a busca — em ficheiros grandes —
|
|
500
|
-
// não é instantânea como um REPLY comum).
|
|
501
|
-
//
|
|
502
|
-
// O "OR REPLY ..." é o fallback: só corre se THINK não encontrar nada
|
|
503
|
-
// com confiança suficiente. Fica preso ao mesmo statement (não é um
|
|
504
|
-
// segundo statement solto no corpo do WHEN) porque só faz sentido
|
|
505
|
-
// junto — um REPLY fallback sem THINK antes seria só um REPLY normal.
|
|
506
|
-
|
|
507
|
-
parseThink() {
|
|
508
|
-
this.expectKeyword('THINK');
|
|
509
|
-
this.expect(TokenType.SYMBOL, '(');
|
|
510
|
-
const file = this.parseFileName();
|
|
511
|
-
this.expect(TokenType.SYMBOL, ')');
|
|
512
|
-
|
|
513
|
-
const waiting = this.tryParseWaiting();
|
|
514
|
-
|
|
515
|
-
let fallback = null;
|
|
516
|
-
if (this.atKeyword('OR')) {
|
|
517
|
-
this.advance();
|
|
518
|
-
fallback = this.parseReply();
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
return { type: 'Think', file, waiting, fallback };
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// ---- WAITING() | WAITING(ficheiro.txt) | WAITING(ficheiro.txt, N) ----
|
|
525
|
-
//
|
|
526
|
-
// Modificador do THINK, sempre logo a seguir a ele, antes do OR (ver
|
|
527
|
-
// parseThink acima). Os dois argumentos são opcionais e independentes:
|
|
528
|
-
// sem nenhum, o interpretador usa texto e tempo mínimo por omissão
|
|
529
|
-
// ("Pensando...", 3 segundos) — ver botql.js, resolveWaitingConfig.
|
|
530
|
-
tryParseWaiting() {
|
|
531
|
-
if (!this.atKeyword('WAITING')) return null;
|
|
532
|
-
this.advance();
|
|
533
|
-
this.expect(TokenType.SYMBOL, '(');
|
|
534
|
-
|
|
535
|
-
let file = null;
|
|
536
|
-
let seconds = null;
|
|
537
|
-
if (!this.at(TokenType.SYMBOL, ')')) {
|
|
538
|
-
file = this.parseFileName();
|
|
539
|
-
if (this.at(TokenType.SYMBOL, ',')) {
|
|
540
|
-
this.advance();
|
|
541
|
-
seconds = this.parseExpression();
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
this.expect(TokenType.SYMBOL, ')');
|
|
546
|
-
return { file, seconds };
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
parseReply() {
|
|
550
|
-
this.expectKeyword('REPLY');
|
|
551
|
-
let target = null;
|
|
552
|
-
if (this.atKeyword('TO')) {
|
|
553
|
-
this.advance();
|
|
554
|
-
target = this.advance().value; // ex: ADMIN
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
if (this.isReplyFileForm()) {
|
|
558
|
-
this.expect(TokenType.SYMBOL, '(');
|
|
559
|
-
const file = this.parseFileName();
|
|
560
|
-
this.expect(TokenType.SYMBOL, ',');
|
|
561
|
-
const index = this.parseExpression();
|
|
562
|
-
this.expect(TokenType.SYMBOL, ')');
|
|
563
|
-
return { type: 'Reply', target, file, index };
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const value = this.parseExpression();
|
|
567
|
-
return { type: 'Reply', target, value };
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Lookahead: "(" IDENT "." IDENT "," ... — só a forma de ficheiro
|
|
571
|
-
// tem "." logo a seguir ao primeiro identificador dentro dos
|
|
572
|
-
// parênteses. Uma chamada de função normal, ex: (NOW()), nunca
|
|
573
|
-
// bate aqui.
|
|
574
|
-
isReplyFileForm() {
|
|
575
|
-
if (!this.at(TokenType.SYMBOL, '(')) return false;
|
|
576
|
-
const next = this.tokens[this.pos + 1];
|
|
577
|
-
const afterNext = this.tokens[this.pos + 2];
|
|
578
|
-
return !!next && next.type === TokenType.IDENT &&
|
|
579
|
-
!!afterNext && afterNext.type === TokenType.SYMBOL && afterNext.value === '.';
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// ---- FORWARD TO "contacto" ----
|
|
583
|
-
|
|
584
|
-
parseForward() {
|
|
585
|
-
this.expectKeyword('FORWARD');
|
|
586
|
-
this.expectKeyword('TO');
|
|
587
|
-
const target = this.parseExpression();
|
|
588
|
-
return { type: 'ForwardTo', target };
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// ---- PARSE SIGNAL ----
|
|
592
|
-
|
|
593
|
-
parseParseSignal() {
|
|
594
|
-
this.expectKeyword('PARSE');
|
|
595
|
-
if (!this.atWord('SIGNAL')) this.error('expected SIGNAL after PARSE');
|
|
596
|
-
this.advance();
|
|
597
|
-
return { type: 'ParseSignal' };
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// ---- SEND TO destino ----
|
|
601
|
-
|
|
602
|
-
parseSend() {
|
|
603
|
-
this.expectKeyword('SEND');
|
|
604
|
-
this.expectKeyword('TO');
|
|
605
|
-
const target = this.advance().value;
|
|
606
|
-
return { type: 'SendTo', target };
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// ---- INSERT INTO tabela [(cols)] [VALUES (vals)] ----
|
|
610
|
-
|
|
611
|
-
parseInsert() {
|
|
612
|
-
this.expectKeyword('INSERT');
|
|
613
|
-
this.expectKeyword('INTO');
|
|
614
|
-
const table = this.expect(TokenType.IDENT).value;
|
|
615
|
-
|
|
616
|
-
let columns = null;
|
|
617
|
-
if (this.at(TokenType.SYMBOL, '(')) {
|
|
618
|
-
columns = this.parseExpressionList();
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
let values = null;
|
|
622
|
-
if (this.atKeyword('VALUES')) {
|
|
623
|
-
this.advance();
|
|
624
|
-
values = this.parseExpressionList();
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
return { type: 'Insert', table, columns, values };
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
parseExpressionList() {
|
|
631
|
-
this.expect(TokenType.SYMBOL, '(');
|
|
632
|
-
const items = [];
|
|
633
|
-
while (!this.at(TokenType.SYMBOL, ')')) {
|
|
634
|
-
items.push(this.parseExpression());
|
|
635
|
-
if (this.at(TokenType.SYMBOL, ',')) this.advance();
|
|
636
|
-
}
|
|
637
|
-
this.expect(TokenType.SYMBOL, ')');
|
|
638
|
-
return items;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// ---- UPDATE tabela SET col = expr WHERE expr ----
|
|
642
|
-
|
|
643
|
-
parseUpdate() {
|
|
644
|
-
this.expectKeyword('UPDATE');
|
|
645
|
-
const table = this.expect(TokenType.IDENT).value;
|
|
646
|
-
this.expectKeyword('SET');
|
|
647
|
-
const column = this.expect(TokenType.IDENT).value;
|
|
648
|
-
this.expect(TokenType.SYMBOL, '=');
|
|
649
|
-
const value = this.parseExpression();
|
|
650
|
-
|
|
651
|
-
let where = null;
|
|
652
|
-
if (this.atKeyword('WHERE')) {
|
|
653
|
-
this.advance();
|
|
654
|
-
where = this.parseComparison();
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
return { type: 'Update', table, set: { column, value }, where };
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// ---- IMPORT {ficheiro.sql} ----
|
|
661
|
-
// ---- IMPORT {ficheiro.txt, N} ----
|
|
662
|
-
// ---- IMPORT {ficheiro.txt, N} AS alias ----
|
|
663
|
-
//
|
|
664
|
-
// A segunda forma importa só a entrada N de um ficheiro de valores
|
|
665
|
-
// (o mesmo formato "N- texto" usado por REPLY (ficheiro, N)), em vez
|
|
666
|
-
// do ficheiro inteiro. O índice é opcional, tal como em REPLY.
|
|
667
|
-
//
|
|
668
|
-
// AS alias só faz sentido junto com o índice (dá nome ao valor lido,
|
|
669
|
-
// para CONNECT RESPONSE/Response(...) e qualquer outra expressão
|
|
670
|
-
// referenciarem por esse nome em vez do nome generico "env"). Sem AS,
|
|
671
|
-
// mantém-se o comportamento original: o valor cai em env.NOME_FICHEIRO.
|
|
672
|
-
|
|
673
|
-
parseImport() {
|
|
674
|
-
this.expectKeyword('IMPORT');
|
|
675
|
-
this.expect(TokenType.SYMBOL, '{');
|
|
676
|
-
const path = this.parseFileName();
|
|
677
|
-
let index = null;
|
|
678
|
-
if (this.at(TokenType.SYMBOL, ',')) {
|
|
679
|
-
this.advance();
|
|
680
|
-
index = this.parseExpression();
|
|
681
|
-
}
|
|
682
|
-
this.expect(TokenType.SYMBOL, '}');
|
|
683
|
-
|
|
684
|
-
let alias = null;
|
|
685
|
-
if (this.atKeyword('AS')) {
|
|
686
|
-
this.advance();
|
|
687
|
-
alias = this.expect(TokenType.IDENT).value;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
return { type: 'Import', path, index, alias };
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// ---- RUN BOT ----
|
|
694
|
-
|
|
695
|
-
parseRunBot() {
|
|
696
|
-
this.expectKeyword('RUN');
|
|
697
|
-
this.expectKeyword('BOT');
|
|
698
|
-
return { type: 'RunBot' };
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// ---- Expressões: literais, identificadores, chamadas, acesso a
|
|
702
|
-
// propriedade (obj.prop) e concatenação com "+" ----
|
|
703
|
-
|
|
704
|
-
parseExpression() {
|
|
705
|
-
let node = this.parsePrimary();
|
|
706
|
-
while (this.at(TokenType.SYMBOL, '+')) {
|
|
707
|
-
this.advance();
|
|
708
|
-
const right = this.parsePrimary();
|
|
709
|
-
node = { type: 'Binary', op: '+', left: node, right };
|
|
710
|
-
}
|
|
711
|
-
return node;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// Igualdade, usada em WHERE (ex: id = LAST_INSERT_ID())
|
|
715
|
-
parseComparison() {
|
|
716
|
-
const left = this.parseExpression();
|
|
717
|
-
if (this.at(TokenType.SYMBOL, '=')) {
|
|
718
|
-
this.advance();
|
|
719
|
-
const right = this.parseExpression();
|
|
720
|
-
return { type: 'Binary', op: '=', left, right };
|
|
721
|
-
}
|
|
722
|
-
return left;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
parsePrimary() {
|
|
726
|
-
const tok = this.current();
|
|
727
|
-
|
|
728
|
-
if (tok.type === TokenType.STRING) {
|
|
729
|
-
this.advance();
|
|
730
|
-
return { type: 'Literal', value: tok.value };
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
if (tok.type === TokenType.NUMBER) {
|
|
734
|
-
this.advance();
|
|
735
|
-
return { type: 'Literal', value: tok.value };
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
if (tok.type === TokenType.IDENT) {
|
|
739
|
-
this.advance();
|
|
740
|
-
let node = { type: 'Identifier', name: tok.value };
|
|
741
|
-
|
|
742
|
-
// Chamada de função: NOME(...)
|
|
743
|
-
if (this.at(TokenType.SYMBOL, '(')) {
|
|
744
|
-
const args = this.parseExpressionList();
|
|
745
|
-
node = { type: 'Call', callee: node.name, args };
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// Acesso a propriedade: NOME.CAMPO (ex: SIGNAL.PAIR)
|
|
749
|
-
while (this.at(TokenType.SYMBOL, '.')) {
|
|
750
|
-
this.advance();
|
|
751
|
-
const prop = this.expect(TokenType.IDENT).value;
|
|
752
|
-
node = { type: 'Member', object: node, property: prop };
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
return node;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
this.error('invalid expression');
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
module.exports = { Parser, Tokenizer, TokenType, KEYWORDS };
|
|
5
|
+
"use strict";const TokenType={KEYWORD:"KEYWORD",STRING:"STRING",NUMBER:"NUMBER",IDENT:"IDENT",SYMBOL:"SYMBOL",EOF:"EOF"},KEYWORDS=new Set(["CREATE","BOT","PLATFORM","CONNECT","TABLE","PREVENT","DEFAULT","ON","START","MESSAGE","FROM","WHEN","CONTAINS","OR","OTHERWISE","KEYWORDS","THINK","WAITING","REPLY","TO","FORWARD","PARSE","SEND","INSERT","INTO","VALUES","UPDATE","SET","WHERE","RUN","IMPORT","AS","RESPONSE"]);class Token{constructor(e,t,s,r,i){this.type=e,this.value=t,this.line=s,this.start=r,this.end=i}}class BotQLSyntaxError extends Error{constructor(e,{line:t=null,start:s=0,end:r=null,near:i=null}={}){super(e),this.name="BotQLSyntaxError",this.line=t,this.start=s,this.end=null===r?s+1:r,this.near=i}}class Tokenizer{constructor(e){this.source=e,this.pos=0,this.line=1,this.tokens=[]}error(e,t,s){let r=void 0!==t?t:this.pos;throw new BotQLSyntaxError(`syntax error: ${e}`,{line:this.line,start:r,end:s})}peekChar(e=0){return this.source[this.pos+e]}tokenize(){for(;this.pos<this.source.length;){let e=this.peekChar(),t=this.pos;if("\n"===e){this.line++,this.pos++;continue}if(/\s/.test(e)){this.pos++;continue}if("-"===e&&"-"===this.peekChar(1)){for(;this.pos<this.source.length&&"\n"!==this.peekChar();)this.pos++;continue}let s;'"'===e||"'"===e?s=this.readString(e):/[0-9]/.test(e)?s=this.readNumber():"{}(),.=+*;".includes(e)?(s=new Token(TokenType.SYMBOL,e,this.line),this.pos++):/[A-Za-z_]/.test(e)?s=this.readWord():this.error(`unexpected character: "${e}"`,t,t+1),s.start=t,s.end=this.pos,this.tokens.push(s)}let r=this.source.length,i=new Token(TokenType.EOF,null,this.line,r,r);return this.tokens.push(i),this.tokens}readString(e){let t=this.line,s=this.pos;this.pos++;let r="";for(;this.pos<this.source.length&&this.peekChar()!==e;){if("\\"===this.peekChar()){this.pos++,r+=this.peekChar(),this.pos++;continue}"\n"===this.peekChar()&&this.line++,r+=this.peekChar(),this.pos++}return this.peekChar()!==e&&this.error("unterminated string",s,this.pos),this.pos++,new Token(TokenType.STRING,r,t)}readNumber(){let e=this.line,t="";for(;this.pos<this.source.length&&/[0-9.]/.test(this.peekChar());)t+=this.peekChar(),this.pos++;return new Token(TokenType.NUMBER,Number(t),e)}readWord(){let e=this.line,t="";for(;this.pos<this.source.length&&/[A-Za-z0-9_]/.test(this.peekChar());)t+=this.peekChar(),this.pos++;let s=t.toUpperCase();return KEYWORDS.has(s)&&t===s?new Token(TokenType.KEYWORD,s,e):new Token(TokenType.IDENT,t,e)}}class Parser{constructor(e){this.source=e,this.tokens=new Tokenizer(e).tokenize(),this.pos=0}error(e){let t=this.current(),s=t&&t.type!==TokenType.EOF?t.value:"EOF",r=t?t.start:this.source.length,i=t?t.end:this.source.length;throw new BotQLSyntaxError(`syntax error: ${e}, near "${s}"`,{line:t?t.line:null,start:r,end:i,near:s})}current(){return this.tokens[this.pos]}at(e,t){let s=this.current();return s.type===e&&(void 0===t||s.value===t)}atKeyword(...e){return this.at(TokenType.KEYWORD)&&e.includes(this.current().value)}atWord(...e){let t=this.current();return(t.type===TokenType.KEYWORD||t.type===TokenType.IDENT)&&e.includes(t.value)}advance(){let e=this.current();return e.type!==TokenType.EOF&&this.pos++,e}expect(e,t){return this.at(e,t)||this.error(`expected ${t||e}`),this.advance()}expectKeyword(e){return this.expect(TokenType.KEYWORD,e)}isAtEnd(){return this.at(TokenType.EOF)}parseProgram(){let e=[];for(;!this.isAtEnd();)e.push(this.parseStatement());return{type:"Program",body:e}}parseBody(){if(this.at(TokenType.SYMBOL,"{")){this.advance();let e=[];for(;!this.at(TokenType.SYMBOL,"}");)this.isAtEnd()&&this.error('unclosed block "{"'),e.push(this.parseStatement());return this.advance(),e}return[this.parseStatement()]}parseStatement(){return this.atKeyword("CREATE")?this.parseCreate():this.atKeyword("PLATFORM")?this.parsePlatform():this.atKeyword("CONNECT")?this.parseConnect():this.atKeyword("ON")?this.parseOn():this.atKeyword("WHEN")?this.parseWhen():this.atKeyword("OTHERWISE")?this.parseOtherwise():this.atKeyword("REPLY")?this.parseReply():this.atKeyword("THINK")?this.parseThink():this.atKeyword("WAITING")?this.parseWaitingStatement():this.atKeyword("FORWARD")?this.parseForward():this.atKeyword("PARSE")?this.parseParseSignal():this.atKeyword("SEND")?this.parseSend():this.atKeyword("INSERT")?this.parseInsert():this.atKeyword("UPDATE")?this.parseUpdate():this.atKeyword("RUN")?this.parseRunBot():this.atKeyword("IMPORT")?this.parseImport():void this.error("unexpected statement")}parseCreate(){if(this.expectKeyword("CREATE"),this.atKeyword("BOT")){this.advance();let e=this.expect(TokenType.STRING).value;return{type:"CreateBot",name:e}}if(this.atKeyword("TABLE")){this.advance();let t=this.expect(TokenType.IDENT).value,s=this.parseColumnList(),r=this.tryParsePreventDefault(),i=this.tryParseDefaultMessage();return{type:"CreateTable",name:t,columns:s,preventDefault:r,defaultMessage:i}}this.error("expected BOT or TABLE after CREATE")}parseColumnList(){this.expect(TokenType.SYMBOL,"(");let e=[];for(;!this.at(TokenType.SYMBOL,")");){let t=this.expect(TokenType.IDENT).value,s=[],r=null;for(;this.at(TokenType.IDENT)&&!this.at(TokenType.SYMBOL,",")&&!this.at(TokenType.SYMBOL,")");){let i=this.advance().value;r?s.push(i):r=i}e.push({name:t,columnType:r,constraints:s}),this.at(TokenType.SYMBOL,",")&&this.advance()}return this.expect(TokenType.SYMBOL,")"),e}tryParsePreventDefault(){return!!this.atKeyword("PREVENT")&&(this.advance(),this.expectKeyword("DEFAULT"),!0)}tryParseDefaultMessage(){if(!this.atKeyword("DEFAULT"))return null;let e=this.tokens[this.pos+1];if(!e||e.type!==TokenType.KEYWORD||"MESSAGE"!==e.value)return null;if(this.advance(),this.advance(),this.isReplyFileForm()){this.expect(TokenType.SYMBOL,"(");let t=this.parseFileName();this.expect(TokenType.SYMBOL,",");let s=this.parseExpression();return this.expect(TokenType.SYMBOL,")"),{file:t,index:s}}let r=this.parseExpression();return{value:r}}parsePlatform(){this.expectKeyword("PLATFORM");let e=this.advance().value;return{type:"Platform",value:e}}parseConnect(){if(this.expectKeyword("CONNECT"),this.atKeyword("RESPONSE")){this.advance();let e=this.expect(TokenType.IDENT).value;return{type:"ConnectResponse",alias:e}}let t=this.advance().value,s=this.advance().value,r=this.expect(TokenType.STRING).value;return{type:"Connect",service:t,kind:s,credential:r}}parseOn(){if(this.expectKeyword("ON"),this.atKeyword("START")){this.advance();let e=this.parseBody();return{type:"On",event:"START",body:e}}if(this.atKeyword("MESSAGE")){this.advance();let t=this.parseBody();return{type:"On",event:"MESSAGE",body:t}}if(this.atWord("SIGNAL")){this.advance(),this.expectKeyword("FROM");let s=this.expect(TokenType.STRING).value,r=this.parseBody();return{type:"On",event:"SIGNAL",source:s,body:r}}this.error("unexpected event after ON")}parseWhen(){this.expectKeyword("WHEN");let e=[this.parseCondition()];for(;this.atKeyword("OR");)this.advance(),e.push(this.parseCondition());let t=this.parseBody();return{type:"When",conditions:e,body:t}}parseCondition(){if(this.expectKeyword("CONTAINS"),this.at(TokenType.SYMBOL,"(")){let e=this.parseStringList();return{op:"CONTAINS_ANY",values:e}}if(this.atKeyword("KEYWORDS")){this.advance(),this.expect(TokenType.SYMBOL,"(");let t=this.parseFileName();return this.expect(TokenType.SYMBOL,")"),{op:"CONTAINS_KEYWORDS_FILE",path:t}}let s=this.expect(TokenType.STRING).value;return{op:"CONTAINS",value:s}}parseStringList(){this.expect(TokenType.SYMBOL,"(");let e=[];for(;!this.at(TokenType.SYMBOL,")");)e.push(this.expect(TokenType.STRING).value),this.at(TokenType.SYMBOL,",")&&this.advance();return this.expect(TokenType.SYMBOL,")"),e}parseFileName(){let e=this.expect(TokenType.IDENT).value;for(;this.at(TokenType.SYMBOL,".");)this.advance(),e+="."+this.expect(TokenType.IDENT).value;return e}parseOtherwise(){this.expectKeyword("OTHERWISE");let e=this.parseBody();return{type:"Otherwise",body:e}}parseThink(){this.expectKeyword("THINK"),this.expect(TokenType.SYMBOL,"(");let e=this.parseFileName();this.expect(TokenType.SYMBOL,")");let t=this.tryParseWaiting(),s=null;return this.atKeyword("OR")&&(this.advance(),s=this.parseReply()),{type:"Think",file:e,waiting:t,fallback:s}}tryParseWaiting(){if(!this.atKeyword("WAITING"))return null;this.advance(),this.expect(TokenType.SYMBOL,"(");let e=null,t=null,s=null;return!this.at(TokenType.SYMBOL,")")&&(this.at(TokenType.STRING)?t=this.advance().value:e=this.parseFileName(),this.at(TokenType.SYMBOL,",")&&(this.advance(),s=this.parseExpression())),this.expect(TokenType.SYMBOL,")"),{file:e,text:t,seconds:s}}parseWaitingStatement(){let e=this.tryParseWaiting();return{type:"Waiting",file:e.file,text:e.text,seconds:e.seconds}}parseReply(){this.expectKeyword("REPLY");let e=null;if(this.atKeyword("TO")&&(this.advance(),e=this.advance().value),this.isReplyFileForm()){this.expect(TokenType.SYMBOL,"(");let t=this.parseFileName();this.expect(TokenType.SYMBOL,",");let s=this.parseExpression();return this.expect(TokenType.SYMBOL,")"),{type:"Reply",target:e,file:t,index:s}}let r=this.parseExpression();return{type:"Reply",target:e,value:r}}isReplyFileForm(){if(!this.at(TokenType.SYMBOL,"("))return!1;let e=this.tokens[this.pos+1],t=this.tokens[this.pos+2];return!!e&&e.type===TokenType.IDENT&&!!t&&t.type===TokenType.SYMBOL&&"."===t.value}parseForward(){this.expectKeyword("FORWARD"),this.expectKeyword("TO");let e=this.parseExpression();return{type:"ForwardTo",target:e}}parseParseSignal(){return this.expectKeyword("PARSE"),this.atWord("SIGNAL")||this.error("expected SIGNAL after PARSE"),this.advance(),{type:"ParseSignal"}}parseSend(){this.expectKeyword("SEND"),this.expectKeyword("TO");let e=this.advance().value;return{type:"SendTo",target:e}}parseInsert(){this.expectKeyword("INSERT"),this.expectKeyword("INTO");let e=this.expect(TokenType.IDENT).value,t=null;this.at(TokenType.SYMBOL,"(")&&(t=this.parseExpressionList());let s=null;return this.atKeyword("VALUES")&&(this.advance(),s=this.parseExpressionList()),{type:"Insert",table:e,columns:t,values:s}}parseExpressionList(){this.expect(TokenType.SYMBOL,"(");let e=[];for(;!this.at(TokenType.SYMBOL,")");)e.push(this.parseExpression()),this.at(TokenType.SYMBOL,",")&&this.advance();return this.expect(TokenType.SYMBOL,")"),e}parseUpdate(){this.expectKeyword("UPDATE");let e=this.expect(TokenType.IDENT).value;this.expectKeyword("SET");let t=this.expect(TokenType.IDENT).value;this.expect(TokenType.SYMBOL,"=");let s=this.parseExpression(),r=null;return this.atKeyword("WHERE")&&(this.advance(),r=this.parseComparison()),{type:"Update",table:e,set:{column:t,value:s},where:r}}parseImport(){this.expectKeyword("IMPORT"),this.expect(TokenType.SYMBOL,"{");let e=this.parseFileName(),t=null;this.at(TokenType.SYMBOL,",")&&(this.advance(),t=this.parseExpression()),this.expect(TokenType.SYMBOL,"}");let s=null;return this.atKeyword("AS")&&(this.advance(),s=this.expect(TokenType.IDENT).value),{type:"Import",path:e,index:t,alias:s}}parseRunBot(){return this.expectKeyword("RUN"),this.expectKeyword("BOT"),{type:"RunBot"}}parseExpression(){let e=this.parsePrimary();for(;this.at(TokenType.SYMBOL,"+");){this.advance();let t=this.parsePrimary();e={type:"Binary",op:"+",left:e,right:t}}return e}parseComparison(){let e=this.parseExpression();if(this.at(TokenType.SYMBOL,"=")){this.advance();let t=this.parseExpression();return{type:"Binary",op:"=",left:e,right:t}}return e}parsePrimary(){let e=this.current();if(e.type===TokenType.STRING||e.type===TokenType.NUMBER)return this.advance(),{type:"Literal",value:e.value};if(e.type===TokenType.IDENT){this.advance();let t={type:"Identifier",name:e.value};if(this.at(TokenType.SYMBOL,"(")){let s=this.parseExpressionList();t={type:"Call",callee:t.name,args:s}}for(;this.at(TokenType.SYMBOL,".");){this.advance();let r=this.expect(TokenType.IDENT).value;t={type:"Member",object:t,property:r}}return t}this.error("invalid expression")}}module.exports={Parser,Tokenizer,TokenType,KEYWORDS,BotQLSyntaxError};
|