The latest instance of the course can be found at: O1: 2024
- CS-A1110
- Supplementary Pages
- Using the Debugger
Luet oppimateriaalin englanninkielistä versiota. Mainitsit kuitenkin taustakyselyssä osaavasi suomea. Siksi suosittelemme, että käytät suomenkielistä versiota, joka on testatumpi ja hieman laajempi ja muutenkin mukava.
Suomenkielinen materiaali kyllä esittelee englanninkielisetkin termit.
Kieli vaihtuu A+:n sivujen yläreunan painikkeesta. Tai tästä: Vaihda suomeksi.
Using the Debugger
About This Page:
Questions Answered: How can I execute my program step by step and examine what it does? What’s a good tool for hunting runtime errors?
Prerequisites: This page assumes prior knowledge of topics from Weeks 1 to 4. You can make the most of this page if you read it sometime after reaching Chapter 4.2.
Points Available: None. This material is optional.
Related Modules: Miscellaneous.
Introduction: Tracing Program Execution
Programmers — and students of programming — commonly need to figure out what an existing program does. For instance, they may need to:
develop an existing program further;
write documentation about a program someone else wrote;
locate errors in a buggy program (self-written or otherwise); or
study an example program provided by a teacher.
In such scenarios, programmers often mentally trace the program’s behavior step by step. Tracing is also useful while writing an entirely new program: in order to determine if the code works, the programmer needs to be able to “run it in their mind”.
As a quick example, consider the Experience
class from Chapter 3.4:
class Experience(val name: String, val description: String, val price: Double, val rating: Int):
def valueForMoney = this.rating / this.price
def isBetterThan(another: Experience) = this.rating > another.rating
def chooseBetter(another: Experience) = if this.isBetterThan(another) then this else another
end Experience
Here’s a little program that calls some of the class’s methods:
@main def testExperiences() =
println("Starting the program.")
val wine1 = Experience("Il Barco 2001", "okay", 6.69, 5)
val wine2 = Experience("Tollo Rosso", "not great", 6.19, 3)
val better = wine1.chooseBetter(wine2)
var result = better.name
println(result)
result = "Better than wine1? " + better.isBetterThan(wine1)
println(result)
Try running testExperiences
mentally step by step. If you do it carefully, you’ll notice
that there is a whole bunch of things you need to keep track of at each step:
where in the program you’re currently at (which line and where on that line?);
the class and object definitions that form the program (although you can easily refresh your memory if you see the code);
each variable’s values at the present time (
var
s demand particular attention);which data is associated with each object (via its instance variables), which is determined by object-creating commands and subsequent effects on state (if any);
while running a method: Which object is the active
this
object for the method call? What are the values of each parameter variable during this method call?when returning from a method: Where was this particular method call initiated? Where should execution resume after returning?
the relevant built-in operators, functions, and classes and how they are used; and
any intermediate results that are formed while evaluating compound expressions.
Not all of those things are directly visible in program code. Since there are a lot of details to remember, it often makes sense to use an auxiliary tool that lightens the programmer’s burden. That way, the programmer may concentrate on the program’s goals and structure, possible bug locations, or whatever the task calls for.
A simple piece of paper can be a big aid: you can make notes of what happens during a program run and sketch out diagrams of the relevant objects and variables.
Instead of, or in addition to, pen and paper, you could use an auxiliary program that illustrates program execution step by step. In this ebook, you’ve already seen various programs illustrated as animated diagrams. Such animations aren’t always available, though, so what to do?
Debuggers
A debugger is a utility program for examining the execution steps of other programs. You can run your program in a debugger and examine its state as you step through it line by line.
The most prominent purpose of debuggers is to help the programmer locate defects in code; hence the name.
The information that a debugger displays is similar to that shown in this ebook’s animations. However, debuggers generally aren’t quite as graphical and detailed as those animations, since they’re usually designed for experienced professionals rather than learners; dealing with large programs calls for a different design. Nevertheless, beginner programmers too can benefit from debuggers.
Some debuggers are standalone programs; others are integrated in an IDE. IntelliJ, for instance, gives you a debugger.
IntelliJ’s Scala Debugger
Learning to handle a debugger fluently takes some effort. Here are some of the things you may want to do:
Browse the list of common debugging commands in IntelliJ below.
Do the little practice task further below on this page to try out the commands.
Experiment on your own: write a small program and examine it in the debugger or explore O1’s example programs.
Find additional material online. For instance, here is a YouTube video (that briefly covers even more debugger features than you need in O1) and IntelliJ’s web site explains how to use the debugger (on some Java example code, but the idea is the same for Scala).
Ask O1’s teaching assistants to help you at the lab sessions.
A list of selected debugger commands
- Setting a breakpoint
Before starting a program in the debugger, you’ll usually want to set at least one breakpoint: a location in the program where execution should pause so that you can examine the program in detail. To do this, start by choosing a line in your program where you want the breakpoint. The choice is up to you, but a good idea to try first is to place a breakpoint at the first command within your app object or main function. Put the cursor on that line and select Run → Toggle Breakpoint or press Ctrl+F8. (Alternatively, you can click the margin between the line and the line number). Doing the same again toggles the breakpoint off. A red dot in the margin indicates that you’ve got a breakpoint there.
- Launching a program in the debugger
Execute your program using the Debug command rather than the usual Run. You’ll find it by selecting your app object and looking at its context menu, or in the Run menu from the menu bar at the top; pressing the bug icon in the tool bar also works. This command launches your program and runs it until the (first) breakpoint. (If you haven’t set any breakpoints, your entire program will run much like usual.)
- Executing one entire line of code
Select Run → Debugging Actions → Step Over or press F8 (or the corresponding icon in the Debug panel). The computer will then execute the current line in full without displaying any intermediate steps. In particular, if the line contains a function call, the steps of executing the function will not be shown. Do this repeatedly to execute multiple lines.
- Executing (part of) a line in detail
Select Run → Debugging Actions → Step Into or press F7. This command is similar to Step Over but doesn’t necessarily execute the entire line: it pauses at certain important events within the line’s execution. In particular, if the line contains a function call, Step Into “jumps inside the function” and pauses there so that you can examine the function’s execution steps in detail. Executing a single line of code can thus be split into several consecutive Step Intos. If there are multiple function calls on the same line, IntelliJ will ask you to use the arrow keys to pick which one you’re interested in. (It may give you the option of stepping into a library function such as
println
, which is however seldom useful.)- Examining program state
You can browse the values of variables in the Debug panel that shows up at the bottom of the IntelliJ window once you launch the debugger.
- Returning from a method to the call site
Select Run → Debugging Actions → Step Out or press Shift+F8. The method you were in runs until it returns. Your debugger session resumes where that method was called.
- Continuing execution without stepping
Select Run → Debugging Actions → Resume or press F9. Your program will run until the next breakpoint, or until the end of the entire program run in case no more breakpoints are reached.
- Stepping backwards
There’s no command for this, but you can terminate the current debugger run and start over.
- Stopping a debugging session
Stop the program with Run → Terminate or Ctrl+F2 or use the other commands listed above until you reach the end of the program.
You’ll find buttons for many of the above commands in the Debug panel at the bottom. For example, you can Stop by pressing the button.
Student question: Why can’t debuggers go backwards?
There are certain technical challenges to overcome, which is why support for backwards stepping hasn’t been built into most debuggers. But it’s not impossible and might become more frequently supported in the future.
A Small Practice Task
Fetch the Miscellaneous project. It contains a class named o1.excursion.Excursion
and
a main function testExcursion
in separate files.
Start here
First study the Scaladocs for Excursion
and skim its program
code. Then proceed.
Now drill into the given code; see below for guidance. As you do so, it will turn out
that there is a little bug in the Excursion
class.
Since the debugger can feel confusing at first, you may want to do this at a lab session where you can ask for assistance!
The instructions below are only a guideline. Do experiment on your own.
Examining Excursion
in the debugger
Set a breakpoint in testExcursion
, on the first line that does a println
.
Launch the program in the debugger.
Execution pauses at the breakpoint. That line is now highlighted. Explore:
Step Over with F8 to have the computer execute the entire highlighted line. The highlight moves to the next line.
Down in the Debug panel, bring up the Console tab. It now displays a partial output: the first
println
has been executed.
Next up, we have a runFactoryScenario()
function call. This time, don’t execute the
entire function at once with Step Over. Let’s instead examine the function’s
behavior in detail:
Press Step Into F7. Execution jumps to the beginning of the called function’s code.
Notice that the Debug panel has a Debugger section, which lists the the frames on the call stack. Right now, you’re in the
runFactoryScenario
function, which has been called fromtestExcursion
.
Step through the code at your own pace:
First try the Step Over command F8 on the first few lines.
Try to understand each execution step as it happens. Notice how the output appears step by step in the Console.
Observe the values in the Debugger panel’s list of variables. In particular,
testTrip
refers to anExcursion
object which has its own list of variables.Try the Step Into command F7, too. Step into one of the
registerInterest
calls, for example.If at any point you end up seeing some strange code in a object named
Predef
, you’ve probably done a Step Into Scala’sprintln
function, which is probably not what you want to do. Not to worry. You can either Step Out Shift+F8 or even start over; no sweat.
Keep an eye on the call stack displayed in the Debugger tab. Notice how it changes as method calls begin and end.
Sooner or later, as you work your way through the program with Step Into and Step Over, you should notice something happen:
Did the call stack and the variables vanish in the Debugger panel? No problem; that’s to be expected. Here’s what happened:
The program run stopped. Browsing the Console, you’ll see that the culprit is an
IndexOutOfBoundsException
error.Under the error’s name, you can see that the error arose while executing
lastParticipant
in theExcursion
class, which has used theArrayBuffer
class. (ArrayBuffer
is a library class that implements Scala’s buffer collections.)The highlighted line of code “threw” an error. This error made the program crash.
You may already have noticed — and you can confirm from the stack trace — that the
error occurred while running the lastParticipant
method call that was initiated on
line 26 of test.scala
.
Use the debugger to study this error further. (Even if you already identified the bug, you can do this to practice.) See below for a suggested workflow.
Exploring the buggy method
Relaunch the debugger. Execution again pauses at the breakpoint that you set earlier.
Add another breakpoint on line 26.
Select Resume F9. The program runs until the second breakpoint that you set just now. The highlighted line, which makes the program crash, wasn’t executed yet.
Select Step Into F7 and choose to step into lastParticipants
(not
println
). You end up inside that method.
The next two lines call numberOfInterested
and numberOfParticipants
, but let’s ignore
the methods’ internals here. Step Over the two lines that call those methods
(F8 twice). Executing those lines (the if
line and the val
line) does not yet
crash the program.
Check the state of the Excursion
object in the Debug panel. (In the variables
list, notice how this
refers to that object while we’re running lastParticipant
on
it.) Among other things, you should see the buffer that interestedStudents
refers to.
And, within that buffer, there are the names of four people and the buffer’s current
size (4).
Make note of the local variable numberOfLast
as well.
Recall: the error we got was IndexOutOfBoundsException
, which means that an index wasn’t
within the appropriate range, given the buffer’s current size. Can you spot the mistake in
lastParticipant
’s code? Study the code and explore further in the debugger as necessary.
If you execute the next line (that begins with Some
), the program crashes again.
Placing Breakpoints
In that example, we placed the breakpoints in the app object. Another alternative would
have been to set a breakpoint directly into the lastParticipant
method. Had we done so,
the debugger would pause the program each time that method is invoked.
Similarly, if your program has a graphical user interface, you can place a breakpoint in one of the GUI’s event-handler methods or one of the model’s methods that is invoked by those event handlers. The debugger will then stop as you use the GUI and cause that method to be reached.
It’s also possible to define a conditional breakpoint that triggers only under specific circumstances. Try right-clicking one of the red dots that mark breakpoints and explore. You can also try setting a breakpoint that interrupts the program whenever a runtime error occurs: Run → View Breakpoints → Java Exception Breakpoints.
An Unfortunate Limitation (in IntelliJ’s Scala debugger)
IntelliJ’s Scala debugger currently has a limitation: it doesn’t work properly for all
App
objects. Consider this example:
object TroubleWithIJDebugger extends App:
println("Starting")
myMethod()
println("Called myMethod")
myMethod()
println("Done")
def myMethod() =
println("Hello from myMethod")
println("Now let's go back to the main code")
end TroubleWithIJDebugger
The debugger won’t show each execution step nicely in the following case:
that code calls methods that you’d like to step into; and
you’d like to return from those method calls back into the calling
code in the App
and resume stepping there.
Fortunately, the problem is easy to circumvent. Either of these two works in the debugger:
object HelperMethod extends App: actuallyDoStuff() def actuallyDoStuff() = // the code to be debugged is here println("Starting") myMethod() println("Called myMethod") myMethod() println("Done") def myMethod() = println("Hello from myMethod") println("Now let's go back to the main code") end HelperMethod@main def mainFunctionInsteadOfApp() = println("Starting") myMethod() println("Called myMethod") myMethod() println("Done") def myMethod() = println("Hello from myMethod") println("Now let's go back to the main code") end mainFunctionInsteadOfApp
The limitation will presumably go away in future versions of IntelliJ, but for now it is
what it is. When you’re debugging some other kind of program, with no need to return to
the body of the App
object from method calls, there’s no need to worry about this bit.
On Debuggers and Debugging
Most of O1’s programming assignments don’t exhort you to use the debugger. Nevertheless, it’s a good idea to gradually make this tool a part of your arsenal. When you run into bugs, remember that the debugger is available to use.
As you’ve already seen, even though the tool is called a “debugger”, it doesn’t magically fix any errors. It doesn’t “even” locate the errors automatically. That still requires work from you the programmer. However, “just” tracing program runs with a debugger is sometimes a great time-saver.
Speaking of how humans are needed for debugging, here’s a 49-second video of Steve Jobs advertising an IDE feature:
Summary of Key Points
A programmer often needs to mentally run programs step by step.
There is software that can help the programmer reason about programs. These tools are especially useful when the program is unfamiliar, buggy, or complex.
A debugger is an auxiliary program that lets you examine the intermediate stages of a program run and the state of the program at those stages.
IntelliJ has a debugger built in. You may find it useful in O1 and elsewhere.
Links to the glossary: debugger, breakpoint.
Feedback
Credits
Thousands of students have given feedback and so contributed to this ebook’s design. Thank you!
The ebook’s chapters, programming assignments, and weekly bulletins have been written in Finnish and translated into English by Juha Sorva.
The appendices (glossary, Scala reference, FAQ, etc.) are by Juha Sorva unless otherwise specified on the page.
The automatic assessment of the assignments has been developed by: (in alphabetical order) Riku Autio, Nikolas Drosdek, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Anna Valldeoriola Cardó, and Aleksi Vartiainen.
The illustrations at the top of each chapter, and the similar drawings elsewhere in the ebook, are the work of Christina Lassheikki.
The animations that detail the execution Scala programs have been designed by Juha Sorva and Teemu Sirkiä. Teemu Sirkiä and Riku Autio did the technical implementation, relying on Teemu’s Jsvee and Kelmu toolkits.
The other diagrams and interactive presentations in the ebook are by Juha Sorva.
The O1Library software has been developed by Aleksi Lukkarinen and Juha Sorva. Several of its key components are built upon Aleksi’s SMCL library.
The pedagogy of using O1Library for simple graphical programming (such as Pic
) is
inspired by the textbooks How to Design Programs by Flatt, Felleisen, Findler, and
Krishnamurthi and Picturing Programs by Stephen Bloch.
The course platform A+ was originally created at Aalto’s LeTech research group as a student project. The open-source project is now shepherded by the Computer Science department’s edu-tech team and hosted by the department’s IT services. Markku Riekkinen is the current lead developer; dozens of Aalto students and others have also contributed.
The A+ Courses plugin, which supports A+ and O1 in IntelliJ IDEA, is another open-source project. It has been designed and implemented by various students in collaboration with O1’s teachers.
For O1’s current teaching staff, please see Chapter 1.1.
Additional credits for this page
Our thanks to the O1 student who recommended the Jobs video.
You’re examining code that is written directly into an
App
object (i.e., not within a method);