mirror of
https://github.com/ChaiScript/ChaiScript.git
synced 2026-04-30 19:09:26 +08:00
* 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> * Address review: use implicit derived-to-base matching instead of copying base class functions Instead of copying all base class methods/attributes into derived classes, make the type matching system recognize inheritance relationships. Base class methods now naturally match derived objects through dynamic_object_typename_match, and dispatch ordering ensures derived overrides are preferred over base methods. This is simpler (net -25 lines) and avoids duplicating function registrations. Requested by @lefticus in PR #641 review. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add tests for passing derived objects to functions expecting Base Tests cover: free functions calling base methods on derived objects, polymorphic dispatch through containers, base attribute access on derived objects, and multi-level inheritance (GrandChild : Derived : Base). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add typed parameter tests for class inheritance Use typed function signatures (e.g., `def call_do_something(Base obj)`) instead of untyped parameters to test that derived objects are accepted by functions expecting a base type, with correct polymorphic dispatch. Requested by @lefticus in PR #641 review. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: leftibot <leftibot@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
22092656fd
commit
f59eff9b2f
@ -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();
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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<const Dynamic_Object &>(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;
|
||||
}
|
||||
|
||||
@ -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 &) {
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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()) {
|
||||
}
|
||||
|
||||
|
||||
137
unittests/class_inheritance.chai
Normal file
137
unittests/class_inheritance.chai
Normal file
@ -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()))
|
||||
Loading…
x
Reference in New Issue
Block a user