packages feed

moonlight-pale-0.1.0.0: bench/ghc-surface/GhcSurfaceBench.hs

-- Parse-and-convert workloads for @moonlight-pale:ghc-surface@.  The
-- common-subset corpus is deliberately restricted to syntax shared with the
-- historical converter; the full-fidelity corpus exercises current structural
-- ownership and is therefore not offered as a historical ratio gate.
module GhcSurfaceBench
  ( GhcSurfaceBenchmarkObstruction (..),
    SemanticConversionManifest (..),
    ConversionBenchmarkDigest,
    PreparedConversionCorpus,
    commonSubsetSemanticManifests,
    commonSubsetWorkload,
    prepareCommonSubsetCorpus,
    convertCommonCorpus,
    conversionBenchmarkDigestHash,
    ghcSurfaceBenchmarks,
  )
where

import BenchSupport (preparedBenchmarks)
import Control.DeepSeq (NFData (rnf))
import Data.Bifunctor (first)
import Data.Foldable (toList)
import Data.List (intercalate)
import Data.Text qualified as Text
import Moonlight.Core (binderIdKey)
import Moonlight.Pale.Ghc.Expr
  ( BinderAnn (..),
    Binding (..),
    BindingGroup,
    Clause (..),
    ConvertedModule (..),
    ConvertedModuleMetrics
      ( cmmBindingCount,
        cmmGlobalVarRefCount,
        cmmLambdaSiteCount,
        cmmLetSiteCount,
        cmmLocalVarRefCount,
        cmmMaxFreeScopeCount,
        cmmObservedContextCount,
        cmmScopedExprCount
      ),
    ConvertObstruction,
    Expr,
    LayoutPolicy (CompactLayout),
    ModuleRenderContext (..),
    RenderRefusal,
    RenderTarget (RenderConvertedModule),
    Rhs (..),
    ScopeCtx (..),
    ScopeLookupFailure,
    SourceRegion (..),
    ConvertedValueBinding,
    bindingGroupBindings,
    bindingGroupScope,
    bindingNames,
    convertHaskellSource,
    convertedModuleBindings,
    convertedModuleMetrics,
    exprFreeScopes,
    exprNode,
    exprRegion,
    exprScope,
    freeScopeSummaryToList,
    renderSource,
    renderRdrName,
    scopeIdKey,
    scopeObservedContexts,
    tlbBinding,
    tlbRegion,
    tlbScope,
  )
import Test.Tasty.Bench (Benchmark, bgroup)

data GhcSurfaceBenchmarkObstruction
  = InvalidCommonSubsetSize !Int
  | BenchmarkConversionRejected !String !ConvertObstruction
  | BenchmarkRenderingRefused !String !RenderRefusal
  | BenchmarkScopeMetadataRejected !String !ScopeLookupFailure
  | UnexpectedBindingCardinality !String !Int !Int
  | UnexpectedBindingNameCardinality !String ![String]
  | UnexpectedOrderedBinders !String ![String] ![String]
  deriving stock (Eq, Show)

instance NFData GhcSurfaceBenchmarkObstruction where
  rnf obstruction =
    rnf (show obstruction)

-- The overlap section used to glue current and historical conversion rows.
-- It is semantic source plus ordered top-level binder evidence, not internal
-- structural node counts whose owners deliberately changed in the rewrite.
data SemanticConversionManifest = SemanticConversionManifest
  { semanticManifestBindingCount :: !Int,
    semanticManifestRenderedModule :: !String,
    semanticManifestOrderedBinders :: ![String]
  }
  deriving stock (Eq, Show)

instance NFData SemanticConversionManifest where
  rnf manifest =
    rnf (semanticManifestBindingCount manifest)
      `seq` rnf (semanticManifestRenderedModule manifest)
      `seq` rnf (semanticManifestOrderedBinders manifest)

-- Current-only readiness evidence.  All eight metrics are forced and retained
-- for performance accounting, but are not cross-version equality evidence.
data RepresentationReadinessDigest = RepresentationReadinessDigest
  { readinessBindingCount :: !Int,
    readinessObservedContextCount :: !Int,
    readinessLambdaSiteCount :: !Int,
    readinessLetSiteCount :: !Int,
    readinessScopedExprCount :: !Int,
    readinessGlobalVarRefCount :: !Int,
    readinessLocalVarRefCount :: !Int,
    readinessMaxFreeScopeCount :: !Int,
    readinessAnnotationDigest :: !Int
  }
  deriving stock (Eq, Show)

instance NFData RepresentationReadinessDigest where
  rnf digest =
    rnf (readinessBindingCount digest)
      `seq` rnf (readinessObservedContextCount digest)
      `seq` rnf (readinessLambdaSiteCount digest)
      `seq` rnf (readinessLetSiteCount digest)
      `seq` rnf (readinessScopedExprCount digest)
      `seq` rnf (readinessGlobalVarRefCount digest)
      `seq` rnf (readinessLocalVarRefCount digest)
      `seq` rnf (readinessMaxFreeScopeCount digest)
      `seq` rnf (readinessAnnotationDigest digest)

data ConversionBenchmarkDigest = ConversionBenchmarkDigest
  { conversionSemanticManifest :: !SemanticConversionManifest,
    conversionRepresentationReadiness :: !RepresentationReadinessDigest
  }
  deriving stock (Eq, Show)

instance NFData ConversionBenchmarkDigest where
  rnf digest =
    rnf (conversionSemanticManifest digest)
      `seq` rnf (conversionRepresentationReadiness digest)

conversionBenchmarkDigestHash :: ConversionBenchmarkDigest -> Int
conversionBenchmarkDigestHash digest =
  let semanticManifest = conversionSemanticManifest digest
      readinessDigest = conversionRepresentationReadiness digest
      semanticHash =
        foldl'
          digestString
          (digestInt 2166136261 (semanticManifestBindingCount semanticManifest))
          ( semanticManifestRenderedModule semanticManifest
              : semanticManifestOrderedBinders semanticManifest
          )
   in foldl'
        digestInt
        semanticHash
        [ readinessBindingCount readinessDigest,
          readinessObservedContextCount readinessDigest,
          readinessLambdaSiteCount readinessDigest,
          readinessLetSiteCount readinessDigest,
          readinessScopedExprCount readinessDigest,
          readinessGlobalVarRefCount readinessDigest,
          readinessLocalVarRefCount readinessDigest,
          readinessMaxFreeScopeCount readinessDigest,
          readinessAnnotationDigest readinessDigest
        ]

data PreparedConversionCorpus = PreparedConversionCorpus
  { preparedCorpusLabel :: !String,
    preparedCorpusSource :: !String
  }
  deriving stock (Eq, Show)

instance NFData PreparedConversionCorpus where
  rnf corpus =
    rnf (preparedCorpusLabel corpus)
      `seq` rnf (preparedCorpusSource corpus)

ghcSurfaceBenchmarks :: Either GhcSurfaceBenchmarkObstruction Benchmark
ghcSurfaceBenchmarks = do
  commonCorpora <- traverse prepareCommonSubsetCorpus commonSubsetSizes
  fullFidelityCorpus <- prepareFullFidelityCorpus
  structuralBenchmarks <- traverse prepareStructuralBenchmark structuralCorpusFamilies
  pure
    ( bgroup
        "ghc-surface"
        [ bgroup
            "common-subset-convert-and-normalize"
            (preparedBenchmarks "bindings" commonCorpora convertCommonCorpus),
          bgroup
            "current-full-fidelity"
            (preparedBenchmarks "modules" [(1, fullFidelityCorpus)] convertFullFidelityCorpus),
          bgroup
            "current-structure-matrix"
            structuralBenchmarks
        ]
    )

prepareStructuralBenchmark ::
  (String, [Int], Int -> String) ->
  Either GhcSurfaceBenchmarkObstruction Benchmark
prepareStructuralBenchmark (familyLabel, sizes, sourceForSize) = do
  preparedCorpora <-
    traverse
      ( \size ->
          prepareStructuralCorpus
            familyLabel
            size
            (sourceForSize size)
      )
      sizes
  pure
    ( bgroup
        familyLabel
        (preparedBenchmarks "size" preparedCorpora convertFullFidelityCorpus)
    )

prepareStructuralCorpus ::
  String ->
  Int ->
  String ->
  Either GhcSurfaceBenchmarkObstruction (Int, PreparedConversionCorpus)
prepareStructuralCorpus familyLabel size sourceText = do
  let corpus =
        PreparedConversionCorpus
          { preparedCorpusLabel = familyLabel <> "/" <> show size,
            preparedCorpusSource = sourceText
          }
  _ <- convertFullFidelityCorpus corpus
  pure (size, corpus)

structuralCorpusFamilies :: [(String, [Int], Int -> String)]
structuralCorpusFamilies =
  [ ("scope-depth", [8, 32, 128], scopeDepthModule),
    ("scope-branch-count", [8, 32, 128], scopeBranchModule),
    ("shadow-depth", [8, 32, 128], shadowDepthModule),
    ("sparse-scc-cardinality", [8, 32, 128], sparseSccModule),
    ("dense-scc-cardinality", [4, 8, 16], denseSccModule),
    ("rendered-list-elements", [32, 256, 2048], renderedListModule),
    ("opaque-declaration-position", [0, 32, 128], opaqueDeclarationPositionModule)
  ]

commonSubsetSemanticManifests :: Either GhcSurfaceBenchmarkObstruction [(Int, SemanticConversionManifest)]
commonSubsetSemanticManifests =
  traverse
    ( \bindingCount ->
        fmap
          ((,) bindingCount . conversionSemanticManifest)
          (commonSubsetWorkload bindingCount)
    )
    commonSubsetSizes

commonSubsetWorkload :: Int -> Either GhcSurfaceBenchmarkObstruction ConversionBenchmarkDigest
commonSubsetWorkload bindingCount
  | bindingCount <= 0 =
      Left (InvalidCommonSubsetSize bindingCount)
  | otherwise = do
      let corpus = commonSubsetCorpus bindingCount
          expectedBinders = fmap (\index -> "f" <> show index) [1 .. bindingCount]
      digest <- convertCommonCorpus corpus
      validateCommonDigest
        (preparedCorpusLabel corpus)
        bindingCount
        expectedBinders
        digest
      pure digest

commonSubsetSizes :: [Int]
commonSubsetSizes =
  [8, 32, 128]

prepareCommonSubsetCorpus :: Int -> Either GhcSurfaceBenchmarkObstruction (Int, PreparedConversionCorpus)
prepareCommonSubsetCorpus bindingCount =
  (bindingCount, commonSubsetCorpus bindingCount)
    <$ commonSubsetWorkload bindingCount

commonSubsetCorpus :: Int -> PreparedConversionCorpus
commonSubsetCorpus bindingCount =
  PreparedConversionCorpus
    { preparedCorpusLabel = "common-subset/" <> show bindingCount,
      preparedCorpusSource = commonSubsetModule bindingCount
    }

prepareFullFidelityCorpus :: Either GhcSurfaceBenchmarkObstruction PreparedConversionCorpus
prepareFullFidelityCorpus = do
  let corpus =
        PreparedConversionCorpus
          { preparedCorpusLabel = "current-full-fidelity",
            preparedCorpusSource = fullFidelityModule
          }
  _ <- convertFullFidelityCorpus corpus
  pure corpus

convertCommonCorpus :: PreparedConversionCorpus -> Either GhcSurfaceBenchmarkObstruction ConversionBenchmarkDigest
convertCommonCorpus corpus = do
  convertedModule <- convertCorpus corpus
  semanticManifest <- commonSemanticManifest (preparedCorpusLabel corpus) convertedModule
  readinessDigest <- representationReadinessDigest (preparedCorpusLabel corpus) convertedModule
  pure
    ConversionBenchmarkDigest
      { conversionSemanticManifest = semanticManifest,
        conversionRepresentationReadiness = readinessDigest
      }

convertFullFidelityCorpus :: PreparedConversionCorpus -> Either GhcSurfaceBenchmarkObstruction ConversionBenchmarkDigest
convertFullFidelityCorpus corpus = do
  convertedModule <- convertCorpus corpus
  renderedModule <-
    first
      (BenchmarkRenderingRefused (preparedCorpusLabel corpus))
      ( Text.unpack
          <$> renderSource
            CompactLayout
            ( RenderConvertedModule
                (ModuleRenderContext "" (Just "Bench"))
                convertedModule
            )
      )
  let metrics = convertedModuleMetrics convertedModule
  let orderedBinders = orderedBindingNames convertedModule
  readinessDigest <- representationReadinessDigest (preparedCorpusLabel corpus) convertedModule
  pure
    ConversionBenchmarkDigest
      { conversionSemanticManifest =
          SemanticConversionManifest
            { semanticManifestBindingCount = cmmBindingCount metrics,
              semanticManifestRenderedModule = renderedModule,
              semanticManifestOrderedBinders = orderedBinders
            },
        conversionRepresentationReadiness = readinessDigest
      }

convertCorpus :: PreparedConversionCorpus -> Either GhcSurfaceBenchmarkObstruction ConvertedModule
convertCorpus corpus =
  case convertHaskellSource "Bench.hs" (preparedCorpusSource corpus) of
    Left obstruction ->
      Left (BenchmarkConversionRejected (preparedCorpusLabel corpus) obstruction)
    Right convertedModule ->
      Right convertedModule

commonSemanticManifest ::
  String ->
  ConvertedModule ->
  Either GhcSurfaceBenchmarkObstruction SemanticConversionManifest
commonSemanticManifest corpusLabel convertedModule = do
  orderedBinders <-
    traverse
      (commonBindingName corpusLabel)
      (convertedModuleBindings convertedModule)
  renderedModule <-
    first
      (BenchmarkRenderingRefused corpusLabel)
      ( Text.unpack
          <$> renderSource
            CompactLayout
            ( RenderConvertedModule
                (ModuleRenderContext "" (Just "Bench"))
                convertedModule
            )
      )
  pure
    SemanticConversionManifest
      { semanticManifestBindingCount = length orderedBinders,
        semanticManifestRenderedModule = renderedModule,
        semanticManifestOrderedBinders = orderedBinders
      }

commonBindingName ::
  String ->
  ConvertedValueBinding ->
  Either GhcSurfaceBenchmarkObstruction String
commonBindingName corpusLabel topLevelBinding =
  case fmap renderRdrName (bindingNames (tlbBinding topLevelBinding)) of
    [bindingName] ->
      Right bindingName
    names ->
      Left (UnexpectedBindingNameCardinality corpusLabel names)

validateCommonDigest ::
  String ->
  Int ->
  [String] ->
  ConversionBenchmarkDigest ->
  Either GhcSurfaceBenchmarkObstruction ()
validateCommonDigest corpusLabel expectedBindingCount expectedBinders digest
  | semanticManifestBindingCount semanticManifest /= expectedBindingCount =
      Left
        ( UnexpectedBindingCardinality
            corpusLabel
            expectedBindingCount
            (semanticManifestBindingCount semanticManifest)
        )
  | semanticManifestOrderedBinders semanticManifest /= expectedBinders =
      Left
        ( UnexpectedOrderedBinders
            corpusLabel
            expectedBinders
            (semanticManifestOrderedBinders semanticManifest)
        )
  | otherwise =
      Right ()
  where
    semanticManifest = conversionSemanticManifest digest

orderedBindingNames :: ConvertedModule -> [String]
orderedBindingNames =
  foldMap
    (fmap renderRdrName . bindingNames . tlbBinding)
    . convertedModuleBindings

representationReadinessDigest ::
  String ->
  ConvertedModule ->
  Either GhcSurfaceBenchmarkObstruction RepresentationReadinessDigest
representationReadinessDigest corpusLabel convertedModule = do
  scopeContexts <-
    first
      (BenchmarkScopeMetadataRejected corpusLabel)
      (scopeObservedContexts (cmScopeIndex convertedModule))
  let metrics = convertedModuleMetrics convertedModule
      annotationDigest =
        foldl'
          digestConvertedValueBindingAnnotations
          ( foldl'
              digestBinderAnn
              ( foldl'
                  digestBinderAnn
                  (foldl' digestScopeContext 2166136261 scopeContexts)
                  (cmLambdaSites convertedModule)
              )
              (cmLetSites convertedModule)
          )
          (convertedModuleBindings convertedModule)
  pure
    RepresentationReadinessDigest
      { readinessBindingCount = cmmBindingCount metrics,
        readinessObservedContextCount = cmmObservedContextCount metrics,
        readinessLambdaSiteCount = cmmLambdaSiteCount metrics,
        readinessLetSiteCount = cmmLetSiteCount metrics,
        readinessScopedExprCount = cmmScopedExprCount metrics,
        readinessGlobalVarRefCount = cmmGlobalVarRefCount metrics,
        readinessLocalVarRefCount = cmmLocalVarRefCount metrics,
        readinessMaxFreeScopeCount = cmmMaxFreeScopeCount metrics,
        readinessAnnotationDigest = annotationDigest
      }

digestConvertedValueBindingAnnotations :: Int -> ConvertedValueBinding -> Int
digestConvertedValueBindingAnnotations digest convertedValueBinding =
  digestBindingAnnotations
    ( digestSourceRegion
        (digestInt digest (scopeIdKey (tlbScope convertedValueBinding)))
        (tlbRegion convertedValueBinding)
    )
    (tlbBinding convertedValueBinding)

digestBindingAnnotations :: Int -> Binding -> Int
digestBindingAnnotations digest = \case
  FunctionBinding binderAnn clauses ->
    foldl'
      digestClauseAnnotations
      (digestBinderAnn digest binderAnn)
      clauses
  PatternBinding _ rhsValue ->
    digestRhsAnnotations digest rhsValue

digestClauseAnnotations :: Int -> Clause -> Int
digestClauseAnnotations digest clauseValue =
  digestRhsAnnotations digest (clauseRhs clauseValue)

digestRhsAnnotations :: Int -> Rhs -> Int
digestRhsAnnotations digest = \case
  UnguardedRhs bodyExpression maybeBindingGroup ->
    foldl'
      digestBindingGroupAnnotations
      (digestExprAnnotations digest bodyExpression)
      maybeBindingGroup
  GuardedRhs guardedAlternatives maybeBindingGroup ->
    foldl'
      digestBindingGroupAnnotations
      ( foldl'
          digestExprAnnotations
          digest
          (foldMap toList guardedAlternatives)
      )
      maybeBindingGroup

digestBindingGroupAnnotations :: Int -> BindingGroup -> Int
digestBindingGroupAnnotations digest bindingGroup =
  foldl'
    digestBindingAnnotations
    (digestInt digest (scopeIdKey (bindingGroupScope bindingGroup)))
    (bindingGroupBindings bindingGroup)

digestExprAnnotations :: Int -> Expr -> Int
digestExprAnnotations digest expressionValue =
  foldl'
    digestExprAnnotations
    ( foldl'
        (\nestedDigest scopeId -> digestInt nestedDigest (scopeIdKey scopeId))
        ( digestSourceRegion
            (digestInt digest (scopeIdKey (exprScope expressionValue)))
            (exprRegion expressionValue)
        )
        (freeScopeSummaryToList (exprFreeScopes expressionValue))
    )
    (exprNode expressionValue)

digestBinderAnn :: Int -> BinderAnn -> Int
digestBinderAnn digest binderAnn =
  foldl'
    (\nestedDigest character -> digestInt nestedDigest (fromEnum character))
    (digestInt digest (binderIdKey (baId binderAnn)))
    (renderRdrName (baName binderAnn))

digestSourceRegion :: Int -> Maybe SourceRegion -> Int
digestSourceRegion digest = \case
  Nothing ->
    digestInt digest 0
  Just region ->
    digestInt
      ( digestInt
          (digestInt (digestInt digest (srStartLine region)) (srStartCol region))
          (srEndLine region)
      )
      (srEndCol region)

digestScopeContext :: Int -> ScopeCtx -> Int
digestScopeContext digest = \case
  ActualScope scopeId ->
    digestInt digest (scopeIdKey scopeId)
  IncompatibleScope ->
    digestInt digest (-1)

digestInt :: Int -> Int -> Int
digestInt digest value =
  (digest * 16777619) + value

digestString :: Int -> String -> Int
digestString =
  foldl'
    (\digest character -> digestInt digest (fromEnum character))

commonSubsetModule :: Int -> String
commonSubsetModule bindingCount =
  unlines ("module Bench where" : "" : fmap binding [1 .. bindingCount])
  where
    binding :: Int -> String
    binding index =
      let name = show index
       in "f" <> name <> " x = let y = x + " <> name <> " in h" <> name <> " (y * y)"

fullFidelityModule :: String
fullFidelityModule =
  unlines
    [ "{-# LANGUAGE MagicHash #-}",
      "{-# LANGUAGE TupleSections #-}",
      "{-# LANGUAGE UnboxedTuples #-}",
      "module Bench where",
      "",
      "infixr 5 <+>",
      "(<+>) left right = left + right",
      "",
      "(answer, label) = (42, \"exact\")",
      "tupleSection value = (, value)",
      "unboxed value = (# value, value + 1 #)",
      "exactFraction = 1.25",
      "primitiveString = \"bytes\"#",
      "multi [] = 0",
      "multi (value : values) = local value + multi values",
      "  where",
      "    local nested = nested <+> 1"
    ]

scopeDepthModule :: Int -> String
scopeDepthModule depth =
  unlines
    [ "module Bench where",
      "",
      "deep = "
        <> foldr
          (\binderName bodySource -> "\\" <> binderName <> " -> " <> bodySource)
          ("level" <> show depth)
          binderNames
    ]
  where
    binderNames =
      fmap (\index -> "level" <> show index) [1 .. depth]

scopeBranchModule :: Int -> String
scopeBranchModule branchCount =
  unlines
    ( [ "module Bench where",
        "",
        "branch value = case value of"
      ]
        <> fmap branchRow [1 .. branchCount]
    )
  where
    branchRow :: Int -> String
    branchRow index =
      "  Branch" <> show index <> " branchValue -> branchValue"

shadowDepthModule :: Int -> String
shadowDepthModule depth =
  unlines
    [ "module Bench where",
      "",
      "shadow = "
        <> foldr
          (\_ bodySource -> "\\value -> " <> bodySource)
          "value"
          [1 .. depth]
    ]

sparseSccModule :: Int -> String
sparseSccModule cardinality =
  unlines
    ( [ "module Bench where",
        "",
        "sparse seed =",
        "  let"
      ]
        <> fmap sparseBindingRow [1 .. cardinality]
        <> ["  in node1"]
    )
  where
    sparseBindingRow index =
      "    node"
        <> show index
        <> " = node"
        <> show (if index == cardinality then 1 else index + 1)
        <> " + seed"

denseSccModule :: Int -> String
denseSccModule cardinality =
  unlines
    ( [ "module Bench where",
        "",
        "dense seed =",
        "  let"
      ]
        <> fmap denseBindingRow [1 .. cardinality]
        <> ["  in node1"]
    )
  where
    denseBindingRow index =
      "    node"
        <> show index
        <> " = "
        <> intercalate
          " + "
          ( "seed"
              : fmap
                (\referencedIndex -> "node" <> show referencedIndex)
                (filter (/= index) [1 .. cardinality])
          )

renderedListModule :: Int -> String
renderedListModule elementCount =
  unlines
    [ "module Bench where",
      "",
      "rendered = [" <> intercalate ", " (fmap show [1 .. elementCount]) <> "]"
    ]

opaqueDeclarationPositionModule :: Int -> String
opaqueDeclarationPositionModule declarationPosition =
  unlines
    ( ["module Bench where", ""]
        <> precedingBindings
        <> [ "class BenchClass value where",
             "  benchMethod :: value -> value"
           ]
        <> remainingBindings
    )
  where
    allBindings =
      fmap
        (\index -> "value" <> show index <> " = " <> show index)
        [1 .. opaquePositionBindingCount]
    (precedingBindings, remainingBindings) =
      splitAt declarationPosition allBindings

opaquePositionBindingCount :: Int
opaquePositionBindingCount =
  128