project-m36-0.2: src/lib/ProjectM36/ScriptSession.hs
module ProjectM36.ScriptSession where
import ProjectM36.Error
import Control.Exception
import Control.Monad
import System.IO.Error
import System.Directory
import Control.Monad.IO.Class
import System.FilePath.Glob
import System.FilePath
import Data.Text (Text, unpack)
import Data.Maybe
import GHC
import GHC.Paths (libdir)
#if __GLASGOW_HASKELL__ >= 800
import GHC.LanguageExtensions
#endif
import DynFlags
import Panic
import Outputable --hiding ((<>))
import PprTyThing
import Unsafe.Coerce
import Type hiding (pprTyThing)
data ScriptSession = ScriptSession {
hscEnv :: HscEnv,
atomFunctionBodyType :: Type,
dbcFunctionBodyType :: Type
}
newtype ScriptSessionError = ScriptSessionLoadError GhcException
deriving (Show)
-- | Configure a GHC environment/session which we will use for all script compilation.
initScriptSession :: [String] -> IO (Either ScriptSessionError ScriptSession)
initScriptSession ghcPkgPaths = do
--for the sake of convenience, for developers' builds, include the local cabal sandbox package database and the cabal new-build package database
eHomeDir <- tryJust (guard . isDoesNotExistError) getHomeDirectory
let homeDir = either (const "/") id eHomeDir
let excHandler exc = pure $ Left (ScriptSessionLoadError exc)
handleGhcException excHandler $ runGhc (Just libdir) $ do
dflags <- getSessionDynFlags
let ghcVersion = projectVersion dflags
sandboxPkgPaths <- liftIO $ concat <$> mapM glob [
"./dist-newstyle/packagedb/ghc-" ++ ghcVersion,
".cabal-sandbox/*ghc-" ++ ghcVersion ++ "-packages.conf.d",
homeDir </> ".cabal/store/ghc-" ++ ghcVersion ++ "/package.db"
]
let localPkgPaths = map PkgConfFile (ghcPkgPaths ++ sandboxPkgPaths)
let dflags' = applyGopts . applyXopts $ dflags { hscTarget = HscInterpreted ,
ghcLink = LinkInMemory,
safeHaskell = Sf_Trustworthy,
safeInfer = True,
safeInferred = True,
--verbosity = 3,
#if __GLASGOW_HASKELL__ >= 800
trustFlags = map TrustPackage required_packages,
#endif
packageFlags = packageFlags dflags ++ packages,
extraPkgConfs = const (localPkgPaths ++ [UserPkgConf, GlobalPkgConf])
}
applyGopts flags = foldl gopt_set flags gopts
applyXopts flags = foldl xopt_set flags xopts
#if __GLASGOW_HASKELL__ >= 800
xopts = [OverloadedStrings, ExtendedDefaultRules, ImplicitPrelude, ScopedTypeVariables]
#else
xopts = [Opt_OverloadedStrings, Opt_ExtendedDefaultRules, Opt_ImplicitPrelude, Opt_ScopedTypeVariables]
#endif
gopts = [] --[Opt_DistrustAllPackages, Opt_PackageTrust]
required_packages = ["base",
"containers",
"Glob",
"directory",
"unordered-containers",
"hashable",
"uuid",
"vector",
"text",
"binary",
"vector-binary-instances",
"time",
"project-m36",
"bytestring"]
#if __GLASGOW_HASKELL__ >= 800
packages = map (\m -> ExposePackage ("-package " ++ m) (PackageArg m) (ModRenaming True [])) required_packages
#else
packages = map TrustPackage required_packages
#endif
--liftIO $ traceShowM (showSDoc dflags' (ppr packages))
_ <- setSessionDynFlags dflags'
let safeImportDecl mn mQual = ImportDecl {
ideclSourceSrc = Nothing,
ideclName = noLoc mn,
ideclPkgQual = Nothing,
ideclSource = False,
ideclSafe = True,
ideclImplicit = False,
ideclQualified = isJust mQual,
ideclAs = mQual,
ideclHiding = Nothing
}
unqualifiedModules = map (\modn -> IIDecl $ safeImportDecl (mkModuleName modn) Nothing) [
"Prelude",
"Data.Map",
"Data.Either",
"Data.Time.Calendar",
"Control.Monad.State",
"ProjectM36.Base",
"ProjectM36.Relation",
"ProjectM36.AtomFunctionError",
"ProjectM36.DatabaseContextFunctionError",
"ProjectM36.DatabaseContextFunctionUtils",
"ProjectM36.RelationalExpression"]
qualifiedModules = map (\(modn, qualNam) -> IIDecl $ safeImportDecl (mkModuleName modn) (Just (mkModuleName qualNam))) [
("Data.Text", "T")
]
setContext (unqualifiedModules ++ qualifiedModules)
env <- getSession
atomFuncType <- mkTypeForName "AtomFunctionBodyType"
dbcFuncType <- mkTypeForName "DatabaseContextFunctionBodyType"
pure (Right (ScriptSession env atomFuncType dbcFuncType))
addImport :: String -> Ghc ()
addImport moduleNam = do
ctx <- getContext
setContext ( (IIDecl $ simpleImportDecl (mkModuleName moduleNam)) : ctx )
showType :: DynFlags -> Type -> String
showType dflags ty = showSDocForUser dflags alwaysQualify (pprTypeForUser ty)
mkTypeForName :: String -> Ghc Type
mkTypeForName name = do
lBodyName <- parseName name
case lBodyName of
[] -> error ("failed to parse " ++ name)
_:_:_ -> error "too many name matches"
[bodyName] -> do
mThing <- lookupName bodyName
case mThing of
Nothing -> error ("failed to find " ++ name)
Just (ATyCon tyCon) -> case synTyConRhs_maybe tyCon of
Just typ -> pure typ
Nothing -> error (name ++ " is not a type synonym")
Just _ -> error ("failed to find type synonym " ++ name)
compileScript :: Type -> Text -> Ghc (Either ScriptCompilationError a)
compileScript funcType script = do
let sScript = unpack script
mErr <- typeCheckScript funcType script
case mErr of
Just err -> pure (Left err)
Nothing -> do
--catch exception here
--we could potentially wrap the script with Atom pattern matching so that the script doesn't have to do it, but the change to an Atom ADT should make it easier. Still, it would be nice if the script didn't have to handle a list of arguments, for example.
-- we can't use dynCompileExpr here because
func <- compileExpr sScript
pure $ Right (unsafeCoerce func)
typeCheckScript :: Type -> Text -> Ghc (Maybe ScriptCompilationError)
typeCheckScript expectedType inp = do
dflags <- getSessionDynFlags
--catch exception for SyntaxError
funcType <- GHC.exprType (unpack inp)
--liftIO $ putStrLn $ showType dflags expectedType ++ ":::" ++ showType dflags funcType
if eqType funcType expectedType then
pure Nothing
else
pure (Just (TypeCheckCompilationError (showType dflags expectedType) (showType dflags funcType)))