3. Lexing and Parsing Brainfuck with Multiple Dispatch

Rare image of a higher-dimensional dispatching to multiple realms

Now that we know the fundamental structure of the function our Brainfuck program will translate to, we will now need to actually translate those instructions. Thankfully, Julia’s type system and multiple dispatch have set us up for success here. Using dispatch on value types, we can directly map the instructions to individual instances of instruction. First, we convert the program string into a vector of Char structs, and then convert each of those Char values into a Symbol , which we can then dispatch on using lex(Val(symbol)) . First, we define our instruction types, our characters, and our op codes.

Then we zip the two const lists and iterate through them, generating an instruction and a relevant lex function for each op code:

For example, for the second iteration in this loop, we have (:-, :Decrement) , which generates the following code:

struct Decrement <: AbstractBFInstruction end

lex(::Val{:-}) = Decrement()

What if we run into a symbol that doesn’t match an instruction? For now, we assign it to a fallback type, which won’t inherit from AbstractBFInstruction . Finally, we dispatch lex on an iterable of Chars , which, in turn, dispatches lex element-wise on each character in the program:

Great! Now we have a list of instructions. The only thing left to do before we put this all back together is to parse the instructions into a collection of expressions that we can insert back into the function body. Here’s the entire parsing routine:

After initializing our expression and loop stacks, this function processes each instruction in two steps: parsing the next instruction and adding the result to our list of expressions.

Parsing

Are we ending an existing loop? Let’s check if we actually started a loop — and if we haven’t, let’s throw an error, because that means our program isn’t valid. On the other hand, if we have started at least one loop, we can pop the most recently added loop expression from the loop stack and use that for step 2.

Otherwise, let’s parse our current instruction into an expression. Here, multiple dispatch saves the day again, by giving us a concise way to map each instruction to an expression:

2. Figuring out where to push the expression

Is our instruction the start of a loop? No problem — we can just push it onto the loop stack. Otherwise, let’s check if the loop stack is empty. That means our expression is at the top level of the function’s local scope, and we can just push it into the expression stack instead. But what if we’re in the middle of a loop? Well, remember when I dumped a while loop expression earlier on in the article? Let’s look at that again:

julia> dump(:(while i != 0; i -= 1; end))

Expr

head: Symbol while

args: Array{Any}((2,))

1: Expr

head: Symbol call

args: Array{Any}((3,))

1: Symbol !=

2: Symbol i

3: Int64 0 <we want to put our expressions here!>

2: Expr |

head: Symbol block v

args: Array{Any}((2,)) <---- <while loop body>

1: LineNumberNode

line: Int64 1

file: Symbol none

2: Expr

head: Symbol -=

args: Array{Any}((2,))

1: Symbol i

2: Int64 1