tova 0.7.0 → 0.9.4
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 +1312 -139
- package/package.json +8 -1
- package/src/analyzer/analyzer.js +539 -11
- package/src/analyzer/browser-analyzer.js +56 -8
- package/src/analyzer/deploy-analyzer.js +44 -0
- package/src/analyzer/scope.js +7 -0
- package/src/analyzer/server-analyzer.js +33 -1
- package/src/codegen/base-codegen.js +1296 -23
- package/src/codegen/browser-codegen.js +725 -20
- package/src/codegen/codegen.js +87 -5
- package/src/codegen/deploy-codegen.js +49 -0
- package/src/codegen/server-codegen.js +54 -6
- package/src/codegen/shared-codegen.js +5 -0
- package/src/codegen/theme-codegen.js +69 -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 +63 -0
- package/src/config/pkg-errors.js +62 -0
- package/src/config/resolve.js +26 -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 +61 -6
- package/src/deploy/deploy.js +217 -0
- package/src/deploy/infer.js +218 -0
- package/src/deploy/provision.js +315 -0
- package/src/diagnostics/security-scorecard.js +111 -0
- package/src/lexer/lexer.js +18 -3
- package/src/lsp/server.js +482 -0
- package/src/parser/animate-ast.js +45 -0
- package/src/parser/ast.js +39 -0
- package/src/parser/browser-ast.js +19 -1
- package/src/parser/browser-parser.js +221 -4
- 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 +42 -5
- package/src/parser/select-ast.js +39 -0
- package/src/parser/theme-ast.js +29 -0
- package/src/parser/theme-parser.js +70 -0
- package/src/registry/plugins/concurrency-plugin.js +32 -0
- package/src/registry/plugins/deploy-plugin.js +33 -0
- package/src/registry/plugins/theme-plugin.js +20 -0
- package/src/registry/register-all.js +6 -0
- package/src/runtime/charts.js +547 -0
- package/src/runtime/embedded.js +6 -2
- package/src/runtime/reactivity.js +60 -0
- package/src/runtime/router.js +703 -295
- package/src/runtime/table.js +606 -33
- package/src/stdlib/inline.js +365 -10
- package/src/stdlib/runtime-bridge.js +152 -0
- package/src/stdlib/string.js +84 -2
- package/src/stdlib/validation.js +1 -1
- 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'
|
|
@@ -186,10 +189,25 @@ export class BaseCodegen {
|
|
|
186
189
|
// Track a builtin and its transitive dependencies from the stdlib dependency graph
|
|
187
190
|
_trackBuiltin(name) {
|
|
188
191
|
this._usedBuiltins.add(name);
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
// Check if this name or any transitive dependency requires Result/Option
|
|
193
|
+
if (name === 'Ok' || name === 'Err' || name === 'Some' || name === 'None') {
|
|
194
|
+
this._needsResultOption = true;
|
|
195
|
+
}
|
|
196
|
+
// Resolve transitive deps using a queue (e.g., iter -> Seq -> Some/None)
|
|
197
|
+
const queue = [name];
|
|
198
|
+
while (queue.length > 0) {
|
|
199
|
+
const current = queue.pop();
|
|
200
|
+
const deps = STDLIB_DEPS[current];
|
|
201
|
+
if (deps) {
|
|
202
|
+
for (const dep of deps) {
|
|
203
|
+
if (!this._usedBuiltins.has(dep)) {
|
|
204
|
+
this._usedBuiltins.add(dep);
|
|
205
|
+
queue.push(dep); // resolve further transitive deps
|
|
206
|
+
}
|
|
207
|
+
if (dep === 'Ok' || dep === 'Err' || dep === 'Some' || dep === 'None') {
|
|
208
|
+
this._needsResultOption = true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
193
211
|
}
|
|
194
212
|
}
|
|
195
213
|
}
|
|
@@ -279,6 +297,8 @@ export class BaseCodegen {
|
|
|
279
297
|
case 'TypeAlias': result = this.genTypeAlias(node); break;
|
|
280
298
|
case 'DeferStatement': result = this.genDeferStatement(node); break;
|
|
281
299
|
case 'WithStatement': result = this.genWithStatement(node); break;
|
|
300
|
+
case 'ConcurrentBlock': result = this.genConcurrentBlock(node); break;
|
|
301
|
+
case 'SelectStatement': result = this.genSelectStatement(node); break;
|
|
282
302
|
case 'ExternDeclaration': result = `${this.i()}// extern: ${node.name}`; break;
|
|
283
303
|
// Config declarations handled at block level — emit nothing in statement context
|
|
284
304
|
case 'AiConfigDeclaration': result = ''; break;
|
|
@@ -311,9 +331,12 @@ export class BaseCodegen {
|
|
|
311
331
|
return this._paramSubstitutions.get(node.name);
|
|
312
332
|
}
|
|
313
333
|
// Track builtin identifier usage (e.g., None used without call)
|
|
334
|
+
// Use _trackBuiltin to resolve transitive deps (including Result/Option)
|
|
314
335
|
if (BUILTIN_NAMES.has(node.name)) {
|
|
315
|
-
this.
|
|
336
|
+
this._trackBuiltin(node.name);
|
|
316
337
|
}
|
|
338
|
+
// Ok/Err/Some/None are in RESULT_OPTION, not BUILTIN_FUNCTIONS,
|
|
339
|
+
// so they need a separate check for _needsResultOption
|
|
317
340
|
if (node.name === 'Ok' || node.name === 'Err' || node.name === 'Some' || node.name === 'None') {
|
|
318
341
|
this._needsResultOption = true;
|
|
319
342
|
}
|
|
@@ -353,6 +376,7 @@ export class BaseCodegen {
|
|
|
353
376
|
case 'ColumnExpression': return this.genColumnExpression(node);
|
|
354
377
|
case 'ColumnAssignment': return this.genColumnAssignment(node);
|
|
355
378
|
case 'NegatedColumnExpression': return `{ __exclude: ${JSON.stringify(node.name)} }`;
|
|
379
|
+
case 'SpawnExpression': return this.genSpawnExpression(node);
|
|
356
380
|
default:
|
|
357
381
|
throw new Error(`Codegen: unknown expression type '${node.type}'`);
|
|
358
382
|
}
|
|
@@ -491,7 +515,9 @@ export class BaseCodegen {
|
|
|
491
515
|
return `${this.i()}const { ${props} } = ${this.genExpression(node.value)};`;
|
|
492
516
|
}
|
|
493
517
|
if (node.pattern.type === 'ArrayPattern' || node.pattern.type === 'TuplePattern') {
|
|
494
|
-
for (const e of node.pattern.elements)
|
|
518
|
+
for (const e of node.pattern.elements) {
|
|
519
|
+
if (e) this.declareVar(e.startsWith('...') ? e.slice(3) : e);
|
|
520
|
+
}
|
|
495
521
|
const els = node.pattern.elements.map(e => e || '').join(', ');
|
|
496
522
|
return `${this.i()}const [${els}] = ${this.genExpression(node.value)};`;
|
|
497
523
|
}
|
|
@@ -572,9 +598,14 @@ export class BaseCodegen {
|
|
|
572
598
|
// Track as user-defined to suppress stdlib version
|
|
573
599
|
if (BUILTIN_NAMES.has(node.name)) this._userDefinedNames.add(node.name);
|
|
574
600
|
const wasmBytes = compileWasmFunction(node);
|
|
575
|
-
|
|
601
|
+
// Track this WASM function for concurrent block routing
|
|
602
|
+
this._wasmFunctions.set(node.name, { node, wasmBytes });
|
|
576
603
|
const exportPrefix = node.isPublic ? 'export ' : '';
|
|
577
|
-
|
|
604
|
+
// Emit bytes constant first, then glue that references it (avoids duplication)
|
|
605
|
+
const bytesExport = generateWasmBytesExport(node.name, wasmBytes);
|
|
606
|
+
const name = node.name;
|
|
607
|
+
const glue = `const ${name} = new WebAssembly.Instance(new WebAssembly.Module(__wasm_bytes_${name})).exports.${name};`;
|
|
608
|
+
return `${this.i()}${bytesExport}\n${this.i()}${exportPrefix}${glue}`;
|
|
578
609
|
} catch (e) {
|
|
579
610
|
// Fall back to JS if WASM compilation fails
|
|
580
611
|
console.error(`Warning: @wasm compilation failed for '${node.name}': ${e.message}. Falling back to JS.`);
|
|
@@ -606,7 +637,14 @@ export class BaseCodegen {
|
|
|
606
637
|
}).join(', ');
|
|
607
638
|
}
|
|
608
639
|
|
|
640
|
+
_rewriteImportSource(node) {
|
|
641
|
+
if (node.source.startsWith('tova:')) {
|
|
642
|
+
node.source = './runtime/' + node.source.slice(5) + '.js';
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
609
646
|
genImport(node) {
|
|
647
|
+
this._rewriteImportSource(node);
|
|
610
648
|
for (const s of node.specifiers) this.declareVar(s.local);
|
|
611
649
|
const specs = node.specifiers.map(s => {
|
|
612
650
|
if (s.imported !== s.local) return `${s.imported} as ${s.local}`;
|
|
@@ -616,11 +654,13 @@ export class BaseCodegen {
|
|
|
616
654
|
}
|
|
617
655
|
|
|
618
656
|
genImportDefault(node) {
|
|
657
|
+
this._rewriteImportSource(node);
|
|
619
658
|
this.declareVar(node.local);
|
|
620
659
|
return `${this.i()}import ${node.local} from ${JSON.stringify(node.source)};`;
|
|
621
660
|
}
|
|
622
661
|
|
|
623
662
|
genImportWildcard(node) {
|
|
663
|
+
this._rewriteImportSource(node);
|
|
624
664
|
this.declareVar(node.local);
|
|
625
665
|
return `${this.i()}import * as ${node.local} from ${JSON.stringify(node.source)};`;
|
|
626
666
|
}
|
|
@@ -981,13 +1021,22 @@ export class BaseCodegen {
|
|
|
981
1021
|
}
|
|
982
1022
|
}
|
|
983
1023
|
|
|
1024
|
+
// Pre-scan for scalar-replaceable Result/Option variables
|
|
1025
|
+
const scalarMap = this._preAnalyzeScalarResults(regularStmts);
|
|
1026
|
+
const prevScalar = this._scalarReplacements;
|
|
1027
|
+
this._scalarReplacements = scalarMap.size > 0 ? new Map([...(prevScalar || new Map()), ...scalarMap]) : prevScalar;
|
|
1028
|
+
|
|
984
1029
|
for (let idx = 0; idx < regularStmts.length; idx++) {
|
|
985
1030
|
if (bodySkipSet.has(idx)) continue;
|
|
986
1031
|
const stmt = regularStmts[idx];
|
|
987
1032
|
const isLast = idx === regularStmts.length - 1;
|
|
1033
|
+
// Scalar replacement: emit boolean+value pair instead of Result/Option allocation
|
|
1034
|
+
if (stmt.type === 'Assignment' && stmt.targets.length === 1 && typeof stmt.targets[0] === 'string' &&
|
|
1035
|
+
scalarMap.has(stmt.targets[0]) && scalarMap.get(stmt.targets[0]).assignIdx === idx) {
|
|
1036
|
+
lines.push(this._genScalarAssignment(stmt, scalarMap.get(stmt.targets[0])));
|
|
988
1037
|
// Implicit return: last expression in function body
|
|
989
1038
|
// Skip implicit return for known void/side-effect-only calls (print, assert, etc.)
|
|
990
|
-
if (isLast && stmt.type === 'ExpressionStatement' && !this._isVoidCall(stmt.expression)) {
|
|
1039
|
+
} else if (isLast && stmt.type === 'ExpressionStatement' && !this._isVoidCall(stmt.expression)) {
|
|
991
1040
|
// IIFE elimination: match/if as last expression in function body → direct returns
|
|
992
1041
|
const expr = stmt.expression;
|
|
993
1042
|
if (expr.type === 'MatchExpression' && !this._isSimpleMatch(expr)) {
|
|
@@ -1006,6 +1055,8 @@ export class BaseCodegen {
|
|
|
1006
1055
|
}
|
|
1007
1056
|
}
|
|
1008
1057
|
|
|
1058
|
+
this._scalarReplacements = prevScalar;
|
|
1059
|
+
|
|
1009
1060
|
if (deferBodies.length > 0) {
|
|
1010
1061
|
this.indent--;
|
|
1011
1062
|
lines.push(`${this.i()}} finally {`);
|
|
@@ -1130,13 +1181,24 @@ export class BaseCodegen {
|
|
|
1130
1181
|
lines.push(fillResult);
|
|
1131
1182
|
}
|
|
1132
1183
|
}
|
|
1184
|
+
// Pre-scan for scalar-replaceable Result/Option variables
|
|
1185
|
+
const scalarMap = this._preAnalyzeScalarResults(stmts);
|
|
1186
|
+
const prevScalar = this._scalarReplacements;
|
|
1187
|
+
this._scalarReplacements = scalarMap.size > 0 ? new Map([...(prevScalar || new Map()), ...scalarMap]) : prevScalar;
|
|
1133
1188
|
for (let i = 0; i < stmts.length; i++) {
|
|
1134
1189
|
if (skipSet.has(i)) continue;
|
|
1135
1190
|
const s = stmts[i];
|
|
1136
|
-
|
|
1191
|
+
// Scalar replacement: emit boolean+value pair instead of Result/Option allocation
|
|
1192
|
+
if (s.type === 'Assignment' && s.targets.length === 1 && typeof s.targets[0] === 'string' &&
|
|
1193
|
+
scalarMap.has(s.targets[0]) && scalarMap.get(s.targets[0]).assignIdx === i) {
|
|
1194
|
+
lines.push(this._genScalarAssignment(s, scalarMap.get(s.targets[0])));
|
|
1195
|
+
} else {
|
|
1196
|
+
lines.push(this.generateStatement(s));
|
|
1197
|
+
}
|
|
1137
1198
|
// Dead code elimination: stop after unconditional return/break/continue
|
|
1138
1199
|
if (s.type === 'ReturnStatement' || s.type === 'BreakStatement' || s.type === 'ContinueStatement') break;
|
|
1139
1200
|
}
|
|
1201
|
+
this._scalarReplacements = prevScalar;
|
|
1140
1202
|
return lines.join('\n');
|
|
1141
1203
|
}
|
|
1142
1204
|
|
|
@@ -1433,6 +1495,41 @@ export class BaseCodegen {
|
|
|
1433
1495
|
const fusedResult = this._tryFuseMapChain(node);
|
|
1434
1496
|
if (fusedResult !== null) return fusedResult;
|
|
1435
1497
|
|
|
1498
|
+
// Compile-time devirtualization: Ok(x).unwrap() → x, Err(e).isOk() → false, etc.
|
|
1499
|
+
const devirt = this._tryDevirtualizeResultOption(node);
|
|
1500
|
+
if (devirt !== null) return devirt;
|
|
1501
|
+
|
|
1502
|
+
// Scalar replacement: intercept method calls on scalar-replaced Result/Option variables
|
|
1503
|
+
if (this._scalarReplacements && this._scalarReplacements.size > 0 &&
|
|
1504
|
+
node.callee.type === 'MemberExpression' && !node.callee.computed &&
|
|
1505
|
+
node.callee.object && node.callee.object.type === 'Identifier') {
|
|
1506
|
+
const varName = node.callee.object.name;
|
|
1507
|
+
const scalarInfo = this._scalarReplacements.get(varName);
|
|
1508
|
+
if (scalarInfo) {
|
|
1509
|
+
const okVar = `${varName}__ok`;
|
|
1510
|
+
const vVar = `${varName}__v`;
|
|
1511
|
+
const method = node.callee.property;
|
|
1512
|
+
|
|
1513
|
+
if (scalarInfo.kind === 'result') {
|
|
1514
|
+
switch (method) {
|
|
1515
|
+
case 'isOk': return okVar;
|
|
1516
|
+
case 'isErr': return `!${okVar}`;
|
|
1517
|
+
case 'unwrap': return vVar;
|
|
1518
|
+
case 'unwrapOr': return node.arguments.length > 0 ? `(${okVar} ? ${vVar} : ${this.genExpression(node.arguments[0])})` : vVar;
|
|
1519
|
+
case 'expect': return vVar;
|
|
1520
|
+
}
|
|
1521
|
+
} else if (scalarInfo.kind === 'option') {
|
|
1522
|
+
switch (method) {
|
|
1523
|
+
case 'isSome': return okVar;
|
|
1524
|
+
case 'isNone': return `!${okVar}`;
|
|
1525
|
+
case 'unwrap': return vVar;
|
|
1526
|
+
case 'unwrapOr': return node.arguments.length > 0 ? `(${okVar} ? ${vVar} : ${this.genExpression(node.arguments[0])})` : vVar;
|
|
1527
|
+
case 'expect': return vVar;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1436
1533
|
// Transform Foo.new(...) → new Foo(...)
|
|
1437
1534
|
if (node.callee.type === 'MemberExpression' && !node.callee.computed && node.callee.property === 'new') {
|
|
1438
1535
|
const obj = this.genExpression(node.callee.object);
|
|
@@ -1452,6 +1549,10 @@ export class BaseCodegen {
|
|
|
1452
1549
|
if (node.callee.name === 'iter') {
|
|
1453
1550
|
this._needsResultOption = true; // Seq.first()/find() return Option
|
|
1454
1551
|
}
|
|
1552
|
+
// LRUCache.get() returns Option — needs Result/Option
|
|
1553
|
+
if (node.callee.name === 'LRUCache') {
|
|
1554
|
+
this._needsResultOption = true;
|
|
1555
|
+
}
|
|
1455
1556
|
|
|
1456
1557
|
// Inline string/collection builtins to direct method calls
|
|
1457
1558
|
const inlined = this._tryInlineBuiltin(node);
|
|
@@ -1464,19 +1565,24 @@ export class BaseCodegen {
|
|
|
1464
1565
|
BUILTIN_NAMES.has(node.callee.object.name)) {
|
|
1465
1566
|
const ns = node.callee.object.name;
|
|
1466
1567
|
this._trackBuiltin(ns);
|
|
1467
|
-
// Namespaces that depend on Ok/Err need Result/Option
|
|
1568
|
+
// Namespaces that depend on Ok/Err/Some/None need Result/Option
|
|
1468
1569
|
const deps = STDLIB_DEPS[ns];
|
|
1469
|
-
if (deps && (deps.includes('Ok') || deps.includes('Err'))) {
|
|
1570
|
+
if (deps && (deps.includes('Ok') || deps.includes('Err') || deps.includes('Some') || deps.includes('None'))) {
|
|
1470
1571
|
this._needsResultOption = true;
|
|
1471
1572
|
}
|
|
1472
1573
|
}
|
|
1473
1574
|
|
|
1474
1575
|
// Check for table operation calls with column expressions
|
|
1475
1576
|
const hasColumnExprs = node.arguments.some(a => this._containsColumnExpr(a));
|
|
1476
|
-
if (hasColumnExprs || (node.callee.type === 'Identifier' && ['agg', 'table_agg'].includes(node.callee.name))) {
|
|
1577
|
+
if (hasColumnExprs || (node.callee.type === 'Identifier' && ['agg', 'table_agg', 'window', 'table_window'].includes(node.callee.name))) {
|
|
1477
1578
|
const tableArgs = this._genTableCallArgs(node);
|
|
1478
1579
|
if (tableArgs) {
|
|
1479
|
-
|
|
1580
|
+
let callee = this.genExpression(node.callee);
|
|
1581
|
+
// Remap window → table_window to avoid browser global conflict
|
|
1582
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'window') {
|
|
1583
|
+
callee = 'table_window';
|
|
1584
|
+
this._usedBuiltins.add('table_window');
|
|
1585
|
+
}
|
|
1480
1586
|
return `${callee}(${tableArgs.join(', ')})`;
|
|
1481
1587
|
}
|
|
1482
1588
|
}
|
|
@@ -1485,6 +1591,15 @@ export class BaseCodegen {
|
|
|
1485
1591
|
const hasNamedArgs = node.arguments.some(a => a.type === 'NamedArgument');
|
|
1486
1592
|
|
|
1487
1593
|
if (hasNamedArgs) {
|
|
1594
|
+
// Check if callee is a type/variant constructor — reorder named args to positional
|
|
1595
|
+
const calleeName = node.callee.type === 'Identifier' ? node.callee.name : null;
|
|
1596
|
+
const fieldOrder = calleeName ? this._variantFields[calleeName] : null;
|
|
1597
|
+
|
|
1598
|
+
if (fieldOrder) {
|
|
1599
|
+
const result = this._reorderNamedArgsToPositional(node.arguments, fieldOrder);
|
|
1600
|
+
return `${callee}(${result.join(', ')})`;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1488
1603
|
const allNamed = node.arguments.every(a => a.type === 'NamedArgument');
|
|
1489
1604
|
if (allNamed) {
|
|
1490
1605
|
// All named args → single object argument
|
|
@@ -1583,6 +1698,207 @@ export class BaseCodegen {
|
|
|
1583
1698
|
return result;
|
|
1584
1699
|
}
|
|
1585
1700
|
|
|
1701
|
+
// ─── Compile-time devirtualization for Result/Option ──────────
|
|
1702
|
+
// When the codegen sees Ok(val).method(), Err(val).method(), Some(val).method(),
|
|
1703
|
+
// or None.method(), it knows the exact type at compile time. Instead of allocating
|
|
1704
|
+
// a Result/Option object and calling a method, inline the method body directly.
|
|
1705
|
+
|
|
1706
|
+
_tryDevirtualizeResultOption(node) {
|
|
1707
|
+
// Must be a CallExpression where callee is a non-computed MemberExpression
|
|
1708
|
+
if (node.callee.type !== 'MemberExpression' || node.callee.computed) return null;
|
|
1709
|
+
const method = node.callee.property;
|
|
1710
|
+
const obj = node.callee.object;
|
|
1711
|
+
const args = node.arguments;
|
|
1712
|
+
|
|
1713
|
+
// Case 1: None.method()
|
|
1714
|
+
if (obj.type === 'Identifier' && obj.name === 'None') {
|
|
1715
|
+
return this._devirtNone(method, args);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// Case 2: Ok(val).method(), Err(val).method(), Some(val).method()
|
|
1719
|
+
if (obj.type === 'CallExpression' && obj.callee.type === 'Identifier' && obj.arguments.length === 1) {
|
|
1720
|
+
const ctor = obj.callee.name;
|
|
1721
|
+
const val = obj.arguments[0];
|
|
1722
|
+
if (ctor === 'Ok') return this._devirtOk(val, method, args);
|
|
1723
|
+
if (ctor === 'Err') return this._devirtErr(val, method, args);
|
|
1724
|
+
if (ctor === 'Some') return this._devirtSome(val, method, args);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
_devirtOk(val, method, args) {
|
|
1731
|
+
switch (method) {
|
|
1732
|
+
case 'unwrap':
|
|
1733
|
+
case 'expect':
|
|
1734
|
+
return this.genExpression(val);
|
|
1735
|
+
case 'unwrapOr':
|
|
1736
|
+
return this.genExpression(val); // Ok ignores default
|
|
1737
|
+
case 'isOk':
|
|
1738
|
+
return 'true';
|
|
1739
|
+
case 'isErr':
|
|
1740
|
+
return 'false';
|
|
1741
|
+
case 'map': {
|
|
1742
|
+
if (!this._isSimpleLambda(args[0])) return null;
|
|
1743
|
+
const parts = this._extractLambdaParts(args[0]);
|
|
1744
|
+
if (!parts) return null;
|
|
1745
|
+
this._needsResultOption = true;
|
|
1746
|
+
return `Ok(${this._substituteParam(parts.bodyExpr, parts.paramName, this.genExpression(val))})`;
|
|
1747
|
+
}
|
|
1748
|
+
case 'flatMap': {
|
|
1749
|
+
if (!this._isSimpleLambda(args[0])) return null;
|
|
1750
|
+
const parts = this._extractLambdaParts(args[0]);
|
|
1751
|
+
if (!parts) return null;
|
|
1752
|
+
return this._substituteParam(parts.bodyExpr, parts.paramName, this.genExpression(val));
|
|
1753
|
+
}
|
|
1754
|
+
case 'mapErr':
|
|
1755
|
+
this._needsResultOption = true;
|
|
1756
|
+
return `Ok(${this.genExpression(val)})`; // passes through
|
|
1757
|
+
case 'or':
|
|
1758
|
+
this._needsResultOption = true;
|
|
1759
|
+
return `Ok(${this.genExpression(val)})`; // passes through
|
|
1760
|
+
case 'and':
|
|
1761
|
+
if (args[0]) return this.genExpression(args[0]);
|
|
1762
|
+
return null;
|
|
1763
|
+
default:
|
|
1764
|
+
return null;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
_devirtErr(val, method, args) {
|
|
1769
|
+
switch (method) {
|
|
1770
|
+
case 'unwrapOr':
|
|
1771
|
+
if (args[0]) return this.genExpression(args[0]);
|
|
1772
|
+
return null;
|
|
1773
|
+
case 'isOk':
|
|
1774
|
+
return 'false';
|
|
1775
|
+
case 'isErr':
|
|
1776
|
+
return 'true';
|
|
1777
|
+
case 'unwrapErr':
|
|
1778
|
+
return this.genExpression(val);
|
|
1779
|
+
case 'map':
|
|
1780
|
+
this._needsResultOption = true;
|
|
1781
|
+
return `Err(${this.genExpression(val)})`; // passes through
|
|
1782
|
+
case 'flatMap':
|
|
1783
|
+
this._needsResultOption = true;
|
|
1784
|
+
return `Err(${this.genExpression(val)})`;
|
|
1785
|
+
case 'mapErr': {
|
|
1786
|
+
if (!this._isSimpleLambda(args[0])) return null;
|
|
1787
|
+
const parts = this._extractLambdaParts(args[0]);
|
|
1788
|
+
if (!parts) return null;
|
|
1789
|
+
this._needsResultOption = true;
|
|
1790
|
+
return `Err(${this._substituteParam(parts.bodyExpr, parts.paramName, this.genExpression(val))})`;
|
|
1791
|
+
}
|
|
1792
|
+
case 'or':
|
|
1793
|
+
if (args[0]) return this.genExpression(args[0]);
|
|
1794
|
+
return null;
|
|
1795
|
+
case 'and':
|
|
1796
|
+
this._needsResultOption = true;
|
|
1797
|
+
return `Err(${this.genExpression(val)})`;
|
|
1798
|
+
default:
|
|
1799
|
+
return null;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
_devirtSome(val, method, args) {
|
|
1804
|
+
switch (method) {
|
|
1805
|
+
case 'unwrap':
|
|
1806
|
+
case 'expect':
|
|
1807
|
+
return this.genExpression(val);
|
|
1808
|
+
case 'unwrapOr':
|
|
1809
|
+
return this.genExpression(val); // Some ignores default
|
|
1810
|
+
case 'isSome':
|
|
1811
|
+
return 'true';
|
|
1812
|
+
case 'isNone':
|
|
1813
|
+
return 'false';
|
|
1814
|
+
case 'map': {
|
|
1815
|
+
if (!this._isSimpleLambda(args[0])) return null;
|
|
1816
|
+
const parts = this._extractLambdaParts(args[0]);
|
|
1817
|
+
if (!parts) return null;
|
|
1818
|
+
this._needsResultOption = true;
|
|
1819
|
+
return `Some(${this._substituteParam(parts.bodyExpr, parts.paramName, this.genExpression(val))})`;
|
|
1820
|
+
}
|
|
1821
|
+
case 'flatMap': {
|
|
1822
|
+
if (!this._isSimpleLambda(args[0])) return null;
|
|
1823
|
+
const parts = this._extractLambdaParts(args[0]);
|
|
1824
|
+
if (!parts) return null;
|
|
1825
|
+
return this._substituteParam(parts.bodyExpr, parts.paramName, this.genExpression(val));
|
|
1826
|
+
}
|
|
1827
|
+
case 'filter': {
|
|
1828
|
+
if (!this._isSimpleLambda(args[0])) return null;
|
|
1829
|
+
const parts = this._extractLambdaParts(args[0]);
|
|
1830
|
+
if (!parts) return null;
|
|
1831
|
+
this._needsResultOption = true;
|
|
1832
|
+
const valCode = this.genExpression(val);
|
|
1833
|
+
const predCode = this._substituteParam(parts.bodyExpr, parts.paramName, valCode);
|
|
1834
|
+
return `(${predCode} ? Some(${valCode}) : None)`;
|
|
1835
|
+
}
|
|
1836
|
+
case 'or':
|
|
1837
|
+
this._needsResultOption = true;
|
|
1838
|
+
return `Some(${this.genExpression(val)})`; // passes through
|
|
1839
|
+
case 'and':
|
|
1840
|
+
if (args[0]) return this.genExpression(args[0]);
|
|
1841
|
+
return null;
|
|
1842
|
+
default:
|
|
1843
|
+
return null;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
_devirtNone(method, args) {
|
|
1848
|
+
switch (method) {
|
|
1849
|
+
case 'unwrapOr':
|
|
1850
|
+
if (args[0]) return this.genExpression(args[0]);
|
|
1851
|
+
return null;
|
|
1852
|
+
case 'isSome':
|
|
1853
|
+
return 'false';
|
|
1854
|
+
case 'isNone':
|
|
1855
|
+
return 'true';
|
|
1856
|
+
case 'map':
|
|
1857
|
+
this._needsResultOption = true;
|
|
1858
|
+
return 'None';
|
|
1859
|
+
case 'flatMap':
|
|
1860
|
+
this._needsResultOption = true;
|
|
1861
|
+
return 'None';
|
|
1862
|
+
case 'filter':
|
|
1863
|
+
this._needsResultOption = true;
|
|
1864
|
+
return 'None';
|
|
1865
|
+
case 'or':
|
|
1866
|
+
if (args[0]) return this.genExpression(args[0]);
|
|
1867
|
+
return null;
|
|
1868
|
+
case 'and':
|
|
1869
|
+
this._needsResultOption = true;
|
|
1870
|
+
return 'None';
|
|
1871
|
+
default:
|
|
1872
|
+
return null;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
_isSimpleLambda(node) {
|
|
1877
|
+
if (!node) return false;
|
|
1878
|
+
if (node.type !== 'FunctionExpression' && node.type !== 'ArrowFunction' && node.type !== 'LambdaExpression') return false;
|
|
1879
|
+
const params = node.params || [];
|
|
1880
|
+
if (params.length !== 1) return false;
|
|
1881
|
+
return true;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
_extractLambdaParts(lambda) {
|
|
1885
|
+
const params = lambda.params || [];
|
|
1886
|
+
const paramName = typeof params[0] === 'string' ? params[0] : (params[0].name || null);
|
|
1887
|
+
if (!paramName) return null;
|
|
1888
|
+
let bodyExpr = lambda.body;
|
|
1889
|
+
if (bodyExpr && bodyExpr.type === 'BlockStatement') {
|
|
1890
|
+
if (bodyExpr.body.length === 1) {
|
|
1891
|
+
const s = bodyExpr.body[0];
|
|
1892
|
+
if (s.type === 'ExpressionStatement') bodyExpr = s.expression;
|
|
1893
|
+
else if (s.type === 'ReturnStatement' && s.value) bodyExpr = s.value;
|
|
1894
|
+
else return null;
|
|
1895
|
+
} else {
|
|
1896
|
+
return null;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
return { paramName, bodyExpr };
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1586
1902
|
// Inline known builtins to direct method calls, eliminating wrapper overhead.
|
|
1587
1903
|
// Returns the inlined code string, or null if not inlineable.
|
|
1588
1904
|
_tryInlineBuiltin(node) {
|
|
@@ -1699,13 +2015,18 @@ export class BaseCodegen {
|
|
|
1699
2015
|
if (right.type === 'CallExpression') {
|
|
1700
2016
|
// Check for table operations with column expressions
|
|
1701
2017
|
const hasColumnExprs = right.arguments.some(a => this._containsColumnExpr(a));
|
|
1702
|
-
if (hasColumnExprs || (right.callee.type === 'Identifier' && ['agg', 'table_agg'].includes(right.callee.name))) {
|
|
2018
|
+
if (hasColumnExprs || (right.callee.type === 'Identifier' && ['agg', 'table_agg', 'window', 'table_window'].includes(right.callee.name))) {
|
|
1703
2019
|
const tableArgs = this._genTableCallArgs(right);
|
|
1704
2020
|
if (tableArgs) {
|
|
1705
|
-
|
|
1706
|
-
//
|
|
2021
|
+
let callee = this.genExpression(right.callee);
|
|
2022
|
+
// Remap window → table_window to avoid browser global conflict
|
|
2023
|
+
if (right.callee.type === 'Identifier' && right.callee.name === 'window') {
|
|
2024
|
+
callee = 'table_window';
|
|
2025
|
+
this._usedBuiltins.add('table_window');
|
|
2026
|
+
}
|
|
2027
|
+
// Track builtin usage (with dependency resolution)
|
|
1707
2028
|
if (right.callee.type === 'Identifier' && BUILTIN_NAMES.has(right.callee.name)) {
|
|
1708
|
-
this.
|
|
2029
|
+
this._trackBuiltin(right.callee.name);
|
|
1709
2030
|
}
|
|
1710
2031
|
return `${callee}(${[left, ...tableArgs].join(', ')})`;
|
|
1711
2032
|
}
|
|
@@ -1857,6 +2178,33 @@ export class BaseCodegen {
|
|
|
1857
2178
|
return this.genExpression(node);
|
|
1858
2179
|
}
|
|
1859
2180
|
|
|
2181
|
+
// Reorder named args to positional order for type/variant constructors
|
|
2182
|
+
_reorderNamedArgsToPositional(args, fieldOrder) {
|
|
2183
|
+
const slots = new Array(fieldOrder.length);
|
|
2184
|
+
let positionalIndex = 0;
|
|
2185
|
+
// First pass: place positional args in order
|
|
2186
|
+
for (const arg of args) {
|
|
2187
|
+
if (arg.type !== 'NamedArgument') {
|
|
2188
|
+
slots[positionalIndex] = this.genExpression(arg);
|
|
2189
|
+
positionalIndex++;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
// Second pass: place named args in their field's slot
|
|
2193
|
+
for (const arg of args) {
|
|
2194
|
+
if (arg.type === 'NamedArgument') {
|
|
2195
|
+
const idx = fieldOrder.indexOf(arg.name);
|
|
2196
|
+
if (idx !== -1) {
|
|
2197
|
+
slots[idx] = this.genExpression(arg.value);
|
|
2198
|
+
} else {
|
|
2199
|
+
// Unknown field — emit as-is (analyzer will warn)
|
|
2200
|
+
slots[positionalIndex] = this.genExpression(arg.value);
|
|
2201
|
+
positionalIndex++;
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
return slots.filter(s => s !== undefined);
|
|
2206
|
+
}
|
|
2207
|
+
|
|
1860
2208
|
// Override genCallExpression to handle table operations with column expressions
|
|
1861
2209
|
_genTableCallArgs(node) {
|
|
1862
2210
|
const calleeName = node.callee.type === 'Identifier' ? node.callee.name : null;
|
|
@@ -1955,6 +2303,52 @@ export class BaseCodegen {
|
|
|
1955
2303
|
return parts;
|
|
1956
2304
|
}
|
|
1957
2305
|
|
|
2306
|
+
// window() / table_window() — partition_by/order_by/desc are meta, rest are window fns
|
|
2307
|
+
if (calleeName === 'window' || calleeName === 'table_window') {
|
|
2308
|
+
const META_KEYS = new Set(['partition_by', 'order_by', 'desc']);
|
|
2309
|
+
const optParts = [];
|
|
2310
|
+
const winParts = [];
|
|
2311
|
+
for (const a of node.arguments) {
|
|
2312
|
+
if (a.type === 'NamedArgument') {
|
|
2313
|
+
if (META_KEYS.has(a.name)) {
|
|
2314
|
+
// Meta keys: partition_by → partition, order_by → order
|
|
2315
|
+
const key = a.name === 'partition_by' ? 'partition' : a.name === 'order_by' ? 'order' : a.name;
|
|
2316
|
+
if (a.name === 'desc') {
|
|
2317
|
+
optParts.push(`desc: ${this.genExpression(a.value)}`);
|
|
2318
|
+
} else if (this._containsColumnExpr(a.value)) {
|
|
2319
|
+
optParts.push(`${key}: (__row) => ${this._genColumnBody(a.value)}`);
|
|
2320
|
+
} else {
|
|
2321
|
+
optParts.push(`${key}: ${this.genExpression(a.value)}`);
|
|
2322
|
+
}
|
|
2323
|
+
} else {
|
|
2324
|
+
// Window function column
|
|
2325
|
+
const val = a.value;
|
|
2326
|
+
if (val.type === 'CallExpression' && val.callee.type === 'Identifier') {
|
|
2327
|
+
const fnName = val.callee.name;
|
|
2328
|
+
const winFn = `win_${fnName}`;
|
|
2329
|
+
const WIN_FNS = ['row_number', 'rank', 'dense_rank', 'percent_rank', 'ntile',
|
|
2330
|
+
'lag', 'lead', 'first_value', 'last_value',
|
|
2331
|
+
'running_sum', 'running_count', 'running_avg', 'running_min', 'running_max',
|
|
2332
|
+
'moving_avg'];
|
|
2333
|
+
if (WIN_FNS.includes(fnName)) {
|
|
2334
|
+
this._usedBuiltins.add(winFn);
|
|
2335
|
+
const innerArgs = val.arguments.map(ia => {
|
|
2336
|
+
if (this._containsColumnExpr(ia)) {
|
|
2337
|
+
return `(__row) => ${this._genColumnBody(ia)}`;
|
|
2338
|
+
}
|
|
2339
|
+
return this.genExpression(ia);
|
|
2340
|
+
});
|
|
2341
|
+
winParts.push(`${a.name}: ${winFn}(${innerArgs.join(', ')})`);
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
winParts.push(`${a.name}: ${this.genExpression(a.value)}`);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
return [`{ ${optParts.join(', ')} }`, `{ ${winParts.join(', ')} }`];
|
|
2350
|
+
}
|
|
2351
|
+
|
|
1958
2352
|
// drop_nil/fill_nil — column expression compiles to string or lambda
|
|
1959
2353
|
if (calleeName === 'drop_nil' || calleeName === 'fill_nil') {
|
|
1960
2354
|
return node.arguments.map(a => {
|
|
@@ -2185,15 +2579,23 @@ export class BaseCodegen {
|
|
|
2185
2579
|
|
|
2186
2580
|
if (arm.pattern.type === 'WildcardPattern' || arm.pattern.type === 'BindingPattern') {
|
|
2187
2581
|
if (idx === node.arms.length - 1 && !arm.guard) {
|
|
2188
|
-
// Default case
|
|
2582
|
+
// Default case — wrap in else block if preceded by if/else-if arms
|
|
2583
|
+
if (idx > 0) {
|
|
2584
|
+
p.push(`${this.i()}else {\n`);
|
|
2585
|
+
this.indent++;
|
|
2586
|
+
}
|
|
2189
2587
|
if (arm.pattern.type === 'BindingPattern') {
|
|
2190
2588
|
p.push(`${this.i()}const ${arm.pattern.name} = ${tempVar};\n`);
|
|
2191
2589
|
}
|
|
2192
2590
|
if (arm.body.type === 'BlockStatement') {
|
|
2193
|
-
p.push(this.
|
|
2591
|
+
p.push(this._genBlockBodyReturns(arm.body) + '\n');
|
|
2194
2592
|
} else {
|
|
2195
2593
|
p.push(`${this.i()}return ${this.genExpression(arm.body)};\n`);
|
|
2196
2594
|
}
|
|
2595
|
+
if (idx > 0) {
|
|
2596
|
+
this.indent--;
|
|
2597
|
+
p.push(`${this.i()}}\n`);
|
|
2598
|
+
}
|
|
2197
2599
|
break;
|
|
2198
2600
|
}
|
|
2199
2601
|
}
|
|
@@ -2206,7 +2608,7 @@ export class BaseCodegen {
|
|
|
2206
2608
|
p.push(this.genPatternBindings(arm.pattern, tempVar));
|
|
2207
2609
|
|
|
2208
2610
|
if (arm.body.type === 'BlockStatement') {
|
|
2209
|
-
p.push(this.
|
|
2611
|
+
p.push(this._genBlockBodyReturns(arm.body) + '\n');
|
|
2210
2612
|
} else {
|
|
2211
2613
|
p.push(`${this.i()}return ${this.genExpression(arm.body)};\n`);
|
|
2212
2614
|
}
|
|
@@ -2613,13 +3015,53 @@ export class BaseCodegen {
|
|
|
2613
3015
|
if (pattern.type === 'BindingPattern') {
|
|
2614
3016
|
cond = `((${pattern.name}) => ${this.genExpression(guard)})(${subject})`;
|
|
2615
3017
|
} else {
|
|
2616
|
-
|
|
3018
|
+
// Collect bindings from sub-patterns (e.g. [1, n] if n < 2)
|
|
3019
|
+
const bindings = this._collectPatternBindings(pattern, subject);
|
|
3020
|
+
if (bindings.length > 0) {
|
|
3021
|
+
const params = bindings.map(b => b.name).join(', ');
|
|
3022
|
+
const args = bindings.map(b => b.accessor).join(', ');
|
|
3023
|
+
cond = `(${cond}) && ((${params}) => ${this.genExpression(guard)})(${args})`;
|
|
3024
|
+
} else {
|
|
3025
|
+
cond = `(${cond}) && (${this.genExpression(guard)})`;
|
|
3026
|
+
}
|
|
2617
3027
|
}
|
|
2618
3028
|
}
|
|
2619
3029
|
|
|
2620
3030
|
return cond;
|
|
2621
3031
|
}
|
|
2622
3032
|
|
|
3033
|
+
_collectPatternBindings(pattern, subject) {
|
|
3034
|
+
const bindings = [];
|
|
3035
|
+
switch (pattern.type) {
|
|
3036
|
+
case 'BindingPattern':
|
|
3037
|
+
bindings.push({ name: pattern.name, accessor: subject });
|
|
3038
|
+
break;
|
|
3039
|
+
case 'ArrayPattern':
|
|
3040
|
+
case 'TuplePattern':
|
|
3041
|
+
for (let i = 0; i < pattern.elements.length; i++) {
|
|
3042
|
+
const el = pattern.elements[i];
|
|
3043
|
+
if (el) bindings.push(...this._collectPatternBindings(el, `${subject}[${i}]`));
|
|
3044
|
+
}
|
|
3045
|
+
break;
|
|
3046
|
+
case 'VariantPattern': {
|
|
3047
|
+
const declaredFields = this._variantFields[pattern.name] || [];
|
|
3048
|
+
for (let i = 0; i < pattern.fields.length; i++) {
|
|
3049
|
+
const f = pattern.fields[i];
|
|
3050
|
+
const fieldName = typeof f === 'string' ? f : (f.type === 'BindingPattern' ? f.name : null);
|
|
3051
|
+
const propName = declaredFields[i] || fieldName || 'value';
|
|
3052
|
+
const accessor = `${subject}.${propName}`;
|
|
3053
|
+
if (typeof f === 'string') {
|
|
3054
|
+
bindings.push({ name: f, accessor });
|
|
3055
|
+
} else {
|
|
3056
|
+
bindings.push(...this._collectPatternBindings(f, accessor));
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
break;
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
return bindings;
|
|
3063
|
+
}
|
|
3064
|
+
|
|
2623
3065
|
genPatternBindings(pattern, subject) {
|
|
2624
3066
|
switch (pattern.type) {
|
|
2625
3067
|
case 'BindingPattern':
|
|
@@ -2819,6 +3261,7 @@ export class BaseCodegen {
|
|
|
2819
3261
|
} else {
|
|
2820
3262
|
this.declareVar(node.name);
|
|
2821
3263
|
const fieldNames = node.variants.map(f => f.name);
|
|
3264
|
+
this._variantFields[node.name] = fieldNames;
|
|
2822
3265
|
const params = fieldNames.join(', ');
|
|
2823
3266
|
const obj = fieldNames.map(f => `${f}`).join(', ');
|
|
2824
3267
|
lines.push(`${this.i()}${exportPrefix}function ${node.name}(${params}) { return Object.assign(Object.create(${node.name}.prototype), { ${obj} }); }`);
|
|
@@ -3013,6 +3456,395 @@ export class BaseCodegen {
|
|
|
3013
3456
|
return p.join('\n');
|
|
3014
3457
|
}
|
|
3015
3458
|
|
|
3459
|
+
genConcurrentBlock(node) {
|
|
3460
|
+
const lines = [];
|
|
3461
|
+
const tasks = []; // { callCode, calleeName, spawn, isWasm }
|
|
3462
|
+
const assignments = [];
|
|
3463
|
+
|
|
3464
|
+
// Collect spawn tasks from body, preserving spawn node info
|
|
3465
|
+
for (const stmt of node.body) {
|
|
3466
|
+
if (stmt.type === 'Assignment' && stmt.values && stmt.values[0] && stmt.values[0].type === 'SpawnExpression') {
|
|
3467
|
+
const spawn = stmt.values[0];
|
|
3468
|
+
const calleeName = spawn.callee && spawn.callee.type === 'Identifier' ? spawn.callee.name : null;
|
|
3469
|
+
const callCode = `${this.genExpression(spawn.callee)}(${spawn.arguments.map(a => this.genExpression(a)).join(', ')})`;
|
|
3470
|
+
const isWasm = calleeName && this._wasmFunctions.has(calleeName);
|
|
3471
|
+
tasks.push({ callCode, calleeName, spawn, isWasm });
|
|
3472
|
+
assignments.push(stmt.targets[0]); // string variable name
|
|
3473
|
+
} else if (stmt.type === 'ExpressionStatement' && stmt.expression && stmt.expression.type === 'SpawnExpression') {
|
|
3474
|
+
const spawn = stmt.expression;
|
|
3475
|
+
const calleeName = spawn.callee && spawn.callee.type === 'Identifier' ? spawn.callee.name : null;
|
|
3476
|
+
const callCode = `${this.genExpression(spawn.callee)}(${spawn.arguments.map(a => this.genExpression(a)).join(', ')})`;
|
|
3477
|
+
const isWasm = calleeName && this._wasmFunctions.has(calleeName);
|
|
3478
|
+
tasks.push({ callCode, calleeName, spawn, isWasm });
|
|
3479
|
+
assignments.push(null); // fire-and-forget
|
|
3480
|
+
} else {
|
|
3481
|
+
// Non-spawn statement — generate normally
|
|
3482
|
+
lines.push(this.generateStatement(stmt));
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
this._needsResultOption = true;
|
|
3487
|
+
|
|
3488
|
+
if (tasks.length === 0) {
|
|
3489
|
+
return lines.join('\n');
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
const base = this._concurrentCounter = (this._concurrentCounter || 0) + 1;
|
|
3493
|
+
const tempVars = tasks.map((_, i) => `__c${base}_${i}`);
|
|
3494
|
+
|
|
3495
|
+
// Classify tasks
|
|
3496
|
+
const wasmTasks = tasks.filter(t => t.isWasm);
|
|
3497
|
+
const allWasm = wasmTasks.length === tasks.length;
|
|
3498
|
+
const hasWasm = wasmTasks.length > 0;
|
|
3499
|
+
|
|
3500
|
+
// Check if all WASM tasks share the same function (shared module optimization)
|
|
3501
|
+
const allSameWasm = allWasm && wasmTasks.every(t => t.calleeName === wasmTasks[0].calleeName);
|
|
3502
|
+
|
|
3503
|
+
// --- WASM path: all tasks are @wasm functions ---
|
|
3504
|
+
if (allWasm && (node.mode === 'all' || node.mode === 'first' || node.mode === 'cancel_on_error' || (node.mode === 'timeout' && node.timeout))) {
|
|
3505
|
+
this._needsRuntimeBridge = true;
|
|
3506
|
+
const rtVar = `__tova_rt`;
|
|
3507
|
+
// Build task descriptors
|
|
3508
|
+
const taskDescs = tasks.map(t => {
|
|
3509
|
+
const argsCode = t.spawn.arguments.map(a => this.genExpression(a)).join(', ');
|
|
3510
|
+
return `{ wasm: __wasm_bytes_${t.calleeName}, func: ${JSON.stringify(t.calleeName)}, args: [${argsCode}] }`;
|
|
3511
|
+
});
|
|
3512
|
+
const taskArrayCode = `[${taskDescs.join(', ')}]`;
|
|
3513
|
+
|
|
3514
|
+
// Choose runtime function based on mode
|
|
3515
|
+
let rtFunc;
|
|
3516
|
+
let rtCallSuffix = '';
|
|
3517
|
+
if (node.mode === 'first') {
|
|
3518
|
+
rtFunc = 'concurrentWasmFirst';
|
|
3519
|
+
} else if (node.mode === 'cancel_on_error') {
|
|
3520
|
+
rtFunc = 'concurrentWasmCancelOnError';
|
|
3521
|
+
} else if (node.mode === 'timeout' && node.timeout) {
|
|
3522
|
+
rtFunc = 'concurrentWasmTimeout';
|
|
3523
|
+
rtCallSuffix = `, ${this.genExpression(node.timeout)}`;
|
|
3524
|
+
} else {
|
|
3525
|
+
rtFunc = allSameWasm ? 'concurrentWasmShared' : 'concurrentWasm';
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
if (node.mode === 'first') {
|
|
3529
|
+
// First mode: single result from WASM, fallback to Promise.race
|
|
3530
|
+
const firstVar = `__first${base}`;
|
|
3531
|
+
lines.push(`${this.i()}let ${firstVar};`);
|
|
3532
|
+
lines.push(`${this.i()}if (typeof ${rtVar} !== 'undefined' && ${rtVar} && ${rtVar}.isRuntimeAvailable()) {`);
|
|
3533
|
+
lines.push(`${this.i()} ${firstVar} = { __result: new Ok(await ${rtVar}.${rtFunc}(${taskArrayCode})) };`);
|
|
3534
|
+
lines.push(`${this.i()}} else {`);
|
|
3535
|
+
// Fallback: Promise.race
|
|
3536
|
+
const acVar = `__ac${base}`;
|
|
3537
|
+
lines.push(`${this.i()} const ${acVar} = new AbortController();`);
|
|
3538
|
+
const fallbackFirst = tasks.map((t, i) =>
|
|
3539
|
+
`(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) }; } })()`
|
|
3540
|
+
);
|
|
3541
|
+
lines.push(`${this.i()} ${firstVar} = await Promise.race([`);
|
|
3542
|
+
for (let i = 0; i < fallbackFirst.length; i++) {
|
|
3543
|
+
lines.push(`${this.i()} ${fallbackFirst[i]}${i < fallbackFirst.length - 1 ? ',' : ''}`);
|
|
3544
|
+
}
|
|
3545
|
+
lines.push(`${this.i()} ]);`);
|
|
3546
|
+
lines.push(`${this.i()}}`);
|
|
3547
|
+
// Assign winner to all named variables
|
|
3548
|
+
for (let i = 0; i < assignments.length; i++) {
|
|
3549
|
+
if (assignments[i]) {
|
|
3550
|
+
this.declareVar(assignments[i]);
|
|
3551
|
+
lines.push(`${this.i()}const ${assignments[i]} = ${firstVar}.__result;`);
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
return lines.join('\n');
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
// All other WASM modes: array result
|
|
3558
|
+
lines.push(`${this.i()}let ${tempVars.join(', ')};`);
|
|
3559
|
+
lines.push(`${this.i()}if (typeof ${rtVar} !== 'undefined' && ${rtVar} && ${rtVar}.isRuntimeAvailable()) {`);
|
|
3560
|
+
if (node.mode === 'cancel_on_error') {
|
|
3561
|
+
// cancel_on_error: WASM function throws on first task error — wrap with try/catch
|
|
3562
|
+
// to provide per-task Err results consistent with JS fallback path
|
|
3563
|
+
lines.push(`${this.i()} try {`);
|
|
3564
|
+
lines.push(`${this.i()} const __wasm_results = await ${rtVar}.${rtFunc}(${taskArrayCode});`);
|
|
3565
|
+
lines.push(`${this.i()} [${tempVars.join(', ')}] = __wasm_results.map(__v => new Ok(__v));`);
|
|
3566
|
+
lines.push(`${this.i()} } catch (__wasm_err) {`);
|
|
3567
|
+
lines.push(`${this.i()} [${tempVars.join(', ')}] = Array(${tasks.length}).fill(new Err(__wasm_err));`);
|
|
3568
|
+
lines.push(`${this.i()} }`);
|
|
3569
|
+
} else if (node.mode === 'timeout' && node.timeout) {
|
|
3570
|
+
// timeout: WASM function throws on timeout — wrap with try/catch
|
|
3571
|
+
lines.push(`${this.i()} try {`);
|
|
3572
|
+
lines.push(`${this.i()} const __wasm_results = await ${rtVar}.${rtFunc}(${taskArrayCode}${rtCallSuffix});`);
|
|
3573
|
+
lines.push(`${this.i()} [${tempVars.join(', ')}] = __wasm_results.map(__v => new Ok(__v));`);
|
|
3574
|
+
lines.push(`${this.i()} } catch (__wasm_err) {`);
|
|
3575
|
+
lines.push(`${this.i()} [${tempVars.join(', ')}] = Array(${tasks.length}).fill(new Err(__wasm_err));`);
|
|
3576
|
+
lines.push(`${this.i()} }`);
|
|
3577
|
+
} else {
|
|
3578
|
+
lines.push(`${this.i()} const __wasm_results = await ${rtVar}.${rtFunc}(${taskArrayCode}${rtCallSuffix});`);
|
|
3579
|
+
lines.push(`${this.i()} [${tempVars.join(', ')}] = __wasm_results.map(__v => new Ok(__v));`);
|
|
3580
|
+
}
|
|
3581
|
+
lines.push(`${this.i()}} else {`);
|
|
3582
|
+
// Fallback: standard Promise.all path
|
|
3583
|
+
if (node.mode === 'cancel_on_error') {
|
|
3584
|
+
const acVar = `__ac${base}`;
|
|
3585
|
+
const abortedVar = `__aborted${base}`;
|
|
3586
|
+
lines.push(`${this.i()} const ${acVar} = new AbortController();`);
|
|
3587
|
+
lines.push(`${this.i()} const ${abortedVar} = new Promise((_, reject) => { ${acVar}.signal.addEventListener('abort', () => reject(new Error('cancelled')), { once: true }); });`);
|
|
3588
|
+
const fallbackExprs = tasks.map(t =>
|
|
3589
|
+
`(async () => { try { return new Ok(await Promise.race([${t.callCode}, ${abortedVar}])); } catch(__e) { if (!${acVar}.signal.aborted) ${acVar}.abort(); return new Err(__e); } })()`
|
|
3590
|
+
);
|
|
3591
|
+
lines.push(`${this.i()} [${tempVars.join(', ')}] = await Promise.all([`);
|
|
3592
|
+
for (let i = 0; i < fallbackExprs.length; i++) {
|
|
3593
|
+
lines.push(`${this.i()} ${fallbackExprs[i]}${i < fallbackExprs.length - 1 ? ',' : ''}`);
|
|
3594
|
+
}
|
|
3595
|
+
lines.push(`${this.i()} ]);`);
|
|
3596
|
+
} else if (node.mode === 'timeout' && node.timeout) {
|
|
3597
|
+
const timeoutMs = this.genExpression(node.timeout);
|
|
3598
|
+
const acVar = `__ac${base}`;
|
|
3599
|
+
const abortedVar = `__aborted${base}`;
|
|
3600
|
+
const timerVar = `__timer${base}`;
|
|
3601
|
+
lines.push(`${this.i()} const ${acVar} = new AbortController();`);
|
|
3602
|
+
lines.push(`${this.i()} const ${abortedVar} = new Promise((_, reject) => { ${acVar}.signal.addEventListener('abort', () => reject(new Error('concurrent timeout')), { once: true }); });`);
|
|
3603
|
+
lines.push(`${this.i()} const ${timerVar} = setTimeout(() => ${acVar}.abort(), ${timeoutMs});`);
|
|
3604
|
+
const fallbackExprs = tasks.map(t =>
|
|
3605
|
+
`(async () => { try { return new Ok(await Promise.race([${t.callCode}, ${abortedVar}])); } catch(__e) { return new Err(__e); } })()`
|
|
3606
|
+
);
|
|
3607
|
+
lines.push(`${this.i()} [${tempVars.join(', ')}] = await Promise.all([`);
|
|
3608
|
+
for (let i = 0; i < fallbackExprs.length; i++) {
|
|
3609
|
+
lines.push(`${this.i()} ${fallbackExprs[i]}${i < fallbackExprs.length - 1 ? ',' : ''}`);
|
|
3610
|
+
}
|
|
3611
|
+
lines.push(`${this.i()} ]);`);
|
|
3612
|
+
lines.push(`${this.i()} clearTimeout(${timerVar});`);
|
|
3613
|
+
} else {
|
|
3614
|
+
const fallbackExprs = tasks.map(t =>
|
|
3615
|
+
`(async () => { try { return new Ok(await ${t.callCode}); } catch(__e) { return new Err(__e); } })()`
|
|
3616
|
+
);
|
|
3617
|
+
lines.push(`${this.i()} [${tempVars.join(', ')}] = await Promise.all([`);
|
|
3618
|
+
for (let i = 0; i < fallbackExprs.length; i++) {
|
|
3619
|
+
lines.push(`${this.i()} ${fallbackExprs[i]}${i < fallbackExprs.length - 1 ? ',' : ''}`);
|
|
3620
|
+
}
|
|
3621
|
+
lines.push(`${this.i()} ]);`);
|
|
3622
|
+
}
|
|
3623
|
+
lines.push(`${this.i()}}`);
|
|
3624
|
+
} else {
|
|
3625
|
+
// --- JS path (with per-task WASM routing for mixed blocks) ---
|
|
3626
|
+
const taskExprs = tasks.map(t => {
|
|
3627
|
+
if (t.isWasm && hasWasm) {
|
|
3628
|
+
// Route individual WASM task through runtime with fallback
|
|
3629
|
+
this._needsRuntimeBridge = true;
|
|
3630
|
+
const argsCode = t.spawn.arguments.map(a => this.genExpression(a)).join(', ');
|
|
3631
|
+
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); } })()`;
|
|
3632
|
+
}
|
|
3633
|
+
return `(async () => { try { return new Ok(await ${t.callCode}); } catch(__e) { return new Err(__e); } })()`;
|
|
3634
|
+
});
|
|
3635
|
+
|
|
3636
|
+
if (node.mode === 'timeout' && node.timeout) {
|
|
3637
|
+
const timeoutMs = this.genExpression(node.timeout);
|
|
3638
|
+
const acVar = `__ac${base}`;
|
|
3639
|
+
const abortedVar = `__aborted${base}`;
|
|
3640
|
+
const timerVar = `__timer${base}`;
|
|
3641
|
+
lines.push(`${this.i()}const ${acVar} = new AbortController();`);
|
|
3642
|
+
lines.push(`${this.i()}const ${abortedVar} = new Promise((_, reject) => { ${acVar}.signal.addEventListener('abort', () => reject(new Error('concurrent timeout')), { once: true }); });`);
|
|
3643
|
+
lines.push(`${this.i()}const ${timerVar} = setTimeout(() => ${acVar}.abort(), ${timeoutMs});`);
|
|
3644
|
+
const taskExprsTimeout = tasks.map(t => {
|
|
3645
|
+
if (t.isWasm && hasWasm) {
|
|
3646
|
+
this._needsRuntimeBridge = true;
|
|
3647
|
+
const argsCode = t.spawn.arguments.map(a => this.genExpression(a)).join(', ');
|
|
3648
|
+
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); } })()`;
|
|
3649
|
+
}
|
|
3650
|
+
return `(async () => { try { return new Ok(await Promise.race([${t.callCode}, ${abortedVar}])); } catch(__e) { return new Err(__e); } })()`;
|
|
3651
|
+
});
|
|
3652
|
+
lines.push(`${this.i()}const [${tempVars.join(', ')}] = await Promise.all([`);
|
|
3653
|
+
for (let i = 0; i < taskExprsTimeout.length; i++) {
|
|
3654
|
+
lines.push(`${this.i()} ${taskExprsTimeout[i]}${i < taskExprsTimeout.length - 1 ? ',' : ''}`);
|
|
3655
|
+
}
|
|
3656
|
+
lines.push(`${this.i()}]);`);
|
|
3657
|
+
lines.push(`${this.i()}clearTimeout(${timerVar});`);
|
|
3658
|
+
} else if (node.mode === 'cancel_on_error') {
|
|
3659
|
+
const acVar = `__ac${base}`;
|
|
3660
|
+
const abortedVar = `__aborted${base}`;
|
|
3661
|
+
lines.push(`${this.i()}const ${acVar} = new AbortController();`);
|
|
3662
|
+
lines.push(`${this.i()}const ${abortedVar} = new Promise((_, reject) => { ${acVar}.signal.addEventListener('abort', () => reject(new Error('cancelled')), { once: true }); });`);
|
|
3663
|
+
const taskExprsAbort = tasks.map(t => {
|
|
3664
|
+
if (t.isWasm && hasWasm) {
|
|
3665
|
+
this._needsRuntimeBridge = true;
|
|
3666
|
+
const argsCode = t.spawn.arguments.map(a => this.genExpression(a)).join(', ');
|
|
3667
|
+
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); } })()`;
|
|
3668
|
+
}
|
|
3669
|
+
return `(async () => { try { return new Ok(await Promise.race([${t.callCode}, ${abortedVar}])); } catch(__e) { if (!${acVar}.signal.aborted) ${acVar}.abort(); return new Err(__e); } })()`;
|
|
3670
|
+
});
|
|
3671
|
+
lines.push(`${this.i()}const [${tempVars.join(', ')}] = await Promise.all([`);
|
|
3672
|
+
for (let i = 0; i < taskExprsAbort.length; i++) {
|
|
3673
|
+
lines.push(`${this.i()} ${taskExprsAbort[i]}${i < taskExprsAbort.length - 1 ? ',' : ''}`);
|
|
3674
|
+
}
|
|
3675
|
+
lines.push(`${this.i()}]);`);
|
|
3676
|
+
} else if (node.mode === 'first') {
|
|
3677
|
+
const acVar = `__ac${base}`;
|
|
3678
|
+
const firstVar = `__first${base}`;
|
|
3679
|
+
lines.push(`${this.i()}const ${acVar} = new AbortController();`);
|
|
3680
|
+
const taskExprsFirst = tasks.map((t, i) => {
|
|
3681
|
+
if (t.isWasm && hasWasm) {
|
|
3682
|
+
this._needsRuntimeBridge = true;
|
|
3683
|
+
const argsCode = t.spawn.arguments.map(a => this.genExpression(a)).join(', ');
|
|
3684
|
+
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) }; } })()`;
|
|
3685
|
+
}
|
|
3686
|
+
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) }; } })()`;
|
|
3687
|
+
});
|
|
3688
|
+
lines.push(`${this.i()}const ${firstVar} = await Promise.race([`);
|
|
3689
|
+
for (let i = 0; i < taskExprsFirst.length; i++) {
|
|
3690
|
+
lines.push(`${this.i()} ${taskExprsFirst[i]}${i < taskExprsFirst.length - 1 ? ',' : ''}`);
|
|
3691
|
+
}
|
|
3692
|
+
lines.push(`${this.i()}]);`);
|
|
3693
|
+
// For first mode, assign the winner's result to all named variables
|
|
3694
|
+
for (let i = 0; i < assignments.length; i++) {
|
|
3695
|
+
if (assignments[i]) {
|
|
3696
|
+
this.declareVar(assignments[i]);
|
|
3697
|
+
lines.push(`${this.i()}const ${assignments[i]} = ${firstVar}.__result;`);
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
return lines.join('\n'); // early return — skip the normal assignment loop
|
|
3701
|
+
} else {
|
|
3702
|
+
lines.push(`${this.i()}const [${tempVars.join(', ')}] = await Promise.all([`);
|
|
3703
|
+
for (let i = 0; i < taskExprs.length; i++) {
|
|
3704
|
+
lines.push(`${this.i()} ${taskExprs[i]}${i < taskExprs.length - 1 ? ',' : ''}`);
|
|
3705
|
+
}
|
|
3706
|
+
lines.push(`${this.i()}]);`);
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
// Assign results to named variables
|
|
3711
|
+
for (let i = 0; i < assignments.length; i++) {
|
|
3712
|
+
if (assignments[i]) {
|
|
3713
|
+
this.declareVar(assignments[i]);
|
|
3714
|
+
lines.push(`${this.i()}const ${assignments[i]} = ${tempVars[i]};`);
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
return lines.join('\n');
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
genSelectStatement(node) {
|
|
3722
|
+
this._needsResultOption = true; // for Some/None in _tryReceive
|
|
3723
|
+
const base = this._selectCounter = (this._selectCounter || 0) + 1;
|
|
3724
|
+
|
|
3725
|
+
const hasDefault = node.cases.some(c => c.kind === 'default');
|
|
3726
|
+
|
|
3727
|
+
if (hasDefault) {
|
|
3728
|
+
return this._genSelectWithDefault(node, base);
|
|
3729
|
+
}
|
|
3730
|
+
return this._genSelectWithRace(node, base);
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
_genSelectWithRace(node, base) {
|
|
3734
|
+
const lines = [];
|
|
3735
|
+
const selVar = `__sel${base}`;
|
|
3736
|
+
const promises = [];
|
|
3737
|
+
|
|
3738
|
+
for (let i = 0; i < node.cases.length; i++) {
|
|
3739
|
+
const c = node.cases[i];
|
|
3740
|
+
if (c.kind === 'receive') {
|
|
3741
|
+
const ch = this.genExpression(c.channel);
|
|
3742
|
+
promises.push(`${ch}.receive().then(__v => ({ __case: ${i}, __value: __v }))`);
|
|
3743
|
+
} else if (c.kind === 'send') {
|
|
3744
|
+
const ch = this.genExpression(c.channel);
|
|
3745
|
+
const val = this.genExpression(c.value);
|
|
3746
|
+
promises.push(`${ch}.send(${val}).then(() => ({ __case: ${i} }))`);
|
|
3747
|
+
} else if (c.kind === 'timeout') {
|
|
3748
|
+
const ms = this.genExpression(c.value);
|
|
3749
|
+
promises.push(`new Promise(__r => setTimeout(() => __r({ __case: ${i} }), ${ms}))`);
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
lines.push(`${this.i()}const ${selVar} = await Promise.race([`);
|
|
3754
|
+
for (let i = 0; i < promises.length; i++) {
|
|
3755
|
+
lines.push(`${this.i()} ${promises[i]}${i < promises.length - 1 ? ',' : ''}`);
|
|
3756
|
+
}
|
|
3757
|
+
lines.push(`${this.i()}]);`);
|
|
3758
|
+
|
|
3759
|
+
// Generate if/else chain (NOT switch — avoids break conflicts in loops)
|
|
3760
|
+
for (let i = 0; i < node.cases.length; i++) {
|
|
3761
|
+
const c = node.cases[i];
|
|
3762
|
+
const prefix = i === 0 ? 'if' : ' else if';
|
|
3763
|
+
lines.push(`${this.i()}${prefix} (${selVar}.__case === ${i}) {`);
|
|
3764
|
+
this.indent++;
|
|
3765
|
+
if (c.kind === 'receive' && c.binding) {
|
|
3766
|
+
this.declareVar(c.binding);
|
|
3767
|
+
lines.push(`${this.i()}const ${c.binding} = ${selVar}.__value.value;`);
|
|
3768
|
+
}
|
|
3769
|
+
for (const stmt of c.body) {
|
|
3770
|
+
lines.push(this.generateStatement(stmt));
|
|
3771
|
+
}
|
|
3772
|
+
this.indent--;
|
|
3773
|
+
lines.push(`${this.i()}}`);
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3776
|
+
return lines.join('\n');
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3779
|
+
_genSelectWithDefault(node, base) {
|
|
3780
|
+
const lines = [];
|
|
3781
|
+
const nonDefault = node.cases.filter(c => c.kind !== 'default');
|
|
3782
|
+
const defaultCase = node.cases.find(c => c.kind === 'default');
|
|
3783
|
+
|
|
3784
|
+
lines.push(`${this.i()}{`);
|
|
3785
|
+
this.indent++;
|
|
3786
|
+
|
|
3787
|
+
let depth = 0;
|
|
3788
|
+
for (let i = 0; i < nonDefault.length; i++) {
|
|
3789
|
+
const c = nonDefault[i];
|
|
3790
|
+
const tryVar = `__try${base}_${i}`;
|
|
3791
|
+
|
|
3792
|
+
if (c.kind === 'receive') {
|
|
3793
|
+
const ch = this.genExpression(c.channel);
|
|
3794
|
+
lines.push(`${this.i()}const ${tryVar} = ${ch}._tryReceive();`);
|
|
3795
|
+
lines.push(`${this.i()}if (${tryVar}.__tag === 'Some') {`);
|
|
3796
|
+
this.indent++;
|
|
3797
|
+
if (c.binding) {
|
|
3798
|
+
this.declareVar(c.binding);
|
|
3799
|
+
lines.push(`${this.i()}const ${c.binding} = ${tryVar}.value;`);
|
|
3800
|
+
}
|
|
3801
|
+
for (const stmt of c.body) {
|
|
3802
|
+
lines.push(this.generateStatement(stmt));
|
|
3803
|
+
}
|
|
3804
|
+
this.indent--;
|
|
3805
|
+
lines.push(`${this.i()}} else {`);
|
|
3806
|
+
this.indent++;
|
|
3807
|
+
depth++;
|
|
3808
|
+
} else if (c.kind === 'send') {
|
|
3809
|
+
const ch = this.genExpression(c.channel);
|
|
3810
|
+
const val = this.genExpression(c.value);
|
|
3811
|
+
lines.push(`${this.i()}const ${tryVar} = ${ch}._trySend(${val});`);
|
|
3812
|
+
lines.push(`${this.i()}if (${tryVar}) {`);
|
|
3813
|
+
this.indent++;
|
|
3814
|
+
for (const stmt of c.body) {
|
|
3815
|
+
lines.push(this.generateStatement(stmt));
|
|
3816
|
+
}
|
|
3817
|
+
this.indent--;
|
|
3818
|
+
lines.push(`${this.i()}} else {`);
|
|
3819
|
+
this.indent++;
|
|
3820
|
+
depth++;
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
// Default case body
|
|
3825
|
+
for (const stmt of defaultCase.body) {
|
|
3826
|
+
lines.push(this.generateStatement(stmt));
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
// Close all else blocks
|
|
3830
|
+
for (let i = 0; i < depth; i++) {
|
|
3831
|
+
this.indent--;
|
|
3832
|
+
lines.push(`${this.i()}}`);
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
this.indent--;
|
|
3836
|
+
lines.push(`${this.i()}}`);
|
|
3837
|
+
|
|
3838
|
+
return lines.join('\n');
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
genSpawnExpression(node) {
|
|
3842
|
+
// Standalone spawn expression (not inside concurrent block codegen path)
|
|
3843
|
+
// This happens when spawn is used directly in an expression context
|
|
3844
|
+
const callCode = `${this.genExpression(node.callee)}(${node.arguments.map(a => this.genExpression(a)).join(', ')})`;
|
|
3845
|
+
return `(async () => { try { return new Ok(await ${callCode}); } catch(__e) { return new Err(__e); } })()`;
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3016
3848
|
// Check if a function body contains yield expressions (for generator detection)
|
|
3017
3849
|
_containsYield(node) {
|
|
3018
3850
|
if (!node) return false;
|
|
@@ -3036,4 +3868,445 @@ export class BaseCodegen {
|
|
|
3036
3868
|
this._yieldCache.set(node, result);
|
|
3037
3869
|
return result;
|
|
3038
3870
|
}
|
|
3871
|
+
|
|
3872
|
+
// ─── Scalar replacement codegen for Result/Option ───────────
|
|
3873
|
+
// Generates boolean+value pairs instead of allocating Result/Option objects.
|
|
3874
|
+
|
|
3875
|
+
/**
|
|
3876
|
+
* Generates scalar-replaced code for an assignment like:
|
|
3877
|
+
* r = if cond { Ok(val) } else { Err(err) }
|
|
3878
|
+
* Output: let r__ok, r__v; if (cond) { r__ok = true; r__v = val; } else { ... }
|
|
3879
|
+
*/
|
|
3880
|
+
_genScalarAssignment(node, info) {
|
|
3881
|
+
const varName = node.targets[0];
|
|
3882
|
+
const okVar = `${varName}__ok`;
|
|
3883
|
+
const vVar = `${varName}__v`;
|
|
3884
|
+
const ifExpr = node.values[0];
|
|
3885
|
+
|
|
3886
|
+
// Mark all three names as declared so later code doesn't redeclare
|
|
3887
|
+
this.declareVar(varName);
|
|
3888
|
+
this.declareVar(okVar);
|
|
3889
|
+
this.declareVar(vVar);
|
|
3890
|
+
|
|
3891
|
+
const parts = [];
|
|
3892
|
+
parts.push(`${this.i()}let ${okVar}, ${vVar};`);
|
|
3893
|
+
|
|
3894
|
+
// Build if/else chain
|
|
3895
|
+
let ifCode = `${this.i()}if (${this.genExpression(ifExpr.condition)}) {\n`;
|
|
3896
|
+
this.indent++;
|
|
3897
|
+
ifCode += this._genScalarBranch(ifExpr.consequent, okVar, vVar, info.kind);
|
|
3898
|
+
this.indent--;
|
|
3899
|
+
ifCode += `\n${this.i()}}`;
|
|
3900
|
+
|
|
3901
|
+
if (ifExpr.alternates) {
|
|
3902
|
+
for (const alt of ifExpr.alternates) {
|
|
3903
|
+
ifCode += ` else if (${this.genExpression(alt.condition)}) {\n`;
|
|
3904
|
+
this.indent++;
|
|
3905
|
+
ifCode += this._genScalarBranch(alt.body, okVar, vVar, info.kind);
|
|
3906
|
+
this.indent--;
|
|
3907
|
+
ifCode += `\n${this.i()}}`;
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
if (ifExpr.elseBody) {
|
|
3912
|
+
ifCode += ` else {\n`;
|
|
3913
|
+
this.indent++;
|
|
3914
|
+
ifCode += this._genScalarBranch(ifExpr.elseBody, okVar, vVar, info.kind);
|
|
3915
|
+
this.indent--;
|
|
3916
|
+
ifCode += `\n${this.i()}}`;
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3919
|
+
parts.push(ifCode);
|
|
3920
|
+
return parts.join('\n');
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
/**
|
|
3924
|
+
* Generates scalar assignments for one branch of an if expression.
|
|
3925
|
+
* Extracts the Ok/Err/Some/None constructor call from the last expression
|
|
3926
|
+
* and emits okVar = true/false; vVar = value;
|
|
3927
|
+
*/
|
|
3928
|
+
_genScalarBranch(block, okVar, vVar, kind) {
|
|
3929
|
+
if (!block) return '';
|
|
3930
|
+
const stmts = block.type === 'BlockStatement' ? block.body : [block];
|
|
3931
|
+
const lines = [];
|
|
3932
|
+
|
|
3933
|
+
// Generate any statements before the last one (side effects)
|
|
3934
|
+
for (let i = 0; i < stmts.length - 1; i++) {
|
|
3935
|
+
lines.push(this.generateStatement(stmts[i]));
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
// Last statement has the constructor
|
|
3939
|
+
const last = stmts[stmts.length - 1];
|
|
3940
|
+
let expr = last;
|
|
3941
|
+
if (last.type === 'ExpressionStatement') expr = last.expression;
|
|
3942
|
+
if (last.type === 'ReturnStatement' && last.value) expr = last.value;
|
|
3943
|
+
|
|
3944
|
+
if (expr.type === 'CallExpression' && expr.callee && expr.callee.type === 'Identifier') {
|
|
3945
|
+
const name = expr.callee.name;
|
|
3946
|
+
if (name === 'Ok' || name === 'Some') {
|
|
3947
|
+
lines.push(`${this.i()}${okVar} = true; ${vVar} = ${this.genExpression(expr.arguments[0])};`);
|
|
3948
|
+
} else if (name === 'Err') {
|
|
3949
|
+
lines.push(`${this.i()}${okVar} = false; ${vVar} = ${this.genExpression(expr.arguments[0])};`);
|
|
3950
|
+
}
|
|
3951
|
+
} else if (expr.type === 'Identifier' && expr.name === 'None') {
|
|
3952
|
+
lines.push(`${this.i()}${okVar} = false; ${vVar} = undefined;`);
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
return lines.join('\n');
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
// ─── Scalar replacement analysis for Result/Option ──────────
|
|
3959
|
+
// Detects local variables assigned from if-expressions that produce
|
|
3960
|
+
// Ok/Err or Some/None, where all subsequent usages are safe method
|
|
3961
|
+
// calls (isOk, unwrap, unwrapOr, etc.). Such variables can be
|
|
3962
|
+
// replaced with a boolean + value pair (zero allocation).
|
|
3963
|
+
|
|
3964
|
+
/**
|
|
3965
|
+
* Scans an array of statements for scalar-replaceable Result/Option
|
|
3966
|
+
* assignments. Returns Map<varName, {kind, assignIdx}>.
|
|
3967
|
+
*/
|
|
3968
|
+
_preAnalyzeScalarResults(stmts) {
|
|
3969
|
+
const map = new Map();
|
|
3970
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
3971
|
+
const stmt = stmts[i];
|
|
3972
|
+
// Must be a single-target, single-value assignment
|
|
3973
|
+
if (stmt.type !== 'Assignment' || stmt.targets.length !== 1 || stmt.values.length !== 1) continue;
|
|
3974
|
+
const target = stmt.targets[0];
|
|
3975
|
+
if (typeof target !== 'string') continue; // skip MemberExpression targets
|
|
3976
|
+
const value = stmt.values[0];
|
|
3977
|
+
// Value must be an IfExpression (or IfStatement used as expression)
|
|
3978
|
+
if (value.type !== 'IfExpression' && value.type !== 'IfStatement') continue;
|
|
3979
|
+
const kind = this._detectResultOptionIf(value);
|
|
3980
|
+
if (kind === null) continue;
|
|
3981
|
+
// Check all subsequent statements for safe usage
|
|
3982
|
+
let allSafe = true;
|
|
3983
|
+
let anyUsed = false;
|
|
3984
|
+
for (let j = i + 1; j < stmts.length; j++) {
|
|
3985
|
+
const usage = this._checkScalarSafeUsage(stmts[j], target, kind);
|
|
3986
|
+
if (usage === 'unsafe') { allSafe = false; break; }
|
|
3987
|
+
if (usage === 'used') anyUsed = true;
|
|
3988
|
+
}
|
|
3989
|
+
if (allSafe && anyUsed) {
|
|
3990
|
+
map.set(target, { kind, assignIdx: i });
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
return map;
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
/**
|
|
3997
|
+
* Checks if ALL branches of an if expression return Ok/Err or Some/None.
|
|
3998
|
+
* Returns 'result' | 'option' | null.
|
|
3999
|
+
*/
|
|
4000
|
+
_detectResultOptionIf(ifExpr) {
|
|
4001
|
+
// Must have an else branch (otherwise result could be undefined)
|
|
4002
|
+
if (!ifExpr.elseBody) return null;
|
|
4003
|
+
|
|
4004
|
+
const types = [];
|
|
4005
|
+
|
|
4006
|
+
// Check consequent block
|
|
4007
|
+
types.push(this._getBlockResultType(ifExpr.consequent));
|
|
4008
|
+
|
|
4009
|
+
// Check elif alternates
|
|
4010
|
+
if (ifExpr.alternates) {
|
|
4011
|
+
for (const alt of ifExpr.alternates) {
|
|
4012
|
+
types.push(this._getBlockResultType(alt.body));
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
// Check else block
|
|
4017
|
+
types.push(this._getBlockResultType(ifExpr.elseBody));
|
|
4018
|
+
|
|
4019
|
+
// All must be non-null
|
|
4020
|
+
if (types.some(t => t === null)) return null;
|
|
4021
|
+
|
|
4022
|
+
// Check if all are Result constructors (Ok/Err)
|
|
4023
|
+
if (types.every(t => t === 'Ok' || t === 'Err')) return 'result';
|
|
4024
|
+
|
|
4025
|
+
// Check if all are Option constructors (Some/None)
|
|
4026
|
+
if (types.every(t => t === 'Some' || t === 'None')) return 'option';
|
|
4027
|
+
|
|
4028
|
+
return null;
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4031
|
+
/**
|
|
4032
|
+
* Gets the constructor name from the last expression in a block.
|
|
4033
|
+
* Returns 'Ok' | 'Err' | 'Some' | 'None' | null.
|
|
4034
|
+
*/
|
|
4035
|
+
_getBlockResultType(block) {
|
|
4036
|
+
if (!block) return null;
|
|
4037
|
+
const stmts = block.type === 'BlockStatement' ? block.body : [block];
|
|
4038
|
+
if (stmts.length === 0) return null;
|
|
4039
|
+
const last = stmts[stmts.length - 1];
|
|
4040
|
+
// Could be ExpressionStatement or bare expression (implicit return)
|
|
4041
|
+
let expr = last;
|
|
4042
|
+
if (last.type === 'ExpressionStatement') expr = last.expression;
|
|
4043
|
+
if (last.type === 'ReturnStatement' && last.value) expr = last.value;
|
|
4044
|
+
// Check for Ok(val), Err(val), Some(val) call
|
|
4045
|
+
if (expr.type === 'CallExpression' && expr.callee && expr.callee.type === 'Identifier') {
|
|
4046
|
+
const name = expr.callee.name;
|
|
4047
|
+
if (['Ok', 'Err', 'Some'].includes(name) && expr.arguments.length === 1) return name;
|
|
4048
|
+
}
|
|
4049
|
+
// Check for bare None identifier
|
|
4050
|
+
if (expr.type === 'Identifier' && expr.name === 'None') return 'None';
|
|
4051
|
+
return null;
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
/**
|
|
4055
|
+
* Walks a statement checking all references to varName.
|
|
4056
|
+
* Returns 'used' | 'unsafe' | 'none'.
|
|
4057
|
+
*/
|
|
4058
|
+
_checkScalarSafeUsage(stmt, varName, kind) {
|
|
4059
|
+
let found = false;
|
|
4060
|
+
let safe = true;
|
|
4061
|
+
|
|
4062
|
+
const SAFE_RESULT = new Set(['isOk', 'isErr', 'unwrap', 'unwrapOr', 'expect']);
|
|
4063
|
+
const SAFE_OPTION = new Set(['isSome', 'isNone', 'unwrap', 'unwrapOr', 'expect']);
|
|
4064
|
+
const safeSet = kind === 'result' ? SAFE_RESULT : SAFE_OPTION;
|
|
4065
|
+
|
|
4066
|
+
// Custom expression walker that handles method calls specially
|
|
4067
|
+
const walkExpr = (expr) => {
|
|
4068
|
+
if (!expr || !safe) return;
|
|
4069
|
+
// Check for varName.method() pattern FIRST
|
|
4070
|
+
if (expr.type === 'CallExpression' &&
|
|
4071
|
+
expr.callee && expr.callee.type === 'MemberExpression' &&
|
|
4072
|
+
!expr.callee.computed &&
|
|
4073
|
+
expr.callee.object && expr.callee.object.type === 'Identifier' &&
|
|
4074
|
+
expr.callee.object.name === varName) {
|
|
4075
|
+
if (safeSet.has(expr.callee.property)) {
|
|
4076
|
+
found = true;
|
|
4077
|
+
// Walk only the arguments (NOT the callee.object to avoid bare ref detection)
|
|
4078
|
+
for (const arg of expr.arguments) walkExpr(arg);
|
|
4079
|
+
return;
|
|
4080
|
+
}
|
|
4081
|
+
// Unsafe method call on the variable
|
|
4082
|
+
safe = false;
|
|
4083
|
+
return;
|
|
4084
|
+
}
|
|
4085
|
+
// Check for bare varName reference (not part of a safe method call)
|
|
4086
|
+
if (expr.type === 'Identifier' && expr.name === varName) {
|
|
4087
|
+
safe = false;
|
|
4088
|
+
return;
|
|
4089
|
+
}
|
|
4090
|
+
// Walk children
|
|
4091
|
+
switch (expr.type) {
|
|
4092
|
+
case 'BinaryExpression':
|
|
4093
|
+
case 'LogicalExpression':
|
|
4094
|
+
walkExpr(expr.left);
|
|
4095
|
+
walkExpr(expr.right);
|
|
4096
|
+
break;
|
|
4097
|
+
case 'UnaryExpression':
|
|
4098
|
+
walkExpr(expr.operand);
|
|
4099
|
+
break;
|
|
4100
|
+
case 'CallExpression':
|
|
4101
|
+
walkExpr(expr.callee);
|
|
4102
|
+
for (const arg of expr.arguments) walkExpr(arg);
|
|
4103
|
+
break;
|
|
4104
|
+
case 'MemberExpression':
|
|
4105
|
+
walkExpr(expr.object);
|
|
4106
|
+
if (expr.computed) walkExpr(expr.property);
|
|
4107
|
+
break;
|
|
4108
|
+
case 'ConditionalExpression':
|
|
4109
|
+
walkExpr(expr.condition);
|
|
4110
|
+
walkExpr(expr.consequent);
|
|
4111
|
+
walkExpr(expr.alternate);
|
|
4112
|
+
break;
|
|
4113
|
+
case 'ArrayLiteral':
|
|
4114
|
+
if (expr.elements) for (const el of expr.elements) walkExpr(el);
|
|
4115
|
+
break;
|
|
4116
|
+
case 'ObjectLiteral':
|
|
4117
|
+
if (expr.properties) for (const prop of expr.properties) walkExpr(prop.value);
|
|
4118
|
+
break;
|
|
4119
|
+
case 'TemplateLiteral':
|
|
4120
|
+
if (expr.expressions) for (const e of expr.expressions) walkExpr(e);
|
|
4121
|
+
break;
|
|
4122
|
+
// Lambdas/functions — don't descend (separate scope)
|
|
4123
|
+
case 'FunctionExpression':
|
|
4124
|
+
case 'ArrowFunction':
|
|
4125
|
+
case 'LambdaExpression':
|
|
4126
|
+
break;
|
|
4127
|
+
// Assignment expression
|
|
4128
|
+
case 'AssignmentExpression':
|
|
4129
|
+
walkExpr(expr.right);
|
|
4130
|
+
break;
|
|
4131
|
+
}
|
|
4132
|
+
};
|
|
4133
|
+
|
|
4134
|
+
// Walk statement dispatching to expression walker
|
|
4135
|
+
this._walkStmtForScalar(stmt, walkExpr);
|
|
4136
|
+
|
|
4137
|
+
if (!safe) return 'unsafe';
|
|
4138
|
+
return found ? 'used' : 'none';
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
/**
|
|
4142
|
+
* Walks all expressions within a statement, calling walkExpr for each
|
|
4143
|
+
* top-level expression found. Used by _checkScalarSafeUsage.
|
|
4144
|
+
*/
|
|
4145
|
+
_walkStmtForScalar(stmt, walkExpr) {
|
|
4146
|
+
if (!stmt) return;
|
|
4147
|
+
switch (stmt.type) {
|
|
4148
|
+
case 'ExpressionStatement':
|
|
4149
|
+
walkExpr(stmt.expression);
|
|
4150
|
+
break;
|
|
4151
|
+
case 'Assignment':
|
|
4152
|
+
case 'VarDeclaration':
|
|
4153
|
+
if (stmt.values) for (const v of stmt.values) walkExpr(v);
|
|
4154
|
+
if (stmt.targets) {
|
|
4155
|
+
for (const t of stmt.targets) {
|
|
4156
|
+
if (typeof t === 'object') walkExpr(t);
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
break;
|
|
4160
|
+
case 'ReturnStatement':
|
|
4161
|
+
if (stmt.value) walkExpr(stmt.value);
|
|
4162
|
+
break;
|
|
4163
|
+
case 'IfStatement':
|
|
4164
|
+
case 'IfExpression':
|
|
4165
|
+
walkExpr(stmt.condition);
|
|
4166
|
+
this._walkBlockForScalar(stmt.consequent, walkExpr);
|
|
4167
|
+
if (stmt.alternates) {
|
|
4168
|
+
for (const alt of stmt.alternates) {
|
|
4169
|
+
walkExpr(alt.condition);
|
|
4170
|
+
this._walkBlockForScalar(alt.body, walkExpr);
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
if (stmt.elseBody) this._walkBlockForScalar(stmt.elseBody, walkExpr);
|
|
4174
|
+
break;
|
|
4175
|
+
case 'ForStatement':
|
|
4176
|
+
walkExpr(stmt.iterable);
|
|
4177
|
+
this._walkBlockForScalar(stmt.body, walkExpr);
|
|
4178
|
+
break;
|
|
4179
|
+
case 'WhileStatement':
|
|
4180
|
+
walkExpr(stmt.condition);
|
|
4181
|
+
this._walkBlockForScalar(stmt.body, walkExpr);
|
|
4182
|
+
break;
|
|
4183
|
+
case 'CallExpression':
|
|
4184
|
+
walkExpr(stmt);
|
|
4185
|
+
break;
|
|
4186
|
+
default:
|
|
4187
|
+
if (stmt.expression) walkExpr(stmt.expression);
|
|
4188
|
+
break;
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
|
|
4192
|
+
/**
|
|
4193
|
+
* Walks all statements in a block, dispatching to _walkStmtForScalar.
|
|
4194
|
+
*/
|
|
4195
|
+
_walkBlockForScalar(block, walkExpr) {
|
|
4196
|
+
if (!block) return;
|
|
4197
|
+
const stmts = block.type === 'BlockStatement' ? block.body : [block];
|
|
4198
|
+
for (const s of stmts) this._walkStmtForScalar(s, walkExpr);
|
|
4199
|
+
}
|
|
4200
|
+
|
|
4201
|
+
/**
|
|
4202
|
+
* General-purpose expression walker. Calls callback(node) for every
|
|
4203
|
+
* expression node in the tree.
|
|
4204
|
+
*/
|
|
4205
|
+
_walkExpressions(node, callback) {
|
|
4206
|
+
if (!node) return;
|
|
4207
|
+
callback(node);
|
|
4208
|
+
switch (node.type) {
|
|
4209
|
+
case 'BinaryExpression':
|
|
4210
|
+
case 'LogicalExpression':
|
|
4211
|
+
this._walkExpressions(node.left, callback);
|
|
4212
|
+
this._walkExpressions(node.right, callback);
|
|
4213
|
+
break;
|
|
4214
|
+
case 'UnaryExpression':
|
|
4215
|
+
this._walkExpressions(node.operand, callback);
|
|
4216
|
+
break;
|
|
4217
|
+
case 'CallExpression':
|
|
4218
|
+
this._walkExpressions(node.callee, callback);
|
|
4219
|
+
for (const arg of node.arguments) this._walkExpressions(arg, callback);
|
|
4220
|
+
break;
|
|
4221
|
+
case 'MemberExpression':
|
|
4222
|
+
this._walkExpressions(node.object, callback);
|
|
4223
|
+
if (node.computed) this._walkExpressions(node.property, callback);
|
|
4224
|
+
break;
|
|
4225
|
+
case 'ConditionalExpression':
|
|
4226
|
+
this._walkExpressions(node.condition, callback);
|
|
4227
|
+
this._walkExpressions(node.consequent, callback);
|
|
4228
|
+
this._walkExpressions(node.alternate, callback);
|
|
4229
|
+
break;
|
|
4230
|
+
case 'ArrayLiteral':
|
|
4231
|
+
if (node.elements) for (const el of node.elements) this._walkExpressions(el, callback);
|
|
4232
|
+
break;
|
|
4233
|
+
case 'ObjectLiteral':
|
|
4234
|
+
if (node.properties) for (const prop of node.properties) this._walkExpressions(prop.value, callback);
|
|
4235
|
+
break;
|
|
4236
|
+
case 'TemplateLiteral':
|
|
4237
|
+
if (node.expressions) for (const expr of node.expressions) this._walkExpressions(expr, callback);
|
|
4238
|
+
break;
|
|
4239
|
+
// Lambdas/functions — don't descend (separate scope)
|
|
4240
|
+
case 'FunctionExpression':
|
|
4241
|
+
case 'ArrowFunction':
|
|
4242
|
+
case 'LambdaExpression':
|
|
4243
|
+
break;
|
|
4244
|
+
// Leaf nodes — no children
|
|
4245
|
+
case 'Identifier':
|
|
4246
|
+
case 'NumberLiteral':
|
|
4247
|
+
case 'StringLiteral':
|
|
4248
|
+
case 'BooleanLiteral':
|
|
4249
|
+
case 'NilLiteral':
|
|
4250
|
+
break;
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
/**
|
|
4255
|
+
* Walks all expressions within a statement.
|
|
4256
|
+
*/
|
|
4257
|
+
_walkStatementExpressions(stmt, callback) {
|
|
4258
|
+
if (!stmt) return;
|
|
4259
|
+
switch (stmt.type) {
|
|
4260
|
+
case 'ExpressionStatement':
|
|
4261
|
+
this._walkExpressions(stmt.expression, callback);
|
|
4262
|
+
break;
|
|
4263
|
+
case 'Assignment':
|
|
4264
|
+
case 'VarDeclaration':
|
|
4265
|
+
if (stmt.values) for (const v of stmt.values) this._walkExpressions(v, callback);
|
|
4266
|
+
if (stmt.targets) {
|
|
4267
|
+
for (const t of stmt.targets) {
|
|
4268
|
+
if (typeof t === 'object') this._walkExpressions(t, callback);
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
break;
|
|
4272
|
+
case 'ReturnStatement':
|
|
4273
|
+
if (stmt.value) this._walkExpressions(stmt.value, callback);
|
|
4274
|
+
break;
|
|
4275
|
+
case 'IfStatement':
|
|
4276
|
+
case 'IfExpression':
|
|
4277
|
+
this._walkExpressions(stmt.condition, callback);
|
|
4278
|
+
this._walkStatementBlock(stmt.consequent, callback);
|
|
4279
|
+
if (stmt.alternates) {
|
|
4280
|
+
for (const alt of stmt.alternates) {
|
|
4281
|
+
this._walkExpressions(alt.condition, callback);
|
|
4282
|
+
this._walkStatementBlock(alt.body, callback);
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
if (stmt.elseBody) this._walkStatementBlock(stmt.elseBody, callback);
|
|
4286
|
+
break;
|
|
4287
|
+
case 'ForStatement':
|
|
4288
|
+
this._walkExpressions(stmt.iterable, callback);
|
|
4289
|
+
this._walkStatementBlock(stmt.body, callback);
|
|
4290
|
+
break;
|
|
4291
|
+
case 'WhileStatement':
|
|
4292
|
+
this._walkExpressions(stmt.condition, callback);
|
|
4293
|
+
this._walkStatementBlock(stmt.body, callback);
|
|
4294
|
+
break;
|
|
4295
|
+
case 'CallExpression':
|
|
4296
|
+
this._walkExpressions(stmt, callback);
|
|
4297
|
+
break;
|
|
4298
|
+
default:
|
|
4299
|
+
if (stmt.expression) this._walkExpressions(stmt.expression, callback);
|
|
4300
|
+
break;
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
/**
|
|
4305
|
+
* Walks all statements in a block for _walkStatementExpressions.
|
|
4306
|
+
*/
|
|
4307
|
+
_walkStatementBlock(block, callback) {
|
|
4308
|
+
if (!block) return;
|
|
4309
|
+
const stmts = block.type === 'BlockStatement' ? block.body : [block];
|
|
4310
|
+
for (const s of stmts) this._walkStatementExpressions(s, callback);
|
|
4311
|
+
}
|
|
3039
4312
|
}
|