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 CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "tova",
3
- "version": "0.11.24",
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": "./bin/tova.js"
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
  }
@@ -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
- const fallbackCode = replCode + (allSave ? '\n' + allSave : '');
420
- // REPL context: fallback execution of compiled Tova code (intentional dynamic eval)
421
- const fn = new Function('__ctx', `${destructure}${fallbackCode}`);
422
- fn(context);
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': result = node.label ? `${this.i()}break ${node.label};` : `${this.i()}break;`; break;
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
- }).join(', ');
526
- return `${this.i()}const { ${props} } = ${this.genExpression(node.value)};`;
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 iterable was empty
915
+ // for-else: run else if loop completes without hitting break
902
916
  const tempVar = `__iter_${this._uid()}`;
903
- const enteredVar = `__entered_${this._uid()}`;
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 ${enteredVar} = false;\n`);
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 (!${enteredVar}) {\n`);
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
- return `((a, s, e, st) => { const r = []; if (st > 0) { for (let i = s !== null ? s : 0; i < (e !== null ? e : a.length); i += st) r.push(a[i]); } else { for (let i = s !== null ? s : a.length - 1; i > (e !== null ? e : -1); i += st) r.push(a[i]); } return r; })(${obj}, ${s}, ${e}, ${step})`;
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()`;
@@ -183,7 +183,8 @@ export const TokenType = {
183
183
  };
184
184
 
185
185
  // Keywords map for quick lookup during lexing
186
- export const Keywords = {
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 {
@@ -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.11.24";
2
+ export const VERSION = "0.12.0";