[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.
This commit is contained in:
parent
7c43cd9574
commit
d5154dff36
@ -67,6 +67,12 @@ library:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- haskell-lsp-types
|
- haskell-lsp-types
|
||||||
|
- aeson
|
||||||
|
- process
|
||||||
|
- lens
|
||||||
|
- lens-aeson
|
||||||
|
- bytestring
|
||||||
|
- unordered-containers
|
||||||
|
|
||||||
executables:
|
executables:
|
||||||
squirrel:
|
squirrel:
|
||||||
@ -75,6 +81,7 @@ executables:
|
|||||||
- hslogger
|
- hslogger
|
||||||
- interpolate
|
- interpolate
|
||||||
- lens
|
- lens
|
||||||
|
- lens-aeson
|
||||||
- ligo-squirrel
|
- ligo-squirrel
|
||||||
- directory
|
- directory
|
||||||
- unix
|
- unix
|
||||||
|
175
tools/lsp/squirrel/src/Cli/Impl.hs
Normal file
175
tools/lsp/squirrel/src/Cli/Impl.hs
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
{-# 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
|
189
tools/lsp/squirrel/src/Cli/Json.hs
Normal file
189
tools/lsp/squirrel/src/Cli/Json.hs
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
|
-- | The definition of type as is represented in ligo JSON output
|
||||||
|
module Cli.Json
|
||||||
|
( LigoTypeFull (..)
|
||||||
|
, LigoTypeContent (..)
|
||||||
|
, LigoTypeContentInner (..)
|
||||||
|
, LigoRecordField (..)
|
||||||
|
, LigoLocation (..)
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Data.Aeson
|
||||||
|
import Data.Char (isUpper, toLower)
|
||||||
|
import Data.Foldable (asum, toList)
|
||||||
|
import qualified Data.HashMap.Strict as HM
|
||||||
|
import Data.Text (Text)
|
||||||
|
import GHC.Generics
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- Types
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | Whole ligo type.
|
||||||
|
-- ```
|
||||||
|
-- { "t" : LigoTypeFull }
|
||||||
|
-- ```
|
||||||
|
data LigoTypeFull = LigoTypeFull
|
||||||
|
{ -- | Location of the definition.
|
||||||
|
ltLocation :: LigoLocation
|
||||||
|
, -- | *Some* meta constructors (e.g. `Some`).
|
||||||
|
ltTypeMeta :: Value
|
||||||
|
, -- | We parse it by a chunks of 2, each odd element of array is a name for
|
||||||
|
-- even element which is `LigoTypeContentInner`.
|
||||||
|
-- ```
|
||||||
|
-- { "type_content": [ <name>, LigoTypeContentInner ] }
|
||||||
|
-- ```
|
||||||
|
ltTypeContent :: [LigoTypeContent]
|
||||||
|
}
|
||||||
|
deriving stock (Generic, Show)
|
||||||
|
|
||||||
|
-- | A pair in "type_content" array `[name, content]`.
|
||||||
|
-- ```
|
||||||
|
-- { "type_content": LigoTypeContent }
|
||||||
|
-- ```
|
||||||
|
data LigoTypeContent = LigoTypeContent
|
||||||
|
{ ltcName :: Text
|
||||||
|
, ltcContentInner :: LigoTypeContentInner
|
||||||
|
}
|
||||||
|
deriving stock (Generic, Show)
|
||||||
|
|
||||||
|
-- | Inner object representing type content that depends on `name` in `LigoTypeContent`.
|
||||||
|
-- ```
|
||||||
|
-- { "type_content": [ <type>, LigoTypeContentInner ] }
|
||||||
|
-- ```
|
||||||
|
data LigoTypeContentInner
|
||||||
|
= -- | Type call represented by the list of arguments and its constructor.
|
||||||
|
LTCConst
|
||||||
|
{ ltciArguments :: [Text]
|
||||||
|
, ltciTypeConst :: Value
|
||||||
|
}
|
||||||
|
| -- | Record type.
|
||||||
|
LTCRecord (HM.HashMap Text LigoRecordField) -- TODO: continue
|
||||||
|
| -- | Arrow type, note that the order of its arguments is reversed.
|
||||||
|
LTCArrow -- "type2" -> "type1"
|
||||||
|
{ ltcType2 :: LigoTypeFull
|
||||||
|
, ltcType1 :: LigoTypeFull
|
||||||
|
}
|
||||||
|
deriving stock (Generic, Show)
|
||||||
|
|
||||||
|
-- | Record field type value.
|
||||||
|
-- ```
|
||||||
|
-- { "type_content": ["T_record", { "key": LigoRecordField } ] }
|
||||||
|
-- ```
|
||||||
|
data LigoRecordField = LigoRecordField
|
||||||
|
{ -- | Declaration position (don't ask me I too don't know what actual
|
||||||
|
-- position is this since from all the example it's somewhat always 0).
|
||||||
|
lrfDeclPos :: Int
|
||||||
|
, -- | How the value is represented in michelson, currently ignored
|
||||||
|
-- during parsing.
|
||||||
|
lrfMichelsonAnnotation :: Value
|
||||||
|
, -- | The type itself.
|
||||||
|
lrfAssociatedType :: LigoTypeFull
|
||||||
|
}
|
||||||
|
deriving stock (Generic, Show)
|
||||||
|
|
||||||
|
-- | Location of type definition.
|
||||||
|
-- ```
|
||||||
|
-- { "location": LigoLocation }
|
||||||
|
-- ```
|
||||||
|
data LigoLocation
|
||||||
|
= Virtual Text
|
||||||
|
| LigoLocation
|
||||||
|
{ llFile :: FilePath
|
||||||
|
, llFromRow :: Int
|
||||||
|
, llFromCol :: Int
|
||||||
|
, llToRow :: Int
|
||||||
|
, llToCol :: Int
|
||||||
|
}
|
||||||
|
deriving stock (Generic, Show)
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- Instances
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- TODO: We trust ligo compiler output for printing even number
|
||||||
|
-- of array elements.
|
||||||
|
instance FromJSON LigoTypeFull where
|
||||||
|
parseJSON = withObject "type_full" $ \o -> do
|
||||||
|
ltLocation <- o .: "location"
|
||||||
|
type_content <- o .: "type_content"
|
||||||
|
ltTypeContent <-
|
||||||
|
withArray "type_content" (mapM proceed . group 2 . toList) type_content
|
||||||
|
ltTypeMeta <- o .: "type_meta"
|
||||||
|
return $ LigoTypeFull {..}
|
||||||
|
where
|
||||||
|
proceed [name, value] = do
|
||||||
|
ltcName <- parseJSON @Text name
|
||||||
|
ltcContentInner <- parseJSON @LigoTypeContentInner value
|
||||||
|
return $ LigoTypeContent {..}
|
||||||
|
proceed _ = error "number of type content elements is not even and cannot be grouped"
|
||||||
|
|
||||||
|
instance ToJSON LigoTypeFull where
|
||||||
|
toJSON = genericToJSON defaultOptions {fieldLabelModifier = prepareField 2}
|
||||||
|
|
||||||
|
instance FromJSON LigoTypeContent where
|
||||||
|
parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = prepareField 3}
|
||||||
|
|
||||||
|
instance ToJSON LigoTypeContent where
|
||||||
|
toJSON = genericToJSON defaultOptions {fieldLabelModifier = prepareField 3}
|
||||||
|
|
||||||
|
instance FromJSON LigoTypeContentInner where
|
||||||
|
parseJSON = withObject "type_content" $ \o ->
|
||||||
|
asum
|
||||||
|
[ LTCConst <$> o .: "arguments" <*> o .: "type_constant"
|
||||||
|
, LTCRecord <$> sequence (parseJSON @LigoRecordField <$> o)
|
||||||
|
, LTCArrow <$> o .: "type2" <*> o .: "type1"
|
||||||
|
]
|
||||||
|
|
||||||
|
instance ToJSON LigoTypeContentInner where
|
||||||
|
toJSON = genericToJSON defaultOptions {fieldLabelModifier = prepareField 3}
|
||||||
|
|
||||||
|
instance FromJSON LigoRecordField where
|
||||||
|
parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = prepareField 3}
|
||||||
|
|
||||||
|
instance ToJSON LigoRecordField where
|
||||||
|
toJSON = genericToJSON defaultOptions {fieldLabelModifier = prepareField 3}
|
||||||
|
|
||||||
|
instance FromJSON LigoLocation where
|
||||||
|
parseJSON = withObject "location" $ \o ->
|
||||||
|
asum
|
||||||
|
[ Virtual <$> o .: "virtual"
|
||||||
|
, do
|
||||||
|
llFile <- o .: "file"
|
||||||
|
llFromRow <- o .: "from_row"
|
||||||
|
llFromCol <- o .: "from_col"
|
||||||
|
llToRow <- o .: "to_row"
|
||||||
|
llToCol <- o .: "to_col"
|
||||||
|
return $ LigoLocation {..}
|
||||||
|
]
|
||||||
|
|
||||||
|
instance ToJSON LigoLocation where
|
||||||
|
toJSON = genericToJSON defaultOptions {fieldLabelModifier = prepareField 2}
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- Helpers
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | Helper function that converts qualified field to its JSON counterpart.
|
||||||
|
--
|
||||||
|
-- >>> prepareField 2 "llFooBar"
|
||||||
|
-- "foo_bar"
|
||||||
|
prepareField :: Int -> String -> String
|
||||||
|
prepareField dropAmount = Prelude.drop (dropAmount + 1) . concatMap process
|
||||||
|
where
|
||||||
|
process c
|
||||||
|
| isUpper c = "_" <> [toLower c]
|
||||||
|
| otherwise = [c]
|
||||||
|
|
||||||
|
-- | Splits an array onto chunks of n elements, throws error otherwise.
|
||||||
|
--
|
||||||
|
-- >>> group 2 [1, 2, 3, 4]
|
||||||
|
-- [[1,2],[3,4]]
|
||||||
|
group :: Int -> [a] -> [[a]]
|
||||||
|
group _ [] = []
|
||||||
|
group n l
|
||||||
|
| n > 0 = (take n l) : (group n (drop n l))
|
||||||
|
| otherwise = error "Negative or zero n"
|
20
tools/lsp/squirrel/src/Cli/Types.hs
Normal file
20
tools/lsp/squirrel/src/Cli/Types.hs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
-- | All the types needed for cli to work.
|
||||||
|
module Cli.Types
|
||||||
|
( LigoClient
|
||||||
|
, LigoClientEnv (..)
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Control.Monad.Trans.Reader (ReaderT)
|
||||||
|
|
||||||
|
-- | Type of the client itself.
|
||||||
|
type LigoClient = ReaderT LigoClientEnv IO
|
||||||
|
|
||||||
|
-- | Environment passed throughout the ligo interaction
|
||||||
|
data LigoClientEnv = LigoClientEnv
|
||||||
|
{ -- | Ligo binary path
|
||||||
|
_lceClientPath :: FilePath
|
||||||
|
, -- | Whether we need to print logs from ligo
|
||||||
|
_lceVerbose :: Bool
|
||||||
|
}
|
||||||
|
deriving stock (Show)
|
Loading…
Reference in New Issue
Block a user