packages feed

ghc-9.14.1: GHC/StgToJS/Sinker/StringsUnfloat.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PatternSynonyms #-}

module GHC.StgToJS.Sinker.StringsUnfloat
  ( unfloatStringLits
  )
  where

import GHC.Prelude
import GHC.Types.Unique.Set
import GHC.Types.Unique.FM
import GHC.Stg.Syntax
import GHC.Types.Id
import GHC.Types.Name
import GHC.Types.Literal
import GHC.Utils.Misc (partitionWith)

import Data.ByteString qualified as BS
import Data.ByteString (ByteString)
import Data.Bifunctor (Bifunctor (..))

-- | We suppose that every string shorter than 80 symbols is safe for sink.
-- Sinker is working on per module. It means that ALL locally defined strings
-- in a module shorter 80 symbols will be unfloated back.
pattern STRING_LIT_MAX_LENGTH :: Int
pattern STRING_LIT_MAX_LENGTH = 80

unfloatStringLits
  :: UniqSet Name
  -> UniqFM Name ByteString
  -> [CgStgBinding]
  -> ([CgStgBinding], UniqSet Name)
unfloatStringLits usedOnceStringLits stringLits =
  unfloatStringLits' (selectStringLitsForUnfloat usedOnceStringLits stringLits)

-- | We are doing attempts to unfloat string literals back to
-- the call site. Further special JS optimizations
-- can generate more performant operations over them.
unfloatStringLits' :: UniqFM Name ByteString -> [CgStgBinding] -> ([CgStgBinding], UniqSet Name)
unfloatStringLits' stringLits allBindings = (binderWithoutChanges ++ binderWithUnfloatedStringLit, actuallyUsedStringLitNames)
  where
    (binderWithoutChanges, binderWithUnfloatedStringLitPairs) = partitionWith substituteStringLit allBindings

    binderWithUnfloatedStringLit = fst <$> binderWithUnfloatedStringLitPairs
    actuallyUsedStringLitNames = unionManyUniqSets (snd <$> binderWithUnfloatedStringLitPairs)

    substituteStringLit :: CgStgBinding -> Either CgStgBinding (CgStgBinding, UniqSet Name)
    substituteStringLit x@(StgRec bnds)
      | isEmptyUniqSet names = Left x
      | otherwise = Right (StgRec bnds', names)
      where
        (bnds', names) = extractNames id $ do
          (i, rhs) <- bnds
          pure $ case processStgRhs rhs of
            Nothing -> Left (i, rhs)
            Just (rhs', names) -> Right ((i, rhs'), names)
    substituteStringLit x@(StgNonRec binder rhs)
      = maybe (Left x)
        (\(body', names) -> Right (StgNonRec binder body', names))
        (processStgRhs rhs)

    processStgRhs :: CgStgRhs -> Maybe (CgStgRhs, UniqSet Name)
    processStgRhs (StgRhsCon ccs dataCon mu ticks args typ)
      | isEmptyUniqSet names = Nothing
      | otherwise = Just (StgRhsCon ccs dataCon mu ticks unified typ, names)
      where
        (unified, names) = substituteArgWithNames args
    processStgRhs (StgRhsClosure fvs ccs upd bndrs body typ)
      = (\(body', names) -> (StgRhsClosure fvs ccs upd bndrs body' typ, names)) <$>
        processStgExpr body

    -- Recursive expressions
    processStgExpr :: CgStgExpr -> Maybe (CgStgExpr, UniqSet Name)
    processStgExpr (StgLit _) = Nothing
    processStgExpr (StgTick _ _) = Nothing
    processStgExpr (StgLet n b e) =
      case (substituteStringLit b, processStgExpr e) of
        (Left _, Nothing) -> Nothing
        (Right (b', names), Nothing) -> Just (StgLet n b' e, names)
        (Left _, Just (e', names)) -> Just (StgLet n b e', names)
        (Right (b', names), Just (e', names')) -> Just (StgLet n b' e', names `unionUniqSets` names')
    processStgExpr (StgLetNoEscape n b e) =
      case (substituteStringLit b, processStgExpr e) of
        (Left _, Nothing) -> Nothing
        (Right (b', names), Nothing) -> Just (StgLetNoEscape n b' e, names)
        (Left _, Just (e', names)) -> Just (StgLetNoEscape n b e', names)
        (Right (b', names), Just (e', names')) -> Just (StgLetNoEscape n b' e', names `unionUniqSets` names')
    -- We should keep the order: See Note [Case expression invariants]
    processStgExpr (StgCase e bndr alt_type alts) =
      case (isEmptyUniqSet names, processStgExpr e) of
        (True, Nothing) -> Nothing
        (True, Just (e', names')) -> Just (StgCase e' bndr alt_type alts, names')
        (False, Nothing) -> Just (StgCase e bndr alt_type unified, names)
        (False, Just (e', names')) -> Just (StgCase e' bndr alt_type unified, names `unionUniqSets` names')
      where
        (unified, names) = extractNames splitAlts alts

        splitAlts :: CgStgAlt -> Either CgStgAlt (CgStgAlt, UniqSet Name)
        splitAlts alt@(GenStgAlt con bndrs rhs) =
          case processStgExpr rhs of
            Nothing -> Left alt
            Just (alt', names) -> Right (GenStgAlt con bndrs alt', names)

    -- No args
    processStgExpr (StgApp _ []) = Nothing
    processStgExpr (StgConApp _ _ [] _) = Nothing
    processStgExpr (StgOpApp _ [] _) = Nothing

    -- Main targets. Preserving the order of args is important
    processStgExpr (StgApp fn args@(_:_))
      | isEmptyUniqSet names = Nothing
      | otherwise = Just (StgApp fn unified, names)
      where
        (unified, names) = substituteArgWithNames args
    processStgExpr (StgConApp dc n args@(_:_) tys)
      | isEmptyUniqSet names = Nothing
      | otherwise = Just (StgConApp dc n unified tys, names)
      where
        (unified, names) = substituteArgWithNames args
    processStgExpr (StgOpApp op args@(_:_) tys)
      | isEmptyUniqSet names = Nothing
      | otherwise = Just (StgOpApp op unified tys, names)
      where
        (unified, names) = substituteArgWithNames args

    substituteArg :: StgArg -> Either StgArg (StgArg, Name)
    substituteArg a@(StgLitArg _) = Left a
    substituteArg a@(StgVarArg i) =
      let name = idName i
      in case lookupUFM stringLits name of
        Nothing -> Left a
        Just b -> Right (StgLitArg $ LitString b, name)

    substituteArgWithNames = extractNames (second (second unitUniqSet) . substituteArg)

    extractNames :: (a -> Either x (x, UniqSet Name)) -> [a] -> ([x], UniqSet Name)
    extractNames splitter target =
      let
        splitted = splitter <$> target
        combined = either (, emptyUniqSet) id <$> splitted
        unified = fst <$> combined
        names = unionManyUniqSets (snd <$> combined)
      in (unified, names)

selectStringLitsForUnfloat :: UniqSet Name -> UniqFM Name ByteString -> UniqFM Name ByteString
selectStringLitsForUnfloat usedOnceStringLits stringLits = alwaysUnfloat `plusUFM` usedOnceUnfloat
  where
    alwaysUnfloat = alwaysUnfloatStringLits stringLits
    usedOnceUnfloat = selectUsedOnceStringLits usedOnceStringLits stringLits

    alwaysUnfloatStringLits :: UniqFM Name ByteString -> UniqFM Name ByteString
    alwaysUnfloatStringLits = filterUFM $ \b -> BS.length b < STRING_LIT_MAX_LENGTH

    selectUsedOnceStringLits :: UniqSet Name -> UniqFM Name ByteString -> UniqFM Name ByteString
    selectUsedOnceStringLits usedOnceStringLits stringLits =
      stringLits `intersectUFM` getUniqSet usedOnceStringLits