mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-04 18:28:02 +00:00
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:
parent
42fd5d3bc9
commit
6a5bea8bcf
7 changed files with 1006 additions and 85 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
325
crates/tinymist-debug/src/debugger.rs
Normal file
325
crates/tinymist-debug/src/debugger.rs
Normal 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
|
||||
);
|
||||
}
|
308
crates/tinymist-debug/src/debugger/instr.rs
Normal file
308
crates/tinymist-debug/src/debugger/instr.rs
Normal 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); } }
|
||||
"###);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue