tilia-0.1.2.0: bench/Tilia/Bench/Cases.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
-- | What the benchmarks of formatting measure: formatting a fixed sample of
-- the Hackage corpus and checking what that printed, and formatting modules
-- made to stress one part of the formatter each.
module Tilia.Bench.Cases
( Stage (..),
stageName,
Benchmark (..),
Subject (..),
corpusSubjects,
syntheticSubjects,
)
where
import Data.ByteString qualified as BS
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding (decodeUtf8')
import Data.Traversable (for)
import GHC.LanguageExtensions.Type (Extension)
import Tilia.Corpus (Example (..))
import Tilia.Cpp (usesCpp)
import Tilia.Format (rewritten)
import Tilia.Parser (parseModule)
import Tilia.TestConfig (formatCppExample, printExample)
import Tilia.TestUtils (effectiveExtensionsFor, parserConfigFor)
-- | What part of a run a benchmark measures.
data Stage
= -- | Formatting a module.
Format
| -- | Checking that what formatting printed is the program it was given,
-- as @--check-ast@ does.
Check
| -- | Reading what the modules of a package establish out of its tarball.
Resolve
| -- | The same, out of a cache that an earlier reading filled.
Recall
| -- | Decoding the interfaces of a package.
Decode
-- | A stage, as the record and the report spell it.
stageName :: Stage -> Text
stageName = \case
Format -> "format"
Check -> "check"
Resolve -> "resolve"
Recall -> "recall"
Decode -> "decode"
-- | A benchmark of something other than formatting a module.
data Benchmark = Benchmark
{ -- | What it measures.
benchmarkStage :: Stage,
-- | What it measures that on.
benchmarkName :: Text,
-- | Set one run up, giving the work, which says how much it computed.
benchmarkRun :: IO (IO Int)
}
-- | A module the benchmarks format, and check where they check it.
data Subject = Subject
{ -- | Its name in the corpus, or the stress it makes.
subjectName :: Text,
-- | The module.
subjectSource :: Text,
-- | Format it, or 'Nothing' where it is declined.
subjectFormat :: Text -> Maybe Text,
-- | Check what formatting it printed, giving what changed, where it is
-- checked.
subjectCheck :: Maybe (Text -> Maybe Text)
}
-- | The corpus modules formatted and checked, by the name the corpus gives
-- them: middling ones from many packages, a few with conditionals among
-- them.
checked :: [FilePath]
checked =
[ "haxl-2.5.1.1/Haxl/Core/Monad.hs",
"vector-0.13.2.0/src/Data/Vector/Generic.hs",
"aeson-2.3.1.0/src/Data/Aeson/KeyMap.hs",
"pandoc-3.10.2/src/Text/Pandoc/Class/PandocPure.hs",
"lifted-base-0.2.3.12/Control/Exception/Lifted.hs",
"text-2.1.4/tests/Tests/Properties/Folds.hs",
"ShellCheck-0.11.0/src/ShellCheck/Analytics.hs",
"Agda-2.8.0/src/full/Agda/Syntax/Translation/ConcreteToAbstract.hs",
"idris-1.3.4/src/Idris/Elab/Term.hs",
"pandoc-3.10.2/src/Text/Pandoc/Writers/Powerpoint/Output.hs",
"purescript-0.15.15/src/Language/PureScript/Errors.hs",
"pandoc-3.10.2/src/Text/Pandoc/Readers/Markdown.hs",
"esqueleto-3.6.0.3/test/PostgreSQL/Test.hs",
"unpacked-containers-0/src/Map/Internal.hs",
"purescript-0.15.15/src/Language/PureScript/TypeChecker/Types.hs",
"distributed-process-0.7.8/src/Control/Distributed/Process/Node.hs",
"servant-server-0.20.3.0/src/Servant/Server/Internal.hs",
"brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal\
\/Transformations/Alt.hs",
"xmonad-0.18.1/src/XMonad/Operations.hs",
"swagger2-2.9.1/src/Data/Swagger/Internal.hs",
"time-1.16.0.1/test/main/Test/Format/ParseTime.hs",
"optparse-applicative-0.19.0.0/tests/test.hs",
"blaze-html-0.9.2.0/src/Text/Blaze/Html5/Attributes.hs",
"tls-2.4.3/Network/TLS/Extension.hs",
"recursion-schemes-5.2.3/src/Data/Functor/Foldable.hs",
"megaparsec-9.8.1/Text/Megaparsec/Internal.hs"
]
-- | The corpus modules only formatted: the largest ones, whose checks would
-- take most of the time.
formattedOnly :: [FilePath]
formattedOnly =
[ "QuickCheck-2.18.0.0/src/Test/QuickCheck/Arbitrary.hs",
"unordered-containers-0.2.21/Data/HashMap/Internal.hs",
"lens-5.3.6/src/Control/Lens/Wrapped.hs",
"text-2.1.4/src/Data/Text/Internal/Fusion/CaseMapping.hs"
]
-- | The modules of the corpus measured, or which one the corpus does not
-- have.
corpusSubjects :: [Example] -> IO (Either Text [Subject])
corpusSubjects examples =
sequence
<$> for
(fmap (,True) checked <> fmap (,False) formattedOnly)
(uncurry load)
where
byName = Map.fromList [(exampleName e, e) | e <- examples]
load name isChecked = case Map.lookup name byName of
Nothing -> pure (Left ("the corpus has no " <> T.pack name))
Just example -> do
bytes <- BS.readFile (exampleInput example)
pure $ case decodeUtf8' bytes of
Left _ -> Left (T.pack name <> " is not valid UTF-8")
Right source ->
Right $
subjectOf
(exampleExtensions example)
isChecked
(T.pack name)
source
-- | Modules made to stress one part of the formatter each.
syntheticSubjects :: [Subject]
syntheticSubjects =
[ subjectOf Nothing False "synthetic/comments" commented,
subjectOf Nothing False "synthetic/imports" imported,
subjectOf Nothing False "synthetic/conditionals" conditional,
subjectOf Nothing False "synthetic/nesting" nested
]
where
commented =
T.unlines $
["module Commented where", ""]
<> concat
[ [ "-- | The function number " <> n <> ", which is documented.",
"f" <> n <> " :: Int -> Int -- the type",
"f" <> n <> " x =",
" {- a block comment -} x + " <> n <> " -- an end-of-line one",
""
]
| n <- numbers 1500
]
imported =
T.unlines $
["module Imported where", ""]
<> [ "import M" <> m <> " (f" <> n <> ", T" <> n <> " (..))"
| n <- numbers 4,
m <- numbers 250
]
<> ["", "x :: Int", "x = 1"]
conditional =
T.unlines $
["{-# LANGUAGE CPP #-}", "module Conditional where", ""]
<> concat
[ [ "#ifdef FLAG" <> n,
"g" <> n <> " :: Int",
"g" <> n <> " = " <> n,
"#else",
"g" <> n <> " :: Integer",
"g" <> n <> " = " <> n <> " + 1",
"#endif",
""
]
| n <- numbers 24
]
nested =
T.unlines
[ "module Nested where",
"",
"deep :: Int -> Int",
"deep x =",
" " <> foldr deeper "x" (numbers 40),
"",
"wide :: [Int]",
"wide = [" <> T.intercalate ", " (numbers 5000) <> "]"
]
deeper n e =
"case x of { " <> n <> " -> (" <> e <> "); _ -> x + " <> n <> " }"
numbers k = fmap (T.pack . show) [1 :: Int .. k]
-- | A module to format, and to check what that printed where it is
-- checked.
subjectOf :: Maybe [Extension] -> Bool -> Text -> Text -> Subject
subjectOf package isChecked name source =
Subject
{ subjectName = name,
subjectSource = source,
subjectFormat = formatted package path,
subjectCheck =
if isChecked
then Just (fst . rewritten parser cpp path (source, tree))
else Nothing
}
where
path = T.unpack name
parser = parserConfigFor package
cpp = usesCpp (effectiveExtensionsFor package source) source
tree
| cpp = Nothing
| otherwise = either (const Nothing) Just (parseModule parser path source)
-- | What formatting a module prints, as the corpus does it, or 'Nothing'
-- where it is declined.
formatted :: Maybe [Extension] -> FilePath -> Text -> Maybe Text
formatted package path source
| usesCpp (effectiveExtensionsFor package source) source =
either (const Nothing) Just (formatCppExample package path source)
| otherwise =
either
(const Nothing)
(Just . printExample package source)
(parseModule (parserConfigFor package) path source)