packages feed

cgrep-9.0.0: src/CGrep/Parser/Token.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
--
-- 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 ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module CGrep.Parser.Token (
    parseTokens,
    filterToken,
    Token (..),
    TokenFilter (..),
    mkTokenFilter,
    eqToken,
    isTokenIdentifier,
    isTokenKeyword,
    isTokenNumber,
    isTokenBracket,
    isTokenString,
    isTokenOperator,
    isTokenUnspecified,
    tTyp,
    tToken,
    tOffset,
    mkTokenIdentifier,
    mkTokenKeyword,
    mkTokenDigit,
    mkTokenBracket,
    mkTokenString,
    mkTokenOperator,
) where

-- import qualified Data.DList as DL

-- import CGrep.Parser.Char (
--     chr,
--     isAlphaNum_,
--     isAlpha_,
--     isBracket',
--     isCharNumber,
--     isDigit,
--     isPunctuation,
--     isSpace,
--  )

-- import Data.List (genericLength)

import CGrep.FileTypeMap (
    -- CharIdentifierF,
    CharSet (..),
    FileTypeInfo (..),
    IsCharSet (..),
    WordType (..),
 )

import qualified Data.HashMap.Strict as HM

-- import Data.Sequence (Seq (Empty, (:<|), (:|>)), (|>))
import Data.Sequence (Seq, (|>))
import qualified Data.Sequence as S

import Control.Monad.ST (ST, runST)
import Data.STRef (STRef, modifySTRef', newSTRef, readSTRef, writeSTRef)

import CGrep.Parser.Chunk

import CGrep.Semantic.ContextFilter
import CGrep.Parser.Char (isBracket', isCharNumber, isDigit, isPunctuation, isSpace)
import CGrep.Text (iterM, textOffsetWord8, textSlice)
import Data.Coerce (coerce)
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import qualified Data.Text.Unsafe as TU
import GHC.Exts (inline)

newtype TokenState = TokenState {_unTokenState :: Int}
    deriving newtype (Eq)

instance Show TokenState where
    show StateSpace = "space"
    show StateIdentifier = "identifier"
    show StateDigit = "digit"
    show StateBracket = "bracket"
    show StateLiteral = "literal"
    show StateOther = "other"
    {-# INLINE show #-}

pattern StateSpace :: TokenState
pattern StateSpace = TokenState 0

pattern StateIdentifier :: TokenState
pattern StateIdentifier = TokenState 1

pattern StateDigit :: TokenState
pattern StateDigit = TokenState 2

pattern StateBracket :: TokenState
pattern StateBracket = TokenState 3

pattern StateLiteral :: TokenState
pattern StateLiteral = TokenState 4

pattern StateOther :: TokenState
pattern StateOther = TokenState 5

{-# COMPLETE StateSpace, StateIdentifier, StateDigit, StateBracket, StateLiteral, StateOther #-}

newtype Token = Token Chunk
    deriving newtype (Eq, Ord)

instance Show Token where
    show (Token (Chunk typ txt)) = case typ of
        ChunkUnspec -> "(*)"
        _ -> "(" <> show typ <> " '" <> T.unpack txt <> "' @" <> show (textOffsetWord8 txt) <> ")"
    {-# INLINE show #-}

eqToken :: Token -> Token -> Bool
eqToken a b =
    tToken a == tToken b
        && tTyp a == tTyp b
{-# INLINE eqToken #-}

mkTokenIdentifier :: T.Text -> Token
mkTokenIdentifier bs = Token $ Chunk ChunkIdentifier bs
{-# INLINE mkTokenIdentifier #-}

mkTokenKeyword :: T.Text -> Token
mkTokenKeyword bs = Token $ Chunk ChunkKeyword bs
{-# INLINE mkTokenKeyword #-}

mkTokenDigit :: T.Text -> Token
mkTokenDigit bs = Token $ Chunk ChunkDigit bs
{-# INLINE mkTokenDigit #-}

mkTokenBracket :: T.Text -> Token
mkTokenBracket bs = Token $ Chunk ChunkBracket bs
{-# INLINE mkTokenBracket #-}

mkTokenOperator :: T.Text -> Token
mkTokenOperator bs = Token $ Chunk ChunkOperator bs
{-# INLINE mkTokenOperator #-}

mkTokenString :: T.Text -> Token
mkTokenString bs = Token $ Chunk ChunkString bs
{-# INLINE mkTokenString #-}

mkTokenNativeType :: T.Text -> Token
mkTokenNativeType bs = Token $ Chunk ChunkNativeType bs
{-# INLINE mkTokenNativeType #-}

mkTokenFromWord :: Maybe FileTypeInfo -> T.Text -> Token
mkTokenFromWord Nothing txt = mkTokenIdentifier txt
mkTokenFromWord (Just info) txt =
    case HM.lookup txt (ftKeywords info) of
        Just typ -> case typ of
            Keyword -> mkTokenKeyword txt
            NativeType -> mkTokenNativeType txt
        _ -> mkTokenIdentifier txt
{-# INLINEABLE mkTokenFromWord #-}

mkToken :: Maybe FileTypeInfo -> TokenState -> T.Text -> Token
mkToken info state = case state of
    StateSpace -> mkTokenOperator
    StateIdentifier -> mkTokenFromWord info
    StateDigit -> mkTokenDigit
    StateBracket -> mkTokenBracket
    StateLiteral -> mkTokenString
    StateOther -> mkTokenOperator
{-# INLINE mkToken #-}

tTyp :: Token -> ChunkType
tTyp = cTyp . coerce
{-# INLINE tTyp #-}

tOffset :: Token -> Int
tOffset t = cOffset (coerce t :: Chunk)
{-# INLINE tOffset #-}

tToken :: Token -> T.Text
tToken t = cToken (coerce t :: Chunk)
{-# INLINE tToken #-}

isTokenIdentifier :: Token -> Bool
isTokenIdentifier t = cTyp (coerce t) == ChunkIdentifier
{-# INLINE isTokenIdentifier #-}

isTokenKeyword :: Token -> Bool
isTokenKeyword t = cTyp (coerce t) == ChunkKeyword
{-# INLINE isTokenKeyword #-}

isTokenNumber :: Token -> Bool
isTokenNumber t = cTyp (coerce t) == ChunkDigit
{-# INLINE isTokenNumber #-}

isTokenBracket :: Token -> Bool
isTokenBracket t = cTyp (coerce t) == ChunkBracket
{-# INLINE isTokenBracket #-}

isTokenOperator :: Token -> Bool
isTokenOperator t = cTyp (coerce t) == ChunkOperator
{-# INLINE isTokenOperator #-}

isTokenString :: Token -> Bool
isTokenString t = cTyp (coerce t) == ChunkString
{-# INLINE isTokenString #-}

isTokenNativeType :: Token -> Bool
isTokenNativeType t = cTyp (coerce t) == ChunkNativeType
{-# INLINE isTokenNativeType #-}

isTokenUnspecified :: Token -> Bool
isTokenUnspecified t = cTyp (coerce t) == ChunkUnspec
{-# INLINE isTokenUnspecified #-}

data TokenFilter = TokenFilter
    { tfIdentifier :: {-# UNPACK #-} !Bool
    , tfKeyword :: {-# UNPACK #-} !Bool
    , tfNativeType :: {-# UNPACK #-} !Bool
    , tfString :: {-# UNPACK #-} !Bool
    , tfNumber :: {-# UNPACK #-} !Bool
    , tfOperator :: {-# UNPACK #-} !Bool
    , tfBracket :: {-# UNPACK #-} !Bool
    }
    deriving stock (Eq, Show)

filterToken :: TokenFilter -> Token -> Bool
filterToken f t = case cTyp (coerce t :: Chunk) of
    ChunkIdentifier -> tfIdentifier f
    ChunkKeyword -> tfKeyword f
    ChunkDigit -> tfNumber f
    ChunkOperator -> tfOperator f
    ChunkString -> tfString f
    ChunkNativeType -> tfNativeType f
    ChunkBracket -> tfBracket f
    ChunkUnspec -> False

mkTokenFilter :: (Traversable t) => t ChunkType -> TokenFilter
mkTokenFilter = foldr go (TokenFilter False False False False False False False)
  where
    go ChunkIdentifier f = f{tfIdentifier = True}
    go ChunkKeyword f = f{tfKeyword = True}
    go ChunkNativeType f = f{tfNativeType = True}
    go ChunkDigit f = f{tfNumber = True}
    go ChunkOperator f = f{tfOperator = True}
    go ChunkString f = f{tfString = True}
    go ChunkBracket f = f{tfBracket = True}
    go ChunkUnspec f = f

(<~) :: STRef s a -> a -> ST s ()
ref <~ !x = writeSTRef ref x
{-# INLINE (<~) #-}

data TokenIdx = TokenIdx
    { _offset :: {-# UNPACK #-} !Int
    , len :: {-# UNPACK #-} !Int
    }

tkString :: TokenIdx -> T.Text -> T.Text
tkString (TokenIdx off len) = TU.takeWord8 len . TU.dropWord8 off
{-# INLINE tkString #-}

data AccOp = Reset | Start {-# UNPACK #-} !Int !Int | Append {-# UNPACK #-} !Int !Int

(<<~) :: STRef s TokenIdx -> AccOp -> ST s ()
ref <<~ Reset = writeSTRef ref (TokenIdx (-1) 0)
ref <<~ Start offset delta = writeSTRef ref (TokenIdx offset delta)
ref <<~ Append offset delta = modifySTRef' ref $ \case
    TokenIdx (-1) 0 -> TokenIdx offset delta
    TokenIdx offset' delta' -> TokenIdx offset' (delta + delta')
{-# INLINE (<<~) #-}

{-# INLINE parseTokens #-}
parseTokens :: TokenFilter -> Maybe FileTypeInfo -> Bool -> T.Text -> Seq Token
parseTokens tf info strict txt =
    runST $ do
        let (runtimeCS1, runtimeCS2) = fromMaybe (None, None) (info >>= ftIdentCharSet)
        case (runtimeCS1, runtimeCS2) of
            (None, None) -> parseToken' @'None @'None tf info True txt
            (Alpha_, AlphaNum_') -> parseToken' @'Alpha_ @'AlphaNum_' tf info strict txt
            (Alpha_, AlphaNum_) -> parseToken' @'Alpha_ @'AlphaNum_ tf info strict txt
            (Alpha, AlphaNum_) -> parseToken' @'Alpha @'AlphaNum_ tf info strict txt
            (Alpha_, AlphaNumDash_) -> parseToken' @'Alpha_ @'AlphaNumDash_ tf info strict txt
            (AlphaDash_, AlphaNumDash_) -> parseToken' @'AlphaDash_ @'AlphaNumDash_ tf info strict txt
            (ClojureIdentStart, ClojureIdentCont) -> parseToken' @'ClojureIdentStart @'ClojureIdentCont tf info strict txt
            (CSharpIdentStart, CSharpIdentCont) -> parseToken' @'CSharpIdentStart @'CSharpIdentCont tf info strict txt
            (HtmlIdentStart, HtmlIdentCont) -> parseToken' @'HtmlIdentStart @'HtmlIdentCont tf info strict txt
            (JavaIdentStart, JavaIdentCont) -> parseToken' @'JavaIdentStart @'JavaIdentCont tf info strict txt
            (JuliaIdentStart, JuliaIdentCont) -> parseToken' @'JuliaIdentStart @'JuliaIdentCont tf info strict txt
            (ListIdent, ListIdent) -> parseToken' @'ListIdent @'ListIdent tf info strict txt
            (Unicode_, UnicodeNum_) -> parseToken' @'Unicode_ @'UnicodeNum_ tf info strict txt
            (UnicodeDollar_, UnicodeNumDollar_) -> parseToken' @'UnicodeDollar_ @'UnicodeNumDollar_ tf info strict txt
            (Unicode_, UnicodeNum_') -> parseToken' @'Unicode_ @'UnicodeNum_' tf info strict txt
            (Unicode_, UnicodeNumDollar_) -> parseToken' @'Unicode_ @'UnicodeNumDollar_ tf info strict txt
            (UnicodeXIDStart_, UnicodeNumXIDCont_) -> parseToken' @'UnicodeXIDStart_ @'UnicodeNumXIDCont_ tf info strict txt
            (AgdaIdent, AgdaIdent) -> parseToken' @'AgdaIdent @'AgdaIdent tf info strict txt
            _ -> error $ "CGrep: unsupported CharSet combination: " <> show (runtimeCS1, runtimeCS2)

parseToken' :: forall (cs1 :: CharSet) (cs :: CharSet) a. (IsCharSet cs1, IsCharSet cs) => TokenFilter -> Maybe FileTypeInfo -> Bool -> T.Text -> ST a (S.Seq Token)
parseToken' tf@TokenFilter{..} info strict txt = do
    stateR <- newSTRef StateSpace
    accR <- newSTRef (TokenIdx (-1) (-1))
    tokensR <- newSTRef S.empty

    iterM txt $ \(# x, cur, delta #) -> do
        state <- readSTRef stateR
        case state of
            StateSpace ->
                if
                    | isSpace x -> do accR <<~ Reset
                    | inline (isValidChar @cs1 x) -> do stateR <~ StateIdentifier; accR <<~ Start cur delta
                    | x == startLiteralMarker -> do stateR <~ StateLiteral; accR <<~ Reset
                    | isDigit x -> do stateR <~ StateDigit; accR <<~ Start cur delta
                    | isBracket' x -> do stateR <~ StateBracket; accR <<~ Start cur delta
                    | otherwise -> do stateR <~ StateOther; accR <<~ Start cur delta
            StateIdentifier ->
                if isValidChar @cs x
                    then accR <<~ Append cur delta
                    else do
                        acc <- readSTRef accR
                        tokens <- readSTRef tokensR
                        if
                            | isSpace x -> do stateR <~ StateSpace; accR <<~ Reset; tokensR <~ (tokens |> buildTokenIf tfIdentifier tfKeyword tfNativeType (mkTokenFromWord info) acc txt)
                            | x == startLiteralMarker -> do stateR <~ StateLiteral; accR <<~ Reset; tokensR <~ (tokens |> buildTokenIf tfIdentifier tfKeyword tfNativeType (mkTokenFromWord info) acc txt)
                            | isBracket' x -> do stateR <~ StateBracket; accR <<~ Start cur delta; tokensR <~ (tokens |> buildTokenIf tfIdentifier tfKeyword tfNativeType (mkTokenFromWord info) acc txt)
                            | otherwise -> do stateR <~ StateOther; accR <<~ Start cur delta; tokensR <~ (tokens |> buildTokenIf tfIdentifier tfKeyword tfNativeType (mkTokenFromWord info) acc txt)
            StateDigit ->
                if isCharNumber x
                    then accR <<~ Append cur delta
                    else do
                        acc <- readSTRef accR
                        tokens <- readSTRef tokensR
                        if
                            | isSpace x -> do stateR <~ StateSpace; accR <<~ Reset; tokensR <~ (tokens |> buildToken tfNumber mkTokenDigit acc txt)
                            | x == startLiteralMarker -> do stateR <~ StateLiteral; accR <<~ Reset; tokensR <~ (tokens |> buildToken tfNumber mkTokenDigit acc txt)
                            | inline (isValidChar @cs1 x) -> do stateR <~ StateIdentifier; accR <<~ Start cur delta; tokensR <~ (tokens |> buildToken tfNumber mkTokenDigit acc txt)
                            | isBracket' x -> do stateR <~ StateBracket; accR <<~ Start cur delta; tokensR <~ (tokens |> buildToken tfNumber mkTokenDigit acc txt)
                            | otherwise -> do stateR <~ StateOther; accR <<~ Start cur delta; tokensR <~ (tokens |> buildToken tfNumber mkTokenDigit acc txt)
            StateLiteral ->
                if x == endLiteralMarker
                    then do
                        acc <- readSTRef accR
                        tokens <- readSTRef tokensR
                        stateR <~ StateSpace
                        accR <<~ Reset
                        tokensR <~ (tokens |> buildToken tfString mkTokenString acc txt)
                    else do accR <<~ Append cur delta
            StateBracket ->
                do
                    acc <- readSTRef accR
                    tokens <- readSTRef tokensR
                    if
                        | isSpace x -> do stateR <~ StateSpace; accR <<~ Reset; tokensR <~ (tokens |> buildToken tfBracket mkTokenBracket acc txt)
                        | inline (isValidChar @cs1 x) -> do stateR <~ StateIdentifier; accR <<~ Start cur delta; tokensR <~ (tokens |> buildToken tfBracket mkTokenBracket acc txt)
                        | isDigit x -> do stateR <~ StateDigit; accR <<~ Start cur delta; tokensR <~ (tokens |> buildToken tfBracket mkTokenBracket acc txt)
                        | isBracket' x -> do accR <<~ Start cur delta; tokensR <~ (tokens |> buildToken tfBracket mkTokenBracket acc txt)
                        | x == startLiteralMarker -> do stateR <~ StateLiteral; accR <<~ Reset; tokensR <~ (tokens |> buildToken tfBracket mkTokenBracket acc txt)
                        | otherwise -> do stateR <~ StateOther; accR <<~ Start cur delta; tokensR <~ (tokens |> buildToken tfBracket mkTokenBracket acc txt)
            StateOther ->
                do
                    acc <- readSTRef accR
                    tokens <- readSTRef tokensR
                    if
                        | isSpace x -> do stateR <~ StateSpace; accR <<~ Reset; tokensR <~ (tokens |> buildToken tfOperator mkTokenOperator acc txt)
                        | inline (isValidChar @cs1 x) -> do stateR <~ StateIdentifier; accR <<~ Start cur delta; tokensR <~ (tokens |> buildToken tfOperator mkTokenOperator acc txt)
                        | isDigit x ->
                            if tkString acc txt == "."
                                then do stateR <~ StateDigit; accR <<~ Append cur delta
                                else do stateR <~ StateDigit; accR <<~ Start cur delta; tokensR <~ (tokens |> buildToken tfOperator mkTokenOperator acc txt)
                        | isBracket' x -> do stateR <~ StateBracket; accR <<~ Start cur delta; tokensR <~ (tokens |> buildToken tfOperator mkTokenOperator acc txt)
                        | x == startLiteralMarker -> do stateR <~ StateLiteral; accR <<~ Reset; tokensR <~ (tokens |> buildToken tfBracket mkTokenBracket acc txt)
                        | isPunctuation x -> do accR <<~ Start cur delta; tokensR <~ (tokens |> buildToken tfOperator mkTokenOperator acc txt)
                        | otherwise ->
                            if strict
                                then do accR <<~ Append cur delta
                                else do accR <<~ Start cur delta; tokensR <~ (tokens |> buildToken tfOperator mkTokenOperator acc txt)

    lastAcc <- readSTRef accR
    tokens <- readSTRef tokensR
    if lastAcc.len == 0
        then return tokens
        else do
            state <- readSTRef stateR
            return $ tokens |> buildFilteredToken tf (mkToken info state) lastAcc txt

buildFilteredToken :: TokenFilter -> (T.Text -> Token) -> TokenIdx -> T.Text -> Token
buildFilteredToken tf f (TokenIdx start len) txt =
    let t = f (textSlice txt start len)
     in if filterToken tf t
            then t
            else unspecifiedToken
{-# INLINE buildFilteredToken #-}

buildToken :: Bool -> (T.Text -> Token) -> TokenIdx -> T.Text -> Token
buildToken True f (TokenIdx start len) txt = f (textSlice txt start len)
buildToken False _ (TokenIdx _start _len) _txt = unspecifiedToken
{-# INLINE buildToken #-}

buildTokenIf :: Bool -> Bool -> Bool -> (T.Text -> Token) -> TokenIdx -> T.Text -> Token
buildTokenIf i k t f (TokenIdx start len) txt =
    if i && isTokenIdentifier tok || k && isTokenKeyword tok || t && isTokenNativeType tok
        then tok
        else unspecifiedToken
  where
    tok = f (textSlice txt start len)

unspecifiedToken :: Token
unspecifiedToken = Token $ Chunk ChunkUnspec T.empty
{-# INLINE unspecifiedToken #-}