diff --git a/include/chaiscript/language/chaiscript_common.hpp b/include/chaiscript/language/chaiscript_common.hpp index 0d80b8ca..8a85a4e6 100644 --- a/include/chaiscript/language/chaiscript_common.hpp +++ b/include/chaiscript/language/chaiscript_common.hpp @@ -11,6 +11,7 @@ #define CHAISCRIPT_COMMON_HPP_ #include +#include #include #include #include @@ -299,7 +300,7 @@ namespace chaiscript { }; /// Errors generated during parsing or evaluation - struct eval_error : std::runtime_error { + struct eval_error : std::runtime_error, std::nested_exception { std::string reason; File_Position start_position; std::string filename; @@ -342,8 +343,31 @@ namespace chaiscript { , reason(t_why) { } + eval_error(const std::string &t_why, Boxed_Value t_bv) noexcept + : std::runtime_error(format_why(t_why) + " " + format_filename("__EVAL__")) + , reason(t_why) + , m_boxed_value(std::move(t_bv)) { + } + + eval_error(const std::string &t_why, const std::string &t_fname, Boxed_Value t_bv) noexcept + : std::runtime_error(format_why(t_why) + " " + format_filename(t_fname)) + , reason(t_why) + , filename(t_fname) + , m_boxed_value(std::move(t_bv)) { + } + eval_error(const eval_error &) = default; + bool has_boxed_value() const noexcept { return !m_boxed_value.is_undef(); } + const Boxed_Value &boxed_value() const noexcept { return m_boxed_value; } + + template + void rethrow_typed(const Engine &t_engine) const { + if (has_boxed_value()) { + (try_rethrow(t_engine), ...); + } + } + std::string pretty_print() const { std::ostringstream ss; @@ -367,6 +391,16 @@ namespace chaiscript { ~eval_error() noexcept override = default; private: + Boxed_Value m_boxed_value; + + template + void try_rethrow(const Engine &t_engine) const { + try { + throw t_engine.template boxed_cast(m_boxed_value); + } catch (const chaiscript::exception::bad_boxed_cast &) { + } + } + template static AST_Node_Type id(const T &t) noexcept { return t.identifier; diff --git a/include/chaiscript/language/chaiscript_engine.hpp b/include/chaiscript/language/chaiscript_engine.hpp index fa46a460..1414709b 100644 --- a/include/chaiscript/language/chaiscript_engine.hpp +++ b/include/chaiscript/language/chaiscript_engine.hpp @@ -101,6 +101,9 @@ namespace chaiscript { } catch (const exception::file_not_found_error &) { // failed to load, try the next path } catch (const exception::eval_error &t_ee) { + if (t_ee.has_boxed_value()) { + throw Boxed_Value(t_ee.boxed_value()); + } throw Boxed_Value(t_ee); } } @@ -114,6 +117,9 @@ namespace chaiscript { try { return do_eval(t_e, "__EVAL__", true); } catch (const exception::eval_error &t_ee) { + if (t_ee.has_boxed_value()) { + throw Boxed_Value(t_ee.boxed_value()); + } throw Boxed_Value(t_ee); } } @@ -718,11 +724,21 @@ namespace chaiscript { eval(const std::string &t_input, const Exception_Handler &t_handler = Exception_Handler(), const std::string &t_filename = "__EVAL__") { try { return do_eval(t_input, t_filename); + } catch (exception::eval_error &ee) { + if (ee.has_boxed_value()) { + if (ee.filename.empty()) { + ee.filename = t_filename; + } + if (t_handler) { + t_handler->handle(ee.boxed_value(), m_engine); + } + } + throw; } catch (Boxed_Value &bv) { if (t_handler) { t_handler->handle(bv, m_engine); } - throw; + throw exception::eval_error("Exception thrown during evaluation", t_filename, bv); } } diff --git a/include/chaiscript/language/chaiscript_eval.hpp b/include/chaiscript/language/chaiscript_eval.hpp index 635aac3f..490e330d 100644 --- a/include/chaiscript/language/chaiscript_eval.hpp +++ b/include/chaiscript/language/chaiscript_eval.hpp @@ -306,6 +306,10 @@ namespace chaiscript { } catch (exception::eval_error &ee) { ee.call_stack.push_back(*this); throw; + } catch (const Boxed_Value &bv) { + exception::eval_error ee("Exception thrown during evaluation", bv); + ee.call_stack.push_back(*this); + throw ee; } } @@ -1787,7 +1791,11 @@ namespace chaiscript { try { retval = this->children[0]->eval(t_ss); } catch (const exception::eval_error &e) { - retval = handle_exception(t_ss, Boxed_Value(std::ref(e))); + if (e.has_boxed_value()) { + retval = handle_exception(t_ss, e.boxed_value()); + } else { + 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) { diff --git a/unittests/compiled_tests.cpp b/unittests/compiled_tests.cpp index 74e8fc97..40ebf8a3 100644 --- a/unittests/compiled_tests.cpp +++ b/unittests/compiled_tests.cpp @@ -165,9 +165,15 @@ TEST_CASE("Generic exception handling with C++") { try { chai.eval("throw(runtime_error(\"error\"));"); REQUIRE(false); - } catch (const chaiscript::Boxed_Value &bv) { - const std::exception &e = chai.boxed_cast(bv); - CHECK(e.what() == std::string("error")); + } catch (const chaiscript::exception::eval_error &ee) { + REQUIRE(ee.nested_ptr() != nullptr); + try { + ee.rethrow_nested(); + REQUIRE(false); + } catch (const chaiscript::Boxed_Value &bv) { + const std::exception &e = chai.boxed_cast(bv); + CHECK(e.what() == std::string("error")); + } } } @@ -193,6 +199,163 @@ TEST_CASE("Throw int or double") { } } +TEST_CASE("eval_error derives from std::nested_exception") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + static_assert(std::is_base_of_v, + "eval_error must derive from std::nested_exception"); + + try { + chai.eval("throw(runtime_error(\"inner error\"));"); + REQUIRE(false); + } catch (const chaiscript::exception::eval_error &ee) { + CHECK(ee.nested_ptr() != nullptr); + try { + ee.rethrow_nested(); + REQUIRE(false); + } catch (const chaiscript::Boxed_Value &bv) { + const std::exception &nested = chai.boxed_cast(bv); + CHECK(nested.what() == std::string("inner error")); + } + } +} + +TEST_CASE("eval_error wraps non-eval exceptions with nested exception") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + try { + chai.eval("throw(42);"); + REQUIRE(false); + } catch (const chaiscript::exception::eval_error &ee) { + CHECK(ee.nested_ptr() != nullptr); + try { + ee.rethrow_nested(); + REQUIRE(false); + } catch (const chaiscript::Boxed_Value &) { + CHECK(true); + } + } +} + +TEST_CASE("eval_error includes nested exception for script-thrown exceptions") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + try { + chai.eval("def foo() { throw(runtime_error(\"from foo\")); } \n foo();"); + REQUIRE(false); + } catch (const chaiscript::exception::eval_error &ee) { + CHECK(ee.nested_ptr() != nullptr); + try { + ee.rethrow_nested(); + REQUIRE(false); + } catch (const chaiscript::Boxed_Value &bv) { + const std::exception &nested = chai.boxed_cast(bv); + CHECK(nested.what() == std::string("from foo")); + } + } +} + +TEST_CASE("eval_error stores boxed_value for script-thrown exceptions") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + try { + chai.eval("throw(42);"); + REQUIRE(false); + } catch (const chaiscript::exception::eval_error &ee) { + REQUIRE(ee.has_boxed_value()); + CHECK(chai.boxed_cast(ee.boxed_value()) == 42); + } +} + +TEST_CASE("eval_error rethrow_typed auto-unboxes runtime_error") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + try { + chai.eval("throw(runtime_error(\"typed error\"));"); + REQUIRE(false); + } catch (const chaiscript::exception::eval_error &ee) { + REQUIRE(ee.has_boxed_value()); + try { + ee.rethrow_typed(chai); + REQUIRE(false); + } catch (const std::runtime_error &e) { + CHECK(e.what() == std::string("typed error")); + } + } +} + +TEST_CASE("eval_error rethrow_typed auto-unboxes int") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + try { + chai.eval("throw(42);"); + REQUIRE(false); + } catch (const chaiscript::exception::eval_error &ee) { + REQUIRE(ee.has_boxed_value()); + try { + ee.rethrow_typed(chai); + REQUIRE(false); + } catch (const int e) { + CHECK(e == 42); + } + } +} + +TEST_CASE("eval_error rethrow_typed with no match does not throw") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + try { + chai.eval("throw(\"a string\");"); + REQUIRE(false); + } catch (const chaiscript::exception::eval_error &ee) { + REQUIRE(ee.has_boxed_value()); + ee.rethrow_typed(chai); + CHECK(true); + } +} + +TEST_CASE("eval_error without boxed_value has_boxed_value returns false") { + const chaiscript::exception::eval_error ee("plain error"); + CHECK_FALSE(ee.has_boxed_value()); +} + +TEST_CASE("eval_error wrapping script throw includes call stack") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + try { + chai.eval("def foo() { throw(42); } \n foo();"); + REQUIRE(false); + } catch (const chaiscript::exception::eval_error &ee) { + REQUIRE(ee.has_boxed_value()); + CHECK_FALSE(ee.call_stack.empty()); + const auto pretty = ee.pretty_print(); + CHECK(pretty.find("foo") != std::string::npos); + } +} + +TEST_CASE("eval_error wrapping script throw includes filename") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + try { + chai.eval("throw(99);", chaiscript::Exception_Handler(), "test_script.chai"); + REQUIRE(false); + } catch (const chaiscript::exception::eval_error &ee) { + REQUIRE(ee.has_boxed_value()); + CHECK(ee.filename == "test_script.chai"); + } +} + +TEST_CASE("exception_specification still auto-unboxes for backward compatibility") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + try { + chai.eval("throw(1)", chaiscript::exception_specification()); + REQUIRE(false); + } catch (const int e) { + CHECK(e == 1); + } +} + TEST_CASE("Deduction of pointer return types") { int val = 5; int *val_ptr = &val; @@ -258,10 +421,8 @@ TEST_CASE("Throw unhandled type") { REQUIRE(false); } catch (float) { REQUIRE(false); - } catch (const std::exception &) { - REQUIRE(false); - } catch (const chaiscript::Boxed_Value &) { - REQUIRE(true); + } catch (const chaiscript::exception::eval_error &ee) { + REQUIRE(ee.nested_ptr() != nullptr); } } @@ -2070,8 +2231,9 @@ TEST_CASE("ChaiScript throw(int) propagates as Boxed_Value to C++") { try { chai.eval("throw(42)"); REQUIRE(false); - } catch (chaiscript::Boxed_Value &bv) { - CHECK(chaiscript::boxed_cast(bv) == 42); + } catch (const chaiscript::exception::eval_error &ee) { + REQUIRE(ee.has_boxed_value()); + CHECK(chai.boxed_cast(ee.boxed_value()) == 42); } } @@ -2081,8 +2243,9 @@ TEST_CASE("ChaiScript throw(string) propagates as Boxed_Value to C++") { try { chai.eval(R"(throw("error msg"))"); REQUIRE(false); - } catch (chaiscript::Boxed_Value &bv) { - CHECK(chaiscript::boxed_cast(bv) == "error msg"); + } catch (const chaiscript::exception::eval_error &ee) { + REQUIRE(ee.has_boxed_value()); + CHECK(chai.boxed_cast(ee.boxed_value()) == "error msg"); } } @@ -2094,10 +2257,10 @@ TEST_CASE("Typed catch with no match propagates exception") { throw(42) } catch(string e) { - // wrong type, should not match + // wrong type, should not match — exception propagates } )"), - chaiscript::Boxed_Value); + chaiscript::exception::eval_error); } TEST_CASE("Typed catch with no match still runs finally block") { @@ -2109,13 +2272,13 @@ TEST_CASE("Typed catch with no match still runs finally block") { throw(42) } catch(string e) { - // wrong type + // wrong type, should not match — exception propagates } finally { finally_ran = true } )"), - chaiscript::Boxed_Value); + chaiscript::exception::eval_error); CHECK(chai.eval("finally_ran") == true); }