Skip to main content

Trace playground

See ethdebug/format in motion. Each example below compiles a small BUG program and lets you step through its execution trace — watching the source line, in-scope variables, call stack, and instruction context light up at every step.

Click "Try it" on an example to open the trace drawer, then use the step controls to walk through execution. The examples build on each other: start at the top and work down, picking up one idea at a time.

Start here: a storage write

Counter increments a single storage slot. Step through it to get a feel for the drawer — the highlighted source line, the value in storage, and the context attached to each instruction.

Counter increment
Increments count from 0 to 1, storing the result
name Counter;

storage {
[0] count: uint256;
}

create {
count = 0;
}

code {
count = count + 1;
}

Following a function call

Real programs call functions, and the debugger follows them. Watch the call stack push a frame on the JUMP into add (an invoke context) and pop it on the JUMP back (a return context).

Function call and return
Calls an internal add function and stores the result
name Adder;

define {
function add(a: uint256, b: uint256) -> uint256 {
return a + b;
};
}

storage {
[0] result: uint256;
}

create {
result = 0;
}

code {
result = add(3, 4);
}

For the exact shape of invoke, return, and revert contexts, see the function call spec and the tracing reference.

Watching the optimizer

Compilers rewrite code as they optimize, and transform contexts record what they did. Set the Opt selector to O2 and step through these tail-recursive programs: the recursive call folds into a loop, and the back-edge JUMP carries transform: ["tailcall"] next to its invoke and return. The call stack stays flat instead of growing one frame per iteration.

Tail-recursive sum
Sums 1..5 with an accumulator; TCO folds it into a loop
name TailSum;

define {
function sum(n: uint256, acc: uint256) -> uint256 {
if (n == 0) { return acc; }
else { return sum(n - 1, acc + n); }
};
}

storage {
[0] result: uint256;
}

create {
result = 0;
}

code {
result = sum(5, 0);
}
Tail-recursive factorial
Computes 5! with an accumulator; TCO folds it into a loop
name TailFactorial;

define {
function fact(n: uint256, acc: uint256) -> uint256 {
if (n == 0) { return acc; }
else { return fact(n - 1, acc * n); }
};
}

storage {
[0] result: uint256;
}

create {
result = 0;
}

code {
result = fact(5, 1);
}

Flip the Opt selector between O0 and O2 to see the call stack change. For how the tailcall transform composes with invoke/return, see the transform context spec and the tail-call optimization walkthrough.