packages feed

dependent-literals-plugin-0.1.0.2: src/DependentLiterals/Plugin.hs

-- Copyright 2020-2021 Google LLC
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--      http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.

{-# LANGUAGE CPP #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ViewPatterns #-}

-- | A GHC plugin making numeric literals and patterns pseudo-dependently-typed.

module DependentLiterals.Plugin (plugin) where

import Data.Foldable (for_)
import Data.Maybe (fromMaybe, isJust)

import qualified Data.Generics as SYB

-- GHC has been overhauling its module hierarchy, but to a significant extent,
-- it's renaming modules entirely and not regrouping exports; most of these we
-- paper over by defining macros for their most-up-to-date name with
-- underscores instead of dots.

-- This one was renamed twice in the range of supported versions.
#if MIN_VERSION_ghc(9,0,0)
#define GHC_Hs_Type GHC.Hs.Type
#elif MIN_VERSION_ghc(8,10,0)
#define GHC_Hs_Type GHC.Hs.Types
#else
#define GHC_Hs_Type HsTypes
#endif

-- This one was double-renamed, too.  Who knows how I managed to call the macro
-- GHC_Types_SourceText before any released versions actually called it that.
-- Maybe I was looking at the source on GitHub?
#if MIN_VERSION_ghc(9,2,0)
#define GHC_Types_SourceText GHC.Types.SourceText
#elif MIN_VERSION_ghc(9,0,0)
#define GHC_Types_SourceText GHC.Types.Basic
#else
#define GHC_Types_SourceText BasicTypes
#endif

-- Renames from 9.0 to 9.2
#if MIN_VERSION_ghc(9,2,0)
#define Language_Haskell_Syntax_Extension Language.Haskell.Syntax.Extension
#else
#define Language_Haskell_Syntax_Extension GHC.Hs.Extension
#endif

-- Renames from 8.10 to 9.0
#if MIN_VERSION_ghc(9,0,0)
#define GHC_Plugins GHC.Plugins
#define GHC_Types_Basic GHC.Types.Basic
#define GHC_Types_Name_Occurrence GHC.Types.Name.Occurrence
#define GHC_Types_Name_Reader GHC.Types.Name.Reader
#define GHC_Unit_Module_Name GHC.Unit.Module.Name
#define GHC_Utils_Outputable GHC.Utils.Outputable
#else
#define GHC_Plugins GhcPlugins
#define GHC_Types_Basic BasicTypes
#define GHC_Types_Name_Occurrence OccName
#define GHC_Types_Name_Reader RdrName
#define GHC_Unit_Module_Name Module
#define GHC_Utils_Outputable Outputable
#endif

-- Renames from 8.8 to 8.10
#if MIN_VERSION_ghc(8,10,0)
#define GHC_Hs GHC.Hs
#define GHC_Hs_Expr GHC.Hs.Expr
#define GHC_Hs_Extension GHC.Hs.Extension
#define GHC_Hs_Lit GHC.Hs.Lit
#define GHC_Hs_Pat GHC.Hs.Pat
#define GHC_Hs_Utils GHC.Hs.Utils
#else
#define GHC_Hs HsSyn
#define GHC_Hs_Expr HsExpr
#define GHC_Hs_Extension HsExtension
#define GHC_Hs_Lit HsLit
#define GHC_Hs_Pat HsPat
#define GHC_Hs_Utils HsUtils
#endif

import GHC_Hs
         ( HsModule(..), HsWildCardBndrs(HsWC)
         , HsTyLit(HsNumTy)
         , ImportDecl(..), IEWrappedName(..), IE(..)
         )
import GHC_Hs_Lit (HsOverLit(..), OverLitVal(HsIntegral))
import GHC_Hs_Expr (HsExpr(HsAppType, HsOverLit, HsApp, NegApp), LHsExpr)
import GHC_Hs_Extension (GhcPs, GhcPass)
import GHC_Hs_Type
         ( HsType(HsAppTy, HsParTy, HsTyLit, HsTyVar)
         , LHsType, HsConDetails(PrefixCon)
         )
import GHC_Hs_Pat (LPat, Pat(NPat, ViewPat))
import GHC_Hs_Utils (nlHsVar, nlHsApp)
import GHC_Plugins
         ( Hsc
         , Plugin(parsedResultAction, pluginRecompile), defaultPlugin
         , PluginRecompile(NoForceRecompile)
         , CommandLineOption
         , DynFlags, Located, GenLocated(L), noSrcSpan
         , getDynFlags, liftIO
         , gopt_set, GeneralFlag(Opt_SuppressModulePrefixes)
         , SrcSpan
         )
import GHC_Types_Name_Occurrence (OccName, mkTcOcc, mkVarOcc, mkDataOcc)
import GHC_Types_Name_Reader (RdrName, mkRdrQual, mkRdrUnqual)
import GHC_Types_SourceText (IntegralLit(IL), SourceText(NoSourceText))
import GHC_Unit_Module_Name (ModuleName, mkModuleName)
import GHC_Utils_Outputable ((<+>), Outputable, nest, ppr, sep, text)

-- For semantic changes, we generally try to paper over them by adding
-- compatibility shims, e.g. pattern synonyms, to make the code below look like
-- it's targeting the most-up-to-date version.

#if MIN_VERSION_ghc(9,2,0)
import GHC_Hs (HsParsedModule(..))
import GHC.Driver.Ppr (showSDoc)
import GHC.Parser.Annotation
         ( SrcAnn, SrcSpanAnn'(..), SrcSpanAnnA
         , EpAnn(EpAnnNotUsed), EpaLocation(EpaDelta)
         , DeltaPos(SameLine), LocatedN
         , noSrcSpanA, noLocA
         )
#else
import GHC_Plugins (HsParsedModule(..))
import GHC_Utils_Outputable (showSDoc)
#endif

#if MIN_VERSION_ghc(8,10,0) && !MIN_VERSION_ghc(9,2,0)
import GHC_Plugins (noLoc)
#endif

#if MIN_VERSION_ghc(9,0,0)
import GHC_Hs_Pat (Pat(ConPat))
import GHC.Unit.Types (IsBootInterface(..))
#else
-- Imports for pre-9.0 compatibily shims.
import GHC_Hs_Pat (Pat(ConPatIn), HsConPatDetails)
import GHC_Hs_Extension (IdP)
#endif

#if MIN_VERSION_ghc(8,10,0)
import GHC.Hs.ImpExp (ImportDeclQualifiedStyle(..))
import Language_Haskell_Syntax_Extension (NoExtField(..))
#else
-- Imports for pre-8.10 compatibily shims.
import HsExtension (NoExt(..))
#endif

#if MIN_VERSION_ghc(8,8,0)
import GHC_Types_Basic (PromotionFlag(..))
#else
-- Imports for pre-8.8 compatibily shims.
import GHC_Plugins (noLoc)
import HsTypes (Promoted(..))
#endif

-- Pre-9.2 compatibility shims.
#if !MIN_VERSION_ghc(9,2,0)
nl :: a -> Located a
nl = L noSrcSpan

ieType :: Located name -> IEWrappedName name
ieType = IEType

type EpAnn = NoExtField

pattern EpAnnNotUsed :: EpAnn
pattern EpAnnNotUsed = NoExtField

type SrcSpanAnnA = SrcSpan
toSrcSpanAnnA :: SrcSpan -> SrcSpan
toSrcSpanAnnA = id

mkPrefixCon :: [arg] -> HsConDetails arg rec
mkPrefixCon = PrefixCon
#else
nl :: a -> GenLocated (SrcAnn ann) a
nl = L noSrcSpanA

ieType :: LocatedN name -> IEWrappedName name
ieType = IEType (EpaDelta (SameLine 1) [])

toSrcSpanAnnA :: SrcSpan -> SrcSpanAnnA
toSrcSpanAnnA l = SrcSpanAnn EpAnnNotUsed l

mkPrefixCon :: [arg] -> HsConDetails tyarg arg rec
mkPrefixCon = PrefixCon []
#endif


-- Pre-9.0 compatibility shims.
#if !MIN_VERSION_ghc(9,0,0)
pattern ConPat :: a -> Located (IdP p) -> HsConPatDetails p -> Pat p
pattern ConPat ext con args <- (((,) NoExtField) -> (ext, ConPatIn con args))
 where
  ConPat _ext con args = ConPatIn con args

type HsModulePs = HsModule GhcPs

pattern NotBoot :: Bool
pattern NotBoot = False
#else
type HsModulePs = HsModule
#endif

-- Pre-8.10 compatibility shims.
#if !MIN_VERSION_ghc(8,10,0)
type NoExtField = NoExt
pattern NoExtField :: NoExtField
pattern NoExtField = NoExt

type ImportDeclQualifiedStyle = Bool
pattern QualifiedPre, NotQualified :: ImportDeclQualifiedStyle
pattern QualifiedPre = True
pattern NotQualified = False
#endif

-- Pre-8.8 compatibility shims.
#if !MIN_VERSION_ghc(8,8,0)
type PromotionFlag = Promoted
pattern IsPromoted :: PromotionFlag
pattern IsPromoted = Promoted
#endif

data Config = Config
  { _cDoLiterals :: Bool
  , _cDoPatterns :: Bool
  , _cTraceThings :: Bool
  }

defaultConfig :: Config
defaultConfig = Config True True False

interpretOpts :: [CommandLineOption] -> Config
interpretOpts opts0 = go opts0 defaultConfig
 where
  go [] c = c
  go ("nolits":opts) c = go opts c { _cDoLiterals = False }
  go ("nopats":opts) c = go opts c { _cDoPatterns = False }
  go ("trace":opts) c = go opts c { _cTraceThings = True }
  go (opt:_) _ = error $
    "Illegal option " ++ show opt ++
    ".\nAll options: " ++ show opts0

plugin :: Plugin
plugin = defaultPlugin
  { parsedResultAction = \opts _ -> parsedResultPlugin (interpretOpts opts)
  , pluginRecompile = \_ -> return NoForceRecompile
  }

parsedResultPlugin :: Config -> HsParsedModule -> Hsc HsParsedModule
parsedResultPlugin cfg m = do
  df <- getDynFlags
  hpm_module' <- transformParsed cfg df (hpm_module m)
  return $ m { hpm_module = hpm_module' }

when_ :: Applicative f => Bool -> (a -> f a) -> a -> f a
when_ True f = f
when_ False _ = pure

pattern LPat :: Pat (GhcPass p) -> LPat (GhcPass p)
#if !MIN_VERSION_ghc(8,8,0) || MIN_VERSION_ghc(8,10,0)
pattern LPat pat <- L _ pat
#else
pattern LPat pat <- pat
#endif

nlPat :: Pat (GhcPass p) -> LPat (GhcPass p)
nlPat = id
#if MIN_VERSION_ghc(9,2,0)
  . noLocA
#elif !MIN_VERSION_ghc(8,8,0) || MIN_VERSION_ghc(8,10,0)
  . noLoc
#endif

transformParsed
  :: Config
  -> DynFlags
  -> Located HsModulePs
  -> Hsc (Located HsModulePs)
transformParsed Config{..} df' (L modLoc HsModule{..}) = do
  decls <-
    pure hsmodDecls
      >>= when_ _cDoLiterals
            ( SYB.everywhereM (SYB.mkM (wrapDebug "expression" transformExp))
            . SYB.everywhere (SYB.mkT foldNegation)
            )
      >>= when_ _cDoPatterns
            (SYB.everywhereM (SYB.mkM (wrapDebug "pattern" transformPat)))

  return $ L modLoc $ HsModule
    { hsmodDecls = decls
    , hsmodImports =
        mkModImport litMod Nothing QualifiedPre Nothing :
        unqualLitModImport :
        qualIntModImport :
        hsmodImports
    , ..
    }
 where
  df = gopt_set df' Opt_SuppressModulePrefixes

  litMod, intMod :: ModuleName
  litMod = mkModuleName "DependentLiterals.Int"
  intMod = mkModuleName "Kinds.Integer"

  -- import qualified DependentLiterals.Int
  mkModImport nm as q imports = nl $ ImportDecl
    EpAnnNotUsed
    NoSourceText
    (nl nm)
    Nothing -- no package qualifier
    NotBoot
    False -- not marked safe
    q -- qualified
    True -- implicit
    as -- "as" rename
    ((False,) . nl . map nl <$> imports) -- no "hiding"

  -- Import a few things unqualified implicitly.  This way, when they appear in
  -- error messages, they won't have bulky module names attached.  All of these
  -- have -XMagicHash names so that they can't conflict with the subset of the
  -- namespace used by reasonable Haskell programmers, and most people can't
  -- ever tell that they're imported.
  -- TODO can we move this plugin post-renamer and do this by generating names
  -- that claim to have been originally unqualified?
  importVar = IEVar NoExtField . nl . IEName .nl
  importAll = IEThingAll EpAnnNotUsed . nl . IEName . nl
  importTyOp = IEThingAbs EpAnnNotUsed . nl . ieType . nl
  unqualLitModImport = mkModImport litMod Nothing NotQualified $ Just
    [ importVar litHashName
    , importTyOp minusHashName
    ]
  qualIntModImport = mkModImport intMod Nothing QualifiedPre $ Just
    [ importAll integerName
    ]

  qual :: OccName -> RdrName
  qual = mkRdrQual litMod

  integerName   = mkRdrUnqual (mkTcOcc "Integer")
  minusHashName = mkRdrUnqual (mkTcOcc "-#")
  negName       = mkRdrQual intMod (mkDataOcc "Neg")
  posName       = mkRdrQual intMod (mkDataOcc "Pos")
  cjustConName  = qual (mkDataOcc "CJust")
  litHashName   = mkRdrUnqual (mkVarOcc "lit#")
  matchHashName = qual (mkVarOcc "match#")

  infixl 4 `mkHsAppType`
  mkHsAppType :: LHsExpr GhcPs -> LHsType GhcPs -> HsExpr GhcPs
  mkHsAppType expr ty = HsAppType
#if MIN_VERSION_ghc(8,8,0)
#if MIN_VERSION_ghc(9,2,0)
    noSrcSpan
#else
    NoExtField
#endif
    expr
    (HsWC NoExtField ty)
#else
    (HsWC NoExtField ty)
    expr
#endif

  infixl 4 `nlHsAppType`
  nlHsAppType :: LHsExpr GhcPs -> LHsType GhcPs -> LHsExpr GhcPs
  nlHsAppType expr ty = nl $ mkHsAppType expr ty

  infixl 4 `nlHsApp_`
  nlHsApp_ = nlHsApp

  litToTyLit :: IntegralLit -> LHsType GhcPs
  litToTyLit (IL txt neg val) =
    nl $ HsParTy EpAnnNotUsed $
    nl $ HsAppTy NoExtField
      (nl $ HsTyVar EpAnnNotUsed IsPromoted $
        nl (if neg then negName else posName))
      (nl $ HsTyLit NoExtField (HsNumTy txt (abs val)))

  debug :: String -> Hsc ()
  debug s
    | _cTraceThings = liftIO (putStrLn s)
    | otherwise = return ()

  wrapDebug :: Outputable a => String -> (a -> Maybe a) -> a -> Hsc a
  wrapDebug thing f x = do
    let r = f x
    for_ r (\x' ->
      debug $ showSDoc df $ sep
        [ text "Rewrote" <+> text thing <+> ppr x <+> text "to"
        , nest 2 $ ppr x'
        ])
    return $ fromMaybe x r

  extractLit :: HsExpr GhcPs -> Maybe (IntegralLit, HsExpr GhcPs)
  extractLit (HsOverLit _ (OverLit _ (HsIntegral il) w)) = Just (il, w)
  extractLit _ = Nothing

  fuseNegation :: Bool -> IntegralLit -> IntegralLit
  fuseNegation negated (IL _txt neg val) =
    let -- You can write patterns/exprs that are the negation of a neg literal.
        -- We'll just sweep those under the rug by making them into a positive
        -- literal.  If there's more than one negation, too bad.  Those will
        -- try to call into a Num instance.
        neg' = neg /= negated

        -- If the thing described in the previous comment happened, we have
        -- e.g. "-4" as the source text.  Just drop the source text always.
        txt' = NoSourceText

        -- Set the sign of the resulting literal according to 'neg''.
        val' = (if neg' then negate else id) (abs val)

        -- Refabricated literal.
    in  IL txt' neg' val'

  buildReprLit :: SrcSpanAnnA -> IntegralLit -> HsExpr GhcPs -> LHsExpr GhcPs
  buildReprLit l il witness =
    L l $ HsOverLit EpAnnNotUsed $ OverLit NoExtField (HsIntegral il) witness

  rewriteLit
    :: SrcSpanAnnA -> Bool -> IntegralLit -> HsExpr GhcPs -> LHsExpr GhcPs
  rewriteLit l negated il witness =
    let il' = fuseNegation negated il
        wrapper = nlHsVar litHashName `nlHsAppType` litToTyLit il'
        lit = buildReprLit l il' witness
    in  L l $ HsApp EpAnnNotUsed (nlHsApp wrapper lit) lit

  foldNegation :: LHsExpr GhcPs -> LHsExpr GhcPs
  foldNegation (L l (NegApp _ (L _ (extractLit -> Just (il, witness))) _)) =
    buildReprLit l (fuseNegation True il) witness
  foldNegation e = e

  transformExp :: LHsExpr GhcPs -> Maybe (LHsExpr GhcPs)
  transformExp (L l (extractLit -> Just (lit, witness))) =
    Just $ rewriteLit l False lit witness
  transformExp _ = Nothing

  transformPat :: LPat GhcPs -> Maybe (LPat GhcPs)
  transformPat (LPat (NPat _ (L l (OverLit _ (HsIntegral il) witness)) negation _)) =
    let il' = fuseNegation (isJust negation) il

        -- Wrapper application of match# to the LitRepr.
        wrappedLit =
          nlHsVar matchHashName
            `nlHsAppType` litToTyLit il'
            `nlHsApp_` buildReprLit (toSrcSpanAnnA l) il' witness
            `nlHsApp_` buildReprLit (toSrcSpanAnnA l) il' witness

    in  Just $ nlPat $ ViewPat EpAnnNotUsed
            wrappedLit
            (nlPat $ ConPat EpAnnNotUsed (nl cjustConName) (mkPrefixCon []))
  transformPat _ = Nothing