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..51f135e6a 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,32 @@ struct TryOpLowering : public TsPattern collectEscapingJumps(tryOp.getBody(), escapingJumpsBody); collectEscapingJumps(tryOp.getCatches(), escapingJumpsCatches); + // 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. 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 and linuxFinallyCatches). + mlir::SmallVector escapingOpsFinally; + auto finallyHasTry = false; + 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 +1621,12 @@ 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); + // 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 @@ -1614,6 +1658,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; + } + } + } + + // 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(); tsContext->finallyBlockOf[tryOp.getOperation()] = finallyBlock; finallyBlockLast = continuation->getPrevNode(); @@ -1770,7 +1835,8 @@ struct TryOpLowering : public TsPattern mlir::Value catchAll; if (enclosingHandler && finallyHasOps || linuxHasCleanups && rttih.hasType() || - linuxCleanupOnlyChainsToParent || linuxTypedCatchChains) + linuxCleanupOnlyChainsToParent || linuxTypedCatchChains || windowsFinallyCatches || + linuxFinallyCatches) { catchAll = (mlir::Value)rewriter.create(loc, mth.getNullType()); } @@ -1802,7 +1868,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 +1920,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 +2002,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()); @@ -1957,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); 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."); +}