Fix #63: Derive eval_error from std::nested_exception and wrap thrown exceptions

eval_error now inherits from both std::runtime_error and std::nested_exception,
enabling users to access the original exception via rethrow_nested() or
nested_ptr(). The engine's eval() method wraps uncaught Boxed_Value exceptions
in eval_error, nesting the original Boxed_Value so it can be recovered.
exception_specification continues to work for typed exception unboxing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
leftibot 2026-04-13 19:02:32 -06:00
parent 362e93fb29
commit 2c80c2a55a
3 changed files with 70 additions and 9 deletions

View File

@ -11,6 +11,7 @@
#define CHAISCRIPT_COMMON_HPP_
#include <algorithm>
#include <exception>
#include <memory>
#include <sstream>
#include <stdexcept>
@ -296,7 +297,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;

View File

@ -695,7 +695,7 @@ namespace chaiscript {
if (t_handler) {
t_handler->handle(bv, m_engine);
}
throw;
throw exception::eval_error("Exception thrown during evaluation");
}
}

View File

@ -166,9 +166,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<const std::exception &>(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<const std::exception &>(bv);
CHECK(e.what() == std::string("error"));
}
}
}
@ -194,6 +200,62 @@ 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<std::nested_exception, chaiscript::exception::eval_error>,
"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<const std::exception &>(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<const std::exception &>(bv);
CHECK(nested.what() == std::string("from foo"));
}
}
}
TEST_CASE("Deduction of pointer return types") {
int val = 5;
int *val_ptr = &val;
@ -259,10 +321,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);
}
}