{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DataKinds #-}
module Language.PureScript.TsdGen.Module where
import Prelude hiding (elem,notElem,lookup)
import Data.Maybe
import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.Text as T
import Data.Text (Text)
import qualified Data.Text.Lazy.Builder as TB
import qualified Data.ByteString.Lazy as BS
import qualified Data.Aeson as JSON
import Control.Arrow (first,second)
import Control.Monad.State
import Control.Monad.Except
import Control.Monad.Reader
import Control.Monad.Writer
import Control.Monad.RWS.Strict
import System.FilePath ((</>))
import Language.PureScript.Externs
import Language.PureScript.Environment
import Language.PureScript.Types
import Language.PureScript.Names
import Language.PureScript.Label
import Language.PureScript.Errors
import Language.PureScript.PSString
import Language.PureScript.Pretty.Kinds
import Language.PureScript.CodeGen.JS.Common
import qualified Language.PureScript.Constants as C
import Language.PureScript.TsdGen.Types
import Data.Version (showVersion)
import Paths_purescript_tsd_gen (version)
data ModuleProcessingError = FileReadError
| JSONDecodeError String FilePath
| PursTypeError ModuleName MultipleErrors
deriving (Show)
data ModuleImport = ModuleImport { moduleImportIdent :: Maybe Text
}
type ModuleImportMap = Map.Map ModuleName ModuleImport
type ModuleWriter = RWST () TB.Builder ModuleImportMap (ExceptT ModuleProcessingError IO)
readExternsForModule :: FilePath -> ModuleName -> ExceptT ModuleProcessingError IO ExternsFile
readExternsForModule dir moduleName = do
let moduleNameText = T.unpack (runModuleName moduleName)
externsPath = dir </> moduleNameText </> "externs.json"
s <- liftIO $ BS.readFile externsPath
case JSON.eitherDecode s of
Left err -> throwError (JSONDecodeError err externsPath)
Right result -> return result
recursivelyLoadExterns :: FilePath -> ModuleName -> StateT (Environment, Map.Map ModuleName (Maybe ExternsFile)) (ExceptT ModuleProcessingError IO) ()
recursivelyLoadExterns dir moduleName
| moduleName == ModuleName [ProperName C.prim] = return ()
| moduleName `List.elem` C.primModules = return ()
| otherwise = do
ef <- lift (readExternsForModule dir moduleName)
modify (second (Map.insert moduleName (Just ef)))
let imports = efImports ef
forM_ (map eiModule imports) $ \importModuleName -> do
alreadyLoading <- gets (Map.member importModuleName . snd)
unless alreadyLoading $ do
modify (second (Map.insert importModuleName Nothing))
recursivelyLoadExterns dir importModuleName
modify (first (applyExternsFileToEnvironment ef))
emitComment :: Text -> ModuleWriter ()
emitComment t = tell ("// " <> TB.fromText t <> "\n")
emitInterface :: Text -> [Text] -> [Field] -> ModuleWriter ()
emitInterface name tyParams fields = do
let tyParamsText | null tyParams = mempty
| otherwise = "<" <> TB.fromText (T.intercalate ", " $ map properToJs tyParams) <> ">"
tell $ "interface " <> TB.fromText name <> tyParamsText <> " {\n" <> TB.fromText (T.concat (map (\f -> " " <> showField f <> ";\n") fields)) <> "}\n"
emitTypeDeclaration :: Maybe Text -> Text -> [Text] -> TSType -> ModuleWriter ()
emitTypeDeclaration comment name tyParams ty = do
let commentPart = case comment of
Just commentText -> "/*" <> TB.fromText commentText <> "*/ "
Nothing -> mempty
let tyParamsText | null tyParams = mempty
| otherwise = "<" <> TB.fromText (T.intercalate ", " $ map properToJs tyParams) <> ">"
tell $ "export type " <> commentPart <> TB.fromText name <> tyParamsText <> " = " <> TB.fromText (showTSType ty) <> ";\n"
emitValueDeclaration :: Maybe Text -> Text -> TSType -> ModuleWriter ()
emitValueDeclaration comment name ty
| nameIsJsReserved name = do
let jsName = "$$" <> TB.fromText name
tell $ "declare const " <> jsName <> ": " <> TB.fromText (showTSType ty) <> ";\nexport " <> commentPart <> "{ " <> jsName <> " as " <> TB.fromText name <> " };\n"
| isIdentifierName name = do
tell $ "export const " <> commentPart <> TB.fromText name <> ": " <> TB.fromText (showTSType ty) <> ";\n"
| otherwise = do
tell $ "// The identifier \"" <> TB.fromText name <> "\" cannot be expressed in JavaScript:\n// export const " <> commentPart <> TB.fromText name <> ": " <> TB.fromText (showTSType ty) <> ";\n"
where commentPart = case comment of
Just commentText -> "/*" <> TB.fromText commentText <> "*/ "
Nothing -> mempty
emitNamespaceImport :: Monad m => Text -> ModuleName -> WriterT TB.Builder m ()
emitNamespaceImport ident moduleName = tell $ "import * as " <> TB.fromText ident <> " from \"../" <> TB.fromText (runModuleName moduleName) <> "\";\n"
emitImport :: Monad m => ModuleName -> WriterT TB.Builder m ()
emitImport moduleName = tell $ "import \"../" <> TB.fromText (runModuleName moduleName) <> "\";\n"
processLoadedModule :: Environment -> ExternsFile -> Bool -> ExceptT ModuleProcessingError IO TB.Builder
processLoadedModule env ef importAll = execWriterT $ do
tell $ "// module " <> TB.fromText (runModuleName currentModuleName) <> ", generated by purescript-tsd-gen " <> TB.fromString (showVersion version) <> "\n"
(moduleImportMap, moduleBody) <- lift (execRWST (mapM_ processDecl (efDeclarations ef)) () (Map.singleton currentModuleName (ModuleImport { moduleImportIdent = Nothing })))
if importAll
then do
let explicitlyImported = List.nub (map eiModule (efImports ef))
allImports = Map.keys moduleImportMap
forM_ (explicitlyImported `List.union` allImports) $
\moduleName ->
case Map.lookup moduleName moduleImportMap of
Just (ModuleImport { moduleImportIdent = Just ident }) -> emitNamespaceImport ident moduleName
Nothing | moduleName /= ModuleName [ProperName C.prim] ->
emitImport moduleName
_ -> return ()
else
forM_ (Map.toList moduleImportMap) $
\m -> case m of
(moduleName, ModuleImport { moduleImportIdent = Just ident }) -> emitNamespaceImport ident moduleName
_ -> return ()
tell moduleBody
where
currentModuleName :: ModuleName
currentModuleName = efModuleName ef
qualCurrentModule :: a -> Qualified a
qualCurrentModule = Qualified (Just currentModuleName)
getModuleId :: ModuleName -> ModuleWriter (Maybe Text)
getModuleId (ModuleName [ProperName prim]) | prim == C.prim = return Nothing
getModuleId moduleName@(ModuleName components) = do
mid <- gets (Map.lookup moduleName)
case mid of
Nothing -> do
let moduleId = Just $ T.intercalate "_" (runProperName <$> components)
modify (Map.insert moduleName (ModuleImport { moduleImportIdent = moduleId }))
return moduleId
Just ModuleImport{..} -> return moduleImportIdent
makeContext :: [Text] -> TypeTranslationContext ModuleWriter
makeContext typeVariables = TypeTranslationContext typeVariables [] Nothing getModuleId env currentModuleName
pursTypeToTSTypeX :: [Text] -> Type -> ModuleWriter TSType
pursTypeToTSTypeX ctx ty = do
e <- runExceptT $ runReaderT (pursTypeToTSType ty) (makeContext ctx)
case e of
Left err -> throwError (PursTypeError currentModuleName err)
Right tsty -> return tsty
processDecl :: ExternsDeclaration -> ModuleWriter ()
processDecl EDType{..} = do
let name = runProperName edTypeName
qTypeName = qualCurrentModule edTypeName
if isSimpleKind edTypeKind
then case edTypeDeclarationKind of
DataType params [(ctorPName,[member])]
| Just (Newtype,_,_,_) <- Map.lookup (qualCurrentModule ctorPName) (dataConstructors env) -> do
case extractTypes edTypeKind params of
Just typeParameters -> do
member' <- pursTypeToTSTypeX typeParameters member
emitTypeDeclaration (Just "newtype") name typeParameters member'
Nothing -> do
emitComment $ "newtype " <> name <> ": kind annotation was not available"
DataType params ctors -> do
case extractTypes edTypeKind params of
Just typeParameters -> do
let buildCtorType (ctorPName,members)
| qualCurrentModule ctorPName `Map.member` dataConstructors env
= let fv = typeParameters `List.intersect` concatMap freeTypeVariables members
in TSNamed Nothing (runProperName edTypeName <> "$$" <> runProperName ctorPName) (map TSTyVar fv)
| otherwise
= TSRecord [ mkField "$$pursType" (TSStringLit $ mkString $ runModuleName currentModuleName <> "." <> runProperName edTypeName)
, mkField "$$pursTag" (TSStringLit $ mkString $ runProperName ctorPName)
, mkField "$$abstractMarker" TSNever
]
emitTypeDeclaration (Just "data") name typeParameters (TSUnion $ map buildCtorType ctors)
Nothing -> do
emitComment $ "data " <> name <> ": kind annotation was not available"
TypeSynonym
| Just (synonymArguments, synonymType) <- Map.lookup qTypeName (typeSynonyms env) -> do
case extractTypes edTypeKind synonymArguments of
Just typeParameters -> do
tsty <- pursTypeToTSTypeX typeParameters synonymType
emitTypeDeclaration (Just "synonym") name typeParameters tsty
Nothing -> do
emitComment $ "type synonym " <> name <> ": kind annotation was not available"
| otherwise -> emitComment ("type (synonym) " <> name <> ": " <> prettyPrintKind edTypeKind)
ExternData
| qTypeName == qnUnit -> do
emitTypeDeclaration (Just "builtin") "Unit" [] (TSRecord [(mkOptionalField "$$pursType" (TSStringLit "Data.Unit.Unit"))])
| qTypeName `List.elem` builtins -> do
pst <- pursTypeToTSTypeX typeParameters (foldl TypeApp (TypeConstructor qTypeName) (map TypeVar typeParameters))
emitTypeDeclaration (Just "builtin") name typeParameters pst
| otherwise -> do
emitTypeDeclaration (Just "foreign") name typeParameters (TSUnknown "foreign")
where builtins = [qnFn0,qnFn2,qnFn3,qnFn4,qnFn5,qnFn6,qnFn7,qnFn8,qnFn9,qnFn10,qnStrMap,qnEffect,qnNullable]
n = numberOfTypeParams edTypeKind
typeParameters = map (\i -> "a" <> T.pack (show i)) [0..n-1]
LocalTypeVariable -> emitComment ("unexpected local type variable: " <> name <> " :: " <> prettyPrintKind edTypeKind)
ScopedTypeVar -> emitComment ("unexpected scoped type variable: " <> name <> " :: " <> prettyPrintKind edTypeKind)
else emitComment ("type " <> name <> " :: " <> prettyPrintKind edTypeKind <> " : unsupported kind")
processDecl EDDataConstructor{..} = do
let name = runProperName edDataCtorName
case Map.lookup (qualCurrentModule edDataCtorTypeCtor) (types env) of
Just (k, DataType typeParameters constructors)
| isSimpleKind k
, Just fieldTypes <- List.lookup edDataCtorName constructors -> do
tsty <- pursTypeToTSTypeX [] edDataCtorType
case edDataCtorOrigin of
Data -> do
let fieldTypeVars = map fst typeParameters `List.intersect` concatMap freeTypeVariables fieldTypes
dataCtorSubtypeName = runProperName edDataCtorTypeCtor <> "$$" <> runProperName edDataCtorName
dataCtorSubtype = TSNamed Nothing dataCtorSubtypeName (map TSTyVar fieldTypeVars)
fieldTypesTS <- mapM (pursTypeToTSTypeX fieldTypeVars) fieldTypes
let mkMarkerField | length constructors == 1 = mkOptionalField
| otherwise = mkField
makerFields = [ mkMarkerField "$$pursType" (TSStringLit (mkString $ runModuleName currentModuleName <> "." <> runProperName edDataCtorTypeCtor))
, mkMarkerField "$$pursTag" (TSStringLit (mkString $ runProperName edDataCtorName))
]
dataFields = zipWith (\f ty -> mkField (Label $ mkString $ runIdent f) ty) edDataCtorFields fieldTypesTS
emitInterface dataCtorSubtypeName fieldTypeVars (makerFields <> dataFields)
let ctorFieldName | null edDataCtorFields = "value"
| otherwise = "create"
ctorType = TSRecord [ mkField ctorFieldName tsty
, NewSignature fieldTypeVars fieldTypesTS dataCtorSubtype
]
emitValueDeclaration (Just "data ctor") name ctorType
Newtype ->
emitValueDeclaration (Just "newtype data ctor") name tsty
Nothing -> emitComment $ "the type of an exported data constructor must be exported: " <> name
Just (k, DataType _typeParameters _constructors) -> emitComment $ "unrecognized data constructor: " <> name <> " kind: " <> prettyPrintKind k
_ -> emitComment $ "unrecognized data constructor: " <> name
processDecl EDValue{..} = do
let name = runIdent edValueName
tsty <- pursTypeToTSTypeX [] edValueType
emitValueDeclaration Nothing name tsty
processDecl EDInstance{..}
| Just constraints <- edInstanceConstraints
, Just (_synonymParams,_synonymType) <- Map.lookup qDictTypeName (typeSynonyms env) = do
let
dictTy = foldl TypeApp (TypeConstructor qDictTypeName) edInstanceTypes
desugaredInstanceType = quantify (foldr ConstrainedType dictTy constraints)
instanceTy <- pursTypeToTSTypeX [] desugaredInstanceType
emitValueDeclaration (Just "instance") name instanceTy
| otherwise = emitComment ("invalid instance declaration '" <> name <> "'")
where name = runIdent edInstanceName :: Text
qDictTypeName = fmap coerceProperName edInstanceClassName :: Qualified (ProperName 'TypeName)
processDecl EDKind{..} = do
let name = runProperName edKindName
emitComment ("kind " <> name)
processDecl EDTypeSynonym{} = do
return ()
processDecl EDClass{} = do
return ()