diff --git a/PROGRESS.md b/PROGRESS.md index bccadd2..b1fa151 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -206,6 +206,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1 - [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) - [x] Step 105: **IMPLEMENTED** — Optimization controls panel with constraint-aware buttons and summaries (2/2 tests pass) +- [x] Step 106: **IMPLEMENTED** — Transform history panel with undo controls and provenance coloring (1/1 tests pass) --- @@ -291,6 +292,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi **Step 103:** Compile and pass (1/1) **Step 104:** Compile and pass (2/2) **Step 105:** Compile and pass (2/2) +**Step 106:** Compile and pass (1/1) --- @@ -405,3 +407,4 @@ Sprint 4 in progress. Step 76 (LayoutManager) done. Next: Step 77 (custom code e | 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. | | 2026-02-09 | Codex | Step 105: Optimization controls panel with constraint-aware buttons and summaries. 2/2 tests pass. | +| 2026-02-09 | Codex | Step 106: Transform history panel with undo controls and provenance coloring. 1/1 tests pass. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 56e426a..124345d 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -570,6 +570,10 @@ add_executable(step105_test tests/step105_test.cpp) target_include_directories(step105_test PRIVATE src) target_link_libraries(step105_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step106_test tests/step106_test.cpp) +target_include_directories(step106_test PRIVATE src) +target_link_libraries(step106_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/IncrementalOptimizer.h b/editor/src/IncrementalOptimizer.h index 361d6be..7a323d9 100644 --- a/editor/src/IncrementalOptimizer.h +++ b/editor/src/IncrementalOptimizer.h @@ -3,6 +3,10 @@ #include #include #include +#include +#include +#include +#include #include "ast/ASTNode.h" #include "ast/Expression.h" #include "ast/Statement.h" @@ -13,6 +17,7 @@ public: std::string transformId; std::string transformName; std::vector affectedNodeIds; + std::string timestamp; }; void setRoot(ASTNode* root) { root_ = root; } @@ -23,6 +28,7 @@ public: InternalRecord record; record.info.transformId = tid; record.info.transformName = transformName; + record.info.timestamp = currentTimestamp(); if (transformName == "constant-fold") { applyConstantFolding(root_, tid, record); @@ -70,6 +76,20 @@ public: return it != provenance_.end() ? it->second : ""; } + static std::string currentTimestamp() { + auto now = std::chrono::system_clock::now(); + std::time_t tt = std::chrono::system_clock::to_time_t(now); + std::tm tm{}; +#ifdef _WIN32 + localtime_s(&tm, &tt); +#else + localtime_r(&tt, &tm); +#endif + std::ostringstream oss; + oss << std::put_time(&tm, "%H:%M:%S"); + return oss.str(); + } + private: ASTNode* root_ = nullptr; int nextId_ = 1; diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 7f12e4f..ef22acf 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -81,6 +81,7 @@ struct BufferState { int generatedHighlightLine = -1; float splitScrollX = 0.0f; float splitScrollY = 0.0f; + IncrementalOptimizer incrementalOptimizer; }; struct EditorState { @@ -227,6 +228,7 @@ struct EditorState { state->editor.setContent(content, language); state->sync.setText(content, language); state->sync.syncNow(); + state->incrementalOptimizer.setRoot(state->sync.getAST()); state->editBuf = content; state->highlightsDirty = true; state->generatedHighlightsDirty = true; @@ -305,6 +307,7 @@ struct EditorState { active()->editor.setContent(active()->editBuf, lang); active()->sync.setText(active()->editBuf, lang); active()->sync.syncNow(); + active()->incrementalOptimizer.setRoot(active()->sync.getAST()); active()->highlightsDirty = true; } @@ -314,6 +317,7 @@ struct EditorState { active()->editor.setContent(active()->editBuf, active()->language); active()->sync.setText(active()->editBuf, active()->language); active()->sync.syncNow(); + active()->incrementalOptimizer.setRoot(active()->sync.getAST()); active()->highlightsDirty = true; active()->generatedHighlightsDirty = true; active()->modified = true; @@ -329,6 +333,7 @@ struct EditorState { active()->editBuf = active()->editor.getContent(); active()->sync.setText(active()->editBuf, active()->language); active()->sync.syncNow(); + active()->incrementalOptimizer.setRoot(active()->sync.getAST()); active()->highlightsDirty = true; active()->generatedHighlightsDirty = true; } @@ -339,6 +344,7 @@ struct EditorState { active()->editBuf = active()->editor.getContent(); active()->sync.setText(active()->editBuf, active()->language); active()->sync.syncNow(); + active()->incrementalOptimizer.setRoot(active()->sync.getAST()); active()->highlightsDirty = true; active()->generatedHighlightsDirty = true; } @@ -371,6 +377,7 @@ struct EditorState { active()->editBuf = active()->editor.getContent(); active()->sync.setText(active()->editBuf, active()->language); active()->sync.syncNow(); + active()->incrementalOptimizer.setRoot(active()->sync.getAST()); active()->highlightsDirty = true; active()->generatedHighlightsDirty = true; active()->modified = true; @@ -439,6 +446,7 @@ struct EditorState { buf->editor.setContent(buf->editBuf, buf->language); buf->sync.setText(buf->editBuf, buf->language); buf->sync.syncNow(); + buf->incrementalOptimizer.setRoot(buf->sync.getAST()); buf->highlightsDirty = true; buf->generatedHighlightsDirty = true; buf->modified = false; @@ -512,6 +520,7 @@ struct EditorState { active()->readOnly = true; active()->sync.setText(generated, targetLanguage); active()->sync.setAST(std::move(projected)); + active()->incrementalOptimizer.setRoot(active()->sync.getAST()); active()->editBuf = generated; active()->editor.setContent(active()->editBuf, targetLanguage); active()->mode.setLanguage(targetLanguage); @@ -639,6 +648,28 @@ static std::string findOptimizationBlockReason(const ASTNode* node) { return ""; } +static std::string findOptimizationLockWarning(const ASTNode* node) { + if (!node) return ""; + if (node->conceptType == "OptimizationLock") { + auto* lock = static_cast(node); + if (lock->lockLevel == "warning") { + return "OptimizationLock: " + lock->lockReason + + " (locked by " + lock->lockedBy + ")"; + } + } + for (auto* child : node->allChildren()) { + std::string found = findOptimizationLockWarning(child); + if (!found.empty()) return found; + } + return ""; +} + +static ImVec4 transformColorForName(const std::string& name) { + if (name == "constant-fold") return ImVec4(0.45f, 0.85f, 0.45f, 1.0f); + if (name == "dead-code-elim") return ImVec4(0.90f, 0.45f, 0.45f, 1.0f); + return ImVec4(0.75f, 0.75f, 0.75f, 1.0f); +} + // --------------------------------------------------------------------------- // ImGui InputTextMultiline with std::string resize callback @@ -2027,22 +2058,31 @@ int main(int, char**) { if (blocked) ImGui::BeginDisabled(); if (ImGui::Button("Constant Fold")) { - TransformEngine engine; - engine.setRoot(ast); - auto res = engine.constantFolding(); + auto& inc = state.active()->incrementalOptimizer; + inc.setRoot(ast); + std::string tid = inc.applyTransform("constant-fold"); + auto history = inc.getTransformHistory(); + size_t nodes = 0; + for (const auto& h : history) { + if (h.transformId == tid) { + nodes = h.affectedNodeIds.size(); + break; + } + } + std::string warning = findOptimizationLockWarning(ast); std::string summary = "Constant Fold: "; - if (res.applied) { - summary += "applied (" + std::to_string(res.nodesModified) + " nodes)"; + if (nodes > 0) { + summary += "applied (" + std::to_string(nodes) + " nodes)"; } else { summary += "no changes"; } - if (!res.warning.empty()) { - summary += " — warning: " + res.warning; - state.outputLog += res.warning + "\n"; + if (!warning.empty()) { + summary += " — warning: " + warning; + state.outputLog += warning + "\n"; } state.optimizeFoldSummary = summary; state.outputLog += summary + "\n"; - if (res.applied) state.refreshActiveTextFromAST(); + if (nodes > 0) state.refreshActiveTextFromAST(); } showBlockedTooltip(blockReason); if (blocked) ImGui::EndDisabled(); @@ -2054,22 +2094,26 @@ int main(int, char**) { if (blocked) ImGui::BeginDisabled(); if (ImGui::Button("Dead Code Elimination")) { - TransformEngine engine; - engine.setRoot(ast); - auto res = engine.deadCodeElimination(); + auto& inc = state.active()->incrementalOptimizer; + inc.setRoot(ast); + std::string tid = inc.applyTransform("dead-code-elim"); + auto history = inc.getTransformHistory(); + size_t nodes = 0; + for (const auto& h : history) { + if (h.transformId == tid) { + nodes = h.affectedNodeIds.size(); + break; + } + } std::string summary = "Dead Code Elimination: "; - if (res.applied) { - summary += "applied (" + std::to_string(res.nodesModified) + " nodes)"; + if (nodes > 0) { + summary += "applied (" + std::to_string(nodes) + " nodes)"; } else { summary += "no changes"; } - if (!res.warning.empty()) { - summary += " — warning: " + res.warning; - state.outputLog += res.warning + "\n"; - } state.optimizeDeadCodeSummary = summary; state.outputLog += summary + "\n"; - if (res.applied) state.refreshActiveTextFromAST(); + if (nodes > 0) state.refreshActiveTextFromAST(); } showBlockedTooltip(blockReason); if (blocked) ImGui::EndDisabled(); @@ -2081,33 +2125,31 @@ int main(int, char**) { if (blocked) ImGui::BeginDisabled(); if (ImGui::Button("Apply All")) { - TransformEngine engine; - engine.setRoot(ast); - auto results = engine.applyAll(); - int totalNodes = 0; - bool applied = false; - std::string warnings; - for (const auto& res : results) { - totalNodes += res.nodesModified; - if (res.applied) applied = true; - if (!res.warning.empty()) { - if (!warnings.empty()) warnings += "; "; - warnings += res.warning; + auto& inc = state.active()->incrementalOptimizer; + inc.setRoot(ast); + std::string tidFold = inc.applyTransform("constant-fold"); + std::string tidDce = inc.applyTransform("dead-code-elim"); + auto history = inc.getTransformHistory(); + size_t totalNodes = 0; + for (const auto& h : history) { + if (h.transformId == tidFold || h.transformId == tidDce) { + totalNodes += h.affectedNodeIds.size(); } } + std::string warning = findOptimizationLockWarning(ast); std::string summary = "Apply All: "; - if (applied) { + if (totalNodes > 0) { summary += "applied (" + std::to_string(totalNodes) + " nodes)"; } else { summary += "no changes"; } - if (!warnings.empty()) { - summary += " — warning: " + warnings; - state.outputLog += warnings + "\n"; + if (!warning.empty()) { + summary += " — warning: " + warning; + state.outputLog += warning + "\n"; } state.optimizeApplySummary = summary; state.outputLog += summary + "\n"; - if (applied) state.refreshActiveTextFromAST(); + if (totalNodes > 0) state.refreshActiveTextFromAST(); } showBlockedTooltip(blockReason); if (blocked) ImGui::EndDisabled(); @@ -2119,12 +2161,78 @@ int main(int, char**) { ImGui::EndTabItem(); } + // Transform history + if (ImGui::BeginTabItem("Transforms")) { + ImGui::PushFont(uiFont); + auto* buf = state.active(); + Module* ast = buf ? buf->sync.getAST() : nullptr; + if (!buf) { + ImGui::TextDisabled("(no active buffer)"); + } else if (!ast) { + ImGui::TextDisabled("(no AST)"); + } else { + auto& inc = buf->incrementalOptimizer; + inc.setRoot(ast); + auto history = inc.getTransformHistory(); + + const bool hasHistory = !history.empty(); + if (!hasHistory) ImGui::BeginDisabled(); + if (ImGui::Button("Undo All")) { + bool any = false; + while (inc.undoLast()) { + any = true; + } + if (any) state.refreshActiveTextFromAST(); + } + if (!hasHistory) ImGui::EndDisabled(); + ImGui::Separator(); + + if (!hasHistory) { + ImGui::TextDisabled("(no transforms)"); + } else { + for (const auto& h : history) { + ImGui::PushID(h.transformId.c_str()); + ImVec4 color = transformColorForName(h.transformName); + ImGui::TextColored(color, "%s", h.transformName.c_str()); + ImGui::SameLine(); + ImGui::TextDisabled("@ %s", h.timestamp.c_str()); + ImGui::SameLine(); + if (ImGui::Button("Undo")) { + if (inc.undoTransform(h.transformId)) { + state.refreshActiveTextFromAST(); + } + } + ImGui::Text("Affected: %d", (int)h.affectedNodeIds.size()); + std::string nodeList; + for (size_t i = 0; i < h.affectedNodeIds.size() && i < 5; ++i) { + if (!nodeList.empty()) nodeList += ", "; + nodeList += h.affectedNodeIds[i]; + } + if (h.affectedNodeIds.size() > 5) nodeList += ", ..."; + if (nodeList.empty()) nodeList = "(none)"; + ImGui::TextWrapped("Nodes: %s", nodeList.c_str()); + ImGui::Separator(); + ImGui::PopID(); + } + } + } + ImGui::PopFont(); + ImGui::EndTabItem(); + } + // AST view if (ImGui::BeginTabItem("AST")) { ImGui::PushFont(monoFont); ImGui::BeginChild("##astScroll", ImVec2(0, 0), false); Module* ast = state.active() ? state.active()->sync.getAST() : nullptr; if (ast) { + std::map transformNames; + if (state.active()) { + auto history = state.active()->incrementalOptimizer.getTransformHistory(); + for (const auto& h : history) { + transformNames[h.transformId] = h.transformName; + } + } // Show basic AST info ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "Module: %s [%s]", @@ -2133,13 +2241,41 @@ int main(int, char**) { auto functions = ast->getChildren("functions"); for (size_t i = 0; i < functions.size(); ++i) { auto* fn = static_cast(functions[i]); - ImGui::TextColored(ImVec4(0.86f, 0.86f, 0.55f, 1.0f), - " Function: %s", fn->name.c_str()); + std::string tid; + std::string tname; + if (state.active()) { + tid = state.active()->incrementalOptimizer.getProvenance(fn->id); + auto it = transformNames.find(tid); + if (it != transformNames.end()) tname = it->second; + } + ImVec4 fnColor = tname.empty() ? + ImVec4(0.86f, 0.86f, 0.55f, 1.0f) : + transformColorForName(tname); + ImGui::TextColored(fnColor, " Function: %s", fn->name.c_str()); + if (!tname.empty() && ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("Transform: %s (%s)", tname.c_str(), tid.c_str()); + ImGui::EndTooltip(); + } auto params = fn->getChildren("parameters"); for (auto* p : params) { auto* param = static_cast(p); - ImGui::TextColored(ImVec4(0.6f, 0.78f, 0.9f, 1.0f), - " param: %s", param->name.c_str()); + std::string pid; + std::string pname; + if (state.active()) { + pid = state.active()->incrementalOptimizer.getProvenance(param->id); + auto it = transformNames.find(pid); + if (it != transformNames.end()) pname = it->second; + } + ImVec4 pColor = pname.empty() ? + ImVec4(0.6f, 0.78f, 0.9f, 1.0f) : + transformColorForName(pname); + ImGui::TextColored(pColor, " param: %s", param->name.c_str()); + if (!pname.empty() && ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("Transform: %s (%s)", pname.c_str(), pid.c_str()); + ImGui::EndTooltip(); + } } auto body = fn->getChildren("body"); ImGui::Text(" body: %d statement(s)", (int)body.size()); diff --git a/editor/tests/step106_test.cpp b/editor/tests/step106_test.cpp new file mode 100644 index 0000000..856592b --- /dev/null +++ b/editor/tests/step106_test.cpp @@ -0,0 +1,44 @@ +// Step 106 TDD Test: Transform history timestamps +// +// Tests: +// 1. Transform history records timestamp entries + +#include +#include +#include "IncrementalOptimizer.h" +#include "ast/Module.h" +#include "ast/Function.h" +#include "ast/Statement.h" +#include "ast/Expression.h" + +int main() { + int passed = 0; + int failed = 0; + + Module mod("m1", "mod", "python"); + auto* fn = new Function("f1", "foo"); + auto* ret = new Return(); + ret->id = "r1"; + auto* bin = new BinaryOperation("b1", "+"); + auto* left = new IntegerLiteral("i1", 1); + auto* right = new IntegerLiteral("i2", 2); + bin->setChild("left", left); + bin->setChild("right", right); + ret->setChild("value", bin); + fn->addChild("body", ret); + mod.addChild("functions", fn); + + IncrementalOptimizer opt; + opt.setRoot(&mod); + opt.applyTransform("constant-fold"); + + auto history = opt.getTransformHistory(); + assert(!history.empty()); + assert(!history[0].timestamp.empty()); + + std::cout << "Test 1 PASS: timestamps recorded" << std::endl; + ++passed; + + std::cout << "\n=== Step 106 Results: " << passed << " passed, " << failed << " failed ===" << std::endl; + return failed > 0 ? 1 : 0; +}