packages feed

scrod-0.2026.2.21: source/library/Scrod/Cpp/Directive.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TemplateHaskellQuotes #-}

-- | Parsing of C preprocessor directives.
--
-- Recognizes the conditional-compilation directives (@#if@, @#ifdef@,
-- @#ifndef@, @#elif@, @#else@, @#endif@), macro definitions (@#define@,
-- @#undef@), and lumps everything else (e.g., @#include@, @#error@) into
-- 'Other'. Leading whitespace and whitespace between @#@ and the keyword
-- are tolerated.
module Scrod.Cpp.Directive where

import qualified Control.Monad as Monad
import qualified Scrod.Spec as Spec
import qualified Text.Parsec as Parsec

data Directive
  = If String
  | Ifdef String
  | Ifndef String
  | Elif String
  | Else
  | Endif
  | -- | A @#define@ with a macro name and an optional replacement value.
    Define String (Maybe String)
  | Undef String
  | -- | Any directive not specifically recognized (e.g., @#include@, @#error@).
    Other
  deriving (Eq, Ord, Show)

-- | Try to parse a single line as a CPP directive. Returns 'Nothing' if the
-- line does not start with @#@ (after optional whitespace).
parse :: String -> Maybe Directive
parse = either (const Nothing) Just . Parsec.parse directive ""

space :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Char
space = Parsec.oneOf " \t"

spaces :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m ()
spaces = Parsec.skipMany space

spaces1 :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m ()
spaces1 = Parsec.skipMany1 space

lexeme :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m a -> Parsec.ParsecT s u m a
lexeme = (<* spaces)

directive :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Directive
directive = do
  spaces
  Monad.void . lexeme $ Parsec.char '#'
  keyword <- Parsec.many Parsec.letter
  case keyword of
    "if" -> If <$> rest
    "ifdef" -> Ifdef <$> name
    "ifndef" -> Ifndef <$> name
    "elif" -> Elif <$> rest
    "else" -> pure Else
    "endif" -> pure Endif
    "define" -> Define <$> name <*> value
    "undef" -> Undef <$> name
    _ -> pure Other

name :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m String
name = spaces1 *> Parsec.many1 (Parsec.choice [Parsec.alphaNum, Parsec.char '_'])

rest :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m String
rest = spaces1 *> Parsec.many Parsec.anyChar

value :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m (Maybe String)
value = Parsec.optionMaybe $ spaces1 *> Parsec.many1 Parsec.anyChar

spec :: (Applicative m, Monad n) => Spec.Spec m n -> n ()
spec s = do
  Spec.named s 'parse $ do
    Spec.it s "parses #if" $ do
      Spec.assertEq s (parse "#if 1") $ Just (If "1")

    Spec.it s "parses #ifdef" $ do
      Spec.assertEq s (parse "#ifdef FOO") $ Just (Ifdef "FOO")

    Spec.it s "parses #ifndef" $ do
      Spec.assertEq s (parse "#ifndef FOO") $ Just (Ifndef "FOO")

    Spec.it s "parses #elif" $ do
      Spec.assertEq s (parse "#elif 0") $ Just (Elif "0")

    Spec.it s "parses #else" $ do
      Spec.assertEq s (parse "#else") $ Just Else

    Spec.it s "parses #endif" $ do
      Spec.assertEq s (parse "#endif") $ Just Endif

    Spec.it s "parses #define without value" $ do
      Spec.assertEq s (parse "#define FOO") $ Just (Define "FOO" Nothing)

    Spec.it s "parses #define with value" $ do
      Spec.assertEq s (parse "#define FOO 42") $ Just (Define "FOO" (Just "42"))

    Spec.it s "parses #undef" $ do
      Spec.assertEq s (parse "#undef FOO") $ Just (Undef "FOO")

    Spec.it s "parses #include as Other" $ do
      Spec.assertEq s (parse "#include <stdio.h>") $ Just Other

    Spec.it s "parses #error as Other" $ do
      Spec.assertEq s (parse "#error msg") $ Just Other

    Spec.it s "handles leading whitespace" $ do
      Spec.assertEq s (parse "  #if 1") $ Just (If "1")

    Spec.it s "handles whitespace after hash" $ do
      Spec.assertEq s (parse "#  if 1") $ Just (If "1")

    Spec.it s "fails on non-directive line" $ do
      Spec.assertEq s (parse "not a directive") Nothing