diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 0b93a6c..3f6f144 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -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) diff --git a/editor/src/CompactAST.h b/editor/src/CompactAST.h index 3babc6f..136f9dd 100644 --- a/editor/src/CompactAST.h +++ b/editor/src/CompactAST.h @@ -403,6 +403,13 @@ inline std::string getNodeName(const ASTNode* node) { if (ct == "BooleanLiteral") return static_cast(node)->value ? "true" : "false"; + // Class declarations (Sprint 11c + 12c) + if (ct == "ClassDeclaration") + return static_cast(node)->name; + if (ct == "InterfaceDeclaration") + return static_cast(node)->name; + if (ct == "MethodDeclaration") + return static_cast(node)->name; // Host Boundary (Step 288) if (ct == "HostCall") return static_cast(node)->name; diff --git a/editor/src/ast/ClassDeclaration.h b/editor/src/ast/ClassDeclaration.h index 74c9c4e..ae09cf3 100644 --- a/editor/src/ast/ClassDeclaration.h +++ b/editor/src/ast/ClassDeclaration.h @@ -3,13 +3,27 @@ #include "Function.h" #include #include +#include +#include + +// 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 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 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>& 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& ancestors) { + std::vector 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 ancestorCount; + for (const auto& base : directBases) { + std::vector ancestors; + ancestors.push_back(base); + collectAncestors(base, ancestors); + // Use a set to avoid counting duplicates within one path + std::vector 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" }; diff --git a/editor/src/ast/Serialization.h b/editor/src/ast/Serialization.h index a1a7246..c849914 100644 --- a/editor/src/ast/Serialization.h +++ b/editor/src/ast/Serialization.h @@ -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(node); @@ -1118,6 +1129,20 @@ inline void setPropertiesFromJson(ASTNode* node, const json& props) { if (props.contains("name")) n->name = props["name"].get(); if (props.contains("superClass")) n->superClass = props["superClass"].get(); if (props.contains("isAbstract")) n->isAbstract = props["isAbstract"].get(); + 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(node); diff --git a/editor/tests/step332_test.cpp b/editor/tests/step332_test.cpp new file mode 100644 index 0000000..c304a38 --- /dev/null +++ b/editor/tests/step332_test.cpp @@ -0,0 +1,245 @@ +// Step 332: Multiple Inheritance in ClassDeclaration (12 tests) + +#include "ast/ClassDeclaration.h" +#include "ast/Serialization.h" +#include +#include +#include + +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> 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(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> 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; +} diff --git a/progress.md b/progress.md index 077b2a5..dfd85eb 100644 --- a/progress.md +++ b/progress.md @@ -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 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+