HiveBrain v1.2.0
Get Started
← Back to all entries
snippetjavascriptTip

Making a Brainfuck interpreter in JavaScript - Part 2

Submitted by: @import:30-seconds-of-code··
0
Viewed 0 times
javascriptinterpreterpartmakingbrainfuck

Problem

In the last article, I explored the basics of Brainfuck tokenization and interpretation, using Abstract Syntax Trees (AST) to represent the program. In this article, I will continue to build on that foundation by implementing a VM-like interpreter that can execute the AST. I'll also wrap the whole thing up, by adding some command-line scripts to parse and run Brainfuck programs.
Previously, we explored how we can add an execute method to the AST and ASTNode classes to run the program. However, we may want far more flexibility in interpretation, especially if we were working with a more complex language with more instructions or more complex constructs.
To deal with this complexity, we can delegate code execution to a separate class. As my implementation is a mix of an interpreter and a VM in itself, I'll call this class, the Runner. So, what are the responsibilities we want to give this class?
First and foremost, we need it to be able to execute the program, provided as an AST. Then, we want it to handle input, output and memory management. The input and output need to be configurable, so we can potentially supply all of the input ahead of time, or output to a file, for example.
Memory management will use the underlying Memory class, but we may want to swap it out for a different implementation in the future, or provide an initial memory setup.

Solution

As you can see, there's a lot going on here and we've yet to implement actual execution logic. We're relying on **Node.js streams** to make input and output configurable, allowing us to pass input as a string or make output print all out at once in debug mode. We're also setting up the early termination system we mentioned to make it easier to stop the program if it runs too long.

### Memory upgrades

You may have noticed I snuck in some changes to the `Memory` class, the most important one of which comes as an **argument** to the `constructor`. We can now initialize our memory with some data, if we so desire, which may come in handy for debugging segments of code without having to perform the entire setup manually.

I also added a subtle change, in expecting `Memory` to respond with a nice output to the `toString()` method. This will make it easier to debug the memory state at any given time. Let's see what that looks like:


To deal with this complexity, we can delegate code execution to a separate class. As my implementation is a mix of an interpreter and a VM in itself, I'll call this class, the Runner. So, what are the responsibilities we want to give this class?
First and foremost, we need it to be able to execute the program, provided as an AST. Then, we want it to handle input, output and memory management. The input and output need to be configurable, so we can potentially supply all of the input ahead of time, or output to a file, for example.
Memory management will use the underlying Memory class, but we may want to swap it out for a different implementation in the future, or provide an initial memory setup.
Next up, we want to provide a maximum number of steps to run the program for, to prevent infinite loops. We'll also add a safeguard to terminate via a signal sent as a method call to the Runner.
> [!NOTE]
>

Code Snippets

As you can see, there's a lot going on here and we've yet to implement actual execution logic. We're relying on **Node.js streams** to make input and output configurable, allowing us to pass input as a string or make output print all out at once in debug mode. We're also setting up the early termination system we mentioned to make it easier to stop the program if it runs too long.

### Memory upgrades

You may have noticed I snuck in some changes to the `Memory` class, the most important one of which comes as an **argument** to the `constructor`. We can now initialize our memory with some data, if we so desire, which may come in handy for debugging segments of code without having to perform the entire setup manually.

I also added a subtle change, in expecting `Memory` to respond with a nice output to the `toString()` method. This will make it easier to debug the memory state at any given time. Let's see what that looks like:
## Running the code

In the previous iteration, we would run the code by calling `execute` on the `AST` object. We would then pass down the memory, input and output streams to the `ASTNode` objects, which would then call the appropriate methods on the `Memory` object.

We won't deviate that much here, but we'll use the `Runner` class instance to wrap all of this in a nice package. We'll also make sure to **delegate responsibility** for some things to the runner, so that nodes have lean implementations, focusing on their own logic.

### Running instructions

The `Runner` class will be given **instructions** in the form of functions. These will correspond to the various `ASTNode` types, but we'll make sure to pass the `Runner` instance to them, so they can delegate responsibility to it. The runner will then perform the necessary **checks**, such as whether the program has terminated or exceeded the instruction count, **run the instruction** and update the **instruction counter**. We can add debugging logic in this step, as needed.

We'll also provide a way to run an entire `AST`, in a similar fashion. The only major change is that the `AST` will be given the `Runner` instance to pass down to its children, thus making the instance available to all subsequent instructions.
### Running the AST

The changes to the `AST` class are, as I said, fairly minimal. We'll swap out the previously unnamed object argument for a `Runner` instance, which we'll pass down to the `ASTNode` objects. We'll also call `runner.runInstruction` on each node, which will then call the appropriate instruction.

Context

From 30-seconds-of-code: brainfuck-interpreter-part-2

Revisions (0)

No revisions yet.