From 8a85ba298da2c77568b93a76e0a34648cf31d999 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 9 Feb 2026 10:57:25 -0700 Subject: [PATCH] Step 104: add cross-language projection action --- PROGRESS.md | 3 + editor/CMakeLists.txt | 4 ++ editor/src/main.cpp | 120 +++++++++++++++++++++++++++++----- editor/tests/step104_test.cpp | 37 +++++++++++ 4 files changed, 148 insertions(+), 16 deletions(-) create mode 100644 editor/tests/step104_test.cpp diff --git a/PROGRESS.md b/PROGRESS.md index eebfebc..8e9df6f 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -204,6 +204,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1 - [x] Step 101: **IMPLEMENTED** — Annotation conflict highlighting with gutter linkage and quick-fix actions (1/1 tests pass) - [x] Step 102: **IMPLEMENTED** — Memory strategy dashboard panel with counts and JSON export (1/1 tests pass) - [x] Step 103: **IMPLEMENTED** — Side-by-side generated code view with scroll lock, target language selection, and line highlighting (1/1 tests pass) +- [x] Step 104: **IMPLEMENTED** — Cross-language projection button with read-only projected tabs and annotation summary (2/2 tests pass) --- @@ -287,6 +288,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi **Step 101:** Compile and pass (1/1) **Step 102:** Compile and pass (1/1) **Step 103:** Compile and pass (1/1) +**Step 104:** Compile and pass (2/2) --- @@ -399,3 +401,4 @@ Sprint 4 in progress. Step 76 (LayoutManager) done. Next: Step 77 (custom code e | 2026-02-09 | Codex | Step 101: Annotation conflict highlighting with quick-fix actions. 1/1 tests pass. | | 2026-02-09 | Codex | Step 102: Memory strategy dashboard panel with counts and JSON export. 1/1 tests pass. | | 2026-02-09 | Codex | Step 103: Side-by-side generated code view with scroll lock, target language selector, and line highlighting. 1/1 tests pass. | +| 2026-02-09 | Codex | Step 104: Cross-language projection button with read-only projected tabs and annotation summary. 2/2 tests pass. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 9259f8a..ce0ebb4 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -562,6 +562,10 @@ add_executable(step103_test tests/step103_test.cpp) target_include_directories(step103_test PRIVATE src) target_link_libraries(step103_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step104_test tests/step104_test.cpp) +target_include_directories(step104_test PRIVATE src) +target_link_libraries(step104_test PRIVATE nlohmann_json::nlohmann_json) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 4b035a4..093b7f1 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -33,6 +33,7 @@ #include "MemoryStrategyInference.h" #include "AnnotationConflict.h" #include "StrategyDashboard.h" +#include "CrossLanguageProjector.h" #include "ast/Generator.h" #include "ast/Annotation.h" @@ -64,6 +65,7 @@ struct BufferState { std::string language = "python"; std::string generatedLanguage = "python"; std::string path = "(untitled)"; + bool readOnly = false; bool modified = false; int cursorLine = 1; int cursorCol = 1; @@ -461,22 +463,7 @@ struct EditorState { void updateGenerated() { if (!active()) return; Module* ast = active()->sync.getAST(); - std::string generated; - if (ast) { - if (active()->generatedLanguage == "python") { - PythonGenerator gen; - generated = gen.generate(ast); - } else if (active()->generatedLanguage == "cpp") { - CppGenerator gen; - generated = gen.generate(ast); - } else if (active()->generatedLanguage == "elisp") { - ElispGenerator gen; - generated = gen.generate(ast); - } else { - PythonGenerator gen; - generated = gen.generate(ast); - } - } + std::string generated = generateForLanguage(ast, active()->generatedLanguage); if (generated != active()->generatedBuf) { active()->generatedBuf = generated; active()->generatedHighlightsDirty = true; @@ -488,6 +475,54 @@ struct EditorState { } } + void projectToLanguage(const std::string& targetLanguage) { + if (!active()) return; + Module* ast = active()->sync.getAST(); + if (!ast) { + outputLog += "Project to " + targetLanguage + ": no AST available.\n"; + return; + } + + CrossLanguageProjector projector; + auto projected = projector.project(ast, targetLanguage); + if (!projected) { + outputLog += "Project to " + targetLanguage + ": failed to project AST.\n"; + return; + } + + const int srcAnnoCount = countAnnotationNodes(ast); + const int projAnnoCount = countAnnotationNodes(projected.get()); + const bool preserved = projector.annotationsPreserved(ast, projected.get()); + std::string generated = generateForLanguage(projected.get(), targetLanguage); + + std::string baseName = "(untitled-projection:" + targetLanguage + ")"; + std::string projName = baseName; + int suffix = 1; + while (buffers.hasBuffer(projName)) { + projName = baseName + "-" + std::to_string(suffix++); + } + + createBuffer(projName, generated, targetLanguage); + if (active()) { + active()->readOnly = true; + active()->sync.setText(generated, targetLanguage); + active()->sync.setAST(std::move(projected)); + active()->editBuf = generated; + active()->editor.setContent(active()->editBuf, targetLanguage); + active()->mode.setLanguage(targetLanguage); + active()->highlightsDirty = true; + active()->generatedLanguage = targetLanguage; + active()->generatedMode.setLanguage(targetLanguage); + active()->generatedHighlightsDirty = true; + active()->modified = false; + } + + outputLog += "Projected to " + targetLanguage + " in " + projName + + " (annotations " + std::to_string(srcAnnoCount) + " -> " + + std::to_string(projAnnoCount) + ", types preserved: " + + (preserved ? "yes" : "no") + ").\n"; + } + void updateCursorPos(int bytePos) { if (!active()) return; active()->cursorLine = 1; @@ -520,6 +555,42 @@ static int countLines(const std::string& text) { return lines; } +static std::string generateForLanguage(const Module* ast, const std::string& language) { + if (!ast) return ""; + if (language == "python") { + PythonGenerator gen; + return gen.generate(ast); + } + if (language == "cpp") { + CppGenerator gen; + return gen.generate(ast); + } + if (language == "elisp") { + ElispGenerator gen; + return gen.generate(ast); + } + PythonGenerator gen; + return gen.generate(ast); +} + +static bool isAnnotationNode(const ASTNode* node) { + if (!node) return false; + if (node->conceptType.find("Annotation") != std::string::npos) return true; + if (node->conceptType == "DerefStrategy") return true; + if (node->conceptType == "OptimizationLock") return true; + if (node->conceptType == "LangSpecific") return true; + return false; +} + +static int countAnnotationNodes(const ASTNode* node) { + if (!node) return 0; + int count = isAnnotationNode(node) ? 1 : 0; + for (auto* child : node->allChildren()) { + count += countAnnotationNodes(child); + } + return count; +} + // --------------------------------------------------------------------------- // ImGui InputTextMultiline with std::string resize callback @@ -1175,6 +1246,22 @@ int main(int, char**) { } ImGui::EndMenu(); } + + const bool canProject = state.active() && state.active()->sync.getAST(); + ImGui::SameLine(); + ImGui::Dummy(ImVec2(12.0f, 0.0f)); + ImGui::SameLine(); + if (!canProject) ImGui::BeginDisabled(); + if (ImGui::Button("Project to...")) { + ImGui::OpenPopup("ProjectToPopup"); + } + if (ImGui::BeginPopup("ProjectToPopup")) { + if (ImGui::MenuItem("Python")) state.projectToLanguage("python"); + if (ImGui::MenuItem("C++")) state.projectToLanguage("cpp"); + if (ImGui::MenuItem("Elisp")) state.projectToLanguage("elisp"); + ImGui::EndPopup(); + } + if (!canProject) ImGui::EndDisabled(); ImGui::EndMainMenuBar(); } @@ -1361,6 +1448,7 @@ int main(int, char**) { CodeEditorOptions opts; opts.showWhitespace = state.showWhitespace; + opts.readOnly = buf->readOnly; opts.mode = &buf->mode; opts.enableFolding = true; opts.showMinimap = state.showMinimap; diff --git a/editor/tests/step104_test.cpp b/editor/tests/step104_test.cpp new file mode 100644 index 0000000..8a2e347 --- /dev/null +++ b/editor/tests/step104_test.cpp @@ -0,0 +1,37 @@ +// Step 104 TDD Test: Cross-language projection +// +// Tests: +// 1. Projected module uses target language +// 2. Annotation types preserved across projection + +#include +#include +#include "CrossLanguageProjector.h" +#include "ast/Module.h" +#include "ast/Function.h" +#include "ast/Annotation.h" + +int main() { + int passed = 0; + int failed = 0; + + Module mod("m1", "mod", "python"); + auto* fn = new Function("f1", "foo"); + auto* anno = new ReclaimAnnotation("a1", "Tracing"); + fn->addChild("annotations", anno); + mod.addChild("functions", fn); + + CrossLanguageProjector projector; + auto projected = projector.project(&mod, "cpp"); + + assert(projected && projected->targetLanguage == "cpp"); + std::cout << "Test 1 PASS: target language set" << std::endl; + ++passed; + + assert(projector.annotationsPreserved(&mod, projected.get())); + std::cout << "Test 2 PASS: annotations preserved" << std::endl; + ++passed; + + std::cout << "\n=== Step 104 Results: " << passed << " passed, " << failed << " failed ===" << std::endl; + return failed > 0 ? 1 : 0; +}