From 69476967aeaa245c452a3b3b92b9ba3c571bb958 Mon Sep 17 00:00:00 2001 From: Glen Fraser Date: Fri, 18 Jun 2021 16:40:38 +0200 Subject: [PATCH 1/7] Reimplement pair_conversion() helper - resolves issue #563. --- .../dispatchkit/type_conversions.hpp | 18 ++++++++++++++++ unittests/compiled_tests.cpp | 21 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/include/chaiscript/dispatchkit/type_conversions.hpp b/include/chaiscript/dispatchkit/type_conversions.hpp index e47afbf7..3f89e66b 100644 --- a/include/chaiscript/dispatchkit/type_conversions.hpp +++ b/include/chaiscript/dispatchkit/type_conversions.hpp @@ -534,6 +534,24 @@ namespace chaiscript { return chaiscript::make_shared>( user_type>(), user_type(), func); } + + template + Type_Conversion pair_conversion() { + auto func = [](const Boxed_Value &t_bv) -> Boxed_Value { + const std::pair &from_pair + = detail::Cast_Helper &>::cast(t_bv, nullptr); + + auto pair = std::make_pair( + detail::Cast_Helper::cast(from_pair.first, nullptr), + detail::Cast_Helper::cast(from_pair.second, nullptr) + ); + + return Boxed_Value(std::move(pair)); + }; + + return chaiscript::make_shared>( + user_type>(), user_type>(), func); + } } // namespace chaiscript #endif diff --git a/unittests/compiled_tests.cpp b/unittests/compiled_tests.cpp index f8873df8..29f49875 100644 --- a/unittests/compiled_tests.cpp +++ b/unittests/compiled_tests.cpp @@ -866,6 +866,27 @@ TEST_CASE("Map conversions") { CHECK(c == 42); } +TEST_CASE("Pair conversions") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + chai.add(chaiscript::pair_conversion()); + chai.add(chaiscript::pair_conversion()); + + { + const auto p = chai.eval>(R"cs( + Pair("chai", "script"); + )cs"); + CHECK(p.first == std::string{ "chai" }); + CHECK(p.second == "script"); + } + { + const auto p = chai.eval>(R"cs( + Pair(5, 3.14); + )cs"); + CHECK(p.first == 5); + CHECK(p.second == Approx(3.14)); + } +} + TEST_CASE("Parse floats with non-posix locale") { #ifdef CHAISCRIPT_MSVC std::setlocale(LC_ALL, "en-ZA"); From 84507f59ef3b9bad0d8d99bc275671e704208e96 Mon Sep 17 00:00:00 2001 From: leftibot Date: Thu, 9 Apr 2026 21:43:56 -0600 Subject: [PATCH 2/7] Fix #12: Document reflection and introspection capabilities Add comprehensive reflection documentation to cheatsheet.md covering type inspection, object methods, Type_Info, function introspection, system introspection, and Dynamic_Object reflection. Add missing global reflection functions (type_name, is_type, function_exists, get_functions, get_objects, type, dump_system, dump_object) and is_type_arithmetic to the Doxygen prelude docs. Include a thorough test exercising all documented features. Co-Authored-By: Claude Opus 4.6 (1M context) --- cheatsheet.md | 120 +++++++++++++++++ .../language/chaiscript_prelude_docs.hpp | 87 +++++++++++++ unittests/reflection_documentation.chai | 123 ++++++++++++++++++ 3 files changed, 330 insertions(+) create mode 100644 unittests/reflection_documentation.chai diff --git a/cheatsheet.md b/cheatsheet.md index 5835c36b..994fbd9d 100644 --- a/cheatsheet.md +++ b/cheatsheet.md @@ -593,6 +593,126 @@ use("filename") // evals file exactly once and returns value of last statement Both `use` and `eval_file` search the 'usepaths' passed to the ChaiScript constructor +## Reflection and Introspection + +ChaiScript provides built-in reflection capabilities for inspecting types, functions, and objects at runtime. + +### Type Inspection + +``` +type_name(x) // returns the type name of a value as a string +is_type(x, "typename") // returns true if x is of the named type +type("typename") // returns a Type_Info object for the named type + +// Examples +type_name(1) // "int" +type_name("hello") // "string" +is_type(1, "int") // true +is_type(1, "string") // false +``` + +### Object Inspection Methods + +Every object in ChaiScript supports these methods: + +``` +x.get_type_info() // returns a Type_Info object for the value +x.is_type("string") // returns true if x is of the named type +x.is_type(string_type) // returns true if x matches the Type_Info +x.is_var_const() // returns true if x is immutable +x.is_var_null() // returns true if x is a null pointer +x.is_var_pointer() // returns true if x is stored as a pointer +x.is_var_reference() // returns true if x is stored as a reference +x.is_var_undef() // returns true if x is undefined +``` + +### Type_Info + +`Type_Info` objects describe a type. You can get them via `type("typename")` or `x.get_type_info()`. + +``` +var ti = type("int") +ti.name() // ChaiScript registered name, e.g. "int" +ti.cpp_name() // mangled C++ type name +ti.cpp_bare_name() // C++ name without const/pointer/reference +ti.bare_equal(other) // true if types match ignoring const/ptr/ref +ti.is_type_const() // true if type is const +ti.is_type_reference() // true if type is a reference +ti.is_type_void() // true if type is void +ti.is_type_undef() // true if type is undefined +ti.is_type_pointer() // true if type is a pointer +ti.is_type_arithmetic() // true if type is arithmetic (int, double, etc.) +``` + +Built-in type constants are available: `int_type`, `double_type`, `string_type`, `bool_type`, `Object_type`, `Function_type`, `vector_type`, `map_type`. + +### Function Introspection + +Function objects support these introspection methods: + +``` +f.get_arity() // number of parameters (-1 for variadic) +f.get_param_types() // Vector of Type_Info (first element is return type) +f.get_contained_functions() // Vector of overloaded functions (empty if not a conglomerate) +f.has_guard() // true if the function has a guard condition +f.get_guard() // returns the guard function (throws if none) +f.get_annotation() // returns the annotation description +f.call([param1, param2]) // call the function with a vector of parameters + +// Examples +def my_func(a, b) { return a + b; } +my_func.get_arity() // 2 +my_func.has_guard() // false + +def guarded(x) : x > 0 { return x; } +guarded.has_guard() // true +guarded.get_guard().get_arity() // 1 + +// Calling functions dynamically +`+`.call([1, 2]) // 3 +``` + +### System Introspection + +``` +get_functions() // returns a Map of all registered functions (name -> function) +get_objects() // returns a Map of all scripting objects (name -> value) +function_exists("f") // returns true if a function named "f" is registered +call_exists(`f`, args) // returns true if f can be called with the given args +dump_system() // prints all registered functions to stdout +dump_object(x) // prints information about a value to stdout + +// Examples +var funcs = get_functions() +funcs["print"] // the print function object +function_exists("print") // true +call_exists(`+`, 1, 2) // true +``` + +### Dynamic_Object Reflection + +ChaiScript-defined classes are Dynamic_Objects internally. They support: + +``` +obj.get_type_name() // returns the ChaiScript class name (e.g. "MyClass") +obj.get_attrs() // returns a Map of all attributes +obj.has_attr("name") // returns true if the attribute exists +obj.get_attr("name") // returns the value of the attribute +obj.set_explicit(true) // disables dynamic attribute creation +obj.is_explicit() // returns true if explicit mode is enabled + +// Example +class MyClass { + var x + def MyClass() { this.x = 10; } +} +var m = MyClass() +m.get_type_name() // "MyClass" +m.get_attrs() // map containing "x" -> 10 +type_name(m) // "Dynamic_Object" (the underlying C++ type) +m.is_type("MyClass") // true (checks the ChaiScript class name) +``` + ## JSON * `from_json` converts a JSON string into its strongly typed (map, vector, int, double, string) representations diff --git a/include/chaiscript/language/chaiscript_prelude_docs.hpp b/include/chaiscript/language/chaiscript_prelude_docs.hpp index c7fe40c7..6dc13797 100644 --- a/include/chaiscript/language/chaiscript_prelude_docs.hpp +++ b/include/chaiscript/language/chaiscript_prelude_docs.hpp @@ -426,6 +426,9 @@ namespace ChaiScript_Language { /// \brief Returns true if the type is "void" bool is_type_void() const; + /// \brief Returns true if the type is an arithmetic type (int, double, etc.) + bool is_type_arithmetic() const; + /// \brief Returns the ChaiScript registered name for the type if one exists. string name() const; }; @@ -764,6 +767,90 @@ namespace ChaiScript_Language { /// \endcode bool call_exists(Function f, ...); + /// \brief Returns the type name of the given object as a string + /// + /// Example: + /// \code + /// eval> type_name(1) + /// int + /// eval> type_name("hello") + /// string + /// \endcode + /// + /// \sa Type_Info::name() + /// \sa Object::get_type_info() + string type_name(Object o); + + /// \brief Returns true if the object is of the named type + /// + /// Example: + /// \code + /// eval> is_type(1, "int") + /// true + /// eval> is_type(1, "string") + /// false + /// \endcode + /// + /// \sa Object::is_type() + bool is_type(Object o, string type_name); + + /// \brief Returns true if a function with the given name is registered + /// + /// Example: + /// \code + /// eval> function_exists("print") + /// true + /// eval> function_exists("nonexistent") + /// false + /// \endcode + bool function_exists(string name); + + /// \brief Returns a Map of all registered functions, keyed by function name + /// + /// Example: + /// \code + /// eval> var funcs = get_functions() + /// eval> funcs["print"].get_arity() + /// 1 + /// \endcode + /// + /// \sa Function + Map get_functions(); + + /// \brief Returns a Map of all scripting objects (variables), keyed by name + /// + /// Example: + /// \code + /// eval> var x = 42 + /// eval> var objs = get_objects() + /// eval> objs.count("x") + /// 1 + /// \endcode + Map get_objects(); + + /// \brief Returns a Type_Info for the named type + /// + /// Example: + /// \code + /// eval> type("int").name() + /// int + /// \endcode + /// + /// \param type_name The name of the type to look up + /// \param throw_on_fail If true (default), throws if the type is not found + /// \sa Type_Info + Type_Info type(string type_name, bool throw_on_fail = true); + + /// \brief Prints all registered functions to stdout + /// + /// Useful for debugging. Outputs a list of all functions registered in the system. + void dump_system(); + + /// \brief Prints information about the given object to stdout + /// + /// Useful for debugging. Outputs the type and value of the object. + void dump_object(Object o); + /// \brief Reverses a Range object so that the elements are accessed in reverse Range retro(Range); diff --git a/unittests/reflection_documentation.chai b/unittests/reflection_documentation.chai new file mode 100644 index 00000000..db89dc2c --- /dev/null +++ b/unittests/reflection_documentation.chai @@ -0,0 +1,123 @@ + +// Tests for ChaiScript reflection / introspection capabilities +// Ensures all documented reflection functions work correctly + +// --- Global reflection functions --- + +// type_name: returns the type name of a value +assert_equal("int", type_name(1)) +assert_equal("string", type_name("hello")) +assert_equal("bool", type_name(true)) +assert_equal("double", type_name(1.0)) + +// is_type: checks if a value is of the given type +assert_true(is_type(1, "int")) +assert_true(is_type("hello", "string")) +assert_false(is_type(1, "string")) + +// function_exists: checks if a named function is registered +assert_true(function_exists("print")) +assert_true(function_exists("type_name")) +assert_true(function_exists("function_exists")) +assert_false(function_exists("this_function_does_not_exist_xyz")) + +// get_functions: returns a Map of all registered functions +var funcs = get_functions() +assert_true(funcs.size() > 0) +assert_true(funcs.count("print") > 0) +assert_true(funcs.count("type_name") > 0) + +// get_objects: returns a Map of all scripting objects +var my_test_var = 42 +var objs = get_objects() +assert_true(objs.size() > 0) +assert_true(objs.count("my_test_var") > 0) + +// type: returns a Type_Info for a named type +var ti = type("int") +assert_equal("int", ti.name()) + +// call_exists: checks if a function call with given params exists +assert_true(call_exists(`+`, 1, 2)) +assert_true(call_exists(`+`, "a", "b")) + +// --- Object methods --- + +// get_type_info: returns Type_Info for a value +var s = "hello" +assert_equal("string", s.get_type_info().name()) +assert_equal("int", 1.get_type_info().name()) + +// is_type on objects +assert_true("hello".is_type("string")) +assert_true(1.is_type("int")) +assert_false(1.is_type("string")) + +// is_type with Type_Info +assert_true("hello".is_type(string_type)) +assert_true(1.is_type(int_type)) + +// is_var_* methods +var x = 5 +assert_false(x.is_var_const()) +assert_false(x.is_var_null()) +assert_false(x.is_var_undef()) + +// --- Type_Info methods --- +var int_ti = type("int") +assert_equal("int", int_ti.name()) +assert_false(int_ti.is_type_const()) +assert_false(int_ti.is_type_void()) +assert_false(int_ti.is_type_undef()) +assert_false(int_ti.is_type_reference()) +assert_false(int_ti.is_type_pointer()) + +// bare_equal: compares types ignoring const/pointer/reference +assert_true(int_ti.bare_equal(1.get_type_info())) + +// --- Function introspection --- + +def my_reflection_test_func(a, b) { return a + b; } + +// get_arity +assert_equal(2, my_reflection_test_func.get_arity()) + +// get_param_types +var param_types = my_reflection_test_func.get_param_types() +assert_true(param_types.size() > 0) + +// get_contained_functions +assert_equal(0, my_reflection_test_func.get_contained_functions().size()) + +// has_guard +assert_false(my_reflection_test_func.has_guard()) + +// Guarded function +def my_guarded_func(x) : x > 0 { return x; } +assert_true(my_guarded_func.has_guard()) +var g = my_guarded_func.get_guard() +assert_equal(1, g.get_arity()) + +// call: invoke a function with a vector of parameters +assert_equal(3, `+`.call([1, 2])) + +// --- Dynamic_Object reflection --- +var obj = Dynamic_Object() +obj.name = "test" +obj.value = 42 +var attrs = obj.get_attrs() +assert_true(attrs.count("name") > 0) +assert_true(attrs.count("value") > 0) + +// --- Class reflection --- +class ReflectionTestClass { + var x + def ReflectionTestClass() { this.x = 10; } + def get_x() { return this.x; } +} + +var rtc = ReflectionTestClass() +assert_equal("ReflectionTestClass", rtc.get_type_name()) +assert_true(rtc.is_type("ReflectionTestClass")) +assert_equal("Dynamic_Object", type_name(rtc)) +assert_equal("ReflectionTestClass", rtc.get_type_name()) From 28894373f7a533a6c07cbf80d9050b47247a7d6d Mon Sep 17 00:00:00 2001 From: leftibot Date: Thu, 9 Apr 2026 22:15:47 -0600 Subject: [PATCH 3/7] Fix #591: Add comprehensive class definition docs to cheatsheet The cheatsheet's "ChaiScript Defined Types" section was minimal, showing only a basic class with one attribute and a getter. Expanded it to document block syntax, open syntax, attribute keywords (var/attr/auto), constructor and method guards, operator overloading, cloning, and added a test exercising all examples. Co-Authored-By: Claude Opus 4.6 (1M context) --- cheatsheet.md | 105 +++++++++++++++++++++++++++----- unittests/class_cheatsheet.chai | 103 +++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 15 deletions(-) create mode 100644 unittests/class_cheatsheet.chai diff --git a/cheatsheet.md b/cheatsheet.md index 5835c36b..8c7d224c 100644 --- a/cheatsheet.md +++ b/cheatsheet.md @@ -477,34 +477,109 @@ n(2); // returns 20 -## ChaiScript Defined Types +## ChaiScript Defined Types (Classes) -Define a type called "MyType" with one member value "a" and a getter +ChaiScript supports user-defined types using the `class` keyword. Classes can have attributes, +constructors, methods, guards, and operator overloads. There is no inheritance between +ChaiScript-defined types, but C++ class hierarchies can be exposed (see *Class Hierarchies* above). -### Preferred +### Class Definition (Block Syntax) + +Define a type with attributes, a constructor, and methods inside a `class` block. +The keywords `var`, `attr`, and `auto` are interchangeable for declaring attributes. ``` -class MyType { - var value; - def MyType() { this.value = "a"; } - def get_value() { "Value Is: " + this.value; } -}; +class Rectangle { + var width + var height + def Rectangle(w, h) { this.width = w; this.height = h; } + def Rectangle() { this.width = 0; this.height = 0; } + def area() { this.width * this.height; } +} + +var r = Rectangle(3, 4) +print(r.area()) // prints 12 ``` -### Alternative +### Class Definition (Open Syntax) + +Equivalently, attributes and methods can be defined outside a block using the `TypeName::` prefix. ``` -attr MyType::value; -def MyType::MyType() { this.value = "a"; } -def MyType::get_value() { "Value Is: " + this.value; } +attr Circle::radius +def Circle::Circle(r) { this.radius = r; } +def Circle::circumference() { 2.0 * 3.14159 * this.radius; } +``` + +Methods can also be added to an existing class after its initial definition: + +``` +def Rectangle::perimeter() { 2 * (this.width + this.height); } ``` ### Using ``` -var m = MyType(); // calls constructor -print(m.get_value()); // prints "Value Is: a" -print(get_value(m)); // prints "Value Is: a" +var m = Rectangle(5, 10) +print(m.area()) // prints 50 — method call syntax +print(area(m)) // prints 50 — function call syntax (equivalent) +``` + +### Constructor and Method Guards + +Constructors and methods can have guard expressions (after `:`) that control which +overload is selected at call time. + +``` +class Clamped { + var value + def Clamped(x) : x >= 0 { this.value = x; } + def Clamped(x) { this.value = 0; } // fallback when guard fails +} + +Clamped(5).value // 5 +Clamped(-3).value // 0 + +class Abs { + var x + def Abs(v) { this.x = v; } + def get() : this.x >= 0 { this.x; } + def get() { -this.x; } +} +``` + +### Operator Overloading + +Operators can be overloaded on user-defined types using backtick-quoted operator names. + +``` +class Vec2 { + var x + var y + def Vec2(x, y) { this.x = x; this.y = y; } + def `+`(other) { Vec2(this.x + other.x, this.y + other.y); } +} + +var v = Vec2(1, 2) + Vec2(3, 4) // v.x == 4, v.y == 6 +``` + +Operators can also be overloaded as free functions with guards: + +``` +def `-`(a, b) : is_type(a, "Vec2") && is_type(b, "Vec2") { + Vec2(a.x - b.x, a.y - b.y) +} +``` + +### Cloning Objects + +Use `clone()` to create a deep copy of a ChaiScript-defined object. + +``` +var original = Rectangle(10, 20) +var copy = clone(original) +copy.width = 99 +print(original.width) // still 10 ``` ## Dynamic Objects diff --git a/unittests/class_cheatsheet.chai b/unittests/class_cheatsheet.chai new file mode 100644 index 00000000..2a6d92e1 --- /dev/null +++ b/unittests/class_cheatsheet.chai @@ -0,0 +1,103 @@ +// Test for class features documented in the cheatsheet (issue #591) + +// --- Class definition (preferred block syntax) --- +class Rectangle { + var width + var height + def Rectangle(w, h) { this.width = w; this.height = h; } + def Rectangle() { this.width = 0; this.height = 0; } + def area() { this.width * this.height; } +} + +auto r = Rectangle(3, 4) +assert_equal(12, r.area()) + +// --- Default constructor --- +auto r2 = Rectangle() +assert_equal(0, r2.area()) + +// --- Alternative open syntax --- +attr Circle::radius +def Circle::Circle(r) { this.radius = r; } +def Circle::circumference() { 2.0 * 3.14159 * this.radius; } + +auto c = Circle(5.0) +assert_equal(5.0, c.radius) + +// --- attr, auto, var all work for attributes --- +class AttrTest { + attr a + auto b + var c + def AttrTest() { this.a = 1; this.b = 2; this.c = 3; } +} + +auto at = AttrTest() +assert_equal(1, at.a) +assert_equal(2, at.b) +assert_equal(3, at.c) + +// --- Constructor guards --- +class Clamped { + var value + def Clamped(x) : x >= 0 { this.value = x; } + def Clamped(x) { this.value = 0; } +} + +assert_equal(5, Clamped(5).value) +assert_equal(0, Clamped(-3).value) + +// --- Method guards --- +class Abs { + var x + def Abs(v) { this.x = v; } + def get() : this.x >= 0 { this.x; } + def get() { -this.x; } +} + +assert_equal(5, Abs(5).get()) +assert_equal(3, Abs(-3).get()) + +// --- Operator overloading --- +class Vec2 { + var x + var y + def Vec2(x, y) { this.x = x; this.y = y; } + def `+`(other) { Vec2(this.x + other.x, this.y + other.y); } +} + +auto v = Vec2(1, 2) + Vec2(3, 4) +assert_equal(4, v.x) +assert_equal(6, v.y) + +// --- Cloning objects --- +auto r3 = Rectangle(10, 20) +auto r4 = clone(r3) +r4.width = 99 +assert_equal(10, r3.width) +assert_equal(99, r4.width) + +// --- Dynamic attributes --- +auto r5 = Rectangle(1, 1) +r5.color = "red" +assert_equal("red", r5.color) + +// --- Explicit mode --- +class Strict { + var x + def Strict() { + this.x = 0 + this.set_explicit(true) + } +} + +auto s = Strict() +assert_equal(0, s.x) +assert_equal(true, s.is_explicit()) + +try { + s.y = 10 + assert_equal(true, false) // should not reach here +} catch(e) { + // expected: cannot add dynamic attribute in explicit mode +} From 25c520ed81ae51d971c782ff4490abcf4f59b81f Mon Sep 17 00:00:00 2001 From: leftibot Date: Fri, 10 Apr 2026 11:31:59 -0600 Subject: [PATCH 4/7] Address review: remove issue number references from code Requested by @lefticus in PR #639 review. Co-Authored-By: Claude Opus 4.6 (1M context) --- unittests/class_cheatsheet.chai | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unittests/class_cheatsheet.chai b/unittests/class_cheatsheet.chai index 2a6d92e1..476f1c49 100644 --- a/unittests/class_cheatsheet.chai +++ b/unittests/class_cheatsheet.chai @@ -1,4 +1,4 @@ -// Test for class features documented in the cheatsheet (issue #591) +// Test for class features documented in the cheatsheet // --- Class definition (preferred block syntax) --- class Rectangle { From 78d2de0ccf3565376bee7feb126f583084669213 Mon Sep 17 00:00:00 2001 From: dinghram Date: Fri, 10 Apr 2026 18:18:17 -0600 Subject: [PATCH 5/7] 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 6/7] 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) From f59eff9b2f45fc54c0b1c1c313ecbf90a884b220 Mon Sep 17 00:00:00 2001 From: leftibot Date: Fri, 10 Apr 2026 19:12:06 -0600 Subject: [PATCH 7/7] Fix #201: Suggestion: class Inheritance (#641) * Fix #201: Add class inheritance support with Derived : Base syntax Classes can now inherit methods and attributes from a base class using C++-style syntax: `class Derived : Base { ... }`. Base class methods and attributes are automatically available on derived objects. Derived classes can override base methods by defining a method with the same name. Inheritance relationships are tracked to support proper type matching in the dispatch system. Co-Authored-By: Claude Opus 4.6 (1M context) * Address review: use implicit derived-to-base matching instead of copying base class functions Instead of copying all base class methods/attributes into derived classes, make the type matching system recognize inheritance relationships. Base class methods now naturally match derived objects through dynamic_object_typename_match, and dispatch ordering ensures derived overrides are preferred over base methods. This is simpler (net -25 lines) and avoids duplicating function registrations. Requested by @lefticus in PR #641 review. Co-Authored-By: Claude Opus 4.6 (1M context) * Add tests for passing derived objects to functions expecting Base Tests cover: free functions calling base methods on derived objects, polymorphic dispatch through containers, base attribute access on derived objects, and multi-level inheritance (GrandChild : Derived : Base). Co-Authored-By: Claude Opus 4.6 (1M context) * Add typed parameter tests for class inheritance Use typed function signatures (e.g., `def call_do_something(Base obj)`) instead of untyped parameters to test that derived objects are accepted by functions expecting a base type, with correct polymorphic dispatch. Requested by @lefticus in PR #641 review. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: leftibot Co-authored-by: Claude Opus 4.6 (1M context) --- .../chaiscript/dispatchkit/dispatchkit.hpp | 13 ++ .../chaiscript/dispatchkit/dynamic_object.hpp | 27 ++++ .../dispatchkit/dynamic_object_detail.hpp | 4 +- .../dispatchkit/proxy_functions.hpp | 8 +- .../chaiscript/language/chaiscript_eval.hpp | 16 +- .../chaiscript/language/chaiscript_parser.hpp | 7 + unittests/class_inheritance.chai | 137 ++++++++++++++++++ 7 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 unittests/class_inheritance.chai diff --git a/include/chaiscript/dispatchkit/dispatchkit.hpp b/include/chaiscript/dispatchkit/dispatchkit.hpp index 06e8992a..b54d42b4 100644 --- a/include/chaiscript/dispatchkit/dispatchkit.hpp +++ b/include/chaiscript/dispatchkit/dispatchkit.hpp @@ -1060,6 +1060,19 @@ namespace chaiscript { return true; } + // Sort more-derived Dynamic_Object types before base types so that + // overridden methods in derived classes are tried first during dispatch + const auto &lhs_dotn = lhs->dynamic_object_type_name(); + const auto &rhs_dotn = rhs->dynamic_object_type_name(); + if (!lhs_dotn.empty() && !rhs_dotn.empty() && lhs_dotn != rhs_dotn) { + if (dispatch::Dynamic_Object::type_matches(lhs_dotn, rhs_dotn)) { + return true; // lhs is derived from rhs, so lhs is more specific + } + if (dispatch::Dynamic_Object::type_matches(rhs_dotn, lhs_dotn)) { + return false; // rhs is derived from lhs, so rhs is more specific + } + } + const auto &lhsparamtypes = lhs->get_param_types(); const auto &rhsparamtypes = rhs->get_param_types(); diff --git a/include/chaiscript/dispatchkit/dynamic_object.hpp b/include/chaiscript/dispatchkit/dynamic_object.hpp index b242a207..03ae5276 100644 --- a/include/chaiscript/dispatchkit/dynamic_object.hpp +++ b/include/chaiscript/dispatchkit/dynamic_object.hpp @@ -44,6 +44,28 @@ namespace chaiscript { Dynamic_Object() = default; + /// Register that derived_name inherits from base_name + static void register_inheritance(const std::string &derived_name, const std::string &base_name) { + inheritance_map()[derived_name] = base_name; + } + + /// Check if type_name is, or inherits from, base_name + static bool type_matches(const std::string &type_name, const std::string &base_name) noexcept { + if (type_name == base_name) { + return true; + } + const auto &m = inheritance_map(); + auto it = m.find(type_name); + while (it != m.end()) { + if (it->second == base_name) { + return true; + } + it = m.find(it->second); + } + return false; + } + + bool is_explicit() const noexcept { return m_option_explicit; } void set_explicit(const bool t_explicit) noexcept { m_option_explicit = t_explicit; } @@ -87,6 +109,11 @@ namespace chaiscript { std::map get_attrs() const { return m_attrs; } private: + static std::map &inheritance_map() { + static std::map s_map; + return s_map; + } + const std::string m_type_name = ""; bool m_option_explicit = false; diff --git a/include/chaiscript/dispatchkit/dynamic_object_detail.hpp b/include/chaiscript/dispatchkit/dynamic_object_detail.hpp index 07477c1e..1a599d0a 100644 --- a/include/chaiscript/dispatchkit/dynamic_object_detail.hpp +++ b/include/chaiscript/dispatchkit/dynamic_object_detail.hpp @@ -72,6 +72,8 @@ namespace chaiscript { bool is_attribute_function() const noexcept override { return m_is_attribute; } + const std::string &dynamic_object_type_name() const noexcept override { return m_type_name; } + bool call_match(const chaiscript::Function_Params &vals, const Type_Conversions_State &t_conversions) const noexcept override { if (dynamic_object_typename_match(vals, m_type_name, m_ti, t_conversions)) { return m_func->call_match(vals, t_conversions); @@ -112,7 +114,7 @@ namespace chaiscript { if (bv.get_type_info().bare_equal(m_doti)) { try { const Dynamic_Object &d = boxed_cast(bv, &t_conversions); - return name == "Dynamic_Object" || d.get_type_name() == name; + return name == "Dynamic_Object" || Dynamic_Object::type_matches(d.get_type_name(), name); } catch (const std::bad_cast &) { return false; } diff --git a/include/chaiscript/dispatchkit/proxy_functions.hpp b/include/chaiscript/dispatchkit/proxy_functions.hpp index a481e2c6..fe65ee5a 100644 --- a/include/chaiscript/dispatchkit/proxy_functions.hpp +++ b/include/chaiscript/dispatchkit/proxy_functions.hpp @@ -120,7 +120,7 @@ namespace chaiscript { if (bv.get_type_info().bare_equal(dynamic_object_type_info)) { try { const Dynamic_Object &d = boxed_cast(bv, &t_conversions); - if (!(name == "Dynamic_Object" || d.get_type_name() == name)) { + if (!(name == "Dynamic_Object" || Dynamic_Object::type_matches(d.get_type_name(), name))) { return std::make_pair(false, false); } } catch (const std::bad_cast &) { @@ -233,6 +233,12 @@ namespace chaiscript { } } + /// Returns the Dynamic_Object type name this function is bound to, or empty string if not a Dynamic_Object function + virtual const std::string &dynamic_object_type_name() const noexcept { + static const std::string empty; + return empty; + } + virtual bool compare_first_type(const Boxed_Value &bv, const Type_Conversions_State &t_conversions) const noexcept { /// TODO is m_types guaranteed to be at least 2?? return compare_type_to_param(m_types[1], bv, t_conversions); diff --git a/include/chaiscript/language/chaiscript_eval.hpp b/include/chaiscript/language/chaiscript_eval.hpp index 55f7d440..34eaba13 100644 --- a/include/chaiscript/language/chaiscript_eval.hpp +++ b/include/chaiscript/language/chaiscript_eval.hpp @@ -828,11 +828,23 @@ namespace chaiscript { Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &t_ss) const override { chaiscript::eval::detail::Scope_Push_Pop spp(t_ss); + const auto &class_name = this->children[0]->text; + /// \todo do this better // put class name in current scope so it can be looked up by the attrs and methods - t_ss.add_object("_current_class_name", const_var(this->children[0]->text)); + t_ss.add_object("_current_class_name", const_var(class_name)); - this->children[1]->eval(t_ss); + const bool has_base_class = (this->children.size() == 3); + const auto &block = has_base_class ? this->children[2] : this->children[1]; + + // Register inheritance before evaluating the class body so that + // function dispatch ordering can account for the relationship + if (has_base_class) { + const auto &base_name = this->children[1]->text; + dispatch::Dynamic_Object::register_inheritance(class_name, base_name); + } + + block->eval(t_ss); return void_var(); } diff --git a/include/chaiscript/language/chaiscript_parser.hpp b/include/chaiscript/language/chaiscript_parser.hpp index df64f9b9..e57e0fa2 100644 --- a/include/chaiscript/language/chaiscript_parser.hpp +++ b/include/chaiscript/language/chaiscript_parser.hpp @@ -1908,6 +1908,13 @@ namespace chaiscript { const auto class_name = m_match_stack.back()->text; + // Optionally parse ': BaseClassName' for inheritance + if (Char(':')) { + if (!Id(true)) { + throw exception::eval_error("Missing base class name in definition", File_Position(m_position.line, m_position.col), *m_filename); + } + } + while (Eol()) { } diff --git a/unittests/class_inheritance.chai b/unittests/class_inheritance.chai new file mode 100644 index 00000000..cd27fa8e --- /dev/null +++ b/unittests/class_inheritance.chai @@ -0,0 +1,137 @@ + +class Base +{ + attr x + + def Base() + { + this.x = 10 + } + + def do_something() + { + return this.x * 2 + } +} + +class Derived : Base +{ + attr y + + def Derived() + { + this.x = 10 + this.y = 20 + } + + def do_other() + { + return this.y * 3 + } +} + +// Test basic inheritance - derived can call base methods +auto d = Derived() +assert_equal(10, d.x) +assert_equal(20, d.y) +assert_equal(20, d.do_something()) +assert_equal(60, d.do_other()) + +// Test base class still works independently +auto b = Base() +assert_equal(10, b.x) +assert_equal(20, b.do_something()) + +// Test method override +class Derived2 : Base +{ + def Derived2() + { + this.x = 5 + } + + def do_something() + { + return this.x * 100 + } +} + +auto d2 = Derived2() +assert_equal(500, d2.do_something()) + +// Test passing a derived object to an untyped free function +def call_do_something_untyped(obj) { + return obj.do_something() +} + +auto d3 = Derived() +assert_equal(20, call_do_something_untyped(d3)) +assert_equal(20, call_do_something_untyped(Base())) + +// Test typed functions: parameter declared as Base, accepts derived objects +def call_do_something(Base obj) { + return obj.do_something() +} + +assert_equal(20, call_do_something(Base())) +assert_equal(20, call_do_something(d3)) + +// Test typed function accessing base attributes on a derived object +def get_x(Base obj) { + return obj.x +} + +assert_equal(10, get_x(d3)) +assert_equal(10, get_x(Base())) + +// Test polymorphic dispatch through typed function: derived override is called +auto d4 = Derived2() +assert_equal(500, call_do_something(d4)) + +// Test mixing base and derived in a container, calling base methods +var objects = [Base(), Derived(), Derived2()] +assert_equal(20, objects[0].do_something()) +assert_equal(20, objects[1].do_something()) +assert_equal(500, objects[2].do_something()) + +// Test that derived objects still report correct type +auto d5 = Derived() +assert_true(d5.is_type("Derived")) + +// Test multi-level inheritance +class GrandChild : Derived +{ + attr z + + def GrandChild() + { + this.x = 1 + this.y = 2 + this.z = 3 + } + + def do_grandchild() + { + return this.z * 4 + } +} + +auto gc = GrandChild() +assert_equal(1, gc.x) +assert_equal(2, gc.y) +assert_equal(3, gc.z) +assert_equal(2, gc.do_something()) // Base method +assert_equal(6, gc.do_other()) // Derived method +assert_equal(12, gc.do_grandchild()) // Own method + +// Test passing grandchild to typed Base function (multi-level inheritance) +assert_equal(2, call_do_something(gc)) +assert_equal(1, get_x(gc)) + +// Test typed function expecting mid-level type +def call_do_other(Derived obj) { + return obj.do_other() +} + +assert_equal(6, call_do_other(gc)) +assert_equal(60, call_do_other(Derived()))