packages feed

nirum-0.2.0: src/Nirum/Cli.hs

{-# LANGUAGE ExtendedDefaultRules, QuasiQuotes, DeriveDataTypeable #-}
module Nirum.Cli (main, writeFiles) where

import Control.Monad (forM_)
import GHC.Exts (IsList(toList))
import System.IO.Error (catchIOError, ioeGetErrorString)

import qualified Data.Map.Strict as M
import qualified Data.Set as S
import qualified Data.Text as T
import qualified Data.Text.IO as TI
import System.Console.CmdArgs.Implicit ( Data
                                       , Typeable
                                       , argPos
                                       , cmdArgs
                                       , explicit
                                       , help
                                       , name
                                       , program
                                       , summary
                                       , typDir
                                       , (&=)
                                       )
import System.Console.CmdArgs.Default (def)
import System.Directory (createDirectoryIfMissing)
import System.FilePath (takeDirectory, (</>))
import Text.InterpolatedString.Perl6 (qq)
import Text.Megaparsec (Token)
import Text.Megaparsec.Error ( Dec
                             , ParseError(errorPos)
                             , parseErrorPretty
                             )
import Text.Megaparsec.Pos (SourcePos(sourceLine, sourceColumn), unPos)

import Nirum.Constructs (Construct(toCode))
import Nirum.Constructs.Identifier (toText)
import Nirum.Constructs.ModulePath (ModulePath)
import Nirum.Package ( PackageError(ParseError, ImportError, ScanError)
                     , ImportError ( CircularImportError
                                   , MissingImportError
                                   , MissingModulePathError
                                   )
                     , scanModules
                     , scanPackage
                     )
import Nirum.Targets.Python (compilePackage)
import Nirum.Version (versionString)

data NirumCli = NirumCli { sourcePath :: FilePath
                         , objectPath :: FilePath
                         } deriving (Show, Data, Typeable)

parseErrortoPrettyMessage :: ParseError (Token T.Text) Dec
                          -> FilePath
                          -> IO String
parseErrortoPrettyMessage parseError' filePath' = do
    sourceCode <- readFile filePath'
    let sourceLines = lines sourceCode
        sl = if length sourceLines < errorLine then ""
             else sourceLines !! (errorLine - 1)
    return [qq|
{parseErrorPretty $ parseError'}
$sl
{arrow}
|]
  where
    error' :: SourcePos
    error' = head $ toList $ errorPos parseError'
    errorLine :: Int
    errorLine = fromEnum $ unPos $ sourceLine error'
    errorColumn :: Int
    errorColumn = fromEnum $ unPos $ sourceColumn error'
    arrow :: T.Text
    arrow = T.snoc (T.concat (replicate (errorColumn - 1) (T.pack " "))) '^'

toModuleNameText :: T.Text -> T.Text
toModuleNameText t = [qq|'{t}'|]

modulePathToRepr :: ModulePath -> T.Text
modulePathToRepr = toModuleNameText . toCode

importErrorToPrettyMessage :: ImportError -> T.Text
importErrorToPrettyMessage (CircularImportError modulePaths) =
    [qq|Circular import detected in following orders: $order|]
  where
    circularModulesText :: [ModulePath] -> [T.Text]
    circularModulesText = map modulePathToRepr
    order :: T.Text
    order = T.intercalate " > " $ circularModulesText modulePaths
importErrorToPrettyMessage (MissingModulePathError path path') =
    [qq|No module named $dataName in $moduleName|]
  where
    moduleName :: T.Text
    moduleName = modulePathToRepr path
    dataName :: T.Text
    dataName = modulePathToRepr path'
importErrorToPrettyMessage (MissingImportError path path' identifier) =
    [qq|Cannot import $importText from $attrText in $foundText|]
  where
    importText :: T.Text
    importText = (toModuleNameText . toText) identifier
    foundText :: T.Text
    foundText = modulePathToRepr path
    attrText :: T.Text
    attrText = modulePathToRepr path'


importErrorsToMessageList :: S.Set ImportError -> [T.Text]
importErrorsToMessageList importErrors =
    S.toList $ S.map importErrorToPrettyMessage importErrors
importErrorsToPrettyMessage :: S.Set ImportError -> String
importErrorsToPrettyMessage importErrors =
    T.unpack $ T.intercalate "\n" withListStyleText
  where
    withListStyleText :: [T.Text]
    withListStyleText =
        map (T.append "- ") (importErrorsToMessageList importErrors)

nirumCli :: NirumCli
nirumCli = NirumCli { objectPath = def &= explicit
                          &= name "o" &= name "output-dir" &= typDir
                          &= help "The directory to place object files"
                    , sourcePath = def &= argPos 1 &= typDir
                    }
         &= program "nirum"
         &= summary ("Nirum Compiler " ++ versionString)

main' :: IO ()
main' = do
    NirumCli src obj <- cmdArgs nirumCli
    scanResult <- scanPackage src
    case scanResult of
        Left (ParseError modulePath error') -> do
            -- FIXME: find more efficient way to determine filename from
            --        the given module path
            filePaths <- scanModules src
            case M.lookup modulePath filePaths of
                Just filePath' -> do
                    m <- parseErrortoPrettyMessage error' filePath'
                    putStrLn m
                Nothing -> putStrLn [qq|$modulePath not found|]
        Left (ImportError importErrors) ->
            putStrLn [qq|Import error:
{importErrorsToPrettyMessage importErrors}
|]
        Left (ScanError _ error') -> putStrLn [qq|Scan error: $error'|]
        Right pkg -> writeFiles obj $ compilePackage pkg

writeFiles :: FilePath -> M.Map FilePath (Either T.Text T.Text) -> IO ()
writeFiles obj m =
    forM_ files $ \(filePath, result) ->
        case result of
            Left compileError -> putStrLn [qq|error: $filePath: $compileError|]
            Right code -> do
                createDirectoryIfMissing True $ takeDirectory filePath
                putStrLn filePath
                TI.writeFile filePath code
  where
    files :: [(FilePath, Either T.Text T.Text)]
    files = [(obj </> f, r) | (f, r) <- M.toList m]

main :: IO ()
main = catchIOError main' $ putStrLn . ioeGetErrorString