Step 332: Multiple Inheritance in ClassDeclaration (12/12 tests)

BaseClass struct with access specifiers and virtual flags. Backward-compatible
getBases() migration from legacy superClass. Diamond inheritance detection via
BFS. Updated Serialization for baseClasses array roundtrip.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bill
2026-02-15 19:31:55 -07:00
parent 6784281489
commit b1654f0b85
6 changed files with 389 additions and 1 deletions

View File

@@ -1996,4 +1996,9 @@ target_link_libraries(step331_test PRIVATE
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go)
# Step 332: Multiple Inheritance in ClassDeclaration
add_executable(step332_test tests/step332_test.cpp)
target_include_directories(step332_test PRIVATE src)
target_link_libraries(step332_test PRIVATE nlohmann_json::nlohmann_json)
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)

View File

@@ -403,6 +403,13 @@ inline std::string getNodeName(const ASTNode* node) {
if (ct == "BooleanLiteral")
return static_cast<const BooleanLiteral*>(node)->value
? "true" : "false";
// Class declarations (Sprint 11c + 12c)
if (ct == "ClassDeclaration")
return static_cast<const ClassDeclaration*>(node)->name;
if (ct == "InterfaceDeclaration")
return static_cast<const InterfaceDeclaration*>(node)->name;
if (ct == "MethodDeclaration")
return static_cast<const MethodDeclaration*>(node)->name;
// Host Boundary (Step 288)
if (ct == "HostCall")
return static_cast<const HostCall*>(node)->name;

View File

@@ -3,13 +3,27 @@
#include "Function.h"
#include <string>
#include <vector>
#include <map>
#include <algorithm>
// BaseClass — represents a single base class with access specifier and virtual flag
struct BaseClass {
std::string name;
std::string accessSpecifier = "public"; // "public" | "protected" | "private"
bool isVirtual = false;
BaseClass() = default;
BaseClass(const std::string& n, const std::string& access = "public", bool virt = false)
: name(n), accessSpecifier(access), isVirtual(virt) {}
};
// ClassDeclaration — represents a class/struct definition
class ClassDeclaration : public ASTNode {
public:
std::string name;
std::string superClass; // base class name, empty if none
std::string superClass; // legacy single base class (deprecated, use baseClasses)
bool isAbstract = false;
std::vector<BaseClass> baseClasses;
ClassDeclaration() { conceptType = "ClassDeclaration"; }
ClassDeclaration(const std::string& id_, const std::string& name_)
@@ -17,6 +31,74 @@ public:
this->id = id_;
this->conceptType = "ClassDeclaration";
}
// Add a base class
void addBase(const std::string& baseName, const std::string& access = "public",
bool isVirt = false) {
baseClasses.emplace_back(baseName, access, isVirt);
// Keep superClass in sync for backward compat
if (baseClasses.size() == 1) superClass = baseName;
}
// Get all base classes; if baseClasses is empty but superClass is set, migrate
std::vector<BaseClass> getBases() const {
if (!baseClasses.empty()) return baseClasses;
if (!superClass.empty()) {
return { BaseClass(superClass, "public", false) };
}
return {};
}
// Detect diamond inheritance: two bases share a common ancestor
// classMap maps class name → list of base class names
static bool hasDiamondInheritance(
const std::string& className,
const std::map<std::string, std::vector<std::string>>& classMap)
{
auto it = classMap.find(className);
if (it == classMap.end()) return false;
const auto& directBases = it->second;
if (directBases.size() < 2) return false;
// Collect all ancestors of each direct base
// Simple BFS: for each direct base, collect all transitive bases
auto collectAncestors = [&](const std::string& base,
std::vector<std::string>& ancestors) {
std::vector<std::string> queue = { base };
while (!queue.empty()) {
std::string cur = queue.back();
queue.pop_back();
auto cit = classMap.find(cur);
if (cit != classMap.end()) {
for (const auto& b : cit->second) {
ancestors.push_back(b);
queue.push_back(b);
}
}
}
};
// Check if any ancestor appears in more than one direct base's ancestry
std::map<std::string, int> ancestorCount;
for (const auto& base : directBases) {
std::vector<std::string> ancestors;
ancestors.push_back(base);
collectAncestors(base, ancestors);
// Use a set to avoid counting duplicates within one path
std::vector<std::string> unique = ancestors;
std::sort(unique.begin(), unique.end());
unique.erase(std::unique(unique.begin(), unique.end()), unique.end());
for (const auto& a : unique) {
ancestorCount[a]++;
}
}
for (const auto& [ancestor, count] : ancestorCount) {
if (count >= 2) return true;
}
return false;
}
// Children roles: "interfaces" (list of CustomType), "fields" (list of Variable),
// "methods" (list of MethodDeclaration), "annotations"
};

View File

@@ -455,6 +455,17 @@ inline json propertiesToJson(const ASTNode* node) {
props["name"] = n->name;
if (!n->superClass.empty()) props["superClass"] = n->superClass;
props["isAbstract"] = n->isAbstract;
if (!n->baseClasses.empty()) {
json bases = json::array();
for (const auto& bc : n->baseClasses) {
json bj;
bj["name"] = bc.name;
bj["accessSpecifier"] = bc.accessSpecifier;
bj["isVirtual"] = bc.isVirtual;
bases.push_back(bj);
}
props["baseClasses"] = bases;
}
}
else if (ct == "InterfaceDeclaration") {
auto* n = static_cast<const InterfaceDeclaration*>(node);
@@ -1118,6 +1129,20 @@ inline void setPropertiesFromJson(ASTNode* node, const json& props) {
if (props.contains("name")) n->name = props["name"].get<std::string>();
if (props.contains("superClass")) n->superClass = props["superClass"].get<std::string>();
if (props.contains("isAbstract")) n->isAbstract = props["isAbstract"].get<bool>();
if (props.contains("baseClasses")) {
n->baseClasses.clear();
for (const auto& bj : props["baseClasses"]) {
BaseClass bc;
bc.name = bj.value("name", "");
bc.accessSpecifier = bj.value("accessSpecifier", "public");
bc.isVirtual = bj.value("isVirtual", false);
n->baseClasses.push_back(bc);
}
// Sync superClass from first base for backward compat
if (!n->baseClasses.empty() && n->superClass.empty()) {
n->superClass = n->baseClasses[0].name;
}
}
}
else if (ct == "InterfaceDeclaration") {
auto* n = static_cast<InterfaceDeclaration*>(node);

View File

@@ -0,0 +1,245 @@
// Step 332: Multiple Inheritance in ClassDeclaration (12 tests)
#include "ast/ClassDeclaration.h"
#include "ast/Serialization.h"
#include <nlohmann/json.hpp>
#include <iostream>
#include <string>
using json = nlohmann::json;
static int passed = 0, failed = 0;
#define TEST(name) { std::cout << " " << #name << "... "; }
#define PASS() { std::cout << "PASS\n"; ++passed; }
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
// 1. Single base class (backward compat via superClass)
void test_single_base_backward_compat() {
TEST(single_base_backward_compat);
ClassDeclaration cd("c1", "Foo");
cd.superClass = "Bar";
auto bases = cd.getBases();
CHECK(bases.size() == 1, "should have 1 base");
CHECK(bases[0].name == "Bar", "base name");
CHECK(bases[0].accessSpecifier == "public", "default public");
CHECK(!bases[0].isVirtual, "default not virtual");
PASS();
}
// 2. Multiple bases via addBase
void test_multiple_bases() {
TEST(multiple_bases);
ClassDeclaration cd("c1", "Diamond");
cd.addBase("Base1", "public");
cd.addBase("Base2", "private");
cd.addBase("Base3", "protected");
CHECK(cd.baseClasses.size() == 3, "3 bases");
CHECK(cd.baseClasses[0].name == "Base1", "first base");
CHECK(cd.baseClasses[1].name == "Base2", "second base");
CHECK(cd.baseClasses[2].name == "Base3", "third base");
// superClass set to first
CHECK(cd.superClass == "Base1", "superClass = first base");
PASS();
}
// 3. Access specifiers
void test_access_specifiers() {
TEST(access_specifiers);
ClassDeclaration cd("c1", "Derived");
cd.addBase("PubBase", "public");
cd.addBase("PrivBase", "private");
cd.addBase("ProtBase", "protected");
CHECK(cd.baseClasses[0].accessSpecifier == "public", "public");
CHECK(cd.baseClasses[1].accessSpecifier == "private", "private");
CHECK(cd.baseClasses[2].accessSpecifier == "protected", "protected");
PASS();
}
// 4. Virtual inheritance flag
void test_virtual_inheritance() {
TEST(virtual_inheritance);
ClassDeclaration cd("c1", "VDerived");
cd.addBase("VBase", "public", true);
cd.addBase("NormalBase", "public", false);
CHECK(cd.baseClasses[0].isVirtual, "first is virtual");
CHECK(!cd.baseClasses[1].isVirtual, "second not virtual");
PASS();
}
// 5. BaseClass JSON roundtrip via Serialization
void test_base_class_json_roundtrip() {
TEST(base_class_json_roundtrip);
ClassDeclaration cd("c1", "Multi");
cd.addBase("A", "public", false);
cd.addBase("B", "private", true);
cd.addBase("C", "protected", false);
cd.isAbstract = true;
json j = propertiesToJson(&cd);
CHECK(j.contains("baseClasses"), "has baseClasses");
CHECK(j["baseClasses"].size() == 3, "3 in JSON");
CHECK(j["baseClasses"][1]["name"] == "B", "B name");
CHECK(j["baseClasses"][1]["isVirtual"] == true, "B virtual");
CHECK(j["baseClasses"][1]["accessSpecifier"] == "private", "B private");
// Roundtrip
ClassDeclaration cd2;
setPropertiesFromJson(&cd2, j);
CHECK(cd2.name == "Multi", "name preserved");
CHECK(cd2.isAbstract, "isAbstract preserved");
CHECK(cd2.baseClasses.size() == 3, "3 bases roundtrip");
CHECK(cd2.baseClasses[0].name == "A", "A roundtrip");
CHECK(cd2.baseClasses[1].name == "B", "B roundtrip");
CHECK(cd2.baseClasses[1].isVirtual, "B virtual roundtrip");
CHECK(cd2.baseClasses[2].accessSpecifier == "protected", "C access roundtrip");
PASS();
}
// 6. Mixed virtual and non-virtual
void test_mixed_virtual() {
TEST(mixed_virtual);
ClassDeclaration cd("c1", "MixedDerived");
cd.addBase("VirtA", "public", true);
cd.addBase("NormB", "public", false);
cd.addBase("VirtC", "private", true);
int virtualCount = 0;
for (const auto& b : cd.baseClasses) {
if (b.isVirtual) virtualCount++;
}
CHECK(virtualCount == 2, "2 virtual bases");
PASS();
}
// 7. Diamond detection: D → B + C, B → A, C → A
void test_diamond_detection() {
TEST(diamond_detection);
std::map<std::string, std::vector<std::string>> classMap;
classMap["A"] = {};
classMap["B"] = {"A"};
classMap["C"] = {"A"};
classMap["D"] = {"B", "C"};
CHECK(ClassDeclaration::hasDiamondInheritance("D", classMap), "D has diamond");
CHECK(!ClassDeclaration::hasDiamondInheritance("B", classMap), "B no diamond");
CHECK(!ClassDeclaration::hasDiamondInheritance("A", classMap), "A no diamond");
PASS();
}
// 8. Empty bases list
void test_empty_bases() {
TEST(empty_bases);
ClassDeclaration cd("c1", "Standalone");
CHECK(cd.baseClasses.empty(), "no bases");
CHECK(cd.superClass.empty(), "no superClass");
auto bases = cd.getBases();
CHECK(bases.empty(), "getBases empty");
// JSON should not have baseClasses key
json j = propertiesToJson(&cd);
CHECK(!j.contains("baseClasses"), "no baseClasses in JSON");
PASS();
}
// 9. Legacy superClass migration via getBases()
void test_legacy_superclass_migration() {
TEST(legacy_superclass_migration);
ClassDeclaration cd("c1", "Legacy");
cd.superClass = "OldBase";
// baseClasses not set — getBases should migrate
auto bases = cd.getBases();
CHECK(bases.size() == 1, "1 migrated base");
CHECK(bases[0].name == "OldBase", "migrated name");
CHECK(bases[0].accessSpecifier == "public", "default public");
CHECK(!bases[0].isVirtual, "default not virtual");
PASS();
}
// 10. addBase helper
void test_addBase_helper() {
TEST(addBase_helper);
ClassDeclaration cd("c1", "TestClass");
cd.addBase("First"); // defaults: public, not virtual
cd.addBase("Second", "private"); // not virtual
cd.addBase("Third", "protected", true); // virtual
CHECK(cd.baseClasses.size() == 3, "3 bases");
CHECK(cd.baseClasses[0].accessSpecifier == "public", "First default public");
CHECK(!cd.baseClasses[0].isVirtual, "First default not virtual");
CHECK(cd.baseClasses[1].accessSpecifier == "private", "Second private");
CHECK(cd.baseClasses[2].isVirtual, "Third virtual");
PASS();
}
// 11. Full serialization roundtrip via toJson/fromJson
void test_full_serialization_roundtrip() {
TEST(full_serialization_roundtrip);
auto* cd = new ClassDeclaration("c1", "FullTest");
cd->addBase("Alpha", "public", false);
cd->addBase("Beta", "private", true);
cd->isAbstract = true;
json j = toJson(cd);
CHECK(j["concept"] == "ClassDeclaration", "conceptType");
CHECK(j["properties"]["baseClasses"].size() == 2, "2 bases in full JSON");
auto* restored = static_cast<ClassDeclaration*>(fromJson(j));
CHECK(restored != nullptr, "restored not null");
CHECK(restored->name == "FullTest", "name restored");
CHECK(restored->baseClasses.size() == 2, "2 bases restored");
CHECK(restored->baseClasses[0].name == "Alpha", "Alpha restored");
CHECK(restored->baseClasses[1].isVirtual, "Beta virtual restored");
CHECK(restored->isAbstract, "isAbstract restored");
delete cd;
delete restored;
PASS();
}
// 12. No diamond: linear chain A → B → C (no sharing)
void test_no_diamond_linear() {
TEST(no_diamond_linear);
std::map<std::string, std::vector<std::string>> classMap;
classMap["A"] = {};
classMap["B"] = {"A"};
classMap["C"] = {"B"};
CHECK(!ClassDeclaration::hasDiamondInheritance("C", classMap), "linear = no diamond");
CHECK(!ClassDeclaration::hasDiamondInheritance("B", classMap), "B = no diamond");
// Multiple bases but no shared ancestor
classMap["X"] = {};
classMap["Y"] = {};
classMap["Z"] = {"X", "Y"};
CHECK(!ClassDeclaration::hasDiamondInheritance("Z", classMap), "disjoint bases = no diamond");
PASS();
}
int main() {
std::cout << "=== Step 332: Multiple Inheritance in ClassDeclaration ===\n";
try {
test_single_base_backward_compat();
test_multiple_bases();
test_access_specifiers();
test_virtual_inheritance();
test_base_class_json_roundtrip();
test_mixed_virtual();
test_diamond_detection();
test_empty_bases();
test_legacy_superclass_migration();
test_addBase_helper();
test_full_serialization_roundtrip();
test_no_diamond_linear();
} catch (const std::exception& e) {
std::cout << "EXCEPTION: " << e.what() << "\n";
++failed;
}
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
return failed > 0 ? 1 : 0;
}

View File

@@ -1467,6 +1467,30 @@ mixed skeleton lifecycle.
**Phase 12b complete:** Steps 326-331 (RoutingEngine, WorkerRegistry,
ContextAssembler, Routing RPC+MCP, ReviewGate, Integration Tests)
## Phase 12c: C++ AST Depth — Inheritance + CRTP
### Step 332: Multiple Inheritance in ClassDeclaration
**Status:** PASS (12/12 tests)
Upgraded ClassDeclaration from single superClass string to vector<BaseClass> with
access specifiers (public/protected/private) and virtual inheritance flags. Backward
compatible: legacy superClass field still works via getBases() migration. Added
diamond inheritance detection via static hasDiamondInheritance() with BFS traversal.
**Files modified:**
- `editor/src/ast/ClassDeclaration.h` — BaseClass struct, addBase(), getBases(),
hasDiamondInheritance() static method
- `editor/src/ast/Serialization.h` — baseClasses array serialization/deserialization
with legacy superClass backward compat
- `editor/src/CompactAST.h` — getNodeName for ClassDeclaration, InterfaceDeclaration,
MethodDeclaration
- `editor/CMakeLists.txt` — step332_test target
**Files created:**
- `editor/tests/step332_test.cpp` — 12 tests: backward compat, multiple bases,
access specifiers, virtual inheritance, JSON roundtrip, diamond detection, legacy
migration, addBase helper, full serialization roundtrip
---
# Roadmap Planning — Sprints 12-25+