2014/10/21

Scripting, Part 2: Architecture and Approaches to Interpretation

This post's a little on the short side because it was meant to be a part of a post covering this, as well as the next two posts. Since that obviously won't work out, I'll just walk you through the design patterns I decided to use for my interpreter and why the decorator pattern fits the problem.

Different approaches to interpretation

That's what I would call execution, decompiling and debugging. Debugging is really just execution with some extra features like viewing the upcoming command and the program state. Decompiling does not require real execution of commands, but you still have to read them one by one and put out text representing each one.

They are similar, but different.
As you can see, the only thing they really share is step 1, reading a command, and the main loop around steps 1 through 4. You'll want reusable methods for executing a command, since executing and debugging share that step, but you'll want to replace it with decompiling at some point, so the strategy pattern might work. To create the debugging mode, you could write a wrapper around the execute strategy that would permorm steps 2 and 4.

There are some problems with this solution, though:
  • If you extract step 3 but not step 1 into the strategies, you'll have to pass information from step 1 into the strategy. To retrieve the information to pass (and to determine if there's any info to pass at all), you will have to at least see if the command is an extended opcode. Now the code to interpret your instruction set is split across at least three classes. This makes changes to the instruction set (which will be inevitable at early stages of development) a pain to implement.
  • If you do extract step 1, all your interpreter class will be is a container of script data and an infinite loop on a strategy. Now the strategies do ALL the work, when they were only supposed to abstract away the differences between the three modes. However, that is still better than option one, so if you feel my method is stupid, this is probably your way to go.
  • The execution and decompiling logic is no longer separate from the language-specific code and therefore not reusable as easily as it could be.
  • Either way, you're passing around a lot of data between the strategies and the interpreter. This looks awful in code and is no fun to implement.
Let's look at the problem differently then. The debugger is really a fancy interpreter that does some special debugging logic before and/or after every command. The decompiler needs to share some logic for text output with the debugger, but handles commands differently from how the interpreter and debugger do.

Describing the problem like that suggests implementing the decorator pattern:
  • We'll define a common interface for all interpreters. We will implement the byte code pattern and the core loop in a BaseInterpreter. The actual logic of executing and decompiling commands of a specific language will be implemented in subclasses of that.
  • The debugger and decompiler will implement the Interpreter interface as well, but only take another interpreter as input.
    • The debugger will override the core loop to add steps 2 and 4 from the table above, but defer the actual execution to the interpreter that was passed in during construction.
    • The decompiler will override the same method, but it will not defer to execution and rely on the code generating functions instead.
This way, subclassing BaseInterpreter (or even implementing Interpreter from scratch) will provide you with a debugger and decompiler ready to go.

(ADDENDUM: Having now written the rest of the series, building a decompiler from the parts used by the compiler is just as easy. However, you still need the command-to-text function for the debugger, so building a decompiler with it won't hurt.) 

No comments:

Post a Comment