tova 0.3.4 → 0.3.6
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 +438 -58
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +172 -32
- package/src/analyzer/client-analyzer.js +21 -5
- package/src/analyzer/scope.js +78 -3
- package/src/codegen/base-codegen.js +754 -45
- package/src/codegen/client-codegen.js +293 -36
- package/src/codegen/codegen.js +10 -15
- package/src/codegen/server-codegen.js +189 -40
- package/src/codegen/wasm-codegen.js +610 -0
- package/src/lexer/lexer.js +157 -109
- package/src/lexer/tokens.js +3 -0
- package/src/lsp/server.js +148 -12
- package/src/parser/ast.js +2 -1
- package/src/parser/client-parser.js +10 -3
- package/src/parser/parser.js +144 -150
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +307 -59
- package/src/runtime/ssr.js +101 -34
- package/src/stdlib/inline.js +333 -24
- package/src/stdlib/native-bridge.js +150 -0
- package/src/version.js +1 -1
package/bin/tova.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { resolve, basename, dirname, join, relative } from 'path';
|
|
4
4
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, copyFileSync, rmSync, watch as fsWatch } from 'fs';
|
|
5
5
|
import { spawn } from 'child_process';
|
|
6
|
-
|
|
6
|
+
// Bun.hash used instead of crypto.createHash for faster hashing
|
|
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';
|
|
@@ -12,7 +12,7 @@ import { Program } from '../src/parser/ast.js';
|
|
|
12
12
|
import { CodeGenerator } from '../src/codegen/codegen.js';
|
|
13
13
|
import { richError, formatDiagnostics, DiagnosticFormatter, formatSummary } from '../src/diagnostics/formatter.js';
|
|
14
14
|
import { getExplanation, lookupCode } from '../src/diagnostics/error-codes.js';
|
|
15
|
-
import { getFullStdlib, buildSelectiveStdlib, BUILTIN_NAMES, PROPAGATE } from '../src/stdlib/inline.js';
|
|
15
|
+
import { getFullStdlib, buildSelectiveStdlib, BUILTIN_NAMES, PROPAGATE, NATIVE_INIT } from '../src/stdlib/inline.js';
|
|
16
16
|
import { Formatter } from '../src/formatter/formatter.js';
|
|
17
17
|
import { REACTIVITY_SOURCE, RPC_SOURCE, ROUTER_SOURCE } from '../src/runtime/embedded.js';
|
|
18
18
|
import '../src/runtime/string-proto.js';
|
|
@@ -225,7 +225,7 @@ function compileTova(source, filename, options = {}) {
|
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
const codegen = new CodeGenerator(ast, filename);
|
|
228
|
+
const codegen = new CodeGenerator(ast, filename, { sourceMaps: options.sourceMaps !== false });
|
|
229
229
|
return codegen.generate();
|
|
230
230
|
}
|
|
231
231
|
|
|
@@ -396,6 +396,7 @@ async function runTests(args) {
|
|
|
396
396
|
});
|
|
397
397
|
} else {
|
|
398
398
|
// Clean up temp dir on exit (non-watch mode)
|
|
399
|
+
try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
399
400
|
process.exitCode = exitCode;
|
|
400
401
|
}
|
|
401
402
|
}
|
|
@@ -454,6 +455,9 @@ async function runBench(args) {
|
|
|
454
455
|
console.error(` Error compiling ${relative('.', file)}: ${err.message}`);
|
|
455
456
|
}
|
|
456
457
|
}
|
|
458
|
+
|
|
459
|
+
// Clean up bench temp dir
|
|
460
|
+
try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
457
461
|
}
|
|
458
462
|
|
|
459
463
|
async function generateDocs(args) {
|
|
@@ -629,11 +633,18 @@ async function buildProject(args) {
|
|
|
629
633
|
const isVerbose = args.includes('--verbose');
|
|
630
634
|
const isQuiet = args.includes('--quiet');
|
|
631
635
|
const isWatch = args.includes('--watch');
|
|
632
|
-
const
|
|
636
|
+
const binaryIdx = args.indexOf('--binary');
|
|
637
|
+
const binaryName = binaryIdx >= 0 ? args[binaryIdx + 1] : null;
|
|
638
|
+
const explicitSrc = args.filter(a => !a.startsWith('--') && a !== binaryName)[0];
|
|
633
639
|
const srcDir = resolve(explicitSrc || config.project.entry || '.');
|
|
634
640
|
const outIdx = args.indexOf('--output');
|
|
635
641
|
const outDir = resolve(outIdx >= 0 ? args[outIdx + 1] : (config.build.output || '.tova-out'));
|
|
636
642
|
|
|
643
|
+
// Binary compilation: compile to single standalone executable
|
|
644
|
+
if (binaryName) {
|
|
645
|
+
return await binaryBuild(srcDir, binaryName, outDir);
|
|
646
|
+
}
|
|
647
|
+
|
|
637
648
|
// Production build uses a separate optimized pipeline
|
|
638
649
|
if (isProduction) {
|
|
639
650
|
return await productionBuild(srcDir, outDir);
|
|
@@ -658,7 +669,7 @@ async function buildProject(args) {
|
|
|
658
669
|
|
|
659
670
|
let errorCount = 0;
|
|
660
671
|
const buildStart = Date.now();
|
|
661
|
-
compilationCache.clear();
|
|
672
|
+
compilationCache.clear(); moduleTypeCache.clear();
|
|
662
673
|
|
|
663
674
|
// Load incremental build cache
|
|
664
675
|
const noCache = args.includes('--no-cache');
|
|
@@ -842,7 +853,8 @@ async function buildProject(args) {
|
|
|
842
853
|
if (!filename || !filename.endsWith('.tova')) return;
|
|
843
854
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
844
855
|
debounceTimer = setTimeout(async () => {
|
|
845
|
-
|
|
856
|
+
const changedPath = resolve(srcDir, filename);
|
|
857
|
+
invalidateFile(changedPath);
|
|
846
858
|
if (!isQuiet) console.log(` Rebuilding (${filename} changed)...`);
|
|
847
859
|
await buildProject(args.filter(a => a !== '--watch'));
|
|
848
860
|
if (!isQuiet) console.log(' Watching for changes...\n');
|
|
@@ -1028,7 +1040,7 @@ async function devServer(args) {
|
|
|
1028
1040
|
let hasClient = false;
|
|
1029
1041
|
|
|
1030
1042
|
// Clear import caches for fresh compilation
|
|
1031
|
-
compilationCache.clear();
|
|
1043
|
+
compilationCache.clear(); moduleTypeCache.clear();
|
|
1032
1044
|
compilationInProgress.clear();
|
|
1033
1045
|
moduleExports.clear();
|
|
1034
1046
|
|
|
@@ -1181,10 +1193,8 @@ async function devServer(args) {
|
|
|
1181
1193
|
const currentFiles = findFiles(srcDir, '.tova');
|
|
1182
1194
|
const newServerFiles = [];
|
|
1183
1195
|
|
|
1184
|
-
//
|
|
1185
|
-
compilationCache.clear();
|
|
1196
|
+
// invalidateFile() was already called by startWatcher — just clear transient state
|
|
1186
1197
|
compilationInProgress.clear();
|
|
1187
|
-
moduleExports.clear();
|
|
1188
1198
|
|
|
1189
1199
|
try {
|
|
1190
1200
|
// Merge each directory group, collect client HTML
|
|
@@ -2162,8 +2172,8 @@ async function migrateStatus(args) {
|
|
|
2162
2172
|
function getStdlibForRuntime() {
|
|
2163
2173
|
return getFullStdlib(); // Full stdlib for REPL
|
|
2164
2174
|
}
|
|
2165
|
-
function getRunStdlib() { //
|
|
2166
|
-
return PROPAGATE;
|
|
2175
|
+
function getRunStdlib() { // NATIVE_INIT + PROPAGATE — codegen tree-shakes stdlib into output.shared
|
|
2176
|
+
return NATIVE_INIT + '\n' + PROPAGATE;
|
|
2167
2177
|
}
|
|
2168
2178
|
|
|
2169
2179
|
// ─── npm Interop Utilities ───────────────────────────────────
|
|
@@ -2387,7 +2397,8 @@ async function startRepl() {
|
|
|
2387
2397
|
const context = {};
|
|
2388
2398
|
const stdlib = getStdlibForRuntime();
|
|
2389
2399
|
// Use authoritative BUILTIN_NAMES + runtime names (Ok, Err, Some, None, __propagate)
|
|
2390
|
-
|
|
2400
|
+
// Filter out internal __ prefixed names that don't define a same-named variable
|
|
2401
|
+
const stdlibNames = [...BUILTIN_NAMES].filter(n => !n.startsWith('__')).concat(['Ok', 'Err', 'Some', 'None', '__propagate']);
|
|
2391
2402
|
const initFn = new Function(stdlib + '\nObject.assign(this, {' + stdlibNames.join(',') + '});');
|
|
2392
2403
|
initFn.call(context);
|
|
2393
2404
|
|
|
@@ -2419,7 +2430,7 @@ async function startRepl() {
|
|
|
2419
2430
|
if (trimmed.startsWith(':type ')) {
|
|
2420
2431
|
const expr = trimmed.slice(6).trim();
|
|
2421
2432
|
try {
|
|
2422
|
-
const output = compileTova(expr, '<repl>');
|
|
2433
|
+
const output = compileTova(expr, '<repl>', { sourceMaps: false });
|
|
2423
2434
|
const code = output.shared || '';
|
|
2424
2435
|
const ctxKeys = Object.keys(context).filter(k => k !== '__mutable');
|
|
2425
2436
|
const destructure = ctxKeys.length > 0 ? `const {${ctxKeys.join(',')}} = __ctx;\n` : '';
|
|
@@ -2527,7 +2538,7 @@ async function startRepl() {
|
|
|
2527
2538
|
buffer = '';
|
|
2528
2539
|
|
|
2529
2540
|
try {
|
|
2530
|
-
const output = compileTova(input, '<repl>', { suppressWarnings: true });
|
|
2541
|
+
const output = compileTova(input, '<repl>', { suppressWarnings: true, sourceMaps: false });
|
|
2531
2542
|
const code = output.shared || '';
|
|
2532
2543
|
if (code.trim()) {
|
|
2533
2544
|
// Extract function/const/let names from compiled code
|
|
@@ -2619,17 +2630,23 @@ async function startRepl() {
|
|
|
2619
2630
|
|
|
2620
2631
|
function startWatcher(srcDir, callback) {
|
|
2621
2632
|
let debounceTimer = null;
|
|
2633
|
+
let pendingChanges = new Set();
|
|
2622
2634
|
|
|
2623
2635
|
console.log(` Watching for changes in ${srcDir}...`);
|
|
2624
2636
|
|
|
2625
2637
|
const watcher = fsWatch(srcDir, { recursive: true }, (eventType, filename) => {
|
|
2626
2638
|
if (!filename || !filename.endsWith('.tova')) return;
|
|
2639
|
+
pendingChanges.add(resolve(srcDir, filename));
|
|
2627
2640
|
// Debounce rapid file changes
|
|
2628
2641
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
2629
2642
|
debounceTimer = setTimeout(() => {
|
|
2630
|
-
|
|
2643
|
+
const changed = pendingChanges;
|
|
2644
|
+
pendingChanges = new Set();
|
|
2645
|
+
console.log(`\n File changed: ${[...changed].map(f => basename(f)).join(', ')}`);
|
|
2631
2646
|
try {
|
|
2632
|
-
|
|
2647
|
+
for (const changedPath of changed) {
|
|
2648
|
+
invalidateFile(changedPath);
|
|
2649
|
+
}
|
|
2633
2650
|
callback();
|
|
2634
2651
|
} catch (err) {
|
|
2635
2652
|
console.error(` Rebuild error: ${err.message}`);
|
|
@@ -2740,6 +2757,88 @@ class SourceMapBuilder {
|
|
|
2740
2757
|
}
|
|
2741
2758
|
}
|
|
2742
2759
|
|
|
2760
|
+
// ─── Binary Build ───────────────────────────────────────────
|
|
2761
|
+
|
|
2762
|
+
async function binaryBuild(srcDir, outputName, outDir) {
|
|
2763
|
+
const tovaFiles = findFiles(srcDir, '.tova');
|
|
2764
|
+
if (tovaFiles.length === 0) {
|
|
2765
|
+
console.error('No .tova files found');
|
|
2766
|
+
process.exit(1);
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
const tmpDir = join(outDir, '.tova-binary-tmp');
|
|
2770
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
2771
|
+
|
|
2772
|
+
console.log(`\n Compiling to binary: ${outputName}\n`);
|
|
2773
|
+
|
|
2774
|
+
// Step 1: Compile all .tova files to JS
|
|
2775
|
+
const sharedParts = [];
|
|
2776
|
+
const serverParts = [];
|
|
2777
|
+
const clientParts = [];
|
|
2778
|
+
|
|
2779
|
+
for (const file of tovaFiles) {
|
|
2780
|
+
try {
|
|
2781
|
+
const source = readFileSync(file, 'utf-8');
|
|
2782
|
+
const output = compileTova(source, file);
|
|
2783
|
+
if (output.shared) sharedParts.push(output.shared);
|
|
2784
|
+
if (output.server) serverParts.push(output.server);
|
|
2785
|
+
if (output.client) clientParts.push(output.client);
|
|
2786
|
+
} catch (err) {
|
|
2787
|
+
console.error(` Error in ${relative(srcDir, file)}: ${err.message}`);
|
|
2788
|
+
process.exit(1);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
// Step 2: Bundle into a single JS file
|
|
2793
|
+
const stdlib = getRunStdlib();
|
|
2794
|
+
const allShared = sharedParts.join('\n');
|
|
2795
|
+
const allServer = serverParts.join('\n');
|
|
2796
|
+
|
|
2797
|
+
let bundledCode;
|
|
2798
|
+
if (allServer.trim()) {
|
|
2799
|
+
// Server app
|
|
2800
|
+
bundledCode = stdlib + '\n' + allShared + '\n' + allServer;
|
|
2801
|
+
} else {
|
|
2802
|
+
// Script/shared-only app
|
|
2803
|
+
bundledCode = stdlib + '\n' + allShared;
|
|
2804
|
+
// Auto-call main() if it exists
|
|
2805
|
+
if (/\bfunction\s+main\s*\(/.test(bundledCode)) {
|
|
2806
|
+
bundledCode += '\nconst __tova_exit = await main(process.argv.slice(2)); if (typeof __tova_exit === "number") process.exitCode = __tova_exit;\n';
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
// Strip import/export statements (everything is inlined)
|
|
2811
|
+
bundledCode = bundledCode.replace(/^export /gm, '');
|
|
2812
|
+
bundledCode = bundledCode.replace(/^\s*import\s+(?:\{[^}]*\}|[\w$]+|\*\s+as\s+[\w$]+)\s+from\s+['"][^'"]+['"];?\s*$/gm, '');
|
|
2813
|
+
|
|
2814
|
+
const entryPath = join(tmpDir, 'entry.js');
|
|
2815
|
+
writeFileSync(entryPath, bundledCode);
|
|
2816
|
+
console.log(` Compiled ${tovaFiles.length} file(s) to JS`);
|
|
2817
|
+
|
|
2818
|
+
// Step 3: Use Bun to compile to standalone binary
|
|
2819
|
+
const outputPath = resolve(outputName);
|
|
2820
|
+
try {
|
|
2821
|
+
const { execFileSync } = await import('child_process');
|
|
2822
|
+
execFileSync('bun', ['build', '--compile', entryPath, '--outfile', outputPath], {
|
|
2823
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
2824
|
+
env: process.env,
|
|
2825
|
+
});
|
|
2826
|
+
|
|
2827
|
+
// Get file size
|
|
2828
|
+
const stat = statSync(outputPath);
|
|
2829
|
+
const sizeMB = (stat.size / (1024 * 1024)).toFixed(1);
|
|
2830
|
+
console.log(` Created binary: ${outputPath} (${sizeMB}MB)`);
|
|
2831
|
+
} finally {
|
|
2832
|
+
// Clean up temp files
|
|
2833
|
+
try {
|
|
2834
|
+
rmSync(tmpDir, { recursive: true });
|
|
2835
|
+
} catch {}
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
const displayPath = outputPath.startsWith(process.cwd()) ? './' + relative(process.cwd(), outputPath) : outputPath;
|
|
2839
|
+
console.log(`\n Done! Run with: ${displayPath}\n`);
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2743
2842
|
// ─── Production Build ────────────────────────────────────────
|
|
2744
2843
|
|
|
2745
2844
|
async function productionBuild(srcDir, outDir) {
|
|
@@ -2753,9 +2852,9 @@ async function productionBuild(srcDir, outDir) {
|
|
|
2753
2852
|
|
|
2754
2853
|
console.log(`\n Production build...\n`);
|
|
2755
2854
|
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2855
|
+
const clientParts = [];
|
|
2856
|
+
const serverParts = [];
|
|
2857
|
+
const sharedParts = [];
|
|
2759
2858
|
let cssContent = '';
|
|
2760
2859
|
|
|
2761
2860
|
for (const file of tovaFiles) {
|
|
@@ -2763,17 +2862,21 @@ async function productionBuild(srcDir, outDir) {
|
|
|
2763
2862
|
const source = readFileSync(file, 'utf-8');
|
|
2764
2863
|
const output = compileTova(source, file);
|
|
2765
2864
|
|
|
2766
|
-
if (output.shared)
|
|
2767
|
-
if (output.server)
|
|
2768
|
-
if (output.client)
|
|
2865
|
+
if (output.shared) sharedParts.push(output.shared);
|
|
2866
|
+
if (output.server) serverParts.push(output.server);
|
|
2867
|
+
if (output.client) clientParts.push(output.client);
|
|
2769
2868
|
} catch (err) {
|
|
2770
2869
|
console.error(` Error in ${relative(srcDir, file)}: ${err.message}`);
|
|
2771
2870
|
process.exit(1);
|
|
2772
2871
|
}
|
|
2773
2872
|
}
|
|
2774
2873
|
|
|
2874
|
+
const allClientCode = clientParts.join('\n');
|
|
2875
|
+
const allServerCode = serverParts.join('\n');
|
|
2876
|
+
const allSharedCode = sharedParts.join('\n');
|
|
2877
|
+
|
|
2775
2878
|
// Generate content hash for cache busting
|
|
2776
|
-
const hashCode = (s) =>
|
|
2879
|
+
const hashCode = (s) => Bun.hash(s).toString(16).slice(0, 12);
|
|
2777
2880
|
|
|
2778
2881
|
// Write server bundle
|
|
2779
2882
|
if (allServerCode.trim()) {
|
|
@@ -2846,20 +2949,16 @@ async function productionBuild(srcDir, outDir) {
|
|
|
2846
2949
|
const filePath = join(outDir, f);
|
|
2847
2950
|
const minPath = join(outDir, f.replace('.js', '.min.js'));
|
|
2848
2951
|
try {
|
|
2849
|
-
// Use Bun.
|
|
2850
|
-
const
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
const ratio = ((1 - minSize / originalSize) * 100).toFixed(0);
|
|
2860
|
-
console.log(` ${f.replace('.js', '.min.js')} (${_formatBytes(minSize)}, ${ratio}% smaller)`);
|
|
2861
|
-
minified++;
|
|
2862
|
-
}
|
|
2952
|
+
// Use Bun.Transpiler for minification without bundling (preserves imports)
|
|
2953
|
+
const source = readFileSync(filePath, 'utf-8');
|
|
2954
|
+
const transpiler = new Bun.Transpiler({ minifyWhitespace: true, trimUnusedImports: true });
|
|
2955
|
+
const minCode = transpiler.transformSync(source);
|
|
2956
|
+
writeFileSync(minPath, minCode);
|
|
2957
|
+
const originalSize = Buffer.byteLength(source);
|
|
2958
|
+
const minSize = Buffer.byteLength(minCode);
|
|
2959
|
+
const ratio = ((1 - minSize / originalSize) * 100).toFixed(0);
|
|
2960
|
+
console.log(` ${f.replace('.js', '.min.js')} (${_formatBytes(minSize)}, ${ratio}% smaller)`);
|
|
2961
|
+
minified++;
|
|
2863
2962
|
} catch {
|
|
2864
2963
|
// Bun.build not available — fall back to simple whitespace stripping
|
|
2865
2964
|
try {
|
|
@@ -2884,17 +2983,221 @@ async function productionBuild(srcDir, outDir) {
|
|
|
2884
2983
|
console.log(`\n Production build complete.\n`);
|
|
2885
2984
|
}
|
|
2886
2985
|
|
|
2887
|
-
//
|
|
2986
|
+
// Fallback JS minifier — string/regex-aware, no AST required
|
|
2888
2987
|
function _simpleMinify(code) {
|
|
2889
|
-
// Strip
|
|
2890
|
-
let
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2988
|
+
// Phase 1: Strip comments while respecting strings and regexes
|
|
2989
|
+
let stripped = '';
|
|
2990
|
+
let i = 0;
|
|
2991
|
+
const len = code.length;
|
|
2992
|
+
while (i < len) {
|
|
2993
|
+
const ch = code[i];
|
|
2994
|
+
// String literals — pass through unchanged
|
|
2995
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
2996
|
+
const quote = ch;
|
|
2997
|
+
stripped += ch;
|
|
2998
|
+
i++;
|
|
2999
|
+
while (i < len) {
|
|
3000
|
+
const c = code[i];
|
|
3001
|
+
stripped += c;
|
|
3002
|
+
if (c === '\\') { i++; if (i < len) { stripped += code[i]; i++; } continue; }
|
|
3003
|
+
if (quote === '`' && c === '$' && code[i + 1] === '{') {
|
|
3004
|
+
// Template literal expression — track brace depth
|
|
3005
|
+
stripped += code[++i]; // '{'
|
|
3006
|
+
i++;
|
|
3007
|
+
let depth = 1;
|
|
3008
|
+
while (i < len && depth > 0) {
|
|
3009
|
+
const tc = code[i];
|
|
3010
|
+
if (tc === '{') depth++;
|
|
3011
|
+
else if (tc === '}') depth--;
|
|
3012
|
+
if (depth > 0) { stripped += tc; i++; }
|
|
3013
|
+
}
|
|
3014
|
+
if (i < len) { stripped += code[i]; i++; } // closing '}'
|
|
3015
|
+
continue;
|
|
3016
|
+
}
|
|
3017
|
+
if (c === quote) { i++; break; }
|
|
3018
|
+
i++;
|
|
3019
|
+
}
|
|
3020
|
+
continue;
|
|
3021
|
+
}
|
|
3022
|
+
// Single-line comment
|
|
3023
|
+
if (ch === '/' && code[i + 1] === '/') {
|
|
3024
|
+
while (i < len && code[i] !== '\n') i++;
|
|
3025
|
+
continue;
|
|
3026
|
+
}
|
|
3027
|
+
// Multi-line comment
|
|
3028
|
+
if (ch === '/' && code[i + 1] === '*') {
|
|
3029
|
+
i += 2;
|
|
3030
|
+
while (i < len - 1 && !(code[i] === '*' && code[i + 1] === '/')) i++;
|
|
3031
|
+
i += 2;
|
|
3032
|
+
continue;
|
|
3033
|
+
}
|
|
3034
|
+
stripped += ch;
|
|
3035
|
+
i++;
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
// Phase 2: Process line by line
|
|
3039
|
+
const lines = stripped.split('\n');
|
|
3040
|
+
const out = [];
|
|
3041
|
+
for (let j = 0; j < lines.length; j++) {
|
|
3042
|
+
let line = lines[j].trim();
|
|
3043
|
+
if (!line) continue; // remove blank lines
|
|
3044
|
+
// Strip console.log/debug/warn/info statements (production only)
|
|
3045
|
+
if (/^\s*console\.(log|debug|warn|info)\s*\(/.test(line)) {
|
|
3046
|
+
// Simple balanced-paren check for single-line console calls
|
|
3047
|
+
let parens = 0, inStr = false, sq = '';
|
|
3048
|
+
for (let k = 0; k < line.length; k++) {
|
|
3049
|
+
const c = line[k];
|
|
3050
|
+
if (inStr) { if (c === '\\') k++; else if (c === sq) inStr = false; continue; }
|
|
3051
|
+
if (c === '"' || c === "'" || c === '`') { inStr = true; sq = c; continue; }
|
|
3052
|
+
if (c === '(') parens++;
|
|
3053
|
+
if (c === ')') { parens--; if (parens === 0) break; }
|
|
3054
|
+
}
|
|
3055
|
+
if (parens === 0) continue; // balanced — safe to strip
|
|
3056
|
+
}
|
|
3057
|
+
out.push(line);
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
// Phase 3: Collapse whitespace — protect string literals with placeholders
|
|
3061
|
+
let joined = out.join('\n');
|
|
3062
|
+
const strings = [];
|
|
3063
|
+
// Extract all string literals into placeholders so regexes don't mangle them
|
|
3064
|
+
let prot = '';
|
|
3065
|
+
let pi = 0;
|
|
3066
|
+
const plen = joined.length;
|
|
3067
|
+
while (pi < plen) {
|
|
3068
|
+
const pc = joined[pi];
|
|
3069
|
+
if (pc === '"' || pc === "'" || pc === '`') {
|
|
3070
|
+
const q = pc;
|
|
3071
|
+
let s = pc;
|
|
3072
|
+
pi++;
|
|
3073
|
+
while (pi < plen) {
|
|
3074
|
+
const c = joined[pi];
|
|
3075
|
+
s += c;
|
|
3076
|
+
if (c === '\\') { pi++; if (pi < plen) { s += joined[pi]; pi++; } continue; }
|
|
3077
|
+
if (q === '`' && c === '$' && joined[pi + 1] === '{') {
|
|
3078
|
+
s += joined[++pi]; pi++;
|
|
3079
|
+
let d = 1;
|
|
3080
|
+
while (pi < plen && d > 0) {
|
|
3081
|
+
const tc = joined[pi];
|
|
3082
|
+
if (tc === '{') d++; else if (tc === '}') d--;
|
|
3083
|
+
if (d > 0) { s += tc; pi++; }
|
|
3084
|
+
}
|
|
3085
|
+
if (pi < plen) { s += joined[pi]; pi++; }
|
|
3086
|
+
continue;
|
|
3087
|
+
}
|
|
3088
|
+
if (c === q) { pi++; break; }
|
|
3089
|
+
pi++;
|
|
3090
|
+
}
|
|
3091
|
+
strings.push(s);
|
|
3092
|
+
prot += `\x01${strings.length - 1}\x01`;
|
|
3093
|
+
} else {
|
|
3094
|
+
prot += pc;
|
|
3095
|
+
pi++;
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
// Collapse runs of spaces/tabs to single space
|
|
3100
|
+
prot = prot.replace(/[ \t]+/g, ' ');
|
|
3101
|
+
// Remove spaces around braces, brackets, parens, semicolons, commas
|
|
3102
|
+
prot = prot.replace(/ ?([{}[\]();,]) ?/g, '$1');
|
|
3103
|
+
// Restore space after keywords that need it
|
|
3104
|
+
prot = prot.replace(/\b(return|const|let|var|if|else|for|while|do|switch|case|throw|new|typeof|instanceof|in|of|yield|await|export|import|from|function|class|extends|async|delete|void)\b(?=[^\s;,})\]])/g, '$1 ');
|
|
3105
|
+
// Add space after colon in object literals (key: value)
|
|
3106
|
+
prot = prot.replace(/([a-zA-Z0-9_$]):([^\s}])/g, '$1: $2');
|
|
3107
|
+
|
|
3108
|
+
// Restore string literals
|
|
3109
|
+
let result = prot.replace(/\x01(\d+)\x01/g, (_, idx) => strings[idx]);
|
|
3110
|
+
|
|
3111
|
+
// Phase 4: Dead function elimination — remove unused top-level functions
|
|
3112
|
+
result = _eliminateDeadFunctions(result);
|
|
3113
|
+
|
|
3114
|
+
return result;
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
// Remove top-level function declarations that are never reachable from non-function code.
|
|
3118
|
+
// Uses reachability analysis to handle mutual recursion (foo↔bar both dead).
|
|
3119
|
+
function _eliminateDeadFunctions(code) {
|
|
3120
|
+
const funcDeclRe = /^function\s+([\w$]+)\s*\(/gm;
|
|
3121
|
+
const allDecls = []; // [{ name, start, end }]
|
|
3122
|
+
let m;
|
|
3123
|
+
|
|
3124
|
+
// First pass: find all top-level function declarations and their full extent
|
|
3125
|
+
while ((m = funcDeclRe.exec(code)) !== null) {
|
|
3126
|
+
const name = m[1];
|
|
3127
|
+
const start = m.index;
|
|
3128
|
+
let depth = 0, i = start, inStr = false, strCh = '', foundOpen = false;
|
|
3129
|
+
while (i < code.length) {
|
|
3130
|
+
const ch = code[i];
|
|
3131
|
+
if (inStr) { if (ch === '\\') { i += 2; continue; } if (ch === strCh) inStr = false; i++; continue; }
|
|
3132
|
+
if (ch === '"' || ch === "'" || ch === '`') { inStr = true; strCh = ch; i++; continue; }
|
|
3133
|
+
if (ch === '{') { depth++; foundOpen = true; }
|
|
3134
|
+
else if (ch === '}') { depth--; if (foundOpen && depth === 0) { i++; break; } }
|
|
3135
|
+
i++;
|
|
3136
|
+
}
|
|
3137
|
+
allDecls.push({ name, start, end: i });
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
if (allDecls.length === 0) return code;
|
|
3141
|
+
|
|
3142
|
+
// Build a set of all declared function names
|
|
3143
|
+
const declaredNames = new Set(allDecls.map(d => d.name));
|
|
3144
|
+
|
|
3145
|
+
// Helper: find which declared names are referenced in a text region
|
|
3146
|
+
function findRefs(text) {
|
|
3147
|
+
const refs = new Set();
|
|
3148
|
+
for (const name of declaredNames) {
|
|
3149
|
+
const escaped = name.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
3150
|
+
if (new RegExp('\\b' + escaped + '\\b').test(text)) refs.add(name);
|
|
3151
|
+
}
|
|
3152
|
+
return refs;
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
// Build "root" code — everything outside function declarations
|
|
3156
|
+
const sortedDecls = [...allDecls].sort((a, b) => a.start - b.start);
|
|
3157
|
+
let rootCode = '';
|
|
3158
|
+
let pos = 0;
|
|
3159
|
+
for (const decl of sortedDecls) {
|
|
3160
|
+
rootCode += code.slice(pos, decl.start);
|
|
3161
|
+
pos = decl.end;
|
|
3162
|
+
}
|
|
3163
|
+
rootCode += code.slice(pos);
|
|
3164
|
+
|
|
3165
|
+
// Find which functions are directly reachable from root code
|
|
3166
|
+
const rootRefs = findRefs(rootCode);
|
|
3167
|
+
|
|
3168
|
+
// Build dependency graph: for each function, which other declared functions does it reference?
|
|
3169
|
+
const deps = new Map();
|
|
3170
|
+
for (const decl of allDecls) {
|
|
3171
|
+
const body = code.slice(decl.start, decl.end);
|
|
3172
|
+
const bodyRefs = findRefs(body);
|
|
3173
|
+
bodyRefs.delete(decl.name); // ignore self-references
|
|
3174
|
+
deps.set(decl.name, bodyRefs);
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
// BFS from root refs to find all transitively reachable functions
|
|
3178
|
+
const reachable = new Set();
|
|
3179
|
+
const queue = [...rootRefs];
|
|
3180
|
+
while (queue.length > 0) {
|
|
3181
|
+
const name = queue.pop();
|
|
3182
|
+
if (reachable.has(name)) continue;
|
|
3183
|
+
reachable.add(name);
|
|
3184
|
+
const fnDeps = deps.get(name);
|
|
3185
|
+
if (fnDeps) for (const dep of fnDeps) queue.push(dep);
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
// Remove unreachable functions
|
|
3189
|
+
const toRemove = allDecls.filter(d => !reachable.has(d.name));
|
|
3190
|
+
if (toRemove.length === 0) return code;
|
|
3191
|
+
|
|
3192
|
+
toRemove.sort((a, b) => b.start - a.start);
|
|
3193
|
+
let result = code;
|
|
3194
|
+
for (const { start, end } of toRemove) {
|
|
3195
|
+
let removeEnd = end;
|
|
3196
|
+
while (removeEnd < result.length && (result[removeEnd] === '\n' || result[removeEnd] === '\r')) removeEnd++;
|
|
3197
|
+
result = result.slice(0, start) + result.slice(removeEnd);
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
return result;
|
|
2898
3201
|
}
|
|
2899
3202
|
|
|
2900
3203
|
function _formatBytes(bytes) {
|
|
@@ -2916,7 +3219,7 @@ class BuildCache {
|
|
|
2916
3219
|
}
|
|
2917
3220
|
|
|
2918
3221
|
_hashContent(content) {
|
|
2919
|
-
return
|
|
3222
|
+
return Bun.hash(content).toString(16);
|
|
2920
3223
|
}
|
|
2921
3224
|
|
|
2922
3225
|
load() {
|
|
@@ -2953,12 +3256,11 @@ class BuildCache {
|
|
|
2953
3256
|
|
|
2954
3257
|
// Hash multiple files together for group caching
|
|
2955
3258
|
_hashGroup(files) {
|
|
2956
|
-
|
|
3259
|
+
let combined = '';
|
|
2957
3260
|
for (const f of files.slice().sort()) {
|
|
2958
|
-
|
|
2959
|
-
hash.update(readFileSync(f, 'utf-8'));
|
|
3261
|
+
combined += f + readFileSync(f, 'utf-8');
|
|
2960
3262
|
}
|
|
2961
|
-
return hash
|
|
3263
|
+
return Bun.hash(combined).toString(16);
|
|
2962
3264
|
}
|
|
2963
3265
|
|
|
2964
3266
|
// Store compiled output for a multi-file group
|
|
@@ -3012,19 +3314,43 @@ class BuildCache {
|
|
|
3012
3314
|
|
|
3013
3315
|
// Determine the compiled JS extension for a .tova file.
|
|
3014
3316
|
// Module files (no blocks) → '.js', app files → '.shared.js'
|
|
3317
|
+
const moduleTypeCache = new Map(); // tovaPath -> '.js' | '.shared.js'
|
|
3318
|
+
|
|
3015
3319
|
function getCompiledExtension(tovaPath) {
|
|
3016
3320
|
// Check compilation cache first
|
|
3017
3321
|
if (compilationCache.has(tovaPath)) {
|
|
3018
3322
|
return compilationCache.get(tovaPath).isModule ? '.js' : '.shared.js';
|
|
3019
3323
|
}
|
|
3020
|
-
//
|
|
3324
|
+
// Check module type cache (set during parsing)
|
|
3325
|
+
if (moduleTypeCache.has(tovaPath)) {
|
|
3326
|
+
return moduleTypeCache.get(tovaPath);
|
|
3327
|
+
}
|
|
3328
|
+
// Fall back: quick-lex the file to detect block keywords at top level
|
|
3021
3329
|
if (existsSync(tovaPath)) {
|
|
3022
3330
|
const src = readFileSync(tovaPath, 'utf-8');
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3331
|
+
try {
|
|
3332
|
+
const lexer = new Lexer(src, tovaPath);
|
|
3333
|
+
const tokens = lexer.tokenize();
|
|
3334
|
+
// Check if any top-level token is a block keyword (shared/server/client/test/bench/data)
|
|
3335
|
+
const BLOCK_KEYWORDS = new Set(['shared', 'server', 'client', 'test', 'bench', 'data']);
|
|
3336
|
+
let depth = 0;
|
|
3337
|
+
for (const tok of tokens) {
|
|
3338
|
+
if (tok.type === 'LBRACE') depth++;
|
|
3339
|
+
else if (tok.type === 'RBRACE') depth--;
|
|
3340
|
+
else if (depth === 0 && tok.type === 'IDENTIFIER' && BLOCK_KEYWORDS.has(tok.value)) {
|
|
3341
|
+
moduleTypeCache.set(tovaPath, '.shared.js');
|
|
3342
|
+
return '.shared.js';
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
moduleTypeCache.set(tovaPath, '.js');
|
|
3346
|
+
return '.js';
|
|
3347
|
+
} catch {
|
|
3348
|
+
// If lexing fails, fall back to regex heuristic
|
|
3349
|
+
if (/^(?:shared|server|client|test|bench|data)\s*(?:\{|")/m.test(src)) {
|
|
3350
|
+
return '.shared.js';
|
|
3351
|
+
}
|
|
3352
|
+
return '.js';
|
|
3026
3353
|
}
|
|
3027
|
-
return '.js';
|
|
3028
3354
|
}
|
|
3029
3355
|
return '.shared.js'; // default fallback
|
|
3030
3356
|
}
|
|
@@ -3036,6 +3362,53 @@ const compilationChain = []; // ordered import chain for circular import error m
|
|
|
3036
3362
|
// Track module exports for cross-file import validation
|
|
3037
3363
|
const moduleExports = new Map();
|
|
3038
3364
|
|
|
3365
|
+
// Dependency graph: file -> Set of files it imports (forward deps)
|
|
3366
|
+
const fileDependencies = new Map();
|
|
3367
|
+
// Reverse dependency graph: file -> Set of files that import it
|
|
3368
|
+
const fileReverseDeps = new Map();
|
|
3369
|
+
|
|
3370
|
+
function trackDependency(fromFile, toFile) {
|
|
3371
|
+
if (!fileDependencies.has(fromFile)) fileDependencies.set(fromFile, new Set());
|
|
3372
|
+
fileDependencies.get(fromFile).add(toFile);
|
|
3373
|
+
if (!fileReverseDeps.has(toFile)) fileReverseDeps.set(toFile, new Set());
|
|
3374
|
+
fileReverseDeps.get(toFile).add(fromFile);
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
// Get all files that transitively depend on changedFile
|
|
3378
|
+
function getTransitiveDependents(changedFile) {
|
|
3379
|
+
const dependents = new Set();
|
|
3380
|
+
const queue = [changedFile];
|
|
3381
|
+
while (queue.length > 0) {
|
|
3382
|
+
const file = queue.pop();
|
|
3383
|
+
dependents.add(file);
|
|
3384
|
+
const rdeps = fileReverseDeps.get(file);
|
|
3385
|
+
if (rdeps) {
|
|
3386
|
+
for (const dep of rdeps) {
|
|
3387
|
+
if (!dependents.has(dep)) queue.push(dep);
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
return dependents;
|
|
3392
|
+
}
|
|
3393
|
+
|
|
3394
|
+
function invalidateFile(changedPath) {
|
|
3395
|
+
const toInvalidate = getTransitiveDependents(changedPath);
|
|
3396
|
+
for (const file of toInvalidate) {
|
|
3397
|
+
compilationCache.delete(file);
|
|
3398
|
+
moduleTypeCache.delete(file);
|
|
3399
|
+
moduleExports.delete(file);
|
|
3400
|
+
// Clear forward deps (will be rebuilt on recompile)
|
|
3401
|
+
const deps = fileDependencies.get(file);
|
|
3402
|
+
if (deps) {
|
|
3403
|
+
for (const dep of deps) {
|
|
3404
|
+
const rdeps = fileReverseDeps.get(dep);
|
|
3405
|
+
if (rdeps) rdeps.delete(file);
|
|
3406
|
+
}
|
|
3407
|
+
fileDependencies.delete(file);
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3039
3412
|
function collectExports(ast, filename) {
|
|
3040
3413
|
const publicExports = new Set();
|
|
3041
3414
|
const allNames = new Set();
|
|
@@ -3115,6 +3488,10 @@ function compileWithImports(source, filename, srcDir) {
|
|
|
3115
3488
|
const parser = new Parser(tokens, filename);
|
|
3116
3489
|
const ast = parser.parse();
|
|
3117
3490
|
|
|
3491
|
+
// Cache module type from AST (avoids regex heuristic on subsequent lookups)
|
|
3492
|
+
const hasBlocks = ast.body.some(n => n.type === 'SharedBlock' || n.type === 'ServerBlock' || n.type === 'ClientBlock' || n.type === 'TestBlock' || n.type === 'BenchBlock' || n.type === 'DataBlock');
|
|
3493
|
+
moduleTypeCache.set(filename, hasBlocks ? '.shared.js' : '.js');
|
|
3494
|
+
|
|
3118
3495
|
// Collect this module's exports for validation
|
|
3119
3496
|
collectExports(ast, filename);
|
|
3120
3497
|
|
|
@@ -3122,6 +3499,7 @@ function compileWithImports(source, filename, srcDir) {
|
|
|
3122
3499
|
for (const node of ast.body) {
|
|
3123
3500
|
if (node.type === 'ImportDeclaration' && node.source.endsWith('.tova')) {
|
|
3124
3501
|
const importPath = resolve(dirname(filename), node.source);
|
|
3502
|
+
trackDependency(filename, importPath);
|
|
3125
3503
|
if (compilationInProgress.has(importPath)) {
|
|
3126
3504
|
const chain = [...compilationChain, importPath].map(f => basename(f)).join(' \u2192 ');
|
|
3127
3505
|
throw new Error(`Circular import detected: ${chain}`);
|
|
@@ -3148,6 +3526,7 @@ function compileWithImports(source, filename, srcDir) {
|
|
|
3148
3526
|
}
|
|
3149
3527
|
if (node.type === 'ImportDefault' && node.source.endsWith('.tova')) {
|
|
3150
3528
|
const importPath = resolve(dirname(filename), node.source);
|
|
3529
|
+
trackDependency(filename, importPath);
|
|
3151
3530
|
if (compilationInProgress.has(importPath)) {
|
|
3152
3531
|
const chain = [...compilationChain, importPath].map(f => basename(f)).join(' \u2192 ');
|
|
3153
3532
|
throw new Error(`Circular import detected: ${chain}`);
|
|
@@ -3160,6 +3539,7 @@ function compileWithImports(source, filename, srcDir) {
|
|
|
3160
3539
|
}
|
|
3161
3540
|
if (node.type === 'ImportWildcard' && node.source.endsWith('.tova')) {
|
|
3162
3541
|
const importPath = resolve(dirname(filename), node.source);
|
|
3542
|
+
trackDependency(filename, importPath);
|
|
3163
3543
|
if (compilationInProgress.has(importPath)) {
|
|
3164
3544
|
const chain = [...compilationChain, importPath].map(f => basename(f)).join(' \u2192 ');
|
|
3165
3545
|
throw new Error(`Circular import detected: ${chain}`);
|