{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskell #-}
module Language.Haskell.SourceMatch
( isStringMatchedToDecl
, isFileMatchedToDecl
, MatchResult
, T_1, T_2, T_3, T_4, T_5, T_6, T_7, T_8, T_9, T_10
) where
import Control.Monad
import Control.Monad.Trans.Class as T
import Control.Monad.Trans.Except
import Control.Monad.Trans.State
import Data.Either
import Data.List
import Data.List.Split
import Data.Map.Strict (Map)
import Data.Maybe
import Language.Haskell.Exts
import Language.Haskell.Exts.Parser
import Language.Haskell.Exts.Syntax as HS
import Language.Haskell.TH as TH
import Language.Haskell.TH.Lib
import Language.Haskell.TH.Quote(QuasiQuoter(..))
import Language.Haskell.TH.Syntax as TH
import Text.InterpolatedString.Perl6 (qc)
import qualified Data.Map.Strict as Map
import qualified Language.Haskell.TH.Syntax as TH
data T_1
data T_2
data T_3
data T_4
data T_5
data T_6
data T_7
data T_8
data T_9
data T_10
matchExpr, matchDecl :: QuasiQuoter
matchExpr = QuasiQuoter {
quoteExp = quoterToBeImplemented
, quotePat = quoterUndefined
, quoteType = quoterUndefined
, quoteDec = quoterUndefined
}
quoterToBeImplemented _ = [| undefined |]
quoterUndefined _ = undefined
matchDecl = QuasiQuoter {
quoteExp = quoterToBeImplemented
, quotePat = quoterUndefined
, quoteType = quoterUndefined
, quoteDec = quoterUndefined
}
dataDecls :: HS.Module a -> Map.Map String [Decl a]
dataDecls = undefined
instanceDecls :: HS.Module a -> Map.Map String [Decl a]
instanceDecls = undefined
type Matches a = a -> Bool
defParseMode :: ParseMode
defParseMode = defaultParseMode
isStringMatchedToDecl :: String -> DecsQ -> IO (Either MatchingError MatchResult)
isStringMatchedToDecl declarationStr expectedDecl =
case parseDeclWithMode defParseMode declarationStr of
ParseFailed _ errorMessage ->
fail [qc|Can't parse "{declarationStr}", error occurs: {errorMessage}|]
ParseOk hsParseResult -> do
allDecls@(thParseResult:_) <- runQ expectedDecl
when (null allDecls) $ fail [qc|Empty `expectedDecl`|]
when (length allDecls >= 2) $ fail [qc|Too long `expectedDecl` is not supported|]
let result = tryMatch thParseResult hsParseResult
return result
isFileMatchedToDecl :: FilePath -> DecsQ -> IO (Either MatchingError MatchResult)
isFileMatchedToDecl filepath expectedDecl = do
allDecls@(thParseResult:_) <- runQ expectedDecl
when (null allDecls) $ fail [qc|Empty `expectedDecl`|]
when (length allDecls >= 2) $ fail [qc|Too long `expectedDecl` is not supported|]
file <- readFile filepath
let extensions = maybe [] snd $ readExtensions file
case parseModuleWithMode (defParseMode { extensions = extensions }) file of
ParseFailed _ errorMessage ->
fail [qc|Can't parse "{filepath}", error occurs: {errorMessage}|]
ParseOk (HS.Module _ _ _ _ decls) -> do
let someMatchings = rights $ map (tryMatch thParseResult) decls
case someMatchings of
[] -> return $ Left (MatchingError "No matches")
(m:_) -> return $ Right m
tryMatch :: (Show a)
=> TH.Dec
-> HS.Decl a
-> Either MatchingError MatchResult
tryMatch thDecl hsDecl = runExcept $ execStateT (thToHsMatchDeclaration thDecl hsDecl) Map.empty
data MetaVariable = MetaVariableDecl String
| MetaVariableConstructor String
| MetaVariableVar String
| MetaVariableType String
deriving (Eq, Ord, Show)
type MatchResult = Map MetaVariable String
newtype MatchingError = MatchingError String deriving Show
matchError :: String -> MatchState a
matchError = T.lift . throwE . MatchingError
type MatchState a = StateT MatchResult (Except MatchingError) a
addMatch :: MetaVariable -> String -> MatchState ()
addMatch metaVar hsName = get >>= Map.alterF insertKey metaVar >>= put
where
insertKey Nothing = return $ Just hsName
insertKey k@(Just hsName')
| hsName == hsName' = return k
| otherwise = matchError [qc|Var ({metaVar}, {hsName}) already matched|]
zipWithLengthCheckM_ :: (Show a, Show b)
=> String -> (a -> b -> MatchState c) -> [a] -> [b] -> MatchState ()
zipWithLengthCheckM_ listName f as bs
| length as /= length bs = matchError [qc|{listName} list length not matched|]
| otherwise = zipWithM_ f as bs
thToHsMatchDeclaration :: (Show a)
=> TH.Dec
-> HS.Decl a
-> MatchState ()
thToHsMatchDeclaration
(TH.DataD _ thDataName _ _ thConstructors thDerivings)
(HS.DataDecl _ (DataType _) _context (DHead _ hsDataName) hsConstructors hsDerivings) = do
thToHsMatchIdents datatypeDeclMatcher thDataName hsDataName
zipWithLengthCheckM_ "contstructors" thToHsMatchConstructors thConstructors hsConstructors
zipWithLengthCheckM_ "derivings" thToHsMatchDerivings thDerivings hsDerivings
thToHsMatchDeclaration th hs = matchError [qc|Mismatch: {th} =/= {hs}|]
thToHsMatchConstructors :: (Show a)
=> Con
-> QualConDecl a
-> MatchState ()
thToHsMatchConstructors
(TH.RecC thConstructorName thFields)
(HS.QualConDecl _ _ _ (RecDecl _ hsConstructorName hsFields)) = do
thToHsMatchIdents constructorMatcher thConstructorName hsConstructorName
zipWithLengthCheckM_ "fields" thToHsMatchConstructorArgs thFields hsFields
thToHsMatchConstructors th hs = matchError [qc|Constructors mismatch: {th} =/ {hs}|]
thToHsMatchConstructorArgs :: (Show a)
=> TH.VarBangType
-> HS.FieldDecl a
-> MatchState ()
thToHsMatchConstructorArgs
(thName, _thBang, thType)
(HS.FieldDecl _ names@(hsName:_) hsType) = do
when (null names) $ matchError "Number of TH names and HS names not matched"
when (length names > 1) $ matchError "Many names is record declaration not supported"
thToHsMatchIdents varMatcher thName hsName
thToHsMatchTypes thType hsType
thToHsMatchConstructorArgs th hs = matchError [qc|Constructor args mismatch: {th} =/ {hs}|]
thToHsMatchTypes :: (Show a)
=> TH.Type
-> HS.Type a
-> MatchState ()
thToHsMatchTypes
(TH.AppT ListT (ConT tsType))
(HS.TyList _ (TyCon _ hsType')) = do
thToHsMatchIdents typenameMatcher tsType (hsQNameToName hsType')
thToHsMatchTypes
(TH.ConT tsType)
(HS.TyCon _ hsType') = do
thToHsMatchIdents typenameMatcher tsType (hsQNameToName hsType')
thToHsMatchTypes th hs = matchError [qc|Types args mismatch: {th} =/ {hs}|]
thToHsMatchDerivings :: (Show a)
=> TH.DerivClause
-> HS.Deriving a
-> MatchState ()
thToHsMatchDerivings
(TH.DerivClause _ thTypes)
(HS.Deriving _ _ instRules) = do
let thDerivNames = map thDerivTypeToNameStr thTypes
thDerivNames' = sort thDerivNames
hsDerivNames = map hsInstRuleToNameStr instRules
hsDerivNames' = sort hsDerivNames
when (thDerivNames' /= hsDerivNames') $
matchError [qc|Derivings TH: {thDerivNames'} is not matched to {hsDerivNames'}|]
return ()
where
thDerivTypeToNameStr (ConT name) = thUnqualName name
thDerivTypeToNameStr t = error [qc|Unsupported {t}|]
hsInstRuleToNameStr (IRule _ _ _ (IHCon _ qname)) =
case hsQNameToName qname of
Ident _ sname -> sname
Symbol _ sname -> sname
hsInstRuleToNameStr inst = error [qc|Unsupported {inst}|]
thUnqualName (Name (OccName occ) _) = occ
thToHsMatchIdents :: (Show a) => IdentMatcher -> TH.Name -> HS.Name a -> MatchState ()
thToHsMatchIdents matcher thName (Ident _ hsName) =
maybe (matchError [qc|TH: '{thNameStr}' is not matched to '{hsName}'|])
(\(thName', hsName') -> addMatch thName' hsName')
(matcher thNameStr hsName)
where
thNameStr = show thName
thToHsMatchIdents matcher thName hsName = matchError [qc|Mismatch: {thName} {hsName}|]
hsQNameToName :: (Show a) => HS.QName a -> HS.Name a
hsQNameToName (Qual _ _ name) = name
hsQNameToName (UnQual _ name) = name
hsQNameToName t = error [qc|Type identifier '{t}' unsupported|]
type IdentMatcher = String -> String -> Maybe (MetaVariable, String)
datatypeDeclMatcher :: IdentMatcher
datatypeDeclMatcher th hs
| hs `isPrefixOf` th = Just (MetaVariableDecl hs, hs)
| otherwise = Nothing
constructorMatcher :: IdentMatcher
constructorMatcher = templateMatcher False MetaVariableConstructor "C"
varMatcher :: IdentMatcher
varMatcher = templateMatcher False MetaVariableVar "v"
typenameMatcher :: IdentMatcher
typenameMatcher th hs = templateMatcher True MetaVariableType "T" (removeQualificationIfTemplate th) hs
where
removeQualificationIfTemplate name =
case splitOn "." name of
[] -> error "Impossible happened"
[_justOneName] -> name
path -> last path
templateMatcher :: Bool -> (String -> MetaVariable) -> String -> IdentMatcher
templateMatcher exactMatch metavarConstr prefix th hs
| length sth >= (if exactMatch then 2 else 3) && head sth == prefix
= if exactMatch then Just (metavarConstr th, hs)
else Just (metavarConstr $ intercalate "_" (init sth), hs)
| hs `isPrefixOf` th = Just (metavarConstr hs, hs)
| otherwise = Nothing
where
sth = splitOn "_" th