depyo 1.0.2 → 1.1.0

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.
@@ -22,7 +22,12 @@ function handleDeleteNameA() {
22
22
  }
23
23
 
24
24
  if (varname.length >= 2 && varname.startsWith('_[')) {
25
- /* Don't show deletes that are a result of list comps. */
25
+ /* Don't show deletes that are a result of list comps.
26
+ * Clear the _listCompAppendRefs cache so the next FOR_ITER outside
27
+ * this listcomp doesn't mistake itself for one. */
28
+ if (this._listCompAppendRefs) {
29
+ delete this._listCompAppendRefs[varname];
30
+ }
26
31
  return;
27
32
  }
28
33
 
@@ -39,7 +44,12 @@ function handleDeleteFastA() {
39
44
  let nameNode = new AST.ASTName(nameVal);
40
45
 
41
46
  if (nameNode.name.startsWith('_[')) {
42
- /* Don't show deletes that are a result of list comps. */
47
+ /* Don't show deletes that are a result of list comps.
48
+ * Clear the _listCompAppendRefs cache so the next FOR_ITER outside
49
+ * this listcomp doesn't mistake itself for one. */
50
+ if (this._listCompAppendRefs) {
51
+ delete this._listCompAppendRefs[nameNode.name];
52
+ }
43
53
  return;
44
54
  }
45
55
 
@@ -112,6 +122,10 @@ function handleLoadClassderefA() {
112
122
  function processLoadDeref() {
113
123
  let varName = this.code.Current.FreeName || "";
114
124
  if (varName.length >= 2 && varName.startsWith('_[')) {
125
+ const cached = this._listCompAppendRefs?.[varName];
126
+ if (cached) {
127
+ this.dataStack.push(cached);
128
+ }
115
129
  return;
116
130
  }
117
131
  let node = new AST.ASTName (varName);
@@ -122,6 +136,14 @@ function processLoadDeref() {
122
136
  function handleLoadFastA() {
123
137
  let varName = this.code.Current.Name || "";
124
138
  if (varName.length >= 2 && varName.startsWith('_[')) {
139
+ // Pre-LIST_APPEND comprehensions (Python 2.3) cache `list.append` in
140
+ // the synthetic `_[N]` slot and re-load it to call per iteration.
141
+ // Replay the stored expression so CALL_FUNCTION has a real callable
142
+ // instead of falling through to `##ERROR##`.
143
+ const cached = this._listCompAppendRefs?.[varName];
144
+ if (cached) {
145
+ this.dataStack.push(cached);
146
+ }
125
147
  return;
126
148
  }
127
149
 
@@ -138,6 +160,10 @@ function handleLoadFastCheckA() {
138
160
  function handleLoadGlobalA() {
139
161
  let varName = this.code.Current.Name || "";
140
162
  if (varName.length >= 2 && varName.startsWith('_[')) {
163
+ const cached = this._listCompAppendRefs?.[varName];
164
+ if (cached) {
165
+ this.dataStack.push(cached);
166
+ }
141
167
  return;
142
168
  }
143
169
 
@@ -157,8 +183,15 @@ function handleLoadGlobalA() {
157
183
  }
158
184
 
159
185
  function handleLoadFromDictOrDerefA() {
160
- // Python 3.12+: load from locals dict or closure; approximate as a normal name load.
161
- const varName = this.code.Current.Name || "";
186
+ // Python 3.12+: LOAD_FROM_DICT_OR_DEREF consumes the locals dict from TOS
187
+ // (pushed by the preceding LOAD_LOCALS) and resolves the name against it,
188
+ // falling back to the deref cell. The argument is a localsplus index into
189
+ // [VarNames | CellVars | FreeVars] — use FreeName which the opcode reader
190
+ // resolved via that table.
191
+ if (this.dataStack.top() instanceof AST.ASTLocals) {
192
+ this.dataStack.pop();
193
+ }
194
+ const varName = this.code.Current.FreeName || this.code.Current.Name || "";
162
195
  if (!varName.length) {
163
196
  return;
164
197
  }
@@ -188,6 +221,10 @@ function handleLoadMethodA() {
188
221
  function handleLoadNameA() {
189
222
  let varName = this.code.Current.Name || "";
190
223
  if (varName.length >= 2 && varName.startsWith('_[')) {
224
+ const cached = this._listCompAppendRefs?.[varName];
225
+ if (cached) {
226
+ this.dataStack.push(cached);
227
+ }
191
228
  return;
192
229
  }
193
230
  let node = new AST.ASTName (varName);
@@ -349,9 +386,30 @@ function handleStoreDerefA() {
349
386
  }
350
387
  }
351
388
  } else {
352
- let valueNode = this.dataStack.pop();
353
389
  let nameNode = new AST.ASTName(this.code.Current.FreeName);
354
390
 
391
+ // Closure-captured for-loop variable: STORE_DEREF to a cell var while an
392
+ // uninitialized For block is on the stack means this is the loop index,
393
+ // not an assignment. Py 3.0 classes with decorators closing over the
394
+ // loop variable use this pattern (STORE_DEREF for the captured name,
395
+ // STORE_FAST for the class afterwards).
396
+ let parentForBlock = null;
397
+ for (let i = this.blocks.length - 1; i >= 0; i--) {
398
+ if ((this.blocks[i].blockType == AST.ASTBlock.BlockType.For ||
399
+ this.blocks[i].blockType == AST.ASTBlock.BlockType.AsyncFor) &&
400
+ !this.blocks[i].inited) {
401
+ parentForBlock = this.blocks[i];
402
+ break;
403
+ }
404
+ }
405
+ if (parentForBlock) {
406
+ parentForBlock.index = nameNode;
407
+ parentForBlock.init();
408
+ return;
409
+ }
410
+
411
+ let valueNode = this.dataStack.pop();
412
+
355
413
  if (valueNode instanceof AST.ASTChainStore) {
356
414
  this.append_to_chain_store(valueNode, nameNode);
357
415
  } else {
@@ -374,13 +432,41 @@ function handleStoreNameA() {
374
432
  processStore.call(this);
375
433
  }
376
434
 
377
- function processStore() {
435
+ function handleStoreAnnotationA() {
436
+ // Python 3.6 only: pops the annotation type and stores it under
437
+ // __annotations__[name]. The variable name is the opcode argument.
438
+ // Patterns:
439
+ // `x: int` → LOAD_NAME int ; STORE_ANNOTATION x
440
+ // `x: int = 1` → LOAD_CONST 1 ; STORE_NAME x ; LOAD_NAME int ; STORE_ANNOTATION x
441
+ const annType = this.dataStack.pop();
442
+ const varName = this.code.Current.Name || "";
443
+ const nameNode = new AST.ASTName(varName);
444
+
445
+ const last = this.curBlock.nodes.top();
446
+ if (last instanceof AST.ASTStore &&
447
+ last.dest instanceof AST.ASTName &&
448
+ last.dest.name === varName) {
449
+ // Combine prior `x = value` with annotation → `x: int = value`
450
+ const annotated = new AST.ASTAnnotatedVar(nameNode, annType);
451
+ annotated.line = this.code.Current.LineNo;
452
+ last.dest = annotated;
453
+ } else {
454
+ // Standalone annotation `x: int`
455
+ const annotated = new AST.ASTAnnotatedVar(nameNode, annType);
456
+ annotated.line = this.code.Current.LineNo;
457
+ this.curBlock.append(annotated);
458
+ }
459
+ }
460
+
461
+ function processStore(nameOverride) {
462
+ const currentName = nameOverride !== undefined ? nameOverride : this.code.Current.Name;
463
+
378
464
  // Pattern matching: first capture expected bindings, then start case body
379
465
  if (this.inMatchPattern) {
380
466
  if (shouldCaptureStoreInPattern(this.patternOps)) {
381
467
  this.patternOps.push({
382
468
  type: 'STORE_FAST',
383
- name: this.code.Current.Name
469
+ name: currentName
384
470
  });
385
471
  return;
386
472
  }
@@ -392,8 +478,8 @@ function processStore() {
392
478
  }
393
479
  }
394
480
 
395
- if (global.g_cliArgs?.debug && this.code.Current.Name && (this.code.Current.Name === 'b' || this.code.Current.Name === 'i')) {
396
- console.log(`[processStore] varName=${this.code.Current.Name}, curBlock=${this.curBlock.type_str}, inited=${this.curBlock.inited}, unpack=${this.unpack}`);
481
+ if (global.g_cliArgs?.debug && currentName && (currentName === 'b' || currentName === 'i')) {
482
+ console.log(`[processStore] varName=${currentName}, curBlock=${this.curBlock.type_str}, inited=${this.curBlock.inited}, unpack=${this.unpack}`);
397
483
  console.log(` Block stack: ${this.blocks.map((b,i) => `[${i}]${b.type_str}(inited=${b.inited})`).join(' → ')}`);
398
484
  }
399
485
 
@@ -409,7 +495,7 @@ function processStore() {
409
495
  }
410
496
 
411
497
  if (this.unpack) {
412
- let nameNode = new AST.ASTName(this.code.Current.Name);
498
+ let nameNode = new AST.ASTName(currentName);
413
499
 
414
500
  let tupleNode = this.dataStack.top();
415
501
  if (tupleNode instanceof AST.ASTTuple) {
@@ -444,9 +530,28 @@ function processStore() {
444
530
  }
445
531
  }
446
532
  } else {
447
- let varName = this.code.Current.Name || "";
533
+ let varName = currentName || "";
448
534
  if (varName.length >= 2 && varName.startsWith('_[')) {
449
- /* Don't show stores of list comp append objects. */
535
+ /* Synthetic `_[N]` is CPython bookkeeping; never in real source.
536
+ * Listcomp case: Py2.3 stores `list.append`, Py2.4-2.6 stores
537
+ * the list itself. Pop + cache so later LOAD can replay.
538
+ * Pre-PEP-343 `with` (2.5+) also uses `_[N]` to park __exit__ /
539
+ * __enter__() result. Those are consumed by downstream opcodes
540
+ * walking the stack backwards — we must NOT pop in that case.
541
+ * Heuristic: if stack top matches listcomp shape, swallow. Else
542
+ * leave stack untouched so the with-handler's backward scan still
543
+ * sees the real expressions. */
544
+ const top = this.dataStack[this.dataStack.length - 1];
545
+ const isListcompList = top instanceof AST.ASTList;
546
+ const isListcompAppend = top instanceof AST.ASTBinary
547
+ && top.op === AST.ASTBinary.BinOp.Attr
548
+ && top.right instanceof AST.ASTName
549
+ && top.right.name === "append";
550
+ if (isListcompList || isListcompAppend) {
551
+ this.dataStack.pop();
552
+ this._listCompAppendRefs = this._listCompAppendRefs || {};
553
+ this._listCompAppendRefs[varName] = top;
554
+ }
450
555
  return;
451
556
  }
452
557
 
@@ -477,6 +582,23 @@ function processStore() {
477
582
  parentForBlock.init();
478
583
  } else {
479
584
  let valueNode = this.dataStack.pop();
585
+ // DUP_TOP for chain assign leaves [V, V, ChainStore] on the
586
+ // stack, so the ChainStore is popped here as `valueNode` rather
587
+ // than being the second-from-top importNode checked below.
588
+ if (valueNode instanceof AST.ASTChainStore) {
589
+ this.dataStack.pop(); // consume the extra duplicate V
590
+ this.append_to_chain_store(valueNode, nameNode);
591
+ if (this.code.Prev?.OpCodeID != this.OpCodes.DUP_TOP &&
592
+ this.code.Prev?.OpCodeID != this.OpCodes.COPY_A) {
593
+ // Terminal store — commit chainstore to block and
594
+ // clear the copy append_to_chain_store just pushed.
595
+ if (this.dataStack.top() === valueNode) {
596
+ this.dataStack.pop();
597
+ }
598
+ this.curBlock.append(valueNode);
599
+ }
600
+ return;
601
+ }
480
602
  let importNode = this.dataStack.top();
481
603
  if (importNode instanceof AST.ASTImport) {
482
604
  let storeNode = new AST.ASTStore(valueNode, nameNode);
@@ -505,6 +627,7 @@ function processStore() {
505
627
  if (valueNode instanceof AST.ASTFunction && !valueNode.code.object.SourceCode) {
506
628
  let decompiler = new PycDecompiler(valueNode.code.object);
507
629
  valueNode.code.object.SourceCode = decompiler.decompile();
630
+ if (decompiler.errors.length) this.errors.push(...decompiler.errors);
508
631
  }
509
632
  let lastBlockNode = this.curBlock.nodes.top();
510
633
  if (
@@ -564,9 +687,25 @@ function handleReserveFastA() {
564
687
  }
565
688
 
566
689
  function handleLoadFastAndClearA() {
567
- // Python 3.11+ LOAD_FAST_AND_CLEAR - loads local and clears it
568
- // Used in comprehensions to save/restore outer scope variables
569
- // For decompilation: just load the variable (clearing is implementation detail)
690
+ // Python 3.12+ inline comprehension save pattern:
691
+ // LOAD_FAST_AND_CLEAR var ; SWAP 2 ; BUILD_<LIST|SET|MAP> 0 ; SWAP 2 ; (GET_ITER)? ; FOR_ITER
692
+ // CPython inlines comprehensions into the parent scope, saving the loop var
693
+ // before and restoring after. Skip the save here so the comprehension stack
694
+ // matches the simpler BUILD/SWAP/GET_ITER/FOR_ITER shape; END_FOR cleans up.
695
+ const next1 = this.code.Next;
696
+ const next2 = next1?.Next;
697
+ const next3 = next2?.Next;
698
+ const isCompStart = next1?.OpCodeID == this.OpCodes.SWAP_A && next1.Argument == 2 &&
699
+ (next2?.OpCodeID == this.OpCodes.BUILD_LIST_A ||
700
+ next2?.OpCodeID == this.OpCodes.BUILD_SET_A ||
701
+ next2?.OpCodeID == this.OpCodes.BUILD_MAP_A) &&
702
+ next2.Argument == 0 &&
703
+ next3?.OpCodeID == this.OpCodes.SWAP_A && next3.Argument == 2;
704
+ if (isCompStart) {
705
+ this._inlineCompSavedVar = this.code.Current.Name;
706
+ this.code.GoNext(); // consume the SWAP 2 right after this op
707
+ return;
708
+ }
570
709
  handleLoadFastA.call(this);
571
710
  }
572
711
 
@@ -575,6 +714,11 @@ function handleLoadFastBorrowA() {
575
714
  handleLoadFastA.call(this);
576
715
  }
577
716
 
717
+ function handleLoadFastBorrowLoadFastBorrowA() {
718
+ // Python 3.15+ packed borrow load (same encoding as LOAD_FAST_LOAD_FAST).
719
+ handleLoadFastLoadFastA.call(this);
720
+ }
721
+
578
722
  function handleLoadCommonConstantA() {
579
723
  // Python 3.14+ common constants table
580
724
  const {PythonObject} = require('../PythonObject');
@@ -597,70 +741,57 @@ function handleLoadCommonConstantA() {
597
741
  }
598
742
 
599
743
  function handleLoadFastLoadFastA() {
600
- // Combined load of two fast locals: arg packs (hi<<8)|lo
744
+ // CPython pushes local[hi] then local[lo]. For 3.13 the two halves are packed
745
+ // nibble-wise in the argument (hi in upper nibble, lo in lower nibble);
746
+ // older bytecode used bytes.
601
747
  const packed = this.code.Current.Argument;
602
748
  const isPackedNibbles = this.object?.Reader?.versionCompare(3, 13) >= 0;
603
- const idx1 = isPackedNibbles ? ((packed) & 0x0F) : (packed & 0xFF);
604
- const idx2 = isPackedNibbles ? ((packed >> 4) & 0x0F) : ((packed >> 8) & 0xFF);
749
+ const hiIdx = isPackedNibbles ? ((packed >> 4) & 0x0F) : ((packed >> 8) & 0xFF);
750
+ const loIdx = isPackedNibbles ? (packed & 0x0F) : (packed & 0xFF);
605
751
  const varNames = this.object?.VarNames?.Value || this.object?.code?.object?.VarNames?.Value || [];
606
- const name1 = varNames[idx1]?.toString?.() || `##var_${idx1}##`;
607
- const name2 = varNames[idx2]?.toString?.() || `##var_${idx2}##`;
608
- // Preserve operand order expected by STORE_ATTR sequences: lower nibble first
609
- this.dataStack.push(new AST.ASTName(name1));
610
- this.dataStack.push(new AST.ASTName(name2));
752
+ const hiName = varNames[hiIdx]?.toString?.() || `##var_${hiIdx}##`;
753
+ const loName = varNames[loIdx]?.toString?.() || `##var_${loIdx}##`;
754
+ this.dataStack.push(new AST.ASTName(hiName));
755
+ this.dataStack.push(new AST.ASTName(loName));
611
756
  }
612
757
 
613
758
  function handleStoreFastLoadFastA() {
614
- // Combined store then load: store to first index, load second
759
+ // CPython: store TOS into local[hi], then push local[lo].
615
760
  const packed = this.code.Current.Argument;
616
761
  const isPackedNibbles = this.object?.Reader?.versionCompare(3, 13) >= 0;
617
- const idx1 = isPackedNibbles ? (packed & 0x0F) : (packed & 0xFF);
618
- const idx2 = isPackedNibbles ? ((packed >> 4) & 0x0F) : ((packed >> 8) & 0xFF);
762
+ const storeIdx = isPackedNibbles ? ((packed >> 4) & 0x0F) : ((packed >> 8) & 0xFF);
763
+ const loadIdx = isPackedNibbles ? (packed & 0x0F) : (packed & 0xFF);
619
764
  const varNames = this.object?.VarNames?.Value || this.object?.code?.object?.VarNames?.Value || [];
620
- const name1 = varNames[idx1]?.toString?.() || `##var_${idx1}##`;
621
- const name2 = varNames[idx2]?.toString?.() || `##var_${idx2}##`;
765
+ const storeName = varNames[storeIdx]?.toString?.() || `##var_${storeIdx}##`;
766
+ const loadName = varNames[loadIdx]?.toString?.() || `##var_${loadIdx}##`;
622
767
 
623
768
  if (this.inMatchPattern && shouldCaptureStoreInPattern(this.patternOps)) {
624
- // Capture binding and surface loaded local for guards
625
769
  this.dataStack.pop();
626
- this.patternOps.push({type: 'STORE_FAST', name: name1});
627
- this.dataStack.push(new AST.ASTName(name2));
770
+ this.patternOps.push({type: 'STORE_FAST', name: storeName});
771
+ this.dataStack.push(new AST.ASTName(loadName));
628
772
  return;
629
773
  }
630
774
 
631
- const valueNode = this.dataStack.pop() || new AST.ASTNone();
632
- const destNode = new AST.ASTName(name1);
633
- this.curBlock.append(new AST.ASTStore(valueNode, destNode));
634
-
635
- // Push second local load as result
636
- this.dataStack.push(new AST.ASTName(name2));
775
+ // Route STORE through processStore so for-block target / unpack / chain-store work.
776
+ processStore.call(this, storeName);
777
+ this.dataStack.push(new AST.ASTName(loadName));
637
778
  }
638
779
 
639
780
  function handleStoreFastStoreFastA() {
640
- // Combined store to two locals: arg packs (hi<<8)|lo
781
+ // CPython: stack is [value2, value1] with value1 on TOS.
782
+ // SETLOCAL(hi, value1); SETLOCAL(lo, value2).
641
783
  const packed = this.code.Current.Argument;
642
784
  const isPackedNibbles = this.object?.Reader?.versionCompare(3, 13) >= 0;
643
- const idx1 = isPackedNibbles ? (packed & 0x0F) : (packed & 0xFF);
644
- const idx2 = isPackedNibbles ? ((packed >> 4) & 0x0F) : ((packed >> 8) & 0xFF);
785
+ const hiIdx = isPackedNibbles ? ((packed >> 4) & 0x0F) : ((packed >> 8) & 0xFF);
786
+ const loIdx = isPackedNibbles ? (packed & 0x0F) : (packed & 0xFF);
645
787
  const varNames = this.object?.VarNames?.Value || this.object?.code?.object?.VarNames?.Value || [];
646
- const name1 = varNames[idx1]?.toString?.() || `##var_${idx1}##`;
647
- const name2 = varNames[idx2]?.toString?.() || `##var_${idx2}##`;
648
-
649
- if (this.inMatchPattern && shouldCaptureStoreInPattern(this.patternOps)) {
650
- // Record bindings in pattern order (high nibble corresponds to first element)
651
- this.dataStack.pop(); // second element
652
- this.dataStack.pop(); // first element
653
- this.patternOps.push({type: 'STORE_FAST', name: name2});
654
- this.patternOps.push({type: 'STORE_FAST', name: name1});
655
- return;
656
- }
657
-
658
- const value2 = this.dataStack.pop() || new AST.ASTNone();
659
- const value1 = this.dataStack.pop() || new AST.ASTNone();
788
+ const hiName = varNames[hiIdx]?.toString?.() || `##var_${hiIdx}##`;
789
+ const loName = varNames[loIdx]?.toString?.() || `##var_${loIdx}##`;
660
790
 
661
- // First store lower nibble value (top of stack), then higher nibble
662
- this.curBlock.append(new AST.ASTStore(value2, new AST.ASTName(name1)));
663
- this.curBlock.append(new AST.ASTStore(value1, new AST.ASTName(name2)));
791
+ // Delegate to processStore so unpack / for-target / chain-store paths work.
792
+ // First store consumes TOS (high nibble target), second store consumes new TOS.
793
+ processStore.call(this, hiName);
794
+ processStore.call(this, loName);
664
795
  }
665
796
 
666
797
  function handleLoadSmallIntA() {
@@ -686,6 +817,7 @@ module.exports = {
686
817
  handleLoadFastCheckA,
687
818
  handleLoadFastAndClearA,
688
819
  handleLoadFastBorrowA,
820
+ handleLoadFastBorrowLoadFastBorrowA,
689
821
  handleLoadFastLoadFastA,
690
822
  handleLoadGlobalA,
691
823
  handleLoadCommonConstantA,
@@ -707,5 +839,6 @@ module.exports = {
707
839
  handleStoreFastStoreFastA,
708
840
  handleStoreGlobalA,
709
841
  handleStoreNameA,
842
+ handleStoreAnnotationA,
710
843
  handleReserveFastA
711
844
  };
@@ -55,6 +55,7 @@ function handleInstrumentedForIterA() {
55
55
  let end = 0;
56
56
  let line = this.code.Current.LineNo;
57
57
  let comprehension = false;
58
+ let pre38ListCompDetected = false;
58
59
  const specialized = this.object.Reader.versionCompare(3, 13) >= 0;
59
60
  const sentinel = specialized ? new AST.ASTNone() : null;
60
61
 
@@ -79,6 +80,23 @@ function handleInstrumentedForIterA() {
79
80
  comprehension = true;
80
81
  }
81
82
  }
83
+ // 3.12+ inline comprehension: extend block end past END_FOR so the
84
+ // for-block isn't auto-popped before handleEndFor runs the cleanup.
85
+ // Without this, the auto-pop at PycDecompiler.js:614 closes the
86
+ // for-block at its natural end (just past JUMP_BACKWARD), and END_FOR
87
+ // can't reach it to call addGenerator.
88
+ if (this._inlineCompSavedVar != null && comprehension) {
89
+ let scanOff = end;
90
+ for (let i = 0; i < 10; i++) {
91
+ const ins = this.code.PeekInstructionAtOffset(scanOff);
92
+ if (!ins) break;
93
+ if (ins.OpCodeID == this.OpCodes.END_FOR) {
94
+ end = ins.Offset + (ins.Size || 2);
95
+ break;
96
+ }
97
+ scanOff += (ins.Size || 2);
98
+ }
99
+ }
82
100
  } else {
83
101
  if ((this.dataStack.top() instanceof AST.ASTSet ||
84
102
  this.dataStack.top() instanceof AST.ASTList ||
@@ -86,6 +104,72 @@ function handleInstrumentedForIterA() {
86
104
  && this.dataStack.top().values.length == 0) {
87
105
  end = this.code.Current.JumpTarget;
88
106
  comprehension = true;
107
+ // 2.7 inline listcomp (no `_[N]` cache, empty ASTList on stack):
108
+ // LIST_APPEND arg=N reads the accumulator via peek-offset, so the
109
+ // sentinel push would leave the real accumulator trapped on the
110
+ // stack for the trailing COMPARE_OP / etc. to swallow as a
111
+ // phantom operand. Skip the sentinel and let LIST_APPEND pop the
112
+ // accumulator directly.
113
+ // Dict/set comps (ASTMap/ASTSet): MAP_ADD/SET_ADD pop one entry
114
+ // assuming the sentinel is present — keep the sentinel there.
115
+ // 2.3-2.6 with `_[N]` cache: handled by the else-if below (top
116
+ // of stack is the enclosing expression, not an empty list).
117
+ // Exception: if the body has UNPACK_SEQUENCE (loop var is a
118
+ // tuple `for k, v in …`), the STORE chain's final seqNode pop
119
+ // would consume the accumulator list instead of a sentinel,
120
+ // stranding the outer callable (e.g. `dict`) on the stack for
121
+ // LIST_APPEND to eat as the "list". Keep the sentinel so STORE
122
+ // consumes it and LIST_APPEND still finds the real accumulator.
123
+ const hasListcompCache = this._listCompAppendRefs
124
+ && Object.keys(this._listCompAppendRefs).length > 0;
125
+ const isList = this.dataStack.top() instanceof AST.ASTList;
126
+ if (!hasListcompCache && isList) {
127
+ let hasUnpackInBody = false;
128
+ const bodyEnd = this.code.Current.JumpTarget;
129
+ let scanOff = this.code.Next?.Offset;
130
+ for (let i = 0; i < 20 && scanOff != null && scanOff < bodyEnd; i++) {
131
+ const ins = this.code.PeekInstructionAtOffset(scanOff);
132
+ if (!ins) break;
133
+ if (ins.OpCodeID === this.OpCodes.UNPACK_SEQUENCE_A) {
134
+ hasUnpackInBody = true;
135
+ break;
136
+ }
137
+ scanOff = ins.Offset + (ins.Size || 3);
138
+ }
139
+ if (!hasUnpackInBody) {
140
+ pre38ListCompDetected = true;
141
+ }
142
+ }
143
+ } else if (this._listCompAppendRefs && Object.keys(this._listCompAppendRefs).length > 0) {
144
+ // Py2.4-2.6 inline listcomp: accumulator stashed in `_[N]` via
145
+ // DUP_TOP/STORE_NAME, so the empty-list heuristic misses it
146
+ // (stack top is the enclosing expression — `dict` for
147
+ // `dict([…])`, or the COMPARE_OP left-list for `assert X == […]`).
148
+ // Body always does `LOAD _[N]` before LIST_APPEND, so the
149
+ // accumulator reaches LIST_APPEND via the cache and the sentinel
150
+ // survives. When the loop var is stored via plain STORE_NAME
151
+ // (parentForBlock path, no pop), nothing consumes the sentinel
152
+ // and it leaks into the trailing COMPARE_OP / CALL_FUNCTION.
153
+ // Look ahead for UNPACK_SEQUENCE in the body: if present, the
154
+ // STORE chain consumes the sentinel as its seqNode pop — keep
155
+ // it. Otherwise skip the sentinel push.
156
+ end = this.code.Current.JumpTarget;
157
+ comprehension = true;
158
+ let hasUnpackInBody = false;
159
+ const bodyEnd = this.code.Current.JumpTarget;
160
+ let scanOff = this.code.Next?.Offset;
161
+ for (let i = 0; i < 20 && scanOff != null && scanOff < bodyEnd; i++) {
162
+ const ins = this.code.PeekInstructionAtOffset(scanOff);
163
+ if (!ins) break;
164
+ if (ins.OpCodeID === this.OpCodes.UNPACK_SEQUENCE_A) {
165
+ hasUnpackInBody = true;
166
+ break;
167
+ }
168
+ scanOff = ins.Offset + (ins.Size || 3);
169
+ }
170
+ if (!hasUnpackInBody) {
171
+ pre38ListCompDetected = true;
172
+ }
89
173
  } else {
90
174
  let top = this.blocks.top();
91
175
  start = top.start;
@@ -94,6 +178,12 @@ function handleInstrumentedForIterA() {
94
178
 
95
179
  if (top.blockType == AST.ASTBlock.BlockType.While) {
96
180
  this.blocks.pop();
181
+ // Py2.4-2.6 genexpr inner code has SETUP_LOOP → While, but
182
+ // it IS a comprehension. Detect by code object name.
183
+ const objName = this.object?.Name;
184
+ if (objName === "<generator expression>" || objName === "<genexpr>") {
185
+ comprehension = true;
186
+ }
97
187
  } else {
98
188
  comprehension = true;
99
189
  }
@@ -104,15 +194,18 @@ function handleInstrumentedForIterA() {
104
194
  forblk.line = line;
105
195
  forblk.comprehension = comprehension;
106
196
 
107
- if (global.g_cliArgs?.debug) {
108
- console.log(`[FOR_ITER] Created for block: start=${start}, end=${end}, comprehension=${comprehension}`);
109
- }
110
-
111
197
  this.blocks.push(forblk);
112
198
  this.curBlock = this.blocks.top();
113
199
 
114
- // 3.13+ FOR_ITER pushes a sentinel used by END_FOR; keep stack balanced.
115
- this.dataStack.push(sentinel);
200
+ // Py2.6 inline listcomp (detected via _[N] cache) has no POP_TOP at the
201
+ // for-block end to consume the sentinel — the enclosing COMPARE_OP/etc
202
+ // would then swallow it as a fake operand. Skip the placeholder push in
203
+ // that case; nothing downstream needs it since the comprehension closes
204
+ // at LIST_APPEND.
205
+ if (!pre38ListCompDetected) {
206
+ // 3.13+ FOR_ITER pushes a sentinel used by END_FOR; keep stack balanced.
207
+ this.dataStack.push(sentinel);
208
+ }
116
209
  }
117
210
 
118
211
  function handleForLoopA() {
@@ -143,6 +236,29 @@ function handleGetAiter() {
143
236
  // Logic similar to FOR_ITER_A
144
237
  let iter = this.dataStack.pop(); // Iterable
145
238
 
239
+ // Async comprehension parameter setup: GET_AITER converts the iterable to an
240
+ // async iterator before passing it to the genexpr/listcomp function call.
241
+ // Don't create an AsyncFor block — just forward the iterable.
242
+ // 3.5-3.6: GET_AITER → LOAD_CONST None → YIELD_FROM → CALL_FUNCTION
243
+ // 3.7: GET_AITER → CALL_FUNCTION (no YIELD_FROM needed)
244
+ const n1 = this.code.Next;
245
+ if (n1) {
246
+ const n2 = n1.Next;
247
+ const n3 = n2?.Next;
248
+ if (n2 && n3 &&
249
+ n1.OpCodeID == this.OpCodes.LOAD_CONST_A &&
250
+ n2.OpCodeID == this.OpCodes.YIELD_FROM &&
251
+ n3.OpCodeID == this.OpCodes.CALL_FUNCTION_A) {
252
+ this.dataStack.push(new AST.ASTIteratorValue(iter));
253
+ this.code.GoNext(2);
254
+ return;
255
+ }
256
+ if (n1.OpCodeID == this.OpCodes.CALL_FUNCTION_A) {
257
+ this.dataStack.push(new AST.ASTIteratorValue(iter));
258
+ return;
259
+ }
260
+ }
261
+
146
262
  let start = this.code.Current.Offset;
147
263
  let end = 0;
148
264
  let line = this.code.Current.LineNo;
@@ -284,6 +400,46 @@ function handleEndFor() {
284
400
  if (global.g_cliArgs?.debug) {
285
401
  console.log(`[END_FOR] at offset ${this.code.Current.Offset}`);
286
402
  }
403
+
404
+ // Inline comprehension cleanup (3.12+): closing pattern after END_FOR is
405
+ // POP_TOP ; SWAP 2 ; STORE_FAST <savedVar>
406
+ // After LIST_APPEND in comprehension mode, the stack has [..., emptyList, ASTComp].
407
+ // We rescue ASTComp, drop the empty accumulator, and skip the cleanup ops.
408
+ if (this._inlineCompSavedVar != null) {
409
+ const top = this.dataStack.top();
410
+ if (top instanceof AST.ASTComprehension) {
411
+ this.dataStack.pop();
412
+ const below = this.dataStack.top();
413
+ if ((below instanceof AST.ASTList && (below.values?.length || 0) === 0) ||
414
+ (below instanceof AST.ASTSet && (below.values?.length || 0) === 0) ||
415
+ (below instanceof AST.ASTMap && (below.values?.length || 0) === 0)) {
416
+ this.dataStack.pop();
417
+ }
418
+ // Pop the inline-comp for-block and attach its generator to the
419
+ // comprehension so it renders as `[expr for x in iter]`. Without
420
+ // this, JUMP_BACKWARD in 3.11+ is a no-op and addGenerator never
421
+ // fires (control_flow_jumps.js only handles JUMP_ABSOLUTE).
422
+ const forBlk = this.curBlock;
423
+ if (forBlk && forBlk.blockType == AST.ASTBlock.BlockType.For && forBlk.comprehension) {
424
+ top.addGenerator(forBlk);
425
+ this.blocks.pop();
426
+ this.curBlock = this.blocks.top();
427
+ }
428
+ this.dataStack.push(top);
429
+ }
430
+ const saved = this._inlineCompSavedVar;
431
+ const cand0 = this.code.Next;
432
+ const cand1 = cand0?.Next;
433
+ const cand2 = cand1?.Next;
434
+ if (cand0?.OpCodeID == this.OpCodes.POP_TOP &&
435
+ cand1?.OpCodeID == this.OpCodes.SWAP_A && cand1.Argument == 2 &&
436
+ cand2?.OpCodeID == this.OpCodes.STORE_FAST_A && cand2.Name == saved) {
437
+ this.code.GoNext(3);
438
+ }
439
+ this._inlineCompSavedVar = null;
440
+ return;
441
+ }
442
+
287
443
  if (this.object.Reader.versionCompare(3, 13) >= 0 && this.dataStack.length > 0) {
288
444
  this.dataStack.pop();
289
445
  }