diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 34f7156..b5abd9c 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2479,4 +2479,13 @@ target_link_libraries(step399_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step400_test tests/step400_test.cpp) +target_include_directories(step400_test PRIVATE src) +target_link_libraries(step400_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/SelfHostHarness.h b/editor/src/SelfHostHarness.h new file mode 100644 index 0000000..b579622 --- /dev/null +++ b/editor/src/SelfHostHarness.h @@ -0,0 +1,308 @@ +#pragma once +// Step 400: Self-Hosting Test Harness +// +// Infrastructure to feed actual Whetstone .h files through the pipeline. +// Compare parsed AST against expected structure. Track coverage. +// Graceful degradation: unparseable sections become opaque blocks, not errors. + +#include +#include +#include +#include +#include +#include +#include "ast/ASTNode.h" +#include "ast/Module.h" +#include "ast/Function.h" +#include "ast/Parser.h" +#include "ast/ClassDeclaration.h" +#include "ast/EnumNamespaceNodes.h" +#include "ast/CppAdvancedNodes.h" + +// --- Coverage tracking --- + +struct ConstructCoverage { + std::string constructType; // e.g. "class", "function", "struct", "enum" + int expected = 0; + int parsed = 0; + int opaque = 0; // unparseable sections that became opaque blocks + + double ratio() const { + return expected > 0 ? static_cast(parsed) / expected : 1.0; + } +}; + +struct SelfHostResult { + std::string filePath; + bool parseSuccess = false; + std::unique_ptr ast; + std::vector coverage; + std::vector warnings; + int totalExpected = 0; + int totalParsed = 0; + int totalOpaque = 0; + + double overallCoverage() const { + return totalExpected > 0 ? static_cast(totalParsed) / totalExpected : 1.0; + } +}; + +// --- Expected structure declaration --- + +struct ExpectedConstruct { + std::string type; // "class", "function", "struct", "namespace", "type_alias" + std::string name; // expected name (empty = don't check) +}; + +struct ExpectedStructure { + std::string filePath; + std::vector constructs; +}; + +// --- Self-Hosting Harness --- + +class SelfHostHarness { +public: + + // Read a file from disk and return its contents + static std::string readFile(const std::string& path) { + std::ifstream ifs(path); + if (!ifs.is_open()) return ""; + std::ostringstream ss; + ss << ifs.rdbuf(); + return ss.str(); + } + + // Parse a Whetstone .h file through the C++ parser + static SelfHostResult parseFile(const std::string& path) { + SelfHostResult result; + result.filePath = path; + + std::string source = readFile(path); + if (source.empty()) { + result.warnings.push_back("File not found or empty: " + path); + return result; + } + + result.ast = TreeSitterParser::parseCpp(source); + result.parseSuccess = (result.ast != nullptr); + + if (!result.parseSuccess) { + result.warnings.push_back("Parse returned null for: " + path); + } + + return result; + } + + // Parse source string directly (for testing without file I/O) + static SelfHostResult parseSource(const std::string& source, + const std::string& label = "") { + SelfHostResult result; + result.filePath = label; + + if (source.empty()) { + result.warnings.push_back("Empty source provided"); + return result; + } + + result.ast = TreeSitterParser::parseCpp(source); + result.parseSuccess = (result.ast != nullptr); + return result; + } + + // Compare parsed AST against expected structure + static void checkExpected(SelfHostResult& result, + const ExpectedStructure& expected) { + if (!result.ast) { + result.totalExpected = static_cast(expected.constructs.size()); + result.totalOpaque = result.totalExpected; + return; + } + + // Group expected constructs by type + std::map> byType; + for (const auto& c : expected.constructs) { + byType[c.type].push_back(c.name); + } + + result.totalExpected = static_cast(expected.constructs.size()); + + for (const auto& [type, names] : byType) { + ConstructCoverage cov; + cov.constructType = type; + cov.expected = static_cast(names.size()); + + if (type == "class" || type == "struct") { + auto& classes = result.ast->getChildren("classes"); + for (const auto& name : names) { + bool found = false; + for (auto* c : classes) { + auto* cls = static_cast(c); + if (name.empty() || cls->name == name) { + found = true; + break; + } + } + if (found) cov.parsed++; + else cov.opaque++; + } + } + else if (type == "function") { + auto& fns = result.ast->getChildren("functions"); + for (const auto& name : names) { + bool found = false; + for (auto* f : fns) { + auto* fn = static_cast(f); + if (name.empty() || fn->name == name) { + found = true; + break; + } + } + if (found) cov.parsed++; + else cov.opaque++; + } + } + else if (type == "namespace") { + auto& stmts = result.ast->getChildren("statements"); + for (const auto& name : names) { + bool found = false; + for (auto* s : stmts) { + if (s->conceptType == "NamespaceDeclaration") { + auto* ns = static_cast(s); + if (name.empty() || ns->name == name) { + found = true; + break; + } + } + } + if (found) cov.parsed++; + else cov.opaque++; + } + } + else if (type == "type_alias") { + auto& stmts = result.ast->getChildren("statements"); + for (const auto& name : names) { + bool found = false; + for (auto* s : stmts) { + if (s->conceptType == "TypeAlias") { + auto* ta = static_cast(s); + if (name.empty() || ta->aliasName == name) { + found = true; + break; + } + } + } + if (found) cov.parsed++; + else cov.opaque++; + } + } + else if (type == "enum") { + auto& stmts = result.ast->getChildren("statements"); + for (const auto& name : names) { + bool found = false; + for (auto* s : stmts) { + if (s->conceptType == "EnumDeclaration") { + auto* en = static_cast(s); + if (name.empty() || en->name == name) { + found = true; + break; + } + } + } + if (found) cov.parsed++; + else cov.opaque++; + } + } + else { + // Unknown type — mark all as opaque + cov.opaque = cov.expected; + result.warnings.push_back("Unknown construct type: " + type); + } + + result.totalParsed += cov.parsed; + result.totalOpaque += cov.opaque; + result.coverage.push_back(std::move(cov)); + } + } + + // Count all classes found in parsed AST + static int countClasses(const Module* mod) { + if (!mod) return 0; + return static_cast(mod->getChildren("classes").size()); + } + + // Count all functions found in parsed AST + static int countFunctions(const Module* mod) { + if (!mod) return 0; + return static_cast(mod->getChildren("functions").size()); + } + + // Count all statements of a specific concept type + static int countStatements(const Module* mod, const std::string& conceptType) { + if (!mod) return 0; + int count = 0; + for (auto* s : mod->getChildren("statements")) { + if (s->conceptType == conceptType) count++; + } + return count; + } + + // Get list of all class names + static std::vector getClassNames(const Module* mod) { + std::vector names; + if (!mod) return names; + for (auto* c : mod->getChildren("classes")) { + names.push_back(static_cast(c)->name); + } + return names; + } + + // Get list of all function names + static std::vector getFunctionNames(const Module* mod) { + std::vector names; + if (!mod) return names; + for (auto* f : mod->getChildren("functions")) { + names.push_back(static_cast(f)->name); + } + return names; + } + + // Check if a specific construct was parsed (by type and name) + static bool hasParsedConstruct(const Module* mod, + const std::string& type, + const std::string& name) { + if (!mod) return false; + if (type == "class" || type == "struct") { + for (auto* c : mod->getChildren("classes")) { + if (static_cast(c)->name == name) return true; + } + } else if (type == "function") { + for (auto* f : mod->getChildren("functions")) { + if (static_cast(f)->name == name) return true; + } + } + return false; + } + + // Generate a coverage report string + static std::string coverageReport(const SelfHostResult& result) { + std::ostringstream out; + out << "Self-Host Report: " << result.filePath << "\n"; + out << " Parse success: " << (result.parseSuccess ? "yes" : "no") << "\n"; + out << " Overall coverage: " << (result.overallCoverage() * 100) << "%" + << " (" << result.totalParsed << "/" << result.totalExpected << ")\n"; + + for (const auto& cov : result.coverage) { + out << " " << cov.constructType << ": " + << cov.parsed << "/" << cov.expected + << " (" << (cov.ratio() * 100) << "%)"; + if (cov.opaque > 0) out << " [" << cov.opaque << " opaque]"; + out << "\n"; + } + + for (const auto& w : result.warnings) { + out << " WARNING: " << w << "\n"; + } + return out.str(); + } +}; diff --git a/editor/tests/step400_test.cpp b/editor/tests/step400_test.cpp new file mode 100644 index 0000000..09d1fcf --- /dev/null +++ b/editor/tests/step400_test.cpp @@ -0,0 +1,258 @@ +// Step 400: Self-Hosting Test Harness (12 tests) + +#include +#include +#include +#include "SelfHostHarness.h" + +int main() { + int passed = 0; + + // Test 1: Parse simple struct from source string + { + std::string src = R"( +struct Foo { + int x; + std::string name; +}; +)"; + auto result = SelfHostHarness::parseSource(src, "test_struct"); + assert(result.parseSuccess); + assert(result.ast != nullptr); + std::cout << "PASS: test 1 — parse simple struct\n"; + passed++; + } + + // Test 2: Parse class + free function from source string + { + std::string src = R"( +class Widget { +public: + void draw() { } +}; + +void process(Widget* w) { + w->draw(); +} +)"; + auto result = SelfHostHarness::parseSource(src, "test_class_func"); + assert(result.parseSuccess); + assert(SelfHostHarness::countClasses(result.ast.get()) >= 1); + assert(SelfHostHarness::countFunctions(result.ast.get()) >= 1); + std::cout << "PASS: test 2 — parse class + free function\n"; + passed++; + } + + // Test 3: Coverage tracking — all constructs found + { + std::string src = R"( +struct Point { int x; int y; }; +void doSomething() { } +)"; + auto result = SelfHostHarness::parseSource(src, "test_coverage"); + ExpectedStructure expected; + expected.filePath = "test_coverage"; + expected.constructs = { + {"struct", "Point"}, + {"function", "doSomething"} + }; + SelfHostHarness::checkExpected(result, expected); + assert(result.totalExpected == 2); + assert(result.totalParsed == 2); + assert(result.totalOpaque == 0); + assert(result.overallCoverage() == 1.0); + std::cout << "PASS: test 3 — coverage tracking, all found\n"; + passed++; + } + + // Test 4: Coverage tracking — missing construct is opaque + { + std::string src = R"( +void foo() { } +)"; + auto result = SelfHostHarness::parseSource(src, "test_missing"); + ExpectedStructure expected; + expected.filePath = "test_missing"; + expected.constructs = { + {"function", "foo"}, + {"class", "MissingClass"} + }; + SelfHostHarness::checkExpected(result, expected); + assert(result.totalExpected == 2); + assert(result.totalParsed == 1); + assert(result.totalOpaque == 1); + assert(result.overallCoverage() == 0.5); + std::cout << "PASS: test 4 — missing construct is opaque\n"; + passed++; + } + + // Test 5: Coverage report generation + { + std::string src = R"( +class Foo { }; +void bar() { } +)"; + auto result = SelfHostHarness::parseSource(src, "report_test"); + ExpectedStructure expected; + expected.filePath = "report_test"; + expected.constructs = { + {"class", "Foo"}, + {"function", "bar"} + }; + SelfHostHarness::checkExpected(result, expected); + std::string report = SelfHostHarness::coverageReport(result); + assert(!report.empty()); + assert(report.find("report_test") != std::string::npos); + assert(report.find("100%") != std::string::npos); + std::cout << "PASS: test 5 — coverage report generation\n"; + passed++; + } + + // Test 6: Graceful degradation — empty source doesn't crash + { + auto result = SelfHostHarness::parseSource("", "empty"); + assert(!result.parseSuccess || result.ast == nullptr || + result.warnings.size() > 0); + std::cout << "PASS: test 6 — graceful degradation on empty source\n"; + passed++; + } + + // Test 7: hasParsedConstruct lookup + { + std::string src = R"( +class Pipeline { }; +void run() { } +)"; + auto result = SelfHostHarness::parseSource(src, "lookup"); + assert(SelfHostHarness::hasParsedConstruct(result.ast.get(), "class", "Pipeline")); + assert(SelfHostHarness::hasParsedConstruct(result.ast.get(), "function", "run")); + assert(!SelfHostHarness::hasParsedConstruct(result.ast.get(), "class", "Missing")); + std::cout << "PASS: test 7 — hasParsedConstruct lookup\n"; + passed++; + } + + // Test 8: getClassNames and getFunctionNames + { + std::string src = R"( +class Alpha { }; +class Beta { }; +void one() { } +void two() { } +)"; + auto result = SelfHostHarness::parseSource(src, "names"); + auto classNames = SelfHostHarness::getClassNames(result.ast.get()); + auto funcNames = SelfHostHarness::getFunctionNames(result.ast.get()); + assert(classNames.size() >= 2); + assert(funcNames.size() >= 2); + std::cout << "PASS: test 8 — getClassNames and getFunctionNames\n"; + passed++; + } + + // Test 9: Parse Whetstone-style header pattern (struct + inline function) + { + std::string src = R"( +#pragma once +#include +#include + +struct ConflictInfo { + std::string type1; + std::string type2; + std::string message; +}; + +inline void checkConflicts(const std::vector& items) { + for (const auto& item : items) { + // process + } +} +)"; + auto result = SelfHostHarness::parseSource(src, "whetstone_pattern"); + assert(result.parseSuccess); + // Should find at least the struct and the function + ExpectedStructure expected; + expected.constructs = { + {"struct", "ConflictInfo"}, + {"function", "checkConflicts"} + }; + SelfHostHarness::checkExpected(result, expected); + assert(result.totalParsed >= 1); // At least struct or function found + std::cout << "PASS: test 9 — Whetstone-style header pattern\n"; + passed++; + } + + // Test 10: Type alias coverage tracking + { + std::string src = R"( +using json = nlohmann::json; +using StringVec = std::vector; +)"; + auto result = SelfHostHarness::parseSource(src, "alias_test"); + assert(result.parseSuccess); + int aliases = SelfHostHarness::countStatements(result.ast.get(), "TypeAlias"); + assert(aliases == 2); + + ExpectedStructure expected; + expected.constructs = { + {"type_alias", "json"}, + {"type_alias", "StringVec"} + }; + SelfHostHarness::checkExpected(result, expected); + assert(result.totalParsed == 2); + std::cout << "PASS: test 10 — type alias coverage tracking\n"; + passed++; + } + + // Test 11: Namespace coverage tracking + { + std::string src = R"( +namespace detail { + void helper() { } +} +)"; + auto result = SelfHostHarness::parseSource(src, "ns_test"); + assert(result.parseSuccess); + + ExpectedStructure expected; + expected.constructs = { + {"namespace", "detail"} + }; + SelfHostHarness::checkExpected(result, expected); + assert(result.totalParsed == 1); + std::cout << "PASS: test 11 — namespace coverage tracking\n"; + passed++; + } + + // Test 12: Mixed coverage with partial success + { + std::string src = R"( +class Validator { +public: + void validate() { } +}; + +void helperFunc() { } +)"; + auto result = SelfHostHarness::parseSource(src, "mixed_test"); + ExpectedStructure expected; + expected.constructs = { + {"class", "Validator"}, + {"function", "helperFunc"}, + {"class", "NonExistent"}, + {"function", "ghostFunc"} + }; + SelfHostHarness::checkExpected(result, expected); + assert(result.totalExpected == 4); + assert(result.totalParsed == 2); + assert(result.totalOpaque == 2); + assert(result.overallCoverage() == 0.5); + + std::string report = SelfHostHarness::coverageReport(result); + assert(report.find("50%") != std::string::npos); + std::cout << "PASS: test 12 — mixed coverage with partial success\n"; + passed++; + } + + std::cout << "\nStep 400 result: " << passed << "/12 tests passed\n"; + return (passed == 12) ? 0 : 1; +}