{- sv2v - Author: Zachary Snow <zach@zachjs.com> -} module Language.SystemVerilog.Parser ( parseFiles , Config(..) ) where import Control.Monad (when) import Control.Monad.IO.Class (liftIO) import Control.Monad.Except import Data.List (elemIndex) import Data.Maybe (catMaybes) import System.Directory (findFile) import qualified Data.Map.Strict as Map import qualified Data.Set as Set import Language.SystemVerilog.AST (AST) import Language.SystemVerilog.Parser.Lex (lexStr) import Language.SystemVerilog.Parser.Parse (parse) import Language.SystemVerilog.Parser.Preprocess (preprocess, annotate, Env, Contents) type Output = (FilePath, AST) type Strings = Set.Set String data Config = Config { cfDefines :: [String] , cfIncludePaths :: [FilePath] , cfLibraryPaths :: [FilePath] , cfSiloed :: Bool , cfSkipPreprocessor :: Bool , cfOversizedNumbers :: Bool } data Context = Context { ctConfig :: Config , ctEnv :: Env , ctUsed :: Strings , ctHave :: Strings } -- parse CLI macro definitions into the internal macro environment format initialEnv :: [String] -> Env initialEnv = Map.map (, []) . Map.fromList . map splitDefine -- split a raw CLI macro definition at the '=', if present splitDefine :: String -> (String, String) splitDefine str = case elemIndex '=' str of Nothing -> (str, "") Just idx -> (name, tail rest) where (name, rest) = splitAt idx str -- parse a list of files according to the given configuration parseFiles :: Config -> [FilePath] -> ExceptT String IO [Output] parseFiles config = parseFiles' context . zip (repeat "") where context = Context config env mempty mempty env = initialEnv $ cfDefines config -- parse files, keeping track of which parts are defined and used parseFiles' :: Context -> [(String, FilePath)] -> ExceptT String IO [Output] -- look for missing parts in libraries if any library paths were provided parseFiles' context [] | null libdirs = return [] | otherwise = do possibleFiles <- catMaybes <$> mapM lookupLibrary missingParts if null possibleFiles then return [] else parseFiles' context possibleFiles where missingParts = Set.toList $ ctUsed context Set.\\ ctHave context libdirs = cfLibraryPaths $ ctConfig context lookupLibrary partName = ((partName, ) <$>) <$> lookupLibFile partName lookupLibFile = liftIO . findFile libdirs . (++ ".sv") -- load the files, but complain if an expected part is missing parseFiles' context ((part, path) : files) = do (context', ast) <- parseFile context path let misdirected = not $ null part || Set.member part (ctHave context') when misdirected $ throwError $ "Expected to find module or interface " ++ show part ++ " in file " ++ show path ++ " selected from the library path." ((path, ast) :) <$> parseFiles' context' files -- parse an individual file, updating the context parseFile :: Context -> FilePath -> ExceptT String IO (Context, AST) parseFile context path = do (context', contents) <- preprocessFile context path tokens <- liftEither $ runExcept $ lexStr contents (ast, used, have) <- parse (cfOversizedNumbers config) tokens let context'' = context' { ctUsed = used <> ctUsed context , ctHave = have <> ctHave context } return (context'', ast) where config = ctConfig context -- preprocess an individual file, potentially updating the environment preprocessFile :: Context -> FilePath -> ExceptT String IO (Context, Contents) preprocessFile context path | cfSkipPreprocessor config = (context, ) <$> annotate path | otherwise = do (env', contents) <- preprocess (cfIncludePaths config) env path let context' = context { ctEnv = if cfSiloed config then env else env' } return (context', contents) where config = ctConfig context env = ctEnv context