packages feed

purescript-0.7.4.1: src/Language/PureScript/Sugar/Names/Imports.hs

-----------------------------------------------------------------------------
--
-- Module      :  Language.PureScript.Sugar.Names.Imports
-- License     :  MIT (http://opensource.org/licenses/MIT)
--
-- Maintainer  :  Phil Freeman <paf31@cantab.net>, Gary Burgess <gary.burgess@gmail.com>
-- Stability   :  experimental
-- Portability :
--
-- |
--
-----------------------------------------------------------------------------

{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}

module Language.PureScript.Sugar.Names.Imports (resolveImports) where

import Data.List (find)
import Data.Maybe (fromMaybe, isNothing)

import Control.Arrow (first)
#if __GLASGOW_HASKELL__ < 710
import Control.Applicative (Applicative(..), (<$>))
#endif
import Control.Monad
import Control.Monad.Error.Class (MonadError(..))
import Control.Monad.Writer (MonadWriter(..), censor)

import qualified Data.Map as M

import Language.PureScript.AST
import Language.PureScript.Names
import Language.PureScript.Errors
import Language.PureScript.Sugar.Names.Env

-- Finds the imports within a module, mapping the imported module name to an optional set of
-- explicitly imported declarations.
findImports :: forall m. (Applicative m, MonadError MultipleErrors m, MonadWriter MultipleErrors m) => [Declaration] -> m (M.Map ModuleName [(Maybe SourceSpan, ImportDeclarationType, Maybe ModuleName)])
findImports = foldM (go Nothing) M.empty
  where
  go pos result (ImportDeclaration mn typ qual) = do
    checkImportRefType typ
    let imp = (pos, typ, qual)
    return $ M.insert mn (maybe [imp] (imp :) (mn `M.lookup` result)) result
  go _ result (PositionedDeclaration pos _ d) = rethrowWithPosition pos $ go (Just pos) result d
  go _ result _ = return result

  -- Ensure that classes don't appear in an `import X hiding (...)`
  checkImportRefType :: ImportDeclarationType -> m ()
  checkImportRefType (Hiding refs) = mapM_ checkImportRef refs
  checkImportRefType _ = return ()
  checkImportRef :: DeclarationRef -> m ()
  checkImportRef (ModuleRef name) = throwError . errorMessage $ ImportHidingModule name
  checkImportRef _ = return ()

-- |
-- Constructs a set of imports for a module.
--
resolveImports :: forall m. (Applicative m, MonadError MultipleErrors m, MonadWriter MultipleErrors m) => Env -> Module -> m Imports
resolveImports env (Module _ _ currentModule decls _) =
  censor (onErrorMessages (ErrorInModule currentModule)) $ do
    scope <- M.insert currentModule [(Nothing, Implicit, Nothing)] <$> findImports decls
    foldM resolveImport' nullImports (M.toList scope)
  where

  resolveImport' :: Imports -> (ModuleName, [(Maybe SourceSpan, ImportDeclarationType, Maybe ModuleName)]) -> m Imports
  resolveImport' ie (mn, imps) = foldM go ie imps
    where
    go :: Imports -> (Maybe SourceSpan, ImportDeclarationType, Maybe ModuleName) -> m Imports
    go ie' (pos, typ, impQual) = do
      modExports <- positioned $ maybe (throwError . errorMessage $ UnknownModule mn) (return . envModuleExports) $ mn `M.lookup` env
      let ie'' = ie' { importedModules = mn : importedModules ie' }
      positioned $ resolveImport currentModule mn modExports ie'' impQual typ
      where
      positioned err = case pos of
        Nothing -> err
        Just pos' -> rethrowWithPosition pos' err

-- |
-- Extends the local environment for a module by resolving an import of another module.
--
resolveImport :: forall m. (Applicative m, MonadError MultipleErrors m, MonadWriter MultipleErrors m) => ModuleName -> ModuleName -> Exports -> Imports -> Maybe ModuleName -> ImportDeclarationType -> m Imports
resolveImport currentModule importModule exps imps impQual =
  resolveByType
  where

  resolveByType :: ImportDeclarationType -> m Imports
  resolveByType Implicit = importAll importExplicit
  resolveByType (Explicit explImports) = checkRefs explImports >> foldM importExplicit imps explImports
  resolveByType (Hiding hiddenImports) = checkRefs hiddenImports >> importAll (importNonHidden hiddenImports)

  -- Check that a 'DeclarationRef' refers to an importable symbol
  checkRefs :: [DeclarationRef] -> m ()
  checkRefs = mapM_ check
    where
    check (PositionedDeclarationRef pos _ r) =
      rethrowWithPosition pos $ check r
    check (ValueRef name) =
      checkImportExists UnknownImportValue (fst `map` exportedValues exps) name
    check (TypeRef name dctors) = do
      checkImportExists UnknownImportType ((fst . fst) `map` exportedTypes exps) name
      let allDctors = fst `map` allExportedDataConstructors name
      maybe (return ()) (mapM_ $ checkDctorExists name allDctors) dctors
    check (TypeClassRef name) =
      checkImportExists UnknownImportTypeClass (fst `map` exportedTypeClasses exps) name
    --check (ModuleRef name) =
    --  checkImportExists (const UnknownModule) (exportedModules exps) name
    check _ = error "Invalid argument to checkRefs"

  -- Check that an explicitly imported item exists in the module it is being imported from
  checkImportExists :: (Eq a, Show a) => (ModuleName -> a -> SimpleErrorMessage) -> [a] -> a -> m ()
  checkImportExists unknown exports item =
    when (item `notElem` exports) $ throwError . errorMessage $ unknown importModule item

  -- Ensure that an explicitly imported data constructor exists for the type it is being imported
  -- from
  checkDctorExists :: ProperName -> [ProperName] -> ProperName -> m ()
  checkDctorExists tcon = checkImportExists (flip UnknownImportDataConstructor tcon)

  importNonHidden :: [DeclarationRef] -> Imports -> DeclarationRef -> m Imports
  importNonHidden hidden m ref | isHidden ref = return m
                               | otherwise = importExplicit m ref
    where
    -- TODO: rework this to be not confusing
    isHidden :: DeclarationRef -> Bool
    isHidden ref'@(TypeRef _ _) = foldl (checkTypeRef ref') False hidden
    isHidden ref' = ref' `elem` hidden
    checkTypeRef :: DeclarationRef -> Bool -> DeclarationRef -> Bool
    checkTypeRef _ True _ = True
    checkTypeRef r acc (PositionedDeclarationRef _ _ h) = checkTypeRef r acc h
    checkTypeRef (TypeRef _ Nothing) acc (TypeRef _ (Just _)) = acc
    checkTypeRef (TypeRef name (Just dctor)) _ (TypeRef name' (Just dctor')) = name == name' && dctor == dctor'
    checkTypeRef (TypeRef name _) _ (TypeRef name' Nothing) = name == name'
    checkTypeRef (PositionedDeclarationRef _ _ r) acc hiddenRef = checkTypeRef r acc hiddenRef
    checkTypeRef _ acc _ = acc

  -- Import all symbols
  importAll :: (Imports -> DeclarationRef -> m Imports) -> m Imports
  importAll importer = do
    imp' <- foldM (\m ((name, dctors), _) -> importer m (TypeRef name (Just dctors))) imps (exportedTypes exps)
    imp'' <- foldM (\m (name, _) -> importer m (ValueRef name)) imp' (exportedValues exps)
    foldM (\m (name, _) -> importer m (TypeClassRef name)) imp'' (exportedTypeClasses exps)

  -- Import something explicitly
  importExplicit :: Imports -> DeclarationRef -> m Imports
  importExplicit imp (PositionedDeclarationRef pos _ r) =
    rethrowWithPosition pos . warnWithPosition pos $ importExplicit imp r
  importExplicit imp (ValueRef name) = do
    values' <- updateImports (importedValues imp) (exportedValues exps) name
    return $ imp { importedValues = values' }
  importExplicit imp (TypeRef name dctors) = do
    types' <- updateImports (importedTypes imp) (first fst `map` exportedTypes exps) name
    let exportedDctors :: [(ProperName, ModuleName)]
        exportedDctors = allExportedDataConstructors name
        dctorNames :: [ProperName]
        dctorNames = fst `map` exportedDctors
    maybe (return ()) (mapM_ $ checkDctorExists name dctorNames) dctors
    when (null dctorNames && isNothing dctors) . tell . errorMessage $ MisleadingEmptyTypeImport importModule name
    dctors' <- foldM (flip updateImports exportedDctors) (importedDataConstructors imp) (fromMaybe dctorNames dctors)
    return $ imp { importedTypes = types', importedDataConstructors = dctors' }
  importExplicit imp (TypeClassRef name) = do
    typeClasses' <- updateImports (importedTypeClasses imp) (exportedTypeClasses exps) name
    return $ imp { importedTypeClasses = typeClasses' }
  importExplicit _ _ = error "Invalid argument to importExplicit"

  -- Find all exported data constructors for a given type
  allExportedDataConstructors :: ProperName -> [(ProperName, ModuleName)]
  allExportedDataConstructors name =
    case find ((== name) . fst . fst) (exportedTypes exps) of
      Nothing -> error "Invalid state in allExportedDataConstructors"
      Just ((_, dctors), mn) -> map (, mn) dctors

  -- Add something to the Imports if it does not already exist there
  updateImports :: (Ord a, Show a) => M.Map (Qualified a) (Qualified a, ModuleName)
                                   -> [(a, ModuleName)]
                                   -> a
                                   -> m (M.Map (Qualified a) (Qualified a, ModuleName))
  updateImports imps' exps' name = case M.lookup (Qualified impQual name) imps' of

    -- If the name is not already present add it to the list, after looking up
    -- where it was originally defined
    Nothing ->
      let mnOrig = fromMaybe (error "Invalid state in updateImports") (name `lookup` exps')
      in return $ M.insert (Qualified impQual name) (Qualified (Just importModule) name, mnOrig) imps'

    -- If the name already is present check whether it's a duplicate import
    -- before rejecting it. For example, if module A defines X, and module B
    -- re-exports A, importing A and B in C should not result in a "conflicting
    -- import for `x`" error
    Just (Qualified (Just mn) _, mnOrig)
       | mnOrig == fromMaybe (error "Invalid state in updateImports") (name `lookup` exps') -> return imps'
       | otherwise -> throwError . errorMessage $ err
        where
        err = if currentModule `elem` [mn, importModule]
              then ConflictingImport (show name) importModule
              else ConflictingImports (show name) mn importModule

    Just (Qualified Nothing _, _) ->
      error "Invalid state in updateImports"