diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 4c79b45..1722bf4 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -187,6 +187,9 @@ target_include_directories(step64_test PRIVATE src) add_executable(step65_test tests/step65_test.cpp) target_include_directories(step65_test PRIVATE src) +add_executable(step66_test tests/step66_test.cpp) +target_include_directories(step66_test PRIVATE src) + add_executable(whetstone_editor src/main.cpp) target_include_directories(whetstone_editor PRIVATE src) # find_package(SDL2 REQUIRED) # Commented out for now to avoid build issues diff --git a/editor/tests/step66_test.cpp b/editor/tests/step66_test.cpp new file mode 100644 index 0000000..cbd8189 --- /dev/null +++ b/editor/tests/step66_test.cpp @@ -0,0 +1,204 @@ +// Step 66 TDD Test: Cross-language memory projection +// +// Tests the "Lossless Projection Logic" from Memory strategy.md Section 3: +// 1. Python → C++: @Reclaim(Tracing) → inject ref-counting shim (shared_ptr) +// 2. C++ → Python: @Deallocate(Explicit) → wrap in try-with-resources pattern +// 3. Rust-like → Python: @Owner(Single) → preserve in metadata, don't enforce +// 4. Round-trip: annotation survives language round-trip +// 5. @Lifetime(RAII) → inject destructor calls at scope end +// +// Will fail until cross-language projection is implemented. + +#include +#include +#include +#include "ast/ASTNode.h" +#include "ast/Module.h" +#include "ast/Function.h" +#include "ast/Variable.h" +#include "ast/Annotation.h" +#include "ast/Generator.h" + +static bool contains(const std::string& haystack, const std::string& needle) { + return haystack.find(needle) != std::string::npos; +} + +// Forward declaration — CrossLanguageProjector +class CrossLanguageProjector { +public: + // Project an AST from source language to target language + // Returns a new AST with annotations adapted for the target + std::unique_ptr project(const Module* source, const std::string& targetLanguage) const; + + // Check if all annotations survived the projection + bool annotationsPreserved(const Module* original, const Module* projected) const; +}; + +int main() { + int passed = 0; + int failed = 0; + + CrossLanguageProjector projector; + + // --- Test 1: Python @Reclaim(Tracing) → C++ shared_ptr --- + { + Module pyMod("m1", "PyModule", "python"); + Function* fn = new Function("f1", "process"); + + ReclaimAnnotation* anno = new ReclaimAnnotation(); + anno->id = "a1"; + anno->strategy = "Tracing"; + fn->addChild("annotations", anno); + + Variable* var = new Variable("v1", "data"); + fn->addChild("body", var); + pyMod.addChild("functions", fn); + + auto cppMod = projector.project(&pyMod, "cpp"); + assert(cppMod != nullptr && "Projection should produce a module"); + assert(cppMod->targetLanguage == "cpp" && "Target should be cpp"); + + // Generate C++ from the projected module + CppGenerator gen; + std::string output = gen.generate(cppMod.get()); + + // @Reclaim(Tracing) in C++ → should use shared_ptr for GC-like semantics + assert(contains(output, "shared_ptr") && + "Python @Reclaim(Tracing) → C++ should use shared_ptr"); + + std::cout << "Test 1 PASS: Python @Reclaim(Tracing) → C++ shared_ptr" << std::endl; + ++passed; + + delete var; + delete anno; + delete fn; + } + + // --- Test 2: C++ @Deallocate(Explicit) → Python wrapper --- + { + Module cppMod("m1", "CppModule", "cpp"); + Function* fn = new Function("f1", "allocate"); + + DeallocateAnnotation* anno = new DeallocateAnnotation(); + anno->id = "a1"; + anno->strategy = "Explicit"; + fn->addChild("annotations", anno); + + Variable* var = new Variable("v1", "ptr"); + fn->addChild("body", var); + cppMod.addChild("functions", fn); + + auto pyMod = projector.project(&cppMod, "python"); + assert(pyMod != nullptr && "Projection should produce a module"); + assert(pyMod->targetLanguage == "python" && "Target should be python"); + + // The annotation should still be present in metadata + auto projectedFns = pyMod->getChildren("functions"); + assert(!projectedFns.empty() && "Should have functions"); + + std::cout << "Test 2 PASS: C++ @Deallocate(Explicit) → Python projection" << std::endl; + ++passed; + + delete var; + delete anno; + delete fn; + } + + // --- Test 3: @Owner(Single) → Python preserves in metadata --- + { + Module rustLike("m1", "RustLike", "cpp"); + Function* fn = new Function("f1", "ownership"); + + OwnerAnnotation* anno = new OwnerAnnotation(); + anno->id = "a1"; + anno->strategy = "Single"; + fn->addChild("annotations", anno); + rustLike.addChild("functions", fn); + + auto pyMod = projector.project(&rustLike, "python"); + + // @Owner(Single) should be preserved in metadata even though Python doesn't enforce it + auto projectedFns = pyMod->getChildren("functions"); + auto* projectedFn = static_cast(projectedFns[0]); + auto annos = projectedFn->getChildren("annotations"); + + bool hasOwner = false; + for (auto* a : annos) { + if (a->conceptType == "OwnerAnnotation") { + hasOwner = true; + } + } + assert(hasOwner && "@Owner(Single) should be preserved in Python projection metadata"); + + std::cout << "Test 3 PASS: @Owner(Single) preserved in Python metadata" << std::endl; + ++passed; + + delete anno; + delete fn; + } + + // --- Test 4: Round-trip preserves annotations --- + { + Module pyMod("m1", "RoundTrip", "python"); + Function* fn = new Function("f1", "process"); + + ReclaimAnnotation* anno = new ReclaimAnnotation(); + anno->id = "a1"; + anno->strategy = "Tracing"; + fn->addChild("annotations", anno); + pyMod.addChild("functions", fn); + + // Python → C++ + auto cppMod = projector.project(&pyMod, "cpp"); + + // C++ → Python + auto pyModBack = projector.project(cppMod.get(), "python"); + + // Annotation should survive round-trip + assert(projector.annotationsPreserved(&pyMod, pyModBack.get()) && + "Annotations should survive Python → C++ → Python round-trip"); + + std::cout << "Test 4 PASS: Round-trip preserves annotations" << std::endl; + ++passed; + + delete anno; + delete fn; + } + + // --- Test 5: @Lifetime(RAII) in C++ generation --- + { + Module cppMod("m1", "RAIIModule", "cpp"); + Function* fn = new Function("f1", "scoped"); + + LifetimeAnnotation* anno = new LifetimeAnnotation(); + anno->id = "a1"; + anno->strategy = "RAII"; + fn->addChild("annotations", anno); + + Variable* var = new Variable("v1", "resource"); + fn->addChild("body", var); + + PrimitiveType* retType = new PrimitiveType("t1", "void"); + fn->setChild("returnType", retType); + cppMod.addChild("functions", fn); + + CppGenerator gen; + std::string output = gen.generate(&cppMod); + + // @Lifetime(RAII) → unique_ptr with RAII semantics + assert(contains(output, "unique_ptr") && + "@Lifetime(RAII) should produce unique_ptr in C++"); + + std::cout << "Test 5 PASS: @Lifetime(RAII) generates unique_ptr" << std::endl; + ++passed; + + delete retType; + delete var; + delete anno; + delete fn; + } + + // --- Summary --- + std::cout << "\n=== Step 66 Results: " << passed << " passed, " << failed << " failed ===" << std::endl; + return failed > 0 ? 1 : 0; +}