feat: add instrument-based breakpoints support to dap (#1529)

* dev: clean up debug crate

* feat: instrument support

* feat: evaluate infra
This commit is contained in:
Myriad-Dreamin 2025-03-17 18:19:31 +08:00 committed by GitHub
parent 42fd5d3bc9
commit 6a5bea8bcf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1006 additions and 85 deletions

2
Cargo.lock generated
View file

@ -4102,11 +4102,13 @@ version = "0.13.8"
dependencies = [
"base64",
"comemo",
"ecow",
"insta",
"parking_lot",
"serde",
"serde_json",
"tinymist-analysis",
"tinymist-debug",
"tinymist-std",
"tinymist-world",
"typst",

View file

@ -1,6 +1,6 @@
[package]
name = "tinymist-dap"
description = "Fast debugger implementation for typst."
description = "Fast DAP implementation for typst."
categories = ["compilers"]
keywords = ["api", "debugger", "typst"]
authors.workspace = true
@ -16,12 +16,14 @@ typst-library.workspace = true
typst.workspace = true
tinymist-std.workspace = true
tinymist-analysis.workspace = true
tinymist-debug.workspace = true
tinymist-world = { workspace = true, features = ["system"] }
parking_lot.workspace = true
comemo.workspace = true
serde.workspace = true
serde_json.workspace = true
base64.workspace = true
ecow.workspace = true
[dev-dependencies]
insta.workspace = true

View file

@ -13,3 +13,224 @@
// this._runtime.on("end", () => {
// this.sendEvent(new TerminatedEvent());
// });
pub use tinymist_debug::BreakpointKind;
use std::sync::{mpsc, Arc};
use comemo::Track;
use comemo::Tracked;
use parking_lot::Mutex;
use tinymist_debug::{set_debug_session, DebugSession, DebugSessionHandler};
use tinymist_std::typst_shim::eval::{Eval, Vm};
use tinymist_world::{CompilerFeat, CompilerWorld};
use typst::{
diag::{SourceResult, Warned},
engine::{Engine, Route, Sink, Traced},
foundations::{Context, Scopes, Value},
introspection::Introspector,
layout::PagedDocument,
syntax::{ast, parse_code, Span},
World, __bail as bail,
};
type RequestId = i64;
/// A debug request.
pub enum DebugRequest {
/// Evaluates an expression.
Evaluate(RequestId, String),
/// Continues the execution.
Continue,
}
/// A handler for debug events.
pub trait DebugAdaptor: Send + Sync {
/// Called before the compilation.
fn before_compile(&self);
/// Called after the compilation.
fn after_compile(&self, result: Warned<SourceResult<PagedDocument>>);
/// Terminates the debug session.
fn terminate(&self);
/// Responds to a debug request.
fn stopped(&self, ctx: &BreakpointContext);
/// Responds to a debug request.
fn respond(&self, id: RequestId, result: SourceResult<Value>);
}
/// Starts a debug session.
pub fn start_session<F: CompilerFeat>(
base: CompilerWorld<F>,
adaptor: Arc<dyn DebugAdaptor>,
rx: mpsc::Receiver<DebugRequest>,
) {
let context = Arc::new(DebugContext {});
std::thread::spawn(move || {
let world = tinymist_debug::instr_breakpoints(&base);
if !set_debug_session(Some(DebugSession::new(context))) {
adaptor.terminate();
return None;
}
let _lock = ResourceLock::new(adaptor.clone(), rx);
adaptor.before_compile();
step_global(BreakpointKind::BeforeCompile, &world);
let result = typst::compile::<PagedDocument>(&world);
adaptor.after_compile(result);
step_global(BreakpointKind::AfterCompile, &world);
*RESOURCES.lock() = None;
set_debug_session(None);
adaptor.terminate();
Some(())
});
}
static RESOURCES: Mutex<Option<Resource>> = Mutex::new(None);
struct Resource {
adaptor: Arc<dyn DebugAdaptor>,
rx: mpsc::Receiver<DebugRequest>,
}
struct ResourceLock;
impl ResourceLock {
fn new(adaptor: Arc<dyn DebugAdaptor>, rx: mpsc::Receiver<DebugRequest>) -> Self {
RESOURCES.lock().replace(Resource { adaptor, rx });
Self
}
}
impl Drop for ResourceLock {
fn drop(&mut self) {
*RESOURCES.lock() = None;
}
}
fn step_global(kind: BreakpointKind, world: &dyn World) {
let mut resource = RESOURCES.lock();
let introspector = Introspector::default();
let traced = Traced::default();
let mut sink = Sink::default();
let route = Route::default();
let engine = Engine {
routines: &typst::ROUTINES,
world: world.track(),
introspector: introspector.track(),
traced: traced.track(),
sink: sink.track_mut(),
route,
};
let context = Context::default();
let span = Span::detached();
let context = BreakpointContext {
engine: &engine,
context: context.track(),
scopes: Scopes::new(Some(world.library())),
span,
kind,
};
step(&context, resource.as_mut().unwrap());
}
/// A breakpoint context.
pub struct BreakpointContext<'a, 'b, 'c> {
/// The breakpoint kind.
pub kind: BreakpointKind,
engine: &'a Engine<'c>,
context: Tracked<'a, Context<'b>>,
scopes: Scopes<'a>,
span: Span,
}
impl BreakpointContext<'_, '_, '_> {
fn evaluate(&self, expr: &str) -> SourceResult<Value> {
let mut root = parse_code(expr);
root.synthesize(self.span);
// Check for well-formedness.
let errors = root.errors();
if !errors.is_empty() {
return Err(errors.into_iter().map(Into::into).collect());
}
// Prepare VM.
let mut sink = Sink::new();
let engine = Engine {
world: self.engine.world,
introspector: self.engine.introspector,
traced: self.engine.traced,
routines: self.engine.routines,
sink: sink.track_mut(),
route: self.engine.route.clone(),
};
let mut vm = Vm::new(engine, self.context, self.scopes.clone(), root.span());
// Evaluate the code.
let output = root.cast::<ast::Code>().unwrap().eval(&mut vm)?;
// Handle control flow.
if let Some(flow) = vm.flow {
bail!(flow.forbidden());
}
Ok(output)
}
}
fn step(ctx: &BreakpointContext, resource: &mut Resource) {
resource.adaptor.stopped(ctx);
loop {
match resource.rx.recv() {
Ok(DebugRequest::Evaluate(id, expr)) => {
let res = ctx.evaluate(&expr);
eprintln!("evaluate: {expr} => {res:?}");
resource.adaptor.respond(id, res);
}
Ok(DebugRequest::Continue) => {
break;
}
Err(mpsc::RecvError) => {
break;
}
}
}
}
struct DebugContext {}
impl DebugSessionHandler for DebugContext {
fn on_breakpoint(
&self,
engine: &Engine,
context: Tracked<Context>,
scopes: Scopes,
span: Span,
kind: BreakpointKind,
) {
let mut resource = RESOURCES.lock();
let context = BreakpointContext {
engine,
context,
scopes,
span,
kind,
};
step(&context, resource.as_mut().unwrap());
}
}

View file

@ -1,23 +1,97 @@
//! Tinymist coverage support for Typst.
use std::collections::HashMap;
use std::sync::{Arc, LazyLock};
use parking_lot::Mutex;
use tinymist_analysis::location::PositionEncoding;
use tinymist_std::debug_loc::LspRange;
use tinymist_std::hash::FxHashMap;
use tinymist_world::vfs::FileId;
use tinymist_world::{CompilerFeat, CompilerWorld};
use typst::diag::FileResult;
use typst::foundations::func;
use typst::syntax::ast::AstNode;
use typst::syntax::{ast, Source, Span, SyntaxNode};
use typst::{World, WorldExt};
use crate::instrument::Instrumenter;
/// The coverage result.
pub struct CoverageResult {
/// The coverage meta.
pub meta: FxHashMap<FileId, Arc<InstrumentMeta>>,
/// The coverage map.
pub regions: FxHashMap<FileId, CovRegion>,
}
impl CoverageResult {
/// Converts the coverage result to JSON.
pub fn to_json<F: CompilerFeat>(&self, w: &CompilerWorld<F>) -> serde_json::Value {
let lsp_position_encoding = PositionEncoding::Utf16;
let mut result = VscodeCoverage::new();
for (file_id, region) in &self.regions {
let file_path = w
.path_for_id(*file_id)
.unwrap()
.as_path()
.to_str()
.unwrap()
.to_string();
let mut details = vec![];
let meta = self.meta.get(file_id).unwrap();
let Ok(typst_source) = w.source(*file_id) else {
continue;
};
let hits = region.hits.lock();
for (idx, (span, _kind)) in meta.meta.iter().enumerate() {
let Some(typst_range) = w.range(*span) else {
continue;
};
let rng = tinymist_analysis::location::to_lsp_range(
typst_range,
&typst_source,
lsp_position_encoding,
);
details.push(VscodeFileCoverageDetail {
executed: hits[idx] > 0,
location: rng,
});
}
result.insert(file_path, details);
}
serde_json::to_value(result).unwrap()
}
}
/// The coverage result in the format of the VSCode coverage data.
pub type VscodeCoverage = HashMap<String, Vec<VscodeFileCoverageDetail>>;
/// Converts the coverage result to the VSCode coverage data.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct VscodeFileCoverageDetail {
/// Whether the location is being executed
pub executed: bool,
/// The location of the coverage.
pub location: LspRange,
}
#[derive(Default)]
pub struct CoverageInstrumenter {
pub struct CovInstr {
/// The coverage map.
pub map: Mutex<FxHashMap<FileId, Arc<InstrumentMeta>>>,
}
impl Instrumenter for CoverageInstrumenter {
impl Instrumenter for CovInstr {
fn instrument(&self, _source: Source) -> FileResult<Source> {
let (new, meta) = instrument_coverage(_source)?;
let region = CovRegion {

View file

@ -0,0 +1,325 @@
//! Tinymist breakpoint support for Typst.
mod instr;
use std::sync::Arc;
use comemo::Tracked;
use parking_lot::RwLock;
use tinymist_std::hash::{FxHashMap, FxHashSet};
use tinymist_world::vfs::FileId;
use typst::diag::FileResult;
use typst::engine::Engine;
use typst::foundations::{func, Binding, Context, Dict, Scopes};
use typst::syntax::{Source, Span};
use typst::World;
use crate::instrument::Instrumenter;
#[derive(Default)]
pub struct BreakpointInstr {}
/// The kind of breakpoint.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BreakpointKind {
// Expr,
// Line,
/// A call breakpoint.
CallStart,
/// A call breakpoint.
CallEnd,
/// A function breakpoint.
Function,
/// A break breakpoint.
Break,
/// A continue breakpoint.
Continue,
/// A return breakpoint.
Return,
/// A block start breakpoint.
BlockStart,
/// A block end breakpoint.
BlockEnd,
/// A show start breakpoint.
ShowStart,
/// A show end breakpoint.
ShowEnd,
/// A doc start breakpoint.
DocStart,
/// A doc end breakpoint.
DocEnd,
/// A before compile breakpoint.
BeforeCompile,
/// A after compile breakpoint.
AfterCompile,
}
impl BreakpointKind {
/// Converts the breakpoint kind to a string.
pub fn to_str(self) -> &'static str {
match self {
BreakpointKind::CallStart => "call_start",
BreakpointKind::CallEnd => "call_end",
BreakpointKind::Function => "function",
BreakpointKind::Break => "break",
BreakpointKind::Continue => "continue",
BreakpointKind::Return => "return",
BreakpointKind::BlockStart => "block_start",
BreakpointKind::BlockEnd => "block_end",
BreakpointKind::ShowStart => "show_start",
BreakpointKind::ShowEnd => "show_end",
BreakpointKind::DocStart => "doc_start",
BreakpointKind::DocEnd => "doc_end",
BreakpointKind::BeforeCompile => "before_compile",
BreakpointKind::AfterCompile => "after_compile",
}
}
}
#[derive(Default)]
pub struct BreakpointInfo {
pub meta: Vec<BreakpointItem>,
}
pub struct BreakpointItem {
pub origin_span: Span,
}
static DEBUG_SESSION: RwLock<Option<DebugSession>> = RwLock::new(None);
/// The debug session handler.
pub trait DebugSessionHandler: Send + Sync {
/// Called when a breakpoint is hit.
fn on_breakpoint(
&self,
engine: &Engine,
context: Tracked<Context>,
scopes: Scopes,
span: Span,
kind: BreakpointKind,
);
}
/// The debug session.
pub struct DebugSession {
enabled: FxHashSet<(FileId, usize, BreakpointKind)>,
/// The breakpoint meta.
breakpoints: FxHashMap<FileId, Arc<BreakpointInfo>>,
/// The handler.
pub handler: Arc<dyn DebugSessionHandler>,
}
impl DebugSession {
/// Creates a new debug session.
pub fn new(handler: Arc<dyn DebugSessionHandler>) -> Self {
Self {
enabled: FxHashSet::default(),
breakpoints: FxHashMap::default(),
handler,
}
}
}
/// Runs function with the debug session.
pub fn with_debug_session<F, R>(f: F) -> Option<R>
where
F: FnOnce(&DebugSession) -> R,
{
Some(f(DEBUG_SESSION.read().as_ref()?))
}
/// Sets the debug session.
pub fn set_debug_session(session: Option<DebugSession>) -> bool {
let mut lock = DEBUG_SESSION.write();
if session.is_some() {
return false;
}
let _ = std::mem::replace(&mut *lock, session);
true
}
/// Software breakpoints
fn check_soft_breakpoint(span: Span, id: usize, kind: BreakpointKind) -> Option<bool> {
let fid = span.id()?;
let session = DEBUG_SESSION.read();
let session = session.as_ref()?;
let bp_feature = (fid, id, kind);
Some(session.enabled.contains(&bp_feature))
}
/// Software breakpoints
fn soft_breakpoint_handle(
engine: &Engine,
context: Tracked<Context>,
span: Span,
id: usize,
kind: BreakpointKind,
scope: Option<Dict>,
) -> Option<()> {
let fid = span.id()?;
let (handler, origin_span) = {
let session = DEBUG_SESSION.read();
let session = session.as_ref()?;
let bp_feature = (fid, id, kind);
if !session.enabled.contains(&bp_feature) {
return None;
}
let item = session.breakpoints.get(&fid)?.meta.get(id)?;
(session.handler.clone(), item.origin_span)
};
let mut scopes = Scopes::new(Some(engine.world.library()));
if let Some(scope) = scope {
for (key, value) in scope.into_iter() {
scopes.top.bind(key.into(), Binding::detached(value));
}
}
handler.on_breakpoint(engine, context, scopes, origin_span, kind);
Some(())
}
pub mod breakpoints {
use super::*;
macro_rules! bp_handler {
($name:ident, $name2:expr, $name3:ident, $name4:expr, $title:expr, $kind:ident) => {
#[func(name = $name2, title = $title)]
pub fn $name(span: Span, id: usize) -> bool {
check_soft_breakpoint(span, id, BreakpointKind::$kind).unwrap_or_default()
}
#[func(name = $name4, title = $title)]
pub fn $name3(
engine: &Engine,
context: Tracked<Context>,
span: Span,
id: usize,
scope: Option<Dict>,
) {
soft_breakpoint_handle(engine, context, span, id, BreakpointKind::$kind, scope);
}
};
}
bp_handler!(
__breakpoint_call_start,
"__breakpoint_call_start",
__breakpoint_call_start_handle,
"__breakpoint_call_start_handle",
"A Software Breakpoint at the start of a call.",
CallStart
);
bp_handler!(
__breakpoint_call_end,
"__breakpoint_call_end",
__breakpoint_call_end_handle,
"__breakpoint_call_end_handle",
"A Software Breakpoint at the end of a call.",
CallEnd
);
bp_handler!(
__breakpoint_function,
"__breakpoint_function",
__breakpoint_function_handle,
"__breakpoint_function_handle",
"A Software Breakpoint at the start of a function.",
Function
);
bp_handler!(
__breakpoint_break,
"__breakpoint_break",
__breakpoint_break_handle,
"__breakpoint_break_handle",
"A Software Breakpoint at a break.",
Break
);
bp_handler!(
__breakpoint_continue,
"__breakpoint_continue",
__breakpoint_continue_handle,
"__breakpoint_continue_handle",
"A Software Breakpoint at a continue.",
Continue
);
bp_handler!(
__breakpoint_return,
"__breakpoint_return",
__breakpoint_return_handle,
"__breakpoint_return_handle",
"A Software Breakpoint at a return.",
Return
);
bp_handler!(
__breakpoint_block_start,
"__breakpoint_block_start",
__breakpoint_block_start_handle,
"__breakpoint_block_start_handle",
"A Software Breakpoint at the start of a block.",
BlockStart
);
bp_handler!(
__breakpoint_block_end,
"__breakpoint_block_end",
__breakpoint_block_end_handle,
"__breakpoint_block_end_handle",
"A Software Breakpoint at the end of a block.",
BlockEnd
);
bp_handler!(
__breakpoint_show_start,
"__breakpoint_show_start",
__breakpoint_show_start_handle,
"__breakpoint_show_start_handle",
"A Software Breakpoint at the start of a show.",
ShowStart
);
bp_handler!(
__breakpoint_show_end,
"__breakpoint_show_end",
__breakpoint_show_end_handle,
"__breakpoint_show_end_handle",
"A Software Breakpoint at the end of a show.",
ShowEnd
);
bp_handler!(
__breakpoint_doc_start,
"__breakpoint_doc_start",
__breakpoint_doc_start_handle,
"__breakpoint_doc_start_handle",
"A Software Breakpoint at the start of a doc.",
DocStart
);
bp_handler!(
__breakpoint_doc_end,
"__breakpoint_doc_end",
__breakpoint_doc_end_handle,
"__breakpoint_doc_end_handle",
"A Software Breakpoint at the end of a doc.",
DocEnd
);
bp_handler!(
__breakpoint_before_compile,
"__breakpoint_before_compile",
__breakpoint_before_compile_handle,
"__breakpoint_before_compile_handle",
"A Software Breakpoint before compilation.",
BeforeCompile
);
bp_handler!(
__breakpoint_after_compile,
"__breakpoint_after_compile",
__breakpoint_after_compile_handle,
"__breakpoint_after_compile_handle",
"A Software Breakpoint after compilation.",
AfterCompile
);
}

View file

@ -0,0 +1,308 @@
use typst::diag::FileError;
use typst::syntax::ast::{self, AstNode};
use typst::syntax::SyntaxNode;
use super::*;
impl Instrumenter for BreakpointInstr {
fn instrument(&self, _source: Source) -> FileResult<Source> {
let (new, meta) = instrument_breakpoints(_source)?;
let mut session = DEBUG_SESSION.write();
let session = session
.as_mut()
.ok_or_else(|| FileError::Other(Some("No active debug session".into())))?;
session.breakpoints.insert(new.id(), meta);
Ok(new)
}
}
#[comemo::memoize]
fn instrument_breakpoints(source: Source) -> FileResult<(Source, Arc<BreakpointInfo>)> {
let node = source.root();
let mut worker = InstrumentWorker {
meta: BreakpointInfo::default(),
instrumented: String::new(),
};
worker.visit_node(node);
let new_source: Source = Source::new(source.id(), worker.instrumented);
Ok((new_source, Arc::new(worker.meta)))
}
struct InstrumentWorker {
meta: BreakpointInfo,
instrumented: String,
}
impl InstrumentWorker {
fn instrument_block_child(&mut self, container: &SyntaxNode, b1: Span, b2: Span) {
for child in container.children() {
if b1 == child.span() || b2 == child.span() {
self.instrument_block(child);
} else {
self.visit_node(child);
}
}
}
fn visit_node(&mut self, node: &SyntaxNode) {
if let Some(expr) = node.cast::<ast::Expr>() {
match expr {
ast::Expr::Code(..) => {
self.instrument_block(node);
return;
}
ast::Expr::While(while_expr) => {
self.instrument_block_child(node, while_expr.body().span(), Span::detached());
return;
}
ast::Expr::For(for_expr) => {
self.instrument_block_child(node, for_expr.body().span(), Span::detached());
return;
}
ast::Expr::Conditional(cond_expr) => {
self.instrument_block_child(
node,
cond_expr.if_body().span(),
cond_expr.else_body().unwrap_or_default().span(),
);
return;
}
ast::Expr::Closure(closure) => {
self.instrument_block_child(node, closure.body().span(), Span::detached());
return;
}
ast::Expr::Show(show_rule) => {
let transform = show_rule.transform().to_untyped().span();
for child in node.children() {
if transform == child.span() {
self.instrument_functor(child);
} else {
self.visit_node(child);
}
}
return;
}
ast::Expr::Text(..)
| ast::Expr::Space(..)
| ast::Expr::Linebreak(..)
| ast::Expr::Parbreak(..)
| ast::Expr::Escape(..)
| ast::Expr::Shorthand(..)
| ast::Expr::SmartQuote(..)
| ast::Expr::Strong(..)
| ast::Expr::Emph(..)
| ast::Expr::Raw(..)
| ast::Expr::Link(..)
| ast::Expr::Label(..)
| ast::Expr::Ref(..)
| ast::Expr::Heading(..)
| ast::Expr::List(..)
| ast::Expr::Enum(..)
| ast::Expr::Term(..)
| ast::Expr::Equation(..)
| ast::Expr::Math(..)
| ast::Expr::MathText(..)
| ast::Expr::MathIdent(..)
| ast::Expr::MathShorthand(..)
| ast::Expr::MathAlignPoint(..)
| ast::Expr::MathDelimited(..)
| ast::Expr::MathAttach(..)
| ast::Expr::MathPrimes(..)
| ast::Expr::MathFrac(..)
| ast::Expr::MathRoot(..)
| ast::Expr::Ident(..)
| ast::Expr::None(..)
| ast::Expr::Auto(..)
| ast::Expr::Bool(..)
| ast::Expr::Int(..)
| ast::Expr::Float(..)
| ast::Expr::Numeric(..)
| ast::Expr::Str(..)
| ast::Expr::Content(..)
| ast::Expr::Parenthesized(..)
| ast::Expr::Array(..)
| ast::Expr::Dict(..)
| ast::Expr::Unary(..)
| ast::Expr::Binary(..)
| ast::Expr::FieldAccess(..)
| ast::Expr::FuncCall(..)
| ast::Expr::Let(..)
| ast::Expr::DestructAssign(..)
| ast::Expr::Set(..)
| ast::Expr::Contextual(..)
| ast::Expr::Import(..)
| ast::Expr::Include(..)
| ast::Expr::Break(..)
| ast::Expr::Continue(..)
| ast::Expr::Return(..) => {}
}
}
self.visit_node_fallback(node);
}
fn visit_node_fallback(&mut self, node: &SyntaxNode) {
let txt = node.text();
if !txt.is_empty() {
self.instrumented.push_str(txt);
}
for child in node.children() {
self.visit_node(child);
}
}
fn make_cov(&mut self, span: Span, kind: BreakpointKind) {
let it = self.meta.meta.len();
self.meta.meta.push(BreakpointItem { origin_span: span });
self.instrumented.push_str("if __breakpoint_");
self.instrumented.push_str(kind.to_str());
self.instrumented.push('(');
self.instrumented.push_str(&it.to_string());
self.instrumented.push_str(") {");
self.instrumented.push_str("__breakpoint_");
self.instrumented.push_str(kind.to_str());
self.instrumented.push_str("_handle(");
self.instrumented.push_str(&it.to_string());
self.instrumented.push_str(", (:)); ");
self.instrumented.push_str("};\n");
}
fn instrument_block(&mut self, child: &SyntaxNode) {
self.instrumented.push_str("{\n");
let (first, last) = {
let mut children = child.children();
let first = children
.next()
.map(|s| s.span())
.unwrap_or_else(Span::detached);
let last = children
.last()
.map(|s| s.span())
.unwrap_or_else(Span::detached);
(first, last)
};
self.make_cov(first, BreakpointKind::BlockStart);
self.visit_node_fallback(child);
self.instrumented.push('\n');
self.make_cov(last, BreakpointKind::BlockEnd);
self.instrumented.push_str("}\n");
}
fn instrument_functor(&mut self, child: &SyntaxNode) {
self.instrumented.push_str("{\nlet __bp_functor = ");
let s = child.span();
self.visit_node_fallback(child);
self.instrumented.push_str("\n__it => {");
self.make_cov(s, BreakpointKind::ShowStart);
self.instrumented.push_str("__bp_functor(__it); } }\n");
}
}
#[cfg(test)]
mod tests {
use super::*;
fn instr(input: &str) -> String {
let source = Source::detached(input);
let (new, _meta) = instrument_breakpoints(source).unwrap();
new.text().to_string()
}
#[test]
fn test_physica_vector() {
let instrumented = instr(include_str!(
"../fixtures/instr_coverage/physica_vector.typ"
));
insta::assert_snapshot!(instrumented, @r###"
// A show rule, should be used like:
// #show: super-plus-as-dagger
// U^+U = U U^+ = I
// or in scope:
// #[
// #show: super-plus-as-dagger
// U^+U = U U^+ = I
// ]
#let super-plus-as-dagger(document) = {
if __breakpoint_block_start(0) {__breakpoint_block_start_handle(0, (:)); };
{
show math.attach: {
let __bp_functor = elem => {
if __breakpoint_block_start(1) {__breakpoint_block_start_handle(1, (:)); };
{
if __eligible(elem.base) and elem.at("t", default: none) == [+] {
if __breakpoint_block_start(2) {__breakpoint_block_start_handle(2, (:)); };
{
$attach(elem.base, t: dagger, b: elem.at("b", default: #none))$
}
if __breakpoint_block_end(3) {__breakpoint_block_end_handle(3, (:)); };
}
else {
if __breakpoint_block_start(4) {__breakpoint_block_start_handle(4, (:)); };
{
elem
}
if __breakpoint_block_end(5) {__breakpoint_block_end_handle(5, (:)); };
}
}
if __breakpoint_block_end(6) {__breakpoint_block_end_handle(6, (:)); };
}
__it => {if __breakpoint_show_start(7) {__breakpoint_show_start_handle(7, (:)); };
__bp_functor(__it); } }
document
}
if __breakpoint_block_end(8) {__breakpoint_block_end_handle(8, (:)); };
}
"###);
}
#[test]
fn test_playground() {
let instrumented = instr(include_str!("../fixtures/instr_coverage/playground.typ"));
insta::assert_snapshot!(instrumented, @"");
}
#[test]
fn test_instrument_coverage() {
let source = Source::detached("#let a = 1;");
let (new, _meta) = instrument_breakpoints(source).unwrap();
insta::assert_snapshot!(new.text(), @"#let a = 1;");
}
#[test]
fn test_instrument_coverage_nested() {
let source = Source::detached("#let a = {1};");
let (new, _meta) = instrument_breakpoints(source).unwrap();
insta::assert_snapshot!(new.text(), @r###"
#let a = {
if __breakpoint_block_start(0) {__breakpoint_block_start_handle(0, (:)); };
{1}
if __breakpoint_block_end(1) {__breakpoint_block_end_handle(1, (:)); };
}
;
"###);
}
#[test]
fn test_instrument_coverage_functor() {
let source = Source::detached("#show: main");
let (new, _meta) = instrument_breakpoints(source).unwrap();
insta::assert_snapshot!(new.text(), @r###"
#show: {
let __bp_functor = main
__it => {if __breakpoint_show_start(0) {__breakpoint_show_start_handle(0, (:)); };
__bp_functor(__it); } }
"###);
}
}

View file

@ -1,114 +1,115 @@
//! Tinymist coverage support for Typst.
pub use debugger::{
set_debug_session, with_debug_session, BreakpointKind, DebugSession, DebugSessionHandler,
};
mod cov;
mod debugger;
mod instrument;
use std::collections::HashMap;
use std::ops::DerefMut;
use std::sync::Arc;
use debugger::BreakpointInstr;
use parking_lot::Mutex;
use tinymist_analysis::location::PositionEncoding;
use tinymist_std::debug_loc::LspRange;
use tinymist_std::{error::prelude::*, hash::FxHashMap};
use tinymist_world::package::PackageSpec;
use tinymist_world::{print_diagnostics, CompilerFeat, CompilerWorld};
use typst::diag::EcoString;
use typst::syntax::package::PackageVersion;
use typst::syntax::FileId;
use typst::utils::LazyHash;
use typst::{Library, World, WorldExt};
use typst::Library;
use cov::*;
use instrument::InstrumentWorld;
/// The coverage result.
pub struct CoverageResult {
/// The coverage meta.
pub meta: FxHashMap<FileId, Arc<InstrumentMeta>>,
/// The coverage map.
pub regions: FxHashMap<FileId, CovRegion>,
}
impl CoverageResult {
/// Converts the coverage result to JSON.
pub fn to_json<F: CompilerFeat>(&self, w: &CompilerWorld<F>) -> serde_json::Value {
let lsp_position_encoding = PositionEncoding::Utf16;
let mut result = VscodeCoverage::new();
for (file_id, region) in &self.regions {
let file_path = w
.path_for_id(*file_id)
.unwrap()
.as_path()
.to_str()
.unwrap()
.to_string();
let mut details = vec![];
let meta = self.meta.get(file_id).unwrap();
let Ok(typst_source) = w.source(*file_id) else {
continue;
};
let hits = region.hits.lock();
for (idx, (span, _kind)) in meta.meta.iter().enumerate() {
let Some(typst_range) = w.range(*span) else {
continue;
};
let rng = tinymist_analysis::location::to_lsp_range(
typst_range,
&typst_source,
lsp_position_encoding,
);
details.push(VscodeFileCoverageDetail {
executed: hits[idx] > 0,
location: rng,
});
}
result.insert(file_path, details);
}
serde_json::to_value(result).unwrap()
}
}
/// Collects the coverage of a single execution.
pub fn collect_coverage<D: typst::Document, F: CompilerFeat>(
base: &CompilerWorld<F>,
) -> Result<CoverageResult> {
let (cov, result) = with_cov(base, |instr| {
if let Err(e) = typst::compile::<D>(&instr).output {
print_diagnostics(instr, e.iter(), tinymist_world::DiagnosticFormat::Human)
.context_ut("failed to print diagnostics")?;
bail!("");
}
Ok(())
});
result?;
cov
}
/// Collects the coverage with a callback.
pub fn with_cov<F: CompilerFeat>(
base: &CompilerWorld<F>,
mut f: impl FnMut(&InstrumentWorld<F, CovInstr>) -> Result<()>,
) -> (Result<CoverageResult>, Result<()>) {
let instr = InstrumentWorld {
base,
library: instrument_library(&base.library),
instr: CoverageInstrumenter::default(),
instr: CovInstr::default(),
instrumented: Mutex::new(FxHashMap::default()),
};
let _cov_lock = cov::COVERAGE_LOCK.lock();
if let Err(e) = typst::compile::<D>(&instr).output {
print_diagnostics(&instr, e.iter(), tinymist_world::DiagnosticFormat::Human)
.context_ut("failed to print diagnostics")?;
bail!("");
}
let result = f(&instr);
let meta = std::mem::take(instr.instr.map.lock().deref_mut());
let CoverageMap { regions, .. } = std::mem::take(cov::COVERAGE_MAP.lock().deref_mut());
Ok(CoverageResult { meta, regions })
(Ok(CoverageResult { meta, regions }), result)
}
/// The world for debugging.
pub type DebuggerWorld<'a, F> = InstrumentWorld<'a, F, BreakpointInstr>;
/// Creates a world for debugging.
pub fn instr_breakpoints<F: CompilerFeat>(base: &CompilerWorld<F>) -> DebuggerWorld<'_, F> {
InstrumentWorld {
base,
library: instrument_library(&base.library),
instr: BreakpointInstr::default(),
instrumented: Mutex::new(FxHashMap::default()),
}
}
#[comemo::memoize]
fn instrument_library(library: &Arc<LazyHash<Library>>) -> Arc<LazyHash<Library>> {
use debugger::breakpoints::*;
let mut library = library.as_ref().clone();
library.global.scope_mut().define_func::<__cov_pc>();
let scope = library.global.scope_mut();
scope.define_func::<__cov_pc>();
scope.define_func::<__breakpoint_call_start>();
scope.define_func::<__breakpoint_call_end>();
scope.define_func::<__breakpoint_function>();
scope.define_func::<__breakpoint_break>();
scope.define_func::<__breakpoint_continue>();
scope.define_func::<__breakpoint_return>();
scope.define_func::<__breakpoint_block_start>();
scope.define_func::<__breakpoint_block_end>();
scope.define_func::<__breakpoint_show_start>();
scope.define_func::<__breakpoint_show_end>();
scope.define_func::<__breakpoint_doc_start>();
scope.define_func::<__breakpoint_doc_end>();
scope.define_func::<__breakpoint_call_start_handle>();
scope.define_func::<__breakpoint_call_end_handle>();
scope.define_func::<__breakpoint_function_handle>();
scope.define_func::<__breakpoint_break_handle>();
scope.define_func::<__breakpoint_continue_handle>();
scope.define_func::<__breakpoint_return_handle>();
scope.define_func::<__breakpoint_block_start_handle>();
scope.define_func::<__breakpoint_block_end_handle>();
scope.define_func::<__breakpoint_show_start_handle>();
scope.define_func::<__breakpoint_show_end_handle>();
scope.define_func::<__breakpoint_doc_start_handle>();
scope.define_func::<__breakpoint_doc_end_handle>();
Arc::new(library)
}
@ -131,15 +132,3 @@ impl<'a> From<&'a PackageSpec> for PackageSpecCmp<'a> {
}
}
}
/// The coverage result in the format of the VSCode coverage data.
pub type VscodeCoverage = HashMap<String, Vec<VscodeFileCoverageDetail>>;
/// Converts the coverage result to the VSCode coverage data.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct VscodeFileCoverageDetail {
/// Whether the location is being executed
pub executed: bool,
/// The location of the coverage.
pub location: LspRange,
}