Step 106: add transform history panel

This commit is contained in:
Bill
2026-02-09 11:08:57 -07:00
parent f7da8eee8d
commit 26cab0dee0
5 changed files with 247 additions and 40 deletions

View File

@@ -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. |

View File

@@ -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)

View File

@@ -3,6 +3,10 @@
#include <vector>
#include <map>
#include <functional>
#include <chrono>
#include <ctime>
#include <iomanip>
#include <sstream>
#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<std::string> 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;

View File

@@ -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<const OptimizationLock*>(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<std::string, std::string> 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<Function*>(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<Parameter*>(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());

View File

@@ -0,0 +1,44 @@
// Step 106 TDD Test: Transform history timestamps
//
// Tests:
// 1. Transform history records timestamp entries
#include <cassert>
#include <iostream>
#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;
}