1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
use crate::reachability_constraints::ScopedReachabilityConstraintId;
use crate::use_def::{ExceptionCheckpointKey, FlowSnapshot, UseDefMapBuilder};
use super::SemanticIndexBuilder;
/// Active exception handlers and the flow states from which they can be entered.
#[derive(Debug, Default)]
pub(super) enum ExceptionHandlers {
/// No handlers are active, including after their snapshots have been taken.
#[default]
None,
/// Handlers may catch the exception, but it can also propagate to an enclosing handler.
Propagating(Vec<FlowSnapshot>),
/// A bare handler catches every exception and stops outward propagation.
CatchAll(Vec<FlowSnapshot>),
}
impl ExceptionHandlers {
pub(super) fn propagating() -> Self {
Self::Propagating(Vec::new())
}
pub(super) fn catch_all() -> Self {
Self::CatchAll(Vec::new())
}
fn is_active(&self) -> bool {
!matches!(self, Self::None)
}
fn is_catch_all(&self) -> bool {
matches!(self, Self::CatchAll(_))
}
}
/// Maintains a separate [`ExceptionContextStack`] for each scope.
#[derive(Debug, Default)]
pub(super) struct ExceptionContextStackManager {
stacks: Vec<ExceptionContextStack>,
/// Number of `try` and `with` contexts still collecting exception checkpoints.
active_handler_count: usize,
}
impl ExceptionContextStackManager {
/// Push a new [`ExceptionContextStack`] onto the stack of stacks.
///
/// Each [`ExceptionContextStack`] is only valid for a single scope.
pub(super) fn enter_nested_scope(&mut self) {
self.stacks.push(ExceptionContextStack::default());
}
/// Pop an [`ExceptionContextStack`] off the stack of stacks.
///
/// Each [`ExceptionContextStack`] is only valid for a single scope.
pub(super) fn exit_scope(&mut self) {
let popped_context = self.stacks.pop();
debug_assert!(
popped_context.is_some(),
"exit_scope() should never be called on an empty stack \
(this indicates an unbalanced `enter_nested_scope()`/`exit_scope()` pair of calls)"
);
}
/// Registers a `try` statement on the current scope's exception-context stack.
///
/// Only suites with handlers collect exception checkpoints; a bare handler prevents those
/// exceptions from propagating to enclosing suites.
pub(super) fn push_try_context(
&mut self,
exception_handlers: ExceptionHandlers,
has_finally: bool,
) {
self.active_handler_count += usize::from(exception_handlers.is_active());
self.current_exception_context_stack()
.push_try_context(exception_handlers, has_finally);
}
/// Registers a context manager after it enters but before its target is assigned.
pub(super) fn push_context_manager_context(&mut self) {
self.active_handler_count += 1;
self.current_exception_context_stack()
.push_context_manager_context();
}
/// Removes the innermost context manager and returns the exceptions it could suppress.
///
/// Removing the context before its exit method runs prevents it from suppressing exceptions
/// raised by its own exit method.
pub(super) fn finish_context_manager_context(&mut self) -> Vec<FlowSnapshot> {
let snapshots = self.take_exception_snapshots();
let context = self.current_exception_context_stack().pop_context();
debug_assert!(matches!(context.kind, ExceptionContextKind::With));
snapshots
}
/// Removes the current `try` context after its handlers have been deactivated.
pub(super) fn pop_try_context(&mut self) -> ExceptionContext {
let context = self.current_exception_context_stack().pop_context();
debug_assert!(matches!(context.kind, ExceptionContextKind::Try { .. }));
debug_assert!(!context.exception_handlers.is_active());
context
}
/// Retrieve the [`ExceptionContext`] at the top of the stack, and take all
/// snapshots recorded while visiting the `try` suite.
///
/// Taking the snapshots deactivates the suite's handlers before their bodies are visited.
pub(super) fn end_try_suite(&mut self) -> Vec<FlowSnapshot> {
self.take_exception_snapshots()
}
/// Records a checkpoint for every active `try` or `with` context that could handle an
/// exception raised at the current point in control flow.
///
/// Crosses eager scopes, but stops at lazy scopes, unreachable flow, and bare handlers.
pub(super) fn record_exception_checkpoint(&mut self, builder: &mut SemanticIndexBuilder) {
debug_assert_eq!(self.stacks.len(), builder.scope_stack.len());
let mut has_intervening_finally = false;
for (scope_stack_index, exception_context_stack) in self.stacks.iter_mut().enumerate().rev()
{
let scope_id = builder.scope_stack[scope_stack_index].file_scope_id;
let use_def_map = &builder.use_def_maps[scope_id];
// Each scope has an independent flow state, so an enclosing scope can still be
// reachable while we analyze an unreachable nested scope.
if use_def_map.reachability == ScopedReachabilityConstraintId::ALWAYS_FALSE {
break;
}
if !exception_context_stack
.record_exception_checkpoint(use_def_map, &mut has_intervening_finally)
{
break;
}
if !builder.exception_checkpoint_crosses_scope_boundary(scope_id) {
break;
}
}
}
/// Returns whether an active `try` or `with` context can receive an exception from this scope.
///
/// A context can remain on the stack for its `finally` suite after its handlers become inactive.
pub(super) fn has_active_exception_handler(&self, builder: &SemanticIndexBuilder) -> bool {
if self.active_handler_count == 0 {
return false;
}
debug_assert_eq!(self.stacks.len(), builder.scope_stack.len());
for (scope_stack_index, exception_context_stack) in self.stacks.iter().enumerate().rev() {
if exception_context_stack.has_active_exception_handler() {
return true;
}
let scope_id = builder.scope_stack[scope_stack_index].file_scope_id;
if !builder.exception_checkpoint_crosses_scope_boundary(scope_id) {
return false;
}
}
false
}
/// Returns whether an enclosing context manager has already seen an exception checkpoint.
pub(super) fn has_context_manager_exception_checkpoint(&self) -> bool {
self.stacks.last().is_some_and(|stack| {
stack.0.iter().any(|context| {
matches!(context.kind, ExceptionContextKind::With)
&& context.last_checkpoint_key.is_some()
})
})
}
/// Records that a context manager makes an apparently terminal control-flow path possibly
/// non-terminal because it may silence an earlier exception. Whether it actually suppresses
/// exceptions is determined during type inference.
pub(super) fn record_deferred_terminal_context_manager_exit(&mut self) {
if let Some(context) = self.current_exception_context_stack().innermost_try() {
context.has_deferred_terminal_context_manager_exit = true;
}
}
/// Forwards a deferred terminal state to the nearest enclosing `try`.
pub(super) fn propagate_deferred_terminal_context_manager_exit(
&mut self,
terminal_snapshot: FlowSnapshot,
) {
if let Some(context) = self.current_exception_context_stack().innermost_try() {
context.has_deferred_terminal_context_manager_exit = true;
context
.terminal_finally_entry_snapshots
.push(terminal_snapshot);
}
}
/// Records a terminal entry for the nearest enclosing `try`, skipping `with` contexts.
pub(super) fn record_terminal_finally_entry(&mut self, builder: &SemanticIndexBuilder) {
self.current_exception_context_stack()
.record_terminal_finally_entry(builder);
}
/// Takes the current context's snapshots and updates the number of active handlers.
fn take_exception_snapshots(&mut self) -> Vec<FlowSnapshot> {
if let Some(snapshots) = self
.current_exception_context_stack()
.take_exception_snapshots()
{
self.active_handler_count -= 1;
snapshots
} else {
Vec::new()
}
}
/// Retrieve the [`ExceptionContextStack`] that is relevant for the current scope.
fn current_exception_context_stack(&mut self) -> &mut ExceptionContextStack {
self.stacks
.last_mut()
.expect("There should always be at least one `ExceptionContextStack` on the stack")
}
}
/// The contexts of nested `try` and `with` statements for a single scope.
#[derive(Debug, Default)]
struct ExceptionContextStack(Vec<ExceptionContext>);
impl ExceptionContextStack {
/// Returns whether a `try` or `with` context is still collecting exception checkpoints.
fn has_active_exception_handler(&self) -> bool {
self.0
.iter()
.any(|context| context.exception_handlers.is_active())
}
/// Registers a `try` statement and whether exceptions must first pass through cleanup.
fn push_try_context(&mut self, exception_handlers: ExceptionHandlers, has_finally: bool) {
self.0.push(ExceptionContext::new(
ExceptionContextKind::Try { has_finally },
exception_handlers,
));
}
/// Registers a context manager that may receive exceptions from its body.
fn push_context_manager_context(&mut self) {
self.0.push(ExceptionContext::new(
ExceptionContextKind::With,
ExceptionHandlers::propagating(),
));
}
/// Pop an [`ExceptionContext`] off the stack.
fn pop_context(&mut self) -> ExceptionContext {
self.0
.pop()
.expect("Cannot pop an exception context off an empty `ExceptionContextStack`")
}
/// Takes the innermost context's snapshots if it has active handlers, deactivating them.
fn take_exception_snapshots(&mut self) -> Option<Vec<FlowSnapshot>> {
let context = self
.0
.last_mut()
.expect("Cannot take snapshots from an empty `ExceptionContextStack`");
match std::mem::take(&mut context.exception_handlers) {
ExceptionHandlers::None => None,
ExceptionHandlers::Propagating(snapshots) | ExceptionHandlers::CatchAll(snapshots) => {
Some(snapshots)
}
}
}
/// Records a checkpoint for every active `try` or `with` context in this scope.
/// Returns whether the checkpoint should continue propagating to an enclosing scope.
///
/// A bare handler consumes the exception, preventing any outer handler from seeing it. A
/// `finally` suite prevents enclosing context managers from receiving a checkpoint until its
/// cleanup has run, while preserving existing outer-`try` handler behavior. The snapshot is
/// constructed only if a handler has not already observed the current flow state.
fn record_exception_checkpoint(
&mut self,
use_def_map: &UseDefMapBuilder<'_>,
has_intervening_finally: &mut bool,
) -> bool {
let checkpoint_key = use_def_map.exception_checkpoint_key();
for context in self.0.iter_mut().rev() {
if *has_intervening_finally && matches!(context.kind, ExceptionContextKind::With) {
continue;
}
match &mut context.exception_handlers {
ExceptionHandlers::None => context.has_escaping_exception = true,
ExceptionHandlers::Propagating(snapshots)
| ExceptionHandlers::CatchAll(snapshots) => {
if context.last_checkpoint_key != Some(checkpoint_key) {
snapshots.push(use_def_map.snapshot());
context.last_checkpoint_key = Some(checkpoint_key);
}
if context.exception_handlers.is_catch_all() {
return false;
}
context.has_escaping_exception = true;
}
}
*has_intervening_finally |= matches!(
context.kind,
ExceptionContextKind::Try { has_finally: true }
);
}
true
}
/// Records a terminal entry for the nearest `try` context, skipping intervening `with` contexts.
fn record_terminal_finally_entry(&mut self, builder: &SemanticIndexBuilder) {
if let Some(context) = self.innermost_try() {
context
.terminal_finally_entry_snapshots
.push(builder.flow_snapshot());
}
}
/// Finds the nearest enclosing `try`, skipping context managers.
fn innermost_try(&mut self) -> Option<&mut ExceptionContext> {
self.0
.iter_mut()
.rev()
.find(|context| matches!(context.kind, ExceptionContextKind::Try { .. }))
}
}
/// Distinguishes `try` exception contexts from `with` exception contexts.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum ExceptionContextKind {
Try { has_finally: bool },
With,
}
/// Exception-entry states for one `try` or `with` statement.
///
/// Only `try` contexts also collect terminal entries for a `finally` suite.
#[derive(Debug)]
pub(super) struct ExceptionContext {
exception_handlers: ExceptionHandlers,
kind: ExceptionContextKind,
last_checkpoint_key: Option<ExceptionCheckpointKey>,
/// Whether an exception escaped this suite and must also propagate after its cleanup.
has_escaping_exception: bool,
/// Whether apparently terminal control flow in a nested context-manager body, such as a
/// `return` or `raise`, may become non-terminal if type inference determines that the context
/// manager suppresses exceptions. This flag belongs to the enclosing `try` context because it
/// affects control flow into its `finally` suite.
has_deferred_terminal_context_manager_exit: bool,
terminal_finally_entry_snapshots: Vec<FlowSnapshot>,
}
impl ExceptionContext {
fn new(kind: ExceptionContextKind, exception_handlers: ExceptionHandlers) -> Self {
Self {
exception_handlers,
kind,
last_checkpoint_key: None,
has_escaping_exception: false,
has_deferred_terminal_context_manager_exit: false,
terminal_finally_entry_snapshots: Vec::new(),
}
}
pub(super) fn into_finally_entry_state(self) -> (Vec<FlowSnapshot>, bool, bool) {
(
self.terminal_finally_entry_snapshots,
self.has_escaping_exception,
self.has_deferred_terminal_context_manager_exit,
)
}
}