vulkan-utils-0.1.3: src/Vulkan/Utils/ShaderQQ/Interpolate.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE QuasiQuotes #-}
module Vulkan.Utils.ShaderQQ.Interpolate
( interpExp
) where
import Control.Applicative ( liftA2 )
import Data.Char
import Language.Haskell.TH
import Text.ParserCombinators.ReadP
-- $setup
-- >>> :set -XTemplateHaskell
-- >>> import Data.Proxy
-- | 'interpExp' performs very simple interpolation of Haskell
-- values into 'String's.
--
-- - Interpolated variables are prefixed with @$@
-- - They can optionally be surrounded with braces like @${foo}@
-- - Interpolated variables are converted to strings with 'show'
-- - To escape a @$@ use @\\$@
--
-- >>> let foo = 123 in $(interpExp "hello, $foo")
-- "hello, 123"
--
-- >>> let foo = "world" in $(interpExp "hello, \\$foo")
-- "hello, $foo"
--
-- >>> let foo = "world" in $(interpExp "hello\r\n\rworld")
-- "hello\r\n\rworld"
interpExp :: String -> Q Exp
interpExp =
foldEither (litE (stringL ""))
(appE (varE 'show) . varOrConE)
(litE . stringL)
(\e1 e2 -> [|$e1 <> $e2|])
. parse
----------------------------------------------------------------
-- The parser
----------------------------------------------------------------
type Var = String
-- | Extract variables and literals from string to be interpolated
--
-- >>> parse ""
-- []
--
-- >>> parse "hello $world"
-- [Right "hello ",Left "world"]
--
-- >>> parse "$hello$world"
-- [Left "hello",Left "world"]
--
-- >>> parse "$"
-- [Right "$"]
--
-- >>> parse "hi"
-- [Right "hi"]
--
-- >>> parse "h$hi"
-- [Right "h",Left "hi"]
--
-- >>> parse "$$hi"
-- [Right "$",Left "hi"]
--
-- >>> parse "$1"
-- [Right "$1"]
--
-- >>> parse "$$$"
-- [Right "$$$"]
--
-- >>> parse "\\"
-- [Right "\\"]
--
-- >>> parse "\\$"
-- [Right "$"]
--
-- >>> parse "\\$hi"
-- [Right "$hi"]
--
-- >>> parse "\\\\$hi"
-- [Right "\\$hi"]
--
-- >>> parse "\\hi"
-- [Right "\\hi"]
--
-- >>> parse "$hi\\$foo"
-- [Left "hi",Right "$foo"]
--
-- >>> parse "hello, \\$foo"
-- [Right "hello, $foo"]
--
-- >>> parse "${fo'o}bar"
-- [Left "fo'o",Right "bar"]
--
-- >>> parse "\\"
-- [Right "\\"]
--
-- >>> parse "\\\\$"
-- [Right "\\$"]
--
-- >>> parse "$"
-- [Right "$"]
parse :: String -> [Either Var String]
parse s =
let -- A haskell var or con
ident = (:) <$> satisfy (isLower <||> isUpper <||> (== '_')) <*> munch
(isAlphaNum <||> (== '\'') <||> (== '_'))
braces = between (char '{') (char '}')
-- parse a var, a '$' followed by an ident
var =
char '$' *> ((Left <$> (ident +++ braces ident)) <++ pure (Right "$"))
-- Everything up to a '$' or '\'
normal = Right <$> munch1 ((/= '$') <&&> (/= '\\'))
-- escape a $
escape = char '\\' *> (Right <$> (string "$" <++ pure "\\"))
-- One normal or var
-- - Check escaped '$' first
-- - variables, starting with $
-- - normal string
one = normal +++ var +++ escape
parser = many one <* eof
in
case readP_to_S parser s of
[(r, "")] -> foldr mergeRights [] r
_ -> error "Failed to parse string"
mergeRights :: Either Var String -> [Either Var String] -> [Either Var String]
mergeRights = \case
Left v -> (Left v :)
Right n -> \case
(Right m : xs) -> Right (n <> m) : xs
xs -> Right n : xs
(<&&>), (<||>) :: Applicative f => f Bool -> f Bool -> f Bool
(<||>) = liftA2 (||)
(<&&>) = liftA2 (&&)
----------------------------------------------------------------
-- Misc utilities
----------------------------------------------------------------
varOrConE :: String -> ExpQ
varOrConE n = (if isLower (head n) then varE else conE) . mkName $ n
foldEither
:: (Foldable t, Functor t)
=> c
-> (a -> c)
-> (b -> c)
-> (c -> c -> c)
-> t (Either a b)
-> c
foldEither i l r f = foldr f i . fmap (either l r)