packages feed

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)