Interpreter Implementation

Building a Trace Visualizer

How to print each step of a grid program with pointer position, current cell and stack so you can see a run, with a small Python sketch and formatting tips.

On this page
  1. What a trace shows
  2. The minimum useful trace
  3. A small Python sketch
  4. Taming long traces
  5. Showing the grid
  6. Visualizing in a browser or terminal
  7. Things that go wrong
  8. Using the trace as a debugging habit

What a trace shows

A trace is a record of what the interpreter did, one line per step. For a grid language, each line normally carries four facts: the step number, the pointer position, the instruction found there, and the stack afterward. Together they let you replay a run on paper and compare it with what you expected.

This is the machine version of tracing by hand. A person doing that work on paper is slow and error-prone, but the interpreter can produce the same record instantly and without mistakes in copying. It is also the quickest way to learn whether a bug is in your program or in your interpreter.

The minimum useful trace

Add a function that formats one line and call it from the main loop just before or after each instruction runs. A line might look like this:

step 12  at (4,0)  dir (1,0)  cell '+'  stack [3, 4] -> [7]

Showing the stack both before and after the step costs little and makes arithmetic mistakes obvious. At a minimum, record:

  • position as column and row, and say which comes first
  • direction, because pointer direction is half of what determines the next cell
  • the character in the cell
  • the stack contents
  • whether string mode is active, since characters read in that mode are pushed instead of executed

A small Python sketch

Suppose your interpreter keeps its state in a few variables. You can add a trace like this:

def trace_line(step, x, y, dx, dy, cell, stack, string_mode):
    mode = " str" if string_mode else ""
    return f"{step:5d} ({x},{y}) d=({dx},{dy}) '{cell}' {stack}{mode}"

Then, in the main loop:

if trace:
    print(trace_line(step, x, y, dx, dy, cell, stack, string_mode))

Send the trace to standard error or a separate file, not to the same stream as the program’s own output. Otherwise the two will interleave and neither will be readable. Keep a flag so that tracing is off by default.

Taming long traces

A loop that runs a thousand times produces a thousand lines. A few simple controls keep that manageable:

  • A step limit, such as stopping the trace after a chosen number of steps. This also protects you from infinite loops while testing.
  • A start step, so you can skip the part you already understand.
  • A watch on one cell or one condition, such as printing only when the stack length changes.
  • A compact mode that prints only direction changes and branches.

A step limit is worth having even outside of tracing, because a program with a geometric loop may legitimately never end. Whether a given program halts is, in general, undecidable, as the halting problem shows, so a limit is the practical safety net.

Showing the grid

Text lines tell you the order of events, but a grid language is spatial, and it often helps to see where the pointer is. You can print the whole playfield with a marker on the current cell. One simple way is to replace the current character with a highlighted version, for example by wrapping it in brackets, and print the grid at selected steps.

For a larger program, consider printing only a window of a few rows and columns around the pointer. Another approach is to record every visited position and print the grid afterward, with a count on each cell. Cells with a count of zero are dead code candidates, and cells with high counts show where the time goes. Those are the same ideas behind flow diagrams, but produced from an actual run rather than drawn by hand.

Visualizing in a browser or terminal

If you want something more polished, there are two common routes.

  1. Terminal output: clear the screen and redraw the grid on each step, with a short delay. This works with plain text and no libraries.
  2. A web page: record the trace as a list of states, then step through them with a slider or buttons. Because the trace is just data, you can generate it with the interpreter and display it with a few dozen lines of page code.

Recording the states first and replaying them later also lets you move backward, which is useful when a mistake happened ten steps before you noticed it.

Things that go wrong

  • If the trace line appears before the instruction runs but shows the stack after it, the numbers will seem off by one step. Decide which you mean and label it.
  • Tabs and wide characters shift alignment. Use fixed-width formatting and escape unprintable cells, for example by showing their numeric code.
  • Self-modifying code changes the grid during a run, so a single printed grid at the start is misleading. Print the changed cell when a write instruction runs.
  • Do not leave tracing on inside tests that compare output, unless it goes to a separate stream.

Using the trace as a debugging habit

When a program fails, run it with the trace and read down until the first line that surprises you. That line is usually the real problem, even if the visible symptom appears much later. This is the core of grid debugging, and it works the same way on your own programs as on test cases for the interpreter. Many people find that they stop needing the trace for simple programs after a while, but it stays useful for the odd, tangled ones.

The complete guide to interpreter implementation

Building Interpreters Step by Step
Read the guide

Keep exploring

Guides from other topics

All topics →

What Is Befunge?

A plain introduction to Befunge, the esoteric language where code lives on a two-dimensional grid and an instruction pointer moves in four directions.

Befunge Basics 5 min read