mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-03 17:58:17 +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
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue