hack-asm/Asmblr.hs

311 lines
12 KiB
Haskell

-- (C) Copyright Collin J. Doering 2015
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
-- File: Asmblr.hs
-- Author: Collin J. Doering <collin.doering@rekahsoft.ca>
-- Date: Jun 5, 2015
----------------------------------------------------------------------------
import Text.ParserCombinators.Parsec
import Text.Parsec.Prim (modifyState)
import Text.Parsec.Char (endOfLine)
import System.IO
import System.FilePath (dropExtension)
import System.Console.GetOpt
import System.Environment (getArgs)
import Control.Monad (liftM)
import Numeric (showIntAtBase)
import Data.Char (intToDigit)
import qualified Data.Map as Map
----------------------------------------------------------------------------
type Label = String
type SymbolTable = Map.Map Label Int
data Instruction = AInstr Int
| CInstr String String String
deriving (Show, Read, Eq)
----------------------------------------------------------------------------
aInstrAddr :: GenParser Char st Instruction
aInstrAddr = AInstr . read <$> many1 digit
aInstrSym :: GenParser Char (Int, SymbolTable) Instruction
aInstrSym = do
var <- symbol
(curMem, symTbl) <- getState
case Map.lookup var symTbl of
Nothing -> do
setState (curMem + 1, Map.insert var curMem symTbl)
return $ AInstr curMem
Just varCurMem -> return $ AInstr varCurMem
aInstr :: GenParser Char (Int, SymbolTable) Instruction
aInstr = do
char '@'
aInstrAddr <|> aInstrSym
cInstrDest :: GenParser Char st (Label, String)
cInstrDest = choice [ try (string "AMD") >> return ("AMD", "111")
, try (string "AD") >> return ("AD", "110")
, try (string "AM") >> return ("AM", "101")
, try (string "MD") >> return ("MD", "011")
, char 'A' >> return ("A", "100")
, char 'D' >> return ("D", "010")
, char 'M' >> return ("M", "001") ]
cInstrJump :: GenParser Char st (Label, String)
cInstrJump = char 'J' >>
choice [ string "MP" >> return ("JMP", "111")
, try (string "LE") >> return ("JLE", "110")
, string "LT" >> return ("JLT", "100")
, string "NE" >> return ("JNE", "101")
, string "EQ" >> return ("JEQ", "010")
, try (string "GE") >> return ("JGE", "011")
, string "GT" >> return ("JGT", "001") ]
cInstrAluOps :: GenParser Char st (Label, String)
cInstrAluOps = choice [ char '0' >> return ("0", "0101010")
, char '1' >> return ("1", "0111111")
, try (string "-1") >> return ("-1", "0111010")
, try (string "-D") >> return ("-D", "0001111")
, try (string "-A") >> return ("-A", "0110011")
, string "-M" >> return ("-M", "1110011")
, try (string "D+1") >> return ("D+1", "0011111")
, try (string "D-1") >> return ("D-1", "0001110")
, try (string "D+A") >> return ("D+A", "0000010")
, try (string "D-A") >> return ("D-A", "0010011")
, try (string "D&A") >> return ("D&A", "0000000")
, try (string "D|A") >> return ("D|A", "0010101")
, try (string "D+M") >> return ("D+M", "1000010")
, try (string "D-M") >> return ("D-M", "1010011")
, try (string "D&M") >> return ("D&M", "1000000")
, try (string "D|M") >> return ("D|M", "1010101")
, char 'D' >> return ("D", "0001100")
, try (string "!D") >> return ("!D", "0001101")
, try (string "!A") >> return ("!A", "0110001")
, string "!M" >> return ("!M", "1110001")
, try (string "A+1") >> return ("A+1", "0110111")
, try (string "A-1") >> return ("A-1", "0110010")
, try (string "A-D") >> return ("A-D", "0000111")
, char 'A' >> return ("A", "0110000")
, try (string "M+1") >> return ("M+1", "1110111")
, try (string "M-1") >> return ("M-1", "1110010")
, try (string "M-D") >> return ("M-D", "1000111")
, char 'M' >> return ("M", "1110000") ]
cInstrNoJump :: GenParser Char st Instruction
cInstrNoJump = do
(_, dest) <- cInstrDest
char '='
(_, aluOp) <- cInstrAluOps
return $ CInstr aluOp dest "000"
cInstrNoDest :: GenParser Char st Instruction
cInstrNoDest = do
(_, aluOp) <- cInstrAluOps
char ';'
(_, jump) <- cInstrJump
return $ CInstr aluOp "000" jump
cInstrDestJump :: GenParser Char st Instruction
cInstrDestJump = do
(_, dest) <- cInstrDest
char '='
(_, aluOp) <- cInstrAluOps
char ';'
(_, jump) <- cInstrJump
return $ CInstr aluOp dest jump
cInstr :: GenParser Char st Instruction
cInstr = try cInstrDestJump <|> try cInstrNoDest <|> cInstrNoJump
instr :: GenParser Char (Int, SymbolTable) Instruction
instr = aInstr <|> cInstr
symbol :: GenParser Char st String
symbol = many1 (alphaNum <|> oneOf "_.$:")
comment :: GenParser Char st String
comment = do
string "//" >> manyTill anyChar (lookAhead endOfLine)
return ""
labelLine :: GenParser Char (a, Map.Map Label a) String
labelLine= do
lbl <- between (char '(') (char ')') symbol
optional lineSpaces
optional comment
(lineNum, symTbl) <- getState
case Map.lookup lbl symTbl of
Nothing -> setState (lineNum, Map.insert lbl lineNum symTbl)
Just _ -> error $ "Aleady used label \"" ++ lbl ++ "\""
return ""
instrLine :: GenParser Char (Int, a) String
instrLine = do
ret <- aInstr' <|> cInstr'
optional lineSpaces
optional comment
modifyState $ \(l, tbl) -> (l + 1, tbl)
return ret
where aInstr' = do
char '@'
str <- many1 digit <|> symbol
return $ "@" ++ str
cInstr' = do
dest <- optionMaybe $ try $ do
(d, _) <- cInstrDest
char '='
return d
(op, _) <- cInstrAluOps
jump <- optionMaybe $ try $ do
char ';'
(j, _) <- cInstrJump
return j
case (dest, jump) of
(Nothing, Nothing) -> error "Must specify either dest or jump"
(Just dest', Nothing) -> return $ dest' ++ "=" ++ op
(Nothing, Just jump') -> return $ op ++ ";" ++ jump'
(Just dest', Just jump') -> return $ dest' ++ "=" ++ op ++ ";" ++ jump'
emptyLine :: GenParser Char st String
emptyLine = manyTill space (lookAhead endOfLine)
lineSpaces :: GenParser Char st String
lineSpaces = many $ oneOf " \t"
firstPass :: GenParser Char (Int, SymbolTable) (String, SymbolTable)
firstPass = do
str <- liftM (unlines . filter (not . null)) $ (flip sepEndBy) endOfLine $ do
optional lineSpaces
comment <|> emptyLine <|> labelLine <|> instrLine
(_, symTbl) <- getState
return (str, symTbl)
secondPass :: GenParser Char (Int, SymbolTable) [Instruction]
secondPass = sepEndBy instr endOfLine
parseHackAsm :: Monad m => String -> m String
parseHackAsm str = case runParser firstPass (0, varSymbols) "" str of
Left err -> return $ show err
Right (str', symTbl) -> case runParser secondPass (16, symTbl) "" str' of
Left err -> return $ show err
Right out -> return $ genHackML out
where varSymbols = Map.fromList [ ("R0", 0), ("SP", 0)
, ("R1", 1), ("LCL", 1)
, ("R2", 2), ("ARG", 2)
, ("R3", 3), ("THIS", 3)
, ("R4", 4), ("THAT", 4)
, ("R5", 5)
, ("R6", 6)
, ("R7", 7)
, ("R8", 8)
, ("R9", 9)
, ("R10", 10)
, ("R11", 11)
, ("R12", 12)
, ("R13", 13)
, ("R14", 14)
, ("R15", 15)
, ("SCREEN", 16384)
, ("KBD", 24576) ]
genHackML :: [Instruction] -> String
genHackML xs = unlines $ map instrToML xs
where instrToML (AInstr n) = leftPad 16 '0' $ showIntAtBase 2 intToDigit n ""
instrToML (CInstr op dest jump) = "111" ++ op ++ dest ++ jump
leftPad n a ys = replicate (n - (length ys)) a ++ ys
----------------------------------------------------------------------------
data Flag = Verbose
| Version
| Help
| Output (Maybe String)
deriving (Eq, Show)
options :: [OptDescr Flag]
options = [ Option ['v'] ["verbose"] (NoArg Verbose) "chatty output on stderr"
, Option ['V'] ["version"] (NoArg Version) "show version number"
, Option ['h'] ["help"] (NoArg Help) "show program usage"
, Option ['o'] ["output"] (OptArg Output "FILE") "output file or '-' for stdout" ]
progVersion :: String
progVersion = unlines [ "Assmblr 1.0"
, "Copyright (C) 2014 RekahSoft, Ltd."
, "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>."
, "This is free software: you are free to change and redistribute it."
, "There is NO WARRANTY, to the extent permitted by law."
, ""
, "Written by Collin J. Doering." ]
-- TODO:
-- * handle if input file dne or is unreadable
-- * handle if output file is unwritable
-- * display parser errors on stdout and do not generate output file
main :: IO ()
main = do
argv <- getArgs
case getOpt Permute options argv of
(o, _, []) | any isVersion o -> putStr progVersion
(o, _, []) | any isHelp o -> putStr $ usageInfo header options
(o, [i], []) -> do
--curDir <- getWorkingDirectory
inFile <- if i == "-"
then return stdin
else openFile i ReadMode
outFile <- case hasOutput o of
Nothing -> if i /= "-"
then openFile (dropExtension i ++ ".hack") WriteMode
else return stdout
Just x | x == "-" -> return stdout
Just x -> openFile x WriteMode
hGetContents inFile >>= parseHackAsm >>= hPutStr outFile
hClose inFile
hClose outFile
(_, _, errs) -> ioError (userError (concat errs ++ usageInfo header options))
where header = "Usage: Asmblr [OPTION...] file"
isVersion Version = True
isVersion _ = False
isHelp Help = True
isHelp _ = False
hasOutput [] = Nothing
hasOutput ((Output (Just x)):_) = Just x
hasOutput (_:xs) = hasOutput xs