packages feed

agda2hs-1.4: src/Agda2Hs/Compile/ClassInstance.hs

module Agda2Hs.Compile.ClassInstance where

import Control.Monad ( when, filterM, unless )
import Control.Monad.Reader ( asks, local )

import Data.Foldable ( toList )
import Data.List ( nub )
import Data.Maybe ( isNothing, mapMaybe )
import qualified Data.HashMap.Strict as HMap

import Agda.Compiler.Backend
import Agda.Compiler.Common ( curDefs, sortDefs )

import Agda.Interaction.BasicOps ( parseName )

import Agda.Syntax.Common hiding ( Ranged )
import Agda.Syntax.Internal
import Agda.Syntax.Internal.Pattern ( patternToTerm )
import Agda.Syntax.Position ( noRange )
import Agda.Syntax.Scope.Base
import Agda.Syntax.Scope.Monad ( resolveName )
import Agda.Syntax.Common.Pretty ( prettyShow )

import Agda.TypeChecking.Pretty
import Agda.TypeChecking.Substitute ( Apply(applyE), absBody, absApp, apply )
import Agda.TypeChecking.Records
import Agda.TypeChecking.Telescope ( mustBePi, piApplyM )

import Agda.Utils.Lens
import Agda.Utils.List ( headWithDefault )
import Agda.Utils.Monad ( ifNotM )
import Agda.Utils.Impossible ( __IMPOSSIBLE__ )

import Agda2Hs.AgdaUtils
import Agda2Hs.Compile.Function
import Agda2Hs.Compile.Name
import Agda2Hs.Compile.Term
import Agda2Hs.Compile.Type
import Agda2Hs.Compile.Types
import Agda2Hs.Compile.Utils

import qualified Agda2Hs.Language.Haskell as Hs
import Agda2Hs.Language.Haskell.Utils ( hsName, pp, replaceName, unQual )

enableCopatterns :: C a -> C a
enableCopatterns = local $ \e -> e { copatternsEnabled = True }

disableCopatterns :: C a -> C a
disableCopatterns = local $ \e -> e { copatternsEnabled = False }


-- | Enable the approriate extensions for a given Haskell deriving strategy.
enableStrategy :: Maybe (Hs.DerivStrategy ()) -> C ()
enableStrategy Nothing = return ()
enableStrategy (Just s) = do
  tellExtension Hs.DerivingStrategies
  case s of
    Hs.DerivStock ()    -> return () -- is included in GHC
    Hs.DerivAnyclass () -> tellExtension Hs.DeriveAnyClass -- since 7.10.1
    Hs.DerivNewtype ()  -> tellExtension Hs.GeneralizedNewtypeDeriving -- since 6.8.1.
    Hs.DerivVia () t    -> tellExtension Hs.DerivingVia -- since 8.6.1


compileInstance :: InstanceTarget -> Definition -> C (Hs.Decl ())

compileInstance (ToDerivation strategy) def@Defn{..} =
  setCurrentRangeQ defName $ do
    reportSDoc "agda2hs.compile.instance" 13 $
      text "compiling instance" <+> prettyTCM defName <+> text "to standalone deriving"
    tellExtension Hs.StandaloneDeriving
    enableStrategy strategy
    ir <- compileInstRule [] (unEl defType)
    return $ Hs.DerivDecl () strategy Nothing ir

compileInstance ToDefinition def@Defn{..} =
  enableCopatterns $ setCurrentRangeQ defName $ do
    reportSDoc "agda2hs.compile.instance" 13 $
      text "compiling instance" <+> prettyTCM defName <+> text "to instance definition"
    ir <- compileInstRule [] (unEl defType)
    withFunctionLocals defName $ do
      reportSDoc "agda2hs.compile.instance" 20 $ vcat $
        text "compileInstance clauses: " :
        map (nest 2 . prettyTCM) funClauses
      let mod = qnameModule defName
      reportSDoc "agda2hs.compile.instance" 25 $ text "compileInstance module: " <+> prettyTCM mod
      tel <- lookupSection mod
      addContext tel $ do
        liftTCM $ setModuleCheckpoint mod
        pars <- getContextArgs
        ty <- defType `piApplyM` pars
        let clauses = funClauses `apply` pars
        (ds, rs) <- concatUnzip
                <$> mapM (\c -> withClauseLocals mod c $ compileInstanceClause mod ty c) clauses
        reportSDoc "agda2hs.compile.instance" 20 $ vcat $
          text "compileInstance compiled clauses: " :
          map (nest 2 . text . pp) ds
        when (length (nub rs) > 1) $
          agda2hsErrorM $ fsep (pwords "More than one minimal record used.")
        return $ Hs.InstDecl () Nothing ir (Just ds)
    where Function{..} = theDef


compileInstRule :: [Hs.Asst ()] -> Term -> C (Hs.InstRule ())
compileInstRule cs ty = do
  reportSDoc "agda2hs.compile.instance" 20 $ text "compileInstRule" <+> prettyTCM ty
  case unSpine1 ty of
    Def f es | Just args <- allApplyElims es -> do
      fty <- defType <$> getConstInfo f
      vs <- compileTypeArgs fty args
      f <- compileQName f
      return $
        Hs.IRule () Nothing (ctx cs) $ foldl (Hs.IHApp ()) (Hs.IHCon () f) (map pars vs)
      where ctx [] = Nothing
            ctx cs = Just (Hs.CxTuple () cs)
            -- put parens around anything except a var or a constant
            pars :: Hs.Type () -> Hs.Type ()
            pars t@(Hs.TyVar () _) = t
            pars t@(Hs.TyCon () _) = t
            pars t = Hs.TyParen () t
    Pi a b -> compileDomType (absName b) a >>= \case
      DomDropped -> underAbstr a b (compileInstRule cs . unEl)
      DomConstraint hsA ->
        underAbstraction a b (compileInstRule (cs ++ [hsA]) . unEl)
      DomType _ t -> __IMPOSSIBLE__
      DomForall _ -> __IMPOSSIBLE__
    _ -> __IMPOSSIBLE__


-- Plan:
--  - ✓ Eta-expand if no copatterns (top-level)
--  - ✓ drop default implementations and chase definitions of primitive methods in minimal records + *checks*
--  - ✓ compileInstanceClause on resulting clauses
--
-- *checks*
--  - ✓ Only one minimal record
--  - ✓ all primitives of the minimal are projected from the same dictionary
--  - ✓ default implementation that get dropped are also projected from that same dictionary


etaExpandClause :: Clause -> C [Clause]
etaExpandClause cl@Clause{clauseBody = Nothing} =
  agda2hsError "Instance definition with absurd pattern!"
etaExpandClause cl@Clause{namedClausePats = ps, clauseBody = Just t} = do
  case t of
    Con c _ _ -> do
      let fields = conFields c
      let cls = [ cl{ namedClausePats = ps ++ [unnamed . ProjP ProjSystem <$> f],
                      clauseBody      = Just $ t `applyE` [Proj ProjSystem $ unArg f] }
                | f <- fields ]
      return cls
    _ -> agda2hsErrorM $ fsep (pwords $
      "Type class instances must be defined using copatterns (or top-level" ++
      " records) and cannot be defined using helper functions.")


compileInstanceClause :: ModuleName -> Type -> Clause -> C ([Hs.InstDecl ()], [QName])
compileInstanceClause curModule ty c = ifNotM (keepClause c) (return ([], [])) $ do

  let naps = namedClausePats c

  -- there are no projection patterns: we need to eta-expand the clause
  if all (isNothing . isProjP) naps then do
    cs <- etaExpandClause c
    reportSDoc "agda2hs.compile.instance" 20 $ vcat $
      text "compileInstance expanded clauses: " :
      map (nest 2 . prettyTCM) cs
    concatUnzip <$> mapM (compileInstanceClause curModule ty) cs

  -- otherwise we seek the first projection pattern
  else addContext (KeepNames $ clauseTel c) $
    compileInstanceClause' curModule ty naps c { clauseTel = EmptyTel }

-- abuse compileClause:
-- 1. drop any patterns before record projection to suppress the instance arg
-- 2. use record proj. as function name
-- 3. process remaing patterns as usual
compileInstanceClause' :: ModuleName -> Type -> NAPs -> Clause -> C ([Hs.InstDecl ()], [QName])
compileInstanceClause' curModule ty [] c = __IMPOSSIBLE__
compileInstanceClause' curModule ty (p:ps) c

  -- reached record projection
  | ProjP _ q <- namedArg p = do

    -- we put back the remaining patterns in the original clause
    let c' = c {namedClausePats = ps}

    -- We want the actual field name, not the instance-opened projection.
    (q, _, _) <- origProjection q
    arg <- fieldArgInfo q


    reportSDoc "agda2hs.compile.instance" 15 $
      text "Compiling instance clause for" <+> prettyTCM (Arg arg $ Def q [])

    reportSDoc "agda2hs.compile.instance" 15 $
      text "Instance type: " <+> prettyTCM ty

    -- retrieve the type of the projection
    Just (unEl -> Pi a b) <- getDefType q ty
    -- We don't really have the information available to reconstruct the instance
    -- head. However, all dependencies on the instance head are in erased positions,
    -- so we can just use a dummy term instead
    let instanceHead = __DUMMY_TERM__
        ty' = b `absApp` instanceHead

    reportSDoc "agda2hs.compile.instance" 15 $
      text "Clause type: " <+> prettyTCM ty'

    reportSDoc "agda2hs.compile.instance" 15 $
      text "Clause patterns:" <+> prettyTCM (namedArg <$> ps)

    reportSDoc "agda2hs.compile.instance" 18 $ text "Current module:" <+> prettyTCM curModule
    ls <- asks locals
    reportSDoc "agda2hs.compile.instance" 18 $ text "Clause locals:" <+> prettyTCM ls

    let uf = hsName $ prettyShow $ nameConcrete $ qnameName q

    let
      (.~) :: QName -> QName -> Bool
      x .~ y = nameConcrete (qnameName x) == nameConcrete (qnameName y)

    if
      -- Instance field: check canonicity.
      | isInstance arg -> do
          unless (null ps) $ agda2hsError "not allowed: explicitly giving superclass"
          body <- case clauseBody c' of
            Nothing -> agda2hsError "not allowed: absurd clause for superclass"
            Just b  -> return b
          addContext (clauseTel c) $ do
            liftTCM $ setModuleCheckpoint curModule
            checkInstance body
          reportSDoc "agda2hs.compile.instance" 20 $ vcat
            [ text "compileInstanceClause dropping clause"
            , nest 2 $ prettyTCM c
            ]
          return ([], [])

      -- Projection of a primitive field: chase down definition and inline as instance clause.
      | Clause {namedClausePats = [], clauseBody = Just (Def n es)} <- c'
      , [(_, f)] <- mapMaybe isProjElim es
      , f .~ q -> do
        reportSDoc "agda2hs.compile.instance" 20 $
          text "Instance clause is projected from" <+> prettyTCM (Def n [])
        reportSDoc "agda2hs.compile.instance" 40 $
          text $ "raw name: " ++ prettyShow (Def n [])
        d@Defn{..} <- getConstInfo n
        let mod = if isExtendedLambdaName defName then curModule else qnameModule defName
        let isMatchingProj [] = False
            isMatchingProj (p:ps) | ProjP _ g <- namedArg p = g .~ q
            isMatchingProj (p:ps) = isMatchingProj ps
        (fc, rs) <- withCurrentModule mod
            $ fmap concatUnzip
            . mapM (compileInstanceClause mod defType)
            . filter (isMatchingProj . namedClausePats)
            $ funClauses theDef
        let hd = hsName $ prettyShow $ nameConcrete $ qnameName defName
        let fc' = {- dropPatterns 1 $ -} replaceName hd uf fc
        reportSDoc "agda2hs.compile.instance" 6 $ vcat $
          text "compileInstanceClause compiled clause: " :
          map (nest 2 . text . pp) fc'
        return (fc', n:rs)

       -- Projection of a default implementation: drop while making sure these are drawn from the
       -- same (minimal) dictionary as the primitive fields.
      | Clause {namedClausePats = [], clauseBody = Just (Def n es)} <- c'
      , n .~ q -> do
        let err = agda2hsError $ "illegal instance declaration: instances using default methods should use a named definition or an anonymous `λ where`."
            filterArgs :: Type -> [Term] -> C [Term]
            filterArgs ty [] = return []
            filterArgs ty (v:vs) = do
              reportSDoc "agda2hs.compile.instance" 25 $ text "filterArgs: v =" <+> prettyTCM v
              (a,b) <- mustBePi ty
              reportSDoc "agda2hs.compile.instance" 25 $ text "filterArgs: a =" <+> prettyTCM a
              let rest = underAbstraction a b $ \b' -> filterArgs b' vs
              compileDom a >>= \case
                DODropped -> rest
                DOType -> rest
                DOTerm -> (v:) <$> rest
                DOInstance -> err
        ty <- defType <$> getConstInfo n
        traverse (filterArgs ty) (map unArg <$> allApplyElims es) >>= \case
          Just [ Def f _ ] -> do
            reportSDoc "agda2hs.compile.instance" 20 $ vcat
                [ text "Dropping default instance clause" <+> prettyTCM c'
                , text "with minimal dictionary" <+> prettyTCM f
                ]
            reportSDoc "agda2hs.compile.instance" 40 $
                text $ "raw dictionary:" ++ prettyShow f
            return ([], [f])
          _ -> err

      -- No minimal dictionary used, proceed with compiling as a regular clause.
      | otherwise -> do
        reportSDoc "agda2hs.compile.instance" 20 $ text "Compiling instance clause" <+> prettyTCM c'
        ms <- disableCopatterns $ compileClause curModule Nothing uf ty' c'
        let cc = Hs.FunBind () (toList ms)
        reportSDoc "agda2hs.compile.instance" 20 $ vcat
          [ text "compileInstanceClause compiled clause"
          , nest 2 $ text $ pp cc
          ]
        return ([Hs.InsDecl () cc], [])

-- finding a pattern other than a projection: we skip
-- NOTE(flupe): actually I think we may want to throw hard errors here
--              if there is something other than erased parameters
compileInstanceClause' curModule ty (p:ps) c = do
  (a, b) <- mustBePi ty
  compileInstanceClause' curModule (absApp b (patternToTerm $ namedArg p)) ps c

fieldArgInfo :: QName -> C ArgInfo
fieldArgInfo f = do
  r <- maybe badness return =<< liftTCM (getRecordOfField f)
  Record{ recFields = fs } <- theDef <$> getConstInfo r
  case filter ((== f) . unDom) fs of
    df : _ -> return $ getArgInfo df
    []     -> badness
  where
    badness = agda2hsErrorM $ text "Not a record field:" <+> prettyTCM f


findDefinitions :: (QName -> Definition -> C Bool) -> ModuleName -> C [Definition]
findDefinitions p m = do
  localDefs    <- (^. sigDefinitions) . (^. stSignature) <$> getTCState
  importedDefs <- (^. sigDefinitions) . (^. stImports)   <$> getTCState
  let allDefs = HMap.union localDefs importedDefs
      inMod = [ (x, def) | (x, def) <- HMap.toList allDefs, isInModule x m ]
  map snd <$> filterM (uncurry p) inMod


resolveStringName :: String -> C QName
resolveStringName s = do
  cqname <- liftTCM $ parseName noRange s
  rname  <- liftTCM $ resolveName cqname
  case rname of
    DefinedName _ aname _ -> return $ anameName aname
    _ -> agda2hsStringError $ "Couldn't find " ++ s


lookupDefaultImplementations :: QName -> [Hs.Name ()] -> C [Definition]
lookupDefaultImplementations recName fields = do
  let modName = qnameToMName recName
      isField f _ = (`elem` fields) <$> compileName (qnameName f)
  findDefinitions isField modName

classMemberNames :: Definition -> C [Hs.Name ()]
classMemberNames def =
  case theDef def of
    Record{recFields = fs} -> fmap unQual <$> traverse compileQName (map unDom fs)
    _ -> agda2hsErrorM $ text "Not a record:" <+> prettyTCM (defName def)