tinymist_debug/debugger/
instr.rs1use 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}