What a specification is for
A specification answers one question precisely: given this program and this input, what must an implementation do? A tutorial or readme helps people get started, but a specification is the reference that two independent implementers could both follow and end up with interpreters that agree. For a hobby language it can be short, often a page or two, but it should leave as little to guesswork as possible.
The friendly, example-led material that helps newcomers is covered in the article on documenting a hobby language. This article is about the rule-book itself.
Define the program and its representation
Start by saying what a program is.
- Character set. Is the source text plain characters? Which encoding is assumed? What counts as a line break?
- Meaningful symbols. Which characters are commands, and what happens to the others? Many languages ignore everything else, others reject it. Say which.
- Structure. Is the program a sequence, a grid or something else? If it is a grid, state its size, or say it is unbounded, and how rows of unequal length are treated.
- Starting point. Where does execution begin, and in what direction or order?
- Ending. What stops the program? Reaching the end, a specific command or an error?
Ambiguity here causes the most trouble, because every other rule depends on these.
Define the machine state
Next, describe everything the program can remember.
- The kinds of memory: a stack, a tape, a grid of cells, registers or something else.
- The initial contents. Are cells zero, empty or undefined?
- The type and range of values. Are they integers? Unbounded or fixed width? What happens on overflow? Real interpreters differ here, so choose and state it. The same issues are discussed in arithmetic edge cases in interpreters.
- Any other state, such as a direction of travel or a pointer position.
If the memory model is a stack, say what happens when you remove from an empty one. That specific point is a common source of disagreement, and the matter is explored in stack underflow surprises.
Define each command
For every command, give a single entry containing:
- the symbol or name
- the effect on the machine state, stated step by step
- any conditions under which it fails or behaves differently
- a tiny example
Use consistent language. If you write “pop” in one place and “remove” in another, a careful reader may think they differ. Where an operation takes arguments, specify their order. For subtraction, division and comparison the order matters, and a reader cannot guess.
A table is a good format for a short instruction list, while special cases deserve a sentence each.
Define input and output
State clearly what the program can read and write.
- Is input read as characters or numbers? One unit at a time or in lines?
- What happens at the end of input?
- Is output buffered? Are numbers printed with separators?
- How are characters converted to and from numeric values?
These choices determine whether the same program produces the same output in two implementations, so they deserve explicit treatment. The practical consequences show up in interpreter input output handling.
Define errors and undefined behavior
Decide what happens when something goes wrong and write it down. There are three basic options:
- Defined error. The program halts with a stated condition.
- Defined recovery. The language specifies a fallback, such as treating an unknown symbol as a no-op.
- Undefined. The specification says implementations may do anything.
Undefined behavior is acceptable if it is declared, and it often keeps the specification short. What matters is that the reader knows which cases are which. A vague gap is worse than a plain “this is undefined.”
Use precise wording
A few writing habits make specifications clearer.
- Use “must” for requirements and “may” for options, and use them consistently.
- Avoid words such as “usually” or “normally” in the rules themselves.
- Define terms once, early, and reuse them exactly.
- Prefer short, numbered rules to long paragraphs.
- Describe behavior in terms of state changes, not intentions.
Pseudocode for the main loop can help, as long as it does not contradict the prose. If it does, say which one wins.
Separate the language from an implementation
Be careful not to let the behavior of your interpreter stand in for the specification. If you write “it does whatever my code does,” then bugs become part of the language. Better to write the rules first, or at least to review them against the code, and fix whichever is wrong. Limits that belong to a particular machine, such as memory capacity, should be marked as implementation limits rather than language rules.
Include conformance examples
A handful of test programs with their expected outputs makes a specification much stronger. Choose cases that exercise corner conditions: an empty program, the first and last cell, a full loop, an error. Anyone building an interpreter can run them and compare. The practice is the same one used in interpreter testing, and it catches misunderstandings early.
Keep the version
Number the specification. When you change a rule, record what changed and why. Even a hobby language benefits from a note such as “version 2 clarifies the handling of empty input.” If you are unsure about a point, label it as unsettled instead of pretending it is decided. The Funge family is a useful reminder of why this matters, since a later specification that generalized an earlier language had to say carefully where the two differed.
A minimal outline
A compact specification can follow this order:
- Overview in two sentences
- Program representation
- Machine state and initial values
- Command reference
- Input and output
- Errors and undefined behavior
- Examples and test programs
- Version notes
If a stranger can write a working interpreter from those eight sections alone, and their interpreter agrees with yours on the examples, the specification is doing its job. Where it does not, the disagreement shows exactly what needs clarifying.