tova 0.3.2 → 0.3.4
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 +79 -29
- package/package.json +7 -2
- package/src/analyzer/analyzer.js +5 -0
- package/src/analyzer/client-analyzer.js +14 -0
- package/src/codegen/base-codegen.js +13 -0
- package/src/codegen/client-codegen.js +44 -2
- package/src/codegen/codegen.js +2 -2
- package/src/codegen/server-codegen.js +43 -18
- package/src/codegen/shared-codegen.js +1 -1
- package/src/lexer/lexer.js +24 -7
- package/src/parser/ast.js +1 -1
- package/src/parser/client-ast.js +9 -0
- package/src/parser/client-parser.js +80 -7
- package/src/parser/parser.js +35 -5
- package/src/version.js +1 -1
package/bin/tova.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createHash } from 'crypto';
|
|
|
7
7
|
import { Lexer } from '../src/lexer/lexer.js';
|
|
8
8
|
import { Parser } from '../src/parser/parser.js';
|
|
9
9
|
import { Analyzer } from '../src/analyzer/analyzer.js';
|
|
10
|
+
import { Symbol } from '../src/analyzer/scope.js';
|
|
10
11
|
import { Program } from '../src/parser/ast.js';
|
|
11
12
|
import { CodeGenerator } from '../src/codegen/codegen.js';
|
|
12
13
|
import { richError, formatDiagnostics, DiagnosticFormatter, formatSummary } from '../src/diagnostics/formatter.js';
|
|
@@ -209,9 +210,15 @@ function compileTova(source, filename, options = {}) {
|
|
|
209
210
|
const ast = parser.parse();
|
|
210
211
|
|
|
211
212
|
const analyzer = new Analyzer(ast, filename, { strict: options.strict || false });
|
|
213
|
+
// Pre-define extra names in the analyzer scope (used by REPL for cross-line persistence)
|
|
214
|
+
if (options.knownNames) {
|
|
215
|
+
for (const name of options.knownNames) {
|
|
216
|
+
analyzer.globalScope.define(name, new Symbol(name, 'variable', null, true, { line: 0, column: 0, file: '<repl>' }));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
212
219
|
const { warnings } = analyzer.analyze();
|
|
213
220
|
|
|
214
|
-
if (warnings.length > 0) {
|
|
221
|
+
if (warnings.length > 0 && !options.suppressWarnings) {
|
|
215
222
|
const formatter = new DiagnosticFormatter(source, filename);
|
|
216
223
|
for (const w of warnings) {
|
|
217
224
|
console.warn(formatter.formatWarning(w.message, { line: w.line, column: w.column }, { hint: w.hint, code: w.code, length: w.length, fix: w.fix }));
|
|
@@ -338,10 +345,8 @@ async function runTests(args) {
|
|
|
338
345
|
const outDir = dirname(outPath);
|
|
339
346
|
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
340
347
|
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
const fullTest = result.test;
|
|
344
|
-
writeFileSync(outPath, fullTest);
|
|
348
|
+
// Shared code (top-level definitions) is now included by generateTests()
|
|
349
|
+
writeFileSync(outPath, result.test);
|
|
345
350
|
compiledFiles.push(outPath);
|
|
346
351
|
console.log(` Compiled: ${relative('.', file)}`);
|
|
347
352
|
}
|
|
@@ -550,8 +555,22 @@ async function runFile(filePath, options = {}) {
|
|
|
550
555
|
const source = readFileSync(resolved, 'utf-8');
|
|
551
556
|
|
|
552
557
|
try {
|
|
553
|
-
//
|
|
554
|
-
const
|
|
558
|
+
// Detect local .tova imports (with or without .tova extension)
|
|
559
|
+
const importDetectRegex = /import\s+(?:\{[^}]*\}|[\w$]+|\*\s+as\s+[\w$]+)\s+from\s+['"]([^'"]+)['"]/gm;
|
|
560
|
+
let importMatch;
|
|
561
|
+
const tovaImportPaths = [];
|
|
562
|
+
while ((importMatch = importDetectRegex.exec(source)) !== null) {
|
|
563
|
+
const importSource = importMatch[1];
|
|
564
|
+
if (!importSource.startsWith('.') && !importSource.startsWith('/')) continue;
|
|
565
|
+
let depPath = resolve(dirname(resolved), importSource);
|
|
566
|
+
if (!depPath.endsWith('.tova') && existsSync(depPath + '.tova')) {
|
|
567
|
+
depPath = depPath + '.tova';
|
|
568
|
+
}
|
|
569
|
+
if (depPath.endsWith('.tova') && existsSync(depPath)) {
|
|
570
|
+
tovaImportPaths.push({ source: importSource, resolved: depPath });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const hasTovaImports = tovaImportPaths.length > 0;
|
|
555
574
|
|
|
556
575
|
const output = compileTova(source, filePath, { strict: options.strict });
|
|
557
576
|
|
|
@@ -562,29 +581,29 @@ async function runFile(filePath, options = {}) {
|
|
|
562
581
|
// Compile .tova dependencies and inline them
|
|
563
582
|
let depCode = '';
|
|
564
583
|
if (hasTovaImports) {
|
|
565
|
-
const importRegex = /import\s+(?:\{[^}]*\}|[\w$]+|\*\s+as\s+[\w$]+)\s+from\s+['"]([^'"]*\.tova)['"]/gm;
|
|
566
|
-
let match;
|
|
567
584
|
const compiled = new Set();
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
// Strip export keywords for inlining
|
|
577
|
-
depShared = depShared.replace(/^export /gm, '');
|
|
578
|
-
depCode += depShared + '\n';
|
|
579
|
-
}
|
|
585
|
+
for (const imp of tovaImportPaths) {
|
|
586
|
+
if (compiled.has(imp.resolved)) continue;
|
|
587
|
+
compiled.add(imp.resolved);
|
|
588
|
+
const depSource = readFileSync(imp.resolved, 'utf-8');
|
|
589
|
+
const dep = compileTova(depSource, imp.resolved, { strict: options.strict });
|
|
590
|
+
let depShared = dep.shared || '';
|
|
591
|
+
depShared = depShared.replace(/^export /gm, '');
|
|
592
|
+
depCode += depShared + '\n';
|
|
580
593
|
}
|
|
581
594
|
}
|
|
582
595
|
|
|
583
596
|
let code = stdlib + '\n' + depCode + (output.shared || '') + '\n' + (output.server || output.client || '');
|
|
584
597
|
// Strip 'export ' keywords — not valid inside AsyncFunction (used in tova build only)
|
|
585
598
|
code = code.replace(/^export /gm, '');
|
|
586
|
-
// Strip import lines for
|
|
599
|
+
// Strip import lines for local modules (already inlined above)
|
|
587
600
|
code = code.replace(/^import\s+(?:\{[^}]*\}|[\w$]+|\*\s+as\s+[\w$]+)\s+from\s+['"][^'"]*\.(?:tova|(?:shared\.)?js)['"];?\s*$/gm, '');
|
|
601
|
+
if (hasTovaImports) {
|
|
602
|
+
for (const imp of tovaImportPaths) {
|
|
603
|
+
const escaped = imp.source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
604
|
+
code = code.replace(new RegExp('^import\\s+(?:\\{[^}]*\\}|[\\w$]+|\\*\\s+as\\s+[\\w$]+)\\s+from\\s+[\'"]' + escaped + '[\'"];?\\s*$', 'gm'), '');
|
|
605
|
+
}
|
|
606
|
+
}
|
|
588
607
|
// Auto-call main() if the compiled code defines a main function
|
|
589
608
|
const scriptArgs = options.scriptArgs || [];
|
|
590
609
|
if (/\bfunction\s+main\s*\(/.test(code)) {
|
|
@@ -689,8 +708,10 @@ async function buildProject(args) {
|
|
|
689
708
|
if (!result) continue;
|
|
690
709
|
|
|
691
710
|
const { output, single } = result;
|
|
692
|
-
//
|
|
693
|
-
const outBaseName = single
|
|
711
|
+
// Preserve relative directory structure in output (e.g., src/lib/math.tova → lib/math.js)
|
|
712
|
+
const outBaseName = single
|
|
713
|
+
? relative(srcDir, files[0]).replace(/\.tova$/, '').replace(/\\/g, '/')
|
|
714
|
+
: (relDir === '.' ? dirName : relDir + '/' + dirName);
|
|
694
715
|
const relLabel = single ? relative(srcDir, files[0]) : `${relDir}/ (${files.length} files merged)`;
|
|
695
716
|
const elapsed = Date.now() - groupStart;
|
|
696
717
|
const timing = isVerbose ? ` (${elapsed}ms)` : '';
|
|
@@ -711,6 +732,10 @@ async function buildProject(args) {
|
|
|
711
732
|
return code;
|
|
712
733
|
};
|
|
713
734
|
|
|
735
|
+
// Ensure output subdirectory exists for nested paths (e.g., lib/math.js)
|
|
736
|
+
const outSubDir = dirname(join(outDir, outBaseName));
|
|
737
|
+
if (outSubDir !== outDir) mkdirSync(outSubDir, { recursive: true });
|
|
738
|
+
|
|
714
739
|
// Module files: write single <name>.js (not .shared.js)
|
|
715
740
|
if (output.isModule) {
|
|
716
741
|
if (output.shared && output.shared.trim()) {
|
|
@@ -856,9 +881,16 @@ async function checkProject(args) {
|
|
|
856
881
|
}
|
|
857
882
|
|
|
858
883
|
const explicitSrc = args.filter(a => !a.startsWith('--'))[0];
|
|
859
|
-
const
|
|
884
|
+
const srcPath = resolve(explicitSrc || '.');
|
|
860
885
|
|
|
861
|
-
|
|
886
|
+
// Support both single file and directory arguments
|
|
887
|
+
let tovaFiles;
|
|
888
|
+
if (existsSync(srcPath) && statSync(srcPath).isFile()) {
|
|
889
|
+
tovaFiles = srcPath.endsWith('.tova') ? [srcPath] : [];
|
|
890
|
+
} else {
|
|
891
|
+
tovaFiles = findFiles(srcPath, '.tova');
|
|
892
|
+
}
|
|
893
|
+
const srcDir = existsSync(srcPath) && statSync(srcPath).isFile() ? dirname(srcPath) : srcPath;
|
|
862
894
|
if (tovaFiles.length === 0) {
|
|
863
895
|
console.error('No .tova files found');
|
|
864
896
|
process.exit(1);
|
|
@@ -2495,7 +2527,7 @@ async function startRepl() {
|
|
|
2495
2527
|
buffer = '';
|
|
2496
2528
|
|
|
2497
2529
|
try {
|
|
2498
|
-
const output = compileTova(input, '<repl>');
|
|
2530
|
+
const output = compileTova(input, '<repl>', { suppressWarnings: true });
|
|
2499
2531
|
const code = output.shared || '';
|
|
2500
2532
|
if (code.trim()) {
|
|
2501
2533
|
// Extract function/const/let names from compiled code
|
|
@@ -2533,8 +2565,16 @@ async function startRepl() {
|
|
|
2533
2565
|
const lines = code.trim().split('\n');
|
|
2534
2566
|
const lastLine = lines[lines.length - 1].trim();
|
|
2535
2567
|
let evalCode = code;
|
|
2536
|
-
//
|
|
2537
|
-
|
|
2568
|
+
// For simple assignments (const x = expr;), echo the assigned value
|
|
2569
|
+
const constAssignMatch = lastLine.match(/^(const|let)\s+([a-zA-Z_]\w*)\s*=\s*(.+);?$/);
|
|
2570
|
+
if (constAssignMatch) {
|
|
2571
|
+
const varName = constAssignMatch[2];
|
|
2572
|
+
if (allSave) {
|
|
2573
|
+
evalCode = `${code}\n${allSave}\nreturn ${varName};`;
|
|
2574
|
+
} else {
|
|
2575
|
+
evalCode = `${code}\nreturn ${varName};`;
|
|
2576
|
+
}
|
|
2577
|
+
} else if (!/^(const |let |var |function |if |for |while |class |try |switch )/.test(lastLine) && !lastLine.endsWith('{')) {
|
|
2538
2578
|
// Replace the last statement with a return
|
|
2539
2579
|
const allButLast = lines.slice(0, -1).join('\n');
|
|
2540
2580
|
// Strip trailing semicolon from last line for the return
|
|
@@ -2745,6 +2785,16 @@ async function productionBuild(srcDir, outDir) {
|
|
|
2745
2785
|
console.log(` server.${hash}.js`);
|
|
2746
2786
|
}
|
|
2747
2787
|
|
|
2788
|
+
// Write script bundle for plain scripts (no server/client blocks)
|
|
2789
|
+
if (!allServerCode.trim() && !allClientCode.trim() && allSharedCode.trim()) {
|
|
2790
|
+
const stdlib = getRunStdlib();
|
|
2791
|
+
const scriptBundle = stdlib + '\n' + allSharedCode;
|
|
2792
|
+
const hash = hashCode(scriptBundle);
|
|
2793
|
+
const scriptPath = join(outDir, `script.${hash}.js`);
|
|
2794
|
+
writeFileSync(scriptPath, scriptBundle);
|
|
2795
|
+
console.log(` script.${hash}.js`);
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2748
2798
|
// Write client bundle
|
|
2749
2799
|
if (allClientCode.trim()) {
|
|
2750
2800
|
const fullClientModule = allSharedCode + '\n' + allClientCode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tova",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
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",
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
"url": "https://github.com/tova-lang/tova-lang/issues"
|
|
37
37
|
},
|
|
38
38
|
"author": "Enoch Kujem Abassey",
|
|
39
|
-
"keywords": [
|
|
39
|
+
"keywords": [
|
|
40
|
+
"language",
|
|
41
|
+
"transpiler",
|
|
42
|
+
"fullstack",
|
|
43
|
+
"javascript"
|
|
44
|
+
],
|
|
40
45
|
"license": "MIT"
|
|
41
46
|
}
|
package/src/analyzer/analyzer.js
CHANGED
|
@@ -953,6 +953,11 @@ export class Analyzer {
|
|
|
953
953
|
const target = node.targets[i];
|
|
954
954
|
const existing = this._lookupAssignTarget(target);
|
|
955
955
|
if (existing) {
|
|
956
|
+
// Allow user code to shadow builtins (e.g., url = "/api")
|
|
957
|
+
if (existing.kind === 'builtin') {
|
|
958
|
+
this.currentScope.define(target, new Symbol(target, 'variable', null, false, node.loc));
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
956
961
|
if (!existing.mutable) {
|
|
957
962
|
this.error(`Cannot reassign immutable variable '${target}'. Use 'var' for mutable variables.`, node.loc, null, {
|
|
958
963
|
code: 'E202',
|
|
@@ -130,6 +130,8 @@ export function installClientAnalyzer(AnalyzerClass) {
|
|
|
130
130
|
this.visitJSXFor(child);
|
|
131
131
|
} else if (child.type === 'JSXIf') {
|
|
132
132
|
this.visitJSXIf(child);
|
|
133
|
+
} else if (child.type === 'JSXMatch') {
|
|
134
|
+
this.visitJSXMatch(child);
|
|
133
135
|
}
|
|
134
136
|
}
|
|
135
137
|
};
|
|
@@ -146,6 +148,8 @@ export function installClientAnalyzer(AnalyzerClass) {
|
|
|
146
148
|
this.visitJSXFor(child);
|
|
147
149
|
} else if (child.type === 'JSXIf') {
|
|
148
150
|
this.visitJSXIf(child);
|
|
151
|
+
} else if (child.type === 'JSXMatch') {
|
|
152
|
+
this.visitJSXMatch(child);
|
|
149
153
|
}
|
|
150
154
|
}
|
|
151
155
|
};
|
|
@@ -188,4 +192,14 @@ export function installClientAnalyzer(AnalyzerClass) {
|
|
|
188
192
|
}
|
|
189
193
|
}
|
|
190
194
|
};
|
|
195
|
+
|
|
196
|
+
AnalyzerClass.prototype.visitJSXMatch = function(node) {
|
|
197
|
+
this.visitExpression(node.subject);
|
|
198
|
+
for (const arm of node.arms) {
|
|
199
|
+
// Visit pattern bindings in a child scope
|
|
200
|
+
for (const child of arm.body) {
|
|
201
|
+
this.visitNode(child);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
191
205
|
}
|
|
@@ -10,6 +10,7 @@ export class BaseCodegen {
|
|
|
10
10
|
this._needsContainsHelper = false; // track if __contains helper is needed
|
|
11
11
|
this._needsPropagateHelper = false; // track if __propagate helper is needed
|
|
12
12
|
this._usedBuiltins = new Set(); // track which stdlib builtins are actually used
|
|
13
|
+
this._userDefinedNames = new Set(); // track user-defined top-level names (to avoid stdlib conflicts)
|
|
13
14
|
this._needsResultOption = false; // track if Ok/Err/Some/None are used
|
|
14
15
|
this._variantFields = { 'Ok': ['value'], 'Err': ['error'], 'Some': ['value'] }; // map variant name -> [field names] for pattern destructuring
|
|
15
16
|
this._traitDecls = new Map(); // traitName -> { methods: [...] }
|
|
@@ -113,6 +114,14 @@ export class BaseCodegen {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
getUsedBuiltins() {
|
|
117
|
+
// Exclude builtins that the user has redefined at top level
|
|
118
|
+
if (this._userDefinedNames.size > 0) {
|
|
119
|
+
const filtered = new Set(this._usedBuiltins);
|
|
120
|
+
for (const name of this._userDefinedNames) {
|
|
121
|
+
filtered.delete(name);
|
|
122
|
+
}
|
|
123
|
+
return filtered;
|
|
124
|
+
}
|
|
116
125
|
return this._usedBuiltins;
|
|
117
126
|
}
|
|
118
127
|
|
|
@@ -300,6 +309,10 @@ export class BaseCodegen {
|
|
|
300
309
|
return `${this.i()}${target} = ${this.genExpression(node.values[0])};`;
|
|
301
310
|
}
|
|
302
311
|
this.declareVar(target);
|
|
312
|
+
// Track top-level user definitions to avoid stdlib conflicts
|
|
313
|
+
if (this._scopes.length === 1 && BUILTIN_NAMES.has(target)) {
|
|
314
|
+
this._userDefinedNames.add(target);
|
|
315
|
+
}
|
|
303
316
|
return `${this.i()}${exportPrefix}const ${target} = ${this.genExpression(node.values[0])};`;
|
|
304
317
|
}
|
|
305
318
|
|
|
@@ -595,7 +595,10 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
595
595
|
if (node.object.type === 'Identifier' && this.storeNames.has(node.object.name)) {
|
|
596
596
|
return true; // Store property access is reactive (getters call signals)
|
|
597
597
|
}
|
|
598
|
-
return this._exprReadsSignal(node.object);
|
|
598
|
+
return this._exprReadsSignal(node.object) || (node.computed && this._exprReadsSignal(node.property));
|
|
599
|
+
}
|
|
600
|
+
if (node.type === 'OptionalChain') {
|
|
601
|
+
return this._exprReadsSignal(node.object) || (node.computed && this._exprReadsSignal(node.property));
|
|
599
602
|
}
|
|
600
603
|
if (node.type === 'TemplateLiteral') {
|
|
601
604
|
return node.parts.some(p => p.type === 'expr' && this._exprReadsSignal(p.value));
|
|
@@ -640,6 +643,7 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
640
643
|
}
|
|
641
644
|
case 'JSXFor': return this.genJSXFor(node);
|
|
642
645
|
case 'JSXIf': return this.genJSXIf(node);
|
|
646
|
+
case 'JSXMatch': return this.genJSXMatch(node);
|
|
643
647
|
default: return this.genExpression(node);
|
|
644
648
|
}
|
|
645
649
|
}
|
|
@@ -667,7 +671,12 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
667
671
|
if (this.stateNames.has(exprName)) {
|
|
668
672
|
// <select> fires 'change', all other inputs fire 'input'
|
|
669
673
|
const eventName = node.tag === 'select' ? 'change' : 'input';
|
|
670
|
-
|
|
674
|
+
// For number/range inputs, coerce e.target.value to Number
|
|
675
|
+
const typeAttr = node.attributes.find(a => a.name === 'type');
|
|
676
|
+
const typeStr = typeAttr && typeAttr.value ? (typeAttr.value.value || '') : '';
|
|
677
|
+
const isNumeric = typeStr === 'number' || typeStr === 'range';
|
|
678
|
+
const valueExpr = isNumeric ? 'Number(e.target.value)' : 'e.target.value';
|
|
679
|
+
events[eventName] = `(e) => { set${capitalize(exprName)}(${valueExpr}); }`;
|
|
671
680
|
}
|
|
672
681
|
} else if (attr.name === 'bind:checked') {
|
|
673
682
|
// Two-way binding: bind:checked={flag} → reactive checked + onChange
|
|
@@ -924,6 +933,39 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
924
933
|
return `() => ${result}`;
|
|
925
934
|
}
|
|
926
935
|
|
|
936
|
+
genJSXMatch(node) {
|
|
937
|
+
const subject = this.genExpression(node.subject);
|
|
938
|
+
const p = [];
|
|
939
|
+
p.push(`((__match) => { `);
|
|
940
|
+
|
|
941
|
+
for (let idx = 0; idx < node.arms.length; idx++) {
|
|
942
|
+
const arm = node.arms[idx];
|
|
943
|
+
const condition = this.genPatternCondition(arm.pattern, '__match', arm.guard);
|
|
944
|
+
const body = arm.body.map(c => this.genJSX(c));
|
|
945
|
+
const bodyExpr = body.length === 1 ? body[0] : `tova_fragment([${body.join(', ')}])`;
|
|
946
|
+
|
|
947
|
+
if (arm.pattern.type === 'WildcardPattern' || arm.pattern.type === 'BindingPattern') {
|
|
948
|
+
if (idx === node.arms.length - 1 && !arm.guard) {
|
|
949
|
+
// Default case
|
|
950
|
+
if (arm.pattern.type === 'BindingPattern') {
|
|
951
|
+
p.push(`const ${arm.pattern.name} = __match; `);
|
|
952
|
+
}
|
|
953
|
+
p.push(`return ${bodyExpr}; `);
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const keyword = idx === 0 ? 'if' : 'else if';
|
|
959
|
+
p.push(`${keyword} (${condition}) { `);
|
|
960
|
+
p.push(this.genPatternBindings(arm.pattern, '__match'));
|
|
961
|
+
p.push(`return ${bodyExpr}; } `);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
p.push(`})(${subject})`);
|
|
965
|
+
// Wrap in reactive closure
|
|
966
|
+
return `() => ${p.join('')}`;
|
|
967
|
+
}
|
|
968
|
+
|
|
927
969
|
genJSXFragment(node) {
|
|
928
970
|
const children = node.children.map(c => this.genJSX(c)).join(', ');
|
|
929
971
|
return `tova_fragment([${children}])`;
|
package/src/codegen/codegen.js
CHANGED
|
@@ -160,7 +160,7 @@ export class CodeGenerator {
|
|
|
160
160
|
let testCode = '';
|
|
161
161
|
if (testBlocks.length > 0) {
|
|
162
162
|
const testGen = new (getServerCodegen())();
|
|
163
|
-
testCode = testGen.generateTests(testBlocks);
|
|
163
|
+
testCode = testGen.generateTests(testBlocks, combinedShared);
|
|
164
164
|
|
|
165
165
|
// Add __handleRequest export to server code
|
|
166
166
|
const defaultServer = servers['default'] || '';
|
|
@@ -173,7 +173,7 @@ export class CodeGenerator {
|
|
|
173
173
|
let benchCode = '';
|
|
174
174
|
if (benchBlocks.length > 0) {
|
|
175
175
|
const benchGen = new (getServerCodegen())();
|
|
176
|
-
benchCode = benchGen.generateBench(benchBlocks);
|
|
176
|
+
benchCode = benchGen.generateBench(benchBlocks, combinedShared);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
// Backward-compatible: if only unnamed blocks, return flat structure
|
|
@@ -2677,7 +2677,7 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
2677
2677
|
this._emitHandlerCall(lines, `__grpChain(req)`, timeoutMs);
|
|
2678
2678
|
}
|
|
2679
2679
|
|
|
2680
|
-
generateTests(testBlocks) {
|
|
2680
|
+
generateTests(testBlocks, sharedCode) {
|
|
2681
2681
|
const lines = [];
|
|
2682
2682
|
lines.push('import { describe, test, expect } from "bun:test";');
|
|
2683
2683
|
lines.push('');
|
|
@@ -2701,6 +2701,12 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
2701
2701
|
lines.push(' if (!condition) throw new Error(message || "Assertion failed");');
|
|
2702
2702
|
lines.push('}');
|
|
2703
2703
|
lines.push('');
|
|
2704
|
+
// Include top-level definitions (functions, variables) so tests can reference them
|
|
2705
|
+
if (sharedCode && sharedCode.trim()) {
|
|
2706
|
+
lines.push('// ── Module Code ──');
|
|
2707
|
+
lines.push(sharedCode);
|
|
2708
|
+
lines.push('');
|
|
2709
|
+
}
|
|
2704
2710
|
|
|
2705
2711
|
for (const block of testBlocks) {
|
|
2706
2712
|
const name = block.name || 'Tests';
|
|
@@ -2729,24 +2735,37 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
2729
2735
|
lines.push(' });');
|
|
2730
2736
|
}
|
|
2731
2737
|
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
const
|
|
2739
|
-
|
|
2738
|
+
const hasFnTests = block.body.some(s => s.type === 'FunctionDeclaration');
|
|
2739
|
+
|
|
2740
|
+
if (hasFnTests) {
|
|
2741
|
+
// Function declarations become individual test cases
|
|
2742
|
+
for (const stmt of block.body) {
|
|
2743
|
+
if (stmt.type === 'FunctionDeclaration') {
|
|
2744
|
+
const fnName = stmt.name;
|
|
2745
|
+
const displayName = fnName.replace(/_/g, ' ');
|
|
2746
|
+
this.pushScope();
|
|
2747
|
+
for (const p of (stmt.params || [])) {
|
|
2748
|
+
const pName = typeof p === 'string' ? p : (p.name || p.identifier);
|
|
2749
|
+
if (pName) this.declareVar(pName);
|
|
2750
|
+
}
|
|
2751
|
+
const body = this.genBlockBody(stmt.body);
|
|
2752
|
+
this.popScope();
|
|
2753
|
+
const timeoutArg = blockTimeout ? `, ${blockTimeout}` : '';
|
|
2754
|
+
lines.push(` test(${JSON.stringify(displayName)}, async () => {`);
|
|
2755
|
+
lines.push(body);
|
|
2756
|
+
lines.push(` }${timeoutArg});`);
|
|
2757
|
+
} else {
|
|
2758
|
+
lines.push(' ' + this.generateStatement(stmt));
|
|
2740
2759
|
}
|
|
2741
|
-
const body = this.genBlockBody(stmt.body);
|
|
2742
|
-
this.popScope();
|
|
2743
|
-
const timeoutArg = blockTimeout ? `, ${blockTimeout}` : '';
|
|
2744
|
-
lines.push(` test(${JSON.stringify(displayName)}, async () => {`);
|
|
2745
|
-
lines.push(body);
|
|
2746
|
-
lines.push(` }${timeoutArg});`);
|
|
2747
|
-
} else {
|
|
2748
|
-
lines.push(' ' + this.generateStatement(stmt));
|
|
2749
2760
|
}
|
|
2761
|
+
} else {
|
|
2762
|
+
// No function declarations — wrap all statements in a single test case
|
|
2763
|
+
const timeoutArg = blockTimeout ? `, ${blockTimeout}` : '';
|
|
2764
|
+
lines.push(` test(${JSON.stringify(name)}, async () => {`);
|
|
2765
|
+
for (const stmt of block.body) {
|
|
2766
|
+
lines.push(' ' + this.generateStatement(stmt));
|
|
2767
|
+
}
|
|
2768
|
+
lines.push(` }${timeoutArg});`);
|
|
2750
2769
|
}
|
|
2751
2770
|
lines.push('});');
|
|
2752
2771
|
lines.push('');
|
|
@@ -2755,10 +2774,16 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
2755
2774
|
return lines.join('\n');
|
|
2756
2775
|
}
|
|
2757
2776
|
|
|
2758
|
-
generateBench(benchBlocks) {
|
|
2777
|
+
generateBench(benchBlocks, sharedCode) {
|
|
2759
2778
|
const lines = [];
|
|
2760
2779
|
lines.push('// ── Tova Benchmark Runner ──');
|
|
2761
2780
|
lines.push('');
|
|
2781
|
+
// Include top-level definitions (functions, variables) so benchmarks can reference them
|
|
2782
|
+
if (sharedCode && sharedCode.trim()) {
|
|
2783
|
+
lines.push('// ── Module Code ──');
|
|
2784
|
+
lines.push(sharedCode);
|
|
2785
|
+
lines.push('');
|
|
2786
|
+
}
|
|
2762
2787
|
lines.push('async function __runBench(name, fn, runs) {');
|
|
2763
2788
|
lines.push(' runs = runs || 100;');
|
|
2764
2789
|
lines.push(' // Warmup');
|
|
@@ -22,7 +22,7 @@ export class SharedCodegen extends BaseCodegen {
|
|
|
22
22
|
helpers.push(this.getPropagateHelper());
|
|
23
23
|
}
|
|
24
24
|
// Include only used builtin functions
|
|
25
|
-
const selectiveStdlib = buildSelectiveStdlib(this.
|
|
25
|
+
const selectiveStdlib = buildSelectiveStdlib(this.getUsedBuiltins());
|
|
26
26
|
if (selectiveStdlib) helpers.push(selectiveStdlib);
|
|
27
27
|
return helpers.join('\n');
|
|
28
28
|
}
|
package/src/lexer/lexer.js
CHANGED
|
@@ -18,7 +18,8 @@ export class Lexer {
|
|
|
18
18
|
this._jsxTagMode = null; // null | 'open' | 'close' — current tag parsing state
|
|
19
19
|
this._jsxSelfClosing = false; // true when / seen in opening tag (before >)
|
|
20
20
|
this._jsxExprDepth = 0; // brace depth for {expr} inside JSX
|
|
21
|
-
this._jsxCF = null; // null | { paren: 0, brace: 0 } — control flow state
|
|
21
|
+
this._jsxCF = null; // null | { paren: 0, brace: 0, keyword? } — control flow state
|
|
22
|
+
this._matchBlockDepth = 0; // brace depth for match body inside JSX
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
error(message) {
|
|
@@ -104,8 +105,9 @@ export class Lexer {
|
|
|
104
105
|
|
|
105
106
|
scanToken() {
|
|
106
107
|
// In JSX children mode, scan raw text instead of normal tokens
|
|
108
|
+
// (but not inside matchblock — match bodies need normal scanning for patterns/arrows)
|
|
107
109
|
if (this._jsxStack.length > 0 && this._jsxExprDepth === 0 &&
|
|
108
|
-
!this._jsxTagMode && !this._jsxCF) {
|
|
110
|
+
!this._jsxTagMode && !this._jsxCF && this._matchBlockDepth === 0) {
|
|
109
111
|
return this._scanInJSXChildren();
|
|
110
112
|
}
|
|
111
113
|
|
|
@@ -226,7 +228,7 @@ export class Lexer {
|
|
|
226
228
|
while (wp < this.length && this.isAlphaNumeric(this.source[wp])) {
|
|
227
229
|
word += this.source[wp]; wp++;
|
|
228
230
|
}
|
|
229
|
-
if (['if', 'for', 'elif', 'else'].includes(word)) {
|
|
231
|
+
if (['if', 'for', 'elif', 'else', 'match'].includes(word)) {
|
|
230
232
|
while (this.pos < pp) this.advance();
|
|
231
233
|
return;
|
|
232
234
|
}
|
|
@@ -254,16 +256,16 @@ export class Lexer {
|
|
|
254
256
|
if (ch === '"') { this.scanString(); return; }
|
|
255
257
|
if (ch === "'") { this.scanSimpleString(); return; }
|
|
256
258
|
|
|
257
|
-
// Check for JSX control flow keywords: if, for, elif, else
|
|
259
|
+
// Check for JSX control flow keywords: if, for, elif, else, match
|
|
258
260
|
if (this.isAlpha(ch)) {
|
|
259
261
|
let word = '', peekPos = this.pos;
|
|
260
262
|
while (peekPos < this.length && this.isAlphaNumeric(this.source[peekPos])) {
|
|
261
263
|
word += this.source[peekPos]; peekPos++;
|
|
262
264
|
}
|
|
263
|
-
if (['if', 'for', 'elif', 'else'].includes(word)) {
|
|
265
|
+
if (['if', 'for', 'elif', 'else', 'match'].includes(word)) {
|
|
264
266
|
this.scanIdentifier();
|
|
265
267
|
// After keyword, enter control flow mode for normal scanning
|
|
266
|
-
this._jsxCF = { paren: 0, brace: 0 };
|
|
268
|
+
this._jsxCF = { paren: 0, brace: 0, keyword: word };
|
|
267
269
|
return;
|
|
268
270
|
}
|
|
269
271
|
}
|
|
@@ -284,7 +286,7 @@ export class Lexer {
|
|
|
284
286
|
while (pp < this.length && this.isAlphaNumeric(this.source[pp])) {
|
|
285
287
|
word += this.source[pp]; pp++;
|
|
286
288
|
}
|
|
287
|
-
if (['if', 'for', 'elif', 'else'].includes(word)) break;
|
|
289
|
+
if (['if', 'for', 'elif', 'else', 'match'].includes(word)) break;
|
|
288
290
|
}
|
|
289
291
|
text += this.advance();
|
|
290
292
|
}
|
|
@@ -924,6 +926,11 @@ export class Lexer {
|
|
|
924
926
|
const prev = this.tokens.length > 1 ? this.tokens[this.tokens.length - 2] : null;
|
|
925
927
|
if (prev && (prev.type === TokenType.ASSIGN || prev.type === TokenType.FOR)) {
|
|
926
928
|
this._jsxCF.brace++;
|
|
929
|
+
} else if (this._jsxCF.keyword === 'match') {
|
|
930
|
+
// Match body: scan normally (patterns, =>, etc.) — not JSX children mode
|
|
931
|
+
this._jsxCF = null;
|
|
932
|
+
this._jsxStack.push('matchblock');
|
|
933
|
+
this._matchBlockDepth = 1;
|
|
927
934
|
} else {
|
|
928
935
|
// This is the block opener for the control flow body
|
|
929
936
|
this._jsxCF = null;
|
|
@@ -932,12 +939,22 @@ export class Lexer {
|
|
|
932
939
|
}
|
|
933
940
|
} else if (this._jsxExprDepth > 0) {
|
|
934
941
|
this._jsxExprDepth++;
|
|
942
|
+
} else if (this._matchBlockDepth > 0) {
|
|
943
|
+
this._matchBlockDepth++;
|
|
935
944
|
}
|
|
936
945
|
break;
|
|
937
946
|
case '}':
|
|
938
947
|
this.tokens.push(new Token(TokenType.RBRACE, '}', startLine, startCol));
|
|
939
948
|
if (this._jsxCF && this._jsxCF.brace > 0) {
|
|
940
949
|
this._jsxCF.brace--;
|
|
950
|
+
} else if (this._matchBlockDepth > 0) {
|
|
951
|
+
this._matchBlockDepth--;
|
|
952
|
+
if (this._matchBlockDepth === 0) {
|
|
953
|
+
// Match body closed — pop matchblock from JSX stack
|
|
954
|
+
if (this._jsxStack.length > 0 && this._jsxStack[this._jsxStack.length - 1] === 'matchblock') {
|
|
955
|
+
this._jsxStack.pop();
|
|
956
|
+
}
|
|
957
|
+
}
|
|
941
958
|
} else if (this._jsxExprDepth > 0) {
|
|
942
959
|
this._jsxExprDepth--;
|
|
943
960
|
}
|
package/src/parser/ast.js
CHANGED
|
@@ -663,7 +663,7 @@ export {
|
|
|
663
663
|
StateDeclaration, ComputedDeclaration, EffectDeclaration,
|
|
664
664
|
ComponentDeclaration, ComponentStyleBlock, StoreDeclaration,
|
|
665
665
|
JSXElement, JSXAttribute, JSXSpreadAttribute, JSXFragment,
|
|
666
|
-
JSXText, JSXExpression, JSXFor, JSXIf,
|
|
666
|
+
JSXText, JSXExpression, JSXFor, JSXIf, JSXMatch,
|
|
667
667
|
} from './client-ast.js';
|
|
668
668
|
|
|
669
669
|
// ============================================================
|
package/src/parser/client-ast.js
CHANGED
|
@@ -136,3 +136,12 @@ export class JSXIf {
|
|
|
136
136
|
this.loc = loc;
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
+
|
|
140
|
+
export class JSXMatch {
|
|
141
|
+
constructor(subject, arms, loc) {
|
|
142
|
+
this.type = 'JSXMatch';
|
|
143
|
+
this.subject = subject; // expression being matched
|
|
144
|
+
this.arms = arms; // Array of { pattern, guard, body: [JSXChild] }
|
|
145
|
+
this.loc = loc;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Client-specific parser methods for the Tova language
|
|
2
2
|
// Extracted from parser.js for lazy loading — only loaded when client { } blocks are encountered.
|
|
3
3
|
|
|
4
|
-
import { TokenType } from '../lexer/tokens.js';
|
|
4
|
+
import { TokenType, Keywords } from '../lexer/tokens.js';
|
|
5
5
|
import * as AST from './ast.js';
|
|
6
6
|
|
|
7
7
|
export function installClientParser(ParserClass) {
|
|
@@ -52,7 +52,7 @@ export function installClientParser(ParserClass) {
|
|
|
52
52
|
body.push(this.parseState());
|
|
53
53
|
} else if (this.check(TokenType.COMPUTED)) {
|
|
54
54
|
body.push(this.parseComputed());
|
|
55
|
-
} else if (this.check(TokenType.FN) && this.peek(1).type === TokenType.IDENTIFIER) {
|
|
55
|
+
} else if (this.check(TokenType.FN) && (this.peek(1).type === TokenType.IDENTIFIER || this._isContextualKeywordToken(this.peek(1)))) {
|
|
56
56
|
body.push(this.parseFunctionDeclaration());
|
|
57
57
|
} else {
|
|
58
58
|
this.error("Expected 'state', 'computed', or 'fn' inside store block");
|
|
@@ -226,6 +226,12 @@ export function installClientParser(ParserClass) {
|
|
|
226
226
|
continue;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
// match inside JSX
|
|
230
|
+
if (this.check(TokenType.MATCH)) {
|
|
231
|
+
children.push(this.parseJSXMatch());
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
229
235
|
break;
|
|
230
236
|
}
|
|
231
237
|
|
|
@@ -270,11 +276,9 @@ export function installClientParser(ParserClass) {
|
|
|
270
276
|
|
|
271
277
|
ParserClass.prototype.parseJSXAttribute = function() {
|
|
272
278
|
const l = this.loc();
|
|
273
|
-
// Accept keywords as attribute names (type, class, for, etc. are valid HTML attributes)
|
|
279
|
+
// Accept keywords as attribute names (type, class, for, async, defer, etc. are valid HTML attributes)
|
|
274
280
|
let name;
|
|
275
|
-
if (this.check(TokenType.IDENTIFIER) || this.
|
|
276
|
-
this.check(TokenType.IN) || this.check(TokenType.AS) || this.check(TokenType.EXPORT) ||
|
|
277
|
-
this.check(TokenType.STATE) || this.check(TokenType.COMPUTED) || this.check(TokenType.ROUTE)) {
|
|
281
|
+
if (this.check(TokenType.IDENTIFIER) || (this.peek().value in Keywords)) {
|
|
278
282
|
name = this.advance().value;
|
|
279
283
|
} else {
|
|
280
284
|
this.error("Expected attribute name");
|
|
@@ -283,7 +287,7 @@ export function installClientParser(ParserClass) {
|
|
|
283
287
|
// Handle namespaced attributes: on:click, bind:value, class:active
|
|
284
288
|
if (this.match(TokenType.COLON)) {
|
|
285
289
|
let suffix;
|
|
286
|
-
if (this.check(TokenType.IDENTIFIER) || this.
|
|
290
|
+
if (this.check(TokenType.IDENTIFIER) || (this.peek().value in Keywords)) {
|
|
287
291
|
suffix = this.advance().value;
|
|
288
292
|
} else {
|
|
289
293
|
suffix = this.expect(TokenType.IDENTIFIER, "Expected name after ':'").value;
|
|
@@ -371,6 +375,12 @@ export function installClientParser(ParserClass) {
|
|
|
371
375
|
continue;
|
|
372
376
|
}
|
|
373
377
|
|
|
378
|
+
// match inside JSX
|
|
379
|
+
if (this.check(TokenType.MATCH)) {
|
|
380
|
+
children.push(this.parseJSXMatch());
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
374
384
|
break;
|
|
375
385
|
}
|
|
376
386
|
|
|
@@ -438,6 +448,12 @@ export function installClientParser(ParserClass) {
|
|
|
438
448
|
this.advance();
|
|
439
449
|
body.push(new AST.JSXExpression(this.parseExpression(), this.loc()));
|
|
440
450
|
this.expect(TokenType.RBRACE);
|
|
451
|
+
} else if (this.check(TokenType.FOR)) {
|
|
452
|
+
body.push(this.parseJSXFor());
|
|
453
|
+
} else if (this.check(TokenType.IF)) {
|
|
454
|
+
body.push(this.parseJSXIf());
|
|
455
|
+
} else if (this.check(TokenType.MATCH)) {
|
|
456
|
+
body.push(this.parseJSXMatch());
|
|
441
457
|
} else {
|
|
442
458
|
break;
|
|
443
459
|
}
|
|
@@ -464,6 +480,12 @@ export function installClientParser(ParserClass) {
|
|
|
464
480
|
this.advance();
|
|
465
481
|
body.push(new AST.JSXExpression(this.parseExpression(), this.loc()));
|
|
466
482
|
this.expect(TokenType.RBRACE);
|
|
483
|
+
} else if (this.check(TokenType.FOR)) {
|
|
484
|
+
body.push(this.parseJSXFor());
|
|
485
|
+
} else if (this.check(TokenType.IF)) {
|
|
486
|
+
body.push(this.parseJSXIf());
|
|
487
|
+
} else if (this.check(TokenType.MATCH)) {
|
|
488
|
+
body.push(this.parseJSXMatch());
|
|
467
489
|
} else {
|
|
468
490
|
break;
|
|
469
491
|
}
|
|
@@ -501,4 +523,55 @@ export function installClientParser(ParserClass) {
|
|
|
501
523
|
|
|
502
524
|
return new AST.JSXIf(condition, consequent, alternate, l, alternates);
|
|
503
525
|
};
|
|
526
|
+
|
|
527
|
+
ParserClass.prototype.parseJSXMatch = function() {
|
|
528
|
+
const l = this.loc();
|
|
529
|
+
this.expect(TokenType.MATCH);
|
|
530
|
+
const subject = this.parseExpression();
|
|
531
|
+
this.expect(TokenType.LBRACE, "Expected '{' to open JSX match body");
|
|
532
|
+
|
|
533
|
+
const arms = [];
|
|
534
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
535
|
+
const al = this.loc();
|
|
536
|
+
const pattern = this.parsePattern();
|
|
537
|
+
|
|
538
|
+
let guard = null;
|
|
539
|
+
if (this.match(TokenType.IF)) {
|
|
540
|
+
guard = this.parseExpression();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
this.expect(TokenType.ARROW, "Expected '=>' in JSX match arm");
|
|
544
|
+
|
|
545
|
+
// Parse arm body as JSX children
|
|
546
|
+
const body = [];
|
|
547
|
+
if (this.check(TokenType.LESS)) {
|
|
548
|
+
body.push(this.parseJSXElementOrFragment());
|
|
549
|
+
} else if (this.check(TokenType.STRING) || this.check(TokenType.STRING_TEMPLATE)) {
|
|
550
|
+
body.push(new AST.JSXText(this.parseStringLiteral(), this.loc()));
|
|
551
|
+
} else if (this.check(TokenType.JSX_TEXT)) {
|
|
552
|
+
const tok = this.advance();
|
|
553
|
+
const text = this._collapseJSXWhitespace(tok.value);
|
|
554
|
+
if (text.length > 0) {
|
|
555
|
+
body.push(new AST.JSXText(new AST.StringLiteral(text, this.loc()), this.loc()));
|
|
556
|
+
}
|
|
557
|
+
} else if (this.check(TokenType.LBRACE)) {
|
|
558
|
+
this.advance();
|
|
559
|
+
body.push(new AST.JSXExpression(this.parseExpression(), this.loc()));
|
|
560
|
+
this.expect(TokenType.RBRACE);
|
|
561
|
+
} else if (this.check(TokenType.FOR)) {
|
|
562
|
+
body.push(this.parseJSXFor());
|
|
563
|
+
} else if (this.check(TokenType.IF)) {
|
|
564
|
+
body.push(this.parseJSXIf());
|
|
565
|
+
} else {
|
|
566
|
+
// Fallback to regular expression (e.g., null, number literals)
|
|
567
|
+
body.push(new AST.JSXExpression(this.parseExpression(), this.loc()));
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
arms.push({ pattern, guard, body, loc: al });
|
|
571
|
+
this.match(TokenType.COMMA); // Optional comma between arms
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close JSX match body");
|
|
575
|
+
return new AST.JSXMatch(subject, arms, l);
|
|
576
|
+
};
|
|
504
577
|
}
|
package/src/parser/parser.js
CHANGED
|
@@ -136,6 +136,20 @@ export class Parser {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// Full-stack keywords (route, state, computed, effect, component, store) are contextual —
|
|
140
|
+
// they act as keywords inside server/client blocks but can be used as identifiers elsewhere.
|
|
141
|
+
_isContextualKeyword() {
|
|
142
|
+
const t = this.current().type;
|
|
143
|
+
return t === TokenType.ROUTE || t === TokenType.STATE || t === TokenType.COMPUTED ||
|
|
144
|
+
t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
_isContextualKeywordToken(token) {
|
|
148
|
+
const t = token.type;
|
|
149
|
+
return t === TokenType.ROUTE || t === TokenType.STATE || t === TokenType.COMPUTED ||
|
|
150
|
+
t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE;
|
|
151
|
+
}
|
|
152
|
+
|
|
139
153
|
_synchronizeBlock() {
|
|
140
154
|
// Don't advance if already at } — that's the block closer we need
|
|
141
155
|
if (!this.isAtEnd() && this.current().type !== TokenType.RBRACE) {
|
|
@@ -550,7 +564,7 @@ export class Parser {
|
|
|
550
564
|
return this.parseForStatement(null, true);
|
|
551
565
|
}
|
|
552
566
|
if (this.check(TokenType.ASYNC) && this.peek(1).type === TokenType.FN) return this.parseAsyncFunctionDeclaration();
|
|
553
|
-
if (this.check(TokenType.FN) && this.peek(1).type === TokenType.IDENTIFIER) return this.parseFunctionDeclaration();
|
|
567
|
+
if (this.check(TokenType.FN) && (this.peek(1).type === TokenType.IDENTIFIER || this._isContextualKeywordToken(this.peek(1)))) return this.parseFunctionDeclaration();
|
|
554
568
|
if (this.check(TokenType.TYPE)) return this.parseTypeDeclaration();
|
|
555
569
|
if (this.check(TokenType.MUT)) this.error("'mut' is not supported in Tova. Use 'var' for mutable variables");
|
|
556
570
|
if (this.check(TokenType.VAR)) return this.parseVarDeclaration();
|
|
@@ -712,7 +726,12 @@ export class Parser {
|
|
|
712
726
|
parseFunctionDeclaration() {
|
|
713
727
|
const l = this.loc();
|
|
714
728
|
this.expect(TokenType.FN);
|
|
715
|
-
|
|
729
|
+
let name;
|
|
730
|
+
if (this._isContextualKeyword()) {
|
|
731
|
+
name = this.advance().value;
|
|
732
|
+
} else {
|
|
733
|
+
name = this.expect(TokenType.IDENTIFIER, "Expected function name").value;
|
|
734
|
+
}
|
|
716
735
|
|
|
717
736
|
// Parse optional type parameters: fn name<T, U>(...)
|
|
718
737
|
let typeParams = [];
|
|
@@ -742,7 +761,12 @@ export class Parser {
|
|
|
742
761
|
const l = this.loc();
|
|
743
762
|
this.expect(TokenType.ASYNC);
|
|
744
763
|
this.expect(TokenType.FN);
|
|
745
|
-
|
|
764
|
+
let name;
|
|
765
|
+
if (this._isContextualKeyword()) {
|
|
766
|
+
name = this.advance().value;
|
|
767
|
+
} else {
|
|
768
|
+
name = this.expect(TokenType.IDENTIFIER, "Expected function name").value;
|
|
769
|
+
}
|
|
746
770
|
|
|
747
771
|
// Parse optional type parameters: async fn name<T, U>(...)
|
|
748
772
|
let typeParams = [];
|
|
@@ -877,7 +901,12 @@ export class Parser {
|
|
|
877
901
|
}
|
|
878
902
|
params.push(param);
|
|
879
903
|
} else {
|
|
880
|
-
|
|
904
|
+
let name;
|
|
905
|
+
if (this._isContextualKeyword()) {
|
|
906
|
+
name = this.advance().value;
|
|
907
|
+
} else {
|
|
908
|
+
name = this.expect(TokenType.IDENTIFIER, "Expected parameter name").value;
|
|
909
|
+
}
|
|
881
910
|
|
|
882
911
|
let typeAnnotation = null;
|
|
883
912
|
if (this.match(TokenType.COLON)) {
|
|
@@ -1947,7 +1976,8 @@ export class Parser {
|
|
|
1947
1976
|
}
|
|
1948
1977
|
|
|
1949
1978
|
// Keywords that can appear as identifiers in expression position
|
|
1950
|
-
if (this.check(TokenType.SERVER) || this.check(TokenType.CLIENT) || this.check(TokenType.SHARED) || this.check(TokenType.DERIVE)
|
|
1979
|
+
if (this.check(TokenType.SERVER) || this.check(TokenType.CLIENT) || this.check(TokenType.SHARED) || this.check(TokenType.DERIVE) ||
|
|
1980
|
+
this._isContextualKeyword()) {
|
|
1951
1981
|
const name = this.advance().value;
|
|
1952
1982
|
return new AST.Identifier(name, l);
|
|
1953
1983
|
}
|
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.3.
|
|
2
|
+
export const VERSION = "0.3.4";
|