Skip to content

Commit f804b13

Browse files
pepeiborrampickeringcocreature
authored
Support for interface files (#457)
* Rules for loading interface files * Typechecking with interface files * Add a note in the README about the optimal project setup * Improve support for hs-boot files The branch was failing to load GHC because the module graph was missing edges between a .hs file and its .hs-boot file. This means the .hs-boot file was getting added into the HPT after the .hs file which led to confusing errors about variables being out of scope. The fix is to maintain a map from hs-boot to hs files and then add an edge for this case when calling `transitiveDependencies`. Also tidy up some code in setupEnv which I assume was attempting to fix this but in an incorrect manner. Add the -boot suffix when looking for hi-boot files. For some reason, the `hi` path is not set to the right thing for `hs-boot` files. I don't know why not perhaps it is ok to use an existing `.hi` file in place of an `hs-boot` file. More investigation needed. My experience is that GHC is quite bad a recompilation avoidance for hs-boot files anyway. For example: https://gitlab.haskell.org/ghc/ghc/issues/17434 Add the -boot suffix when writing interface files * Generate .hi and .hie files during type checking * Refactor GetModIface to not retain TypeChecked module in memory This improves memory performance on a cold cache. * Trailing whitespace * Turn debug log messages into diagnostics * Implement "hie" files for ghc-8.6.5 This means that the .hi files patch can also be used with 8.6.5 * Add tests for hover/definition on imported symbols * hlints * Generate .hie files when missing * Fix subtle bug in setDefaultHieDir * Simplify optimal project setup in README * Move interface loading diagnostics behind --test flag Reusing the --test flag for this seems harmless, I cannot justify introducing a new flag * Avoid expensive interface file generation for files of interest * avoid redundant arguments (thanks Moritz K) * qualify a DAML only comment * Skip module source when generating hie file thanks Moritz Kiefer for noting that we don't care for the generated .hie files to embed module sources * runGhcEnv <-> evalGhcEnv * Apply suggestions from code review Thanks Moritz Kiefer Co-Authored-By: Moritz Kiefer <[email protected]> * Add suggested Show instance Co-Authored-By: Matthew Pickering <[email protected]> * Use Control.Exception.Safe This is to avoid accidentally capturing asynchronous exceptions * Rename atomicFileUpdate * Fix a flaky test We have to be careful with module naming in tests to avoid interference of .hi files across tests * Undo formatting of D.IDE.GHC.Util * follow changes in master Co-authored-by: Matthew Pickering <[email protected]> Co-authored-by: Moritz Kiefer <[email protected]>
1 parent 9b6e712 commit f804b13

File tree

21 files changed

+2519
-223
lines changed

21 files changed

+2519
-223
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ The `ghcide` executable mostly relies on [`hie-bios`](https://github.com/mpicker
6464

6565
If you can't get `ghcide` working outside the editor, see [this setup troubleshooting guide](docs/Setup.md). Once you have got `ghcide` working outside the editor, the next step is to pick which editor to integrate with.
6666

67+
### Optimal project setup
68+
69+
`ghcide` has been designed to handle projects with hundreds or thousands of modules. If `ghci` can handle it, then `ghcide` should be able to handle it. The only caveat is that this currently requires GHC >= 8.6, and that the first time a module is loaded in the editor will trigger generation of support files in the background if those do not already exist.
70+
6771
### Using with VS Code
6872

6973
You can install the VSCode extension from the [VSCode

exe/Main.hs

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ main = do
100100
{ optReportProgress = clientSupportsProgress caps
101101
, optShakeProfiling = argsShakeProfiling
102102
, optTesting = argsTesting
103+
, optInterfaceLoadingDiagnostics = argsTesting
103104
}
104105
debouncer <- newAsyncDebouncer
105106
initialise caps (cradleRules >> mainRule >> pluginRules plugins >> action kick)

ghcide.cabal

+5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ library
3030
default-language: Haskell2010
3131
build-depends:
3232
aeson,
33+
array,
3334
async,
3435
base == 4.*,
3536
binary,
@@ -143,6 +144,9 @@ library
143144
Development.IDE.Plugin.CodeAction.RuleTypes
144145
Development.IDE.Plugin.Completions.Logic
145146
Development.IDE.Plugin.Completions.Types
147+
if impl(ghc > 8.7) || flag(ghc-lib)
148+
other-modules:
149+
Development.IDE.GHC.HieAst
146150
ghc-options: -Wall -Wno-name-shadowing
147151

148152
executable ghcide-test-preprocessor
@@ -252,6 +256,7 @@ test-suite ghcide-tests
252256
QuickCheck,
253257
quickcheck-instances,
254258
rope-utf16-splay,
259+
shake,
255260
tasty,
256261
tasty-expected-failure,
257262
tasty-hunit,

src/Development/IDE/Core/Compile.hs

+164-55
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ module Development.IDE.Core.Compile
1717
, addRelativeImport
1818
, mkTcModuleResult
1919
, generateByteCode
20+
, generateAndWriteHieFile
21+
, generateAndWriteHiFile
2022
, loadHieFile
23+
, loadInterface
24+
, loadDepModule
25+
, loadModuleHome
2126
) where
2227

2328
import Development.IDE.Core.RuleTypes
@@ -31,6 +36,7 @@ import Development.IDE.GHC.Util
3136
import qualified GHC.LanguageExtensions.Type as GHC
3237
import Development.IDE.Types.Options
3338
import Development.IDE.Types.Location
39+
import Outputable
3440

3541
#if MIN_GHC_API_VERSION(8,6,0)
3642
import DynamicLoading (initializePlugins)
@@ -46,28 +52,34 @@ import ErrUtils
4652

4753
import Finder
4854
import qualified Development.IDE.GHC.Compat as GHC
55+
import qualified Development.IDE.GHC.Compat as Compat
4956
import GhcMonad
5057
import GhcPlugins as GHC hiding (fst3, (<>))
5158
import qualified HeaderInfo as Hdr
5259
import HscMain (hscInteractive, hscSimplify)
60+
import LoadIface (readIface)
61+
import qualified Maybes
5362
import MkIface
5463
import NameCache
5564
import StringBuffer as SB
56-
import TcRnMonad (tcg_th_coreplugins)
65+
import TcRnMonad (initIfaceLoad, tcg_th_coreplugins)
66+
import TcIface (typecheckIface)
5767
import TidyPgm
5868

69+
import Control.Exception.Safe
5970
import Control.Monad.Extra
6071
import Control.Monad.Except
6172
import Control.Monad.Trans.Except
62-
import Data.Function
63-
import Data.Ord
6473
import qualified Data.Text as T
6574
import Data.IORef
6675
import Data.List.Extra
6776
import Data.Maybe
6877
import Data.Tuple.Extra
6978
import qualified Data.Map.Strict as Map
7079
import System.FilePath
80+
import System.Directory
81+
import System.IO.Extra
82+
import Data.Either.Extra (maybeToEither)
7183

7284

7385
-- | Given a string buffer, return the string (after preprocessing) and the 'ParsedModule'.
@@ -79,7 +91,7 @@ parseModule
7991
-> IO (IdeResult (StringBuffer, ParsedModule))
8092
parseModule IdeOptions{..} env filename mbContents =
8193
fmap (either (, Nothing) id) $
82-
runGhcEnv env $ runExceptT $ do
94+
evalGhcEnv env $ runExceptT $ do
8395
(contents, dflags) <- preprocessor filename mbContents
8496
(diag, modu) <- parseFileContents optPreprocessor dflags filename contents
8597
return (diag, Just (contents, modu))
@@ -97,30 +109,35 @@ computePackageDeps env pkg = do
97109
T.pack $ "unknown package: " ++ show pkg]
98110
Just pkgInfo -> return $ Right $ depends pkgInfo
99111

100-
101-
-- | Typecheck a single module using the supplied dependencies and packages.
102-
typecheckModule
103-
:: IdeDefer
104-
-> HscEnv
105-
-> [TcModuleResult]
106-
-> ParsedModule
107-
-> IO (IdeResult TcModuleResult)
108-
typecheckModule (IdeDefer defer) packageState deps pm =
109-
let demoteIfDefer = if defer then demoteTypeErrorsToWarnings else id
110-
in
111-
fmap (either (, Nothing) (second Just)) $
112-
runGhcEnv packageState $
113-
catchSrcErrors "typecheck" $ do
114-
setupEnv deps
115-
let modSummary = pm_mod_summary pm
116-
dflags = ms_hspp_opts modSummary
117-
modSummary' <- initPlugins modSummary
118-
(warnings, tcm) <- withWarnings "typecheck" $ \tweak ->
119-
GHC.typecheckModule $ enableTopLevelWarnings
120-
$ demoteIfDefer pm{pm_mod_summary = tweak modSummary'}
121-
tcm2 <- mkTcModuleResult tcm
122-
let errorPipeline = unDefer . hideDiag dflags
123-
return (map errorPipeline warnings, tcm2)
112+
typecheckModule :: IdeDefer
113+
-> HscEnv
114+
-> [(ModSummary, (ModIface, Maybe Linkable))]
115+
-> ParsedModule
116+
-> IO (IdeResult (HscEnv, TcModuleResult))
117+
typecheckModule (IdeDefer defer) hsc depsIn pm = do
118+
fmap (either (, Nothing) (second Just) . fmap sequence . sequence) $
119+
runGhcEnv hsc $
120+
catchSrcErrors "typecheck" $ do
121+
-- Currently GetDependencies returns things in topological order so A comes before B if A imports B.
122+
-- We need to reverse this as GHC gets very unhappy otherwise and complains about broken interfaces.
123+
-- Long-term we might just want to change the order returned by GetDependencies
124+
let deps = reverse depsIn
125+
126+
setupFinderCache (map fst deps)
127+
128+
let modSummary = pm_mod_summary pm
129+
dflags = ms_hspp_opts modSummary
130+
131+
mapM_ (uncurry loadDepModule . snd) deps
132+
modSummary' <- initPlugins modSummary
133+
(warnings, tcm) <- withWarnings "typecheck" $ \tweak ->
134+
GHC.typecheckModule $ enableTopLevelWarnings
135+
$ demoteIfDefer pm{pm_mod_summary = tweak modSummary'}
136+
tcm2 <- mkTcModuleResult tcm
137+
let errorPipeline = unDefer . hideDiag dflags
138+
return (map errorPipeline warnings, tcm2)
139+
where
140+
demoteIfDefer = if defer then demoteTypeErrorsToWarnings else id
124141

125142
initPlugins :: GhcMonad m => ModSummary -> m ModSummary
126143
initPlugins modSummary = do
@@ -143,14 +160,14 @@ newtype RunSimplifier = RunSimplifier Bool
143160
compileModule
144161
:: RunSimplifier
145162
-> HscEnv
146-
-> [TcModuleResult]
163+
-> [(ModSummary, HomeModInfo)]
147164
-> TcModuleResult
148165
-> IO (IdeResult (SafeHaskellMode, CgGuts, ModDetails))
149166
compileModule (RunSimplifier simplify) packageState deps tmr =
150167
fmap (either (, Nothing) (second Just)) $
151-
runGhcEnv packageState $
168+
evalGhcEnv packageState $
152169
catchSrcErrors "compile" $ do
153-
setupEnv (deps ++ [tmr])
170+
setupEnv (deps ++ [(tmrModSummary tmr, tmrModInfo tmr)])
154171

155172
let tm = tmrModule tmr
156173
session <- getSession
@@ -170,12 +187,12 @@ compileModule (RunSimplifier simplify) packageState deps tmr =
170187
(guts, details) <- liftIO $ tidyProgram session desugared_guts
171188
return (map snd warnings, (mg_safe_haskell desugar, guts, details))
172189

173-
generateByteCode :: HscEnv -> [TcModuleResult] -> TcModuleResult -> CgGuts -> IO (IdeResult Linkable)
190+
generateByteCode :: HscEnv -> [(ModSummary, HomeModInfo)] -> TcModuleResult -> CgGuts -> IO (IdeResult Linkable)
174191
generateByteCode hscEnv deps tmr guts =
175192
fmap (either (, Nothing) (second Just)) $
176-
runGhcEnv hscEnv $
193+
evalGhcEnv hscEnv $
177194
catchSrcErrors "bytecode" $ do
178-
setupEnv (deps ++ [tmr])
195+
setupEnv (deps ++ [(tmrModSummary tmr, tmrModInfo tmr)])
179196
session <- getSession
180197
(warnings, (_, bytecode, sptEntries)) <- withWarnings "bytecode" $ \tweak ->
181198
#if MIN_GHC_API_VERSION(8,10,0)
@@ -254,19 +271,70 @@ mkTcModuleResult tcm = do
254271
where
255272
(tcGblEnv, details) = tm_internals_ tcm
256273

274+
atomicFileWrite :: FilePath -> (FilePath -> IO a) -> IO ()
275+
atomicFileWrite targetPath write = do
276+
let dir = takeDirectory targetPath
277+
createDirectoryIfMissing True dir
278+
(tempFilePath, cleanUp) <- newTempFileWithin dir
279+
(write tempFilePath >> renameFile tempFilePath targetPath) `onException` cleanUp
280+
281+
generateAndWriteHieFile :: HscEnv -> TypecheckedModule -> IO [FileDiagnostic]
282+
generateAndWriteHieFile hscEnv tcm =
283+
handleGenerationErrors dflags "extended interface generation" $ do
284+
case tm_renamed_source tcm of
285+
Just rnsrc -> do
286+
hf <- runHsc hscEnv $
287+
GHC.mkHieFile mod_summary (fst $ tm_internals_ tcm) rnsrc ""
288+
atomicFileWrite targetPath $ flip GHC.writeHieFile hf
289+
_ ->
290+
return ()
291+
where
292+
dflags = hsc_dflags hscEnv
293+
mod_summary = pm_mod_summary $ tm_parsed_module tcm
294+
mod_location = ms_location mod_summary
295+
targetPath = Compat.ml_hie_file mod_location
296+
297+
generateAndWriteHiFile :: HscEnv -> TcModuleResult -> IO [FileDiagnostic]
298+
generateAndWriteHiFile hscEnv tc =
299+
handleGenerationErrors dflags "interface generation" $ do
300+
atomicFileWrite targetPath $ \fp ->
301+
writeIfaceFile dflags fp modIface
302+
where
303+
modIface = hm_iface $ tmrModInfo tc
304+
modSummary = tmrModSummary tc
305+
targetPath = withBootSuffix $ ml_hi_file $ ms_location $ tmrModSummary tc
306+
withBootSuffix = case ms_hsc_src modSummary of
307+
HsBootFile -> addBootSuffix
308+
_ -> id
309+
dflags = hsc_dflags hscEnv
310+
311+
handleGenerationErrors :: DynFlags -> T.Text -> IO () -> IO [FileDiagnostic]
312+
handleGenerationErrors dflags source action =
313+
action >> return [] `catches`
314+
[ Handler $ return . diagFromGhcException source dflags
315+
, Handler $ return . diagFromString source DsError (noSpan "<internal>")
316+
. (("Error during " ++ T.unpack source) ++) . show @SomeException
317+
]
318+
319+
257320
-- | Setup the environment that GHC needs according to our
258321
-- best understanding (!)
259-
setupEnv :: GhcMonad m => [TcModuleResult] -> m ()
260-
setupEnv tmsIn = do
261-
-- if both a .hs-boot file and a .hs file appear here, we want to make sure that the .hs file
262-
-- takes precedence, so put the .hs-boot file earlier in the list
263-
let isSourceFile = (==HsBootFile) . ms_hsc_src . pm_mod_summary . tm_parsed_module . tmrModule
264-
tms = sortBy (compare `on` Down . isSourceFile) tmsIn
322+
--
323+
-- This involves setting up the finder cache and populating the
324+
-- HPT.
325+
setupEnv :: GhcMonad m => [(ModSummary, HomeModInfo)] -> m ()
326+
setupEnv tms = do
327+
setupFinderCache (map fst tms)
328+
-- load dependent modules, which must be in topological order.
329+
modifySession $ \e ->
330+
foldl' (\e (_, hmi) -> loadModuleHome hmi e) e tms
265331

332+
-- | Initialise the finder cache, dependencies should be topologically
333+
-- sorted.
334+
setupFinderCache :: GhcMonad m => [ModSummary] -> m ()
335+
setupFinderCache mss = do
266336
session <- getSession
267337

268-
let mss = map (pm_mod_summary . tm_parsed_module . tmrModule) tms
269-
270338
-- set the target and module graph in the session
271339
let graph = mkModuleGraph mss
272340
setSession session { hsc_mod_graph = graph }
@@ -285,26 +353,40 @@ setupEnv tmsIn = do
285353
newFinderCacheVar <- liftIO $ newIORef $! newFinderCache
286354
modifySession $ \s -> s { hsc_FC = newFinderCacheVar }
287355

288-
-- load dependent modules, which must be in topological order.
289-
mapM_ loadModuleHome tms
290-
291356

292357
-- | Load a module, quickly. Input doesn't need to be desugared.
293358
-- A module must be loaded before dependent modules can be typechecked.
294359
-- This variant of loadModuleHome will *never* cause recompilation, it just
295360
-- modifies the session.
361+
--
362+
-- The order modules are loaded is important when there are hs-boot files.
363+
-- In particular you should make sure to load the .hs version of a file after the
364+
-- .hs-boot version.
296365
loadModuleHome
297-
:: (GhcMonad m)
298-
=> TcModuleResult
299-
-> m ()
300-
loadModuleHome tmr = modifySession $ \e ->
301-
e { hsc_HPT = addToHpt (hsc_HPT e) mod mod_info }
302-
where
303-
ms = pm_mod_summary . tm_parsed_module . tmrModule $ tmr
304-
mod_info = tmrModInfo tmr
305-
mod = ms_mod_name ms
306-
366+
:: HomeModInfo
367+
-> HscEnv
368+
-> HscEnv
369+
loadModuleHome mod_info e =
370+
e { hsc_HPT = addToHpt (hsc_HPT e) mod_name mod_info }
371+
where
372+
mod_name = moduleName $ mi_module $ hm_iface mod_info
373+
374+
-- | Load module interface.
375+
loadDepModuleIO :: ModIface -> Maybe Linkable -> HscEnv -> IO HscEnv
376+
loadDepModuleIO iface linkable hsc = do
377+
details <- liftIO $ fixIO $ \details -> do
378+
let hsc' = hsc { hsc_HPT = addToHpt (hsc_HPT hsc) mod (HomeModInfo iface details linkable) }
379+
initIfaceLoad hsc' (typecheckIface iface)
380+
let mod_info = HomeModInfo iface details linkable
381+
return $ loadModuleHome mod_info hsc
382+
where
383+
mod = moduleName $ mi_module iface
307384

385+
loadDepModule :: GhcMonad m => ModIface -> Maybe Linkable -> m ()
386+
loadDepModule iface linkable = do
387+
e <- getSession
388+
e' <- liftIO $ loadDepModuleIO iface linkable e
389+
setSession e'
308390

309391
-- | GhcMonad function to chase imports of a module given as a StringBuffer. Returns given module's
310392
-- name and its imports.
@@ -424,3 +506,30 @@ loadHieFile f = do
424506
u <- mkSplitUniqSupply 'a'
425507
let nameCache = initNameCache u []
426508
fmap (GHC.hie_file_result . fst) $ GHC.readHieFile nameCache f
509+
510+
-- | Retuns an up-to-date module interface if available.
511+
-- Assumes file exists.
512+
-- Requires the 'HscEnv' to be set up with dependencies
513+
loadInterface
514+
:: HscEnv
515+
-> ModSummary
516+
-> [HiFileResult]
517+
-> IO (Either String ModIface)
518+
loadInterface session ms deps = do
519+
let hiFile = case ms_hsc_src ms of
520+
HsBootFile -> addBootSuffix (ml_hi_file $ ms_location ms)
521+
_ -> ml_hi_file $ ms_location ms
522+
r <- initIfaceLoad session $ readIface (ms_mod ms) hiFile
523+
case r of
524+
Maybes.Succeeded iface -> do
525+
session' <- foldM (\e d -> loadDepModuleIO (hirModIface d) Nothing e) session deps
526+
(reason, iface') <- checkOldIface session' ms SourceUnmodified (Just iface)
527+
return $ maybeToEither (showReason reason) iface'
528+
Maybes.Failed err -> do
529+
let errMsg = showSDoc (hsc_dflags session) err
530+
return $ Left errMsg
531+
532+
showReason :: RecompileRequired -> String
533+
showReason MustCompile = "Stale"
534+
showReason (RecompBecause reason) = "Stale (" ++ reason ++ ")"
535+
showReason UpToDate = "Up to date"

0 commit comments

Comments
 (0)