leo debug
is a powerful tool that developers can use to interactively step through executions and track down bugs. In this workshop, we'll use the Leo debugger to explore and gain a deeper understanding of a variety of programs. You will also build up the skills to adeptly use the debugger in your development work.
This tutorial assumes that you are already familiar with Leo. If you'd like a refresher, see the Developer Docs.
We're always looking to improve Leo's developer experience. If you have any feedback, please feel free to file an issue!
Install the latest version of the workshop.
Install Leo. You may also use the install script in the workshop.
First, let's fire up the debugger.
leo debug
You'll see this prompt pop up.
This is the Leo Interpreter. Try the command `#help`.
? Command? βΊ
Let's go ahead and run the #help
command.
β Command? Β· #help
You probably want to start by running a function or transition.
For instance
#into program.aleo/main()
Once a function is running, commands include
#into to evaluate into the next expression or statement;
#step to take one step towards evaluating the current expression or statement;
#over to complete evaluating the current expression or statement;
#run to finish evaluating
#quit to quit the interpreter.
You can set a breakpoint with
#break program_name line_number
When executing Aleo VM code, you can print the value of a register like this:
#print 2
Some of the commands may be run with one letter abbreviations, such as #i.
Note that this interpreter is not line oriented as in many common debuggers;
rather it is oriented around expressions and statements.
As you step into code, individual expressions or statements will
be evaluated one by one, including arguments of function calls.
You may simply enter Leo expressions or statements on the command line
to evaluate. For instance, if you want to see the value of a variable w:
w
If you want to set w to a new value:
w = z + 2u8;
Note that statements (like the assignment above) must end with a semicolon.
If there are futures available to be executed, they will be listed by
numerical index, and you may run them using `#future` (or `#f`); for instance
#future 0
The interpreter begins in a global context, not in any Leo program. You can set
the current program with
#set_program program_name
This allows you to refer to structs and other items in the indicated program.
The interpreter may enter an invalid state, often due to Leo code entered at the
REPL. In this case, you may use the command
#restore
Which will restore to the last saved state of the interpreter. Any time you
enter Leo code at the prompt, interpreter state is saved.
Input history is available - use the up and down arrow keys.
The leo debug
initializes a REPL loop in which you may run standalone Leo code.
β Command? Β· 1u32 + 2u32
Result: 3u32
β Command? Β· let x: u32 = 1u32;
β Command? Β· let y: u32 = 2u32;
β Command? Β· let z: u32 = x + y;
β Command? Β· x
Result: 1u32
β Command? Β· y
Result: 2u32
β Command? Β· z
Result: 3u32
More often than not, you'll be using the debugger to step through a program. Let's see how that works.
First, let's go to the program under investigation. In the directory where you installed the source material, run
cd workshop/learn_to_debug/point_math
leo debug
The debugger will type check the point_math.aleo
and initialize a REPL loop with access to the program definition. In additional to directly evaluating Leo code, the debugger provides commands with which the user can selectively step through code:
#into | #i
- to evaluate into the next expression or statement#step | #s
- to take one step towards evaluating the current expression or statement#over | #o
- to complete evaluating the current expression or statement#run | #r
- to finish evaluating#break | #b <PROGRAM_NAME> <LINE_NUMBER>
- to set a breakpoint#watch
|#w <EXPRESSION>
- to watch an expression. It's value will be printed out each step of the interpreter.
#into
is particularly useful as users can prefix a Leo statement or expression with an #into
command to step through the associated code. We'll now use the debugger to step through an evaluation of the sqrt_bitwise
function.
β Command? Β· #i point_math.aleo/sqrt_bitwise(0u32)
Prepared to evaluate:
point_math.aleo/sqrt_bitwise(0u32)
β Command? Β· #i
Prepared to evaluate:
point_math.aleo/sqrt_bitwise(0u32)
β Command? Β· #s
Result: 0u32
Prepared to evaluate:
point_math.aleo/sqrt_bitwise(0u32)
β Command? Β· #s
Prepared to evaluate:
point_math.aleo/sqrt_bitwise(0u32)
β Command? Β· #s
Result: 0u32
-
Use the above commands to step through evaluations of
sqrt_bitwise
for inputs0u32
,1u32
,4u32
,9u32
. Are they as you expect? -
Use the debugger and the above commands to: a. Create and store two distinct
Point
records. b. Calculate the distance between them. c. Add the points together.
Transcripts for each of the above challenges are given below.
β Command? Β· #i point_math.aleo/sqrt_bitwise(0u32)
Prepared to evaluate:
point_math.aleo/sqrt_bitwise(0u32)
β Command? Β· #o
Result: 0u32
β Command? Β· #i point_math.aleo/sqrt_bitwise(1u32)
Prepared to evaluate:
point_math.aleo/sqrt_bitwise(1u32)
β Command? Β· #o
Result: 1u32
β Command? Β· #i point_math.aleo/sqrt_bitwise(4u32)
Prepared to evaluate:
point_math.aleo/sqrt_bitwise(4u32)
β Command? Β· #o
Result: 2u32
β Command? Β· #i point_math.aleo/sqrt_bitwise(9u32)
Prepared to evaluate:
point_math.aleo/sqrt_bitwise(9u32)
β Command? Β· #o
Result: 3u32
β Command? Β· let p1: Point = point_math.aleo/create_point(1u32, 2u32);
β Command? Β· let p2: Point = point_math.aleo/create_point(3u32, 4u32);
β Command? Β· p1
Result: Point {owner: aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px, x: 1u32, y: 2u32}
β Command? Β· p2
Result: Point {owner: aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px, x: 3u32, y: 4u32}
β Command? Β· let distance: u32 = point_math.aleo/distance(p1, p2);
β Command? Β· distance
Result: 2u32
β Command? Β· let sum: Point = point_math.aleo/add_points(p1, p2);
β Command? Β· sum
Result: Point {owner: aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px, x: 4u32, y: 6u32}
The Leo debugger also provides a number of features to help with more advanced debugging. These will be helpful as you go through the more complex examples below.
As you step through and evaluate code, you may run into a state where your debugger halts. This can happen for a number of reasons including, accessing values that don't exist, attempting to evaluate expressions that overflow, etc. If this happens, the #restore
command can help you recover the debugger to the last code point.
The Leo debugger initializes in a "global" context, which contains the programs and dependencies in your project. When you invoke a transition or function, the debugger implicitly steps into the associated program's scope.
You may find youself wanting to initialize structs and records defined in a specific program. To do so, you can explicitly set the program scope and directly instantiate data types.
By setting a program scope, you can also directly invoke functions and interact with mappings defined in that scope. This is useful as specifying the full path (program name and resource) can be cumbersome.
β Command? Β· #set_program point_math
β Command? Β· let p: Point = Point { owner: self.caller, x: 1u32, y: 2u32 };
β Command? Β· p
Result: Point {owner: aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px, x: 1u32, y: 2u32}
β Command? Β· let a: u32 = sqrt_bitwise(0u32);
β Command? Β· a
Result: 0u32
The debugger also provides users with a number of "cheatcodes" to aid in debugging. The supported cheatcodes include:
CheatCode::print_mapping(<MAPPING>)
CheatCode::set_block_height(<u32>)
The Leo debugger also provides a user with a more sophisticated GUI, which can help step through code cleanly. The interface can be enabled with the --tui
command and provides better code visualization, where the current line is highlighted.
βcodeβββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β } β
β β
β // An implementation of integer square root. β
β function sqrt_bitwise(n: u32) -> u32 { β
β let res: u32 = 0u32; β
β // Iterate over all 32 bits from most significant to least significant β
β for inv_shift: u8 in 0u8..32u8 { β
β let shift: u8 = 31u8 - inv_shift; β
β let bit: u32 = 1u32 << shift; β
β let temp: u32 = res | bit; β
β // Check if temp is safe to square without overflow β
β if temp <= 65535u32 { // β(2^32 - 1) = 65535 β
β let square: u32 = temp.mul_wrapped(temp); β
β if square <= n { β
β res = temp; // Update res if temp^2 <= n β
β } β
β } β
β } β
β return res; β
β } β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βResultββββββββββββββββββββββββββββββββββββββββββββββWatchpointsββββββββββββββββββββββββββββββββββββββββ
β0u32 ββ β
β ββ β
β ββ β
β ββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βMessageββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βCommand:βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
In the previous example, we use leo debug
to evaluate simple transitions with strictly off-chain execution. The debugger can also be used to evaluate on-chain code.
Let's dive in further by looking at some code. First let's navigate to the code and fire up the debugger.
cd workshop/learn_to_debug/access_control
leo debug
Using the debugger, a user can directly produce and evaluate futures. Users can also interact with mappings by directly executing Leo code.
β Command? Β· #set_program access_control
β Command? Β· let f: Future = set_timelock(self.caller, 1u32);
β Command? Β· f.await()
Result: ()
β Command? Β· CheatCode::print_mapping(timelocks)
Mapping: timelocks
Metadata {locker: aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px, lockee: aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px} -> 1u32
Result: ()
β Command? Β· let m: Metadata = Metadata { locker: self.caller, lockee: self.caller };
β Command? Β· timelocks.get(m)
Result: 1u32
β Command? Β· timelocks.set(m, 2u32);
β Command? Β· timelocks.get(m)
Result: 2u32
Navigate to the timelocked_credits
example and fire up the debugger.
cd workshop/learn_to_debug/timelocked_credits
leo debug
- Deposit
10
credits. Note that you'll need to set the state of theaccount
mapping incredits.aleo
. - Increment the block height by 1 and attempt to withdraw.
- Increment the block height by 3 and withdraw.
While you are stepping through the code, be sure to the use the #i
and #s
commands to visualize the flow of the execution.
You may also use the Leo debugger to step through AVM bytecode. This can be useful for debugging programs that may be deployed on-chain, but whose Leo source code is not available.
The debugger provides the #print | #p <REGISTER_NUMBER>
command to display register values.
-
Go to the Provable Explorer and pick out a program. Note that this program needs to be deployed on the same network that you have configured in your
.env
file. You can always default tocredits.aleo
. -
Add the program as a dev dependency via
leo add
. -
Step through an execution of the program.
In this tutorial, you learned how to use the Leo debugger. If you made it through all of the challenges you will have:
- Stepped through simple, local evaluations of a program.
- Simulated on-chain state to test more complex behaviors.
- Explicitly evaluated futures, giving you a better understanding of the async programming model.
- Debugged a deployed program that you may not have written directly.
We hope that this tool makes your Leo development experience easier! If you have any feedback or run into any issues, please feel free to file an issue here on the Leo repo.