153 lines
5.6 KiB
Haskell
153 lines
5.6 KiB
Haskell
import Control.Monad.State
|
|
import Data.List (intersperse, nub, find)
|
|
|
|
data TapeMovement = MoveLeft | MoveRight | Stay deriving (Show, Eq)
|
|
-- Rule = (state 1, input, output, movement, state 2)
|
|
type Rule a = (a, a, a, TapeMovement, a)
|
|
-- Execution = (tape position, current machine state, tape)
|
|
type Execution a = (Int, a, [a])
|
|
type Log a = [Execution a]
|
|
type UTM a b = State (Machine a) b
|
|
|
|
-- can work with data of any type
|
|
data Machine a = Machine
|
|
{ allStates :: [a] -- not used actually
|
|
, initialState :: a -- not used actually, initial state in "current"
|
|
, finalStates :: [a]
|
|
, symbols :: [a] -- not used actually
|
|
, blank :: a
|
|
, noOpSymbol :: a -- means: don't change input / don't shift tape
|
|
, rules :: [Rule a]
|
|
, current :: Execution a
|
|
, machineLog :: Log a -- stores state changes from last to first
|
|
, machineLogActive :: Bool -- if true, intermediate steps are stored
|
|
, noRuleMsg :: a -- error symbol if no rule matches
|
|
, stopMsg :: a } -- symbol to append to the end result
|
|
deriving (Show)
|
|
|
|
-- it is not checked whether the input and output symbols are valid
|
|
apply :: Eq a => Rule a -> UTM a a
|
|
apply (_, _, output, direction, stateUpdate) = do
|
|
m <- get
|
|
let (pos, currentState, tape) = current m
|
|
tapeUpdate = if output == noOpSymbol m
|
|
then tape
|
|
else take pos tape ++ [output] ++ drop (pos + 1) tape
|
|
newTape
|
|
| pos == 0 && direction == MoveLeft = blank m : tapeUpdate
|
|
| succ pos == length tape && direction == MoveRight = tapeUpdate ++ [blank m]
|
|
| otherwise = tapeUpdate
|
|
newPosition = case direction of
|
|
MoveLeft -> if pos == 0 then 0 else pred pos
|
|
MoveRight -> succ pos
|
|
Stay -> pos
|
|
newState = if stateUpdate == noOpSymbol m
|
|
then currentState
|
|
else stateUpdate
|
|
put $! m { current = (newPosition, newState, newTape) }
|
|
return newState
|
|
|
|
-- rules with no-operation symbols and states must be underneath
|
|
-- rules with defined symbols and states
|
|
lookupRule :: Eq a => UTM a (Maybe (Rule a))
|
|
lookupRule = do
|
|
m <- get
|
|
let (pos, currentState, tape) = current m
|
|
item = tape !! pos
|
|
isValid (e, i, _, _, _) = e == currentState &&
|
|
(i == item || i == noOpSymbol m)
|
|
return $! find isValid (rules m)
|
|
|
|
msgToLog :: a -> UTM a ()
|
|
msgToLog e = do
|
|
m <- get
|
|
let (pos, currentState, tape) = current m
|
|
put $! m { machineLog = (pos, currentState, tape ++ [e]) : machineLog m }
|
|
|
|
toLog :: UTM a ()
|
|
toLog = do
|
|
m <- get
|
|
put $! m { machineLog = current m : machineLog m }
|
|
|
|
-- execute the machine's program
|
|
execute :: Eq a => UTM a ()
|
|
execute = do
|
|
toLog -- log the initial state
|
|
loop
|
|
where
|
|
loop = do
|
|
m <- get
|
|
r <- lookupRule -- look for a matching rule
|
|
case r of
|
|
Nothing -> msgToLog (noRuleMsg m)
|
|
Just rule -> do
|
|
stateUpdate <- apply rule
|
|
if stateUpdate `elem` finalStates m
|
|
then msgToLog (stopMsg m)
|
|
else do
|
|
when (machineLogActive m) toLog
|
|
loop
|
|
|
|
---------------------------
|
|
-- convenient functions
|
|
---------------------------
|
|
|
|
-- run execute, format and print the output
|
|
runMachine :: Machine String -> IO ()
|
|
runMachine m@(Machine { current = (_, _, tape) }) =
|
|
if null tape
|
|
then putStrLn "NO TAPE"
|
|
else case machineLog $ execState execute m of
|
|
[] -> putStrLn "NO OUTPUT"
|
|
xs -> do
|
|
mapM_ (\(pos, _, output) -> do
|
|
let formatOutput = concat output
|
|
putStrLn formatOutput
|
|
putStrLn (replicate pos ' ' ++ "^")) $ reverse xs
|
|
putStrLn $ show (length xs) ++ " STEPS. FINAL STATE: " ++
|
|
let (_, finalState, _) = head xs in show finalState
|
|
|
|
-- convert a string with format state+space+input+space+output+space+
|
|
-- direction+space+new state to a rule
|
|
toRule :: String -> Rule String
|
|
toRule xs =
|
|
let [a, b, c, d, e] = take 5 $ words xs
|
|
dir = case d of
|
|
"l" -> MoveLeft
|
|
"r" -> MoveRight
|
|
"*" -> Stay
|
|
in (a, b, c, dir, e)
|
|
|
|
-- load a text file and parse it to a machine.
|
|
-- see comments and examples
|
|
-- lines in the file starting with ';' are header lines or comments
|
|
-- header and input lines must contain a ':' and after that the content to be parsed
|
|
-- so there can be comments between ';' and ':' in those lines
|
|
loadMachine :: FilePath -> IO (Machine String)
|
|
loadMachine n = do
|
|
f <- readFile n
|
|
|
|
let ls = lines f
|
|
-- header: first 4 lines
|
|
([e1, e2, e3, e4], rest) = splitAt 4 ls
|
|
-- rules and input: rest of the file
|
|
re = map toRule . filter (not . null) $ map (takeWhile (/= ';')) rest
|
|
ei = head . words . tail . snd $ break (== ':') e1
|
|
va = head . words . tail . snd $ break (== ':') e3
|
|
ci = words . intersperse ' ' . tail . snd $ break (== ':') $ last rest
|
|
|
|
return Machine
|
|
{ rules = re
|
|
, initialState = ei
|
|
, finalStates = words . tail . snd $ break (== ':') e2
|
|
, blank = va
|
|
, noOpSymbol = head . words . tail . snd $ break (== ':') e4
|
|
, allStates = nub $ concatMap (\(a, _, _, _, e) -> [a, e]) re
|
|
, symbols = nub $ concatMap (\(_, b, c, _, _) -> [b, c]) re
|
|
, current = (0, ei, if null ci then [va] else ci)
|
|
-- we assume
|
|
, noRuleMsg = "\tNO RULE." -- error: no matching rule found
|
|
, stopMsg = "\tHALT." -- message: machine reached a final state
|
|
, machineLog = []
|
|
, machineLogActive = True }
|