packages feed

cgrep-9.0.0: src/CGrep/Semantic/ContextFilter.hs

--
-- Copyright (c) 2013-2025 Nicola Bonelli <nicola@larthia.com>
--
-- This program is free software; you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation; either version 2 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program; if not, write to the Free Software
-- Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
--
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE UnboxedTuples #-}

module CGrep.Semantic.ContextFilter (
    FilterFunction,
    ContextFilter (..),
    isContextFilterAll,
    contextBitCode,
    contextBitComment,
    contextBitLiteral,
    mkContextFilter,
    mkParConfig,
    (~!),
    (~?),
    runContextFilter,
    startLiteralMarker,
    endLiteralMarker,
) where

import CGrep.Boundary (Boundary (..))
import CGrep.Parser.Char (chr, isSpace)
import CGrep.Text (blankByWidth)
import Data.Bits (Bits (complement, (.&.), (.|.)))
import Data.Int (Int32)
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Data.Vector as V
import Options (Options (..))

startLiteralMarker :: Char
startLiteralMarker = chr 2

endLiteralMarker :: Char
endLiteralMarker = chr 3

type FilterFunction = ContextFilter -> T.Text -> T.Text

data Context = Code | Comment | Literal
    deriving stock (Eq, Show)

newtype ContextBit = ContextBit Int32
    deriving stock (Show)
    deriving newtype (Eq, Bits)

contextBitEmpty :: ContextBit
contextBitEmpty = ContextBit 0

contextBitCode :: ContextBit
contextBitCode = ContextBit 0x1

contextBitComment :: ContextBit
contextBitComment = ContextBit 0x2

contextBitLiteral :: ContextBit
contextBitLiteral = ContextBit 0x4

(~=) :: ContextBit -> Bool -> ContextBit
b ~= True = b
_ ~= False = contextBitEmpty
{-# INLINE (~=) #-}

(~?) :: ContextFilter -> ContextBit -> Bool
f ~? b = (unFilter f .&. b) /= contextBitEmpty
{-# INLINE (~?) #-}

(~!) :: ContextFilter -> ContextBit -> ContextFilter
a ~! b = ContextFilter $ unFilter a .&. complement b
{-# INLINE (~!) #-}

newtype ContextFilter = ContextFilter {unFilter :: ContextBit}
    deriving stock (Show)
    deriving newtype (Eq, Bits)

contextFilterAll :: ContextFilter
contextFilterAll = ContextFilter (contextBitCode .|. contextBitComment .|. contextBitLiteral)
{-# NOINLINE contextFilterAll #-}

isContextFilterAll :: ContextFilter -> Bool
isContextFilterAll f = f == contextFilterAll
{-# INLINE isContextFilterAll #-}

codeFilter :: ContextFilter -> Bool
codeFilter f = (unFilter f .&. contextBitCode) /= contextBitEmpty
{-# INLINE codeFilter #-}

commentFilter :: ContextFilter -> Bool
commentFilter f = (unFilter f .&. contextBitComment) /= contextBitEmpty
{-# INLINE commentFilter #-}

literalFilter :: ContextFilter -> Bool
literalFilter f = (unFilter f .&. contextBitLiteral) /= contextBitEmpty
{-# INLINE literalFilter #-}

data RegionBoundary = RegionBoundary
    { rbBegin :: !(# Char, T.Text #)
    , rbBeginLen :: {-# UNPACK #-} !Int
    , rbEnd :: !(# Char, T.Text #)
    , rbEndLen :: {-# UNPACK #-} !Int
    }

toRegionBoundary :: Boundary -> RegionBoundary
toRegionBoundary Boundary{..} =
    RegionBoundary
        { rbBegin = (# T.head bBegin, T.tail bBegin #)
        , rbBeginLen = bBeginLen
        , rbEnd = (# T.head bEnd, T.tail bEnd #)
        , rbEndLen = bEndLen
        }

data ParConfig = ParConfig
    { commBound :: !(V.Vector RegionBoundary)
    , litrBound :: !(V.Vector RegionBoundary)
    , rawBound :: !(V.Vector RegionBoundary)
    , chrBound :: !(V.Vector RegionBoundary)
    , inits :: !T.Text
    , useMakers :: !Bool
    }

mkParConfig :: [Boundary] -> [Boundary] -> [Boundary] -> [Boundary] -> Bool -> ParConfig
mkParConfig cs ls rs chs ab =
    ParConfig
        { commBound = V.fromList $ toRegionBoundary <$> cs
        , litrBound = V.fromList $ toRegionBoundary <$> ls
        , rawBound = V.fromList $ toRegionBoundary <$> rs
        , chrBound = V.fromList $ toRegionBoundary <$> chs
        , inits =
            T.concat . Set.toList . Set.fromList $
                (safeHead . bBegin <$> cs)
                    <> (safeHead . bBegin <$> ls)
                    <> (safeHead . bBegin <$> rs)
                    <> (safeHead . bBegin <$> chs)
        , useMakers = ab
        }

data ParState = ParState
    { parCtxState :: !ContextState
    , parNextState :: !ContextState
    , parDisplay :: !Bool
    , parSkip :: {-# UNPACK #-} !Int
    , parText :: !T.Text
    }
    deriving stock (Show)

data ContextState
    = CodeState1
    | CodeStateN
    | CommState1 {-# UNPACK #-} !Int
    | CommStateN {-# UNPACK #-} !Int
    | ChrState {-# UNPACK #-} !Int
    | LitrState1 {-# UNPACK #-} !Int
    | LitrStateN {-# UNPACK #-} !Int
    | RawState {-# UNPACK #-} !Int
    deriving stock (Show, Eq, Ord)

mkContextFilter :: Options -> ContextFilter
mkContextFilter Options{..} =
    if not (code || comment || literal)
        then contextFilterAll
        else ContextFilter $ contextBitCode ~= code .|. contextBitComment ~= comment .|. contextBitLiteral ~= literal

getContext :: ContextState -> Context
getContext CodeState1 = Code
getContext CodeStateN = Code
getContext (CommState1 _) = Comment
getContext (CommStateN _) = Comment
getContext (LitrState1 _) = Literal
getContext (LitrStateN _) = Literal
getContext (RawState _) = Literal
getContext (ChrState _) = Literal
{-# INLINE getContext #-}

-- contextFilterFun:
--

runContextFilter :: ParConfig -> ContextFilter -> T.Text -> T.Text
runContextFilter conf@ParConfig{..} f txt
    | useMakers = T.unfoldr contextFilter' initialState
    | otherwise = T.unfoldr contextFilter'' initialState
  where
    !initialState =
        ParState
            { parCtxState = CodeState1
            , parNextState = CodeState1
            , parDisplay = codeFilter f
            , parSkip = 0
            , parText = txt
            }

    contextFilter' :: ParState -> Maybe (Char, ParState)
    contextFilter' !state = case T.uncons (parText state) of
        Nothing -> Nothing
        Just (!x, !xs) ->
            let !nextState = nextContextState conf state x xs f
                !nextState' = nextState{parText = xs}
                !disp = parDisplay nextState
             in if disp
                    then
                        let !currCtx = getContext (parCtxState state)
                            !nextCtx = getContext (parCtxState nextState)
                         in case currCtx of
                                Code | nextCtx == Literal -> Just (startLiteralMarker, nextState')
                                Literal | nextCtx == Code -> Just (endLiteralMarker, nextState')
                                _ -> Just (x, nextState')
                    else Just (if isSpace x then x else blankByWidth x, nextState')
    {-# INLINE contextFilter' #-}

    contextFilter'' :: ParState -> Maybe (Char, ParState)
    contextFilter'' !state = case T.uncons (parText state) of
        Nothing -> Nothing
        Just (!x, !xs) ->
            let !nextState = nextContextState conf state x xs f
                !nextState' = nextState{parText = xs}
                !shouldDisplay = parDisplay nextState || isSpace x
             in Just (if shouldDisplay then x else blankByWidth x, nextState')
    {-# INLINE contextFilter'' #-}

nextContextState :: ParConfig -> ParState -> Char -> T.Text -> ContextFilter -> ParState
nextContextState !conf !s@ParState{..} !c !cont !f
    | parSkip > 0 =
        let !newSkip = parSkip - 1
         in if newSkip == 0
                then s{parCtxState = parNextState, parSkip = 0}
                else s{parSkip = newSkip}
    | CodeState1 <- parCtxState = handleCodeState True
    | CodeStateN <- parCtxState = handleCodeState False
    | CommState1 n <- parCtxState = handleCommentState n True
    | CommStateN n <- parCtxState = handleCommentState n False
    | LitrState1 n <- parCtxState = handleLiteralState n True
    | LitrStateN n <- parCtxState = handleLiteralState n False
    | ChrState n <- parCtxState = handleCharState n
    | RawState n <- parCtxState = handleRawState n
  where
    !codeDisp = codeFilter f
    !commDisp = commentFilter f
    !litrDisp = literalFilter f

    {-# INLINE transitionWith #-}
    transitionWith !nextSt !disp !skipLen =
        let !skip = skipLen - 1
         in if skip == 0
                then s{parCtxState = nextSt, parNextState = nextSt, parDisplay = disp, parSkip = 0}
                else s{parNextState = nextSt, parDisplay = disp, parSkip = skip}

    {-# INLINE handleCodeState #-}
    handleCodeState !isFirst =
        if c `T.elem` inits conf
            then case findPrefixBoundary c cont (commBound conf) of
                (# i, Just !b #) -> transitionWith (CommState1 i) commDisp (rbBeginLen b)
                _ -> case findPrefixBoundary c cont (litrBound conf) of
                    (# i, Just !b #) -> transitionWith (LitrState1 i) codeDisp (rbBeginLen b)
                    _ -> case findPrefixBoundary c cont (rawBound conf) of
                        (# i, Just !b #) -> transitionWith (RawState i) codeDisp (rbBeginLen b)
                        _ -> case findPrefixBoundary' c cont (chrBound conf) of
                            (# i, Just !b #) -> transitionWith (ChrState i) codeDisp (rbBeginLen b)
                            _ ->
                                if isFirst
                                    then s{parCtxState = CodeStateN, parNextState = CodeStateN, parDisplay = codeDisp, parSkip = 0}
                                    else s
            else
                if isFirst
                    then s{parCtxState = CodeStateN, parNextState = CodeStateN, parDisplay = codeDisp, parSkip = 0}
                    else s

    {-# INLINE handleCommentState #-}
    handleCommentState !n !isFirst =
        let !RegionBoundary{..} = V.unsafeIndex (commBound conf) n
            (# ec, es #) = rbEnd
         in if c == ec && es `T.isPrefixOf` cont
                then transitionWith CodeState1 commDisp rbEndLen
                else
                    if isFirst
                        then s{parCtxState = CommStateN n, parNextState = CommStateN n, parDisplay = commDisp, parSkip = 0}
                        else s

    {-# INLINE handleLiteralState #-}
    handleLiteralState !n !isFirst =
        if c == '\\'
            then s{parDisplay = litrDisp, parSkip = 1}
            else
                let !RegionBoundary{..} = V.unsafeIndex (litrBound conf) n
                    (# ec, es #) = rbEnd
                 in if c == ec && es `T.isPrefixOf` cont
                        then
                            let !skip = rbEndLen - 1
                             in if skip == 0
                                    then s{parCtxState = CodeState1, parNextState = CodeState1, parDisplay = codeDisp, parSkip = 0}
                                    else s{parCtxState = CodeState1, parNextState = CodeState1, parDisplay = codeDisp, parSkip = skip}
                        else
                            if isFirst
                                then s{parCtxState = LitrStateN n, parNextState = LitrStateN n, parDisplay = litrDisp, parSkip = 0}
                                else s

    {-# INLINE handleCharState #-}
    handleCharState !n =
        if c == '\\'
            then s{parDisplay = litrDisp, parSkip = 1}
            else
                let !RegionBoundary{..} = V.unsafeIndex (chrBound conf) n
                    (# ec, es #) = rbEnd
                 in if c == ec && es `T.isPrefixOf` cont
                        then
                            let !skip = rbEndLen - 1
                             in if skip == 0
                                    then s{parCtxState = CodeState1, parNextState = CodeState1, parDisplay = codeDisp, parSkip = 0}
                                    else s{parCtxState = CodeState1, parNextState = CodeState1, parDisplay = codeDisp, parSkip = skip}
                        else s{parDisplay = litrDisp, parSkip = 0}

    {-# INLINE handleRawState #-}
    handleRawState !n =
        let !RegionBoundary{..} = V.unsafeIndex (rawBound conf) n
            (# ec, es #) = rbEnd
         in if c == ec && es `T.isPrefixOf` cont
                then
                    let !skip = rbEndLen - 1
                     in if skip == 0
                            then s{parCtxState = CodeState1, parNextState = CodeState1, parDisplay = codeDisp, parSkip = 0}
                            else s{parCtxState = CodeState1, parNextState = CodeState1, parDisplay = codeDisp, parSkip = skip}
                else s{parDisplay = litrDisp, parSkip = 0}
{-# INLINE nextContextState #-}

findPrefixBoundary :: Char -> T.Text -> V.Vector RegionBoundary -> (# Int, Maybe RegionBoundary #)
findPrefixBoundary !x !xs !vb = go 0
  where
    !len = V.length vb
    go !i
        | i >= len = (# 0, Nothing #)
        | otherwise =
            let !RegionBoundary{..} = V.unsafeIndex vb i
                (# !c, !cont #) = rbBegin
             in if c == x
                    then
                        if cont `T.isPrefixOf` xs
                            then (# i, Just (V.unsafeIndex vb i) #)
                            else go (i + 1)
                    else go (i + 1)
{-# INLINE findPrefixBoundary #-}

findPrefixBoundary' :: Char -> T.Text -> V.Vector RegionBoundary -> (# Int, Maybe RegionBoundary #)
findPrefixBoundary' !x !xs !vb = go 0
  where
    !len = V.length vb
    go !i
        | i >= len = (# 0, Nothing #)
        | otherwise =
            let !RegionBoundary{..} = V.unsafeIndex vb i
                (# !c, !cont #) = rbBegin
             in if c /= x
                    then go (i + 1)
                    else
                        if not (cont `T.isPrefixOf` xs)
                            then go (i + 1)
                            else case xs of
                                (T.uncons -> Just (!y, !ys)) ->
                                    let !skip = if y == '\\' then 1 else 0
                                     in case T.drop skip ys of
                                            (T.uncons -> Just (!z, !zs)) ->
                                                let (# !e, !es #) = rbEnd
                                                 in if z == e && es `T.isPrefixOf` zs
                                                        then (# i, Just (V.unsafeIndex vb i) #)
                                                        else go (i + 1)
                                            _ -> go (i + 1)
                                _ -> go (i + 1)

safeHead :: T.Text -> T.Text
safeHead txt = case T.uncons txt of
    Just (x, _) -> T.singleton x
    Nothing -> T.empty
{-# INLINE safeHead #-}