-- | Every function in this module takes a flag that determines whether the input is an Oberon or Oberon-2 module.
module Language.Oberon (parseModule, parseAndResolveModule, parseAndResolveModuleFile) where
import Language.Oberon.AST (Module(..))
import qualified Language.Oberon.Grammar as Grammar
import qualified Language.Oberon.Resolver as Resolver
import qualified Language.Oberon.TypeChecker as TypeChecker
import Control.Monad (when)
import Data.Either.Validation (Validation(..))
import Data.Functor.Identity (Identity)
import Data.Functor.Compose (getCompose)
import Data.List.NonEmpty (NonEmpty((:|)))
import qualified Data.Map.Lazy as Map
import Data.Map.Lazy (Map)
import Data.Monoid ((<>))
import Data.Text (Text, unpack)
import Data.Text.IO (readFile)
import Text.Grampa (Ambiguous, Grammar, ParseResults, parseComplete)
import qualified Text.Grampa.ContextFree.LeftRecursive as LeftRecursive
import System.Directory (doesFileExist)
import System.FilePath (FilePath, addExtension, combine, takeDirectory)
import Prelude hiding (readFile)
-- | Parse the given text of a single module, without resolving the syntactic ambiguities.
parseModule :: Bool -> Text -> ParseResults [Module Ambiguous Ambiguous]
parseModule oberon2 = getCompose . Grammar.module_prod
. parseComplete (if oberon2 then Grammar.oberon2Grammar else Grammar.oberonGrammar)
-- | Parse the given text of a single /definition/ module, without resolving the syntactic ambiguities.
parseDefinitionModule :: Bool -> Text -> ParseResults [Module Ambiguous Ambiguous]
parseDefinitionModule oberon2 = getCompose . Grammar.module_prod
. parseComplete (if oberon2 then Grammar.oberon2DefinitionGrammar
else Grammar.oberonDefinitionGrammar)
parseNamedModule :: Bool -> FilePath -> Text -> IO (ParseResults [Module Ambiguous Ambiguous])
parseNamedModule oberon2 path name =
do let basePath = combine path (unpack name)
isDefn <- doesFileExist (addExtension basePath "Def")
let grammar = if oberon2
then if isDefn then Grammar.oberon2DefinitionGrammar else Grammar.oberon2Grammar
else if isDefn then Grammar.oberonDefinitionGrammar else Grammar.oberonGrammar
getCompose . Grammar.module_prod . parseComplete grammar
<$> readFile (addExtension basePath $ if isDefn then "Def" else "Mod")
parseImportsOf :: Bool -> FilePath -> Map Text (Module Ambiguous Ambiguous) -> IO (Map Text (Module Ambiguous Ambiguous))
parseImportsOf oberon2 path modules =
case filter (`Map.notMember` modules) moduleImports
of [] -> return modules
newImports -> (((modules <>) . Map.fromList . map assertSuccess) <$>
(traverse . traverse) (parseNamedModule oberon2 path) [(p, p) | p <- newImports])
>>= parseImportsOf oberon2 path
where moduleImports = foldMap importsOf modules
importsOf (Module _ imports _ _ _) = snd <$> imports
assertSuccess (m, Left err) = error ("Parse error in module " <> unpack m <> ":" <> show err)
assertSuccess (m, Right [p]) = (m, p)
assertSuccess (m, Right _) = error ("Ambiguous parses of module " <> unpack m)
-- | Given a directory path for module imports, parse the given module text and all the module files it imports, then
-- use all the information to resolve the syntactic ambiguities.
parseAndResolveModule :: Bool -> Bool -> FilePath -> Text
-> IO (Validation (NonEmpty Resolver.Error) (Module Identity Identity))
parseAndResolveModule checkTypes oberon2 path source =
case parseModule oberon2 source
of Left err -> return (Failure $ Resolver.UnparseableModule err :| [])
Right [rootModule@(Module moduleName imports _ _ _)] ->
do importedModules <- parseImportsOf oberon2 path (Map.singleton moduleName rootModule)
let resolvedImportMap = Resolver.resolveModule predefinedScope resolvedImportMap <$> importedModules
predefinedScope = if oberon2 then Resolver.predefined2 else Resolver.predefined
successful (Success a) = Just a
successful _ = Nothing
typeErrors = TypeChecker.checkModules
(if oberon2 then TypeChecker.predefined2 else TypeChecker.predefined)
(Map.mapMaybe successful resolvedImportMap)
when (checkTypes && not (null typeErrors)) (error $ show typeErrors)
return $ resolvedImportMap Map.! moduleName
Right _ -> return (Failure $ Resolver.AmbiguousParses :| [])
-- | Parse the module file at the given path, assuming all its imports are in the same directory.
parseAndResolveModuleFile :: Bool -> Bool -> FilePath
-> IO (Validation (NonEmpty Resolver.Error) (Module Identity Identity))
parseAndResolveModuleFile checkTypes oberon2 path =
readFile path >>= parseAndResolveModule checkTypes oberon2 (takeDirectory path)