packages feed

aihc-parser-1.0.0.2: src/Aihc/Parser/Lex/Pragmas.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}

module Aihc.Parser.Lex.Pragmas
  ( tryParsePragma,
    parsePragmaType,
    parseControlPragma,
  )
where

import Aihc.Parser.Lex.Types
  ( DirectiveUpdate (..),
    LexToken (..),
    LexTokenKind (..),
    LexerState (..),
    advanceN,
    mkToken,
  )
import Aihc.Parser.Syntax
import Data.Char (isDigit, isSpace)
import Data.Maybe (mapMaybe)
import Data.Text (Text, pattern Empty, pattern (:<))
import Data.Text qualified as T
import Text.Read (readMaybe)

-- | Single entry point for pragma parsing.
-- Scans "{-# ... #-}" once, then parses the body to determine which Pragma it is.
tryParsePragma :: LexerState -> Maybe (LexToken, LexerState)
tryParsePragma st = do
  (rawBody, consumedLen) <- extractPragmaBody (lexerInput st)
  let fullText = "{-#" <> rawBody <> "#-}"
      pragma = Pragma {pragmaType = parsePragmaType rawBody, pragmaRawText = fullText}
      st' = advanceN consumedLen st
  Just (mkToken st st' fullText (TkPragma pragma), st')

-- | Extract the raw body text between "{-#" and "#-}".
-- Returns (body_text, total_consumed_length) where total includes "{-#" and "#-}".
extractPragmaBody :: Text -> Maybe (Text, Int)
extractPragmaBody input = do
  rest0 <- T.stripPrefix "{-#" input
  let (body, marker) = T.breakOn "#-}" rest0
  if T.null marker
    then Nothing
    else
      let consumedLen = T.length input - T.length (T.drop 3 marker)
       in Just (body, consumedLen)

-- | Parse pragma body text into a structured PragmaType.
-- The body is the text between "{-#" and "#-}" (not including the delimiters).
parsePragmaType :: Text -> PragmaType
parsePragmaType rawBody =
  let trimmed = T.strip rawBody
      upperBody = T.toUpper trimmed
   in case tryParseNamedPragma trimmed upperBody of
        Just pt -> pt
        Nothing -> PragmaUnknown ("{-#" <> rawBody <> "#-}")

tryParseNamedPragma :: Text -> Text -> Maybe PragmaType
tryParseNamedPragma body upperBody
  | Just pt <- parseLanguagePragma body upperBody = Just pt
  | Just pt <- parseInstanceOverlapPragma body upperBody = Just pt
  | Just pt <- parseOptionsPragma body upperBody = Just pt
  | Just pt <- parseWarningPragma body upperBody = Just pt
  | Just pt <- parseDeprecatedPragma body upperBody = Just pt
  | Just pt <- parseInlinePragma body upperBody = Just pt
  | Just pt <- parseUnpackPragma body upperBody = Just pt
  | Just pt <- parseSourcePragma body upperBody = Just pt
  | Just pt <- parseSCCPragma body upperBody = Just pt
  | otherwise = Nothing

parseLanguagePragma :: Text -> Text -> Maybe PragmaType
parseLanguagePragma body upperBody
  | Just rest <- T.stripPrefix "LANGUAGE" upperBody,
    isPragmaBodyEnd rest =
      let names = parseLanguagePragmaNames (dropPragmaName "LANGUAGE" body)
       in Just (PragmaLanguage names)
  | otherwise = Nothing

parseInstanceOverlapPragma :: Text -> Text -> Maybe PragmaType
parseInstanceOverlapPragma _body upperBody
  | Just rest <- T.stripPrefix "OVERLAPPING" upperBody,
    isPragmaBodyEnd rest =
      Just (PragmaInstanceOverlap Overlapping)
  | Just rest <- T.stripPrefix "OVERLAPPABLE" upperBody,
    isPragmaBodyEnd rest =
      Just (PragmaInstanceOverlap Overlappable)
  | Just rest <- T.stripPrefix "OVERLAPS" upperBody,
    isPragmaBodyEnd rest =
      Just (PragmaInstanceOverlap Overlaps)
  | Just rest <- T.stripPrefix "INCOHERENT" upperBody,
    isPragmaBodyEnd rest =
      Just (PragmaInstanceOverlap Incoherent)
  | otherwise = Nothing

parseOptionsPragma :: Text -> Text -> Maybe PragmaType
parseOptionsPragma body upperBody
  | Just rest <- T.stripPrefix "OPTIONS_GHC" upperBody,
    isPragmaBodyEnd rest =
      let settings = parseOptionsPragmaSettings (dropPragmaName "OPTIONS_GHC" body)
       in Just (PragmaLanguage settings)
  | Just rest <- T.stripPrefix "OPTIONS" upperBody,
    isPragmaBodyEnd rest =
      let settings = parseOptionsPragmaSettings (dropPragmaName "OPTIONS" body)
       in Just (PragmaLanguage settings)
  | otherwise = Nothing

parseWarningPragma :: Text -> Text -> Maybe PragmaType
parseWarningPragma body upperBody
  | Just rest <- T.stripPrefix "WARNING" upperBody,
    isPragmaBodyEnd rest =
      let msgBody = dropPragmaName "WARNING" body
          txt = T.strip msgBody
          msg = extractPragmaMessage txt
       in Just (PragmaWarning msg)
  | otherwise = Nothing

parseDeprecatedPragma :: Text -> Text -> Maybe PragmaType
parseDeprecatedPragma body upperBody
  | Just rest <- T.stripPrefix "DEPRECATED" upperBody,
    isPragmaBodyEnd rest =
      let msgBody = dropPragmaName "DEPRECATED" body
          txt = T.strip msgBody
          msg = extractPragmaMessage txt
       in Just (PragmaDeprecated msg)
  | otherwise = Nothing

parseInlinePragma :: Text -> Text -> Maybe PragmaType
parseInlinePragma body upperBody
  | Just rest <- T.stripPrefix "INLINEABLE" upperBody,
    isPragmaBodyEnd rest =
      let fullBody = T.strip (dropPragmaName "INLINEABLE" body)
       in Just (PragmaInline "INLINEABLE" fullBody)
  | Just rest <- T.stripPrefix "INLINABLE" upperBody,
    isPragmaBodyEnd rest =
      let fullBody = T.strip (dropPragmaName "INLINABLE" body)
       in Just (PragmaInline "INLINABLE" fullBody)
  | Just rest <- T.stripPrefix "NOINLINEABLE" upperBody,
    isPragmaBodyEnd rest =
      let fullBody = T.strip (dropPragmaName "NOINLINEABLE" body)
       in Just (PragmaInline "NOINLINEABLE" fullBody)
  | Just rest <- T.stripPrefix "NOINLINABLE" upperBody,
    isPragmaBodyEnd rest =
      let fullBody = T.strip (dropPragmaName "NOINLINABLE" body)
       in Just (PragmaInline "NOINLINABLE" fullBody)
  | Just rest <- T.stripPrefix "INLINE" upperBody,
    isPragmaBodyEnd rest =
      let fullBody = T.strip (dropPragmaName "INLINE" body)
       in Just (PragmaInline "INLINE" fullBody)
  | Just rest <- T.stripPrefix "NOINLINE" upperBody,
    isPragmaBodyEnd rest =
      let fullBody = T.strip (dropPragmaName "NOINLINE" body)
       in Just (PragmaInline "NOINLINE" fullBody)
  | Just rest <- T.stripPrefix "CONLIKE" upperBody,
    isPragmaBodyEnd rest =
      let fullBody = T.strip (dropPragmaName "CONLIKE" body)
       in Just (PragmaInline "CONLIKE" fullBody)
  | otherwise = Nothing

parseUnpackPragma :: Text -> Text -> Maybe PragmaType
parseUnpackPragma _body upperBody
  | Just rest <- T.stripPrefix "UNPACK" upperBody,
    isPragmaBodyEnd rest =
      Just (PragmaUnpack UnpackPragma)
  | Just rest <- T.stripPrefix "NOUNPACK" upperBody,
    isPragmaBodyEnd rest =
      Just (PragmaUnpack NoUnpackPragma)
  | otherwise = Nothing

parseSourcePragma :: Text -> Text -> Maybe PragmaType
parseSourcePragma body upperBody
  | Just rest <- T.stripPrefix "SOURCE" upperBody,
    isPragmaBodyEnd rest =
      let fullBody = T.strip (dropPragmaName "SOURCE" body)
       in Just (PragmaSource fullBody fullBody)
  | otherwise = Nothing

parseSCCPragma :: Text -> Text -> Maybe PragmaType
parseSCCPragma body upperBody
  | Just rest <- T.stripPrefix "SCC" upperBody,
    isPragmaBodyEnd rest =
      let label = T.strip (dropPragmaName "SCC" body)
       in Just (PragmaSCC label)
  | otherwise = Nothing

dropPragmaName :: Text -> Text -> Text
dropPragmaName name = T.drop (T.length name)

-- | Check if the remaining text after a pragma name is valid (whitespace or end).
isPragmaBodyEnd :: Text -> Bool
isPragmaBodyEnd text =
  case text of
    Empty -> True
    c :< _ -> isSpace c

-- | Extract message text from pragma body, handling quoted strings.
extractPragmaMessage :: Text -> Text
extractPragmaMessage txt =
  case txt of
    '"' :< _ ->
      case reads (T.unpack txt) of
        [(decoded, "")] -> T.pack decoded
        _ -> txt
    _ -> txt

-- | Parse the body of a LANGUAGE pragma into extension settings.
parseLanguagePragmaNames :: Text -> [ExtensionSetting]
parseLanguagePragmaNames body =
  mapMaybe (parseExtensionSettingName . T.strip . T.takeWhile (/= '#')) (T.splitOn "," body)

-- | Parse the body of an OPTIONS pragma into extension settings.
parseOptionsPragmaSettings :: Text -> [ExtensionSetting]
parseOptionsPragmaSettings body = go (pragmaWords body)
  where
    go ws =
      case ws of
        [] -> []
        "-cpp" : rest -> EnableExtension CPP : go rest
        "-fffi" : rest -> EnableExtension ForeignFunctionInterface : go rest
        "-fglasgow-exts" : rest -> glasgowExtsSettings <> go rest
        opt : rest
          | Just ext <- T.stripPrefix "-X" opt,
            not (T.null ext) ->
              case parseExtensionSettingName ext of
                Just setting -> setting : go rest
                Nothing -> go rest
        _ : rest -> go rest

glasgowExtsSettings :: [ExtensionSetting]
glasgowExtsSettings =
  map
    EnableExtension
    [ ConstrainedClassMethods,
      DeriveDataTypeable,
      DeriveFoldable,
      DeriveFunctor,
      DeriveGeneric,
      DeriveTraversable,
      EmptyDataDecls,
      ExistentialQuantification,
      ExplicitNamespaces,
      FlexibleContexts,
      FlexibleInstances,
      ForeignFunctionInterface,
      FunctionalDependencies,
      GeneralizedNewtypeDeriving,
      ImplicitParams,
      InterruptibleFFI,
      KindSignatures,
      LiberalTypeSynonyms,
      MagicHash,
      MultiParamTypeClasses,
      ParallelListComp,
      PatternGuards,
      PostfixOperators,
      RankNTypes,
      RecursiveDo,
      ScopedTypeVariables,
      StandaloneDeriving,
      TypeOperators,
      TypeSynonymInstances,
      UnboxedTuples,
      UnicodeSyntax,
      UnliftedFFITypes
    ]

-- | Split pragma text into words, respecting quoted strings.
pragmaWords :: Text -> [Text]
pragmaWords txt = go [] [] Nothing (T.unpack txt)
  where
    go acc current quote chars =
      case chars of
        [] ->
          let acc' = pushCurrent acc current
           in reverse acc'
        c : rest ->
          case quote of
            Just q
              | c == q -> go acc current Nothing rest
              | c == '\\' ->
                  case rest of
                    escaped : rest' -> go acc (escaped : current) quote rest'
                    [] -> go acc current quote []
              | otherwise -> go acc (c : current) quote rest
            Nothing
              | c == '"' || c == '\'' -> go acc current (Just c) rest
              | c == '\\' ->
                  case rest of
                    escaped : rest' -> go acc (escaped : current) Nothing rest'
                    [] -> go acc current Nothing []
              | c `elem` [' ', '\n', '\r', '\t'] ->
                  let acc' = pushCurrent acc current
                   in go acc' [] Nothing rest
              | otherwise -> go acc (c : current) Nothing rest

    pushCurrent acc current =
      case reverse current of
        [] -> acc
        token -> T.pack token : acc

parseControlPragma :: Text -> Maybe (Text, Either Text DirectiveUpdate)
parseControlPragma input = do
  (body, _) <- extractPragmaBody input
  let trimmed = T.strip body
      upperBody = T.toUpper trimmed
  case () of
    _
      | Just rest <- T.stripPrefix "LINE" upperBody,
        isPragmaBodyEnd rest ->
          let bodyAfter = dropPragmaName "LINE" trimmed
              ws = T.words bodyAfter
           in case ws of
                lineNo : _
                  | T.all isDigit lineNo ->
                      case readBoundedInt lineNo of
                        Just parsedLine ->
                          Just
                            ( "{-#" <> body <> "#-}",
                              Right
                                DirectiveUpdate
                                  { directiveLine = Just parsedLine,
                                    directiveCol = Just 1,
                                    directiveSourceName = parseDirectiveSourceName (T.dropWhile isSpace (T.drop (T.length lineNo) bodyAfter))
                                  }
                            )
                        Nothing -> Just ("{-#" <> body <> "#-}", Left "malformed LINE pragma")
                _ -> Just ("{-#" <> body <> "#-}", Left "malformed LINE pragma")
    _
      | Just rest <- T.stripPrefix "COLUMN" upperBody,
        isPragmaBodyEnd rest ->
          let bodyAfter = dropPragmaName "COLUMN" trimmed
              ws = T.words bodyAfter
           in case ws of
                colNo : _
                  | T.all isDigit colNo ->
                      case readBoundedInt colNo of
                        Just parsedCol ->
                          Just
                            ( "{-#" <> body <> "#-}",
                              Right DirectiveUpdate {directiveLine = Nothing, directiveCol = Just parsedCol, directiveSourceName = Nothing}
                            )
                        Nothing -> Just ("{-#" <> body <> "#-}", Left "malformed COLUMN pragma")
                _ -> Just ("{-#" <> body <> "#-}", Left "malformed COLUMN pragma")
    _ -> Nothing

parseDirectiveSourceName :: Text -> Maybe FilePath
parseDirectiveSourceName rest =
  let rest' = T.dropWhile isSpace rest
   in case rest' of
        '"' :< more ->
          let (name, trailing) = T.break (== '"') more
           in case trailing of
                '"' :< _ -> Just (T.unpack name)
                _ -> Nothing
        _ -> Nothing

readBoundedInt :: Text -> Maybe Int
readBoundedInt txt = do
  n <- readMaybe (T.unpack txt) :: Maybe Integer
  if n >= fromIntegral (minBound :: Int) && n <= fromIntegral (maxBound :: Int)
    then Just (fromInteger n)
    else Nothing