depyo 1.0.1 → 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/README.md +4 -0
- package/depyo.js +103 -2
- package/lib/OpCodes.js +22 -5
- package/lib/PycDecompiler.js +402 -39
- package/lib/PycDisassembler.js +1 -1
- package/lib/PycReader.js +51 -4
- 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
package/lib/PycDecompiler.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const AST = require('./ast/ast_node');
|
|
4
|
+
const { resolveBoolChainFrames } = require('./handlers/control_flow_jumps');
|
|
4
5
|
|
|
5
6
|
Array.prototype.top = function ArrayTop (pos = 0) {
|
|
6
7
|
return this[this.length - pos - 1];
|
|
@@ -13,6 +14,7 @@ Array.prototype.empty = function ArrayIsEmpty () {
|
|
|
13
14
|
class PycDecompiler {
|
|
14
15
|
static opCodeHandlers = {};
|
|
15
16
|
cleanBuild = false;
|
|
17
|
+
errors = []; // Per-opcode exceptions caught by main loop. Non-empty → cleanBuild=false.
|
|
16
18
|
object = null;
|
|
17
19
|
code = null;
|
|
18
20
|
|
|
@@ -97,21 +99,32 @@ class PycDecompiler {
|
|
|
97
99
|
typeof fileExports[handlerName] === 'function' &&
|
|
98
100
|
handlerName.startsWith("handle")) {
|
|
99
101
|
|
|
100
|
-
// Convert handler name (e.g., "handleJumpForwardA") to opcode name ("JUMP_FORWARD_A")
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
// Convert handler name (e.g., "handleJumpForwardA") to opcode name ("JUMP_FORWARD_A").
|
|
103
|
+
// Primary rule: camelCase → SNAKE_CASE with explicit runs-of-caps handling
|
|
104
|
+
// (HandleABCDef → ABC_DEF). Some legacy handlers like handleCallIntrinsic1A
|
|
105
|
+
// still map via the looser regex — OpCodes.js provides both _1A and _1_A aliases.
|
|
106
|
+
const body = handlerName.replace(/^handle/, '');
|
|
107
|
+
let opCodeName = body
|
|
108
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
109
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1_$2')
|
|
110
|
+
.toUpperCase();
|
|
111
|
+
if (!(opCodeName in OpCodesMap)) {
|
|
112
|
+
const legacy = body
|
|
113
|
+
.replaceAll(/([A-Z][a-z]+)/g, m => m.toUpperCase() + '_')
|
|
114
|
+
.replace(/_$/, '');
|
|
115
|
+
if (legacy in OpCodesMap) opCodeName = legacy;
|
|
116
|
+
}
|
|
104
117
|
|
|
105
118
|
if (opCodeName in OpCodesMap) {
|
|
106
119
|
const opCodeId = OpCodesMap[opCodeName];
|
|
107
120
|
const handlerFunc = fileExports[handlerName];
|
|
108
121
|
|
|
109
122
|
if (PycDecompiler.opCodeHandlers[opCodeId]) {
|
|
110
|
-
|
|
123
|
+
throw new Error(`Static handler collision: OpCode ${opCodeName} (${opCodeId}) already bound; refusing silent overwrite from ${entry.name}:${handlerName}.`);
|
|
111
124
|
}
|
|
112
125
|
PycDecompiler.opCodeHandlers[opCodeId] = handlerFunc;
|
|
113
126
|
} else {
|
|
114
|
-
|
|
127
|
+
throw new Error(`Static handler mapping failed: "${opCodeName}" (from ${entry.name}:${handlerName}) is not in OpCodes. Rename the handler to match a known opcode, or add the opcode to lib/OpCodes.js.`);
|
|
115
128
|
}
|
|
116
129
|
}
|
|
117
130
|
}
|
|
@@ -140,6 +153,10 @@ class PycDecompiler {
|
|
|
140
153
|
this.dedupeExceptHandlers(functonBody);
|
|
141
154
|
this.removeDuplicateReturns(functonBody);
|
|
142
155
|
|
|
156
|
+
if (this.object.Name == "<lambda>") {
|
|
157
|
+
this.foldLambdaConditional(functonBody);
|
|
158
|
+
}
|
|
159
|
+
|
|
143
160
|
if (this.object.Name != "<lambda>" && functonBody.last instanceof AST.ASTReturn && functonBody.last.value instanceof AST.ASTNone) {
|
|
144
161
|
functonBody.list.pop();
|
|
145
162
|
}
|
|
@@ -152,6 +169,81 @@ class PycDecompiler {
|
|
|
152
169
|
return functonBody;
|
|
153
170
|
}
|
|
154
171
|
|
|
172
|
+
foldLambdaConditional(body) {
|
|
173
|
+
// Lambda bodies must be single expressions. CPython often compiles a
|
|
174
|
+
// conditional expression `A if cond else B` as:
|
|
175
|
+
// POP_JUMP_IF_FALSE X; LOAD A; RETURN_VALUE; X: LOAD B; RETURN_VALUE
|
|
176
|
+
// The decompiler's default path leaves this as two statements which is
|
|
177
|
+
// invalid Python inside a lambda. Detect the patterns and fold into a
|
|
178
|
+
// single ASTReturn(ASTTernary).
|
|
179
|
+
const list = body?.list;
|
|
180
|
+
if (!Array.isArray(list) || list.length !== 2) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const first = list[0];
|
|
184
|
+
const second = list[1];
|
|
185
|
+
if (!(second instanceof AST.ASTReturn)) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const elseVal = second.value;
|
|
189
|
+
|
|
190
|
+
const makeCondBlock = (condExpr, negative) => {
|
|
191
|
+
const cb = new AST.ASTCondBlock(AST.ASTBlock.BlockType.If, 0, 0, condExpr, !!negative);
|
|
192
|
+
return cb;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
let tern = null;
|
|
196
|
+
|
|
197
|
+
// Case 1: first is `ASTReturn(ASTBinary(cond AND/OR val))` — the line-900
|
|
198
|
+
// collapse already ran. Recover cond/val/direction from the binary op.
|
|
199
|
+
if (first instanceof AST.ASTReturn && first.value instanceof AST.ASTBinary) {
|
|
200
|
+
const bin = first.value;
|
|
201
|
+
const isAnd = bin.op === AST.ASTBinary.BinOp.LogicalAnd;
|
|
202
|
+
const isOr = bin.op === AST.ASTBinary.BinOp.LogicalOr;
|
|
203
|
+
if (isAnd || isOr) {
|
|
204
|
+
const cond = bin.left;
|
|
205
|
+
const thenVal = bin.right;
|
|
206
|
+
const cb = makeCondBlock(cond, isOr);
|
|
207
|
+
tern = new AST.ASTTernary(cb, thenVal, elseVal);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Case 2: first is an ASTCondBlock (if-return not collapsed by line 900).
|
|
212
|
+
if (!tern && first instanceof AST.ASTCondBlock &&
|
|
213
|
+
first.blockType === AST.ASTBlock.BlockType.If &&
|
|
214
|
+
Array.isArray(first.nodes) && first.nodes.length === 1 &&
|
|
215
|
+
first.nodes[0] instanceof AST.ASTReturn) {
|
|
216
|
+
const thenVal = first.nodes[0].value;
|
|
217
|
+
const cb = makeCondBlock(first.condition, first.negative);
|
|
218
|
+
tern = new AST.ASTTernary(cb, thenVal, elseVal);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!tern) {
|
|
222
|
+
// Two plain-return statements with no folded condition = optimizer
|
|
223
|
+
// eliminated the `if X else Y` when X was a constant, or the first
|
|
224
|
+
// return short-circuits the whole lambda (e.g. chained compare).
|
|
225
|
+
// The second return is unreachable; drop it so the lambda renders
|
|
226
|
+
// as a valid expression. Also covers generator lambdas where the
|
|
227
|
+
// yield expression is followed by the implicit `return None`.
|
|
228
|
+
const secondIsImplicitNone =
|
|
229
|
+
second instanceof AST.ASTReturn &&
|
|
230
|
+
second.rettype === AST.ASTReturn.RetType.Return &&
|
|
231
|
+
(!second.value || second.value instanceof AST.ASTNone);
|
|
232
|
+
if (first instanceof AST.ASTReturn &&
|
|
233
|
+
(first.rettype === AST.ASTReturn.RetType.Return || secondIsImplicitNone)) {
|
|
234
|
+
body.list.length = 1;
|
|
235
|
+
first.inLambda = true;
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const folded = new AST.ASTReturn(tern);
|
|
241
|
+
folded.inLambda = true;
|
|
242
|
+
folded.line = second.line ?? first.line;
|
|
243
|
+
body.list.length = 0;
|
|
244
|
+
body.list.push(folded);
|
|
245
|
+
}
|
|
246
|
+
|
|
155
247
|
append_to_chain_store(chainStore, item)
|
|
156
248
|
{
|
|
157
249
|
if (this.dataStack.top() == item) {
|
|
@@ -243,6 +335,23 @@ class PycDecompiler {
|
|
|
243
335
|
if (!(rit instanceof AST.ASTBlock) ||
|
|
244
336
|
rit.blockType != AST.ASTBlock.BlockType.If)
|
|
245
337
|
return;
|
|
338
|
+
// Peek before mutating: a real ternary needs both branches as expressions
|
|
339
|
+
// on the data stack. Otherwise the if/else are real statements (the bytecode
|
|
340
|
+
// path covers a control-flow if, not an if-expr) and rewriting them produces
|
|
341
|
+
// a ##ERROR## ternary that masks the real source.
|
|
342
|
+
const else_candidate = this.dataStack.top();
|
|
343
|
+
if (else_candidate == null) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const ifBlockCandidate = this.curBlock.nodes[this.curBlock.nodes.length - 2];
|
|
347
|
+
const stackDepth = this.dataStack.length;
|
|
348
|
+
const ifExprOnStack = stackDepth >= 2 ? this.dataStack[stackDepth - 2] : null;
|
|
349
|
+
const ifExprFromBody = ifExprOnStack == null && ifBlockCandidate.nodes.length == 1
|
|
350
|
+
? ifBlockCandidate.nodes[0]
|
|
351
|
+
: ifExprOnStack;
|
|
352
|
+
if (ifExprFromBody == null) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
246
355
|
let else_expr = this.dataStack.pop();
|
|
247
356
|
this.curBlock.removeLast();
|
|
248
357
|
let if_block = this.curBlock.nodes.top();
|
|
@@ -520,7 +629,22 @@ class PycDecompiler {
|
|
|
520
629
|
if (!matches.length) {
|
|
521
630
|
return this.maxExceptionHandlerEnd || null;
|
|
522
631
|
}
|
|
523
|
-
|
|
632
|
+
let end = Math.max(...matches.map(e => e.end));
|
|
633
|
+
// CPython 3.11+ splits a single source-level except handler into
|
|
634
|
+
// contiguous depth>0 sub-regions (alias-binding, body, cleanup).
|
|
635
|
+
// Walk forward through adjacent depth>0 entries so the Except block
|
|
636
|
+
// covers the full handler body, not just the alias-binding region.
|
|
637
|
+
let guard = 0;
|
|
638
|
+
while (guard++ < 32) {
|
|
639
|
+
const next = entries.find(e =>
|
|
640
|
+
e.depth > 0 &&
|
|
641
|
+
e.start >= end &&
|
|
642
|
+
e.start <= end + 2 &&
|
|
643
|
+
e.end > end);
|
|
644
|
+
if (!next) break;
|
|
645
|
+
end = next.end;
|
|
646
|
+
}
|
|
647
|
+
return end;
|
|
524
648
|
}
|
|
525
649
|
|
|
526
650
|
statements () {
|
|
@@ -538,6 +662,83 @@ class PycDecompiler {
|
|
|
538
662
|
// Open blocks based on 3.11+ exception table (no SETUP_EXCEPT opcodes)
|
|
539
663
|
this.ensureExceptionTableBlocks();
|
|
540
664
|
|
|
665
|
+
// Early-return/raise in try body: CPython omits POP_BLOCK+JUMP_FORWARD,
|
|
666
|
+
// so the Try block is still open when we reach the except/finally handler.
|
|
667
|
+
// Close it and open Except/Finally here so handler prologue is consumed
|
|
668
|
+
// in the right block and __exception__ doesn't leak into Try.
|
|
669
|
+
if (this.code.Current.Offset > 0 && this.blocks.length > 1) {
|
|
670
|
+
let tryIdx = -1, containerIdx = -1, isFinally = false;
|
|
671
|
+
for (let i = this.blocks.length - 1; i >= 0; i--) {
|
|
672
|
+
const blk = this.blocks[i];
|
|
673
|
+
if (blk.blockType === AST.ASTBlock.BlockType.Container) {
|
|
674
|
+
if (blk.hasExcept && blk.except === this.code.Current.Offset) {
|
|
675
|
+
containerIdx = i;
|
|
676
|
+
isFinally = false;
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
if (blk.hasFinally && !blk.hasExcept
|
|
680
|
+
&& blk.finally === this.code.Current.Offset) {
|
|
681
|
+
containerIdx = i;
|
|
682
|
+
isFinally = true;
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (blk.blockType === AST.ASTBlock.BlockType.Try && tryIdx === -1) {
|
|
687
|
+
tryIdx = i;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (containerIdx >= 0 && tryIdx > containerIdx
|
|
691
|
+
&& this.curBlock.blockType !== AST.ASTBlock.BlockType.Except
|
|
692
|
+
&& this.curBlock.blockType !== AST.ASTBlock.BlockType.Finally) {
|
|
693
|
+
while (this.blocks.length - 1 > containerIdx) {
|
|
694
|
+
const top = this.blocks[this.blocks.length - 1];
|
|
695
|
+
this.blocks.pop();
|
|
696
|
+
this.curBlock = this.blocks.top();
|
|
697
|
+
this.curBlock.append(top);
|
|
698
|
+
}
|
|
699
|
+
const cont = this.curBlock;
|
|
700
|
+
const handlerStart = isFinally ? cont.finally : cont.except;
|
|
701
|
+
let handlerEnd = handlerStart;
|
|
702
|
+
let cursor = handlerStart;
|
|
703
|
+
for (let k = 0; k < 500; k++) {
|
|
704
|
+
const instr = this.code.PeekInstructionAtOffset(cursor);
|
|
705
|
+
if (!instr) break;
|
|
706
|
+
const op = instr.OpCodeID;
|
|
707
|
+
if (op === this.OpCodes.END_FINALLY
|
|
708
|
+
|| op === this.OpCodes.POP_EXCEPT
|
|
709
|
+
|| op === this.OpCodes.RERAISE_A
|
|
710
|
+
|| op === this.OpCodes.RERAISE) {
|
|
711
|
+
handlerEnd = instr.Offset;
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
if (op === this.OpCodes.RETURN_VALUE
|
|
715
|
+
|| op === this.OpCodes.RETURN_VALUE_A
|
|
716
|
+
|| op === this.OpCodes.RETURN_CONST_A
|
|
717
|
+
|| op === this.OpCodes.RAISE_VARARGS_A) {
|
|
718
|
+
handlerEnd = instr.Offset + (instr.Size || 2);
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
cursor = instr.Offset + (instr.Size || 2);
|
|
722
|
+
}
|
|
723
|
+
if (isFinally) {
|
|
724
|
+
let finallyBlk = new AST.ASTBlock(AST.ASTBlock.BlockType.Finally, this.code.Current.Offset, handlerEnd, true);
|
|
725
|
+
this.blocks.push(finallyBlk);
|
|
726
|
+
this.curBlock = this.blocks.top();
|
|
727
|
+
if (global.g_cliArgs?.debug) {
|
|
728
|
+
console.log(`[EarlyReturnFinally] Opened Finally at ${this.code.Current.Offset}, handlerEnd=${handlerEnd}`);
|
|
729
|
+
}
|
|
730
|
+
} else {
|
|
731
|
+
let except = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Except, this.code.Current.Offset, handlerEnd, null, false);
|
|
732
|
+
except.init();
|
|
733
|
+
this.blocks.push(except);
|
|
734
|
+
this.curBlock = this.blocks.top();
|
|
735
|
+
if (global.g_cliArgs?.debug) {
|
|
736
|
+
console.log(`[EarlyReturnExcept] Opened Except at ${this.code.Current.Offset}, handlerEnd=${handlerEnd}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
541
742
|
// Python 3.8: When entering exception handler, push exception instance to stack
|
|
542
743
|
if (this.exceptionHandlerOffsets.has(this.code.Current.Offset)) {
|
|
543
744
|
// Create synthetic exception instance placeholder
|
|
@@ -708,10 +909,33 @@ class PycDecompiler {
|
|
|
708
909
|
}
|
|
709
910
|
}
|
|
710
911
|
|
|
912
|
+
// Resolve any open boolean-expression chain frames whose target
|
|
913
|
+
// offset matches the current instruction. This folds pending
|
|
914
|
+
// JUMP_IF_FALSE/TRUE lhs+rhs pairs into ASTBinary AND/OR nodes
|
|
915
|
+
// and pushes the result onto dataStack before the consumer
|
|
916
|
+
// (STORE_*, RETURN_*, CALL_*, or a nested JUMP_IF_*) fires.
|
|
917
|
+
if (this.boolChainStack && this.boolChainStack.length > 0) {
|
|
918
|
+
resolveBoolChainFrames(this, this.code.Current.Offset);
|
|
919
|
+
}
|
|
920
|
+
|
|
711
921
|
if (this.code.Current.OpCodeID in PycDecompiler.opCodeHandlers)
|
|
712
922
|
{
|
|
713
923
|
PycDecompiler.opCodeHandlers[this.code.Current.OpCodeID].call(this);
|
|
714
924
|
} else {
|
|
925
|
+
// Unsupported opcode: bail out of the loop with what we have.
|
|
926
|
+
// Record so `--strict` and the dirty-files report can surface it.
|
|
927
|
+
this.errors.push({
|
|
928
|
+
opcode: this.code.Current.InstructionName,
|
|
929
|
+
argument: this.code.Current.Argument,
|
|
930
|
+
offset: this.code.Current.Offset,
|
|
931
|
+
fileOffset: this.object.codeOffset + this.code.Current.Offset,
|
|
932
|
+
codeObject: this.object.Name,
|
|
933
|
+
message: `Unsupported opcode ${this.code.Current.InstructionName}`,
|
|
934
|
+
unsupported: true
|
|
935
|
+
});
|
|
936
|
+
if (global.g_cliArgs?.strict) {
|
|
937
|
+
throw new Error(`Unsupported opcode ${this.code.Current.InstructionName} at offset ${this.code.Current.Offset}`);
|
|
938
|
+
}
|
|
715
939
|
if (!g_cliArgs?.silent) {
|
|
716
940
|
console.error(`Unsupported opcode ${this.code.Current.InstructionName} at pos ${this.code.Current.Offset}\n`);
|
|
717
941
|
}
|
|
@@ -727,6 +951,21 @@ class PycDecompiler {
|
|
|
727
951
|
&& (this.curBlock.end == this.code.Next?.Offset);
|
|
728
952
|
|
|
729
953
|
} catch (ex) {
|
|
954
|
+
// Record the failure so callers can detect partial output. Without this
|
|
955
|
+
// accumulator the main loop swallows handler exceptions silently and
|
|
956
|
+
// cleanBuild gets reset to true at end-of-loop, masking corruption.
|
|
957
|
+
this.errors.push({
|
|
958
|
+
opcode: this.code.Current.InstructionName,
|
|
959
|
+
argument: this.code.Current.Argument,
|
|
960
|
+
offset: this.code.Current.Offset,
|
|
961
|
+
fileOffset: this.object.codeOffset + this.code.Current.Offset,
|
|
962
|
+
codeObject: this.object.Name,
|
|
963
|
+
message: ex.message,
|
|
964
|
+
stack: ex.stack
|
|
965
|
+
});
|
|
966
|
+
if (global.g_cliArgs?.strict) {
|
|
967
|
+
throw ex;
|
|
968
|
+
}
|
|
730
969
|
if (!g_cliArgs?.silent) {
|
|
731
970
|
console.error(`EXCEPTION for OpCode ${this.code.Current.InstructionName} (${this.code.Current.Argument}) at offset ${this.code.Current.Offset} in code object '${this.object.Name}', file offset ${this.object.codeOffset + this.code.Current.Offset} : ${ex.message}\n\n`);
|
|
732
971
|
if (global.g_cliArgs?.debug) {
|
|
@@ -764,7 +1003,7 @@ class PycDecompiler {
|
|
|
764
1003
|
}
|
|
765
1004
|
}
|
|
766
1005
|
|
|
767
|
-
this.cleanBuild =
|
|
1006
|
+
this.cleanBuild = this.errors.length === 0;
|
|
768
1007
|
let mainNode = new AST.ASTNodeList(this.defBlock.nodes);
|
|
769
1008
|
return mainNode;
|
|
770
1009
|
}
|
|
@@ -1123,10 +1362,19 @@ class PycDecompiler {
|
|
|
1123
1362
|
return true;
|
|
1124
1363
|
});
|
|
1125
1364
|
if (aliasName) {
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1365
|
+
const existing = blk.condition;
|
|
1366
|
+
if (existing instanceof AST.ASTStore) {
|
|
1367
|
+
// Condition already encodes `Type as alias` (set by misc_other.js
|
|
1368
|
+
// POP_TOP→STORE_FAST→POP_TOP path). Don't double-wrap.
|
|
1369
|
+
if (matchType) {
|
|
1370
|
+
blk.isExceptStar = true;
|
|
1371
|
+
}
|
|
1372
|
+
} else {
|
|
1373
|
+
const cond = matchType || existing || new AST.ASTName("Exception");
|
|
1374
|
+
blk.condition = new AST.ASTStore(cond, aliasName);
|
|
1375
|
+
if (matchType) {
|
|
1376
|
+
blk.isExceptStar = true;
|
|
1377
|
+
}
|
|
1130
1378
|
}
|
|
1131
1379
|
}
|
|
1132
1380
|
if (blk.m_nodes.length > 3) {
|
|
@@ -1256,14 +1504,15 @@ class PycDecompiler {
|
|
|
1256
1504
|
const alias = node.condition instanceof AST.ASTStore ? node.condition.dest : null;
|
|
1257
1505
|
const filtered = (node.nodes || []).filter(n => !isCleanupNode(n, alias));
|
|
1258
1506
|
|
|
1259
|
-
//
|
|
1260
|
-
//
|
|
1261
|
-
|
|
1507
|
+
// Only synthetic except* handlers without a condition AND no real body
|
|
1508
|
+
// should be dropped; user-written `except:` and `except E:` blocks must
|
|
1509
|
+
// be preserved (with `pass` if the body was nothing but cleanup).
|
|
1510
|
+
if (filtered.length === 0) {
|
|
1262
1511
|
node.m_nodes = [new AST.ASTKeyword(AST.ASTKeyword.Word.Pass)];
|
|
1263
1512
|
} else if (filtered.length !== (node.nodes || []).length) {
|
|
1264
1513
|
node.m_nodes = filtered;
|
|
1265
1514
|
}
|
|
1266
|
-
if (Array.isArray(parentArr) &&
|
|
1515
|
+
if (Array.isArray(parentArr) && node.isExceptStar && !node.condition && filtered.length === 0) {
|
|
1267
1516
|
parentArr.splice(idx, 1);
|
|
1268
1517
|
continue;
|
|
1269
1518
|
}
|
|
@@ -1361,28 +1610,70 @@ class PycDecompiler {
|
|
|
1361
1610
|
}
|
|
1362
1611
|
|
|
1363
1612
|
rewriteClassDefinitions(root) {
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1613
|
+
const hasDataclass = (root instanceof AST.ASTNodeList) && this.astHasDataclassImport(root);
|
|
1614
|
+
this._rewriteClassDefsInNodes(this._collectStatementNodes(root), hasDataclass, new Set());
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
_collectStatementNodes(container) {
|
|
1618
|
+
if (!container) return [];
|
|
1619
|
+
if (container instanceof AST.ASTNodeList) return container.list;
|
|
1620
|
+
if (container instanceof AST.ASTBlock) return container.nodes;
|
|
1621
|
+
return [];
|
|
1622
|
+
}
|
|
1367
1623
|
|
|
1368
|
-
|
|
1624
|
+
_rewriteClassDefsInNodes(nodes, hasDataclass, visited) {
|
|
1625
|
+
if (!Array.isArray(nodes) || visited.has(nodes)) return;
|
|
1626
|
+
visited.add(nodes);
|
|
1369
1627
|
|
|
1370
|
-
for (const node of
|
|
1371
|
-
if (
|
|
1372
|
-
|
|
1628
|
+
for (const node of nodes) {
|
|
1629
|
+
if (node instanceof AST.ASTStore) {
|
|
1630
|
+
this._maybeRewriteClassStore(node, hasDataclass);
|
|
1373
1631
|
}
|
|
1632
|
+
for (const child of this._childContainers(node)) {
|
|
1633
|
+
this._rewriteClassDefsInNodes(child, hasDataclass, visited);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1374
1637
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1638
|
+
_maybeRewriteClassStore(node, hasDataclass) {
|
|
1639
|
+
if (node.src instanceof AST.ASTCall && node.src.func instanceof AST.ASTClass &&
|
|
1640
|
+
this.isPlainClassCall(node.src)) {
|
|
1641
|
+
node.src = node.src.func;
|
|
1642
|
+
this.cleanupClassBody(node.src);
|
|
1643
|
+
if (hasDataclass) {
|
|
1644
|
+
node.addDecorator(new AST.ASTName('dataclass'));
|
|
1645
|
+
}
|
|
1646
|
+
} else if (node.src instanceof AST.ASTCall && node.src.func instanceof AST.ASTClass &&
|
|
1647
|
+
this.isClassCallWithOnlyKwargs(node.src)) {
|
|
1648
|
+
const callNode = node.src;
|
|
1649
|
+
const classNode = callNode.func;
|
|
1650
|
+
classNode.kwargs = callNode.kwparams || [];
|
|
1651
|
+
node.src = classNode;
|
|
1652
|
+
this.cleanupClassBody(classNode);
|
|
1653
|
+
if (hasDataclass) {
|
|
1654
|
+
node.addDecorator(new AST.ASTName('dataclass'));
|
|
1655
|
+
}
|
|
1656
|
+
} else if (node.src instanceof AST.ASTClass) {
|
|
1657
|
+
this.cleanupClassBody(node.src);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
_childContainers(node) {
|
|
1662
|
+
const containers = [];
|
|
1663
|
+
if (node instanceof AST.ASTBlock) {
|
|
1664
|
+
containers.push(node.nodes);
|
|
1665
|
+
}
|
|
1666
|
+
if (node?.body && (node.body instanceof AST.ASTBlock || node.body instanceof AST.ASTNodeList)) {
|
|
1667
|
+
containers.push(this._collectStatementNodes(node.body));
|
|
1668
|
+
}
|
|
1669
|
+
// Generic walk for nested blocks/lists in object properties (e.g. ifBlock.elseBlock).
|
|
1670
|
+
for (const key of ['elseBlock', 'finallyBlock', 'tryBlock', 'thenBlock']) {
|
|
1671
|
+
const val = node?.[key];
|
|
1672
|
+
if (val instanceof AST.ASTBlock || val instanceof AST.ASTNodeList) {
|
|
1673
|
+
containers.push(this._collectStatementNodes(val));
|
|
1384
1674
|
}
|
|
1385
1675
|
}
|
|
1676
|
+
return containers;
|
|
1386
1677
|
}
|
|
1387
1678
|
|
|
1388
1679
|
isPlainClassCall(call) {
|
|
@@ -1395,6 +1686,15 @@ class PycDecompiler {
|
|
|
1395
1686
|
return !hasParams;
|
|
1396
1687
|
}
|
|
1397
1688
|
|
|
1689
|
+
isClassCallWithOnlyKwargs(call) {
|
|
1690
|
+
if (!(call.func instanceof AST.ASTClass)) {
|
|
1691
|
+
return false;
|
|
1692
|
+
}
|
|
1693
|
+
const hasPparams = call.pparams && call.pparams.length > 0;
|
|
1694
|
+
const hasKwparams = call.kwparams && call.kwparams.length > 0;
|
|
1695
|
+
return !hasPparams && hasKwparams && !call.hasVar && !call.hasKw;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1398
1698
|
astHasDataclassImport(root) {
|
|
1399
1699
|
if (!(root instanceof AST.ASTNodeList)) {
|
|
1400
1700
|
return false;
|
|
@@ -1520,12 +1820,23 @@ class PycDecompiler {
|
|
|
1520
1820
|
}
|
|
1521
1821
|
}
|
|
1522
1822
|
|
|
1523
|
-
// Drop trivial generic except handlers that only contain pass/cleanup.
|
|
1823
|
+
// Drop trivial generic except* handlers (exception-group artifacts) that only contain pass/cleanup.
|
|
1824
|
+
// User-written `except:` and `except Exception:` must be preserved.
|
|
1825
|
+
// Python forbids mixing `except` and `except*` inside the same try, so a
|
|
1826
|
+
// non-star trivial `except Exception [as X]: pass` in a try that has any
|
|
1827
|
+
// `except*` sibling is always a decompiler artifact.
|
|
1828
|
+
const hasExceptStarSibling = nodes.some(n =>
|
|
1829
|
+
n instanceof AST.ASTCondBlock &&
|
|
1830
|
+
n.blockType === AST.ASTBlock.BlockType.Except &&
|
|
1831
|
+
n.isExceptStar);
|
|
1524
1832
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
1525
1833
|
const blk = nodes[i];
|
|
1526
1834
|
if (!(blk instanceof AST.ASTCondBlock) || blk.blockType !== AST.ASTBlock.BlockType.Except) {
|
|
1527
1835
|
continue;
|
|
1528
1836
|
}
|
|
1837
|
+
if (!blk.isExceptStar && !hasExceptStarSibling) {
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1529
1840
|
const condNode = blk.condition instanceof AST.ASTStore ? blk.condition.src : blk.condition;
|
|
1530
1841
|
const condText = (typeof condNode?.codeFragment === 'function' ? condNode.codeFragment()?.toString?.() : null) || condNode?.name;
|
|
1531
1842
|
const body = blk.nodes || [];
|
|
@@ -1603,6 +1914,11 @@ class PycDecompiler {
|
|
|
1603
1914
|
if (!(node instanceof AST.ASTCondBlock)) {
|
|
1604
1915
|
return false;
|
|
1605
1916
|
}
|
|
1917
|
+
// `while True: pass` / `while 1:` has no explicit condition but is a legitimate
|
|
1918
|
+
// user construct; only If blocks are null-sentinel artifacts from with-cleanup.
|
|
1919
|
+
if (node.blockType !== AST.ASTBlock.BlockType.If) {
|
|
1920
|
+
return false;
|
|
1921
|
+
}
|
|
1606
1922
|
const condition = node.condition;
|
|
1607
1923
|
|
|
1608
1924
|
// Drop degenerate IF blocks with no condition and empty body (often leftover from with cleanup tests)
|
|
@@ -1843,25 +2159,72 @@ class PycDecompiler {
|
|
|
1843
2159
|
}
|
|
1844
2160
|
|
|
1845
2161
|
const targetName = node.dest?.name || node.dest?.codeFragment?.();
|
|
2162
|
+
const isClassLike = targetName && targetName[0] === targetName[0]?.toUpperCase?.();
|
|
2163
|
+
|
|
2164
|
+
// Preferred path (3.13+): the wrapper's own SourceCode ends with
|
|
2165
|
+
// `return <ASTFunction>` where the function already has correct
|
|
2166
|
+
// annotations (via SET_FUNCTION_ATTRIBUTE) and typeParams with
|
|
2167
|
+
// PEP 696 defaults (via CALL_INTRINSIC_2 intrinsics 5 + 4).
|
|
2168
|
+
const wrapperBody = func.code?.object?.SourceCode?.list || [];
|
|
2169
|
+
let returnedNode = null;
|
|
2170
|
+
for (let j = wrapperBody.length - 1; j >= 0; j--) {
|
|
2171
|
+
const stmt = wrapperBody[j];
|
|
2172
|
+
if (stmt instanceof AST.ASTReturn && stmt.value) {
|
|
2173
|
+
returnedNode = stmt.value;
|
|
2174
|
+
break;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
if (!isClassLike && returnedNode instanceof AST.ASTFunction) {
|
|
2178
|
+
node.m_src = returnedNode;
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
2181
|
+
// Classes: the wrapper's direct return is an ASTClass whose base is
|
|
2182
|
+
// an internal CPython temp (".generic_base" for SUBSCRIPT_GENERIC).
|
|
2183
|
+
// Fall through to the Consts-based rebuild which infers clean
|
|
2184
|
+
// typeParams from string names instead.
|
|
2185
|
+
|
|
2186
|
+
// Fallback (pre-3.13 or irregular wrappers): rebuild from Consts.
|
|
1846
2187
|
const codeConsts = func.code?.object?.Consts?.Value || [];
|
|
1847
|
-
const
|
|
2188
|
+
const allCodeObjects = codeConsts.filter(c => c?.ClassName === 'Py_CodeObject');
|
|
2189
|
+
let innerCodeObj = allCodeObjects.find(c => c.Name?.toString?.() === targetName);
|
|
2190
|
+
if (!innerCodeObj) {
|
|
2191
|
+
innerCodeObj = allCodeObjects[0];
|
|
2192
|
+
}
|
|
1848
2193
|
if (!innerCodeObj) {
|
|
1849
2194
|
continue;
|
|
1850
2195
|
}
|
|
1851
|
-
|
|
1852
|
-
const typeParams = codeConsts
|
|
2196
|
+
const typeParamNames = codeConsts
|
|
1853
2197
|
.filter(c => c?.ClassName === 'Py_String')
|
|
1854
2198
|
.map(c => c.Value)
|
|
1855
2199
|
.filter(v => /^[A-Z][A-Za-z0-9_]*$/.test(v || '') && v !== targetName);
|
|
1856
2200
|
|
|
1857
|
-
|
|
2201
|
+
const typeParams = typeParamNames.map(name => {
|
|
2202
|
+
const defaultCode = allCodeObjects.find(c =>
|
|
2203
|
+
c !== innerCodeObj &&
|
|
2204
|
+
(c.ArgCount || 0) === 0 &&
|
|
2205
|
+
c.Name?.toString?.() === name
|
|
2206
|
+
);
|
|
2207
|
+
if (!defaultCode) return name;
|
|
2208
|
+
try {
|
|
2209
|
+
const dd = new PycDecompiler(defaultCode);
|
|
2210
|
+
const body = dd.decompile();
|
|
2211
|
+
defaultCode.SourceCode = body;
|
|
2212
|
+
if (dd.errors.length) this.errors.push(...dd.errors);
|
|
2213
|
+
const list = body?.list || [];
|
|
2214
|
+
const top = list.top?.() || list[list.length - 1] || list[0];
|
|
2215
|
+
if (top instanceof AST.ASTReturn && top.value) {
|
|
2216
|
+
return { name, default: top.value };
|
|
2217
|
+
}
|
|
2218
|
+
} catch (_) { /* fall through */ }
|
|
2219
|
+
return name;
|
|
2220
|
+
});
|
|
2221
|
+
|
|
1858
2222
|
const innerDecompiler = new PycDecompiler(innerCodeObj);
|
|
1859
2223
|
const innerBody = innerDecompiler.decompile();
|
|
1860
2224
|
innerCodeObj.SourceCode = innerBody;
|
|
2225
|
+
if (innerDecompiler.errors.length) this.errors.push(...innerDecompiler.errors);
|
|
1861
2226
|
const astObj = new AST.ASTObject(innerCodeObj);
|
|
1862
2227
|
|
|
1863
|
-
const isClassLike = targetName && targetName[0] === targetName[0]?.toUpperCase?.();
|
|
1864
|
-
|
|
1865
2228
|
if (!isClassLike) {
|
|
1866
2229
|
const fn = new AST.ASTFunction(astObj);
|
|
1867
2230
|
fn.annotations = innerDecompiler.funcAnnotations || fn.annotations;
|
package/lib/PycDisassembler.js
CHANGED
|
@@ -17,7 +17,7 @@ class PycDisassembler
|
|
|
17
17
|
if (opCode.HasConstant) {
|
|
18
18
|
argValue = opCode.Constant;
|
|
19
19
|
} else if (opCode.HasName || opCode.HasLocal || opCode.HasFree) {
|
|
20
|
-
argValue = opCode.Name;
|
|
20
|
+
argValue = opCode.Name || opCode.LocalName || opCode.FreeName;
|
|
21
21
|
} else if (opCode.HasJumpRelative) {
|
|
22
22
|
argValue = opCode.Argument;
|
|
23
23
|
} else if (opCode.HasCompare) {
|