tinymist_debug/debugger/
instr.rs

1use typst::diag::FileError;
2use typst::syntax::SyntaxNode;
3use typst::syntax::ast::{self, AstNode};
4
5use super::*;
6
7impl Instrumenter for BreakpointInstr {
8    fn instrument(&self, source: Source) -> FileResult<Source> {
9        let (new, meta) = instrument_breakpoints(source.clone())?;
10
11        let mut session = DEBUG_SESSION.write();
12        let session = session
13            .as_mut()
14            .ok_or_else(|| FileError::Other(Some("No active debug session".into())))?;
15
16        session.enable_breakpoints_for(&source, &meta);
17        session.breakpoints.insert(new.id(), meta);
18
19        Ok(new)
20    }
21}
22
23#[comemo::memoize]
24fn instrument_breakpoints(source: Source) -> FileResult<(Source, Arc<BreakpointInfo>)> {
25    let node = source.root();
26    let mut worker = InstrumentWorker {
27        meta: BreakpointInfo::default(),
28        instrumented: String::new(),
29    };
30
31    worker.visit_node(node);
32    let new_source: Source = Source::new(source.id(), worker.instrumented);
33
34    Ok((new_source, Arc::new(worker.meta)))
35}
36
37struct InstrumentWorker {
38    meta: BreakpointInfo,
39    instrumented: String,
40}
41
42impl InstrumentWorker {
43    fn instrument_block_child(&mut self, container: &SyntaxNode, b1: Span, b2: Span) {
44        for child in container.children() {
45            if b1 == child.span() || b2 == child.span() {
46                self.instrument_block(child);
47            } else {
48                self.visit_node(child);
49            }
50        }
51    }
52
53    fn visit_node(&mut self, node: &SyntaxNode) {
54        if let Some(expr) = node.cast::<ast::Expr>() {
55            match expr {
56                ast::Expr::CodeBlock(..) => {
57                    self.instrument_block(node);
58                    return;
59                }
60                ast::Expr::WhileLoop(while_expr) => {
61                    self.instrument_block_child(node, while_expr.body().span(), Span::detached());
62                    return;
63                }
64                ast::Expr::ForLoop(for_expr) => {
65                    self.instrument_block_child(node, for_expr.body().span(), Span::detached());
66                    return;
67                }
68                ast::Expr::Conditional(cond_expr) => {
69                    self.instrument_block_child(
70                        node,
71                        cond_expr.if_body().span(),
72                        cond_expr
73                            .else_body()
74                            .map(|expr| expr.span())
75                            .unwrap_or(Span::detached()),
76                    );
77                    return;
78                }
79                ast::Expr::Closure(closure) => {
80                    self.instrument_closure(node, closure);
81                    return;
82                }
83                ast::Expr::ShowRule(show_rule) => {
84                    let transform = show_rule.transform().to_untyped().span();
85
86                    for child in node.children() {
87                        if transform == child.span() {
88                            self.instrument_functor(child);
89                        } else {
90                            self.visit_node(child);
91                        }
92                    }
93                    return;
94                }
95                ast::Expr::FuncReturn(ret) => {
96                    self.instrument_return(node, ret);
97                    return;
98                }
99                ast::Expr::Text(..)
100                | ast::Expr::Space(..)
101                | ast::Expr::Linebreak(..)
102                | ast::Expr::Parbreak(..)
103                | ast::Expr::Escape(..)
104                | ast::Expr::Shorthand(..)
105                | ast::Expr::SmartQuote(..)
106                | ast::Expr::Strong(..)
107                | ast::Expr::Emph(..)
108                | ast::Expr::Raw(..)
109                | ast::Expr::Link(..)
110                | ast::Expr::Label(..)
111                | ast::Expr::Ref(..)
112                | ast::Expr::Heading(..)
113                | ast::Expr::ListItem(..)
114                | ast::Expr::EnumItem(..)
115                | ast::Expr::TermItem(..)
116                | ast::Expr::Equation(..)
117                | ast::Expr::Math(..)
118                | ast::Expr::MathText(..)
119                | ast::Expr::MathIdent(..)
120                | ast::Expr::MathShorthand(..)
121                | ast::Expr::MathAlignPoint(..)
122                | ast::Expr::MathDelimited(..)
123                | ast::Expr::MathAttach(..)
124                | ast::Expr::MathPrimes(..)
125                | ast::Expr::MathFrac(..)
126                | ast::Expr::MathRoot(..)
127                | ast::Expr::MathFieldAccess(..)
128                | ast::Expr::MathCall(..)
129                | ast::Expr::Ident(..)
130                | ast::Expr::None(..)
131                | ast::Expr::Auto(..)
132                | ast::Expr::Bool(..)
133                | ast::Expr::Int(..)
134                | ast::Expr::Float(..)
135                | ast::Expr::Numeric(..)
136                | ast::Expr::Str(..)
137                | ast::Expr::ContentBlock(..)
138                | ast::Expr::Parenthesized(..)
139                | ast::Expr::Array(..)
140                | ast::Expr::Dict(..)
141                | ast::Expr::Unary(..)
142                | ast::Expr::Binary(..)
143                | ast::Expr::FieldAccess(..)
144                | ast::Expr::FuncCall(..)
145                | ast::Expr::LetBinding(..)
146                | ast::Expr::DestructAssignment(..)
147                | ast::Expr::SetRule(..)
148                | ast::Expr::Contextual(..)
149                | ast::Expr::ModuleImport(..)
150                | ast::Expr::ModuleInclude(..)
151                | ast::Expr::LoopBreak(..)
152                | ast::Expr::LoopContinue(..) => {}
153            }
154        }
155
156        self.visit_node_fallback(node);
157    }
158
159    fn visit_node_fallback(&mut self, node: &SyntaxNode) {
160        let txt = node.leaf_text();
161        if !txt.is_empty() {
162            self.instrumented.push_str(txt);
163        }
164
165        for child in node.children() {
166            self.visit_node(child);
167        }
168    }
169
170    fn make_cov(&mut self, span: Span, kind: BreakpointKind) {
171        self.make_cov_with_scope(span, kind, "(:)", None);
172    }
173
174    fn make_cov_with_scope(
175        &mut self,
176        span: Span,
177        kind: BreakpointKind,
178        scope: &str,
179        function_name: Option<String>,
180    ) {
181        let it = self.meta.meta.len();
182        self.meta.meta.push(BreakpointItem {
183            kind,
184            function_name,
185            origin_span: span,
186        });
187        self.instrumented.push_str("if __breakpoint_");
188        self.instrumented.push_str(kind.to_str());
189        self.instrumented.push('(');
190        self.instrumented.push_str(&it.to_string());
191        self.instrumented.push_str(") {");
192        self.instrumented.push_str("__breakpoint_");
193        self.instrumented.push_str(kind.to_str());
194        self.instrumented.push_str("_handle(");
195        self.instrumented.push_str(&it.to_string());
196        self.instrumented.push_str(", ");
197        self.instrumented.push_str(scope);
198        self.instrumented.push_str("); ");
199        self.instrumented.push_str("};\n");
200    }
201
202    fn instrument_block(&mut self, child: &SyntaxNode) {
203        self.instrumented.push_str("{\n");
204        let (first, last) = {
205            let mut children = child.children();
206            let first = children
207                .next()
208                .map(|s| s.span())
209                .unwrap_or_else(Span::detached);
210            let last = children
211                .last()
212                .map(|s| s.span())
213                .unwrap_or_else(Span::detached);
214
215            (first, last)
216        };
217        self.make_cov(first, BreakpointKind::BlockStart);
218        self.visit_node_fallback(child);
219        self.instrumented.push('\n');
220        self.make_cov(last, BreakpointKind::BlockEnd);
221        self.instrumented.push_str("}\n");
222    }
223
224    fn instrument_functor(&mut self, child: &SyntaxNode) {
225        self.instrumented.push_str("{\nlet __bp_functor = ");
226        let s = child.span();
227        self.visit_node(child);
228        self.instrumented.push_str("\n__it => {");
229        self.make_cov(s, BreakpointKind::ShowStart);
230        self.instrumented.push_str("__bp_functor(__it); } }\n");
231    }
232
233    fn instrument_return(&mut self, node: &SyntaxNode, ret: ast::FuncReturn) {
234        self.instrumented.push_str("{\n");
235
236        if let Some(body) = ret.body() {
237            self.instrumented.push_str("let __tinymist_return_value = ");
238            self.visit_node(body.to_untyped());
239            self.instrumented.push_str(";\n");
240            self.make_cov(node.span(), BreakpointKind::Return);
241            self.instrumented
242                .push_str("return __tinymist_return_value\n");
243        } else {
244            self.make_cov(node.span(), BreakpointKind::Return);
245            self.instrumented.push_str("return\n");
246        }
247
248        self.instrumented.push_str("}\n");
249    }
250
251    fn instrument_closure(&mut self, node: &SyntaxNode, closure: ast::Closure) {
252        let body = closure.body().span();
253        let name = closure.name();
254        let origin = name.map_or_else(|| node.span(), |name| name.span());
255        let function_name = name.map(|name| name.as_str().to_owned());
256        let scope = Self::closure_scope(closure);
257
258        for child in node.children() {
259            if body == child.span() {
260                self.instrumented.push_str("{\n");
261                self.make_cov_with_scope(
262                    origin,
263                    BreakpointKind::Function,
264                    &scope,
265                    function_name.clone(),
266                );
267                let body_is_code_block = is_code_block(child);
268                if body_is_code_block {
269                    self.visit_node(child);
270                    self.instrumented.push('\n');
271                } else {
272                    self.instrumented.push('(');
273                    self.visit_node(child);
274                    self.instrumented.push(')');
275                    self.instrumented.push_str(";\n");
276                }
277                self.make_cov(body, BreakpointKind::Return);
278                self.instrumented.push_str("\n}\n");
279            } else {
280                self.visit_node(child);
281            }
282        }
283    }
284
285    fn closure_scope(closure: ast::Closure) -> String {
286        let mut bindings = Vec::new();
287        for param in closure.params().children() {
288            match param {
289                ast::Param::Pos(pattern) => {
290                    for binding in pattern.bindings() {
291                        Self::push_scope_binding(&mut bindings, binding);
292                    }
293                }
294                ast::Param::Named(named) => {
295                    Self::push_scope_binding(&mut bindings, named.name());
296                }
297                ast::Param::Spread(spread) => {
298                    if let Some(binding) = spread.sink_ident() {
299                        Self::push_scope_binding(&mut bindings, binding);
300                    }
301                }
302            }
303        }
304
305        if bindings.is_empty() {
306            return "(:)".to_owned();
307        }
308
309        let mut scope = String::from("(");
310        for (idx, binding) in bindings.iter().enumerate() {
311            if idx > 0 {
312                scope.push_str(", ");
313            }
314            scope.push_str(binding);
315            scope.push_str(": ");
316            scope.push_str(binding);
317        }
318        scope.push(')');
319        scope
320    }
321
322    fn push_scope_binding(bindings: &mut Vec<String>, binding: ast::Ident) {
323        let binding = binding.as_str();
324        if !bindings.iter().any(|it| it == binding) {
325            bindings.push(binding.to_owned());
326        }
327    }
328}
329
330fn is_code_block(node: &SyntaxNode) -> bool {
331    matches!(node.cast::<ast::Expr>(), Some(ast::Expr::CodeBlock(_)))
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    fn instr(input: &str) -> String {
339        let source = Source::detached(input);
340        let (new, _meta) = instrument_breakpoints(source).unwrap();
341        new.text().to_string()
342    }
343
344    #[test]
345    fn test_physica_vector() {
346        let instrumented = instr(include_str!(
347            "../fixtures/instr_coverage/physica_vector.typ"
348        ));
349        insta::assert_snapshot!(instrumented, @r#"
350        // A show rule, should be used like:
351        //   #show: super-plus-as-dagger
352        //   U^+U = U U^+ = I
353        // or in scope:
354        //   #[
355        //     #show: super-plus-as-dagger
356        //     U^+U = U U^+ = I
357        //   ]
358        #let super-plus-as-dagger(document) = {
359        if __breakpoint_function(0) {__breakpoint_function_handle(0, (document: document)); };
360        {
361        if __breakpoint_block_start(1) {__breakpoint_block_start_handle(1, (:)); };
362        {
363          show math.attach: {
364        let __bp_functor = elem => {
365        if __breakpoint_function(2) {__breakpoint_function_handle(2, (elem: elem)); };
366        {
367        if __breakpoint_block_start(3) {__breakpoint_block_start_handle(3, (:)); };
368        {
369            if __eligible(elem.base) and elem.at("t", default: none) == [+] {
370        if __breakpoint_block_start(4) {__breakpoint_block_start_handle(4, (:)); };
371        {
372              $attach(elem.base, t: dagger, b: elem.at("b", default: #none))$
373            }
374        if __breakpoint_block_end(5) {__breakpoint_block_end_handle(5, (:)); };
375        }
376         else {
377        if __breakpoint_block_start(6) {__breakpoint_block_start_handle(6, (:)); };
378        {
379              elem
380            }
381        if __breakpoint_block_end(7) {__breakpoint_block_end_handle(7, (:)); };
382        }
383
384          }
385        if __breakpoint_block_end(8) {__breakpoint_block_end_handle(8, (:)); };
386        }
387
388        if __breakpoint_return(9) {__breakpoint_return_handle(9, (:)); };
389
390        }
391
392        __it => {if __breakpoint_show_start(10) {__breakpoint_show_start_handle(10, (:)); };
393        __bp_functor(__it); } }
394
395
396          document
397        }
398        if __breakpoint_block_end(11) {__breakpoint_block_end_handle(11, (:)); };
399        }
400
401        if __breakpoint_return(12) {__breakpoint_return_handle(12, (:)); };
402
403        }
404        "#);
405    }
406
407    #[test]
408    fn test_playground() {
409        let instrumented = instr(include_str!("../fixtures/instr_coverage/playground.typ"));
410        insta::assert_snapshot!(instrumented, @"");
411    }
412
413    #[test]
414    fn test_instrument_breakpoint() {
415        let source = Source::detached("#let a = 1;");
416        let (new, _meta) = instrument_breakpoints(source).unwrap();
417        insta::assert_snapshot!(new.text(), @"#let a = 1;");
418    }
419
420    #[test]
421    fn test_instrument_breakpoint_nested() {
422        let source = Source::detached("#let a = {1};");
423        let (new, _meta) = instrument_breakpoints(source).unwrap();
424        insta::assert_snapshot!(new.text(), @"
425        #let a = {
426        if __breakpoint_block_start(0) {__breakpoint_block_start_handle(0, (:)); };
427        {1}
428        if __breakpoint_block_end(1) {__breakpoint_block_end_handle(1, (:)); };
429        }
430        ;
431        ");
432    }
433
434    #[test]
435    fn test_instrument_breakpoint_function() {
436        let source = Source::detached(
437            r#"#let add(x, y: 1, ..rest) = x + y
438#let inc = value => value + 1"#,
439        );
440        let (new, _meta) = instrument_breakpoints(source).unwrap();
441        assert!(new.root().errors_and_warnings().0.is_empty());
442        insta::assert_snapshot!(new.text(), @r"
443        #let add(x, y: 1, ..rest) = {
444        if __breakpoint_function(0) {__breakpoint_function_handle(0, (x: x, y: y, rest: rest)); };
445        (x + y);
446        if __breakpoint_return(1) {__breakpoint_return_handle(1, (:)); };
447
448        }
449
450        #let inc = value => {
451        if __breakpoint_function(2) {__breakpoint_function_handle(2, (value: value)); };
452        (value + 1);
453        if __breakpoint_return(3) {__breakpoint_return_handle(3, (:)); };
454
455        }
456        ");
457    }
458
459    #[test]
460    fn test_instrument_breakpoint_return() {
461        let source = Source::detached(
462            r#"#let f(x) = {
463  if x == 1 {
464    return x + 1
465  }
466  g(return)
467}
468"#,
469        );
470        let (new, _meta) = instrument_breakpoints(source).unwrap();
471        let errors = new.root().errors_and_warnings().0;
472        assert!(errors.is_empty(), "{errors:#?}\n{}", new.text());
473        insta::assert_snapshot!(new.text(), @r"
474        #let f(x) = {
475        if __breakpoint_function(0) {__breakpoint_function_handle(0, (x: x)); };
476        {
477        if __breakpoint_block_start(1) {__breakpoint_block_start_handle(1, (:)); };
478        {
479          if x == 1 {
480        if __breakpoint_block_start(2) {__breakpoint_block_start_handle(2, (:)); };
481        {
482            {
483        let __tinymist_return_value = x + 1;
484        if __breakpoint_return(3) {__breakpoint_return_handle(3, (:)); };
485        return __tinymist_return_value
486        }
487
488          }
489        if __breakpoint_block_end(4) {__breakpoint_block_end_handle(4, (:)); };
490        }
491
492          g({
493        if __breakpoint_return(5) {__breakpoint_return_handle(5, (:)); };
494        return
495        }
496        )
497        }
498        if __breakpoint_block_end(6) {__breakpoint_block_end_handle(6, (:)); };
499        }
500
501        if __breakpoint_return(7) {__breakpoint_return_handle(7, (:)); };
502
503        }
504        ");
505    }
506
507    #[test]
508    fn test_instrument_breakpoint_functor() {
509        let source = Source::detached("#show: main");
510        let (new, _meta) = instrument_breakpoints(source).unwrap();
511        insta::assert_snapshot!(new.text(), @"
512        #show: {
513        let __bp_functor = main
514        __it => {if __breakpoint_show_start(0) {__breakpoint_show_start_handle(0, (:)); };
515        __bp_functor(__it); } }
516        ");
517    }
518
519    struct NoopHandler;
520
521    impl DebugSessionHandler for NoopHandler {
522        fn on_breakpoint(
523            &self,
524            _engine: &Engine,
525            _context: Tracked<Context>,
526            _scopes: Scopes,
527            _span: Span,
528            _kind: BreakpointKind,
529            _function_name: Option<String>,
530        ) {
531        }
532    }
533
534    #[test]
535    fn test_source_breakpoint_resolves_to_block_start() {
536        let source = Source::detached(
537            r#"#let answer = {
538  let x = 40
539  x + 2
540}
541#answer"#,
542        );
543        let (_new, meta) = instrument_breakpoints(source.clone()).unwrap();
544        let mut session = DebugSession::new(Arc::new(NoopHandler));
545        session.breakpoints.insert(source.id(), meta);
546
547        let resolutions = session.set_source_breakpoints_for(
548            &source,
549            vec![SourceBreakpoint {
550                line: 1,
551                column: None,
552            }],
553        );
554
555        assert_eq!(resolutions.len(), 1);
556        let resolved = resolutions[0].resolved.unwrap();
557        assert_eq!(resolved.kind, BreakpointKind::BlockStart);
558        assert_eq!(resolved.line, 0);
559    }
560}