{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Proof.Assistant.Agda where
import Control.Concurrent.Async
import Data.ByteString (ByteString)
import Data.Coerce
import System.Directory (getTemporaryDirectory)
import System.FilePath ((</>), (<.>))
import System.Mem
import Telegram.Bot.API (ChatId (..))
import Agda.Interaction.Command
import Agda.Interaction.State
import Agda.TypeChecking.Monad.Base (envCurrentPath)
import Agda.Utils.FileName (absolute)
import Proof.Assistant.Helpers
import Proof.Assistant.Request
import Proof.Assistant.Settings
import Proof.Assistant.State
import qualified Data.ByteString.Char8 as BS8
-- | Call Agda with its state. It will parse and execute command from the 'InterpreterRequest'.
callAgda :: AgdaState -> InterpreterRequest -> IO ByteString
callAgda currentAgdaState@AgdaState{..} InterpreterRequest{..} = do
let InternalInterpreterSettings{..} = internal (settings interpreterState)
asyncApi = withChat interpreterRequestTelegramChatId currentAgdaState $ do
enableAllocationLimit
setAllocationCounter (fromIntegral allocations)
interpretAgda currentAgdaState interpreterRequestMessage
asyncTimer = asyncWait (coerce timeout)
eresult <- race asyncTimer (handleErrorMaybe asyncApi)
case eresult of
Left () -> pure "Time limit exceeded"
Right result -> pure result
-- | Since every chat associated with its own Agda state, we need to modify 'TCEnv'
-- by setting 'envCurrentPath' with an absolute path to the corresponding file.
withChat :: ChatId -> AgdaState -> IO a -> IO a
withChat chatId state action = do
tmpDir <- getTemporaryDirectory
let InternalInterpreterSettings{..} = (internal . settings . interpreterState) state
chatIdToString = show . coerce @ChatId @Integer
sourceFile = tmpDir
</> sourceFilePrefix
<> chatIdToString chatId
<.> sourceFileExtension
absSourceFile <- absolute sourceFile
let modifiedEnv s = s { envCurrentPath = Just absSourceFile }
setEnv state modifiedEnv
action
-- | Parse user input as command and execute in Agda.
interpretAgda :: AgdaState -> ByteString -> IO ByteString
interpretAgda state req = case parseRequest req of
Left err -> pure err
Right (ecmd, request) -> do
let fetchResponse = chooseCommand state ecmd
runAgda state =<< fetchResponse request
-- | Parse command. It could be unknown command or 'AgdaCommand' sub-command with its arguments
-- or even expression to evaluate.
parseRequest :: ByteString -> Either ByteString (Either () AgdaCommand, ByteString)
parseRequest rawCmd = case BS8.words rawSubCommand of
[] -> Left "Empty command"
cmd : _ -> if BS8.isPrefixOf "/" cmd
then case matchSupported cmd of
Nothing -> Left $ "Unknown command: " <> cmd
Just aCmd -> Right (Right aCmd, dropCommand rawSubCommand)
else Right (Left (), rawSubCommand)
where
rawSubCommand = dropCommand rawCmd