haskell / haskell/cabal

Tweaking options for Haddock generation

Open
#10,852 2 comments 0 reactions 0 assignees View on GitHub
cabal-install: cmd/haddock Cabal: cmd/haddock type: discussion
Dominant language
Haskell
Stars
1.7k
Forks
750
Avg merge
4d 3h
Merged PRs (30d)
28

Description

PR #9177 introduced changes to how Haddock is invoked. Because it is designed to avoid recompilation of the source to generate the Haddock, this also means that the undocumented `cabal.project` option `haddock-options` is now a no-op. PR #9177 says to reach out if you need CPP defines for Haddock specifically. While CPP defines specifically I can [work around](https://github.com/haskell/cabal/issues/10818#issuecomment-2724758089), I thought it would be good to reach out and explain the use case we have in Clash. Also, I couldn't satisfactorily work around a specific issue, so I'll start with that.

But not before noting that we always upload the Haddock for [`clash-compiler`](https://github.com/clash-lang/clash-compiler) to Hackage ourselves, from a GitLab CI job, so we get the rendering we want. This does not work for Stackage, where our documentation consequently is pretty suboptimal. It would be really cool if the machinery could be extended to directly support our use case and always generate the Haddock we want, but I'm not actively "campaigning" to get towards this goal.

I managed to re-upload the documentation for our latest release to Hackage, so it contains all the tweaks discussed. However, to be able to show the difference, I've just uploaded a candidate for a slightly older version that doesn't use any of the tweaks, so I can show you what it looks like without. I don't see any reason why we should ever upload another candidate, so I expect the links to keep working, but if we /do/ upload another candidate, my links won't show the intended effect any more.

## Tweaking the theme

Our code tends to have [really long constraints](https://hackage.haskell.org/package/clash-prelude-1.8.1/docs/Clash-Prelude-BlockRam.html#v:blockRamU). The default Linuwial theme [disallows line breaks in constraints](https://gitlab.haskell.org/ghc/ghc/-/blob/47646ce28319aca8b92582c00f4be37714ca2a2d/utils/haddock/haddock-api/resources/html/Linuwial.std-theme/linuwial.css#L574-576), making that doc render [like this](https://hackage.haskell.org/package/clash-prelude-1.8.1/candidate/docs/Clash-Prelude-BlockRam.html#v:blockRamU) (try it in a browser with the width of half your monitor). So we [patch this CSS](https://github.com/clash-lang/clash-compiler/blob/45d09f609c16f04188fd8e658d51953dd888ee9a/clash-prelude/doc/linuwial-wrap-types.css) as follows:

```css
@import "linuwial.css";

/* Re-enable wordwrapping in (parts of) type signatures */
#interface td.src {
white-space: normal !important;
}
```

Then we do [the following incantation](https://github.com/clash-lang/clash-compiler/blob/45d09f609c16f04188fd8e658d51953dd888ee9a/cabal.project#L38-L41):

```
haddock-options: --theme=doc/linuwial-wrap-types.css --theme=Linuwial
```

The extra `--theme` means that `linuwial.css` will also be included in the output, meaning we can `@import` it and patch it without including the whole file (a file which by the way has small differences depending on the Haddock version).

I could not find a workaround for this other than including the whole CSS file instead of using `@import`, which has the issue that the file differs between GHC versions. If I simply enter two `haddock-css` option stanzas in `cabal.project`, only the last one survives. Admittedly passing `--theme` twice is perhaps not a deliberate functionality of Haddock, but we do need something with the functionality of having multiple CSS files some way.

As a different way of dealing with this, GHC 9.0 adds the option to write multiple constraints `C1 a => C2 a => ...` instead of a single tuple `(C1 a, C2 a) => ...`. This could be a way out, making the doc a lot more readable. Unfortunately, while Haddock for GHC 9.0 to 9.6 renders a single constraint per line in the doc, actually GHC 9.8 and later combine them back into a single tuple on a single line. So this is not a long-term solution for us either :-(. For now we also support GHC 8.10, so we can't use it until we drop that support (which might happen soon). We build our doc for Hackage with GHC 8.10.7 as well; I'm cautious about upgrading because I've noticed differences in rendering that sometimes broke the rendering of our documentation.

## GHC errors from doctests

We frequently show GHC error messages in our doctests in the Haddock. However, this output sometimes changes with new GHC releases. So we have to use CPP to adjust the doctest to the GHC version [like we do here](https://github.com/clash-lang/clash-compiler/blob/45d09f609c16f04188fd8e658d51953dd888ee9a/clash-prelude/src/Clash/Sized/Vector.hs#L418-L451):

```haskell
#if __GLASGOW_HASKELL__ >= 910
>>> head Nil

#elif __GLASGOW_HASKELL__ >= 900
>>> head Nil

#else
>>> head Nil

#endif
```

However, by default, CPP will emit line number markers, as we reported to Haddock [here](https://github.com/haskell/haddock/issues/1382) (I could not find the issue in the new repo). So we do this:

```
haddock-options: --optghc="-optP -P"
```

This says that CPP should not render line markers. With that change, it [renders as you'd expect](https://hackage.haskell.org/package/clash-prelude-1.8.1/docs/Clash-Sized-Vector.html#v:head), without it it [renders like this](https://hackage.haskell.org/package/clash-prelude-1.8.1/candidate/docs/Clash-Sized-Vector.html#v:head), almost unrecognisable.

This can be [worked around](https://github.com/haskell/cabal/issues/10818#issuecomment-2724758089), although we could also just add it to the CPP options for all cases.

## Do not document everything

Sometimes we just want to omit a whole bunch of declarations. For instance, for easier access to type-level naturals, we define `d0 :: SNat 0`, `d1 :: SNat 1` and so on until `d1024 :: SNat 1024` (by the way, `d` is for decimal. We had vague plans to add hexadecimal as well, but never actually did). Now, we don't want these all to occur in the documentation individually! Because then [if you go to the index of the letter _d_](https://hackage.haskell.org/package/clash-prelude-1.8.1/candidate/docs/doc-index-D.html), that's a lot of scrolling before you get to anything else! So we use a CPP macro named `HADDOCK_ONLY` to limit our Template Haskell code, in this case to [render just 10 of those _d_'s](https://hackage.haskell.org/package/clash-prelude-1.8.1/docs/doc-index-D.html).

Another case is with instances for tuples. Quite a few of our classes provide instances for tuples. By default, these are usually limited to 12-tuples at most, because generating them for up to 63-tuples makes compilation quite slow; we provide a flag `large-tuples` to compile with all of them. However, even 12-tuples can get annoying in the Haddock. And sometimes we create even larger tuples; a good example is the class we use to allow [Altera PLL IP](https://hackage.haskell.org/package/clash-prelude-1.8.1/docs/Clash-Intel-ClockGen.html#v:alteraPllSync) to generate up to 18 output clocks, which means outputting a 36-tuple with 18 clocks and the 18 accompanying reset signals.

To not get whole walls of instances in the Haddock of our classes, we limit documentation for tuples to 3-tuples (or in the case of the PLL clocks, tuples of size 2, 4 and 6). These instances leak to quite a few of our classes. If you look at [the doc for the `Clock` class](https://hackage.haskell.org/package/clash-prelude-1.8.1/docs/Clash-Explicit-Signal.html#t:Clock), you'll see the documentation of the instances is already a full monitor's height. Now if we actually have all the existing clock output tuples, that turns into [this](https://hackage.haskell.org/package/clash-prelude-1.8.1/candidate/docs/Clash-Signal-Internal.html#t:Clock), which is not something we like to present to our users.

This can once again be [worked around](https://github.com/haskell/cabal/issues/10818#issuecomment-2724758089), but to me the true end game would be a pragma that tells Haddock to omit the documentation for some declaration. We could have our Template Haskell code emit those pragma's for the larger tuples, and have only the smallish ones documented. Bonus points for a pragma that limits which instances are documented under which class definition, for extra flexibility. That way, you can document more of the instances of `ClocksSync` under the `ClocksSync` class, but document fewer of those same instances under the `Clock` and `Reset` classes. I'm not actively asking for this to be implemented, I'm just saying it seems like a nice way to deal with this properly.

Of course omitting declarations in the documentation is risky, since the documentation no longer matches one-to-one with the actual code. However, given how the documentation looks if you include it all, I personally greatly prefer omitting some things, and instead having the Haddock specifically call out "we omitted some stuff here", which is something we just add to the documentation text ourselves. In a perfect world, maybe Haddock would include a one-line message _"(some declarations omitted)"_ and clicking that message would then show all the declarations that were omitted?

Contributor guide

Open the contributing guide

Research direction

Start with PR #9177 and the current handling of the undocumented `haddock-options` setting in `cabal.project`. Compare the reported needs for multiple themes, CPP options, and selective declaration documentation, along with the workaround in issue #10818. Done requires narrowing these use cases into an agreed scope and defining tests for the selected behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
haskell
Domain
build-system, documentation
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.