Skip to content

Nest a try's pads in the catch funclet around it (Windows) - #360

Merged
ASDAlexander77 merged 1 commit into
mainfrom
fix-win-using-in-catch
Sep 24, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
fix-win-using-in-catch

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Summary

A using inside a catch clause did not work on Windows:

try { throw 1; } catch (o) {
    try { using r = new Res(); ... } catch (e: number) { ... }
}
shape Windows on main
no try around the catch Win32ExceptionPass crashed the compiler (the JIT at every level)
a try around the catch ahead-of-time compilation failed LLVM's verifier ("Unwind edges out of a catch must have the same unwind dest as the parent catchswitch")

Win32ExceptionPass scanned a function's instructions in order with one current region, and made every pad within none. A try inside a catch got by because a throw in it ended the enclosing catch first, so the nested try ran outside the catch's funclet. A using breaks that, because its first call is inside the catch, before any throw. The scan then took the using's cleanup pad for the end of the catch. That left the catch with no end at all (the crash), and gave the cleanup pad one edge from inside the catch funclet and one from outside it (the verifier error).

Pads now nest:

  • Tagging (TryOpLowering, LandingPadOpLowering). TryOpLowering knows which catch clause a try sits in. On Windows it tags each landing pad with its catch pad's id and the id of the catch it is nested in: the nearest try around it that holds it in its catch clause, looking through any try bodies in between. LandingPadOpLowering emits the tag as a call to ts.internal.eh_pad right after the landingpad.
  • Nesting (Win32ExceptionPass). The pass reads and removes the tag, and scans with a stack of open regions. A nested region's code sits in the middle of the enclosing catch's code, and the enclosing catch carries on when the nested one ends. An end marker right after a nested catch ends the enclosing one. Nested pads are created within the enclosing catchpad. A catchswitch unwinds to the first invoke that leaves its region, never into its own descendants.

The pass could not work out the nesting from the IR alone, so the tag is required. A pad reached from a catch is either nested in it or is that catch's exit, and the IR looks the same in both cases. The old "a throw in a nested try ends the catch first" shortcut is now Linux-only.

This is the Windows gap listed as not fixed in #359.

Verification

  • 00try_mismatch_in_catch_clause_using.ts now runs on every platform (compile, jit, and the rc/none corpus). Before this PR it was Linux-only. The no-try-around case that crashed the compiler is added to it.

🤖 Generated with Claude Code

A `using` inside a catch clause did not work on Windows:

    try { throw 1; } catch (o) {
        try { using r = new Res(); ... } catch (e: number) { ... }
    }

With no try around the catch, Win32ExceptionPass crashed the compiler
(the JIT at every level). With one, ahead-of-time compilation failed
LLVM's verifier ("Unwind edges out of a catch must have the same unwind
dest as the parent catchswitch").

Win32ExceptionPass scans a function's instructions in order with one
current region, and makes every pad `within none`. A try inside a catch
got by because a throw in it ended the enclosing catch first, so the
nested try ran outside the catch's funclet. A `using` defeats that: its
first call is inside the catch, before any throw. The scan then took the
using's cleanup pad for the end of the catch, left the catch with no
end at all (the crash), and gave the cleanup pad an edge from inside the
catch funclet and one from outside it (the verifier error).

Pads now nest:

- TryOpLowering knows which catch clause a try sits in. On Windows it
  tags each landing pad with its catch pad's id and the id of the catch
  it is nested in (the nearest try around it that holds it in its catch
  clause, through any try bodies between). LandingPadOpLowering emits the
  tag as a call to ts.internal.eh_pad right after the landingpad.
- Win32ExceptionPass reads and removes the tag, and scans with a stack
  of open regions: a nested region's code sits in the middle of the
  enclosing catch's, which carries on when the nested one ends; an end
  marker right after a nested catch ends the enclosing one. Nested pads
  are created within the enclosing catchpad, and a catchswitch unwinds
  to the first invoke that leaves its region, never into its own
  descendants.
- A throw inside a try nested in a catch no longer ends that catch on
  Windows: the nested try's pad takes it, inside the funclet. The
  Itanium path, which has no funclets, keeps ending it as before.

00try_mismatch_in_catch_clause_using.ts now runs on every platform
(compile, jit, and the rc/none corpus), with the no-try-around case
that crashed the compiler added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 5671e4c into main Sep 24, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-win-using-in-catch branch September 24, 2026 17:00
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