From 83bfb474ea0b74f57a18f5f337588dc638932bb8 Mon Sep 17 00:00:00 2001 From: reloginn Date: Thu, 21 Aug 2025 02:26:51 +0600 Subject: [PATCH 1/7] `feature`: implemented debugger --- Cargo.lock | 42 +- Cargo.toml | 2 + debugger/.gitignore | 1 + debugger/Cargo.lock | 353 ++++++++++++ debugger/Cargo.toml | 19 + debugger/examples/simple_debug.rs | 128 +++++ debugger/src/adapter.rs | 671 ++++++++++++++++++++++ debugger/src/breakpoints.rs | 59 ++ debugger/src/lib.rs | 891 ++++++++++++++++++++++++++++++ debugger/src/location.rs | 38 ++ debugger/src/stop_reason.rs | 14 + debugger/src/watch.rs | 5 + debugger/src/watch/entry.rs | 10 + debugger/src/watch/mode.rs | 5 + debugger/src/watch/spec.rs | 8 + src/closure.rs | 4 +- src/thread/executor.rs | 4 +- src/thread/mod.rs | 9 +- src/thread/thread.rs | 63 +-- src/thread/vm.rs | 2 +- 20 files changed, 2285 insertions(+), 43 deletions(-) create mode 100644 debugger/.gitignore create mode 100644 debugger/Cargo.lock create mode 100644 debugger/Cargo.toml create mode 100644 debugger/examples/simple_debug.rs create mode 100644 debugger/src/adapter.rs create mode 100644 debugger/src/breakpoints.rs create mode 100644 debugger/src/lib.rs create mode 100644 debugger/src/location.rs create mode 100644 debugger/src/stop_reason.rs create mode 100644 debugger/src/watch.rs create mode 100644 debugger/src/watch/entry.rs create mode 100644 debugger/src/watch/mode.rs create mode 100644 debugger/src/watch/spec.rs diff --git a/Cargo.lock b/Cargo.lock index e3476b71..47ffc76b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ahash" @@ -76,6 +76,12 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.6.0" @@ -227,6 +233,12 @@ version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "libc" version = "0.2.155" @@ -293,6 +305,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "piccolo-debugger" +version = "0.3.3" +dependencies = [ + "base64", + "piccolo", + "serde", + "serde_json", +] + [[package]] name = "piccolo-util" version = "0.3.3" @@ -402,6 +424,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "serde" version = "1.0.203" @@ -422,6 +450,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "smallvec" version = "1.13.2" diff --git a/Cargo.toml b/Cargo.toml index 06712a6e..bd7f9a88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,12 @@ resolver = "2" members = [ "util", + "debugger" ] default-members = [ ".", "util", + "debugger" ] [workspace.package] diff --git a/debugger/.gitignore b/debugger/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/debugger/.gitignore @@ -0,0 +1 @@ +/target diff --git a/debugger/Cargo.lock b/debugger/Cargo.lock new file mode 100644 index 00000000..81b8a3b6 --- /dev/null +++ b/debugger/Cargo.lock @@ -0,0 +1,353 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "gc-arena" +version = "0.5.3" +source = "git+https://github.com/kyren/gc-arena?rev=5a7534b883b703f23cfb8c3cfdf033460aa77ea9#5a7534b883b703f23cfb8c3cfdf033460aa77ea9" +dependencies = [ + "allocator-api2", + "gc-arena-derive", + "hashbrown", + "sptr", +] + +[[package]] +name = "gc-arena-derive" +version = "0.5.3" +source = "git+https://github.com/kyren/gc-arena?rev=5a7534b883b703f23cfb8c3cfdf033460aa77ea9#5a7534b883b703f23cfb8c3cfdf033460aa77ea9" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "piccolo" +version = "0.3.3" +dependencies = [ + "ahash", + "allocator-api2", + "anyhow", + "gc-arena", + "hashbrown", + "rand", + "thiserror", +] + +[[package]] +name = "piccolo-debugger" +version = "0.1.0" +dependencies = [ + "base64", + "piccolo", + "serde", + "serde_json", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "sptr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/debugger/Cargo.toml b/debugger/Cargo.toml new file mode 100644 index 00000000..086ec409 --- /dev/null +++ b/debugger/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "piccolo-debugger" +description = "Debugger for the `piccolo`" +readme = "README.md" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +piccolo.workspace = true +serde.workspace = true +serde_json = "1.0" +base64 = "0.22" + +[[example]] +name = "simple_debug" +path = "examples/simple_debug.rs" diff --git a/debugger/examples/simple_debug.rs b/debugger/examples/simple_debug.rs new file mode 100644 index 00000000..42d74844 --- /dev/null +++ b/debugger/examples/simple_debug.rs @@ -0,0 +1,128 @@ +use piccolo::{Closure, Executor, Fuel, Lua, Table, Value}; +use piccolo_debugger::Debugger; + +fn main() { + let mut lua = Lua::core(); + + lua.enter(|ctx| { + let source = br#" + x = 0 + local outer = 10 + local function foo(n) + outer = outer + n + x = x + n + return outer + x + end + + local function bar() + local y = foo(1) + t.k = y + return y + end + + local z = bar() + "#; + + let chunk = Closure::load(ctx, Some("test.lua"), source).unwrap(); + + let t = Table::new(&ctx); + ctx.globals().set_field(ctx, "t", t); + + let mut exec = Executor::start(ctx, chunk.into(), ()); + + let mut dbg = Debugger::new(exec); + + dbg.add_breakpoint("test.lua", 3); + dbg.add_breakpoint("test.lua", 9); + dbg.add_breakpoint("test.lua", 13); + + let _ = dbg.add_function_breakpoint(ctx, "foo"); + + println!("Breakpoints:"); + for (src, lines) in dbg.list_breakpoints() { + let mut v: Vec = lines.iter().copied().collect(); + v.sort_unstable(); + println!(" {src}: {:?}", v); + } + + dbg.remove_breakpoint("test.lua", 9); + + dbg.watch_global("x"); + dbg.watch_table_key(t, Value::String(ctx.intern(b"k"))); + + println!("Continue…"); + let mut stop = dbg.continue_run(ctx); + println!("Stopped: {:?}", stop); + + let bt = dbg.backtrace(ctx); + println!("Backtrace:"); + for (i, loc) in bt.iter().enumerate() { + println!( + " #{i} {}:{} {}", + loc.chunk(), + loc.line(), + loc.function_ref() + ); + } + + if let Some(dis) = dbg.disassemble("", None, None) { + println!("Disassembly (current function):"); + for l in dis.iter().take(10) { + println!(" {l:?}"); + } + } + + if let Some(readed) = dbg.read_memory("", None, None) { + println!("Readed: {:?}", readed); + } + + if let Some(regs) = dbg.read_registers(ctx) { + println!("Top frame registers ({}):", regs.len()); + for (i, v) in regs.iter().enumerate() { + println!(" r{i} = {}", v.display()); + } + } + + println!("Stack snapshot (before stepping):"); + for (loc, regs) in dbg.stack_snapshot(ctx) { + println!( + " {}:{} {} ({} regs)", + loc.chunk(), + loc.line(), + loc.function_ref(), + regs.len() + ); + } + + println!("Step into…"); + stop = dbg.step_into(ctx); + println!("Stopped: {:?}", stop); + + if let Some(ups) = dbg.read_upvalues(ctx) { + println!("Upvalues ({}):", ups.len()); + for (i, v) in ups.iter().enumerate() { + println!(" uv{i} = {}", v.display()); + } + } + + println!("Step over…"); + stop = dbg.step_over(ctx); + println!("Stopped: {:?}", stop); + + println!("Step out…"); + stop = dbg.step_out(ctx); + println!("Stopped: {:?}", stop); + + println!("Continue again…"); + stop = dbg.continue_run(ctx); + println!("Stopped: {:?}", stop); + + if let Some(v0) = dbg.read_register(ctx, 0) { + println!("r0 = {}", v0.display()); + } + + exec = dbg.executor(); + let mut fuel = Fuel::with(10_000); + let _ = exec.step(ctx, &mut fuel); + }); +} diff --git a/debugger/src/adapter.rs b/debugger/src/adapter.rs new file mode 100644 index 00000000..49a7108e --- /dev/null +++ b/debugger/src/adapter.rs @@ -0,0 +1,671 @@ +use std::mem::ManuallyDrop; + +use base64::Engine; +use piccolo::{Context, Executor}; + +use super::Debugger; + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum State { + NotLaunched, + Launched, + Suspended, + Terminated, +} + +pub enum Error { + AdapterNotLaunched, + AdapterSuspended, + AdapterTerminated, +} + +impl Error { + pub fn reason(state: State) -> Self { + match state { + State::Suspended => Error::AdapterSuspended, + State::Terminated => Error::AdapterTerminated, + State::NotLaunched => Error::AdapterNotLaunched, + _ => unreachable!(), + } + } +} + +#[allow(dead_code)] +pub struct Capabilities { + supports_configuration_done_request: bool, + supports_function_breakpoints: bool, + supports_conditional_breakpoints: bool, + supports_hit_conditional_breakpoints: bool, + supports_evaluate_for_hovers: bool, + supports_step_back: bool, + supports_set_variable: bool, + supports_restart_frame: bool, + supports_goto_targets_request: bool, + supports_step_in_targets_request: bool, + supports_completions_request: bool, + supports_modules_request: bool, + supports_restart_request: bool, + supports_exception_options: bool, + supports_value_formatting_options: bool, + supports_exception_info_request: bool, + support_terminate_debuggee: bool, + support_suspend_debuggee: bool, + supports_delayed_stack_trace_loading: bool, + supports_loaded_sources_request: bool, + supports_log_points: bool, + supports_terminate_threads_request: bool, + supports_set_expression: bool, + supports_terminate_request: bool, + supports_data_breakpoints: bool, + supports_read_memory_request: bool, + supports_write_memory_request: bool, + supports_disassemble_request: bool, + supports_cancel_request: bool, + supports_breakpoint_locations_request: bool, + supports_clipboard_context: bool, + supports_stepping_granularity: bool, + supports_instruction_breakpoints: bool, + supports_exception_filter_options: bool, + supports_single_thread_execution_requests: bool, + supports_data_breakpoint_bytes: bool, + supports_ansi_styling: bool, +} + +pub struct AttachResponse; + +pub enum SteppingGranularity {} + +pub struct StepOutArguments { + thread_id: i64, + single_thread: Option, + granularity: Option, +} + +pub struct StepBackResponse; + +pub struct StepOutResponse; + +pub struct StepInArguments { + thread_id: i64, + single_thread: Option, + target_id: Option, + granularity: Option, +} + +pub struct StepInResponse; + +pub struct StepInTargetsArguments { + frame_id: i64, +} + +pub struct StepInTarget { + id: usize, + label: String, + line: Option, + column: Option, + end_line: Option, + end_column: Option, +} + +pub struct StepInTargetsResponse { + targets: Vec, +} + +#[derive(Clone)] +pub struct Source { + name: Option, + path: Option, + source_reference: Option, + presentation_hint: Option, + origin: Option, + sources: Option>, + adapter_data: Option, + checksums: Option>, +} + +pub struct SourceBreakpoint { + line: usize, + column: Option, + condition: Option, + hit_condition: Option, + log_message: Option, + mode: Option, +} + +pub struct SetBreakpointsArguments { + source: Source, + breakpoints: Option>, + lines: Option>, + source_modified: Option, +} + +pub struct Breakpoint { + id: Option, + verified: bool, + message: Option, + source: Option, + line: Option, + column: Option, + end_line: Option, + end_column: Option, + instruction_reference: Option, + offset: Option, + reason: Option, +} + +pub struct SetBreakpointsResponse { + breakpoints: Vec, +} + +pub enum DataBreakpointAccessType { + Read, + Write, + ReadWrite, +} + +pub struct DataBreakpoint { + data_id: String, + access_type: DataBreakpointAccessType, + condition: Option, + hit_condition: Option, +} + +pub struct SetDataBreakpointsArguments { + breakpoints: Vec, +} + +pub struct SetDataBreakpointsResponse { + breakpoints: Vec, +} + +pub struct FunctionBreakpoint { + name: String, + condition: Option, + hit_condition: Option, +} + +pub struct SetFunctionBreakpointsArguments { + breakpoints: Vec, +} + +pub struct SetFunctionBreakpointsResponse { + breakpoints: Vec, +} + +pub struct InstructionBreakpoint { + instruction_reference: String, + offset: Option, + condition: Option, + hit_condition: Option, + mode: Option, +} + +pub struct SetInstructionBreakpointsArguments { + breakpoints: Vec, +} + +pub struct SetInstructionBreakpointsResponse { + breakpoints: Vec, +} + +pub struct StackTraceFormat { + parameters: Option, + parameter_types: Option, + parameter_names: Option, + parameter_values: Option, + line: Option, + module: Option, + include_all: Option, +} + +pub struct StackTraceArguments { + thread_id: i64, + start_frame: Option, + levels: Option, + format: Option, +} + +pub struct StackTraceResponse { + stack_frames: Vec, + total_frames: usize, +} + +pub struct StackFrame { + id: usize, + name: String, + source: Option, + line: usize, + column: usize, + end_line: Option, + end_column: Option, + can_restart: Option, + instruction_pointer_reference: Option, + module_id: Option, + presentation_hint: Option, +} + +pub struct ReadMemoryArguments { + memory_reference: String, + offset: Option, + count: Option, +} + +pub struct ReadMemoryResponse { + address: String, + unreadable_bytes: Option, + data: Option, +} + +pub struct DisassembleArguments { + memory_reference: String, + offset: Option, + instruction_offset: Option, + instruction_count: Option, + resolve_symbols: Option, +} + +pub struct DisassembledInstruction { + address: String, + instruction_bytes: Option, + instruction: String, + symbol: Option, + location: Option, + line: Option, + column: Option, + end_line: Option, + end_column: Option, + presentation_hint: Option, +} + +pub struct DisassembleResponse { + instructions: Vec, +} + +pub struct ContinueArguments { + thread_id: usize, + single_thread: Option, +} + +pub struct ContinueResponse { + all_threads_continued: Option, +} + +pub struct PauseArguments { + thread_id: usize, +} + +pub struct PauseResponse; + +pub struct BreakpointLocationsArguments { + source: Source, + line: usize, + column: Option, + end_line: Option, + end_column: Option, +} + +pub struct BreakpointLocation { + line: usize, + column: Option, + end_line: Option, + end_column: Option, +} + +pub struct BreakpointLocationsResponse { + breakpoints: Vec, +} + +pub struct DisconnectArguments { + restart: Option, + terminate_debuggee: Option, + suspend_debuggee: Option, +} + +pub struct DisconnectResponse; + +pub struct LaunchArguments { + no_debug: Option, +} + +pub struct Adapter<'gc> { + debugger: Option>, + state: State, +} + +impl<'gc> Adapter<'gc> { + pub fn new() -> Self { + Self { + debugger: None, + state: State::NotLaunched, + } + } + + pub fn initialize() -> Capabilities { + Capabilities { + supports_configuration_done_request: false, + supports_function_breakpoints: true, + supports_conditional_breakpoints: false, + supports_hit_conditional_breakpoints: false, + supports_evaluate_for_hovers: false, + supports_step_back: true, + supports_set_variable: true, + supports_restart_frame: false, + supports_goto_targets_request: false, + supports_step_in_targets_request: true, + supports_completions_request: false, + supports_modules_request: false, + supports_restart_request: true, + supports_exception_options: false, + supports_value_formatting_options: false, + supports_exception_info_request: false, + support_terminate_debuggee: true, + support_suspend_debuggee: true, + supports_delayed_stack_trace_loading: true, + supports_loaded_sources_request: false, + supports_log_points: false, + supports_terminate_threads_request: false, + supports_set_expression: false, + supports_terminate_request: true, + supports_data_breakpoints: true, + supports_read_memory_request: true, + supports_write_memory_request: false, + supports_disassemble_request: true, + supports_cancel_request: false, + supports_breakpoint_locations_request: true, + supports_clipboard_context: false, + supports_stepping_granularity: false, + supports_instruction_breakpoints: true, + supports_exception_filter_options: false, + supports_single_thread_execution_requests: false, + supports_data_breakpoint_bytes: false, + supports_ansi_styling: false, + } + } + + pub fn attach(&mut self, executor: Executor<'gc>) -> Result { + if self.state == State::NotLaunched { + self.debugger = Some(Debugger::new(executor)); + self.state = State::Launched; + Ok(AttachResponse) + } else { + Err(Error::reason(self.state)) + } + } + + pub fn step_in( + &mut self, + ctx: Context<'gc>, + _arguments: StepInArguments, + ) -> Result { + if self.state != State::Launched { + return Err(Error::reason(self.state)); + } + let Some(ref mut debugger) = self.debugger else { + return Err(Error::AdapterNotLaunched); + }; + debugger.step_into(ctx); + Ok(StepInResponse) + } + + pub fn step_out( + &mut self, + ctx: Context<'gc>, + _arguments: StepOutArguments, + ) -> Result { + if self.state != State::Launched { + return Err(Error::reason(self.state)); + } + let Some(ref mut debugger) = self.debugger else { + return Err(Error::AdapterNotLaunched); + }; + debugger.step_out(ctx); + Ok(StepOutResponse) + } + + pub fn set_breakpoints( + &mut self, + arguments: SetBreakpointsArguments, + ) -> SetBreakpointsResponse { + SetBreakpointsResponse { + breakpoints: vec![], + } + } + + pub fn set_data_breakpoints( + &mut self, + arguments: SetDataBreakpointsArguments, + ) -> SetDataBreakpointsResponse { + SetDataBreakpointsResponse { + breakpoints: vec![], + } + } + + pub fn set_function_breakpoints( + &mut self, + arguments: SetFunctionBreakpointsArguments, + ctx: piccolo::Context<'gc>, + ) -> SetFunctionBreakpointsResponse { + SetFunctionBreakpointsResponse { + breakpoints: vec![], + } + } + + pub fn set_instruction_breakpoints( + &mut self, + arguments: SetInstructionBreakpointsArguments, + ) -> SetInstructionBreakpointsResponse { + SetInstructionBreakpointsResponse { + breakpoints: vec![], + } + } + + pub fn stack_trace( + &mut self, + arguments: StackTraceArguments, + ctx: piccolo::Context<'gc>, + ) -> StackTraceResponse { + let mut stack_frames = Vec::new(); + + if let Some(ref debugger) = self.debugger { + let backtrace = debugger.backtrace(ctx); + let start_frame = arguments.start_frame.unwrap_or(0) as usize; + let levels = arguments.levels.map(|l| l as usize); + + let frames_to_take = if let Some(levels) = levels { + levels + } else { + backtrace.len().saturating_sub(start_frame) + }; + + for (index, location) in backtrace + .iter() + .skip(start_frame) + .take(frames_to_take) + .enumerate() + { + let frame_id = start_frame + index; + let function_name = location.function_ref().to_string(); + + stack_frames.push(StackFrame { + id: frame_id, + name: function_name, + source: Some(Source { + name: Some(location.chunk().to_string()), + path: Some(location.chunk().to_string()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: location.line(), + column: 1, // Default to column 1 + end_line: None, + end_column: None, + can_restart: Some(false), + instruction_pointer_reference: Some(format!("frame:{}", frame_id)), + module_id: None, + presentation_hint: None, + }); + } + + StackTraceResponse { + stack_frames, + total_frames: backtrace.len(), + } + } else { + StackTraceResponse { + stack_frames: vec![], + total_frames: 0, + } + } + } + + pub fn read_memory( + &mut self, + arguments: ReadMemoryArguments, + ) -> Result { + if self.state != State::Launched { + return Err(Error::reason(self.state)); + } + + let Some(ref debugger) = self.debugger else { + return Err(Error::AdapterNotLaunched); + }; + + let Some(bytes) = debugger.read_memory( + &arguments.memory_reference, + arguments.offset, + arguments.count, + ) else { + return Ok(ReadMemoryResponse { + address: arguments.memory_reference, + unreadable_bytes: arguments.count, + data: None, + }); + }; + + let data = base64::engine::general_purpose::STANDARD.encode(&bytes); + Ok(ReadMemoryResponse { + address: arguments.memory_reference, + unreadable_bytes: None, + data: Some(data), + }) + } + + pub fn disassemble( + &mut self, + arguments: DisassembleArguments, + ) -> Result { + if self.state != State::Launched { + return Err(Error::reason(self.state)); + } + + let Some(ref debugger) = self.debugger else { + return Err(Error::AdapterNotLaunched); + }; + + let Some(disassembled) = debugger.disassemble( + &arguments.memory_reference, + arguments.instruction_offset, + arguments.instruction_count, + ) else { + return Ok(DisassembleResponse { + instructions: vec![], + }); + }; + + Ok(DisassembleResponse { + instructions: disassembled + .into_iter() + .map(|disassembled| DisassembledInstruction { + address: disassembled.index.to_string(), + instruction_bytes: None, + instruction: format!("{:?}", disassembled.operation), + symbol: Some(disassembled.symbol.to_string()), + location: None, + line: Some(disassembled.line), + column: None, + end_line: None, + end_column: None, + presentation_hint: None, + }) + .collect(), + }) + } + + pub fn r#continue( + &mut self, + _arguments: ContinueArguments, + ctx: Context<'gc>, + ) -> Result { + if self.state != State::Launched { + return Err(Error::reason(self.state)); + } + + let Some(ref mut debugger) = self.debugger else { + return Err(Error::AdapterNotLaunched); + }; + + debugger.continue_run(ctx); + + Ok(ContinueResponse { + all_threads_continued: Some(true), + }) + } + + pub fn pause(&mut self, _arguments: PauseArguments) -> Result { + self.state = State::Suspended; + Ok(PauseResponse) + } + + pub fn breakpoint_locations( + &mut self, + arguments: BreakpointLocationsArguments, + ) -> BreakpointLocationsResponse { + BreakpointLocationsResponse { + breakpoints: vec![], + } + } + + pub fn restart(&mut self, executor: Executor<'gc>) { + self.debugger.take(); + self.debugger = Some(Debugger::new(executor)); + self.state = State::Launched; + } + + pub fn disconnect( + &mut self, + arguments: DisconnectArguments, + executor: Executor<'gc>, + ) -> Result { + let terminate = arguments.terminate_debuggee.unwrap_or(false); + let suspend = arguments.suspend_debuggee.unwrap_or(false); + let restart = arguments.restart.unwrap_or(false); + + if terminate { + self.terminate(); + } else if suspend { + self.pause(PauseArguments { thread_id: 0 })?; + } + + if restart { + self.restart(executor); + } + + Ok(DisconnectResponse) + } + + pub fn terminate(&mut self) { + self.state = State::Terminated; + self.debugger.take(); + } + + pub fn launch(&mut self, executor: Executor<'gc>, arguments: LaunchArguments) { + let no_debug = matches!(arguments.no_debug, Some(true)); + if !no_debug { + self.debugger = Some(Debugger::new(executor)); + } + } +} diff --git a/debugger/src/breakpoints.rs b/debugger/src/breakpoints.rs new file mode 100644 index 00000000..f10ac4c1 --- /dev/null +++ b/debugger/src/breakpoints.rs @@ -0,0 +1,59 @@ +use std::collections::BTreeSet; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Breakpoint { + pub id: usize, + pub source: String, + pub line: usize, +} + +#[derive(Debug, Default, Clone)] +pub struct Breakpoints { + breakpoints: BTreeSet, + next_breakpoint: usize, +} + +impl Breakpoints { + pub fn add(&mut self, chunk: impl Into, line: usize) -> usize { + let chunk = chunk.into(); + let id = self.next_breakpoint; + self.next_breakpoint += 1; + self.breakpoints.insert(Breakpoint { + id, + source: chunk, + line, + }); + id + } + + pub fn remove(&mut self, chunk: &str, line: usize) { + self.breakpoints + .retain(|breakpoint| !(breakpoint.source == chunk && breakpoint.line == line)) + } + + pub fn list(&self) -> impl Iterator)> { + let sources: BTreeSet<&str> = self + .breakpoints + .iter() + .map(|breakpoint| breakpoint.source.as_str()) + .collect(); + sources.into_iter().map(|source| { + ( + source, + self.breakpoints + .iter() + .filter(|breakpoint| breakpoint.source == source) + .map(|breakpoint| breakpoint.line) + .collect(), + ) + }) + } + + pub fn matches(&self, chunk: &str, line: usize) -> Vec { + self.breakpoints + .iter() + .filter(|breakpoint| breakpoint.source == chunk && breakpoint.line == line) + .map(|breakpoint| breakpoint.id) + .collect() + } +} diff --git a/debugger/src/lib.rs b/debugger/src/lib.rs new file mode 100644 index 00000000..d89e7a33 --- /dev/null +++ b/debugger/src/lib.rs @@ -0,0 +1,891 @@ +use piccolo::{ + compiler::LineNumber, + opcode::{OpCode, Operation}, + thread::{vm::run_vm, Executor, Frame, LuaFrame}, + Context, Fuel, FunctionPrototype, String as LuaString, Value, +}; +use std::mem::size_of; +use watch::{WatchEntry, WatchSpec}; + +#[allow(dead_code)] +#[derive(Debug, Clone)] +struct PrototypeReference { + chunk_name: String, + prototype_index: usize, +} + +pub use self::{ + breakpoints::{Breakpoint, Breakpoints}, + location::Location, + stop_reason::StopReason, + watch::WatchMode, +}; + +pub mod adapter; +pub mod breakpoints; +pub mod location; +pub mod stop_reason; +mod watch; + +#[derive(Debug)] +pub struct Disassembled { + symbol: &'static str, + index: usize, + line: usize, + operation: Operation, +} + +pub struct Debugger<'gc> { + executor: Executor<'gc>, + breakpoints: Breakpoints, + watchpoints: Vec>, +} + +impl<'gc> Debugger<'gc> { + pub fn new(executor: Executor<'gc>) -> Self { + Self { + executor, + breakpoints: Breakpoints::default(), + watchpoints: Vec::new(), + } + } + + pub fn executor(&self) -> Executor<'gc> { + self.executor + } + + pub fn add_breakpoint(&mut self, chunk: &str, line: usize) -> usize { + self.breakpoints.add(chunk, line) + } + + pub fn remove_breakpoint(&mut self, chunk: &str, line: usize) { + self.breakpoints.remove(chunk, line) + } + + pub fn list_breakpoints(&self) -> impl Iterator)> { + self.breakpoints.list() + } + + pub fn matches_breakpoint(&self, chunk: &str, line: usize) -> Vec { + self.breakpoints.matches(chunk, line) + } + + pub fn add_function_breakpoint(&mut self, ctx: Context<'gc>, name: &str) -> Option { + self.resolve_function_breakpoint(ctx, name) + .map(|(chunk, line)| self.add_breakpoint(&chunk, line)) + } + + pub fn resolve_function_breakpoint( + &self, + ctx: Context<'gc>, + name: &str, + ) -> Option<(String, usize)> { + let env = ctx.globals(); + let mut segments = name.split('.'); + let first = segments.next()?; + let mut current = env.get_value(ctx, ctx.intern(first.as_bytes())); + for segment in segments { + match current { + Value::Table(t) => { + current = t.get_raw(Value::String(ctx.intern(segment.as_bytes()))); + } + _ => return None, + } + } + match current { + Value::Function(piccolo::Function::Closure(closure)) => { + let proto = closure.prototype(); + let chunk_name = lua_string_to_string(proto.chunk_name); + let first_line = if proto.opcode_line_numbers.is_empty() { + 0 + } else { + proto.opcode_line_numbers[0].1 .0 as usize + }; + Some((chunk_name, first_line)) + } + _ => None, + } + } + + pub fn continue_run(&mut self, ctx: Context<'gc>) -> StopReason { + loop { + match self.current_location(ctx) { + Ok(Some(location)) => { + if let Some(stop_reason) = self.stop_if_breakpoint(&location) { + return stop_reason; + } + } + Ok(None) => {} + Err(stop_reason) => return stop_reason, + } + match self.step_one(ctx) { + StopReason::Step(_) => continue, + other => return other, + } + } + } + + pub fn step_into(&mut self, ctx: Context<'gc>) -> StopReason { + let start_location = match self.current_location(ctx) { + Ok(Some(location)) => location, + Ok(None) => return StopReason::Finished, + Err(stop_reason) => return stop_reason, + }; + + if let Some(stop_reason) = self.stop_if_breakpoint(&start_location) { + return stop_reason; + } + + loop { + match self.step_one(ctx) { + StopReason::Step(location) => { + if let Some(stop_reason) = self.stop_if_breakpoint(&location) { + return stop_reason; + } + if location.line() != start_location.line() + || location.chunk() != start_location.chunk() + { + return StopReason::Step(location); + } + } + StopReason::Watchpoint(hit) => return StopReason::Watchpoint(hit), + other => return other, + } + } + } + + pub fn step_over(&mut self, ctx: Context<'gc>) -> StopReason { + let (start_depth, start_location) = match self.stack_depth_and_location(ctx) { + Ok((depth, Some(location))) => (depth, location), + Ok((_depth, None)) => return StopReason::Finished, + Err(stop_reason) => return stop_reason, + }; + + if let Some(stop_reason) = self.stop_if_breakpoint(&start_location) { + return stop_reason; + } + + loop { + match self.step_one(ctx) { + StopReason::Step(location) => { + if let Some(stop_reason) = self.stop_if_breakpoint(&location) { + return stop_reason; + } + let depth = match self.current_stack_depth(ctx) { + Ok(depth) => depth, + Err(stop_reason) => return stop_reason, + }; + if depth <= start_depth + && (location.line() != start_location.line() + || location.chunk() != start_location.chunk()) + { + return StopReason::Step(location); + } + } + StopReason::Watchpoint(hit) => return StopReason::Watchpoint(hit), + other => return other, + } + } + } + + pub fn step_out(&mut self, ctx: Context<'gc>) -> StopReason { + let start_depth = match self.current_stack_depth(ctx) { + Ok(depth) => depth, + Err(stop_reason) => return stop_reason, + }; + + loop { + match self.step_one(ctx) { + StopReason::Step(location) => { + if let Some(stop_reason) = self.stop_if_breakpoint(&location) { + return stop_reason; + } + let depth = match self.current_stack_depth(ctx) { + Ok(depth) => depth, + Err(stop_reason) => return stop_reason, + }; + if depth < start_depth { + return StopReason::Step(location); + } + } + StopReason::Watchpoint(hit) => return StopReason::Watchpoint(hit), + other => return other, + } + } + } + + pub fn backtrace(&self, _ctx: Context<'gc>) -> Vec { + let mut out = Vec::new(); + let _ = self.with_top_thread_state(|state| { + out.reserve(state.frames.len()); + for frame in state.frames.iter() { + if let Frame::Lua { closure, pc, .. } = frame { + let proto = closure.prototype(); + let chunk_name = lua_string_to_string(proto.chunk_name); + let function_ref = proto.reference.map_strings(lua_string_to_string); + let line = opcode_index_to_line(&proto.opcode_line_numbers, *pc); + out.push(Location::new(chunk_name, function_ref, line)); + } + } + }); + out + } + + pub fn read_register(&self, _ctx: Context<'gc>, register_index: usize) -> Option> { + let mut value = None; + let _ = self.with_top_thread_state(|state| { + if let Some(Frame::Lua { + base, stack_size, .. + }) = state.frames.last() + { + let base = *base; + let size = *stack_size; + if register_index < size { + value = Some(state.stack[base + register_index]); + } + } + }); + value + } + + pub fn read_global(&self, ctx: Context<'gc>, name: &str) -> Value<'gc> { + let key = ctx.intern(name.as_bytes()); + ctx.globals().get_value(ctx, key) + } + + pub fn step_one(&mut self, ctx: Context<'gc>) -> StopReason { + const FUEL_NON_LUA_CALLBACK_STEP: i32 = 11; + const FUEL_NON_LUA_SEQUENCE_STEP: i32 = 7; + const FUEL_NON_LUA_ERROR_STEP: i32 = 3; + + match self.executor.mode() { + piccolo::thread::ExecutorMode::Stopped => return StopReason::Finished, + piccolo::thread::ExecutorMode::Result => return StopReason::Finished, + piccolo::thread::ExecutorMode::Suspended => return StopReason::Suspended, + piccolo::thread::ExecutorMode::Running => return StopReason::Error, + piccolo::thread::ExecutorMode::Normal => {} + } + + let (is_lua_top, non_lua_fuel) = + match self.with_top_thread_state(|state| match state.frames.last() { + Some(Frame::Lua { .. }) => (true, 0), + Some(Frame::Callback { .. }) => (false, FUEL_NON_LUA_CALLBACK_STEP), + Some(Frame::Sequence { .. }) => (false, FUEL_NON_LUA_SEQUENCE_STEP), + Some(Frame::Error(_)) => (false, FUEL_NON_LUA_ERROR_STEP), + Some(Frame::Result { .. }) => (false, -1), + Some(Frame::WaitThread) | Some(Frame::Yielded) | Some(Frame::Start(_)) => { + (false, 3) + } + None => (false, -1), + }) { + Ok(value) => value, + Err(stop_reason) => return stop_reason, + }; + + if non_lua_fuel == -1 { + return StopReason::Finished; + } + + if is_lua_top { + let _ = self.with_top_thread_state_mut(ctx, |thread, state| { + let mut fuel = Fuel::with(1_000_000); + let lua_frame = LuaFrame { + state, + thread, + fuel: &mut fuel, + }; + let _ = run_vm(ctx, lua_frame, 1); + }); + } else { + let mut fuel = Fuel::with(non_lua_fuel); + let _ = self.executor.step(ctx, &mut fuel); + } + + let location = match self.current_location(ctx) { + Ok(Some(location)) => location, + Ok(None) => return StopReason::Finished, + Err(stop_reason) => return stop_reason, + }; + if let Some(hit) = self.poll_watchpoints(ctx) { + return StopReason::Watchpoint(hit); + } + StopReason::Step(location) + } + + fn current_stack_depth(&self, _ctx: Context<'gc>) -> Result { + self.with_top_thread_state(|state| state.frames.len()) + } + + fn stack_depth_and_location( + &self, + _ctx: Context<'gc>, + ) -> Result<(usize, Option), StopReason> { + self.with_top_thread_state(|state| { + let depth = state.frames.len(); + let loc = current_location_from_state_in_thread(state); + (depth, loc) + }) + } + + fn current_location(&self, _ctx: Context<'gc>) -> Result, StopReason> { + self.with_top_thread_state(|state| current_location_from_state_in_thread(state)) + } + + fn with_top_thread_state( + &self, + f: impl FnOnce(&piccolo::thread::ThreadState<'gc>) -> R, + ) -> Result { + let guard = self.executor.0.borrow(); + let thread = *guard.thread_stack.last().ok_or(StopReason::Finished)?; + drop(guard); + let thread_state = thread.into_inner().borrow(); + Ok(f(&thread_state)) + } + + fn with_top_thread_state_mut( + &self, + ctx: Context<'gc>, + f: impl FnOnce(piccolo::Thread<'gc>, &mut piccolo::thread::ThreadState<'gc>) -> R, + ) -> Result { + let guard = self.executor.0.borrow(); + let top_thread = *guard.thread_stack.last().ok_or(StopReason::Finished)?; + drop(guard); + let mut thread_state = top_thread.into_inner().borrow_mut(&ctx); + let r = f(top_thread, &mut thread_state); + drop(thread_state); + Ok(r) + } + + pub fn read_registers(&self, _ctx: Context<'gc>) -> Option>> { + let mut out = None; + let _ = self.with_top_thread_state(|state| { + if let Some(Frame::Lua { + base, stack_size, .. + }) = state.frames.last() + { + let base = *base; + let size = *stack_size; + out = Some(state.stack[base..base + size].to_vec()); + } + }); + out + } + + pub fn read_upvalues(&self, _ctx: Context<'gc>) -> Option>> { + let mut out = None; + let _ = self.with_top_thread_state(|state| { + if let Some(Frame::Lua { closure, .. }) = state.frames.last() { + let mut values = Vec::new(); + for &upvalue in closure.upvalues() { + if let piccolo::closure::UpValueState::Closed(value) = upvalue.get() { + values.push(value); + } else { + values.push(Value::Nil); + } + } + out = Some(values); + } + }); + out + } + + pub fn stack_snapshot(&self, _ctx: Context<'gc>) -> Vec<(Location, Vec>)> { + let mut out = Vec::new(); + let _ = self.with_top_thread_state(|state| { + out.reserve(state.frames.len()); + for frame in state.frames.iter() { + if let Frame::Lua { + base, + stack_size, + closure, + pc, + .. + } = frame + { + let proto = closure.prototype(); + let chunk_name = lua_string_to_string(proto.chunk_name); + let function_ref = proto.reference.map_strings(lua_string_to_string); + let line = opcode_index_to_line(&proto.opcode_line_numbers, *pc); + let location = Location::new(chunk_name, function_ref, line); + let base = *base; + let size = *stack_size; + out.push((location, state.stack[base..base + size].to_vec())); + } + } + }); + out + } + + pub fn disassemble( + &self, + memory_reference: &str, + instruction_offset: Option, + instruction_count: Option, + ) -> Option> { + let (ty, s) = memory_reference.split_once(':').unwrap_or(("frame", "")); + match ty { + "frame" => self.disassemble_frame( + if s.is_empty() { + usize::MAX + } else { + s.parse().ok()? + }, + instruction_offset, + instruction_count, + ), + "prototype" => self.disassemble_prototype(s, instruction_offset, instruction_count), + _ => None, + } + } + + fn disassemble_frame( + &self, + frame: usize, + instruction_offset: Option, + instruction_count: Option, + ) -> Option> { + self.with_top_thread_state(|state| { + if let Some(Frame::Lua { closure, pc, .. }) = if frame == usize::MAX { + state.frames.last() + } else { + state.frames.get(frame) + } { + let prototype = closure.prototype(); + let current_pc = *pc; + Some(self.disassemble_prototype_opcodes( + &prototype, + Some(current_pc), + instruction_offset, + instruction_count, + )) + } else { + None + } + }) + .ok()? + } + + fn disassemble_prototype( + &self, + prototype: &str, + instruction_offset: Option, + instruction_count: Option, + ) -> Option> { + let (chunk, index) = prototype.split_once(':')?; + + let index: usize = index.parse().ok()?; + + self.with_top_thread_state(|state| { + for frame in state.frames.iter() { + if let Frame::Lua { closure, .. } = frame { + let proto = closure.prototype(); + let frame_chunk = lua_string_to_string(proto.chunk_name); + + if frame_chunk == chunk { + if let Some(nested_proto) = proto.prototypes.get(index) { + return Some(self.disassemble_prototype_opcodes( + nested_proto, + None, + instruction_offset, + instruction_count, + )); + } + } + } + } + None + }) + .ok()? + } + + fn disassemble_prototype_opcodes( + &self, + prototype: &FunctionPrototype, + current_pc: Option, + instruction_offset: Option, + instruction_count: Option, + ) -> Vec { + let start_offset = instruction_offset.unwrap_or(0); + let max_count = instruction_count.unwrap_or(prototype.opcodes.len()); + + let end_offset = std::cmp::min(start_offset + max_count, prototype.opcodes.len()); + + let mut disassembled = Vec::with_capacity(end_offset - start_offset); + + for index in start_offset..end_offset { + if let Some(opcode) = prototype.opcodes.get(index) { + let operation = (*opcode).decode(); + let line = opcode_index_to_line(&prototype.opcode_line_numbers, index) + 1; + let symbol = if Some(index) == current_pc { + "=>" + } else { + " " + }; + disassembled.push(Disassembled { + symbol, + index, + line, + operation, + }); + } + } + + disassembled + } + + pub fn read_memory( + &self, + memory_reference: &str, + byte_offset: Option, + byte_count: Option, + ) -> Option> { + let (ty, s) = memory_reference.split_once(':').unwrap_or(("frame", "")); + + match ty { + "frame" => self.read_frame_memory( + if s.is_empty() { + usize::MAX + } else { + s.parse().ok()? + }, + byte_offset, + byte_count, + ), + "prototype" => self.read_prototype_memory(s, byte_offset, byte_count), + _ => None, + } + } + + fn read_frame_memory( + &self, + frame: usize, + byte_offset: Option, + byte_count: Option, + ) -> Option> { + self.with_top_thread_state(|state| { + if let Some(piccolo::thread::Frame::Lua { closure, .. }) = if frame == usize::MAX { + state.frames.last() + } else { + state.frames.get(frame) + } { + let prototype = closure.prototype(); + Some(self.read_opcodes(&prototype.opcodes, byte_offset, byte_count)) + } else { + None + } + }) + .ok()? + } + + fn read_prototype_memory( + &self, + prototype: &str, + byte_offset: Option, + byte_count: Option, + ) -> Option> { + let (chunk, index) = prototype.split_once(':')?; + + let index: usize = index.parse().ok()?; + + self.with_top_thread_state(|state| { + for frame in state.frames.iter() { + if let Frame::Lua { closure, .. } = frame { + let proto = closure.prototype(); + let frame_chunk = lua_string_to_string(proto.chunk_name); + + if frame_chunk == chunk { + if let Some(nested_proto) = proto.prototypes.get(index) { + return Some( + (self.read_opcodes(&nested_proto.opcodes, byte_offset, byte_count)), + ); + } + } + } + } + None + }) + .ok()? + } + + fn read_opcodes( + &self, + opcodes: &[OpCode], + byte_offset: Option, + byte_count: Option, + ) -> Vec { + fn to_bytes(value: T) -> Vec { + let mut bytes = Vec::with_capacity(size_of::()); + + // SAFETY: We are copying the bytes of a plain-old-data (POD) type T into a Vec with enough capacity. + // The Vec is pre-allocated with the correct size, and we set the length after copying. + unsafe { + std::ptr::copy_nonoverlapping( + std::ptr::addr_of!(value).cast::(), + bytes.as_mut_ptr(), + size_of::(), + ); + bytes.set_len(size_of::()); + } + + bytes + } + + let mut raw = Vec::new(); + + for opcode in opcodes { + let bytes = to_bytes(*opcode); + raw.extend_from_slice(&bytes); + } + + let byte_offset = byte_offset.unwrap_or(0); + let byte_count = byte_count.unwrap_or(raw.len()); + + let start = std::cmp::min(byte_offset, raw.len()); + let end = std::cmp::min(start + byte_count, raw.len()); + + raw[start..end].to_vec() + } + + pub fn watch_register(&mut self, register: usize) { + self.watchpoints.push(WatchEntry { + spec: WatchSpec::Register(register), + last: None, + mode: WatchMode::Modify, + last_line_seen: None, + }) + } + + pub fn watch_global(&mut self, name: impl Into) { + self.watchpoints.push(WatchEntry { + spec: WatchSpec::Global(name.into()), + last: None, + mode: WatchMode::Modify, + last_line_seen: None, + }) + } + + pub fn watch_table_key(&mut self, table: piccolo::Table<'gc>, key: Value<'gc>) { + self.watchpoints.push(WatchEntry { + spec: WatchSpec::TableKey(table, key), + last: None, + mode: WatchMode::Modify, + last_line_seen: None, + }) + } + + pub fn watch_register_with_mode(&mut self, register: usize, mode: WatchMode) { + self.watchpoints.push(WatchEntry { + spec: WatchSpec::Register(register), + last: None, + mode, + last_line_seen: None, + }) + } + + pub fn watch_global_with_mode(&mut self, name: impl Into, mode: WatchMode) { + self.watchpoints.push(WatchEntry { + spec: WatchSpec::Global(name.into()), + last: None, + mode, + last_line_seen: None, + }) + } + + pub fn watch_table_key_with_mode( + &mut self, + table: piccolo::Table<'gc>, + key: Value<'gc>, + mode: WatchMode, + ) { + self.watchpoints.push(WatchEntry { + spec: WatchSpec::TableKey(table, key), + last: None, + mode, + last_line_seen: None, + }) + } + + pub fn clear_watchpoints(&mut self) { + self.watchpoints.clear(); + } + + fn poll_watchpoints(&mut self, ctx: Context<'gc>) -> Option { + for index in 0..self.watchpoints.len() { + let Some(entry) = self.watchpoints.get(index) else { + continue; + }; + let spec = match &entry.spec { + WatchSpec::Register(i) => WatchSpec::Register(*i), + WatchSpec::Global(n) => WatchSpec::Global(n.clone()), + WatchSpec::TableKey(t, k) => WatchSpec::TableKey(*t, *k), + }; + let last = entry.last; + + let current = self.eval_watch(ctx, &spec); + let should_fire = match entry.mode { + WatchMode::Modify => match (last, current) { + (None, Some(_)) => true, + (Some(_), None) => true, + (Some(left), Some(right)) => !values_equal(left, right), + (None, None) => false, + }, + WatchMode::Access => { + if current.is_some() { + let current_line = self + .current_location(ctx) + .ok() + .flatten() + .map(|l| (l.chunk().to_owned(), l.line())); + if let (Some((chunk, line)), Some(entry)) = + (current_line, self.watchpoints.get(index)) + { + if entry.last_line_seen != Some((chunk.to_owned(), line)) { + if let Some(entry) = self.watchpoints.get_mut(index) { + entry.last_line_seen = Some((chunk.to_owned(), line)); + } + true + } else { + false + } + } else { + false + } + } else { + false + } + } + }; + if should_fire { + let message = format!( + "watchpoint hit: {}", + self.describe_watch_change(&spec, last, current) + ); + if let Some(entry) = self.watchpoints.get_mut(index) { + entry.last = current; + } + return Some(message); + } + if let Some(entry) = self.watchpoints.get_mut(index) { + entry.last = current; + } + } + None + } + + fn eval_watch(&self, ctx: Context<'gc>, spec: &WatchSpec<'gc>) -> Option> { + match spec { + WatchSpec::Register(index) => { + let mut out = None; + let _ = self.with_top_thread_state(|state| { + if let Some(Frame::Lua { + base, stack_size, .. + }) = state.frames.last() + { + let index = *index; + let base = *base; + let size = *stack_size; + if index < size { + out = Some(state.stack[base + index]); + } + } + }); + out + } + WatchSpec::Global(name) => { + let key = ctx.intern(name.as_bytes()); + Some(ctx.globals().get_value(ctx, key)) + } + WatchSpec::TableKey(table, key) => Some(table.get_raw(*key)), + } + } + + fn describe_watch_change( + &self, + spec: &WatchSpec<'gc>, + old: Option>, + new: Option>, + ) -> String { + let spec = match spec { + WatchSpec::Register(index) => format!("register[{}]", index), + WatchSpec::Global(name) => format!("global['{}']", name), + WatchSpec::TableKey(t, k) => format!( + "table({:p})[{}]", + piccolo::table::Table::into_inner(*t).as_ptr(), + value_to_string(*k) + ), + }; + format!( + "{}: {} -> {}", + spec, + old.map(|value| value_to_string(value)) + .unwrap_or("".into()), + new.map(|value| value_to_string(value)) + .unwrap_or("".into()) + ) + } + + fn stop_if_breakpoint(&self, location: &Location) -> Option { + let breakpoint_ids = self.matches_breakpoint(location.chunk(), location.line()); + if breakpoint_ids.is_empty() { + None + } else { + Some(StopReason::Breakpoint { + location: location.to_owned(), + breakpoint_ids, + }) + } + } +} + +fn current_location_from_state_in_thread<'gc>( + thread_state: &piccolo::thread::ThreadState<'gc>, +) -> Option { + match thread_state.frames.last() { + Some(Frame::Lua { closure, pc, .. }) => { + let proto = closure.prototype(); + let chunk_name = lua_string_to_string(proto.chunk_name); + let line = opcode_index_to_line(&proto.opcode_line_numbers, *pc); + Some(Location::new( + chunk_name, + proto.reference.map_strings(lua_string_to_string), + line, + )) + } + _ => None, + } +} + +fn opcode_index_to_line(op_lines: &[(usize, LineNumber)], pc: usize) -> usize { + if op_lines.is_empty() { + return 0; + } + match op_lines.binary_search_by_key(&pc, |(opi, _)| *opi) { + Ok(i) => op_lines[i].1 .0 as usize, + Err(0) => op_lines[0].1 .0 as usize, + Err(i) => op_lines[i - 1].1 .0 as usize, + } +} + +fn values_equal<'gc>(left: Value<'gc>, right: Value<'gc>) -> bool { + use Value::*; + match (left, right) { + (Nil, Nil) => true, + (Boolean(x), Boolean(y)) => x == y, + (Integer(x), Integer(y)) => x == y, + (Number(x), Number(y)) => x.to_bits() == y.to_bits(), + (String(x), String(y)) => x == y, + (Table(x), Table(y)) => x == y, + (Function(x), Function(y)) => x == y, + (Thread(x), Thread(y)) => x == y, + (UserData(x), UserData(y)) => x == y, + _ => false, + } +} + +fn value_to_string<'gc>(value: Value<'gc>) -> String { + format!("{}", value.display()) +} + +fn lua_string_to_string(s: LuaString<'_>) -> String { + format!("{}", s.display_lossy()) +} diff --git a/debugger/src/location.rs b/debugger/src/location.rs new file mode 100644 index 00000000..2c6f10ff --- /dev/null +++ b/debugger/src/location.rs @@ -0,0 +1,38 @@ +use piccolo::compiler::FunctionRef; + +#[derive(Debug, Clone)] +pub struct Location { + chunk_name: String, + function_ref: FunctionRef, + line: usize, +} + +impl Location { + pub fn new(chunk_name: String, function_ref: FunctionRef, line: usize) -> Self { + Self { + chunk_name, + function_ref, + line, + } + } + + pub fn chunk(&self) -> &str { + &self.chunk_name + } + + pub fn line(&self) -> usize { + self.line + } + + pub fn function_ref(&self) -> &FunctionRef { + &self.function_ref + } +} + +impl PartialEq for Location { + fn eq(&self, other: &Self) -> bool { + self.chunk_name == other.chunk_name && self.line == other.line + } +} + +impl Eq for Location {} diff --git a/debugger/src/stop_reason.rs b/debugger/src/stop_reason.rs new file mode 100644 index 00000000..ce06ae47 --- /dev/null +++ b/debugger/src/stop_reason.rs @@ -0,0 +1,14 @@ +use super::Location; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum StopReason { + Breakpoint { + location: Location, + breakpoint_ids: Vec, + }, + Step(Location), + Watchpoint(String), + Finished, + Suspended, + Error, +} diff --git a/debugger/src/watch.rs b/debugger/src/watch.rs new file mode 100644 index 00000000..08318977 --- /dev/null +++ b/debugger/src/watch.rs @@ -0,0 +1,5 @@ +pub mod entry; +pub mod mode; +pub mod spec; + +pub use self::{entry::WatchEntry, mode::WatchMode, spec::WatchSpec}; diff --git a/debugger/src/watch/entry.rs b/debugger/src/watch/entry.rs new file mode 100644 index 00000000..b69bfe4e --- /dev/null +++ b/debugger/src/watch/entry.rs @@ -0,0 +1,10 @@ +use super::{WatchMode, WatchSpec}; +use piccolo::Value; + +#[derive(Debug, Clone)] +pub struct WatchEntry<'gc> { + pub spec: WatchSpec<'gc>, + pub last: Option>, + pub mode: WatchMode, + pub last_line_seen: Option<(String, usize)>, +} diff --git a/debugger/src/watch/mode.rs b/debugger/src/watch/mode.rs new file mode 100644 index 00000000..0c565db6 --- /dev/null +++ b/debugger/src/watch/mode.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Clone, Copy)] +pub enum WatchMode { + Access, + Modify, +} diff --git a/debugger/src/watch/spec.rs b/debugger/src/watch/spec.rs new file mode 100644 index 00000000..7f023647 --- /dev/null +++ b/debugger/src/watch/spec.rs @@ -0,0 +1,8 @@ +use piccolo::Value; + +#[derive(Debug, Clone)] +pub enum WatchSpec<'gc> { + Register(usize), + Global(String), + TableKey(piccolo::Table<'gc>, Value<'gc>), +} diff --git a/src/closure.rs b/src/closure.rs index 31325544..f2b266a0 100644 --- a/src/closure.rs +++ b/src/closure.rs @@ -187,8 +187,8 @@ pub enum ClosureError { #[derive(Debug, Collect)] #[collect(no_drop)] pub struct ClosureInner<'gc> { - proto: Gc<'gc, FunctionPrototype<'gc>>, - upvalues: vec::Vec, MetricsAlloc<'gc>>, + pub proto: Gc<'gc, FunctionPrototype<'gc>>, + pub upvalues: vec::Vec, MetricsAlloc<'gc>>, } /// A garbage collected pointer to an executable Lua function. diff --git a/src/thread/executor.rs b/src/thread/executor.rs index 4f4630bd..37a443cd 100644 --- a/src/thread/executor.rs +++ b/src/thread/executor.rs @@ -42,7 +42,7 @@ pub struct BadExecutorMode { #[derive(Debug, Collect)] #[collect(no_drop)] pub struct ExecutorState<'gc> { - thread_stack: vec::Vec, MetricsAlloc<'gc>>, + pub thread_stack: vec::Vec, MetricsAlloc<'gc>>, } pub type ExecutorInner<'gc> = RefLock>; @@ -66,7 +66,7 @@ pub type ExecutorInner<'gc> = RefLock>; /// run by the same `Executor` which called the callback. #[derive(Debug, Copy, Clone, Collect)] #[collect(no_drop)] -pub struct Executor<'gc>(Gc<'gc, ExecutorInner<'gc>>); +pub struct Executor<'gc>(pub Gc<'gc, ExecutorInner<'gc>>); impl<'gc> PartialEq for Executor<'gc> { fn eq(&self, other: &Executor<'gc>) -> bool { diff --git a/src/thread/mod.rs b/src/thread/mod.rs index 8e6a0591..ef8cf93a 100644 --- a/src/thread/mod.rs +++ b/src/thread/mod.rs @@ -1,6 +1,6 @@ -mod executor; +pub mod executor; mod thread; -mod vm; +pub mod vm; use thiserror::Error; @@ -11,7 +11,10 @@ pub use self::{ BadExecutorMode, CurrentThread, Execution, Executor, ExecutorInner, ExecutorMode, UpperLuaFrame, }, - thread::{BadThreadMode, OpenUpValue, Thread, ThreadInner, ThreadMode}, + thread::{ + BadThreadMode, Frame, LuaFrame, LuaRegisters, OpenUpValue, Thread, ThreadInner, ThreadMode, + ThreadState, + }, }; #[derive(Debug, Clone, Error)] diff --git a/src/thread/thread.rs b/src/thread/thread.rs index a03d4c2a..564da0ef 100644 --- a/src/thread/thread.rs +++ b/src/thread/thread.rs @@ -57,7 +57,7 @@ pub type ThreadInner<'gc> = RefLock>; /// `Thread`s, suspend them, resume them, and may yield to calling `Thread`s. #[derive(Debug, Clone, Copy, Collect)] #[collect(no_drop)] -pub struct Thread<'gc>(Gc<'gc, RefLock>>); +pub struct Thread<'gc>(pub Gc<'gc, RefLock>>); impl<'gc> PartialEq for Thread<'gc> { fn eq(&self, other: &Thread<'gc>) -> bool { @@ -264,7 +264,7 @@ impl<'gc> OpenUpValue<'gc> { #[derive(Debug, Copy, Clone, Collect)] #[collect(require_static)] -pub(super) enum MetaReturn { +pub enum MetaReturn { /// No return value is expected. None, /// Place a single return value at an index relative to the returned to function's stack bottom. @@ -275,7 +275,7 @@ pub(super) enum MetaReturn { #[derive(Debug, Copy, Clone, Collect)] #[collect(require_static)] -pub(super) enum LuaReturn { +pub enum LuaReturn { /// Normal function call, place return values at the bottom of the returning function's stack, /// as normal. Normal(VarCount), @@ -285,7 +285,7 @@ pub(super) enum LuaReturn { #[derive(Debug, Collect)] #[collect(no_drop)] -pub(super) enum Frame<'gc> { +pub enum Frame<'gc> { /// A running Lua frame. Lua { bottom: usize, @@ -327,8 +327,8 @@ pub(super) enum Frame<'gc> { #[derive(Debug, Collect)] #[collect(no_drop)] pub struct ThreadState<'gc> { - pub(super) frames: vec::Vec, MetricsAlloc<'gc>>, - pub(super) stack: vec::Vec, MetricsAlloc<'gc>>, + pub frames: vec::Vec, MetricsAlloc<'gc>>, + pub stack: vec::Vec, MetricsAlloc<'gc>>, pub(super) open_upvalues: vec::Vec, MetricsAlloc<'gc>>, } @@ -529,10 +529,10 @@ impl<'gc> ThreadState<'gc> { } } -pub(super) struct LuaFrame<'gc, 'a> { - pub(super) thread: Thread<'gc>, - pub(super) state: &'a mut ThreadState<'gc>, - pub(super) fuel: &'a mut Fuel, +pub struct LuaFrame<'gc, 'a> { + pub state: &'a mut ThreadState<'gc>, + pub thread: Thread<'gc>, + pub fuel: &'a mut Fuel, } impl<'gc, 'a> LuaFrame<'gc, 'a> { @@ -540,7 +540,7 @@ impl<'gc, 'a> LuaFrame<'gc, 'a> { const FUEL_PER_ITEM: i32 = 1; // Returns the active closure for this Lua frame - pub(super) fn closure(&self) -> Closure<'gc> { + pub fn closure(&self) -> Closure<'gc> { match self.state.frames.last() { Some(Frame::Lua { closure, .. }) => *closure, _ => panic!("top frame is not lua frame"), @@ -548,7 +548,7 @@ impl<'gc, 'a> LuaFrame<'gc, 'a> { } /// returns a view of the Lua frame's registers - pub(super) fn registers<'b>(&'b mut self) -> LuaRegisters<'gc, 'b> { + pub fn registers<'b>(&'b mut self) -> LuaRegisters<'gc, 'b> { match self.state.frames.last_mut() { Some(Frame::Lua { bottom, base, pc, .. @@ -569,7 +569,7 @@ impl<'gc, 'a> LuaFrame<'gc, 'a> { } /// Place the current frame's varargs at the given register, expecting the given count - pub(super) fn varargs(&mut self, dest: RegisterIndex, count: VarCount) -> Result<(), VMError> { + pub fn varargs(&mut self, dest: RegisterIndex, count: VarCount) -> Result<(), VMError> { let Some(Frame::Lua { bottom, base, @@ -622,7 +622,7 @@ impl<'gc, 'a> LuaFrame<'gc, 'a> { /// /// Expects a table at register `table_base`, the current table index at `table_base + 1`, and /// `count` elements following this. - pub(super) fn set_table_list( + pub fn set_table_list( &mut self, mc: &Mutation<'gc>, table_base: RegisterIndex, @@ -684,7 +684,7 @@ impl<'gc, 'a> LuaFrame<'gc, 'a> { /// Call the function at the given register with the given arguments. On return, results will be /// placed starting at the function register. - pub(super) fn call_function( + pub fn call_function( self, ctx: Context<'gc>, func: RegisterIndex, @@ -730,7 +730,7 @@ impl<'gc, 'a> LuaFrame<'gc, 'a> { /// Calls the function at the given index with a constant number of arguments without /// invalidating the function or its arguments. Returns are placed *after* the function and its /// arguments, and all registers past this are invalidated as normal. - pub(super) fn call_function_keep( + pub fn call_function_keep( self, ctx: Context<'gc>, func: RegisterIndex, @@ -778,7 +778,7 @@ impl<'gc, 'a> LuaFrame<'gc, 'a> { /// places an optional single result of this function call at the given register. /// /// Nothing at all in the frame is invalidated, other than optionally placing the return value. - pub(super) fn call_meta_function( + pub fn call_meta_function( self, _ctx: Context<'gc>, func: Function<'gc>, @@ -819,7 +819,7 @@ impl<'gc, 'a> LuaFrame<'gc, 'a> { /// Tail-call the function at the given register with the given arguments. Pops the current Lua /// frame, pushing a new frame for the given function. - pub(super) fn tail_call_function( + pub fn tail_call_function( self, ctx: Context<'gc>, func: RegisterIndex, @@ -866,7 +866,7 @@ impl<'gc, 'a> LuaFrame<'gc, 'a> { } /// Return to the upper frame with results starting at the given register index. - pub(super) fn return_upper( + pub fn return_upper( self, mc: &Mutation<'gc>, start: RegisterIndex, @@ -906,18 +906,18 @@ impl<'gc, 'a> LuaFrame<'gc, 'a> { } } -pub(super) struct LuaRegisters<'gc, 'a> { +pub struct LuaRegisters<'gc, 'a> { pub pc: &'a mut usize, pub stack_frame: &'a mut [Value<'gc>], - upper_stack: &'a mut [Value<'gc>], - bottom: usize, - base: usize, - open_upvalues: &'a mut vec::Vec, MetricsAlloc<'gc>>, - thread: Thread<'gc>, + pub upper_stack: &'a mut [Value<'gc>], + pub bottom: usize, + pub base: usize, + pub open_upvalues: &'a mut vec::Vec, MetricsAlloc<'gc>>, + pub thread: Thread<'gc>, } impl<'gc, 'a> LuaRegisters<'gc, 'a> { - pub(super) fn open_upvalue(&mut self, mc: &Mutation<'gc>, reg: RegisterIndex) -> UpValue<'gc> { + pub fn open_upvalue(&mut self, mc: &Mutation<'gc>, reg: RegisterIndex) -> UpValue<'gc> { let ind = self.base + reg.0 as usize; match self .open_upvalues @@ -938,7 +938,7 @@ impl<'gc, 'a> LuaRegisters<'gc, 'a> { } } - pub(super) fn get_upvalue(&self, mc: &Mutation<'gc>, upvalue: UpValue<'gc>) -> Value<'gc> { + pub fn get_upvalue(&self, mc: &Mutation<'gc>, upvalue: UpValue<'gc>) -> Value<'gc> { match upvalue.get() { UpValueState::Open(open_upvalue) => { if open_upvalue.thread.as_ptr() == Gc::as_ptr(self.thread.0) { @@ -955,12 +955,7 @@ impl<'gc, 'a> LuaRegisters<'gc, 'a> { } } - pub(super) fn set_upvalue( - &mut self, - mc: &Mutation<'gc>, - upvalue: UpValue<'gc>, - value: Value<'gc>, - ) { + pub fn set_upvalue(&mut self, mc: &Mutation<'gc>, upvalue: UpValue<'gc>, value: Value<'gc>) { match upvalue.get() { UpValueState::Open(open_upvalue) => { if open_upvalue.thread.as_ptr() == Gc::as_ptr(self.thread.0) { @@ -979,7 +974,7 @@ impl<'gc, 'a> LuaRegisters<'gc, 'a> { } } - pub(super) fn close_upvalues(&mut self, mc: &Mutation<'gc>, bottom_register: RegisterIndex) { + pub fn close_upvalues(&mut self, mc: &Mutation<'gc>, bottom_register: RegisterIndex) { let bottom = self.base + bottom_register.0 as usize; let start = match self .open_upvalues diff --git a/src/thread/vm.rs b/src/thread/vm.rs index 51e42b6f..94739933 100644 --- a/src/thread/vm.rs +++ b/src/thread/vm.rs @@ -16,7 +16,7 @@ use super::{thread::LuaFrame, VMError}; // changed. // // Returns the number of instructions that were run. -pub(super) fn run_vm<'gc>( +pub fn run_vm<'gc>( ctx: Context<'gc>, mut lua_frame: LuaFrame<'gc, '_>, max_instructions: u32, From 4badb173e9153b36522ef9c57e950e89f9828def Mon Sep 17 00:00:00 2001 From: reloginn Date: Sat, 23 Aug 2025 02:57:31 +0600 Subject: [PATCH 2/7] `feature`: implemented multi-session and multi-thread on debugger level (refactor needed!) --- debugger/examples/simple_debug.rs | 70 +- debugger/src/adapter.rs | 5 +- debugger/src/breakpoints.rs | 8 +- debugger/src/lib.rs | 1498 ++++++++++++++++++++--------- debugger/src/watch/entry.rs | 7 +- debugger/src/watch/spec.rs | 4 +- 6 files changed, 1060 insertions(+), 532 deletions(-) diff --git a/debugger/examples/simple_debug.rs b/debugger/examples/simple_debug.rs index 42d74844..1443d35c 100644 --- a/debugger/examples/simple_debug.rs +++ b/debugger/examples/simple_debug.rs @@ -1,11 +1,8 @@ -use piccolo::{Closure, Executor, Fuel, Lua, Table, Value}; +use piccolo::{Value}; use piccolo_debugger::Debugger; fn main() { - let mut lua = Lua::core(); - - lua.enter(|ctx| { - let source = br#" + let source = r#" x = 0 local outer = 10 local function foo(n) @@ -22,41 +19,37 @@ fn main() { local z = bar() "#; + let path = "test.lua"; - let chunk = Closure::load(ctx, Some("test.lua"), source).unwrap(); - - let t = Table::new(&ctx); - ctx.globals().set_field(ctx, "t", t); + let mut dbg = Debugger::new(); + let session_id = dbg.add_session(source, path); - let mut exec = Executor::start(ctx, chunk.into(), ()); + dbg.launch(session_id); - let mut dbg = Debugger::new(exec); + dbg.add_breakpoint(session_id, "test.lua".to_string(), 3); + dbg.add_breakpoint(session_id, "test.lua".to_string(), 9); + dbg.add_breakpoint(session_id, "test.lua".to_string(), 13); - dbg.add_breakpoint("test.lua", 3); - dbg.add_breakpoint("test.lua", 9); - dbg.add_breakpoint("test.lua", 13); - - let _ = dbg.add_function_breakpoint(ctx, "foo"); + dbg.add_function_breakpoint(session_id, "foo".to_string()); println!("Breakpoints:"); - for (src, lines) in dbg.list_breakpoints() { + for (src, lines) in dbg.list_breakpoints(session_id).unwrap() { let mut v: Vec = lines.iter().copied().collect(); v.sort_unstable(); println!(" {src}: {:?}", v); } - dbg.remove_breakpoint("test.lua", 9); + dbg.remove_breakpoint(session_id, "test.lua".to_string(), 9); - dbg.watch_global("x"); - dbg.watch_table_key(t, Value::String(ctx.intern(b"k"))); + dbg.watch_global(session_id, "x".to_string(), None); println!("Continue…"); - let mut stop = dbg.continue_run(ctx); + let mut stop = dbg.continue_run(session_id); println!("Stopped: {:?}", stop); - let bt = dbg.backtrace(ctx); + let bt = dbg.backtrace(session_id); println!("Backtrace:"); - for (i, loc) in bt.iter().enumerate() { + for (i, loc) in bt.unwrap().iter().enumerate() { println!( " #{i} {}:{} {}", loc.chunk(), @@ -65,26 +58,26 @@ fn main() { ); } - if let Some(dis) = dbg.disassemble("", None, None) { + if let Some(dis) = dbg.disassemble(session_id, "".to_string(), None, None).unwrap() { println!("Disassembly (current function):"); for l in dis.iter().take(10) { println!(" {l:?}"); } } - if let Some(readed) = dbg.read_memory("", None, None) { + if let Some(readed) = dbg.read_memory(session_id, "".to_string(), None, None).unwrap() { println!("Readed: {:?}", readed); } - if let Some(regs) = dbg.read_registers(ctx) { + if let Some(regs) = dbg.read_registers(session_id).unwrap() { println!("Top frame registers ({}):", regs.len()); for (i, v) in regs.iter().enumerate() { - println!(" r{i} = {}", v.display()); + println!(" r{i} = {}", v); } } println!("Stack snapshot (before stepping):"); - for (loc, regs) in dbg.stack_snapshot(ctx) { + for (loc, regs) in dbg.stack_snapshot(session_id).unwrap() { println!( " {}:{} {} ({} regs)", loc.chunk(), @@ -95,34 +88,29 @@ fn main() { } println!("Step into…"); - stop = dbg.step_into(ctx); + stop = dbg.step_into(session_id); println!("Stopped: {:?}", stop); - if let Some(ups) = dbg.read_upvalues(ctx) { + if let Some(ups) = dbg.read_upvalues(session_id).unwrap() { println!("Upvalues ({}):", ups.len()); for (i, v) in ups.iter().enumerate() { - println!(" uv{i} = {}", v.display()); + println!(" uv{i} = {}", v); } } println!("Step over…"); - stop = dbg.step_over(ctx); + stop = dbg.step_over(session_id); println!("Stopped: {:?}", stop); println!("Step out…"); - stop = dbg.step_out(ctx); + stop = dbg.step_out(session_id); println!("Stopped: {:?}", stop); println!("Continue again…"); - stop = dbg.continue_run(ctx); + stop = dbg.continue_run(session_id); println!("Stopped: {:?}", stop); - if let Some(v0) = dbg.read_register(ctx, 0) { - println!("r0 = {}", v0.display()); + if let Some(v0) = dbg.read_register(session_id, 0).unwrap() { + println!("r0 = {}", v0); } - - exec = dbg.executor(); - let mut fuel = Fuel::with(10_000); - let _ = exec.step(ctx, &mut fuel); - }); } diff --git a/debugger/src/adapter.rs b/debugger/src/adapter.rs index 49a7108e..c719a58f 100644 --- a/debugger/src/adapter.rs +++ b/debugger/src/adapter.rs @@ -1,5 +1,3 @@ -use std::mem::ManuallyDrop; - use base64::Engine; use piccolo::{Context, Executor}; @@ -462,12 +460,11 @@ impl<'gc> Adapter<'gc> { pub fn stack_trace( &mut self, arguments: StackTraceArguments, - ctx: piccolo::Context<'gc>, ) -> StackTraceResponse { let mut stack_frames = Vec::new(); if let Some(ref debugger) = self.debugger { - let backtrace = debugger.backtrace(ctx); + let backtrace = debugger.backtrace(); let start_frame = arguments.start_frame.unwrap_or(0) as usize; let levels = arguments.levels.map(|l| l as usize); diff --git a/debugger/src/breakpoints.rs b/debugger/src/breakpoints.rs index f10ac4c1..a1760602 100644 --- a/debugger/src/breakpoints.rs +++ b/debugger/src/breakpoints.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashMap}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Breakpoint { @@ -31,7 +31,7 @@ impl Breakpoints { .retain(|breakpoint| !(breakpoint.source == chunk && breakpoint.line == line)) } - pub fn list(&self) -> impl Iterator)> { + pub fn list(&self) -> HashMap> { let sources: BTreeSet<&str> = self .breakpoints .iter() @@ -39,14 +39,14 @@ impl Breakpoints { .collect(); sources.into_iter().map(|source| { ( - source, + source.to_owned(), self.breakpoints .iter() .filter(|breakpoint| breakpoint.source == source) .map(|breakpoint| breakpoint.line) .collect(), ) - }) + }).collect() } pub fn matches(&self, chunk: &str, line: usize) -> Vec { diff --git a/debugger/src/lib.rs b/debugger/src/lib.rs index d89e7a33..e717941f 100644 --- a/debugger/src/lib.rs +++ b/debugger/src/lib.rs @@ -1,10 +1,7 @@ use piccolo::{ - compiler::LineNumber, - opcode::{OpCode, Operation}, - thread::{vm::run_vm, Executor, Frame, LuaFrame}, - Context, Fuel, FunctionPrototype, String as LuaString, Value, + compiler::{FunctionRef, LineNumber}, opcode::{OpCode, Operation}, thread::{vm::run_vm, Executor, Frame, LuaFrame}, Closure, Context, Fuel, FunctionPrototype, Lua, String as LuaString, Value }; -use std::mem::size_of; +use std::{collections::HashMap, mem::size_of, sync::{atomic::AtomicPtr, mpsc::{Receiver, Sender}, Arc, RwLock}, thread::JoinHandle}; use watch::{WatchEntry, WatchSpec}; #[allow(dead_code)] @@ -14,6 +11,9 @@ struct PrototypeReference { prototype_index: usize, } +#[derive(Debug)] +pub struct Dead; + pub use self::{ breakpoints::{Breakpoint, Breakpoints}, location::Location, @@ -21,12 +21,13 @@ pub use self::{ watch::WatchMode, }; -pub mod adapter; +// pub mod adapter; pub mod breakpoints; pub mod location; pub mod stop_reason; mod watch; + #[derive(Debug)] pub struct Disassembled { symbol: &'static str, @@ -35,111 +36,393 @@ pub struct Disassembled { operation: Operation, } -pub struct Debugger<'gc> { - executor: Executor<'gc>, - breakpoints: Breakpoints, - watchpoints: Vec>, +#[derive(Debug)] +pub enum RequestMessage { + AddBreakpoint { + chunk: String, + line: usize + }, + AddFunctionBreakpoint(String), + RemoveBreakpoint { + chunk: String, + line: usize + }, + ListBreakpoints, + MatchesBreakpoint { + chunk: String, + line: usize + }, + WatchRegister { + register: usize, + mode: Option + }, + WatchGlobal { + name: String, + mode: Option + }, + ContinueRun, + StepInto, + StepOver, + StepOut, + Backtrace, + StackSnapshot, + ReadRegister(usize), + ReadUpvalues, + ReadRegisters, + ReadGlobal(String), + ReadMemory(String, Option, Option), + Disassemble(String, Option, Option), } -impl<'gc> Debugger<'gc> { - pub fn new(executor: Executor<'gc>) -> Self { - Self { - executor, - breakpoints: Breakpoints::default(), - watchpoints: Vec::new(), +#[derive(Debug)] +pub enum ResponseMessage { + BreakpointAdded(usize), + BreakpointRemoved, + FunctionBreakpointAdded(Option), + ListBreakpoints(HashMap>), + MatchesBreakpoint(Vec), + WatchRegisterAdded, + WatchGlobalAdded, + ContinueRun(StopReason), + StepInto(StopReason), + StepOver(StopReason), + StepOut(StopReason), + Backtrace(Result, StopReason>), + StackSnapshot(Result)>, StopReason>), + ReadRegister(Result, StopReason>), + ReadUpvalues(Result>, StopReason>), + ReadRegisters(Result>, StopReason>), + ReadGlobal(String), + ReadMemory(Option>), + Disassemble(Option>), +} + +pub struct Session { + id: usize, + source: String, + path: String, + thread: Option>, + sender: Option>, + receiver: Option> +} + +fn current_location_from_state_in_thread<'gc>( + thread_state: &piccolo::thread::ThreadState<'gc>, +) -> Option { + match thread_state.frames.last() { + Some(Frame::Lua { closure, pc, .. }) => { + let proto = closure.prototype(); + let chunk_name = lua_string_to_string(proto.chunk_name); + let line = opcode_index_to_line(&proto.opcode_line_numbers, *pc); + Some(Location::new( + chunk_name, + proto.reference.map_strings(lua_string_to_string), + line, + )) } + _ => None, } +} - pub fn executor(&self) -> Executor<'gc> { - self.executor +fn opcode_index_to_line(op_lines: &[(usize, LineNumber)], pc: usize) -> usize { + if op_lines.is_empty() { + return 0; } - - pub fn add_breakpoint(&mut self, chunk: &str, line: usize) -> usize { - self.breakpoints.add(chunk, line) + match op_lines.binary_search_by_key(&pc, |(opi, _)| *opi) { + Ok(i) => op_lines[i].1 .0 as usize, + Err(0) => op_lines[0].1 .0 as usize, + Err(i) => op_lines[i - 1].1 .0 as usize, } +} - pub fn remove_breakpoint(&mut self, chunk: &str, line: usize) { - self.breakpoints.remove(chunk, line) - } +fn value_to_string<'gc>(value: Value<'gc>) -> String { + format!("{}", value.display()) +} - pub fn list_breakpoints(&self) -> impl Iterator)> { - self.breakpoints.list() - } +fn lua_string_to_string(s: LuaString<'_>) -> String { + format!("{}", s.display_lossy()) +} - pub fn matches_breakpoint(&self, chunk: &str, line: usize) -> Vec { - self.breakpoints.matches(chunk, line) - } +fn current_stack_depth<'gc>(executor: Executor<'gc>) -> Result { + with_top_thread_state(executor, |state| state.frames.len()) +} + +fn stack_depth_and_location<'gc>(executor: Executor<'gc>) -> Result<(usize, Option), StopReason> { + with_top_thread_state(executor, |state| { + let depth = state.frames.len(); + let loc = current_location_from_state_in_thread(state); + (depth, loc) + }) +} + +fn current_location<'gc>(executor: Executor<'gc>) -> Result, StopReason> { + with_top_thread_state(executor, |state| current_location_from_state_in_thread(state)) +} + +fn with_top_thread_state<'gc, R>( + executor: Executor<'gc>, + f: impl FnOnce(&piccolo::thread::ThreadState<'gc>) -> R, +) -> Result { + let guard = executor.0.borrow(); + let thread = *guard.thread_stack.last().ok_or(StopReason::Finished)?; + drop(guard); + let thread_state = thread.into_inner().borrow(); + Ok(f(&thread_state)) +} + +fn with_top_thread_state_mut<'gc, R>( + executor: Executor<'gc>, + ctx: Context<'gc>, + f: impl FnOnce(piccolo::Thread<'gc>, &mut piccolo::thread::ThreadState<'gc>) -> R, +) -> Result { + let guard = executor.0.borrow(); + let top_thread = *guard.thread_stack.last().ok_or(StopReason::Finished)?; + drop(guard); + let mut thread_state = top_thread.into_inner().borrow_mut(&ctx); + let r = f(top_thread, &mut thread_state); + drop(thread_state); + Ok(r) +} - pub fn add_function_breakpoint(&mut self, ctx: Context<'gc>, name: &str) -> Option { - self.resolve_function_breakpoint(ctx, name) - .map(|(chunk, line)| self.add_breakpoint(&chunk, line)) +fn stop_if_breakpoint(location: &Location, breakpoints: &Breakpoints) -> Option { + let breakpoint_ids = breakpoints.matches(location.chunk(), location.line()); + if breakpoint_ids.is_empty() { + None + } else { + Some(StopReason::Breakpoint { + location: location.to_owned(), + breakpoint_ids, + }) } +} - pub fn resolve_function_breakpoint( - &self, - ctx: Context<'gc>, - name: &str, - ) -> Option<(String, usize)> { - let env = ctx.globals(); - let mut segments = name.split('.'); - let first = segments.next()?; - let mut current = env.get_value(ctx, ctx.intern(first.as_bytes())); - for segment in segments { - match current { - Value::Table(t) => { - current = t.get_raw(Value::String(ctx.intern(segment.as_bytes()))); +fn eval_watch<'gc>(spec: &WatchSpec, executor: Executor<'gc>, ctx: Context<'gc>) -> Option { + match spec { + WatchSpec::Register(index) => { + with_top_thread_state(executor, |state| { + if let Some(Frame::Lua { + base, stack_size, .. + }) = state.frames.last() + { + let index = *index; + let base = *base; + let size = *stack_size; + if index < size { + return Some(state.stack[base + index].display().to_string()); + } } - _ => return None, - } + None + }).ok()? } + WatchSpec::Global(name) => { + let key = ctx.intern(name.as_bytes()); + Some(ctx.globals().get_value(ctx, key).display().to_string()) + } + } +} + +fn resolve_function_breakpoint<'gc>( + ctx: Context<'gc>, + name: &str, +) -> Option<(String, usize)> { + let env = ctx.globals(); + let mut segments = name.split('.'); + let first = segments.next()?; + let mut current = env.get_value(ctx, ctx.intern(first.as_bytes())); + for segment in segments { match current { - Value::Function(piccolo::Function::Closure(closure)) => { - let proto = closure.prototype(); - let chunk_name = lua_string_to_string(proto.chunk_name); - let first_line = if proto.opcode_line_numbers.is_empty() { - 0 - } else { - proto.opcode_line_numbers[0].1 .0 as usize - }; - Some((chunk_name, first_line)) + Value::Table(t) => { + current = t.get_raw(Value::String(ctx.intern(segment.as_bytes()))); } - _ => None, + _ => return None, } } + match current { + Value::Function(piccolo::Function::Closure(closure)) => { + let proto = closure.prototype(); + let chunk_name = lua_string_to_string(proto.chunk_name); + let first_line = if proto.opcode_line_numbers.is_empty() { + 0 + } else { + proto.opcode_line_numbers[0].1 .0 as usize + }; + Some((chunk_name, first_line)) + } + _ => None, + } +} - pub fn continue_run(&mut self, ctx: Context<'gc>) -> StopReason { - loop { - match self.current_location(ctx) { - Ok(Some(location)) => { - if let Some(stop_reason) = self.stop_if_breakpoint(&location) { - return stop_reason; +fn describe_watch_change<'gc>( + spec: &WatchSpec, + old: &Option, + new: &Option, +) -> String { + let spec = match spec { + WatchSpec::Register(index) => format!("register[{}]", index), + WatchSpec::Global(name) => format!("global['{}']", name), + }; + format!( + "{}: {} -> {}", + spec, + old.as_ref().unwrap_or(&"".to_string()), + new.as_ref().unwrap_or(&"".to_string()) + ) +} + +fn poll_watchpoints<'gc>(watchpoints: &mut Vec, executor: Executor<'gc>, ctx: Context<'gc>) -> Option { + for index in 0..watchpoints.len() { + let Some(entry) = watchpoints.get(index) else { + continue; + }; + let entry = entry.to_owned(); + let spec = match &entry.spec { + WatchSpec::Register(i) => WatchSpec::Register(*i), + WatchSpec::Global(n) => WatchSpec::Global(n.clone()), + }; + let last = entry.last; + + let current = eval_watch(&spec, executor, ctx); + let should_fire = match entry.mode { + WatchMode::Modify => match (last.as_ref(), current.as_ref()) { + (None, Some(_)) => true, + (Some(_), None) => true, + (Some(left), Some(right)) => left != right, + (None, None) => false, + }, + WatchMode::Access => { + if current.is_some() { + let current_line = current_location(executor) + .ok() + .flatten() + .map(|l| (l.chunk().to_owned(), l.line())); + if let (Some((chunk, line)), Some(entry)) = + (current_line, watchpoints.get(index)) + { + if entry.last_line_seen != Some((chunk.to_owned(), line)) { + if let Some(entry) = watchpoints.get_mut(index) { + entry.last_line_seen = Some((chunk.to_owned(), line)); + } + true + } else { + false + } + } else { + false } + } else { + false } - Ok(None) => {} - Err(stop_reason) => return stop_reason, } - match self.step_one(ctx) { - StopReason::Step(_) => continue, - other => return other, + }; + if should_fire { + let message = format!( + "watchpoint hit: {}", + describe_watch_change(&spec, &last, ¤t) + ); + if let Some(entry) = watchpoints.get_mut(index) { + entry.last = current; + } + return Some(message); + } + if let Some(entry) = watchpoints.get_mut(index) { + entry.last = current; + } + } + None +} + +pub fn step_one<'gc>(executor: Executor<'gc>, ctx: Context<'gc>, watchpoints: &mut Vec) -> StopReason { + const FUEL_NON_LUA_CALLBACK_STEP: i32 = 11; + const FUEL_NON_LUA_SEQUENCE_STEP: i32 = 7; + const FUEL_NON_LUA_ERROR_STEP: i32 = 3; + + match executor.mode() { + piccolo::thread::ExecutorMode::Stopped => return StopReason::Finished, + piccolo::thread::ExecutorMode::Result => return StopReason::Finished, + piccolo::thread::ExecutorMode::Suspended => return StopReason::Suspended, + piccolo::thread::ExecutorMode::Running => return StopReason::Error, + piccolo::thread::ExecutorMode::Normal => {} + } + + let (is_lua_top, non_lua_fuel) = + match with_top_thread_state(executor, |state| match state.frames.last() { + Some(Frame::Lua { .. }) => (true, 0), + Some(Frame::Callback { .. }) => (false, FUEL_NON_LUA_CALLBACK_STEP), + Some(Frame::Sequence { .. }) => (false, FUEL_NON_LUA_SEQUENCE_STEP), + Some(Frame::Error(_)) => (false, FUEL_NON_LUA_ERROR_STEP), + Some(Frame::Result { .. }) => (false, -1), + Some(Frame::WaitThread) | Some(Frame::Yielded) | Some(Frame::Start(_)) => { + (false, 3) + } + None => (false, -1), + }) { + Ok(value) => value, + Err(stop_reason) => return stop_reason, + }; + + if non_lua_fuel == -1 { + return StopReason::Finished; + } + + if is_lua_top { + let _ = with_top_thread_state_mut(executor, ctx, |thread, state| { + let mut fuel = Fuel::with(1_000_000); + let lua_frame = LuaFrame { + state, + thread, + fuel: &mut fuel, + }; + let _ = run_vm(ctx, lua_frame, 1); + }); + } else { + let mut fuel = Fuel::with(non_lua_fuel); + let _ = executor.step(ctx, &mut fuel); + } + + let location = match current_location(executor) { + Ok(Some(location)) => location, + Ok(None) => return StopReason::Finished, + Err(stop_reason) => return stop_reason, + }; + if let Some(hit) = poll_watchpoints(watchpoints, executor, ctx) { + return StopReason::Watchpoint(hit); + } + StopReason::Step(location) +} + +pub fn continue_run<'gc>(executor: Executor<'gc>, ctx: Context<'gc>, breakpoints: &Breakpoints, watchpoints: &mut Vec) -> StopReason { + loop { + match current_location(executor) { + Ok(Some(location)) => { + if let Some(stop_reason) = stop_if_breakpoint(&location, &breakpoints) { + return stop_reason; + } } + Ok(None) => {} + Err(stop_reason) => return stop_reason, + } + match step_one(executor, ctx, watchpoints) { + StopReason::Step(_) => continue, + other => return other, } } +} - pub fn step_into(&mut self, ctx: Context<'gc>) -> StopReason { - let start_location = match self.current_location(ctx) { +pub fn step_into<'gc>(executor: Executor<'gc>, ctx: Context<'gc>, breakpoints: &Breakpoints, watchpoints: &mut Vec) -> StopReason { + let start_location = match current_location(executor) { Ok(Some(location)) => location, Ok(None) => return StopReason::Finished, Err(stop_reason) => return stop_reason, }; - if let Some(stop_reason) = self.stop_if_breakpoint(&start_location) { + if let Some(stop_reason) = stop_if_breakpoint(&start_location, breakpoints) { return stop_reason; } loop { - match self.step_one(ctx) { + match step_one(executor, ctx, watchpoints) { StopReason::Step(location) => { - if let Some(stop_reason) = self.stop_if_breakpoint(&location) { + if let Some(stop_reason) = stop_if_breakpoint(&location, breakpoints) { return stop_reason; } if location.line() != start_location.line() @@ -154,24 +437,24 @@ impl<'gc> Debugger<'gc> { } } - pub fn step_over(&mut self, ctx: Context<'gc>) -> StopReason { - let (start_depth, start_location) = match self.stack_depth_and_location(ctx) { + pub fn step_over<'gc>(executor: Executor<'gc>, ctx: Context<'gc>, breakpoints: &Breakpoints, watchpoints: &mut Vec) -> StopReason { + let (start_depth, start_location) = match stack_depth_and_location(executor) { Ok((depth, Some(location))) => (depth, location), Ok((_depth, None)) => return StopReason::Finished, Err(stop_reason) => return stop_reason, }; - if let Some(stop_reason) = self.stop_if_breakpoint(&start_location) { + if let Some(stop_reason) = stop_if_breakpoint(&start_location, breakpoints) { return stop_reason; } loop { - match self.step_one(ctx) { + match step_one(executor, ctx, watchpoints) { StopReason::Step(location) => { - if let Some(stop_reason) = self.stop_if_breakpoint(&location) { + if let Some(stop_reason) = stop_if_breakpoint(&location, breakpoints) { return stop_reason; } - let depth = match self.current_stack_depth(ctx) { + let depth = match current_stack_depth(executor) { Ok(depth) => depth, Err(stop_reason) => return stop_reason, }; @@ -188,19 +471,19 @@ impl<'gc> Debugger<'gc> { } } - pub fn step_out(&mut self, ctx: Context<'gc>) -> StopReason { - let start_depth = match self.current_stack_depth(ctx) { + pub fn step_out<'gc>(executor: Executor<'gc>, ctx: Context<'gc>, breakpoints: &Breakpoints, watchpoints: &mut Vec) -> StopReason { + let start_depth = match current_stack_depth(executor) { Ok(depth) => depth, Err(stop_reason) => return stop_reason, }; loop { - match self.step_one(ctx) { + match step_one(executor, ctx, watchpoints) { StopReason::Step(location) => { - if let Some(stop_reason) = self.stop_if_breakpoint(&location) { + if let Some(stop_reason) = stop_if_breakpoint(&location, breakpoints) { return stop_reason; } - let depth = match self.current_stack_depth(ctx) { + let depth = match current_stack_depth(executor) { Ok(depth) => depth, Err(stop_reason) => return stop_reason, }; @@ -214,10 +497,9 @@ impl<'gc> Debugger<'gc> { } } - pub fn backtrace(&self, _ctx: Context<'gc>) -> Vec { - let mut out = Vec::new(); - let _ = self.with_top_thread_state(|state| { - out.reserve(state.frames.len()); + pub fn backtrace<'gc>(executor: Executor<'gc>) -> Result, StopReason> { + with_top_thread_state(executor, |state| { + let mut out = Vec::with_capacity(state.frames.len()); for frame in state.frames.iter() { if let Frame::Lua { closure, pc, .. } = frame { let proto = closure.prototype(); @@ -227,13 +509,12 @@ impl<'gc> Debugger<'gc> { out.push(Location::new(chunk_name, function_ref, line)); } } - }); - out + out + }) } - pub fn read_register(&self, _ctx: Context<'gc>, register_index: usize) -> Option> { - let mut value = None; - let _ = self.with_top_thread_state(|state| { + pub fn read_register<'gc>(executor: Executor<'gc>, register_index: usize) -> Result, StopReason> { + with_top_thread_state(executor, |state| { if let Some(Frame::Lua { base, stack_size, .. }) = state.frames.last() @@ -241,158 +522,52 @@ impl<'gc> Debugger<'gc> { let base = *base; let size = *stack_size; if register_index < size { - value = Some(state.stack[base + register_index]); + return Some(value_to_string(state.stack[base + register_index])); } } - }); - value - } - - pub fn read_global(&self, ctx: Context<'gc>, name: &str) -> Value<'gc> { - let key = ctx.intern(name.as_bytes()); - ctx.globals().get_value(ctx, key) - } - - pub fn step_one(&mut self, ctx: Context<'gc>) -> StopReason { - const FUEL_NON_LUA_CALLBACK_STEP: i32 = 11; - const FUEL_NON_LUA_SEQUENCE_STEP: i32 = 7; - const FUEL_NON_LUA_ERROR_STEP: i32 = 3; - - match self.executor.mode() { - piccolo::thread::ExecutorMode::Stopped => return StopReason::Finished, - piccolo::thread::ExecutorMode::Result => return StopReason::Finished, - piccolo::thread::ExecutorMode::Suspended => return StopReason::Suspended, - piccolo::thread::ExecutorMode::Running => return StopReason::Error, - piccolo::thread::ExecutorMode::Normal => {} - } - - let (is_lua_top, non_lua_fuel) = - match self.with_top_thread_state(|state| match state.frames.last() { - Some(Frame::Lua { .. }) => (true, 0), - Some(Frame::Callback { .. }) => (false, FUEL_NON_LUA_CALLBACK_STEP), - Some(Frame::Sequence { .. }) => (false, FUEL_NON_LUA_SEQUENCE_STEP), - Some(Frame::Error(_)) => (false, FUEL_NON_LUA_ERROR_STEP), - Some(Frame::Result { .. }) => (false, -1), - Some(Frame::WaitThread) | Some(Frame::Yielded) | Some(Frame::Start(_)) => { - (false, 3) - } - None => (false, -1), - }) { - Ok(value) => value, - Err(stop_reason) => return stop_reason, - }; - - if non_lua_fuel == -1 { - return StopReason::Finished; - } - - if is_lua_top { - let _ = self.with_top_thread_state_mut(ctx, |thread, state| { - let mut fuel = Fuel::with(1_000_000); - let lua_frame = LuaFrame { - state, - thread, - fuel: &mut fuel, - }; - let _ = run_vm(ctx, lua_frame, 1); - }); - } else { - let mut fuel = Fuel::with(non_lua_fuel); - let _ = self.executor.step(ctx, &mut fuel); - } - - let location = match self.current_location(ctx) { - Ok(Some(location)) => location, - Ok(None) => return StopReason::Finished, - Err(stop_reason) => return stop_reason, - }; - if let Some(hit) = self.poll_watchpoints(ctx) { - return StopReason::Watchpoint(hit); - } - StopReason::Step(location) - } - - fn current_stack_depth(&self, _ctx: Context<'gc>) -> Result { - self.with_top_thread_state(|state| state.frames.len()) - } - - fn stack_depth_and_location( - &self, - _ctx: Context<'gc>, - ) -> Result<(usize, Option), StopReason> { - self.with_top_thread_state(|state| { - let depth = state.frames.len(); - let loc = current_location_from_state_in_thread(state); - (depth, loc) + None }) } - fn current_location(&self, _ctx: Context<'gc>) -> Result, StopReason> { - self.with_top_thread_state(|state| current_location_from_state_in_thread(state)) - } - - fn with_top_thread_state( - &self, - f: impl FnOnce(&piccolo::thread::ThreadState<'gc>) -> R, - ) -> Result { - let guard = self.executor.0.borrow(); - let thread = *guard.thread_stack.last().ok_or(StopReason::Finished)?; - drop(guard); - let thread_state = thread.into_inner().borrow(); - Ok(f(&thread_state)) - } - - fn with_top_thread_state_mut( - &self, - ctx: Context<'gc>, - f: impl FnOnce(piccolo::Thread<'gc>, &mut piccolo::thread::ThreadState<'gc>) -> R, - ) -> Result { - let guard = self.executor.0.borrow(); - let top_thread = *guard.thread_stack.last().ok_or(StopReason::Finished)?; - drop(guard); - let mut thread_state = top_thread.into_inner().borrow_mut(&ctx); - let r = f(top_thread, &mut thread_state); - drop(thread_state); - Ok(r) + pub fn read_global<'gc>(ctx: Context<'gc>, name: &str) -> String { + let key = ctx.intern(name.as_bytes()); + value_to_string(ctx.globals().get_value(ctx, key)) } - pub fn read_registers(&self, _ctx: Context<'gc>) -> Option>> { - let mut out = None; - let _ = self.with_top_thread_state(|state| { + pub fn read_registers<'gc>(executor: Executor<'gc>) -> Result>, StopReason> { + with_top_thread_state(executor, |state| { if let Some(Frame::Lua { base, stack_size, .. }) = state.frames.last() { let base = *base; let size = *stack_size; - out = Some(state.stack[base..base + size].to_vec()); + return Some(state.stack[base..base + size].iter().map(|v| value_to_string(*v)).collect()); } - }); - out + None + }) } - pub fn read_upvalues(&self, _ctx: Context<'gc>) -> Option>> { - let mut out = None; - let _ = self.with_top_thread_state(|state| { + pub fn read_upvalues<'gc>(executor: Executor<'gc>) -> Result>, StopReason> { + with_top_thread_state(executor, |state| { if let Some(Frame::Lua { closure, .. }) = state.frames.last() { let mut values = Vec::new(); for &upvalue in closure.upvalues() { if let piccolo::closure::UpValueState::Closed(value) = upvalue.get() { - values.push(value); + values.push(value_to_string(value)); } else { - values.push(Value::Nil); + values.push(String::new()); } } - out = Some(values); + return Some(values); } - }); - out + None + }) } - pub fn stack_snapshot(&self, _ctx: Context<'gc>) -> Vec<(Location, Vec>)> { - let mut out = Vec::new(); - let _ = self.with_top_thread_state(|state| { - out.reserve(state.frames.len()); + pub fn stack_snapshot<'gc>(executor: Executor<'gc>) -> Result)>, StopReason> { + with_top_thread_state(executor, |state| { + let mut out = Vec::with_capacity(state.frames.len()); for frame in state.frames.iter() { if let Frame::Lua { base, @@ -409,42 +584,20 @@ impl<'gc> Debugger<'gc> { let location = Location::new(chunk_name, function_ref, line); let base = *base; let size = *stack_size; - out.push((location, state.stack[base..base + size].to_vec())); + out.push((location, state.stack[base..base + size].iter().map(|v| value_to_string(*v)).collect())); } } - }); - out - } - - pub fn disassemble( - &self, - memory_reference: &str, - instruction_offset: Option, - instruction_count: Option, - ) -> Option> { - let (ty, s) = memory_reference.split_once(':').unwrap_or(("frame", "")); - match ty { - "frame" => self.disassemble_frame( - if s.is_empty() { - usize::MAX - } else { - s.parse().ok()? - }, - instruction_offset, - instruction_count, - ), - "prototype" => self.disassemble_prototype(s, instruction_offset, instruction_count), - _ => None, - } + out + }) } - fn disassemble_frame( - &self, + fn disassemble_frame<'gc>( + executor: Executor<'gc>, frame: usize, instruction_offset: Option, instruction_count: Option, ) -> Option> { - self.with_top_thread_state(|state| { + with_top_thread_state(executor, |state| { if let Some(Frame::Lua { closure, pc, .. }) = if frame == usize::MAX { state.frames.last() } else { @@ -452,7 +605,7 @@ impl<'gc> Debugger<'gc> { } { let prototype = closure.prototype(); let current_pc = *pc; - Some(self.disassemble_prototype_opcodes( + Some(disassemble_prototype_opcodes( &prototype, Some(current_pc), instruction_offset, @@ -465,8 +618,8 @@ impl<'gc> Debugger<'gc> { .ok()? } - fn disassemble_prototype( - &self, + fn disassemble_prototype<'gc>( + executor: Executor<'gc>, prototype: &str, instruction_offset: Option, instruction_count: Option, @@ -475,7 +628,7 @@ impl<'gc> Debugger<'gc> { let index: usize = index.parse().ok()?; - self.with_top_thread_state(|state| { + with_top_thread_state(executor, |state| { for frame in state.frames.iter() { if let Frame::Lua { closure, .. } = frame { let proto = closure.prototype(); @@ -483,7 +636,7 @@ impl<'gc> Debugger<'gc> { if frame_chunk == chunk { if let Some(nested_proto) = proto.prototypes.get(index) { - return Some(self.disassemble_prototype_opcodes( + return Some(disassemble_prototype_opcodes( nested_proto, None, instruction_offset, @@ -498,8 +651,7 @@ impl<'gc> Debugger<'gc> { .ok()? } - fn disassemble_prototype_opcodes( - &self, + fn disassemble_prototype_opcodes<'gc>( prototype: &FunctionPrototype, current_pc: Option, instruction_offset: Option, @@ -533,8 +685,31 @@ impl<'gc> Debugger<'gc> { disassembled } - pub fn read_memory( - &self, + pub fn disassemble<'gc>( + executor: Executor<'gc>, + memory_reference: &str, + instruction_offset: Option, + instruction_count: Option, + ) -> Option> { + let (ty, s) = memory_reference.split_once(':').unwrap_or(("frame", "")); + match ty { + "frame" => disassemble_frame( + executor, + if s.is_empty() { + usize::MAX + } else { + s.parse().ok()? + }, + instruction_offset, + instruction_count, + ), + "prototype" => disassemble_prototype(executor, s, instruction_offset, instruction_count), + _ => None, + } + } + + pub fn read_memory<'gc>( + executor: Executor<'gc>, memory_reference: &str, byte_offset: Option, byte_count: Option, @@ -542,7 +717,8 @@ impl<'gc> Debugger<'gc> { let (ty, s) = memory_reference.split_once(':').unwrap_or(("frame", "")); match ty { - "frame" => self.read_frame_memory( + "frame" => read_frame_memory( + executor, if s.is_empty() { usize::MAX } else { @@ -551,25 +727,25 @@ impl<'gc> Debugger<'gc> { byte_offset, byte_count, ), - "prototype" => self.read_prototype_memory(s, byte_offset, byte_count), + "prototype" => read_prototype_memory(executor, s, byte_offset, byte_count), _ => None, } } - fn read_frame_memory( - &self, + fn read_frame_memory<'gc>( + executor: Executor<'gc>, frame: usize, byte_offset: Option, byte_count: Option, ) -> Option> { - self.with_top_thread_state(|state| { + with_top_thread_state(executor, |state| { if let Some(piccolo::thread::Frame::Lua { closure, .. }) = if frame == usize::MAX { state.frames.last() } else { state.frames.get(frame) } { let prototype = closure.prototype(); - Some(self.read_opcodes(&prototype.opcodes, byte_offset, byte_count)) + Some(read_opcodes(&prototype.opcodes, byte_offset, byte_count)) } else { None } @@ -577,8 +753,8 @@ impl<'gc> Debugger<'gc> { .ok()? } - fn read_prototype_memory( - &self, + fn read_prototype_memory<'gc>( + executor: Executor<'gc>, prototype: &str, byte_offset: Option, byte_count: Option, @@ -587,7 +763,7 @@ impl<'gc> Debugger<'gc> { let index: usize = index.parse().ok()?; - self.with_top_thread_state(|state| { + with_top_thread_state(executor, |state| { for frame in state.frames.iter() { if let Frame::Lua { closure, .. } = frame { let proto = closure.prototype(); @@ -596,7 +772,7 @@ impl<'gc> Debugger<'gc> { if frame_chunk == chunk { if let Some(nested_proto) = proto.prototypes.get(index) { return Some( - (self.read_opcodes(&nested_proto.opcodes, byte_offset, byte_count)), + read_opcodes(&nested_proto.opcodes, byte_offset, byte_count), ); } } @@ -607,8 +783,7 @@ impl<'gc> Debugger<'gc> { .ok()? } - fn read_opcodes( - &self, + fn read_opcodes<'gc>( opcodes: &[OpCode], byte_offset: Option, byte_count: Option, @@ -646,246 +821,617 @@ impl<'gc> Debugger<'gc> { raw[start..end].to_vec() } - pub fn watch_register(&mut self, register: usize) { - self.watchpoints.push(WatchEntry { - spec: WatchSpec::Register(register), - last: None, - mode: WatchMode::Modify, - last_line_seen: None, - }) +impl<'gc> Session { + pub fn new(id: usize, source: impl Into, path: impl Into) -> Self { + + Self { + id, + source: source.into(), + path: path.into(), + thread: None, + sender: None, + receiver: None, + } } - pub fn watch_global(&mut self, name: impl Into) { - self.watchpoints.push(WatchEntry { - spec: WatchSpec::Global(name.into()), - last: None, - mode: WatchMode::Modify, - last_line_seen: None, - }) + pub fn launch(&mut self) { + let (first, second) = std::sync::mpsc::channel(); + let (thirst, fourth) = std::sync::mpsc::channel(); + self.sender = Some(first); + self.receiver = Some(fourth); + + let source = self.source.as_bytes().to_vec(); + let path = self.path.to_owned(); + let thread = std::thread::spawn(move || { + let mut lua = Lua::full(); + lua.enter(|ctx| { + let mut breakpoints = Breakpoints::default(); + let mut watchpoints = Vec::new(); + let closure = Closure::load(ctx, Some(&path), &source).unwrap(); + let executor = Executor::start(ctx, closure.into(), ()); + + loop { + match second.recv() { + Ok(message) => { + match message { + RequestMessage::AddBreakpoint { chunk, line } => { + let id = breakpoints.add(chunk, line); + thirst.send(ResponseMessage::BreakpointAdded(id)); + } + RequestMessage::RemoveBreakpoint { chunk, line } => { + breakpoints.remove(&chunk, line); + thirst.send(ResponseMessage::BreakpointRemoved); + } + RequestMessage::AddFunctionBreakpoint(name) => { + let id = resolve_function_breakpoint(ctx, name.as_str()) + .map(|(chunk, line)| breakpoints.add(chunk, line)); + thirst.send(ResponseMessage::FunctionBreakpointAdded(id)); + } + RequestMessage::ListBreakpoints => { + let list = breakpoints.list(); + thirst.send(ResponseMessage::ListBreakpoints(list)); + } + RequestMessage::MatchesBreakpoint { chunk, line } => { + let matches = breakpoints.matches(&chunk, line); + thirst.send(ResponseMessage::MatchesBreakpoint(matches)); + } + RequestMessage::WatchRegister { register, mode } => { + watchpoints.push(WatchEntry { + spec: WatchSpec::Register(register), + last: None, + mode: mode.unwrap_or(WatchMode::Modify), + last_line_seen: None, + }); + thirst.send(ResponseMessage::WatchRegisterAdded); + } + RequestMessage::WatchGlobal { name, mode } => { + watchpoints.push(WatchEntry { + spec: WatchSpec::Global(name), + last: None, + mode: mode.unwrap_or(WatchMode::Modify), + last_line_seen: None, + }); + thirst.send(ResponseMessage::WatchGlobalAdded); + } + RequestMessage::ContinueRun => { + let stop_reason = continue_run(executor, ctx, &breakpoints, &mut watchpoints); + thirst.send(ResponseMessage::ContinueRun(stop_reason)); + } + RequestMessage::StepInto => { + let stop_reason = step_into(executor, ctx, &breakpoints, &mut watchpoints); + thirst.send(ResponseMessage::StepInto(stop_reason)); + } + RequestMessage::StepOver => { + let stop_reason = step_over(executor, ctx, &breakpoints, &mut watchpoints); + thirst.send(ResponseMessage::StepOver(stop_reason)); + } + RequestMessage::StepOut => { + let stop_reason = step_out(executor, ctx, &breakpoints, &mut watchpoints); + thirst.send(ResponseMessage::StepOut(stop_reason)); + } + RequestMessage::Backtrace => { + let backtrace = backtrace(executor); + thirst.send(ResponseMessage::Backtrace(backtrace)); + } + RequestMessage::StackSnapshot => { + let stack_snapshot = stack_snapshot(executor); + thirst.send(ResponseMessage::StackSnapshot(stack_snapshot)); + } + RequestMessage::ReadRegister(register) => { + let register = read_register(executor, register); + thirst.send(ResponseMessage::ReadRegister(register)); + } + RequestMessage::ReadRegisters => { + let registers = read_registers(executor); + thirst.send(ResponseMessage::ReadRegisters(registers)); + } + RequestMessage::ReadGlobal(name) => { + let global = read_global(ctx, name.as_str()); + thirst.send(ResponseMessage::ReadGlobal(global)); + } + RequestMessage::ReadMemory(memory_reference, byte_offset, byte_count) => { + let memory = read_memory(executor, memory_reference.as_str(), byte_offset, byte_count); + thirst.send(ResponseMessage::ReadMemory(memory)); + } + RequestMessage::Disassemble(memory_reference, instruction_offset, instruction_count) => { + let disassembled = disassemble(executor, memory_reference.as_str(), instruction_offset, instruction_count); + thirst.send(ResponseMessage::Disassemble(disassembled)); + } + RequestMessage::ReadUpvalues => { + let upvalues = read_upvalues(executor); + thirst.send(ResponseMessage::ReadUpvalues(upvalues)); + } + } + } + Err(e) => { + eprintln!("Error receiving message: {}", e); + } + } + } + }); + }); + + self.thread = Some(thread); } - pub fn watch_table_key(&mut self, table: piccolo::Table<'gc>, key: Value<'gc>) { - self.watchpoints.push(WatchEntry { - spec: WatchSpec::TableKey(table, key), - last: None, - mode: WatchMode::Modify, - last_line_seen: None, - }) + pub fn restart(&mut self) { + self.thread.take().map(|thread| thread.join().unwrap()); + self.launch(); } - pub fn watch_register_with_mode(&mut self, register: usize, mode: WatchMode) { - self.watchpoints.push(WatchEntry { - spec: WatchSpec::Register(register), - last: None, - mode, - last_line_seen: None, - }) + pub fn disconnect(&mut self) { + self.sender.take(); } - pub fn watch_global_with_mode(&mut self, name: impl Into, mode: WatchMode) { - self.watchpoints.push(WatchEntry { - spec: WatchSpec::Global(name.into()), - last: None, - mode, - last_line_seen: None, - }) + pub fn terminate(&mut self) { + self.thread.take().map(|thread| thread.join().unwrap()); + self.sender.take(); } - pub fn watch_table_key_with_mode( - &mut self, - table: piccolo::Table<'gc>, - key: Value<'gc>, - mode: WatchMode, - ) { - self.watchpoints.push(WatchEntry { - spec: WatchSpec::TableKey(table, key), - last: None, - mode, - last_line_seen: None, - }) + pub fn add_breakpoint(&mut self, chunk: String, line: usize) -> Result { + let Some(sender) = self.sender.as_ref() else { + return Err(Dead); + }; + + sender.send(RequestMessage::AddBreakpoint { chunk, line }); + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Dead); + }; + + match receiver.recv() { + Ok(ResponseMessage::BreakpointAdded(id)) => Ok(id), + _ => Err(Dead) + } } - pub fn clear_watchpoints(&mut self) { - self.watchpoints.clear(); + pub fn add_function_breakpoint(&mut self, name: String) -> Result, Dead> { + let Some(sender) = self.sender.as_ref() else { + return Err(Dead); + }; + + sender.send(RequestMessage::AddFunctionBreakpoint(name)); + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Dead); + }; + + match receiver.recv() { + Ok(ResponseMessage::FunctionBreakpointAdded(id)) => Ok(id), + _ => Err(Dead) + } } - fn poll_watchpoints(&mut self, ctx: Context<'gc>) -> Option { - for index in 0..self.watchpoints.len() { - let Some(entry) = self.watchpoints.get(index) else { - continue; - }; - let spec = match &entry.spec { - WatchSpec::Register(i) => WatchSpec::Register(*i), - WatchSpec::Global(n) => WatchSpec::Global(n.clone()), - WatchSpec::TableKey(t, k) => WatchSpec::TableKey(*t, *k), - }; - let last = entry.last; - - let current = self.eval_watch(ctx, &spec); - let should_fire = match entry.mode { - WatchMode::Modify => match (last, current) { - (None, Some(_)) => true, - (Some(_), None) => true, - (Some(left), Some(right)) => !values_equal(left, right), - (None, None) => false, - }, - WatchMode::Access => { - if current.is_some() { - let current_line = self - .current_location(ctx) - .ok() - .flatten() - .map(|l| (l.chunk().to_owned(), l.line())); - if let (Some((chunk, line)), Some(entry)) = - (current_line, self.watchpoints.get(index)) - { - if entry.last_line_seen != Some((chunk.to_owned(), line)) { - if let Some(entry) = self.watchpoints.get_mut(index) { - entry.last_line_seen = Some((chunk.to_owned(), line)); - } - true - } else { - false - } - } else { - false - } - } else { - false - } - } - }; - if should_fire { - let message = format!( - "watchpoint hit: {}", - self.describe_watch_change(&spec, last, current) - ); - if let Some(entry) = self.watchpoints.get_mut(index) { - entry.last = current; - } - return Some(message); - } - if let Some(entry) = self.watchpoints.get_mut(index) { - entry.last = current; - } + pub fn remove_breakpoint(&mut self, chunk: String, line: usize) -> Result<(), Dead> { + let Some(sender) = self.sender.as_ref() else { + return Err(Dead); + }; + + sender.send(RequestMessage::RemoveBreakpoint { chunk, line }); + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Dead); + }; + + match receiver.recv() { + Ok(ResponseMessage::BreakpointRemoved) => Ok(()), + _ => Err(Dead) } - None } - fn eval_watch(&self, ctx: Context<'gc>, spec: &WatchSpec<'gc>) -> Option> { - match spec { - WatchSpec::Register(index) => { - let mut out = None; - let _ = self.with_top_thread_state(|state| { - if let Some(Frame::Lua { - base, stack_size, .. - }) = state.frames.last() - { - let index = *index; - let base = *base; - let size = *stack_size; - if index < size { - out = Some(state.stack[base + index]); - } - } - }); - out - } - WatchSpec::Global(name) => { - let key = ctx.intern(name.as_bytes()); - Some(ctx.globals().get_value(ctx, key)) + pub fn list_breakpoints(&self) -> Result>, Dead> { + let Some(sender) = self.sender.as_ref() else { + return Err(Dead); + }; + + sender.send(RequestMessage::ListBreakpoints); + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Dead); + }; + + match receiver.recv() { + Ok(ResponseMessage::ListBreakpoints(list)) => Ok(list), + _ => Err(Dead) + } + } + + pub fn matches_breakpoint(&self, chunk: String, line: usize) -> Result, Dead> { + let Some(sender) = self.sender.as_ref() else { + return Err(Dead); + }; + + sender.send(RequestMessage::MatchesBreakpoint { chunk, line }); + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Dead); + }; + + match receiver.recv() { + Ok(ResponseMessage::MatchesBreakpoint(matches)) => Ok(matches), + _ => Err(Dead) + } + } + + pub fn watch_register(&mut self, register: usize, mode: Option) -> Result<(), Dead> { + let Some(sender) = self.sender.as_ref() else { + return Err(Dead); + }; + + sender.send(RequestMessage::WatchRegister { register, mode }); + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Dead); + }; + + match receiver.recv() { + Ok(ResponseMessage::WatchRegisterAdded) => Ok(()), + _ => Err(Dead) + } + } + + pub fn watch_global(&mut self, name: impl Into, mode: Option) -> Result<(), Dead> { + let Some(sender) = self.sender.as_ref() else { + return Err(Dead); + }; + + sender.send(RequestMessage::WatchGlobal { name: name.into(), mode }); + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Dead); + }; + + match receiver.recv() { + Ok(ResponseMessage::WatchGlobalAdded) => Ok(()), + _ => Err(Dead) + } + } + + pub fn continue_run(&mut self) -> StopReason { + let Some(sender) = self.sender.as_ref() else { + return StopReason::Error; + }; + + sender.send(RequestMessage::ContinueRun); + + let Some(receiver) = self.receiver.as_ref() else { + return StopReason::Error; + }; + + match receiver.recv() { + Ok(ResponseMessage::ContinueRun(stop_reason)) => stop_reason, + _ => StopReason::Error, + } + } + + pub fn step_into(&mut self) -> StopReason { + let Some(sender) = self.sender.as_ref() else { + return StopReason::Error; + }; + + sender.send(RequestMessage::StepInto); + + let Some(receiver) = self.receiver.as_ref() else { + return StopReason::Error; + }; + + match receiver.recv() { + Ok(ResponseMessage::StepInto(stop_reason)) => stop_reason, + _ => StopReason::Error, + } + } + + pub fn step_over(&mut self) -> StopReason { + let Some(sender) = self.sender.as_ref() else { + return StopReason::Error; + }; + + sender.send(RequestMessage::StepOver); + + let Some(receiver) = self.receiver.as_ref() else { + return StopReason::Error; + }; + + match receiver.recv() { + Ok(ResponseMessage::StepOver(stop_reason)) => stop_reason, + _ => StopReason::Error, + } + } + + pub fn step_out(&mut self) -> StopReason { + let Some(sender) = self.sender.as_ref() else { + return StopReason::Error; + }; + + sender.send(RequestMessage::StepOut); + + let Some(receiver) = self.receiver.as_ref() else { + return StopReason::Error; + }; + + match receiver.recv() { + Ok(ResponseMessage::StepOut(stop_reason)) => stop_reason, + _ => StopReason::Error, + } + } + + pub fn backtrace(&mut self) -> Result, StopReason> { + let Some(sender) = self.sender.as_ref() else { + println!("no sender"); + return Err(StopReason::Error); + }; + + sender.send(RequestMessage::Backtrace); + + let Some(receiver) = self.receiver.as_ref() else { + println!("no receiver"); + return Err(StopReason::Error); + }; + + match receiver.recv() { + Ok(ResponseMessage::Backtrace(backtrace)) => backtrace, + other => { + println!("no backtrace: {:?}", other.unwrap()); + Err(StopReason::Error) } - WatchSpec::TableKey(table, key) => Some(table.get_raw(*key)), - } - } - - fn describe_watch_change( - &self, - spec: &WatchSpec<'gc>, - old: Option>, - new: Option>, - ) -> String { - let spec = match spec { - WatchSpec::Register(index) => format!("register[{}]", index), - WatchSpec::Global(name) => format!("global['{}']", name), - WatchSpec::TableKey(t, k) => format!( - "table({:p})[{}]", - piccolo::table::Table::into_inner(*t).as_ptr(), - value_to_string(*k) - ), + } + } + + pub fn stack_snapshot(&mut self) -> Result)>, StopReason> { + let Some(sender) = self.sender.as_ref() else { + return Err(StopReason::Error); + }; + + sender.send(RequestMessage::StackSnapshot); + + let Some(receiver) = self.receiver.as_ref() else { + return Err(StopReason::Error); }; - format!( - "{}: {} -> {}", - spec, - old.map(|value| value_to_string(value)) - .unwrap_or("".into()), - new.map(|value| value_to_string(value)) - .unwrap_or("".into()) - ) + + match receiver.recv() { + Ok(ResponseMessage::StackSnapshot(stack_snapshot)) => stack_snapshot, + _ => Err(StopReason::Error) + } } - fn stop_if_breakpoint(&self, location: &Location) -> Option { - let breakpoint_ids = self.matches_breakpoint(location.chunk(), location.line()); - if breakpoint_ids.is_empty() { - None - } else { - Some(StopReason::Breakpoint { - location: location.to_owned(), - breakpoint_ids, - }) + pub fn read_register(&mut self, register: usize) -> Result, StopReason> { + let Some(sender) = self.sender.as_ref() else { + return Err(StopReason::Error); + }; + + sender.send(RequestMessage::ReadRegister(register)); + + let Some(receiver) = self.receiver.as_ref() else { + return Err(StopReason::Error); + }; + + match receiver.recv() { + Ok(ResponseMessage::ReadRegister(register)) => register, + _ => Err(StopReason::Error) } } -} -fn current_location_from_state_in_thread<'gc>( - thread_state: &piccolo::thread::ThreadState<'gc>, -) -> Option { - match thread_state.frames.last() { - Some(Frame::Lua { closure, pc, .. }) => { - let proto = closure.prototype(); - let chunk_name = lua_string_to_string(proto.chunk_name); - let line = opcode_index_to_line(&proto.opcode_line_numbers, *pc); - Some(Location::new( - chunk_name, - proto.reference.map_strings(lua_string_to_string), - line, - )) + pub fn read_upvalues(&mut self) -> Result>, StopReason> { + let Some(sender) = self.sender.as_ref() else { + return Err(StopReason::Error); + }; + + sender.send(RequestMessage::ReadUpvalues); + + let Some(receiver) = self.receiver.as_ref() else { + return Err(StopReason::Error); + }; + + match receiver.recv() { + Ok(ResponseMessage::ReadUpvalues(upvalues)) => upvalues, + _ => Err(StopReason::Error) } - _ => None, } -} -fn opcode_index_to_line(op_lines: &[(usize, LineNumber)], pc: usize) -> usize { - if op_lines.is_empty() { - return 0; + pub fn read_registers(&mut self) -> Result>, StopReason> { + let Some(sender) = self.sender.as_ref() else { + return Err(StopReason::Error); + }; + + sender.send(RequestMessage::ReadRegisters); + + let Some(receiver) = self.receiver.as_ref() else { + return Err(StopReason::Error); + }; + + match receiver.recv() { + Ok(ResponseMessage::ReadRegisters(registers)) => registers, + _ => Err(StopReason::Error) + } } - match op_lines.binary_search_by_key(&pc, |(opi, _)| *opi) { - Ok(i) => op_lines[i].1 .0 as usize, - Err(0) => op_lines[0].1 .0 as usize, - Err(i) => op_lines[i - 1].1 .0 as usize, + + pub fn read_global(&mut self, name: String) -> Result { + let Some(sender) = self.sender.as_ref() else { + return Err(StopReason::Error); + }; + + sender.send(RequestMessage::ReadGlobal(name)); + + let Some(receiver) = self.receiver.as_ref() else { + return Err(StopReason::Error); + }; + + match receiver.recv() { + Ok(ResponseMessage::ReadGlobal(global)) => Ok(global), + _ => Err(StopReason::Error) + } } -} -fn values_equal<'gc>(left: Value<'gc>, right: Value<'gc>) -> bool { - use Value::*; - match (left, right) { - (Nil, Nil) => true, - (Boolean(x), Boolean(y)) => x == y, - (Integer(x), Integer(y)) => x == y, - (Number(x), Number(y)) => x.to_bits() == y.to_bits(), - (String(x), String(y)) => x == y, - (Table(x), Table(y)) => x == y, - (Function(x), Function(y)) => x == y, - (Thread(x), Thread(y)) => x == y, - (UserData(x), UserData(y)) => x == y, - _ => false, + pub fn read_memory(&mut self, memory_reference: String, byte_offset: Option, byte_count: Option) -> Result>, StopReason> { + let Some(sender) = self.sender.as_ref() else { + return Err(StopReason::Error); + }; + + sender.send(RequestMessage::ReadMemory(memory_reference, byte_offset, byte_count)); + + let Some(receiver) = self.receiver.as_ref() else { + return Err(StopReason::Error); + }; + + match receiver.recv() { + Ok(ResponseMessage::ReadMemory(memory)) => Ok(memory), + _ => Err(StopReason::Error) + } + } + + pub fn disassemble(&mut self, memory_reference: String, instruction_offset: Option, instruction_count: Option) -> Result>, StopReason> { + let Some(sender) = self.sender.as_ref() else { + return Err(StopReason::Error); + }; + + sender.send(RequestMessage::Disassemble(memory_reference, instruction_offset, instruction_count)); + + let Some(receiver) = self.receiver.as_ref() else { + return Err(StopReason::Error); + }; + + match receiver.recv() { + Ok(ResponseMessage::Disassemble(disassemble)) => Ok(disassemble), + _ => Err(StopReason::Error) + } } } -fn value_to_string<'gc>(value: Value<'gc>) -> String { - format!("{}", value.display()) +pub struct Debugger { + next_session: usize, + sessions: Vec, } -fn lua_string_to_string(s: LuaString<'_>) -> String { - format!("{}", s.display_lossy()) +impl Debugger { + pub fn new() -> Self { + Self { + next_session: 0, + sessions: Vec::new(), + } + } + + pub fn launch(&mut self, session_id: usize) { + self.sessions.get_mut(session_id).map(|session| session.launch()); + } + + pub fn restart(&mut self, session_id: usize) { + self.sessions.get_mut(session_id).map(|session| session.restart()); + } + + pub fn disconnect(&mut self, session_id: usize) { + self.sessions.get_mut(session_id).map(|session| session.disconnect()); + } + + pub fn terminate(&mut self, session_id: usize) { + self.sessions.get_mut(session_id).map(|session| session.terminate()); + } + + pub fn add_session(&mut self, source: impl Into, path: impl Into) -> usize { + let id = self.next_session; + self.next_session += 1; + let source = source.into(); + let path = path.into(); + let session = Session::new(id, source, path); + self.sessions.push(session); + id + } + + pub fn add_breakpoint(&mut self, session_id: usize, chunk: String, line: usize) -> Result { + self.sessions.get_mut(session_id).map(|session| session.add_breakpoint(chunk, line)) + .unwrap_or(Err(Dead)) + } + + pub fn add_function_breakpoint(&mut self, session_id: usize, name: String) -> Result, Dead> { + self.sessions.get_mut(session_id).map(|session| session.add_function_breakpoint(name)) + .unwrap_or(Err(Dead)) + } + + pub fn remove_breakpoint(&mut self, session_id: usize, chunk: String, line: usize) -> Result<(), Dead> { + self.sessions.get_mut(session_id).map(|session| session.remove_breakpoint(chunk, line)) + .unwrap_or(Err(Dead)) + } + + pub fn list_breakpoints(&self, session_id: usize) -> Result>, Dead> { + self.sessions + .get(session_id) + .map(|session| session.list_breakpoints()) + .unwrap_or(Err(Dead)) + } + + pub fn matches_breakpoint(&self, session_id: usize, chunk: String, line: usize) -> Result, Dead> { + self.sessions + .get(session_id) + .map(|session| session.matches_breakpoint(chunk, line)) + .unwrap_or(Err(Dead)) + } + + pub fn watch_register(&mut self, session_id: usize, register: usize, mode: Option) -> Result<(), Dead> { + self.sessions.get_mut(session_id).map(|session| session.watch_register(register, mode)) + .unwrap_or(Err(Dead)) + } + + pub fn watch_global(&mut self, session_id: usize, name: impl Into, mode: Option) -> Result<(), Dead> { + self.sessions.get_mut(session_id).map(|session| session.watch_global(name, mode)) + .unwrap_or(Err(Dead)) + } + + pub fn continue_run(&mut self, session_id: usize) -> StopReason { + self.sessions.get_mut(session_id).map(|session| session.continue_run()) + .unwrap_or(StopReason::Error) + } + + pub fn step_into(&mut self, session_id: usize) -> StopReason { + self.sessions.get_mut(session_id).map(|session| session.step_into()) + .unwrap_or(StopReason::Error) + } + + pub fn step_over(&mut self, session_id: usize) -> StopReason { + self.sessions.get_mut(session_id).map(|session| session.step_over()) + .unwrap_or(StopReason::Error) + } + + pub fn step_out(&mut self, session_id: usize) -> StopReason { + self.sessions.get_mut(session_id).map(|session| session.step_out()) + .unwrap_or(StopReason::Error) + } + + pub fn backtrace(&mut self, session_id: usize) -> Result, StopReason> { + self.sessions.get_mut(session_id).map(|session| session.backtrace()) + .unwrap_or(Err(StopReason::Error)) + } + + pub fn stack_snapshot(&mut self, session_id: usize) -> Result)>, StopReason> { + self.sessions.get_mut(session_id).map(|session| session.stack_snapshot()) + .unwrap_or(Err(StopReason::Error)) + } + + pub fn read_register(&mut self, session_id: usize, register: usize) -> Result, StopReason> { + self.sessions.get_mut(session_id).map(|session| session.read_register(register)) + .unwrap_or(Err(StopReason::Error)) + } + + pub fn read_upvalues(&mut self, session_id: usize) -> Result>, StopReason> { + self.sessions.get_mut(session_id).map(|session| session.read_upvalues()) + .unwrap_or(Err(StopReason::Error)) + } + + pub fn read_registers(&mut self, session_id: usize) -> Result>, StopReason> { + self.sessions.get_mut(session_id).map(|session| session.read_registers()) + .unwrap_or(Err(StopReason::Error)) + } + + pub fn read_global(&mut self, session_id: usize, name: String) -> Result { + self.sessions.get_mut(session_id).map(|session| session.read_global(name)) + .unwrap_or(Err(StopReason::Error)) + } + + pub fn read_memory(&mut self, session_id: usize, memory_reference: String, byte_offset: Option, byte_count: Option) -> Result>, StopReason> { + self.sessions.get_mut(session_id).map(|session| session.read_memory(memory_reference, byte_offset, byte_count)) + .unwrap_or(Err(StopReason::Error)) + } + + pub fn disassemble(&mut self, session_id: usize, memory_reference: String, instruction_offset: Option, instruction_count: Option) -> Result>, StopReason> { + self.sessions.get_mut(session_id).map(|session| session.disassemble(memory_reference, instruction_offset, instruction_count)) + .unwrap_or(Err(StopReason::Error)) + } } diff --git a/debugger/src/watch/entry.rs b/debugger/src/watch/entry.rs index b69bfe4e..dbcf69d8 100644 --- a/debugger/src/watch/entry.rs +++ b/debugger/src/watch/entry.rs @@ -1,10 +1,9 @@ use super::{WatchMode, WatchSpec}; -use piccolo::Value; #[derive(Debug, Clone)] -pub struct WatchEntry<'gc> { - pub spec: WatchSpec<'gc>, - pub last: Option>, +pub struct WatchEntry { + pub spec: WatchSpec, + pub last: Option, pub mode: WatchMode, pub last_line_seen: Option<(String, usize)>, } diff --git a/debugger/src/watch/spec.rs b/debugger/src/watch/spec.rs index 7f023647..f75a7932 100644 --- a/debugger/src/watch/spec.rs +++ b/debugger/src/watch/spec.rs @@ -1,8 +1,6 @@ -use piccolo::Value; #[derive(Debug, Clone)] -pub enum WatchSpec<'gc> { +pub enum WatchSpec { Register(usize), Global(String), - TableKey(piccolo::Table<'gc>, Value<'gc>), } From 1f485a42e120657f6d6f29f321053e9a5c592cac Mon Sep 17 00:00:00 2001 From: reloginn Date: Sat, 23 Aug 2025 20:24:57 +0600 Subject: [PATCH 3/7] `refactor`: fixed behavior [`Adapter`], code split into modules and visually improved --- debugger/examples/simple_debug.rs | 146 +-- debugger/src/adapter.rs | 603 +++++++----- debugger/src/breakpoints.rs | 23 +- debugger/src/error.rs | 22 + debugger/src/lib.rs | 1495 +++-------------------------- debugger/src/session.rs | 637 ++++++++++++ debugger/src/session/debug.rs | 765 +++++++++++++++ debugger/src/session/request.rs | 39 + debugger/src/session/response.rs | 26 + debugger/src/stop_reason.rs | 37 +- debugger/src/watch/spec.rs | 1 - 11 files changed, 2144 insertions(+), 1650 deletions(-) create mode 100644 debugger/src/error.rs create mode 100644 debugger/src/session.rs create mode 100644 debugger/src/session/debug.rs create mode 100644 debugger/src/session/request.rs create mode 100644 debugger/src/session/response.rs diff --git a/debugger/examples/simple_debug.rs b/debugger/examples/simple_debug.rs index 1443d35c..2a5a2343 100644 --- a/debugger/examples/simple_debug.rs +++ b/debugger/examples/simple_debug.rs @@ -1,4 +1,4 @@ -use piccolo::{Value}; +use piccolo::Value; use piccolo_debugger::Debugger; fn main() { @@ -32,85 +32,91 @@ fn main() { dbg.add_function_breakpoint(session_id, "foo".to_string()); - println!("Breakpoints:"); - for (src, lines) in dbg.list_breakpoints(session_id).unwrap() { - let mut v: Vec = lines.iter().copied().collect(); - v.sort_unstable(); - println!(" {src}: {:?}", v); - } + println!("Breakpoints:"); + for (src, lines) in dbg.list_breakpoints(session_id).unwrap() { + let mut v: Vec = lines.iter().copied().collect(); + v.sort_unstable(); + println!(" {src}: {:?}", v); + } dbg.remove_breakpoint(session_id, "test.lua".to_string(), 9); dbg.watch_global(session_id, "x".to_string(), None); - println!("Continue…"); - let mut stop = dbg.continue_run(session_id); - println!("Stopped: {:?}", stop); - - let bt = dbg.backtrace(session_id); - println!("Backtrace:"); - for (i, loc) in bt.unwrap().iter().enumerate() { - println!( - " #{i} {}:{} {}", - loc.chunk(), - loc.line(), - loc.function_ref() - ); + println!("Continue…"); + let mut stop = dbg.continue_run(session_id); + println!("Stopped: {:?}", stop); + + let bt = dbg.backtrace(session_id); + println!("Backtrace:"); + for (i, loc) in bt.unwrap().iter().enumerate() { + println!( + " #{i} {}:{} {}", + loc.chunk(), + loc.line(), + loc.function_ref() + ); + } + + if let Some(dis) = dbg + .disassemble(session_id, "".to_string(), None, None) + .unwrap() + { + println!("Disassembly (current function):"); + for l in dis.iter().take(10) { + println!(" {l:?}"); } - - if let Some(dis) = dbg.disassemble(session_id, "".to_string(), None, None).unwrap() { - println!("Disassembly (current function):"); - for l in dis.iter().take(10) { - println!(" {l:?}"); - } + } + + if let Some(readed) = dbg + .read_memory(session_id, "".to_string(), None, None) + .unwrap() + { + println!("Readed: {:?}", readed); + } + + if let Some(regs) = dbg.read_registers(session_id).unwrap() { + println!("Top frame registers ({}):", regs.len()); + for (i, v) in regs.iter().enumerate() { + println!(" r{i} = {}", v); } - - if let Some(readed) = dbg.read_memory(session_id, "".to_string(), None, None).unwrap() { - println!("Readed: {:?}", readed); + } + + println!("Stack snapshot (before stepping):"); + for (loc, regs) in dbg.stack_snapshot(session_id).unwrap() { + println!( + " {}:{} {} ({} regs)", + loc.chunk(), + loc.line(), + loc.function_ref(), + regs.len() + ); + } + + println!("Step into…"); + stop = dbg.step_into(session_id); + println!("Stopped: {:?}", stop); + + if let Some(ups) = dbg.read_upvalues(session_id).unwrap() { + println!("Upvalues ({}):", ups.len()); + for (i, v) in ups.iter().enumerate() { + println!(" uv{i} = {}", v); } + } - if let Some(regs) = dbg.read_registers(session_id).unwrap() { - println!("Top frame registers ({}):", regs.len()); - for (i, v) in regs.iter().enumerate() { - println!(" r{i} = {}", v); - } - } + println!("Step over…"); + stop = dbg.step_over(session_id); + println!("Stopped: {:?}", stop); - println!("Stack snapshot (before stepping):"); - for (loc, regs) in dbg.stack_snapshot(session_id).unwrap() { - println!( - " {}:{} {} ({} regs)", - loc.chunk(), - loc.line(), - loc.function_ref(), - regs.len() - ); - } + println!("Step out…"); + stop = dbg.step_out(session_id); + println!("Stopped: {:?}", stop); - println!("Step into…"); - stop = dbg.step_into(session_id); - println!("Stopped: {:?}", stop); + println!("Continue again…"); + stop = dbg.continue_run(session_id); + println!("Stopped: {:?}", stop); - if let Some(ups) = dbg.read_upvalues(session_id).unwrap() { - println!("Upvalues ({}):", ups.len()); - for (i, v) in ups.iter().enumerate() { - println!(" uv{i} = {}", v); - } - } - - println!("Step over…"); - stop = dbg.step_over(session_id); - println!("Stopped: {:?}", stop); - - println!("Step out…"); - stop = dbg.step_out(session_id); - println!("Stopped: {:?}", stop); - - println!("Continue again…"); - stop = dbg.continue_run(session_id); - println!("Stopped: {:?}", stop); - - if let Some(v0) = dbg.read_register(session_id, 0).unwrap() { - println!("r0 = {}", v0); - } + if let Some(v0) = dbg.read_register(session_id, 0).unwrap() { + println!("r0 = {}", v0); + } } diff --git a/debugger/src/adapter.rs b/debugger/src/adapter.rs index c719a58f..1db91081 100644 --- a/debugger/src/adapter.rs +++ b/debugger/src/adapter.rs @@ -1,32 +1,10 @@ -use base64::Engine; -use piccolo::{Context, Executor}; - -use super::Debugger; +use std::sync::mpsc::{Receiver, Sender}; -#[derive(PartialEq, Eq, Clone, Copy)] -pub enum State { - NotLaunched, - Launched, - Suspended, - Terminated, -} +use base64::Engine; -pub enum Error { - AdapterNotLaunched, - AdapterSuspended, - AdapterTerminated, -} +use crate::StopReason; -impl Error { - pub fn reason(state: State) -> Self { - match state { - State::Suspended => Error::AdapterSuspended, - State::Terminated => Error::AdapterTerminated, - State::NotLaunched => Error::AdapterNotLaunched, - _ => unreachable!(), - } - } -} +use super::{Debugger, Error}; #[allow(dead_code)] pub struct Capabilities { @@ -69,12 +47,14 @@ pub struct Capabilities { supports_ansi_styling: bool, } +pub struct AttachArguments; + pub struct AttachResponse; pub enum SteppingGranularity {} pub struct StepOutArguments { - thread_id: i64, + thread_id: usize, single_thread: Option, granularity: Option, } @@ -84,16 +64,16 @@ pub struct StepBackResponse; pub struct StepOutResponse; pub struct StepInArguments { - thread_id: i64, + thread_id: usize, single_thread: Option, - target_id: Option, + target_id: Option, granularity: Option, } pub struct StepInResponse; pub struct StepInTargetsArguments { - frame_id: i64, + frame_id: usize, } pub struct StepInTarget { @@ -217,9 +197,9 @@ pub struct StackTraceFormat { } pub struct StackTraceArguments { - thread_id: i64, - start_frame: Option, - levels: Option, + thread_id: usize, + start_frame: Option, + levels: Option, format: Option, } @@ -243,6 +223,7 @@ pub struct StackFrame { } pub struct ReadMemoryArguments { + thread_id: Option, memory_reference: String, offset: Option, count: Option, @@ -255,6 +236,7 @@ pub struct ReadMemoryResponse { } pub struct DisassembleArguments { + thread_id: Option, memory_reference: String, offset: Option, instruction_offset: Option, @@ -323,23 +305,123 @@ pub struct DisconnectResponse; pub struct LaunchArguments { no_debug: Option, + /// Name of the thread to launch (may be file path/name or function name) + name: String, + /// Source code to launch + source: String, } -pub struct Adapter<'gc> { - debugger: Option>, - state: State, +pub struct RestartResponse; + +pub struct TerminateArguments { + restart: Option, } -impl<'gc> Adapter<'gc> { - pub fn new() -> Self { - Self { - debugger: None, - state: State::NotLaunched, - } +pub struct TerminateResponse; + +pub struct TerminateThreadsArguments { + thread_ids: Vec, +} + +pub struct TerminateThreadsResponse; + +pub struct Thread { + id: usize, + name: String, +} + +pub struct ThreadsResponse { + threads: Vec, +} + +pub enum BreakpointReason { + Changed, + New, + Removed, +} + +pub enum StartMethod { + Launch, + Attach, +} + +pub enum StoppedReason { + Step, + Breakpoint, + Pause, + FunctionBreakpoint, + DataBreakpoint, + InstructionBreakpoint, +} + +pub enum ThreadReason { + Started, + Exited, +} + +pub enum Event { + Initialized, + Breakpoint { + thread_id: usize, + reason: BreakpointReason, + breakpoint: Breakpoint, + }, + Continued { + thread_id: usize, + all_threads_continued: Option, + }, + Process { + thread_id: usize, + name: String, + system_process_id: Option, + is_local_process: Option, + start_method: Option, + pointer_size: Option, + }, + Stopped { + reason: StoppedReason, + description: Option, + thread_id: Option, + preserve_focus_hint: Option, + text: Option, + all_threads_stopped: Option, + hit_breakpoint_ids: Option>, + }, + Terminated { + restart: Option, + }, + Thread { + reason: ThreadReason, + thread_id: usize, + }, +} + +pub struct Adapter { + debugger: Debugger, + sender: Sender, +} + +impl Adapter { + pub fn new() -> (Self, Receiver) { + let (sender, receiver) = std::sync::mpsc::channel(); + let adapter = Self { + sender, + debugger: Debugger::new(), + }; + (adapter, receiver) } - pub fn initialize() -> Capabilities { - Capabilities { + pub fn from_debuggee(debuggee: Debugger) -> (Self, Receiver) { + let (sender, receiver) = std::sync::mpsc::channel(); + let adapter = Self { + sender, + debugger: debuggee, + }; + (adapter, receiver) + } + + pub fn initialize(&self) -> Result { + let capabilities = Capabilities { supports_configuration_done_request: false, supports_function_breakpoints: true, supports_conditional_breakpoints: false, @@ -361,7 +443,7 @@ impl<'gc> Adapter<'gc> { supports_delayed_stack_trace_loading: true, supports_loaded_sources_request: false, supports_log_points: false, - supports_terminate_threads_request: false, + supports_terminate_threads_request: true, supports_set_expression: false, supports_terminate_request: true, supports_data_breakpoints: true, @@ -377,47 +459,203 @@ impl<'gc> Adapter<'gc> { supports_single_thread_execution_requests: false, supports_data_breakpoint_bytes: false, supports_ansi_styling: false, - } + }; + self.sender + .send(Event::Initialized) + .map_err(|_| Error::ReceiverDead)?; + Ok(capabilities) } - pub fn attach(&mut self, executor: Executor<'gc>) -> Result { - if self.state == State::NotLaunched { - self.debugger = Some(Debugger::new(executor)); - self.state = State::Launched; - Ok(AttachResponse) - } else { - Err(Error::reason(self.state)) + pub fn threads(&self) -> ThreadsResponse { + let threads = self + .debugger + .sessions + .iter() + .map(|session| Thread { + id: session.id(), + name: session.name().to_string(), + }) + .collect::>(); + ThreadsResponse { threads } + } + + pub fn attach(&mut self) -> AttachResponse { + AttachResponse + } + + pub fn restart(&mut self, _arguments: AttachArguments) -> RestartResponse { + self.debugger.sessions.iter_mut().for_each(|session| { + session.restart(); + }); + RestartResponse + } + + pub fn restart_session(&mut self, session_id: usize) { + self.debugger.restart(session_id); + } + + pub fn disconnect( + &mut self, + arguments: DisconnectArguments, + ) -> Result { + let terminate = arguments.terminate_debuggee.unwrap_or(true); + let suspend = arguments.suspend_debuggee.unwrap_or(false); + let restart = arguments.restart.unwrap_or(false); + + match (terminate, suspend, restart) { + (true, _, true) => { + self.terminate(TerminateArguments { + restart: Some(true), + })?; + } + (true, _, false) => { + self.terminate(TerminateArguments { restart: None })?; + } + (false, true, _) => { + self.debugger.sessions.iter_mut().for_each(|session| { + session.disconnect(); // pause + }); + } + (false, false, _) => { + self.debugger.sessions.iter_mut().for_each(|session| { + session.disconnect(); + }); + } } + + Ok(DisconnectResponse) + } + + pub fn disconnect_session(&mut self, session_id: usize) { + self.debugger.disconnect(session_id); } - pub fn step_in( + pub fn terminate(&mut self, arguments: TerminateArguments) -> Result { + self.debugger.sessions.iter_mut().for_each(|session| { + session.terminate(); + }); + self.sender + .send(Event::Terminated { + restart: arguments.restart, + }) + .map_err(|_| Error::ReceiverDead)?; + Ok(TerminateResponse) + } + + pub fn terminate_session(&mut self, session_id: usize) { + self.debugger.terminate(session_id); + } + + pub fn terminate_threads( &mut self, - ctx: Context<'gc>, - _arguments: StepInArguments, - ) -> Result { - if self.state != State::Launched { - return Err(Error::reason(self.state)); + arguments: TerminateThreadsArguments, + ) -> TerminateThreadsResponse { + arguments + .thread_ids + .iter() + .for_each(|id| self.debugger.terminate(*id)); + TerminateThreadsResponse + } + + pub fn launch(&mut self, arguments: LaunchArguments) -> Result<(), Error> { + let no_debug = matches!(arguments.no_debug, Some(true)); + if no_debug { + unreachable!("TODO") + } else { + let thread_id = self + .debugger + .add_session(&arguments.source, &arguments.name); + self.sender + .send(Event::Thread { + reason: ThreadReason::Started, + thread_id, + }) + .map_err(|_| Error::ReceiverDead)?; + self.debugger.launch(thread_id); + self.sender + .send(Event::Process { + thread_id, + name: arguments.name, + system_process_id: None, + is_local_process: Some(true), + start_method: Some(StartMethod::Launch), + pointer_size: None, + }) + .map_err(|_| Error::ReceiverDead)?; } - let Some(ref mut debugger) = self.debugger else { - return Err(Error::AdapterNotLaunched); - }; - debugger.step_into(ctx); + Ok(()) + } + + pub fn r#continue(&mut self, arguments: ContinueArguments) -> Result { + self.debugger.continue_run(arguments.thread_id)?; + + self.sender + .send(Event::Continued { + thread_id: arguments.thread_id, + all_threads_continued: None, + }) + .map_err(|_| Error::ReceiverDead)?; + + Ok(ContinueResponse { + all_threads_continued: None, + }) + } + + pub fn step_in(&mut self, arguments: StepInArguments) -> Result { + let reason = self.debugger.step_into(arguments.thread_id)?; + self.sender + .send(Event::Stopped { + reason: match reason { + StopReason::Breakpoint { .. } => StoppedReason::Breakpoint, + StopReason::Step { .. } => StoppedReason::Step, + StopReason::Watchpoint { .. } => StoppedReason::DataBreakpoint, + StopReason::Suspended => StoppedReason::Pause, + }, + description: Some(reason.reason().to_string()), + thread_id: Some(arguments.thread_id), + preserve_focus_hint: None, + text: Some(reason.to_string()), + all_threads_stopped: None, + hit_breakpoint_ids: match reason { + StopReason::Breakpoint { breakpoint_ids, .. } => Some(breakpoint_ids), + _ => None, + }, + }) + .map_err(|_| Error::ReceiverDead)?; Ok(StepInResponse) } - pub fn step_out( + pub fn step_out(&mut self, arguments: StepOutArguments) -> Result { + let reason = self.debugger.step_out(arguments.thread_id)?; + self.sender + .send(Event::Stopped { + reason: match reason { + StopReason::Breakpoint { .. } => StoppedReason::Breakpoint, + StopReason::Step { .. } => StoppedReason::Step, + StopReason::Watchpoint { .. } => StoppedReason::DataBreakpoint, + StopReason::Suspended => StoppedReason::Pause, + }, + description: Some(reason.reason().to_string()), + thread_id: Some(arguments.thread_id), + preserve_focus_hint: None, + text: Some(reason.to_string()), + all_threads_stopped: None, + hit_breakpoint_ids: match reason { + StopReason::Breakpoint { breakpoint_ids, .. } => Some(breakpoint_ids), + _ => None, + }, + }) + .map_err(|_| Error::ReceiverDead)?; + Ok(StepOutResponse) + } + + pub fn breakpoint_locations( &mut self, - ctx: Context<'gc>, - _arguments: StepOutArguments, - ) -> Result { - if self.state != State::Launched { - return Err(Error::reason(self.state)); + arguments: BreakpointLocationsArguments, + ) -> BreakpointLocationsResponse { + BreakpointLocationsResponse { + breakpoints: vec![], } - let Some(ref mut debugger) = self.debugger else { - return Err(Error::AdapterNotLaunched); - }; - debugger.step_out(ctx); - Ok(StepOutResponse) } pub fn set_breakpoints( @@ -441,7 +679,6 @@ impl<'gc> Adapter<'gc> { pub fn set_function_breakpoints( &mut self, arguments: SetFunctionBreakpointsArguments, - ctx: piccolo::Context<'gc>, ) -> SetFunctionBreakpointsResponse { SetFunctionBreakpointsResponse { breakpoints: vec![], @@ -460,82 +697,76 @@ impl<'gc> Adapter<'gc> { pub fn stack_trace( &mut self, arguments: StackTraceArguments, - ) -> StackTraceResponse { + ) -> Result { let mut stack_frames = Vec::new(); - if let Some(ref debugger) = self.debugger { - let backtrace = debugger.backtrace(); - let start_frame = arguments.start_frame.unwrap_or(0) as usize; - let levels = arguments.levels.map(|l| l as usize); - - let frames_to_take = if let Some(levels) = levels { - levels - } else { - backtrace.len().saturating_sub(start_frame) - }; - - for (index, location) in backtrace - .iter() - .skip(start_frame) - .take(frames_to_take) - .enumerate() - { - let frame_id = start_frame + index; - let function_name = location.function_ref().to_string(); - - stack_frames.push(StackFrame { - id: frame_id, - name: function_name, - source: Some(Source { - name: Some(location.chunk().to_string()), - path: Some(location.chunk().to_string()), - source_reference: None, - presentation_hint: None, - origin: None, - sources: None, - adapter_data: None, - checksums: None, - }), - line: location.line(), - column: 1, // Default to column 1 - end_line: None, - end_column: None, - can_restart: Some(false), - instruction_pointer_reference: Some(format!("frame:{}", frame_id)), - module_id: None, - presentation_hint: None, - }); - } + let backtrace = self.debugger.backtrace(arguments.thread_id)?; - StackTraceResponse { - stack_frames, - total_frames: backtrace.len(), - } + let start_frame = arguments.start_frame.unwrap_or(0) as usize; + let levels = arguments.levels.map(|l| l as usize); + + let frames_to_take = if let Some(levels) = levels { + levels } else { - StackTraceResponse { - stack_frames: vec![], - total_frames: 0, - } + backtrace.len().saturating_sub(start_frame) + }; + + for (index, location) in backtrace + .iter() + .skip(start_frame) + .take(frames_to_take) + .enumerate() + { + let frame_id = start_frame + index; + let function_name = location.function_ref().to_string(); + + stack_frames.push(StackFrame { + id: frame_id, + name: function_name, + source: Some(Source { + name: Some(location.chunk().to_string()), + path: Some(location.chunk().to_string()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: location.line(), + column: 1, + end_line: None, + end_column: None, + can_restart: Some(false), + instruction_pointer_reference: Some(format!("frame:{}", frame_id)), + module_id: None, + presentation_hint: None, + }); } + + Ok(StackTraceResponse { + stack_frames, + total_frames: backtrace.len(), + }) } pub fn read_memory( &mut self, arguments: ReadMemoryArguments, ) -> Result { - if self.state != State::Launched { - return Err(Error::reason(self.state)); - } - - let Some(ref debugger) = self.debugger else { - return Err(Error::AdapterNotLaunched); - }; - - let Some(bytes) = debugger.read_memory( - &arguments.memory_reference, + let Some(bytes) = self.debugger.read_memory( + arguments.thread_id.unwrap_or( + self.debugger + .sessions + .last() + .map(|session| session.id()) + .unwrap_or_default(), + ), + arguments.memory_reference.to_owned(), arguments.offset, arguments.count, - ) else { + )? + else { return Ok(ReadMemoryResponse { address: arguments.memory_reference, unreadable_bytes: arguments.count, @@ -555,19 +786,19 @@ impl<'gc> Adapter<'gc> { &mut self, arguments: DisassembleArguments, ) -> Result { - if self.state != State::Launched { - return Err(Error::reason(self.state)); - } - - let Some(ref debugger) = self.debugger else { - return Err(Error::AdapterNotLaunched); - }; - - let Some(disassembled) = debugger.disassemble( - &arguments.memory_reference, + let Some(disassembled) = self.debugger.disassemble( + arguments.thread_id.unwrap_or( + self.debugger + .sessions + .last() + .map(|session| session.id()) + .unwrap_or_default(), + ), + arguments.memory_reference, arguments.instruction_offset, arguments.instruction_count, - ) else { + )? + else { return Ok(DisassembleResponse { instructions: vec![], }); @@ -591,78 +822,4 @@ impl<'gc> Adapter<'gc> { .collect(), }) } - - pub fn r#continue( - &mut self, - _arguments: ContinueArguments, - ctx: Context<'gc>, - ) -> Result { - if self.state != State::Launched { - return Err(Error::reason(self.state)); - } - - let Some(ref mut debugger) = self.debugger else { - return Err(Error::AdapterNotLaunched); - }; - - debugger.continue_run(ctx); - - Ok(ContinueResponse { - all_threads_continued: Some(true), - }) - } - - pub fn pause(&mut self, _arguments: PauseArguments) -> Result { - self.state = State::Suspended; - Ok(PauseResponse) - } - - pub fn breakpoint_locations( - &mut self, - arguments: BreakpointLocationsArguments, - ) -> BreakpointLocationsResponse { - BreakpointLocationsResponse { - breakpoints: vec![], - } - } - - pub fn restart(&mut self, executor: Executor<'gc>) { - self.debugger.take(); - self.debugger = Some(Debugger::new(executor)); - self.state = State::Launched; - } - - pub fn disconnect( - &mut self, - arguments: DisconnectArguments, - executor: Executor<'gc>, - ) -> Result { - let terminate = arguments.terminate_debuggee.unwrap_or(false); - let suspend = arguments.suspend_debuggee.unwrap_or(false); - let restart = arguments.restart.unwrap_or(false); - - if terminate { - self.terminate(); - } else if suspend { - self.pause(PauseArguments { thread_id: 0 })?; - } - - if restart { - self.restart(executor); - } - - Ok(DisconnectResponse) - } - - pub fn terminate(&mut self) { - self.state = State::Terminated; - self.debugger.take(); - } - - pub fn launch(&mut self, executor: Executor<'gc>, arguments: LaunchArguments) { - let no_debug = matches!(arguments.no_debug, Some(true)); - if !no_debug { - self.debugger = Some(Debugger::new(executor)); - } - } } diff --git a/debugger/src/breakpoints.rs b/debugger/src/breakpoints.rs index a1760602..59730b6c 100644 --- a/debugger/src/breakpoints.rs +++ b/debugger/src/breakpoints.rs @@ -37,16 +37,19 @@ impl Breakpoints { .iter() .map(|breakpoint| breakpoint.source.as_str()) .collect(); - sources.into_iter().map(|source| { - ( - source.to_owned(), - self.breakpoints - .iter() - .filter(|breakpoint| breakpoint.source == source) - .map(|breakpoint| breakpoint.line) - .collect(), - ) - }).collect() + sources + .into_iter() + .map(|source| { + ( + source.to_owned(), + self.breakpoints + .iter() + .filter(|breakpoint| breakpoint.source == source) + .map(|breakpoint| breakpoint.line) + .collect(), + ) + }) + .collect() } pub fn matches(&self, chunk: &str, line: usize) -> Vec { diff --git a/debugger/src/error.rs b/debugger/src/error.rs new file mode 100644 index 00000000..dff2152f --- /dev/null +++ b/debugger/src/error.rs @@ -0,0 +1,22 @@ +#[derive(Debug)] +pub enum Error { + SenderDead, + ReceiverDead, + UnexpectedResponse, + ExecutorFinished, + ExecutorAlreadyRunning, + SessionNotFound, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::SenderDead => write!(f, "the sender is dead"), + Error::ReceiverDead => write!(f, "the receiver is dead"), + Error::UnexpectedResponse => write!(f, "the response is unexpected"), + Error::ExecutorFinished => write!(f, "the executor has finished"), + Error::ExecutorAlreadyRunning => write!(f, "the executor is already running"), + Error::SessionNotFound => write!(f, "the session is not found"), + } + } +} diff --git a/debugger/src/lib.rs b/debugger/src/lib.rs index e717941f..c82821e1 100644 --- a/debugger/src/lib.rs +++ b/debugger/src/lib.rs @@ -1,9 +1,3 @@ -use piccolo::{ - compiler::{FunctionRef, LineNumber}, opcode::{OpCode, Operation}, thread::{vm::run_vm, Executor, Frame, LuaFrame}, Closure, Context, Fuel, FunctionPrototype, Lua, String as LuaString, Value -}; -use std::{collections::HashMap, mem::size_of, sync::{atomic::AtomicPtr, mpsc::{Receiver, Sender}, Arc, RwLock}, thread::JoinHandle}; -use watch::{WatchEntry, WatchSpec}; - #[allow(dead_code)] #[derive(Debug, Clone)] struct PrototypeReference { @@ -11,23 +5,22 @@ struct PrototypeReference { prototype_index: usize, } -#[derive(Debug)] -pub struct Dead; +use std::collections::HashMap; + +use piccolo::opcode::Operation; -pub use self::{ - breakpoints::{Breakpoint, Breakpoints}, - location::Location, - stop_reason::StopReason, - watch::WatchMode, -}; +pub use self::{error::Error, location::Location, stop_reason::StopReason, watch::WatchMode}; -// pub mod adapter; -pub mod breakpoints; +use self::session::Session; + +pub mod adapter; +mod breakpoints; +pub mod error; pub mod location; +mod session; pub mod stop_reason; mod watch; - #[derive(Debug)] pub struct Disassembled { symbol: &'static str, @@ -36,1267 +29,6 @@ pub struct Disassembled { operation: Operation, } -#[derive(Debug)] -pub enum RequestMessage { - AddBreakpoint { - chunk: String, - line: usize - }, - AddFunctionBreakpoint(String), - RemoveBreakpoint { - chunk: String, - line: usize - }, - ListBreakpoints, - MatchesBreakpoint { - chunk: String, - line: usize - }, - WatchRegister { - register: usize, - mode: Option - }, - WatchGlobal { - name: String, - mode: Option - }, - ContinueRun, - StepInto, - StepOver, - StepOut, - Backtrace, - StackSnapshot, - ReadRegister(usize), - ReadUpvalues, - ReadRegisters, - ReadGlobal(String), - ReadMemory(String, Option, Option), - Disassemble(String, Option, Option), -} - -#[derive(Debug)] -pub enum ResponseMessage { - BreakpointAdded(usize), - BreakpointRemoved, - FunctionBreakpointAdded(Option), - ListBreakpoints(HashMap>), - MatchesBreakpoint(Vec), - WatchRegisterAdded, - WatchGlobalAdded, - ContinueRun(StopReason), - StepInto(StopReason), - StepOver(StopReason), - StepOut(StopReason), - Backtrace(Result, StopReason>), - StackSnapshot(Result)>, StopReason>), - ReadRegister(Result, StopReason>), - ReadUpvalues(Result>, StopReason>), - ReadRegisters(Result>, StopReason>), - ReadGlobal(String), - ReadMemory(Option>), - Disassemble(Option>), -} - -pub struct Session { - id: usize, - source: String, - path: String, - thread: Option>, - sender: Option>, - receiver: Option> -} - -fn current_location_from_state_in_thread<'gc>( - thread_state: &piccolo::thread::ThreadState<'gc>, -) -> Option { - match thread_state.frames.last() { - Some(Frame::Lua { closure, pc, .. }) => { - let proto = closure.prototype(); - let chunk_name = lua_string_to_string(proto.chunk_name); - let line = opcode_index_to_line(&proto.opcode_line_numbers, *pc); - Some(Location::new( - chunk_name, - proto.reference.map_strings(lua_string_to_string), - line, - )) - } - _ => None, - } -} - -fn opcode_index_to_line(op_lines: &[(usize, LineNumber)], pc: usize) -> usize { - if op_lines.is_empty() { - return 0; - } - match op_lines.binary_search_by_key(&pc, |(opi, _)| *opi) { - Ok(i) => op_lines[i].1 .0 as usize, - Err(0) => op_lines[0].1 .0 as usize, - Err(i) => op_lines[i - 1].1 .0 as usize, - } -} - -fn value_to_string<'gc>(value: Value<'gc>) -> String { - format!("{}", value.display()) -} - -fn lua_string_to_string(s: LuaString<'_>) -> String { - format!("{}", s.display_lossy()) -} - -fn current_stack_depth<'gc>(executor: Executor<'gc>) -> Result { - with_top_thread_state(executor, |state| state.frames.len()) -} - -fn stack_depth_and_location<'gc>(executor: Executor<'gc>) -> Result<(usize, Option), StopReason> { - with_top_thread_state(executor, |state| { - let depth = state.frames.len(); - let loc = current_location_from_state_in_thread(state); - (depth, loc) - }) -} - -fn current_location<'gc>(executor: Executor<'gc>) -> Result, StopReason> { - with_top_thread_state(executor, |state| current_location_from_state_in_thread(state)) -} - -fn with_top_thread_state<'gc, R>( - executor: Executor<'gc>, - f: impl FnOnce(&piccolo::thread::ThreadState<'gc>) -> R, -) -> Result { - let guard = executor.0.borrow(); - let thread = *guard.thread_stack.last().ok_or(StopReason::Finished)?; - drop(guard); - let thread_state = thread.into_inner().borrow(); - Ok(f(&thread_state)) -} - -fn with_top_thread_state_mut<'gc, R>( - executor: Executor<'gc>, - ctx: Context<'gc>, - f: impl FnOnce(piccolo::Thread<'gc>, &mut piccolo::thread::ThreadState<'gc>) -> R, -) -> Result { - let guard = executor.0.borrow(); - let top_thread = *guard.thread_stack.last().ok_or(StopReason::Finished)?; - drop(guard); - let mut thread_state = top_thread.into_inner().borrow_mut(&ctx); - let r = f(top_thread, &mut thread_state); - drop(thread_state); - Ok(r) -} - -fn stop_if_breakpoint(location: &Location, breakpoints: &Breakpoints) -> Option { - let breakpoint_ids = breakpoints.matches(location.chunk(), location.line()); - if breakpoint_ids.is_empty() { - None - } else { - Some(StopReason::Breakpoint { - location: location.to_owned(), - breakpoint_ids, - }) - } -} - -fn eval_watch<'gc>(spec: &WatchSpec, executor: Executor<'gc>, ctx: Context<'gc>) -> Option { - match spec { - WatchSpec::Register(index) => { - with_top_thread_state(executor, |state| { - if let Some(Frame::Lua { - base, stack_size, .. - }) = state.frames.last() - { - let index = *index; - let base = *base; - let size = *stack_size; - if index < size { - return Some(state.stack[base + index].display().to_string()); - } - } - None - }).ok()? - } - WatchSpec::Global(name) => { - let key = ctx.intern(name.as_bytes()); - Some(ctx.globals().get_value(ctx, key).display().to_string()) - } - } -} - -fn resolve_function_breakpoint<'gc>( - ctx: Context<'gc>, - name: &str, -) -> Option<(String, usize)> { - let env = ctx.globals(); - let mut segments = name.split('.'); - let first = segments.next()?; - let mut current = env.get_value(ctx, ctx.intern(first.as_bytes())); - for segment in segments { - match current { - Value::Table(t) => { - current = t.get_raw(Value::String(ctx.intern(segment.as_bytes()))); - } - _ => return None, - } - } - match current { - Value::Function(piccolo::Function::Closure(closure)) => { - let proto = closure.prototype(); - let chunk_name = lua_string_to_string(proto.chunk_name); - let first_line = if proto.opcode_line_numbers.is_empty() { - 0 - } else { - proto.opcode_line_numbers[0].1 .0 as usize - }; - Some((chunk_name, first_line)) - } - _ => None, - } -} - -fn describe_watch_change<'gc>( - spec: &WatchSpec, - old: &Option, - new: &Option, -) -> String { - let spec = match spec { - WatchSpec::Register(index) => format!("register[{}]", index), - WatchSpec::Global(name) => format!("global['{}']", name), - }; - format!( - "{}: {} -> {}", - spec, - old.as_ref().unwrap_or(&"".to_string()), - new.as_ref().unwrap_or(&"".to_string()) - ) -} - -fn poll_watchpoints<'gc>(watchpoints: &mut Vec, executor: Executor<'gc>, ctx: Context<'gc>) -> Option { - for index in 0..watchpoints.len() { - let Some(entry) = watchpoints.get(index) else { - continue; - }; - let entry = entry.to_owned(); - let spec = match &entry.spec { - WatchSpec::Register(i) => WatchSpec::Register(*i), - WatchSpec::Global(n) => WatchSpec::Global(n.clone()), - }; - let last = entry.last; - - let current = eval_watch(&spec, executor, ctx); - let should_fire = match entry.mode { - WatchMode::Modify => match (last.as_ref(), current.as_ref()) { - (None, Some(_)) => true, - (Some(_), None) => true, - (Some(left), Some(right)) => left != right, - (None, None) => false, - }, - WatchMode::Access => { - if current.is_some() { - let current_line = current_location(executor) - .ok() - .flatten() - .map(|l| (l.chunk().to_owned(), l.line())); - if let (Some((chunk, line)), Some(entry)) = - (current_line, watchpoints.get(index)) - { - if entry.last_line_seen != Some((chunk.to_owned(), line)) { - if let Some(entry) = watchpoints.get_mut(index) { - entry.last_line_seen = Some((chunk.to_owned(), line)); - } - true - } else { - false - } - } else { - false - } - } else { - false - } - } - }; - if should_fire { - let message = format!( - "watchpoint hit: {}", - describe_watch_change(&spec, &last, ¤t) - ); - if let Some(entry) = watchpoints.get_mut(index) { - entry.last = current; - } - return Some(message); - } - if let Some(entry) = watchpoints.get_mut(index) { - entry.last = current; - } - } - None -} - -pub fn step_one<'gc>(executor: Executor<'gc>, ctx: Context<'gc>, watchpoints: &mut Vec) -> StopReason { - const FUEL_NON_LUA_CALLBACK_STEP: i32 = 11; - const FUEL_NON_LUA_SEQUENCE_STEP: i32 = 7; - const FUEL_NON_LUA_ERROR_STEP: i32 = 3; - - match executor.mode() { - piccolo::thread::ExecutorMode::Stopped => return StopReason::Finished, - piccolo::thread::ExecutorMode::Result => return StopReason::Finished, - piccolo::thread::ExecutorMode::Suspended => return StopReason::Suspended, - piccolo::thread::ExecutorMode::Running => return StopReason::Error, - piccolo::thread::ExecutorMode::Normal => {} - } - - let (is_lua_top, non_lua_fuel) = - match with_top_thread_state(executor, |state| match state.frames.last() { - Some(Frame::Lua { .. }) => (true, 0), - Some(Frame::Callback { .. }) => (false, FUEL_NON_LUA_CALLBACK_STEP), - Some(Frame::Sequence { .. }) => (false, FUEL_NON_LUA_SEQUENCE_STEP), - Some(Frame::Error(_)) => (false, FUEL_NON_LUA_ERROR_STEP), - Some(Frame::Result { .. }) => (false, -1), - Some(Frame::WaitThread) | Some(Frame::Yielded) | Some(Frame::Start(_)) => { - (false, 3) - } - None => (false, -1), - }) { - Ok(value) => value, - Err(stop_reason) => return stop_reason, - }; - - if non_lua_fuel == -1 { - return StopReason::Finished; - } - - if is_lua_top { - let _ = with_top_thread_state_mut(executor, ctx, |thread, state| { - let mut fuel = Fuel::with(1_000_000); - let lua_frame = LuaFrame { - state, - thread, - fuel: &mut fuel, - }; - let _ = run_vm(ctx, lua_frame, 1); - }); - } else { - let mut fuel = Fuel::with(non_lua_fuel); - let _ = executor.step(ctx, &mut fuel); - } - - let location = match current_location(executor) { - Ok(Some(location)) => location, - Ok(None) => return StopReason::Finished, - Err(stop_reason) => return stop_reason, - }; - if let Some(hit) = poll_watchpoints(watchpoints, executor, ctx) { - return StopReason::Watchpoint(hit); - } - StopReason::Step(location) -} - -pub fn continue_run<'gc>(executor: Executor<'gc>, ctx: Context<'gc>, breakpoints: &Breakpoints, watchpoints: &mut Vec) -> StopReason { - loop { - match current_location(executor) { - Ok(Some(location)) => { - if let Some(stop_reason) = stop_if_breakpoint(&location, &breakpoints) { - return stop_reason; - } - } - Ok(None) => {} - Err(stop_reason) => return stop_reason, - } - match step_one(executor, ctx, watchpoints) { - StopReason::Step(_) => continue, - other => return other, - } - } -} - -pub fn step_into<'gc>(executor: Executor<'gc>, ctx: Context<'gc>, breakpoints: &Breakpoints, watchpoints: &mut Vec) -> StopReason { - let start_location = match current_location(executor) { - Ok(Some(location)) => location, - Ok(None) => return StopReason::Finished, - Err(stop_reason) => return stop_reason, - }; - - if let Some(stop_reason) = stop_if_breakpoint(&start_location, breakpoints) { - return stop_reason; - } - - loop { - match step_one(executor, ctx, watchpoints) { - StopReason::Step(location) => { - if let Some(stop_reason) = stop_if_breakpoint(&location, breakpoints) { - return stop_reason; - } - if location.line() != start_location.line() - || location.chunk() != start_location.chunk() - { - return StopReason::Step(location); - } - } - StopReason::Watchpoint(hit) => return StopReason::Watchpoint(hit), - other => return other, - } - } - } - - pub fn step_over<'gc>(executor: Executor<'gc>, ctx: Context<'gc>, breakpoints: &Breakpoints, watchpoints: &mut Vec) -> StopReason { - let (start_depth, start_location) = match stack_depth_and_location(executor) { - Ok((depth, Some(location))) => (depth, location), - Ok((_depth, None)) => return StopReason::Finished, - Err(stop_reason) => return stop_reason, - }; - - if let Some(stop_reason) = stop_if_breakpoint(&start_location, breakpoints) { - return stop_reason; - } - - loop { - match step_one(executor, ctx, watchpoints) { - StopReason::Step(location) => { - if let Some(stop_reason) = stop_if_breakpoint(&location, breakpoints) { - return stop_reason; - } - let depth = match current_stack_depth(executor) { - Ok(depth) => depth, - Err(stop_reason) => return stop_reason, - }; - if depth <= start_depth - && (location.line() != start_location.line() - || location.chunk() != start_location.chunk()) - { - return StopReason::Step(location); - } - } - StopReason::Watchpoint(hit) => return StopReason::Watchpoint(hit), - other => return other, - } - } - } - - pub fn step_out<'gc>(executor: Executor<'gc>, ctx: Context<'gc>, breakpoints: &Breakpoints, watchpoints: &mut Vec) -> StopReason { - let start_depth = match current_stack_depth(executor) { - Ok(depth) => depth, - Err(stop_reason) => return stop_reason, - }; - - loop { - match step_one(executor, ctx, watchpoints) { - StopReason::Step(location) => { - if let Some(stop_reason) = stop_if_breakpoint(&location, breakpoints) { - return stop_reason; - } - let depth = match current_stack_depth(executor) { - Ok(depth) => depth, - Err(stop_reason) => return stop_reason, - }; - if depth < start_depth { - return StopReason::Step(location); - } - } - StopReason::Watchpoint(hit) => return StopReason::Watchpoint(hit), - other => return other, - } - } - } - - pub fn backtrace<'gc>(executor: Executor<'gc>) -> Result, StopReason> { - with_top_thread_state(executor, |state| { - let mut out = Vec::with_capacity(state.frames.len()); - for frame in state.frames.iter() { - if let Frame::Lua { closure, pc, .. } = frame { - let proto = closure.prototype(); - let chunk_name = lua_string_to_string(proto.chunk_name); - let function_ref = proto.reference.map_strings(lua_string_to_string); - let line = opcode_index_to_line(&proto.opcode_line_numbers, *pc); - out.push(Location::new(chunk_name, function_ref, line)); - } - } - out - }) - } - - pub fn read_register<'gc>(executor: Executor<'gc>, register_index: usize) -> Result, StopReason> { - with_top_thread_state(executor, |state| { - if let Some(Frame::Lua { - base, stack_size, .. - }) = state.frames.last() - { - let base = *base; - let size = *stack_size; - if register_index < size { - return Some(value_to_string(state.stack[base + register_index])); - } - } - None - }) - } - - pub fn read_global<'gc>(ctx: Context<'gc>, name: &str) -> String { - let key = ctx.intern(name.as_bytes()); - value_to_string(ctx.globals().get_value(ctx, key)) - } - - pub fn read_registers<'gc>(executor: Executor<'gc>) -> Result>, StopReason> { - with_top_thread_state(executor, |state| { - if let Some(Frame::Lua { - base, stack_size, .. - }) = state.frames.last() - { - let base = *base; - let size = *stack_size; - return Some(state.stack[base..base + size].iter().map(|v| value_to_string(*v)).collect()); - } - None - }) - } - - pub fn read_upvalues<'gc>(executor: Executor<'gc>) -> Result>, StopReason> { - with_top_thread_state(executor, |state| { - if let Some(Frame::Lua { closure, .. }) = state.frames.last() { - let mut values = Vec::new(); - for &upvalue in closure.upvalues() { - if let piccolo::closure::UpValueState::Closed(value) = upvalue.get() { - values.push(value_to_string(value)); - } else { - values.push(String::new()); - } - } - return Some(values); - } - None - }) - } - - pub fn stack_snapshot<'gc>(executor: Executor<'gc>) -> Result)>, StopReason> { - with_top_thread_state(executor, |state| { - let mut out = Vec::with_capacity(state.frames.len()); - for frame in state.frames.iter() { - if let Frame::Lua { - base, - stack_size, - closure, - pc, - .. - } = frame - { - let proto = closure.prototype(); - let chunk_name = lua_string_to_string(proto.chunk_name); - let function_ref = proto.reference.map_strings(lua_string_to_string); - let line = opcode_index_to_line(&proto.opcode_line_numbers, *pc); - let location = Location::new(chunk_name, function_ref, line); - let base = *base; - let size = *stack_size; - out.push((location, state.stack[base..base + size].iter().map(|v| value_to_string(*v)).collect())); - } - } - out - }) - } - - fn disassemble_frame<'gc>( - executor: Executor<'gc>, - frame: usize, - instruction_offset: Option, - instruction_count: Option, - ) -> Option> { - with_top_thread_state(executor, |state| { - if let Some(Frame::Lua { closure, pc, .. }) = if frame == usize::MAX { - state.frames.last() - } else { - state.frames.get(frame) - } { - let prototype = closure.prototype(); - let current_pc = *pc; - Some(disassemble_prototype_opcodes( - &prototype, - Some(current_pc), - instruction_offset, - instruction_count, - )) - } else { - None - } - }) - .ok()? - } - - fn disassemble_prototype<'gc>( - executor: Executor<'gc>, - prototype: &str, - instruction_offset: Option, - instruction_count: Option, - ) -> Option> { - let (chunk, index) = prototype.split_once(':')?; - - let index: usize = index.parse().ok()?; - - with_top_thread_state(executor, |state| { - for frame in state.frames.iter() { - if let Frame::Lua { closure, .. } = frame { - let proto = closure.prototype(); - let frame_chunk = lua_string_to_string(proto.chunk_name); - - if frame_chunk == chunk { - if let Some(nested_proto) = proto.prototypes.get(index) { - return Some(disassemble_prototype_opcodes( - nested_proto, - None, - instruction_offset, - instruction_count, - )); - } - } - } - } - None - }) - .ok()? - } - - fn disassemble_prototype_opcodes<'gc>( - prototype: &FunctionPrototype, - current_pc: Option, - instruction_offset: Option, - instruction_count: Option, - ) -> Vec { - let start_offset = instruction_offset.unwrap_or(0); - let max_count = instruction_count.unwrap_or(prototype.opcodes.len()); - - let end_offset = std::cmp::min(start_offset + max_count, prototype.opcodes.len()); - - let mut disassembled = Vec::with_capacity(end_offset - start_offset); - - for index in start_offset..end_offset { - if let Some(opcode) = prototype.opcodes.get(index) { - let operation = (*opcode).decode(); - let line = opcode_index_to_line(&prototype.opcode_line_numbers, index) + 1; - let symbol = if Some(index) == current_pc { - "=>" - } else { - " " - }; - disassembled.push(Disassembled { - symbol, - index, - line, - operation, - }); - } - } - - disassembled - } - - pub fn disassemble<'gc>( - executor: Executor<'gc>, - memory_reference: &str, - instruction_offset: Option, - instruction_count: Option, - ) -> Option> { - let (ty, s) = memory_reference.split_once(':').unwrap_or(("frame", "")); - match ty { - "frame" => disassemble_frame( - executor, - if s.is_empty() { - usize::MAX - } else { - s.parse().ok()? - }, - instruction_offset, - instruction_count, - ), - "prototype" => disassemble_prototype(executor, s, instruction_offset, instruction_count), - _ => None, - } - } - - pub fn read_memory<'gc>( - executor: Executor<'gc>, - memory_reference: &str, - byte_offset: Option, - byte_count: Option, - ) -> Option> { - let (ty, s) = memory_reference.split_once(':').unwrap_or(("frame", "")); - - match ty { - "frame" => read_frame_memory( - executor, - if s.is_empty() { - usize::MAX - } else { - s.parse().ok()? - }, - byte_offset, - byte_count, - ), - "prototype" => read_prototype_memory(executor, s, byte_offset, byte_count), - _ => None, - } - } - - fn read_frame_memory<'gc>( - executor: Executor<'gc>, - frame: usize, - byte_offset: Option, - byte_count: Option, - ) -> Option> { - with_top_thread_state(executor, |state| { - if let Some(piccolo::thread::Frame::Lua { closure, .. }) = if frame == usize::MAX { - state.frames.last() - } else { - state.frames.get(frame) - } { - let prototype = closure.prototype(); - Some(read_opcodes(&prototype.opcodes, byte_offset, byte_count)) - } else { - None - } - }) - .ok()? - } - - fn read_prototype_memory<'gc>( - executor: Executor<'gc>, - prototype: &str, - byte_offset: Option, - byte_count: Option, - ) -> Option> { - let (chunk, index) = prototype.split_once(':')?; - - let index: usize = index.parse().ok()?; - - with_top_thread_state(executor, |state| { - for frame in state.frames.iter() { - if let Frame::Lua { closure, .. } = frame { - let proto = closure.prototype(); - let frame_chunk = lua_string_to_string(proto.chunk_name); - - if frame_chunk == chunk { - if let Some(nested_proto) = proto.prototypes.get(index) { - return Some( - read_opcodes(&nested_proto.opcodes, byte_offset, byte_count), - ); - } - } - } - } - None - }) - .ok()? - } - - fn read_opcodes<'gc>( - opcodes: &[OpCode], - byte_offset: Option, - byte_count: Option, - ) -> Vec { - fn to_bytes(value: T) -> Vec { - let mut bytes = Vec::with_capacity(size_of::()); - - // SAFETY: We are copying the bytes of a plain-old-data (POD) type T into a Vec with enough capacity. - // The Vec is pre-allocated with the correct size, and we set the length after copying. - unsafe { - std::ptr::copy_nonoverlapping( - std::ptr::addr_of!(value).cast::(), - bytes.as_mut_ptr(), - size_of::(), - ); - bytes.set_len(size_of::()); - } - - bytes - } - - let mut raw = Vec::new(); - - for opcode in opcodes { - let bytes = to_bytes(*opcode); - raw.extend_from_slice(&bytes); - } - - let byte_offset = byte_offset.unwrap_or(0); - let byte_count = byte_count.unwrap_or(raw.len()); - - let start = std::cmp::min(byte_offset, raw.len()); - let end = std::cmp::min(start + byte_count, raw.len()); - - raw[start..end].to_vec() - } - -impl<'gc> Session { - pub fn new(id: usize, source: impl Into, path: impl Into) -> Self { - - Self { - id, - source: source.into(), - path: path.into(), - thread: None, - sender: None, - receiver: None, - } - } - - pub fn launch(&mut self) { - let (first, second) = std::sync::mpsc::channel(); - let (thirst, fourth) = std::sync::mpsc::channel(); - self.sender = Some(first); - self.receiver = Some(fourth); - - let source = self.source.as_bytes().to_vec(); - let path = self.path.to_owned(); - let thread = std::thread::spawn(move || { - let mut lua = Lua::full(); - lua.enter(|ctx| { - let mut breakpoints = Breakpoints::default(); - let mut watchpoints = Vec::new(); - let closure = Closure::load(ctx, Some(&path), &source).unwrap(); - let executor = Executor::start(ctx, closure.into(), ()); - - loop { - match second.recv() { - Ok(message) => { - match message { - RequestMessage::AddBreakpoint { chunk, line } => { - let id = breakpoints.add(chunk, line); - thirst.send(ResponseMessage::BreakpointAdded(id)); - } - RequestMessage::RemoveBreakpoint { chunk, line } => { - breakpoints.remove(&chunk, line); - thirst.send(ResponseMessage::BreakpointRemoved); - } - RequestMessage::AddFunctionBreakpoint(name) => { - let id = resolve_function_breakpoint(ctx, name.as_str()) - .map(|(chunk, line)| breakpoints.add(chunk, line)); - thirst.send(ResponseMessage::FunctionBreakpointAdded(id)); - } - RequestMessage::ListBreakpoints => { - let list = breakpoints.list(); - thirst.send(ResponseMessage::ListBreakpoints(list)); - } - RequestMessage::MatchesBreakpoint { chunk, line } => { - let matches = breakpoints.matches(&chunk, line); - thirst.send(ResponseMessage::MatchesBreakpoint(matches)); - } - RequestMessage::WatchRegister { register, mode } => { - watchpoints.push(WatchEntry { - spec: WatchSpec::Register(register), - last: None, - mode: mode.unwrap_or(WatchMode::Modify), - last_line_seen: None, - }); - thirst.send(ResponseMessage::WatchRegisterAdded); - } - RequestMessage::WatchGlobal { name, mode } => { - watchpoints.push(WatchEntry { - spec: WatchSpec::Global(name), - last: None, - mode: mode.unwrap_or(WatchMode::Modify), - last_line_seen: None, - }); - thirst.send(ResponseMessage::WatchGlobalAdded); - } - RequestMessage::ContinueRun => { - let stop_reason = continue_run(executor, ctx, &breakpoints, &mut watchpoints); - thirst.send(ResponseMessage::ContinueRun(stop_reason)); - } - RequestMessage::StepInto => { - let stop_reason = step_into(executor, ctx, &breakpoints, &mut watchpoints); - thirst.send(ResponseMessage::StepInto(stop_reason)); - } - RequestMessage::StepOver => { - let stop_reason = step_over(executor, ctx, &breakpoints, &mut watchpoints); - thirst.send(ResponseMessage::StepOver(stop_reason)); - } - RequestMessage::StepOut => { - let stop_reason = step_out(executor, ctx, &breakpoints, &mut watchpoints); - thirst.send(ResponseMessage::StepOut(stop_reason)); - } - RequestMessage::Backtrace => { - let backtrace = backtrace(executor); - thirst.send(ResponseMessage::Backtrace(backtrace)); - } - RequestMessage::StackSnapshot => { - let stack_snapshot = stack_snapshot(executor); - thirst.send(ResponseMessage::StackSnapshot(stack_snapshot)); - } - RequestMessage::ReadRegister(register) => { - let register = read_register(executor, register); - thirst.send(ResponseMessage::ReadRegister(register)); - } - RequestMessage::ReadRegisters => { - let registers = read_registers(executor); - thirst.send(ResponseMessage::ReadRegisters(registers)); - } - RequestMessage::ReadGlobal(name) => { - let global = read_global(ctx, name.as_str()); - thirst.send(ResponseMessage::ReadGlobal(global)); - } - RequestMessage::ReadMemory(memory_reference, byte_offset, byte_count) => { - let memory = read_memory(executor, memory_reference.as_str(), byte_offset, byte_count); - thirst.send(ResponseMessage::ReadMemory(memory)); - } - RequestMessage::Disassemble(memory_reference, instruction_offset, instruction_count) => { - let disassembled = disassemble(executor, memory_reference.as_str(), instruction_offset, instruction_count); - thirst.send(ResponseMessage::Disassemble(disassembled)); - } - RequestMessage::ReadUpvalues => { - let upvalues = read_upvalues(executor); - thirst.send(ResponseMessage::ReadUpvalues(upvalues)); - } - } - } - Err(e) => { - eprintln!("Error receiving message: {}", e); - } - } - } - }); - }); - - self.thread = Some(thread); - } - - pub fn restart(&mut self) { - self.thread.take().map(|thread| thread.join().unwrap()); - self.launch(); - } - - pub fn disconnect(&mut self) { - self.sender.take(); - } - - pub fn terminate(&mut self) { - self.thread.take().map(|thread| thread.join().unwrap()); - self.sender.take(); - } - - pub fn add_breakpoint(&mut self, chunk: String, line: usize) -> Result { - let Some(sender) = self.sender.as_ref() else { - return Err(Dead); - }; - - sender.send(RequestMessage::AddBreakpoint { chunk, line }); - - let Some(receiver) = self.receiver.as_ref() else { - return Err(Dead); - }; - - match receiver.recv() { - Ok(ResponseMessage::BreakpointAdded(id)) => Ok(id), - _ => Err(Dead) - } - } - - pub fn add_function_breakpoint(&mut self, name: String) -> Result, Dead> { - let Some(sender) = self.sender.as_ref() else { - return Err(Dead); - }; - - sender.send(RequestMessage::AddFunctionBreakpoint(name)); - - let Some(receiver) = self.receiver.as_ref() else { - return Err(Dead); - }; - - match receiver.recv() { - Ok(ResponseMessage::FunctionBreakpointAdded(id)) => Ok(id), - _ => Err(Dead) - } - } - - pub fn remove_breakpoint(&mut self, chunk: String, line: usize) -> Result<(), Dead> { - let Some(sender) = self.sender.as_ref() else { - return Err(Dead); - }; - - sender.send(RequestMessage::RemoveBreakpoint { chunk, line }); - - let Some(receiver) = self.receiver.as_ref() else { - return Err(Dead); - }; - - match receiver.recv() { - Ok(ResponseMessage::BreakpointRemoved) => Ok(()), - _ => Err(Dead) - } - } - - pub fn list_breakpoints(&self) -> Result>, Dead> { - let Some(sender) = self.sender.as_ref() else { - return Err(Dead); - }; - - sender.send(RequestMessage::ListBreakpoints); - - let Some(receiver) = self.receiver.as_ref() else { - return Err(Dead); - }; - - match receiver.recv() { - Ok(ResponseMessage::ListBreakpoints(list)) => Ok(list), - _ => Err(Dead) - } - } - - pub fn matches_breakpoint(&self, chunk: String, line: usize) -> Result, Dead> { - let Some(sender) = self.sender.as_ref() else { - return Err(Dead); - }; - - sender.send(RequestMessage::MatchesBreakpoint { chunk, line }); - - let Some(receiver) = self.receiver.as_ref() else { - return Err(Dead); - }; - - match receiver.recv() { - Ok(ResponseMessage::MatchesBreakpoint(matches)) => Ok(matches), - _ => Err(Dead) - } - } - - pub fn watch_register(&mut self, register: usize, mode: Option) -> Result<(), Dead> { - let Some(sender) = self.sender.as_ref() else { - return Err(Dead); - }; - - sender.send(RequestMessage::WatchRegister { register, mode }); - - let Some(receiver) = self.receiver.as_ref() else { - return Err(Dead); - }; - - match receiver.recv() { - Ok(ResponseMessage::WatchRegisterAdded) => Ok(()), - _ => Err(Dead) - } - } - - pub fn watch_global(&mut self, name: impl Into, mode: Option) -> Result<(), Dead> { - let Some(sender) = self.sender.as_ref() else { - return Err(Dead); - }; - - sender.send(RequestMessage::WatchGlobal { name: name.into(), mode }); - - let Some(receiver) = self.receiver.as_ref() else { - return Err(Dead); - }; - - match receiver.recv() { - Ok(ResponseMessage::WatchGlobalAdded) => Ok(()), - _ => Err(Dead) - } - } - - pub fn continue_run(&mut self) -> StopReason { - let Some(sender) = self.sender.as_ref() else { - return StopReason::Error; - }; - - sender.send(RequestMessage::ContinueRun); - - let Some(receiver) = self.receiver.as_ref() else { - return StopReason::Error; - }; - - match receiver.recv() { - Ok(ResponseMessage::ContinueRun(stop_reason)) => stop_reason, - _ => StopReason::Error, - } - } - - pub fn step_into(&mut self) -> StopReason { - let Some(sender) = self.sender.as_ref() else { - return StopReason::Error; - }; - - sender.send(RequestMessage::StepInto); - - let Some(receiver) = self.receiver.as_ref() else { - return StopReason::Error; - }; - - match receiver.recv() { - Ok(ResponseMessage::StepInto(stop_reason)) => stop_reason, - _ => StopReason::Error, - } - } - - pub fn step_over(&mut self) -> StopReason { - let Some(sender) = self.sender.as_ref() else { - return StopReason::Error; - }; - - sender.send(RequestMessage::StepOver); - - let Some(receiver) = self.receiver.as_ref() else { - return StopReason::Error; - }; - - match receiver.recv() { - Ok(ResponseMessage::StepOver(stop_reason)) => stop_reason, - _ => StopReason::Error, - } - } - - pub fn step_out(&mut self) -> StopReason { - let Some(sender) = self.sender.as_ref() else { - return StopReason::Error; - }; - - sender.send(RequestMessage::StepOut); - - let Some(receiver) = self.receiver.as_ref() else { - return StopReason::Error; - }; - - match receiver.recv() { - Ok(ResponseMessage::StepOut(stop_reason)) => stop_reason, - _ => StopReason::Error, - } - } - - pub fn backtrace(&mut self) -> Result, StopReason> { - let Some(sender) = self.sender.as_ref() else { - println!("no sender"); - return Err(StopReason::Error); - }; - - sender.send(RequestMessage::Backtrace); - - let Some(receiver) = self.receiver.as_ref() else { - println!("no receiver"); - return Err(StopReason::Error); - }; - - match receiver.recv() { - Ok(ResponseMessage::Backtrace(backtrace)) => backtrace, - other => { - println!("no backtrace: {:?}", other.unwrap()); - Err(StopReason::Error) - } - } - } - - pub fn stack_snapshot(&mut self) -> Result)>, StopReason> { - let Some(sender) = self.sender.as_ref() else { - return Err(StopReason::Error); - }; - - sender.send(RequestMessage::StackSnapshot); - - let Some(receiver) = self.receiver.as_ref() else { - return Err(StopReason::Error); - }; - - match receiver.recv() { - Ok(ResponseMessage::StackSnapshot(stack_snapshot)) => stack_snapshot, - _ => Err(StopReason::Error) - } - } - - pub fn read_register(&mut self, register: usize) -> Result, StopReason> { - let Some(sender) = self.sender.as_ref() else { - return Err(StopReason::Error); - }; - - sender.send(RequestMessage::ReadRegister(register)); - - let Some(receiver) = self.receiver.as_ref() else { - return Err(StopReason::Error); - }; - - match receiver.recv() { - Ok(ResponseMessage::ReadRegister(register)) => register, - _ => Err(StopReason::Error) - } - } - - pub fn read_upvalues(&mut self) -> Result>, StopReason> { - let Some(sender) = self.sender.as_ref() else { - return Err(StopReason::Error); - }; - - sender.send(RequestMessage::ReadUpvalues); - - let Some(receiver) = self.receiver.as_ref() else { - return Err(StopReason::Error); - }; - - match receiver.recv() { - Ok(ResponseMessage::ReadUpvalues(upvalues)) => upvalues, - _ => Err(StopReason::Error) - } - } - - pub fn read_registers(&mut self) -> Result>, StopReason> { - let Some(sender) = self.sender.as_ref() else { - return Err(StopReason::Error); - }; - - sender.send(RequestMessage::ReadRegisters); - - let Some(receiver) = self.receiver.as_ref() else { - return Err(StopReason::Error); - }; - - match receiver.recv() { - Ok(ResponseMessage::ReadRegisters(registers)) => registers, - _ => Err(StopReason::Error) - } - } - - pub fn read_global(&mut self, name: String) -> Result { - let Some(sender) = self.sender.as_ref() else { - return Err(StopReason::Error); - }; - - sender.send(RequestMessage::ReadGlobal(name)); - - let Some(receiver) = self.receiver.as_ref() else { - return Err(StopReason::Error); - }; - - match receiver.recv() { - Ok(ResponseMessage::ReadGlobal(global)) => Ok(global), - _ => Err(StopReason::Error) - } - } - - pub fn read_memory(&mut self, memory_reference: String, byte_offset: Option, byte_count: Option) -> Result>, StopReason> { - let Some(sender) = self.sender.as_ref() else { - return Err(StopReason::Error); - }; - - sender.send(RequestMessage::ReadMemory(memory_reference, byte_offset, byte_count)); - - let Some(receiver) = self.receiver.as_ref() else { - return Err(StopReason::Error); - }; - - match receiver.recv() { - Ok(ResponseMessage::ReadMemory(memory)) => Ok(memory), - _ => Err(StopReason::Error) - } - } - - pub fn disassemble(&mut self, memory_reference: String, instruction_offset: Option, instruction_count: Option) -> Result>, StopReason> { - let Some(sender) = self.sender.as_ref() else { - return Err(StopReason::Error); - }; - - sender.send(RequestMessage::Disassemble(memory_reference, instruction_offset, instruction_count)); - - let Some(receiver) = self.receiver.as_ref() else { - return Err(StopReason::Error); - }; - - match receiver.recv() { - Ok(ResponseMessage::Disassemble(disassemble)) => Ok(disassemble), - _ => Err(StopReason::Error) - } - } -} - pub struct Debugger { next_session: usize, sessions: Vec, @@ -1311,19 +43,27 @@ impl Debugger { } pub fn launch(&mut self, session_id: usize) { - self.sessions.get_mut(session_id).map(|session| session.launch()); + self.sessions + .get_mut(session_id) + .map(|session| session.launch()); } pub fn restart(&mut self, session_id: usize) { - self.sessions.get_mut(session_id).map(|session| session.restart()); + self.sessions + .get_mut(session_id) + .map(|session| session.restart()); } pub fn disconnect(&mut self, session_id: usize) { - self.sessions.get_mut(session_id).map(|session| session.disconnect()); + self.sessions + .get_mut(session_id) + .map(|session| session.disconnect()); } pub fn terminate(&mut self, session_id: usize) { - self.sessions.get_mut(session_id).map(|session| session.terminate()); + self.sessions + .get_mut(session_id) + .map(|session| session.terminate()); } pub fn add_session(&mut self, source: impl Into, path: impl Into) -> usize { @@ -1336,102 +76,169 @@ impl Debugger { id } - pub fn add_breakpoint(&mut self, session_id: usize, chunk: String, line: usize) -> Result { - self.sessions.get_mut(session_id).map(|session| session.add_breakpoint(chunk, line)) - .unwrap_or(Err(Dead)) + pub fn add_breakpoint( + &mut self, + session_id: usize, + chunk: String, + line: usize, + ) -> Result { + self.get_mut(session_id, |session| session.add_breakpoint(chunk, line)) + .ok_or(Error::SessionNotFound)? } - pub fn add_function_breakpoint(&mut self, session_id: usize, name: String) -> Result, Dead> { - self.sessions.get_mut(session_id).map(|session| session.add_function_breakpoint(name)) - .unwrap_or(Err(Dead)) + pub fn add_function_breakpoint( + &mut self, + session_id: usize, + name: String, + ) -> Result, Error> { + self.get_mut(session_id, |session| session.add_function_breakpoint(name)) + .ok_or(Error::SessionNotFound)? } - pub fn remove_breakpoint(&mut self, session_id: usize, chunk: String, line: usize) -> Result<(), Dead> { - self.sessions.get_mut(session_id).map(|session| session.remove_breakpoint(chunk, line)) - .unwrap_or(Err(Dead)) + pub fn remove_breakpoint( + &mut self, + session_id: usize, + chunk: String, + line: usize, + ) -> Result<(), Error> { + self.get_mut(session_id, |session| session.remove_breakpoint(chunk, line)) + .ok_or(Error::SessionNotFound)? } - pub fn list_breakpoints(&self, session_id: usize) -> Result>, Dead> { - self.sessions - .get(session_id) - .map(|session| session.list_breakpoints()) - .unwrap_or(Err(Dead)) + pub fn list_breakpoints( + &self, + session_id: usize, + ) -> Result>, Error> { + self.get(session_id, |session| session.list_breakpoints()) + .ok_or(Error::SessionNotFound)? } - pub fn matches_breakpoint(&self, session_id: usize, chunk: String, line: usize) -> Result, Dead> { - self.sessions - .get(session_id) - .map(|session| session.matches_breakpoint(chunk, line)) - .unwrap_or(Err(Dead)) + pub fn matches_breakpoint( + &self, + session_id: usize, + chunk: String, + line: usize, + ) -> Result, Error> { + self.get(session_id, |session| { + session.matches_breakpoint(chunk, line) + }) + .ok_or(Error::SessionNotFound)? } - pub fn watch_register(&mut self, session_id: usize, register: usize, mode: Option) -> Result<(), Dead> { - self.sessions.get_mut(session_id).map(|session| session.watch_register(register, mode)) - .unwrap_or(Err(Dead)) + pub fn watch_register( + &mut self, + session_id: usize, + register: usize, + mode: Option, + ) -> Result<(), Error> { + self.get_mut(session_id, |session| session.watch_register(register, mode)) + .ok_or(Error::SessionNotFound)? } - pub fn watch_global(&mut self, session_id: usize, name: impl Into, mode: Option) -> Result<(), Dead> { - self.sessions.get_mut(session_id).map(|session| session.watch_global(name, mode)) - .unwrap_or(Err(Dead)) + pub fn watch_global( + &mut self, + session_id: usize, + name: impl Into, + mode: Option, + ) -> Result<(), Error> { + self.get_mut(session_id, |session| session.watch_global(name, mode)) + .ok_or(Error::SessionNotFound)? } - pub fn continue_run(&mut self, session_id: usize) -> StopReason { - self.sessions.get_mut(session_id).map(|session| session.continue_run()) - .unwrap_or(StopReason::Error) + pub fn continue_run(&mut self, session_id: usize) -> Result { + self.get_mut(session_id, |session| session.continue_run()) + .ok_or(Error::SessionNotFound)? } - pub fn step_into(&mut self, session_id: usize) -> StopReason { - self.sessions.get_mut(session_id).map(|session| session.step_into()) - .unwrap_or(StopReason::Error) + pub fn step_into(&mut self, session_id: usize) -> Result { + self.get_mut(session_id, |session| session.step_into()) + .ok_or(Error::SessionNotFound)? } - pub fn step_over(&mut self, session_id: usize) -> StopReason { - self.sessions.get_mut(session_id).map(|session| session.step_over()) - .unwrap_or(StopReason::Error) + pub fn step_over(&mut self, session_id: usize) -> Result { + self.get_mut(session_id, |session| session.step_over()) + .ok_or(Error::SessionNotFound)? } - pub fn step_out(&mut self, session_id: usize) -> StopReason { - self.sessions.get_mut(session_id).map(|session| session.step_out()) - .unwrap_or(StopReason::Error) + pub fn step_out(&mut self, session_id: usize) -> Result { + self.get_mut(session_id, |session| session.step_out()) + .ok_or(Error::SessionNotFound)? } - pub fn backtrace(&mut self, session_id: usize) -> Result, StopReason> { - self.sessions.get_mut(session_id).map(|session| session.backtrace()) - .unwrap_or(Err(StopReason::Error)) + pub fn backtrace(&mut self, session_id: usize) -> Result, Error> { + self.get_mut(session_id, |session| session.backtrace()) + .ok_or(Error::SessionNotFound)? } - pub fn stack_snapshot(&mut self, session_id: usize) -> Result)>, StopReason> { - self.sessions.get_mut(session_id).map(|session| session.stack_snapshot()) - .unwrap_or(Err(StopReason::Error)) + pub fn stack_snapshot( + &mut self, + session_id: usize, + ) -> Result)>, Error> { + self.get_mut(session_id, |session| session.stack_snapshot()) + .ok_or(Error::SessionNotFound)? } - pub fn read_register(&mut self, session_id: usize, register: usize) -> Result, StopReason> { - self.sessions.get_mut(session_id).map(|session| session.read_register(register)) - .unwrap_or(Err(StopReason::Error)) + pub fn read_register( + &mut self, + session_id: usize, + register: usize, + ) -> Result, Error> { + self.get_mut(session_id, |session| session.read_register(register)) + .ok_or(Error::SessionNotFound)? } - pub fn read_upvalues(&mut self, session_id: usize) -> Result>, StopReason> { - self.sessions.get_mut(session_id).map(|session| session.read_upvalues()) - .unwrap_or(Err(StopReason::Error)) + pub fn read_upvalues(&mut self, session_id: usize) -> Result>, Error> { + self.get_mut(session_id, |session| session.read_upvalues()) + .ok_or(Error::SessionNotFound)? } - pub fn read_registers(&mut self, session_id: usize) -> Result>, StopReason> { - self.sessions.get_mut(session_id).map(|session| session.read_registers()) - .unwrap_or(Err(StopReason::Error)) - } + pub fn read_registers(&mut self, session_id: usize) -> Result>, Error> { + self.get_mut(session_id, |session| session.read_registers()) + .ok_or(Error::SessionNotFound)? + } - pub fn read_global(&mut self, session_id: usize, name: String) -> Result { - self.sessions.get_mut(session_id).map(|session| session.read_global(name)) - .unwrap_or(Err(StopReason::Error)) + pub fn read_global(&mut self, session_id: usize, name: String) -> Result { + self.get_mut(session_id, |session| session.read_global(name)) + .ok_or(Error::SessionNotFound)? + } + + pub fn read_memory( + &mut self, + session_id: usize, + memory_reference: String, + byte_offset: Option, + byte_count: Option, + ) -> Result>, Error> { + self.get_mut(session_id, |session| { + session.read_memory(memory_reference, byte_offset, byte_count) + }) + .ok_or(Error::SessionNotFound)? + } + + pub fn disassemble( + &mut self, + session_id: usize, + memory_reference: String, + instruction_offset: Option, + instruction_count: Option, + ) -> Result>, Error> { + self.get_mut(session_id, |session| { + session.disassemble(memory_reference, instruction_offset, instruction_count) + }) + .ok_or(Error::SessionNotFound)? } - pub fn read_memory(&mut self, session_id: usize, memory_reference: String, byte_offset: Option, byte_count: Option) -> Result>, StopReason> { - self.sessions.get_mut(session_id).map(|session| session.read_memory(memory_reference, byte_offset, byte_count)) - .unwrap_or(Err(StopReason::Error)) + fn get(&self, session_id: usize, f: F) -> Option + where + F: FnOnce(&Session) -> R, + { + self.sessions.get(session_id).map(f) } - pub fn disassemble(&mut self, session_id: usize, memory_reference: String, instruction_offset: Option, instruction_count: Option) -> Result>, StopReason> { - self.sessions.get_mut(session_id).map(|session| session.disassemble(memory_reference, instruction_offset, instruction_count)) - .unwrap_or(Err(StopReason::Error)) + fn get_mut(&mut self, session_id: usize, f: F) -> Option + where + F: FnOnce(&mut Session) -> R, + { + self.sessions.get_mut(session_id).map(f) } } diff --git a/debugger/src/session.rs b/debugger/src/session.rs new file mode 100644 index 00000000..e2821fd3 --- /dev/null +++ b/debugger/src/session.rs @@ -0,0 +1,637 @@ +use std::{ + collections::HashMap, + sync::mpsc::{Receiver, Sender}, + thread::JoinHandle, +}; + +mod debug; +mod request; +mod response; + +use piccolo::{Closure, Executor, Lua}; + +use crate::{ + breakpoints::Breakpoints, + watch::{WatchEntry, WatchSpec}, + Disassembled, Error, Location, StopReason, WatchMode, +}; + +use self::{request::Request, response::Response}; + +#[derive(Debug)] +pub struct Session { + id: usize, + source: String, + path: String, + thread: Option>, + sender: Option>, + receiver: Option>, +} + +impl<'gc> Session { + pub fn new(id: usize, source: impl Into, path: impl Into) -> Self { + Self { + id, + source: source.into(), + path: path.into(), + thread: None, + sender: None, + receiver: None, + } + } + + pub fn id(&self) -> usize { + self.id + } + + pub fn name(&self) -> &str { + &self.source + } + + pub fn source(&self) -> &str { + &self.source + } + + pub fn launch(&mut self) { + let (first, second) = std::sync::mpsc::channel(); + let (thirst, fourth) = std::sync::mpsc::channel(); + self.sender = Some(first); + self.receiver = Some(fourth); + + let source = self.source.as_bytes().to_vec(); + let path = self.path.to_owned(); + let thread = std::thread::spawn(move || { + let mut lua = Lua::full(); + lua.enter(|ctx| { + let mut breakpoints = Breakpoints::default(); + let mut watchpoints = Vec::new(); + let closure = Closure::load(ctx, Some(&path), &source).unwrap(); + let executor = Executor::start(ctx, closure.into(), ()); + + loop { + match second.recv() { + Ok(message) => match message { + Request::AddBreakpoint { chunk, line } => { + let id = breakpoints.add(chunk, line); + if let Err(_) = thirst.send(Response::BreakpointAdded(id)) { + break; + } + } + Request::RemoveBreakpoint { chunk, line } => { + breakpoints.remove(&chunk, line); + if let Err(_) = thirst.send(Response::BreakpointRemoved) { + break; + } + } + Request::AddFunctionBreakpoint(name) => { + let id = debug::resolve_function_breakpoint(ctx, name.as_str()) + .map(|(chunk, line)| breakpoints.add(chunk, line)); + if let Err(_) = thirst.send(Response::FunctionBreakpointAdded(id)) { + break; + } + } + Request::ListBreakpoints => { + let list = breakpoints.list(); + if let Err(_) = thirst.send(Response::ListBreakpoints(list)) { + break; + } + } + Request::MatchesBreakpoint { chunk, line } => { + let matches = breakpoints.matches(&chunk, line); + if let Err(_) = thirst.send(Response::MatchesBreakpoint(matches)) { + break; + } + } + Request::WatchRegister { register, mode } => { + watchpoints.push(WatchEntry { + spec: WatchSpec::Register(register), + last: None, + mode: mode.unwrap_or(WatchMode::Modify), + last_line_seen: None, + }); + if let Err(_) = thirst.send(Response::WatchRegisterAdded) { + break; + } + } + Request::WatchGlobal { name, mode } => { + watchpoints.push(WatchEntry { + spec: WatchSpec::Global(name), + last: None, + mode: mode.unwrap_or(WatchMode::Modify), + last_line_seen: None, + }); + if let Err(_) = thirst.send(Response::WatchGlobalAdded) { + break; + } + } + Request::ContinueRun => { + let stop_reason = debug::continue_run( + executor, + ctx, + &breakpoints, + &mut watchpoints, + ); + if let Err(_) = thirst.send(Response::ContinueRun(stop_reason)) { + break; + } + } + Request::StepInto => { + let stop_reason = + debug::step_into(executor, ctx, &breakpoints, &mut watchpoints); + if let Err(_) = thirst.send(Response::StepInto(stop_reason)) { + break; + } + } + Request::StepOver => { + let stop_reason = + debug::step_over(executor, ctx, &breakpoints, &mut watchpoints); + if let Err(_) = thirst.send(Response::StepOver(stop_reason)) { + break; + } + } + Request::StepOut => { + let stop_reason = + debug::step_out(executor, ctx, &breakpoints, &mut watchpoints); + if let Err(_) = thirst.send(Response::StepOut(stop_reason)) { + break; + } + } + Request::Backtrace => { + let backtrace = debug::backtrace(executor); + if let Err(_) = thirst.send(Response::Backtrace(backtrace)) { + break; + } + } + Request::StackSnapshot => { + let stack_snapshot = debug::stack_snapshot(executor); + if let Err(_) = thirst.send(Response::StackSnapshot(stack_snapshot)) + { + break; + } + } + Request::ReadRegister(register) => { + let register = debug::read_register(executor, register); + if let Err(_) = thirst.send(Response::ReadRegister(register)) { + break; + } + } + Request::ReadRegisters => { + let registers = debug::read_registers(executor); + if let Err(_) = thirst.send(Response::ReadRegisters(registers)) { + break; + } + } + Request::ReadGlobal(name) => { + let global = debug::read_global(ctx, name.as_str()); + if let Err(_) = thirst.send(Response::ReadGlobal(global)) { + break; + } + } + Request::ReadMemory(memory_reference, byte_offset, byte_count) => { + let memory = debug::read_memory( + executor, + memory_reference.as_str(), + byte_offset, + byte_count, + ); + if let Err(_) = thirst.send(Response::ReadMemory(memory)) { + break; + } + } + Request::Disassemble( + memory_reference, + instruction_offset, + instruction_count, + ) => { + let disassembled = debug::disassemble( + executor, + memory_reference.as_str(), + instruction_offset, + instruction_count, + ); + if let Err(_) = thirst.send(Response::Disassemble(disassembled)) { + break; + } + } + Request::ReadUpvalues => { + let upvalues = debug::read_upvalues(executor); + if let Err(_) = thirst.send(Response::ReadUpvalues(upvalues)) { + break; + } + } + }, + _ => break, + } + } + }); + }); + + self.thread = Some(thread); + } + + pub fn restart(&mut self) { + self.thread.take().map(|thread| thread.join().unwrap()); + self.sender.take(); + self.receiver.take(); + self.launch(); + } + + pub fn disconnect(&mut self) { + self.sender.take(); + } + + pub fn terminate(&mut self) { + self.thread.take().map(|thread| thread.join().unwrap()); + self.sender.take(); + } + + pub fn add_breakpoint(&mut self, chunk: String, line: usize) -> Result { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::AddBreakpoint { chunk, line }) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::BreakpointAdded(id)) => Ok(id), + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn add_function_breakpoint(&mut self, name: String) -> Result, Error> { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::AddFunctionBreakpoint(name)) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::FunctionBreakpointAdded(id)) => Ok(id), + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn remove_breakpoint(&mut self, chunk: String, line: usize) -> Result<(), Error> { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::RemoveBreakpoint { chunk, line }) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::BreakpointRemoved) => Ok(()), + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn list_breakpoints(&self) -> Result>, Error> { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::ListBreakpoints) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::ListBreakpoints(list)) => Ok(list), + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn matches_breakpoint(&self, chunk: String, line: usize) -> Result, Error> { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::MatchesBreakpoint { chunk, line }) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::MatchesBreakpoint(matches)) => Ok(matches), + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn watch_register( + &mut self, + register: usize, + mode: Option, + ) -> Result<(), Error> { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::WatchRegister { register, mode }) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::WatchRegisterAdded) => Ok(()), + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn watch_global( + &mut self, + name: impl Into, + mode: Option, + ) -> Result<(), Error> { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::WatchGlobal { + name: name.into(), + mode, + }) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::WatchGlobalAdded) => Ok(()), + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn continue_run(&mut self) -> Result { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::ContinueRun) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::ContinueRun(stop_reason)) => stop_reason, + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn step_into(&mut self) -> Result { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::StepInto) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::StepInto(stop_reason)) => stop_reason, + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn step_over(&mut self) -> Result { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::StepOver) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::StepOver(stop_reason)) => stop_reason, + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn step_out(&mut self) -> Result { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::StepOut) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::StepOut(stop_reason)) => stop_reason, + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn backtrace(&mut self) -> Result, Error> { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::Backtrace) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::Backtrace(backtrace)) => backtrace, + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn stack_snapshot(&mut self) -> Result)>, Error> { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::StackSnapshot) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::StackSnapshot(stack_snapshot)) => stack_snapshot, + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn read_register(&mut self, register: usize) -> Result, Error> { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::ReadRegister(register)) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::ReadRegister(register)) => register, + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn read_upvalues(&mut self) -> Result>, Error> { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::ReadUpvalues) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::ReadUpvalues(upvalues)) => upvalues, + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn read_registers(&mut self) -> Result>, Error> { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::ReadRegisters) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::ReadRegisters(registers)) => registers, + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn read_global(&mut self, name: String) -> Result { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::ReadGlobal(name)) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::ReadGlobal(global)) => Ok(global), + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn read_memory( + &mut self, + memory_reference: String, + byte_offset: Option, + byte_count: Option, + ) -> Result>, Error> { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::ReadMemory( + memory_reference, + byte_offset, + byte_count, + )) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::ReadMemory(memory)) => Ok(memory), + _ => Err(Error::UnexpectedResponse), + } + } + + pub fn disassemble( + &mut self, + memory_reference: String, + instruction_offset: Option, + instruction_count: Option, + ) -> Result>, Error> { + let Some(sender) = self.sender.as_ref() else { + return Err(Error::SenderDead); + }; + + sender + .send(Request::Disassemble( + memory_reference, + instruction_offset, + instruction_count, + )) + .map_err(|_| Error::ReceiverDead)?; + + let Some(receiver) = self.receiver.as_ref() else { + return Err(Error::ReceiverDead); + }; + + match receiver.recv() { + Ok(Response::Disassemble(disassemble)) => Ok(disassemble), + _ => Err(Error::UnexpectedResponse), + } + } +} diff --git a/debugger/src/session/debug.rs b/debugger/src/session/debug.rs new file mode 100644 index 00000000..32d0cfee --- /dev/null +++ b/debugger/src/session/debug.rs @@ -0,0 +1,765 @@ +use piccolo::{ + compiler::LineNumber, + opcode::OpCode, + thread::{vm::run_vm, Frame, LuaFrame}, + Context, Executor, ExecutorMode, Fuel, FunctionPrototype, String as LuaString, Value, +}; + +use crate::{ + breakpoints::Breakpoints, + watch::{WatchEntry, WatchSpec}, + Disassembled, Error, Location, StopReason, WatchMode, +}; + +fn current_location_from_state_in_thread<'gc>( + thread_state: &piccolo::thread::ThreadState<'gc>, +) -> Option { + match thread_state.frames.last() { + Some(Frame::Lua { closure, pc, .. }) => { + let proto = closure.prototype(); + let chunk_name = lua_string_to_string(proto.chunk_name); + let line = opcode_index_to_line(&proto.opcode_line_numbers, *pc); + Some(Location::new( + chunk_name, + proto.reference.map_strings(lua_string_to_string), + line, + )) + } + _ => None, + } +} + +fn opcode_index_to_line(op_lines: &[(usize, LineNumber)], pc: usize) -> usize { + if op_lines.is_empty() { + return 0; + } + match op_lines.binary_search_by_key(&pc, |(opi, _)| *opi) { + Ok(i) => op_lines[i].1 .0 as usize, + Err(0) => op_lines[0].1 .0 as usize, + Err(i) => op_lines[i - 1].1 .0 as usize, + } +} + +fn value_to_string<'gc>(value: Value<'gc>) -> String { + format!("{}", value.display()) +} + +fn lua_string_to_string(s: LuaString<'_>) -> String { + format!("{}", s.display_lossy()) +} + +fn current_stack_depth<'gc>(executor: Executor<'gc>) -> Result { + with_top_thread_state(executor, |state| state.frames.len()) +} + +fn stack_depth_and_location<'gc>( + executor: Executor<'gc>, +) -> Result<(usize, Option), Error> { + with_top_thread_state(executor, |state| { + let depth = state.frames.len(); + let loc = current_location_from_state_in_thread(state); + (depth, loc) + }) +} + +fn current_location<'gc>(executor: Executor<'gc>) -> Result, Error> { + with_top_thread_state(executor, |state| { + current_location_from_state_in_thread(state) + }) +} + +fn with_top_thread_state<'gc, R>( + executor: Executor<'gc>, + f: impl FnOnce(&piccolo::thread::ThreadState<'gc>) -> R, +) -> Result { + let guard = executor.0.borrow(); + let thread = *guard.thread_stack.last().ok_or(Error::ExecutorFinished)?; + drop(guard); + let thread_state = thread.into_inner().borrow(); + Ok(f(&thread_state)) +} + +fn with_top_thread_state_mut<'gc, R>( + executor: Executor<'gc>, + ctx: Context<'gc>, + f: impl FnOnce(piccolo::Thread<'gc>, &mut piccolo::thread::ThreadState<'gc>) -> R, +) -> Result { + let guard = executor.0.borrow(); + let top_thread = *guard.thread_stack.last().ok_or(Error::ExecutorFinished)?; + drop(guard); + let mut thread_state = top_thread.into_inner().borrow_mut(&ctx); + let r = f(top_thread, &mut thread_state); + drop(thread_state); + Ok(r) +} + +fn stop_if_breakpoint(location: &Location, breakpoints: &Breakpoints) -> Option { + let breakpoint_ids = breakpoints.matches(location.chunk(), location.line()); + if breakpoint_ids.is_empty() { + None + } else { + Some(StopReason::Breakpoint { + location: location.to_owned(), + breakpoint_ids, + }) + } +} + +fn eval_watch<'gc>(spec: &WatchSpec, executor: Executor<'gc>, ctx: Context<'gc>) -> Option { + match spec { + WatchSpec::Register(index) => with_top_thread_state(executor, |state| { + if let Some(Frame::Lua { + base, stack_size, .. + }) = state.frames.last() + { + let index = *index; + let base = *base; + let size = *stack_size; + if index < size { + return Some(state.stack[base + index].display().to_string()); + } + } + None + }) + .ok()?, + WatchSpec::Global(name) => { + let key = ctx.intern(name.as_bytes()); + Some(ctx.globals().get_value(ctx, key).display().to_string()) + } + } +} + +pub fn resolve_function_breakpoint<'gc>(ctx: Context<'gc>, name: &str) -> Option<(String, usize)> { + let env = ctx.globals(); + let mut segments = name.split('.'); + let first = segments.next()?; + let mut current = env.get_value(ctx, ctx.intern(first.as_bytes())); + for segment in segments { + match current { + Value::Table(t) => { + current = t.get_raw(Value::String(ctx.intern(segment.as_bytes()))); + } + _ => return None, + } + } + match current { + Value::Function(piccolo::Function::Closure(closure)) => { + let proto = closure.prototype(); + let chunk_name = lua_string_to_string(proto.chunk_name); + let first_line = if proto.opcode_line_numbers.is_empty() { + 0 + } else { + proto.opcode_line_numbers[0].1 .0 as usize + }; + Some((chunk_name, first_line)) + } + _ => None, + } +} + +fn describe_watch_change<'gc>( + spec: &WatchSpec, + old: &Option, + new: &Option, +) -> String { + let spec = match spec { + WatchSpec::Register(index) => format!("register[{}]", index), + WatchSpec::Global(name) => format!("global['{}']", name), + }; + format!( + "{}: {} -> {}", + spec, + old.as_ref().unwrap_or(&"".to_string()), + new.as_ref().unwrap_or(&"".to_string()) + ) +} + +fn poll_watchpoints<'gc>( + watchpoints: &mut Vec, + executor: Executor<'gc>, + ctx: Context<'gc>, +) -> Option { + for index in 0..watchpoints.len() { + let Some(entry) = watchpoints.get(index) else { + continue; + }; + let entry = entry.to_owned(); + let spec = match &entry.spec { + WatchSpec::Register(i) => WatchSpec::Register(*i), + WatchSpec::Global(n) => WatchSpec::Global(n.clone()), + }; + let last = entry.last; + + let current = eval_watch(&spec, executor, ctx); + let should_fire = match entry.mode { + WatchMode::Modify => match (last.as_ref(), current.as_ref()) { + (None, Some(_)) => true, + (Some(_), None) => true, + (Some(left), Some(right)) => left != right, + (None, None) => false, + }, + WatchMode::Access => { + if current.is_some() { + let current_line = current_location(executor) + .ok() + .flatten() + .map(|l| (l.chunk().to_owned(), l.line())); + if let (Some((chunk, line)), Some(entry)) = + (current_line, watchpoints.get(index)) + { + if entry.last_line_seen != Some((chunk.to_owned(), line)) { + if let Some(entry) = watchpoints.get_mut(index) { + entry.last_line_seen = Some((chunk.to_owned(), line)); + } + true + } else { + false + } + } else { + false + } + } else { + false + } + } + }; + if should_fire { + let message = format!( + "watchpoint hit: {}", + describe_watch_change(&spec, &last, ¤t) + ); + if let Some(entry) = watchpoints.get_mut(index) { + entry.last = current; + } + return Some(message); + } + if let Some(entry) = watchpoints.get_mut(index) { + entry.last = current; + } + } + None +} + +fn step_one<'gc>( + executor: Executor<'gc>, + ctx: Context<'gc>, + watchpoints: &mut Vec, +) -> Result { + const FUEL_NON_LUA_CALLBACK_STEP: i32 = 11; + const FUEL_NON_LUA_SEQUENCE_STEP: i32 = 7; + const FUEL_NON_LUA_ERROR_STEP: i32 = 3; + + match executor.mode() { + ExecutorMode::Stopped => return Err(Error::ExecutorFinished), + ExecutorMode::Result => return Err(Error::ExecutorFinished), + ExecutorMode::Suspended => return Ok(StopReason::Suspended), + ExecutorMode::Running => return Err(Error::ExecutorAlreadyRunning), + ExecutorMode::Normal => {} + } + + let (is_lua_top, non_lua_fuel) = + match with_top_thread_state(executor, |state| match state.frames.last() { + Some(Frame::Lua { .. }) => (true, 0), + Some(Frame::Callback { .. }) => (false, FUEL_NON_LUA_CALLBACK_STEP), + Some(Frame::Sequence { .. }) => (false, FUEL_NON_LUA_SEQUENCE_STEP), + Some(Frame::Error(_)) => (false, FUEL_NON_LUA_ERROR_STEP), + Some(Frame::Result { .. }) => (false, -1), + Some(Frame::WaitThread) | Some(Frame::Yielded) | Some(Frame::Start(_)) => (false, 3), + None => (false, -1), + }) { + Ok(value) => value, + Err(err) => return Err(err), + }; + + if non_lua_fuel == -1 { + return Err(Error::ExecutorFinished); + } + + if is_lua_top { + let _ = with_top_thread_state_mut(executor, ctx, |thread, state| { + let mut fuel = Fuel::with(1_000_000); + let lua_frame = LuaFrame { + state, + thread, + fuel: &mut fuel, + }; + let _ = run_vm(ctx, lua_frame, 1); + }); + } else { + let mut fuel = Fuel::with(non_lua_fuel); + let _ = executor.step(ctx, &mut fuel); + } + + let location = match current_location(executor) { + Ok(Some(location)) => location, + Ok(None) => return Err(Error::ExecutorFinished), + Err(err) => return Err(err), + }; + if let Some(hit) = poll_watchpoints(watchpoints, executor, ctx) { + return Ok(StopReason::Watchpoint(hit)); + } + Ok(StopReason::Step(location)) +} + +pub fn continue_run<'gc>( + executor: Executor<'gc>, + ctx: Context<'gc>, + breakpoints: &Breakpoints, + watchpoints: &mut Vec, +) -> Result { + loop { + match current_location(executor) { + Ok(Some(location)) => { + if let Some(stop_reason) = stop_if_breakpoint(&location, &breakpoints) { + return Ok(stop_reason); + } + } + Ok(None) => {} + Err(err) => return Err(err), + } + match step_one(executor, ctx, watchpoints)? { + StopReason::Step(_) => continue, + other => return Ok(other), + } + } +} + +pub fn step_into<'gc>( + executor: Executor<'gc>, + ctx: Context<'gc>, + breakpoints: &Breakpoints, + watchpoints: &mut Vec, +) -> Result { + let start_location = match current_location(executor) { + Ok(Some(location)) => location, + Ok(None) => return Err(Error::ExecutorFinished), + Err(err) => return Err(err), + }; + + if let Some(stop_reason) = stop_if_breakpoint(&start_location, breakpoints) { + return Ok(stop_reason); + } + + loop { + match step_one(executor, ctx, watchpoints)? { + StopReason::Step(location) => { + if let Some(stop_reason) = stop_if_breakpoint(&location, breakpoints) { + return Ok(stop_reason); + } + if location.line() != start_location.line() + || location.chunk() != start_location.chunk() + { + return Ok(StopReason::Step(location)); + } + } + StopReason::Watchpoint(hit) => return Ok(StopReason::Watchpoint(hit)), + other => return Ok(other), + } + } +} + +pub fn step_over<'gc>( + executor: Executor<'gc>, + ctx: Context<'gc>, + breakpoints: &Breakpoints, + watchpoints: &mut Vec, +) -> Result { + let (start_depth, start_location) = match stack_depth_and_location(executor) { + Ok((depth, Some(location))) => (depth, location), + Ok((_depth, None)) => return Err(Error::ExecutorFinished), + Err(err) => return Err(err), + }; + + if let Some(stop_reason) = stop_if_breakpoint(&start_location, breakpoints) { + return Ok(stop_reason); + } + + loop { + match step_one(executor, ctx, watchpoints)? { + StopReason::Step(location) => { + if let Some(stop_reason) = stop_if_breakpoint(&location, breakpoints) { + return Ok(stop_reason); + } + let depth = match current_stack_depth(executor) { + Ok(depth) => depth, + Err(err) => return Err(err), + }; + if depth <= start_depth + && (location.line() != start_location.line() + || location.chunk() != start_location.chunk()) + { + return Ok(StopReason::Step(location)); + } + } + StopReason::Watchpoint(hit) => return Ok(StopReason::Watchpoint(hit)), + other => return Ok(other), + } + } +} + +pub fn step_out<'gc>( + executor: Executor<'gc>, + ctx: Context<'gc>, + breakpoints: &Breakpoints, + watchpoints: &mut Vec, +) -> Result { + let start_depth = match current_stack_depth(executor) { + Ok(depth) => depth, + Err(err) => return Err(err), + }; + + loop { + match step_one(executor, ctx, watchpoints)? { + StopReason::Step(location) => { + if let Some(stop_reason) = stop_if_breakpoint(&location, breakpoints) { + return Ok(stop_reason); + } + let depth = match current_stack_depth(executor) { + Ok(depth) => depth, + Err(err) => return Err(err), + }; + if depth < start_depth { + return Ok(StopReason::Step(location)); + } + } + StopReason::Watchpoint(hit) => return Ok(StopReason::Watchpoint(hit)), + other => return Ok(other), + } + } +} + +pub fn backtrace<'gc>(executor: Executor<'gc>) -> Result, Error> { + with_top_thread_state(executor, |state| { + let mut out = Vec::with_capacity(state.frames.len()); + for frame in state.frames.iter() { + if let Frame::Lua { closure, pc, .. } = frame { + let proto = closure.prototype(); + let chunk_name = lua_string_to_string(proto.chunk_name); + let function_ref = proto.reference.map_strings(lua_string_to_string); + let line = opcode_index_to_line(&proto.opcode_line_numbers, *pc); + out.push(Location::new(chunk_name, function_ref, line)); + } + } + out + }) +} + +pub fn read_register<'gc>( + executor: Executor<'gc>, + register_index: usize, +) -> Result, Error> { + with_top_thread_state(executor, |state| { + if let Some(Frame::Lua { + base, stack_size, .. + }) = state.frames.last() + { + let base = *base; + let size = *stack_size; + if register_index < size { + return Some(value_to_string(state.stack[base + register_index])); + } + } + None + }) +} + +pub fn read_global<'gc>(ctx: Context<'gc>, name: &str) -> String { + let key = ctx.intern(name.as_bytes()); + value_to_string(ctx.globals().get_value(ctx, key)) +} + +pub fn read_registers<'gc>(executor: Executor<'gc>) -> Result>, Error> { + with_top_thread_state(executor, |state| { + if let Some(Frame::Lua { + base, stack_size, .. + }) = state.frames.last() + { + let base = *base; + let size = *stack_size; + return Some( + state.stack[base..base + size] + .iter() + .map(|v| value_to_string(*v)) + .collect(), + ); + } + None + }) +} + +pub fn read_upvalues<'gc>(executor: Executor<'gc>) -> Result>, Error> { + with_top_thread_state(executor, |state| { + if let Some(Frame::Lua { closure, .. }) = state.frames.last() { + let mut values = Vec::new(); + for &upvalue in closure.upvalues() { + if let piccolo::closure::UpValueState::Closed(value) = upvalue.get() { + values.push(value_to_string(value)); + } else { + values.push(String::new()); + } + } + return Some(values); + } + None + }) +} + +pub fn stack_snapshot<'gc>(executor: Executor<'gc>) -> Result)>, Error> { + with_top_thread_state(executor, |state| { + let mut out = Vec::with_capacity(state.frames.len()); + for frame in state.frames.iter() { + if let Frame::Lua { + base, + stack_size, + closure, + pc, + .. + } = frame + { + let proto = closure.prototype(); + let chunk_name = lua_string_to_string(proto.chunk_name); + let function_ref = proto.reference.map_strings(lua_string_to_string); + let line = opcode_index_to_line(&proto.opcode_line_numbers, *pc); + let location = Location::new(chunk_name, function_ref, line); + let base = *base; + let size = *stack_size; + out.push(( + location, + state.stack[base..base + size] + .iter() + .map(|v| value_to_string(*v)) + .collect(), + )); + } + } + out + }) +} + +fn disassemble_frame<'gc>( + executor: Executor<'gc>, + frame: usize, + instruction_offset: Option, + instruction_count: Option, +) -> Option> { + with_top_thread_state(executor, |state| { + if let Some(Frame::Lua { closure, pc, .. }) = if frame == usize::MAX { + state.frames.last() + } else { + state.frames.get(frame) + } { + let prototype = closure.prototype(); + let current_pc = *pc; + Some(disassemble_prototype_opcodes( + &prototype, + Some(current_pc), + instruction_offset, + instruction_count, + )) + } else { + None + } + }) + .ok()? +} + +fn disassemble_prototype<'gc>( + executor: Executor<'gc>, + prototype: &str, + instruction_offset: Option, + instruction_count: Option, +) -> Option> { + let (chunk, index) = prototype.split_once(':')?; + + let index: usize = index.parse().ok()?; + + with_top_thread_state(executor, |state| { + for frame in state.frames.iter() { + if let Frame::Lua { closure, .. } = frame { + let proto = closure.prototype(); + let frame_chunk = lua_string_to_string(proto.chunk_name); + + if frame_chunk == chunk { + if let Some(nested_proto) = proto.prototypes.get(index) { + return Some(disassemble_prototype_opcodes( + nested_proto, + None, + instruction_offset, + instruction_count, + )); + } + } + } + } + None + }) + .ok()? +} + +fn disassemble_prototype_opcodes<'gc>( + prototype: &FunctionPrototype, + current_pc: Option, + instruction_offset: Option, + instruction_count: Option, +) -> Vec { + let start_offset = instruction_offset.unwrap_or(0); + let max_count = instruction_count.unwrap_or(prototype.opcodes.len()); + + let end_offset = std::cmp::min(start_offset + max_count, prototype.opcodes.len()); + + let mut disassembled = Vec::with_capacity(end_offset - start_offset); + + for index in start_offset..end_offset { + if let Some(opcode) = prototype.opcodes.get(index) { + let operation = (*opcode).decode(); + let line = opcode_index_to_line(&prototype.opcode_line_numbers, index) + 1; + let symbol = if Some(index) == current_pc { + "=>" + } else { + " " + }; + disassembled.push(Disassembled { + symbol, + index, + line, + operation, + }); + } + } + + disassembled +} + +pub fn disassemble<'gc>( + executor: Executor<'gc>, + memory_reference: &str, + instruction_offset: Option, + instruction_count: Option, +) -> Option> { + let (ty, s) = memory_reference.split_once(':').unwrap_or(("frame", "")); + match ty { + "frame" => disassemble_frame( + executor, + if s.is_empty() { + usize::MAX + } else { + s.parse().ok()? + }, + instruction_offset, + instruction_count, + ), + "prototype" => disassemble_prototype(executor, s, instruction_offset, instruction_count), + _ => None, + } +} + +pub fn read_memory<'gc>( + executor: Executor<'gc>, + memory_reference: &str, + byte_offset: Option, + byte_count: Option, +) -> Option> { + let (ty, s) = memory_reference.split_once(':').unwrap_or(("frame", "")); + + match ty { + "frame" => read_frame_memory( + executor, + if s.is_empty() { + usize::MAX + } else { + s.parse().ok()? + }, + byte_offset, + byte_count, + ), + "prototype" => read_prototype_memory(executor, s, byte_offset, byte_count), + _ => None, + } +} + +fn read_frame_memory<'gc>( + executor: Executor<'gc>, + frame: usize, + byte_offset: Option, + byte_count: Option, +) -> Option> { + with_top_thread_state(executor, |state| { + if let Some(piccolo::thread::Frame::Lua { closure, .. }) = if frame == usize::MAX { + state.frames.last() + } else { + state.frames.get(frame) + } { + let prototype = closure.prototype(); + Some(read_opcodes(&prototype.opcodes, byte_offset, byte_count)) + } else { + None + } + }) + .ok()? +} + +fn read_prototype_memory<'gc>( + executor: Executor<'gc>, + prototype: &str, + byte_offset: Option, + byte_count: Option, +) -> Option> { + let (chunk, index) = prototype.split_once(':')?; + + let index: usize = index.parse().ok()?; + + with_top_thread_state(executor, |state| { + for frame in state.frames.iter() { + if let Frame::Lua { closure, .. } = frame { + let proto = closure.prototype(); + let frame_chunk = lua_string_to_string(proto.chunk_name); + + if frame_chunk == chunk { + if let Some(nested_proto) = proto.prototypes.get(index) { + return Some(read_opcodes(&nested_proto.opcodes, byte_offset, byte_count)); + } + } + } + } + None + }) + .ok()? +} + +fn read_opcodes<'gc>( + opcodes: &[OpCode], + byte_offset: Option, + byte_count: Option, +) -> Vec { + fn to_bytes(value: T) -> Vec { + let mut bytes = Vec::with_capacity(size_of::()); + + // SAFETY: We are copying the bytes of a plain-old-data (POD) type T into a Vec with enough capacity. + // The Vec is pre-allocated with the correct size, and we set the length after copying. + unsafe { + std::ptr::copy_nonoverlapping( + std::ptr::addr_of!(value).cast::(), + bytes.as_mut_ptr(), + size_of::(), + ); + bytes.set_len(size_of::()); + } + + bytes + } + + let mut raw = Vec::new(); + + for opcode in opcodes { + let bytes = to_bytes(*opcode); + raw.extend_from_slice(&bytes); + } + + let byte_offset = byte_offset.unwrap_or(0); + let byte_count = byte_count.unwrap_or(raw.len()); + + let start = std::cmp::min(byte_offset, raw.len()); + let end = std::cmp::min(start + byte_count, raw.len()); + + raw[start..end].to_vec() +} diff --git a/debugger/src/session/request.rs b/debugger/src/session/request.rs new file mode 100644 index 00000000..4670aabf --- /dev/null +++ b/debugger/src/session/request.rs @@ -0,0 +1,39 @@ +use crate::WatchMode; + +#[derive(Debug)] +pub enum Request { + AddBreakpoint { + chunk: String, + line: usize, + }, + AddFunctionBreakpoint(String), + RemoveBreakpoint { + chunk: String, + line: usize, + }, + ListBreakpoints, + MatchesBreakpoint { + chunk: String, + line: usize, + }, + WatchRegister { + register: usize, + mode: Option, + }, + WatchGlobal { + name: String, + mode: Option, + }, + ContinueRun, + StepInto, + StepOver, + StepOut, + Backtrace, + StackSnapshot, + ReadRegister(usize), + ReadUpvalues, + ReadRegisters, + ReadGlobal(String), + ReadMemory(String, Option, Option), + Disassemble(String, Option, Option), +} diff --git a/debugger/src/session/response.rs b/debugger/src/session/response.rs new file mode 100644 index 00000000..86ddd061 --- /dev/null +++ b/debugger/src/session/response.rs @@ -0,0 +1,26 @@ +use std::collections::HashMap; + +use crate::{Disassembled, Error, Location, StopReason}; + +#[derive(Debug)] +pub enum Response { + BreakpointAdded(usize), + BreakpointRemoved, + FunctionBreakpointAdded(Option), + ListBreakpoints(HashMap>), + MatchesBreakpoint(Vec), + WatchRegisterAdded, + WatchGlobalAdded, + ContinueRun(Result), + StepInto(Result), + StepOver(Result), + StepOut(Result), + Backtrace(Result, Error>), + StackSnapshot(Result)>, Error>), + ReadRegister(Result, Error>), + ReadUpvalues(Result>, Error>), + ReadRegisters(Result>, Error>), + ReadGlobal(String), + ReadMemory(Option>), + Disassemble(Option>), +} diff --git a/debugger/src/stop_reason.rs b/debugger/src/stop_reason.rs index ce06ae47..d4876530 100644 --- a/debugger/src/stop_reason.rs +++ b/debugger/src/stop_reason.rs @@ -8,7 +8,40 @@ pub enum StopReason { }, Step(Location), Watchpoint(String), - Finished, Suspended, - Error, +} + +impl StopReason { + pub const fn reason(&self) -> &'static str { + match self { + StopReason::Breakpoint { .. } => "breakpoint", + StopReason::Step { .. } => "step", + StopReason::Watchpoint { .. } => "watchpoint", + StopReason::Suspended => "suspended", + } + } +} + +impl std::fmt::Display for StopReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StopReason::Breakpoint { + location, + breakpoint_ids, + } => write!( + f, + "StopReason::Breakpoint {{ location: {:?}, breakpoint_ids: {:?} }}", + location, breakpoint_ids + ), + StopReason::Step(location) => { + write!(f, "StopReason::Step {{ location: {:?} }}", location) + } + StopReason::Watchpoint(watchpoint) => write!( + f, + "StopReason::Watchpoint {{ watchpoint: {:?} }}", + watchpoint + ), + StopReason::Suspended => write!(f, "StopReason::Suspended"), + } + } } diff --git a/debugger/src/watch/spec.rs b/debugger/src/watch/spec.rs index f75a7932..1175980e 100644 --- a/debugger/src/watch/spec.rs +++ b/debugger/src/watch/spec.rs @@ -1,4 +1,3 @@ - #[derive(Debug, Clone)] pub enum WatchSpec { Register(usize), From 91ce42d6cab154a6d88fb47c7d51fad694827391 Mon Sep 17 00:00:00 2001 From: reloginn Date: Sat, 23 Aug 2025 22:37:28 +0600 Subject: [PATCH 4/7] `fix`: bug in `step_one` (`max_instructions` was computed incorrectly), bug in `resolve_function_breakpoint` --- debugger/examples/simple_debug.rs | 18 +++-- debugger/src/session.rs | 6 +- debugger/src/session/debug.rs | 130 +++++++++++++++++++++++------- 3 files changed, 111 insertions(+), 43 deletions(-) diff --git a/debugger/examples/simple_debug.rs b/debugger/examples/simple_debug.rs index 2a5a2343..23bce2de 100644 --- a/debugger/examples/simple_debug.rs +++ b/debugger/examples/simple_debug.rs @@ -5,15 +5,14 @@ fn main() { let source = r#" x = 0 local outer = 10 - local function foo(n) + function foo(n) outer = outer + n x = x + n return outer + x end - local function bar() + function bar() local y = foo(1) - t.k = y return y end @@ -28,9 +27,9 @@ fn main() { dbg.add_breakpoint(session_id, "test.lua".to_string(), 3); dbg.add_breakpoint(session_id, "test.lua".to_string(), 9); - dbg.add_breakpoint(session_id, "test.lua".to_string(), 13); + dbg.add_breakpoint(session_id, "test.lua".to_string(), 7); - dbg.add_function_breakpoint(session_id, "foo".to_string()); + dbg.add_function_breakpoint(session_id, "bar".to_string()); println!("Breakpoints:"); for (src, lines) in dbg.list_breakpoints(session_id).unwrap() { @@ -39,7 +38,12 @@ fn main() { println!(" {src}: {:?}", v); } - dbg.remove_breakpoint(session_id, "test.lua".to_string(), 9); + dbg.remove_breakpoint(session_id, "test.lua".to_string(), 7); + + println!( + "breakpoints: {:?}", + dbg.list_breakpoints(session_id).unwrap() + ); dbg.watch_global(session_id, "x".to_string(), None); @@ -104,7 +108,7 @@ fn main() { } } - println!("Step over…"); + println!("Step over..."); stop = dbg.step_over(session_id); println!("Stopped: {:?}", stop); diff --git a/debugger/src/session.rs b/debugger/src/session.rs index e2821fd3..e81a1c99 100644 --- a/debugger/src/session.rs +++ b/debugger/src/session.rs @@ -48,10 +48,6 @@ impl<'gc> Session { &self.source } - pub fn source(&self) -> &str { - &self.source - } - pub fn launch(&mut self) { let (first, second) = std::sync::mpsc::channel(); let (thirst, fourth) = std::sync::mpsc::channel(); @@ -84,7 +80,7 @@ impl<'gc> Session { } } Request::AddFunctionBreakpoint(name) => { - let id = debug::resolve_function_breakpoint(ctx, name.as_str()) + let id = debug::resolve_function_breakpoint(executor, ctx, name.as_str()) .map(|(chunk, line)| breakpoints.add(chunk, line)); if let Err(_) = thirst.send(Response::FunctionBreakpointAdded(id)) { break; diff --git a/debugger/src/session/debug.rs b/debugger/src/session/debug.rs index 32d0cfee..f9352bc0 100644 --- a/debugger/src/session/debug.rs +++ b/debugger/src/session/debug.rs @@ -40,6 +40,30 @@ fn opcode_index_to_line(op_lines: &[(usize, LineNumber)], pc: usize) -> usize { } } +/// Calculates how many VM instructions remain in the current source line starting at `pc`. +/// This uses the prototype's `opcode_line_numbers` table which stores boundaries where the +/// line number changes. +fn instructions_to_end_of_line(prototype: &FunctionPrototype, pc: usize) -> u32 { + let op_lines = &prototype.opcode_line_numbers; + if op_lines.is_empty() { + return 1; + } + + let seg_index = match op_lines.binary_search_by_key(&pc, |(opi, _)| *opi) { + Ok(i) => i, + Err(0) => 0, + Err(i) => i - 1, + }; + + let end_op_index = op_lines + .get(seg_index + 1) + .map(|(opi, _)| *opi) + .unwrap_or(prototype.opcodes.len()); + + let remaining = end_op_index.saturating_sub(pc); + u32::try_from(remaining.max(1)).unwrap_or(1) +} + fn value_to_string<'gc>(value: Value<'gc>) -> String { format!("{}", value.display()) } @@ -129,11 +153,63 @@ fn eval_watch<'gc>(spec: &WatchSpec, executor: Executor<'gc>, ctx: Context<'gc>) } } -pub fn resolve_function_breakpoint<'gc>(ctx: Context<'gc>, name: &str) -> Option<(String, usize)> { +pub fn resolve_function_breakpoint<'gc>( + executor: Executor<'gc>, + ctx: Context<'gc>, + name: &str, +) -> Option<(String, usize)> { + fn find_named<'gc>( + proto: &FunctionPrototype<'gc>, + target: &str, + ) -> Option<(String, usize)> { + for child in proto.prototypes.iter() { + match child.reference { + piccolo::compiler::FunctionRef::Named(n, _) => { + let child_name = lua_string_to_string(n); + if child_name == target { + let chunk_name = lua_string_to_string(child.chunk_name); + let first_line = if child.opcode_line_numbers.is_empty() { + 0 + } else { + child.opcode_line_numbers[0].1 .0 as usize + }; + return Some((chunk_name, first_line)); + } + } + _ => {} + } + if let Some(found) = find_named(&*child, target) { + return Some(found); + } + } + None + } + + let last_segment = name.rsplit('.').next().unwrap_or(name); + + if let Ok(found) = with_top_thread_state(executor, |state| match state.frames.last() { + Some(Frame::Lua { closure, .. }) => { + let root = closure.prototype(); + find_named(&root, last_segment) + } + Some(Frame::Start(func)) => match func { + piccolo::Function::Closure(c) => { + let root = c.prototype(); + find_named(&root, last_segment) + } + _ => None, + }, + _ => None, + }) { + if let Some(pair) = found { + return Some(pair); + } + } + let env = ctx.globals(); let mut segments = name.split('.'); let first = segments.next()?; - let mut current = env.get_value(ctx, ctx.intern(first.as_bytes())); + let mut current = env.get_value(ctx, first.to_string()); for segment in segments { match current { Value::Table(t) => { @@ -142,18 +218,17 @@ pub fn resolve_function_breakpoint<'gc>(ctx: Context<'gc>, name: &str) -> Option _ => return None, } } - match current { - Value::Function(piccolo::Function::Closure(closure)) => { - let proto = closure.prototype(); - let chunk_name = lua_string_to_string(proto.chunk_name); - let first_line = if proto.opcode_line_numbers.is_empty() { - 0 - } else { - proto.opcode_line_numbers[0].1 .0 as usize - }; - Some((chunk_name, first_line)) - } - _ => None, + if let Value::Function(piccolo::Function::Closure(closure)) = current { + let proto = closure.prototype(); + let chunk_name = lua_string_to_string(proto.chunk_name); + let first_line = if proto.opcode_line_numbers.is_empty() { + 0 + } else { + proto.opcode_line_numbers[0].1 .0 as usize + }; + Some((chunk_name, first_line)) + } else { + None } } @@ -277,13 +352,19 @@ fn step_one<'gc>( if is_lua_top { let _ = with_top_thread_state_mut(executor, ctx, |thread, state| { + let mut max_instructions: u32 = 1; + if let Some(Frame::Lua { closure, pc, .. }) = state.frames.last() { + let proto = closure.prototype(); + max_instructions = instructions_to_end_of_line(&proto, *pc); + } + let mut fuel = Fuel::with(1_000_000); let lua_frame = LuaFrame { state, thread, fuel: &mut fuel, }; - let _ = run_vm(ctx, lua_frame, 1); + let _ = run_vm(ctx, lua_frame, max_instructions); }); } else { let mut fuel = Fuel::with(non_lua_fuel); @@ -308,17 +389,12 @@ pub fn continue_run<'gc>( watchpoints: &mut Vec, ) -> Result { loop { - match current_location(executor) { - Ok(Some(location)) => { - if let Some(stop_reason) = stop_if_breakpoint(&location, &breakpoints) { + match step_one(executor, ctx, watchpoints)? { + StopReason::Step(location) => { + if let Some(stop_reason) = stop_if_breakpoint(&location, breakpoints) { return Ok(stop_reason); } } - Ok(None) => {} - Err(err) => return Err(err), - } - match step_one(executor, ctx, watchpoints)? { - StopReason::Step(_) => continue, other => return Ok(other), } } @@ -336,10 +412,6 @@ pub fn step_into<'gc>( Err(err) => return Err(err), }; - if let Some(stop_reason) = stop_if_breakpoint(&start_location, breakpoints) { - return Ok(stop_reason); - } - loop { match step_one(executor, ctx, watchpoints)? { StopReason::Step(location) => { @@ -370,10 +442,6 @@ pub fn step_over<'gc>( Err(err) => return Err(err), }; - if let Some(stop_reason) = stop_if_breakpoint(&start_location, breakpoints) { - return Ok(stop_reason); - } - loop { match step_one(executor, ctx, watchpoints)? { StopReason::Step(location) => { From df01f2a2e17bb50203c4908e28eb089eb80826a2 Mon Sep 17 00:00:00 2001 From: reloginn Date: Sun, 24 Aug 2025 00:49:43 +0600 Subject: [PATCH 5/7] `feature`: implemented set* in `Adapter` --- debugger/examples/simple_debug.rs | 100 +++++------ debugger/src/adapter.rs | 286 ++++++++++++++++++++++++++++-- debugger/src/error.rs | 2 +- debugger/src/lib.rs | 8 +- debugger/src/session.rs | 8 +- debugger/src/session/debug.rs | 5 +- 6 files changed, 332 insertions(+), 77 deletions(-) diff --git a/debugger/examples/simple_debug.rs b/debugger/examples/simple_debug.rs index 23bce2de..612a58fa 100644 --- a/debugger/examples/simple_debug.rs +++ b/debugger/examples/simple_debug.rs @@ -1,7 +1,6 @@ -use piccolo::Value; -use piccolo_debugger::Debugger; +use piccolo_debugger::{Debugger, Error}; -fn main() { +fn main() -> Result<(), Error> { let source = r#" x = 0 local outer = 10 @@ -20,107 +19,102 @@ fn main() { "#; let path = "test.lua"; - let mut dbg = Debugger::new(); - let session_id = dbg.add_session(source, path); + let mut debugger = Debugger::new(); + let session_id = debugger.add_session(source, path); - dbg.launch(session_id); + debugger.launch(session_id); - dbg.add_breakpoint(session_id, "test.lua".to_string(), 3); - dbg.add_breakpoint(session_id, "test.lua".to_string(), 9); - dbg.add_breakpoint(session_id, "test.lua".to_string(), 7); + debugger.add_breakpoint(session_id, "test.lua".to_string(), 3)?; + debugger.add_breakpoint(session_id, "test.lua".to_string(), 9)?; + debugger.add_breakpoint(session_id, "test.lua".to_string(), 7)?; - dbg.add_function_breakpoint(session_id, "bar".to_string()); + debugger.add_function_breakpoint(session_id, "bar".to_string())?; println!("Breakpoints:"); - for (src, lines) in dbg.list_breakpoints(session_id).unwrap() { + for (source, lines) in debugger.list_breakpoints(session_id)? { let mut v: Vec = lines.iter().copied().collect(); v.sort_unstable(); - println!(" {src}: {:?}", v); + println!(" {source}: {:?}", v); } - dbg.remove_breakpoint(session_id, "test.lua".to_string(), 7); + debugger.remove_breakpoint(session_id, "test.lua".to_string(), 7)?; - println!( - "breakpoints: {:?}", - dbg.list_breakpoints(session_id).unwrap() - ); + println!("breakpoints: {:?}", debugger.list_breakpoints(session_id)?); - dbg.watch_global(session_id, "x".to_string(), None); + debugger.watch_global(session_id, "x".to_string(), None)?; println!("Continue…"); - let mut stop = dbg.continue_run(session_id); + let mut stop = debugger.continue_run(session_id)?; println!("Stopped: {:?}", stop); - let bt = dbg.backtrace(session_id); + let bt = debugger.backtrace(session_id)?; println!("Backtrace:"); - for (i, loc) in bt.unwrap().iter().enumerate() { + for (index, location) in bt.iter().enumerate() { println!( - " #{i} {}:{} {}", - loc.chunk(), - loc.line(), - loc.function_ref() + " #{index} {}:{} {}", + location.chunk(), + location.line(), + location.function_ref() ); } - if let Some(dis) = dbg - .disassemble(session_id, "".to_string(), None, None) - .unwrap() + if let Some(disassembled_instructions) = + debugger.disassemble(session_id, "".to_string(), None, None)? { println!("Disassembly (current function):"); - for l in dis.iter().take(10) { - println!(" {l:?}"); + for instruction in disassembled_instructions.iter().take(10) { + println!(" {instruction:?}"); } } - if let Some(readed) = dbg - .read_memory(session_id, "".to_string(), None, None) - .unwrap() - { - println!("Readed: {:?}", readed); + if let Some(readed_memory) = debugger.read_memory(session_id, "".to_string(), None, None)? { + println!("Readed: {:?}", readed_memory); } - if let Some(regs) = dbg.read_registers(session_id).unwrap() { - println!("Top frame registers ({}):", regs.len()); - for (i, v) in regs.iter().enumerate() { - println!(" r{i} = {}", v); + if let Some(registers) = debugger.read_registers(session_id)? { + println!("Top frame registers ({}):", registers.len()); + for (index, value) in registers.iter().enumerate() { + println!(" r{index} = {}", value); } } println!("Stack snapshot (before stepping):"); - for (loc, regs) in dbg.stack_snapshot(session_id).unwrap() { + for (location, registers) in debugger.stack_snapshot(session_id)? { println!( " {}:{} {} ({} regs)", - loc.chunk(), - loc.line(), - loc.function_ref(), - regs.len() + location.chunk(), + location.line(), + location.function_ref(), + registers.len() ); } println!("Step into…"); - stop = dbg.step_into(session_id); + stop = debugger.step_into(session_id)?; println!("Stopped: {:?}", stop); - if let Some(ups) = dbg.read_upvalues(session_id).unwrap() { - println!("Upvalues ({}):", ups.len()); - for (i, v) in ups.iter().enumerate() { - println!(" uv{i} = {}", v); + if let Some(upvalues) = debugger.read_upvalues(session_id)? { + println!("Upvalues ({}):", upvalues.len()); + for (index, value) in upvalues.iter().enumerate() { + println!(" uv{index} = {}", value); } } println!("Step over..."); - stop = dbg.step_over(session_id); + stop = debugger.step_over(session_id)?; println!("Stopped: {:?}", stop); println!("Step out…"); - stop = dbg.step_out(session_id); + stop = debugger.step_out(session_id)?; println!("Stopped: {:?}", stop); println!("Continue again…"); - stop = dbg.continue_run(session_id); + stop = debugger.continue_run(session_id)?; println!("Stopped: {:?}", stop); - if let Some(v0) = dbg.read_register(session_id, 0).unwrap() { + if let Some(v0) = debugger.read_register(session_id, 0)? { println!("r0 = {}", v0); } + + Ok(()) } diff --git a/debugger/src/adapter.rs b/debugger/src/adapter.rs index 1db91081..c332fed2 100644 --- a/debugger/src/adapter.rs +++ b/debugger/src/adapter.rs @@ -2,7 +2,7 @@ use std::sync::mpsc::{Receiver, Sender}; use base64::Engine; -use crate::StopReason; +use crate::{watch::WatchSpec, StopReason, WatchMode}; use super::{Debugger, Error}; @@ -55,7 +55,9 @@ pub enum SteppingGranularity {} pub struct StepOutArguments { thread_id: usize, + #[allow(dead_code)] single_thread: Option, + #[allow(dead_code)] granularity: Option, } @@ -65,17 +67,22 @@ pub struct StepOutResponse; pub struct StepInArguments { thread_id: usize, + #[allow(dead_code)] single_thread: Option, + #[allow(dead_code)] target_id: Option, + #[allow(dead_code)] granularity: Option, } pub struct StepInResponse; +#[allow(dead_code)] pub struct StepInTargetsArguments { frame_id: usize, } +#[allow(dead_code)] pub struct StepInTarget { id: usize, label: String, @@ -85,6 +92,7 @@ pub struct StepInTarget { end_column: Option, } +#[allow(dead_code)] pub struct StepInTargetsResponse { targets: Vec, } @@ -93,14 +101,19 @@ pub struct StepInTargetsResponse { pub struct Source { name: Option, path: Option, - source_reference: Option, + #[allow(dead_code)] presentation_hint: Option, + #[allow(dead_code)] origin: Option, + #[allow(dead_code)] sources: Option>, + #[allow(dead_code)] adapter_data: Option, + #[allow(dead_code)] checksums: Option>, } +#[allow(dead_code)] pub struct SourceBreakpoint { line: usize, column: Option, @@ -113,10 +126,12 @@ pub struct SourceBreakpoint { pub struct SetBreakpointsArguments { source: Source, breakpoints: Option>, - lines: Option>, + #[allow(dead_code)] source_modified: Option, } +#[allow(dead_code)] +#[derive(Default)] pub struct Breakpoint { id: Option, verified: bool, @@ -131,6 +146,7 @@ pub struct Breakpoint { reason: Option, } +#[allow(dead_code)] pub struct SetBreakpointsResponse { breakpoints: Vec, } @@ -144,7 +160,9 @@ pub enum DataBreakpointAccessType { pub struct DataBreakpoint { data_id: String, access_type: DataBreakpointAccessType, + #[allow(dead_code)] condition: Option, + #[allow(dead_code)] hit_condition: Option, } @@ -152,13 +170,16 @@ pub struct SetDataBreakpointsArguments { breakpoints: Vec, } +#[allow(dead_code)] pub struct SetDataBreakpointsResponse { breakpoints: Vec, } pub struct FunctionBreakpoint { name: String, + #[allow(dead_code)] condition: Option, + #[allow(dead_code)] hit_condition: Option, } @@ -166,10 +187,12 @@ pub struct SetFunctionBreakpointsArguments { breakpoints: Vec, } +#[allow(dead_code)] pub struct SetFunctionBreakpointsResponse { breakpoints: Vec, } +#[allow(dead_code)] pub struct InstructionBreakpoint { instruction_reference: String, offset: Option, @@ -182,10 +205,12 @@ pub struct SetInstructionBreakpointsArguments { breakpoints: Vec, } +#[allow(dead_code)] pub struct SetInstructionBreakpointsResponse { breakpoints: Vec, } +#[allow(dead_code)] pub struct StackTraceFormat { parameters: Option, parameter_types: Option, @@ -200,14 +225,17 @@ pub struct StackTraceArguments { thread_id: usize, start_frame: Option, levels: Option, + #[allow(dead_code)] format: Option, } +#[allow(dead_code)] pub struct StackTraceResponse { stack_frames: Vec, total_frames: usize, } +#[allow(dead_code)] pub struct StackFrame { id: usize, name: String, @@ -229,6 +257,7 @@ pub struct ReadMemoryArguments { count: Option, } +#[allow(dead_code)] pub struct ReadMemoryResponse { address: String, unreadable_bytes: Option, @@ -238,12 +267,15 @@ pub struct ReadMemoryResponse { pub struct DisassembleArguments { thread_id: Option, memory_reference: String, + #[allow(dead_code)] offset: Option, instruction_offset: Option, instruction_count: Option, + #[allow(dead_code)] resolve_symbols: Option, } +#[allow(dead_code)] pub struct DisassembledInstruction { address: String, instruction_bytes: Option, @@ -257,19 +289,23 @@ pub struct DisassembledInstruction { presentation_hint: Option, } +#[allow(dead_code)] pub struct DisassembleResponse { instructions: Vec, } pub struct ContinueArguments { thread_id: usize, + #[allow(dead_code)] single_thread: Option, } +#[allow(dead_code)] pub struct ContinueResponse { all_threads_continued: Option, } +#[allow(dead_code)] pub struct PauseArguments { thread_id: usize, } @@ -284,6 +320,7 @@ pub struct BreakpointLocationsArguments { end_column: Option, } +#[allow(dead_code)] pub struct BreakpointLocation { line: usize, column: Option, @@ -291,6 +328,7 @@ pub struct BreakpointLocation { end_column: Option, } +#[allow(dead_code)] pub struct BreakpointLocationsResponse { breakpoints: Vec, } @@ -325,11 +363,13 @@ pub struct TerminateThreadsArguments { pub struct TerminateThreadsResponse; +#[allow(dead_code)] pub struct Thread { id: usize, name: String, } +#[allow(dead_code)] pub struct ThreadsResponse { threads: Vec, } @@ -653,45 +693,260 @@ impl Adapter { &mut self, arguments: BreakpointLocationsArguments, ) -> BreakpointLocationsResponse { - BreakpointLocationsResponse { - breakpoints: vec![], - } + let source_path = arguments + .source + .path + .as_ref() + .or(arguments.source.name.as_ref()); + + let has_session = source_path + .map(|path| self.debugger.map_by_name(path).is_some()) + .unwrap_or(false); + + let breakpoints = if has_session { + vec![BreakpointLocation { + line: arguments.line, + column: arguments.column, + end_line: arguments.end_line, + end_column: arguments.end_column, + }] + } else { + vec![] + }; + + BreakpointLocationsResponse { breakpoints } } pub fn set_breakpoints( &mut self, arguments: SetBreakpointsArguments, ) -> SetBreakpointsResponse { - SetBreakpointsResponse { - breakpoints: vec![], + let Some(path) = arguments.source.path.to_owned() else { + return SetBreakpointsResponse { + breakpoints: vec![Breakpoint { + id: None, + verified: false, + message: Some("source path is required".to_string()), + ..Default::default() + }], + }; + }; + + let Some(session_id) = self.debugger.map_by_name(&path) else { + let breakpoints = arguments + .breakpoints + .unwrap_or_default() + .into_iter() + .map(|bp| Breakpoint { + id: None, + verified: false, + message: Some("source path not found in any session".to_string()), + source: Some(arguments.source.clone()), + line: Some(bp.line), + ..Default::default() + }) + .collect(); + + return SetBreakpointsResponse { breakpoints }; + }; + + let mut breakpoints = Vec::with_capacity( + arguments + .breakpoints + .as_ref() + .map(|breakpoints| breakpoints.len()) + .unwrap_or(0), + ); + for breakpoint in arguments.breakpoints.unwrap_or_default() { + let Ok(id) = self + .debugger + .add_breakpoint(session_id, path.clone(), breakpoint.line) + else { + breakpoints.push(Breakpoint { + id: None, + verified: false, + message: Some(format!( + "failed to add breakpoint at line {}", + breakpoint.line + )), + ..Default::default() + }); + continue; + }; + breakpoints.push(Breakpoint { + id: Some(id), + verified: true, + message: None, + source: Some(arguments.source.clone()), + line: Some(breakpoint.line), + column: None, + end_line: None, + end_column: None, + instruction_reference: None, + offset: None, + reason: None, + }); } + SetBreakpointsResponse { breakpoints } } pub fn set_data_breakpoints( &mut self, arguments: SetDataBreakpointsArguments, ) -> SetDataBreakpointsResponse { - SetDataBreakpointsResponse { - breakpoints: vec![], + fn parse_data_id(s: &str) -> WatchSpec { + let lower = s.to_ascii_lowercase(); + if let Some(register) = lower + .strip_prefix("register:") + .or(lower.strip_prefix("reg:")) + { + let register = register.trim().parse::().unwrap_or_default(); + + return WatchSpec::Register(register); + } + + if let Some(register) = lower.strip_prefix("r") { + if let Ok(register) = register.trim().parse::() { + return WatchSpec::Register(register); + } + } + + WatchSpec::Global(s.to_string()) } + + let mut breakpoints = Vec::with_capacity(arguments.breakpoints.len()); + for breakpoint in arguments.breakpoints.into_iter() { + let mode = match breakpoint.access_type { + DataBreakpointAccessType::Read => Some(WatchMode::Access), + DataBreakpointAccessType::Write => Some(WatchMode::Modify), + DataBreakpointAccessType::ReadWrite => Some(WatchMode::Access), + }; + + match parse_data_id(&breakpoint.data_id) { + WatchSpec::Register(register) => { + let verified = self + .debugger + .sessions + .iter_mut() + .map(|session| session.watch_register(register, mode).is_ok()) + .collect::>(); + if verified.iter().all(|value| !value) { + breakpoints.push(Breakpoint { + id: None, + verified: false, + message: Some(format!("failed to watch register:{}", register)), + ..Default::default() + }); + } else { + breakpoints.push(Breakpoint { + id: None, + verified: true, + message: Some(format!("watch register:{}", register)), + ..Default::default() + }); + } + } + WatchSpec::Global(name) => { + let verified = self + .debugger + .sessions + .iter_mut() + .map(|session| session.watch_global(name.clone(), mode).is_ok()) + .collect::>(); + if verified.iter().all(|value| !value) { + breakpoints.push(Breakpoint { + id: None, + verified: false, + message: Some(format!("failed to watch global:{}", name)), + ..Default::default() + }); + } else { + breakpoints.push(Breakpoint { + id: None, + verified: true, + message: Some(format!("watch global:{}", name)), + ..Default::default() + }); + } + } + } + } + + SetDataBreakpointsResponse { breakpoints } } pub fn set_function_breakpoints( &mut self, arguments: SetFunctionBreakpointsArguments, ) -> SetFunctionBreakpointsResponse { - SetFunctionBreakpointsResponse { - breakpoints: vec![], + let mut breakpoints = Vec::with_capacity(arguments.breakpoints.len()); + for breakpoint in arguments.breakpoints.into_iter() { + let results: Vec<_> = self + .debugger + .sessions + .iter_mut() + .map(|session| session.add_function_breakpoint(breakpoint.name.clone())) + .collect(); + + if results.iter().all(|result| result.is_err()) { + breakpoints.push(Breakpoint { + id: None, + verified: false, + message: Some(format!( + "function '{}' not found in any session", + breakpoint.name + )), + ..Default::default() + }); + continue; + } + + let first_success = results.iter().find_map(|result| result.ok().flatten()); + + match first_success { + Some(id) => { + breakpoints.push(Breakpoint { + id: Some(id), + verified: true, + message: None, + ..Default::default() + }); + } + None => { + breakpoints.push(Breakpoint { + id: None, + verified: false, + message: Some(format!("function '{}' not found", breakpoint.name)), + ..Default::default() + }); + } + } } + SetFunctionBreakpointsResponse { breakpoints } } pub fn set_instruction_breakpoints( &mut self, arguments: SetInstructionBreakpointsArguments, ) -> SetInstructionBreakpointsResponse { - SetInstructionBreakpointsResponse { - breakpoints: vec![], - } + let breakpoints = arguments + .breakpoints + .into_iter() + .map(|_| Breakpoint { + id: None, + verified: false, + message: Some("instruction breakpoints are not supported".to_string()), + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + instruction_reference: None, + offset: None, + reason: None, + }) + .collect(); + SetInstructionBreakpointsResponse { breakpoints } } pub fn stack_trace( @@ -726,7 +981,6 @@ impl Adapter { source: Some(Source { name: Some(location.chunk().to_string()), path: Some(location.chunk().to_string()), - source_reference: None, presentation_hint: None, origin: None, sources: None, diff --git a/debugger/src/error.rs b/debugger/src/error.rs index dff2152f..2bbee697 100644 --- a/debugger/src/error.rs +++ b/debugger/src/error.rs @@ -1,4 +1,4 @@ -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub enum Error { SenderDead, ReceiverDead, diff --git a/debugger/src/lib.rs b/debugger/src/lib.rs index c82821e1..f737b0bc 100644 --- a/debugger/src/lib.rs +++ b/debugger/src/lib.rs @@ -9,7 +9,9 @@ use std::collections::HashMap; use piccolo::opcode::Operation; -pub use self::{error::Error, location::Location, stop_reason::StopReason, watch::WatchMode}; +pub use self::{ + adapter::*, error::Error, location::Location, stop_reason::StopReason, watch::WatchMode, +}; use self::session::Session; @@ -66,6 +68,10 @@ impl Debugger { .map(|session| session.terminate()); } + pub fn map_by_name(&self, name: &str) -> Option { + self.sessions.iter().position(|s| s.name() == name) + } + pub fn add_session(&mut self, source: impl Into, path: impl Into) -> usize { let id = self.next_session; self.next_session += 1; diff --git a/debugger/src/session.rs b/debugger/src/session.rs index e81a1c99..426c9590 100644 --- a/debugger/src/session.rs +++ b/debugger/src/session.rs @@ -80,8 +80,12 @@ impl<'gc> Session { } } Request::AddFunctionBreakpoint(name) => { - let id = debug::resolve_function_breakpoint(executor, ctx, name.as_str()) - .map(|(chunk, line)| breakpoints.add(chunk, line)); + let id = debug::resolve_function_breakpoint( + executor, + ctx, + name.as_str(), + ) + .map(|(chunk, line)| breakpoints.add(chunk, line)); if let Err(_) = thirst.send(Response::FunctionBreakpointAdded(id)) { break; } diff --git a/debugger/src/session/debug.rs b/debugger/src/session/debug.rs index f9352bc0..83c5eaad 100644 --- a/debugger/src/session/debug.rs +++ b/debugger/src/session/debug.rs @@ -158,10 +158,7 @@ pub fn resolve_function_breakpoint<'gc>( ctx: Context<'gc>, name: &str, ) -> Option<(String, usize)> { - fn find_named<'gc>( - proto: &FunctionPrototype<'gc>, - target: &str, - ) -> Option<(String, usize)> { + fn find_named<'gc>(proto: &FunctionPrototype<'gc>, target: &str) -> Option<(String, usize)> { for child in proto.prototypes.iter() { match child.reference { piccolo::compiler::FunctionRef::Named(n, _) => { From 670768a444dcb4fd98cfa667c92c35bbd3fe742f Mon Sep 17 00:00:00 2001 From: reloginn Date: Sun, 24 Aug 2025 02:37:29 +0600 Subject: [PATCH 6/7] `feature`: added `assert_eq` in simple_debug.rs --- debugger/Cargo.toml | 2 +- debugger/examples/simple_debug.rs | 42 ++++++++++++++++++++++++++++--- debugger/src/lib.rs | 9 +++---- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/debugger/Cargo.toml b/debugger/Cargo.toml index 086ec409..01f7ee40 100644 --- a/debugger/Cargo.toml +++ b/debugger/Cargo.toml @@ -16,4 +16,4 @@ base64 = "0.22" [[example]] name = "simple_debug" -path = "examples/simple_debug.rs" +path = "examples/simple_debug.rs" \ No newline at end of file diff --git a/debugger/examples/simple_debug.rs b/debugger/examples/simple_debug.rs index 612a58fa..fa8ae248 100644 --- a/debugger/examples/simple_debug.rs +++ b/debugger/examples/simple_debug.rs @@ -1,4 +1,5 @@ -use piccolo_debugger::{Debugger, Error}; +use piccolo::compiler::LineNumber; +use piccolo_debugger::{Debugger, Error, Location, StopReason}; fn main() -> Result<(), Error> { let source = r#" @@ -30,6 +31,16 @@ fn main() -> Result<(), Error> { debugger.add_function_breakpoint(session_id, "bar".to_string())?; + + let breakpoints = debugger.list_breakpoints(session_id)?; + + + assert_eq!(breakpoints.len(), 1); + assert_eq!(breakpoints.get("test.lua").map(|b| b.contains(&3)), Some(true)); + assert_eq!(breakpoints.get("test.lua").map(|b| b.contains(&9)), Some(true)); + assert_eq!(breakpoints.get("test.lua").map(|b| b.contains(&7)), Some(true)); + assert_eq!(breakpoints.get("test.lua").map(|b| b.contains(&10)), Some(true)); // function breakpoint + println!("Breakpoints:"); for (source, lines) in debugger.list_breakpoints(session_id)? { let mut v: Vec = lines.iter().copied().collect(); @@ -39,17 +50,30 @@ fn main() -> Result<(), Error> { debugger.remove_breakpoint(session_id, "test.lua".to_string(), 7)?; + let breakpoints = debugger.list_breakpoints(session_id)?; + assert_eq!(breakpoints.len(), 1); + assert_eq!(breakpoints.get("test.lua").map(|b| b.contains(&3)), Some(true)); + assert_eq!(breakpoints.get("test.lua").map(|b| b.contains(&9)), Some(true)); + assert_eq!(breakpoints.get("test.lua").map(|b| b.contains(&7)), Some(false)); + assert_eq!(breakpoints.get("test.lua").map(|b| b.contains(&10)), Some(true)); // function breakpoint + println!("breakpoints: {:?}", debugger.list_breakpoints(session_id)?); debugger.watch_global(session_id, "x".to_string(), None)?; println!("Continue…"); let mut stop = debugger.continue_run(session_id)?; + assert_eq!(stop, StopReason::Watchpoint("watchpoint hit: global['x']: -> 0".to_string())); println!("Stopped: {:?}", stop); - let bt = debugger.backtrace(session_id)?; + let backtrace = debugger.backtrace(session_id)?; + assert_eq!(backtrace.len(), 1); + assert_eq!(backtrace.first().map(|b| b.chunk()), Some("test.lua")); + assert_eq!(backtrace.first().map(|b| b.line()), Some(2)); + assert_eq!(backtrace.first().map(|b| b.function_ref().to_string()), Some("".to_string())); + println!("Backtrace:"); - for (index, location) in bt.iter().enumerate() { + for (index, location) in backtrace.iter().enumerate() { println!( " #{index} {}:{} {}", location.chunk(), @@ -58,8 +82,14 @@ fn main() -> Result<(), Error> { ); } + + let disassembled_instructions = debugger.disassemble(session_id, "".to_string(), None, None)?; + + + assert_eq!(disassembled_instructions.as_ref().map(|d| d.len()), Some(10)); + if let Some(disassembled_instructions) = - debugger.disassemble(session_id, "".to_string(), None, None)? + disassembled_instructions { println!("Disassembly (current function):"); for instruction in disassembled_instructions.iter().take(10) { @@ -91,6 +121,7 @@ fn main() -> Result<(), Error> { println!("Step into…"); stop = debugger.step_into(session_id)?; + assert_eq!(stop, StopReason::Breakpoint { location: Location::new("test.lua".to_string(), piccolo::compiler::FunctionRef::Chunk, 3), breakpoint_ids: vec![0] }); println!("Stopped: {:?}", stop); if let Some(upvalues) = debugger.read_upvalues(session_id)? { @@ -102,14 +133,17 @@ fn main() -> Result<(), Error> { println!("Step over..."); stop = debugger.step_over(session_id)?; + assert_eq!(stop, StopReason::Breakpoint { location: Location::new("test.lua".to_string(), piccolo::compiler::FunctionRef::Chunk, 9), breakpoint_ids: vec![1] }); println!("Stopped: {:?}", stop); println!("Step out…"); stop = debugger.step_out(session_id)?; + assert_eq!(stop, StopReason::Breakpoint { location: Location::new("test.lua".to_string(), piccolo::compiler::FunctionRef::Named("bar".to_string(), LineNumber(9)), 10), breakpoint_ids: vec![3] }); println!("Stopped: {:?}", stop); println!("Continue again…"); stop = debugger.continue_run(session_id)?; + assert_eq!(stop, StopReason::Watchpoint("watchpoint hit: global['x']: 0 -> 1".to_string())); println!("Stopped: {:?}", stop); if let Some(v0) = debugger.read_register(session_id, 0)? { diff --git a/debugger/src/lib.rs b/debugger/src/lib.rs index f737b0bc..5ab506ea 100644 --- a/debugger/src/lib.rs +++ b/debugger/src/lib.rs @@ -6,7 +6,6 @@ struct PrototypeReference { } use std::collections::HashMap; - use piccolo::opcode::Operation; pub use self::{ @@ -25,10 +24,10 @@ mod watch; #[derive(Debug)] pub struct Disassembled { - symbol: &'static str, - index: usize, - line: usize, - operation: Operation, + pub symbol: &'static str, + pub index: usize, + pub line: usize, + pub operation: Operation, } pub struct Debugger { From e613d1c11595da2c97b4a07c3370b1615157558c Mon Sep 17 00:00:00 2001 From: reloginn Date: Sun, 24 Aug 2025 17:26:30 +0600 Subject: [PATCH 7/7] `fix`: CI --- Cargo.lock | 1 + Cargo.toml | 1 + debugger/examples/simple_debug.rs | 104 +++++++++++++++++++++++------- debugger/src/lib.rs | 2 +- 4 files changed, 85 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47ffc76b..a66a98dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -300,6 +300,7 @@ dependencies = [ "clap", "gc-arena", "hashbrown", + "piccolo-debugger", "rand", "rustyline", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index bd7f9a88..24e825c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,3 +51,4 @@ thiserror.workspace = true [dev-dependencies] clap = { version = "4.5", features = ["cargo"] } rustyline = "14.0" +piccolo-debugger = { path = "debugger" } diff --git a/debugger/examples/simple_debug.rs b/debugger/examples/simple_debug.rs index fa8ae248..9e7704d2 100644 --- a/debugger/examples/simple_debug.rs +++ b/debugger/examples/simple_debug.rs @@ -31,15 +31,25 @@ fn main() -> Result<(), Error> { debugger.add_function_breakpoint(session_id, "bar".to_string())?; - let breakpoints = debugger.list_breakpoints(session_id)?; - assert_eq!(breakpoints.len(), 1); - assert_eq!(breakpoints.get("test.lua").map(|b| b.contains(&3)), Some(true)); - assert_eq!(breakpoints.get("test.lua").map(|b| b.contains(&9)), Some(true)); - assert_eq!(breakpoints.get("test.lua").map(|b| b.contains(&7)), Some(true)); - assert_eq!(breakpoints.get("test.lua").map(|b| b.contains(&10)), Some(true)); // function breakpoint + assert_eq!( + breakpoints.get("test.lua").map(|b| b.contains(&3)), + Some(true) + ); + assert_eq!( + breakpoints.get("test.lua").map(|b| b.contains(&9)), + Some(true) + ); + assert_eq!( + breakpoints.get("test.lua").map(|b| b.contains(&7)), + Some(true) + ); + assert_eq!( + breakpoints.get("test.lua").map(|b| b.contains(&10)), + Some(true) + ); // function breakpoint println!("Breakpoints:"); for (source, lines) in debugger.list_breakpoints(session_id)? { @@ -52,10 +62,22 @@ fn main() -> Result<(), Error> { let breakpoints = debugger.list_breakpoints(session_id)?; assert_eq!(breakpoints.len(), 1); - assert_eq!(breakpoints.get("test.lua").map(|b| b.contains(&3)), Some(true)); - assert_eq!(breakpoints.get("test.lua").map(|b| b.contains(&9)), Some(true)); - assert_eq!(breakpoints.get("test.lua").map(|b| b.contains(&7)), Some(false)); - assert_eq!(breakpoints.get("test.lua").map(|b| b.contains(&10)), Some(true)); // function breakpoint + assert_eq!( + breakpoints.get("test.lua").map(|b| b.contains(&3)), + Some(true) + ); + assert_eq!( + breakpoints.get("test.lua").map(|b| b.contains(&9)), + Some(true) + ); + assert_eq!( + breakpoints.get("test.lua").map(|b| b.contains(&7)), + Some(false) + ); + assert_eq!( + breakpoints.get("test.lua").map(|b| b.contains(&10)), + Some(true) + ); // function breakpoint println!("breakpoints: {:?}", debugger.list_breakpoints(session_id)?); @@ -63,14 +85,20 @@ fn main() -> Result<(), Error> { println!("Continue…"); let mut stop = debugger.continue_run(session_id)?; - assert_eq!(stop, StopReason::Watchpoint("watchpoint hit: global['x']: -> 0".to_string())); + assert_eq!( + stop, + StopReason::Watchpoint("watchpoint hit: global['x']: -> 0".to_string()) + ); println!("Stopped: {:?}", stop); let backtrace = debugger.backtrace(session_id)?; assert_eq!(backtrace.len(), 1); assert_eq!(backtrace.first().map(|b| b.chunk()), Some("test.lua")); assert_eq!(backtrace.first().map(|b| b.line()), Some(2)); - assert_eq!(backtrace.first().map(|b| b.function_ref().to_string()), Some("".to_string())); + assert_eq!( + backtrace.first().map(|b| b.function_ref().to_string()), + Some("".to_string()) + ); println!("Backtrace:"); for (index, location) in backtrace.iter().enumerate() { @@ -82,15 +110,14 @@ fn main() -> Result<(), Error> { ); } - let disassembled_instructions = debugger.disassemble(session_id, "".to_string(), None, None)?; + assert_eq!( + disassembled_instructions.as_ref().map(|d| d.len()), + Some(10) + ); - assert_eq!(disassembled_instructions.as_ref().map(|d| d.len()), Some(10)); - - if let Some(disassembled_instructions) = - disassembled_instructions - { + if let Some(disassembled_instructions) = disassembled_instructions { println!("Disassembly (current function):"); for instruction in disassembled_instructions.iter().take(10) { println!(" {instruction:?}"); @@ -121,7 +148,17 @@ fn main() -> Result<(), Error> { println!("Step into…"); stop = debugger.step_into(session_id)?; - assert_eq!(stop, StopReason::Breakpoint { location: Location::new("test.lua".to_string(), piccolo::compiler::FunctionRef::Chunk, 3), breakpoint_ids: vec![0] }); + assert_eq!( + stop, + StopReason::Breakpoint { + location: Location::new( + "test.lua".to_string(), + piccolo::compiler::FunctionRef::Chunk, + 3 + ), + breakpoint_ids: vec![0] + } + ); println!("Stopped: {:?}", stop); if let Some(upvalues) = debugger.read_upvalues(session_id)? { @@ -133,17 +170,40 @@ fn main() -> Result<(), Error> { println!("Step over..."); stop = debugger.step_over(session_id)?; - assert_eq!(stop, StopReason::Breakpoint { location: Location::new("test.lua".to_string(), piccolo::compiler::FunctionRef::Chunk, 9), breakpoint_ids: vec![1] }); + assert_eq!( + stop, + StopReason::Breakpoint { + location: Location::new( + "test.lua".to_string(), + piccolo::compiler::FunctionRef::Chunk, + 9 + ), + breakpoint_ids: vec![1] + } + ); println!("Stopped: {:?}", stop); println!("Step out…"); stop = debugger.step_out(session_id)?; - assert_eq!(stop, StopReason::Breakpoint { location: Location::new("test.lua".to_string(), piccolo::compiler::FunctionRef::Named("bar".to_string(), LineNumber(9)), 10), breakpoint_ids: vec![3] }); + assert_eq!( + stop, + StopReason::Breakpoint { + location: Location::new( + "test.lua".to_string(), + piccolo::compiler::FunctionRef::Named("bar".to_string(), LineNumber(9)), + 10 + ), + breakpoint_ids: vec![3] + } + ); println!("Stopped: {:?}", stop); println!("Continue again…"); stop = debugger.continue_run(session_id)?; - assert_eq!(stop, StopReason::Watchpoint("watchpoint hit: global['x']: 0 -> 1".to_string())); + assert_eq!( + stop, + StopReason::Watchpoint("watchpoint hit: global['x']: 0 -> 1".to_string()) + ); println!("Stopped: {:?}", stop); if let Some(v0) = debugger.read_register(session_id, 0)? { diff --git a/debugger/src/lib.rs b/debugger/src/lib.rs index 5ab506ea..94b8c317 100644 --- a/debugger/src/lib.rs +++ b/debugger/src/lib.rs @@ -5,8 +5,8 @@ struct PrototypeReference { prototype_index: usize, } -use std::collections::HashMap; use piccolo::opcode::Operation; +use std::collections::HashMap; pub use self::{ adapter::*, error::Error, location::Location, stop_reason::StopReason, watch::WatchMode,