camfort-1.0: src/Camfort/Specification/Units/Analysis/Consistent.hs
{- |
Module : Camfort.Specification.Units.Analysis.Consistent
Description : Analysis to verify units are consistent.
Copyright : (c) 2017, Dominic Orchard, Andrew Rice, Mistral Contrastin, Matthew Danish
License : Apache-2.0
Maintainer : dom.orchard@gmail.com
Stability : experimental
-}
{-# LANGUAGE ExistentialQuantification #-}
module Camfort.Specification.Units.Analysis.Consistent
( ConsistencyError
, ConsistencyReport(Consistent, Inconsistent)
, checkUnits
) where
import Camfort.Analysis (ExitCodeOfReport(..), Describe(..))
import Camfort.Specification.Units.Analysis (UnitAnalysis, runInference)
import qualified Camfort.Specification.Units.Annotation as UA
import qualified Camfort.Specification.Units.BackendTypes as B
import Camfort.Specification.Units.Environment
import Camfort.Specification.Units.InferenceBackend (inconsistentConstraints)
import Camfort.Specification.Units.Monad
import Control.DeepSeq
import Control.Monad.Reader (asks)
import Control.Monad.State (get)
import Data.Generics.Uniplate.Operations
import Data.List (partition, find, group, sort)
import qualified Data.Map.Strict as M
import Data.Maybe (maybeToList, maybe)
import qualified Language.Fortran.AST as F
import qualified Language.Fortran.Util.Position as FU
-- | A report that summarises unit consistency.
data ConsistencyReport
-- | All units were consistent.
= forall a. Consistent (F.ProgramFile a) Int
-- | An inconsistency was found in units of the program.
| Inconsistent ConsistencyError
instance NFData ConsistencyReport where
rnf _ = ()
instance Show ConsistencyReport where
show (Consistent pf nVars) = concat ["\n", fname, ": Consistent ", show nVars, " variables checked."]
where fname = F.pfGetFilename pf
show (Inconsistent e) = show e
instance ExitCodeOfReport ConsistencyReport where
exitCodeOf (Consistent {}) = 0
exitCodeOf (Inconsistent _) = 1
instance Describe ConsistencyReport
data ConsistencyError =
Inconsistency (F.ProgramFile UA) Constraints
instance Show ConsistencyError where
show (Inconsistency pf cons) = concat [ "\n", fname, ": Inconsistent:\n", reportErrors ]
where
fname = F.pfGetFilename pf
reportErrors = unlines [ maybe "" showSS ss ++ str | (ss, str) <- reports ]
where
reports = map head . group . sort . map reportError . filter relevantConstraints $ cons
showSS = (++ ": ") . (" - at "++) . showSpanStart
relevantConstraints c = not (isPolymorphic0 c) && not (isReflexive c)
isPolymorphic0 (ConEq UnitParamLitAbs{} _) = True
isPolymorphic0 (ConEq _ UnitParamLitAbs{}) = True
isPolymorphic0 _ = False
isReflexive (ConEq u1 u2) = u1 == u2
isReflexive _ = error "isReflexive without ConEq"
reportError con = (errSpan, pprintConstr . orient . unrename . shift . simplify $ con)
where
errSpan = findCon con
orient (ConEq u v) | 0 `elem` [unitPower u, unitPower v] = balanceConEq ((< 0) . unitPower) (ConEq u v)
orient (ConEq u (UnitVar v)) = ConEq (UnitVar v) u
orient (ConEq u (UnitParamVarUse v)) = ConEq (UnitParamVarUse v) u
orient (ConEq u v)
| all ((< 0) . unitPower) $ lhs ++ rhs = orient $ ConEq (foldUnits (negateCons lhs)) (foldUnits (negateCons rhs))
where
lhs = flattenUnits u
rhs = flattenUnits v
orient c = c
-- partitionUnits f u = (foldUnits a, foldUnits b)
-- where (a, b) = partition f (flattenUnits u)
unitPower (UnitPow u k) = unitPower u * k
unitPower UnitlessLit = 0
unitPower UnitlessVar = 0
unitPower _ = 1
-- When reporting inconsistent constraints, shift all the
-- UnitNames (e.g. m, kg) and Polymorphic Units (e.g. 'a,
-- 'b) to the right-hand-side, and other things to the left.
shift = shiftConEq isUnitRHS
-- Units that should appear on the right-hand-side of the matrix during solving
isUnitRHS (UnitPow (UnitName _) _) = True
isUnitRHS (UnitPow (UnitParamEAPAbs _) _) = True
isUnitRHS _ = False
simplify = B.dimToConstraint . B.constraintToDim
findCon :: Constraint -> Maybe FU.SrcSpan
findCon con = lookupWith (eq con) constraints
where -- constraintToDim normalises as it builds the Dim, so we can use dimParamEq directly.
eq c1 c2 = or [ B.constraintToDim c1 `B.dimParamEq` B.constraintToDim c2' | c2' <- universeBi c2 ]
constraints = [ (c, srcSpan)
| x <- universeBi pf :: [F.Expression UA]
, let srcSpan = FU.getSpan x
, c <- maybeToList (UA.getConstraint x)
] ++
[ (c, srcSpan)
| x <- universeBi pf :: [F.Statement UA]
, let srcSpan = FU.getSpan x
, c <- maybeToList (UA.getConstraint x)
] ++
[ (c, srcSpan)
| x <- universeBi pf :: [F.Argument UA]
, let srcSpan = FU.getSpan x
, c <- maybeToList (UA.getConstraint x)
] ++
[ (c, srcSpan)
| x <- universeBi pf :: [F.Declarator UA]
, let srcSpan = FU.getSpan x
, c <- maybeToList (UA.getConstraint x)
] ++
-- Why reverse? So that PUFunction and PUSubroutine appear
-- first in the list, before PUModule.
reverse [ (c, srcSpan)
| x <- universeBi pf :: [F.ProgramUnit UA]
, let srcSpan = FU.getSpan x
, c <- maybeToList (UA.getConstraint x)
]
instance Describe ConsistencyError
{-| Check units-of-measure for a program -}
checkUnits :: UnitAnalysis ConsistencyReport
checkUnits = do
pf <- asks unitProgramFile
(eCons, state) <- runInference runInconsistentConstraints
-- number of 'real' variables checked, e.g. not parametric
let
nVars = M.size . M.filter (not . isParametricUnit) $ usVarUnitMap state
pfUA :: F.ProgramFile UA
pfUA = usProgramFile state -- the program file after units analysis is done
pure $!! case eCons of
Nothing -> Consistent pf nVars
(Just cons) -> Inconsistent $ Inconsistency pfUA cons
where
isParametricUnit u = case u of UnitParamPosAbs {} -> True; UnitParamPosUse {} -> True
UnitParamVarAbs {} -> True; UnitParamVarUse {} -> True
_ -> False
lookupWith :: (a -> Bool) -> [(a,b)] -> Maybe b
lookupWith f = fmap snd . find (f . fst)
-- | Return a possible list of unsolvable constraints.
runInconsistentConstraints :: UnitSolver (Maybe Constraints)
runInconsistentConstraints = do
cons <- usConstraints `fmap` get
pure $ inconsistentConstraints cons
-- clear out the unique names in the UnitInfos.
unrename :: Data a => a -> a
unrename = transformBi $ \ x -> case x of
UnitVar (_, s) -> UnitVar (s, s)
UnitParamVarAbs ((_, f), (_, s)) -> UnitParamVarAbs ((f, f), (s, s))
UnitParamVarUse ((_, f), (_, s), i) -> UnitParamVarUse ((f, f), (s, s), i)
UnitParamEAPAbs (_, s) -> UnitParamEAPAbs (s, s)
UnitParamEAPUse ((_, s), i) -> UnitParamEAPUse ((s, s), i)
u -> u
-- | Show only the start position of the 'SrcSpan'.
showSpanStart :: FU.SrcSpan -> String
showSpanStart (FU.SrcSpan l _) = show l
-- | Shift terms to the right if predicate f is satisfied and to the left otherwise.
shiftConEq :: (UnitInfo -> Bool) -> Constraint -> Constraint
shiftConEq f (ConEq l r) = ConEq (foldUnits (lhsOk ++ negateCons rhsShift)) (foldUnits (rhsOk ++ negateCons lhsShift))
where
(lhsOk, lhsShift) = partition (not . f) (flattenUnits l)
(rhsOk, rhsShift) = partition f (flattenUnits r)
shiftConEq f (ConConj cs) = ConConj $ map (shiftConEq f) cs
-- | Balance equations by shifting terms that satisfy predicate f
balanceConEq :: (UnitInfo -> Bool) -> Constraint -> Constraint
balanceConEq f (ConEq l r) = ConEq (foldUnits (lhsOk ++ negateCons rhsShift)) (foldUnits (rhsOk ++ negateCons lhsShift))
where
(lhsShift, lhsOk) = partition f (flattenUnits l)
(rhsShift, rhsOk) = partition f (flattenUnits r)
balanceConEq f (ConConj cs) = ConConj $ map (balanceConEq f) cs
negateCons :: [UnitInfo] -> [UnitInfo]
negateCons = map (\ x -> case x of
UnitPow u k -> UnitPow u (-k)
u -> UnitPow u (-1))