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)?;
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.breakpoints.insert(new.id(), meta);
17
18        Ok(new)
19    }
20}
21
22#[comemo::memoize]
23fn instrument_breakpoints(source: Source) -> FileResult<(Source, Arc<BreakpointInfo>)> {
24    let node = source.root();
25    let mut worker = InstrumentWorker {
26        meta: BreakpointInfo::default(),
27        instrumented: String::new(),
28    };
29
30    worker.visit_node(node);
31    let new_source: Source = Source::new(source.id(), worker.instrumented);
32
33    Ok((new_source, Arc::new(worker.meta)))
34}
35
36struct InstrumentWorker {
37    meta: BreakpointInfo,
38    instrumented: String,
39}
40
41impl InstrumentWorker {
42    fn instrument_block_child(&mut self, container: &SyntaxNode, b1: Span, b2: Span) {
43        for child in container.children() {
44            if b1 == child.span() || b2 == child.span() {
45                self.instrument_block(child);
46            } else {
47                self.visit_node(child);
48            }
49        }
50    }
51
52    fn visit_node(&mut self, node: &SyntaxNode) {
53        if let Some(expr) = node.cast::<ast::Expr>() {
54            match expr {
55                ast::Expr::Code(..) => {
56                    self.instrument_block(node);
57                    return;
58                }
59                ast::Expr::While(while_expr) => {
60                    self.instrument_block_child(node, while_expr.body().span(), Span::detached());
61                    return;
62                }
63                ast::Expr::For(for_expr) => {
64                    self.instrument_block_child(node, for_expr.body().span(), Span::detached());
65                    return;
66                }
67                ast::Expr::Conditional(cond_expr) => {
68                    self.instrument_block_child(
69                        node,
70                        cond_expr.if_body().span(),
71                        cond_expr.else_body().unwrap_or_default().span(),
72                    );
73                    return;
74                }
75                ast::Expr::Closure(closure) => {
76                    self.instrument_block_child(node, closure.body().span(), Span::detached());
77                    return;
78                }
79                ast::Expr::Show(show_rule) => {
80                    let transform = show_rule.transform().to_untyped().span();
81
82                    for child in node.children() {
83                        if transform == child.span() {
84                            self.instrument_functor(child);
85                        } else {
86                            self.visit_node(child);
87                        }
88                    }
89                    return;
90                }
91                ast::Expr::Text(..)
92                | ast::Expr::Space(..)
93                | ast::Expr::Linebreak(..)
94                | ast::Expr::Parbreak(..)
95                | ast::Expr::Escape(..)
96                | ast::Expr::Shorthand(..)
97                | ast::Expr::SmartQuote(..)
98                | ast::Expr::Strong(..)
99                | ast::Expr::Emph(..)
100                | ast::Expr::Raw(..)
101                | ast::Expr::Link(..)
102                | ast::Expr::Label(..)
103                | ast::Expr::Ref(..)
104                | ast::Expr::Heading(..)
105                | ast::Expr::List(..)
106                | ast::Expr::Enum(..)
107                | ast::Expr::Term(..)
108                | ast::Expr::Equation(..)
109                | ast::Expr::Math(..)
110                | ast::Expr::MathText(..)
111                | ast::Expr::MathIdent(..)
112                | ast::Expr::MathShorthand(..)
113                | ast::Expr::MathAlignPoint(..)
114                | ast::Expr::MathDelimited(..)
115                | ast::Expr::MathAttach(..)
116                | ast::Expr::MathPrimes(..)
117                | ast::Expr::MathFrac(..)
118                | ast::Expr::MathRoot(..)
119                | ast::Expr::Ident(..)
120                | ast::Expr::None(..)
121                | ast::Expr::Auto(..)
122                | ast::Expr::Bool(..)
123                | ast::Expr::Int(..)
124                | ast::Expr::Float(..)
125                | ast::Expr::Numeric(..)
126                | ast::Expr::Str(..)
127                | ast::Expr::Content(..)
128                | ast::Expr::Parenthesized(..)
129                | ast::Expr::Array(..)
130                | ast::Expr::Dict(..)
131                | ast::Expr::Unary(..)
132                | ast::Expr::Binary(..)
133                | ast::Expr::FieldAccess(..)
134                | ast::Expr::FuncCall(..)
135                | ast::Expr::Let(..)
136                | ast::Expr::DestructAssign(..)
137                | ast::Expr::Set(..)
138                | ast::Expr::Contextual(..)
139                | ast::Expr::Import(..)
140                | ast::Expr::Include(..)
141                | ast::Expr::Break(..)
142                | ast::Expr::Continue(..)
143                | ast::Expr::Return(..) => {}
144            }
145        }
146
147        self.visit_node_fallback(node);
148    }
149
150    fn visit_node_fallback(&mut self, node: &SyntaxNode) {
151        let txt = node.text();
152        if !txt.is_empty() {
153            self.instrumented.push_str(txt);
154        }
155
156        for child in node.children() {
157            self.visit_node(child);
158        }
159    }
160
161    fn make_cov(&mut self, span: Span, kind: BreakpointKind) {
162        let it = self.meta.meta.len();
163        self.meta.meta.push(BreakpointItem { origin_span: span });
164        self.instrumented.push_str("if __breakpoint_");
165        self.instrumented.push_str(kind.to_str());
166        self.instrumented.push('(');
167        self.instrumented.push_str(&it.to_string());
168        self.instrumented.push_str(") {");
169        self.instrumented.push_str("__breakpoint_");
170        self.instrumented.push_str(kind.to_str());
171        self.instrumented.push_str("_handle(");
172        self.instrumented.push_str(&it.to_string());
173        self.instrumented.push_str(", (:)); ");
174        self.instrumented.push_str("};\n");
175    }
176
177    fn instrument_block(&mut self, child: &SyntaxNode) {
178        self.instrumented.push_str("{\n");
179        let (first, last) = {
180            let mut children = child.children();
181            let first = children
182                .next()
183                .map(|s| s.span())
184                .unwrap_or_else(Span::detached);
185            let last = children
186                .last()
187                .map(|s| s.span())
188                .unwrap_or_else(Span::detached);
189
190            (first, last)
191        };
192        self.make_cov(first, BreakpointKind::BlockStart);
193        self.visit_node_fallback(child);
194        self.instrumented.push('\n');
195        self.make_cov(last, BreakpointKind::BlockEnd);
196        self.instrumented.push_str("}\n");
197    }
198
199    fn instrument_functor(&mut self, child: &SyntaxNode) {
200        self.instrumented.push_str("{\nlet __bp_functor = ");
201        let s = child.span();
202        self.visit_node_fallback(child);
203        self.instrumented.push_str("\n__it => {");
204        self.make_cov(s, BreakpointKind::ShowStart);
205        self.instrumented.push_str("__bp_functor(__it); } }\n");
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    fn instr(input: &str) -> String {
214        let source = Source::detached(input);
215        let (new, _meta) = instrument_breakpoints(source).unwrap();
216        new.text().to_string()
217    }
218
219    #[test]
220    fn test_physica_vector() {
221        let instrumented = instr(include_str!(
222            "../fixtures/instr_coverage/physica_vector.typ"
223        ));
224        insta::assert_snapshot!(instrumented, @r###"
225        // A show rule, should be used like:
226        //   #show: super-plus-as-dagger
227        //   U^+U = U U^+ = I
228        // or in scope:
229        //   #[
230        //     #show: super-plus-as-dagger
231        //     U^+U = U U^+ = I
232        //   ]
233        #let super-plus-as-dagger(document) = {
234        if __breakpoint_block_start(0) {__breakpoint_block_start_handle(0, (:)); };
235        {
236          show math.attach: {
237        let __bp_functor = elem => {
238        if __breakpoint_block_start(1) {__breakpoint_block_start_handle(1, (:)); };
239        {
240            if __eligible(elem.base) and elem.at("t", default: none) == [+] {
241        if __breakpoint_block_start(2) {__breakpoint_block_start_handle(2, (:)); };
242        {
243              $attach(elem.base, t: dagger, b: elem.at("b", default: #none))$
244            }
245        if __breakpoint_block_end(3) {__breakpoint_block_end_handle(3, (:)); };
246        }
247         else {
248        if __breakpoint_block_start(4) {__breakpoint_block_start_handle(4, (:)); };
249        {
250              elem
251            }
252        if __breakpoint_block_end(5) {__breakpoint_block_end_handle(5, (:)); };
253        }
254
255          }
256        if __breakpoint_block_end(6) {__breakpoint_block_end_handle(6, (:)); };
257        }
258
259        __it => {if __breakpoint_show_start(7) {__breakpoint_show_start_handle(7, (:)); };
260        __bp_functor(__it); } }
261
262
263          document
264        }
265        if __breakpoint_block_end(8) {__breakpoint_block_end_handle(8, (:)); };
266        }
267        "###);
268    }
269
270    #[test]
271    fn test_playground() {
272        let instrumented = instr(include_str!("../fixtures/instr_coverage/playground.typ"));
273        insta::assert_snapshot!(instrumented, @"");
274    }
275
276    #[test]
277    fn test_instrument_coverage() {
278        let source = Source::detached("#let a = 1;");
279        let (new, _meta) = instrument_breakpoints(source).unwrap();
280        insta::assert_snapshot!(new.text(), @"#let a = 1;");
281    }
282
283    #[test]
284    fn test_instrument_coverage_nested() {
285        let source = Source::detached("#let a = {1};");
286        let (new, _meta) = instrument_breakpoints(source).unwrap();
287        insta::assert_snapshot!(new.text(), @r###"
288        #let a = {
289        if __breakpoint_block_start(0) {__breakpoint_block_start_handle(0, (:)); };
290        {1}
291        if __breakpoint_block_end(1) {__breakpoint_block_end_handle(1, (:)); };
292        }
293        ;
294        "###);
295    }
296
297    #[test]
298    fn test_instrument_coverage_functor() {
299        let source = Source::detached("#show: main");
300        let (new, _meta) = instrument_breakpoints(source).unwrap();
301        insta::assert_snapshot!(new.text(), @r###"
302        #show: {
303        let __bp_functor = main
304        __it => {if __breakpoint_show_start(0) {__breakpoint_show_start_handle(0, (:)); };
305        __bp_functor(__it); } }
306        "###);
307    }
308}