depyo 1.0.3 → 1.2.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.
@@ -241,11 +241,25 @@ function reconstructPattern(patternOps) {
241
241
  ));
242
242
  }
243
243
 
244
- // Literal OR pattern: multiple COMPARE_OP checks in a row
244
+ // Literal OR pattern: multiple COMPARE_OP checks in a row.
245
+ // Pattern compares share the same `left` (the matched subject). Anything
246
+ // with a different left is a guard expression that bled into patternOps.
245
247
  const allCompareOps = ops.filter(op => op.type === 'COMPARE');
246
- if (!hasMatchSeq && allCompareOps.length > 1) {
248
+ const sameLeft = (a, b) => {
249
+ if (a === b) return true;
250
+ if (!a || !b) return false;
251
+ const af = a.codeFragment?.()?.toString?.();
252
+ const bf = b.codeFragment?.()?.toString?.();
253
+ return !!af && af === bf;
254
+ };
255
+ let patternCompareOps = allCompareOps;
256
+ if (allCompareOps.length > 1) {
257
+ const subjectLeft = allCompareOps[0].left;
258
+ patternCompareOps = allCompareOps.filter(op => sameLeft(op.left, subjectLeft));
259
+ }
260
+ if (!hasMatchSeq && patternCompareOps.length > 1) {
247
261
  const orPatterns = [];
248
- for (const cmp of allCompareOps) {
262
+ for (const cmp of patternCompareOps) {
249
263
  markConsumed(cmp);
250
264
  if (cmp.right instanceof AST.ASTObject) {
251
265
  orPatterns.push(new AST.ASTPattern(AST.ASTPattern.PatternType.Literal, cmp.right));
@@ -366,6 +380,21 @@ function hasUpcomingMatchCase() {
366
380
  return true;
367
381
  }
368
382
 
383
+ // `case _ as name:` final clause — CPython emits a bare STORE for the
384
+ // binding right after the previous case's RETURN. No LOAD precedes it
385
+ // (the stored value is the matched subject preserved on the stack).
386
+ if (this.currentMatch && this.matchSubject &&
387
+ (nextOpCodeId == this.OpCodes.STORE_FAST_A ||
388
+ nextOpCodeId == this.OpCodes.STORE_NAME_A)) {
389
+ const prevId = this.code.Current.OpCodeID;
390
+ const isAfterReturn = prevId == this.OpCodes.RETURN_VALUE ||
391
+ prevId == this.OpCodes.RETURN_VALUE_A ||
392
+ prevId == this.OpCodes.RETURN_CONST_A;
393
+ if (isAfterReturn) {
394
+ return true;
395
+ }
396
+ }
397
+
369
398
  // NOP followed by code (no COMPARE_OP) = default case (case _:)
370
399
  // This pattern appears in 3.10/3.11 after the last literal case
371
400
  if (nextOpCodeId == this.OpCodes.NOP && this.currentMatch) {
@@ -582,7 +611,10 @@ function hasUpcomingStringBuild(context) {
582
611
  context.OpCodes.STORE_DEREF_A
583
612
  ]);
584
613
 
585
- for (let step = 1; step <= 12; step++) {
614
+ // 3.11+ inline caches inflate the physical instruction stream; a single
615
+ // f-string with a few interpolations easily spans 30+ code slots before
616
+ // reaching BUILD_STRING. Cap generously and rely on earlyStops to bound.
617
+ for (let step = 1; step <= 48; step++) {
586
618
  const instr = context.code.PeekInstructionAtOffset(context.code.Current.Offset + step * 2);
587
619
  if (!instr) {
588
620
  break;
@@ -745,6 +777,28 @@ function handlePopTop() {
745
777
  return;
746
778
  }
747
779
 
780
+ // Mid-UNPACK_SEQUENCE POP_TOP: CPython emits `a, _, b = tup` as
781
+ // UNPACK 3 / STORE a / POP_TOP / STORE b, where POP_TOP drops the
782
+ // middle slot. Our unpack-state-machine accumulates STORE targets
783
+ // into a tuple; treat POP_TOP as an anonymous `_` slot so the
784
+ // remaining STORE_FAST still sees the tuple on the stack.
785
+ if (this.unpack > 0) {
786
+ let tupleNode = this.dataStack.top();
787
+ if (tupleNode instanceof AST.ASTTuple) {
788
+ let placeholder = new AST.ASTName("_");
789
+ if (this.starPos-- == 0) placeholder.name = "*_";
790
+ tupleNode.add(placeholder);
791
+ if (--this.unpack <= 0) {
792
+ this.dataStack.pop();
793
+ let seqNode = this.dataStack.pop();
794
+ let node = new AST.ASTStore(seqNode, tupleNode);
795
+ node.line = this.code.Current.LineNo;
796
+ this.curBlock.append(node);
797
+ }
798
+ return;
799
+ }
800
+ }
801
+
748
802
  if ([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)) {
749
803
  if (this.curBlock.blockType == AST.ASTBlock.BlockType.Except) {
750
804
  // Skipping POP_TOP, POP_TOP, POP_TOP
@@ -1111,7 +1165,15 @@ function handleReturnConstA() {
1111
1165
  nextOp.OpCodeID === this.OpCodes.RERAISE;
1112
1166
  const prevIsPOPExcept = this.code.Prev?.OpCodeID === this.OpCodes.POP_EXCEPT;
1113
1167
  const nextIsSWAP = nextOp?.OpCodeID === this.OpCodes.SWAP_A;
1168
+ const nextIsRERAISE = nextOp?.OpCodeID === this.OpCodes.RERAISE ||
1169
+ nextOp?.OpCodeID === this.OpCodes.RERAISE_A;
1114
1170
  const isExceptCleanup = isNone && prevIsPOPExcept && nextIsSWAP;
1171
+ // CPython 3.11+ places the function's implicit `return None` at the tail of an
1172
+ // except handler (POP_EXCEPT → RETURN_CONST None → RERAISE or end-of-function).
1173
+ // The source has no explicit return there; drop it so the handler body doesn't
1174
+ // inherit a phantom return.
1175
+ const isExceptTailImplicitReturn = isNone && prevIsPOPExcept &&
1176
+ (nextIsRERAISE || !nextOp);
1115
1177
 
1116
1178
  if (isNone && isModuleLevel && isAtEnd) {
1117
1179
  return; // Skip implicit return None at module end
@@ -1119,6 +1181,9 @@ function handleReturnConstA() {
1119
1181
  if (isExceptCleanup) {
1120
1182
  return; // Skip return None in exception cleanup (except* success path)
1121
1183
  }
1184
+ if (isExceptTailImplicitReturn) {
1185
+ return; // Skip synthetic return None at end of except handler
1186
+ }
1122
1187
 
1123
1188
  let value = new AST.ASTObject(this.code.Current.ConstantObject);
1124
1189
  let node = new AST.ASTReturn(value);
@@ -1294,6 +1359,12 @@ function handleInstrumentedLineA() {
1294
1359
  }
1295
1360
  }
1296
1361
 
1362
+ // CPython 3.13+: POP_ITER replaces the post-loop POP_TOP that discards an
1363
+ // exhausted iterator. Semantically identical to POP_TOP for our purposes.
1364
+ function handlePopIter() {
1365
+ return handlePopTop.call(this);
1366
+ }
1367
+
1297
1368
  module.exports = {
1298
1369
  beginMatchCaseFromPattern,
1299
1370
  flushCurrentCaseBody,
@@ -1302,6 +1373,7 @@ module.exports = {
1302
1373
  handleFormatSimple,
1303
1374
  handleFormatWithSpec,
1304
1375
  handlePopTop,
1376
+ handlePopIter,
1305
1377
  handlePrintExpr,
1306
1378
  handlePrintItem,
1307
1379
  handlePrintItemTo,
@@ -28,7 +28,8 @@ function handleDupTop() {
28
28
 
29
29
  // Look ahead to confirm match pattern immediately
30
30
  if (!this.currentMatch && this.lookAheadForMatchPattern()) {
31
- this.currentMatch = new AST.ASTMatch(this.potentialMatchSubject);
31
+ this.matchSubject = this.potentialMatchSubject;
32
+ this.currentMatch = new AST.ASTMatch(this.matchSubject);
32
33
  this.currentMatch.line = this.code.Current.LineNo;
33
34
  this.matchParentBlock = this.curBlock;
34
35
  this.inMatchPattern = true; // Start tracking pattern operations
@@ -46,7 +47,8 @@ function handleDupTop() {
46
47
  if (!this.currentMatch && this.potentialMatchSubject &&
47
48
  this.object.Reader.versionCompare(3, 10) >= 0) {
48
49
  // First time - create match (fallback if look-ahead didn't work)
49
- this.currentMatch = new AST.ASTMatch(this.potentialMatchSubject);
50
+ this.matchSubject = this.potentialMatchSubject;
51
+ this.currentMatch = new AST.ASTMatch(this.matchSubject);
50
52
  this.currentMatch.line = this.code.Current.LineNo;
51
53
  this.matchParentBlock = this.curBlock;
52
54
  this.inMatchPattern = true;
@@ -69,6 +69,13 @@ function handleUnaryPositive() {
69
69
 
70
70
  function handleToBool() {
71
71
  // Python 3.13+ TO_BOOL: normalize truthiness. Preserve stack value.
72
+ // The preceding LOAD+COPY 1 here is the boolean-shortcut idiom for
73
+ // `if not X` / `while X`, not a match-case subject. Clear the
74
+ // potential-match candidate so a later COMPARE_OP (e.g. `e.code == 409`
75
+ // inside an except handler) doesn't get retroactively wrapped in a
76
+ // synthetic `match X:` rooted on this stale subject.
77
+ this.potentialMatchSubject = null;
78
+ this.matchCandidateStart = -1;
72
79
  const arg = this.dataStack.pop();
73
80
  this.dataStack.push(arg);
74
81
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "depyo",
3
- "version": "1.0.3",
4
- "description": "Python bytecode decompiler (Python 1.0–3.14) implemented in Node.js",
3
+ "version": "1.2.0",
4
+ "description": "Python bytecode decompiler (Python 1.0–3.15) implemented in Node.js",
5
5
  "bin": {
6
6
  "depyo": "./depyo.js"
7
7
  },