logicaffeine_compile/debug_session.rs
1//! W11 — `DebugSession`: the one seam the DAP adapter and the Studio drawer both
2//! sit on.
3//!
4//! A thin DAP-vocabulary facade over the bytecode [`Debugger`](crate::debug), so
5//! the two consumers can never disagree about the paused program: `threads`,
6//! `stackTrace`, `scopes`/`variables`, the stepping verbs (including the headline
7//! `stepBack`/`reverseContinue` time-travel), and `evaluate` — routed through the
8//! Wave-10 [`frame_eval`](crate::frame_eval), the tree-walker oracle, so a watch
9//! expression means exactly what the same expression means in a program.
10//!
11//! The stdio DAP server (`largo dap`) is a serializer *over* this facade; the
12//! Studio debug drawer is a renderer *over* it. `session_parity_lock` proves both
13//! observe the identical state the underlying `Debugger` reports — the facade is
14//! a lens, never a second source of truth.
15
16use serde::Serialize;
17
18use crate::debug::Debugger;
19use crate::frame_eval::{frame_eval, Binding};
20
21/// A DAP thread. Single-task debugging (real tasks arrive with Wave 12's
22/// concurrency debugger) ⇒ exactly one main thread.
23#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
24pub struct DapThread {
25 pub id: i64,
26 pub name: String,
27}
28
29/// A DAP stack frame, innermost-first (DAP orders the current frame at index 0;
30/// the debugger snapshot lists Main first, current last, so the facade reverses).
31/// `line` carries the time-travel step — the honest bytecode-tier analogue of a
32/// source line, and what makes the scrubber and the editor gutter agree.
33#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
34pub struct DapFrame {
35 pub id: i64,
36 pub name: String,
37 pub line: usize,
38}
39
40/// A DAP variable in a frame's Locals scope.
41#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
42pub struct DapVariable {
43 pub name: String,
44 #[serde(rename = "type")]
45 pub kind: String,
46 pub value: String,
47}
48
49/// A debug session: owns a [`Debugger`] and answers DAP-shaped queries against it.
50pub struct DebugSession {
51 dbg: Debugger,
52}
53
54impl DebugSession {
55 /// `launch` — compile the program and stop before the first op.
56 pub fn launch(src: &str) -> Result<DebugSession, String> {
57 Ok(DebugSession {
58 dbg: Debugger::from_source(src)?,
59 })
60 }
61
62 /// `threads` — the single main thread.
63 pub fn threads(&self) -> Vec<DapThread> {
64 vec![DapThread {
65 id: 1,
66 name: "main".to_string(),
67 }]
68 }
69
70 /// `stackTrace` — the call frames, innermost-first (DAP order).
71 pub fn stack_trace(&self) -> Vec<DapFrame> {
72 let snap = self.dbg.snapshot();
73 let line = snap.step;
74 snap.frames
75 .iter()
76 .rev()
77 .enumerate()
78 .map(|(i, f)| DapFrame {
79 id: i as i64,
80 name: f.function.clone().unwrap_or_else(|| "Main".to_string()),
81 line,
82 })
83 .collect()
84 }
85
86 /// `variables` for a frame (addressed by its `stackTrace` id) — every register
87 /// in that frame, named when the compiler's debug-info pass knew the source
88 /// name (else `R{index}`).
89 pub fn variables(&self, frame_id: i64) -> Vec<DapVariable> {
90 let snap = self.dbg.snapshot();
91 let n = snap.frames.len();
92 // The DAP id is the reversed index; map back to Main-first order.
93 let idx = match n.checked_sub(1).and_then(|last| {
94 usize::try_from(frame_id)
95 .ok()
96 .and_then(|f| last.checked_sub(f))
97 }) {
98 Some(i) => i,
99 None => return Vec::new(),
100 };
101 match snap.frames.get(idx) {
102 None => Vec::new(),
103 Some(frame) => frame
104 .registers
105 .iter()
106 .map(|r| DapVariable {
107 name: r.name.clone().unwrap_or_else(|| format!("R{}", r.index)),
108 kind: r.kind.clone(),
109 value: r.value.clone(),
110 })
111 .collect(),
112 }
113 }
114
115 // ── the stepping verbs ─────────────────────────────────────────────────
116 /// `next` — step over the current op.
117 pub fn next(&mut self) {
118 self.dbg.step_over();
119 }
120 /// `stepIn` — execute exactly one op.
121 pub fn step_in(&mut self) {
122 self.dbg.step();
123 }
124 /// `stepOut` — run to the caller.
125 pub fn step_out(&mut self) {
126 self.dbg.step_out();
127 }
128 /// `continue` — run to the next breakpoint or the end.
129 pub fn continue_(&mut self) {
130 self.dbg.resume();
131 }
132 /// `stepBack` — the time-travel headline (`supportsStepBack: true`).
133 pub fn step_back(&mut self) {
134 self.dbg.step_back();
135 }
136 /// `reverseContinue` — run backwards to the previous breakpoint or the start.
137 pub fn reverse_continue(&mut self) {
138 self.dbg.reverse_resume();
139 }
140 /// `setBreakpoints` — arm one op-address breakpoint.
141 pub fn set_breakpoint(&mut self, pc: usize) {
142 self.dbg.set_breakpoint(pc);
143 }
144 /// Clear one op-address breakpoint.
145 pub fn clear_breakpoint(&mut self, pc: usize) {
146 self.dbg.clear_breakpoint(pc);
147 }
148
149 // ── state queries ──────────────────────────────────────────────────────
150 /// The time-travel cursor (ops executed so far).
151 pub fn step(&self) -> usize {
152 self.dbg.snapshot().step
153 }
154 /// The pc the session is stopped at (the op about to execute).
155 pub fn pc(&self) -> usize {
156 self.dbg.snapshot().pc
157 }
158 /// Whether the program is still running (not finished, not errored).
159 pub fn is_running(&self) -> bool {
160 self.dbg.is_running()
161 }
162 /// The program output emitted so far (the `Show` lines).
163 pub fn output(&self) -> Vec<String> {
164 self.dbg.snapshot().output
165 }
166
167 /// `evaluate` — evaluate a LOGOS expression against the current (innermost)
168 /// frame's named variables, through the Wave-10 frame evaluator (the
169 /// tree-walker oracle). Scalar bindings round-trip through LOGOS source;
170 /// complex values fail exactly as the tree-walker would.
171 pub fn evaluate(&self, expr: &str) -> Result<String, String> {
172 let snap = self.dbg.snapshot();
173 let frame = match snap.frames.last() {
174 Some(f) => f,
175 None => return Err("no active frame".to_string()),
176 };
177 let bindings: Vec<(String, String)> = frame
178 .registers
179 .iter()
180 .filter_map(|r| r.name.clone().map(|n| (n, r.value.clone())))
181 .collect();
182 let refs: Vec<Binding<'_>> = bindings
183 .iter()
184 .map(|(n, v)| Binding { name: n, value: v })
185 .collect();
186 frame_eval(&refs, expr)
187 }
188}