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) <noreply@anthropic.com>
This commit is contained in:
leftibot 2026-04-12 17:03:27 -06:00
parent 5a6050210d
commit ef018f57c8
3 changed files with 78 additions and 6 deletions

View File

@ -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<ConstFunctionTypeRef>(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<true>(t_ss); }
private:
mutable std::atomic_uint_fast32_t m_loc = {0};
};
template<typename T>

View File

@ -15,6 +15,7 @@
#include <utility>
#include <vector>
#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<typename Class, typename ModuleType>
void add_class(ModuleType &t_module,
const std::string &t_class_name,
const std::vector<chaiscript::Proxy_Function> &t_constructors,
const std::vector<std::pair<chaiscript::Proxy_Function, std::string>> &t_funcs,
const std::vector<std::pair<chaiscript::Proxy_Function, std::string>> &t_static_funcs) {
add_class<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 Enum, typename ModuleType>
typename std::enable_if<std::is_enum<Enum>::value, void>::type
add_class(ModuleType &t_module,

View File

@ -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<chaiscript::Module>();
using namespace chaiscript;
chaiscript::utility::add_class<Utility_Test_Static>(
*m,
"UTS",
{constructor<Utility_Test_Static()>(), constructor<Utility_Test_Static(int, int)>(),
constructor<Utility_Test_Static(const Utility_Test_Static &)>()},
{{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 &(Utility_Test_Static::*)(const Utility_Test_Static &)>(&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<int>("auto t = UTS(3, 4); t.area()") == 12);
// Static functions accessible via namespace dot syntax
CHECK(chai.eval<int>("UTS.area(5, 6)") == 30);
CHECK(chai.eval<std::string>("UTS.name()") == "Utility_Test_Static");
// Constructor still works alongside the namespace
CHECK(chai.eval<int>("auto t2 = UTS(7, 8); t2.x") == 7);
}
enum Utility_Test_Numbers {
ONE,
TWO,