packages feed

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

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

module Aihc.Parser.Types
  ( TokStream (..),
    mkTokStream,
    mkTokStreamModule,
    mkTokStreamFromTokens,
    setTokStreamLayout,
    setTokStreamPendingPragmas,
    runTokStreamParser,
    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, ExtensionSet, SourceSpan, applyExtensionSetting, applyImpliedExtensions, mkExtensionSet)
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 and pending layout).
    tokStreamLayoutState :: LayoutState,
    -- | Tokens ready for the parser, including virtual layout tokens.
    tokStreamBuffer :: [LexToken],
    -- | 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,
    tokStreamExtensionSet :: ExtensionSet,
    -- | Whether this stream has already emitted TkEOF.
    -- After EOF is emitted, 'take1_' returns Nothing.
    tokStreamEOFEmitted :: !Bool,
    -- | Memoized result of stepping this stream by one token. Lookahead and
    -- backtracking step the same position many times; sharing the successor
    -- means the layout transition for each raw token runs once.
    --
    -- Never update the other fields with record syntax: build streams with
    -- 'buildTokStream' so this field stays consistent.
    tokStreamNext :: Maybe (LexToken, TokStream)
  }

-- -- 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)
      <> ", extensionSet = "
      <> show (tokStreamExtensionSet 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 (tokStreamExtensionSet 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 normalizeTokStreamParts
        (scanAllTokens env lexSt)
        (mkInitialLayoutState False exts)
        []
        []
        Nothing
        (mkExtensionSet exts)
        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 normalizeTokStreamParts
        (scanAllTokens env lexSt)
        (mkInitialLayoutState True effectiveExts)
        []
        []
        Nothing
        (mkExtensionSet effectiveExts)
        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 normalizeTokStreamParts
        (scanAllTokens env lexSt)
        (mkInitialLayoutState False [])
        toks
        []
        Nothing
        (mkExtensionSet [])
        False

-- | Replace the layout state and buffer of a stream. The result is a fresh
-- stream with a consistent memoized successor.
setTokStreamLayout :: LayoutState -> [LexToken] -> TokStream -> TokStream
setTokStreamLayout layoutState buffer ts =
  buildTokStream
    (tokStreamRawTokens ts)
    layoutState
    buffer
    (tokStreamPendingPragmas ts)
    (tokStreamPrevToken ts)
    (tokStreamExtensionSet ts)
    (tokStreamEOFEmitted ts)

-- | Replace the pending hidden pragmas of a stream.
setTokStreamPendingPragmas :: [Pragma] -> TokStream -> TokStream
setTokStreamPendingPragmas pendingPragmas ts =
  buildTokStream
    (tokStreamRawTokens ts)
    (tokStreamLayoutState ts)
    (tokStreamBuffer ts)
    pendingPragmas
    (tokStreamPrevToken ts)
    (tokStreamExtensionSet ts)
    (tokStreamEOFEmitted ts)

-- | Build a stream whose memoized successor is computed from its fields.
buildTokStream :: [LexToken] -> LayoutState -> [LexToken] -> [Pragma] -> Maybe LexToken -> ExtensionSet -> Bool -> TokStream
buildTokStream rawTokens layoutState buffer pendingPragmas prevToken extensionSet eofEmitted =
  let ts =
        TokStream
          { tokStreamRawTokens = rawTokens,
            tokStreamLayoutState = layoutState,
            tokStreamBuffer = buffer,
            tokStreamPendingPragmas = pendingPragmas,
            tokStreamPrevToken = prevToken,
            tokStreamExtensionSet = extensionSet,
            tokStreamEOFEmitted = eofEmitted,
            tokStreamNext = stepOne ts
          }
   in ts

-- | Advance through layout and hidden tokens until the stream is ready for
-- 'stepOne'. Keeping the stream fields separate lets a token step normalize
-- its successor without first allocating an intermediate 'TokStream'.
normalizeTokStreamParts :: [LexToken] -> LayoutState -> [LexToken] -> [Pragma] -> Maybe LexToken -> ExtensionSet -> Bool -> TokStream
normalizeTokStreamParts rawTokens layoutState buffer pendingPragmas prevToken extensionSet eofEmitted
  | eofEmitted = finish rawTokens layoutState buffer pendingPragmas
  | otherwise = go rawTokens layoutState buffer pendingPragmas
  where
    finish rawTokens' layoutState' buffer' pendingPragmas' =
      buildTokStream rawTokens' layoutState' buffer' pendingPragmas' prevToken extensionSet eofEmitted

    go rawTokens' layoutState' buffer' pendingPragmas' =
      case buffer' of
        tok : rest ->
          case lexTokenKind tok of
            TkPragma pragma' ->
              go rawTokens' layoutState' rest (pendingPragmas' <> [pragma'])
            TkLineComment -> go rawTokens' layoutState' rest pendingPragmas'
            TkBlockComment -> go rawTokens' layoutState' rest pendingPragmas'
            _ -> finish rawTokens' layoutState' buffer' pendingPragmas'
        [] ->
          case rawTokens' of
            [] -> finish [] layoutState' [] pendingPragmas'
            rawTok : rawRest ->
              let (allToks, laySt') = layoutTransition layoutState' rawTok
               in go rawRest laySt' allToks pendingPragmas'

-- | Step one token from the stream. This is the core primitive used by all
-- Stream methods; its result is memoized in 'tokStreamNext'.
--
-- 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 =
      case tokStreamBuffer ts of
        -- Drain buffered tokens first (virtual braces/semicolons)
        tok : rest ->
          let kind = lexTokenKind tok
              isEOF = case kind of
                TkEOF -> True
                _ -> False
              pendingPragmas =
                case lexTokenOrigin tok of
                  InsertedLayout -> tokStreamPendingPragmas ts
                  FromSource
                    | TkSpecialSemicolon <- kind -> tokStreamPendingPragmas ts
                    | otherwise -> []
           in Just
                ( tok,
                  normalizeTokStreamParts
                    (tokStreamRawTokens ts)
                    (tokStreamLayoutState ts)
                    rest
                    pendingPragmas
                    (Just tok)
                    (tokStreamExtensionSet ts)
                    isEOF
                )
        [] ->
          Nothing

-- | Run a token parser from the start of a stream.
--
-- The position state that Megaparsec keeps for error rendering is detached
-- from the stream, so the memoized successor chain can be collected as the
-- parser advances.
runTokStreamParser :: MP.Parsec ParserErrorComponent TokStream a -> FilePath -> TokStream -> Either ParseErrorBundle a
runTokStreamParser parser sourceName ts =
  snd (MP.runParser' parser initialState)
  where
    initialState =
      MP.State
        { MP.stateInput = ts,
          MP.stateOffset = 0,
          MP.statePosState =
            MP.PosState
              { MP.pstateInput = detachedStream,
                MP.pstateOffset = 0,
                MP.pstateSourcePos = MP.initialPos sourceName,
                MP.pstateTabWidth = MP.defaultTabWidth,
                MP.pstateLinePrefix = ""
              },
          MP.stateParseErrors = []
        }
    detachedStream = buildTokStream [] (tokStreamLayoutState ts) [] [] Nothing (tokStreamExtensionSet ts) True

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

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

  take1_ = tokStreamNext

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

  takeWhile_ f =
    go []
    where
      go acc s =
        case tokStreamNext 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