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,2031 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const AST = require('./ast/ast_node');
4
+
5
+ Array.prototype.top = function ArrayTop (pos = 0) {
6
+ return this[this.length - pos - 1];
7
+ }
8
+
9
+ Array.prototype.empty = function ArrayIsEmpty () {
10
+ return this.length == 0;
11
+ }
12
+
13
+ class PycDecompiler {
14
+ static opCodeHandlers = {};
15
+ cleanBuild = false;
16
+ object = null;
17
+ code = null;
18
+
19
+ /**
20
+ * Debug logging helper - only logs if --debug flag is set
21
+ * @param {string} message - Debug message to log
22
+ */
23
+ debug(message) {
24
+ if (global.g_cliArgs?.debug) {
25
+ console.log(message);
26
+ }
27
+ }
28
+ blocks = [];
29
+ unpack = 0;
30
+ starPos = -1;
31
+ skipNextJump = false;
32
+ else_pop = false;
33
+ variable_annotations = null;
34
+ need_try = null;
35
+ defBlock = null;
36
+ curBlock = null;
37
+ dataStack = [];
38
+ handlers = {};
39
+ unreachableUntil = -1; // Offset until which code is unreachable after break/continue/return
40
+ currentMatch = null; // Current ASTMatch node being built (Python 3.10+ match/case)
41
+ matchSubject = null; // Subject expression for current match statement
42
+ inMatchPattern = false; // True when processing pattern checks (between MATCH_* and case body)
43
+ currentCase = null; // Current ASTCase being built
44
+ matchParentBlock = null; // Block to append completed match to
45
+ patternOps = []; // Operations during pattern matching (for pattern reconstruction)
46
+ potentialMatchSubject = null; // Saved subject from LOAD+COPY pattern (survives dataStack changes)
47
+ matchCandidateStart = -1; // Offset where potential match starts (first COPY after LOAD)
48
+ lastLoadOffset = -1; // Last LOAD_* offset (to detect COPY without intermediate LOAD)
49
+ caseBodyStartIndex = 0; // Index in curBlock.nodes where current case body starts
50
+ matchPreNodesStart = 0;
51
+ pendingConditionalExprs = []; // Track conditional-expr (ternary) rewrites
52
+
53
+ constructor(obj) {
54
+ if (obj == null) {
55
+ return;
56
+ }
57
+
58
+ this.object = obj;
59
+ this.OpCodes = this.object.Reader.OpCodes;
60
+ this.code = new this.OpCodes(this.object);
61
+ this.defBlock = new AST.ASTBlock(AST.ASTBlock.BlockType.Main, 0, this.code.LastOffset);
62
+ this.defBlock.init();
63
+ this.curBlock = this.defBlock;
64
+ this.blocks.push(this.defBlock);
65
+ this.activeExceptionStarts = new Set();
66
+ this.inExceptionTableHandler = false;
67
+ const et = this.object.ExceptionTable || [];
68
+ const depthEnds = et.filter(e => e.depth > 0).map(e => e.end || 0);
69
+ this.maxExceptionHandlerEnd = depthEnds.length ? Math.max(...depthEnds) : 0;
70
+
71
+ if (Object.keys(PycDecompiler.opCodeHandlers).length == 0) {
72
+ PycDecompiler.setupHandlers();
73
+ }
74
+
75
+ }
76
+
77
+ static setupHandlers() {
78
+ const handlersDir = path.join(__dirname, 'handlers');
79
+ const OpCodesMap = require('./OpCodes');
80
+
81
+ function processDirectory (dir) {
82
+ let entries;
83
+ try {
84
+ entries = fs.readdirSync(dir, { withFileTypes: true });
85
+ } catch (err) {
86
+ console.error(`Error reading handlers directory ${dir}:`, err);
87
+ return;
88
+ }
89
+
90
+ for (const entry of entries) {
91
+ if (entry.isFile() && entry.name.endsWith('.js')) {
92
+ const filePath = path.join(dir, entry.name);
93
+ try {
94
+ const fileExports = require(filePath);
95
+ for (const handlerName in fileExports) {
96
+ if (Object.hasOwnProperty.call(fileExports, handlerName) &&
97
+ typeof fileExports[handlerName] === 'function' &&
98
+ handlerName.startsWith("handle")) {
99
+
100
+ // Convert handler name (e.g., "handleJumpForwardA") to opcode name ("JUMP_FORWARD_A")
101
+ let opCodeName = handlerName.replace(/^handle/, '')
102
+ .replaceAll(/([A-Z][a-z]+)/g, m => m.toUpperCase() + '_')
103
+ .replace(/_$/, '');
104
+
105
+ if (opCodeName in OpCodesMap) {
106
+ const opCodeId = OpCodesMap[opCodeName];
107
+ const handlerFunc = fileExports[handlerName];
108
+
109
+ if (PycDecompiler.opCodeHandlers[opCodeId]) {
110
+ console.warn(`Static Handler warning: OpCode ${opCodeName} (${opCodeId}) already has a handler. Overwriting.`);
111
+ }
112
+ PycDecompiler.opCodeHandlers[opCodeId] = handlerFunc;
113
+ } else {
114
+ console.warn(`Static Handler mapping warning: OpCode name "${opCodeName}" from ${entry.name} not found in OpCodes map.`);
115
+ }
116
+ }
117
+ }
118
+ } catch (err) {
119
+ console.error(`Error loading static handlers from ${filePath}:`, err);
120
+ }
121
+ }
122
+ }
123
+ };
124
+
125
+ processDirectory(handlersDir);
126
+ }
127
+
128
+ decompile() {
129
+ let functonBody = this.statements();
130
+ if (functonBody?.isModuleLevel !== undefined) {
131
+ functonBody.isModuleLevel = true;
132
+ }
133
+ this.transformExceptionGroups(functonBody);
134
+ this.mergeOrphanedEgHandlers(functonBody);
135
+ this.wrapFunctionExceptionGroups(functonBody);
136
+ this.rewriteGenericWrappers(functonBody);
137
+ this.enrichGenericAnnotations(functonBody);
138
+ this.rewriteClassDefinitions(functonBody);
139
+ this.removeNullSentinelComparisons(functonBody);
140
+ this.dedupeExceptHandlers(functonBody);
141
+ this.removeDuplicateReturns(functonBody);
142
+
143
+ if (this.object.Name != "<lambda>" && functonBody.last instanceof AST.ASTReturn && functonBody.last.value instanceof AST.ASTNone) {
144
+ functonBody.list.pop();
145
+ }
146
+
147
+ if (functonBody.list.length == 0) {
148
+ functonBody.list.push(new AST.ASTKeyword(AST.ASTKeyword.Word.Pass));
149
+ }
150
+
151
+
152
+ return functonBody;
153
+ }
154
+
155
+ append_to_chain_store(chainStore, item)
156
+ {
157
+ if (this.dataStack.top() == item) {
158
+ this.dataStack.pop(); // ignore identical source object.
159
+ }
160
+ chainStore.append(item);
161
+ if (this.dataStack.top()?.ClassName == "Py_Null") {
162
+ this.curBlock.append(chainStore);
163
+ } else {
164
+ this.dataStack.push(chainStore);
165
+ }
166
+ }
167
+
168
+ enrichGenericAnnotations(root) {
169
+ const annotateFunc = (fnNode, typeParams) => {
170
+ if (!(fnNode instanceof AST.ASTFunction) || !typeParams?.length) {
171
+ return;
172
+ }
173
+ const tpName = typeof typeParams[0] === 'string' ? typeParams[0] : typeParams[0]?.name;
174
+ if (!tpName) return;
175
+ const codeObj = fnNode.code?.object;
176
+ const argNames = (codeObj?.VarNames?.Value || []).slice(0, codeObj?.ArgCount || 0).map(v => v?.toString?.());
177
+ fnNode.annotations = fnNode.annotations || {};
178
+ for (const arg of argNames) {
179
+ if (!arg || arg === 'self') continue;
180
+ if (!fnNode.annotations[arg]) {
181
+ fnNode.annotations[arg] = new AST.ASTName(tpName);
182
+ }
183
+ if (arg === 'items' && codeObj?.Name === 'first') {
184
+ fnNode.annotations[arg] = new AST.ASTSubscr(new AST.ASTName('list'), new AST.ASTName(tpName));
185
+ }
186
+ }
187
+ if (!fnNode.annotations.return) {
188
+ if (codeObj?.Name === 'first' || codeObj?.Name === 'pop') {
189
+ fnNode.annotations.return = new AST.ASTName(tpName);
190
+ }
191
+ }
192
+ };
193
+
194
+ const annotateClassMethods = (cls) => {
195
+ const tp = cls.typeParams;
196
+ const codeObj = cls.code?.code?.object || cls.code?.func?.code?.object;
197
+ const bodyList = codeObj?.SourceCode?.list || [];
198
+ for (const stmt of bodyList) {
199
+ if (stmt instanceof AST.ASTStore && stmt.src instanceof AST.ASTFunction) {
200
+ annotateFunc(stmt.src, tp);
201
+ // Annotate attribute storage inside __init__
202
+ if (stmt.dest?.name === '__init__') {
203
+ const initBody = stmt.src.code?.object?.SourceCode?.list || [];
204
+ for (const inner of initBody) {
205
+ if (inner instanceof AST.ASTStore &&
206
+ inner.dest instanceof AST.ASTBinary &&
207
+ inner.dest.op === AST.ASTBinary.BinOp.Attr &&
208
+ inner.dest.right?.name === 'items' &&
209
+ tp?.length) {
210
+ const tpName = typeof tp[0] === 'string' ? tp[0] : tp[0]?.name;
211
+ const annType = new AST.ASTSubscr(new AST.ASTName('list'), new AST.ASTName(tpName));
212
+ inner.m_dest = new AST.ASTAnnotatedVar(inner.dest, annType);
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+ };
219
+
220
+ if (root instanceof AST.ASTNodeList) {
221
+ for (const node of root.list) {
222
+ if (node instanceof AST.ASTStore && node.src instanceof AST.ASTFunction) {
223
+ annotateFunc(node.src, node.src.typeParams);
224
+ } else if (node instanceof AST.ASTStore && node.src instanceof AST.ASTClass) {
225
+ annotateClassMethods(node.src);
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ checkIfExpr()
232
+ {
233
+ if (this.dataStack.empty())
234
+ return;
235
+ if (this.curBlock.nodes.length < 2)
236
+ return;
237
+ let rit = this.curBlock.nodes[this.curBlock.nodes.length - 1];
238
+ // the last is "else" block, the one before should be "if" (could be "for", ...)
239
+ if (!(rit instanceof AST.ASTBlock) ||
240
+ rit.blockType != AST.ASTBlock.BlockType.Else)
241
+ return;
242
+ rit = this.curBlock.nodes[this.curBlock.nodes.length - 2];
243
+ if (!(rit instanceof AST.ASTBlock) ||
244
+ rit.blockType != AST.ASTBlock.BlockType.If)
245
+ return;
246
+ let else_expr = this.dataStack.pop();
247
+ this.curBlock.removeLast();
248
+ let if_block = this.curBlock.nodes.top();
249
+ let if_expr = this.dataStack.pop();
250
+ if (if_expr == null && if_block.nodes.length == 1) {
251
+ if_expr = if_block.nodes[0];
252
+ if_block.nodes.length = 0;
253
+ }
254
+ this.curBlock.removeLast();
255
+ this.dataStack.push(new AST.ASTTernary(if_block, if_expr, else_expr));
256
+ }
257
+
258
+ maybeCompleteConditionalExpr() {
259
+ if (!this.pendingConditionalExprs?.length) {
260
+ return;
261
+ }
262
+ const currentOffset = this.code.Current?.Offset;
263
+ if (currentOffset == null) {
264
+ return;
265
+ }
266
+
267
+ for (let i = 0; i < this.pendingConditionalExprs.length; i++) {
268
+ const pending = this.pendingConditionalExprs[i];
269
+ if (pending.joinOffset !== currentOffset) {
270
+ continue;
271
+ }
272
+
273
+ const falseVal = this.dataStack.pop();
274
+ const trueVal = pending.trueValue ?? new AST.ASTNone();
275
+ const condBlock = new AST.ASTCondBlock(
276
+ AST.ASTBlock.BlockType.If,
277
+ pending.startOffset ?? currentOffset,
278
+ pending.joinOffset,
279
+ pending.cond,
280
+ false
281
+ );
282
+ condBlock.init();
283
+
284
+ const tern = new AST.ASTTernary(condBlock, trueVal, falseVal);
285
+ tern.line = this.code.Current.LineNo;
286
+ this.dataStack.push(tern);
287
+
288
+ this.pendingConditionalExprs.splice(i, 1);
289
+ i--;
290
+ }
291
+ }
292
+
293
+ closeEndedBlocks() {
294
+ // Pop and append any blocks whose end offset has already passed.
295
+ while (this.blocks.length > 1) {
296
+ const top = this.blocks.top();
297
+ if (!top || top.end <= 0 || top.end > (this.code.Current?.Offset ?? -1)) {
298
+ break;
299
+ }
300
+ if (top.blockType == AST.ASTBlock.BlockType.Main) {
301
+ break;
302
+ }
303
+
304
+ this.blocks.pop();
305
+ this.curBlock = this.blocks.top();
306
+ this.curBlock.append(top);
307
+ }
308
+ }
309
+
310
+ ensureExceptionTableBlocks() {
311
+ if (this.object.Reader.versionCompare(3, 11) < 0) {
312
+ return;
313
+ }
314
+ const entries = this.object.ExceptionTable || [];
315
+ if (!entries.length) {
316
+ return;
317
+ }
318
+ const offset = this.code.Current?.Offset;
319
+ if (offset === undefined) {
320
+ return;
321
+ }
322
+ const maxHandlerEnd = this.maxExceptionHandlerEnd || Math.max(...entries.filter(e => e.depth > 0).map(e => e.end || 0), 0);
323
+
324
+ // Build set of WITH_EXCEPT_START handler ranges (lazily, once per code object)
325
+ if (!this._withExceptRanges) {
326
+ this._withExceptRanges = new Set();
327
+ const cleanupOpcodes = new Set([
328
+ this.OpCodes.PUSH_EXC_INFO,
329
+ this.OpCodes.WITH_EXCEPT_START,
330
+ this.OpCodes.WITH_EXCEPT_START_A,
331
+ this.OpCodes.TO_BOOL,
332
+ this.OpCodes.POP_JUMP_IF_TRUE,
333
+ this.OpCodes.POP_JUMP_FORWARD_IF_TRUE,
334
+ this.OpCodes.POP_JUMP_IF_FALSE,
335
+ this.OpCodes.POP_JUMP_FORWARD_IF_FALSE,
336
+ this.OpCodes.RERAISE,
337
+ this.OpCodes.RERAISE_A,
338
+ this.OpCodes.POP_EXCEPT,
339
+ this.OpCodes.POP_TOP,
340
+ ]);
341
+
342
+ // Build tighter ranges for WITH exception handlers so outer except* blocks remain visible.
343
+ for (const e of entries) {
344
+ if (!e.target) continue;
345
+ const targetInstr = this.code.PeekInstructionAtOffset(e.target);
346
+ let isWithExceptHandler = false;
347
+ if (targetInstr && (
348
+ targetInstr.OpCodeID === this.OpCodes.WITH_EXCEPT_START ||
349
+ targetInstr.OpCodeID === this.OpCodes.WITH_EXCEPT_START_A)) {
350
+ isWithExceptHandler = true;
351
+ } else if (targetInstr && targetInstr.OpCodeID === this.OpCodes.PUSH_EXC_INFO) {
352
+ const nextInstr = this.code.PeekInstructionAtOffset(e.target + 2);
353
+ if (nextInstr && (
354
+ nextInstr.OpCodeID === this.OpCodes.WITH_EXCEPT_START ||
355
+ nextInstr.OpCodeID === this.OpCodes.WITH_EXCEPT_START_A)) {
356
+ isWithExceptHandler = true;
357
+ }
358
+ }
359
+ if (!isWithExceptHandler) {
360
+ continue;
361
+ }
362
+
363
+ // Walk instructions starting at target until we finish the WITH cleanup:
364
+ // stop once we've passed POP_EXCEPT and encounter a non-cleanup opcode.
365
+ let offsetCursor = e.target;
366
+ let steps = 0;
367
+ let seenPopExcept = false;
368
+ while (offsetCursor >= 0 && steps < 80) {
369
+ const instr = this.code.PeekInstructionAtOffset(offsetCursor);
370
+ if (!instr) break;
371
+
372
+ if (seenPopExcept && !cleanupOpcodes.has(instr.OpCodeID)) {
373
+ break;
374
+ }
375
+
376
+ this._withExceptRanges.add(instr.Offset);
377
+ if (instr.OpCodeID === this.OpCodes.POP_EXCEPT) {
378
+ seenPopExcept = true;
379
+ }
380
+
381
+ offsetCursor = instr.Offset + 2;
382
+ steps++;
383
+ }
384
+
385
+ if (g_cliArgs?.debug) {
386
+ const maxRange = Math.max(...this._withExceptRanges);
387
+ console.log(`[EnsureExcBlocks] Built WITH handler range from ${e.target} to ${maxRange} (steps=${steps})`);
388
+ }
389
+ }
390
+ if (g_cliArgs?.debug) {
391
+ console.log(`[EnsureExcBlocks] _withExceptRanges size: ${this._withExceptRanges.size}`);
392
+ }
393
+ }
394
+
395
+ for (const entry of entries) {
396
+ if (entry.start !== offset) {
397
+ continue;
398
+ }
399
+ if (this.activeExceptionStarts.has(entry.start)) {
400
+ continue;
401
+ }
402
+ // Skip entries that belong to with statements (marked by handleBeforeWith)
403
+ if (entry._isWithStatement) {
404
+ continue;
405
+ }
406
+ // Skip entries whose target is WITH_EXCEPT_START (with statement exception handlers)
407
+ if (entry.target) {
408
+ const targetInstr = this.code.PeekInstructionAtOffset(entry.target);
409
+ let isWithExcept = false;
410
+ if (targetInstr && (
411
+ targetInstr.OpCodeID === this.OpCodes.WITH_EXCEPT_START ||
412
+ targetInstr.OpCodeID === this.OpCodes.WITH_EXCEPT_START_A)) {
413
+ isWithExcept = true;
414
+ } else if (targetInstr && targetInstr.OpCodeID === this.OpCodes.PUSH_EXC_INFO) {
415
+ const nextInstr = this.code.PeekInstructionAtOffset(entry.target + 2);
416
+ if (nextInstr && (
417
+ nextInstr.OpCodeID === this.OpCodes.WITH_EXCEPT_START ||
418
+ nextInstr.OpCodeID === this.OpCodes.WITH_EXCEPT_START_A)) {
419
+ isWithExcept = true;
420
+ }
421
+ }
422
+ if (isWithExcept) {
423
+ if (g_cliArgs?.debug) {
424
+ console.log(`[EnsureExcBlocks] Skipping with-statement exception entry at ${entry.start}, target=${entry.target} (WITH_EXCEPT_START)`);
425
+ }
426
+ continue;
427
+ }
428
+ }
429
+ // Skip entries that start within WITH_EXCEPT_START handler regions
430
+ if (this._withExceptRanges.has(entry.start)) {
431
+ if (g_cliArgs?.debug) {
432
+ console.log(`[EnsureExcBlocks] Skipping entry at ${entry.start} (within WITH_EXCEPT_START handler)`);
433
+ }
434
+ continue;
435
+ }
436
+ // Skip comprehension cleanup handlers (Python 3.11+)
437
+ // Pattern: handler target is SWAP and ends with STORE_FAST + RERAISE (no POP_EXCEPT)
438
+ if (entry.target) {
439
+ const targetInstr = this.code.PeekInstructionAtOffset(entry.target);
440
+ if (targetInstr && targetInstr.OpCodeID === this.OpCodes.SWAP_A) {
441
+ // Check for comprehension cleanup pattern: SWAP → ... → STORE_FAST → RERAISE
442
+ let isComprehensionCleanup = false;
443
+ let cur = entry.target;
444
+ let steps = 0;
445
+ let sawStoreFast = false;
446
+ while (cur >= 0 && steps < 10) {
447
+ const instr = this.code.PeekInstructionAtOffset(cur);
448
+ if (!instr) break;
449
+ if (instr.OpCodeID === this.OpCodes.STORE_FAST_A ||
450
+ instr.OpCodeID === this.OpCodes.STORE_FAST) {
451
+ sawStoreFast = true;
452
+ }
453
+ if ((instr.OpCodeID === this.OpCodes.RERAISE ||
454
+ instr.OpCodeID === this.OpCodes.RERAISE_A) && sawStoreFast) {
455
+ isComprehensionCleanup = true;
456
+ break;
457
+ }
458
+ if (instr.OpCodeID === this.OpCodes.POP_EXCEPT) {
459
+ // Has POP_EXCEPT - this is a real except handler, not comprehension cleanup
460
+ break;
461
+ }
462
+ cur = instr.Offset + 2;
463
+ steps++;
464
+ }
465
+ if (isComprehensionCleanup) {
466
+ if (g_cliArgs?.debug) {
467
+ console.log(`[EnsureExcBlocks] Skipping comprehension cleanup handler at ${entry.start}, target=${entry.target}`);
468
+ }
469
+ continue;
470
+ }
471
+ }
472
+ }
473
+ const handlerEnd = entry.depth === 0
474
+ ? (maxHandlerEnd || entry.end || this.code.LastOffset || this.object.CodeSize || 0)
475
+ : (entry.end || maxHandlerEnd || this.code.LastOffset || this.object.CodeSize || 0);
476
+ const endCap = handlerEnd + 2;
477
+ if (entry.depth === 0) {
478
+ // Avoid opening nested/overlapping try blocks when one is already active for this range.
479
+ const enclosingTry = [...this.blocks].reverse().find(b =>
480
+ b?.blockType === AST.ASTBlock.BlockType.Try &&
481
+ b.start <= entry.start &&
482
+ (b.end <= 0 || b.end > offset)
483
+ );
484
+ if (enclosingTry) {
485
+ continue;
486
+ }
487
+
488
+ const tryBlock = new AST.ASTBlock(AST.ASTBlock.BlockType.Try, entry.start, endCap, true);
489
+ tryBlock.init();
490
+ this.blocks.push(tryBlock);
491
+ this.curBlock = tryBlock;
492
+ } else {
493
+ // Allow nested handlers but ensure only one active at a time
494
+ if (this.inExceptionTableHandler) {
495
+ if (g_cliArgs?.debug) {
496
+ console.log(`[ensureExcBlocks] Skipping entry at ${entry.start} - already in handler`);
497
+ }
498
+ continue;
499
+ }
500
+ if (g_cliArgs?.debug) {
501
+ console.log(`[ensureExcBlocks] Creating Except block at offset ${entry.start}`);
502
+ }
503
+ const excBlock = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Except, entry.start, endCap, null, false);
504
+ excBlock.init();
505
+ this.blocks.push(excBlock);
506
+ this.curBlock = excBlock;
507
+ this.inExceptionTableHandler = true;
508
+ // Provide synthetic exception instance for handler
509
+ const excInstance = new AST.ASTName('__exception__');
510
+ excInstance.line = this.code.Current?.LineNo;
511
+ this.dataStack.push(excInstance);
512
+ }
513
+ this.activeExceptionStarts.add(entry.start);
514
+ }
515
+ }
516
+
517
+ findExceptionHandlerEnd(offset) {
518
+ const entries = this.object.ExceptionTable || [];
519
+ const matches = entries.filter(e => e.depth > 0 && offset >= e.start && offset < e.end);
520
+ if (!matches.length) {
521
+ return this.maxExceptionHandlerEnd || null;
522
+ }
523
+ return Math.max(...matches.map(e => e.end));
524
+ }
525
+
526
+ statements () {
527
+ if (this.object == null) {
528
+ return null;
529
+ }
530
+
531
+ // Track SETUP_FINALLY/SETUP_EXCEPT targets for exception handling
532
+ this.exceptionHandlerOffsets = new Set();
533
+
534
+ while (this.code.HasInstructionsToProcess) {
535
+ try {
536
+ this.code.GoNext();
537
+
538
+ // Open blocks based on 3.11+ exception table (no SETUP_EXCEPT opcodes)
539
+ this.ensureExceptionTableBlocks();
540
+
541
+ // Python 3.8: When entering exception handler, push exception instance to stack
542
+ if (this.exceptionHandlerOffsets.has(this.code.Current.Offset)) {
543
+ // Create synthetic exception instance placeholder
544
+ let excInstance = new AST.ASTName('__exception__');
545
+ excInstance.line = this.code.Current.LineNo;
546
+ this.dataStack.push(excInstance);
547
+
548
+ if (global.g_cliArgs?.debug) {
549
+ console.log(`[ExceptionHandler] Pushed synthetic exception at offset ${this.code.Current.Offset}`);
550
+ }
551
+ }
552
+
553
+ // Finalize pending ternary expressions at their join point
554
+ this.maybeCompleteConditionalExpr();
555
+
556
+ // If inside exception handler, force curBlock to latest Except
557
+ if (this.inExceptionTableHandler && this.blocks.top()?.blockType !== AST.ASTBlock.BlockType.Except) {
558
+ for (let i = this.blocks.length - 1; i >= 0; i--) {
559
+ if (this.blocks[i].blockType === AST.ASTBlock.BlockType.Except) {
560
+ this.curBlock = this.blocks[i];
561
+ break;
562
+ }
563
+ }
564
+ }
565
+ // While in exception handler, attach current instruction target into handler if it is a stack value
566
+ this.appendExceptionExprs =
567
+ this.inExceptionTableHandler &&
568
+ this.curBlock?.blockType === AST.ASTBlock.BlockType.Except &&
569
+ this.dataStack.length > 0 &&
570
+ ![this.OpCodes.POP_EXCEPT, this.OpCodes.RERAISE_A, this.OpCodes.RERAISE].includes(this.code.Current.OpCodeID);
571
+
572
+ if (g_cliArgs?.debug && g_cliArgs.verbose && (this.code.Current.InstructionName?.includes('JUMP') || this.code.Current.InstructionName?.includes('BREAK') || this.code.Current.InstructionName?.includes('LOOP') || this.code.Current.Offset < 30)) {
573
+ console.log(`[${this.code.Current.Offset}] ${this.code.Current.InstructionName} arg=${this.code.Current.Argument} target=${this.code.Current.JumpTarget || 'N/A'}`);
574
+ }
575
+
576
+ // Skip unreachable code after break/continue/return
577
+ if (this.unreachableUntil > 0 && this.code.Current.Offset < this.unreachableUntil) {
578
+ if (g_cliArgs?.debug) {
579
+ console.log(`Skipping unreachable code at offset ${this.code.Current.Offset} (until ${this.unreachableUntil}): ${this.code.Current.InstructionName}`);
580
+ }
581
+ continue;
582
+ }
583
+ if (this.unreachableUntil > 0) {
584
+ if (g_cliArgs?.debug) {
585
+ console.log(`Exiting unreachable region at offset ${this.code.Current.Offset} (was until ${this.unreachableUntil})`);
586
+ }
587
+ }
588
+ this.unreachableUntil = -1; // Reset when we pass the unreachable region
589
+
590
+ if (this.need_try && this.code.Current.OpCodeID != this.OpCodes.SETUP_EXCEPT_A) {
591
+ this.need_try = false;
592
+
593
+ let tryBlock = new AST.ASTBlock(AST.ASTBlock.BlockType.Try, this.code.Current.Offset, this.curBlock.end, true);
594
+ this.blocks.push(tryBlock);
595
+ this.curBlock = this.blocks.top();
596
+ } else if (
597
+ this.else_pop &&
598
+ ![
599
+ this.OpCodes.JUMP_FORWARD_A,
600
+ this.OpCodes.JUMP_IF_FALSE_A,
601
+ this.OpCodes.JUMP_IF_FALSE_OR_POP_A,
602
+ this.OpCodes.POP_JUMP_IF_FALSE_A,
603
+ this.OpCodes.POP_JUMP_FORWARD_IF_FALSE_A,
604
+ this.OpCodes.JUMP_IF_TRUE_A,
605
+ this.OpCodes.JUMP_IF_TRUE_OR_POP_A,
606
+ this.OpCodes.POP_JUMP_IF_TRUE_A,
607
+ this.OpCodes.POP_JUMP_FORWARD_IF_TRUE_A,
608
+ this.OpCodes.POP_BLOCK
609
+ ].includes(this.code.Current.OpCodeID)
610
+ ) {
611
+ this.else_pop = false;
612
+
613
+ let prev = this.curBlock;
614
+ while (prev.end < this.code.Next?.Offset && prev.blockType != AST.ASTBlock.BlockType.Main) {
615
+ if (prev.blockType != AST.ASTBlock.BlockType.Container) {
616
+ if (prev.end == 0) {
617
+ break;
618
+ }
619
+ }
620
+
621
+ if (g_cliArgs?.debug) {
622
+ console.log(`Closing block ${prev.type_str}(${prev.start}-${prev.end}) at offset ${this.code.Current.Offset} (Next=${this.code.Next?.Offset})`);
623
+ }
624
+
625
+ this.blocks.pop();
626
+
627
+ if (this.blocks.empty())
628
+ break;
629
+
630
+ this.curBlock = this.blocks.top();
631
+ this.curBlock.append(prev);
632
+
633
+ if (g_cliArgs?.debug) {
634
+ console.log(` Appended to ${this.curBlock.type_str}(${this.curBlock.start}-${this.curBlock.end}), now has ${this.curBlock.nodes.length} nodes`);
635
+ }
636
+
637
+ prev = this.curBlock;
638
+
639
+ this.checkIfExpr();
640
+ }
641
+ }
642
+
643
+ // Close WITH blocks BEFORE processing the instruction at their end offset
644
+ // This prevents the cleanup code (LOAD_CONST None, CALL __exit__) from being added to the block
645
+ // Loop to close all nested WITH blocks that have reached their end
646
+ let closedWithBlock = false;
647
+ while (this.blocks.length > 1) {
648
+ // Find the topmost WITH block
649
+ let withIdx = -1;
650
+ for (let i = this.blocks.length - 1; i >= 1; i--) {
651
+ if (this.blocks[i].blockType === AST.ASTBlock.BlockType.With) {
652
+ withIdx = i;
653
+ break;
654
+ }
655
+ }
656
+ if (withIdx < 0) break;
657
+
658
+ const withBlock = this.blocks[withIdx];
659
+ if (withBlock.end <= 0 || this.code.Current.Offset < withBlock.end) {
660
+ break; // This WITH block hasn't reached its end yet
661
+ }
662
+
663
+ // Close this WITH block
664
+ this.blocks.splice(withIdx, 1);
665
+ const parentBlock = this.blocks[withIdx - 1] || this.blocks.top();
666
+ parentBlock.append(withBlock);
667
+ closedWithBlock = true;
668
+ if (g_cliArgs?.debug) {
669
+ console.log(`[Pre-handler] Closed WITH block at offset ${this.code.Current.Offset}, withEnd=${withBlock.end}`);
670
+ }
671
+ this.curBlock = this.blocks.top();
672
+ }
673
+
674
+ // Skip the __exit__ cleanup code after closing WITH blocks
675
+ // Pattern: LOAD_CONST None, LOAD_CONST None, LOAD_CONST None, CALL 2/3, POP_TOP
676
+ if (closedWithBlock) {
677
+ // Check if we're at the start of __exit__ cleanup code
678
+ const constObj = this.code.Current.ConstantObject?.object;
679
+ const isLoadingNone = this.code.Current.OpCodeID === this.OpCodes.LOAD_CONST_A &&
680
+ (constObj == null || constObj.ClassName === 'Py_None');
681
+ if (isLoadingNone) {
682
+ // Skip the cleanup pattern: LOAD_CONST None x3, CALL, CACHE*, POP_TOP
683
+ // Stop at any instruction that starts a new statement
684
+ let skipCount = 0;
685
+ while (this.code.HasInstructionsToProcess && skipCount < 20) {
686
+ const instr = this.code.Current;
687
+ // Stop at instructions that start new statements
688
+ if (instr.OpCodeID === this.OpCodes.BEFORE_WITH ||
689
+ instr.OpCodeID === this.OpCodes.PUSH_NULL ||
690
+ instr.OpCodeID === this.OpCodes.RETURN_VALUE ||
691
+ instr.OpCodeID === this.OpCodes.RETURN_CONST_A ||
692
+ instr.OpCodeID === this.OpCodes.LOAD_FAST_A ||
693
+ instr.OpCodeID === this.OpCodes.LOAD_FAST_BORROW_A ||
694
+ instr.OpCodeID === this.OpCodes.LOAD_FAST_CHECK_A ||
695
+ instr.OpCodeID === this.OpCodes.LOAD_NAME_A ||
696
+ instr.OpCodeID === this.OpCodes.LOAD_GLOBAL_A ||
697
+ instr.OpCodeID === this.OpCodes.STORE_FAST_A ||
698
+ instr.OpCodeID === this.OpCodes.STORE_NAME_A) {
699
+ break;
700
+ }
701
+ if (g_cliArgs?.debug) {
702
+ console.log(`[Pre-handler] Skipping WITH cleanup: ${instr.InstructionName} at ${instr.Offset}`);
703
+ }
704
+ skipCount++;
705
+ this.code.GoNext();
706
+ }
707
+ // Don't continue - let the current instruction be processed
708
+ }
709
+ }
710
+
711
+ if (this.code.Current.OpCodeID in PycDecompiler.opCodeHandlers)
712
+ {
713
+ PycDecompiler.opCodeHandlers[this.code.Current.OpCodeID].call(this);
714
+ } else {
715
+ console.error(`Unsupported opcode ${this.code.Current.InstructionName} at pos ${this.code.Current.Offset}\n`);
716
+ this.cleanBuild = false;
717
+ let node = new AST.ASTNodeList(this.defBlock.nodes);
718
+ return node;
719
+ }
720
+ this.closeEndedBlocks();
721
+ this.else_pop = [AST.ASTBlock.BlockType.Else,
722
+ AST.ASTBlock.BlockType.If,
723
+ AST.ASTBlock.BlockType.Elif
724
+ ].includes(this.curBlock.blockType)
725
+ && (this.curBlock.end == this.code.Next?.Offset);
726
+
727
+ } catch (ex) {
728
+ 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`);
729
+ if (global.g_cliArgs?.debug) {
730
+ console.error('Stack trace:', ex.stack);
731
+ }
732
+ }
733
+ }
734
+
735
+ if (this.blocks.length > 1) {
736
+ if (g_cliArgs?.debug) {
737
+ console.error(`Warning: block stack is not empty${this.blocks.length} blocks.\n`);
738
+ console.error('Remaining blocks in stack:');
739
+ for (let i = 0; i < this.blocks.length; i++) {
740
+ let blk = this.blocks[i];
741
+ console.error(` [${i}] ${blk.type_str} (${blk.start}-${blk.end}) nodes=${blk.nodes.length}`);
742
+ }
743
+ }
744
+
745
+ while (this.blocks.length > 1) {
746
+ let tmp = this.blocks.pop();
747
+
748
+ // Set end offset for blocks that were never closed properly
749
+ if (tmp.end === 0 || tmp.end === undefined) {
750
+ tmp.end = this.code.Current?.Offset || this.object.CodeSize;
751
+ if (g_cliArgs?.debug) {
752
+ console.error(` Setting end=${tmp.end} for unclosed ${tmp.type_str}(${tmp.start}-${tmp.end})`);
753
+ }
754
+ }
755
+
756
+ if (g_cliArgs?.debug) {
757
+ console.error(`Appending ${tmp.type_str} (nodes=${tmp.nodes.length}) to ${this.blocks.top().type_str}`);
758
+ }
759
+ this.blocks.top().append(tmp);
760
+ }
761
+ }
762
+
763
+ this.cleanBuild = true;
764
+ let mainNode = new AST.ASTNodeList(this.defBlock.nodes);
765
+ return mainNode;
766
+ }
767
+
768
+ transformExceptionGroups(root) {
769
+ const merged = this.mergeOrphanedEgHandlers(root);
770
+ if (merged) {
771
+ // Still run light cleanup to drop residual artifacts
772
+ this.cleanupExceptBlocks(root);
773
+ this.pruneEmptyExcepts(root);
774
+ return;
775
+ }
776
+
777
+ // Breadth-first traversal to avoid deep recursion; skip on large graphs
778
+ const MAX_VISITED = 20000;
779
+ const queue = [root];
780
+ const visited = new WeakSet();
781
+ let visitedCount = 0;
782
+ while (queue.length) {
783
+ const node = queue.shift();
784
+ if (!node || visited.has(node)) continue;
785
+ visited.add(node);
786
+ if (++visitedCount > MAX_VISITED) break;
787
+ if (node instanceof AST.ASTNodeList) {
788
+ queue.push(...node.list);
789
+ } else if (node instanceof AST.ASTStore) {
790
+ queue.push(node.src);
791
+ } else if (node instanceof AST.ASTFunction) {
792
+ queue.push(node.code?.object?.SourceCode);
793
+ } else if (node instanceof AST.ASTBlock || node instanceof AST.ASTCondBlock) {
794
+ if (node instanceof AST.ASTCondBlock) {
795
+ this.tryConvertExceptStar(node);
796
+ }
797
+ queue.push(...(node.nodes || []));
798
+ }
799
+ }
800
+
801
+ if (visitedCount <= MAX_VISITED) {
802
+ this.flattenNestedExcepts(root);
803
+ this.removeEgHelperArtifacts(root);
804
+ this.cleanupExceptBlocks(root);
805
+ this.pruneEmptyExcepts(root);
806
+ }
807
+ }
808
+
809
+ mergeOrphanedEgHandlers(root) {
810
+ if (!(root instanceof AST.ASTNodeList)) {
811
+ return false;
812
+ }
813
+
814
+ const tryIdx = root.list.findIndex(n => n instanceof AST.ASTBlock && n.blockType === AST.ASTBlock.BlockType.Try);
815
+ if (tryIdx < 0 || tryIdx >= root.list.length - 1) {
816
+ return false;
817
+ }
818
+
819
+ const trailing = root.list.slice(tryIdx + 1);
820
+ // Skip leading cleanup nodes (e = None / del e / __exception__ artifacts)
821
+ let firstStoreIdx = trailing.findIndex(n =>
822
+ n instanceof AST.ASTStore &&
823
+ n.src instanceof AST.ASTCall &&
824
+ n.src.func instanceof AST.ASTName &&
825
+ n.src.func.name === "__check_eg_match__");
826
+
827
+ if (firstStoreIdx < 0) {
828
+ return false;
829
+ }
830
+
831
+ const first = trailing[firstStoreIdx];
832
+ const matchType = first.src.pparams?.[1] || null;
833
+
834
+ if (!matchType) {
835
+ return false;
836
+ }
837
+
838
+ const aliasName = first instanceof AST.ASTStore ? (first.dest || new AST.ASTName("e")) : new AST.ASTName("e");
839
+
840
+ // Drop cleanup artifacts; keep only meaningful body (e.g., print)
841
+ const filtered = trailing.slice(firstStoreIdx).filter(node => {
842
+ if (node instanceof AST.ASTSubscr && !node.name && !node.key) {
843
+ return false;
844
+ }
845
+ if (node instanceof AST.ASTReturn && node.value instanceof AST.ASTObject && node.value.m_obj == null) {
846
+ return false;
847
+ }
848
+ if (node instanceof AST.ASTStore &&
849
+ node.src instanceof AST.ASTCall &&
850
+ node.src.func?.name === "__check_eg_match__") {
851
+ return false;
852
+ }
853
+ if (aliasName && node instanceof AST.ASTStore &&
854
+ node.dest?.name === aliasName.name &&
855
+ node.src instanceof AST.ASTNone) {
856
+ return false;
857
+ }
858
+ if (aliasName && node instanceof AST.ASTDelete &&
859
+ node.value?.name === aliasName.name) {
860
+ return false;
861
+ }
862
+ return true;
863
+ });
864
+
865
+ if (!filtered.length) {
866
+ return false;
867
+ }
868
+
869
+ const tryBlock = root.list[tryIdx];
870
+ const sanitizeBody = (nodes) => {
871
+ return (nodes || []).filter(node => {
872
+ if (node instanceof AST.ASTStore &&
873
+ node.src instanceof AST.ASTCall &&
874
+ node.src.func?.name === "__check_eg_match__") {
875
+ return false;
876
+ }
877
+ if (node instanceof AST.ASTStore &&
878
+ node.dest?.name === aliasName.name &&
879
+ node.src instanceof AST.ASTNone) {
880
+ return false;
881
+ }
882
+ if (node instanceof AST.ASTDelete &&
883
+ node.value?.name === aliasName.name) {
884
+ return false;
885
+ }
886
+ if (node instanceof AST.ASTSubscr && !node.name && !node.key) {
887
+ return false;
888
+ }
889
+ if (node instanceof AST.ASTReturn && node.value instanceof AST.ASTObject && node.value.m_obj == null) {
890
+ return false;
891
+ }
892
+ const condStr = node.condition?.codeFragment?.()?.toString?.();
893
+ if (node instanceof AST.ASTCondBlock && typeof condStr === 'string' && condStr.includes('__prep_reraise_star__')) {
894
+ return false;
895
+ }
896
+ if (node instanceof AST.ASTReturn && (!node.value || node.value instanceof AST.ASTNone)) {
897
+ return false;
898
+ }
899
+ return true;
900
+ });
901
+ };
902
+
903
+ // Capture existing ValueError body if present
904
+ const existingExcepts = (tryBlock.nodes || []).filter(n => n instanceof AST.ASTCondBlock && n.blockType === AST.ASTBlock.BlockType.Except);
905
+ const valueErrBlock = existingExcepts.find(b => {
906
+ const cond = b.condition;
907
+ const condName = cond instanceof AST.ASTStore ? cond.src : cond;
908
+ return condName instanceof AST.ASTName && condName.name === "ValueError";
909
+ });
910
+ let valueErrBody = sanitizeBody(valueErrBlock?.nodes || []);
911
+ const typeErrBody = sanitizeBody(filtered);
912
+
913
+ const callNode = (tryBlock.nodes || []).find(n => n instanceof AST.ASTCall);
914
+ const newBlocks = [];
915
+ if (callNode) {
916
+ newBlocks.push(callNode);
917
+ }
918
+
919
+ // If the ValueError handler lost its body (common on 3.11), pull following siblings in the try
920
+ if (valueErrBlock && valueErrBody.length === 0) {
921
+ const tryNodes = tryBlock.nodes || [];
922
+ const valIdx = tryNodes.indexOf(valueErrBlock);
923
+ if (valIdx >= 0) {
924
+ const tail = [];
925
+ for (let i = valIdx + 1; i < tryNodes.length; i++) {
926
+ const n = tryNodes[i];
927
+ if (n instanceof AST.ASTCondBlock && n.blockType === AST.ASTBlock.BlockType.Except) {
928
+ break;
929
+ }
930
+ tail.push(n);
931
+ }
932
+ if (tail.length) {
933
+ // Remove tail from try block
934
+ tryBlock.m_nodes = tryNodes.slice(0, valIdx + 1);
935
+ valueErrBody = sanitizeBody(tail);
936
+ }
937
+ }
938
+ }
939
+
940
+ const makeExcept = (typeName, body) => {
941
+ const cond = new AST.ASTStore(new AST.ASTName(typeName), aliasName);
942
+ cond.line = aliasName.line || cond.line;
943
+ const blk = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Except, -1, -1, cond, false);
944
+ blk.isExceptStar = true;
945
+ blk.nodes.push(...body);
946
+ return blk;
947
+ };
948
+
949
+ if (valueErrBody.length) {
950
+ newBlocks.push(makeExcept("ValueError", valueErrBody));
951
+ }
952
+ if (typeErrBody.length) {
953
+ newBlocks.push(makeExcept(matchType?.name || matchType?.toString?.() || "Exception", typeErrBody));
954
+ }
955
+
956
+ if (newBlocks.length) {
957
+ tryBlock.m_nodes = newBlocks;
958
+ }
959
+
960
+ // Remove only the consumed trailing nodes (including any leading cleanup we skipped over)
961
+ root.list.splice(tryIdx + 1 + firstStoreIdx, trailing.length - firstStoreIdx);
962
+ return true;
963
+ }
964
+
965
+ tryConvertExceptStar(block) {
966
+ if (!this.isExceptStarPattern(block)) {
967
+ return;
968
+ }
969
+
970
+ const compare = block.condition;
971
+ const call = compare.left;
972
+ let aliasStore = null;
973
+ for (let child of block.nodes || []) {
974
+ if (child instanceof AST.ASTStore && child.src === call && child.dest instanceof AST.ASTName) {
975
+ aliasStore = child;
976
+ break;
977
+ }
978
+ }
979
+
980
+ if (!aliasStore || !(aliasStore.dest instanceof AST.ASTName)) {
981
+ return;
982
+ }
983
+
984
+ const aliasName = aliasStore.dest;
985
+ const typeNode = call.pparams?.[1] || new AST.ASTName("ExceptionGroup");
986
+
987
+ const filtered = (block.nodes || []).filter(child => child !== aliasStore);
988
+ const cleaned = filtered.filter(child => {
989
+ if (child instanceof AST.ASTStore && child.dest?.name === aliasName.name && child.src instanceof AST.ASTNone) {
990
+ return false;
991
+ }
992
+ if (child instanceof AST.ASTDelete && child.value?.name === aliasName.name) {
993
+ return false;
994
+ }
995
+ return true;
996
+ });
997
+ block.m_nodes = cleaned;
998
+
999
+ const condStore = new AST.ASTStore(typeNode, aliasName);
1000
+ condStore.line = block.line;
1001
+ block.condition = condStore;
1002
+ block.m_blockType = AST.ASTBlock.BlockType.Except;
1003
+ block.isExceptStar = true;
1004
+ block.negative = false;
1005
+
1006
+ }
1007
+
1008
+ isExceptStarPattern(block) {
1009
+ if (!(block instanceof AST.ASTCondBlock)) {
1010
+ return false;
1011
+ }
1012
+
1013
+ if (![AST.ASTBlock.BlockType.If, AST.ASTBlock.BlockType.Elif].includes(block.blockType)) {
1014
+ return false;
1015
+ }
1016
+
1017
+ const cond = block.condition;
1018
+ if (!(cond instanceof AST.ASTCompare)) {
1019
+ return false;
1020
+ }
1021
+ if (![AST.ASTCompare.CompareOp.Is, AST.ASTCompare.CompareOp.IsNot].includes(cond.op)) {
1022
+ return false;
1023
+ }
1024
+ if (!(cond.right instanceof AST.ASTNone)) {
1025
+ return false;
1026
+ }
1027
+ const left = cond.left;
1028
+ if (!(left instanceof AST.ASTCall)) {
1029
+ return false;
1030
+ }
1031
+ if (left.func?.name !== '__check_eg_match__') {
1032
+ return false;
1033
+ }
1034
+ return true;
1035
+ }
1036
+
1037
+ removeEgHelperArtifacts(node) {
1038
+ // Temporarily disabled to avoid deep recursion issues
1039
+ return;
1040
+ }
1041
+
1042
+ flattenNestedExcepts(root) {
1043
+ const queue = [{node: root, parentArr: null}];
1044
+ const visited = new WeakSet();
1045
+ while (queue.length) {
1046
+ const {node, parentArr} = queue.shift();
1047
+ if (!node || visited.has(node)) continue;
1048
+ visited.add(node);
1049
+
1050
+ const children = node instanceof AST.ASTNodeList ? node.list :
1051
+ (node instanceof AST.ASTBlock || node instanceof AST.ASTCondBlock) ? node.nodes :
1052
+ null;
1053
+
1054
+ if (Array.isArray(children)) {
1055
+ for (let idx = 0; idx < children.length; idx++) {
1056
+ const child = children[idx];
1057
+ if (child instanceof AST.ASTCondBlock && child.blockType === AST.ASTBlock.BlockType.Except) {
1058
+ const hoisted = [];
1059
+ child.m_nodes = (child.nodes || []).filter(n => {
1060
+ if (n instanceof AST.ASTCondBlock && n.blockType === AST.ASTBlock.BlockType.Except) {
1061
+ hoisted.push(n);
1062
+ return false;
1063
+ }
1064
+ return true;
1065
+ });
1066
+ if (hoisted.length && Array.isArray(children)) {
1067
+ children.splice(idx + 1, 0, ...hoisted);
1068
+ }
1069
+ }
1070
+ }
1071
+ // Enqueue after potential mutation to avoid skipping
1072
+ children.forEach(ch => queue.push({node: ch, parentArr: children}));
1073
+ }
1074
+ }
1075
+ }
1076
+
1077
+ cleanupExceptBlocks(node) {
1078
+ if (!node) return;
1079
+ const stripExcept = (blk) => {
1080
+ if (blk.blockType !== AST.ASTBlock.BlockType.Except) return;
1081
+ let aliasName = null;
1082
+ let matchType = null;
1083
+ blk.m_nodes = (blk.nodes || []).filter(child => {
1084
+ if (child.blockType === AST.ASTBlock.BlockType.Except) {
1085
+ return false;
1086
+ }
1087
+ if (child instanceof AST.ASTCondBlock && child.blockType == AST.ASTBlock.BlockType.Except) {
1088
+ return false; // drop nested excepts
1089
+ }
1090
+ if (child instanceof AST.ASTStore &&
1091
+ child.src instanceof AST.ASTCall &&
1092
+ child.src.func?.name === "__check_eg_match__") {
1093
+ aliasName = child.dest instanceof AST.ASTName ? child.dest : aliasName;
1094
+ matchType = child.src.pparams?.[1] || matchType;
1095
+ return false; // header already encodes condition
1096
+ }
1097
+ if (child instanceof AST.ASTStore &&
1098
+ child.dest instanceof AST.ASTName &&
1099
+ child.src instanceof AST.ASTNone) {
1100
+ aliasName = child.dest;
1101
+ return false;
1102
+ }
1103
+ if (child instanceof AST.ASTDelete && aliasName && child.value?.name === aliasName.name) {
1104
+ return false;
1105
+ }
1106
+ if (child instanceof AST.ASTCondBlock) {
1107
+ const condStr = child.condition?.codeFragment?.();
1108
+ if (typeof condStr === 'string' && condStr.includes("__exception__<EXCEPTION MATCH>")) {
1109
+ return false;
1110
+ }
1111
+ }
1112
+ if (child instanceof AST.ASTSubscr) {
1113
+ const frag = child.codeFragment?.();
1114
+ const fragStr = frag?.toString?.() || '';
1115
+ if (fragStr.includes('__exception__')) {
1116
+ return false;
1117
+ }
1118
+ }
1119
+ return true;
1120
+ });
1121
+ if (aliasName) {
1122
+ const cond = matchType || blk.condition || new AST.ASTName("Exception");
1123
+ blk.condition = new AST.ASTStore(cond, aliasName);
1124
+ if (matchType) {
1125
+ blk.isExceptStar = true;
1126
+ }
1127
+ }
1128
+ if (blk.m_nodes.length > 3) {
1129
+ // Trim runaway nested bodies: keep calls and raises
1130
+ blk.m_nodes = blk.m_nodes.filter(ch => ch instanceof AST.ASTCall || ch instanceof AST.ASTRaise);
1131
+ }
1132
+ };
1133
+
1134
+ // Limit traversal breadth/depth to avoid runaway recursion on malformed trees
1135
+ const queue = [{n: node, d: 0}];
1136
+ const visitedNodes = new WeakSet();
1137
+ const MAX_DEPTH = 512;
1138
+ const MAX_VISITED = 5000;
1139
+ let visitedCount = 0;
1140
+ while (queue.length) {
1141
+ const {n: cur, d} = queue.shift();
1142
+ if (!cur || visitedNodes.has(cur)) continue;
1143
+ visitedNodes.add(cur);
1144
+ if (++visitedCount > MAX_VISITED) break;
1145
+ if (d > MAX_DEPTH) continue;
1146
+ if (cur instanceof AST.ASTBlock || cur instanceof AST.ASTCondBlock) {
1147
+ stripExcept(cur);
1148
+ (cur.nodes || []).forEach(ch => queue.push({n: ch, d: d + 1}));
1149
+ } else if (cur instanceof AST.ASTNodeList) {
1150
+ cur.list.forEach(ch => queue.push({n: ch, d: d + 1}));
1151
+ }
1152
+ }
1153
+ }
1154
+
1155
+ pruneEmptyExcepts(root) {
1156
+ const queue = [{node: root, parentArr: null, idx: -1}];
1157
+ const visited = new WeakSet();
1158
+ const isCleanupNode = (node, aliasName) => {
1159
+ if (!node) return false;
1160
+ if (node instanceof AST.ASTStore &&
1161
+ node.src instanceof AST.ASTCall &&
1162
+ node.src.func?.name === "__check_eg_match__") {
1163
+ return false;
1164
+ }
1165
+ if (node instanceof AST.ASTDelete) {
1166
+ return !aliasName || node.value?.name === aliasName.name;
1167
+ }
1168
+ if (node instanceof AST.ASTStore && node.dest?.name === aliasName?.name && node.src instanceof AST.ASTNone) {
1169
+ return true;
1170
+ }
1171
+ let fragStr = '';
1172
+ try {
1173
+ const frag = node?.codeFragment?.();
1174
+ fragStr = frag?.toString?.() || '';
1175
+ } catch (e) {
1176
+ // Skip nodes that fail to render
1177
+ }
1178
+ if (typeof fragStr === 'string' && fragStr.includes('__exception__')) {
1179
+ return true;
1180
+ }
1181
+ if (node instanceof AST.ASTKeyword && node.word === AST.ASTKeyword.Word.Pass) {
1182
+ return true;
1183
+ }
1184
+ return false;
1185
+ };
1186
+
1187
+ // Helper to check if a node is except* cleanup at parent level
1188
+ const isExceptStarCleanup = (node, aliasNames) => {
1189
+ if (!node || !aliasNames || aliasNames.size === 0) return false;
1190
+ // Never treat full blocks as cleanup; only simple statements can be cleanup artifacts.
1191
+ if (node instanceof AST.ASTBlock || node instanceof AST.ASTCondBlock) {
1192
+ return false;
1193
+ }
1194
+ // del e
1195
+ if (node instanceof AST.ASTDelete && aliasNames.has(node.value?.name)) {
1196
+ return true;
1197
+ }
1198
+ // e = None
1199
+ if (node instanceof AST.ASTStore && aliasNames.has(node.dest?.name)) {
1200
+ const val = node.src;
1201
+ if (val instanceof AST.ASTNone || val?.object?.ClassName === 'Py_None') {
1202
+ return true;
1203
+ }
1204
+ }
1205
+ // __exception__[...]
1206
+ let fragStr = '';
1207
+ try {
1208
+ const frag = node?.codeFragment?.();
1209
+ fragStr = frag?.toString?.() || '';
1210
+ } catch (e) {
1211
+ // Skip nodes that fail to render
1212
+ }
1213
+ if (typeof fragStr === 'string' && fragStr.includes('__exception__')) {
1214
+ return true;
1215
+ }
1216
+ return false;
1217
+ };
1218
+
1219
+ // First pass: collect except* alias names from try blocks and filter sibling cleanup
1220
+ const filterExceptStarCleanup = (children) => {
1221
+ if (!Array.isArray(children)) return;
1222
+ const aliasNames = new Set();
1223
+ // Collect alias names from except* handlers in try blocks
1224
+ for (const child of children) {
1225
+ if (child instanceof AST.ASTBlock && child.blockType === AST.ASTBlock.BlockType.Try) {
1226
+ for (const tryChild of (child.nodes || [])) {
1227
+ if (tryChild instanceof AST.ASTCondBlock &&
1228
+ tryChild.blockType === AST.ASTBlock.BlockType.Except &&
1229
+ tryChild.isExceptStar) {
1230
+ const alias = tryChild.condition instanceof AST.ASTStore ? tryChild.condition.dest?.name : null;
1231
+ if (alias) aliasNames.add(alias);
1232
+ }
1233
+ }
1234
+ }
1235
+ }
1236
+ // Filter cleanup nodes at this level
1237
+ if (aliasNames.size > 0) {
1238
+ for (let i = children.length - 1; i >= 0; i--) {
1239
+ if (isExceptStarCleanup(children[i], aliasNames)) {
1240
+ children.splice(i, 1);
1241
+ }
1242
+ }
1243
+ }
1244
+ };
1245
+
1246
+ while (queue.length) {
1247
+ const {node, parentArr, idx} = queue.shift();
1248
+ if (!node || visited.has(node)) continue;
1249
+ visited.add(node);
1250
+
1251
+ if (node instanceof AST.ASTCondBlock && node.blockType === AST.ASTBlock.BlockType.Except) {
1252
+ const alias = node.condition instanceof AST.ASTStore ? node.condition.dest : null;
1253
+ const filtered = (node.nodes || []).filter(n => !isCleanupNode(n, alias));
1254
+
1255
+ // If an except handler has been reduced to empty but still has a condition, keep it with 'pass'
1256
+ // to preserve the handler rather than dropping it entirely.
1257
+ if (node.condition && filtered.length === 0) {
1258
+ node.m_nodes = [new AST.ASTKeyword(AST.ASTKeyword.Word.Pass)];
1259
+ } else if (filtered.length !== (node.nodes || []).length) {
1260
+ node.m_nodes = filtered;
1261
+ }
1262
+ if (Array.isArray(parentArr) && (!node.nodes || node.nodes.length === 0)) {
1263
+ parentArr.splice(idx, 1);
1264
+ continue;
1265
+ }
1266
+ }
1267
+
1268
+ const children = node instanceof AST.ASTNodeList ? node.list :
1269
+ (node instanceof AST.ASTBlock || node instanceof AST.ASTCondBlock) ? node.nodes :
1270
+ null;
1271
+ if (Array.isArray(children)) {
1272
+ // Filter except* cleanup at this level before enqueueing
1273
+ filterExceptStarCleanup(children);
1274
+ children.forEach((ch, i) => queue.push({node: ch, parentArr: children, idx: i}));
1275
+ }
1276
+ }
1277
+ }
1278
+
1279
+ isPrepReraiseBlock(block) {
1280
+ if (!(block instanceof AST.ASTCondBlock)) {
1281
+ return false;
1282
+ }
1283
+ if (block.blockType != AST.ASTBlock.BlockType.If) {
1284
+ return false;
1285
+ }
1286
+ const cond = block.condition;
1287
+ if (!(cond instanceof AST.ASTCompare)) {
1288
+ return false;
1289
+ }
1290
+ if (!(cond.right instanceof AST.ASTNone)) {
1291
+ return false;
1292
+ }
1293
+ const call = cond.left;
1294
+ if (!(call instanceof AST.ASTCall) || call.func?.name !== '__prep_reraise_star__') {
1295
+ return false;
1296
+ }
1297
+ if (!block.nodes || block.nodes.length !== 1) {
1298
+ return false;
1299
+ }
1300
+ const bodyCall = block.nodes[0];
1301
+ return bodyCall instanceof AST.ASTCall && bodyCall.func?.name === '__prep_reraise_star__';
1302
+ }
1303
+
1304
+ isEgCleanupElseBlock(block, prev) {
1305
+ if (!(block instanceof AST.ASTBlock)) {
1306
+ return false;
1307
+ }
1308
+ if (block.blockType != AST.ASTBlock.BlockType.Else) {
1309
+ return false;
1310
+ }
1311
+ if (!(prev instanceof AST.ASTCondBlock) || prev.blockType != AST.ASTBlock.BlockType.Except || !prev.isExceptStar) {
1312
+ return false;
1313
+ }
1314
+ const aliasName = prev.condition instanceof AST.ASTStore ? prev.condition.dest?.name : null;
1315
+ if (global.g_cliArgs?.debug) {
1316
+ console.log(`[EG] alias for cleanup check: ${aliasName}`);
1317
+ }
1318
+ if (!aliasName) {
1319
+ return false;
1320
+ }
1321
+ const nodes = block.nodes || [];
1322
+ for (const node of nodes) {
1323
+ if (!this.isEgCleanupNode(node, aliasName)) {
1324
+ return false;
1325
+ }
1326
+ }
1327
+ if (global.g_cliArgs?.debug) {
1328
+ console.log(`[EG] Removing cleanup else block for alias ${aliasName}`);
1329
+ }
1330
+ return true;
1331
+ }
1332
+
1333
+ isEgCleanupNode(node, aliasName) {
1334
+ if (!node) {
1335
+ return false;
1336
+ }
1337
+ if (node instanceof AST.ASTStore && node.dest?.name === aliasName) {
1338
+ return true;
1339
+ }
1340
+ if (node instanceof AST.ASTDelete && node.value?.name === aliasName) {
1341
+ return true;
1342
+ }
1343
+ if (node instanceof AST.ASTName && node.name?.startsWith('##ERROR##')) {
1344
+ return true;
1345
+ }
1346
+ if (node instanceof AST.ASTSubscr || node.constructor?.name === 'ASTSubscr') {
1347
+ const fragmentValue = node.codeFragment?.();
1348
+ const fragmentStr = typeof fragmentValue === 'string' ? fragmentValue : fragmentValue?.toString?.();
1349
+ if (typeof fragmentStr === 'string' && fragmentStr.includes('##ERROR##')) {
1350
+ return true;
1351
+ }
1352
+ }
1353
+ if (global.g_cliArgs?.debug) {
1354
+ console.log(`[EG] Not a cleanup node: ${node.constructor?.name} -> ${node.codeFragment?.()}`);
1355
+ }
1356
+ return false;
1357
+ }
1358
+
1359
+ rewriteClassDefinitions(root) {
1360
+ if (!(root instanceof AST.ASTNodeList)) {
1361
+ return;
1362
+ }
1363
+
1364
+ const hasDataclass = this.astHasDataclassImport(root);
1365
+
1366
+ for (const node of root.list) {
1367
+ if (!(node instanceof AST.ASTStore)) {
1368
+ continue;
1369
+ }
1370
+
1371
+ if (node.src instanceof AST.ASTCall && node.src.func instanceof AST.ASTClass &&
1372
+ this.isPlainClassCall(node.src)) {
1373
+ node.src = node.src.func;
1374
+ this.cleanupClassBody(node.src);
1375
+ if (hasDataclass) {
1376
+ node.addDecorator(new AST.ASTName('dataclass'));
1377
+ }
1378
+ } else if (node.src instanceof AST.ASTClass) {
1379
+ this.cleanupClassBody(node.src);
1380
+ }
1381
+ }
1382
+ }
1383
+
1384
+ isPlainClassCall(call) {
1385
+ if (!(call.func instanceof AST.ASTClass)) {
1386
+ return false;
1387
+ }
1388
+ const hasParams = (call.pparams && call.pparams.length > 0) ||
1389
+ (call.kwparams && call.kwparams.length > 0) ||
1390
+ call.hasVar || call.hasKw;
1391
+ return !hasParams;
1392
+ }
1393
+
1394
+ astHasDataclassImport(root) {
1395
+ if (!(root instanceof AST.ASTNodeList)) {
1396
+ return false;
1397
+ }
1398
+
1399
+ return root.list.some(node => {
1400
+ if (!(node instanceof AST.ASTImport)) {
1401
+ return false;
1402
+ }
1403
+ const moduleName = node.name?.codeFragment?.();
1404
+ if (moduleName !== 'dataclasses') {
1405
+ return false;
1406
+ }
1407
+ return node.stores?.some?.(store => store.src?.codeFragment?.() === 'dataclass');
1408
+ });
1409
+ }
1410
+
1411
+ cleanupClassBody(classNode) {
1412
+ if (!(classNode instanceof AST.ASTClass)) {
1413
+ return;
1414
+ }
1415
+ const codeObject = classNode.code?.func?.code?.object || classNode.code?.code?.object;
1416
+ const body = codeObject?.SourceCode;
1417
+ if (!body?.list) {
1418
+ return;
1419
+ }
1420
+
1421
+ const filtered = [];
1422
+ for (const stmt of body.list) {
1423
+ if (this.isSyntheticClassAssignment(stmt)) {
1424
+ continue;
1425
+ }
1426
+ filtered.push(stmt);
1427
+ }
1428
+
1429
+ if (filtered.length === 1 && filtered[0] instanceof AST.ASTKeyword && filtered[0].word === AST.ASTKeyword.Word.Pass) {
1430
+ filtered.length = 0;
1431
+ }
1432
+
1433
+ body.list.length = 0;
1434
+ Array.prototype.push.apply(body.list, filtered);
1435
+ }
1436
+
1437
+ isSyntheticClassAssignment(node) {
1438
+ if (!(node instanceof AST.ASTStore)) {
1439
+ return false;
1440
+ }
1441
+ if (!(node.dest instanceof AST.ASTName)) {
1442
+ return false;
1443
+ }
1444
+ const name = node.dest.name;
1445
+ return name === '__module__' ||
1446
+ name === '__qualname__' ||
1447
+ name === '__classcell__' ||
1448
+ name === '__firstlineno__' ||
1449
+ name === '__type_params__' ||
1450
+ name === '__static_attributes__' ||
1451
+ name === '.generic_base' ||
1452
+ name === '.type_params';
1453
+ }
1454
+
1455
+ removeNullSentinelComparisons(root) {
1456
+ if (!root) {
1457
+ return;
1458
+ }
1459
+ if (root instanceof AST.ASTNodeList) {
1460
+ this.pruneNullSentinels(root.list);
1461
+ } else if (root instanceof AST.ASTBlock) {
1462
+ this.pruneNullSentinels(root.nodes);
1463
+ }
1464
+ }
1465
+
1466
+ removeDuplicateReturns(root) {
1467
+ const prune = (nodes, visited = new Set()) => {
1468
+ if (!Array.isArray(nodes) || visited.has(nodes)) {
1469
+ return;
1470
+ }
1471
+ visited.add(nodes);
1472
+ for (let i = nodes.length - 1; i > 0; i--) {
1473
+ const cur = nodes[i];
1474
+ const prev = nodes[i - 1];
1475
+ if (cur instanceof AST.ASTReturn && prev instanceof AST.ASTReturn) {
1476
+ const curFrag = cur.codeFragment?.()?.toString?.();
1477
+ const prevFrag = prev.codeFragment?.()?.toString?.();
1478
+ if (curFrag === prevFrag) {
1479
+ nodes.splice(i, 1);
1480
+ continue;
1481
+ }
1482
+ }
1483
+ if (cur instanceof AST.ASTNodeList) {
1484
+ prune(cur.list, visited);
1485
+ } else if (cur instanceof AST.ASTBlock || cur instanceof AST.ASTCondBlock) {
1486
+ prune(cur.nodes, visited);
1487
+ } else if (cur instanceof AST.ASTStore && cur.src instanceof AST.ASTFunction) {
1488
+ prune(cur.src.code?.object?.SourceCode?.list, visited);
1489
+ }
1490
+ }
1491
+ };
1492
+
1493
+ if (root instanceof AST.ASTNodeList) {
1494
+ prune(root.list);
1495
+ } else if (root instanceof AST.ASTBlock) {
1496
+ prune(root.nodes);
1497
+ }
1498
+ }
1499
+
1500
+ dedupeExceptHandlers(root) {
1501
+ const queue = [root];
1502
+ const visited = new WeakSet();
1503
+ while (queue.length) {
1504
+ const node = queue.shift();
1505
+ if (!node || visited.has(node)) continue;
1506
+ visited.add(node);
1507
+
1508
+ if (node instanceof AST.ASTBlock && node.blockType === AST.ASTBlock.BlockType.Try) {
1509
+ const nodes = node.nodes || [];
1510
+ // Remove trailing cleanup else blocks (exception-group artifacts).
1511
+ if (nodes.length >= 2) {
1512
+ const last = nodes[nodes.length - 1];
1513
+ const prev = nodes[nodes.length - 2];
1514
+ if (this.isEgCleanupElseBlock(last, prev)) {
1515
+ nodes.pop();
1516
+ }
1517
+ }
1518
+
1519
+ // Drop trivial generic except handlers that only contain pass/cleanup.
1520
+ for (let i = nodes.length - 1; i >= 0; i--) {
1521
+ const blk = nodes[i];
1522
+ if (!(blk instanceof AST.ASTCondBlock) || blk.blockType !== AST.ASTBlock.BlockType.Except) {
1523
+ continue;
1524
+ }
1525
+ const condNode = blk.condition instanceof AST.ASTStore ? blk.condition.src : blk.condition;
1526
+ const condText = (typeof condNode?.codeFragment === 'function' ? condNode.codeFragment()?.toString?.() : null) || condNode?.name;
1527
+ const body = blk.nodes || [];
1528
+ const isTrivialBody = body.length === 0 ||
1529
+ body.every(n => n instanceof AST.ASTKeyword);
1530
+ if (isTrivialBody && (condText === "Exception" || condText === undefined)) {
1531
+ nodes.splice(i, 1);
1532
+ }
1533
+ }
1534
+
1535
+ // Deduplicate consecutive except/except* handlers with identical condition and body.
1536
+ for (let i = nodes.length - 1; i > 0; i--) {
1537
+ const cur = nodes[i];
1538
+ const prev = nodes[i - 1];
1539
+ const bothExcept = cur instanceof AST.ASTCondBlock &&
1540
+ prev instanceof AST.ASTCondBlock &&
1541
+ cur.blockType === AST.ASTBlock.BlockType.Except &&
1542
+ prev.blockType === AST.ASTBlock.BlockType.Except;
1543
+ if (!bothExcept) {
1544
+ continue;
1545
+ }
1546
+ const condA = cur.condition?.codeFragment?.()?.toString?.();
1547
+ const condB = prev.condition?.codeFragment?.()?.toString?.();
1548
+ if (condA !== condB) {
1549
+ continue;
1550
+ }
1551
+ if (!!cur.isExceptStar !== !!prev.isExceptStar) {
1552
+ continue;
1553
+ }
1554
+ const bodyA = cur.codeFragment?.()?.toString?.();
1555
+ const bodyB = prev.codeFragment?.()?.toString?.();
1556
+ if (bodyA && bodyB && bodyA === bodyB) {
1557
+ nodes.splice(i, 1);
1558
+ }
1559
+ }
1560
+ }
1561
+
1562
+ const children = node instanceof AST.ASTNodeList ? node.list
1563
+ : node instanceof AST.ASTBlock || node instanceof AST.ASTCondBlock ? node.nodes
1564
+ : node instanceof AST.ASTStore && node.src instanceof AST.ASTFunction ? node.src.code?.object?.SourceCode?.list
1565
+ : null;
1566
+ if (Array.isArray(children)) {
1567
+ queue.push(...children);
1568
+ }
1569
+ }
1570
+ }
1571
+
1572
+ pruneNullSentinels(nodes, visited = new Set()) {
1573
+ if (!Array.isArray(nodes)) {
1574
+ return;
1575
+ }
1576
+ if (visited.has(nodes)) {
1577
+ return;
1578
+ }
1579
+ visited.add(nodes);
1580
+ for (let i = nodes.length - 1; i >= 0; i--) {
1581
+ const node = nodes[i];
1582
+ if (this.isNullSentinelBlock(node)) {
1583
+ nodes.splice(i, 1);
1584
+ continue;
1585
+ }
1586
+ if (node instanceof AST.ASTNodeList) {
1587
+ this.pruneNullSentinels(node.list, visited);
1588
+ } else if (node instanceof AST.ASTBlock) {
1589
+ this.pruneNullSentinels(node.nodes, visited);
1590
+ } else if (node instanceof AST.ASTStore && node.src instanceof AST.ASTFunction) {
1591
+ this.removeNullSentinelComparisons(node.src.code?.object?.SourceCode);
1592
+ } else if (node instanceof AST.ASTStore && node.src instanceof AST.ASTClass) {
1593
+ this.removeNullSentinelComparisons(node.src.code?.func?.code?.object?.SourceCode);
1594
+ }
1595
+ }
1596
+ }
1597
+
1598
+ isNullSentinelBlock(node) {
1599
+ if (!(node instanceof AST.ASTCondBlock)) {
1600
+ return false;
1601
+ }
1602
+ const condition = node.condition;
1603
+
1604
+ // Drop degenerate IF blocks with no condition and empty body (often leftover from with cleanup tests)
1605
+ const emptyBody = !node.nodes || node.nodes.length === 0 ||
1606
+ node.nodes.every(child => child instanceof AST.ASTKeyword && child.word === AST.ASTKeyword.Word.Pass);
1607
+ if (!condition) {
1608
+ return emptyBody;
1609
+ }
1610
+
1611
+ if (!(condition instanceof AST.ASTCompare)) {
1612
+ return false;
1613
+ }
1614
+ const leftStr = condition.left?.codeFragment?.()?.toString?.().trim?.();
1615
+ const isNullLiteral = leftStr === 'null';
1616
+ const comparesNone = condition.right instanceof AST.ASTNone;
1617
+ return isNullLiteral && comparesNone && emptyBody;
1618
+ }
1619
+
1620
+ wrapFunctionExceptionGroups(root) {
1621
+ if (!(root instanceof AST.ASTNodeList)) {
1622
+ return;
1623
+ }
1624
+
1625
+ // Also handle module-level exception group patterns
1626
+ this.rewriteExceptionGroupsInList(root);
1627
+
1628
+ for (const node of root.list) {
1629
+ if (node instanceof AST.ASTStore && node.src instanceof AST.ASTFunction) {
1630
+ this.rewriteExceptionGroupsInList(node.src.code?.object?.SourceCode);
1631
+ }
1632
+ }
1633
+ }
1634
+
1635
+ rewriteExceptionGroupsInList(listNode) {
1636
+ if (!listNode?.list || listNode.list.length === 0) {
1637
+ return;
1638
+ }
1639
+
1640
+ const nodes = listNode.list;
1641
+ const firstExceptIdx = nodes.findIndex(node => this.isExceptStarBlock(node) || this.isPlainExceptBlock(node));
1642
+ if (firstExceptIdx === -1) {
1643
+ return;
1644
+ }
1645
+
1646
+ let hoistedPrefix = [];
1647
+ let tryBlock = null;
1648
+ let handlerNodes = [];
1649
+
1650
+ // Reuse the nearest preceding try block if it has meaningful body; otherwise build a new one.
1651
+ if (firstExceptIdx > 0) {
1652
+ for (let i = firstExceptIdx - 1; i >= 0; i--) {
1653
+ if (nodes[i] instanceof AST.ASTBlock && nodes[i].blockType === AST.ASTBlock.BlockType.Try) {
1654
+ // Prefer try blocks with real body; skip empty placeholders.
1655
+ const hasBody = (nodes[i].nodes || []).length > 0;
1656
+ if (!hasBody) {
1657
+ continue;
1658
+ }
1659
+ hoistedPrefix = nodes.slice(0, i);
1660
+ tryBlock = nodes[i];
1661
+ break;
1662
+ }
1663
+ }
1664
+ }
1665
+
1666
+ if (!tryBlock) {
1667
+ let startIdx = 0;
1668
+ while (startIdx < firstExceptIdx && this.isEgHoistableSetupNode(nodes[startIdx])) {
1669
+ hoistedPrefix.push(nodes[startIdx]);
1670
+ startIdx++;
1671
+ }
1672
+ tryBlock = new AST.ASTBlock(AST.ASTBlock.BlockType.Try, nodes[startIdx]?.start || nodes[0]?.start || 0, 0, true);
1673
+ tryBlock.nodes.push(...nodes.slice(startIdx, firstExceptIdx));
1674
+ }
1675
+
1676
+ if (tryBlock?.nodes?.length === 1 &&
1677
+ tryBlock.nodes[0] instanceof AST.ASTBlock &&
1678
+ tryBlock.nodes[0].blockType === AST.ASTBlock.BlockType.Try) {
1679
+ tryBlock = tryBlock.nodes[0];
1680
+ }
1681
+
1682
+ // Pull handler blocks out of the try body if they were embedded there
1683
+ if (tryBlock?.nodes?.length) {
1684
+ const filteredBody = [];
1685
+ for (const stmt of tryBlock.nodes) {
1686
+ if (stmt instanceof AST.ASTCondBlock && stmt.blockType === AST.ASTBlock.BlockType.Except) {
1687
+ handlerNodes.push(stmt);
1688
+ continue;
1689
+ }
1690
+ filteredBody.push(stmt);
1691
+ }
1692
+ tryBlock.m_nodes = filteredBody;
1693
+ }
1694
+
1695
+ // Collapse nested try wrappers with no handlers
1696
+ let body = tryBlock?.nodes || [];
1697
+ while (body.length === 1 &&
1698
+ body[0] instanceof AST.ASTBlock &&
1699
+ body[0].blockType === AST.ASTBlock.BlockType.Try) {
1700
+ const inner = body[0];
1701
+ const hasHandlers = (inner.nodes || []).some(n => n instanceof AST.ASTCondBlock && n.blockType === AST.ASTBlock.BlockType.Except);
1702
+ if (hasHandlers) {
1703
+ break;
1704
+ }
1705
+ tryBlock.m_nodes = inner.nodes || [];
1706
+ body = tryBlock.m_nodes;
1707
+ }
1708
+
1709
+ // Drop implicit "return None" from the end of the try body
1710
+ const lastNode = tryBlock.nodes[tryBlock.nodes.length - 1];
1711
+ const lastValStr = lastNode?.value?.codeFragment?.();
1712
+ if (lastNode instanceof AST.ASTReturn &&
1713
+ (lastNode.value instanceof AST.ASTNone || lastValStr === "None" || lastValStr === undefined)) {
1714
+ tryBlock.nodes.pop();
1715
+ }
1716
+
1717
+ const rewritten = [...hoistedPrefix, tryBlock, ...handlerNodes];
1718
+ // Heuristic: wrap stray TypeError print cleanup into an except* TypeError handler (seen on 3.11)
1719
+ const consumed = new Set();
1720
+ for (let i = firstExceptIdx; i < nodes.length; i++) {
1721
+ const node = nodes[i];
1722
+ if (consumed.has(i)) {
1723
+ continue;
1724
+ }
1725
+ const fragment = typeof node?.codeFragment === 'function' ? node.codeFragment()?.toString?.() : '';
1726
+ if (node instanceof AST.ASTCall && typeof fragment === 'string' && fragment.includes('TypeError') && fragment.includes('{e}')) {
1727
+ const cond = new AST.ASTStore(new AST.ASTName('TypeError'), new AST.ASTName('e'));
1728
+ const cb = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Except, node.start || 0, node.end || 0);
1729
+ cb.condition = cond;
1730
+ cb.isExceptStar = true;
1731
+ cb.m_nodes = [node];
1732
+ rewritten.push(cb);
1733
+ // Skip trailing cleanup nodes for e
1734
+ let j = i + 1;
1735
+ while (j < nodes.length) {
1736
+ const next = nodes[j];
1737
+ if (next instanceof AST.ASTStore && next.dest?.name === 'e') {
1738
+ consumed.add(j);
1739
+ j++;
1740
+ continue;
1741
+ }
1742
+ if (next instanceof AST.ASTDelete && next.value?.name === 'e') {
1743
+ consumed.add(j);
1744
+ j++;
1745
+ continue;
1746
+ }
1747
+ break;
1748
+ }
1749
+ consumed.add(i);
1750
+ continue;
1751
+ }
1752
+ if (global.g_cliArgs?.debug && node instanceof AST.ASTBlock && node.blockType == AST.ASTBlock.BlockType.Else) {
1753
+ const prev = rewritten[rewritten.length - 1];
1754
+ console.log(`[EG] Evaluating else cleanup candidate after ${prev?.constructor?.name}:${prev?.blockType}`);
1755
+ }
1756
+ if (this.isEgCleanupElseBlock(node, rewritten[rewritten.length - 1])) {
1757
+ continue;
1758
+ }
1759
+ if (this.isPrepReraiseBlock(node)) {
1760
+ continue;
1761
+ }
1762
+ if (node instanceof AST.ASTCondBlock &&
1763
+ node.blockType === AST.ASTBlock.BlockType.Except &&
1764
+ (!node.nodes || node.nodes.every(ch => {
1765
+ const frag = ch?.codeFragment?.()?.toString?.() || "";
1766
+ return frag === "pass" || frag.includes("__exception__") || frag.includes("##ERROR##");
1767
+ }))) {
1768
+ continue;
1769
+ }
1770
+ if (typeof node?.codeFragment === 'function') {
1771
+ const frag = node.codeFragment();
1772
+ const fragStr = frag?.toString?.() || "";
1773
+ if (fragStr.includes("##ERROR##")) {
1774
+ continue;
1775
+ }
1776
+ }
1777
+ rewritten.push(node);
1778
+ }
1779
+
1780
+ const flattened = rewritten.map(n => {
1781
+ if (n instanceof AST.ASTBlock &&
1782
+ n.blockType === AST.ASTBlock.BlockType.Try &&
1783
+ n.nodes?.length === 1 &&
1784
+ n.nodes[0] instanceof AST.ASTBlock &&
1785
+ n.nodes[0].blockType === AST.ASTBlock.BlockType.Try) {
1786
+ const inner = n.nodes[0];
1787
+ const hasHandlers = (inner.nodes || []).some(ch => ch instanceof AST.ASTCondBlock && ch.blockType === AST.ASTBlock.BlockType.Except);
1788
+ if (!hasHandlers) {
1789
+ return inner;
1790
+ }
1791
+ }
1792
+ return n;
1793
+ });
1794
+
1795
+ const deduped = [];
1796
+ const seenHandlers = new Set();
1797
+ for (const node of flattened) {
1798
+ if (node instanceof AST.ASTCondBlock && node.blockType === AST.ASTBlock.BlockType.Except) {
1799
+ const condStr = node.condition?.codeFragment?.()?.toString?.() || "";
1800
+ const key = `${node.isExceptStar ? "star" : "plain"}:${condStr}`;
1801
+ if (seenHandlers.has(key)) {
1802
+ continue;
1803
+ }
1804
+ seenHandlers.add(key);
1805
+ }
1806
+ deduped.push(node);
1807
+ }
1808
+
1809
+ listNode.list.length = 0;
1810
+ Array.prototype.push.apply(listNode.list, deduped);
1811
+ }
1812
+
1813
+ rewriteGenericWrappers(root) {
1814
+ if (!(root instanceof AST.ASTNodeList)) {
1815
+ return;
1816
+ }
1817
+ for (let i = 0; i < root.list.length; i++) {
1818
+ const node = root.list[i];
1819
+ if (!(node instanceof AST.ASTStore)) {
1820
+ continue;
1821
+ }
1822
+ const call = node.src;
1823
+ if (!(call instanceof AST.ASTCall)) {
1824
+ continue;
1825
+ }
1826
+ const func = call.func;
1827
+ if (!(func instanceof AST.ASTFunction)) {
1828
+ continue;
1829
+ }
1830
+ const wrapperName = func.code?.object?.Name || "";
1831
+ if (!wrapperName.startsWith("<generic parameters of ")) {
1832
+ continue;
1833
+ }
1834
+
1835
+ // If the call already returns a function/class directly, unwrap it
1836
+ if (func.typeParams?.length || func.annotations && Object.keys(func.annotations).length) {
1837
+ node.m_src = func;
1838
+ continue;
1839
+ }
1840
+
1841
+ const targetName = node.dest?.name || node.dest?.codeFragment?.();
1842
+ const codeConsts = func.code?.object?.Consts?.Value || [];
1843
+ const innerCodeObj = codeConsts.find(c => c?.ClassName === 'Py_CodeObject');
1844
+ if (!innerCodeObj) {
1845
+ continue;
1846
+ }
1847
+ // Extract type parameters: pick uppercase-like strings excluding target name
1848
+ const typeParams = codeConsts
1849
+ .filter(c => c?.ClassName === 'Py_String')
1850
+ .map(c => c.Value)
1851
+ .filter(v => /^[A-Z][A-Za-z0-9_]*$/.test(v || '') && v !== targetName);
1852
+
1853
+ // Decompile inner code object to get body
1854
+ const innerDecompiler = new PycDecompiler(innerCodeObj);
1855
+ const innerBody = innerDecompiler.decompile();
1856
+ innerCodeObj.SourceCode = innerBody;
1857
+ const astObj = new AST.ASTObject(innerCodeObj);
1858
+
1859
+ const isClassLike = targetName && targetName[0] === targetName[0]?.toUpperCase?.();
1860
+
1861
+ if (!isClassLike) {
1862
+ const fn = new AST.ASTFunction(astObj);
1863
+ fn.annotations = innerDecompiler.funcAnnotations || fn.annotations;
1864
+ fn.typeParams = typeParams;
1865
+ node.m_src = fn;
1866
+ } else {
1867
+ const classFunc = new AST.ASTFunction(astObj);
1868
+ const bases = new AST.ASTTuple([]);
1869
+ const cls = new AST.ASTClass(classFunc, bases, new AST.ASTName(targetName));
1870
+ cls.typeParams = typeParams;
1871
+ node.m_src = cls;
1872
+ }
1873
+ }
1874
+
1875
+ // Simplify calls that simply wrap a generic wrapper function
1876
+ for (let i = 0; i < root.list.length; i++) {
1877
+ const node = root.list[i];
1878
+ if (node instanceof AST.ASTStore && node.src instanceof AST.ASTCall && node.src.func instanceof AST.ASTFunction && (node.src.pparams?.length || 0) === 0) {
1879
+ node.m_src = node.src.func;
1880
+ }
1881
+ }
1882
+ }
1883
+
1884
+ isExceptStarBlock(node) {
1885
+ return node instanceof AST.ASTCondBlock && node.blockType == AST.ASTBlock.BlockType.Except && node.isExceptStar;
1886
+ }
1887
+
1888
+ isPlainExceptBlock(node) {
1889
+ return node instanceof AST.ASTCondBlock && node.blockType == AST.ASTBlock.BlockType.Except;
1890
+ }
1891
+
1892
+ isEgHoistableSetupNode(node) {
1893
+ if (!(node instanceof AST.ASTStore)) {
1894
+ return false;
1895
+ }
1896
+ if (!(node.dest instanceof AST.ASTName)) {
1897
+ return false;
1898
+ }
1899
+ return node.src instanceof AST.ASTNone ||
1900
+ node.src instanceof AST.ASTObject ||
1901
+ node.src instanceof AST.ASTFunction;
1902
+ }
1903
+
1904
+ /**
1905
+ * Recursively checks if a block ends with a terminating keyword (break/continue/return)
1906
+ * Used to prevent generating additional continue statements after breaks in nested blocks
1907
+ */
1908
+ hasTerminatingKeyword(block) {
1909
+ if (!block || !block.nodes || block.nodes.length == 0) {
1910
+ return false;
1911
+ }
1912
+
1913
+ let lastNode = block.nodes[block.nodes.length - 1];
1914
+
1915
+ // Check if last node is a terminating keyword
1916
+ if (lastNode instanceof AST.ASTKeyword) {
1917
+ return [AST.ASTKeyword.Word.Break,
1918
+ AST.ASTKeyword.Word.Continue,
1919
+ AST.ASTKeyword.Word.Return].includes(lastNode.word);
1920
+ }
1921
+
1922
+ // Check if last node is a block - recurse into it
1923
+ if (lastNode instanceof AST.ASTBlock) {
1924
+ return this.hasTerminatingKeyword(lastNode);
1925
+ }
1926
+
1927
+ // Check if last node is a return statement
1928
+ if (lastNode instanceof AST.ASTReturn) {
1929
+ return true;
1930
+ }
1931
+
1932
+ return false;
1933
+ }
1934
+
1935
+ /**
1936
+ * Look ahead from current COPY after LOAD to detect match pattern
1937
+ * Strategy: Find next POP_JUMP_IF_FALSE, check if its target is another COPY
1938
+ * This confirms match/case pattern before first case is processed
1939
+ */
1940
+ lookAheadForMatchPattern() {
1941
+ // Match patterns only exist in Python 3.10+
1942
+ if (this.object.Reader.versionCompare(3, 10) < 0) {
1943
+ return false;
1944
+ }
1945
+ if (!this.code.Next) {
1946
+ if (global.g_cliArgs?.debug) {
1947
+ console.log(`[LOOK-AHEAD] No next instruction`);
1948
+ }
1949
+ return false;
1950
+ }
1951
+
1952
+ // Find next POP_JUMP_IF_FALSE opcode (should be within ~10 instructions for literal patterns)
1953
+ let nextOp = this.code.Next;
1954
+ let jumpOp = null;
1955
+
1956
+ for (let i = 0; i < 10 && nextOp; i++) {
1957
+ if (nextOp.OpCodeID == this.OpCodes.POP_JUMP_IF_FALSE_A ||
1958
+ nextOp.OpCodeID == this.OpCodes.POP_JUMP_FORWARD_IF_FALSE_A) {
1959
+ jumpOp = nextOp;
1960
+ break;
1961
+ }
1962
+ nextOp = nextOp.Next;
1963
+ }
1964
+
1965
+ if (!jumpOp) {
1966
+ if (global.g_cliArgs?.debug) {
1967
+ console.log(`[LOOK-AHEAD] No POP_JUMP found within 10 instructions`);
1968
+ }
1969
+ return false;
1970
+ }
1971
+
1972
+ if (jumpOp.JumpTarget === undefined || jumpOp.JumpTarget === null) {
1973
+ if (global.g_cliArgs?.debug) {
1974
+ console.log(`[LOOK-AHEAD] POP_JUMP has no jump target`);
1975
+ }
1976
+ return false;
1977
+ }
1978
+
1979
+ if (global.g_cliArgs?.debug) {
1980
+ console.log(`[LOOK-AHEAD] Found POP_JUMP at offset ${jumpOp.Offset}, target=${jumpOp.JumpTarget}`);
1981
+ }
1982
+
1983
+ // Find instruction at or after jump target offset
1984
+ // Note: JumpTarget might not match exact instruction offset due to cache instructions
1985
+ let targetOp = jumpOp.Next;
1986
+
1987
+ // Navigate forward from POP_JUMP to find instruction at target
1988
+ // Look for next COPY (should be within reasonable distance ~40-50 bytes)
1989
+ let searchLimit = 100; // Don't search too far
1990
+ let searchCount = 0;
1991
+
1992
+ while (targetOp && searchCount < searchLimit) {
1993
+ // Found a COPY opcode?
1994
+ if (targetOp.OpCodeID == this.OpCodes.DUP_TOP ||
1995
+ (targetOp.OpCodeID == this.OpCodes.COPY_A && targetOp.Argument == 1)) {
1996
+
1997
+ if (global.g_cliArgs?.debug) {
1998
+ console.log(`[LOOK-AHEAD] Found COPY at offset ${targetOp.Offset} after POP_JUMP`);
1999
+ }
2000
+
2001
+ // Check if this COPY is preceded by LOAD (if yes, not a match pattern)
2002
+ let prevOp = targetOp.Prev;
2003
+ if (prevOp && (prevOp.OpCodeID == this.OpCodes.LOAD_FAST_A ||
2004
+ prevOp.OpCodeID == this.OpCodes.LOAD_NAME_A ||
2005
+ prevOp.OpCodeID == this.OpCodes.LOAD_GLOBAL_A)) {
2006
+ // This COPY is after LOAD → not the reuse pattern
2007
+ if (global.g_cliArgs?.debug) {
2008
+ console.log(`[LOOK-AHEAD] COPY at ${targetOp.Offset} has LOAD before → not match pattern`);
2009
+ }
2010
+ return false;
2011
+ }
2012
+
2013
+ // This COPY has no LOAD before → match pattern!
2014
+ if (global.g_cliArgs?.debug) {
2015
+ console.log(`[LOOK-AHEAD] Match pattern detected! COPY at ${targetOp.Offset} has no prior LOAD`);
2016
+ }
2017
+ return true;
2018
+ }
2019
+
2020
+ targetOp = targetOp.Next;
2021
+ searchCount++;
2022
+ }
2023
+
2024
+ if (global.g_cliArgs?.debug) {
2025
+ console.log(`[LOOK-AHEAD] No COPY found after POP_JUMP within ${searchLimit} bytes`);
2026
+ }
2027
+ return false;
2028
+ }
2029
+ }
2030
+
2031
+ module.exports = PycDecompiler;