hiccup-0.35: Hiccup.hs
module Hiccup (runTcl, mkInterp,runInterp,hiccupTests) where
import Control.Monad.State
import qualified Data.Map as Map
import Control.Arrow
import System.IO
import qualified System.IO.Error as IOE
import Control.Monad.Error
import System.Exit
import Data.IORef
import Data.Char (toLower,toUpper)
import Data.List (intersperse)
import Data.Maybe
import qualified Data.ByteString.Char8 as B
import BSParse (TclWord(..), wrapInterp)
import qualified TclObj as T
import Test.HUnit -- IGNORE
type BString = B.ByteString
data Err = ERet !RetVal | EBreak | EContinue | EDie String deriving (Eq)
instance Error Err where
noMsg = EDie "An error occurred."
strMsg s = EDie s
data TclEnv = TclEnv { vars :: VarMap, procs :: ProcMap, upMap :: Map.Map BString (Int,BString) }
type TclM = ErrorT Err (StateT TclState IO)
type TclProc = [T.TclObj] -> TclM RetVal
type ProcMap = Map.Map BString TclProc
type VarMap = Map.Map BString T.TclObj
type ChanMap = Map.Map BString T.TclChan
type RetVal = T.TclObj -- IGNORE
coreProcs, baseProcs, mathProcs :: ProcMap
coreProcs = Map.fromList . map (B.pack *** id) $
[("proc",procProc),("set",procSet),("upvar",procUpVar),
("uplevel", procUpLevel),("if",procIf),
("while",procWhile),("eval", procEval),("exit",procExit),
("list",procList),("return",procReturn),
("break",procRetv EBreak),("catch",procCatch),
("continue",procRetv EContinue),("eq",procEq),("ne",procNe),
("==",procEql), ("error", procError), ("info", procInfo), ("global", procGlobal)]
baseProcs = Map.fromList . map (B.pack *** id) $
[("puts",procPuts),("gets",procGets),
("lindex",procLindex),("llength",procLlength),
("string", procString), ("append", procAppend),
("source", procSource), ("incr", procIncr),
("open", procOpen), ("close", procClose)]
mathProcs = Map.fromList . map (B.pack *** procMath) $
[("+",(+)), ("*",(*)), ("-",(-)),
("/",div), ("<", toI (<)),("<=",toI (<=)),("!=",toI (/=))]
io :: IO a -> TclM a
io = liftIO
toI :: (Int -> Int -> Bool) -> (Int -> Int -> Int)
toI n a b = if n a b then 1 else 0
baseEnv = TclEnv { vars = Map.empty, procs = procMap, upMap = Map.empty }
where procMap = Map.unions [coreProcs, baseProcs, mathProcs]
data TclState = TclState { tclChans :: IORef ChanMap, tclStack :: [TclEnv] }
data Interpreter = Interpreter (IORef TclState)
mkInterp :: IO Interpreter
mkInterp = do chans <- newIORef baseChans
st <- newIORef (TclState chans [baseEnv])
return (Interpreter st)
baseChans = Map.fromList (map (\x -> (T.chanName x, x)) T.tclStdChans )
runInterp :: BString -> Interpreter -> IO (Either BString BString)
runInterp s = runInterp' (doTcl s)
runInterp' t (Interpreter i) = do
bEnv <- readIORef i
(r,i') <- runStateT (runErrorT (t)) bEnv
writeIORef i i'
return (fixErr r)
where perr (EDie s) = B.pack s
perr (ERet v) = T.asBStr v
perr EBreak = B.pack $ "invoked \"break\" outside of a loop"
perr EContinue = B.pack $ "invoked \"continue\" outside of a loop"
fixErr (Left x) = Left (perr x)
fixErr (Right v) = Right (T.asBStr v)
runTcl v = mkInterp >>= runInterp v
ret = return T.empty
doTcl s = runCmds =<< getParsed s
runCmds = liftM last . mapM runCommand
getParsed str = do p <- T.asParsed str
return $ filter (not . null) p
(.==) bs str = (T.asBStr bs) == B.pack str
{-# INLINE (.==) #-}
doCond :: T.TclObj -> TclM Bool
doCond str = do
p <- getParsed str
case p of
[x] -> runCommand x >>= return . T.asBool
_ -> tclErr "Too many statements in conditional"
putStack s = modify (\v -> v { tclStack = s })
modStack f = getStack >>= putStack . f
withScope :: TclM RetVal -> TclM RetVal
withScope f = do
(o:old) <- getStack
putStack $ (baseEnv { procs = procs o }) : o : old
f `ensure` (modStack tail)
ensure :: TclM RetVal -> TclM () -> TclM RetVal
ensure action p = do
r <- action `catchError` (\e -> p >> throwError e)
p
return r
set :: BString -> T.TclObj -> TclM ()
set str v = do when (B.null str) $ tclErr "Empty varname to set!"
(env:es) <- getStack
case upped str env of
Just (i,s) -> uplevel i (set s v)
Nothing -> putStack ((env { vars = Map.insert str v (vars env) }):es)
upped s e = Map.lookup s (upMap e)
getProc str = getFrame >>= return . Map.lookup str . procs
regProc name pr = modStack (\(x:xs) -> (x { procs = Map.insert name pr (procs x) }):xs) >> ret
evalw :: TclWord -> TclM RetVal
evalw (Word s) = interp s
evalw (NoSub s (Just (p,ps))) = if B.null ps then return $ T.mkTclBStrP s (Right p)
else return $ T.mkTclBStrP s (Left ("bad parse: " ++ show ps))
evalw (NoSub s Nothing) = return $ T.mkTclBStrP s (Left "bad parse")
evalw (Subcommand c) = runCommand c
runCommand :: [TclWord] -> TclM RetVal
runCommand [Subcommand s] = runCommand s
runCommand args = do
(name:evArgs) <- mapM evalw args
proc <- getProc (T.asBStr name)
maybe (tclErr ("invalid command name: " ++ (T.asStr name))) ($! evArgs) proc
procProc, procSet, procPuts, procIf, procWhile, procReturn, procUpLevel :: TclProc
procSet args = case args of
[s1,s2] -> set (T.asBStr s1) s2 >> return s2
[s1] -> varGet (T.asBStr s1)
_ -> argErr "set"
procPuts args = case args of
[s] -> tPutLn stdout s
[a1,str] -> if a1 .== "-nonewline" then tPut stdout str
else do h <- getWritable a1
tPutLn h str
[a1,a2,str] ->do unless (a1 .== "-nonewline") bad
h <- getWritable a2
tPut h str
_ -> bad
where tPut h s = (io . B.hPutStr h . T.asBStr) s >> ret
tPutLn h s = (io . B.hPutStrLn h . T.asBStr) s >> ret
getWritable c = getChan (T.asBStr c) >>= checkWritable . T.chanHandle
bad = argErr "puts"
procGets args = case args of
[ch] -> do h <- getReadable ch
eof <- io (hIsEOF h)
if eof then ret else (io . B.hGetLine) h >>= treturn
[ch,vname] -> do h <- getReadable ch
eof <- io (hIsEOF h)
if eof
then set (T.asBStr vname) (T.empty) >> return (T.mkTclInt (-1))
else do s <- io (B.hGetLine h)
set (T.asBStr vname) (T.mkTclBStr s)
return $ T.mkTclInt (B.length s)
_ -> argErr "gets"
where getReadable c = getChan (T.asBStr c) >>= checkReadable . T.chanHandle
checkReadable c = do r <- io (hIsReadable c)
if r then return c else (tclErr $ "channel wasn't opened for reading")
checkWritable c = do r <- io (hIsWritable c)
if r then return c else (tclErr $ "channel wasn't opened for writing")
getChans = do s <- gets tclChans
(io . readIORef) s
addChan c = do s <- gets tclChans
io (modifyIORef s (Map.insert (T.chanName c) c))
removeChan c = do s <- gets tclChans
io (modifyIORef s (Map.delete (T.chanName c)))
getChan :: BString -> TclM T.TclChan
getChan c = do chans <- getChans
maybe (tclErr ("cannot find channel named " ++ show c)) return (Map.lookup c chans)
procEq args = case args of
[a,b] -> return $ T.fromBool (a == b)
_ -> argErr "eq"
procNe args = case args of
[a,b] -> return $ T.fromBool (a /= b)
_ -> argErr "ne"
argErr s = tclErr ("wrong # of args: " ++ s)
procMath :: (Int -> Int -> Int) -> TclProc
procMath op [s1,s2] = liftM2 op (T.asInt s1) (T.asInt s2) >>= return . T.mkTclInt
procMath _ _ = argErr "math"
procEql [a,b] = case (T.asInt a, T.asInt b) of
(Just ia, Just ib) -> return $ T.fromBool (ia == ib)
_ -> procEq [a,b]
procEql _ = argErr "=="
procEval [s] = doTcl s
procEval x = tclErr $ "Bad eval args: " ++ show x
procOpen args = case args of
[fn] -> do eh <- io (IOE.try (openFile (T.asStr fn) ReadMode))
case eh of
Left e -> if IOE.isDoesNotExistError e
then tclErr $ "could not open " ++ show (T.asStr fn) ++ ": no such file or directory"
else tclErr (show e)
Right h -> do
chan <- io (T.mkChan h)
addChan chan
treturn (T.chanName chan)
_ -> argErr "open"
procClose args = case args of
[ch] -> do h <- getChan (T.asBStr ch)
removeChan h
io (hClose (T.chanHandle h))
ret
_ -> argErr "close"
procSource args = case args of
[s] -> io (B.readFile (T.asStr s)) >>= doTcl
_ -> argErr "source"
procExit args = case args of
[] -> io (exitWith ExitSuccess)
[i] -> do v <- T.asInt i
let ecode = if v == 0 then ExitSuccess else ExitFailure v
io (exitWith ecode)
_ -> argErr "exit"
procCatch args = case args of
[s] -> (doTcl s >> procReturn [T.tclFalse]) `catchError` (return . catchRes)
_ -> argErr "catch"
where catchRes (EDie _) = T.tclTrue
catchRes _ = T.tclFalse
retmod f = \v -> treturn (f `onObj` v)
onObj f o = (f (T.asBStr o))
procString :: TclProc
procString (f:s:args)
| f .== "trim" = treturn (T.trim s)
| f .== "tolower" = retmod (B.map toLower) s
| f .== "toupper" = retmod (B.map toUpper) s
| f .== "length" = return $ T.mkTclInt (B.length `onObj` s)
| f .== "index" = case args of
[i] -> do ind <- T.asInt i
if ind >= (B.length `onObj` s) || ind < 0 then ret else treturn $ B.singleton (B.index (T.asBStr s) ind)
_ -> tclErr $ "Bad args to string index."
| otherwise = tclErr $ "Can't do string action: " ++ show f
procString _ = tclErr $ "Bad string args"
tclErr = throwError . EDie
getFrame = liftM head getStack
procInfo [x]
| x .== "commands" = getFrame >>= procList . toObs . Map.keys . procs
| x .== "vars" = getFrame >>= procList . toObs . Map.keys . vars
| otherwise = tclErr $ "Unknown info command: " ++ show (T.asBStr x)
procInfo _ = argErr "info"
toObs = map T.mkTclBStr
procAppend args = case args of
(v:vx) -> do val <- varGet (T.asBStr v) `catchError` \_ -> ret
procSet [v, oconcat (val:vx)]
_ -> argErr "append"
where oconcat = T.mkTclBStr . B.concat . map T.asBStr
procList :: TclProc
procList a = treturn $ (map (escape . T.asBStr) a) `joinWith` ' '
where escape s = if B.elem ' ' s then B.concat [B.singleton '{', s, B.singleton '}'] else s
procLindex args = case args of
[l] -> return l
[l,i] -> do items <- liftM (map to_s . head) (getParsed l)
ind <- T.asInt i
if ind >= length items then ret else treturn (items !! ind)
_ -> argErr "lindex"
to_s (Word s) = s
to_s (NoSub s _) = s
to_s x = error $ "to_s doesn't understand: " ++ show x
procIncr [vname] = incr vname 1
procIncr [vname,val] = T.asInt val >>= incr vname
procIncr _ = argErr "incr"
incr n i = do v <- varGet bsname
intval <- T.asInt v
let res = (T.mkTclInt (intval + i))
set bsname res
return res
where bsname = T.asBStr n
procLlength args = case args of
[lst] -> if B.null `onObj` lst
then return T.tclFalse
else liftM (T.mkTclInt . length . head) (getParsed lst)
_ -> argErr "llength"
procIf (cond:yes:rest) = do
condVal <- doCond cond
if condVal then doTcl yes
else case rest of
[s,blk] -> if s .== "else" then doTcl blk else tclErr "Invalid If"
(s:r) -> if s .== "elseif" then procIf r else tclErr "Invalid If"
[] -> ret
procIf _ = argErr "if"
procWhile [cond,body] = loop `catchError` herr
where herr EBreak = ret
herr (ERet s) = return s
herr EContinue = loop `catchError` herr
herr x = throwError x
loop = do condVal <- doCond cond
pbody <- getParsed body
if condVal then runCmds pbody >> loop else ret
procWhile _ = argErr "while"
procReturn args = case args of
[s] -> throwError (ERet s)
[] -> throwError (ERet T.empty)
_ -> argErr "return"
procRetv c [] = throwError c
procRetv c _ = argErr $ st c
where st EContinue = "continue"
st EBreak = "break"
st _ = "??"
procError [s] = tclErr (T.asStr s)
procError _ = argErr "error"
procUpLevel args = case args of
[p] -> uplevel 1 (procEval [p])
(si:p) -> T.asInt si >>= \i -> uplevel i (procEval p)
_ -> argErr "uplevel"
getStack = gets tclStack
uplevel i p = do
(curr,new) <- liftM (splitAt i) getStack
putStack new
res <- p
modStack (curr ++)
return res
procUpVar args = case args of
[d,s] -> upvar 1 d s
[si,d,s] -> T.asInt si >>= \i -> upvar i d s
_ -> argErr "upvar"
procGlobal lst@(_:_) = mapM_ inner lst >> ret
where inner g = do lst <- getStack
let len = length lst - 1
upvar len g g
procGlobal _ = argErr "global"
upvar n d s = do (e:es) <- getStack
putStack ((e { upMap = Map.insert (T.asBStr s) (n, (T.asBStr d)) (upMap e) }):es)
ret
type ArgList = [Either BString (BString,BString)]
type ParamList = (Bool,ArgList)
procProc [name,args,body] = do
params <- parseParams args
pbody <- getParsed body
regProc (T.asBStr name) (procRunner params pbody)
procProc x = tclErr $ "proc: Wrong arg count (" ++ show (length x) ++ "): " ++ show (map T.asBStr x)
parseParams :: T.TclObj -> TclM ParamList
parseParams args =
case getParsed args of
Nothing -> tclErr $ "arg parse failed: " ++ show (T.asBStr args)
Just [] -> countRet []
Just [r] -> countRet r
x -> tclErr $ "What the heck is " ++ show x ++ "? " ++ show args
where parseArg (Word s) = Left s
parseArg (NoSub _ (Just ([[Word z, Word n]],_))) = Right (z,n)
parseArg (NoSub _ (Just ([[ Word z, Word n],[]],_))) = Right (z,n) -- FIXME: Whuh?
parseArg x = error $ "parseArg doesn't understand: " ++ show x
countRet l = return (hasArgs l, map parseArg l)
hasArgs pl = (not . null) pl && (to_s (last pl) .== "args")
procRunner :: ParamList -> [[TclWord]] -> [T.TclObj] -> TclM RetVal
procRunner pl body args =
withScope $ do
bindArgs pl args
runCmds body `catchError` herr
where herr (ERet s) = return s
herr (EDie s) = tclErr s
herr EBreak = tclErr "invoked \"break\" outside of a loop"
herr EContinue = tclErr "invoked \"continue\" outside of a loop"
bindArgs (hasArgs, pl) args = do
walkBoth used args
where walkBoth ((Left v):xs) (a:as) = set v a >> walkBoth xs as
walkBoth ((Right (k,_)):xs) (a:as) = set k a >> walkBoth xs as
walkBoth ((Left _):_) [] = badArgs
walkBoth ((Right (k,v)):xs) [] = set k (T.mkTclBStr v) >> walkBoth xs []
walkBoth [] xl = if hasArgs then do procList xl >>= set (B.pack "args")
else when (not (null xl)) badArgs
a2s (Left s) = s
a2s (Right (k,_)) = B.cons '?' (B.snoc k '?')
badArgs = argErr $ "should be " ++ show ((map a2s used) `joinWith` ' ') ++ if hasArgs then " ..." else ""
used = if hasArgs then init pl else pl
joinWith bsl c = B.concat (intersperse (B.singleton c) bsl)
varGet :: BString -> TclM RetVal
varGet name = do env <- getFrame
case upped name env of
Nothing -> maybe (tclErr ("can't read \"$" ++ T.asStr name ++ "\": no such variable"))
return
(Map.lookup name (vars env))
Just (i,n) -> uplevel i (varGet n)
treturn = return . T.mkTclBStr
interp :: BString -> TclM RetVal
interp str = case wrapInterp str of
Left s -> treturn s
Right x -> handle x
where f (Word match) = varGet match
f x = runCommand [x]
handle (b,m,a) = do mid <- f m
let front = B.append b (T.asBStr mid)
interp a >>= \v -> treturn (B.append front (T.asBStr v))
-- # TESTS # --
parseArrRef str = do start <- B.elemIndex '(' str
guard (start /= 0)
guard (B.last str == ')')
let (pre,post) = B.splitAt start str
return (pre, B.tail (B.init post))
run :: TclM RetVal -> Either Err RetVal -> IO Bool
run t v = do chans <- newIORef Map.empty
ret <- liftM fst (runStateT (runErrorT (t)) (TclState chans [baseEnv]))
return (ret == v)
testProcEq = TestList [
"1 eq 1 -> t" ~: (procEq [int 1, int 1]) `is` True
,"1 == 1 -> t" ~: (procEql [int 1, int 1]) `is` True
,"' 1 ' == 1 -> t" ~: procEql [str " 1 ", int 1] `is` True
,"' 1 ' eq 1 -> f" ~: procEq [str " 1 ", int 1] `is` False
,"' 1 ' eq ' 1 ' -> t" ~: procEq [str " 1 ", str " 1 "] `is` True
,"' 1 ' ne '1' -> t" ~: procNe [str " 1 ", str "1"] `is` True
,"'cats' eq 'cats' -> t" ~: procEq [str "cats", str "cats"] `is` True
,"'cats' eq 'caps' -> f" ~: procEq [str "cats", str "caps"] `is` False
,"'cats' ne 'cats' -> t" ~: procNe [str "cats", str "cats"] `is` False
,"'cats' ne 'caps' -> f" ~: procNe [str "cats", str "caps"] `is` True
]
where (?=?) a b = assert (run b (Right a))
is c b = (T.fromBool b) ?=? c
int i = T.mkTclInt i
str s = T.mkTclStr s
testArr = TestList [
"december" `should_be` Nothing
,"dec(mber" `should_be` Nothing
,"dec)mber" `should_be` Nothing
,"(cujo)" `should_be` Nothing
,"de(c)mber" `should_be` Nothing
,"a(1)" ?=> ("a","1")
,"xx(september)" ?=> ("xx","september")
,"arr(3,4,5)" ?=> ("arr","3,4,5")
,"arr()" ?=> ("arr","")
]
where (?=>) a b@(b1,b2) = (a ++ " -> " ++ show b) ~: parseArrRef (B.pack a) ~=? Just (B.pack b1, B.pack b2)
should_be x r = (x ++ " should be " ++ show r) ~: parseArrRef (B.pack x) ~=? r
hiccupTests = TestList [ testProcEq, testArr ]
runUnit = runTestTT hiccupTests
-- # ENDTESTS # --