From 71cd9bdaf80358e834da8f4d3c55291c88370eed Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Thu, 13 Aug 2026 10:07:03 +0900 Subject: [PATCH] codegen: preserve symbol tables across copied finally bodies Assisted-by: OpenAI Codex:gpt-5 --- crates/codegen/src/compile.rs | 6 +++ extra_tests/snippets/syntax_try.py | 82 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 1ca7afb8e1e..afe868dab6c 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -2406,7 +2406,13 @@ impl<'warnings> Compiler<'warnings> { } if let FBlockDatum::FinallyBody(ref body) = info.fb_datum { + // This is an extra copy of the finally body, emitted for the + // path that leaves the try block early. The try statement + // emits its own copies afterwards, so rewind the symbol table + // cursors and leave the nested scopes for those copies. + let symbol_table_cursors = self.current_symbol_table_cursors(); self.compile_statements(body)?; + self.set_symbol_table_cursors(symbol_table_cursors); } if preserve_tos { diff --git a/extra_tests/snippets/syntax_try.py b/extra_tests/snippets/syntax_try.py index 1f46caae3e7..5610cb23e6a 100644 --- a/extra_tests/snippets/syntax_try.py +++ b/extra_tests/snippets/syntax_try.py @@ -285,3 +285,85 @@ def y(): try: pass """) + + +# leaving the try block early emits an extra copy of the finally body, which +# must not consume the symbol tables of the nested scopes it contains +def return_from_try(): + log = [] + try: + return "returned" + finally: + log.append((lambda x: x * 2)(3)) + log.append({t for t in [1, 2]}) + log.append([t for t in [3]]) + log.append({k: k for k in [4]}) + + def nested(): + return 5 + + class Nested: + value = 6 + + assert log == [6, {1, 2}, [3], {4: 4}], log + assert nested() == 5 + assert Nested.value == 6 + + +assert return_from_try() == "returned" + + +def break_and_continue_from_try(): + seen = [] + for i in range(4): + try: + if i == 1: + continue + if i == 3: + break + seen.append(i) + finally: + seen.append({t for t in [i]}) + return seen + + +assert break_and_continue_from_try() == [0, {0}, {1}, 2, {2}, {3}] + + +def return_from_try_runs_finally_once(): + log = [] + + def inner(): + try: + return "value" + finally: + log.append(sorted({t for t in "ab"})) + + assert inner() == "value" + return log + + +assert return_from_try_runs_finally_once() == [["a", "b"]] + + +def generator_return_from_try(): + log = [] + + def gen(): + try: + return (yield "yielded") + finally: + log.append([t for t in "z"]) + + g = gen() + assert g.send(None) == "yielded" + try: + g.send("sent") + except StopIteration as stop: + assert stop.value == "sent", stop.value + else: + assert False, "generator did not stop" + return log + + +assert generator_return_from_try() == [["z"]]