packages feed

caliper-0.1.0.0: src/Caliper/Syntax/Parsing.hs

module Caliper.Syntax.Parsing where

import Control.DeepSeq
import Control.Monad
import Data.Bifunctor qualified as Bi
import Data.Char
import Data.Function
import Data.List.NonEmpty qualified as NE
import Data.Map qualified as M
import Data.Text qualified as T
import Data.Time.Calendar
import Data.Time.LocalTime
import Data.Void
import Text.Megaparsec hiding (ParseError)
import Text.Megaparsec.Char

import Caliper.Syntax.Ast
import Caliper.Syntax.Position

type Parser = Parsec Void T.Text

drainLine :: Parser prefix -> Parser ()
drainLine p = do
  _ <- many (p & consumeCommentBlock)
  -- Think of sequence as products, failure is mzero and would make the entire chain 0.
  -- Here, if there are some spaces followed by something /not eol/, eol would fail.
  -- This would make entire part fail whilst hspace has consumed some space.
  -- We then backtrack the parser in such case.
  _ <- (p & consumeCommentLine) <|> try (hspace *> void eol)
  pure ()

consumeCommentBlock :: Parser prefix -> Parser ()
consumeCommentBlock p = hidden $ do
  _ <- try $ p *> "{-"
  _ <- manyTill (nested <|> void anySingle) (void "-}")
  pure ()
  where
    nested = pure () & consumeCommentBlock

consumeCommentLine :: Parser prefix -> Parser ()
consumeCommentLine p = hidden $ do
  _ <- try $ p *> "--"
  _ <- manyTill anySingle (void eol <|> eof)
  pure ()

caliperAst :: Parser RawAst
caliperAst =
  many (pure () & drainLine)
    *> ( RawAst
           <$> (T.pack . sourceName <$> getSourcePos)
           <*> typeDecls
           <*> entriesOrTZ
       )
    <* eof

typeDecls :: Parser [T.Text]
typeDecls = typeDecl `sepEndBy` many (hspace & drainLine)

typeDecl :: Parser T.Text
typeDecl = label "type declaration" $ do
  _ <- "type" *> hspace1
  t <- T.pack <$> some valueChar
  _ <- hspace & drainLine
  pure t

entriesOrTZ :: Parser [EntriesOrTZ]
entriesOrTZ =
  many $
    (RawEntries <$> entries)
      <|> (TZDecl <$> tzDecl)

entries :: Parser [RawEntry]
entries = entry `sepEndBy1` many (hspace & drainLine)

tzDecl :: Parser TimeZone
tzDecl = do
  _ <- "timezone" *> hspace1
  sign <- (-1) <$ char '-' <|> 1 <$ char '+' <|> pure 1
  hh <- (try (decimalOfLength 2) <|> decimalOfLength 1) <* sep
  mm <- decimalOfLength 2
  _ <- many (hspace & drainLine)
  pure $ TimeZone (sign * (hh * 60 + mm)) False ""
  where
    sep :: Parser Char
    sep = satisfy (not . isDigit) <?> "non-digit separator"

entry :: Parser RawEntry
entry = label "entry" $ do
  entryStart <- localTime
  _ <- sep *> "..." <* sep
  entryEnd <- localTime
  _ <- hspace & drainLine -- End of line comment
  tags <-
    many (hspace & drainLine) -- Leading inner comment
      *> metadata

  -- This part creates thunk build-up, force the thunk here
  let re =
        RawEntry
          { rawEntryStart = fst entryStart
          , rawEntryEnd = entryEnd
          , rawEntryMetadata = tags
          }
  re `deepseq` pure re
  where
    sep :: Parser ()
    sep = void $ many go
      where
        go = hspace1 <|> (pure () & consumeCommentBlock)

-- | A time in the format of yyyy-mm-dd-hh-mm-ss, where dashes are a single non digit separator.
localTime :: Parser (LocalTime, SrcSpan)
localTime = do
  mkSrcSpan <- srcSpan
  startPos <- srcPosition
  t <- LocalTime <$> (parseDay <* sep) <*> parseTime
  endPos <- srcPosition
  pure (t, mkSrcSpan startPos endPos)
  where
    sep :: Parser Char
    sep = satisfy (not . isDigit) <?> "non-digit separator"

    parseDay :: Parser Day
    parseDay = do
      backtrack <- getOffset
      yyyy <- decimalOfLength 4 <* sep
      mm <- decimalOfLength 2 <* sep
      dd <- decimalOfLength 2
      case fromGregorianValid yyyy mm dd of
        Nothing -> do
          setOffset backtrack
          unexpected (Label $ 'i' NE.:| "nvalid calendar date")
        Just x -> pure x

    parseTime :: Parser TimeOfDay
    parseTime = do
      backtrack <- getOffset
      hh <- decimalOfLength 2 <* sep
      mm <- decimalOfLength 2 <* sep
      ss <- (decimalOfLength 2) <|> (0 <$ "__") -- unknown second notation
      case makeTimeOfDayValid hh mm ss of
        Nothing -> do
          setOffset backtrack
          unexpected (Label $ 'i' NE.:| "nvalid time")
        Just x -> pure x

decimalOfLength :: Num a => Int -> Parser a
decimalOfLength = fmap (fromIntegral . foldl' go 0) . flip replicateM (satisfy isDigit)
  where
    go :: Int -> Char -> Int
    go acc x = acc * 10 + fromEnum x - fromEnum '0'

metadata :: Parser (M.Map T.Text (NE.NonEmpty (T.Text, SrcSpan)))
metadata =
  finalize
    . M.fromListWith (<>)
    . nonEmptyValue
    <$> (hspace1 & keyValuePair) `sepEndBy` many (hspace & drainLine)
  where
    finalize = fmap NE.reverse

    nonEmptyValue :: [(a, b)] -> [(a, NE.NonEmpty b)]
    nonEmptyValue = map (Bi.second pure)

keyValuePair :: Parser prefix -> Parser (T.Text, (T.Text, SrcSpan))
keyValuePair p = do
  mkSrcSpan <- srcSpan
  (startPos, k) <- try $ do
    _ <- p
    liftA2 (,) srcPosition (T.pack <$> some keyChar)
  _ <- single ':'
  v <- T.pack <$> some valueChar
  endPos <- srcPosition
  let spn = mkSrcSpan startPos endPos
  pure (k, (v, spn))

keyChar :: Parser Char
keyChar = letterChar

valueChar :: Parser Char
valueChar = alphaNumChar <|> char '\'' -- we haskellers gotta allow ' in names

srcSpan :: Parser (SrcPosition -> SrcPosition -> SrcSpan)
srcSpan = do
  SourcePos {sourceName = fname} <- getSourcePos
  pure (SrcSpan (T.pack fname))

srcPosition :: Parser SrcPosition
srcPosition = do
  SourcePos
    { sourceLine = l
    , sourceColumn = c
    } <-
    getSourcePos
  pure (SrcPosition (unPos l) (unPos c))