Skip to content
This repository has been archived by the owner on Jan 2, 2021. It is now read-only.

Multi Component #522

Merged
merged 17 commits into from
Jun 2, 2020
Merged

Multi Component #522

merged 17 commits into from
Jun 2, 2020

Conversation

mpickering
Copy link
Contributor

In this commit we add support for loading multiple components into one
ghcide session.

The current behaviour is that each component is loaded lazily into the
session. When a file from an unrecognised component is loaded, the
cradle is consulted again to get a new set of options for the new
component. This will cause all the currently loaded files to be
reloaded into a new HscEnv which is shared by all the currently known
components. The result of this is that functions such as go-to
definition work between components if they have been loaded into the
same session but you have to open at least one file from each component
before it will work.

Only minimal changes are needed to the internals to ghcide to make the
file searching logic look in include directories for all currently
loaded components. The main changes are in exe/Main.hs which has been
heavily rewritten to avoid shake indirections. A global map is created
which maps a filepath to the HscEnv which should be used to compile it.
When a new component is created this map is completely refreshed so each
path maps to a new

Which paths belong to a componenent is determined by the targets listed
by the cradle. Therefore it is important that each cradle also lists all
the targets for the cradle. There are some other choices here as well
which are less accurate such as mapping via include directories which
is the aproach that I implemented in haskell-ide-engine.

The commit has been tested so far with cabal and hadrian.

Also deleted the .ghci file which was causing errors during testing and
seemed broken anyway.

alanz added a commit to alanz/haskell-language-server that referenced this pull request Apr 6, 2020
@runeksvendsen
Copy link

I seem to experience an intermittent error using this branch of ghcide in a project of mine, in which a test and an executable depend on the library. The error is that the library in question ( rule-lang) cannot be found:

ghcide: <command line>: cannot satisfy -package rule-lang-0.1.0.0

After outputting the above ghcide hangs (i.e. the executable never exits, but simply hangs after printing the above line).

I made a commit of the code that exhibits the above symptoms here: runeksvendsen/rule-lang@6bdea22.

I'm wondering if there's some non-determinism in the way multiple components are loaded, such that if an executable or test (which depend on the library) is loaded before the library then ghcide will complain that the library can't be found.

Full log output of running ghcide executable :

ghcide version: 0.1.0 (GHC: 8.6.5) (PATH: /Users/runesvendsen/.local/bin/ghcide) (GIT hash: c43a078c829be65d002c6ebcd3b1c9f2f534ca43)
Ghcide setup tester in /Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs.
Report bugs at https://github.com/digital-asset/ghcide/issues

Step 1/6: Finding files to test in /Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs
Found 17 files

Step 2/6: Looking for hie.yaml files that control setup
Found 1 cradle

Step 3/6: Initializing the IDE

Step 4/6: Type checking the files
Shelling out to cabal "/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/src/Absyn.hs"
> rule-lang> initial-build-steps (lib + exe)
> Configuring GHCi with the following packages: rule-lang
> /Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/install/x86_64-osx/94377dc6990ad6b243b760bbfb93aab30063cf768abe840214a74b980aac577e/8.6.5/pkgdb:/Users/runesvendsen/.stack/snapshots/x86_64-osx/94377dc6990ad6b243b760bbfb93aab30063cf768abe840214a74b980aac577e/8.6.5/pkgdb:/Users/runesvendsen/.stack/programs/x86_64-osx/ghc-8.6.5/lib/ghc-8.6.5/package.conf.d
ComponentOptions {componentOptions = ["-i","-odir=/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/odir","-hidir=/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/odir","-hide-all-packages","-XOverloadedStrings","-XStandaloneDeriving","-XDeriveGeneric","-XNoImplicitPrelude","-XFlexibleContexts","-i/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/dist/x86_64-osx/Cabal-2.4.0.1/build","-i/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/src","-i/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/dist/x86_64-osx/Cabal-2.4.0.1/build/autogen","-i/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/dist/x86_64-osx/Cabal-2.4.0.1/build/global-autogen","-stubdir=/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/dist/x86_64-osx/Cabal-2.4.0.1/build","-package-id=aeson-1.4.4.0-8dIyFiOKltcCLD7t2J0OHI","-package-id=base-4.12.0.0","-package-id=containers-0.6.0.1","-package-id=errors-2.3.0-GKWdqDKmaPrGy1XArjac2y","-package-id=hashable-1.2.7.0-2SI038axTEd7AEZJ275kpi","-package-id=megaparsec-7.0.5-9VrJ76Lt7nJ5Iji4PA7A8S","-package-id=parsers-0.12.10-GWrQJtO53B83vKycLKDMVC","-package-id=protolude-0.2.3-6EKDEuuh3ZRFm0XjK4luPw","-package-id=text-1.2.3.1","-package-id=transformers-0.5.6.2","-package-id=unordered-containers-0.2.10.0-LgoTL3wbBEY5bZIDJiyxW4","-Wall","-optP-include","-optP/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/ghci/168fd77d/cabal_macros.h","-ghci-script=/private/var/folders/dm/d2y4x8mx50n__pwvsmpj_j_r0000gn/T/haskell-stack-ghci/fda1367d/ghci-script","-package-db","/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/install/x86_64-osx/94377dc6990ad6b243b760bbfb93aab30063cf768abe840214a74b980aac577e/8.6.5/pkgdb","-package-db","/Users/runesvendsen/.stack/snapshots/x86_64-osx/94377dc6990ad6b243b760bbfb93aab30063cf768abe840214a74b980aac577e/8.6.5/pkgdb","-package-db","/Users/runesvendsen/.stack/programs/x86_64-osx/ghc-8.6.5/lib/ghc-8.6.5/package.conf.d"], componentDependencies = ["rule-lang.cabal","package.yaml","stack.yaml"]}
"Making new HscEnv[main]"
(HscEnvEq 1,fromList [("package.yaml",Just 2020-03-31 08:05:45.386518558 UTC),("rule-lang.cabal",Just 2020-04-07 08:50:32.00053941 UTC),("stack.yaml",Just 2020-03-17 16:08:13.409014676 UTC)])
Shelling out to cabal "/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/test/Spec/Parse.hs"
> Using main module: 1. Package `rule-lang' component rule-lang:test:rule-lang-test with main-is file: /Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/test/Spec.hs
> rule-lang> initial-build-steps (lib + exe)
> The following GHC options are incompatible with GHCi and have not been passed to it: -O2
> Configuring GHCi with the following packages: rule-lang
> /Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/install/x86_64-osx/94377dc6990ad6b243b760bbfb93aab30063cf768abe840214a74b980aac577e/8.6.5/pkgdb:/Users/runesvendsen/.stack/snapshots/x86_64-osx/94377dc6990ad6b243b760bbfb93aab30063cf768abe840214a74b980aac577e/8.6.5/pkgdb:/Users/runesvendsen/.stack/programs/x86_64-osx/ghc-8.6.5/lib/ghc-8.6.5/package.conf.d
ComponentOptions {componentOptions = ["-i","-odir=/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/odir","-hidir=/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/odir","-hide-all-packages","-XOverloadedStrings","-XStandaloneDeriving","-XDeriveGeneric","-XNoImplicitPrelude","-XFlexibleContexts","-i/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/dist/x86_64-osx/Cabal-2.4.0.1/build/rule-lang-test","-i/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/test","-i/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/dist/x86_64-osx/Cabal-2.4.0.1/build/rule-lang-test/autogen","-i/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/dist/x86_64-osx/Cabal-2.4.0.1/build/global-autogen","-i/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/dist/x86_64-osx/Cabal-2.4.0.1/build/rule-lang-test/rule-lang-test-tmp","-stubdir=/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/dist/x86_64-osx/Cabal-2.4.0.1/build","-package-id=base-4.12.0.0","-package-id=hspec-2.7.1-BxmpM3H5M4w4Y2ViaUKRtb","-package-id=hspec-expectations-pretty-diff-0.7.2.4-DNpNgJ1MOyu63xg9pNKuPu","-package-id=hspec-smallcheck-0.5.2-7Iq7U1gcFRNBmJABwPLv5E","-package-id=megaparsec-7.0.5-9VrJ76Lt7nJ5Iji4PA7A8S","-package=rule-lang-0.1.0.0","-package-id=smallcheck-1.1.5-8zXm09BFgY5BYxznod0SEL","-package-id=tasty-1.2.3-hTb5MwkGozC98ZFlavggW","-package-id=tasty-smallcheck-0.8.1-1OLucf7iboX6WjE9zcAsBp","-package-id=text-1.2.3.1","-Wall","-optP-include","-optP/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/ghci/33d2892b/cabal_macros.h","-ghci-script=/private/var/folders/dm/d2y4x8mx50n__pwvsmpj_j_r0000gn/T/haskell-stack-ghci/29833fcc/ghci-script","-package-db","/Users/runesvendsen/Documents/ITU/06BSc/code/RuleLangHs/.stack-work/install/x86_64-osx/94377dc6990ad6b243b760bbfb93aab30063cf768abe840214a74b980aac577e/8.6.5/pkgdb","-package-db","/Users/runesvendsen/.stack/snapshots/x86_64-osx/94377dc6990ad6b243b760bbfb93aab30063cf768abe840214a74b980aac577e/8.6.5/pkgdb","-package-db","/Users/runesvendsen/.stack/programs/x86_64-osx/ghc-8.6.5/lib/ghc-8.6.5/package.conf.d"], componentDependencies = ["rule-lang.cabal","package.yaml","stack.yaml"]}
ghcide: <command line>: cannot satisfy -package rule-lang-0.1.0.0
    (use -v for more information)
^C

@mpickering
Copy link
Contributor Author

@runeksvendsen I tested your branch using cabal and it worked for me (after I fixed the build errors in the library component).

@mpickering
Copy link
Contributor Author

I noticed the logic to ensure that multiple initialisations do not happen concurrently is not currently correct.

@runeksvendsen
Copy link

runeksvendsen commented Apr 7, 2020

I'm no longer able to reproduce the above behaviour. I'm guessing the problem appeared when I edit the library component, so that it doesn't compile, and then go to a file in the executable target, which would result in VS Code hanging with the status "Processing x/y..." since ghcide didn't exit.

Copy link
Collaborator

@pepeiborra pepeiborra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the right thing to do with autogen Cabal modules?
I got an error on the ghcide workspace (using this branch):

Shelling out to cabal "/home/pepe/scratch/ghcide/dist-newstyle/build/x86_64-linux/ghc-8.8.3/ghcide-0.1.0/build/autogen/Paths_ghcide.hs"
ghcide: CradleError ExitSuccess ["Multi Cradle: No prefixes matched","pwd: /home/pepe/scratch/ghcide","filepath: /home/pepe/scratch/ghcide/dist-newstyle/build/x86_64-linux/ghc-8.8.3/ghcide-0.1.0/build/autogen/Paths_ghcide.hs","prefixes:","(\"./src\",Cabal {component = Just \"ghcide:lib:ghcide\"})","(\"./exe\",Cabal {component = Just \"ghcide:exe:ghcide\"})","(\"./test\",Cabal {component = Just \"ghcide:test:ghcide-tests\"})","(\"./test/preprocessor\",Cabal {component = Just \"ghcide:exe:ghcide-test-preprocessor\"})"]

exe/Main.hs Outdated
print opts
res <- fst <$> session (hieYaml, toNormalizedFilePath' cfp, opts)
signalBarrier finished_barrier res
waitBarrier finished_barrier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the thread never signals the barrier, ghcide could lock up in line 382 if this function is being called masked.
Consider using forkFinally or async instead of forkIO

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After further testing I don't think this code works as I intended at all. Need to rework it.

exe/Main.hs Outdated
modifyVar_ hscEnvs (return . Map.adjust (\(h, _) -> (h, [])) hieYaml )
Nothing -> return ()
-- We sort so exact matches come first.
case HM.lookup (toNormalizedFilePath' cfp) v of
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this lookup is also performed in line 358. Did you forget to readVar fileToFlags?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this doesn’t look right to me.

exe/Main.hs Outdated
return res

lock <- newLock
cradle_lock <- newLock
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These locks could use a comment explaining what shared resource they are protecting

exe/Main.hs Outdated
-- We will modify the unitId and DynFlags used for
-- compilation but these are the true source of
-- information.
new_deps = (thisInstalledUnitId df, df, targets, cfp, dep_info) : maybe [] snd oldDeps
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

time to upgrade this 5-tuple to a record?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a good idea 👍

@mpickering
Copy link
Contributor Author

What is the right thing to do with autogen Cabal modules?
I got an error on the ghcide workspace (using this branch):

Shelling out to cabal "/home/pepe/scratch/ghcide/dist-newstyle/build/x86_64-linux/ghc-8.8.3/ghcide-0.1.0/build/autogen/Paths_ghcide.hs"
ghcide: CradleError ExitSuccess ["Multi Cradle: No prefixes matched","pwd: /home/pepe/scratch/ghcide","filepath: /home/pepe/scratch/ghcide/dist-newstyle/build/x86_64-linux/ghc-8.8.3/ghcide-0.1.0/build/autogen/Paths_ghcide.hs","prefixes:","(\"./src\",Cabal {component = Just \"ghcide:lib:ghcide\"})","(\"./exe\",Cabal {component = Just \"ghcide:exe:ghcide\"})","(\"./test\",Cabal {component = Just \"ghcide:test:ghcide-tests\"})","(\"./test/preprocessor\",Cabal {component = Just \"ghcide:exe:ghcide-test-preprocessor\"})"]

How do you trigger that error? Did you open that file in an editor? What I would expect to happen is that if you previously loaded any other file from the component then it should work. Otherwise it is a limitation of the manual multi-cradle that you are manually specifying the directories.

@pepeiborra
Copy link
Collaborator

What is the right thing to do with autogen Cabal modules?
I got an error on the ghcide workspace (using this branch):

Shelling out to cabal "/home/pepe/scratch/ghcide/dist-newstyle/build/x86_64-linux/ghc-8.8.3/ghcide-0.1.0/build/autogen/Paths_ghcide.hs"
ghcide: CradleError ExitSuccess ["Multi Cradle: No prefixes matched","pwd: /home/pepe/scratch/ghcide","filepath: /home/pepe/scratch/ghcide/dist-newstyle/build/x86_64-linux/ghc-8.8.3/ghcide-0.1.0/build/autogen/Paths_ghcide.hs","prefixes:","(\"./src\",Cabal {component = Just \"ghcide:lib:ghcide\"})","(\"./exe\",Cabal {component = Just \"ghcide:exe:ghcide\"})","(\"./test\",Cabal {component = Just \"ghcide:test:ghcide-tests\"})","(\"./test/preprocessor\",Cabal {component = Just \"ghcide:exe:ghcide-test-preprocessor\"})"]

How do you trigger that error? Did you open that file in an editor? What I would expect to happen is that if you previously loaded any other file from the component then it should work. Otherwise it is a limitation of the manual multi-cradle that you are manually specifying the directories.

I did not open that file directly. Was trying to get a hover in exe/Main.hs after having loaded some modules from the library component.

@aherrmann-da
Copy link
Contributor

I tested this in combination with rules_haskell and Bazel. (I'm working on ghcide/hie-bios support for rules_haskell, see tweag/rules_haskell#1262)

Reproduce

  • Checkout https://github.com/tweag/rules_haskell/tree/ghcide-multi (at 3f9395be3d8c119962023236043cb0ae0b39aef4 at the time of writing).
  • Configure it for Nix
    • Linux:
      $ echo "build --host_platform=@rules_haskell//haskell/platforms:linux_x86_64_nixpkgs" >> .bazelrc.local
      
    • MacOS:
      $ echo "build --host_platform=@rules_haskell//haskell/platforms:darwin_x86_64_nixpkgs" >> .bazelrc.local
      
  • Enter a Nix shell
    • Linux:
      $ nix-shell
      
    • MacOS:
      $ nix-shell --arg docTools false
      
  • Then invoke ghcide on different files (first invocation is going to take a while to build ghcide)
    $ ./.ghcide tests/RunTests.hs
    $ ./.ghcide tests/library-deps/TestLib.hs
    $ ./.ghcide tests/binary-indirect-cbits/Main.hs
    
  • For context, hie.yaml looks like this:
    cradle:
      multi:
        - path: "tests"
          config: {cradle: {bios: {program: ".hie-bios"}}}
        - path: "tests/binary-indirect-cbits"
          config: {cradle: {bios: {program: "tests/binary-indirect-cbits/.hie-bios"}}}
        - path: "tests/library-deps"
          config: {cradle: {bios: {program: "tests/library-deps/.hie-bios"}}}
    

Observations

The first two seem to be working fine. Also opening these files in an editor gives expected IDE features, like hover and goto definition. (There's an issue with goto-definition across files where it tries to open files under an absolute path, e.g. /tests/library-deps/sublib/TestSubLib.hs instead of tests/library-deps/sublib/TestSubLib.hs, but AFAIK that's not related to multi-cradle support).

However, the third one freezes after printing the following to the terminal:

Shelling out to cabal "/home/aj/tweag.io/da/bazel-projects/rules_haskell_ghcide_multi/tests/binary-indirect-cbits/Main.hs"
ComponentOptions {componentOptions = ["-package-id","base-4.12.0.0","-ltests_Sdata_Slibourclibrary","-Lbazel-out/k8-fastbuild/bin/_solib_k8","-pgma","bazel-out/host/bin/haskell/cc_wrapper-python","-pgmc","bazel-out/host/bin/haskell/cc_wrapper-python","-pgml","bazel-out/host/bin/haskell/cc_wrapper-python","-pgmP","bazel-out/host/bin/haskell/cc_wrapper-python","-optc-fno-stack-protector","-optP-E","-optP-undef","-optP-traditional","-itests/binary-indirect-cbits/","-ibazel-out/k8-fastbuild/bin/tests/binary-indirect-cbits/","-itests/library-with-cbits/","-ibazel-out/k8-fastbuild/bin/tests/library-with-cbits/","-ibazel-out/k8-fastbuild/bin/tests/library-with-cbits/_hsc/library-with-cbits","-XStandaloneDeriving","-threaded","-DTESTS_TOOLCHAIN_COMPILER_FLAGS","-XNoOverloadedStrings"], componentDependencies = []}
"Making new HscEnv[main]"
(HscEnvEq 1,fromList [])
Shelling out to cabal "bazel-out/k8-fastbuild/bin/tests/library-with-cbits/_hsc/library-with-cbits/AddOne.hs"
ghcide: CradleError ExitSuccess ["Multi Cradle: No prefixes matched","pwd: /home/aj/tweag.io/da/bazel-projects/rules_haskell_ghcide_multi","filepath: /home/aj/.cache/bazel/_bazel_aj/0a879060aafa76d7881430a3c48b06d5/execroot/rules_haskell/bazel-out/k8-fastbuild/bin/tests/library-with-cbits/_hsc/library-with-cbits/AddOne.hs","prefixes:","(\"tests\",Bios {prog = \".hie-bios\", depsProg = Nothing})","(\"tests/binary-indirect-cbits\",Bios {prog = \"tests/binary-indirect-cbits/.hie-bios\", depsProg = Nothing})","(\"tests/library-deps\",Bios {prog = \"tests/library-deps/.hie-bios\", depsProg = Nothing})"]

The trouble seems to be that the third one depends on AddOne.hs, which is generated by rules_haskell from AddOne.hsc. Generated files are placed under a different prefix, here ~/.cache/.../k8-fastbuild/.../tests/library-with-cbits/_hsc, so it seems they are not associated with the same cradle. I can work around this by adding another cradle that points to the same .hie-bios for the generated files.

cradle:
  multi:
    - path: "bazel-bin/tests/library-with-cbits"
      config: {cradle: {bios: {program: "tests/binary-indirect-cbits/.hie-bios"}}}

Fortunately, this works when using the convenience symlink bazel-bin that Bazel generates. However, it is unfortunate that such a workaround is necessary.

Issues

  • I found that ghcide prints Shelling out to cabal which is not accurate in a bios cradle like these here.
  • I don't think ghcide should hang as it does with the generated AddOne.hs described above.
  • The workaround for the generated files is not unbearable, but it is annoying and adding boilerplate. Do you have an idea of how this may be fixed? I assume e.g. Cabal would have similar issues with generated files that end up under dist or similar. (This seems similar to what @pepeiborra is encountering above)

@mpickering
Copy link
Contributor Author

@aherrmann Is there a reason you are using the multi-cradle AND bios cradle together? You should ideally modify your bios script so that given a filepath it returns the set of options for the component which contains that file.

Just so we are clear here, the multi-cradle is intended to patch over the fact that build tools do not in general support querying for a component based on filepaths.

It is also possible your cradle is not listing all the targets for the component it is trying to load. If it doesn't then it will not work with this branch.

Also the specific behaviour that I have implemented about what should/shouldn't happen in the following situations is up for discussion. What I have implemented in this patch are the foundations which mean making these other decision is easier.

  • Which components do we load on initialisation? (Currently none)
  • When do we load new components? (When the file is opened for the first time)
  • What happens when a file does not match any component (There is an error, improving on this is related to implementing support for the none cradle I believe)
  • What happens when a file matches no existing components? (The cradle is consulted again and all existing HscEnvs are replaced with a new "super" HscEnv which combines together all the currently known components)
  • How do we decide which files belong to an already loaded component? (We create all the possible paths by combining the targets and include directories as returned by the cradle)

Ultimately it is not just up to me to decide on these things and to also implement them. The branch is working for my purposes working on GHC and with cabal projects so it will take a wider community effort to bring it to a state where it can be merged.

@mpickering
Copy link
Contributor Author

mpickering commented Apr 8, 2020

@aherrmann-da I looked at your PR a little and it seems the .hie-bios script does not take the filepath into account when working out the correct options for the component. If you can add a mapping from path to component and then make the script return the right options, things should work a lot better. I believe this is how the first iteration of support for bazel that I wrote worked.

You can probably also fix the issue with go-to definition by using absolute paths for the include directories (-i flags), I had the same issue with hadrian and that fixed it. (See #488)

@aherrmann-da
Copy link
Contributor

@aherrmann Is there a reason you are using the multi-cradle AND bios cradle together? You should ideally modify your bios script so that given a filepath it returns the set of options for the component which contains that file.

That's a very useful feature (that the filepath is passed to the bios script). I was not aware that this PR also introduced it. Note, this is not available as of ghcide master (3960533). I've verified this by echoing $@ into a log file in .hie-bios. I didn't see this feature mentioned in this PR's description, did I miss it somewhere?

It is also possible your cradle is not listing all the targets for the component it is trying to load. If it doesn't then it will not work with this branch.

Apart from the issue around the path prefix of generated files the targets are complete. Note, that the additional entry I added for the generated files points to the same .hie-bios script.

@aherrmann-da I looked at your PR a little and it seems the .hie-bios script does not take the filepath into account when working out the correct options for the component. If you can add a mapping from path to component and then make the script return the right options, things should work a lot better.

That PR is targeting ghcide as of master. So, the .hie-bios script does not receive any arguments. The multi component experiments are tracked in a separate branch, see the "reproduce" section in my comment above.

I'll experiment with using the argument to .hie-bios. So far it's looking good, thanks!

If you can add a mapping from path to component and then make the script return the right options, things should work a lot better. I believe this is how the first iteration of support for bazel that I wrote worked.

Yes, it was a builtin cradle (not bios) that, given a source file, queried for the corresponding haskell_* target, then took the corresponding @repl script and plucked the ghci flags out of it. The approach taken in tweag/rules_haskell#1262 is aiming to be more flexible. rules_haskell has since gained the haskell_repl rule, which allows to define a REPL for multiple haskell_* targets. Meaning if you have interdependent targets :lib-a <-- :lib-b <-- :exe then you can load them all into the same ghci session by source, instead of having to compile say :lib-a and :lib-b to load :exe. We can use this for ghcide to load multiple targets into the same session.

However, this means there is no longer a 1:1 mapping from source to REPL target, as a user might have multiple REPL targets (indirectly) including the same source file. That made multi cradle look like an obvious candidate to let the user define this mapping. Defining that mapping in .hie-bios is an alternative option.

@mpickering
Copy link
Contributor Author

That's a very useful feature (that the filepath is passed to the bios script). I was not aware that this PR also introduced it. Note, this is not available as of ghcide master (3960533). I've verified this by echoing $@ into a log file in .hie-bios. I didn't see this feature mentioned in this PR's description, did I miss it somewhere?

All cradles have had this interface since the earliest iteration of hie-bios. ghcide has never supported it and always passed an empty string when querying a cradle to get the options. You are right that I can't see that is is documented in the hie-bios README.

You are right there is an issue about deciding how to map a FilePath to a component, but that is for the author of the build tool to determine, not ghcide.

exe/Main.hs Outdated
setNameCache nc hsc = hsc { hsc_NC = nc }

-- This is the key function which implements multi-component support. All
-- components mapping to the same hie,yaml file are mapped to the same
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

Suggested change
-- components mapping to the same hie,yaml file are mapped to the same
-- components mapping to the same hie.yaml file are mapped to the same

@fendor
Copy link
Collaborator

fendor commented Apr 16, 2020

I am using this branch as my daily driver for about two weeks now. Except for the occasional GHC bug, #529, my expierence has been vastly positive.
Memory usage is good so far, performance is good and even automatic reload if dependencies change works.
My experience is only based on cabal so far.

@domenkozar
Copy link
Contributor

Even if imperfect, it is better than no multi-component support :)

@pepeiborra
Copy link
Collaborator

This needs rebasing.

@mpickering
Copy link
Contributor Author

I have some more fixes on the staging branch and waiting for one PR to be ready.

Copy link
Collaborator

@cocreature cocreature left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all, I’m very sorry for doing such a terrible job at reviewing this in a timely fashion.
Second, fantastic work, thank you so much!
I’ve left a bunch of comments and it looks like this also needs to get rebased on top of the header-only parsing. Ideally, I’d also like to see a small test but if this turns out to be too complex, I could be persuaded to omit this for now.

exe/Main.hs Outdated
import Rules
import RuleTypes
import Data.Either
--import Outputable (pprTraceM, ppr, text)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
--import Outputable (pprTraceM, ppr, text)

exe/Main.hs Outdated
--import Outputable (pprTraceM, ppr, text)
import qualified Crypto.Hash.SHA1 as H
import qualified Data.ByteString.Char8 as B
import Data.ByteString.Base16 (encode)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don’t align imports. It just makes diffs more noisy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, but there are other places where imports are already aligned..

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, the formatting on this code reveals the history of multiple different authors. One day it would be great to run a real formatter over it, since that's the only way to recover from the mess we have now.

exe/Main.hs Outdated
Comment on lines 82 to 84
--import Rules
--import RuleTypes
--
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
--import Rules
--import RuleTypes
--

exe/Main.hs Outdated
Comment on lines 166 to 167
-- results <- runActionSync ide $ use TypeCheck $ toNormalizedFilePath' "src/Development/IDE/Core/Rules.hs"
-- results <- runActionSync ide $ use TypeCheck $ toNormalizedFilePath' "exe/Main.hs"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
-- results <- runActionSync ide $ use TypeCheck $ toNormalizedFilePath' "src/Development/IDE/Core/Rules.hs"
-- results <- runActionSync ide $ use TypeCheck $ toNormalizedFilePath' "exe/Main.hs"

-- Parse DynFlags for the newly discovered component
hscEnv <- emptyHscEnv
(df, targets) <- evalGhcEnv hscEnv $ do
setOptions opts (hsc_dflags hscEnv)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The do seems to be redundant and the indentation is a bit weird.

exe/Main.hs Outdated
modifyVar_ hscEnvs (return . Map.adjust (\(h, _) -> (h, [])) hieYaml )
Nothing -> return ()
-- We sort so exact matches come first.
case HM.lookup (toNormalizedFilePath' cfp) v of
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this doesn’t look right to me.

exe/Main.hs Outdated Show resolved Hide resolved
exe/Main.hs Outdated
finished <- poll as
case finished of
Just {} -> do
as <- async $ getOptions file
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you can get interrupted between creating the async and updating the IORef. Should this be masked?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes good point.

src/Development/IDE/Core/Compile.hs Outdated Show resolved Hide resolved
test/exe/Main.hs Outdated
getWatchedFilesSubscriptionsUntilProgressEnd :: Session [Maybe Value]
getWatchedFilesSubscriptionsUntilProgressEnd = do
msgs <- manyTill (Just <$> message @RegisterCapabilityRequest <|> Nothing <$ anyMessage) (message @WorkDoneProgressEndNotification)
getWatchedFilesSubscriptionsUntil :: forall end . (FromJSON end, Typeable end) => Session [Maybe Value]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How hard is it to setup a test that verifies that multi cradles at least work to some degree and we don’t break them completely? I guess direct-style cradles are a bit annoying to setup correctly but maybe we can make some simple example with two packages A, B and a cabal file and verify that we can first load a file from B check that go to definition to A does not work. Then load a file from A and check that it works?

@mpickering
Copy link
Contributor Author

Thanks for your comments Moritz. There is a rebased version on my fork which also has a few other fixes which are not obvious so I will get that pushed sometime next week. There is one known issue which I am not sure what to do about.

If you have two packages in your project A and C and if A depends on B which depends on C then if you load A and C into a session but not B then you get confusing errors about the same type coming from different packages.

My idea about how to fix this is to try using OneShot mode instead of --make mode, which seems to make sense to me anyway. I am predicting this will fix it as this will bypass the HPT which is why the fake_uid thing is necessary in the first place, to my understanding.

Would you still merge the patch with this issue? It is still an improvement anyway over no multi-component support at all but I am not sure how to detect this situation and fail gracefully.

@cocreature
Copy link
Collaborator

Would you still merge the patch with this issue? It is still an improvement anyway over no multi-component support at all but I am not sure how to detect this situation and fail gracefully.

Yes, that doesn’t seem too bad. Just add it to the troubleshooting section.

@mpickering
Copy link
Contributor Author

ok then the plan is I will update the PR with the other changes from the fork and address your review comments. Then ping you again for a second review and after it is merged I can try the OneShot mode.

@mpickering
Copy link
Contributor Author

@cocreature I have pushed a new version which contains some bugs fixes.

  • Supporting PackageImports properly
  • Creating different interface file directories for different components
  • Many more comments and code clean ups

I think it would benefit from another complete read.

@@ -166,16 +167,20 @@ moduleImportPath (takeDirectory . fromNormalizedFilePath -> pathDir) mn
-- if they are created with the same call to 'newHscEnvEq'.
data HscEnvEq
= HscEnvEq !Unique !HscEnv
[(InstalledUnitId, DynFlags)] -- In memory components for this HscEnv
-- This is only used at the moment for the import dirs in
-- the DynFlags
| GhcVersionMismatch { compileTime :: !Version
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can make this GhcVersionMismatch into a proper diagnostic now I think rather than this special constructor on HscEnvEq. Not sure whether to do that in this patch.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have another PR making changes to HscEnvEq which will need to be reviewed once this is merged, and I am happy to convert GhcVersionMismatch to a proper diagnostic if that is possible at all

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you prefer me to remove it in this patch? Or leave it until later?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is quite big already, I'm offering to take care of it myself once this is merged.

@@ -472,7 +480,7 @@ instance Hashable GhcSessionIO
instance NFData GhcSessionIO
instance Binary GhcSessionIO

newtype GhcSessionFun = GhcSessionFun (FilePath -> Action HscEnvEq)
newtype GhcSessionFun = GhcSessionFun (FilePath -> Action (IdeResult HscEnvEq))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice I made GhcSessionFun return a proper IdeResult so that you can get proper diagnostics for failing cradle loads now.

Comment on lines +1 to +10
cradle:
cabal:
- path: "./src"
component: "ghcide:lib:ghcide"
- path: "./exe"
component: "ghcide:exe:ghcide"
- path: "./test"
component: "ghcide:test:ghcide-tests"
- path: "./test/preprocessor"
component: "ghcide:exe:ghcide-test-preprocessor"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a stack pendant

@mpickering
Copy link
Contributor Author

I think what is happening is this:

  1. A is loaded, which depends on B so all identifiers from A and B are loaded into the NameCache
  2. Because B is not initially in the HPT, the Name which is put into the NameCache has an empty nameSrcSpan field.
  3. Therefore when we attempt to jump to definition into B, nameSrcSpan is empty so we fall back to the HIE file case.
  4. However, there is also not an HIE file for module B as it was only loaded by the TypeCheck rule, which doesn't generate .hi files or .hie files.

I think the way forward here is:

  1. For now, always generate .hie and .hi files for modules.
  2. When hiedb lands, this code path will be rewritten to query the database so the problem will also be avoided.

Thanks to @fendor and @wz1000 for helping me debug it.

@mpickering
Copy link
Contributor Author

Hmm always generating hie files doesn't help the situation.

@cocreature
Copy link
Collaborator

I’m not quite following your explanation:

  1. It’s not A that depends on B. B imports A and we load A first.
  2. If we do it the other way around and load B first, then I thought loading A would recompile B. Does this not refresh the namecache?
  3. By default cabal-install does not generate .hie files afaik so I don’t understand why having a dirty dist-newstyle directory helps. Are you manually enabling .hie files?

@pepeiborra
Copy link
Collaborator

pepeiborra commented Jun 1, 2020

By default cabal-install does not generate .hie files afaik so I don’t understand why having a dirty dist-newstyle directory helps. Are you manually enabling .hie files?

ghcide does not end up customizing the hieDir as it turns out that the flag always has a value, so the .hie files do end up inside the dist-newstyle folder.
https://github.com/digital-asset/ghcide/blob/0c9a0961abbeef851b4117e6408f15a6d46eb1f1/src/Development/IDE/GHC/Compat.hs#L192-L195

A is loaded, which depends on B so all identifiers from A and B are loaded into the NameCache
Because B is not initially in the HPT, the Name which is put into the NameCache has an empty nameSrcSpan field.
Therefore when we attempt to jump to definition into B, nameSrcSpan is empty so we fall back to the HIE file case.
However, there is also not an HIE file for module B as it was only loaded by the TypeCheck rule, which doesn't generate .hi files or .hie files.

I'm flipping A and B mentally in order to agree with what the test does. Why is the nameSrcSpan empty given that both A and B are files of interest, and therefore typechecked in full and not loaded from interface files? Could it be that the cradle for B.hs is using -package A for some reason?

I can reproduce the problem using VSCode. And indeed, a dirty dist-newstyle folder makes the problem go away. Wiping the dist-newstyle folder and reloading brings the problem back. As an experiment I tried to wipe everything except for the .hie files, and the problem came back after reloading, so I don't think this is related to .hie files.

@mpickering
Copy link
Contributor Author

I have worked out why this is happening now and it's nothing to do with hie files as pointed out by Moritz and Pepe. The bug is in some new code I added to make package imports work.

You have to use the DynFlags for the file we are currently compiling to
get the right packages in the package db so that lookupPackage doesn't
always fail.
@mpickering
Copy link
Contributor Author

Pushed some changes which I hope should fix the tests now, and a bonus test as well which loads the components in the other order.

Who would have thought adding a test would actually show up a bug.

@mpickering
Copy link
Contributor Author

It now appears that stack sets an env var which cabal doesn't like. 🤷‍♀️

As usual, stack doesn’t understand Cabal properly and doesn’t seem to
like ** wildcards so I’ve enumerated it manually.
@cocreature
Copy link
Collaborator

Pushed a fix (hopefully) for that.

@mpickering
Copy link
Contributor Author

Thanks Moritz, you beat me to a fix.. my push failed! Did you come up with that list of variablesthrough testing locally or from using your mind?

@cocreature
Copy link
Collaborator

Tested locally, I already had this change from when I was looking at the failing test on the weekend. I just forgot to push it since it failed with cabal as well so I forgot about it.

@mpickering
Copy link
Contributor Author

Thanks, looks like the simple-multi tests pass on CI now.

@cocreature
Copy link
Collaborator

Crap that breaks the plugin tests, let me try to unset the env more locally.

Copy link
Collaborator

@cocreature cocreature left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit: Fantastic work @mpickering!

@cocreature cocreature merged commit 373c406 into haskell:master Jun 2, 2020
aherrmann-da pushed a commit to digital-asset/daml that referenced this pull request Jul 15, 2020
The type changed with multi component support:
haskell/ghcide#522
pepeiborra pushed a commit to pepeiborra/ide that referenced this pull request Dec 29, 2020
* Multi component support

In this commit we add support for loading multiple components into one
ghcide session.

The current behaviour is that each component is loaded lazily into the
session. When a file from an unrecognised component is loaded, the
cradle is consulted again to get a new set of options for the new
component. This will cause all the currently loaded files to be
reloaded into a new HscEnv which is shared by all the currently known
components. The result of this is that functions such as go-to
definition work between components if they have been loaded into the
same session but you have to open at least one file from each component
before it will work.

Only minimal changes are needed to the internals to ghcide to make the
file searching logic look in include directories for all currently
loaded components. The main changes are in exe/Main.hs which has been
heavily rewritten to avoid shake indirections. A global map is created
which maps a filepath to the HscEnv which should be used to compile it.
When a new component is created this map is completely refreshed so each
path maps to a new

Which paths belong to a componenent is determined by the targets listed
by the cradle. Therefore it is important that each cradle also lists all
the targets for the cradle. There are some other choices here as well
which are less accurate such as mapping via include directories  which
is the aproach that I implemented in haskell-ide-engine.

The commit has been tested so far with cabal and hadrian.

Also deleted the .ghci file which was causing errors during testing and
seemed broken anyway.

Co-authored-by: Alan Zimmerman <alan.zimm@gmail.com>
Co-authored-by: fendor <power.walross@gmail.com>

* Final tweaks?

* Fix 8.4 build

* Add multi-component test

* Fix hlint

* Add cabal to CI images

* Modify path

* Set PATH in the right place (hopefully)

* Always generate interface files and hie files

* Use correct DynFlags in mkImportDirs

You have to use the DynFlags for the file we are currently compiling to
get the right packages in the package db so that lookupPackage doesn't
always fail.

* Revert "Always generate interface files and hie files"

This reverts commit 820aa241890c4498c566e29b0823a803fb2fd297.

* remove traces

* Another test

* lint

* Unset env vars set my stack

* Fix extra-source-files

As usual, stack doesn’t understand Cabal properly and doesn’t seem to
like ** wildcards so I’ve enumerated it manually.

* Unset env locally

Co-authored-by: Alan Zimmerman <alan.zimm@gmail.com>
Co-authored-by: fendor <power.walross@gmail.com>
Co-authored-by: Moritz Kiefer <moritz.kiefer@purelyfunctional.org>
pepeiborra pushed a commit to pepeiborra/ide that referenced this pull request Dec 29, 2020
* Multi component support

In this commit we add support for loading multiple components into one
ghcide session.

The current behaviour is that each component is loaded lazily into the
session. When a file from an unrecognised component is loaded, the
cradle is consulted again to get a new set of options for the new
component. This will cause all the currently loaded files to be
reloaded into a new HscEnv which is shared by all the currently known
components. The result of this is that functions such as go-to
definition work between components if they have been loaded into the
same session but you have to open at least one file from each component
before it will work.

Only minimal changes are needed to the internals to ghcide to make the
file searching logic look in include directories for all currently
loaded components. The main changes are in exe/Main.hs which has been
heavily rewritten to avoid shake indirections. A global map is created
which maps a filepath to the HscEnv which should be used to compile it.
When a new component is created this map is completely refreshed so each
path maps to a new

Which paths belong to a componenent is determined by the targets listed
by the cradle. Therefore it is important that each cradle also lists all
the targets for the cradle. There are some other choices here as well
which are less accurate such as mapping via include directories  which
is the aproach that I implemented in haskell-ide-engine.

The commit has been tested so far with cabal and hadrian.

Also deleted the .ghci file which was causing errors during testing and
seemed broken anyway.

Co-authored-by: Alan Zimmerman <alan.zimm@gmail.com>
Co-authored-by: fendor <power.walross@gmail.com>

* Final tweaks?

* Fix 8.4 build

* Add multi-component test

* Fix hlint

* Add cabal to CI images

* Modify path

* Set PATH in the right place (hopefully)

* Always generate interface files and hie files

* Use correct DynFlags in mkImportDirs

You have to use the DynFlags for the file we are currently compiling to
get the right packages in the package db so that lookupPackage doesn't
always fail.

* Revert "Always generate interface files and hie files"

This reverts commit 820aa241890c4498c566e29b0823a803fb2fd297.

* remove traces

* Another test

* lint

* Unset env vars set my stack

* Fix extra-source-files

As usual, stack doesn’t understand Cabal properly and doesn’t seem to
like ** wildcards so I’ve enumerated it manually.

* Unset env locally

Co-authored-by: Alan Zimmerman <alan.zimm@gmail.com>
Co-authored-by: fendor <power.walross@gmail.com>
Co-authored-by: Moritz Kiefer <moritz.kiefer@purelyfunctional.org>
pepeiborra pushed a commit to pepeiborra/ide that referenced this pull request Dec 29, 2020
* Multi component support

In this commit we add support for loading multiple components into one
ghcide session.

The current behaviour is that each component is loaded lazily into the
session. When a file from an unrecognised component is loaded, the
cradle is consulted again to get a new set of options for the new
component. This will cause all the currently loaded files to be
reloaded into a new HscEnv which is shared by all the currently known
components. The result of this is that functions such as go-to
definition work between components if they have been loaded into the
same session but you have to open at least one file from each component
before it will work.

Only minimal changes are needed to the internals to ghcide to make the
file searching logic look in include directories for all currently
loaded components. The main changes are in exe/Main.hs which has been
heavily rewritten to avoid shake indirections. A global map is created
which maps a filepath to the HscEnv which should be used to compile it.
When a new component is created this map is completely refreshed so each
path maps to a new

Which paths belong to a componenent is determined by the targets listed
by the cradle. Therefore it is important that each cradle also lists all
the targets for the cradle. There are some other choices here as well
which are less accurate such as mapping via include directories  which
is the aproach that I implemented in haskell-ide-engine.

The commit has been tested so far with cabal and hadrian.

Also deleted the .ghci file which was causing errors during testing and
seemed broken anyway.

Co-authored-by: Alan Zimmerman <alan.zimm@gmail.com>
Co-authored-by: fendor <power.walross@gmail.com>

* Final tweaks?

* Fix 8.4 build

* Add multi-component test

* Fix hlint

* Add cabal to CI images

* Modify path

* Set PATH in the right place (hopefully)

* Always generate interface files and hie files

* Use correct DynFlags in mkImportDirs

You have to use the DynFlags for the file we are currently compiling to
get the right packages in the package db so that lookupPackage doesn't
always fail.

* Revert "Always generate interface files and hie files"

This reverts commit 820aa241890c4498c566e29b0823a803fb2fd297.

* remove traces

* Another test

* lint

* Unset env vars set my stack

* Fix extra-source-files

As usual, stack doesn’t understand Cabal properly and doesn’t seem to
like ** wildcards so I’ve enumerated it manually.

* Unset env locally

Co-authored-by: Alan Zimmerman <alan.zimm@gmail.com>
Co-authored-by: fendor <power.walross@gmail.com>
Co-authored-by: Moritz Kiefer <moritz.kiefer@purelyfunctional.org>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants