diff --git a/include/chaiscript/dispatchkit/operators.hpp b/include/chaiscript/dispatchkit/operators.hpp index 2545bfce..d32ee9d6 100644 --- a/include/chaiscript/dispatchkit/operators.hpp +++ b/include/chaiscript/dispatchkit/operators.hpp @@ -15,168 +15,168 @@ #include "register_function.hpp" namespace chaiscript::bootstrap::operators { - template - void assign(Module &m) { + template + void assign(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs = rhs; }), "="); } - template - void assign_bitwise_and(Module &m) { + template + void assign_bitwise_and(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs &= rhs; }), "&="); } - template - void assign_xor(Module &m) { + template + void assign_xor(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs ^= rhs; }), "^="); } - template - void assign_bitwise_or(Module &m) { + template + void assign_bitwise_or(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs |= rhs; }), "|="); } - template - void assign_difference(Module &m) { + template + void assign_difference(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs -= rhs; }), "-="); } - template - void assign_left_shift(Module &m) { + template + void assign_left_shift(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs <<= rhs; }), "<<="); } - template - void assign_product(Module &m) { + template + void assign_product(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs <<= rhs; }), "*="); } - template - void assign_quotient(Module &m) { + template + void assign_quotient(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs /= rhs; }), "/="); } - template - void assign_remainder(Module &m) { + template + void assign_remainder(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs %= rhs; }), "%="); } - template - void assign_right_shift(Module &m) { + template + void assign_right_shift(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs >>= rhs; }), ">>="); } - template - void assign_sum(Module &m) { + template + void assign_sum(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs += rhs; }), "+="); } - template - void prefix_decrement(Module &m) { + template + void prefix_decrement(ModuleType &m) { m.add(chaiscript::fun([](T &lhs) -> T & { return --lhs; }), "--"); } - template - void prefix_increment(Module &m) { + template + void prefix_increment(ModuleType &m) { m.add(chaiscript::fun([](T &lhs) -> T & { return ++lhs; }), "++"); } - template - void equal(Module &m) { + template + void equal(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs == rhs; }), "=="); } - template - void greater_than(Module &m) { + template + void greater_than(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs > rhs; }), ">"); } - template - void greater_than_equal(Module &m) { + template + void greater_than_equal(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs >= rhs; }), ">="); } - template - void less_than(Module &m) { + template + void less_than(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs < rhs; }), "<"); } - template - void less_than_equal(Module &m) { + template + void less_than_equal(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs <= rhs; }), "<="); } - template - void logical_compliment(Module &m) { + template + void logical_compliment(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs) { return !lhs; }), "!"); } - template - void not_equal(Module &m) { + template + void not_equal(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs != rhs; }), "!="); } - template - void addition(Module &m) { + template + void addition(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs + rhs; }), "+"); } - template - void unary_plus(Module &m) { + template + void unary_plus(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs) { return +lhs; }), "+"); } - template - void subtraction(Module &m) { + template + void subtraction(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs - rhs; }), "-"); } - template - void unary_minus(Module &m) { + template + void unary_minus(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs) { return -lhs; }), "-"); } - template - void bitwise_and(Module &m) { + template + void bitwise_and(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs & rhs; }), "&"); } - template - void bitwise_compliment(Module &m) { + template + void bitwise_compliment(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs) { return ~lhs; }), "~"); } - template - void bitwise_xor(Module &m) { + template + void bitwise_xor(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs ^ rhs; }), "^"); } - template - void bitwise_or(Module &m) { + template + void bitwise_or(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs | rhs; }), "|"); } - template - void division(Module &m) { + template + void division(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs / rhs; }), "/"); } - template - void left_shift(Module &m) { + template + void left_shift(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs << rhs; }), "<<"); } - template - void multiplication(Module &m) { + template + void multiplication(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs * rhs; }), "*"); } - template - void remainder(Module &m) { + template + void remainder(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs % rhs; }), "%"); } - template - void right_shift(Module &m) { + template + void right_shift(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs >> rhs; }), ">>"); } } // namespace chaiscript::bootstrap::operators diff --git a/include/chaiscript/language/chaiscript_eval.hpp b/include/chaiscript/language/chaiscript_eval.hpp index 20e30c07..f59b34ce 100644 --- a/include/chaiscript/language/chaiscript_eval.hpp +++ b/include/chaiscript/language/chaiscript_eval.hpp @@ -1471,6 +1471,7 @@ namespace chaiscript { Boxed_Value handle_exception(const chaiscript::detail::Dispatch_State &t_ss, const Boxed_Value &t_except) const { Boxed_Value retval; + bool handled = false; size_t end_point = this->children.size(); if (this->children.back()->identifier == AST_Node_Type::Finally) { @@ -1484,6 +1485,7 @@ namespace chaiscript { if (catch_block.children.size() == 1) { // No variable capture retval = catch_block.children[0]->eval(t_ss); + handled = true; break; } else if (catch_block.children.size() == 2 || catch_block.children.size() == 3) { const auto name = Arg_List_AST_Node::get_arg_name(*catch_block.children[0]); @@ -1497,17 +1499,19 @@ namespace chaiscript { if (catch_block.children.size() == 2) { // Variable capture retval = catch_block.children[1]->eval(t_ss); + handled = true; break; } } } else { - if (this->children.back()->identifier == AST_Node_Type::Finally) { - this->children.back()->children[0]->eval(t_ss); - } throw exception::eval_error("Internal error: catch block size unrecognized"); } } + if (!handled) { + throw; + } + return retval; } @@ -1517,17 +1521,19 @@ namespace chaiscript { chaiscript::eval::detail::Scope_Push_Pop spp(t_ss); try { - retval = this->children[0]->eval(t_ss); - } catch (const exception::eval_error &e) { - retval = handle_exception(t_ss, Boxed_Value(std::ref(e))); - } catch (const std::runtime_error &e) { - retval = handle_exception(t_ss, Boxed_Value(std::ref(e))); - } catch (const std::out_of_range &e) { - retval = handle_exception(t_ss, Boxed_Value(std::ref(e))); - } catch (const std::exception &e) { - retval = handle_exception(t_ss, Boxed_Value(std::ref(e))); - } catch (Boxed_Value &e) { - retval = handle_exception(t_ss, e); + try { + retval = this->children[0]->eval(t_ss); + } catch (const exception::eval_error &e) { + retval = handle_exception(t_ss, Boxed_Value(std::ref(e))); + } catch (const std::runtime_error &e) { + retval = handle_exception(t_ss, Boxed_Value(std::ref(e))); + } catch (const std::out_of_range &e) { + retval = handle_exception(t_ss, Boxed_Value(std::ref(e))); + } catch (const std::exception &e) { + retval = handle_exception(t_ss, Boxed_Value(std::ref(e))); + } catch (Boxed_Value &e) { + retval = handle_exception(t_ss, e); + } } catch (...) { if (this->children.back()->identifier == AST_Node_Type::Finally) { this->children.back()->children[0]->eval(t_ss); diff --git a/unittests/compiled_tests.cpp b/unittests/compiled_tests.cpp index 7b601b15..09e14d9a 100644 --- a/unittests/compiled_tests.cpp +++ b/unittests/compiled_tests.cpp @@ -604,6 +604,45 @@ TEST_CASE("Utility_Test utility class wrapper for enum") { CHECK_NOTHROW(chai.eval("var o = ONE; o = TWO")); } +// Issue #601: add_class for enums should work directly with ChaiScript reference +enum class Issue601_EnumClass { Apple, Banana, Pear }; + +TEST_CASE("Issue 601: add_class enum with ChaiScript reference directly") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + // This should compile and work — previously it failed because the operator + // functions in chaiscript::bootstrap::operators hardcoded Module& as their + // first parameter instead of using a template parameter. + chaiscript::utility::add_class(chai, + "Issue601_EnumClass", + {{Issue601_EnumClass::Apple, "Apple"}, + {Issue601_EnumClass::Banana, "Banana"}, + {Issue601_EnumClass::Pear, "Pear"}}); + + CHECK(chai.eval("Apple == Apple")); + CHECK(chai.eval("Apple != Banana")); + CHECK_NOTHROW(chai.eval("var e = Apple; e = Pear")); + CHECK(chai.eval("Banana") == Issue601_EnumClass::Banana); +} + +// Also test non-scoped enum directly with ChaiScript reference +enum Issue601_PlainEnum { Issue601_Red = 0, Issue601_Green = 1, Issue601_Blue = 2 }; + +TEST_CASE("Issue 601: add_class plain enum with ChaiScript reference directly") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + chaiscript::utility::add_class(chai, + "Issue601_PlainEnum", + {{Issue601_Red, "Red"}, + {Issue601_Green, "Green"}, + {Issue601_Blue, "Blue"}}); + + CHECK(chai.eval("Red == Red")); + CHECK(chai.eval("Red == 0")); + CHECK(chai.eval("Red != Green")); + CHECK_NOTHROW(chai.eval("var c = Red; c = Blue")); +} + ////// Object copy count test class Object_Copy_Count_Test { @@ -1782,3 +1821,191 @@ TEST_CASE("eval_error with AST_Node_Trace call stack compiles in C++20") { (void)stack; } } + +TEST_CASE("C++ runtime_error thrown from registered function is catchable in ChaiScript") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + chai.add(chaiscript::fun([]() -> int { throw std::runtime_error("cpp_runtime_error"); }), "cpp_throw_runtime"); + + CHECK(chai.eval(R"( + var caught = false + try { + cpp_throw_runtime() + } + catch(e) { + caught = true + } + caught + )") == true); +} + +TEST_CASE("C++ out_of_range thrown from registered function is catchable in ChaiScript") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + chai.add(chaiscript::fun([]() -> int { throw std::out_of_range("cpp_out_of_range"); }), "cpp_throw_oor"); + + CHECK(chai.eval(R"( + var caught = false + try { + cpp_throw_oor() + } + catch(e) { + caught = true + } + caught + )") == true); +} + +TEST_CASE("C++ logic_error thrown from registered function is catchable in ChaiScript") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + chai.add(chaiscript::fun([]() -> int { throw std::logic_error("cpp_logic_error"); }), "cpp_throw_logic"); + + CHECK(chai.eval(R"( + var caught = false + try { + cpp_throw_logic() + } + catch(e) { + caught = true + } + caught + )") == true); +} + +TEST_CASE("ChaiScript throw(int) propagates as Boxed_Value to C++") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + try { + chai.eval("throw(42)"); + REQUIRE(false); + } catch (chaiscript::Boxed_Value &bv) { + CHECK(chaiscript::boxed_cast(bv) == 42); + } +} + +TEST_CASE("ChaiScript throw(string) propagates as Boxed_Value to C++") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + try { + chai.eval(R"(throw("error msg"))"); + REQUIRE(false); + } catch (chaiscript::Boxed_Value &bv) { + CHECK(chaiscript::boxed_cast(bv) == "error msg"); + } +} + +TEST_CASE("Typed catch with no match propagates exception") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + CHECK_THROWS_AS(chai.eval(R"( + try { + throw(42) + } + catch(string e) { + // wrong type, should not match + } + )"), chaiscript::Boxed_Value); +} + +TEST_CASE("Typed catch with no match still runs finally block") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + CHECK_THROWS_AS(chai.eval(R"( + var finally_ran = false + try { + throw(42) + } + catch(string e) { + // wrong type + } + finally { + finally_ran = true + } + )"), chaiscript::Boxed_Value); + + CHECK(chai.eval("finally_ran") == true); +} + +TEST_CASE("Multiple C++ exception types from registered functions") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + chai.add(chaiscript::fun([](int which) -> int { + switch (which) { + case 0: throw std::runtime_error("runtime"); + case 1: throw std::out_of_range("range"); + case 2: throw std::logic_error("logic"); + default: return which; + } + }), "cpp_multi_throw"); + + CHECK(chai.eval(R"( + var catch_count = 0 + for (var i = 0; i < 3; ++i) { + try { + cpp_multi_throw(i) + } + catch(e) { + catch_count = catch_count + 1 + } + } + catch_count + )") == 3); + + CHECK(chai.eval("cpp_multi_throw(5)") == 5); +} + +TEST_CASE("Exception from C++ binary operator is catchable in ChaiScript") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + struct ThrowingType { + int value; + }; + + chai.add(chaiscript::user_type(), "ThrowingType"); + chai.add(chaiscript::constructor(), "ThrowingType"); + chai.add(chaiscript::fun([](const ThrowingType &, const ThrowingType &) -> ThrowingType { + throw std::runtime_error("cpp operator+ threw"); + }), "+"); + + CHECK(chai.eval(R"( + var caught = false + try { + var a = ThrowingType(1) + var b = ThrowingType(2) + var c = a + b + } + catch(e) { + caught = true + } + caught + )") == true); +} + +TEST_CASE("Exception from C++ [] operator is catchable in ChaiScript") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + struct IndexableType { + int value; + }; + + chai.add(chaiscript::user_type(), "IndexableType"); + chai.add(chaiscript::constructor(), "IndexableType"); + chai.add(chaiscript::fun([](const IndexableType &, int idx) -> int { + if (idx < 0) { throw std::out_of_range("negative index"); } + return idx; + }), "[]"); + + CHECK(chai.eval("var obj = IndexableType(0); obj[5]") == 5); + + CHECK(chai.eval(R"( + var caught = false + try { + var x = obj[-1] + } + catch(e) { + caught = true + } + caught + )") == true); +} diff --git a/unittests/exception_comprehensive.chai b/unittests/exception_comprehensive.chai new file mode 100644 index 00000000..6e39ceb0 --- /dev/null +++ b/unittests/exception_comprehensive.chai @@ -0,0 +1,862 @@ +// Comprehensive exception throwing and catching tests +// Tests throw/catch from various contexts: operators, functions, lambdas, +// methods, [] operator, nested scopes, and with various value types. + +// ============================================================ +// Section 1: Throwing and catching different value types +// ============================================================ + +// Throw int +try { + throw(42) +} +catch(e) { + assert_equal(42, e) +} + +// Throw string +try { + throw("error message") +} +catch(e) { + assert_equal("error message", e) +} + +// Throw double +try { + throw(3.14) +} +catch(e) { + assert_equal(3.14, e) +} + +// Throw bool +try { + throw(true) +} +catch(e) { + assert_equal(true, e) +} + +// Throw a Vector (via named variable to preserve mutability) +auto thrown_vec = [1, 2, 3] +try { + throw(thrown_vec) +} +catch(e) { + assert_equal(3, e.size()) + assert_equal(1, e[0]) + assert_equal(2, e[1]) + assert_equal(3, e[2]) +} + +// Throw a Map (via named variable to preserve mutability) +auto thrown_map = ["key": 42] +try { + throw(thrown_map) +} +catch(e) { + assert_equal(42, e["key"]) +} + +// ============================================================ +// Section 2: Typed catch blocks +// ============================================================ + +// Typed catch matching int +auto typed_result = 0 +try { + throw(10) +} +catch(int e) { + typed_result = e + 1 +} +assert_equal(11, typed_result) + +// Typed catch matching string +typed_result = 0 +try { + throw("hello") +} +catch(string e) { + typed_result = 1 +} +assert_equal(1, typed_result) + +// Typed catch mismatch falls through to untyped +typed_result = 0 +try { + throw(42) +} +catch(string e) { + typed_result = 1 +} +catch(e) { + typed_result = 2 +} +assert_equal(2, typed_result) + +// Multiple typed catches - first match wins +typed_result = 0 +try { + throw("test") +} +catch(int e) { + typed_result = 1 +} +catch(string e) { + typed_result = 2 +} +catch(e) { + typed_result = 3 +} +assert_equal(2, typed_result) + +// Typed catch with int, multiple typed blocks, none match except untyped +typed_result = 0 +try { + throw(3.14) +} +catch(int e) { + typed_result = 1 +} +catch(string e) { + typed_result = 2 +} +catch(e) { + typed_result = 3 +} +assert_equal(3, typed_result) + +// ============================================================ +// Section 3: Catch-all (no variable) catch block +// ============================================================ + +auto catch_all_reached = false +try { + throw(99) +} +catch { + catch_all_reached = true +} +assert_true(catch_all_reached) + +// ============================================================ +// Section 4: Finally block semantics +// ============================================================ + +// Finally runs after exception +auto finally_ran = false +try { + throw(1) +} +catch(e) { + // caught +} +finally { + finally_ran = true +} +assert_true(finally_ran) + +// Finally runs without exception +finally_ran = false +try { + auto x = 1 +} +catch(e) { + // not reached +} +finally { + finally_ran = true +} +assert_true(finally_ran) + +// Finally runs even with typed catch that matches +finally_ran = false +try { + throw(42) +} +catch(int e) { + assert_equal(42, e) +} +finally { + finally_ran = true +} +assert_true(finally_ran) + +// Finally runs when typed catch does NOT match (exception not caught) +finally_ran = false +auto outer_caught = false +try { + try { + throw(42) + } + catch(string e) { + // wrong type, won't match + } + finally { + finally_ran = true + } +} +catch(e) { + outer_caught = true +} +assert_true(finally_ran) +assert_true(outer_caught) + +// ============================================================ +// Section 5: Throwing from functions +// ============================================================ + +def throwing_function() { + throw("from function") +} + +try { + throwing_function() +} +catch(e) { + assert_equal("from function", e) +} + +// Throwing from nested function calls +def inner_throw() { + throw("inner") +} + +def outer_call() { + inner_throw() +} + +try { + outer_call() +} +catch(e) { + assert_equal("inner", e) +} + +// Function that throws conditionally +def conditional_throw(should_throw) { + if (should_throw) { + throw("conditional") + } + return "no throw" +} + +assert_equal("no throw", conditional_throw(false)) + +try { + conditional_throw(true) +} +catch(e) { + assert_equal("conditional", e) +} + +// ============================================================ +// Section 6: Throwing from lambdas +// ============================================================ + +auto throwing_lambda = fun() { throw("from lambda") } + +try { + throwing_lambda() +} +catch(e) { + assert_equal("from lambda", e) +} + +// Lambda that captures and throws +auto captured_val = "captured" +auto capture_lambda = fun[captured_val]() { throw(captured_val) } + +try { + capture_lambda() +} +catch(e) { + assert_equal("captured", e) +} + +// ============================================================ +// Section 7: Throwing from binary operators +// ============================================================ + +// Define a type and an operator that throws +attr ThrowOnAdd::val +def ThrowOnAdd::ThrowOnAdd(v) { this.val = v } + +def `+`(ThrowOnAdd x, ThrowOnAdd y) { + throw("add not supported") +} + +try { + auto a = ThrowOnAdd(1) + auto b = ThrowOnAdd(2) + auto c = a + b + assert_true(false) // should not reach here +} +catch(e) { + assert_equal("add not supported", e) +} + +// Operator that throws for specific values +def `-`(ThrowOnAdd x, ThrowOnAdd y) { + if (x.val == y.val) { + throw("cannot subtract equal values") + } + return ThrowOnAdd(x.val - y.val) +} + +try { + auto a = ThrowOnAdd(5) + auto b = ThrowOnAdd(5) + auto c = a - b + assert_true(false) +} +catch(e) { + assert_equal("cannot subtract equal values", e) +} + +// Multiplication operator that throws (not pre-defined for Dynamic_Object) +def `*`(ThrowOnAdd x, ThrowOnAdd y) { + throw("multiply not supported") +} + +try { + auto a = ThrowOnAdd(1) + auto b = ThrowOnAdd(2) + auto c = a * b + assert_true(false) +} +catch(e) { + assert_equal("multiply not supported", e) +} + +// Subtraction works for non-equal values +auto sub_result = ThrowOnAdd(10) - ThrowOnAdd(3) +assert_equal(7, sub_result.val) + +// ============================================================ +// Section 8: Throwing from unary/prefix operators +// ============================================================ + +def `++`(ThrowOnAdd x) { + throw("increment not supported") +} + +try { + auto a = ThrowOnAdd(1) + ++a + assert_true(false) +} +catch(e) { + assert_equal("increment not supported", e) +} + +// ============================================================ +// Section 9: Throwing from [] operator +// ============================================================ + +attr ThrowOnIndex::data +def ThrowOnIndex::ThrowOnIndex() { this.data = [1, 2, 3] } + +def `[]`(ThrowOnIndex obj, int idx) { + if (idx < 0) { + throw("negative index not allowed") + } + return obj.data[idx] +} + +auto toi = ThrowOnIndex() +assert_equal(1, toi[0]) +assert_equal(2, toi[1]) + +try { + auto val = toi[-1] + assert_true(false) +} +catch(e) { + assert_equal("negative index not allowed", e) +} + +// ============================================================ +// Section 10: Throwing from member functions +// ============================================================ + +attr Validatable::value +def Validatable::Validatable(v) { this.value = v } + +def Validatable::validate() { + if (this.value < 0) { + throw("validation failed: negative value") + } + return true +} + +auto valid_obj = Validatable(10) +assert_true(valid_obj.validate()) + +auto invalid_obj = Validatable(-1) +try { + invalid_obj.validate() + assert_true(false) +} +catch(e) { + assert_equal("validation failed: negative value", e) +} + +// ============================================================ +// Section 11: Nested try/catch +// ============================================================ + +auto inner_caught = false +auto outer_caught_val = 0 +try { + try { + throw(1) + } + catch(e) { + inner_caught = true + throw(e + 10) + } +} +catch(e) { + outer_caught_val = e +} +assert_true(inner_caught) +assert_equal(11, outer_caught_val) + +// Deeply nested try/catch +auto depth = 0 +try { + try { + try { + throw("deep") + } + catch(e) { + depth = 1 + throw(e + "er") + } + } + catch(e) { + depth = 2 + throw(e + "est") + } +} +catch(e) { + depth = 3 + assert_equal("deeperest", e) +} +assert_equal(3, depth) + +// ============================================================ +// Section 12: Rethrow from catch block +// ============================================================ + +auto rethrow_caught = false +try { + try { + throw("rethrown") + } + catch(e) { + throw(e) + } +} +catch(e) { + rethrow_caught = true + assert_equal("rethrown", e) +} +assert_true(rethrow_caught) + +// ============================================================ +// Section 13: Exception in for loop +// ============================================================ + +auto loop_exception_val = 0 +try { + for (auto i = 0; i < 10; ++i) { + if (i == 5) { + throw(i) + } + } +} +catch(e) { + loop_exception_val = e +} +assert_equal(5, loop_exception_val) + +// ============================================================ +// Section 14: Exception in while loop +// ============================================================ + +auto while_exc_val = 0 +auto counter = 0 +try { + while (true) { + ++counter + if (counter == 3) { + throw(counter) + } + } +} +catch(e) { + while_exc_val = e +} +assert_equal(3, while_exc_val) + +// ============================================================ +// Section 15: Exception preserves value through nested calls +// ============================================================ + +def deep_throw(val) { + throw(val) +} + +def middle_call(val) { + deep_throw(val) +} + +def top_call(val) { + middle_call(val) +} + +auto nested_map = ["key": "value"] +try { + top_call(nested_map) +} +catch(e) { + assert_equal("value", e["key"]) +} + +auto nested_vec = [10, 20, 30] +try { + top_call(nested_vec) +} +catch(e) { + assert_equal(3, e.size()) + assert_equal(20, e[1]) +} + +// ============================================================ +// Section 16: Code after throw is not executed +// ============================================================ + +auto after_throw = false +try { + throw(1) + after_throw = true +} +catch(e) { + // caught +} +assert_false(after_throw) + +// ============================================================ +// Section 17: Exception value is usable in catch block arithmetic +// ============================================================ + +auto catch_computed = 0 +try { + throw(1) +} +catch(e) { + catch_computed = e + 100 +} +assert_equal(101, catch_computed) + +// ============================================================ +// Section 18: No exception means catch is skipped +// ============================================================ + +auto catch_skipped = true +try { + auto x = 42 +} +catch(e) { + catch_skipped = false +} +assert_true(catch_skipped) + +// ============================================================ +// Section 19: Exception from dynamic object method chaining +// ============================================================ + +attr Chain::val +def Chain::Chain(v) { this.val = v } + +def Chain::add(n) { + if (this.val + n > 100) { + throw("overflow: " + to_string(this.val + n)) + } + this.val = this.val + n + return this +} + +auto chain = Chain(50) +try { + chain.add(30).add(30) + assert_true(false) +} +catch(e) { + assert_equal("overflow: 110", e) +} +assert_equal(80, chain.val) + +// ============================================================ +// Section 20: Exception thrown during map construction +// ============================================================ + +def exploding_value() { + throw("boom during construction") +} + +try { + auto m = ["ok": 1, "bad": exploding_value()] + assert_true(false) +} +catch(e) { + assert_equal("boom during construction", e) +} + +// ============================================================ +// Section 21: Exception thrown during vector construction +// ============================================================ + +try { + auto v = [1, 2, exploding_value(), 4] + assert_true(false) +} +catch(e) { + assert_equal("boom during construction", e) +} + +// ============================================================ +// Section 22: Exception in if-condition +// ============================================================ + +def exploding_condition() { + throw("condition exploded") +} + +try { + if (exploding_condition()) { + assert_true(false) + } +} +catch(e) { + assert_equal("condition exploded", e) +} + +// ============================================================ +// Section 23: Multiple catch blocks - only first matching runs +// ============================================================ + +auto catch_count = 0 +try { + throw(42) +} +catch(int e) { + ++catch_count +} +catch(e) { + ++catch_count +} +assert_equal(1, catch_count) + +// ============================================================ +// Section 24: Throwing from within catch, with finally +// ============================================================ + +auto s24_finally = false +auto s24_outer = false +try { + try { + throw("original") + } + catch(e) { + throw("replaced: " + e) + } + finally { + s24_finally = true + } +} +catch(e) { + s24_outer = true + assert_equal("replaced: original", e) +} +assert_true(s24_finally) +assert_true(s24_outer) + +// ============================================================ +// Section 25: Unhandled typed catch propagates exception +// ============================================================ + +auto s25_caught = false +try { + try { + throw(3.14) + } + catch(int e) { + assert_true(false) // should not match double + } + catch(string e) { + assert_true(false) // should not match double + } +} +catch(e) { + s25_caught = true + assert_equal(3.14, e) +} +assert_true(s25_caught) + +// ============================================================ +// Section 26: Throw from range-based for +// ============================================================ + +auto s26_val = 0 +try { + for (x : [10, 20, 30, 40]) { + if (x == 30) { + throw(x) + } + } +} +catch(e) { + s26_val = e +} +assert_equal(30, s26_val) + +// ============================================================ +// Section 27: Throw from eval +// ============================================================ + +try { + eval("throw(\"from eval\")") +} +catch(e) { + assert_equal("from eval", e) +} + +// ============================================================ +// Section 28: Exception from built-in operations (out of range) +// ============================================================ + +auto s28_caught = false +try { + auto v = [1, 2, 3] + auto x = v[10] +} +catch(e) { + s28_caught = true +} +assert_true(s28_caught) + +// ============================================================ +// Section 29: Throw zero and empty string (falsy values) +// ============================================================ + +try { + throw(0) +} +catch(e) { + assert_equal(0, e) +} + +try { + throw("") +} +catch(e) { + assert_equal("", e) +} + +// ============================================================ +// Section 30: Throw from ternary-style inline_if +// ============================================================ + +def maybe_throw(do_it) { + if (do_it) { throw("inline threw") } else { "ok" } +} + +try { + maybe_throw(true) +} +catch(e) { + assert_equal("inline threw", e) +} +assert_equal("ok", maybe_throw(false)) + +// ============================================================ +// Section 31: Verify catch variable scope isolation +// ============================================================ + +auto outer_e = "untouched" +try { + throw("caught_value") +} +catch(e) { + assert_equal("caught_value", e) +} +assert_equal("untouched", outer_e) + +// ============================================================ +// Section 32: Exception from recursive function +// ============================================================ + +def recursive_throw(n) { + if (n == 0) { + throw("bottom") + } + recursive_throw(n - 1) +} + +try { + recursive_throw(5) +} +catch(e) { + assert_equal("bottom", e) +} + +// ============================================================ +// Section 33: Try/catch in a function body +// ============================================================ + +def safe_divide(a, b) { + try { + if (b == 0) { + throw("division by zero") + } + return a / b + } + catch(e) { + return e + } +} + +assert_equal(5, safe_divide(10, 2)) +assert_equal("division by zero", safe_divide(10, 0)) + +// ============================================================ +// Section 34: Throw from [] on a Map with missing key +// ============================================================ + +auto s34_caught = false +try { + auto m = ["a": 1] + auto x = m["nonexistent"] +} +catch(e) { + s34_caught = true +} +assert_true(s34_caught) + +// ============================================================ +// Section 35: Arithmetic exception (divide by zero) +// ============================================================ + +auto s35_caught = false +try { + auto x = 1 / 0 +} +catch(e) { + s35_caught = true +} +assert_true(s35_caught)