tova 0.7.0 → 0.8.2
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 +192 -10
- package/package.json +2 -7
- package/src/analyzer/analyzer.js +134 -2
- package/src/analyzer/deploy-analyzer.js +44 -0
- package/src/codegen/base-codegen.js +1159 -10
- package/src/codegen/codegen.js +20 -0
- package/src/codegen/deploy-codegen.js +49 -0
- package/src/codegen/shared-codegen.js +5 -0
- package/src/codegen/wasm-codegen.js +6 -0
- package/src/config/edit-toml.js +6 -2
- package/src/config/git-resolver.js +128 -0
- package/src/config/lock-file.js +57 -0
- package/src/config/module-cache.js +58 -0
- package/src/config/module-entry.js +37 -0
- package/src/config/module-path.js +31 -0
- package/src/config/pkg-errors.js +62 -0
- package/src/config/resolve.js +17 -0
- package/src/config/resolver.js +139 -0
- package/src/config/search.js +28 -0
- package/src/config/semver.js +72 -0
- package/src/config/toml.js +48 -5
- package/src/deploy/deploy.js +217 -0
- package/src/deploy/infer.js +218 -0
- package/src/deploy/provision.js +311 -0
- package/src/lsp/server.js +482 -0
- package/src/parser/ast.js +24 -0
- package/src/parser/concurrency-ast.js +15 -0
- package/src/parser/concurrency-parser.js +236 -0
- package/src/parser/deploy-ast.js +37 -0
- package/src/parser/deploy-parser.js +132 -0
- package/src/parser/parser.js +21 -3
- package/src/parser/select-ast.js +39 -0
- package/src/registry/plugins/concurrency-plugin.js +32 -0
- package/src/registry/plugins/deploy-plugin.js +33 -0
- package/src/registry/register-all.js +4 -0
- package/src/stdlib/inline.js +35 -3
- package/src/stdlib/runtime-bridge.js +152 -0
- package/src/version.js +1 -1
package/bin/tova.js
CHANGED
|
@@ -70,6 +70,8 @@ Commands:
|
|
|
70
70
|
info Show Tova version, Bun version, project config, and installed dependencies
|
|
71
71
|
doctor Check your development environment
|
|
72
72
|
completions <sh> Generate shell completions (bash, zsh, fish)
|
|
73
|
+
deploy <env> Deploy to a server (--plan, --rollback, --logs, --status)
|
|
74
|
+
env <env> <cmd> Manage secrets (list, set KEY=value)
|
|
73
75
|
explain <code> Show detailed explanation for an error/warning code (e.g., tova explain E202)
|
|
74
76
|
|
|
75
77
|
Options:
|
|
@@ -143,6 +145,74 @@ async function main() {
|
|
|
143
145
|
case 'remove':
|
|
144
146
|
await removeDep(args[1]);
|
|
145
147
|
break;
|
|
148
|
+
case 'update': {
|
|
149
|
+
const updatePkg = args[1] || null;
|
|
150
|
+
const updateConfig = resolveConfig(process.cwd());
|
|
151
|
+
const updateDeps = updatePkg
|
|
152
|
+
? { [updatePkg]: updateConfig.dependencies?.[updatePkg] || '*' }
|
|
153
|
+
: updateConfig.dependencies || {};
|
|
154
|
+
|
|
155
|
+
if (Object.keys(updateDeps).length === 0) {
|
|
156
|
+
console.log(' No Tova dependencies to update.');
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(' Checking for updates...');
|
|
161
|
+
// Delete lock file to force fresh resolution
|
|
162
|
+
const lockPath = join(process.cwd(), 'tova.lock');
|
|
163
|
+
if (existsSync(lockPath)) {
|
|
164
|
+
rmSync(lockPath);
|
|
165
|
+
}
|
|
166
|
+
await installDeps();
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case 'cache': {
|
|
170
|
+
const subCmd = args[1] || 'list';
|
|
171
|
+
const { getCacheDir } = await import('../src/config/module-cache.js');
|
|
172
|
+
const cacheDir = getCacheDir();
|
|
173
|
+
|
|
174
|
+
if (subCmd === 'path') {
|
|
175
|
+
console.log(cacheDir);
|
|
176
|
+
} else if (subCmd === 'list') {
|
|
177
|
+
console.log(` Cache: ${cacheDir}\n`);
|
|
178
|
+
if (existsSync(cacheDir)) {
|
|
179
|
+
try {
|
|
180
|
+
const hosts = readdirSync(cacheDir).filter(h => !h.startsWith('.'));
|
|
181
|
+
let found = false;
|
|
182
|
+
for (const host of hosts) {
|
|
183
|
+
const hostPath = join(cacheDir, host);
|
|
184
|
+
if (!statSync(hostPath).isDirectory()) continue;
|
|
185
|
+
const owners = readdirSync(hostPath);
|
|
186
|
+
for (const owner of owners) {
|
|
187
|
+
const ownerPath = join(hostPath, owner);
|
|
188
|
+
if (!statSync(ownerPath).isDirectory()) continue;
|
|
189
|
+
const repos = readdirSync(ownerPath);
|
|
190
|
+
for (const repo of repos) {
|
|
191
|
+
const repoPath = join(ownerPath, repo);
|
|
192
|
+
if (!statSync(repoPath).isDirectory()) continue;
|
|
193
|
+
const versions = readdirSync(repoPath).filter(v => v.startsWith('v'));
|
|
194
|
+
console.log(` ${host}/${owner}/${repo}: ${versions.join(', ') || '(empty)'}`);
|
|
195
|
+
found = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (!found) console.log(' (empty)');
|
|
200
|
+
} catch { console.log(' (empty)'); }
|
|
201
|
+
} else {
|
|
202
|
+
console.log(' (empty)');
|
|
203
|
+
}
|
|
204
|
+
} else if (subCmd === 'clean') {
|
|
205
|
+
if (existsSync(cacheDir)) {
|
|
206
|
+
rmSync(cacheDir, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
console.log(' Cache cleared.');
|
|
209
|
+
} else {
|
|
210
|
+
console.error(` Unknown cache subcommand: ${subCmd}`);
|
|
211
|
+
console.error(' Usage: tova cache [list|path|clean]');
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
146
216
|
case 'fmt':
|
|
147
217
|
formatFile(args.slice(1));
|
|
148
218
|
break;
|
|
@@ -173,6 +243,9 @@ async function main() {
|
|
|
173
243
|
case 'migrate:status':
|
|
174
244
|
await migrateStatus(args.slice(1));
|
|
175
245
|
break;
|
|
246
|
+
case 'deploy':
|
|
247
|
+
await deployCommand(args.slice(1));
|
|
248
|
+
break;
|
|
176
249
|
case 'explain': {
|
|
177
250
|
const code = args[1];
|
|
178
251
|
if (!code) {
|
|
@@ -546,6 +619,22 @@ function findTovaFiles(dir) {
|
|
|
546
619
|
return files;
|
|
547
620
|
}
|
|
548
621
|
|
|
622
|
+
// ─── Deploy ──────────────────────────────────────────────────
|
|
623
|
+
|
|
624
|
+
async function deployCommand(args) {
|
|
625
|
+
const { parseDeployArgs } = await import('../src/deploy/deploy.js');
|
|
626
|
+
const deployArgs = parseDeployArgs(args);
|
|
627
|
+
|
|
628
|
+
if (!deployArgs.envName && !deployArgs.list) {
|
|
629
|
+
console.error(color.red('Error: deploy requires an environment name (e.g., tova deploy prod)'));
|
|
630
|
+
process.exit(1);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// For now, just parse and build — full SSH deployment is wired in integration
|
|
634
|
+
console.log(color.cyan('Deploy feature is being implemented...'));
|
|
635
|
+
console.log('Parsed args:', deployArgs);
|
|
636
|
+
}
|
|
637
|
+
|
|
549
638
|
// ─── Run ────────────────────────────────────────────────────
|
|
550
639
|
|
|
551
640
|
async function runFile(filePath, options = {}) {
|
|
@@ -1912,6 +2001,62 @@ async function installDeps() {
|
|
|
1912
2001
|
return;
|
|
1913
2002
|
}
|
|
1914
2003
|
|
|
2004
|
+
// Resolve Tova module dependencies (if any)
|
|
2005
|
+
const tovaDeps = config.dependencies || {};
|
|
2006
|
+
const { isTovModule: _isTovMod } = await import('../src/config/module-path.js');
|
|
2007
|
+
const tovModuleKeys = Object.keys(tovaDeps).filter(k => _isTovMod(k));
|
|
2008
|
+
|
|
2009
|
+
if (tovModuleKeys.length > 0) {
|
|
2010
|
+
const { resolveDependencies } = await import('../src/config/resolver.js');
|
|
2011
|
+
const { listRemoteTags, fetchModule, getCommitSha } = await import('../src/config/git-resolver.js');
|
|
2012
|
+
const { isVersionCached, getModuleCachePath } = await import('../src/config/module-cache.js');
|
|
2013
|
+
const { readLockFile, writeLockFile } = await import('../src/config/lock-file.js');
|
|
2014
|
+
|
|
2015
|
+
console.log(' Resolving Tova dependencies...');
|
|
2016
|
+
|
|
2017
|
+
const lock = readLockFile(cwd);
|
|
2018
|
+
const tovaModuleDeps = {};
|
|
2019
|
+
for (const k of tovModuleKeys) {
|
|
2020
|
+
tovaModuleDeps[k] = tovaDeps[k];
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
try {
|
|
2024
|
+
const { resolved, npmDeps } = await resolveDependencies(tovaModuleDeps, {
|
|
2025
|
+
getAvailableVersions: async (mod) => {
|
|
2026
|
+
if (lock?.modules?.[mod]) return [lock.modules[mod].version];
|
|
2027
|
+
const tags = await listRemoteTags(mod);
|
|
2028
|
+
return tags.map(t => t.version);
|
|
2029
|
+
},
|
|
2030
|
+
getModuleConfig: async (mod, version) => {
|
|
2031
|
+
if (!isVersionCached(mod, version)) {
|
|
2032
|
+
console.log(` Fetching ${mod}@v${version}...`);
|
|
2033
|
+
await fetchModule(mod, version);
|
|
2034
|
+
}
|
|
2035
|
+
const modPath = getModuleCachePath(mod, version);
|
|
2036
|
+
try {
|
|
2037
|
+
return resolveConfig(modPath);
|
|
2038
|
+
} catch { return null; }
|
|
2039
|
+
},
|
|
2040
|
+
getVersionSha: async (mod, version) => {
|
|
2041
|
+
if (lock?.modules?.[mod]?.sha) return lock.modules[mod].sha;
|
|
2042
|
+
return await getCommitSha(mod, version);
|
|
2043
|
+
},
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
writeLockFile(cwd, resolved, npmDeps);
|
|
2047
|
+
console.log(` Resolved ${Object.keys(resolved).length} Tova module(s)`);
|
|
2048
|
+
|
|
2049
|
+
// Merge transitive npm deps into config for package.json generation
|
|
2050
|
+
if (Object.keys(npmDeps).length > 0) {
|
|
2051
|
+
if (!config.npm) config.npm = {};
|
|
2052
|
+
if (!config.npm.prod) config.npm.prod = {};
|
|
2053
|
+
Object.assign(config.npm.prod, npmDeps);
|
|
2054
|
+
}
|
|
2055
|
+
} catch (err) {
|
|
2056
|
+
console.error(` Failed to resolve Tova dependencies: ${err.message}`);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
1915
2060
|
// Generate shadow package.json from tova.toml
|
|
1916
2061
|
const wrote = writePackageJson(config, cwd);
|
|
1917
2062
|
if (wrote) {
|
|
@@ -1920,7 +2065,9 @@ async function installDeps() {
|
|
|
1920
2065
|
const code = await new Promise(res => proc.on('close', res));
|
|
1921
2066
|
process.exit(code);
|
|
1922
2067
|
} else {
|
|
1923
|
-
|
|
2068
|
+
if (tovModuleKeys.length === 0) {
|
|
2069
|
+
console.log(' No npm dependencies in tova.toml. Nothing to install.\n');
|
|
2070
|
+
}
|
|
1924
2071
|
}
|
|
1925
2072
|
}
|
|
1926
2073
|
|
|
@@ -1985,21 +2132,56 @@ async function addDep(args) {
|
|
|
1985
2132
|
await installDeps();
|
|
1986
2133
|
} else {
|
|
1987
2134
|
// Tova native dependency
|
|
1988
|
-
|
|
1989
|
-
|
|
2135
|
+
const { isTovModule: isTovMod } = await import('../src/config/module-path.js');
|
|
2136
|
+
|
|
2137
|
+
// Parse potential @version suffix
|
|
2138
|
+
let pkgName = actualPkg;
|
|
2139
|
+
let versionConstraint = null;
|
|
2140
|
+
if (pkgName.includes('@') && !pkgName.startsWith('@')) {
|
|
2141
|
+
const atIdx = pkgName.lastIndexOf('@');
|
|
2142
|
+
versionConstraint = pkgName.slice(atIdx + 1);
|
|
2143
|
+
pkgName = pkgName.slice(0, atIdx);
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
if (isTovMod(pkgName)) {
|
|
2147
|
+
// Tova module: fetch tags, pick version, add to [dependencies]
|
|
2148
|
+
const { listRemoteTags, pickLatestTag } = await import('../src/config/git-resolver.js');
|
|
2149
|
+
try {
|
|
2150
|
+
const tags = await listRemoteTags(pkgName);
|
|
2151
|
+
if (tags.length === 0) {
|
|
2152
|
+
console.error(` No version tags found for ${pkgName}`);
|
|
2153
|
+
process.exit(1);
|
|
2154
|
+
}
|
|
2155
|
+
if (!versionConstraint) {
|
|
2156
|
+
const latest = pickLatestTag(tags);
|
|
2157
|
+
versionConstraint = `^${latest.version}`;
|
|
2158
|
+
}
|
|
2159
|
+
addToSection(tomlPath, 'dependencies', `"${pkgName}"`, versionConstraint);
|
|
2160
|
+
console.log(` Added ${pkgName}@${versionConstraint} to [dependencies] in tova.toml`);
|
|
2161
|
+
await installDeps();
|
|
2162
|
+
} catch (err) {
|
|
2163
|
+
console.error(` Failed to add ${pkgName}: ${err.message}`);
|
|
2164
|
+
process.exit(1);
|
|
2165
|
+
}
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// Local path or generic dependency
|
|
2170
|
+
let name = pkgName;
|
|
2171
|
+
let source = pkgName;
|
|
1990
2172
|
|
|
1991
2173
|
// Detect source type
|
|
1992
|
-
if (
|
|
2174
|
+
if (pkgName.startsWith('file:') || pkgName.startsWith('./') || pkgName.startsWith('../') || pkgName.startsWith('/')) {
|
|
1993
2175
|
// Local path dependency
|
|
1994
|
-
source =
|
|
1995
|
-
name = basename(
|
|
1996
|
-
} else if (
|
|
2176
|
+
source = pkgName.startsWith('file:') ? pkgName : `file:${pkgName}`;
|
|
2177
|
+
name = basename(pkgName.replace(/^file:/, ''));
|
|
2178
|
+
} else if (pkgName.startsWith('git:') || pkgName.includes('.git')) {
|
|
1997
2179
|
// Git dependency
|
|
1998
|
-
source =
|
|
1999
|
-
name = basename(
|
|
2180
|
+
source = pkgName.startsWith('git:') ? pkgName : `git:${pkgName}`;
|
|
2181
|
+
name = basename(pkgName.replace(/\.git$/, '').replace(/^git:/, ''));
|
|
2000
2182
|
} else {
|
|
2001
2183
|
// Tova registry package (future: for now, just store the name)
|
|
2002
|
-
source = `*`;
|
|
2184
|
+
source = versionConstraint || `*`;
|
|
2003
2185
|
}
|
|
2004
2186
|
|
|
2005
2187
|
addToSection(tomlPath, 'dependencies', name, source);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tova",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.2",
|
|
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,12 +36,7 @@
|
|
|
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
|
"devDependencies": {
|
|
47
42
|
"@codemirror/autocomplete": "^6.20.0",
|
package/src/analyzer/analyzer.js
CHANGED
|
@@ -900,6 +900,19 @@ export class Analyzer {
|
|
|
900
900
|
}
|
|
901
901
|
this.visitExpression(node.argument);
|
|
902
902
|
return;
|
|
903
|
+
case 'SpawnExpression':
|
|
904
|
+
if (!this._concurrentDepth) {
|
|
905
|
+
this.warn("'spawn' should be used inside a 'concurrent' block", node.loc, null, {
|
|
906
|
+
code: 'W_SPAWN_OUTSIDE_CONCURRENT',
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
if (node.callee) this.visitExpression(node.callee);
|
|
910
|
+
if (node.arguments) {
|
|
911
|
+
for (const arg of node.arguments) {
|
|
912
|
+
this.visitExpression(arg);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return;
|
|
903
916
|
case 'YieldExpression':
|
|
904
917
|
if (node.argument) this.visitExpression(node.argument);
|
|
905
918
|
return;
|
|
@@ -1019,6 +1032,123 @@ export class Analyzer {
|
|
|
1019
1032
|
}
|
|
1020
1033
|
}
|
|
1021
1034
|
|
|
1035
|
+
visitConcurrentBlock(node) {
|
|
1036
|
+
// Validate mode
|
|
1037
|
+
const validModes = new Set(['all', 'cancel_on_error', 'first', 'timeout']);
|
|
1038
|
+
if (!validModes.has(node.mode)) {
|
|
1039
|
+
this.warn(`Unknown concurrent block mode '${node.mode}'`, node.loc, null, {
|
|
1040
|
+
code: 'W_UNKNOWN_CONCURRENT_MODE',
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Validate timeout
|
|
1045
|
+
if (node.mode === 'timeout' && !node.timeout) {
|
|
1046
|
+
this.warn("concurrent timeout mode requires a timeout value", node.loc, null, {
|
|
1047
|
+
code: 'W_MISSING_TIMEOUT',
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Warn on empty block
|
|
1052
|
+
if (node.body.length === 0) {
|
|
1053
|
+
this.warn("Empty concurrent block", node.loc, null, {
|
|
1054
|
+
code: 'W_EMPTY_CONCURRENT',
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Track concurrent depth for spawn validation
|
|
1059
|
+
this._concurrentDepth = (this._concurrentDepth || 0) + 1;
|
|
1060
|
+
|
|
1061
|
+
// Visit body statements (concurrent block does NOT create a new scope —
|
|
1062
|
+
// variables assigned inside should be visible after the block)
|
|
1063
|
+
for (const stmt of node.body) {
|
|
1064
|
+
this.visitNode(stmt);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Check spawned functions for WASM compatibility — warn if mixed WASM/non-WASM
|
|
1068
|
+
let hasWasm = false;
|
|
1069
|
+
let hasNonWasm = false;
|
|
1070
|
+
for (const stmt of node.body) {
|
|
1071
|
+
const spawn = (stmt.type === 'Assignment' && stmt.values && stmt.values[0] && stmt.values[0].type === 'SpawnExpression')
|
|
1072
|
+
? stmt.values[0]
|
|
1073
|
+
: (stmt.type === 'ExpressionStatement' && stmt.expression && stmt.expression.type === 'SpawnExpression')
|
|
1074
|
+
? stmt.expression
|
|
1075
|
+
: null;
|
|
1076
|
+
if (!spawn) continue;
|
|
1077
|
+
const calleeName = spawn.callee && spawn.callee.type === 'Identifier' ? spawn.callee.name : null;
|
|
1078
|
+
if (calleeName) {
|
|
1079
|
+
const sym = this.currentScope.lookup(calleeName);
|
|
1080
|
+
if (sym && sym.isWasm) {
|
|
1081
|
+
hasWasm = true;
|
|
1082
|
+
} else {
|
|
1083
|
+
hasNonWasm = true;
|
|
1084
|
+
}
|
|
1085
|
+
} else {
|
|
1086
|
+
// Lambda or complex expression — always non-WASM
|
|
1087
|
+
hasNonWasm = true;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (hasWasm && hasNonWasm) {
|
|
1091
|
+
this.warn(
|
|
1092
|
+
"concurrent block mixes @wasm and non-WASM tasks — non-WASM tasks will fall back to async JS execution",
|
|
1093
|
+
node.loc, null, { code: 'W_SPAWN_WASM_FALLBACK' }
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
this._concurrentDepth--;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
visitSelectStatement(node) {
|
|
1101
|
+
if (node.cases.length === 0) {
|
|
1102
|
+
this.warn("Empty select block", node.loc, null, {
|
|
1103
|
+
code: 'W_EMPTY_SELECT',
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
let defaultCount = 0;
|
|
1108
|
+
let timeoutCount = 0;
|
|
1109
|
+
for (const c of node.cases) {
|
|
1110
|
+
if (c.kind === 'default') defaultCount++;
|
|
1111
|
+
if (c.kind === 'timeout') timeoutCount++;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (defaultCount > 1) {
|
|
1115
|
+
this.warn("select block has multiple default cases", node.loc, null, {
|
|
1116
|
+
code: 'W_DUPLICATE_SELECT_DEFAULT',
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
if (timeoutCount > 1) {
|
|
1120
|
+
this.warn("select block has multiple timeout cases", node.loc, null, {
|
|
1121
|
+
code: 'W_DUPLICATE_SELECT_TIMEOUT',
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
if (defaultCount > 0 && timeoutCount > 0) {
|
|
1125
|
+
this.warn("select block has both default and timeout — default makes timeout unreachable", node.loc, null, {
|
|
1126
|
+
code: 'W_SELECT_DEFAULT_TIMEOUT',
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Visit each case's expressions and body
|
|
1131
|
+
for (const c of node.cases) {
|
|
1132
|
+
if (c.channel) this.visitNode(c.channel);
|
|
1133
|
+
if (c.value) this.visitNode(c.value);
|
|
1134
|
+
|
|
1135
|
+
if (c.kind === 'receive' && c.binding) {
|
|
1136
|
+
// Create scope for the binding variable
|
|
1137
|
+
this.pushScope('select-case');
|
|
1138
|
+
this.currentScope.define(c.binding,
|
|
1139
|
+
new Symbol(c.binding, 'variable', null, false, c.loc));
|
|
1140
|
+
for (const stmt of c.body) {
|
|
1141
|
+
this.visitNode(stmt);
|
|
1142
|
+
}
|
|
1143
|
+
this.popScope();
|
|
1144
|
+
} else {
|
|
1145
|
+
for (const stmt of c.body) {
|
|
1146
|
+
this.visitNode(stmt);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1022
1152
|
_validateCliCrossBlock() {
|
|
1023
1153
|
const cliBlocks = this.ast.body.filter(n => n.type === 'CliBlock');
|
|
1024
1154
|
if (cliBlocks.length === 0) return;
|
|
@@ -1614,9 +1744,10 @@ export class Analyzer {
|
|
|
1614
1744
|
} else if (node.pattern.type === 'ArrayPattern' || node.pattern.type === 'TuplePattern') {
|
|
1615
1745
|
for (const el of node.pattern.elements) {
|
|
1616
1746
|
if (el) {
|
|
1747
|
+
const varName = el.startsWith('...') ? el.slice(3) : el;
|
|
1617
1748
|
try {
|
|
1618
|
-
this.currentScope.define(
|
|
1619
|
-
new Symbol(
|
|
1749
|
+
this.currentScope.define(varName,
|
|
1750
|
+
new Symbol(varName, 'variable', null, false, node.loc));
|
|
1620
1751
|
} catch (e) {
|
|
1621
1752
|
this.error(e.message);
|
|
1622
1753
|
}
|
|
@@ -1634,6 +1765,7 @@ export class Analyzer {
|
|
|
1634
1765
|
sym._paramTypes = node.params.map(p => p.typeAnnotation || null);
|
|
1635
1766
|
sym._typeParams = node.typeParams || [];
|
|
1636
1767
|
sym.isPublic = node.isPublic || false;
|
|
1768
|
+
sym.isWasm = !!(node.decorators && node.decorators.some(d => d.name === 'wasm'));
|
|
1637
1769
|
this.currentScope.define(node.name, sym);
|
|
1638
1770
|
} catch (e) {
|
|
1639
1771
|
this.error(e.message);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Deploy-specific analyzer methods for the Tova language
|
|
2
|
+
// Extracted from analyzer.js for lazy loading — only loaded when deploy { } blocks are encountered.
|
|
3
|
+
|
|
4
|
+
const KNOWN_DEPLOY_FIELDS = new Set([
|
|
5
|
+
'server', 'domain', 'instances', 'memory', 'branch',
|
|
6
|
+
'health', 'health_interval', 'health_timeout',
|
|
7
|
+
'restart_on_failure', 'keep_releases',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const REQUIRED_DEPLOY_FIELDS = ['server', 'domain'];
|
|
11
|
+
|
|
12
|
+
export function installDeployAnalyzer(AnalyzerClass) {
|
|
13
|
+
if (AnalyzerClass.prototype._deployAnalyzerInstalled) return;
|
|
14
|
+
AnalyzerClass.prototype._deployAnalyzerInstalled = true;
|
|
15
|
+
|
|
16
|
+
AnalyzerClass.prototype.visitDeployBlock = function(node) {
|
|
17
|
+
// Collect config field keys present in the deploy block body
|
|
18
|
+
const presentFields = new Set();
|
|
19
|
+
for (const stmt of node.body) {
|
|
20
|
+
if (stmt.type === 'DeployConfigField') {
|
|
21
|
+
// Validate unknown fields
|
|
22
|
+
if (!KNOWN_DEPLOY_FIELDS.has(stmt.key)) {
|
|
23
|
+
this.error(
|
|
24
|
+
`Unknown deploy config field "${stmt.key}"`,
|
|
25
|
+
stmt.loc,
|
|
26
|
+
`Known fields: ${[...KNOWN_DEPLOY_FIELDS].join(', ')}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
presentFields.add(stmt.key);
|
|
30
|
+
}
|
|
31
|
+
// DeployEnvBlock and DeployDbBlock are valid sub-blocks — no additional validation needed
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Validate required fields
|
|
35
|
+
for (const required of REQUIRED_DEPLOY_FIELDS) {
|
|
36
|
+
if (!presentFields.has(required)) {
|
|
37
|
+
this.error(
|
|
38
|
+
`Deploy block "${node.name}" is missing required field "${required}"`,
|
|
39
|
+
node.loc
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|