depyo 1.0.2 → 1.1.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.
@@ -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({
@@ -521,7 +582,10 @@ function hasUpcomingStringBuild(context) {
521
582
  context.OpCodes.STORE_DEREF_A
522
583
  ]);
523
584
 
524
- for (let step = 1; step <= 12; step++) {
585
+ // 3.11+ inline caches inflate the physical instruction stream; a single
586
+ // f-string with a few interpolations easily spans 30+ code slots before
587
+ // reaching BUILD_STRING. Cap generously and rely on earlyStops to bound.
588
+ for (let step = 1; step <= 48; step++) {
525
589
  const instr = context.code.PeekInstructionAtOffset(context.code.Current.Offset + step * 2);
526
590
  if (!instr) {
527
591
  break;
@@ -603,6 +667,27 @@ function handlePopTop() {
603
667
  this.OpCodes.JUMP_IF_FALSE_A
604
668
  ].includes(this.code.Prev?.OpCodeID);
605
669
 
670
+ // A POP_TOP following ROT_TWO/ROT_THREE/ROT_FOUR is part of binding-extraction
671
+ // stack cleanup inside a class/sequence pattern, not the body-start POP_TOP.
672
+ let prevIsRot = [
673
+ this.OpCodes.ROT_TWO,
674
+ this.OpCodes.ROT_THREE,
675
+ this.OpCodes.ROT_FOUR
676
+ ].includes(this.code.Prev?.OpCodeID);
677
+
678
+ // If the next non-CACHE opcode is STORE_FAST/STORE_NAME, this POP_TOP is mid-pattern
679
+ // binding cleanup (pops the match result tuple before the remaining STORE captures).
680
+ let nextIsStoreBinding = (() => {
681
+ let scan = this.code.Next;
682
+ let steps = 0;
683
+ while (scan && steps < 4) {
684
+ if (scan.OpCodeID == this.OpCodes.CACHE) { scan = scan.Next; steps++; continue; }
685
+ return scan.OpCodeID == this.OpCodes.STORE_FAST_A ||
686
+ scan.OpCodeID == this.OpCodes.STORE_NAME_A;
687
+ }
688
+ return false;
689
+ })();
690
+
606
691
  const patternHasOps = (this.patternOps?.length || 0) > 0;
607
692
  let isLiteralPattern = patternHasOps && !this.patternOps.some(op => ["MATCH_SEQUENCE","MATCH_CLASS","MATCH_MAPPING","MATCH_KEYS"].includes(op.type));
608
693
  let shouldStartCase = readyForWildcard || !!this.inMatchPattern;
@@ -612,6 +697,41 @@ function handlePopTop() {
612
697
  if (patternHasOps && isLiteralPattern && !prevIsJump) {
613
698
  shouldStartCase = false;
614
699
  }
700
+ if (patternHasOps && prevIsRot) {
701
+ shouldStartCase = false;
702
+ }
703
+ if (patternHasOps && nextIsStoreBinding) {
704
+ shouldStartCase = false;
705
+ }
706
+
707
+ // When readyForWildcard fires on the match-exit POP_TOP and the only
708
+ // code that follows is the function's implicit `return None` tail,
709
+ // the source didn't actually include a `case _:` clause — CPython
710
+ // emits the tail unconditionally. Suppress the phantom wildcard.
711
+ if (readyForWildcard && !patternHasOps) {
712
+ let scan = this.code.Next;
713
+ let steps = 0;
714
+ let sawLoadNone = false;
715
+ while (scan && steps < 6) {
716
+ const op = scan.OpCodeID;
717
+ if (op == this.OpCodes.CACHE ||
718
+ op == this.OpCodes.RESUME_A ||
719
+ op == this.OpCodes.NOP) {
720
+ scan = scan.Next; steps++; continue;
721
+ }
722
+ if (op == this.OpCodes.LOAD_CONST_A && !sawLoadNone) {
723
+ sawLoadNone = true;
724
+ scan = scan.Next; steps++; continue;
725
+ }
726
+ if (op == this.OpCodes.RETURN_VALUE || op == this.OpCodes.RETURN_CONST_A) {
727
+ if (sawLoadNone || op == this.OpCodes.RETURN_CONST_A) {
728
+ shouldStartCase = false;
729
+ }
730
+ break;
731
+ }
732
+ break;
733
+ }
734
+ }
615
735
 
616
736
  if (shouldStartCase) {
617
737
  if (beginMatchCaseFromPattern.call(this, {reason: 'pop_top'})) {
@@ -628,7 +748,29 @@ function handlePopTop() {
628
748
  return;
629
749
  }
630
750
 
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)) {
751
+ // Mid-UNPACK_SEQUENCE POP_TOP: CPython emits `a, _, b = tup` as
752
+ // UNPACK 3 / STORE a / POP_TOP / STORE b, where POP_TOP drops the
753
+ // middle slot. Our unpack-state-machine accumulates STORE targets
754
+ // into a tuple; treat POP_TOP as an anonymous `_` slot so the
755
+ // remaining STORE_FAST still sees the tuple on the stack.
756
+ if (this.unpack > 0) {
757
+ let tupleNode = this.dataStack.top();
758
+ if (tupleNode instanceof AST.ASTTuple) {
759
+ let placeholder = new AST.ASTName("_");
760
+ if (this.starPos-- == 0) placeholder.name = "*_";
761
+ tupleNode.add(placeholder);
762
+ if (--this.unpack <= 0) {
763
+ this.dataStack.pop();
764
+ let seqNode = this.dataStack.pop();
765
+ let node = new AST.ASTStore(seqNode, tupleNode);
766
+ node.line = this.code.Current.LineNo;
767
+ this.curBlock.append(node);
768
+ }
769
+ return;
770
+ }
771
+ }
772
+
773
+ 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
774
  if (this.curBlock.blockType == AST.ASTBlock.BlockType.Except) {
633
775
  // Skipping POP_TOP, POP_TOP, POP_TOP
634
776
  if ([this.OpCodes.JUMP_ABSOLUTE_A, this.OpCodes.JUMP_FORWARD_A].includes(this.code.Prev.OpCodeID)) {
@@ -685,7 +827,13 @@ function handlePopTop() {
685
827
  }
686
828
  if (!this.curBlock.inited) {
687
829
  if (this.curBlock.blockType == AST.ASTBlock.BlockType.With) {
830
+ // POP_TOP initializing a WITH block consumes the context-manager
831
+ // expression off the stack (Py3.0+ `with EXPR:` with no `as`).
832
+ // The value IS the with-expr, not a body statement — return before
833
+ // the fall-through append would add a phantom duplicate.
688
834
  this.curBlock.expr = value;
835
+ this.curBlock.init();
836
+ return;
689
837
  } else if (this.curBlock.blockType == AST.ASTBlock.BlockType.If && !this.curBlock.condition) {
690
838
  this.curBlock.condition = value;
691
839
  }
@@ -702,17 +850,59 @@ function handlePopTop() {
702
850
  this.curBlock.append(value);
703
851
  }
704
852
 
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) {
853
+ /* Python 2.3 listcomp body uses `_[1](x)` (the cached list.append call)
854
+ * instead of LIST_APPEND. Detect this call, whether emitted directly in
855
+ * a For+comp body or nested inside if-filters (`[x for x in y if c]`),
856
+ * and materialize an ASTComprehension instead of leaving the call as a
857
+ * bogus body statement. */
858
+ if (value instanceof AST.ASTCall) {
859
+ let forBlock = null;
860
+ let ifBlocks = [];
861
+ for (let i = this.blocks.length - 1; i >= 0; i--) {
862
+ const blk = this.blocks[i];
863
+ if (blk.blockType == AST.ASTBlock.BlockType.For && blk.comprehension) {
864
+ forBlock = blk;
865
+ break;
866
+ }
867
+ if (blk.blockType == AST.ASTBlock.BlockType.If) {
868
+ ifBlocks.push(blk);
869
+ } else {
870
+ break;
871
+ }
872
+ }
873
+
874
+ const funcIsCompAppend = forBlock && value.func &&
875
+ this._listCompAppendRefs &&
876
+ Object.values(this._listCompAppendRefs).includes(value.func);
877
+ const isDirectCompBody = forBlock && this.curBlock === forBlock;
878
+
879
+ if (funcIsCompAppend || isDirectCompBody) {
712
880
  let pparams = value.pparams;
713
881
  if (!pparams.empty()) {
882
+ if (funcIsCompAppend && this.curBlock !== forBlock) {
883
+ for (const ifBlk of ifBlocks) {
884
+ let cond = ifBlk.condition;
885
+ if (!cond) continue;
886
+ if (ifBlk.negative) {
887
+ const notCond = new AST.ASTUnary(cond, AST.ASTUnary.UnaryOp.Not);
888
+ notCond.line = this.code.Current.LineNo;
889
+ cond = notCond;
890
+ }
891
+ if (forBlock.condition) {
892
+ const andCond = new AST.ASTBinary(forBlock.condition, cond, AST.ASTBinary.BinOp.LogicalAnd);
893
+ andCond.line = this.code.Current.LineNo;
894
+ forBlock.condition = andCond;
895
+ } else {
896
+ forBlock.condition = cond;
897
+ }
898
+ }
899
+ while (this.curBlock !== forBlock && this.blocks.length > 1) {
900
+ this.blocks.pop();
901
+ this.curBlock = this.blocks.top();
902
+ }
903
+ }
714
904
  let res = pparams[0];
715
- let node = new AST.ASTComprehension (res);
905
+ let node = new AST.ASTComprehension(res);
716
906
  node.line = this.code.Current.LineNo;
717
907
  this.dataStack.push(node);
718
908
  }
@@ -809,7 +999,15 @@ function handleReturnValue() {
809
999
  // Only start new case if no current case AND (not in pattern OR has unflushed pattern ops)
810
1000
  const hasUnflushedPattern = (this.patternOps?.length || 0) > 0;
811
1001
  const needsNewCase = !this.currentCase && (!this.inMatchPattern || hasUnflushedPattern);
812
- if (needsNewCase) {
1002
+ // Suppress phantom wildcard when this is the function-tail implicit
1003
+ // return None: no pattern ops recorded, LOAD_CONST None precedes us,
1004
+ // and this is the last RETURN_VALUE in the code object. CPython
1005
+ // emits the tail whether or not source had `case _:`.
1006
+ const topIsNone = this.dataStack.top() instanceof AST.ASTNone ||
1007
+ (this.dataStack.top() instanceof AST.ASTObject &&
1008
+ this.dataStack.top().obj?.ClassName === 'Py_None');
1009
+ const isImplicitTail = !hasUnflushedPattern && topIsNone && !this.code.Next;
1010
+ if (needsNewCase && !isImplicitTail) {
813
1011
  beginMatchCaseFromPattern.call(this, {reason: 'return'});
814
1012
  }
815
1013
  }
@@ -938,7 +1136,15 @@ function handleReturnConstA() {
938
1136
  nextOp.OpCodeID === this.OpCodes.RERAISE;
939
1137
  const prevIsPOPExcept = this.code.Prev?.OpCodeID === this.OpCodes.POP_EXCEPT;
940
1138
  const nextIsSWAP = nextOp?.OpCodeID === this.OpCodes.SWAP_A;
1139
+ const nextIsRERAISE = nextOp?.OpCodeID === this.OpCodes.RERAISE ||
1140
+ nextOp?.OpCodeID === this.OpCodes.RERAISE_A;
941
1141
  const isExceptCleanup = isNone && prevIsPOPExcept && nextIsSWAP;
1142
+ // CPython 3.11+ places the function's implicit `return None` at the tail of an
1143
+ // except handler (POP_EXCEPT → RETURN_CONST None → RERAISE or end-of-function).
1144
+ // The source has no explicit return there; drop it so the handler body doesn't
1145
+ // inherit a phantom return.
1146
+ const isExceptTailImplicitReturn = isNone && prevIsPOPExcept &&
1147
+ (nextIsRERAISE || !nextOp);
942
1148
 
943
1149
  if (isNone && isModuleLevel && isAtEnd) {
944
1150
  return; // Skip implicit return None at module end
@@ -946,6 +1152,9 @@ function handleReturnConstA() {
946
1152
  if (isExceptCleanup) {
947
1153
  return; // Skip return None in exception cleanup (except* success path)
948
1154
  }
1155
+ if (isExceptTailImplicitReturn) {
1156
+ return; // Skip synthetic return None at end of except handler
1157
+ }
949
1158
 
950
1159
  let value = new AST.ASTObject(this.code.Current.ConstantObject);
951
1160
  let node = new AST.ASTReturn(value);
@@ -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
  };