SJW/src/Module.hs

195 lines
5.9 KiB
Haskell

{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
module Module (
Compiler
, Contextual
, Entry(..)
, Failable
, ImportTree(..)
, Log
, Module(..)
, Modules
, ModuleSpace(..)
, Reference(..)
, buildEntry
, emptySpace
, parse
, register
, reorder
) where
import Context (CodePath(..), Context(..), Path(..))
import Control.Applicative ((<|>), many, optional)
import Control.Monad.Except (MonadError(..))
import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.RWS (MonadReader, MonadState, MonadWriter, asks, modify)
import Data.Attoparsec.Text (
Parser, char, count, digit, inClass, isEndOfLine, letter, parseOnly, sepBy
, string, takeTill, takeWhile
)
import Data.Map (Map, foldlWithKey)
import qualified Data.Map as Map (empty, insert, lookup)
import qualified Data.Set as Set (empty, insert, toList)
import Data.Text (Text)
import qualified Data.Text as Text (pack)
import Prelude hiding (takeWhile)
import Priority (Queue, empty)
import System.Directory (doesFileExist)
import System.FilePath ((</>), (<.>))
import Text.Printf (printf)
data Reference =
ModulePath {modulePath :: Path}
| Object {modulePath :: Path, field :: String}
deriving Show
data ImportTree = ImportTree {
target :: Maybe Reference
, children :: Map String ImportTree
} deriving Show
recurse :: (a -> [String] -> Reference -> a) -> a -> ImportTree -> a
recurse f initValue = recAux [] initValue
where
next _ value Nothing = value
next stack value (Just ref) = f value (reverse stack) ref
recAux stack value tree =
let nextValue = next stack value (target tree) in
foldlWithKey (\a k b -> recAux (k:stack) a b) nextValue (children tree)
data Mapping = Mapping {
exposedName :: Path
, reference :: Reference
}
data Module = Module {
imports :: ImportTree
, payload :: [Text]
} deriving Show
data Entry = Entry {
content :: Module
, dependencies :: [Path]
}
data ModuleSpace = ModuleSpace {
entries :: Map Path Entry
, queue :: Queue Path
}
register :: Modules m => Path -> Entry -> m ()
register path entry = modify $ \moduleSpace -> moduleSpace {
entries = Map.insert path entry (entries moduleSpace)
}
reorder :: Modules m => (Queue Path -> Queue Path) -> m ()
reorder f = modify $ \moduleSpace -> moduleSpace {
queue = f (queue moduleSpace)
}
emptySpace :: ModuleSpace
emptySpace = ModuleSpace {
entries = Map.empty
, queue = Priority.empty
}
type Contextual = MonadReader Context
type Log = MonadWriter [String]
type Modules = MonadState ModuleSpace
type Failable = MonadError String
type Compiler m = (Contextual m, Log m, Modules m, MonadIO m, Failable m)
space :: Parser ()
space = takeWhile (inClass " \t") *> pure ()
between :: Parser a -> (Parser b, Parser c) -> Parser a
between p (left, right) = left *> space *> p <* space <* right
keyword :: String -> Parser ()
keyword k = space <* string (Text.pack k) <* space
name :: Parser String
name = (:) <$> letter <*> many (letter <|> digit)
aliasedName :: Parser (Maybe String, String)
aliasedName =
((,) <$> (Just <$> name) <* keyword "as" <*> name)
<|> ((\s -> (Just s, s)) <$> name)
buildMappings :: Maybe [(Maybe String, String)] -> Path -> [Mapping]
buildMappings Nothing modulePath =
[Mapping modulePath (ModulePath modulePath)]
buildMappings (Just nameAssocs) modulePath = mappingOf <$> nameAssocs
where
mappingOf (Nothing, dest) = Mapping (Path [dest]) (ModulePath modulePath)
mappingOf (Just source, dest) =
Mapping (Path [dest]) (Object modulePath source)
mappingParser :: Parser [Mapping]
mappingParser =
buildMappings <$> optional fromClause <*> (Path <$> name `sepBy` char '.')
where
fromClause =
(count 1 (aliasedName <|> star) <|> namesBlock) <* keyword "from"
namesBlock =
(aliasedName `sepBy` (char ',' *> space)) `between` (char '{', char '}')
star = (,) <$> (char '*' *> pure Nothing) <* keyword "as" <*> name
importParser :: Parser [Mapping]
importParser = mappingParser `between` (string "import", char ';')
emptyTree :: ImportTree
emptyTree = ImportTree {
target = Nothing
, children = Map.empty
}
insertMapping :: ImportTree -> Mapping -> ImportTree
insertMapping tmpTree (Mapping {exposedName, reference}) =
insertAt components tmpTree
where
Path components = exposedName
insertAt [] tree = tree {target = Just reference}
insertAt (next:restOfPath) tree@(ImportTree {children}) =
let subTree = maybe emptyTree id $ Map.lookup next children in tree {
children = Map.insert next (insertAt restOfPath subTree) children
}
moduleParser :: Parser Module
moduleParser = Module
<$> (foldl (foldl insertMapping) emptyTree <$> importParser `sepBy` blank)
<*> (blank *> line `sepBy` eol)
where
eol = string "\r\n" <|> string "\r" <|> string "\n"
blank = takeWhile (inClass " \t\r\n")
line = takeTill isEndOfLine
buildEntry :: Module -> Entry
buildEntry content = Entry {content, dependencies}
where
dependencies =
Set.toList . recurse pushDependency Set.empty $ imports content
pushDependency set _ ref = Set.insert (modulePath ref) set
parse :: (Contextual m, MonadIO m, Failable m) => Path -> m Module
parse path = do
searchPath <- asks codePaths
maybeSource <- find searchPath path
case maybeSource of
Nothing -> throwError $
printf "Module %s not found in paths : %s" (show path) (show searchPath)
Just source -> either throwError return =<<
parseOnly moduleParser . Text.pack <$> liftIO (readFile source)
find :: MonadIO m => CodePath -> Path -> m (Maybe FilePath)
find (CodePath []) _ = return Nothing
find (CodePath (dir:otherDirs)) path@(Path components) = do
fileExists <- liftIO $ doesFileExist filePath
if fileExists
then return (Just filePath)
else find (CodePath otherDirs) path
where
filePath = foldl (</>) dir components <.> "js"