packages feed

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

module Agda2Hs.Compile.Record where

import Control.Monad ( unless, when )
import Control.Monad.Reader ( MonadReader(local) )

import Data.List ( (\\), nub )
import Data.List.NonEmpty ( NonEmpty(..) )
import Data.Map ( Map )
import qualified Data.Map as Map

import Agda.Compiler.Backend

import Agda.Syntax.Common ( Arg(unArg), defaultArg, HasEta'(..) )
import Agda.Syntax.Internal
import Agda.Syntax.Common.Pretty ( prettyShow )

import Agda.TypeChecking.Pretty ( ($$), (<+>), text, vcat, prettyTCM, pretty, prettyList_ )
import Agda.TypeChecking.Substitute ( TelV(TelV), Apply(apply) )
import Agda.TypeChecking.Telescope

import Agda.Utils.Singleton
import Agda.Utils.Impossible ( __IMPOSSIBLE__ )

import Agda2Hs.AgdaUtils
import Agda2Hs.Compile.ClassInstance
import Agda2Hs.Compile.Function ( compileFun )
import Agda2Hs.Compile.Type ( compileDomType, compileTeleBinds, compileDom, DomOutput(..) )
import Agda2Hs.Compile.Types
import Agda2Hs.Compile.Utils

import qualified Agda2Hs.Language.Haskell as Hs
import Agda2Hs.Language.Haskell.Utils ( hsName, definedName, pp, isQuantifiedAsst )

-- | Primitive fields and default implementations
type MinRecord = ([Hs.Name ()], Map (Hs.Name ()) (Hs.Decl ()))

withMinRecord :: QName -> C a -> C a
withMinRecord m = local $ \ e -> e { minRecordName = Just (qnameToMName m) }

compileMinRecord :: [Hs.Name ()] -> QName -> C MinRecord
compileMinRecord fieldNames m = withMinRecord m $ do
  reportSDoc "agda2hs.record.min" 20 $
    text "Compiling minimal record" <+> pretty m <+>
    text "with field names" <+> prettyList_ (map (text . pp) fieldNames)
  rdef <- getConstInfo m
  definedFields <- classMemberNames rdef
  let Record{recPars = npars, recTel = tel} = theDef rdef
      pars = map Apply $ take npars $ teleArgs tel
      rtype = El __DUMMY_SORT__ $ Def m pars
  defaults <- lookupDefaultImplementations m (fieldNames \\ definedFields)
  -- We can't simply compileFun here for two reasons:
  -- * it has an explicit dictionary argument
  -- * it's using the fields and definitions from the minimal record and not the parent record
  compiled <- addContext (defaultDom rtype) $ compileLocal $
    fmap concat $ traverse (compileFun False) defaults
  let declMap = Map.fromList [ (definedName c, def) | def@(Hs.FunBind _ (c : _)) <- compiled ]
  reportSDoc "agda2hs.record.min" 20 $
    text "Done compiling minimal record" <+> pretty m <+>
    text "defined fields: " <+> prettyList_ (map (text . pp) definedFields)
  return (definedFields, declMap)


compileMinRecords :: Definition -> [String] -> C [Hs.Decl ()]
compileMinRecords def sls = do

  members <- classMemberNames def

  qnames <- traverse resolveStringName sls
  (prims, defaults) <- unzip <$> traverse (compileMinRecord members) qnames

  -- 0. [OPTIONAL] check all record signatures match (or simply leave to GHC)

  -- 1. build minimal pragma

  let
    -- make the formula for a list of names of methods for for a single minimal instance
    helpAnd :: [Hs.Name ()] -> Hs.BooleanFormula ()
    helpAnd xs = Hs.AndFormula () $ Hs.VarFormula () <$> xs

    -- combine formulae for all minimal instances
    helpOr :: [Hs.BooleanFormula ()] -> Hs.Decl ()
    helpOr bs = Hs.MinimalPragma () (Just $ Hs.OrFormula () bs)

    minPragma = helpOr (map helpAnd prims)

  -- 2. assert that all default implementations are the same (for a certain field)
  let getUnique f (x :| xs)
        | all (x ==) xs = return x
        | otherwise     = agda2hsErrorM $ do
          text ("Conflicting default implementations for " ++ pp f ++ ":") $$
            vcat [ "-" <+> multilineText (pp d) | d <- nub (x : xs) ]
  decls <- Map.traverseWithKey getUnique
         $ Map.unionsWith (<>) $ (map . fmap) (:| []) defaults

  -- TODO: order default implementations differently?
  return ([minPragma | not (null prims)] ++ Map.elems decls)


compileRecord :: RecordTarget -> Definition -> C (Hs.Decl ())
compileRecord target def = do
  TelV tel _ <- telViewUpTo recPars (defType def)
  addContext tel $ do
    checkValidTypeName rName
    binds <- compileTeleBinds tel
    let hd = foldl (Hs.DHApp ()) (Hs.DHead () rName) binds
    let fieldTel = snd $ splitTelescopeAt recPars recTel
    case target of
      ToClass ms -> do
        when (length binds > 1) $ tellExtension Hs.MultiParamTypeClasses
        (classConstraints, classDecls) <- compileRecFields classDecl recFields fieldTel
        let context = case classConstraints of
              []     -> Nothing
              [asst] | not (isQuantifiedAsst asst) -> Just (Hs.CxSingle () asst)
              assts  -> Just (Hs.CxTuple () assts)
        defaultDecls <- compileMinRecords def ms
        return $ Hs.ClassDecl () context hd [] (Just (classDecls ++ map (Hs.ClsDecl ()) defaultDecls))
      ToRecord newtyp ds -> do
        checkValidConName cName
        when (theEtaEquality recEtaEquality' == YesEta) $ agda2hsErrorM $
          "Agda records compiled to Haskell should have eta-equality disabled." <+>
          "Add no-eta-equality to the definition of" <+> (text (pp rName) <> ".")
        (constraints, fieldDecls) <- compileRecFields fieldDecl recFields fieldTel
        when newtyp $ checkNewtypeCon cName fieldDecls
        let target = if newtyp then Hs.NewType () else Hs.DataType ()
        compileDataRecord constraints fieldDecls target hd ds

  where
    rName = hsName $ prettyShow $ qnameName $ defName def
    cName | recNamedCon = hsName $ prettyShow $ qnameName $ conName recConHead
          | otherwise   = rName   -- Reuse record name for constructor if no given name

    -- In Haskell, projections live in the same scope as the record type, so check here that the
    -- record module has been opened.
    checkFieldInScope f = isInScopeUnqualified f >>= \case
      True  -> return ()
      False -> setCurrentRangeQ f $ agda2hsErrorM $
        ("Record projections (`" <> prettyTCM (qnameName f) <>"` in this case)") <+>
        "must be brought into scope when compiling to Haskell record types." <+>
        "Add `open" <+> text (Hs.prettyPrint rName) <+> "public` after the record declaration to fix this."

    Record{..} = theDef def

    classDecl :: Hs.Name () -> Hs.Type () -> Hs.ClassDecl ()
    classDecl n = Hs.ClsDecl () . Hs.TypeSig () [n]

    fieldDecl :: Hs.Name () -> Hs.Type () -> Hs.FieldDecl ()
    fieldDecl n = Hs.FieldDecl () [n]

    compileRecFields :: (Hs.Name () -> Hs.Type () -> b)
                     -> [Dom QName] -> Telescope -> C ([Hs.Asst ()], [b])
    compileRecFields decl ns tel = case (ns, tel) of
      (_   , EmptyTel          ) -> return ([], [])
      (n:ns, ExtendTel dom tel') -> do
        hsDom <- withNestedType $ compileDomType (absName tel') dom
        (hsAssts, hsFields) <- underAbstraction dom tel' $ compileRecFields decl ns
        case hsDom of
          DomType s hsA -> do
            let fieldName = hsName $ prettyShow $ qnameName $ unDom n
            fieldType <- addTyBang s hsA
            checkValidFunName fieldName
            return (hsAssts, decl fieldName fieldType : hsFields)
          DomConstraint hsA -> case target of
            ToClass{} -> do
              when (isQuantifiedAsst hsA) $ tellExtension Hs.QuantifiedConstraints
              return (hsA : hsAssts , hsFields)
            ToRecord{} -> agda2hsError $
              "Not supported: record/class with constraint fields"
          DomDropped -> return (hsAssts , hsFields)
          DomForall{} -> __IMPOSSIBLE__
      (_, _) -> __IMPOSSIBLE__

    compileDataRecord
      :: [Hs.Asst ()]
      -> [Hs.FieldDecl ()] -- ^ compiled rec fields
      -> Hs.DataOrNew ()   -- ^ whether to compile to data or newtype
      -> Hs.DeclHead ()    -- ^ the head of the type declaration
      -> [Hs.Deriving ()]  -- ^ data extracted from the pragma
      -> C (Hs.Decl ())
    compileDataRecord constraints fieldDecls don hd ds = do
      unless (null constraints) __IMPOSSIBLE__ -- no constraints for records
      mapM_ checkFieldInScope (map unDom recFields)
      let conDecl = Hs.QualConDecl () Nothing Nothing $ Hs.RecDecl () cName fieldDecls
      return $ Hs.DataDecl () don Nothing hd [conDecl] ds

checkUnboxPragma :: Definition -> C ()
checkUnboxPragma def = do
  let Record{..} = theDef def

  -- recRecursive can be used again after agda 2.6.4.2 is released
  -- see agda/agda#7042
  unless (all null recMutual) $ agda2hsErrorM $
        "Unboxed record" <+> prettyTCM (defName def)
    <+> "cannot be recursive"

  TelV tel _ <- telViewUpTo recPars (defType def)
  addContext tel $ do
    pars <- getContextArgs
    let fieldTel = recTel `apply` pars
    fields <- nonErasedFields fieldTel
    unless (length fields == 1) $ agda2hsErrorM $
          "Unboxed record" <+> prettyTCM (defName def)
      <+> "should have exactly one non-erased field"

  where
    nonErasedFields :: Telescope -> C [String]
    nonErasedFields EmptyTel = return []
    nonErasedFields (ExtendTel a tel) = compileDom a >>= \case
      DODropped  -> underAbstraction a tel nonErasedFields
      DOType -> agda2hsError "Type field in unboxed record not supported"
      DOInstance -> agda2hsError "Instance field in unboxed record not supported"
      DOTerm -> (absName tel:) <$> underAbstraction a tel nonErasedFields