packages feed

tilia-0.1.0.0: src/Tilia/Cpp/Directives.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

-- | Reading the preprocessor directives of a module, and blanking lines.
module Tilia.Cpp.Directives
  ( -- * Reading a module
    usesCpp,
    blankCpp,
    withoutRuledOut,
    withoutOpaque,
    CppError (..),
    Malformation (..),
    describeCppError,

    -- * Splitting
    Guard (..),
    Configurations (..),
    configurations,
    configurationsOn,
    Varied (..),
    untouched,
    leaves,
    branchLeaves,
    implied,
    settledBranch,
    correspondingBranches,
    ruledOutBranch,
    unconditionalErrors,
    linearLeaves,
    countLeaves,
    answeredLeaves,
    answeredLinearLeaves,
    resolved,

    -- * The directives themselves
    Directive (..),
    dSpan,
    readConditionals,
    scanConditionals,
    isDirective,
    GroupSpec (..),
    gsOwnLines,
    gsCount,
    nestedIn,
    allGroups,
    blanking,
    blankingFor,
    droppedFor,
    opaqueDirectives,
    macroLines,
  )
where

import Data.Char (isAlphaNum, isSpace)
import Data.List (sortOn, tails)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Maybe (fromMaybe, isJust, listToMaybe, maybeToList)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import GHC.LanguageExtensions.Type (Extension (..))
import Tilia.Cpp.Macros (Macros, guardHolds, impliedBy)
import Tilia.Doc.Internal (Conditional (..))
import Tilia.Parser (ParseError, describeParseError)
import Tilia.Source (directiveOnLine)
import Tilia.Span
  ( Span,
    mkSpan,
    spanEndLine,
    spanStartLine,
  )

----------------------------------------------------------------------------
-- Reading a module

-- | Is CPP enabled and there is at least one CPP directive present?
usesCpp :: [Extension] -> Text -> Bool
usesCpp extensions source =
  Cpp `elem` extensions && any isDirective (T.lines source)

-- | Blank out the directive lines, keeping every branch.
blankCpp :: Text -> Text
blankCpp source =
  blanking
    ([(dLine d, dLastLine d) | d <- directives source] <> [(n, n) | (n, _) <- macroLines source])
    source

-- | Blank out every branch the macros rule out, and the conditionals that
-- ask about them.
withoutRuledOut :: Macros -> Text -> Text
withoutRuledOut macros source = case scanConditionals source of
  Nothing -> source
  Just forest ->
    blanking
      [ range
      | gs <- allGroups forest,
        Just taken <- [branchTaken macros gs],
        range <- blankingFor gs taken
      ]
      source

-- | Which branch of a conditional the macros settle on.
branchTaken :: Macros -> GroupSpec -> Maybe Int
branchTaken macros = branchFor (guardHolds macros . guardText)

-- | Which branch of a conditional is taken, given which of its guards hold
-- where that is settled.
branchFor :: (Guard -> Maybe Bool) -> GroupSpec -> Maybe Int
branchFor holds = go 0 . gsGuards
  where
    go i = \case
      [] -> Just i
      g : rest -> case holds g of
        Just True -> Just i
        Just False -> go (i + 1) rest
        Nothing -> Nothing

-- | A module with the opaque directives and the lines using its macros
-- blanked out.
withoutOpaque :: Text -> Text
withoutOpaque source =
  blanking
    ([(dLine d, dLastLine d) | d <- opaqueDirectives source] <> [(n, n) | (n, _) <- macroLines source])
    source

-- | Why a module using the preprocessor could not be formatted.
data CppError
  = -- | A conditional that does not make sense: the line and the keyword of
    -- the directive that gives it away, and what is wrong with it.
    MalformedConditional Int Text Malformation
  | -- | More configurations than 'configurationBudget' allows.
    TooManyConfigurations
  | -- | A configuration the parser rejected, and which one it was.
    ConfigurationNotParsed [([Guard], Int)] ParseError
  | -- | A directive written inside a quasi-quote or a multi-line string, its
    -- line, and its keyword.
    DirectiveInQuotedText Int Text
  | -- | A line using a macro written inside a quasi-quote or a multi-line
    -- string, its line, and what is written on it.
    MacroInQuotedText Int Text
  | -- | A branch holding something that a conditional around it, asking the
    -- same question, rules out, and the line of the directive opening it.
    RuledOutBranch Int
  | -- | An alternative that aborts with @#error@ that cannot be formatted
    -- without parsing it, and the line of the @#error@.
    AbortingAlternative Int

-- | What is wrong with a conditional directive.
data Malformation
  = -- | It opens a conditional that nothing closes.
    NeverClosed
  | -- | It continues or closes a conditional where none is open.
    NothingOpen
  | -- | It continues a conditional after its @#else@.
    AfterElse

-- | Say what went wrong, in one line. The edge of the system.
describeCppError :: CppError -> Text
describeCppError = \case
  MalformedConditional n k m ->
    "the #" <> k <> " at line " <> T.pack (show n) <> case m of
      NeverClosed -> " is never closed"
      NothingOpen -> " has no conditional to belong to"
      AfterElse -> " comes after the #else of its conditional"
  TooManyConfigurations -> "too many configurations to format"
  ConfigurationNotParsed c e -> describeParseError e <> inConfiguration c
  DirectiveInQuotedText n k ->
    "the #" <> k <> " at line " <> T.pack (show n) <> " is inside a quasi-quote or a multi-line string"
  MacroInQuotedText n t ->
    t <> " at line " <> T.pack (show n) <> " is inside a quasi-quote or a multi-line string"
  RuledOutBranch n ->
    "the branch at line "
      <> T.pack (show n)
      <> " is ruled out by a conditional around it that asks the same question"
  AbortingAlternative n ->
    "the alternative that the #error at line "
      <> T.pack (show n)
      <> " aborts cannot be formatted without parsing it"

-- | Which configuration, in words. Empty where there is only one.
inConfiguration :: [([Guard], Int)] -> Text
inConfiguration [] = ""
inConfiguration answers =
  ", in the configuration taking " <> T.intercalate ", then " (fmap said answers)
  where
    said (guards, i) = case (drop i guards, guards) of
      (g : _, _) -> "#" <> guardText g
      ([], g : _) -> "no branch of #" <> guardText g
      ([], []) -> "no branch"

----------------------------------------------------------------------------
-- Splitting

-- | One conditional directive, as written after its hash.
newtype Guard = Guard{guardText :: Text}
  deriving (Eq, Ord, Show)

-- | What one conditional splits a module into.
data Configurations = Configurations
  { -- | The directives: the @#if@ of the group, and then one per @#elif@.
    cfgGuards :: [Guard],
    -- | One module text per branch, in the same order as the directives,
    -- and then one more for the @#else@.
    cfgTexts :: [Text],
    -- | What each of those branches leaves out, in the same order.
    cfgDropped :: [[(Int, Int)]],
    -- | From each tied group's @#if@ to its @#endif@, inclusive.
    cfgWholes :: Varied,
    -- | Each tied group as its author wrote it, in the same order.
    cfgConditionals :: [Conditional]
  }
  deriving (Eq, Show)

-- | Split a module on its first outermost conditional, and on every other
-- one written behind the same directives, wherever in the module it sits.
configurations :: Text -> Maybe Configurations
configurations source = do
  forest <- scanConditionals source
  gs <- listToMaybe forest
  pure (configurationsOn gs forest source)

-- | Split a module on one of its conditionals, and on every other one
-- written behind the same directives.
configurationsOn ::
  -- | The conditional.
  GroupSpec ->
  -- | The module's conditionals.
  [GroupSpec] ->
  -- | The module.
  Text ->
  Configurations
configurationsOn gs forest source =
  Configurations
    { cfgGuards = gsGuards gs,
      cfgTexts =
        [ blanking (concatMap (`blankingFor` i) tied) source
        | i <- [0 .. gsCount gs - 1]
        ],
      cfgDropped =
        [concatMap (`droppedFor` i) tied | i <- [0 .. gsCount gs - 1]],
      cfgWholes = Varied (fmap gsWhole tied),
      cfgConditionals = fmap gsConditional tied
    }
  where
    tied = sameGuard gs forest

-- | Every group in a module written behind the same directives as this one.
sameGuard :: GroupSpec -> [GroupSpec] -> [GroupSpec]
sameGuard gs forest = [g | g <- allGroups forest, gsGuards g == gsGuards gs]

-- | The lines one conditional could have printed differently.
newtype Varied = Varied{variedLines :: [(Int, Int)]}
  deriving (Eq, Show)

-- | Was this region printed from lines the conditional left alone?
untouched :: Varied -> Span -> Bool
untouched (Varied ranges) s = not (any reaches ranges)
  where
    reaches (from, to) = spanStartLine s <= to && from <= spanEndLine s

-- | Every configuration of a module, with every conditional resolved.
leaves :: Text -> Either CppError [Text]
leaves = fmap (fmap snd) . answeredLeaves

-- | One configuration for every branch of every conditional, and no more.
branchLeaves :: Text -> Either CppError [Text]
branchLeaves source = do
  forest <- readConditionals source
  traverse
    resolved
    ( filter
        (null . unconditionalErrors)
        ( distinct
            [ configurationOf forest a source
            | a <- branchAssignments forest,
              possible forest (completed forest a)
            ]
        )
    )
  where
    distinct = Map.elems . Map.fromList . fmap (\t -> (t, t))

-- | The empty assignment, and then one for every branch of every
-- conditional, which reaches the conditional and takes the branch.
branchAssignments :: [GroupSpec] -> [Assignment]
branchAssignments forest =
  Map.empty
    : [ Map.insert (gsGuards gs) i asked
      | (asked, gs) <- reachable Map.empty forest,
        i <- [0 .. gsCount gs - 1]
      ]
  where
    reachable asked gss =
      concat
        [ (asked, gs)
            : concat
              [ reachable (Map.insert (gsGuards gs) i asked) nested
              | (i, nested) <- zip [0 ..] (gsNested gs)
              ]
        | gs <- gss
        ]

-- | The configuration of a module an assignment selects, a conditional it
-- does not mention taking the branch the assignment settles it on, or else
-- its first.
configurationOf :: [GroupSpec] -> Assignment -> Text -> Text
configurationOf forest assignment =
  blanking
    [ r
    | gs <- allGroups forest,
      r <- blankingFor gs (Map.findWithDefault 0 (gsGuards gs) answers)
    ]
  where
    answers = completed forest assignment

-- | Answer every conditional a configuration reaches that an assignment
-- does not mention with the branch the answers settle it on, or else with
-- its first.
completed :: [GroupSpec] -> Assignment -> Assignment
completed forest assignment = foldl' visit assignment forest
  where
    visit asked gs =
      let i = fromMaybe settled (Map.lookup (gsGuards gs) asked)
          settled =
            fromMaybe 0 (implied (Map.toList asked) >>= (`settledBranch` gs))
       in foldl' visit (Map.insert (gsGuards gs) i asked) (nestedIn gs i)

-- | The answers an assignment gives the conditionals a configuration
-- reaches.
reachedBy :: [GroupSpec] -> Assignment -> [([Guard], Int)]
reachedBy forest assignment = concatMap reach forest
  where
    reach gs =
      let i = Map.findWithDefault 0 (gsGuards gs) assignment
       in (gsGuards gs, i) : concatMap reach (nestedIn gs i)

-- | Is there a definition of the macros that answers the conditionals a
-- configuration reaches as an assignment does?
possible :: [GroupSpec] -> Assignment -> Bool
possible forest = isJust . implied . reachedBy forest

-- | What answering these questions as given implies about the macros, or
-- 'Nothing' where no definition of the macros answers them so.
implied :: [([Guard], Int)] -> Maybe Macros
implied answers
  | and [settledGuard known g /= Just (not h) | (g, h) <- said] = Just known
  | otherwise = Nothing
  where
    said = concat [zip gs (replicate i False <> [True]) | (gs, i) <- answers]
    known = foldMap (\(g, h) -> impliedBy h (guardText g)) said

-- | Which branch of a conditional what is known about the macros settles it
-- on, where the conditional does not settle itself.
--
-- An @#if 0@ is how code is put aside, and is formatted as though it asked
-- something.
settledBranch :: Macros -> GroupSpec -> Maybe Int
settledBranch known = branchFor (settledGuard known)

-- | Does a guard hold, where what is known settles it and the guard does
-- not settle itself?
settledGuard :: Macros -> Guard -> Maybe Bool
settledGuard known (Guard written) = case guardHolds mempty written of
  Just _ -> Nothing
  Nothing -> guardHolds known written

-- | The line of the first directive whose branch holds something although
-- a conditional around it, asking the same question, rules that branch out.
ruledOutBranch :: Text -> Maybe Int
ruledOutBranch source = do
  forest <- scanConditionals source
  listToMaybe (go Map.empty forest)
  where
    written = zip [1 ..] (T.lines source)
    holdsSomething (from, to) =
      any (\(n, l) -> from <= n && n <= to && not (T.all isSpace l)) written
    go asked forest =
      concat
        [ [ opening
          | Just j <- [Map.lookup (gsGuards gs) asked],
            (i, opening, r) <- zip3 [0 :: Int ..] (gsOwnLines gs) (gsBranches gs),
            i /= j,
            holdsSomething r
          ]
            <> concat
              [ go (Map.insert (gsGuards gs) i asked) nested
              | (i, nested) <- zip [0 ..] (gsNested gs)
              ]
        | gs <- forest
        ]

-- | Read both spellings under the same CPP choices.
--
-- Sorting or deduplicating the resulting source text separately loses the
-- association with the guards: formatting can change that order or make two
-- formerly different strings identical. Cover every branch of either
-- spelling, including its ancestors. 'Nothing' denotes a configuration
-- deliberately rejected by @#error@.
correspondingBranches ::
  -- | Before.
  Text ->
  -- | After.
  Text ->
  Either CppError [(Maybe Text, Maybe Text)]
correspondingBranches before after = do
  left <- readConditionals before
  right <- readConditionals after
  let forest = left <> right
      choices =
        Map.keys . Map.fromList $
          [ (answers, ())
          | a <- branchAssignments left <> branchAssignments right,
            let answers = completed forest a,
            possible forest answers
          ]
  traverse (\a -> (,) <$> reading before left a <*> reading after right a) choices
  where
    reading source forest assignment =
      let selected = configurationOf forest assignment source
       in if null (unconditionalErrors selected)
            then Just <$> resolved selected
            else Right Nothing

-- | An unconditional @#error@ means this configuration has no Haskell
-- program to parse. Conditional errors are only considered after choosing a
-- branch.
unconditionalErrors :: Text -> [Directive]
unconditionalErrors source =
  [ d
  | d <- opaqueDirectives source,
    dKeyword d == "error",
    not (any (encloses (dLine d)) groups)
  ]
  where
    groups = [gsWhole g | forest <- maybeToList (scanConditionals source), g <- allGroups forest]
    encloses n (from, to) = from < n && n < to

-- | The configurations reached by varying one conditional at a time.
linearLeaves :: Text -> Either CppError [Text]
linearLeaves = fmap (fmap snd) . answeredLinearLeaves

-- | How many configurations a module has, without building any of them,
-- and so counting those no definition of the macros gives.
countLeaves :: Text -> Either CppError Integer
countLeaves source = do
  forest <- readConditionals source
  pure (sum [across answers forest | answers <- combinations (afforded forest)])
  where
    across answers = product . fmap (one answers)
    one answers gs = case lookup (gsGuards gs) answers of
      Just i -> across answers (nestedIn gs i)
      Nothing -> sum [across answers (nestedIn gs i) | i <- [0 .. gsCount gs - 1]]
    combinations = traverse (\(g, k) -> [(g, i) | i <- [0 .. k - 1]])
    afforded forest = go 1 (repeated forest)
      where
        go _ [] = []
        go n ((g, k) : rest)
          | n * toInteger k <= guardsToTie = (g, k) : go (n * toInteger k) rest
          | otherwise = []

-- | The guards a module asks more than once, and how many answers each has.
repeated :: [GroupSpec] -> [([Guard], Int)]
repeated forest = distinct Map.empty [q | q@(g, _) <- asked forest, twice g]
  where
    asked gss = concat [(gsGuards gs, gsCount gs) : asked (concat (gsNested gs)) | gs <- gss]
    times = Map.fromListWith (+) [(g, 1 :: Int) | (g, _) <- asked forest]
    twice g = Map.findWithDefault 0 g times >= 2

    distinct _ [] = []
    distinct seen (q@(g, _) : rest)
      | Map.member g seen = distinct seen rest
      | otherwise = q : distinct (Map.insert g () seen) rest

-- | How many combinations of answers 'countLeaves' will enumerate.
guardsToTie :: Integer
guardsToTie = 4096

-- | Every configuration some definition of the macros gives, and the
-- answers that reach it.
answeredLeaves :: Text -> Either CppError [(Assignment, Text)]
answeredLeaves =
  fmap (filter (isJust . implied . Map.toList . fst)) . go Map.empty
  where
    go answers source = case configurations source of
      Nothing | not (null (unconditionalErrors source)) -> Right []
      Nothing -> (\t -> [(answers, t)]) <$> resolved source
      Just c ->
        concat
          <$> traverse
            (\(i, t) -> go (Map.insert (cfgGuards c) i answers) t)
            (zip [0 ..] (cfgTexts c))

-- | Which branch every question was answered with to reach a configuration.
type Assignment = Map [Guard] Int

-- | The same, labelled by the answers that reach each one, and for the same
-- reason as 'answeredLeaves'.
answeredLinearLeaves :: Text -> Either CppError [(Assignment, Text)]
answeredLinearLeaves =
  fmap (filter (isJust . implied . Map.toList . fst)) . go Map.empty
  where
    go answers source = case configurations source of
      Nothing -> (\t -> [(answers, t)]) <$> resolved source
      Just c -> case zip [0 ..] (cfgTexts c) of
        [] -> Right []
        (i, first) : rest ->
          (<>)
            <$> go (Map.insert (cfgGuards c) i answers) first
            <*> traverse (held answers (cfgGuards c)) rest
      where
        held before gs (i, t) = answeredBaseline (Map.insert gs i before) t

-- | The configuration in which every question still to be asked takes its
-- first branch, and the answers that gives.
answeredBaseline :: Assignment -> Text -> Either CppError (Assignment, Text)
answeredBaseline answers source = case configurations source of
  Nothing -> (answers,) <$> resolved source
  Just c -> case cfgTexts c of
    [] -> Right (answers, source)
    t : _ -> answeredBaseline (Map.insert (cfgGuards c) 0 answers) t

-- | A module with every conditional answered, and its other directives
-- taken out, or why its conditionals do not make sense.
resolved :: Text -> Either CppError Text
resolved source = withoutOpaque source <$ readConditionals source

----------------------------------------------------------------------------
-- The directives themselves

-- | One preprocessor directive, as written.
data Directive = Directive
  { dLine :: !Int,
    -- | The last line the directive is written on, which is its first unless
    -- a line of it ends in a backslash.
    dLastLine :: !Int,
    dKeyword :: !Text,
    -- | What follows its hash, on every line it is written on.
    dText :: !Text
  }
  deriving (Eq, Show)

-- | The lines a directive was written on, as a span.
dSpan :: Directive -> Span
dSpan d = mkSpan (dLine d, 1) (dLastLine d, 1)

-- | Every directive in a module, in the order they are written.
directives :: Text -> [Directive]
directives = go . zip [1 ..] . T.lines
  where
    go = \case
      [] -> []
      (n, l) : ls
        | Just (keyword, body) <- directiveOnLine l,
          keyword `elem` directiveKeywords ->
            let (continued, rest) = continuation l ls
             in Directive
                  { dLine = n,
                    dLastLine = n + length continued,
                    dKeyword = keyword,
                    dText = T.intercalate "\n" (fmap T.stripEnd (body : continued))
                  }
                  : go rest
        | otherwise -> go ls
    continuation l ls
      | T.isSuffixOf "\\" (T.stripEnd l),
        (_, next) : rest <- ls =
          let (more, rest') = continuation next rest in (next : more, rest')
      | otherwise = ([], ls)

-- | A module's conditionals, each with the ones inside its branches, or why
-- they do not make sense.
readConditionals :: Text -> Either CppError [GroupSpec]
readConditionals = go [] [] . filter ((`elem` conditionalKeywords) . dKeyword) . directives
  where
    -- The groups finished where the reading is, last first, and the
    -- conditionals open around it, innermost first.
    go done open = \case
      [] -> case open of
        [] -> Right (reverse done)
        o : _ -> Left (MalformedConditional (dLine (oOpener o)) (dKeyword (oOpener o)) NeverClosed)
      d : ds
        | dKeyword d `elem` opensGroup -> go [] (Open d [] [] done : open) ds
        | dKeyword d `elem` continuesGroup -> case open of
            [] -> malformed NothingOpen
            o : rest
              | any ((== "else") . dKeyword) (take 1 (oLater o)) -> malformed AfterElse
              | otherwise ->
                  go [] (o{oLater = d : oLater o, oEarlier = reverse done : oEarlier o} : rest) ds
        | otherwise -> case open of
            [] -> malformed NothingOpen
            o : rest ->
              let gs =
                    groupSpec
                      (oOpener o)
                      (reverse (oLater o))
                      d
                      (reverse (reverse done : oEarlier o))
               in go (gs : oBefore o) rest ds
        where
          malformed = Left . MalformedConditional (dLine d) (dKeyword d)

-- | A conditional still open as the directives are read.
data Open = Open
  { -- | The directive that opened it.
    oOpener :: Directive,
    -- | The directives that continued it, last first.
    oLater :: [Directive],
    -- | The groups inside each of its branches read so far, last first.
    oEarlier :: [[GroupSpec]],
    -- | The groups before it where it is, last first.
    oBefore :: [GroupSpec]
  }

-- | A module's conditionals, or 'Nothing' if they do not make sense.
scanConditionals :: Text -> Maybe [GroupSpec]
scanConditionals = either (const Nothing) Just . readConditionals

-- | The directives that ask a question, and so split a module in two.
conditionalKeywords :: [Text]
conditionalKeywords = opensGroup <> continuesGroup <> ["endif"]

-- | The keywords that open a group or continue one.
opensGroup, continuesGroup :: [Text]
opensGroup = ["if", "ifdef", "ifndef"]
continuesGroup = ["elif", "elifdef", "elifndef", "else"]

-- | Does this line begin with a preprocessor directive?
isDirective :: Text -> Bool
isDirective l = case directiveOnLine l of
  Just (keyword, _) -> keyword `elem` directiveKeywords
  Nothing -> False

-- | Every directive the C preprocessor takes, whether or not this module
-- can do anything with the ones it names.
directiveKeywords :: [Text]
directiveKeywords = conditionalKeywords <> opaqueKeywords

-- | The unconditional directives that do not split the source code.
opaqueKeywords :: [Text]
opaqueKeywords =
  ["define", "undef", "include", "line", "error", "warning", "pragma"]

-- | One conditional group, read off the directives that make it up.
data GroupSpec = GroupSpec
  { gsGuards :: [Guard],
    gsHasElse :: Bool,
    gsConditional :: Conditional,
    gsOwnRanges :: [(Int, Int)],
    gsBranches :: [(Int, Int)],
    gsWhole :: (Int, Int),
    -- | The groups inside each of its branches.
    gsNested :: [[GroupSpec]]
  }

-- | Read a group off its directives.
groupSpec ::
  -- | The directive opening it.
  Directive ->
  -- | The ones continuing it.
  [Directive] ->
  -- | The one closing it.
  Directive ->
  -- | The groups inside each of its branches.
  [[GroupSpec]] ->
  GroupSpec
groupSpec opener later end nested =
  GroupSpec
    { gsGuards = [Guard (dText d) | d <- opener : later, dKeyword d /= "else"],
      gsHasElse = any ((== "else") . dKeyword) later,
      gsConditional =
        Conditional
          { conditionalLines = fmap dLine group,
            conditionalElse = foldMap afterKeyword (filter ((== "else") . dKeyword) later),
            conditionalEndif = afterKeyword end
          },
      gsOwnRanges = [(dLine d, dLastLine d) | d <- group],
      gsBranches = [(dLastLine a + 1, dLine b - 1) | (a, b) <- zip group (drop 1 group)],
      gsWhole = (dLine opener, dLastLine end),
      gsNested = nested
    }
  where
    group = opener : later <> [end]
    afterKeyword d = T.drop (T.length (dKeyword d)) (dText d)

-- | The lines of a group's directives, the @#if@ first and the @#endif@ last.
gsOwnLines :: GroupSpec -> [Int]
gsOwnLines = conditionalLines . gsConditional

-- | How many configurations a group has: one per condition, and one more
-- for when none of them holds.
gsCount :: GroupSpec -> Int
gsCount gs = length (gsGuards gs) + 1

-- | The groups inside the branch configuration @i@ of a group takes.
nestedIn :: GroupSpec -> Int -> [GroupSpec]
nestedIn gs i = concat (take 1 (drop i (gsNested gs)))

-- | Every conditional in a module, outermost first.
allGroups :: [GroupSpec] -> [GroupSpec]
allGroups = concat . takeWhile (not . null) . iterate (concatMap (concat . gsNested))

-- | Replace the given line ranges with empty lines, keeping every other line
-- where it was.
blanking :: [(Int, Int)] -> Text -> Text
blanking ranges = T.unlines . go (sortOn fst ranges) . zip [1 ..] . T.lines
  where
    go rs = \case
      [] -> []
      (n, l) : ls -> case dropWhile ((< n) . snd) rs of
        rs'@((from, _) : _) | from <= n -> "" : go rs' ls
        rs' -> l : go rs' ls

-- | The lines to blank so that configuration @i@ of a group is what is left.
--
-- The branches this configuration does not take, and the directives
-- themselves: a directive belongs to no configuration, which is the whole of
-- what separates this from 'droppedFor'.
blankingFor :: GroupSpec -> Int -> [(Int, Int)]
blankingFor gs i = droppedFor gs i <> gsOwnRanges gs

-- | The lines a configuration of a group is not including.
droppedFor :: GroupSpec -> Int -> [(Int, Int)]
droppedFor gs i
  | i < length (gsGuards gs) || gsHasElse gs =
      [r | (k, r) <- zip [0 :: Int ..] (gsBranches gs), k /= i]
  | otherwise = gsBranches gs

-- | The directives that do not introduce configurations.
opaqueDirectives :: Text -> [Directive]
opaqueDirectives = filter ((`elem` opaqueKeywords) . dKeyword) . directives

-- | The lines that hold nothing but a use of a function-like macro the
-- module defines, each with what is written on it.
--
-- Only a use written with its parenthesis right after the name counts, since
-- the printer never writes one so, and what formatting produces must not
-- become such a line. A use indented other than the code under it, or than
-- the margin at the end of the module, carries on the code above or is
-- carried on by the code under it, which a line put back at the level of
-- that code cannot, unless that code closes a bracket, as the printer
-- closes a record under its fields.
macroLines :: Text -> [(Int, Text)]
macroLines source =
  [ (n, T.strip l)
  | (n, l) : below <- tails numbered,
    not (inDirective n),
    uses l,
    case take 1 (filter code below) of
      (_, l') : _ -> indentation l == indentation l' || closing l'
      [] -> indentation l == 0
  ]
  where
    numbered = zip [1 ..] (T.lines source)
    written = directives source
    inDirective n = any (\d -> dLine d <= n && n <= dLastLine d) written
    code (n, l) = not (T.null (T.strip l) || inDirective n || uses l)
    indentation = T.length . T.takeWhile isSpace
    closing = maybe False ((`elem` ("})]" :: String)) . fst) . T.uncons . T.stripStart
    uses l = Set.member name defined && enclosed arguments
      where
        (name, arguments) = T.span isNameChar (T.strip l)
    defined =
      Set.fromList
        [ name
        | d <- written,
          dKeyword d == "define",
          let (name, rest) = T.span isNameChar (T.stripStart (T.drop (T.length "define") (dText d))),
          T.isPrefixOf "(" rest
        ]
    -- One parenthesized list of arguments, and nothing after it.
    enclosed arguments =
      T.isPrefixOf "(" arguments
        && fmap snd (T.unsnoc arguments) == Just ')'
        && all (> 0) (init depths)
        && last depths == 0
      where
        depths = drop 1 (scanl deeper 0 (T.unpack arguments))
        deeper :: Int -> Char -> Int
        deeper k = \case
          '(' -> k + 1
          ')' -> k - 1
          _ -> k
    isNameChar c = isAlphaNum c || c == '_'