From f933dae713085bd63e5ef2be353c431a22c45a5c Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 17 Oct 2026 21:06:59 +0100 Subject: [PATCH 1/2] A finally that returns, breaks or holds a try works on the exceptional path on Windows Two shapes crashed tslang in Win32ExceptionPass with 0xC0000005 under every memory model: function f(pre: string) { let s = pre + "1"; try { h(); } finally { return s; } } function g(pre: string) { let s = pre + "2"; try { h(); } finally { try { h(); } catch { print("in " + s); } finally { print("inner " + s); } } return s; } On Windows the copy of a finally that runs while an exception unwinds is a cleanup funclet, and a cleanup funclet can do neither thing these finallys need: - It cannot return to normal flow, only carry on unwinding. The `return` branched out of the cleanup region, so the pass found no `resume` to end it, left the region's `end` null and dereferenced it. - It cannot hold pads of its own. TryOpLowering tagged the inner try's pads as top-level, so the pass closed the cleanup region on reaching them, again with no end. Nesting them inside the cleanup would not have been enough: LLVM gives a pad in a cleanup funclet no EH state under the MSVC personality, so the inner throw would not be caught there and the process would terminate. TryOpLowering now enters the finally's exceptional-path copy through a catch-all pad when that finally holds a `return`, a break/continue out of the try, or a try. A `return` (or jump) in that copy ends the catch first, as one in a catch clause does, so it becomes a catchret and the exception is swallowed. A copy that runs to its end finishes as a cleanup does (end-catch, then `resume`), and Win32ExceptionPass turns a catch region that ends in `resume` into a rethrow from inside the funclet, to the handler the region unwinds to or to the caller. The finally's pad gets an id now, and a try in that copy of the finally (only that copy: the normal-exit, return and jump copies run outside the funclet) names it as the funclet it is nested in. Win32ExceptionPass also reports a region it found no end for with report_fatal_error instead of dereferencing null. New test 00try_finally_funclet.ts: return in finally with and without a throw, a try in finally whose exception carries on out, the same inside an enclosing try that catches, a return in a finally inside a try that catches, and a break in a finally. Compile + JIT, and TSLANG_CORPUS (gc, rc, none). The try-in-finally case passes the string as a parameter rather than holding an owned local: an owned local in a function an exception leaves through a try/finally is a known rc leak (still open from #595), which the ownership-verifier sweep would report. Not changed here: a finally (or catch) that leaves only on some paths - `finally { if (c) return s; }` - still breaks, because the pass's scan assumes a region has one exit. The same holds for `catch { if (c) return s; ... }` before this change. Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/TypeScriptPassContext.h | 7 + tslang/lib/TypeScript/LowerToAffineLoops.cpp | 107 ++++++++++++-- .../Win32ExceptionPass.cpp | 43 +++++- tslang/test/tester/CMakeLists.txt | 3 + .../tester/tests/00try_finally_funclet.ts | 136 ++++++++++++++++++ 5 files changed, 284 insertions(+), 12 deletions(-) create mode 100644 tslang/test/tester/tests/00try_finally_funclet.ts diff --git a/tslang/include/TypeScript/TypeScriptPassContext.h b/tslang/include/TypeScript/TypeScriptPassContext.h index a728dd819..858e304ec 100644 --- a/tslang/include/TypeScript/TypeScriptPassContext.h +++ b/tslang/include/TypeScript/TypeScriptPassContext.h @@ -131,6 +131,8 @@ struct TSContext landingBlockOf.beginRun(); leavesCatch.beginRun(); catchPadIdOf.beginRun(); + finallyPadIdOf.beginRun(); + inParentTryFinallyPad.beginRun(); } // options @@ -161,6 +163,11 @@ struct TSContext // catch clause can name it as the funclet they are nested in - see tagPad in TryOpLowering // and ts.internal.eh_pad in Win32ExceptionPass. Ids are unique within the module. OpSideTable catchPadIdOf{"catchPadIdOf"}; + // Windows: the same for the pad a try's finally is entered through on the exceptional path, + // and whether a try sits in that copy of its parentTryOp's finally - the one copy that runs + // inside the pad's funclet, where the other copies (normal exit, return, break) do not. + OpSideTable finallyPadIdOf{"finallyPadIdOf"}; + OpSideTable inParentTryFinallyPad{"inParentTryFinallyPad"}; int nextCatchPadId = 0; mlir::Block *returnBlock; }; diff --git a/tslang/lib/TypeScript/LowerToAffineLoops.cpp b/tslang/lib/TypeScript/LowerToAffineLoops.cpp index b685d2df3..cd44fa18c 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -1347,8 +1347,13 @@ struct TryOpLowering : public TsPattern // Windows: which catch funclet this try's pads are nested in - the catch clause of the // nearest try around it that holds it in its catch, through any try bodies in between. // Win32ExceptionPass cannot tell that from the IR: a pad reached from inside a catch is - // either a try nested in it or where exceptions leave it, and the two look alike. A try - // in a finally is left at the top, as every pad was before. + // either a try nested in it or where exceptions leave it, and the two look alike. + // + // The same goes for a try in the copy of a finally that runs on the exceptional path: it + // runs inside that finally's pad, and its own pads have to be nested in it. Left at the + // top, they ended the finally's region in the pass before its end was reached, and the + // pass crashed on the region with no end. A try in a copy of a finally on a normal path + // is left at the top, as before. auto funcletParentId = 0; if (tsContext->compileOptions.isWindows) { @@ -1361,6 +1366,12 @@ struct TryOpLowering : public TsPattern break; } + if (tsContext->inParentTryFinallyPad.lookup(child)) + { + funcletParentId = tsContext->finallyPadIdOf.lookup(parent); + break; + } + if (!tsContext->inParentTryBody.lookup(child)) { break; @@ -1369,15 +1380,15 @@ struct TryOpLowering : public TsPattern } auto catchPadId = 0; - auto tagPad = [&](mlir_ts::LandingPadOp landingPadOp, bool isCatchPad) { + auto tagPad = [&](mlir_ts::LandingPadOp landingPadOp, int padId) { if (!tsContext->compileOptions.isWindows) { return; } - if (isCatchPad && catchPadId) + if (padId) { - landingPadOp->setAttr(EH_PAD_ID_ATTR_NAME, rewriter.getI32IntegerAttr(catchPadId)); + landingPadOp->setAttr(EH_PAD_ID_ATTR_NAME, rewriter.getI32IntegerAttr(padId)); } if (funcletParentId) @@ -1427,6 +1438,7 @@ struct TryOpLowering : public TsPattern tsContext->parentTryOp[op] = tryOp.getOperation(); tsContext->inParentTryBody[op] = inBody; tsContext->inParentTryCatch[op] = inCatch; + tsContext->inParentTryFinallyPad[op] = false; } }; inBody = true; @@ -1473,6 +1485,33 @@ struct TryOpLowering : public TsPattern collectEscapingJumps(tryOp.getBody(), escapingJumpsBody); collectEscapingJumps(tryOp.getCatches(), escapingJumpsCatches); + // Windows: the copy of the finally on the exceptional path runs in a cleanup funclet, + // which can do two things a finally may need only from a catch funclet: + // - return to normal flow: a `return`, or a break/continue out of the try, ends the + // exception there and then (`finally { return s; }` swallows it), where a cleanup + // can only carry on unwinding; + // - hold pads of its own: a try in the finally. LLVM numbers no EH state for a pad in a + // cleanup funclet (the MSVC personality does not allow one), so an exception thrown + // in that try was never caught there, and the process terminated. + // Such a finally is entered through a catch-all pad instead, which rethrows at its end + // (see windowsFinallyCatches). + mlir::SmallVector escapingOpsFinally; + auto finallyHasTry = false; + if (tsContext->compileOptions.isWindows) + { + tryOp.getFinally().walk([&](Operation *op) { + if (isa(op) || + (isa(op) && tsContext->jumps.contains(op))) + { + escapingOpsFinally.push_back(op); + } + else if (isa(op)) + { + finallyHasTry = true; + } + }); + } + // inline structure OpBuilder::InsertionGuard guard(rewriter); mlir::Block *currentBlock = rewriter.getInsertionBlock(); @@ -1583,6 +1622,9 @@ struct TryOpLowering : public TsPattern mlir::Block *finallyBlock = nullptr; mlir::Block *finallyBlockLast = nullptr; mlir::Block *exitBlockLast = nullptr; + auto finallyPadId = 0; + auto windowsFinallyCatches = + tsContext->compileOptions.isWindows && finallyHasOps && (!escapingOpsFinally.empty() || finallyHasTry); if (finallyHasOps) { // cloneRegionBefore makes brand-new Operation* instances, so any tsContext @@ -1614,6 +1656,27 @@ struct TryOpLowering : public TsPattern rewriter.cloneRegionBefore(tryOp.getFinally(), *continuation->getParent(), continuation->getIterator(), finallyMapping); propagateTsContextEntries(finallyMapping); + if (tsContext->compileOptions.isWindows) + { + // this copy is the exceptional path's, which runs inside the finally's pad + finallyPadId = ++tsContext->nextCatchPadId; + tsContext->finallyPadIdOf[tryOp.getOperation()] = finallyPadId; + for (auto &[oldOp, newOp] : finallyMapping.getOperationMap()) + { + if (isa(newOp)) + { + tsContext->inParentTryFinallyPad[newOp] = true; + } + } + + // and the catch-all pad such a copy is entered through has to be ended on the + // way out, as a catch clause's `return` ends it + for (auto op : escapingOpsFinally) + { + tsContext->unwind[finallyMapping.lookup(op)] = beforeFinallyBlock->getNextNode(); + } + } + finallyBlock = beforeFinallyBlock->getNextNode(); tsContext->finallyBlockOf[tryOp.getOperation()] = finallyBlock; finallyBlockLast = continuation->getPrevNode(); @@ -1770,7 +1833,7 @@ struct TryOpLowering : public TsPattern mlir::Value catchAll; if (enclosingHandler && finallyHasOps || linuxHasCleanups && rttih.hasType() || - linuxCleanupOnlyChainsToParent || linuxTypedCatchChains) + linuxCleanupOnlyChainsToParent || linuxTypedCatchChains || windowsFinallyCatches) { catchAll = (mlir::Value)rewriter.create(loc, mth.getNullType()); } @@ -1802,7 +1865,7 @@ struct TryOpLowering : public TsPattern auto landingPadCleanupOp = rewriter.create( loc, rttih.getLandingPadType(), rewriter.getBoolAttr(true), ValueRange{undefArrayValue}); - tagPad(landingPadCleanupOp, false); + tagPad(landingPadCleanupOp, 0); auto beginCleanupCallInfo = rewriter.create(loc); rewriter.setInsertionPoint(cleanupBlockLast->getTerminator()); @@ -1854,7 +1917,7 @@ struct TryOpLowering : public TsPattern auto landingPadOp = rewriter.create(loc, rttih.getLandingPadType(), rewriter.getBoolAttr(false), catchTypes); - tagPad(landingPadOp, true); + tagPad(landingPadOp, catchPadId); // On Windows a typed catch that does not match is left by the runtime, through the // catchswitch's unwind edge - but Win32ExceptionPass can only point that edge at the @@ -1936,13 +1999,35 @@ struct TryOpLowering : public TsPattern } while (false); } - if (tsContext->compileOptions.isWindows) + if (windowsFinallyCatches) + { + // see escapingOpsFinally: a catch-all, which a `return` in it ends; run to its + // end, it ends the same way a cleanup does, and Win32ExceptionPass rethrows + // there from the catch (a catch region ending in `resume`) + rewriter.setInsertionPointToStart(finallyBlock); + + auto landingPadCatchAllOp = rewriter.create( + loc, rttih.getLandingPadType(), rewriter.getBoolAttr(false), ValueRange{catchAll}); + tagPad(landingPadCatchAllOp, finallyPadId); + rewriter.create(loc, mth.getOpaqueType(), landingPadCatchAllOp); + + rewriter.setInsertionPoint(finallyBlockLast->getTerminator()); + mlir::SmallVector unwindDests; + if (enclosingHandler) + { + unwindDests.push_back(enclosingHandler); + } + + auto resultOpFinally = cast(finallyBlockLast->getTerminator()); + rewriter.replaceOpWithNewOp(resultOpFinally, landingPadCatchAllOp, unwindDests); + } + else if (tsContext->compileOptions.isWindows) { rewriter.setInsertionPointToStart(finallyBlock); - + auto landingPadCleanupOp = rewriter.create( loc, rttih.getLandingPadType(), rewriter.getBoolAttr(true), ValueRange{undefArrayValue}); - tagPad(landingPadCleanupOp, false); + tagPad(landingPadCleanupOp, finallyPadId); auto beginCleanupCallInfo = rewriter.create(loc); rewriter.setInsertionPoint(finallyBlockLast->getTerminator()); diff --git a/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp b/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp index f4e4a89a7..e9bfa1beb 100644 --- a/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp +++ b/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp @@ -353,6 +353,16 @@ struct Win32ExceptionPassCode } } + // a region the scan found no end for cannot be made a funclet; say so rather than crash + for (auto &catchRegion : catchRegionsWorkSet) + { + if (!catchRegion.end) + { + report_fatal_error(Twine("Win32ExceptionPass: no end found for the exception handling region of a landing pad in '") + + F.getName() + "'"); + } + } + chooseUnwindInfoOps(catchRegionsWorkSet); // create begin of catch block (a parent comes before the regions nested in it) @@ -538,7 +548,38 @@ struct Win32ExceptionPassCode if (catchRegion.isCatch()) { - if (!endInvoke) + if (isa(I)) + { + // A catch-all that ends the way a cleanup does: the copy of a `finally` on the + // exceptional path, which TryOpLowering enters through a catch when it holds a + // try or a `return` (a cleanup funclet can do neither). Run to its end, the + // finally hands the exception on: rethrow it from inside the funclet, to the + // handler the region unwinds to, or to the caller. + llvm::SmallVector opBundle; + opBundle.emplace_back(OperandBundleDef("funclet", catchRegion.catchPad)); + + auto throwFunc = getThrowFn(Ctx, F.getParent()); + auto nullI8Ptr = ConstantPointerNull::get(PointerType::get(Ctx, 0)); + auto nullTI = ConstantPointerNull::get(cast(throwFunc.getFunctionType()->params()[1])); + + // With a handler to go on to, the end-of-catch marker is an invoke that ends + // a block of its own, and is removed with the rest: the rethrow takes its place. + auto *rethrowAt = isa_and_nonnull(catchRegion.cxaEndCatch) ? catchRegion.cxaEndCatch : I; + auto *II = catchRegion.unwindInfoOp; + if (auto *handlerPad = II ? II->getUnwindDest() : nullptr) + { + auto *unreachableBB = BasicBlock::Create(Ctx, "rethrow.cont", &F); + new UnreachableInst(Ctx, unreachableBB); + InvokeInst::Create(throwFunc, unreachableBB, handlerPad, {nullI8Ptr, nullTI}, opBundle, "", + rethrowAt->getIterator()); + } + else + { + CallInst::Create(throwFunc, {nullI8Ptr, nullTI}, opBundle, "", rethrowAt->getIterator()); + new UnreachableInst(Ctx, rethrowAt->getIterator()); + } + } + else if (!endInvoke) { assert(catchRegion.catchPad); auto CR = CatchReturnInst::Create(catchRegion.catchPad, retBlock, BI ? BI->getParent() : I->getParent()); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 1a9513139..1e8a83103 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -651,6 +651,7 @@ tslang_add_test(NAME test-compile-00-try-catch-return COMMAND test-runner "${PRO tslang_add_test(NAME test-compile-00-try-finally COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_finally.ts") tslang_add_test(NAME test-compile-01-try-finally COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/01try_finally.ts") tslang_add_test(NAME test-compile-00-try-finally-return COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_finally_return.ts") +tslang_add_test(NAME test-compile-00-try-finally-funclet COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_finally_funclet.ts") tslang_add_test(NAME test-compile-00-try-finally-break-continue COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_finally_break_continue.ts") tslang_add_test(NAME test-compile-00-try-catch-rethrow COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_catch_rethrow.ts") tslang_add_test(NAME test-compile-00-try-catch-mismatch-rethrow COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_catch_mismatch_rethrow.ts") @@ -1193,6 +1194,7 @@ tslang_add_test(NAME test-jit-00-try-catch-return COMMAND test-runner -jit "${PR tslang_add_test(NAME test-jit-00-try-finally COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_finally.ts") tslang_add_test(NAME test-jit-01-try-finally COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/01try_finally.ts") tslang_add_test(NAME test-jit-00-try-finally-return COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_finally_return.ts") +tslang_add_test(NAME test-jit-00-try-finally-funclet COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_finally_funclet.ts") tslang_add_test(NAME test-jit-00-try-finally-break-continue COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_finally_break_continue.ts") tslang_add_test(NAME test-jit-00-try-catch-rethrow COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_catch_rethrow.ts") # pre-existing JIT limitation (reproduces on unmodified main too): the JIT's @@ -2099,6 +2101,7 @@ set(TSLANG_CORPUS 00try_catch.ts 00try_finally_break_continue.ts 00try_finally_return.ts + 00try_finally_funclet.ts 00try_in_catch_with_finally.ts 00try_finally.ts 00try_using_catch.ts diff --git a/tslang/test/tester/tests/00try_finally_funclet.ts b/tslang/test/tester/tests/00try_finally_funclet.ts new file mode 100644 index 000000000..b19dd5f7d --- /dev/null +++ b/tslang/test/tester/tests/00try_finally_funclet.ts @@ -0,0 +1,136 @@ +// A `finally` that holds a try, or that returns or breaks, on the exceptional path (Windows). +// +// There the copy of the finally that runs while an exception unwinds was a cleanup funclet, +// and a cleanup funclet can neither return to normal flow nor hold pads of its own. So +// `finally { return s; }` left the region with no end, and a try in the finally had its pads +// placed outside the funclet they sit in; Win32ExceptionPass crashed on both. Such a finally is +// now entered through a catch-all, which its `return` ends, and which rethrows when the finally +// runs to its end. + +let trail = ""; + +function thrower() { + throw 1; +} + +// the return swallows the exception +function returnInFinally(pre: string) { + let s = pre + "1"; + try { + thrower(); + } finally { + return s; + } +} + +// and with nothing thrown, it is just the return +function returnInFinallyNoThrow(pre: string) { + let s = pre + "n"; + try { + trail += "body;"; + } finally { + return s; + } +} + +// a try in the finally catches its own exception, and the first one carries on out +function tryInFinally(pre: string) { + try { + thrower(); + } finally { + try { + thrower(); + } catch { + trail += "in " + pre + ";"; + } finally { + trail += "inner " + pre + ";"; + } + } + + trail += "unreached;"; + return pre; +} + +// the same with a try around it in the function, which takes the first exception +function tryInFinallyCaughtFurtherOut(pre: string) { + let s = pre + "4"; + try { + try { + thrower(); + } finally { + try { + thrower(); + } catch { + trail += "in " + s + ";"; + } + + trail += "fin " + s + ";"; + } + } catch { + trail += "outer " + s + ";"; + } + + trail += "after " + s + ";"; +} + +// a return in a finally inside a try that catches: the catch never sees the exception +function returnInFinallyCaughtFurtherOut(pre: string): string { + let s = pre + "5"; + try { + try { + thrower(); + } finally { + return s; + } + } catch { + trail += "never;"; + } + + return "no"; +} + +// a break out of the finally swallows the exception as well +function breakInFinally(pre: string) { + let s = pre + "6"; + for (let i = 0; i < 3; i++) { + try { + thrower(); + } finally { + trail += "loop " + s + ";"; + break; + } + } + + trail += "done " + s + ";"; +} + +function main() { + assert(returnInFinally("p") == "p1", "return in finally"); + + trail = ""; + assert(returnInFinallyNoThrow("p") == "pn", "return in finally, nothing thrown"); + assert(trail == "body;", "return in finally, nothing thrown: trail"); + + trail = ""; + try { + tryInFinally("p2"); + } catch { + trail += "c;"; + } + + assert(trail == "in p2;inner p2;c;", "try in finally"); + + trail = ""; + tryInFinallyCaughtFurtherOut("p"); + assert(trail == "in p4;fin p4;outer p4;after p4;", "try in finally, caught further out"); + + trail = ""; + assert(returnInFinallyCaughtFurtherOut("p") == "p5", "return in finally, caught further out"); + assert(trail == "", "return in finally, caught further out: trail"); + + trail = ""; + breakInFinally("p"); + assert(trail == "loop p6;done p6;", "break in finally"); + + print("done."); +} From 3da5c4f3637c2e1b3707e2567edeb8aaa9cc2b53 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 17 Oct 2026 21:29:15 +0100 Subject: [PATCH 2/2] A finally that returns or breaks swallows the exception on the Itanium path too 00try_finally_funclet.ts failed on Linux with "terminate called after throwing an instance of 'int'" for `finally { return s; }` and `finally { break; }` when no try further out catches. There the finally's pad is a cleanup, and an exception with only cleanups to run is never unwound: the search phase finds no handler and the program terminates before the finally runs, so its return never gets the chance to swallow the exception. The finally of a try with no enclosing handler in the function is now entered through a catch-all landing pad when it can leave (the shape the enclosingHandler case already has): a `return` or jump in that copy ends the catch first, and a run to the end rethrows. The end-catch marking of those escaping ops now applies on both paths, as the pad is a catch-all on both. Verified on Linux (WSL): the new test under compile, JIT and the rc/none corpus, and the 261 try/catch/finally/using/throw/ownership tests; on Windows the same subset (267 tests). Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/LowerToAffineLoops.cpp | 85 +++++++++++++------- 1 file changed, 55 insertions(+), 30 deletions(-) diff --git a/tslang/lib/TypeScript/LowerToAffineLoops.cpp b/tslang/lib/TypeScript/LowerToAffineLoops.cpp index cd44fa18c..51f135e6a 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -1485,32 +1485,31 @@ struct TryOpLowering : public TsPattern collectEscapingJumps(tryOp.getBody(), escapingJumpsBody); collectEscapingJumps(tryOp.getCatches(), escapingJumpsCatches); - // Windows: the copy of the finally on the exceptional path runs in a cleanup funclet, - // which can do two things a finally may need only from a catch funclet: + // The copy of the finally on the exceptional path is entered through a cleanup pad, which + // can do two things a finally may need only from a catch: // - return to normal flow: a `return`, or a break/continue out of the try, ends the - // exception there and then (`finally { return s; }` swallows it), where a cleanup - // can only carry on unwinding; - // - hold pads of its own: a try in the finally. LLVM numbers no EH state for a pad in a - // cleanup funclet (the MSVC personality does not allow one), so an exception thrown - // in that try was never caught there, and the process terminated. + // exception there and then (`finally { return s; }` swallows it), where a cleanup can + // only carry on unwinding. On Windows the funclet had no way out; on the Itanium path + // an exception with only cleanups to run is never unwound at all - the search phase + // finds no handler and the program terminates before the finally runs. + // - hold pads of its own (Windows): a try in the finally. LLVM numbers no EH state for a + // pad in a cleanup funclet (the MSVC personality does not allow one), so an exception + // thrown in that try was never caught there, and the process terminated. // Such a finally is entered through a catch-all pad instead, which rethrows at its end - // (see windowsFinallyCatches). + // (see windowsFinallyCatches and linuxFinallyCatches). mlir::SmallVector escapingOpsFinally; auto finallyHasTry = false; - if (tsContext->compileOptions.isWindows) - { - tryOp.getFinally().walk([&](Operation *op) { - if (isa(op) || - (isa(op) && tsContext->jumps.contains(op))) - { - escapingOpsFinally.push_back(op); - } - else if (isa(op)) - { - finallyHasTry = true; - } - }); - } + tryOp.getFinally().walk([&](Operation *op) { + if (isa(op) || + (isa(op) && tsContext->jumps.contains(op))) + { + escapingOpsFinally.push_back(op); + } + else if (isa(op)) + { + finallyHasTry = true; + } + }); // inline structure OpBuilder::InsertionGuard guard(rewriter); @@ -1625,6 +1624,9 @@ struct TryOpLowering : public TsPattern auto finallyPadId = 0; auto windowsFinallyCatches = tsContext->compileOptions.isWindows && finallyHasOps && (!escapingOpsFinally.empty() || finallyHasTry); + // with a try around it in the function the Itanium finally pad is a catch-all already + auto linuxFinallyCatches = !tsContext->compileOptions.isWindows && finallyHasOps && !enclosingHandler && + !escapingOpsFinally.empty(); if (finallyHasOps) { // cloneRegionBefore makes brand-new Operation* instances, so any tsContext @@ -1668,13 +1670,13 @@ struct TryOpLowering : public TsPattern tsContext->inParentTryFinallyPad[newOp] = true; } } + } - // and the catch-all pad such a copy is entered through has to be ended on the - // way out, as a catch clause's `return` ends it - for (auto op : escapingOpsFinally) - { - tsContext->unwind[finallyMapping.lookup(op)] = beforeFinallyBlock->getNextNode(); - } + // the catch-all pad a finally that can leave is entered through (on either path) has + // to be ended on the way out, as a catch clause's `return` ends it + for (auto op : escapingOpsFinally) + { + tsContext->unwind[finallyMapping.lookup(op)] = beforeFinallyBlock->getNextNode(); } finallyBlock = beforeFinallyBlock->getNextNode(); @@ -1833,7 +1835,8 @@ struct TryOpLowering : public TsPattern mlir::Value catchAll; if (enclosingHandler && finallyHasOps || linuxHasCleanups && rttih.hasType() || - linuxCleanupOnlyChainsToParent || linuxTypedCatchChains || windowsFinallyCatches) + linuxCleanupOnlyChainsToParent || linuxTypedCatchChains || windowsFinallyCatches || + linuxFinallyCatches) { catchAll = (mlir::Value)rewriter.create(loc, mth.getNullType()); } @@ -2042,7 +2045,29 @@ struct TryOpLowering : public TsPattern } else { - if (!enclosingHandler) + if (linuxFinallyCatches) + { + // see escapingOpsFinally: a catch-all, which a `return` in it ends; run to + // its end, it rethrows - the shape the enclosingHandler case below has + rewriter.setInsertionPointToStart(linuxHasCleanupsForFinally ? cleanupBlock : finallyBlock); + + auto landingPadCatchAllOp = rewriter.create( + loc, rttih.getLandingPadType(), rewriter.getBoolAttr(false), ValueRange{catchAll}); + + if (linuxHasCleanupsForFinally) + { + rewriter.setInsertionPointToStart(finallyBlock); + } + + rewriter.create(loc, mth.getOpaqueType(), landingPadCatchAllOp); + + rewriter.setInsertionPoint(finallyBlockLast->getTerminator()); + auto nullVal = rewriter.create(loc, mth.getNullType()); + + auto resultOpOfFinally = cast(finallyBlockLast->getTerminator()); + rewriter.replaceOpWithNewOp(resultOpOfFinally, nullVal); + } + else if (!enclosingHandler) { rewriter.setInsertionPointToStart(linuxHasCleanupsForFinally ? cleanupBlock : finallyBlock);