packages feed

keiro-dsl-0.17.0.0: src/Keiro/Dsl/TransitionFamily.hs

-- | Deterministic, declaration-order-independent transition family comparison.
module Keiro.Dsl.TransitionFamily
  ( TransitionFamilyKey (..),
    TransitionFamilyDelta (..),
    ReplayBodyKey (..),
    ReplayBodyStatus (..),
    ReplayBodyDelta (..),
    transitionFamilyDeltas,
    replayBodyKey,
    replayBodyDeltas,
    guardAlternatives,
    guardUnion,
    guardImplies,
    unionPreserved,
  )
where

import Data.List (sortOn)
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict qualified as Map
import Data.Maybe (mapMaybe)
import Data.Set qualified as Set
import Data.Text (Text)
import Keiro.Dsl.CanonicalEncoding (canonicalExpr, canonicalTransition)
import Keiro.Dsl.Grammar
  ( Atom (..),
    Expr (..),
    Name,
    ScalarLiteral (..),
    Transition (..),
    TransitionMode (..),
    complementExpr,
    noLoc,
  )

-- | The structural identity shared by ordinary diff and replay-impact analysis.
data TransitionFamilyKey = TransitionFamilyKey
  { familyMode :: !TransitionMode,
    familySource :: !Name,
    familyCommand :: !Name
  }
  deriving stock (Eq, Show)

instance Ord TransitionFamilyKey where
  compare left right =
    compare
      (modeRank ((.familyMode) left), (.familySource) left, (.familyCommand) left)
      (modeRank ((.familyMode) right), (.familySource) right, (.familyCommand) right)

-- | The exact old and new remainders after duplicate-aware cancellation.
data TransitionFamilyDelta = TransitionFamilyDelta
  { familyKey :: !TransitionFamilyKey,
    oldRemainder :: ![Transition],
    newRemainder :: ![Transition]
  }
  deriving stock (Eq, Show)

-- | Frozen replay behavior within one family, excluding guard, mode, location,
-- and forward-only outcomes.
newtype ReplayBodyKey = ReplayBodyKey {unReplayBodyKey :: Text}
  deriving stock (Eq, Ord, Show)

-- | Directional classification of the complete old and new guard unions for
-- one replay body.
data ReplayBodyStatus
  = ReplayBodyPreserved
  | ReplayBodyChanged
  | ReplayBodyRemoved
  | ReplayBodyAdded
  deriving stock (Eq, Ord, Show)

-- | One affected replay body. Presence is represented by the member lists;
-- within a present body, an absent guard means the Boolean constant true.
data ReplayBodyDelta = ReplayBodyDelta
  { bodyFamilyKey :: !TransitionFamilyKey,
    bodyKey :: !ReplayBodyKey,
    oldBodyMembers :: ![Transition],
    newBodyMembers :: ![Transition],
    bodyStatus :: !ReplayBodyStatus
  }
  deriving stock (Eq, Show)

-- | Partition transitions into ordered families and cancel exact canonical
-- matches as a multiset. Fully cancelled families remain in the result.
transitionFamilyDeltas :: [Transition] -> [Transition] -> [TransitionFamilyDelta]
transitionFamilyDeltas oldTransitions newTransitions =
  [ let (oldRemainder, newRemainder) =
          cancelExact
            (Map.findWithDefault [] key oldFamilies)
            (Map.findWithDefault [] key newFamilies)
     in TransitionFamilyDelta {familyKey = key, oldRemainder, newRemainder}
  | key <- Set.toAscList (Map.keysSet oldFamilies <> Map.keysSet newFamilies)
  ]
  where
    oldFamilies = transitionFamilies oldTransitions
    newFamilies = transitionFamilies newTransitions

-- | Classify complete guard unions for replay bodies selected by an emitting
-- exact remainder. Cancelled siblings remain members of the recovered body.
replayBodyDeltas :: [Transition] -> [Transition] -> [ReplayBodyDelta]
replayBodyDeltas oldTransitions newTransitions =
  concatMap classifyFamily (transitionFamilyDeltas oldTransitions newTransitions)
  where
    oldFamilies = transitionFamilies oldTransitions
    newFamilies = transitionFamilies newTransitions

    classifyFamily familyDelta =
      [ let oldMembers = membersFor key body oldFamilies
            newMembers = membersFor key body newFamilies
         in ReplayBodyDelta
              { bodyFamilyKey = key,
                bodyKey = body,
                oldBodyMembers = oldMembers,
                newBodyMembers = newMembers,
                bodyStatus = classifyBody oldMembers newMembers
              }
      | body <- affectedBodies familyDelta
      ]
      where
        key = (.familyKey) familyDelta

    affectedBodies familyDelta =
      Set.toAscList . Set.fromList $
        [ replayBodyKey transition
        | transition <- (.oldRemainder) familyDelta <> (.newRemainder) familyDelta,
          not (null transition.emits)
        ]

    membersFor family body families =
      sortOn
        canonicalTransition
        [ transition
        | transition <- Map.findWithDefault [] family families,
          replayBodyKey transition == body
        ]

    classifyBody [] (_ : _) = ReplayBodyAdded
    classifyBody (_ : _) [] = ReplayBodyRemoved
    classifyBody oldMembers newMembers
      | Just oldGuards <- NE.nonEmpty (map (.guard) oldMembers),
        Just newGuards <- NE.nonEmpty (map (.guard) newMembers),
        unionPreserved oldGuards newGuards =
          ReplayBodyPreserved
      | otherwise = ReplayBodyChanged

-- | Derive a body key through the frozen transition encoding rather than a
-- second expression or transition renderer.
replayBodyKey :: Transition -> ReplayBodyKey
replayBodyKey transition =
  ReplayBodyKey . canonicalTransition $
    Transition
      { source = transition.source,
        command = transition.command,
        implementation = transition.implementation,
        guard = Nothing,
        writes = transition.writes,
        emits = transition.emits,
        outcome = Nothing,
        outcomeDuplicateLocs = [],
        goto = transition.goto,
        mode = TmLive,
        loc = noLoc
      }

-- | Canonical, comparison-local alternatives for a present guard union.
-- 'Nothing' represents true and absorbs every other alternative.
guardAlternatives :: NonEmpty (Maybe Expr) -> NonEmpty (Maybe Expr)
guardAlternatives guards
  | any isTrueGuard guards = Nothing :| []
  | otherwise =
      case NE.nonEmpty (map Just normalized) of
        Just alternatives -> alternatives
        Nothing -> Nothing :| []
  where
    normalized =
      Map.elems . Map.fromList $
        [ (canonicalExpr alternative, alternative)
        | alternative <- concatMap (maybe [] flattenOr) (NE.toList guards)
        ]

    flattenOr (EOr left right) = flattenOr left <> flattenOr right
    flattenOr expression = [expression]

-- | Build the normalized union. The result is 'Nothing' exactly when the
-- union is true; explicit false remains an ordinary alternative.
guardUnion :: NonEmpty (Maybe Expr) -> Maybe Expr
guardUnion guards =
  case guardAlternatives guards of
    Nothing :| [] -> Nothing
    Just first :| rest -> Just (foldr1 EOr (first : mapMaybe id rest))
    _ -> Nothing

-- | Prove that every old alternative remains accepted by the new union.
unionPreserved :: NonEmpty (Maybe Expr) -> NonEmpty (Maybe Expr) -> Bool
unionPreserved oldGuards newGuards =
  canonicalUnion oldAlternatives == canonicalUnion newAlternatives
    || all (`guardImplies` newUnion) oldAlternatives
  where
    oldAlternatives = NE.toList (guardAlternatives oldGuards)
    newAlternatives = NE.toList (guardAlternatives newGuards)
    newUnion = guardUnion newGuards
    canonicalUnion = maybe "" canonicalExpr . guardUnion . NE.fromList

-- | A deliberately small implication fragment used by diff and replay impact.
-- Unknown shapes return false so analysis stays conservative.
guardImplies :: Maybe Expr -> Maybe Expr -> Bool
guardImplies _ Nothing = True
guardImplies Nothing (Just newGuard) = isTrueExpr newGuard
guardImplies (Just oldGuard) (Just newGuard) = implies oldGuard newGuard
  where
    implies old new
      | old == new = True
    implies old _ | isFalseExpr old = True
    implies _ new | isTrueExpr new = True
    implies (EAnd left right) new = implies left new || implies right new
    implies old (EOr (EAnd leftA rightA) (EAnd leftB rightB))
      | canonicalExpr leftA == canonicalExpr leftB =
          implies old leftA && implies old (EOr rightA rightB)
    implies _ (EOr left right) | complementary left right = True
    implies old (EOr left right) = implies old left || implies old right
    implies _ _ = False
    complementary left right =
      canonicalExpr (complementExpr left) == canonicalExpr right
        || canonicalExpr left == canonicalExpr (complementExpr right)

isTrueGuard :: Maybe Expr -> Bool
isTrueGuard Nothing = True
isTrueGuard (Just expression) = isTrueExpr expression

isTrueExpr :: Expr -> Bool
isTrueExpr (EAtom (ABool True)) = True
isTrueExpr (ELiteral _ (LiteralBool True)) = True
isTrueExpr _ = False

isFalseExpr :: Expr -> Bool
isFalseExpr (EAtom (ABool False)) = True
isFalseExpr (ELiteral _ (LiteralBool False)) = True
isFalseExpr _ = False

transitionFamilies :: [Transition] -> Map.Map TransitionFamilyKey [Transition]
transitionFamilies =
  Map.fromListWith (flip (<>))
    . map (\transition -> (transitionFamilyKey transition, [transition]))

transitionFamilyKey :: Transition -> TransitionFamilyKey
transitionFamilyKey transition =
  TransitionFamilyKey
    { familyMode = transition.mode,
      familySource = transition.source,
      familyCommand = transition.command
    }

modeRank :: TransitionMode -> Int
modeRank TmLive = 0
modeRank TmReplayOnly = 1

cancelExact :: [Transition] -> [Transition] -> ([Transition], [Transition])
cancelExact oldTransitions newTransitions = go sortedOld sortedNew [] []
  where
    sortedOld = sortOn canonicalTransition oldTransitions
    sortedNew = sortOn canonicalTransition newTransitions
    go [] remainingNew unmatchedOld unmatchedNew =
      (reverse unmatchedOld, reverse unmatchedNew <> remainingNew)
    go remainingOld [] unmatchedOld unmatchedNew =
      (reverse unmatchedOld <> remainingOld, reverse unmatchedNew)
    go old@(oldTransition : remainingOld) new@(newTransition : remainingNew) unmatchedOld unmatchedNew =
      case compare (canonicalTransition oldTransition) (canonicalTransition newTransition) of
        LT -> go remainingOld new (oldTransition : unmatchedOld) unmatchedNew
        EQ -> go remainingOld remainingNew unmatchedOld unmatchedNew
        GT -> go old remainingNew unmatchedOld (newTransition : unmatchedNew)