commercialhaskell / stack

The Haskell Tool Stack
http://haskellstack.org
BSD 3-Clause "New" or "Revised" License
3.99k stars 845 forks source link

Improve stack support for HLS #6154

Open fendor opened 1 year ago

fendor commented 1 year ago

Hi!

Setting the stage, Haskell Language Server's support for stack projects is currently lacking. There are many reasons why using HLS with stack is a subpar experience.

I am trying to list the most important issues here, in the hope of better collaboration, so we can improve the developer experience with both HLS and stack.

Finding the compilation options of a filepath

Essentially, HLS is a compiler. As the compiler, it needs to know how to compile a component / file. Stack knows how to compile a file part of a stack project, and we want to extract that information.

The status quo is that we call a variation of stack repl with a special GHC shim that intercepts the ghc options when stack repl tries to invoke ghc --interactive. Then we use these options to compile the project ourselves. This works reasonably well, but has some shortcomings. Most notably, the issues:

Some of these are full-blown blockers and stack users have suffered in the past.

You can work around this using implicit-hie, which is its own can of worms, and not a proper solution for the needs of HLS.

What does HLS need

It is not a requirement for us to keep using this ugly hack of stack repl.

What we fundamentally need is this:

Given a filepath, we need to be able to find the exact compilation options for it (preferably for the whole component it belongs to) in the quickest way possible, with as little work as possible. Additionally, the compilation options must be enough to actually compile the file. Potential configure hooks must have been run before HLS tries to compile the file. In the light of multiple home units coming to cabal and ghc, we would like even a bit more, but I can't specify right now what exactly.

Going forward

To get this stuff sorted out, there are multiple ways. In Cabal, we've added a couple of months/years ago the --enable-build-info flag with writes out build-info.json files containing the build information required to compile a component.

If someone wants to collaborate on this, I'd be happy to give more information, maybe even in a sync call.

fendor commented 1 year ago

@mpilgrem if possible, I would like to know what you think :)

mpilgrem commented 1 year ago

@fendor, I support the objective.

Stack has a command stack ide with subcommands that are provided with the aim of helping IDEs; and a command stack query that aims to output 'general build information' in JSON format. If something can be added to Stack (perhaps a new subcommand of stack ide or an enhancement of stack query?) that helps HLS support Stack users, that is all to the good.

In terms of helping with the implementation of the idea, I am conscious of my own limitations but I would do what I can to help.

To help me understand what is required from Stack, it would help to make the abstract more concrete, with a simple example. For example, if you had a plain vanilla stack new foo project, does a 'filepath' mean something like app/Main.hs or src/Lib.hs?

In this context, what is meant by 'all the exact compilation options'? If you command stack --verbose build --cabal-verbose, you can see the detail of how Stack is commanding Cabal (the library) (with the configure and build commands) and how (in turn) Cabal is commanding GHC. In this simple example, the configure step looks something like (on Windows):

--verbose=2 
--builddir=.stack-work\dist\8a54c84f 
configure 
--with-ghc=C:\Users\mikep\AppData\Local\Programs\stack\x86_64-windows\ghc-9.2.8\bin\ghc-9.2.8.exe 
--with-ghc-pkg=C:\Users\mikep\AppData\Local\Programs\stack\x86_64-windows\ghc-9.2.8\bin\ghc-pkg-9.2.8.exe 
--user 
--package-db=clear 
--package-db=global 
--package-db=C:\sr\snapshots\ee96b439\pkgdb 
--package-db=C:\Users\mikep\Documents\Code\Haskell\foo\.stack-work\install\0216c472\pkgdb 
--libdir=C:\Users\mikep\Documents\Code\Haskell\foo\.stack-work\install\0216c472\lib 
--bindir=C:\Users\mikep\Documents\Code\Haskell\foo\.stack-work\install\0216c472\bin 
--datadir=C:\Users\mikep\Documents\Code\Haskell\foo\.stack-work\install\0216c472\share 
--libexecdir=C:\Users\mikep\Documents\Code\Haskell\foo\.stack-work\install\0216c472\libexec 
--sysconfdir=C:\Users\mikep\Documents\Code\Haskell\foo\.stack-work\install\0216c472\etc 
--docdir=C:\Users\mikep\Documents\Code\Haskell\foo\.stack-work\install\0216c472\doc\foo-0.1.0.0 
--htmldir=C:\Users\mikep\Documents\Code\Haskell\foo\.stack-work\install\0216c472\doc\foo-0.1.0.0 
--haddockdir=C:\Users\mikep\Documents\Code\Haskell\foo\.stack-work\install\0216c472\doc\foo-0.1.0.0 
--dependency=base=base-4.16.4.0 
--extra-include-dirs=C:\Users\mikep\AppData\Local\Programs\stack\x86_64-windows\msys2-20210604\mingw64\include 
--extra-lib-dirs=C:\Users\mikep\AppData\Local\Programs\stack\x86_64-windows\msys2-20210604\mingw64\lib 
--extra-lib-dirs=C:\Users\mikep\AppData\Local\Programs\stack\x86_64-windows\msys2-20210604\mingw64\bin 
--exact-configuration 
--ghc-option=-fhide-source-paths

and the build step looks something like:

--verbose=2
--builddir=.stack-work\dist\8a54c84f 
build 
lib:foo 
exe:foo-exe 
--ghc-options " -fdiagnostics-color=always"

You may already know this, but Stack uses the version of Cabal (the library) that ships with the version of GHC in question, which may limit the availability to Stack of information provided by Cabal's --enable-build-info flag (it seems to me that the flag was first documented in the guide for Cabal 3.8).

hasufell commented 1 year ago

I'm somewhat familiar with stack codebase and don't mind contributing in this effort.

mpilgrem commented 1 year ago

Thinking out loud:

fendor commented 1 year ago

Thank you for your extensive comments!

Let me add some clarifications:

For example, if you had a plain vanilla stack new foo project, does a 'filepath' mean something like app/Main.hs or src/Lib.hs?

Yes, that is exactly correct. When talking about filepaths, I mean haskell source files that are part of a stack project.

In this context, what is meant by 'all the exact compilation options'?

Yes, what you correctly inferred. The problem, for example, with stack build is: it does too much work. It compiles the component/project, even though it doesn't need to, since we are going to rebuild all of that. For big projects, the start-up time would likely explode.

(it seems to me that the flag was first documented in the guide for Cabal 3.8).

That is true, but let's not delay improvements just because some people cannot benefit from it immediately.


I think stack ide and stack query are both fine commands, if we can tweak them to our needs, that'd be great :)

mpilgrem commented 1 year ago

@fendor, does the following assist?

As noted above, recent versions of Cabal (the library) offer the --enable-build-info flag. The Cabal documentation says that the flag causes Cabal to add a file build-info.json to the root of Cabal's 'build' directory.

stack Setup.hs configure --help suggests that the flag relates to the Cabal configure command. So, a Stack user can currently specify it for, say, all targets by adding to Stack's configuration file:

configure-options:
  $targets:
  - --enable-build-info

After a stack build, Cabal's 'build' directory is found at .stack-work/dist/<hash>. The location of that directory (relative to the Stack project-level confiuration file) is provided by stack path --dist-dir. With the above Stack configuration option set, it will include the build-info.json file.

For example, with a 'plain vanilla' stack new foo project (which currently uses Stackage LTS Haskell LTS 21.1, GHC 9.4.6 and Cabal-3.8.1.0), the build-info.json file contains (formatted):

{
  "cabal-lib-version": "3.8.1.0",
  "compiler": {
    "flavour": "ghc",
    "compiler-id": "ghc-9.4.6",
    "path": "C:\\Users\\mikep\\AppData\\Local\\Programs\\stack\\x86_64-windows\\ghc-9.4.6\\bin\\ghc-9.4.6.exe"
  },
  "components": [
    {
      "type": "lib",
      "name": "lib",
      "unit-id": "foo-0.1.0.0-RDnI1UeKkS9SDv3Gmnx7A",
      "compiler-args": [
        "-fbuilding-cabal-package",
        "-O",
        "-outputdir",
        ".stack-work\\dist\\51f21a8f\\build",
        "-odir",
        ".stack-work\\dist\\51f21a8f\\build",
        "-hidir",
        ".stack-work\\dist\\51f21a8f\\build",
        "-stubdir",
        ".stack-work\\dist\\51f21a8f\\build",
        "-i",
        "-i.stack-work\\dist\\51f21a8f\\build",
        "-isrc",
        "-i.stack-work\\dist\\51f21a8f\\build\\autogen",
        "-i.stack-work\\dist\\51f21a8f\\build\\global-autogen",
        "-I.stack-work\\dist\\51f21a8f\\build\\autogen",
        "-I.stack-work\\dist\\51f21a8f\\build\\global-autogen",
        "-I.stack-work\\dist\\51f21a8f\\build",
        "-IC:\\Users\\mikep\\AppData\\Local\\Programs\\stack\\x86_64-windows\\msys2-20230526\\mingw64\\include",
        "-optP-include",
        "-optP.stack-work\\dist\\51f21a8f\\build\\autogen\\cabal_macros.h",
        "-this-unit-id",
        "foo-0.1.0.0-RDnI1UeKkS9SDv3Gmnx7A",
        "-hide-all-packages",
        "-Wmissing-home-modules",
        "-no-user-package-db",
        "-package-db",
        "C:\\sr\\snapshots\\3fe291b3\\pkgdb",
        "-package-db",
        "C:\\Users\\mikep\\Documents\\Code\\Haskell\\foo\\.stack-work\\install\\9bf47987\\pkgdb",
        "-package-id",
        "base-4.17.2.0",
        "-XHaskell2010",
        "-Wall",
        "-Wcompat",
        "-Widentities",
        "-Wincomplete-record-updates",
        "-Wincomplete-uni-patterns",
        "-Wmissing-export-lists",
        "-Wmissing-home-modules",
        "-Wpartial-fields",
        "-Wredundant-constraints"
      ],
      "modules": [
        "Lib",
        "Paths_foo"
      ],
      "src-files": [],
      "hs-src-dirs": [
        "src"
      ],
      "src-dir": "C:\\Users\\mikep\\Documents\\Code\\Haskell\\foo\\",
      "cabal-file": ".\\foo.cabal"
    },
    {
      "type": "exe",
      "name": "exe:foo-exe",
      "unit-id": "foo-0.1.0.0-8kxBowjWnorJiiEogpBLXn-foo-exe",
      "compiler-args": [
        "-fbuilding-cabal-package",
        "-O",
        "-outputdir",
        ".stack-work\\dist\\51f21a8f\\build",
        "-odir",
        ".stack-work\\dist\\51f21a8f\\build",
        "-hidir",
        ".stack-work\\dist\\51f21a8f\\build",
        "-stubdir",
        ".stack-work\\dist\\51f21a8f\\build",
        "-i",
        "-i.stack-work\\dist\\51f21a8f\\build",
        "-iapp",
        "-i.stack-work\\dist\\51f21a8f\\build\\foo-exe\\autogen",
        "-i.stack-work\\dist\\51f21a8f\\build\\global-autogen",
        "-I.stack-work\\dist\\51f21a8f\\build\\foo-exe\\autogen",
        "-I.stack-work\\dist\\51f21a8f\\build\\global-autogen",
        "-I.stack-work\\dist\\51f21a8f\\build",
        "-IC:\\Users\\mikep\\AppData\\Local\\Programs\\stack\\x86_64-windows\\msys2-20230526\\mingw64\\include",
        "-optP-include",
        "-optP.stack-work\\dist\\51f21a8f\\build\\foo-exe\\autogen\\cabal_macros.h",
        "-hide-all-packages",
        "-Wmissing-home-modules",
        "-no-user-package-db",
        "-package-db",
        "C:\\sr\\snapshots\\3fe291b3\\pkgdb",
        "-package-db",
        "C:\\Users\\mikep\\Documents\\Code\\Haskell\\foo\\.stack-work\\install\\9bf47987\\pkgdb",
        "-package-id",
        "base-4.17.2.0",
        "-package-id",
        "foo-0.1.0.0-RDnI1UeKkS9SDv3Gmnx7A",
        "-XHaskell2010",
        "-Wall",
        "-Wcompat",
        "-Widentities",
        "-Wincomplete-record-updates",
        "-Wincomplete-uni-patterns",
        "-Wmissing-export-lists",
        "-Wmissing-home-modules",
        "-Wpartial-fields",
        "-Wredundant-constraints",
        "-threaded",
        "-rtsopts",
        "-with-rtsopts=-N"
      ],
      "modules": [
        "Paths_foo"
      ],
      "src-files": [
        "Main.hs"
      ],
      "hs-src-dirs": [
        "app"
      ],
      "src-dir": "C:\\Users\\mikep\\Documents\\Code\\Haskell\\foo\\",
      "cabal-file": ".\\foo.cabal"
    }
  ]
}

There is, currently, no Stack command-line equivalent of the configuration option above. However, it would be (I think) trivial to add a stack build --enable-build-info flag that was equivalent to setting the configuration file.

fendor commented 1 year ago

I think that works in general...

The only disadvantage is that we can't load a component if one of its dependencies doesn't build due to type errors, I believe. It would be much nicer, if we had something like https://github.com/haskell/cabal/pull/8726 in stack as well.

However, I will play around with it, time to polish https://github.com/fendor/cabal-build-info/

mpilgrem commented 1 year ago

@fendor, I did not follow what was the relevant part of https://github.com/haskell/cabal/pull/8726. The initial post refers to a goal of starting GHCi with more than one package loaded. stack ghci already has a --package option that allows you to load GHCi with an additional package: https://docs.haskellstack.org/en/stable/ghci/#specifying-extra-packages-to-build-or-depend-on

fendor commented 1 year ago

In the process of that PR, the author implemented the concept of a promised-dependency. Ie a dependency that is promised to exist at compile time but not at configure time. Currently, if you configure a package and one of the dependencies is not already installed in the package-db, the build crashes.

The promised-dependency features allows us to configure all components and then load them into ghci immediately, without having to build anything from stack's perspective, speeding up the startup time of HLS immensely. As a nice side effect, it allows us to load a project, even if not all local dependencies typecheck at the moment, since the build cannot fail in that case.

mpilgrem commented 1 year ago

@fendor, thanks. Adding here a link to the blog post on that topic: https://well-typed.com/blog/2023/03/cabal-multi-unit/.