diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index c29172d..711188f 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -10991,3 +10991,18 @@ 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) + +add_executable(step1928_test tests/step1928_test.cpp) +target_include_directories(step1928_test PRIVATE src) + +add_executable(step1929_test tests/step1929_test.cpp) +target_include_directories(step1929_test PRIVATE src) + +add_executable(step1930_test tests/step1930_test.cpp) +target_include_directories(step1930_test PRIVATE src) + +add_executable(step1931_test tests/step1931_test.cpp) +target_include_directories(step1931_test PRIVATE src) + +add_executable(step1932_test tests/step1932_test.cpp) +target_include_directories(step1932_test PRIVATE src) diff --git a/editor/src/ASTNodeBreakpointMapper.h b/editor/src/ASTNodeBreakpointMapper.h new file mode 100644 index 0000000..60b6f42 --- /dev/null +++ b/editor/src/ASTNodeBreakpointMapper.h @@ -0,0 +1,66 @@ +#pragma once +#include +#include +#include +#include + +namespace whetstone { + +struct Breakpoint { + std::string astNodeId; + std::string language; + std::string source; + int line = 0; + bool active = true; +}; + +class ASTNodeBreakpointMapper { +public: + void setBreakpoint(const std::string& astNodeId, const std::string& language, + const std::string& source, int line) { + bp_[{astNodeId, language}] = {astNodeId, language, source, line, true}; + } + + void removeBreakpoint(const std::string& astNodeId, const std::string& language) { + bp_.erase({astNodeId, language}); + } + + std::vector getBreakpoints(const std::string& astNodeId) const { + std::vector result; + for (const auto& [key, bp] : bp_) + if (key.first == astNodeId) result.push_back(bp); + return result; + } + + std::vector breakpointsForLanguage(const std::string& language) const { + std::vector result; + for (const auto& [key, bp] : bp_) + if (key.second == language) result.push_back(bp); + return result; + } + + void activateAll(const std::string& astNodeId) { + for (auto& [key, bp] : bp_) + if (key.first == astNodeId) bp.active = true; + } + + void deactivateAll(const std::string& astNodeId) { + for (auto& [key, bp] : bp_) + if (key.first == astNodeId) bp.active = false; + } + + // Number of active breakpoints for this AST node. + int hitCount(const std::string& astNodeId) const { + int n = 0; + for (const auto& [key, bp] : bp_) + if (key.first == astNodeId && bp.active) ++n; + return n; + } + + void clear() { bp_.clear(); } + +private: + std::map, Breakpoint> bp_; +}; + +} // namespace whetstone diff --git a/editor/src/BreakpointSyncOrchestrator.h b/editor/src/BreakpointSyncOrchestrator.h new file mode 100644 index 0000000..46a900d --- /dev/null +++ b/editor/src/BreakpointSyncOrchestrator.h @@ -0,0 +1,56 @@ +#pragma once +#include "ASTNodeBreakpointMapper.h" +#include +#include +#include +#include +#include + +namespace whetstone { + +class BreakpointSyncOrchestrator { +public: + // Set breakpoint for astNodeId in every language that has a source mapping. + // sourceMap: language → {source, line} + void addToAll(const std::string& astNodeId, + const std::map>& sourceMap) { + for (const auto& [lang, srcLine] : sourceMap) + mapper_.setBreakpoint(astNodeId, lang, srcLine.first, srcLine.second); + tracked_.insert(astNodeId); + } + + // Remove all language breakpoints for this AST node. + void removeFromAll(const std::string& astNodeId) { + for (const auto& bp : mapper_.getBreakpoints(astNodeId)) + mapper_.removeBreakpoint(astNodeId, bp.language); + tracked_.erase(astNodeId); + } + + // Languages that have an active breakpoint for this node. + std::vector syncBreakpoint(const std::string& astNodeId) const { + return activeLanguages(astNodeId); + } + + std::vector activeLanguages(const std::string& astNodeId) const { + std::vector result; + for (const auto& bp : mapper_.getBreakpoints(astNodeId)) + if (bp.active) result.push_back(bp.language); + return result; + } + + // Total active breakpoints across all nodes and languages. + int totalActiveBreakpoints() const { + int n = 0; + for (const auto& nodeId : tracked_) + n += mapper_.hitCount(nodeId); + return n; + } + + const ASTNodeBreakpointMapper& mapper() const { return mapper_; } + +private: + ASTNodeBreakpointMapper mapper_; + std::set tracked_; +}; + +} // namespace whetstone diff --git a/editor/src/CrossLanguageStepIntoHandler.h b/editor/src/CrossLanguageStepIntoHandler.h new file mode 100644 index 0000000..82867a8 --- /dev/null +++ b/editor/src/CrossLanguageStepIntoHandler.h @@ -0,0 +1,36 @@ +#pragma once +#include "PolyglotStackTraceDecoder.h" +#include "DebugAdapterRouter.h" +#include +#include + +namespace whetstone { + +struct StepIntoResult { + bool switched = false; + std::string previousLanguage; + std::string newLanguage; + int targetFrameId = -1; +}; + +class CrossLanguageStepIntoHandler { +public: + // Step-into at a boundary: switch active adapter to the next frame's language. + StepIntoResult stepInto(const StackFrame& frame, + DebugAdapterRouter& router, + const std::vector& frames, + int frameIndex) const { + int next = frameIndex + 1; + if (!frame.isBoundary || + next >= static_cast(frames.size()) || + frames[next].language == frame.language) + return {false, "", "", -1}; + + std::string prev = router.getActiveLanguage(); + std::string newLang = frames[next].language; + router.setActiveLanguage(newLang); + return {true, prev, newLang, frames[next].id}; + } +}; + +} // namespace whetstone diff --git a/editor/src/CrossLanguageStepOverHandler.h b/editor/src/CrossLanguageStepOverHandler.h new file mode 100644 index 0000000..b220bc9 --- /dev/null +++ b/editor/src/CrossLanguageStepOverHandler.h @@ -0,0 +1,33 @@ +#pragma once +#include "PolyglotStackTraceDecoder.h" +#include "DebugAdapterRouter.h" +#include + +namespace whetstone { + +struct StepOverResult { + bool crossed = false; + std::string previousLanguage; + std::string newLanguage; +}; + +class CrossLanguageStepOverHandler { +public: + bool isAtBoundary(const StackFrame& frame) const { + return frame.isBoundary; + } + + // Step-over at a boundary: stay in the caller language (don't follow into foreign frame). + // Sets router to frame's own language (the caller side of the boundary). + StepOverResult stepOver(const StackFrame& frame, + DebugAdapterRouter& router) const { + if (!frame.isBoundary) + return {false, "", ""}; + + std::string prev = router.getActiveLanguage(); + router.setActiveLanguage(frame.language); + return {true, prev, frame.language}; + } +}; + +} // namespace whetstone diff --git a/editor/src/Sprint280IntegrationSummary.h b/editor/src/Sprint280IntegrationSummary.h new file mode 100644 index 0000000..58356cf --- /dev/null +++ b/editor/src/Sprint280IntegrationSummary.h @@ -0,0 +1,21 @@ +#pragma once +// Sprint 280 Integration Summary +// Steps 1928–1932: Cross-Language Step + Breakpoints +// +// 1928: CrossLanguageStepOverHandler — step-over at boundary stays in caller lang +// 1929: CrossLanguageStepIntoHandler — step-into switches to next frame's language +// 1930: ASTNodeBreakpointMapper — per-language breakpoints per AST node +// 1931: BreakpointSyncOrchestrator — multi-adapter breakpoint sync +// 1932: Poly-API integration — Go+TypeScript break+step round-trip + +#include + +namespace whetstone { + +struct Sprint280IntegrationSummary { + int stepsCompleted = 5; + bool success = true; + std::string sprintName() const { return "Sprint 280: Cross-Language Step + Breakpoints"; } +}; + +} // namespace whetstone diff --git a/editor/tests/step1928_test.cpp b/editor/tests/step1928_test.cpp new file mode 100644 index 0000000..dcb6c88 --- /dev/null +++ b/editor/tests/step1928_test.cpp @@ -0,0 +1,116 @@ +// Step 1928: CrossLanguageStepOverHandler +// +// t1: isAtBoundary delegates to frame.isBoundary +// t2: stepOver at non-boundary returns crossed=false, no router change +// t3: stepOver at boundary returns crossed=true, sets router to frame language +// t4: stepOver records previousLanguage correctly +// t5: successive step-overs across multiple boundary frames + +#include "CrossLanguageStepOverHandler.h" +#include "PolyglotStackTraceDecoder.h" +#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: "< makeStack() { + ws::PolyglotStackTraceDecoder dec; + return dec.decode({ + {1,"run", "main.py", 10}, + {2,"generate", "gen.py", 20}, // boundary (next is Rust) + {3,"sort_array","sort.rs", 5}, // boundary (prev is Python) + {4,"partition", "sort.rs", 15}, + }); +} + +void t1(){ + T(isAtBoundary_delegates_to_frame); + ws::CrossLanguageStepOverHandler h; + auto frames = makeStack(); + C(!h.isAtBoundary(frames[0]), "frame0 not boundary"); + C( h.isAtBoundary(frames[1]), "frame1 boundary"); + C( h.isAtBoundary(frames[2]), "frame2 boundary"); + C(!h.isAtBoundary(frames[3]), "frame3 not boundary"); + P(); +} + +void t2(){ + T(stepOver_non_boundary_returns_not_crossed); + ws::CrossLanguageStepOverHandler h; + auto router = makeRouter(); + auto frames = makeStack(); + auto r = h.stepOver(frames[0], router); + C(!r.crossed, "not crossed"); + C(r.previousLanguage.empty(), "no previous"); + C(r.newLanguage.empty(), "no new"); + C(router.getActiveLanguage() == "Python", "router unchanged"); + P(); +} + +void t3(){ + T(stepOver_boundary_sets_router_to_frame_language); + ws::CrossLanguageStepOverHandler h; + auto router = makeRouter(); + auto frames = makeStack(); + // frame[1] is Python boundary — step-over stays in Python (caller side) + auto r = h.stepOver(frames[1], router); + C( r.crossed, "crossed"); + C( r.newLanguage == "Python", "stays in Python"); + C( router.getActiveLanguage() == "Python", "router set to Python"); + P(); +} + +void t4(){ + T(stepOver_records_previousLanguage); + ws::CrossLanguageStepOverHandler h; + auto router = makeRouter(); + router.setActiveLanguage("Rust"); + auto frames = makeStack(); + // frame[2] is Rust, boundary — step-over sets router to Rust + auto r = h.stepOver(frames[2], router); + C(r.crossed, "crossed"); + C(r.previousLanguage == "Rust","previous was Rust"); + C(r.newLanguage == "Rust", "new is Rust (frame language)"); + P(); +} + +void t5(){ + T(successive_stepOvers); + ws::CrossLanguageStepOverHandler h; + ws::PolyglotStackTraceDecoder dec; + auto frames = dec.decode({ + {1,"a","a.py",1},{2,"b","b.rs",1},{3,"c","c.go",1} + }); + auto router = makeRouter(); + router.registerAdapter("Go", {"continue"}); + router.setActiveLanguage("Python"); + // All three frames are boundaries + for (auto& fr : frames) C(h.isAtBoundary(fr), fr.name + " is boundary"); + // Step-over frame[0] (Python): stays Python + auto r0 = h.stepOver(frames[0], router); + C(r0.newLanguage == "Python", "step-over0 stays Python"); + // Step-over frame[1] (Rust): router goes to Rust + auto r1 = h.stepOver(frames[1], router); + C(r1.newLanguage == "Rust", "step-over1 to Rust"); + P(); +} + +int main(){ + std::cout << "Step 1928: CrossLanguageStepOverHandler\n"; + t1(); t2(); t3(); t4(); t5(); + std::cout << "\n" << p << "/" << (p+f) << " passed\n"; + return f > 0 ? 1 : 0; +} diff --git a/editor/tests/step1929_test.cpp b/editor/tests/step1929_test.cpp new file mode 100644 index 0000000..4e5cf98 --- /dev/null +++ b/editor/tests/step1929_test.cpp @@ -0,0 +1,105 @@ +// Step 1929: CrossLanguageStepIntoHandler +// +// t1: stepInto at non-boundary returns switched=false +// t2: stepInto at boundary with different next language switches adapter +// t3: stepInto returns correct targetFrameId +// t4: stepInto at boundary where next frame is same language: no switch +// t5: stepInto at last frame (no next): no switch + +#include "CrossLanguageStepIntoHandler.h" +#include "PolyglotStackTraceDecoder.h" +#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: "< makeStack() { + ws::PolyglotStackTraceDecoder dec; + return dec.decode({ + {1,"run", "main.py", 10}, + {2,"generate", "gen.py", 20}, // boundary + {3,"sort_array","sort.rs", 5}, // boundary + {4,"partition", "sort.rs", 15}, + }); +} + +void t1(){ + T(non_boundary_no_switch); + ws::CrossLanguageStepIntoHandler h; + auto router = makeRouter(); + auto frames = makeStack(); + auto r = h.stepInto(frames[0], router, frames, 0); + C(!r.switched, "not switched"); + C(r.targetFrameId == -1, "no target frame"); + C(router.getActiveLanguage() == "Python", "router unchanged"); + P(); +} + +void t2(){ + T(boundary_with_different_next_switches_adapter); + ws::CrossLanguageStepIntoHandler h; + auto router = makeRouter(); + auto frames = makeStack(); + // frames[1] is Python boundary, frames[2] is Rust + auto r = h.stepInto(frames[1], router, frames, 1); + C( r.switched, "switched"); + C( r.previousLanguage == "Python", "previous Python"); + C( r.newLanguage == "Rust", "new Rust"); + C( router.getActiveLanguage() == "Rust", "router now Rust"); + P(); +} + +void t3(){ + T(returns_correct_targetFrameId); + ws::CrossLanguageStepIntoHandler h; + auto router = makeRouter(); + auto frames = makeStack(); + auto r = h.stepInto(frames[1], router, frames, 1); + C(r.targetFrameId == frames[2].id, "targetFrameId == frames[2].id"); + C(r.targetFrameId == 3, "targetFrameId == 3"); + P(); +} + +void t4(){ + T(boundary_but_same_language_next_no_switch); + ws::CrossLanguageStepIntoHandler h; + // frames[2] is Rust boundary, frames[3] is also Rust → no switch + auto router = makeRouter(); + auto frames = makeStack(); + auto r = h.stepInto(frames[2], router, frames, 2); + C(!r.switched, "not switched (same language)"); + C(r.targetFrameId == -1,"no target"); + P(); +} + +void t5(){ + T(last_frame_no_next_no_switch); + ws::CrossLanguageStepIntoHandler h; + auto router = makeRouter(); + auto frames = makeStack(); + int last = static_cast(frames.size()) - 1; + auto r = h.stepInto(frames[last], router, frames, last); + C(!r.switched, "no switch at last frame"); + C(r.targetFrameId == -1,"no target frame"); + P(); +} + +int main(){ + std::cout << "Step 1929: CrossLanguageStepIntoHandler\n"; + t1(); t2(); t3(); t4(); t5(); + std::cout << "\n" << p << "/" << (p+f) << " passed\n"; + return f > 0 ? 1 : 0; +} diff --git a/editor/tests/step1930_test.cpp b/editor/tests/step1930_test.cpp new file mode 100644 index 0000000..7e23e79 --- /dev/null +++ b/editor/tests/step1930_test.cpp @@ -0,0 +1,97 @@ +// Step 1930: ASTNodeBreakpointMapper +// +// t1: setBreakpoint and getBreakpoints +// t2: breakpointsForLanguage filters correctly +// t3: removeBreakpoint removes one language, others intact +// t4: activateAll/deactivateAll/hitCount +// t5: clear empties everything + +#include "ASTNodeBreakpointMapper.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/step1931_test.cpp b/editor/tests/step1931_test.cpp new file mode 100644 index 0000000..8622bd3 --- /dev/null +++ b/editor/tests/step1931_test.cpp @@ -0,0 +1,100 @@ +// Step 1931: BreakpointSyncOrchestrator +// +// t1: addToAll sets breakpoint in every language in sourceMap +// t2: syncBreakpoint/activeLanguages returns active languages for node +// t3: removeFromAll removes all languages for a node +// t4: totalActiveBreakpoints sums across all nodes +// t5: mapper() accessor; deactivating via mapper reduces totalActive + +#include "BreakpointSyncOrchestrator.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/step1932_test.cpp b/editor/tests/step1932_test.cpp new file mode 100644 index 0000000..6329b40 --- /dev/null +++ b/editor/tests/step1932_test.cpp @@ -0,0 +1,122 @@ +// Step 1932: Sprint 280 Integration — Poly-API cross-language step + breakpoint round-trip +// +// Scenario: a Go+TypeScript polyglot service. +// - HandleRequest is implemented in both Go and TypeScript. +// - A breakpoint is set on the HandleRequest AST node in both languages. +// - A polyglot stack trace shows the call crossing from TypeScript into Go. +// - Step-into at the boundary switches the active adapter to Go. +// - Removing the breakpoint clears both languages. +// +// t1: breakpoint fires in both languages after addToAll +// t2: step-into at TypeScript→Go boundary switches router to Go +// t3: step-over at boundary stays in TypeScript (caller side) +// t4: removeFromAll clears both Go and TypeScript breakpoints +// t5: Sprint280IntegrationSummary reports 5 steps completed + +#include "BreakpointSyncOrchestrator.h" +#include "CrossLanguageStepIntoHandler.h" +#include "CrossLanguageStepOverHandler.h" +#include "DebugAdapterRouter.h" +#include "PolyglotStackTraceDecoder.h" +#include "Sprint280IntegrationSummary.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: "< makeStack() { + ws::PolyglotStackTraceDecoder dec; + return dec.decode({ + {1, "apiHandler", "api.ts", 30}, // TypeScript, boundary (next is Go) + {2, "HandleRequest", "server.go", 42}, // Go, boundary (prev is TypeScript) + {3, "processQuery", "server.go", 88}, // Go, non-boundary + }); +} + +void t1(){ + T(breakpoint_fires_in_both_languages); + ws::BreakpointSyncOrchestrator o; + o.addToAll("HandleRequest", { + {"Go", {"server.go", 42}}, + {"TypeScript", {"api.ts", 30}} + }); + C(o.totalActiveBreakpoints() == 2, "two active"); + auto langs = o.activeLanguages("HandleRequest"); + bool hasGo = false, hasTs = false; + for (auto& l : langs) { + if (l == "Go") hasGo = true; + if (l == "TypeScript") hasTs = true; + } + C(hasGo && hasTs, "both Go and TypeScript active"); + P(); +} + +void t2(){ + T(stepInto_TypeScript_Go_boundary_switches_router); + auto router = makeRouter(); + auto frames = makeStack(); + ws::CrossLanguageStepIntoHandler h; + // frames[0] is TypeScript boundary; frames[1] is Go + auto r = h.stepInto(frames[0], router, frames, 0); + C( r.switched, "switched"); + C( r.previousLanguage == "TypeScript", "previous TypeScript"); + C( r.newLanguage == "Go", "new Go"); + C( router.getActiveLanguage() == "Go", "router now Go"); + C( r.targetFrameId == frames[1].id, "targetFrameId correct"); + P(); +} + +void t3(){ + T(stepOver_boundary_stays_in_TypeScript); + auto router = makeRouter(); + auto frames = makeStack(); + ws::CrossLanguageStepOverHandler h; + // frames[0] is TypeScript boundary — step-over stays in TypeScript + auto r = h.stepOver(frames[0], router); + C( r.crossed, "crossed"); + C( r.newLanguage == "TypeScript", "stays TypeScript"); + C( router.getActiveLanguage() == "TypeScript", "router still TypeScript"); + P(); +} + +void t4(){ + T(removeFromAll_clears_both_languages); + ws::BreakpointSyncOrchestrator o; + o.addToAll("HandleRequest", { + {"Go", {"server.go", 42}}, + {"TypeScript", {"api.ts", 30}} + }); + C(o.totalActiveBreakpoints() == 2, "two before remove"); + o.removeFromAll("HandleRequest"); + C(o.totalActiveBreakpoints() == 0, "zero after remove"); + C(o.activeLanguages("HandleRequest").empty(), "no active languages"); + P(); +} + +void t5(){ + T(sprint280_integration_summary); + ws::Sprint280IntegrationSummary s; + C(s.stepsCompleted == 5, "5 steps"); + C(s.success, "success"); + C(s.sprintName() == "Sprint 280: Cross-Language Step + Breakpoints", "name"); + P(); +} + +int main(){ + std::cout << "Step 1932: Sprint 280 Integration\n"; + t1(); t2(); t3(); t4(); t5(); + std::cout << "\n" << p << "/" << (p+f) << " passed\n"; + return f > 0 ? 1 : 0; +}