{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE FlexibleContexts #-}
module Lambdabot.Plugin.Haskell.Djinn (djinnPlugin) where
import Lambdabot.Config.Haskell
import Lambdabot.Logging
import Lambdabot.Plugin
import Lambdabot.Util
import Control.Exception.Lifted as E
import Control.Monad
import Control.Monad.Trans
import Data.Char
import Data.List
import Data.Maybe
import System.Process (readProcess)
import Text.Regex.TDFA
type DjinnEnv = ([Decl] , [Decl])
type Djinn = ModuleT (Maybe DjinnEnv) LB
type Decl = String
djinnPlugin :: Module (Maybe DjinnEnv)
djinnPlugin = newModule
{ moduleSerialize = Nothing
, moduleDefState = return Nothing
, moduleInit = void (djinn [] "")
, moduleCmds = return
[ (command "djinn")
{ help = mapM_ say
[ "djinn <type>."
, "Generates Haskell code from a type."
, "https://github.com/augustss/djinn"
]
, process = rejectingCmds djinnCmd
}
, (command "djinn-add")
{ help = do
say "djinn-add <expr>."
say "Define a new function type or type synonym"
, process = rejectingCmds djinnAddCmd
}
, (command "djinn-del")
{ help = do
say "djinn-del <ident>."
say "Remove a symbol from the environment"
, process = rejectingCmds djinnDelCmd
}
, (command "djinn-env")
{ help = do
say "djinn-env."
say "Show the current djinn environment"
, process = const djinnEnvCmd
}
, (command "djinn-names")
{ help = do
say "djinn-names."
say "Show the current djinn environment, compactly."
, process = const djinnNamesCmd
}
, (command "djinn-clr")
{ help = do
say "djinn-clr."
say "Reset the djinn environment"
, process = const djinnClrCmd
}
, (command "djinn-ver")
{ help = do
say "djinn-ver."
say "Show current djinn version"
, process = const djinnVerCmd
}
]
}
getSavedEnv :: Djinn DjinnEnv
getSavedEnv = withMS $ \st write ->
case st of
Just env -> return env
Nothing -> do
st' <- getDjinnEnv ([],[])
let newMS = (either (const []) snd st', [])
write (Just newMS)
return newMS
getUserEnv :: Djinn [Decl]
getUserEnv = fmap snd getSavedEnv
rejectingCmds :: Monad m => ([Char] -> Cmd m ()) -> [Char] -> Cmd m ()
rejectingCmds action args
| take 1 (dropWhile isSpace args) == ":"
= say "Invalid command"
| otherwise = action args
djinnCmd :: [Char] -> Cmd Djinn ()
djinnCmd s = do
env <- lift getUserEnv
e <- djinn env $ ":set +sorted\nf ? " ++ dropForall s
mapM_ say $ either id (parse . lines) e
where
dropForall t = maybe t mrAfter (t =~~ re)
re = "^forall [[:alnum:][:space:]]+\\."
parse :: [String] -> [String]
parse x = if length x < 2
then ["No output from Djinn; installed?"]
else tail x
djinnAddCmd :: [Char] -> Cmd Djinn ()
djinnAddCmd s = do
(p,st) <- lift getSavedEnv
est <- getDjinnEnv (p, strip isSpace s : st)
case est of
Left e -> say (head e)
Right st' -> writeMS (Just st')
djinnEnvCmd :: Cmd Djinn ()
djinnEnvCmd = do
(prelude,st) <- lift getSavedEnv
mapM_ say $ prelude ++ st
djinnNamesCmd :: Cmd Djinn ()
djinnNamesCmd = do
(prelude,st) <- lift getSavedEnv
let names = concat $ intersperse " " $ concatMap extractNames $ prelude ++ st
say names
where extractNames = filter (isUpper . head) . unfoldr (\x -> case x of _:_ -> listToMaybe (lex x); _ -> Nothing)
djinnClrCmd :: Cmd Djinn ()
djinnClrCmd = writeMS Nothing
djinnDelCmd :: [Char] -> Cmd Djinn ()
djinnDelCmd s = do
(_,env) <- lift getSavedEnv
eenv <- djinn env $ ":delete " ++ strip isSpace s ++ "\n:environment"
case eenv of
Left e -> say (head e)
Right env' -> modifyMS . fmap $ \(prel,_) ->
(prel,filter (`notElem` prel) . nub . lines $ env')
djinnVerCmd :: Cmd Djinn ()
djinnVerCmd = say =<< getDjinnVersion
getDjinnEnv :: (MonadLB m) => DjinnEnv -> m (Either [String] DjinnEnv)
getDjinnEnv (prel,env') = do
env <- djinn env' ":environment"
return (either Left (Right . readEnv) env)
where
readEnv o = let new = filter (\p -> p `notElem` prel) . nub . lines $ o
in (prel, new)
getDjinnVersion :: MonadLB m => m String
getDjinnVersion = do
binary <- getConfig djinnBinary
io (fmap readVersion (readProcess binary [] ":q"))
`E.catch` \SomeException{} ->
return "The djinn command does not appear to be installed."
where
readVersion = extractVersion . unlines . take 1 . lines
extractVersion str = case str =~~ "version [0-9]+(-[0-9]+)*" of
Nothing -> "Unknown"
Just m -> m
djinn :: MonadLB m => [Decl] -> String -> m (Either [String] String)
djinn env src = do
binary <- getConfig djinnBinary
io (tryDjinn binary env src)
`E.catch` \e@SomeException{} -> do
let cmdDesc = case binary of
"djinn" -> ""
_ -> "(" ++ binary ++ ") "
msg = "Djinn command " ++ cmdDesc ++ "failed: " ++ show e
errorM msg
return (Left [msg])
tryDjinn :: String -> [Decl] -> String -> IO (Either [String] String)
tryDjinn binary env src = do
out <- readProcess binary [] (unlines (env ++ [src, ":q"]))
let safeInit [] = []
safeInit xs = init xs
o = dropFromEnd (== '\n') . clean_ . unlines . safeInit . drop 2 . lines $ out
return $ case () of {_
| o =~ "Cannot parse command" ||
o =~ "cannot be realized" ||
o =~ "^Error:" -> Left (lines o)
| otherwise -> Right o
}
clean_ :: String -> String
clean_ s | Just mr <- s =~~ prompt = mrBefore mr ++ mrAfter mr
| otherwise = s
where
prompt = "(Djinn> *)+"