tova 0.5.0 → 0.7.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.
Files changed (43) hide show
  1. package/bin/tova.js +109 -57
  2. package/package.json +7 -2
  3. package/src/analyzer/analyzer.js +315 -79
  4. package/src/analyzer/{client-analyzer.js → browser-analyzer.js} +20 -17
  5. package/src/analyzer/form-analyzer.js +113 -0
  6. package/src/analyzer/scope.js +2 -2
  7. package/src/codegen/base-codegen.js +1 -0
  8. package/src/codegen/{client-codegen.js → browser-codegen.js} +444 -5
  9. package/src/codegen/cli-codegen.js +386 -0
  10. package/src/codegen/codegen.js +163 -45
  11. package/src/codegen/edge-codegen.js +1351 -0
  12. package/src/codegen/form-codegen.js +553 -0
  13. package/src/codegen/security-codegen.js +5 -5
  14. package/src/codegen/server-codegen.js +88 -7
  15. package/src/diagnostics/error-codes.js +1 -1
  16. package/src/docs/generator.js +1 -1
  17. package/src/formatter/formatter.js +4 -4
  18. package/src/lexer/tokens.js +12 -2
  19. package/src/lsp/server.js +1 -1
  20. package/src/parser/ast.js +45 -5
  21. package/src/parser/{client-ast.js → browser-ast.js} +3 -3
  22. package/src/parser/{client-parser.js → browser-parser.js} +42 -15
  23. package/src/parser/cli-ast.js +35 -0
  24. package/src/parser/cli-parser.js +140 -0
  25. package/src/parser/edge-ast.js +83 -0
  26. package/src/parser/edge-parser.js +262 -0
  27. package/src/parser/form-ast.js +80 -0
  28. package/src/parser/form-parser.js +206 -0
  29. package/src/parser/parser.js +86 -53
  30. package/src/registry/block-registry.js +56 -0
  31. package/src/registry/plugins/bench-plugin.js +23 -0
  32. package/src/registry/plugins/browser-plugin.js +30 -0
  33. package/src/registry/plugins/cli-plugin.js +24 -0
  34. package/src/registry/plugins/data-plugin.js +21 -0
  35. package/src/registry/plugins/edge-plugin.js +32 -0
  36. package/src/registry/plugins/security-plugin.js +27 -0
  37. package/src/registry/plugins/server-plugin.js +46 -0
  38. package/src/registry/plugins/shared-plugin.js +17 -0
  39. package/src/registry/plugins/test-plugin.js +23 -0
  40. package/src/registry/register-all.js +25 -0
  41. package/src/runtime/ssr.js +2 -2
  42. package/src/stdlib/inline.js +178 -3
  43. package/src/version.js +1 -1
@@ -136,6 +136,69 @@ export class ServerCodegen extends BaseCodegen {
136
136
  return checks;
137
137
  }
138
138
 
139
+ // Generate validation checks from type-level validators (Phase 3)
140
+ // Returns an array of JS code lines for inline validation
141
+ _genTypeValidatorCode(paramName, typeInfo, indent = ' ') {
142
+ const checks = [];
143
+ for (const field of typeInfo.fields) {
144
+ if (!field.validators || field.validators.length === 0) continue;
145
+ const accessor = `${paramName}.${field.name}`;
146
+ for (const v of field.validators) {
147
+ // The last argument is typically the error message
148
+ const msgArg = v.args.length > 0 ? this.genExpression(v.args[v.args.length - 1]) : null;
149
+ switch (v.name) {
150
+ case 'required': {
151
+ const msg = msgArg || `"${field.name} is required"`;
152
+ checks.push(`${indent}if (${accessor} === undefined || ${accessor} === null || ${accessor} === "") __validationErrors.push({ field: "${field.name}", message: ${msg} });`);
153
+ break;
154
+ }
155
+ case 'email': {
156
+ const msg = msgArg || `"${field.name} must be a valid email"`;
157
+ checks.push(`${indent}if (${accessor} && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(${accessor})) __validationErrors.push({ field: "${field.name}", message: ${msg} });`);
158
+ break;
159
+ }
160
+ case 'min': {
161
+ const minVal = this.genExpression(v.args[0]);
162
+ const minMsg = v.args.length >= 2 ? this.genExpression(v.args[1]) : `"${field.name} must be at least ${v.args[0].value || v.args[0].name}"`;
163
+ checks.push(`${indent}if (typeof ${accessor} === "number" && ${accessor} < ${minVal}) __validationErrors.push({ field: "${field.name}", message: ${minMsg} });`);
164
+ break;
165
+ }
166
+ case 'max': {
167
+ const maxVal = this.genExpression(v.args[0]);
168
+ const maxMsg = v.args.length >= 2 ? this.genExpression(v.args[1]) : `"${field.name} must be at most ${v.args[0].value || v.args[0].name}"`;
169
+ checks.push(`${indent}if (typeof ${accessor} === "number" && ${accessor} > ${maxVal}) __validationErrors.push({ field: "${field.name}", message: ${maxMsg} });`);
170
+ break;
171
+ }
172
+ case 'minLength': {
173
+ const minLen = this.genExpression(v.args[0]);
174
+ const minLenMsg = v.args.length >= 2 ? this.genExpression(v.args[1]) : `"${field.name} is too short"`;
175
+ checks.push(`${indent}if (typeof ${accessor} === "string" && ${accessor}.length < ${minLen}) __validationErrors.push({ field: "${field.name}", message: ${minLenMsg} });`);
176
+ break;
177
+ }
178
+ case 'maxLength': {
179
+ const maxLen = this.genExpression(v.args[0]);
180
+ const maxLenMsg = v.args.length >= 2 ? this.genExpression(v.args[1]) : `"${field.name} is too long"`;
181
+ checks.push(`${indent}if (typeof ${accessor} === "string" && ${accessor}.length > ${maxLen}) __validationErrors.push({ field: "${field.name}", message: ${maxLenMsg} });`);
182
+ break;
183
+ }
184
+ case 'pattern': {
185
+ const regex = this.genExpression(v.args[0]);
186
+ const patMsg = v.args.length >= 2 ? this.genExpression(v.args[1]) : `"${field.name} has invalid format"`;
187
+ checks.push(`${indent}if (typeof ${accessor} === "string" && !${regex}.test(${accessor})) __validationErrors.push({ field: "${field.name}", message: ${patMsg} });`);
188
+ break;
189
+ }
190
+ case 'oneOf': {
191
+ const vals = this.genExpression(v.args[0]);
192
+ const oneOfMsg = v.args.length >= 2 ? this.genExpression(v.args[1]) : `"${field.name} has invalid value"`;
193
+ checks.push(`${indent}if (!${vals}.includes(${accessor})) __validationErrors.push({ field: "${field.name}", message: ${oneOfMsg} });`);
194
+ break;
195
+ }
196
+ }
197
+ }
198
+ }
199
+ return checks;
200
+ }
201
+
139
202
  // Emit handler call, optionally wrapped in Promise.race for timeout
140
203
  _emitHandlerCall(lines, callExpr, timeoutMs) {
141
204
  if (timeoutMs) {
@@ -304,14 +367,19 @@ export class ServerCodegen extends BaseCodegen {
304
367
  }
305
368
 
306
369
  // Collect type declarations from shared blocks for model/ORM generation
307
- const sharedTypes = new Map(); // typeName -> { fields: [{ name, type }] }
370
+ const sharedTypes = new Map(); // typeName -> { fields: [{ name, type, validators? }] }
308
371
  const _collectTypes = (stmts) => {
309
372
  for (const stmt of stmts) {
310
373
  if (stmt.type === 'TypeDeclaration' && stmt.variants) {
311
374
  const fields = [];
312
375
  for (const v of stmt.variants) {
313
376
  if (v.type === 'TypeField' && v.typeAnnotation) {
314
- fields.push({ name: v.name, type: v.typeAnnotation.name || (v.typeAnnotation.type === 'ArrayTypeAnnotation' ? 'Array' : 'Any') });
377
+ const fieldInfo = { name: v.name, type: v.typeAnnotation.name || (v.typeAnnotation.type === 'ArrayTypeAnnotation' ? 'Array' : 'Any') };
378
+ // Capture type-level validators (Phase 3) if present
379
+ if (v.validators && v.validators.length > 0) {
380
+ fieldInfo.validators = v.validators;
381
+ }
382
+ fields.push(fieldInfo);
315
383
  }
316
384
  }
317
385
  if (fields.length > 0) {
@@ -2166,9 +2234,19 @@ export class ServerCodegen extends BaseCodegen {
2166
2234
  lines.push(` const ${paramNames[pi]} = body.__args ? body.__args[${pi}] : body.${paramNames[pi]};`);
2167
2235
  }
2168
2236
  const validationChecks = this._genValidationCode(fn.params);
2169
- if (validationChecks.length > 0) {
2237
+ // Phase 3: Also generate type-level validator checks for typed parameters
2238
+ const typeValidatorChecks = [];
2239
+ for (const p of fn.params) {
2240
+ if (p.typeAnnotation && p.typeAnnotation.type === 'TypeAnnotation' && sharedTypes.has(p.typeAnnotation.name)) {
2241
+ const typeInfo = sharedTypes.get(p.typeAnnotation.name);
2242
+ const tvChecks = this._genTypeValidatorCode(p.name, typeInfo);
2243
+ typeValidatorChecks.push(...tvChecks);
2244
+ }
2245
+ }
2246
+ const allChecks = [...validationChecks, ...typeValidatorChecks];
2247
+ if (allChecks.length > 0) {
2170
2248
  lines.push(` const __validationErrors = [];`);
2171
- for (const check of validationChecks) {
2249
+ for (const check of allChecks) {
2172
2250
  lines.push(check);
2173
2251
  }
2174
2252
  lines.push(` if (__validationErrors.length > 0) return __errorResponse(400, "VALIDATION_FAILED", "Validation failed", __validationErrors);`);
@@ -2702,6 +2780,9 @@ export class ServerCodegen extends BaseCodegen {
2702
2780
  lines.push(' responses: { "200": { description: "Success" } },');
2703
2781
  }
2704
2782
 
2783
+ // Close the OpenAPI path entry object
2784
+ lines.push('};');
2785
+
2705
2786
  // Auto-generate error responses based on route context
2706
2787
  const routeHasAuth = (route.decorators || []).some(d => d.name === 'auth');
2707
2788
  const routeHasRateLimit = (route.decorators || []).some(d => d.name === 'rate_limit');
@@ -2709,7 +2790,8 @@ export class ServerCodegen extends BaseCodegen {
2709
2790
  const routeHasTimeout = (route.decorators || []).some(d => d.name === 'timeout');
2710
2791
  const errRef = '{ "$ref": "#/components/schemas/ErrorResponse" }';
2711
2792
 
2712
- // Merge error responses into existing 200 response
2793
+ // Merge error responses into existing 200 response (wrapped in block scope to avoid __r redeclaration)
2794
+ lines.push('{');
2713
2795
  lines.push(`const __r = __openApiSpec.paths[${JSON.stringify(path)}][${JSON.stringify(method)}].responses;`);
2714
2796
  if (routeHasValidation || ['post', 'put', 'patch'].includes(method)) {
2715
2797
  lines.push(`__r["400"] = { description: "Validation Failed", content: { "application/json": { schema: ${errRef} } } };`);
@@ -2733,8 +2815,7 @@ export class ServerCodegen extends BaseCodegen {
2733
2815
  lines.push(`__r["504"] = { description: "Gateway Timeout", content: { "application/json": { schema: ${errRef} } } };`);
2734
2816
  }
2735
2817
  lines.push(`__r["500"] = { description: "Internal Server Error", content: { "application/json": { schema: ${errRef} } } };`);
2736
-
2737
- lines.push('};');
2818
+ lines.push('}');
2738
2819
  }
2739
2820
 
2740
2821
  // Add the /docs endpoint
@@ -33,7 +33,7 @@ export const ErrorCode = {
33
33
  // === Context Errors (E300–E399) ===
34
34
  E300: { code: 'E300', title: 'Invalid context: await', category: 'context' },
35
35
  E301: { code: 'E301', title: 'Invalid context: return', category: 'context' },
36
- E302: { code: 'E302', title: 'Invalid context: client-only', category: 'context' },
36
+ E302: { code: 'E302', title: 'Invalid context: browser-only', category: 'context' },
37
37
  E303: { code: 'E303', title: 'Invalid context: server-only', category: 'context' },
38
38
  E304: { code: 'E304', title: 'Invalid context: function-only', category: 'context' },
39
39
 
@@ -25,7 +25,7 @@ export class DocGenerator {
25
25
  if (entry) docs.push(entry);
26
26
  }
27
27
  if (node.body && Array.isArray(node.body)) walk(node.body);
28
- if ((node.type === 'ServerBlock' || node.type === 'ClientBlock' || node.type === 'SharedBlock') && node.body) {
28
+ if ((node.type === 'ServerBlock' || node.type === 'BrowserBlock' || node.type === 'SharedBlock') && node.body) {
29
29
  walk(node.body);
30
30
  }
31
31
  }
@@ -31,7 +31,7 @@ export class Formatter {
31
31
 
32
32
  _needsBlankLine(prevType, currType) {
33
33
  const declTypes = ['FunctionDeclaration', 'TypeDeclaration', 'InterfaceDeclaration',
34
- 'ImplDeclaration', 'TraitDeclaration', 'ServerBlock', 'ClientBlock', 'SharedBlock',
34
+ 'ImplDeclaration', 'TraitDeclaration', 'ServerBlock', 'BrowserBlock', 'SharedBlock',
35
35
  'ComponentDeclaration', 'TestBlock'];
36
36
  return declTypes.includes(prevType) || declTypes.includes(currType);
37
37
  }
@@ -63,7 +63,7 @@ export class Formatter {
63
63
  case 'TraitDeclaration': return this.formatTraitDeclaration(node);
64
64
  case 'DeferStatement': return this.formatDeferStatement(node);
65
65
  case 'ServerBlock': return this.formatServerBlock(node);
66
- case 'ClientBlock': return this.formatClientBlock(node);
66
+ case 'BrowserBlock': return this.formatBrowserBlock(node);
67
67
  case 'SharedBlock': return this.formatSharedBlock(node);
68
68
  case 'CompoundAssignment': return `${this.i()}${this.formatExpr(node.target)} ${node.operator} ${this.formatExpr(node.value)}`;
69
69
  case 'BlockStatement': return this.formatBlock(node);
@@ -424,9 +424,9 @@ export class Formatter {
424
424
  return code;
425
425
  }
426
426
 
427
- formatClientBlock(node) {
427
+ formatBrowserBlock(node) {
428
428
  const name = node.name ? ` "${node.name}"` : '';
429
- let code = `${this.i()}client${name} {\n`;
429
+ let code = `${this.i()}browser${name} {\n`;
430
430
  this.indent++;
431
431
  code += node.body.map(s => this.formatNode(s)).join('\n');
432
432
  this.indent--;
@@ -85,7 +85,8 @@ export const TokenType = {
85
85
 
86
86
  // Full-stack keywords
87
87
  SERVER: 'SERVER',
88
- CLIENT: 'CLIENT',
88
+ BROWSER: 'BROWSER',
89
+ CLIENT: 'BROWSER', // deprecated alias — both map to BROWSER
89
90
  SHARED: 'SHARED',
90
91
  ROUTE: 'ROUTE',
91
92
  STATE: 'STATE',
@@ -93,6 +94,10 @@ export const TokenType = {
93
94
  EFFECT: 'EFFECT',
94
95
  COMPONENT: 'COMPONENT',
95
96
  STORE: 'STORE',
97
+ FORM: 'FORM',
98
+ FIELD: 'FIELD',
99
+ GROUP: 'GROUP',
100
+ STEPS: 'STEPS',
96
101
  STYLE_BLOCK: 'STYLE_BLOCK',
97
102
 
98
103
  // HTTP methods (used in route declarations)
@@ -211,7 +216,8 @@ export const Keywords = {
211
216
  'is': TokenType.IS,
212
217
  'with': TokenType.WITH,
213
218
  'server': TokenType.SERVER,
214
- 'client': TokenType.CLIENT,
219
+ 'browser': TokenType.BROWSER,
220
+ 'client': TokenType.BROWSER, // deprecated alias
215
221
  'shared': TokenType.SHARED,
216
222
  'route': TokenType.ROUTE,
217
223
  'state': TokenType.STATE,
@@ -219,6 +225,10 @@ export const Keywords = {
219
225
  'effect': TokenType.EFFECT,
220
226
  'component': TokenType.COMPONENT,
221
227
  'store': TokenType.STORE,
228
+ 'form': TokenType.FORM,
229
+ 'field': TokenType.FIELD,
230
+ 'group': TokenType.GROUP,
231
+ 'steps': TokenType.STEPS,
222
232
  };
223
233
 
224
234
  // Token class
package/src/lsp/server.js CHANGED
@@ -475,7 +475,7 @@ class TovaLanguageServer {
475
475
  const keywords = [
476
476
  'fn', 'let', 'if', 'elif', 'else', 'for', 'while', 'loop', 'when', 'in',
477
477
  'return', 'match', 'type', 'import', 'from', 'true', 'false',
478
- 'nil', 'server', 'client', 'shared', 'pub', 'mut',
478
+ 'nil', 'server', 'browser', 'client', 'shared', 'pub', 'mut',
479
479
  'try', 'catch', 'finally', 'break', 'continue', 'async', 'await',
480
480
  'guard', 'interface', 'derive', 'route', 'model', 'db',
481
481
  ];
package/src/parser/ast.js CHANGED
@@ -27,14 +27,16 @@ export class ServerBlock {
27
27
  }
28
28
  }
29
29
 
30
- export class ClientBlock {
30
+ export class BrowserBlock {
31
31
  constructor(body, loc, name = null) {
32
- this.type = 'ClientBlock';
32
+ this.type = 'BrowserBlock';
33
33
  this.name = name;
34
34
  this.body = body;
35
35
  this.loc = loc;
36
36
  }
37
37
  }
38
+ // Deprecated alias for backward compatibility
39
+ export { BrowserBlock as ClientBlock };
38
40
 
39
41
  export class SharedBlock {
40
42
  constructor(body, loc, name = null) {
@@ -53,6 +55,24 @@ export class SecurityBlock {
53
55
  }
54
56
  }
55
57
 
58
+ export class CliBlock {
59
+ constructor(config, commands, loc) {
60
+ this.type = 'CliBlock';
61
+ this.config = config; // Array of CliConfigField
62
+ this.commands = commands; // Array of CliCommandDeclaration
63
+ this.loc = loc;
64
+ }
65
+ }
66
+
67
+ export class EdgeBlock {
68
+ constructor(body, loc, name = null) {
69
+ this.type = 'EdgeBlock';
70
+ this.name = name; // optional string name for named edge blocks
71
+ this.body = body; // Array of edge statements (config, bindings, routes, functions, etc.)
72
+ this.loc = loc;
73
+ }
74
+ }
75
+
56
76
  // ============================================================
57
77
  // Declarations
58
78
  // ============================================================
@@ -128,11 +148,12 @@ export class TypeVariant {
128
148
  }
129
149
 
130
150
  export class TypeField {
131
- constructor(name, typeAnnotation, loc) {
151
+ constructor(name, typeAnnotation, loc, validators) {
132
152
  this.type = 'TypeField';
133
153
  this.name = name;
134
154
  this.typeAnnotation = typeAnnotation;
135
155
  this.loc = loc;
156
+ this.validators = validators || [];
136
157
  }
137
158
  }
138
159
 
@@ -665,7 +686,7 @@ export class RangePattern {
665
686
  }
666
687
 
667
688
  // ============================================================
668
- // Client-specific nodes (lazy-loaded from client-ast.js, re-exported for backward compat)
689
+ // Browser-specific nodes (lazy-loaded from browser-ast.js, re-exported for backward compat)
669
690
  // ============================================================
670
691
 
671
692
  export {
@@ -673,7 +694,7 @@ export {
673
694
  ComponentDeclaration, ComponentStyleBlock, StoreDeclaration,
674
695
  JSXElement, JSXAttribute, JSXSpreadAttribute, JSXFragment,
675
696
  JSXText, JSXExpression, JSXFor, JSXIf, JSXMatch,
676
- } from './client-ast.js';
697
+ } from './browser-ast.js';
677
698
 
678
699
  // ============================================================
679
700
  // Server-specific nodes (lazy-loaded from server-ast.js, re-exported for backward compat)
@@ -704,6 +725,25 @@ export {
704
725
  SecurityTrustProxyDeclaration, SecurityHstsDeclaration,
705
726
  } from './security-ast.js';
706
727
 
728
+ // ============================================================
729
+ // Edge-specific nodes (lazy-loaded from edge-ast.js, re-exported for backward compat)
730
+ // ============================================================
731
+
732
+ export {
733
+ EdgeConfigField, EdgeKVDeclaration, EdgeSQLDeclaration,
734
+ EdgeStorageDeclaration, EdgeQueueDeclaration, EdgeEnvDeclaration,
735
+ EdgeSecretDeclaration, EdgeScheduleDeclaration, EdgeConsumeDeclaration,
736
+ } from './edge-ast.js';
737
+
738
+ // ============================================================
739
+ // Form-specific nodes (lazy-loaded from form-ast.js, re-exported for backward compat)
740
+ // ============================================================
741
+
742
+ export {
743
+ FormDeclaration, FormFieldDeclaration, FormGroupDeclaration,
744
+ FormArrayDeclaration, FormValidator, FormStepsDeclaration, FormStep,
745
+ } from './form-ast.js';
746
+
707
747
  export class TestBlock {
708
748
  constructor(name, body, loc, options = {}) {
709
749
  this.type = 'TestBlock';
@@ -1,8 +1,8 @@
1
- // Client-specific AST Node definitions for the Tova language
2
- // Extracted from ast.js for lazy loading — only loaded when client { } blocks are used.
1
+ // Browser-specific AST Node definitions for the Tova language
2
+ // Extracted from ast.js for lazy loading — only loaded when browser { } blocks are used.
3
3
 
4
4
  // ============================================================
5
- // Client-specific nodes
5
+ // Browser-specific nodes
6
6
  // ============================================================
7
7
 
8
8
  export class StateDeclaration {
@@ -1,42 +1,55 @@
1
- // Client-specific parser methods for the Tova language
2
- // Extracted from parser.js for lazy loading — only loaded when client { } blocks are encountered.
1
+ // Browser-specific parser methods for the Tova language
2
+ // Extracted from parser.js for lazy loading — only loaded when browser { } blocks are encountered.
3
3
 
4
4
  import { TokenType, Keywords } from '../lexer/tokens.js';
5
5
  import * as AST from './ast.js';
6
+ import { installFormParser } from './form-parser.js';
6
7
 
7
- export function installClientParser(ParserClass) {
8
- if (ParserClass.prototype._clientParserInstalled) return;
9
- ParserClass.prototype._clientParserInstalled = true;
8
+ export function installBrowserParser(ParserClass) {
9
+ if (ParserClass.prototype._browserParserInstalled) return;
10
+ ParserClass.prototype._browserParserInstalled = true;
10
11
 
11
- ParserClass.prototype.parseClientBlock = function() {
12
+ installFormParser(ParserClass);
13
+
14
+ ParserClass.prototype.parseBrowserBlock = function() {
12
15
  const l = this.loc();
13
- this.expect(TokenType.CLIENT);
14
- // Optional block name: client "admin" { }
16
+ // Capture the keyword value before consuming for deprecation warning
17
+ const keyword = this.current().value;
18
+ this.expect(TokenType.BROWSER);
19
+ if (keyword === 'client') {
20
+ this.warnings = this.warnings || [];
21
+ this.warnings.push({
22
+ message: "`client` block is deprecated, use `browser` instead",
23
+ loc: l,
24
+ });
25
+ }
26
+ // Optional block name: browser "admin" { }
15
27
  let name = null;
16
28
  if (this.check(TokenType.STRING)) {
17
29
  name = this.advance().value;
18
30
  }
19
- this.expect(TokenType.LBRACE, "Expected '{' after 'client'");
31
+ this.expect(TokenType.LBRACE, "Expected '{' after 'browser'");
20
32
  const body = [];
21
33
  while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
22
34
  try {
23
- const stmt = this.parseClientStatement();
35
+ const stmt = this.parseBrowserStatement();
24
36
  if (stmt) body.push(stmt);
25
37
  } catch (e) {
26
38
  this.errors.push(e);
27
39
  this._synchronizeBlock();
28
40
  }
29
41
  }
30
- this.expect(TokenType.RBRACE, "Expected '}' to close client block");
31
- return new AST.ClientBlock(body, l, name);
42
+ this.expect(TokenType.RBRACE, "Expected '}' to close browser block");
43
+ return new AST.BrowserBlock(body, l, name);
32
44
  };
33
45
 
34
- ParserClass.prototype.parseClientStatement = function() {
46
+ ParserClass.prototype.parseBrowserStatement = function() {
35
47
  if (this.check(TokenType.STATE)) return this.parseState();
36
48
  if (this.check(TokenType.COMPUTED)) return this.parseComputed();
37
49
  if (this.check(TokenType.EFFECT)) return this.parseEffect();
38
50
  if (this.check(TokenType.COMPONENT)) return this.parseComponent();
39
51
  if (this.check(TokenType.STORE)) return this.parseStore();
52
+ if (this.check(TokenType.FORM)) return this.parseFormDeclaration();
40
53
  return this.parseStatement();
41
54
  };
42
55
 
@@ -125,6 +138,8 @@ export function installClientParser(ParserClass) {
125
138
  body.push(this.parseEffect());
126
139
  } else if (this.check(TokenType.COMPONENT)) {
127
140
  body.push(this.parseComponent());
141
+ } else if (this.check(TokenType.FORM)) {
142
+ body.push(this.parseFormDeclaration());
128
143
  } else {
129
144
  body.push(this.parseStatement());
130
145
  }
@@ -242,7 +257,13 @@ export function installClientParser(ParserClass) {
242
257
  const l = this.loc();
243
258
  this.expect(TokenType.LESS, "Expected '<'");
244
259
 
245
- const tag = this.expect(TokenType.IDENTIFIER, "Expected tag name").value;
260
+ // Accept identifiers and keywords as JSX tag names (e.g., <form>, <label>)
261
+ let tag;
262
+ if (this.check(TokenType.IDENTIFIER) || (this.peek().value in Keywords)) {
263
+ tag = this.advance().value;
264
+ } else {
265
+ tag = this.expect(TokenType.IDENTIFIER, "Expected tag name").value;
266
+ }
246
267
 
247
268
  // Parse attributes (including spread: {...expr})
248
269
  const attributes = [];
@@ -330,7 +351,13 @@ export function installClientParser(ParserClass) {
330
351
  if (this.check(TokenType.LESS) && this.peek(1).type === TokenType.SLASH) {
331
352
  this.advance(); // <
332
353
  this.advance(); // /
333
- const closeTag = this.expect(TokenType.IDENTIFIER, "Expected closing tag name").value;
354
+ // Accept identifiers and keywords as JSX closing tag names (e.g., </form>)
355
+ let closeTag;
356
+ if (this.check(TokenType.IDENTIFIER) || (this.peek().value in Keywords)) {
357
+ closeTag = this.advance().value;
358
+ } else {
359
+ closeTag = this.expect(TokenType.IDENTIFIER, "Expected closing tag name").value;
360
+ }
334
361
  if (closeTag !== parentTag) {
335
362
  this.error(`Mismatched closing tag: expected </${parentTag}>, got </${closeTag}>`);
336
363
  }
@@ -0,0 +1,35 @@
1
+ // CLI-specific AST Node definitions for the Tova language
2
+ // Extracted for lazy loading — only loaded when cli { } blocks are used.
3
+
4
+ export class CliConfigField {
5
+ constructor(key, value, loc) {
6
+ this.type = 'CliConfigField';
7
+ this.key = key; // string — "name", "version", "description"
8
+ this.value = value; // Expression (StringLiteral, etc.)
9
+ this.loc = loc;
10
+ }
11
+ }
12
+
13
+ export class CliCommandDeclaration {
14
+ constructor(name, params, body, isAsync, loc) {
15
+ this.type = 'CliCommandDeclaration';
16
+ this.name = name; // string — command name
17
+ this.params = params; // Array of CliParam
18
+ this.body = body; // BlockStatement
19
+ this.isAsync = isAsync; // boolean
20
+ this.loc = loc;
21
+ }
22
+ }
23
+
24
+ export class CliParam {
25
+ constructor(name, typeAnnotation, defaultValue, isFlag, isOptional, isRepeated, loc) {
26
+ this.type = 'CliParam';
27
+ this.name = name; // string — parameter name
28
+ this.typeAnnotation = typeAnnotation; // string or null — "String", "Int", "Float", "Bool"
29
+ this.defaultValue = defaultValue; // Expression or null
30
+ this.isFlag = isFlag; // boolean — prefixed with --
31
+ this.isOptional = isOptional; // boolean — Type? suffix
32
+ this.isRepeated = isRepeated; // boolean — [Type] array flag
33
+ this.loc = loc;
34
+ }
35
+ }
@@ -0,0 +1,140 @@
1
+ // CLI-specific parser methods for the Tova language
2
+ // Extracted from parser.js for lazy loading — only loaded when cli { } blocks are encountered.
3
+
4
+ import { TokenType } from '../lexer/tokens.js';
5
+ import * as AST from './ast.js';
6
+ import { CliConfigField, CliCommandDeclaration, CliParam } from './cli-ast.js';
7
+
8
+ // Valid config keys inside cli blocks
9
+ const CLI_CONFIG_KEYS = new Set(['name', 'version', 'description']);
10
+
11
+ export function installCliParser(ParserClass) {
12
+ if (ParserClass.prototype._cliParserInstalled) return;
13
+ ParserClass.prototype._cliParserInstalled = true;
14
+
15
+ ParserClass.prototype.parseCliBlock = function() {
16
+ const l = this.loc();
17
+ this.advance(); // consume 'cli'
18
+ this.expect(TokenType.LBRACE, "Expected '{' after 'cli'");
19
+ const config = [];
20
+ const commands = [];
21
+
22
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
23
+ try {
24
+ // fn or async fn → command
25
+ if (this.check(TokenType.FN) ||
26
+ (this.check(TokenType.ASYNC) && this.peek(1).type === TokenType.FN)) {
27
+ commands.push(this.parseCliCommand());
28
+ }
29
+ // identifier followed by colon → config field
30
+ else if (this.check(TokenType.IDENTIFIER) && this.peek(1).type === TokenType.COLON) {
31
+ config.push(this.parseCliConfigField());
32
+ }
33
+ else {
34
+ this.error("Expected config field (name: ...) or command (fn ...) inside cli block");
35
+ }
36
+ } catch (e) {
37
+ this.errors.push(e);
38
+ this._synchronizeBlock();
39
+ }
40
+ }
41
+
42
+ this.expect(TokenType.RBRACE, "Expected '}' to close cli block");
43
+ return new AST.CliBlock(config, commands, l);
44
+ };
45
+
46
+ ParserClass.prototype.parseCliConfigField = function() {
47
+ const l = this.loc();
48
+ const key = this.advance().value; // consume identifier
49
+ this.expect(TokenType.COLON, "Expected ':' after config key");
50
+ const value = this.parseExpression();
51
+ return new CliConfigField(key, value, l);
52
+ };
53
+
54
+ ParserClass.prototype.parseCliCommand = function() {
55
+ const l = this.loc();
56
+ let isAsync = false;
57
+ if (this.check(TokenType.ASYNC)) {
58
+ isAsync = true;
59
+ this.advance(); // consume 'async'
60
+ }
61
+ this.expect(TokenType.FN, "Expected 'fn' for cli command");
62
+ const name = this.expect(TokenType.IDENTIFIER, "Expected command name").value;
63
+ this.expect(TokenType.LPAREN, "Expected '(' after command name");
64
+ const params = this.parseCliParameterList();
65
+ this.expect(TokenType.RPAREN, "Expected ')' after parameters");
66
+
67
+ // Parse body
68
+ const body = this.parseBlock();
69
+
70
+ return new CliCommandDeclaration(name, params, body, isAsync, l);
71
+ };
72
+
73
+ ParserClass.prototype.parseCliParameterList = function() {
74
+ const params = [];
75
+ while (!this.check(TokenType.RPAREN) && !this.isAtEnd()) {
76
+ if (params.length > 0) {
77
+ this.expect(TokenType.COMMA, "Expected ',' between parameters");
78
+ }
79
+ params.push(this.parseCliParam());
80
+ }
81
+ return params;
82
+ };
83
+
84
+ ParserClass.prototype.parseCliParam = function() {
85
+ const l = this.loc();
86
+ let isFlag = false;
87
+
88
+ // Check for -- prefix (two MINUS tokens)
89
+ if (this.check(TokenType.MINUS) && this.peek(1).type === TokenType.MINUS) {
90
+ isFlag = true;
91
+ this.advance(); // consume first -
92
+ this.advance(); // consume second -
93
+ }
94
+
95
+ const name = this.expect(TokenType.IDENTIFIER, "Expected parameter name").value;
96
+
97
+ // Parse optional type annotation: param: Type, param: Type?, param: [Type]
98
+ let typeAnnotation = null;
99
+ let isOptional = false;
100
+ let isRepeated = false;
101
+
102
+ if (this.match(TokenType.COLON)) {
103
+ // [Type] → repeated
104
+ if (this.check(TokenType.LBRACKET)) {
105
+ isRepeated = true;
106
+ this.advance(); // consume [
107
+ typeAnnotation = this._parseCliTypeName();
108
+ this.expect(TokenType.RBRACKET, "Expected ']' after array type");
109
+ } else {
110
+ typeAnnotation = this._parseCliTypeName();
111
+ // Type? → optional
112
+ if (this.check(TokenType.QUESTION)) {
113
+ isOptional = true;
114
+ this.advance(); // consume ?
115
+ }
116
+ }
117
+ }
118
+
119
+ // Bool flags are implicitly optional (default false)
120
+ if (isFlag && typeAnnotation === 'Bool' && !isOptional) {
121
+ isOptional = true;
122
+ }
123
+
124
+ // Parse default value: = expr
125
+ let defaultValue = null;
126
+ if (this.match(TokenType.ASSIGN)) {
127
+ defaultValue = this.parseExpression();
128
+ }
129
+
130
+ return new CliParam(name, typeAnnotation, defaultValue, isFlag, isOptional, isRepeated, l);
131
+ };
132
+
133
+ // Helper to parse a simple type name (String, Int, Float, Bool, or IDENTIFIER)
134
+ ParserClass.prototype._parseCliTypeName = function() {
135
+ if (this.check(TokenType.IDENTIFIER) || this.check(TokenType.TYPE)) {
136
+ return this.advance().value;
137
+ }
138
+ this.error("Expected type name");
139
+ };
140
+ }