{-# LANGUAGE TupleSections #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE OverloadedStrings #-} {-# OPTIONS -Wall -fno-warn-orphans #-} -- | Miscellaneous functions used throughout the compiler. module Fay.Compiler.Misc where import qualified Fay.Compiler.ModuleScope as ModuleScope import Fay.Types import Control.Applicative import Control.Monad.Error import Control.Monad.IO import Control.Monad.RWS import Data.List import Data.Maybe import qualified Data.Set as S import Data.String import Data.Version (parseVersion) import Language.Haskell.Exts.Syntax import Language.Haskell.Exts.Parser import Language.Haskell.Exts.Pretty import Language.Haskell.Exts.Extension import Prelude hiding (exp, mod) import System.Directory import System.FilePath import System.IO import System.Process (readProcess) import Text.ParserCombinators.ReadP (readP_to_S) -- | Extra the string from an ident. unname :: Name -> String unname (Ident str) = str unname _ = error "Expected ident from uname." -- FIXME: -- | Make an identifier from the built-in HJ module. fayBuiltin :: String -> QName fayBuiltin = Qual (ModuleName "Fay$") . Ident -- | Wrap an expression in a thunk. thunk :: JsExp -> JsExp -- thunk exp = JsNew (fayBuiltin "Thunk") [JsFun [] [] (Just exp)] thunk expr = case expr of -- JS constants don't need to be in thunks, they're already strict. JsLit{} -> expr -- Functions (e.g. lets) used for introducing a new lexical scope -- aren't necessary inside a thunk. This is a simple aesthetic -- optimization. JsApp fun@JsFun{} [] -> JsNew JsThunk [fun] -- Otherwise make a regular thunk. _ -> JsNew JsThunk [JsFun [] [] (Just expr)] -- | Wrap an expression in a thunk. stmtsThunk :: [JsStmt] -> JsExp stmtsThunk stmts = JsNew JsThunk [JsFun [] stmts Nothing] -- | Generate unique names. uniqueNames :: [JsName] uniqueNames = map JsParam [1::Integer ..] -- | Resolve a given maybe-qualified name to a fully qualifed name. resolveName :: QName -> Compile QName resolveName special@Special{} = return special resolveName q@Qual{} = do env <- gets stateModuleScope maybe (throwError $ UnableResolveQualified q) return (ModuleScope.resolveName q env) resolveName u@(UnQual name) = do names <- gets stateLocalScope env <- gets stateModuleScope if S.member name names then return (UnQual name) else maybe (qualify name) return (ModuleScope.resolveName u env) -- | Qualify a name for the current module. qualify :: Name -> Compile QName qualify name = do modulename <- gets stateModuleName return (Qual modulename name) -- | Make a top-level binding. bindToplevel :: SrcLoc -> Bool -> Name -> JsExp -> Compile JsStmt bindToplevel srcloc toplevel name expr = do qname <- (if toplevel then qualify else return . UnQual) name return (JsMappedVar srcloc (JsNameVar qname) expr) -- | Create a temporary environment and discard it after the given computation. withModuleScope :: Compile a -> Compile a withModuleScope m = do scope <- gets stateModuleScope value <- m modify $ \s -> s { stateModuleScope = scope } return value -- | Create a temporary scope and discard it after the given computation. withScope :: Compile a -> Compile a withScope m = do scope <- gets stateLocalScope value <- m modify $ \s -> s { stateLocalScope = scope } return value -- | Run a compiler and just get the scope information. generateScope :: Compile a -> Compile () generateScope m = do st <- get _ <- m scope <- gets stateLocalScope put st { stateLocalScope = scope } -- | Bind a variable in the current scope. bindVar :: Name -> Compile () bindVar name = do modify $ \s -> s { stateLocalScope = S.insert name (stateLocalScope s) } -- | Emit exported names. emitExport :: ExportSpec -> Compile () emitExport spec = case spec of EVar (UnQual n) -> emitVar n EVar q@Qual{} -> modify $ addCurrentExport q EThingAll (UnQual name) -> do emitVar name r <- lookup (UnQual name) <$> gets stateRecords maybe (return ()) (mapM_ (emitVar . unQName)) r EThingWith (UnQual name) ns -> do emitVar name mapM_ emitCName ns EAbs _ -> return () -- Type only, skip EModuleContents mod -> mapM_ (emitExport . EVar) =<< ModuleScope.moduleLocals mod <$> gets stateModuleScope -- Skip qualified exports for type exports in fay-base since -- qualified imports are not supported yet an error will be thrown -- on the import so hopefully this won't be confusing. EThingAll (Qual _ _) -> return () e -> do liftIO (print e) throwError $ UnsupportedExportSpec e where emitVar = return . UnQual >=> resolveName >=> emitExport . EVar emitCName (VarName n) = emitVar n emitCName (ConName n) = emitVar n unQName (UnQual u) = u unQName _ = error "unQName Qual or Special -- should never happen" -- | Force an expression in a thunk. force :: JsExp -> JsExp force expr | isConstant expr = expr | otherwise = JsApp (JsName JsForce) [expr] -- | Is a JS expression a literal (constant)? isConstant :: JsExp -> Bool isConstant JsLit{} = True isConstant _ = False -- | Deconstruct a parse result (a la maybe, foldr, either). parseResult :: ((SrcLoc,String) -> b) -> (a -> b) -> ParseResult a -> b parseResult die ok result = case result of ParseOk a -> ok a ParseFailed srcloc msg -> die (srcloc,msg) -- | Get a config option. config :: (CompileConfig -> a) -> Compile a config f = asks (f . readerConfig) -- | Optimize pattern matching conditions by merging conditions in common. optimizePatConditions :: [[JsStmt]] -> [[JsStmt]] optimizePatConditions = concatMap merge . groupBy sameIf where sameIf [JsIf cond1 _ _] [JsIf cond2 _ _] = cond1 == cond2 sameIf _ _ = False merge xs@([JsIf cond _ _]:_) = [[JsIf cond (concat (optimizePatConditions (map getIfConsequent xs))) []]] merge noifs = noifs getIfConsequent [JsIf _ cons _] = cons getIfConsequent other = other -- | Throw a JS exception. throw :: String -> JsExp -> JsStmt throw msg expr = JsThrow (JsList [JsLit (JsStr msg),expr]) -- | Throw a JS exception (in an expression). throwExp :: String -> JsExp -> JsExp throwExp msg expr = JsThrowExp (JsList [JsLit (JsStr msg),expr]) -- | Is an alt a wildcard? isWildCardAlt :: Alt -> Bool isWildCardAlt (Alt _ pat _ _) = isWildCardPat pat -- | Is a pattern a wildcard? isWildCardPat :: Pat -> Bool isWildCardPat PWildCard{} = True isWildCardPat PVar{} = True isWildCardPat _ = False -- | Generate a temporary, SCOPED name for testing conditions and -- such. withScopedTmpJsName :: (JsName -> Compile a) -> Compile a withScopedTmpJsName withName = do depth <- gets stateNameDepth modify $ \s -> s { stateNameDepth = depth + 1 } ret <- withName $ JsTmp depth modify $ \s -> s { stateNameDepth = depth } return ret -- | Generate a temporary, SCOPED name for testing conditions and -- such. We don't have name tracking yet, so instead we use this. withScopedTmpName :: (Name -> Compile a) -> Compile a withScopedTmpName withName = do depth <- gets stateNameDepth modify $ \s -> s { stateNameDepth = depth + 1 } ret <- withName $ Ident $ "$gen" ++ show depth modify $ \s -> s { stateNameDepth = depth } return ret -- | Print out a compiler warning. warn :: String -> Compile () warn "" = return () warn w = do shouldWarn <- config configWarn when shouldWarn . liftIO . hPutStrLn stderr $ "Warning: " ++ w -- | Pretty print a source location. printSrcLoc :: SrcLoc -> String printSrcLoc SrcLoc{..} = srcFilename ++ ":" ++ show srcLine ++ ":" ++ show srcColumn -- | Lookup the record for a given type name. typeToRecs :: QName -> Compile [QName] typeToRecs typ = fromMaybe [] . lookup typ <$> gets stateRecordTypes -- | Get the fields for a given type. typeToFields :: QName -> Compile [QName] typeToFields typ = do allrecs <- gets stateRecords typerecs <- typeToRecs typ return . concatMap snd . filter ((`elem` typerecs) . fst) $ allrecs -- | Get the flag used for GHC, this differs between GHC-7.6.0 and -- GHC-everything-else so we need to specially test for that. It's -- lame, but that's random flag name changes for you. getGhcPackageDbFlag :: IO String getGhcPackageDbFlag = do s <- readProcess "ghc" ["--version"] "" return $ case (mapMaybe readVersion $ words s, readVersion "7.6.0") of (v:_, Just min') | v > min' -> "-package-db" _ -> "-package-conf" where readVersion = listToMaybe . filter (null . snd) . readP_to_S parseVersion -- | Find an import's filepath and contents from its module name. findImport :: [FilePath] -> ModuleName -> Compile (FilePath,String) findImport alldirs mname = go alldirs mname where go (dir:dirs) name = do exists <- io (doesFileExist path) if exists then fmap (path,) (fmap stdlibHack (io (readFile path))) else go dirs name where path = dir replace '.' '/' (prettyPrint name) ++ ".hs" replace c r = map (\x -> if x == c then r else x) go [] name = throwError $ Couldn'tFindImport name alldirs stdlibHack | mname == ModuleName "Language.Fay.Stdlib" = \s -> s ++ "\n\ndata Maybe a = Just a | Nothing" | mname == ModuleName "Language.Fay.FFI" = const "module Language.Fay.FFI where\n\ndata Nullable a = Nullable a | Null\n\ndata Defined a = Defined a | Undefined" | otherwise = id -- | Convert a GADT to a normal data type. convertGADT :: GadtDecl -> QualConDecl convertGADT d = case d of GadtDecl srcloc name typ -> QualConDecl srcloc tyvars context (ConDecl name (convertFunc typ)) where tyvars = [] context = [] convertFunc (TyCon _) = [] convertFunc (TyFun x xs) = UnBangedTy x : convertFunc xs convertFunc (TyParen x) = convertFunc x convertFunc _ = [] -- | Run the compiler. runCompile :: CompileReader -> CompileState -> Compile a -> IO (Either CompileError (a,CompileState,CompileWriter)) runCompile reader' state' m = runErrorT (runRWST (unCompile m) reader' state') -- | Parse some Fay code. parseFay :: Parseable ast => FilePath -> String -> ParseResult ast parseFay filepath = parseWithMode parseMode { parseFilename = filepath } . applyCPP -- | Apply incredibly simplistic CPP handling. It only recognizes the following: -- -- > #if FAY -- > #ifdef FAY -- > #ifndef FAY -- > #else -- > #endif -- -- Note that this implementation replaces all removed lines with blanks, so -- that line numbers remain accurate. applyCPP :: String -> String applyCPP = unlines . loop NoCPP . lines where loop _ [] = [] loop state' ("#if FAY":rest) = "" : loop (CPPIf True state') rest loop state' ("#ifdef FAY":rest) = "" : loop (CPPIf True state') rest loop state' ("#ifndef FAY":rest) = "" : loop (CPPIf False state') rest loop (CPPIf b oldState') ("#else":rest) = "" : loop (CPPElse (not b) oldState') rest loop (CPPIf _ oldState') ("#endif":rest) = "" : loop oldState' rest loop (CPPElse _ oldState') ("#endif":rest) = "" : loop oldState' rest loop state' (x:rest) = (if toInclude state' then x else "") : loop state' rest toInclude NoCPP = True toInclude (CPPIf x state') = x && toInclude state' toInclude (CPPElse x state') = x && toInclude state' -- | The CPP's parsing state. data CPPState = NoCPP | CPPIf Bool CPPState | CPPElse Bool CPPState -- | The parse mode for Fay. parseMode :: ParseMode parseMode = defaultParseMode { extensions = [GADTs ,StandaloneDeriving ,PackageImports ,EmptyDataDecls ,TypeOperators ,RecordWildCards ,NamedFieldPuns] }