{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleInstances #-}
{-# OPTIONS_GHC -fno-warn-missing-fields #-}
module Text.Heredoc ( heredoc
, heredocFile
) where
import Control.Applicative ((<$>), (<*>))
import Control.Arrow ((***))
import Data.Function (on)
import Data.List (intercalate)
import Data.Monoid ((<>))
import Text.ParserCombinators.Parsec hiding (Line)
import Text.ParserCombinators.Parsec.Error
import Language.Haskell.TH
import Language.Haskell.TH.Quote
heredoc :: QuasiQuoter
heredoc = QuasiQuoter { quoteExp = heredocFromString }
heredocFile :: FilePath -> Q Exp
heredocFile fp = do
content <- runIO $ readFile fp
heredocFromString content
heredocFromString :: String -> Q Exp
heredocFromString
= either err (concatToQExp . arrange) . parse doc "heredoc"
where
err = infixE <$> Just . pos <*> pure (varE '(<>)) <*> Just . msg
pos = litE <$> (stringL <$> show . errorPos)
msg = litE <$> (stringL <$> concatMap messageString . errorMessages)
type Indent = Int
type Line' = (Indent, Line)
type ChildBlock = [Line']
type AltFlag = Bool
data InLine = Raw String
| Quoted [Expr]
deriving Show
data Line = CtrlForall [Expr] [Expr] ChildBlock
| CtrlMaybe AltFlag [Expr] [Expr] ChildBlock ChildBlock
| CtrlNothing
| CtrlIf AltFlag [Expr] ChildBlock ChildBlock
| CtrlElse
| CtrlCase [Expr] [([Expr], ChildBlock)]
| CtrlOf [Expr]
| CtrlLet [Expr] [Expr] ChildBlock
| Normal [InLine]
deriving Show
data Expr = S String
| I Integer
| W
| A String Expr
| V String
| V' String
| C String
| O String
| O' String
| E [Expr]
| T [[Expr]]
| L [[Expr]]
| N
deriving Show
eol :: Parser String
eol = try (string "\n\r")
<|> try (string "\r\n")
<|> string "\n"
<|> string "\r"
<?> fail "end of line"
spaceTabs :: Parser String
spaceTabs = many (oneOf " \t")
doc :: Parser [(Indent, Line)]
doc = line `sepBy` eol
line :: Parser (Indent, Line)
line = (,) <$> indent <*> contents
indent :: Parser Indent
indent = fmap sum $
many ((char ' ' >> pure 1) <|>
(char '\t' >> fail "Tabs are not allowed in indentation"))
contents :: Parser Line
contents = try ctrlForall <|>
try ctrlMaybe <|>
try ctrlNothing <|>
try ctrlIf <|>
try ctrlElse <|>
try ctrlCase <|>
try ctrlOf <|>
try ctrlLet <|>
normal
ctrlForall :: Parser Line
ctrlForall = CtrlForall <$> bindVal <*> expr <*> pure []
where
bindVal = string "$forall" *> spaceTabs *>
binding
<* spaceTabs <* string "<-" <* spaceTabs
ctrlMaybe :: Parser Line
ctrlMaybe = CtrlMaybe <$> pure False <*> bindVal <*> expr <*> pure [] <*> pure []
where
bindVal = string "$maybe" *> spaceTabs *>
binding
<* spaceTabs <* string "<-" <* spaceTabs
ctrlNothing :: Parser Line
ctrlNothing = string "$nothing" *> spaceTabs >> pure CtrlNothing
ctrlIf :: Parser Line
ctrlIf = CtrlIf <$> pure False <*> (string "$if" *> spaceTabs *> expr <* spaceTabs) <*> pure [] <*> pure []
ctrlElse :: Parser Line
ctrlElse = string "$else" *> spaceTabs >> pure CtrlElse
ctrlCase :: Parser Line
ctrlCase = CtrlCase <$> (string "$case" *> spaceTabs *> expr <* spaceTabs) <*> pure []
ctrlOf :: Parser Line
ctrlOf = CtrlOf <$> bindVal
where
bindVal = string "$of" *> spaceTabs *>
binding
<* spaceTabs
ctrlLet :: Parser Line
ctrlLet = CtrlLet <$> bindVal <*> expr <*> pure []
where
bindVal = string "$let" *> spaceTabs *>
binding
<* spaceTabs <* string "=" <* spaceTabs
binding :: Parser [Expr]
binding = spaceTabs *> many1 (try (A <$> var <* char '@' <*> term) <|>
term)
where
term :: Parser Expr
term = (T <$> tuple <|>
(try (nil >> pure N) <|>
try (L <$> list) <|>
try (O <$> string ":")) <|>
(try (V <$> ((<>) <$> wild <*> many1 (alphaNum <|> oneOf "_'"))) <|>
try (wild >> pure W) <|>
V <$> var) <|>
C <$> con) <* spaceTabs
expr :: Parser [Expr]
expr = spaceTabs *> many1 (try (A <$> var <* char '@' <*> term) <|>
term)
where
term :: Parser Expr
term = (S <$> str <|>
T <$> tuple <|>
(try (nil >> pure N) <|>
try (L <$> list) <|>
try (O <$> op)) <|>
(try (O' <$> op') <|> try (E <$> subexp)) <|>
V' <$> var' <|>
(try (V <$> ((<>) <$> wild <*> many1 (alphaNum <|> oneOf "_'"))) <|>
try (wild >> pure W) <|>
V <$> var) <|>
C <$> con <|>
I <$> integer) <* spaceTabs
tuple :: Parser [[Expr]]
tuple = char '(' *> sepBy expr (char ',') <* char ')'
list :: Parser [[Expr]]
list = char '[' *> sepBy expr (char ',') <* char ']'
integer :: Parser Integer
integer = read <$> many1 digit
str :: Parser String
str = char '"' *> many quotedChar <* char '"'
where
quotedChar :: Parser Char
quotedChar = noneOf "\\\"" <|> try (string "\\\"" >> pure '"')
subexp :: Parser [Expr]
subexp = char '(' *> expr <* char ')'
var :: Parser String
var = try ((+.+) <$> modul <*> v) <|> v
where
x +.+ y = x <> "." <> y
v :: Parser String
v = (:) <$> lower <*> many (alphaNum <|> oneOf "_'")
modul :: Parser String
modul = try (intercalate "." <$> many1 (mod' <* char '.')) <|> mod'
where
mod' :: Parser String
mod' = (:) <$> upper <*> many alphaNum
var' :: Parser String
var' = char '`' *> var <* char '`'
wild :: Parser String
wild = string "_"
nil :: Parser String
nil = string "[]"
con :: Parser String
con = (:) <$> upper <*> many (alphaNum <|> oneOf "_'")
op :: Parser String
op = many1 (oneOf ":!#$%&*+./<=>?@\\^|-~")
op' :: Parser String
op' = char '(' *> op <* char ')'
normal :: Parser Line
normal = Normal <$> many (try quoted <|> try raw' <|> try raw)
quoted :: Parser InLine
quoted = Quoted <$> (string "${" *> expr <* string "}")
raw' :: Parser InLine
raw' = Raw <$> ((:) <$> (char '$')
<*> ((:) <$> noneOf "{" <*> many (noneOf "$\n\r")))
raw :: Parser InLine
raw = Raw <$> many1 (noneOf "$\n\r")
arrange :: [(Indent, Line)] -> [(Indent, Line)]
arrange = norm . rev . foldl (flip push) []
where
isCtrlNothing (_, CtrlNothing) = True
isCtrlNothing _ = False
isCtrlElse (_, CtrlElse) = True
isCtrlElse _ = False
isCtrlOf (_, CtrlOf _) = True
isCtrlOf _ = False
push :: Line' -> [Line'] -> [Line']
push x [] = x:[]
push x ss'@((_, Normal _):_) = x:ss'
push x@(i, _) ss'@((j, CtrlForall b e body):ss)
| i > j = (j, CtrlForall b e (push x body)):ss
| otherwise = x:ss'
push x@(i, _) ss'@((j, CtrlLet b e body):ss)
| i > j = (j, CtrlLet b e (push x body)):ss
| otherwise = x:ss'
push x@(i, _) ss'@((j, CtrlMaybe flg b e body alt):ss)
| i > j = if flg
then (j, CtrlMaybe flg b e body (push x alt)):ss
else (j, CtrlMaybe flg b e (push x body) alt):ss
| i == j && isCtrlNothing x
= if flg
then error "too many $nothing found"
else (j, CtrlMaybe True b e body alt):ss
| otherwise = x:ss'
push x ((j, CtrlNothing):_) = error "orphan $nothing found"
push x@(i, _) ss'@((j, CtrlIf flg e body alt):ss)
| i > j = if flg
then (j, CtrlIf flg e body (push x alt)):ss
else (j, CtrlIf flg e (push x body) alt):ss
| i == j && isCtrlElse x
= if flg
then error "too many $else found"
else (j, CtrlIf True e body alt):ss
| otherwise = x:ss'
push x ((j, CtrlElse):_) = error "orphan $else found"
push x@(i, _) ss'@((j, CtrlCase e alts):ss)
| i > j = (j, CtrlCase e (push' x alts)):ss
| otherwise
= if isCtrlOf x
then error "orphan $of found"
else x:ss'
push x ((j, CtrlOf _):_) = error "orphan $of found"
push' x@(i, CtrlOf e) alts = (e, []):alts
push' x [] = error "$of not found"
push' x ((e, body):alts) = (e, (push x body)):alts
rev :: [Line'] -> [Line']
rev = foldr (\x xs -> xs ++ [rev' x]) []
rev' :: Line' -> Line'
rev' x@(_, Normal _) = x
rev' (i, CtrlForall b e body)
= (i, CtrlForall b e (rev body))
rev' (i, CtrlLet b e body)
= (i, CtrlLet b e (rev body))
rev' (i, CtrlMaybe flg b e body alt)
= (i, CtrlMaybe flg b e (rev body) (rev alt))
rev' (i, CtrlIf flg e body alt)
= (i, CtrlIf flg e (rev body) (rev alt))
rev' (i, CtrlCase e alts)
= (i, CtrlCase e (map (id *** rev) $ reverse alts))
norm :: [Line'] -> [Line']
norm = foldr (\x xs -> norm' x:xs) []
norm' :: Line' -> Line'
norm' x@(_, Normal _) = x
norm' (i, CtrlForall b e body)
= (i, CtrlForall b e (normsub i body ++ blockEnd))
norm' (i, CtrlLet b e body)
= (i, CtrlLet b e (normsub i body ++ blockEnd))
norm' (i, CtrlMaybe flg b e body alt)
= (i, CtrlMaybe flg b e (normsub i body ++ blockEnd) (normsub i alt ++ blockEnd))
norm' (i, CtrlNothing) = error "orphan $nothing found"
norm' (i, CtrlIf flg e body alt)
= (i, CtrlIf flg e (normsub i body ++ blockEnd) (normsub i alt ++ blockEnd))
norm' (i, CtrlElse) = error "orphan $else found"
norm' (i, CtrlCase e alts)
= (i, CtrlCase e (map (id *** (++ blockEnd) . normsub i) alts))
norm' (i, CtrlOf _) = error "orphan $of found"
normsub :: Indent -> [Line'] -> [Line']
normsub i body = let j = minimum $ map fst body
deIndent n = i+(n-j)
in norm $ map (deIndent *** id) body
blockEnd :: [Line']
blockEnd = [(0, Normal [])]
class ToQPat a where
toQPat :: a -> Q Pat
concatToQPat :: [a] -> Q Pat
instance ToQPat Expr where
toQPat (S s) = litP (stringL s)
toQPat (I i) = litP (integerL i)
toQPat W = wildP
toQPat (V v) = varP (mkName v)
toQPat (O o) = varP (mkName o)
toQPat (E e) = concatToQPat e
toQPat (C c) = conP (mkName c) []
toQPat (T t) = tupP $ map concatToQPat t
toQPat (A a e) = asP (mkName a) $ toQPat e
concatToQPat (x:O ":":xs) = infixP (toQPat x)
(mkName ":")
(concatToQPat xs)
concatToQPat ((C c):args) = conP (mkName c) $ map toQPat args
concatToQPat ((V v):args) = varP (mkName v)
concatToQPat (p@(T t):[]) = toQPat p
concatToQPat (W:[]) = wildP
concatToQPat (p@(A _ _):[]) = toQPat p
concatToQPat _ = error "don't support this pattern"
class ToQExp a where
toQExp :: a -> Q Exp
concatToQExp :: [a] -> Q Exp
instance ToQExp Expr where
toQExp (S s) = litE (stringL s)
toQExp (I i) = litE (integerL i)
toQExp W = error "wildcard is NOT legal expression"
toQExp (V v) = varE (mkName v)
toQExp (O o) = varE (mkName o)
toQExp (E e) = concatToQExp e
toQExp (C c) = conE (mkName c)
toQExp (T t) = tupE $ map concatToQExp t
toQExp N = listE []
toQExp (L l) = listE $ map concatToQExp l
concatToQExp xs = concatToQ' Nothing xs
where
concatToQ' (Just acc) [] = acc
concatToQ' Nothing [x] = toQExp x
concatToQ' Nothing (x:xs) = concatToQ' (Just (toQExp x)) xs
concatToQ' (Just acc) ((O ":"):xs)
= infixE (Just acc)
(conE (mkName ":"))
(Just (concatToQExp xs))
concatToQ' (Just acc) ((O o):xs)
= infixE (Just acc)
(varE (mkName o))
(Just (concatToQExp xs))
concatToQ' (Just acc) ((V' v'):xs)
= infixE (Just acc)
(varE (mkName v'))
(Just (concatToQExp xs))
concatToQ' (Just acc) (x:xs)
= concatToQ' (Just (appE acc (toQExp x))) xs
instance ToQExp InLine where
toQExp (Raw s) = litE (stringL s)
toQExp (Quoted expr) = concatToQExp expr
concatToQExp [] = litE (stringL "")
concatToQExp (x:xs) = infixE (Just (toQExp x))
(varE '(<>))
(Just (concatToQExp xs))
instance ToQExp Line where
toQExp (CtrlForall b e body)
= appE (appE (appE (varE 'foldr)
(lamE [concatToQPat b]
(infixE (Just (concatToQExp body))
(varE '(<>))
Nothing)))
(litE (stringL "")))
(concatToQExp e)
toQExp (CtrlMaybe flg b e body alt)
= caseE (concatToQExp e)
[ match (conP 'Just [concatToQPat b])
(normalB (concatToQExp body))
[]
, match (conP 'Nothing [])
(normalB (concatToQExp alt))
[]
]
toQExp (CtrlIf flg e body alt)
= condE (concatToQExp e) (concatToQExp body) (concatToQExp alt)
toQExp CtrlElse = error "illegal $else found"
toQExp (CtrlCase e alts)
= caseE (concatToQExp e) (map mkMatch alts)
where
mkMatch (e', body) = match (concatToQPat e')
(normalB (concatToQExp body))
[]
toQExp (CtrlOf e) = error "illegal $of found"
toQExp (CtrlLet b e body)
= letE [valD (concatToQPat b) (normalB $ concatToQExp e) []]
(concatToQExp body)
toQExp (Normal xs) = concatToQExp xs
concatToQExp (x:[]) = toQExp x
concatToQExp (x:xs) = infixE (Just (toQExp x))
(varE '(<>))
(Just (concatToQExp xs))
instance ToQExp Line' where
toQExp (n, x@(Normal _))
= infixE (Just (litE (stringL (replicate n ' '))))
(varE '(<>))
(Just (toQExp x))
toQExp (n, x) = toQExp x
concatToQExp [] = litE (stringL "")
concatToQExp (x@(_, Normal _):y:ys)
= infixE (Just (infixE (Just (toQExp x))
(varE '(<>))
(Just (litE (stringL "\n")))))
(varE '(<>))
(Just (concatToQExp (y:ys)))
concatToQExp (x@(_, Normal _):xs)
= infixE (Just (toQExp x))
(varE '(<>))
(Just (concatToQExp xs))
concatToQExp (x:xs) = infixE (Just (toQExp x))
(varE '(<>))
(Just (concatToQExp xs))