Skip to content

Run enclosing finally when break/continue jumps out of a try block#234

Merged
ASDAlexander77 merged 1 commit into
mainfrom
fix/break-continue-in-try-finally
Jul 14, 2026
Merged

Run enclosing finally when break/continue jumps out of a try block#234
ASDAlexander77 merged 1 commit into
mainfrom
fix/break-continue-in-try-finally

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Summary

Fixes the follow-up tracked as item 5b in docs/LowerToAffineLoops-LowerToLLVM-review.md (found while testing #233): break/continue inside a try body or catch clause jumped straight to the loop's continuation/increment block, silently skipping the enclosing finally.

Fixed the same way return in try/finally already worked, per the repo owner's suggestion:

  • Escape detection: TryOpLowering collects BreakOp/ContinueOp ops in the body/catches that already have a tsContext->jumps entry at try-lowering time. That's a clean discriminator for "jumps out of this try" — the target loop/label encloses the try, so its lowering (which populates jumps) ran first in the pre-order worklist, while jumps targeting a loop inside the try aren't resolved yet.
  • Routing: for each distinct escape target, the finally region is cloned once (mirroring the existing return-cleanup clone), the clone is terminated with a branch to the original target, and jumps[op] is re-pointed at the clone. BreakOpLowering/ContinueOpLowering need no structural change — they just branch into the finally copy, which continues to the real target.
  • Nested try/finally chains compose naturally: the outer try lowers first and re-points the op at its finally copy; a nested try later re-points the same op at its copy, which branches to the outer copy — finallys run innermost-first.
  • Catch clauses: escaping jumps inside a catch additionally set unwind[op], and the break/continue lowerings now emit EndCatchOp when it's set, mirroring return-from-catch.

Test plan

  • ctest --test-dir __build/tslang/windows-msbuild-2026-debug -C Debug -j8 — 690/690 passed (JIT + AOT)
  • 00try_finally_break_continue.ts extended with: break in try body, continue in try body, two-level nested finally chain (both finallys run), break from a catch clause (catch + finally + EndCatch), one try with both a break and a continue escaping (two distinct targets → two separate finally copies), and a negative case (break of a loop fully inside the try is not rerouted — finally runs exactly once)
  • Each scenario verified by JIT execution with value-checked asserts before being formalized into the test file

@ASDAlexander77
ASDAlexander77 merged commit 1ffdaa4 into main Jul 14, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix/break-continue-in-try-finally branch July 14, 2026 23:02
ASDAlexander77 added a commit that referenced this pull request Jul 15, 2026
A follow-up static audit of LowerToAffineLoops.cpp, LowerToLLVM.cpp, and
the TypeScriptExceptionPass files (beyond the 10 findings already closed
in PRs #230/#232/#233/#234/#235) found and fixes 7 more confirmed bugs:

- SwitchStateOpLowering: generator yield/resume state labels were
  collected into a SmallPtrSet<Operation*,16>, whose iteration order is
  only insertion order below 16 entries; past that it silently falls back
  to pointer-hash order, scrambling the positional switch dispatch used
  by generator resume. Replaced with an order-preserving SmallVector.
- StringConcatOpLowering: the stack-allocation path sized its Alloca
  using a pointer type instead of i8, over-allocating by sizeof(ptr) for
  every multi-argument string concatenation (e.g. console.log with 2+
  args).
- ArraySpliceOpLowering: mixed genuine MLIR index-dialect ops with
  already-LLVM-converted operands, a type mismatch that made every
  Array.prototype.splice() call fail to lower correctly (the only
  .splice() call in the test suite was commented out because of this).
  Reworked to stay consistently in the LLVM-converted domain, matching
  every sibling array-mutation lowering.
- GlobalOpLowering: the side-effect enumeration deciding whether a global
  initializer can be inlined as a constant vs. must run as a real
  constructor was missing several ops with real side effects (array
  push/unshift/splice/pop/shift, delete, set-length, string concat/
  char-to-string).
- LandingPadFixPass: MadeChange was set unconditionally for every
  landingpad visited, even when nothing was rewritten, causing needless
  analysis invalidation on every EH-containing function on every compile.
- Win32ExceptionPass: the PHI-user removal loop erased only the first PHI
  user found then unconditionally erased the underlying value, violating
  LLVM's no-remaining-uses invariant when more than one PHI referenced the
  same to-be-removed value (own TODO comment flagged this as incomplete).
- Win32ExceptionPass: getThrowFn's fallback path created a stray
  _CxxThrowException Function without attaching it to the module.

Added 00array_splice.ts (JIT + AOT) covering shrink/grow/equal-size
splice cases, since this path had zero prior test coverage. Full ctest
suite green: 692/692.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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