Compilation time increases quadratically with the number of statements in a do-block
## Summary
`hspec-discover` combines a list of test modules into a single *test driver*, e.g.:
```haskell
import qualified FooSpec
import qualified Foo.BarSpec
import qualified BazSpec
main :: IO ()
main = hspec $ do
describe "Foo" FooSpec.spec
describe "Foo.Bar" Foo.BarSpec.spec
describe "Baz" BazSpec.spec
```
If you have hundreds or even thousands of test modules, compiling the test driver becomes very slow ([corresponding Hspec issue](https://github.com/hspec/hspec/issues/952#issue-3960276745)).
Specifically, GHC compilation time increases quadratically with the number of test modules.
## Steps to reproduce
The issue can be reproduced with a single test module and a test driver that "includes" the test module multiple times.
Given a test module
```haskell
-- test/FooSpec.hs
module FooSpec where
import Test.Hspec
{-# OPAQUE spec #-}
spec :: Spec
spec = do
it "should be true" $ do
True `shouldBe` True
```
and a test driver
```haskell
-- test/Spec.hs
module Main (main) where
import qualified FooSpec
import Test.Hspec
main :: IO ()
main = hspec spec
spec :: Spec
spec = do
FooSpec.spec
...
FooSpec.spec
```
that uses the test module `n` times, compilation time grows quadratically with `n`.
Reproducer: https://github.com/sol/hspec-discover-performance-issue
## Expected behavior
Things I've tried:
- Adding `{-# OPTIONS_GHC -O0 #-}` to the test driver decreases compilation time by a constant factor, but it still stays quadratic in regards to `n`.
- Adding `{-# OPTIONS_GHC -fignore-interface-pragmas #-}` to the test driver does not have any effect (I assume this is because `-fignore-interface-pragmas` only changes how interface files are read, but at this point the interface file for `FooSpec` has already been read).
- Adding `-fignore-interface-pragmas` to the `.cabal` solves the issue, giving logarithmic compilation time.
From what I understand:
- GHC spends most of the compilation time on "simplifying" things and *code generation*.
- If I compile the test drive with `-O0` then the "simplifying" part doesn't change, but code generation goes down to ~0s.
- Once I enable `-fignore-interface-pragmas` globally "simplifying" goes down to ~0s. At this point it doesn't matter whether I use `-O0` or `-O1` because (I assume) without the "simplifying" there's no code blow up and hence nothing the optimizer could chew on.
Marking definition as `NOINLINE` or `OPAQUE` doesn't seem to help at all.
Questions:
- If I compile a test drive with `-O0`, for what reason does "simplifying" still take a long time? Is there a way of telling GHC to stop doing that for a single module?
- What's the reason that that `NOINLINE` or `OPAQUE` seemingly don't help?
- Is there a way to work around this issue somehow without requiring every user to enable `-fignore-interface-pragmas`? Ideally, I would wanna address this in `hspec-discover` by generating code that doesn't blow up.
## Environment
* GHC version used: 9.14.1
issue