Skip to main content

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}