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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Base code generation utilities shared across all codegen targets
|
|
2
2
|
import { RESULT_OPTION, PROPAGATE, BUILTIN_NAMES, STDLIB_DEPS } from '../stdlib/inline.js';
|
|
3
3
|
import { PIPE_TARGET } from '../parser/ast.js';
|
|
4
|
-
import { compileWasmFunction, compileWasmModule, generateWasmGlue, generateMultiWasmGlue } from './wasm-codegen.js';
|
|
4
|
+
import { compileWasmFunction, compileWasmModule, generateWasmGlue, generateMultiWasmGlue, generateWasmBytesExport } from './wasm-codegen.js';
|
|
5
5
|
|
|
6
6
|
export class BaseCodegen {
|
|
7
7
|
constructor() {
|
|
@@ -25,6 +25,9 @@ export class BaseCodegen {
|
|
|
25
25
|
this._sourceMappings = []; // {sourceLine, sourceCol, outputLine, outputCol, sourceFile?}
|
|
26
26
|
this._outputLineCount = 0;
|
|
27
27
|
this._sourceFile = null; // current source file for multi-file source maps
|
|
28
|
+
// @wasm function tracking for concurrent block WASM routing
|
|
29
|
+
this._wasmFunctions = new Map(); // funcName -> { node, wasmBytes }
|
|
30
|
+
this._needsRuntimeBridge = false; // set true when concurrent block uses WASM tasks
|
|
28
31
|
// @fast mode for TypedArray optimization
|
|
29
32
|
this._fastMode = false;
|
|
30
33
|
this._typedArrayParams = new Map(); // paramName -> 'Float64Array' | 'Int32Array' | 'Uint8Array'
|
|
@@ -279,6 +282,8 @@ export class BaseCodegen {
|
|
|
279
282
|
case 'TypeAlias': result = this.genTypeAlias(node); break;
|
|
280
283
|
case 'DeferStatement': result = this.genDeferStatement(node); break;
|
|
281
284
|
case 'WithStatement': result = this.genWithStatement(node); break;
|
|
285
|
+
case 'ConcurrentBlock': result = this.genConcurrentBlock(node); break;
|
|
286
|
+
case 'SelectStatement': result = this.genSelectStatement(node); break;
|
|
282
287
|
case 'ExternDeclaration': result = `${this.i()}// extern: ${node.name}`; break;
|
|
283
288
|
// Config declarations handled at block level — emit nothing in statement context
|
|
284
289
|
case 'AiConfigDeclaration': result = ''; break;
|
|
@@ -353,6 +358,7 @@ export class BaseCodegen {
|
|
|
353
358
|
case 'ColumnExpression': return this.genColumnExpression(node);
|
|
354
359
|
case 'ColumnAssignment': return this.genColumnAssignment(node);
|
|
355
360
|
case 'NegatedColumnExpression': return `{ __exclude: ${JSON.stringify(node.name)} }`;
|
|
361
|
+
case 'SpawnExpression': return this.genSpawnExpression(node);
|
|
356
362
|
default:
|
|
357
363
|
throw new Error(`Codegen: unknown expression type '${node.type}'`);
|
|
358
364
|
}
|
|
@@ -491,7 +497,9 @@ export class BaseCodegen {
|
|
|
491
497
|
return `${this.i()}const { ${props} } = ${this.genExpression(node.value)};`;
|
|
492
498
|
}
|
|
493
499
|
if (node.pattern.type === 'ArrayPattern' || node.pattern.type === 'TuplePattern') {
|
|
494
|
-
for (const e of node.pattern.elements)
|
|
500
|
+
for (const e of node.pattern.elements) {
|
|
501
|
+
if (e) this.declareVar(e.startsWith('...') ? e.slice(3) : e);
|
|
502
|
+
}
|
|
495
503
|
const els = node.pattern.elements.map(e => e || '').join(', ');
|
|
496
504
|
return `${this.i()}const [${els}] = ${this.genExpression(node.value)};`;
|
|
497
505
|
}
|
|
@@ -572,9 +580,14 @@ export class BaseCodegen {
|
|
|
572
580
|
// Track as user-defined to suppress stdlib version
|
|
573
581
|
if (BUILTIN_NAMES.has(node.name)) this._userDefinedNames.add(node.name);
|
|
574
582
|
const wasmBytes = compileWasmFunction(node);
|
|
575
|
-
|
|
583
|
+
// Track this WASM function for concurrent block routing
|
|
584
|
+
this._wasmFunctions.set(node.name, { node, wasmBytes });
|
|
576
585
|
const exportPrefix = node.isPublic ? 'export ' : '';
|
|
577
|
-
|
|
586
|
+
// Emit bytes constant first, then glue that references it (avoids duplication)
|
|
587
|
+
const bytesExport = generateWasmBytesExport(node.name, wasmBytes);
|
|
588
|
+
const name = node.name;
|
|
589
|
+
const glue = `const ${name} = new WebAssembly.Instance(new WebAssembly.Module(__wasm_bytes_${name})).exports.${name};`;
|
|
590
|
+
return `${this.i()}${bytesExport}\n${this.i()}${exportPrefix}${glue}`;
|
|
578
591
|
} catch (e) {
|
|
579
592
|
// Fall back to JS if WASM compilation fails
|
|
580
593
|
console.error(`Warning: @wasm compilation failed for '${node.name}': ${e.message}. Falling back to JS.`);
|
|
@@ -981,13 +994,22 @@ export class BaseCodegen {
|
|
|
981
994
|
}
|
|
982
995
|
}
|
|
983
996
|
|
|
997
|
+
// Pre-scan for scalar-replaceable Result/Option variables
|
|
998
|
+
const scalarMap = this._preAnalyzeScalarResults(regularStmts);
|
|
999
|
+
const prevScalar = this._scalarReplacements;
|
|
1000
|
+
this._scalarReplacements = scalarMap.size > 0 ? new Map([...(prevScalar || new Map()), ...scalarMap]) : prevScalar;
|
|
1001
|
+
|
|
984
1002
|
for (let idx = 0; idx < regularStmts.length; idx++) {
|
|
985
1003
|
if (bodySkipSet.has(idx)) continue;
|
|
986
1004
|
const stmt = regularStmts[idx];
|
|
987
1005
|
const isLast = idx === regularStmts.length - 1;
|
|
1006
|
+
// Scalar replacement: emit boolean+value pair instead of Result/Option allocation
|
|
1007
|
+
if (stmt.type === 'Assignment' && stmt.targets.length === 1 && typeof stmt.targets[0] === 'string' &&
|
|
1008
|
+
scalarMap.has(stmt.targets[0]) && scalarMap.get(stmt.targets[0]).assignIdx === idx) {
|
|
1009
|
+
lines.push(this._genScalarAssignment(stmt, scalarMap.get(stmt.targets[0])));
|
|
988
1010
|
// Implicit return: last expression in function body
|
|
989
1011
|
// Skip implicit return for known void/side-effect-only calls (print, assert, etc.)
|
|
990
|
-
if (isLast && stmt.type === 'ExpressionStatement' && !this._isVoidCall(stmt.expression)) {
|
|
1012
|
+
} else if (isLast && stmt.type === 'ExpressionStatement' && !this._isVoidCall(stmt.expression)) {
|
|
991
1013
|
// IIFE elimination: match/if as last expression in function body → direct returns
|
|
992
1014
|
const expr = stmt.expression;
|
|
993
1015
|
if (expr.type === 'MatchExpression' && !this._isSimpleMatch(expr)) {
|
|
@@ -1006,6 +1028,8 @@ export class BaseCodegen {
|
|
|
1006
1028
|
}
|
|
1007
1029
|
}
|
|
1008
1030
|
|
|
1031
|
+
this._scalarReplacements = prevScalar;
|
|
1032
|
+
|
|
1009
1033
|
if (deferBodies.length > 0) {
|
|
1010
1034
|
this.indent--;
|
|
1011
1035
|
lines.push(`${this.i()}} finally {`);
|
|
@@ -1130,13 +1154,24 @@ export class BaseCodegen {
|
|
|
1130
1154
|
lines.push(fillResult);
|
|
1131
1155
|
}
|
|
1132
1156
|
}
|
|
1157
|
+
// Pre-scan for scalar-replaceable Result/Option variables
|
|
1158
|
+
const scalarMap = this._preAnalyzeScalarResults(stmts);
|
|
1159
|
+
const prevScalar = this._scalarReplacements;
|
|
1160
|
+
this._scalarReplacements = scalarMap.size > 0 ? new Map([...(prevScalar || new Map()), ...scalarMap]) : prevScalar;
|
|
1133
1161
|
for (let i = 0; i < stmts.length; i++) {
|
|
1134
1162
|
if (skipSet.has(i)) continue;
|
|
1135
1163
|
const s = stmts[i];
|
|
1136
|
-
|
|
1164
|
+
// Scalar replacement: emit boolean+value pair instead of Result/Option allocation
|
|
1165
|
+
if (s.type === 'Assignment' && s.targets.length === 1 && typeof s.targets[0] === 'string' &&
|
|
1166
|
+
scalarMap.has(s.targets[0]) && scalarMap.get(s.targets[0]).assignIdx === i) {
|
|
1167
|
+
lines.push(this._genScalarAssignment(s, scalarMap.get(s.targets[0])));
|
|
1168
|
+
} else {
|
|
1169
|
+
lines.push(this.generateStatement(s));
|
|
1170
|
+
}
|
|
1137
1171
|
// Dead code elimination: stop after unconditional return/break/continue
|
|
1138
1172
|
if (s.type === 'ReturnStatement' || s.type === 'BreakStatement' || s.type === 'ContinueStatement') break;
|
|
1139
1173
|
}
|
|
1174
|
+
this._scalarReplacements = prevScalar;
|
|
1140
1175
|
return lines.join('\n');
|
|
1141
1176
|
}
|
|
1142
1177
|
|
|
@@ -1433,6 +1468,41 @@ export class BaseCodegen {
|
|
|
1433
1468
|
const fusedResult = this._tryFuseMapChain(node);
|
|
1434
1469
|
if (fusedResult !== null) return fusedResult;
|
|
1435
1470
|
|
|
1471
|
+
// Compile-time devirtualization: Ok(x).unwrap() → x, Err(e).isOk() → false, etc.
|
|
1472
|
+
const devirt = this._tryDevirtualizeResultOption(node);
|
|
1473
|
+
if (devirt !== null) return devirt;
|
|
1474
|
+
|
|
1475
|
+
// Scalar replacement: intercept method calls on scalar-replaced Result/Option variables
|
|
1476
|
+
if (this._scalarReplacements && this._scalarReplacements.size > 0 &&
|
|
1477
|
+
node.callee.type === 'MemberExpression' && !node.callee.computed &&
|
|
1478
|
+
node.callee.object && node.callee.object.type === 'Identifier') {
|
|
1479
|
+
const varName = node.callee.object.name;
|
|
1480
|
+
const scalarInfo = this._scalarReplacements.get(varName);
|
|
1481
|
+
if (scalarInfo) {
|
|
1482
|
+
const okVar = `${varName}__ok`;
|
|
1483
|
+
const vVar = `${varName}__v`;
|
|
1484
|
+
const method = node.callee.property;
|
|
1485
|
+
|
|
1486
|
+
if (scalarInfo.kind === 'result') {
|
|
1487
|
+
switch (method) {
|
|
1488
|
+
case 'isOk': return okVar;
|
|
1489
|
+
case 'isErr': return `!${okVar}`;
|
|
1490
|
+
case 'unwrap': return vVar;
|
|
1491
|
+
case 'unwrapOr': return node.arguments.length > 0 ? `(${okVar} ? ${vVar} : ${this.genExpression(node.arguments[0])})` : vVar;
|
|
1492
|
+
case 'expect': return vVar;
|
|
1493
|
+
}
|
|
1494
|
+
} else if (scalarInfo.kind === 'option') {
|
|
1495
|
+
switch (method) {
|
|
1496
|
+
case 'isSome': return okVar;
|
|
1497
|
+
case 'isNone': return `!${okVar}`;
|
|
1498
|
+
case 'unwrap': return vVar;
|
|
1499
|
+
case 'unwrapOr': return node.arguments.length > 0 ? `(${okVar} ? ${vVar} : ${this.genExpression(node.arguments[0])})` : vVar;
|
|
1500
|
+
case 'expect': return vVar;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1436
1506
|
// Transform Foo.new(...) → new Foo(...)
|
|
1437
1507
|
if (node.callee.type === 'MemberExpression' && !node.callee.computed && node.callee.property === 'new') {
|
|
1438
1508
|
const obj = this.genExpression(node.callee.object);
|
|
@@ -1583,6 +1653,207 @@ export class BaseCodegen {
|
|
|
1583
1653
|
return result;
|
|
1584
1654
|
}
|
|
1585
1655
|
|
|
1656
|
+
// ─── Compile-time devirtualization for Result/Option ──────────
|
|
1657
|
+
// When the codegen sees Ok(val).method(), Err(val).method(), Some(val).method(),
|
|
1658
|
+
// or None.method(), it knows the exact type at compile time. Instead of allocating
|
|
1659
|
+
// a Result/Option object and calling a method, inline the method body directly.
|
|
1660
|
+
|
|
1661
|
+
_tryDevirtualizeResultOption(node) {
|
|
1662
|
+
// Must be a CallExpression where callee is a non-computed MemberExpression
|
|
1663
|
+
if (node.callee.type !== 'MemberExpression' || node.callee.computed) return null;
|
|
1664
|
+
const method = node.callee.property;
|
|
1665
|
+
const obj = node.callee.object;
|
|
1666
|
+
const args = node.arguments;
|
|
1667
|
+
|
|
1668
|
+
// Case 1: None.method()
|
|
1669
|
+
if (obj.type === 'Identifier' && obj.name === 'None') {
|
|
1670
|
+
return this._devirtNone(method, args);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// Case 2: Ok(val).method(), Err(val).method(), Some(val).method()
|
|
1674
|
+
if (obj.type === 'CallExpression' && obj.callee.type === 'Identifier' && obj.arguments.length === 1) {
|
|
1675
|
+
const ctor = obj.callee.name;
|
|
1676
|
+
const val = obj.arguments[0];
|
|
1677
|
+
if (ctor === 'Ok') return this._devirtOk(val, method, args);
|
|
1678
|
+
if (ctor === 'Err') return this._devirtErr(val, method, args);
|
|
1679
|
+
if (ctor === 'Some') return this._devirtSome(val, method, args);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
return null;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
_devirtOk(val, method, args) {
|
|
1686
|
+
switch (method) {
|
|
1687
|
+
case 'unwrap':
|
|
1688
|
+
case 'expect':
|
|
1689
|
+
return this.genExpression(val);
|
|
1690
|
+
case 'unwrapOr':
|
|
1691
|
+
return this.genExpression(val); // Ok ignores default
|
|
1692
|
+
case 'isOk':
|
|
1693
|
+
return 'true';
|
|
1694
|
+
case 'isErr':
|
|
1695
|
+
return 'false';
|
|
1696
|
+
case 'map': {
|
|
1697
|
+
if (!this._isSimpleLambda(args[0])) return null;
|
|
1698
|
+
const parts = this._extractLambdaParts(args[0]);
|
|
1699
|
+
if (!parts) return null;
|
|
1700
|
+
this._needsResultOption = true;
|
|
1701
|
+
return `Ok(${this._substituteParam(parts.bodyExpr, parts.paramName, this.genExpression(val))})`;
|
|
1702
|
+
}
|
|
1703
|
+
case 'flatMap': {
|
|
1704
|
+
if (!this._isSimpleLambda(args[0])) return null;
|
|
1705
|
+
const parts = this._extractLambdaParts(args[0]);
|
|
1706
|
+
if (!parts) return null;
|
|
1707
|
+
return this._substituteParam(parts.bodyExpr, parts.paramName, this.genExpression(val));
|
|
1708
|
+
}
|
|
1709
|
+
case 'mapErr':
|
|
1710
|
+
this._needsResultOption = true;
|
|
1711
|
+
return `Ok(${this.genExpression(val)})`; // passes through
|
|
1712
|
+
case 'or':
|
|
1713
|
+
this._needsResultOption = true;
|
|
1714
|
+
return `Ok(${this.genExpression(val)})`; // passes through
|
|
1715
|
+
case 'and':
|
|
1716
|
+
if (args[0]) return this.genExpression(args[0]);
|
|
1717
|
+
return null;
|
|
1718
|
+
default:
|
|
1719
|
+
return null;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
_devirtErr(val, method, args) {
|
|
1724
|
+
switch (method) {
|
|
1725
|
+
case 'unwrapOr':
|
|
1726
|
+
if (args[0]) return this.genExpression(args[0]);
|
|
1727
|
+
return null;
|
|
1728
|
+
case 'isOk':
|
|
1729
|
+
return 'false';
|
|
1730
|
+
case 'isErr':
|
|
1731
|
+
return 'true';
|
|
1732
|
+
case 'unwrapErr':
|
|
1733
|
+
return this.genExpression(val);
|
|
1734
|
+
case 'map':
|
|
1735
|
+
this._needsResultOption = true;
|
|
1736
|
+
return `Err(${this.genExpression(val)})`; // passes through
|
|
1737
|
+
case 'flatMap':
|
|
1738
|
+
this._needsResultOption = true;
|
|
1739
|
+
return `Err(${this.genExpression(val)})`;
|
|
1740
|
+
case 'mapErr': {
|
|
1741
|
+
if (!this._isSimpleLambda(args[0])) return null;
|
|
1742
|
+
const parts = this._extractLambdaParts(args[0]);
|
|
1743
|
+
if (!parts) return null;
|
|
1744
|
+
this._needsResultOption = true;
|
|
1745
|
+
return `Err(${this._substituteParam(parts.bodyExpr, parts.paramName, this.genExpression(val))})`;
|
|
1746
|
+
}
|
|
1747
|
+
case 'or':
|
|
1748
|
+
if (args[0]) return this.genExpression(args[0]);
|
|
1749
|
+
return null;
|
|
1750
|
+
case 'and':
|
|
1751
|
+
this._needsResultOption = true;
|
|
1752
|
+
return `Err(${this.genExpression(val)})`;
|
|
1753
|
+
default:
|
|
1754
|
+
return null;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
_devirtSome(val, method, args) {
|
|
1759
|
+
switch (method) {
|
|
1760
|
+
case 'unwrap':
|
|
1761
|
+
case 'expect':
|
|
1762
|
+
return this.genExpression(val);
|
|
1763
|
+
case 'unwrapOr':
|
|
1764
|
+
return this.genExpression(val); // Some ignores default
|
|
1765
|
+
case 'isSome':
|
|
1766
|
+
return 'true';
|
|
1767
|
+
case 'isNone':
|
|
1768
|
+
return 'false';
|
|
1769
|
+
case 'map': {
|
|
1770
|
+
if (!this._isSimpleLambda(args[0])) return null;
|
|
1771
|
+
const parts = this._extractLambdaParts(args[0]);
|
|
1772
|
+
if (!parts) return null;
|
|
1773
|
+
this._needsResultOption = true;
|
|
1774
|
+
return `Some(${this._substituteParam(parts.bodyExpr, parts.paramName, this.genExpression(val))})`;
|
|
1775
|
+
}
|
|
1776
|
+
case 'flatMap': {
|
|
1777
|
+
if (!this._isSimpleLambda(args[0])) return null;
|
|
1778
|
+
const parts = this._extractLambdaParts(args[0]);
|
|
1779
|
+
if (!parts) return null;
|
|
1780
|
+
return this._substituteParam(parts.bodyExpr, parts.paramName, this.genExpression(val));
|
|
1781
|
+
}
|
|
1782
|
+
case 'filter': {
|
|
1783
|
+
if (!this._isSimpleLambda(args[0])) return null;
|
|
1784
|
+
const parts = this._extractLambdaParts(args[0]);
|
|
1785
|
+
if (!parts) return null;
|
|
1786
|
+
this._needsResultOption = true;
|
|
1787
|
+
const valCode = this.genExpression(val);
|
|
1788
|
+
const predCode = this._substituteParam(parts.bodyExpr, parts.paramName, valCode);
|
|
1789
|
+
return `(${predCode} ? Some(${valCode}) : None)`;
|
|
1790
|
+
}
|
|
1791
|
+
case 'or':
|
|
1792
|
+
this._needsResultOption = true;
|
|
1793
|
+
return `Some(${this.genExpression(val)})`; // passes through
|
|
1794
|
+
case 'and':
|
|
1795
|
+
if (args[0]) return this.genExpression(args[0]);
|
|
1796
|
+
return null;
|
|
1797
|
+
default:
|
|
1798
|
+
return null;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
_devirtNone(method, args) {
|
|
1803
|
+
switch (method) {
|
|
1804
|
+
case 'unwrapOr':
|
|
1805
|
+
if (args[0]) return this.genExpression(args[0]);
|
|
1806
|
+
return null;
|
|
1807
|
+
case 'isSome':
|
|
1808
|
+
return 'false';
|
|
1809
|
+
case 'isNone':
|
|
1810
|
+
return 'true';
|
|
1811
|
+
case 'map':
|
|
1812
|
+
this._needsResultOption = true;
|
|
1813
|
+
return 'None';
|
|
1814
|
+
case 'flatMap':
|
|
1815
|
+
this._needsResultOption = true;
|
|
1816
|
+
return 'None';
|
|
1817
|
+
case 'filter':
|
|
1818
|
+
this._needsResultOption = true;
|
|
1819
|
+
return 'None';
|
|
1820
|
+
case 'or':
|
|
1821
|
+
if (args[0]) return this.genExpression(args[0]);
|
|
1822
|
+
return null;
|
|
1823
|
+
case 'and':
|
|
1824
|
+
this._needsResultOption = true;
|
|
1825
|
+
return 'None';
|
|
1826
|
+
default:
|
|
1827
|
+
return null;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
_isSimpleLambda(node) {
|
|
1832
|
+
if (!node) return false;
|
|
1833
|
+
if (node.type !== 'FunctionExpression' && node.type !== 'ArrowFunction' && node.type !== 'LambdaExpression') return false;
|
|
1834
|
+
const params = node.params || [];
|
|
1835
|
+
if (params.length !== 1) return false;
|
|
1836
|
+
return true;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
_extractLambdaParts(lambda) {
|
|
1840
|
+
const params = lambda.params || [];
|
|
1841
|
+
const paramName = typeof params[0] === 'string' ? params[0] : (params[0].name || null);
|
|
1842
|
+
if (!paramName) return null;
|
|
1843
|
+
let bodyExpr = lambda.body;
|
|
1844
|
+
if (bodyExpr && bodyExpr.type === 'BlockStatement') {
|
|
1845
|
+
if (bodyExpr.body.length === 1) {
|
|
1846
|
+
const s = bodyExpr.body[0];
|
|
1847
|
+
if (s.type === 'ExpressionStatement') bodyExpr = s.expression;
|
|
1848
|
+
else if (s.type === 'ReturnStatement' && s.value) bodyExpr = s.value;
|
|
1849
|
+
else return null;
|
|
1850
|
+
} else {
|
|
1851
|
+
return null;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
return { paramName, bodyExpr };
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1586
1857
|
// Inline known builtins to direct method calls, eliminating wrapper overhead.
|
|
1587
1858
|
// Returns the inlined code string, or null if not inlineable.
|
|
1588
1859
|
_tryInlineBuiltin(node) {
|
|
@@ -2185,15 +2456,23 @@ export class BaseCodegen {
|
|
|
2185
2456
|
|
|
2186
2457
|
if (arm.pattern.type === 'WildcardPattern' || arm.pattern.type === 'BindingPattern') {
|
|
2187
2458
|
if (idx === node.arms.length - 1 && !arm.guard) {
|
|
2188
|
-
// Default case
|
|
2459
|
+
// Default case — wrap in else block if preceded by if/else-if arms
|
|
2460
|
+
if (idx > 0) {
|
|
2461
|
+
p.push(`${this.i()}else {\n`);
|
|
2462
|
+
this.indent++;
|
|
2463
|
+
}
|
|
2189
2464
|
if (arm.pattern.type === 'BindingPattern') {
|
|
2190
2465
|
p.push(`${this.i()}const ${arm.pattern.name} = ${tempVar};\n`);
|
|
2191
2466
|
}
|
|
2192
2467
|
if (arm.body.type === 'BlockStatement') {
|
|
2193
|
-
p.push(this.
|
|
2468
|
+
p.push(this._genBlockBodyReturns(arm.body) + '\n');
|
|
2194
2469
|
} else {
|
|
2195
2470
|
p.push(`${this.i()}return ${this.genExpression(arm.body)};\n`);
|
|
2196
2471
|
}
|
|
2472
|
+
if (idx > 0) {
|
|
2473
|
+
this.indent--;
|
|
2474
|
+
p.push(`${this.i()}}\n`);
|
|
2475
|
+
}
|
|
2197
2476
|
break;
|
|
2198
2477
|
}
|
|
2199
2478
|
}
|
|
@@ -2206,7 +2485,7 @@ export class BaseCodegen {
|
|
|
2206
2485
|
p.push(this.genPatternBindings(arm.pattern, tempVar));
|
|
2207
2486
|
|
|
2208
2487
|
if (arm.body.type === 'BlockStatement') {
|
|
2209
|
-
p.push(this.
|
|
2488
|
+
p.push(this._genBlockBodyReturns(arm.body) + '\n');
|
|
2210
2489
|
} else {
|
|
2211
2490
|
p.push(`${this.i()}return ${this.genExpression(arm.body)};\n`);
|
|
2212
2491
|
}
|
|
@@ -2613,13 +2892,53 @@ export class BaseCodegen {
|
|
|
2613
2892
|
if (pattern.type === 'BindingPattern') {
|
|
2614
2893
|
cond = `((${pattern.name}) => ${this.genExpression(guard)})(${subject})`;
|
|
2615
2894
|
} else {
|
|
2616
|
-
|
|
2895
|
+
// Collect bindings from sub-patterns (e.g. [1, n] if n < 2)
|
|
2896
|
+
const bindings = this._collectPatternBindings(pattern, subject);
|
|
2897
|
+
if (bindings.length > 0) {
|
|
2898
|
+
const params = bindings.map(b => b.name).join(', ');
|
|
2899
|
+
const args = bindings.map(b => b.accessor).join(', ');
|
|
2900
|
+
cond = `(${cond}) && ((${params}) => ${this.genExpression(guard)})(${args})`;
|
|
2901
|
+
} else {
|
|
2902
|
+
cond = `(${cond}) && (${this.genExpression(guard)})`;
|
|
2903
|
+
}
|
|
2617
2904
|
}
|
|
2618
2905
|
}
|
|
2619
2906
|
|
|
2620
2907
|
return cond;
|
|
2621
2908
|
}
|
|
2622
2909
|
|
|
2910
|
+
_collectPatternBindings(pattern, subject) {
|
|
2911
|
+
const bindings = [];
|
|
2912
|
+
switch (pattern.type) {
|
|
2913
|
+
case 'BindingPattern':
|
|
2914
|
+
bindings.push({ name: pattern.name, accessor: subject });
|
|
2915
|
+
break;
|
|
2916
|
+
case 'ArrayPattern':
|
|
2917
|
+
case 'TuplePattern':
|
|
2918
|
+
for (let i = 0; i < pattern.elements.length; i++) {
|
|
2919
|
+
const el = pattern.elements[i];
|
|
2920
|
+
if (el) bindings.push(...this._collectPatternBindings(el, `${subject}[${i}]`));
|
|
2921
|
+
}
|
|
2922
|
+
break;
|
|
2923
|
+
case 'VariantPattern': {
|
|
2924
|
+
const declaredFields = this._variantFields[pattern.name] || [];
|
|
2925
|
+
for (let i = 0; i < pattern.fields.length; i++) {
|
|
2926
|
+
const f = pattern.fields[i];
|
|
2927
|
+
const fieldName = typeof f === 'string' ? f : (f.type === 'BindingPattern' ? f.name : null);
|
|
2928
|
+
const propName = declaredFields[i] || fieldName || 'value';
|
|
2929
|
+
const accessor = `${subject}.${propName}`;
|
|
2930
|
+
if (typeof f === 'string') {
|
|
2931
|
+
bindings.push({ name: f, accessor });
|
|
2932
|
+
} else {
|
|
2933
|
+
bindings.push(...this._collectPatternBindings(f, accessor));
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
break;
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
return bindings;
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2623
2942
|
genPatternBindings(pattern, subject) {
|
|
2624
2943
|
switch (pattern.type) {
|
|
2625
2944
|
case 'BindingPattern':
|
|
@@ -3013,6 +3332,395 @@ export class BaseCodegen {
|
|
|
3013
3332
|
return p.join('\n');
|
|
3014
3333
|
}
|
|
3015
3334
|
|
|
3335
|
+
genConcurrentBlock(node) {
|
|
3336
|
+
const lines = [];
|
|
3337
|
+
const tasks = []; // { callCode, calleeName, spawn, isWasm }
|
|
3338
|
+
const assignments = [];
|
|
3339
|
+
|
|
3340
|
+
// Collect spawn tasks from body, preserving spawn node info
|
|
3341
|
+
for (const stmt of node.body) {
|
|
3342
|
+
if (stmt.type === 'Assignment' && stmt.values && stmt.values[0] && stmt.values[0].type === 'SpawnExpression') {
|
|
3343
|
+
const spawn = stmt.values[0];
|
|
3344
|
+
const calleeName = spawn.callee && spawn.callee.type === 'Identifier' ? spawn.callee.name : null;
|
|
3345
|
+
const callCode = `${this.genExpression(spawn.callee)}(${spawn.arguments.map(a => this.genExpression(a)).join(', ')})`;
|
|
3346
|
+
const isWasm = calleeName && this._wasmFunctions.has(calleeName);
|
|
3347
|
+
tasks.push({ callCode, calleeName, spawn, isWasm });
|
|
3348
|
+
assignments.push(stmt.targets[0]); // string variable name
|
|
3349
|
+
} else if (stmt.type === 'ExpressionStatement' && stmt.expression && stmt.expression.type === 'SpawnExpression') {
|
|
3350
|
+
const spawn = stmt.expression;
|
|
3351
|
+
const calleeName = spawn.callee && spawn.callee.type === 'Identifier' ? spawn.callee.name : null;
|
|
3352
|
+
const callCode = `${this.genExpression(spawn.callee)}(${spawn.arguments.map(a => this.genExpression(a)).join(', ')})`;
|
|
3353
|
+
const isWasm = calleeName && this._wasmFunctions.has(calleeName);
|
|
3354
|
+
tasks.push({ callCode, calleeName, spawn, isWasm });
|
|
3355
|
+
assignments.push(null); // fire-and-forget
|
|
3356
|
+
} else {
|
|
3357
|
+
// Non-spawn statement — generate normally
|
|
3358
|
+
lines.push(this.generateStatement(stmt));
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
this._needsResultOption = true;
|
|
3363
|
+
|
|
3364
|
+
if (tasks.length === 0) {
|
|
3365
|
+
return lines.join('\n');
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
const base = this._concurrentCounter = (this._concurrentCounter || 0) + 1;
|
|
3369
|
+
const tempVars = tasks.map((_, i) => `__c${base}_${i}`);
|
|
3370
|
+
|
|
3371
|
+
// Classify tasks
|
|
3372
|
+
const wasmTasks = tasks.filter(t => t.isWasm);
|
|
3373
|
+
const allWasm = wasmTasks.length === tasks.length;
|
|
3374
|
+
const hasWasm = wasmTasks.length > 0;
|
|
3375
|
+
|
|
3376
|
+
// Check if all WASM tasks share the same function (shared module optimization)
|
|
3377
|
+
const allSameWasm = allWasm && wasmTasks.every(t => t.calleeName === wasmTasks[0].calleeName);
|
|
3378
|
+
|
|
3379
|
+
// --- WASM path: all tasks are @wasm functions ---
|
|
3380
|
+
if (allWasm && (node.mode === 'all' || node.mode === 'first' || node.mode === 'cancel_on_error' || (node.mode === 'timeout' && node.timeout))) {
|
|
3381
|
+
this._needsRuntimeBridge = true;
|
|
3382
|
+
const rtVar = `__tova_rt`;
|
|
3383
|
+
// Build task descriptors
|
|
3384
|
+
const taskDescs = tasks.map(t => {
|
|
3385
|
+
const argsCode = t.spawn.arguments.map(a => this.genExpression(a)).join(', ');
|
|
3386
|
+
return `{ wasm: __wasm_bytes_${t.calleeName}, func: ${JSON.stringify(t.calleeName)}, args: [${argsCode}] }`;
|
|
3387
|
+
});
|
|
3388
|
+
const taskArrayCode = `[${taskDescs.join(', ')}]`;
|
|
3389
|
+
|
|
3390
|
+
// Choose runtime function based on mode
|
|
3391
|
+
let rtFunc;
|
|
3392
|
+
let rtCallSuffix = '';
|
|
3393
|
+
if (node.mode === 'first') {
|
|
3394
|
+
rtFunc = 'concurrentWasmFirst';
|
|
3395
|
+
} else if (node.mode === 'cancel_on_error') {
|
|
3396
|
+
rtFunc = 'concurrentWasmCancelOnError';
|
|
3397
|
+
} else if (node.mode === 'timeout' && node.timeout) {
|
|
3398
|
+
rtFunc = 'concurrentWasmTimeout';
|
|
3399
|
+
rtCallSuffix = `, ${this.genExpression(node.timeout)}`;
|
|
3400
|
+
} else {
|
|
3401
|
+
rtFunc = allSameWasm ? 'concurrentWasmShared' : 'concurrentWasm';
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
if (node.mode === 'first') {
|
|
3405
|
+
// First mode: single result from WASM, fallback to Promise.race
|
|
3406
|
+
const firstVar = `__first${base}`;
|
|
3407
|
+
lines.push(`${this.i()}let ${firstVar};`);
|
|
3408
|
+
lines.push(`${this.i()}if (typeof ${rtVar} !== 'undefined' && ${rtVar} && ${rtVar}.isRuntimeAvailable()) {`);
|
|
3409
|
+
lines.push(`${this.i()} ${firstVar} = { __result: new Ok(await ${rtVar}.${rtFunc}(${taskArrayCode})) };`);
|
|
3410
|
+
lines.push(`${this.i()}} else {`);
|
|
3411
|
+
// Fallback: Promise.race
|
|
3412
|
+
const acVar = `__ac${base}`;
|
|
3413
|
+
lines.push(`${this.i()} const ${acVar} = new AbortController();`);
|
|
3414
|
+
const fallbackFirst = tasks.map((t, i) =>
|
|
3415
|
+
`(async () => { try { const __r = await ${t.callCode}; ${acVar}.abort(); return { __idx: ${i}, __result: new Ok(__r) }; } catch(__e) { return { __idx: ${i}, __result: new Err(__e) }; } })()`
|
|
3416
|
+
);
|
|
3417
|
+
lines.push(`${this.i()} ${firstVar} = await Promise.race([`);
|
|
3418
|
+
for (let i = 0; i < fallbackFirst.length; i++) {
|
|
3419
|
+
lines.push(`${this.i()} ${fallbackFirst[i]}${i < fallbackFirst.length - 1 ? ',' : ''}`);
|
|
3420
|
+
}
|
|
3421
|
+
lines.push(`${this.i()} ]);`);
|
|
3422
|
+
lines.push(`${this.i()}}`);
|
|
3423
|
+
// Assign winner to all named variables
|
|
3424
|
+
for (let i = 0; i < assignments.length; i++) {
|
|
3425
|
+
if (assignments[i]) {
|
|
3426
|
+
this.declareVar(assignments[i]);
|
|
3427
|
+
lines.push(`${this.i()}const ${assignments[i]} = ${firstVar}.__result;`);
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
return lines.join('\n');
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
// All other WASM modes: array result
|
|
3434
|
+
lines.push(`${this.i()}let ${tempVars.join(', ')};`);
|
|
3435
|
+
lines.push(`${this.i()}if (typeof ${rtVar} !== 'undefined' && ${rtVar} && ${rtVar}.isRuntimeAvailable()) {`);
|
|
3436
|
+
if (node.mode === 'cancel_on_error') {
|
|
3437
|
+
// cancel_on_error: WASM function throws on first task error — wrap with try/catch
|
|
3438
|
+
// to provide per-task Err results consistent with JS fallback path
|
|
3439
|
+
lines.push(`${this.i()} try {`);
|
|
3440
|
+
lines.push(`${this.i()} const __wasm_results = await ${rtVar}.${rtFunc}(${taskArrayCode});`);
|
|
3441
|
+
lines.push(`${this.i()} [${tempVars.join(', ')}] = __wasm_results.map(__v => new Ok(__v));`);
|
|
3442
|
+
lines.push(`${this.i()} } catch (__wasm_err) {`);
|
|
3443
|
+
lines.push(`${this.i()} [${tempVars.join(', ')}] = Array(${tasks.length}).fill(new Err(__wasm_err));`);
|
|
3444
|
+
lines.push(`${this.i()} }`);
|
|
3445
|
+
} else if (node.mode === 'timeout' && node.timeout) {
|
|
3446
|
+
// timeout: WASM function throws on timeout — wrap with try/catch
|
|
3447
|
+
lines.push(`${this.i()} try {`);
|
|
3448
|
+
lines.push(`${this.i()} const __wasm_results = await ${rtVar}.${rtFunc}(${taskArrayCode}${rtCallSuffix});`);
|
|
3449
|
+
lines.push(`${this.i()} [${tempVars.join(', ')}] = __wasm_results.map(__v => new Ok(__v));`);
|
|
3450
|
+
lines.push(`${this.i()} } catch (__wasm_err) {`);
|
|
3451
|
+
lines.push(`${this.i()} [${tempVars.join(', ')}] = Array(${tasks.length}).fill(new Err(__wasm_err));`);
|
|
3452
|
+
lines.push(`${this.i()} }`);
|
|
3453
|
+
} else {
|
|
3454
|
+
lines.push(`${this.i()} const __wasm_results = await ${rtVar}.${rtFunc}(${taskArrayCode}${rtCallSuffix});`);
|
|
3455
|
+
lines.push(`${this.i()} [${tempVars.join(', ')}] = __wasm_results.map(__v => new Ok(__v));`);
|
|
3456
|
+
}
|
|
3457
|
+
lines.push(`${this.i()}} else {`);
|
|
3458
|
+
// Fallback: standard Promise.all path
|
|
3459
|
+
if (node.mode === 'cancel_on_error') {
|
|
3460
|
+
const acVar = `__ac${base}`;
|
|
3461
|
+
const abortedVar = `__aborted${base}`;
|
|
3462
|
+
lines.push(`${this.i()} const ${acVar} = new AbortController();`);
|
|
3463
|
+
lines.push(`${this.i()} const ${abortedVar} = new Promise((_, reject) => { ${acVar}.signal.addEventListener('abort', () => reject(new Error('cancelled')), { once: true }); });`);
|
|
3464
|
+
const fallbackExprs = tasks.map(t =>
|
|
3465
|
+
`(async () => { try { return new Ok(await Promise.race([${t.callCode}, ${abortedVar}])); } catch(__e) { if (!${acVar}.signal.aborted) ${acVar}.abort(); return new Err(__e); } })()`
|
|
3466
|
+
);
|
|
3467
|
+
lines.push(`${this.i()} [${tempVars.join(', ')}] = await Promise.all([`);
|
|
3468
|
+
for (let i = 0; i < fallbackExprs.length; i++) {
|
|
3469
|
+
lines.push(`${this.i()} ${fallbackExprs[i]}${i < fallbackExprs.length - 1 ? ',' : ''}`);
|
|
3470
|
+
}
|
|
3471
|
+
lines.push(`${this.i()} ]);`);
|
|
3472
|
+
} else if (node.mode === 'timeout' && node.timeout) {
|
|
3473
|
+
const timeoutMs = this.genExpression(node.timeout);
|
|
3474
|
+
const acVar = `__ac${base}`;
|
|
3475
|
+
const abortedVar = `__aborted${base}`;
|
|
3476
|
+
const timerVar = `__timer${base}`;
|
|
3477
|
+
lines.push(`${this.i()} const ${acVar} = new AbortController();`);
|
|
3478
|
+
lines.push(`${this.i()} const ${abortedVar} = new Promise((_, reject) => { ${acVar}.signal.addEventListener('abort', () => reject(new Error('concurrent timeout')), { once: true }); });`);
|
|
3479
|
+
lines.push(`${this.i()} const ${timerVar} = setTimeout(() => ${acVar}.abort(), ${timeoutMs});`);
|
|
3480
|
+
const fallbackExprs = tasks.map(t =>
|
|
3481
|
+
`(async () => { try { return new Ok(await Promise.race([${t.callCode}, ${abortedVar}])); } catch(__e) { return new Err(__e); } })()`
|
|
3482
|
+
);
|
|
3483
|
+
lines.push(`${this.i()} [${tempVars.join(', ')}] = await Promise.all([`);
|
|
3484
|
+
for (let i = 0; i < fallbackExprs.length; i++) {
|
|
3485
|
+
lines.push(`${this.i()} ${fallbackExprs[i]}${i < fallbackExprs.length - 1 ? ',' : ''}`);
|
|
3486
|
+
}
|
|
3487
|
+
lines.push(`${this.i()} ]);`);
|
|
3488
|
+
lines.push(`${this.i()} clearTimeout(${timerVar});`);
|
|
3489
|
+
} else {
|
|
3490
|
+
const fallbackExprs = tasks.map(t =>
|
|
3491
|
+
`(async () => { try { return new Ok(await ${t.callCode}); } catch(__e) { return new Err(__e); } })()`
|
|
3492
|
+
);
|
|
3493
|
+
lines.push(`${this.i()} [${tempVars.join(', ')}] = await Promise.all([`);
|
|
3494
|
+
for (let i = 0; i < fallbackExprs.length; i++) {
|
|
3495
|
+
lines.push(`${this.i()} ${fallbackExprs[i]}${i < fallbackExprs.length - 1 ? ',' : ''}`);
|
|
3496
|
+
}
|
|
3497
|
+
lines.push(`${this.i()} ]);`);
|
|
3498
|
+
}
|
|
3499
|
+
lines.push(`${this.i()}}`);
|
|
3500
|
+
} else {
|
|
3501
|
+
// --- JS path (with per-task WASM routing for mixed blocks) ---
|
|
3502
|
+
const taskExprs = tasks.map(t => {
|
|
3503
|
+
if (t.isWasm && hasWasm) {
|
|
3504
|
+
// Route individual WASM task through runtime with fallback
|
|
3505
|
+
this._needsRuntimeBridge = true;
|
|
3506
|
+
const argsCode = t.spawn.arguments.map(a => this.genExpression(a)).join(', ');
|
|
3507
|
+
return `(async () => { try { if (typeof __tova_rt !== 'undefined' && __tova_rt.isRuntimeAvailable()) { return new Ok(await __tova_rt.execWasm(__wasm_bytes_${t.calleeName}, ${JSON.stringify(t.calleeName)}, [${argsCode}])); } return new Ok(await ${t.callCode}); } catch(__e) { return new Err(__e); } })()`;
|
|
3508
|
+
}
|
|
3509
|
+
return `(async () => { try { return new Ok(await ${t.callCode}); } catch(__e) { return new Err(__e); } })()`;
|
|
3510
|
+
});
|
|
3511
|
+
|
|
3512
|
+
if (node.mode === 'timeout' && node.timeout) {
|
|
3513
|
+
const timeoutMs = this.genExpression(node.timeout);
|
|
3514
|
+
const acVar = `__ac${base}`;
|
|
3515
|
+
const abortedVar = `__aborted${base}`;
|
|
3516
|
+
const timerVar = `__timer${base}`;
|
|
3517
|
+
lines.push(`${this.i()}const ${acVar} = new AbortController();`);
|
|
3518
|
+
lines.push(`${this.i()}const ${abortedVar} = new Promise((_, reject) => { ${acVar}.signal.addEventListener('abort', () => reject(new Error('concurrent timeout')), { once: true }); });`);
|
|
3519
|
+
lines.push(`${this.i()}const ${timerVar} = setTimeout(() => ${acVar}.abort(), ${timeoutMs});`);
|
|
3520
|
+
const taskExprsTimeout = tasks.map(t => {
|
|
3521
|
+
if (t.isWasm && hasWasm) {
|
|
3522
|
+
this._needsRuntimeBridge = true;
|
|
3523
|
+
const argsCode = t.spawn.arguments.map(a => this.genExpression(a)).join(', ');
|
|
3524
|
+
return `(async () => { try { if (typeof __tova_rt !== 'undefined' && __tova_rt.isRuntimeAvailable()) { return new Ok(await Promise.race([__tova_rt.execWasm(__wasm_bytes_${t.calleeName}, ${JSON.stringify(t.calleeName)}, [${argsCode}]), ${abortedVar}])); } return new Ok(await Promise.race([${t.callCode}, ${abortedVar}])); } catch(__e) { return new Err(__e); } })()`;
|
|
3525
|
+
}
|
|
3526
|
+
return `(async () => { try { return new Ok(await Promise.race([${t.callCode}, ${abortedVar}])); } catch(__e) { return new Err(__e); } })()`;
|
|
3527
|
+
});
|
|
3528
|
+
lines.push(`${this.i()}const [${tempVars.join(', ')}] = await Promise.all([`);
|
|
3529
|
+
for (let i = 0; i < taskExprsTimeout.length; i++) {
|
|
3530
|
+
lines.push(`${this.i()} ${taskExprsTimeout[i]}${i < taskExprsTimeout.length - 1 ? ',' : ''}`);
|
|
3531
|
+
}
|
|
3532
|
+
lines.push(`${this.i()}]);`);
|
|
3533
|
+
lines.push(`${this.i()}clearTimeout(${timerVar});`);
|
|
3534
|
+
} else if (node.mode === 'cancel_on_error') {
|
|
3535
|
+
const acVar = `__ac${base}`;
|
|
3536
|
+
const abortedVar = `__aborted${base}`;
|
|
3537
|
+
lines.push(`${this.i()}const ${acVar} = new AbortController();`);
|
|
3538
|
+
lines.push(`${this.i()}const ${abortedVar} = new Promise((_, reject) => { ${acVar}.signal.addEventListener('abort', () => reject(new Error('cancelled')), { once: true }); });`);
|
|
3539
|
+
const taskExprsAbort = tasks.map(t => {
|
|
3540
|
+
if (t.isWasm && hasWasm) {
|
|
3541
|
+
this._needsRuntimeBridge = true;
|
|
3542
|
+
const argsCode = t.spawn.arguments.map(a => this.genExpression(a)).join(', ');
|
|
3543
|
+
return `(async () => { try { if (typeof __tova_rt !== 'undefined' && __tova_rt.isRuntimeAvailable()) { return new Ok(await Promise.race([__tova_rt.execWasm(__wasm_bytes_${t.calleeName}, ${JSON.stringify(t.calleeName)}, [${argsCode}]), ${abortedVar}])); } return new Ok(await Promise.race([${t.callCode}, ${abortedVar}])); } catch(__e) { if (!${acVar}.signal.aborted) ${acVar}.abort(); return new Err(__e); } })()`;
|
|
3544
|
+
}
|
|
3545
|
+
return `(async () => { try { return new Ok(await Promise.race([${t.callCode}, ${abortedVar}])); } catch(__e) { if (!${acVar}.signal.aborted) ${acVar}.abort(); return new Err(__e); } })()`;
|
|
3546
|
+
});
|
|
3547
|
+
lines.push(`${this.i()}const [${tempVars.join(', ')}] = await Promise.all([`);
|
|
3548
|
+
for (let i = 0; i < taskExprsAbort.length; i++) {
|
|
3549
|
+
lines.push(`${this.i()} ${taskExprsAbort[i]}${i < taskExprsAbort.length - 1 ? ',' : ''}`);
|
|
3550
|
+
}
|
|
3551
|
+
lines.push(`${this.i()}]);`);
|
|
3552
|
+
} else if (node.mode === 'first') {
|
|
3553
|
+
const acVar = `__ac${base}`;
|
|
3554
|
+
const firstVar = `__first${base}`;
|
|
3555
|
+
lines.push(`${this.i()}const ${acVar} = new AbortController();`);
|
|
3556
|
+
const taskExprsFirst = tasks.map((t, i) => {
|
|
3557
|
+
if (t.isWasm && hasWasm) {
|
|
3558
|
+
this._needsRuntimeBridge = true;
|
|
3559
|
+
const argsCode = t.spawn.arguments.map(a => this.genExpression(a)).join(', ');
|
|
3560
|
+
return `(async () => { try { let __r; if (typeof __tova_rt !== 'undefined' && __tova_rt.isRuntimeAvailable()) { __r = await __tova_rt.execWasm(__wasm_bytes_${t.calleeName}, ${JSON.stringify(t.calleeName)}, [${argsCode}]); } else { __r = await ${t.callCode}; } ${acVar}.abort(); return { __idx: ${i}, __result: new Ok(__r) }; } catch(__e) { return { __idx: ${i}, __result: new Err(__e) }; } })()`;
|
|
3561
|
+
}
|
|
3562
|
+
return `(async () => { try { const __r = await ${t.callCode}; ${acVar}.abort(); return { __idx: ${i}, __result: new Ok(__r) }; } catch(__e) { return { __idx: ${i}, __result: new Err(__e) }; } })()`;
|
|
3563
|
+
});
|
|
3564
|
+
lines.push(`${this.i()}const ${firstVar} = await Promise.race([`);
|
|
3565
|
+
for (let i = 0; i < taskExprsFirst.length; i++) {
|
|
3566
|
+
lines.push(`${this.i()} ${taskExprsFirst[i]}${i < taskExprsFirst.length - 1 ? ',' : ''}`);
|
|
3567
|
+
}
|
|
3568
|
+
lines.push(`${this.i()}]);`);
|
|
3569
|
+
// For first mode, assign the winner's result to all named variables
|
|
3570
|
+
for (let i = 0; i < assignments.length; i++) {
|
|
3571
|
+
if (assignments[i]) {
|
|
3572
|
+
this.declareVar(assignments[i]);
|
|
3573
|
+
lines.push(`${this.i()}const ${assignments[i]} = ${firstVar}.__result;`);
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
return lines.join('\n'); // early return — skip the normal assignment loop
|
|
3577
|
+
} else {
|
|
3578
|
+
lines.push(`${this.i()}const [${tempVars.join(', ')}] = await Promise.all([`);
|
|
3579
|
+
for (let i = 0; i < taskExprs.length; i++) {
|
|
3580
|
+
lines.push(`${this.i()} ${taskExprs[i]}${i < taskExprs.length - 1 ? ',' : ''}`);
|
|
3581
|
+
}
|
|
3582
|
+
lines.push(`${this.i()}]);`);
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
// Assign results to named variables
|
|
3587
|
+
for (let i = 0; i < assignments.length; i++) {
|
|
3588
|
+
if (assignments[i]) {
|
|
3589
|
+
this.declareVar(assignments[i]);
|
|
3590
|
+
lines.push(`${this.i()}const ${assignments[i]} = ${tempVars[i]};`);
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
return lines.join('\n');
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
genSelectStatement(node) {
|
|
3598
|
+
this._needsResultOption = true; // for Some/None in _tryReceive
|
|
3599
|
+
const base = this._selectCounter = (this._selectCounter || 0) + 1;
|
|
3600
|
+
|
|
3601
|
+
const hasDefault = node.cases.some(c => c.kind === 'default');
|
|
3602
|
+
|
|
3603
|
+
if (hasDefault) {
|
|
3604
|
+
return this._genSelectWithDefault(node, base);
|
|
3605
|
+
}
|
|
3606
|
+
return this._genSelectWithRace(node, base);
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
_genSelectWithRace(node, base) {
|
|
3610
|
+
const lines = [];
|
|
3611
|
+
const selVar = `__sel${base}`;
|
|
3612
|
+
const promises = [];
|
|
3613
|
+
|
|
3614
|
+
for (let i = 0; i < node.cases.length; i++) {
|
|
3615
|
+
const c = node.cases[i];
|
|
3616
|
+
if (c.kind === 'receive') {
|
|
3617
|
+
const ch = this.genExpression(c.channel);
|
|
3618
|
+
promises.push(`${ch}.receive().then(__v => ({ __case: ${i}, __value: __v }))`);
|
|
3619
|
+
} else if (c.kind === 'send') {
|
|
3620
|
+
const ch = this.genExpression(c.channel);
|
|
3621
|
+
const val = this.genExpression(c.value);
|
|
3622
|
+
promises.push(`${ch}.send(${val}).then(() => ({ __case: ${i} }))`);
|
|
3623
|
+
} else if (c.kind === 'timeout') {
|
|
3624
|
+
const ms = this.genExpression(c.value);
|
|
3625
|
+
promises.push(`new Promise(__r => setTimeout(() => __r({ __case: ${i} }), ${ms}))`);
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3629
|
+
lines.push(`${this.i()}const ${selVar} = await Promise.race([`);
|
|
3630
|
+
for (let i = 0; i < promises.length; i++) {
|
|
3631
|
+
lines.push(`${this.i()} ${promises[i]}${i < promises.length - 1 ? ',' : ''}`);
|
|
3632
|
+
}
|
|
3633
|
+
lines.push(`${this.i()}]);`);
|
|
3634
|
+
|
|
3635
|
+
// Generate if/else chain (NOT switch — avoids break conflicts in loops)
|
|
3636
|
+
for (let i = 0; i < node.cases.length; i++) {
|
|
3637
|
+
const c = node.cases[i];
|
|
3638
|
+
const prefix = i === 0 ? 'if' : ' else if';
|
|
3639
|
+
lines.push(`${this.i()}${prefix} (${selVar}.__case === ${i}) {`);
|
|
3640
|
+
this.indent++;
|
|
3641
|
+
if (c.kind === 'receive' && c.binding) {
|
|
3642
|
+
this.declareVar(c.binding);
|
|
3643
|
+
lines.push(`${this.i()}const ${c.binding} = ${selVar}.__value.value;`);
|
|
3644
|
+
}
|
|
3645
|
+
for (const stmt of c.body) {
|
|
3646
|
+
lines.push(this.generateStatement(stmt));
|
|
3647
|
+
}
|
|
3648
|
+
this.indent--;
|
|
3649
|
+
lines.push(`${this.i()}}`);
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
return lines.join('\n');
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
_genSelectWithDefault(node, base) {
|
|
3656
|
+
const lines = [];
|
|
3657
|
+
const nonDefault = node.cases.filter(c => c.kind !== 'default');
|
|
3658
|
+
const defaultCase = node.cases.find(c => c.kind === 'default');
|
|
3659
|
+
|
|
3660
|
+
lines.push(`${this.i()}{`);
|
|
3661
|
+
this.indent++;
|
|
3662
|
+
|
|
3663
|
+
let depth = 0;
|
|
3664
|
+
for (let i = 0; i < nonDefault.length; i++) {
|
|
3665
|
+
const c = nonDefault[i];
|
|
3666
|
+
const tryVar = `__try${base}_${i}`;
|
|
3667
|
+
|
|
3668
|
+
if (c.kind === 'receive') {
|
|
3669
|
+
const ch = this.genExpression(c.channel);
|
|
3670
|
+
lines.push(`${this.i()}const ${tryVar} = ${ch}._tryReceive();`);
|
|
3671
|
+
lines.push(`${this.i()}if (${tryVar}.__tag === 'Some') {`);
|
|
3672
|
+
this.indent++;
|
|
3673
|
+
if (c.binding) {
|
|
3674
|
+
this.declareVar(c.binding);
|
|
3675
|
+
lines.push(`${this.i()}const ${c.binding} = ${tryVar}.value;`);
|
|
3676
|
+
}
|
|
3677
|
+
for (const stmt of c.body) {
|
|
3678
|
+
lines.push(this.generateStatement(stmt));
|
|
3679
|
+
}
|
|
3680
|
+
this.indent--;
|
|
3681
|
+
lines.push(`${this.i()}} else {`);
|
|
3682
|
+
this.indent++;
|
|
3683
|
+
depth++;
|
|
3684
|
+
} else if (c.kind === 'send') {
|
|
3685
|
+
const ch = this.genExpression(c.channel);
|
|
3686
|
+
const val = this.genExpression(c.value);
|
|
3687
|
+
lines.push(`${this.i()}const ${tryVar} = ${ch}._trySend(${val});`);
|
|
3688
|
+
lines.push(`${this.i()}if (${tryVar}) {`);
|
|
3689
|
+
this.indent++;
|
|
3690
|
+
for (const stmt of c.body) {
|
|
3691
|
+
lines.push(this.generateStatement(stmt));
|
|
3692
|
+
}
|
|
3693
|
+
this.indent--;
|
|
3694
|
+
lines.push(`${this.i()}} else {`);
|
|
3695
|
+
this.indent++;
|
|
3696
|
+
depth++;
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
// Default case body
|
|
3701
|
+
for (const stmt of defaultCase.body) {
|
|
3702
|
+
lines.push(this.generateStatement(stmt));
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
// Close all else blocks
|
|
3706
|
+
for (let i = 0; i < depth; i++) {
|
|
3707
|
+
this.indent--;
|
|
3708
|
+
lines.push(`${this.i()}}`);
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
this.indent--;
|
|
3712
|
+
lines.push(`${this.i()}}`);
|
|
3713
|
+
|
|
3714
|
+
return lines.join('\n');
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
genSpawnExpression(node) {
|
|
3718
|
+
// Standalone spawn expression (not inside concurrent block codegen path)
|
|
3719
|
+
// This happens when spawn is used directly in an expression context
|
|
3720
|
+
const callCode = `${this.genExpression(node.callee)}(${node.arguments.map(a => this.genExpression(a)).join(', ')})`;
|
|
3721
|
+
return `(async () => { try { return new Ok(await ${callCode}); } catch(__e) { return new Err(__e); } })()`;
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3016
3724
|
// Check if a function body contains yield expressions (for generator detection)
|
|
3017
3725
|
_containsYield(node) {
|
|
3018
3726
|
if (!node) return false;
|
|
@@ -3036,4 +3744,445 @@ export class BaseCodegen {
|
|
|
3036
3744
|
this._yieldCache.set(node, result);
|
|
3037
3745
|
return result;
|
|
3038
3746
|
}
|
|
3747
|
+
|
|
3748
|
+
// ─── Scalar replacement codegen for Result/Option ───────────
|
|
3749
|
+
// Generates boolean+value pairs instead of allocating Result/Option objects.
|
|
3750
|
+
|
|
3751
|
+
/**
|
|
3752
|
+
* Generates scalar-replaced code for an assignment like:
|
|
3753
|
+
* r = if cond { Ok(val) } else { Err(err) }
|
|
3754
|
+
* Output: let r__ok, r__v; if (cond) { r__ok = true; r__v = val; } else { ... }
|
|
3755
|
+
*/
|
|
3756
|
+
_genScalarAssignment(node, info) {
|
|
3757
|
+
const varName = node.targets[0];
|
|
3758
|
+
const okVar = `${varName}__ok`;
|
|
3759
|
+
const vVar = `${varName}__v`;
|
|
3760
|
+
const ifExpr = node.values[0];
|
|
3761
|
+
|
|
3762
|
+
// Mark all three names as declared so later code doesn't redeclare
|
|
3763
|
+
this.declareVar(varName);
|
|
3764
|
+
this.declareVar(okVar);
|
|
3765
|
+
this.declareVar(vVar);
|
|
3766
|
+
|
|
3767
|
+
const parts = [];
|
|
3768
|
+
parts.push(`${this.i()}let ${okVar}, ${vVar};`);
|
|
3769
|
+
|
|
3770
|
+
// Build if/else chain
|
|
3771
|
+
let ifCode = `${this.i()}if (${this.genExpression(ifExpr.condition)}) {\n`;
|
|
3772
|
+
this.indent++;
|
|
3773
|
+
ifCode += this._genScalarBranch(ifExpr.consequent, okVar, vVar, info.kind);
|
|
3774
|
+
this.indent--;
|
|
3775
|
+
ifCode += `\n${this.i()}}`;
|
|
3776
|
+
|
|
3777
|
+
if (ifExpr.alternates) {
|
|
3778
|
+
for (const alt of ifExpr.alternates) {
|
|
3779
|
+
ifCode += ` else if (${this.genExpression(alt.condition)}) {\n`;
|
|
3780
|
+
this.indent++;
|
|
3781
|
+
ifCode += this._genScalarBranch(alt.body, okVar, vVar, info.kind);
|
|
3782
|
+
this.indent--;
|
|
3783
|
+
ifCode += `\n${this.i()}}`;
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
if (ifExpr.elseBody) {
|
|
3788
|
+
ifCode += ` else {\n`;
|
|
3789
|
+
this.indent++;
|
|
3790
|
+
ifCode += this._genScalarBranch(ifExpr.elseBody, okVar, vVar, info.kind);
|
|
3791
|
+
this.indent--;
|
|
3792
|
+
ifCode += `\n${this.i()}}`;
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
parts.push(ifCode);
|
|
3796
|
+
return parts.join('\n');
|
|
3797
|
+
}
|
|
3798
|
+
|
|
3799
|
+
/**
|
|
3800
|
+
* Generates scalar assignments for one branch of an if expression.
|
|
3801
|
+
* Extracts the Ok/Err/Some/None constructor call from the last expression
|
|
3802
|
+
* and emits okVar = true/false; vVar = value;
|
|
3803
|
+
*/
|
|
3804
|
+
_genScalarBranch(block, okVar, vVar, kind) {
|
|
3805
|
+
if (!block) return '';
|
|
3806
|
+
const stmts = block.type === 'BlockStatement' ? block.body : [block];
|
|
3807
|
+
const lines = [];
|
|
3808
|
+
|
|
3809
|
+
// Generate any statements before the last one (side effects)
|
|
3810
|
+
for (let i = 0; i < stmts.length - 1; i++) {
|
|
3811
|
+
lines.push(this.generateStatement(stmts[i]));
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
// Last statement has the constructor
|
|
3815
|
+
const last = stmts[stmts.length - 1];
|
|
3816
|
+
let expr = last;
|
|
3817
|
+
if (last.type === 'ExpressionStatement') expr = last.expression;
|
|
3818
|
+
if (last.type === 'ReturnStatement' && last.value) expr = last.value;
|
|
3819
|
+
|
|
3820
|
+
if (expr.type === 'CallExpression' && expr.callee && expr.callee.type === 'Identifier') {
|
|
3821
|
+
const name = expr.callee.name;
|
|
3822
|
+
if (name === 'Ok' || name === 'Some') {
|
|
3823
|
+
lines.push(`${this.i()}${okVar} = true; ${vVar} = ${this.genExpression(expr.arguments[0])};`);
|
|
3824
|
+
} else if (name === 'Err') {
|
|
3825
|
+
lines.push(`${this.i()}${okVar} = false; ${vVar} = ${this.genExpression(expr.arguments[0])};`);
|
|
3826
|
+
}
|
|
3827
|
+
} else if (expr.type === 'Identifier' && expr.name === 'None') {
|
|
3828
|
+
lines.push(`${this.i()}${okVar} = false; ${vVar} = undefined;`);
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
return lines.join('\n');
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
// ─── Scalar replacement analysis for Result/Option ──────────
|
|
3835
|
+
// Detects local variables assigned from if-expressions that produce
|
|
3836
|
+
// Ok/Err or Some/None, where all subsequent usages are safe method
|
|
3837
|
+
// calls (isOk, unwrap, unwrapOr, etc.). Such variables can be
|
|
3838
|
+
// replaced with a boolean + value pair (zero allocation).
|
|
3839
|
+
|
|
3840
|
+
/**
|
|
3841
|
+
* Scans an array of statements for scalar-replaceable Result/Option
|
|
3842
|
+
* assignments. Returns Map<varName, {kind, assignIdx}>.
|
|
3843
|
+
*/
|
|
3844
|
+
_preAnalyzeScalarResults(stmts) {
|
|
3845
|
+
const map = new Map();
|
|
3846
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
3847
|
+
const stmt = stmts[i];
|
|
3848
|
+
// Must be a single-target, single-value assignment
|
|
3849
|
+
if (stmt.type !== 'Assignment' || stmt.targets.length !== 1 || stmt.values.length !== 1) continue;
|
|
3850
|
+
const target = stmt.targets[0];
|
|
3851
|
+
if (typeof target !== 'string') continue; // skip MemberExpression targets
|
|
3852
|
+
const value = stmt.values[0];
|
|
3853
|
+
// Value must be an IfExpression (or IfStatement used as expression)
|
|
3854
|
+
if (value.type !== 'IfExpression' && value.type !== 'IfStatement') continue;
|
|
3855
|
+
const kind = this._detectResultOptionIf(value);
|
|
3856
|
+
if (kind === null) continue;
|
|
3857
|
+
// Check all subsequent statements for safe usage
|
|
3858
|
+
let allSafe = true;
|
|
3859
|
+
let anyUsed = false;
|
|
3860
|
+
for (let j = i + 1; j < stmts.length; j++) {
|
|
3861
|
+
const usage = this._checkScalarSafeUsage(stmts[j], target, kind);
|
|
3862
|
+
if (usage === 'unsafe') { allSafe = false; break; }
|
|
3863
|
+
if (usage === 'used') anyUsed = true;
|
|
3864
|
+
}
|
|
3865
|
+
if (allSafe && anyUsed) {
|
|
3866
|
+
map.set(target, { kind, assignIdx: i });
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
return map;
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3872
|
+
/**
|
|
3873
|
+
* Checks if ALL branches of an if expression return Ok/Err or Some/None.
|
|
3874
|
+
* Returns 'result' | 'option' | null.
|
|
3875
|
+
*/
|
|
3876
|
+
_detectResultOptionIf(ifExpr) {
|
|
3877
|
+
// Must have an else branch (otherwise result could be undefined)
|
|
3878
|
+
if (!ifExpr.elseBody) return null;
|
|
3879
|
+
|
|
3880
|
+
const types = [];
|
|
3881
|
+
|
|
3882
|
+
// Check consequent block
|
|
3883
|
+
types.push(this._getBlockResultType(ifExpr.consequent));
|
|
3884
|
+
|
|
3885
|
+
// Check elif alternates
|
|
3886
|
+
if (ifExpr.alternates) {
|
|
3887
|
+
for (const alt of ifExpr.alternates) {
|
|
3888
|
+
types.push(this._getBlockResultType(alt.body));
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
// Check else block
|
|
3893
|
+
types.push(this._getBlockResultType(ifExpr.elseBody));
|
|
3894
|
+
|
|
3895
|
+
// All must be non-null
|
|
3896
|
+
if (types.some(t => t === null)) return null;
|
|
3897
|
+
|
|
3898
|
+
// Check if all are Result constructors (Ok/Err)
|
|
3899
|
+
if (types.every(t => t === 'Ok' || t === 'Err')) return 'result';
|
|
3900
|
+
|
|
3901
|
+
// Check if all are Option constructors (Some/None)
|
|
3902
|
+
if (types.every(t => t === 'Some' || t === 'None')) return 'option';
|
|
3903
|
+
|
|
3904
|
+
return null;
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
/**
|
|
3908
|
+
* Gets the constructor name from the last expression in a block.
|
|
3909
|
+
* Returns 'Ok' | 'Err' | 'Some' | 'None' | null.
|
|
3910
|
+
*/
|
|
3911
|
+
_getBlockResultType(block) {
|
|
3912
|
+
if (!block) return null;
|
|
3913
|
+
const stmts = block.type === 'BlockStatement' ? block.body : [block];
|
|
3914
|
+
if (stmts.length === 0) return null;
|
|
3915
|
+
const last = stmts[stmts.length - 1];
|
|
3916
|
+
// Could be ExpressionStatement or bare expression (implicit return)
|
|
3917
|
+
let expr = last;
|
|
3918
|
+
if (last.type === 'ExpressionStatement') expr = last.expression;
|
|
3919
|
+
if (last.type === 'ReturnStatement' && last.value) expr = last.value;
|
|
3920
|
+
// Check for Ok(val), Err(val), Some(val) call
|
|
3921
|
+
if (expr.type === 'CallExpression' && expr.callee && expr.callee.type === 'Identifier') {
|
|
3922
|
+
const name = expr.callee.name;
|
|
3923
|
+
if (['Ok', 'Err', 'Some'].includes(name) && expr.arguments.length === 1) return name;
|
|
3924
|
+
}
|
|
3925
|
+
// Check for bare None identifier
|
|
3926
|
+
if (expr.type === 'Identifier' && expr.name === 'None') return 'None';
|
|
3927
|
+
return null;
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3930
|
+
/**
|
|
3931
|
+
* Walks a statement checking all references to varName.
|
|
3932
|
+
* Returns 'used' | 'unsafe' | 'none'.
|
|
3933
|
+
*/
|
|
3934
|
+
_checkScalarSafeUsage(stmt, varName, kind) {
|
|
3935
|
+
let found = false;
|
|
3936
|
+
let safe = true;
|
|
3937
|
+
|
|
3938
|
+
const SAFE_RESULT = new Set(['isOk', 'isErr', 'unwrap', 'unwrapOr', 'expect']);
|
|
3939
|
+
const SAFE_OPTION = new Set(['isSome', 'isNone', 'unwrap', 'unwrapOr', 'expect']);
|
|
3940
|
+
const safeSet = kind === 'result' ? SAFE_RESULT : SAFE_OPTION;
|
|
3941
|
+
|
|
3942
|
+
// Custom expression walker that handles method calls specially
|
|
3943
|
+
const walkExpr = (expr) => {
|
|
3944
|
+
if (!expr || !safe) return;
|
|
3945
|
+
// Check for varName.method() pattern FIRST
|
|
3946
|
+
if (expr.type === 'CallExpression' &&
|
|
3947
|
+
expr.callee && expr.callee.type === 'MemberExpression' &&
|
|
3948
|
+
!expr.callee.computed &&
|
|
3949
|
+
expr.callee.object && expr.callee.object.type === 'Identifier' &&
|
|
3950
|
+
expr.callee.object.name === varName) {
|
|
3951
|
+
if (safeSet.has(expr.callee.property)) {
|
|
3952
|
+
found = true;
|
|
3953
|
+
// Walk only the arguments (NOT the callee.object to avoid bare ref detection)
|
|
3954
|
+
for (const arg of expr.arguments) walkExpr(arg);
|
|
3955
|
+
return;
|
|
3956
|
+
}
|
|
3957
|
+
// Unsafe method call on the variable
|
|
3958
|
+
safe = false;
|
|
3959
|
+
return;
|
|
3960
|
+
}
|
|
3961
|
+
// Check for bare varName reference (not part of a safe method call)
|
|
3962
|
+
if (expr.type === 'Identifier' && expr.name === varName) {
|
|
3963
|
+
safe = false;
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
// Walk children
|
|
3967
|
+
switch (expr.type) {
|
|
3968
|
+
case 'BinaryExpression':
|
|
3969
|
+
case 'LogicalExpression':
|
|
3970
|
+
walkExpr(expr.left);
|
|
3971
|
+
walkExpr(expr.right);
|
|
3972
|
+
break;
|
|
3973
|
+
case 'UnaryExpression':
|
|
3974
|
+
walkExpr(expr.operand);
|
|
3975
|
+
break;
|
|
3976
|
+
case 'CallExpression':
|
|
3977
|
+
walkExpr(expr.callee);
|
|
3978
|
+
for (const arg of expr.arguments) walkExpr(arg);
|
|
3979
|
+
break;
|
|
3980
|
+
case 'MemberExpression':
|
|
3981
|
+
walkExpr(expr.object);
|
|
3982
|
+
if (expr.computed) walkExpr(expr.property);
|
|
3983
|
+
break;
|
|
3984
|
+
case 'ConditionalExpression':
|
|
3985
|
+
walkExpr(expr.condition);
|
|
3986
|
+
walkExpr(expr.consequent);
|
|
3987
|
+
walkExpr(expr.alternate);
|
|
3988
|
+
break;
|
|
3989
|
+
case 'ArrayLiteral':
|
|
3990
|
+
if (expr.elements) for (const el of expr.elements) walkExpr(el);
|
|
3991
|
+
break;
|
|
3992
|
+
case 'ObjectLiteral':
|
|
3993
|
+
if (expr.properties) for (const prop of expr.properties) walkExpr(prop.value);
|
|
3994
|
+
break;
|
|
3995
|
+
case 'TemplateLiteral':
|
|
3996
|
+
if (expr.expressions) for (const e of expr.expressions) walkExpr(e);
|
|
3997
|
+
break;
|
|
3998
|
+
// Lambdas/functions — don't descend (separate scope)
|
|
3999
|
+
case 'FunctionExpression':
|
|
4000
|
+
case 'ArrowFunction':
|
|
4001
|
+
case 'LambdaExpression':
|
|
4002
|
+
break;
|
|
4003
|
+
// Assignment expression
|
|
4004
|
+
case 'AssignmentExpression':
|
|
4005
|
+
walkExpr(expr.right);
|
|
4006
|
+
break;
|
|
4007
|
+
}
|
|
4008
|
+
};
|
|
4009
|
+
|
|
4010
|
+
// Walk statement dispatching to expression walker
|
|
4011
|
+
this._walkStmtForScalar(stmt, walkExpr);
|
|
4012
|
+
|
|
4013
|
+
if (!safe) return 'unsafe';
|
|
4014
|
+
return found ? 'used' : 'none';
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
/**
|
|
4018
|
+
* Walks all expressions within a statement, calling walkExpr for each
|
|
4019
|
+
* top-level expression found. Used by _checkScalarSafeUsage.
|
|
4020
|
+
*/
|
|
4021
|
+
_walkStmtForScalar(stmt, walkExpr) {
|
|
4022
|
+
if (!stmt) return;
|
|
4023
|
+
switch (stmt.type) {
|
|
4024
|
+
case 'ExpressionStatement':
|
|
4025
|
+
walkExpr(stmt.expression);
|
|
4026
|
+
break;
|
|
4027
|
+
case 'Assignment':
|
|
4028
|
+
case 'VarDeclaration':
|
|
4029
|
+
if (stmt.values) for (const v of stmt.values) walkExpr(v);
|
|
4030
|
+
if (stmt.targets) {
|
|
4031
|
+
for (const t of stmt.targets) {
|
|
4032
|
+
if (typeof t === 'object') walkExpr(t);
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
break;
|
|
4036
|
+
case 'ReturnStatement':
|
|
4037
|
+
if (stmt.value) walkExpr(stmt.value);
|
|
4038
|
+
break;
|
|
4039
|
+
case 'IfStatement':
|
|
4040
|
+
case 'IfExpression':
|
|
4041
|
+
walkExpr(stmt.condition);
|
|
4042
|
+
this._walkBlockForScalar(stmt.consequent, walkExpr);
|
|
4043
|
+
if (stmt.alternates) {
|
|
4044
|
+
for (const alt of stmt.alternates) {
|
|
4045
|
+
walkExpr(alt.condition);
|
|
4046
|
+
this._walkBlockForScalar(alt.body, walkExpr);
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
if (stmt.elseBody) this._walkBlockForScalar(stmt.elseBody, walkExpr);
|
|
4050
|
+
break;
|
|
4051
|
+
case 'ForStatement':
|
|
4052
|
+
walkExpr(stmt.iterable);
|
|
4053
|
+
this._walkBlockForScalar(stmt.body, walkExpr);
|
|
4054
|
+
break;
|
|
4055
|
+
case 'WhileStatement':
|
|
4056
|
+
walkExpr(stmt.condition);
|
|
4057
|
+
this._walkBlockForScalar(stmt.body, walkExpr);
|
|
4058
|
+
break;
|
|
4059
|
+
case 'CallExpression':
|
|
4060
|
+
walkExpr(stmt);
|
|
4061
|
+
break;
|
|
4062
|
+
default:
|
|
4063
|
+
if (stmt.expression) walkExpr(stmt.expression);
|
|
4064
|
+
break;
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
/**
|
|
4069
|
+
* Walks all statements in a block, dispatching to _walkStmtForScalar.
|
|
4070
|
+
*/
|
|
4071
|
+
_walkBlockForScalar(block, walkExpr) {
|
|
4072
|
+
if (!block) return;
|
|
4073
|
+
const stmts = block.type === 'BlockStatement' ? block.body : [block];
|
|
4074
|
+
for (const s of stmts) this._walkStmtForScalar(s, walkExpr);
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
/**
|
|
4078
|
+
* General-purpose expression walker. Calls callback(node) for every
|
|
4079
|
+
* expression node in the tree.
|
|
4080
|
+
*/
|
|
4081
|
+
_walkExpressions(node, callback) {
|
|
4082
|
+
if (!node) return;
|
|
4083
|
+
callback(node);
|
|
4084
|
+
switch (node.type) {
|
|
4085
|
+
case 'BinaryExpression':
|
|
4086
|
+
case 'LogicalExpression':
|
|
4087
|
+
this._walkExpressions(node.left, callback);
|
|
4088
|
+
this._walkExpressions(node.right, callback);
|
|
4089
|
+
break;
|
|
4090
|
+
case 'UnaryExpression':
|
|
4091
|
+
this._walkExpressions(node.operand, callback);
|
|
4092
|
+
break;
|
|
4093
|
+
case 'CallExpression':
|
|
4094
|
+
this._walkExpressions(node.callee, callback);
|
|
4095
|
+
for (const arg of node.arguments) this._walkExpressions(arg, callback);
|
|
4096
|
+
break;
|
|
4097
|
+
case 'MemberExpression':
|
|
4098
|
+
this._walkExpressions(node.object, callback);
|
|
4099
|
+
if (node.computed) this._walkExpressions(node.property, callback);
|
|
4100
|
+
break;
|
|
4101
|
+
case 'ConditionalExpression':
|
|
4102
|
+
this._walkExpressions(node.condition, callback);
|
|
4103
|
+
this._walkExpressions(node.consequent, callback);
|
|
4104
|
+
this._walkExpressions(node.alternate, callback);
|
|
4105
|
+
break;
|
|
4106
|
+
case 'ArrayLiteral':
|
|
4107
|
+
if (node.elements) for (const el of node.elements) this._walkExpressions(el, callback);
|
|
4108
|
+
break;
|
|
4109
|
+
case 'ObjectLiteral':
|
|
4110
|
+
if (node.properties) for (const prop of node.properties) this._walkExpressions(prop.value, callback);
|
|
4111
|
+
break;
|
|
4112
|
+
case 'TemplateLiteral':
|
|
4113
|
+
if (node.expressions) for (const expr of node.expressions) this._walkExpressions(expr, callback);
|
|
4114
|
+
break;
|
|
4115
|
+
// Lambdas/functions — don't descend (separate scope)
|
|
4116
|
+
case 'FunctionExpression':
|
|
4117
|
+
case 'ArrowFunction':
|
|
4118
|
+
case 'LambdaExpression':
|
|
4119
|
+
break;
|
|
4120
|
+
// Leaf nodes — no children
|
|
4121
|
+
case 'Identifier':
|
|
4122
|
+
case 'NumberLiteral':
|
|
4123
|
+
case 'StringLiteral':
|
|
4124
|
+
case 'BooleanLiteral':
|
|
4125
|
+
case 'NilLiteral':
|
|
4126
|
+
break;
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
/**
|
|
4131
|
+
* Walks all expressions within a statement.
|
|
4132
|
+
*/
|
|
4133
|
+
_walkStatementExpressions(stmt, callback) {
|
|
4134
|
+
if (!stmt) return;
|
|
4135
|
+
switch (stmt.type) {
|
|
4136
|
+
case 'ExpressionStatement':
|
|
4137
|
+
this._walkExpressions(stmt.expression, callback);
|
|
4138
|
+
break;
|
|
4139
|
+
case 'Assignment':
|
|
4140
|
+
case 'VarDeclaration':
|
|
4141
|
+
if (stmt.values) for (const v of stmt.values) this._walkExpressions(v, callback);
|
|
4142
|
+
if (stmt.targets) {
|
|
4143
|
+
for (const t of stmt.targets) {
|
|
4144
|
+
if (typeof t === 'object') this._walkExpressions(t, callback);
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
break;
|
|
4148
|
+
case 'ReturnStatement':
|
|
4149
|
+
if (stmt.value) this._walkExpressions(stmt.value, callback);
|
|
4150
|
+
break;
|
|
4151
|
+
case 'IfStatement':
|
|
4152
|
+
case 'IfExpression':
|
|
4153
|
+
this._walkExpressions(stmt.condition, callback);
|
|
4154
|
+
this._walkStatementBlock(stmt.consequent, callback);
|
|
4155
|
+
if (stmt.alternates) {
|
|
4156
|
+
for (const alt of stmt.alternates) {
|
|
4157
|
+
this._walkExpressions(alt.condition, callback);
|
|
4158
|
+
this._walkStatementBlock(alt.body, callback);
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
if (stmt.elseBody) this._walkStatementBlock(stmt.elseBody, callback);
|
|
4162
|
+
break;
|
|
4163
|
+
case 'ForStatement':
|
|
4164
|
+
this._walkExpressions(stmt.iterable, callback);
|
|
4165
|
+
this._walkStatementBlock(stmt.body, callback);
|
|
4166
|
+
break;
|
|
4167
|
+
case 'WhileStatement':
|
|
4168
|
+
this._walkExpressions(stmt.condition, callback);
|
|
4169
|
+
this._walkStatementBlock(stmt.body, callback);
|
|
4170
|
+
break;
|
|
4171
|
+
case 'CallExpression':
|
|
4172
|
+
this._walkExpressions(stmt, callback);
|
|
4173
|
+
break;
|
|
4174
|
+
default:
|
|
4175
|
+
if (stmt.expression) this._walkExpressions(stmt.expression, callback);
|
|
4176
|
+
break;
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
|
|
4180
|
+
/**
|
|
4181
|
+
* Walks all statements in a block for _walkStatementExpressions.
|
|
4182
|
+
*/
|
|
4183
|
+
_walkStatementBlock(block, callback) {
|
|
4184
|
+
if (!block) return;
|
|
4185
|
+
const stmts = block.type === 'BlockStatement' ? block.body : [block];
|
|
4186
|
+
for (const s of stmts) this._walkStatementExpressions(s, callback);
|
|
4187
|
+
}
|
|
3039
4188
|
}
|