packages feed

futhark-0.28.2: src/Futhark/LSP/Compile.hs

-- | Building blocks for "recompiling" (actually just type-checking)
-- the Futhark program managed by the language server.  The challenge
-- here is that if the program becomes type-invalid, we want to keep
-- the old state around.
module Futhark.LSP.Compile (tryTakeStateFromIORef, tryReCompile) where

import Colog.Core (Severity (Debug, Warning), (<&))
import Control.Monad.IO.Class (MonadIO (liftIO))
import Data.IORef (IORef, readIORef, writeIORef)
import Data.Int (Int32)
import Data.Maybe (fromMaybe, isJust)
import Futhark.Compiler.Program (LoadedProg, lpFilePaths, lpWarnings, noLoadedProg, reloadProg)
import Futhark.LSP.Diagnostic (errorDiagnostics, publishDiagnostics, showUnlocatedErrors, warningDiagnostics)
import Futhark.LSP.State (State (..), updateStaleContent, updateStaleMapping)
import Futhark.LSP.Tool (computeMapping, logWithSeverity, transformVFS)
import Futhark.Util (showText)
import Language.Futhark.Warnings (listWarnings)
import Language.LSP.Protocol.Types
  ( filePathToUri,
    toNormalizedUri,
  )
import Language.LSP.Server (LspT, getVirtualFile, getVirtualFiles)
import Language.LSP.VFS (virtualFileVersion)

-- | Get the current state.  If the given file is not part of the
-- current program, first try to compile the program rooted at that
-- file, unless that already failed for this version of the file.
tryTakeStateFromIORef :: IORef State -> Maybe FilePath -> LspT () IO State
tryTakeStateFromIORef state_mvar Nothing = liftIO $ readIORef state_mvar
tryTakeStateFromIORef state_mvar (Just file_path) = do
  old_state <- liftIO $ readIORef state_mvar
  case stateProgram old_state of
    Just prog
      | file_path `elem` lpFilePaths prog -> pure old_state
      | otherwise -> do
          logWithSeverity Warning <& ("File not part of program: " <> showText file_path)
          logWithSeverity Warning <& ("Program contains: " <> showText (lpFilePaths prog))
          compileUnlessFailed old_state
    Nothing -> compileUnlessFailed old_state
  where
    compileUnlessFailed old_state = do
      attempt <- compileAttempt file_path
      if isJust attempt && attempt == stateFailed old_state
        then pure old_state
        else do
          new_state <- either id id <$> tryCompile old_state file_path noLoadedProg
          liftIO $ writeIORef state_mvar new_state
          pure new_state

-- | Try to (re)-compile, replace old state if successful.
tryReCompile :: IORef State -> Maybe FilePath -> LspT () IO ()
tryReCompile _ Nothing = pure ()
tryReCompile state_mvar (Just file_path) = do
  logWithSeverity Debug <& "(Re)-compiling ..."
  old_state <- liftIO $ readIORef state_mvar
  result <- tryCompile old_state file_path $ getLoadedProg old_state
  case result of
    Left failed_state -> do
      logWithSeverity Debug <& "Failed to (re)-compile, using old state or Nothing"
      logWithSeverity Debug <& "Computing PositionMapping for: " <> showText file_path
      mapping <- computeMapping failed_state (Just file_path)
      liftIO $ writeIORef state_mvar $ updateStaleMapping (Just file_path) mapping failed_state
    Right new_state -> do
      logWithSeverity Debug <& "(Re)-compile successful"
      liftIO $ writeIORef state_mvar new_state

-- | The file and its current version, as recorded in 'stateFailed'.
compileAttempt :: FilePath -> LspT () IO (Maybe (FilePath, Int32))
compileAttempt file_path =
  fmap ((file_path,) . virtualFileVersion)
    <$> getVirtualFile (toNormalizedUri $ filePathToUri file_path)

-- | Try to compile and publish diagnostics on warnings and errors.
-- Returns the new state on success, and on failure the old state
-- with the failed attempt recorded.  Single point where the
-- compilation is done, and shouldn't be exported.
tryCompile :: State -> FilePath -> LoadedProg -> LspT () IO (Either State State)
tryCompile state path old_loaded_prog = do
  logWithSeverity Debug <& "Reloading program from " <> showText path
  vfs <- getVirtualFiles
  res <- liftIO $ reloadProg old_loaded_prog [path] (transformVFS vfs) -- NOTE: vfs only keeps track of current opened files
  diagnosed <-
    publishDiagnostics vfs (stateDiagnosed state) $
      either errorDiagnostics (warningDiagnostics . listWarnings . lpWarnings) res
  case res of
    Right new_loaded_prog -> do
      logWithSeverity Debug <& "Successfully compiled"
      maybe_virtual_file <- getVirtualFile $ toNormalizedUri $ filePathToUri path
      let new_state = case maybe_virtual_file of
            Nothing -> state {stateProgram = Just new_loaded_prog} -- should never happen
            Just virtual_file -> updateStaleContent path virtual_file new_loaded_prog state
      pure $ Right new_state {stateFailed = Nothing, stateDiagnosed = diagnosed}
    Left prog_error -> do
      logWithSeverity Debug <& "Compilation failed"
      showUnlocatedErrors prog_error
      attempt <- compileAttempt path
      pure $ Left state {stateFailed = attempt, stateDiagnosed = diagnosed}

getLoadedProg :: State -> LoadedProg
getLoadedProg state = fromMaybe noLoadedProg (stateProgram state)