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,318 @@
1
+ const AST = require('../ast/ast_node');
2
+
3
+ function handleBreakLoop() {
4
+ let keywordNode = new AST.ASTKeyword(AST.ASTKeyword.Word.Break);
5
+ keywordNode.line = this.code.Current.LineNo;
6
+ this.curBlock.append(keywordNode);
7
+
8
+ // Mark code as unreachable until the end of CURRENT BLOCK (not entire loop)
9
+ // This allows alternative branches (elif/else) to be processed correctly
10
+ //
11
+ // WRONG approach: unreachableUntil = loopBlock.end (too broad, skips elif!)
12
+ // RIGHT approach: unreachableUntil = curBlock.end (only this branch)
13
+ if (this.curBlock.end > this.code.Current.Offset) {
14
+ this.unreachableUntil = this.curBlock.end;
15
+
16
+ if (global.g_cliArgs?.debug) {
17
+ console.log(`BREAK at offset ${this.code.Current.Offset}: curBlock=${this.curBlock.type_str} (${this.curBlock.start}-${this.curBlock.end})`);
18
+ console.log(` Block stack: ${this.blocks.map((b,i) => `[${i}]${b.type_str}(${b.start}-${b.end})`).join(', ')}`);
19
+ console.log(` Marking unreachable until ${this.unreachableUntil}`);
20
+ }
21
+ }
22
+ }
23
+
24
+ function handleSetupLoopA() {
25
+ if (global.g_cliArgs?.debug) {
26
+ console.log(`[handleSetupLoopA] Creating while at offset ${this.code.Current.Offset}, JumpTarget=${this.code.Current.JumpTarget}`);
27
+ console.log(` Data stack size: ${this.dataStack.length}`);
28
+ if (this.dataStack.length > 0) {
29
+ let top = this.dataStack.top();
30
+ console.log(` Stack top: ${top?.constructor?.name} = ${top?.codeFragment ? top.codeFragment() : top}`);
31
+ }
32
+ }
33
+
34
+ let nextBlock = new AST.ASTCondBlock(AST.ASTBlock.BlockType.While, this.code.Current.Offset, this.code.Current.JumpTarget, null, false);
35
+ nextBlock.line = this.code.Current.LineNo;
36
+ this.blocks.push(nextBlock);
37
+ this.curBlock = this.blocks.top();
38
+ }
39
+
40
+ function handleContinueLoopA() {
41
+ let node = new AST.ASTKeyword (AST.ASTKeyword.Word.Continue);
42
+ node.line = this.code.Current.LineNo;
43
+ this.curBlock.nodes.push(node);
44
+ }
45
+
46
+ function handleForIterA() {
47
+ handleInstrumentedForIterA.call(this);
48
+ }
49
+
50
+ function handleInstrumentedForIterA() {
51
+ let iter = this.dataStack.pop(); // Iterable
52
+ /* Pop it? Don't pop it? */
53
+
54
+ let start = this.code.Current.Offset;
55
+ let end = 0;
56
+ let line = this.code.Current.LineNo;
57
+ let comprehension = false;
58
+ const specialized = this.object.Reader.versionCompare(3, 13) >= 0;
59
+ const sentinel = specialized ? new AST.ASTNone() : null;
60
+
61
+ // before 3.8, there is a SETUP_LOOP instruction with block start and end position,
62
+ // the this.code.Current.Argument is usually a jump to a POP_BLOCK instruction
63
+ // after 3.8, block extent has to be inferred implicitly; the this.code.Current.Argument is a jump to a position after the for block
64
+ if (this.object.Reader.versionCompare(3, 8) >= 0) {
65
+ end = this.code.Current.Argument;
66
+ if (this.object.Reader.versionCompare(3, 10) >= 0)
67
+ end *= 2; // // BPO-27129
68
+ end += this.code.Next?.Offset;
69
+ [end] = this.code.FindEndOfBlock(end);
70
+ const objName = this.object?.Name;
71
+ comprehension = this.code.Current.Name == "<listcomp>" ||
72
+ objName == "<listcomp>" ||
73
+ objName == "<setcomp>" ||
74
+ objName == "<dictcomp>";
75
+ if (!comprehension) {
76
+ const container = this.dataStack.top();
77
+ if ((container instanceof AST.ASTList || container instanceof AST.ASTSet || container instanceof AST.ASTMap) &&
78
+ (container.values?.length === 0)) {
79
+ comprehension = true;
80
+ }
81
+ }
82
+ } else {
83
+ if ((this.dataStack.top() instanceof AST.ASTSet ||
84
+ this.dataStack.top() instanceof AST.ASTList ||
85
+ this.dataStack.top() instanceof AST.ASTMap)
86
+ && this.dataStack.top().values.length == 0) {
87
+ end = this.code.Current.JumpTarget;
88
+ comprehension = true;
89
+ } else {
90
+ let top = this.blocks.top();
91
+ start = top.start;
92
+ end = top.end; // block end position from SETUP_LOOP
93
+ line = top.line;
94
+
95
+ if (top.blockType == AST.ASTBlock.BlockType.While) {
96
+ this.blocks.pop();
97
+ } else {
98
+ comprehension = true;
99
+ }
100
+ }
101
+ }
102
+
103
+ let forblk = new AST.ASTIterBlock(AST.ASTBlock.BlockType.For, start, end, iter);
104
+ forblk.line = line;
105
+ forblk.comprehension = comprehension;
106
+
107
+ if (global.g_cliArgs?.debug) {
108
+ console.log(`[FOR_ITER] Created for block: start=${start}, end=${end}, comprehension=${comprehension}`);
109
+ }
110
+
111
+ this.blocks.push(forblk);
112
+ this.curBlock = this.blocks.top();
113
+
114
+ // 3.13+ FOR_ITER pushes a sentinel used by END_FOR; keep stack balanced.
115
+ this.dataStack.push(sentinel);
116
+ }
117
+
118
+ function handleForLoopA() {
119
+ let curidx = this.dataStack.pop(); // Current index
120
+ let iter = this.dataStack.pop(); // Iterable
121
+
122
+ let comprehension = false;
123
+ let top = this.blocks.top();
124
+
125
+ if (top.blockType == AST.ASTBlock.BlockType.While) {
126
+ this.blocks.pop();
127
+ } else {
128
+ comprehension = true;
129
+ }
130
+
131
+ let forblk = new AST.ASTIterBlock(AST.ASTBlock.BlockType.For, this.code.Current.Offset, top.end, iter);
132
+ forblk.line = this.code.Current.LineNo;
133
+ forblk.comprehension = comprehension;
134
+ this.blocks.push(forblk);
135
+ this.curBlock = this.blocks.top();
136
+
137
+ this.dataStack.push(iter);
138
+ this.dataStack.push(curidx);
139
+ this.dataStack.push(null);
140
+ }
141
+
142
+ function handleGetAiter() {
143
+ // Logic similar to FOR_ITER_A
144
+ let iter = this.dataStack.pop(); // Iterable
145
+
146
+ let start = this.code.Current.Offset;
147
+ let end = 0;
148
+ let line = this.code.Current.LineNo;
149
+ let comprehension = false;
150
+
151
+ let top = this.blocks.top();
152
+
153
+ // Python 3.8+ removed SETUP_LOOP, so we need to infer the block end
154
+ if (this.object.Reader.versionCompare(3, 8) >= 0 || top.blockType != AST.ASTBlock.BlockType.While) {
155
+ // Python 3.11+: Check exception table first for END_ASYNC_FOR location
156
+ if (this.object.Reader.versionCompare(3, 11) >= 0 && this.object.ExceptionTable && this.object.ExceptionTable.length > 0) {
157
+ if (global.g_cliArgs?.debug) {
158
+ console.log(`[GET_AITER] Python 3.11+ with exception table (${this.object.ExceptionTable.length} entries)`);
159
+ this.object.ExceptionTable.forEach((entry, i) => {
160
+ console.log(` Entry ${i}: start=${entry.start}, end=${entry.end}, target=${entry.target}, depth=${entry.depth}`);
161
+ });
162
+ }
163
+
164
+ // Find exception handler that covers the async for loop
165
+ // The handler's target offset should point to END_ASYNC_FOR
166
+ // Note: exception table start may be GET_ANEXT (after GET_AITER)
167
+ for (const entry of this.object.ExceptionTable) {
168
+ // Check if this entry covers range starting at/near GET_AITER
169
+ // GET_AITER is at 'start', exception table may start at next instruction
170
+ if (start >= entry.start - 4 && start <= entry.start + 4) {
171
+ // The target is the exception handler (END_ASYNC_FOR)
172
+ end = entry.target + 2; // END_ASYNC_FOR is 2 bytes, end is after it
173
+
174
+ if (global.g_cliArgs?.debug) {
175
+ console.log(` ✓ Found END_ASYNC_FOR via exception table: target=${entry.target}, end=${end}`);
176
+ }
177
+ break;
178
+ }
179
+ }
180
+ }
181
+
182
+ // If not found via exception table, fall back to search
183
+ if (end == 0) {
184
+ // Find END_ASYNC_FOR to determine block end
185
+ // Scan forward by offset (not by instruction index)
186
+ let searchOffset = this.code.Next.Offset;
187
+ let searchLimit = 200;
188
+ let searchCount = 0;
189
+
190
+ if (global.g_cliArgs?.debug) {
191
+ console.log(`[GET_AITER] Fallback: searching for END_ASYNC_FOR from offset ${searchOffset}`);
192
+ console.log(` Last offset: ${this.code.LastOffset}, instructions: ${this.code.Instructions.length}`);
193
+ }
194
+
195
+ while (searchCount < searchLimit) {
196
+ let instr = this.code.PeekInstructionAtOffset(searchOffset);
197
+ if (!instr) {
198
+ if (global.g_cliArgs?.debug) {
199
+ console.log(` No instruction at offset ${searchOffset}, stopping search (searched ${searchCount} bytes)`);
200
+ }
201
+ break;
202
+ }
203
+
204
+ if (global.g_cliArgs?.debug && searchCount < 30) {
205
+ console.log(` [${searchCount}] offset=${instr.Offset}, opcode=${instr.InstructionName}, id=${instr.OpCodeID}, size=${instr.Size}`);
206
+ }
207
+
208
+ if (instr.OpCodeID == this.OpCodes.END_ASYNC_FOR) {
209
+ let instrSize = instr.Size || 2;
210
+ end = instr.Offset + instrSize;
211
+ if (global.g_cliArgs?.debug) {
212
+ console.log(` ✓ Found END_ASYNC_FOR at offset ${instr.Offset}, size=${instrSize}, end=${end}`);
213
+ }
214
+ break;
215
+ }
216
+
217
+ // Python 3.6+ uses 2-byte (word-aligned) instructions
218
+ let instrSize = instr.Size || 2;
219
+ searchOffset += instrSize;
220
+ searchCount += instrSize;
221
+
222
+ if (global.g_cliArgs?.debug && searchCount < 10) {
223
+ console.log(` Next search: offset=${searchOffset}, count=${searchCount}`);
224
+ }
225
+ }
226
+
227
+ if (end == 0) {
228
+ if (global.g_cliArgs?.debug) {
229
+ console.error(`Could not find END_ASYNC_FOR for GET_AITER at offset ${start}`);
230
+ }
231
+ end = this.code.LastOffset + 2; // Conservative fallback: run to end of code
232
+ }
233
+ }
234
+
235
+ // Check if this is an async comprehension (generator expression)
236
+ comprehension = this.code.Current.Name &&
237
+ (this.code.Current.Name.includes("comp>") ||
238
+ this.code.Current.Name.includes("genexpr>"));
239
+
240
+ let forblk = new AST.ASTIterBlock(AST.ASTBlock.BlockType.AsyncFor, start, end, iter);
241
+ forblk.line = line;
242
+ forblk.comprehension = comprehension;
243
+ this.blocks.push(forblk);
244
+ this.curBlock = this.blocks.top();
245
+ this.dataStack.push(null);
246
+ } else if (top.blockType == AST.ASTBlock.BlockType.While) {
247
+ // Python 3.7 and earlier with SETUP_LOOP: SETUP_LOOP creates While block first
248
+ if (global.g_cliArgs?.debug) {
249
+ console.log(`[GET_AITER] Converting While to AsyncFor at offset ${start}, end=${top.end}`);
250
+ }
251
+ this.blocks.pop();
252
+ let forblk = new AST.ASTIterBlock(AST.ASTBlock.BlockType.AsyncFor, top.start, top.end, iter);
253
+ forblk.line = line;
254
+ this.blocks.push(forblk);
255
+ this.curBlock = this.blocks.top();
256
+ this.dataStack.push(null);
257
+ } else {
258
+ console.error(`Unexpected block type for GET_AITER: ${top.type_str} at offset ${start}\n`);
259
+ }
260
+ }
261
+
262
+ function handleGetAnext() {
263
+ let iter = this.dataStack.top();
264
+ let callNode = new AST.ASTCall(new AST.ASTName('await'), [new AST.ASTBinary(iter, new AST.ASTName('__anext__'), AST.ASTBinary.BinOp.Attr)], []);
265
+ callNode.line = this.code.Current.LineNo;
266
+ this.dataStack.push(callNode);
267
+ }
268
+
269
+ function handleGetIter() {
270
+ handleGetYieldFromIter.call(this);
271
+ }
272
+
273
+ function handleGetYieldFromIter() {
274
+ /* We just entirely ignore this */
275
+ if (this.code.Next?.OpCodeID == this.OpCodes.CALL_FUNCTION_A) {
276
+ this.dataStack.push(new AST.ASTIteratorValue(this.dataStack.pop()));
277
+ }
278
+ }
279
+
280
+ function handleEndFor() {
281
+ // Python 3.13+ END_FOR opcode
282
+ // Cleans up after for loop iteration
283
+ // In Python 3.13, FOR_ITER pushes sentinel, END_FOR pops it
284
+ if (global.g_cliArgs?.debug) {
285
+ console.log(`[END_FOR] at offset ${this.code.Current.Offset}`);
286
+ }
287
+ if (this.object.Reader.versionCompare(3, 13) >= 0 && this.dataStack.length > 0) {
288
+ this.dataStack.pop();
289
+ }
290
+ }
291
+
292
+ function handleInstrumentedEndForA() {
293
+ // Instrumented variant mirrors END_FOR behavior.
294
+ handleEndFor.call(this);
295
+ }
296
+
297
+ function handleInstrumentedPopIterA() {
298
+ // Instrumentation helper; no stack effect for decompilation.
299
+ if (global.g_cliArgs?.debug) {
300
+ console.log(`[INSTRUMENTED_POP_ITER] at offset ${this.code.Current.Offset}`);
301
+ }
302
+ }
303
+
304
+ module.exports = {
305
+ handleBreakLoop,
306
+ handleContinueLoopA,
307
+ handleEndFor,
308
+ handleInstrumentedEndForA,
309
+ handleInstrumentedPopIterA,
310
+ handleForIterA,
311
+ handleInstrumentedForIterA,
312
+ handleForLoopA,
313
+ handleGetAiter,
314
+ handleGetAnext,
315
+ handleGetIter,
316
+ handleGetYieldFromIter,
317
+ handleSetupLoopA,
318
+ };