mirror of
https://github.com/ChaiScript/ChaiScript.git
synced 2026-04-30 19:09:26 +08:00
When all typed catch blocks failed to match a thrown exception's type,
handle_exception() would silently discard the exception and return a
default-constructed Boxed_Value instead of propagating it. This meant
code like `try { throw(42) } catch(string e) { }` would swallow the
int exception rather than letting it propagate to an outer handler.
The fix adds an explicit re-throw when no catch block matches, and
restructures eval_internal() with a nested try/catch to ensure the
finally block still executes before the unhandled exception propagates.
Co-authored-by: leftibot <leftibot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
863 lines
17 KiB
ChaiScript
863 lines
17 KiB
ChaiScript
// 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)
|