From b1f1b9b0097dd8777c58938b0b8152521f70e3de Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 17 Oct 2026 19:13:43 +0100 Subject: [PATCH] An empty catch clause catches An untyped `catch { }` emits no ops into the TryOp's catches region, and TryOpLowering took a region holding only its terminator for "no catch clause" - the shape of the TryOps that only wrap a scope for its cleanup - so it made no landing pad and the exception went uncaught. mlirGen(TryStatement) now marks a TryOp that has a catch clause with HAS_CATCH_ATTR_NAME, and TryOpLowering treats a marked TryOp as having a catch whatever its region holds. The cleanup-only TryOps stay unmarked. Test 00try_empty_catch.ts (compile, JIT, corpus). Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/Defines.h | 6 + tslang/lib/TypeScript/LowerToAffineLoops.cpp | 2 + tslang/lib/TypeScript/MLIRGenStatements.cpp | 2 + tslang/test/tester/CMakeLists.txt | 3 + tslang/test/tester/tests/00try_empty_catch.ts | 118 ++++++++++++++++++ 5 files changed, 131 insertions(+) create mode 100644 tslang/test/tester/tests/00try_empty_catch.ts diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 4699a7730..97e9e4567 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -73,6 +73,12 @@ // so would be given a frame's reference it has no owner for. A box's owner is the closure. #define CAPTURE_BOX_ATTR_NAME "__capture_box" +// On a `ts.Try`: the statement has a catch clause. Its catches region may still hold nothing but +// the terminator - an untyped `catch { }` emits no ops - and without this marker it would be +// indistinguishable from the catch-less TryOps that only wrap a scope for its cleanup, so the +// lowering would make no landing pad and the exception would go uncaught. +#define HAS_CATCH_ATTR_NAME "__has_catch" + // -mm=own only, set by OwnershipInferencePass for the lowering. On a `ts.CreateBoundFunction`: the // closure does not escape, so its box borrows what it captures - the box's routine frees the box // and nothing it holds. On a `ts.ReleaseCell`: the cell borrows its value (a captured parameter's, diff --git a/tslang/lib/TypeScript/LowerToAffineLoops.cpp b/tslang/lib/TypeScript/LowerToAffineLoops.cpp index b685d2df3..f75e357d3 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -1480,7 +1480,9 @@ struct TryOpLowering : public TsPattern auto cleanupHasOps = llvm::any_of(tryOp.getCleanup(), [](auto &block) { return &block.front() != block.getTerminator(); }); + // an empty `catch { }` still catches: HAS_CATCH_ATTR_NAME tells it from no catch clause auto catchHasOps = + tryOp->hasAttr(HAS_CATCH_ATTR_NAME) || llvm::any_of(tryOp.getCatches(), [](auto &block) { return &block.front() != block.getTerminator(); }); auto finallyHasOps = llvm::any_of(tryOp.getFinally(), [](auto &block) { return &block.front() != block.getTerminator(); }); diff --git a/tslang/lib/TypeScript/MLIRGenStatements.cpp b/tslang/lib/TypeScript/MLIRGenStatements.cpp index 317b90015..07b2f5ef5 100644 --- a/tslang/lib/TypeScript/MLIRGenStatements.cpp +++ b/tslang/lib/TypeScript/MLIRGenStatements.cpp @@ -1504,6 +1504,8 @@ namespace mlirgen builder.setInsertionPointToStart(&tryOp.getCatches().front()); if (catchClause && catchClause->block) { + tryOp->setAttr(HAS_CATCH_ATTR_NAME, builder.getUnitAttr()); + auto location = loc(catchClause->block); if (!varName.empty()) { diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 1a9513139..57264bca5 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -529,6 +529,7 @@ tslang_add_test(NAME test-compile-00-class-new COMMAND test-runner "${PROJECT_SO tslang_add_test(NAME test-compile-00-array-growth COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_growth.ts") tslang_add_test(NAME test-compile-00-global-from-global COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_from_global.ts") tslang_add_test(NAME test-compile-00-try-cleanup-then-catch COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_cleanup_then_catch.ts") +tslang_add_test(NAME test-compile-00-try-empty-catch COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_empty_catch.ts") tslang_add_test(NAME test-compile-00-global-union-from-call COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_union_from_call.ts") tslang_add_test(NAME test-compile-00-capture-in-new-arguments COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00capture_in_new_arguments.ts") tslang_add_test(NAME test-compile-01-class-new COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/01class_new.ts") @@ -1073,6 +1074,7 @@ tslang_add_test(NAME test-jit-00-class-new COMMAND test-runner -jit "${PROJECT_S tslang_add_test(NAME test-jit-00-array-growth COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_growth.ts") tslang_add_test(NAME test-jit-00-global-from-global COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_from_global.ts") tslang_add_test(NAME test-jit-00-try-cleanup-then-catch COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_cleanup_then_catch.ts") +tslang_add_test(NAME test-jit-00-try-empty-catch COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_empty_catch.ts") tslang_add_test(NAME test-jit-00-global-union-from-call COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_union_from_call.ts") tslang_add_test(NAME test-jit-00-capture-in-new-arguments COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00capture_in_new_arguments.ts") tslang_add_test(NAME test-jit-01-class-new COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/01class_new.ts") @@ -1949,6 +1951,7 @@ set(TSLANG_CORPUS 00in.ts 00global_from_global.ts 00try_cleanup_then_catch.ts + 00try_empty_catch.ts 00global_object_literal.ts 00infer.ts 00const_object_literal_field.ts diff --git a/tslang/test/tester/tests/00try_empty_catch.ts b/tslang/test/tester/tests/00try_empty_catch.ts new file mode 100644 index 000000000..cc7e099b8 --- /dev/null +++ b/tslang/test/tester/tests/00try_empty_catch.ts @@ -0,0 +1,118 @@ +// An empty catch clause catches. An untyped `catch { }` emits no ops into the try's catches region, +// and the lowering took a region holding only its terminator for "no catch clause" - the form of the +// try a scope's cleanup is wrapped in - so it made no landing pad and the exception went uncaught. +class Node { + v = 0; +} + +function h() { + throw 1; +} + +function emptyCatch() { + try { + h(); + } catch { + } +} + +function emptyCatchWithVariable() { + try { + h(); + } catch (e) { + } +} + +function emptyStatementInCatch() { + try { + h(); + } catch { + ; + } +} + +function emptyCatchThenFinally() { + let finallyRan = false; + try { + h(); + } catch { + } finally { + finallyRan = true; + } + + return finallyRan; +} + +function emptyCatchWithOwnedLocal() { + try { + let n = new Node(); + n.v = 1; + h(); + } catch { + } +} + +function nestedEmptyCatches() { + let reached = false; + try { + try { + h(); + } catch { + } + + reached = true; + h(); + } catch { + } + + return reached; +} + +function emptyCatchInLoop() { + let count = 0; + for (let i = 0; i < 5; i++) { + try { + h(); + } catch { + } + + count++; + } + + return count; +} + +function noThrowEmptyCatch() { + let value = 0; + try { + value = 7; + } catch { + } + + return value; +} + +class C { + run() { + try { + h(); + } catch { + } + + return true; + } +} + +function main() { + emptyCatch(); + emptyCatchWithVariable(); + emptyStatementInCatch(); + assert(emptyCatchThenFinally(), "the finally after an empty catch runs"); + emptyCatchWithOwnedLocal(); + assert(nestedEmptyCatches(), "an inner empty catch lets the outer try go on"); + assert(emptyCatchInLoop() == 5, "an empty catch in a loop"); + assert(noThrowEmptyCatch() == 7, "a try that does not throw"); + assert(new C().run(), "an empty catch in a method"); + + print("done."); +}