PageRenderTime 24ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/HaskFuck.hs

http://github.com/alexsparrow/HaskFuck
Haskell | 193 lines | 118 code | 25 blank | 50 comment | 11 complexity | e1283d94375004019ccaa7fab0cdef1b MD5 | raw file
  1. {-
  2. Experimental (i.e. not working properly) brainfuck interpreter in Haskell
  3. The test program below works but others may not
  4. The , (comma) command has not been implemented yet
  5. I mainly wrote this to try to wrap my head around monad transformers and improve
  6. my general Haskell skills. Please feel free to steal from/modify but bear in
  7. mind I am no Haskell expert (yet) so it may not be a good example to learn from!
  8. -}
  9. import Control.Monad.State
  10. import Data.Maybe
  11. import Control.Monad.Reader
  12. import Data.Array.IO
  13. import Data.List
  14. import Data.Char
  15. import System.Environment
  16. -- Brainfuck program for testing purposes
  17. test = unlines [
  18. "+++++ +++++ initialize counter (cell #0) to 10",
  19. "[ use loop to set the next four cells to 70/100/30/10",
  20. " > +++++ ++ add 7 to cell #1",
  21. " > +++++ +++++ add 10 to cell #2 ",
  22. " > +++ add 3 to cell #3",
  23. " > + add 1 to cell #4",
  24. " <<<< - decrement counter (cell #0)",
  25. "] ",
  26. "> ++ . print 'H'",
  27. "> + . print 'e'",
  28. "+++++ ++ . print 'l'",
  29. ". print 'l'",
  30. "+++ . print 'o'",
  31. "> ++ . print ' '",
  32. "<< +++++ +++++ +++++ . print 'W'",
  33. "> . print 'o'",
  34. "+++ . print 'r'",
  35. "----- - . print 'l'",
  36. "----- --- . print 'd'",
  37. "> + . print '!'",
  38. "> . print '\n'"
  39. ]
  40. -- Brainfuck command type
  41. data Cmd = Next | Prev | Inc | Dec | Out | In | Start | End deriving Show
  42. -- State of execution
  43. -- (instruction index, data pointer)
  44. type BFState = (Int, Int)
  45. -- Shared environment
  46. -- cmdStream : List of brainfuck commands in program
  47. -- memArray : Mutable array used for brainfuck memory
  48. data BFRecord = BFRecord {
  49. cmdStream :: [Cmd],
  50. memArray :: IOArray Int Char
  51. }
  52. -- Monad layers
  53. -- First layer : IO monad for I/O and access to mutable arrays
  54. -- Second layer : State monad storing BFState above
  55. -- Third layer : Reader monad for storing command list and reference to mutable memory array
  56. type StateIO = StateT BFState IO
  57. type ReaderStateIO = ReaderT BFRecord StateIO
  58. -- Lift us out of the Reader/State monad into IO
  59. io = lift.lift
  60. -- Parse a single character into a BrainFuck command
  61. -- Valid BF commands are wrapped with Just
  62. -- All other characters return Nothing
  63. parse :: Char -> Maybe Cmd
  64. parse c = case c of
  65. '>' -> Just Next
  66. '<' -> Just Prev
  67. '+' -> Just Inc
  68. '-' -> Just Dec
  69. '.' -> Just Out
  70. ',' -> Just In
  71. '[' -> Just Start
  72. ']' -> Just End
  73. otherwise -> Nothing
  74. -- Parse string into brainfuck commands discarding invalid chars
  75. parseCmds :: String -> [Cmd]
  76. parseCmds = mapMaybe parse
  77. -- Use to accumulate bracket nesting depth
  78. -- If we encounter a [, the depth is increased
  79. -- If we encounter a ], decreased
  80. -- Other commands have no effect
  81. depth :: Int -> Cmd -> Int
  82. depth x c = case c of
  83. Start -> x + 1
  84. End -> x - 1
  85. otherwise -> x
  86. -- Return nesting depth as a list of ints
  87. -- Depending on starting command, the initial depth will be +/- 1
  88. -- Scan through instruction list and build list of nesting depths
  89. depths :: [Cmd] -> [Int]
  90. depths (Start:cmds) = scanl depth 1 cmds
  91. depths (End:cmds) = scanl depth (-1) cmds
  92. depths (_:cmds) = error "Should be Start or End"
  93. -- Return matching brace by walking through list of depths until zero is found
  94. findMatching :: [Cmd] -> Int
  95. findMatching cmds = fromJust $ findIndex (==0) (depths cmds)
  96. -- Takes a reference to a mutable array, index into the array and a function f
  97. -- Modifies element at given position by applying function f
  98. transArray ref idx f = do
  99. val <- io $ readArray ref idx
  100. io $ writeArray ref idx (f val)
  101. -- Execute a given command
  102. execute :: Cmd -> ReaderStateIO ()
  103. -- Increment data pointer
  104. execute Next = modify $ \(pc, ptr) -> (pc+1, ptr+1)
  105. -- Decrement data pointer
  106. execute Prev = modify $ \(pc, ptr) -> (pc+1, ptr-1)
  107. -- Increment data value
  108. execute Inc = do
  109. env <- ask
  110. (pc, ptr) <- get
  111. transArray (memArray env) ptr (\x -> chr $ ord x + 1)
  112. put (pc+1, ptr)
  113. -- Decrement data value
  114. execute Dec = do
  115. env <- ask
  116. (pc, ptr) <- get
  117. transArray (memArray env) ptr (\x -> chr $ ord x - 1)
  118. put (pc+1, ptr)
  119. -- If data pointer is 0 then jump past matching closing bracket
  120. -- Else just move to next instruction
  121. execute Start = do
  122. env <- ask
  123. (pc, ptr) <- get
  124. let match = findMatching (drop pc $ cmdStream env)
  125. val <- io $ readArray (memArray env) ptr
  126. modify $ \(pc, ptr) -> if val == (chr 0) then (pc+match+1, ptr) else (pc+1, ptr)
  127. -- If data pointer is non-zero jump to instruction after matching opening bracket
  128. -- Otherwise just move to next instruction
  129. execute End = do
  130. env <- ask
  131. (pc, ptr) <- get
  132. let match = findMatching (reverse $ take (pc +1) $ cmdStream env)
  133. val <- io $ readArray (memArray env) ptr
  134. modify $ \(pc, ptr) -> if val /= (chr 0) then (pc-match+1, ptr) else (pc+1, ptr)
  135. -- Print a character
  136. execute Out = do
  137. env <- ask
  138. (pc, ptr) <- get
  139. val <- io $ readArray (memArray env) ptr
  140. io $ putChar $ val
  141. put (pc+1, ptr)
  142. execute cmd = error $ "Not implemented: " ++ (show cmd)
  143. -- Execute program until we're at the end
  144. step ::ReaderStateIO ()
  145. step = do
  146. env <- ask
  147. (pc, _) <- get
  148. let cmd = (cmdStream env) !! pc
  149. -- io $ putStrLn $ (show cmd) ++ (show pc)
  150. execute cmd
  151. (pc, _) <- get
  152. if pc == length (cmdStream env) then
  153. return ()
  154. else step
  155. -- Run from input file (if supplied)
  156. -- or test program (defined above) otherwise
  157. main = do
  158. args <- getArgs
  159. input <- if length args == 0 then do
  160. return test
  161. else do
  162. s <- readFile $ args!!0
  163. return s
  164. mem <- newArray (0, 30000) (chr 0) :: IO (IOArray Int Char)
  165. runStateT (runReaderT step (BFRecord (parseCmds input) mem)) (0,0)
  166. return ()