packages feed

large-records-0.1.0.0: src/Data/Record/QQ/CodeGen.hs

{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell     #-}
{-# LANGUAGE TupleSections       #-}

-- | Quasi-quoter support for large records
--
-- NOTE: The only reason for the existence of this module is that record pattern
-- syonyms in @ghc@ are currently not useable: when we declare a record pattern
-- synonym, @ghc@ automatically derives field accessors for every field in the
-- record. We don't want those accessors: they result in name clashes
-- (DuplicateRecordFields does not apply to record pattern synonyms) and, more
-- importantly, they result in quadratic code size again. Once the
-- @NoFieldSelectors@ language extension is merged (probably @ghc@ 9.2), we
-- can reconsider whether this module is still required.
--
-- See also:
--
-- * <https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0160-no-toplevel-field-selectors.rst>
-- * <https://gitlab.haskell.org/ghc/ghc/-/merge_requests/4743>
module Data.Record.QQ.CodeGen (
    lr

    -- * Exported for the benefit of tests
  , lrExp
  , lrPat
  ) where

import Data.List (intercalate)
import Language.Haskell.TH
import Language.Haskell.TH.Quote
import Language.Haskell.TH.Syntax

import qualified Data.Generics         as SYB
import qualified Language.Haskell.Exts as HSE
import qualified Language.Haskell.Meta as HSE.Meta

import Data.Record.Internal.CodeGen
import Data.Record.Internal.Naming
import Data.Record.Internal.Record
import Data.Record.Internal.TH.Util
import Data.Record.QQ.CodeGen.HSE
import Data.Record.QQ.CodeGen.Parser
import Data.Record.QQ.Runtime.MatchHasField
import Data.Record.TH.CodeGen.Tree

import qualified Data.Record.Internal.TH.Name as N

{-------------------------------------------------------------------------------
  Top-level quasi-quoter
-------------------------------------------------------------------------------}

-- | Construct or match on @large-records@-style records
--
-- Example construction usage:
--
-- > inOrder :: R Bool
-- > inOrder = [lr| MkR { x = 1234, y = [True] } |]
--
-- or:
--
-- > constructorApp :: R Bool
-- > constructorApp = [lr| MkR |] 1234 [True]
--
-- Example matching usage:
--
-- > projectOne :: T Bool -> Int
-- > projectOne [lr| MkT { x = a } |] = a
lr :: QuasiQuoter
lr = QuasiQuoter {
      quoteExp  = lrExp
    , quotePat  = lrPat
    , quoteType = unsupported
    , quoteDec  = unsupported
    }
  where
    unsupported :: String -> Q a
    unsupported _ = fail "lr can only be used for expressions or patterns"

{-------------------------------------------------------------------------------
  Individual quasi-quoters
-------------------------------------------------------------------------------}

lrExp :: forall m. Quasi m => String -> m Exp
lrExp = \str -> do
    exts <- runQ extsEnabled
    case parseExp exts str of
      Left  err  -> fail $ parseErr err
      Right expr -> construct expr
  where
    parseExp :: [Extension] -> String -> Either String Exp
    parseExp exts str =
        case HSE.parseExpWithMode (parseMode exts) str of
          HSE.ParseFailed _loc err -> Left err
          HSE.ParseOk e -> Right (HSE.Meta.toExp e)

    parseErr :: String -> String
    parseErr err = concat [
          "Could not parse expression: "
        , map (\c -> if c == '\n' then ' ' else c) err
        ]

lrPat :: forall m. Quasi m => String -> m Pat
lrPat = \str -> do
    exts <- runQ extsEnabled
    case parsePat exts str of
      Left  err  -> fail $ parseErr err
      Right expr -> deconstruct expr
  where
    parsePat :: [Extension] -> String -> Either String Pat
    parsePat exts str =
        case HSE.parsePatWithMode (parseMode exts) str of
          HSE.ParseFailed _loc err -> Left err
          HSE.ParseOk p -> Right (HSE.Meta.toPat (processRecordPuns p))

    parseErr :: String -> String
    parseErr err = concat [
          "Could not parse pattern: "
        , map (\c -> if c == '\n' then ' ' else c) err
        ]

{-------------------------------------------------------------------------------
  Construction
-------------------------------------------------------------------------------}

construct :: forall m. Quasi m => Exp -> m Exp
construct = \case
    ConE constr -> do
      constrFn <- resolveKnownHseName nameRecordTypedConstructorFn (fromHseName constr)
      runQ $ N.varE constrFn
    expr ->
      -- Assume this is a record construction expression
      SYB.everywhereM (SYB.mkM go) expr
  where
    go :: Exp -> m Exp
    go e = do
        mTerm <- parseRecordExp e
        case mTerm of
          Nothing ->
            -- Leave non-record expressions alone
            return e
          Just NotKnownLargeRecord ->
            return e
          Just (UnknownFields unknown) -> runQ $ do
            reportError $ "Unknown fields: " ++ intercalate ", " unknown
            [| undefined |]
          Just (ParsedRecordInfo qual Record{..}) -> runQ $ do
            appsE $ N.varE (N.qualify qual (nameRecordTypedConstructorFn recordConstr))
                  : map mkArg recordFields

    mkArg :: Field (Maybe Exp) -> Q Exp
    mkArg Field{..}
      | Just e <- fieldVal = return e
      | otherwise = do
          reportWarning $ "No value for field " ++ fieldName
          [| error $ "No value given for field " ++ $(lift fieldName) |]

{-------------------------------------------------------------------------------
  Deconstruction
-------------------------------------------------------------------------------}

deconstruct :: forall m. Quasi m => Pat -> m Pat
deconstruct = \pat -> do
    requiresExtensions [TypeApplications, ViewPatterns, DataKinds]
    SYB.everywhereM (SYB.mkM go) pat
  where
    go :: Pat -> m Pat
    go p = do
         mTerm <- parseRecordPat p
         case mTerm of
           Nothing -> -- Not a record pattern
             return p
           Just NotKnownLargeRecord ->
             return p
           Just (UnknownFields unknown) -> runQ $ do
             reportError $ "Unknown fields: " ++ intercalate ", " unknown
             return p
           Just (ParsedRecordInfo qual r) -> runQ $
             viewP (varE 'viewAtType `appE` recordUndefinedValueE qual r) $
               case recordFields (dropMissingRecordFields r) of
                 [] -> wildP
                 fs -> outerViewPat fs

    outerViewPat :: [Field Pat] -> Q Pat
    outerViewPat fs =
        viewP (varE 'matchHasField) $
          mkTupleP innerViewPat $ nest (MaxTupleElems 2) fs

    innerViewPat :: Field Pat -> Q Pat
    innerViewPat f@Field{..} =
        viewP
          (varE 'fieldNamed `appTypeE` fieldNameT f)
          (return fieldVal)

{-------------------------------------------------------------------------------
  Auxiliary
-------------------------------------------------------------------------------}

parseMode :: [Extension] -> HSE.ParseMode
parseMode exts = HSE.defaultParseMode {
      HSE.extensions = concat [
          -- Include extensions enabled in the module
          map extensionFromTH exts

          -- But also include the default
          --
          -- We do this primarily because 'fromTH' doesn't actually parse
          -- all extensions
        , HSE.extensions HSE.defaultParseMode
        ]
    }