7.4 Leo Debugger
In this module, we'll explore Leo's built-in debugger. You'll learn how to pause, inspect, and step through code in a controlled environment. Along the way, we’ll also unpack some helpful commands, examine breakpoints, and simulate more complex behaviors like async execution and mapping access. If you're familiar with debuggers in languages like Python or Rust, it should help, though there are a few Leo-specific quirks to keep in mind.
Getting Set Up
To follow along, make sure you’ve done the following:
Installed the latest version of Leo.
Cloned the workshop repository or created a project to test against:
https://github.com/ProvableHQ/workshop/tree/master/debuggin-out
Navigated into your project directory.
cd workshop/debuggin-out/point_math
Then launch the debugger:
leo debug
If everything is installed correctly, you’ll see a prompt like this:
✔ Command? ·
You’re now inside the Leo Interpreter, an interactive Read-Eval-Print Loop (REPL) environment designed for stepping through Leo code.
First Commands
Let’s ease in by running a few basic commands. Start by typing:
#help
This will display a list of all available commands. Some of the most commonly used are:
#into
or#i
– Step into a function or expression#step
or#s
– Take a single step in evaluation#over
or#o
– Evaluate the current statement or expression#run
or#r
– Run until completion#break <program> <line_number>
– Set a breakpoint#restore
– Revert to the last stable state#set_program <program>
– Set a working program context
These commands will become second nature as you begin debugging more frequently.
Using the REPL
Before we step into a real program, try evaluating some simple Leo code directly at the prompt.
✔ Command? · 3u8 + 4u8
Result: 7u8
Declare a variable:
✔ Command? · let x: u32 = 10u32;
✔ Command? · x
Result: 10u32
The REPL behaves just like a live Leo environment. You can declare variables, assign values, even create structs, so long as your program context is properly set.
Stepping Through a Function
Let’s walk through the sqrt_bitwise
function in the point_math
program. This function implements an integer square root using bitwise logic.
Start by setting the program context:
✔ Command? · #set_program point_math
Then evaluate the function step-by-step:
✔ Command? · #i sqrt_bitwise(9u32)
✔ Command? · #o
Result: 3u32
Try it again with different inputs:
✔ Command? · #i sqrt_bitwise(0u32)
✔ Command? · #o
Result: 0u32
✔ Command? · #i sqrt_bitwise(4u32)
✔ Command? · #o
Result: 2u32
Each invocation gives you a chance to step in, pause, and understand the control flow in detail.
Creating Structs and Working with Data
Let’s go one level deeper and work with a custom struct. This example uses the Point
struct defined in point_math.aleo
.
Create two points:
✔ Command? · let p1: Point = create_point(1u32, 2u32);
✔ Command? · let p2: Point = create_point(3u32, 4u32);
Check their values:
✔ Command? · p1
Result: Point { owner: ..., x: 1u32, y: 2u32 }
✔ Command? · p2
Result: Point { owner: ..., x: 3u32, y: 4u32 }
Calculate the distance:
✔ Command? · let d: u32 = distance(p1, p2);
✔ Command? · d
Result: 2u32
And finally, sum the two points:
✔ Command? · let sum: Point = add_points(p1, p2);
✔ Command? · sum
Result: Point { owner: ..., x: 4u32, y: 6u32 }
All of this happens within the debugger, no need to compile and re-run the full program.
Pro Tip: Debugger Gotchas
Leo’s debugger is powerful, but it’s not immune to user error. If you ever find yourself in an invalid state, perhaps after a mistyped line of code or an overflow, the REPL might freeze or reject new input.
To recover:
✔ Command? · #restore
This resets the interpreter to the last good state.
Watching Expressions
Sometimes, you’ll want to keep an eye on a particular value as you step through the code. The #watch
command does exactly that.
✔ Command? · #watch res
Now every step will also show the current value of res
.
You can add multiple watchpoints for variables or expressions you're tracking across iterations.
Bonus: The GUI Mode
Prefer a more visual experience? The debugger supports a text-based UI (TUI) mode that highlights the current line of code and shows watch variables in real-time.
Just launch the debugger like this:
leo debug --tui
You’ll see something like this:
┌───────────────────────────────┐
│ let res: u32 = 0u32; │ <–– currently evaluated line
│ for inv_shift in ... │
└───────────────────────────────┘
Useful when stepping through more complex loops or recursive functions.
Advanced Example: Working With Futures
Let’s test your skills on a slightly more complex program. Go to the following directory:
cd workshop/debuggin-out/access_control
leo debug
Now create and await a future:
✔ Command? · #set_program access_control
✔ Command? · let f: Future = set_timelock(self.caller, 1u32);
✔ Command? · f.await()
Result: ()
Inspect mappings with cheatcodes:
✔ Command? · CheatCode::print_mapping(timelocks)
✔ Command? · let m: Metadata = Metadata { locker: self.caller, lockee: self.caller };
✔ Command? · timelocks.get(m)
Result: 1u32
You’ve now stepped into the world of async debugging, one of Leo’s more advanced features.
Challenge
Try this exercise to check your understanding.
cd workshop/debuggin-out/timelocked_credits
leo debug
Deposit 10 credits using the REPL.
Increment block height by 1, attempt withdrawal.
Increment again by 3, then withdraw.
Use
#i
and#s
to follow execution closely.
Hint: Use CheatCode::set_block_height(2u32)
to simulate block progression.
Bytecode Debugging
Leo’s debugger can also operate at the AVM (Aleo Virtual Machine) level. This is useful for auditing deployed code, especially if the source isn't available.
Use #print <REGISTER_NUMBER>
to inspect AVM register values:
✔ Command? · #p 2
Result: 123u32
This allows for incredibly low-level debugging, and we’ll explore it more in a future module on AVM internals.
If you run into any issues or have feedback, don’t hesitate to open an issue on the Leo GitHub repo. Also remember that debugging is a skill that grows with practice!
Last updated