...
This commit is contained in:
54
rhai_engine/rhaibook/engine/debugging/break-points.md
Normal file
54
rhai_engine/rhaibook/engine/debugging/break-points.md
Normal file
@@ -0,0 +1,54 @@
|
||||
Break-Points
|
||||
============
|
||||
|
||||
{{#include ../../links.md}}
|
||||
|
||||
A _break-point_ **always** stops the current evaluation and calls the [debugging][debugger]
|
||||
callback.
|
||||
|
||||
A break-point is represented by the `debugger::BreakPoint` type, which is an `enum` with
|
||||
the following variants.
|
||||
|
||||
| `BreakPoint` variant | Not available under | Description |
|
||||
| ---------------------------------------- | :-----------------: | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `AtPosition { source, pos, enabled }` | [`no_position`] | breaks at the specified position in the specified source (empty if none);<br/>if `pos` is at beginning of line, breaks anywhere on the line |
|
||||
| `AtFunctionName { name, enabled }` | | breaks when a function matching the specified name is called (can be [operator]) |
|
||||
| `AtFunctionCall { name, args, enabled }` | | breaks when a function matching the specified name (can be [operator]) and the specified number of arguments is called |
|
||||
| `AtProperty { name, enabled }` | [`no_object`] | breaks at the specified property access |
|
||||
|
||||
|
||||
Access Break-Points
|
||||
-------------------
|
||||
|
||||
The following [`debugger::Debugger`] methods allow access to break-points for manipulation.
|
||||
|
||||
| Method | Return type | Description |
|
||||
| ------------------ | :--------------------: | ------------------------------------------------- |
|
||||
| `break_points` | `&[BreakPoint]` | returns a slice of all `BreakPoint`'s |
|
||||
| `break_points_mut` | `&mut Vec<BreakPoint>` | returns a mutable reference to all `BreakPoint`'s |
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
```rust
|
||||
use rhai::debugger::*;
|
||||
|
||||
let debugger = &mut context.global_runtime_state_mut().debugger_mut();
|
||||
|
||||
// Get number of break-points.
|
||||
let num_break_points = debugger.break_points().len();
|
||||
|
||||
// Add a new break-point on calls to 'foo(_, _, _)'
|
||||
debugger.break_points_mut().push(
|
||||
BreakPoint::AtFunctionCall { name: "foo".into(), args: 3 }
|
||||
);
|
||||
|
||||
// Display all break-points
|
||||
for bp in debugger.break_points().iter() {
|
||||
println!("{bp}");
|
||||
}
|
||||
|
||||
// Clear all break-points
|
||||
debugger.break_points_mut().clear();
|
||||
```
|
32
rhai_engine/rhaibook/engine/debugging/call-stack.md
Normal file
32
rhai_engine/rhaibook/engine/debugging/call-stack.md
Normal file
@@ -0,0 +1,32 @@
|
||||
Call Stack
|
||||
==========
|
||||
|
||||
{{#include ../../links.md}}
|
||||
|
||||
```admonish info.side.wide "Call stack frames"
|
||||
|
||||
Each "frame" in the call stack corresponds to one layer of [function] call (script-defined
|
||||
or native Rust).
|
||||
|
||||
A call stack frame has the type `debugger::CallStackFrame`.
|
||||
```
|
||||
|
||||
The [debugger] keeps a _call stack_ of [function] calls with argument values.
|
||||
|
||||
This call stack can be examined to determine the control flow at any particular point.
|
||||
|
||||
The `Debugger::call_stack` method returns a slice of all call stack frames.
|
||||
|
||||
```rust
|
||||
use rhai::debugger::*;
|
||||
|
||||
let debugger = &mut context.global_runtime_state().debugger();
|
||||
|
||||
// Get depth of the call stack.
|
||||
let depth = debugger.call_stack().len();
|
||||
|
||||
// Display all function calls
|
||||
for frame in debugger.call_stack().iter() {
|
||||
println!("{frame}");
|
||||
}
|
||||
```
|
110
rhai_engine/rhaibook/engine/debugging/debugger.md
Normal file
110
rhai_engine/rhaibook/engine/debugging/debugger.md
Normal file
@@ -0,0 +1,110 @@
|
||||
Register with the Debugger
|
||||
==========================
|
||||
|
||||
{{#include ../../links.md}}
|
||||
|
||||
Hooking up a debugging interface is as simple as providing closures to the [`Engine`]'s built-in
|
||||
debugger via `Engine::register_debugger`.
|
||||
|
||||
```rust
|
||||
use rhai::debugger::{ASTNode, DebuggerCommand};
|
||||
|
||||
let mut engine = Engine::new();
|
||||
|
||||
engine.register_debugger(
|
||||
// Provide a callback to initialize the debugger state
|
||||
|engine, mut debugger| {
|
||||
debugger.set_state(...);
|
||||
debugger
|
||||
},
|
||||
// Provide a callback for each debugging step
|
||||
|context, event, node, source, pos| {
|
||||
...
|
||||
|
||||
DebuggerCommand::StepOver
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
~~~admonish tip.small "Tip: Accessing the `Debugger`"
|
||||
|
||||
The type `debugger::Debugger` allows for manipulating [break-points], among others.
|
||||
|
||||
The [`Engine`]'s debugger instance can be accessed via `context.global_runtime_state().debugger()` (immutable)
|
||||
or `context.global_runtime_state_mut().debugger_mut()` (mutable).
|
||||
~~~
|
||||
|
||||
|
||||
Callback Functions Signature
|
||||
----------------------------
|
||||
|
||||
There are two callback functions to register for the debugger.
|
||||
|
||||
The first is simply a function to initialize the state of the debugger with the following signature.
|
||||
|
||||
> ```rust
|
||||
> Fn(&Engine, debugger::Debugger) -> debugger::Debugger
|
||||
> ```
|
||||
|
||||
The second callback is a function which will be called by the debugger during each step, with the
|
||||
following signature.
|
||||
|
||||
> ```rust
|
||||
> Fn(context: EvalContext, event: debugger::DebuggerEvent, node: ASTNode, source: &str, pos: Position) -> Result<debugger::DebuggerCommand, Box<EvalAltResult>>
|
||||
> ```
|
||||
|
||||
where:
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | :-------------: | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `context` | [`EvalContext`] | the current _evaluation context_ |
|
||||
| `event` | `DebuggerEvent` | an `enum` indicating the event that triggered the debugger |
|
||||
| `node` | `ASTNode` | an `enum` with two variants: `Expr` or `Stmt`, corresponding to the current expression node or statement node in the [`AST`] |
|
||||
| `source` | `&str` | the source of the current [`AST`], or empty if none |
|
||||
| `pos` | `Position` | position of the current node, same as `node.position()` |
|
||||
|
||||
and [`EvalContext`] is a type that encapsulates the current _evaluation context_.
|
||||
|
||||
### Event
|
||||
|
||||
The `event` parameter of the second closure passed to `Engine::register_debugger` contains a
|
||||
`debugger::DebuggerEvent` which is an `enum` with the following variants.
|
||||
|
||||
| `DebuggerEvent` variant | Description |
|
||||
| -------------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `Start` | the debugger is triggered at the beginning of evaluation |
|
||||
| `Step` | the debugger is triggered at the next step of evaluation |
|
||||
| `BreakPoint(`_n_`)` | the debugger is triggered by the _n_-th [break-point] |
|
||||
| `FunctionExitWithValue(`_r_`)` | the debugger is triggered by a function call returning with value _r_ which is `&Dynamic` |
|
||||
| `FunctionExitWithError(`_err_`)` | the debugger is triggered by a function call exiting with error _err_ which is `&EvalAltResult` |
|
||||
| `End` | the debugger is triggered at the end of evaluation |
|
||||
|
||||
### Return value
|
||||
|
||||
```admonish tip.side.wide "Tip: Initialization"
|
||||
|
||||
When a script starts evaluation, the debugger always stops at the very _first_ [`AST`] node
|
||||
with the `event` parameter set to `DebuggerStatus::Start`.
|
||||
|
||||
This allows initialization to be done (e.g. setting up [break-points]).
|
||||
```
|
||||
|
||||
The second closure passed to `Engine::register_debugger` will be called when stepping into or over
|
||||
expressions and statements, or when [break-points] are hit.
|
||||
|
||||
The return type of the closure is `Result<debugger::DebuggerCommand, Box<EvalAltResult>>`.
|
||||
|
||||
If an error is returned, the script evaluation at that particular instance returns with that
|
||||
particular error. It is thus possible to _abort_ the script evaluation by returning an error that is
|
||||
not _catchable_, such as `EvalAltResult::ErrorTerminated`.
|
||||
|
||||
If no error is returned, then the return `debugger::DebuggerCommand` variant determines the
|
||||
continued behavior of the debugger.
|
||||
|
||||
| `DebuggerCommand` variant | Behavior | `gdb` equivalent |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | :--------------: |
|
||||
| `Continue` | continue with normal script evaluation | `continue` |
|
||||
| `StepInto` | run to the next expression or statement, diving into functions | `step` |
|
||||
| `StepOver` | run to the next expression or statement, skipping over functions | |
|
||||
| `Next` | run to the next statement, skipping over functions | `next` |
|
||||
| `FunctionExit` | run to the end of the current function call; debugger is triggered _before_ the function call returns and the [`Scope`] cleared | `finish` |
|
50
rhai_engine/rhaibook/engine/debugging/index.md
Normal file
50
rhai_engine/rhaibook/engine/debugging/index.md
Normal file
@@ -0,0 +1,50 @@
|
||||
Debugging Interface
|
||||
===================
|
||||
|
||||
{{#include ../../links.md}}
|
||||
|
||||
For systems open to external user-created scripts, it is usually desirable to provide a _debugging_
|
||||
experience to the user. The alternative is to provide a custom implementation of [`debug`] via
|
||||
`Engine::on_debug` that traps debug output to show in a side panel, for example, which is actually
|
||||
extremely simple.
|
||||
|
||||
Nevertheless, in some systems, it may not be convenient, or even possible, for the user to debug his
|
||||
or her scripts simply via good-old [`print`] or [`debug`] statements – the system does not
|
||||
have any facility for printed output, for instance.
|
||||
|
||||
Or the system may require more advanced debugging facilities than mere [`print`] statements –
|
||||
such as [break-points].
|
||||
|
||||
For these advanced scenarios, Rhai contains a _Debugging_ interface, turned on via the [`debugging`]
|
||||
feature (which implies the [`internals`] feature).
|
||||
|
||||
The debugging interface resides under the `debugger` sub-module.
|
||||
|
||||
|
||||
```admonish tip.small "The Rhai Debugger"
|
||||
|
||||
The [`rhai-dbg`]({{repoHome}}/src/bin/rhai-dbg.rs) bin tool shows a simple example of
|
||||
employing the debugging interface to create a debugger for Rhai scripts!
|
||||
```
|
||||
|
||||
|
||||
Built-in Functions
|
||||
------------------
|
||||
|
||||
The following functions (defined in the [`DebuggingPackage`][built-in packages] but excluded when
|
||||
using a [raw `Engine`]) provides runtime information for debugging purposes.
|
||||
|
||||
| Function | Parameter(s) | Not available under | Description |
|
||||
| ------------ | ------------ | :---------------------------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `back_trace` | _none_ | [`no_function`], [`no_index`] | returns an [array] of [object maps] or [strings], each containing one level of [function] call;</br>returns an empty [array] if no [debugger] is registered |
|
||||
|
||||
```rust
|
||||
// This recursive function prints its own call stack during each run
|
||||
fn foo(x) {
|
||||
print(back_trace()); // prints the current call stack
|
||||
|
||||
if x > 0 {
|
||||
foo(x - 1)
|
||||
}
|
||||
}
|
||||
```
|
92
rhai_engine/rhaibook/engine/debugging/server.md
Normal file
92
rhai_engine/rhaibook/engine/debugging/server.md
Normal file
@@ -0,0 +1,92 @@
|
||||
Implement a Debugging Server
|
||||
============================
|
||||
|
||||
Sometimes it is desirable to embed a debugging _server_ inside the application such that an external
|
||||
debugger interface can connect to the application's running instance at runtime.
|
||||
|
||||
This way, when scripts are run within the application, it is easy for an external interface to debug
|
||||
those scripts as they run.
|
||||
|
||||
Such connections may take the form of any communication channel, for example a TCP/IP connection, a
|
||||
named pipe, or an MPSC channel.
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
### Server side
|
||||
|
||||
The following example assumes bi-direction, blocking messaging channels, such as a WebSocket
|
||||
connection, with a server that accepts connections and creates those channels.
|
||||
|
||||
```rust
|
||||
use rhai::debugger::{ASTNode, DebuggerCommand};
|
||||
|
||||
let mut engine = Engine::new();
|
||||
|
||||
engine.register_debugger(
|
||||
// Use the initialization callback to set up the communications channel
|
||||
// and listen to it
|
||||
|engine, mut debugger| {
|
||||
// Create server that will listen to requests
|
||||
let mut server = MyCommServer::new();
|
||||
server.listen("localhost:8080");
|
||||
|
||||
// Wrap it up in a shared locked cell so it can be 'Clone'
|
||||
let server = Rc::new(RefCell::new(server));
|
||||
|
||||
// Store the channel in the debugger state
|
||||
debugger.set_state(Dynamic::from(server));
|
||||
debugger
|
||||
},
|
||||
// Trigger the server during each debugger stop point
|
||||
|context, event, node, source, pos| {
|
||||
// Get the state
|
||||
let mut state = context.tag_mut();
|
||||
|
||||
// Get the server
|
||||
let mut server = state.write_lock::<MyCommServer>().unwrap();
|
||||
|
||||
// Send the event to the server - blocking call
|
||||
server.send_message(...);
|
||||
|
||||
// Receive command - blocking call
|
||||
match server.receive_message() {
|
||||
None => DebuggerCommand::StepOver,
|
||||
// Decode command
|
||||
Ok(...) => { ... }
|
||||
Ok(...) => { ... }
|
||||
Ok(...) => { ... }
|
||||
Ok(...) => { ... }
|
||||
Ok(...) => { ... }
|
||||
:
|
||||
:
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Client side
|
||||
|
||||
The client can be any system that can work with WebSockets for messaging.
|
||||
|
||||
```js
|
||||
// Connect to the application's debugger
|
||||
let webSocket = new WebSocket("wss://localhost:8080");
|
||||
|
||||
webSocket.on_message = (event) => {
|
||||
let msg = JSON.parse(event.data);
|
||||
|
||||
switch msg.type {
|
||||
// handle debugging events from the application...
|
||||
case "step": {
|
||||
:
|
||||
}
|
||||
:
|
||||
:
|
||||
}
|
||||
};
|
||||
|
||||
// Send command to the application
|
||||
webSocket.send("step-over");
|
||||
```
|
67
rhai_engine/rhaibook/engine/debugging/state.md
Normal file
67
rhai_engine/rhaibook/engine/debugging/state.md
Normal file
@@ -0,0 +1,67 @@
|
||||
Debugger State
|
||||
==============
|
||||
|
||||
{{#include ../../links.md}}
|
||||
|
||||
Sometimes it is useful to keep a persistent _state_ within the [debugger].
|
||||
|
||||
The `Engine::register_debugger` API accepts a function that returns the initial value of the
|
||||
[debugger's][debugger] state, which is a [`Dynamic`] and can hold any value.
|
||||
|
||||
This state value is the stored into the [debugger]'s custom state.
|
||||
|
||||
|
||||
Access the Debugger State
|
||||
-------------------------
|
||||
|
||||
Use `EvalContext::global_runtime_state().debugger()` (immutable) or
|
||||
`EvalContext::global_runtime_state_mut().debugger_mut()` (mutable) to gain access to the current
|
||||
[`debugger::Debugger`] instance.
|
||||
|
||||
The following [`debugger::Debugger`] methods allow access to the custom [debugger] state.
|
||||
|
||||
| Method | Parameter type | Return type | Description |
|
||||
| ----------- | :-------------------------------: | :-------------------------: | ----------------------------------------------- |
|
||||
| `state` | _none_ | [`&Dynamic`][`Dynamic`] | returns the custom state |
|
||||
| `state_mut` | _none_ | [`&mut Dynamic`][`Dynamic`] | returns a mutable reference to the custom state |
|
||||
| `set_state` | [`impl Into<Dynamic>`][`Dynamic`] | _none_ | sets the value of the custom state |
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
```rust
|
||||
engine.register_debugger(
|
||||
|engine, mut debugger| {
|
||||
// Say, use an object map for the debugger state
|
||||
let mut state = Map::new();
|
||||
// Initialize properties
|
||||
state.insert("hello".into(), 42_64.into());
|
||||
state.insert("foo".into(), false.into());
|
||||
|
||||
debugger.set_state(state);
|
||||
debugger
|
||||
},
|
||||
|context, node, source, pos| {
|
||||
// Print debugger state - which is an object map
|
||||
let state = context.global_runtime_state().debugger().state();
|
||||
println!("Current state = {state}");
|
||||
|
||||
// Get the state as an object map
|
||||
let mut state = context.global_runtime_state_mut()
|
||||
.debugger_mut().state_mut()
|
||||
.write_lock::<Map>().unwrap();
|
||||
|
||||
// Read state
|
||||
let hello = state.get("hello").unwrap().as_int().unwrap();
|
||||
|
||||
// Modify state
|
||||
state.insert("hello".into(), (hello + 1).into());
|
||||
state.insert("foo".into(), true.into());
|
||||
state.insert("something_new".into(), "hello, world!".into());
|
||||
|
||||
// Continue with debugging
|
||||
Ok(DebuggerCommand::StepInto)
|
||||
}
|
||||
);
|
||||
```
|
Reference in New Issue
Block a user