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,1201 @@
1
+ const AST = require('../ast/ast_node');
2
+
3
+ /**
4
+ * Reconstruct pattern from recorded operations
5
+ * @param {Array} patternOps - Array of pattern operations
6
+ * @returns {{pattern: ASTPattern, remainderOps: Array}} pattern plus leftover operations
7
+ */
8
+ function reconstructPattern(patternOps) {
9
+ const ops = patternOps || [];
10
+ const consumed = new Set();
11
+
12
+ const markConsumed = (op) => {
13
+ if (op) {
14
+ consumed.add(op);
15
+ }
16
+ };
17
+
18
+ const buildResult = (pattern) => {
19
+ let remainderOps = ops.filter(op => !consumed.has(op));
20
+
21
+ // AS-pattern: leftover single STORE_FAST wraps existing pattern
22
+ const asStore = remainderOps.find(op => op.type === 'STORE_FAST');
23
+ if (asStore && pattern) {
24
+ markConsumed(asStore);
25
+ pattern = new AST.ASTPattern(AST.ASTPattern.PatternType.As, {
26
+ pattern,
27
+ name: asStore.name
28
+ });
29
+ remainderOps = ops.filter(op => !consumed.has(op));
30
+ }
31
+
32
+ return {pattern, remainderOps};
33
+ };
34
+
35
+ if (ops.length === 0) {
36
+ return buildResult(new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_'));
37
+ }
38
+
39
+ // Find key operations
40
+ let matchSeqOp = ops.find(op => op.type === 'MATCH_SEQUENCE');
41
+ let hasMatchSeq = !!matchSeqOp;
42
+ let matchClassOp = ops.find(op => op.type === 'MATCH_CLASS');
43
+ let matchKeysOp = ops.find(op => op.type === 'MATCH_KEYS');
44
+ let matchMappingOp = ops.find(op => op.type === 'MATCH_MAPPING');
45
+ let unpackOp = ops.find(op => op.type === 'UNPACK_SEQUENCE');
46
+ let compareOp = ops.find(op => op.type === 'COMPARE');
47
+ let getLenOpIndex = ops.findIndex(op => op.type === 'GET_LEN');
48
+
49
+ // In 3.13+, MATCH_SEQUENCE is preceded by GET_LEN/COMPARE length checks.
50
+ if (hasMatchSeq && getLenOpIndex >= 0) {
51
+ markConsumed(ops[getLenOpIndex]);
52
+ const cmpAfterLen = ops.slice(getLenOpIndex + 1).find(op => op.type === 'COMPARE');
53
+ if (cmpAfterLen) {
54
+ markConsumed(cmpAfterLen);
55
+ }
56
+ }
57
+
58
+ if (matchKeysOp || matchMappingOp) {
59
+ if (matchKeysOp) {
60
+ markConsumed(matchKeysOp);
61
+ }
62
+ if (matchMappingOp) {
63
+ markConsumed(matchMappingOp);
64
+ }
65
+
66
+ const keys = matchKeysOp?.keys || [];
67
+ const stores = ops.filter(op => op.type === 'STORE_FAST');
68
+ const entries = [];
69
+
70
+ let idx = 0;
71
+ for (; idx < keys.length; idx++) {
72
+ const keyNode = keys[idx];
73
+ let pattern = null;
74
+ if (stores[idx]) {
75
+ pattern = new AST.ASTPattern(AST.ASTPattern.PatternType.Variable, stores[idx].name);
76
+ markConsumed(stores[idx]);
77
+ } else {
78
+ pattern = new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_');
79
+ }
80
+ entries.push({key: keyNode, pattern});
81
+ }
82
+
83
+ // Consume any extra stores associated with mapping binding
84
+ for (; idx < stores.length; idx++) {
85
+ markConsumed(stores[idx]);
86
+ }
87
+
88
+ return buildResult(new AST.ASTPattern(
89
+ AST.ASTPattern.PatternType.Mapping,
90
+ entries
91
+ ));
92
+ }
93
+
94
+ if (matchClassOp) {
95
+ markConsumed(matchClassOp);
96
+ const attributes = [];
97
+ const attrNames = matchClassOp.attrNames || [];
98
+
99
+ // Determine how many positional attributes are expected (fallback to opcode count)
100
+ let attrCount = attrNames.length;
101
+ if (!attrCount && matchClassOp.count) {
102
+ attrCount = matchClassOp.count;
103
+ }
104
+
105
+ // Focus on operations that happen after UNPACK_SEQUENCE (if present)
106
+ let attrOpsStart = ops.indexOf(matchClassOp) + 1;
107
+ let attrOps = ops.slice(attrOpsStart);
108
+ const unpackIndex = attrOps.findIndex(op => op.type === 'UNPACK_SEQUENCE');
109
+ if (unpackIndex >= 0) {
110
+ attrOps = attrOps.slice(unpackIndex + 1);
111
+ if (unpackOp) {
112
+ markConsumed(unpackOp);
113
+ }
114
+ }
115
+
116
+ // Handle SWAP 2 which reverses attribute evaluation order
117
+ let reverseOrder = false;
118
+ if (attrOps.length && attrOps[0].type === 'SWAP' && attrOps[0].depth === 2) {
119
+ markConsumed(attrOps[0]);
120
+ reverseOrder = true;
121
+ attrOps = attrOps.slice(1);
122
+ }
123
+
124
+ // Collect COMPARE / STORE_FAST operations that correspond to attributes
125
+ const attributeOps = [];
126
+ for (const op of attrOps) {
127
+ if (op.type === 'COMPARE' || op.type === 'STORE_FAST') {
128
+ attributeOps.push(op);
129
+ markConsumed(op);
130
+ }
131
+ if (attrCount && attributeOps.length >= attrCount) {
132
+ break;
133
+ }
134
+ }
135
+
136
+ if (reverseOrder) {
137
+ attributeOps.reverse();
138
+ }
139
+
140
+ let attrIndex = 0;
141
+ for (const op of attributeOps) {
142
+ if (attrCount && attrIndex >= attrCount) {
143
+ break;
144
+ }
145
+
146
+ if (op.type === 'COMPARE') {
147
+ const literalPattern = new AST.ASTPattern(
148
+ AST.ASTPattern.PatternType.Literal,
149
+ op.right
150
+ );
151
+ const name = attrNames.length ? (attrNames[attrIndex] || `_attr${attrIndex}`) : null;
152
+ attributes.push({name, pattern: literalPattern});
153
+ } else if (op.type === 'STORE_FAST') {
154
+ const varPattern = new AST.ASTPattern(
155
+ AST.ASTPattern.PatternType.Variable,
156
+ op.name
157
+ );
158
+ const name = attrNames.length ? (attrNames[attrIndex] || op.name || `_attr${attrIndex}`) : null;
159
+ attributes.push({name, pattern: varPattern});
160
+ }
161
+
162
+ attrIndex++;
163
+ }
164
+
165
+ // If there are declared attributes without recorded ops, fill them with wildcards
166
+ while (attrIndex < attrNames.length) {
167
+ attributes.push({
168
+ name: attrNames[attrIndex],
169
+ pattern: new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_')
170
+ });
171
+ attrIndex++;
172
+ }
173
+
174
+ return buildResult(new AST.ASTPattern(
175
+ AST.ASTPattern.PatternType.Class,
176
+ {
177
+ classExpr: matchClassOp.classExpr,
178
+ attributes
179
+ }
180
+ ));
181
+ }
182
+
183
+ // Literal OR pattern: multiple COMPARE_OP checks in a row
184
+ const allCompareOps = ops.filter(op => op.type === 'COMPARE');
185
+ if (!hasMatchSeq && allCompareOps.length > 1) {
186
+ const orPatterns = [];
187
+ for (const cmp of allCompareOps) {
188
+ markConsumed(cmp);
189
+ if (cmp.right instanceof AST.ASTObject) {
190
+ orPatterns.push(new AST.ASTPattern(AST.ASTPattern.PatternType.Literal, cmp.right));
191
+ } else if (cmp.right instanceof AST.ASTName) {
192
+ orPatterns.push(new AST.ASTPattern(AST.ASTPattern.PatternType.Variable, cmp.right.name));
193
+ } else {
194
+ orPatterns.push(new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_'));
195
+ }
196
+ }
197
+ return buildResult(new AST.ASTPattern(AST.ASTPattern.PatternType.Or, orPatterns));
198
+ }
199
+
200
+ // Literal pattern: Just COMPARE (no MATCH_SEQUENCE)
201
+ if (!hasMatchSeq && compareOp) {
202
+ markConsumed(compareOp);
203
+ // Pure literal pattern: case 1: or case "string":
204
+ // right operand is the literal value
205
+ if (compareOp.right instanceof AST.ASTObject) {
206
+ return buildResult(new AST.ASTPattern(AST.ASTPattern.PatternType.Literal, compareOp.right));
207
+ }
208
+ // Fallback
209
+ return buildResult(new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_'));
210
+ }
211
+
212
+ if (!hasMatchSeq) {
213
+ // No MATCH_SEQUENCE and no COMPARE → wildcard
214
+ return buildResult(new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_'));
215
+ }
216
+
217
+ if (!unpackOp) {
218
+ // MATCH_SEQUENCE but no UNPACK → wildcard sequence match
219
+ return buildResult(new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_'));
220
+ }
221
+
222
+ // Build sequence pattern with unpacked elements
223
+ let count = unpackOp.count;
224
+ let elements = [];
225
+
226
+ // Find element patterns after UNPACK_SEQUENCE
227
+ // Next COMPARE or STORE_FAST operations define elements
228
+ let unpackIndex = ops.findIndex(op => op.type === 'UNPACK_SEQUENCE');
229
+ let afterUnpack = ops.slice(unpackIndex + 1);
230
+
231
+ // Check for SWAP operation (indicates element order reversal)
232
+ let hasSwap = afterUnpack.length > 0 && afterUnpack[0].type === 'SWAP' && afterUnpack[0].depth === 2;
233
+
234
+ // Skip SWAP when processing elements
235
+ if (hasSwap) {
236
+ markConsumed(afterUnpack[0]);
237
+ afterUnpack = afterUnpack.slice(1);
238
+ }
239
+
240
+ // Process operations to extract element patterns
241
+ let elementIndex = 0;
242
+ for (let i = 0; i < afterUnpack.length && elementIndex < count; i++) {
243
+ let op = afterUnpack[i];
244
+
245
+ if (op.type === 'COMPARE') {
246
+ // Literal pattern: value is compared
247
+ // right is ASTObject with the literal value
248
+ if (op.right instanceof AST.ASTObject) {
249
+ let literalValue = op.right;
250
+ elements.push(new AST.ASTPattern(AST.ASTPattern.PatternType.Literal, literalValue));
251
+ markConsumed(op);
252
+ elementIndex++;
253
+ }
254
+ } else if (op.type === 'STORE_FAST') {
255
+ // Variable pattern: value is captured
256
+ let varName = op.name;
257
+ elements.push(new AST.ASTPattern(AST.ASTPattern.PatternType.Variable, varName));
258
+ markConsumed(op);
259
+ elementIndex++;
260
+ }
261
+ }
262
+
263
+ // Fill remaining with wildcards if needed
264
+ while (elements.length < count) {
265
+ elements.push(new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_'));
266
+ }
267
+
268
+ // If SWAP was used, reverse element order
269
+ // SWAP 2 after UNPACK indicates elements were swapped to change check order
270
+ // We need to reverse to restore source pattern order
271
+ if (hasSwap) {
272
+ elements.reverse();
273
+ }
274
+
275
+ // Create sequence pattern
276
+ markConsumed(matchSeqOp);
277
+ markConsumed(unpackOp);
278
+ return buildResult(new AST.ASTPattern(AST.ASTPattern.PatternType.Sequence, elements));
279
+ }
280
+
281
+ function hasUpcomingMatchCase() {
282
+ const nextOpCodeId = this.code.Next?.OpCodeID;
283
+ const nextNextOpCodeId = this.code.Next?.Next?.OpCodeID;
284
+ const matchOpcodes = [
285
+ this.OpCodes.MATCH_SEQUENCE,
286
+ this.OpCodes.MATCH_MAPPING,
287
+ this.OpCodes.MATCH_KEYS,
288
+ this.OpCodes.MATCH_CLASS_A
289
+ ];
290
+
291
+ if (matchOpcodes.includes(nextOpCodeId)) {
292
+ return true;
293
+ }
294
+
295
+ let isCopy = (nextOpCodeId == this.OpCodes.COPY_A && this.code.Next?.Argument == 1) ||
296
+ nextOpCodeId == this.OpCodes.DUP_TOP;
297
+ if (isCopy) {
298
+ return true;
299
+ }
300
+
301
+ let nextIsPop = nextOpCodeId == this.OpCodes.POP_TOP;
302
+ let nextNextIsCopy = nextNextOpCodeId == this.OpCodes.COPY_A;
303
+ let nextNextIsMatch = nextNextOpCodeId == this.OpCodes.MATCH_SEQUENCE;
304
+ if (nextIsPop && (nextNextIsCopy || nextNextIsMatch)) {
305
+ return true;
306
+ }
307
+
308
+ // NOP followed by code (no COMPARE_OP) = default case (case _:)
309
+ // This pattern appears in 3.10/3.11 after the last literal case
310
+ if (nextOpCodeId == this.OpCodes.NOP && this.currentMatch) {
311
+ // Check if this NOP is followed by executable code (not another pattern)
312
+ let afterNop = this.code.Next?.Next;
313
+ if (afterNop && afterNop.OpCodeID != this.OpCodes.COMPARE_OP_A &&
314
+ afterNop.OpCodeID != this.OpCodes.COPY_A &&
315
+ afterNop.OpCodeID != this.OpCodes.DUP_TOP) {
316
+ return true; // Default case ahead
317
+ }
318
+ }
319
+
320
+ // 3.13 literal-only match compilation: sequences of CACHE/LOAD_CONST/COMPARE_OP
321
+ let scan = this.code.Next;
322
+ let steps = 0;
323
+ while (scan && steps < 8) {
324
+ if (scan.OpCodeID == this.OpCodes.COMPARE_OP_A) {
325
+ return true;
326
+ }
327
+ if (scan.OpCodeID == this.OpCodes.LOAD_CONST_A ||
328
+ scan.OpCodeID == this.OpCodes.CACHE ||
329
+ scan.OpCodeID == this.OpCodes.COPY_A ||
330
+ scan.OpCodeID == this.OpCodes.POP_JUMP_IF_FALSE_A) {
331
+ scan = scan.Next;
332
+ steps++;
333
+ continue;
334
+ }
335
+ break;
336
+ }
337
+
338
+ let look = this.code.Next;
339
+ steps = 0;
340
+ while (look && steps < 50) {
341
+ if (matchOpcodes.includes(look.OpCodeID) || look.OpCodeID == this.OpCodes.POP_TOP) {
342
+ return true;
343
+ }
344
+ look = look.Next;
345
+ steps++;
346
+ }
347
+
348
+ return false;
349
+ }
350
+
351
+ function flushCurrentCaseBody() {
352
+ if (!this.currentCase) {
353
+ return false;
354
+ }
355
+
356
+ let startIdx = this.caseBodyStartIndex || 0;
357
+ let bodyNodes = this.curBlock.nodes.slice(startIdx);
358
+ const {guard, bodyNodes: normalized} = extractGuardFromBody(bodyNodes);
359
+ const filtered = normalized.filter(node => !(node instanceof AST.ASTName) && !(node instanceof AST.ASTTuple));
360
+
361
+ if (global.g_cliArgs?.debug) {
362
+ const debugNodes = filtered.map(n => `${n?.constructor?.name || typeof n}:${n?.codeFragment ? n.codeFragment() : ''}`);
363
+ console.log(`[MATCH] Case nodes: ${debugNodes.join(' | ')}`);
364
+ if (guard) {
365
+ console.log(`[MATCH] Guard detected: ${guard.codeFragment?.() || guard.constructor?.name}`);
366
+ }
367
+ }
368
+
369
+ let body = new AST.ASTNodeList(filtered);
370
+ this.currentCase.m_body = body;
371
+ if (guard) {
372
+ this.currentCase.m_guard = guard;
373
+ }
374
+ this.currentMatch.addCase(this.currentCase);
375
+ this.curBlock.nodes.length = startIdx;
376
+ this.currentCase = null;
377
+
378
+ if (global.g_cliArgs?.debug) {
379
+ console.log(`[MATCH] Case body recorded with ${filtered.length} node(s)`);
380
+ }
381
+
382
+ return true;
383
+ }
384
+
385
+ function beginMatchCaseFromPattern(options = {}) {
386
+ if (!this.currentMatch) {
387
+ return false;
388
+ }
389
+
390
+ // Close the previous case body when a new case starts
391
+ flushCurrentCaseBody.call(this);
392
+
393
+ const {pattern, remainderOps} = reconstructPattern(this.patternOps);
394
+ this.caseBodyStartIndex = this.curBlock?.nodes?.length || 0;
395
+
396
+ const initialGuard = guardFromPatternRemainder(remainderOps);
397
+ this.currentCase = new AST.ASTCase(pattern, null, initialGuard);
398
+ this.currentCase.line = options.line || this.code.Current.LineNo;
399
+ this.inMatchPattern = false;
400
+ this.patternOps = [];
401
+
402
+ if (global.g_cliArgs?.debug) {
403
+ console.log(`[MATCH] Starting case (${options.reason || 'pop_top'}) at offset ${this.code.Current.Offset}`);
404
+ console.log(` caseBodyStartIndex=${this.caseBodyStartIndex}`);
405
+ }
406
+
407
+ return true;
408
+ }
409
+
410
+ function finalizeMatchCase() {
411
+ if (!this.currentMatch) {
412
+ return false;
413
+ }
414
+
415
+ flushCurrentCaseBody.call(this);
416
+
417
+ // Normalize guard-only cases that lost their bodies due to literal/guard separation
418
+ const normalizeMatchCases = (matchNode) => {
419
+ if (!matchNode?.cases || matchNode.cases.length < 2) {
420
+ return;
421
+ }
422
+ const wildcardType = AST.ASTPattern.PatternType.Wildcard;
423
+ const cleaned = [];
424
+ for (let i = 0; i < matchNode.cases.length; i++) {
425
+ const cur = matchNode.cases[i];
426
+ const next = matchNode.cases[i + 1];
427
+ const curBodyLen = cur.body?.list?.length || 0;
428
+ const curHasGuard = !!cur.guard;
429
+ const nextIsWildcard = next && next.pattern?.type === wildcardType && !next.guard;
430
+ if (curHasGuard && curBodyLen === 0 && nextIsWildcard) {
431
+ // Move body from wildcard fallback into guarded case
432
+ cur.m_body = next.body;
433
+ i++; // Skip the wildcard case
434
+ }
435
+ // Skip empty wildcard cases (no guard, no body)
436
+ if (cur.pattern?.type === wildcardType && !cur.guard && curBodyLen === 0) {
437
+ continue;
438
+ }
439
+ cleaned.push(cur);
440
+ }
441
+ matchNode.m_cases = cleaned;
442
+ };
443
+ normalizeMatchCases(this.currentMatch);
444
+
445
+ if (global.g_cliArgs?.debug) {
446
+ console.log(`[MATCH] Match complete, appending ASTMatch to parent block`);
447
+ }
448
+
449
+ if (this.dataStack.length > 0 && this.dataStack.top() === this.matchSubject) {
450
+ this.dataStack.pop();
451
+ }
452
+
453
+ let targetBlock = this.matchParentBlock || this.curBlock;
454
+
455
+ const preIndex = Math.max(0, this.matchPreNodesStart || 0);
456
+ targetBlock.nodes.length = preIndex;
457
+ targetBlock.append(this.currentMatch);
458
+ this.currentMatch = null;
459
+ this.matchSubject = null;
460
+ this.matchParentBlock = null;
461
+ this.matchPreNodesStart = 0;
462
+ this.caseBodyStartIndex = 0;
463
+ return true;
464
+ }
465
+
466
+ function handleExecStmt() {
467
+ if (this.dataStack.top() instanceof AST.ASTChainStore) {
468
+ this.dataStack.pop();
469
+ }
470
+ let loc = this.dataStack.pop();
471
+ let glob = this.dataStack.pop();
472
+ let stmt = this.dataStack.pop();
473
+ let node = new AST.ASTExec(stmt, glob, loc);
474
+ node.line = this.code.Current.LineNo;
475
+ this.curBlock.append(node);
476
+ }
477
+
478
+ function handleFormatValueA() {
479
+ let conversion_flag = this.code.Current.Argument;
480
+ switch (conversion_flag) {
481
+ case AST.ASTFormattedValue.ConversionFlag.None:
482
+ case AST.ASTFormattedValue.ConversionFlag.Str:
483
+ case AST.ASTFormattedValue.ConversionFlag.Repr:
484
+ case AST.ASTFormattedValue.ConversionFlag.ASCII:
485
+ {
486
+ let val = this.dataStack.pop();
487
+ let node = new AST.ASTFormattedValue (val, conversion_flag, null);
488
+ node.line = this.code.Current.LineNo;
489
+ this.dataStack.push(node);
490
+ }
491
+ break;
492
+ case AST.ASTFormattedValue.ConversionFlag.FmtSpec:
493
+ {
494
+ let format_spec = this.dataStack.pop();
495
+ let val = this.dataStack.pop();
496
+ let node = new AST.ASTFormattedValue (val, conversion_flag, format_spec);
497
+ node.line = this.code.Current.LineNo;
498
+ this.dataStack.push(node);
499
+ }
500
+ break;
501
+ default:
502
+ console.error(`Unsupported FORMAT_VALUE_A conversion flag: ${this.code.Current.Argument}\n`);
503
+ }
504
+ }
505
+
506
+ function hasUpcomingStringBuild(context) {
507
+ const lookahead = [
508
+ context.OpCodes.BUILD_STRING_A,
509
+ context.OpCodes.BUILD_INTERPOLATION_A,
510
+ context.OpCodes.BUILD_TEMPLATE
511
+ ];
512
+ const earlyStops = new Set([
513
+ context.OpCodes.RETURN_VALUE,
514
+ context.OpCodes.CALL_A,
515
+ context.OpCodes.CALL_KW_A,
516
+ context.OpCodes.CALL_FUNCTION_EX,
517
+ context.OpCodes.STORE_FAST_A,
518
+ context.OpCodes.STORE_NAME_A,
519
+ context.OpCodes.STORE_GLOBAL_A,
520
+ context.OpCodes.STORE_ATTR_A,
521
+ context.OpCodes.STORE_DEREF_A
522
+ ]);
523
+
524
+ for (let step = 1; step <= 12; step++) {
525
+ const instr = context.code.PeekInstructionAtOffset(context.code.Current.Offset + step * 2);
526
+ if (!instr) {
527
+ break;
528
+ }
529
+ if (lookahead.includes(instr.OpCodeID)) {
530
+ return true;
531
+ }
532
+ if (earlyStops.has(instr.OpCodeID)) {
533
+ break;
534
+ }
535
+ }
536
+ return false;
537
+ }
538
+
539
+ function handleFormatSimple() {
540
+ // Python 3.13+ FORMAT_SIMPLE: format TOS without spec (conversion may have been applied by CONVERT_VALUE)
541
+ const val = this.dataStack.pop();
542
+
543
+ let node;
544
+ if (val instanceof AST.ASTFormattedValue) {
545
+ node = val;
546
+ } else {
547
+ node = new AST.ASTFormattedValue(
548
+ val,
549
+ AST.ASTFormattedValue.ConversionFlag.None,
550
+ null
551
+ );
552
+ node.line = this.code.Current.LineNo;
553
+ }
554
+
555
+ if (!hasUpcomingStringBuild(this)) {
556
+ const joined = new AST.ASTJoinedStr([node]);
557
+ joined.line = node.line;
558
+ this.dataStack.push(joined);
559
+ return;
560
+ }
561
+
562
+ this.dataStack.push(node);
563
+ }
564
+
565
+ function handleFormatWithSpec() {
566
+ // Python 3.14 FORMAT_WITH_SPEC: value already converted (optional) + format spec on stack.
567
+ const formatSpec = this.dataStack.pop();
568
+ const val = this.dataStack.pop();
569
+
570
+ let target;
571
+ if (val instanceof AST.ASTFormattedValue) {
572
+ target = val;
573
+ } else {
574
+ target = new AST.ASTFormattedValue(val, AST.ASTFormattedValue.ConversionFlag.None, null);
575
+ target.line = this.code.Current.LineNo;
576
+ }
577
+ target.m_format_spec = formatSpec;
578
+ if (!hasUpcomingStringBuild(this)) {
579
+ const joined = new AST.ASTJoinedStr([target]);
580
+ joined.line = target.line;
581
+ this.dataStack.push(joined);
582
+ return;
583
+ }
584
+
585
+ this.dataStack.push(target);
586
+ }
587
+
588
+ function handlePopTop() {
589
+ // Match/case: Detect transition from pattern checks to case body
590
+ const readyForWildcard = this.currentMatch && !this.inMatchPattern && (this.patternOps?.length || 0) === 0 && !hasUpcomingMatchCase.call(this);
591
+ if (this.inMatchPattern || readyForWildcard) {
592
+ if (global.g_cliArgs?.debug) {
593
+ console.log(`[POP_TOP] Evaluating match state at offset ${this.code.Current.Offset}, prev=${this.code.Prev?.InstructionName}`);
594
+ }
595
+ // Check if this is the success path (pattern matched)
596
+ // Two scenarios:
597
+ // 1. Sequence patterns: POP_JUMP → UNPACK/etc → POP_TOP (Prev is NOT jump)
598
+ // 2. Literal patterns: POP_JUMP → POP_TOP (Prev IS jump)
599
+ let prevIsJump = [
600
+ this.OpCodes.POP_JUMP_IF_FALSE_A,
601
+ this.OpCodes.POP_JUMP_FORWARD_IF_FALSE_A,
602
+ this.OpCodes.POP_JUMP_IF_TRUE_A,
603
+ this.OpCodes.JUMP_IF_FALSE_A
604
+ ].includes(this.code.Prev?.OpCodeID);
605
+
606
+ const patternHasOps = (this.patternOps?.length || 0) > 0;
607
+ let isLiteralPattern = patternHasOps && !this.patternOps.some(op => ["MATCH_SEQUENCE","MATCH_CLASS","MATCH_MAPPING","MATCH_KEYS"].includes(op.type));
608
+ let shouldStartCase = readyForWildcard || !!this.inMatchPattern;
609
+ if (patternHasOps) {
610
+ shouldStartCase = true;
611
+ }
612
+ if (patternHasOps && isLiteralPattern && !prevIsJump) {
613
+ shouldStartCase = false;
614
+ }
615
+
616
+ if (shouldStartCase) {
617
+ if (beginMatchCaseFromPattern.call(this, {reason: 'pop_top'})) {
618
+ return;
619
+ }
620
+ } else if (global.g_cliArgs?.debug) {
621
+ console.log(`[POP_TOP] Skipping case start at offset ${this.code.Current.Offset} (literal=${isLiteralPattern}, prevIsJump=${prevIsJump})`);
622
+ console.log(` patternOps=${JSON.stringify(this.patternOps.map(op => op.type))}`);
623
+ }
624
+ // Consume POP_TOP related to match even if no case starts
625
+ if (this.dataStack.length > 0) {
626
+ this.dataStack.pop();
627
+ }
628
+ return;
629
+ }
630
+
631
+ if (!(this.dataStack.top() instanceof AST.ASTComprehension) && [this.OpCodes.JUMP_ABSOLUTE_A, this.OpCodes.JUMP_FORWARD_A, this.OpCodes.POP_JUMP_IF_FALSE_A, this.OpCodes.JUMP_IF_FALSE_A].includes(this.code.Prev?.OpCodeID)) {
632
+ if (this.curBlock.blockType == AST.ASTBlock.BlockType.Except) {
633
+ // Skipping POP_TOP, POP_TOP, POP_TOP
634
+ if ([this.OpCodes.JUMP_ABSOLUTE_A, this.OpCodes.JUMP_FORWARD_A].includes(this.code.Prev.OpCodeID)) {
635
+ if (this.code.Next?.OpCodeID == this.OpCodes.POP_TOP && this.code.Next?.Next?.OpCodeID == this.OpCodes.POP_TOP) {
636
+ this.code.GoNext(2);
637
+ }
638
+ } else if (this.code.Prev.OpCodeID == this.OpCodes.POP_JUMP_IF_FALSE_A) {
639
+ if ([this.OpCodes.STORE_NAME_A, this.OpCodes.STORE_FAST_A].includes(this.code.Next?.OpCodeID) && this.code.Next?.Next?.OpCodeID == this.OpCodes.POP_TOP) {
640
+ let exceptionTypeNode = this.curBlock.condition;
641
+ if (!(exceptionTypeNode instanceof AST.ASTName)) {
642
+ const typeName = exceptionTypeNode?.constructor?.name || 'null';
643
+ if (global.g_cliArgs?.debug) {
644
+ console.error(`Expected ASTName, but got ${typeName}`);
645
+ }
646
+ return;
647
+ }
648
+ let exceptionName = new AST.ASTName(this.code.Next.Name);
649
+ exceptionName.line = this.code.Current.LineNo;
650
+ this.curBlock.condition = new AST.ASTStore(exceptionTypeNode, exceptionName);
651
+ this.code.GoNext(2);
652
+ }
653
+ } else if (this.code.Prev.OpCodeID == this.OpCodes.JUMP_IF_FALSE_A) {
654
+ if ( this.code.Next?.OpCodeID == this.OpCodes.POP_TOP && [this.OpCodes.STORE_NAME_A, this.OpCodes.STORE_FAST_A].includes(this.code.Next?.Next?.OpCodeID) && this.code.Next?.Next?.Next?.OpCodeID == this.OpCodes.POP_TOP) {
655
+ let exceptionTypeNode = this.curBlock.condition;
656
+ if (!(exceptionTypeNode instanceof AST.ASTName)) {
657
+ const typeName = exceptionTypeNode?.constructor?.name || 'null';
658
+ if (global.g_cliArgs?.debug) {
659
+ console.error(`Expected ASTName, but got ${typeName}`);
660
+ }
661
+ return;
662
+ }
663
+ let exceptionName = new AST.ASTName(this.code.Next.Next.Name);
664
+ exceptionName.line = this.code.Current.LineNo;
665
+ this.curBlock.condition = new AST.ASTStore(exceptionTypeNode, exceptionName);
666
+ this.code.GoNext(2);
667
+ }
668
+ }
669
+ }
670
+ return;
671
+ } else if ([this.OpCodes.PRINT_ITEM_TO].includes(this.code.Prev.OpCodeID) && this.curBlock.nodes.top() instanceof AST.ASTPrint) {
672
+ let printNode = this.curBlock.nodes.top();
673
+ if (printNode.stream && printNode.stream == this.dataStack.top()) {
674
+ this.dataStack.pop();
675
+ return;
676
+ }
677
+ }
678
+ let value = this.dataStack.pop();
679
+ if (this.appendExceptionExprs && this.curBlock.blockType == AST.ASTBlock.BlockType.Except) {
680
+ // direct expression in handler (e.g., add_note call result is discarded)
681
+ if (value && !(value instanceof AST.ASTNone)) {
682
+ this.curBlock.append(value);
683
+ }
684
+ return;
685
+ }
686
+ if (!this.curBlock.inited) {
687
+ if (this.curBlock.blockType == AST.ASTBlock.BlockType.With) {
688
+ this.curBlock.expr = value;
689
+ } else if (this.curBlock.blockType == AST.ASTBlock.BlockType.If && !this.curBlock.condition) {
690
+ this.curBlock.condition = value;
691
+ }
692
+ // Don't initialize For/AsyncFor blocks here - they are initialized by STORE_FAST/STORE_NAME when setting the index
693
+ if (this.curBlock.blockType != AST.ASTBlock.BlockType.For &&
694
+ this.curBlock.blockType != AST.ASTBlock.BlockType.AsyncFor) {
695
+ this.curBlock.init();
696
+ }
697
+ } else if (value == null || value.processed) {
698
+ return;
699
+ }
700
+
701
+ if (!(value instanceof AST.ASTObject)) {
702
+ this.curBlock.append(value);
703
+ }
704
+
705
+ if (this.curBlock.blockType == AST.ASTBlock.BlockType.For
706
+ && this.curBlock.comprehension) {
707
+ /* This relies on some really uncertain logic...
708
+ * If it's a comprehension, the only POP_TOP should be
709
+ * a call to append the iter to the list.
710
+ */
711
+ if (value instanceof AST.ASTCall) {
712
+ let pparams = value.pparams;
713
+ if (!pparams.empty()) {
714
+ let res = pparams[0];
715
+ let node = new AST.ASTComprehension (res);
716
+ node.line = this.code.Current.LineNo;
717
+ this.dataStack.push(node);
718
+ }
719
+ }
720
+ }
721
+ }
722
+
723
+ function handlePrintExpr() {
724
+ processPrint.call(this);
725
+ }
726
+
727
+ function handlePrintItem() {
728
+ processPrint.call(this);
729
+ }
730
+
731
+ function processPrint() {
732
+ let printNode;
733
+ if (this.curBlock.nodes.length > 0 && this.curBlock.nodes.top() instanceof AST.ASTPrint) {
734
+ printNode = this.curBlock.nodes.top();
735
+ }
736
+ if (printNode && printNode.stream == null && !printNode.eol) {
737
+ printNode.add(this.dataStack.pop());
738
+ } else {
739
+ let node = new AST.ASTPrint(this.dataStack.pop());
740
+ node.line = this.code.Current.LineNo;
741
+ this.curBlock.append(node);
742
+ }
743
+ }
744
+
745
+ function handlePrintItemTo() {
746
+ let stream = this.dataStack.pop();
747
+ let printNode;
748
+
749
+ if (this.curBlock.nodes.length > 0 && this.curBlock.nodes.top() instanceof AST.ASTPrint) {
750
+ printNode = this.curBlock.nodes.top();
751
+ }
752
+
753
+ if (printNode && printNode.stream == stream && !printNode.eol) {
754
+ printNode.add(this.dataStack.pop());
755
+ } else {
756
+ let node = new AST.ASTPrint(this.dataStack.pop(), stream);
757
+ node.line = this.code.Current.LineNo;
758
+ this.curBlock.append(node);
759
+ }
760
+ }
761
+
762
+ function handlePrintNewline() {
763
+ let printNode;
764
+ if (!this.curBlock.empty() && this.curBlock.nodes.top() instanceof AST.ASTPrint)
765
+ printNode = this.curBlock.nodes.top();
766
+ if (printNode && printNode.stream == null && !printNode.eol)
767
+ printNode.eol = true;
768
+ else {
769
+ let node = new AST.ASTPrint();
770
+ node.line = this.code.Current.LineNo;
771
+ this.curBlock.append(node);
772
+ }
773
+ this.dataStack.pop();
774
+ }
775
+
776
+ function handlePrintNewlineTo() {
777
+ let stream = this.dataStack.pop();
778
+
779
+ let printNode;
780
+ if (!this.curBlock.empty() && this.curBlock.nodes.top() instanceof AST.ASTPrint) {
781
+ printNode = this.curBlock.nodes.top();
782
+ }
783
+
784
+ if (printNode && printNode.stream == stream && !printNode.eol) {
785
+ printNode.eol = true;
786
+ } else {
787
+ let node = new AST.ASTPrint(null, stream);
788
+ node.line = this.code.Current.LineNo;
789
+ this.curBlock.append(node);
790
+ }
791
+ this.dataStack.pop();
792
+ }
793
+
794
+ function handleReturnGenerator() {
795
+ // Python 3.11+ RETURN_GENERATOR opcode
796
+ // Appears at the start of generator/async generator functions
797
+ // Creates the generator object - no action needed in decompiler
798
+ if (global.g_cliArgs?.debug) {
799
+ console.log(`[RETURN_GENERATOR] at offset ${this.code.Current.Offset} - generator function detected`);
800
+ }
801
+ }
802
+
803
+ function handleInstrumentedReturnValueA() {
804
+ this.handleReturnValue();
805
+ }
806
+
807
+ function handleReturnValue() {
808
+ if (this.currentMatch) {
809
+ // Only start new case if no current case AND (not in pattern OR has unflushed pattern ops)
810
+ const hasUnflushedPattern = (this.patternOps?.length || 0) > 0;
811
+ const needsNewCase = !this.currentCase && (!this.inMatchPattern || hasUnflushedPattern);
812
+ if (needsNewCase) {
813
+ beginMatchCaseFromPattern.call(this, {reason: 'return'});
814
+ }
815
+ }
816
+
817
+ // CRITICAL: Close blocks that have ended before processing return
818
+ // Return statements often appear at block boundaries
819
+ while (this.curBlock.end > 0 &&
820
+ this.curBlock.end <= this.code.Current.Offset &&
821
+ this.curBlock.blockType != AST.ASTBlock.BlockType.Main &&
822
+ this.blocks.length > 1) {
823
+
824
+ if (global.g_cliArgs?.debug) {
825
+ console.log(`[handleReturnValue] Closing ended block ${this.curBlock.type_str}(${this.curBlock.start}-${this.curBlock.end}) at offset ${this.code.Current.Offset}`);
826
+ }
827
+
828
+ let closedBlock = this.blocks.pop();
829
+ this.curBlock = this.blocks.top();
830
+ this.curBlock.append(closedBlock);
831
+
832
+ if (global.g_cliArgs?.debug) {
833
+ console.log(` → Appended to ${this.curBlock.type_str}(${this.curBlock.start}-${this.curBlock.end}), now has ${this.curBlock.nodes.length} nodes`);
834
+ }
835
+ }
836
+
837
+ // 3.12 exception-group prolog sometimes leaves synthetic None; drop only if it's a synthetic None before PUSH_EXC_INFO.
838
+ let value = this.dataStack.pop();
839
+ const nextOp = this.code.Next;
840
+ // Only drop return if it's a synthetic None followed by PUSH_EXC_INFO (exception-group prolog pattern).
841
+ // Real returns before exception handlers (like in with-statements) should NOT be dropped.
842
+ if (value instanceof AST.ASTNone && nextOp?.OpCodeID === this.OpCodes.PUSH_EXC_INFO) {
843
+ // Check if this is a synthetic return (no explicit line number or same line as previous instruction)
844
+ const curLine = this.code.Current?.LineNo;
845
+ const prevLine = this.code.Previous?.LineNo;
846
+ if (curLine === prevLine || !curLine) {
847
+ // Likely synthetic None from exception prolog - drop it
848
+ return;
849
+ }
850
+ }
851
+ if (value == null) {
852
+ value = new AST.ASTNone();
853
+ }
854
+ let node = new AST.ASTReturn(value);
855
+ node.inLambda = this.object.Name == '<lambda>';
856
+ node.line = this.code.Current.LineNo;
857
+
858
+ this.curBlock.append(node);
859
+
860
+ if (this.currentMatch) {
861
+ const hasDefaultAhead = (() => {
862
+ let scan = this.code.Next;
863
+ let steps = 0;
864
+ const retOps = [this.OpCodes.RETURN_CONST_A, this.OpCodes.RETURN_VALUE, this.OpCodes.RETURN_VALUE_A];
865
+ const skipOps = [this.OpCodes.CACHE, this.OpCodes.NOP];
866
+ while (scan && steps < 4) {
867
+ if (retOps.includes(scan.OpCodeID)) return true;
868
+ if (skipOps.includes(scan.OpCodeID)) { scan = scan.Next; steps++; continue; }
869
+ break;
870
+ }
871
+ return false;
872
+ })();
873
+ if (hasDefaultAhead) {
874
+ // Default case ahead - flush current case before default starts
875
+ if (this.currentCase) {
876
+ flushCurrentCaseBody.call(this);
877
+ }
878
+ return;
879
+ }
880
+ if (!hasUpcomingMatchCase.call(this)) {
881
+ finalizeMatchCase.call(this);
882
+ return;
883
+ }
884
+ // More cases coming - flush current case before next one starts
885
+ if (this.currentCase) {
886
+ flushCurrentCaseBody.call(this);
887
+ }
888
+ }
889
+
890
+ if (!this.currentCase && [AST.ASTBlock.BlockType.If, AST.ASTBlock.BlockType.Else].includes(this.curBlock.blockType)
891
+ && (this.object.Reader.versionCompare(2, 6) >= 0)) {
892
+ let prev = this.curBlock;
893
+ this.blocks.pop();
894
+ this.curBlock = this.blocks.top();
895
+ if (
896
+ prev instanceof AST.ASTCondBlock &&
897
+ prev.nodes.length == 1 &&
898
+ prev.line == value.line
899
+ ) {
900
+ prev = new AST.ASTReturn(new AST.ASTBinary(prev.condition, value, prev.negative ? AST.ASTBinary.BinOp.LogicalOr : AST.ASTBinary.BinOp.LogicalAnd));
901
+ }
902
+
903
+ this.curBlock.append(prev);
904
+
905
+ if ([this.OpCodes.JUMP_ABSOLUTE_A, this.OpCodes.JUMP_FORWARD_A].includes(this.code.Next?.OpCodeID)) {
906
+ this.code.GoNext();
907
+ }
908
+ }
909
+ }
910
+
911
+ function handleInstrumentedReturnConstA() {
912
+ this.handleReturnConstA();
913
+ }
914
+
915
+ function handleReturnConstA() {
916
+ // If a new pattern matched but case body not started yet, open it now
917
+ // Only start new case if no current case AND (not in pattern OR has unflushed pattern ops)
918
+ if (this.currentMatch) {
919
+ const hasUnflushedPattern = (this.patternOps?.length || 0) > 0;
920
+ const needsNewCase = !this.currentCase && (!this.inMatchPattern || hasUnflushedPattern);
921
+ if (needsNewCase) {
922
+ beginMatchCaseFromPattern.call(this, {reason: 'return'});
923
+ }
924
+ }
925
+
926
+ const nextOp = this.code.Next;
927
+ if (nextOp?.OpCodeID === this.OpCodes.PUSH_EXC_INFO) {
928
+ return;
929
+ }
930
+
931
+ // Skip implicit return None in various contexts:
932
+ // 1. Module-level implicit return at end
933
+ // 2. Exception cleanup after successful except* handling (POP_EXCEPT → RETURN_CONST None → SWAP)
934
+ const constObj = this.code.Current.ConstantObject;
935
+ const isNone = constObj?.ClassName === 'Py_None' || constObj === null || constObj === undefined;
936
+ const isModuleLevel = this.object.Name === '<module>';
937
+ const isAtEnd = !nextOp || nextOp.OpCodeID === this.OpCodes.RERAISE_A ||
938
+ nextOp.OpCodeID === this.OpCodes.RERAISE;
939
+ const prevIsPOPExcept = this.code.Prev?.OpCodeID === this.OpCodes.POP_EXCEPT;
940
+ const nextIsSWAP = nextOp?.OpCodeID === this.OpCodes.SWAP_A;
941
+ const isExceptCleanup = isNone && prevIsPOPExcept && nextIsSWAP;
942
+
943
+ if (isNone && isModuleLevel && isAtEnd) {
944
+ return; // Skip implicit return None at module end
945
+ }
946
+ if (isExceptCleanup) {
947
+ return; // Skip return None in exception cleanup (except* success path)
948
+ }
949
+
950
+ let value = new AST.ASTObject(this.code.Current.ConstantObject);
951
+ let node = new AST.ASTReturn(value);
952
+ node.line = this.code.Current.LineNo;
953
+
954
+ this.curBlock.append(node);
955
+
956
+ if (this.currentMatch) {
957
+ const hasDefaultAhead = (() => {
958
+ let scan = this.code.Next;
959
+ let steps = 0;
960
+ const retOps = [this.OpCodes.RETURN_CONST_A, this.OpCodes.RETURN_VALUE, this.OpCodes.RETURN_VALUE_A];
961
+ const skipOps = [this.OpCodes.CACHE, this.OpCodes.NOP];
962
+ while (scan && steps < 4) {
963
+ if (retOps.includes(scan.OpCodeID)) return true;
964
+ if (skipOps.includes(scan.OpCodeID)) { scan = scan.Next; steps++; continue; }
965
+ break;
966
+ }
967
+ return false;
968
+ })();
969
+ if (hasDefaultAhead) {
970
+ // Default case ahead - flush current case before default starts
971
+ if (this.currentCase) {
972
+ flushCurrentCaseBody.call(this);
973
+ }
974
+ return;
975
+ }
976
+ if (!hasUpcomingMatchCase.call(this)) {
977
+ finalizeMatchCase.call(this);
978
+ return;
979
+ }
980
+ // More cases coming - flush current case before next one starts
981
+ if (this.currentCase) {
982
+ flushCurrentCaseBody.call(this);
983
+ }
984
+ }
985
+
986
+ }
987
+
988
+ function handleSetLinenoA() {}
989
+
990
+ function handleSetupAnnotations() {
991
+ this.variable_annotations = true;
992
+ }
993
+
994
+ function handleEndAsyncFor() {
995
+ // Python 3.9+ END_ASYNC_FOR opcode
996
+ // Python 3.9-3.10: finally handler for SETUP_FINALLY in async for loops
997
+ // Python 3.11+: direct exception handler (no CONTAINER/finally wrapper)
998
+
999
+ if (global.g_cliArgs?.debug) {
1000
+ console.log(`[END_ASYNC_FOR] curBlock=${this.curBlock.type_str}, stack depth=${this.blocks.length}`);
1001
+ console.log(` Block stack: ${this.blocks.map((b,i) => `[${i}]${b.type_str}`).join(' → ')}`);
1002
+ }
1003
+
1004
+ // Python 3.11+: Direct async for block (no CONTAINER)
1005
+ if (this.object.Reader.versionCompare(3, 11) >= 0 &&
1006
+ this.curBlock.blockType == AST.ASTBlock.BlockType.AsyncFor) {
1007
+
1008
+ if (global.g_cliArgs?.debug) {
1009
+ console.log(` Python 3.11+ direct AsyncFor, nodes: ${this.curBlock.nodes.length}`);
1010
+ }
1011
+
1012
+ // If body is empty, add pass
1013
+ if (this.curBlock.nodes.length === 0) {
1014
+ let passNode = new AST.ASTKeyword(AST.ASTKeyword.Word.Pass);
1015
+ passNode.line = this.code.Current.LineNo;
1016
+ this.curBlock.nodes.push(passNode);
1017
+
1018
+ if (global.g_cliArgs?.debug) {
1019
+ console.log(` ✓ Added pass statement to empty Python 3.11 async for body`);
1020
+ }
1021
+ }
1022
+
1023
+ // Close the async for block
1024
+ this.blocks.pop();
1025
+ this.blocks.top().append(this.curBlock);
1026
+ this.curBlock = this.blocks.top();
1027
+
1028
+ if (global.g_cliArgs?.debug) {
1029
+ console.log(` ✓ Closed Python 3.11 AsyncFor block`);
1030
+ }
1031
+
1032
+ return;
1033
+ }
1034
+
1035
+ // Python 3.9-3.10: CONTAINER/finally pattern
1036
+ if (this.curBlock.blockType == AST.ASTBlock.BlockType.Finally) {
1037
+ let finallyBlock = this.curBlock;
1038
+
1039
+ // Pop the finally block
1040
+ this.blocks.pop();
1041
+ this.curBlock = this.blocks.top();
1042
+
1043
+ // Check if we're in CONTAINER → AsyncFor pattern
1044
+ if (this.curBlock.blockType == AST.ASTBlock.BlockType.Container &&
1045
+ this.blocks.length > 1) {
1046
+ let cont = this.curBlock;
1047
+ let parentBlock = this.blocks[this.blocks.length - 2];
1048
+
1049
+ if (parentBlock.blockType == AST.ASTBlock.BlockType.AsyncFor &&
1050
+ parentBlock.inited) {
1051
+
1052
+ if (global.g_cliArgs?.debug) {
1053
+ console.log(` Found CONTAINER→AsyncFor(inited), hiding CONTAINER`);
1054
+ console.log(` AsyncFor has ${parentBlock.nodes.length} nodes, Finally has ${finallyBlock.nodes.length} nodes`);
1055
+ }
1056
+
1057
+ // Extract loop body from finally block (Python 3.9 uses finally, not try)
1058
+ // Skip STORE (index already set by processStore) and skip async implementation details
1059
+ for (let i = 0; i < finallyBlock.nodes.length; i++) {
1060
+ let node = finallyBlock.nodes[i];
1061
+ // Skip implementation details: STORE, yield from, await calls
1062
+ if (node &&
1063
+ !(node instanceof AST.ASTStore) &&
1064
+ node.constructor.name !== 'ASTReturn' &&
1065
+ !(node.constructor.name === 'ASTCall' && node.func?.name === 'await')) {
1066
+ parentBlock.nodes.push(node);
1067
+ }
1068
+ }
1069
+
1070
+ // If body is empty, add pass
1071
+ if (parentBlock.nodes.length === 0) {
1072
+ let passNode = new AST.ASTKeyword(AST.ASTKeyword.Word.Pass);
1073
+ passNode.line = finallyBlock.line;
1074
+ parentBlock.nodes.push(passNode);
1075
+
1076
+ if (global.g_cliArgs?.debug) {
1077
+ console.log(` ✓ Added pass statement to empty async for body`);
1078
+ }
1079
+ }
1080
+
1081
+ // Pop CONTAINER without appending it
1082
+ this.blocks.pop();
1083
+ this.curBlock = this.blocks.top();
1084
+
1085
+ // Close AsyncFor early and mark rest as unreachable
1086
+ if (this.curBlock.blockType == AST.ASTBlock.BlockType.AsyncFor) {
1087
+ let asyncForEnd = this.curBlock.end;
1088
+ this.blocks.pop();
1089
+ this.blocks.top().append(this.curBlock);
1090
+ this.curBlock = this.blocks.top();
1091
+
1092
+ // Mark everything until the original AsyncFor end as unreachable
1093
+ this.unreachableUntil = asyncForEnd;
1094
+
1095
+ if (global.g_cliArgs?.debug) {
1096
+ console.log(` ✓ Closed AsyncFor, marking ${this.code.Current.Offset}-${asyncForEnd} as unreachable`);
1097
+ }
1098
+ }
1099
+ return;
1100
+ }
1101
+ }
1102
+ }
1103
+ }
1104
+
1105
+ function handleInstrumentedEndAsyncForA() {
1106
+ // Instrumented variant mirrors END_ASYNC_FOR behavior.
1107
+ handleEndAsyncFor.call(this);
1108
+ }
1109
+
1110
+ function handleInstrumentedInstructionA() {
1111
+ // Instrumentation hook; no AST impact.
1112
+ if (global.g_cliArgs?.debug) {
1113
+ console.log(`[INSTRUMENTED_INSTRUCTION] at offset ${this.code.Current.Offset}`);
1114
+ }
1115
+ }
1116
+
1117
+ function handleInstrumentedLineA() {
1118
+ // Line profiling hook; skip for decompilation.
1119
+ if (global.g_cliArgs?.debug) {
1120
+ console.log(`[INSTRUMENTED_LINE] at offset ${this.code.Current.Offset}`);
1121
+ }
1122
+ }
1123
+
1124
+ module.exports = {
1125
+ beginMatchCaseFromPattern,
1126
+ flushCurrentCaseBody,
1127
+ handleExecStmt,
1128
+ handleFormatValueA,
1129
+ handleFormatSimple,
1130
+ handleFormatWithSpec,
1131
+ handlePopTop,
1132
+ handlePrintExpr,
1133
+ handlePrintItem,
1134
+ handlePrintItemTo,
1135
+ handlePrintNewline,
1136
+ handlePrintNewlineTo,
1137
+ handleReturnGenerator,
1138
+ handleInstrumentedReturnValueA,
1139
+ handleReturnValue,
1140
+ handleInstrumentedReturnConstA,
1141
+ handleReturnConstA,
1142
+ handleSetLinenoA,
1143
+ handleSetupAnnotations,
1144
+ handleEndAsyncFor,
1145
+ handleInstrumentedEndAsyncForA,
1146
+ handleInstrumentedInstructionA,
1147
+ handleInstrumentedLineA
1148
+ };
1149
+
1150
+ function extractGuardFromBody(nodes) {
1151
+ if (!nodes || nodes.length === 0) {
1152
+ return {guard: null, bodyNodes: []};
1153
+ }
1154
+
1155
+ if (global.g_cliArgs?.debug) {
1156
+ const raw = nodes.map(n => `${n?.constructor?.name || typeof n}:${n?.codeFragment ? n.codeFragment() : ''}`);
1157
+ console.log(`[MATCH] Raw case nodes: ${raw.join(' | ')}`);
1158
+ }
1159
+
1160
+ let guard = null;
1161
+ let working = nodes.slice();
1162
+ const prefix = [];
1163
+ while (working.length > 0 && working[0] instanceof AST.ASTStore) {
1164
+ prefix.push(working.shift());
1165
+ }
1166
+
1167
+ if (working.length === 1 && working[0] instanceof AST.ASTCondBlock &&
1168
+ working[0].blockType == AST.ASTBlock.BlockType.If && !working[0].negative &&
1169
+ !(working[0].nextSibling instanceof AST.ASTBlock)) {
1170
+ guard = working[0].condition;
1171
+ working = prefix.concat(working[0].nodes || []);
1172
+ } else {
1173
+ working = nodes.slice();
1174
+ }
1175
+
1176
+ return {guard, bodyNodes: working};
1177
+ }
1178
+
1179
+ function guardFromPatternRemainder(remainderOps) {
1180
+ if (!remainderOps || remainderOps.length === 0) {
1181
+ return null;
1182
+ }
1183
+
1184
+ const compareOps = remainderOps.filter(op => op.type === 'COMPARE');
1185
+ if (compareOps.length === 0) {
1186
+ return null;
1187
+ }
1188
+
1189
+ let guardExpr = null;
1190
+
1191
+ for (const comp of compareOps) {
1192
+ const compareNode = new AST.ASTCompare(comp.left, comp.right, comp.op);
1193
+ if (guardExpr) {
1194
+ guardExpr = new AST.ASTBinary(guardExpr, compareNode, AST.ASTBinary.BinOp.LogicalAnd);
1195
+ } else {
1196
+ guardExpr = compareNode;
1197
+ }
1198
+ }
1199
+
1200
+ return guardExpr;
1201
+ }