Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
allendowney
GitHub Repository: allendowney/cpython
Path: blob/main/Objects/exception_handling_notes.txt
12 views
1
Description of exception handling in Python 3.11
2
------------------------------------------------
3
4
Python 3.11 uses what is known as "zero-cost" exception handling.
5
Prior to 3.11, exceptions were handled by a runtime stack of "blocks".
6
7
In zero-cost exception handling, the cost of supporting exceptions is minimized.
8
In the common case (where no exception is raised) the cost is reduced
9
to zero (or close to zero).
10
The cost of raising an exception is increased, but not by much.
11
12
The following code:
13
14
def f():
15
try:
16
g(0)
17
except:
18
return "fail"
19
20
compiles as follows in 3.10:
21
22
2 0 SETUP_FINALLY 7 (to 16)
23
24
3 2 LOAD_GLOBAL 0 (g)
25
4 LOAD_CONST 1 (0)
26
6 CALL_NO_KW 1
27
8 POP_TOP
28
10 POP_BLOCK
29
12 LOAD_CONST 0 (None)
30
14 RETURN_VALUE
31
32
4 >> 16 POP_TOP
33
18 POP_TOP
34
20 POP_TOP
35
36
5 22 POP_EXCEPT
37
24 LOAD_CONST 3 ('fail')
38
26 RETURN_VALUE
39
40
Note the explicit instructions to push and pop from the "block" stack:
41
SETUP_FINALLY and POP_BLOCK.
42
43
In 3.11, the SETUP_FINALLY and POP_BLOCK are eliminated, replaced with
44
a table to determine where to jump to when an exception is raised.
45
46
1 0 RESUME 0
47
48
2 2 NOP
49
50
3 4 LOAD_GLOBAL 1 (NULL + g)
51
16 LOAD_CONST 1 (0)
52
18 PRECALL 1
53
22 CALL 1
54
32 POP_TOP
55
34 LOAD_CONST 0 (None)
56
36 RETURN_VALUE
57
>> 38 PUSH_EXC_INFO
58
59
4 40 POP_TOP
60
61
5 42 POP_EXCEPT
62
44 LOAD_CONST 2 ('fail')
63
46 RETURN_VALUE
64
>> 48 COPY 3
65
50 POP_EXCEPT
66
52 RERAISE 1
67
ExceptionTable:
68
4 to 32 -> 38 [0]
69
38 to 40 -> 48 [1] lasti
70
71
(Note this code is from 3.11, later versions may have slightly different bytecode.)
72
73
If an instruction raises an exception then its offset is used to find the target to jump to.
74
For example, the CALL at offset 22, falls into the range 4 to 32.
75
So, if g() raises an exception, then control jumps to offset 38.
76
77
78
Unwinding
79
---------
80
81
When an exception is raised, the current instruction offset is used to find following:
82
target to jump to, stack depth, and 'lasti', which determines whether the instruction
83
offset of the raising instruction should be pushed.
84
85
This information is stored in the exception table, described below.
86
87
If there is no relevant entry, the exception bubbles up to the caller.
88
89
If there is an entry, then:
90
1. pop values from the stack until it matches the stack depth for the handler.
91
2. if 'lasti' is true, then push the offset that the exception was raised at.
92
3. push the exception to the stack.
93
4. jump to the target offset and resume execution.
94
95
96
Format of the exception table
97
-----------------------------
98
99
Conceptually, the exception table consists of a sequence of 5-tuples:
100
1. start-offset (inclusive)
101
2. end-offset (exclusive)
102
3. target
103
4. stack-depth
104
5. push-lasti (boolean)
105
106
All offsets and lengths are in instructions, not bytes.
107
108
We want the format to be compact, but quickly searchable.
109
For it to be compact, it needs to have variable sized entries so that we can store common (small) offsets compactly, but handle large offsets if needed.
110
For it to be searchable quickly, we need to support binary search giving us log(n) performance in all cases.
111
Binary search typically assumes fixed size entries, but that is not necessary, as long as we can identify the start of an entry.
112
113
It is worth noting that the size (end-start) is always smaller than the end, so we encode the entries as:
114
start, size, target, depth, push-lasti
115
116
Also, sizes are limited to 2**30 as the code length cannot exceed 2**31 and each instruction takes 2 bytes.
117
It also happens that depth is generally quite small.
118
119
So, we need to encode:
120
start (up to 30 bits)
121
size (up to 30 bits)
122
target (up to 30 bits)
123
depth (up to ~8 bits)
124
lasti (1 bit)
125
126
We need a marker for the start of the entry, so the first byte of entry will have the most significant bit set.
127
Since the most significant bit is reserved for marking the start of an entry, we have 7 bits per byte to encode offsets.
128
Encoding uses a standard varint encoding, but with only 7 bits instead of the usual 8.
129
The 8 bits of a bit are (msb left) SXdddddd where S is the start bit. X is the extend bit meaning that the next byte is required to extend the offset.
130
131
In addition, we will combine depth and lasti into a single value, ((depth<<1)+lasti), before encoding.
132
133
For example, the exception entry:
134
start: 20
135
end: 28
136
target: 100
137
depth: 3
138
lasti: False
139
140
is encoded first by converting to the more compact four value form:
141
start: 20
142
size: 8
143
target: 100
144
depth<<1+lasti: 6
145
146
which is then encoded as:
147
148 (MSB + 20 for start)
148
8 (size)
149
65 (Extend bit + 1)
150
36 (Remainder of target, 100 == (1<<6)+36)
151
6
152
153
for a total of five bytes.
154
155
156
157
Script to parse the exception table
158
-----------------------------------
159
160
def parse_varint(iterator):
161
b = next(iterator)
162
val = b & 63
163
while b&64:
164
val <<= 6
165
b = next(iterator)
166
val |= b&63
167
return val
168
169
def parse_exception_table(code):
170
iterator = iter(code.co_exceptiontable)
171
try:
172
while True:
173
start = parse_varint(iterator)*2
174
length = parse_varint(iterator)*2
175
end = start + length - 2 # Present as inclusive, not exclusive
176
target = parse_varint(iterator)*2
177
dl = parse_varint(iterator)
178
depth = dl >> 1
179
lasti = bool(dl&1)
180
yield start, end, target, depth, lasti
181
except StopIteration:
182
return
183
184