Labels don't seem to be handled correctly with Python<3.9
Closed this issue · 5 comments
I am currently working with the following abstract code
stopiter = Label()
loop = Label()
genexit = Label()
exc = Label()
propagate = Label()
instrs[-1:-1] = [
Instr("DUP_TOP", lineno=lineno),
Instr("STORE_FAST", "__ddgen", lineno=lineno),
Instr("LOAD_ATTR", "asend", lineno=lineno),
Instr("STORE_FAST", "__ddgensend", lineno=lineno),
Instr("LOAD_FAST", "__ddgen", lineno=lineno),
Instr("LOAD_ATTR", "__anext__", lineno=lineno),
Instr("CALL_FUNCTION", 0, lineno=lineno),
loop,
Instr("SETUP_EXCEPT" if PY < (3, 8) else "SETUP_FINALLY", stopiter, lineno=lineno),
Instr("GET_AWAITABLE", lineno=lineno),
Instr("LOAD_CONST", None, lineno=lineno),
Instr("YIELD_FROM", lineno=lineno),
Instr("POP_BLOCK", lineno=lineno),
Instr("SETUP_EXCEPT" if PY < (3, 8) else "SETUP_FINALLY", genexit, lineno=lineno),
Instr("YIELD_VALUE", lineno=lineno),
Instr("POP_BLOCK", lineno=lineno),
Instr("LOAD_FAST", "__ddgensend", lineno=lineno),
Instr("ROT_TWO", lineno=lineno),
Instr("CALL_FUNCTION", 1, lineno=lineno),
Instr("JUMP_ABSOLUTE", loop, lineno=lineno),
stopiter, # except StopAsyncIteration:
Instr("DUP_TOP", lineno=lineno),
Instr("LOAD_CONST", StopAsyncIteration, lineno=lineno),
compare_exc(propagate, lineno),
jump_if_false(propagate, lineno),
Instr("POP_TOP", lineno=lineno),
Instr("POP_TOP", lineno=lineno),
Instr("POP_TOP", lineno=lineno),
Instr("POP_EXCEPT", lineno=lineno),
Instr("LOAD_CONST", None, lineno=lineno),
Instr("RETURN_VALUE", lineno=lineno),
propagate, # finally:
Instr("END_FINALLY" if PY < (3, 9) else "RERAISE", lineno=lineno),
genexit, # except GeneratorExit:
Instr("DUP_TOP", lineno=lineno),
Instr("LOAD_CONST", GeneratorExit, lineno=lineno),
compare_exc(exc, lineno),
jump_if_false(exc, lineno),
Instr("POP_TOP", lineno=lineno),
Instr("POP_TOP", lineno=lineno),
Instr("POP_TOP", lineno=lineno),
Instr("LOAD_FAST", "__ddgen", lineno=lineno),
Instr("LOAD_ATTR", "aclose", lineno=lineno),
Instr("CALL_FUNCTION", 0, lineno=lineno),
Instr("GET_AWAITABLE", lineno=lineno),
Instr("LOAD_CONST", None, lineno=lineno),
Instr("YIELD_FROM", lineno=lineno),
Instr("POP_EXCEPT", lineno=lineno),
Instr("RETURN_VALUE", lineno=lineno),
exc, # except:
Instr("POP_TOP", lineno=lineno),
Instr("POP_TOP", lineno=lineno),
Instr("POP_TOP", lineno=lineno),
Instr("LOAD_FAST", "__ddgen", lineno=lineno),
Instr("LOAD_ATTR", "athrow", lineno=lineno),
Instr("LOAD_CONST", sys.exc_info, lineno=lineno),
Instr("CALL_FUNCTION", 0, lineno=lineno),
Instr("CALL_FUNCTION_EX", 0, lineno=lineno),
Instr("GET_AWAITABLE", lineno=lineno),
Instr("LOAD_CONST", None, lineno=lineno),
Instr("YIELD_FROM", lineno=lineno),
Instr("POP_EXCEPT", lineno=lineno),
Instr("RETURN_VALUE", lineno=lineno),
]
When compiled to concrete Python 3.9 bytecode I get what I would expect (note the correct jump to RERAISE
)
12 DUP_TOP
14 STORE_FAST 1 (__ddgen)
16 LOAD_ATTR 0 (asend)
18 STORE_FAST 2 (__ddgensend)
20 LOAD_FAST 1 (__ddgen)
22 LOAD_ATTR 1 (__anext__)
24 CALL_FUNCTION 0
>> 26 SETUP_FINALLY 22 (to 50)
28 GET_AWAITABLE
30 LOAD_CONST 2 (None)
32 YIELD_FROM
34 POP_BLOCK
36 SETUP_FINALLY 34 (to 72)
38 YIELD_VALUE
40 POP_BLOCK
42 LOAD_FAST 2 (__ddgensend)
44 ROT_TWO
46 CALL_FUNCTION 1
48 JUMP_ABSOLUTE 26
>> 50 DUP_TOP
52 LOAD_CONST 3 (<class 'StopAsyncIteration'>)
54 JUMP_IF_NOT_EXC_MATCH 70
56 NOP
58 POP_TOP
60 POP_TOP
62 POP_TOP
64 POP_EXCEPT
66 LOAD_CONST 2 (None)
68 RETURN_VALUE
>> 70 RERAISE
>> 72 DUP_TOP
74 LOAD_CONST 4 (<class 'GeneratorExit'>)
76 JUMP_IF_NOT_EXC_MATCH 102
78 NOP
80 POP_TOP
82 POP_TOP
84 POP_TOP
86 LOAD_FAST 1 (__ddgen)
88 LOAD_ATTR 2 (aclose)
90 CALL_FUNCTION 0
92 GET_AWAITABLE
94 LOAD_CONST 2 (None)
96 YIELD_FROM
98 POP_EXCEPT
100 RETURN_VALUE
>> 102 POP_TOP
104 POP_TOP
106 POP_TOP
108 LOAD_FAST 1 (__ddgen)
110 LOAD_ATTR 3 (athrow)
112 LOAD_CONST 5 (<built-in function exc_info>)
114 CALL_FUNCTION 0
116 CALL_FUNCTION_EX 0
118 GET_AWAITABLE
120 LOAD_CONST 2 (None)
122 YIELD_FROM
124 POP_EXCEPT
126 RETURN_VALUE
128 RETURN_VALUE
However, with earlier Python versions the jump to the propagate
label is not resolved correctly and actually ends up targeting the exc
label (END_FINALLY
is now in place of the newer RERASE
, but the jump is not there!):
12 DUP_TOP
14 STORE_FAST 1 (__ddgen)
16 LOAD_ATTR 0 (asend)
18 STORE_FAST 2 (__ddgensend)
20 LOAD_FAST 1 (__ddgen)
22 LOAD_ATTR 1 (__anext__)
24 CALL_FUNCTION 0
>> 26 SETUP_FINALLY 22 (to 50)
28 GET_AWAITABLE
30 LOAD_CONST 2 (None)
32 YIELD_FROM
34 POP_BLOCK
36 SETUP_FINALLY 34 (to 72)
38 YIELD_VALUE
40 POP_BLOCK
42 LOAD_FAST 2 (__ddgensend)
44 ROT_TWO
46 CALL_FUNCTION 1
48 JUMP_ABSOLUTE 26
>> 50 DUP_TOP
52 LOAD_CONST 3 (<class 'StopAsyncIteration'>)
54 COMPARE_OP 10 (exception match)
56 POP_JUMP_IF_FALSE 102
58 POP_TOP
60 POP_TOP
62 POP_TOP
64 POP_EXCEPT
66 LOAD_CONST 2 (None)
68 RETURN_VALUE
70 END_FINALLY
>> 72 DUP_TOP
74 LOAD_CONST 4 (<class 'GeneratorExit'>)
76 COMPARE_OP 10 (exception match)
78 POP_JUMP_IF_FALSE 102
80 POP_TOP
82 POP_TOP
84 POP_TOP
86 LOAD_FAST 1 (__ddgen)
88 LOAD_ATTR 2 (aclose)
90 CALL_FUNCTION 0
92 GET_AWAITABLE
94 LOAD_CONST 2 (None)
96 YIELD_FROM
98 POP_EXCEPT
100 RETURN_VALUE
>> 102 POP_TOP
104 POP_TOP
106 POP_TOP
108 LOAD_FAST 1 (__ddgen)
110 LOAD_ATTR 3 (athrow)
112 LOAD_CONST 5 (<built-in function exc_info>)
114 CALL_FUNCTION 0
116 CALL_FUNCTION_EX 0
118 GET_AWAITABLE
120 LOAD_CONST 2 (None)
122 YIELD_FROM
124 POP_EXCEPT
126 RETURN_VALUE
128 RETURN_VALUE
This seems to point to a wrong resolution of the branching label.
Just to help me follow could you post some equivalent code/pseudo-code ?
This is an async_generator wrapper. This should be close to the following Python code
async def w(g):
c = g.__anext__
while True:
_ = c()
try:
_ = await _
except StopAsyncIteration:
return
try:
s = yield _
except GeneratorExit:
await g.aclose()
except:
await g.athrow(*sys.exc_info())
c = lambda: g.asend(s)
Can you also explicit your two functions compare_exc and pop_jump_if_false, so that I know what the bytecode looks for both versions ?
Big facepalm moment! Your last question helped me find the actual issue, which was due to copy-pasta in my code! 😢 These are the functions
def compare_exc(label, lineno):
return (
Instr("COMPARE_OP", Compare.EXC_MATCH, lineno=lineno)
if PY < (3, 9)
else Instr("JUMP_IF_NOT_EXC_MATCH", label, lineno=lineno)
)
def jump_if_false(label, lineno):
return Instr("POP_JUMP_IF_FALSE", label, lineno=lineno) if PY < (3, 9) else Instr("NOP", lineno=lineno)
which are defined locally inside another function. I had exc
in place of label
in jump_if_false
. So indeed the generated bytecode was correct as it was getting exc
from the outer scope! 😭 . Now that gives me RuntimeError: Failed to compute stacksize, got negative size
but I should be able to fix this.
Apologies for this non-issue!
Always happy to help !