tinymist_debug/
debugger.rs

1//! Tinymist breakpoint support for Typst.
2
3mod instr;
4
5use std::sync::Arc;
6
7use comemo::Tracked;
8use parking_lot::RwLock;
9use tinymist_analysis::location::{PositionEncoding, to_lsp_position};
10use tinymist_std::hash::{FxHashMap, FxHashSet};
11use tinymist_world::vfs::FileId;
12use typst::World;
13use typst::diag::FileResult;
14use typst::engine::Engine;
15use typst::foundations::{Binding, Context, Dict, Scopes, func};
16use typst::syntax::{Source, Span};
17use typst_shim::syntax::source_range;
18
19use crate::instrument::Instrumenter;
20
21#[derive(Default)]
22pub struct BreakpointInstr {}
23
24/// The kind of breakpoint.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum BreakpointKind {
27    // Expr,
28    // Line,
29    /// A call breakpoint.
30    CallStart,
31    /// A call breakpoint.
32    CallEnd,
33    /// A function breakpoint.
34    Function,
35    /// A break breakpoint.
36    Break,
37    /// A continue breakpoint.
38    Continue,
39    /// A return breakpoint.
40    Return,
41    /// A block start breakpoint.
42    BlockStart,
43    /// A block end breakpoint.
44    BlockEnd,
45    /// A show start breakpoint.
46    ShowStart,
47    /// A show end breakpoint.
48    ShowEnd,
49    /// A doc start breakpoint.
50    DocStart,
51    /// A doc end breakpoint.
52    DocEnd,
53    /// A before compile breakpoint.
54    BeforeCompile,
55    /// A after compile breakpoint.
56    AfterCompile,
57}
58
59impl BreakpointKind {
60    /// Converts the breakpoint kind to a string.
61    pub fn to_str(self) -> &'static str {
62        match self {
63            BreakpointKind::CallStart => "call_start",
64            BreakpointKind::CallEnd => "call_end",
65            BreakpointKind::Function => "function",
66            BreakpointKind::Break => "break",
67            BreakpointKind::Continue => "continue",
68            BreakpointKind::Return => "return",
69            BreakpointKind::BlockStart => "block_start",
70            BreakpointKind::BlockEnd => "block_end",
71            BreakpointKind::ShowStart => "show_start",
72            BreakpointKind::ShowEnd => "show_end",
73            BreakpointKind::DocStart => "doc_start",
74            BreakpointKind::DocEnd => "doc_end",
75            BreakpointKind::BeforeCompile => "before_compile",
76            BreakpointKind::AfterCompile => "after_compile",
77        }
78    }
79}
80
81#[derive(Default)]
82pub struct BreakpointInfo {
83    pub meta: Vec<BreakpointItem>,
84}
85
86pub struct BreakpointItem {
87    pub kind: BreakpointKind,
88    pub function_name: Option<String>,
89    pub origin_span: Span,
90}
91
92/// A source breakpoint requested by line and optional column.
93///
94/// Lines and columns are zero-based. Columns are measured as UTF-16 code units.
95#[derive(Debug, Clone, PartialEq, Eq, Hash)]
96pub struct SourceBreakpoint {
97    /// The requested source line.
98    pub line: u32,
99    /// The requested source column.
100    pub column: Option<u32>,
101}
102
103/// The result of resolving a source breakpoint to a structural breakpoint.
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct SourceBreakpointResolution {
106    /// The original requested breakpoint.
107    pub requested: SourceBreakpoint,
108    /// The structural breakpoint this request maps to, if any.
109    pub resolved: Option<ResolvedSourceBreakpoint>,
110    /// Whether the source breakpoint is waiting for instrumentation metadata.
111    pub pending: bool,
112}
113
114/// A structural breakpoint selected for a source breakpoint.
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub struct ResolvedSourceBreakpoint {
117    /// The structural breakpoint id within the instrumented source metadata.
118    pub id: usize,
119    /// The kind of structural breakpoint.
120    pub kind: BreakpointKind,
121    /// The resolved zero-based source line.
122    pub line: u32,
123    /// The resolved zero-based UTF-16 source column.
124    pub column: u32,
125}
126
127static DEBUG_SESSION: RwLock<Option<DebugSession>> = RwLock::new(None);
128
129/// The debug session handler.
130pub trait DebugSessionHandler: Send + Sync {
131    /// Called when a breakpoint is hit.
132    fn on_breakpoint(
133        &self,
134        engine: &Engine,
135        context: Tracked<Context>,
136        scopes: Scopes,
137        span: Span,
138        kind: BreakpointKind,
139        function_name: Option<String>,
140    );
141}
142
143/// The debug session.
144pub struct DebugSession {
145    enabled_function_breakpoints: FxHashSet<(FileId, usize, BreakpointKind)>,
146    enabled_source_breakpoints: FxHashSet<(FileId, usize, BreakpointKind)>,
147    function_breakpoints: FxHashSet<String>,
148    source_breakpoints: FxHashMap<FileId, Vec<SourceBreakpoint>>,
149    /// The breakpoint meta.
150    breakpoints: FxHashMap<FileId, Arc<BreakpointInfo>>,
151
152    /// The handler.
153    pub handler: Arc<dyn DebugSessionHandler>,
154}
155
156impl DebugSession {
157    /// Creates a new debug session.
158    pub fn new(handler: Arc<dyn DebugSessionHandler>) -> Self {
159        Self {
160            enabled_function_breakpoints: FxHashSet::default(),
161            enabled_source_breakpoints: FxHashSet::default(),
162            function_breakpoints: FxHashSet::default(),
163            source_breakpoints: FxHashMap::default(),
164            breakpoints: FxHashMap::default(),
165            handler,
166        }
167    }
168
169    /// Replaces the currently enabled function breakpoints by name.
170    pub fn set_function_breakpoints(&mut self, names: impl IntoIterator<Item = String>) {
171        self.function_breakpoints = names.into_iter().collect();
172        self.enabled_function_breakpoints.clear();
173
174        for (fid, info) in self.breakpoints.clone() {
175            self.enable_function_breakpoints_for(fid, &info);
176        }
177    }
178
179    /// Replaces source breakpoints for a file id.
180    pub fn set_source_breakpoints(
181        &mut self,
182        fid: FileId,
183        breakpoints: impl IntoIterator<Item = SourceBreakpoint>,
184    ) {
185        let breakpoints = breakpoints.into_iter().collect::<Vec<_>>();
186
187        if breakpoints.is_empty() {
188            self.source_breakpoints.remove(&fid);
189        } else {
190            self.source_breakpoints.insert(fid, breakpoints);
191        }
192
193        self.enabled_source_breakpoints
194            .retain(|(enabled_fid, _, _)| *enabled_fid != fid);
195    }
196
197    /// Replaces source breakpoints for a source and resolves them if metadata is available.
198    pub fn set_source_breakpoints_for(
199        &mut self,
200        source: &Source,
201        breakpoints: impl IntoIterator<Item = SourceBreakpoint>,
202    ) -> Vec<SourceBreakpointResolution> {
203        let fid = source.id();
204        self.set_source_breakpoints(fid, breakpoints);
205
206        let Some(info) = self.breakpoints.get(&fid).cloned() else {
207            return self.pending_source_breakpoints(fid);
208        };
209
210        self.enable_source_breakpoints_for(source, &info)
211    }
212
213    fn enable_breakpoints_for(
214        &mut self,
215        source: &Source,
216        info: &BreakpointInfo,
217    ) -> Vec<SourceBreakpointResolution> {
218        let fid = source.id();
219        self.enable_function_breakpoints_for(fid, info);
220        self.enable_source_breakpoints_for(source, info)
221    }
222
223    fn enable_function_breakpoints_for(&mut self, fid: FileId, info: &BreakpointInfo) {
224        for (id, item) in info.meta.iter().enumerate() {
225            if !matches!(item.kind, BreakpointKind::Function) {
226                continue;
227            }
228
229            if item
230                .function_name
231                .as_ref()
232                .is_some_and(|name| self.function_breakpoints.contains(name))
233            {
234                self.enabled_function_breakpoints
235                    .insert((fid, id, BreakpointKind::Function));
236            }
237        }
238    }
239
240    fn enable_source_breakpoints_for(
241        &mut self,
242        source: &Source,
243        info: &BreakpointInfo,
244    ) -> Vec<SourceBreakpointResolution> {
245        let fid = source.id();
246        self.enabled_source_breakpoints
247            .retain(|(enabled_fid, _, _)| *enabled_fid != fid);
248
249        let Some(breakpoints) = self.source_breakpoints.get(&fid).cloned() else {
250            return vec![];
251        };
252
253        let candidates = SourceBreakpointCandidate::collect(source, info);
254
255        breakpoints
256            .into_iter()
257            .map(|requested| {
258                let resolved =
259                    best_source_breakpoint_candidate(&requested, &candidates).map(|candidate| {
260                        self.enabled_source_breakpoints
261                            .insert((fid, candidate.id, candidate.kind));
262
263                        ResolvedSourceBreakpoint {
264                            id: candidate.id,
265                            kind: candidate.kind,
266                            line: candidate.line,
267                            column: candidate.column,
268                        }
269                    });
270
271                SourceBreakpointResolution {
272                    requested,
273                    resolved,
274                    pending: false,
275                }
276            })
277            .collect()
278    }
279
280    fn pending_source_breakpoints(&self, fid: FileId) -> Vec<SourceBreakpointResolution> {
281        self.source_breakpoints
282            .get(&fid)
283            .into_iter()
284            .flatten()
285            .cloned()
286            .map(|requested| SourceBreakpointResolution {
287                requested,
288                resolved: None,
289                pending: true,
290            })
291            .collect()
292    }
293
294    fn breakpoint_enabled(&self, bp: &(FileId, usize, BreakpointKind)) -> bool {
295        self.enabled_function_breakpoints.contains(bp)
296            || self.enabled_source_breakpoints.contains(bp)
297    }
298}
299
300#[derive(Debug, Clone, Copy)]
301struct SourceBreakpointCandidate {
302    id: usize,
303    kind: BreakpointKind,
304    line: u32,
305    column: u32,
306}
307
308impl SourceBreakpointCandidate {
309    fn collect(source: &Source, info: &BreakpointInfo) -> Vec<Self> {
310        info.meta
311            .iter()
312            .enumerate()
313            .filter_map(|(id, item)| {
314                if !is_source_breakpoint_target(item.kind) {
315                    return None;
316                }
317
318                let range = source_range(source, item.origin_span)?;
319                let position = to_lsp_position(range.start, PositionEncoding::Utf16, source);
320
321                Some(Self {
322                    id,
323                    kind: item.kind,
324                    line: position.line,
325                    column: position.character,
326                })
327            })
328            .collect()
329    }
330}
331
332fn best_source_breakpoint_candidate(
333    breakpoint: &SourceBreakpoint,
334    candidates: &[SourceBreakpointCandidate],
335) -> Option<SourceBreakpointCandidate> {
336    candidates
337        .iter()
338        .min_by_key(|candidate| {
339            let line_bucket = if matches!(candidate.kind, BreakpointKind::BlockEnd) {
340                3
341            } else if candidate.line == breakpoint.line {
342                0
343            } else if candidate.line > breakpoint.line {
344                1
345            } else {
346                2
347            };
348            let column = breakpoint.column.unwrap_or(0);
349
350            (
351                line_bucket,
352                candidate.line.abs_diff(breakpoint.line),
353                source_breakpoint_kind_priority(candidate.kind),
354                candidate.column.abs_diff(column),
355                candidate.id,
356            )
357        })
358        .copied()
359}
360
361fn is_source_breakpoint_target(kind: BreakpointKind) -> bool {
362    matches!(
363        kind,
364        BreakpointKind::Function
365            | BreakpointKind::BlockStart
366            | BreakpointKind::ShowStart
367            | BreakpointKind::Return
368            | BreakpointKind::BlockEnd
369    )
370}
371
372fn source_breakpoint_kind_priority(kind: BreakpointKind) -> u8 {
373    match kind {
374        BreakpointKind::Function => 0,
375        BreakpointKind::BlockStart => 1,
376        BreakpointKind::ShowStart => 2,
377        BreakpointKind::Return => 3,
378        BreakpointKind::BlockEnd => 4,
379        _ => 5,
380    }
381}
382
383/// Runs function with the debug session.
384pub fn with_debug_session<F, R>(f: F) -> Option<R>
385where
386    F: FnOnce(&DebugSession) -> R,
387{
388    Some(f(DEBUG_SESSION.read().as_ref()?))
389}
390
391/// Sets the debug session.
392pub fn set_debug_session(session: Option<DebugSession>) -> bool {
393    let mut lock = DEBUG_SESSION.write();
394
395    if session.is_some() && lock.is_some() {
396        return false;
397    }
398
399    let _ = std::mem::replace(&mut *lock, session);
400    true
401}
402
403/// Updates function breakpoints for the active debug session, if any.
404pub fn set_debug_function_breakpoints(names: impl IntoIterator<Item = String>) -> bool {
405    let mut session = DEBUG_SESSION.write();
406    let Some(session) = session.as_mut() else {
407        return false;
408    };
409
410    session.set_function_breakpoints(names);
411    true
412}
413
414/// Updates source breakpoints for an active debug session, if any.
415pub fn set_debug_source_breakpoints(
416    source: Source,
417    breakpoints: impl IntoIterator<Item = SourceBreakpoint>,
418) -> Option<Vec<SourceBreakpointResolution>> {
419    let mut session = DEBUG_SESSION.write();
420    let session = session.as_mut()?;
421
422    Some(session.set_source_breakpoints_for(&source, breakpoints))
423}
424
425/// Software breakpoints
426fn check_soft_breakpoint(span: Span, id: usize, kind: BreakpointKind) -> Option<bool> {
427    let fid = span.id()?;
428
429    let session = DEBUG_SESSION.read();
430    let session = session.as_ref()?;
431
432    let bp_feature = (fid, id, kind);
433    Some(session.breakpoint_enabled(&bp_feature))
434}
435
436/// Software breakpoints
437fn soft_breakpoint_handle(
438    engine: &Engine,
439    context: Tracked<Context>,
440    span: Span,
441    id: usize,
442    kind: BreakpointKind,
443    scope: Option<Dict>,
444) -> Option<()> {
445    let fid = span.id()?;
446
447    let (handler, origin_span, function_name) = {
448        let session = DEBUG_SESSION.read();
449        let session = session.as_ref()?;
450
451        let bp_feature = (fid, id, kind);
452        if !session.breakpoint_enabled(&bp_feature) {
453            return None;
454        }
455
456        let item = session.breakpoints.get(&fid)?.meta.get(id)?;
457        (
458            session.handler.clone(),
459            item.origin_span,
460            item.function_name.clone(),
461        )
462    };
463
464    let mut scopes = Scopes::new(Some(engine.world.library()));
465    if let Some(scope) = scope {
466        for (key, value) in scope.into_iter() {
467            scopes.top.bind(key.into(), Binding::detached(value));
468        }
469    }
470
471    handler.on_breakpoint(engine, context, scopes, origin_span, kind, function_name);
472    Some(())
473}
474
475pub mod breakpoints {
476
477    use super::*;
478
479    macro_rules! bp_handler {
480        ($name:ident, $name2:expr, $name3:ident, $name4:expr, $title:expr, $kind:ident) => {
481            #[func(name = $name2, title = $title)]
482            pub fn $name(span: Span, id: usize) -> bool {
483                check_soft_breakpoint(span, id, BreakpointKind::$kind).unwrap_or_default()
484            }
485            #[func(name = $name4, title = $title)]
486            pub fn $name3(
487                engine: &Engine,
488                context: Tracked<Context>,
489                span: Span,
490                id: usize,
491                scope: Option<Dict>,
492            ) {
493                soft_breakpoint_handle(engine, context, span, id, BreakpointKind::$kind, scope);
494            }
495        };
496    }
497
498    bp_handler!(
499        __breakpoint_call_start,
500        "__breakpoint_call_start",
501        __breakpoint_call_start_handle,
502        "__breakpoint_call_start_handle",
503        "A Software Breakpoint at the start of a call.",
504        CallStart
505    );
506    bp_handler!(
507        __breakpoint_call_end,
508        "__breakpoint_call_end",
509        __breakpoint_call_end_handle,
510        "__breakpoint_call_end_handle",
511        "A Software Breakpoint at the end of a call.",
512        CallEnd
513    );
514    bp_handler!(
515        __breakpoint_function,
516        "__breakpoint_function",
517        __breakpoint_function_handle,
518        "__breakpoint_function_handle",
519        "A Software Breakpoint at the start of a function.",
520        Function
521    );
522    bp_handler!(
523        __breakpoint_break,
524        "__breakpoint_break",
525        __breakpoint_break_handle,
526        "__breakpoint_break_handle",
527        "A Software Breakpoint at a break.",
528        Break
529    );
530    bp_handler!(
531        __breakpoint_continue,
532        "__breakpoint_continue",
533        __breakpoint_continue_handle,
534        "__breakpoint_continue_handle",
535        "A Software Breakpoint at a continue.",
536        Continue
537    );
538    bp_handler!(
539        __breakpoint_return,
540        "__breakpoint_return",
541        __breakpoint_return_handle,
542        "__breakpoint_return_handle",
543        "A Software Breakpoint at a return.",
544        Return
545    );
546    bp_handler!(
547        __breakpoint_block_start,
548        "__breakpoint_block_start",
549        __breakpoint_block_start_handle,
550        "__breakpoint_block_start_handle",
551        "A Software Breakpoint at the start of a block.",
552        BlockStart
553    );
554    bp_handler!(
555        __breakpoint_block_end,
556        "__breakpoint_block_end",
557        __breakpoint_block_end_handle,
558        "__breakpoint_block_end_handle",
559        "A Software Breakpoint at the end of a block.",
560        BlockEnd
561    );
562    bp_handler!(
563        __breakpoint_show_start,
564        "__breakpoint_show_start",
565        __breakpoint_show_start_handle,
566        "__breakpoint_show_start_handle",
567        "A Software Breakpoint at the start of a show.",
568        ShowStart
569    );
570    bp_handler!(
571        __breakpoint_show_end,
572        "__breakpoint_show_end",
573        __breakpoint_show_end_handle,
574        "__breakpoint_show_end_handle",
575        "A Software Breakpoint at the end of a show.",
576        ShowEnd
577    );
578    bp_handler!(
579        __breakpoint_doc_start,
580        "__breakpoint_doc_start",
581        __breakpoint_doc_start_handle,
582        "__breakpoint_doc_start_handle",
583        "A Software Breakpoint at the start of a doc.",
584        DocStart
585    );
586    bp_handler!(
587        __breakpoint_doc_end,
588        "__breakpoint_doc_end",
589        __breakpoint_doc_end_handle,
590        "__breakpoint_doc_end_handle",
591        "A Software Breakpoint at the end of a doc.",
592        DocEnd
593    );
594    bp_handler!(
595        __breakpoint_before_compile,
596        "__breakpoint_before_compile",
597        __breakpoint_before_compile_handle,
598        "__breakpoint_before_compile_handle",
599        "A Software Breakpoint before compilation.",
600        BeforeCompile
601    );
602    bp_handler!(
603        __breakpoint_after_compile,
604        "__breakpoint_after_compile",
605        __breakpoint_after_compile_handle,
606        "__breakpoint_after_compile_handle",
607        "A Software Breakpoint after compilation.",
608        AfterCompile
609    );
610}