packages feed

keiro-dsl-0.21.0.0: src/Keiro/Dsl/FoldEvidence.hs

{-# OPTIONS_GHC -Werror=incomplete-patterns #-}

-- | Canonical identities for the aggregate fold surface used while hydrating
-- event streams. The fingerprint deliberately excludes payload codecs,
-- projections, snapshot policy, and source locations: those inputs do not change
-- how an existing event log becomes aggregate state.
module Keiro.Dsl.FoldEvidence
  ( FoldSurfaceError (..),
    renderFoldSurfaceError,
    aggregateFoldFingerprintForService,
    aggregateFoldSurfaceForService,
    FoldEvidence (..),
    BodyEvidence (..),
    aggregateFoldEvidenceForService,
  )
where

import Data.Aeson qualified as Aeson
import Data.ByteString.Lazy qualified as LazyBytes
import Data.List (find)
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict qualified as Map
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding qualified as TextEncoding
import Keiro.Codec.IdDomain (v5OrV7IdDomainVersion)
import Keiro.Dsl.AggregateType
import Keiro.Dsl.CanonicalEncoding (canonicalExpr, foldFingerprint128)
import Keiro.Dsl.EventOutput
import Keiro.Dsl.Expression
import Keiro.Dsl.Grammar
import Keiro.Dsl.LanguageVersion (RuntimeCapability (NominalEqualityV2), runtimeProfileHasCapability)
import Keiro.Dsl.NominalType
import Keiro.Dsl.SemanticContract (CheckedService, EffectiveLanguageContract (..), checkedLanguageContract, checkedSpec, checkedTypeGraph, runtimeSemanticsFingerprintSegments)
import Keiro.Dsl.TransitionFamily (ReplayBodyKey, TransitionFamilyKey (..), replayBodyKey)
import Keiro.Dsl.TypeGraph

-- | A checked service can retain language provenance before semantic
-- validation succeeds. Fold identity therefore reports every resolution
-- failure instead of silently omitting or inventing canonical segments.
data FoldSurfaceError
  = FoldTypeGraphResolutionFailed !Text
  | FoldNominalResolutionFailed !Text
  | FoldRegisterTypeResolutionFailed !Name !Text
  | FoldRegisterInitialResolutionFailed !Name !Text
  | FoldGuardResolutionFailed !Name !Name !Text
  | FoldWriteResolutionFailed !Name !Name !Text
  | FoldEventOutputResolutionFailed !Name !Name !Name !Text
  deriving stock (Eq, Show)

renderFoldSurfaceError :: FoldSurfaceError -> Text
renderFoldSurfaceError = \case
  FoldTypeGraphResolutionFailed detail -> "aggregate fold type-graph resolution failed: " <> detail
  FoldNominalResolutionFailed detail -> "aggregate fold nominal resolution failed: " <> detail
  FoldRegisterTypeResolutionFailed registerName detail ->
    "aggregate fold register '" <> registerName <> "' type resolution failed: " <> detail
  FoldRegisterInitialResolutionFailed registerName detail ->
    "aggregate fold register '" <> registerName <> "' initial resolution failed: " <> detail
  FoldGuardResolutionFailed aggregateName commandName detail ->
    "aggregate fold guard resolution failed for '" <> aggregateName <> "." <> commandName <> "': " <> detail
  FoldWriteResolutionFailed aggregateName registerName detail -> "aggregate fold write resolution failed for " <> aggregateName <> "." <> registerName <> ": " <> detail
  FoldEventOutputResolutionFailed aggregateName commandName eventName detail ->
    "aggregate fold output resolution failed for '"
      <> aggregateName
      <> "."
      <> commandName
      <> "' emitting '"
      <> eventName
      <> "': "
      <> detail

-- | The thirty-two-hex-digit identity of an aggregate's replay fold under the
-- service's effective runtime semantics.
aggregateFoldFingerprintForService :: CheckedService -> Aggregate -> Either FoldSurfaceError Text
aggregateFoldFingerprintForService service = fmap foldFingerprint128 . aggregateFoldSurfaceForService service

-- | Canonical pre-hash text under a checked semantic contract. A runtime
-- discriminator is included only for a contract that can change fold behavior;
-- source declaration provenance and grammar-only versions never enter it.
aggregateFoldSurfaceForService :: CheckedService -> Aggregate -> Either FoldSurfaceError Text
aggregateFoldSurfaceForService service aggregate =
  fmap (T.intercalate "\n" . (.canonicalSegments)) (aggregateFoldEvidenceForService service aggregate)

-- | Comparison-only evidence; never serialized as persisted fold identity.
-- Every segment is resolved against the real aggregate's own checked service.
data FoldEvidence = FoldEvidence
  { canonicalSegments :: ![Text],
    aggregateSegments :: ![Text],
    eventSegments :: !(Map.Map Name (Set Text)),
    bodySegments :: !(Map.Map (TransitionFamilyKey, ReplayBodyKey) BodyEvidence)
  }

data BodyEvidence = BodyEvidence
  { outputSegments :: !(Set Text),
    ruleSegments :: !(Map.Map Name Text),
    equalitySegments :: !(Map.Map Name (Set Text)),
    guardEqualitySegments :: !(Map.Map Text (Set Text)),
    emittedEvents :: !(Set Name)
  }

aggregateFoldEvidenceForService :: CheckedService -> Aggregate -> Either FoldSurfaceError FoldEvidence
aggregateFoldEvidenceForService service aggregate = do
  graph <- mapLeft (FoldTypeGraphResolutionFailed . showText) (checkedTypeGraph service)
  let symbols = aggregateSymbolsFromGraph graph spec
  nominalRegistry <- mapLeft (FoldNominalResolutionFailed . showText) (resolveNominalTypes spec)
  registerSegments <- traverse (registerSegment symbols) ((.regs) aggregate)
  equalityUses <- nominalEqualityUses graph service aggregate ((.transitions) aggregate)
  transitionSegments <- traverse (transitionSegment graph spec aggregate) ((.transitions) aggregate)
  bodies <- traverse (bodyEvidence graph) ((.transitions) aggregate)
  let global =
        runtimeSemanticsFingerprintSegments (checkedLanguageContract service)
          ++ map stateSegment ((.states) aggregate)
          ++ registerSegments
          ++ mappedRegisterSegments graph
      directNominals =
        [ (eventName, segment)
        | (eventName, useSite, typeName) <- nominalUseNames aggregate,
          Just nominal <- [lookupNominalType typeName nominalRegistry],
          segment <- nominalUseSegments useSite nominal
        ]
      nestedNominals =
        [ (eventNameFor path, nestedNominalUseSegment path leaf)
        | leaf <- Map.elems ((.nominalLeaves) graph),
          path <- nominalUsePaths graph ((.name) leaf),
          isNestedAggregateFoldUse ((.name) aggregate) path
        ]
      nominals = directNominals ++ nestedNominals
  pure
    FoldEvidence
      { canonicalSegments =
          global
            ++ map snd directNominals
            ++ map snd nestedNominals
            ++ ["nominal-equality-use:" <> identity | identity <- Set.toAscList (Set.fromList (map snd equalityUses))]
            ++ transitionSegments
            ++ map ruleSegment (referencedRules ((.transitions) aggregate)),
        aggregateSegments = global ++ [segment | (Nothing, segment) <- nominals],
        eventSegments =
          Map.union
            (Map.fromListWith Set.union [(name, Set.singleton segment) | (Just name, segment) <- nominals])
            (Map.fromList [((.name) event, Set.empty) | event <- (.events) aggregate]),
        bodySegments = Map.fromListWith mergeBodyEvidence bodies
      }
  where
    spec = checkedSpec service
    referencedRules transitions =
      [ rule
      | rule <- (.rules) spec,
        (.name) rule `Set.member` referencedRuleNames spec transitions
      ]
    bodyEvidence graph transition = do
      resolvedOutputs <- transitionOutputs graph spec aggregate transition
      let outputs =
            [ eventName <> "=" <> case mapping of
                HandOwnedEventOutput {} -> "hand-owned"
                GeneratedCommandIdentity {} -> eventOutputCanonical mapping
            | (eventName, mapping) <- resolvedOutputs
            ]
      equalities <- nominalEqualityUses graph service aggregate [transition]
      guardEvidence <- case transition.guard of
        Nothing -> Right Map.empty
        Just syntax -> case resolveGuardExpr (expressionEnvironmentFromGraph graph spec aggregate transition) syntax of
          Right expression -> Right (equalitySites (checkedLanguageContract service) syntax expression)
          Left detail
            | runtimeProfileHasCapability ((.runtimeProfile) (checkedLanguageContract service)) NominalEqualityV2 ->
                Left (FoldGuardResolutionFailed aggregate.name transition.command (showText detail))
            | otherwise -> Right Map.empty
      pure
        ( (TransitionFamilyKey transition.mode transition.source transition.command, replayBodyKey transition),
          BodyEvidence
            { outputSegments = Set.fromList outputs,
              ruleSegments = Map.fromList [((.name) rule, ruleSegment rule) | rule <- referencedRules [transition]],
              equalitySegments = Map.fromListWith Set.union [(name, Set.singleton identity) | (name, identity) <- equalities],
              guardEqualitySegments = guardEvidence,
              emittedEvents = Set.fromList transition.emits
            }
        )
    mappedRegisterSegments graph =
      [ mappedRegisterSegment graph declaration
      | register <- (.regs) aggregate,
        TRef typeName <- [(.valueType) register],
        Just declaration <- [Map.lookup (MappedKey typeName) ((.declarations) graph)]
      ]
    eventNameFor path = case (.root) path of
      RootEventField _ eventName _ -> Just eventName
      _ -> Nothing

mergeBodyEvidence :: BodyEvidence -> BodyEvidence -> BodyEvidence
mergeBodyEvidence left right =
  BodyEvidence
    { outputSegments = left.outputSegments <> right.outputSegments,
      ruleSegments = Map.union left.ruleSegments right.ruleSegments,
      equalitySegments = Map.unionWith Set.union left.equalitySegments right.equalitySegments,
      guardEqualitySegments = Map.unionWith Set.union left.guardEqualitySegments right.guardEqualitySegments,
      emittedEvents = left.emittedEvents <> right.emittedEvents
    }

-- | Nested nominal leaves participate in an aggregate fold wherever persisted
-- event payloads or snapshot registers contain their enclosing structural
-- declaration.  The structural register's own initial binding constructs the
-- whole value, so a nested nominal's @initial@ symbol is deliberately absent.
isNestedAggregateFoldUse :: Name -> UsePath -> Bool
isNestedAggregateFoldUse aggregateName UsePath {root, segments} =
  isNested segments && case root of
    RootEventField owner _ _ -> owner == aggregateName
    RootRegister owner _ -> owner == aggregateName
    RootCommandField {} -> False
    RootWorkqueueField {} -> False
    RootReadModelQueryInput {} -> False
    RootReadModelQueryResult {} -> False
    RootContractField {} -> False
  where
    isNested (SegDecl {} : _) = True
    isNested _ = False

nestedNominalUseSegment :: UsePath -> NominalLeaf -> Text
nestedNominalUseSegment path leaf =
  T.intercalate
    "|"
    ( [ "nested-nominal-use:" <> renderUsePath path,
        "name=" <> (.name) leaf,
        "representation=" <> nominalLeafRepresentationSegment ((.kind) leaf)
      ]
        <> ownershipSegments ((.ownership) leaf)
    )
  where
    ownershipSegments GeneratedLeaf = ["owner=generated"]
    ownershipSegments (ConsumerLeaf binding) =
      [ "owner=consumer",
        "canonical=" <> unCanonicalTypeId ((.canonical) binding),
        "binding=" <> unQualifiedValueName ((.binding) binding),
        "binding-version=" <> unBindingVersion ((.bindingVersion) binding)
      ]

nominalLeafRepresentationSegment :: NominalLeafKind -> Text
nominalLeafRepresentationSegment = \case
  NominalIdLeaf prefix admission -> idRepresentationSegment prefix admission
  NominalEnumLeaf constructors ->
    "enum:" <> T.intercalate "," [constructor <> "=" <> wire | (constructor, wire) <- NE.toList constructors]
  NominalScalarLeaf representation -> case representation of
    NominalText -> "Text"
    NominalInt -> "Int"
    NominalNatural -> "Natural"
    NominalBool -> "Bool"
    NominalTime -> "Time"

-- | Equality representation belongs in the fold identity only when a guard
-- actually compares that declaration. This keeps unrelated binding metadata out
-- of replay compatibility while ensuring a witness/domain change cannot silently
-- retain the old fold fingerprint.
nominalEqualityUses :: TypeGraph -> CheckedService -> Aggregate -> [Transition] -> Either FoldSurfaceError [(Name, Text)]
nominalEqualityUses graph service aggregate transitions =
  if runtimeProfileHasCapability ((.runtimeProfile) (checkedLanguageContract service)) NominalEqualityV2
    then fmap concat (traverse transitionIdentities transitions)
    else
      Right
        [ identity
        | transition <- transitions,
          guardSyntax <- maybeToList ((.guard) transition),
          Right guardExpression <- [resolveGuardExpr (expressionEnvironmentFromGraph graph spec aggregate transition) guardSyntax],
          identity <- equalityIdentities (checkedLanguageContract service) guardExpression
        ]
  where
    spec = checkedSpec service
    transitionIdentities transition = case (.guard) transition of
      Nothing -> Right []
      Just guardSyntax -> do
        guardExpression <-
          mapLeft
            (FoldGuardResolutionFailed ((.name) aggregate) ((.command) transition) . showText)
            (resolveGuardExpr (expressionEnvironmentFromGraph graph spec aggregate transition) guardSyntax)
        pure (equalityIdentities (checkedLanguageContract service) guardExpression)

equalityIdentities :: EffectiveLanguageContract -> TypedScalarExpr -> [(Name, Text)]
equalityIdentities languageContract expression =
  current <> children
  where
    current = case (.node) expression of
      TypedEqual left _ -> equalityIdentity left
      TypedNotEqual left _ -> equalityIdentity left
      _ -> []
    equalityIdentity operand = case (.valueType) operand of
      AggregateNominal nominal -> [((.name) nominal, identity) | identity <- maybeToList (nominalEqualityIdentityForService languageContract nominal)]
      _ -> []
    children = case (.node) expression of
      TypedMappedLift _ _ child -> equalityIdentities languageContract child
      TypedMappedInitial {} -> []
      TypedLiteral {} -> []
      TypedRoot {} -> []
      TypedProject {} -> []
      TypedAdd _ left right -> recurse left right
      TypedSubtract _ left right -> recurse left right
      TypedMultiply _ left right -> recurse left right
      TypedEqual left right -> recurse left right
      TypedNotEqual left right -> recurse left right
      TypedCompare _ left right -> recurse left right
      TypedAnd left right -> recurse left right
      TypedOr left right -> recurse left right
    recurse left right = equalityIdentities languageContract left <> equalityIdentities languageContract right

-- Compare retained equality expressions even when a surrounding guard is
-- loosened or split. Empty evidence matters: a nominal-to-primitive edit must
-- not disappear just because the new expression no longer uses a nominal.
equalitySites :: EffectiveLanguageContract -> Expr -> TypedScalarExpr -> Map.Map Text (Set Text)
equalitySites contract syntax expression =
  Map.unionsWith Set.union (current : zipWith (equalitySites contract) syntaxChildren typedChildren)
  where
    current = case expression.node of
      TypedEqual {} -> site
      TypedNotEqual {} -> site
      _ -> Map.empty
    site = Map.singleton (canonicalExpr syntax) (Set.fromList (map snd (equalityIdentities contract expression)))
    syntaxChildren = case syntax of
      EWriteMapped _ child -> [child]
      EOr left right -> [left, right]
      EAnd left right -> [left, right]
      ECmp _ left right -> [left, right]
      EAdd _ left right -> [left, right]
      ESubtract _ left right -> [left, right]
      EMultiply _ left right -> [left, right]
      _ -> []
    typedChildren = case expression.node of
      TypedMappedLift _ _ child -> [child]
      TypedOr left right -> [left, right]
      TypedAnd left right -> [left, right]
      TypedEqual left right -> [left, right]
      TypedNotEqual left right -> [left, right]
      TypedCompare _ left right -> [left, right]
      TypedAdd _ left right -> [left, right]
      TypedSubtract _ left right -> [left, right]
      TypedMultiply _ left right -> [left, right]
      _ -> []

nominalUseNames :: Aggregate -> [(Maybe Name, Text, Name)]
nominalUseNames aggregate =
  [ (Nothing, "register:" <> (.name) register, typeName)
  | register <- (.regs) aggregate,
    TRef typeName <- [(.valueType) register]
  ]
    <> [ (Just ((.name) event), "event:" <> (.name) event <> "." <> (.name) field, typeName)
       | event <- (.events) aggregate,
         field <- eventFields event,
         TRef typeName <- maybe [] pure ((.valueType) field)
       ]
  where
    eventFields event = case (.body) event of
      EventFields fields -> fields
      EventFromCommand commandName -> concat [(.fields) command | command <- (.commands) aggregate, (.name) command == commandName]

nominalUseSegment :: Text -> ResolvedNominalType -> ConsumerNominalBinding -> Text
nominalUseSegment useSite nominal binding =
  T.intercalate
    "|"
    [ "nominal-use:" <> useSite,
      "name=" <> (.name) nominal,
      "representation=" <> nominalRepresentationSegment ((.representation) nominal),
      "canonical=" <> unCanonicalTypeId ((.canonical) binding),
      "binding=" <> unQualifiedValueName ((.binding) binding),
      "binding-version=" <> unBindingVersion ((.bindingVersion) binding),
      "initial=" <> maybe "(none)" unQualifiedValueName ((.initial) binding)
    ]

-- Released generated IDs contributed no direct-use segment. Preserve that
-- exact surface for implicit v7 while making the candidate wider domain visible
-- on direct registers and event fields. Consumer-owned nominal segments already
-- existed and now carry the selected domain through their representation.
nominalUseSegments :: Text -> ResolvedNominalType -> [Text]
nominalUseSegments useSite nominal = case (.ownership) nominal of
  ConsumerNominal binding -> [nominalUseSegment useSite nominal binding]
  GeneratedNominal -> case (.representation) nominal of
    IdRepresentation _ TypeIdV5OrV7 ->
      [ T.intercalate
          "|"
          [ "nominal-use:" <> useSite,
            "name=" <> (.name) nominal,
            "representation=" <> nominalRepresentationSegment ((.representation) nominal),
            "owner=generated"
          ]
      ]
    _ -> []

nominalRepresentationSegment :: NominalRepresentation -> Text
nominalRepresentationSegment representation = case representation of
  IdRepresentation prefix admission -> idRepresentationSegment prefix admission
  EnumRepresentation constructors -> "enum:" <> T.intercalate "," [constructor <> "=" <> wire | (constructor, wire) <- NE.toList constructors]
  ScalarRepresentation scalar -> case scalar of
    NominalText -> "Text"
    NominalInt -> "Int"
    NominalNatural -> "Natural"
    NominalBool -> "Bool"
    NominalTime -> "Time"

idRepresentationSegment :: Text -> IdAdmission -> Text
idRepresentationSegment prefix TypeIdV7 = "id:" <> prefix
idRepresentationSegment prefix TypeIdV5OrV7 = "id:" <> prefix <> ";domain=" <> v5OrV7IdDomainVersion

mappedRegisterSegment :: TypeGraph -> ResolvedMappedDecl -> Text
mappedRegisterSegment graph (ResolvedStructural declaration _) =
  T.intercalate
    "|"
    [ "mapped-register:" <> (.name) declaration,
      "wire=" <> wireFingerprint graph ((.name) declaration),
      "canonical=" <> unCanonicalTypeId ((.canonical) declaration),
      "binding=" <> unQualifiedValueName ((.binding) declaration),
      "binding-version=" <> unBindingVersion ((.bindingVersion) declaration),
      "initial=" <> maybe "(missing)" unQualifiedValueName ((.initial) declaration)
    ]
mappedRegisterSegment _ (ResolvedOpaque declaration) =
  T.intercalate
    "|"
    [ "mapped-register:" <> (.name) declaration,
      "codec=" <> unCodecIdentity ((.codecIdentity) declaration),
      "codec-version=" <> unCodecVersion ((.codecVersion) declaration),
      "initial=" <> maybe "(missing)" unQualifiedValueName ((.initial) declaration)
    ]

stateSegment :: StateDecl -> Text
stateSegment state =
  "state:"
    <> (.name) state
    <> "|terminal="
    <> if (.terminal) state then "true" else "false"

registerSegment :: AggregateSymbols -> RegDecl -> Either FoldSurfaceError Text
registerSegment symbols register = do
  resolvedType <-
    mapLeft
      (FoldRegisterTypeResolutionFailed ((.name) register) . showText)
      (resolveAggregateType symbols ((.loc) register) RegisterUse ((.valueType) register))
  resolvedInitial <-
    mapLeft
      (FoldRegisterInitialResolutionFailed ((.name) register) . showText)
      (resolveRegisterInitial symbols ((.loc) register) resolvedType ((.initial) register))
  pure
    ( "reg:"
        <> (.name) register
        <> ":"
        <> typeExprCanonicalName ((.valueType) register)
        <> "="
        <> registerInitialCanonicalName resolvedInitial
    )

transitionSegment :: TypeGraph -> Spec -> Aggregate -> Transition -> Either FoldSurfaceError Text
transitionSegment graph spec aggregate transition = do
  outputOwnershipSegment <- transitionOutputSegments graph spec aggregate transition
  resolvedWrites <-
    traverse
      ( \(register, expression) ->
          fmap
            (register,)
            ( mapLeft
                (FoldWriteResolutionFailed aggregate.name register . showText)
                (resolveWriteExpr (expressionEnvironmentFromGraph graph spec aggregate transition) register expression)
            )
      )
      [(register, expression) | (register, expression) <- transition.writes, case expression of EWriteInitial {} -> True; EWriteMapped {} -> True; _ -> False]
  let lifts =
        [ [register, name, liftSourceName lift.source, unQualifiedValueName lift.function, unLiftVersion lift.version]
        | (register, TypedScalarExpr {node = TypedMappedLift name lift _}) <- resolvedWrites
        ]
      liftSegment = ["mapped-write-lifts=" <> TextEncoding.decodeUtf8 (LazyBytes.toStrict (Aeson.encode lifts)) | not (null lifts)]
  pure
    ( T.intercalate
        "|"
        ( [ "transition:" <> renderMode ((.mode) transition),
            (.source) transition,
            (.command) transition
          ]
            ++ implementationSegment
            ++ [ "guard=" <> maybe "" canonicalExpr ((.guard) transition),
                 "writes=" <> T.intercalate ";" (map renderWrite ((.writes) transition)),
                 "emits=" <> T.intercalate "," ((.emits) transition)
               ]
            ++ outputOwnershipSegment
            ++ ["goto=" <> (.goto) transition]
            ++ liftSegment
        )
    )
  where
    renderWrite (registerName, expression) = registerName <> ":=" <> canonicalExpr expression
    implementationSegment = case (.implementation) transition of
      LegacyHoleImplementation -> []
      GeneratedImplementation -> ["implementation=generated"]
      HoleImplementation -> ["implementation=hole"]

-- Shared by the frozen renderer and comparison evidence. No second output resolver.
transitionOutputSegments :: TypeGraph -> Spec -> Aggregate -> Transition -> Either FoldSurfaceError [Text]
transitionOutputSegments graph spec aggregate transition = case (.implementation) transition of
  LegacyHoleImplementation -> Right []
  GeneratedImplementation -> renderOutputs
  HoleImplementation -> renderOutputs
  where
    renderOutputs = fmap (pure . ("outputs=" <>) . T.intercalate "," . map (\(name, mapping) -> name <> "=" <> eventOutputCanonical mapping)) (transitionOutputs graph spec aggregate transition)

-- Resolve against local declarations. The persisted renderer retains the
-- complete obligation key. Comparison uses ownership without its guard-derived
-- provenance: guard coverage is already compared by the replay-body analysis.
transitionOutputs :: TypeGraph -> Spec -> Aggregate -> Transition -> Either FoldSurfaceError [(Name, EventOutputMapping)]
transitionOutputs graph spec aggregate transition = case transition.implementation of
  LegacyHoleImplementation -> Right []
  _ -> traverse (uncurry output) (zip [1 ..] transition.emits)
  where
    output emitIndex eventName = do
      mapping <-
        mapLeft
          (FoldEventOutputResolutionFailed ((.name) aggregate) ((.command) transition) eventName . showText)
          (eventOutputMappingFromGraph graph spec aggregate transition emitIndex eventName)
      pure (eventName, mapping)

renderMode :: TransitionMode -> Text
renderMode TmLive = "live"
renderMode TmReplayOnly = "replay-only"

ruleSegment :: RuleDecl -> Text
ruleSegment rule =
  T.intercalate
    "|"
    [ "rule:" <> (.name) rule,
      (.domain) rule,
      (.codomain) rule,
      "cases=" <> T.intercalate ";" (map renderCase ((.cases) rule))
    ]
  where
    renderCase (constructorName, expression) = constructorName <> "=>" <> canonicalExpr expression

referencedRuleNames :: Spec -> [Transition] -> Set Name
referencedRuleNames spec transitions = close directNames
  where
    rules = (.rules) spec
    directNames =
      Set.unions
        [ exprNames expression
        | transition <- transitions,
          expression <- maybeToList ((.guard) transition) ++ map snd ((.writes) transition)
        ]
    close names =
      let expanded =
            Set.unions
              ( names
                  : [ Set.unions (map (exprNames . snd) ((.cases) rule))
                    | name <- Set.toList names,
                      Just rule <- [find ((== name) . (.name)) rules]
                    ]
              )
       in if expanded == names then names else close expanded

exprNames :: Expr -> Set Name
exprNames = \case
  EOr left right -> exprNames left <> exprNames right
  EAnd left right -> exprNames left <> exprNames right
  ECmp _ left right -> exprNames left <> exprNames right
  EAdd _ left right -> exprNames left <> exprNames right
  ESubtract _ left right -> exprNames left <> exprNames right
  EMultiply _ left right -> exprNames left <> exprNames right
  EPath _ _ (name : _) -> Set.singleton name
  EPath _ _ [] -> Set.empty
  EWriteMapped _ child -> exprNames child
  EWriteInitial {} -> Set.empty
  ELiteral {} -> Set.empty
  EAtom (AName name) -> Set.singleton name
  EAtom (ABool _) -> Set.empty

maybeToList :: Maybe a -> [a]
maybeToList Nothing = []
maybeToList (Just value) = [value]

mapLeft :: (errorValue -> otherError) -> Either errorValue value -> Either otherError value
mapLeft convert = either (Left . convert) Right

showText :: (Show value) => value -> Text
showText = T.pack . show