diff --git a/compiler+runtime/include/cpp/jank/analyze/expr/try.hpp b/compiler+runtime/include/cpp/jank/analyze/expr/try.hpp index 088537884..e4cc210ef 100644 --- a/compiler+runtime/include/cpp/jank/analyze/expr/try.hpp +++ b/compiler+runtime/include/cpp/jank/analyze/expr/try.hpp @@ -1,7 +1,9 @@ #pragma once + #include +#include #include namespace jank::runtime::obj @@ -36,7 +38,7 @@ namespace jank::analyze::expr void walk(std::function)> const &f) override; do_ref body; - jtl::option catch_body{}; + native_vector catch_bodies{}; jtl::option finally_body{}; }; } diff --git a/compiler+runtime/include/cpp/jank/codegen/processor.hpp b/compiler+runtime/include/cpp/jank/codegen/processor.hpp index 048215b59..7452b2570 100644 --- a/compiler+runtime/include/cpp/jank/codegen/processor.hpp +++ b/compiler+runtime/include/cpp/jank/codegen/processor.hpp @@ -171,7 +171,7 @@ namespace jank::codegen void build_footer(); jtl::immutable_string expression_str(bool box_needed); - jtl::immutable_string module_init_str(jtl::immutable_string const &module); + jtl::immutable_string module_init_str(jtl::immutable_string const &module_name); void format_elided_var(jtl::immutable_string const &start, jtl::immutable_string const &end, diff --git a/compiler+runtime/include/cpp/jank/detail/to_runtime_data.hpp b/compiler+runtime/include/cpp/jank/detail/to_runtime_data.hpp index d2d555814..94fa6f535 100644 --- a/compiler+runtime/include/cpp/jank/detail/to_runtime_data.hpp +++ b/compiler+runtime/include/cpp/jank/detail/to_runtime_data.hpp @@ -1,13 +1,15 @@ #pragma once + #include #include -#include -#include #include #include +#include +#include +#include namespace jank::detail { @@ -100,4 +102,16 @@ namespace jank::detail { return m; } + + template + object_ref to_runtime_data(native_vector const &m) + { + /* NOLINTNEXTLINE(misc-const-correctness): Can't be const. */ + runtime::detail::native_persistent_vector ret; + for(auto const &e : m) + { + (void)ret.push_back(to_runtime_data(e)); + } + return make_box(ret); + } } diff --git a/compiler+runtime/src/cpp/jank/analyze/cpp_util.cpp b/compiler+runtime/src/cpp/jank/analyze/cpp_util.cpp index aac12ebaa..3d42701d6 100644 --- a/compiler+runtime/src/cpp/jank/analyze/cpp_util.cpp +++ b/compiler+runtime/src/cpp/jank/analyze/cpp_util.cpp @@ -71,6 +71,17 @@ namespace jank::analyze::cpp_util jtl::ptr resolve_type(jtl::immutable_string const &sym, u8 const ptr_count) { + /* Clang canonicalizes "char" to "signed char" on some platforms, which breaks exception + * handling since they are distinct types. We use resolve_literal_type to get the + * exact type for "char". */ + if(sym == "char") + { + if(auto const res{ resolve_literal_type("char").expect_ok() }) + { + return apply_pointers(res, ptr_count); + } + } + auto const type{ Cpp::GetType(sym) }; if(type) { diff --git a/compiler+runtime/src/cpp/jank/analyze/expr/try.cpp b/compiler+runtime/src/cpp/jank/analyze/expr/try.cpp index 04a333742..5a7b67f45 100644 --- a/compiler+runtime/src/cpp/jank/analyze/expr/try.cpp +++ b/compiler+runtime/src/cpp/jank/analyze/expr/try.cpp @@ -34,11 +34,14 @@ namespace jank::analyze::expr { position = pos; body->propagate_position(pos); - if(catch_body) + if(!catch_bodies.empty()) { - catch_body.unwrap().propagate_position(pos); + for(auto const &catch_body : catch_bodies) + { + catch_body.propagate_position(pos); + } } - /* The result of the 'finally' body is discarded, so we always keep it in statement position. */ + /* The result of the 'finally' body is discarded, so we always keep it in the statement position. */ } runtime::object_ref try_::to_runtime_data() const @@ -50,7 +53,7 @@ namespace jank::analyze::expr persistent_array_map::create_unique(make_box("body"), body->to_runtime_data(), make_box("catch"), - jank::detail::to_runtime_data(catch_body), + jank::detail::to_runtime_data(catch_bodies), make_box("finally"), jank::detail::to_runtime_data(finally_body))); } @@ -58,9 +61,12 @@ namespace jank::analyze::expr void try_::walk(std::function)> const &f) { f(body); - if(catch_body.is_some()) + if(!catch_bodies.empty()) { - f(catch_body.unwrap().body); + for(auto const &catch_body : catch_bodies) + { + f(catch_body.body); + } } if(finally_body.is_some()) { diff --git a/compiler+runtime/src/cpp/jank/analyze/processor.cpp b/compiler+runtime/src/cpp/jank/analyze/processor.cpp index 0d5262d3d..397dae42e 100644 --- a/compiler+runtime/src/cpp/jank/analyze/processor.cpp +++ b/compiler+runtime/src/cpp/jank/analyze/processor.cpp @@ -2259,7 +2259,7 @@ namespace jank::analyze /* All bindings in a letfn appear simultaneously and may be mutually recursive. * This makes creating a letfn locals frame a bit more involved than let, where locals - * are introduced left-to-right. For example, each binding in (letfn [(a [] b) (b [] a)]) + * are introduced left-to-right. For example, each binding in (letfn [(a [] b) (b [] a)]) * requires the other to be in scope in order to be analyzed. * * We tackle this in two steps. First, we create empty local bindings for all names. @@ -2616,7 +2616,6 @@ namespace jank::analyze auto try_frame(jtl::make_ref(local_frame::frame_type::try_, current_frame)); /* We introduce a new frame so that we can register the sym as a local. * It holds the exception value which was caught. */ - auto catch_frame(jtl::make_ref(local_frame::frame_type::catch_, current_frame)); auto finally_frame(jtl::make_ref(local_frame::frame_type::finally, current_frame)); auto ret{ jtl::make_ref(position, try_frame, true, jtl::make_ref()) }; @@ -2701,69 +2700,124 @@ namespace jank::analyze object_source(item), latest_expansion(macro_expansions)); } - if(has_catch) - { - /* TODO: Note where the other catch is. */ - return error::analyze_invalid_try("Only one 'catch' form may be supplied.", - object_source(item), - latest_expansion(macro_expansions)); - } has_catch = true; - /* Verify we have (catch ...) */ + /* Verify we have (catch cpp/type ...) */ auto const catch_list(runtime::list(item)); - auto const catch_body_size(catch_list->count()); - if(catch_body_size == 1) + if(auto const catch_body_size(catch_list->count()); catch_body_size < 2) { return error::analyze_invalid_try( - "A symbol is required after 'catch', which is used as the binding to " - "hold the exception value.", + "Each 'catch' form requires the type of exception to catch and a symbol for the " + "name of the exception value.", object_source(item), latest_expansion(macro_expansions)); } + auto catch_it(catch_list->data.rest()); + auto const catch_type_form(catch_it.first().unwrap()); + catch_it = catch_it.rest(); + auto const catch_sym_form(catch_it.first().unwrap()); + auto const catch_type(analyze(catch_type_form, current_frame, position, fn_ctx, true)); + if(catch_type.is_err()) + { + return error::analyze_invalid_try(catch_type.expect_err()->message, + object_source(item), + error::note{ + "An exception type is required before this form.", + object_source(catch_sym_form), + }, + latest_expansion(macro_expansions)) + ->add_usage(read::parse::reparse_nth(item, 1)); + } + + if(catch_type.expect_ok()->kind != expression_kind::cpp_type) + { + return error::analyze_invalid_try("Exception is not a type.", + object_source(item), + error::note{ + "An exception type is required before this form.", + object_source(catch_sym_form), + }, + latest_expansion(macro_expansions)) + ->add_usage(read::parse::reparse_nth(item, 1)); + } - auto const sym_obj(catch_list->data.rest().first().unwrap()); - if(sym_obj->type != runtime::object_type::symbol) + if(catch_sym_form->type != runtime::object_type::symbol) { return error::analyze_invalid_try( - "A symbol required after 'catch', which is used as the binding to " + "A symbol is required after 'catch', which is used as the binding to " "hold the exception value.", object_source(item), error::note{ "A symbol is required before this form.", - object_source(sym_obj), + object_source(catch_sym_form), }, latest_expansion(macro_expansions)) - ->add_usage(read::parse::reparse_nth(item, 1)); + ->add_usage(read::parse::reparse_nth(item, 2)); + } + auto const catch_sym(runtime::expect_object(catch_sym_form)); + if(!catch_sym->get_namespace().empty()) + { + return error::analyze_invalid_try( + "The binding symbol in 'catch' must be unqualified.", + object_source(item), + latest_expansion(macro_expansions)); } + auto const catch_type_ref(static_ref_cast(catch_type.expect_ok())); + /* If we're catching a C++ class/struct by value, we want to promote it to a reference + * to avoid object slicing and to enable polymorphism. + * However, we must NOT promote types in the jank::runtime namespace (like object_ref), + * as these are smart pointers expected to be passed by value in the runtime. */ + if(!Cpp::IsPointerType(catch_type_ref->type) + && !Cpp::IsReferenceType(catch_type_ref->type) + && !Cpp::IsBuiltin(catch_type_ref->type)) + { + /* Check if this type is in the jank::runtime namespace */ + auto const type_scope{ Cpp::GetScopeFromType(catch_type_ref->type) }; + auto const type_name{ cpp_util::get_qualified_name(type_scope) }; + bool const is_jank_runtime_type{ type_name.find("jank::runtime::") == 0 }; - auto const sym(runtime::expect_object(sym_obj)); - if(!sym->get_namespace().empty()) + if(!is_jank_runtime_type) + { + catch_type_ref->type = Cpp::GetLValueReferenceType(catch_type_ref->type); + } + } + + /* Check for duplicate catch types. */ + /*TODO Add full error handling for duplicated catch types: + * Add a new error kind and notes pointing to the duplicated types*/ + for(auto const &existing_catch : ret->catch_bodies) { - return error::analyze_invalid_try("The symbol after 'catch' must be unqualified.", - object_source(sym_obj), - latest_expansion(macro_expansions)); + if(existing_catch.type.data == catch_type_ref->type.data) + { + return error::analyze_invalid_try("Each catch form must specify a unique type.", + object_source(item), + latest_expansion(macro_expansions)); + } } - catch_frame->locals.emplace(sym, local_binding{ sym, sym->name, none, catch_frame }); + auto catch_frame( + jtl::make_ref(local_frame::frame_type::catch_, current_frame)); + catch_frame->locals.emplace(catch_sym, + local_binding{ catch_sym, + catch_sym->name, + none, + catch_frame, + true, + false, + false, + catch_type_ref->type }); /* Now we just turn the body into a do block and have the do analyzer handle the rest. */ - auto const do_list( - catch_list->data.rest().rest().conj(make_box("do"))); + auto const do_list(catch_it.rest().conj(make_box("do"))); auto const do_res(analyze(make_box(do_list), catch_frame, position, fn_ctx, true)); if(do_res.is_err()) { return do_res.expect_err(); } - - /* TODO: Read this from the catch form. */ - static auto const object_ref_type{ cpp_util::resolve_literal_type( - "jank::runtime::oref") - .expect_ok() }; - - ret->catch_body = expr::catch_{ sym, - object_ref_type, - static_ref_cast(do_res.expect_ok()) }; + do_res.expect_ok()->frame = catch_frame; + ret->catch_bodies.emplace_back(catch_sym, + catch_type_ref->type, + static_ref_cast(do_res.expect_ok())); } break; case try_expression_type::finally_: @@ -2797,10 +2851,6 @@ namespace jank::analyze ret->body->frame = try_frame; ret->body->propagate_position(position); - if(ret->catch_body.is_some()) - { - ret->catch_body.unwrap().body->frame = catch_frame; - } if(ret->finally_body.is_some()) { ret->finally_body.unwrap()->frame = finally_frame; @@ -2861,12 +2911,12 @@ namespace jank::analyze /* Eval the literal to resolve exprs such as quotes. */ auto const pre_eval_expr( jtl::make_ref(position, current_frame, true, std::move(exprs), o->meta)); - auto const o(evaluate::eval(pre_eval_expr)); + auto const oref(evaluate::eval(pre_eval_expr)); /* TODO: Order lifted constants. Use sub constants during codegen. */ - current_frame->lift_constant(o); + current_frame->lift_constant(oref); - return jtl::make_ref(position, current_frame, true, o); + return jtl::make_ref(position, current_frame, true, oref); } return jtl::make_ref(position, current_frame, true, std::move(exprs), o->meta); diff --git a/compiler+runtime/src/cpp/jank/codegen/llvm_processor.cpp b/compiler+runtime/src/cpp/jank/codegen/llvm_processor.cpp index d606df146..08b03fd12 100644 --- a/compiler+runtime/src/cpp/jank/codegen/llvm_processor.cpp +++ b/compiler+runtime/src/cpp/jank/codegen/llvm_processor.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -73,6 +75,16 @@ namespace jank::codegen std::unique_ptr pic; std::unique_ptr si; llvm::ModulePassManager mpm; + + struct exception_handler_info + { + llvm::BasicBlock *dispatch_block; + llvm::PHINode *ex_phi; + llvm::PHINode *sel_phi; + analyze::expr::try_ref expr; + }; + + std::vector exception_handlers; }; struct llvm_processor::impl @@ -126,6 +138,38 @@ namespace jank::codegen llvm::Value *gen(analyze::expr::cpp_new_ref, analyze::expr::function_arity const &); llvm::Value *gen(analyze::expr::cpp_delete_ref, analyze::expr::function_arity const &); + void route_unhandled_exception(llvm::Value *ex_ptr, + llvm::Value *selector, + llvm::BasicBlock *current_block) const; + + void register_catch_clause_rtti(expr::catch_ const &catch_clause, + llvm::LandingPadInst *landing_pad, + native_set ®istered_rtti); + + void generate_catch_block(size_t catch_index, + expr::try_ref const expr, + llvm::BasicBlock *catch_block, + llvm::Value *current_ex_ptr, + llvm::AllocaInst *result_slot, + llvm::BasicBlock *finally_block, + llvm::AllocaInst *unwind_flag_slot, + llvm::BasicBlock *continue_block, + expr::function_arity const &arity); + + void generate_catch_dispatch(expr::try_ref const expr, + llvm::PHINode *exception_phi, + llvm::PHINode *selector_phi, + llvm::BasicBlock *dispatch_block, + llvm::BasicBlock *continue_block, + llvm::AllocaInst *result_slot, + llvm::BasicBlock *finally_block, + llvm::AllocaInst *unwind_flag_slot, + llvm::AllocaInst *exception_slot, + llvm::BasicBlock *catch_cleanup_block, + expr::function_arity const &arity); + void register_parent_catch_clauses(llvm::LandingPadInst *landing_pad, + native_set ®istered_rtti); + llvm::Value *gen_var(obj::symbol_ref qualified_name) const; llvm::Value *gen_var_root(obj::symbol_ref qualified_name, var_root_kind kind) const; llvm::Value *gen_c_string(jtl::immutable_string const &s) const; @@ -496,7 +540,8 @@ namespace jank::codegen return arg; } - if(llvm::isa(arg) && cpp_util::is_any_object(type)) + if(llvm::isa(arg) + && (cpp_util::is_any_object(type) || Cpp::IsPointerType(type) || Cpp::IsReferenceType(type))) { arg = ctx->builder->CreateLoad(ctx->builder->getPtrTy(), arg); } @@ -1638,6 +1683,7 @@ namespace jank::codegen llvm_processor::impl::gen(expr::throw_ref const expr, expr::function_arity const &arity) { auto const value(gen(expr->value, arity)); + auto const loaded_val{ load_if_needed(ctx, value) }; auto const fn_type( llvm::FunctionType::get(ctx->builder->getVoidTy(), { ctx->builder->getPtrTy() }, false)); auto fn(llvm_module->getOrInsertFunction("jank_throw", fn_type)); @@ -1651,12 +1697,12 @@ namespace jank::codegen ctx->builder->CreateInvoke(fn, unreachable_dest, lpad_and_catch_body_stack.back().lpad_bb, - { value }); + { loaded_val }); ctx->builder->SetInsertPoint(unreachable_dest); } else { - ctx->builder->CreateCall(fn, { value }); + ctx->builder->CreateCall(fn, { loaded_val }); } /* Since this code path never completes, it doesn't matter what we return. @@ -1669,17 +1715,308 @@ namespace jank::codegen return ret; } + void llvm_processor::impl::route_unhandled_exception(llvm::Value *ex_ptr, + llvm::Value *selector, + llvm::BasicBlock *current_block) const + { + /* Get parent handler (if exists) */ + reusable_context::exception_handler_info const *parent_handler{}; + if(ctx->exception_handlers.size() > 1) + { + parent_handler = &ctx->exception_handlers[ctx->exception_handlers.size() - 2]; + } + + if(parent_handler) + { + /* Branch to parent handler */ + ctx->builder->CreateBr(parent_handler->dispatch_block); + parent_handler->ex_phi->addIncoming(ex_ptr, current_block); + parent_handler->sel_phi->addIncoming(selector, current_block); + } + else + { + /* No parent handler - resume unwinding */ + auto const resume_fn{ llvm_module->getOrInsertFunction("_Unwind_Resume", + ctx->builder->getVoidTy(), + ctx->builder->getPtrTy()) }; + ctx->builder->CreateCall(resume_fn, { ex_ptr }); + ctx->builder->CreateUnreachable(); + } + } + + void llvm_processor::impl::register_catch_clause_rtti(expr::catch_ const &catch_clause, + llvm::LandingPadInst *landing_pad, + native_set ®istered_rtti) + { + auto const catch_type{ catch_clause.type }; + auto const exception_rtti{ Cpp::MangleRTTI(catch_type) }; + if constexpr(jtl::current_platform == jtl::platform::macos_like) + { + static native_set rtti_syms; + if(!rtti_syms.contains(exception_rtti)) + { + cpp_util::register_rtti(catch_type); + rtti_syms.emplace(exception_rtti); + } + auto const callable{ + Cpp::MakeRTTICallable(catch_type, exception_rtti, __rt_ctx->unique_munged_string()) + }; + global_rtti.emplace(exception_rtti, callable); + } + auto const exception_rtti_global{ llvm_module->getOrInsertGlobal(exception_rtti, + ctx->builder->getPtrTy()) }; + if(!registered_rtti.contains(exception_rtti_global)) + { + landing_pad->addClause(exception_rtti_global); + registered_rtti.emplace(exception_rtti_global); + } + } + + void llvm_processor::impl::generate_catch_block(size_t catch_index, + expr::try_ref const expr, + llvm::BasicBlock *catch_block, + llvm::Value *current_ex_ptr, + llvm::AllocaInst *result_slot, + llvm::BasicBlock *finally_block, + llvm::AllocaInst *unwind_flag_slot, + llvm::BasicBlock *continue_block, + expr::function_arity const &arity) + { + auto const ptr_ty{ ctx->builder->getPtrTy() }; + auto const &catch_clause{ expr->catch_bodies[catch_index] }; + auto const &[catch_sym, catch_type, catch_body]{ catch_clause }; + + ctx->builder->SetInsertPoint(catch_block); + + auto old_locals(locals); + auto const begin_catch_fn{ + llvm_module->getOrInsertFunction("__cxa_begin_catch", ptr_ty, ptr_ty) + }; + auto const caught_ptr{ ctx->builder->CreateCall(begin_catch_fn, { current_ex_ptr }) }; + auto const ex_val_type{ llvm_type(*ctx, llvm_ctx, catch_type).type.data }; + /* For pointer types (e.g., void*, int*), the exception system stores the pointer VALUE + * itself at caught_ptr, so we use it directly. For non-pointer types (e.g., int, double), + * the value is stored AT the caught_ptr address, so we must dereference with CreateLoad. */ + if(Cpp::IsPointerType(catch_type) || Cpp::IsReferenceType(catch_type) + || (!Cpp::IsBuiltin(catch_type) && !Cpp::IsEnumType(catch_type) + && !cpp_util::is_any_object(catch_type))) + { + /* Pointer exception: caught_ptr IS the value we want */ + locals[catch_sym] = caught_ptr; + } + else + { + llvm::Value *raw_ex_val{}; + /* Non-pointer exception: must dereference to get the value */ + raw_ex_val = ctx->builder->CreateLoad(ex_val_type, caught_ptr, "ex.val"); + auto const current_fn = ctx->builder->GetInsertBlock()->getParent(); + llvm::AllocaInst *ex_val_slot{}; + { + llvm::IRBuilder<>::InsertPointGuard const guard(*ctx->builder); + llvm::IRBuilder<> entry_builder_local(¤t_fn->getEntryBlock(), + current_fn->getEntryBlock().getFirstInsertionPt()); + ex_val_slot + = entry_builder_local.CreateAlloca(ex_val_type, + nullptr, + util::format("{}.slot", catch_sym->name).data()); + } + ctx->builder->CreateStore(raw_ex_val, ex_val_slot); + + locals[catch_sym] = ex_val_slot; + } + + + auto const original_catch_pos{ catch_body->position }; + catch_body->propagate_position(expression_position::value); + util::scope_exit const restore_catch_pos{ [&]() { + catch_body->propagate_position(original_catch_pos); + } }; + auto catch_val{ gen(catch_body, arity) }; + + auto const end_catch_fn{ llvm_module->getOrInsertFunction("__cxa_end_catch", + ctx->builder->getVoidTy()) }; + ctx->builder->CreateCall(end_catch_fn, {}); + auto body_type{ cpp_util::expression_type(catch_body) }; + if(!body_type) + { + body_type = catch_type; + } + auto const loaded_val{ load_if_needed(ctx, catch_val, body_type) }; + auto object_ref_type{ Cpp::GetType("jank::runtime::object_ref") }; + if(!object_ref_type) + { + object_ref_type = Cpp::GetType("jank::runtime::oref"); + } + if(!object_ref_type) + { + object_ref_type = Cpp::GetType("long long"); + } + + auto const converted_val{ convert_object(*ctx, + llvm_ctx, + llvm_module, + conversion_policy::into_object, + body_type, + object_ref_type, + body_type, + loaded_val) }; + locals = std::move(old_locals); + + if(!ctx->builder->GetInsertBlock()->getTerminator()) + { + if(!catch_val) + { + catch_val = gen_global(jank_nil); + ctx->builder->CreateStore(catch_val, result_slot); + } + else + { + ctx->builder->CreateStore(converted_val, result_slot); + } + if(finally_block) + { + ctx->builder->CreateStore(ctx->builder->getFalse(), unwind_flag_slot); + ctx->builder->CreateBr(finally_block); + } + else + { + ctx->builder->CreateBr(continue_block); + } + } + } + + void llvm_processor::impl::generate_catch_dispatch(expr::try_ref const expr, + llvm::PHINode *exception_phi, + llvm::PHINode *selector_phi, + llvm::BasicBlock *dispatch_block, + llvm::BasicBlock *continue_block, + llvm::AllocaInst *result_slot, + llvm::BasicBlock *finally_block, + llvm::AllocaInst *unwind_flag_slot, + llvm::AllocaInst *exception_slot, + llvm::BasicBlock *catch_cleanup_block, + expr::function_arity const &arity) + { + auto const current_fn = ctx->builder->GetInsertBlock()->getParent(); + auto const has_finally = (finally_block != nullptr); + + ctx->builder->SetInsertPoint(dispatch_block); + + /* Use PHI values for dispatch */ + auto const current_ex_ptr{ exception_phi }; + auto const current_sel{ selector_phi }; + + auto const typeid_fn_type{ + llvm::FunctionType::get(ctx->builder->getInt32Ty(), { ctx->builder->getPtrTy() }, false) + }; + auto const typeid_fn{ llvm_module->getOrInsertFunction("llvm.eh.typeid.for.p0", + typeid_fn_type) }; + + std::vector catch_blocks; + for(size_t i = 0; i < expr->catch_bodies.size(); ++i) + { + auto catch_block + = llvm::BasicBlock::Create(*llvm_ctx, util::format("catch.{}", i).data(), current_fn); + catch_blocks.push_back(catch_block); + } + + auto const fallback_block = llvm::BasicBlock::Create(*llvm_ctx, "catch.fallback", current_fn); + + /* Generate type matching and conditional branches */ + for(size_t i = 0; i < expr->catch_bodies.size(); ++i) + { + auto const &catch_clause{ expr->catch_bodies[i] }; + auto const catch_type{ catch_clause.type }; + auto const exception_rtti{ Cpp::MangleRTTI(catch_type) }; + auto const exception_rtti_global{ llvm_module->getOrInsertGlobal(exception_rtti, + ctx->builder->getPtrTy()) }; + + auto const type_id = ctx->builder->CreateCall(typeid_fn, + { exception_rtti_global }, + util::format("typeid.{}", i).data()); + + auto const matches + = ctx->builder->CreateICmpEQ(current_sel, type_id, util::format("matches.{}", i).data()); + + llvm::BasicBlock *no_match_target{}; + if(i + 1 < expr->catch_bodies.size()) + { + no_match_target = llvm::BasicBlock::Create(*llvm_ctx, + util::format("catch.check.{}", i + 1).data(), + current_fn); + } + else + { + no_match_target = fallback_block; + } + + ctx->builder->CreateCondBr(matches, catch_blocks[i], no_match_target); + + if(i + 1 < expr->catch_bodies.size()) + { + ctx->builder->SetInsertPoint(no_match_target); + } + } + + /* Push the cleanup pad for catch blocks. + * Any exception thrown from within a catch block (including rethrows) will land here. */ + lpad_and_catch_body_stack.emplace_back(catch_cleanup_block, nullptr); + util::scope_exit const pop_catch_lpad{ [this]() { lpad_and_catch_body_stack.pop_back(); } }; + + /* Generate catch block bodies */ + for(size_t i = 0; i < expr->catch_bodies.size(); ++i) + { + generate_catch_block(i, + expr, + catch_blocks[i], + current_ex_ptr, + result_slot, + finally_block, + unwind_flag_slot, + continue_block, + arity); + } + + /* Generate fallback block for unmatched exceptions */ + ctx->builder->SetInsertPoint(fallback_block); + if(has_finally) + { + ctx->builder->CreateStore(current_ex_ptr, exception_slot); + ctx->builder->CreateStore(ctx->builder->getTrue(), unwind_flag_slot); + ctx->builder->CreateBr(finally_block); + } + else + { + /* No finally in current try. Check for parent handler. */ + route_unhandled_exception(current_ex_ptr, current_sel, fallback_block); + } + } + + void + llvm_processor::impl::register_parent_catch_clauses(llvm::LandingPadInst *landing_pad, + native_set ®istered_rtti) + { + for(size_t i = 0; i < ctx->exception_handlers.size() - 1; ++i) + { + for(auto const &parent_expr = ctx->exception_handlers[i].expr; + auto const &catch_clause : parent_expr->catch_bodies) + { + register_catch_clause_rtti(catch_clause, landing_pad, registered_rtti); + } + } + } + llvm::Value * llvm_processor::impl::gen(expr::try_ref const expr, expr::function_arity const &arity) { - if(expr->catch_body.is_none() && expr->finally_body.is_none()) + if(expr->catch_bodies.empty() && expr->finally_body.is_none()) { return gen(expr->body, arity); } auto const current_fn(ctx->builder->GetInsertBlock()->getParent()); - auto &entry_bb{ current_fn->getEntryBlock() }; - llvm::IRBuilder<> entry_builder(&entry_bb, entry_bb.getFirstInsertionPt()); + auto &entry_block{ current_fn->getEntryBlock() }; + llvm::IRBuilder<> entry_builder(&entry_block, entry_block.getFirstInsertionPt()); auto const ptr_ty{ ctx->builder->getPtrTy() }; if(!current_fn->hasPersonalityFn()) @@ -1698,19 +2035,7 @@ namespace jank::codegen auto const is_return(expr->position == expression_position::tail); auto const has_finally{ expr->finally_body.is_some() }; - auto const has_catch{ expr->catch_body.is_some() }; - - /* unwind_flag_slot: An alloca for a boolean (i1). This flag is set to true if the 'finally' - * block is being entered as part of an exception unwinding process (e.g., from a landing pad). - * It's false if 'finally' is entered after normal completion of the try or catch block. - * This controls whether to resume unwinding or continue normally after the "finally" block. */ - llvm::AllocaInst *unwind_flag_slot{}; - - /* exception_slot: An alloca for a pointer. When unwinding_flag_slot is true, this slot holds - * the exception object (typically an i8* or a struct pointer) that was caught by the landing - * pad. This pointer is needed if the exception needs to be resumed or rethrown after the - * 'finally' block. */ - llvm::AllocaInst *exception_slot{}; + auto const has_catch{ !expr->catch_bodies.empty() }; /* result_slot: An alloca for a pointer (object_ref). This slot holds the llvm::Value* * that represents the result of the (try ...) expression. @@ -1719,354 +2044,277 @@ namespace jank::codegen * - If an exception is caught and handled by a 'catch' block, the result of the * 'catch' block is stored here. * - * This value is then loaded in the continuation block ('cont_bb') after any - * 'finally' block has executed. Because control flow always passes through the 'finally' - * block on normal exits (if a finally exists), we can't directly use a PHI node in - * 'cont_bb' with predecessors from the end of 'try' and 'catch'. This slot acts as a - * temporary variable to hold the result before entering 'finally'. */ + * This value is then loaded in the continuation block ('continue_block') after any + * 'finally' block has executed. + * */ llvm::AllocaInst *result_slot{ entry_builder.CreateAlloca(ptr_ty, nullptr, "try.result.slot") }; - llvm::BasicBlock *finally_bb{}; - llvm::BasicBlock *unwind_action_bb{}; ctx->builder->CreateStore(gen_global(jank_nil), result_slot); + llvm::AllocaInst *dispatch_ex_slot{ + entry_builder.CreateAlloca(ptr_ty, nullptr, "dispatch.ex") + }; + llvm::AllocaInst *selector_slot{ + entry_builder.CreateAlloca(ctx->builder->getInt32Ty(), nullptr, "selector.slot") + }; + + llvm::BasicBlock *finally_block{}; + llvm::BasicBlock *unwind_action_block{}; + + /* unwind_flag_slot: An alloca for a boolean (i1). This flag is set to true if the 'finally' + * block is being entered as part of an exception unwinding process (e.g., from a landing pad). + * It's false if 'finally' is entered after normal completion of the try or catch block. + * This controls whether to resume unwinding or continue normally after the "finally" block. */ + llvm::AllocaInst *unwind_flag_slot{}; + + /* preserved_ex_slot: An alloca for a pointer. When unwinding_flag_slot is true, this slot holds + * the exception object (typically an i8* or a struct pointer) that was caught by the landing + * pad. This pointer is needed if the exception needs to be resumed or rethrown after the + * 'finally' block. */ + llvm::AllocaInst *preserved_ex_slot{}; + if(has_finally) { unwind_flag_slot = entry_builder.CreateAlloca(ctx->builder->getInt1Ty(), nullptr, "unwind.flag.slot"); - exception_slot = entry_builder.CreateAlloca(ptr_ty, nullptr, "exception.slot"); - finally_bb = llvm::BasicBlock::Create(*llvm_ctx, "finally"); - unwind_action_bb = llvm::BasicBlock::Create(*llvm_ctx, "unwind.action"); + preserved_ex_slot = entry_builder.CreateAlloca(ptr_ty, nullptr, "preserved.ex"); + finally_block = llvm::BasicBlock::Create(*llvm_ctx, "finally"); + unwind_action_block = llvm::BasicBlock::Create(*llvm_ctx, "unwind.action"); + } + auto const continue_block{ llvm::BasicBlock::Create(*llvm_ctx, "try.cont") }; + auto const landing_pad_block{ llvm::BasicBlock::Create(*llvm_ctx, "lpad", current_fn) }; + /* dispatch_block: The entry point for exception dispatching. It takes the exception pointer + * and selector as PHI nodes, allowing entry from the landing pad OR from inner try blocks. */ + auto const dispatch_block{ llvm::BasicBlock::Create(*llvm_ctx, "dispatch", current_fn) }; + + llvm::BasicBlock *catch_cleanup_block{}; + /* NOLINTNEXTLINE(misc-const-correctness): Can't be const. */ + llvm::BasicBlock *catch_body_block{}; + if(has_catch) + { + catch_body_block = llvm::BasicBlock::Create(*llvm_ctx, "catch.body"); + /* catch_cleanup_block: A cleanup landing pad for the catch blocks themselves. + * If a catch block throws (rethrow or new throw), we must call __cxa_end_catch + * to release the currently caught exception before propagating the new one. + * We also need to route to 'finally' if it exists. */ + catch_cleanup_block = llvm::BasicBlock::Create(*llvm_ctx, "catch.cleanup", current_fn); } - auto const cont_bb{ llvm::BasicBlock::Create(*llvm_ctx, "try.cont") }; - auto const lpad_bb{ llvm::BasicBlock::Create(*llvm_ctx, "lpad", current_fn) }; - llvm::BasicBlock *catch_body_bb{}; - if(has_catch) + /* Create PHI nodes in dispatch_block early so we can push them to the stack. */ + llvm::PHINode *exception_phi{}; + llvm::PHINode *selector_phi{}; { - catch_body_bb = llvm::BasicBlock::Create(*llvm_ctx, "catch.body"); + llvm::IRBuilder<>::InsertPointGuard const guard(*ctx->builder); + ctx->builder->SetInsertPoint(dispatch_block); + exception_phi = ctx->builder->CreatePHI(ptr_ty, 1, "ex.phi"); + selector_phi = ctx->builder->CreatePHI(ctx->builder->getInt32Ty(), 1, "sel.phi"); } - lpad_and_catch_body_stack.emplace_back(lpad_bb, catch_body_bb); - util::scope_exit const pop_landing_pad{ [this]() { lpad_and_catch_body_stack.pop_back(); } }; + /* Push current handler info for nested tries to use. */ + ctx->exception_handlers.push_back({ dispatch_block, exception_phi, selector_phi, expr }); + util::scope_exit const pop_handler{ [this]() { ctx->exception_handlers.pop_back(); } }; /* --- Try block --- */ - auto const original_try_pos{ expr->body->position }; - - /* We put the try body into the value position so that no return is generated, which allows - * us to continue onto the finally block, if we have one. */ - expr->body->propagate_position(expression_position::value); - auto const try_val{ gen(expr->body, arity) }; - expr->body->propagate_position(original_try_pos); - - /* Handles the normal completion of the 'try' block. - * If code generation for the 'try' body produces a value (try_val is not null) - * and the current basic block doesn't already have a terminator (e.g., from a return - * or throw within the try body itself), this block adds the necessary instructions. - * - * 1. Store Result: The result of the 'try' block (try_val) is stored into the - * 'result_slot' to be potentially used after the 'finally' block. - * 2. Branch to finally or continuation: - * - If a 'finally' block exists ('has_finally' is true), it prepares for - * entering the finally block normally. This involves setting the 'unwind_flag_slot' - * to false (signifying not unwinding from an exception) and creating an - * unconditional branch to 'finally_bb'. - * - If there's no 'finally' block, it branches directly to the continuation - * block 'cont_bb', as the try-catch-finally construct is complete. */ - if(try_val && !ctx->builder->GetInsertBlock()->getTerminator()) - { - ctx->builder->CreateStore(try_val, result_slot); - if(has_finally) - { - ctx->builder->CreateStore(ctx->builder->getFalse(), unwind_flag_slot); - ctx->builder->CreateBr(finally_bb); - } - else + { + lpad_and_catch_body_stack.emplace_back(landing_pad_block, catch_body_block); + util::scope_exit const pop_landing_pad{ [this]() { lpad_and_catch_body_stack.pop_back(); } }; + + auto const original_try_pos{ expr->body->position }; + + /* We put the try body into the value position so that no return is generated, which allows + * us to continue onto the finally block, if we have one. */ + expr->body->propagate_position(expression_position::value); + auto const try_val{ gen(expr->body, arity) }; + expr->body->propagate_position(original_try_pos); + + /* Handles the normal completion of the 'try' block. + * If code generation for the 'try' body produces a value (try_val is not null) + */ + if(try_val && !ctx->builder->GetInsertBlock()->getTerminator()) { - ctx->builder->CreateBr(cont_bb); + ctx->builder->CreateStore(try_val, result_slot); + if(has_finally) + { + ctx->builder->CreateStore(ctx->builder->getFalse(), unwind_flag_slot); + ctx->builder->CreateBr(finally_block); + } + else + { + ctx->builder->CreateBr(continue_block); + } } } - - /* --- Landing Pad & Catch/Resume Logic --- - * We are now about to generate code for the landing pad (lpad_bb), which catches - * exceptions thrown from the preceding 'try' block. - * - * IMPORTANT: Exceptions thrown from *within* the 'catch' or 'finally' clauses - * associated with THIS try-catch-finally statement should NOT be caught by this same - * landing pad (lpad_bb). Instead, they should be handled by any outer exception - * handlers or propagate up. - * - * To achieve this, we pop the current (lpad_bb, catch_body_bb) pair from the - * 'lpad_and_catch_body_stack'. This stack is used by 'CreateInvoke' to determine - * the unwind destination. By popping, any 'invoke' calls within the catch/finally - * code will use the *next* landing pad on the stack (if any), belonging to an - * enclosing 'try' statement. */ - lpad_and_catch_body_stack.pop_back(); - ctx->builder->SetInsertPoint(lpad_bb); + /* --- Landing Pad & Catch/Resume Logic --- */ + /* The scope exit above automatically popped the landing pad stack entry. */ + ctx->builder->SetInsertPoint(landing_pad_block); auto const i32_ty{ ctx->builder->getInt32Ty() }; auto const lpad_ty{ llvm::StructType::get(*llvm_ctx, { ptr_ty, i32_ty }) }; auto const landing_pad{ ctx->builder->CreateLandingPad(lpad_ty, 1) }; if(has_finally) { - /* Mark the landing pad as a "cleanup" landing pad. - * A cleanup landing pad indicates that there is cleanup code (the 'finally' block) - * that MUST be executed regardless of whether the current exception is caught by - * any of the clauses in this landing pad instruction or not. - * - * Effect: When an exception is caught by this landing_pad: - * 1. The personality function is called. - * 2. If the exception type matches any of the 'addClause' types, control might - * go to the catch block. - * 3. CRUCIALLY, because setCleanup(true) is set, even if the exception type does - * NOT match any clause, or after a catch block finishes, the control flow - * is structured to eventually execute the cleanup code associated with this - * unwind path (which we've designed to be the 'finally_bb'). - * - * In essence, 'setCleanup(true)' ensures that the unwinding process will not - * bypass the 'finally' block's execution. After the 'finally' block, the exception - * handling might continue (e.g., by resuming the unwind if the exception wasn't - * fully handled). */ landing_pad->setCleanup(true); } auto exception_ptr{ ctx->builder->CreateExtractValue(landing_pad, 0, "ex.ptr") }; + ctx->builder->CreateStore(exception_ptr, dispatch_ex_slot); + auto const selector{ ctx->builder->CreateExtractValue(landing_pad, 1, "ex.sel") }; + ctx->builder->CreateStore(selector, selector_slot); + native_set registered_rtti{}; if(has_catch) { - /* To make the landing pad catch specific types of exceptions, we need to add clauses. - * Each clause represents a type of exception this landing pad can handle. - * - * We need a reference to the type information for the exception type we want to catch. - * The Itanium C++ ABI exception handling mechanism uses type info globals. - * The exact type of the global doesn't matter as much as its address, which is used - * by the personality function to identify the exception type. - * - * When an exception is thrown, the personality function compares the thrown - * exception's type info with the clauses added to the landing pads in the call stack. - * If a match is found, control is transferred to this landing pad. */ - auto const catch_type{ expr->catch_body.unwrap().type }; - auto const exception_rtti{ Cpp::MangleRTTI(catch_type) }; - - /* macOS requires explicit registration of RTTI symbols. */ - if constexpr(jtl::current_platform == jtl::platform::macos_like) + for(auto const &catch_clause : expr->catch_bodies) { - static native_set rtti_syms; - if(!rtti_syms.contains(exception_rtti)) - { - /* We need to register this RTTI right now, for the JIT. */ - cpp_util::register_rtti(catch_type); - rtti_syms.emplace(exception_rtti); - } - - /* We also need to surface this RTTI upward, to the module level, so it - * can end up in the generated object file. */ - auto const callable{ - Cpp::MakeRTTICallable(catch_type, exception_rtti, __rt_ctx->unique_munged_string()) - }; - global_rtti.emplace(exception_rtti, callable); + register_catch_clause_rtti(catch_clause, landing_pad, registered_rtti); } + } - auto const exception_rtti_global{ llvm_module->getOrInsertGlobal(exception_rtti, - ctx->builder->getPtrTy()) }; - - landing_pad->addClause(exception_rtti_global); - - /* Setup for handling exceptions that might be thrown FROM WITHIN the catch block itself. - * We need to ensure that if an exception occurs inside the 'catch' body, - * any 'finally' block is still executed. This is achieved by having a dedicated - * landing pad ('cleanup_lpad_bb') for the 'catch' body's scope. */ - llvm::BasicBlock *cleanup_lpad_bb{}; - if(has_finally) - { - /* To make any potentially throwing function calls (which will be generated as - * llvm::InvokeInst) within the *catch body* unwind to our 'cleanup_lpad_bb', - * we must push 'cleanup_lpad_bb' onto the 'lpad_and_catch_body_stack'. - * The code generation for 'invoke' uses the top of this stack as the - * unwind destination. */ - cleanup_lpad_bb = llvm::BasicBlock::Create(*llvm_ctx, "cleanup.lpad", current_fn); - lpad_and_catch_body_stack.emplace_back(cleanup_lpad_bb, nullptr); - } - util::scope_exit const pop_cleanup_lpad{ [this, has_finally]() { - if(has_finally) - { - lpad_and_catch_body_stack.pop_back(); - } - } }; + /* === NESTED TRY HANDLING === + * + * When try blocks are nested within each other, each inner try's landing pad must be aware + * of ALL catch clauses from both the current try AND all parent try blocks. This is required + * for correct exception routing during the stack unwinding process. + * + * EXAMPLE SCENARIO: + * (try ; Outer try + * (try ; Inner try + * (throw (std.runtime_error "error")) + * (catch cpp/std.logic_error e ; Inner catch (won't match) + * (println "inner"))) + * (catch cpp/std.runtime_error e ; Outer catch (SHOULD match) + * (println "outer"))) + * + * When the exception is thrown from the inner try: + * 1. Control transfers to the inner try's landing pad + * 2. The landing pad's personality function checks registered catch types + * 3. Inner catch (std.logic_error) doesn't match std.runtime_error + * 4. The personality function finds outer catch (std.runtime_error) IS registered + * 5. Returns the selector for std.runtime_error + * 6. Inner dispatch block sees no local match, routes to parent handler + * 7. Outer try catches the exception + * + * WHY REGISTER PARENT CLAUSES: + * - The personality function (__gxx_personality_v0) needs to know if ANY handler + * in the call stack can handle this exception type + * - If no matching handler is found in the RTTI clauses, the personality function + * will continue unwinding to the next function frame + * - By registering parent clauses, we tell the personality function "don't leave + * this function yet - we have a handler further up" + * + * HANDLER STACK MECHANICS: + * - ctx->exception_handlers is a stack of try blocks in the current function + * - exception_handlers[0] = outermost try + * - exception_handlers[size-1] = current (innermost) try + * - We iterate exception_handlers[0..size-2] to register ALL parent catch clauses + * + * This registration happens even if the current try has NO catch clauses (only finally), + * because the landing pad still needs to know about parent handlers for proper routing. + */ + register_parent_catch_clauses(landing_pad, registered_rtti); + + /* Branch to dispatch block and populate PHIs */ + ctx->builder->CreateBr(dispatch_block); + exception_phi->addIncoming(exception_ptr, landing_pad_block); + selector_phi->addIncoming(selector, landing_pad_block); + + /* --- Dispatch Block --- */ + if(!expr->catch_bodies.empty()) + { + generate_catch_dispatch(expr, + exception_phi, + selector_phi, + dispatch_block, + continue_block, + result_slot, + has_finally ? finally_block : nullptr, + has_finally ? unwind_flag_slot : nullptr, + preserved_ex_slot, + catch_cleanup_block, + arity); + } + + /* --- Catch Cleanup Block --- */ + /* This block is the landing pad for exceptions thrown FROM within a catch block. */ + if(has_catch) + { + ctx->builder->SetInsertPoint(catch_cleanup_block); + auto const catch_lpad{ ctx->builder->CreateLandingPad(lpad_ty, 1) }; + catch_lpad->setCleanup(true); - ctx->builder->CreateBr(catch_body_bb); - current_fn->insert(current_fn->end(), catch_body_bb); - ctx->builder->SetInsertPoint(catch_body_bb); + /* catch_cleanup_block only contains the parent's catch clauses. */ + registered_rtti.clear(); + register_parent_catch_clauses(catch_lpad, registered_rtti); - auto const &[catch_sym, _, catch_body]{ expr->catch_body.unwrap() }; - auto old_locals(locals); - auto const begin_catch_fn{ - llvm_module->getOrInsertFunction("__cxa_begin_catch", ptr_ty, ptr_ty) + /* We must call __cxa_end_catch to release the exception we were catching. */ + auto const end_catch_fn_cleanup{ + llvm_module->getOrInsertFunction("__cxa_end_catch", ctx->builder->getVoidTy()) }; - auto const caught_ptr{ ctx->builder->CreateCall(begin_catch_fn, { exception_ptr }) }; - locals[catch_sym] = ctx->builder->CreateLoad(ptr_ty, caught_ptr, "ex.val"); + ctx->builder->CreateCall(end_catch_fn_cleanup, {}); - auto const original_catch_pos{ catch_body->position }; - catch_body->propagate_position(expression_position::value); - util::scope_exit const restore_catch_pos{ [&]() { - catch_body->propagate_position(original_catch_pos); - } }; - auto catch_val{ gen(catch_body, arity) }; - - auto const end_catch_fn{ llvm_module->getOrInsertFunction("__cxa_end_catch", - ctx->builder->getVoidTy()) }; - ctx->builder->CreateCall(end_catch_fn, {}); - locals = std::move(old_locals); - - if(!ctx->builder->GetInsertBlock()->getTerminator()) - { - if(!catch_val) - { - catch_val = gen_global(jank_nil); - } - ctx->builder->CreateStore(catch_val, result_slot); - if(has_finally) - { - ctx->builder->CreateStore(ctx->builder->getFalse(), unwind_flag_slot); - ctx->builder->CreateBr(finally_bb); - } - else - { - ctx->builder->CreateBr(cont_bb); - } - } + auto const catch_ex_ptr{ ctx->builder->CreateExtractValue(catch_lpad, 0, "catch.ex.ptr") }; + auto const catch_sel{ ctx->builder->CreateExtractValue(catch_lpad, 1, "catch.ex.sel") }; + /* Determine how to route the exception: + * 1. If we have a finally block, go there first (it will resumeunwind after) + * 2. If we have a parent handler, propagate to it + * 3. Otherwise, resume unwinding */ if(has_finally) { - /* This block populates 'cleanup_lpad_bb', which acts as the landing pad - * for any exception thrown *within* the execution of the 'catch' block body. - * Its primary purpose is to ensure the 'finally' block is executed - * even if the catch handler itself throws. */ - ctx->builder->SetInsertPoint(cleanup_lpad_bb); - - /* Create the landing pad instruction for the catch block's cleanup. - * It takes no clauses because it's not trying to "catch" and handle - * the exception in the sense of stopping propagation, but rather to - * perform the necessary cleanups. */ - auto const cleanup_lpad{ ctx->builder->CreateLandingPad(lpad_ty, 0) }; - cleanup_lpad->setCleanup(true); - - /* Extract the pointer to the new exception object that was caught. And store the pointer - * to the exception object that was caught *inside the catch block*. - * This exception object will be needed in 'unwind_action_bb' after the 'finally' - * block runs to resume the stack unwinding process with this new exception. */ - auto cleanup_ex_ptr{ ctx->builder->CreateExtractValue(cleanup_lpad, 0, "cleanup.ex.ptr") }; - ctx->builder->CreateStore(cleanup_ex_ptr, exception_slot); - - /* Set the unwind flag to TRUE. We are inside a landing pad (cleanup_lpad_bb), - * which is only ever entered as a result of an exception being thrown. - * Therefore, we are definitely in an exception unwinding state. This flag - * signals to the code in 'finally_bb' that it should branch to - * 'unwind_action_bb' after completing the 'finally' logic, rather than - * continuing to 'cont_bb' as would happen in a normal execution flow. */ + ctx->builder->CreateStore(catch_ex_ptr, preserved_ex_slot); ctx->builder->CreateStore(ctx->builder->getTrue(), unwind_flag_slot); - ctx->builder->CreateBr(finally_bb); + ctx->builder->CreateStore(catch_sel, selector_slot); + ctx->builder->CreateBr(finally_block); + } + else + { + /* No finally in current try. Check for parent handler. */ + route_unhandled_exception(catch_ex_ptr, catch_sel, catch_cleanup_block); } } else { /* No catch, must have 'finally'. */ - ctx->builder->CreateStore(exception_ptr, exception_slot); + ctx->builder->SetInsertPoint(dispatch_block); + auto const current_ex_ptr = exception_phi; + ctx->builder->CreateStore(current_ex_ptr, preserved_ex_slot); ctx->builder->CreateStore(ctx->builder->getTrue(), unwind_flag_slot); - ctx->builder->CreateBr(finally_bb); + ctx->builder->CreateBr(finally_block); } /* --- Finally block --- */ if(has_finally) { - current_fn->insert(current_fn->end(), finally_bb); - ctx->builder->SetInsertPoint(finally_bb); + current_fn->insert(current_fn->end(), finally_block); + ctx->builder->SetInsertPoint(finally_block); gen(expr->finally_body.unwrap(), arity); if(!ctx->builder->GetInsertBlock()->getTerminator()) { auto unwind_flag = ctx->builder->CreateLoad(ctx->builder->getInt1Ty(), unwind_flag_slot); - ctx->builder->CreateCondBr(unwind_flag, unwind_action_bb, cont_bb); + ctx->builder->CreateCondBr(unwind_flag, unwind_action_block, continue_block); } /* --- Unwind Action block --- - * This block is entered from 'finally_bb' ONLY when the 'finally' block + * This block is entered from 'finally_block' ONLY when the 'finally' block * was executed as part of an exception unwinding process (i.e., unwind_flag_slot was true). * The purpose of this block is to continue the exception propagation after - * the cleanup code in 'finally_bb' has run. The exception object to be - * propagated was saved in 'exception_slot'. */ - current_fn->insert(current_fn->end(), unwind_action_bb); - ctx->builder->SetInsertPoint(unwind_action_bb); - auto current_ex = ctx->builder->CreateLoad(ptr_ty, exception_slot); + * the cleanup code in 'finally_block' has run. The exception object to be + * propagated was saved in 'preserved_ex_slot'. */ + current_fn->insert(current_fn->end(), unwind_action_block); + ctx->builder->SetInsertPoint(unwind_action_block); + auto current_ex = ctx->builder->CreateLoad(ptr_ty, preserved_ex_slot); + /* Load selector for propagation */ + auto current_sel = ctx->builder->CreateLoad(ctx->builder->getInt32Ty(), selector_slot); /* Determine how to propagate the exception: - * - If 'lpad_and_catch_body_stack' is not empty, it means there's an enclosing - * 'try' block within the *same* function. We should "rethrow" the exception - * in a way that it can be caught by the landing pad of that outer 'try' block. - * - If the stack is empty, there are no more exception handlers within this - * function to transfer control to, so we must resume the standard stack unwinding - * process, allowing handlers in caller functions to catch the exception. */ - if(!lpad_and_catch_body_stack.empty()) - { - /* Propagate to an outer landing pad in the same function. - * To ensure the outer catch receives the correct user exception object, - * we first need to extract it using the C++ ABI helper functions. */ - auto exception_ptr_reloaded{ current_ex }; - auto const begin_catch_fn{ - llvm_module->getOrInsertFunction("__cxa_begin_catch", ptr_ty, ptr_ty) - }; - auto const caught_ptr{ ctx->builder->CreateCall(begin_catch_fn, - { exception_ptr_reloaded }) }; - auto const ex_val_ptr{ ctx->builder->CreateLoad(ptr_ty, caught_ptr, "ex.val") }; - auto const end_catch_fn{ llvm_module->getOrInsertFunction("__cxa_end_catch", - ctx->builder->getVoidTy()) }; - ctx->builder->CreateCall(end_catch_fn, {}); - - /* Now, rethrow the *user exception object* (ex_val_ptr) using jank_throw. - * This call is wrapped in CreateInvoke, with the outer try's landing pad - * as the unwind destination. */ - auto const unreachable_dest{ - llvm::BasicBlock::Create(*llvm_ctx, "unreachable.throw", current_fn) - }; - auto const fn_type{ - llvm::FunctionType::get(ctx->builder->getVoidTy(), { ctx->builder->getPtrTy() }, false) - }; - auto function_callee{ llvm_module->getOrInsertFunction("jank_throw", fn_type) }; - llvm::cast(function_callee.getCallee())->setDoesNotReturn(); - - ctx->builder->CreateInvoke(function_callee, - unreachable_dest, - lpad_and_catch_body_stack.back().lpad_bb, - { ex_val_ptr }); - ctx->builder->SetInsertPoint(unreachable_dest); - ctx->builder->CreateUnreachable(); - } - else - { - /* No outer 'try' handlers within this function's scope. We need to resume - * the standard stack unwinding process. This allows the exception to propagate - * up the call stack to potentially be caught by handlers in caller functions. - * The 'llvm.resume' instruction is used for this purpose. - * - * We need to reconstruct the two-element struct { i8*, i32 } that 'llvm.resume' - * expects. This struct is the same type as what a 'landing pad' instruction returns. - * The first element is the exception pointer, and the second is a selector value. */ - auto lpad_val{ - ctx->builder->CreateInsertValue(llvm::UndefValue::get(lpad_ty), current_ex, 0) - }; - lpad_val = ctx->builder->CreateInsertValue(lpad_val, ctx->builder->getInt32(0), 1); - ctx->builder->CreateResume(lpad_val); - } + * - If we have a parent handler (nested try in same function), branch to it. + * - Else, resume unwinding. */ + route_unhandled_exception(current_ex, current_sel, unwind_action_block); } - /* We pushed the landing pad for our `try` block. It has now been popped, before - * generating catch/finally. We must push it back on so that the scope_exit - * guard at the top can correctly pop it later, restoring the stack for the rest - * of this function's codegen. */ - lpad_and_catch_body_stack.emplace_back(lpad_bb, catch_body_bb); - /* --- Continuation block --- */ - current_fn->insert(current_fn->end(), cont_bb); - ctx->builder->SetInsertPoint(cont_bb); + current_fn->insert(current_fn->end(), continue_block); + ctx->builder->SetInsertPoint(continue_block); auto final_val = ctx->builder->CreateLoad(ptr_ty, result_slot); if(is_return) @@ -2434,7 +2682,22 @@ namespace jank::codegen args_array, sret }; - ctx->builder->CreateCall(target_fn, ctor_args); + if(!lpad_and_catch_body_stack.empty()) + { + llvm::BasicBlock *normal_dest + = llvm::BasicBlock::Create(*llvm_ctx, + "invoke.cxx.normal", + ctx->builder->GetInsertBlock()->getParent()); + ctx->builder->CreateInvoke(target_fn, + normal_dest, + lpad_and_catch_body_stack.back().lpad_bb, + ctor_args); + ctx->builder->SetInsertPoint(normal_dest); + } + else + { + ctx->builder->CreateCall(target_fn, ctor_args); + } if(position == expression_position::tail) { diff --git a/compiler+runtime/src/cpp/jank/codegen/processor.cpp b/compiler+runtime/src/cpp/jank/codegen/processor.cpp index 6fba00c93..5b3998d41 100644 --- a/compiler+runtime/src/cpp/jank/codegen/processor.cpp +++ b/compiler+runtime/src/cpp/jank/codegen/processor.cpp @@ -1563,7 +1563,7 @@ namespace jank::codegen analyze::expr::function_arity const &fn_arity, bool const box_needed) { - auto const has_catch{ expr->catch_body.is_some() }; + auto const has_catch{ !expr->catch_bodies.empty() }; auto ret_tmp(runtime::munge(__rt_ctx->unique_namespaced_string("try"))); util::format_to(body_buffer, "object_ref {}{ };", ret_tmp); @@ -1599,8 +1599,8 @@ namespace jank::codegen */ util::format_to(body_buffer, "catch(jank::runtime::object_ref const {}) {", - runtime::munge(expr->catch_body.unwrap().sym->name)); - auto const &catch_tmp(gen(expr->catch_body.unwrap().body, fn_arity, box_needed)); + runtime::munge(expr->catch_bodies[0].sym->name)); + auto const &catch_tmp(gen(expr->catch_bodies[0].body, fn_arity, box_needed)); if(catch_tmp.is_some()) { util::format_to(body_buffer, "{} = {};", ret_tmp, catch_tmp.unwrap().str(box_needed)); @@ -2339,11 +2339,13 @@ namespace jank::codegen /* TODO: Not sure if we want any of this. The module dependency loading feels wrong, * since it should be tied to calls to require instead. */ - jtl::immutable_string processor::module_init_str(jtl::immutable_string const &module) + jtl::immutable_string processor::module_init_str(jtl::immutable_string const &module_name) { jtl::string_builder module_buffer; - util::format_to(module_buffer, "namespace {} {", runtime::module::module_to_native_ns(module)); + util::format_to(module_buffer, + "namespace {} {", + runtime::module::module_to_native_ns(module_name)); util::format_to(module_buffer, R"( @@ -2356,7 +2358,7 @@ namespace jank::codegen util::format_to(module_buffer, "constexpr auto const deps(jank::util::make_array("); bool needs_comma{}; - for(auto const &dep : __rt_ctx->module_dependencies[module]) + for(auto const &dep : __rt_ctx->module_dependencies[module_name]) { if(needs_comma) { diff --git a/compiler+runtime/src/cpp/jank/evaluate.cpp b/compiler+runtime/src/cpp/jank/evaluate.cpp index cfe9c7b9c..b6bd72c3c 100644 --- a/compiler+runtime/src/cpp/jank/evaluate.cpp +++ b/compiler+runtime/src/cpp/jank/evaluate.cpp @@ -74,9 +74,12 @@ namespace jank::evaluate else if constexpr(std::same_as) { walk(expr.body, f); - if(expr.catch_body.is_some()) + if(!expr.catch_bodies.empty()) { - walk(expr.catch_body.unwrap().body, f); + for(auto const &catch_body : expr.catch_bodies) + { + walk(catch_body.body, f); + } } if(expr.finally_body.is_some()) { @@ -685,28 +688,7 @@ namespace jank::evaluate object_ref eval(expr::try_ref const expr) { - util::scope_exit const finally{ [=]() { - if(expr->finally_body) - { - eval(expr->finally_body.unwrap()); - } - } }; - - if(!expr->catch_body) - { - return eval(expr->body); - } - try - { - return eval(expr->body); - } - catch(object_ref const e) - { - return dynamic_call(eval(wrap_expression(expr->catch_body.unwrap().body, - "catch", - { expr->catch_body.unwrap().sym })), - e); - } + return dynamic_call(eval(wrap_expression(expr, "try", {}))); } object_ref eval(expr::case_ref const expr) diff --git a/compiler+runtime/src/jank/clojure/core.jank b/compiler+runtime/src/jank/clojure/core.jank index 9c2cd137a..224762ed8 100644 --- a/compiler+runtime/src/jank/clojure/core.jank +++ b/compiler+runtime/src/jank/clojure/core.jank @@ -4228,7 +4228,7 @@ (if load (try (load lib need-ns? require) - (catch e + (catch cpp/jank.runtime.object_ref e (when undefined-on-entry? (remove-ns lib)) (throw e))) diff --git a/compiler+runtime/src/jank/clojure/test.jank b/compiler+runtime/src/jank/clojure/test.jank index 7ff5b83f9..195818a44 100644 --- a/compiler+runtime/src/jank/clojure/test.jank +++ b/compiler+runtime/src/jank/clojure/test.jank @@ -415,7 +415,7 @@ "Like var-get but returns nil if the var is unbound." [v] (try (var-get v) - (catch _))) + (catch cpp/jank.runtime.object_ref _))) (defn function? "Returns true if argument is a function or a symbol that resolves to @@ -502,7 +502,7 @@ `(try (do ~@body) (do-report {:type :fail :message ~msg :expected '~form :actual nil}) - (catch e# + (catch cpp/jank.runtime.object_ref e# (do-report {:type :pass :message ~msg :expected '~form :actual e#}) e#)))) @@ -516,7 +516,7 @@ body (nthnext form 2)] `(try (do ~@body) (do-report {:type :fail :message ~msg :expected '~form :actual nil}) - (catch e# + (catch cpp/jank.runtime.object_ref e# (let [m# (ex-message e#)] (if (and (string? m#) (re-find ~re m#)) (do-report {:type :pass :message ~msg @@ -531,7 +531,7 @@ You don't call this." [msg form] `(try (do ~(assert-expr msg form)) - (catch t# + (catch cpp/jank.runtime.object_ref t# (do-report {:type :error :message ~msg :expected '~form :actual t#})))) @@ -715,7 +715,7 @@ (do-report {:type :begin-test-var :var v}) (inc-report-counter :test) (try (t) - (catch e + (catch cpp/jank.runtime.object_ref e (do-report {:type :error :message "Uncaught exception, not in assertion." :expected nil :actual e}))) (do-report {:type :end-test-var :var v})))) diff --git a/compiler+runtime/test/jank/cpp/try/pass-local-reference-to-cpp-value.jank b/compiler+runtime/test/jank/cpp/try/pass-local-reference-to-cpp-value.jank deleted file mode 100644 index f6fa7e92f..000000000 --- a/compiler+runtime/test/jank/cpp/try/pass-local-reference-to-cpp-value.jank +++ /dev/null @@ -1,4 +0,0 @@ -(let [bar cpp/true] - (try - (if bar - :success))) diff --git a/compiler+runtime/test/jank/form/try/fail-catch-after-finally.jank b/compiler+runtime/test/jank/form/try/fail-catch-after-finally.jank index 14532af20..5a533ee87 100644 --- a/compiler+runtime/test/jank/form/try/fail-catch-after-finally.jank +++ b/compiler+runtime/test/jank/form/try/fail-catch-after-finally.jank @@ -1,5 +1,3 @@ (try - (finally - ) - (catch _ - )) + (finally) + (catch cpp/jank.runtime.object_ref _)) diff --git a/compiler+runtime/test/jank/form/try/fail-duplicate-catch-type.jank b/compiler+runtime/test/jank/form/try/fail-duplicate-catch-type.jank new file mode 100644 index 000000000..00e127bba --- /dev/null +++ b/compiler+runtime/test/jank/form/try/fail-duplicate-catch-type.jank @@ -0,0 +1,7 @@ +;; Test that duplicate catch types are rejected at compile time +(try + (throw :test) + (catch cpp/jank.runtime.object_ref e1 + :first) + (catch cpp/jank.runtime.object_ref e2 + :second)) diff --git a/compiler+runtime/test/jank/form/try/fail-multiple-catch.jank b/compiler+runtime/test/jank/form/try/fail-multiple-catch.jank deleted file mode 100644 index 8681f12de..000000000 --- a/compiler+runtime/test/jank/form/try/fail-multiple-catch.jank +++ /dev/null @@ -1,5 +0,0 @@ -(try - (catch a - ) - (catch b - )) diff --git a/compiler+runtime/test/jank/form/try/fail-multiple-finally.jank b/compiler+runtime/test/jank/form/try/fail-multiple-finally.jank index 2c7bcad42..cbbe950ab 100644 --- a/compiler+runtime/test/jank/form/try/fail-multiple-finally.jank +++ b/compiler+runtime/test/jank/form/try/fail-multiple-finally.jank @@ -1,5 +1,3 @@ (try - (finally - ) - (finally - )) + (finally) + (finally)) diff --git a/compiler+runtime/test/jank/form/try/fail-other-form-after-finally.jank b/compiler+runtime/test/jank/form/try/fail-other-form-after-finally.jank index a2521023f..51b805a02 100644 --- a/compiler+runtime/test/jank/form/try/fail-other-form-after-finally.jank +++ b/compiler+runtime/test/jank/form/try/fail-other-form-after-finally.jank @@ -1,7 +1,5 @@ (try (+ 1 2) - (catch e - ) - (finally - ) + (catch cpp/jank.runtime.object_ref e) + (finally) :success) diff --git a/compiler+runtime/test/jank/form/try/fail-recur-in-catch.jank b/compiler+runtime/test/jank/form/try/fail-recur-in-catch.jank index 03c11b560..4ca22de57 100644 --- a/compiler+runtime/test/jank/form/try/fail-recur-in-catch.jank +++ b/compiler+runtime/test/jank/form/try/fail-recur-in-catch.jank @@ -1,5 +1,5 @@ (fn* [] (try :success - (catch _ + (catch cpp/jank.runtime.object_ref _ (recur)))) diff --git a/compiler+runtime/test/jank/form/try/fail-recur-in-finally.jank b/compiler+runtime/test/jank/form/try/fail-recur-in-finally.jank index 7138a4470..005010c48 100644 --- a/compiler+runtime/test/jank/form/try/fail-recur-in-finally.jank +++ b/compiler+runtime/test/jank/form/try/fail-recur-in-finally.jank @@ -1,7 +1,6 @@ (fn* [] (try :success - (catch _ - ) + (catch cpp/jank.runtime.object_ref _) (finally (recur)))) diff --git a/compiler+runtime/test/jank/form/try/fail-recur-in-try.jank b/compiler+runtime/test/jank/form/try/fail-recur-in-try.jank index 4d85496b0..d7e622b35 100644 --- a/compiler+runtime/test/jank/form/try/fail-recur-in-try.jank +++ b/compiler+runtime/test/jank/form/try/fail-recur-in-try.jank @@ -1,5 +1,4 @@ (fn* [] (try (recur) - (catch _ - ))) + (catch cpp/jank.runtime.object_ref _))) diff --git a/compiler+runtime/test/jank/form/try/pass-capture-within-let.jank b/compiler+runtime/test/jank/form/try/pass-capture-within-let.jank index d382fec06..97653c040 100644 --- a/compiler+runtime/test/jank/form/try/pass-capture-within-let.jank +++ b/compiler+runtime/test/jank/form/try/pass-capture-within-let.jank @@ -2,5 +2,5 @@ (try (let* [y x] y) - (catch _ - ))) + (catch cpp/jank.runtime.object_ref e + e))) diff --git a/compiler+runtime/test/jank/form/try/pass-catch-cpp-behavior.jank b/compiler+runtime/test/jank/form/try/pass-catch-cpp-behavior.jank new file mode 100644 index 000000000..18ab2be9f --- /dev/null +++ b/compiler+runtime/test/jank/form/try/pass-catch-cpp-behavior.jank @@ -0,0 +1,44 @@ +;; Tests for complex C++ exception behavior (rethrow, uncaught) + +(cpp/raw "namespace pass_catch_cpp_behavior + { + void throw_runtime_error() { throw std::runtime_error(\"error\"); } + void throw_current() { throw; } + }") + +;; 1. Rethrowing C++ exception +(let [a (atom []) + msg (atom nil)] + (try + (try + (cpp/pass_catch_cpp_behavior.throw_runtime_error) + (catch cpp/std.runtime_error e + (swap! a conj :caught-inner) + (cpp/pass_catch_cpp_behavior.throw_current))) + ;; Rethrow using helper function + (catch cpp/std.runtime_error e + (swap! a conj :caught-outer) + (reset! msg (cpp/.what e)))) + + (assert (= [:caught-inner :caught-outer] @a) (pr-str @a)) + (assert (= "error" @msg) "Should preserve exception message on rethrow")) + +;; 2. Uncaught C++ exception with finally +(let [a (atom []) + msg (atom nil)] + (try + (try + (cpp/pass_catch_cpp_behavior.throw_runtime_error) + ;; No matching catch for runtime_error (inner catch is for logic_error) + (catch cpp/std.logic_error e + (swap! a conj :wrong-catch)) + (finally + (swap! a conj :finally-ran))) + (catch cpp/std.runtime_error e + (swap! a conj :caught-outer) + (reset! msg (cpp/.what e)))) + + (assert (= [:finally-ran :caught-outer] @a) (pr-str @a)) + (assert (= "error" @msg) "Should catch exception with correct message")) + +:success diff --git a/compiler+runtime/test/jank/form/try/pass-catch-cpp-multiple-clauses.jank b/compiler+runtime/test/jank/form/try/pass-catch-cpp-multiple-clauses.jank new file mode 100644 index 000000000..97140fcfe --- /dev/null +++ b/compiler+runtime/test/jank/form/try/pass-catch-cpp-multiple-clauses.jank @@ -0,0 +1,42 @@ +;; Test multiple catch with C++ exceptions and finally blocks +(cpp/raw "namespace pass_multiple_catch_cpp_with_finally + { + void throw_cpp_double() { throw 5.5; } + void throw_uncaught_type() { throw 'x'; } + }") + +(let [cleanup (atom false)] + (assert + (= 5.5 + (try + (cpp/pass_multiple_catch_cpp_with_finally.throw_cpp_double) + (catch cpp/int i -1) + (catch cpp/double d d) + (catch cpp/std.runtime_error e -1.0) + (finally + (reset! cleanup true))))) + (assert (true? @cleanup) "Finally should execute")) + +;; Test finally executes even when exception is not caught +(let [cleanup (atom false) + caught (atom false)] + (try + (try + (cpp/pass_multiple_catch_cpp_with_finally.throw_uncaught_type) + (catch cpp/int i + (reset! caught true)) + (catch cpp/double d + (reset! caught true)) + (finally + (reset! cleanup true))) + (catch cpp/char c + c)) + (assert (true? @cleanup) "Finally should execute before unwinding") + (assert (false? @caught) "No inner catch should match") + (assert + (= \x + (try (cpp/pass_multiple_catch_cpp_with_finally.throw_uncaught_type) + (catch cpp/char c c))) + "Should catch char 'x'")) + +:success diff --git a/compiler+runtime/test/jank/form/try/pass-catch-cpp-no-slicing.jank b/compiler+runtime/test/jank/form/try/pass-catch-cpp-no-slicing.jank new file mode 100644 index 000000000..0eb050139 --- /dev/null +++ b/compiler+runtime/test/jank/form/try/pass-catch-cpp-no-slicing.jank @@ -0,0 +1,36 @@ +;; Test to verify that object slicing does not occur when catching by base type +;; See ERR61-CPP: Catch exceptions by lvalue reference + +(cpp/raw " +#include + +namespace pass_catch_no_slicing +{ + struct Base + { + virtual std::string name() const { return \"Base\"; } + virtual ~Base() = default; + }; + + struct Derived : Base + { + std::string name() const override { return \"Derived\"; } + }; + + void throw_derived() + { + throw Derived(); + } +}") + +(let [result (atom nil)] + (try + (cpp/pass_catch_no_slicing.throw_derived) + (catch cpp/pass_catch_no_slicing.Base e + ;; If slicing occurred, e would be a Base object and name() would return "Base" + ;; Since we catch by reference (implicitly), it should remain Derived + (reset! result (cpp/.name e)))) + + (assert (= "Derived" @result) "Object slicing occurred! Expected 'Derived' but got 'Base'")) + +:success diff --git a/compiler+runtime/test/jank/form/try/pass-catch-cpp-primitives.jank b/compiler+runtime/test/jank/form/try/pass-catch-cpp-primitives.jank new file mode 100644 index 000000000..18a2ad6bc --- /dev/null +++ b/compiler+runtime/test/jank/form/try/pass-catch-cpp-primitives.jank @@ -0,0 +1,60 @@ +;; Test catching various C++ primitive types +(cpp/raw "using my_uint = unsigned int;") +(cpp/raw "using void_ptr = void*;") +(cpp/raw "using ulong = unsigned long;") +(cpp/raw "namespace pass_catch_cpp_primitives + { + using unsigned_int = unsigned int; + unsigned long to_ulong(void* p) { return reinterpret_cast(p); } + void throw_char() { throw 'c'; } + void throw_double() { throw 1.23; } + void throw_float() { throw 0.5f; } + void throw_int() { throw 123; } + void throw_unsigned_int() { throw 123u; } + void throw_pointer() { throw (void*)0x1234; } + }") + +;; 1. char +(assert + (= \c + (try + (cpp/pass_catch_cpp_primitives.throw_char) + (catch cpp/char c c)))) + +;; 2. double +(assert + (= 1.23 + (try + (cpp/pass_catch_cpp_primitives.throw_double) + (catch cpp/double d d)))) + +;; 3. float +(assert + (= 0.5 + (try + (cpp/pass_catch_cpp_primitives.throw_float) + (catch cpp/float f f)))) + +;; 4. int +(assert + (= 123 + (try + (cpp/pass_catch_cpp_primitives.throw_int) + (catch cpp/int i i)))) + +;; 5. unsigned int +(assert + (= 123 + (try + (cpp/pass_catch_cpp_primitives.throw_unsigned_int) + (catch cpp/my_uint u u)))) + +;; 6. pointer +(assert + (= 0x1234 + (try + (cpp/pass_catch_cpp_primitives.throw_pointer) + (catch cpp/void_ptr p + (cpp/pass_catch_cpp_primitives.to_ulong p))))) + +:success diff --git a/compiler+runtime/test/jank/form/try/pass-catch-cpp-std-bad-cast.jank b/compiler+runtime/test/jank/form/try/pass-catch-cpp-std-bad-cast.jank new file mode 100644 index 000000000..749f3e73a --- /dev/null +++ b/compiler+runtime/test/jank/form/try/pass-catch-cpp-std-bad-cast.jank @@ -0,0 +1,21 @@ +;; Test catching std::bad_cast +(cpp/raw " +#include +namespace catch_cpp_bad_cast +{ + void throw_bad_cast() + { + throw std::bad_cast(); + } +}") + +(let [caught (atom nil)] + (try + (cpp/catch_cpp_bad_cast.throw_bad_cast) + (catch cpp/std.runtime_error e) + (catch cpp/std.bad_cast e + (reset! caught (cpp/.what e)))) + (assert (not (nil? @caught)) "Should have caught std::bad_cast") + (assert (= "std::bad_cast" @caught) "Error message should be 'std::bad_cast'")) + +:success diff --git a/compiler+runtime/test/jank/form/try/pass-catch-cpp-std-exception.jank b/compiler+runtime/test/jank/form/try/pass-catch-cpp-std-exception.jank new file mode 100644 index 000000000..778d6d104 --- /dev/null +++ b/compiler+runtime/test/jank/form/try/pass-catch-cpp-std-exception.jank @@ -0,0 +1,11 @@ +;; Test catching std::exception base class (with implicit reference promotion) +(cpp/raw "namespace pass_catch_cpp_std_exception + { + void throw_runtime_error_as_exception() { throw std::runtime_error(\"derived error\"); } + }") + +(try + (cpp/pass_catch_cpp_std_exception.throw_runtime_error_as_exception) + (catch cpp/std.exception e + (assert (= "derived error" (cpp/.what e))) + :success)) diff --git a/compiler+runtime/test/jank/form/try/pass-catch-cpp-std-runtime-error.jank b/compiler+runtime/test/jank/form/try/pass-catch-cpp-std-runtime-error.jank new file mode 100644 index 000000000..d988ddbb5 --- /dev/null +++ b/compiler+runtime/test/jank/form/try/pass-catch-cpp-std-runtime-error.jank @@ -0,0 +1,15 @@ +;; Test catching std::runtime_error +(cpp/raw "namespace pass_catch_cpp_std_runtime_error + { + void throw_runtime_error() { throw std::runtime_error(\"runtime error\"); } + }") + +(let [caught (atom nil)] + (try + (cpp/pass_catch_cpp_std_runtime_error.throw_runtime_error) + (catch cpp/std.logic_error e) + (catch cpp/std.runtime_error e + (reset! caught (cpp/.what e)))) + (assert (= "runtime error" @caught) "Should have caught std::runtime_error with correct message")) + +:success diff --git a/compiler+runtime/test/jank/form/try/pass-catch-mixed-types.jank b/compiler+runtime/test/jank/form/try/pass-catch-mixed-types.jank new file mode 100644 index 000000000..15de2580c --- /dev/null +++ b/compiler+runtime/test/jank/form/try/pass-catch-mixed-types.jank @@ -0,0 +1,39 @@ +;; Test mixing jank and C++ exception types in multiple catch +(cpp/raw "namespace pass_multiple_catch_mixed_types + { + void throw_cpp_int() { throw 123; } + void throw_runtime_error() { throw std::runtime_error(\"mixed\"); } + }") + +;; Test jank exception followed by C++ catches +(assert + (= :jank-caught + (try + (throw :jank-exception) + (catch cpp/int i + :cpp-int-caught) + (catch cpp/jank.runtime.object_ref e + :jank-caught)))) + +;; Test C++ exception followed by jank catch +(assert + (= 123 + (try + (cpp/pass_multiple_catch_mixed_types.throw_cpp_int) + (catch cpp/int i + i) + (catch cpp/jank.runtime.object_ref e + :jank-caught)))) + +;; Test that C++ std exception is caught correctly (not by jank catch) +(let [msg (atom nil)] + (try + (cpp/pass_multiple_catch_mixed_types.throw_runtime_error) + (catch cpp/std.runtime_error ex + (reset! msg (cpp/.what ex))) + (catch cpp/jank.runtime.object_ref e + (assert false "Should not reach jank catch for C++ exception"))) + (assert (not (nil? @msg)) "Should have caught C++ exception") + (assert (= "mixed" @msg) "Error message should be 'mixed'")) + +:success diff --git a/compiler+runtime/test/jank/form/try/pass-catch-returns-exception.jank b/compiler+runtime/test/jank/form/try/pass-catch-returns-exception.jank deleted file mode 100644 index 952d86862..000000000 --- a/compiler+runtime/test/jank/form/try/pass-catch-returns-exception.jank +++ /dev/null @@ -1,4 +0,0 @@ -(try - (throw :success) - (catch e - e)) diff --git a/compiler+runtime/test/jank/form/try/pass-closure-capture.jank b/compiler+runtime/test/jank/form/try/pass-closure-capture.jank new file mode 100644 index 000000000..09d4f5f60 --- /dev/null +++ b/compiler+runtime/test/jank/form/try/pass-closure-capture.jank @@ -0,0 +1,31 @@ +;; Test closure capture in try-catch blocks +;; Verifies that try-catch correctly captures variables from parent scopes + +;; Test: Closure over both function parameter and local variable +((fn* [a] + (let [b 1] + (assert + (= true + (try + (throw (= a b)) + (catch cpp/jank.runtime.object_ref r + (assert (= 1 b) "Should capture local variable b") + r))) + "Should capture both parameter a and local b correctly"))) + 1) + +;; Test: Closure over function parameter in catch block +(assert + (= :success + ((fn* [fun] + (try + (fun true) + (catch cpp/jank.runtime.object_ref _ + (fun false)))) + (fn* [throw?] + (if throw? + (throw :success) + :success)))) + "Should call function parameter correctly in catch block") + +:success diff --git a/compiler+runtime/test/jank/form/try/pass-closure-from-parent-parameter-and-local.jank b/compiler+runtime/test/jank/form/try/pass-closure-from-parent-parameter-and-local.jank deleted file mode 100644 index 860a12772..000000000 --- a/compiler+runtime/test/jank/form/try/pass-closure-from-parent-parameter-and-local.jank +++ /dev/null @@ -1,8 +0,0 @@ -((fn* [a] - (let [b 1] - (try - (throw (= a b)) - (catch r - (when (and r (= 1 b)) - :success))))) - 1) diff --git a/compiler+runtime/test/jank/form/try/pass-closure-from-parent-parameter.jank b/compiler+runtime/test/jank/form/try/pass-closure-from-parent-parameter.jank deleted file mode 100644 index 159cb951e..000000000 --- a/compiler+runtime/test/jank/form/try/pass-closure-from-parent-parameter.jank +++ /dev/null @@ -1,12 +0,0 @@ -((fn* [fun] - (try - (fun true) - (catch _ - (fun false)) - (finally - ))) - - (fn* [throw?] - (if throw? - (throw :success) - :success))) diff --git a/compiler+runtime/test/jank/form/try/pass-closure.jank b/compiler+runtime/test/jank/form/try/pass-closure.jank deleted file mode 100644 index 26b766ade..000000000 --- a/compiler+runtime/test/jank/form/try/pass-closure.jank +++ /dev/null @@ -1,8 +0,0 @@ -(let* [a 1] - (try - (throw a) - (catch e - (when (= a e) - :success)) - (finally - (assert (= 1 a))))) diff --git a/compiler+runtime/test/jank/form/try/pass-expression-no-throw.jank b/compiler+runtime/test/jank/form/try/pass-expression-no-throw.jank deleted file mode 100644 index 57fda7e9e..000000000 --- a/compiler+runtime/test/jank/form/try/pass-expression-no-throw.jank +++ /dev/null @@ -1,6 +0,0 @@ -(assert (= (try - :success - (catch _)) - :success)) - -:success diff --git a/compiler+runtime/test/jank/form/try/pass-expression-with-throw.jank b/compiler+runtime/test/jank/form/try/pass-expression-with-throw.jank deleted file mode 100644 index 8748f5816..000000000 --- a/compiler+runtime/test/jank/form/try/pass-expression-with-throw.jank +++ /dev/null @@ -1,6 +0,0 @@ -(assert (= (try - (throw :success) - (catch e - e)))) - -:success diff --git a/compiler+runtime/test/jank/form/try/pass-finally-behavior.jank b/compiler+runtime/test/jank/form/try/pass-finally-behavior.jank new file mode 100644 index 000000000..51728d498 --- /dev/null +++ b/compiler+runtime/test/jank/form/try/pass-finally-behavior.jank @@ -0,0 +1,44 @@ +;; Tests for finally block behavior + +;; 1. Finally executes but does not affect return value (no exception) +(let [a (atom [])] + (assert + (= [:try] + (try + (swap! a conj :try) + (finally + (swap! a conj :finally))))) + (assert (= [:try :finally] @a) (pr-str @a))) + +;; 2. Finally executes but does not affect return value (with exception) +(let [a (atom [])] + (try + (try + (swap! a conj :inner-try) + (throw "skip finally") + (swap! a conj :inner-try-after-throw) + (finally + (swap! a conj :inner-finally))) + (catch cpp/jank.runtime.object_ref e + (swap! a conj [:outer-catch e]))) + (assert (= [:inner-try :inner-finally [:outer-catch "skip finally"]] @a) + (pr-str @a))) + +;; 3. Finally value is discarded (no exception) +(assert + (= + (try 1 + (catch cpp/jank.runtime.object_ref _ 2) + (finally 3)) + 1)) + +;; 4. Finally value is discarded (with exception) +(assert + (= + (try + (throw :anything) + (catch cpp/jank.runtime.object_ref _ 2) + (finally 3)) + 2)) + +:success diff --git a/compiler+runtime/test/jank/form/try/pass-finally-no-catch.jank b/compiler+runtime/test/jank/form/try/pass-finally-no-catch.jank deleted file mode 100644 index b3a76969f..000000000 --- a/compiler+runtime/test/jank/form/try/pass-finally-no-catch.jank +++ /dev/null @@ -1,21 +0,0 @@ -(let [a (atom [])] - (assert (= [:try] - (try - (swap! a conj :try) - (finally - (swap! a conj :finally))))) - (assert (= [:try :finally] @a) (pr-str @a))) - -(let [a (atom [])] - (try (try - (swap! a conj :inner-try) - (throw "skip finally") - (swap! a conj :inner-try-after-throw) - (finally - (swap! a conj :inner-finally))) - (catch e - (swap! a conj [:outer-catch e]))) - (assert (= [:inner-try :inner-finally [:outer-catch "skip finally"]] @a) - (pr-str @a))) - -:success diff --git a/compiler+runtime/test/jank/form/try/pass-nested-fn-finally.jank b/compiler+runtime/test/jank/form/try/pass-nested-fn-finally.jank new file mode 100644 index 000000000..85fe571f8 --- /dev/null +++ b/compiler+runtime/test/jank/form/try/pass-nested-fn-finally.jank @@ -0,0 +1,15 @@ +(let* [steps (atom []) + b (fn* b [] + (try + (throw :bthrow) + (catch cpp/jank.runtime.object_ref e (swap! steps conj :b-catch) (throw e)) + (finally (swap! steps conj :b-finally)))) + a (fn* a [] + (try + (b) + (catch cpp/jank.runtime.object_ref e (swap! steps conj :a-catch)) + (finally (swap! steps conj :a-finally))))] + (a) + (if (= [:b-catch :b-finally :a-catch :a-finally] @steps) + :success + (do (println "Failed:" @steps) :failure))) diff --git a/compiler+runtime/test/jank/form/try/pass-nested-no-catch.jank b/compiler+runtime/test/jank/form/try/pass-nested-no-catch.jank index ffaac5997..623741368 100644 --- a/compiler+runtime/test/jank/form/try/pass-nested-no-catch.jank +++ b/compiler+runtime/test/jank/form/try/pass-nested-no-catch.jank @@ -6,7 +6,7 @@ a (fn* a [] (try (b) - (catch e (swap! steps conj :a-catch))))] + (catch cpp/jank.runtime.object_ref e (swap! steps conj :a-catch))))] (a) (assert (= [:b-finally :a-catch] @steps) (pr-str @steps))) @@ -15,7 +15,7 @@ b (fn* b [] (try (throw :bthrow) - (catch e (swap! steps conj :b-catch)))) + (catch cpp/jank.runtime.object_ref e (swap! steps conj :b-catch)))) a (fn* a [] (try (b) @@ -32,7 +32,7 @@ a (fn* a [] (try (b) - (catch e (swap! steps conj :a-catch)) + (catch cpp/jank.runtime.object_ref e (swap! steps conj :a-catch)) (finally (swap! steps conj :a-finally))))] (a) (assert (= [:b-finally :a-catch :a-finally] @steps) @@ -42,11 +42,11 @@ b (fn* b [] (try (throw :bthrow) - (catch e (swap! steps conj :b-catch) (throw e)))) + (catch cpp/jank.runtime.object_ref e (swap! steps conj :b-catch) (throw e)))) a (fn* a [] (try (b) - (catch e (swap! steps conj :a-catch))))] + (catch cpp/jank.runtime.object_ref e (swap! steps conj :a-catch))))] (a) (assert (= [:b-catch :a-catch] @steps) (pr-str @steps))) @@ -55,12 +55,12 @@ b (fn* b [] (try (throw :bthrow) - (catch e (swap! steps conj :b-catch) (throw e)) + (catch cpp/jank.runtime.object_ref e (swap! steps conj :b-catch) (throw e)) (finally (swap! steps conj :b-finally)))) a (fn* a [] (try (b) - (catch e (swap! steps conj :a-catch)) + (catch cpp/jank.runtime.object_ref e (swap! steps conj :a-catch)) (finally (swap! steps conj :a-finally))))] (a) (assert (= [:b-catch :b-finally :a-catch :a-finally] @steps) diff --git a/compiler+runtime/test/jank/form/try/pass-nested-try-catch.jank b/compiler+runtime/test/jank/form/try/pass-nested-try-catch.jank new file mode 100644 index 000000000..1c724a28b --- /dev/null +++ b/compiler+runtime/test/jank/form/try/pass-nested-try-catch.jank @@ -0,0 +1,25 @@ +;; Test nested try/catch scenarios + +;; Test 1: Inner catch handles exception completely +(assert + (= :inner-caught + (try + (try + (throw :inner-exception) + (catch cpp/jank.runtime.object_ref e1 + :inner-caught)) + (catch cpp/jank.runtime.object_ref outer-e + :outer-caught)))) + +;; Test 2: Inner catch rethrows to outer catch +(assert + (= :success + (try + (try + (throw :success) + (catch cpp/jank.runtime.object_ref e1 + (throw e1))) + (catch cpp/jank.runtime.object_ref e2 + (if (= e2 :success) :success))))) + +:success diff --git a/compiler+runtime/test/jank/form/try/pass-nested.jank b/compiler+runtime/test/jank/form/try/pass-nested.jank deleted file mode 100644 index 6b09b61c1..000000000 --- a/compiler+runtime/test/jank/form/try/pass-nested.jank +++ /dev/null @@ -1,7 +0,0 @@ -(try - (try - (throw :success) - (catch e1 - (throw e1))) - (catch e2 - e2)) diff --git a/compiler+runtime/test/jank/form/try/pass-no-catch-no-finally.jank b/compiler+runtime/test/jank/form/try/pass-no-catch-no-finally.jank deleted file mode 100644 index fedea2248..000000000 --- a/compiler+runtime/test/jank/form/try/pass-no-catch-no-finally.jank +++ /dev/null @@ -1,3 +0,0 @@ -(let* [v (try (+ 1 2))] - (if (= v 3) - :success)) diff --git a/compiler+runtime/test/jank/form/try/pass-no-throw-with-catch-and-finally.jank b/compiler+runtime/test/jank/form/try/pass-no-throw-with-catch-and-finally.jank deleted file mode 100644 index 2d7c4114e..000000000 --- a/compiler+runtime/test/jank/form/try/pass-no-throw-with-catch-and-finally.jank +++ /dev/null @@ -1,6 +0,0 @@ -(try - :success - (catch _ - ) - (finally - )) diff --git a/compiler+runtime/test/jank/form/try/pass-no-throw-with-catch.jank b/compiler+runtime/test/jank/form/try/pass-no-throw-with-catch.jank deleted file mode 100644 index 2744628ae..000000000 --- a/compiler+runtime/test/jank/form/try/pass-no-throw-with-catch.jank +++ /dev/null @@ -1,3 +0,0 @@ -(try - :success - (catch _)) diff --git a/compiler+runtime/test/jank/form/try/pass-no-value-from-finally.jank b/compiler+runtime/test/jank/form/try/pass-no-value-from-finally.jank deleted file mode 100644 index 3429cb4f6..000000000 --- a/compiler+runtime/test/jank/form/try/pass-no-value-from-finally.jank +++ /dev/null @@ -1,17 +0,0 @@ -(assert (= (try - 1 - (catch _ - 2) - (finally - 3)) - 1)) - -(assert (= (try - (throw :anything) - (catch _ - 2) - (finally - 3)) - 2)) - -:success diff --git a/compiler+runtime/test/jank/form/try/pass-try-basic.jank b/compiler+runtime/test/jank/form/try/pass-try-basic.jank new file mode 100644 index 000000000..e8d2c1696 --- /dev/null +++ b/compiler+runtime/test/jank/form/try/pass-try-basic.jank @@ -0,0 +1,29 @@ +;; Basic try/catch form logic tests + +;; 1. Try expression with no throw returns body value +(assert + (= + (try :success + (catch cpp/jank.runtime.object_ref _)) + :success)) + +;; 2. Try expression with throw returns caught value +(assert + (= + (try + (throw :success) + (catch cpp/jank.runtime.object_ref e e)) + :success)) + +;; 3. Try with no catch/finally +(let* [v (try (+ 1 2))] + (assert (= v 3))) + +;; 4. Local reference to C++ value in try block +(let [bar cpp/true] + (assert + (= :success + (try + (if bar :success))))) + +:success diff --git a/compiler+runtime/test/jank/form/try/pass-try-catch-finally-execution.jank b/compiler+runtime/test/jank/form/try/pass-try-catch-finally-execution.jank new file mode 100644 index 000000000..cbd1494ed --- /dev/null +++ b/compiler+runtime/test/jank/form/try/pass-try-catch-finally-execution.jank @@ -0,0 +1,36 @@ +;; Comprehensive tests for try-catch-finally execution order + +;; Test 1: Basic try-catch-finally order +(let [execution-order (atom [])] + (try + (swap! execution-order conj :try) + (throw :exception) + (catch cpp/jank.runtime.object_ref e1 + (swap! execution-order conj :catch)) + (finally + (swap! execution-order conj :finally))) + (assert (= [:try :catch :finally] @execution-order) "Basic execution order failed")) + +;; Test 2: Finally executes after catch returns value +(let [cleanup (atom false)] + (assert + (= :caught + (try + (throw "error") + (catch cpp/jank.runtime.object_ref e :caught) + (finally + (reset! cleanup true))))) + (assert (true? @cleanup) "Finally block did not execute")) + +;; Test 3: Nested try with finally - inner finally executes before outer catch +(let* [finally-executed (atom false) + caught-exception (try + (try + (throw "inner-throw") + (finally (reset! finally-executed true))) + (catch cpp/jank.runtime.object_ref e e))] + (assert @finally-executed "Inner finally should have executed before outer catch") + (assert (= "inner-throw" caught-exception) + (str "Expected 'inner-throw' but got: " caught-exception))) + +:success diff --git a/compiler+runtime/test/jank/form/try/pass-unboxed-position.jank b/compiler+runtime/test/jank/form/try/pass-unboxed-position.jank index df43ecd16..185224d80 100644 --- a/compiler+runtime/test/jank/form/try/pass-unboxed-position.jank +++ b/compiler+runtime/test/jank/form/try/pass-unboxed-position.jank @@ -1,10 +1,15 @@ -; A box will be required. -(def a (fn* [] - (let* [a 5 - b (try - 7 - (catch _ - ))] - []))) +;; Regression test: Verify that unboxed values from try expressions +;; are correctly boxed when assigned to bindings and used in boxed contexts +(def a + (fn* [] + (let* [a 5 + b (try 7 + (catch cpp/jank.runtime.object_ref _))] + (assert (= 7 b) "Try should return unboxed value 7") + (assert (= [7] [b]) "Boxed value should work correctly in vector") + []))) + +;; Execute the test function +(a) :success