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:

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
  1. Deposit 10 credits using the REPL.

  2. Increment block height by 1, attempt withdrawal.

  3. Increment again by 3, then withdraw.

  4. 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