{- (c) 2022 Owen Bechtel License: MIT (see LICENSE file) -} {-# LANGUAGE LambdaCase #-} module Main (main) where import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE) import Control.Monad.Trans.State (StateT, runStateT, gets, modify) import Text.Read (readMaybe) import Data.List (nub) import Data.Char (ord, chr) import Data.Function (on) import Data.Bifunctor (first, second) import Control.Applicative (liftA2) import Control.Monad (foldM, void, when) import Control.Exception (IOException, catch) import System.Environment (getArgs, getProgName) import qualified Examples main :: IO () main = getArgs >>= \case ["run", path] -> runFile path ["examples"] -> Examples.copy _ -> do name <- getProgName putStrLn "Unique version 0.1.0.0" putStrLn "Commands: " putStrLn (" " ++ name ++ " run interpret a Unique file") putStrLn (" " ++ name ++ " examples create example programs") runFile :: FilePath -> IO () runFile path = safeReadFile path >>= \case Nothing -> putStrLn ("ERROR: file '" ++ path ++ "' does not exist") Just program -> do runUnique (runProgram program) >>= \case Left err -> putStrLn ("ERROR: " ++ err) Right () -> return () safeReadFile :: FilePath -> IO (Maybe String) safeReadFile path = fmap Just (readFile path) `catch` \e -> return (const Nothing (e :: IOException)) type Stack = [[Int]] data Mode = CommandMode | LengthMode | PushMode Int type Unique = ExceptT String (StateT (Stack, Mode) IO) runUnique :: Unique a -> IO (Either String a) runUnique u = fmap fst (runStateT (runExceptT u) ([], CommandMode)) fromIO :: IO a -> Unique a fromIO = lift . lift getStack :: Unique Stack getStack = lift (gets fst) putStack :: Stack -> Unique () putStack = modifyStack . const modifyStack :: (Stack -> Stack) -> Unique () modifyStack = lift . modify . first getMode :: Unique Mode getMode = lift (gets snd) putMode :: Mode -> Unique () putMode = modifyMode . const modifyMode :: (Mode -> Mode) -> Unique () modifyMode = lift . modify . second runProgram :: String -> Unique () runProgram program = do case evalProgram (removeComments program) of Left err -> throwE err Right commands -> execCommands commands removeComments :: String -> String removeComments = unlines . map removeComment . lines where removeComment = takeWhile (/= '#') execCommands :: [Int] -> Unique () execCommands commands = do mapM_ execute commands mode <- getMode case mode of CommandMode -> return () _ -> throwE "fewer numbers in array than expected" execute :: Int -> Unique () execute num = getMode >>= \case CommandMode -> execCommand num LengthMode -> if num <= 0 then throwE "array length must be positive" else putMode (PushMode num) PushMode len -> do modifyStack (insert num) putMode (if len == 1 then CommandMode else PushMode (len - 1)) execCommand :: Int -> Unique () execCommand = \case --PUSH 0 -> do push [] putMode LengthMode --DELETE 1 -> void pop --SWAP 2 -> do xs <- pop ys <- pop push xs push ys --ROTATE 3 -> do xs <- pop ys <- pop zs <- pop push ys push xs push zs --DUPLICATE 4 -> do xs <- pop push xs push xs --APPEND 5 -> do xs <- pop ys <- pop push (xs ++ ys) --UNFOLD 6 -> do xs <- pop mapM_ (push . return) (reverse xs) --IF 7 -> do commands <- pop conditions <- pop when (all truthy conditions) (execCommands commands) --IF ELSE 8 -> do elseCommands <- pop commands <- pop conditions <- pop if all truthy conditions then execCommands commands else execCommands elseCommands --WHILE 9 -> do commands <- pop whileLoop commands --ADD 10 -> apOp (+) 11 -> zipOp (+) --SUBTRACT 12 -> apOp (-) 13 -> zipOp (-) --MULTIPLY 14 -> apOp (*) 15 -> zipOp (*) --DIVIDE 16 -> apOpM divide 17 -> zipOpM divide --MODULO 18 -> apOpM modulo 19 -> zipOpM modulo --POWER 20 -> apOpM power 21 -> zipOpM power --OR 22 -> boolApOp ((||) `on` truthy) 23 -> boolZipOp ((||) `on` truthy) --AND 24 -> boolApOp ((&&) `on` truthy) 25 -> boolZipOp ((&&) `on` truthy) --LESS THAN 26 -> boolApOp (<) 27 -> boolZipOp (<) --GREATER THAN 28 -> boolApOp (>) 29 -> boolZipOp (>) --EQUAL TO 30 -> boolApOp (==) 31 -> boolZipOp (==) --NEGATE 32 -> do xs <- pop push (map negate xs) --NOT 33 -> do xs <- pop push (map (toInt . not . truthy) xs) --REVERSE 34 -> do xs <- pop push (reverse xs) --LENGTH 35 -> foldOp length --SUM 36 -> foldOp sum --PRODUCT 37 -> foldOp product --ANY 38 -> foldOp (toInt . any truthy) --ALL 39 -> foldOp (toInt . all truthy) --INPUT CHARACTER 40 -> do ch <- fromIO getChar push [ord ch] --INPUT STRING 41 -> do str <- fromIO getLine push (map ord str) --INPUT NUMBER 42 -> do num <- fromIO getLine case readMaybe num of Nothing -> throwE "input is not a number" Just x -> push [x] --OUTPUT CHARACTERS 43 -> do xs <- pop fromIO (putStr (map chr xs)) --OUTPUT NUMBERS 44 -> do xs <- pop fromIO (mapM_ print xs) --OTHER num -> throwE ("no command associated with number '" ++ show num ++ "'") truthy :: Int -> Bool truthy = (/= 0) toInt :: Bool -> Int toInt False = 0 toInt True = 1 insert :: Int -> Stack -> Stack insert num = \case [] -> [[num]] ([] : stack) -> [num] : stack (xs : stack) -> (num : xs) : stack pop :: Unique [Int] pop = getStack >>= \case [] -> throwE "no values in stack to pop" (xs : new) -> do putStack new return xs push :: [Int] -> Unique () push xs = modifyStack (xs:) apOp :: (Int -> Int -> Int) -> Unique () apOp f = do xs <- pop ys <- pop push (liftA2 f ys xs) zipOp :: (Int -> Int -> Int) -> Unique () zipOp f = do xs <- pop ys <- pop push (zipWith f ys xs) apOpM :: (Int -> Int -> Unique Int) -> Unique () apOpM f = do xs <- pop ys <- pop res <- sequence (liftA2 f ys xs) push res zipOpM :: (Int -> Int -> Unique Int) -> Unique () zipOpM f = do xs <- pop ys <- pop res <- sequence (zipWith f ys xs) push res boolApOp :: (Int -> Int -> Bool) -> Unique () boolApOp f = apOp (\x y -> toInt (f x y)) boolZipOp :: (Int -> Int -> Bool) -> Unique () boolZipOp f = zipOp (\x y -> toInt (f x y)) foldOp :: ([Int] -> Int) -> Unique () foldOp f = do xs <- pop push [f xs] divide :: Int -> Int -> Unique Int divide x y = if y == 0 then throwE "division by zero" else return (div x y) modulo :: Int -> Int -> Unique Int modulo x y = if y == 0 then throwE "modulo by zero" else return (mod x y) power :: Int -> Int -> Unique Int power x y = if y < 0 then throwE "negative exponent" else return (x ^ y) whileLoop :: [Int] -> Unique () whileLoop commands = do conditions <- pop when (all truthy conditions) $ do execCommands commands whileLoop commands data Symbol = Lit Int | Add | Subtract | Multiply | Ignore evalProgram :: String -> Either String [Int] evalProgram program = do symbols <- mapM readSymbol (words program) if getNums symbols == nub (getNums symbols) then reverse <$> foldM execSymbol [] symbols else Left "number occurs more than once" getNums :: [Symbol] -> [Int] getNums = \case [] -> [] (Lit x : ss) -> x : getNums ss (_ : ss) -> getNums ss readSymbol :: String -> Either String Symbol readSymbol = \case "+" -> return Add "-" -> return Subtract "*" -> return Multiply "[" -> return Ignore "]" -> return Ignore sym -> case readMaybe sym of Nothing -> Left ("cannot parse symbol '" ++ sym ++ "'") Just x -> return (Lit x) execSymbol :: [Int] -> Symbol -> Either String [Int] execSymbol xs = \case Lit x -> return (x : xs) Add -> operation (+) "add" Subtract -> operation (-) "subtract" Multiply -> operation (*) "multiply" Ignore -> return xs where operation f name = case xs of (x : y : t) -> return (f y x : t) _ -> Left ("not enough numbers to " ++ name)