///|
/// Owns debugger configuration and application state for one frontend
/// session. Adapter resources and protocol state are held behind backend.
pub struct DebugSession {
priv executable : String
priv mut source_breakpoints : Array[SourceBreakpoint]
priv mut function_breakpoints : Array[FunctionBreakpoint]
priv mut next_breakpoint_id : Int
priv backend : &DebugBackend
priv debug_context : DebugContext
priv application : DebugApplicationState
priv source_sync_results : Map[Int, BreakpointSnapshot]
priv function_sync_results : Map[Int, FunctionBreakpointSnapshot]
}
///|
/// Creates a session around an injected debugger backend.
pub fn DebugSession::new(
executable : String,
backend : &DebugBackend,
debug_context? : DebugContext = DebugContext::empty(),
) -> DebugSession {
{
executable,
source_breakpoints: [],
function_breakpoints: [],
next_breakpoint_id: 1,
backend,
debug_context,
application: DebugApplicationState::new(),
source_sync_results: Map([]),
function_sync_results: Map([]),
}
}
///|
pub fn DebugSession::state(self : DebugSession) -> DebugSessionState {
self.application.phase
}
///|
pub fn DebugSession::execution_epoch(self : DebugSession) -> ExecutionEpoch? {
self.application.execution_epoch()
}
///|
pub fn DebugSession::stop_epoch(self : DebugSession) -> StopEpoch? {
self.application.stop_epoch()
}
///|
pub fn DebugSession::selected_thread(self : DebugSession) -> ThreadSelection? {
self.application.selected_thread()
}
///|
pub fn DebugSession::selected_frame(self : DebugSession) -> FrameSelection? {
self.application.selected_frame()
}
///|
/// Returns the currently loaded stack window for the stable stop.
pub fn DebugSession::stack_snapshot(self : DebugSession) -> StackSnapshot? {
self.application.stack_snapshot()
}
///|
/// Returns the selected frame without changing the original stop snapshot.
pub fn DebugSession::selected_frame_snapshot(
self : DebugSession,
) -> StackFrameSnapshot? {
self.application.selected_frame_snapshot()
}
///|
/// Selects a frame by physical stack index, loading consecutive windows when
/// necessary.
pub async fn DebugSession::select_frame(
self : DebugSession,
physical_index : Int,
) -> StackFrameSnapshot raise DebugSessionError {
guard physical_index >= 0 else { raise FrameNotFound(physical_index) }
for ;; {
guard self.application.phase is Stopped &&
self.application.stack_snapshot() is Some(stack) else {
raise OperationNotAllowed(
state=self.application.phase,
action="select a frame",
)
}
if stack.is_loaded(physical_index) {
return self.application.select_frame(physical_index)
}
guard !stack.is_complete() else { raise FrameNotFound(physical_index) }
self.load_next_stack_window(stack) |> ignore
}
}
///|
/// Selects the next visible caller without renumbering hidden frames.
pub async fn DebugSession::select_visible_caller_frame(
self : DebugSession,
) -> StackFrameSnapshot raise DebugSessionError {
for ;; {
guard self.application.phase is Stopped &&
self.application.stack_snapshot() is Some(stack) &&
self.application.selected_frame() is Some(frame) else {
if self.application.phase is Stopped {
raise NoFrameSelected
}
raise OperationNotAllowed(
state=self.application.phase,
action="select a caller frame",
)
}
match stack.visible_caller(frame) {
FrameAvailable(selection) =>
return self.application.select_frame(selection.index())
MoreFramesRequired(_) => self.load_next_stack_window(stack) |> ignore
VisibleFrameBoundary => raise NoVisibleCaller(frame.index())
}
}
}
///|
/// Selects the next visible callee without renumbering hidden frames.
pub fn DebugSession::select_visible_callee_frame(
self : DebugSession,
) -> StackFrameSnapshot raise DebugSessionError {
self.application.select_visible_callee()
}
///|
/// Loads the complete physical stack for the current thread. `totalFrames`
/// hints never terminate this loop; only a backend-confirmed short/empty page
/// marks the returned snapshot complete.
pub async fn DebugSession::complete_stack_snapshot(
self : DebugSession,
) -> StackSnapshot raise DebugSessionError {
for ;; {
guard self.application.phase is Stopped &&
self.application.stack_snapshot() is Some(stack) else {
raise OperationNotAllowed(
state=self.application.phase,
action="load the complete stack",
)
}
if stack.is_complete() {
return stack
}
self.load_next_stack_window(stack) |> ignore
}
}
///|
pub async fn DebugSession::add_source_breakpoint(
self : DebugSession,
source_path : String,
line : Int,
display_path? : String = source_path,
) -> BreakpointSnapshot {
let phase = self.application.phase
guard phase is Ready || phase is Stopped else {
raise OperationNotAllowed(state=phase, action="add source breakpoint")
}
let breakpoint = SourceBreakpoint::new(
self.next_breakpoint_id,
source_path,
line,
display_path~,
)
self.next_breakpoint_id += 1
self.source_breakpoints.push(breakpoint)
guard phase is Stopped else { return breakpoint.pending_snapshot() }
let result = self.synchronize_breakpoints(self.breakpoint_configuration())
result
.sources()
.iter()
.find_first(item => item.id() == breakpoint.id())
.unwrap_or(breakpoint.pending_snapshot())
}
///|
pub async fn DebugSession::add_function_breakpoint(
self : DebugSession,
request : FunctionBreakpointRequest,
) -> FunctionBreakpointSnapshot {
let phase = self.application.phase
guard phase is Ready || phase is Stopped else {
raise OperationNotAllowed(state=phase, action="add function breakpoint")
}
let current_package = self.debug_context.current_package()
let (package_name, function_name, entry_point) = match request {
CurrentPackageFunction(function_name) =>
(current_package, function_name, function_name == "main")
ImportedPackageFunction(package_alias, function_name) =>
(
self.debug_context.package_for_alias(package_alias),
function_name,
false,
)
}
let selector = if entry_point {
ExactPhysicalFunction("moonbit_main")
} else {
MoonBitFunctionFamily(
@name_mangle.mangle_top_level_function_base(package_name, function_name),
)
}
let breakpoint = FunctionBreakpoint::new(
self.next_breakpoint_id,
request.display_name(),
selector,
)
self.next_breakpoint_id += 1
self.function_breakpoints.push(breakpoint)
guard phase is Stopped else { return breakpoint.pending_snapshot() }
let result = self.synchronize_breakpoints(self.breakpoint_configuration())
result
.functions()
.iter()
.find_first(item => item.id() == breakpoint.id())
.unwrap_or(breakpoint.pending_snapshot())
}
///|
pub fn DebugSession::source_breakpoints(
self : DebugSession,
) -> Array[BreakpointSnapshot] {
let backend_snapshots = self.backend.breakpoint_snapshots()
let snapshots_by_id : Map[Int, BreakpointSnapshot] = Map([])
for snapshot in backend_snapshots {
snapshots_by_id.set(snapshot.id(), snapshot)
}
for id, snapshot in self.source_sync_results {
snapshots_by_id.set(id, snapshot)
}
self.source_breakpoints.map(breakpoint => {
match snapshots_by_id.get(breakpoint.id()) {
Some(snapshot) => snapshot
None => breakpoint.pending_snapshot()
}
})
}
///|
pub fn DebugSession::function_breakpoints(
self : DebugSession,
) -> Array[FunctionBreakpointSnapshot] {
let backend_snapshots = self.backend.function_breakpoint_snapshots()
let snapshots_by_id : Map[Int, FunctionBreakpointSnapshot] = Map([])
for snapshot in backend_snapshots {
snapshots_by_id.set(snapshot.id(), snapshot)
}
for id, snapshot in self.function_sync_results {
snapshots_by_id.set(id, snapshot)
}
self.function_breakpoints.map(breakpoint => {
match snapshots_by_id.get(breakpoint.id()) {
Some(snapshot) => snapshot
None => breakpoint.pending_snapshot()
}
})
}
///|
pub fn DebugSession::last_stop_snapshot(self : DebugSession) -> StopSnapshot? {
self.application.last_stop_snapshot()
}
///|
pub fn DebugSession::take_output(self : DebugSession) -> Array[DebugOutput] {
self.backend.take_output()
}
///|
async fn DebugSession::accept_outcome(
self : DebugSession,
outcome : RunOutcome,
) -> RunOutcome {
match outcome {
Stopped(snapshot) => {
let thread = ThreadSelection::initial()
let initial_window = StackWindow::initial()
let initial = self.backend.stack_snapshot(thread, initial_window) catch {
DebugBackendError::BackendFailure(message) =>
self.backend_failed(message, next_state=Ready)
error => self.backend_failed(@debug.to_string(error), next_state=Failed)
}
guard initial.thread() == thread && initial.loaded_start() == 0 else {
self.backend_failed(
"backend returned an invalid initial stack window",
next_state=Ready,
)
}
guard initial.loaded_count() <= initial_window.count() else {
self.backend_failed(
"backend returned more frames than the initial stack window",
next_state=Ready,
)
}
let stack = for stack = initial {
if stack.default_selection() is Some(_) || stack.is_complete() {
break stack
}
continue self.request_stack_extension(stack)
}
let initial_snapshot = match stack.default_selection() {
Some(selection) =>
match stack.frame(selection.index()) {
Some(frame) =>
StopSnapshot::new(
snapshot.reason(),
frame.display_name(),
frame.source_path(),
frame.line(),
frame.column(),
)
None => snapshot
}
None => snapshot
}
self.application.accept_stopped(initial_snapshot, stack)
RunOutcome::stopped(initial_snapshot)
}
Exited(exit_code) => {
self.application.accept_exit()
RunOutcome::exited(exit_code)
}
}
}
///|
async fn DebugSession::request_stack_extension(
self : DebugSession,
current : StackSnapshot,
) -> StackSnapshot raise DebugSessionError {
let window = StackWindow::new(
current.loaded_end(),
StackWindow::initial().count(),
) catch {
error => self.backend_failed(@debug.to_string(error), next_state=Failed)
}
let next = self.backend.stack_snapshot(current.thread(), window) catch {
DebugBackendError::BackendFailure(message) =>
self.backend_failed(message, next_state=Ready)
error => self.backend_failed(@debug.to_string(error), next_state=Failed)
}
current.validate_extension(next) catch {
error =>
self.backend_failed(
"backend returned an invalid stack extension: \{@debug.to_string(error)}",
next_state=Ready,
)
}
next
}
///|
async fn DebugSession::load_next_stack_window(
self : DebugSession,
current : StackSnapshot,
) -> StackSnapshot raise DebugSessionError {
let next = self.request_stack_extension(current)
self.application.update_stack(next)
next
}
///|
fn[A] DebugSession::backend_failed(
self : DebugSession,
message : String,
next_state~ : DebugSessionState,
) -> A raise DebugSessionError {
self.application.transition_without_stop(next_state)
raise AdapterFailure(message)
}
///|
pub async fn DebugSession::run(self : DebugSession) -> RunOutcome {
guard self.application.phase is Ready || self.application.phase is Stopped else {
raise DebugSessionError::OperationNotAllowed(
state=self.application.phase,
action="run the program",
)
}
self.application.begin_execution()
self.source_sync_results.clear()
self.function_sync_results.clear()
let outcome = self.backend.launch(
self.executable,
self.source_breakpoints.filter(item => item.enabled()),
self.function_breakpoints.filter(item => item.enabled()),
) catch {
DebugBackendError::BackendFailure(message) =>
self.backend_failed(message, next_state=Ready)
error => self.backend_failed(@debug.to_string(error), next_state=Failed)
}
self.accept_outcome(outcome)
}
///|
async fn DebugSession::resume_execution(
self : DebugSession,
action : String,
operation : async (ThreadSelection) -> RunOutcome,
) -> RunOutcome {
guard self.application.phase is Stopped &&
self.application.selected_thread() is Some(thread) else {
raise DebugSessionError::OperationNotAllowed(
state=self.application.phase,
action~,
)
}
self.application.begin_resume()
self.source_sync_results.clear()
self.function_sync_results.clear()
let outcome = operation(thread) catch {
DebugBackendError::BackendFailure(message) =>
self.backend_failed(message, next_state=Ready)
error => self.backend_failed(@debug.to_string(error), next_state=Failed)
}
self.accept_outcome(outcome)
}
///|
pub async fn DebugSession::continue_execution(
self : DebugSession,
) -> RunOutcome {
self.resume_execution("continue execution", thread => {
self.backend.continue_execution(thread)
})
}
///|
pub async fn DebugSession::next(self : DebugSession) -> RunOutcome {
self.resume_execution("step over", thread => self.backend.next(thread))
}
///|
pub async fn DebugSession::step(self : DebugSession) -> RunOutcome {
self.resume_execution("step into", thread => self.backend.step(thread))
}
///|
pub async fn DebugSession::finish(self : DebugSession) -> RunOutcome {
self.resume_execution("step out", thread => self.backend.finish(thread))
}
///|
pub async fn DebugSession::find_variable(
self : DebugSession,
path : VariablePath,
) -> VariableLookupResult {
guard self.application.phase is Stopped &&
self.application.selected_thread() is Some(thread) &&
self.application.selected_frame() is Some(frame) else {
raise DebugSessionError::OperationNotAllowed(
state=self.application.phase,
action="inspect a variable",
)
}
self.backend.find_variable(thread, frame, path) catch {
DebugBackendError::BackendFailure(message) =>
self.backend_failed(message, next_state=Ready)
error => self.backend_failed(@debug.to_string(error), next_state=Failed)
}
}
///|
/// Returns bounded local-variable summaries for the selected physical frame.
pub async fn DebugSession::local_variables(
self : DebugSession,
) -> LocalVariablesSnapshot {
guard self.application.phase is Stopped &&
self.application.selected_thread() is Some(thread) &&
self.application.selected_frame() is Some(frame) else {
raise DebugSessionError::OperationNotAllowed(
state=self.application.phase,
action="inspect local variables",
)
}
self.backend.local_variables(thread, frame) catch {
DebugBackendError::BackendFailure(message) =>
self.backend_failed(message, next_state=Ready)
error => self.backend_failed(@debug.to_string(error), next_state=Failed)
}
}
///|
pub async fn DebugSession::disconnect(self : DebugSession) -> Unit {
self.backend.disconnect() catch {
_ => ()
}
self.application.transition_without_stop(Exited)
}
///|
pub fn DebugSession::close(self : DebugSession) -> Unit {
self.backend.close()
self.application.transition_without_stop(Exited)
}