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) <noreply@anthropic.com>
This commit is contained in:
leftibot 2026-04-09 22:41:34 -06:00
parent 2eb3279c39
commit 1334e02900
6 changed files with 156 additions and 3 deletions

View File

@ -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<std::string, Boxed_Value> get_attrs() const { return m_attrs; }
private:
static std::map<std::string, std::string> &inheritance_map() {
static std::map<std::string, std::string> s_map;
return s_map;
}
const std::string m_type_name = "";
bool m_option_explicit = false;

View File

@ -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);

View File

@ -120,7 +120,7 @@ namespace chaiscript {
if (bv.get_type_info().bare_equal(dynamic_object_type_info)) {
try {
const Dynamic_Object &d = boxed_cast<const Dynamic_Object &>(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 &) {

View File

@ -16,6 +16,7 @@
#include <map>
#include <memory>
#include <ostream>
#include <set>
#include <stdexcept>
#include <string>
#include <vector>
@ -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<std::string> get_defined_method_names(chaiscript::detail::Dispatch_Engine &t_engine,
const std::string &type_name) {
std::set<std::string> names;
for (const auto &[func_name, func] : t_engine.get_functions()) {
const auto *dof = dynamic_cast<const dispatch::detail::Dynamic_Object_Function *>(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<const dispatch::detail::Dynamic_Object_Function *>(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<dispatch::detail::Dynamic_Object_Function>(
derived_name,
std::const_pointer_cast<dispatch::Proxy_Function_Base>(contained[0]),
dof->is_attribute_function()),
func_name);
} catch (const chaiscript::exception::name_conflict_error &) {
// already registered, skip
}
}
}
}
}
};
template<typename T>

View File

@ -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()) {
}

View File

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