Interpreter Implementation

Avoiding Stack Underflow Surprises

What an interpreter can do when an instruction needs more values than the stack holds: default to zero, halt with an error, or warn, and how to test each choice.

On this page
  1. What underflow means
  2. Policy 1: Treat missing values as zero
  3. Policy 2: Stop with an error
  4. Policy 3: Warn and continue
  5. Where to put the check
  6. Instructions that peek or rearrange
  7. A reminder about multiple stacks
  8. Testing underflow
  9. Choosing sensibly

What underflow means

Stack underflow happens when an instruction needs a value and the stack has none. Adding two numbers needs two values. Printing a number needs one. A duplicate instruction needs one to copy. If a program reaches such an instruction too early, the interpreter has to do something, and “crash with an exception from the host language” is rarely the best answer.

In a hand-written program this is easy to cause: a loop that pops once too often, a branch that skips a push, or a subtle mix-up between two paths that reach the same cell with different stacks. The program may not be wrong in an obvious way, which is why it is worth deciding the interpreter’s policy ahead of time.

Policy 1: Treat missing values as zero

Several Funge-family definitions say that popping from an empty stack gives zero. This is forgiving and keeps programs running. It also lets some programs rely on it deliberately, for example using the implicit zero as a string terminator without pushing one first. If you are building an interpreter to run existing programs, this is usually the policy to follow, since those programs may depend on it.

The drawback is that real mistakes go unnoticed. A program that adds two numbers but only pushed one will quietly add zero and print a plausible-looking result.

Policy 2: Stop with an error

A stricter interpreter halts and reports the problem, ideally with the pointer position and the instruction that failed. This is helpful for people learning, because the mistake shows up where it happens instead of ten steps later. The cost is compatibility: a program that relied on the zero-default will fail.

If you take this route, say so in your documentation. A strict mode is a reasonable design, as long as readers know it differs from the standard behavior.

Policy 3: Warn and continue

A middle path returns zero but also records a warning, perhaps printed to a separate error stream. This keeps old programs working and still tells a developer something went wrong. A command-line flag can switch between quiet, warning and strict behavior.

Where to put the check

The most reliable design routes every removal of a value through one function, such as a pop helper, so the policy lives in a single place. If instructions pop directly from the underlying list, you will have the check in some places and not others, and the behavior will depend on which instruction triggered the underflow.

A small sketch in Python:

def pop(stack, strict=False):
    if stack:
        return stack.pop()
    if strict:
        raise RuntimeError("stack underflow")
    return 0

Instructions that need two values call it twice. Remember the order: the first value popped is the top of the stack, which is the second operand of a subtraction.

Instructions that peek or rearrange

Not every instruction pops and pushes. Duplicate, swap and discard need their own thought:

  • Duplicate on an empty stack: with the zero policy, it pushes two zeros, since the popped zero is copied. Some implementations behave slightly differently, so follow your reference.
  • Swap with one value: the missing value counts as zero, so the lone value ends up underneath the zero.
  • Discard on an empty stack: nothing to remove, and nothing happens.

Write these down as explicit rules. They are small cases, but they are the ones that trip up test programs.

A reminder about multiple stacks

Funge-98 includes a stack of stacks, where underflow can involve more than one level, and where some operations on an empty or small stack have their own rules. If you work toward that specification, treat the details as something to read carefully instead of assuming they match the single-stack case. The topic is covered more thoroughly in the article on the stack stack.

Testing underflow

Write one tiny program per instruction that runs it with an empty stack, then another with just enough values. For example:

  • An add instruction on an empty stack should leave a single zero.
  • A print-number instruction on an empty stack should print 0 under the zero policy and report an error under the strict one.
  • A swap on a single value should leave the original value and a zero in the order your notes describe.

These programs make good members of the ladder described in the article on interpreter testing, and they are easy to inspect with a trace. A few lines of output showing the stack before and after each step will confirm what the interpreter did. Many beginner mistakes in Befunge are actually underflow in disguise, so the same checks help when you are debugging a puzzle rather than the interpreter itself.

Choosing sensibly

If you are not sure, copy the specification. If there is no specification, prefer the forgiving zero default with an optional strict mode, and write the policy at the top of your documentation. What matters most is that the behavior is consistent, visible and tested.

The complete guide to interpreter implementation

Building Interpreters Step by Step
Read the guide

Keep exploring

Guides from other topics

All topics →