tova 0.3.7 → 0.3.9
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 +723 -104
- package/package.json +2 -7
- package/src/analyzer/analyzer.js +43 -4
- package/src/codegen/base-codegen.js +216 -1
- package/src/codegen/codegen.js +6 -2
- package/src/stdlib/inline.js +1 -0
- package/src/version.js +1 -1
package/bin/tova.js
CHANGED
|
@@ -24,6 +24,17 @@ import { stringifyTOML } from '../src/config/toml.js';
|
|
|
24
24
|
|
|
25
25
|
import { VERSION } from '../src/version.js';
|
|
26
26
|
|
|
27
|
+
// ─── CLI Color Helpers ──────────────────────────────────────
|
|
28
|
+
const isTTY = process.stdout?.isTTY;
|
|
29
|
+
const color = {
|
|
30
|
+
bold: s => isTTY ? `\x1b[1m${s}\x1b[0m` : s,
|
|
31
|
+
green: s => isTTY ? `\x1b[32m${s}\x1b[0m` : s,
|
|
32
|
+
yellow: s => isTTY ? `\x1b[33m${s}\x1b[0m` : s,
|
|
33
|
+
red: s => isTTY ? `\x1b[31m${s}\x1b[0m` : s,
|
|
34
|
+
cyan: s => isTTY ? `\x1b[36m${s}\x1b[0m` : s,
|
|
35
|
+
dim: s => isTTY ? `\x1b[2m${s}\x1b[0m` : s,
|
|
36
|
+
};
|
|
37
|
+
|
|
27
38
|
const HELP = `
|
|
28
39
|
╦ ╦ ╦═╗ ╦
|
|
29
40
|
║ ║ ║ ║ ╠╣
|
|
@@ -41,7 +52,7 @@ Commands:
|
|
|
41
52
|
check [dir] Type-check .tova files without generating code
|
|
42
53
|
clean Delete .tova-out build artifacts
|
|
43
54
|
dev Start development server with live reload
|
|
44
|
-
new <name> Create a new Tova project
|
|
55
|
+
new <name> Create a new Tova project (--template fullstack|api|script|library|blank)
|
|
45
56
|
install Install npm dependencies from tova.toml
|
|
46
57
|
add <pkg> Add an npm dependency (--dev for dev dependency)
|
|
47
58
|
remove <pkg> Remove an npm dependency
|
|
@@ -57,6 +68,8 @@ Commands:
|
|
|
57
68
|
migrate:status [file.tova] Show migration status
|
|
58
69
|
upgrade Upgrade Tova to the latest version
|
|
59
70
|
info Show Tova version, Bun version, project config, and installed dependencies
|
|
71
|
+
doctor Check your development environment
|
|
72
|
+
completions <sh> Generate shell completions (bash, zsh, fish)
|
|
60
73
|
explain <code> Show detailed explanation for an error/warning code (e.g., tova explain E202)
|
|
61
74
|
|
|
62
75
|
Options:
|
|
@@ -116,7 +129,7 @@ async function main() {
|
|
|
116
129
|
await startLsp();
|
|
117
130
|
break;
|
|
118
131
|
case 'new':
|
|
119
|
-
newProject(args
|
|
132
|
+
await newProject(args.slice(1));
|
|
120
133
|
break;
|
|
121
134
|
case 'init':
|
|
122
135
|
initProject();
|
|
@@ -186,6 +199,12 @@ async function main() {
|
|
|
186
199
|
case 'info':
|
|
187
200
|
await infoCommand();
|
|
188
201
|
break;
|
|
202
|
+
case 'doctor':
|
|
203
|
+
await doctorCommand();
|
|
204
|
+
break;
|
|
205
|
+
case 'completions':
|
|
206
|
+
completionsCommand(args[1]);
|
|
207
|
+
break;
|
|
189
208
|
default:
|
|
190
209
|
if (command.endsWith('.tova')) {
|
|
191
210
|
const directArgs = args.filter(a => a !== '--strict').slice(1);
|
|
@@ -1405,42 +1424,282 @@ ${inlineClient}
|
|
|
1405
1424
|
|
|
1406
1425
|
// ─── New Project ────────────────────────────────────────────
|
|
1407
1426
|
|
|
1408
|
-
|
|
1427
|
+
// ─── Template Definitions ────────────────────────────────────
|
|
1428
|
+
|
|
1429
|
+
const PROJECT_TEMPLATES = {
|
|
1430
|
+
fullstack: {
|
|
1431
|
+
label: 'Full-stack app',
|
|
1432
|
+
description: 'server + client + shared blocks',
|
|
1433
|
+
tomlDescription: 'A full-stack Tova application',
|
|
1434
|
+
entry: 'src',
|
|
1435
|
+
file: 'src/app.tova',
|
|
1436
|
+
content: name => `// ${name} — Built with Tova
|
|
1437
|
+
|
|
1438
|
+
shared {
|
|
1439
|
+
type Message {
|
|
1440
|
+
text: String
|
|
1441
|
+
timestamp: String
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
server {
|
|
1446
|
+
fn get_message() -> Message {
|
|
1447
|
+
Message("Hello from Tova!", Date.new().toLocaleTimeString())
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
route GET "/api/message" => get_message
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
client {
|
|
1454
|
+
state message = ""
|
|
1455
|
+
state timestamp = ""
|
|
1456
|
+
state refreshing = false
|
|
1457
|
+
|
|
1458
|
+
effect {
|
|
1459
|
+
result = server.get_message()
|
|
1460
|
+
message = result.text
|
|
1461
|
+
timestamp = result.timestamp
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
fn handle_refresh() {
|
|
1465
|
+
refreshing = true
|
|
1466
|
+
result = server.get_message()
|
|
1467
|
+
message = result.text
|
|
1468
|
+
timestamp = result.timestamp
|
|
1469
|
+
refreshing = false
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
component FeatureCard(icon, title, description) {
|
|
1473
|
+
<div class="group relative bg-white rounded-2xl p-6 shadow-sm border border-gray-100 hover:shadow-lg hover:border-indigo-100 transition-all duration-300">
|
|
1474
|
+
<div class="w-10 h-10 bg-indigo-50 rounded-xl flex items-center justify-center text-lg mb-4 group-hover:bg-indigo-100 transition-colors">
|
|
1475
|
+
"{icon}"
|
|
1476
|
+
</div>
|
|
1477
|
+
<h3 class="font-semibold text-gray-900 mb-1">"{title}"</h3>
|
|
1478
|
+
<p class="text-sm text-gray-500 leading-relaxed">"{description}"</p>
|
|
1479
|
+
</div>
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
component App {
|
|
1483
|
+
<div class="min-h-screen bg-gradient-to-br from-slate-50 via-white to-indigo-50">
|
|
1484
|
+
<nav class="border-b border-gray-100 bg-white/80 backdrop-blur-sm sticky top-0 z-10">
|
|
1485
|
+
<div class="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
|
|
1486
|
+
<div class="flex items-center gap-2">
|
|
1487
|
+
<div class="w-8 h-8 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg"></div>
|
|
1488
|
+
<span class="font-bold text-gray-900 text-lg">"${name}"</span>
|
|
1489
|
+
</div>
|
|
1490
|
+
<div class="flex items-center gap-4">
|
|
1491
|
+
<a href="https://github.com/tova-lang/tova-lang" class="text-sm text-gray-500 hover:text-gray-900 transition-colors">"Docs"</a>
|
|
1492
|
+
<a href="https://github.com/tova-lang/tova-lang" class="text-sm bg-gray-900 text-white px-4 py-2 rounded-lg hover:bg-gray-800 transition-colors">"GitHub"</a>
|
|
1493
|
+
</div>
|
|
1494
|
+
</div>
|
|
1495
|
+
</nav>
|
|
1496
|
+
|
|
1497
|
+
<main class="max-w-5xl mx-auto px-6">
|
|
1498
|
+
<div class="py-20 text-center">
|
|
1499
|
+
<div class="inline-flex items-center gap-2 bg-indigo-50 text-indigo-700 text-sm font-medium px-4 py-1.5 rounded-full mb-6">
|
|
1500
|
+
<span class="w-1.5 h-1.5 bg-indigo-500 rounded-full"></span>
|
|
1501
|
+
"Powered by Tova"
|
|
1502
|
+
</div>
|
|
1503
|
+
<h1 class="text-5xl font-bold text-gray-900 tracking-tight mb-4">"Welcome to " <span class="bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">"${name}"</span></h1>
|
|
1504
|
+
<p class="text-xl text-gray-500 max-w-2xl mx-auto mb-10">"A modern full-stack app. Edit " <code class="text-sm bg-gray-100 text-indigo-600 px-2 py-1 rounded-md font-mono">"src/app.tova"</code> " to get started."</p>
|
|
1505
|
+
|
|
1506
|
+
<div class="inline-flex items-center gap-3 bg-white border border-gray-200 rounded-2xl p-2 shadow-sm">
|
|
1507
|
+
<div class="bg-gradient-to-r from-indigo-500 to-purple-500 text-white px-5 py-2.5 rounded-xl font-medium">
|
|
1508
|
+
"{message}"
|
|
1509
|
+
</div>
|
|
1510
|
+
<button
|
|
1511
|
+
on:click={handle_refresh}
|
|
1512
|
+
class="px-4 py-2.5 text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 rounded-xl transition-all font-medium text-sm"
|
|
1513
|
+
>
|
|
1514
|
+
if refreshing {
|
|
1515
|
+
"..."
|
|
1516
|
+
} else {
|
|
1517
|
+
"Refresh"
|
|
1518
|
+
}
|
|
1519
|
+
</button>
|
|
1520
|
+
</div>
|
|
1521
|
+
if timestamp != "" {
|
|
1522
|
+
<p class="text-xs text-gray-400 mt-3">"Last fetched at " "{timestamp}"</p>
|
|
1523
|
+
}
|
|
1524
|
+
</div>
|
|
1525
|
+
|
|
1526
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-5 pb-20">
|
|
1527
|
+
<FeatureCard
|
|
1528
|
+
icon="⚙"
|
|
1529
|
+
title="Full-Stack"
|
|
1530
|
+
description="Server and client in one file. Shared types, RPC calls, and reactive UI — all type-safe."
|
|
1531
|
+
/>
|
|
1532
|
+
<FeatureCard
|
|
1533
|
+
icon="⚡"
|
|
1534
|
+
title="Fast Refresh"
|
|
1535
|
+
description="Edit your code and see changes instantly. The dev server recompiles on save."
|
|
1536
|
+
/>
|
|
1537
|
+
<FeatureCard
|
|
1538
|
+
icon="🎨"
|
|
1539
|
+
title="Tailwind Built-in"
|
|
1540
|
+
description="Style with utility classes out of the box. No config or build step needed."
|
|
1541
|
+
/>
|
|
1542
|
+
</div>
|
|
1543
|
+
|
|
1544
|
+
<div class="border-t border-gray-100 py-8 text-center">
|
|
1545
|
+
<p class="text-sm text-gray-400">"Built with " <a href="https://github.com/tova-lang/tova-lang" class="text-indigo-500 hover:text-indigo-600 transition-colors">"Tova"</a></p>
|
|
1546
|
+
</div>
|
|
1547
|
+
</main>
|
|
1548
|
+
</div>
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
`,
|
|
1552
|
+
nextSteps: name => ` cd ${name}\n tova dev`,
|
|
1553
|
+
},
|
|
1554
|
+
api: {
|
|
1555
|
+
label: 'API server',
|
|
1556
|
+
description: 'HTTP routes, no frontend',
|
|
1557
|
+
tomlDescription: 'A Tova API server',
|
|
1558
|
+
entry: 'src',
|
|
1559
|
+
file: 'src/app.tova',
|
|
1560
|
+
content: name => `// ${name} — Built with Tova
|
|
1561
|
+
|
|
1562
|
+
server {
|
|
1563
|
+
fn health() -> { status: String } {
|
|
1564
|
+
{ status: "ok" }
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
route GET "/api/health" => health
|
|
1568
|
+
}
|
|
1569
|
+
`,
|
|
1570
|
+
nextSteps: name => ` cd ${name}\n tova dev`,
|
|
1571
|
+
},
|
|
1572
|
+
script: {
|
|
1573
|
+
label: 'Script',
|
|
1574
|
+
description: 'standalone .tova script',
|
|
1575
|
+
tomlDescription: 'A Tova script',
|
|
1576
|
+
entry: 'src',
|
|
1577
|
+
file: 'src/main.tova',
|
|
1578
|
+
content: name => `// ${name} — Built with Tova
|
|
1579
|
+
|
|
1580
|
+
name = "world"
|
|
1581
|
+
print("Hello, {name}!")
|
|
1582
|
+
`,
|
|
1583
|
+
nextSteps: name => ` cd ${name}\n tova run src/main.tova`,
|
|
1584
|
+
},
|
|
1585
|
+
library: {
|
|
1586
|
+
label: 'Library',
|
|
1587
|
+
description: 'reusable module with exports',
|
|
1588
|
+
tomlDescription: 'A Tova library',
|
|
1589
|
+
entry: 'src',
|
|
1590
|
+
noEntry: true,
|
|
1591
|
+
file: 'src/lib.tova',
|
|
1592
|
+
content: name => `// ${name} — A Tova library
|
|
1593
|
+
|
|
1594
|
+
pub fn greet(name: String) -> String {
|
|
1595
|
+
"Hello, {name}!"
|
|
1596
|
+
}
|
|
1597
|
+
`,
|
|
1598
|
+
nextSteps: name => ` cd ${name}\n tova build`,
|
|
1599
|
+
},
|
|
1600
|
+
blank: {
|
|
1601
|
+
label: 'Blank',
|
|
1602
|
+
description: 'empty project skeleton',
|
|
1603
|
+
tomlDescription: 'A Tova project',
|
|
1604
|
+
entry: 'src',
|
|
1605
|
+
file: null,
|
|
1606
|
+
content: null,
|
|
1607
|
+
nextSteps: name => ` cd ${name}`,
|
|
1608
|
+
},
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1611
|
+
const TEMPLATE_ORDER = ['fullstack', 'api', 'script', 'library', 'blank'];
|
|
1612
|
+
|
|
1613
|
+
async function newProject(rawArgs) {
|
|
1614
|
+
const name = rawArgs.find(a => !a.startsWith('-'));
|
|
1615
|
+
const templateFlag = rawArgs.find(a => a.startsWith('--template'));
|
|
1616
|
+
let templateName = null;
|
|
1617
|
+
if (templateFlag) {
|
|
1618
|
+
const idx = rawArgs.indexOf(templateFlag);
|
|
1619
|
+
if (templateFlag.includes('=')) {
|
|
1620
|
+
templateName = templateFlag.split('=')[1];
|
|
1621
|
+
} else {
|
|
1622
|
+
templateName = rawArgs[idx + 1];
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1409
1626
|
if (!name) {
|
|
1410
|
-
console.error('Error: No project name specified');
|
|
1411
|
-
console.error('Usage: tova new <project-name>');
|
|
1627
|
+
console.error(color.red('Error: No project name specified'));
|
|
1628
|
+
console.error('Usage: tova new <project-name> [--template fullstack|api|script|library|blank]');
|
|
1412
1629
|
process.exit(1);
|
|
1413
1630
|
}
|
|
1414
1631
|
|
|
1415
1632
|
const projectDir = resolve(name);
|
|
1416
1633
|
if (existsSync(projectDir)) {
|
|
1417
|
-
console.error(`Error: Directory '${name}' already exists`);
|
|
1634
|
+
console.error(color.red(`Error: Directory '${name}' already exists`));
|
|
1418
1635
|
process.exit(1);
|
|
1419
1636
|
}
|
|
1420
1637
|
|
|
1421
|
-
|
|
1638
|
+
// Resolve template
|
|
1639
|
+
if (templateName && !PROJECT_TEMPLATES[templateName]) {
|
|
1640
|
+
console.error(color.red(`Error: Unknown template '${templateName}'`));
|
|
1641
|
+
console.error(`Available templates: ${TEMPLATE_ORDER.join(', ')}`);
|
|
1642
|
+
process.exit(1);
|
|
1643
|
+
}
|
|
1422
1644
|
|
|
1645
|
+
if (!templateName) {
|
|
1646
|
+
// Interactive picker
|
|
1647
|
+
console.log(`\n ${color.bold('Creating new Tova project:')} ${color.cyan(name)}\n`);
|
|
1648
|
+
console.log(' Pick a template:\n');
|
|
1649
|
+
TEMPLATE_ORDER.forEach((key, i) => {
|
|
1650
|
+
const t = PROJECT_TEMPLATES[key];
|
|
1651
|
+
const num = color.bold(`${i + 1}`);
|
|
1652
|
+
const label = color.cyan(t.label);
|
|
1653
|
+
console.log(` ${num}. ${label} ${color.dim('—')} ${t.description}`);
|
|
1654
|
+
});
|
|
1655
|
+
console.log('');
|
|
1656
|
+
|
|
1657
|
+
const { createInterface } = await import('readline');
|
|
1658
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1659
|
+
const answer = await new Promise(resolve => {
|
|
1660
|
+
rl.question(` Enter choice ${color.dim('[1]')}: `, ans => {
|
|
1661
|
+
rl.close();
|
|
1662
|
+
resolve(ans.trim());
|
|
1663
|
+
});
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
const choice = answer === '' ? 1 : parseInt(answer, 10);
|
|
1667
|
+
if (isNaN(choice) || choice < 1 || choice > TEMPLATE_ORDER.length) {
|
|
1668
|
+
console.error(color.red(`\n Invalid choice. Please enter a number 1-${TEMPLATE_ORDER.length}.`));
|
|
1669
|
+
process.exit(1);
|
|
1670
|
+
}
|
|
1671
|
+
templateName = TEMPLATE_ORDER[choice - 1];
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
const template = PROJECT_TEMPLATES[templateName];
|
|
1675
|
+
console.log(`\n ${color.bold('Creating new Tova project:')} ${color.cyan(name)} ${color.dim(`(${template.label})`)}\n`);
|
|
1676
|
+
|
|
1677
|
+
// Create directories
|
|
1423
1678
|
mkdirSync(projectDir, { recursive: true });
|
|
1424
1679
|
mkdirSync(join(projectDir, 'src'));
|
|
1425
1680
|
|
|
1681
|
+
const createdFiles = [];
|
|
1682
|
+
|
|
1426
1683
|
// tova.toml
|
|
1427
|
-
const
|
|
1684
|
+
const tomlConfig = {
|
|
1428
1685
|
project: {
|
|
1429
1686
|
name,
|
|
1430
1687
|
version: '0.1.0',
|
|
1431
|
-
description:
|
|
1432
|
-
entry: 'src',
|
|
1688
|
+
description: template.tomlDescription,
|
|
1433
1689
|
},
|
|
1434
1690
|
build: {
|
|
1435
1691
|
output: '.tova-out',
|
|
1436
1692
|
},
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1693
|
+
};
|
|
1694
|
+
if (!template.noEntry) {
|
|
1695
|
+
tomlConfig.project.entry = template.entry;
|
|
1696
|
+
}
|
|
1697
|
+
if (templateName === 'fullstack' || templateName === 'api') {
|
|
1698
|
+
tomlConfig.dev = { port: 3000 };
|
|
1699
|
+
tomlConfig.npm = {};
|
|
1700
|
+
}
|
|
1701
|
+
writeFileSync(join(projectDir, 'tova.toml'), stringifyTOML(tomlConfig));
|
|
1702
|
+
createdFiles.push('tova.toml');
|
|
1444
1703
|
|
|
1445
1704
|
// .gitignore
|
|
1446
1705
|
writeFileSync(join(projectDir, '.gitignore'), `node_modules/
|
|
@@ -1451,75 +1710,43 @@ bun.lock
|
|
|
1451
1710
|
*.db-shm
|
|
1452
1711
|
*.db-wal
|
|
1453
1712
|
`);
|
|
1713
|
+
createdFiles.push('.gitignore');
|
|
1454
1714
|
|
|
1455
|
-
//
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
type Message {
|
|
1460
|
-
text: String
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
server {
|
|
1465
|
-
fn get_message() -> Message {
|
|
1466
|
-
Message("Hello from Tova! 🌟")
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
route GET "/api/message" => get_message
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
client {
|
|
1473
|
-
state message = ""
|
|
1474
|
-
|
|
1475
|
-
effect {
|
|
1476
|
-
result = server.get_message()
|
|
1477
|
-
message = result.text
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
component App {
|
|
1481
|
-
<div class="app">
|
|
1482
|
-
<h1>"{message}"</h1>
|
|
1483
|
-
<p>"Edit src/app.tova to get started."</p>
|
|
1484
|
-
</div>
|
|
1715
|
+
// Template source file
|
|
1716
|
+
if (template.file && template.content) {
|
|
1717
|
+
writeFileSync(join(projectDir, template.file), template.content(name));
|
|
1718
|
+
createdFiles.push(template.file);
|
|
1485
1719
|
}
|
|
1486
|
-
}
|
|
1487
|
-
`);
|
|
1488
1720
|
|
|
1489
1721
|
// README
|
|
1490
1722
|
writeFileSync(join(projectDir, 'README.md'), `# ${name}
|
|
1491
1723
|
|
|
1492
1724
|
Built with [Tova](https://github.com/tova-lang/tova-lang) — a modern full-stack language.
|
|
1493
1725
|
|
|
1494
|
-
##
|
|
1495
|
-
|
|
1496
|
-
\`\`\`bash
|
|
1497
|
-
tova install
|
|
1498
|
-
tova dev
|
|
1499
|
-
\`\`\`
|
|
1500
|
-
|
|
1501
|
-
## Build
|
|
1726
|
+
## Getting started
|
|
1502
1727
|
|
|
1503
1728
|
\`\`\`bash
|
|
1504
|
-
|
|
1729
|
+
${template.nextSteps(name).trim()}
|
|
1505
1730
|
\`\`\`
|
|
1731
|
+
`);
|
|
1732
|
+
createdFiles.push('README.md');
|
|
1506
1733
|
|
|
1507
|
-
|
|
1734
|
+
// Print created files
|
|
1735
|
+
for (const f of createdFiles) {
|
|
1736
|
+
console.log(` ${color.green('✓')} Created ${color.bold(name + '/' + f)}`);
|
|
1737
|
+
}
|
|
1508
1738
|
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
`);
|
|
1739
|
+
// git init (silent, only if git is available)
|
|
1740
|
+
try {
|
|
1741
|
+
const gitProc = Bun.spawnSync(['git', 'init'], { cwd: projectDir, stdout: 'pipe', stderr: 'pipe' });
|
|
1742
|
+
if (gitProc.exitCode === 0) {
|
|
1743
|
+
console.log(` ${color.green('✓')} Initialized git repository`);
|
|
1744
|
+
}
|
|
1745
|
+
} catch {}
|
|
1514
1746
|
|
|
1515
|
-
console.log(
|
|
1516
|
-
console.log(
|
|
1517
|
-
console.log(
|
|
1518
|
-
console.log(` ✓ Created ${name}/README.md`);
|
|
1519
|
-
console.log(`\n Get started:\n`);
|
|
1520
|
-
console.log(` cd ${name}`);
|
|
1521
|
-
console.log(` tova install`);
|
|
1522
|
-
console.log(` tova dev\n`);
|
|
1747
|
+
console.log(`\n ${color.green('Done!')} Next steps:\n`);
|
|
1748
|
+
console.log(color.cyan(template.nextSteps(name)));
|
|
1749
|
+
console.log('');
|
|
1523
1750
|
}
|
|
1524
1751
|
|
|
1525
1752
|
// ─── Init (in-place) ────────────────────────────────────────
|
|
@@ -3843,58 +4070,450 @@ function findFiles(dir, ext) {
|
|
|
3843
4070
|
return results;
|
|
3844
4071
|
}
|
|
3845
4072
|
|
|
4073
|
+
// ─── Doctor Command ──────────────────────────────────────────
|
|
4074
|
+
|
|
4075
|
+
async function doctorCommand() {
|
|
4076
|
+
console.log(`\n ${color.bold('Tova Doctor')}\n`);
|
|
4077
|
+
|
|
4078
|
+
let allPassed = true;
|
|
4079
|
+
let hasWarning = false;
|
|
4080
|
+
|
|
4081
|
+
function pass(label, detail) {
|
|
4082
|
+
console.log(` ${color.green('✓')} ${label.padEnd(22)} ${color.dim(detail)}`);
|
|
4083
|
+
}
|
|
4084
|
+
function warn(label, detail) {
|
|
4085
|
+
console.log(` ${color.yellow('⚠')} ${label.padEnd(22)} ${color.yellow(detail)}`);
|
|
4086
|
+
hasWarning = true;
|
|
4087
|
+
}
|
|
4088
|
+
function fail(label, detail) {
|
|
4089
|
+
console.log(` ${color.red('✗')} ${label.padEnd(22)} ${color.red(detail)}`);
|
|
4090
|
+
allPassed = false;
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
// 1. Tova version & location
|
|
4094
|
+
const execPath = process.execPath || process.argv[0];
|
|
4095
|
+
pass(`Tova v${VERSION}`, execPath);
|
|
4096
|
+
|
|
4097
|
+
// 2. Bun availability
|
|
4098
|
+
try {
|
|
4099
|
+
const bunProc = Bun.spawnSync(['bun', '--version']);
|
|
4100
|
+
const bunVer = bunProc.stdout.toString().trim();
|
|
4101
|
+
if (bunProc.exitCode === 0 && bunVer) {
|
|
4102
|
+
const major = parseInt(bunVer.split('.')[0], 10);
|
|
4103
|
+
if (major >= 1) {
|
|
4104
|
+
pass(`Bun v${bunVer}`, Bun.spawnSync(['which', 'bun']).stdout.toString().trim());
|
|
4105
|
+
} else {
|
|
4106
|
+
warn(`Bun v${bunVer}`, 'Bun >= 1.0 recommended');
|
|
4107
|
+
}
|
|
4108
|
+
} else {
|
|
4109
|
+
fail('Bun', 'not found — install from https://bun.sh');
|
|
4110
|
+
}
|
|
4111
|
+
} catch {
|
|
4112
|
+
fail('Bun', 'not found — install from https://bun.sh');
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4115
|
+
// 3. PATH configured
|
|
4116
|
+
const tovaDir = join(process.env.HOME || '', '.tova', 'bin');
|
|
4117
|
+
if ((process.env.PATH || '').includes(tovaDir)) {
|
|
4118
|
+
pass('PATH configured', `${tovaDir} in $PATH`);
|
|
4119
|
+
} else if (execPath.includes('.tova/bin')) {
|
|
4120
|
+
warn('PATH configured', `${tovaDir} not in $PATH`);
|
|
4121
|
+
} else {
|
|
4122
|
+
pass('PATH configured', 'installed via npm/bun');
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
// 4. Shell profile
|
|
4126
|
+
const shellName = basename(process.env.SHELL || '');
|
|
4127
|
+
let profilePath = '';
|
|
4128
|
+
if (shellName === 'zsh') profilePath = join(process.env.HOME || '', '.zshrc');
|
|
4129
|
+
else if (shellName === 'bash') profilePath = join(process.env.HOME || '', '.bashrc');
|
|
4130
|
+
else if (shellName === 'fish') profilePath = join(process.env.HOME || '', '.config', 'fish', 'conf.d', 'tova.fish');
|
|
4131
|
+
else profilePath = join(process.env.HOME || '', '.profile');
|
|
4132
|
+
|
|
4133
|
+
if (profilePath && existsSync(profilePath)) {
|
|
4134
|
+
try {
|
|
4135
|
+
const profileContent = readFileSync(profilePath, 'utf-8');
|
|
4136
|
+
if (profileContent.includes('.tova/bin') || !execPath.includes('.tova/bin')) {
|
|
4137
|
+
pass('Shell profile', profilePath);
|
|
4138
|
+
} else {
|
|
4139
|
+
warn('Shell profile', `${profilePath} missing Tova PATH entry`);
|
|
4140
|
+
}
|
|
4141
|
+
} catch {
|
|
4142
|
+
warn('Shell profile', `could not read ${profilePath}`);
|
|
4143
|
+
}
|
|
4144
|
+
} else if (!execPath.includes('.tova/bin')) {
|
|
4145
|
+
pass('Shell profile', 'installed via npm/bun');
|
|
4146
|
+
} else {
|
|
4147
|
+
warn('Shell profile', `${profilePath} not found`);
|
|
4148
|
+
}
|
|
4149
|
+
|
|
4150
|
+
// 5. git
|
|
4151
|
+
try {
|
|
4152
|
+
const gitProc = Bun.spawnSync(['git', '--version']);
|
|
4153
|
+
if (gitProc.exitCode === 0) {
|
|
4154
|
+
const gitVer = gitProc.stdout.toString().trim();
|
|
4155
|
+
pass('git available', gitVer);
|
|
4156
|
+
} else {
|
|
4157
|
+
warn('git', 'not found');
|
|
4158
|
+
}
|
|
4159
|
+
} catch {
|
|
4160
|
+
warn('git', 'not found');
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
// 6. tova.toml
|
|
4164
|
+
const tomlPath = resolve('tova.toml');
|
|
4165
|
+
if (existsSync(tomlPath)) {
|
|
4166
|
+
pass('tova.toml', 'found in current directory');
|
|
4167
|
+
} else {
|
|
4168
|
+
warn('No tova.toml', 'not in a Tova project');
|
|
4169
|
+
}
|
|
4170
|
+
|
|
4171
|
+
// 7. Build output
|
|
4172
|
+
const config = resolveConfig(process.cwd());
|
|
4173
|
+
const outDir = resolve(config.build?.output || '.tova-out');
|
|
4174
|
+
if (existsSync(outDir)) {
|
|
4175
|
+
try {
|
|
4176
|
+
const testFile = join(outDir, '.doctor-check');
|
|
4177
|
+
writeFileSync(testFile, '');
|
|
4178
|
+
rmSync(testFile);
|
|
4179
|
+
pass('Build output', `${relative('.', outDir)}/ exists and writable`);
|
|
4180
|
+
} catch {
|
|
4181
|
+
warn('Build output', `${relative('.', outDir)}/ exists but not writable`);
|
|
4182
|
+
}
|
|
4183
|
+
} else if (existsSync(tomlPath)) {
|
|
4184
|
+
warn('Build output', 'not built yet — run tova build');
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4187
|
+
// Summary
|
|
4188
|
+
console.log('');
|
|
4189
|
+
if (allPassed && !hasWarning) {
|
|
4190
|
+
console.log(` ${color.green('All checks passed.')}\n`);
|
|
4191
|
+
} else if (allPassed) {
|
|
4192
|
+
console.log(` ${color.yellow('All checks passed with warnings.')}\n`);
|
|
4193
|
+
} else {
|
|
4194
|
+
console.log(` ${color.red('Some checks failed.')}\n`);
|
|
4195
|
+
process.exit(1);
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
// ─── Completions Command ─────────────────────────────────────
|
|
4200
|
+
|
|
4201
|
+
function completionsCommand(shell) {
|
|
4202
|
+
if (!shell) {
|
|
4203
|
+
console.error('Usage: tova completions <bash|zsh|fish>');
|
|
4204
|
+
process.exit(1);
|
|
4205
|
+
}
|
|
4206
|
+
|
|
4207
|
+
const commands = [
|
|
4208
|
+
'run', 'build', 'check', 'clean', 'dev', 'new', 'install', 'add', 'remove',
|
|
4209
|
+
'repl', 'lsp', 'fmt', 'test', 'bench', 'doc', 'init', 'upgrade', 'info',
|
|
4210
|
+
'explain', 'doctor', 'completions',
|
|
4211
|
+
'migrate:create', 'migrate:up', 'migrate:down', 'migrate:reset', 'migrate:fresh', 'migrate:status',
|
|
4212
|
+
];
|
|
4213
|
+
|
|
4214
|
+
const globalFlags = ['--help', '--version', '--output', '--production', '--watch', '--verbose', '--quiet', '--debug', '--strict'];
|
|
4215
|
+
|
|
4216
|
+
switch (shell) {
|
|
4217
|
+
case 'bash': {
|
|
4218
|
+
const script = `# tova bash completions
|
|
4219
|
+
# Add to ~/.bashrc: eval "$(tova completions bash)"
|
|
4220
|
+
_tova() {
|
|
4221
|
+
local cur prev commands
|
|
4222
|
+
COMPREPLY=()
|
|
4223
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
4224
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
4225
|
+
commands="${commands.join(' ')}"
|
|
4226
|
+
|
|
4227
|
+
case "\${prev}" in
|
|
4228
|
+
tova)
|
|
4229
|
+
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
|
|
4230
|
+
return 0
|
|
4231
|
+
;;
|
|
4232
|
+
new)
|
|
4233
|
+
COMPREPLY=( $(compgen -W "--template" -- "\${cur}") )
|
|
4234
|
+
return 0
|
|
4235
|
+
;;
|
|
4236
|
+
--template)
|
|
4237
|
+
COMPREPLY=( $(compgen -W "fullstack api script library blank" -- "\${cur}") )
|
|
4238
|
+
return 0
|
|
4239
|
+
;;
|
|
4240
|
+
completions)
|
|
4241
|
+
COMPREPLY=( $(compgen -W "bash zsh fish" -- "\${cur}") )
|
|
4242
|
+
return 0
|
|
4243
|
+
;;
|
|
4244
|
+
run|build|check|fmt|doc)
|
|
4245
|
+
COMPREPLY=( $(compgen -f -X '!*.tova' -- "\${cur}") $(compgen -d -- "\${cur}") )
|
|
4246
|
+
return 0
|
|
4247
|
+
;;
|
|
4248
|
+
esac
|
|
4249
|
+
|
|
4250
|
+
if [[ "\${cur}" == -* ]]; then
|
|
4251
|
+
COMPREPLY=( $(compgen -W "${globalFlags.join(' ')} --filter --coverage --serial --check --template --dev --binary --no-cache" -- "\${cur}") )
|
|
4252
|
+
return 0
|
|
4253
|
+
fi
|
|
4254
|
+
}
|
|
4255
|
+
complete -F _tova tova
|
|
4256
|
+
`;
|
|
4257
|
+
console.log(script);
|
|
4258
|
+
console.error(`${color.dim('# Add to your ~/.bashrc:')}`);
|
|
4259
|
+
console.error(`${color.dim('# eval "$(tova completions bash)"')}`);
|
|
4260
|
+
break;
|
|
4261
|
+
}
|
|
4262
|
+
case 'zsh': {
|
|
4263
|
+
const script = `#compdef tova
|
|
4264
|
+
# tova zsh completions
|
|
4265
|
+
# Add to ~/.zshrc: eval "$(tova completions zsh)"
|
|
4266
|
+
|
|
4267
|
+
_tova() {
|
|
4268
|
+
local -a commands
|
|
4269
|
+
commands=(
|
|
4270
|
+
${commands.map(c => ` '${c}:${c} command'`).join('\n')}
|
|
4271
|
+
)
|
|
4272
|
+
|
|
4273
|
+
_arguments -C \\
|
|
4274
|
+
'1:command:->cmd' \\
|
|
4275
|
+
'*::arg:->args'
|
|
4276
|
+
|
|
4277
|
+
case $state in
|
|
4278
|
+
cmd)
|
|
4279
|
+
_describe -t commands 'tova command' commands
|
|
4280
|
+
;;
|
|
4281
|
+
args)
|
|
4282
|
+
case $words[1] in
|
|
4283
|
+
new)
|
|
4284
|
+
_arguments \\
|
|
4285
|
+
'--template[Project template]:template:(fullstack api script library blank)' \\
|
|
4286
|
+
'*:name:'
|
|
4287
|
+
;;
|
|
4288
|
+
run|build|check|fmt|doc)
|
|
4289
|
+
_files -g '*.tova'
|
|
4290
|
+
;;
|
|
4291
|
+
test|bench)
|
|
4292
|
+
_arguments \\
|
|
4293
|
+
'--filter[Filter pattern]:pattern:' \\
|
|
4294
|
+
'--watch[Watch mode]' \\
|
|
4295
|
+
'--coverage[Enable coverage]' \\
|
|
4296
|
+
'--serial[Sequential execution]'
|
|
4297
|
+
;;
|
|
4298
|
+
completions)
|
|
4299
|
+
_values 'shell' bash zsh fish
|
|
4300
|
+
;;
|
|
4301
|
+
explain)
|
|
4302
|
+
_message 'error code (e.g., E202)'
|
|
4303
|
+
;;
|
|
4304
|
+
*)
|
|
4305
|
+
_arguments \\
|
|
4306
|
+
'--help[Show help]' \\
|
|
4307
|
+
'--version[Show version]' \\
|
|
4308
|
+
'--output[Output directory]:dir:_dirs' \\
|
|
4309
|
+
'--production[Production build]' \\
|
|
4310
|
+
'--watch[Watch mode]' \\
|
|
4311
|
+
'--verbose[Verbose output]' \\
|
|
4312
|
+
'--quiet[Quiet mode]' \\
|
|
4313
|
+
'--debug[Debug output]' \\
|
|
4314
|
+
'--strict[Strict type checking]'
|
|
4315
|
+
;;
|
|
4316
|
+
esac
|
|
4317
|
+
;;
|
|
4318
|
+
esac
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4321
|
+
_tova "$@"
|
|
4322
|
+
`;
|
|
4323
|
+
console.log(script);
|
|
4324
|
+
console.error(`${color.dim('# Add to your ~/.zshrc:')}`);
|
|
4325
|
+
console.error(`${color.dim('# eval "$(tova completions zsh)"')}`);
|
|
4326
|
+
break;
|
|
4327
|
+
}
|
|
4328
|
+
case 'fish': {
|
|
4329
|
+
const descriptions = {
|
|
4330
|
+
run: 'Compile and execute a .tova file',
|
|
4331
|
+
build: 'Compile .tova files to JavaScript',
|
|
4332
|
+
check: 'Type-check without generating code',
|
|
4333
|
+
clean: 'Delete build artifacts',
|
|
4334
|
+
dev: 'Start development server',
|
|
4335
|
+
new: 'Create a new Tova project',
|
|
4336
|
+
install: 'Install npm dependencies',
|
|
4337
|
+
add: 'Add an npm dependency',
|
|
4338
|
+
remove: 'Remove an npm dependency',
|
|
4339
|
+
repl: 'Start interactive REPL',
|
|
4340
|
+
lsp: 'Start Language Server Protocol server',
|
|
4341
|
+
fmt: 'Format .tova files',
|
|
4342
|
+
test: 'Run test blocks',
|
|
4343
|
+
bench: 'Run bench blocks',
|
|
4344
|
+
doc: 'Generate documentation',
|
|
4345
|
+
init: 'Initialize project in current directory',
|
|
4346
|
+
upgrade: 'Upgrade Tova to latest version',
|
|
4347
|
+
info: 'Show version and project info',
|
|
4348
|
+
explain: 'Explain an error code',
|
|
4349
|
+
doctor: 'Check development environment',
|
|
4350
|
+
completions: 'Generate shell completions',
|
|
4351
|
+
'migrate:create': 'Create a migration file',
|
|
4352
|
+
'migrate:up': 'Run pending migrations',
|
|
4353
|
+
'migrate:down': 'Roll back last migration',
|
|
4354
|
+
'migrate:reset': 'Roll back all migrations',
|
|
4355
|
+
'migrate:fresh': 'Drop tables and re-migrate',
|
|
4356
|
+
'migrate:status': 'Show migration status',
|
|
4357
|
+
};
|
|
4358
|
+
|
|
4359
|
+
let script = `# tova fish completions
|
|
4360
|
+
# Save to: tova completions fish > ~/.config/fish/completions/tova.fish
|
|
4361
|
+
|
|
4362
|
+
`;
|
|
4363
|
+
for (const [cmd, desc] of Object.entries(descriptions)) {
|
|
4364
|
+
script += `complete -c tova -n '__fish_use_subcommand' -a '${cmd}' -d '${desc}'\n`;
|
|
4365
|
+
}
|
|
4366
|
+
script += `\n# Flags\n`;
|
|
4367
|
+
script += `complete -c tova -l help -s h -d 'Show help'\n`;
|
|
4368
|
+
script += `complete -c tova -l version -s v -d 'Show version'\n`;
|
|
4369
|
+
script += `complete -c tova -l output -s o -d 'Output directory'\n`;
|
|
4370
|
+
script += `complete -c tova -l production -d 'Production build'\n`;
|
|
4371
|
+
script += `complete -c tova -l watch -d 'Watch mode'\n`;
|
|
4372
|
+
script += `complete -c tova -l verbose -d 'Verbose output'\n`;
|
|
4373
|
+
script += `complete -c tova -l quiet -d 'Quiet mode'\n`;
|
|
4374
|
+
script += `complete -c tova -l debug -d 'Debug output'\n`;
|
|
4375
|
+
script += `complete -c tova -l strict -d 'Strict type checking'\n`;
|
|
4376
|
+
script += `\n# Template completions for 'new'\n`;
|
|
4377
|
+
script += `complete -c tova -n '__fish_seen_subcommand_from new' -l template -d 'Project template' -xa 'fullstack api script library blank'\n`;
|
|
4378
|
+
script += `\n# Shell completions for 'completions'\n`;
|
|
4379
|
+
script += `complete -c tova -n '__fish_seen_subcommand_from completions' -xa 'bash zsh fish'\n`;
|
|
4380
|
+
|
|
4381
|
+
console.log(script);
|
|
4382
|
+
console.error(`${color.dim('# Save to:')}`);
|
|
4383
|
+
console.error(`${color.dim('# tova completions fish > ~/.config/fish/completions/tova.fish')}`);
|
|
4384
|
+
break;
|
|
4385
|
+
}
|
|
4386
|
+
default:
|
|
4387
|
+
console.error(`Unknown shell: ${shell}. Supported: bash, zsh, fish`);
|
|
4388
|
+
process.exit(1);
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
|
|
3846
4392
|
// ─── Upgrade Command ─────────────────────────────────────────
|
|
3847
4393
|
|
|
4394
|
+
function detectInstallMethod() {
|
|
4395
|
+
const execPath = process.execPath || process.argv[0];
|
|
4396
|
+
if (execPath.includes('.tova/bin')) return 'binary';
|
|
4397
|
+
return 'npm';
|
|
4398
|
+
}
|
|
4399
|
+
|
|
3848
4400
|
async function upgradeCommand() {
|
|
3849
|
-
console.log(`\n Current version: Tova v
|
|
4401
|
+
console.log(`\n Current version: ${color.bold('Tova v' + VERSION)}\n`);
|
|
3850
4402
|
console.log(' Checking for updates...');
|
|
3851
4403
|
|
|
4404
|
+
const installMethod = detectInstallMethod();
|
|
4405
|
+
|
|
3852
4406
|
try {
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
4407
|
+
if (installMethod === 'binary') {
|
|
4408
|
+
// Binary install: check GitHub releases
|
|
4409
|
+
const res = await fetch('https://api.github.com/repos/tova-lang/tova-lang/releases/latest');
|
|
4410
|
+
if (!res.ok) {
|
|
4411
|
+
console.error(color.red(' Could not reach GitHub. Check your network connection.'));
|
|
4412
|
+
process.exit(1);
|
|
4413
|
+
}
|
|
4414
|
+
const data = await res.json();
|
|
4415
|
+
const latestVersion = (data.tag_name || '').replace(/^v/, '');
|
|
3861
4416
|
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
4417
|
+
if (latestVersion === VERSION) {
|
|
4418
|
+
console.log(` ${color.green('Already on the latest version')} (v${VERSION}).\n`);
|
|
4419
|
+
return;
|
|
4420
|
+
}
|
|
3866
4421
|
|
|
3867
|
-
|
|
3868
|
-
|
|
4422
|
+
console.log(` New version available: ${color.green('v' + latestVersion)}\n`);
|
|
4423
|
+
console.log(' Upgrading via binary...');
|
|
4424
|
+
|
|
4425
|
+
// Detect platform
|
|
4426
|
+
const platform = process.platform === 'darwin' ? 'darwin' : process.platform === 'linux' ? 'linux' : 'windows';
|
|
4427
|
+
const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
|
|
4428
|
+
const assetName = `tova-${platform}-${arch}`;
|
|
4429
|
+
const downloadUrl = `https://github.com/tova-lang/tova-lang/releases/download/${data.tag_name}/${assetName}.gz`;
|
|
4430
|
+
|
|
4431
|
+
const installDir = join(process.env.HOME || '', '.tova', 'bin');
|
|
4432
|
+
const tmpPath = join(installDir, 'tova.download');
|
|
4433
|
+
const binPath = join(installDir, 'tova');
|
|
4434
|
+
|
|
4435
|
+
// Download compressed binary
|
|
4436
|
+
const dlRes = await fetch(downloadUrl);
|
|
4437
|
+
if (!dlRes.ok) {
|
|
4438
|
+
// Fall back to uncompressed
|
|
4439
|
+
const dlRes2 = await fetch(downloadUrl.replace('.gz', ''));
|
|
4440
|
+
if (!dlRes2.ok) {
|
|
4441
|
+
console.error(color.red(` Download failed. URL: ${downloadUrl.replace('.gz', '')}`));
|
|
4442
|
+
process.exit(1);
|
|
4443
|
+
}
|
|
4444
|
+
writeFileSync(tmpPath, new Uint8Array(await dlRes2.arrayBuffer()));
|
|
4445
|
+
} else {
|
|
4446
|
+
// Decompress gzip
|
|
4447
|
+
const compressed = new Uint8Array(await dlRes.arrayBuffer());
|
|
4448
|
+
const decompressed = Bun.gunzipSync(compressed);
|
|
4449
|
+
writeFileSync(tmpPath, decompressed);
|
|
4450
|
+
}
|
|
3869
4451
|
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
4452
|
+
// Make executable
|
|
4453
|
+
const { chmodSync } = await import('fs');
|
|
4454
|
+
chmodSync(tmpPath, 0o755);
|
|
4455
|
+
|
|
4456
|
+
// Verify the new binary works
|
|
4457
|
+
const verifyProc = Bun.spawnSync([tmpPath, '--version'], { stdout: 'pipe', stderr: 'pipe' });
|
|
4458
|
+
if (verifyProc.exitCode !== 0) {
|
|
4459
|
+
rmSync(tmpPath, { force: true });
|
|
4460
|
+
console.error(color.red(' Downloaded binary verification failed.'));
|
|
4461
|
+
process.exit(1);
|
|
4462
|
+
}
|
|
3876
4463
|
|
|
3877
|
-
|
|
3878
|
-
|
|
4464
|
+
// Atomic rename
|
|
4465
|
+
const { renameSync } = await import('fs');
|
|
4466
|
+
renameSync(tmpPath, binPath);
|
|
3879
4467
|
|
|
3880
|
-
|
|
3881
|
-
console.log(`\n Upgraded to Tova v${latestVersion}.\n`);
|
|
4468
|
+
console.log(`\n ${color.green('✓')} Upgraded: v${VERSION} -> ${color.bold('v' + latestVersion)}\n`);
|
|
3882
4469
|
} else {
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
4470
|
+
// npm/bun install: check npm registry
|
|
4471
|
+
const res = await fetch('https://registry.npmjs.org/tova/latest');
|
|
4472
|
+
if (!res.ok) {
|
|
4473
|
+
console.error(color.red(' Could not reach the npm registry. Check your network connection.'));
|
|
4474
|
+
process.exit(1);
|
|
4475
|
+
}
|
|
4476
|
+
const data = await res.json();
|
|
4477
|
+
const latestVersion = data.version;
|
|
4478
|
+
|
|
4479
|
+
if (latestVersion === VERSION) {
|
|
4480
|
+
console.log(` ${color.green('Already on the latest version')} (v${VERSION}).\n`);
|
|
4481
|
+
return;
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
console.log(` New version available: ${color.green('v' + latestVersion)}\n`);
|
|
4485
|
+
console.log(' Upgrading...');
|
|
4486
|
+
|
|
4487
|
+
const pm = detectPackageManager();
|
|
4488
|
+
const installCmd = pm === 'bun' ? ['bun', ['add', '-g', 'tova@latest']]
|
|
4489
|
+
: pm === 'pnpm' ? ['pnpm', ['add', '-g', 'tova@latest']]
|
|
4490
|
+
: pm === 'yarn' ? ['yarn', ['global', 'add', 'tova@latest']]
|
|
4491
|
+
: ['npm', ['install', '-g', 'tova@latest']];
|
|
4492
|
+
|
|
4493
|
+
const proc = spawn(installCmd[0], installCmd[1], { stdio: 'inherit' });
|
|
4494
|
+
const exitCode = await new Promise(res => proc.on('close', res));
|
|
4495
|
+
|
|
4496
|
+
if (exitCode === 0) {
|
|
4497
|
+
console.log(`\n ${color.green('✓')} Upgraded to Tova v${latestVersion}.\n`);
|
|
4498
|
+
} else {
|
|
4499
|
+
console.error(color.red(`\n Upgrade failed (exit code ${exitCode}).`));
|
|
4500
|
+
console.error(` Try manually: ${installCmd[0]} ${installCmd[1].join(' ')}\n`);
|
|
4501
|
+
process.exit(1);
|
|
4502
|
+
}
|
|
3886
4503
|
}
|
|
3887
4504
|
} catch (err) {
|
|
3888
|
-
console.error(` Upgrade failed: ${err.message}`);
|
|
3889
|
-
|
|
4505
|
+
console.error(color.red(` Upgrade failed: ${err.message}`));
|
|
4506
|
+
if (installMethod === 'binary') {
|
|
4507
|
+
console.error(' Try manually: curl -fsSL https://raw.githubusercontent.com/tova-lang/tova-lang/main/install.sh | sh\n');
|
|
4508
|
+
} else {
|
|
4509
|
+
console.error(' Try manually: bun add -g tova@latest\n');
|
|
4510
|
+
}
|
|
3890
4511
|
process.exit(1);
|
|
3891
4512
|
}
|
|
3892
4513
|
}
|
|
3893
4514
|
|
|
3894
4515
|
function detectPackageManager() {
|
|
3895
|
-
// Check if we're running under bun (most likely for Tova)
|
|
3896
4516
|
if (typeof Bun !== 'undefined') return 'bun';
|
|
3897
|
-
// Check npm_config_user_agent for the package manager
|
|
3898
4517
|
const ua = process.env.npm_config_user_agent || '';
|
|
3899
4518
|
if (ua.includes('pnpm')) return 'pnpm';
|
|
3900
4519
|
if (ua.includes('yarn')) return 'yarn';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tova",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
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,11 +36,6 @@
|
|
|
36
36
|
"url": "https://github.com/tova-lang/tova-lang/issues"
|
|
37
37
|
},
|
|
38
38
|
"author": "Enoch Kujem Abassey",
|
|
39
|
-
"keywords": [
|
|
40
|
-
"language",
|
|
41
|
-
"transpiler",
|
|
42
|
-
"fullstack",
|
|
43
|
-
"javascript"
|
|
44
|
-
],
|
|
39
|
+
"keywords": ["language", "transpiler", "fullstack", "javascript"],
|
|
45
40
|
"license": "MIT"
|
|
46
41
|
}
|
package/src/analyzer/analyzer.js
CHANGED
|
@@ -17,23 +17,62 @@ const ARITHMETIC_OPS = new Set(['-', '*', '/', '%', '**']);
|
|
|
17
17
|
const NUMERIC_TYPES = new Set(['Int', 'Float']);
|
|
18
18
|
|
|
19
19
|
const _JS_GLOBALS = new Set([
|
|
20
|
+
// Core globals
|
|
20
21
|
'console', 'document', 'window', 'globalThis', 'self',
|
|
21
22
|
'JSON', 'Math', 'Date', 'RegExp', 'Error', 'TypeError', 'RangeError',
|
|
22
|
-
'
|
|
23
|
+
'SyntaxError', 'ReferenceError', 'URIError', 'EvalError', 'AggregateError',
|
|
24
|
+
'Promise', 'Set', 'Map', 'WeakSet', 'WeakMap', 'WeakRef', 'Symbol',
|
|
23
25
|
'Array', 'Object', 'String', 'Number', 'Boolean', 'Function',
|
|
26
|
+
'BigInt', 'Proxy', 'Reflect',
|
|
24
27
|
'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'NaN', 'Infinity',
|
|
25
28
|
'undefined', 'null', 'true', 'false',
|
|
29
|
+
'encodeURI', 'decodeURI', 'encodeURIComponent', 'decodeURIComponent',
|
|
30
|
+
// Timers & scheduling
|
|
26
31
|
'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
|
|
32
|
+
'requestAnimationFrame', 'cancelAnimationFrame',
|
|
33
|
+
'requestIdleCallback', 'cancelIdleCallback',
|
|
27
34
|
'queueMicrotask', 'structuredClone',
|
|
35
|
+
// Fetch & network
|
|
28
36
|
'URL', 'URLSearchParams', 'Headers', 'Request', 'Response',
|
|
29
37
|
'FormData', 'Blob', 'File', 'FileReader',
|
|
30
38
|
'AbortController', 'AbortSignal',
|
|
39
|
+
'fetch', 'WebSocket', 'EventSource', 'XMLHttpRequest',
|
|
40
|
+
// Encoding
|
|
31
41
|
'TextEncoder', 'TextDecoder',
|
|
32
|
-
'
|
|
42
|
+
'atob', 'btoa', 'Buffer',
|
|
43
|
+
// Browser APIs
|
|
44
|
+
'crypto', 'performance', 'navigator', 'location', 'history', 'screen',
|
|
33
45
|
'localStorage', 'sessionStorage',
|
|
34
|
-
'
|
|
46
|
+
'alert', 'confirm', 'prompt',
|
|
47
|
+
'getComputedStyle', 'matchMedia', 'getSelection',
|
|
48
|
+
'scrollTo', 'scrollBy', 'scrollX', 'scrollY',
|
|
49
|
+
'innerWidth', 'innerHeight', 'outerWidth', 'outerHeight',
|
|
50
|
+
'devicePixelRatio',
|
|
51
|
+
// DOM & Events
|
|
52
|
+
'Event', 'CustomEvent', 'ErrorEvent',
|
|
53
|
+
'MouseEvent', 'KeyboardEvent', 'FocusEvent', 'InputEvent',
|
|
54
|
+
'TouchEvent', 'PointerEvent', 'WheelEvent', 'DragEvent',
|
|
55
|
+
'ClipboardEvent', 'AnimationEvent', 'TransitionEvent',
|
|
56
|
+
'HTMLElement', 'Element', 'Node', 'NodeList', 'DocumentFragment',
|
|
57
|
+
'DOMParser', 'MutationObserver', 'IntersectionObserver', 'ResizeObserver',
|
|
58
|
+
'Image', 'Audio',
|
|
59
|
+
// Workers & channels
|
|
60
|
+
'Worker', 'SharedWorker', 'BroadcastChannel', 'MessageChannel', 'MessagePort',
|
|
61
|
+
// Media & graphics
|
|
62
|
+
'AudioContext', 'OfflineAudioContext',
|
|
63
|
+
'CanvasRenderingContext2D', 'WebGLRenderingContext',
|
|
64
|
+
'MediaRecorder', 'MediaStream', 'MediaSource',
|
|
65
|
+
// Notifications & clipboard
|
|
66
|
+
'Notification', 'ClipboardItem',
|
|
67
|
+
// Typed arrays
|
|
68
|
+
'ArrayBuffer', 'SharedArrayBuffer', 'DataView',
|
|
69
|
+
'Int8Array', 'Uint8Array', 'Uint8ClampedArray',
|
|
70
|
+
'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array',
|
|
71
|
+
'Float32Array', 'Float64Array', 'BigInt64Array', 'BigUint64Array',
|
|
72
|
+
// Streams
|
|
73
|
+
'ReadableStream', 'WritableStream', 'TransformStream',
|
|
74
|
+
// Server / Node / Bun
|
|
35
75
|
'Bun', 'Deno', 'process', 'require', 'module', 'exports', '__dirname', '__filename',
|
|
36
|
-
'Buffer', 'atob', 'btoa',
|
|
37
76
|
]);
|
|
38
77
|
|
|
39
78
|
function levenshtein(a, b) {
|
|
@@ -66,6 +66,30 @@ export class BaseCodegen {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// Check if an AST expression references a given variable name
|
|
70
|
+
_exprReferencesName(node, name) {
|
|
71
|
+
if (!node) return false;
|
|
72
|
+
switch (node.type) {
|
|
73
|
+
case 'Identifier': return node.name === name;
|
|
74
|
+
case 'BinaryExpression':
|
|
75
|
+
case 'LogicalExpression':
|
|
76
|
+
return this._exprReferencesName(node.left, name) || this._exprReferencesName(node.right, name);
|
|
77
|
+
case 'UnaryExpression':
|
|
78
|
+
return this._exprReferencesName(node.operand, name);
|
|
79
|
+
case 'CallExpression':
|
|
80
|
+
return this._exprReferencesName(node.callee, name) || node.arguments.some(a => this._exprReferencesName(a, name));
|
|
81
|
+
case 'MemberExpression':
|
|
82
|
+
return this._exprReferencesName(node.object, name) || (node.computed && this._exprReferencesName(node.property, name));
|
|
83
|
+
case 'NumberLiteral':
|
|
84
|
+
case 'StringLiteral':
|
|
85
|
+
case 'BooleanLiteral':
|
|
86
|
+
case 'NilLiteral':
|
|
87
|
+
return false;
|
|
88
|
+
default:
|
|
89
|
+
return true; // conservative: assume it references the name
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
69
93
|
// Known void/side-effect-only calls that shouldn't be implicitly returned
|
|
70
94
|
static VOID_FNS = new Set(['print', 'assert', 'assert_eq', 'assert_ne']);
|
|
71
95
|
_isVoidCall(expr) {
|
|
@@ -281,6 +305,10 @@ export class BaseCodegen {
|
|
|
281
305
|
|
|
282
306
|
switch (node.type) {
|
|
283
307
|
case 'Identifier':
|
|
308
|
+
// Parameter substitution for map chain fusion
|
|
309
|
+
if (this._paramSubstitutions && this._paramSubstitutions.has(node.name)) {
|
|
310
|
+
return this._paramSubstitutions.get(node.name);
|
|
311
|
+
}
|
|
284
312
|
// Track builtin identifier usage (e.g., None used without call)
|
|
285
313
|
if (BUILTIN_NAMES.has(node.name)) {
|
|
286
314
|
this._usedBuiltins.add(node.name);
|
|
@@ -941,7 +969,19 @@ export class BaseCodegen {
|
|
|
941
969
|
this.indent++;
|
|
942
970
|
}
|
|
943
971
|
|
|
972
|
+
// Pre-scan for array fill patterns in function bodies
|
|
973
|
+
const bodySkipSet = new Set();
|
|
974
|
+
for (let i = 0; i < regularStmts.length - 1; i++) {
|
|
975
|
+
const fillResult = this._detectArrayFillPattern(regularStmts[i], regularStmts[i + 1]);
|
|
976
|
+
if (fillResult) {
|
|
977
|
+
bodySkipSet.add(i);
|
|
978
|
+
bodySkipSet.add(i + 1);
|
|
979
|
+
lines.push(fillResult);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
944
983
|
for (let idx = 0; idx < regularStmts.length; idx++) {
|
|
984
|
+
if (bodySkipSet.has(idx)) continue;
|
|
945
985
|
const stmt = regularStmts[idx];
|
|
946
986
|
const isLast = idx === regularStmts.length - 1;
|
|
947
987
|
// Implicit return: last expression in function body
|
|
@@ -1079,7 +1119,19 @@ export class BaseCodegen {
|
|
|
1079
1119
|
if (!block) return '';
|
|
1080
1120
|
const stmts = block.type === 'BlockStatement' ? block.body : [block];
|
|
1081
1121
|
const lines = [];
|
|
1082
|
-
|
|
1122
|
+
const skipSet = new Set(); // indices to skip (consumed by pattern optimizations)
|
|
1123
|
+
// Pre-scan for array fill patterns: arr = []; for i in range(n) { arr.push(val) }
|
|
1124
|
+
for (let i = 0; i < stmts.length - 1; i++) {
|
|
1125
|
+
const fillResult = this._detectArrayFillPattern(stmts[i], stmts[i + 1]);
|
|
1126
|
+
if (fillResult) {
|
|
1127
|
+
skipSet.add(i);
|
|
1128
|
+
skipSet.add(i + 1);
|
|
1129
|
+
lines.push(fillResult);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
1133
|
+
if (skipSet.has(i)) continue;
|
|
1134
|
+
const s = stmts[i];
|
|
1083
1135
|
lines.push(this.generateStatement(s));
|
|
1084
1136
|
// Dead code elimination: stop after unconditional return/break/continue
|
|
1085
1137
|
if (s.type === 'ReturnStatement' || s.type === 'BreakStatement' || s.type === 'ContinueStatement') break;
|
|
@@ -1087,6 +1139,90 @@ export class BaseCodegen {
|
|
|
1087
1139
|
return lines.join('\n');
|
|
1088
1140
|
}
|
|
1089
1141
|
|
|
1142
|
+
// Detect pattern: arr = []; for i in range(n) { arr.push(val) }
|
|
1143
|
+
// Also handles: var arr = []; for i in range(n) { arr.push(val) }
|
|
1144
|
+
// Returns optimized code string or null
|
|
1145
|
+
_detectArrayFillPattern(assignStmt, forStmt) {
|
|
1146
|
+
// Step 1: Check first statement is `arr = []` or `var arr = []` (empty array)
|
|
1147
|
+
let target, isVar = false, exportPrefix = '';
|
|
1148
|
+
if (assignStmt.type === 'Assignment') {
|
|
1149
|
+
if (assignStmt.targets.length !== 1 || assignStmt.values.length !== 1) return null;
|
|
1150
|
+
target = assignStmt.targets[0];
|
|
1151
|
+
if (typeof target !== 'string') return null;
|
|
1152
|
+
const val = assignStmt.values[0];
|
|
1153
|
+
if (val.type !== 'ArrayLiteral' || val.elements.length !== 0) return null;
|
|
1154
|
+
exportPrefix = assignStmt.isPublic ? 'export ' : '';
|
|
1155
|
+
} else if (assignStmt.type === 'VarDeclaration') {
|
|
1156
|
+
if (assignStmt.targets.length !== 1 || assignStmt.values.length !== 1) return null;
|
|
1157
|
+
target = assignStmt.targets[0];
|
|
1158
|
+
if (typeof target !== 'string') return null;
|
|
1159
|
+
const val = assignStmt.values[0];
|
|
1160
|
+
if (val.type !== 'ArrayLiteral' || val.elements.length !== 0) return null;
|
|
1161
|
+
isVar = true;
|
|
1162
|
+
exportPrefix = assignStmt.isPublic ? 'export ' : '';
|
|
1163
|
+
} else {
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Step 2: Check second statement is `for _ in range(n) { arr.push(fillVal) }`
|
|
1168
|
+
if (forStmt.type !== 'ForStatement') return null;
|
|
1169
|
+
if (forStmt.elseBody || forStmt.guard || forStmt.isAsync) return null;
|
|
1170
|
+
if (!this._isRangeForOptimizable(forStmt)) return null;
|
|
1171
|
+
|
|
1172
|
+
// Get the range size
|
|
1173
|
+
let sizeExpr;
|
|
1174
|
+
if (forStmt.iterable.type === 'RangeExpression') {
|
|
1175
|
+
return null; // Only handle range(n) for now
|
|
1176
|
+
} else {
|
|
1177
|
+
const args = forStmt.iterable.arguments;
|
|
1178
|
+
if (args.length === 1) {
|
|
1179
|
+
sizeExpr = this.genExpression(args[0]);
|
|
1180
|
+
} else {
|
|
1181
|
+
return null; // range(start, end) / range(start, end, step) — not a simple fill
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Step 3: Check the body is a single `arr.push(fillVal)` statement
|
|
1186
|
+
const body = forStmt.body;
|
|
1187
|
+
const bodyStmts = body.type === 'BlockStatement' ? body.body : [body];
|
|
1188
|
+
if (bodyStmts.length !== 1) return null;
|
|
1189
|
+
const bodyStmt = bodyStmts[0];
|
|
1190
|
+
// It should be an ExpressionStatement wrapping a CallExpression
|
|
1191
|
+
let callExpr;
|
|
1192
|
+
if (bodyStmt.type === 'ExpressionStatement') {
|
|
1193
|
+
callExpr = bodyStmt.expression;
|
|
1194
|
+
} else if (bodyStmt.type === 'CallExpression') {
|
|
1195
|
+
callExpr = bodyStmt;
|
|
1196
|
+
} else {
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1199
|
+
if (callExpr.type !== 'CallExpression') return null;
|
|
1200
|
+
if (callExpr.callee.type !== 'MemberExpression') return null;
|
|
1201
|
+
if (callExpr.callee.property !== 'push') return null;
|
|
1202
|
+
if (callExpr.callee.object.type !== 'Identifier') return null;
|
|
1203
|
+
if (callExpr.callee.object.name !== target) return null;
|
|
1204
|
+
if (callExpr.arguments.length !== 1) return null;
|
|
1205
|
+
|
|
1206
|
+
const fillArg = callExpr.arguments[0];
|
|
1207
|
+
// Ensure the fill value doesn't reference the loop variable (must be a constant fill)
|
|
1208
|
+
const loopVar = Array.isArray(forStmt.variable) ? forStmt.variable[0] : forStmt.variable;
|
|
1209
|
+
if (this._exprReferencesName(fillArg, loopVar)) return null;
|
|
1210
|
+
const fillExpr = this.genExpression(fillArg);
|
|
1211
|
+
|
|
1212
|
+
// Declare the variable
|
|
1213
|
+
const isDeclared = this.isDeclared(target);
|
|
1214
|
+
if (!isDeclared) {
|
|
1215
|
+
this.declareVar(target);
|
|
1216
|
+
}
|
|
1217
|
+
const declKeyword = isVar ? `${exportPrefix}let ` : (isDeclared ? '' : `${exportPrefix}const `);
|
|
1218
|
+
// Boolean fills use Uint8Array for contiguous memory (3x faster for sieve-like patterns)
|
|
1219
|
+
if (fillArg.type === 'BooleanLiteral') {
|
|
1220
|
+
const fillVal = fillArg.value ? 1 : 0;
|
|
1221
|
+
return `${this.i()}${declKeyword}${target} = new Uint8Array(${sizeExpr})${fillVal ? '.fill(1)' : ''};`;
|
|
1222
|
+
}
|
|
1223
|
+
return `${this.i()}${declKeyword}${target} = new Array(${sizeExpr}).fill(${fillExpr});`;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1090
1226
|
// ─── Expressions ──────────────────────────────────────────
|
|
1091
1227
|
|
|
1092
1228
|
genTemplateLiteral(node) {
|
|
@@ -1292,6 +1428,10 @@ export class BaseCodegen {
|
|
|
1292
1428
|
}
|
|
1293
1429
|
|
|
1294
1430
|
genCallExpression(node) {
|
|
1431
|
+
// Result/Option .map() chain fusion: Ok(val).map(fn(x) e1).map(fn(x) e2) → Ok(e2(e1(val)))
|
|
1432
|
+
const fusedResult = this._tryFuseMapChain(node);
|
|
1433
|
+
if (fusedResult !== null) return fusedResult;
|
|
1434
|
+
|
|
1295
1435
|
// Transform Foo.new(...) → new Foo(...)
|
|
1296
1436
|
if (node.callee.type === 'MemberExpression' && !node.callee.computed && node.callee.property === 'new') {
|
|
1297
1437
|
const obj = this.genExpression(node.callee.object);
|
|
@@ -1367,6 +1507,81 @@ export class BaseCodegen {
|
|
|
1367
1507
|
return `${callee}(${args})`;
|
|
1368
1508
|
}
|
|
1369
1509
|
|
|
1510
|
+
// Fuse Ok(val).map(fn(x) e1).map(fn(x) e2) → Ok(composed(val))
|
|
1511
|
+
// Eliminates intermediate Ok/Some allocations in .map() chains
|
|
1512
|
+
_tryFuseMapChain(node) {
|
|
1513
|
+
// Must be a .map() call with exactly 1 argument
|
|
1514
|
+
if (node.callee.type !== 'MemberExpression' || node.callee.computed) return null;
|
|
1515
|
+
if (node.callee.property !== 'map') return null;
|
|
1516
|
+
if (node.arguments.length !== 1) return null;
|
|
1517
|
+
|
|
1518
|
+
// Collect the chain of .map() calls
|
|
1519
|
+
const mapFns = []; // array of lambda AST nodes, outermost first
|
|
1520
|
+
let current = node;
|
|
1521
|
+
while (
|
|
1522
|
+
current.type === 'CallExpression' &&
|
|
1523
|
+
current.callee.type === 'MemberExpression' &&
|
|
1524
|
+
!current.callee.computed &&
|
|
1525
|
+
current.callee.property === 'map' &&
|
|
1526
|
+
current.arguments.length === 1
|
|
1527
|
+
) {
|
|
1528
|
+
const lambda = current.arguments[0];
|
|
1529
|
+
// Only fuse simple single-expression lambdas with exactly 1 param
|
|
1530
|
+
if (lambda.type !== 'FunctionExpression' && lambda.type !== 'ArrowFunction' && lambda.type !== 'LambdaExpression') return null;
|
|
1531
|
+
const params = lambda.params || [];
|
|
1532
|
+
if (params.length !== 1) return null;
|
|
1533
|
+
const paramName = typeof params[0] === 'string' ? params[0] : (params[0].name || null);
|
|
1534
|
+
if (!paramName) return null;
|
|
1535
|
+
// Body must be a single expression (not a BlockStatement, or a block with 1 expression)
|
|
1536
|
+
let bodyExpr = lambda.body;
|
|
1537
|
+
if (bodyExpr && bodyExpr.type === 'BlockStatement') {
|
|
1538
|
+
if (bodyExpr.body.length === 1) {
|
|
1539
|
+
const s = bodyExpr.body[0];
|
|
1540
|
+
if (s.type === 'ExpressionStatement') bodyExpr = s.expression;
|
|
1541
|
+
else if (s.type === 'ReturnStatement' && s.value) bodyExpr = s.value;
|
|
1542
|
+
else return null;
|
|
1543
|
+
} else {
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
mapFns.unshift({ paramName, bodyExpr }); // prepend so inner is first
|
|
1548
|
+
current = current.callee.object;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// Need at least 2 .map() calls to benefit from fusion
|
|
1552
|
+
if (mapFns.length < 2) return null;
|
|
1553
|
+
|
|
1554
|
+
// The base must be Ok(val) or Some(val)
|
|
1555
|
+
if (current.type !== 'CallExpression') return null;
|
|
1556
|
+
if (current.callee.type !== 'Identifier') return null;
|
|
1557
|
+
const wrapperFn = current.callee.name;
|
|
1558
|
+
if (wrapperFn !== 'Ok' && wrapperFn !== 'Some') return null;
|
|
1559
|
+
if (current.arguments.length !== 1) return null;
|
|
1560
|
+
|
|
1561
|
+
this._needsResultOption = true;
|
|
1562
|
+
|
|
1563
|
+
// Compose the lambdas: val → f1(val) → f2(f1(val)) → ...
|
|
1564
|
+
// Generate inner-to-outer composition
|
|
1565
|
+
let innerExpr = this.genExpression(current.arguments[0]);
|
|
1566
|
+
for (const { paramName, bodyExpr } of mapFns) {
|
|
1567
|
+
// Substitute paramName with the current innerExpr in bodyExpr
|
|
1568
|
+
innerExpr = this._substituteParam(bodyExpr, paramName, innerExpr);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
return `${wrapperFn}(${innerExpr})`;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// Generate expression code with a parameter substituted by a value expression string
|
|
1575
|
+
_substituteParam(exprNode, paramName, valueCode) {
|
|
1576
|
+
// Simple approach: generate the expression, but override identifier resolution
|
|
1577
|
+
// We save and restore a substitution map
|
|
1578
|
+
if (!this._paramSubstitutions) this._paramSubstitutions = new Map();
|
|
1579
|
+
this._paramSubstitutions.set(paramName, valueCode);
|
|
1580
|
+
const result = this.genExpression(exprNode);
|
|
1581
|
+
this._paramSubstitutions.delete(paramName);
|
|
1582
|
+
return result;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1370
1585
|
// Inline known builtins to direct method calls, eliminating wrapper overhead.
|
|
1371
1586
|
// Returns the inlined code string, or null if not inlineable.
|
|
1372
1587
|
_tryInlineBuiltin(node) {
|
package/src/codegen/codegen.js
CHANGED
|
@@ -65,7 +65,9 @@ export class CodeGenerator {
|
|
|
65
65
|
const moduleGen = new SharedCodegen();
|
|
66
66
|
moduleGen._sourceMapsEnabled = this._sourceMaps;
|
|
67
67
|
moduleGen.setSourceFile(this.filename);
|
|
68
|
-
|
|
68
|
+
// Use genBlockStatements for pattern optimization (array fill detection, etc.)
|
|
69
|
+
const fakeBlock = { type: 'BlockStatement', body: topLevel };
|
|
70
|
+
const moduleCode = moduleGen.genBlockStatements(fakeBlock);
|
|
69
71
|
const helpers = moduleGen.generateHelpers();
|
|
70
72
|
const combined = [helpers, moduleCode].filter(s => s.trim()).join('\n').trim();
|
|
71
73
|
return {
|
|
@@ -84,7 +86,9 @@ export class CodeGenerator {
|
|
|
84
86
|
|
|
85
87
|
// All shared blocks (regardless of name) are merged into one shared output
|
|
86
88
|
const sharedCode = sharedBlocks.map(b => sharedGen.generate(b)).join('\n');
|
|
87
|
-
const topLevelCode = topLevel.
|
|
89
|
+
const topLevelCode = topLevel.length > 0
|
|
90
|
+
? sharedGen.genBlockStatements({ type: 'BlockStatement', body: topLevel })
|
|
91
|
+
: '';
|
|
88
92
|
|
|
89
93
|
// Pre-scan server/client blocks for builtin usage so shared stdlib includes them
|
|
90
94
|
this._scanBlocksForBuiltins([...serverBlocks, ...clientBlocks], sharedGen._usedBuiltins);
|
package/src/stdlib/inline.js
CHANGED
|
@@ -88,6 +88,7 @@ export const BUILTIN_FUNCTIONS = {
|
|
|
88
88
|
last: `function last(arr) { return arr.length > 0 ? arr[arr.length - 1] : null; }`,
|
|
89
89
|
count: `function count(arr, fn) { return arr.filter(fn).length; }`,
|
|
90
90
|
partition: `function partition(arr, fn) { const y = [], n = []; for (const v of arr) { (fn(v) ? y : n).push(v); } return [y, n]; }`,
|
|
91
|
+
filled: `function filled(n, val) { return new Array(n).fill(val); }`,
|
|
91
92
|
abs: `function abs(n) { return Math.abs(n); }`,
|
|
92
93
|
floor: `function floor(n) { return Math.floor(n); }`,
|
|
93
94
|
ceil: `function ceil(n) { return Math.ceil(n); }`,
|
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.9";
|