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

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());
}
}