nirum-0.4.0: src/Nirum/Targets/Python/CodeGen.hs
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Nirum.Targets.Python.CodeGen
( Code
, CodeGen
, CodeGenContext (..)
, CompileError
, Python (..)
, PythonVersion (..)
, RenameMap
, addDependency
, addOptionalDependency
, baseIntegerClass
, baseStringClass
, collectionsAbc
, empty
, getPythonVersion
, importBuiltins
, importStandardLibrary
, importTypingForPython3
, indent
, insertLocalImport
, insertStandardImport
, insertStandardImportA
, insertThirdPartyImports
, insertThirdPartyImportsA
, keywords
, localImportsMap
, mangleVar
, minimumRuntime
, renameModulePath
, renameModulePath'
, runCodeGen
, stringLiteral
, toAttributeName
, toAttributeName'
, toBehindSnakeCaseText
, toClassName
, toClassName'
, toImportPath'
, toImportPath
, toImportPaths
) where
import Control.Monad.State
import Data.Maybe
import Data.Typeable
import GHC.Exts
import Data.Map.Strict hiding (empty, member, toAscList)
import Data.SemVer hiding (Identifier)
import Data.Set hiding (empty)
import qualified Data.Set
import Data.Text hiding (empty)
import qualified Data.Text
import Data.Text.Lazy (toStrict)
import qualified Data.Text.Lazy
import Text.Blaze (ToMarkup (preEscapedToMarkup))
import Text.Blaze.Renderer.Text (renderMarkup)
import Text.InterpolatedString.Perl6 (qq)
import Text.Printf (printf)
import qualified Nirum.CodeGen
import Nirum.Constructs.Identifier
import Nirum.Constructs.ModulePath
import Nirum.Constructs.Name
minimumRuntime :: Version
minimumRuntime = version 0 6 0 [] []
-- | The set of Python reserved keywords.
-- See also: https://docs.python.org/3/reference/lexical_analysis.html#keywords
keywords :: Set Code
keywords = [ "False", "None", "True"
, "and", "as", "assert", "break", "class", "continue"
, "def", "del" , "elif", "else", "except", "finally"
, "for", "from", "global", "if", "import", "in", "is"
, "lambda", "nonlocal", "not", "or", "pass", "raise"
, "return", "try", "while", "with", "yield"
]
type RenameMap = Map ModulePath ModulePath
data Python = Python { packageName :: Text
, minimumRuntimeVersion :: Version
, renames :: RenameMap
, classifiers :: [Text]
} deriving (Eq, Ord, Show, Typeable)
data PythonVersion = Python2
| Python3
deriving (Eq, Ord, Show)
type CompileError = Text
type Code = Text
data CodeGenContext
= CodeGenContext { standardImports :: Map Text Text
, standardImportSet :: Set Text
, thirdPartyImports :: Map Text (Map Text Text)
, localImports :: Map Text (Set Text)
, pythonVersion :: PythonVersion
, dependencies :: Set Text
, optionalDependencies :: Map (Int, Int) (Set Text)
, globalDefinitions :: Set Code
}
deriving (Eq, Ord, Show)
instance Nirum.CodeGen.Failure CodeGenContext CompileError where
fromString = return . pack
empty :: PythonVersion -> CodeGenContext
empty pythonVer = CodeGenContext
{ standardImports = []
, standardImportSet = []
, thirdPartyImports = []
, localImports = []
, pythonVersion = pythonVer
, dependencies = []
, optionalDependencies = []
, globalDefinitions = []
}
localImportsMap :: CodeGenContext -> Map Text (Map Text Text)
localImportsMap CodeGenContext { localImports = imports } =
Data.Map.Strict.map (Data.Map.Strict.fromSet id) imports
type CodeGen = Nirum.CodeGen.CodeGen CodeGenContext CompileError
runCodeGen :: CodeGen a
-> CodeGenContext
-> (Either CompileError a, CodeGenContext)
runCodeGen = Nirum.CodeGen.runCodeGen
importStandardLibrary :: Text -> CodeGen Code
importStandardLibrary module' = do
insertStandardImportA alias module'
return alias
where
alias :: Code
alias
| "_" `isPrefixOf` module' = module'
| otherwise = '_' `cons` Data.Text.replace "." "_" module'
importBuiltins :: CodeGen Code
importBuiltins = do
pyVer <- getPythonVersion
case pyVer of
Python3 -> do
insertStandardImportA "__builtin__" "builtins"
return "__builtin__"
Python2 -> importStandardLibrary "__builtin__"
insertStandardImport :: Text -> CodeGen ()
insertStandardImport module' =
insertStandardImportA module' module'
insertStandardImportA :: Code -> Text -> CodeGen ()
insertStandardImportA alias module' = modify insert'
where
insert' c@CodeGenContext { standardImports = si, standardImportSet = ss } =
c { standardImports = Data.Map.Strict.insert alias module' si
, standardImportSet = Data.Set.insert module' ss
}
insertThirdPartyImports :: [(Text, Set Text)] -> CodeGen ()
insertThirdPartyImports imports =
insertThirdPartyImportsA [ (from, Data.Map.Strict.fromSet id objects)
| (from, objects) <- imports
]
insertThirdPartyImportsA :: [(Text, Map Text Text)] -> CodeGen ()
insertThirdPartyImportsA imports =
modify insert'
where
insert' c@CodeGenContext { thirdPartyImports = ti } =
c { thirdPartyImports = Prelude.foldl (unionWith Data.Map.Strict.union)
ti
importList
}
importList :: [Map Text (Map Text Text)]
importList = [ Data.Map.Strict.singleton from objects
| (from, objects) <- imports
]
insertLocalImport :: Text -> Text -> CodeGen ()
insertLocalImport module' object = modify insert'
where
insert' c@CodeGenContext { localImports = li } =
c { localImports = insertWith Data.Set.union module' [object] li }
importTypingForPython3 :: CodeGen ()
importTypingForPython3 = do
pyVer <- getPythonVersion
case pyVer of
Python2 -> return ()
Python3 -> insertStandardImport "typing"
addDependency :: Text -> CodeGen ()
addDependency package =
modify $ \ c@CodeGenContext { dependencies = deps } ->
c { dependencies = Data.Set.insert package deps }
addOptionalDependency :: (Int, Int) -- | Python version already stasified.
-> Text -- | PyPI package name.
-> CodeGen ()
addOptionalDependency pyVer package =
modify $ \ c@CodeGenContext { optionalDependencies = oldOptDeps } ->
c { optionalDependencies = newOptDeps oldOptDeps }
where
newOptDeps :: Map (Int, Int) (Set Text) -> Map (Int, Int) (Set Text)
newOptDeps = Data.Map.Strict.alter
(Just . Data.Set.insert package . fromMaybe Data.Set.empty)
pyVer
getPythonVersion :: CodeGen PythonVersion
getPythonVersion = fmap pythonVersion Control.Monad.State.get
renameModulePath :: RenameMap -> ModulePath -> ModulePath
renameModulePath renameMap path' =
rename (Data.Map.Strict.toDescList renameMap)
-- longest paths should be processed first
where
rename :: [(ModulePath, ModulePath)] -> ModulePath
rename ((from, to) : xs) = let r = replacePrefix from to path'
in if r == path'
then rename xs
else r
rename [] = path'
renameModulePath' :: Python -> ModulePath -> ModulePath
renameModulePath' Python { renames = table } = renameModulePath table
toImportPath' :: ModulePath -> Text
toImportPath' =
intercalate "." . fmap toAttributeName . GHC.Exts.toList
toImportPath :: Python -> ModulePath -> Text
toImportPath target' = toImportPath' . renameModulePath' target'
toImportPaths :: Python -> Set ModulePath -> [Text]
toImportPaths target' paths =
toAscList $ Data.Set.map toImportPath' $ hierarchies renamedPaths
where
renamedPaths :: Set ModulePath
renamedPaths = Data.Set.map (renameModulePath' target') paths
toClassName :: Identifier -> Text
toClassName identifier =
if className `member` keywords then className `snoc` '_' else className
where
className :: Text
className = toPascalCaseText identifier
toClassName' :: Name -> Text
toClassName' = toClassName . facialName
toAttributeName :: Identifier -> Text
toAttributeName identifier =
if attrName `member` keywords then attrName `snoc` '_' else attrName
where
attrName :: Text
attrName = toSnakeCaseText identifier
toAttributeName' :: Name -> Text
toAttributeName' = toAttributeName . facialName
toBehindSnakeCaseText :: Name -> Text
toBehindSnakeCaseText = toSnakeCaseText . behindName
mangleVar :: Code -> Text -> Code
mangleVar expr arbitrarySideName = Data.Text.concat
[ "__nirum_"
, (`Data.Text.map` expr) $ \ c ->
if 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || c == '_'
then c
else '_'
, "__"
, arbitrarySideName
, "__"
]
collectionsAbc :: CodeGen Code
collectionsAbc = do
ver <- getPythonVersion
importStandardLibrary $ case ver of
Python2 -> "collections"
Python3 -> "collections.abc"
-- | Indent the given code. If there are empty lines these are not indented.
indent :: ToMarkup m => Code -> m -> Code
indent space =
intercalate "\n"
. fmap indentLn
. Data.Text.Lazy.split (== '\n')
. renderMarkup
. preEscapedToMarkup
where
indentLn :: Data.Text.Lazy.Text -> Code
indentLn line
| Data.Text.Lazy.null line = Data.Text.empty
| otherwise = space `append` toStrict line
stringLiteral :: Text -> Code
stringLiteral string =
open $ Data.Text.concatMap esc string `snoc` '"'
where
open :: Text -> Text
open =
if Data.Text.any (> '\xff') string
then Data.Text.append "u\""
else Data.Text.cons '"'
esc :: Char -> Text
esc '"' = "\\\""
esc '\\' = "\\\\"
esc '\t' = "\\t"
esc '\n' = "\\n"
esc '\r' = "\\r"
esc c
| c >= '\x10000' = pack $ printf "\\U%08x" c
| c >= '\xff' = pack $ printf "\\u%04x" c
| c < ' ' || c >= '\x7f' = pack $ printf "\\x%02x" c
| otherwise = Data.Text.singleton c
baseStringClass :: CodeGen Code
baseStringClass = do
builtinsMod <- importBuiltins
pyVer <- getPythonVersion
let className = case pyVer of
Python2 -> "basestring" :: Code
Python3 -> "str"
return [qq|$builtinsMod.$className|]
baseIntegerClass :: CodeGen Code
baseIntegerClass = do
builtinsMod <- importBuiltins
pyVer <- getPythonVersion
return $ case pyVer of
Python2 -> [qq|($builtinsMod.int, $builtinsMod.long)|]
Python3 -> [qq|$builtinsMod.int|]