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}