packages feed

aihc-parser-1.0.0.2: src/Aihc/Parser/Types.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}

module Aihc.Parser.Types
  ( TokStream (..),
    mkTokStream,
    mkTokStreamModule,
    mkTokStreamFromTokens,
    ParserErrorComponent (..),
    FoundToken (..),
    mkFoundToken,
    ParseErrorBundle,
    ParseResult (..),
    ParserConfig (..),
  )
where

import Aihc.Parser.Lex
  ( LayoutState (..),
    LexToken (..),
    LexTokenKind (..),
    Pragma,
    TokenOrigin (..),
    layoutTransition,
    mkInitialLayoutState,
    mkInitialLexerState,
    readModuleHeaderExtensions,
    scanAllTokens,
  )
import Aihc.Parser.Syntax (Extension, SourceSpan, applyExtensionSetting, applyImpliedExtensions)
import Control.DeepSeq (NFData (..))
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics (Generic)
import Text.Megaparsec qualified as MP
import Text.Megaparsec.Error qualified as MPE
import Text.Megaparsec.Stream (Stream (..))

-- | Raw Megaparsec parse error bundle for low-level parser entry points.
type ParseErrorBundle = MPE.ParseErrorBundle TokStream ParserErrorComponent

data FoundToken = FoundToken
  { foundTokenText :: !Text,
    foundTokenKind :: !(Maybe LexTokenKind),
    foundTokenSpan :: !SourceSpan,
    foundTokenOrigin :: !TokenOrigin
  }
  deriving (Eq, Ord, Show, Generic, NFData)

data ParserErrorComponent
  = UnexpectedTokenExpecting
  { unexpectedFound :: Maybe FoundToken,
    unexpectedExpecting :: Text,
    unexpectedContext :: [Text]
  }
  deriving (Eq, Ord, Show, Generic)

instance MPE.ShowErrorComponent ParserErrorComponent where
  showErrorComponent (UnexpectedTokenExpecting _ expecting _) = "expecting " <> T.unpack expecting

mkFoundToken :: LexToken -> FoundToken
mkFoundToken tok =
  FoundToken
    { foundTokenText = lexTokenText tok,
      foundTokenKind = Just (lexTokenKind tok),
      foundTokenSpan = lexTokenSpan tok,
      foundTokenOrigin = lexTokenOrigin tok
    }

-- | Token stream backed by a shared lazy raw token list and a layout overlay.
--
-- The raw token list (@tokStreamRawTokens@) is produced lazily by scanning the
-- input once. Backtracking never causes characters to be re-scanned — it only
-- restores a pointer into this shared list. The layout overlay
-- (@tokStreamLayoutState@) runs 'layoutTransition' on each raw token to produce
-- virtual @{@, @;@, @}@ tokens. This allows the parser to influence layout
-- decisions (e.g., by closing implicit layout contexts via
-- 'closeImplicitLayoutContext').
data TokStream = TokStream
  { -- | Shared lazy list of raw tokens (never re-scanned on backtrack).
    tokStreamRawTokens :: [LexToken],
    -- | Layout engine state (context stack, pending layout, buffer).
    tokStreamLayoutState :: LayoutState,
    -- | Hidden pragmas that appeared before the next source token.
    -- Parsers may inspect these explicitly, but they never participate in the
    -- ordinary token stream.
    tokStreamPendingPragmas :: [Pragma],
    tokStreamPrevToken :: Maybe LexToken,
    tokStreamExtensions :: [Extension],
    -- | Whether this stream has already emitted TkEOF.
    -- After EOF is emitted, 'take1_' returns Nothing.
    tokStreamEOFEmitted :: !Bool
  }

-- -- Manual Eq instance — we skip tokStreamRawTokens since list position is not
-- -- efficiently comparable and Megaparsec tracks offset separately.
-- instance Eq TokStream where
--   a == b =
--     tokStreamLayoutState a == tokStreamLayoutState b
--       && tokStreamPendingPragmas a == tokStreamPendingPragmas b
--       && tokStreamPrevToken a == tokStreamPrevToken b
--       && tokStreamExtensions a == tokStreamExtensions b
--       && tokStreamEOFEmitted a == tokStreamEOFEmitted b

-- -- Manual Ord instance
-- instance Ord TokStream where
--   compare a b =
--     compare (tokStreamEOFEmitted a) (tokStreamEOFEmitted b)

-- Manual Show instance
instance Show TokStream where
  show ts =
    "TokStream { eofEmitted = "
      <> show (tokStreamEOFEmitted ts)
      <> ", pendingPragmas = "
      <> show (length (tokStreamPendingPragmas ts))
      <> ", prevToken = "
      <> show (tokStreamPrevToken ts)
      <> ", extensions = "
      <> show (tokStreamExtensions ts)
      <> " }"

-- Manual NFData instance — don't force the lazy raw token list.
instance NFData TokStream where
  rnf ts =
    rnf (tokStreamPendingPragmas ts) `seq`
      rnf (tokStreamPrevToken ts) `seq`
        rnf (tokStreamExtensions ts) `seq`
          rnf (tokStreamEOFEmitted ts)

-- | Create a TokStream for parsing expressions/declarations (no module layout).
mkTokStream :: FilePath -> [Extension] -> Text -> TokStream
mkTokStream sourceName exts input =
  let (env, lexSt) = mkInitialLexerState sourceName exts input
   in normalizeTokStream
        TokStream
          { tokStreamRawTokens = scanAllTokens env lexSt,
            tokStreamLayoutState = mkInitialLayoutState False exts,
            tokStreamPendingPragmas = [],
            tokStreamPrevToken = Nothing,
            tokStreamExtensions = exts,
            tokStreamEOFEmitted = False
          }

-- | Create a TokStream for parsing full modules (with module-body layout).
-- Also bootstraps LANGUAGE pragma extensions from the module header.
mkTokStreamModule :: FilePath -> [Extension] -> Text -> TokStream
mkTokStreamModule sourceName baseExts input =
  let (env, lexSt) = mkInitialLexerState sourceName effectiveExts input
   in normalizeTokStream
        TokStream
          { tokStreamRawTokens = scanAllTokens env lexSt,
            tokStreamLayoutState = mkInitialLayoutState True effectiveExts,
            tokStreamPendingPragmas = [],
            tokStreamPrevToken = Nothing,
            tokStreamExtensions = effectiveExts,
            tokStreamEOFEmitted = False
          }
  where
    headerSettings = readModuleHeaderExtensions input
    effectiveExts = applyImpliedExtensions (foldr applyExtensionSetting baseExts headerSettings)

-- | Create a TokStream from pre-lexed tokens (for testing/compatibility).
-- Layout tokens must already be inserted in the token list.
mkTokStreamFromTokens :: [LexToken] -> TokStream
mkTokStreamFromTokens toks =
  let (env, lexSt) = mkInitialLexerState "<tokens>" [] ""
   in normalizeTokStream
        TokStream
          { tokStreamRawTokens = scanAllTokens env lexSt,
            tokStreamLayoutState =
              (mkInitialLayoutState False [])
                { layoutBuffer = toks
                },
            tokStreamPendingPragmas = [],
            tokStreamPrevToken = Nothing,
            tokStreamExtensions = [],
            tokStreamEOFEmitted = False
          }

pragmaFromToken :: LexToken -> Maybe Pragma
pragmaFromToken tok =
  case lexTokenKind tok of
    TkPragma pragma' -> Just pragma'
    _ -> Nothing

isHiddenToken :: LexToken -> Bool
isHiddenToken tok =
  case lexTokenKind tok of
    TkPragma _ -> True
    TkLineComment -> True
    TkBlockComment -> True
    _ -> False

normalizeTokStream :: TokStream -> TokStream
normalizeTokStream ts0
  | tokStreamEOFEmitted ts0 = ts0
  | otherwise = go ts0
  where
    go ts =
      case layoutBuffer (tokStreamLayoutState ts) of
        tok : rest
          | Just pragma' <- pragmaFromToken tok ->
              go
                ts
                  { tokStreamLayoutState = (tokStreamLayoutState ts) {layoutBuffer = rest},
                    tokStreamPendingPragmas = tokStreamPendingPragmas ts <> [pragma']
                  }
          | isHiddenToken tok ->
              go
                ts
                  { tokStreamLayoutState = (tokStreamLayoutState ts) {layoutBuffer = rest}
                  }
          | otherwise -> ts
        [] ->
          case tokStreamRawTokens ts of
            [] -> ts
            rawTok : rawRest ->
              let (allToks, laySt') = layoutTransition (tokStreamLayoutState ts) rawTok
                  laySt'' = laySt' {layoutBuffer = allToks}
               in go ts {tokStreamRawTokens = rawRest, tokStreamLayoutState = laySt''}

-- | Step one token from the stream. This is the core primitive used by all
-- Stream methods.
--
-- Tokens are produced in two phases:
--
-- 1. Drain any buffered tokens from the layout state (virtual braces/semicolons
--    from 'layoutTransition' or 'closeImplicitLayoutContext').
-- 2. Fetch the next raw token from the shared lazy list and run
--    'layoutTransition' to produce virtual layout tokens and the raw token
--    itself. The first token is returned; the rest are buffered.
--
-- Because the raw token list is a shared lazy list, backtracking to an earlier
-- stream position is O(1) and never re-scans characters.
stepOne :: TokStream -> Maybe (LexToken, TokStream)
stepOne ts
  | tokStreamEOFEmitted ts = Nothing
  | otherwise =
      let ts0 = normalizeTokStream ts
          laySt = tokStreamLayoutState ts0
       in case layoutBuffer laySt of
            -- Drain buffered tokens first (virtual braces/semicolons)
            tok : rest ->
              let isEOF = lexTokenKind tok == TkEOF
                  pendingPragmas =
                    case lexTokenOrigin tok of
                      InsertedLayout -> tokStreamPendingPragmas ts0
                      FromSource
                        | lexTokenKind tok == TkSpecialSemicolon -> tokStreamPendingPragmas ts0
                        | otherwise -> []
                  ts' =
                    ts0
                      { tokStreamLayoutState = laySt {layoutBuffer = rest},
                        tokStreamPendingPragmas = pendingPragmas,
                        tokStreamPrevToken = Just tok,
                        tokStreamEOFEmitted = isEOF
                      }
               in Just
                    (tok, normalizeTokStream ts')
            [] ->
              Nothing

instance Stream TokStream where
  type Token TokStream = LexToken
  type Tokens TokStream = [LexToken]

  tokenToChunk _ tok = [tok]
  tokensToChunk _ = id
  chunkToTokens _ = id
  chunkLength _ = length
  chunkEmpty _ = null

  take1_ = stepOne

  takeN_ n ts
    | n <= 0 = Just ([], ts)
    | otherwise =
        case stepOne ts of
          Nothing -> Nothing
          Just (tok, ts') ->
            let go 1 acc s = Just (reverse (tok : acc), s)
                go k acc s =
                  case stepOne s of
                    Nothing -> Just (reverse (tok : acc), s)
                    Just (t, s') -> go (k - 1) (t : acc) s'
             in go n [] ts'

  takeWhile_ f =
    go []
    where
      go acc s =
        case stepOne s of
          Nothing -> (reverse acc, s)
          Just (tok, s')
            | f tok -> go (tok : acc) s'
            | otherwise ->
                -- Put the non-matching token back by using the pre-step state
                (reverse acc, s)

data ParserConfig = ParserConfig
  { parserSourceName :: FilePath,
    parserExtensions :: [Extension]
  }
  deriving (Eq, Show, Generic, NFData)

data ParseResult a
  = ParseOk a
  | ParseErr [(SourceSpan, Text)]

instance (NFData a) => NFData (ParseResult a) where
  rnf parseResult =
    case parseResult of
      ParseOk parsed -> rnf parsed
      ParseErr errs -> rnf errs