depyo 1.0.3 → 1.2.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.
- package/README.md +160 -83
- package/lib/PycDecompiler.js +761 -1
- package/lib/PycReader.js +10 -1
- package/lib/PythonObject.js +10 -2
- package/lib/ast/ast_node.js +130 -21
- package/lib/bytecode/python_3_14.js +1 -1
- package/lib/handlers/binary_ops.js +9 -0
- package/lib/handlers/comparisons.js +3 -10
- package/lib/handlers/exceptions_blocks.js +7 -2
- package/lib/handlers/function_calls.js +24 -5
- package/lib/handlers/function_class_build.js +11 -1
- package/lib/handlers/load_store_names.js +46 -1
- package/lib/handlers/misc_other.js +76 -4
- package/lib/handlers/stack_ops.js +4 -2
- package/lib/handlers/unary_ops.js +7 -0
- package/package.json +2 -2
|
@@ -241,11 +241,25 @@ function reconstructPattern(patternOps) {
|
|
|
241
241
|
));
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
// Literal OR pattern: multiple COMPARE_OP checks in a row
|
|
244
|
+
// Literal OR pattern: multiple COMPARE_OP checks in a row.
|
|
245
|
+
// Pattern compares share the same `left` (the matched subject). Anything
|
|
246
|
+
// with a different left is a guard expression that bled into patternOps.
|
|
245
247
|
const allCompareOps = ops.filter(op => op.type === 'COMPARE');
|
|
246
|
-
|
|
248
|
+
const sameLeft = (a, b) => {
|
|
249
|
+
if (a === b) return true;
|
|
250
|
+
if (!a || !b) return false;
|
|
251
|
+
const af = a.codeFragment?.()?.toString?.();
|
|
252
|
+
const bf = b.codeFragment?.()?.toString?.();
|
|
253
|
+
return !!af && af === bf;
|
|
254
|
+
};
|
|
255
|
+
let patternCompareOps = allCompareOps;
|
|
256
|
+
if (allCompareOps.length > 1) {
|
|
257
|
+
const subjectLeft = allCompareOps[0].left;
|
|
258
|
+
patternCompareOps = allCompareOps.filter(op => sameLeft(op.left, subjectLeft));
|
|
259
|
+
}
|
|
260
|
+
if (!hasMatchSeq && patternCompareOps.length > 1) {
|
|
247
261
|
const orPatterns = [];
|
|
248
|
-
for (const cmp of
|
|
262
|
+
for (const cmp of patternCompareOps) {
|
|
249
263
|
markConsumed(cmp);
|
|
250
264
|
if (cmp.right instanceof AST.ASTObject) {
|
|
251
265
|
orPatterns.push(new AST.ASTPattern(AST.ASTPattern.PatternType.Literal, cmp.right));
|
|
@@ -366,6 +380,21 @@ function hasUpcomingMatchCase() {
|
|
|
366
380
|
return true;
|
|
367
381
|
}
|
|
368
382
|
|
|
383
|
+
// `case _ as name:` final clause — CPython emits a bare STORE for the
|
|
384
|
+
// binding right after the previous case's RETURN. No LOAD precedes it
|
|
385
|
+
// (the stored value is the matched subject preserved on the stack).
|
|
386
|
+
if (this.currentMatch && this.matchSubject &&
|
|
387
|
+
(nextOpCodeId == this.OpCodes.STORE_FAST_A ||
|
|
388
|
+
nextOpCodeId == this.OpCodes.STORE_NAME_A)) {
|
|
389
|
+
const prevId = this.code.Current.OpCodeID;
|
|
390
|
+
const isAfterReturn = prevId == this.OpCodes.RETURN_VALUE ||
|
|
391
|
+
prevId == this.OpCodes.RETURN_VALUE_A ||
|
|
392
|
+
prevId == this.OpCodes.RETURN_CONST_A;
|
|
393
|
+
if (isAfterReturn) {
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
369
398
|
// NOP followed by code (no COMPARE_OP) = default case (case _:)
|
|
370
399
|
// This pattern appears in 3.10/3.11 after the last literal case
|
|
371
400
|
if (nextOpCodeId == this.OpCodes.NOP && this.currentMatch) {
|
|
@@ -582,7 +611,10 @@ function hasUpcomingStringBuild(context) {
|
|
|
582
611
|
context.OpCodes.STORE_DEREF_A
|
|
583
612
|
]);
|
|
584
613
|
|
|
585
|
-
|
|
614
|
+
// 3.11+ inline caches inflate the physical instruction stream; a single
|
|
615
|
+
// f-string with a few interpolations easily spans 30+ code slots before
|
|
616
|
+
// reaching BUILD_STRING. Cap generously and rely on earlyStops to bound.
|
|
617
|
+
for (let step = 1; step <= 48; step++) {
|
|
586
618
|
const instr = context.code.PeekInstructionAtOffset(context.code.Current.Offset + step * 2);
|
|
587
619
|
if (!instr) {
|
|
588
620
|
break;
|
|
@@ -745,6 +777,28 @@ function handlePopTop() {
|
|
|
745
777
|
return;
|
|
746
778
|
}
|
|
747
779
|
|
|
780
|
+
// Mid-UNPACK_SEQUENCE POP_TOP: CPython emits `a, _, b = tup` as
|
|
781
|
+
// UNPACK 3 / STORE a / POP_TOP / STORE b, where POP_TOP drops the
|
|
782
|
+
// middle slot. Our unpack-state-machine accumulates STORE targets
|
|
783
|
+
// into a tuple; treat POP_TOP as an anonymous `_` slot so the
|
|
784
|
+
// remaining STORE_FAST still sees the tuple on the stack.
|
|
785
|
+
if (this.unpack > 0) {
|
|
786
|
+
let tupleNode = this.dataStack.top();
|
|
787
|
+
if (tupleNode instanceof AST.ASTTuple) {
|
|
788
|
+
let placeholder = new AST.ASTName("_");
|
|
789
|
+
if (this.starPos-- == 0) placeholder.name = "*_";
|
|
790
|
+
tupleNode.add(placeholder);
|
|
791
|
+
if (--this.unpack <= 0) {
|
|
792
|
+
this.dataStack.pop();
|
|
793
|
+
let seqNode = this.dataStack.pop();
|
|
794
|
+
let node = new AST.ASTStore(seqNode, tupleNode);
|
|
795
|
+
node.line = this.code.Current.LineNo;
|
|
796
|
+
this.curBlock.append(node);
|
|
797
|
+
}
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
748
802
|
if ([this.OpCodes.JUMP_ABSOLUTE_A, this.OpCodes.JUMP_FORWARD_A, this.OpCodes.POP_JUMP_IF_FALSE_A, this.OpCodes.JUMP_IF_FALSE_A].includes(this.code.Prev?.OpCodeID)) {
|
|
749
803
|
if (this.curBlock.blockType == AST.ASTBlock.BlockType.Except) {
|
|
750
804
|
// Skipping POP_TOP, POP_TOP, POP_TOP
|
|
@@ -1111,7 +1165,15 @@ function handleReturnConstA() {
|
|
|
1111
1165
|
nextOp.OpCodeID === this.OpCodes.RERAISE;
|
|
1112
1166
|
const prevIsPOPExcept = this.code.Prev?.OpCodeID === this.OpCodes.POP_EXCEPT;
|
|
1113
1167
|
const nextIsSWAP = nextOp?.OpCodeID === this.OpCodes.SWAP_A;
|
|
1168
|
+
const nextIsRERAISE = nextOp?.OpCodeID === this.OpCodes.RERAISE ||
|
|
1169
|
+
nextOp?.OpCodeID === this.OpCodes.RERAISE_A;
|
|
1114
1170
|
const isExceptCleanup = isNone && prevIsPOPExcept && nextIsSWAP;
|
|
1171
|
+
// CPython 3.11+ places the function's implicit `return None` at the tail of an
|
|
1172
|
+
// except handler (POP_EXCEPT → RETURN_CONST None → RERAISE or end-of-function).
|
|
1173
|
+
// The source has no explicit return there; drop it so the handler body doesn't
|
|
1174
|
+
// inherit a phantom return.
|
|
1175
|
+
const isExceptTailImplicitReturn = isNone && prevIsPOPExcept &&
|
|
1176
|
+
(nextIsRERAISE || !nextOp);
|
|
1115
1177
|
|
|
1116
1178
|
if (isNone && isModuleLevel && isAtEnd) {
|
|
1117
1179
|
return; // Skip implicit return None at module end
|
|
@@ -1119,6 +1181,9 @@ function handleReturnConstA() {
|
|
|
1119
1181
|
if (isExceptCleanup) {
|
|
1120
1182
|
return; // Skip return None in exception cleanup (except* success path)
|
|
1121
1183
|
}
|
|
1184
|
+
if (isExceptTailImplicitReturn) {
|
|
1185
|
+
return; // Skip synthetic return None at end of except handler
|
|
1186
|
+
}
|
|
1122
1187
|
|
|
1123
1188
|
let value = new AST.ASTObject(this.code.Current.ConstantObject);
|
|
1124
1189
|
let node = new AST.ASTReturn(value);
|
|
@@ -1294,6 +1359,12 @@ function handleInstrumentedLineA() {
|
|
|
1294
1359
|
}
|
|
1295
1360
|
}
|
|
1296
1361
|
|
|
1362
|
+
// CPython 3.13+: POP_ITER replaces the post-loop POP_TOP that discards an
|
|
1363
|
+
// exhausted iterator. Semantically identical to POP_TOP for our purposes.
|
|
1364
|
+
function handlePopIter() {
|
|
1365
|
+
return handlePopTop.call(this);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1297
1368
|
module.exports = {
|
|
1298
1369
|
beginMatchCaseFromPattern,
|
|
1299
1370
|
flushCurrentCaseBody,
|
|
@@ -1302,6 +1373,7 @@ module.exports = {
|
|
|
1302
1373
|
handleFormatSimple,
|
|
1303
1374
|
handleFormatWithSpec,
|
|
1304
1375
|
handlePopTop,
|
|
1376
|
+
handlePopIter,
|
|
1305
1377
|
handlePrintExpr,
|
|
1306
1378
|
handlePrintItem,
|
|
1307
1379
|
handlePrintItemTo,
|
|
@@ -28,7 +28,8 @@ function handleDupTop() {
|
|
|
28
28
|
|
|
29
29
|
// Look ahead to confirm match pattern immediately
|
|
30
30
|
if (!this.currentMatch && this.lookAheadForMatchPattern()) {
|
|
31
|
-
this.
|
|
31
|
+
this.matchSubject = this.potentialMatchSubject;
|
|
32
|
+
this.currentMatch = new AST.ASTMatch(this.matchSubject);
|
|
32
33
|
this.currentMatch.line = this.code.Current.LineNo;
|
|
33
34
|
this.matchParentBlock = this.curBlock;
|
|
34
35
|
this.inMatchPattern = true; // Start tracking pattern operations
|
|
@@ -46,7 +47,8 @@ function handleDupTop() {
|
|
|
46
47
|
if (!this.currentMatch && this.potentialMatchSubject &&
|
|
47
48
|
this.object.Reader.versionCompare(3, 10) >= 0) {
|
|
48
49
|
// First time - create match (fallback if look-ahead didn't work)
|
|
49
|
-
this.
|
|
50
|
+
this.matchSubject = this.potentialMatchSubject;
|
|
51
|
+
this.currentMatch = new AST.ASTMatch(this.matchSubject);
|
|
50
52
|
this.currentMatch.line = this.code.Current.LineNo;
|
|
51
53
|
this.matchParentBlock = this.curBlock;
|
|
52
54
|
this.inMatchPattern = true;
|
|
@@ -69,6 +69,13 @@ function handleUnaryPositive() {
|
|
|
69
69
|
|
|
70
70
|
function handleToBool() {
|
|
71
71
|
// Python 3.13+ TO_BOOL: normalize truthiness. Preserve stack value.
|
|
72
|
+
// The preceding LOAD+COPY 1 here is the boolean-shortcut idiom for
|
|
73
|
+
// `if not X` / `while X`, not a match-case subject. Clear the
|
|
74
|
+
// potential-match candidate so a later COMPARE_OP (e.g. `e.code == 409`
|
|
75
|
+
// inside an except handler) doesn't get retroactively wrapped in a
|
|
76
|
+
// synthetic `match X:` rooted on this stale subject.
|
|
77
|
+
this.potentialMatchSubject = null;
|
|
78
|
+
this.matchCandidateStart = -1;
|
|
72
79
|
const arg = this.dataStack.pop();
|
|
73
80
|
this.dataStack.push(arg);
|
|
74
81
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "depyo",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Python bytecode decompiler (Python 1.0–3.
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Python bytecode decompiler (Python 1.0–3.15) implemented in Node.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"depyo": "./depyo.js"
|
|
7
7
|
},
|