In our last blog post, we implemented a WebAssembly interpreter that can do arithmetic and comparison operations on i32 values.
In this post, we’re going to extend the interpreter: rather than only operating on literal values, we’ll add support for local and global variables. And just like before, we’re writing everything from scratch, in JavaScript — no libraries or frameworks.
If that sounds like your cup of tea, keep reading.
🛠️ Setup
If you’d like to follow along by running the code yourself, then you should start by creating a file named wasm-vm-02.js. All the snippets we show will be added to that file. Let’s start with a few imports:
We’ll also import a few definitions from the previous post:
If you don’t have that code in a file already, you can go to the end of that post and click “Copy up to here” under the last snippet (or copy it from StackBlitz) then paste it into a file called wasm-vm-01.js.
Okay, let’s get started.
An important thing to know about WebAssembly is that inside a module, things aren’t referenced by name, but by index. Things of different types are grouped together in different index spaces, a fancy name for what is basically just an array.
Local variables are no exception; they’re stored in the locals index space, which maps from an index to a value of one of the core Wasm types (i32, i64, f32, f64).
To keep the code short and simple we’re going to extend the VM class and add a locals index space. It’s not really the “right way” to do it, but in the next post we’ll rewrite this class anyways, so let’s go with a simple solution:
Now that we have a locals space, we need some instructions to work with it. Let’s define the first instruction: local.get, which takes the index as an immediate and pushes the variable’s current value onto the stack:
Since we don’t have an instruction to set local variables yet, let’s test it by creating a FlatFrame with a local variable initialized to 100:
In the instruction implementation above we check if the index is in bounds (i.e., it refers to an existing variable). Let’s extract that check into a separate function that can be reused for the other instructions:
Now we can implement the local.set instruction, which also takes the variable’s index as an immediate. But unlike local.get, it also requires a dynamic argument: the new value of the variable. It gets this value from the stack.
Let’s test it:
The local.set instruction pops the value from the stack, but sometimes you want to set a local variable and leave the original value in the stack too. For example, many languages (including JavaScript) support assignment expressions, which assign a value to a variable, but also produce a value:
let a = 0;function plusOne(val) { return val + 1;}plusOne(a = 6); // Returns 7console.log(a); // Prints '6'
That’s one example of where the local.tee instruction comes in handy. We can easily implement it, by using peek() to read the top-of-stack value without popping it:
Okay, let’s test it:
Now we have local variables, but local to what? Right now they are the only kinds of variables we have.
The answer, of course, is that they’re local to a function — or, to be more specific, they’re local to a specific call stack frame. Hence the name of our FlatFrame class: we’re modeling a virtual machine with a single frame, rather than a stack of frames.
In WebAssembly, like JavaScript, functions are grouped into modules. To make our locals truly local, we can put the FlatFrame inside a module instance. For now, we’ll still assume a single function and a single stack frame, stored in a currentFrame property.
And then we can also add support for global variables, in a property named…you guessed it, globals:
The MonoInstance constructor takes the same arguments as FlatFrame, plus an extra one for globals.
To make all our previous instruction definitions still work and keep the new ones as simple as possible, let’s
“wrap and forward” some of the FlatFrame methods in our new class:
To make sure everything works, we can run the local.tee test from above on our new MonoInstance:
If you compare the tests, you’ll see that the only thing that changed is what class we instantiate for the vm.
With local instructions completed we can now move to the global instructions. Let’s start with global.get:
You may notice that it’s almost identical to local.get. The only difference is what index space it uses.
As before, we define a function to check that the index is valid:
Okay — let’s test our new instruction:
Finally, we’ll define global.set:
…and test it too:
And we are done! 🎉
Not bad — with a fairly small amount of code, we were able to extend our WebAssembly VM to support local and global variables. In the next post, we’ll extend it further, and add support for multiple functions.
Let’s finish up by exporting everything we’ll need in the next post:
If you enjoyed this post, you should check out WebAssembly from the Ground Up — an online book to learn Wasm by building a simple compiler in JavaScript.
If you’d like to be notified when we publish the next post, you can sign up for our mailing list, where we share periodic updates on the book and interesting WebAssembly tidbits.