tova 0.11.24 → 0.12.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/package.json +2 -2
- package/src/analyzer/cli-analyzer.js +7 -0
- package/src/analyzer/edge-analyzer.js +19 -0
- package/src/analyzer/scope.js +1 -1
- package/src/analyzer/server-analyzer.js +10 -10
- package/src/cli/repl.js +8 -4
- package/src/codegen/base-codegen.js +27 -10
- package/src/lexer/tokens.js +3 -2
- package/src/parser/parser.js +12 -2
- package/src/version.js +1 -1
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tova",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Tova — a modern programming language that transpiles to JavaScript, unifying frontend and backend",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"tova": "
|
|
8
|
+
"tova": "bin/tova.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"bin/",
|
|
@@ -50,11 +50,18 @@ export function installCliAnalyzer(AnalyzerClass) {
|
|
|
50
50
|
|
|
51
51
|
// Visit command body with params in scope
|
|
52
52
|
this.pushScope('function');
|
|
53
|
+
const prevAsyncDepth = this._asyncDepth;
|
|
54
|
+
if (cmd.isAsync) {
|
|
55
|
+
this._asyncDepth++;
|
|
56
|
+
} else {
|
|
57
|
+
this._asyncDepth = 0;
|
|
58
|
+
}
|
|
53
59
|
for (const param of cmd.params) {
|
|
54
60
|
this.currentScope.define(param.name,
|
|
55
61
|
new Symbol(param.name, 'parameter', null, false, param.loc));
|
|
56
62
|
}
|
|
57
63
|
this.visitNode(cmd.body);
|
|
64
|
+
this._asyncDepth = prevAsyncDepth;
|
|
58
65
|
this.popScope();
|
|
59
66
|
}
|
|
60
67
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Edge-specific analyzer methods for the Tova language
|
|
2
2
|
// Extracted from analyzer.js for lazy loading — only loaded when edge { } blocks are encountered.
|
|
3
3
|
|
|
4
|
+
import { Symbol } from './scope.js';
|
|
5
|
+
|
|
4
6
|
export function installEdgeAnalyzer(AnalyzerClass) {
|
|
5
7
|
if (AnalyzerClass.prototype._edgeAnalyzerInstalled) return;
|
|
6
8
|
AnalyzerClass.prototype._edgeAnalyzerInstalled = true;
|
|
@@ -33,6 +35,19 @@ export function installEdgeAnalyzer(AnalyzerClass) {
|
|
|
33
35
|
|
|
34
36
|
this.pushScope('edge');
|
|
35
37
|
|
|
38
|
+
// Pre-define all binding names (kv, sql, storage, queue, env, secret) in scope
|
|
39
|
+
// so route handlers can reference them without "not defined" warnings
|
|
40
|
+
for (const stmt of node.body) {
|
|
41
|
+
if (stmt.type === 'EdgeKVDeclaration' || stmt.type === 'EdgeSQLDeclaration' ||
|
|
42
|
+
stmt.type === 'EdgeStorageDeclaration' || stmt.type === 'EdgeQueueDeclaration' ||
|
|
43
|
+
stmt.type === 'EdgeEnvDeclaration' || stmt.type === 'EdgeSecretDeclaration') {
|
|
44
|
+
try {
|
|
45
|
+
this.currentScope.define(stmt.name,
|
|
46
|
+
new Symbol(stmt.name, 'binding', null, false, stmt.loc));
|
|
47
|
+
} catch (e) { /* ignore redefinition — duplicate check below handles it */ }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
let kvCount = 0;
|
|
37
52
|
const queueNames = new Set();
|
|
38
53
|
const consumers = [];
|
|
@@ -166,7 +181,11 @@ export function installEdgeAnalyzer(AnalyzerClass) {
|
|
|
166
181
|
// Visit child nodes — edge-specific types are noop in the registry,
|
|
167
182
|
// so explicitly visit bodies that contain statements
|
|
168
183
|
if (stmt.type === 'EdgeScheduleDeclaration' && stmt.body) {
|
|
184
|
+
// Schedule bodies are implicitly async (Cloudflare scheduled handler, Deno.cron callback)
|
|
185
|
+
const prevAsyncDepth = this._asyncDepth;
|
|
186
|
+
this._asyncDepth++;
|
|
169
187
|
for (const s of stmt.body.body || []) this.visitNode(s);
|
|
188
|
+
this._asyncDepth = prevAsyncDepth;
|
|
170
189
|
} else if (stmt.type === 'FunctionDeclaration' || stmt.type === 'RouteDeclaration') {
|
|
171
190
|
this.visitNode(stmt);
|
|
172
191
|
}
|
package/src/analyzer/scope.js
CHANGED
|
@@ -60,7 +60,7 @@ export class Scope {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
getContext() {
|
|
63
|
-
if (this.context === 'server' || this.context === 'browser' || this.context === 'client' || this.context === 'shared') {
|
|
63
|
+
if (this.context === 'server' || this.context === 'browser' || this.context === 'client' || this.context === 'shared' || this.context === 'edge') {
|
|
64
64
|
return this.context;
|
|
65
65
|
}
|
|
66
66
|
if (this.parent) {
|
|
@@ -110,8 +110,8 @@ export function installServerAnalyzer(AnalyzerClass) {
|
|
|
110
110
|
|
|
111
111
|
AnalyzerClass.prototype.visitRouteDeclaration = function(node) {
|
|
112
112
|
const ctx = this.currentScope.getContext();
|
|
113
|
-
if (ctx !== 'server') {
|
|
114
|
-
this.error(`'route' can only be used inside a server block`, node.loc, "move this inside a server { } block", { code: 'E303' });
|
|
113
|
+
if (ctx !== 'server' && ctx !== 'edge') {
|
|
114
|
+
this.error(`'route' can only be used inside a server or edge block`, node.loc, "move this inside a server { } or edge { } block", { code: 'E303' });
|
|
115
115
|
}
|
|
116
116
|
this.visitExpression(node.handler);
|
|
117
117
|
|
|
@@ -145,8 +145,8 @@ export function installServerAnalyzer(AnalyzerClass) {
|
|
|
145
145
|
|
|
146
146
|
AnalyzerClass.prototype.visitMiddlewareDeclaration = function(node) {
|
|
147
147
|
const ctx = this.currentScope.getContext();
|
|
148
|
-
if (ctx !== 'server') {
|
|
149
|
-
this.error(`'middleware' can only be used inside a server block`, node.loc, "move this inside a server { } block", { code: 'E303' });
|
|
148
|
+
if (ctx !== 'server' && ctx !== 'edge') {
|
|
149
|
+
this.error(`'middleware' can only be used inside a server or edge block`, node.loc, "move this inside a server { } or edge { } block", { code: 'E303' });
|
|
150
150
|
}
|
|
151
151
|
try {
|
|
152
152
|
this.currentScope.define(node.name,
|
|
@@ -173,15 +173,15 @@ export function installServerAnalyzer(AnalyzerClass) {
|
|
|
173
173
|
|
|
174
174
|
AnalyzerClass.prototype.visitHealthCheckDeclaration = function(node) {
|
|
175
175
|
const ctx = this.currentScope.getContext();
|
|
176
|
-
if (ctx !== 'server') {
|
|
177
|
-
this.error(`'health' can only be used inside a server block`, node.loc, "move this inside a server { } block", { code: 'E303' });
|
|
176
|
+
if (ctx !== 'server' && ctx !== 'edge') {
|
|
177
|
+
this.error(`'health' can only be used inside a server or edge block`, node.loc, "move this inside a server { } or edge { } block", { code: 'E303' });
|
|
178
178
|
}
|
|
179
179
|
};
|
|
180
180
|
|
|
181
181
|
AnalyzerClass.prototype.visitCorsDeclaration = function(node) {
|
|
182
182
|
const ctx = this.currentScope.getContext();
|
|
183
|
-
if (ctx !== 'server') {
|
|
184
|
-
this.error(`'cors' can only be used inside a server block`, node.loc, "move this inside a server { } block", { code: 'E303' });
|
|
183
|
+
if (ctx !== 'server' && ctx !== 'edge') {
|
|
184
|
+
this.error(`'cors' can only be used inside a server or edge block`, node.loc, "move this inside a server { } or edge { } block", { code: 'E303' });
|
|
185
185
|
}
|
|
186
186
|
for (const value of Object.values(node.config)) {
|
|
187
187
|
this.visitExpression(value);
|
|
@@ -190,8 +190,8 @@ export function installServerAnalyzer(AnalyzerClass) {
|
|
|
190
190
|
|
|
191
191
|
AnalyzerClass.prototype.visitErrorHandlerDeclaration = function(node) {
|
|
192
192
|
const ctx = this.currentScope.getContext();
|
|
193
|
-
if (ctx !== 'server') {
|
|
194
|
-
this.error(`'on_error' can only be used inside a server block`, node.loc, "move this inside a server { } block", { code: 'E303' });
|
|
193
|
+
if (ctx !== 'server' && ctx !== 'edge') {
|
|
194
|
+
this.error(`'on_error' can only be used inside a server or edge block`, node.loc, "move this inside a server { } or edge { } block", { code: 'E303' });
|
|
195
195
|
}
|
|
196
196
|
const prevScope = this.currentScope;
|
|
197
197
|
this.currentScope = this.currentScope.child('function');
|
package/src/cli/repl.js
CHANGED
|
@@ -416,10 +416,14 @@ async function startRepl() {
|
|
|
416
416
|
}
|
|
417
417
|
} catch (e) {
|
|
418
418
|
// If return-wrapping fails, fall back to plain execution
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
419
|
+
try {
|
|
420
|
+
const fallbackCode = replCode + (allSave ? '\n' + allSave : '');
|
|
421
|
+
// REPL context: fallback execution of compiled Tova code (intentional dynamic eval)
|
|
422
|
+
const fn = new Function('__ctx', `${destructure}${fallbackCode}`);
|
|
423
|
+
fn(context);
|
|
424
|
+
} catch (e2) {
|
|
425
|
+
console.error(` Error: ${e2.message}`);
|
|
426
|
+
}
|
|
423
427
|
}
|
|
424
428
|
}
|
|
425
429
|
} catch (err) {
|
|
@@ -39,6 +39,8 @@ export class BaseCodegen {
|
|
|
39
39
|
this._substitutionShadowed = new Set(); // names shadowed by lambda/function params
|
|
40
40
|
// Track function nesting depth for auto-await of async stdlib functions
|
|
41
41
|
this._functionDepth = 0; // 0 = top level (scripts run in AsyncFunction wrapper)
|
|
42
|
+
// For-else break tracking: stack of __broke variable names
|
|
43
|
+
this._forElseBrokeStack = [];
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
static TYPED_ARRAY_MAP = {
|
|
@@ -298,7 +300,15 @@ export class BaseCodegen {
|
|
|
298
300
|
case 'ExpressionStatement': result = `${this.i()}${this.genExpression(node.expression)};`; break;
|
|
299
301
|
case 'BlockStatement': result = this.genBlock(node); break;
|
|
300
302
|
case 'CompoundAssignment': result = this.genCompoundAssignment(node); break;
|
|
301
|
-
case 'BreakStatement':
|
|
303
|
+
case 'BreakStatement': {
|
|
304
|
+
const brokeVar = this._forElseBrokeStack.length > 0 && !node.label ? this._forElseBrokeStack[this._forElseBrokeStack.length - 1] : null;
|
|
305
|
+
if (brokeVar) {
|
|
306
|
+
result = `${this.i()}${brokeVar} = true; break;`;
|
|
307
|
+
} else {
|
|
308
|
+
result = node.label ? `${this.i()}break ${node.label};` : `${this.i()}break;`;
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
302
312
|
case 'ContinueStatement': result = node.label ? `${this.i()}continue ${node.label};` : `${this.i()}continue;`; break;
|
|
303
313
|
case 'GuardStatement': result = this.genGuardStatement(node); break;
|
|
304
314
|
case 'InterfaceDeclaration': result = this.genInterfaceDeclaration(node); break;
|
|
@@ -517,13 +527,17 @@ export class BaseCodegen {
|
|
|
517
527
|
genLetDestructure(node) {
|
|
518
528
|
if (node.pattern.type === 'ObjectPattern') {
|
|
519
529
|
for (const p of node.pattern.properties) this.declareVar(p.value);
|
|
530
|
+
if (node.pattern.rest) this.declareVar(node.pattern.rest);
|
|
520
531
|
const props = node.pattern.properties.map(p => {
|
|
521
532
|
let str = p.key;
|
|
522
533
|
if (p.value !== p.key) str += `: ${p.value}`;
|
|
523
534
|
if (p.defaultValue) str += ` = ${this.genExpression(p.defaultValue)}`;
|
|
524
535
|
return str;
|
|
525
|
-
})
|
|
526
|
-
|
|
536
|
+
});
|
|
537
|
+
if (node.pattern.rest) {
|
|
538
|
+
props.push(`...${node.pattern.rest}`);
|
|
539
|
+
}
|
|
540
|
+
return `${this.i()}const { ${props.join(', ')} } = ${this.genExpression(node.value)};`;
|
|
527
541
|
}
|
|
528
542
|
if (node.pattern.type === 'ArrayPattern' || node.pattern.type === 'TuplePattern') {
|
|
529
543
|
for (const e of node.pattern.elements) {
|
|
@@ -898,14 +912,14 @@ export class BaseCodegen {
|
|
|
898
912
|
const iterExpr = this.genExpression(node.iterable);
|
|
899
913
|
|
|
900
914
|
if (node.elseBody) {
|
|
901
|
-
// for-else: run else if
|
|
915
|
+
// for-else: run else if loop completes without hitting break
|
|
902
916
|
const tempVar = `__iter_${this._uid()}`;
|
|
903
|
-
const
|
|
917
|
+
const brokeVar = `__broke_${this._uid()}`;
|
|
904
918
|
const p = [];
|
|
905
919
|
p.push(`${this.i()}{\n`);
|
|
906
920
|
this.indent++;
|
|
907
921
|
p.push(`${this.i()}const ${tempVar} = ${iterExpr};\n`);
|
|
908
|
-
p.push(`${this.i()}let ${
|
|
922
|
+
p.push(`${this.i()}let ${brokeVar} = false;\n`);
|
|
909
923
|
this.pushScope();
|
|
910
924
|
for (const v of vars) this.declareVar(v);
|
|
911
925
|
if (vars.length === 2) {
|
|
@@ -916,16 +930,18 @@ export class BaseCodegen {
|
|
|
916
930
|
p.push(`${this.i()}${labelPrefix}for${awaitKeyword} (const ${vars[0]} of ${tempVar}) {\n`);
|
|
917
931
|
}
|
|
918
932
|
this.indent++;
|
|
919
|
-
p.push(`${this.i()}${enteredVar} = true;\n`);
|
|
920
933
|
if (node.guard) {
|
|
921
934
|
p.push(`${this.i()}if (!(${this.genExpression(node.guard)})) continue;\n`);
|
|
922
935
|
}
|
|
936
|
+
// Push broke var so break statements inside the body set it
|
|
937
|
+
this._forElseBrokeStack.push(brokeVar);
|
|
923
938
|
p.push(this.genBlockStatements(node.body));
|
|
939
|
+
this._forElseBrokeStack.pop();
|
|
924
940
|
this.indent--;
|
|
925
941
|
p.push(`\n${this.i()}}\n`);
|
|
926
942
|
this.popScope();
|
|
927
943
|
this.pushScope();
|
|
928
|
-
p.push(`${this.i()}if (!${
|
|
944
|
+
p.push(`${this.i()}if (!${brokeVar}) {\n`);
|
|
929
945
|
this.indent++;
|
|
930
946
|
p.push(this.genBlockStatements(node.elseBody));
|
|
931
947
|
this.indent--;
|
|
@@ -3356,8 +3372,9 @@ export class BaseCodegen {
|
|
|
3356
3372
|
const step = this.genExpression(node.step);
|
|
3357
3373
|
const s = node.start ? this.genExpression(node.start) : 'null';
|
|
3358
3374
|
const e = node.end ? this.genExpression(node.end) : 'null';
|
|
3359
|
-
// Handles both positive and negative step directions
|
|
3360
|
-
|
|
3375
|
+
// Handles both positive and negative step directions; guards against step === 0
|
|
3376
|
+
// Normalizes negative indices relative to array length
|
|
3377
|
+
return `((a, s, e, st) => { if (st === 0) return []; const len = a.length; if (s !== null && s < 0) s = Math.max(0, len + s); if (e !== null && e < 0) e = Math.max(0, len + e); const r = []; if (st > 0) { for (let i = s !== null ? s : 0; i < (e !== null ? e : len); i += st) r.push(a[i]); } else { for (let i = s !== null ? s : len - 1; i > (e !== null ? e : -1); i += st) r.push(a[i]); } return r; })(${obj}, ${s}, ${e}, ${step})`;
|
|
3361
3378
|
}
|
|
3362
3379
|
|
|
3363
3380
|
if (!start && !end) return `${obj}.slice()`;
|
package/src/lexer/tokens.js
CHANGED
|
@@ -183,7 +183,8 @@ export const TokenType = {
|
|
|
183
183
|
};
|
|
184
184
|
|
|
185
185
|
// Keywords map for quick lookup during lexing
|
|
186
|
-
|
|
186
|
+
// Use Object.create(null) to avoid prototype pollution (e.g., 'toString', 'valueOf')
|
|
187
|
+
export const Keywords = Object.assign(Object.create(null), {
|
|
187
188
|
'var': TokenType.VAR,
|
|
188
189
|
'let': TokenType.LET,
|
|
189
190
|
'fn': TokenType.FN,
|
|
@@ -242,7 +243,7 @@ export const Keywords = {
|
|
|
242
243
|
'field': TokenType.FIELD,
|
|
243
244
|
'group': TokenType.GROUP,
|
|
244
245
|
'steps': TokenType.STEPS,
|
|
245
|
-
};
|
|
246
|
+
});
|
|
246
247
|
|
|
247
248
|
// Token class
|
|
248
249
|
export class Token {
|
package/src/parser/parser.js
CHANGED
|
@@ -1625,7 +1625,7 @@ export class Parser {
|
|
|
1625
1625
|
if (expr._isDestructurePattern) {
|
|
1626
1626
|
// Already parsed as a destructuring pattern with defaults
|
|
1627
1627
|
pattern = new AST.ObjectPattern(
|
|
1628
|
-
expr.properties.map(p => {
|
|
1628
|
+
expr.properties.filter(p => !p.spread).map(p => {
|
|
1629
1629
|
const key = typeof p.key === 'string' ? p.key : p.key.name || p.key;
|
|
1630
1630
|
const val = p.shorthand ? key
|
|
1631
1631
|
: (p.value && p.value.type === 'Identifier' ? p.value.name : key);
|
|
@@ -1633,9 +1633,14 @@ export class Parser {
|
|
|
1633
1633
|
}),
|
|
1634
1634
|
expr.loc
|
|
1635
1635
|
);
|
|
1636
|
+
// Handle rest/spread element: { name, ...other }
|
|
1637
|
+
const spreadProp = expr.properties.find(p => p.spread);
|
|
1638
|
+
if (spreadProp && spreadProp.argument && spreadProp.argument.type === 'Identifier') {
|
|
1639
|
+
pattern.rest = spreadProp.argument.name;
|
|
1640
|
+
}
|
|
1636
1641
|
} else {
|
|
1637
1642
|
pattern = new AST.ObjectPattern(
|
|
1638
|
-
expr.properties.map(p => {
|
|
1643
|
+
expr.properties.filter(p => !p.spread).map(p => {
|
|
1639
1644
|
const key = typeof p.key === 'string' ? p.key : p.key.name || p.key;
|
|
1640
1645
|
let val, defaultValue = p.defaultValue || null;
|
|
1641
1646
|
if (p.shorthand) {
|
|
@@ -1663,6 +1668,11 @@ export class Parser {
|
|
|
1663
1668
|
}),
|
|
1664
1669
|
expr.loc
|
|
1665
1670
|
);
|
|
1671
|
+
// Handle rest/spread element: { name, ...other }
|
|
1672
|
+
const spreadProp = expr.properties.find(p => p.spread);
|
|
1673
|
+
if (spreadProp && spreadProp.argument && spreadProp.argument.type === 'Identifier') {
|
|
1674
|
+
pattern.rest = spreadProp.argument.name;
|
|
1675
|
+
}
|
|
1666
1676
|
}
|
|
1667
1677
|
const value = this.parseExpression();
|
|
1668
1678
|
return new AST.LetDestructure(pattern, value, l);
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/embed-runtime.js — do not edit
|
|
2
|
-
export const VERSION = "0.
|
|
2
|
+
export const VERSION = "0.12.0";
|