|
Conditional Coding.Struggle with C preprocessor interleaved iffy Haskell code? |
{-# LANGUAGE CPP #-} module GHC.TcPluginM.Extra #if __GLASGOW_HASKELL__ < 711 #endif #if __GLASGOW_HASKELL__ < 711 #endif #if MIN_VERSION_ghc(9,2,0) #elif MIN_VERSION_ghc(9,0,0) #endif #if MIN_VERSION_ghc(9,0,0) #else #if __GLASGOW_HASKELL__ < 711 #endif #if MIN_VERSION_ghc(8,5,0) #endif #if __GLASGOW_HASKELL__ >= 711 #else #endif #if __GLASGOW_HASKELL__ < 711 #else #if __GLASGOW_HASKELL__ < 809 #else #endif #endif #if __GLASGOW_HASKELL__ < 802 #endif #if __GLASGOW_HASKELL__ >= 711 #else #endif #if __GLASGOW_HASKELL__ < 809 #if __GLASGOW_HASKELL__ >= 806 #endif #else #endif #if __GLASGOW_HASKELL__ < 809 #else #endif #if __GLASGOW_HASKELL__ < 711 #else #endif #endif #if __GLASGOW_HASKELL__ < 802 #endif #if __GLASGOW_HASKELL__ >= 711 #endif #if __GLASGOW_HASKELL__ < 711 #endif #if __GLASGOW_HASKELL__ >= 711 #else #endif #if MIN_VERSION_ghc(8,5,0) #elif __GLASGOW_HASKELL__ >= 711 #else #endif #if __GLASGOW_HASKELL__ >= 711 #else #endif #if MIN_VERSION_ghc(8,5,0) #else #if __GLASGOW_HASKELL__ < 711 #endif #endif #if __GLASGOW_HASKELL__ >= 711 #else #endif #if __GLASGOW_HASKELL__ < 711 #endif #if __GLASGOW_HASKELL__ >= 711 #else #endif #if __GLASGOW_HASKELL__ >= 711 #else #endif #if __GLASGOW_HASKELL__ >= 711 #else #endif #if __GLASGOW_HASKELL__ < 802 #else #endif #if MIN_VERSION_ghc(8,4,0) #elif MIN_VERSION_ghc(8,0,0) #else #endif #if MIN_VERSION_ghc(9,2,0) #else #endif #if MIN_VERSION_ghc(8,6,0) #endif #if __GLASGOW_HASKELL__ >= 900 #elif __GLASGOW_HASKELL__ >= 809 #elif __GLASGOW_HASKELL__ >= 802 #elif __GLASGOW_HASKELL__ < 711 #endif #if __GLASGOW_HASKELL__ > 711 #endif
I have a dependency on ghc-tcplugins-extra. The panel on the right shows the
#ifdefs of its one module, GHC.TcPluginM.Extra
. I’m happy with this
package and don’t help maintain it so why am I making a disruptive pull request with 63
changed files, 1,902 additions and 458 deletions?
CPP Hell, No!
I don’t much like CPP
and find nested conditional blocks hard to
disentangle. One or two is fine but when they’re nested and the conditions range
over many GHC versions I find it hard to take in the whole at a glance let alone
see the difference between one GHC version and the next. What is more #ifdefs
are noise in the source file.
We can can stop or reduce {-# LANGUAGE CPP #-}
pragma use even when we need
to switch code between GHC versions. I’ll show you how using ghc-tcplugins-extra
as an example.
One Internal Indirection
To start, I gut src/GHC.TcPluginM.Extra
1 and defer to import Internal
for the implementation so that this module only imports and re-exports. None of
the definitions remain.
module GHC.TcPluginM.Extra
-- * Create new constraints
(
newWanted
, newGiven
, newDerived-- * Creating evidence
, evByFiat-- * Lookup
, lookupModule
, lookupName-- * Trace state of the plugin
, tracePlugin-- * Substitutions
, flattenGivens
, mkSubst
, mkSubst'
, substType
, substCtwhere
)
import Internal
I thought this might screw around with the haddocks but they look good, the internal module is invisible and the module tree is unchanged.
- GHC
- TcPluginM
In the implementation I have two module hierarchies, GhcApi.*
and
Internal.*
.
Cabal Conditionals
In the cabal file with impl(ghc?)
conditonals we can pick which files to
compile. This is how we’re going to branch instead of using #ifdefs interleaved
with the source code.
library
exposed-modules:
GHC.TcPluginM.Extra
other-modules:
Internal
hs-source-dirs:
src
if impl(ghc >= 9.2) && impl(ghc < 9.4)
hs-source-dirs:
src-ghc-tree
src-ghc-9.2
if impl(ghc >= 9.0) && impl(ghc < 9.2)
hs-source-dirs:
src-ghc-tree
src-ghc-9.0
if impl(ghc >= 8.10) && impl(ghc < 9.0)
hs-source-dirs:
src-ghc-flat
src-ghc-8.10
if impl(ghc >= 8.8) && impl(ghc < 8.10)
hs-source-dirs:
src-ghc-flat
src-ghc-8.8
if impl(ghc >= 8.6) && impl(ghc < 8.8)
hs-source-dirs:
src-ghc-flat
src-ghc-8.6
if impl(ghc >= 8.4) && impl(ghc < 8.6)
hs-source-dirs:
src-ghc-flat
src-ghc-8.4
if impl(ghc >= 8.2) && impl(ghc < 8.4)
hs-source-dirs:
src-ghc-flat
src-ghc-8.2
if impl(ghc >= 8.0) && impl(ghc < 8.2)
hs-source-dirs:
src-ghc-flat
src-ghc-8.0
if impl(ghc >= 7.10) && impl(ghc < 8.0)
hs-source-dirs:
src-ghc-cpp
When some things stay the same but others change between GHC versions we can
group modules into different hs-source-dirs
directories.
When 8.0 <= ghc < 9.0
in src-ghc-flat
we import from the flatter GHC
module hierarchy but with src-ghc-tree
we import from the newer layout of
GHC modules with a deeper hierarchy. To track less sweeping changes between GHC
dot-even-numbered releases we’ll use version-specific directories like
src-ghc-9.0
and src-ghc-9.2
.
File Diffing
We’re trading duplicating modules for ease of diffing. With everyday file diff tooling we can review how we tracked GHC changes more explicitly. No more squinting at mixed language source files. For a library such as ghc-tcplugins-extra, supporting a newer GHC version starts with copying a directory, recompiling and then making whatever changes are necessary without fear of screwing up support for older versions because the version-specific directories are isolated. If we stuffed up the cabal conditionals somehow we’d get an error when compiling, either about missing modules or about duplicate modules.
One change between src-ghc-9.0
and src-ghc-9.2
.
--- src-ghc-9.0/GhcApi/Constraint.hs
+++ src-ghc-9.2/GhcApi/Constraint.hs
module GhcApi.Constraint
( Ct(..
, CtEvidence(..)
, CtLoc+ , CanEqLHS(..)
, ctLoc
, ctEvId
, mkNonCanonical
) where
import GHC.Tc.Types.Constraint- ( Ct(..), CtEvidence(..), CtLoc
+ ( Ct(..), CtEvidence(..), CanEqLHS(..), CtLoc
, ctLoc, ctEvId, mkNonCanonical )
An example of reacting to GHC’s change to a deeper module hierarchy.
--- src-ghc-flat/GhcApi/Predicate.hs
+++ src-ghc-tree/GhcApi/Predicate.hs
module GhcApi.Predicate (mkPrimEqPred) where
- import Predicate (mkPrimEqPred)
+ import GHC.Core.Coercion (mkPrimEqPred)
Cabal Mixins
With mixins we can rename and alias module names2. The hiding ()
exposes the current names and the as
does the aliasing.
mixins:
ghc hiding ()
, ghc (TcRnTypes as Constraint)
, ghc (Type as Predicate)
I could have avoided using mixins like this but it helped insulate me from GHC API changes.
module GhcApi.Constraint
Ct(..)
( CtEvidence(..)
, CtLoc
,
, ctLoc
, ctEvId
, mkNonCanonicalwhere
)
import Constraint (Ct (..), CtEvidence (..), CtLoc, ctLoc, ctEvId, mkNonCanonical)
I can import Constraint
when that module exists in GHC and when it doesn’t
exist.
> cabal build all
Build profile: -w ghc-8.10.7 -O1
[1 of 8] Compiling GhcApi.Constraint
[2 of 8] Compiling GhcApi.GhcPlugins
[3 of 8] Compiling GhcApi.Predicate
[4 of 8] Compiling Internal.Constraint
[5 of 8] Compiling Internal.Evidence
[6 of 8] Compiling Internal.Type
[7 of 8] Compiling Internal
[8 of 8] Compiling GHC.TcPluginM.Extra
Without the mixin renaming TcRnTypes to Constraint this module errors.
> cabal build all
Build profile: -w ghc-8.8.4 -O1
src-ghc-flat/GhcApi/Constraint.hs:11:1: error:
Could not find module ‘Constraint’
Perhaps you meant Constants (from ghc-8.8.4)
Use -v (or `:set -v` in ghci) to see a list of the files searched for.
|
11 | import Constraint
| ^^^^^^^^^^^^^^^^^...
Simpler with Dhall
Tweaking the cabal file with all these conditionals and mixins is a bit too much repetitive work but with hpack-dhall we can simplify this chore.
λ(low : Text) →
λ(high : Text) →
λ(srcs : List Text) →
λ(ghc : { name : Text, mixin : List Text }) →
λ(mods : List Text) →
{ condition = "impl(ghc >= ${low}) && impl(ghc < ${high})"
, source-dirs =List/map Text Text (λ(x : Text) → "src-ghc-${x}") srcs
Prelude/[ ghc ⫽ { version = ">=${low} && <${high}" } ]
, dependencies =
, other-modules = mods}
Why do this?
I feel that copying code files this way is better than using CPP. We can single thread our thoughts looking at plain Haskell code uninterrupted by C prepocessor #ifdefs and deal with only one GHC version at a time when getting the code to compile against a newer GHC version or when debugging a problem.
Backporting changes is simpler too because of the diffing but may require more edits if ranging back over multiple GHC versions. If we don’t care about backporting then the set of modules for an older GHC version can be left alone as we don’t need to touch them with CPP #ifdefs.
Except for
ghc < 8.0
where I have left the original CPP-heavy module alone untouched.↩︎Mixins are a cabal 2.0 feature and requires
impl(ghc >= 8.2)
. That’s true if I use stack but with cabal-install I can getimpl(ghc >= 7.10.3) && impl(ghc < 8.0)
to compile a package using cabal mixins. With the cabal github action in.github/workflows/cabal.yml
cabal can build against GHC versions[ 7.10.3, 8.0.2, 8.2.2, 8.4.4, 8.6.5, 8.8.4, 8.10.7, 9.0.1, 9.2.1 ]
↩︎