packages feed

yasi-0.2.0.2: src/Yasi/Internal.hs

-- | Internal module, no stability guarantees
module Yasi.Internal
  ( Segment (..),
    parseSegments,
    ipExpr,
    interpolator,
    Stringish (..),
  )
where

import Control.Monad ((>=>))
import Data.Char qualified as C
import Data.List (foldl')
import Data.Text qualified as T
import Data.Text.Builder.Linear qualified as TBL
import Data.Text.Display qualified as TD
import Data.Text.Lazy qualified as TL
import GHC.Generics (Generic)
import Language.Haskell.Meta.Parse qualified as GhcHsMeta
import Language.Haskell.TH.Lib qualified as TH
import Language.Haskell.TH.Quote qualified as TH
import Language.Haskell.TH.Syntax qualified as TH

data Segment
  = Lit String
  | Exp String
  | Abs -- idea due to interpolate
  deriving (Show, Eq, Generic)

parseSegments :: (MonadFail m) => Char -> String -> m [Segment]
parseSegments c = fmap (group []) . go
  where
    -- ugly, but simple enough™
    go s
      | let (lit, rest) = span (/= c) s, not (null lit) = (Lit lit :) <$> go rest
      | s == "" = pure []
      | s == [c] = fail $ "should not end with single " <> [c]
      | _ : c' : rest <- s, c == c' = (Lit [c] :) <$> go rest
      | _ : '{' : rest <- s = case span (/= '}') rest of -- TODO smarter?
          (exp, '}' : rest) ->
            let seg = if exp == "" then Abs else Exp exp
             in (seg :) <$> go rest
          _ -> fail "missing closing bracket"
      | _ : v : rest' <- s,
        isVarStartChar v =
          let (vs, rest) = span isVarChar rest'
           in (Exp (v : vs) :) <$> go rest
      | otherwise = fail $ "invalid char after " <> [c]
    isVarStartChar v = C.isAscii v && C.isAlpha v
    isVarChar v = C.isAscii v && (C.isAlphaNum v || v == '_' || v == '\'')

    group ls [] = lit ls
    group ls (Lit l : ss) = group (l : ls) ss
    group ls (s : ss) = lit ls <> (s : group [] ss)
    lit [] = []
    lit ls = [Lit $ mconcat $ reverse ls]

ipExpr :: (TH.Exp -> TH.Exp) -> [Segment] -> TH.Q TH.Exp
ipExpr transform segs = do
  (ls, lams) <- go segs
  pure
    . lams
    . transform
    . TH.AppE (TH.VarE 'stringish)
    . foldl' (\a b -> TH.InfixE (Just a) (TH.VarE '(<>)) (Just b)) (TH.VarE 'mempty)
    $ ls
  where
    go = \case
      [] -> pure ([], id)
      s : ss -> prep (go ss) case s of
        Lit l -> (,id) <$> [|$(TH.stringE l) :: String|]
        Exp e -> do
          exts <- TH.extsEnabled
          exp <- case GhcHsMeta.parseExpWithExts exts e of
            Right e -> pure e
            Left (line, col, msg) ->
              fail . unlines $
                [ "Parse error at splice `" <> e <> "`:",
                  show line <> ":" <> show col <> ": " <> msg
                ]
          pure (exp, id)
        Abs -> do
          n <- TH.newName "int"
          pure (TH.VarE n, TH.LamE [TH.VarP n])
    prep asg af = do
      (as, g) <- asg
      (a, f) <- af
      pure (TH.AppE (TH.VarE 'TD.displayBuilder) a : as, f . g)

interpolator ::
  Char ->
  -- | postprocess the 'TH.Exp'
  (TH.Exp -> TH.Exp) ->
  TH.QuasiQuoter
interpolator c pp = TH.QuasiQuoter {..}
  where
    quoteExp = parseSegments c >=> ipExpr pp
    quotePat = const $ fail "pattern context not supported"
    quoteType = const $ fail "type context not supported"
    quoteDec = const $ fail "declaration context not supported"

class Stringish a where
  stringish :: TBL.Builder -> a

instance Stringish String where
  stringish = T.unpack . TBL.runBuilder

instance Stringish T.Text where
  stringish = TBL.runBuilder

instance Stringish TL.Text where
  stringish = TL.fromStrict . TBL.runBuilder