Mostly on what you mean by interpreter:
- A Class that reads a stream of data and issues a commensurate stream of commands?
- A System of objects that parse a given language (usually textual) and produce effects equivalent to the described program?
Somewhat on what you mean by testable.
- Verifying that a given program as written is executed as expected
- Verifying that this specific input to this specific function calls this other function.
And on what kinds of optimisations you are going to entertain.
To answer the question, naively. The interpreter class that takes the byte codes as input, calls functions through an interface called
Engine implementation is responsible for tracking frames and state. All the interpreter does is map a byte code, to one or more function calls.
Unfortunately this does not buy you a lot, not at byte code level. Largely because at this level, the indirection cost is extreme. Perhaps you could get away with a compile time interface, by compiling the interpreter against a "test" implementation for testing, and a real implementation for execution.
But that really isn't answering the question. The problem is that each optimisation changes how the mass of objects implementing your interpreter are formed. To the point where a reference and an optimised version have next to nothing in common. Not even the layout of memory frames.
Your best bet is to step back and implement a HEL (Hardware Emulation Layer). Think of this as a platform stub. If the interpreter wants to read a file, it talks to the HEL and during normal execution this plumbs out to the real platform. During a test though it talks to an in memory implementation. Same for network ports, drawing, etc... In fact a HEL is just good practice for porting to different platforms, even embedding in different hosts.
With a HEL in place you can now create a series of test environments, and to be run within that environment a series of scripts, with their expected outcomes. The beauty here is that:
- you can test new platforms by really configuring the environment that way.
- you can test the engine independently of any specific platform.
- you can test a script/code fragment against a myriad of environments to verify behaviour.
- you can drop a completely different interpreter implementation into the middle and meaningfully compare it.