ampersand-3.1.0: src/Database/Design/Ampersand/Input/Parsing.hs
{-# LANGUAGE ScopedTypeVariables #-}
-- This module provides an interface to be able to parse a script and to
-- return an FSpec, as tuned by the command line options.
-- This might include that RAP is included in the returned FSpec.
module Database.Design.Ampersand.Input.Parsing (
parseADL, parseADL1pExpr, parseRule, parseCtx, runParser
) where
import Control.Applicative
import Data.List
import Data.Char(toLower)
import Data.Traversable (sequenceA)
import Database.Design.Ampersand.ADL1
import Database.Design.Ampersand.Basics
import Database.Design.Ampersand.Input.ADL1.CtxError
import Database.Design.Ampersand.Input.ADL1.Lexer
import Database.Design.Ampersand.Input.ADL1.Parser
import Database.Design.Ampersand.Core.ParseTree (mkContextOfPopsOnly)
import Database.Design.Ampersand.Misc
import Prelude hiding (putStrLn, writeFile) -- make sure everything is UTF8
import System.Directory
import System.FilePath
import Text.Parsec.Error (Message(..), showErrorMessages, errorMessages, ParseError, errorPos)
import Text.Parsec.Prim (runP)
import Database.Design.Ampersand.Input.Xslx.XLSX
import Control.Exception
import Database.Design.Ampersand.Prototype.StaticFiles_Generated(getStaticFileContent,FileKind(FormalAmpersand))
fatal :: Int -> String -> a
fatal = fatalMsg "Parsing"
-- | Parse an Ampersand file and all transitive includes
parseADL :: Options -- ^ The options given through the command line
-> Either FilePath MetaType -- ^ The path of the file to be parsed OR the MetaType. In the latter case, the files will be taken from `allStaticFiles`
-> IO (Guarded P_Context) -- ^ The resulting context
parseADL opts thingToParse =
whenCheckedIO (parseSingleADL opts useAllStaticFiles filePath) $ \(ctxt, filePaths) ->
whenCheckedIO (parseADLs opts useAllStaticFiles [filePath] filePaths) $ \ctxts ->
return $ Checked $ foldl mergeContexts ctxt ctxts
where (filePath, useAllStaticFiles) = case thingToParse of
Left fp -> (fp ,False)
Right Generics -> ("Generics.adl",True )
Right AST -> ("AST.adl" ,True )
-- | Parses several ADL files
parseADLs :: Options -- ^ The options given through the command line
-> Bool -- ^ True iff the file is from FormalAmpersand files in `allStaticFiles`
-> [FilePath] -- ^ The list of files that have already been parsed
-> [FilePath] -- ^ The list of files to parse
-> IO (Guarded [P_Context]) -- ^ The resulting contexts
parseADLs _ _ _ [] = return $ Checked []
parseADLs opts useAllStaticFiles parsedFilePaths filePaths =
do { let filePathsToParse = nub filePaths \\ parsedFilePaths
; whenCheckedIO (sequenceA <$> mapM (parseSingleADL opts useAllStaticFiles) filePathsToParse) $ \ctxtNewFilePathss ->
do { let (ctxts, newFilesToParse) = unzip ctxtNewFilePathss
; whenCheckedIO (parseADLs opts useAllStaticFiles (parsedFilePaths ++ filePaths) $ concat newFilesToParse) $ \ctxts' ->
return $ Checked $ ctxts ++ ctxts'
}
}
-- | Parse an Ampersand file, but not its includes (which are simply returned as a list)
parseSingleADL ::
Options
-> Bool -- True iff the file is from FormalAmpersand files in `allStaticFiles`
-> FilePath -> IO (Guarded (P_Context, [FilePath]))
parseSingleADL opts useAllStaticFiles filePath
= do verboseLn opts $ "Reading file " ++ filePath ++ if useAllStaticFiles then " (from within ampersand.exe)" else ""
exists <- doesFileExist filePath
if useAllStaticFiles || exists
then parseSingleADL'
else return . makeError $ "Could not find `"++filePath++"`."
where
parseSingleADL' :: IO(Guarded (P_Context, [FilePath]))
parseSingleADL'
| extension == ".xlsx" =
do { popFromExcel <- catchInvalidXlsx $ parseXlsxFile opts useAllStaticFiles filePath
; return ((\pops -> (mkContextOfPopsOnly pops,[])) <$> popFromExcel) -- Excel file cannot contain include files
}
| otherwise =
do { mFileContents
<- if useAllStaticFiles
then case getStaticFileContent FormalAmpersand filePath of
Just cont -> do return (Right $ stripBom cont)
Nothing -> fatalMsg ("Statically included "++ show FormalAmpersand++ " files. ") 0 $
"Cannot find `"++filePath++"`."
else readUTF8File filePath
; case mFileContents of
Left err -> return $ makeError ("ERROR reading file " ++ filePath ++ ":\n" ++ err)
Right fileContents ->
whenCheckedIO (return $ parseCtx filePath fileContents) $ \(ctxts, relativePaths) ->
do return (Checked (ctxts, relativePaths))
}
where stripBom :: String -> String
stripBom ('\239':'\187':'\191': s) = s
stripBom s = s
extension = map toLower $ takeExtension filePath
catchInvalidXlsx :: IO a -> IO a
catchInvalidXlsx m = catch m f
where f :: SomeException -> IO a
f exception = fatal 34 $ "The file does not seem to have a valid .xlsx structure:\n "++show exception
parseErrors :: Lang -> ParseError -> [CtxError]
parseErrors lang err = [PE (Message msg)]
where msg :: String
msg = "In file " ++ show (errorPos err) ++ ":" ++ showLang lang (errorMessages err)
showLang :: Lang -> [Message] -> String
showLang English = showErrorMessages "or" "unknown parse error" "at that point expecting" "Parsing stumbled upon" "end of input"
showLang Dutch = showErrorMessages "of" "onbekende parsingfout" "verwacht" "onverwacht" "einde van de invoer"
parse :: AmpParser a -> FilePath -> [Token] -> Guarded a
parse p fn ts =
-- runP :: Parsec s u a -> u -> FilePath -> s -> Either ParseError a
case runP p pos fn ts of
--TODO: Add language support to the parser errors
Left err -> Errors $ parseErrors English err
Right a -> Checked a
where pos | null ts = initPos fn
| otherwise = tokPos (head ts)
--TODO: Give the errors in a better way
lexerError2CtxError :: LexerError -> CtxError
lexerError2CtxError (LexerError pos err) =
PE (Message ("Lexer error at "++show pos++"\n "
++ intercalate "\n " (showLexerErrorInfo err)
)
)
-- | Runs the given parser
runParser :: AmpParser a -- ^ The parser to run
-> FilePath -- ^ Name of the file (for error messages)
-> String -- ^ String to parse
-> Guarded a -- ^ The result
runParser parser filename input =
-- lexer :: [Options] -> String -> [Char] -> Either LexerError ([Token], [LexerWarning])
--TODO: Give options to the lexer
let lexed = lexer [] filename input
in case lexed of
Left err -> Errors [lexerError2CtxError err]
--TODO: Do something with the warnings. The warnings cannot be shown with the current Guarded data type
Right (tokens, _) -> whenChecked (parse parser filename tokens) Checked
-- | Parses an isolated rule
-- In order to read derivation rules, we use the Ampersand parser.
-- Since it is applied on static code only, error messagea may be produced as fatals.
parseRule :: String -- ^ The string to be parsed
-> Term TermPrim -- ^ The resulting rule
parseRule str
= case runParser pRule "inside Haskell code" str of
Checked result -> result
Errors msg -> fatal 274 ("Parse errors in "++str++":\n "++show msg)
-- | Parses an isolated ADL1 expression string
parseADL1pExpr :: String -- ^ The string to be parsed
-> FilePath -- ^ The name of the file (used for error messages)
-> Either String (Term TermPrim) -- ^ The result: Either an error message, or a good result
parseADL1pExpr str fn =
case runParser pTerm fn str of
Checked result -> Right result
Errors msg -> Left $ "Parse errors:\n"++show msg
-- | Parses an Ampersand context
parseCtx :: FilePath -- ^ The file name (used for error messages)
-> String -- ^ The string to be parsed
-> Guarded (P_Context, [String]) -- ^ The context and a list of included files
parseCtx = runParser pContext