packages feed

lentil-1.3.2.0: src/Lentil/Parse/Run.hs

-----------------------------------------------------------------------------
-- |
-- Module      :  Lentil.Parse.Run
-- Copyright   :  © 2015 Francesco Ariis
-- License     :  GPLv3 (see the LICENSE file)
--
-- Parsing functions interface
-----------------------------------------------------------------------------

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}

module Lentil.Parse.Run where

import Lentil.Types
import Lentil.Helpers
import Lentil.Parse.Issue
import Lentil.Parse.Source
import Lentil.Parse.Syntaxes

import Text.Megaparsec

import qualified Control.Exception    as E
import qualified Control.DeepSeq      as DS
import qualified Control.Monad        as CM
import qualified Control.Monad.Reader as R
import qualified Control.Monad.Trans  as T
import qualified Data.DList           as D
import qualified Data.IORef           as I
import qualified Data.Text            as T
import qualified Data.Text.IO         as TI
import qualified Data.Text.Lazy       as TL
import qualified System.Directory     as D
import qualified System.ProgressBar   as PB

-----------
-- TYPES --
-----------

data RunEnv = RunEnv
            { -- reader part
              reAliases :: [Alias],    -- for parsing purposes
              reFWords  :: [FlagWord], -- for parsing purposes

              -- stateful part
              reFiles   :: I.IORef [FilePath],      -- fnames to process
              reIssues  :: I.IORef (D.DList Issue), -- processed issues
              reBar     :: PB.ProgressBar () }      -- progress bar

newtype Runner a = Runner { runRunner :: R.ReaderT RunEnv IO a }
            deriving (Functor, Applicative, Monad,
                      T.MonadIO, R.MonadReader RunEnv)


issueFinder :: [Alias] -> [FlagWord] -> [FilePath] -> IO [Issue]
issueFinder as fws fps =
            creaEnv >>= \e ->
            let r = runRunner runnerFinder in
            R.runReaderT r e
    where
          creaEnv :: IO RunEnv
          creaEnv =
                let bIO = initProgressBar (length fps) in
                I.newIORef fps     >>= \ifs ->
                I.newIORef D.empty >>= \iis ->
                R.liftIO bIO       >>= \pb ->
                return (RunEnv as fws
                               ifs iis pb)


runnerFinder :: Runner [Issue]
runnerFinder = looperino                    >>
               D.toList <$> asksIO reIssues
    where
          looperino :: Runner ()
          looperino =
                asksIO reFiles >>= \case
                  [] -> return ()
                  _  -> processFile >>
                        looperino

processFile :: Runner ()
processFile = -- 1. does exist
              fetchFile        >>= \fp ->
              doesFileExist fp >>= \bf ->
              if not bf
                then return ()
              else

              -- 2. execute parse (and store results)
              parseFile fp >>

              -- 3. update counter and blit it
              blitCounter


-------------
-- PARSING --
-------------

parseFile :: FilePath -> Runner ()
parseFile fp =
        -- 1. language parser finder
        R.asks reAliases >>= \as ->
        case langParserAlias as fp of
          Nothing -> return ()
          Just p  -> -- 2. parse comments
                     parseComments fp p >>= \cs ->
                     -- 3. parse issues (and write them)
                     mapM_ (parseIssues fp) cs

parseComments :: FilePath -> StateParser () [CommentString] ->
                 Runner [Comment]
parseComments fp p =
            T.liftIO (safeRead fp) >>= \t ->
            runPar (comms2Tuple <$> p) fp () t
            -- meglio di
            -- comms2Tuple <$> runPar p fp () t
            -- così evitiamo istanza deepseq per CommentString

-- from a single comment
parseIssues :: FilePath -> (Row, String) -> Runner ()
parseIssues fp (r, cs) =
            -- parse
            R.asks reFWords         >>= \fws ->
            runPar issPar fp fws cs >>= \dis ->

            -- write
            let appd bd = D.append bd dis      in
            R.asks reIssues                    >>= \iis ->
            T.liftIO (I.modifyIORef' iis appd)
    where
          issPar :: StateParser [FlagWord] (D.DList Issue)
          issPar = D.fromList <$> (setRow r >> issues)

-- generic parsing
runPar :: (Monoid o, DS.NFData o) =>
          StateParser s o -> FilePath -> s -> String -> Runner o
runPar p fp s i =
        case runStateParser p s fp i of
          Left l  -> rperr (fp ++ " : parse error " ++
                            errorBundlePretty l)       >>
                     return mempty
          Right r -> return (DS.deepseq r r)

-- todo [refactor] Row should be carried on by issues, not be manually set!
setRow :: Row -> ParIssue ()
setRow r = updateParserState
             (\(State i o (PosState pix po (SourcePos n _ cx) tw lp) es) ->
                let l' = mkPos r in
               State i o (PosState pix po (SourcePos n l' cx) tw lp) es)


------------------
-- PROGRESS BAR --
------------------

blitCounter :: Runner ()
blitCounter = R.asks reBar                   >>= \pb ->
              T.liftIO (PB.incProgress pb 1)

-- t: total issues
initProgressBar :: Int -> IO (PB.ProgressBar ())
initProgressBar t = PB.newProgressBar myBar 2 (PB.Progress 0 t ())
    where
          myBar = PB.defStyle {
                    PB.styleDone = '*',
                    PB.styleCurrent = '*',
                    PB.styleTodo = ' ',
                    PB.styleWidth = PB.ConstantWidth 40,
                    PB.stylePrefix = PB.msg barMes
                    }

          barMes = TL.pack $ show t ++ " source files"


-----------------
-- ANCILLARIES --
-----------------

asksIO :: (RunEnv -> I.IORef a) -> Runner a
asksIO a = R.asks a                >>= \ia ->
           T.liftIO (I.readIORef ia)

fetchFile :: Runner FilePath
fetchFile = asksIO reFiles >>= \case
              []     -> error "(assert) fetchFile called on empty filelist!"
              (f:fs) -> R.asks reFiles               >>= \ifs ->
                        T.liftIO (I.writeIORef ifs fs) >>
                        return f

doesFileExist :: FilePath -> Runner Bool
doesFileExist fp =
            T.liftIO (D.doesFileExist fp)             >>= \fb ->
            CM.unless fb
                    (rperr $ fp ++ " : no such file") >>
            return fb

rperr :: String -> Runner ()
rperr s = T.liftIO $ perr s

safeRead :: FilePath -> IO String
safeRead fp = E.try (TI.readFile fp) >>= \case
                Right t -> return (T.unpack t)
                Left x  -> perr (fp ++ " : " ++ show (x :: E.IOException) ++
                                 " -- ignoring file") >>
                           return ""