tova 0.3.0 → 0.3.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/bin/tova.js +1401 -111
- package/package.json +4 -7
- package/src/analyzer/analyzer.js +831 -709
- package/src/analyzer/client-analyzer.js +191 -0
- package/src/analyzer/server-analyzer.js +467 -0
- package/src/analyzer/types.js +20 -4
- package/src/codegen/base-codegen.js +467 -109
- package/src/codegen/client-codegen.js +92 -42
- package/src/codegen/codegen.js +65 -5
- package/src/codegen/server-codegen.js +290 -36
- package/src/diagnostics/error-codes.js +255 -0
- package/src/diagnostics/formatter.js +150 -28
- package/src/docs/generator.js +390 -0
- package/src/lexer/lexer.js +305 -63
- package/src/lexer/tokens.js +19 -0
- package/src/lsp/server.js +892 -30
- package/src/parser/ast.js +81 -368
- package/src/parser/client-ast.js +138 -0
- package/src/parser/client-parser.js +504 -0
- package/src/parser/parser.js +491 -1064
- package/src/parser/server-ast.js +240 -0
- package/src/parser/server-parser.js +602 -0
- package/src/runtime/array-proto.js +32 -0
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +191 -10
- package/src/stdlib/advanced-collections.js +81 -0
- package/src/stdlib/inline.js +549 -6
- package/src/version.js +1 -1
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
// Server-specific parser methods for the Tova language
|
|
2
|
+
// Extracted from parser.js for lazy loading — only loaded when server { } blocks are encountered.
|
|
3
|
+
|
|
4
|
+
import { TokenType } from '../lexer/tokens.js';
|
|
5
|
+
import * as AST from './ast.js';
|
|
6
|
+
|
|
7
|
+
export function installServerParser(ParserClass) {
|
|
8
|
+
if (ParserClass.prototype._serverParserInstalled) return;
|
|
9
|
+
ParserClass.prototype._serverParserInstalled = true;
|
|
10
|
+
|
|
11
|
+
ParserClass.prototype.parseServerBlock = function() {
|
|
12
|
+
const l = this.loc();
|
|
13
|
+
this.expect(TokenType.SERVER);
|
|
14
|
+
// Optional block name: server "api" { }
|
|
15
|
+
let name = null;
|
|
16
|
+
if (this.check(TokenType.STRING)) {
|
|
17
|
+
name = this.advance().value;
|
|
18
|
+
}
|
|
19
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'server'");
|
|
20
|
+
const body = [];
|
|
21
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
22
|
+
try {
|
|
23
|
+
const stmt = this.parseServerStatement();
|
|
24
|
+
if (stmt) body.push(stmt);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
this.errors.push(e);
|
|
27
|
+
this._synchronizeBlock();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close server block");
|
|
31
|
+
return new AST.ServerBlock(body, l, name);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
ParserClass.prototype.parseServerStatement = function() {
|
|
35
|
+
if (this.check(TokenType.ROUTE)) return this.parseRoute();
|
|
36
|
+
|
|
37
|
+
// Contextual keywords in server blocks
|
|
38
|
+
if (this.check(TokenType.IDENTIFIER)) {
|
|
39
|
+
const val = this.current().value;
|
|
40
|
+
if (val === 'middleware' && this.peek(1).type === TokenType.FN) {
|
|
41
|
+
return this.parseMiddleware();
|
|
42
|
+
}
|
|
43
|
+
if (val === 'health') {
|
|
44
|
+
return this.parseHealthCheck();
|
|
45
|
+
}
|
|
46
|
+
if (val === 'cors' && this.peek(1).type === TokenType.LBRACE) {
|
|
47
|
+
return this.parseCorsConfig();
|
|
48
|
+
}
|
|
49
|
+
if (val === 'on_error' && this.peek(1).type === TokenType.FN) {
|
|
50
|
+
return this.parseErrorHandler();
|
|
51
|
+
}
|
|
52
|
+
if (val === 'ws' && this.peek(1).type === TokenType.LBRACE) {
|
|
53
|
+
return this.parseWebSocket();
|
|
54
|
+
}
|
|
55
|
+
if (val === 'static' && this.peek(1).type === TokenType.STRING) {
|
|
56
|
+
return this.parseStaticDeclaration();
|
|
57
|
+
}
|
|
58
|
+
if (val === 'discover' && this.peek(1).type === TokenType.STRING) {
|
|
59
|
+
return this.parseDiscover();
|
|
60
|
+
}
|
|
61
|
+
if (val === 'auth' && this.peek(1).type === TokenType.LBRACE) {
|
|
62
|
+
return this.parseAuthConfig();
|
|
63
|
+
}
|
|
64
|
+
if (val === 'max_body') {
|
|
65
|
+
return this.parseMaxBody();
|
|
66
|
+
}
|
|
67
|
+
if (val === 'routes' && this.peek(1).type === TokenType.STRING) {
|
|
68
|
+
return this.parseRouteGroup();
|
|
69
|
+
}
|
|
70
|
+
if (val === 'rate_limit' && this.peek(1).type === TokenType.LBRACE) {
|
|
71
|
+
return this.parseRateLimitConfig();
|
|
72
|
+
}
|
|
73
|
+
if (val === 'on_start' && this.peek(1).type === TokenType.FN) {
|
|
74
|
+
return this.parseLifecycleHook('start');
|
|
75
|
+
}
|
|
76
|
+
if (val === 'on_stop' && this.peek(1).type === TokenType.FN) {
|
|
77
|
+
return this.parseLifecycleHook('stop');
|
|
78
|
+
}
|
|
79
|
+
if (val === 'subscribe' && this.peek(1).type === TokenType.STRING) {
|
|
80
|
+
return this.parseSubscribe();
|
|
81
|
+
}
|
|
82
|
+
if (val === 'env' && this.peek(1).type === TokenType.IDENTIFIER) {
|
|
83
|
+
return this.parseEnvDeclaration();
|
|
84
|
+
}
|
|
85
|
+
if (val === 'schedule' && this.peek(1).type === TokenType.STRING) {
|
|
86
|
+
return this.parseSchedule();
|
|
87
|
+
}
|
|
88
|
+
if (val === 'upload' && this.peek(1).type === TokenType.LBRACE) {
|
|
89
|
+
return this.parseUploadConfig();
|
|
90
|
+
}
|
|
91
|
+
if (val === 'session' && this.peek(1).type === TokenType.LBRACE) {
|
|
92
|
+
return this.parseSessionConfig();
|
|
93
|
+
}
|
|
94
|
+
if (val === 'db' && this.peek(1).type === TokenType.LBRACE) {
|
|
95
|
+
return this.parseDbConfig();
|
|
96
|
+
}
|
|
97
|
+
if (val === 'tls' && this.peek(1).type === TokenType.LBRACE) {
|
|
98
|
+
return this.parseTlsConfig();
|
|
99
|
+
}
|
|
100
|
+
if (val === 'compression' && this.peek(1).type === TokenType.LBRACE) {
|
|
101
|
+
return this.parseCompressionConfig();
|
|
102
|
+
}
|
|
103
|
+
if (val === 'background' && this.peek(1).type === TokenType.FN) {
|
|
104
|
+
return this.parseBackgroundJob();
|
|
105
|
+
}
|
|
106
|
+
if (val === 'cache' && this.peek(1).type === TokenType.LBRACE) {
|
|
107
|
+
return this.parseCacheConfig();
|
|
108
|
+
}
|
|
109
|
+
if (val === 'sse' && this.peek(1).type === TokenType.STRING) {
|
|
110
|
+
return this.parseSseDeclaration();
|
|
111
|
+
}
|
|
112
|
+
if (val === 'model' && this.peek(1).type === TokenType.IDENTIFIER) {
|
|
113
|
+
return this.parseModelDeclaration();
|
|
114
|
+
}
|
|
115
|
+
// ai { ... } or ai "name" { ... }
|
|
116
|
+
if (val === 'ai' && (this.peek(1).type === TokenType.LBRACE || this.peek(1).type === TokenType.STRING)) {
|
|
117
|
+
return this.parseAiConfig();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return this.parseStatement();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
ParserClass.prototype.parseMiddleware = function() {
|
|
125
|
+
const l = this.loc();
|
|
126
|
+
this.advance(); // consume 'middleware'
|
|
127
|
+
this.expect(TokenType.FN);
|
|
128
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected middleware name").value;
|
|
129
|
+
this.expect(TokenType.LPAREN, "Expected '(' after middleware name");
|
|
130
|
+
const params = this.parseParameterList();
|
|
131
|
+
this.expect(TokenType.RPAREN, "Expected ')' after middleware parameters");
|
|
132
|
+
const body = this.parseBlock();
|
|
133
|
+
return new AST.MiddlewareDeclaration(name, params, body, l);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
ParserClass.prototype.parseHealthCheck = function() {
|
|
137
|
+
const l = this.loc();
|
|
138
|
+
this.advance(); // consume 'health'
|
|
139
|
+
const path = this.expect(TokenType.STRING, "Expected health check path string");
|
|
140
|
+
return new AST.HealthCheckDeclaration(path.value, l);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
ParserClass.prototype.parseCorsConfig = function() {
|
|
144
|
+
const l = this.loc();
|
|
145
|
+
this.advance(); // consume 'cors'
|
|
146
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'cors'");
|
|
147
|
+
const config = {};
|
|
148
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
149
|
+
const key = this.expect(TokenType.IDENTIFIER, "Expected cors config key").value;
|
|
150
|
+
this.expect(TokenType.COLON, "Expected ':' after cors key");
|
|
151
|
+
const value = this.parseExpression();
|
|
152
|
+
config[key] = value;
|
|
153
|
+
this.match(TokenType.COMMA);
|
|
154
|
+
}
|
|
155
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close cors config");
|
|
156
|
+
return new AST.CorsDeclaration(config, l);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
ParserClass.prototype.parseErrorHandler = function() {
|
|
160
|
+
const l = this.loc();
|
|
161
|
+
this.advance(); // consume 'on_error'
|
|
162
|
+
this.expect(TokenType.FN);
|
|
163
|
+
this.expect(TokenType.LPAREN, "Expected '(' after 'fn'");
|
|
164
|
+
const params = this.parseParameterList();
|
|
165
|
+
this.expect(TokenType.RPAREN, "Expected ')' after error handler parameters");
|
|
166
|
+
const body = this.parseBlock();
|
|
167
|
+
return new AST.ErrorHandlerDeclaration(params, body, l);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
ParserClass.prototype.parseWebSocket = function() {
|
|
171
|
+
const l = this.loc();
|
|
172
|
+
this.advance(); // consume 'ws'
|
|
173
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'ws'");
|
|
174
|
+
|
|
175
|
+
const handlers = {};
|
|
176
|
+
const config = {};
|
|
177
|
+
const validEvents = ['on_open', 'on_message', 'on_close', 'on_error'];
|
|
178
|
+
const validConfigKeys = ['auth'];
|
|
179
|
+
|
|
180
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
181
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected WebSocket event handler name or config key").value;
|
|
182
|
+
if (validConfigKeys.includes(name)) {
|
|
183
|
+
// Config key: auth: <expr>
|
|
184
|
+
this.expect(TokenType.COLON, `Expected ':' after '${name}'`);
|
|
185
|
+
config[name] = this.parseExpression();
|
|
186
|
+
this.match(TokenType.COMMA);
|
|
187
|
+
} else if (validEvents.includes(name)) {
|
|
188
|
+
this.expect(TokenType.FN, "Expected 'fn' after event name");
|
|
189
|
+
this.expect(TokenType.LPAREN);
|
|
190
|
+
const params = this.parseParameterList();
|
|
191
|
+
this.expect(TokenType.RPAREN);
|
|
192
|
+
const body = this.parseBlock();
|
|
193
|
+
handlers[name] = { params, body };
|
|
194
|
+
} else {
|
|
195
|
+
this.error(`Invalid WebSocket key '${name}'. Expected one of: ${[...validConfigKeys, ...validEvents].join(', ')}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close ws block");
|
|
200
|
+
const wsConfig = Object.keys(config).length > 0 ? config : null;
|
|
201
|
+
return new AST.WebSocketDeclaration(handlers, l, wsConfig);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
ParserClass.prototype.parseStaticDeclaration = function() {
|
|
205
|
+
const l = this.loc();
|
|
206
|
+
this.advance(); // consume 'static'
|
|
207
|
+
const urlPath = this.expect(TokenType.STRING, "Expected URL path for static files").value;
|
|
208
|
+
this.expect(TokenType.ARROW, "Expected '=>' after static path");
|
|
209
|
+
const dir = this.expect(TokenType.STRING, "Expected directory path for static files").value;
|
|
210
|
+
let fallback = null;
|
|
211
|
+
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'fallback') {
|
|
212
|
+
this.advance(); // consume 'fallback'
|
|
213
|
+
fallback = this.expect(TokenType.STRING, "Expected fallback file path").value;
|
|
214
|
+
}
|
|
215
|
+
return new AST.StaticDeclaration(urlPath, dir, l, fallback);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
ParserClass.prototype.parseDiscover = function() {
|
|
219
|
+
const l = this.loc();
|
|
220
|
+
this.advance(); // consume 'discover'
|
|
221
|
+
const peerName = this.expect(TokenType.STRING, "Expected peer name string after 'discover'").value;
|
|
222
|
+
// Expect 'at' as contextual keyword
|
|
223
|
+
const atTok = this.expect(TokenType.IDENTIFIER, "Expected 'at' after peer name");
|
|
224
|
+
if (atTok.value !== 'at') {
|
|
225
|
+
this.error("Expected 'at' after peer name in discover declaration");
|
|
226
|
+
}
|
|
227
|
+
const urlExpression = this.parseExpression();
|
|
228
|
+
let config = null;
|
|
229
|
+
if (this.check(TokenType.WITH)) {
|
|
230
|
+
this.advance(); // consume 'with'
|
|
231
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'with'");
|
|
232
|
+
config = {};
|
|
233
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
234
|
+
const key = this.expect(TokenType.IDENTIFIER, "Expected config key").value;
|
|
235
|
+
this.expect(TokenType.COLON, "Expected ':' after config key");
|
|
236
|
+
const value = this.parseExpression();
|
|
237
|
+
config[key] = value;
|
|
238
|
+
this.match(TokenType.COMMA);
|
|
239
|
+
}
|
|
240
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close discover config");
|
|
241
|
+
}
|
|
242
|
+
return new AST.DiscoverDeclaration(peerName, urlExpression, l, config);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
ParserClass.prototype.parseAuthConfig = function() {
|
|
246
|
+
const l = this.loc();
|
|
247
|
+
this.advance(); // consume 'auth'
|
|
248
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'auth'");
|
|
249
|
+
const config = {};
|
|
250
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
251
|
+
// Accept keywords (like 'type') and identifiers as config keys
|
|
252
|
+
let key;
|
|
253
|
+
if (this.check(TokenType.IDENTIFIER) || this.check(TokenType.TYPE)) {
|
|
254
|
+
key = this.advance().value;
|
|
255
|
+
} else {
|
|
256
|
+
this.error("Expected auth config key");
|
|
257
|
+
}
|
|
258
|
+
this.expect(TokenType.COLON, "Expected ':' after auth key");
|
|
259
|
+
const value = this.parseExpression();
|
|
260
|
+
config[key] = value;
|
|
261
|
+
this.match(TokenType.COMMA);
|
|
262
|
+
}
|
|
263
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close auth config");
|
|
264
|
+
return new AST.AuthDeclaration(config, l);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
ParserClass.prototype.parseMaxBody = function() {
|
|
268
|
+
const l = this.loc();
|
|
269
|
+
this.advance(); // consume 'max_body'
|
|
270
|
+
const limit = this.parseExpression();
|
|
271
|
+
return new AST.MaxBodyDeclaration(limit, l);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
ParserClass.prototype.parseRouteGroup = function() {
|
|
275
|
+
const l = this.loc();
|
|
276
|
+
this.advance(); // consume 'routes'
|
|
277
|
+
const prefix = this.expect(TokenType.STRING, "Expected route group prefix string").value;
|
|
278
|
+
|
|
279
|
+
// Optional version config: routes "/api/v2" version: "2" deprecated: true { ... }
|
|
280
|
+
let version = null;
|
|
281
|
+
while (this.check(TokenType.IDENTIFIER) && !this.isAtEnd()) {
|
|
282
|
+
const key = this.current().value;
|
|
283
|
+
if (key === 'version' || key === 'deprecated' || key === 'sunset') {
|
|
284
|
+
this.advance(); // consume key
|
|
285
|
+
this.expect(TokenType.COLON, `Expected ':' after '${key}'`);
|
|
286
|
+
const value = this.parseExpression();
|
|
287
|
+
if (!version) version = {};
|
|
288
|
+
if (key === 'version') {
|
|
289
|
+
version.version = value.value !== undefined ? value.value : value;
|
|
290
|
+
} else if (key === 'deprecated') {
|
|
291
|
+
version.deprecated = value.value !== undefined ? value.value : true;
|
|
292
|
+
} else if (key === 'sunset') {
|
|
293
|
+
version.sunset = value.value !== undefined ? value.value : value;
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.expect(TokenType.LBRACE, "Expected '{' after route group prefix");
|
|
301
|
+
const body = [];
|
|
302
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
303
|
+
try {
|
|
304
|
+
const stmt = this.parseServerStatement();
|
|
305
|
+
if (stmt) body.push(stmt);
|
|
306
|
+
} catch (e) {
|
|
307
|
+
this.errors.push(e);
|
|
308
|
+
this._synchronizeBlock();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close route group");
|
|
312
|
+
return new AST.RouteGroupDeclaration(prefix, body, l, version);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
ParserClass.prototype.parseRateLimitConfig = function() {
|
|
316
|
+
const l = this.loc();
|
|
317
|
+
this.advance(); // consume 'rate_limit'
|
|
318
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'rate_limit'");
|
|
319
|
+
const config = {};
|
|
320
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
321
|
+
const key = this.expect(TokenType.IDENTIFIER, "Expected rate_limit config key").value;
|
|
322
|
+
this.expect(TokenType.COLON, "Expected ':' after rate_limit key");
|
|
323
|
+
const value = this.parseExpression();
|
|
324
|
+
config[key] = value;
|
|
325
|
+
this.match(TokenType.COMMA);
|
|
326
|
+
}
|
|
327
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close rate_limit config");
|
|
328
|
+
return new AST.RateLimitDeclaration(config, l);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
ParserClass.prototype.parseLifecycleHook = function(hookName) {
|
|
332
|
+
const l = this.loc();
|
|
333
|
+
this.advance(); // consume 'on_start' or 'on_stop'
|
|
334
|
+
this.expect(TokenType.FN);
|
|
335
|
+
this.expect(TokenType.LPAREN, "Expected '(' after 'fn'");
|
|
336
|
+
const params = this.parseParameterList();
|
|
337
|
+
this.expect(TokenType.RPAREN, "Expected ')' after lifecycle hook parameters");
|
|
338
|
+
const body = this.parseBlock();
|
|
339
|
+
return new AST.LifecycleHookDeclaration(hookName, params, body, l);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
ParserClass.prototype.parseSubscribe = function() {
|
|
343
|
+
const l = this.loc();
|
|
344
|
+
this.advance(); // consume 'subscribe'
|
|
345
|
+
const event = this.expect(TokenType.STRING, "Expected event name string").value;
|
|
346
|
+
this.expect(TokenType.FN, "Expected 'fn' after event name");
|
|
347
|
+
this.expect(TokenType.LPAREN, "Expected '(' after 'fn'");
|
|
348
|
+
const params = this.parseParameterList();
|
|
349
|
+
this.expect(TokenType.RPAREN, "Expected ')' after subscribe parameters");
|
|
350
|
+
const body = this.parseBlock();
|
|
351
|
+
return new AST.SubscribeDeclaration(event, params, body, l);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
ParserClass.prototype.parseEnvDeclaration = function() {
|
|
355
|
+
const l = this.loc();
|
|
356
|
+
this.advance(); // consume 'env'
|
|
357
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected env variable name").value;
|
|
358
|
+
this.expect(TokenType.COLON, "Expected ':' after env variable name");
|
|
359
|
+
const typeAnnotation = this.parseTypeAnnotation();
|
|
360
|
+
let defaultValue = null;
|
|
361
|
+
if (this.match(TokenType.ASSIGN)) {
|
|
362
|
+
defaultValue = this.parseExpression();
|
|
363
|
+
}
|
|
364
|
+
return new AST.EnvDeclaration(name, typeAnnotation, defaultValue, l);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
ParserClass.prototype.parseSchedule = function() {
|
|
368
|
+
const l = this.loc();
|
|
369
|
+
this.advance(); // consume 'schedule'
|
|
370
|
+
const pattern = this.expect(TokenType.STRING, "Expected schedule pattern string").value;
|
|
371
|
+
this.expect(TokenType.FN, "Expected 'fn' after schedule pattern");
|
|
372
|
+
let name = null;
|
|
373
|
+
if (this.check(TokenType.IDENTIFIER)) {
|
|
374
|
+
name = this.advance().value;
|
|
375
|
+
}
|
|
376
|
+
this.expect(TokenType.LPAREN, "Expected '(' after schedule fn");
|
|
377
|
+
const params = this.parseParameterList();
|
|
378
|
+
this.expect(TokenType.RPAREN, "Expected ')' after schedule parameters");
|
|
379
|
+
const body = this.parseBlock();
|
|
380
|
+
return new AST.ScheduleDeclaration(pattern, name, params, body, l);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
ParserClass.prototype.parseUploadConfig = function() {
|
|
384
|
+
const l = this.loc();
|
|
385
|
+
this.advance(); // consume 'upload'
|
|
386
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'upload'");
|
|
387
|
+
const config = {};
|
|
388
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
389
|
+
const key = this.expect(TokenType.IDENTIFIER, "Expected upload config key").value;
|
|
390
|
+
this.expect(TokenType.COLON, "Expected ':' after upload key");
|
|
391
|
+
const value = this.parseExpression();
|
|
392
|
+
config[key] = value;
|
|
393
|
+
this.match(TokenType.COMMA);
|
|
394
|
+
}
|
|
395
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close upload config");
|
|
396
|
+
return new AST.UploadDeclaration(config, l);
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
ParserClass.prototype.parseSessionConfig = function() {
|
|
400
|
+
const l = this.loc();
|
|
401
|
+
this.advance(); // consume 'session'
|
|
402
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'session'");
|
|
403
|
+
const config = {};
|
|
404
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
405
|
+
const key = this.expect(TokenType.IDENTIFIER, "Expected session config key").value;
|
|
406
|
+
this.expect(TokenType.COLON, "Expected ':' after session key");
|
|
407
|
+
const value = this.parseExpression();
|
|
408
|
+
config[key] = value;
|
|
409
|
+
this.match(TokenType.COMMA);
|
|
410
|
+
}
|
|
411
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close session config");
|
|
412
|
+
return new AST.SessionDeclaration(config, l);
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
ParserClass.prototype.parseAiConfig = function() {
|
|
416
|
+
const l = this.loc();
|
|
417
|
+
this.advance(); // consume 'ai'
|
|
418
|
+
|
|
419
|
+
// Optional name: ai "claude" { ... }
|
|
420
|
+
let name = null;
|
|
421
|
+
if (this.check(TokenType.STRING)) {
|
|
422
|
+
name = this.advance().value;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'ai'");
|
|
426
|
+
const config = {};
|
|
427
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
428
|
+
const key = this.expect(TokenType.IDENTIFIER, "Expected ai config key").value;
|
|
429
|
+
this.expect(TokenType.COLON, "Expected ':' after ai config key");
|
|
430
|
+
const value = this.parseExpression();
|
|
431
|
+
config[key] = value;
|
|
432
|
+
this.match(TokenType.COMMA);
|
|
433
|
+
}
|
|
434
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close ai config");
|
|
435
|
+
return new AST.AiConfigDeclaration(name, config, l);
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
ParserClass.prototype.parseDbConfig = function() {
|
|
439
|
+
const l = this.loc();
|
|
440
|
+
this.advance(); // consume 'db'
|
|
441
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'db'");
|
|
442
|
+
const config = {};
|
|
443
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
444
|
+
const key = this.expect(TokenType.IDENTIFIER, "Expected db config key").value;
|
|
445
|
+
this.expect(TokenType.COLON, "Expected ':' after db key");
|
|
446
|
+
const value = this.parseExpression();
|
|
447
|
+
config[key] = value;
|
|
448
|
+
this.match(TokenType.COMMA);
|
|
449
|
+
}
|
|
450
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close db config");
|
|
451
|
+
return new AST.DbDeclaration(config, l);
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
ParserClass.prototype.parseTlsConfig = function() {
|
|
455
|
+
const l = this.loc();
|
|
456
|
+
this.advance(); // consume 'tls'
|
|
457
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'tls'");
|
|
458
|
+
const config = {};
|
|
459
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
460
|
+
const key = this.expect(TokenType.IDENTIFIER, "Expected tls config key").value;
|
|
461
|
+
this.expect(TokenType.COLON, "Expected ':' after tls key");
|
|
462
|
+
const value = this.parseExpression();
|
|
463
|
+
config[key] = value;
|
|
464
|
+
this.match(TokenType.COMMA);
|
|
465
|
+
}
|
|
466
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close tls config");
|
|
467
|
+
return new AST.TlsDeclaration(config, l);
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
ParserClass.prototype.parseCompressionConfig = function() {
|
|
471
|
+
const l = this.loc();
|
|
472
|
+
this.advance(); // consume 'compression'
|
|
473
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'compression'");
|
|
474
|
+
const config = {};
|
|
475
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
476
|
+
const key = this.expect(TokenType.IDENTIFIER, "Expected compression config key").value;
|
|
477
|
+
this.expect(TokenType.COLON, "Expected ':' after compression key");
|
|
478
|
+
const value = this.parseExpression();
|
|
479
|
+
config[key] = value;
|
|
480
|
+
this.match(TokenType.COMMA);
|
|
481
|
+
}
|
|
482
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close compression config");
|
|
483
|
+
return new AST.CompressionDeclaration(config, l);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
ParserClass.prototype.parseBackgroundJob = function() {
|
|
487
|
+
const l = this.loc();
|
|
488
|
+
this.advance(); // consume 'background'
|
|
489
|
+
this.expect(TokenType.FN, "Expected 'fn' after 'background'");
|
|
490
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected background job name").value;
|
|
491
|
+
this.expect(TokenType.LPAREN, "Expected '(' after background job name");
|
|
492
|
+
const params = this.parseParameterList();
|
|
493
|
+
this.expect(TokenType.RPAREN, "Expected ')' after background job parameters");
|
|
494
|
+
const body = this.parseBlock();
|
|
495
|
+
return new AST.BackgroundJobDeclaration(name, params, body, l);
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
ParserClass.prototype.parseCacheConfig = function() {
|
|
499
|
+
const l = this.loc();
|
|
500
|
+
this.advance(); // consume 'cache'
|
|
501
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'cache'");
|
|
502
|
+
const config = {};
|
|
503
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
504
|
+
const key = this.expect(TokenType.IDENTIFIER, "Expected cache config key").value;
|
|
505
|
+
this.expect(TokenType.COLON, "Expected ':' after cache key");
|
|
506
|
+
const value = this.parseExpression();
|
|
507
|
+
config[key] = value;
|
|
508
|
+
this.match(TokenType.COMMA);
|
|
509
|
+
}
|
|
510
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close cache config");
|
|
511
|
+
return new AST.CacheDeclaration(config, l);
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
ParserClass.prototype.parseSseDeclaration = function() {
|
|
515
|
+
const l = this.loc();
|
|
516
|
+
this.advance(); // consume 'sse'
|
|
517
|
+
const path = this.expect(TokenType.STRING, "Expected SSE endpoint path").value;
|
|
518
|
+
this.expect(TokenType.FN, "Expected 'fn' after SSE path");
|
|
519
|
+
this.expect(TokenType.LPAREN, "Expected '(' after 'fn'");
|
|
520
|
+
const params = this.parseParameterList();
|
|
521
|
+
this.expect(TokenType.RPAREN, "Expected ')' after SSE parameters");
|
|
522
|
+
const body = this.parseBlock();
|
|
523
|
+
return new AST.SseDeclaration(path, params, body, l);
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
ParserClass.prototype.parseModelDeclaration = function() {
|
|
527
|
+
const l = this.loc();
|
|
528
|
+
this.advance(); // consume 'model'
|
|
529
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected model/type name after 'model'").value;
|
|
530
|
+
let config = null;
|
|
531
|
+
if (this.check(TokenType.LBRACE)) {
|
|
532
|
+
this.advance(); // consume '{'
|
|
533
|
+
config = {};
|
|
534
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
535
|
+
const key = this.expect(TokenType.IDENTIFIER, "Expected model config key").value;
|
|
536
|
+
this.expect(TokenType.COLON, "Expected ':' after model config key");
|
|
537
|
+
const value = this.parseExpression();
|
|
538
|
+
config[key] = value;
|
|
539
|
+
this.match(TokenType.COMMA);
|
|
540
|
+
}
|
|
541
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close model config");
|
|
542
|
+
}
|
|
543
|
+
return new AST.ModelDeclaration(name, config, l);
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
ParserClass.prototype.parseRoute = function() {
|
|
547
|
+
const l = this.loc();
|
|
548
|
+
this.expect(TokenType.ROUTE);
|
|
549
|
+
|
|
550
|
+
// HTTP method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS (as identifiers)
|
|
551
|
+
const methodTok = this.expect(TokenType.IDENTIFIER, "Expected HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)");
|
|
552
|
+
const method = methodTok.value.toUpperCase();
|
|
553
|
+
if (!['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].includes(method)) {
|
|
554
|
+
this.error(`Invalid HTTP method: ${method}`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const path = this.expect(TokenType.STRING, "Expected route path string");
|
|
558
|
+
|
|
559
|
+
// Optional body type annotation: route POST "/api/users" body: User => handler
|
|
560
|
+
let bodyType = null;
|
|
561
|
+
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'body') {
|
|
562
|
+
const next = this.peek(1);
|
|
563
|
+
if (next && next.type === TokenType.COLON) {
|
|
564
|
+
this.advance(); // consume 'body'
|
|
565
|
+
this.advance(); // consume ':'
|
|
566
|
+
bodyType = this.parseTypeAnnotation();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Optional decorators: route GET "/path" with auth, role("admin") => handler
|
|
571
|
+
let decorators = [];
|
|
572
|
+
if (this.check(TokenType.WITH)) {
|
|
573
|
+
this.advance(); // consume 'with'
|
|
574
|
+
// Parse comma-separated decorator list
|
|
575
|
+
do {
|
|
576
|
+
const decName = this.expect(TokenType.IDENTIFIER, "Expected decorator name").value;
|
|
577
|
+
let decArgs = [];
|
|
578
|
+
if (this.check(TokenType.LPAREN)) {
|
|
579
|
+
this.advance(); // (
|
|
580
|
+
while (!this.check(TokenType.RPAREN) && !this.isAtEnd()) {
|
|
581
|
+
decArgs.push(this.parseExpression());
|
|
582
|
+
if (!this.match(TokenType.COMMA)) break;
|
|
583
|
+
}
|
|
584
|
+
this.expect(TokenType.RPAREN, "Expected ')' after decorator arguments");
|
|
585
|
+
}
|
|
586
|
+
decorators.push({ name: decName, args: decArgs });
|
|
587
|
+
} while (this.match(TokenType.COMMA));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Optional response type annotation: route GET "/api/users" -> [User] => handler
|
|
591
|
+
let responseType = null;
|
|
592
|
+
if (this.check(TokenType.THIN_ARROW)) {
|
|
593
|
+
this.advance(); // consume '->'
|
|
594
|
+
responseType = this.parseTypeAnnotation();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
this.expect(TokenType.ARROW, "Expected '=>' after route path");
|
|
598
|
+
const handler = this.parseExpression();
|
|
599
|
+
|
|
600
|
+
return new AST.RouteDeclaration(method, path.value, handler, l, decorators, bodyType, responseType);
|
|
601
|
+
};
|
|
602
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Tova array method extensions — bridges Tova method syntax to JavaScript
|
|
2
|
+
// Allows: [1,2,3].sorted() instead of requiring sorted([1,2,3])
|
|
3
|
+
|
|
4
|
+
const methods = {
|
|
5
|
+
sorted(key) { const c = [...this]; if (key) c.sort((x, y) => { const kx = key(x), ky = key(y); return kx < ky ? -1 : kx > ky ? 1 : 0; }); else c.sort((x, y) => x < y ? -1 : x > y ? 1 : 0); return c; },
|
|
6
|
+
reversed() { return [...this].reverse(); },
|
|
7
|
+
unique() { return [...new Set(this)]; },
|
|
8
|
+
chunk(n) { const r = []; for (let i = 0; i < this.length; i += n) r.push(this.slice(i, i + n)); return r; },
|
|
9
|
+
flatten() { return this.flat(); },
|
|
10
|
+
first() { return this[0] ?? null; },
|
|
11
|
+
last() { return this[this.length - 1] ?? null; },
|
|
12
|
+
take(n) { return this.slice(0, n); },
|
|
13
|
+
drop(n) { return this.slice(n); },
|
|
14
|
+
compact() { return this.filter(v => v != null); },
|
|
15
|
+
sum() { return this.reduce((a, b) => a + b, 0); },
|
|
16
|
+
min_val() { if (this.length === 0) return null; let m = this[0]; for (let i = 1; i < this.length; i++) if (this[i] < m) m = this[i]; return m; },
|
|
17
|
+
max_val() { if (this.length === 0) return null; let m = this[0]; for (let i = 1; i < this.length; i++) if (this[i] > m) m = this[i]; return m; },
|
|
18
|
+
group_by(fn) { const r = {}; for (const v of this) { const k = fn(v); if (!r[k]) r[k] = []; r[k].push(v); } return r; },
|
|
19
|
+
partition(fn) { const y = [], n = []; for (const v of this) { (fn(v) ? y : n).push(v); } return [y, n]; },
|
|
20
|
+
zip_with(other) { const m = Math.min(this.length, other.length); const r = []; for (let i = 0; i < m; i++) r.push([this[i], other[i]]); return r; },
|
|
21
|
+
frequencies() { const r = {}; for (const v of this) { const k = String(v); r[k] = (r[k] || 0) + 1; } return r; },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
for (const [name, fn] of Object.entries(methods)) {
|
|
25
|
+
if (!Array.prototype[name]) {
|
|
26
|
+
Object.defineProperty(Array.prototype, name, {
|
|
27
|
+
value: fn,
|
|
28
|
+
writable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|