packages feed

ogma-core-1.13.0: src/Language/Trans/SpecAnalysis.hs

-- Copyright 2024 United States Government as represented by the Administrator
-- of the National Aeronautics and Space Administration. All Rights Reserved.
--
-- Disclaimers
--
-- Licensed under the Apache License, Version 2.0 (the "License"); you may
-- not use this file except in compliance with the License. You may obtain a
-- copy of the License at
--
--      https://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-- WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-- License for the specific language governing permissions and limitations
-- under the License.

-- | Formally analyze specifications and provide information about them.
module Language.Trans.SpecAnalysis
    ( AnalysisResult(..)
    , specAnalyze
    )
  where

-- External imports
import qualified Copilot.Core                 as Core
import qualified Copilot.Language             as Copilot
import qualified Copilot.Language.Reify       as Copilot
import           Copilot.Theorem.What4        (SatResult (..), Solver (Z3),
                                               prove)
import           Data.List                    (intercalate, lookup)
import           Data.Maybe                   (fromMaybe)
import qualified Language.Haskell.Interpreter as HI

-- External imports: auxiliary
import Data.String.Extra (sanitizeLCIdentifier, sanitizeUCIdentifier)

-- External imports: ogma-spec
import Data.OgmaSpec (ExternalVariableDef (..), InternalVariableDef (..),
                      Requirement (..), Spec (..))

-- * Analysis of Specs

-- | Result of analyzing a specification.
data AnalysisResult = AnalysisResult
  { numAlwaysTrue  :: Int  -- ^ Number of always true requirements.
  , numAlwaysFalse :: Int  -- ^ Number of always false requirements.
  , consistent     :: Bool -- ^ Whether requirements are mutually consistent.
  }

-- | Formally analyze a specification for redundancies, conflicts, etc.
specAnalyze :: [(String, String)]             -- Type substitution table
            -> ([(String, String)] -> a -> a) -- Expr substitution function
            -> (a -> String)                  -- Expr show function
            -> Spec a                         -- Specification
            -> IO (Either String AnalysisResult)
specAnalyze typeMaps exprTransform showExpr spec = do
  let structuredSpec =
        spec2Copilot typeMaps exprTransform showExpr spec

  coreSpec <- reifySpec defaultSpecImports $ showSpec structuredSpec

  let properties     = zip propertyNames propertyGuards
      propertyNames  = map (\(_, p, _, _, _) -> p)
                     $ copilotProperties structuredSpec
      propertyGuards = map Core.triggerGuard $ Core.specTriggers coreSpec

  constantProperties <- mapM (uncurry $ exprIsConstant coreSpec) properties

  let numTrue  = length $ filter fst constantProperties
      numFalse = length $ filter snd constantProperties

  let negatedConjunction = Core.Op1 Core.Not
                         $ foldr (Core.Op2 Core.And) true propertyGuards
      true               = Core.Const Core.Bool True

  provedNegatedConjunction <-
    exprIsConstant coreSpec "ogma_inc" negatedConjunction

  -- The requirements are considered consistent if it was *not* possible to
  -- prove that their conjunction is always false.
  let consistent = not $ fst provedNegatedConjunction

  return $ Right $ AnalysisResult numTrue numFalse consistent

-- * Auxiliary

-- ** Structured Copilot specifications

-- | A structured Copilot specification.
data CopilotSpec = CopilotSpec
  { copilotProperties :: [(String, String, String, String, String)]
                      -- ^ Requirement name, property name, handler name,
                      -- implementation and arguments.

  , copilotAuxDefs    :: [(String, String, String)]
                      -- ^ Name, type, implementation
  }

-- | Given a 'Spec', return a structured version the corresponding Copilot spec
-- that differentiates between the auxiliary definitions (inputs, outputs) and
-- the properties or requirements to check.
--
-- PRE: There are no name clashes between the variables and names used in the
-- specification and any definitions in Haskell's Prelude or in Copilot.
spec2Copilot :: [(String, String)]             -- Type substitution table
             -> ([(String, String)] -> a -> a) -- Expr substitution function
             -> (a -> String)                  -- Expr show function
             -> Spec a                         -- Specification
             -> CopilotSpec
spec2Copilot typeMaps exprTransform showExpr spec = CopilotSpec reqs auxDefs
  where
    -- Encoding of requirements as boolean streams
    reqs :: [(String, String, String, String, String)]
    reqs = map reqToDecl (requirements spec)
      where
        reqToDecl i =
            ( reqName
            , propName
            , handlerName
            , reqBody nameSubstitutions
            , handlerArg
            )
          where
            reqName = requirementName i

            propName = safeMap nameSubstitutions (requirementName i)

            handlerName = "handler" ++ sanitizeUCIdentifier (requirementName i)

            -- Definition implementation. We use an auxiliary function to
            -- transform the implementation into Copilot, applying a
            -- substitution.
            reqBody subs = showExpr (exprTransform subs (requirementExpr i))

            handlerArg  =
              case (requirementResultType i, requirementResultExpr i) of
                (Just _, Just ex) -> "[ arg (" ++ showExpr ex ++ " ) ]"
                _                 -> "[]"

    auxDefs :: [(String, String, String)]
    auxDefs = externs ++ internals

    externs :: [(String, String, String)]
    externs = map externVarToDecl (externalVariables spec)
      where
        externVarToDecl i = (propName, streamType, implementation)
          where
            propName = safeMap nameSubstitutions (externalVariableName i)

            streamType = "Stream " ++ "(" ++ valueType ++ ")"
            valueType  = safeMap typeMaps (externalVariableType i)

            implementation = "extern" ++ " " ++ show (externalVariableName i)
                          ++ " " ++ "Nothing"

    -- Internal stream definitions
    internals :: [(String, String, String)]
    internals = map internalVarToDecl (internalVariables spec)
      where
        internalVarToDecl i = (propName, streamType, implementation)
          where
            propName = safeMap nameSubstitutions (internalVariableName i)

            streamType = "Stream " ++ "(" ++ valueType ++ ")"
            valueType  = safeMap typeMaps (internalVariableType i)

            implementation = internalVariableExpr i

    nameSubstitutions = internalVariableMap
                     ++ externalVariableMap
                     ++ requirementNameMap

    -- Map from a variable name to its desired identifier in the code
    -- generated.
    internalVariableMap =
      map (\x -> (x, sanitizeLCIdentifier x)) internalVariableNames

    externalVariableMap =
      map (\x -> (x, sanitizeLCIdentifier x)) externalVariableNames

    requirementNameMap =
      map (\x -> (x, "prop" ++ sanitizeUCIdentifier x)) requirementNames

    -- Variable/requirement names used in the input spec.
    internalVariableNames = map internalVariableName
                          $ internalVariables spec

    externalVariableNames = map externalVariableName
                          $ externalVariables spec

    requirementNames = map requirementName
                     $ requirements spec

-- | Render a 'CopilotSpec' as a Haskell definition of a 'Copilot.Spec',
-- listing the elements in the spec with the necessary indentation.
--
-- The shown 'Copilot.Spec' has a list of top-level triggers, as well as
-- several auxiliary definitions.
showSpec :: CopilotSpec -> String
showSpec spec = template ++ "\n" ++ extra ++ "\n" ++ triggers
  where
    -- Initial template used for analysis purposes.
    template :: String
    template = unlines
      [ "do let"
      , ""
      , "       clock :: Stream Int64"
      , "       clock = [0] ++ (clock + 1)"
      , ""
      , "       ftp :: Stream Bool"
      , "       ftp = [True] ++ false"
      , ""
      , "       pre :: Stream Bool -> Stream Bool"
      , "       pre = ([False] ++)"
      , ""
      , "       tpre :: Stream Bool -> Stream Bool"
      , "       tpre = ([True] ++)"
      , ""
      , "       notPreviousNot :: Stream Bool -> Stream Bool"
      , "       notPreviousNot = not . PTLTL.previous . not"
      ]

    extra = unlines
          $ intercalate [""]
          $ map formatDef
          $ copilotAuxDefs spec

    triggers = unlines
             $ intercalate [""]
             $ map formatTrigger
             $ copilotProperties spec

    formatDef (n, t, i) =
      map ("       " ++) [ n ++ " :: " ++ t, n ++ " = " ++ i ]

    formatTrigger (_, _, h, g, a) =
      map ("   " ++) [ "trigger " ++ show h ++ " (" ++ g ++ ") " ++ a ]

-- | Default imports for a 'Spec' that was converted into a 'Copilot.Spec'.
defaultSpecImports :: [(String, Maybe String)]
defaultSpecImports =
  [ ("Control.Monad.Writer",  Nothing)
  , ("Copilot.Language",      Nothing)
  , ("Copilot.Language.Spec", Nothing)
  , ("Data.Functor.Identity", Nothing)
  , ("Language.Copilot",      Nothing)
  , ("Copilot.Library.PTLTL", Just "PTLTL")
  , ("Prelude",               Just "P")
  ]

-- ** Typechecking of Copilot specs

-- | Read a specification from a 'String' and reify it.
--
-- This function receives a list of possibly qualified imports.
reifySpec :: [(String, Maybe String)] -> String -> IO Core.Spec
reifySpec imports specText = do
  coreSpecE <- HI.runInterpreter $ do
                 HI.setImportsQ imports
                 copilotSpec <- HI.interpret specText (HI.as :: Copilot.Spec)
                 HI.liftIO $ Copilot.reify copilotSpec

  case coreSpecE of
    Left err -> do putStrLn $ "Error: " ++ show err
                   error $ show err

    Right coreSpec -> return coreSpec

-- ** Analysis of Copilot specs

-- | Determine if a boolean expression is always 'True' or always 'False'.
--
-- The first boolean in the result is 'True' if the expression can be proven
-- always 'True'. The second boolean in the expression is 'True' is the
-- expression can be proven always 'False'.
--
-- They values in the tuple cannot both 'True' at the same time.
exprIsConstant :: Core.Spec
               -> Core.Name
               -> Core.Expr Bool
               -> IO (Bool, Bool)
exprIsConstant spec name expr = do
  r1 <- propIsValid spec name (Core.Forall expr)
  r2 <- propIsValid spec name (Core.Forall (Core.Op1 Core.Not expr))
  pure (r1, r2)

-- | 'True' if the Copilot 'Prop' with the given name and expression is
-- constantly 'True', or valid, and 'False' otherwise (not always 'True' or
-- unknown).
propIsValid :: Core.Spec
            -> Core.Name
            -> Core.Prop
            -> IO Bool
propIsValid spec name expr =
    maybe False isValid . lookup name <$> prove Z3 spec'
  where
    spec' = spec { Core.specProperties = prop' : Core.specProperties spec }
    prop' = Core.Property name expr

    isValid :: SatResult -> Bool
    isValid Valid = True
    isValid _     = False

-- ** Auxiliary list functions

-- | Substitute a key based on a given substitution table from key to
-- alternative key.
--
-- They key is left unchanged if it cannot be found in the substitution table.
safeMap :: Eq k => [(k, k)] -> k -> k
safeMap ls k = fromMaybe k $ lookup k ls