tova 0.5.1 → 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 +261 -60
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +351 -11
- package/src/analyzer/{client-analyzer.js → browser-analyzer.js} +20 -17
- package/src/analyzer/deploy-analyzer.js +44 -0
- package/src/analyzer/form-analyzer.js +113 -0
- package/src/analyzer/scope.js +2 -2
- package/src/codegen/base-codegen.js +1160 -10
- package/src/codegen/{client-codegen.js → browser-codegen.js} +444 -5
- package/src/codegen/codegen.js +119 -28
- package/src/codegen/deploy-codegen.js +49 -0
- package/src/codegen/edge-codegen.js +1351 -0
- package/src/codegen/form-codegen.js +553 -0
- package/src/codegen/security-codegen.js +5 -5
- package/src/codegen/server-codegen.js +88 -7
- 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/diagnostics/error-codes.js +1 -1
- package/src/docs/generator.js +1 -1
- package/src/formatter/formatter.js +4 -4
- package/src/lexer/tokens.js +12 -2
- package/src/lsp/server.js +483 -1
- package/src/parser/ast.js +60 -5
- package/src/parser/{client-ast.js → browser-ast.js} +3 -3
- package/src/parser/{client-parser.js → browser-parser.js} +42 -15
- 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/edge-ast.js +83 -0
- package/src/parser/edge-parser.js +262 -0
- package/src/parser/form-ast.js +80 -0
- package/src/parser/form-parser.js +206 -0
- package/src/parser/parser.js +82 -14
- package/src/parser/select-ast.js +39 -0
- package/src/registry/plugins/browser-plugin.js +30 -0
- package/src/registry/plugins/concurrency-plugin.js +32 -0
- package/src/registry/plugins/deploy-plugin.js +33 -0
- package/src/registry/plugins/edge-plugin.js +32 -0
- package/src/registry/register-all.js +8 -2
- package/src/runtime/ssr.js +2 -2
- package/src/stdlib/inline.js +38 -6
- package/src/stdlib/runtime-bridge.js +152 -0
- package/src/version.js +1 -1
- package/src/registry/plugins/client-plugin.js +0 -30
|
@@ -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;
|
|
@@ -286,6 +291,7 @@ export class BaseCodegen {
|
|
|
286
291
|
case 'SourceDeclaration': result = ''; break;
|
|
287
292
|
case 'PipelineDeclaration': result = ''; break;
|
|
288
293
|
case 'ValidateBlock': result = ''; break;
|
|
294
|
+
case 'FormDeclaration': result = ''; break;
|
|
289
295
|
case 'RefreshPolicy': result = ''; break;
|
|
290
296
|
case 'RefinementType': result = this.genRefinementType(node); break;
|
|
291
297
|
default:
|
|
@@ -352,6 +358,7 @@ export class BaseCodegen {
|
|
|
352
358
|
case 'ColumnExpression': return this.genColumnExpression(node);
|
|
353
359
|
case 'ColumnAssignment': return this.genColumnAssignment(node);
|
|
354
360
|
case 'NegatedColumnExpression': return `{ __exclude: ${JSON.stringify(node.name)} }`;
|
|
361
|
+
case 'SpawnExpression': return this.genSpawnExpression(node);
|
|
355
362
|
default:
|
|
356
363
|
throw new Error(`Codegen: unknown expression type '${node.type}'`);
|
|
357
364
|
}
|
|
@@ -490,7 +497,9 @@ export class BaseCodegen {
|
|
|
490
497
|
return `${this.i()}const { ${props} } = ${this.genExpression(node.value)};`;
|
|
491
498
|
}
|
|
492
499
|
if (node.pattern.type === 'ArrayPattern' || node.pattern.type === 'TuplePattern') {
|
|
493
|
-
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
|
+
}
|
|
494
503
|
const els = node.pattern.elements.map(e => e || '').join(', ');
|
|
495
504
|
return `${this.i()}const [${els}] = ${this.genExpression(node.value)};`;
|
|
496
505
|
}
|
|
@@ -571,9 +580,14 @@ export class BaseCodegen {
|
|
|
571
580
|
// Track as user-defined to suppress stdlib version
|
|
572
581
|
if (BUILTIN_NAMES.has(node.name)) this._userDefinedNames.add(node.name);
|
|
573
582
|
const wasmBytes = compileWasmFunction(node);
|
|
574
|
-
|
|
583
|
+
// Track this WASM function for concurrent block routing
|
|
584
|
+
this._wasmFunctions.set(node.name, { node, wasmBytes });
|
|
575
585
|
const exportPrefix = node.isPublic ? 'export ' : '';
|
|
576
|
-
|
|
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}`;
|
|
577
591
|
} catch (e) {
|
|
578
592
|
// Fall back to JS if WASM compilation fails
|
|
579
593
|
console.error(`Warning: @wasm compilation failed for '${node.name}': ${e.message}. Falling back to JS.`);
|
|
@@ -980,13 +994,22 @@ export class BaseCodegen {
|
|
|
980
994
|
}
|
|
981
995
|
}
|
|
982
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
|
+
|
|
983
1002
|
for (let idx = 0; idx < regularStmts.length; idx++) {
|
|
984
1003
|
if (bodySkipSet.has(idx)) continue;
|
|
985
1004
|
const stmt = regularStmts[idx];
|
|
986
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])));
|
|
987
1010
|
// Implicit return: last expression in function body
|
|
988
1011
|
// Skip implicit return for known void/side-effect-only calls (print, assert, etc.)
|
|
989
|
-
if (isLast && stmt.type === 'ExpressionStatement' && !this._isVoidCall(stmt.expression)) {
|
|
1012
|
+
} else if (isLast && stmt.type === 'ExpressionStatement' && !this._isVoidCall(stmt.expression)) {
|
|
990
1013
|
// IIFE elimination: match/if as last expression in function body → direct returns
|
|
991
1014
|
const expr = stmt.expression;
|
|
992
1015
|
if (expr.type === 'MatchExpression' && !this._isSimpleMatch(expr)) {
|
|
@@ -1005,6 +1028,8 @@ export class BaseCodegen {
|
|
|
1005
1028
|
}
|
|
1006
1029
|
}
|
|
1007
1030
|
|
|
1031
|
+
this._scalarReplacements = prevScalar;
|
|
1032
|
+
|
|
1008
1033
|
if (deferBodies.length > 0) {
|
|
1009
1034
|
this.indent--;
|
|
1010
1035
|
lines.push(`${this.i()}} finally {`);
|
|
@@ -1129,13 +1154,24 @@ export class BaseCodegen {
|
|
|
1129
1154
|
lines.push(fillResult);
|
|
1130
1155
|
}
|
|
1131
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;
|
|
1132
1161
|
for (let i = 0; i < stmts.length; i++) {
|
|
1133
1162
|
if (skipSet.has(i)) continue;
|
|
1134
1163
|
const s = stmts[i];
|
|
1135
|
-
|
|
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
|
+
}
|
|
1136
1171
|
// Dead code elimination: stop after unconditional return/break/continue
|
|
1137
1172
|
if (s.type === 'ReturnStatement' || s.type === 'BreakStatement' || s.type === 'ContinueStatement') break;
|
|
1138
1173
|
}
|
|
1174
|
+
this._scalarReplacements = prevScalar;
|
|
1139
1175
|
return lines.join('\n');
|
|
1140
1176
|
}
|
|
1141
1177
|
|
|
@@ -1432,6 +1468,41 @@ export class BaseCodegen {
|
|
|
1432
1468
|
const fusedResult = this._tryFuseMapChain(node);
|
|
1433
1469
|
if (fusedResult !== null) return fusedResult;
|
|
1434
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
|
+
|
|
1435
1506
|
// Transform Foo.new(...) → new Foo(...)
|
|
1436
1507
|
if (node.callee.type === 'MemberExpression' && !node.callee.computed && node.callee.property === 'new') {
|
|
1437
1508
|
const obj = this.genExpression(node.callee.object);
|
|
@@ -1582,6 +1653,207 @@ export class BaseCodegen {
|
|
|
1582
1653
|
return result;
|
|
1583
1654
|
}
|
|
1584
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
|
+
|
|
1585
1857
|
// Inline known builtins to direct method calls, eliminating wrapper overhead.
|
|
1586
1858
|
// Returns the inlined code string, or null if not inlineable.
|
|
1587
1859
|
_tryInlineBuiltin(node) {
|
|
@@ -2184,15 +2456,23 @@ export class BaseCodegen {
|
|
|
2184
2456
|
|
|
2185
2457
|
if (arm.pattern.type === 'WildcardPattern' || arm.pattern.type === 'BindingPattern') {
|
|
2186
2458
|
if (idx === node.arms.length - 1 && !arm.guard) {
|
|
2187
|
-
// 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
|
+
}
|
|
2188
2464
|
if (arm.pattern.type === 'BindingPattern') {
|
|
2189
2465
|
p.push(`${this.i()}const ${arm.pattern.name} = ${tempVar};\n`);
|
|
2190
2466
|
}
|
|
2191
2467
|
if (arm.body.type === 'BlockStatement') {
|
|
2192
|
-
p.push(this.
|
|
2468
|
+
p.push(this._genBlockBodyReturns(arm.body) + '\n');
|
|
2193
2469
|
} else {
|
|
2194
2470
|
p.push(`${this.i()}return ${this.genExpression(arm.body)};\n`);
|
|
2195
2471
|
}
|
|
2472
|
+
if (idx > 0) {
|
|
2473
|
+
this.indent--;
|
|
2474
|
+
p.push(`${this.i()}}\n`);
|
|
2475
|
+
}
|
|
2196
2476
|
break;
|
|
2197
2477
|
}
|
|
2198
2478
|
}
|
|
@@ -2205,7 +2485,7 @@ export class BaseCodegen {
|
|
|
2205
2485
|
p.push(this.genPatternBindings(arm.pattern, tempVar));
|
|
2206
2486
|
|
|
2207
2487
|
if (arm.body.type === 'BlockStatement') {
|
|
2208
|
-
p.push(this.
|
|
2488
|
+
p.push(this._genBlockBodyReturns(arm.body) + '\n');
|
|
2209
2489
|
} else {
|
|
2210
2490
|
p.push(`${this.i()}return ${this.genExpression(arm.body)};\n`);
|
|
2211
2491
|
}
|
|
@@ -2612,13 +2892,53 @@ export class BaseCodegen {
|
|
|
2612
2892
|
if (pattern.type === 'BindingPattern') {
|
|
2613
2893
|
cond = `((${pattern.name}) => ${this.genExpression(guard)})(${subject})`;
|
|
2614
2894
|
} else {
|
|
2615
|
-
|
|
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
|
+
}
|
|
2616
2904
|
}
|
|
2617
2905
|
}
|
|
2618
2906
|
|
|
2619
2907
|
return cond;
|
|
2620
2908
|
}
|
|
2621
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
|
+
|
|
2622
2942
|
genPatternBindings(pattern, subject) {
|
|
2623
2943
|
switch (pattern.type) {
|
|
2624
2944
|
case 'BindingPattern':
|
|
@@ -3012,6 +3332,395 @@ export class BaseCodegen {
|
|
|
3012
3332
|
return p.join('\n');
|
|
3013
3333
|
}
|
|
3014
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
|
+
|
|
3015
3724
|
// Check if a function body contains yield expressions (for generator detection)
|
|
3016
3725
|
_containsYield(node) {
|
|
3017
3726
|
if (!node) return false;
|
|
@@ -3035,4 +3744,445 @@ export class BaseCodegen {
|
|
|
3035
3744
|
this._yieldCache.set(node, result);
|
|
3036
3745
|
return result;
|
|
3037
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
|
+
}
|
|
3038
4188
|
}
|