Anton Myasnikov d5154dff36
[LIGO-41] Ligo binary integration
Problem: We want to integrate our lsp client with ligo binary to
be able to typecheck declarations as well as extract their scopes.

Solution: Implement a simple client that calls ligo and some helpers
functions to extract scopes from its output.
2020-09-08 18:48:53 +03:00

176 lines
5.9 KiB
Haskell

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE RecordWildCards #-}
-- | Module that handles ligo binary execution.
module Cli.Impl where
import Cli.Json
import Cli.Types
import Control.Exception (Exception (..), IOException, catch, throwIO)
import Control.Lens hiding ((<.>))
import Control.Monad.Catch (MonadThrow (throwM))
import Data.Aeson
import Data.Aeson.Lens
import qualified Data.ByteString.Lazy.Char8 as C8
import Data.Text
import Data.Text.Encoding (encodeUtf8)
import Duplo.Pretty
import System.Exit
import System.IO
import System.Process (readProcessWithExitCode)
----------------------------------------------------------------------------
-- Errors
----------------------------------------------------------------------------
data LigoError
= -- | @ligo@ call unexpectedly failed (returned non-zero exit code).
-- The error contains the error code, stdout and stderr contents.
UnexpectedClientFailure
Int -- ^ Exit code
Text -- ^ stdout
Text -- ^ stderr
| -- Below are the errors which may fail due to some changes in ligo compiller.
-- | Ligo compiller produced a type which we consider is malformed
MalformedType
Text
| -- | Parse error occured during scope parsing.
ScopeParseError
Text
| -- | Scopes from which variables are failed to extract.
VariableExtractError
Value
deriving (Show) via PP LigoError
instance Exception LigoError where
displayException = show . pp
instance Pretty LigoError where
pp = \case
UnexpectedClientFailure errCode output errOutput ->
"ligo binary unexpectedly failed with error code" <+> pp errCode
<+> ".\nStdout:\n" <.> pp output <.> "\nStderr:\n" <.> pp errOutput
MalformedType t ->
"ligo binary produced type which we consider malformed:\n" <.> pp t
ScopeParseError err ->
"ligo binary produced scope which we consider malformed:\n" <.> pp err
VariableExtractError scopes ->
"ligo produced scopes which we consider malformed since we cannot extract variables from it:\n" <.> text (show scopes)
----------------------------------------------------------------------------
-- Execution
----------------------------------------------------------------------------
-- | Call ligo binary.
callLigo
:: LigoClientEnv -> [String] -> IO String
callLigo LigoClientEnv {..} args = do
logDebug "Running: "
readProcessWithExitCode' _lceClientPath args "" >>= \case
(ExitSuccess, output, errOutput) ->
output <$ logOutput output errOutput
(ExitFailure errCode, pack -> output, pack -> errOutput) ->
throwM $ UnexpectedClientFailure errCode output errOutput
-- output <$ logOutput output errOutput
-- | Helper that outputs debug message to stderr immediately.
logDebug :: String -> IO ()
logDebug msg = do
hPutStrLn stderr msg
hFlush stdout
-- | Helper that outputs message to stdout and stderr immediately.
logOutput :: String -> String -> IO ()
logOutput msg err = do
hPutStrLn stdout msg
hFlush stdout
hPutStrLn stderr err
hFlush stderr
-- | Variant of @readProcessWithExitCode@ that prints a better error in case of
-- an exception in the inner @readProcessWithExitCode@ call.
readProcessWithExitCode'
:: FilePath
-> [String]
-> String
-> IO (ExitCode, String, String)
readProcessWithExitCode' fp args inp =
catch
(readProcessWithExitCode fp args inp)
handler
where
handler :: IOException -> IO (ExitCode, String, String)
handler e = do
hPutStrLn stderr errorMsg
throwIO e
errorMsg =
mconcat
[ "ERROR!! There was an error in executing `"
, show fp
, "` program. Is the executable available in PATH ?"
]
----------------------------------------------------------------------------
-- Execution
----------------------------------------------------------------------------
-- | Extract types from a ligo scope resolution file generated by
-- ```
-- ligo get-scope contract --format=json --with-types
-- ```
parseLigoTypesFor
:: FilePath
-> Text
-> IO [(Text, LigoTypeFull)]
parseLigoTypesFor contractPath name = do
output <- C8.readFile contractPath
case eitherDecodeStrict' @Value . encodeUtf8 . pack . C8.unpack $ output of
Left err -> throwM $ ScopeParseError (pack err)
Right scopes -> do
let variables = scopes ^? key "definitions" . key "variables"
case variables of
Nothing -> throwM $ VariableExtractError scopes
Just variables' -> return $ extractLigoTypesFrom name variables'
-- | Get scopes from ligo compiler and extract a list of types associated with some specific variable.
getLigoTypesFor
:: LigoClientEnv
-> FilePath
-> Text
-> IO [(Text, LigoTypeFull)]
getLigoTypesFor env contractPath name = do
output <- callLigo env ["get-scope", contractPath, "--format=json", "--with-types"]
case eitherDecodeStrict' @Value . encodeUtf8 . pack $ output of
Left err -> throwM $ ScopeParseError (pack err)
Right scopes -> do
let variables = scopes ^? key "definitions" . key "variables"
case variables of
Nothing -> throwM $ VariableExtractError scopes
Just variables' -> return $ extractLigoTypesFrom name variables'
-- | Extract a list of types in scopes from aeson @Value@ for some specific declaration.
extractLigoTypesFrom :: Text -> Value -> [(Text, LigoTypeFull)]
extractLigoTypesFrom name context =
let current =
context
^@.. members
<. filteredBy
(key "name"
. _String
. filtered (== name))
. key "t"
. (_JSON :: Prism' Value LigoTypeFull)
in -- TODO: needs research on nested scopes, currently we think that the list is
-- flat, but if it's not, you can simply uncomment code below
-- deeper =
-- context
-- ^. members
-- . members
-- . key "t"
-- . to (f name)
current -- <> deeper