An Interpreter¶
Finally! It’s time to actually execute some code.
We have bytecode, which essentially is a tape-like sequence of instructions that we will interpret. We’ve casted our (potentially) complicated language into a sequence of simple stack manipulation operations.
Some of you will find interpretation to be the most “fun” part of the process. We need to implement the appropriate stack manipulation for each bytecode we wish to interpret.
The Main Loop¶
Interpreting bytecode will take place within a main loop similar to the CPython VM’s main loop or PyPy’s main loop. A giant loop that simply performs whatever bytecode instruction is at the current position we’re processing.
A program counter should keep track of that position within our bytecode. Our interpreter will read sequentially through the bytecode sequence, possibly moving the program counter to some other position (if you implement a jump or conditional expression in a later exercise).
Along with a stack (an RPython list), we’ll implement our plan.
For each bytecode instruction that your compiler produces, implement the appropriate stack manipulation.
Note
Depending on the particular bytecode instruction, you may find it difficult at this stage to write tests without simply making tests that assert about the internal state of your stack.
Try this out.
You might find it more reasonable as you progress to write tests that use the print bytecode mentioned below instead once you have a working entry point.
Print¶
Until you implement full support for function calls, it will likely be useful to special-case the print() instruction by giving it its own bytecode.
Note that you cannot use the sys
module in RPython for the most part, nor
do you have access to open
. You may write to stdout directly via
os.write
by passing in an fd of 1
for stdout.
You may also want to check out the streamio
module from the RPython
standard library which can provide some provisional file-like support for
RPython.
Once you have the ability to print values, you can begin print-debugging your own interpreter!
Wrapper Objects¶
Much like Python, our toy Snap language allows you to print objects that aren’t necessarily strings.
It becomes useful to start applying OOP techniques to objects at your language level and not just at the RPython level for your interpreter. For example, you may have an integer object which represents a Snap integer within your runtime.
The convention is to call objects like these W_Integer
, for example, where
the W_
prefix indicates that this object is a wrapper object.
Once you have wrapper objects, you can begin to encapsulate Snap
features on each wrapper object. A W_Integer
for example may have
a .to_string
method in RPython, which returns a W_String
Snap
string. Your PRINT
bytecode might then be implemented by simply
delegating to this method in order to produce a string, which you can
implement polymorphically on each Snap type you might have.
If you begin to implement method calls in Snap, your to_string
method might
further become a Snap-accessible method that you can call on your Snap objects
directly if so chosen.