From ef018f57c8119331e03e1b6a2ed1a90570ee920c Mon Sep 17 00:00:00 2001 From: leftibot Date: Sun, 12 Apr 2026 17:03:27 -0600 Subject: [PATCH] Fix #531: Support static member functions via ClassName.func() syntax Add a new overload of utility::add_class that accepts a fifth parameter for static functions. Static functions are registered as attributes of a Dynamic_Object namespace with the class name, enabling ClassName.func() call syntax. To allow constructors to coexist with the namespace global, Fun_Call_AST_Node now falls back to dispatch-by-name when the evaluated identifier is not directly callable. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../chaiscript/language/chaiscript_eval.hpp | 19 +++++--- include/chaiscript/utility/utility.hpp | 20 +++++++++ unittests/compiled_tests.cpp | 45 +++++++++++++++++++ 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/include/chaiscript/language/chaiscript_eval.hpp b/include/chaiscript/language/chaiscript_eval.hpp index 03189fb4..e0fc3daa 100644 --- a/include/chaiscript/language/chaiscript_eval.hpp +++ b/include/chaiscript/language/chaiscript_eval.hpp @@ -324,13 +324,17 @@ namespace chaiscript { false, *t_ss); } catch (const exception::bad_boxed_cast &) { + // Value is not directly callable. Fall back to calling by name through the + // dispatch engine, which handles the case where a global (e.g., class namespace) + // shadows a function (e.g., constructor) with the same name. try { - using ConstFunctionTypeRef = const Const_Proxy_Function &; - Const_Proxy_Function f = t_ss->boxed_cast(fn); - // handle the case where there is only 1 function to try to call and dispatch fails on it - throw exception::eval_error("Error calling function '" + this->children[0]->text + "'", params, make_vector(f), false, *t_ss); - } catch (const exception::bad_boxed_cast &) { - throw exception::eval_error("'" + this->children[0]->pretty_print() + "' does not evaluate to a function."); + return t_ss->call_function(this->children[0]->text, m_loc, Function_Params{params}, t_ss.conversions()); + } catch (const exception::dispatch_error &e) { + throw exception::eval_error(std::string(e.what()) + " with function '" + this->children[0]->text + "'", + e.parameters, + e.functions, + false, + *t_ss); } } catch (const exception::arity_error &e) { throw exception::eval_error(std::string(e.what()) + " with function '" + this->children[0]->text + "'"); @@ -342,6 +346,9 @@ namespace chaiscript { } Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &t_ss) const override { return do_eval_internal(t_ss); } + + private: + mutable std::atomic_uint_fast32_t m_loc = {0}; }; template diff --git a/include/chaiscript/utility/utility.hpp b/include/chaiscript/utility/utility.hpp index 22a1ff01..fa149627 100644 --- a/include/chaiscript/utility/utility.hpp +++ b/include/chaiscript/utility/utility.hpp @@ -15,6 +15,7 @@ #include #include +#include "../dispatchkit/dynamic_object.hpp" #include "../dispatchkit/operators.hpp" #include "../dispatchkit/register_function.hpp" #include "../language/chaiscript_common.hpp" @@ -59,6 +60,25 @@ namespace chaiscript::utility { } } + /// Overload of add_class that also registers static functions accessible via ClassName.func() syntax. + /// Static functions are exposed as attributes of a namespace object with the class name. + template + void add_class(ModuleType &t_module, + const std::string &t_class_name, + const std::vector &t_constructors, + const std::vector> &t_funcs, + const std::vector> &t_static_funcs) { + add_class(t_module, t_class_name, t_constructors, t_funcs); + + if (!t_static_funcs.empty()) { + dispatch::Dynamic_Object ns(t_class_name); + for (const auto &sf : t_static_funcs) { + ns.get_attr(sf.second) = chaiscript::Boxed_Value(sf.first); + } + t_module.add_global_const(chaiscript::const_var(ns), t_class_name); + } + } + template typename std::enable_if::value, void>::type add_class(ModuleType &t_module, diff --git a/unittests/compiled_tests.cpp b/unittests/compiled_tests.cpp index 7b601b15..c590579b 100644 --- a/unittests/compiled_tests.cpp +++ b/unittests/compiled_tests.cpp @@ -556,6 +556,51 @@ TEST_CASE("Utility_Test utility class wrapper") { chai.eval("t = Utility_Test();"); } +class Utility_Test_Static { +public: + Utility_Test_Static(int t_x, int t_y) : m_x(t_x), m_y(t_y) {} + Utility_Test_Static() : m_x(0), m_y(0) {} + + int area() const { return m_x * m_y; } + + static int static_area(int t_x, int t_y) { return t_x * t_y; } + static std::string name() { return "Utility_Test_Static"; } + + int m_x; + int m_y; +}; + +TEST_CASE("Utility_Test add_class with static functions") { + auto m = std::make_shared(); + + using namespace chaiscript; + + chaiscript::utility::add_class( + *m, + "UTS", + {constructor(), constructor(), + constructor()}, + {{fun(&Utility_Test_Static::area), "area"}, + {fun(&Utility_Test_Static::m_x), "x"}, + {fun(&Utility_Test_Static::m_y), "y"}, + {fun(static_cast(&Utility_Test_Static::operator=)), "="}}, + {{fun(&Utility_Test_Static::static_area), "area"}, + {fun(&Utility_Test_Static::name), "name"}}); + + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + chai.add(m); + + // Instance methods still work + CHECK(chai.eval("auto t = UTS(3, 4); t.area()") == 12); + + // Static functions accessible via namespace dot syntax + CHECK(chai.eval("UTS.area(5, 6)") == 30); + CHECK(chai.eval("UTS.name()") == "Utility_Test_Static"); + + // Constructor still works alongside the namespace + CHECK(chai.eval("auto t2 = UTS(7, 8); t2.x") == 7); +} + enum Utility_Test_Numbers { ONE, TWO,