moonlight-pale-0.1.0.0: bench/ghc-surface/HieBench.hs
-- Current HIE type-graph property workload. The historical encoder unfolds
-- the same doubling DAG exponentially, so these rows certify the exact current
-- linear wire law rather than manufacturing a ratio between different wire
-- contracts.
module HieBench
( HieBenchmarkObstruction (..),
hieBenchmarks,
)
where
import BenchSupport (preparedBenchmarks)
import Control.DeepSeq (NFData (rnf))
import Data.Array (Array, array, assocs)
import Data.Map.Strict qualified as Map
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Word (Word64)
import GHC.Iface.Ext.Types (HieArgs (..), HieType (..), HieTypeFlat, TypeIndex)
import GHC.Types.Name (Name, mkSystemName)
import GHC.Types.Name.Occurrence (mkTyVarOcc)
import GHC.Types.Unique (mkUnique)
import Language.Haskell.Syntax.Specificity (data Specified)
import Moonlight.Pale.Ghc.Hie.Oracle (ModuleNameOracle (..))
import Moonlight.Pale.Ghc.Hie.SourceKey
( HieOracleArtifact (..),
HieSourceKeyKind (..),
OracleLookup (..),
OracleQuery (..),
buildHieOracleIndex,
lookupModuleOracle,
)
import Moonlight.Pale.Ghc.Hie.TypeWords
( TypeGraphObstruction (..),
TypeWords,
hieTypeIndexTypeWords,
hieTypeRootsTypeWords,
typeWordsList,
)
import Test.Tasty.Bench (Benchmark, bgroup)
data HieBenchmarkObstruction
= HieGraphCompilationRejected !Int !TypeGraphObstruction
| HieRootSetCompilationRejected !Int !TypeGraphObstruction
| UnexpectedHieWireLength !Int !Int !Int
| UnexpectedHieRootCount !Int !Int !Int
| InvalidHieFailureCorpusAccepted !String
| UnexpectedHieFailureObstruction !String !TypeGraphObstruction
| UnexpectedSourceKeyLookup !String !OracleLookup
deriving stock (Eq, Show)
instance NFData HieBenchmarkObstruction where
rnf obstruction =
rnf (show obstruction)
data HieWireDigest = HieWireDigest
{ hieWireWordCount :: !Int,
hieWireWordHash :: !Word64
}
deriving stock (Eq, Show)
instance NFData HieWireDigest where
rnf (HieWireDigest wordCount wordHash) =
rnf wordCount `seq` rnf wordHash
data PreparedHieCorpus = PreparedHieCorpus
{ preparedHieDepth :: !Int,
preparedHieTypeTable :: !(Array TypeIndex HieTypeFlat)
}
instance NFData PreparedHieCorpus where
rnf corpus =
rnf (preparedHieDepth corpus)
`seq` forceTypeTable (preparedHieTypeTable corpus)
forceTypeTable :: Array TypeIndex HieTypeFlat -> ()
forceTypeTable =
foldr forceTypeEntry () . assocs
forceTypeEntry :: (TypeIndex, HieTypeFlat) -> () -> ()
forceTypeEntry (typeIndex, flatType) forcedTail =
rnf typeIndex
`seq` case flatType of
HCoercionTy ->
forcedTail
HAppTy functionIndex (HieArgs argumentIndices) ->
rnf functionIndex
`seq` foldr
( \(visible, argumentIndex) nestedTail ->
rnf visible `seq` rnf argumentIndex `seq` nestedTail
)
forcedTail
argumentIndices
HCastTy childIndex ->
rnf childIndex `seq` forcedTail
HForAllTy ((binderName, binderKind), specificity) bodyIndex ->
binderName
`seq` rnf binderKind
`seq` specificity
`seq` rnf bodyIndex
`seq` forcedTail
HTyVarTy variableName ->
variableName `seq` forcedTail
otherFlatType ->
otherFlatType `seq` forcedTail
data PreparedTypeGraphCorpus = PreparedTypeGraphCorpus
{ preparedTypeGraphSize :: !Int,
preparedTypeGraphRoot :: !TypeIndex,
preparedTypeGraphTable :: !(Array TypeIndex HieTypeFlat)
}
instance NFData PreparedTypeGraphCorpus where
rnf corpus =
rnf (preparedTypeGraphSize corpus)
`seq` rnf (preparedTypeGraphRoot corpus)
`seq` forceTypeTable (preparedTypeGraphTable corpus)
data PreparedRootSetCorpus = PreparedRootSetCorpus
{ preparedRootSetSize :: !Int,
preparedRootSetRoots :: !(Set TypeIndex),
preparedRootSetTable :: !(Array TypeIndex HieTypeFlat)
}
instance NFData PreparedRootSetCorpus where
rnf corpus =
rnf (preparedRootSetSize corpus)
`seq` rnf (Set.toAscList (preparedRootSetRoots corpus))
`seq` forceTypeTable (preparedRootSetTable corpus)
data SourceKeyExpectation
= ExpectLongestSingleton
| ExpectAmbiguousSuffix
deriving stock (Eq, Show)
data PreparedSourceKeyCorpus = PreparedSourceKeyCorpus
{ preparedSourceKeyLabel :: !String,
preparedSourceKeyPaths :: ![FilePath],
preparedSourceKeyQuery :: !OracleQuery,
preparedSourceKeyExpectation :: !SourceKeyExpectation
}
instance NFData PreparedSourceKeyCorpus where
rnf corpus =
rnf (preparedSourceKeyLabel corpus)
`seq` rnf (preparedSourceKeyPaths corpus)
`seq` rnf (show (preparedSourceKeyQuery corpus))
`seq` rnf (show (preparedSourceKeyExpectation corpus))
data SourceKeyDigest = SourceKeyDigest
{ sourceKeyDigestCandidateCount :: !Int,
sourceKeyDigestHash :: !Int
}
deriving stock (Eq, Show)
instance NFData SourceKeyDigest where
rnf digest =
rnf (sourceKeyDigestCandidateCount digest)
`seq` rnf (sourceKeyDigestHash digest)
hieBenchmarks :: Either HieBenchmarkObstruction Benchmark
hieBenchmarks = do
validateInvalidHieCorpora
preparedCorpora <- traverse prepareHieCorpus currentHieDepths
graphBenchmarks <- traverse prepareTypeGraphBenchmark typeGraphFamilies
repeatedRootCorpora <- traverse prepareRepeatedRootCorpus repeatedRootSizes
sourceKeyBenchmarks <- traverse prepareSourceKeyBenchmark sourceKeyFamilies
pure
( bgroup
"hie-type-words"
[ bgroup
"doubling-dag-linear-wire"
(preparedBenchmarks "depth" preparedCorpora compileHieWireDigest),
bgroup
"adversarial-type-graphs"
graphBenchmarks,
bgroup
"repeated-structural-roots"
(preparedBenchmarks "roots" repeatedRootCorpora compileRepeatedRootDigest),
bgroup
"source-path-collisions"
sourceKeyBenchmarks
]
)
currentHieDepths :: [Int]
currentHieDepths =
[8, 16, 32, 128, 512]
typeGraphFamilies ::
[(String, [Int], Int -> (TypeIndex, Array TypeIndex HieTypeFlat))]
typeGraphFamilies =
[ ("diamond-fanout", [8, 64, 512], diamondFanoutCorpus),
("deep-forall", [8, 32, 128], deepForAllCorpus),
("variable-rich-doubling", [8, 32, 128, 512], variableRichDoublingCorpus)
]
prepareTypeGraphBenchmark ::
(String, [Int], Int -> (TypeIndex, Array TypeIndex HieTypeFlat)) ->
Either HieBenchmarkObstruction Benchmark
prepareTypeGraphBenchmark (familyLabel, sizes, corpusForSize) = do
preparedCorpora <-
traverse
( \size -> do
let (rootIndex, typeTable) = corpusForSize size
corpus =
PreparedTypeGraphCorpus
{ preparedTypeGraphSize = size,
preparedTypeGraphRoot = rootIndex,
preparedTypeGraphTable = typeTable
}
_ <- compileTypeGraphDigest corpus
pure (size, corpus)
)
sizes
pure
( bgroup
familyLabel
(preparedBenchmarks "size" preparedCorpora compileTypeGraphDigest)
)
compileTypeGraphDigest ::
PreparedTypeGraphCorpus ->
Either HieBenchmarkObstruction HieWireDigest
compileTypeGraphDigest corpus =
case
hieTypeIndexTypeWords
(preparedTypeGraphTable corpus)
(preparedTypeGraphRoot corpus)
of
Left obstruction ->
Left (HieGraphCompilationRejected (preparedTypeGraphSize corpus) obstruction)
Right typeWordsValue ->
Right (foldl' digestHieWord emptyHieWireDigest (typeWordsList typeWordsValue))
repeatedRootSizes :: [Int]
repeatedRootSizes =
[8, 64, 512]
prepareRepeatedRootCorpus ::
Int ->
Either HieBenchmarkObstruction (Int, PreparedRootSetCorpus)
prepareRepeatedRootCorpus rootCount = do
let corpus =
PreparedRootSetCorpus
{ preparedRootSetSize = rootCount,
preparedRootSetRoots = Set.fromList [1 .. fromIntegral rootCount],
preparedRootSetTable = repeatedRootTable rootCount
}
_ <- compileRepeatedRootDigest corpus
pure (rootCount, corpus)
compileRepeatedRootDigest ::
PreparedRootSetCorpus ->
Either HieBenchmarkObstruction HieWireDigest
compileRepeatedRootDigest corpus = do
compiledRoots <-
firstRootFailure
(preparedRootSetSize corpus)
( hieTypeRootsTypeWords
(preparedRootSetTable corpus)
(preparedRootSetRoots corpus)
)
let actualRootCount = Map.size compiledRoots
if actualRootCount /= preparedRootSetSize corpus
then
Left
( UnexpectedHieRootCount
(preparedRootSetSize corpus)
(preparedRootSetSize corpus)
actualRootCount
)
else
Right
( Map.foldlWithKey'
digestCompiledRoot
emptyHieWireDigest
compiledRoots
)
firstRootFailure ::
Int ->
Map.Map TypeIndex (Either TypeGraphObstruction typeWords) ->
Either HieBenchmarkObstruction (Map.Map TypeIndex typeWords)
firstRootFailure rootCount =
traverse
(either (Left . HieRootSetCompilationRejected rootCount) Right)
digestCompiledRoot :: HieWireDigest -> TypeIndex -> TypeWords -> HieWireDigest
digestCompiledRoot digest rootIndex typeWordsValue =
foldl'
digestHieWord
(digestHieWord digest (fromIntegral rootIndex))
(typeWordsList typeWordsValue)
sourceKeyFamilies :: [(String, SourceKeyExpectation)]
sourceKeyFamilies =
[ ("longest-singleton-suffix", ExpectLongestSingleton),
("ambiguous-shared-suffix", ExpectAmbiguousSuffix)
]
prepareSourceKeyBenchmark ::
(String, SourceKeyExpectation) ->
Either HieBenchmarkObstruction Benchmark
prepareSourceKeyBenchmark (familyLabel, expectation) = do
preparedCorpora <-
traverse
(prepareSourceKeyCorpus familyLabel expectation)
[8, 64, 512]
pure
( bgroup
familyLabel
(preparedBenchmarks "paths" preparedCorpora compileSourceKeyDigest)
)
prepareSourceKeyCorpus ::
String ->
SourceKeyExpectation ->
Int ->
Either HieBenchmarkObstruction (Int, PreparedSourceKeyCorpus)
prepareSourceKeyCorpus familyLabel expectation pathCount = do
let corpus =
PreparedSourceKeyCorpus
{ preparedSourceKeyLabel = familyLabel <> "/" <> show pathCount,
preparedSourceKeyPaths =
fmap
(\pathIndex -> "pkg" <> show pathIndex <> "/src/Foo.hs")
[1 .. pathCount],
preparedSourceKeyQuery =
OracleQuery
{ oqGivenPath =
case expectation of
ExpectLongestSingleton ->
"/workspace/pkg1/src/Foo.hs"
ExpectAmbiguousSuffix ->
"/workspace/src/Foo.hs",
oqAbsolutePath = Nothing,
oqSourceRoots = []
},
preparedSourceKeyExpectation = expectation
}
_ <- compileSourceKeyDigest corpus
pure (pathCount, corpus)
compileSourceKeyDigest ::
PreparedSourceKeyCorpus ->
Either HieBenchmarkObstruction SourceKeyDigest
compileSourceKeyDigest corpus =
let lookupResult =
lookupModuleOracle
(buildHieOracleIndex (fmap emptyArtifact (preparedSourceKeyPaths corpus)))
(preparedSourceKeyQuery corpus)
in case (preparedSourceKeyExpectation corpus, lookupResult) of
(ExpectLongestSingleton, OracleFound ModuleSuffixKey artifact) ->
Right
SourceKeyDigest
{ sourceKeyDigestCandidateCount = 1,
sourceKeyDigestHash =
digestString
2166136261
(mnoSourcePath (hieArtifactOracle artifact))
}
(ExpectAmbiguousSuffix, OracleAmbiguous ModuleSuffixKey matchedPath candidates) ->
Right
SourceKeyDigest
{ sourceKeyDigestCandidateCount = length candidates,
sourceKeyDigestHash =
foldl'
digestString
(digestString 2166136261 matchedPath)
candidates
}
_ ->
Left
( UnexpectedSourceKeyLookup
(preparedSourceKeyLabel corpus)
lookupResult
)
emptyOracle :: FilePath -> ModuleNameOracle
emptyOracle sourcePath =
ModuleNameOracle
{ mnoSourcePath = sourcePath,
mnoGlobalUsesAtSpan = Map.empty,
mnoGlobalUses = Map.empty,
mnoEvidenceAtSpan = Map.empty,
mnoTypeAtSpan = Map.empty
}
emptyArtifact :: FilePath -> HieOracleArtifact
emptyArtifact sourcePath =
HieOracleArtifact
{ hieArtifactPath = sourcePath <> ".hie",
hieArtifactOracle = emptyOracle sourcePath
}
validateInvalidHieCorpora :: Either HieBenchmarkObstruction ()
validateInvalidHieCorpora = do
expectHieObstruction
"cycle"
(CyclicTypeIndex 0)
(hieTypeIndexTypeWords (array (0, 0) [(0, HCastTy 0)]) 0)
expectHieObstruction
"out-of-range"
(MissingTypeIndex 1)
(hieTypeIndexTypeWords (array (0, 0) [(0, HCastTy 1)]) 0)
expectHieObstruction ::
String ->
TypeGraphObstruction ->
Either TypeGraphObstruction typeWords ->
Either HieBenchmarkObstruction ()
expectHieObstruction corpusLabel expectedObstruction = \case
Right _ ->
Left (InvalidHieFailureCorpusAccepted corpusLabel)
Left actualObstruction
| actualObstruction == expectedObstruction ->
Right ()
| otherwise ->
Left
( UnexpectedHieFailureObstruction
corpusLabel
actualObstruction
)
prepareHieCorpus :: Int -> Either HieBenchmarkObstruction (Int, PreparedHieCorpus)
prepareHieCorpus depth = do
let corpus =
PreparedHieCorpus
{ preparedHieDepth = depth,
preparedHieTypeTable = doublingDagTable depth
}
_ <- compileHieWireDigest corpus
pure (depth, corpus)
compileHieWireDigest :: PreparedHieCorpus -> Either HieBenchmarkObstruction HieWireDigest
compileHieWireDigest corpus =
case
hieTypeIndexTypeWords
(preparedHieTypeTable corpus)
(fromIntegral (preparedHieDepth corpus))
of
Left obstruction ->
Left (HieGraphCompilationRejected (preparedHieDepth corpus) obstruction)
Right typeWordsValue ->
let digest = foldl' digestHieWord emptyHieWireDigest (typeWordsList typeWordsValue)
expectedLength = currentHieWireLength (preparedHieDepth corpus)
in if hieWireWordCount digest == expectedLength
then Right digest
else
Left
( UnexpectedHieWireLength
(preparedHieDepth corpus)
expectedLength
(hieWireWordCount digest)
)
emptyHieWireDigest :: HieWireDigest
emptyHieWireDigest =
HieWireDigest
{ hieWireWordCount = 0,
hieWireWordHash = 14695981039346656037
}
digestHieWord :: HieWireDigest -> Word64 -> HieWireDigest
digestHieWord digest wordValue =
HieWireDigest
{ hieWireWordCount = hieWireWordCount digest + 1,
hieWireWordHash = (hieWireWordHash digest * 1099511628211) + wordValue
}
currentHieWireLength :: Int -> Int
currentHieWireLength depth =
(7 * depth) + 7
doublingDagTable :: Int -> Array TypeIndex HieTypeFlat
doublingDagTable depth =
array
(0, fromIntegral depth)
( (0, HCoercionTy)
: fmap
( \typeIndex ->
( typeIndex,
HAppTy
(typeIndex - 1)
(HieArgs [(True, typeIndex - 1)])
)
)
[1 .. fromIntegral depth]
)
diamondFanoutCorpus :: Int -> (TypeIndex, Array TypeIndex HieTypeFlat)
diamondFanoutCorpus fanout =
let branchIndices =
[2 .. fromIntegral fanout + 1]
rootIndex =
fromIntegral fanout + 2
in ( rootIndex,
array
(0, rootIndex)
( [ (0, HCoercionTy),
(1, HCastTy 0)
]
<> fmap
(\branchIndex -> (branchIndex, HAppTy 1 (HieArgs [(True, 0)])))
branchIndices
<> [ ( rootIndex,
HAppTy
1
(HieArgs (fmap (\branchIndex -> (True, branchIndex)) branchIndices))
)
]
)
)
deepForAllCorpus :: Int -> (TypeIndex, Array TypeIndex HieTypeFlat)
deepForAllCorpus depth =
let binderNames =
fmap hieBinderName [1 .. depth]
innermostVariableIndex =
1
forallEntries =
fmap
( \(entryOffset, binderName) ->
let forallIndex =
fromIntegral entryOffset + 2
bodyIndex =
if entryOffset == 0
then innermostVariableIndex
else forallIndex - 1
in ( forallIndex,
HForAllTy
((binderName, 0), Specified)
bodyIndex
)
)
(zip [0 :: Int ..] (reverse binderNames))
rootIndex =
fromIntegral depth + 1
innermostBinderName =
maybe (hieBinderName 0) id (lastMaybe binderNames)
in ( rootIndex,
array
(0, rootIndex)
( [ (0, HCoercionTy),
(innermostVariableIndex, HTyVarTy innermostBinderName)
]
<> forallEntries
)
)
variableRichDoublingCorpus ::
Int ->
(TypeIndex, Array TypeIndex HieTypeFlat)
variableRichDoublingCorpus requestedSize =
let variableCount =
max 1 requestedSize
wrapperDepth =
max 1 requestedSize
variableEntries =
fmap
( \variableOffset ->
( fromIntegral variableOffset,
HTyVarTy (hieFreeVariableName (variableOffset + 1))
)
)
[0 .. variableCount - 1]
combinationEntries =
fmap
( \combinationOffset ->
let combinationIndex =
fromIntegral (variableCount + combinationOffset)
functionIndex =
if combinationOffset == 0
then 0
else combinationIndex - 1
argumentIndex =
fromIntegral (combinationOffset + 1)
in ( combinationIndex,
HAppTy
functionIndex
(HieArgs [(True, argumentIndex)])
)
)
[0 .. variableCount - 2]
combinedRootIndex =
if variableCount == 1
then 0
else fromIntegral ((2 * variableCount) - 2)
wrapperEntries =
fmap
( \wrapperOffset ->
let wrapperIndex =
combinedRootIndex + fromIntegral wrapperOffset
childIndex =
wrapperIndex - 1
in ( wrapperIndex,
HAppTy
childIndex
(HieArgs [(True, childIndex)])
)
)
[1 .. wrapperDepth]
rootIndex =
combinedRootIndex + fromIntegral wrapperDepth
in ( rootIndex,
array
(0, rootIndex)
(variableEntries <> combinationEntries <> wrapperEntries)
)
lastMaybe :: [value] -> Maybe value
lastMaybe =
foldl' (\_ value -> Just value) Nothing
hieBinderName :: Int -> Name
hieBinderName binderIndex =
mkSystemName
(mkUnique 'h' (fromIntegral binderIndex))
(mkTyVarOcc ("type" <> show binderIndex))
hieFreeVariableName :: Int -> Name
hieFreeVariableName variableIndex =
mkSystemName
(mkUnique 'v' (fromIntegral variableIndex))
(mkTyVarOcc ("free" <> show variableIndex))
repeatedRootTable :: Int -> Array TypeIndex HieTypeFlat
repeatedRootTable rootCount =
array
(0, fromIntegral rootCount)
( (0, HCoercionTy)
: fmap
(\rootIndex -> (rootIndex, HCastTy 0))
[1 .. fromIntegral rootCount]
)
digestString :: Int -> String -> Int
digestString =
foldl'
(\digest character -> (digest * 16777619) + fromEnum character)