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.
@@ -33,7 +33,10 @@ function extractWithPattern() {
33
33
 
34
34
  if (thirdInstr && thirdInstr.OpCodeID === this.OpCodes.STORE_FAST_A) {
35
35
  // Has "as variable"
36
- asVariable = new AST.ASTName(thirdInstr.Name?.toString() || "###FIXME###");
36
+ if (thirdInstr.Name == null) {
37
+ throw new Error(`SETUP_WITH at offset ${this.code.Current.Offset} (file offset ${this.object.codeOffset + this.code.Current.Offset}): STORE_FAST has null Name (expected "as" variable)`);
38
+ }
39
+ asVariable = new AST.ASTName(thirdInstr.Name.toString());
37
40
  } else if (thirdInstr && thirdInstr.OpCodeID === this.OpCodes.POP_TOP) {
38
41
  // No "as" clause
39
42
  asVariable = null;
@@ -62,11 +65,14 @@ function extractWithPattern() {
62
65
  instr.OpCodeID === this.OpCodes.LOAD_GLOBAL_A ||
63
66
  instr.OpCodeID === this.OpCodes.LOAD_NAME_A ||
64
67
  instr.OpCodeID === this.OpCodes.LOAD_DEREF_A) {
65
- ctxMgrExpr = new AST.ASTName(instr.Name?.toString() || "###FIXME###");
66
- }else {
67
- // For CALL_FUNCTION, LOAD_ATTR, etc., we can't easily reconstruct
68
- // the expression without replaying the stack. Use placeholder for now.
69
- ctxMgrExpr = new AST.ASTName("###FIXME###");
68
+ if (instr.Name == null) {
69
+ throw new Error(`SETUP_WITH at offset ${this.code.Current.Offset} (file offset ${this.object.codeOffset + this.code.Current.Offset}): ${instr.InstructionName} at offset ${instr.Offset} has null Name`);
70
+ }
71
+ ctxMgrExpr = new AST.ASTName(instr.Name.toString());
72
+ } else {
73
+ // CALL_FUNCTION/LOAD_ATTR/BINARY_SUBSCR: reconstructing the
74
+ // expression without replaying the stack isn't supported yet.
75
+ throw new Error(`SETUP_WITH at offset ${this.code.Current.Offset} (file offset ${this.object.codeOffset + this.code.Current.Offset}): unsupported context manager opcode ${instr.InstructionName} at offset ${instr.Offset} — needs stack replay`);
70
76
  }
71
77
  break;
72
78
  }
@@ -117,6 +123,102 @@ function handleWithCleanupStart() {
117
123
  }
118
124
 
119
125
  function handleWithCleanup() {
126
+ // Async-with closure runs first because the 3.8+ pattern uses BEGIN_FINALLY
127
+ // (no-op handler) instead of LOAD_CONST None — so the data stack may be
128
+ // empty when WITH_CLEANUP_START fires. Don't gate on the None pop here.
129
+ if (this.curBlock.blockType == AST.ASTBlock.BlockType.AsyncWith
130
+ && this.curBlock.end == this.code.Current.Offset) {
131
+ let asyncWithBlock = this.curBlock;
132
+
133
+ // Skip the standard 4-op cleanup epilogue:
134
+ // GET_AWAITABLE, LOAD_CONST None, YIELD_FROM, WITH_CLEANUP_FINISH
135
+ let standardEpilogue = [
136
+ this.OpCodes.GET_AWAITABLE,
137
+ this.OpCodes.LOAD_CONST_A,
138
+ this.OpCodes.YIELD_FROM,
139
+ this.OpCodes.WITH_CLEANUP_FINISH,
140
+ ];
141
+ for (let expectedOp of standardEpilogue) {
142
+ if (this.code.Next?.OpCodeID === expectedOp) {
143
+ this.code.GoNext();
144
+ } else {
145
+ break;
146
+ }
147
+ }
148
+
149
+ // After the standard epilogue, two patterns are possible (3.8+):
150
+ // Normal exit: END_FINALLY
151
+ // Early return path: POP_FINALLY 0 / LOAD_CONST X / RETURN_VALUE
152
+ // / WITH_CLEANUP_START / GET_AWAITABLE / LOAD_CONST None
153
+ // / YIELD_FROM / WITH_CLEANUP_FINISH / END_FINALLY
154
+ // [/ LOAD_CONST None / RETURN_VALUE -- dead implicit return]
155
+ // For the early-return case we synthesize the explicit return into the body
156
+ // and skip the trailing exception handler + dead implicit return.
157
+ let after = this.code.Next;
158
+ if (after?.OpCodeID === this.OpCodes.POP_FINALLY_A
159
+ || after?.OpCodeID === this.OpCodes.POP_FINALLY) {
160
+ this.code.GoNext();
161
+
162
+ let loadConst = this.code.Next;
163
+ let returnValue = this.code.PeekNextInstruction(2);
164
+ if (loadConst?.OpCodeID === this.OpCodes.LOAD_CONST_A
165
+ && returnValue?.OpCodeID === this.OpCodes.RETURN_VALUE) {
166
+ let constObj = new AST.ASTObject(loadConst.ConstantObject);
167
+ constObj.line = loadConst.LineNo;
168
+ let retValue;
169
+ if (constObj.object == null || constObj.object.ClassName == "Py_None") {
170
+ retValue = new AST.ASTNone();
171
+ } else {
172
+ retValue = constObj;
173
+ }
174
+ let retNode = new AST.ASTReturn(retValue);
175
+ retNode.line = loadConst.LineNo;
176
+ asyncWithBlock.append(retNode);
177
+ this.code.GoNext();
178
+ this.code.GoNext();
179
+ }
180
+
181
+ let exceptionHandler = [
182
+ this.OpCodes.WITH_CLEANUP_START,
183
+ this.OpCodes.GET_AWAITABLE,
184
+ this.OpCodes.LOAD_CONST_A,
185
+ this.OpCodes.YIELD_FROM,
186
+ this.OpCodes.WITH_CLEANUP_FINISH,
187
+ this.OpCodes.END_FINALLY,
188
+ ];
189
+ for (let expectedOp of exceptionHandler) {
190
+ if (this.code.Next?.OpCodeID === expectedOp) {
191
+ this.code.GoNext();
192
+ } else {
193
+ break;
194
+ }
195
+ }
196
+
197
+ // Skip the dead implicit LOAD_CONST None / RETURN_VALUE if it's
198
+ // the very tail of the function (no more instructions after).
199
+ let load = this.code.Next;
200
+ let ret = this.code.PeekNextInstruction(2);
201
+ let beyond = this.code.PeekNextInstruction(3);
202
+ if (load?.OpCodeID === this.OpCodes.LOAD_CONST_A
203
+ && ret?.OpCodeID === this.OpCodes.RETURN_VALUE
204
+ && beyond == null) {
205
+ this.code.GoNext();
206
+ this.code.GoNext();
207
+ }
208
+ } else if (after?.OpCodeID === this.OpCodes.END_FINALLY) {
209
+ this.code.GoNext();
210
+ }
211
+
212
+ this.blocks.pop();
213
+ this.curBlock = this.blocks.top();
214
+ this.curBlock.append(asyncWithBlock);
215
+
216
+ if (global.g_cliArgs?.debug) {
217
+ console.log(`ASYNC WITH block closed, nodes count=${asyncWithBlock.nodes.length}`);
218
+ }
219
+ return;
220
+ }
221
+
120
222
  // Stack top should be a None. Ignore it.
121
223
  let none = this.dataStack.pop();
122
224
 
@@ -226,19 +328,106 @@ function handleBeforeWith() {
226
328
  }
227
329
  }
228
330
 
331
+ /**
332
+ * Handler for BEFORE_ASYNC_WITH (Python 3.5-3.10).
333
+ *
334
+ * Bytecode pattern for `async with EXPR [as VAR]:`:
335
+ * LOAD_* EXPR (context manager)
336
+ * BEFORE_ASYNC_WITH <-- we are here
337
+ * GET_AWAITABLE
338
+ * LOAD_CONST None
339
+ * YIELD_FROM
340
+ * SETUP_ASYNC_WITH +N (N targets WITH_CLEANUP_START — end of body)
341
+ * POP_TOP | STORE_FAST VAR
342
+ * ... body ...
343
+ * POP_BLOCK
344
+ * LOAD_CONST None
345
+ * WITH_CLEANUP_START (closes block, see handleWithCleanup)
346
+ * GET_AWAITABLE
347
+ * LOAD_CONST None
348
+ * YIELD_FROM
349
+ * WITH_CLEANUP_FINISH
350
+ * END_FINALLY
351
+ *
352
+ * We pop the context manager off the data stack, then use GoNext to consume
353
+ * the entire setup epilogue. This bypasses the GET_AWAITABLE / YIELD_FROM
354
+ * handlers (which would otherwise emit `await await(EXPR.__aenter__)` noise)
355
+ * and creates a clean ASTAsyncWithBlock for the body.
356
+ */
229
357
  function handleBeforeAsyncWith() {
230
- let ctxmgr = this.dataStack.top();
231
- let callNode = new AST.ASTCall(new AST.ASTName('await'), [new AST.ASTBinary(ctxmgr, new AST.ASTName('__aenter__'), AST.ASTBinary.BinOp.Attr)], []);
232
- callNode.line = this.code.Current.LineNo;
233
- this.dataStack.push(callNode);
234
- }
358
+ let ctxMgr = this.dataStack.pop();
235
359
 
236
- function handleSetupAsyncWithA() {
237
- let asyncWithBlock = new AST.ASTBlock(AST.ASTBlock.BlockType.AsyncWith, this.code.Current.Offset, this.code.Current.JumpTarget);
360
+ let getAwaitable = this.code.PeekNextInstruction(1);
361
+ let loadNone = this.code.PeekNextInstruction(2);
362
+ let yieldFrom = this.code.PeekNextInstruction(3);
363
+ let setupAsyncWith = this.code.PeekNextInstruction(4);
364
+
365
+ if (!getAwaitable || getAwaitable.OpCodeID !== this.OpCodes.GET_AWAITABLE
366
+ || !loadNone || loadNone.OpCodeID !== this.OpCodes.LOAD_CONST_A
367
+ || !yieldFrom || yieldFrom.OpCodeID !== this.OpCodes.YIELD_FROM
368
+ || !setupAsyncWith || setupAsyncWith.OpCodeID !== this.OpCodes.SETUP_ASYNC_WITH_A) {
369
+ throw new Error(`BEFORE_ASYNC_WITH at offset ${this.code.Current.Offset}: expected GET_AWAITABLE/LOAD_CONST/YIELD_FROM/SETUP_ASYNC_WITH sequence, got ${getAwaitable?.InstructionName}/${loadNone?.InstructionName}/${yieldFrom?.InstructionName}/${setupAsyncWith?.InstructionName}`);
370
+ }
371
+
372
+ let asClause = this.code.PeekNextInstruction(5);
373
+ let asVar = null;
374
+ let extraSkip = 0;
375
+ if (asClause?.OpCodeID === this.OpCodes.STORE_FAST_A) {
376
+ if (asClause.Name == null) {
377
+ throw new Error(`BEFORE_ASYNC_WITH at offset ${this.code.Current.Offset}: STORE_FAST has null Name (expected "as" variable)`);
378
+ }
379
+ asVar = new AST.ASTName(asClause.Name.toString());
380
+ extraSkip = 1;
381
+ } else if (asClause?.OpCodeID === this.OpCodes.POP_TOP) {
382
+ extraSkip = 1;
383
+ }
384
+
385
+ // Walk forward from the body start to find the matching WITH_CLEANUP_START
386
+ // for THIS async-with (not a nested one). Track depth so nested
387
+ // SETUP_WITH/SETUP_ASYNC_WITH are skipped over correctly.
388
+ //
389
+ // Note: SETUP_ASYNC_WITH.JumpTarget is unreliable here — for the early-return
390
+ // pattern it points to the exception-path cleanup, not the normal exit.
391
+ let blockEnd = setupAsyncWith.JumpTarget;
392
+ let depth = 0;
393
+ let searchIdx = this.code.CurrentInstructionIndex + 5 + extraSkip;
394
+ for (let i = searchIdx; i < this.code.Instructions.length; i++) {
395
+ let instr = this.code.Instructions[i];
396
+ if (!instr) break;
397
+ if (instr.OpCodeID === this.OpCodes.SETUP_ASYNC_WITH_A
398
+ || instr.OpCodeID === this.OpCodes.SETUP_WITH_A) {
399
+ depth++;
400
+ continue;
401
+ }
402
+ if (instr.OpCodeID === this.OpCodes.WITH_CLEANUP_START
403
+ || instr.OpCodeID === this.OpCodes.WITH_CLEANUP) {
404
+ if (depth === 0) {
405
+ blockEnd = instr.Offset;
406
+ break;
407
+ }
408
+ depth--;
409
+ }
410
+ }
411
+
412
+ // Skip GET_AWAITABLE, LOAD_CONST None, YIELD_FROM, SETUP_ASYNC_WITH, [POP_TOP|STORE_FAST]
413
+ this.code.GoNext(4 + extraSkip);
414
+
415
+ let asyncWithBlock = new AST.ASTAsyncWithBlock(this.code.Current.Offset, blockEnd);
416
+ asyncWithBlock.expr = ctxMgr;
417
+ asyncWithBlock.var = asVar;
418
+ asyncWithBlock.init();
238
419
  this.blocks.push(asyncWithBlock);
239
420
  this.curBlock = this.blocks.top();
240
421
  }
241
422
 
423
+ function handleSetupAsyncWithA() {
424
+ // Normally consumed by handleBeforeAsyncWith via GoNext. If we land here,
425
+ // the lookahead failed — bail silently rather than pushing a malformed block.
426
+ if (global.g_cliArgs?.debug) {
427
+ console.error(`SETUP_ASYNC_WITH at offset ${this.code.Current.Offset}: reached without preceding BEFORE_ASYNC_WITH consumption — ignoring`);
428
+ }
429
+ }
430
+
242
431
  module.exports = {
243
432
  handleBeforeWith,
244
433
  handleBeforeAsyncWith,