depyo 1.0.2 → 1.0.3
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/depyo.js +43 -2
- package/lib/OpCodes.js +22 -5
- package/lib/PycDecompiler.js +402 -39
- package/lib/PycDisassembler.js +1 -1
- package/lib/PycReader.js +22 -3
- package/lib/PythonObject.js +40 -6
- package/lib/ast/ast_node.js +292 -71
- package/lib/bytecode/python_3_0.js +1 -1
- package/lib/bytecode/python_3_12.js +1 -1
- package/lib/bytecode/python_3_13.js +13 -13
- package/lib/bytecode/python_3_14.js +13 -13
- package/lib/bytecode/python_3_15.js +183 -0
- package/lib/code_reader.js +107 -146
- package/lib/handlers/collections_update.js +50 -1
- package/lib/handlers/context_managers.js +202 -13
- package/lib/handlers/control_flow_jumps.js +516 -23
- package/lib/handlers/exceptions_blocks.js +85 -22
- package/lib/handlers/formatting.js +60 -17
- package/lib/handlers/function_calls.js +454 -57
- package/lib/handlers/function_class_build.js +159 -64
- package/lib/handlers/generators_async.js +67 -0
- package/lib/handlers/load_store_names.js +190 -57
- package/lib/handlers/loop_iterator.js +162 -6
- package/lib/handlers/misc_other.js +216 -43
- package/lib/handlers/stack_ops.js +81 -19
- package/lib/handlers/subscript_slice.js +103 -1
- package/lib/handlers/unpack.js +18 -16
- package/package.json +1 -1
|
@@ -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+:
|
|
161
|
-
|
|
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
|
|
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:
|
|
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 &&
|
|
396
|
-
console.log(`[processStore] varName=${
|
|
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(
|
|
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 =
|
|
533
|
+
let varName = currentName || "";
|
|
448
534
|
if (varName.length >= 2 && varName.startsWith('_[')) {
|
|
449
|
-
/*
|
|
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.
|
|
568
|
-
//
|
|
569
|
-
//
|
|
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
|
-
//
|
|
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
|
|
604
|
-
const
|
|
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
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
this.dataStack.push(new AST.ASTName(
|
|
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
|
-
//
|
|
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
|
|
618
|
-
const
|
|
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
|
|
621
|
-
const
|
|
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:
|
|
627
|
-
this.dataStack.push(new AST.ASTName(
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
this.
|
|
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
|
-
//
|
|
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
|
|
644
|
-
const
|
|
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
|
|
647
|
-
const
|
|
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
|
-
//
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
//
|
|
115
|
-
|
|
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
|
}
|