1mod 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum BreakpointKind {
27 CallStart,
31 CallEnd,
33 Function,
35 Break,
37 Continue,
39 Return,
41 BlockStart,
43 BlockEnd,
45 ShowStart,
47 ShowEnd,
49 DocStart,
51 DocEnd,
53 BeforeCompile,
55 AfterCompile,
57}
58
59impl BreakpointKind {
60 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
96pub struct SourceBreakpoint {
97 pub line: u32,
99 pub column: Option<u32>,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct SourceBreakpointResolution {
106 pub requested: SourceBreakpoint,
108 pub resolved: Option<ResolvedSourceBreakpoint>,
110 pub pending: bool,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub struct ResolvedSourceBreakpoint {
117 pub id: usize,
119 pub kind: BreakpointKind,
121 pub line: u32,
123 pub column: u32,
125}
126
127static DEBUG_SESSION: RwLock<Option<DebugSession>> = RwLock::new(None);
128
129pub trait DebugSessionHandler: Send + Sync {
131 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
143pub 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 breakpoints: FxHashMap<FileId, Arc<BreakpointInfo>>,
151
152 pub handler: Arc<dyn DebugSessionHandler>,
154}
155
156impl DebugSession {
157 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 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 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 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
383pub 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
391pub 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
403pub 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
414pub 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
425fn 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
436fn 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}