-- | Parser for @.agda-lib@ files.
--
--   Example file:
--
--   @
--     name: Main
--     depend:
--       standard-library
--     include: .
--       src more-src
--
--   @
--
--   Should parse as:
--
--   @
--     AgdaLib
--       { libName     = "Main"
--       , libFile     = path_to_this_file
--       , libIncludes = [ "." , "src" , "more-src" ]
--       , libDepends  = [ "standard-library" ]
--       }
--   @
--
module Agda.Interaction.Library.Parse
  ( parseLibFile
  , splitCommas
  , trimLineComment
  , runP
  ) where

import qualified Control.Exception as E
import Control.Monad
import Control.Monad.Except
import Control.Monad.Writer
import Data.Char
import qualified Data.List as List
import System.FilePath

import Agda.Interaction.Library.Base

import Agda.Utils.Applicative
import Agda.Utils.IO                ( catchIO )
import qualified Agda.Utils.IO.UTF8 as UTF8
import Agda.Utils.Lens
import Agda.Utils.List              ( duplicates )
import Agda.Utils.List1             ( List1, toList )
import qualified Agda.Utils.List1   as List1
import Agda.Utils.String            ( ltrim )

-- | Parser monad: Can throw @LibParseError@s, and collects
-- @LibWarning'@s library warnings.
type P = ExceptT LibParseError (Writer [LibWarning'])

runP :: P a -> (Either LibParseError a, [LibWarning'])
runP :: forall a. P a -> (Either LibParseError a, [LibWarning'])
runP = forall w a. Writer w a -> (a, w)
runWriter forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall e (m :: * -> *) a. ExceptT e m a -> m (Either e a)
runExceptT

warningP :: LibWarning' -> P ()
warningP :: LibWarning' -> ExceptT LibParseError (Writer [LibWarning']) ()
warningP = forall w (m :: * -> *). MonadWriter w m => w -> m ()
tell forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall (f :: * -> *) a. Applicative f => a -> f a
pure

-- | The config files we parse have the generic structure of a sequence
--   of @field : content@ entries.
type GenericFile = [GenericEntry]

data GenericEntry = GenericEntry
  { GenericEntry -> String
geHeader   :: String   -- ^ E.g. field name.    @trim@med.
  , GenericEntry -> [String]
_geContent :: [String] -- ^ E.g. field content. @trim@med.
  }

-- | Library file field format format [sic!].
data Field = forall a. Field
  { Field -> String
fName     :: String            -- ^ Name of the field.
  , Field -> Bool
fOptional :: Bool              -- ^ Is it optional?
  , ()
fParse    :: [String] -> P a   -- ^ Content parser for this field.
  , ()
fSet      :: LensSet a AgdaLibFile
    -- ^ Sets parsed content in 'AgdaLibFile' structure.
  }

optionalField :: String -> ([String] -> P a) -> Lens' a AgdaLibFile -> Field
optionalField :: forall a.
String -> ([String] -> P a) -> Lens' a AgdaLibFile -> Field
optionalField String
str [String] -> P a
p Lens' a AgdaLibFile
l = forall a.
String
-> Bool -> ([String] -> P a) -> LensSet a AgdaLibFile -> Field
Field String
str Bool
True [String] -> P a
p (forall i o. Lens' i o -> LensSet i o
set Lens' a AgdaLibFile
l)

-- | @.agda-lib@ file format with parsers and setters.
agdaLibFields :: [Field]
agdaLibFields :: [Field]
agdaLibFields =
  -- Andreas, 2017-08-23, issue #2708, field "name" is optional.
  [ forall a.
String -> ([String] -> P a) -> Lens' a AgdaLibFile -> Field
optionalField String
"name"    [String] -> P String
parseName                      Lens' String AgdaLibFile
libName
  , forall a.
String -> ([String] -> P a) -> Lens' a AgdaLibFile -> Field
optionalField String
"include" (forall (f :: * -> *) a. Applicative f => a -> f a
pure forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap String -> [String]
parsePaths)  Lens' [String] AgdaLibFile
libIncludes
  , forall a.
String -> ([String] -> P a) -> Lens' a AgdaLibFile -> Field
optionalField String
"depend"  (forall (f :: * -> *) a. Applicative f => a -> f a
pure forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap String -> [String]
splitCommas) Lens' [String] AgdaLibFile
libDepends
  , forall a.
String -> ([String] -> P a) -> Lens' a AgdaLibFile -> Field
optionalField String
"flags"   (forall (f :: * -> *) a. Applicative f => a -> f a
pure forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap String -> [String]
parseFlags)  Lens' [String] AgdaLibFile
libPragmas
  ]
  where
    parseName :: [String] -> P LibName
    parseName :: [String] -> P String
parseName [String
s] | [String
name] <- String -> [String]
words String
s = forall (f :: * -> *) a. Applicative f => a -> f a
pure String
name
    parseName [String]
ls = forall e (m :: * -> *) a. MonadError e m => e -> m a
throwError forall a b. (a -> b) -> a -> b
$ String -> LibParseError
BadLibraryName forall a b. (a -> b) -> a -> b
$ [String] -> String
unwords [String]
ls

    parsePaths :: String -> [FilePath]
    parsePaths :: String -> [String]
parsePaths = (String -> String) -> String -> [String]
go forall a. a -> a
id where
      fixup :: ([a] -> t a) -> f (t a)
fixup [a] -> t a
acc = let fp :: t a
fp = [a] -> t a
acc [] in Bool -> Bool
not (forall (t :: * -> *) a. Foldable t => t a -> Bool
null t a
fp) forall (f :: * -> *) a. Alternative f => Bool -> a -> f a
?$> t a
fp
      go :: (String -> String) -> String -> [String]
go String -> String
acc []           = forall {f :: * -> *} {t :: * -> *} {a} {a}.
(Alternative f, Foldable t) =>
([a] -> t a) -> f (t a)
fixup String -> String
acc
      go String -> String
acc (Char
'\\' : Char
' '  :String
cs) = (String -> String) -> String -> [String]
go (String -> String
acc forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char
' 'forall a. a -> [a] -> [a]
:)) String
cs
      go String -> String
acc (Char
'\\' : Char
'\\' :String
cs) = (String -> String) -> String -> [String]
go (String -> String
acc forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char
'\\'forall a. a -> [a] -> [a]
:)) String
cs
      go String -> String
acc (       Char
' '  :String
cs) = forall {f :: * -> *} {t :: * -> *} {a} {a}.
(Alternative f, Foldable t) =>
([a] -> t a) -> f (t a)
fixup String -> String
acc forall a. [a] -> [a] -> [a]
++ (String -> String) -> String -> [String]
go forall a. a -> a
id String
cs
      go String -> String
acc (Char
c           :String
cs) = (String -> String) -> String -> [String]
go (String -> String
acc forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char
cforall a. a -> [a] -> [a]
:)) String
cs

    parseFlags :: String -> [String]
    parseFlags :: String -> [String]
parseFlags = String -> [String]
words

-- | Parse @.agda-lib@ file.
--
-- Sets 'libFile' name and turn mentioned include directories into absolute
-- pathes (provided the given 'FilePath' is absolute).
--
parseLibFile :: FilePath -> IO (P AgdaLibFile)
parseLibFile :: String -> IO (P AgdaLibFile)
parseLibFile String
file =
  (forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap AgdaLibFile -> AgdaLibFile
setPath forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> P AgdaLibFile
parseLib forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> String -> IO String
UTF8.readFile String
file) forall (m :: * -> *) a.
CatchIO m =>
m a -> (IOException -> m a) -> m a
`catchIO` \IOException
e ->
    forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$ forall e (m :: * -> *) a. MonadError e m => e -> m a
throwError forall a b. (a -> b) -> a -> b
$ String -> IOException -> LibParseError
ReadFailure String
file IOException
e
  where
    setPath :: AgdaLibFile -> AgdaLibFile
setPath      AgdaLibFile
lib = String -> AgdaLibFile -> AgdaLibFile
unrelativise (String -> String
takeDirectory String
file) (forall i o. Lens' i o -> LensSet i o
set Lens' String AgdaLibFile
libFile String
file AgdaLibFile
lib)
    unrelativise :: String -> AgdaLibFile -> AgdaLibFile
unrelativise String
dir = forall i o. Lens' i o -> LensMap i o
over Lens' [String] AgdaLibFile
libIncludes (forall a b. (a -> b) -> [a] -> [b]
map (String
dir String -> String -> String
</>))

-- | Parse file contents.
parseLib :: String -> P AgdaLibFile
parseLib :: String -> P AgdaLibFile
parseLib String
s = GenericFile -> P AgdaLibFile
fromGeneric forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< String -> P GenericFile
parseGeneric String
s

-- | Parse 'GenericFile' with 'agdaLibFields' descriptors.
fromGeneric :: GenericFile -> P AgdaLibFile
fromGeneric :: GenericFile -> P AgdaLibFile
fromGeneric = [Field] -> GenericFile -> P AgdaLibFile
fromGeneric' [Field]
agdaLibFields

-- | Given a list of 'Field' descriptors (with their custom parsers),
--   parse a 'GenericFile' into the 'AgdaLibFile' structure.
--
--   Checks mandatory fields are present;
--   no duplicate fields, no unknown fields.

fromGeneric' :: [Field] -> GenericFile -> P AgdaLibFile
fromGeneric' :: [Field] -> GenericFile -> P AgdaLibFile
fromGeneric' [Field]
fields GenericFile
fs = do
  [Field]
-> [String] -> ExceptT LibParseError (Writer [LibWarning']) ()
checkFields [Field]
fields (forall a b. (a -> b) -> [a] -> [b]
map GenericEntry -> String
geHeader GenericFile
fs)
  forall (t :: * -> *) (m :: * -> *) b a.
(Foldable t, Monad m) =>
(b -> a -> m b) -> b -> t a -> m b
foldM AgdaLibFile -> GenericEntry -> P AgdaLibFile
upd AgdaLibFile
emptyLibFile GenericFile
fs
  where
    upd :: AgdaLibFile -> GenericEntry -> P AgdaLibFile
    upd :: AgdaLibFile -> GenericEntry -> P AgdaLibFile
upd AgdaLibFile
l (GenericEntry String
h [String]
cs) = do
      Maybe Field
mf <- String -> [Field] -> P (Maybe Field)
findField String
h [Field]
fields
      case Maybe Field
mf of
        Just Field{Bool
String
LensSet a AgdaLibFile
[String] -> P a
fSet :: LensSet a AgdaLibFile
fParse :: [String] -> P a
fOptional :: Bool
fName :: String
fSet :: ()
fParse :: ()
fOptional :: Field -> Bool
fName :: Field -> String
..} -> do
          a
x <- [String] -> P a
fParse [String]
cs
          forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$ LensSet a AgdaLibFile
fSet a
x AgdaLibFile
l
        Maybe Field
Nothing -> forall (m :: * -> *) a. Monad m => a -> m a
return AgdaLibFile
l

-- | Ensure that there are no duplicate fields and no mandatory fields are missing.
checkFields :: [Field] -> [String] -> P ()
checkFields :: [Field]
-> [String] -> ExceptT LibParseError (Writer [LibWarning']) ()
checkFields [Field]
fields [String]
fs = do
  -- Report missing mandatory fields.
  () <- forall m a. Null m => [a] -> (List1 a -> m) -> m
List1.unlessNull [String]
missing forall a b. (a -> b) -> a -> b
$ forall e (m :: * -> *) a. MonadError e m => e -> m a
throwError forall b c a. (b -> c) -> (a -> b) -> a -> c
. List1 String -> LibParseError
MissingFields
  -- Report duplicate fields.
  forall m a. Null m => [a] -> (List1 a -> m) -> m
List1.unlessNull (forall a. Ord a => [a] -> [a]
duplicates [String]
fs) forall a b. (a -> b) -> a -> b
$ forall e (m :: * -> *) a. MonadError e m => e -> m a
throwError forall b c a. (b -> c) -> (a -> b) -> a -> c
. List1 String -> LibParseError
DuplicateFields
  where
  mandatory :: [String]
  mandatory :: [String]
mandatory = [ Field -> String
fName Field
f | Field
f <- [Field]
fields, Bool -> Bool
not forall a b. (a -> b) -> a -> b
$ Field -> Bool
fOptional Field
f ]
  missing   :: [String]
  missing :: [String]
missing   = [String]
mandatory forall a. Eq a => [a] -> [a] -> [a]
List.\\ [String]
fs

-- | Find 'Field' with given 'fName', throw error if unknown.
findField :: String -> [Field] -> P (Maybe Field)
findField :: String -> [Field] -> P (Maybe Field)
findField String
s [Field]
fs = forall b a. b -> (a -> b) -> Maybe a -> b
maybe P (Maybe Field)
err (forall (m :: * -> *) a. Monad m => a -> m a
return forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. a -> Maybe a
Just) forall a b. (a -> b) -> a -> b
$ forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
List.find ((String
s forall a. Eq a => a -> a -> Bool
==) forall b c a. (b -> c) -> (a -> b) -> a -> c
. Field -> String
fName) [Field]
fs
  where err :: P (Maybe Field)
err = LibWarning' -> ExceptT LibParseError (Writer [LibWarning']) ()
warningP (String -> LibWarning'
UnknownField String
s) forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> forall (m :: * -> *) a. Monad m => a -> m a
return forall a. Maybe a
Nothing

-- Generic file parser ----------------------------------------------------

-- | Example:
--
-- @
--     parseGeneric "name:Main--BLA\ndepend:--BLA\n  standard-library--BLA\ninclude : . --BLA\n  src more-src   \n"
--     == Right [("name",["Main"]),("depend",["standard-library"]),("include",[".","src more-src"])]
-- @
parseGeneric :: String -> P GenericFile
parseGeneric :: String -> P GenericFile
parseGeneric String
s =
  [GenericLine] -> P GenericFile
groupLines forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> forall (m :: * -> *) a b c.
Applicative m =>
(a -> b -> m c) -> [a] -> [b] -> m [c]
zipWithM LineNumber
-> String
-> ExceptT LibParseError (Writer [LibWarning']) [GenericLine]
parseLine [LineNumber
1..] (forall a b. (a -> b) -> [a] -> [b]
map String -> String
stripComments forall a b. (a -> b) -> a -> b
$ String -> [String]
lines String
s)

-- | Lines with line numbers.
data GenericLine
  = Header  LineNumber String
      -- ^ Header line, like a field name, e.g. "include :".  Cannot be indented.
      --   @String@ is 'trim'med.
  | Content LineNumber String
      -- ^ Other line.  Must be indented.
      --   @String@ is 'trim'med.
  deriving (LineNumber -> GenericLine -> String -> String
[GenericLine] -> String -> String
GenericLine -> String
forall a.
(LineNumber -> a -> String -> String)
-> (a -> String) -> ([a] -> String -> String) -> Show a
showList :: [GenericLine] -> String -> String
$cshowList :: [GenericLine] -> String -> String
show :: GenericLine -> String
$cshow :: GenericLine -> String
showsPrec :: LineNumber -> GenericLine -> String -> String
$cshowsPrec :: LineNumber -> GenericLine -> String -> String
Show)

-- | Parse line into 'Header' and 'Content' components.
--
--   Precondition: line comments and trailing whitespace have been stripped away.
--
--   Example file:
--
--   @
--     name: Main
--     depend:
--       standard-library
--     include: .
--       src more-src
--   @
--
--   This should give
--
--   @
--     [ Header  1 "name"
--     , Content 1 "Main"
--     , Header  2 "depend"
--     , Content 3 "standard-library"
--     , Header  4 "include"
--     , Content 4 "."
--     , Content 5 "src more-src"
--     ]
--   @
parseLine :: LineNumber -> String -> P [GenericLine]
parseLine :: LineNumber
-> String
-> ExceptT LibParseError (Writer [LibWarning']) [GenericLine]
parseLine LineNumber
_ String
"" = forall (f :: * -> *) a. Applicative f => a -> f a
pure []
parseLine LineNumber
l s :: String
s@(Char
c:String
_)
    -- Indented lines are 'Content'.
  | Char -> Bool
isSpace Char
c   = forall (f :: * -> *) a. Applicative f => a -> f a
pure [LineNumber -> String -> GenericLine
Content LineNumber
l forall a b. (a -> b) -> a -> b
$ String -> String
ltrim String
s]
    -- Non-indented lines are 'Header'.
  | Bool
otherwise   =
    case forall a. (a -> Bool) -> [a] -> ([a], [a])
break (forall a. Eq a => a -> a -> Bool
==Char
':') String
s of
      -- Headers are single words followed by a colon.
      -- Anything after the colon that is not whitespace is 'Content'.
      (String
h, Char
':' : String
r) ->
        case String -> [String]
words String
h of
          [String
h] -> forall (f :: * -> *) a. Applicative f => a -> f a
pure forall a b. (a -> b) -> a -> b
$ LineNumber -> String -> GenericLine
Header LineNumber
l String
h forall a. a -> [a] -> [a]
: [LineNumber -> String -> GenericLine
Content LineNumber
l String
r' | let r' :: String
r' = String -> String
ltrim String
r, Bool -> Bool
not (forall (t :: * -> *) a. Foldable t => t a -> Bool
null String
r')]
          []  -> forall e (m :: * -> *) a. MonadError e m => e -> m a
throwError forall a b. (a -> b) -> a -> b
$ LineNumber -> LibParseError
MissingFieldName LineNumber
l
          [String]
hs  -> forall e (m :: * -> *) a. MonadError e m => e -> m a
throwError forall a b. (a -> b) -> a -> b
$ LineNumber -> String -> LibParseError
BadFieldName LineNumber
l String
h
      (String, String)
_ -> forall e (m :: * -> *) a. MonadError e m => e -> m a
throwError forall a b. (a -> b) -> a -> b
$ LineNumber -> String -> LibParseError
MissingColonForField LineNumber
l (String -> String
ltrim String
s)

-- | Collect 'Header' and subsequent 'Content's into 'GenericEntry'.
--
--   Leading 'Content's?  That's an error.
--
groupLines :: [GenericLine] -> P GenericFile
groupLines :: [GenericLine] -> P GenericFile
groupLines [] = forall (f :: * -> *) a. Applicative f => a -> f a
pure []
groupLines (Content LineNumber
l String
c : [GenericLine]
_) = forall e (m :: * -> *) a. MonadError e m => e -> m a
throwError forall a b. (a -> b) -> a -> b
$ LineNumber -> LibParseError
ContentWithoutField LineNumber
l
groupLines (Header LineNumber
_ String
h : [GenericLine]
ls) = (String -> [String] -> GenericEntry
GenericEntry String
h [ String
c | Content LineNumber
_ String
c <- [GenericLine]
cs ] forall a. a -> [a] -> [a]
:) forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [GenericLine] -> P GenericFile
groupLines [GenericLine]
ls1
  where
    ([GenericLine]
cs, [GenericLine]
ls1) = forall a. (a -> Bool) -> [a] -> ([a], [a])
span GenericLine -> Bool
isContent [GenericLine]
ls
    isContent :: GenericLine -> Bool
isContent Content{} = Bool
True
    isContent Header{} = Bool
False

-- | Remove leading whitespace and line comment.
trimLineComment :: String -> String
trimLineComment :: String -> String
trimLineComment = String -> String
stripComments forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> String
ltrim

-- | Break a comma-separated string.  Result strings are @trim@med.
splitCommas :: String -> [String]
splitCommas :: String -> [String]
splitCommas = String -> [String]
words forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b. (a -> b) -> [a] -> [b]
map (\Char
c -> if Char
c forall a. Eq a => a -> a -> Bool
== Char
',' then Char
' ' else Char
c)

-- | ...and trailing, but not leading, whitespace.
stripComments :: String -> String
stripComments :: String -> String
stripComments String
"" = String
""
stripComments (Char
'-':Char
'-':Char
c:String
_) | Char -> Bool
isSpace Char
c = String
""
stripComments (Char
c : String
s) = Char -> String -> String
cons Char
c (String -> String
stripComments String
s)
  where
    cons :: Char -> String -> String
cons Char
c String
"" | Char -> Bool
isSpace Char
c = String
""
    cons Char
c String
s = Char
c forall a. a -> [a] -> [a]
: String
s