packages feed

moonlight-core-0.1.0.0: src-basis/Moonlight/Core/ProofManifest.hs

-- | Pure JSON rendering and parsing for theorem manifests.
module Moonlight.Core.ProofManifest
  ( ProofManifestError (..),
    renderTheoremManifestJson,
    parseTheoremManifestNames,
    canonicalTheoremManifestNames,
  )
where

import Data.Char (chr, digitToInt, isHexDigit, isSpace, ord)
import Data.Kind (Type)
import Data.List (find, intercalate)
import Data.Maybe (listToMaybe)
import Data.Set qualified as Set
import Moonlight.Core.Dedup (firstDuplicate)
import Text.ParserCombinators.ReadP
  ( ReadP,
    char,
    eof,
    many,
    pfail,
    readP_to_S,
    satisfy,
    sepBy,
    skipSpaces,
    string,
    (+++),
  )
import Prelude

type ProofManifestError :: Type
data ProofManifestError
  = ProofManifestParseFailure
  | EmptyTheoremManifestName
  | WhitespacePaddedTheoremManifestName !String
  | DuplicateTheoremManifestName !String
  deriving stock (Eq, Show)

renderTheoremManifestJson :: [String] -> String
renderTheoremManifestJson theoremIdentifiers =
  "{\"theorems\":["
    <> intercalate "," (map quoteJsonString (canonicalTheoremManifestNames theoremIdentifiers))
    <> "]}"

parseTheoremManifestNames :: String -> Either ProofManifestError [String]
parseTheoremManifestNames source =
  parseManifestJson source >>= validateTheoremManifestNames

canonicalTheoremManifestNames :: [String] -> [String]
canonicalTheoremManifestNames =
  Set.toAscList . Set.fromList

trimWhitespace :: String -> String
trimWhitespace =
  dropWhile isSpace
    . reverse
    . dropWhile isSpace
    . reverse

validateTheoremManifestNames :: [String] -> Either ProofManifestError [String]
validateTheoremManifestNames theoremNames =
  case find null theoremNames of
    Just _ ->
      Left EmptyTheoremManifestName
    Nothing ->
      case find hasPadding theoremNames of
        Just theoremName ->
          Left (WhitespacePaddedTheoremManifestName theoremName)
        Nothing ->
          case duplicateTheoremName theoremNames of
            Just theoremName ->
              Left (DuplicateTheoremManifestName theoremName)
            Nothing ->
              Right theoremNames
  where
    hasPadding theoremName = trimWhitespace theoremName /= theoremName

duplicateTheoremName :: [String] -> Maybe String
duplicateTheoremName =
  firstDuplicate

parseManifestJson :: String -> Either ProofManifestError [String]
parseManifestJson source =
  case listToMaybe (reverse parses) of
    Nothing -> Left ProofManifestParseFailure
    Just (theoremNames, _) -> Right theoremNames
  where
    parses = readP_to_S (theoremManifestParser <* skipSpaces <* eof) source

theoremManifestParser :: ReadP [String]
theoremManifestParser = do
  skipSpaces
  _ <- char '{'
  skipSpaces
  _ <- string "\"theorems\""
  skipSpaces
  _ <- char ':'
  theoremNames <- jsonStringArrayParser
  skipSpaces
  _ <- char '}'
  pure theoremNames

jsonStringArrayParser :: ReadP [String]
jsonStringArrayParser = do
  skipSpaces
  _ <- char '['
  skipSpaces
  theoremNames <- sepBy jsonStringParser (skipSpaces *> char ',' <* skipSpaces)
  skipSpaces
  _ <- char ']'
  pure theoremNames

jsonStringParser :: ReadP String
jsonStringParser = do
  _ <- char '"'
  stringValue <- many jsonStringCharacterParser
  _ <- char '"'
  pure stringValue

jsonStringCharacterParser :: ReadP Char
jsonStringCharacterParser =
  escapedCharacterParser +++ satisfy jsonStringUnescapedCharacter

escapedCharacterParser :: ReadP Char
escapedCharacterParser = do
  _ <- char '\\'
  char '"'
    +++ char '\\'
    +++ char '/'
    +++ (char 'n' >> pure '\n')
    +++ (char 't' >> pure '\t')
    +++ (char 'r' >> pure '\r')
    +++ (char 'b' >> pure '\b')
    +++ (char 'f' >> pure '\f')
    +++ unicodeEscapeParser

unicodeEscapeParser :: ReadP Char
unicodeEscapeParser = do
  _ <- char 'u'
  h3 <- satisfy isHexDigit
  h2 <- satisfy isHexDigit
  h1 <- satisfy isHexDigit
  h0 <- satisfy isHexDigit
  let codePoint = digitToInt h3 * 4096 + digitToInt h2 * 256 + digitToInt h1 * 16 + digitToInt h0
  if jsonStringScalarEscape codePoint
    then pure (chr codePoint)
    else pfail

jsonStringUnescapedCharacter :: Char -> Bool
jsonStringUnescapedCharacter character =
  character /= '"' && character /= '\\' && ord character >= 0x20

jsonStringScalarEscape :: Int -> Bool
jsonStringScalarEscape codePoint =
  codePoint < 0xD800 || codePoint > 0xDFFF

quoteJsonString :: String -> String
quoteJsonString value =
  "\"" <> concatMap escapeJsonCharacter value <> "\""

escapeJsonCharacter :: Char -> String
escapeJsonCharacter '"' = "\\\""
escapeJsonCharacter '\\' = "\\\\"
escapeJsonCharacter c
  | ord c < 0x20 = "\\u" <> padHex4 (ord c)
  | otherwise = [c]

padHex4 :: Int -> String
padHex4 n =
  let d3 = n `div` 4096
      d2 = (n `mod` 4096) `div` 256
      d1 = (n `mod` 256) `div` 16
      d0 = n `mod` 16
   in fmap hexDigit [d3, d2, d1, d0]
  where
    hexDigit digit
      | digit < 10 = chr (ord '0' + digit)
      | digit < 16 = chr (ord 'a' + digit - 10)
      | otherwise = '?'