packages feed

camfort-0.905: src/Camfort/Specification/Hoare/CheckBackend.hs

{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE FunctionalDependencies     #-}
{-# LANGUAGE GADTs                      #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE RankNTypes                 #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# OPTIONS_GHC -Wall #-}

module Camfort.Specification.Hoare.CheckBackend
  ( AnnotatedProgramUnit(..)
  , apuPreconditions
  , apuPostconditions
  , apuPU
  , apuAuxDecls
  , BackendAnalysis
  , HoareCheckResult(..)
  , HoareBackendError(..)
  , checkPU
  ) where

import           Control.Exception                      (Exception (..))
import           Control.Lens
import           Control.Monad.Reader
import           Control.Monad.State.Strict
import           Control.Monad.Writer.Strict
import           Control.Monad.Trans.Maybe
import           Data.Data                              (Data)
import           Data.Foldable                          (foldlM)
import           Data.Generics.Uniplate.Operations      (childrenBi,
                                                         transformBi)
import           Data.Map                               (Map)
import qualified Data.Map                               as Map
import           Data.Maybe                             (isJust, maybeToList)
import           Data.Void                              (Void)

import           Data.SBV                               (SBool, defaultSMTCfg)

import qualified Language.Fortran.Analysis              as F
import qualified Language.Fortran.AST                   as F
import qualified Language.Fortran.LValue                as F
import qualified Language.Fortran.Util.Position         as F

import           Camfort.Analysis
import           Camfort.Analysis.Logger                (Builder, Text)
import           Camfort.Helpers.TypeLevel
import           Camfort.Specification.Hoare.Annotation
import           Camfort.Specification.Hoare.Syntax
import           Camfort.Specification.Hoare.Translate
import           Language.Fortran.Model
import           Language.Fortran.Model.Repr.Prim
import           Language.Fortran.Model.Translate
import           Language.Fortran.Model.Vars

import           Language.Expression
import           Language.Expression.Choice
import           Language.Expression.Pretty
import           Language.Expression.Prop
import           Language.Verification
import           Language.Verification.Conditions

--------------------------------------------------------------------------------
--  Data types
--------------------------------------------------------------------------------

data AnnotatedProgramUnit =
  AnnotatedProgramUnit
  { _apuPreconditions  :: [PrimFormula InnerHA]
  , _apuPostconditions :: [PrimFormula InnerHA]
  , _apuAuxDecls       :: [AuxDecl InnerHA]
  , _apuPU             :: F.ProgramUnit HA
  }

data AnnotationError
  = MissingWhileInvariant
  -- ^ The while block had no associated invariant
  | MissingSequenceAnn
  -- ^ A sequence annotation was required but not found

data HoareBackendError
  = VerifierError (VerifierError FortranVar)
  | TranslateErrorAnn TranslateError
  -- ^ Unit errors come from translating annotation formulae
  | TranslateErrorSrc TranslateError
  -- ^ HA errors come from translating actual source Fortran
  | InvalidSourceName SourceName
  -- ^ A program source name had no unique name
  | UnsupportedBlock (F.Block HA)
  -- ^ Found a block that we don't know how to deal with
  | UnexpectedBlock (F.Block HA)
  -- ^ Found a block in an illegal place
  | ArgWithoutDecl NamePair
  -- ^ Found an argument that didn't come with a variable declaration
  | AuxVarConflict F.Name
  -- ^ An auxiliary variable name conflicted with a program source name
  | AssignVarNotInScope NamePair
  -- ^ The variable was referenced in an assignment but not in scope
  | WrongAssignmentType Text SomeType
  -- ^ Expected array type but got the given type instead
  | NonLValueAssignment
  -- ^ Assigning to an expression that isn't an lvalue
  | UnsupportedAssignment Text
  -- ^ Tried to assign to something that's valid Fortran but unsupported
  | AnnotationError AnnotationError
  -- ^ There was a problem with the annotations

instance Describe AnnotationError where
  describeBuilder =
    \case
      MissingSequenceAnn ->
        "the program was insufficiently annotated; " <>
        "`seq` annotation required before this block"
      MissingWhileInvariant ->
        "found a `do while` block with no invariant; " <>
        "invariant annotations must appear at the start of every `do while` loop"

instance Describe HoareBackendError where
  describeBuilder =
    \case
      VerifierError e ->
        "verifier error: " <> describeBuilder (displayException e)
      TranslateErrorAnn te ->
        "translation error in logic annotation: " <> describeBuilder te
      TranslateErrorSrc te ->
        "translation error in source code: " <> describeBuilder te
      InvalidSourceName nm ->
        "a program source name had no associated unique name: " <>
        describeBuilder (pretty nm)
      UnsupportedBlock _ -> "encountered unsupported block"
      UnexpectedBlock _ -> "a block was found in an illegal location"
      ArgWithoutDecl nm ->
        "argument " <> describeBuilder (show nm) <>
        " doesn't have an associated type declaration"
      AuxVarConflict nm ->
        "auxiliary variable " <> describeBuilder nm <>
        " has the same name as a program variable; this is not allowed"
      AnnotationError e -> describeBuilder e
      AssignVarNotInScope nm ->
        "variable " <> describeBuilder (pretty nm) <>
        " is being assigned to but is not in scope"
      WrongAssignmentType message gotType ->
        "unexpected variable type; expected " <> describeBuilder message <>
        "; got " <>
        describeBuilder (pretty gotType)
      NonLValueAssignment ->
        "assignment an expression which is not a valid lvalue"
      UnsupportedAssignment message ->
        "unsupported assignment; " <> describeBuilder message

type HoareBackendWarning = Void
type BackendAnalysis = AnalysisT HoareBackendError HoareBackendWarning IO

data HoareCheckResult = HoareCheckResult (F.ProgramUnit HA) Bool
  deriving (Show)

instance ExitCodeOfReport HoareCheckResult where
  exitCodeOf (HoareCheckResult _ r) = if r then 0 else 1

describePuName :: F.ProgramUnitName -> Builder
describePuName (F.Named n)         = describeBuilder n
describePuName F.NamelessBlockData = "<nameless block data>"
describePuName F.NamelessComment   = "<nameless comment>"
describePuName F.NamelessMain      = "<nameless main>"

instance Describe HoareCheckResult where
  describeBuilder (HoareCheckResult pu result) =
    "Program unit '" <> describePuName (F.puSrcName pu) <> "': " <>
    (if result then "verified!" else "unverifiable!")

type ScopeVars = Map UniqueName SomeVar

data CheckHoareEnv =
  CheckHoareEnv
  { _heImplicitVars   :: Bool
  , _heVarsInScope    :: ScopeVars
    -- ^ The variables in scope. Associates unique names with name pairs and types.
  , _heSourceToUnique :: Map SourceName [UniqueName]
    -- ^ The corresponding unique names for all the source names we have seen.
  , _heReprHandler    :: forall p k a. Prim p k a -> PrimReprHandler a
  , _hePU             :: F.ProgramUnit HA
  }

emptyEnv :: F.ProgramUnit HA -> PrimReprSpec -> CheckHoareEnv
emptyEnv pu spec = CheckHoareEnv True mempty mempty (makeSymRepr spec) pu

makeLenses ''AnnotatedProgramUnit
makeLenses ''CheckHoareEnv

instance HasPrimReprHandlers CheckHoareEnv where
  primReprHandler = view heReprHandler

--------------------------------------------------------------------------------
--  Main function
--------------------------------------------------------------------------------

checkPU :: AnnotatedProgramUnit
        -> PrimReprSpec
        -> BackendAnalysis HoareCheckResult
checkPU apu symSpec = do

  let pu = apu ^. apuPU

  -- The first part of the checking process has a mutable 'CheckHoareEnv' in
  -- 'StateT' as it collects information to add to the environment.
  ((bodyTriple, initialAssignments), env) <- flip runStateT (emptyEnv pu symSpec) $ do
    logInfo' pu $ " - Setting up"

    (body, initialAssignments) <- initialSetup

    unless (null initialAssignments) $
      logDebug' pu $
      "Found " <> describeShow (length initialAssignments) <>
      " initial assignments: " <> describeShow (map pretty initialAssignments)

    addAuxVariables apu

    let translatePUFormulae =
            readerOfState
          . traverse (tryTranslateFormula pu)

    preconds <- translatePUFormulae (apu ^. apuPreconditions)
    postconds <- translatePUFormulae (apu ^. apuPostconditions)

    logInfo' pu $ " - Interpreting pre- and postconditions"

    let precond = propAnd preconds
        postcond = propAnd postconds

    logInfo' pu $ " - Found preconditions: "  <> describe (pretty precond)
    logInfo' pu $ " - Found postconditions: " <> describe (pretty postcond)

    -- Modify the postcondition by substituting in variable values from the
    -- initial assignments
    let postcond' = chainSub postcond initialAssignments

    return ((precond, postcond', body), initialAssignments)

  -- The second part has an immutable 'CheckHoareEnv' in 'ReaderT'.
  flip runReaderT env $ do
    logInfo' pu $ " - Computing verification conditions"
    (_, vcs) <- runGenM (genBody' initialAssignments bodyTriple)

    logInfo' pu $ " - Verifying conditions:"

    let checkVcs _ [] = return True
        checkVcs i (vc : rest) = do
          logInfo' pu $ "   " <> describeShow i <> ". " <> describe (pretty vc)
          result <- verifyVc (failAnalysis' pu) vc
          if result
            then checkVcs (1 + i) rest
            else do
            logInfo' pu "    - Failed!"
            zipWithM_ printUnchecked [(1 + i)..] vcs
            return False

        printUnchecked i vc = do
          logInfo' pu $ "   " <> describeShow i <> ". " <> describe (pretty vc)
          logInfo' pu   "    - Unchecked"

    HoareCheckResult pu <$> checkVcs (1 :: Int) vcs

--------------------------------------------------------------------------------
--  Variables and names
--------------------------------------------------------------------------------

varOfType :: NamePair -> SomeType -> SomeVar
varOfType names (Some d) = Some (FortranVar d names)


expNamePair :: F.Expression (F.Analysis a) -> NamePair
expNamePair = NamePair <$> UniqueName . F.varName <*> SourceName . F.srcName


functionNamePair :: F.ProgramUnit (F.Analysis a) -> NamePair
functionNamePair =
  NamePair <$> UniqueName . fromPuName . F.puName
           <*> SourceName . fromPuName . F.puSrcName
  where
    fromPuName (F.Named n) = n
    fromPuName _           = error "impossible: function has no name"


-- TODO: Consider reporting a warning when two variables have the same source
-- name.

-- | Create a variable in scope with the given name and type.
newVar :: NamePair -> SomeType -> CheckHoareEnv -> CheckHoareEnv
newVar np@(NamePair uniq src) ty
  = (heVarsInScope . at uniq .~ Just (varOfType np ty))
  . (heSourceToUnique . at src %~ \case
        Nothing -> Just [uniq]
        Just xs -> Just (uniq : xs))


-- | In specifications attached to program units (pre- and post-conditions), the
-- @fortran-src@ renamer doesn't have access to a renaming environment so it
-- doesn't assign the right unique names. Once we have access to unique names
-- from inside the program unit, this function assigns those names to variables
-- in the PU specifications.
setFormulaUniqueNames
  :: (Data ann)
  => Map SourceName [UniqueName]
  -> PrimFormula (F.Analysis ann)
  -> PrimFormula (F.Analysis ann)
setFormulaUniqueNames nameMap = transformBi setExpUN
  where
    setExpUN :: F.Expression InnerHA -> F.Expression InnerHA
    setExpUN = do
      np <- realNamePair <$> expNamePair
      F.modifyAnnotation (setAnnUniq (np ^. npUnique . _Wrapped))

    realNamePair np@(NamePair _ src) =
      -- TODO: How sound is it to take the first available? Should I throw
      -- warnings?
      case nameMap ^? ix src . _Cons . _1 of
        Just uniq -> NamePair uniq src
        Nothing   -> np

    setAnnUniq uniq a = a { F.uniqueName = Just uniq }

--------------------------------------------------------------------------------
--  Check Monad
--------------------------------------------------------------------------------

type CheckHoareMut = StateT CheckHoareEnv BackendAnalysis
type CheckHoare = ReaderT CheckHoareEnv BackendAnalysis

type FortranAssignment = Assignment MetaExpr FortranVar

-- | Sets up the environment for checking the program unit, including reading
-- past variable declarations. Returns the assignments made in variable
-- declarations, and blocks after the variable declarations.
initialSetup :: CheckHoareMut ([F.Block HA], [FortranAssignment])
initialSetup = do
  pu <- use hePU
  let body = childrenBi pu :: [F.Block HA]

  -- If the program unit is a function, we might need to treat its name as a
  -- variable with its return type.

  -- If the program is a function or subroutine, it might have arguments that we
  -- need to treat as variables.
  rawArgNames <- case pu of
    F.PUFunction _ _ (Just rettype) _ _ funargs retvalue _ _ -> do
      rettype' <- readerOfState $ tryTranslateTypeInfo (typeInfo rettype)

      let retNames = case retvalue  of
            Just rv -> expNamePair rv
            Nothing -> functionNamePair pu

      modify $ newVar retNames rettype'

      return (maybe [] F.aStrip funargs)

    F.PUSubroutine _ _ _ _ subargs _ _ -> return (maybe [] F.aStrip subargs)
    _ -> return []

  let argNames = map expNamePair rawArgNames

  (restBody, initialAssignments) <- readInitialBlocks body

  -- Verify that all argument names have types associated with them.
  forM_ argNames $ \argName -> do
    hasType <- isJust <$> use (heVarsInScope . at (argName ^. npUnique))
    unless hasType $ failAnalysis' pu (ArgWithoutDecl argName)

  return (restBody, initialAssignments)


-- | Uses the auxiliary variable declaration annotations to add auxiliary
-- variables into scope.
addAuxVariables :: AnnotatedProgramUnit -> CheckHoareMut ()
addAuxVariables apu =
  forM_ (apu ^. apuAuxDecls) $ \auxDecl -> do
    let nm = auxDecl ^. adName
    uniqNm <- uniqueAux nm

    let srcNm = SourceName nm
    sourceToUnique <- use heSourceToUnique

    -- Make sure auxiliary variable source names don't conflict with other
    -- variables (including other auxiliary variables).
    when (srcNm `Map.member` sourceToUnique) $
      failAnalysis' (apu ^. apuPU) $ AuxVarConflict nm

    ty <- readerOfState . tryTranslateTypeInfo . typeInfo $ auxDecl ^. adTy
    modify $ newVar (NamePair uniqNm srcNm) ty
  where
    -- This a bit of a hack: keep prepending underscores until we arrive at a
    -- unique name that hasn't been used yet.
    uniqueAux nm = do
      varsInScope <- use heVarsInScope
      return $ UniqueName
             . head
             . dropWhile ((`Map.member` varsInScope) . UniqueName)
             . iterate (' ' :)
             $ nm


-- | As part of the initial setup, reads setup blocks like declarations and
-- implicit statements. Updates the environment accordingly. Returns the rest of
-- the blocks, after the setup blocks.
readInitialBlocks :: [F.Block HA] -> CheckHoareMut ([F.Block HA], [FortranAssignment])
readInitialBlocks = runWriterT . dropWhileM readInitialBlock
  where
    -- This function returns 'True' if the block may be part of the setup, and
    -- 'False' otherwise.
    readInitialBlock :: F.Block HA -> WriterT [FortranAssignment] CheckHoareMut Bool
    readInitialBlock bl = case bl of
      F.BlStatement _ _ _ st ->
        case st of
          F.StDeclaration _ _ astTypeSpec attrs decls -> do
            -- This is the part of the type info that applies to every variable
            -- in the declaration list.
            let topTypeInfo =
                  typeInfo astTypeSpec &
                  tiAttributes .~ attrs

            -- Each variable may have extra information that modifies its type info
            declVarsTis <- forM (F.aStrip decls) $ \case
              F.DeclVariable _ _ nameExp declLength mInitialValue -> do
                return (nameExp,
                        topTypeInfo
                        & tiDeclaratorLength .~ declLength,
                        mInitialValue)

              F.DeclArray _ _ nameExp declDims declLength mInitialValue ->
                return (nameExp,
                        topTypeInfo
                        & tiDeclaratorLength .~ declLength
                        & tiDimensionDeclarators .~ Just declDims,
                        mInitialValue)

            forM_ declVarsTis $ \(varNameExp, varTypeInfo, mInitialValue) -> do
              let varNames = expNamePair varNameExp

              varType <- readerOfState $ tryTranslateTypeInfo varTypeInfo

              -- Put the new variable in scope
              modify $ newVar varNames varType

              -- Record the assignment if there is one. NB this must be done
              -- after putting the variable in scope, because making an
              -- assignment checks that it is in scope.
              tell =<< traverse (readerOfState . simpleAssignment varNames)
                          (maybeToList mInitialValue)

            return True

          F.StImplicit _ _ Nothing -> do
            -- TODO: Deal with implicits properly
            return True
          F.StImplicit _ _ (Just _) -> failAnalysis' bl (UnsupportedBlock bl)
          _ -> return False

      -- Skip comments that don't have sequence annotations
      F.BlComment{} | Nothing <- getBlockSeqAnnotation bl -> return True
      _ -> return False


verifyVc :: (HoareBackendError -> CheckHoare Bool) -> MetaFormula Bool -> CheckHoare Bool
verifyVc handle prop = do
  let getSrProp :: HighRepr Bool -> SBool
      getSrProp (HRHigh x) = x
      getSrProp (HRCore _) = error "absurd"

  let debug = False
      cfg | debug = defaultSMTCfg { verbose = True, transcript = Just "transcript.smt2" }
          | otherwise = defaultSMTCfg

  env <- asks primReprHandlers

  let res = query (getSrProp <$> runReaderT (evalProp' lift (pure . HRCore) prop) env) env
  res' <- liftIO . runVerifierWith cfg $ res

  case res' of
    Right b -> return b
    Left e  -> handle (VerifierError e)

--------------------------------------------------------------------------------
--  Generation Monad
--------------------------------------------------------------------------------

-- | The verification condition generation monad. A writer of meta-formulae with
-- an immutable 'CheckHoareEnv'.
newtype GenM a = GenM (WriterT [MetaFormula Bool] CheckHoare a)
  deriving
    ( Functor
    , Applicative
    , Monad
    , MonadReader CheckHoareEnv
    , MonadWriter [MetaFormula Bool]
    , MonadLogger HoareBackendError HoareBackendWarning
    , MonadAnalysis HoareBackendError HoareBackendWarning
    )


runGenM :: GenM a -> CheckHoare (a, [MetaFormula Bool])
runGenM (GenM action) = runWriterT action


type FortranTriplet a = Triplet MetaExpr FortranVar a


genBody' :: [FortranAssignment] -> FortranTriplet [F.Block HA] -> GenM ()
genBody' as (precond, postcond, body) = do
  let seqL = JustAssign as
  seqR <- bodyToSequence body

  case seqL `joinAnnSeq` seqR of
    Just x -> void $ sequenceVCs genBlock (precond, postcond, x)
    Nothing -> failAnalysis' body $ AnnotationError MissingSequenceAnn


genBody :: FortranTriplet [F.Block HA] -> GenM ()
genBody = genBody' []


genBlock :: FortranTriplet (F.Block HA) -> GenM ()
genBlock (precond, postcond, bl) = do
  case bl of
    F.BlIf _ _ _ _ conds bodies _ -> do
      condsExprs <- traverse (traverse tryTranslateBoolExpr) conds
      multiIfVCs genBody expr (precond, postcond, (zip condsExprs bodies))

    F.BlDoWhile _ _ _ _ _ cond body _ -> do
      primInvariant <-
        case body of
         b : _ | Just (SodSpec (Specification SpecInvariant f))
                 <- getAnnSod (F.getAnnotation b)
                 -> return f
         _ -> failAnalysis' bl $ AnnotationError MissingWhileInvariant

      invariant <- tryTranslateFormula body primInvariant
      condExpr <- tryTranslateBoolExpr cond

      whileVCs genBody expr invariant (precond, postcond, (condExpr, body))

    F.BlComment _ _ _ -> return ()
    _ -> failAnalysis' bl $ UnsupportedBlock bl


bodyToSequence :: [F.Block HA] -> GenM (AnnSeq MetaExpr FortranVar (F.Block HA))
bodyToSequence blocks = do
  foldlM combineBlockSequence emptyAnnSeq blocks


combineBlockSequence
  :: AnnSeq MetaExpr FortranVar (F.Block HA)
  -> F.Block HA
  -> GenM (AnnSeq MetaExpr FortranVar (F.Block HA))
combineBlockSequence prevSeq bl = do
  blSeq <- blockToSequence bl

  case prevSeq `joinAnnSeq` blSeq of
    Just r  -> return r
    Nothing -> failAnalysis' bl $ AnnotationError MissingSequenceAnn


blockToSequence :: F.Block HA -> GenM (AnnSeq MetaExpr FortranVar (F.Block HA))
blockToSequence bl = do
  chooseFrom [assignment, sequenceSpec, other]
  where
    assignment = fmap (JustAssign . (: [])) <$> tryBlockToAssignment bl

    sequenceSpec =
      traverse
      (fmap propAnnSeq . tryTranslateFormula bl)
      (getBlockSeqAnnotation bl)

    other = return $ case bl of
      F.BlComment{} -> Just emptyAnnSeq
      _             -> Just $ cmdAnnSeq bl

    -- Tries each action in the list, using the first that works and otherwise
    -- reporting an error.
    chooseFrom :: [GenM (Maybe a)] -> GenM a
    chooseFrom =
      (>>= fromMaybeM (failAnalysis' bl $ AnnotationError MissingSequenceAnn)) .
      runMaybeT . msum . map MaybeT


getBlockSeqAnnotation :: F.Block HA -> Maybe (PrimFormula InnerHA)
getBlockSeqAnnotation = preview (to F.getAnnotation . to getAnnSod . _Just . _SodSpec . _SpecSeq)

--------------------------------------------------------------------------------
-- Handling assignments


tryBlockToAssignment
  :: ( MonadReader CheckHoareEnv m
     , MonadAnalysis HoareBackendError HoareBackendWarning m
     )
  => F.Block HA -> m (Maybe FortranAssignment)
tryBlockToAssignment bl = do
  case bl of
    F.BlStatement _ _ _ stAst@(F.StExpressionAssign _ _ lexp rvalue) ->
      Just <$> do
        lvalue <- fromMaybeM (failAnalysis' lexp NonLValueAssignment)
                  (F.toLValue lexp)

        case lvalue of
          F.LvSimpleVar {} -> simpleAssignment (expNamePair lexp) rvalue

          F.LvSubscript _ _ lvar@(F.LvSimpleVar {}) ixs ->
            case ixs of
              F.AList _ _ [F.IxSingle _ _ _ ixExpr] ->
                arrayAssignment (lvVarNames lvar) ixExpr rvalue

              _ -> failAnalysis' ixs $
                   UnsupportedAssignment "only simple indices are supported for now"
          _ -> failAnalysis' stAst $
               UnsupportedAssignment "complex assignment"

    _ -> return Nothing


-- | Create an assignment where the whole value is written to. TODO: this
-- currently only supports primitive values.
simpleAssignment
  :: ( ReportAnn (F.Analysis ann)
     , MonadReader CheckHoareEnv m
     , MonadAnalysis HoareBackendError HoareBackendWarning m
     )
  => NamePair
  -> F.Expression (F.Analysis ann)
  -> m FortranAssignment
simpleAssignment nm rvalAst = do
  Some varV@(FortranVar varD _) <- varFromScope rvalAst nm

  case varD of
    DPrim _ -> do
      rvalExpr <- tryTranslateCoerceExpr varD rvalAst
      return (Assignment varV rvalExpr)
    _ -> failAnalysis' rvalAst $
         WrongAssignmentType
         "primitive value (others unsupported for now)"
         (Some varD)

arrayAssignment
  :: ( ReportAnn (F.Analysis ann)
     , MonadReader CheckHoareEnv m
     , MonadAnalysis HoareBackendError HoareBackendWarning m
     )
  => NamePair
  -> F.Expression (F.Analysis ann)
  -> F.Expression (F.Analysis ann)
  -> m FortranAssignment
arrayAssignment arrName ixAst rvalAst = do
  Some varV@(FortranVar varD _) <- varFromScope rvalAst arrName

  case varD of
    DArray ixIndex valAv -> do
      let ixD = dIndex ixIndex
          valD = dArrValue valAv

      ixExpr   <- intoMetaExpr <$> tryTranslateExpr ixD ixAst
      rvalExpr <- intoMetaExpr <$> tryTranslateExpr valD rvalAst

      -- Replace instances of the array variable with the same array, but with
      -- the new value written at the given index.
      let arrExpr = HFree' $ HPure varV
          arrExpr' = hwrap' $ MopWriteArr varD arrExpr ixExpr rvalExpr

      return (Assignment varV arrExpr')

    _ -> failAnalysis' rvalAst $ WrongAssignmentType "array type" (Some varD)

--------------------------------------------------------------------------------

varFromScope
  :: ( F.Spanned a
     , MonadReader CheckHoareEnv m
     , MonadAnalysis HoareBackendError HoareBackendWarning m
     )
  => a -> NamePair -> m SomeVar
varFromScope loc np = do
  let uniq = np ^. npUnique
  mscoped <- view (heVarsInScope . at uniq)
  case mscoped of
    Just v  -> return v
    Nothing -> failAnalysis' loc $ AssignVarNotInScope np

--------------------------------------------------------------------------------
--  Translation
--------------------------------------------------------------------------------

class Show a => ReportAnn a where
  fromTranslateError :: proxy a -> TranslateError -> HoareBackendError

instance ReportAnn HA      where fromTranslateError _ = TranslateErrorSrc
instance ReportAnn InnerHA where fromTranslateError _ = TranslateErrorAnn


doTranslate
  :: (MonadReader CheckHoareEnv m, ReportAnn ann)
  => (HoareBackendError -> m a) -> (f ann -> TranslateT m a) -> f ann -> m a
doTranslate handle trans ast = do
  env <- asks toTranslateEnv
  transResult <- runTranslateT (trans ast) env
  case transResult of
    Right x  -> return x
    Left err -> handle (fromTranslateError ast err)


toTranslateEnv :: CheckHoareEnv -> TranslateEnv
toTranslateEnv env =
  defaultTranslateEnv
    & teImplicitVars .~ env ^. heImplicitVars
    & teVarsInScope .~ env ^. heVarsInScope

--------------------------------------------------------------------------------
-- Shorthands for translating expressions and failing the analysis if the
-- translation fails

tryTranslateExpr
  :: ( ReportAnn (F.Analysis ann)
     , MonadReader CheckHoareEnv m
     , MonadAnalysis HoareBackendError w m
     )
  => D a -> F.Expression (F.Analysis ann) -> m (FortranExpr a)
tryTranslateExpr d e = doTranslate (failAnalysis' e) (translateExpression' d) e

tryTranslateCoerceExpr
  :: ( ReportAnn (F.Analysis ann)
     , MonadReader CheckHoareEnv m
     , MonadAnalysis HoareBackendError w m
     )
  => D a -> F.Expression (F.Analysis ann) -> m (MetaExpr FortranVar a)
tryTranslateCoerceExpr d e =
  doTranslate (failAnalysis' e)
  (fmap squashExpression . translateCoerceExpression d) e

tryTranslateTypeInfo
  :: ( ReportAnn (F.Analysis ann)
     , MonadReader CheckHoareEnv m
     , MonadAnalysis HoareBackendError w m
     )
  => TypeInfo (F.Analysis ann) -> m SomeType
tryTranslateTypeInfo ti = doTranslate (failAnalysis' ti) translateTypeInfo ti

tryTranslateBoolExpr
  :: ( ReportAnn (F.Analysis ann)
     , MonadReader CheckHoareEnv m
     , MonadAnalysis HoareBackendError w m
     )
  => F.Expression (F.Analysis ann) -> m (MetaExpr FortranVar Bool)
tryTranslateBoolExpr e = doTranslate (failAnalysis' e) translateBoolExpression e

tryTranslateFormula
  :: ( F.Spanned o
     , ReportAnn (F.Analysis ann)
     , Data ann
     , MonadReader CheckHoareEnv m
     , MonadAnalysis HoareBackendError w m
     )
  => o -> PrimFormula (F.Analysis ann) -> m (MetaFormula Bool)
tryTranslateFormula loc formula = do
  sourceToUnique <- view heSourceToUnique
  -- TODO: Instead of setting unique names before translation, can we get the
  -- renamer to work inside annotations? I've tried to make this work but ran
  -- into some problems:
  -- - We have to run the renamer before calling 'annotateComments' or it
  --   doesn't find any comments (why?).
  -- - When the renamer is run /after/ calling 'annotateComemnts' it doesn't do
  --   any renaming inside annotations (why?)
  let formulaUN = setFormulaUniqueNames sourceToUnique formula
  doTranslate (failAnalysis' loc) translateFormula formulaUN

--------------------------------------------------------------------------------
--  Utility functions
--------------------------------------------------------------------------------

dropWhileM :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
dropWhileM _ [] = return []
dropWhileM f (x : xs) = do
  continue <- f x
  if continue
    then dropWhileM f xs
    else return (x : xs)


fromMaybeM :: (Monad m) => m a -> Maybe a -> m a
fromMaybeM e = maybe e return


getAnnSod :: HA -> Maybe (SpecOrDecl InnerHA)
getAnnSod = view (to F.prevAnnotation . hoareSod)


lvVarNames :: F.LValue (F.Analysis x) -> NamePair
lvVarNames e =
  let uniqueName = UniqueName $ F.lvVarName e
      srcName = SourceName $ F.lvSrcName e
  in NamePair uniqueName srcName


readerOfState :: (MonadState s m) => ReaderT s m a -> m a
readerOfState action = do
  st <- get
  runReaderT action st