Skip to content

Combine try, catch and finally blocks soundly - #366

Merged
CatarinaGamboa merged 2 commits into
mainfrom
fix/364-try-catch-join
Oct 8, 2026
Merged

CatarinaGamboa merged 2 commits into
mainfrom
fix/364-try-catch-join

Conversation

@CatarinaGamboa

@CatarinaGamboa CatarinaGamboa commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #339 and #364.

try/catch was checked as straight-line code: first the try, then the catch. An exception can leave the try after any statement, and a finally also runs after returns and uncaught exceptions. Because of that, errors in all three places were missed.

Example

int y = 1;
try {
    y = -1;
    load();               // may throw
    y = 2;
} catch (Exception e) {
    // (1) start of catch
    y = 3;
} finally {
    // (3) start of finally
}
// (2) after the try statement
Point main assumes This PR assumes
(1) start of catch y == 2 (the whole try ran) y == 1 ∨ y == -1 ∨ y == 2
(2) after the try statement y == 3 (the catch always ran) c ? y == 2 : y == 3
(3) start of finally y == 3 any of the above

Walkthrough

  1. Start of the catch: while checking the try, every new value of y is recorded (-1, 2). The catch starts from "the value before the try or any recorded value". So @Refinement("_ > 0") int a = y; placed at (1) is now an error; on main it passed.
  2. After the try statement: the try and the catch are joined like an if/else on an unknown condition c, reusing the if machinery. So @Refinement("_ == 3") int b = y; placed at (2) is now an error.
  3. Start of the finally: it starts from any value seen in the try or the catch. After it, checking continues from the joined state (2), plus whatever the finally assigns.

Path conditions from inside a branch (e.g. if (x <= 0) return; in the try) are also dropped before the next branch, the finally and the code after, since the exception may come before the check.

Changes

  • New TryChecker: holds the try/catch/finally logic. The existing try-with-resources code moved there from RefinementTypeChecker, which is now smaller than on main.
  • Context: startRecordingInstances / stopRecordingInstances record the new values variables get inside a block.
  • Tests: try_catch_error and try_catch_correct, 13 cases each.

Limitations

  • A finally that updates a variable from itself (y = y + 1) starts from every possible value. This is sound but imprecise.
  • Fields are not tracked, the same as for ifs.
  • Catch and finally entries conservatively include the pre-try state even when only later operations can throw. This can reject valid code such as x = 1; risky(); followed by a catch requiring x == 1. Exception-point-aware snapshots would improve precision.

🤖 Generated with Claude Code

@CatarinaGamboa
CatarinaGamboa force-pushed the fix/364-try-catch-join branch from 540db82 to 4340fb3 Compare October 7, 2026 14:37
@CatarinaGamboa CatarinaGamboa changed the title Combine try and catch blocks like if branches Combine try, catch and finally blocks soundly Oct 7, 2026
try/catch blocks were checked one after the other, so after them only the
last catch path was considered and errors on the try path were missed.

- The try and catch blocks are visited like an if-else-if chain with
  unknown conditions, and each variable is combined after the join with
  the existing if machinery.
- An exception may leave the try block after any of its statements, so a
  catch block starts with each variable in any of the states it had before
  or during the try block (instances are recorded while visiting it).
- The finally block starts with any of the states of the try and catch
  blocks; after it, the normal completion state continues, updated with
  what the finally block changed.
- Path conditions from inside a branch do not leak into later branches,
  the finally block, or the code after the try statement.
- Catch parameters get an instance so refinements referring to them
  outlive the catch block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CatarinaGamboa
CatarinaGamboa force-pushed the fix/364-try-catch-join branch from 4340fb3 to 6559819 Compare October 8, 2026 11:25
The Apache Derby shape behind DERBY-2472: exceptions caught in a loop are
chained with initCause, but their constructor may already have set the cause,
so the call can throw "Can't overwrite cause". The catch parameter's state is
unknown, so the call is reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CatarinaGamboa
CatarinaGamboa merged commit dcfc4eb into main Oct 8, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Soundness: a catch block is checked as if the whole try block had completed

1 participant