Bytecode Debugger
What Is a Debugger?
Imagine you're following a recipe but something tastes wrong. You could cook the whole thing again from scratch — or you could go through it step by step, tasting after each ingredient, to find exactly where the problem is.
A debugger does the same thing for code. Instead of running your whole program at once, it lets you pause at each step, look around at what values your variables hold, and decide when to move forward.
Starting the Debugger
Add the --debug flag when running a Pebble file:
The program pauses at the very first line and shows you the (pdb) prompt.
That's where you type debugger commands.
Commands
| Command | Short | What it does |
|---|---|---|
step |
s |
Go to the next source line |
istep |
n |
Go to the next bytecode instruction |
continue |
c |
Run until the next breakpoint |
break 5 |
b 5 |
Set a breakpoint at line 5 |
clear 5 |
Remove the breakpoint at line 5 | |
print x |
p x |
Show the value of variable x |
locals |
Show all local variables | |
stack |
Show the operand stack | |
backtrace |
bt |
Show the call stack with line numbers |
list |
l |
Show source code around current line |
help |
h |
Show all commands |
quit |
q |
Stop the program |
Step-by-Step Example
Say you have this program saved as add.pbl:
Here's what a debugging session looks like:
$ pebble --debug add.pbl
1: let x = 3
(pdb) s
2: let y = 7
(pdb) print x
x = 3
(pdb) s
3: print(x + y)
(pdb) print y
y = 7
(pdb) locals
x = 3
y = 7
(pdb) s
10
Each s (step) command moves to the next source line. Between steps you can
inspect variables with print or see everything with locals.
A value the debugger can't show, like a whole number with more than 4,300
digits, appears as the error that stops it being written out, for example
x = <error: Number is too long to turn into text (more than 4,300 digits)>.
Your program keeps going.
Source-Line vs Instruction Stepping
Pebble compiles your code into bytecode instructions — small operations like "push the number 3 onto the stack" or "store a value in variable x". A single line of source code often produces several instructions.
step(ors) pauses once per source line. It skips over the individual bytecode instructions that make up one line.istep(orn) pauses before every single instruction. This is useful when you want to see exactly what the VM is doing under the hood.
In instruction mode you see the opcode name (like STORE_NAME x) below the
source line.
Breakpoints
Sometimes you don't want to step through every line — you just want to jump straight to the interesting part. That's what breakpoints are for.
break 3 tells the debugger "pause when you reach line 3". Then continue
lets the program run freely until it hits that line.
Use clear 3 to remove a breakpoint, or break (no number) to see which
breakpoints are currently set.
How It Works Under the Hood
The debugger uses a hook pattern — it plugs into the VM without changing how the VM actually runs your code.
The Hook Protocol
The VM defines a DebugHook protocol with one method:
Before every instruction, the VM calls this method (if a hook is attached). The hook can return:
CONTINUE— keep runningQUIT— stop the program
What Happens Each Step
VM fetches instruction
↓
Hook fires: on_instruction(...)
↓
Hook checks: should I stop here?
├── Yes → show source line, wait for command
└── No → return CONTINUE immediately
↓
VM executes the instruction normally
The beauty of this design is that the VM barely changes — just four extra lines of code. All the debugger logic lives in a separate module that the VM knows nothing about.
DebugAction
DebugAction is a simple enum:
The debugger returns CONTINUE after each step or continue command, and
QUIT when the user types quit.
Summary
- A debugger lets you pause your program and look around
- Use
stepto go line by line,istepto see every bytecode instruction - Set breakpoints to jump straight to the interesting parts
- The debugger hooks into the VM's execution loop without changing it
- The hook pattern keeps the debugger code completely separate from the VM code