From 1334e02900201d5a3ffc6fc239fa80084e0f48c2 Mon Sep 17 00:00:00 2001 From: leftibot Date: Thu, 9 Apr 2026 22:41:34 -0600 Subject: [PATCH] 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) --- .../chaiscript/dispatchkit/dynamic_object.hpp | 27 ++++++++ .../dispatchkit/dynamic_object_detail.hpp | 2 + .../dispatchkit/proxy_functions.hpp | 2 +- .../chaiscript/language/chaiscript_eval.hpp | 61 ++++++++++++++++++- .../chaiscript/language/chaiscript_parser.hpp | 7 +++ unittests/class_inheritance.chai | 60 ++++++++++++++++++ 6 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 unittests/class_inheritance.chai 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..4644530d 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 &get_dynamic_object_type_name() const noexcept { 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); diff --git a/include/chaiscript/dispatchkit/proxy_functions.hpp b/include/chaiscript/dispatchkit/proxy_functions.hpp index a481e2c6..c46be369 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 &) { diff --git a/include/chaiscript/language/chaiscript_eval.hpp b/include/chaiscript/language/chaiscript_eval.hpp index cd171b36..761bec6f 100644 --- a/include/chaiscript/language/chaiscript_eval.hpp +++ b/include/chaiscript/language/chaiscript_eval.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -827,14 +828,70 @@ 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]; + + // Evaluate the class block first (registers derived-specific methods/attrs) + block->eval(t_ss); + + if (has_base_class) { + const auto &base_name = this->children[1]->text; + dispatch::Dynamic_Object::register_inheritance(class_name, base_name); + copy_base_functions(*t_ss, base_name, class_name); + } return void_var(); } + + private: + /// Collect the set of method/attr names already defined for a given dynamic object type + static std::set get_defined_method_names(chaiscript::detail::Dispatch_Engine &t_engine, + const std::string &type_name) { + std::set names; + for (const auto &[func_name, func] : t_engine.get_functions()) { + const auto *dof = dynamic_cast(func.get()); + if (dof && dof->get_dynamic_object_type_name() == type_name) { + names.insert(func_name); + } + } + return names; + } + + /// Copy base class methods and attributes to derived class, skipping overrides + static void copy_base_functions(chaiscript::detail::Dispatch_Engine &t_engine, + const std::string &base_name, + const std::string &derived_name) { + const auto derived_methods = get_defined_method_names(t_engine, derived_name); + const auto functions = t_engine.get_functions(); + + for (const auto &[func_name, func] : functions) { + const auto *dof = dynamic_cast(func.get()); + if (dof && dof->get_dynamic_object_type_name() == base_name) { + // Skip if derived class already defines this method/attr + if (derived_methods.count(func_name) > 0) { + continue; + } + auto contained = dof->get_contained_functions(); + if (!contained.empty()) { + try { + t_engine.add(std::make_shared( + derived_name, + std::const_pointer_cast(contained[0]), + dof->is_attribute_function()), + func_name); + } catch (const chaiscript::exception::name_conflict_error &) { + // already registered, skip + } + } + } + } + } }; template 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..9b222950 --- /dev/null +++ b/unittests/class_inheritance.chai @@ -0,0 +1,60 @@ + +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())