tova 0.3.4 → 0.3.6

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.
@@ -1,12 +1,15 @@
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
5
 
5
6
  export class BaseCodegen {
6
7
  constructor() {
7
8
  this.indent = 0;
8
9
  this._counter = 0;
9
10
  this._scopes = [new Set()]; // scope stack for tracking declared variables
11
+ this._visibleNames = new Set(); // flattened view of all declared names for O(1) lookup
12
+ this._nameRefCount = new Map(); // name -> count of scopes declaring it (for O(1) popScope)
10
13
  this._needsContainsHelper = false; // track if __contains helper is needed
11
14
  this._needsPropagateHelper = false; // track if __propagate helper is needed
12
15
  this._usedBuiltins = new Set(); // track which stdlib builtins are actually used
@@ -16,10 +19,31 @@ export class BaseCodegen {
16
19
  this._traitDecls = new Map(); // traitName -> { methods: [...] }
17
20
  this._traitImpls = new Map(); // "TraitName:TypeName" -> ImplDeclaration node
18
21
  // Source map tracking
22
+ this._sourceMapsEnabled = true; // can be disabled for REPL/check mode
23
+ this._propagateCache = new WeakMap(); // memoize _containsPropagate()
24
+ this._yieldCache = new WeakMap(); // memoize _containsYield()
19
25
  this._sourceMappings = []; // {sourceLine, sourceCol, outputLine, outputCol, sourceFile?}
20
26
  this._outputLineCount = 0;
21
27
  this._sourceFile = null; // current source file for multi-file source maps
22
- }
28
+ // @fast mode for TypedArray optimization
29
+ this._fastMode = false;
30
+ this._typedArrayParams = new Map(); // paramName -> 'Float64Array' | 'Int32Array' | 'Uint8Array'
31
+ this._typedArrayLocals = new Map(); // varName -> 'Float64Array' | 'Int32Array' | 'Uint8Array'
32
+ }
33
+
34
+ static TYPED_ARRAY_MAP = {
35
+ 'Int': 'Int32Array',
36
+ 'Float': 'Float64Array',
37
+ 'Byte': 'Uint8Array',
38
+ 'Int8': 'Int8Array',
39
+ 'Int16': 'Int16Array',
40
+ 'Int32': 'Int32Array',
41
+ 'Uint8': 'Uint8Array',
42
+ 'Uint16': 'Uint16Array',
43
+ 'Uint32': 'Uint32Array',
44
+ 'Float32': 'Float32Array',
45
+ 'Float64': 'Float64Array',
46
+ };
23
47
 
24
48
  _uid() {
25
49
  return this._counter++;
@@ -43,11 +67,11 @@ export class BaseCodegen {
43
67
  }
44
68
 
45
69
  // Known void/side-effect-only calls that shouldn't be implicitly returned
70
+ static VOID_FNS = new Set(['print', 'assert', 'assert_eq', 'assert_ne']);
46
71
  _isVoidCall(expr) {
47
72
  if (expr.type !== 'CallExpression') return false;
48
73
  if (expr.callee.type === 'Identifier') {
49
- const voidFns = new Set(['print', 'assert', 'assert_eq', 'assert_ne']);
50
- return voidFns.has(expr.callee.name);
74
+ return BaseCodegen.VOID_FNS.has(expr.callee.name);
51
75
  }
52
76
  return false;
53
77
  }
@@ -59,18 +83,27 @@ export class BaseCodegen {
59
83
  }
60
84
 
61
85
  popScope() {
62
- this._scopes.pop();
86
+ const removed = this._scopes.pop();
87
+ // O(n) cleanup using reference counts instead of O(n*m) scope search
88
+ for (const name of removed) {
89
+ const rc = this._nameRefCount.get(name) - 1;
90
+ if (rc <= 0) {
91
+ this._nameRefCount.delete(name);
92
+ this._visibleNames.delete(name);
93
+ } else {
94
+ this._nameRefCount.set(name, rc);
95
+ }
96
+ }
63
97
  }
64
98
 
65
99
  declareVar(name) {
66
100
  this._scopes[this._scopes.length - 1].add(name);
101
+ this._visibleNames.add(name);
102
+ this._nameRefCount.set(name, (this._nameRefCount.get(name) || 0) + 1);
67
103
  }
68
104
 
69
105
  isDeclared(name) {
70
- for (let i = this._scopes.length - 1; i >= 0; i--) {
71
- if (this._scopes[i].has(name)) return true;
72
- }
73
- return false;
106
+ return this._visibleNames.has(name);
74
107
  }
75
108
 
76
109
  // ─── Helpers ────────────────────────────────────────────────
@@ -86,6 +119,7 @@ export class BaseCodegen {
86
119
 
87
120
  // Source map: record a mapping from source location to output line
88
121
  _addMapping(node, outputLine) {
122
+ if (!this._sourceMapsEnabled) return;
89
123
  if (node && node.loc && node.loc.line) {
90
124
  const mapping = {
91
125
  sourceLine: node.loc.line - 1, // 0-based
@@ -155,20 +189,24 @@ export class BaseCodegen {
155
189
  _containsPropagate(node) {
156
190
  if (!node) return false;
157
191
  if (node.type === 'PropagateExpression') return true;
158
- // Stop at nested function/lambda boundaries — they get their own wrapper
159
192
  if (node.type === 'FunctionDeclaration' || node.type === 'LambdaExpression') return false;
193
+ const cached = this._propagateCache.get(node);
194
+ if (cached !== undefined) return cached;
195
+ let result = false;
160
196
  for (const key of Object.keys(node)) {
161
197
  if (key === 'loc' || key === 'type') continue;
162
198
  const val = node[key];
163
199
  if (Array.isArray(val)) {
164
200
  for (const item of val) {
165
- if (item && typeof item === 'object' && this._containsPropagate(item)) return true;
201
+ if (item && typeof item === 'object' && this._containsPropagate(item)) { result = true; break; }
166
202
  }
167
203
  } else if (val && typeof val === 'object' && val.type) {
168
- if (this._containsPropagate(val)) return true;
204
+ if (this._containsPropagate(val)) { result = true; break; }
169
205
  }
206
+ if (result) break;
170
207
  }
171
- return false;
208
+ this._propagateCache.set(node, result);
209
+ return result;
172
210
  }
173
211
 
174
212
  getPropagateHelper() {
@@ -186,8 +224,8 @@ export class BaseCodegen {
186
224
  generateStatement(node) {
187
225
  if (!node) return '';
188
226
 
189
- // Record source mapping before generating
190
- this._addMapping(node, this._outputLineCount);
227
+ // Record source mapping before generating (skip when source maps disabled)
228
+ if (this._sourceMapsEnabled) this._addMapping(node, this._outputLineCount);
191
229
 
192
230
  let result;
193
231
  switch (node.type) {
@@ -230,8 +268,8 @@ export class BaseCodegen {
230
268
  result = `${this.i()}${this.genExpression(node)};`;
231
269
  }
232
270
 
233
- // Track output line count using fast character scan
234
- if (result) {
271
+ // Track output line count using fast character scan (skip when source maps disabled)
272
+ if (this._sourceMapsEnabled && result) {
235
273
  this._outputLineCount += this._countLines(result) + 1; // +1 for the join newline
236
274
  }
237
275
 
@@ -297,23 +335,59 @@ export class BaseCodegen {
297
335
  const exportPrefix = node.isPublic ? 'export ' : '';
298
336
  if (node.targets.length === 1 && node.values.length === 1) {
299
337
  const target = node.targets[0];
338
+ const value = node.values[0];
300
339
  // Member expression target: obj.x = expr, arr[i] = expr
301
340
  if (typeof target === 'object' && target.type === 'MemberExpression') {
302
- return `${this.i()}${this.genExpression(target)} = ${this.genExpression(node.values[0])};`;
341
+ // IIFE elimination: match/if on RHS of member assignment
342
+ if (this._needsIIFE(value)) {
343
+ const memberExpr = this.genExpression(target);
344
+ const lines = [];
345
+ if (value.type === 'MatchExpression') {
346
+ lines.push(this._genMatchAssign(value, memberExpr));
347
+ } else {
348
+ lines.push(this._genIfAssign(value, memberExpr));
349
+ }
350
+ return lines.join('\n');
351
+ }
352
+ return `${this.i()}${this.genExpression(target)} = ${this.genExpression(value)};`;
303
353
  }
304
354
  if (target === '_') {
305
- return `${this.i()}${this.genExpression(node.values[0])};`;
355
+ return `${this.i()}${this.genExpression(value)};`;
306
356
  }
307
357
  if (this.isDeclared(target)) {
308
358
  // Reassignment to an already-declared variable (must be mutable)
309
- return `${this.i()}${target} = ${this.genExpression(node.values[0])};`;
359
+ // IIFE elimination: match/if on RHS of reassignment (skip if binding conflicts)
360
+ if (this._needsIIFE(value) && !this._matchBindingsConflict(value, target)) {
361
+ if (value.type === 'MatchExpression') {
362
+ return this._genMatchAssign(value, target);
363
+ } else {
364
+ return this._genIfAssign(value, target);
365
+ }
366
+ }
367
+ return `${this.i()}${target} = ${this.genExpression(value)};`;
310
368
  }
311
369
  this.declareVar(target);
312
370
  // Track top-level user definitions to avoid stdlib conflicts
313
371
  if (this._scopes.length === 1 && BUILTIN_NAMES.has(target)) {
314
372
  this._userDefinedNames.add(target);
315
373
  }
316
- return `${this.i()}${exportPrefix}const ${target} = ${this.genExpression(node.values[0])};`;
374
+ // @fast mode: track typed array local variables for loop optimization
375
+ if (this._fastMode && this._typedArrayLocals) {
376
+ const taType = this._detectTypedArrayExpr(value);
377
+ if (taType) this._typedArrayLocals.set(target, taType);
378
+ }
379
+ // IIFE elimination: match/if on RHS of new const declaration (skip if binding conflicts)
380
+ if (this._needsIIFE(value) && !this._matchBindingsConflict(value, target)) {
381
+ const lines = [];
382
+ lines.push(`${this.i()}${exportPrefix}let ${target};`);
383
+ if (value.type === 'MatchExpression') {
384
+ lines.push(this._genMatchAssign(value, target));
385
+ } else {
386
+ lines.push(this._genIfAssign(value, target));
387
+ }
388
+ return lines.join('\n');
389
+ }
390
+ return `${this.i()}${exportPrefix}const ${target} = ${this.genExpression(value)};`;
317
391
  }
318
392
 
319
393
  // Multiple assignment: a, b = 1, 2 (uses destructuring for atomicity)
@@ -351,8 +425,21 @@ export class BaseCodegen {
351
425
  genVarDeclaration(node) {
352
426
  const exportPrefix = node.isPublic ? 'export ' : '';
353
427
  if (node.targets.length === 1 && node.values.length === 1) {
354
- this.declareVar(node.targets[0]);
355
- return `${this.i()}${exportPrefix}let ${node.targets[0]} = ${this.genExpression(node.values[0])};`;
428
+ const target = node.targets[0];
429
+ const value = node.values[0];
430
+ this.declareVar(target);
431
+ // IIFE elimination for var declarations too (skip if binding conflicts)
432
+ if (this._needsIIFE(value) && !this._matchBindingsConflict(value, target)) {
433
+ const lines = [];
434
+ lines.push(`${this.i()}${exportPrefix}let ${target};`);
435
+ if (value.type === 'MatchExpression') {
436
+ lines.push(this._genMatchAssign(value, target));
437
+ } else {
438
+ lines.push(this._genIfAssign(value, target));
439
+ }
440
+ return lines.join('\n');
441
+ }
442
+ return `${this.i()}${exportPrefix}let ${target} = ${this.genExpression(value)};`;
356
443
  }
357
444
  const lines = [];
358
445
  for (let idx = 0; idx < node.targets.length; idx++) {
@@ -383,6 +470,30 @@ export class BaseCodegen {
383
470
  }
384
471
 
385
472
  genFunctionDeclaration(node) {
473
+ // Check for @wasm decorator — compile to WebAssembly
474
+ if (node.decorators && node.decorators.some(d => d.name === 'wasm')) {
475
+ return this.genWasmFunction(node);
476
+ }
477
+ // Check for @fast decorator — enable TypedArray optimizations
478
+ const isFast = node.decorators && node.decorators.some(d => d.name === 'fast');
479
+ const prevFastMode = this._fastMode;
480
+ const prevTypedParams = this._typedArrayParams;
481
+ const prevTypedLocals = this._typedArrayLocals;
482
+ if (isFast) {
483
+ this._fastMode = true;
484
+ this._typedArrayParams = new Map();
485
+ this._typedArrayLocals = new Map(); // track locally-created typed arrays
486
+ // Scan params for typed array annotations: param: [Int], param: [Float], param: [Byte]
487
+ for (const p of node.params) {
488
+ if (p.typeAnnotation && p.typeAnnotation.type === 'ArrayTypeAnnotation' && p.typeAnnotation.elementType) {
489
+ const elemName = p.typeAnnotation.elementType.name;
490
+ const typedArrayType = BaseCodegen.TYPED_ARRAY_MAP[elemName];
491
+ if (typedArrayType) {
492
+ this._typedArrayParams.set(p.name, typedArrayType);
493
+ }
494
+ }
495
+ }
496
+ }
386
497
  const params = this.genParams(node.params);
387
498
  const hasPropagate = this._containsPropagate(node.body);
388
499
  const isGenerator = this._containsYield(node.body);
@@ -399,20 +510,48 @@ export class BaseCodegen {
399
510
  }
400
511
  const body = this.genBlockBody(node.body);
401
512
  this.popScope();
402
- const p = [];
403
- p.push(`${this.i()}${exportPrefix}${asyncPrefix}function${genStar} ${node.name}(${params}) {`);
513
+ const lines = [];
514
+ lines.push(`${this.i()}${exportPrefix}${asyncPrefix}function${genStar} ${node.name}(${params}) {`);
515
+ // In @fast mode, convert typed array params at function entry
516
+ if (isFast && this._typedArrayParams.size > 0) {
517
+ for (const [pName, taType] of this._typedArrayParams) {
518
+ lines.push(`${this.i()} ${pName} = ${pName} instanceof ${taType} ? ${pName} : new ${taType}(${pName});`);
519
+ }
520
+ }
404
521
  if (hasPropagate) {
405
- p.push(`${this.i()} try {`);
406
- p.push(body);
407
- p.push(`${this.i()} } catch (__e) {`);
408
- p.push(`${this.i()} if (__e && __e.__tova_propagate) return __e.value;`);
409
- p.push(`${this.i()} throw __e;`);
410
- p.push(`${this.i()} }`);
522
+ lines.push(`${this.i()} try {`);
523
+ lines.push(body);
524
+ lines.push(`${this.i()} } catch (__e) {`);
525
+ lines.push(`${this.i()} if (__e && __e.__tova_propagate) return __e.value;`);
526
+ lines.push(`${this.i()} throw __e;`);
527
+ lines.push(`${this.i()} }`);
411
528
  } else {
412
- p.push(body);
529
+ lines.push(body);
530
+ }
531
+ lines.push(`${this.i()}}`);
532
+ // Restore @fast state
533
+ if (isFast) {
534
+ this._fastMode = prevFastMode;
535
+ this._typedArrayParams = prevTypedParams;
536
+ this._typedArrayLocals = prevTypedLocals;
537
+ }
538
+ return lines.join('\n');
539
+ }
540
+
541
+ genWasmFunction(node) {
542
+ try {
543
+ // Track as user-defined to suppress stdlib version
544
+ if (BUILTIN_NAMES.has(node.name)) this._userDefinedNames.add(node.name);
545
+ const wasmBytes = compileWasmFunction(node);
546
+ const glue = generateWasmGlue(node, wasmBytes);
547
+ const exportPrefix = node.isPublic ? 'export ' : '';
548
+ return `${this.i()}${exportPrefix}${glue}`;
549
+ } catch (e) {
550
+ // Fall back to JS if WASM compilation fails
551
+ console.error(`Warning: @wasm compilation failed for '${node.name}': ${e.message}. Falling back to JS.`);
552
+ node.decorators = node.decorators.filter(d => d.name !== 'wasm');
553
+ return this.genFunctionDeclaration(node);
413
554
  }
414
- p.push(`${this.i()}}`);
415
- return p.join('\n');
416
555
  }
417
556
 
418
557
  genParams(params) {
@@ -490,12 +629,136 @@ export class BaseCodegen {
490
629
  return p.join('');
491
630
  }
492
631
 
632
+ // Check if a for-loop over a range can be emitted as a C-style for loop
633
+ _isRangeForOptimizable(node) {
634
+ const vars = Array.isArray(node.variable) ? node.variable : [node.variable];
635
+ if (vars.length !== 1 || typeof vars[0] !== 'string' || node.isAsync || node.elseBody) return false;
636
+ if (node.iterable.type === 'RangeExpression') return true;
637
+ // Optimize for i in range(n) / range(start, end) / range(start, end, step)
638
+ if (node.iterable.type === 'CallExpression' &&
639
+ node.iterable.callee.type === 'Identifier' &&
640
+ node.iterable.callee.name === 'range' &&
641
+ node.iterable.arguments.length >= 1 && node.iterable.arguments.length <= 3) return true;
642
+ return false;
643
+ }
644
+
645
+ // @fast mode: detect if an expression produces a TypedArray
646
+ // Returns the TypedArray type string (e.g. 'Float64Array') or null
647
+ _detectTypedArrayExpr(value) {
648
+ if (!value) return null;
649
+ // Type.new(n) → new Type(n), where Type is a TypedArray
650
+ if (value.type === 'MethodCall' && value.methodName === 'new' &&
651
+ value.object && value.object.type === 'Identifier') {
652
+ const taType = BaseCodegen.TYPED_ARRAY_MAP[value.object.name];
653
+ if (taType) return taType;
654
+ // Direct TypedArray names: Float64Array.new(n)
655
+ if (Object.values(BaseCodegen.TYPED_ARRAY_MAP).includes(value.object.name)) return value.object.name;
656
+ }
657
+ // typed_add/typed_scale/typed_map/typed_sort return same type as input
658
+ if (value.type === 'CallExpression' && value.callee && value.callee.type === 'Identifier') {
659
+ const fname = value.callee.name;
660
+ if (['typed_add', 'typed_scale', 'typed_map', 'typed_sort'].includes(fname)) {
661
+ return 'Float64Array'; // conservative default
662
+ }
663
+ // typed_linspace returns Float64Array
664
+ if (fname === 'typed_linspace') return 'Float64Array';
665
+ }
666
+ return null;
667
+ }
668
+
669
+ // @fast mode: check if a for-loop iterates over a known typed array
670
+ // Returns the TypedArray type or null
671
+ _getTypedArrayIterable(node) {
672
+ if (!this._fastMode) return null;
673
+ const iter = node.iterable;
674
+ if (iter.type !== 'Identifier') return null;
675
+ const name = iter.name;
676
+ return this._typedArrayParams.get(name) || (this._typedArrayLocals && this._typedArrayLocals.get(name)) || null;
677
+ }
678
+
493
679
  genForStatement(node) {
494
680
  const vars = Array.isArray(node.variable) ? node.variable : [node.variable];
495
- const iterExpr = this.genExpression(node.iterable);
496
681
  const labelPrefix = node.label ? `${node.label}: ` : '';
497
682
  const awaitKeyword = node.isAsync ? ' await' : '';
498
683
 
684
+ // Optimization: for i in start..end => C-style for loop (avoids array allocation)
685
+ if (this._isRangeForOptimizable(node)) {
686
+ const varName = vars[0];
687
+ let start, end, step, cmpOp;
688
+
689
+ if (node.iterable.type === 'RangeExpression') {
690
+ start = this.genExpression(node.iterable.start);
691
+ end = this.genExpression(node.iterable.end);
692
+ cmpOp = node.iterable.inclusive ? '<=' : '<';
693
+ step = null;
694
+ } else {
695
+ // range(n) / range(start, end) / range(start, end, step)
696
+ const args = node.iterable.arguments;
697
+ if (args.length === 1) {
698
+ start = '0';
699
+ end = this.genExpression(args[0]);
700
+ cmpOp = '<';
701
+ } else if (args.length === 2) {
702
+ start = this.genExpression(args[0]);
703
+ end = this.genExpression(args[1]);
704
+ cmpOp = '<';
705
+ } else {
706
+ start = this.genExpression(args[0]);
707
+ end = this.genExpression(args[1]);
708
+ step = this.genExpression(args[2]);
709
+ cmpOp = '<';
710
+ }
711
+ }
712
+
713
+ this.pushScope();
714
+ this.declareVar(varName);
715
+ const p = [];
716
+ if (step) {
717
+ // With explicit step: need to handle positive and negative step
718
+ const stepVar = `__step_${this._uid()}`;
719
+ p.push(`${this.i()}${labelPrefix}{ const ${stepVar} = ${step};\n`);
720
+ p.push(`${this.i()}for (let ${varName} = ${start}; ${stepVar} > 0 ? ${varName} < ${end} : ${varName} > ${end}; ${varName} += ${stepVar}) {\n`);
721
+ } else {
722
+ p.push(`${this.i()}${labelPrefix}for (let ${varName} = ${start}; ${varName} ${cmpOp} ${end}; ${varName}++) {\n`);
723
+ }
724
+ this.indent++;
725
+ if (node.guard) {
726
+ p.push(`${this.i()}if (!(${this.genExpression(node.guard)})) continue;\n`);
727
+ }
728
+ p.push(this.genBlockStatements(node.body));
729
+ this.indent--;
730
+ p.push(`\n${this.i()}}`);
731
+ if (step) p.push(`\n${this.i()}}`);
732
+ this.popScope();
733
+ return p.join('');
734
+ }
735
+
736
+ // @fast mode optimization: for val in typedArray => index-based loop (avoids iterator overhead)
737
+ if (vars.length === 1 && !node.isAsync && !node.elseBody) {
738
+ const taType = this._getTypedArrayIterable(node);
739
+ if (taType) {
740
+ const varName = vars[0];
741
+ const arrName = node.iterable.name;
742
+ const idxVar = `__i_${this._uid()}`;
743
+ this.pushScope();
744
+ this.declareVar(varName);
745
+ const p = [];
746
+ p.push(`${this.i()}${labelPrefix}for (let ${idxVar} = 0; ${idxVar} < ${arrName}.length; ${idxVar}++) {\n`);
747
+ this.indent++;
748
+ p.push(`${this.i()}const ${varName} = ${arrName}[${idxVar}];\n`);
749
+ if (node.guard) {
750
+ p.push(`${this.i()}if (!(${this.genExpression(node.guard)})) continue;\n`);
751
+ }
752
+ p.push(this.genBlockStatements(node.body));
753
+ this.indent--;
754
+ p.push(`\n${this.i()}}`);
755
+ this.popScope();
756
+ return p.join('');
757
+ }
758
+ }
759
+
760
+ const iterExpr = this.genExpression(node.iterable);
761
+
499
762
  if (node.elseBody) {
500
763
  // for-else: run else if iterable was empty
501
764
  const tempVar = `__iter_${this._uid()}`;
@@ -684,7 +947,15 @@ export class BaseCodegen {
684
947
  // Implicit return: last expression in function body
685
948
  // Skip implicit return for known void/side-effect-only calls (print, assert, etc.)
686
949
  if (isLast && stmt.type === 'ExpressionStatement' && !this._isVoidCall(stmt.expression)) {
687
- lines.push(`${this.i()}return ${this.genExpression(stmt.expression)};`);
950
+ // IIFE elimination: match/if as last expression in function body → direct returns
951
+ const expr = stmt.expression;
952
+ if (expr.type === 'MatchExpression' && !this._isSimpleMatch(expr)) {
953
+ lines.push(this._genMatchReturn(expr));
954
+ } else if (expr.type === 'IfExpression' && this._needsIIFE(expr)) {
955
+ lines.push(this._genIfReturn(expr));
956
+ } else {
957
+ lines.push(`${this.i()}return ${this.genExpression(stmt.expression)};`);
958
+ }
688
959
  } else if (isLast && stmt.type === 'IfStatement' && stmt.elseBody) {
689
960
  lines.push(this._genIfStatementWithReturns(stmt));
690
961
  } else if (isLast && stmt.type === 'MatchExpression') {
@@ -759,10 +1030,61 @@ export class BaseCodegen {
759
1030
  return lines.join('\n');
760
1031
  }
761
1032
 
1033
+ _genBlockBodyAssign(block, targetVar) {
1034
+ // Like _genBlockBodyReturns but emits `targetVar = expr` instead of `return expr`
1035
+ if (!block) return '';
1036
+ const stmts = block.type === 'BlockStatement' ? block.body : [block];
1037
+ this.indent++;
1038
+ const lines = [];
1039
+ for (let idx = 0; idx < stmts.length; idx++) {
1040
+ const stmt = stmts[idx];
1041
+ const isLast = idx === stmts.length - 1;
1042
+ if (isLast && stmt.type === 'ExpressionStatement') {
1043
+ lines.push(`${this.i()}${targetVar} = ${this.genExpression(stmt.expression)};`);
1044
+ } else if (isLast && stmt.type === 'IfStatement' && stmt.elseBody) {
1045
+ lines.push(this._genIfStatementWithAssigns(stmt, targetVar));
1046
+ } else if (isLast && stmt.type === 'MatchExpression') {
1047
+ // Nested match inside block — generate as assignment too
1048
+ lines.push(this._genMatchAssign(stmt, targetVar));
1049
+ } else {
1050
+ lines.push(this.generateStatement(stmt));
1051
+ }
1052
+ }
1053
+ this.indent--;
1054
+ return lines.join('\n');
1055
+ }
1056
+
1057
+ _genIfStatementWithAssigns(node, targetVar) {
1058
+ const p = [];
1059
+ p.push(`${this.i()}if (${this.genExpression(node.condition)}) {\n`);
1060
+ p.push(this._genBlockBodyAssign(node.consequent, targetVar));
1061
+ p.push(`\n${this.i()}}`);
1062
+
1063
+ for (const alt of node.alternates) {
1064
+ p.push(` else if (${this.genExpression(alt.condition)}) {\n`);
1065
+ p.push(this._genBlockBodyAssign(alt.body, targetVar));
1066
+ p.push(`\n${this.i()}}`);
1067
+ }
1068
+
1069
+ if (node.elseBody) {
1070
+ p.push(` else {\n`);
1071
+ p.push(this._genBlockBodyAssign(node.elseBody, targetVar));
1072
+ p.push(`\n${this.i()}}`);
1073
+ }
1074
+
1075
+ return p.join('');
1076
+ }
1077
+
762
1078
  genBlockStatements(block) {
763
1079
  if (!block) return '';
764
1080
  const stmts = block.type === 'BlockStatement' ? block.body : [block];
765
- return stmts.map(s => this.generateStatement(s)).join('\n');
1081
+ const lines = [];
1082
+ for (const s of stmts) {
1083
+ lines.push(this.generateStatement(s));
1084
+ // Dead code elimination: stop after unconditional return/break/continue
1085
+ if (s.type === 'ReturnStatement' || s.type === 'BreakStatement' || s.type === 'ContinueStatement') break;
1086
+ }
1087
+ return lines.join('\n');
766
1088
  }
767
1089
 
768
1090
  // ─── Expressions ──────────────────────────────────────────
@@ -778,9 +1100,32 @@ export class BaseCodegen {
778
1100
  }
779
1101
 
780
1102
  genBinaryExpression(node) {
1103
+ const op = node.operator;
1104
+
1105
+ // Constant folding: arithmetic on two number literals
1106
+ if (node.left.type === 'NumberLiteral' && node.right.type === 'NumberLiteral') {
1107
+ const l = node.left.value, r = node.right.value;
1108
+ let folded = null;
1109
+ switch (op) {
1110
+ case '+': folded = l + r; break;
1111
+ case '-': folded = l - r; break;
1112
+ case '*': folded = l * r; break;
1113
+ case '/': if (r !== 0) folded = l / r; break;
1114
+ case '%': if (r !== 0) folded = l % r; break;
1115
+ case '**': folded = l ** r; break;
1116
+ }
1117
+ if (folded !== null && Number.isFinite(folded)) {
1118
+ return folded < 0 ? `(${folded})` : String(folded);
1119
+ }
1120
+ }
1121
+
1122
+ // Constant folding: string concatenation with ++
1123
+ if (op === '++' && node.left.type === 'StringLiteral' && node.right.type === 'StringLiteral') {
1124
+ return JSON.stringify(node.left.value + node.right.value);
1125
+ }
1126
+
781
1127
  const left = this.genExpression(node.left);
782
1128
  const right = this.genExpression(node.right);
783
- const op = node.operator;
784
1129
 
785
1130
  // String multiply: "ha" * 3 => "ha".repeat(3), also x * 3 when x is string
786
1131
  if (op === '*' &&
@@ -1533,12 +1878,83 @@ export class BaseCodegen {
1533
1878
  return `(${result})`;
1534
1879
  }
1535
1880
 
1881
+ // Check if all arms are literal patterns (string/number/boolean) or wildcard, with no guards
1882
+ _isLiteralMatch(node) {
1883
+ let hasWildcard = false;
1884
+ for (const arm of node.arms) {
1885
+ if (arm.guard) return false;
1886
+ const pt = arm.pattern.type;
1887
+ if (pt === 'LiteralPattern') continue;
1888
+ if (pt === 'WildcardPattern' || pt === 'BindingPattern') {
1889
+ hasWildcard = true;
1890
+ continue;
1891
+ }
1892
+ return false;
1893
+ }
1894
+ return true;
1895
+ }
1896
+
1897
+ _genSwitchMatch(node) {
1898
+ const subject = this.genExpression(node.subject);
1899
+ const tempVar = '__match';
1900
+ const p = [];
1901
+ p.push(`((${tempVar}) => {\n`);
1902
+ this.indent++;
1903
+ p.push(`${this.i()}switch (${tempVar}) {\n`);
1904
+ this.indent++;
1905
+
1906
+ for (const arm of node.arms) {
1907
+ if (arm.pattern.type === 'WildcardPattern') {
1908
+ p.push(`${this.i()}default:\n`);
1909
+ this.indent++;
1910
+ if (arm.body.type === 'BlockStatement') {
1911
+ p.push(this.genBlockBody(arm.body) + '\n');
1912
+ } else {
1913
+ p.push(`${this.i()}return ${this.genExpression(arm.body)};\n`);
1914
+ }
1915
+ this.indent--;
1916
+ } else if (arm.pattern.type === 'BindingPattern') {
1917
+ p.push(`${this.i()}default: {\n`);
1918
+ this.indent++;
1919
+ p.push(`${this.i()}const ${arm.pattern.name} = ${tempVar};\n`);
1920
+ if (arm.body.type === 'BlockStatement') {
1921
+ p.push(this.genBlockBody(arm.body) + '\n');
1922
+ } else {
1923
+ p.push(`${this.i()}return ${this.genExpression(arm.body)};\n`);
1924
+ }
1925
+ this.indent--;
1926
+ p.push(`${this.i()}}\n`);
1927
+ } else {
1928
+ // LiteralPattern
1929
+ p.push(`${this.i()}case ${JSON.stringify(arm.pattern.value)}:\n`);
1930
+ this.indent++;
1931
+ if (arm.body.type === 'BlockStatement') {
1932
+ p.push(this.genBlockBody(arm.body) + '\n');
1933
+ } else {
1934
+ p.push(`${this.i()}return ${this.genExpression(arm.body)};\n`);
1935
+ }
1936
+ this.indent--;
1937
+ }
1938
+ }
1939
+
1940
+ this.indent--;
1941
+ p.push(`${this.i()}}\n`);
1942
+ this.indent--;
1943
+ p.push(`${this.i()}})(${subject})`);
1944
+ return p.join('');
1945
+ }
1946
+
1536
1947
  genMatchExpression(node) {
1537
1948
  // Optimization: simple matches emit ternary chain instead of IIFE
1538
1949
  if (this._isSimpleMatch(node)) {
1539
1950
  return this._genSimpleMatch(node);
1540
1951
  }
1541
1952
 
1953
+ // Optimization: literal-only patterns emit switch for V8 jump tables
1954
+ if (this._isLiteralMatch(node)) {
1955
+ return this._genSwitchMatch(node);
1956
+ }
1957
+
1542
1958
  // Generate as IIFE with if-else chain
1543
1959
  const subject = this.genExpression(node.subject);
1544
1960
  const tempVar = '__match';
@@ -1635,6 +2051,280 @@ export class BaseCodegen {
1635
2051
  return p.join('');
1636
2052
  }
1637
2053
 
2054
+ // ─── IIFE-free match/if codegen (assign to variable instead of return) ───
2055
+
2056
+ _genMatchAssign(node, targetVar) {
2057
+ // Block-scoped match that assigns to targetVar instead of wrapping in IIFE
2058
+ // Handles switch optimization and general if-else chain
2059
+
2060
+ // Simple ternary matches don't need this optimization (already no IIFE)
2061
+ // Literal-only switch matches benefit from assignment form
2062
+
2063
+ if (this._isLiteralMatch(node)) {
2064
+ return this._genSwitchMatchAssign(node, targetVar);
2065
+ }
2066
+
2067
+ const subject = this.genExpression(node.subject);
2068
+ const tempVar = `__match`;
2069
+ const p = [];
2070
+ p.push(`${this.i()}{\n`);
2071
+ this.indent++;
2072
+ p.push(`${this.i()}const ${tempVar} = ${subject};\n`);
2073
+
2074
+ for (let idx = 0; idx < node.arms.length; idx++) {
2075
+ const arm = node.arms[idx];
2076
+ const condition = this.genPatternCondition(arm.pattern, tempVar, arm.guard);
2077
+
2078
+ if (arm.pattern.type === 'WildcardPattern' || arm.pattern.type === 'BindingPattern') {
2079
+ if (idx === node.arms.length - 1 && !arm.guard) {
2080
+ // Default case — wrap in else if preceded by if branches
2081
+ if (idx > 0) {
2082
+ p.push(` else {\n`);
2083
+ this.indent++;
2084
+ }
2085
+ if (arm.pattern.type === 'BindingPattern') {
2086
+ p.push(`${this.i()}const ${arm.pattern.name} = ${tempVar};\n`);
2087
+ }
2088
+ if (arm.body.type === 'BlockStatement') {
2089
+ p.push(this._genBlockBodyAssign(arm.body, targetVar) + '\n');
2090
+ } else {
2091
+ p.push(`${this.i()}${targetVar} = ${this.genExpression(arm.body)};\n`);
2092
+ }
2093
+ if (idx > 0) {
2094
+ this.indent--;
2095
+ p.push(`${this.i()}}\n`);
2096
+ }
2097
+ break;
2098
+ }
2099
+ }
2100
+
2101
+ if (idx === 0) {
2102
+ p.push(`${this.i()}if (${condition}) {\n`);
2103
+ } else {
2104
+ p.push(` else if (${condition}) {\n`);
2105
+ }
2106
+ this.indent++;
2107
+
2108
+ // Bind variables from pattern
2109
+ p.push(this.genPatternBindings(arm.pattern, tempVar));
2110
+
2111
+ if (arm.body.type === 'BlockStatement') {
2112
+ p.push(this._genBlockBodyAssign(arm.body, targetVar) + '\n');
2113
+ } else {
2114
+ p.push(`${this.i()}${targetVar} = ${this.genExpression(arm.body)};\n`);
2115
+ }
2116
+ this.indent--;
2117
+ p.push(`${this.i()}}`);
2118
+ }
2119
+ p.push('\n');
2120
+
2121
+ this.indent--;
2122
+ p.push(`${this.i()}}`);
2123
+ return p.join('');
2124
+ }
2125
+
2126
+ _genSwitchMatchAssign(node, targetVar) {
2127
+ // Switch-based match that assigns to targetVar instead of wrapping in IIFE
2128
+ const subject = this.genExpression(node.subject);
2129
+ const tempVar = '__match';
2130
+ const p = [];
2131
+ p.push(`${this.i()}{\n`);
2132
+ this.indent++;
2133
+ p.push(`${this.i()}const ${tempVar} = ${subject};\n`);
2134
+ p.push(`${this.i()}switch (${tempVar}) {\n`);
2135
+ this.indent++;
2136
+
2137
+ for (const arm of node.arms) {
2138
+ if (arm.pattern.type === 'WildcardPattern') {
2139
+ p.push(`${this.i()}default:\n`);
2140
+ this.indent++;
2141
+ if (arm.body.type === 'BlockStatement') {
2142
+ p.push(this._genBlockBodyAssign(arm.body, targetVar) + '\n');
2143
+ } else {
2144
+ p.push(`${this.i()}${targetVar} = ${this.genExpression(arm.body)};\n`);
2145
+ }
2146
+ p.push(`${this.i()}break;\n`);
2147
+ this.indent--;
2148
+ } else if (arm.pattern.type === 'BindingPattern') {
2149
+ p.push(`${this.i()}default: {\n`);
2150
+ this.indent++;
2151
+ p.push(`${this.i()}const ${arm.pattern.name} = ${tempVar};\n`);
2152
+ if (arm.body.type === 'BlockStatement') {
2153
+ p.push(this._genBlockBodyAssign(arm.body, targetVar) + '\n');
2154
+ } else {
2155
+ p.push(`${this.i()}${targetVar} = ${this.genExpression(arm.body)};\n`);
2156
+ }
2157
+ p.push(`${this.i()}break;\n`);
2158
+ this.indent--;
2159
+ p.push(`${this.i()}}\n`);
2160
+ } else {
2161
+ // LiteralPattern
2162
+ p.push(`${this.i()}case ${JSON.stringify(arm.pattern.value)}:\n`);
2163
+ this.indent++;
2164
+ if (arm.body.type === 'BlockStatement') {
2165
+ p.push(this._genBlockBodyAssign(arm.body, targetVar) + '\n');
2166
+ } else {
2167
+ p.push(`${this.i()}${targetVar} = ${this.genExpression(arm.body)};\n`);
2168
+ }
2169
+ p.push(`${this.i()}break;\n`);
2170
+ this.indent--;
2171
+ }
2172
+ }
2173
+
2174
+ this.indent--;
2175
+ p.push(`${this.i()}}\n`);
2176
+ this.indent--;
2177
+ p.push(`${this.i()}}`);
2178
+ return p.join('');
2179
+ }
2180
+
2181
+ _genIfAssign(node, targetVar) {
2182
+ // Block-scoped if expression that assigns to targetVar instead of wrapping in IIFE
2183
+ const p = [];
2184
+ p.push(`${this.i()}if (${this.genExpression(node.condition)}) {\n`);
2185
+ p.push(this._genBlockBodyAssign(node.consequent, targetVar));
2186
+ p.push(`\n${this.i()}}`);
2187
+
2188
+ for (const alt of node.alternates) {
2189
+ p.push(` else if (${this.genExpression(alt.condition)}) {\n`);
2190
+ p.push(this._genBlockBodyAssign(alt.body, targetVar));
2191
+ p.push(`\n${this.i()}}`);
2192
+ }
2193
+
2194
+ if (node.elseBody) {
2195
+ p.push(` else {\n`);
2196
+ p.push(this._genBlockBodyAssign(node.elseBody, targetVar));
2197
+ p.push(`\n${this.i()}}`);
2198
+ }
2199
+
2200
+ return p.join('');
2201
+ }
2202
+
2203
+ // IIFE-free match/if that emits direct returns (for function body last expression)
2204
+ _genMatchReturn(node) {
2205
+ const subject = this.genExpression(node.subject);
2206
+ const tempVar = `__match`;
2207
+ const p = [];
2208
+ p.push(`${this.i()}{\n`);
2209
+ this.indent++;
2210
+ p.push(`${this.i()}const ${tempVar} = ${subject};\n`);
2211
+
2212
+ for (let idx = 0; idx < node.arms.length; idx++) {
2213
+ const arm = node.arms[idx];
2214
+ const condition = this.genPatternCondition(arm.pattern, tempVar, arm.guard);
2215
+
2216
+ if (arm.pattern.type === 'WildcardPattern' || arm.pattern.type === 'BindingPattern') {
2217
+ if (idx === node.arms.length - 1 && !arm.guard) {
2218
+ if (idx > 0) {
2219
+ p.push(` else {\n`);
2220
+ this.indent++;
2221
+ }
2222
+ if (arm.pattern.type === 'BindingPattern') {
2223
+ p.push(`${this.i()}const ${arm.pattern.name} = ${tempVar};\n`);
2224
+ }
2225
+ if (arm.body.type === 'BlockStatement') {
2226
+ p.push(this._genBlockBodyReturns(arm.body) + '\n');
2227
+ } else {
2228
+ p.push(`${this.i()}return ${this.genExpression(arm.body)};\n`);
2229
+ }
2230
+ if (idx > 0) {
2231
+ this.indent--;
2232
+ p.push(`${this.i()}}\n`);
2233
+ }
2234
+ break;
2235
+ }
2236
+ }
2237
+
2238
+ if (idx === 0) {
2239
+ p.push(`${this.i()}if (${condition}) {\n`);
2240
+ } else {
2241
+ p.push(` else if (${condition}) {\n`);
2242
+ }
2243
+ this.indent++;
2244
+ p.push(this.genPatternBindings(arm.pattern, tempVar));
2245
+ if (arm.body.type === 'BlockStatement') {
2246
+ p.push(this._genBlockBodyReturns(arm.body) + '\n');
2247
+ } else {
2248
+ p.push(`${this.i()}return ${this.genExpression(arm.body)};\n`);
2249
+ }
2250
+ this.indent--;
2251
+ p.push(`${this.i()}}`);
2252
+ }
2253
+ p.push('\n');
2254
+ this.indent--;
2255
+ p.push(`${this.i()}}`);
2256
+ return p.join('');
2257
+ }
2258
+
2259
+ _genIfReturn(node) {
2260
+ const p = [];
2261
+ p.push(`${this.i()}if (${this.genExpression(node.condition)}) {\n`);
2262
+ p.push(this._genBlockBodyReturns(node.consequent));
2263
+ p.push(`\n${this.i()}}`);
2264
+
2265
+ for (const alt of node.alternates) {
2266
+ p.push(` else if (${this.genExpression(alt.condition)}) {\n`);
2267
+ p.push(this._genBlockBodyReturns(alt.body));
2268
+ p.push(`\n${this.i()}}`);
2269
+ }
2270
+
2271
+ if (node.elseBody) {
2272
+ p.push(` else {\n`);
2273
+ p.push(this._genBlockBodyReturns(node.elseBody));
2274
+ p.push(`\n${this.i()}}`);
2275
+ }
2276
+
2277
+ return p.join('');
2278
+ }
2279
+
2280
+ // Check if a match/if expression would need IIFE (not simple ternary)
2281
+ _needsIIFE(node) {
2282
+ if (node.type === 'MatchExpression') {
2283
+ return !this._isSimpleMatch(node);
2284
+ }
2285
+ if (node.type === 'IfExpression') {
2286
+ const isSingleExpr = (block) =>
2287
+ block.type === 'BlockStatement' && block.body.length === 1 && block.body[0].type === 'ExpressionStatement';
2288
+ // Simple if/elif → ternary (no IIFE)
2289
+ if (node.alternates.length === 0 && isSingleExpr(node.consequent) && isSingleExpr(node.elseBody)) return false;
2290
+ if (node.alternates.length > 0 && node.elseBody &&
2291
+ isSingleExpr(node.consequent) && isSingleExpr(node.elseBody) &&
2292
+ node.alternates.every(alt => isSingleExpr(alt.body))) return false;
2293
+ // Otherwise needs IIFE (or our new assign path)
2294
+ return true;
2295
+ }
2296
+ return false;
2297
+ }
2298
+
2299
+ // Check if a match has any pattern bindings that would conflict with the target variable
2300
+ _matchBindingsConflict(node, targetVar) {
2301
+ if (node.type !== 'MatchExpression') return false;
2302
+ for (const arm of node.arms) {
2303
+ if (this._patternBindsName(arm.pattern, targetVar)) return true;
2304
+ }
2305
+ return false;
2306
+ }
2307
+
2308
+ _patternBindsName(pattern, name) {
2309
+ switch (pattern.type) {
2310
+ case 'BindingPattern':
2311
+ return pattern.name === name;
2312
+ case 'VariantPattern':
2313
+ return pattern.fields.some(f => {
2314
+ if (typeof f === 'string') return f === name;
2315
+ if (f && f.type) return this._patternBindsName(f, name);
2316
+ return false;
2317
+ });
2318
+ case 'ArrayPattern':
2319
+ case 'TuplePattern':
2320
+ return pattern.elements.some(el => el && this._patternBindsName(el, name));
2321
+ case 'StringConcatPattern':
2322
+ return pattern.rest && pattern.rest.type === 'BindingPattern' && pattern.rest.name === name;
2323
+ default:
2324
+ return false;
2325
+ }
2326
+ }
2327
+
1638
2328
  genPatternCondition(pattern, subject, guard) {
1639
2329
  let cond;
1640
2330
 
@@ -1764,6 +2454,12 @@ export class BaseCodegen {
1764
2454
 
1765
2455
  genArrayLiteral(node) {
1766
2456
  const elements = node.elements.map(e => this.genExpression(e)).join(', ');
2457
+ // In @fast mode, detect all-numeric arrays and emit TypedArrays
2458
+ if (this._fastMode && node.elements.length > 0 && node.elements.every(e => e.type === 'NumberLiteral')) {
2459
+ const hasFloat = node.elements.some(e => String(e.value).includes('.'));
2460
+ const taType = hasFloat ? 'Float64Array' : 'Int32Array';
2461
+ return `new ${taType}([${elements}])`;
2462
+ }
1767
2463
  return `[${elements}]`;
1768
2464
  }
1769
2465
 
@@ -1791,7 +2487,8 @@ export class BaseCodegen {
1791
2487
  if (expr === varName) {
1792
2488
  return `${iter}.filter((${varName}) => ${cond})`;
1793
2489
  }
1794
- return `${iter}.filter((${varName}) => ${cond}).map((${varName}) => ${expr})`;
2490
+ // Single-pass loop avoids intermediate array from filter().map()
2491
+ return `${iter}.reduce((acc, ${varName}) => { if (${cond}) acc.push(${expr}); return acc; }, [])`;
1795
2492
  }
1796
2493
  return `${iter}.map((${varName}) => ${expr})`;
1797
2494
  }
@@ -1815,10 +2512,12 @@ export class BaseCodegen {
1815
2512
  genRangeExpression(node) {
1816
2513
  const start = this.genExpression(node.start);
1817
2514
  const end = this.genExpression(node.end);
2515
+ // Use stdlib range() — handles step and direction, avoids Array.from overhead
2516
+ this._trackBuiltin('range');
1818
2517
  if (node.inclusive) {
1819
- return `Array.from({length: (${end}) - (${start}) + 1}, (_, i) => (${start}) + i)`;
2518
+ return `range(${start}, (${end}) + 1)`;
1820
2519
  }
1821
- return `Array.from({length: (${end}) - (${start})}, (_, i) => (${start}) + i)`;
2520
+ return `range(${start}, ${end})`;
1822
2521
  }
1823
2522
 
1824
2523
  genSliceExpression(node) {
@@ -1885,14 +2584,19 @@ export class BaseCodegen {
1885
2584
  for (const variant of node.variants) {
1886
2585
  if (variant.type === 'TypeVariant') {
1887
2586
  this.declareVar(variant.name);
1888
- const fieldNames = variant.fields.map(f => f.name);
2587
+ const rawFieldNames = variant.fields.map(f => f.name);
2588
+ // Deduplicate field names: Add(Expr, Expr) → _0, _1 (prevents property collision)
2589
+ const nameCount = {};
2590
+ rawFieldNames.forEach(n => nameCount[n] = (nameCount[n] || 0) + 1);
2591
+ const hasDupes = Object.values(nameCount).some(c => c > 1);
2592
+ const fieldNames = hasDupes ? rawFieldNames.map((_, i) => `_${i}`) : rawFieldNames;
1889
2593
  this._variantFields[variant.name] = fieldNames;
1890
2594
  if (variant.fields.length === 0) {
1891
2595
  lines.push(`${this.i()}${exportPrefix}const ${variant.name} = Object.freeze({ __tag: "${variant.name}" });`);
1892
2596
  } else {
1893
2597
  const params = fieldNames.join(', ');
1894
2598
  const obj = fieldNames.map(f => `${f}`).join(', ');
1895
- lines.push(`${this.i()}${exportPrefix}function ${variant.name}(${params}) { return Object.freeze({ __tag: "${variant.name}", ${obj} }); }`);
2599
+ lines.push(`${this.i()}${exportPrefix}function ${variant.name}(${params}) { return { __tag: "${variant.name}", ${obj} }; }`);
1896
2600
  }
1897
2601
  }
1898
2602
  }
@@ -2087,17 +2791,22 @@ export class BaseCodegen {
2087
2791
  if (!node) return false;
2088
2792
  if (node.type === 'YieldExpression') return true;
2089
2793
  if (node.type === 'FunctionDeclaration' || node.type === 'LambdaExpression') return false;
2794
+ const cached = this._yieldCache.get(node);
2795
+ if (cached !== undefined) return cached;
2796
+ let result = false;
2090
2797
  for (const key of Object.keys(node)) {
2091
2798
  if (key === 'loc' || key === 'type') continue;
2092
2799
  const val = node[key];
2093
2800
  if (Array.isArray(val)) {
2094
2801
  for (const item of val) {
2095
- if (item && typeof item === 'object' && this._containsYield(item)) return true;
2802
+ if (item && typeof item === 'object' && this._containsYield(item)) { result = true; break; }
2096
2803
  }
2097
2804
  } else if (val && typeof val === 'object' && val.type) {
2098
- if (this._containsYield(val)) return true;
2805
+ if (this._containsYield(val)) { result = true; break; }
2099
2806
  }
2807
+ if (result) break;
2100
2808
  }
2101
- return false;
2809
+ this._yieldCache.set(node, result);
2810
+ return result;
2102
2811
  }
2103
2812
  }