Skip to content

A break, continue or return out of a catch clause ends the catch on every path out of it - #601

Merged
ASDAlexander77 merged 1 commit into
mainfrom
fix-jump-from-catch
Oct 17, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
fix-jump-from-catch

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

A break or continue out of a catch clause crashed with 0x80000003 on Windows (JIT and AOT, gc/rc/none, with or without a finally), and so did a return out of a catch that could also fall off its end:

for (let i = 0; i < 3; i++) {
    try { h(); }
    catch { if (i == 1) { break; } }
    trail += "a" + i + ";";
}

There were two defects.

MLIR (TryOpLowering). A break/continue out of a catch was marked as leaving the catch only when the try had a finally. Without one, the jump ended no catch. A jump out of a catch nested in another catch ended only one of the two. TSContext::catchesLeft now counts how many catch clauses a return, break or continue leaves, and the lowering emits one end-of-catch for each. The return/jump in #599's catch-all finally copy uses the same counter.

Win32ExceptionPass. Each catch region got exactly one end. The scan follows block layout and closed the region at the first __cxa_end_catch. Any other way out was left as a bare branch out of the catchpad, and the calls on that path had no funclet bundle, so WinEHPrepare turned them into unreachable code. The new findOtherCatchExits walks each catch clause by control flow from its pad. It walks nested catches first and continues after their ends. It collects every end marker and the calls on the way to each one, and each extra end gets its own catchret. The same walk fixes the shape #599 left open: a finally that returns or breaks on only some paths (finally { if (c) return s; ... }). For that shape, the rethrowing run-to-end exit is now always the region's main end. An invoked end marker is also recorded among the region's invokes, so the rethrow goes to the enclosing handler.

New test 00try_jump_from_catch.ts (compile, JIT, TSLANG_CORPUS) covers:

  • break, continue and return out of a catch, with and without a finally
  • a finally that reads a local of the block around the try
  • a typed catch
  • a catch nested in another catch
  • a loop inside a catch
  • a labelled continue
  • a finally that returns or breaks on some paths only, with and without an enclosing try

Verified on Windows:

  • the new test passes under gc/rc/none, with and without --opt
  • --emit=mlir-affine --verify-ownership -mm=rc reports nothing on it
  • ctest -C Release -j 8 --timeout 300: 4168/4168 passed

The Itanium path also emits the extra __cxa_end_catch calls, but I have not run it locally; the Linux CI job is its check.

🤖 Generated with Claude Code

…very path out of it

A `break` or `continue` out of a catch clause crashed with 0x80000003 on Windows (JIT and AOT,
every memory model, with or without a finally), and so did a `return` out of one whenever the
clause could also fall off its end. Two defects:

- TryOpLowering only marked a break/continue out of a catch as leaving the catch when the try had
  a finally, so without one the jump ended no catch at all. A jump out of a catch nested in another
  catch ended only one of the two. TSContext::catchesLeft now counts the catch clauses a return,
  break or continue leaves, and their lowering ends that many. The return or jump in the copy of a
  finally entered through a catch-all pad (#599) counts that pad as one more.

- Win32ExceptionPass gave each catch region a single end: its scan follows the layout and closed
  the region at the first end-of-catch marker. Every other way out of the clause was left as a
  bare branch out of the catchpad, with no funclet bundle on its calls, which WinEHPrepare turns
  into unreachable code. findOtherCatchExits now walks each catch clause by its control flow from
  the pad (a nested catch first, continuing after its ends), collects every end-of-catch marker
  and the calls on the way to them, and each extra end gets its own catchret.

  The same goes for the copy of a finally entered through a catch-all pad (#599) that leaves on
  some paths only (`finally { if (c) return s; ... }`): its run-to-the-end exit, which rethrows,
  is now always the region's main end and its `return` the other, where the layout could put the
  `return` first and give the rethrow path a catchret into a `resume` (SimplifyCFG crashed on it).
  An invoked end-of-catch marker is also recorded among the region's invokes, so that rethrow goes
  to the enclosing handler rather than to the caller.

00try_jump_from_catch.ts covers break, continue and return out of a catch with and without a
finally, a finally reading a local of the block around the try, a typed catch, a catch nested in
another, a loop inside a catch, a labelled continue, and a finally that returns or breaks on some
paths only, with and without a try around it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit d89ae91 into main Oct 17, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-jump-from-catch branch October 17, 2026 21:32
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.

1 participant