depyo 1.0.2 → 1.0.3

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.
@@ -106,7 +106,8 @@ function reconstructPattern(patternOps) {
106
106
  let attrOpsStart = ops.indexOf(matchClassOp) + 1;
107
107
  let attrOps = ops.slice(attrOpsStart);
108
108
  const unpackIndex = attrOps.findIndex(op => op.type === 'UNPACK_SEQUENCE');
109
- if (unpackIndex >= 0) {
109
+ const hasUnpack = unpackIndex >= 0;
110
+ if (hasUnpack) {
110
111
  attrOps = attrOps.slice(unpackIndex + 1);
111
112
  if (unpackOp) {
112
113
  markConsumed(unpackOp);
@@ -121,47 +122,107 @@ function reconstructPattern(patternOps) {
121
122
  attrOps = attrOps.slice(1);
122
123
  }
123
124
 
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;
125
+ // Two bytecode shapes for class-pattern attribute extraction:
126
+ //
127
+ // (A) 3.11+ UNPACK_SEQUENCE shape — each attribute emits either a
128
+ // COMPARE (literal) or STORE_FAST (binding) inline at its
129
+ // position. SWAP 2 indicates reverseOrder across all attrs.
130
+ //
131
+ // (B) 3.10 DUP_TOP/ROT_TWO shape — BIND_STASH marks a binding
132
+ // position; its STORE_FAST appears later, after any subsequent
133
+ // COMPAREs. STORE_FASTs are FIFO-matched to BIND_STASHes.
134
+ if (hasUnpack) {
135
+ const attributeOps = [];
136
+ for (const op of attrOps) {
137
+ if (op.type === 'COMPARE' || op.type === 'STORE_FAST') {
138
+ attributeOps.push(op);
139
+ markConsumed(op);
140
+ }
141
+ if (attrCount && attributeOps.length >= attrCount) {
142
+ break;
143
+ }
133
144
  }
134
- }
135
-
136
- if (reverseOrder) {
137
- attributeOps.reverse();
138
- }
139
145
 
140
- let attrIndex = 0;
141
- for (const op of attributeOps) {
142
- if (attrCount && attrIndex >= attrCount) {
143
- break;
146
+ if (reverseOrder) {
147
+ attributeOps.reverse();
144
148
  }
145
149
 
146
- if (op.type === 'COMPARE') {
147
- const literalPattern = new AST.ASTPattern(
148
- AST.ASTPattern.PatternType.Literal,
149
- op.right
150
- );
150
+ let attrIndex = 0;
151
+ for (const op of attributeOps) {
152
+ if (attrCount && attrIndex >= attrCount) {
153
+ break;
154
+ }
151
155
  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});
156
+ if (op.type === 'COMPARE') {
157
+ attributes.push({
158
+ name,
159
+ pattern: new AST.ASTPattern(AST.ASTPattern.PatternType.Literal, op.right)
160
+ });
161
+ } else {
162
+ attributes.push({
163
+ name,
164
+ pattern: new AST.ASTPattern(AST.ASTPattern.PatternType.Variable, op.name)
165
+ });
166
+ }
167
+ attrIndex++;
168
+ }
169
+ } else {
170
+ const inlineOps = []; // COMPARE or BIND_STASH in position order
171
+ const storeOps = []; // Trailing STORE_FAST queue, FIFO to stashes
172
+ for (const op of attrOps) {
173
+ if (op.type === 'COMPARE' || op.type === 'BIND_STASH') {
174
+ if (!attrCount || inlineOps.length < attrCount) {
175
+ inlineOps.push(op);
176
+ markConsumed(op);
177
+ }
178
+ } else if (op.type === 'STORE_FAST') {
179
+ storeOps.push(op);
180
+ markConsumed(op);
181
+ }
160
182
  }
161
183
 
162
- attrIndex++;
184
+ // Legacy fixtures (no BIND_STASH recorded) fall back to inline
185
+ // STORE_FAST handling so pre-existing Case-4-style snapshots don't
186
+ // regress.
187
+ if (inlineOps.length === 0 && storeOps.length > 0) {
188
+ const attributeOps = storeOps;
189
+ let attrIndex = 0;
190
+ for (const op of attributeOps) {
191
+ if (attrCount && attrIndex >= attrCount) break;
192
+ const name = attrNames.length ? (attrNames[attrIndex] || op.name || `_attr${attrIndex}`) : null;
193
+ attributes.push({
194
+ name,
195
+ pattern: new AST.ASTPattern(AST.ASTPattern.PatternType.Variable, op.name)
196
+ });
197
+ attrIndex++;
198
+ }
199
+ } else {
200
+ let stashIdx = 0;
201
+ let attrIndex = 0;
202
+ for (const op of inlineOps) {
203
+ if (attrCount && attrIndex >= attrCount) break;
204
+ const name = attrNames.length ? (attrNames[attrIndex] || `_attr${attrIndex}`) : null;
205
+ if (op.type === 'COMPARE') {
206
+ attributes.push({
207
+ name,
208
+ pattern: new AST.ASTPattern(AST.ASTPattern.PatternType.Literal, op.right)
209
+ });
210
+ } else {
211
+ const storeOp = storeOps[stashIdx++];
212
+ attributes.push({
213
+ name,
214
+ pattern: storeOp
215
+ ? new AST.ASTPattern(AST.ASTPattern.PatternType.Variable, storeOp.name)
216
+ : new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_')
217
+ });
218
+ }
219
+ attrIndex++;
220
+ }
221
+ }
163
222
  }
164
223
 
224
+ let attrIndex = attributes.length;
225
+
165
226
  // If there are declared attributes without recorded ops, fill them with wildcards
166
227
  while (attrIndex < attrNames.length) {
167
228
  attributes.push({
@@ -603,6 +664,27 @@ function handlePopTop() {
603
664
  this.OpCodes.JUMP_IF_FALSE_A
604
665
  ].includes(this.code.Prev?.OpCodeID);
605
666
 
667
+ // A POP_TOP following ROT_TWO/ROT_THREE/ROT_FOUR is part of binding-extraction
668
+ // stack cleanup inside a class/sequence pattern, not the body-start POP_TOP.
669
+ let prevIsRot = [
670
+ this.OpCodes.ROT_TWO,
671
+ this.OpCodes.ROT_THREE,
672
+ this.OpCodes.ROT_FOUR
673
+ ].includes(this.code.Prev?.OpCodeID);
674
+
675
+ // If the next non-CACHE opcode is STORE_FAST/STORE_NAME, this POP_TOP is mid-pattern
676
+ // binding cleanup (pops the match result tuple before the remaining STORE captures).
677
+ let nextIsStoreBinding = (() => {
678
+ let scan = this.code.Next;
679
+ let steps = 0;
680
+ while (scan && steps < 4) {
681
+ if (scan.OpCodeID == this.OpCodes.CACHE) { scan = scan.Next; steps++; continue; }
682
+ return scan.OpCodeID == this.OpCodes.STORE_FAST_A ||
683
+ scan.OpCodeID == this.OpCodes.STORE_NAME_A;
684
+ }
685
+ return false;
686
+ })();
687
+
606
688
  const patternHasOps = (this.patternOps?.length || 0) > 0;
607
689
  let isLiteralPattern = patternHasOps && !this.patternOps.some(op => ["MATCH_SEQUENCE","MATCH_CLASS","MATCH_MAPPING","MATCH_KEYS"].includes(op.type));
608
690
  let shouldStartCase = readyForWildcard || !!this.inMatchPattern;
@@ -612,6 +694,41 @@ function handlePopTop() {
612
694
  if (patternHasOps && isLiteralPattern && !prevIsJump) {
613
695
  shouldStartCase = false;
614
696
  }
697
+ if (patternHasOps && prevIsRot) {
698
+ shouldStartCase = false;
699
+ }
700
+ if (patternHasOps && nextIsStoreBinding) {
701
+ shouldStartCase = false;
702
+ }
703
+
704
+ // When readyForWildcard fires on the match-exit POP_TOP and the only
705
+ // code that follows is the function's implicit `return None` tail,
706
+ // the source didn't actually include a `case _:` clause — CPython
707
+ // emits the tail unconditionally. Suppress the phantom wildcard.
708
+ if (readyForWildcard && !patternHasOps) {
709
+ let scan = this.code.Next;
710
+ let steps = 0;
711
+ let sawLoadNone = false;
712
+ while (scan && steps < 6) {
713
+ const op = scan.OpCodeID;
714
+ if (op == this.OpCodes.CACHE ||
715
+ op == this.OpCodes.RESUME_A ||
716
+ op == this.OpCodes.NOP) {
717
+ scan = scan.Next; steps++; continue;
718
+ }
719
+ if (op == this.OpCodes.LOAD_CONST_A && !sawLoadNone) {
720
+ sawLoadNone = true;
721
+ scan = scan.Next; steps++; continue;
722
+ }
723
+ if (op == this.OpCodes.RETURN_VALUE || op == this.OpCodes.RETURN_CONST_A) {
724
+ if (sawLoadNone || op == this.OpCodes.RETURN_CONST_A) {
725
+ shouldStartCase = false;
726
+ }
727
+ break;
728
+ }
729
+ break;
730
+ }
731
+ }
615
732
 
616
733
  if (shouldStartCase) {
617
734
  if (beginMatchCaseFromPattern.call(this, {reason: 'pop_top'})) {
@@ -628,7 +745,7 @@ function handlePopTop() {
628
745
  return;
629
746
  }
630
747
 
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)) {
748
+ 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)) {
632
749
  if (this.curBlock.blockType == AST.ASTBlock.BlockType.Except) {
633
750
  // Skipping POP_TOP, POP_TOP, POP_TOP
634
751
  if ([this.OpCodes.JUMP_ABSOLUTE_A, this.OpCodes.JUMP_FORWARD_A].includes(this.code.Prev.OpCodeID)) {
@@ -685,7 +802,13 @@ function handlePopTop() {
685
802
  }
686
803
  if (!this.curBlock.inited) {
687
804
  if (this.curBlock.blockType == AST.ASTBlock.BlockType.With) {
805
+ // POP_TOP initializing a WITH block consumes the context-manager
806
+ // expression off the stack (Py3.0+ `with EXPR:` with no `as`).
807
+ // The value IS the with-expr, not a body statement — return before
808
+ // the fall-through append would add a phantom duplicate.
688
809
  this.curBlock.expr = value;
810
+ this.curBlock.init();
811
+ return;
689
812
  } else if (this.curBlock.blockType == AST.ASTBlock.BlockType.If && !this.curBlock.condition) {
690
813
  this.curBlock.condition = value;
691
814
  }
@@ -702,17 +825,59 @@ function handlePopTop() {
702
825
  this.curBlock.append(value);
703
826
  }
704
827
 
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) {
828
+ /* Python 2.3 listcomp body uses `_[1](x)` (the cached list.append call)
829
+ * instead of LIST_APPEND. Detect this call, whether emitted directly in
830
+ * a For+comp body or nested inside if-filters (`[x for x in y if c]`),
831
+ * and materialize an ASTComprehension instead of leaving the call as a
832
+ * bogus body statement. */
833
+ if (value instanceof AST.ASTCall) {
834
+ let forBlock = null;
835
+ let ifBlocks = [];
836
+ for (let i = this.blocks.length - 1; i >= 0; i--) {
837
+ const blk = this.blocks[i];
838
+ if (blk.blockType == AST.ASTBlock.BlockType.For && blk.comprehension) {
839
+ forBlock = blk;
840
+ break;
841
+ }
842
+ if (blk.blockType == AST.ASTBlock.BlockType.If) {
843
+ ifBlocks.push(blk);
844
+ } else {
845
+ break;
846
+ }
847
+ }
848
+
849
+ const funcIsCompAppend = forBlock && value.func &&
850
+ this._listCompAppendRefs &&
851
+ Object.values(this._listCompAppendRefs).includes(value.func);
852
+ const isDirectCompBody = forBlock && this.curBlock === forBlock;
853
+
854
+ if (funcIsCompAppend || isDirectCompBody) {
712
855
  let pparams = value.pparams;
713
856
  if (!pparams.empty()) {
857
+ if (funcIsCompAppend && this.curBlock !== forBlock) {
858
+ for (const ifBlk of ifBlocks) {
859
+ let cond = ifBlk.condition;
860
+ if (!cond) continue;
861
+ if (ifBlk.negative) {
862
+ const notCond = new AST.ASTUnary(cond, AST.ASTUnary.UnaryOp.Not);
863
+ notCond.line = this.code.Current.LineNo;
864
+ cond = notCond;
865
+ }
866
+ if (forBlock.condition) {
867
+ const andCond = new AST.ASTBinary(forBlock.condition, cond, AST.ASTBinary.BinOp.LogicalAnd);
868
+ andCond.line = this.code.Current.LineNo;
869
+ forBlock.condition = andCond;
870
+ } else {
871
+ forBlock.condition = cond;
872
+ }
873
+ }
874
+ while (this.curBlock !== forBlock && this.blocks.length > 1) {
875
+ this.blocks.pop();
876
+ this.curBlock = this.blocks.top();
877
+ }
878
+ }
714
879
  let res = pparams[0];
715
- let node = new AST.ASTComprehension (res);
880
+ let node = new AST.ASTComprehension(res);
716
881
  node.line = this.code.Current.LineNo;
717
882
  this.dataStack.push(node);
718
883
  }
@@ -809,7 +974,15 @@ function handleReturnValue() {
809
974
  // Only start new case if no current case AND (not in pattern OR has unflushed pattern ops)
810
975
  const hasUnflushedPattern = (this.patternOps?.length || 0) > 0;
811
976
  const needsNewCase = !this.currentCase && (!this.inMatchPattern || hasUnflushedPattern);
812
- if (needsNewCase) {
977
+ // Suppress phantom wildcard when this is the function-tail implicit
978
+ // return None: no pattern ops recorded, LOAD_CONST None precedes us,
979
+ // and this is the last RETURN_VALUE in the code object. CPython
980
+ // emits the tail whether or not source had `case _:`.
981
+ const topIsNone = this.dataStack.top() instanceof AST.ASTNone ||
982
+ (this.dataStack.top() instanceof AST.ASTObject &&
983
+ this.dataStack.top().obj?.ClassName === 'Py_None');
984
+ const isImplicitTail = !hasUnflushedPattern && topIsNone && !this.code.Next;
985
+ if (needsNewCase && !isImplicitTail) {
813
986
  beginMatchCaseFromPattern.call(this, {reason: 'return'});
814
987
  }
815
988
  }
@@ -86,17 +86,58 @@ function handleDupTop() {
86
86
  nextOpCode == this.OpCodes.STORE_FAST_A ||
87
87
  nextOpCode == this.OpCodes.STORE_GLOBAL_A);
88
88
 
89
+ // Pre-3.8 list/set/dict comprehensions compile to
90
+ // BUILD_LIST 0 / DUP_TOP / STORE_NAME _[N] / ... / LIST_APPEND / ...
91
+ // which shares the DUP→STORE→non-STORE shape with walrus. `_[N]` is
92
+ // CPython's synthetic append slot, never a real identifier, so the
93
+ // STORE is swallowed (see processStore) and the DUP must be too —
94
+ // otherwise the walrus branch leaks isWalrusOperator to the next STORE
95
+ // (the FOR_ITER's iteration variable), stealing its valueNode.
96
+ let nextName = this.code.Next?.Name || "";
97
+ if (isStoreOp && nextName.length >= 2 && nextName.startsWith('_[')) {
98
+ return;
99
+ }
100
+
89
101
  if (isStoreOp && this.code.Next?.Next) {
90
- let afterStoreOpCode = this.code.Next.Next.OpCodeID;
91
- let isAnotherStore = (afterStoreOpCode == this.OpCodes.STORE_NAME_A ||
92
- afterStoreOpCode == this.OpCodes.STORE_FAST_A ||
93
- afterStoreOpCode == this.OpCodes.STORE_GLOBAL_A ||
94
- afterStoreOpCode == this.OpCodes.STORE_ATTR_A ||
95
- afterStoreOpCode == this.OpCodes.STORE_DEREF_A);
96
-
97
- if (!isAnotherStore) {
98
- // This is a walrus operator (named expression)
99
- // Don't duplicate - just set flag. The value will be wrapped in ASTNamedExpr.
102
+ // Chain assign and nested walrus share the shape
103
+ // (DUP STORE)+ ending either with STORE (chain, statement)
104
+ // or STORE / POP_TOP (walrus, expression).
105
+ // Walk forward over the alternating DUP/STORE run and inspect
106
+ // what follows the last STORE to distinguish them.
107
+ const storeOps = new Set([
108
+ this.OpCodes.STORE_NAME_A,
109
+ this.OpCodes.STORE_FAST_A,
110
+ this.OpCodes.STORE_GLOBAL_A,
111
+ this.OpCodes.STORE_ATTR_A,
112
+ this.OpCodes.STORE_DEREF_A
113
+ ]);
114
+ const instructions = this.code.Instructions;
115
+ let idx = this.code.CurrentInstructionIndex + 1; // at STORE
116
+ let lastStoreIdx = -1;
117
+ let storeCount = 0;
118
+ while (idx < instructions.length) {
119
+ const op = instructions[idx].OpCodeID;
120
+ if (storeOps.has(op)) {
121
+ lastStoreIdx = idx;
122
+ storeCount++;
123
+ idx++;
124
+ } else if (op == this.OpCodes.DUP_TOP) {
125
+ idx++;
126
+ } else {
127
+ break;
128
+ }
129
+ }
130
+ const afterRunOp = lastStoreIdx >= 0
131
+ ? instructions[lastStoreIdx + 1]?.OpCodeID
132
+ : undefined;
133
+ // Chain assign needs 2+ STOREs; single-STORE runs are walrus
134
+ // regardless of what follows.
135
+ const isChainAssign = storeCount >= 2 &&
136
+ afterRunOp !== undefined &&
137
+ afterRunOp !== this.OpCodes.POP_TOP;
138
+
139
+ if (!isChainAssign) {
140
+ // Nested walrus operator — mark this DUP as walrus-producing.
100
141
  this.isWalrusOperator = true;
101
142
  return;
102
143
  }
@@ -210,6 +251,19 @@ function handleSwapA() {
210
251
  }
211
252
 
212
253
  function handleRotTwo() {
254
+ // In Py3.10 match/case, ROT_TWO plays two roles:
255
+ // 1. Sequence-pattern element-order reversal (follows UNPACK_SEQUENCE)
256
+ // → record SWAP so reconstructPattern's reverseOrder kicks in.
257
+ // 2. Class-pattern binding-extraction dance (follows BINARY_SUBSCR)
258
+ // → record BIND_STASH so reconstructPattern can place the later
259
+ // STORE_FAST at the correct attribute position.
260
+ if (this.inMatchPattern) {
261
+ if (this.code.Prev?.OpCodeID == this.OpCodes.UNPACK_SEQUENCE_A) {
262
+ this.patternOps.push({type: 'SWAP', depth: 2});
263
+ } else if (this.code.Prev?.OpCodeID == this.OpCodes.BINARY_SUBSCR) {
264
+ this.patternOps.push({type: 'BIND_STASH'});
265
+ }
266
+ }
213
267
  let one = this.dataStack.pop();
214
268
  if (this.dataStack.top() instanceof AST.ASTChainStore) {
215
269
  this.dataStack.pop();
@@ -221,18 +275,26 @@ function handleRotTwo() {
221
275
  }
222
276
 
223
277
  function handleRotThree() {
224
- let one = this.dataStack.pop();
225
- let two = this.dataStack.pop();
226
- if (this.dataStack.top() instanceof AST.ASTChainStore) {
227
- this.dataStack.pop();
228
- }
229
- let three = this.dataStack.pop();
230
- this.dataStack.push(one);
231
- this.dataStack.push(three);
232
- this.dataStack.push(two);
278
+ if (this.inMatchPattern &&
279
+ this.code.Prev?.OpCodeID == this.OpCodes.BINARY_SUBSCR) {
280
+ this.patternOps.push({type: 'BIND_STASH'});
281
+ }
282
+ let one = this.dataStack.pop();
283
+ let two = this.dataStack.pop();
284
+ if (this.dataStack.top() instanceof AST.ASTChainStore) {
285
+ this.dataStack.pop();
233
286
  }
287
+ let three = this.dataStack.pop();
288
+ this.dataStack.push(one);
289
+ this.dataStack.push(three);
290
+ this.dataStack.push(two);
291
+ }
234
292
 
235
293
  function handleRotFour() {
294
+ if (this.inMatchPattern &&
295
+ this.code.Prev?.OpCodeID == this.OpCodes.BINARY_SUBSCR) {
296
+ this.patternOps.push({type: 'BIND_STASH'});
297
+ }
236
298
  let one = this.dataStack.pop();
237
299
  let two = this.dataStack.pop();
238
300
  let three = this.dataStack.pop();
@@ -25,6 +25,26 @@ function handleMapAddA() {
25
25
  return;
26
26
  }
27
27
 
28
+ // Async dict comprehension inner code: GET_ANEXT iteration uses
29
+ // SETUP_EXCEPT/FINALLY rather than FOR_ITER, so no For+comp block
30
+ // exists. Save the key/value so the CALL-site reconstruction can use
31
+ // them as the comprehension's yield expression.
32
+ let forBlock = null;
33
+ for (let i = this.blocks.length - 1; i >= 0; i--) {
34
+ const blk = this.blocks[i];
35
+ if (blk.blockType == AST.ASTBlock.BlockType.For && blk.comprehension) {
36
+ forBlock = blk;
37
+ break;
38
+ }
39
+ if (blk.blockType != AST.ASTBlock.BlockType.If) {
40
+ break;
41
+ }
42
+ }
43
+ if (!forBlock) {
44
+ this.object._asyncCompYieldExpr = value;
45
+ this.object._asyncCompYieldKey = key;
46
+ }
47
+
28
48
  let mapNode = this.dataStack.pop();
29
49
 
30
50
  if (!(mapNode instanceof AST.ASTMap)) {
@@ -38,7 +58,23 @@ function handleMapAddA() {
38
58
  }
39
59
 
40
60
  function handleStoreMap() {
41
- handleBuildSliceA.call(this);
61
+ // Py2.x STORE_MAP: TOS=key, SECOND=value, THIRD=dict under construction.
62
+ // Pops key and value, leaves dict on stack with entry added.
63
+ // Without this, BUILD_MAP yields an empty ASTMap and the raw key/value
64
+ // LOAD_CONSTs pile onto the stack; the trailing STORE_NAME then picks
65
+ // up the last-pushed key (a literal int/string) instead of the dict.
66
+ let key = this.dataStack.pop();
67
+ let value = this.dataStack.pop();
68
+ let mapNode = this.dataStack.top();
69
+ if (mapNode instanceof AST.ASTMap) {
70
+ mapNode.add(key, value);
71
+ mapNode.line = this.code.Current.LineNo;
72
+ } else {
73
+ // No dict under construction — unexpected; restore stack so downstream
74
+ // handlers still see the values and can diagnose the shape.
75
+ this.dataStack.push(value);
76
+ this.dataStack.push(key);
77
+ }
42
78
  }
43
79
 
44
80
  function handleBuildSliceA() {
@@ -297,6 +333,70 @@ function storeMatchesAnnotation(storeNode, annotationKeyNode) {
297
333
  return false;
298
334
  }
299
335
 
336
+ function handleBinarySlice() {
337
+ // Python 3.12+ BINARY_SLICE: (container, start, stop -- container[start:stop])
338
+ let stop = this.dataStack.pop();
339
+ let start = this.dataStack.pop();
340
+ let container = this.dataStack.pop();
341
+
342
+ if (start instanceof AST.ASTObject && (start.object == null || start.object.ClassName == 'Py_None')) {
343
+ start = null;
344
+ }
345
+ if (stop instanceof AST.ASTObject && (stop.object == null || stop.object.ClassName == 'Py_None')) {
346
+ stop = null;
347
+ }
348
+
349
+ let sliceOp;
350
+ if (!start && !stop) {
351
+ sliceOp = AST.ASTSlice.SliceOp.Slice0;
352
+ } else if (!start) {
353
+ sliceOp = AST.ASTSlice.SliceOp.Slice2;
354
+ } else if (!stop) {
355
+ sliceOp = AST.ASTSlice.SliceOp.Slice1;
356
+ } else {
357
+ sliceOp = AST.ASTSlice.SliceOp.Slice3;
358
+ }
359
+
360
+ let sliceNode = new AST.ASTSlice(sliceOp, start, stop);
361
+ sliceNode.line = this.code.Current.LineNo;
362
+ let node = new AST.ASTSubscr(container, sliceNode);
363
+ node.line = this.code.Current.LineNo;
364
+ this.dataStack.push(node);
365
+ }
366
+
367
+ function handleStoreSlice() {
368
+ // Python 3.12+ STORE_SLICE: (value, container, start, stop -- )
369
+ // Stores container[start:stop] = value
370
+ let stop = this.dataStack.pop();
371
+ let start = this.dataStack.pop();
372
+ let container = this.dataStack.pop();
373
+ let value = this.dataStack.pop();
374
+
375
+ if (start instanceof AST.ASTObject && (start.object == null || start.object.ClassName == 'Py_None')) {
376
+ start = null;
377
+ }
378
+ if (stop instanceof AST.ASTObject && (stop.object == null || stop.object.ClassName == 'Py_None')) {
379
+ stop = null;
380
+ }
381
+
382
+ let sliceOp;
383
+ if (!start && !stop) {
384
+ sliceOp = AST.ASTSlice.SliceOp.Slice0;
385
+ } else if (!start) {
386
+ sliceOp = AST.ASTSlice.SliceOp.Slice2;
387
+ } else if (!stop) {
388
+ sliceOp = AST.ASTSlice.SliceOp.Slice1;
389
+ } else {
390
+ sliceOp = AST.ASTSlice.SliceOp.Slice3;
391
+ }
392
+
393
+ let sliceNode = new AST.ASTSlice(sliceOp, start, stop);
394
+ sliceNode.line = this.code.Current.LineNo;
395
+ let node = new AST.ASTStore(value, new AST.ASTSubscr(container, sliceNode));
396
+ node.line = this.code.Current.LineNo;
397
+ this.curBlock.append(node);
398
+ }
399
+
300
400
  function handleStoreSubscr() {
301
401
  if (this.unpack) {
302
402
  let subscrNode = this.dataStack.pop();
@@ -390,5 +490,7 @@ module.exports = {
390
490
  handleStoreSlice1,
391
491
  handleStoreSlice2,
392
492
  handleStoreSlice3,
493
+ handleBinarySlice,
494
+ handleStoreSlice,
393
495
  handleStoreSubscr,
394
496
  };
@@ -102,28 +102,30 @@ function handleBuildSetUnpackA() {
102
102
  }
103
103
 
104
104
  function handleBuildMapUnpackA() {
105
- processBuildMapUnpack.call(this);
105
+ // Py 3.5-3.8: count of mappings to merge for {**a, **b} dict literal.
106
+ processBuildMapUnpack.call(this, /*withCall=*/false);
106
107
  }
107
108
 
108
109
  function handleBuildMapUnpackWithCallA() {
109
- processBuildMapUnpack.call(this);
110
+ // Py 3.5 only: oparg = count + (fn_loc << 8). Lower byte is the map count,
111
+ // upper byte locates the callable for error reporting.
112
+ // Py 3.6-3.8: oparg = plain count (this opcode is kept for unpack-with-call
113
+ // semantics but the fn_loc byte is dropped).
114
+ processBuildMapUnpack.call(this, /*withCall=*/true);
110
115
  }
111
116
 
112
- function processBuildMapUnpack() {
113
- let mapNode = new AST.ASTMap();
114
- for (let idx = 0; idx < this.code.Current.Argument; idx++) {
115
- let pair = this.dataStack.pop(); // Should be a dictionary
116
- if (pair instanceof AST.ASTMap) {
117
- for (const entry of pair.values) {
118
- mapNode.add(entry.key, entry.value);
119
- }
120
- } else {
121
- if (global.g_cliArgs?.debug) {
122
- console.error("Expected a map for BUILD_MAP_UNPACK");
123
- }
124
- }
117
+ function processBuildMapUnpack(withCall) {
118
+ let count = this.code.Current.Argument;
119
+ if (withCall && this.object.Reader.versionCompare(3, 6) < 0) {
120
+ count = count & 0xFF;
121
+ }
122
+ let items = [];
123
+ for (let idx = 0; idx < count; idx++) {
124
+ items.unshift(this.dataStack.pop());
125
125
  }
126
- this.dataStack.push(mapNode);
126
+ let node = new AST.ASTMapUnpack(items);
127
+ node.line = this.code.Current.LineNo;
128
+ this.dataStack.push(node);
127
129
  }
128
130
 
129
131
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "depyo",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Python bytecode decompiler (Python 1.0–3.14) implemented in Node.js",
5
5
  "bin": {
6
6
  "depyo": "./depyo.js"