packages feed

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

{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      : Aihc.Cpp.Parser
-- Description : Directive parsing over raw bytes
-- License     : Unlicense
--
-- Directives are parsed straight from the input bytes. Every character
-- that is significant to the C preprocessor is ASCII, so no decoding is
-- required; bytes >= 0x80 are only ever carried along inside identifiers,
-- macro bodies and message text.
module Aihc.Cpp.Parser
  ( Directive (..),
    parseDirective,
    parseDirectiveBody,
    parseDefine,
    parseInclude,
    parseLineDirective,
    parseIdentifier,
    parseQuoted,
    parseDefineParams,
    isIdentStart,
    isIdentChar,
    isOpChar,
    isSpaceChar,
    strip,
    stripStart,
  )
where

import Aihc.Cpp.Types (IncludeKind (..))
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as C
import Data.Char (isAsciiLower, isAsciiUpper, isDigit)

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

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

parseDirectiveBody :: ByteString -> Maybe Directive
parseDirectiveBody body =
  let (name, rest0) = C.span isIdentChar body
      rest = stripStart rest0
   in if C.null name
        then case C.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 :: ByteString -> Maybe Directive
parseLineDirective body =
  case C.uncons body of
    -- 'C.readInt' also accepts a leading sign; a #line number must not.
    Just (c, _) | isDigit c ->
      case C.readInt body of
        Nothing -> Nothing
        Just (lineNumber, rest0) ->
          let rest = stripStart rest0
           in case parseQuoted rest of
                Nothing -> Just (DirLine lineNumber Nothing)
                Just path -> Just (DirLine lineNumber (Just (C.unpack path)))
    _ -> Nothing

parsePragma :: ByteString -> Maybe Directive
parsePragma body =
  if strip body == "once" then Just DirPragmaOnce else Nothing

parseDefine :: ByteString -> Maybe Directive
parseDefine rest = do
  let (name, rest0) = C.span isIdentChar rest
  if C.null name
    then Nothing
    else case C.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 (stripStart restAfterParams))
      _ -> Just (DirDefineObject name (stripStart rest0))

parseDefineParams :: ByteString -> (Maybe [ByteString], ByteString)
parseDefineParams input =
  let (inside, suffix) = C.break (== ')') input
   in if C.null suffix
        then (Nothing, "")
        else
          let rawParams = C.split ',' inside
              params = map (C.takeWhile isIdentChar . strip) rawParams
           in if C.null (strip inside)
                then (Just [], C.drop 1 suffix)
                else
                  if any C.null params
                    then (Nothing, C.drop 1 suffix)
                    else (Just params, C.drop 1 suffix)

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

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

parseQuoted :: ByteString -> Maybe ByteString
parseQuoted txt = do
  ('"', rest) <- C.uncons txt
  let (path, suffix) = C.break (== '"') rest
  if C.null suffix then Nothing else Just path

-- | ASCII whitespace.
--
-- Deliberately not 'Data.Char.isSpace': applied to a byte, that would
-- classify 0xA0 (Latin-1 NBSP, and a perfectly ordinary UTF-8
-- continuation byte) as whitespace and split a multi-byte character in
-- half. For the same reason this module avoids 'C.words' and 'C.strip'.
isSpaceChar :: Char -> Bool
isSpaceChar c =
  c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'

-- | Drop leading ASCII whitespace.
stripStart :: ByteString -> ByteString
stripStart = C.dropWhile isSpaceChar

-- | Drop leading and trailing ASCII whitespace.
strip :: ByteString -> ByteString
strip = C.dropWhile isSpaceChar . C.dropWhileEnd isSpaceChar

-- | First character of an identifier.
--
-- Any byte >= 0x80 qualifies, so a non-ASCII identifier is scanned as one
-- token regardless of the source encoding, and a byte that decodes to
-- nothing at all is simply part of whatever token contains it.
isIdentStart :: Char -> Bool
isIdentStart c = c == '_' || isAsciiAlpha c || c >= '\x80'

-- | Subsequent characters of an identifier. See 'isIdentStart'.
isIdentChar :: Char -> Bool
isIdentChar c = c == '_' || isAsciiAlpha c || isDigit c || c >= '\x80'

isAsciiAlpha :: Char -> Bool
isAsciiAlpha c = isAsciiLower c || isAsciiUpper c

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