brainfuck in Clojure. Part I: interpreter
Snarøya coast |
Let's write a simple, idiomatic brainfuck interpreter ourselves, step by step. It turns out that the transition from mutability to immutability is quite straightforward - rather than mutating state in-place we simply exchange previous state with the new one. In Brainfuck state is represented by
cells
(memory), cell
(pointer to one of the cells
, an index within cells
) and ip
(instruction pointer, an instruction currently being executed):(loop [cells [0N], cell 0, ip 0]I don't mutate any of the state variables (actually, I'cant by definition) but in each iteration I produce new set of state variables, discarding the old ones. Typically we will at least increment instruction pointer (to evaluate next instruction in the program) but possibly more. That's pretty much it, in each iteration we read one character of the
; interpretation
(recur cells cell (inc ip)))
program
(sequence of brainfuck opcodes) and proceed with appropriately updated state:(loop [cells [0N], cell 0, ip 0]This should be self-explanatory -
(condp = (get program ip)
\> (recur cells (inc cell) (inc ip))
\< (recur cells (dec cell) (inc ip))
\+ (recur (update-in cells [cell] inc) cell (inc ip))
\- (recur (update-in cells [cell] dec) cell (inc ip))
; more to come
(recur cells cell (inc ip))))
>
and <
move cell
pointer while +
and -
incremenet/decrement current cell accordingly. In all cases instruction pointer is incremented in order to execute next instruction during next iteration. So far so good. Code for >
is actually slightly more complex to achieve infinite growing of cells
vector but that's irrelevant. Handling loops in brainfuck is more interesting. Every time we encounter opening square bracket we conditionally jump to corresponding (not first encountered) closing bracket. A little bit of logic is required to handle that:(defn brainfuck-interpreter [& lines]Opening bracket jumps to corresponding closing bracket if current cell is zero and proceeds to next instruction otherwise. Closing bracket jumps unconditionally to corresponding opening bracket. Think of them as nested
(let goto-bracket (fn [same-bracket other-bracket ip dir]
(loop [i (dir ip) opened 0]
(condp = (nth program i)
same-bracket (recur (dir i) (inc opened))
other-bracket (if (zero? opened) i (recur (dir i) (dec opened)))
(recur (dir i) opened))))]
(loop [cells [0N], cell 0, ip 0]
(condp = (get program ip)
\[ (recur cells cell (inc (if (zero? (nth cells cell))
(goto-bracket \[ \] ip inc)
ip)))
\] (recur cells cell (goto-bracket \] \[ ip dec))
;...
nil cells
(recur cells cell (inc ip))))))
while
loops. Guess what, we just implemented brainfuck interpreter in functional language without mutating state, at all! The full source code follows, including impure I/O operations and all supporting code:(ns com.blogspot.nurkiewicz.brainfuck.interpreter)Using this interpreter is quite simple. It terminates when it encounters end of the program.
(defn brainfuck-interpreter [& lines]
(let [program (apply str lines)
goto-bracket (fn [same-bracket other-bracket ip dir]
(loop [i (dir ip) opened 0]
(condp = (nth program i)
same-bracket (recur (dir i) (inc opened))
other-bracket (if (zero? opened) i (recur (dir i) (dec opened)))
(recur (dir i) opened))))]
(loop [cells [0N], cell 0, ip 0]
(condp = (get program ip)
\> (let [next-ptr (inc cell)
next-cells (if (= next-ptr (count cells)) (conj cells 0N) cells)]
(recur next-cells next-ptr (inc ip)))
\< (recur cells (dec cell) (inc ip))
\+ (recur (update-in cells [cell] inc) cell (inc ip))
\- (recur (update-in cells [cell] dec) cell (inc ip))
\. (do
(print (char (nth cells cell)))
(recur cells cell (inc ip)))
\, (let [ch (.read System/in)]
(recur (assoc cells cell ch) cell (inc ip)))
\[ (recur cells cell (inc (if (zero? (nth cells cell))
(goto-bracket \[ \] ip inc)
ip)))
\] (recur cells cell (goto-bracket \] \[ ip dec))
nil cells
(recur cells cell (inc ip))))))
brainfuck-interpreter
returns state as it was upon termination to allow easier unit testing. This project is available on GitHub, but it was merely a warm-up. In the next article we shall write a brainfuck compiler in Clojure. In 100 lines of code. Stay tuned!Tags: brainfuck, clojure, functional programming