packages feed

aihc-cpp-1.0.0.0: src/Aihc/Cpp/Parser.hs

{-# LANGUAGE OverloadedStrings #-}

module Aihc.Cpp.Parser
  ( Directive (..),
    parseDirective,
    parseDirectiveBody,
    parseDefine,
    parseInclude,
    parseLineDirective,
    parseIdentifier,
    parseQuotedText,
    parseDefineParams,
    isIdentStart,
    isIdentChar,
    isOpChar,
  )
where

import Aihc.Cpp.Types (IncludeKind (..))
import Data.Char (isAlphaNum, isDigit, isLetter)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Read as TR

data Directive
  = DirDefineObject !Text !Text
  | DirDefineFunction !Text ![Text] !Text
  | DirUndef !Text
  | DirInclude !IncludeKind !Text
  | DirIf !Text
  | DirIfDef !Text
  | DirIfNDef !Text
  | DirElif !Text
  | DirElse
  | DirEndIf
  | DirLine !Int !(Maybe FilePath)
  | DirPragmaOnce
  | DirWarning !Text
  | DirError !Text
  | DirUnsupported !Text

parseDirective :: Text -> Maybe Directive
parseDirective raw =
  let trimmed = T.stripStart raw
   in if "#" `T.isPrefixOf` trimmed
        then
          let body = T.stripStart (T.drop 1 trimmed)
           in case T.uncons body of
                Just (c, _) | isLetter c || isDigit c -> parseDirectiveBody body
                _ -> Nothing
        else Nothing

parseDirectiveBody :: Text -> Maybe Directive
parseDirectiveBody body =
  let (name, rest0) = T.span isIdentChar body
      rest = T.stripStart rest0
   in if T.null name
        then case T.uncons body of
          Just (c, _) | isDigit c -> parseLineDirective body
          _ -> Nothing
        else case name of
          "define" -> parseDefine rest
          "undef" -> DirUndef <$> parseIdentifier rest
          "include" -> parseInclude rest
          "if" -> Just (DirIf rest)
          "ifdef" -> DirIfDef <$> parseIdentifier rest
          "ifndef" -> DirIfNDef <$> parseIdentifier rest
          -- Keep `#isndef` as explicitly unsupported for diagnostics on common typo input.
          "isndef" -> Just (DirUnsupported "isndef")
          "elif" -> Just (DirElif rest)
          "elseif" -> Just (DirElif rest)
          "else" -> Just DirElse
          "endif" -> Just DirEndIf
          "line" -> parseLineDirective rest
          "pragma" -> parsePragma rest
          "warning" -> Just (DirWarning rest)
          "error" -> Just (DirError rest)
          _ -> Nothing

parseLineDirective :: Text -> Maybe Directive
parseLineDirective body =
  case TR.decimal body of
    Left _ -> Nothing
    Right (lineNumber, rest0) ->
      let rest = T.stripStart rest0
       in case parseQuotedText rest of
            Nothing -> Just (DirLine lineNumber Nothing)
            Just path -> Just (DirLine lineNumber (Just (T.unpack path)))

parsePragma :: Text -> Maybe Directive
parsePragma body =
  case T.words body of
    ["once"] -> Just DirPragmaOnce
    _ -> Nothing

parseDefine :: Text -> Maybe Directive
parseDefine rest = do
  let (name, rest0) = T.span isIdentChar rest
  if T.null name
    then Nothing
    else case T.uncons rest0 of
      Just ('(', afterOpen) ->
        let (params, restAfterParams) = parseDefineParams afterOpen
         in case params of
              Nothing -> Just (DirUnsupported "define-function-macro")
              Just names -> Just (DirDefineFunction name names (T.stripStart restAfterParams))
      _ -> Just (DirDefineObject name (T.stripStart rest0))

parseDefineParams :: Text -> (Maybe [Text], Text)
parseDefineParams input =
  let (inside, suffix) = T.breakOn ")" input
   in if T.null suffix
        then (Nothing, "")
        else
          let rawParams = T.splitOn "," inside
              params = map (T.takeWhile isIdentChar . T.strip) rawParams
           in if T.null (T.strip inside)
                then (Just [], T.drop 1 suffix)
                else
                  if any T.null params
                    then (Nothing, T.drop 1 suffix)
                    else (Just params, T.drop 1 suffix)

parseIdentifier :: Text -> Maybe Text
parseIdentifier txt =
  let ident = T.takeWhile isIdentChar (T.stripStart txt)
   in if T.null ident then Nothing else Just ident

parseInclude :: Text -> Maybe Directive
parseInclude txt =
  case T.uncons (T.stripStart txt) of
    Just ('"', rest) ->
      let (path, suffix) = T.breakOn "\"" rest
       in if T.null suffix then Nothing else Just (DirInclude IncludeLocal path)
    Just ('<', rest) ->
      let (path, suffix) = T.breakOn ">" rest
       in if T.null suffix then Nothing else Just (DirInclude IncludeSystem path)
    _ -> Nothing

parseQuotedText :: Text -> Maybe Text
parseQuotedText txt = do
  ('"', rest) <- T.uncons txt
  let (path, suffix) = T.breakOn "\"" rest
  if T.null suffix then Nothing else Just path

isIdentStart :: Char -> Bool
isIdentStart c = c == '_' || isLetter c

isIdentChar :: Char -> Bool
isIdentChar c = c == '_' || isAlphaNum c

isOpChar :: Char -> Bool
isOpChar c =
  c == '+'
    || c == '-'
    || c == '*'
    || c == '/'
    || c == '%'
    || c == '&'
    || c == '|'
    || c == '!'
    || c == '='
    || c == '<'
    || c == '>'