packages feed

indigo-0.4: src/Indigo/Internal/Expr/TH.hs

-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ

{-# LANGUAGE QuasiQuotes #-}

module Indigo.Internal.Expr.TH
       ( deriveExprBuildable
       ) where

import qualified Data.Map as M
import qualified Data.Set as S
import Language.Haskell.TH
import Language.Haskell.TH.Syntax
import qualified Language.Haskell.TH.Syntax as TH
import Prelude hiding (Const)
import qualified Text.Casing as C

pattern CName :: String -> (TH.Name, [BangType])
pattern CName nm <- (TH.Name (OccName nm) _, _)

type UntypedConstr = (Name, [BangType])

-- | Generates an Buildable instance for a Expr GADT. /Note:/ This will not generate
-- additional constraints to the generated instance if those are required.
-- Inspired by 'deriveGADTNFData' from Util.TH.
deriveExprBuildable :: Name -> Q [Dec]
deriveExprBuildable name = do
  TyConI (DataD _ dataName vars _ cons _) <- reify name

  let getNameFromVar (PlainTV n) = n
      getNameFromVar (KindedTV n _) = n
      convertTyVars orig = foldr (\a b -> AppT b . VarT $ getNameFromVar a) orig vars

      -- Unfolds multiple constructors of form "A, B, C :: A -> Stuff"
      -- into a list of tuples of constructor names and their data
      unfoldConstructor (GadtC cs bangs _) = map (,bangs) cs
      unfoldConstructor (ForallC _ _ c) = unfoldConstructor c
      unfoldConstructor _ = fail "Non GADT constructors are not supported."

      (bLeft, bRight, comma) = (LitE (StringL "("), LitE (StringL ")"), LitE (StringL ", "))

      mappendAll :: [Exp] -> Exp
      mappendAll [] = error "impossible empty list"
      mappendAll (hd : rest) = foldl (\res term -> InfixE (Just res) (VarE (mkName "<>")) (Just term)) hd rest

      omitUnaryConstr :: (Exp -> Exp) -> UntypedConstr -> Q Clause
      omitUnaryConstr fun (conName, _) = do
        var <- newName "a"
        return $
          Clause
            [ConP conName [VarP var]]
            (NormalB $ fun (VarE var))
            []

      -- Specific to Expr
      toFunName (CName "Right'") = "right"
      toFunName (CName "Left'")  = "left"
      toFunName (CName "Concat'") = "concatAll"
      toFunName (CName "Int'")  = "toInt"
      toFunName (TH.Name (OccName nm) _, _) = C.toCamel $ C.fromHumps nm

      operators :: Map String String
      operators = M.fromList [ ("Add", "+"), ("Sub", "-"), ("Mul", "*"), ("Div", "/"), ("Mod", "%")
                             , ("Lsl", "<<<"), ("Lsr", ">>>"), ("Eq'", "=="), ("Neq", "/="), ("Le", "=<")
                             , ("Lt", "<"), ("Ge", ">="), ("Gt", ">="), ("Or", "||"), ("Xor", "^"), ("And", "&&")
                             , ("Cons", ".:"), ("Concant", "<>")
                             ]

      braces :: Set String
      braces = S.fromList ["Add", "Sub", "Or", "Xor", "Lsl", "Lsr"]

      makeClause :: UntypedConstr -> Q Clause
      makeClause c@(CName "C") = omitUnaryConstr (AppE (VarE $ mkName "pretty") . AppE (ConE $ mkName "PrintAsValue")) c
      makeClause c@(CName "V") = omitUnaryConstr (AppE (VarE $ mkName "pretty")) c
      makeClause c@(CName "ObjMan") = omitUnaryConstr (AppE (VarE $ mkName "pretty"))  c
      makeClause c@(CName "Construct") = construct c
      makeClause c@(CName "ConstructWithoutNamed") = construct c
      makeClause c@(CName "StInsertNew") =
        generalClauseImpl "pretty" c Nothing
      makeClause c@(TH.Name (OccName nm) _, _) =
        generalClauseImpl "pretty" c $ (, S.member nm braces) <$> M.lookup nm operators

      construct (conName, _) = do
        proxy <- newName "proxy"
        r <- newName "rec"
        let showTypeRep = AppE (VarE $ mkName "show") . AppE (VarE $ mkName "typeRep")
        mappendRec <- [| \x -> mconcat (intersperse "," (recordToList (rmap (\ex -> Const (pretty ex)) x))) |]
        return $
          Clause
            [ConP conName [VarP proxy, VarP r]]
            (NormalB $
              mappendAll [showTypeRep (VarE proxy), bLeft, (AppE mappendRec (VarE r)), bRight])
            []

      -- Constructs a clause "build (ConName a1 a2) = "CON_NAME" <> "(" <> pretty a1 <> pretty a2 <> ")"
      -- The first argument is a workaround: it returns name of printing function either "pretty" or "show"
      -- by index argument.
      -- This is mainly for 'StInsertNew' constructor to print error with show
      generalClauseImpl :: String -> (Name, [BangType]) -> Maybe (String, Bool) -> Q Clause
      generalClauseImpl funName c@(conName, bangs) isInfix = do
        varNames <- traverse (\_ -> newName "a") bangs
        -- useful constants
        let funStr = LitE (StringL $ toFunName c)
        let pretties =
              case map (\e -> AppE (VarE $ mkName funName) (VarE e)) varNames of
                -- Nullary operator: "function_name"
                []    -> [funStr]
                -- Infix binary operator without braces: pretty x <> " operator " <> pretty y
                [x, y] | Just (op, False) <- isInfix ->
                  [x, LitE (StringL $ " " <> op <> " "), y]
                -- Infix binary operator with braces: "(" <> pretty x <> " operator " <> pretty y <> ")"
                [x, y] | Just (op, True) <- isInfix ->
                  [bLeft, x, LitE (StringL $ " " <> op <> " "), y, bRight]
                -- Infix binary operator with braces: "function_name" <> "(" <> pretty x1 <> "," ... <> pretty xn <> ")"
                xs -> funStr : bLeft : (intersperse comma xs ++ [bRight])
        return $
          Clause
            [ConP conName $ map VarP varNames]
            (NormalB $ mappendAll pretties)
            []

  clauses <- traverse makeClause $ cons >>= unfoldConstructor
  return
    [ InstanceD
        Nothing
        []
        (AppT (ConT $ mkName "Buildable") (convertTyVars $ ConT dataName))
        [FunD (mkName "build") clauses]
    ]