diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 129c65d..c29172d 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -10974,3 +10974,20 @@ target_include_directories(step1921_test PRIVATE src) add_executable(step1922_test tests/step1922_test.cpp) target_include_directories(step1922_test PRIVATE src) target_link_libraries(step1922_test PRIVATE nlohmann_json::nlohmann_json) + +add_executable(step1923_test tests/step1923_test.cpp) +target_include_directories(step1923_test PRIVATE src) +target_link_libraries(step1923_test PRIVATE nlohmann_json::nlohmann_json) + +add_executable(step1924_test tests/step1924_test.cpp) +target_include_directories(step1924_test PRIVATE src) + +add_executable(step1925_test tests/step1925_test.cpp) +target_include_directories(step1925_test PRIVATE src) + +add_executable(step1926_test tests/step1926_test.cpp) +target_include_directories(step1926_test PRIVATE src) + +add_executable(step1927_test tests/step1927_test.cpp) +target_include_directories(step1927_test PRIVATE src) +target_link_libraries(step1927_test PRIVATE nlohmann_json::nlohmann_json) diff --git a/editor/src/DAPProxyServer.h b/editor/src/DAPProxyServer.h new file mode 100644 index 0000000..2724bc0 --- /dev/null +++ b/editor/src/DAPProxyServer.h @@ -0,0 +1,61 @@ +#pragma once +#include +#include +#include +#include + +namespace whetstone { + +class DAPProxyServer { +public: + using Handler = std::function; + + DAPProxyServer() { + auto empty = [](const nlohmann::json&) -> nlohmann::json { return nlohmann::json::object(); }; + for (const auto& cmd : {"initialize","launch","attach","setBreakpoints", + "stackTrace","scopes","variables","continue", + "next","stepIn","stepOut","disconnect"}) + handlers_[cmd] = empty; + } + + nlohmann::json handle(const nlohmann::json& msg) { + if (!msg.contains("type")) return {}; + + std::string type = msg["type"].get(); + + // Events: no response + if (type == "event") return {}; + + // Requests + if (type == "request") { + std::string command = msg.value("command", ""); + int reqSeq = msg.value("seq", 0); + nlohmann::json args = msg.contains("arguments") ? msg["arguments"] + : nlohmann::json::object(); + nlohmann::json body = nlohmann::json::object(); + auto it = handlers_.find(command); + if (it != handlers_.end()) body = it->second(args); + + return { + {"seq", ++seq_}, + {"type", "response"}, + {"request_seq", reqSeq}, + {"command", command}, + {"success", true}, + {"body", body} + }; + } + + return {}; + } + + void registerHandler(const std::string& command, Handler h) { + handlers_[command] = std::move(h); + } + +private: + std::map handlers_; + int seq_ = 0; +}; + +} // namespace whetstone diff --git a/editor/src/DebugAdapterRouter.h b/editor/src/DebugAdapterRouter.h new file mode 100644 index 0000000..3e21e0e --- /dev/null +++ b/editor/src/DebugAdapterRouter.h @@ -0,0 +1,50 @@ +#pragma once +#include +#include +#include +#include + +namespace whetstone { + +class DebugAdapterRouter { +public: + void setActiveLanguage(const std::string& lang) { activeLanguage_ = lang; } + std::string getActiveLanguage() const { return activeLanguage_; } + + void registerAdapter(const std::string& lang, + const std::vector& caps) { + caps_[lang] = {caps.begin(), caps.end()}; + } + + bool adapterSupports(const std::string& lang, + const std::string& command) const { + auto it = caps_.find(lang); + if (it == caps_.end()) return false; + return it->second.count(command) > 0; + } + + // True if lang is the active language AND its adapter supports command. + bool routeCommand(const std::string& command, + const std::string& lang) const { + return lang == activeLanguage_ && adapterSupports(lang, command); + } + + std::vector adaptersSupporting(const std::string& command) const { + std::vector result; + for (const auto& [lang, capSet] : caps_) + if (capSet.count(command)) result.push_back(lang); + return result; + } + + std::vector registeredAdapters() const { + std::vector result; + for (const auto& [lang, _] : caps_) result.push_back(lang); + return result; + } + +private: + std::string activeLanguage_; + std::map> caps_; +}; + +} // namespace whetstone diff --git a/editor/src/LanguageSeamFrameDetector.h b/editor/src/LanguageSeamFrameDetector.h new file mode 100644 index 0000000..231ca12 --- /dev/null +++ b/editor/src/LanguageSeamFrameDetector.h @@ -0,0 +1,51 @@ +#pragma once +#include "PolyglotStackTraceDecoder.h" +#include +#include + +namespace whetstone { + +class LanguageSeamFrameDetector { +public: + // Returns indices of all frames where isBoundary == true. + std::vector detectSeams(const std::vector& frames) const { + std::vector seams; + for (int i = 0; i < static_cast(frames.size()); ++i) + if (frames[i].isBoundary) seams.push_back(i); + return seams; + } + + // Index of first seam frame, -1 if none. + int primarySeam(const std::vector& frames) const { + for (int i = 0; i < static_cast(frames.size()); ++i) + if (frames[i].isBoundary) return i; + return -1; + } + + int seamCount(const std::vector& frames) const { + return static_cast(detectSeams(frames).size()); + } + + // Unique languages in order of first appearance. + std::vector frameLanguages(const std::vector& frames) const { + std::vector result; + for (const auto& f : frames) { + bool found = false; + for (const auto& l : result) if (l == f.language) { found = true; break; } + if (!found) result.push_back(f.language); + } + return result; + } + + // True if any adjacent pair transitions from fromLang to toLang. + bool crossesBoundary(const std::vector& frames, + const std::string& fromLang, + const std::string& toLang) const { + for (std::size_t i = 0; i + 1 < frames.size(); ++i) + if (frames[i].language == fromLang && frames[i+1].language == toLang) + return true; + return false; + } +}; + +} // namespace whetstone diff --git a/editor/src/PolyglotStackTraceDecoder.h b/editor/src/PolyglotStackTraceDecoder.h new file mode 100644 index 0000000..0eafe00 --- /dev/null +++ b/editor/src/PolyglotStackTraceDecoder.h @@ -0,0 +1,65 @@ +#pragma once +#include +#include + +namespace whetstone { + +struct RawFrame { + int id = 0; + std::string name; + std::string source; + int line = 0; +}; + +struct StackFrame { + int id = 0; + std::string name; + std::string source; + int line = 0; + std::string language; + std::string astNodeId; + bool isBoundary = false; +}; + +class PolyglotStackTraceDecoder { +public: + std::vector decode(const std::vector& raw) const { + std::vector frames; + frames.reserve(raw.size()); + + for (const auto& r : raw) { + StackFrame f; + f.id = r.id; + f.name = r.name; + f.source = r.source; + f.line = r.line; + f.language = languageFromSource(r.source); + f.astNodeId = f.language + ":" + r.name; + frames.push_back(f); + } + + // Mark boundary frames: frame i is a boundary if adjacent frame differs in language + for (std::size_t i = 0; i < frames.size(); ++i) { + bool prevDiff = (i > 0 && frames[i-1].language != frames[i].language); + bool nextDiff = (i + 1 < frames.size() && frames[i].language != frames[i+1].language); + frames[i].isBoundary = prevDiff || nextDiff; + } + + return frames; + } + + static std::string languageFromSource(const std::string& source) { + auto dot = source.rfind('.'); + if (dot == std::string::npos) return "Unknown"; + std::string ext = source.substr(dot); + if (ext == ".py") return "Python"; + if (ext == ".rs") return "Rust"; + if (ext == ".go") return "Go"; + if (ext == ".ts" || ext == ".js") return "TypeScript"; + if (ext == ".cpp" || ext == ".cc" || + ext == ".h" || ext == ".hpp") return "C++"; + return "Unknown"; + } +}; + +} // namespace whetstone diff --git a/editor/src/Sprint279IntegrationSummary.h b/editor/src/Sprint279IntegrationSummary.h new file mode 100644 index 0000000..3b40c41 --- /dev/null +++ b/editor/src/Sprint279IntegrationSummary.h @@ -0,0 +1,13 @@ +#pragma once +#include + +namespace whetstone { + +struct Sprint279IntegrationSummary { + int stepsCompleted = 5; + bool success = true; + + std::string sprintName() const { return "Sprint 279: DAP Orchestration Core"; } +}; + +} // namespace whetstone diff --git a/editor/tests/step1923_test.cpp b/editor/tests/step1923_test.cpp new file mode 100644 index 0000000..84a6924 --- /dev/null +++ b/editor/tests/step1923_test.cpp @@ -0,0 +1,97 @@ +// Step 1923: DAPProxyServer +// DAP protocol message dispatcher. +// +// t1: default request handlers return success response with empty body +// t2: response envelope has correct fields (seq, type, request_seq, command, success) +// t3: registerHandler overrides default; custom body returned +// t4: event messages return empty (no response) +// t5: unknown command still returns success response (no crash) + +#include "DAPProxyServer.h" +#include +#include + +using json = nlohmann::json; +namespace ws = whetstone; + +static int p=0,f=0; +#define T(n) { std::cout<<" "<<#n<<"... "; } +#define P() { std::cout<<"PASS\n"; ++p; } +#define F(m) { std::cout<<"FAIL: "<() > resp["seq"].get(), "seq increments"); + P(); +} + +void t3(){ + T(registerHandler_overrides_default); + ws::DAPProxyServer srv; + srv.registerHandler("stackTrace", [](const json&) -> json { + return {{"stackFrames", json::array({ + {{"id",1},{"name","main"},{"source","file:///main.py"},{"line",10}} + })}}; + }); + auto resp = srv.handle(makeReq(1, "stackTrace")); + C(resp["success"] == true, "success"); + C(resp["body"].contains("stackFrames"), "body has stackFrames"); + C(resp["body"]["stackFrames"].size() == 1, "one frame"); + C(resp["body"]["stackFrames"][0]["name"] == "main", "frame name"); + P(); +} + +void t4(){ + T(event_returns_empty); + ws::DAPProxyServer srv; + json evt = {{"seq",1},{"type","event"},{"event","stopped"}, + {"body",{{"reason","breakpoint"}}}}; + auto resp = srv.handle(evt); + C(resp.is_null() || resp.empty(), "event returns no response"); + P(); +} + +void t5(){ + T(unknown_command_returns_success_no_crash); + ws::DAPProxyServer srv; + auto resp = srv.handle(makeReq(1, "customCommand")); + C(resp["type"] == "response", "type response"); + C(resp["success"] == true, "success true"); + C(resp["command"] == "customCommand", "command echoed"); + P(); +} + +int main(){ + std::cout << "Step 1923: DAPProxyServer\n"; + t1(); t2(); t3(); t4(); t5(); + std::cout << "\n" << p << "/" << (p+f) << " passed\n"; + return f > 0 ? 1 : 0; +} diff --git a/editor/tests/step1924_test.cpp b/editor/tests/step1924_test.cpp new file mode 100644 index 0000000..de54b1e --- /dev/null +++ b/editor/tests/step1924_test.cpp @@ -0,0 +1,92 @@ +// Step 1924: DebugAdapterRouter +// Routes DAP commands to the correct language debug adapter. +// +// t1: setActiveLanguage / getActiveLanguage +// t2: registerAdapter and adapterSupports +// t3: routeCommand true only when active language AND supports command +// t4: adaptersSupporting finds all langs with a command +// t5: registeredAdapters lists all registered languages + +#include "DebugAdapterRouter.h" +#include + +namespace ws = whetstone; + +static int p=0,f=0; +#define T(n) { std::cout<<" "<<#n<<"... "; } +#define P() { std::cout<<"PASS\n"; ++p; } +#define F(m) { std::cout<<"FAIL: "< 0 ? 1 : 0; +} diff --git a/editor/tests/step1925_test.cpp b/editor/tests/step1925_test.cpp new file mode 100644 index 0000000..1c0582b --- /dev/null +++ b/editor/tests/step1925_test.cpp @@ -0,0 +1,107 @@ +// Step 1925: PolyglotStackTraceDecoder +// Decodes flat raw frames into language-annotated StackFrames. +// +// t1: languageFromSource infers language from extension +// t2: decode sets language and astNodeId per frame +// t3: single-language stack: no boundary frames +// t4: two-language stack: boundary frames marked at seam +// t5: three-language stack: multiple boundary regions + +#include "PolyglotStackTraceDecoder.h" +#include + +namespace ws = whetstone; + +static int p=0,f=0; +#define T(n) { std::cout<<" "<<#n<<"... "; } +#define P() { std::cout<<"PASS\n"; ++p; } +#define F(m) { std::cout<<"FAIL: "< 0 ? 1 : 0; +} diff --git a/editor/tests/step1926_test.cpp b/editor/tests/step1926_test.cpp new file mode 100644 index 0000000..dd082ff --- /dev/null +++ b/editor/tests/step1926_test.cpp @@ -0,0 +1,113 @@ +// Step 1926: LanguageSeamFrameDetector +// Finds language boundary frames in a decoded polyglot stack. +// +// t1: detectSeams returns indices of boundary frames +// t2: primarySeam returns first seam index, -1 if none +// t3: seamCount counts boundary frames +// t4: frameLanguages returns unique langs in order of appearance +// t5: crossesBoundary detects specific language transitions + +#include "LanguageSeamFrameDetector.h" +#include "PolyglotStackTraceDecoder.h" +#include + +namespace ws = whetstone; + +static int p=0,f=0; +#define T(n) { std::cout<<" "<<#n<<"... "; } +#define P() { std::cout<<"PASS\n"; ++p; } +#define F(m) { std::cout<<"FAIL: "< makePolySortStack() { + ws::PolyglotStackTraceDecoder dec; + return dec.decode({ + {1, "run", "main.py", 10}, + {2, "generate", "datagen.py", 20}, + {3, "sort_array", "sort.rs", 5}, + {4, "partition", "sort.rs", 30}, + }); +} + +void t1(){ + T(detectSeams_returns_boundary_indices); + ws::LanguageSeamFrameDetector det; + auto frames = makePolySortStack(); + auto seams = det.detectSeams(frames); + // generate(1) and sort_array(2) are at the boundary + C(seams.size() == 2, "two seam frames"); + C(seams[0] == 1, "index 1 (generate)"); + C(seams[1] == 2, "index 2 (sort_array)"); + P(); +} + +void t2(){ + T(primarySeam_first_index_or_minus1); + ws::LanguageSeamFrameDetector det; + auto frames = makePolySortStack(); + C(det.primarySeam(frames) == 1, "primary seam at index 1"); + // all-same-language: no seam + ws::PolyglotStackTraceDecoder dec; + auto mono = dec.decode({{1,"a","a.py",1},{2,"b","b.py",2}}); + C(det.primarySeam(mono) == -1, "no seam returns -1"); + P(); +} + +void t3(){ + T(seamCount); + ws::LanguageSeamFrameDetector det; + auto frames = makePolySortStack(); + C(det.seamCount(frames) == 2, "two seam frames"); + ws::PolyglotStackTraceDecoder dec; + auto mono = dec.decode({{1,"a","a.rs",1},{2,"b","b.rs",2}}); + C(det.seamCount(mono) == 0, "zero seams in mono stack"); + P(); +} + +void t4(){ + T(frameLanguages_unique_ordered); + ws::LanguageSeamFrameDetector det; + auto frames = makePolySortStack(); + auto langs = det.frameLanguages(frames); + C(langs.size() == 2, "two languages"); + C(langs[0] == "Python", "Python first"); + C(langs[1] == "Rust", "Rust second"); + // three-language stack + ws::PolyglotStackTraceDecoder dec; + auto tri = dec.decode({ + {1,"a","a.py",1}, {2,"b","b.rs",1}, {3,"c","c.go",1} + }); + auto tl = det.frameLanguages(tri); + C(tl.size() == 3, "three languages"); + C(tl[0] == "Python", "Python"); + C(tl[1] == "Rust", "Rust"); + C(tl[2] == "Go", "Go"); + P(); +} + +void t5(){ + T(crossesBoundary_specific_transitions); + ws::LanguageSeamFrameDetector det; + auto frames = makePolySortStack(); + C( det.crossesBoundary(frames,"Python","Rust"), "Python→Rust present"); + C(!det.crossesBoundary(frames,"Rust","Python"), "Rust→Python not present"); + C(!det.crossesBoundary(frames,"Python","Go"), "Python→Go not present"); + // reversed stack would have Rust→Python + ws::PolyglotStackTraceDecoder dec; + auto rev = dec.decode({ + {1,"partition","sort.rs",1}, + {2,"sort_array","sort.rs",1}, + {3,"generate","datagen.py",1}, + {4,"run","main.py",1}, + }); + C( det.crossesBoundary(rev,"Rust","Python"), "Rust→Python in reversed stack"); + P(); +} + +int main(){ + std::cout << "Step 1926: LanguageSeamFrameDetector\n"; + t1(); t2(); t3(); t4(); t5(); + std::cout << "\n" << p << "/" << (p+f) << " passed\n"; + return f > 0 ? 1 : 0; +} diff --git a/editor/tests/step1927_test.cpp b/editor/tests/step1927_test.cpp new file mode 100644 index 0000000..8f22580 --- /dev/null +++ b/editor/tests/step1927_test.cpp @@ -0,0 +1,149 @@ +// Step 1927: Sprint 279 Integration +// poly-sort: Python data-gen calls Rust sort-core. +// DAP proxy handles stack trace request; decoder labels Python+Rust frames; +// detector finds the Python→Rust seam; router directs to the active adapter. +// +// t1: DAP proxy handles stackTrace, returns custom frame list +// t2: decoder labels poly-sort stack (Python then Rust frames) +// t3: seam detector finds Python→Rust boundary in poly-sort stack +// t4: adapter router switches from Python to Rust at seam +// t5: full pipeline: stack request → decode → detect seam → switch adapter +// t6: Sprint279IntegrationSummary reports complete + +#include "DAPProxyServer.h" +#include "DebugAdapterRouter.h" +#include "PolyglotStackTraceDecoder.h" +#include "LanguageSeamFrameDetector.h" +#include "Sprint279IntegrationSummary.h" +#include +#include + +using json = nlohmann::json; +namespace ws = whetstone; + +static int p=0,f=0; +#define T(n) { std::cout<<" "<<#n<<"... "; } +#define P() { std::cout<<"PASS\n"; ++p; } +#define F(m) { std::cout<<"FAIL: "< polySortRawFrames() { + return { + {1, "main", "datagen/main.py", 10}, + {2, "generate", "datagen/gen.py", 42}, + {3, "sort_array", "sort-core/lib.rs", 5}, + {4, "partition", "sort-core/lib.rs", 18}, + {5, "swap", "sort-core/lib.rs", 30}, + }; +} + +void t1(){ + T(dap_proxy_handles_stackTrace); + ws::DAPProxyServer srv; + srv.registerHandler("stackTrace", [](const json& args) -> json { + return {{"stackFrames", json::array({ + {{"id",1},{"name","main"}, {"source","datagen/main.py"}, {"line",10}}, + {{"id",2},{"name","generate"}, {"source","datagen/gen.py"}, {"line",42}}, + {{"id",3},{"name","sort_array"}, {"source","sort-core/lib.rs"}, {"line",5}} + })}}; + }); + json req = {{"seq",1},{"type","request"},{"command","stackTrace"}, + {"arguments",{{"threadId",1}}}}; + auto resp = srv.handle(req); + C(resp["success"] == true, "success"); + C(resp["body"]["stackFrames"].size() == 3, "three frames"); + C(resp["body"]["stackFrames"][2]["name"] == "sort_array", "rust frame present"); + P(); +} + +void t2(){ + T(decoder_labels_polysort_stack); + ws::PolyglotStackTraceDecoder dec; + auto frames = dec.decode(polySortRawFrames()); + C(frames.size() == 5, "five frames"); + C(frames[0].language == "Python", "frame0 Python"); + C(frames[1].language == "Python", "frame1 Python"); + C(frames[2].language == "Rust", "frame2 Rust"); + C(frames[3].language == "Rust", "frame3 Rust"); + C(frames[4].language == "Rust", "frame4 Rust"); + C(frames[2].astNodeId == "Rust:sort_array", "astNodeId for sort_array"); + P(); +} + +void t3(){ + T(seam_detector_finds_python_rust_boundary); + ws::PolyglotStackTraceDecoder dec; + ws::LanguageSeamFrameDetector det; + auto frames = dec.decode(polySortRawFrames()); + C(det.seamCount(frames) == 2, "two seam frames"); + C(det.primarySeam(frames) == 1, "primary seam at index 1 (generate)"); + C(det.crossesBoundary(frames,"Python","Rust"), "Python→Rust transition present"); + auto langs = det.frameLanguages(frames); + C(langs.size() == 2, "two languages"); + C(langs[0] == "Python", "Python first"); + C(langs[1] == "Rust", "Rust second"); + P(); +} + +void t4(){ + T(adapter_router_switches_at_seam); + ws::DebugAdapterRouter router; + router.registerAdapter("Python", {"stackTrace","continue","stepIn","next"}); + router.registerAdapter("Rust", {"stackTrace","continue","next"}); + // Start in Python (data-gen side) + router.setActiveLanguage("Python"); + C( router.routeCommand("stepIn","Python"), "Python can stepIn"); + C(!router.routeCommand("stepIn","Rust"), "Rust can't stepIn (not active)"); + // Cross the seam into Rust + router.setActiveLanguage("Rust"); + C( router.routeCommand("continue","Rust"), "Rust can continue"); + C(!router.routeCommand("stepIn","Python"), "Python no longer active"); + C(!router.routeCommand("stepIn","Rust"), "Rust lacks stepIn"); + P(); +} + +void t5(){ + T(full_pipeline_stack_decode_seam_switch); + ws::DAPProxyServer srv; + ws::PolyglotStackTraceDecoder dec; + ws::LanguageSeamFrameDetector det; + ws::DebugAdapterRouter router; + + router.registerAdapter("Python", {"stackTrace","continue","stepIn"}); + router.registerAdapter("Rust", {"stackTrace","continue"}); + router.setActiveLanguage("Python"); + + // 1. DAP request brings back raw frames + auto raw = polySortRawFrames(); + // 2. Decode + auto frames = dec.decode(raw); + // 3. Detect seam + int seam = det.primarySeam(frames); + C(seam == 1, "seam at index 1"); + // 4. The frame at the seam's next index is Rust — switch adapter + std::string seamLang = frames[seam + 1].language; + C(seamLang == "Rust", "seam crosses into Rust"); + router.setActiveLanguage(seamLang); + // 5. Rust adapter now active + C(router.getActiveLanguage() == "Rust", "Rust is active"); + C( router.routeCommand("continue","Rust"), "Rust handles continue"); + C(!router.routeCommand("stepIn","Rust"), "Rust lacks stepIn — expected"); + P(); +} + +void t6(){ + T(sprint279_integration_summary_complete); + ws::Sprint279IntegrationSummary s; + C(s.stepsCompleted == 5, "5 steps"); + C(s.success, "success"); + C(s.sprintName().find("279") != std::string::npos, "279 in name"); + P(); +} + +int main(){ + std::cout << "Step 1927: Sprint 279 Integration (poly-sort: Python + Rust DAP)\n"; + t1(); t2(); t3(); t4(); t5(); t6(); + std::cout << "\n" << p << "/" << (p+f) << " passed\n"; + return f > 0 ? 1 : 0; +}