rhai/eval/
debugger.rs

1//! Module defining the debugging interface.
2#![cfg(feature = "debugging")]
3
4use super::{Caches, EvalContext, GlobalRuntimeState};
5use crate::ast::{ASTNode, Expr, Stmt};
6use crate::{
7    Dynamic, Engine, EvalAltResult, ImmutableString, Position, RhaiResultOf, Scope, ThinVec,
8};
9#[cfg(feature = "no_std")]
10use std::prelude::v1::*;
11use std::{fmt, iter::repeat, mem};
12
13/// Callback function to initialize the debugger.
14#[cfg(not(feature = "sync"))]
15pub type OnDebuggingInit = dyn Fn(&Engine, Debugger) -> Debugger;
16/// Callback function to initialize the debugger.
17#[cfg(feature = "sync")]
18pub type OnDebuggingInit = dyn Fn(&Engine, Debugger) -> Debugger + Send + Sync;
19
20/// Callback function for debugging.
21#[cfg(not(feature = "sync"))]
22pub type OnDebuggerCallback = dyn Fn(
23    EvalContext,
24    DebuggerEvent,
25    ASTNode,
26    Option<&str>,
27    Position,
28) -> RhaiResultOf<DebuggerCommand>;
29/// Callback function for debugging.
30#[cfg(feature = "sync")]
31pub type OnDebuggerCallback = dyn Fn(EvalContext, DebuggerEvent, ASTNode, Option<&str>, Position) -> RhaiResultOf<DebuggerCommand>
32    + Send
33    + Sync;
34
35/// A command for the debugger on the next iteration.
36#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Hash)]
37#[non_exhaustive]
38pub enum DebuggerCommand {
39    /// Continue normal execution.
40    #[default]
41    Continue,
42    /// Step into the next expression, diving into functions.
43    StepInto,
44    /// Run to the next expression or statement, stepping over functions.
45    StepOver,
46    /// Run to the next statement, skipping over functions.
47    Next,
48    /// Run to the end of the current function call.
49    FunctionExit,
50}
51
52/// The debugger status.
53#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
54#[non_exhaustive]
55pub enum DebuggerStatus {
56    // Script evaluation starts.
57    Init,
58    // Stop at the next statement or expression.
59    Next(bool, bool),
60    // Run to the end of the current level of function call.
61    FunctionExit(usize),
62    // Script evaluation ends.
63    Terminate,
64}
65
66impl DebuggerStatus {
67    pub const CONTINUE: Self = Self::Next(false, false);
68    pub const STEP: Self = Self::Next(true, true);
69    pub const NEXT: Self = Self::Next(true, false);
70    pub const INTO: Self = Self::Next(false, true);
71}
72
73/// A event that triggers the debugger.
74#[derive(Debug, Clone, Copy)]
75#[non_exhaustive]
76pub enum DebuggerEvent<'a> {
77    /// Script evaluation starts.
78    Start,
79    /// Break on next step.
80    Step,
81    /// Break on break-point.
82    BreakPoint(usize),
83    /// Return from a function with a value.
84    FunctionExitWithValue(&'a Dynamic),
85    /// Return from a function with a value.
86    FunctionExitWithError(&'a EvalAltResult),
87    /// Script evaluation ends.
88    End,
89}
90
91/// A break-point for debugging.
92#[derive(Debug, Clone, Eq, PartialEq, Hash)]
93#[non_exhaustive]
94pub enum BreakPoint {
95    /// Break at a particular position under a particular source.
96    ///
97    /// Not available under `no_position`.
98    #[cfg(not(feature = "no_position"))]
99    AtPosition {
100        /// Source (empty if not available) of the break-point.
101        source: Option<ImmutableString>,
102        /// [Position] of the break-point.
103        pos: Position,
104        /// Is the break-point enabled?
105        enabled: bool,
106    },
107    /// Break at a particular function call.
108    AtFunctionName {
109        /// Function name.
110        name: ImmutableString,
111        /// Is the break-point enabled?
112        enabled: bool,
113    },
114    /// Break at a particular function call with a particular number of arguments.
115    AtFunctionCall {
116        /// Function name.
117        name: ImmutableString,
118        /// Number of arguments.
119        args: usize,
120        /// Is the break-point enabled?
121        enabled: bool,
122    },
123    /// Break at a particular property .
124    ///
125    /// Not available under `no_object`.
126    #[cfg(not(feature = "no_object"))]
127    AtProperty {
128        /// Property name.
129        name: ImmutableString,
130        /// Is the break-point enabled?
131        enabled: bool,
132    },
133}
134
135impl fmt::Display for BreakPoint {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        match self {
138            #[cfg(not(feature = "no_position"))]
139            Self::AtPosition {
140                source,
141                pos,
142                enabled,
143            } => {
144                if let Some(ref source) = source {
145                    write!(f, "{source} ")?;
146                }
147                write!(f, "@ {pos:?}")?;
148                if !*enabled {
149                    f.write_str(" (disabled)")?;
150                }
151                Ok(())
152            }
153            Self::AtFunctionName { name, enabled } => {
154                write!(f, "{name} (...)")?;
155                if !*enabled {
156                    f.write_str(" (disabled)")?;
157                }
158                Ok(())
159            }
160            Self::AtFunctionCall {
161                name,
162                args,
163                enabled,
164            } => {
165                write!(
166                    f,
167                    "{name} ({})",
168                    repeat("_").take(*args).collect::<Vec<_>>().join(", ")
169                )?;
170                if !*enabled {
171                    f.write_str(" (disabled)")?;
172                }
173                Ok(())
174            }
175            #[cfg(not(feature = "no_object"))]
176            Self::AtProperty { name, enabled } => {
177                write!(f, ".{name}")?;
178                if !*enabled {
179                    f.write_str(" (disabled)")?;
180                }
181                Ok(())
182            }
183        }
184    }
185}
186
187impl BreakPoint {
188    /// Is this [`BreakPoint`] enabled?
189    #[inline(always)]
190    #[must_use]
191    pub const fn is_enabled(&self) -> bool {
192        match self {
193            #[cfg(not(feature = "no_position"))]
194            Self::AtPosition { enabled, .. } => *enabled,
195            Self::AtFunctionName { enabled, .. } | Self::AtFunctionCall { enabled, .. } => *enabled,
196            #[cfg(not(feature = "no_object"))]
197            Self::AtProperty { enabled, .. } => *enabled,
198        }
199    }
200    /// Enable/disable this [`BreakPoint`].
201    #[inline(always)]
202    pub fn enable(&mut self, value: bool) {
203        match self {
204            #[cfg(not(feature = "no_position"))]
205            Self::AtPosition { enabled, .. } => *enabled = value,
206            Self::AtFunctionName { enabled, .. } | Self::AtFunctionCall { enabled, .. } => {
207                *enabled = value
208            }
209            #[cfg(not(feature = "no_object"))]
210            Self::AtProperty { enabled, .. } => *enabled = value,
211        }
212    }
213}
214
215/// A function call.
216#[derive(Debug, Clone, Hash)]
217pub struct CallStackFrame {
218    /// Function name.
219    pub fn_name: ImmutableString,
220    /// Copies of function call arguments, if any.
221    pub args: ThinVec<Dynamic>,
222    /// Source of the function.
223    pub source: Option<ImmutableString>,
224    /// [Position][`Position`] of the function call.
225    pub pos: Position,
226}
227
228impl fmt::Display for CallStackFrame {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        let mut fp = f.debug_tuple(&self.fn_name);
231
232        for arg in &self.args {
233            fp.field(arg);
234        }
235
236        fp.finish()?;
237
238        if !self.pos.is_none() {
239            if let Some(ref source) = self.source {
240                write!(f, ": {source}")?;
241            }
242            write!(f, " @ {:?}", self.pos)?;
243        }
244
245        Ok(())
246    }
247}
248
249/// A type providing debugging facilities.
250#[derive(Debug, Clone, Hash)]
251pub struct Debugger {
252    /// The current status command.
253    pub(crate) status: DebuggerStatus,
254    /// The current set of break-points.
255    break_points: Vec<BreakPoint>,
256    /// The current function call stack.
257    call_stack: Vec<CallStackFrame>,
258    /// The current state.
259    state: Dynamic,
260}
261
262impl Debugger {
263    /// Create a new [`Debugger`].
264    #[inline(always)]
265    #[must_use]
266    pub fn new(status: DebuggerStatus) -> Self {
267        Self {
268            status,
269            break_points: Vec::new(),
270            call_stack: Vec::new(),
271            state: Dynamic::UNIT,
272        }
273    }
274    /// Get the current call stack.
275    #[inline(always)]
276    #[must_use]
277    pub fn call_stack(&self) -> &[CallStackFrame] {
278        &self.call_stack
279    }
280    /// Rewind the function call stack to a particular depth.
281    #[inline(always)]
282    pub(crate) fn rewind_call_stack(&mut self, len: usize) {
283        self.call_stack.truncate(len);
284    }
285    /// Add a new frame to the function call stack.
286    #[inline(always)]
287    pub(crate) fn push_call_stack_frame(
288        &mut self,
289        fn_name: ImmutableString,
290        args: impl IntoIterator<Item = Dynamic>,
291        source: Option<ImmutableString>,
292        pos: Position,
293    ) {
294        self.call_stack.push(CallStackFrame {
295            fn_name,
296            args: args.into_iter().collect(),
297            source,
298            pos,
299        });
300    }
301    /// Change the current status to [`CONTINUE`][DebuggerStatus::CONTINUE] and return the previous status.
302    pub(crate) fn clear_status_if(
303        &mut self,
304        filter: impl FnOnce(&DebuggerStatus) -> bool,
305    ) -> Option<DebuggerStatus> {
306        if filter(&self.status) {
307            Some(mem::replace(&mut self.status, DebuggerStatus::CONTINUE))
308        } else {
309            None
310        }
311    }
312    /// Override the status of this [`Debugger`] if the current status is
313    /// [`CONTINUE`][DebuggerStatus::CONTINUE].
314    #[inline(always)]
315    pub(crate) fn reset_status(&mut self, status: DebuggerStatus) {
316        if self.status == DebuggerStatus::CONTINUE {
317            self.status = status;
318        }
319    }
320    /// Returns the first break-point triggered by a particular [`AST` Node][ASTNode].
321    #[must_use]
322    pub fn is_break_point(&self, src: Option<&str>, node: ASTNode) -> Option<usize> {
323        let _src = src;
324
325        self.break_points()
326            .iter()
327            .enumerate()
328            .filter(|(.., bp)| bp.is_enabled())
329            .find(|(.., bp)| match bp {
330                #[cfg(not(feature = "no_position"))]
331                BreakPoint::AtPosition { pos, .. } if pos.is_none() => false,
332                #[cfg(not(feature = "no_position"))]
333                BreakPoint::AtPosition { source, pos, .. } if pos.is_beginning_of_line() => {
334                    node.position().line().unwrap_or(0) == pos.line().unwrap()
335                        && _src == source.as_deref()
336                }
337                #[cfg(not(feature = "no_position"))]
338                BreakPoint::AtPosition { source, pos, .. } => {
339                    node.position() == *pos && _src == source.as_deref()
340                }
341                BreakPoint::AtFunctionName { name, .. } => match node {
342                    ASTNode::Expr(Expr::FnCall(x, ..)) | ASTNode::Stmt(Stmt::FnCall(x, ..)) => {
343                        x.name == *name
344                    }
345                    ASTNode::Stmt(Stmt::Expr(e)) => match &**e {
346                        Expr::FnCall(x, ..) => x.name == *name,
347                        _ => false,
348                    },
349                    _ => false,
350                },
351                BreakPoint::AtFunctionCall { name, args, .. } => match node {
352                    ASTNode::Expr(Expr::FnCall(x, ..)) | ASTNode::Stmt(Stmt::FnCall(x, ..)) => {
353                        x.args.len() == *args && x.name == *name
354                    }
355                    ASTNode::Stmt(Stmt::Expr(e)) => match &**e {
356                        Expr::FnCall(x, ..) => x.args.len() == *args && x.name == *name,
357                        _ => false,
358                    },
359                    _ => false,
360                },
361                #[cfg(not(feature = "no_object"))]
362                BreakPoint::AtProperty { name, .. } => match node {
363                    ASTNode::Expr(Expr::Property(x, ..)) => x.2 == *name,
364                    _ => false,
365                },
366            })
367            .map(|(i, ..)| i)
368    }
369    /// Get a slice of all [`BreakPoint`]'s.
370    #[inline(always)]
371    #[must_use]
372    pub fn break_points(&self) -> &[BreakPoint] {
373        &self.break_points
374    }
375    /// Get the underlying [`Vec`] holding all [`BreakPoint`]'s.
376    #[inline(always)]
377    #[must_use]
378    pub fn break_points_mut(&mut self) -> &mut Vec<BreakPoint> {
379        &mut self.break_points
380    }
381    /// Get the custom state.
382    #[inline(always)]
383    pub const fn state(&self) -> &Dynamic {
384        &self.state
385    }
386    /// Get a mutable reference to the custom state.
387    #[inline(always)]
388    pub fn state_mut(&mut self) -> &mut Dynamic {
389        &mut self.state
390    }
391    /// Set the custom state.
392    #[inline(always)]
393    pub fn set_state(&mut self, state: impl Into<Dynamic>) {
394        self.state = state.into();
395    }
396}
397
398impl Engine {
399    /// Run the debugger callback if there is a debugging interface registered.
400    #[inline(always)]
401    pub(crate) fn dbg<'a>(
402        &self,
403        global: &mut GlobalRuntimeState,
404        caches: &mut Caches,
405        scope: &mut Scope,
406        this_ptr: Option<&mut Dynamic>,
407        node: impl Into<ASTNode<'a>>,
408    ) -> RhaiResultOf<()> {
409        if self.is_debugger_registered() {
410            if let Some(cmd) = self.dbg_reset_raw(global, caches, scope, this_ptr, node)? {
411                global.debugger_mut().status = cmd;
412            }
413        }
414
415        Ok(())
416    }
417    /// Run the debugger callback if there is a debugging interface registered.
418    ///
419    /// Returns [`Some`] if the debugger needs to be reactivated at the end of the block, statement or
420    /// function call.
421    ///
422    /// It is up to the [`Engine`] to reactivate the debugger.
423    #[inline(always)]
424    pub(crate) fn dbg_reset<'a>(
425        &self,
426        global: &mut GlobalRuntimeState,
427        caches: &mut Caches,
428        scope: &mut Scope,
429        this_ptr: Option<&mut Dynamic>,
430        node: impl Into<ASTNode<'a>>,
431    ) -> RhaiResultOf<Option<DebuggerStatus>> {
432        if self.is_debugger_registered() {
433            self.dbg_reset_raw(global, caches, scope, this_ptr, node)
434        } else {
435            Ok(None)
436        }
437    }
438    /// Run the debugger callback.
439    ///
440    /// Returns [`Some`] if the debugger needs to be reactivated at the end of the block, statement or
441    /// function call.
442    ///
443    /// It is up to the [`Engine`] to reactivate the debugger.
444    #[inline]
445    pub(crate) fn dbg_reset_raw<'a>(
446        &self,
447        global: &mut GlobalRuntimeState,
448        caches: &mut Caches,
449        scope: &mut Scope,
450        this_ptr: Option<&mut Dynamic>,
451        node: impl Into<ASTNode<'a>>,
452    ) -> RhaiResultOf<Option<DebuggerStatus>> {
453        let node = node.into();
454
455        // Skip transitive nodes
456        match node {
457            ASTNode::Expr(Expr::Stmt(..)) | ASTNode::Stmt(Stmt::Expr(..)) => return Ok(None),
458            _ => (),
459        }
460
461        match global.debugger {
462            Some(ref dbg) => {
463                let event = match dbg.status {
464                    DebuggerStatus::Init => Some(DebuggerEvent::Start),
465                    DebuggerStatus::NEXT if node.is_stmt() => Some(DebuggerEvent::Step),
466                    DebuggerStatus::INTO if node.is_expr() => Some(DebuggerEvent::Step),
467                    DebuggerStatus::STEP => Some(DebuggerEvent::Step),
468                    DebuggerStatus::Terminate => Some(DebuggerEvent::End),
469                    _ => None,
470                };
471
472                let event = match event {
473                    Some(e) => e,
474                    None => match dbg.is_break_point(global.source(), node) {
475                        Some(bp) => DebuggerEvent::BreakPoint(bp),
476                        None => return Ok(None),
477                    },
478                };
479
480                self.dbg_raw(global, caches, scope, this_ptr, node, event)
481            }
482            None => Ok(None),
483        }
484    }
485    /// Run the debugger callback unconditionally.
486    ///
487    /// Returns [`Some`] if the debugger needs to be reactivated at the end of the block, statement or
488    /// function call.
489    ///
490    /// It is up to the [`Engine`] to reactivate the debugger.
491    #[inline]
492    pub(crate) fn dbg_raw(
493        &self,
494        global: &mut GlobalRuntimeState,
495        caches: &mut Caches,
496        scope: &mut Scope,
497        this_ptr: Option<&mut Dynamic>,
498        node: ASTNode,
499        event: DebuggerEvent,
500    ) -> Result<Option<DebuggerStatus>, Box<crate::EvalAltResult>> {
501        match self.debugger_interface {
502            Some(ref x) => {
503                let orig_scope_len = scope.len();
504
505                let src = global.source_raw().cloned();
506                let context = EvalContext::new(self, global, caches, scope, this_ptr);
507                let (.., ref on_debugger) = *x;
508
509                let command = on_debugger(context, event, node, src.as_deref(), node.position());
510
511                if orig_scope_len != scope.len() {
512                    // The scope is changed, always search from now on
513                    global.always_search_scope = true;
514                }
515
516                match command? {
517                    DebuggerCommand::Continue => {
518                        global.debugger_mut().status = DebuggerStatus::CONTINUE;
519                        Ok(None)
520                    }
521                    DebuggerCommand::Next => {
522                        global.debugger_mut().status = DebuggerStatus::CONTINUE;
523                        Ok(Some(DebuggerStatus::NEXT))
524                    }
525                    DebuggerCommand::StepOver => {
526                        global.debugger_mut().status = DebuggerStatus::CONTINUE;
527                        Ok(Some(DebuggerStatus::STEP))
528                    }
529                    DebuggerCommand::StepInto => {
530                        global.debugger_mut().status = DebuggerStatus::STEP;
531                        Ok(None)
532                    }
533                    DebuggerCommand::FunctionExit => {
534                        // Bump a level if it is a function call
535                        let level = match node {
536                            ASTNode::Expr(Expr::FnCall(..)) | ASTNode::Stmt(Stmt::FnCall(..)) => {
537                                global.level + 1
538                            }
539                            ASTNode::Stmt(Stmt::Expr(e)) if matches!(**e, Expr::FnCall(..)) => {
540                                global.level + 1
541                            }
542                            _ => global.level,
543                        };
544                        global.debugger_mut().status = DebuggerStatus::FunctionExit(level);
545                        Ok(None)
546                    }
547                }
548            }
549            None => Ok(None),
550        }
551    }
552}