depyo 1.0.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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/depyo.js +213 -0
  4. package/lib/BinaryReader.js +153 -0
  5. package/lib/OpCode.js +90 -0
  6. package/lib/OpCodes.js +940 -0
  7. package/lib/PycDecompiler.js +2031 -0
  8. package/lib/PycDisassembler.js +55 -0
  9. package/lib/PycReader.js +905 -0
  10. package/lib/PycResult.js +82 -0
  11. package/lib/PythonObject.js +242 -0
  12. package/lib/Unpickle.js +173 -0
  13. package/lib/ast/ast_node.js +3442 -0
  14. package/lib/bytecode/python_1_0.js +116 -0
  15. package/lib/bytecode/python_1_1.js +116 -0
  16. package/lib/bytecode/python_1_3.js +119 -0
  17. package/lib/bytecode/python_1_4.js +121 -0
  18. package/lib/bytecode/python_1_5.js +120 -0
  19. package/lib/bytecode/python_1_6.js +124 -0
  20. package/lib/bytecode/python_2_0.js +137 -0
  21. package/lib/bytecode/python_2_1.js +142 -0
  22. package/lib/bytecode/python_2_2.js +147 -0
  23. package/lib/bytecode/python_2_3.js +145 -0
  24. package/lib/bytecode/python_2_4.js +147 -0
  25. package/lib/bytecode/python_2_5.js +147 -0
  26. package/lib/bytecode/python_2_6.js +147 -0
  27. package/lib/bytecode/python_2_7.js +151 -0
  28. package/lib/bytecode/python_3_0.js +132 -0
  29. package/lib/bytecode/python_3_1.js +135 -0
  30. package/lib/bytecode/python_3_10.js +312 -0
  31. package/lib/bytecode/python_3_11.js +284 -0
  32. package/lib/bytecode/python_3_12.js +327 -0
  33. package/lib/bytecode/python_3_13.js +173 -0
  34. package/lib/bytecode/python_3_14.js +177 -0
  35. package/lib/bytecode/python_3_2.js +136 -0
  36. package/lib/bytecode/python_3_3.js +136 -0
  37. package/lib/bytecode/python_3_4.js +137 -0
  38. package/lib/bytecode/python_3_5.js +149 -0
  39. package/lib/bytecode/python_3_6.js +153 -0
  40. package/lib/bytecode/python_3_7.js +292 -0
  41. package/lib/bytecode/python_3_8.js +294 -0
  42. package/lib/bytecode/python_3_9.js +296 -0
  43. package/lib/code_reader.js +146 -0
  44. package/lib/handlers/binary_ops.js +174 -0
  45. package/lib/handlers/collections_update.js +239 -0
  46. package/lib/handlers/comparisons.js +95 -0
  47. package/lib/handlers/context_managers.js +250 -0
  48. package/lib/handlers/control_flow_jumps.js +954 -0
  49. package/lib/handlers/exceptions_blocks.js +952 -0
  50. package/lib/handlers/formatting.js +31 -0
  51. package/lib/handlers/function_calls.js +496 -0
  52. package/lib/handlers/function_class_build.js +330 -0
  53. package/lib/handlers/generators_async.js +172 -0
  54. package/lib/handlers/imports.js +53 -0
  55. package/lib/handlers/load_store_names.js +711 -0
  56. package/lib/handlers/loop_iterator.js +318 -0
  57. package/lib/handlers/misc_other.js +1201 -0
  58. package/lib/handlers/pattern_matching.js +226 -0
  59. package/lib/handlers/stack_ops.js +280 -0
  60. package/lib/handlers/subscript_slice.js +394 -0
  61. package/lib/handlers/unary_ops.js +91 -0
  62. package/lib/handlers/unpack.js +141 -0
  63. package/lib/stack_history.js +63 -0
  64. package/lib/zip_reader.js +217 -0
  65. package/package.json +35 -0
@@ -0,0 +1,954 @@
1
+ const AST = require('../ast/ast_node');
2
+
3
+ function isInsideLoop(blocks) {
4
+ return blocks.some(b => [AST.ASTBlock.BlockType.While, AST.ASTBlock.BlockType.For, AST.ASTBlock.BlockType.AsyncFor].includes(b.blockType));
5
+ }
6
+
7
+ function findBackwardJump(code, startIndex, currentOffset, backJumpOpcodes) {
8
+ const instructions = code.Instructions || [];
9
+ const maxLookahead = 100;
10
+ for (let i = startIndex + 1; i < instructions.length && i <= startIndex + maxLookahead; i++) {
11
+ const instr = instructions[i];
12
+ if (!instr) {
13
+ break;
14
+ }
15
+ if (backJumpOpcodes.includes(instr.OpCodeID) && instr.JumpTarget <= currentOffset) {
16
+ return instr;
17
+ }
18
+ // Bail out once we are well past the current conditional to avoid accidental matches.
19
+ if (instr.Offset - currentOffset > 300) {
20
+ break;
21
+ }
22
+ }
23
+ return null;
24
+ }
25
+
26
+ function tryStartConditionalExpression(ctx, cond, falseOffset) {
27
+ if (!cond || falseOffset <= ctx.code.Current.Offset) {
28
+ return false;
29
+ }
30
+ const falseIdx = ctx.code.GetIndexByOffset(falseOffset);
31
+ if (falseIdx < 0) {
32
+ return false;
33
+ }
34
+
35
+ let joinInstr = null;
36
+ for (let i = ctx.code.CurrentInstructionIndex + 1; i < falseIdx; i++) {
37
+ const instr = ctx.code.Instructions[i];
38
+ if (!instr) {
39
+ continue;
40
+ }
41
+ if ([ctx.OpCodes.JUMP_FORWARD_A, ctx.OpCodes.JUMP_ABSOLUTE_A].includes(instr.OpCodeID)) {
42
+ const target = instr.JumpTarget ?? -1;
43
+ if (target > falseOffset && target <= falseOffset + 50) {
44
+ joinInstr = instr;
45
+ break;
46
+ }
47
+ }
48
+ }
49
+
50
+ if (!joinInstr) {
51
+ return false;
52
+ }
53
+
54
+ ctx.pendingConditionalExprs ||= [];
55
+ ctx.pendingConditionalExprs.push({
56
+ cond,
57
+ falseOffset,
58
+ joinOffset: joinInstr.JumpTarget,
59
+ trueValue: null,
60
+ startOffset: ctx.code.Current.Offset
61
+ });
62
+ return true;
63
+ }
64
+
65
+ function captureTrueBranchForConditional(ctx) {
66
+ if (!ctx.pendingConditionalExprs?.length) {
67
+ return false;
68
+ }
69
+ const target = ctx.code.Current.JumpTarget;
70
+ const pending = ctx.pendingConditionalExprs.find(p => p.joinOffset === target);
71
+ if (!pending) {
72
+ return false;
73
+ }
74
+ pending.trueValue = ctx.dataStack.pop();
75
+ return true;
76
+ }
77
+
78
+ function handleJumpIfFalseA() {
79
+ processJumpOps.call(this);
80
+ }
81
+
82
+ function handleJumpIfTrueA() {
83
+ processJumpOps.call(this);
84
+ }
85
+
86
+ function handleJumpIfFalseOrPopA() {
87
+ processJumpOps.call(this);
88
+ }
89
+
90
+ function handleJumpIfTrueOrPopA() {
91
+ processJumpOps.call(this);
92
+ }
93
+
94
+ function handlePopJumpIfFalseA() {
95
+ processJumpOps.call(this);
96
+ }
97
+
98
+ function handlePopJumpIfTrueA() {
99
+ processJumpOps.call(this);
100
+ }
101
+
102
+ function handlePopJumpForwardIfFalseA() {
103
+ processJumpOps.call(this);
104
+ }
105
+
106
+ function handlePopJumpForwardIfTrueA() {
107
+ processJumpOps.call(this);
108
+ }
109
+
110
+ function handlePopJumpForwardIfNoneA() {
111
+ processJumpOps.call(this);
112
+ }
113
+
114
+ function handlePopJumpForwardIfNotNoneA() {
115
+ processJumpOps.call(this);
116
+ }
117
+
118
+ function handlePopJumpBackwardIfNoneA() {
119
+ processJumpOps.call(this);
120
+ }
121
+
122
+ function handlePopJumpBackwardIfNotNoneA() {
123
+ processJumpOps.call(this);
124
+ }
125
+
126
+ function handlePopJumpIfNoneA() {
127
+ processJumpOps.call(this);
128
+ }
129
+
130
+ function handlePopJumpIfNotNoneA() {
131
+ processJumpOps.call(this);
132
+ }
133
+
134
+ function handleInstrumentedPopJumpIfFalseA() {
135
+ processJumpOps.call(this);
136
+ }
137
+
138
+ function handleInstrumentedPopJumpIfTrueA() {
139
+ processJumpOps.call(this);
140
+ }
141
+
142
+ function handleInstrumentedPopJumpIfNoneA() {
143
+ processJumpOps.call(this);
144
+ }
145
+
146
+ function handleInstrumentedPopJumpIfNotNoneA() {
147
+ processJumpOps.call(this);
148
+ }
149
+
150
+ function processJumpOps() {
151
+ if (this.skipNextJump) {
152
+ this.skipNextJump = false;
153
+ if (this.code.Next?.OpCodeID == this.OpCodes.POP_TOP) {
154
+ this.code.GoNext();
155
+ }
156
+ return;
157
+ }
158
+
159
+ const wrapNoneComparison = (expectNone) => {
160
+ let value = this.dataStack.pop();
161
+ let cmp = new AST.ASTCompare(
162
+ value,
163
+ new AST.ASTNone(),
164
+ expectNone ? AST.ASTCompare.CompareOp.Is : AST.ASTCompare.CompareOp.IsNot
165
+ );
166
+ cmp.line = this.code.Current.LineNo;
167
+ this.dataStack.push(cmp);
168
+ };
169
+
170
+ const jumpIfNotNoneOpcodes = [
171
+ this.OpCodes.POP_JUMP_FORWARD_IF_NOT_NONE_A,
172
+ this.OpCodes.POP_JUMP_BACKWARD_IF_NOT_NONE_A,
173
+ this.OpCodes.POP_JUMP_IF_NOT_NONE_A,
174
+ this.OpCodes.INSTRUMENTED_POP_JUMP_IF_NOT_NONE_A
175
+ ];
176
+
177
+ const jumpIfNoneOpcodes = [
178
+ this.OpCodes.POP_JUMP_FORWARD_IF_NONE_A,
179
+ this.OpCodes.POP_JUMP_BACKWARD_IF_NONE_A,
180
+ this.OpCodes.POP_JUMP_IF_NONE_A,
181
+ this.OpCodes.INSTRUMENTED_POP_JUMP_IF_NONE_A
182
+ ];
183
+
184
+ if (jumpIfNoneOpcodes.includes(this.code.Current.OpCodeID)) {
185
+ wrapNoneComparison(true);
186
+ } else if (jumpIfNotNoneOpcodes.includes(this.code.Current.OpCodeID)) {
187
+ wrapNoneComparison(false);
188
+ }
189
+
190
+ if (this.inMatchPattern) {
191
+ this.debug(`[processJumpOps] Inside match statement - tracking pattern instead of creating if block`);
192
+
193
+ if ([
194
+ this.OpCodes.POP_JUMP_IF_FALSE_A,
195
+ this.OpCodes.POP_JUMP_FORWARD_IF_FALSE_A,
196
+ this.OpCodes.POP_JUMP_IF_TRUE_A,
197
+ this.OpCodes.POP_JUMP_FORWARD_IF_TRUE_A,
198
+ this.OpCodes.POP_JUMP_FORWARD_IF_NONE_A,
199
+ this.OpCodes.POP_JUMP_FORWARD_IF_NOT_NONE_A,
200
+ this.OpCodes.POP_JUMP_BACKWARD_IF_NONE_A,
201
+ this.OpCodes.POP_JUMP_BACKWARD_IF_NOT_NONE_A,
202
+ this.OpCodes.POP_JUMP_IF_NONE_A,
203
+ this.OpCodes.POP_JUMP_IF_NOT_NONE_A,
204
+ this.OpCodes.INSTRUMENTED_POP_JUMP_IF_FALSE_A,
205
+ this.OpCodes.INSTRUMENTED_POP_JUMP_IF_TRUE_A,
206
+ this.OpCodes.INSTRUMENTED_POP_JUMP_IF_NONE_A,
207
+ this.OpCodes.INSTRUMENTED_POP_JUMP_IF_NOT_NONE_A
208
+ ].includes(this.code.Current.OpCodeID)) {
209
+ this.dataStack.pop();
210
+ }
211
+
212
+ return;
213
+ }
214
+
215
+ // CRITICAL: Close blocks that have ended before creating new conditional block
216
+ // This ensures proper sibling relationships between if/elif blocks
217
+ while (this.curBlock.end > 0 &&
218
+ this.curBlock.end <= this.code.Current.Offset &&
219
+ this.curBlock.blockType != AST.ASTBlock.BlockType.Main &&
220
+ this.blocks.length > 1) {
221
+
222
+ if (global.g_cliArgs?.debug) {
223
+ console.log(`[processJumpOps] Closing ended block ${this.curBlock.type_str}(${this.curBlock.start}-${this.curBlock.end}) at offset ${this.code.Current.Offset}`);
224
+ }
225
+
226
+ let closedBlock = this.blocks.pop();
227
+ this.curBlock = this.blocks.top();
228
+ this.curBlock.append(closedBlock);
229
+
230
+ if (global.g_cliArgs?.debug) {
231
+ console.log(` → Appended to ${this.curBlock.type_str}(${this.curBlock.start}-${this.curBlock.end}), now has ${this.curBlock.nodes.length} nodes`);
232
+ }
233
+ }
234
+
235
+ if (this.ignoreNextConditional) {
236
+ this.ignoreNextConditional = false;
237
+ const baseDepth = this.cleanupStackDepth || this.dataStack.length;
238
+ while (this.dataStack.length >= baseDepth) {
239
+ this.dataStack.pop();
240
+ }
241
+ this.cleanupStackDepth = null;
242
+ return;
243
+ }
244
+
245
+ let cond = this.dataStack.top();
246
+ let ifblk = null;
247
+ let popped = AST.ASTCondBlock.InitCondition.Uninited;
248
+
249
+ if ([
250
+ this.OpCodes.POP_JUMP_IF_FALSE_A,
251
+ this.OpCodes.POP_JUMP_IF_TRUE_A,
252
+ this.OpCodes.POP_JUMP_FORWARD_IF_FALSE_A,
253
+ this.OpCodes.POP_JUMP_FORWARD_IF_TRUE_A,
254
+ this.OpCodes.POP_JUMP_FORWARD_IF_NONE_A,
255
+ this.OpCodes.POP_JUMP_FORWARD_IF_NOT_NONE_A,
256
+ this.OpCodes.POP_JUMP_BACKWARD_IF_NONE_A,
257
+ this.OpCodes.POP_JUMP_BACKWARD_IF_NOT_NONE_A,
258
+ this.OpCodes.POP_JUMP_IF_NONE_A,
259
+ this.OpCodes.POP_JUMP_IF_NOT_NONE_A,
260
+ this.OpCodes.INSTRUMENTED_POP_JUMP_IF_FALSE_A,
261
+ this.OpCodes.INSTRUMENTED_POP_JUMP_IF_TRUE_A,
262
+ this.OpCodes.INSTRUMENTED_POP_JUMP_IF_NONE_A,
263
+ this.OpCodes.INSTRUMENTED_POP_JUMP_IF_NOT_NONE_A
264
+ ].includes(this.code.Current.OpCodeID)) {
265
+
266
+ /* Pop condition before the jump */
267
+ this.dataStack.pop();
268
+ popped = AST.ASTCondBlock.InitCondition.PrePopped;
269
+ } else if ([
270
+ this.OpCodes.JUMP_IF_FALSE_OR_POP_A,
271
+ this.OpCodes.JUMP_IF_TRUE_OR_POP_A,
272
+ this.OpCodes.JUMP_IF_FALSE_A,
273
+ this.OpCodes.JUMP_IF_TRUE_A
274
+ ].includes(this.code.Current.OpCodeID)) {
275
+ /* Pop condition only if condition is met */
276
+ this.dataStack.pop();
277
+ popped = AST.ASTCondBlock.InitCondition.Popped;
278
+ }
279
+
280
+ /* "Jump if true" means "Jump if not false" */
281
+ let neg = [
282
+ this.OpCodes.JUMP_IF_TRUE_A, this.OpCodes.JUMP_IF_TRUE_OR_POP_A,
283
+ this.OpCodes.POP_JUMP_IF_TRUE_A, this.OpCodes.POP_JUMP_FORWARD_IF_TRUE_A,
284
+ this.OpCodes.POP_JUMP_BACKWARD_IF_TRUE_A, this.OpCodes.INSTRUMENTED_POP_JUMP_IF_TRUE_A
285
+ ].includes(this.code.Current.OpCodeID);
286
+
287
+ let offs = this.code.Current.Argument;
288
+ if (this.object.Reader.versionCompare(3, 10) >= 0)
289
+ offs *= 2; // // BPO-27129
290
+ if (this.object.Reader.versionCompare(3, 12) >= 0
291
+ || [
292
+ this.OpCodes.JUMP_IF_FALSE_A, this.OpCodes.JUMP_IF_TRUE_A,
293
+ this.OpCodes.POP_JUMP_FORWARD_IF_TRUE_A, this.OpCodes.POP_JUMP_FORWARD_IF_FALSE_A,
294
+ this.OpCodes.POP_JUMP_FORWARD_IF_NONE_A, this.OpCodes.POP_JUMP_FORWARD_IF_NOT_NONE_A,
295
+ this.OpCodes.POP_JUMP_IF_NONE_A, this.OpCodes.POP_JUMP_IF_NOT_NONE_A,
296
+ this.OpCodes.INSTRUMENTED_POP_JUMP_IF_NONE_A, this.OpCodes.INSTRUMENTED_POP_JUMP_IF_NOT_NONE_A
297
+ ].includes(this.code.Current.OpCodeID)) {
298
+ /* Offset is relative in these cases */
299
+ offs += this.code.Next?.Offset;
300
+ }
301
+
302
+ const rawJumpTarget = offs; // target before block end normalization
303
+ [offs] = this.code.FindEndOfBlock(offs);
304
+
305
+ const condJumpOpcodes = new Set([
306
+ this.OpCodes.POP_JUMP_IF_FALSE_A,
307
+ this.OpCodes.POP_JUMP_IF_TRUE_A,
308
+ this.OpCodes.POP_JUMP_FORWARD_IF_FALSE_A,
309
+ this.OpCodes.POP_JUMP_FORWARD_IF_TRUE_A,
310
+ this.OpCodes.POP_JUMP_FORWARD_IF_NONE_A,
311
+ this.OpCodes.POP_JUMP_FORWARD_IF_NOT_NONE_A,
312
+ this.OpCodes.POP_JUMP_BACKWARD_IF_NONE_A,
313
+ this.OpCodes.POP_JUMP_BACKWARD_IF_NOT_NONE_A,
314
+ this.OpCodes.POP_JUMP_IF_NONE_A,
315
+ this.OpCodes.POP_JUMP_IF_NOT_NONE_A,
316
+ this.OpCodes.INSTRUMENTED_POP_JUMP_IF_FALSE_A,
317
+ this.OpCodes.INSTRUMENTED_POP_JUMP_IF_TRUE_A,
318
+ this.OpCodes.INSTRUMENTED_POP_JUMP_IF_NONE_A,
319
+ this.OpCodes.INSTRUMENTED_POP_JUMP_IF_NOT_NONE_A
320
+ ]);
321
+ const backJumpOpcodes = [
322
+ this.OpCodes.JUMP_ABSOLUTE_A,
323
+ this.OpCodes.JUMP_BACKWARD_A,
324
+ this.OpCodes.JUMP_BACKWARD_NO_INTERRUPT_A
325
+ ];
326
+
327
+ // Python 3.8+ while-loops no longer emit SETUP_LOOP; detect via backward jumps.
328
+ if (!this.inMatchPattern &&
329
+ condJumpOpcodes.has(this.code.Current.OpCodeID) &&
330
+ !isInsideLoop(this.blocks)) {
331
+ const backJump = findBackwardJump(this.code, this.code.CurrentInstructionIndex, this.code.Current.Offset, backJumpOpcodes);
332
+ // Treat as a while-loop only when the body extends past the backward jump target.
333
+ if (backJump && offs > backJump.Offset) {
334
+ const loopStart = backJump.JumpTarget;
335
+ const whileBlk = new AST.ASTCondBlock(AST.ASTBlock.BlockType.While, loopStart, offs, null, false);
336
+ whileBlk.line = this.code.Current.LineNo;
337
+ this.blocks.push(whileBlk);
338
+ this.curBlock = whileBlk;
339
+ }
340
+ }
341
+
342
+ // Conditional expression (a if cond else b) heuristic:
343
+ // POP_JUMP_IF_* to falseOffset, then JUMP_FORWARD to joinOffset > falseOffset.
344
+ if (!this.inMatchPattern &&
345
+ condJumpOpcodes.has(this.code.Current.OpCodeID) &&
346
+ tryStartConditionalExpression(this, cond, rawJumpTarget)) {
347
+ return;
348
+ }
349
+
350
+ if ([ this.OpCodes.JUMP_IF_FALSE_A,
351
+ this.OpCodes.JUMP_IF_TRUE_A
352
+ ].includes(this.code.Current.OpCodeID) &&
353
+ this.code.Next?.OpCodeID == this.OpCodes.POP_TOP
354
+ ) {
355
+ this.code.GoNext();
356
+ }
357
+
358
+ if (global.g_cliArgs?.debug) {
359
+ console.log(`\nConditional jump at offset ${this.code.Current.Offset}: curBlock=${this.curBlock.type_str} (type=${this.curBlock.blockType}), size=${this.curBlock.size}, inited=${this.curBlock.inited}`);
360
+ if (this.curBlock.size > 0) {
361
+ console.log(` Block nodes:`, this.curBlock.nodes.map(n => `${n.constructor.name}`));
362
+ }
363
+ }
364
+
365
+ const isEgMatchCall = cond instanceof AST.ASTCall &&
366
+ cond.func instanceof AST.ASTName &&
367
+ cond.func.name === "__check_eg_match__";
368
+ const isExceptCompare = cond instanceof AST.ASTCompare &&
369
+ cond.op == AST.ASTCompare.CompareOp.Exception;
370
+
371
+ if (isExceptCompare || isEgMatchCall) {
372
+ if (global.g_cliArgs?.debug) {
373
+ const matchType = isExceptCompare ? cond.right : cond.pparams?.[1];
374
+ console.log(` EXCEPTION MATCH detected: Creating Except block with condition=${matchType?.constructor?.name} ${matchType?.codeFragment?.()}`);
375
+ }
376
+
377
+ const handlerEnd = this.findExceptionHandlerEnd ? this.findExceptionHandlerEnd(this.code.Current.Offset) : null;
378
+ if (this.curBlock.blockType == AST.ASTBlock.BlockType.Except
379
+ && this.curBlock.condition == null) {
380
+ // Reuse current except block (from exception table) instead of creating a nested one
381
+ this.curBlock.condition = isExceptCompare ? cond.right : cond.pparams?.[1];
382
+ if (handlerEnd) {
383
+ this.curBlock.end = handlerEnd;
384
+ }
385
+ this.curBlock.init();
386
+ ifblk = null; // no extra push
387
+ } else {
388
+ const end = handlerEnd || offs;
389
+ const matchType = isExceptCompare ? cond.right : cond.pparams?.[1];
390
+ ifblk = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Except, this.code.Current.Offset, end, matchType, false);
391
+ this.inExceptionTableHandler = true;
392
+ }
393
+ } else if (this.curBlock.blockType == AST.ASTBlock.BlockType.Else) {
394
+ if (global.g_cliArgs?.debug) {
395
+ console.log(` Checking ELIF conditions: size=${this.curBlock.size}, blockType=${this.curBlock.blockType}`);
396
+ if (this.curBlock.size == 1) {
397
+ console.log(` First node: ${this.curBlock.nodes[0].constructor.name}, blockType=${this.curBlock.nodes[0].blockType}`);
398
+ }
399
+ }
400
+
401
+ if (this.curBlock.size == 0 ||
402
+ (this.curBlock.size == 1 &&
403
+ this.curBlock.nodes[0] instanceof AST.ASTCondBlock &&
404
+ this.curBlock.nodes[0].blockType == AST.ASTBlock.BlockType.If)) {
405
+ /* Collapse into elif statement */
406
+ if (global.g_cliArgs?.debug) {
407
+ console.log(`ELIF DETECTED: else block size=${this.curBlock.size}, converting to elif at offset ${this.code.Current.Offset}`);
408
+ }
409
+ let startOffset = this.curBlock.start;
410
+
411
+ // If else block contains an if statement, remove it (we're converting it to elif)
412
+ if (this.curBlock.size == 1) {
413
+ this.curBlock.nodes.pop();
414
+ }
415
+
416
+ this.blocks.pop();
417
+ ifblk = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Elif, startOffset, offs, cond, neg);
418
+ }
419
+ } else if (this.curBlock.blockType == AST.ASTBlock.BlockType.Else
420
+ && this.curBlock.size > 0) {
421
+ /* Else block not empty - elif not possible */
422
+ if (global.g_cliArgs?.debug) {
423
+ console.log(`ELIF NOT CREATED: else block size=${this.curBlock.size} (not 0) at offset ${this.code.Current.Offset}, nodes:`, this.curBlock.nodes.map(n => n.constructor.name));
424
+ }
425
+ }
426
+ if (this.curBlock.size == 0 && !this.curBlock.inited
427
+ && this.curBlock.blockType == AST.ASTBlock.BlockType.While
428
+ && this.code.Current.LineNo == this.curBlock.line) {
429
+ /* The condition for a while loop */
430
+ let top = this.blocks.top();
431
+ top.condition = cond;
432
+ top.negative = neg;
433
+ if (popped) {
434
+ top.init(popped);
435
+ }
436
+
437
+ if (global.g_cliArgs?.debug) {
438
+ console.log(`[processJumpOps] Set while condition at offset ${this.code.Current.Offset}: ${cond?.constructor?.name} = ${cond?.codeFragment ? cond.codeFragment() : cond}`);
439
+ }
440
+ return;
441
+ } else if (this.curBlock.size == 0 && this.curBlock.end <= offs
442
+ && [ AST.ASTBlock.BlockType.If,
443
+ AST.ASTBlock.BlockType.Elif,
444
+ AST.ASTBlock.BlockType.While
445
+ ].includes(this.curBlock.blockType)) {
446
+ let newcond;
447
+ let top = this.curBlock;
448
+ let cond1 = top.condition;
449
+ this.blocks.pop();
450
+
451
+ if (this.curBlock.end == offs
452
+ || (this.curBlock.end == this.code.Next?.Offset && !top.negative)) {
453
+ /* if blah and blah */
454
+ newcond = new AST.ASTBinary(cond1, cond, AST.ASTBinary.BinOp.LogicalAnd);
455
+ } else {
456
+ /* if <condition 1> or <condition 2> */
457
+ newcond = new AST.ASTBinary(cond1, cond, AST.ASTBinary.BinOp.LogicalOr);
458
+ }
459
+ newcond.line = this.code.Current.LineNo;
460
+ ifblk = new AST.ASTCondBlock(top.blockType, top.start, offs, newcond, neg);
461
+ } else if (this.curBlock.blockType == AST.ASTBlock.BlockType.For
462
+ && this.curBlock.comprehension
463
+ && this.object.Reader.versionCompare(2, 7) >= 0) {
464
+ /* Comprehension condition */
465
+ this.curBlock.condition = cond;
466
+ return;
467
+ }
468
+
469
+ if (!ifblk) {
470
+ /* Plain old if statement - but check if it should be elif */
471
+ let shouldBeElif = false;
472
+
473
+ // Check if this should be an elif instead of if
474
+ // This happens when there's no else block (e.g., when previous if/elif has return)
475
+ if (this.blocks.length > 0) {
476
+ let parent = this.blocks.top();
477
+ if (parent.size > 0) {
478
+ let lastNode = parent.nodes[parent.size - 1];
479
+
480
+ // If the last node in parent is an if/elif block, this should be elif
481
+ // The key insight: if lastNode is CLOSED (not in block stack), it's a sibling
482
+ // Check if lastNode is in block stack - if not, it's closed and safe to use as elif base
483
+ let lastNodeInStack = false;
484
+ for (let i = 0; i < this.blocks.length; i++) {
485
+ if (this.blocks[i] === lastNode) {
486
+ lastNodeInStack = true;
487
+ break;
488
+ }
489
+ }
490
+
491
+ if (lastNode instanceof AST.ASTCondBlock &&
492
+ (lastNode.blockType == AST.ASTBlock.BlockType.If ||
493
+ lastNode.blockType == AST.ASTBlock.BlockType.Elif) &&
494
+ !lastNodeInStack) { // CLOSED, not in stack = sibling!
495
+
496
+ shouldBeElif = true;
497
+
498
+ if (global.g_cliArgs?.debug) {
499
+ console.log(`ELIF DETECTED: Creating elif at offset ${this.code.Current.Offset} (follows ${lastNode.type_str} at ${lastNode.start})`);
500
+ }
501
+ }
502
+ }
503
+ }
504
+
505
+ if (shouldBeElif) {
506
+ ifblk = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Elif, this.code.Current.Offset, offs, cond, neg);
507
+ } else {
508
+ ifblk = new AST.ASTCondBlock(AST.ASTBlock.BlockType.If, this.code.Current.Offset, offs, cond, neg);
509
+ }
510
+ ifblk.line = this.code.Current.LineNo;
511
+ }
512
+
513
+ if (ifblk) {
514
+ if (popped)
515
+ ifblk.init(popped);
516
+
517
+ this.blocks.push(ifblk);
518
+ this.curBlock = this.blocks.top();
519
+ }
520
+ }
521
+
522
+ function handleJumpAbsoluteA() {
523
+ if (this.skipNextJump) {
524
+ this.skipNextJump = false;
525
+ return;
526
+ }
527
+
528
+ // CRITICAL: Close blocks that have ended before processing jump
529
+ // Unconditional jumps often mark the end of blocks (especially loops)
530
+ while (this.curBlock.end > 0 &&
531
+ this.curBlock.end <= this.code.Current.Offset &&
532
+ this.curBlock.blockType != AST.ASTBlock.BlockType.Main &&
533
+ this.blocks.length > 1) {
534
+
535
+ if (global.g_cliArgs?.debug) {
536
+ console.log(`[handleJumpAbsolute] Closing ended block ${this.curBlock.type_str}(${this.curBlock.start}-${this.curBlock.end}) at offset ${this.code.Current.Offset}`);
537
+ }
538
+
539
+ let closedBlock = this.blocks.pop();
540
+ this.curBlock = this.blocks.top();
541
+ this.curBlock.append(closedBlock);
542
+
543
+ if (global.g_cliArgs?.debug) {
544
+ console.log(` → Appended to ${this.curBlock.type_str}(${this.curBlock.start}-${this.curBlock.end}), now has ${this.curBlock.nodes.length} nodes`);
545
+ }
546
+ }
547
+
548
+ let offs = this.code.Current.Argument;
549
+ if (this.object.Reader.versionCompare(3, 10) >= 0) {
550
+ offs *= 2; // 2 bytes size - BPO-27129
551
+ }
552
+
553
+ // [offs] = this.code.FindEndOfBlock(offs);
554
+
555
+ if (offs <= this.code.Next?.Offset) {
556
+ if (this.curBlock.blockType == AST.ASTBlock.BlockType.For) {
557
+ let is_jump_to_start = offs == this.curBlock.start;
558
+ let should_pop_for_block = this.curBlock.comprehension;
559
+ // in v3.8, SETUP_LOOP is deprecated and for blocks aren't terminated by POP_BLOCK, so we add them here
560
+ let should_add_for_block = this.object.Reader.versionCompare(3, 8) >= 0 && is_jump_to_start && !this.curBlock.comprehension; // ||
561
+ // this.object.Reader.versionCompare(3, 8) < 0 && is_jump_to_start && this.curBlock.comprehension;
562
+
563
+ if (should_pop_for_block || should_add_for_block) {
564
+ let top = this.dataStack.top();
565
+
566
+ if (top instanceof AST.ASTComprehension) {
567
+ let comp = this.dataStack.pop();
568
+ comp.addGenerator(this.curBlock);
569
+ this.blocks.pop();
570
+ this.curBlock = this.blocks.top();
571
+ this.curBlock.append(comp);
572
+ } else {
573
+ let tmp = this.curBlock;
574
+ this.blocks.pop();
575
+ this.curBlock = this.blocks.top();
576
+ if (should_add_for_block ||
577
+ (this.curBlock === this.blocks[0] && this.curBlock.nodes.length == 0)) {
578
+ this.curBlock.append(tmp);
579
+ }
580
+ }
581
+ }
582
+ } else if (this.curBlock.blockType == AST.ASTBlock.BlockType.Else) {
583
+ this.blocks.pop();
584
+ this.blocks.top().append(this.curBlock);
585
+ this.curBlock = this.blocks.top();
586
+
587
+ if (this.curBlock.blockType == AST.ASTBlock.BlockType.Container
588
+ && !this.curBlock.hasFinally) {
589
+ this.blocks.pop();
590
+ this.blocks.top().append(this.curBlock);
591
+ this.curBlock = this.blocks.top();
592
+ }
593
+ } else {
594
+ // First of all we have to figure out if there is any While or For blocks wer are in
595
+ let loopBlock = null;
596
+ for (let blockIdx = this.blocks.length - 1; blockIdx > 0; blockIdx--) {
597
+ if ([AST.ASTBlock.BlockType.While, AST.ASTBlock.BlockType.For, AST.ASTBlock.BlockType.AsyncFor].includes(this.blocks[blockIdx].blockType)) {
598
+ loopBlock = this.blocks[blockIdx];
599
+ break;
600
+ }
601
+ }
602
+
603
+ if (!loopBlock) {
604
+ return;
605
+ }
606
+
607
+ // CRITICAL FIX: If JUMP target is OUTSIDE current loop (jump to outer loop or beyond),
608
+ // this marks the END of the current loop! Correct the loop's end offset.
609
+ // This handles nested loops where inner loop jumps to outer loop start.
610
+ if (loopBlock.start > offs) {
611
+ // Jump target is BEFORE loop start = jumping to outer scope
612
+ // Current offset should be the TRUE end of this loop
613
+ if (loopBlock.end > this.code.Current.Offset) {
614
+ if (global.g_cliArgs?.debug) {
615
+ console.log(`[handleJumpAbsolute] Correcting loop end: ${loopBlock.type_str}(${loopBlock.start}-${loopBlock.end}) → end=${this.code.Current.Offset} (jump to outer at ${offs})`);
616
+ }
617
+ loopBlock.end = this.code.Current.Offset;
618
+ }
619
+ }
620
+
621
+ if (this.curBlock.end == this.code.Next?.Offset) {
622
+ return;
623
+ }
624
+
625
+ if ([this.OpCodes.JUMP_ABSOLUTE_A, this.OpCodes.JUMP_FORWARD_A].includes(this.code.Prev?.OpCodeID)) {
626
+ return;
627
+ }
628
+
629
+ // Check if current block ends with a terminating keyword (break/continue/return)
630
+ // This check is recursive - it looks into nested blocks to find terminators
631
+ if (this.hasTerminatingKeyword(this.curBlock)) {
632
+ return;
633
+ }
634
+
635
+ if ([AST.ASTBlock.BlockType.If, AST.ASTBlock.BlockType.Elif, AST.ASTBlock.BlockType.Else].includes(this.curBlock.blockType) && this.curBlock.nodes.length == 0) {
636
+ this.curBlock.append(new AST.ASTKeyword(AST.ASTKeyword.Word.Continue));
637
+ return;
638
+ }
639
+
640
+ // Let's find actual end of block
641
+ let blockEnd = loopBlock.end;
642
+ let instr = this.code.PeekInstructionAtOffset(blockEnd);
643
+ if (!instr) {
644
+ return;
645
+ }
646
+ let currentIndex = instr.InstructionIndex;
647
+
648
+ while (blockEnd > loopBlock.start) {
649
+ if (instr.OpCodeID == this.OpCodes.JUMP_ABSOLUTE_A &&
650
+ (instr.JumpTarget == loopBlock.start + 3 ||
651
+ instr.JumpTarget < loopBlock.start)
652
+ ) {
653
+ currentIndex--;
654
+ instr = this.code.PeekInstructionAt(currentIndex);
655
+ blockEnd = instr.Offset;
656
+ } else {
657
+ return;
658
+ }
659
+ }
660
+
661
+ if (this.code.Current.Offset < blockEnd) {
662
+ this.curBlock.append(new AST.ASTKeyword(AST.ASTKeyword.Word.Continue));
663
+ }
664
+ }
665
+
666
+ /* We're in a loop, this jumps back to the start */
667
+ /* I think we'll just ignore this case... */
668
+ return; // Bad idea? Probably!
669
+ }
670
+
671
+ if (this.curBlock.blockType == AST.ASTBlock.BlockType.Container) {
672
+ let cont = this.curBlock;
673
+ // EXPERIMENT
674
+ if (cont.hasExcept && this.code.Next?.Offset <= cont.except) {
675
+ let except = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Except, this.code.Current.Offset, this.code.Current.JumpTarget, null, false);
676
+ except.init();
677
+ this.blocks.push(except);
678
+ this.curBlock = this.blocks.top();
679
+ }
680
+ return;
681
+ }
682
+
683
+ let prev = this.curBlock;
684
+
685
+ if (this.blocks.length > 1) {
686
+ do {
687
+ this.blocks.pop();
688
+ this.blocks.top().append(prev);
689
+
690
+ if ([
691
+ AST.ASTBlock.BlockType.If,
692
+ AST.ASTBlock.BlockType.Elif
693
+ ].includes(prev.blockType)) {
694
+ let top = this.blocks.top();
695
+ let next = new AST.ASTBlock(AST.ASTBlock.BlockType.Else, this.code.Current.Offset, top.end);
696
+ top.end = this.code.Current.Offset;
697
+ if (prev.inited == AST.ASTCondBlock.InitCondition.PrePopped) {
698
+ next.init(AST.ASTCondBlock.InitCondition.PrePopped);
699
+ }
700
+
701
+ if (global.g_cliArgs?.debug) {
702
+ console.log(`ELSE BLOCK CREATED at offset ${this.code.Current.Offset}, end=${next.end}`);
703
+ }
704
+
705
+ this.blocks.push(next);
706
+ prev = null;
707
+ } else if (prev.blockType == AST.ASTBlock.BlockType.Except) {
708
+ let top = this.blocks.top();
709
+ let next = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Except, top.start, top.end, null, false);
710
+ next.init();
711
+
712
+ this.blocks.push(next);
713
+ prev = null;
714
+ } else if (prev.blockType == AST.ASTBlock.BlockType.Else) {
715
+ /* Special case */
716
+ if (this.blocks.top().blockType != AST.ASTBlock.BlockType.Main) {
717
+ prev = this.blocks.top();
718
+ } else {
719
+ prev = null;
720
+ }
721
+ } else {
722
+ prev = null;
723
+ }
724
+
725
+ } while (prev != null);
726
+ }
727
+
728
+ this.curBlock = this.blocks.top();
729
+ }
730
+
731
+ function handleJumpForwardA() {
732
+ processJumpForward.call(this);
733
+ }
734
+
735
+ function handleInstrumentedJumpForwardA() {
736
+ processJumpForward.call(this);
737
+ }
738
+
739
+ function handleJumpA() {
740
+ // Python 3.13+ JUMP: treat as forward jump
741
+ processJumpForward.call(this);
742
+ }
743
+
744
+ function handleJumpNoInterruptA() {
745
+ // Python 3.13+ JUMP_NO_INTERRUPT: same as JUMP for decompilation
746
+ processJumpForward.call(this);
747
+ }
748
+
749
+ function processJumpForward() {
750
+ if (this.skipNextJump) {
751
+ this.skipNextJump = false;
752
+ return;
753
+ }
754
+
755
+ // Capture true-branch value for conditional expression (ternary) rewrites.
756
+ if (captureTrueBranchForConditional(this)) {
757
+ return;
758
+ }
759
+
760
+ let offs = this.code.Current.Argument;
761
+ if (this.object.Reader.versionCompare(3, 10) >= 0)
762
+ offs *= 2; // 2 bytes per offset as per BPO-27129
763
+
764
+ if (this.curBlock.blockType == AST.ASTBlock.BlockType.Container) {
765
+ let cont = this.curBlock;
766
+ if (cont.hasExcept) {
767
+ this.curBlock.end = this.code.Next?.Offset + offs;
768
+ let except = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Except, this.code.Current.Offset, this.curBlock.end, null, false);
769
+ except.init();
770
+ this.blocks.push(except);
771
+ this.curBlock = this.blocks.top();
772
+ }
773
+ return;
774
+ }
775
+
776
+ let prev = this.curBlock;
777
+
778
+ if (this.blocks.length > 1) {
779
+ do {
780
+ this.blocks.pop();
781
+
782
+ if (!this.blocks.empty())
783
+ this.blocks.top().append(prev);
784
+
785
+ if (prev.blockType == AST.ASTBlock.BlockType.If
786
+ || prev.blockType == AST.ASTBlock.BlockType.Elif) {
787
+ if (offs < 3) {
788
+ prev = null;
789
+ continue;
790
+ }
791
+ let next = new AST.ASTBlock(AST.ASTBlock.BlockType.Else, this.code.Current.Offset, this.code.Next?.Offset + offs);
792
+ if (prev.inited == AST.ASTCondBlock.InitCondition.PrePopped) {
793
+ next.init(AST.ASTCondBlock.InitCondition.PrePopped);
794
+ }
795
+
796
+ this.blocks.push(next);
797
+ prev = null;
798
+ } else if (prev.blockType == AST.ASTBlock.BlockType.Except && offs > 2) {
799
+ // For exception groups: only create chained except if there's another CHECK_EG_MATCH ahead.
800
+ // Internal cleanup jumps should not create new except blocks.
801
+ if (this.inExceptionGroup) {
802
+ const jumpTarget = this.code.Next?.Offset + offs;
803
+ let hasNextEgMatch = false;
804
+ // Scan ahead to see if there's a CHECK_EG_MATCH within reasonable range
805
+ let scanOffset = this.code.Next?.Offset;
806
+ for (let i = 0; i < 40 && scanOffset <= jumpTarget + 20; i++) {
807
+ const instr = this.code.PeekInstructionAtOffset(scanOffset);
808
+ if (!instr) break;
809
+ if (instr.OpCodeID === this.OpCodes.CHECK_EG_MATCH) {
810
+ hasNextEgMatch = true;
811
+ break;
812
+ }
813
+ if (instr.OpCodeID === this.OpCodes.CALL_INTRINSIC_2 ||
814
+ instr.OpCodeID === this.OpCodes.POP_EXCEPT ||
815
+ instr.OpCodeID === this.OpCodes.RERAISE) {
816
+ // Reached cleanup, no more handlers
817
+ break;
818
+ }
819
+ scanOffset += 2;
820
+ }
821
+ if (!hasNextEgMatch) {
822
+ prev = null;
823
+ break;
824
+ }
825
+ }
826
+ let next = null;
827
+
828
+ if (this.code.Next?.OpCodeID == this.OpCodes.END_FINALLY) {
829
+ next = new AST.ASTBlock(AST.ASTBlock.BlockType.Else, this.code.Current.Offset, this.code.Current.JumpTarget);
830
+ next.init();
831
+ } else {
832
+ next = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Except, this.code.Current.Offset, this.code.Next?.Offset + offs, null, false);
833
+ next.init();
834
+ }
835
+
836
+ this.blocks.push(next);
837
+ prev = null;
838
+ } else if (prev.blockType == AST.ASTBlock.BlockType.Else) {
839
+ /* Special case */
840
+ prev = this.blocks.top();
841
+
842
+ if (prev.blockType == AST.ASTBlock.BlockType.Main) {
843
+ /* Something went out of the control! */
844
+ prev = null;
845
+ }
846
+ } else if (prev.blockType == AST.ASTBlock.BlockType.Try
847
+ && prev.end < this.code.Next?.Offset + offs) {
848
+ this.dataStack.pop();
849
+
850
+ if (this.blocks.top().blockType == AST.ASTBlock.BlockType.Container) {
851
+ let cont = this.blocks.top();
852
+ if (cont.hasExcept) {
853
+
854
+ let except = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Except, prev.end, this.code.Next?.Offset + offs, null, false);
855
+ except.init();
856
+ this.blocks.push(except);
857
+ }
858
+ } else {
859
+ if (global.g_cliArgs?.debug) {
860
+ console.error("Something TERRIBLE happened!!\n");
861
+ }
862
+ }
863
+ prev = null;
864
+ } else {
865
+ prev = null;
866
+ }
867
+
868
+ } while (prev != null);
869
+ }
870
+
871
+ this.curBlock = this.blocks.top();
872
+
873
+ if (this.curBlock.blockType == AST.ASTBlock.BlockType.Except) {
874
+ this.curBlock.end = this.code.Next?.Offset + offs;
875
+ }
876
+ }
877
+
878
+ function handleJumpBackwardA() {
879
+ // Python 3.11+ JUMP_BACKWARD opcode
880
+ // Unconditional backward jump (used in loops)
881
+ // Similar to JUMP_ABSOLUTE but with relative negative offset
882
+
883
+ if (global.g_cliArgs?.debug) {
884
+ console.log(`[JUMP_BACKWARD] at offset ${this.code.Current.Offset}, target_delta=${this.code.Current.Argument}`);
885
+ }
886
+
887
+ // For loops (for/while/async for), this jumps back to loop start
888
+ // In decompiler context, we don't need to generate explicit "continue" or "goto"
889
+ // The loop structure is already represented by For/While/AsyncFor blocks
890
+
891
+ // No action needed - loop control is implicit in block structure
892
+ }
893
+
894
+ function handleJumpBackwardNoInterruptA() {
895
+ // Python 3.11+ JUMP_BACKWARD_NO_INTERRUPT
896
+ // Like JUMP_BACKWARD but doesn't check for pending signals
897
+ // Used in tight loops for optimization
898
+
899
+ if (global.g_cliArgs?.debug) {
900
+ console.log(`[JUMP_BACKWARD_NO_INTERRUPT] at offset ${this.code.Current.Offset}`);
901
+ }
902
+
903
+ // Same as JUMP_BACKWARD for decompiler purposes
904
+ }
905
+
906
+ function handleJumpIfNotExcMatchA() {
907
+ // Use same logic as other conditional jumps; stack top is comparison result.
908
+ processJumpOps.call(this);
909
+ }
910
+
911
+ function handleNotTaken() {
912
+ // Instrumentation hint (3.13+) used in instrumented builds; ignore for decompilation.
913
+ if (global.g_cliArgs?.debug) {
914
+ console.log(`[NOT_TAKEN] at offset ${this.code.Current.Offset}`);
915
+ }
916
+ }
917
+
918
+ function handleInstrumentedNotTakenA() {
919
+ // Instrumentation marker for untaken branch; no stack effect.
920
+ if (global.g_cliArgs?.debug) {
921
+ console.log(`[INSTRUMENTED_NOT_TAKEN] at offset ${this.code.Current.Offset}`);
922
+ }
923
+ }
924
+
925
+ module.exports = {
926
+ handleJumpIfFalseA,
927
+ handleJumpIfTrueA,
928
+ handleJumpIfFalseOrPopA,
929
+ handleJumpIfTrueOrPopA,
930
+ handlePopJumpIfFalseA,
931
+ handlePopJumpIfTrueA,
932
+ handlePopJumpForwardIfFalseA,
933
+ handlePopJumpForwardIfTrueA,
934
+ handlePopJumpForwardIfNoneA,
935
+ handlePopJumpForwardIfNotNoneA,
936
+ handlePopJumpBackwardIfNoneA,
937
+ handlePopJumpBackwardIfNotNoneA,
938
+ handlePopJumpIfNoneA,
939
+ handlePopJumpIfNotNoneA,
940
+ handleInstrumentedPopJumpIfFalseA,
941
+ handleInstrumentedPopJumpIfTrueA,
942
+ handleInstrumentedPopJumpIfNoneA,
943
+ handleInstrumentedPopJumpIfNotNoneA,
944
+ handleJumpAbsoluteA,
945
+ handleJumpForwardA,
946
+ handleInstrumentedJumpForwardA,
947
+ handleJumpA,
948
+ handleJumpNoInterruptA,
949
+ handleJumpBackwardA,
950
+ handleJumpBackwardNoInterruptA,
951
+ handleJumpIfNotExcMatchA,
952
+ handleNotTaken,
953
+ handleInstrumentedNotTakenA
954
+ };