I have designed a Brainf*ck interpreter in Haskell.
I would appreciate the code review, especially any tips related to error handling (e.g. parsing errors, runtime errors, etc.).
{-
Brainf**k interpreter
Instructions:
> Increment data pointer so that it points to next location in memory.
< Decrement data pointer so that it points to previous location in memory.
+ Increment the byte pointed by data pointer by 1. If it is already at its maximum value, 255, then new value will be 0.
- Decrement the byte pointed by data pointer by 1. If it is at its minimum value, 0, then new value will be 255.
. Output the character represented by the byte at the data pointer.
, Read one byte and store it at the memory location pointed by data pointer.
[ If the byte pointed by data pointer is zero, then move instruction pointer to next matching ']', otherwise move instruction pointer to next command.
] If the byte pointed by data pointer is non-zero, then move instruction pointer to previous matching '[' command, otherwise to next command.
Example. The first line of hello-world.bf must contain the input ('$' means there is no input).
hello-world.bf:
$
+++++ +++++ initialize counter (cell #0) to 10
[ use loop to set the next four cells to 70/100/30/10
> +++++ ++ add 7 to cell #1
> +++++ +++++ add 10 to cell #2
> +++ add 3 to cell #3
> + add 1 to cell #4
<<<< - decrement counter (cell #0)
]
> ++ . print 'H'
> + . print 'e'
+++++ ++ . print 'l'
. print 'l'
+++ . print 'o'
> ++ . print ' '
<< +++++ +++++ +++++ . print 'W'
> . print 'o'
+++ . print 'r'
----- - . print 'l'
----- --- . print 'd'
> + . print '!'
$ ghc -O2 brainf.hs
$ ./brainf < hello-world.bf
Hello World!
-}
{-# LANGUAGE BangPatterns #-}
import qualified Data.Vector.Storable as V
import Data.Word ( Word8 )
import Data.Char ( chr )
import qualified Data.Map.Strict as Map
import qualified GHC.Prim as Prim
import Unsafe.Coerce ( unsafeCoerce )
import Debug.Trace
newtype MemCell = MemCell Word8 deriving Show
type Memory = [MemCell]
type Instructions = V.Vector Char
type JumpMap = Map.Map Position Position
-- The program consists of an instructions vector and matching brackets map.
-- This could be replaced with a double-linked (bidirectional) tree
type Program = (Instructions, JumpMap)
-- Currently executed command position
newtype Position = Position Int deriving (Show, Ord, Eq)
type Input = (Char, Word8) -- Instruction and external input
type Counter = Int
data State = State Memory Memory Position
type Output = (Maybe Char, CMD)
data CMD = Continue | Jump Direction deriving (Show, Eq)
data Direction = L | R deriving (Show, Eq)
incP = '>'
decP = '<'
incB = '+'
decB = '-'
prnt = '.'
rd = ','
moveR = '['
moveL = ']'
parse :: String -> Program
parse s = (V.fromList instrucs, buildMap 0 [] instrucs)
where
instrucs = filter f $ s
f x | x == incP = True
| x == decP = True
| x == incB = True
| x == decB = True
| x == prnt = True
| x == rd = True
| x == moveR = True
| x == moveL = True
| otherwise = False
buildMap :: Int -> [Int] -> [Char] -> Map.Map Position Position
buildMap _ _ [] = Map.empty
buildMap pos stack (x:xs) = r
where r | x == moveR = buildMap pos' (pos:stack) xs
| x == moveL = Map.insert (Position pos) (Position s) (Map.insert (Position s) (Position pos) $ buildMap pos' ss xs)
| otherwise = buildMap pos' stack xs
pos' = pos + 1
s = head stack
ss = tail stack
initial :: State
initial = State [] (repeat (MemCell 0)) (Position 0)
pu :: State -> Input -> (State, Output)
pu state@(State memL memR@((MemCell cell):tMemR) (Position pos)) (instruc, inpt) = (state', (out, cmd))
where state' = State memL' memR' (Position pos')
pos' = 0 -- Dummy
memR' | instruc == incB = (MemCell (cell + 1)) : tMemR
| instruc == decB = (MemCell (cell - 1)) : tMemR
| instruc == rd = (MemCell inpt) : tMemR
| instruc == incP = tMemR
| instruc == decP = (head memL) : memR
| otherwise = memR
memL' | instruc == incP = (MemCell cell) : memL
| instruc == decP = tail memL
| otherwise = memL
out | instruc == prnt = Just (chr . fromIntegral $ cell)
| otherwise = Nothing
cmd | instruc == moveR = Jump R
| instruc == moveL = Jump L
| otherwise = Continue
getInstruc :: Instructions -> Position -> Char
getInstruc prg (Position pos) = prg V.! pos
jump :: CMD -> JumpMap -> MemCell -> Position -> Position
jump (Jump R) jm (MemCell 0) pos = jm Map.! pos
jump (Jump R) _ _ (Position i) = Position $ i + 1
jump (Jump L) _ (MemCell 0) (Position i) = Position $ i + 1
jump (Jump L) jm _ pos = jm Map.! pos
handler :: Program -> State -> Int -> [Word8] -> IO String
handler prg@(!instr, !jumpMap) state@(State memL memR@(cell:_) pos@(Position !i)) !cnt input = r
where
((State memL' memR' _), (out, cmd)) = pu state (instruc, head input)
instruc = instr `getInstruc` pos
input' = if instruc == rd
then (tail input)
else input
pos' = case cmd of
Jump _ -> jump cmd jumpMap cell pos
otherwise -> Position (i + 1)
r | cnt == maxIter = return "\n(Reached maximal number of iterations)"
| i == V.length instr = return "" -- Normal termination
| otherwise = do
r1 <- handler prg (State memL' memR' pos') (cnt + 1) input'
let m Nothing = r1
m (Just chr) = chr : r1
return $ m out
interpret = handler
toWord8 :: Char -> Word8
toWord8 = unsafeCoerce
maxIter = 100000
main = do
input <- map toWord8 <$> getLine
code <- getContents
let program = parse code
interpret program initial 0 input >>= putStr
[Update]
The latest version with respect to the code review by Zeta, is here https://gist.github.com/masterdezign/2c3eae1aadaa3f84aa148c6ee9747ac9