packages feed

scripths-0.3.0.1: src/ScriptHs/Render.hs

{- | Rendering 'ScriptHs.Parser.Line' sequences into GHCi-compatible scripts.

GHCi imposes constraints that raw Haskell source does not: multi-line
definitions must be wrapped in @:{@ \/ @:}@ blocks, and monadic binds
(@<-@) or Template Haskell splices (@$(@) must be issued as individual
statements rather than grouped with pure definitions. 'toGhciScript'
handles all of this automatically
-}
module ScriptHs.Render (
    toGhciScript,
) where

import Data.Text (Text)
import qualified Data.Text as T
import ScriptHs.Parser (Line (..))

data Block
    = SingleLine Line
    | MultiLine [Line]
    deriving (Show, Eq)

{- | Render a list of 'Line's as a GHCi script.

Lines are grouped into blocks and wrapped in @:{@ \/ @:}@ where necessary.
Monadic bind expressions (@<-@) and Template Haskell splices (@$(@) are
always emitted as individual GHCi statements, since GHCi does not allow
them inside multi-line blocks.

Example — a multi-line definition is wrapped in a single block:

@
toGhciScript
  [ HaskellLine "double :: Int -> Int"
  , HaskellLine "double = (*2)"
  ]
-- :{
-- double :: Int -> Int
-- double = (*2)
-- :}
@

Example — an IO bind is kept as a standalone statement:

@
toGhciScript
  [ HaskellLine "x <- getLine"
  , HaskellLine "putStrLn x"
  ]
-- x <- getLine
-- putStrLn x
@
-}
toGhciScript :: [Line] -> Text
toGhciScript = T.unlines . concatMap renderBlock . groupBlocks

groupBlocks :: [Line] -> [Block]
groupBlocks = concatMap splitIOBinds . groupRaw

groupRaw :: [Line] -> [Block]
groupRaw [] = []
groupRaw (Blank : rest) = SingleLine Blank : groupRaw rest
groupRaw (GhciCommand t : rest) = SingleLine (GhciCommand t) : groupRaw rest
groupRaw ls =
    let (block, rest) = span isBlockLine ls
        (block', rest') = takeIfIndented block rest
     in classifyBlock block' : groupRaw rest'

takeIfIndented :: [Line] -> [Line] -> ([Line], [Line])
takeIfIndented block rest = (block ++ takeWhile isIndented rest, dropWhile isIndented rest)

isIndented :: Line -> Bool
isIndented Blank = True
isIndented (HaskellLine t) = T.isPrefixOf " " t || T.isPrefixOf "\t" t
isIndented _ = False

splitIOBinds :: Block -> [Block]
splitIOBinds (MultiLine ls) =
    concatMap (splitDefIO . classifyBlock) (splitOn isIOLine ls)
  where
    isIOLine l = isIOorTH (lineText l)
splitIOBinds b = [b]

{- | Split a block that mixes definitions and IO actions.
  Only splits when there's a clear boundary: a non-indented line
  without @=@ or @::@ following a line that has @=@ or @::@, or vice versa.
  Indented lines always attach to the preceding line's group.
-}
splitDefIO :: Block -> [Block]
splitDefIO (MultiLine ls)
    | hasMix ls = map classifyBlock (groupByKind ls)
    | otherwise = [MultiLine ls]
splitDefIO b = [b]

-- | Check if a block has both definitions and IO actions at the top level.
hasMix :: [Line] -> Bool
hasMix ls =
    let topLevel = filter (not . isIndented) ls
        defs = filter (isDef . lineText) topLevel
        actions = filter (not . isDef . lineText) topLevel
     in not (null defs) && not (null actions)

groupByKind :: [Line] -> [[Line]]
groupByKind [] = []
groupByKind (l : ls) =
    let kind = lineKindOf l
        -- Collect lines of the same kind, plus any indented continuations
        (same, rest) = spanSameKind kind ls
     in (l : same) : groupByKind rest

spanSameKind :: Bool -> [Line] -> ([Line], [Line])
spanSameKind _ [] = ([], [])
spanSameKind kind (l : ls)
    | isIndented l =
        -- Indented lines attach to the current group
        let (more, rest) = spanSameKind kind ls
         in (l : more, rest)
    | lineKindOf l == kind =
        let (more, rest) = spanSameKind kind ls
         in (l : more, rest)
    | otherwise = ([], l : ls)

-- | True if the line looks like a definition (has @=@ or @::@).
lineKindOf :: Line -> Bool
lineKindOf l = isDef (lineText l)

isDef :: Text -> Bool
isDef t =
    hasTopLevelEquals t || " :: " `T.isInfixOf` t

hasTopLevelEquals :: Text -> Bool
hasTopLevelEquals t =
    " = " `T.isInfixOf` t || T.isSuffixOf " =" t

splitOn :: (a -> Bool) -> [a] -> [[a]]
splitOn _ [] = []
splitOn p (x : xs)
    | p x = [x] : splitOn p xs
    | otherwise =
        let (run, rest) = break p xs
         in (x : run) : splitOn p rest

isBlockLine :: Line -> Bool
isBlockLine Blank = False
isBlockLine (GhciCommand _) = False
isBlockLine _ = True

classifyBlock :: [Line] -> Block
classifyBlock [l] = SingleLine l
classifyBlock ls = MultiLine ls

renderBlock :: Block -> [Text]
renderBlock (SingleLine Blank) = [""]
renderBlock (SingleLine (GhciCommand t)) = [t]
renderBlock (SingleLine (Pragma t)) = [t]
renderBlock (SingleLine (Import t)) = [t]
renderBlock (SingleLine (HaskellLine t)) = wrapMulti [t]
renderBlock (MultiLine ls)
    | allIOorTH ls = concatMap (\l -> wrapMulti [lineText l]) ls
    | otherwise = wrapMulti (map lineText ls)

wrapMulti :: [Text] -> [Text]
wrapMulti ls = [":{"] ++ ls ++ [":}"]

lineText :: Line -> Text
lineText Blank = ""
lineText (GhciCommand t) = t
lineText (Pragma t) = t
lineText (Import t) = t
lineText (HaskellLine t) = t

isIOorTH :: Text -> Bool
isIOorTH t =
    not (T.isPrefixOf " " t)
        && ( T.isInfixOf "<-" t
                || T.isInfixOf "$(" t
                || T.isPrefixOf "_ = ();" (T.stripStart t)
           )

allIOorTH :: [Line] -> Bool
allIOorTH = all (isIOorTH . lineText)