2014/10/22

Scripting, Part 4: Debugger, Decompiler, Decorators

In the last post, we defined an interface for all our interpreters and implemented the byte code pattern in a bare bones interpreter that you can subclass to create your own scripting system. In the post before that, I claimed that, with the decorator pattern, this would allow easy implementation of a debugger and decompiler.

If you're just here for the source code so far, or want to read it along with my explanations, look no further than this paste.

The pattern itself

An abstract decorator is simply an implementation of an interface that defers method calls to another object that adheres to the same interface.
A concrete decorator is derived from the abstract one and overrides some of the methods to add some functionality. Since you can call the overriden method at any time, or not at all, it becomes possible to add code that should be executed before or after a method, while the object that adds the functionality remains completely unaware of what exactly the decorated object is doing.
Since decorators implement the interface they decorate themselves, you can also use multiple levels of them, adding multiple pieces of logic independently, though we are not going to need that.

What we're interested in is this bit:
[...] it becomes possible to add code that should be executed before or after a method,
I already explained how debugging and interpreting are really only overriding or adding logic to the execution process; they are decorations to that process. But the best part?
[...] it becomes possible to add code that should be executed before or after a method, while the object that adds the functionality remains completely unaware of what exactly the decorated object is doing.
In other words: our debugger and decompiler are implementation agnostic, and in our architecture, therefore language independent. That means that, if you were to implement multiple languages (e.g. one for plot events and NPCs, one for combat abilities, one for loading certain asset types, etc.), you would need to implement just a single debugger and decompiler!

...Which we will get to now.
The decompiler is pretty straightforward: override NextCommand, and don't call base.NextCommand, since you want to replace its original functionality. Instead, call base.DecompileCommand, and write the output to a stream.
In the example code I wrote directly to console, but redirecting the strings to an arbitrary stream is easy when using a StreamWriter.

The debugger

The much more interesting problem is the debugger, though that has become near trivial as well (of course, you can make it non-trivial by adding more complex features). Before we can really code anything though, we'll need to decide how the debugger will be used and what functionality to implement.

The debugger in Visual Studio, for instance, is directly integrated into the IDE. It runs your program normally until it reaches a break point. When a break point is reached, the user can access the state of the program (i.e. see the value of a variable), step through his program line by line (F10, F11), or continue execution until a return (shift + F11) or another break point is reached (F5).

Our debugger cannot support most of this: not all languages are going to support variables, so we can't have that in our interface, therefore not in our debugger. The same goes for continuing execution until a return occurs, since not all languages will have control flow.
We don't have source code yet, let alone IDEs, so we can't even highlight the next command in an editor. Instead, we'll display a decompiled command via the console and add breakpoints so users don't have to manually step through every command.

For now, let's assume we have a boolean variable that magically switches to true when a break point is reached. You could then write something like this:
 boolean breakMode;  
   
 public override void NextCommand()  
 {  
   if (breakMode)  
   {  
     int pc = ProgramCounter;  
     Console.WriteLine(DecompileCommand());  
     ProgramCounter = pc;  
       
     string input = Console.ReadLine();  
     breakMode = (input != "continue");  
     }  
   }  
     
   base.NextCommand();  
 }  

The important parts are int pc = ProgramCounter; ProgramCounter = pc and Console.ReadLine(). The former pair needs to surround any call to DecompileCommand, since that reads from the same sctream as the execution method and would therefore force the decompiled command to be excluded from execution. The latter waits for user input, so we can actually step through the script and leave break mode if we want to.

This last step is not necessary when you're building an IDE of some sort, since you can just call NextCommand from the single-step event, rather than in a loop. In that scenario, you should also replace Console.WriteLine(DecompileCommand()) before command execution with a callback after execution, so you can highlight the command that's executed next in the source code. This would also save you the trouble of saving and restoring the program counter.

Now it's time to make the magic happen and conditionally enter break mode. I don't know how professional class debuggers do it, but I just keep a list of values to compare the program counter to. If there's a match, we enter break mode. We only need to do that when the debugger isn't in break mode already. Using a sorted set will speed up the search as well.
Basically, insert this at the top of the method:
 if (!breakMode && breakPoints.Contains(ProgramCounter))  
   breakMode = true;  

And that's about it! When running a console debugger, you may want to add some more keywords, e.g. to add or remove break points at will, even after execution has begun. You might also consider extending the command interface (or adding a strategy) to deal with language specific debug keywords. In a GUI, you could still access the decorated debugger (and through it, its state) from code, so that would not be needed there either.

No comments:

Post a Comment