Skip to content

Commit 73f3f65

Browse files
committed
codegen: seek to the finally body's own scopes before copying it
An early exit from a try block emits an extra copy of the finally body. Nested scopes are handed out by position, and the cursors still sit inside the try block's own run of scopes there, so the copy took those instead of the ones the finally body opens: a generator expression got a sibling's symbol table and raised "the symbol 'k' must be present in the symbol table", or, where the sibling was a named scope, built a code object with no `.0` argument that raised UnboundLocalError when it ran. Seek the cursors to the first scope beginning on or after the finally body's first line before compiling the copy. The existing restore then leaves the try statement's own copies starting from the same place. Assisted-by: Claude
1 parent fa71120 commit 73f3f65

2 files changed

Lines changed: 153 additions & 3 deletions

File tree

‎crates/codegen/src/compile.rs‎

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2414,10 +2414,22 @@ impl<'warnings> Compiler<'warnings> {
24142414

24152415
if let FBlockDatum::FinallyBody(ref body) = info.fb_datum {
24162416
// This is an extra copy of the finally body, emitted for the
2417-
// path that leaves the try block early. The try statement
2418-
// emits its own copies afterwards, so rewind the symbol table
2419-
// cursors and leave the nested scopes for those copies.
2417+
// path that leaves the try block early. Nested scopes are
2418+
// handed out by position, and the cursors are still inside
2419+
// the try block's own run of scopes here, so seek them to the
2420+
// ones this body opened before compiling it. The try
2421+
// statement emits its own copies from the same place
2422+
// afterwards, so put the cursors back when the copy is done.
24202423
let symbol_table_cursors = self.current_symbol_table_cursors();
2424+
if let Some(first) = body.first() {
2425+
let line = self
2426+
.source_file
2427+
.to_source_code()
2428+
.line_index(first.range().start())
2429+
.get()
2430+
.to_u32();
2431+
self.seek_symbol_table_cursors_to_line(line);
2432+
}
24212433
self.compile_statements(body)?;
24222434
self.set_symbol_table_cursors(symbol_table_cursors);
24232435
}
@@ -10354,6 +10366,43 @@ impl<'warnings> Compiler<'warnings> {
1035410366
table.next_inlined_comprehension_block = cursors.inlined_comprehension_block;
1035510367
}
1035610368

10369+
/// Advance the nested-scope cursors to the first scope that begins on or
10370+
/// after `line_number`.
10371+
///
10372+
/// A statement list can be compiled more than once — `finally` bodies are
10373+
/// re-emitted on every path that leaves the try block early — and each copy
10374+
/// has to be handed the same scopes as the last. Positioning by line works
10375+
/// because `finally` is the last clause of its statement: every scope the
10376+
/// preceding clauses opened begins on an earlier line than the body being
10377+
/// re-compiled, and every scope that body opens begins on its own line or
10378+
/// later.
10379+
fn seek_symbol_table_cursors_to_line(&mut self, line_number: u32) {
10380+
fn seek(tables: &[SymbolTable], cursor: &mut usize, line_number: u32) {
10381+
while tables
10382+
.get(*cursor)
10383+
.is_some_and(|table| table.line_number < line_number)
10384+
{
10385+
*cursor += 1;
10386+
}
10387+
}
10388+
10389+
let table = self
10390+
.symbol_table_stack
10391+
.last_mut()
10392+
.expect("no current symbol table");
10393+
seek(&table.sub_tables, &mut table.next_sub_table, line_number);
10394+
seek(
10395+
&table.hidden_annotation_blocks,
10396+
&mut table.next_hidden_annotation_block,
10397+
line_number,
10398+
);
10399+
seek(
10400+
&table.inlined_comprehension_blocks,
10401+
&mut table.next_inlined_comprehension_block,
10402+
line_number,
10403+
);
10404+
}
10405+
1035710406
fn lookup_comprehension_symbol_table_after_skipped_nested_scopes_in_expr(
1035810407
&mut self,
1035910408
expression: &ast::Expr,

‎extra_tests/snippets/syntax_try.py‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,3 +367,104 @@ def gen():
367367

368368

369369
assert generator_return_from_try() == [["z"]]
370+
371+
372+
# the copy of the finally body is emitted where the try block is left, so its
373+
# nested scopes must be looked up past the ones the rest of the try block opens
374+
def scopes_after_the_early_exit():
375+
log = []
376+
377+
def run(data, leave_early):
378+
try:
379+
if leave_early:
380+
return "early"
381+
return list(s * 2 for s in data)
382+
finally:
383+
log.append(sorted(k for k in data))
384+
385+
assert run([1, 2], True) == "early"
386+
assert run([3, 1], False) == [6, 2]
387+
return log
388+
389+
390+
assert scopes_after_the_early_exit() == [[1, 2], [1, 3]]
391+
392+
393+
# a nested function in the try block is a scope too: taking its symbol table
394+
# for the generator expression below built one without the `.0` argument
395+
def named_scope_after_the_early_exit():
396+
log = []
397+
398+
def run(data, leave_early):
399+
try:
400+
if leave_early:
401+
return "early"
402+
403+
def inner():
404+
return [x + 1 for x in data]
405+
406+
return inner()
407+
finally:
408+
log.append(sorted(k for k in data))
409+
410+
assert run([2, 1], True) == "early"
411+
assert run([2, 1], False) == [3, 2]
412+
return log
413+
414+
415+
assert named_scope_after_the_early_exit() == [[1, 2], [1, 2]]
416+
417+
418+
# scopes that share a name resolve by position, so `inner` must not be found
419+
# where the try block declares it
420+
def same_name_scope_after_the_early_exit():
421+
log = []
422+
423+
def run(value, leave_early):
424+
try:
425+
if leave_early:
426+
return "early"
427+
428+
def inner():
429+
return value
430+
431+
return inner()
432+
finally:
433+
434+
def inner():
435+
return log
436+
437+
assert inner() is log
438+
log.append((lambda x: x + value)(1))
439+
440+
assert run(10, True) == "early"
441+
assert run(20, False) == 20
442+
return log
443+
444+
445+
assert same_name_scope_after_the_early_exit() == [11, 21]
446+
447+
448+
# breaking and continuing out of a loop copy the finally body the same way
449+
def loop_exit_with_scopes_after_it():
450+
seen = []
451+
for i in range(4):
452+
try:
453+
if i == 1:
454+
continue
455+
if i == 3:
456+
break
457+
seen.append(sorted(t for t in [i]))
458+
finally:
459+
seen.append(sorted(k for k in [i, i + 1]))
460+
return seen
461+
462+
463+
assert loop_exit_with_scopes_after_it() == [
464+
[0],
465+
[0, 1],
466+
[1, 2],
467+
[2],
468+
[2, 3],
469+
[3, 4],
470+
], loop_exit_with_scopes_after_it()

0 commit comments

Comments
 (0)