Skip to content

Commit

Permalink
Add a timeout to LSP's parseAndInfer (#52)
Browse files Browse the repository at this point in the history
This PR adds a timeout to the LSP so that if parsing or type checking
hangs/takes too long, the editor doesn't break too. A timeout results in
an error diagnostic, so that if the user modifies the script, the LSP
can process the new version:

<img width="346" alt="image"
src="https://github.com/plow-technologies/inferno/assets/10712637/810e9862-cddf-474c-b2e1-b06b65ee0ba5">

(This example takes too long to parse, see #51 )
  • Loading branch information
siddharth-krishna authored Jul 11, 2023
1 parent 77bb919 commit f16a3f6
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 13 deletions.
3 changes: 3 additions & 0 deletions inferno-lsp/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Revision History for inferno-lsp
*Note*: we use https://pvp.haskell.org/ (MAJOR.MAJOR.MINOR.PATCH)

## 0.1.6 -- 2023-07-11
* Add a timeout to parseAndInfer

## 0.1.5 -- 2023-06-26
* Update inferno-vc version

Expand Down
2 changes: 1 addition & 1 deletion inferno-lsp/inferno-lsp.cabal
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cabal-version: >=1.10
name: inferno-lsp
version: 0.1.5
version: 0.1.6
synopsis: LSP for Inferno
description: A language server protocol implementation for the Inferno language
category: IDE,DSL,Scripting
Expand Down
43 changes: 31 additions & 12 deletions inferno-lsp/src/Inferno/LSP/Server.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
Expand Down Expand Up @@ -31,7 +32,7 @@ import qualified Data.Text.Utf16.Rope as Rope
import Data.Time.Clock (UTCTime, getCurrentTime)
import qualified Data.UUID.V4 as UUID.V4
import Inferno.LSP.Completion (completionQueryAt, filterModuleNameCompletionItems, findInPrelude, identifierCompletionItems, mkCompletionItem, rwsCompletionItems)
import Inferno.LSP.ParseInfer (parseAndInfer)
import Inferno.LSP.ParseInfer (errorDiagnostic, parseAndInfer)
import Inferno.Module.Prelude (ModuleMap, preludeNameToTypeMap)
import Inferno.Types.Syntax (Expr, Ident (..), InfernoType)
import Inferno.Types.Type (TCScheme)
Expand Down Expand Up @@ -64,6 +65,7 @@ import Plow.Logging (IOTracer (..), traceWith)
import Plow.Logging.Async (withAsyncHandleTracer)
import Prettyprinter (Pretty)
import System.IO (BufferMode (NoBuffering), hFlush, hSetBuffering, hSetEncoding, stderr, stdin, stdout, utf8)
import System.Timeout (timeout)

-- This is the entry point for launching an LSP server, explicitly passing in handles for input and output
-- the `getIdents` parameter is a handle for input parameters, only used by the frontend.
Expand Down Expand Up @@ -161,15 +163,6 @@ lspOptions =
-- | Helper type to reduce typing
type ParsedResult = Either [J.Diagnostic] (Expr (Pinned VCObjectHash) (), TCScheme, [(J.Range, J.MarkupContent)])

withParseAndInfer :: MonadIO m => ((UUID, UTCTime) -> m ()) -> ((UUID, UTCTime) -> ParsedResult -> m ParsedResult) -> m ParsedResult -> m ParsedResult
withParseAndInfer before after action = do
ts <- liftIO getCurrentTime
uuid <- UUID <$> liftIO UUID.V4.nextRandom

before (uuid, ts)
result <- action
after (uuid, ts) result

data InfernoEnv = InfernoEnv
{ hovers :: TVar (Map (J.NormalizedUri, J.Int32) [(J.Range, J.MarkupContent)]),
tracer :: IOTracer T.Text,
Expand Down Expand Up @@ -212,6 +205,30 @@ sendDiagnostics :: J.NormalizedUri -> J.TextDocumentVersion -> [J.Diagnostic] ->
sendDiagnostics fileUri version diags =
publishDiagnostics 100 fileUri version (partitionBySource diags)

parseAndInferWithTimeout ::
forall m1 m2 c.
(MonadIO m2, MonadThrow m1, Pretty c, Eq c) =>
((UUID, UTCTime) -> IO ()) ->
((UUID, UTCTime) -> ParsedResult -> IO ParsedResult) ->
ModuleMap m1 c ->
[Maybe Ident] ->
T.Text ->
(InfernoType -> Either T.Text ()) ->
m2 ParsedResult
parseAndInferWithTimeout beforeParse afterParse prelude idents doc_txt validateInput = do
ts <- liftIO getCurrentTime
uuid <- UUID <$> liftIO UUID.V4.nextRandom

liftIO $ beforeParse (uuid, ts)
result <- do
-- Timeout parsing and type checking after 10 seconds
let timeLimit = 10
mResult <- liftIO $ timeout (timeLimit * 1_000_000) $ parseAndInfer @m1 @_ @c prelude idents doc_txt validateInput
case mResult of
Nothing -> pure $ Left $ [errorDiagnostic 1 1 1 1 (Just "inferno.lsp") $ "Inferno timed out in " <> T.pack (show timeLimit) <> "s"]
Just res -> pure res
liftIO $ afterParse (uuid, ts) result

-- | Check if we have a handler, and if we create a haskell-lsp handler to pass it as
-- input into the reactor
lspHandlers :: forall m c. (MonadThrow m, Pretty c, Eq c) => ModuleMap m c -> TChan ReactorInput -> Handlers InfernoLspM
Expand All @@ -232,6 +249,8 @@ lspHandlers prelude rin = mapHandlers goReq goNot (handle @m @c prelude)
-- | Where the actual logic resides for handling requests and notifications.
handle :: forall m c. (MonadThrow m, Pretty c, Eq c) => ModuleMap m c -> Handlers InfernoLspM
handle prelude =
-- Note: at some point we should handle CancelReqest and cancel a previous parseAndInfer if a new one superceeds it. E.g.:
-- https://github.com/haskell/haskell-language-server/blob/baf2fecfa1384dd18e869a837ee2768d9bce18bd/ghcide/src/Development/IDE/LSP/LanguageServer.hs#L266
mconcat
[ notificationHandler J.STextDocumentDidOpen $ \msg -> do
InfernoEnv {hovers = hoversTV, getIdents, beforeParse, afterParse, validateInput} <- getInfernoEnv
Expand All @@ -240,7 +259,7 @@ handle prelude =
idents <- liftIO getIdents
trace $ "Processing DidOpenTextDocument for: " ++ show doc_uri
hovers <-
withParseAndInfer (liftIO . beforeParse) (\x y -> liftIO $ afterParse x y) (parseAndInfer @m @_ @c prelude idents doc_txt validateInput) >>= \case
parseAndInferWithTimeout beforeParse afterParse prelude idents doc_txt validateInput >>= \case
Left errs -> do
sendDiagnostics doc_uri (Just 0) errs
pure mempty
Expand Down Expand Up @@ -268,7 +287,7 @@ handle prelude =
trace $ "Processing DidChangeTextDocument for: " ++ show doc_uri ++ " - " ++ show doc_version
idents <- liftIO getIdents
hovers <-
withParseAndInfer (liftIO . beforeParse) (\x y -> liftIO $ afterParse x y) (parseAndInfer @m @_ @c prelude idents txt validateInput) >>= \case
parseAndInferWithTimeout beforeParse afterParse prelude idents txt validateInput >>= \case
Left errs -> do
trace $ "Sending errs: " ++ show errs
sendDiagnostics doc_uri (Just doc_version) errs
Expand Down

0 comments on commit f16a3f6

Please sign in to comment.