retrie-2.0.0: Retrie/Context.hs
-- Copyright (c) 2025 Andrew Farmer
-- Copyright (c) 2020-2024 Facebook, Inc. and its affiliates.
--
-- This source code is licensed under the MIT license found in the
-- LICENSE file in the root directory of this source tree.
--
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Retrie.Context
( ContextUpdater
, updateContext
, emptyContext
) where
import Control.Monad.IO.Class
import Data.Char (isDigit)
import Data.Either (partitionEithers)
import Data.Generics hiding (Fixity)
#if __GLASGOW_HASKELL__ < 912
import Data.List
#endif
import Data.Maybe
import Retrie.AlphaEnv
import Retrie.ExactPrint
import Retrie.Fixity
import Retrie.FreeVars
import Retrie.GHC
import Retrie.Substitution
import Retrie.SYB
import Retrie.Types
import Retrie.Universe
-------------------------------------------------------------------------------
-- | Type of context update functions for 'apply'.
-- When defining your own 'ContextUpdater', you probably want to extend
-- 'updateContext' using SYB combinators such as 'mkQ' and 'extQ'.
type ContextUpdater = forall m. MonadIO m => GenericCU (TransformT m) Context
-- | Default context update function.
updateContext :: forall m. MonadIO m => GenericCU (TransformT m) Context
updateContext c i =
const (return c)
`extQ` (return . updExp)
`extQ` (return . updType)
`extQ` (return . updMatch)
`extQ` (return . updGRHSs)
`extQ` (return . updGRHS)
`extQ` (return . updStmt)
`extQ` (return . updPat)
`extQ` updStmtList
`extQ` (return . updHsBind)
`extQ` (return . updTyClDecl)
where
neverParen = c { ctxtParentPrec = NeverParen }
withOpPrec :: LHsExpr GhcPs -> Context
updExp :: HsExpr GhcPs -> Context
updType :: HsType GhcPs -> Context
updType HsTupleTy{} = neverParen
updType HsListTy{} = neverParen
updType HsParTy{} = neverParen
#if __GLASGOW_HASKELL__ < 912
updType HsAppTy{} = withPrec c (SourceText "HsAppTy") (getPrec appPrec) InfixL i
updType HsFunTy{} = withPrec c (SourceText "HsFunTy") (getPrec funPrec) InfixR (i - 1)
updType _ = withPrec c (SourceText "HsType") (getPrec appPrec) InfixN i
withOpPrec op
| Fixity source prec dir <- lookupOp op $ ctxtFixityEnv c =
withPrec c source prec dir i
updExp HsApp{} = withPrec c (SourceText "HsApp") 10 InfixL i
updExp RecordUpd{}
| i == firstChild = withPrec c (SourceText "RecordUpd") 11 InfixN i
updExp HsGetField{}
| i == firstChild = withPrec c (SourceText "HsGetField") 11 InfixN i
updExp NegApp{} = withPrec c (SourceText "NegApp") 6 InfixN i
updExp (HsLet _ _ lbs _ _) = addInScope neverParen $ collectLocalBinders CollNoDictBinders lbs
#else
updType HsAppTy{} = withPrec c (getPrec appPrec) InfixL i
updType HsFunTy{} = withPrec c (getPrec funPrec) InfixR (i - 1)
updType _ = withPrec c (getPrec appPrec) InfixN i
withOpPrec op
| Fixity prec dir <- lookupOp op $ ctxtFixityEnv c =
withPrec c prec dir i
updExp HsApp{} = withPrec c 10 InfixL i
updExp RecordUpd{}
| i == firstChild = withPrec c 11 InfixN i
updExp HsGetField{}
| i == firstChild = withPrec c 11 InfixN i
updExp NegApp{} = withPrec c 6 InfixN i
updExp (HsLet _ lbs _) = addInScope neverParen $ collectLocalBinders CollNoDictBinders lbs
#endif
updExp (OpApp _ _ op _) = withOpPrec op
updExp (SectionL _ _ op) = withOpPrec op
updExp (SectionR _ op _) = withOpPrec op
updExp _ = neverParen
updMatch :: Match GhcPs (LHsExpr GhcPs) -> Context
updMatch
| i == 2 -- m_pats field
#if __GLASGOW_HASKELL__ < 912
= addInScope c{ctxtParentPrec = IsLhs} . collectPatsBinders CollNoDictBinders . m_pats
| otherwise = addInScope neverParen . collectPatsBinders CollNoDictBinders . m_pats
#else
= addInScope c{ctxtParentPrec = IsLhs} . collectPatsBinders CollNoDictBinders . unLoc . m_pats
| otherwise
= addInScope neverParen . collectPatsBinders CollNoDictBinders . unLoc . m_pats
#endif
where
updGRHSs :: GRHSs GhcPs (LHsExpr GhcPs) -> Context
updGRHSs = addInScope neverParen . collectLocalBinders CollNoDictBinders . grhssLocalBinds
updGRHS :: GRHS GhcPs (LHsExpr GhcPs) -> Context
updGRHS (GRHS _ gs _)
-- binders are in scope over the body (right child) only
| i > firstChild = addInScope neverParen bs
| otherwise = fst $ updateSubstitution neverParen bs
where
bs = collectLStmtsBinders CollNoDictBinders gs
updStmt :: Stmt GhcPs (LHsExpr GhcPs) -> Context
updStmt _ = neverParen
updStmtList :: [LStmt GhcPs (LHsExpr GhcPs)] -> TransformT m Context
updStmtList [] = return neverParen
updStmtList (ls:_)
-- binders are in scope over tail of list (right child)
| i > 0 = insertDependentRewrites neverParen bs ls
-- lets are recursive in do-blocks
| L _ (LetStmt _ bnds) <- ls =
return $ addInScope neverParen $ collectLocalBinders CollNoDictBinders bnds
| otherwise = return $ fst $ updateSubstitution neverParen bs
where
bs = collectLStmtBinders CollNoDictBinders ls
updHsBind :: HsBind GhcPs -> Context
updHsBind FunBind{..} =
let rdr = unLoc fun_id
in addBinders (addInScope neverParen [rdr]) [rdr]
updHsBind _ = neverParen
updTyClDecl :: TyClDecl GhcPs -> Context
updTyClDecl SynDecl{..} = addInScope neverParen [unLoc tcdLName]
updTyClDecl DataDecl{..} = addInScope neverParen [unLoc tcdLName]
updTyClDecl ClassDecl{..} = addInScope neverParen [unLoc tcdLName]
updTyClDecl _ = neverParen
updPat :: Pat GhcPs -> Context
updPat _ = neverParen
getPrec :: PprPrec -> Int
getPrec (PprPrec prec) = prec
#if __GLASGOW_HASKELL__ < 912
withPrec :: Context -> SourceText -> Int -> FixityDirection -> Int -> Context
withPrec c source prec dir i = c{ ctxtParentPrec = HasPrec fixity }
where
fixity = Fixity source prec d
#else
withPrec :: Context -> Int -> FixityDirection -> Int -> Context
withPrec c prec dir i = c{ ctxtParentPrec = HasPrec fixity }
where
fixity = Fixity prec d
#endif
d = case dir of
InfixL
| i == firstChild -> InfixL
| otherwise -> InfixN
InfixR
| i == firstChild -> InfixN
| otherwise -> InfixR
InfixN -> InfixN
-- | Create an empty 'Context' with given 'FixityEnv', rewriter, and dependent
-- rewrite generator.
emptyContext :: FixityEnv -> Rewriter -> Rewriter -> Context
emptyContext ctxtFixityEnv ctxtRewriter ctxtDependents = Context{..}
where
ctxtBinders = []
ctxtInScope = emptyAlphaEnv
ctxtParentPrec = NeverParen
ctxtSubst = Nothing
-- Deal with Trees-That-Grow adding extension points
-- as the first child everywhere.
firstChild :: Int
firstChild = 1
-- | Add dependent rewrites to 'ctxtRewriter' if necessary.
insertDependentRewrites
:: (Matchable k, MonadIO m) => Context -> [RdrName] -> k -> TransformT m Context
insertDependentRewrites c bs x = do
r <- runRewriter id c (ctxtDependents c) x
let
c' = addInScope c bs
case r of
NoMatch -> return c'
MatchResult _ Template{..} -> do
let
rrs = fromMaybe [] tDependents
ds = rewritesWithDependents rrs
f = foldMap (mkLocalRewriter $ ctxtInScope c')
return c'
{ ctxtRewriter = f rrs <> ctxtRewriter c'
, ctxtDependents = f ds <> ctxtDependents c'
}
-- | Add set of binders to 'ctxtInScope'.
addInScope :: Context -> [RdrName] -> Context
addInScope c bs =
c' { ctxtInScope = foldr extendAlphaEnv (ctxtInScope c') bs' }
where
(c', bs') = updateSubstitution c bs
-- | Add set of binders to 'ctxtBinders'.
addBinders :: Context -> [RdrName] -> Context
addBinders c bs = c { ctxtBinders = bs ++ ctxtBinders c }
-- Capture-avoiding substitution
--------------------------------------------------------------------------------
-- | Update the Context's substitution appropriately for a set of binders.
-- Returns a new Context and a potentially alpha-renamed set of binders.
updateSubstitution :: Context -> [RdrName] -> (Context, [RdrName])
updateSubstitution c rdrs =
case ctxtSubst c of
Nothing -> (c, rdrs)
Just sub ->
let
-- This prevents substituting for 'x' under a binding for 'x'.
sub' = deleteSubst sub $ map rdrFS rdrs
-- Compute free vars of substitution that could possibly be captured.
fvs = substFVs sub'
-- Partition binders into noncapturing and capturing.
(noncapturing, capturing) =
partitionEithers $ map (updateBinder fvs) rdrs
-- Extend substitution with alpha-renamings.
alphaSub = foldl' (uncurry . extendSubst) sub'
[ (rdrFS rdr, HoleRdr rdr') | (rdr, rdr') <- capturing ]
-- There are no telescopes in source Haskell, so order doesn't matter.
-- Capturing should be rare, so put it first to avoid quadratic append.
rdrs' = map snd capturing ++ noncapturing
in (c { ctxtSubst = Just alphaSub }, rdrs')
-- | Check if RdrName is in FreeVars.
--
-- If so, return a pair of it and its new name (Right).
-- If not, return it unchanged (Left).
updateBinder :: FreeVars -> RdrName -> Either RdrName (RdrName, RdrName)
updateBinder fvs rdr
| elemFVs rdr fvs = Right (rdr, renameBinder rdr fvs)
| otherwise = Left rdr
-- | Given a RdrName, rename it to something not in given FreeVars.
--
-- x => x1
-- x1 => x2
-- x9 => x10
--
-- etc.
--
-- Only works on unqualified RdrNames. This is fine, as we only use this to
-- rename local binders.
renameBinder :: RdrName -> FreeVars -> RdrName
renameBinder rdr fvs = headNoWarn
[ rdr'
| i <- [n..]
, let rdr' = mkVarUnqual $ mkFastString $ baseName ++ show i
, not $ rdr' `elemFVs` fvs
]
where
(ds, rest) = span isDigit $ reverse $ occNameString $ occName rdr
baseName = reverse rest
-- We build with -Wall -Werror, and there is a warning about how `head` is
-- partial. Using `head` is safe here because the list is infinite, so the
-- nil case is impossible. Define our own to avoid the warning.
headNoWarn (x:_) = x
headNoWarn _ = error "headNoWarn: impossible!"
n :: Int
n | null ds = 1
| otherwise = read (reverse ds) + 1