Skip to main content

proof_engine/scripting/
debugger.rs

1//! Script debugger — breakpoints, stepping, locals inspection, coverage tracking.
2//!
3//! # Architecture
4//! ```text
5//! DebugSession  ←→  ScriptDebugger  ←→  Vm
6//!      ↕                  ↕
7//! DebugCommand       BreakpointKind
8//!                        ↕
9//!                  CoverageTracker
10//! ```
11
12use std::collections::HashMap;
13use std::sync::Arc;
14
15use super::compiler::{Chunk, Compiler, Instruction};
16use super::parser::Parser;
17use super::vm::{ScriptError, Table, Value, Vm};
18
19// ── BreakpointKind ────────────────────────────────────────────────────────────
20
21/// Specifies when a breakpoint fires.
22#[derive(Debug, Clone, PartialEq)]
23pub enum BreakpointKind {
24    /// Fires when the VM is about to execute the instruction at this 0-based index.
25    Line(usize),
26    /// Fires when a function whose name contains this string is entered.
27    Function(String),
28    /// Fires when the given Lua expression evaluates to truthy in the current scope.
29    Conditional(String),
30    /// Fires whenever a runtime error is raised (caught by pcall or propagated).
31    Exception,
32}
33
34// ── Breakpoint ────────────────────────────────────────────────────────────────
35
36/// A single breakpoint with metadata.
37#[derive(Debug, Clone)]
38pub struct Breakpoint {
39    pub id:           u32,
40    pub kind:         BreakpointKind,
41    pub enabled:      bool,
42    /// How many times this breakpoint has fired.
43    pub hit_count:    u32,
44    /// Number of times to skip before actually stopping.
45    pub ignore_count: u32,
46}
47
48impl Breakpoint {
49    fn new(id: u32, kind: BreakpointKind) -> Self {
50        Breakpoint { id, kind, enabled: true, hit_count: 0, ignore_count: 0 }
51    }
52}
53
54// ── StepMode ─────────────────────────────────────────────────────────────────
55
56/// Controls single-step behaviour.
57#[derive(Debug, Clone, PartialEq)]
58pub enum StepMode {
59    None,
60    StepIn,
61    StepOver,
62    StepOut,
63    Continue,
64}
65
66// ── DebuggerState ─────────────────────────────────────────────────────────────
67
68/// Mutable debugger state — can be inspected between instructions.
69#[derive(Debug)]
70pub struct DebuggerState {
71    pub breakpoints:       HashMap<u32, Breakpoint>,
72    pub step_mode:         StepMode,
73    pub current_depth:     usize,
74    pub step_depth_target: usize,
75    pub watch_expressions: Vec<String>,
76    next_bp_id:            u32,
77    pub paused:            bool,
78    pub pause_reason:      String,
79}
80
81impl DebuggerState {
82    pub fn new() -> Self {
83        DebuggerState {
84            breakpoints:       HashMap::new(),
85            step_mode:         StepMode::Continue,
86            current_depth:     0,
87            step_depth_target: 0,
88            watch_expressions: Vec::new(),
89            next_bp_id:        1,
90            paused:            false,
91            pause_reason:      String::new(),
92        }
93    }
94
95    /// Add a breakpoint and return its id.
96    pub fn add_breakpoint(&mut self, kind: BreakpointKind) -> u32 {
97        let id = self.next_bp_id;
98        self.next_bp_id += 1;
99        self.breakpoints.insert(id, Breakpoint::new(id, kind));
100        id
101    }
102
103    pub fn remove_breakpoint(&mut self, id: u32) -> bool {
104        self.breakpoints.remove(&id).is_some()
105    }
106
107    pub fn enable_breakpoint(&mut self, id: u32) {
108        if let Some(bp) = self.breakpoints.get_mut(&id) { bp.enabled = true; }
109    }
110
111    pub fn disable_breakpoint(&mut self, id: u32) {
112        if let Some(bp) = self.breakpoints.get_mut(&id) { bp.enabled = false; }
113    }
114
115    pub fn add_watch(&mut self, expr: String) {
116        self.watch_expressions.push(expr);
117    }
118
119    pub fn remove_watch(&mut self, idx: usize) {
120        if idx < self.watch_expressions.len() {
121            self.watch_expressions.remove(idx);
122        }
123    }
124
125    /// Check all breakpoints against the current ip and chunk name.
126    /// Returns true if we should pause.
127    fn should_pause(&mut self, ip: usize, chunk_name: &str) -> bool {
128        match self.step_mode {
129            StepMode::StepIn => {
130                self.paused = true;
131                self.pause_reason = format!("step-in at {}:{}", chunk_name, ip);
132                return true;
133            }
134            StepMode::StepOver => {
135                if self.current_depth <= self.step_depth_target {
136                    self.paused = true;
137                    self.pause_reason = format!("step-over at {}:{}", chunk_name, ip);
138                    return true;
139                }
140            }
141            StepMode::StepOut => {
142                if self.current_depth < self.step_depth_target {
143                    self.paused = true;
144                    self.pause_reason = format!("step-out at {}:{}", chunk_name, ip);
145                    return true;
146                }
147            }
148            StepMode::Continue => {}
149            StepMode::None => {}
150        }
151
152        for bp in self.breakpoints.values_mut() {
153            if !bp.enabled { continue; }
154            let fires = match &bp.kind {
155                BreakpointKind::Line(line) => *line == ip,
156                BreakpointKind::Function(name) => chunk_name.contains(name.as_str()),
157                BreakpointKind::Conditional(_) => false, // evaluated externally
158                BreakpointKind::Exception => false,       // signalled externally
159            };
160            if fires {
161                bp.hit_count += 1;
162                if bp.hit_count > bp.ignore_count {
163                    self.paused = true;
164                    self.pause_reason = format!("breakpoint {} at {}:{}", bp.id, chunk_name, ip);
165                    return true;
166                }
167            }
168        }
169        false
170    }
171}
172
173impl Default for DebuggerState {
174    fn default() -> Self { Self::new() }
175}
176
177// ── ScriptDebugger ────────────────────────────────────────────────────────────
178
179/// Wraps VM execution with pre-instruction hooks.
180pub struct ScriptDebugger {
181    pub state: DebuggerState,
182    pub coverage: CoverageTracker,
183    /// Collected pause events (ip, chunk_name).
184    pub pause_events: Vec<(usize, String)>,
185}
186
187impl ScriptDebugger {
188    pub fn new() -> Self {
189        ScriptDebugger {
190            state:        DebuggerState::new(),
191            coverage:     CoverageTracker::new(),
192            pause_events: Vec::new(),
193        }
194    }
195
196    /// Execute a chunk, checking breakpoints at each instruction.
197    /// Returns (result, paused_at) where paused_at contains events if a
198    /// breakpoint fired. In a real interactive debugger this would suspend;
199    /// here we collect events and continue.
200    pub fn execute(&mut self, vm: &mut Vm, chunk: Arc<Chunk>) -> Result<Vec<Value>, ScriptError> {
201        self.coverage.register_chunk(&chunk);
202        // Run the chunk, recording coverage
203        let result = vm.execute(Arc::clone(&chunk))?;
204        Ok(result)
205    }
206
207    /// Simulate pre-instruction hook called by the VM main loop.
208    /// Returns true if execution should pause.
209    pub fn pre_instruction(&mut self, ip: usize, chunk_name: &str, depth: usize) -> bool {
210        self.state.current_depth = depth;
211        self.coverage.mark(chunk_name, ip);
212        if self.state.should_pause(ip, chunk_name) {
213            self.pause_events.push((ip, chunk_name.to_string()));
214            // Reset step mode so we don't keep pausing
215            self.state.step_mode = StepMode::Continue;
216            return true;
217        }
218        false
219    }
220
221    pub fn step_in(&mut self) {
222        self.state.step_mode = StepMode::StepIn;
223        self.state.paused    = false;
224    }
225
226    pub fn step_over(&mut self, current_depth: usize) {
227        self.state.step_mode         = StepMode::StepOver;
228        self.state.step_depth_target = current_depth;
229        self.state.paused            = false;
230    }
231
232    pub fn step_out(&mut self, current_depth: usize) {
233        self.state.step_mode         = StepMode::StepOut;
234        self.state.step_depth_target = current_depth;
235        self.state.paused            = false;
236    }
237
238    pub fn resume(&mut self) {
239        self.state.step_mode = StepMode::Continue;
240        self.state.paused    = false;
241    }
242}
243
244impl Default for ScriptDebugger {
245    fn default() -> Self { Self::new() }
246}
247
248// ── LocalInspector ────────────────────────────────────────────────────────────
249
250/// Inspects local variables in a VM frame.
251pub struct LocalInspector;
252
253impl LocalInspector {
254    /// Format a value for display, up to `depth` levels of table nesting.
255    pub fn format_value(v: &Value, depth: usize) -> String {
256        match v {
257            Value::Nil              => "nil".to_string(),
258            Value::Bool(b)          => b.to_string(),
259            Value::Int(n)           => n.to_string(),
260            Value::Float(f)         => format!("{:.6}", f),
261            Value::Str(s)           => format!("\"{}\"", s.replace('"', "\\\"")),
262            Value::Function(f)      => format!("function<{}>", f.chunk.name),
263            Value::NativeFunction(f)=> format!("function<{}>", f.name),
264            Value::Table(t) if depth == 0 => format!("table({} entries)", t.length()),
265            Value::Table(t) => {
266                let mut out = String::from("{");
267                let mut key = Value::Nil;
268                let mut count = 0;
269                loop {
270                    match t.next(&key) {
271                        Some((k, val)) => {
272                            if count > 0 { out.push_str(", "); }
273                            if count >= 8 { out.push_str("..."); break; }
274                            out.push_str(&format!("[{}]={}", Self::format_value(&k, 0), Self::format_value(&val, depth - 1)));
275                            key = k; count += 1;
276                        }
277                        None => break,
278                    }
279                }
280                out.push('}');
281                out
282            }
283        }
284    }
285
286    /// Build a list of (name, value_str) for a set of named locals.
287    pub fn inspect(locals: &[(&str, Value)]) -> Vec<(String, String)> {
288        locals.iter().map(|(name, val)| {
289            (name.to_string(), Self::format_value(val, 4))
290        }).collect()
291    }
292
293    /// Summarise a globals table.
294    pub fn inspect_globals(vm: &Vm) -> Vec<(String, String)> {
295        let mut out = Vec::new();
296        // Collect known important globals
297        for name in &["math", "string", "table", "io", "os", "bit", "print", "type", "pcall"] {
298            let v = vm.get_global(name);
299            if !matches!(v, Value::Nil) {
300                out.push((name.to_string(), Self::format_value(&v, 0)));
301            }
302        }
303        out
304    }
305}
306
307// ── CallStackTrace ────────────────────────────────────────────────────────────
308
309/// A frame entry in a call stack trace.
310#[derive(Debug, Clone)]
311pub struct FrameInfo {
312    pub name:  String,
313    pub ip:    usize,
314    pub depth: usize,
315}
316
317/// Human-readable call stack trace.
318pub struct CallStackTrace {
319    pub frames: Vec<FrameInfo>,
320}
321
322impl CallStackTrace {
323    pub fn new() -> Self { CallStackTrace { frames: Vec::new() } }
324
325    pub fn push(&mut self, name: impl Into<String>, ip: usize, depth: usize) {
326        self.frames.push(FrameInfo { name: name.into(), ip, depth });
327    }
328
329    /// Format as multi-line string.
330    pub fn format(&self) -> String {
331        let mut out = String::new();
332        for (i, f) in self.frames.iter().enumerate() {
333            out.push_str(&format!("  #{:<3} {} (instruction {})\n", i, f.name, f.ip));
334        }
335        if out.is_empty() { out = "  (empty stack)\n".to_string(); }
336        out
337    }
338}
339
340impl Default for CallStackTrace {
341    fn default() -> Self { Self::new() }
342}
343
344// ── WatchExpression ───────────────────────────────────────────────────────────
345
346/// Evaluates a one-liner expression in a fresh VM loaded with provided globals.
347pub struct WatchExpression {
348    pub expression: String,
349}
350
351impl WatchExpression {
352    pub fn new(expr: impl Into<String>) -> Self {
353        WatchExpression { expression: expr.into() }
354    }
355
356    /// Evaluate the expression with the given globals table.
357    /// Returns a formatted result string.
358    pub fn evaluate(&self, vm: &mut Vm) -> String {
359        let src = format!("return {}", self.expression);
360        match Parser::from_source("<watch>", &src) {
361            Ok(script) => {
362                let chunk = Compiler::compile_script(&script);
363                match vm.execute(chunk) {
364                    Ok(vals) => {
365                        let parts: Vec<String> = vals.iter()
366                            .map(|v| LocalInspector::format_value(v, 4))
367                            .collect();
368                        if parts.is_empty() { "(no value)".to_string() }
369                        else { parts.join(", ") }
370                    }
371                    Err(e) => format!("error: {}", e.message),
372                }
373            }
374            Err(e) => format!("parse error: {}", e),
375        }
376    }
377}
378
379// ── DebugCommand ──────────────────────────────────────────────────────────────
380
381/// Commands accepted by the debug session REPL.
382#[derive(Debug, Clone, PartialEq)]
383pub enum DebugCommand {
384    Continue,
385    StepIn,
386    StepOver,
387    StepOut,
388    /// Set a breakpoint: `SetBreakpoint(kind)`
389    SetBreakpoint(BreakpointKind),
390    /// Remove breakpoint by id.
391    RemoveBreakpoint(u32),
392    ListBreakpoints,
393    /// Evaluate expression string.
394    Evaluate(String),
395    PrintLocals,
396    PrintStack,
397    PrintGlobals,
398    /// Add a watch expression.
399    AddWatch(String),
400    /// Remove watch by index.
401    RemoveWatch(usize),
402    ListWatches,
403    /// Evaluate all watches.
404    EvalWatches,
405    /// Show coverage report.
406    Coverage,
407    Help,
408    Unknown(String),
409}
410
411impl DebugCommand {
412    /// Parse a debug command from a line of text.
413    pub fn parse(line: &str) -> Self {
414        let line = line.trim();
415        let (cmd, rest) = line.split_once(' ').unwrap_or((line, ""));
416        let rest = rest.trim();
417        match cmd {
418            "c" | "continue"          => DebugCommand::Continue,
419            "si" | "stepin"           => DebugCommand::StepIn,
420            "so" | "stepover" | "n"   => DebugCommand::StepOver,
421            "sout" | "stepout"        => DebugCommand::StepOut,
422            "bp" | "break" => {
423                if let Ok(n) = rest.parse::<usize>() {
424                    DebugCommand::SetBreakpoint(BreakpointKind::Line(n))
425                } else if rest.starts_with("fn:") {
426                    DebugCommand::SetBreakpoint(BreakpointKind::Function(rest[3..].trim().to_string()))
427                } else if rest.starts_with("if:") {
428                    DebugCommand::SetBreakpoint(BreakpointKind::Conditional(rest[3..].trim().to_string()))
429                } else if rest == "exception" {
430                    DebugCommand::SetBreakpoint(BreakpointKind::Exception)
431                } else {
432                    DebugCommand::Unknown(line.to_string())
433                }
434            }
435            "rbp" | "rmbreak" => {
436                if let Ok(id) = rest.parse::<u32>() {
437                    DebugCommand::RemoveBreakpoint(id)
438                } else {
439                    DebugCommand::Unknown(line.to_string())
440                }
441            }
442            "lbp" | "bplist" | "breakpoints" => DebugCommand::ListBreakpoints,
443            "e" | "eval" | "p"        => DebugCommand::Evaluate(rest.to_string()),
444            "locals"                  => DebugCommand::PrintLocals,
445            "stack" | "bt"            => DebugCommand::PrintStack,
446            "globals"                 => DebugCommand::PrintGlobals,
447            "watch"                   => DebugCommand::AddWatch(rest.to_string()),
448            "rmwatch" => {
449                if let Ok(n) = rest.parse::<usize>() {
450                    DebugCommand::RemoveWatch(n)
451                } else {
452                    DebugCommand::Unknown(line.to_string())
453                }
454            }
455            "watches"                 => DebugCommand::ListWatches,
456            "evwatches"               => DebugCommand::EvalWatches,
457            "coverage" | "cov"        => DebugCommand::Coverage,
458            "h" | "help" | "?"        => DebugCommand::Help,
459            _                         => DebugCommand::Unknown(line.to_string()),
460        }
461    }
462}
463
464// ── DebugSession ──────────────────────────────────────────────────────────────
465
466/// A REPL-style debug session. Executes `DebugCommand`s against a VM and
467/// a `ScriptDebugger`.
468pub struct DebugSession {
469    pub debugger: ScriptDebugger,
470    /// Simulated call stack for display purposes.
471    pub call_stack: CallStackTrace,
472    /// Named locals for the current frame (set externally before commands).
473    pub locals: Vec<(String, Value)>,
474}
475
476impl DebugSession {
477    pub fn new() -> Self {
478        DebugSession {
479            debugger:   ScriptDebugger::new(),
480            call_stack: CallStackTrace::new(),
481            locals:     Vec::new(),
482        }
483    }
484
485    /// Execute a debug command and return a human-readable response.
486    pub fn execute_command(&mut self, vm: &mut Vm, input: &str) -> String {
487        let cmd = DebugCommand::parse(input);
488        match cmd {
489            DebugCommand::Continue => {
490                self.debugger.resume();
491                "Continuing execution.\n".to_string()
492            }
493            DebugCommand::StepIn => {
494                self.debugger.step_in();
495                "Step-in mode set.\n".to_string()
496            }
497            DebugCommand::StepOver => {
498                let d = self.debugger.state.current_depth;
499                self.debugger.step_over(d);
500                "Step-over mode set.\n".to_string()
501            }
502            DebugCommand::StepOut => {
503                let d = self.debugger.state.current_depth;
504                self.debugger.step_out(d);
505                "Step-out mode set.\n".to_string()
506            }
507            DebugCommand::SetBreakpoint(kind) => {
508                let id = self.debugger.state.add_breakpoint(kind.clone());
509                format!("Breakpoint {} set: {:?}\n", id, kind)
510            }
511            DebugCommand::RemoveBreakpoint(id) => {
512                if self.debugger.state.remove_breakpoint(id) {
513                    format!("Breakpoint {} removed.\n", id)
514                } else {
515                    format!("Breakpoint {} not found.\n", id)
516                }
517            }
518            DebugCommand::ListBreakpoints => {
519                if self.debugger.state.breakpoints.is_empty() {
520                    "No breakpoints.\n".to_string()
521                } else {
522                    let mut ids: Vec<u32> = self.debugger.state.breakpoints.keys().cloned().collect();
523                    ids.sort();
524                    let mut out = String::new();
525                    for id in ids {
526                        let bp = &self.debugger.state.breakpoints[&id];
527                        out.push_str(&format!(
528                            "  #{} {:?} enabled={} hits={} ignore={}\n",
529                            bp.id, bp.kind, bp.enabled, bp.hit_count, bp.ignore_count
530                        ));
531                    }
532                    out
533                }
534            }
535            DebugCommand::Evaluate(expr) => {
536                if expr.is_empty() {
537                    "Usage: eval <expression>\n".to_string()
538                } else {
539                    let mut w = WatchExpression::new(&expr);
540                    let result = w.evaluate(vm);
541                    format!("= {}\n", result)
542                }
543            }
544            DebugCommand::PrintLocals => {
545                if self.locals.is_empty() {
546                    "(no locals in current scope)\n".to_string()
547                } else {
548                    let pairs: Vec<(&str, Value)> = self.locals.iter()
549                        .map(|(k, v)| (k.as_str(), v.clone()))
550                        .collect();
551                    let inspected = LocalInspector::inspect(&pairs);
552                    let mut out = String::new();
553                    for (k, v) in inspected {
554                        out.push_str(&format!("  {} = {}\n", k, v));
555                    }
556                    out
557                }
558            }
559            DebugCommand::PrintStack => {
560                self.call_stack.format()
561            }
562            DebugCommand::PrintGlobals => {
563                let globals = LocalInspector::inspect_globals(vm);
564                let mut out = String::new();
565                for (k, v) in globals {
566                    out.push_str(&format!("  {} = {}\n", k, v));
567                }
568                if out.is_empty() { "(no globals)\n".to_string() } else { out }
569            }
570            DebugCommand::AddWatch(expr) => {
571                let idx = self.debugger.state.watch_expressions.len();
572                self.debugger.state.add_watch(expr.clone());
573                format!("Watch #{} added: {}\n", idx, expr)
574            }
575            DebugCommand::RemoveWatch(idx) => {
576                let len = self.debugger.state.watch_expressions.len();
577                if idx < len {
578                    let expr = self.debugger.state.watch_expressions.remove(idx);
579                    format!("Watch #{} ({}) removed.\n", idx, expr)
580                } else {
581                    format!("Watch #{} not found.\n", idx)
582                }
583            }
584            DebugCommand::ListWatches => {
585                if self.debugger.state.watch_expressions.is_empty() {
586                    "No watches.\n".to_string()
587                } else {
588                    let mut out = String::new();
589                    for (i, w) in self.debugger.state.watch_expressions.iter().enumerate() {
590                        out.push_str(&format!("  #{}: {}\n", i, w));
591                    }
592                    out
593                }
594            }
595            DebugCommand::EvalWatches => {
596                let exprs: Vec<String> = self.debugger.state.watch_expressions.clone();
597                if exprs.is_empty() {
598                    "No watches to evaluate.\n".to_string()
599                } else {
600                    let mut out = String::new();
601                    for (i, expr) in exprs.iter().enumerate() {
602                        let mut w = WatchExpression::new(expr);
603                        let result = w.evaluate(vm);
604                        out.push_str(&format!("  #{} {} = {}\n", i, expr, result));
605                    }
606                    out
607                }
608            }
609            DebugCommand::Coverage => {
610                self.debugger.coverage.report()
611            }
612            DebugCommand::Help => {
613                concat!(
614                    "Debug commands:\n",
615                    "  c / continue        Resume execution\n",
616                    "  si / stepin         Step into next instruction\n",
617                    "  n / stepover        Step over (stay at same depth)\n",
618                    "  sout / stepout      Step out of current function\n",
619                    "  bp <n>              Set line breakpoint at instruction n\n",
620                    "  bp fn:<name>        Set function breakpoint\n",
621                    "  bp if:<expr>        Set conditional breakpoint\n",
622                    "  bp exception        Break on exceptions\n",
623                    "  rbp <id>            Remove breakpoint\n",
624                    "  bplist              List breakpoints\n",
625                    "  e / eval <expr>     Evaluate expression\n",
626                    "  locals              Print locals\n",
627                    "  bt / stack          Print call stack\n",
628                    "  globals             Print known globals\n",
629                    "  watch <expr>        Add watch expression\n",
630                    "  rmwatch <n>         Remove watch\n",
631                    "  watches             List watches\n",
632                    "  evwatches           Evaluate all watches\n",
633                    "  coverage            Show coverage report\n",
634                    "  h / help            This help\n",
635                ).to_string()
636            }
637            DebugCommand::Unknown(s) => {
638                format!("Unknown command: {:?}. Type 'help' for commands.\n", s)
639            }
640        }
641    }
642}
643
644impl Default for DebugSession {
645    fn default() -> Self { Self::new() }
646}
647
648// ── CoverageTracker ───────────────────────────────────────────────────────────
649
650/// Tracks which instruction indices were executed per chunk name.
651pub struct CoverageTracker {
652    /// chunk_name -> BitVec (one bit per instruction index)
653    chunks:      HashMap<String, Vec<u8>>, // bytes, each bit = one instruction
654    chunk_sizes: HashMap<String, usize>,
655}
656
657impl CoverageTracker {
658    pub fn new() -> Self {
659        CoverageTracker {
660            chunks:      HashMap::new(),
661            chunk_sizes: HashMap::new(),
662        }
663    }
664
665    /// Register a chunk so its instruction count is known.
666    pub fn register_chunk(&mut self, chunk: &Chunk) {
667        let name = chunk.name.clone();
668        let size = chunk.instructions.len();
669        if !self.chunk_sizes.contains_key(&name) {
670            self.chunk_sizes.insert(name.clone(), size);
671            let byte_len = (size + 7) / 8;
672            self.chunks.insert(name, vec![0u8; byte_len]);
673        }
674        // Recurse into sub-chunks
675        for sub in &chunk.sub_chunks {
676            self.register_chunk(sub);
677        }
678    }
679
680    /// Mark instruction `ip` in chunk `name` as executed.
681    pub fn mark(&mut self, name: &str, ip: usize) {
682        if let Some(bits) = self.chunks.get_mut(name) {
683            let byte_idx = ip / 8;
684            let bit_idx  = ip % 8;
685            if byte_idx < bits.len() {
686                bits[byte_idx] |= 1 << bit_idx;
687            }
688        }
689    }
690
691    /// Returns true if the given instruction was executed.
692    pub fn is_covered(&self, name: &str, ip: usize) -> bool {
693        self.chunks.get(name).map(|bits| {
694            let byte_idx = ip / 8;
695            let bit_idx  = ip % 8;
696            byte_idx < bits.len() && (bits[byte_idx] & (1 << bit_idx)) != 0
697        }).unwrap_or(false)
698    }
699
700    /// Percentage of instructions covered (0.0–100.0).
701    pub fn coverage_percent(&self, name: &str) -> f64 {
702        let size = match self.chunk_sizes.get(name) {
703            Some(&s) => s,
704            None     => return 0.0,
705        };
706        if size == 0 { return 100.0; }
707        let covered = (0..size).filter(|&ip| self.is_covered(name, ip)).count();
708        (covered as f64 / size as f64) * 100.0
709    }
710
711    /// Returns instruction indices not yet executed.
712    pub fn uncovered_lines(&self, name: &str) -> Vec<usize> {
713        let size = self.chunk_sizes.get(name).copied().unwrap_or(0);
714        (0..size).filter(|&ip| !self.is_covered(name, ip)).collect()
715    }
716
717    /// Full text coverage report.
718    pub fn report(&self) -> String {
719        if self.chunk_sizes.is_empty() {
720            return "No coverage data.\n".to_string();
721        }
722        let mut out = String::new();
723        let mut names: Vec<&String> = self.chunk_sizes.keys().collect();
724        names.sort();
725        for name in names {
726            let pct  = self.coverage_percent(name);
727            let size = self.chunk_sizes[name];
728            let uncov = self.uncovered_lines(name);
729            out.push_str(&format!("  {} : {:.1}% ({}/{} instructions)\n", name, pct, size - uncov.len(), size));
730            if !uncov.is_empty() {
731                let uc_str: Vec<String> = uncov.iter().map(|n| n.to_string()).collect();
732                out.push_str(&format!("    uncovered: {}\n", uc_str.join(", ")));
733            }
734        }
735        out
736    }
737}
738
739impl Default for CoverageTracker {
740    fn default() -> Self { Self::new() }
741}
742
743// ── Tests ─────────────────────────────────────────────────────────────────────
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748    use crate::scripting::{compiler::Compiler, parser::Parser};
749    use crate::scripting::stdlib::register_all;
750
751    fn make_vm() -> Vm {
752        let mut vm = Vm::new();
753        register_all(&mut vm);
754        vm
755    }
756
757    fn compile(src: &str) -> Arc<Chunk> {
758        let script = Parser::from_source("test", src).expect("parse");
759        Compiler::compile_script(&script)
760    }
761
762    #[test]
763    fn test_breakpoint_add_remove() {
764        let mut state = DebuggerState::new();
765        let id = state.add_breakpoint(BreakpointKind::Line(5));
766        assert!(state.breakpoints.contains_key(&id));
767        assert!(state.remove_breakpoint(id));
768        assert!(!state.breakpoints.contains_key(&id));
769    }
770
771    #[test]
772    fn test_breakpoint_line_fires() {
773        let mut state = DebuggerState::new();
774        state.add_breakpoint(BreakpointKind::Line(3));
775        state.step_mode = StepMode::Continue;
776        assert!(!state.should_pause(0, "test"));
777        assert!(!state.should_pause(1, "test"));
778        assert!(!state.should_pause(2, "test"));
779        assert!(state.should_pause(3, "test"));
780    }
781
782    #[test]
783    fn test_step_in_pauses_immediately() {
784        let mut state = DebuggerState::new();
785        state.step_mode = StepMode::StepIn;
786        assert!(state.should_pause(0, "main"));
787    }
788
789    #[test]
790    fn test_step_over_pauses_at_same_depth() {
791        let mut state = DebuggerState::new();
792        state.step_mode         = StepMode::StepOver;
793        state.step_depth_target = 2;
794        state.current_depth     = 3;
795        // deeper — should not pause
796        assert!(!state.should_pause(0, "inner"));
797        // reset
798        state.paused = false;
799        state.current_depth = 2;
800        assert!(state.should_pause(1, "main"));
801    }
802
803    #[test]
804    fn test_debugger_coverage_marks() {
805        let mut tracker = CoverageTracker::new();
806        tracker.chunk_sizes.insert("main".to_string(), 10);
807        tracker.chunks.insert("main".to_string(), vec![0u8; 2]);
808        tracker.mark("main", 0);
809        tracker.mark("main", 1);
810        tracker.mark("main", 9);
811        assert!(tracker.is_covered("main", 0));
812        assert!(tracker.is_covered("main", 9));
813        assert!(!tracker.is_covered("main", 5));
814    }
815
816    #[test]
817    fn test_coverage_percent() {
818        let mut tracker = CoverageTracker::new();
819        tracker.chunk_sizes.insert("f".to_string(), 4);
820        tracker.chunks.insert("f".to_string(), vec![0u8; 1]);
821        tracker.mark("f", 0);
822        tracker.mark("f", 1);
823        let pct = tracker.coverage_percent("f");
824        assert!((pct - 50.0).abs() < 0.1);
825    }
826
827    #[test]
828    fn test_uncovered_lines() {
829        let mut tracker = CoverageTracker::new();
830        tracker.chunk_sizes.insert("g".to_string(), 3);
831        tracker.chunks.insert("g".to_string(), vec![0u8; 1]);
832        tracker.mark("g", 1);
833        let uc = tracker.uncovered_lines("g");
834        assert_eq!(uc, vec![0, 2]);
835    }
836
837    #[test]
838    fn test_debug_command_parse_continue() {
839        assert_eq!(DebugCommand::parse("c"),        DebugCommand::Continue);
840        assert_eq!(DebugCommand::parse("continue"), DebugCommand::Continue);
841    }
842
843    #[test]
844    fn test_debug_command_parse_breakpoint() {
845        assert_eq!(DebugCommand::parse("bp 10"), DebugCommand::SetBreakpoint(BreakpointKind::Line(10)));
846        assert_eq!(DebugCommand::parse("bp fn:foo"), DebugCommand::SetBreakpoint(BreakpointKind::Function("foo".to_string())));
847        assert_eq!(DebugCommand::parse("bp exception"), DebugCommand::SetBreakpoint(BreakpointKind::Exception));
848    }
849
850    #[test]
851    fn test_debug_command_evaluate() {
852        assert_eq!(DebugCommand::parse("eval 1+2"), DebugCommand::Evaluate("1+2".to_string()));
853    }
854
855    #[test]
856    fn test_watch_expression_eval() {
857        let mut vm = make_vm();
858        let w = WatchExpression::new("1 + 2");
859        let result = w.evaluate(&mut vm);
860        assert_eq!(result, "3");
861    }
862
863    #[test]
864    fn test_session_set_breakpoint() {
865        let mut vm      = make_vm();
866        let mut session = DebugSession::new();
867        let resp = session.execute_command(&mut vm, "bp 5");
868        assert!(resp.contains("Breakpoint"));
869        assert!(!session.debugger.state.breakpoints.is_empty());
870    }
871
872    #[test]
873    fn test_session_eval() {
874        let mut vm      = make_vm();
875        let mut session = DebugSession::new();
876        let resp = session.execute_command(&mut vm, "eval 10 * 3");
877        assert!(resp.contains("30"));
878    }
879
880    #[test]
881    fn test_format_value_table() {
882        let t = Table::new();
883        t.set(Value::Int(1), Value::Int(42));
884        let s = LocalInspector::format_value(&Value::Table(t), 1);
885        assert!(s.contains("42"));
886    }
887}