packages feed

haskell-brainfuck (empty) → 0.1.0.0

raw patch · 9 files changed

+628/−0 lines, 9 filesdep +QuickCheckdep +basedep +bytestringsetup-changed

Dependencies added: QuickCheck, base, bytestring, haskell-brainfuck, mtl, parsec, tasty, tasty-quickcheck

Files

+ LICENSE view
@@ -0,0 +1,21 @@+The MIT License (MIT)++Copyright (c) <year> <copyright holders>++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+THE SOFTWARE.
+ README.md view
@@ -0,0 +1,43 @@+# haskell-brainfuck++Interpreter for the+[brainfuck programming language](http://www.muppetlabs.com/~breadbox/bf/)++[![Build Status](https://api.travis-ci.org/paraseba/haskell-brainfuck.svg?branch=master)](https://travis-ci.org/paraseba/haskell-brainfuck)++haskel-brainfuck is distributed as a library, but it also includes an executable+to run brainfuck programs. You can find haskell-brainfuck in+[Hackage](https://hackage.haskell.org/package/haskell-brainfuck-0.1.0.0)++## Usage+### Library+```haskell+import HaskBF.Eval+import qualified Data.ByteString.Lazy as BS+import Control.Monad.State++main = do+  -- The following will evaluate the file using stdin and stdout for I/O.+  -- Evaluation results in an EvalResult++  file <- BS.readFile "/path/to/file.bf"+  (EvalSuccess _) <- evalBS defaultIOMachine file+  print "ok"+++  -- The following will evaluate the file using the State monad and input+  -- provided by input++  let input  = []+      output = []+      result = execState (evalStr simulatorMachine "+.>-.") (SimState input output)+  print $ simStateOutput result == [1, -1]+```++### Executable+```bash+brainfuck fib.bf+```++## Documentation+http://paraseba.github.io/haskell-brainfuck/
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ haskell-brainfuck.cabal view
@@ -0,0 +1,62 @@+name:                haskell-brainfuck+version:             0.1.0.0++synopsis: BrainFuck interpreter++description: BrainFuck language interpreter.+             Provides a library for evaluation and an executable to evaluate+             brainfuck files. Evaluation happens under an arbitrary monad so+             programn can be evaluated doing I/O to stdin/stdout or in memory+             using the State monad.++license:             MIT+license-file:        LICENSE++author:              Sebastian Galkin <paraseba@gmail.com>+maintainer:          Sebastian Galkin <paraseba@gmail.com>++copyright: (c) 2014 Sebastian Galkin++category:            Language+build-type:          Simple+extra-source-files: README.md++cabal-version:       >=1.10++source-repository head+  type: git+  location: https://github.com/paraseba/haskell-brainfuck++library+    exposed-modules:+      HaskBF.Parser, HaskBF.Tape, HaskBF.Eval+    hs-source-dirs: src/+    build-depends:        base >=4.7 && <4.8+                         ,bytestring==0.10.4.0+                         ,parsec==3.1.5+                         ,mtl==2.1.3.1+    default-extensions: TupleSections+    default-language: Haskell2010+    ghc-options: -Wall -fwarn-tabs -fno-warn-unused-do-bind -fno-warn-deprecated-flags+                 -funbox-strict-fields++executable brainfuck+  main-is: src/Main.hs+  build-depends:         base >=4.7 && <4.8+                       , bytestring==0.10.4.0+                       , haskell-brainfuck+  default-language:    Haskell2010+++Test-Suite test+  type: exitcode-stdio-1.0+  main-is: test.hs+  default-language: Haskell2010+  hs-source-dirs: tests+  build-depends:   base >=4.7 && <4.8+                 , haskell-brainfuck+                 , bytestring==0.10.4.0+                 , mtl==2.1.3.1+                 , tasty==0.8.1.3+                 , tasty-quickcheck==0.8.1+                 , QuickCheck==2.7.5
+ src/HaskBF/Eval.hs view
@@ -0,0 +1,193 @@+{- |+Module      : HaskBF.Eval+Description : Evaluate a BrainFuck program+Copyright   : (c) Sebastian Galkin, 2014+License     : MIT+Maintainer  : paraseba@gmail.com+Stability   : experimental++This module exports functions that allow to evaluate a BrainFuck program.+Evaluation supports two types of error, parsing and execution, by returning+instances of 'EvalResult'++This module should be all library users need to import.+-}++module HaskBF.Eval (+    eval, evalBS, evalStr+  , EvalResult (..)+  , Machine (..)+  , defaultIOMachine+  , simulatorMachine, SimState (SimState), simStateOutput+  , emptyState++  , module HaskBF.Tape+  , module HaskBF.Parser+) where++import Data.Int+  ( Int8 )++import Control.Monad+  ( liftM, when )++import Control.Monad.State+  ( State, modify, state, StateT ( StateT ), execStateT, get)++import Control.Monad.Error+  ( ErrorT ( ErrorT ), runErrorT )++import qualified Data.ByteString.Lazy as BS+import qualified Data.ByteString.Lazy.Char8 as BSC++import qualified HaskBF.Parser as P++import HaskBF.Parser+  ( ParseError )++import HaskBF.Tape+  ( Tape ( Tape ), blankTape, BFTape, BFExError, errMsg+  , errTape, rTape, wTape, left, right, inc, dec)++type ExecutionState m = StateT BFTape (ErrorT BFExError m)++{- | Underlying input output for the evaluation machine. Changing the monad 'm'+ - achives different results. For instance using the 'IO' monad an evaluator can+ - be created that does input/output to stdin/stdout. If the monad is 'State',+ - for instance, input/output can happen in memory.+ -+ - We offer two implementations of 'Machine':+ -+ -   * 'defaultIOMachine': under the 'IO' monad, does input/output using the+ -   standard streams+ -   * 'simulatorMachine': under the 'State' monad, does input/output on lists+ -+ - It's easy to create other 'Machine's by using different monads and+ - functions. -}+data Machine m =+  Machine { putByte :: Int8 -> m () -- ^ Write a byte to the output, under+                                    -- monad 'm'+          , getByte :: m Int8       -- ^ Get a byte under the 'm' monad+          }++evalTape :: Monad m+         => Machine m+         -> P.Program+         -> ExecutionState m ()+evalTape m = mapM_ (evalOp m)+++{- | Evaluate a parsed BrainFuck program using I/O provided by the given+ - 'Machine'+ -+ - The result is either an execution error or the 'BFTape' representing the+ - resulting state of the tape after the last instruction was executed. -}+eval :: Monad m+     => Machine m                    -- ^ The machine used to do I/O+     -> P.Program                    -- ^ The program to evaluate+     -> m (Either BFExError BFTape)  -- ^ Resulting tape after evaluation or+                                     -- execution error+eval m p = runErrorT $ execStateT (evalTape m p) blankTape++-- | Evaluation result of an unparsed BrainFuck program+data EvalResult = EvalSuccess BFTape+                  {- ^ Parsing and evaluation were successful. The resulting+                   - state of the tape after the last instruction+                   - was executed. -}++                | EvalExecError BFExError+                  {- ^ The program was parsed successfully but evaluation+                   - failed.+                   - The reason for failure is overflowing a limit of the tape.+                   - The state of the tape before the error is included -}++                | EvalParseError ParseError+                  {- ^ The program can not be parsed. Parsing error message is+                   - included -}++{- | Evaluate an unparsed BrainFuck program using I/O provided by the given+ - 'Machine'+ -+ - The result is returned as an 'EvalResult' -}+evalBS :: Monad m+       => Machine m     -- ^ The machine used to do I/O+       -> BS.ByteString -- ^ The code to evaluate+       -> m EvalResult  -- ^ Parsing/evaluation result+evalBS machine program =+  either parseError evaluate . P.parseProgram $ program+  where parseError = return . EvalParseError+        evaluate = liftM (either EvalExecError EvalSuccess) . eval machine++{- | Evaluate an unparsed BrainFuck program using I/O provided by the given+ - 'Machine'+ -+ - The result is returned as an 'EvalResult' -}+evalStr :: Monad m+        => Machine m    -- ^ The machine used to do I/O+        -> String       -- ^ The code to evaluate+        -> m EvalResult -- ^ Parsing/evaluation result+evalStr m = evalBS m . BSC.pack++evolve :: Monad m+          => (BFTape -> m (Either BFExError BFTape))+          -> ExecutionState m ()+evolve g = StateT $ ErrorT . (>>= return . liftM ((),)) . g++evalOp :: Monad m+       => Machine m+       -> P.Op+       -> ExecutionState m ()++evalOp _ P.IncP = evolve $ return . right+evalOp _ P.DecP = evolve $ return . left+evalOp _ P.Inc  = evolve $ return . Right . inc+evalOp _ P.Dec  = evolve $ return . Right . dec++evalOp ( Machine { putByte = putB } ) P.PutByte =+  evolve $ \ tape -> liftM ( const ( Right tape ) ) $ ( putB . rTape ) tape++evalOp ( Machine {getByte = getB } ) P.GetByte =+  evolve $ \ tape -> liftM ( Right . flip wTape tape ) getB++evalOp machine (P.Loop ops) = do+  tape <- get+  when (rTape tape /= 0) $+    evalTape machine ops >> evalOp machine (P.Loop ops)+++{- | A 'Machine' that can evaluate code under the 'IO' monad by doing I/O+ - to stdin/stout.+ -+ - Bytes are read by comnverting them from the ASCII code -}+defaultIOMachine :: Machine IO+defaultIOMachine = Machine (putChar . toEnum . fromIntegral)+                           (fmap (fromIntegral . fromEnum) getChar)++{- | State used by 'simulatorMachine' to evaluate code under the 'State' monad+ -+ - It maintains input and output bytes inside lists -}+data SimState =+  SimState {input :: [Int8], -- ^ Input bytes to use when the program+                             -- does @ "," @+            output :: [Int8] -- ^ Store for the program outputs done with+                             -- @ "." @+           }++-- | Extract the output stream from a 'simulatorMachine' state+simStateOutput :: SimState -> [Int8]+simStateOutput = reverse . output++-- | Initial 'simulatorMachine' state+emptyState :: SimState+emptyState = SimState [] []++{- | A 'Machine' that can evaluate program doing in-memory I/O under the 'State'+ - monad. It stores state as 'SimState' -}+simulatorMachine :: Machine (State SimState)+simulatorMachine =+  Machine (modify . writeByte)+          (state readByte)+  where writeByte byte s@(SimState {output = o} ) = s {output = byte : o}+        readByte s@(SimState {input = (byte : rest)}) = (byte, s {input = rest})+        readByte _ = error "Not enough input available"+
+ src/HaskBF/Parser.hs view
@@ -0,0 +1,85 @@+{-# OPTIONS_HADDOCK show-extensions #-}++{- |+Module      : HaskBF.Parser+Description : Parse brainfuck program into a Program abstract datatype+Copyright   : (c) Sebastian Galkin, 2014+License     : MIT+Maintainer  : paraseba@gmail.com+Stability   : experimental++-}++module HaskBF.Parser (+       Program+     , Op (..)+     , parseProgram+     , module Text.Parsec.Error+     ) where++import Text.Parsec.Prim+  ( (<|>), runP, skipMany )++import Text.Parsec.Combinator+  ( between, sepEndBy, eof )++import Text.Parsec.Error+  ( ParseError )++import Text.Parsec.ByteString.Lazy+  ( Parser )++import Text.Parsec.Char+  ( space, spaces, char )++import Control.Applicative+  ( (<*), (<$>) )++import qualified Data.ByteString.Lazy as BS++-- | Brainfuck operations+data Op+  = IncP      -- ^ Increment the byte pointer: @ ">" @+  | DecP      -- ^ Decrement the byte pointer: @ "<" @+  | Inc       -- ^ Increment the byte pointed by the pointer: @ "+" @+  | Dec       -- ^ Decrement the byte pointed by the pointer: @ "-" @+  | PutByte   -- ^ Write the byte pointed by the pointer (side-effect): @ "." @+  | GetByte   -- ^ Read a byte, write at the current location+              -- (side-effect): @ "," @+  | Loop [Op] -- ^ Loop over 'Op's untill current @ == 0 @: @ "[...]" @+  deriving (Show, Eq)++-- | A program is a list of 'Op's. One possible 'Op' is a 'Loop' of other 'Op's+type Program = [Op]++program :: Parser Program+program = do+  skipMany space+  sepEndBy operation spaces++fullProgram :: Parser Program+fullProgram = program <* eof++operation :: Parser Op+operation = simpleOp <|> loop++simpleChar :: Parser Char+simpleChar = (char '>') <|> (char '<') <|> (char '+') <|>+             (char '-') <|> (char '.') <|> (char ',')++simpleOp :: Parser Op+simpleOp = fmap build simpleChar+  where build '>' = IncP+        build '<' = DecP+        build '+' = Inc+        build '-' = Dec+        build '.' = PutByte+        build ',' = GetByte+        build _   = error "Unknown character" -- this should never happen++loop :: Parser Op+loop = Loop <$> between (char '[') (char ']') program++-- | Parse program stream. Returns an error or the parsed 'Program'+parseProgram :: BS.ByteString -> Either ParseError Program+parseProgram = runP fullProgram () ""
+ src/HaskBF/Tape.hs view
@@ -0,0 +1,115 @@+{-# LANGUAGE FlexibleInstances #-}++{- |+Module      : HaskBF.Tape+Description : Implement the brainfuck tape+Copyright   : (c) Sebastian Galkin, 2014+License     : MIT+Maintainer  : paraseba@gmail.com+Stability   : experimental++Provides a type and operations to implement the brainfuck tape. The tape has+the concept of a pointer, and the pointer can be incremented or decremented.++-}++module HaskBF.Tape (+  Tape (Tape)+  , ExecutionError (errMsg, errTape)+  , rTape, wTape+  , inc, dec+  , right, left+  , BFExError+  , BFTape+  , blankTape+) where++import Data.Int+  ( Int8 )+import Control.Monad.Error+  ( Error, strMsg )++{- | Brainfuck tape. Constructor arguments correspond to+ -+ -   1. left of the current pointer+ -   2. current pointed value+ -   3. right of the current pointer+ -+ - The left part of the tape is reversed, so the first element of the list+ - is the rightmost position. The right list is normal order, its first element+ - is the leftmost one. -}+data Tape t = Tape [t] t [t]+  deriving (Show)++-- | Write element to the current position in the tape+wTape :: t      -- ^ The element to write+      -> Tape t -- ^ The tape+      -> Tape t -- ^ The modified tape+wTape b (Tape l _ r) = Tape l b r++-- | Read the pointed element+rTape :: Tape t -- ^ The tape+      -> t      -- ^ The element currently pointed by the pointer+rTape (Tape _ current _) = current++update :: (t -> t) -> Tape t -> Tape t+update f t = wTape (f $ rTape t) t++-- | Increment the currently pointed element+inc :: Num a+    => Tape a -- ^ The tape+    -> Tape a -- ^ The tape with the current position incremented+inc = update (+ 1)++-- | Decrement the currently pointed element+dec :: Num a+    => Tape a -- ^ The tape+    -> Tape a -- ^ The tape with the current position decremented+dec = update (+ (- 1))++{- | Type for execution errors, trying to move the tape beyond one of its+ - ends. The 'String' argument is the error message and the 'Tape' is in+ - the state right before the faulting operation -}+data ExecutionError a = ExecutionError {errMsg :: String, errTape :: Tape a}++-- | Move the pinter to the right+right :: Tape a  -- ^ The tape+      -> Either (ExecutionError a) (Tape a)+      {- ^ A new tape with its pointer pointing to the+       - element to the right of the pointer in the+       - original tape; or an execution error if the+       - tape to the right is exhausted -}+right t@(Tape _ _ []) =+  Left $ ExecutionError "Error trying to go right on an empty tape" t++right (Tape l c (r : rs)) = Right $ Tape (c : l) r rs++-- | Move the pinter to the left+left :: Tape a  -- ^ The tape+     -> Either (ExecutionError a) (Tape a)+     {- ^ A new tape with its pointer pointing to+      - the element to the left of the pointer in+      - the original tape; or an execution error if+      - the tape to the left is exhausted -}+left t@(Tape [] _ _) =+  Left $ ExecutionError "Error trying to go left on an empty tape" t++left (Tape (l : ls) c r) = Right $ Tape ls l (c : r)++constTape :: t -> Tape t+constTape b = Tape [] b (repeat b)++-- | Execution error type for basic Brainfuck tapes+type BFExError = ExecutionError Int8++-- | Brainfuck tapes type+type BFTape = Tape Int8++instance Error BFExError where+  strMsg s = ExecutionError s (Tape [] 0 [])++{- | A @(0 :: 'Int8')@ initialized, infinite 'Tape' pointing to its+ - leftmost position. An attemp to move the pointer left will result+ - in an error -}+blankTape :: BFTape+blankTape = constTape 0
+ src/Main.hs view
@@ -0,0 +1,76 @@+{- |+Module      : Main+Description : Evaluate a BrainFuck program+Copyright   : (c) Sebastian Galkin, 2014+License     : MIT+Maintainer  : paraseba@gmail.com+Stability   : experimental++Usage:++> brainfuck path/to/program.bf++It will parse the program and evaluate it by doing I/O to the console. In case+of parsing or execution errors it reports them to stderr++-}++module Main(main) where++import HaskBF.Eval+  ( evalBS, EvalResult ( EvalSuccess, EvalExecError, EvalParseError )+  , defaultIOMachine, BFExError, Tape ( Tape ), BFTape, errMsg, errTape, rTape )++import System.Exit+  ( ExitCode (..), exitWith )++import System.Environment+  ( getArgs, getProgName )++import qualified Data.ByteString.Lazy as BS++main :: IO ExitCode+main =+  getProgram                >>=+    BS.readFile             >>=+    evalBS defaultIOMachine >>=+    reportResults           >>=+    exitWith++{- | Read command line and obtain the path to the program. Display error message+ - if missing argument -}+getProgram :: IO FilePath+getProgram = do+  args <- getArgs+  if length args /= 1+    then do+           exe <- getProgName+           error $ "Usage: " ++ exe ++ " filepath"+    else return $ head args++-- | Report result of program parsing and evaluation+reportResults :: EvalResult -> IO ExitCode++reportResults (EvalSuccess _) = return ExitSuccess++reportResults (EvalParseError parseError) = do+  putStrLn "Error parsing program:"+  print parseError+  return $ ExitFailure 1++reportResults (EvalExecError err) = do+  putStrLn $ "Error evaluating program: " ++ errMsg err+  putStrLn $ "Current tape value: " ++ (show . rTape . errTape) err+  putStrLn $ "Consumed tape: " ++ (showConsumed . errTape) err+  return $ ExitFailure 2++{- | Use heuristic to display tape state. It estimates consumed right tape by+ - calling 'consumed' -}+showConsumed :: BFTape -> String+showConsumed (Tape _ _ r) =+  "[" ++ concatMap ( (++ ",") . show ) (consumed r ++ [0, 0, 0]) ++ "..."++-- | Return consumed tape by assuming that it is unused after 10 zeros.+consumed :: (Eq a, Num a) => [a] -> [a]+consumed (0 : 0 : 0 : 0 : 0 : 0 : 0 : 0 : 0 : 0 : _) = []+consumed (x : xs) = x : consumed xs
+ tests/test.hs view
@@ -0,0 +1,31 @@++{-main :: IO ()-}+{-main = do-}+  {-Test.Parser.tests-}++import Test.Tasty+import Test.Tasty.QuickCheck as QC+import Test.QuickCheck (Testable)++import Data.List+import Data.Ord++import qualified Test.Parser+import qualified Test.Tape+import qualified Test.Eval++main = defaultMain tests++tests :: TestTree+tests = testGroup "Tests" [properties]++properties :: TestTree+properties = testGroup "Properties" qcProps++mkGroup :: Testable prop => TestName -> [(TestName, prop)] -> TestTree+mkGroup name tests = testGroup name $ map (uncurry QC.testProperty) tests++qcProps = [mkGroup "Parser" Test.Parser.properties+          ,mkGroup "Tape"   Test.Tape.properties+          ,mkGroup "Eval"   Test.Eval.properties+          ]