tova 0.3.8 → 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 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[1]);
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
- function newProject(name) {
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="&#9881;"
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="&#9889;"
1534
+ title="Fast Refresh"
1535
+ description="Edit your code and see changes instantly. The dev server recompiles on save."
1536
+ />
1537
+ <FeatureCard
1538
+ icon="&#127912;"
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
- console.log(`\n Creating new Tova project: ${name}\n`);
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 tomlContent = stringifyTOML({
1684
+ const tomlConfig = {
1428
1685
  project: {
1429
1686
  name,
1430
1687
  version: '0.1.0',
1431
- description: 'A full-stack Tova application',
1432
- entry: 'src',
1688
+ description: template.tomlDescription,
1433
1689
  },
1434
1690
  build: {
1435
1691
  output: '.tova-out',
1436
1692
  },
1437
- dev: {
1438
- port: 3000,
1439
- },
1440
- dependencies: {},
1441
- npm: {},
1442
- });
1443
- writeFileSync(join(projectDir, 'tova.toml'), tomlContent);
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
- // Main app file
1456
- writeFileSync(join(projectDir, 'src', 'app.tova'), `// ${name} — Built with Tova
1457
-
1458
- shared {
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
- ## Development
1495
-
1496
- \`\`\`bash
1497
- tova install
1498
- tova dev
1499
- \`\`\`
1500
-
1501
- ## Build
1726
+ ## Getting started
1502
1727
 
1503
1728
  \`\`\`bash
1504
- tova build
1729
+ ${template.nextSteps(name).trim()}
1505
1730
  \`\`\`
1731
+ `);
1732
+ createdFiles.push('README.md');
1506
1733
 
1507
- ## Add npm packages
1734
+ // Print created files
1735
+ for (const f of createdFiles) {
1736
+ console.log(` ${color.green('✓')} Created ${color.bold(name + '/' + f)}`);
1737
+ }
1508
1738
 
1509
- \`\`\`bash
1510
- tova add htmx
1511
- tova add prettier --dev
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(` ✓ Created ${name}/tova.toml`);
1516
- console.log(` ✓ Created ${name}/.gitignore`);
1517
- console.log(` ✓ Created ${name}/src/app.tova`);
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${VERSION}\n`);
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
- // Check the npm registry for the latest version
3854
- const res = await fetch('https://registry.npmjs.org/tova/latest');
3855
- if (!res.ok) {
3856
- console.error(' Could not reach the npm registry. Check your network connection.');
3857
- process.exit(1);
3858
- }
3859
- const data = await res.json();
3860
- const latestVersion = data.version;
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
- if (latestVersion === VERSION) {
3863
- console.log(` Already on the latest version (v${VERSION}).\n`);
3864
- return;
3865
- }
4417
+ if (latestVersion === VERSION) {
4418
+ console.log(` ${color.green('Already on the latest version')} (v${VERSION}).\n`);
4419
+ return;
4420
+ }
3866
4421
 
3867
- console.log(` New version available: v${latestVersion}\n`);
3868
- console.log(' Upgrading...');
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
- // Detect the package manager used to install tova
3871
- const pm = detectPackageManager();
3872
- const installCmd = pm === 'bun' ? ['bun', ['add', '-g', 'tova@latest']]
3873
- : pm === 'pnpm' ? ['pnpm', ['add', '-g', 'tova@latest']]
3874
- : pm === 'yarn' ? ['yarn', ['global', 'add', 'tova@latest']]
3875
- : ['npm', ['install', '-g', 'tova@latest']];
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
- const proc = spawn(installCmd[0], installCmd[1], { stdio: 'inherit' });
3878
- const exitCode = await new Promise(res => proc.on('close', res));
4464
+ // Atomic rename
4465
+ const { renameSync } = await import('fs');
4466
+ renameSync(tmpPath, binPath);
3879
4467
 
3880
- if (exitCode === 0) {
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
- console.error(`\n Upgrade failed (exit code ${exitCode}).`);
3884
- console.error(` Try manually: ${installCmd[0]} ${installCmd[1].join(' ')}\n`);
3885
- process.exit(1);
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
- console.error(' Try manually: bun add -g tova@latest\n');
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.8",
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
  }
@@ -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
- 'Promise', 'Set', 'Map', 'WeakSet', 'WeakMap', 'Symbol',
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
- 'crypto', 'performance', 'navigator', 'location', 'history',
42
+ 'atob', 'btoa', 'Buffer',
43
+ // Browser APIs
44
+ 'crypto', 'performance', 'navigator', 'location', 'history', 'screen',
33
45
  'localStorage', 'sessionStorage',
34
- 'fetch', 'alert', 'confirm', 'prompt',
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
- for (const s of stmts) {
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) {
@@ -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
- const moduleCode = topLevel.map(s => moduleGen.generateStatement(s)).join('\n');
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.map(s => sharedGen.generateStatement(s)).join('\n');
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);
@@ -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.8";
2
+ export const VERSION = "0.3.9";