From 78d2de0ccf3565376bee7feb126f583084669213 Mon Sep 17 00:00:00 2001 From: dinghram Date: Fri, 10 Apr 2026 18:18:17 -0600 Subject: [PATCH 1/2] Fix #421: Switch with type_conversion compares destroyed objects Add Function_Push_Pop to Switch_AST_Node case comparison to properly manage the lifetime of temporaries created during type conversions. Make Binary_Operator_AST_Node::do_oper static and public so it can be reused by Switch_AST_Node for case equality checks, ensuring consistent lifetime management across all operator invocations. Original-PR: #422 Co-Authored-By: dinghram --- include/chaiscript/language/chaiscript_eval.hpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/include/chaiscript/language/chaiscript_eval.hpp b/include/chaiscript/language/chaiscript_eval.hpp index cd171b36..55f7d440 100644 --- a/include/chaiscript/language/chaiscript_eval.hpp +++ b/include/chaiscript/language/chaiscript_eval.hpp @@ -220,15 +220,16 @@ namespace chaiscript { Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &t_ss) const override { auto lhs = this->children[0]->eval(t_ss); auto rhs = this->children[1]->eval(t_ss); - return do_oper(t_ss, m_oper, this->text, lhs, rhs); + return do_oper(t_ss, m_oper, this->text, lhs, rhs, m_loc); } - protected: - Boxed_Value do_oper(const chaiscript::detail::Dispatch_State &t_ss, + // static and public so we can use this to process Switch_AST_Node case equality + static Boxed_Value do_oper(const chaiscript::detail::Dispatch_State &t_ss, Operators::Opers t_oper, const std::string &t_oper_string, const Boxed_Value &t_lhs, - const Boxed_Value &t_rhs) const { + const Boxed_Value &t_rhs, + std::atomic_uint_fast32_t &t_loc) { try { if (t_oper != Operators::Opers::invalid && t_lhs.get_type_info().is_arithmetic() && t_rhs.get_type_info().is_arithmetic()) { // If it's an arithmetic operation we want to short circuit dispatch @@ -243,7 +244,7 @@ namespace chaiscript { chaiscript::eval::detail::Function_Push_Pop fpp(t_ss); std::array params{t_lhs, t_rhs}; fpp.save_params(Function_Params(params)); - return t_ss->call_function(t_oper_string, m_loc, Function_Params(params), t_ss.conversions()); + return t_ss->call_function(t_oper_string, t_loc, Function_Params(params), t_ss.conversions()); } } catch (const exception::dispatch_error &e) { throw exception::eval_error("Can not find appropriate '" + t_oper_string + "' operator.", e.parameters, e.functions, false, *t_ss); @@ -985,8 +986,7 @@ namespace chaiscript { if (this->children[currentCase]->identifier == AST_Node_Type::Case) { // This is a little odd, but because want to see both the switch and the case simultaneously, I do a downcast here. try { - std::array p{match_value, this->children[currentCase]->children[0]->eval(t_ss)}; - if (hasMatched || boxed_cast(t_ss->call_function("==", m_loc, Function_Params{p}, t_ss.conversions()))) { + if (hasMatched || boxed_cast(Binary_Operator_AST_Node::do_oper(t_ss, Operators::Opers::equals, "==", match_value, this->children[currentCase]->children[0]->eval(t_ss), m_loc))) { this->children[currentCase]->eval(t_ss); hasMatched = true; } From a329be16ad68c949892ae8affa22431a8bc38226 Mon Sep 17 00:00:00 2001 From: leftibot Date: Fri, 10 Apr 2026 18:18:25 -0600 Subject: [PATCH 2/2] Add tests for #421: switch with type_conversion lifetime bug Add both a compiled C++ test (with registered type_conversion) and a pure ChaiScript test (with dynamic objects and custom == operator) to verify that switch case comparisons properly manage object lifetimes. Co-Authored-By: Claude Opus 4.6 (1M context) --- unittests/compiled_tests.cpp | 94 +++++++++++++++++++++++++++ unittests/switch_type_conversion.chai | 50 ++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 unittests/switch_type_conversion.chai diff --git a/unittests/compiled_tests.cpp b/unittests/compiled_tests.cpp index d72fea9b..950a5e18 100644 --- a/unittests/compiled_tests.cpp +++ b/unittests/compiled_tests.cpp @@ -1304,3 +1304,97 @@ TEST_CASE("Test if non copyable/movable types can be registered") { chai.add(chaiscript::user_type(), "Nothing"); chai.add(chaiscript::constructor(), "Nothing"); } + +// Issue #421: Class with type_conversion from int and "==" operator +// causes switch statement to compare destroyed objects. +// The switch case comparison must use Function_Push_Pop to properly +// manage the lifetime of temporaries created by type conversions. +TEST_CASE("Issue #421 - Switch with type_conversion does not compare destroyed objects") { + struct MyType { + int value; + explicit MyType(int v) : value(v) {} + }; + + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + chai.add(chaiscript::user_type(), "MyType"); + chai.add(chaiscript::constructor(), "MyType"); + chai.add(chaiscript::fun(&MyType::value), "value"); + chai.add(chaiscript::fun([](const MyType &a, const MyType &b) { return a.value == b.value; }), "=="); + chai.add(chaiscript::type_conversion([](const int &i) { return MyType(i); })); + + // Test switch with type conversion - the case integer literals must be + // converted to MyType via the registered conversion before comparison. + // Without Function_Push_Pop, the converted temporaries may be destroyed + // before the == operator can compare them. + CHECK(chai.eval(R"({ + var result = 0 + var obj = MyType(2) + switch(obj) { + case (1) { + result = 1 + break + } + case (2) { + result = 2 + break + } + case (3) { + result = 3 + break + } + } + result + })") == 2); + + // Test fall-through with type conversion + CHECK(chai.eval(R"({ + var total = 0 + var obj = MyType(2) + switch(obj) { + case (1) { + total += 1 + } + case (2) { + total += 2 + } + case (3) { + total += 4 + } + } + total + })") == 6); + + // Test matching the first case + CHECK(chai.eval(R"({ + var result = 0 + var obj = MyType(1) + switch(obj) { + case (1) { + result = 10 + break + } + case (2) { + result = 20 + break + } + } + result + })") == 10); + + // Test no match + CHECK(chai.eval(R"({ + var result = 0 + var obj = MyType(5) + switch(obj) { + case (1) { + result = 1 + break + } + case (2) { + result = 2 + break + } + } + result + })") == 0); +} diff --git a/unittests/switch_type_conversion.chai b/unittests/switch_type_conversion.chai new file mode 100644 index 00000000..ff80737f --- /dev/null +++ b/unittests/switch_type_conversion.chai @@ -0,0 +1,50 @@ +// Test for issue #421: switch statement with custom == operator on +// dynamic objects must properly manage object lifetimes during +// case comparisons. + +class MyType { + var value + def MyType(v) { this.value = v } +} + +def `==`(a, b) : a.is_type("MyType") && b.is_type("MyType") { + return a.value == b.value +} + +var result = 0 +var obj = MyType(2) + +switch(obj) { + case (MyType(1)) { + result = 1 + break + } + case (MyType(2)) { + result = 2 + break + } + case (MyType(3)) { + result = 3 + break + } +} + +assert_equal(result, 2) + +// Also test fall-through with custom == operator +var total = 0 +var obj2 = MyType(2) + +switch(obj2) { + case (MyType(1)) { + total += 1 + } + case (MyType(2)) { + total += 2 + } + case (MyType(3)) { + total += 4 + } +} + +assert_equal(total, 6)