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()))