packages feed

aihc-parser-3.0.1.0: test/Test/Performance/Suite.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.Performance.Suite
  ( parserPerformanceTests,
  )
where

import Aihc.Parser
import Aihc.Parser.Syntax (Extension (Arrows, PatternSynonyms, RecursiveDo, ViewPatterns), parseExtensionName)
import Control.DeepSeq (force)
import Control.Exception (evaluate)
import Data.Aeson ((.!=), (.:), (.:?))
import Data.Aeson.Types (parseEither, withObject)
import Data.Char (chr, toLower)
import Data.List (sort)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import Data.Text.IO qualified as TIO
import Data.Yaml qualified as Y
import ParserGolden (ExpectedStatus (..))
import System.Directory (doesDirectoryExist, listDirectory)
import System.FilePath (takeExtension, (</>))
import System.Timeout (timeout)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (Assertion, assertFailure, testCase, testCaseInfo)

data PerfCase = PerfCase
  { perfCaseId :: !String,
    perfCaseSourceName :: !FilePath,
    perfCaseExtensions :: ![Extension],
    perfCaseInput :: !Text,
    perfCaseStatus :: !ExpectedStatus,
    perfCaseReason :: !String
  }

fixtureRoot :: FilePath
fixtureRoot = "test/Test/Fixtures/performance/module"

timeoutMicros :: Int
timeoutMicros = 1000000

generatedCaseSize :: Int
generatedCaseSize = 200

-- | Depth for nested list-pattern cases. Keep the source below 10KiB.
listPatternSize :: Int
listPatternSize = 4000

-- | Depth for nested record-pattern cases. Keep the source below 10KiB.
recordPatternSize :: Int
recordPatternSize = 2000

-- | Depth for nested view-pattern cases. Keep the source below 10KiB.
viewPatternSize :: Int
viewPatternSize = 1200

-- | Depth for nested proc-do command parentheses. Keep the source below 10KiB.
procDoParenSize :: Int
procDoParenSize = 2000

parserPerformanceTests :: IO TestTree
parserPerformanceTests = do
  fixtureCases <- loadPerfCases
  pure $
    testGroup
      "performance"
      [ testGroup
          "parse-under-1s"
          [ testGroup "fixtures" (map mkPerfCaseTest fixtureCases),
            testGroup "generated" (map mkPerfCaseTest generatedPerfCases)
          ]
      ]

mkPerfCaseTest :: PerfCase -> TestTree
mkPerfCaseTest perfCase = case perfCaseStatus perfCase of
  StatusXFail -> testCaseInfo (perfCaseId perfCase) (xfailDetails perfCase <* assertPerfCase perfCase)
  _ -> testCase (perfCaseId perfCase) (assertPerfCase perfCase)

xfailDetails :: PerfCase -> IO String
xfailDetails perfCase = do
  outcome <-
    timeout timeoutMicros $
      evaluate $
        force $
          parseModule
            defaultConfig
              { parserSourceName = perfCaseSourceName perfCase,
                parserExtensions = perfCaseExtensions perfCase
              }
            (perfCaseInput perfCase)
  case outcome of
    Nothing -> pure ("known bug still present: module parse exceeded " <> show timeoutMicros <> "us")
    Just (errs, _)
      | null errs -> assertFailure "expected xfail performance case to still fail"
      | otherwise ->
          pure
            ( "known bug still present: "
                <> formatParseErrors (perfCaseSourceName perfCase) (Just (perfCaseInput perfCase)) errs
            )

assertPerfCase :: PerfCase -> Assertion
assertPerfCase perfCase = do
  outcome <-
    timeout timeoutMicros $
      evaluate $
        force $
          parseModule
            defaultConfig
              { parserSourceName = perfCaseSourceName perfCase,
                parserExtensions = perfCaseExtensions perfCase
              }
            (perfCaseInput perfCase)
  case (perfCaseStatus perfCase, outcome) of
    (status, Nothing)
      | status /= StatusXFail ->
          assertFailure
            ( "module parse exceeded "
                <> show timeoutMicros
                <> "us for "
                <> perfCaseId perfCase
            )
    (StatusPass, Just (errs, _))
      | not (null errs) ->
          assertFailure
            ( "expected parse success for performance case "
                <> perfCaseId perfCase
                <> ", got parse error: "
                <> formatParseErrors (perfCaseSourceName perfCase) (Just (perfCaseInput perfCase)) errs
            )
    (StatusFail, Just (errs, _))
      | null errs -> assertFailure ("expected parse failure for performance case " <> perfCaseId perfCase)
    (StatusXFail, Nothing) -> pure ()
    (StatusXFail, Just (errs, _))
      | null errs ->
          assertFailure
            ( "Unexpected pass in xfail performance case "
                <> perfCaseId perfCase
                <> " reason="
                <> perfCaseReason perfCase
            )
    _ -> pure ()

loadPerfCases :: IO [PerfCase]
loadPerfCases = do
  exists <- doesDirectoryExist fixtureRoot
  if not exists
    then pure []
    else do
      paths <- listFixtureFiles fixtureRoot
      mapM loadPerfCase paths

loadPerfCase :: FilePath -> IO PerfCase
loadPerfCase path = do
  source <- TIO.readFile path
  case parsePerfCaseText path source of
    Left err -> fail err
    Right perfCase -> pure perfCase

parsePerfCaseText :: FilePath -> Text -> Either String PerfCase
parsePerfCaseText path source = do
  value <-
    case Y.decodeEither' (TE.encodeUtf8 source) of
      Left err -> Left ("Invalid performance fixture " <> path <> ": " <> Y.prettyPrintParseException err)
      Right parsed -> Right parsed
  (extNames, inputText, statusText, reasonText) <-
    case parseEither
      ( withObject "performance fixture" $ \obj -> do
          exts <- obj .:? "extensions" .!= []
          inputText <- obj .: "input"
          status <- obj .:? "status" .!= "pass"
          reason <- obj .:? "reason" .!= ""
          pure (exts, inputText, status, reason)
      )
      value of
      Left err -> Left ("Invalid performance fixture schema in " <> path <> ": " <> err)
      Right parsed -> Right parsed
  exts <- traverse (parseExtension path) extNames
  status <- parseStatus path statusText
  pure
    PerfCase
      { perfCaseId = dropRootPrefix path,
        perfCaseSourceName = dropRootPrefix path,
        perfCaseExtensions = exts,
        perfCaseInput = inputText,
        perfCaseStatus = status,
        perfCaseReason = T.unpack reasonText
      }
  where
    parseExtension fixturePath raw =
      case parseExtensionName raw of
        Just ext -> Right ext
        Nothing -> Left ("Unknown parser extension " <> show raw <> " in " <> fixturePath)
    parseStatus fixturePath raw =
      case map toLower (T.unpack (T.strip raw)) of
        "pass" -> Right StatusPass
        "fail" -> Right StatusFail
        "xfail" -> Right StatusXFail
        _ -> Left ("Invalid [status] in " <> fixturePath <> ": " <> T.unpack raw)

listFixtureFiles :: FilePath -> IO [FilePath]
listFixtureFiles dir = do
  entries <- sort <$> listDirectory dir
  concat
    <$> mapM
      ( \entry -> do
          let path = dir </> entry
          isDir <- doesDirectoryExist path
          if isDir
            then listFixtureFiles path
            else
              if map toLower (takeExtension path) `elem` [".yaml", ".yml"]
                then pure [path]
                else pure []
      )
      entries

dropRootPrefix :: FilePath -> FilePath
dropRootPrefix path =
  case splitAt (length fixtureRoot + 1) path of
    (prefix, rest)
      | prefix == fixtureRoot <> "/" -> rest
    _ -> path

generatedPerfCases :: [PerfCase]
generatedPerfCases =
  [ mkGeneratedPerfCase "tuple-expression-nested" (mkExprModule (nestedTupleExpr generatedCaseSize)),
    mkGeneratedPerfCase "tuple-expression-wide" (mkExprModule (wideTupleExpr generatedCaseSize)),
    mkGeneratedPerfCase "expression-list" (mkExprModule (longListExpr generatedCaseSize)),
    mkGeneratedPerfCase "tuple-type-nested" (mkTypeModule (nestedTupleType generatedCaseSize)),
    mkGeneratedPerfCase "tuple-type-wide" (mkTypeModule (wideTupleType generatedCaseSize)),
    mkGeneratedPerfCase "tuple-pattern-nested" (mkPatternModule (nestedTuplePattern generatedCaseSize)),
    mkGeneratedPerfCase "tuple-pattern-wide" (mkPatternModule (wideTuplePattern generatedCaseSize)),
    mkGeneratedPerfCase "tuple-pattern-function-nested" (mkTuplePatternFunctionModule (nestedTuplePattern generatedCaseSize)),
    mkGeneratedPerfCase "tuple-pattern-function-wide" (mkTuplePatternFunctionModule (wideTuplePattern generatedCaseSize)),
    mkGeneratedPerfCase "enum-data-constructors" (mkDataModule (enumDataDecl generatedCaseSize)),
    mkGeneratedPerfCase "record-data-fields" (mkDataModule (recordDataDecl generatedCaseSize)),
    mkGeneratedPerfCase "type-right-leaning-terms" (mkTypeModule (rightLeaningType generatedCaseSize)),
    mkGeneratedPerfCase "type-left-leaning-terms" (mkTypeModule (leftLeaningType generatedCaseSize)),
    mkGeneratedPerfCase "type-parameters" (mkTypeModule (typeWithParameters generatedCaseSize)),
    mkGeneratedPerfCase "string-escapes" (mkExprModule (escapedStringExpr (generatedCaseSize * 500))),
    mkGeneratedPerfCase "nested-application" (mkExprModule (nestedAppExpr generatedCaseSize)),
    mkGeneratedPerfCaseWithStatus "invalid-module" "module Generated where\nvalue = { x = 1, }\n" StatusFail "",
    -- Nested block expressions must not be parsed again as patterns.
    mkGeneratedPerfCase "nested-paren-do" (mkExprModule (nestedParenDoExpr generatedCaseSize)),
    mkGeneratedPerfCase "nested-paren-do-case" (mkExprModule (nestedParenDoCaseExpr generatedCaseSize)),
    mkGeneratedPerfCase "nested-paren-do-if" (mkExprModule (nestedParenDoIfExpr generatedCaseSize)),
    mkGeneratedPerfCase "nested-paren-do-lambda" (mkExprModule (nestedParenDoLambdaExpr generatedCaseSize)),
    mkGeneratedPerfCase "nested-paren-do-let" (mkExprModule (nestedParenDoLetExpr generatedCaseSize)),
    mkGeneratedPerfCase "nested-paren-do-comp" (mkCompModule (nestedParenDoExpr generatedCaseSize)),
    mkGeneratedPerfCase "nested-paren-do-guard" (mkGuardModule (nestedParenDoExpr generatedCaseSize)),
    mkGeneratedPerfCase
      "nested-paren-do-bind"
      (mkExprModule (nestedWrap "do { x <- (" "); pure x }" "pure 1" generatedCaseSize)),
    mkGeneratedPerfCaseFull
      "nested-paren-mdo"
      generatedCaseSize
      [RecursiveDo]
      (mkExprModule ("mdo { (" <> nestedParenDoExpr generatedCaseSize <> ") }"))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "malformed-nested-proc-do-blocks"
      generatedCaseSize
      [Arrows]
      (mkProcModule (nestedParenDoExpr generatedCaseSize))
      StatusFail
      "",
    mkGeneratedPerfCaseFull
      "nested-proc-do-blocks"
      generatedCaseSize
      [Arrows]
      (mkProcModule (nestedWrap "do { (" ") }" "returnA -< x" generatedCaseSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "malformed-nested-let"
      generatedCaseSize
      []
      (mkExprModule (malformedNestedLetExpr generatedCaseSize))
      StatusFail
      "",
    -- Nested patterns must reuse the expression parse.
    mkGeneratedPerfCaseFull
      "nested-list-pattern"
      listPatternSize
      []
      (mkTuplePatternFunctionModule (nestedListPattern listPatternSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "nested-list-pattern-bind"
      listPatternSize
      []
      (mkPatBindModule (nestedListPattern listPatternSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "nested-list-pattern-case"
      listPatternSize
      []
      (mkCaseModule (nestedListPattern listPatternSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "nested-list-pattern-lambda"
      listPatternSize
      []
      (mkLambdaModule (nestedListPattern listPatternSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "nested-list-pattern-do"
      listPatternSize
      []
      (mkDoStmtModule (nestedListPattern listPatternSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "nested-list-pattern-guard"
      listPatternSize
      []
      (mkGuardModule (nestedListPattern listPatternSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "nested-list-pattern-comp"
      listPatternSize
      []
      (mkCompModule (nestedListPattern listPatternSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "nested-list-pattern-where"
      listPatternSize
      []
      (mkWherePatModule (nestedListPattern listPatternSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "nested-list-pattern-synonym"
      listPatternSize
      [PatternSynonyms]
      (mkPatSynModule (nestedListPattern listPatternSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "nested-list-tuple-pattern"
      listPatternSize
      []
      (mkTuplePatternFunctionModule (nestedWrap "[" "]" "(x, y)" listPatternSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "nested-record-pattern"
      recordPatternSize
      []
      (mkTuplePatternFunctionModule (nestedRecordPattern recordPatternSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "nested-record-pattern-bind"
      recordPatternSize
      []
      (mkPatBindModule (nestedRecordPattern recordPatternSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "nested-record-pattern-case"
      recordPatternSize
      []
      (mkCaseModule (nestedRecordPattern recordPatternSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "nested-record-pattern-lambda"
      recordPatternSize
      []
      (mkLambdaModule (nestedRecordPattern recordPatternSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "nested-view-pattern"
      viewPatternSize
      [ViewPatterns]
      (mkTuplePatternFunctionModule (nestedViewPattern viewPatternSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "nested-view-pattern-bind"
      viewPatternSize
      [ViewPatterns]
      (mkPatBindModule (nestedViewPattern viewPatternSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "nested-view-pattern-case"
      viewPatternSize
      [ViewPatterns]
      (mkCaseModule (nestedViewPattern viewPatternSize))
      StatusPass
      "",
    mkGeneratedPerfCaseFull
      "nested-proc-do-parens"
      procDoParenSize
      [Arrows]
      (mkProcModule ("do { " <> nestedWrap "(" ")" "returnA -< x" procDoParenSize <> " }"))
      StatusPass
      ""
  ]

mkGeneratedPerfCase :: String -> Text -> PerfCase
mkGeneratedPerfCase label inputText =
  mkGeneratedPerfCaseWithStatus label inputText StatusPass ""

mkGeneratedPerfCaseWithStatus :: String -> Text -> ExpectedStatus -> String -> PerfCase
mkGeneratedPerfCaseWithStatus label =
  mkGeneratedPerfCaseFull label generatedCaseSize []

mkGeneratedPerfCaseFull :: String -> Int -> [Extension] -> Text -> ExpectedStatus -> String -> PerfCase
mkGeneratedPerfCaseFull label size exts inputText status reason =
  let caseId = "generated/" <> label <> "-" <> show size <> ".hs"
   in PerfCase
        { perfCaseId = caseId,
          perfCaseSourceName = caseId,
          perfCaseExtensions = exts,
          perfCaseInput = inputText,
          perfCaseStatus = status,
          perfCaseReason = reason
        }

mkExprModule :: Text -> Text
mkExprModule expr = T.unlines ["module Generated where", "value = " <> expr]

mkTypeModule :: Text -> Text
mkTypeModule ty = T.unlines ["module Generated where", "value :: " <> ty, "value = undefined"]

mkPatternModule :: Text -> Text
mkPatternModule pat = T.unlines ["module Generated where", "value " <> pat <> " = x1", "  where", "    x1 = 1"]

mkTuplePatternFunctionModule :: Text -> Text
mkTuplePatternFunctionModule pat = T.unlines ["module Generated where", "fn " <> pat <> " = ()"]

mkDataModule :: Text -> Text
mkDataModule decl = T.unlines ["module Generated where", decl]

mkPatBindModule :: Text -> Text
mkPatBindModule pat = T.unlines ["module Generated where", pat <> " = ()"]

mkCaseModule :: Text -> Text
mkCaseModule pat = T.unlines ["module Generated where", "fn v = case v of", "  " <> pat <> " -> ()"]

mkLambdaModule :: Text -> Text
mkLambdaModule pat = T.unlines ["module Generated where", "fn = \\" <> pat <> " -> ()"]

mkDoStmtModule :: Text -> Text
mkDoStmtModule stmt = T.unlines ["module Generated where", "fn = do", "  " <> stmt, "  pure ()"]

mkGuardModule :: Text -> Text
mkGuardModule guard = T.unlines ["module Generated where", "fn | " <> guard <> " = ()"]

mkCompModule :: Text -> Text
mkCompModule qual = T.unlines ["module Generated where", "fn = [() | " <> qual <> "]"]

mkWherePatModule :: Text -> Text
mkWherePatModule pat = T.unlines ["module Generated where", "fn = ()", "  where", "    " <> pat <> " = ()"]

mkPatSynModule :: Text -> Text
mkPatSynModule pat = T.unlines ["module Generated where", "pattern P = " <> pat]

mkProcModule :: Text -> Text
mkProcModule cmd = T.unlines ["module Generated where", "fn = proc x -> " <> cmd]

-- | Wrap an inner term in @n@ copies of an open/close pair.
nestedWrap :: Text -> Text -> Text -> Int -> Text
nestedWrap open close inner n =
  T.concat (replicate n open) <> inner <> T.concat (replicate n close)

nestedParenDoExpr :: Int -> Text
nestedParenDoExpr = nestedWrap "do { (" ") }" "pure 1"

nestedParenDoCaseExpr :: Int -> Text
nestedParenDoCaseExpr = nestedWrap "do { (case x of { _ -> " " }) }" "1"

nestedParenDoIfExpr :: Int -> Text
nestedParenDoIfExpr = nestedWrap "do { (if True then " " else 0) }" "1"

nestedParenDoLambdaExpr :: Int -> Text
nestedParenDoLambdaExpr = nestedWrap "do { (\\x -> " ") }" "x"

nestedParenDoLetExpr :: Int -> Text
nestedParenDoLetExpr = nestedWrap "do { (let x = " " in x) }" "1"

malformedNestedLetExpr :: Int -> Text
malformedNestedLetExpr n = nestedWrap "let x = " " in " "1" n <> "x"

nestedListPattern :: Int -> Text
nestedListPattern = nestedWrap "[" "]" "x"

nestedRecordPattern :: Int -> Text
nestedRecordPattern = nestedWrap "T{x=" "}" "y"

nestedViewPattern :: Int -> Text
nestedViewPattern = nestedWrap "(id -> " ")" "x"

nestedTupleExpr :: Int -> Text
nestedTupleExpr n =
  case n of
    0 -> "a"
    _ -> "(" <> "a, " <> nestedTupleExpr (n - 1) <> ")"

wideTupleExpr :: Int -> Text
wideTupleExpr n = tupleText n "a"

longListExpr :: Int -> Text
longListExpr n = "[" <> T.intercalate ", " (replicate n "a") <> "]"

nestedTupleType :: Int -> Text
nestedTupleType n =
  case n of
    0 -> "A"
    _ -> "(" <> "A, " <> nestedTupleType (n - 1) <> ")"

wideTupleType :: Int -> Text
wideTupleType n = tupleText n "A"

nestedTuplePattern :: Int -> Text
nestedTuplePattern n =
  case n of
    0 -> "x1"
    _ -> "(" <> T.pack ("x" <> show (n + 1)) <> ", " <> nestedTuplePattern (n - 1) <> ")"

wideTuplePattern :: Int -> Text
wideTuplePattern n = tupleItemsText (patternVars n)

enumDataDecl :: Int -> Text
enumDataDecl n =
  "data T = "
    <> T.intercalate " | " [T.pack ("C" <> show ix) | ix <- [1 .. n]]

recordDataDecl :: Int -> Text
recordDataDecl n =
  "data T = T\n  { "
    <> T.intercalate "\n  , " [T.pack ("field" <> show ix) <> " :: A" | ix <- [1 .. n]]
    <> "\n  }"

rightLeaningType :: Int -> Text
rightLeaningType n =
  case n of
    0 -> "A"
    1 -> "A"
    _ -> "A -> (" <> rightLeaningType (n - 1) <> ")"

leftLeaningType :: Int -> Text
leftLeaningType n =
  case n of
    0 -> "A"
    1 -> "A"
    _ -> "(" <> leftLeaningType (n - 1) <> " -> A)"

typeWithParameters :: Int -> Text
typeWithParameters n =
  T.unwords ("A" : replicate n "a")

escapedStringExpr :: Int -> Text
escapedStringExpr desiredLength =
  T.concat ["\"", takeEscapedText desiredLength (cycle escapedStringFragments), "\""]

escapedStringFragments :: [Text]
escapedStringFragments =
  [shownCharBody (chr n) | n <- [0 .. 255]]
  where
    shownCharBody ch =
      let shown = show [ch]
       in T.pack (take (length shown - 2) (drop 1 shown))

takeEscapedText :: Int -> [Text] -> Text
takeEscapedText desiredLength = go 0
  where
    go accLen (fragment : rest)
      | accLen >= desiredLength = ""
      | otherwise =
          fragment <> go (accLen + T.length fragment) rest
    go _ [] = ""

tupleText :: Int -> Text -> Text
tupleText n atom =
  tupleItemsText (replicate n atom)

tupleItemsText :: [Text] -> Text
tupleItemsText items =
  "(" <> T.intercalate ", " items <> ")"

patternVars :: Int -> [Text]
patternVars n = [T.pack ("x" <> show ix) | ix <- [1 .. n]]

-- | Generate deeply nested constructor application: A(A(A(...A(a)...)))
-- This exercises the paren expression parser's performance under deep nesting.
nestedAppExpr :: Int -> Text
nestedAppExpr n =
  case n of
    0 -> "a"
    _ -> "A(" <> nestedAppExpr (n - 1) <> ")"