diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index baf19af8f..50c89b2c4 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -61,6 +61,10 @@ #define RETURN_VARIABLE_NAME ".return" #define CAPTURED_NAME ".captured" #define LABEL_ATTR_NAME "label" +// Windows: a landing pad's catch-pad id, and the id of the catch it is nested in - see tagPad in +// TryOpLowering and ts.internal.eh_pad in Win32ExceptionPass +#define EH_PAD_ID_ATTR_NAME "__eh_pad_id" +#define EH_PAD_PARENT_ATTR_NAME "__eh_pad_parent" #define UNDEFINED_NAME "undefined" #define INFINITY_NAME "Infinity" #define NAN_NAME "NaN" diff --git a/tslang/include/TypeScript/TypeScriptPassContext.h b/tslang/include/TypeScript/TypeScriptPassContext.h index e97d51f21..a728dd819 100644 --- a/tslang/include/TypeScript/TypeScriptPassContext.h +++ b/tslang/include/TypeScript/TypeScriptPassContext.h @@ -130,6 +130,7 @@ struct TSContext finallyBlockOf.beginRun(); landingBlockOf.beginRun(); leavesCatch.beginRun(); + catchPadIdOf.beginRun(); } // options @@ -156,6 +157,11 @@ struct TSContext // a throw cannot: `unwind` already means its invoke destination, which is a different // question with a different answer. OpSideTable leavesCatch{"leavesCatch"}; + // Windows: the id a try's catch landing pad goes by, so that the pads of a try inside that + // 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"}; + int nextCatchPadId = 0; mlir::Block *returnBlock; }; diff --git a/tslang/lib/TypeScript/LowerToAffineLoops.cpp b/tslang/lib/TypeScript/LowerToAffineLoops.cpp index 6430fe0e7..a2aa3b5e8 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -1329,6 +1329,48 @@ struct TryOpLowering : public TsPattern MLIRRTTIHelperVC rttih(rewriter, module, tsContext->compileOptions); auto i8PtrTy = mth.getOpaqueType(); + // 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. + auto funcletParentId = 0; + if (tsContext->compileOptions.isWindows) + { + for (Operation *child = tryOp.getOperation(), *parent = parentTryOp; parent; + child = parent, parent = tsContext->parentTryOp.lookup(parent)) + { + if (tsContext->inParentTryCatch.lookup(child)) + { + funcletParentId = tsContext->catchPadIdOf.lookup(parent); + break; + } + + if (!tsContext->inParentTryBody.lookup(child)) + { + break; + } + } + } + + auto catchPadId = 0; + auto tagPad = [&](mlir_ts::LandingPadOp landingPadOp, bool isCatchPad) { + if (!tsContext->compileOptions.isWindows) + { + return; + } + + if (isCatchPad && catchPadId) + { + landingPadOp->setAttr(EH_PAD_ID_ATTR_NAME, rewriter.getI32IntegerAttr(catchPadId)); + } + + if (funcletParentId) + { + landingPadOp->setAttr(EH_PAD_PARENT_ATTR_NAME, rewriter.getI32IntegerAttr(funcletParentId)); + } + }; + // find catch var // // This region walk must stop at a nested `try`: a `try/catch` written inside a catch @@ -1487,13 +1529,18 @@ struct TryOpLowering : public TsPattern // is what ends the catch - ending it here as well runs it twice and // breaks the unwind (51exceptions.ts is the case that proves it). // - // Except for a throw inside a try nested in this catch: that one unwinds to - // the nested try's own pad, not to the finally. Left inside this catch's - // funclet, the nested pad would be a catchswitch `within none` reached from - // inside a catchpad - invalid funclet nesting, and a crash at run time. So - // it ends the catch first, exactly as it does when there is no finally. + // A throw inside a try nested in this catch does not leave the catch: the + // nested try's pad takes it, and on Windows Win32ExceptionPass nests that + // pad in this catch's funclet. It used to end the catch first, when the pass + // could not nest pads - which, with a `using` in the nested try, ended the + // catch between the using's first call and its throw, both unwinding to the + // same cleanup pad: one edge inside the funclet and one outside it, which + // the verifier rejects. The Itanium path has no funclets and keeps ending + // the catch there, as it always did. auto inNestedTry = op->getParentOfType() != nullptr; - if (!finallyHasOps || inNestedTry) + auto leaves = tsContext->compileOptions.isWindows ? !finallyHasOps && !inNestedTry + : !finallyHasOps || inNestedTry; + if (leaves) { tsContext->leavesCatch[op] = true; } @@ -1740,6 +1787,7 @@ struct TryOpLowering : public TsPattern auto landingPadCleanupOp = rewriter.create( loc, rttih.getLandingPadType(), rewriter.getBoolAttr(true), ValueRange{undefArrayValue}); + tagPad(landingPadCleanupOp, false); auto beginCleanupCallInfo = rewriter.create(loc); rewriter.setInsertionPoint(cleanupBlockLast->getTerminator()); @@ -1767,6 +1815,12 @@ struct TryOpLowering : public TsPattern rewriter.replaceOpWithNewOp(resultOpCleanup, landingPadCleanupOp, unwindDests); } + if (catchHasOps && tsContext->compileOptions.isWindows) + { + catchPadId = ++tsContext->nextCatchPadId; + tsContext->catchPadIdOf[tryOp.getOperation()] = catchPadId; + } + mlir::Value cmpValue; SmallVector cmpWidenedValues; if (catchHasOps) @@ -1785,6 +1839,7 @@ struct TryOpLowering : public TsPattern auto landingPadOp = rewriter.create(loc, rttih.getLandingPadType(), rewriter.getBoolAttr(false), catchTypes); + tagPad(landingPadOp, true); // 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 @@ -1872,6 +1927,7 @@ struct TryOpLowering : public TsPattern auto landingPadCleanupOp = rewriter.create( loc, rttih.getLandingPadType(), rewriter.getBoolAttr(true), ValueRange{undefArrayValue}); + tagPad(landingPadCleanupOp, false); auto beginCleanupCallInfo = rewriter.create(loc); rewriter.setInsertionPoint(finallyBlockLast->getTerminator()); diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 6a84bfe16..3b8ed0767 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -4736,11 +4736,37 @@ struct LandingPadOpLowering : public TsLlvmPattern ConversionPatternRewriter &rewriter) const final { Location loc = landingPadOp.getLoc(); + + // the funclet nesting TryOpLowering worked out, for Win32ExceptionPass: a call right + // after the pad, ts.internal.eh_pad(this catch pad's id, id of the catch it is nested + // in), 0 for none. The pass reads it and removes it. + auto padId = landingPadOp->getAttrOfType(EH_PAD_ID_ATTR_NAME); + auto padParent = landingPadOp->getAttrOfType(EH_PAD_PARENT_ATTR_NAME); + auto tagPad = [&](LLVM::LandingpadOp newLandingPadOp) { + if (!padId && !padParent) + { + return; + } + + TypeHelper th(rewriter); + LLVMCodeHelper ch(landingPadOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + CodeLogicHelper clh(landingPadOp, rewriter); + + auto ehPadFunc = ch.getOrInsertFunction( + "ts.internal.eh_pad", th.getFunctionType(th.getVoidType(), {th.getI32Type(), th.getI32Type()})); + + mlir::OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPointAfter(newLandingPadOp); + rewriter.create(loc, ehPadFunc, + ValueRange{clh.createI32ConstantOf(padId ? padId.getInt() : 0), + clh.createI32ConstantOf(padParent ? padParent.getInt() : 0)}); + }; + if (!landingPadOp.getCleanup()) { auto catch1 = transformed.getCatches().front(); mlir::Type llvmLandingPadTy = getTypeConverter()->convertType(landingPadOp.getType()); - rewriter.replaceOpWithNewOp(landingPadOp, llvmLandingPadTy, false, ValueRange{catch1}); + tagPad(rewriter.replaceOpWithNewOp(landingPadOp, llvmLandingPadTy, false, ValueRange{catch1})); } else { @@ -4754,8 +4780,8 @@ struct LandingPadOpLowering : public TsLlvmPattern auto cleanupMarker = transformed.getCatches().front(); mlir::Type llvmLandingPadTy = getTypeConverter()->convertType(landingPadOp.getType()); - rewriter.replaceOpWithNewOp(landingPadOp, llvmLandingPadTy, true, - ValueRange{cleanupMarker}); + tagPad(rewriter.replaceOpWithNewOp(landingPadOp, llvmLandingPadTy, true, + ValueRange{cleanupMarker})); } return success(); diff --git a/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp b/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp index 533ade721..4bbc923ac 100644 --- a/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp +++ b/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp @@ -12,6 +12,9 @@ using namespace PatternMatch; #define DEBUG_TYPE "pass" +// see readPadTag +constexpr const char *EH_PAD_FUNC_NAME = "ts.internal.eh_pad"; + //#define SAVE_STACK true struct CatchRegion @@ -29,6 +32,15 @@ struct CatchRegion llvm::Instruction *saveCatch; llvm::Instruction *cxaEndCatch; llvm::Instruction *end; + // every invoke in the region, in order - unwindInfoOp is chosen from these once all the + // regions are known (see chooseUnwindInfoOps) + llvm::SmallVector invokes; + // index of the region this one is nested in, or -1; and the ids ts.internal.eh_pad gave + int parent = -1; + int padId = 0; + // the block the landing pad began, which is what invokes unwind to - making the catchpad + // splits the pad off into a block of its own + BasicBlock *padBlock = nullptr; bool isCatch() { @@ -106,14 +118,47 @@ struct Win32ExceptionPassCode auto endOfCatch = false; auto endOfCatchIfResume = false; llvm::SmallVector toRemoveWorkSet; + + // The regions still open, innermost last, as indexes into catchRegionsWorkSet (which + // reallocates). A pad can be nested in a catch - a try, or a `using`, inside a catch + // clause - and TryOpLowering says which catch in the tag after the pad (readPadTag). Its + // code sits in the middle of the enclosing catch's, whose end only comes after it: + // taking the new pad as the end of the old region, as this scan used to, left the catch + // with no end at all (the pass then crashed on it), and making every pad `within none` + // put an edge out of the catch funclet that the verifier rejects. + llvm::SmallVector openRegions; + auto closeRegion = [&]() { + if (!openRegions.empty()) + { + openRegions.pop_back(); + } + + catchRegion = openRegions.empty() ? nullptr : &catchRegionsWorkSet[openRegions.back()]; + }; + for (auto &I : instructions(F)) { if (auto *LPI = dyn_cast(&I)) { + auto [padId, parentPadId] = readPadTag(LPI, toRemoveWorkSet); + auto parent = findRegionByPadId(catchRegionsWorkSet, parentPadId); + catchRegionsWorkSet.push_back(CatchRegion()); catchRegion = &catchRegionsWorkSet.back(); catchRegion->landingPad = LPI; + catchRegion->parent = parent; + catchRegion->padId = padId; + catchRegion->padBlock = LPI->getParent(); + + // leave open what encloses the new pad, and nothing that was nested deeper; a pad + // not nested in an open region starts afresh, as every pad always did + while (!openRegions.empty() && openRegions.back() != parent) + { + openRegions.pop_back(); + } + + openRegions.push_back(catchRegionsWorkSet.size() - 1); endOfCatch = false; continue; @@ -158,23 +203,33 @@ struct Win32ExceptionPassCode { toRemoveWorkSet.push_back(&I); MadeChange = true; + + // Unless this region is nested in one still open: then the marker is the + // enclosing catch's own - a try inside a catch clause ends both, inner one + // first. The inner catch returns here, into the enclosing funclet, and the + // enclosing catch ends at what follows. + if (openRegions.size() > 1) + { + catchRegion->end = &I; + closeRegion(); + catchRegion->cxaEndCatch = &I; + } + continue; } // BR, or instraction without BR catchRegion->end = &I; endOfCatch = false; - catchRegion = nullptr; + + // the region it was nested in carries on + closeRegion(); continue; } - if (catchRegion->unwindInfoOp == nullptr) + if (auto *II = dyn_cast(&I)) { - if (auto *II = dyn_cast(&I)) - { - LLVM_DEBUG(llvm::dbgs() << "\n!! set (unwindInfoOp) : " << *II << "\n";); - catchRegion->unwindInfoOp = II; - } + catchRegion->invokes.push_back(II); } if (dyn_cast(&I)) @@ -200,6 +255,12 @@ struct Win32ExceptionPassCode continue; } + if (CI->getCalledFunction()->getName() == EH_PAD_FUNC_NAME) + { + // read at the pad by readPadTag, and removed there + continue; + } + if (CI->getCalledFunction()->getName() == "ts.internal.save_catch_var") { catchRegion->saveCatch = &I; @@ -292,10 +353,13 @@ struct Win32ExceptionPassCode } } - // create begin of catch block + chooseUnwindInfoOps(catchRegionsWorkSet); + + // create begin of catch block (a parent comes before the regions nested in it) for (auto &catchRegion : catchRegionsWorkSet) { auto *LPI = catchRegion.landingPad; + auto *parentPad = parentPadOf(catchRegionsWorkSet, catchRegion); LLVM_DEBUG(llvm::dbgs() << "\n!! Processing: " << *LPI << " isKnownSentinel: " << (LPI->isKnownSentinel() ? "true" : "false") << "\n";); @@ -313,7 +377,7 @@ struct Win32ExceptionPassCode CurrentBB->getTerminator()->eraseFromParent(); auto *II = catchRegion.unwindInfoOp; - auto *CSI = CatchSwitchInst::Create(ConstantTokenNone::get(Ctx), + auto *CSI = CatchSwitchInst::Create(parentPad, II ? II->getUnwindDest() : nullptr /*unwind to caller if null*/, 1, "catch.switch", CurrentBB); @@ -342,7 +406,7 @@ struct Win32ExceptionPassCode { assert(catchRegion.isCleanup()); - catchRegion.cleanupPad = CleanupPadInst::Create(ConstantTokenNone::get(Ctx), {}, "cleanuppad", LPI->getIterator()); + catchRegion.cleanupPad = CleanupPadInst::Create(parentPad, {}, "cleanuppad", LPI->getIterator()); } auto opBundle = getCallBundleFromCatchRegion(catchRegion); @@ -534,7 +598,8 @@ struct Win32ExceptionPassCode // add rethrow code auto *II = catchRegion.unwindInfoOp; - auto *CSI = CatchSwitchInst::Create(ConstantTokenNone::get(Ctx), + // beside the cleanup pad, in the same parent: cleanupret unwinds to it + auto *CSI = CatchSwitchInst::Create(parentPadOf(catchRegionsWorkSet, catchRegion), II ? II->getUnwindDest() : nullptr /*unwind to caller if null*/, 1, "catchswitch", CSIBlock); @@ -603,7 +668,9 @@ struct Win32ExceptionPassCode { if (auto *II = cast(callBase)) { - if (II->getUnwindDest() != CSIBlock) + // an invoke into a pad nested in this cleanup stays as it is + if (II->getUnwindDest() != CSIBlock && + !isDescendantPad(catchRegionsWorkSet, catchRegion, II->getUnwindDest())) { LLVM_DEBUG(llvm::dbgs() << "\n!! FIX INVOKE(cleanup): " << *callBase << "\n"); @@ -654,6 +721,102 @@ struct Win32ExceptionPassCode return MadeChange; } + // The tag TryOpLowering puts right after a pad on Windows (see LandingPadOpLowering): + // ts.internal.eh_pad(this catch pad's id, id of the catch pad it is nested in). The call is + // queued for removal. {0, 0} for an untagged pad - one at the top, as every pad used to be. + static std::pair readPadTag(LandingPadInst *LPI, llvm::SmallVector &toRemove) + { + auto *CI = dyn_cast_or_null(LPI->getNextNode()); + if (!CI || !CI->getCalledFunction() || CI->getCalledFunction()->getName() != EH_PAD_FUNC_NAME) + { + return {0, 0}; + } + + toRemove.push_back(CI); + auto id = cast(CI->getArgOperand(0))->getSExtValue(); + auto parent = cast(CI->getArgOperand(1))->getSExtValue(); + return {static_cast(id), static_cast(parent)}; + } + + static int findRegionByPadId(llvm::SmallVector ®ions, int padId) + { + if (padId == 0) + { + return -1; + } + + for (auto index = static_cast(regions.size()) - 1; index >= 0; index--) + { + if (regions[index].padId == padId) + { + return index; + } + } + + return -1; + } + + static bool isDescendantPad(llvm::SmallVector ®ions, CatchRegion ®ion, BasicBlock *block) + { + auto index = static_cast(®ion - regions.data()); + for (auto &other : regions) + { + if (other.padBlock != block) + { + continue; + } + + for (auto parent = other.parent; parent >= 0; parent = regions[parent].parent) + { + if (parent == index) + { + return true; + } + } + } + + return false; + } + + // A catchswitch unwinds where exceptions leaving its region go: the first invoke of the + // region that unwinds out of it. An invoke into a pad nested in the region is not one - + // a catchswitch cannot unwind into its own descendants. + static void chooseUnwindInfoOps(llvm::SmallVector ®ions) + { + for (auto ®ion : regions) + { + for (auto *II : region.invokes) + { + if (!isDescendantPad(regions, region, II->getUnwindDest())) + { + LLVM_DEBUG(llvm::dbgs() << "\n!! set (unwindInfoOp) : " << *II << "\n";); + region.unwindInfoOp = II; + break; + } + } + } + } + + // the pad a region's own pads are created within: its parent region's, or none + static Value *parentPadOf(llvm::SmallVector ®ions, CatchRegion ®ion) + { + if (region.parent >= 0) + { + auto &parent = regions[region.parent]; + if (parent.catchPad) + { + return parent.catchPad; + } + + if (parent.cleanupPad) + { + return parent.cleanupPad; + } + } + + return ConstantTokenNone::get(region.landingPad->getContext()); + } + llvm::SmallVector getCallBundleFromCatchRegion(CatchRegion &catchRegion) { llvm::SmallVector opBundle; diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 4f5367b8e..48f5d4d52 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1455,8 +1455,10 @@ tslang_add_test(NAME test-jit-none-alloc-in-catch COMMAND test-runner -jit -mm=n # A try/catch inside a catch clause - crashed in every model at every optimisation level. tslang_add_test(NAME test-compile-00-nested-catch COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00nested_catch.ts") tslang_add_test(NAME test-compile-00-try-mismatch-in-catch-clause COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_mismatch_in_catch_clause.ts") +tslang_add_test(NAME test-compile-00-try-mismatch-in-catch-clause-using COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_mismatch_in_catch_clause_using.ts") tslang_add_test(NAME test-jit-00-nested-catch COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00nested_catch.ts") tslang_add_test(NAME test-jit-00-try-mismatch-in-catch-clause COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_mismatch_in_catch_clause.ts") +tslang_add_test(NAME test-jit-00-try-mismatch-in-catch-clause-using COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_mismatch_in_catch_clause_using.ts") tslang_add_test(NAME test-jit-rc-nested-catch COMMAND test-runner -jit -mm=rc "${PROJECT_SOURCE_DIR}/test/tester/tests/00nested_catch.ts") tslang_add_test(NAME test-jit-none-nested-catch COMMAND test-runner -jit -mm=none "${PROJECT_SOURCE_DIR}/test/tester/tests/00nested_catch.ts") @@ -1660,6 +1662,7 @@ set(TSLANG_CORPUS 00names_conflict.ts 00nested_catch.ts 00try_mismatch_in_catch_clause.ts + 00try_mismatch_in_catch_clause_using.ts 00new_delete.ts 00ns_bug.ts 00ns.ts @@ -2467,12 +2470,6 @@ if (TSLANG_TEST_X86) "${x86_jit_skipped_count} -jit registrations without a twin") endif() -# A mismatched typed catch in a try with a `using`, inside another try's catch clause. Linux -# only: on Windows a `using` inside a catch clause is broken already (see the test's header). -if (NOT WIN32) - tslang_add_test(NAME test-compile-00-try-mismatch-in-catch-clause-using COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_mismatch_in_catch_clause_using.ts") - tslang_add_test(NAME test-jit-00-try-mismatch-in-catch-clause-using COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_mismatch_in_catch_clause_using.ts") -endif() # An untyped catch of a C++ exception tslang did not throw (Itanium only: the box thunk field of # a __pointer_type_info is a Linux mechanism). foreign_throw.cpp throws a `const char*`, a # pointer to a C++ struct and an int; the catch must box each without calling into the type_info. diff --git a/tslang/test/tester/tests/00try_mismatch_in_catch_clause_using.ts b/tslang/test/tester/tests/00try_mismatch_in_catch_clause_using.ts index a318cd5ef..7676346f8 100644 --- a/tslang/test/tester/tests/00try_mismatch_in_catch_clause_using.ts +++ b/tslang/test/tester/tests/00try_mismatch_in_catch_clause_using.ts @@ -1,9 +1,9 @@ -// 00try_mismatch_in_catch_clause.ts, with a `using` in the inner try. Linux only: on Windows a -// `using` inside a catch clause is broken on main already - its cleanup pad is not nested in the -// catch funclet, which fails LLVM's verifier ahead of time, and with a typed catch and no try -// around it the JIT crashes in Win32ExceptionPass (see 00try_using_catch.ts, section 9.11). -// On Linux the mismatch rethrow after the cleanup ran with nothing caught: "terminate called -// without an active exception". +// 00try_mismatch_in_catch_clause.ts, with a `using` in the inner try. On Linux the mismatch +// rethrow after the cleanup ran with nothing caught: "terminate called without an active +// exception". On Windows a `using` inside a catch clause did not work at all: its cleanup pad was +// not nested in the catch funclet - LLVM's verifier rejected the edge out of the catch ahead of +// time - and without a try around it Win32ExceptionPass crashed the compiler, having taken the +// cleanup pad for the end of the catch. let disposed = 0; let finals = 0; @@ -59,6 +59,21 @@ function withUsingAndFinally() { } } +// the shape that crashed the compiler on Windows: no try around the catch, and nothing thrown in +// the inner try, so its typed catch is not taken +function noTryAround() { + try { + throw 1; + } catch (o) { + try { + using r = new Res(); + finals++; + } catch (e: number) { + wrong++; + } + } +} + function main() { reset(); withUsing(); @@ -68,5 +83,9 @@ function main() { withUsingAndFinally(); assert(disposed == 1 && finals == 1 && wrong == 0 && outer == 1, "with a using and a finally"); + reset(); + noTryAround(); + assert(disposed == 1 && finals == 1 && wrong == 0, "no try around the catch"); + print("done."); }