Interpreter Implementation

Handling Input and Output in an Interpreter

How to deal with output buffering, end of input, and the difference between reading a character and reading a number when building a grid-language interpreter.

On this page
  1. Two small instruction families, many decisions
  2. Output: characters and numbers
  3. Buffering and flushing
  4. Reading a character
  5. Reading a number
  6. End of input
  7. Interactive versus piped input
  8. File-based input and output
  9. Keep the layer thin

Two small instruction families, many decisions

Output and input look like the easiest part of an interpreter. The program prints a character or a number; the program asks for one. In practice, this is where many small differences between implementations show up, because the host language, the terminal and the operating system each add their own behavior. The decisions are not hard, but they should be made on purpose.

Output: characters and numbers

Grid languages normally have two output instructions: one prints the top of the stack as a character, and one prints it as a decimal number. Details to settle:

  • Separators. Does the number output add a trailing space? Some Funge definitions do, and programs written for them depend on it, so compare against your reference.
  • Character range. A number outside the range your output can encode needs a rule: wrap it, print a substitute character, or stop with an error. Characters from 0 to 127 are plain ASCII; beyond that, what appears depends on the text encoding.
  • Newlines. Printing the value 10 should produce a line break. On some systems, text output translates this to a longer sequence. Let the host handle it unless you are writing binary output.

Buffering and flushing

For speed, most languages collect output in a buffer and write it out in chunks. That is fine for batch runs, but it can confuse interactive programs: a prompt may not appear until after the program has already waited for input.

The usual fixes are to flush the output stream before every read, and to flush at exit. If a program crashes with an unhandled exception, the final buffered text may be lost, which can make the output look incomplete and send you hunting for a bug that is not in the program. A try-and-finally block around the main loop that flushes output is a cheap safeguard.

If you capture output for tests, it is simpler to collect it in a list or string and compare it after the run. Allow the output function to be swapped, so the same interpreter can print to a console in one situation and to a test buffer in another.

Reading a character

The character-read instruction takes one character from input and pushes its code. Questions:

  • Is input read a line at a time or a key at a time? Terminals normally deliver whole lines, so a program that wants one character will still wait for the user to press Enter.
  • What happens to the newline at the end of the line? It stays in the input and will be returned by the next character read, which surprises beginners.
  • How is a multi-byte character handled? Decide whether you read bytes or decoded characters, and stay consistent.

Reading a number

The number-read instruction must parse digits from the input. This is more complicated than it sounds, and it is the heart of numeric input in practice. You need rules for:

  1. skipping leading spaces or newlines
  2. an optional minus sign
  3. where the number ends, usually at the first non-digit character
  4. what to do if there is no number at all

A common approach is to read digits until a non-digit appears, and then leave that non-digit in the input. For malformed input, some interpreters ask again, some push zero and some stop. Pick one and test it.

Keep an input buffer in the interpreter object, and let both read instructions work from it. Then a program that reads a number followed by a character behaves sensibly, since both draw from the same stream.

End of input

Eventually the input runs out, and the instruction must do something. The choices vary between definitions and implementations:

  • Push a special value, such as -1 for a character read. This is the C tradition for end-of-file.
  • Reverse the pointer’s direction, which is what some Funge-98 descriptions specify for failed reads.
  • Treat it as an error and halt.

Check the specification you follow. If there is none, the -1 convention is easy to explain, and programs can test for it with a simple comparison. Note that -1 cannot be confused with a real character code, which is why it works. A program that loops while reading characters until a particular value is returned relies on this behavior, so test it with a small program and with empty input.

Interactive versus piped input

A program might be run by a person typing, or by a script feeding a file. Both are the same to the interpreter if you read from standard input, but the prompt flush matters more in the first case, and end of input matters more in the second. Test both, since a program that works with typed input may misbehave when the input is a file without a final newline.

File-based input and output

Funge-98 includes instructions that read and write files, which add another layer of decisions about paths, permissions and safety. Those are the concern of file handling, and they should be treated with more caution than console input, because a program can then touch the user’s files.

Keep the layer thin

Place all reads and writes behind a handful of functions: write character, write number, read character, read number. Then buffering, encoding and end-of-input rules each live in one spot. It also makes it easy to feed test input and capture output, which is the foundation of the small-program tests described in the article on interpreter testing.

The complete guide to interpreter implementation

Building Interpreters Step by Step
Read the guide

Keep exploring

Guides from other topics

All topics →

What Is Funge-98?

Funge-98 generalizes Befunge into a larger, better-defined family of grid languages. Learn its goals, its main differences from Befunge-93, and why it matters.

Funge-98 5 min read