scrod-0.2026.2.21: source/library/Scrod/Cpp.hs
{-# LANGUAGE TemplateHaskellQuotes #-}
-- | A minimal C preprocessor for Haskell source.
--
-- Processes @#if@\/@#ifdef@\/@#ifndef@\/@#elif@\/@#else@\/@#endif@, @#define@,
-- and @#undef@. Directive lines are replaced with blank lines to preserve
-- line numbering. Unrecognized directives (e.g., @#include@) are silently
-- blanked out.
module Scrod.Cpp where
import qualified Data.Map.Strict as Map
import qualified Data.Maybe as Maybe
import qualified Scrod.Cpp.Directive as Directive
import qualified Scrod.Cpp.Expr as Expr
import qualified Scrod.Spec as Spec
-- | State of a branch in a @#if@\/@#elif@\/@#else@ chain.
--
-- * 'Active' — this branch's condition was true; output its lines.
-- * 'Inactive' — condition was false; may still activate on a later @#elif@\/@#else@.
-- * 'Done' — a previous branch in this chain already activated; skip the rest.
data Branch
= Active
| Inactive
| Done
deriving (Eq, Ord, Show)
data State = MkState
{ defines :: Map.Map String String,
stack :: [Branch],
seenElse :: [Bool]
}
initialState :: State
initialState =
MkState
{ defines = Map.empty,
stack = [],
seenElse = []
}
cpp :: String -> Either String String
cpp input = do
let ls = lines input
outputLines <- go ls initialState []
Right $ unlines (reverse outputLines)
go :: [String] -> State -> [String] -> Either String [String]
go ls state acc = case ls of
[] ->
if null (stack state)
then Right acc
else Left "unterminated #if"
line : rest ->
case Directive.parse line of
Nothing -> go rest state (keepOrBlank state line : acc)
Just dir -> do
state' <- processDirective state dir
go rest state' ("" : acc)
isActive :: State -> Bool
isActive = isActiveStack . stack
isActiveStack :: [Branch] -> Bool
isActiveStack bs = case bs of
[] -> True
Active : _ -> True
_ -> False
keepOrBlank :: State -> String -> String
keepOrBlank state line =
if isActive state then line else ""
processDirective :: State -> Directive.Directive -> Either String State
processDirective state dir = case dir of
Directive.If expr -> do
if isActive state
then do
val <- Expr.evaluate (defines state) expr
let branch = if val /= 0 then Active else Inactive
Right state {stack = branch : stack state, seenElse = False : seenElse state}
else
Right state {stack = Inactive : stack state, seenElse = False : seenElse state}
Directive.Ifdef name ->
processDirective state (Directive.If $ "defined(" <> name <> ")")
Directive.Ifndef name ->
processDirective state (Directive.If $ "!defined(" <> name <> ")")
Directive.Elif expr -> case (stack state, seenElse state) of
([], _) -> Left "unexpected #elif without matching #if"
(_, True : _) -> Left "unexpected #elif after #else"
(Active : rest, e : es) ->
Right state {stack = Done : rest, seenElse = e : es}
(Inactive : rest, e : es) ->
if isActiveStack rest
then do
val <- Expr.evaluate (defines state) expr
let branch = if val /= 0 then Active else Inactive
Right state {stack = branch : rest, seenElse = e : es}
else Right state {stack = Inactive : rest, seenElse = e : es}
(Done : _, _ : _) ->
Right state
(_, []) -> Left "unexpected #elif without matching #if"
Directive.Else -> case (stack state, seenElse state) of
([], _) -> Left "unexpected #else without matching #if"
(_, True : _) -> Left "unexpected #else after #else"
(Active : rest, _ : es) ->
Right state {stack = Done : rest, seenElse = True : es}
(Inactive : rest, _ : es) ->
if isActiveStack rest
then Right state {stack = Active : rest, seenElse = True : es}
else Right state {stack = Inactive : rest, seenElse = True : es}
(Done : _, _ : es) ->
Right state {stack = stack state, seenElse = True : es}
(_, []) -> Left "unexpected #else without matching #if"
Directive.Endif -> case (stack state, seenElse state) of
([], _) -> Left "unexpected #endif without matching #if"
(_ : rest, _ : es) -> Right state {stack = rest, seenElse = es}
(_, []) -> Left "unexpected #endif without matching #if"
Directive.Define name mValue ->
if isActive state
then
let v = Maybe.fromMaybe "1" mValue
in Right state {defines = Map.insert name v (defines state)}
else Right state
Directive.Undef name ->
if isActive state
then Right state {defines = Map.delete name (defines state)}
else Right state
Directive.Other -> Right state
spec :: (Applicative m, Monad n) => Spec.Spec m n -> n ()
spec s = do
Spec.named s 'cpp $ do
Spec.it s "passes through source without directives" $ do
Spec.assertEq s (cpp "module M where") $ Right "module M where\n"
Spec.it s "replaces directive lines with blank lines" $ do
Spec.assertEq s (cpp "#define FOO\nmodule M where") $ Right "\nmodule M where\n"
Spec.it s "keeps active ifdef branch" $ do
Spec.assertEq s (cpp "#define FOO\n#ifdef FOO\nkeep\n#endif") $ Right "\n\nkeep\n\n"
Spec.it s "removes inactive ifdef branch" $ do
Spec.assertEq s (cpp "#ifdef FOO\nskip\n#endif") $ Right "\n\n\n"
Spec.it s "handles ifndef" $ do
Spec.assertEq s (cpp "#ifndef FOO\nkeep\n#endif") $ Right "\nkeep\n\n"
Spec.it s "handles else branch" $ do
Spec.assertEq s (cpp "#ifdef FOO\nskip\n#else\nkeep\n#endif") $ Right "\n\n\nkeep\n\n"
Spec.it s "handles elif" $ do
Spec.assertEq s (cpp "#if 0\nskip\n#elif 1\nkeep\n#endif") $ Right "\n\n\nkeep\n\n"
Spec.it s "handles nested if" $ do
Spec.assertEq s (cpp "#if 1\n#if 1\nkeep\n#endif\n#endif") $ Right "\n\nkeep\n\n\n"
Spec.it s "handles nested if in inactive branch" $ do
Spec.assertEq s (cpp "#if 0\n#if 1\nskip\n#endif\n#endif") $ Right "\n\n\n\n\n"
Spec.it s "handles undef" $ do
Spec.assertEq s (cpp "#define FOO\n#undef FOO\n#ifdef FOO\nskip\n#endif") $ Right "\n\n\n\n\n"
Spec.it s "fails with unmatched endif" $ do
Spec.assertEq s (cpp "#endif") $ Left "unexpected #endif without matching #if"
Spec.it s "fails with unterminated if" $ do
Spec.assertEq s (cpp "#if 1\ncode") $ Left "unterminated #if"
Spec.it s "fails with else after else" $ do
Spec.assertEq s (cpp "#if 1\n#else\n#else\n#endif") $ Left "unexpected #else after #else"
Spec.it s "fails with elif after else" $ do
Spec.assertEq s (cpp "#if 0\n#else\n#elif 1\n#endif") $ Left "unexpected #elif after #else"
Spec.it s "handles else after elif" $ do
Spec.assertEq s (cpp "#if 1\nkeep\n#elif 0\nskip\n#else\nskip\n#endif") $ Right "\nkeep\n\n\n\n\n\n"
Spec.it s "preserves line count" $ do
Spec.assertEq s (length . lines <$> cpp "#if 1\nline2\nline3\n#endif\nline5") $ Right 5
Spec.it s "handles typical Haskell CPP pattern" $ do
Spec.assertEq
s
( cpp $
unlines
[ "#ifdef __GLASGOW_HASKELL__",
"import GHC.Specific",
"#else",
"import Data.Old",
"#endif"
]
)
( Right $
unlines
[ "",
"",
"",
"import Data.Old",
""
]
)
Spec.it s "handles define then ifdef" $ do
Spec.assertEq
s
( cpp $
unlines
[ "#define MY_FLAG",
"#ifdef MY_FLAG",
"import My.Module",
"#endif"
]
)
( Right $
unlines
[ "",
"",
"import My.Module",
""
]
)
Spec.it s "handles define with value in if" $ do
Spec.assertEq s (cpp "#define VER 10\n#if VER >= 10\nkeep\n#endif") $ Right "\n\nkeep\n\n"
Spec.it s "replaces other directives with blank lines" $ do
Spec.assertEq s (cpp "#include <stdio.h>\nmodule M where") $ Right "\nmodule M where\n"